Skip to content

Commit

Permalink
receive HTLCs in wallet
Browse files Browse the repository at this point in the history
  • Loading branch information
elnosh committed Oct 27, 2024
1 parent d4a68df commit c12f3c1
Show file tree
Hide file tree
Showing 3 changed files with 250 additions and 28 deletions.
78 changes: 71 additions & 7 deletions cashu/nuts/nut14/nut14.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,42 +4,106 @@ 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 {
Preimage string `json:"preimage"`
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
}
135 changes: 114 additions & 21 deletions cmd/nutw/nutw.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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{
Expand All @@ -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",
Expand All @@ -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)
}
Expand All @@ -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)
Expand Down
65 changes: 65 additions & 0 deletions wallet/wallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit c12f3c1

Please sign in to comment.