Skip to content

Commit

Permalink
mint - support for NUT-14 HTLCs
Browse files Browse the repository at this point in the history
  • Loading branch information
elnosh committed Oct 27, 2024
1 parent c12f3c1 commit 5981683
Show file tree
Hide file tree
Showing 5 changed files with 530 additions and 90 deletions.
33 changes: 26 additions & 7 deletions cashu/nuts/nut11/nut11.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,17 @@ const (
Unknown
)

// errors
// NUT-11 specific errors
var (
InvalidTagErr = cashu.Error{Detail: "invalid tag", Code: NUT11ErrCode}
TooManyTagsErr = cashu.Error{Detail: "too many tags", Code: NUT11ErrCode}
NSigsMustBePositiveErr = cashu.Error{Detail: "n_sigs must be a positive integer", Code: NUT11ErrCode}
EmptyPubkeysErr = cashu.Error{Detail: "pubkeys tag cannot be empty if n_sigs tag is present", Code: NUT11ErrCode}
InvalidWitness = cashu.Error{Detail: "invalid witness", Code: NUT11ErrCode}
InvalidKindErr = cashu.Error{Detail: "invalid kind in secret", Code: NUT11ErrCode}
DuplicateSignaturesErr = cashu.Error{Detail: "witness has duplicate signatures", Code: NUT11ErrCode}
NotEnoughSignaturesErr = cashu.Error{Detail: "not enough valid signatures provided", Code: NUT11ErrCode}
NoSignaturesErr = cashu.Error{Detail: "no signatures provided in witness", Code: NUT11ErrCode}
AllSigAllFlagsErr = cashu.Error{Detail: "all flags must be SIG_ALL", Code: NUT11ErrCode}
SigAllKeysMustBeEqualErr = cashu.Error{Detail: "all public keys must be the same for SIG_ALL", Code: NUT11ErrCode}
SigAllOnlySwap = cashu.Error{Detail: "SIG_ALL can only be used in /swap operation", Code: NUT11ErrCode}
Expand Down Expand Up @@ -231,11 +234,15 @@ func PublicKeys(secret nut10.WellKnownSecret) ([]*btcec.PublicKey, error) {
return nil, err
}

pubkey, err := ParsePublicKey(secret.Data.Data)
if err != nil {
return nil, err
pubkeys := p2pkTags.Pubkeys
// if P2PK proof, add key from data field
if secret.Kind == nut10.P2PK {
pubkey, err := ParsePublicKey(secret.Data.Data)
if err != nil {
return nil, err
}
pubkeys = append(pubkeys, pubkey)
}
pubkeys := append([]*btcec.PublicKey{pubkey}, p2pkTags.Pubkeys...)
return pubkeys, nil
}

Expand Down Expand Up @@ -280,12 +287,24 @@ func CanSign(secret nut10.WellKnownSecret, key *btcec.PrivateKey) bool {
return false
}

func HasValidSignatures(hash []byte, witness P2PKWitness, Nsigs int, pubkeys []*btcec.PublicKey) bool {
func DuplicateSignatures(signatures []string) bool {
sigs := make(map[string]bool)
for _, sig := range signatures {
if sigs[sig] {
return true
} else {
sigs[sig] = true
}
}
return false
}

func HasValidSignatures(hash []byte, signatures []string, Nsigs int, pubkeys []*btcec.PublicKey) bool {
pubkeysCopy := make([]*btcec.PublicKey, len(pubkeys))
copy(pubkeysCopy, pubkeys)

validSignatures := 0
for _, signature := range witness.Signatures {
for _, signature := range signatures {
sig, err := ParseSignature(signature)
if err != nil {
continue
Expand Down
13 changes: 11 additions & 2 deletions cashu/nuts/nut14/nut14.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@ import (
"github.com/elnosh/gonuts/cashu/nuts/nut11"
)

const (
NUT14ErrCode cashu.CashuErrCode = 30004
)

// NUT-14 specific errors
var (
InvalidPreimageErr = cashu.Error{Detail: "Invalid preimage for HTLC", Code: NUT14ErrCode}
InvalidHashErr = cashu.Error{Detail: "Invalid hash in secret", Code: NUT14ErrCode}
)

type HTLCWitness struct {
Preimage string `json:"preimage"`
Signatures []string `json:"signatures"`
Expand All @@ -34,14 +44,13 @@ func AddWitnessHTLC(
}

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")
}

publicKey := signingKey.PubKey().SerializeCompressed()
canSign := false
// read pubkeys and check signingKey can sign
for _, pk := range tags.Pubkeys {
Expand Down
133 changes: 119 additions & 14 deletions mint/mint.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"github.com/elnosh/gonuts/cashu/nuts/nut07"
"github.com/elnosh/gonuts/cashu/nuts/nut10"
"github.com/elnosh/gonuts/cashu/nuts/nut11"
"github.com/elnosh/gonuts/cashu/nuts/nut14"
"github.com/elnosh/gonuts/crypto"
"github.com/elnosh/gonuts/mint/lightning"
"github.com/elnosh/gonuts/mint/storage"
Expand Down Expand Up @@ -492,8 +493,8 @@ func (m *Mint) Swap(proofs cashu.Proofs, blindedMessages cashu.BlindedMessages)

// if sig all, verify signatures in blinded messages
if nut11.ProofsSigAll(proofs) {
m.logDebugf("P2PK locked proofs have SIG_ALL flag. Verifying blinded messages")
if err := verifyP2PKBlindedMessages(proofs, blindedMessages); err != nil {
m.logDebugf("locked proofs have SIG_ALL flag. Verifying blinded messages")
if err := verifyBlindedMessages(proofs, blindedMessages); err != nil {
return nil, err
}
}
Expand Down Expand Up @@ -1059,6 +1060,14 @@ func (m *Mint) verifyProofs(proofs cashu.Proofs, Ys []string) error {
m.logDebugf("verified P2PK locked proof")
}

// verify if HTLC
if err == nil && nut10Secret.Kind == nut10.HTLC {
if err := verifyHTLCProof(proof, nut10Secret); err != nil {
return err
}
m.logDebugf("verified HTLC proof")
}

Cbytes, err := hex.DecodeString(proof.C)
if err != nil {
errmsg := fmt.Sprintf("invalid C: %v", err)
Expand All @@ -1077,13 +1086,72 @@ func (m *Mint) verifyProofs(proofs cashu.Proofs, Ys []string) error {
return nil
}

func verifyP2PKLockedProof(proof cashu.Proof, proofSecret nut10.WellKnownSecret) error {
var p2pkWitness nut11.P2PKWitness
err := json.Unmarshal([]byte(proof.Witness), &p2pkWitness)
func verifyHTLCProof(proof cashu.Proof, proofSecret nut10.WellKnownSecret) error {
var htlcWitness nut14.HTLCWitness
json.Unmarshal([]byte(proof.Witness), &htlcWitness)

p2pkTags, err := nut11.ParseP2PKTags(proofSecret.Data.Tags)
if err != nil {
p2pkWitness.Signatures = []string{}
return err
}

// if locktime is expired and there is no refund pubkey, treat as anyone can spend
// if refund pubkey present, check signature
if p2pkTags.Locktime > 0 && time.Now().Local().Unix() > p2pkTags.Locktime {
if len(p2pkTags.Refund) == 0 {
return nil
} else {
hash := sha256.Sum256([]byte(proof.Secret))
if len(htlcWitness.Signatures) < 1 {
return nut11.InvalidWitness
}
if !nut11.HasValidSignatures(hash[:], htlcWitness.Signatures, 1, p2pkTags.Refund) {
return nut11.NotEnoughSignaturesErr
}
}
return nil
}

// verify valid preimage
preimageBytes, err := hex.DecodeString(htlcWitness.Preimage)
if err != nil {
return nut14.InvalidPreimageErr
}
hashBytes := sha256.Sum256(preimageBytes)
hash := hex.EncodeToString(hashBytes[:])

if len(proofSecret.Data.Data) != 64 {
return nut14.InvalidHashErr
}
if hash != proofSecret.Data.Data {
return nut14.InvalidPreimageErr
}

// if n_sigs flag present, verify signatures
if p2pkTags.NSigs > 0 {
//signaturesRequired := p2pkTags.NSigs
if len(htlcWitness.Signatures) < 1 {
return nut11.NoSignaturesErr
}

hash := sha256.Sum256([]byte(proof.Secret))

if nut11.DuplicateSignatures(htlcWitness.Signatures) {
return nut11.DuplicateSignaturesErr
}

if !nut11.HasValidSignatures(hash[:], htlcWitness.Signatures, p2pkTags.NSigs, p2pkTags.Pubkeys) {
return nut11.NotEnoughSignaturesErr
}
}

return nil
}

func verifyP2PKLockedProof(proof cashu.Proof, proofSecret nut10.WellKnownSecret) error {
var p2pkWitness nut11.P2PKWitness
json.Unmarshal([]byte(proof.Witness), &p2pkWitness)

p2pkTags, err := nut11.ParseP2PKTags(proofSecret.Data.Tags)
if err != nil {
return err
Expand All @@ -1100,7 +1168,7 @@ func verifyP2PKLockedProof(proof cashu.Proof, proofSecret nut10.WellKnownSecret)
if len(p2pkWitness.Signatures) < 1 {
return nut11.InvalidWitness
}
if !nut11.HasValidSignatures(hash[:], p2pkWitness, signaturesRequired, p2pkTags.Refund) {
if !nut11.HasValidSignatures(hash[:], p2pkWitness.Signatures, signaturesRequired, p2pkTags.Refund) {
return nut11.NotEnoughSignaturesErr
}
}
Expand All @@ -1124,18 +1192,27 @@ func verifyP2PKLockedProof(proof cashu.Proof, proofSecret nut10.WellKnownSecret)
if len(p2pkWitness.Signatures) < 1 {
return nut11.InvalidWitness
}
if !nut11.HasValidSignatures(hash[:], p2pkWitness, signaturesRequired, keys) {

if nut11.DuplicateSignatures(p2pkWitness.Signatures) {
return nut11.DuplicateSignaturesErr
}

if !nut11.HasValidSignatures(hash[:], p2pkWitness.Signatures, signaturesRequired, keys) {
return nut11.NotEnoughSignaturesErr
}
}
return nil
}

func verifyP2PKBlindedMessages(proofs cashu.Proofs, blindedMessages cashu.BlindedMessages) error {
// verifyBlindedMessages used to verify blinded messages are signed when SIG_ALL flag
// is present in either a P2PK or HTLC locked proofs
func verifyBlindedMessages(proofs cashu.Proofs, blindedMessages cashu.BlindedMessages) error {
secret, err := nut10.DeserializeSecret(proofs[0].Secret)
if err != nil {
return cashu.BuildCashuError(err.Error(), cashu.StandardErrCode)
}

// pubkeys will hold list of public keys that can sign
pubkeys, err := nut11.PublicKeys(secret)
if err != nil {
return err
Expand Down Expand Up @@ -1194,13 +1271,40 @@ func verifyP2PKBlindedMessages(proofs cashu.Proofs, blindedMessages cashu.Blinde
}
hash := sha256.Sum256(B_bytes)

var witness nut11.P2PKWitness
err = json.Unmarshal([]byte(bm.Witness), &witness)
if err != nil || len(witness.Signatures) < 1 {
return nut11.InvalidWitness
var signatures []string
switch secret.Kind {
case nut10.P2PK:
var witness nut11.P2PKWitness
if err := json.Unmarshal([]byte(bm.Witness), &witness); err != nil {
return nut11.InvalidWitness
}
signatures = witness.Signatures
case nut10.HTLC:
var witness nut14.HTLCWitness
if err := json.Unmarshal([]byte(bm.Witness), &witness); err != nil {
return nut11.InvalidWitness
}

// verify valid preimage
preimageBytes, err := hex.DecodeString(witness.Preimage)
if err != nil {
return nut14.InvalidPreimageErr
}
hashBytes := sha256.Sum256(preimageBytes)
hash := hex.EncodeToString(hashBytes[:])

if len(secret.Data.Data) != 64 {
return nut14.InvalidHashErr
}
if hash != secret.Data.Data {
return nut14.InvalidPreimageErr
}
signatures = witness.Signatures
default:
return nut11.InvalidKindErr
}

if !nut11.HasValidSignatures(hash[:], witness, signaturesRequired, pubkeys) {
if !nut11.HasValidSignatures(hash[:], signatures, signaturesRequired, pubkeys) {
return nut11.NotEnoughSignaturesErr
}
}
Expand Down Expand Up @@ -1325,6 +1429,7 @@ func (m *Mint) SetMintInfo(mintInfo MintInfo) {
10: map[string]bool{"supported": true},
11: map[string]bool{"supported": true},
12: map[string]bool{"supported": true},
14: map[string]bool{"supported": true},
}

info := nut06.MintInfo{
Expand Down
Loading

0 comments on commit 5981683

Please sign in to comment.