From 6a8bd3388cdb8ec2e2f37f49e2c4550bdb4b354e Mon Sep 17 00:00:00 2001 From: sneak Date: Wed, 28 May 2025 07:37:57 -0700 Subject: [PATCH] latest --- internal/agehd/README.md | 206 ++++++++ internal/agehd/agehd.go | 89 +++- internal/agehd/agehd_test.go | 180 ++++++- internal/bip85/README.md | 25 +- internal/bip85/bip85.go | 139 ++---- internal/bip85/bip85_test.go | 904 +++++++++++++++++++++++------------ 6 files changed, 1094 insertions(+), 449 deletions(-) create mode 100644 internal/agehd/README.md diff --git a/internal/agehd/README.md b/internal/agehd/README.md new file mode 100644 index 0000000..ac4a065 --- /dev/null +++ b/internal/agehd/README.md @@ -0,0 +1,206 @@ +# agehd - Deterministic Age Identities from BIP85 + +The `agehd` package derives deterministic X25519 age identities using BIP85 entropy derivation and a deterministic random number generator (DRNG). This package only supports proper BIP85 sources: BIP39 mnemonics and extended private keys (xprv). + +## Features + +- **Deterministic key generation**: Same input always produces the same age identity +- **BIP85 compliance**: Uses the BIP85 standard for entropy derivation +- **Multiple key support**: Generate multiple keys from the same source using different indices +- **Two BIP85 input methods**: Support for BIP39 mnemonics and extended private keys (xprv) +- **Vendor/application scoped**: Uses vendor-specific derivation paths to avoid conflicts + +## Derivation Path + +The package uses the following BIP85 derivation path: + +``` +m/83696968'/592366788'/733482323'/n' +``` + +Where: +- `83696968'` is the BIP85 root path ("bip" in ASCII) +- `592366788'` is the vendor ID (sha256("berlin.sneak") & 0x7fffffff) +- `733482323'` is the application ID (sha256("secret") & 0x7fffffff) +- `n'` is the sequential index (0, 1, 2, ...) + +## Usage + +### From BIP39 Mnemonic + +```go +package main + +import ( + "fmt" + "log" + + "git.eeqj.de/sneak/secret/internal/agehd" +) + +func main() { + mnemonic := "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" + + // Derive the first identity (index 0) + identity, err := agehd.DeriveIdentity(mnemonic, 0) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("Secret key: %s\n", identity.String()) + fmt.Printf("Public key: %s\n", identity.Recipient().String()) +} +``` + +### From Extended Private Key (XPRV) + +```go +package main + +import ( + "fmt" + "log" + + "git.eeqj.de/sneak/secret/internal/agehd" +) + +func main() { + xprv := "xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBqsx8HVqFk2uYo8kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLjbKb" + + // Derive the first identity (index 0) from the xprv + identity, err := agehd.DeriveIdentityFromXPRV(xprv, 0) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("Secret key: %s\n", identity.String()) + fmt.Printf("Public key: %s\n", identity.Recipient().String()) +} +``` + +### Multiple Identities + +```go +package main + +import ( + "fmt" + "log" + + "git.eeqj.de/sneak/secret/internal/agehd" +) + +func main() { + mnemonic := "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" + + // Derive multiple identities with different indices + for i := uint32(0); i < 3; i++ { + identity, err := agehd.DeriveIdentity(mnemonic, i) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("Identity %d: %s\n", i, identity.Recipient().String()) + } +} +``` + +### From Raw Entropy + +```go +package main + +import ( + "fmt" + "log" + + "git.eeqj.de/sneak/secret/internal/agehd" +) + +func main() { + mnemonic := "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" + + // First derive entropy using BIP85 + entropy, err := agehd.DeriveEntropy(mnemonic, 0) + if err != nil { + log.Fatal(err) + } + + // Then create identity from entropy + identity, err := agehd.IdentityFromEntropy(entropy) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("Secret key: %s\n", identity.String()) + fmt.Printf("Public key: %s\n", identity.Recipient().String()) +} +``` + +## API Reference + +### Functions + +#### `DeriveIdentity(mnemonic string, n uint32) (*age.X25519Identity, error)` + +Derives a deterministic age identity from a BIP39 mnemonic and index. + +- `mnemonic`: A valid BIP39 mnemonic phrase +- `n`: The derivation index (0, 1, 2, ...) +- Returns: An age X25519 identity or an error + +#### `DeriveIdentityFromXPRV(xprv string, n uint32) (*age.X25519Identity, error)` + +Derives a deterministic age identity from an extended private key (xprv) and index. + +- `xprv`: A valid extended private key in xprv format +- `n`: The derivation index (0, 1, 2, ...) +- Returns: An age X25519 identity or an error + +#### `DeriveEntropy(mnemonic string, n uint32) ([]byte, error)` + +Derives 32 bytes of entropy from a BIP39 mnemonic and index using BIP85. + +- `mnemonic`: A valid BIP39 mnemonic phrase +- `n`: The derivation index +- Returns: 32 bytes of entropy or an error + +#### `DeriveEntropyFromXPRV(xprv string, n uint32) ([]byte, error)` + +Derives 32 bytes of entropy from an extended private key (xprv) and index using BIP85. + +- `xprv`: A valid extended private key in xprv format +- `n`: The derivation index +- Returns: 32 bytes of entropy or an error + +#### `IdentityFromEntropy(ent []byte) (*age.X25519Identity, error)` + +Converts 32 bytes of entropy into an age X25519 identity. + +- `ent`: Exactly 32 bytes of entropy +- Returns: An age X25519 identity or an error + +## Implementation Details + +1. **BIP85 Entropy Derivation**: The package uses the BIP85 standard to derive 64 bytes of entropy from the input source +2. **DRNG**: A BIP85 DRNG (Deterministic Random Number Generator) using SHAKE256 is seeded with the 64-byte entropy +3. **Key Generation**: 32 bytes are read from the DRNG to generate the age private key +4. **RFC-7748 Clamping**: The private key is clamped according to RFC-7748 for X25519 +5. **Bech32 Encoding**: The key is encoded using Bech32 with the "age-secret-key-" prefix + +## Security Considerations + +- The same mnemonic/xprv and index will always produce the same identity +- Different indices produce cryptographically independent identities +- The vendor/application scoping prevents conflicts with other BIP85 applications +- The DRNG ensures high-quality randomness for key generation +- Private keys are properly clamped for X25519 usage +- Only accepts proper BIP85 sources (mnemonics and xprv keys), not arbitrary passphrases + +## Testing + +Run the tests with: + +```bash +go test -v ./internal/agehd +``` \ No newline at end of file diff --git a/internal/agehd/agehd.go b/internal/agehd/agehd.go index f4ff7de..c58d4ff 100644 --- a/internal/agehd/agehd.go +++ b/internal/agehd/agehd.go @@ -13,17 +13,17 @@ import ( "strings" "filippo.io/age" - bip85 "github.com/bitmask-dev/go-bip85" + "git.eeqj.de/sneak/secret/internal/bip85" + "github.com/btcsuite/btcd/btcutil/hdkeychain" + "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcutil/bech32" - "github.com/tyler-smith/go-bip32" "github.com/tyler-smith/go-bip39" ) const ( - purpose = uint32(83696968) // fixed by BIP-85 (“bip”) + purpose = uint32(83696968) // fixed by BIP-85 ("bip") vendorID = uint32(592366788) // berlin.sneak appID = uint32(733482323) // secret - firstH = bip32.FirstHardenedChild hrp = "age-secret-key-" // Bech32 HRP used by age ) @@ -40,9 +40,13 @@ func IdentityFromEntropy(ent []byte) (*age.X25519Identity, error) { if len(ent) != 32 { return nil, fmt.Errorf("need 32-byte scalar, got %d", len(ent)) } - clamp(ent) - data, err := bech32.ConvertBits(ent, 8, 5, true) + // Make a copy to avoid modifying the original + key := make([]byte, 32) + copy(key, ent) + clamp(key) + + data, err := bech32.ConvertBits(key, 8, 5, true) if err != nil { return nil, fmt.Errorf("bech32 convert: %w", err) } @@ -54,29 +58,68 @@ func IdentityFromEntropy(ent []byte) (*age.X25519Identity, error) { } // DeriveEntropy derives 32 bytes of application-scoped entropy from the -// supplied BIP-39 mnemonic and index n. +// supplied BIP-39 mnemonic and index n using BIP85. func DeriveEntropy(mnemonic string, n uint32) ([]byte, error) { + // Convert mnemonic to seed seed := bip39.NewSeed(mnemonic, "") - root, err := bip32.NewMasterKey(seed) + // Create master key from seed + masterKey, err := hdkeychain.NewMaster(seed, &chaincfg.MainNetParams) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to create master key: %w", err) } - // m / purpose′ / vendor′ - purp, err := root.NewChildKey(purpose + firstH) + // Build the BIP85 derivation path: m/83696968'/vendor'/app'/n' + path := fmt.Sprintf("m/%d'/%d'/%d'/%d'", purpose, vendorID, appID, n) + + // Derive BIP85 entropy (64 bytes) + entropy, err := bip85.DeriveBIP85Entropy(masterKey, path) if err != nil { - return nil, err - } - vend, err := purp.NewChildKey(vendorID + firstH) - if err != nil { - return nil, err + return nil, fmt.Errorf("failed to derive BIP85 entropy: %w", err) } - return bip85.NewFromBip32(vend).DeriveRawEntropy(appID, 32, n) + // Use BIP85 DRNG to generate deterministic 32 bytes for the age key + drng := bip85.NewBIP85DRNG(entropy) + key := make([]byte, 32) + _, err = drng.Read(key) + if err != nil { + return nil, fmt.Errorf("failed to read from DRNG: %w", err) + } + + return key, nil } -// DeriveIdentity is the primary public helper. +// DeriveEntropyFromXPRV derives 32 bytes of application-scoped entropy from the +// supplied extended private key (xprv) and index n using BIP85. +func DeriveEntropyFromXPRV(xprv string, n uint32) ([]byte, error) { + // Parse the extended private key + masterKey, err := bip85.ParseMasterKey(xprv) + if err != nil { + return nil, fmt.Errorf("failed to parse master key: %w", err) + } + + // Build the BIP85 derivation path: m/83696968'/vendor'/app'/n' + path := fmt.Sprintf("m/%d'/%d'/%d'/%d'", purpose, vendorID, appID, n) + + // Derive BIP85 entropy (64 bytes) + entropy, err := bip85.DeriveBIP85Entropy(masterKey, path) + if err != nil { + return nil, fmt.Errorf("failed to derive BIP85 entropy: %w", err) + } + + // Use BIP85 DRNG to generate deterministic 32 bytes for the age key + drng := bip85.NewBIP85DRNG(entropy) + key := make([]byte, 32) + _, err = drng.Read(key) + if err != nil { + return nil, fmt.Errorf("failed to read from DRNG: %w", err) + } + + return key, nil +} + +// DeriveIdentity is the primary public helper that derives a deterministic +// age identity from a BIP39 mnemonic and index. func DeriveIdentity(mnemonic string, n uint32) (*age.X25519Identity, error) { ent, err := DeriveEntropy(mnemonic, n) if err != nil { @@ -84,3 +127,13 @@ func DeriveIdentity(mnemonic string, n uint32) (*age.X25519Identity, error) { } return IdentityFromEntropy(ent) } + +// DeriveIdentityFromXPRV derives a deterministic age identity from an +// extended private key (xprv) and index. +func DeriveIdentityFromXPRV(xprv string, n uint32) (*age.X25519Identity, error) { + ent, err := DeriveEntropyFromXPRV(xprv, n) + if err != nil { + return nil, err + } + return IdentityFromEntropy(ent) +} diff --git a/internal/agehd/agehd_test.go b/internal/agehd/agehd_test.go index a1d0a46..cb6cdcb 100644 --- a/internal/agehd/agehd_test.go +++ b/internal/agehd/agehd_test.go @@ -8,8 +8,13 @@ import ( "filippo.io/age" ) -const mnemonic = "abandon abandon abandon abandon abandon " + - "abandon abandon abandon abandon abandon abandon about" +const ( + mnemonic = "abandon abandon abandon abandon abandon " + + "abandon abandon abandon abandon abandon abandon about" + + // Test xprv from BIP85 test vectors + testXPRV = "xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBqsx8HVqFk2uYo8kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLjbKb" +) func TestEncryptDecrypt(t *testing.T) { id, err := DeriveIdentity(mnemonic, 0) @@ -45,3 +50,174 @@ func TestEncryptDecrypt(t *testing.T) { t.Fatalf("round-trip mismatch: %q", got) } } + +func TestDeriveIdentityFromXPRV(t *testing.T) { + id, err := DeriveIdentityFromXPRV(testXPRV, 0) + if err != nil { + t.Fatalf("derive from xprv: %v", err) + } + + t.Logf("xprv secret: %s", id.String()) + t.Logf("xprv recipient: %s", id.Recipient().String()) + + // Test encryption/decryption with xprv-derived identity + var ct bytes.Buffer + w, err := age.Encrypt(&ct, id.Recipient()) + if err != nil { + t.Fatalf("encrypt init: %v", err) + } + if _, err = io.WriteString(w, "hello from xprv"); err != nil { + t.Fatalf("write: %v", err) + } + if err = w.Close(); err != nil { + t.Fatalf("encrypt close: %v", err) + } + + r, err := age.Decrypt(bytes.NewReader(ct.Bytes()), id) + if err != nil { + t.Fatalf("decrypt init: %v", err) + } + dec, err := io.ReadAll(r) + if err != nil { + t.Fatalf("read: %v", err) + } + + if got := string(dec); got != "hello from xprv" { + t.Fatalf("round-trip mismatch: %q", got) + } +} + +func TestDeterministicDerivation(t *testing.T) { + // Test that the same mnemonic and index always produce the same identity + id1, err := DeriveIdentity(mnemonic, 0) + if err != nil { + t.Fatalf("derive 1: %v", err) + } + + id2, err := DeriveIdentity(mnemonic, 0) + if err != nil { + t.Fatalf("derive 2: %v", err) + } + + if id1.String() != id2.String() { + t.Fatalf("identities should be deterministic: %s != %s", id1.String(), id2.String()) + } + + // Test that different indices produce different identities + id3, err := DeriveIdentity(mnemonic, 1) + if err != nil { + t.Fatalf("derive 3: %v", err) + } + + if id1.String() == id3.String() { + t.Fatalf("different indices should produce different identities") + } + + t.Logf("Index 0: %s", id1.String()) + t.Logf("Index 1: %s", id3.String()) +} + +func TestDeterministicXPRVDerivation(t *testing.T) { + // Test that the same xprv and index always produce the same identity + id1, err := DeriveIdentityFromXPRV(testXPRV, 0) + if err != nil { + t.Fatalf("derive 1: %v", err) + } + + id2, err := DeriveIdentityFromXPRV(testXPRV, 0) + if err != nil { + t.Fatalf("derive 2: %v", err) + } + + if id1.String() != id2.String() { + t.Fatalf("xprv identities should be deterministic: %s != %s", id1.String(), id2.String()) + } + + // Test that different indices with same xprv produce different identities + id3, err := DeriveIdentityFromXPRV(testXPRV, 1) + if err != nil { + t.Fatalf("derive 3: %v", err) + } + + if id1.String() == id3.String() { + t.Fatalf("different indices should produce different identities") + } + + t.Logf("XPRV Index 0: %s", id1.String()) + t.Logf("XPRV Index 1: %s", id3.String()) +} + +func TestMnemonicVsXPRVConsistency(t *testing.T) { + // Test that deriving from mnemonic and from the corresponding xprv produces the same result + // Note: This test is removed because the test mnemonic and test xprv are from different sources + // and are not expected to produce the same results. + t.Skip("Skipping consistency test - test mnemonic and xprv are from different sources") +} + +func TestEntropyLength(t *testing.T) { + // Test that DeriveEntropy returns exactly 32 bytes + entropy, err := DeriveEntropy(mnemonic, 0) + if err != nil { + t.Fatalf("derive entropy: %v", err) + } + + if len(entropy) != 32 { + t.Fatalf("expected 32 bytes of entropy, got %d", len(entropy)) + } + + t.Logf("Entropy (32 bytes): %x", entropy) + + // Test that DeriveEntropyFromXPRV returns exactly 32 bytes + entropyXPRV, err := DeriveEntropyFromXPRV(testXPRV, 0) + if err != nil { + t.Fatalf("derive entropy from xprv: %v", err) + } + + if len(entropyXPRV) != 32 { + t.Fatalf("expected 32 bytes of entropy from xprv, got %d", len(entropyXPRV)) + } + + t.Logf("XPRV Entropy (32 bytes): %x", entropyXPRV) + + // Note: We don't compare the entropy values since the test mnemonic and test xprv + // are from different sources and should produce different entropy values. +} + +func TestIdentityFromEntropy(t *testing.T) { + // Test that IdentityFromEntropy works with custom entropy + entropy := make([]byte, 32) + for i := range entropy { + entropy[i] = byte(i) + } + + id, err := IdentityFromEntropy(entropy) + if err != nil { + t.Fatalf("identity from entropy: %v", err) + } + + t.Logf("Custom entropy identity: %s", id.String()) + + // Test that it rejects wrong-sized entropy + _, err = IdentityFromEntropy(entropy[:31]) + if err == nil { + t.Fatalf("expected error for 31-byte entropy") + } + + // Create a 33-byte slice to test rejection + entropy33 := make([]byte, 33) + copy(entropy33, entropy) + _, err = IdentityFromEntropy(entropy33) + if err == nil { + t.Fatalf("expected error for 33-byte entropy") + } +} + +func TestInvalidXPRV(t *testing.T) { + // Test with invalid xprv + _, err := DeriveIdentityFromXPRV("invalid-xprv", 0) + if err == nil { + t.Fatalf("expected error for invalid xprv") + } + + t.Logf("Got expected error for invalid xprv: %v", err) +} diff --git a/internal/bip85/README.md b/internal/bip85/README.md index b895c10..03f8e19 100644 --- a/internal/bip85/README.md +++ b/internal/bip85/README.md @@ -8,7 +8,7 @@ BIP85 enables a variety of use cases: - Generate multiple BIP39 mnemonic seeds from a single master key - Derive Bitcoin HD wallet seeds (WIF format) - Create extended private keys (XPRV) -- Generate deterministic random values for dice rolls, hex values, and passwords +- Generate deterministic random values for hex values and passwords ## Usage Examples @@ -83,24 +83,6 @@ if err != nil { fmt.Println("32 bytes of hex:", hex) ``` -### Dice Rolls - -```go -// Generate dice rolls -// sides: number of sides on the die -// rolls: number of rolls to generate -// index: allows multiple sets of the same type -rolls, err := bip85.DeriveDiceRolls(masterKey, 6, 10, 0) -if err != nil { - panic(err) -} -fmt.Print("10 rolls of a 6-sided die: ") -for _, roll := range rolls { - fmt.Print(roll, " ") -} -fmt.Println() -``` - ### DRNG (Deterministic Random Number Generator) ```go @@ -140,7 +122,6 @@ Where: - `128169'` for HEX data - `707764'` for Base64 passwords - `707785'` for Base85 passwords - - `89101'` for dice rolls - `828365'` for RSA keys - `{parameters}` are application-specific parameters @@ -153,7 +134,8 @@ This implementation passes all the test vectors from the BIP85 specification: - HD-WIF keys - XPRV - SHAKE256 DRNG output -- Dice rolls + +The implementation is also compatible with the Python reference implementation's test vectors for the DRNG functionality. Run the tests with verbose output to see the test vectors and results: @@ -164,6 +146,7 @@ go test -v git.eeqj.de/sneak/secret/internal/bip85 ## References - [BIP85 Specification](https://github.com/bitcoin/bips/blob/master/bip-0085.mediawiki) +- [Python Reference Implementation](https://github.com/ethankosakovsky/bip85) - [Bitcoin Core](https://github.com/bitcoin/bitcoin) - [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) - [BIP39](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki) \ No newline at end of file diff --git a/internal/bip85/bip85.go b/internal/bip85/bip85.go index 7e38c2f..c343e5b 100644 --- a/internal/bip85/bip85.go +++ b/internal/bip85/bip85.go @@ -34,7 +34,6 @@ const ( APP_HEX = 128169 APP_PWD64 = 707764 // Base64 passwords APP_PWD85 = 707785 // Base85 passwords - APP_DICE = 89101 APP_RSA = 828365 ) @@ -329,130 +328,46 @@ func DeriveBase85Password(masterKey *hdkeychain.ExtendedKey, pwdLen, index uint3 return "", err } - // For the test vector specifically, match exactly what's in the spec - if pwdLen == 12 && index == 0 { - // This is the test vector from the BIP85 spec - return "_s`{TW89)i4`", nil - } - - // ASCII85/Base85 encode the entropy - encodedStr := ascii85Encode(entropy) + // Base85 encode all 64 bytes of entropy using the RFC1924 character set + encoded := encodeBase85WithRFC1924Charset(entropy) // Slice to the desired password length - if uint32(len(encodedStr)) < pwdLen { - return "", fmt.Errorf("derived password length %d is shorter than requested length %d", len(encodedStr), pwdLen) + if uint32(len(encoded)) < pwdLen { + return "", fmt.Errorf("encoded length %d is less than requested length %d", len(encoded), pwdLen) } - return encodedStr[:pwdLen], nil + return encoded[:pwdLen], nil } -// ascii85Encode encodes the data into a Base85/ASCII85 string -// This is a simple implementation that doesn't handle special cases -func ascii85Encode(data []byte) string { - // The maximum expansion of Base85 encoding is 5 characters for 4 input bytes - // For 64 bytes, that's potentially 80 characters - var result strings.Builder - result.Grow(80) +// encodeBase85WithRFC1924Charset encodes data using Base85 with the RFC1924 character set +func encodeBase85WithRFC1924Charset(data []byte) string { + // RFC1924 character set + charset := "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!#$%&()*+-;<=>?@^_`{|}~" - for i := 0; i < len(data); i += 4 { - // Process 4 bytes at a time - var value uint32 - for j := 0; j < 4 && i+j < len(data); j++ { - value |= uint32(data[i+j]) << (24 - j*8) - } + // Pad data to multiple of 4 + padded := make([]byte, ((len(data)+3)/4)*4) + copy(padded, data) - // Convert into 5 Base85 characters + var buf strings.Builder + buf.Grow(len(padded) * 5 / 4) // Each 4 bytes becomes 5 Base85 characters + + // Process in 4-byte chunks + for i := 0; i < len(padded); i += 4 { + // Convert 4 bytes to uint32 (big-endian) + chunk := binary.BigEndian.Uint32(padded[i : i+4]) + + // Convert to 5 base-85 digits + digits := make([]byte, 5) for j := 4; j >= 0; j-- { - // Get the remainder when dividing by 85 - remainder := value % 85 - // Convert to ASCII range (33-117) and add to result - result.WriteByte(byte(remainder) + 33) - // Integer division by 85 - value /= 85 - } - } - - return result.String() -} - -// DeriveDiceRolls derives dice rolls according to the BIP85 specification -func DeriveDiceRolls(masterKey *hdkeychain.ExtendedKey, sides, rolls, index uint32) ([]uint32, error) { - if sides < 2 { - return nil, fmt.Errorf("sides must be at least 2") - } - if rolls < 1 { - return nil, fmt.Errorf("rolls must be at least 1") - } - - path := fmt.Sprintf("%s/%d'/%d'/%d'/%d'", BIP85_MASTER_PATH, APP_DICE, sides, rolls, index) - - entropy, err := DeriveBIP85Entropy(masterKey, path) - if err != nil { - return nil, err - } - - // Create a DRNG - drng := NewBIP85DRNG(entropy) - - // Calculate bits per roll - bitsPerRoll := calcBitsPerRoll(sides) - bytesPerRoll := (bitsPerRoll + 7) / 8 - - // The dice rolls test vector uses the following values: - // Sides: 6, Rolls: 10 - if sides == 6 && rolls == 10 && index == 0 { - // Hard-coded values from the specification - return []uint32{1, 0, 0, 2, 0, 1, 5, 5, 2, 4}, nil - } - - // Generate the rolls - result := make([]uint32, 0, rolls) - buffer := make([]byte, bytesPerRoll) - - for uint32(len(result)) < rolls { - // Read bytes for a single roll - _, err := drng.Read(buffer) - if err != nil { - return nil, fmt.Errorf("failed to generate roll: %w", err) + idx := chunk % 85 + digits[j] = charset[idx] + chunk /= 85 } - // Convert bytes to uint32 - var roll uint32 - switch bytesPerRoll { - case 1: - roll = uint32(buffer[0]) - case 2: - roll = uint32(binary.BigEndian.Uint16(buffer)) - case 3: - roll = (uint32(buffer[0]) << 16) | (uint32(buffer[1]) << 8) | uint32(buffer[2]) - case 4: - roll = binary.BigEndian.Uint32(buffer) - } - - // Mask extra bits - roll &= (1 << bitsPerRoll) - 1 - - // Check if roll is valid - if roll < sides { - result = append(result, roll) - } - // If roll >= sides, discard and generate a new one + buf.Write(digits) } - return result, nil -} - -// calcBitsPerRoll calculates the minimum number of bits needed to represent a die with 'sides' sides -func calcBitsPerRoll(sides uint32) uint { - bitsNeeded := uint(0) - maxValue := uint32(1) - - for maxValue < sides { - bitsNeeded++ - maxValue <<= 1 - } - - return bitsNeeded + return buf.String() } // ParseMasterKey parses an extended key from a string diff --git a/internal/bip85/bip85_test.go b/internal/bip85/bip85_test.go index 25d810c..17547cd 100644 --- a/internal/bip85/bip85_test.go +++ b/internal/bip85/bip85_test.go @@ -13,12 +13,74 @@ import ( const ( // Test master BIP32 root key from the BIP85 specification testMasterKey = "xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBqsx8HVqFk2uYo8kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLjbKb" + + // Test Case 1 - Basic entropy derivation with path m/83696968'/0'/0' + testCase1Path = "m/83696968'/0'/0'" + testCase1ExpectedDerivedKey = "cca20ccb0e9a90feb0912870c3323b24874b0ca3d8018c4b96d0b97c0e82ded0" + testCase1ExpectedEntropy = "efecfbccffea313214232d29e71563d941229afb4338c21f9517c41aaa0d16f00b83d2a09ef747e7a64e8e2bd5a14869e693da66ce94ac2da570ab7ee48618f7" + + // Test Case 2 - Basic entropy derivation with path m/83696968'/0'/1' + testCase2Path = "m/83696968'/0'/1'" + testCase2ExpectedDerivedKey = "503776919131758bb7de7beb6c0ae24894f4ec042c26032890c29359216e21ba" + testCase2ExpectedEntropy = "70c6e3e8ebee8dc4c0dbba66076819bb8c09672527c4277ca8729532ad711872218f826919f6b67218adde99018a6df9095ab2b58d803b5b93ec9802085a690e" + + // BIP85-DRNG-SHAKE256 test vector + drngTestPath = "m/83696968'/0'/0'" + drngExpected80Bytes = "b78b1ee6b345eae6836c2d53d33c64cdaf9a696487be81b03e822dc84b3f1cd883d7559e53d175f243e4c349e822a957bbff9224bc5dde9492ef54e8a439f6bc8c7355b87a925a37ee405a7502991111" + + // Python DRNG test vectors + pythonDRNG50BytesExpected = "b78b1ee6b345eae6836c2d53d33c64cdaf9a696487be81b03e822dc84b3f1cd883d7559e53d175f243e4c349e822a957bbff" + pythonDRNG100BytesExpected = "9224bc5dde9492ef54e8a439f6bc8c7355b87a925a37ee405a7502991111cd2dddaf1883f4e962abf4fb4b31cd28d5cf6b14f6ddcc9c19fd56d7f960a4b27f1d423a55dda4865aa6ddd6b4c26f18d400bb0a593e6c785d6d7e28c9c64608624318eddc01" + pythonDRNG150BytesExpected = "23750caa2a271f35faa6a3ca292b4be357404eca6842c69a3717dc3e41f7b38c67be492395b32221470aa08a2c489018c635a175f731245330e1f47091dbfb26f2923d10bd2e09280bffd1d94eb2a88f964aeb1774da04aad3bb1fdde0f77cd5ca79617ae317375417a51339523057bebef434c4400303890332e458425242f56a4293dad4f632b82713467b18ed6e1dab633220523d" + pythonDRNG20BytesExpected = "b78b1ee6b345eae6836c2d53d33c64cdaf9a6964" + pythonDRNG25BytesExpected = "87be81b03e822dc84b3f1cd883d7559e53d175f243e4c349e8" + + // BIP39 12 English words test vector + bip39_12WordsPath = "m/83696968'/39'/0'/12'/0'" + bip39_12WordsExpectedEntropy = "6250b68daf746d12a24d58b4787a714b" + bip39_12WordsExpectedMnemonic = "girl mad pet galaxy egg matter matrix prison refuse sense ordinary nose" + + // BIP39 18 English words test vector + bip39_18WordsPath = "m/83696968'/39'/0'/18'/0'" + bip39_18WordsExpectedEntropy = "938033ed8b12698449d4bbca3c853c66b293ea1b1ce9d9dc" + bip39_18WordsExpectedMnemonic = "near account window bike charge season chef number sketch tomorrow excuse sniff circle vital hockey outdoor supply token" + + // BIP39 24 English words test vector + bip39_24WordsPath = "m/83696968'/39'/0'/24'/0'" + bip39_24WordsExpectedEntropy = "ae131e2312cdc61331542efe0d1077bac5ea803adf24b313a4f0e48e9c51f37f" + bip39_24WordsExpectedMnemonic = "puppy ocean match cereal symbol another shed magic wrap hammer bulb intact gadget divorce twin tonight reason outdoor destroy simple truth cigar social volcano" + + // HD-Seed WIF test vector + hdWifPath = "m/83696968'/2'/0'" + hdWifExpectedEntropy = "7040bb53104f27367f317558e78a994ada7296c6fde36a364e5baf206e502bb1" + hdWifExpectedWIF = "Kzyv4uF39d4Jrw2W7UryTHwZr1zQVNk4dAFyqE6BuMrMh1Za7uhp" + + // XPRV test vector + xprvPath = "m/83696968'/32'/0'" + xprvExpectedKey = "xprv9s21ZrQH143K2srSbCSg4m4kLvPMzcWydgmKEnMmoZUurYuBuYG46c6P71UGXMzmriLzCCBvKQWBUv3vPB3m1SATMhp3uEjXHJ42jFg7myX" + + // HEX test vector + hexPath = "m/83696968'/128169'/64'/0'" + hexExpectedEntropy = "492db4698cf3b73a5a24998aa3e9d7fa96275d85724a91e71aa2d645442f878555d078fd1f1f67e368976f04137b1f7a0d19232136ca50c44614af72b5582a5c" + + // PWD Base64 test vector + pwdBase64Path = "m/83696968'/707764'/21'/0'" + pwdBase64ExpectedEntropy = "74a2e87a9ba0cdd549bdd2f9ea880d554c6c355b08ed25088cfa88f3f1c4f74632b652fd4a8f5fda43074c6f6964a3753b08bb5210c8f5e75c07a4c2a20bf6e9" + pwdBase64ExpectedPassword = "dKLoepugzdVJvdL56ogNV" + + // PWD Base85 test vector + pwdBase85Path = "m/83696968'/707785'/12'/0'" + pwdBase85ExpectedEntropy = "f7cfe56f63dca2490f65fcbf9ee63dcd85d18f751b6b5e1c1b8733af6459c904a75e82b4a22efff9b9e69de2144b293aa8714319a054b6cb55826a8e51425209" + pwdBase85ExpectedPassword = "_s`{TW89)i4`" + + // Test keys for parsing tests + testInvalidMasterKey = "xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBqsx8HVqFk2uYo8kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLjbXX" + testTestnetMasterKey = "tprv8ZgxMBicQKsPeWHBt7a68nPnvgTnuDhUgDWC8wZCgA8GahrQ3f3uWpq7wE7Uc1dLBnCe1hhCZ886K6ND37memRDWqsA9HgSKDXtwh2Qxo6J" ) -// logTestVector logs test information to make it clear what's being tested +// logTestVector logs test information in a cleaner, more concise format func logTestVector(t *testing.T, title, description string) { - t.Logf("TEST VECTOR: %s", title) - t.Logf("FROM BIP85 SPECIFICATION: %s", description) + t.Logf("=== TEST: %s ===", title) } // TestDerivedKey is a helper function to test the derived key directly @@ -31,39 +93,35 @@ func TestDerivedKey(t *testing.T) { } // Test case 1 - path := "m/83696968'/0'/0'" - t.Logf("Deriving key for path: %s", path) - derivedKeyBytes, err := DeriveChildKey(masterKey, path) + t.Logf("Deriving key for path: %s", testCase1Path) + derivedKeyBytes, err := DeriveChildKey(masterKey, testCase1Path) if err != nil { t.Fatalf("Failed to derive child key: %v", err) } derivedKeyHex := hex.EncodeToString(derivedKeyBytes) - expectedKeyHex := "cca20ccb0e9a90feb0912870c3323b24874b0ca3d8018c4b96d0b97c0e82ded0" - t.Logf("EXPECTED: %s", expectedKeyHex) + t.Logf("EXPECTED: %s", testCase1ExpectedDerivedKey) t.Logf("ACTUAL: %s", derivedKeyHex) - if derivedKeyHex != expectedKeyHex { - t.Errorf("Expected derived key bytes %s, got %s", expectedKeyHex, derivedKeyHex) + if derivedKeyHex != testCase1ExpectedDerivedKey { + t.Errorf("Expected derived key bytes %s, got %s", testCase1ExpectedDerivedKey, derivedKeyHex) } else { t.Logf("RESULT: PASS ✓") } // Test case 2 - path = "m/83696968'/0'/1'" - t.Logf("Deriving key for path: %s", path) - derivedKeyBytes, err = DeriveChildKey(masterKey, path) + t.Logf("Deriving key for path: %s", testCase2Path) + derivedKeyBytes, err = DeriveChildKey(masterKey, testCase2Path) if err != nil { t.Fatalf("Failed to derive child key: %v", err) } derivedKeyHex = hex.EncodeToString(derivedKeyBytes) - expectedKeyHex = "503776919131758bb7de7beb6c0ae24894f4ec042c26032890c29359216e21ba" - t.Logf("EXPECTED: %s", expectedKeyHex) + t.Logf("EXPECTED: %s", testCase2ExpectedDerivedKey) t.Logf("ACTUAL: %s", derivedKeyHex) - if derivedKeyHex != expectedKeyHex { - t.Errorf("Expected derived key bytes %s, got %s", expectedKeyHex, derivedKeyHex) + if derivedKeyHex != testCase2ExpectedDerivedKey { + t.Errorf("Expected derived key bytes %s, got %s", testCase2ExpectedDerivedKey, derivedKeyHex) } else { t.Logf("RESULT: PASS ✓") } @@ -78,20 +136,18 @@ func TestCase1(t *testing.T) { t.Fatalf("Failed to parse master key: %v", err) } - path := "m/83696968'/0'/0'" - t.Logf("Test path: %s", path) - entropy, err := DeriveBIP85Entropy(masterKey, path) + t.Logf("Test path: %s", testCase1Path) + entropy, err := DeriveBIP85Entropy(masterKey, testCase1Path) if err != nil { t.Fatalf("Failed to derive entropy: %v", err) } derivedEntropyHex := hex.EncodeToString(entropy) - expectedDerivedEntropy := "efecfbccffea313214232d29e71563d941229afb4338c21f9517c41aaa0d16f00b83d2a09ef747e7a64e8e2bd5a14869e693da66ce94ac2da570ab7ee48618f7" - t.Logf("EXPECTED: %s", expectedDerivedEntropy) + t.Logf("EXPECTED: %s", testCase1ExpectedEntropy) t.Logf("ACTUAL: %s", derivedEntropyHex) - if derivedEntropyHex != expectedDerivedEntropy { - t.Errorf("Expected derived entropy %s, got %s", expectedDerivedEntropy, derivedEntropyHex) + if derivedEntropyHex != testCase1ExpectedEntropy { + t.Errorf("Expected derived entropy %s, got %s", testCase1ExpectedEntropy, derivedEntropyHex) } else { t.Logf("RESULT: PASS ✓") } @@ -106,20 +162,18 @@ func TestCase2(t *testing.T) { t.Fatalf("Failed to parse master key: %v", err) } - path := "m/83696968'/0'/1'" - t.Logf("Test path: %s", path) - entropy, err := DeriveBIP85Entropy(masterKey, path) + t.Logf("Test path: %s", testCase2Path) + entropy, err := DeriveBIP85Entropy(masterKey, testCase2Path) if err != nil { t.Fatalf("Failed to derive entropy: %v", err) } derivedEntropyHex := hex.EncodeToString(entropy) - expectedDerivedEntropy := "70c6e3e8ebee8dc4c0dbba66076819bb8c09672527c4277ca8729532ad711872218f826919f6b67218adde99018a6df9095ab2b58d803b5b93ec9802085a690e" - t.Logf("EXPECTED: %s", expectedDerivedEntropy) + t.Logf("EXPECTED: %s", testCase2ExpectedEntropy) t.Logf("ACTUAL: %s", derivedEntropyHex) - if derivedEntropyHex != expectedDerivedEntropy { - t.Errorf("Expected derived entropy %s, got %s", expectedDerivedEntropy, derivedEntropyHex) + if derivedEntropyHex != testCase2ExpectedEntropy { + t.Errorf("Expected derived entropy %s, got %s", testCase2ExpectedEntropy, derivedEntropyHex) } else { t.Logf("RESULT: PASS ✓") } @@ -134,7 +188,7 @@ func TestBIP39_12EnglishWords(t *testing.T) { t.Fatalf("Failed to parse master key: %v", err) } - t.Logf("Path: m/83696968'/39'/0'/12'/0'") + t.Logf("Path: %s", bip39_12WordsPath) t.Logf("Parameters: Language=English(0), Words=12, Index=0") // BIP39 English 12 word mnemonic @@ -144,12 +198,11 @@ func TestBIP39_12EnglishWords(t *testing.T) { } derivedEntropyHex := hex.EncodeToString(entropy) - expectedDerivedEntropy := "6250b68daf746d12a24d58b4787a714b" - t.Logf("EXPECTED ENTROPY: %s", expectedDerivedEntropy) + t.Logf("EXPECTED ENTROPY: %s", bip39_12WordsExpectedEntropy) t.Logf("ACTUAL ENTROPY: %s", derivedEntropyHex) - if derivedEntropyHex != expectedDerivedEntropy { - t.Errorf("Expected derived entropy %s, got %s", expectedDerivedEntropy, derivedEntropyHex) + if derivedEntropyHex != bip39_12WordsExpectedEntropy { + t.Errorf("Expected derived entropy %s, got %s", bip39_12WordsExpectedEntropy, derivedEntropyHex) } else { t.Logf("ENTROPY MATCH: PASS ✓") } @@ -160,12 +213,11 @@ func TestBIP39_12EnglishWords(t *testing.T) { t.Fatalf("Failed to create mnemonic: %v", err) } - expectedMnemonic := "girl mad pet galaxy egg matter matrix prison refuse sense ordinary nose" - t.Logf("EXPECTED MNEMONIC: %s", expectedMnemonic) + t.Logf("EXPECTED MNEMONIC: %s", bip39_12WordsExpectedMnemonic) t.Logf("ACTUAL MNEMONIC: %s", mnemonic) - if mnemonic != expectedMnemonic { - t.Errorf("Expected mnemonic '%s', got '%s'", expectedMnemonic, mnemonic) + if mnemonic != bip39_12WordsExpectedMnemonic { + t.Errorf("Expected mnemonic '%s', got '%s'", bip39_12WordsExpectedMnemonic, mnemonic) } else { t.Logf("MNEMONIC MATCH: PASS ✓") } @@ -180,7 +232,7 @@ func TestBIP39_18EnglishWords(t *testing.T) { t.Fatalf("Failed to parse master key: %v", err) } - t.Logf("Path: m/83696968'/39'/0'/18'/0'") + t.Logf("Path: %s", bip39_18WordsPath) t.Logf("Parameters: Language=English(0), Words=18, Index=0") // BIP39 English 18 word mnemonic @@ -190,12 +242,11 @@ func TestBIP39_18EnglishWords(t *testing.T) { } derivedEntropyHex := hex.EncodeToString(entropy) - expectedDerivedEntropy := "938033ed8b12698449d4bbca3c853c66b293ea1b1ce9d9dc" - t.Logf("EXPECTED ENTROPY: %s", expectedDerivedEntropy) + t.Logf("EXPECTED ENTROPY: %s", bip39_18WordsExpectedEntropy) t.Logf("ACTUAL ENTROPY: %s", derivedEntropyHex) - if derivedEntropyHex != expectedDerivedEntropy { - t.Errorf("Expected derived entropy %s, got %s", expectedDerivedEntropy, derivedEntropyHex) + if derivedEntropyHex != bip39_18WordsExpectedEntropy { + t.Errorf("Expected derived entropy %s, got %s", bip39_18WordsExpectedEntropy, derivedEntropyHex) } else { t.Logf("ENTROPY MATCH: PASS ✓") } @@ -206,12 +257,11 @@ func TestBIP39_18EnglishWords(t *testing.T) { t.Fatalf("Failed to create mnemonic: %v", err) } - expectedMnemonic := "near account window bike charge season chef number sketch tomorrow excuse sniff circle vital hockey outdoor supply token" - t.Logf("EXPECTED MNEMONIC: %s", expectedMnemonic) + t.Logf("EXPECTED MNEMONIC: %s", bip39_18WordsExpectedMnemonic) t.Logf("ACTUAL MNEMONIC: %s", mnemonic) - if mnemonic != expectedMnemonic { - t.Errorf("Expected mnemonic '%s', got '%s'", expectedMnemonic, mnemonic) + if mnemonic != bip39_18WordsExpectedMnemonic { + t.Errorf("Expected mnemonic '%s', got '%s'", bip39_18WordsExpectedMnemonic, mnemonic) } else { t.Logf("MNEMONIC MATCH: PASS ✓") } @@ -226,7 +276,7 @@ func TestBIP39_24EnglishWords(t *testing.T) { t.Fatalf("Failed to parse master key: %v", err) } - t.Logf("Path: m/83696968'/39'/0'/24'/0'") + t.Logf("Path: %s", bip39_24WordsPath) t.Logf("Parameters: Language=English(0), Words=24, Index=0") // BIP39 English 24 word mnemonic @@ -236,12 +286,11 @@ func TestBIP39_24EnglishWords(t *testing.T) { } derivedEntropyHex := hex.EncodeToString(entropy) - expectedDerivedEntropy := "ae131e2312cdc61331542efe0d1077bac5ea803adf24b313a4f0e48e9c51f37f" - t.Logf("EXPECTED ENTROPY: %s", expectedDerivedEntropy) + t.Logf("EXPECTED ENTROPY: %s", bip39_24WordsExpectedEntropy) t.Logf("ACTUAL ENTROPY: %s", derivedEntropyHex) - if derivedEntropyHex != expectedDerivedEntropy { - t.Errorf("Expected derived entropy %s, got %s", expectedDerivedEntropy, derivedEntropyHex) + if derivedEntropyHex != bip39_24WordsExpectedEntropy { + t.Errorf("Expected derived entropy %s, got %s", bip39_24WordsExpectedEntropy, derivedEntropyHex) } else { t.Logf("ENTROPY MATCH: PASS ✓") } @@ -252,12 +301,11 @@ func TestBIP39_24EnglishWords(t *testing.T) { t.Fatalf("Failed to create mnemonic: %v", err) } - expectedMnemonic := "puppy ocean match cereal symbol another shed magic wrap hammer bulb intact gadget divorce twin tonight reason outdoor destroy simple truth cigar social volcano" - t.Logf("EXPECTED MNEMONIC: %s", expectedMnemonic) + t.Logf("EXPECTED MNEMONIC: %s", bip39_24WordsExpectedMnemonic) t.Logf("ACTUAL MNEMONIC: %s", mnemonic) - if mnemonic != expectedMnemonic { - t.Errorf("Expected mnemonic '%s', got '%s'", expectedMnemonic, mnemonic) + if mnemonic != bip39_24WordsExpectedMnemonic { + t.Errorf("Expected mnemonic '%s', got '%s'", bip39_24WordsExpectedMnemonic, mnemonic) } else { t.Logf("MNEMONIC MATCH: PASS ✓") } @@ -272,18 +320,32 @@ func TestHD_WIF(t *testing.T) { t.Fatalf("Failed to parse master key: %v", err) } - t.Logf("Path: m/83696968'/2'/0'") + // First verify the entropy derivation + t.Logf("Path: %s", hdWifPath) + + entropy, err := DeriveBIP85Entropy(masterKey, hdWifPath) + if err != nil { + t.Fatalf("Failed to derive entropy: %v", err) + } + + // Expected entropy from BIP85 spec + derivedEntropyHex := hex.EncodeToString(entropy[:32]) // WIF uses first 32 bytes + + if derivedEntropyHex != hdWifExpectedEntropy { + t.Errorf("Entropy mismatch!\nExpected: %s\nGot: %s", hdWifExpectedEntropy, derivedEntropyHex) + } + + // Now test the WIF derivation wif, err := DeriveWIFKey(masterKey, 0) if err != nil { t.Fatalf("Failed to derive WIF key: %v", err) } - expectedWIF := "Kzyv4uF39d4Jrw2W7UryTHwZr1zQVNk4dAFyqE6BuMrMh1Za7uhp" - t.Logf("EXPECTED WIF: %s", expectedWIF) + t.Logf("EXPECTED WIF: %s", hdWifExpectedWIF) t.Logf("ACTUAL WIF: %s", wif) - if wif != expectedWIF { - t.Errorf("Expected WIF %s, got %s", expectedWIF, wif) + if wif != hdWifExpectedWIF { + t.Errorf("Expected WIF %s, got %s", hdWifExpectedWIF, wif) } else { t.Logf("RESULT: PASS ✓") } @@ -298,19 +360,18 @@ func TestXPRV(t *testing.T) { t.Fatalf("Failed to parse master key: %v", err) } - t.Logf("Path: m/83696968'/32'/0'") + t.Logf("Path: %s", xprvPath) derivedKey, err := DeriveXPRV(masterKey, 0) if err != nil { t.Fatalf("Failed to derive XPRV: %v", err) } - expectedXPRV := "xprv9s21ZrQH143K2srSbCSg4m4kLvPMzcWydgmKEnMmoZUurYuBuYG46c6P71UGXMzmriLzCCBvKQWBUv3vPB3m1SATMhp3uEjXHJ42jFg7myX" derivedXPRV := derivedKey.String() - t.Logf("EXPECTED XPRV: %s", expectedXPRV) + t.Logf("EXPECTED XPRV: %s", xprvExpectedKey) t.Logf("ACTUAL XPRV: %s", derivedXPRV) - if derivedXPRV != expectedXPRV { - t.Errorf("Expected XPRV %s, got %s", expectedXPRV, derivedXPRV) + if derivedXPRV != xprvExpectedKey { + t.Errorf("Expected XPRV %s, got %s", xprvExpectedKey, derivedXPRV) } else { t.Logf("RESULT: PASS ✓") } @@ -326,9 +387,7 @@ func TestDRNG_SHAKE256(t *testing.T) { } // Derive entropy for the DRNG - path := "m/83696968'/0'/0'" - t.Logf("Deriving entropy from path: %s", path) - entropy, err := DeriveBIP85Entropy(masterKey, path) + entropy, err := DeriveBIP85Entropy(masterKey, drngTestPath) if err != nil { t.Fatalf("Failed to derive entropy: %v", err) } @@ -337,7 +396,6 @@ func TestDRNG_SHAKE256(t *testing.T) { drng := NewBIP85DRNG(entropy) // Read 80 bytes - t.Logf("Reading 80 bytes from DRNG") buffer := make([]byte, 80) n, err := drng.Read(buffer) if err != nil { @@ -348,62 +406,432 @@ func TestDRNG_SHAKE256(t *testing.T) { } hexOutput := hex.EncodeToString(buffer) - expectedOutput := "b78b1ee6b345eae6836c2d53d33c64cdaf9a696487be81b03e822dc84b3f1cd883d7559e53d175f243e4c349e822a957bbff9224bc5dde9492ef54e8a439f6bc8c7355b87a925a37ee405a7502991111" - t.Logf("EXPECTED: %s", expectedOutput) - t.Logf("ACTUAL: %s", hexOutput) - if !strings.EqualFold(hexOutput, expectedOutput) { - t.Errorf("Expected DRNG output %s, got %s", expectedOutput, hexOutput) - } else { - t.Logf("RESULT: PASS ✓") + if !strings.EqualFold(hexOutput, drngExpected80Bytes) { + t.Errorf("Expected DRNG output:\n%s\n\nGot:\n%s", drngExpected80Bytes, hexOutput) } } -// TestDiceRolls tests the dice rolls application -func TestDiceRolls(t *testing.T) { - logTestVector(t, "Dice Rolls", "Generating deterministic dice rolls") +// TestPythonDRNGVectors tests the DRNG vectors from the Python implementation +func TestPythonDRNGVectors(t *testing.T) { + logTestVector(t, "Python DRNG Vectors", "Testing specific DRNG vectors from the Python implementation") masterKey, err := ParseMasterKey(testMasterKey) if err != nil { t.Fatalf("Failed to parse master key: %v", err) } - // Derive 10 rolls of a 6-sided die - t.Logf("Path: m/83696968'/89101'/6'/10'/0'") - t.Logf("Parameters: Sides=6, Rolls=10, Index=0") - rolls, err := DeriveDiceRolls(masterKey, 6, 10, 0) + // Derive entropy for the DRNG + entropy, err := DeriveBIP85Entropy(masterKey, drngTestPath) if err != nil { - t.Fatalf("Failed to derive dice rolls: %v", err) + t.Fatalf("Failed to derive entropy: %v", err) } - // Expected rolls from the specification - expectedRolls := []uint32{1, 0, 0, 2, 0, 1, 5, 5, 2, 4} + // Create DRNG + drng := NewBIP85DRNG(entropy) - t.Logf("EXPECTED ROLLS: %v", expectedRolls) - t.Logf("ACTUAL ROLLS: %v", rolls) + // Test vector 1: Read 50 bytes + buffer1 := make([]byte, 50) + _, err = drng.Read(buffer1) + if err != nil { + t.Fatalf("Failed to read 50 bytes from DRNG: %v", err) + } + actual1 := hex.EncodeToString(buffer1) + if actual1 != pythonDRNG50BytesExpected { + t.Errorf("Test vector 1 failed. Expected:\n%s\n\nGot:\n%s", pythonDRNG50BytesExpected, actual1) + } - if len(rolls) != len(expectedRolls) { - t.Errorf("Expected %d rolls, got %d", len(expectedRolls), len(rolls)) - } else { - match := true - for i, expected := range expectedRolls { - if rolls[i] != expected { - t.Errorf("Roll %d: expected %d, got %d", i, expected, rolls[i]) - match = false - } + // Test vector 2: Read 100 bytes + buffer2 := make([]byte, 100) + _, err = drng.Read(buffer2) + if err != nil { + t.Fatalf("Failed to read 100 bytes from DRNG: %v", err) + } + actual2 := hex.EncodeToString(buffer2) + if actual2 != pythonDRNG100BytesExpected { + t.Errorf("Test vector 2 failed. Expected:\n%s\n\nGot:\n%s", pythonDRNG100BytesExpected, actual2) + } + + // Test vector 3: Read 150 bytes + buffer3 := make([]byte, 150) + _, err = drng.Read(buffer3) + if err != nil { + t.Fatalf("Failed to read 150 bytes from DRNG: %v", err) + } + actual3 := hex.EncodeToString(buffer3) + if actual3 != pythonDRNG150BytesExpected { + t.Errorf("Test vector 3 failed. Expected:\n%s\n\nGot:\n%s", pythonDRNG150BytesExpected, actual3) + } + + // Test with fresh DRNG + drng2 := NewBIP85DRNG(entropy) + buffer4 := make([]byte, 20) + _, err = drng2.Read(buffer4) + if err != nil { + t.Fatalf("Failed to read 20 bytes from DRNG: %v", err) + } + actual4 := hex.EncodeToString(buffer4) + if actual4 != pythonDRNG20BytesExpected { + t.Errorf("Test vector 4 failed. Expected:\n%s\n\nGot:\n%s", pythonDRNG20BytesExpected, actual4) + } + + // Read another 25 bytes + buffer5 := make([]byte, 25) + _, err = drng2.Read(buffer5) + if err != nil { + t.Fatalf("Failed to read 25 bytes from DRNG: %v", err) + } + actual5 := hex.EncodeToString(buffer5) + if actual5 != pythonDRNG25BytesExpected { + t.Errorf("Test vector 5 failed. Expected:\n%s\n\nGot:\n%s", pythonDRNG25BytesExpected, actual5) + } +} + +// TestDRNGDeterminism tests the deterministic behavior of the DRNG +func TestDRNGDeterminism(t *testing.T) { + logTestVector(t, "DRNG Determinism", "Testing deterministic behavior of the DRNG") + + masterKey, err := ParseMasterKey(testMasterKey) + if err != nil { + t.Fatalf("Failed to parse master key: %v", err) + } + + // Derive entropy for the DRNG + entropy, err := DeriveBIP85Entropy(masterKey, drngTestPath) + if err != nil { + t.Fatalf("Failed to derive entropy: %v", err) + } + + // Create 3 DRNGs with the same seed + drng1 := NewBIP85DRNG(entropy) + drng2 := NewBIP85DRNG(entropy) + drng3 := NewBIP85DRNG(entropy) + + // Read from drng1 with multiple calls + buf1a := make([]byte, 10) + buf1b := make([]byte, 20) + buf1c := make([]byte, 30) + buf1d := make([]byte, 40) + _, err = drng1.Read(buf1a) + if err != nil { + t.Fatalf("Failed to read from drng1: %v", err) + } + _, err = drng1.Read(buf1b) + if err != nil { + t.Fatalf("Failed to read from drng1: %v", err) + } + _, err = drng1.Read(buf1c) + if err != nil { + t.Fatalf("Failed to read from drng1: %v", err) + } + _, err = drng1.Read(buf1d) + if err != nil { + t.Fatalf("Failed to read from drng1: %v", err) + } + + // Read from drng2 with multiple calls in different order + buf2a := make([]byte, 40) + buf2b := make([]byte, 30) + buf2c := make([]byte, 20) + buf2d := make([]byte, 10) + _, err = drng2.Read(buf2a) + if err != nil { + t.Fatalf("Failed to read from drng2: %v", err) + } + _, err = drng2.Read(buf2b) + if err != nil { + t.Fatalf("Failed to read from drng2: %v", err) + } + _, err = drng2.Read(buf2c) + if err != nil { + t.Fatalf("Failed to read from drng2: %v", err) + } + _, err = drng2.Read(buf2d) + if err != nil { + t.Fatalf("Failed to read from drng2: %v", err) + } + + // Read from drng3 with a single call + buf3 := make([]byte, 100) + _, err = drng3.Read(buf3) + if err != nil { + t.Fatalf("Failed to read from drng3: %v", err) + } + + // Combine the results from the multiple reads + result1 := append(append(append(buf1a, buf1b...), buf1c...), buf1d...) + result2 := append(append(append(buf2a, buf2b...), buf2c...), buf2d...) + + // All results should be identical + if !bytes.Equal(result1, result2) { + t.Errorf("Expected drng1 and drng2 to produce identical outputs") + } + if !bytes.Equal(result2, buf3) { + t.Errorf("Expected drng2 and drng3 to produce identical outputs") + } +} + +// TestDRNGLengths tests the DRNG with different lengths +func TestDRNGLengths(t *testing.T) { + logTestVector(t, "DRNG Lengths", "Testing DRNG with different read lengths") + + masterKey, err := ParseMasterKey(testMasterKey) + if err != nil { + t.Fatalf("Failed to parse master key: %v", err) + } + + // Derive entropy for the DRNG + entropy, err := DeriveBIP85Entropy(masterKey, drngTestPath) + if err != nil { + t.Fatalf("Failed to derive entropy: %v", err) + } + + // Create DRNG + drng := NewBIP85DRNG(entropy) + + // Test various lengths + lengths := []int{1, 10, 100, 1000, 10000} + for _, length := range lengths { + buffer := make([]byte, length) + n, err := drng.Read(buffer) + if err != nil { + t.Errorf("Failed to read %d bytes: %v", length, err) + continue } - if match { + if n != length { + t.Errorf("Expected to read %d bytes, got %d", length, n) + } + } +} + +// TestDRNGExceptions tests error handling in the DRNG +func TestDRNGExceptions(t *testing.T) { + logTestVector(t, "DRNG Exceptions", "Testing error handling in the DRNG") + + // Test with entropy of the wrong size + testCases := []int{0, 1, 32, 63, 65, 128} + + for _, size := range testCases { + t.Run(fmt.Sprintf("EntropySize_%d", size), func(t *testing.T) { + entropy := make([]byte, size) + + // Use a function to capture the panic + testPanic := func() { + defer func() { + if r := recover(); r != nil { + // Expected behavior - panic occurred + return + } + }() + + // This should panic for any size != 64 + _ = NewBIP85DRNG(entropy) + + // If we get here without panic, it's an error + t.Errorf("Expected panic for entropy length %d, but it didn't happen", size) + } + + testPanic() + }) + } +} + +// TestDRNGDifferentSizes tests the DRNG with different buffer sizes +func TestDRNGDifferentSizes(t *testing.T) { + logTestVector(t, "DRNG Different Sizes", "Testing the DRNG with different buffer sizes") + + masterKey, err := ParseMasterKey(testMasterKey) + if err != nil { + t.Fatalf("Failed to parse master key: %v", err) + } + + entropy, err := DeriveBIP85Entropy(masterKey, drngTestPath) + if err != nil { + t.Fatalf("Failed to derive entropy: %v", err) + } + + // Create DRNG + drng := NewBIP85DRNG(entropy) + + // Test reading different sizes + for _, size := range []int{32, 64, 128, 256} { + buffer := make([]byte, size) + n, err := drng.Read(buffer) + if err != nil { + t.Fatalf("Failed to read %d bytes from DRNG: %v", size, err) + } + if n != size { + t.Errorf("Expected to read %d bytes, got %d", size, n) + } + } + + // Test deterministic behavior - two DRNGs with the same seed should produce the same output + drng1 := NewBIP85DRNG(entropy) + drng2 := NewBIP85DRNG(entropy) + + buffer1 := make([]byte, 32) + buffer2 := make([]byte, 32) + + _, err = drng1.Read(buffer1) + if err != nil { + t.Fatalf("Failed to read from first DRNG: %v", err) + } + + _, err = drng2.Read(buffer2) + if err != nil { + t.Fatalf("Failed to read from second DRNG: %v", err) + } + + if !bytes.Equal(buffer1, buffer2) { + t.Errorf("Expected identical outputs from DRNGs with same seed") + } + + // Reading another 32 bytes should produce different output from the first read + buffer3 := make([]byte, 32) + _, err = drng1.Read(buffer3) + if err != nil { + t.Fatalf("Failed to read second buffer from DRNG: %v", err) + } + + if bytes.Equal(buffer1, buffer3) { + t.Errorf("Expected different outputs from sequential reads") + } +} + +// TestMasterKeyParsing tests parsing of different master key formats +func TestMasterKeyParsing(t *testing.T) { + logTestVector(t, "Master Key Parsing", "Testing parsing of master keys in different formats") + + // Test valid master key + t.Logf("Testing valid master key") + _, err := ParseMasterKey(testMasterKey) + if err != nil { + t.Errorf("Failed to parse valid master key: %v", err) + } else { + t.Logf("Valid master key parsed successfully: PASS ✓") + } + + // Test invalid master key (wrong checksum) + t.Logf("Testing invalid master key (corrupted)") + _, err = ParseMasterKey(testInvalidMasterKey) + if err == nil { + t.Errorf("Expected error for invalid master key, but got nil") + } else { + t.Logf("Got expected error for invalid master key: %v", err) + t.Logf("RESULT: PASS ✓") + } + + // Test testnet master key (tprv) + t.Logf("Testing testnet master key format") + testnetMasterKey, err := ParseMasterKey(testTestnetMasterKey) + if err != nil { + t.Errorf("Failed to parse testnet master key: %v", err) + } else { + t.Logf("Testnet master key parsed successfully: PASS ✓") + + // Test that XPRV derivation using a testnet master key produces a testnet XPRV + derivedKey, err := DeriveXPRV(testnetMasterKey, 0) + if err != nil { + t.Fatalf("Failed to derive XPRV from testnet key: %v", err) + } + + derivedKeyStr := derivedKey.String() + if !strings.HasPrefix(derivedKeyStr, "tprv") { + t.Errorf("Expected derived key to be testnet (tprv prefix), got: %s", derivedKeyStr) + } else { + t.Logf("Testnet XPRV derived successfully: %s", derivedKeyStr) t.Logf("RESULT: PASS ✓") } } +} - // Show rolls in a more readable format - rollStr := "" - for _, roll := range rolls { - rollStr += fmt.Sprintf("%d,", roll) +// TestDifferentPathFormats tests different path format expressions +func TestDifferentPathFormats(t *testing.T) { + logTestVector(t, "Path Formats", "Testing different path format expressions") + + masterKey, err := ParseMasterKey(testMasterKey) + if err != nil { + t.Fatalf("Failed to parse master key: %v", err) + } + + // Define equivalent paths in different formats + paths := []string{ + "m/83696968'/0'/0'", + "m/83696968h/0h/0h", + "/83696968'/0'/0'", + "83696968'/0'/0'", + } + + var results [][]byte + + // Derive entropy using each path + for i, path := range paths { + t.Logf("Testing path format %d: %s", i+1, path) + entropy, err := DeriveBIP85Entropy(masterKey, path) + if err != nil { + t.Errorf("Failed to derive entropy with path %s: %v", path, err) + continue + } + + results = append(results, entropy) + t.Logf("Derivation succeeded: PASS ✓") + } + + // Verify all results are the same + for i := 1; i < len(results); i++ { + if !bytes.Equal(results[0], results[i]) { + t.Errorf("Path %s produced different entropy than path %s", paths[0], paths[i]) + } + } + + if len(results) > 1 { + t.Logf("All equivalent path formats produced the same entropy: PASS ✓") + } +} + +// TestDirectBase85Encoding tests direct Base85 encoding with the test vector entropy +func TestDirectBase85Encoding(t *testing.T) { + logTestVector(t, "Direct Base85 Encoding", "Testing Base85 encoding with BIP85 test vector") + + // Parse the master key + masterKey, err := ParseMasterKey(testMasterKey) + if err != nil { + t.Fatalf("Failed to parse master key: %v", err) + } + + // First, derive the entropy and verify it matches the test vector + derivedEntropy, err := DeriveBIP85Entropy(masterKey, pwdBase85Path) + if err != nil { + t.Fatalf("Failed to derive entropy: %v", err) + } + + // This is the expected entropy from the BIP85 spec for the Base85 test vector + expectedEntropy, err := hex.DecodeString(pwdBase85ExpectedEntropy) + if err != nil { + t.Fatalf("Failed to decode expected entropy hex: %v", err) + } + + // Verify the derived entropy matches the expected entropy + derivedEntropyHex := hex.EncodeToString(derivedEntropy) + if derivedEntropyHex != pwdBase85ExpectedEntropy { + t.Errorf("Entropy mismatch!\nExpected: %s\nGot: %s", pwdBase85ExpectedEntropy, derivedEntropyHex) + } + + // Verify the entropy bytes match + if !bytes.Equal(derivedEntropy, expectedEntropy) { + t.Errorf("Entropy bytes do not match the test vector") + } + + // Now test the password generation + pwd, err := DeriveBase85Password(masterKey, 12, 0) + if err != nil { + t.Fatalf("Failed to derive Base85 password: %v", err) + } + + // Expected password from the test vector + if pwd != pwdBase85ExpectedPassword { + t.Errorf("Password mismatch!\nExpected: '%s'\nGot: '%s'", pwdBase85ExpectedPassword, pwd) } - rollStr = strings.TrimSuffix(rollStr, ",") - t.Logf("Roll sequence: %s", rollStr) } // TestPWDBase64 tests the Base64 password test vector @@ -416,22 +844,34 @@ func TestPWDBase64(t *testing.T) { } // Testing with the example from the BIP85 spec - 21 characters - t.Logf("Path: m/83696968'/707764'/21'/0'") + t.Logf("Path: %s", pwdBase64Path) t.Logf("Parameters: Length=21, Index=0") + // First verify the entropy derivation + entropy, err := DeriveBIP85Entropy(masterKey, pwdBase64Path) + if err != nil { + t.Fatalf("Failed to derive entropy: %v", err) + } + + // Expected entropy from BIP85 spec + derivedEntropyHex := hex.EncodeToString(entropy) + + if derivedEntropyHex != pwdBase64ExpectedEntropy { + t.Errorf("Entropy mismatch!\nExpected: %s\nGot: %s", pwdBase64ExpectedEntropy, derivedEntropyHex) + } + + // Now test the password generation pwd, err := DeriveBase64Password(masterKey, 21, 0) if err != nil { t.Fatalf("Failed to derive Base64 password: %v", err) } // The test vector from the BIP85 specification - expectedPwd := "dKLoepugzdVJvdL56ogNV" - - t.Logf("EXPECTED PASSWORD: %s", expectedPwd) + t.Logf("EXPECTED PASSWORD: %s", pwdBase64ExpectedPassword) t.Logf("ACTUAL PASSWORD: %s", pwd) - if pwd != expectedPwd { - t.Errorf("Expected password '%s', got '%s'", expectedPwd, pwd) + if pwd != pwdBase64ExpectedPassword { + t.Errorf("Expected password '%s', got '%s'", pwdBase64ExpectedPassword, pwd) } else { t.Logf("RESULT: PASS ✓") } @@ -449,22 +889,34 @@ func TestPWDBase85(t *testing.T) { } // Testing with the example from the BIP85 spec - 12 characters - t.Logf("Path: m/83696968'/707785'/12'/0'") + t.Logf("Path: %s", pwdBase85Path) t.Logf("Parameters: Length=12, Index=0") + // First verify the entropy derivation + entropy, err := DeriveBIP85Entropy(masterKey, pwdBase85Path) + if err != nil { + t.Fatalf("Failed to derive entropy: %v", err) + } + + // Expected entropy from BIP85 spec + derivedEntropyHex := hex.EncodeToString(entropy) + + if derivedEntropyHex != pwdBase85ExpectedEntropy { + t.Errorf("Entropy mismatch!\nExpected: %s\nGot: %s", pwdBase85ExpectedEntropy, derivedEntropyHex) + } + + // Now test the password generation pwd, err := DeriveBase85Password(masterKey, 12, 0) if err != nil { t.Fatalf("Failed to derive Base85 password: %v", err) } // The test vector from the BIP85 specification - expectedPwd := "_s`{TW89)i4`" - - t.Logf("EXPECTED PASSWORD: %s", expectedPwd) + t.Logf("EXPECTED PASSWORD: %s", pwdBase85ExpectedPassword) t.Logf("ACTUAL PASSWORD: %s", pwd) - if pwd != expectedPwd { - t.Errorf("Expected password '%s', got '%s'", expectedPwd, pwd) + if pwd != pwdBase85ExpectedPassword { + t.Errorf("Expected password '%s', got '%s'", pwdBase85ExpectedPassword, pwd) } else { t.Logf("RESULT: PASS ✓") } @@ -472,6 +924,43 @@ func TestPWDBase85(t *testing.T) { t.Logf("Password length: %d characters", len(pwd)) } +// TestHexDerivation tests the HEX derivation test vector +func TestHexDerivation(t *testing.T) { + logTestVector(t, "HEX Derivation", "Testing HEX data derivation with BIP85 test vector") + + masterKey, err := ParseMasterKey(testMasterKey) + if err != nil { + t.Fatalf("Failed to parse master key: %v", err) + } + + // Test vector from BIP85 spec + t.Logf("Path: %s", hexPath) + t.Logf("Parameters: NumBytes=64, Index=0") + + // First verify the entropy derivation + entropy, err := DeriveBIP85Entropy(masterKey, hexPath) + if err != nil { + t.Fatalf("Failed to derive entropy: %v", err) + } + + // Expected entropy from BIP85 spec + derivedEntropyHex := hex.EncodeToString(entropy[:64]) // HEX uses first 64 bytes + + if derivedEntropyHex != hexExpectedEntropy { + t.Errorf("Entropy mismatch!\nExpected: %s\nGot: %s", hexExpectedEntropy, derivedEntropyHex) + } + + // Now test the hex derivation + hexData, err := DeriveHex(masterKey, 64, 0) + if err != nil { + t.Fatalf("Failed to derive hex data: %v", err) + } + + if hexData != hexExpectedEntropy { + t.Errorf("Hex data mismatch!\nExpected: %s\nGot: %s", hexExpectedEntropy, hexData) + } +} + // TestInvalidParameters tests error conditions for parameter validation func TestInvalidParameters(t *testing.T) { logTestVector(t, "Invalid Parameters", "Testing error handling for invalid inputs") @@ -535,20 +1024,6 @@ func TestInvalidParameters(t *testing.T) { return err }, }, - { - name: "Dice roll invalid sides", - testFunc: func() error { - _, err := DeriveDiceRolls(masterKey, 1, 10, 0) // Min sides is 2 - return err - }, - }, - { - name: "Dice roll zero rolls", - testFunc: func() error { - _, err := DeriveDiceRolls(masterKey, 6, 0, 0) // Min rolls is 1 - return err - }, - }, } // Run all validation test cases @@ -617,166 +1092,3 @@ func TestAdditionalDeriveHex(t *testing.T) { t.Logf("Different indexes produced different outputs: PASS ✓") } } - -// TestDRNGDifferentSizes tests the DRNG with different buffer sizes -func TestDRNGDifferentSizes(t *testing.T) { - logTestVector(t, "DRNG Different Sizes", "Testing the DRNG with different buffer sizes") - - masterKey, err := ParseMasterKey(testMasterKey) - if err != nil { - t.Fatalf("Failed to parse master key: %v", err) - } - - path := "m/83696968'/0'/0'" - entropy, err := DeriveBIP85Entropy(masterKey, path) - if err != nil { - t.Fatalf("Failed to derive entropy: %v", err) - } - - // Create DRNG - drng := NewBIP85DRNG(entropy) - - // Test reading different sizes - for _, size := range []int{32, 64, 128, 256} { - t.Logf("Reading %d bytes from DRNG", size) - buffer := make([]byte, size) - n, err := drng.Read(buffer) - if err != nil { - t.Fatalf("Failed to read %d bytes from DRNG: %v", size, err) - } - if n != size { - t.Errorf("Expected to read %d bytes, got %d", size, n) - } else { - t.Logf("Successfully read %d bytes: PASS ✓", size) - } - } - - // Test deterministic behavior - two DRNGs with the same seed should produce the same output - drng1 := NewBIP85DRNG(entropy) - drng2 := NewBIP85DRNG(entropy) - - buffer1 := make([]byte, 32) - buffer2 := make([]byte, 32) - - _, err = drng1.Read(buffer1) - if err != nil { - t.Fatalf("Failed to read from first DRNG: %v", err) - } - - _, err = drng2.Read(buffer2) - if err != nil { - t.Fatalf("Failed to read from second DRNG: %v", err) - } - - if !bytes.Equal(buffer1, buffer2) { - t.Errorf("Expected identical outputs from DRNGs with same seed") - } else { - t.Logf("DRNGs with same seed produced identical outputs: PASS ✓") - } - - // Reading another 32 bytes should produce different output from the first read - buffer3 := make([]byte, 32) - _, err = drng1.Read(buffer3) - if err != nil { - t.Fatalf("Failed to read second buffer from DRNG: %v", err) - } - - if bytes.Equal(buffer1, buffer3) { - t.Errorf("Expected different outputs from sequential reads") - } else { - t.Logf("Sequential reads produced different outputs: PASS ✓") - } -} - -// TestMasterKeyParsing tests parsing of different master key formats -func TestMasterKeyParsing(t *testing.T) { - logTestVector(t, "Master Key Parsing", "Testing parsing of master keys in different formats") - - // Test valid master key - t.Logf("Testing valid master key") - _, err := ParseMasterKey(testMasterKey) - if err != nil { - t.Errorf("Failed to parse valid master key: %v", err) - } else { - t.Logf("Valid master key parsed successfully: PASS ✓") - } - - // Test invalid master key (wrong checksum) - t.Logf("Testing invalid master key (corrupted)") - invalidKey := "xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBqsx8HVqFk2uYo8kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLjbXX" - _, err = ParseMasterKey(invalidKey) - if err == nil { - t.Errorf("Expected error for invalid master key, but got nil") - } else { - t.Logf("Got expected error for invalid master key: %v", err) - t.Logf("RESULT: PASS ✓") - } - - // Test testnet master key (tprv) - t.Logf("Testing testnet master key format") - testnetKey := "tprv8ZgxMBicQKsPeWHBt7a68nPnvgTnuDhUgDWC8wZCgA8GahrQ3f3uWpq7wE7Uc1dLBnCe1hhCZ886K6ND37memRDWqsA9HgSKDXtwh2Qxo6J" - testnetMasterKey, err := ParseMasterKey(testnetKey) - if err != nil { - t.Errorf("Failed to parse testnet master key: %v", err) - } else { - t.Logf("Testnet master key parsed successfully: PASS ✓") - - // Test that XPRV derivation using a testnet master key produces a testnet XPRV - derivedKey, err := DeriveXPRV(testnetMasterKey, 0) - if err != nil { - t.Fatalf("Failed to derive XPRV from testnet key: %v", err) - } - - derivedKeyStr := derivedKey.String() - if !strings.HasPrefix(derivedKeyStr, "tprv") { - t.Errorf("Expected derived key to be testnet (tprv prefix), got: %s", derivedKeyStr) - } else { - t.Logf("Testnet XPRV derived successfully: %s", derivedKeyStr) - t.Logf("RESULT: PASS ✓") - } - } -} - -// TestDifferentPathFormats tests different path format expressions -func TestDifferentPathFormats(t *testing.T) { - logTestVector(t, "Path Formats", "Testing different path format expressions") - - masterKey, err := ParseMasterKey(testMasterKey) - if err != nil { - t.Fatalf("Failed to parse master key: %v", err) - } - - // Define equivalent paths in different formats - paths := []string{ - "m/83696968'/0'/0'", - "m/83696968h/0h/0h", - "/83696968'/0'/0'", - "83696968'/0'/0'", - } - - var results [][]byte - - // Derive entropy using each path - for i, path := range paths { - t.Logf("Testing path format %d: %s", i+1, path) - entropy, err := DeriveBIP85Entropy(masterKey, path) - if err != nil { - t.Errorf("Failed to derive entropy with path %s: %v", path, err) - continue - } - - results = append(results, entropy) - t.Logf("Derivation succeeded: PASS ✓") - } - - // Verify all results are the same - for i := 1; i < len(results); i++ { - if !bytes.Equal(results[0], results[i]) { - t.Errorf("Path %s produced different entropy than path %s", paths[0], paths[i]) - } - } - - if len(results) > 1 { - t.Logf("All equivalent path formats produced the same entropy: PASS ✓") - } -}