Skip to content

Commit

Permalink
wallet - receive locked ecash
Browse files Browse the repository at this point in the history
  • Loading branch information
elnosh committed Jul 8, 2024
1 parent 63e2374 commit 9f2ce6a
Show file tree
Hide file tree
Showing 6 changed files with 364 additions and 89 deletions.
74 changes: 55 additions & 19 deletions cashu/cashu.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ package cashu

import (
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"

"github.com/decred/dcrd/dcrec/secp256k1/v4"
)

type SecretKind int
Expand All @@ -18,15 +21,35 @@ const (

// Cashu BlindedMessage. See https://github.com/cashubtc/nuts/blob/main/00.md#blindedmessage
type BlindedMessage struct {
Amount uint64 `json:"amount"`
B_ string `json:"B_"`
Id string `json:"id"`

// including Witness field for now to avoid throwing error when parsing json
// from clients that include this field even when mint does not support it.
Amount uint64 `json:"amount"`
B_ string `json:"B_"`
Id string `json:"id"`
Witness string `json:"witness,omitempty"`
}

func NewBlindedMessage(id string, amount uint64, B_ *secp256k1.PublicKey) BlindedMessage {
B_str := hex.EncodeToString(B_.SerializeCompressed())
return BlindedMessage{Amount: amount, B_: B_str, Id: id}
}

func SortBlindedMessages(blindedMessages BlindedMessages, secrets []string, rs []*secp256k1.PrivateKey) {
// sort messages, secrets and rs
for i := 0; i < len(blindedMessages)-1; i++ {
for j := i + 1; j < len(blindedMessages); j++ {
if blindedMessages[i].Amount > blindedMessages[j].Amount {
// Swap blinded messages
blindedMessages[i], blindedMessages[j] = blindedMessages[j], blindedMessages[i]

// Swap secrets
secrets[i], secrets[j] = secrets[j], secrets[i]

// Swap rs
rs[i], rs[j] = rs[j], rs[i]
}
}
}
}

type BlindedMessages []BlindedMessage

func (bm BlindedMessages) Amount() uint64 {
Expand All @@ -48,34 +71,47 @@ type BlindedSignatures []BlindedSignature

// Cashu Proof. See https://github.com/cashubtc/nuts/blob/main/00.md#proof
type Proof struct {
Amount uint64 `json:"amount"`
Id string `json:"id"`
Secret string `json:"secret"`
C string `json:"C"`

// including Witness field for now to avoid throwing error when parsing json
// from clients that include this field even when mint does not support it.
Amount uint64 `json:"amount"`
Id string `json:"id"`
Secret string `json:"secret"`
C string `json:"C"`
Witness string `json:"witness,omitempty"`
}

// TODO
func (p Proof) IsSecretP2PK() bool {
return false
return p.SecretType() == P2PK
}

// TODO
func (p Proof) SecretType() SecretKind {
var rawJsonSecret []json.RawMessage
// if not valid json, assume it is random secret
if err := json.Unmarshal([]byte(p.Secret), &rawJsonSecret); err != nil {
return Random
}

// Well-known secret should have a length of at least 2
if len(rawJsonSecret) < 2 {
return Random
}

var kind string
if err := json.Unmarshal(rawJsonSecret[0], &kind); err != nil {
return Random
}

if kind == "P2PK" {
return P2PK
}

return Random
}

func (kind SecretKind) String() string {
switch kind {
case Random:
return "random"
case P2PK:
return "P2PK"
default:
return "unknown"
return "random"
}
}

Expand Down
28 changes: 27 additions & 1 deletion cashu/nuts/nut10/nut10.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package nut10

import (
"encoding/json"
"errors"
"fmt"

"github.com/elnosh/gonuts/cashu"
Expand All @@ -22,6 +23,31 @@ func SerializeSecret(kind cashu.SecretKind, secretData WellKnownSecret) (string,

secretKind := kind.String()
secret := fmt.Sprintf("[\"%s\", %v]", secretKind, string(jsonSecret))

return secret, nil
}

// DeserializeSecret returns Well-known secret struct.
// It returns error if it's not valid according to NUT-10
func DeserializeSecret(secret string) (WellKnownSecret, error) {
var rawJsonSecret []json.RawMessage
if err := json.Unmarshal([]byte(secret), &rawJsonSecret); err != nil {
return WellKnownSecret{}, err
}

// Well-known secret should have a length of at least 2
if len(rawJsonSecret) < 2 {
return WellKnownSecret{}, errors.New("invalid secret: length < 2")
}

var kind string
if err := json.Unmarshal(rawJsonSecret[0], &kind); err != nil {
return WellKnownSecret{}, errors.New("invalid kind for secret")
}

var secretData WellKnownSecret
if err := json.Unmarshal(rawJsonSecret[1], &secretData); err != nil {
return WellKnownSecret{}, fmt.Errorf("invalid secret: %v", err)
}

return secretData, nil
}
92 changes: 92 additions & 0 deletions cashu/nuts/nut11/nut11.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,14 @@ package nut11

import (
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"reflect"

"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/elnosh/gonuts/cashu"
"github.com/elnosh/gonuts/cashu/nuts/nut10"
)
Expand Down Expand Up @@ -35,3 +41,89 @@ func P2PKSecret(pubkey string) (string, error) {

return secret, nil
}

func AddSignatureToInputs(inputs cashu.Proofs, signingKey *btcec.PrivateKey) (cashu.Proofs, error) {
for i, proof := range inputs {
hash := sha256.Sum256([]byte(proof.Secret))
signature, err := schnorr.Sign(signingKey, hash[:])
if err != nil {
return nil, err
}
signatureBytes := signature.Serialize()

p2pkWitness := P2PKWitness{
Signatures: []string{hex.EncodeToString(signatureBytes)},
}

witness, err := json.Marshal(p2pkWitness)
if err != nil {
return nil, err
}
proof.Witness = string(witness)
inputs[i] = proof
}

return inputs, nil
}

func AddSignatureToOutputs(
outputs cashu.BlindedMessages,
signingKey *btcec.PrivateKey,
) (cashu.BlindedMessages, error) {
for i, output := range outputs {
msgToSign, err := hex.DecodeString(output.B_)
if err != nil {
return nil, err
}

hash := sha256.Sum256(msgToSign)
signature, err := schnorr.Sign(signingKey, hash[:])
if err != nil {
return nil, err
}
signatureBytes := signature.Serialize()

p2pkWitness := P2PKWitness{
Signatures: []string{hex.EncodeToString(signatureBytes)},
}

witness, err := json.Marshal(p2pkWitness)
if err != nil {
return nil, err
}
output.Witness = string(witness)
outputs[i] = output
}

return outputs, nil
}

func IsSigAll(secret nut10.WellKnownSecret) bool {
for _, tag := range secret.Tags {
if len(tag) == 2 {
if tag[0] == "sigflag" && tag[1] == "SIG_ALL" {
return true
}
}
}

return false
}

func CanSign(secret nut10.WellKnownSecret, key *btcec.PrivateKey) bool {
secretData, err := hex.DecodeString(secret.Data)
if err != nil {
return false
}

publicKey, err := secp256k1.ParsePubKey(secretData)
if err != nil {
return false
}

if reflect.DeepEqual(publicKey.SerializeCompressed(), key.PubKey().SerializeCompressed()) {
return true
}

return false
}
17 changes: 17 additions & 0 deletions cmd/nutw/nutw.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ func main() {
sendCmd,
receiveCmd,
payCmd,
p2pkLockCmd,
mnemonicCmd,
restoreCmd,
},
Expand Down Expand Up @@ -345,6 +346,22 @@ func pay(ctx *cli.Context) error {
return nil
}

var p2pkLockCmd = &cli.Command{
Name: "p2pk-lock",
Before: setupWallet,
Action: p2pkLock,
}

func p2pkLock(ctx *cli.Context) error {
lockpubkey := nutw.GetReceivePubkey()
pubkey := hex.EncodeToString(lockpubkey.SerializeCompressed())

fmt.Printf("Pay to Public Key (P2PK) lock: %v\n\n", pubkey)
fmt.Println("You can unlock ecash locked to this public key")

return nil
}

var mnemonicCmd = &cli.Command{
Name: "mnemonic",
Before: setupWallet,
Expand Down
Loading

0 comments on commit 9f2ce6a

Please sign in to comment.