separate opening fee params logic

This commit is contained in:
Jesse de Wit
2023-08-14 13:15:18 +02:00
parent 14c412ec33
commit c61741baa2
3 changed files with 198 additions and 135 deletions

View File

@@ -2,12 +2,10 @@ package main
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"log"
"sort"
"time"
"github.com/breez/lspd/basetypes"
@@ -22,7 +20,6 @@ import (
"google.golang.org/grpc/status"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/lnwire"
@@ -30,14 +27,17 @@ import (
type channelOpenerServer struct {
lspdrpc.ChannelOpenerServer
store interceptor.InterceptStore
store interceptor.InterceptStore
openingService shared.OpeningService
}
func NewChannelOpenerServer(
store interceptor.InterceptStore,
openingService shared.OpeningService,
) *channelOpenerServer {
return &channelOpenerServer{
store: store,
store: store,
openingService: openingService,
}
}
@@ -49,11 +49,23 @@ func (s *channelOpenerServer) ChannelInformation(ctx context.Context, in *lspdrp
return nil, err
}
params, err := s.createOpeningParamsMenu(ctx, node, token)
params, err := s.openingService.GetFeeParamsMenu(token, node.PrivateKey)
if err != nil {
return nil, err
}
var menu []*lspdrpc.OpeningFeeParams
for _, p := range params {
menu = append(menu, &lspdrpc.OpeningFeeParams{
MinMsat: p.MinFeeMsat,
Proportional: p.Proportional,
ValidUntil: p.ValidUntil,
MaxIdleTime: p.MinLifetime,
MaxClientToSelfDelay: p.MaxClientToSelfDelay,
Promise: p.Promise,
})
}
return &lspdrpc.ChannelInformationReply{
Name: node.NodeConfig.Name,
Pubkey: node.NodeConfig.NodePubkey,
@@ -68,136 +80,10 @@ func (s *channelOpenerServer) ChannelInformation(ctx context.Context, in *lspdrp
ChannelMinimumFeeMsat: int64(node.NodeConfig.ChannelMinimumFeeMsat),
LspPubkey: node.PublicKey.SerializeCompressed(), // TODO: Is the publicKey different from the ecies public key?
MaxInactiveDuration: int64(node.NodeConfig.MaxInactiveDuration),
OpeningFeeParamsMenu: params,
OpeningFeeParamsMenu: menu,
}, nil
}
func (s *channelOpenerServer) createOpeningParamsMenu(
ctx context.Context,
node *shared.Node,
token string,
) ([]*lspdrpc.OpeningFeeParams, error) {
var menu []*lspdrpc.OpeningFeeParams
settings, err := s.store.GetFeeParamsSettings(token)
if err != nil {
log.Printf("Failed to fetch fee params settings: %v", err)
return nil, fmt.Errorf("failed to get opening_fee_params")
}
if len(settings) == 0 {
log.Printf("No fee params setings found in the db [token=%v]", token)
}
for _, setting := range settings {
validUntil := time.Now().UTC().Add(setting.Validity)
params := &lspdrpc.OpeningFeeParams{
MinMsat: setting.Params.MinMsat,
Proportional: setting.Params.Proportional,
ValidUntil: validUntil.Format(basetypes.TIME_FORMAT),
MaxIdleTime: setting.Params.MaxIdleTime,
MaxClientToSelfDelay: setting.Params.MaxClientToSelfDelay,
}
promise, err := createPromise(node, params)
if err != nil {
log.Printf("Failed to create promise: %v", err)
return nil, err
}
params.Promise = *promise
menu = append(menu, params)
}
sort.Slice(menu, func(i, j int) bool {
if menu[i].MinMsat == menu[j].MinMsat {
return menu[i].Proportional < menu[j].Proportional
}
return menu[i].MinMsat < menu[j].MinMsat
})
return menu, nil
}
func paramsHash(params *lspdrpc.OpeningFeeParams) ([]byte, error) {
// First hash all the values in the params in a fixed order.
items := []interface{}{
params.MinMsat,
params.Proportional,
params.ValidUntil,
params.MaxIdleTime,
params.MaxClientToSelfDelay,
}
blob, err := json.Marshal(items)
if err != nil {
log.Printf("paramsHash error: %v", err)
return nil, err
}
hash := sha256.Sum256(blob)
return hash[:], nil
}
func createPromise(node *shared.Node, params *lspdrpc.OpeningFeeParams) (*string, error) {
hash, err := paramsHash(params)
if err != nil {
return nil, err
}
// Sign the hash with the private key of the LSP id.
sig, err := ecdsa.SignCompact(node.PrivateKey, hash[:], true)
if err != nil {
log.Printf("createPromise: SignCompact error: %v", err)
return nil, err
}
promise := hex.EncodeToString(sig)
return &promise, nil
}
func verifyPromise(node *shared.Node, params *lspdrpc.OpeningFeeParams) error {
hash, err := paramsHash(params)
if err != nil {
return err
}
sig, err := hex.DecodeString(params.Promise)
if err != nil {
log.Printf("verifyPromise: hex.DecodeString error: %v", err)
return err
}
pub, _, err := ecdsa.RecoverCompact(sig, hash)
if err != nil {
log.Printf("verifyPromise: RecoverCompact(%x) error: %v", sig, err)
return err
}
if !node.PublicKey.IsEqual(pub) {
log.Print("verifyPromise: not signed by us", err)
return fmt.Errorf("invalid promise")
}
return nil
}
func validateOpeningFeeParams(node *shared.Node, params *lspdrpc.OpeningFeeParams) bool {
if params == nil {
return false
}
err := verifyPromise(node, params)
if err != nil {
return false
}
t, err := time.Parse(basetypes.TIME_FORMAT, params.ValidUntil)
if err != nil {
log.Printf("validateOpeningFeeParams: time.Parse(%v, %v) error: %v", basetypes.TIME_FORMAT, params.ValidUntil, err)
return false
}
if time.Now().UTC().After(t) {
log.Printf("validateOpeningFeeParams: promise not valid anymore: %v", t)
return false
}
return true
}
func (s *channelOpenerServer) RegisterPayment(
ctx context.Context,
in *lspdrpc.RegisterPaymentRequest,
@@ -241,7 +127,17 @@ func (s *channelOpenerServer) RegisterPayment(
// TODO: Remove this nil check and the else cluase when we enforce all
// clients to use opening_fee_params.
if pi.OpeningFeeParams != nil {
valid := validateOpeningFeeParams(node, pi.OpeningFeeParams)
valid := s.openingService.ValidateOpeningFeeParams(
&shared.OpeningFeeParams{
MinFeeMsat: pi.OpeningFeeParams.MinMsat,
Proportional: pi.OpeningFeeParams.Proportional,
ValidUntil: pi.OpeningFeeParams.ValidUntil,
MinLifetime: pi.OpeningFeeParams.MaxIdleTime,
MaxClientToSelfDelay: pi.OpeningFeeParams.MaxClientToSelfDelay,
Promise: pi.OpeningFeeParams.Promise,
},
node.PublicKey,
)
if !valid {
return nil, fmt.Errorf("invalid opening_fee_params")
}

View File

@@ -89,6 +89,7 @@ func main() {
notificationsStore := postgresql.NewNotificationsStore(pool)
notificationService := notifications.NewNotificationService(notificationsStore)
openingService := shared.NewOpeningService(interceptStore, nodesService)
var interceptors []interceptor.HtlcInterceptor
nodes := nodesService.GetNodes()
for _, node := range nodes {
@@ -141,7 +142,7 @@ func main() {
address := os.Getenv("LISTEN_ADDRESS")
certMagicDomain := os.Getenv("CERTMAGIC_DOMAIN")
cs := NewChannelOpenerServer(interceptStore)
cs := NewChannelOpenerServer(interceptStore, openingService)
ns := notifications.NewNotificationsServer(notificationsStore)
s, err := NewGrpcServer(nodesService, address, certMagicDomain, cs, ns)
if err != nil {

166
shared/opening_service.go Normal file
View File

@@ -0,0 +1,166 @@
package shared
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"log"
"sort"
"time"
"github.com/breez/lspd/basetypes"
"github.com/breez/lspd/interceptor"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
)
type OpeningService interface {
GetFeeParamsMenu(token string, privateKey *btcec.PrivateKey) ([]*OpeningFeeParams, error)
ValidateOpeningFeeParams(params *OpeningFeeParams, publicKey *btcec.PublicKey) bool
}
type openingService struct {
store interceptor.InterceptStore
nodesService NodesService
}
func NewOpeningService(
store interceptor.InterceptStore,
nodesService NodesService,
) OpeningService {
return &openingService{
store: store,
nodesService: nodesService,
}
}
type OpeningFeeParams struct {
MinFeeMsat uint64 `json:"min_fee_msat,string"`
Proportional uint32 `json:"proportional"`
ValidUntil string `json:"valid_until"`
MinLifetime uint32 `json:"min_lifetime"`
MaxClientToSelfDelay uint32 `json:"max_client_to_self_delay"`
Promise string `json:"promise"`
}
func (s *openingService) GetFeeParamsMenu(token string, privateKey *btcec.PrivateKey) ([]*OpeningFeeParams, error) {
var menu []*OpeningFeeParams
settings, err := s.store.GetFeeParamsSettings(token)
if err != nil {
log.Printf("Failed to fetch fee params settings: %v", err)
return nil, fmt.Errorf("failed to get opening_fee_params")
}
if len(settings) == 0 {
log.Printf("No fee params setings found in the db [token=%v]", token)
}
for _, setting := range settings {
validUntil := time.Now().UTC().Add(setting.Validity)
params := &OpeningFeeParams{
MinFeeMsat: setting.Params.MinMsat,
Proportional: setting.Params.Proportional,
ValidUntil: validUntil.Format(basetypes.TIME_FORMAT),
MinLifetime: setting.Params.MaxIdleTime,
MaxClientToSelfDelay: setting.Params.MaxClientToSelfDelay,
}
promise, err := createPromise(privateKey, params)
if err != nil {
log.Printf("Failed to create promise: %v", err)
return nil, err
}
params.Promise = *promise
menu = append(menu, params)
}
sort.Slice(menu, func(i, j int) bool {
if menu[i].MinFeeMsat == menu[j].MinFeeMsat {
return menu[i].Proportional < menu[j].Proportional
}
return menu[i].MinFeeMsat < menu[j].MinFeeMsat
})
return menu, nil
}
func (s *openingService) ValidateOpeningFeeParams(params *OpeningFeeParams, publicKey *btcec.PublicKey) bool {
if params == nil {
return false
}
err := verifyPromise(publicKey, params)
if err != nil {
return false
}
t, err := time.Parse(basetypes.TIME_FORMAT, params.ValidUntil)
if err != nil {
log.Printf("validateOpeningFeeParams: time.Parse(%v, %v) error: %v", basetypes.TIME_FORMAT, params.ValidUntil, err)
return false
}
if time.Now().UTC().After(t) {
log.Printf("validateOpeningFeeParams: promise not valid anymore: %v", t)
return false
}
return true
}
func createPromise(lspPrivateKey *btcec.PrivateKey, params *OpeningFeeParams) (*string, error) {
hash, err := paramsHash(params)
if err != nil {
return nil, err
}
// Sign the hash with the private key of the LSP id.
sig, err := ecdsa.SignCompact(lspPrivateKey, hash[:], true)
if err != nil {
log.Printf("createPromise: SignCompact error: %v", err)
return nil, err
}
promise := hex.EncodeToString(sig)
return &promise, nil
}
func paramsHash(params *OpeningFeeParams) ([]byte, error) {
// First hash all the values in the params in a fixed order.
items := []interface{}{
params.MinFeeMsat,
params.Proportional,
params.ValidUntil,
params.MinLifetime,
params.MaxClientToSelfDelay,
}
blob, err := json.Marshal(items)
if err != nil {
log.Printf("paramsHash error: %v", err)
return nil, err
}
hash := sha256.Sum256(blob)
return hash[:], nil
}
func verifyPromise(lspPublicKey *btcec.PublicKey, params *OpeningFeeParams) error {
hash, err := paramsHash(params)
if err != nil {
return err
}
sig, err := hex.DecodeString(params.Promise)
if err != nil {
log.Printf("verifyPromise: hex.DecodeString error: %v", err)
return err
}
pub, _, err := ecdsa.RecoverCompact(sig, hash)
if err != nil {
log.Printf("verifyPromise: RecoverCompact(%x) error: %v", sig, err)
return err
}
if !lspPublicKey.IsEqual(pub) {
log.Print("verifyPromise: not signed by us", err)
return fmt.Errorf("invalid promise")
}
return nil
}