From c12f3c1a9ab4c322ae6084c7402f7116f70c60f1 Mon Sep 17 00:00:00 2001 From: elnosh Date: Fri, 25 Oct 2024 11:40:17 -0500 Subject: [PATCH] receive HTLCs in wallet --- cashu/nuts/nut14/nut14.go | 78 ++++++++++++++++++++-- cmd/nutw/nutw.go | 135 ++++++++++++++++++++++++++++++++------ wallet/wallet.go | 65 ++++++++++++++++++ 3 files changed, 250 insertions(+), 28 deletions(-) diff --git a/cashu/nuts/nut14/nut14.go b/cashu/nuts/nut14/nut14.go index 641b787..5957f54 100644 --- a/cashu/nuts/nut14/nut14.go +++ b/cashu/nuts/nut14/nut14.go @@ -4,10 +4,14 @@ import ( "crypto/sha256" "encoding/hex" "encoding/json" + "errors" + "slices" "github.com/btcsuite/btcd/btcec/v2" - "github.com/decred/dcrd/dcrec/secp256k1/v4/schnorr" + "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/elnosh/gonuts/cashu" + "github.com/elnosh/gonuts/cashu/nuts/nut10" + "github.com/elnosh/gonuts/cashu/nuts/nut11" ) type HTLCWitness struct { @@ -15,31 +19,91 @@ type HTLCWitness struct { Signatures []string `json:"signatures"` } +// AddWitnessHTLC will add the preimage to the HTLCWitness. +// It will also read the tags in the secret and add the signatures +// if needed. func AddWitnessHTLC( proofs cashu.Proofs, + secret nut10.WellKnownSecret, preimage string, signingKey *btcec.PrivateKey, ) (cashu.Proofs, error) { + tags, err := nut11.ParseP2PKTags(secret.Data.Tags) + if err != nil { + return nil, err + } + + signatureNeeded := false + publicKey := signingKey.PubKey().SerializeCompressed() + + if tags.NSigs > 0 { + // return error if it requires more than 1 signature + if tags.NSigs > 1 { + return nil, errors.New("unable to provide enough signatures") + } + + canSign := false + // read pubkeys and check signingKey can sign + for _, pk := range tags.Pubkeys { + if slices.Equal(pk.SerializeCompressed(), publicKey) { + canSign = true + break + } + } + if !canSign { + return nil, errors.New("signing key is not part of public keys list that can provide signatures") + } + + // if it gets to here, signature is needed in the witness + signatureNeeded = true + } + for i, proof := range proofs { - hash := sha256.Sum256([]byte(proof.Secret)) + htlcWitness := HTLCWitness{Preimage: preimage} + if signatureNeeded { + hash := sha256.Sum256([]byte(proof.Secret)) + signature, err := schnorr.Sign(signingKey, hash[:]) + if err != nil { + return nil, err + } + htlcWitness.Signatures = []string{hex.EncodeToString(signature.Serialize())} + } + + witness, err := json.Marshal(htlcWitness) + if err != nil { + return nil, err + } + proof.Witness = string(witness) + proofs[i] = proof + } + + return proofs, nil +} + +func AddWitnessHTLCToOutputs( + outputs cashu.BlindedMessages, + preimage string, + signingKey *btcec.PrivateKey, +) (cashu.BlindedMessages, error) { + for i, output := range outputs { + hash := sha256.Sum256([]byte(output.B_)) signature, err := schnorr.Sign(signingKey, hash[:]) if err != nil { return nil, err } - signatureBytes := signature.Serialize() htlcWitness := HTLCWitness{ Preimage: preimage, - Signatures: []string{hex.EncodeToString(signatureBytes)}, + Signatures: []string{hex.EncodeToString(signature.Serialize())}, } witness, err := json.Marshal(htlcWitness) if err != nil { return nil, err } - proof.Witness = string(witness) - proofs[i] = proof + output.Witness = string(witness) + outputs[i] = output } - return proofs, nil + return outputs, nil } diff --git a/cmd/nutw/nutw.go b/cmd/nutw/nutw.go index 3f687f6..0e57a62 100644 --- a/cmd/nutw/nutw.go +++ b/cmd/nutw/nutw.go @@ -15,9 +15,10 @@ import ( "strconv" "strings" - "github.com/btcsuite/btcd/btcec/v2" + "github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/elnosh/gonuts/cashu" "github.com/elnosh/gonuts/cashu/nuts/nut05" + "github.com/elnosh/gonuts/cashu/nuts/nut11" "github.com/elnosh/gonuts/wallet" "github.com/joho/godotenv" decodepay "github.com/nbd-wtf/ln-decodepay" @@ -153,12 +154,22 @@ func getBalance(ctx *cli.Context) error { return nil } +const ( + preimageFlag = "preimage" +) + var receiveCmd = &cli.Command{ Name: "receive", Usage: "Receive token", ArgsUsage: "[TOKEN]", Before: setupWallet, Action: receive, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: preimageFlag, + Usage: "preimage if receiving ecash HTLC", + }, + }, } func receive(ctx *cli.Context) error { @@ -172,10 +183,20 @@ func receive(ctx *cli.Context) error { if err != nil { printErr(err) } + mintURL := token.Mint() + + if ctx.IsSet(preimageFlag) { + preimage := ctx.String(preimageFlag) + receivedAmount, err := nutw.ReceiveHTLC(token, preimage) + if err != nil { + printErr(err) + } + fmt.Printf("%v sats received from ecash HTLC\n", receivedAmount) + return nil + } swap := true trustedMints := nutw.TrustedMints() - mintURL := token.Mint() isTrusted := slices.Contains(trustedMints, mintURL) if !isTrusted { @@ -282,10 +303,15 @@ func mintTokens(paymentRequest string) error { } const ( - lockFlag = "lock" - noFeesFlag = "no-fees" - legacyFlag = "legacy" - includeDLEQFlag = "include-dleq" + p2pklockFlag = "lock-p2pk" + htlcLockFlag = "lock-htlc" + requiredSigsFlag = "required-signatures" + pubkeysFlag = "pubkeys" + locktimeFlag = "locktime" + refundKeysFlag = "refund-keys" + noFeesFlag = "no-fees" + legacyFlag = "legacy" + includeDLEQFlag = "include-dleq" ) var sendCmd = &cli.Command{ @@ -295,9 +321,37 @@ var sendCmd = &cli.Command{ Before: setupWallet, Flags: []cli.Flag{ &cli.StringFlag{ - Name: lockFlag, + Name: p2pklockFlag, Usage: "generate ecash locked to a public key", }, + &cli.StringFlag{ + Name: htlcLockFlag, + Usage: "generate ecash locked to hash of preimage", + }, + + // --------------- Optional lock flags category ---------------------- + &cli.IntFlag{ + Name: requiredSigsFlag, + Usage: "number of required signatures", + Category: "Optional lock flags for P2PK or HTLC", + }, + &cli.StringSliceFlag{ + Name: pubkeysFlag, + Usage: "additional public keys that can provide signatures.", + Category: "Optional lock flags for P2PK or HTLC", + }, + &cli.Int64Flag{ + Name: locktimeFlag, + Usage: "Unix timestamp for P2PK or HTLC to expire", + Category: "Optional lock flags for P2PK or HTLC", + }, + &cli.StringSliceFlag{ + Name: refundKeysFlag, + Usage: "list of public keys that can sign after locktime", + Category: "Optional lock flags for P2PK or HTLC", + }, + // --------------- Optional lock flags category ---------------------- + &cli.BoolFlag{ Name: noFeesFlag, Usage: "do not include fees for receiver in the token generated", @@ -322,8 +376,8 @@ func send(ctx *cli.Context) error { if args.Len() < 1 { printErr(errors.New("specify an amount to send")) } - amountStr := args.First() - sendAmount, err := strconv.ParseUint(amountStr, 10, 64) + amountArg := args.First() + sendAmount, err := strconv.ParseUint(amountArg, 10, 64) if err != nil { printErr(err) } @@ -336,21 +390,60 @@ func send(ctx *cli.Context) error { } var proofsToSend cashu.Proofs - // if lock flag is set, get ecash locked to the pubkey - if ctx.IsSet(lockFlag) { - lockpubkey := ctx.String(lockFlag) - lockbytes, err := hex.DecodeString(lockpubkey) - if err != nil { - printErr(err) + + // if either P2PK or HTLC, read optional flags + if ctx.IsSet(p2pklockFlag) || ctx.IsSet(htlcLockFlag) { + tags := nut11.P2PKTags{ + NSigs: ctx.Int(requiredSigsFlag), + Locktime: ctx.Int64(locktimeFlag), } - pubkey, err := btcec.ParsePubKey(lockbytes) - if err != nil { - printErr(err) + + for _, pubkey := range ctx.StringSlice(pubkeysFlag) { + pubkeyBytes, err := hex.DecodeString(pubkey) + if err != nil { + printErr(err) + } + + publicKey, err := secp256k1.ParsePubKey(pubkeyBytes) + if err != nil { + printErr(err) + } + tags.Pubkeys = append(tags.Pubkeys, publicKey) } - proofsToSend, err = nutw.SendToPubkey(sendAmount, selectedMint, pubkey, nil, includeFees) - if err != nil { - printErr(err) + for _, pubkey := range ctx.StringSlice(refundKeysFlag) { + pubkeyBytes, err := hex.DecodeString(pubkey) + if err != nil { + printErr(err) + } + + publicKey, err := secp256k1.ParsePubKey(pubkeyBytes) + if err != nil { + printErr(err) + } + tags.Refund = append(tags.Refund, publicKey) + } + + if ctx.IsSet(p2pklockFlag) { + lockpubkey := ctx.String(p2pklockFlag) + lockbytes, err := hex.DecodeString(lockpubkey) + if err != nil { + printErr(err) + } + pubkey, err := secp256k1.ParsePubKey(lockbytes) + if err != nil { + printErr(err) + } + proofsToSend, err = nutw.SendToPubkey(sendAmount, selectedMint, pubkey, &tags, includeFees) + if err != nil { + printErr(err) + } + } else { + preimage := ctx.String(htlcLockFlag) + proofsToSend, err = nutw.HTLCLockedProofs(sendAmount, selectedMint, preimage, &tags, includeFees) + if err != nil { + printErr(err) + } } } else { proofsToSend, err = nutw.Send(sendAmount, selectedMint, includeFees) diff --git a/wallet/wallet.go b/wallet/wallet.go index 8d5342c..18b402d 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -28,6 +28,7 @@ import ( "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/cashu/nuts/nut14" "github.com/elnosh/gonuts/crypto" "github.com/elnosh/gonuts/wallet/storage" "github.com/tyler-smith/go-bip39" @@ -614,6 +615,70 @@ func (w *Wallet) Receive(token cashu.Token, swapToTrusted bool) (uint64, error) } } +// ReceiveHTLC will add the preimage and any signatures if needed in order to redeem the +// locked ecash. If successful, it will make a swap and store the new proofs. +// It will add the mint in the token to the list of trusted mints. +func (w *Wallet) ReceiveHTLC(token cashu.Token, preimage string) (uint64, error) { + proofs := token.Proofs() + tokenMint := token.Mint() + + keyset, err := w.getActiveSatKeyset(tokenMint) + if err != nil { + return 0, fmt.Errorf("could not get active keyset: %v", err) + } + // verify DLEQ in proofs if present + if !nut12.VerifyProofsDLEQ(proofs, *keyset) { + return 0, errors.New("invalid DLEQ proof") + } + + nut10Secret, err := nut10.DeserializeSecret(proofs[0].Secret) + if err == nil && nut10Secret.Kind == nut10.HTLC { + proofs, err = nut14.AddWitnessHTLC(proofs, nut10Secret, preimage, w.privateKey) + if err != nil { + return 0, fmt.Errorf("could not add HTLC witness: %v", err) + } + + // only add mint if not previously trusted + _, ok := w.mints[tokenMint] + if !ok { + _, err := w.addMint(tokenMint) + if err != nil { + return 0, err + } + } + + req, err := w.createSwapRequest(proofs, tokenMint) + if err != nil { + return 0, fmt.Errorf("could not create swap request: %v", err) + } + + //if `SIG_ALL` flag, sign outputs + if nut11.IsSigAll(nut10Secret) { + req.outputs, err = nut14.AddWitnessHTLCToOutputs(req.outputs, preimage, w.privateKey) + if err != nil { + return 0, fmt.Errorf("could not add HTLC witness to outputs: %v", err) + } + } + + newProofs, err := w.swap(tokenMint, req) + if err != nil { + return 0, fmt.Errorf("could not swap proofs: %v", err) + } + + err = w.db.IncrementKeysetCounter(req.keyset.Id, uint32(len(req.outputs))) + if err != nil { + return 0, fmt.Errorf("error incrementing keyset counter: %v", err) + } + + if err := w.db.SaveProofs(newProofs); err != nil { + return 0, fmt.Errorf("error storing proofs: %v", err) + } + return newProofs.Amount(), nil + } + + return 0, errors.New("ecash does not have an HTLC spending condition") +} + type swapRequestPayload struct { inputs cashu.Proofs outputs cashu.BlindedMessages