mirror of
https://github.com/aljazceru/lspd.git
synced 2026-01-07 16:14:28 +01:00
separate opening fee params logic
This commit is contained in:
@@ -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")
|
||||
}
|
||||
|
||||
3
main.go
3
main.go
@@ -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
166
shared/opening_service.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user