Skip to content

Commit

Permalink
Merge pull request #595 from bitcoin-sv/feat-spv-790-Create-outputs-t…
Browse files Browse the repository at this point in the history
…emplate-and-store-evaluated-template-in-destination

feat(spv-790) Create outputs template and store evaluated template in destination
  • Loading branch information
dorzepowski authored May 27, 2024
2 parents 38fd49f + 20f0fdb commit 47efa37
Show file tree
Hide file tree
Showing 10 changed files with 828 additions and 0 deletions.
59 changes: 59 additions & 0 deletions engine/examples/client/pike/main.go
Original file line number Diff line number Diff line change
@@ -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
}
52 changes: 52 additions & 0 deletions engine/pike/example_test.go
Original file line number Diff line number Diff line change
@@ -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
}
62 changes: 62 additions & 0 deletions engine/pike/pike.go
Original file line number Diff line number Diff line change
@@ -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
}
48 changes: 48 additions & 0 deletions engine/pike/pike_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
77 changes: 77 additions & 0 deletions engine/script/template/evaluate.go
Original file line number Diff line number Diff line change
@@ -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
}
54 changes: 54 additions & 0 deletions engine/script/template/p2pkh.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit 47efa37

Please sign in to comment.