code.vegaprotocol.io/vega@v0.79.0/core/nodewallets/commander.go (about)

     1  // Copyright (C) 2023 Gobalsky Labs Limited
     2  //
     3  // This program is free software: you can redistribute it and/or modify
     4  // it under the terms of the GNU Affero General Public License as
     5  // published by the Free Software Foundation, either version 3 of the
     6  // License, or (at your option) any later version.
     7  //
     8  // This program is distributed in the hope that it will be useful,
     9  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    10  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    11  // GNU Affero General Public License for more details.
    12  //
    13  // You should have received a copy of the GNU Affero General Public License
    14  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    15  
    16  package nodewallets
    17  
    18  import (
    19  	"context"
    20  	"fmt"
    21  	"time"
    22  
    23  	"code.vegaprotocol.io/vega/commands"
    24  	"code.vegaprotocol.io/vega/core/nodewallets/vega"
    25  	"code.vegaprotocol.io/vega/core/txn"
    26  	"code.vegaprotocol.io/vega/logging"
    27  	api "code.vegaprotocol.io/vega/protos/vega/api/v1"
    28  	commandspb "code.vegaprotocol.io/vega/protos/vega/commands/v1"
    29  
    30  	"github.com/cenkalti/backoff"
    31  	tmctypes "github.com/cometbft/cometbft/rpc/core/types"
    32  	"github.com/golang/protobuf/proto"
    33  )
    34  
    35  const (
    36  	commanderNamedLogger = "commander"
    37  )
    38  
    39  //go:generate go run github.com/golang/mock/mockgen -destination mocks/chain_mock.go -package mocks code.vegaprotocol.io/vega/core/nodewallets Chain
    40  type Chain interface {
    41  	SubmitTransactionSync(ctx context.Context, tx *commandspb.Transaction) (*tmctypes.ResultBroadcastTx, error)
    42  	SubmitTransactionAsync(ctx context.Context, tx *commandspb.Transaction) (*tmctypes.ResultBroadcastTx, error)
    43  	GetChainID(ctx context.Context) (string, error)
    44  }
    45  
    46  //go:generate go run github.com/golang/mock/mockgen -destination mocks/blockchain_stats_mock.go -package mocks code.vegaprotocol.io/vega/core/nodewallets BlockchainStats
    47  type BlockchainStats interface {
    48  	Height() uint64
    49  }
    50  
    51  type Commander struct {
    52  	log    *logging.Logger
    53  	bc     Chain
    54  	wallet *vega.Wallet
    55  	bstats BlockchainStats
    56  }
    57  
    58  // NewCommander - used to sign and send transaction from core
    59  // e.g. NodeRegistration, NodeVote
    60  // chain argument can't be passed in cmd package, but is used for tests.
    61  func NewCommander(cfg Config, log *logging.Logger, bc Chain, w *vega.Wallet, bstats BlockchainStats) (*Commander, error) {
    62  	log = log.Named(commanderNamedLogger)
    63  	log.SetLevel(cfg.Level.Get())
    64  
    65  	return &Commander{
    66  		log:    log,
    67  		bc:     bc,
    68  		wallet: w,
    69  		bstats: bstats,
    70  	}, nil
    71  }
    72  
    73  func (c *Commander) NewTransaction(ctx context.Context, cmd txn.Command, payload proto.Message) ([]byte, error) {
    74  	chainID, err := c.bc.GetChainID(ctx)
    75  	if err != nil {
    76  		c.log.Error("couldn't retrieve chain ID",
    77  			logging.Error(err),
    78  		)
    79  		return nil, err
    80  	}
    81  	inputData := commands.NewInputData(c.bstats.Height())
    82  	wrapPayloadIntoInputData(inputData, cmd, payload)
    83  	marshalInputData, err := commands.MarshalInputData(inputData)
    84  	if err != nil {
    85  		// this should never be possible
    86  		c.log.Panic("could not marshal core transaction", logging.Error(err))
    87  	}
    88  
    89  	signature, err := c.sign(commands.BundleInputDataForSigning(marshalInputData, chainID))
    90  	if err != nil {
    91  		// this should never be possible too
    92  		c.log.Panic("could not sign command", logging.Error(err))
    93  	}
    94  
    95  	tx := commands.NewTransaction(c.wallet.PubKey().Hex(), marshalInputData, signature)
    96  	marshalledTx, err := proto.Marshal(tx)
    97  	if err != nil {
    98  		return nil, err
    99  	}
   100  	return marshalledTx, nil
   101  }
   102  
   103  // Command - send command to chain.
   104  // Note: beware when passing in an exponential back off since the done function may be called many times.
   105  func (c *Commander) Command(ctx context.Context, cmd txn.Command, payload proto.Message, done func(string, error), bo *backoff.ExponentialBackOff) {
   106  	c.command(ctx, cmd, payload, done, api.SubmitTransactionRequest_TYPE_SYNC, bo, nil)
   107  }
   108  
   109  func (c *Commander) CommandSync(ctx context.Context, cmd txn.Command, payload proto.Message, done func(string, error), bo *backoff.ExponentialBackOff) {
   110  	c.command(ctx, cmd, payload, done, api.SubmitTransactionRequest_TYPE_SYNC, bo, nil)
   111  }
   112  
   113  func (c *Commander) CommandWithPoW(ctx context.Context, cmd txn.Command, payload proto.Message, done func(string, error), bo *backoff.ExponentialBackOff, pow *commandspb.ProofOfWork) {
   114  	c.command(ctx, cmd, payload, done, api.SubmitTransactionRequest_TYPE_SYNC, bo, pow)
   115  }
   116  
   117  func (c *Commander) command(ctx context.Context, cmd txn.Command, payload proto.Message, done func(string, error), ty api.SubmitTransactionRequest_Type, bo *backoff.ExponentialBackOff, pow *commandspb.ProofOfWork) {
   118  	if c.bc == nil {
   119  		panic("commander was instantiated without a chain")
   120  	}
   121  	f := func() error {
   122  		innerCtx, cfunc := context.WithTimeout(ctx, 5*time.Second)
   123  		defer cfunc()
   124  
   125  		chainID, err := c.bc.GetChainID(innerCtx)
   126  		if err != nil {
   127  			c.log.Error("couldn't retrieve chain ID",
   128  				logging.Error(err),
   129  			)
   130  			return err
   131  		}
   132  
   133  		inputData := commands.NewInputData(c.bstats.Height())
   134  		wrapPayloadIntoInputData(inputData, cmd, payload)
   135  		marshalInputData, err := commands.MarshalInputData(inputData)
   136  		if err != nil {
   137  			// this should never be possible
   138  			c.log.Panic("could not marshal core transaction", logging.Error(err))
   139  		}
   140  
   141  		signature, err := c.sign(commands.BundleInputDataForSigning(marshalInputData, chainID))
   142  		if err != nil {
   143  			// this should never be possible too
   144  			c.log.Panic("could not sign command", logging.Error(err))
   145  		}
   146  
   147  		tx := commands.NewTransaction(c.wallet.PubKey().Hex(), marshalInputData, signature)
   148  		tx.Pow = pow
   149  
   150  		var resp *tmctypes.ResultBroadcastTx
   151  		if ty == api.SubmitTransactionRequest_TYPE_SYNC {
   152  			resp, err = c.bc.SubmitTransactionSync(innerCtx, tx)
   153  		} else {
   154  			resp, err = c.bc.SubmitTransactionAsync(innerCtx, tx)
   155  		}
   156  
   157  		var txHash string
   158  		switch {
   159  		case err != nil:
   160  			c.log.Error("could not send transaction to tendermint",
   161  				logging.Error(err),
   162  				logging.String("tx", payload.String()))
   163  		case resp.Code != 0:
   164  			err = fmt.Errorf("%s", string(resp.Data.Bytes()))
   165  			c.log.Error("transaction reached network but was rejected",
   166  				logging.Error(err))
   167  		default:
   168  			txHash = resp.Hash.String()
   169  		}
   170  
   171  		if done != nil {
   172  			done(txHash, err)
   173  		}
   174  
   175  		return err
   176  	}
   177  
   178  	if bo != nil {
   179  		go backoff.Retry(f, bo)
   180  	} else {
   181  		go f()
   182  	}
   183  }
   184  
   185  func (c *Commander) sign(marshalledData []byte) (*commandspb.Signature, error) {
   186  	sig, err := c.wallet.Sign(marshalledData)
   187  	if err != nil {
   188  		return nil, err
   189  	}
   190  
   191  	return commands.NewSignature(sig, c.wallet.Algo(), c.wallet.Version()), nil
   192  }
   193  
   194  func wrapPayloadIntoInputData(data *commandspb.InputData, cmd txn.Command, payload proto.Message) {
   195  	switch cmd {
   196  	case txn.SubmitOrderCommand, txn.CancelOrderCommand, txn.AmendOrderCommand, txn.VoteCommand, txn.WithdrawCommand, txn.LiquidityProvisionCommand, txn.ProposeCommand, txn.BatchProposeCommand, txn.SubmitOracleDataCommand, txn.StopOrdersCancellationCommand, txn.StopOrdersSubmissionCommand:
   197  		panic("command is not supported to be sent by a node.")
   198  	case txn.DelayedTransactionsWrapper:
   199  		if underlyingCmd, ok := payload.(*commandspb.DelayedTransactionsWrapper); ok {
   200  			data.Command = &commandspb.InputData_DelayedTransactionsWrapper{
   201  				DelayedTransactionsWrapper: underlyingCmd,
   202  			}
   203  		}
   204  	case txn.ProtocolUpgradeCommand:
   205  		if underlyingCmd, ok := payload.(*commandspb.ProtocolUpgradeProposal); ok {
   206  			data.Command = &commandspb.InputData_ProtocolUpgradeProposal{
   207  				ProtocolUpgradeProposal: underlyingCmd,
   208  			}
   209  		} else {
   210  			panic("failed to wrap to ProtocolUpgradeProposal")
   211  		}
   212  	case txn.AnnounceNodeCommand:
   213  		if underlyingCmd, ok := payload.(*commandspb.AnnounceNode); ok {
   214  			data.Command = &commandspb.InputData_AnnounceNode{
   215  				AnnounceNode: underlyingCmd,
   216  			}
   217  		} else {
   218  			panic("failed to wrap to AnnounceNode")
   219  		}
   220  	case txn.ValidatorHeartbeatCommand:
   221  		if underlyingCmd, ok := payload.(*commandspb.ValidatorHeartbeat); ok {
   222  			data.Command = &commandspb.InputData_ValidatorHeartbeat{
   223  				ValidatorHeartbeat: underlyingCmd,
   224  			}
   225  		} else {
   226  			panic("failed to wrap to ValidatorHeartbeat")
   227  		}
   228  	case txn.NodeVoteCommand:
   229  		if underlyingCmd, ok := payload.(*commandspb.NodeVote); ok {
   230  			data.Command = &commandspb.InputData_NodeVote{
   231  				NodeVote: underlyingCmd,
   232  			}
   233  		} else {
   234  			panic("failed to wrap to NodeVote")
   235  		}
   236  	case txn.NodeSignatureCommand:
   237  		if underlyingCmd, ok := payload.(*commandspb.NodeSignature); ok {
   238  			data.Command = &commandspb.InputData_NodeSignature{
   239  				NodeSignature: underlyingCmd,
   240  			}
   241  		} else {
   242  			panic("failed to wrap to NodeSignature")
   243  		}
   244  	case txn.ChainEventCommand:
   245  		if underlyingCmd, ok := payload.(*commandspb.ChainEvent); ok {
   246  			data.Command = &commandspb.InputData_ChainEvent{
   247  				ChainEvent: underlyingCmd,
   248  			}
   249  		} else {
   250  			panic("failed to wrap to ChainEvent")
   251  		}
   252  	case txn.StateVariableProposalCommand:
   253  		if underlyingCmd, ok := payload.(*commandspb.StateVariableProposal); ok {
   254  			data.Command = &commandspb.InputData_StateVariableProposal{
   255  				StateVariableProposal: underlyingCmd,
   256  			}
   257  		} else {
   258  			panic("failed to wrap StateVariableProposal")
   259  		}
   260  	case txn.RotateEthereumKeySubmissionCommand:
   261  		if underlyingCmd, ok := payload.(*commandspb.EthereumKeyRotateSubmission); ok {
   262  			data.Command = &commandspb.InputData_EthereumKeyRotateSubmission{
   263  				EthereumKeyRotateSubmission: underlyingCmd,
   264  			}
   265  		} else {
   266  			panic("failed to wrap RotateEthereumKeySubmissionCommand")
   267  		}
   268  	default:
   269  		panic(fmt.Errorf("command %v is not supported", cmd))
   270  	}
   271  }