Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wallet: NUT-12 DLEQ proofs support #60

Merged
merged 4 commits into from
Sep 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 68 additions & 6 deletions cashu/cashu.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -123,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}
}
Expand Down Expand Up @@ -198,13 +216,20 @@ 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"`
}

func NewTokenV4(proofs Proofs, mint string, unit string) (TokenV4, error) {
type DLEQV4 struct {
E []byte `json:"e"`
S []byte `json:"s"`
R []byte `json:"r"`
}

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)
Expand All @@ -217,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)
}

Expand Down Expand Up @@ -271,6 +325,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)
}
}
Expand Down
136 changes: 136 additions & 0 deletions cashu/nuts/nut12/nut12.go
Original file line number Diff line number Diff line change
@@ -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
}
47 changes: 47 additions & 0 deletions cashu/nuts/nut12/nut12_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
}
23 changes: 17 additions & 6 deletions cmd/nutw/nutw.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -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,
}
Expand Down Expand Up @@ -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))
}
Expand Down Expand Up @@ -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,
}
Expand Down
Loading
Loading