From 93734294c019dabc794d5b0e76e8c65f8b28b0a5 Mon Sep 17 00:00:00 2001 From: elnosh Date: Tue, 17 Sep 2024 17:46:03 -0500 Subject: [PATCH 1/4] dleq proofs crypto --- cashu/cashu.go | 12 +++++ crypto/bdhke.go | 97 +++++++++++++++++++++++++++++++++++++++ crypto/bdhke_test.go | 53 +++++++++++++++++++++ wallet/storage/storage.go | 3 ++ 4 files changed, 165 insertions(+) diff --git a/cashu/cashu.go b/cashu/cashu.go index b07082c..cc1aab3 100644 --- a/cashu/cashu.go +++ b/cashu/cashu.go @@ -66,6 +66,9 @@ type BlindedSignature struct { Amount uint64 `json:"amount"` C_ string `json:"C_"` Id string `json:"id"` + // doing pointer here so that omitempty works. + // an empty struct would still get marshalled + DLEQ *DLEQProof `json:"dleq,omitempty"` } type BlindedSignatures []BlindedSignature @@ -77,10 +80,19 @@ type Proof struct { Secret string `json:"secret"` C string `json:"C"` Witness string `json:"witness,omitempty"` + // doing pointer here so that omitempty works. + // an empty struct would still get marshalled + DLEQ *DLEQProof `json:"dleq,omitempty"` } type Proofs []Proof +type DLEQProof struct { + E string `json:"e"` + S string `json:"s"` + R string `json:"r,omitempty"` +} + // Amount returns the total amount from // the array of Proof func (proofs Proofs) Amount() uint64 { diff --git a/crypto/bdhke.go b/crypto/bdhke.go index 72ba1fb..c48e56d 100644 --- a/crypto/bdhke.go +++ b/crypto/bdhke.go @@ -7,8 +7,10 @@ package crypto import ( "crypto/sha256" "encoding/binary" + "encoding/hex" "errors" "math" + "reflect" "github.com/decred/dcrd/dcrec/secp256k1/v4" ) @@ -124,3 +126,98 @@ func verify(Y *secp256k1.PublicKey, k *secp256k1.PrivateKey, C *secp256k1.Public return C.IsEqual(pk) } + +func HashE(publicKeys []*secp256k1.PublicKey) [32]byte { + var keys string + for _, pk := range publicKeys { + keys += hex.EncodeToString(pk.SerializeUncompressed()) + } + return sha256.Sum256([]byte(keys)) +} + +func GenerateDLEQ( + a *secp256k1.PrivateKey, + B_ *secp256k1.PublicKey, + C_ *secp256k1.PublicKey, +) (*secp256k1.PrivateKey, *secp256k1.PrivateKey) { + // random r + var r *secp256k1.PrivateKey + var err error + for r == nil || err != nil { + r, err = secp256k1.GeneratePrivateKey() + } + + // r*B' + var B_Point, R2Point secp256k1.JacobianPoint + B_.AsJacobian(&B_Point) + secp256k1.ScalarMultNonConst(&r.Key, &B_Point, &R2Point) + R2Point.ToAffine() + + // R1 = r*G + R1 := r.PubKey() + // R2 = r*B' + R2 := secp256k1.NewPublicKey(&R2Point.X, &R2Point.Y) + + // e = hash(R1,R2,A,C') + ebytes := HashE([]*secp256k1.PublicKey{R1, R2, a.PubKey(), C_}) + e := secp256k1.PrivKeyFromBytes(ebytes[:]) + + // s = r + e*a + ea := e.Key.Mul(&a.Key) + scalar := r.Key.Add(ea) + s := secp256k1.NewPrivateKey(scalar) + + // can't return e here because value was modified in Mul + // so getting private key e from hash + return secp256k1.PrivKeyFromBytes(ebytes[:]), s +} + +func VerifyDLEQ( + e *secp256k1.PrivateKey, + s *secp256k1.PrivateKey, + A *secp256k1.PublicKey, + B_ *secp256k1.PublicKey, + C_ *secp256k1.PublicKey, +) bool { + var R1Point, R2Point secp256k1.JacobianPoint + + // s*G + var SPoint secp256k1.JacobianPoint + s.PubKey().AsJacobian(&SPoint) + + // -e*A + var eNeg secp256k1.ModNScalar + eNeg.NegateVal(&e.Key) + var APoint, eA_Point secp256k1.JacobianPoint + A.AsJacobian(&APoint) + secp256k1.ScalarMultNonConst(&eNeg, &APoint, &eA_Point) + eA_Point.ToAffine() + + // R1 = s*G - e*A + secp256k1.AddNonConst(&SPoint, &eA_Point, &R1Point) + R1Point.ToAffine() + + // s*B' + var B_Point, sB_Point secp256k1.JacobianPoint + B_.AsJacobian(&B_Point) + secp256k1.ScalarMultNonConst(&s.Key, &B_Point, &sB_Point) + sB_Point.ToAffine() + + // -e*C' + var C_Point, eC_Point secp256k1.JacobianPoint + C_.AsJacobian(&C_Point) + secp256k1.ScalarMultNonConst(&eNeg, &C_Point, &eC_Point) + eC_Point.ToAffine() + + // R2 = s*B' - e*C' + secp256k1.AddNonConst(&sB_Point, &eC_Point, &R2Point) + R2Point.ToAffine() + + R1PublicKey := secp256k1.NewPublicKey(&R1Point.X, &R1Point.Y) + R2PublicKey := secp256k1.NewPublicKey(&R2Point.X, &R2Point.Y) + + hash := HashE([]*secp256k1.PublicKey{R1PublicKey, R2PublicKey, A, C_}) + ebytes := e.Serialize() + + return reflect.DeepEqual(ebytes, hash[:]) +} diff --git a/crypto/bdhke_test.go b/crypto/bdhke_test.go index 953b977..a66d751 100644 --- a/crypto/bdhke_test.go +++ b/crypto/bdhke_test.go @@ -166,3 +166,56 @@ func TestVerify(t *testing.T) { t.Error("failed verification") } } + +func TestHashE(t *testing.T) { + R1Hex, _ := hex.DecodeString("020000000000000000000000000000000000000000000000000000000000000001") + R2Hex, _ := hex.DecodeString("020000000000000000000000000000000000000000000000000000000000000001") + KHex, _ := hex.DecodeString("020000000000000000000000000000000000000000000000000000000000000001") + C_Hex, _ := hex.DecodeString("02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2") + + R1, _ := secp256k1.ParsePubKey(R1Hex) + R2, _ := secp256k1.ParsePubKey(R2Hex) + K, _ := secp256k1.ParsePubKey(KHex) + C_, _ := secp256k1.ParsePubKey(C_Hex) + + hash := HashE([]*secp256k1.PublicKey{R1, R2, K, C_}) + hashHex := hex.EncodeToString(hash[:]) + expected := "a4dc034b74338c28c6bc3ea49731f2a24440fc7c4affc08b31a93fc9fbe6401e" + + if hashHex != expected { + t.Errorf("expected '%v' but got '%v' instead\n", expected, hashHex) + } +} + +func TestGenerateDLEQ(t *testing.T) { + r, err := secp256k1.GeneratePrivateKey() + if err != nil { + t.Fatalf("unexpected error generating private key: %v", err) + } + + B_, _, _ := BlindMessage("test_message", r) + C_ := SignBlindedMessage(B_, r) + + e, s := GenerateDLEQ(r, B_, C_) + if !VerifyDLEQ(e, s, r.PubKey(), B_, C_) { + t.Errorf("VerifyDLEQ failed") + } +} + +func TestVerifyDLEQ(t *testing.T) { + eHex, _ := hex.DecodeString("9818e061ee51d5c8edc3342369a554998ff7b4381c8652d724cdf46429be73d9") + sHex, _ := hex.DecodeString("9818e061ee51d5c8edc3342369a554998ff7b4381c8652d724cdf46429be73da") + AHex, _ := hex.DecodeString("0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798") + B_Hex, _ := hex.DecodeString("02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2") + C_Hex, _ := hex.DecodeString("02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2") + + e := secp256k1.PrivKeyFromBytes(eHex) + s := secp256k1.PrivKeyFromBytes(sHex) + A, _ := secp256k1.ParsePubKey(AHex) + B_, _ := secp256k1.ParsePubKey(B_Hex) + C_, _ := secp256k1.ParsePubKey(C_Hex) + + if !VerifyDLEQ(e, s, A, B_, C_) { + t.Errorf("VerifyDLEQ failed") + } +} diff --git a/wallet/storage/storage.go b/wallet/storage/storage.go index e7cdf59..c51d792 100644 --- a/wallet/storage/storage.go +++ b/wallet/storage/storage.go @@ -27,15 +27,18 @@ type DB interface { SaveMnemonicSeed(string, []byte) GetSeed() []byte GetMnemonic() string + SaveProof(cashu.Proof) error GetProofsByKeysetId(string) cashu.Proofs GetProofs() cashu.Proofs DeleteProof(string) error + SaveKeyset(*crypto.WalletKeyset) error GetKeysets() crypto.KeysetsMap GetKeyset(string) *crypto.WalletKeyset IncrementKeysetCounter(string, uint32) error GetKeysetCounter(string) uint32 + SaveInvoice(Invoice) error GetInvoice(string) *Invoice GetInvoices() []Invoice From 823f6ce1876d67c608b1834d1577ad2f898f3fbd Mon Sep 17 00:00:00 2001 From: elnosh Date: Wed, 18 Sep 2024 12:50:44 -0500 Subject: [PATCH 2/4] wallet - verify DLEQ proofs for mints, swaps and receives --- cashu/cashu.go | 23 +++++- cashu/nuts/nut12/nut12.go | 136 +++++++++++++++++++++++++++++++++ cashu/nuts/nut12/nut12_test.go | 47 ++++++++++++ wallet/wallet.go | 49 +++++++++++- wallet/wallet_test.go | 4 +- 5 files changed, 250 insertions(+), 9 deletions(-) create mode 100644 cashu/nuts/nut12/nut12.go create mode 100644 cashu/nuts/nut12/nut12_test.go diff --git a/cashu/cashu.go b/cashu/cashu.go index cc1aab3..b5127cf 100644 --- a/cashu/cashu.go +++ b/cashu/cashu.go @@ -210,10 +210,17 @@ type TokenV4Proof struct { } type ProofV4 struct { - Amount uint64 `json:"a"` - Secret string `json:"s"` - C []byte `json:"c"` - Witness string `json:"w,omitempty"` + Amount uint64 `json:"a"` + Secret string `json:"s"` + C []byte `json:"c"` + Witness string `json:"w,omitempty"` + DLEQ *DLEQV4 `json:"d,omitempty"` +} + +type DLEQV4 struct { + E []byte `json:"e"` + S []byte `json:"s"` + R []byte `json:"r"` } func NewTokenV4(proofs Proofs, mint string, unit string) (TokenV4, error) { @@ -283,6 +290,14 @@ func (t TokenV4) Proofs() Proofs { C: hex.EncodeToString(proofV4.C), Witness: proofV4.Witness, } + if proofV4.DLEQ != nil { + dleq := &DLEQProof{ + E: hex.EncodeToString(proofV4.DLEQ.E), + S: hex.EncodeToString(proofV4.DLEQ.S), + R: hex.EncodeToString(proofV4.DLEQ.R), + } + proof.DLEQ = dleq + } proofs = append(proofs, proof) } } diff --git a/cashu/nuts/nut12/nut12.go b/cashu/nuts/nut12/nut12.go new file mode 100644 index 0000000..bfdd551 --- /dev/null +++ b/cashu/nuts/nut12/nut12.go @@ -0,0 +1,136 @@ +package nut12 + +import ( + "encoding/hex" + + "github.com/decred/dcrd/dcrec/secp256k1/v4" + "github.com/elnosh/gonuts/cashu" + "github.com/elnosh/gonuts/crypto" +) + +// VerifyProofsDLEQ will verify the DLEQ proofs if present. If the DLEQ proofs are not present +// it will continue and return true +func VerifyProofsDLEQ(proofs cashu.Proofs, keysets map[string]crypto.WalletKeyset) bool { + for _, proof := range proofs { + if proof.DLEQ == nil { + continue + } else { + keyset, ok := keysets[proof.Id] + if !ok { + return false + } + + pubkey, ok := keyset.PublicKeys[proof.Amount] + if !ok { + return false + } + + if !VerifyProofDLEQ(proof, pubkey) { + return false + } + } + } + return true +} + +func VerifyProofDLEQ( + proof cashu.Proof, + A *secp256k1.PublicKey, +) bool { + e, s, r, err := ParseDLEQ(*proof.DLEQ) + if err != nil || r == nil { + return false + } + + B_, _, err := crypto.BlindMessage(proof.Secret, r) + if err != nil { + return false + } + + CBytes, err := hex.DecodeString(proof.C) + if err != nil { + return false + } + + C, err := secp256k1.ParsePubKey(CBytes) + if err != nil { + return false + } + + var CPoint, APoint secp256k1.JacobianPoint + C.AsJacobian(&CPoint) + A.AsJacobian(&APoint) + + // C' = C + r*A + var C_Point, rAPoint secp256k1.JacobianPoint + secp256k1.ScalarMultNonConst(&r.Key, &APoint, &rAPoint) + rAPoint.ToAffine() + secp256k1.AddNonConst(&CPoint, &rAPoint, &C_Point) + C_Point.ToAffine() + C_ := secp256k1.NewPublicKey(&C_Point.X, &C_Point.Y) + + return crypto.VerifyDLEQ(e, s, A, B_, C_) +} + +func VerifyBlindSignatureDLEQ( + dleq cashu.DLEQProof, + A *secp256k1.PublicKey, + B_str string, + C_str string, +) bool { + e, s, _, err := ParseDLEQ(dleq) + if err != nil { + return false + } + + B_bytes, err := hex.DecodeString(B_str) + if err != nil { + return false + } + B_, err := secp256k1.ParsePubKey(B_bytes) + if err != nil { + return false + } + + C_bytes, err := hex.DecodeString(C_str) + if err != nil { + return false + } + C_, err := secp256k1.ParsePubKey(C_bytes) + if err != nil { + return false + } + + return crypto.VerifyDLEQ(e, s, A, B_, C_) +} + +func ParseDLEQ(dleq cashu.DLEQProof) ( + *secp256k1.PrivateKey, + *secp256k1.PrivateKey, + *secp256k1.PrivateKey, + error, +) { + ebytes, err := hex.DecodeString(dleq.E) + if err != nil { + return nil, nil, nil, err + } + e := secp256k1.PrivKeyFromBytes(ebytes) + + sbytes, err := hex.DecodeString(dleq.S) + if err != nil { + return nil, nil, nil, err + } + s := secp256k1.PrivKeyFromBytes(sbytes) + + if dleq.R == "" { + return e, s, nil, nil + } + + rbytes, err := hex.DecodeString(dleq.R) + if err != nil { + return nil, nil, nil, err + } + r := secp256k1.PrivKeyFromBytes(rbytes) + + return e, s, r, nil +} diff --git a/cashu/nuts/nut12/nut12_test.go b/cashu/nuts/nut12/nut12_test.go new file mode 100644 index 0000000..abce905 --- /dev/null +++ b/cashu/nuts/nut12/nut12_test.go @@ -0,0 +1,47 @@ +package nut12 + +import ( + "encoding/hex" + "testing" + + "github.com/decred/dcrd/dcrec/secp256k1/v4" + "github.com/elnosh/gonuts/cashu" +) + +func TestVerifyBlindSiagnatureDLEQ(t *testing.T) { + Ahex, _ := hex.DecodeString("0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798") + A, _ := secp256k1.ParsePubKey(Ahex) + B_ := "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2" + C_ := "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2" + + dleq := cashu.DLEQProof{ + E: "9818e061ee51d5c8edc3342369a554998ff7b4381c8652d724cdf46429be73d9", + S: "9818e061ee51d5c8edc3342369a554998ff7b4381c8652d724cdf46429be73da", + } + + if !VerifyBlindSignatureDLEQ(dleq, A, B_, C_) { + t.Errorf("DLEQ verification on blind signature failed") + } + +} + +func TestVerifyProofDLEQ(t *testing.T) { + Ahex, _ := hex.DecodeString("0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798") + A, _ := secp256k1.ParsePubKey(Ahex) + + proof := cashu.Proof{ + Amount: 1, + Id: "00882760bfa2eb41", + Secret: "daf4dd00a2b68a0858a80450f52c8a7d2ccf87d375e43e216e0c571f089f63e9", + C: "024369d2d22a80ecf78f3937da9d5f30c1b9f74f0c32684d583cca0fa6a61cdcfc", + DLEQ: &cashu.DLEQProof{ + E: "b31e58ac6527f34975ffab13e70a48b6d2b0d35abc4b03f0151f09ee1a9763d4", + S: "8fbae004c59e754d71df67e392b6ae4e29293113ddc2ec86592a0431d16306d8", + R: "a6d13fcd7a18442e6076f5e1e7c887ad5de40a019824bdfa9fe740d302e8d861", + }, + } + + if !VerifyProofDLEQ(proof, A) { + t.Errorf("DLEQ verification on proof failed") + } +} diff --git a/wallet/wallet.go b/wallet/wallet.go index f71877e..05199a7 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -25,6 +25,7 @@ import ( "github.com/elnosh/gonuts/cashu/nuts/nut09" "github.com/elnosh/gonuts/cashu/nuts/nut10" "github.com/elnosh/gonuts/cashu/nuts/nut11" + "github.com/elnosh/gonuts/cashu/nuts/nut12" "github.com/elnosh/gonuts/cashu/nuts/nut13" "github.com/elnosh/gonuts/crypto" "github.com/elnosh/gonuts/wallet/storage" @@ -397,7 +398,7 @@ func (w *Wallet) MintTokens(quoteId string) (cashu.Proofs, error) { } // unblind the signatures from the promises and build the proofs - proofs, err := constructProofs(mintResponse.Signatures, secrets, rs, activeKeyset) + proofs, err := constructProofs(mintResponse.Signatures, blindedMessages, secrets, rs, activeKeyset) if err != nil { return nil, fmt.Errorf("error constructing proofs: %v", err) } @@ -477,6 +478,27 @@ func (w *Wallet) Receive(token cashu.Token, swapToTrusted bool) (uint64, error) proofsToSwap := token.Proofs() tokenMint := token.Mint() + var keysets map[string]crypto.WalletKeyset + mint, ok := w.mints[tokenMint] + if !ok { + // get keysets if mint not trusted + var err error + keysets, err = GetMintActiveKeysets(tokenMint) + if err != nil { + return 0, err + } + } else { + keysets = mint.activeKeysets + for id, keyset := range mint.inactiveKeysets { + keysets[id] = keyset + } + } + + // verify DLEQ in proofs if present + if !nut12.VerifyProofsDLEQ(proofsToSwap, keysets) { + return 0, errors.New("invalid DLEQ proof") + } + if swapToTrusted { trustedMintProofs, err := w.swapToTrusted(token) if err != nil { @@ -571,7 +593,7 @@ func (w *Wallet) swap(proofsToSwap cashu.Proofs, mintURL string) (cashu.Proofs, } // unblind signatures to get proofs and save them to db - proofs, err := constructProofs(swapResponse.Signatures, secrets, rs, activeSatKeyset) + proofs, err := constructProofs(swapResponse.Signatures, outputs, secrets, rs, activeSatKeyset) if err != nil { return nil, fmt.Errorf("wallet.ConstructProofs: %v", err) } @@ -944,7 +966,7 @@ func (w *Wallet) swapToSend( w.db.DeleteProof(proof.Secret) } - proofsFromSwap, err := constructProofs(swapResponse.Signatures, secrets, rs, activeSatKeyset) + proofsFromSwap, err := constructProofs(swapResponse.Signatures, blindedMessages, secrets, rs, activeSatKeyset) if err != nil { return nil, fmt.Errorf("wallet.ConstructProofs: %v", err) } @@ -1217,6 +1239,7 @@ func blindedMessagesFromLock(splitAmounts []uint64, keysetId string, lockPubkey // constructProofs unblinds the blindedSignatures and returns the proofs func constructProofs( blindedSignatures cashu.BlindedSignatures, + blindedMessages cashu.BlindedMessages, secrets []string, rs []*secp256k1.PrivateKey, keyset *crypto.WalletKeyset, @@ -1233,6 +1256,25 @@ func constructProofs( return nil, errors.New("key not found") } + var dleq *cashu.DLEQProof + // verify DLEQ if present + if blindedSignature.DLEQ != nil { + if !nut12.VerifyBlindSignatureDLEQ( + *blindedSignature.DLEQ, + pubkey, + blindedMessages[i].B_, + blindedSignature.C_, + ) { + return nil, errors.New("got blinded signature with invalid DLEQ proof") + } else { + dleq = &cashu.DLEQProof{ + E: blindedSignature.DLEQ.E, + S: blindedSignature.DLEQ.S, + R: hex.EncodeToString(rs[i].Serialize()), + } + } + } + C, err := unblindSignature(blindedSignature.C_, rs[i], pubkey) if err != nil { return nil, err @@ -1243,6 +1285,7 @@ func constructProofs( Secret: secrets[i], C: C, Id: blindedSignature.Id, + DLEQ: dleq, } proofs[i] = proof } diff --git a/wallet/wallet_test.go b/wallet/wallet_test.go index 72f16f9..77ce89e 100644 --- a/wallet/wallet_test.go +++ b/wallet/wallet_test.go @@ -101,7 +101,7 @@ func TestConstructProofs(t *testing.T) { rs[i] = secp256k1.PrivKeyFromBytes(key) } - proofs, err := constructProofs(signatures, secrets, rs, keyset) + proofs, err := constructProofs(signatures, cashu.BlindedMessages{}, secrets, rs, keyset) if err != nil { t.Fatal(err) } @@ -171,7 +171,7 @@ func TestConstructProofsError(t *testing.T) { rs[i] = secp256k1.PrivKeyFromBytes(key) } - proofs, err := constructProofs(test.signatures, test.secrets, rs, test.keyset) + proofs, err := constructProofs(test.signatures, cashu.BlindedMessages{}, test.secrets, rs, test.keyset) if proofs != nil { t.Errorf("expected nil proofs but got '%v'", proofs) } From 13720f2c1970190aa1b56d180313fe7a32c05f9e Mon Sep 17 00:00:00 2001 From: elnosh Date: Thu, 19 Sep 2024 10:28:36 -0500 Subject: [PATCH 3/4] wallet - flag to include DLEQ proofs in token --- cashu/cashu.go | 39 +++++++++++++++++++++++++++++-- cmd/nutw/nutw.go | 23 +++++++++++++----- wallet/wallet_integration_test.go | 20 ++++++++-------- 3 files changed, 64 insertions(+), 18 deletions(-) diff --git a/cashu/cashu.go b/cashu/cashu.go index b5127cf..3eea538 100644 --- a/cashu/cashu.go +++ b/cashu/cashu.go @@ -135,7 +135,13 @@ type TokenV3Proof struct { Proofs Proofs `json:"proofs"` } -func NewTokenV3(proofs Proofs, mint string, unit string) TokenV3 { +func NewTokenV3(proofs Proofs, mint, unit string, includeDLEQ bool) TokenV3 { + if !includeDLEQ { + for i := 0; i < len(proofs); i++ { + proofs[i].DLEQ = nil + } + } + tokenProof := TokenV3Proof{Mint: mint, Proofs: proofs} return TokenV3{Token: []TokenV3Proof{tokenProof}, Unit: unit} } @@ -223,7 +229,7 @@ type DLEQV4 struct { R []byte `json:"r"` } -func NewTokenV4(proofs Proofs, mint string, unit string) (TokenV4, error) { +func NewTokenV4(proofs Proofs, mint, unit string, includeDLEQ bool) (TokenV4, error) { proofsMap := make(map[string][]ProofV4) for _, proof := range proofs { C, err := hex.DecodeString(proof.C) @@ -236,6 +242,35 @@ func NewTokenV4(proofs Proofs, mint string, unit string) (TokenV4, error) { C: C, Witness: proof.Witness, } + if includeDLEQ { + if proof.DLEQ != nil { + e, err := hex.DecodeString(proof.DLEQ.E) + if err != nil { + return TokenV4{}, fmt.Errorf("invalid e in DLEQ proof: %v", err) + } + s, err := hex.DecodeString(proof.DLEQ.S) + if err != nil { + return TokenV4{}, fmt.Errorf("invalid s in DLEQ proof: %v", err) + } + + var r []byte + if len(proof.DLEQ.R) > 0 { + r, err = hex.DecodeString(proof.DLEQ.R) + if err != nil { + return TokenV4{}, fmt.Errorf("invalid r in DLEQ proof: %v", err) + } + } else { + return TokenV4{}, errors.New("r in DLEQ proof cannot be empty") + } + + dleq := &DLEQV4{ + E: e, + S: s, + R: r, + } + proofV4.DLEQ = dleq + } + } proofsMap[proof.Id] = append(proofsMap[proof.Id], proofV4) } diff --git a/cmd/nutw/nutw.go b/cmd/nutw/nutw.go index bda7b15..1a469a6 100644 --- a/cmd/nutw/nutw.go +++ b/cmd/nutw/nutw.go @@ -276,9 +276,10 @@ func mintTokens(paymentRequest string) error { } const ( - lockFlag = "lock" - noFeesFlag = "no-fees" - legacyFlag = "legacy" + lockFlag = "lock" + noFeesFlag = "no-fees" + legacyFlag = "legacy" + includeDLEQFlag = "include-dleq" ) var sendCmd = &cli.Command{ @@ -301,6 +302,11 @@ var sendCmd = &cli.Command{ Usage: "generate token in legacy (V3) format", DisableDefaultText: true, }, + &cli.BoolFlag{ + Name: includeDLEQFlag, + Usage: "include DLEQ proofs", + DisableDefaultText: true, + }, }, Action: send, } @@ -347,11 +353,16 @@ func send(ctx *cli.Context) error { } } + includeDLEQ := false + if ctx.Bool(includeDLEQFlag) { + includeDLEQ = true + } + var token cashu.Token if ctx.Bool(legacyFlag) { - token = cashu.NewTokenV3(proofsToSend, selectedMint, "sat") + token = cashu.NewTokenV3(proofsToSend, selectedMint, "sat", includeDLEQ) } else { - token, err = cashu.NewTokenV4(proofsToSend, selectedMint, "sat") + token, err = cashu.NewTokenV4(proofsToSend, selectedMint, "sat", includeDLEQ) if err != nil { printErr(fmt.Errorf("could not serialize token: %v", err)) } @@ -404,7 +415,7 @@ func pay(ctx *cli.Context) error { var p2pkLockCmd = &cli.Command{ Name: "p2pk-lock", - Usage: "Retrieves a public key to which ecash can locked", + Usage: "Retrieves a public key to which ecash can be locked", Before: setupWallet, Action: p2pkLock, } diff --git a/wallet/wallet_integration_test.go b/wallet/wallet_integration_test.go index 34b5974..319c2ad 100644 --- a/wallet/wallet_integration_test.go +++ b/wallet/wallet_integration_test.go @@ -277,7 +277,7 @@ func TestReceive(t *testing.T) { if err != nil { t.Fatalf("got unexpected error in send: %v", err) } - token, _ := cashu.NewTokenV4(proofsToSend, mint2URL, testutils.SAT_UNIT) + token, _ := cashu.NewTokenV4(proofsToSend, mint2URL, testutils.SAT_UNIT, false) // test receive swap == true _, err = testWallet.Receive(token, true) @@ -298,7 +298,7 @@ func TestReceive(t *testing.T) { if err != nil { t.Fatalf("got unexpected error in send: %v", err) } - token, _ = cashu.NewTokenV4(proofsToSend, mint2URL, testutils.SAT_UNIT) + token, _ = cashu.NewTokenV4(proofsToSend, mint2URL, testutils.SAT_UNIT, false) // test receive swap == false _, err = testWallet.Receive(token, false) @@ -347,7 +347,7 @@ func TestReceiveFees(t *testing.T) { if err != nil { t.Fatalf("got unexpected error in send: %v", err) } - token, _ := cashu.NewTokenV4(proofsToSend, mintURL, testutils.SAT_UNIT) + token, _ := cashu.NewTokenV4(proofsToSend, mintURL, testutils.SAT_UNIT, false) amountReceived, err := testWallet2.Receive(token, false) if err != nil { @@ -546,7 +546,7 @@ func TestWalletBalanceFees(t *testing.T) { if err != nil { t.Fatalf("unexpected error in send: %v", err) } - token, _ := cashu.NewTokenV4(proofsToSend, mintURL, testutils.SAT_UNIT) + token, _ := cashu.NewTokenV4(proofsToSend, mintURL, testutils.SAT_UNIT, false) // test balance in receiving wallet balanceBeforeReceive := balanceTestWallet2.GetBalance() @@ -566,7 +566,7 @@ func TestWalletBalanceFees(t *testing.T) { if err != nil { t.Fatalf("unexpected error in send: %v", err) } - token, _ := cashu.NewTokenV4(proofsToSend, mintURL, testutils.SAT_UNIT) + token, _ := cashu.NewTokenV4(proofsToSend, mintURL, testutils.SAT_UNIT, false) fees, err := testutils.Fees(proofsToSend, mintURL) if err != nil { @@ -648,7 +648,7 @@ func testWalletRestore( if err != nil { t.Fatalf("unexpected error in send: %v", err) } - token, _ := cashu.NewTokenV4(proofsToSend, mintURL, testutils.SAT_UNIT) + token, _ := cashu.NewTokenV4(proofsToSend, mintURL, testutils.SAT_UNIT, false) _, err = testWallet2.Receive(token, false) if err != nil { @@ -660,7 +660,7 @@ func testWalletRestore( if err != nil { t.Fatalf("unexpected error in send: %v", err) } - token, _ = cashu.NewTokenV4(proofsToSend, mintURL, testutils.SAT_UNIT) + token, _ = cashu.NewTokenV4(proofsToSend, mintURL, testutils.SAT_UNIT, false) _, err = testWallet2.Receive(token, false) if err != nil { @@ -764,7 +764,7 @@ func testP2PK( if err != nil { t.Fatalf("unexpected error generating locked ecash: %v", err) } - lockedEcash, _ := cashu.NewTokenV4(lockedProofs, testWallet.CurrentMint(), testutils.SAT_UNIT) + lockedEcash, _ := cashu.NewTokenV4(lockedProofs, testWallet.CurrentMint(), testutils.SAT_UNIT, false) // try receiving invalid _, err = testWallet.Receive(lockedEcash, true) @@ -792,7 +792,7 @@ func testP2PK( if err != nil { t.Fatalf("unexpected error generating locked ecash: %v", err) } - lockedEcash, _ = cashu.NewTokenV4(lockedProofs, testWallet.CurrentMint(), testutils.SAT_UNIT) + lockedEcash, _ = cashu.NewTokenV4(lockedProofs, testWallet.CurrentMint(), testutils.SAT_UNIT, false) // unlock ecash and trust mint amountReceived, err = testWallet2.Receive(lockedEcash, false) @@ -842,7 +842,7 @@ func TestNutshell(t *testing.T) { if err != nil { t.Fatalf("got unexpected error: %v", err) } - token, _ := cashu.NewTokenV4(proofsToSend, nutshellURL, testutils.SAT_UNIT) + token, _ := cashu.NewTokenV4(proofsToSend, nutshellURL, testutils.SAT_UNIT, false) fees, _ := testutils.Fees(proofsToSend, nutshellURL) if proofsToSend.Amount() != sendAmount+uint64(fees) { From e6430b073f6bc093b379dfde688c70fb67373716 Mon Sep 17 00:00:00 2001 From: elnosh Date: Thu, 19 Sep 2024 11:34:21 -0500 Subject: [PATCH 4/4] wallet - test DLEQ proofs from nutshell mint --- wallet/wallet_integration_test.go | 61 +++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/wallet/wallet_integration_test.go b/wallet/wallet_integration_test.go index 319c2ad..4c47c56 100644 --- a/wallet/wallet_integration_test.go +++ b/wallet/wallet_integration_test.go @@ -14,6 +14,7 @@ import ( btcdocker "github.com/elnosh/btc-docker-test" "github.com/elnosh/gonuts/cashu" + "github.com/elnosh/gonuts/cashu/nuts/nut12" "github.com/elnosh/gonuts/testutils" "github.com/elnosh/gonuts/wallet" "github.com/lightningnetwork/lnd/lnrpc" @@ -896,6 +897,66 @@ func TestSendToPubkeyNutshell(t *testing.T) { testP2PK(t, testWallet, testWallet2, true) } +func TestDLEQProofsNutshell(t *testing.T) { + nutshellMint, err := testutils.CreateNutshellMintContainer(ctx, 0) + if err != nil { + t.Fatalf("error starting nutshell mint: %v", err) + } + defer nutshellMint.Terminate(ctx) + nutshellURL := nutshellMint.Host + + testWalletPath := filepath.Join(".", "/testwalletdleq") + testWallet, err := testutils.CreateTestWallet(testWalletPath, nutshellURL) + if err != nil { + t.Fatal(err) + } + defer func() { + os.RemoveAll(testWalletPath) + }() + + keysets, err := wallet.GetMintActiveKeysets(nutshellURL) + if err != nil { + t.Fatalf("unexpected error getting keysets: %v", err) + } + + mintRes, err := testWallet.RequestMint(10000) + if err != nil { + t.Fatalf("unexpected error requesting mint: %v", err) + } + + proofs, err := testWallet.MintTokens(mintRes.Quote) + if err != nil { + t.Fatalf("unexpected error minting tokens: %v", err) + } + + for _, proof := range proofs { + if proof.DLEQ == nil { + t.Fatal("got nil DLEQ proof from MintTokens") + } + + pubkey := keysets[proof.Id].PublicKeys[proof.Amount] + if !nut12.VerifyProofDLEQ(proof, pubkey) { + t.Fatal("invalid DLEQ proof returned from MintTokens") + } + } + + proofsToSend, err := testWallet.Send(2100, nutshellURL, false) + if err != nil { + t.Fatalf("unexpected error in Send: %v", err) + } + for _, proof := range proofsToSend { + if proof.DLEQ == nil { + t.Fatal("got nil DLEQ proof from Send") + } + + pubkey := keysets[proof.Id].PublicKeys[proof.Amount] + if !nut12.VerifyProofDLEQ(proof, pubkey) { + t.Fatal("invalid DLEQ proof returned from Send") + } + } + +} + func TestWalletRestoreNutshell(t *testing.T) { nutshellMint, err := testutils.CreateNutshellMintContainer(ctx, 0) if err != nil {