Files
lspd/channel_opener_server.go
2023-10-29 15:52:27 +02:00

442 lines
14 KiB
Go

package main
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"log"
"sort"
"time"
"github.com/breez/lspd/basetypes"
"github.com/breez/lspd/btceclegacy"
"github.com/breez/lspd/interceptor"
"github.com/breez/lspd/lightning"
lspdrpc "github.com/breez/lspd/rpc"
ecies "github.com/ecies/go/v2"
"github.com/golang/protobuf/proto"
"google.golang.org/grpc/codes"
"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"
)
type channelOpenerServer struct {
lspdrpc.ChannelOpenerServer
store interceptor.InterceptStore
}
func NewChannelOpenerServer(
store interceptor.InterceptStore,
) *channelOpenerServer {
return &channelOpenerServer{
store: store,
}
}
type contextKey string
func (s *channelOpenerServer) ChannelInformation(ctx context.Context, in *lspdrpc.ChannelInformationRequest) (*lspdrpc.ChannelInformationReply, error) {
node, token, err := s.getNode(ctx)
if err != nil {
return nil, err
}
params, err := s.createOpeningParamsMenu(ctx, node, token)
if err != nil {
return nil, err
}
return &lspdrpc.ChannelInformationReply{
Name: node.nodeConfig.Name,
Pubkey: node.nodeConfig.NodePubkey,
Host: node.nodeConfig.Host,
ChannelCapacity: int64(node.nodeConfig.PublicChannelAmount),
TargetConf: int32(node.nodeConfig.TargetConf),
MinHtlcMsat: int64(node.nodeConfig.MinHtlcMsat),
BaseFeeMsat: int64(node.nodeConfig.BaseFeeMsat),
FeeRate: node.nodeConfig.FeeRate,
TimeLockDelta: node.nodeConfig.TimeLockDelta,
ChannelFeePermyriad: int64(node.nodeConfig.ChannelFeePermyriad),
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,
}, nil
}
func (s *channelOpenerServer) createOpeningParamsMenu(
ctx context.Context,
node *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 *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 *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 *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,
) (*lspdrpc.RegisterPaymentReply, error) {
node, token, err := s.getNode(ctx)
if err != nil {
return nil, err
}
data, err := ecies.Decrypt(node.eciesPrivateKey, in.Blob)
if err != nil {
log.Printf("ecies.Decrypt(%x) error: %v", in.Blob, err)
data, err = btceclegacy.Decrypt(node.privateKey, in.Blob)
if err != nil {
log.Printf("btcec.Decrypt(%x) error: %v", in.Blob, err)
return nil, fmt.Errorf("btcec.Decrypt(%x) error: %w", in.Blob, err)
}
}
var pi lspdrpc.PaymentInformation
err = proto.Unmarshal(data, &pi)
if err != nil {
log.Printf("proto.Unmarshal(%x) error: %v", data, err)
return nil, fmt.Errorf("proto.Unmarshal(%x) error: %w", data, err)
}
log.Printf("RegisterPayment - Destination: %x, pi.PaymentHash: %x, pi.PaymentSecret: %x, pi.IncomingAmountMsat: %v, pi.OutgoingAmountMsat: %v, pi.Tag: %v",
pi.Destination, pi.PaymentHash, pi.PaymentSecret, pi.IncomingAmountMsat, pi.OutgoingAmountMsat, pi.Tag)
if len(pi.Tag) > 1000 {
return nil, fmt.Errorf("tag too long")
}
if len(pi.Tag) != 0 {
var tag json.RawMessage
err = json.Unmarshal([]byte(pi.Tag), &tag)
if err != nil {
return nil, fmt.Errorf("tag is not a valid json object")
}
}
// 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)
if !valid {
return nil, fmt.Errorf("invalid opening_fee_params")
}
} else {
log.Printf("DEPRECATED: RegisterPayment with deprecated fee mechanism.")
pi.OpeningFeeParams = &lspdrpc.OpeningFeeParams{
MinMsat: uint64(node.nodeConfig.ChannelMinimumFeeMsat),
Proportional: uint32(node.nodeConfig.ChannelFeePermyriad * 100),
ValidUntil: time.Now().UTC().Add(time.Duration(time.Hour * 24)).Format(basetypes.TIME_FORMAT),
MaxIdleTime: uint32(node.nodeConfig.MaxInactiveDuration / 600),
MaxClientToSelfDelay: uint32(10000),
}
}
err = checkPayment(pi.OpeningFeeParams, pi.IncomingAmountMsat, pi.OutgoingAmountMsat)
if err != nil {
log.Printf("checkPayment(%v, %v) error: %v", pi.IncomingAmountMsat, pi.OutgoingAmountMsat, err)
return nil, fmt.Errorf("checkPayment(%v, %v) error: %v", pi.IncomingAmountMsat, pi.OutgoingAmountMsat, err)
}
params := &interceptor.OpeningFeeParams{
MinMsat: pi.OpeningFeeParams.MinMsat,
Proportional: pi.OpeningFeeParams.Proportional,
ValidUntil: pi.OpeningFeeParams.ValidUntil,
MaxIdleTime: pi.OpeningFeeParams.MaxIdleTime,
MaxClientToSelfDelay: pi.OpeningFeeParams.MaxClientToSelfDelay,
Promise: pi.OpeningFeeParams.Promise,
}
err = s.store.RegisterPayment(token, params, pi.Destination, pi.PaymentHash, pi.PaymentSecret, pi.IncomingAmountMsat, pi.OutgoingAmountMsat, pi.Tag)
if err != nil {
log.Printf("RegisterPayment() error: %v", err)
return nil, fmt.Errorf("RegisterPayment() error: %w", err)
}
return &lspdrpc.RegisterPaymentReply{}, nil
}
func (s *channelOpenerServer) OpenChannel(ctx context.Context, in *lspdrpc.OpenChannelRequest) (*lspdrpc.OpenChannelReply, error) {
node, _, err := s.getNode(ctx)
if err != nil {
return nil, err
}
r, err, _ := node.openChannelReqGroup.Do(in.Pubkey, func() (interface{}, error) {
pubkey, err := hex.DecodeString(in.Pubkey)
if err != nil {
return nil, err
}
channelCount, err := node.client.GetNodeChannelCount(pubkey)
if err != nil {
return nil, err
}
var outPoint *wire.OutPoint
if channelCount == 0 {
outPoint, err = node.client.OpenChannel(&lightning.OpenChannelRequest{
CapacitySat: node.nodeConfig.ChannelAmount,
Destination: pubkey,
TargetConf: &node.nodeConfig.TargetConf,
MinHtlcMsat: node.nodeConfig.MinHtlcMsat,
IsPrivate: node.nodeConfig.ChannelPrivate,
})
if err != nil {
log.Printf("Error in OpenChannel: %v", err)
return nil, err
}
log.Printf("Response from OpenChannel: (TX: %v)", outPoint.String())
}
return &lspdrpc.OpenChannelReply{TxHash: outPoint.Hash.String(), OutputIndex: outPoint.Index}, nil
})
if err != nil {
return nil, err
}
return r.(*lspdrpc.OpenChannelReply), err
}
func (n *node) getSignedEncryptedData(in *lspdrpc.Encrypted) (string, []byte, bool, error) {
usedEcies := true
signedBlob, err := ecies.Decrypt(n.eciesPrivateKey, in.Data)
if err != nil {
log.Printf("ecies.Decrypt(%x) error: %v", in.Data, err)
usedEcies = false
signedBlob, err = btceclegacy.Decrypt(n.privateKey, in.Data)
if err != nil {
log.Printf("btcec.Decrypt(%x) error: %v", in.Data, err)
return "", nil, usedEcies, fmt.Errorf("btcec.Decrypt(%x) error: %w", in.Data, err)
}
}
var signed lspdrpc.Signed
err = proto.Unmarshal(signedBlob, &signed)
if err != nil {
log.Printf("proto.Unmarshal(%x) error: %v", signedBlob, err)
return "", nil, usedEcies, fmt.Errorf("proto.Unmarshal(%x) error: %w", signedBlob, err)
}
pubkey, err := btcec.ParsePubKey(signed.Pubkey)
if err != nil {
log.Printf("unable to parse pubkey: %v", err)
return "", nil, usedEcies, fmt.Errorf("unable to parse pubkey: %w", err)
}
wireSig, err := lnwire.NewSigFromRawSignature(signed.Signature)
if err != nil {
return "", nil, usedEcies, fmt.Errorf("failed to decode signature: %v", err)
}
sig, err := wireSig.ToSignature()
if err != nil {
return "", nil, usedEcies, fmt.Errorf("failed to convert from wire format: %v",
err)
}
// The signature is over the sha256 hash of the message.
digest := chainhash.HashB(signed.Data)
if !sig.Verify(digest, pubkey) {
return "", nil, usedEcies, fmt.Errorf("invalid signature")
}
return hex.EncodeToString(signed.Pubkey), signed.Data, usedEcies, nil
}
func (s *channelOpenerServer) CheckChannels(ctx context.Context, in *lspdrpc.Encrypted) (*lspdrpc.Encrypted, error) {
node, _, err := s.getNode(ctx)
if err != nil {
return nil, err
}
nodeID, data, usedEcies, err := node.getSignedEncryptedData(in)
if err != nil {
log.Printf("getSignedEncryptedData error: %v", err)
return nil, fmt.Errorf("getSignedEncryptedData error: %v", err)
}
var checkChannelsRequest lspdrpc.CheckChannelsRequest
err = proto.Unmarshal(data, &checkChannelsRequest)
if err != nil {
log.Printf("proto.Unmarshal(%x) error: %v", data, err)
return nil, fmt.Errorf("proto.Unmarshal(%x) error: %w", data, err)
}
closedChannels, err := node.client.GetClosedChannels(nodeID, checkChannelsRequest.WaitingCloseChannels)
if err != nil {
log.Printf("GetClosedChannels(%v) error: %v", checkChannelsRequest.FakeChannels, err)
return nil, fmt.Errorf("GetClosedChannels(%v) error: %w", checkChannelsRequest.FakeChannels, err)
}
checkChannelsReply := lspdrpc.CheckChannelsReply{
NotFakeChannels: make(map[string]uint64),
ClosedChannels: closedChannels,
}
dataReply, err := proto.Marshal(&checkChannelsReply)
if err != nil {
log.Printf("proto.Marshall() error: %v", err)
return nil, fmt.Errorf("proto.Marshal() error: %w", err)
}
pubkey, err := btcec.ParsePubKey(checkChannelsRequest.EncryptPubkey)
if err != nil {
log.Printf("unable to parse pubkey: %v", err)
return nil, fmt.Errorf("unable to parse pubkey: %w", err)
}
var encrypted []byte
if usedEcies {
encrypted, err = ecies.Encrypt(node.eciesPublicKey, dataReply)
if err != nil {
log.Printf("ecies.Encrypt() error: %v", err)
return nil, fmt.Errorf("ecies.Encrypt() error: %w", err)
}
} else {
encrypted, err = btceclegacy.Encrypt(pubkey, dataReply)
if err != nil {
log.Printf("btcec.Encrypt() error: %v", err)
return nil, fmt.Errorf("btcec.Encrypt() error: %w", err)
}
}
return &lspdrpc.Encrypted{Data: encrypted}, nil
}
func (s *channelOpenerServer) getNode(ctx context.Context) (*node, string, error) {
nd := ctx.Value(contextKey("node"))
if nd == nil {
return nil, "", status.Errorf(codes.PermissionDenied, "Not authorized")
}
nodeContext, ok := nd.(*nodeContext)
if !ok {
return nil, "", status.Errorf(codes.PermissionDenied, "Not authorized")
}
return nodeContext.node, nodeContext.token, nil
}
func checkPayment(params *lspdrpc.OpeningFeeParams, incomingAmountMsat, outgoingAmountMsat int64) error {
fees := incomingAmountMsat * int64(params.Proportional) / 1_000_000 / 1_000 * 1_000
if fees < int64(params.MinMsat) {
fees = int64(params.MinMsat)
}
if incomingAmountMsat-outgoingAmountMsat < fees {
return fmt.Errorf("not enough fees")
}
return nil
}