diff --git a/engine/examples/client/pike/main.go b/engine/examples/client/pike/main.go new file mode 100644 index 00000000..7b9addd0 --- /dev/null +++ b/engine/examples/client/pike/main.go @@ -0,0 +1,59 @@ +package main + +import ( + "encoding/hex" + "fmt" + + "github.com/libsv/go-bk/bec" + + "github.com/bitcoin-sv/spv-wallet/engine/pike" + "github.com/bitcoin-sv/spv-wallet/engine/script/template" +) + +func main() { + // Example sender's public key (replace with actual sender's public key) + // generating keys + senderPublicKeyHex := "034252e5359a1de3b8ec08e6c29b80594e88fb47e6ae9ce65ee5a94f0d371d2cde" + senderPublicKeyBytes, err := hex.DecodeString(senderPublicKeyHex) + if err != nil { + panic(err) + } + senderPubKey, err := bec.ParsePubKey(senderPublicKeyBytes, bec.S256()) + if err != nil { + panic(err) + } + + receiverPublicKeyHex := "027c1404c3ecb034053e6dd90bc68f7933284559c7d0763367584195a8796d9b0e" + receiverPublicKeyBytes, err := hex.DecodeString(receiverPublicKeyHex) + if err != nil { + panic(err) + } + receiverPubKey, err := bec.ParsePubKey(receiverPublicKeyBytes, bec.S256()) + if err != nil { + panic(err) + } + + // example of usage pike.GenerateOutputsTemplate + outputsTemplate, _ := pike.GenerateOutputsTemplate(10000) + fmt.Println(formatOutputs(outputsTemplate)) + + lockingScripts, err := pike.GenerateLockingScriptsFromTemplates(outputsTemplate, senderPubKey, receiverPubKey, "reference") + if err != nil { + fmt.Println("Error:", err) + return + } + + for _, script := range lockingScripts { + fmt.Println("Locking Script:", script) + } + +} + +// Helper function to format the outputs into a string +func formatOutputs(outputs []*template.OutputTemplate) string { + var result string + for i, output := range outputs { + result += fmt.Sprintf("Output %d: %v\n", i+1, output) + } + return result +} diff --git a/engine/pike/example_test.go b/engine/pike/example_test.go new file mode 100644 index 00000000..90461c1d --- /dev/null +++ b/engine/pike/example_test.go @@ -0,0 +1,52 @@ +package pike_test + +import ( + "encoding/hex" + "fmt" + + "github.com/libsv/go-bk/bec" + + "github.com/bitcoin-sv/spv-wallet/engine/pike" +) + +func Example_generateLockingScripts() { + // Example sender's public key (replace with actual sender's public key) + senderPublicKeyHex := "034252e5359a1de3b8ec08e6c29b80594e88fb47e6ae9ce65ee5a94f0d371d2cde" + senderPublicKeyBytes, err := hex.DecodeString(senderPublicKeyHex) + if err != nil { + panic(err) + } + senderPubKey, err := bec.ParsePubKey(senderPublicKeyBytes, bec.S256()) + if err != nil { + panic(err) + } + + receiverPublicKeyHex := "027c1404c3ecb034053e6dd90bc68f7933284559c7d0763367584195a8796d9b0e" + receiverPublicKeyBytes, err := hex.DecodeString(receiverPublicKeyHex) + if err != nil { + panic(err) + } + receiverPubKey, err := bec.ParsePubKey(receiverPublicKeyBytes, bec.S256()) + if err != nil { + panic(err) + } + + // Example usage of GenerateOutputsTemplate + outputsTemplate, err := pike.GenerateOutputsTemplate(10000) + if err != nil { + panic(fmt.Errorf("Error generating outputs template - %w", err)) + } + + // Example usage of GenerateLockingScriptsFromTemplates + lockingScripts, err := pike.GenerateLockingScriptsFromTemplates(outputsTemplate, senderPubKey, receiverPubKey, "reference") + if err != nil { + panic(fmt.Errorf("Error generating locking scripts - %w", err)) + } + + for _, script := range lockingScripts { + fmt.Println("Locking Script:", script) + } + + // Output: + // Locking Script: 76a9147327490be831259f38b0f9ab019413e51d1b40c688ac +} diff --git a/engine/pike/pike.go b/engine/pike/pike.go new file mode 100644 index 00000000..d540ccdc --- /dev/null +++ b/engine/pike/pike.go @@ -0,0 +1,62 @@ +// Package pike provides functionality to work with Pay-to-PubKey-Hash (P2PKH) scripts in a blockchain context. +// +// P2PKH is a common type of Bitcoin transaction that locks funds to a specific public key hash, requiring the +// corresponding private key to produce a valid signature for spending the funds. This package offers utilities +// to create, decode, and validate P2PKH scripts. +// +// The package includes: +// - Functions to generate P2PKH addresses from public keys. +// - Methods to construct P2PKH scriptPubKey and scriptSig scripts. +// - Utilities to decode and inspect P2PKH scripts and addresses. +// - Validation functions to ensure the integrity and correctness of P2PKH scripts. +// +// This package is intended for developers working with Bitcoin or other cryptocurrencies that support P2PKH transactions. +// It abstracts the low-level details and provides a high-level interface for creating and handling P2PKH scripts. +package pike + +import ( + "fmt" + + "github.com/libsv/go-bk/bec" + "github.com/libsv/go-bt/v2/bscript" + + "github.com/bitcoin-sv/spv-wallet/engine/script/template" + "github.com/bitcoin-sv/spv-wallet/engine/types/type42" +) + +// GenerateOutputsTemplate creates a Pike output template +func GenerateOutputsTemplate(satoshis uint64) ([]*template.OutputTemplate, error) { + p2pkhTemplate, err := template.P2PKH(satoshis) + if err != nil { + return nil, err + } + return []*template.OutputTemplate{p2pkhTemplate}, nil +} + +// GenerateLockingScriptsFromTemplates converts Pike outputs templates to scripts +func GenerateLockingScriptsFromTemplates(outputsTemplate []*template.OutputTemplate, senderPubKey, receiverPubKey *bec.PublicKey, reference string) ([]string, error) { + lockingScripts := make([]string, len(outputsTemplate)) + + for idx, output := range outputsTemplate { + templateScript, err := bscript.NewFromHexString(output.Script) + if err != nil { + return nil, fmt.Errorf("error creating script from hex string - %w", err) + } + + dPK, err := type42.DeriveLinkedKey(senderPubKey, receiverPubKey, fmt.Sprintf("%s-%d", reference, idx)) + if err != nil { + return nil, err + } + + scriptBytes, err := template.Evaluate(*templateScript, dPK) + if err != nil { + return nil, fmt.Errorf("error evaluating template script - %w", err) + } + + finalScript := bscript.Script(scriptBytes) + + lockingScripts[idx] = finalScript.String() + } + + return lockingScripts, nil +} diff --git a/engine/pike/pike_test.go b/engine/pike/pike_test.go new file mode 100644 index 00000000..ec678aa4 --- /dev/null +++ b/engine/pike/pike_test.go @@ -0,0 +1,48 @@ +package pike + +import ( + "encoding/hex" + "testing" + + "github.com/libsv/go-bk/bec" + assert "github.com/stretchr/testify/require" + + "github.com/bitcoin-sv/spv-wallet/engine/script/template" +) + +func TestGenerateLockingScriptsFromTemplates(t *testing.T) { + // Define sample data + senderPubKeyHex := "027c1404c3ecb034053e6dd90bc68f7933284559c7d0763367584195a8796d9b0e" + senderPubKeyBytes, err := hex.DecodeString(senderPubKeyHex) + assert.NoError(t, err) + senderPubKey, err := bec.ParsePubKey(senderPubKeyBytes, bec.S256()) + assert.NoError(t, err) + + receiverPubKeyHex := "03a34e456deecb6e6e9237e63e5b7d045d1d2a456eb6be43de1ec4e9ac9a07b50d" + receiverPubKeyBytes, err := hex.DecodeString(receiverPubKeyHex) + assert.NoError(t, err) + receiverPubKey, err := bec.ParsePubKey(receiverPubKeyBytes, bec.S256()) + assert.NoError(t, err) + + outputsTemplate := []*template.OutputTemplate{ + {Script: "76a914000000000000000000000000000000000000000088ac"}, + {Script: "76a914111111111111111111111111111111111111111188ac"}, + } + + t.Run("Valid Case", func(t *testing.T) { + lockingScripts, err := GenerateLockingScriptsFromTemplates(outputsTemplate, senderPubKey, receiverPubKey, "test-reference") + assert.NoError(t, err) + assert.Len(t, lockingScripts, len(outputsTemplate)) + assert.Equal(t, outputsTemplate[0].Script, lockingScripts[0]) + assert.Equal(t, outputsTemplate[1].Script, lockingScripts[1]) + }) + + t.Run("Invalid Template Script", func(t *testing.T) { + invalidTemplate := []*template.OutputTemplate{ + {Script: "invalid-hex-string"}, // Invalid hex string + } + lockingScripts, err := GenerateLockingScriptsFromTemplates(invalidTemplate, senderPubKey, receiverPubKey, "test-reference") + assert.Error(t, err) + assert.Nil(t, lockingScripts) + }) +} diff --git a/engine/script/template/evaluate.go b/engine/script/template/evaluate.go new file mode 100644 index 00000000..5df9eafe --- /dev/null +++ b/engine/script/template/evaluate.go @@ -0,0 +1,77 @@ +package template + +import ( + "errors" + + "github.com/libsv/go-bk/bec" + "github.com/libsv/go-bk/crypto" + "github.com/libsv/go-bt/v2/bscript" + "github.com/libsv/go-bt/v2/bscript/interpreter" +) + +// Evaluate processes a given Bitcoin script by parsing it, replacing certain opcodes +// with the public key hash, and returning the resulting script as a byte array. +// Will replace any OP_PUBKEYHASH or OP_PUBKEY +// +// Parameters: +// - script: A byte array representing the input script. +// - pubKey: A pointer to a bec.PublicKey which provides the dedicated public key to be used in the evaluation. +// +// Returns: +// - A byte array representing the evaluated script, or nil if an error occurs. +func Evaluate(script []byte, pubKey *bec.PublicKey) ([]byte, error) { + s := bscript.Script(script) + + parser := interpreter.DefaultOpcodeParser{} + parsedScript, err := parser.Parse(&s) + if err != nil { + return nil, err + } + + // Validate parsed opcodes + for _, op := range parsedScript { + if op.Value() == 0xFF { + return nil, errors.New("invalid opcode") + } + } + + // Serialize the public key to compressed format + dPKBytes := pubKey.SerialiseCompressed() + + // Apply Hash160 (SHA-256 followed by RIPEMD-160) to the compressed public key + dPKHash := crypto.Hash160(dPKBytes) + + // Create a new script with the public key hash + newScript := new(bscript.Script) + if err := newScript.AppendPushData(dPKHash); err != nil { + return nil, err + } + + // Parse the public key hash script + pkhParsed, err := parser.Parse(newScript) + if err != nil { + return nil, err + } + + // Replace OP_PUBKEYHASH with the actual public key hash + evaluated := make([]interpreter.ParsedOpcode, 0, len(parsedScript)) + for _, op := range parsedScript { + switch op.Value() { + case bscript.OpPUBKEYHASH: + evaluated = append(evaluated, pkhParsed...) + case bscript.OpPUBKEY: + return nil, errors.New("OP_PUBKEY not supported yet") + default: + evaluated = append(evaluated, op) + } + } + + // Unparse the evaluated opcodes back into a script + finalScript, err := parser.Unparse(evaluated) + if err != nil { + return nil, err + } + + // Cast *bscript.Script back to []byte + return []byte(*finalScript), nil +} diff --git a/engine/script/template/p2pkh.go b/engine/script/template/p2pkh.go new file mode 100644 index 00000000..d3e0296b --- /dev/null +++ b/engine/script/template/p2pkh.go @@ -0,0 +1,54 @@ +// Package template provides a collection of functions and types for working with script templates. +package template + +import ( + "encoding/hex" + "errors" + "sync" + + "github.com/libsv/go-bt/v2/bscript" +) + +var ( + scriptHex string + once sync.Once +) + +func initScriptHex() { + opcodes := []byte{ + bscript.OpDUP, + bscript.OpHASH160, + bscript.OpPUBKEYHASH, + bscript.OpEQUALVERIFY, + bscript.OpCHECKSIG, + } + + // Convert opcodes to hexadecimal string + scriptHex = hex.EncodeToString(opcodes) +} + +// OutputTemplate represents the script and satoshis for a Pike output +type OutputTemplate struct { + Script string `json:"script"` + Satoshis uint64 `json:"satoshis"` +} + +// P2PKH creates a single output with the PIKE template +func P2PKH(satoshis uint64) (*OutputTemplate, error) { + + if satoshis == 0 { + return nil, errors.New("satoshis cannot be zero") + } + if satoshis == ^uint64(0) { + return nil, errors.New("invalid satoshis") + } + + // Initialize the scriptHex once + once.Do(initScriptHex) + + // Create and return the PikeOutputsTemplate + return &OutputTemplate{ + Script: scriptHex, + Satoshis: satoshis, + }, nil +} diff --git a/engine/script/template/p2pkh_test.go b/engine/script/template/p2pkh_test.go new file mode 100644 index 00000000..bb6ce3f5 --- /dev/null +++ b/engine/script/template/p2pkh_test.go @@ -0,0 +1,168 @@ +package template + +import ( + "encoding/hex" + "testing" + + "github.com/libsv/go-bk/bec" + "github.com/libsv/go-bk/crypto" + "github.com/libsv/go-bt/bscript" + assert "github.com/stretchr/testify/require" +) + +func TestP2PKH(t *testing.T) { + validTests := []struct { + name string + satoshis uint64 + expected *OutputTemplate + }{ + { + name: "valid input", + satoshis: 1000, + expected: &OutputTemplate{ + Script: "76a9fd88ac", + Satoshis: 1000, + }, + }, + { + name: "zero satoshis", + satoshis: 1, + expected: &OutputTemplate{ + Script: "76a9fd88ac", + Satoshis: 1, + }, + }, + } + + t.Run("Valid Cases", func(t *testing.T) { + for _, tt := range validTests { + tt := tt // capture range variable + t.Run(tt.name, func(t *testing.T) { + got, err := P2PKH(tt.satoshis) + assert.NoError(t, err) + assert.Equal(t, tt.expected, got) + }) + } + }) + + t.Run("Valid Cases", func(t *testing.T) { + for _, tt := range validTests { + tt := tt // capture range variable + t.Run(tt.name, func(t *testing.T) { + got, err := P2PKH(tt.satoshis) + assert.NoError(t, err) + assert.Equal(t, tt.expected, got) + }) + } + }) + + errorTests := []struct { + name string + satoshis uint64 + }{ + { + name: "negative satoshis", + satoshis: ^uint64(0), // Simulating a case that would cause an error, maximum uint64 value, bitwise NOT of 0 is -1 + }, + { + name: "zero satoshis", + satoshis: 0, + }, + } + + t.Run("Error Cases", func(t *testing.T) { + for _, tt := range errorTests { + tt := tt // capture range variable + t.Run(tt.name, func(t *testing.T) { + _, err := P2PKH(tt.satoshis) + assert.Error(t, err) + }) + } + }) +} + +func TestEvaluate(t *testing.T) { + pubKeyHex := "027c1404c3ecb034053e6dd90bc68f7933284559c7d0763367584195a8796d9b0e" + pubKeyBytes, err := hex.DecodeString(pubKeyHex) + assert.NoError(t, err) + mockPublicKey, err := bec.ParsePubKey(pubKeyBytes, bec.S256()) + assert.NoError(t, err) + mockPubKeyHash := crypto.Hash160(mockPublicKey.SerialiseCompressed()) + + t.Run("Valid Cases", func(t *testing.T) { + validTests := []struct { + name string + script []byte + publicKey *bec.PublicKey + expected []byte + }{ + { + name: "valid script with OP_PUBKEYHASH", + script: []byte{bscript.OpDUP, bscript.OpHASH160, bscript.OpPUBKEYHASH, bscript.OpEQUALVERIFY, bscript.OpCHECKSIG}, + publicKey: mockPublicKey, + expected: append([]byte{bscript.OpDUP, bscript.OpHASH160, bscript.OpDATA20}, append(mockPubKeyHash, bscript.OpEQUALVERIFY, bscript.OpCHECKSIG)...), + }, + { + name: "valid script without OP_PUBKEYHASH or OP_PUBKEY", + script: []byte{bscript.OpDUP, bscript.OpHASH160, bscript.OpEQUALVERIFY, bscript.OpCHECKSIG}, + publicKey: mockPublicKey, + expected: []byte{bscript.OpDUP, bscript.OpHASH160, bscript.OpEQUALVERIFY, bscript.OpCHECKSIG}, + }, + { + name: "script with OP_PUSHDATA1 and hex data matching PUBKEY and PUBKEYHASH opcodes", + script: []byte{bscript.OpPUSHDATA1, 1, bscript.OpPUBKEYHASH, bscript.OpADD, bscript.OpPUSHDATA1, 1, bscript.OpPUBKEY, bscript.OpEQUALVERIFY}, + publicKey: mockPublicKey, + expected: []byte{bscript.OpPUSHDATA1, 1, bscript.OpPUBKEYHASH, bscript.OpADD, bscript.OpPUSHDATA1, 1, bscript.OpPUBKEY, bscript.OpEQUALVERIFY}, + }, + { + name: "empty script", + script: []byte{}, + publicKey: mockPublicKey, + expected: []byte{}, + }, + { + name: "script with only valid push data", + script: []byte{bscript.OpPUSHDATA1, 2, 0xaa, 0xbb}, + publicKey: mockPublicKey, + expected: []byte{bscript.OpPUSHDATA1, 2, 0xaa, 0xbb}, + }, + } + + for _, tt := range validTests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + result, err := Evaluate(tt.script, tt.publicKey) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, tt.expected, result) + }) + } + }) + + t.Run("Invalid Cases", func(t *testing.T) { + invalidTests := []struct { + name string + script []byte + publicKey *bec.PublicKey + }{ + { + name: "invalid script", + script: []byte{0xFF}, // Invalid opcode + publicKey: mockPublicKey, + }, + { + name: "valid script with OP_PUBKEY", + script: []byte{bscript.OpDUP, bscript.OpHASH160, bscript.OpPUBKEY, bscript.OpEQUALVERIFY, bscript.OpCHECKSIG}, + publicKey: mockPublicKey, + }, + } + + for _, tt := range invalidTests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + _, err := Evaluate(tt.script, tt.publicKey) + assert.Error(t, err) + }) + } + }) +} diff --git a/engine/types/type42/linking_key.go b/engine/types/type42/linking_key.go new file mode 100644 index 00000000..6992c1d4 --- /dev/null +++ b/engine/types/type42/linking_key.go @@ -0,0 +1,101 @@ +package type42 + +import ( + "crypto/hmac" + "crypto/sha256" + "errors" + "fmt" + "math/big" + + "github.com/libsv/go-bk/bec" +) + +// calculateHMAC calculates the HMAC of the provided public shared secret using a reference string. +// The reference string, which acts as the HMAC key, can be an invoice number or any other string. +// The function returns the HMAC result as a byte slice. +// +// Parameters: +// - pubSharedSecret: A byte slice representing the public shared secret to be hashed. +// - reference: A string used as the HMAC key. +// +// Returns: +// - A byte slice containing the HMAC result as a byte slice or an error if the HMAC calculation fails. +func calculateHMAC(pubSharedSecret []byte, message string) ([]byte, error) { + if message == "" { + return nil, errors.New("invalid invoice number") + } + h := hmac.New(sha256.New, pubSharedSecret) + if _, err := h.Write([]byte(message)); err != nil { + return nil, fmt.Errorf("error writing HMAC message - %w", err) + } + return h.Sum(nil), nil +} + +// calculateLinkedPublicKey calculates the dedicated public key (dPK) using the HMAC result and the receiver's public key. +// The HMAC result is used as a scalar to perform elliptic curve scalar multiplication and point addition. +// Returns the resulting public key or an error if the calculation fails. +func calculateLinkedPublicKey(hmacResult []byte, receiverPubKey *bec.PublicKey) (*bec.PublicKey, error) { + if len(hmacResult) == 0 { + return nil, fmt.Errorf("HMAC result is empty") + } + if receiverPubKey == nil { + return nil, fmt.Errorf("receiver public key is nil") + } + + // Convert HMAC result to a big integer + hn := new(big.Int).SetBytes(hmacResult) + curve := bec.S256() // Use secp256k1 curve + hn.Mod(hn, curve.Params().N) // Ensure the scalar is within the curve order + + // Perform scalar multiplication: hn * G + rx, ry := curve.ScalarBaseMult(hn.Bytes()) + + // Perform point addition: (receiverPubKey + (hn * G)) + dedicatedPubKeyX, dedicatedPubKeyY := curve.Add(receiverPubKey.X, receiverPubKey.Y, rx, ry) + + // Verify that the resulting point is on the curve + if !curve.IsOnCurve(dedicatedPubKeyX, dedicatedPubKeyY) { + return nil, fmt.Errorf("resulting public key is not on curve") + } + + // Create the dedicated public key + dPK := &bec.PublicKey{ + Curve: curve, + X: dedicatedPubKeyX, + Y: dedicatedPubKeyY, + } + return dPK, nil +} + +// DeriveLinkedKey derives a child public key from the source public key and link it with public key +// with use of invoiceNumber as reference of this derivation. +func DeriveLinkedKey(source *bec.PublicKey, linkPubKey *bec.PublicKey, invoiceNumber string) (*bec.PublicKey, error) { + if source == nil || linkPubKey == nil { + return nil, errors.New("source or receiver public key is nil") + } + + // Check for nil receiver public key + if source.X == nil || source.Y == nil { + return nil, errors.New("source public key is nil") + } + if linkPubKey.X == nil || linkPubKey.Y == nil { + return nil, errors.New("receiver public key is nil") + } + + // Compute the shared secret + publicKeyBytes := source.SerialiseCompressed() + + // Compute the HMAC result + hmacResult, err := calculateHMAC(publicKeyBytes, invoiceNumber) + if err != nil { + return nil, err + } + + // Calculate the dedicated public key + linkedPK, err := calculateLinkedPublicKey(hmacResult, linkPubKey) + if err != nil { + return nil, err + } + + return linkedPK, nil +} diff --git a/engine/types/type42/linking_key_test.go b/engine/types/type42/linking_key_test.go new file mode 100644 index 00000000..f9851ca1 --- /dev/null +++ b/engine/types/type42/linking_key_test.go @@ -0,0 +1,179 @@ +package type42 + +import ( + "encoding/hex" + "fmt" + "testing" + + "github.com/libsv/go-bk/bec" + assert "github.com/stretchr/testify/require" +) + +func TestDeriveLinkedKey(t *testing.T) { + sourcePubKeyHex := "027c1404c3ecb034053e6dd90bc68f7933284559c7d0763367584195a8796d9b0e" + sourcePubKeyBytes, err := hex.DecodeString(sourcePubKeyHex) + assert.NoError(t, err) + sourcePubKey, err := bec.ParsePubKey(sourcePubKeyBytes, bec.S256()) + assert.NoError(t, err) + + linkPubKeyHex := "03a34e456deecb6e6e9237e63e5b7d045d1d2a456eb6be43de1ec4e9ac9a07b50d" + linkPubKeyBytes, err := hex.DecodeString(linkPubKeyHex) + assert.NoError(t, err) + linkPubKey, err := bec.ParsePubKey(linkPubKeyBytes, bec.S256()) + assert.NoError(t, err) + + validHMAC, err := calculateHMAC(sourcePubKey.SerialiseCompressed(), "valid-invoice") + assert.NoError(t, err) + validDerivedKey, err := calculateLinkedPublicKey(validHMAC, linkPubKey) + assert.NoError(t, err) + + validTests := []struct { + name string + source bec.PublicKey + linkPubKey bec.PublicKey + invoiceNumber string + expectedResult *bec.PublicKey + }{ + { + name: "valid case", + source: *sourcePubKey, + linkPubKey: *linkPubKey, + invoiceNumber: "valid-invoice", + expectedResult: validDerivedKey, + }, + } + + t.Run("Valid Cases", func(t *testing.T) { + for _, tt := range validTests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + result, err := DeriveLinkedKey(&tt.source, &tt.linkPubKey, tt.invoiceNumber) + assert.NoError(t, err) + assert.Equal(t, tt.expectedResult, result) + }) + } + }) + + errorTests := []struct { + name string + source bec.PublicKey + linkPubKey bec.PublicKey + invoiceNumber string + }{ + { + name: "invalid HMAC result", + source: *sourcePubKey, + linkPubKey: *linkPubKey, + invoiceNumber: "", // Empty invoice number causing HMAC calculation to fail + }, + { + name: "nil receiver public key", + source: *sourcePubKey, + linkPubKey: bec.PublicKey{}, // Empty public key causing dedicated public key calculation to fail + invoiceNumber: "valid-invoice", + }, + } + + t.Run("Error Cases", func(t *testing.T) { + for _, tt := range errorTests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + result, err := DeriveLinkedKey(&tt.source, &tt.linkPubKey, tt.invoiceNumber) + assert.Error(t, err) + assert.Nil(t, result) + }) + } + }) +} + +// Helper function to parse a public key from a hex string +func mustParsePubKey(hexKey string) *bec.PublicKey { + keyBytes, err := hex.DecodeString(hexKey) + if err != nil { + panic(fmt.Sprintf("invalid hex key: %s", err)) + } + key, err := bec.ParsePubKey(keyBytes, bec.S256()) + if err != nil { + panic(fmt.Sprintf("invalid public key: %s", err)) + } + return key +} + +// TestDeriveLinkedKey tests the DeriveLinkedKey function +func TestDeriveLinkedKeyCases(t *testing.T) { + validTests := []struct { + name string + source *bec.PublicKey + linkPubKey *bec.PublicKey + invoiceNumber string + expectedResult *bec.PublicKey + }{ + { + name: "case 1", + source: mustParsePubKey("033f9160df035156f1c48e75eae99914fa1a1546bec19781e8eddb900200bff9d1"), + linkPubKey: mustParsePubKey("02133b035cda4ba15f93b5fdde11c1f73eb9f1a79b60c6caa1c78e1c4c64ed72ce"), + invoiceNumber: "f3WCaUmnN9U=", + expectedResult: mustParsePubKey("02d4049747553b4b956a419a7e1ddef418d57f317de8cc7d024d05c75f29309f26"), + }, + { + name: "case 2", + source: mustParsePubKey("027775fa43959548497eb510541ac34b01d5ee9ea768de74244a4a25f7b60fae8d"), + linkPubKey: mustParsePubKey("02dfcbe35d95b55b5f3168ea8f12717e266ceddf88d04d2ff741272dfb0e542c2a"), + invoiceNumber: "2Ska++APzEc=", + expectedResult: mustParsePubKey("02679645bc44a771154f66eb52ca93507c3e777997a774e87611a7b17d30d748ee"), + }, + { + name: "case 3", + source: mustParsePubKey("0338d2e0d12ba645578b0955026ee7554889ae4c530bd7a3b6f688233d763e169f"), + linkPubKey: mustParsePubKey("023c1db4cce57a44c95e05f0ed499085a9ce8ac83bf80dd4c7b4658a9d6c4a122e"), + invoiceNumber: "cN/yQ7+k7pg=", + expectedResult: mustParsePubKey("03c07884a8e8f02bb1cfb276edddfa8dade7654d8cb744e0ff9e4b0c5dffa63ae0"), + }, + { + name: "case 4", + source: mustParsePubKey("02830212a32a47e68b98d477000bde08cb916f4d44ef49d47ccd4918d9aaabe9c8"), + linkPubKey: mustParsePubKey("0363ec92e374974e9c904875ff8daff50bfa22d8ec0a39891a31e5e760aca5e9cf"), + invoiceNumber: "m2/QAsmwaA4=", + expectedResult: mustParsePubKey("024d369f8440e91fdd58577bd6bc471d868fdfc58e605eb2ec5a2362df589d43cd"), + }, + { + name: "case 5", + source: mustParsePubKey("03f20a7e71c4b276753969e8b7e8b67e2dbafc3958d66ecba98dedc60a6615336d"), + linkPubKey: mustParsePubKey("02cef40bd6826d1b1960bc94094c0c0a19547b291c33b7a07380cbdc4580f3678b"), + invoiceNumber: "jgpUIjWFlVQ=", + expectedResult: mustParsePubKey("03aa3ea0de12f44642a51593b2f8a0e0ff9813cdb4ab39ab8039654dce2ae5bdb3"), + }, + { + name: "case 6", + source: mustParsePubKey("03f20a7e71c4b276753969e8b7e8b67e2dbafc3958d66ecba98dedc60a6615336d"), + linkPubKey: mustParsePubKey("0363ec92e374974e9c904875ff8daff50bfa22d8ec0a39891a31e5e760aca5e9cf"), + invoiceNumber: "jgpUIjWFlVQ=", + expectedResult: mustParsePubKey("032ac4dccb237d777361a7fade10fc2d21b642df9b0e4634d3e27fdce131526e4f"), + }, + { + name: "case 7", + source: mustParsePubKey("02830212a32a47e68b98d477000bde08cb916f4d44ef49d47ccd4918d9aaabe9c8"), + linkPubKey: mustParsePubKey("02cef40bd6826d1b1960bc94094c0c0a19547b291c33b7a07380cbdc4580f3678b"), + invoiceNumber: "jgpUIjWFlVQ=", + expectedResult: mustParsePubKey("02caa04e2020a0642e5283c978257d2135c00882c049adcd71a71dffe9d7208a39"), + }, + { + name: "case 8", + source: mustParsePubKey("02830212a32a47e68b98d477000bde08cb916f4d44ef49d47ccd4918d9aaabe9c8"), + linkPubKey: mustParsePubKey("02cef40bd6826d1b1960bc94094c0c0a19547b291c33b7a07380cbdc4580f3678b"), + invoiceNumber: "m2/QAsmwaA4=", + expectedResult: mustParsePubKey("030467472149ac58d9d04e4182b03af99593a7af312623c4be7d96f2fde08f6421"), + }, + } + + t.Run("Test Keys", func(t *testing.T) { + for _, tt := range validTests { + tt := tt // capture range variable + t.Run(tt.name, func(t *testing.T) { + result, err := DeriveLinkedKey(tt.source, tt.linkPubKey, tt.invoiceNumber) + assert.NoError(t, err) + assert.Equal(t, tt.expectedResult, result) + }) + } + }) +} diff --git a/engine/types/type42/test_helpers.go b/engine/types/type42/test_helpers.go new file mode 100644 index 00000000..b3f7e718 --- /dev/null +++ b/engine/types/type42/test_helpers.go @@ -0,0 +1,28 @@ +package type42 + +import ( + "crypto/hmac" + "crypto/sha256" + "fmt" + + "github.com/libsv/go-bk/bec" +) + +// Helper function to generate a random public key +func generateRandomPublicKey() *bec.PublicKey { + privKey, _ := bec.NewPrivateKey(bec.S256()) + return privKey.PubKey() +} + +// Helper function to compute the SHA-256 hash of a string and return it as a slice of bytes +func sha256Hash(data string) []byte { + hash := sha256.Sum256([]byte(data)) + return hash[:] +} + +// Helper function to compute the expected HMAC +func computeExpectedHMAC(pubSharedSecret []byte, reference string, idx int) []byte { + h := hmac.New(sha256.New, pubSharedSecret) + h.Write([]byte(fmt.Sprintf("%s-%d", reference, idx))) + return h.Sum(nil) +}