github.com/ethersphere/bee/v2@v2.2.0/pkg/settlement/swap/swapprotocol/swapprotocol.go (about)

     1  // Copyright 2020 The Swarm Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package swapprotocol
     6  
     7  import (
     8  	"context"
     9  	"encoding/json"
    10  	"errors"
    11  	"fmt"
    12  	"math/big"
    13  	"time"
    14  
    15  	"github.com/ethereum/go-ethereum/common"
    16  	"github.com/ethersphere/bee/v2/pkg/log"
    17  	"github.com/ethersphere/bee/v2/pkg/p2p"
    18  	"github.com/ethersphere/bee/v2/pkg/p2p/protobuf"
    19  	"github.com/ethersphere/bee/v2/pkg/settlement/swap/chequebook"
    20  	swap "github.com/ethersphere/bee/v2/pkg/settlement/swap/headers"
    21  	"github.com/ethersphere/bee/v2/pkg/settlement/swap/priceoracle"
    22  	"github.com/ethersphere/bee/v2/pkg/settlement/swap/swapprotocol/pb"
    23  	"github.com/ethersphere/bee/v2/pkg/swarm"
    24  )
    25  
    26  // loggerName is the tree path name of the logger for this package.
    27  const loggerName = "swapprotocol"
    28  
    29  const (
    30  	protocolName    = "swap"
    31  	protocolVersion = "1.0.0"
    32  	streamName      = "swap" // stream for cheques
    33  )
    34  
    35  var (
    36  	ErrNegotiateRate      = errors.New("exchange rates mismatch")
    37  	ErrNegotiateDeduction = errors.New("deduction values mismatch")
    38  	ErrHaveDeduction      = errors.New("received deduction not zero")
    39  )
    40  
    41  type SendChequeFunc chequebook.SendChequeFunc
    42  
    43  type IssueFunc func(ctx context.Context, beneficiary common.Address, amount *big.Int, sendChequeFunc chequebook.SendChequeFunc) (*big.Int, error)
    44  
    45  // (context.Context, common.Address, *big.Int, chequebook.SendChequeFunc) (*big.Int, error)
    46  
    47  // Interface is the main interface to send messages over swap protocol.
    48  type Interface interface {
    49  	// EmitCheque sends a signed cheque to a peer.
    50  	EmitCheque(ctx context.Context, peer swarm.Address, beneficiary common.Address, amount *big.Int, issue IssueFunc) (balance *big.Int, err error)
    51  }
    52  
    53  // Swap is the interface the settlement layer should implement to receive cheques.
    54  type Swap interface {
    55  	// ReceiveCheque is called by the swap protocol if a cheque is received.
    56  	ReceiveCheque(ctx context.Context, peer swarm.Address, cheque *chequebook.SignedCheque, exchangeRate, deduction *big.Int) error
    57  	// Handshake is called by the swap protocol when a handshake is received.
    58  	Handshake(peer swarm.Address, beneficiary common.Address) error
    59  	GetDeductionForPeer(peer swarm.Address) (bool, error)
    60  	GetDeductionByPeer(peer swarm.Address) (bool, error)
    61  	AddDeductionByPeer(peer swarm.Address) error
    62  }
    63  
    64  // Service is the main implementation of the swap protocol.
    65  type Service struct {
    66  	streamer    p2p.Streamer
    67  	logger      log.Logger
    68  	swap        Swap
    69  	priceOracle priceoracle.Service
    70  	beneficiary common.Address
    71  }
    72  
    73  // New creates a new swap protocol Service.
    74  func New(streamer p2p.Streamer, logger log.Logger, beneficiary common.Address, priceOracle priceoracle.Service) *Service {
    75  	return &Service{
    76  		streamer:    streamer,
    77  		logger:      logger.WithName(loggerName).Register(),
    78  		beneficiary: beneficiary,
    79  		priceOracle: priceOracle,
    80  	}
    81  }
    82  
    83  // SetSwap sets the swap to notify.
    84  func (s *Service) SetSwap(swap Swap) {
    85  	s.swap = swap
    86  }
    87  
    88  func (s *Service) Protocol() p2p.ProtocolSpec {
    89  	return p2p.ProtocolSpec{
    90  		Name:    protocolName,
    91  		Version: protocolVersion,
    92  		StreamSpecs: []p2p.StreamSpec{
    93  			{
    94  				Name:    streamName,
    95  				Handler: s.handler,
    96  				Headler: s.headler,
    97  			},
    98  		},
    99  		ConnectOut: s.init,
   100  		ConnectIn:  s.init,
   101  	}
   102  }
   103  
   104  // init is called on outgoing connections and triggers handshake exchange
   105  func (s *Service) init(ctx context.Context, p p2p.Peer) error {
   106  	beneficiary := common.BytesToAddress(p.EthereumAddress)
   107  	return s.swap.Handshake(p.Address, beneficiary)
   108  }
   109  
   110  func (s *Service) handler(ctx context.Context, p p2p.Peer, stream p2p.Stream) (err error) {
   111  	r := protobuf.NewReader(stream)
   112  	defer func() {
   113  		if err != nil {
   114  			_ = stream.Reset()
   115  		} else {
   116  			_ = stream.FullClose()
   117  		}
   118  	}()
   119  
   120  	var req pb.EmitCheque
   121  	if err := r.ReadMsgWithContext(ctx, &req); err != nil {
   122  		return fmt.Errorf("read request from peer %v: %w", p.Address, err)
   123  	}
   124  
   125  	responseHeaders := stream.ResponseHeaders()
   126  	exchangeRate, deduction, err := swap.ParseSettlementResponseHeaders(responseHeaders)
   127  	if err != nil {
   128  		if !errors.Is(err, swap.ErrNoDeductionHeader) {
   129  			return err
   130  		}
   131  		deduction = big.NewInt(0)
   132  	}
   133  
   134  	var signedCheque *chequebook.SignedCheque
   135  	err = json.Unmarshal(req.Cheque, &signedCheque)
   136  	if err != nil {
   137  		return err
   138  	}
   139  
   140  	// signature validation
   141  	return s.swap.ReceiveCheque(ctx, p.Address, signedCheque, exchangeRate, deduction)
   142  }
   143  
   144  func (s *Service) headler(receivedHeaders p2p.Headers, peerAddress swarm.Address) (returnHeaders p2p.Headers) {
   145  
   146  	exchangeRate, deduction, err := s.priceOracle.CurrentRates()
   147  	if err != nil {
   148  		return p2p.Headers{}
   149  	}
   150  
   151  	checkPeer, err := s.swap.GetDeductionForPeer(peerAddress)
   152  	if err != nil {
   153  		return p2p.Headers{}
   154  	}
   155  
   156  	if checkPeer {
   157  		deduction = big.NewInt(0)
   158  	}
   159  
   160  	returnHeaders = swap.MakeSettlementHeaders(exchangeRate, deduction)
   161  	return
   162  }
   163  
   164  // InitiateCheque attempts to send a cheque to a peer.
   165  func (s *Service) EmitCheque(ctx context.Context, peer swarm.Address, beneficiary common.Address, amount *big.Int, issue IssueFunc) (balance *big.Int, err error) {
   166  	loggerV1 := s.logger.V(1).Register()
   167  
   168  	ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
   169  	defer cancel()
   170  
   171  	stream, err := s.streamer.NewStream(ctx, peer, nil, protocolName, protocolVersion, streamName)
   172  	if err != nil {
   173  		return nil, err
   174  	}
   175  	defer func() {
   176  		if err != nil {
   177  			_ = stream.Reset()
   178  		} else {
   179  			_ = stream.FullClose()
   180  		}
   181  	}()
   182  
   183  	// reading exchangeRated headers
   184  	returnedHeaders := stream.Headers()
   185  	exchangeRate, deduction, err := swap.ParseSettlementResponseHeaders(returnedHeaders)
   186  	if err != nil {
   187  		if !errors.Is(err, swap.ErrNoDeductionHeader) {
   188  			return nil, err
   189  		}
   190  		deduction = big.NewInt(0)
   191  	}
   192  
   193  	// comparing received headers to known truth
   194  
   195  	// get whether peer have deducted in the past
   196  	checkPeer, err := s.swap.GetDeductionByPeer(peer)
   197  	if err != nil {
   198  		return nil, err
   199  	}
   200  
   201  	// if peer is not entitled for deduction but sent non zero deduction value, return with error
   202  	if checkPeer && deduction.Cmp(big.NewInt(0)) != 0 {
   203  		return nil, ErrHaveDeduction
   204  	}
   205  
   206  	// get current global exchangeRate rate and deduction
   207  	checkExchangeRate, checkDeduction, err := s.priceOracle.CurrentRates()
   208  	if err != nil {
   209  		return nil, err
   210  	}
   211  
   212  	// exchangeRate rates should match
   213  	if exchangeRate.Cmp(checkExchangeRate) != 0 {
   214  		return nil, ErrNegotiateRate
   215  	}
   216  
   217  	// deduction values should match or be zero
   218  	if deduction.Cmp(checkDeduction) != 0 && deduction.Cmp(big.NewInt(0)) != 0 {
   219  		return nil, ErrNegotiateDeduction
   220  	}
   221  
   222  	paymentAmount := new(big.Int).Mul(amount, exchangeRate)
   223  	sentAmount := new(big.Int).Add(paymentAmount, deduction)
   224  
   225  	// issue cheque call with provided callback for sending cheque to finish transaction
   226  
   227  	balance, err = issue(ctx, beneficiary, sentAmount, func(cheque *chequebook.SignedCheque) error {
   228  		// for simplicity we use json marshaller. can be replaced by a binary encoding in the future.
   229  		encodedCheque, err := json.Marshal(cheque)
   230  		if err != nil {
   231  			return err
   232  		}
   233  
   234  		// sending cheque
   235  		loggerV1.Debug("sending cheque message to peer", "peer_address", peer, "cheque", cheque)
   236  
   237  		w := protobuf.NewWriter(stream)
   238  		return w.WriteMsgWithContext(ctx, &pb.EmitCheque{
   239  			Cheque: encodedCheque,
   240  		})
   241  
   242  	})
   243  	if err != nil {
   244  		return nil, err
   245  	}
   246  
   247  	if deduction.Cmp(big.NewInt(0)) != 0 {
   248  		err = s.swap.AddDeductionByPeer(peer)
   249  		if err != nil {
   250  			return nil, err
   251  		}
   252  	}
   253  
   254  	return balance, nil
   255  }