diff --git a/README.md b/README.md index c5d816ec..f329430e 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ for upcoming, planned features. 8. [Validation plan](#validation-plan) 9. [Properties path inference](#properties-path-inference) 10. [Testing helpers](#testing-helpers) + 11. [Minimal dependencies](#dependency-policy) 4. [Rationale](#rationale) 1. [Reflection](#reflection) 2. [Trivia](#trivia) @@ -731,6 +732,18 @@ writing unit tests for validation rules defined with govy. Checkout [testable examples](https://pkg.go.dev/github.com/nobl9/govy/pkg/govytest#pkg-examples) for a concise overview of the package's capabilities. +### Dependency policy + +Govy **strives** to be dependency free. +We'd rather implement the required code ourselves then burden your projects +with bloated dependency tree. +Doing otherwise is in our honest opinion lazy and not secure! + +The only external dependencies you will inherit from govy are the modules +officially maintained by the Go team under [golang.org/x](https://pkg.go.dev/golang.org/x). +Here, the trade-off is in favor of the external source, +which is high quality and under constant security scrutiny and maintenance. + ## Rationale Why was this library created? diff --git a/cspell.json b/cspell.json index 27b27b5f..093ba513 100644 --- a/cspell.json +++ b/cspell.json @@ -13,6 +13,8 @@ ], "ignoreRegExpList": [ "/\\|\\s+[T|G]{2,}\\s+\\|/g", + "/\\b[13mn2][0-9A-HJ-NP-Za-km-z]{25,34}\\b/g", + "/\\b(?:bc|tb)1[ac-hj-np-z02-9]{11,71}\\b/gi", "/https?:\\/\\/.*/g", "/github.*/g", "/nolint.*/g", @@ -42,6 +44,8 @@ "UUIDRFC", "afbf", "atoi", + "bech", + "bips", "cidrv", "devbox", "edebfe", @@ -61,6 +65,7 @@ "govyconfig", "govytest", "issn", + "keccak", "ldflags", "Leonean", "luhn", @@ -70,18 +75,23 @@ "println", "rtmp", "renminbi", + "segwit", "sloctl", "strconv", "structs", "tmpl", "tpng", + "unchecksummed", "uuidv", "vuln", "vulns", + "wpkh", "Implicits", "mygovy", "unpadded", "JWSB64", - "ITIN" + "ITIN", + "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz", + "qpzry9x8gf2tvdw0s3jn54khce6mua7l" ] } diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 7c1d10ab..344bb926 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -71,6 +71,10 @@ Refer to this [README.md](../internal/validation/README.md) for more information ## Dependencies +The only acceptable dependencies are the +[golang.org/x](https://pkg.go.dev/golang.org/x) modules. +Any other third-party dependency is to be rejected. + Renovate is configured to automatically merge minor and patch updates. For major versions, which sadly includes GitHub Actions, manual approval is required. diff --git a/go.mod b/go.mod index 42f4b8d8..9c7532bf 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/nobl9/govy go 1.25.5 require ( + golang.org/x/crypto v0.54.0 golang.org/x/text v0.40.0 golang.org/x/tools v0.48.0 ) @@ -10,4 +11,5 @@ require ( require ( golang.org/x/mod v0.38.0 // indirect golang.org/x/sync v0.22.0 // indirect + golang.org/x/sys v0.47.0 // indirect ) diff --git a/go.sum b/go.sum index 65f8ef7e..c01a6a3a 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,13 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= +golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE= diff --git a/internal/messagetemplates/templatekey_string.go b/internal/messagetemplates/templatekey_string.go index 1f7eb616..13fc9c54 100644 --- a/internal/messagetemplates/templatekey_string.go +++ b/internal/messagetemplates/templatekey_string.go @@ -97,15 +97,18 @@ func _() { _ = x[StringLatitudeTemplate-87] _ = x[StringLongitudeTemplate-88] _ = x[StringKubernetesQualifiedNameTemplate-89] - _ = x[StringMongoDBObjectIDTemplate-90] - _ = x[URLTemplate-91] - _ = x[SliceUniqueTemplate-92] - _ = x[UniquePropertiesTemplate-93] + _ = x[StringBTCAddressTemplate-90] + _ = x[StringBTCBech32AddressTemplate-91] + _ = x[StringETHAddressTemplate-92] + _ = x[StringMongoDBObjectIDTemplate-93] + _ = x[URLTemplate-94] + _ = x[SliceUniqueTemplate-95] + _ = x[UniquePropertiesTemplate-96] } -const _templateKey_name = "LengthTemplateMinLengthTemplateMaxLengthTemplateEQTemplateNEQTemplateGTTemplateGTETemplateLTTemplateLTETemplateEqualPropertiesTemplateGTPropertiesTemplateGTEPropertiesTemplateLTPropertiesTemplateLTEPropertiesTemplateGTComparablePropertiesTemplateGTEComparablePropertiesTemplateLTComparablePropertiesTemplateLTEComparablePropertiesTemplateDurationPrecisionTemplateForbiddenTemplateOneOfTemplateNotOneOfTemplateOneOfPropertiesTemplateMutuallyExclusiveTemplateMutuallyDependentTemplateRequiredTemplateStringNonEmptyTemplateStringMatchRegexpTemplateStringDenyRegexpTemplateStringBase64TemplateStringBase64URLTemplateStringBase64RawURLTemplateStringHexadecimalTemplateStringMD5TemplateStringSHA256TemplateStringSHA384TemplateStringSHA512TemplateStringEmailTemplateStringE164TemplateStringMACTemplateStringIPTemplateStringIPv4TemplateStringIPv6TemplateStringCIDRTemplateStringCIDRv4TemplateStringCIDRv6TemplateStringCreditCardTemplateStringLuhnChecksumTemplateStringBICTemplateStringBICISO93622014TemplateStringUUIDRFC4122TemplateStringUUIDv3TemplateStringUUIDv4TemplateStringUUIDv5TemplateStringULIDTemplateStringEINTemplateStringSSNTemplateStringJSONTemplateStringSemverTemplateStringCVETemplateStringISBNTemplateStringISBN10TemplateStringISBN13TemplateStringISSNTemplateStringJWTTemplateStringContainsTemplateStringExcludesTemplateStringStartsWithTemplateStringEndsWithTemplateStringTitleTemplateStringGitRefTemplateStringFileSystemPathTemplateStringFilePathTemplateStringDirPathTemplateStringMatchFileSystemPathTemplateStringRegexpTemplateStringCrontabTemplateStringDateTimeTemplateStringTimeZoneTemplateStringBCP47LanguageTagTemplateStringBCP47StrictLanguageTagTemplateStringISO3166Alpha2TemplateStringISO3166Alpha3TemplateStringISO3166NumericTemplateStringISO31662TemplateStringISO4217TemplateStringLatitudeTemplateStringLongitudeTemplateStringKubernetesQualifiedNameTemplateStringMongoDBObjectIDTemplateURLTemplateSliceUniqueTemplateUniquePropertiesTemplate" +const _templateKey_name = "LengthTemplateMinLengthTemplateMaxLengthTemplateEQTemplateNEQTemplateGTTemplateGTETemplateLTTemplateLTETemplateEqualPropertiesTemplateGTPropertiesTemplateGTEPropertiesTemplateLTPropertiesTemplateLTEPropertiesTemplateGTComparablePropertiesTemplateGTEComparablePropertiesTemplateLTComparablePropertiesTemplateLTEComparablePropertiesTemplateDurationPrecisionTemplateForbiddenTemplateOneOfTemplateNotOneOfTemplateOneOfPropertiesTemplateMutuallyExclusiveTemplateMutuallyDependentTemplateRequiredTemplateStringNonEmptyTemplateStringMatchRegexpTemplateStringDenyRegexpTemplateStringBase64TemplateStringBase64URLTemplateStringBase64RawURLTemplateStringHexadecimalTemplateStringMD5TemplateStringSHA256TemplateStringSHA384TemplateStringSHA512TemplateStringEmailTemplateStringE164TemplateStringMACTemplateStringIPTemplateStringIPv4TemplateStringIPv6TemplateStringCIDRTemplateStringCIDRv4TemplateStringCIDRv6TemplateStringCreditCardTemplateStringLuhnChecksumTemplateStringBICTemplateStringBICISO93622014TemplateStringUUIDRFC4122TemplateStringUUIDv3TemplateStringUUIDv4TemplateStringUUIDv5TemplateStringULIDTemplateStringEINTemplateStringSSNTemplateStringJSONTemplateStringSemverTemplateStringCVETemplateStringISBNTemplateStringISBN10TemplateStringISBN13TemplateStringISSNTemplateStringJWTTemplateStringContainsTemplateStringExcludesTemplateStringStartsWithTemplateStringEndsWithTemplateStringTitleTemplateStringGitRefTemplateStringFileSystemPathTemplateStringFilePathTemplateStringDirPathTemplateStringMatchFileSystemPathTemplateStringRegexpTemplateStringCrontabTemplateStringDateTimeTemplateStringTimeZoneTemplateStringBCP47LanguageTagTemplateStringBCP47StrictLanguageTagTemplateStringISO3166Alpha2TemplateStringISO3166Alpha3TemplateStringISO3166NumericTemplateStringISO31662TemplateStringISO4217TemplateStringLatitudeTemplateStringLongitudeTemplateStringKubernetesQualifiedNameTemplateStringBTCAddressTemplateStringBTCBech32AddressTemplateStringETHAddressTemplateStringMongoDBObjectIDTemplateURLTemplateSliceUniqueTemplateUniquePropertiesTemplate" -var _templateKey_index = [...]uint16{0, 14, 31, 48, 58, 69, 79, 90, 100, 111, 134, 154, 175, 195, 216, 246, 277, 307, 338, 363, 380, 393, 409, 432, 457, 482, 498, 520, 545, 569, 589, 612, 638, 663, 680, 700, 720, 740, 759, 777, 794, 810, 828, 846, 864, 884, 904, 928, 954, 971, 999, 1024, 1044, 1064, 1084, 1102, 1119, 1136, 1154, 1174, 1191, 1209, 1229, 1249, 1267, 1284, 1306, 1328, 1352, 1374, 1393, 1413, 1441, 1463, 1484, 1517, 1537, 1558, 1580, 1602, 1632, 1668, 1695, 1722, 1750, 1772, 1793, 1815, 1838, 1875, 1904, 1915, 1934, 1958} +var _templateKey_index = [...]uint16{0, 14, 31, 48, 58, 69, 79, 90, 100, 111, 134, 154, 175, 195, 216, 246, 277, 307, 338, 363, 380, 393, 409, 432, 457, 482, 498, 520, 545, 569, 589, 612, 638, 663, 680, 700, 720, 740, 759, 777, 794, 810, 828, 846, 864, 884, 904, 928, 954, 971, 999, 1024, 1044, 1064, 1084, 1102, 1119, 1136, 1154, 1174, 1191, 1209, 1229, 1249, 1267, 1284, 1306, 1328, 1352, 1374, 1393, 1413, 1441, 1463, 1484, 1517, 1537, 1558, 1580, 1602, 1632, 1668, 1695, 1722, 1750, 1772, 1793, 1815, 1838, 1875, 1899, 1929, 1953, 1982, 1993, 2012, 2036} func (i templateKey) String() string { idx := int(i) - 1 diff --git a/internal/messagetemplates/templates.go b/internal/messagetemplates/templates.go index c22eff9e..9428a486 100644 --- a/internal/messagetemplates/templates.go +++ b/internal/messagetemplates/templates.go @@ -102,6 +102,9 @@ const ( StringLatitudeTemplate StringLongitudeTemplate StringKubernetesQualifiedNameTemplate + StringBTCAddressTemplate + StringBTCBech32AddressTemplate + StringETHAddressTemplate StringMongoDBObjectIDTemplate URLTemplate SliceUniqueTemplate @@ -256,7 +259,10 @@ var rawMessageTemplates = map[templateKey]string{ string must be a valid Kubernetes Qualified Name {{- end }} `, - StringMongoDBObjectIDTemplate: "string must be a 24-character hexadecimal MongoDB ObjectID", + StringBTCAddressTemplate: "string must be a valid mainnet legacy Bitcoin Base58Check address", + StringBTCBech32AddressTemplate: "string must be a valid mainnet Bitcoin Bech32 address", + StringETHAddressTemplate: "string must be a valid Ethereum address (0x plus 40 hexadecimal characters)", + StringMongoDBObjectIDTemplate: "string must be a 24-character hexadecimal MongoDB ObjectID", SliceUniqueTemplate: `elements are not unique, {{ .Custom.FirstOrdinal }} and {{ .Custom.SecondOrdinal }} elements collide {{- if gt (len .Custom.Constraints) 0 }} based on constraints: {{ joinSlice .Custom.Constraints "" }}{{- end }}`, UniquePropertiesTemplate: `all of [{{ joinSlice .ComparisonValue "" }}] properties must be unique, but '{{ .Custom.FirstProperty }}' collides with '{{ .Custom.SecondProperty }}' diff --git a/pkg/rules/error_codes.go b/pkg/rules/error_codes.go index 829c5066..ba92021a 100644 --- a/pkg/rules/error_codes.go +++ b/pkg/rules/error_codes.go @@ -90,6 +90,9 @@ const ( ErrorCodeStringAlphanumericUnicode govy.ErrorCode = "string_alphanumeric_unicode" ErrorCodeStringFQDN govy.ErrorCode = "string_fqdn" ErrorCodeStringKubernetesQualifiedName govy.ErrorCode = "string_kubernetes_qualified_name" + ErrorCodeStringBTCAddress govy.ErrorCode = "string_btc_address" + ErrorCodeStringBTCBech32Address govy.ErrorCode = "string_btc_bech32_address" + ErrorCodeStringETHAddress govy.ErrorCode = "string_eth_address" ErrorCodeStringMongoDBObjectID govy.ErrorCode = "string_mongodb_object_id" ErrorCodeSliceLength govy.ErrorCode = "slice_length" ErrorCodeSliceMinLength govy.ErrorCode = "slice_min_length" diff --git a/pkg/rules/regex.go b/pkg/rules/regex.go index 30ea77d3..d6c67070 100644 --- a/pkg/rules/regex.go +++ b/pkg/rules/regex.go @@ -29,6 +29,7 @@ var ( fqdnRegexp = lazyRegexCompile( `^([a-zA-Z0-9]{1}[a-zA-Z0-9-]{0,62})(\.[a-zA-Z0-9]{1}[a-zA-Z0-9-]{0,62})*?(\.[a-zA-Z]{1}[a-zA-Z0-9]{0,62})\.?$`, ) + btcAddressRegexp = lazyRegexCompile(`^[13][1-9A-HJ-NP-Za-km-z]{25,34}$`) ) // lazyRegexCompile returns a function that compiles the regular expression diff --git a/pkg/rules/string_format_crypto.go b/pkg/rules/string_format_crypto.go new file mode 100644 index 00000000..0568b1d4 --- /dev/null +++ b/pkg/rules/string_format_crypto.go @@ -0,0 +1,293 @@ +package rules + +import ( + "crypto/sha256" + + "golang.org/x/crypto/sha3" + + "github.com/nobl9/govy/internal/messagetemplates" + "github.com/nobl9/govy/pkg/govy" +) + +const ( + bitcoinBase58Alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" + bech32Charset = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" + + btcP2PKHVersion byte = 0x00 + btcP2SHVersion byte = 0x05 +) + +var ( + bitcoinBase58Values = newASCIIValueTable(bitcoinBase58Alphabet) + bech32CharsetValues = newASCIIValueTable(bech32Charset) +) + +// StringBTCAddress ensures the property's value is a mainnet legacy Bitcoin +// Base58Check address. +// It validates the Base58Check checksum and accepts pay-to-public-key-hash +// (P2PKH) and pay-to-script-hash (P2SH) version bytes. +func StringBTCAddress() govy.Rule[string] { + tpl := messagetemplates.Get(messagetemplates.StringBTCAddressTemplate) + + return govy.NewRule(func(s string) error { + if !isBTCAddress(s) { + return govy.NewRuleErrorTemplate(govy.TemplateVars{ + PropertyValue: s, + }) + } + return nil + }). + WithErrorCode(ErrorCodeStringBTCAddress). + WithMessageTemplate(tpl). + WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{})) +} + +// StringBTCBech32Address ensures the property's value is a mainnet Bitcoin +// Bech32 address as defined by Bitcoin Improvement Proposal 173 (BIP-173). +// It validates the Bech32 checksum and accepts native version 0 Segregated +// Witness (SegWit) addresses. +// It does not accept Bech32m addresses such as Taproot addresses. +func StringBTCBech32Address() govy.Rule[string] { + tpl := messagetemplates.Get(messagetemplates.StringBTCBech32AddressTemplate) + + return govy.NewRule(func(s string) error { + if !isBTCBech32Address(s) { + return govy.NewRuleErrorTemplate(govy.TemplateVars{ + PropertyValue: s, + }) + } + return nil + }). + WithErrorCode(ErrorCodeStringBTCBech32Address). + WithMessageTemplate(tpl). + WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{})) +} + +// StringETHAddress ensures the property's value is an Ethereum address with a +// 0x prefix followed by 40 hexadecimal characters. +// Mixed-case addresses must satisfy the Ethereum Improvement Proposal 55 +// (EIP-55) checksum. +// All-lowercase and all-uppercase payloads are accepted as unchecksummed +// addresses. +func StringETHAddress() govy.Rule[string] { + tpl := messagetemplates.Get(messagetemplates.StringETHAddressTemplate) + + return govy.NewRule(func(s string) error { + if !isETHAddress(s) { + return govy.NewRuleErrorTemplate(govy.TemplateVars{ + PropertyValue: s, + }) + } + return nil + }). + WithErrorCode(ErrorCodeStringETHAddress). + WithMessageTemplate(tpl). + WithDescription(mustExecuteTemplate(tpl, govy.TemplateVars{})) +} + +func isBTCAddress(s string) bool { + if !btcAddressRegexp().MatchString(s) { + return false + } + + decoded, ok := decodeBitcoinBase58(s) + if !ok { + return false + } + if decoded[0] != btcP2PKHVersion && decoded[0] != btcP2SHVersion { + return false + } + + checksum := bitcoinChecksum(decoded[:21]) + return checksum[0] == decoded[21] && + checksum[1] == decoded[22] && + checksum[2] == decoded[23] && + checksum[3] == decoded[24] +} + +func isETHAddress(s string) bool { + if len(s) != 42 || s[0] != '0' || s[1] != 'x' { + return false + } + + payload := s[len("0x"):] + var lowercase [40]byte + var hasLower, hasUpper bool + for i := range len(payload) { + switch b := payload[i]; { + case b >= '0' && b <= '9': + lowercase[i] = b + case b >= 'a' && b <= 'f': + lowercase[i] = b + hasLower = true + case b >= 'A' && b <= 'F': + lowercase[i] = b + ('a' - 'A') + hasUpper = true + default: + return false + } + } + if !hasLower || !hasUpper { + return true + } + return hasValidETHChecksum(payload, lowercase) +} + +func hasValidETHChecksum(payload string, lowercase [40]byte) bool { + h := sha3.NewLegacyKeccak256() + _, _ = h.Write(lowercase[:]) + var hash [32]byte + digest := h.Sum(hash[:0]) + + for i := range len(payload) { + b := payload[i] + if b >= '0' && b <= '9' { + continue + } + + uppercase := ethChecksumNibble(digest, i) >= 8 + if uppercase != (b >= 'A' && b <= 'F') { + return false + } + } + return true +} + +func ethChecksumNibble(hash []byte, index int) byte { + if index%2 == 0 { + return hash[index/2] >> 4 + } + return hash[index/2] & 0x0f +} + +func decodeBitcoinBase58(s string) ([25]byte, bool) { + var decoded [25]byte + for i := range len(s) { + if s[i] >= byte(len(bitcoinBase58Values)) { + return decoded, false + } + + value := bitcoinBase58Values[s[i]] + if value == 0 { + return decoded, false + } + + carry := uint32(value - 1) + for j := len(decoded) - 1; j >= 0; j-- { + carry += uint32(decoded[j]) * 58 + decoded[j] = byte(carry & 0xff) //nolint:gosec // The mask bounds the conversion. + carry >>= 8 + } + if carry != 0 { + return decoded, false + } + } + + leadingZeroes := 0 + for leadingZeroes < len(s) && s[leadingZeroes] == '1' { + leadingZeroes++ + } + firstNonZero := 0 + for firstNonZero < len(decoded) && decoded[firstNonZero] == 0 { + firstNonZero++ + } + if leadingZeroes+len(decoded)-firstNonZero != len(decoded) { + return decoded, false + } + return decoded, true +} + +func bitcoinChecksum(payload []byte) [4]byte { + first := sha256.Sum256(payload) + second := sha256.Sum256(first[:]) + return [4]byte{second[0], second[1], second[2], second[3]} +} + +func isBTCBech32Address(s string) bool { + if len(s) != 42 && len(s) != 62 { + return false + } + if s[0]|('a'-'A') != 'b' || s[1]|('a'-'A') != 'c' || s[2] != '1' { + return false + } + if s[3]|('a'-'A') != 'q' { + return false + } + + hasLower := s[0] == 'b' || s[1] == 'c' + hasUpper := s[0] == 'B' || s[1] == 'C' + if hasLower && hasUpper { + return false + } + + checksum := uint32(1) + checksum = bech32PolymodStep(checksum, 'b'>>5) + checksum = bech32PolymodStep(checksum, 'c'>>5) + checksum = bech32PolymodStep(checksum, 0) + checksum = bech32PolymodStep(checksum, 'b'&31) + checksum = bech32PolymodStep(checksum, 'c'&31) + + for i := len("bc1"); i < len(s); i++ { + b := s[i] + switch { + case b >= 'a' && b <= 'z': + hasLower = true + case b >= 'A' && b <= 'Z': + hasUpper = true + b += 'a' - 'A' + } + if hasLower && hasUpper { + return false + } + if b >= byte(len(bech32CharsetValues)) { + return false + } + + encoded := bech32CharsetValues[b] + if encoded == 0 { + return false + } + value := encoded - 1 + if len(s) == 62 && i == len(s)-7 && value&0x0f != 0 { + return false + } + checksum = bech32PolymodStep(checksum, value) + } + return checksum == 1 +} + +func bech32PolymodStep(checksum uint32, value byte) uint32 { + top := checksum >> 25 + checksum = (checksum&0x1ffffff)<<5 ^ uint32(value) + if top&1 != 0 { + checksum ^= 0x3b6a57b2 + } + if top&2 != 0 { + checksum ^= 0x26508e6d + } + if top&4 != 0 { + checksum ^= 0x1ea119fa + } + if top&8 != 0 { + checksum ^= 0x3d4233dd + } + if top&16 != 0 { + checksum ^= 0x2a1462b3 + } + return checksum +} + +func newASCIIValueTable(alphabet string) [128]byte { + var values [128]byte + for i := range len(alphabet) { + values[alphabet[i]] = byteFromInt(i + 1) + } + return values +} + +func byteFromInt(n int) byte { + if n < 0 || n > 255 { + panic("integer out of byte range") + } + return byte(n) +} diff --git a/pkg/rules/string_format_crypto_test.go b/pkg/rules/string_format_crypto_test.go new file mode 100644 index 00000000..e0c2cc4f --- /dev/null +++ b/pkg/rules/string_format_crypto_test.go @@ -0,0 +1,749 @@ +package rules + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/nobl9/govy/internal/assert" + "github.com/nobl9/govy/pkg/govy" +) + +// cspell:disable +var validBTCAddressTestCases = map[string]string{ + // Fourteen historical regressions; not attributed to the pinned Bitcoin Core source. + "p2pkh 1AGNa": "1AGNa15ZQXAZUgFiqJ2i7Z2DPU2J6hW62i", + "p2sh script hash 74f209": "3CMNFxN1oHBc4R1EpboAL5yzHGgE611Xou", + "p2pkh 1Ax4g": "1Ax4gZtb7gAit2TivwejZHYtNNLT18PUXJ", + "p2sh 3QjYX": "3QjYXhTkvuj8qPaXHTTWb5wjXhdsLAAWVy", + "p2pkh 1C5bS": "1C5bSj1iEGUgSTbziymG7Cn18ENQuT36vv", + "p2sh 3AnNx": "3AnNxabYGoTxYiTEZwFEnerUoeFXK2Zoks", + "p2pkh 1Gqk4": "1Gqk4Tv79P91Cc1STQtU3s1W6277M2CVWu", + "p2sh 33vt8": "33vt8ViH5jsr115AGkW6cEmEz9MpvJSwDk", + "p2pkh 1JwMW": "1JwMWBVLtiqtscbaRHai4pqHokhFCbtoB4", + "p2sh 3QCzv": "3QCzvfL4ZRvmJFiWWBVwxfdaNBT8EtxB5y", + "p2pkh 19dca": "19dcawoKcZdQz365WpXWMhX6QCUpR9SY4r", + "p2sh 37Sp6": "37Sp6Rv3y4kVd1nQ1JV5pfqXccHNyZm1x3", + "p2pkh 13p1i": "13p1ijLwsnrcuyqcTvJXkq2ASdXqcnEBLE", + "p2sh script hash 5ece0c": "3ALJH9Y951VCGcVZYAdpA3KchoP9McEj1G", + // Common examples and locally constructed boundary cases. + "p2pkh commonly used": "1BoatSLRHtKNngkdXEeobR76b53LETtpyT", + "p2sh commonly used": "3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy", + "p2pkh zero payload": "1111111111111111111114oLvT2", + "p2pkh maximum payload": "1QLbz7JHiBTspS962RLKV8GndWFwi5j6Qr", + "p2sh zero payload": "31h1vYVSYuKP6AhS86fbRdMw9XHieotbST", + "p2sh maximum payload": "3R2cuenjG5nFubqX9Wzuukdin2YfBbQ6Kw", +} + +var invalidBTCAddressTestCases = map[string]string{ + "empty": "", + "p2pkh checksum mutation": "1BoatSLRHtKNngkdXEeobR76b53LETtpyU", + "p2sh checksum mutation": "3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLz", + "testnet p2pkh reference": "mipcBbFg9gMiCh81Kj8tqqdgoZub1ZJRfn", + "testnet p2pkh mo9n": "mo9ncXisMeAoXwqcV5EWuyncbmCcQN4rVs", + "testnet p2sh": "2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br", + "unsupported version": "3R2cuenjG5nFubqX9Wzuukdin2YfLYZyD1", + "19 byte payload": "111111111111111111117K4nzc", + "21 byte payload": "11111111111111111111116iowaD", + "maximum textual length": "11111111111111111111111111111111111", + "forbidden digit zero": "1BoatSLRHtKNngkdXEeobR76b53LETtpy0", + "forbidden uppercase O": "1BoatSLRHtKNngkdXEeobR76b53LETtpyO", + "forbidden uppercase I": "1BoatSLRHtKNngkdXEeobR76b53LETtpyI", + "forbidden lowercase l": "1BoatSLRHtKNngkdXEeobR76b53LETtpyl", + "bech32 address": "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4", + "leading space": " 1BoatSLRHtKNngkdXEeobR76b53LETtpyT", + "trailing space": "1BoatSLRHtKNngkdXEeobR76b53LETtpyT ", + "trailing newline": "1BoatSLRHtKNngkdXEeobR76b53LETtpyT\n", + "Cyrillic A confusable": "1\u0410GNa15ZQXAZUgFiqJ2i7Z2DPU2J6hW62i", +} + +func TestStringBTCAddress(t *testing.T) { + rule := StringBTCAddress() + t.Run("valid addresses", func(t *testing.T) { + for name, address := range validBTCAddressTestCases { + t.Run(name, func(t *testing.T) { + assert.NoError(t, rule.Validate(address)) + }) + } + }) + t.Run("invalid addresses", func(t *testing.T) { + for name, address := range invalidBTCAddressTestCases { + t.Run(name, func(t *testing.T) { + err := rule.Validate(address) + assert.EqualError(t, err, "string must be a valid mainnet legacy Bitcoin Base58Check address") + assert.True(t, govy.HasErrorCode(err, ErrorCodeStringBTCAddress)) + }) + } + }) +} + +func BenchmarkStringBTCAddress(b *testing.B) { + benchmarkStringFormatRule( + b, + StringBTCAddress(), + validBTCAddressTestCases, + invalidBTCAddressTestCases, + ) +} + +var validBTCBech32AddressTestCases = map[string]string{ + "bip173 p2wpkh": "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4", + "bip173 p2wsh": "bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3", + "bip173 uppercase p2wpkh": "BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4", + "bitcoin core p2wpkh vyq0": "bc1qvyq0cc6rahyvsazfdje0twl7ez82ndmuac2lhv", + "bitcoin core p2wsh reference": "bc1qyucykdlhp62tezs0hagqury402qwhk589q80tqs5myh3rxq34nwqhkdhv7", + "bitcoin core p2wpkh hxt0": "bc1qhxt04s5xnpy0kxw4x99n5hpdf5pmtzpqs52es2", + "bitcoin core p2wsh gc9l": "bc1qgc9ljrvdf2e0zg9rmmq86xklqwfys7r6wptjlacdgrcdc7sa6ggqu4rrxf", + "bitcoin core uppercase p2wsh": "BC1QRP33G0Q5C5TXSP9ARYSRX4K6ZDKFS4NCE4XJ0GDCCCEFVPYSXF3QCCFMV3", +} + +var invalidBTCBech32AddressTestCases = map[string]string{ + "empty": "", + "checksum mutation": "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t5", + "testnet hrp": "tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx", + "mixed case": "bc1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4", + "bip173 witness v1": "bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7k7grplx", + "bip350 witness v1": "bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqzk5jj0", + "unknown hrp": "tc1qw508d6qejxtdg4y5r3zarvary0c5xw7kg3g4ty", + "invalid witness version": "BC13W508D6QEJXTDG4Y5R3ZARVARY0C5XW7KN40WF2", + "short witness program": "bc1qqqqsyydq4q", + "long witness program": "bc1qqqqsyqcyq5rqwzqfpg9scrgwpugpzysnzs23v9ccrydpk8qarc0jqwad46q", + "invalid witness v0 program length": "BC1QR508D6QEJXTDG4Y5R3ZARVARYV98GJ9P", + "excess zero padding": "bc1qw508d6qejxtdg4y5r3zarvaryvqkyqvzl", + "nonzero padding": "bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3p9waw3r", + "even nonzero padding 0x02": "bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3ztagclu", + "even nonzero padding 0x04": "bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3yhjtart", + "even nonzero padding 0x06": "bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3xyh27sx", + "even nonzero padding 0x08": "bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3gxvdhjv", + "even nonzero padding 0x0a": "bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf324fv5pp", + "even nonzero padding 0x0c": "bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3vfx03ak", + "even nonzero padding 0x0e": "bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3w6rwjwm", + "empty data": "bc1gmk9yu", + "bech32m checksum for witness v0": "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kemeawh", + "leading space": " bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4", + "trailing space": "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 ", + "trailing newline": "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4\n", + "zero width space": "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4\u200b", +} + +func TestStringBTCBech32Address(t *testing.T) { + rule := StringBTCBech32Address() + t.Run("valid addresses", func(t *testing.T) { + for name, address := range validBTCBech32AddressTestCases { + t.Run(name, func(t *testing.T) { + assert.NoError(t, rule.Validate(address)) + }) + } + }) + t.Run("invalid addresses", func(t *testing.T) { + for name, address := range invalidBTCBech32AddressTestCases { + t.Run(name, func(t *testing.T) { + err := rule.Validate(address) + assert.EqualError(t, err, "string must be a valid mainnet Bitcoin Bech32 address") + assert.True(t, govy.HasErrorCode(err, ErrorCodeStringBTCBech32Address)) + }) + } + }) +} + +func BenchmarkStringBTCBech32Address(b *testing.B) { + benchmarkStringFormatRule( + b, + StringBTCBech32Address(), + validBTCBech32AddressTestCases, + invalidBTCBech32AddressTestCases, + ) +} + +var validETHAddressTestCases = map[string]string{ + "zero": "0x0000000000000000000000000000000000000000", + "all lowercase de709": "0xde709f2102306220921060314715629080e2fb77", + "all lowercase 27b1": "0x27b1fdb04752bbc536007a920d24acb045561c26", + "all uppercase 529084": "0x52908400098527886E0F7030069857D2E4169EE7", + "all uppercase 8617": "0x8617E340B3D01FA5F11F306F4090FD50E238070D", + "all uppercase DE709": "0xDE709F2102306220921060314715629080E2FB77", + "eip55 5aAeb": "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed", + "eip55 fB691": "0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359", + "eip55 dbF03": "0xdbF03B407c01E7cD3CBea99509d93f8DDDC8C6FB", + "eip55 D1220": "0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb", + "additional mixed case": "0x5AEDA56215b167893e80B4fE645BA6d5Bab767DE", +} + +var invalidETHAddressTestCases = map[string]string{ + "empty": "", + "eip55 529084 case mutation": "0x52908400098527886e0F7030069857D2E4169EE7", + "additional mixed case mutation": "0x5AEDA56215b167893e80B4fE645BA6d5Bab767De", + "eip55 5aAeb case mutation": "0x5AAeb6053F3E94C9b9A09f33669435E7Ef1BeAed", + "eip55 fB691 case mutation": "0xfb6916095ca1df60bB79Ce92cE3Ea74c37c5d359", + "eip55 dbF03 case mutation": "0xDbF03B407c01E7cD3CBea99509d93f8DDDC8C6FB", + "eip55 D1220 case mutation": "0xd1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb", + "missing prefix": "de709f2102306220921060314715629080e2fb77", + "uppercase prefix": "0Xde709f2102306220921060314715629080e2fb77", + "double prefix": "0x0xde709f2102306220921060314715629080e2fb77", + "short payload": "0xde709f2102306220921060314715629080e2fb7", + "nonhexadecimal character": "0xde709f2102306220921060314715629080e2fb7g", + "leading space": " 0xde709f2102306220921060314715629080e2fb77", + "trailing space": "0xde709f2102306220921060314715629080e2fb77 ", + "trailing newline": "0xde709f2102306220921060314715629080e2fb77\n", + "full width x prefix": "0\uff58de709f2102306220921060314715629080e2fb77", + "zero width space after prefix": "0x\u200bde709f2102306220921060314715629080e2fb77", +} + +// cspell:enable + +func TestStringETHAddress(t *testing.T) { + rule := StringETHAddress() + t.Run("valid addresses", func(t *testing.T) { + for name, address := range validETHAddressTestCases { + t.Run(name, func(t *testing.T) { + assert.NoError(t, rule.Validate(address)) + }) + } + }) + t.Run("invalid addresses", func(t *testing.T) { + for name, address := range invalidETHAddressTestCases { + t.Run(name, func(t *testing.T) { + err := rule.Validate(address) + assert.EqualError(t, err, "string must be a valid Ethereum address (0x plus 40 hexadecimal characters)") + assert.True(t, govy.HasErrorCode(err, ErrorCodeStringETHAddress)) + }) + } + }) +} + +func BenchmarkStringETHAddress(b *testing.B) { + benchmarkStringFormatRule( + b, + StringETHAddress(), + validETHAddressTestCases, + invalidETHAddressTestCases, + ) +} + +// The fixtures are exact copies of Bitcoin Core's key_io_valid.json and +// key_io_invalid.json at commit 006f8f7d49a39bc1e3a302269c8ad244f05a209b. +// Their SHA-256 hashes are asserted while loading so corpus changes require an +// explicit source-pin update. Private-key WIF rows are counted but excluded +// because [StringBTCAddress] and [StringBTCBech32Address] validate destinations. +func TestStringBTCAddressRules_BitcoinCoreKeyIO(t *testing.T) { + const ( + legacyErrorMessage = "string must be a valid mainnet legacy Bitcoin Base58Check address" + bech32ErrorMessage = "string must be a valid mainnet Bitcoin Bech32 address" + ) + + legacyRule := StringBTCAddress() + bech32Rule := StringBTCBech32Address() + valid := loadBitcoinCoreKeyIOValid(t) + partitions := map[string]int{"valid rows": len(valid)} + caseFlipped := make(map[string]struct{}, 32) + + for row, vector := range valid { + if vector.Metadata.IsPrivkey { + partitions["WIF excluded"]++ + if vector.Metadata.TryCaseFlip { + t.Errorf("valid fixture row %d: WIF unexpectedly requests case flip", row+1) + } + continue + } + + partitions["destination rows"]++ + kind, witnessVersion := classifyBitcoinCoreDestination(t, vector) + wantLegacy := kind == "legacy" && vector.Metadata.Chain == "main" + wantBech32 := kind == "segwit" && vector.Metadata.Chain == "main" && witnessVersion == 0 + assertStringFormatRuleResult( + t, + legacyRule, + vector.Input, + wantLegacy, + legacyErrorMessage, + ErrorCodeStringBTCAddress, + "valid fixture legacy boundary", + row+1, + ) + assertStringFormatRuleResult( + t, + bech32Rule, + vector.Input, + wantBech32, + bech32ErrorMessage, + ErrorCodeStringBTCBech32Address, + "valid fixture Bech32 boundary", + row+1, + ) + + switch kind { + case "legacy": + partitions["legacy destinations"]++ + if vector.Metadata.Chain == "main" { + partitions["legacy mainnet"]++ + } else { + partitions["legacy non-mainnet"]++ + } + case "segwit": + partitions["SegWit destinations"]++ + network := "non-mainnet" + if vector.Metadata.Chain == "main" { + network = "mainnet" + } + version := "v1+" + if witnessVersion == 0 { + version = "v0" + } + partitions["SegWit "+network+" "+version]++ + } + + if !vector.Metadata.TryCaseFlip { + continue + } + partitions["case flips"]++ + if strings.ToLower(vector.Input) != vector.Input { + t.Errorf("valid fixture row %d: tryCaseFlip source is not lowercase: %q", row+1, vector.Input) + } + uppercase := strings.ToUpper(vector.Input) + if _, exists := caseFlipped[uppercase]; exists { + t.Errorf("valid fixture row %d: duplicate uppercase mutation %q", row+1, uppercase) + } + caseFlipped[uppercase] = struct{}{} + assertStringFormatRuleResult( + t, + legacyRule, + uppercase, + wantLegacy, + legacyErrorMessage, + ErrorCodeStringBTCAddress, + "tryCaseFlip legacy boundary", + row+1, + ) + assertStringFormatRuleResult( + t, + bech32Rule, + uppercase, + wantBech32, + bech32ErrorMessage, + ErrorCodeStringBTCBech32Address, + "tryCaseFlip Bech32 boundary", + row+1, + ) + } + + expectedPartitions := map[string]int{ + "valid rows": 70, + "WIF excluded": 16, + "destination rows": 54, + "legacy destinations": 22, + "legacy mainnet": 6, + "legacy non-mainnet": 16, + "SegWit destinations": 32, + "SegWit mainnet v0": 4, + "SegWit mainnet v1+": 4, + "SegWit non-mainnet v0": 12, + "SegWit non-mainnet v1+": 12, + "case flips": 32, + } + for name, expected := range expectedPartitions { + if actual := partitions[name]; actual != expected { + t.Errorf("%s count is %d; want %d", name, actual, expected) + } + } + + invalid := loadBitcoinCoreKeyIOInvalid(t) + for row, input := range invalid { + assertStringFormatRuleResult( + t, + legacyRule, + input, + false, + legacyErrorMessage, + ErrorCodeStringBTCAddress, + "invalid fixture legacy boundary", + row+1, + ) + assertStringFormatRuleResult( + t, + bech32Rule, + input, + false, + bech32ErrorMessage, + ErrorCodeStringBTCBech32Address, + "invalid fixture Bech32 boundary", + row+1, + ) + } +} + +// The fixtures preserve every native SegWit vector from BIP-173 and BIP-350 +// at bitcoin/bips commit 8c369ac8e60629ac6c032ffe21bb5ec5b35213d7. +// Each exact source input is classified against [StringBTCBech32Address]'s +// intentionally narrower mainnet, witness-v0, Bech32-only contract. +func TestStringBTCBech32Address_BIPSegWitVectors(t *testing.T) { + const errorMessage = "string must be a valid mainnet Bitcoin Bech32 address" + rule := StringBTCBech32Address() + tests := map[string]struct { + fixture string + fixtureSHA256 string + sourceFile string + sourceFileSHA256 string + expectedValid int + expectedInvalid int + expectedPartitions map[string]int + }{ + "BIP-173": { + fixture: "bip173_segwit_vectors.json", + fixtureSHA256: "043e7295b39a985e519a720638712e3a6f1883cecb87936f60a136caf7a12c15", + sourceFile: "bip-0173.mediawiki", + sourceFileSHA256: "b19eab06b264bbd15ad7f7d640fa34de8f4a8809fba8a6ecc3e165545b27a543", + expectedValid: 6, + expectedInvalid: 10, + expectedPartitions: map[string]int{ + "source-valid": 6, + "source-invalid": 10, + "accepted": 1, + "non-mainnet": 2, + "v1+": 3, + "non-mainnet v1+": 0, + }, + }, + "BIP-350": { + fixture: "bip350_segwit_vectors.json", + fixtureSHA256: "1fcd84cc0388ab57c3371158bd59717ce66f853a9fb285497832cbd0fbb417f9", + sourceFile: "bip-0350.mediawiki", + sourceFileSHA256: "63634b06aa8bae88b31929674736e74964c4598684269ac2b0b140c43a7a0dec", + expectedValid: 8, + expectedInvalid: 15, + expectedPartitions: map[string]int{ + "source-valid": 8, + "source-invalid": 15, + "accepted": 1, + "non-mainnet": 2, + "v1+": 4, + "non-mainnet v1+": 1, + }, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + fixture := loadBIPSegWitFixture(t, tc.fixture, tc.fixtureSHA256) + if fixture.Source.Repository != "https://github.com/bitcoin/bips" { + t.Errorf("source repository is %q", fixture.Source.Repository) + } + if fixture.Source.Commit != "8c369ac8e60629ac6c032ffe21bb5ec5b35213d7" { + t.Errorf("source commit is %q", fixture.Source.Commit) + } + if fixture.Source.File != tc.sourceFile { + t.Errorf("source file is %q; want %q", fixture.Source.File, tc.sourceFile) + } + if fixture.Source.FileSHA256 != tc.sourceFileSHA256 { + t.Errorf("source file SHA-256 is %q; want %q", fixture.Source.FileSHA256, tc.sourceFileSHA256) + } + if fixture.Source.ValidCount != tc.expectedValid || fixture.Source.InvalidCount != tc.expectedInvalid { + t.Errorf( + "source counts are %d valid/%d invalid; want %d/%d", + fixture.Source.ValidCount, + fixture.Source.InvalidCount, + tc.expectedValid, + tc.expectedInvalid, + ) + } + + partitions := make(map[string]int, len(tc.expectedPartitions)) + for row, vector := range fixture.Vectors { + contract := classifyBIPSegWitVector(t, tc.fixture, row+1, vector) + if vector.SourceValid { + partitions["source-valid"]++ + partitions[contract]++ + } else { + partitions["source-invalid"]++ + } + assertStringFormatRuleResult( + t, + rule, + vector.Address, + contract == "accepted", + errorMessage, + ErrorCodeStringBTCBech32Address, + tc.fixture, + row+1, + ) + } + for partition, expected := range tc.expectedPartitions { + if actual := partitions[partition]; actual != expected { + t.Errorf("%s count is %d; want %d", partition, actual, expected) + } + } + }) + } +} + +func benchmarkStringFormatRule( + b *testing.B, + rule govy.Rule[string], + testCaseGroups ...map[string]string, +) { + b.Helper() + + for _, testCases := range testCaseGroups { + for name, address := range testCases { + b.Run(name, func(b *testing.B) { + for b.Loop() { + _ = rule.Validate(address) + } + }) + } + } +} + +type bitcoinCoreKeyIOValidMetadata struct { + Chain string `json:"chain"` + IsPrivkey bool `json:"isPrivkey"` + TryCaseFlip bool `json:"tryCaseFlip"` +} + +type bitcoinCoreKeyIOValidVector struct { + Input string + Script string + Metadata bitcoinCoreKeyIOValidMetadata +} + +type bipSegWitFixtureSource struct { + Repository string `json:"repository"` + Commit string `json:"commit"` + File string `json:"file"` + FileSHA256 string `json:"file_sha256"` + ValidCount int `json:"valid_count"` + InvalidCount int `json:"invalid_count"` +} + +type bipSegWitVector struct { + Address string `json:"address"` + SourceValid bool `json:"source_valid"` + ScriptPubKey string `json:"script_pub_key"` + Reason string `json:"reason"` + Network string `json:"network"` + WitnessVersion *int `json:"witness_version"` + Encoding string `json:"encoding"` + Contract string `json:"contract"` +} + +type bipSegWitFixture struct { + Source bipSegWitFixtureSource `json:"source"` + Vectors []bipSegWitVector `json:"vectors"` +} + +func loadBitcoinCoreKeyIOValid(t *testing.T) []bitcoinCoreKeyIOValidVector { + t.Helper() + data := readPinnedCryptoFixture( + t, + "bitcoin_core_key_io_valid.json", + "90bd1d35d12763e0d00c5400b2c9fe551e532a821e1e466c20cad3aced70a7fe", + ) + + var rows [][]json.RawMessage + if err := json.Unmarshal(data, &rows); err != nil { + t.Fatalf("decode Bitcoin Core valid fixture: %v", err) + } + if len(rows) != 70 { + t.Fatalf("Bitcoin Core valid fixture contains %d rows; want 70", len(rows)) + } + + vectors := make([]bitcoinCoreKeyIOValidVector, 0, len(rows)) + seen := make(map[string]int, len(rows)) + for row, fields := range rows { + if len(fields) != 3 { + t.Fatalf("Bitcoin Core valid fixture row %d has %d fields; want 3", row+1, len(fields)) + } + var vector bitcoinCoreKeyIOValidVector + if err := json.Unmarshal(fields[0], &vector.Input); err != nil { + t.Fatalf("decode Bitcoin Core valid fixture row %d input: %v", row+1, err) + } + if err := json.Unmarshal(fields[1], &vector.Script); err != nil { + t.Fatalf("decode Bitcoin Core valid fixture row %d script: %v", row+1, err) + } + if err := json.Unmarshal(fields[2], &vector.Metadata); err != nil { + t.Fatalf("decode Bitcoin Core valid fixture row %d metadata: %v", row+1, err) + } + if firstRow, exists := seen[vector.Input]; exists { + t.Fatalf("Bitcoin Core valid fixture row %d duplicates row %d input %q", row+1, firstRow, vector.Input) + } + seen[vector.Input] = row + 1 + vectors = append(vectors, vector) + } + return vectors +} + +func loadBitcoinCoreKeyIOInvalid(t *testing.T) []string { + t.Helper() + data := readPinnedCryptoFixture( + t, + "bitcoin_core_key_io_invalid.json", + "c3ca74ddad7c01faaca7c26063537feb73ff2be03eb7fc83a42a655d2979261d", + ) + + var rows [][]json.RawMessage + if err := json.Unmarshal(data, &rows); err != nil { + t.Fatalf("decode Bitcoin Core invalid fixture: %v", err) + } + if len(rows) != 70 { + t.Fatalf("Bitcoin Core invalid fixture contains %d rows; want 70", len(rows)) + } + + inputs := make([]string, 0, len(rows)) + seen := make(map[string]int, len(rows)) + for row, fields := range rows { + if len(fields) != 1 { + t.Fatalf("Bitcoin Core invalid fixture row %d has %d fields; want 1", row+1, len(fields)) + } + var input string + if err := json.Unmarshal(fields[0], &input); err != nil { + t.Fatalf("decode Bitcoin Core invalid fixture row %d: %v", row+1, err) + } + if firstRow, exists := seen[input]; exists { + t.Fatalf("Bitcoin Core invalid fixture row %d duplicates row %d input %q", row+1, firstRow, input) + } + seen[input] = row + 1 + inputs = append(inputs, input) + } + return inputs +} + +func loadBIPSegWitFixture(t *testing.T, name, expectedSHA256 string) bipSegWitFixture { + t.Helper() + data := readPinnedCryptoFixture(t, name, expectedSHA256) + var fixture bipSegWitFixture + if err := json.Unmarshal(data, &fixture); err != nil { + t.Fatalf("decode %s: %v", name, err) + } + expectedCount := fixture.Source.ValidCount + fixture.Source.InvalidCount + if len(fixture.Vectors) != expectedCount { + t.Fatalf("%s contains %d vectors; source metadata declares %d", name, len(fixture.Vectors), expectedCount) + } + seen := make(map[string]int, len(fixture.Vectors)) + for row, vector := range fixture.Vectors { + if firstRow, exists := seen[vector.Address]; exists { + t.Fatalf("%s row %d duplicates row %d input %q", name, row+1, firstRow, vector.Address) + } + seen[vector.Address] = row + 1 + } + return fixture +} + +func readPinnedCryptoFixture(t *testing.T, name, expectedSHA256 string) []byte { + t.Helper() + path := filepath.Join("testdata", name) + data, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read %s: %v", path, err) + } + sum := sha256.Sum256(data) + if actual := hex.EncodeToString(sum[:]); actual != expectedSHA256 { + t.Fatalf("%s SHA-256 is %s; want %s", path, actual, expectedSHA256) + } + return data +} + +func classifyBitcoinCoreDestination( + t *testing.T, + vector bitcoinCoreKeyIOValidVector, +) (kind string, witnessVersion int) { + t.Helper() + script, err := hex.DecodeString(vector.Script) + if err != nil { + t.Fatalf("decode script for %q: %v", vector.Input, err) + } + if len(script) == 25 && script[0] == 0x76 && script[1] == 0xa9 && script[2] == 0x14 && + script[23] == 0x88 && script[24] == 0xac { + return "legacy", -1 + } + if len(script) == 23 && script[0] == 0xa9 && script[1] == 0x14 && script[22] == 0x87 { + return "legacy", -1 + } + if len(script) < 4 || len(script) != int(script[1])+2 { + t.Fatalf("unexpected destination script for %q: %s", vector.Input, vector.Script) + } + + switch opcode := script[0]; { + case opcode == 0: + return "segwit", 0 + case opcode >= 0x51 && opcode <= 0x60: + return "segwit", int(opcode - 0x50) + default: + t.Fatalf("unexpected witness version opcode for %q: 0x%02x", vector.Input, opcode) + return "", -1 + } +} + +func classifyBIPSegWitVector(t *testing.T, fixture string, row int, vector bipSegWitVector) string { + t.Helper() + if !vector.SourceValid { + if vector.Contract != "source-invalid" || vector.Reason == "" || vector.ScriptPubKey != "" || + vector.WitnessVersion != nil { + t.Fatalf("%s row %d has inconsistent source-invalid metadata", fixture, row) + } + return "source-invalid" + } + if vector.ScriptPubKey == "" || vector.Reason != "" || vector.WitnessVersion == nil { + t.Fatalf("%s row %d has incomplete source-valid metadata", fixture, row) + } + if _, err := hex.DecodeString(vector.ScriptPubKey); err != nil { + t.Fatalf("%s row %d scriptPubKey is not hexadecimal: %v", fixture, row, err) + } + expectedEncoding := "bech32" + if strings.HasPrefix(fixture, "bip350_") && *vector.WitnessVersion > 0 { + expectedEncoding = "bech32m" + } + if vector.Encoding != expectedEncoding { + t.Fatalf("%s row %d encoding is %q; want %q", fixture, row, vector.Encoding, expectedEncoding) + } + + var expected string + switch { + case vector.Network == "mainnet" && *vector.WitnessVersion == 0 && vector.Encoding == "bech32": + expected = "accepted" + case vector.Network == "testnet" && *vector.WitnessVersion == 0 && vector.Encoding == "bech32": + expected = "non-mainnet" + case vector.Network == "mainnet" && *vector.WitnessVersion > 0: + expected = "v1+" + case vector.Network == "testnet" && *vector.WitnessVersion > 0: + expected = "non-mainnet v1+" + default: + t.Fatalf("%s row %d has unsupported source-valid classification", fixture, row) + } + if vector.Contract != expected { + t.Fatalf("%s row %d contract is %q; want %q", fixture, row, vector.Contract, expected) + } + return expected +} + +func assertStringFormatRuleResult( + t *testing.T, + rule govy.Rule[string], + input string, + wantValid bool, + errorMessage string, + errorCode govy.ErrorCode, + source string, + row int, +) { + t.Helper() + err := rule.Validate(input) + if wantValid { + if err != nil { + t.Errorf("%s row %d rejected %q: %v", source, row, input, err) + } + return + } + if err == nil { + t.Errorf("%s row %d accepted %q", source, row, input) + return + } + if err.Error() != errorMessage { + t.Errorf("%s row %d error is %q; want %q", source, row, err.Error(), errorMessage) + } + if !govy.HasErrorCode(err, errorCode) { + t.Errorf("%s row %d error does not contain code %q", source, row, errorCode) + } +} diff --git a/pkg/rules/testdata/bip173_segwit_vectors.json b/pkg/rules/testdata/bip173_segwit_vectors.json new file mode 100644 index 00000000..67cce90d --- /dev/null +++ b/pkg/rules/testdata/bip173_segwit_vectors.json @@ -0,0 +1,126 @@ +{ + "source": { + "repository": "https://github.com/bitcoin/bips", + "commit": "8c369ac8e60629ac6c032ffe21bb5ec5b35213d7", + "file": "bip-0173.mediawiki", + "file_sha256": "b19eab06b264bbd15ad7f7d640fa34de8f4a8809fba8a6ecc3e165545b27a543", + "valid_count": 6, + "invalid_count": 10 + }, + "vectors": [ + { + "address": "BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4", + "source_valid": true, + "script_pub_key": "0014751e76e8199196d454941c45d1b3a323f1433bd6", + "network": "mainnet", + "witness_version": 0, + "encoding": "bech32", + "contract": "accepted" + }, + { + "address": "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7", + "source_valid": true, + "script_pub_key": "00201863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262", + "network": "testnet", + "witness_version": 0, + "encoding": "bech32", + "contract": "non-mainnet" + }, + { + "address": "bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7k7grplx", + "source_valid": true, + "script_pub_key": "5128751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd6", + "network": "mainnet", + "witness_version": 1, + "encoding": "bech32", + "contract": "v1+" + }, + { + "address": "BC1SW50QA3JX3S", + "source_valid": true, + "script_pub_key": "6002751e", + "network": "mainnet", + "witness_version": 16, + "encoding": "bech32", + "contract": "v1+" + }, + { + "address": "bc1zw508d6qejxtdg4y5r3zarvaryvg6kdaj", + "source_valid": true, + "script_pub_key": "5210751e76e8199196d454941c45d1b3a323", + "network": "mainnet", + "witness_version": 2, + "encoding": "bech32", + "contract": "v1+" + }, + { + "address": "tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy", + "source_valid": true, + "script_pub_key": "0020000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433", + "network": "testnet", + "witness_version": 0, + "encoding": "bech32", + "contract": "non-mainnet" + }, + { + "address": "tc1qw508d6qejxtdg4y5r3zarvary0c5xw7kg3g4ty", + "source_valid": false, + "reason": "Invalid human-readable part", + "contract": "source-invalid" + }, + { + "address": "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t5", + "source_valid": false, + "reason": "Invalid checksum", + "contract": "source-invalid" + }, + { + "address": "BC13W508D6QEJXTDG4Y5R3ZARVARY0C5XW7KN40WF2", + "source_valid": false, + "reason": "Invalid witness version", + "contract": "source-invalid" + }, + { + "address": "bc1rw5uspcuh", + "source_valid": false, + "reason": "Invalid program length", + "contract": "source-invalid" + }, + { + "address": "bc10w508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kw5rljs90", + "source_valid": false, + "reason": "Invalid program length", + "contract": "source-invalid" + }, + { + "address": "BC1QR508D6QEJXTDG4Y5R3ZARVARYV98GJ9P", + "source_valid": false, + "reason": "Invalid program length for witness version 0 (per BIP141)", + "contract": "source-invalid" + }, + { + "address": "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sL5k7", + "source_valid": false, + "reason": "Mixed case", + "contract": "source-invalid" + }, + { + "address": "bc1zw508d6qejxtdg4y5r3zarvaryvqyzf3du", + "source_valid": false, + "reason": "zero padding of more than 4 bits", + "contract": "source-invalid" + }, + { + "address": "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3pjxtptv", + "source_valid": false, + "reason": "Non-zero padding in 8-to-5 conversion", + "contract": "source-invalid" + }, + { + "address": "bc1gmk9yu", + "source_valid": false, + "reason": "Empty data section", + "contract": "source-invalid" + } + ] +} diff --git a/pkg/rules/testdata/bip350_segwit_vectors.json b/pkg/rules/testdata/bip350_segwit_vectors.json new file mode 100644 index 00000000..3b49c3c0 --- /dev/null +++ b/pkg/rules/testdata/bip350_segwit_vectors.json @@ -0,0 +1,174 @@ +{ + "source": { + "repository": "https://github.com/bitcoin/bips", + "commit": "8c369ac8e60629ac6c032ffe21bb5ec5b35213d7", + "file": "bip-0350.mediawiki", + "file_sha256": "63634b06aa8bae88b31929674736e74964c4598684269ac2b0b140c43a7a0dec", + "valid_count": 8, + "invalid_count": 15 + }, + "vectors": [ + { + "address": "BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4", + "source_valid": true, + "script_pub_key": "0014751e76e8199196d454941c45d1b3a323f1433bd6", + "network": "mainnet", + "witness_version": 0, + "encoding": "bech32", + "contract": "accepted" + }, + { + "address": "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7", + "source_valid": true, + "script_pub_key": "00201863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262", + "network": "testnet", + "witness_version": 0, + "encoding": "bech32", + "contract": "non-mainnet" + }, + { + "address": "bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kt5nd6y", + "source_valid": true, + "script_pub_key": "5128751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd6", + "network": "mainnet", + "witness_version": 1, + "encoding": "bech32m", + "contract": "v1+" + }, + { + "address": "BC1SW50QGDZ25J", + "source_valid": true, + "script_pub_key": "6002751e", + "network": "mainnet", + "witness_version": 16, + "encoding": "bech32m", + "contract": "v1+" + }, + { + "address": "bc1zw508d6qejxtdg4y5r3zarvaryvaxxpcs", + "source_valid": true, + "script_pub_key": "5210751e76e8199196d454941c45d1b3a323", + "network": "mainnet", + "witness_version": 2, + "encoding": "bech32m", + "contract": "v1+" + }, + { + "address": "tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy", + "source_valid": true, + "script_pub_key": "0020000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433", + "network": "testnet", + "witness_version": 0, + "encoding": "bech32", + "contract": "non-mainnet" + }, + { + "address": "tb1pqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesf3hn0c", + "source_valid": true, + "script_pub_key": "5120000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433", + "network": "testnet", + "witness_version": 1, + "encoding": "bech32m", + "contract": "non-mainnet v1+" + }, + { + "address": "bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqzk5jj0", + "source_valid": true, + "script_pub_key": "512079be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "network": "mainnet", + "witness_version": 1, + "encoding": "bech32m", + "contract": "v1+" + }, + { + "address": "tc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vq5zuyut", + "source_valid": false, + "reason": "Invalid human-readable part", + "contract": "source-invalid" + }, + { + "address": "bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqh2y7hd", + "source_valid": false, + "reason": "Invalid checksum (Bech32 instead of Bech32m)", + "contract": "source-invalid" + }, + { + "address": "tb1z0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqglt7rf", + "source_valid": false, + "reason": "Invalid checksum (Bech32 instead of Bech32m)", + "contract": "source-invalid" + }, + { + "address": "BC1S0XLXVLHEMJA6C4DQV22UAPCTQUPFHLXM9H8Z3K2E72Q4K9HCZ7VQ54WELL", + "source_valid": false, + "reason": "Invalid checksum (Bech32 instead of Bech32m)", + "contract": "source-invalid" + }, + { + "address": "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kemeawh", + "source_valid": false, + "reason": "Invalid checksum (Bech32m instead of Bech32)", + "contract": "source-invalid" + }, + { + "address": "tb1q0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vq24jc47", + "source_valid": false, + "reason": "Invalid checksum (Bech32m instead of Bech32)", + "contract": "source-invalid" + }, + { + "address": "bc1p38j9r5y49hruaue7wxjce0updqjuyyx0kh56v8s25huc6995vvpql3jow4", + "source_valid": false, + "reason": "Invalid character in checksum", + "contract": "source-invalid" + }, + { + "address": "BC130XLXVLHEMJA6C4DQV22UAPCTQUPFHLXM9H8Z3K2E72Q4K9HCZ7VQ7ZWS8R", + "source_valid": false, + "reason": "Invalid witness version", + "contract": "source-invalid" + }, + { + "address": "bc1pw5dgrnzv", + "source_valid": false, + "reason": "Invalid program length (1 byte)", + "contract": "source-invalid" + }, + { + "address": "bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7v8n0nx0muaewav253zgeav", + "source_valid": false, + "reason": "Invalid program length (41 bytes)", + "contract": "source-invalid" + }, + { + "address": "BC1QR508D6QEJXTDG4Y5R3ZARVARYV98GJ9P", + "source_valid": false, + "reason": "Invalid program length for witness version 0 (per BIP141)", + "contract": "source-invalid" + }, + { + "address": "tb1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vq47Zagq", + "source_valid": false, + "reason": "Mixed case", + "contract": "source-invalid" + }, + { + "address": "bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7v07qwwzcrf", + "source_valid": false, + "reason": "zero padding of more than 4 bits", + "contract": "source-invalid" + }, + { + "address": "tb1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vpggkg4j", + "source_valid": false, + "reason": "Non-zero padding in 8-to-5 conversion", + "contract": "source-invalid" + }, + { + "address": "bc1gmk9yu", + "source_valid": false, + "reason": "Empty data section", + "contract": "source-invalid" + } + ] +} diff --git a/pkg/rules/testdata/bitcoin_core_key_io_invalid.json b/pkg/rules/testdata/bitcoin_core_key_io_invalid.json new file mode 100644 index 00000000..8f55abfe --- /dev/null +++ b/pkg/rules/testdata/bitcoin_core_key_io_invalid.json @@ -0,0 +1,212 @@ +[ + [ + "" + ], + [ + "x" + ], + [ + "1GAdfviErV2Ew95FPtZyikz2qGP3gyCB6Hyu94sedAkPpA523m3fQwps9YKUZkKgQckGPKhRsFR" + ], + [ + "37G2kMDLpmWVhimxRdzwNfE8JFvWXnJYnVcXeeGrek2qumdJuK7XArcVVpRtLLjRra3t64BEPF2" + ], + [ + "giymtio7u7oqWtmC9YnvAEKkLF3JQpAdkEFkVJKYrVDfaLbhaDpX1ihfF2vZmya1i61fwLPC3YQ" + ], + [ + "8iVk9nLM3nYwRuwypjy9NK5rsuZH7BbrQRZ1pgcQmvMnjAgRXD" + ], + [ + "cPTVQ1hbo4qdoysf6Jx5GthqucNmdfqt6J2pZRFeXv8Ep7Kmjqud" + ], + [ + "cQbR2Ny85XFBzUMx3Ed6HsTLw2pVruSgPvt5AofnBUnhiv86gYeW" + ], + [ + "2UB3iG3VJbX2TRrMwm6ssWskgvU9VjFBYSqCzwqkrihCwo7mg4mtS4WuGZgxTKuxf5A3EcotYEymz" + ], + [ + "cQe12pqwPR6ExtZKfrKf1q4b3CTh1Qi7MwuvMvzs79nWXDvESfBJ" + ], + [ + "tc1qeul5g2xfkvdkrhcfmdursv73ad64jnkjl9c40f" + ], + [ + "bt1pq65rzej5glw3ra79gav6fqnx4haa0z257qr3mc8cggkefahmgvyseufhc0" + ], + [ + "tb13hty4qmumlwpp6chxjvcyzza4duqgtmxw3xhm3u9ahj4nyhtwz8eq7ynrj4" + ], + [ + "bcrt1r2qxpwuge" + ], + [ + "bc10uexgzna2dpfk0vjt35srz6a27ps6m0l89jweznt83n2sqn2fx4hvn9ym5af8wut34sfrqhk3" + ], + [ + "tb1qum6uh0pt4q253qaf520929737v63w5gf" + ], + [ + "bcrt1q888ryfgxpvl0k7vum8zpyar2u2sexvdhkf38ue37yknmqq0ycrwpl3w48y" + ], + [ + "bc1qdsuzmn04k2z8vryw8l4dj8m5ygqgnne5n" + ], + [ + "tb1qlj8es50nc8j8r8xshrjgzmw5azx89efghmw8ju6zcqla0g6xcnrstsjz7k" + ], + [ + "bcrt1qzwmyj0z924g7fzs5yvnrkc43y76RVyr2lh5t4r" + ], + [ + "bc1qpu6d26mrulzetu4jqhd7rsunv9aqru26f5c4j8" + ], + [ + "tb1qun6d26ufh77ghny6u5u8cwz9da7qwc6k4wkuceae9tth06eqlw0syupl4w" + ], + [ + "bcrt1qj7g2jps453kj9htk9cxyyc2nxe69x4kzzmth7v" + ], + [ + "bc1p702xksx4z3uqf0u2phllxkfe5cgu0adxptqs0uelx0tqt8e885sqryes2l" + ], + [ + "tb1z7gmh0v6pc30z4xum76lmw8w86yswrlmw" + ], + [ + "bcrt1sjsrw6nun4h502cr97xmnyyuhkr22q0s6efrgtu" + ], + [ + "2UVPFpGYnLHJezFzjUo42our6PMEoozzRdM" + ], + [ + "2MygHQjE1U33q3LSC53p69YqFjP8PihumJAF" + ], + [ + "KzNbAQ4mexfAxa6RKBzHQqfoTycaeWpv2p" + ], + [ + "2jDPrDfAKihCGPbPD9ztY8TswAia4V8Bc6vx" + ], + [ + "4VQUNG1hG64QFtaNyQZQWDdwpxB275Pwb3tvyPt2HDxB8Mi2MgH8Tz3AC83YYiz9LydsLNXEZJLHY" + ], + [ + "39TKsUQ5QpEL1wowc6GMUqak94ijirPuP69ooV3xsFmiKQX2dau" + ], + [ + "2UEJjT3dSdwc8dAo7oedPzznXceXCEsBbDfAvSymqpqDrkZMv7JBEUpLyhkghioYAWC9W4sKysry" + ], + [ + "7VmMEkphxCFSV1y659Th4dkk6x6bJS5eQvbt8rzUYKQyd6ACgwQ4vXHtXKFUwP2kW3XULipnHJdZ7" + ], + [ + "tc1qdlapns4zkn03juf2k9xwwpct209suj6mgcd9gh" + ], + [ + "bt1psa5eptk29c4jc9yumeseat3a0l5e2fpmw635za2p4gpwdnthueysxga9je" + ], + [ + "tb13w8c43lykfj3lvm9sgp6dsnfjla3d57cm83seykunf0ltxjc9lt2q4efm4d" + ], + [ + "bcrt1rjqr2tdkm" + ], + [ + "bc10lyxwnxa70l270e6fcmxr4x7dtgu2yvy7gzkurwxy4zhdvgaqrrn6pfg2flyhqzy5t5se8yu3" + ], + [ + "TB1QFDFM763VXVSUNZHQLPWC0Q8FG5LJX6ZN" + ], + [ + "bcrt1q60chha7wfwlau4kdr4mlvyeyc8mnnh9dhxk05e0hmrxcuhghefj36uwyha" + ], + [ + "bc1gmk9yu" + ], + [ + "tb1ly0q7p" + ], + [ + "bcrt1qdwttaw38uf42wxw40kwk3u8nguyTQH3hx6jmqp" + ], + [ + "bc1qtsvlht6730n04f2mpaj5vv8hrledn5n5ug8c79" + ], + [ + "tb1dclvmr" + ], + [ + "bcrt1q3fqvctqu48wsvggrt09vj0yk2gzzcscdp4h98u" + ], + [ + "bc1prklpq7tjcawg89cmwwqr3u5apwav36xa4zz56ady7crsllm6mpnqts7p86" + ], + [ + "tb1zkm58zyhxz3ffkfgsyprflg543slsl4c4" + ], + [ + "bcrt1snzr5kaypnfhpnjanrhd20fhqcjxm3hfh7dw9fu" + ], + [ + "2GgnYKqBGuA2Mm5GnrPsMTZR81xPhNtgMYoFUZngZGiobhCuUpCaTriUHRcgFreEekNdPAR17q8d" + ], + [ + "AZEah8d1EK362okRBS66e8SvdtYkrE8tsX" + ], + [ + "gep8xr77FyPW6zYP15RiV9W8nL6w2HyHB16cUDakfyDceMA6ZzUdhJjk2LPuLYHnLkBqkRTTi6z" + ], + [ + "2NDNP7GY59tTJPZTpbkprhM9SR99Nn5rUs7" + ], + [ + "2Csgzy2T287YAjeU5tFtt1nPshBZAUFQi4WtgaWyZGKSBNnKXHy2Tmxo8QK4Mfdds977ShcDWC5o" + ], + [ + "Kwjk3Vy6sdXMQDGWJzaWmqFxUNtWZCX1q4F4Kpt8jNNUoWJUUaTY" + ], + [ + "Svj8kk98bAS9V4L2crmxakbhmnPm3cJ1tJ4Je4yVzDreU8eSTFURS1SPYv5oWEQD8Q9VBDvx5uF" + ], + [ + "KNYsv6v9GtkGeD4WdQnBEJCrPKQm91PTxAbCfXr66LEd4JDmhPWC" + ], + [ + "2UJ2H2xvAeXmFKfQwMyDoSdQTTPFMNCT3SsoUafBWKzoGP3NsUK1buEgQZG38viyD53jgMdpqfT7" + ], + [ + "6aLMfayKF4TW4ecn5SEc8FExpyJA2peKxYRGZhes6tQ4NTTzuGy" + ], + [ + "7VP4FmcebU2thJns9MnXde7LWfuqR5vMizrAuUoq2GcJjzTyA4RHFcPVdZL8PLg1SbpSFdJrvLXoY5" + ], + [ + "tc1q5qdvt99uc92jyz663dtdpfpv6nr67ahmgwcpq2" + ], + [ + "bt1peu3ppd7x796sjjenp09r8cs22rhylqm9lhggk72qp8q22vzft0wq2a0x6j" + ], + [ + "tb1323z3lnz7dl3kd0nsuh6xy4he9almzl67anxgg3xdzkaxc9rwntlqdhdzd7" + ], + [ + "bcrt1r2gc42sky" + ], + [ + "bc10fd889x4hd54tqu2ewg9t4hhft2wl7m6x50av4uswzw46xe6as0xmltfg7vrjfkvm459vld7w" + ], + [ + "TB1QZY7V0F2AT3308YGGNGN66ULJTCN3RY6F" + ], + [ + "bcrt1qjg3cwht92znyw0l4r5rtctmls337nrc7g0ry9drjxmlecjd3atl3fake7c" + ], + [ + "bc1qmgf8xt8xkecl79k04mma3lz34gqep7hg4" + ], + [ + "TB1Q3F9WGNXE9ZMTTMDN5VKVKHYZ8Y0LCV72YV7V5LSXTJXEYHNHEHASLYL0TZ" + ] +] diff --git a/pkg/rules/testdata/bitcoin_core_key_io_valid.json b/pkg/rules/testdata/bitcoin_core_key_io_valid.json new file mode 100644 index 00000000..bff7ecff --- /dev/null +++ b/pkg/rules/testdata/bitcoin_core_key_io_valid.json @@ -0,0 +1,610 @@ +[ + [ + "1FsSia9rv4NeEwvJ2GvXrX7LyxYspbN2mo", + "76a914a31c06bd463e3923bc1aadbde48b16976c08071788ac", + { + "chain": "main", + "isPrivkey": false + } + ], + [ + "36j4NfKv6Akva9amjWrLG6MuSQym1GuEmm", + "a914373b819a068f32b7a6b38b6b38729647cfde01c287", + { + "chain": "main", + "isPrivkey": false + } + ], + [ + "mzK2FFDEhxqHcmrJw1ysqFkVyhUULo45hZ", + "76a914ce28b26c57472737f5c3561a1761185bd8589a4388ac", + { + "chain": "testnet4", + "isPrivkey": false + } + ], + [ + "2NC2hEhe28ULKAJkW5MjZ3jtTMJdvXmByvK", + "a914ce0bba75891ff9ec60148d4bd4a09ee2dc5c933187", + { + "chain": "testnet4", + "isPrivkey": false + } + ], + [ + "mww4LvqtTMKvmeQvizPz2EQv26xTneWrbg", + "76a914b4110ba93ac54afc14da3bdd19614774a2d55d2988ac", + { + "chain": "signet", + "isPrivkey": false + } + ], + [ + "2N1r7aC69VHeE7yQJPDLi9T1PYq4wnwvjuT", + "a9145e5a35ab44b3efaea5129ba22b88ba3e2976614587", + { + "chain": "signet", + "isPrivkey": false + } + ], + [ + "n4fajahJrAuKbN7uNsKjLjQkz9Qn5ewJXQ", + "76a914fdeca3b08e38af53d7c4c60e3ad208ce5066441088ac", + { + "chain": "regtest", + "isPrivkey": false + } + ], + [ + "2MxFajLApXpYk4VodBSZSt7rw8y4ryABkfA", + "a91436e9f191e0b75036a77f65e2eaa4752443233fbe87", + { + "chain": "regtest", + "isPrivkey": false + } + ], + [ + "5JuW2AMDYu4xVwRG9DZW18VbzQrGcd5RCgb99sS6ehJsNQXu5b9", + "8f8943bf956de595665c38ffff23827e17c10cdc1c27a028caae6c9810626198", + { + "chain": "main", + "isCompressed": false, + "isPrivkey": true + } + ], + [ + "L5nJeqKmpHp4P7F8ZYyjwc5a7P4d8EabuGAzfGJk7yC1BJyzNaEd", + "ff778740f88ddcf102aeb81daee289c044c4a4571c4b6f287400f4b8e0b843f8", + { + "chain": "main", + "isCompressed": true, + "isPrivkey": true + } + ], + [ + "92ZdE5HoLafywnTBbzPxbvRmp75pSfzvdU3XaZGh1cToipgdHVh", + "80c32d81e91bdea04cd7a3819b32275fc3298af4c7ec87eb0099527d041ced5c", + { + "chain": "testnet4", + "isCompressed": false, + "isPrivkey": true + } + ], + [ + "cV83kKisF3RQSvXbUCm9ox3kaz5JjEUBWcx8tNydfGJcyeUxuH47", + "e0fcd4ce4e3d0e3de091f21415bb7cd011fac288c42020a879f28c2a4387df9b", + { + "chain": "testnet4", + "isCompressed": true, + "isPrivkey": true + } + ], + [ + "92QuSnywrhsV7WPZChTgSQA23uSmj9MCEEno1eRBDG9sg8M29cX", + "6cf636ed8ac1bab033b64f66feaba65f70e684731e3f39105605968d3a963801", + { + "chain": "signet", + "isCompressed": false, + "isPrivkey": true + } + ], + [ + "cND53Dhp8eCZqG2ghe8YhSCGesXZ8fE5PGD1khrqNvEi4RBoXhEK", + "12b5a10f3a11e708dc5412833c47ab7c368a21b9efe19293793ec879ce683018", + { + "chain": "signet", + "isCompressed": true, + "isPrivkey": true + } + ], + [ + "91mn1wYKEB1zyof1VFm8tMtocZx1oBrKKRCu9GCpgZvPmBLEJjp", + "18a86e5a6c6977ddba0daca7fba5190f67ba56ccdc1b3f31308972236c2e4776", + { + "chain": "regtest", + "isCompressed": false, + "isPrivkey": true + } + ], + [ + "cPisAUdLvqqAr6MYtXnrWvgvyUAwuNyuTvZkDGw6miPhZdaiSDNH", + "3fdfec1371cedcdb8c190ca6ff8ad603f817edc0d93c2a687c7b36dd66e70f2a", + { + "chain": "regtest", + "isCompressed": true, + "isPrivkey": true + } + ], + [ + "bc1qvyq0cc6rahyvsazfdje0twl7ez82ndmuac2lhv", + "00146100fc6343edc8c874496cb2f5bbfec88ea9b77c", + { + "chain": "main", + "isPrivkey": false, + "tryCaseFlip": true + } + ], + [ + "bc1qyucykdlhp62tezs0hagqury402qwhk589q80tqs5myh3rxq34nwqhkdhv7", + "002027304b37f70e94bc8a0fbf500e0c957a80ebda87280ef58214d92f119811acdc", + { + "chain": "main", + "isPrivkey": false, + "tryCaseFlip": true + } + ], + [ + "bc1p83n3au0rjylefxq2nc2xh2y4jzz4pm6zxj4mw5pagdjjr2a9f36s6jjnnu", + "51203c671ef1e3913f94980a9e146ba895908550ef4234abb7503d436521aba54c75", + { + "chain": "main", + "isPrivkey": false, + "tryCaseFlip": true + } + ], + [ + "bc1z2rksukkjr8", + "520250ed", + { + "chain": "main", + "isPrivkey": false, + "tryCaseFlip": true + } + ], + [ + "tb1qcrh3yqn4nlleplcez2yndq2ry8h9ncg3qh7n54", + "0014c0ef1202759fff90ff19128936814321ee59e111", + { + "chain": "testnet4", + "isPrivkey": false, + "tryCaseFlip": true + } + ], + [ + "tb1quyl9ujpgwr2chdzdnnalen48sup245vdfnh2jxhsuq3yx80rrwlq5hqfe4", + "0020e13e5e482870d58bb44d9cfbfccea78702aad18d4ceea91af0e022431de31bbe", + { + "chain": "testnet4", + "isPrivkey": false, + "tryCaseFlip": true + } + ], + [ + "tb1p35n52jy6xkm4wd905tdy8qtagrn73kqdz73xe4zxpvq9t3fp50aqk3s6gz", + "51208d2745489a35b75734afa2da43817d40e7e8d80d17a26cd4460b0055c521a3fa", + { + "chain": "testnet4", + "isPrivkey": false, + "tryCaseFlip": true + } + ], + [ + "tb1rgv5m6uvdk3kc7qsuz0c79v88ycr5w4wa", + "53104329bd718db46d8f021c13f1e2b0e726", + { + "chain": "testnet4", + "isPrivkey": false, + "tryCaseFlip": true + } + ], + [ + "tb1q3vya2h5435jkugq2few7dmktlrwq4ejmfaw7kr", + "00148b09d55e958d256e200a4e5de6eecbf8dc0ae65b", + { + "chain": "signet", + "isPrivkey": false, + "tryCaseFlip": true + } + ], + [ + "tb1qxkhrl2s6ttrclckldruea0e8anhrehffl8xv7t0pdyrzm08v2hyqy408nf", + "002035ae3faa1a5ac78fe2df68f99ebf27ecee3cdd29f9cccf2de169062dbcec55c8", + { + "chain": "signet", + "isPrivkey": false, + "tryCaseFlip": true + } + ], + [ + "tb1pae5um27ahn8n73pgexe3kcwlp8dhswpn684h2k2w6t9a7w3eq65qephd5y", + "5120ee69cdabddbccf3f4428c9b31b61df09db783833d1eb75594ed2cbdf3a3906a8", + { + "chain": "signet", + "isPrivkey": false, + "tryCaseFlip": true + } + ], + [ + "tb1rx9n9g37az8mu236e5jpxdt0m67y4fuq8rhs0ss3djnm0kscfrwvq0ntlyg", + "532031665447dd11f7c54759a48266adfbd78954f0071de0f8422d94f6fb43091b98", + { + "chain": "signet", + "isPrivkey": false, + "tryCaseFlip": true + } + ], + [ + "bcrt1qdavt4j2sd7dlhqsavtnfxvzppw6k7qy97tmnu9", + "00146f58bac9506f9bfb821d62e69330410bb56f0085", + { + "chain": "regtest", + "isPrivkey": false, + "tryCaseFlip": true + } + ], + [ + "bcrt1qan8gntac7z7me2ejt4hpru42ad2f759fmy0m3ejvs98656znv7eqga4uhv", + "0020ecce89afb8f0bdbcab325d6e11f2aaeb549f50a9d91fb8e64c814faa685367b2", + { + "chain": "regtest", + "isPrivkey": false, + "tryCaseFlip": true + } + ], + [ + "bcrt1pfwxjqvtt4tcxrtdluukfmy2dv7xd2qzdfy6kajv5nwn4yam3wxkq3553uh", + "51204b8d20316baaf061adbfe72c9d914d678cd5004d49356ec9949ba752777171ac", + { + "chain": "regtest", + "isPrivkey": false, + "tryCaseFlip": true + } + ], + [ + "bcrt1sx6p8njlx7h9mc2agz4yg82dzne23050ncq72cneeecez2pst8mahn8xecsf8g6hzx94420", + "6028368279cbe6f5cbbc2ba8154883a9a29e5517d1f3c03cac4f39ce3225060b3efb799cd9c412746ae2", + { + "chain": "regtest", + "isPrivkey": false, + "tryCaseFlip": true + } + ], + [ + "1FjL87pn8ky6Vbavd1ZHeChRXtoxwRGCRd", + "76a914a19331b7b2627e663e25a7b001e4c0dcc5e21bc788ac", + { + "chain": "main", + "isPrivkey": false + } + ], + [ + "3BZECeAH8gSKkjrTx8PwMrNQBLG18yHpvf", + "a9146c382dcdf5b284760c8e3fead91f7422cd76aa8787", + { + "chain": "main", + "isPrivkey": false + } + ], + [ + "n4YNbYuFdPwFrxSP8sjHFbAhUbLMUiY9jE", + "76a914fc8f9851f3c1e4719cd0b8e4816dd4e88c72e52888ac", + { + "chain": "testnet4", + "isPrivkey": false + } + ], + [ + "2NAeQVZayzVFAtgeC3iYJsjpjWDmsDph71A", + "a914bedc797342c03fd7a346c4c7857ca03d467013b687", + { + "chain": "testnet4", + "isPrivkey": false + } + ], + [ + "mnCBpkNMJEJLehgdEkzSo2eioniyJMxLpZ", + "76a914493c455551e48a1423263b62b127b436106a685488ac", + { + "chain": "signet", + "isPrivkey": false + } + ], + [ + "2N5sNHomeNJDZv67AcFx9ES7FBZY4jx9KDA", + "a9148a776a0f34d56b63e7c595f2b205dbe1c393617a87", + { + "chain": "signet", + "isPrivkey": false + } + ], + [ + "mfhE6jAUwjUDNZhaX1PAsDTKfneQF2Nshc", + "76a91401f15a4cc063dae4f4d56b89bfbc8bcc9ae5387c88ac", + { + "chain": "regtest", + "isPrivkey": false + } + ], + [ + "2MxNm1VHyVU4RuP3u1c1v5aQLk2dQjwy1Qk", + "a91438456f7c076356abadcc67b92ad777eb20fb9f8887", + { + "chain": "regtest", + "isPrivkey": false + } + ], + [ + "5HsL2nZuEebU5nM3RxNVQD9GcAnvNMahqQskf4fkqHe54zwd14e", + "06e8649790a90615a46d22dd762e0c42615336745356c2e16147c0f3d46b40d5", + { + "chain": "main", + "isCompressed": false, + "isPrivkey": true + } + ], + [ + "KwuVvu6hsuEMHrfFWJQV64tRrWX3QzqHH18JuAHYqYV6dqBvNKxd", + "147804bf8a0dfff35939a611c7f5a60ac107f33f33d6059f273d2079ab1d90f2", + { + "chain": "main", + "isCompressed": true, + "isPrivkey": true + } + ], + [ + "921M1RNxghFcsVGqAJksQVbSgx36Yz4u6vebfz1wDujNvgNt93B", + "3777b341c45e2a9b9bf6bfb71dc7d129f64f1b9406ed4f93ade8f56065f1b732", + { + "chain": "testnet4", + "isCompressed": false, + "isPrivkey": true + } + ], + [ + "cNEnbfF2fcxmmCLWqMAaq6fxJvVkwMbyU3kCbpQznz4Z1j6TZDGb", + "1397b0d4a03e1ab2c54dd9af99ce1ecbfb90c80a58886da95e1181a55703d96b", + { + "chain": "testnet4", + "isCompressed": true, + "isPrivkey": true + } + ], + [ + "93BcpCMKPmFCuY8bqS4k3HFrhJ1Afxi4uSsEeJFvX86GYW7PC7W", + "d27d1b6ef55ca2e4d475b5276f2dbb85f7a6459dceeb89c67b776fd3bb974452", + { + "chain": "signet", + "isCompressed": false, + "isPrivkey": true + } + ], + [ + "cUtwbyxoL1owPxUafgH2meEpydeywjhnTYv2mJaFHHchz39AaEgy", + "da3ed4ef1647e1733ec076919cab6156077ed9532e7c365acc425747e198b3e1", + { + "chain": "signet", + "isCompressed": true, + "isPrivkey": true + } + ], + [ + "927zPWny2SiNaUmHF5NnGQXQWDwbByfFzXGgu88j91ZoutSosvE", + "468e0284f230153db8687d8ec23db079a5b67d72ca04174b3867b13e4ea9945e", + { + "chain": "regtest", + "isCompressed": false, + "isPrivkey": true + } + ], + [ + "cRez45VGSp5EXNqm89K3NJJPSKKapJg5Kbw3atxr2337x2gtgYed", + "798d87586cffbe8c545ab374454e403b1eb831501ebe89f3c3b02f3137bd7b46", + { + "chain": "regtest", + "isCompressed": true, + "isPrivkey": true + } + ], + [ + "bc1qhxt04s5xnpy0kxw4x99n5hpdf5pmtzpqs52es2", + "0014b996fac2869848fb19d5314b3a5c2d4d03b58820", + { + "chain": "main", + "isPrivkey": false, + "tryCaseFlip": true + } + ], + [ + "bc1qgc9ljrvdf2e0zg9rmmq86xklqwfys7r6wptjlacdgrcdc7sa6ggqu4rrxf", + "0020460bf90d8d4ab2f120a3dec07d1adf039248787a70572ff70d40f0dc7a1dd210", + { + "chain": "main", + "isPrivkey": false, + "tryCaseFlip": true + } + ], + [ + "bc1pve739yap4uxjvfk0jrey69078u0gasm2nwvv483ec6zkzulgw9xqu4w9fd", + "5120667d1293a1af0d2626cf90f24d15fe3f1e8ec36a9b98ca9e39c6856173e8714c", + { + "chain": "main", + "isPrivkey": false, + "tryCaseFlip": true + } + ], + [ + "bc1zmjtqxkzs89", + "5202dc96", + { + "chain": "main", + "isPrivkey": false, + "tryCaseFlip": true + } + ], + [ + "tb1ql4k5ayv7p7w0t0ge7tpntgpkgw53g2payxkszr", + "0014fd6d4e919e0f9cf5bd19f2c335a03643a914283d", + { + "chain": "testnet4", + "isPrivkey": false, + "tryCaseFlip": true + } + ], + [ + "tb1q9jx3x2qqdpempxrcfgyrkjd5fzeacaqj4ua7cs7fe2sfd2wdaueq5wn26y", + "00202c8d1328006873b098784a083b49b448b3dc7412af3bec43c9caa096a9cdef32", + { + "chain": "testnet4", + "isPrivkey": false, + "tryCaseFlip": true + } + ], + [ + "tb1pdswckwd9ym5yf5eyzg8j4jjwnzla8y0tf9cp7aasfkek0u29sz9qfr00yf", + "51206c1d8b39a526e844d324120f2aca4e98bfd391eb49701f77b04db367f145808a", + { + "chain": "testnet4", + "isPrivkey": false, + "tryCaseFlip": true + } + ], + [ + "tb1r0ecpfxg2udhtc556gqrpwwhk4sw3f0kc", + "53107e7014990ae36ebc529a4006173af6ac", + { + "chain": "testnet4", + "isPrivkey": false, + "tryCaseFlip": true + } + ], + [ + "tb1q6mwf89hnqhlu8txjgjfs4s7p93ugffn3k062ll", + "0014d6dc9396f305ffc3acd244930ac3c12c7884a671", + { + "chain": "signet", + "isPrivkey": false, + "tryCaseFlip": true + } + ], + [ + "tb1qafrjalu4d73dql0czau9j6z422434kef235mzljf48ckd5xz3sys09jm97", + "0020ea472eff956fa2d07df8177859685552ab1adb295469b17e49a9f166d0c28c09", + { + "chain": "signet", + "isPrivkey": false, + "tryCaseFlip": true + } + ], + [ + "tb1pwst9qszjrhuv2e7as0flcq9gm698v6gdxzz9e87p07s8rssdx3zqklm3vf", + "512074165040521df8c567dd83d3fc00a8de8a76690d30845c9fc17fa071c20d3444", + { + "chain": "signet", + "isPrivkey": false, + "tryCaseFlip": true + } + ], + [ + "tb1r3ss76jtsuxe8c8c8lxsehnpak55ylrgr345pww076l536ahjr6jsydamx3", + "53208c21ed4970e1b27c1f07f9a19bcc3db5284f8d038d681739fed7e91d76f21ea5", + { + "chain": "signet", + "isPrivkey": false, + "tryCaseFlip": true + } + ], + [ + "bcrt1q65nhlm4hf2ptg3t264al57p7wjxj2c3s6kyt83", + "0014d5277feeb74a82b4456ad57bfa783e748d256230", + { + "chain": "regtest", + "isPrivkey": false, + "tryCaseFlip": true + } + ], + [ + "bcrt1qawvc90lpytw3z3k9etdx54l0exq5f5sqfzu5e45kjnl6slwayeeqx2dyac", + "0020eb9982bfe122dd1146c5cada6a57efc98144d20048b94cd69694ffa87ddd2672", + { + "chain": "regtest", + "isPrivkey": false, + "tryCaseFlip": true + } + ], + [ + "bcrt1p39a4s4vdcw9kqa8w2t0rp7aj8kfxyw7mce5sk5d70x6wnnmpvt7skf2kxy", + "5120897b58558dc38b6074ee52de30fbb23d92623bdbc6690b51be79b4e9cf6162fd", + { + "chain": "regtest", + "isPrivkey": false, + "tryCaseFlip": true + } + ], + [ + "bcrt1s489d9fhmyel0vzfqsrmew4x7r80asuqesm5hgqacy35daflcyufh3j8cgdtflvt99ph05m", + "6028a9cad2a6fb267ef6092080f79754de19dfd8701986e97403b82468dea7f8271378c8f843569fb165", + { + "chain": "regtest", + "isPrivkey": false, + "tryCaseFlip": true + } + ], + [ + "1G9A9j6W8TLuh6dEeVwWeyibK1Uc5MfVFV", + "76a914a614da54daacdb8861f451a0b7e3c27cdf8a099e88ac", + { + "chain": "main", + "isPrivkey": false + } + ], + [ + "33GA3ZXbw5o5HeUrBEaqkWXFYYZmdxGRRP", + "a914113ca1afeb49ff3abf176ffa19c2a2b4df19712a87", + { + "chain": "main", + "isPrivkey": false + } + ], + [ + "mwgS2HRbjyfYxFnR1nF9VKLvmdgMfFBmGq", + "76a914b14ce7070b53cb0e4b5b5f6e253e876990aeca2e88ac", + { + "chain": "testnet4", + "isPrivkey": false + } + ], + [ + "2MwBVrJQ76BdaGD76CTmou8cZzQYLpe4NqU", + "a9142b2c149cde619eae3d7fe995243b76a3417541aa87", + { + "chain": "testnet4", + "isPrivkey": false + } + ], + [ + "mfnJ8tEkqKNFE5YaHTXFxyHk2mnDK2fvDh", + "76a91402e6cd77e649ad8b281271f158fc964ca3f66cb088ac", + { + "chain": "signet", + "isPrivkey": false + } + ], + [ + "2My83D67ir7K8PPzeT6mE2oth3ZwNTVRS9F", + "a9144074d84d32ff62da7b1b3c61925b934bfeb34b0587", + { + "chain": "signet", + "isPrivkey": false + } + ] +]