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 }