Skip to content

Commit

Permalink
mint - tests for NUT11 support
Browse files Browse the repository at this point in the history
  • Loading branch information
elnosh committed Aug 14, 2024
1 parent a91f727 commit 5306122
Show file tree
Hide file tree
Showing 5 changed files with 379 additions and 72 deletions.
34 changes: 32 additions & 2 deletions cashu/nuts/nut11/nut11.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ var (
TooManyTagsErr = cashu.Error{Detail: "too many tags", Code: NUT11ErrCode}
NSigsMustBePositiveErr = cashu.Error{Detail: "n_sigs must be a positive integer", Code: NUT11ErrCode}
EmptyPubkeysErr = cashu.Error{Detail: "pubkeys tag cannot be empty if n_sigs tag is present", Code: NUT11ErrCode}
EmptyWitnessErr = cashu.Error{Detail: "witness cannot be empty", Code: NUT11ErrCode}
InvalidWitness = cashu.Error{Detail: "invalid witness", Code: NUT11ErrCode}
NotEnoughSignaturesErr = cashu.Error{Detail: "not enough valid signatures provided", Code: NUT11ErrCode}
AllSigAllFlagsErr = cashu.Error{Detail: "all flags must be SIG_ALL", Code: NUT11ErrCode}
SigAllKeysMustBeEqualErr = cashu.Error{Detail: "all public keys must be the same for SIG_ALL", Code: NUT11ErrCode}
Expand All @@ -68,7 +68,7 @@ type P2PKTags struct {

// P2PKSecret returns a secret with a spending condition
// that will lock ecash to a public key
func P2PKSecret(pubkey string) (string, error) {
func P2PKSecret(pubkey string, p2pkTags P2PKTags) (string, error) {
// generate random nonce
nonceBytes := make([]byte, 32)
_, err := rand.Read(nonceBytes)
Expand All @@ -77,9 +77,39 @@ func P2PKSecret(pubkey string) (string, error) {
}
nonce := hex.EncodeToString(nonceBytes)

var tags [][]string
if len(p2pkTags.Sigflag) > 0 {
tags = append(tags, []string{SIGFLAG, p2pkTags.Sigflag})
}
if p2pkTags.NSigs > 0 {
numStr := strconv.Itoa(p2pkTags.NSigs)
tags = append(tags, []string{NSIGS, numStr})
}
if len(p2pkTags.Pubkeys) > 0 {
pubkeys := []string{PUBKEYS}
for _, pubkey := range p2pkTags.Pubkeys {
key := hex.EncodeToString(pubkey.SerializeCompressed())
pubkeys = append(pubkeys, key)
}
tags = append(tags, pubkeys)
}
if p2pkTags.Locktime > 0 {
locktime := strconv.Itoa(int(p2pkTags.Locktime))
tags = append(tags, []string{LOCKTIME, locktime})
}
if len(p2pkTags.Refund) > 0 {
refundKeys := []string{REFUND}
for _, pubkey := range p2pkTags.Refund {
key := hex.EncodeToString(pubkey.SerializeCompressed())
refundKeys = append(refundKeys, key)
}
tags = append(tags, refundKeys)
}

secretData := nut10.WellKnownSecret{
Nonce: nonce,
Data: pubkey,
Tags: tags,
}

secret, err := nut10.SerializeSecret(nut10.P2PK, secretData)
Expand Down
124 changes: 56 additions & 68 deletions mint/mint.go
Original file line number Diff line number Diff line change
Expand Up @@ -623,11 +623,7 @@ func verifyP2PKLockedProof(proof cashu.Proof) error {
var p2pkWitness nut11.P2PKWitness
err = json.Unmarshal([]byte(proof.Witness), &p2pkWitness)
if err != nil {
errmsg := fmt.Sprintf("invalid witness: %v", err)
return cashu.BuildCashuError(errmsg, nut11.NUT11ErrCode)
}
if len(p2pkWitness.Signatures) < 1 {
return nut11.EmptyWitnessErr
p2pkWitness.Signatures = []string{}
}

p2pkTags, err := nut11.ParseP2PKTags(p2pkWellKnownSecret.Tags)
Expand All @@ -643,6 +639,9 @@ func verifyP2PKLockedProof(proof cashu.Proof) error {
return nil
} else {
hash := sha256.Sum256([]byte(proof.Secret))
if len(p2pkWitness.Signatures) < 1 {
return nut11.InvalidWitness
}
if !nut11.HasValidSignatures(hash[:], p2pkWitness, signaturesRequired, p2pkTags.Refund) {
return nut11.NotEnoughSignaturesErr
}
Expand All @@ -664,6 +663,9 @@ func verifyP2PKLockedProof(proof cashu.Proof) error {
keys = append(keys, p2pkTags.Pubkeys...)
}

if len(p2pkWitness.Signatures) < 1 {
return nut11.InvalidWitness
}
if !nut11.HasValidSignatures(hash[:], p2pkWitness, signaturesRequired, keys) {
return nut11.NotEnoughSignaturesErr
}
Expand All @@ -672,90 +674,76 @@ func verifyP2PKLockedProof(proof cashu.Proof) error {
}

func verifyP2PKBlindedMessages(proofs cashu.Proofs, blindedMessages cashu.BlindedMessages) error {
isSigAll := false
for _, proof := range proofs {
secret, err := nut10.DeserializeSecret(proof.Secret)
if err != nil {
continue
}
secret, err := nut10.DeserializeSecret(proofs[0].Secret)
if err != nil {
return cashu.BuildCashuError(err.Error(), cashu.StandardErrCode)
}
pubkeys, err := nut11.PublicKeys(secret)
if err != nil {
return err
}

if nut11.IsSigAll(secret) {
isSigAll = true
break
}
signaturesRequired := 1
p2pkTags, err := nut11.ParseP2PKTags(secret.Tags)
if err != nil {
return err
}
if p2pkTags.NSigs > 0 {
signaturesRequired = p2pkTags.NSigs
}

if isSigAll {
secret, err := nut10.DeserializeSecret(proofs[0].Secret)
// Check that the conditions across all proofs are the same
for _, proof := range proofs {
secret, err := nut10.DeserializeSecret(proof.Secret)
if err != nil {
return cashu.BuildCashuError(err.Error(), cashu.StandardErrCode)
}
pubkeys, err := nut11.PublicKeys(secret)
if err != nil {
return err
// all flags need to be SIG_ALL
if !nut11.IsSigAll(secret) {
return nut11.AllSigAllFlagsErr
}

signaturesRequired := 1
currentSignaturesRequired := 1
p2pkTags, err := nut11.ParseP2PKTags(secret.Tags)
if err != nil {
return err
}
if p2pkTags.NSigs > 0 {
signaturesRequired = p2pkTags.NSigs
currentSignaturesRequired = p2pkTags.NSigs
}

for _, proof := range proofs {
secret, err := nut10.DeserializeSecret(proof.Secret)
if err != nil {
return cashu.BuildCashuError(err.Error(), cashu.StandardErrCode)
}
// all flags need to be SIG_ALL
if !nut11.IsSigAll(secret) {
return nut11.AllSigAllFlagsErr
}

currentSignaturesRequired := 1
p2pkTags, err := nut11.ParseP2PKTags(secret.Tags)
if err != nil {
return err
}
if p2pkTags.NSigs > 0 {
currentSignaturesRequired = p2pkTags.NSigs
}

currentKeys, err := nut11.PublicKeys(secret)
if err != nil {
return err
}
currentKeys, err := nut11.PublicKeys(secret)
if err != nil {
return err
}

// list of valid keys should be the same
// across all proofs
if !reflect.DeepEqual(pubkeys, currentKeys) {
return nut11.SigAllKeysMustBeEqualErr
}
// list of valid keys should be the same
// across all proofs
if !reflect.DeepEqual(pubkeys, currentKeys) {
return nut11.SigAllKeysMustBeEqualErr
}

// all n_sigs must be same
if signaturesRequired != currentSignaturesRequired {
return nut11.NSigsMustBeEqualErr
}
// all n_sigs must be same
if signaturesRequired != currentSignaturesRequired {
return nut11.NSigsMustBeEqualErr
}
}

for _, bm := range blindedMessages {
hash := sha256.Sum256([]byte(bm.B_))
for _, bm := range blindedMessages {
B_bytes, err := hex.DecodeString(bm.B_)
if err != nil {
return cashu.BuildCashuError(err.Error(), cashu.StandardErrCode)
}
hash := sha256.Sum256(B_bytes)

var witness nut11.P2PKWitness
err := json.Unmarshal([]byte(bm.Witness), &witness)
if err != nil {
errmsg := fmt.Sprintf("invalid witness: %v", err)
return cashu.BuildCashuError(errmsg, nut11.NUT11ErrCode)
}
if len(witness.Signatures) < 1 {
return nut11.EmptyWitnessErr
}
var witness nut11.P2PKWitness
err = json.Unmarshal([]byte(bm.Witness), &witness)
if err != nil || len(witness.Signatures) < 1 {
return nut11.InvalidWitness
}

if !nut11.HasValidSignatures(hash[:], witness, signaturesRequired, pubkeys) {
return nut11.NotEnoughSignaturesErr
}
if !nut11.HasValidSignatures(hash[:], witness, signaturesRequired, pubkeys) {
return nut11.NotEnoughSignaturesErr
}
}

Expand Down
142 changes: 142 additions & 0 deletions mint/mint_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,14 @@ import (
"os"
"path/filepath"
"testing"
"time"

"github.com/btcsuite/btcd/btcec/v2"
btcdocker "github.com/elnosh/btc-docker-test"
"github.com/elnosh/gonuts/cashu"
"github.com/elnosh/gonuts/cashu/nuts/nut04"
"github.com/elnosh/gonuts/cashu/nuts/nut05"
"github.com/elnosh/gonuts/cashu/nuts/nut11"
"github.com/elnosh/gonuts/crypto"
"github.com/elnosh/gonuts/mint"
"github.com/elnosh/gonuts/testutils"
Expand Down Expand Up @@ -658,5 +661,144 @@ func TestMintLimits(t *testing.T) {
if err != nil {
t.Fatalf("got unexpected error requesting mint quote: %v", err)
}
}

func TestNUT11P2PK(t *testing.T) {
lock, _ := btcec.NewPrivateKey()

p2pkMintPath := filepath.Join(".", "p2pkmint")
p2pkMint, err := testutils.CreateTestMint(lnd1, p2pkMintPath, dbMigrationPath, 0, mint.MintLimits{})
if err != nil {
t.Fatal(err)
}
defer func() {
os.RemoveAll(p2pkMintPath)
}()

keyset := p2pkMint.GetActiveKeyset()

var mintAmount uint64 = 1500
lockedProofs, err := testutils.GetProofsWithLock(mintAmount, lock.PubKey(), nut11.P2PKTags{}, p2pkMint, lnd2)
if err != nil {
t.Fatalf("error getting locked proofs: %v", err)
}
blindedMessages, _, _, _ := testutils.CreateBlindedMessages(mintAmount, keyset)

// swap with proofs that do not have valid witness
_, err = p2pkMint.Swap(lockedProofs, blindedMessages)
if !errors.Is(err, nut11.InvalidWitness) {
t.Fatalf("expected error '%v' but got '%v' instead", nut11.InvalidWitness, err)
}

// invalid proofs signed with another key
anotherKey, _ := btcec.NewPrivateKey()
invalidProofs, _ := nut11.AddSignatureToInputs(lockedProofs, anotherKey)
_, err = p2pkMint.Swap(invalidProofs, blindedMessages)
if !errors.Is(err, nut11.NotEnoughSignaturesErr) {
t.Fatalf("expected error '%v' but got '%v' instead", nut11.NotEnoughSignaturesErr, err)
}

// valid signed proofs
signedProofs, _ := nut11.AddSignatureToInputs(lockedProofs, lock)
_, err = p2pkMint.Swap(signedProofs, blindedMessages)
if err != nil {
t.Fatalf("unexpected error in swap: %v", err)
}

// test multisig
key1, _ := btcec.NewPrivateKey()
key2, _ := btcec.NewPrivateKey()
multisigKeys := []*btcec.PublicKey{key1.PubKey(), key2.PubKey()}
tags := nut11.P2PKTags{
Sigflag: nut11.SIGALL,
NSigs: 2,
Pubkeys: multisigKeys,
}
multisigProofs, err := testutils.GetProofsWithLock(mintAmount, lock.PubKey(), tags, p2pkMint, lnd2)
if err != nil {
t.Fatalf("error getting locked proofs: %v", err)
}

// proofs with only 1 signature but require 2
blindedMessages, _, _, _ = testutils.CreateBlindedMessages(mintAmount, keyset)
notEnoughSigsProofs, _ := nut11.AddSignatureToInputs(multisigProofs, lock)
_, err = p2pkMint.Swap(notEnoughSigsProofs, blindedMessages)
if !errors.Is(err, nut11.NotEnoughSignaturesErr) {
t.Fatalf("expected error '%v' but got '%v' instead", nut11.NotEnoughSignaturesErr, err)
}

signingKeys := []*btcec.PrivateKey{key1, key2}
// enough signatures but blinded messages not signed
signedProofs, _ = testutils.AddSignaturesToInputs(multisigProofs, signingKeys)
_, err = p2pkMint.Swap(signedProofs, blindedMessages)
if !errors.Is(err, nut11.InvalidWitness) {
t.Fatalf("expected error '%v' but got '%v' instead", nut11.InvalidWitness, err)
}

// inputs and outputs with valid signatures
signedBlindedMessages, _ := testutils.AddSignaturesToOutputs(blindedMessages, signingKeys)
_, err = p2pkMint.Swap(signedProofs, signedBlindedMessages)
if err != nil {
t.Fatalf("unexpected error in swap: %v", err)
}

// test with locktime
tags = nut11.P2PKTags{
Locktime: time.Now().Add(time.Minute * 1).Unix(),
}
locktimeProofs, err := testutils.GetProofsWithLock(mintAmount, lock.PubKey(), tags, p2pkMint, lnd2)
if err != nil {
t.Fatalf("error getting locked proofs: %v", err)
}
blindedMessages, _, _, _ = testutils.CreateBlindedMessages(mintAmount, keyset)
// unsigned proofs
_, err = p2pkMint.Swap(locktimeProofs, blindedMessages)
if !errors.Is(err, nut11.InvalidWitness) {
t.Fatalf("expected error '%v' but got '%v' instead", nut11.InvalidWitness, err)
}

signedProofs, _ = nut11.AddSignatureToInputs(locktimeProofs, lock)
_, err = p2pkMint.Swap(signedProofs, blindedMessages)
if err != nil {
t.Fatalf("unexpected error in swap: %v", err)
}

tags = nut11.P2PKTags{
Locktime: time.Now().Add(-(time.Minute * 10)).Unix(),
}
locktimeProofs, err = testutils.GetProofsWithLock(mintAmount, lock.PubKey(), tags, p2pkMint, lnd2)
if err != nil {
t.Fatalf("error getting locked proofs: %v", err)
}

blindedMessages, _, _, _ = testutils.CreateBlindedMessages(mintAmount, keyset)
// locktime expired so spendable without signature
_, err = p2pkMint.Swap(locktimeProofs, blindedMessages)
if err != nil {
t.Fatalf("unexpected error in swap: %v", err)
}

// test locktime expired but with refund keys
tags = nut11.P2PKTags{
Locktime: time.Now().Add(-(time.Minute * 10)).Unix(),
Refund: []*btcec.PublicKey{key1.PubKey()},
}
locktimeProofs, err = testutils.GetProofsWithLock(mintAmount, lock.PubKey(), tags, p2pkMint, lnd2)
if err != nil {
t.Fatalf("error getting locked proofs: %v", err)
}
// unsigned proofs should fail because there were refund pubkeys in the tags
_, err = p2pkMint.Swap(locktimeProofs, blindedMessages)
if err == nil {
t.Fatal("expected error but got 'nil' instead")
}

// sign with refund pubkey
signedProofs, _ = testutils.AddSignaturesToInputs(locktimeProofs, []*btcec.PrivateKey{key1})
blindedMessages, _, _, _ = testutils.CreateBlindedMessages(mintAmount, keyset)
_, err = p2pkMint.Swap(signedProofs, blindedMessages)
if err != nil {
t.Fatalf("unexpected error in swap: %v", err)
}

}
Loading

0 comments on commit 5306122

Please sign in to comment.