github.com/mavryk-network/mvgo@v1.19.9/rpc/run.go (about) 1 // Copyright (c) 2020-2022 Blockwatch Data Inc. 2 // Author: alex@blockwatch.cc 3 4 package rpc 5 6 import ( 7 "bytes" 8 "context" 9 "encoding/hex" 10 "fmt" 11 12 "github.com/mavryk-network/mvgo/codec" 13 "github.com/mavryk-network/mvgo/mavryk" 14 "github.com/mavryk-network/mvgo/micheline" 15 "github.com/mavryk-network/mvgo/signer" 16 ) 17 18 const ExtraSafetyMargin int64 = 100 // used to adjust gas and storage estimations 19 20 var ( 21 // for reveal 22 DefaultRevealLimits = mavryk.Limits{ 23 Fee: 1000, 24 GasLimit: 1000, 25 } 26 // for transfers to mv1/2/3 27 DefaultTransferLimitsEOA = mavryk.Limits{ 28 Fee: 1000, 29 GasLimit: 1420, // 1820 when source is emptied 30 } 31 // for transfers to manager.tz 32 DefaultTransferLimitsKT1 = mavryk.Limits{ 33 Fee: 1000, 34 GasLimit: 2078, 35 } 36 // for delegation 37 DefaultDelegationLimitsEOA = mavryk.Limits{ 38 Fee: 1000, 39 GasLimit: 1000, 40 } 41 // for baker registration 42 DefaultBakerRegistrationLimits = mavryk.Limits{ 43 Fee: 1000, 44 GasLimit: 1000, 45 } 46 // for simulating contract calls and other operations 47 // used when no explicit costs are set 48 DefaultSimulationLimits = mavryk.Limits{ 49 GasLimit: mavryk.DefaultParams.HardGasLimitPerOperation, 50 StorageLimit: mavryk.DefaultParams.HardStorageLimitPerOperation, 51 } 52 ) 53 54 type CallOptions struct { 55 Confirmations int64 // number of confirmations to wait after broadcast 56 MaxFee int64 // max acceptable fee, optional (default = 0) 57 TTL int64 // max lifetime for operations in blocks 58 IgnoreLimits bool // ignore simulated limits and use user-defined limits from op 59 ExtraGasMargin int64 // safety margin in case simulation underestimates future usage 60 SimulationBlockID BlockID // custom block id to simulate operation (default is head, use to select a past block) 61 SimulationOffset int64 // custom block offset for future block simulations 62 Signer signer.Signer // optional signer interface to use for signing the transaction 63 Sender mavryk.Address // optional address to sign for (use when signer manages multiple addresses) 64 Observer *Observer // optional custom block observer for waiting on confirmations 65 } 66 67 var DefaultOptions = CallOptions{ 68 Confirmations: 2, 69 TTL: mavryk.DefaultParams.MaxOperationsTTL - 2, 70 MaxFee: 1_000_000, 71 ExtraGasMargin: ExtraSafetyMargin, 72 SimulationOffset: 5, // use pessimistic value to prevent gas exhausted errors (node's default is 3) 73 } 74 75 func NewCallOptions() *CallOptions { 76 o := DefaultOptions 77 return &o 78 } 79 80 type RunOperationRequest struct { 81 Operation *codec.Op `json:"operation"` 82 ChainId mavryk.ChainIdHash `json:"chain_id"` 83 Latency int64 `json:"latency,omitempty"` 84 } 85 86 type RunViewRequest struct { 87 Contract mavryk.Address `json:"contract"` 88 Entrypoint string `json:"entrypoint,omitempty"` 89 View string `json:"view,omitempty"` 90 Input micheline.Prim `json:"input"` 91 ChainId mavryk.ChainIdHash `json:"chain_id"` 92 Source mavryk.Address `json:"source"` 93 Payer mavryk.Address `json:"payer"` 94 Gas mavryk.N `json:"gas"` 95 Mode string `json:"unparsing_mode"` // "Readable" | "Optimized" 96 UnlimitedGas bool `json:"unlimited_gas,omitempty"` // view 97 Now string `json:"now,omitempty"` // view 98 } 99 100 type RunViewResponse struct { 101 Data micheline.Prim `json:"data"` 102 } 103 104 type RunCodeRequest struct { 105 ChainId mavryk.ChainIdHash `json:"chain_id"` 106 Script micheline.Code `json:"script"` 107 Storage micheline.Prim `json:"storage"` 108 Input micheline.Prim `json:"input"` 109 Amount mavryk.N `json:"amount"` 110 Balance mavryk.N `json:"balance"` 111 Source *mavryk.Address `json:"source,omitempty"` 112 Payer *mavryk.Address `json:"payer,omitempty"` 113 Gas *mavryk.N `json:"gas,omitempty"` 114 Entrypoint string `json:"entrypoint,omitempty"` 115 } 116 117 // RunCodeResponse - 118 type RunCodeResponse struct { 119 Operations []Operation `json:"operations"` 120 Storage micheline.Prim `json:"storage"` 121 BigmapDiff micheline.BigmapEvents `json:"big_map_diff,omitempty"` 122 LazyStorageDiff micheline.LazyEvents `json:"lazy_storage_diff,omitempty"` 123 } 124 125 // Complete ensures an operation is compatible with the current source account's 126 // on-chain state. Sets branch for TTL control, replay counters, and reveals 127 // the sender's pubkey if not published yet. 128 func (c *Client) Complete(ctx context.Context, o *codec.Op, key mavryk.Key) error { 129 needBranch := !o.Branch.IsValid() 130 needCounter := o.NeedCounter() 131 mayNeedReveal := len(o.Contents) > 0 && o.Contents[0].Kind() != mavryk.OpTypeReveal 132 133 if !needBranch && !mayNeedReveal && !needCounter { 134 return nil 135 } 136 137 // add branch for TTL control 138 if needBranch { 139 ofs := o.Params.MaxOperationsTTL - o.TTL 140 hash, err := c.GetBlockHash(ctx, NewBlockOffset(Head, -ofs)) 141 if err != nil { 142 return err 143 } 144 o.WithBranch(hash) 145 } 146 147 if needCounter || mayNeedReveal { 148 // fetch current state 149 state, err := c.GetContractExt(ctx, key.Address(), Head) 150 if err != nil { 151 return err 152 } 153 154 // add reveal if necessary 155 if mayNeedReveal && !state.IsRevealed() { 156 reveal := &codec.Reveal{ 157 Manager: codec.Manager{ 158 Source: key.Address(), 159 }, 160 PublicKey: key, 161 } 162 reveal.WithLimits(DefaultRevealLimits) 163 o.WithContentsFront(reveal) 164 needCounter = true 165 } 166 167 // add counters 168 if needCounter { 169 nextCounter := state.Counter + 1 170 for _, op := range o.Contents { 171 // skip non-manager ops 172 if op.GetCounter() < 0 { 173 continue 174 } 175 op.WithCounter(nextCounter) 176 nextCounter++ 177 } 178 } 179 } 180 return nil 181 } 182 183 // Simulate dry-runs the execution of the operation against the current state 184 // of a Tezos node in order to estimate execution costs and fees (fee/burn/gas/storage). 185 func (c *Client) Simulate(ctx context.Context, o *codec.Op, opts *CallOptions) (*Receipt, error) { 186 sim := &codec.Op{ 187 Branch: o.Branch, 188 Contents: o.Contents, 189 Signature: mavryk.ZeroSignature, 190 TTL: o.TTL, 191 Params: c.Params, 192 } 193 194 if opts == nil { 195 opts = &DefaultOptions 196 } 197 198 if sim.TTL == 0 && opts != nil { 199 sim.TTL = opts.TTL 200 } 201 202 if !sim.Branch.IsValid() { 203 ofs := o.Params.MaxOperationsTTL - sim.TTL 204 hash, err := c.GetBlockHash(ctx, NewBlockOffset(Head, -ofs)) 205 if err != nil { 206 return nil, err 207 } 208 sim.Branch = hash 209 } 210 211 if !opts.IgnoreLimits { 212 // use default gas/storage limits, set min fee 213 for _, op := range o.Contents { 214 l := op.Limits() 215 if l.GasLimit == 0 { 216 l.GasLimit = DefaultSimulationLimits.GasLimit / int64(len(o.Contents)) 217 } 218 if l.StorageLimit == 0 { 219 l.StorageLimit = DefaultSimulationLimits.StorageLimit / int64(len(o.Contents)) 220 } 221 op.WithLimits(l) 222 } 223 } 224 225 req := RunOperationRequest{ 226 Operation: sim, 227 ChainId: c.ChainId, 228 } 229 var err error 230 resp := &Operation{} 231 232 // select simulation method based on requested block 233 if opts.SimulationBlockID != nil { 234 // simulate in the past 235 err = c.RunOperation(ctx, opts.SimulationBlockID, req, resp) 236 } else { 237 // simulate in the future 238 req.Latency = opts.SimulationOffset 239 err = c.SimulateOperation(ctx, Head, req, resp) 240 } 241 if err != nil { 242 return nil, err 243 } 244 245 // TODO: adjust min fee using known gas units before return so that res.Cost() 246 // reflects the entire cost that Send() will pay 247 rcpt := &Receipt{ 248 Op: resp, 249 } 250 251 // fail with Tezos error when simulation failed 252 if !rcpt.IsSuccess() { 253 return rcpt, rcpt.Error() 254 } 255 return rcpt, nil 256 } 257 258 // Validate compares local serializiation against remote RPC serialization of the 259 // operation and returns an error on mismatch. 260 func (c *Client) Validate(ctx context.Context, o *codec.Op) error { 261 op := &codec.Op{ 262 Branch: o.Branch, 263 Contents: o.Contents, 264 } 265 local := op.Bytes() 266 var remote mavryk.HexBytes 267 if err := c.ForgeOperation(ctx, Head, op, &remote); err != nil { 268 return err 269 } 270 if !bytes.Equal(local, remote.Bytes()) { 271 return fmt.Errorf("tezos: mismatch between local and remote serialized operations:\n local=%s\n remote=%s", 272 hex.EncodeToString(local), hex.EncodeToString(remote)) 273 } 274 return nil 275 } 276 277 // Broadcast sends the signed operation to network and returns the operation hash 278 // on successful pre-validation. 279 func (c *Client) Broadcast(ctx context.Context, o *codec.Op) (mavryk.OpHash, error) { 280 return c.BroadcastOperation(ctx, o.Bytes()) 281 } 282 283 // Send is a convenience wrapper for sending operations. It auto-completes gas and storage limit, 284 // ensures minimum fees are set, protects against fee overpayment, signs and broadcasts the final 285 // operation and waits for a defined number of confirmations. 286 func (c *Client) Send(ctx context.Context, op *codec.Op, opts *CallOptions) (*Receipt, error) { 287 if opts == nil { 288 opts = &DefaultOptions 289 } 290 291 signer := c.Signer 292 if opts.Signer != nil { 293 signer = opts.Signer 294 } 295 296 // identify the sender address for signing the message 297 addr := opts.Sender 298 if !addr.IsValid() { 299 addrs, err := signer.ListAddresses(ctx) 300 if err != nil { 301 return nil, err 302 } 303 addr = addrs[0] 304 } 305 306 key, err := signer.GetKey(ctx, addr) 307 if err != nil { 308 return nil, err 309 } 310 311 // use custom observer when provided 312 mon := c.BlockObserver 313 if opts.Observer != nil { 314 mon = opts.Observer 315 } 316 317 // ensure block observer is running 318 mon.Listen(c) 319 320 // set source and params on all ops 321 op.WithSource(key.Address()).WithParams(c.Params) 322 323 // auto-complete op with branch/ttl, source counter, reveal 324 err = c.Complete(ctx, op, key) 325 if err != nil { 326 return nil, err 327 } 328 329 // simulate to check tx validity and estimate cost 330 sim, err := c.Simulate(ctx, op, opts) 331 if err != nil { 332 return nil, err 333 } 334 335 // fail with Tezos error when simulation failed 336 if !sim.IsSuccess() { 337 return nil, sim.Error() 338 } 339 340 // apply simulated cost as limits to tx list 341 if !opts.IgnoreLimits { 342 op.WithLimits(sim.MinLimits(), opts.ExtraGasMargin) 343 } 344 345 // log info about tx costs 346 c.logDebug(func() { 347 costs := sim.Costs() 348 for i, v := range op.Contents { 349 verb := "used" 350 if opts.IgnoreLimits { 351 verb = "forced" 352 } 353 limits := v.Limits() 354 c.Log.Debugf("OP#%03d: %s gas_used(sim)=%d storage_used(sim)=%d storage_burn(sim)=%d alloc_burn(sim)=%d fee(%s)=%d gas_limit(%s)=%d storage_limit(%s)=%d ", 355 i, v.Kind(), costs[i].GasUsed, costs[i].StorageUsed, costs[i].StorageBurn, costs[i].AllocationBurn, 356 verb, limits.Fee, verb, limits.GasLimit, verb, limits.StorageLimit, 357 ) 358 } 359 }) 360 361 // check minFee calc against maxFee if set 362 if opts.MaxFee > 0 { 363 if l := op.Limits(); l.Fee > opts.MaxFee { 364 return nil, fmt.Errorf("estimated cost %d > max %d", l.Fee, opts.MaxFee) 365 } 366 } 367 368 // sign digest 369 sig, err := signer.SignOperation(ctx, addr, op) 370 if err != nil { 371 return nil, err 372 } 373 op.WithSignature(sig) 374 375 // trace what we'll broadcast 376 c.logTrace(func() { 377 buf, _ := op.MarshalJSON() 378 c.Log.Tracef("Broadcast: %s", string(buf)) 379 }) 380 381 // broadcast 382 hash, err := c.Broadcast(ctx, op) 383 if err != nil { 384 return nil, err 385 } 386 387 // wait for confirmations 388 res := NewResult(hash).WithTTL(op.TTL).WithConfirmations(opts.Confirmations) 389 390 // wait for confirmations 391 res.Listen(mon) 392 res.WaitContext(ctx) 393 if err := res.Err(); err != nil { 394 return nil, err 395 } 396 397 // return receipt 398 return res.GetReceipt(ctx) 399 } 400 401 // RunOperation simulates executing an operation without requiring a valid signature. 402 // The call returns the execution result as regular operation receipt. 403 func (c *Client) RunOperation(ctx context.Context, id BlockID, body, resp interface{}) error { 404 u := fmt.Sprintf("chains/main/blocks/%s/helpers/scripts/run_operation", id) 405 return c.Post(ctx, u, body, resp) 406 } 407 408 // RunCode simulates executing of provided code on the context of a contract at selected block. 409 func (c *Client) RunCode(ctx context.Context, id BlockID, body, resp interface{}) error { 410 u := fmt.Sprintf("chains/main/blocks/%s/helpers/scripts/run_code", id) 411 return c.Post(ctx, u, body, resp) 412 } 413 414 // RunCallback simulates executing of TZip4 view on the context of a contract at selected block. 415 func (c *Client) RunCallback(ctx context.Context, id BlockID, body, resp interface{}) error { 416 u := fmt.Sprintf("chains/main/blocks/%s/helpers/scripts/run_view", id) 417 return c.Post(ctx, u, body, resp) 418 } 419 420 // RunView simulates executing of on on-chain view on the context of a contract at selected block. 421 func (c *Client) RunView(ctx context.Context, id BlockID, body, resp interface{}) error { 422 u := fmt.Sprintf("chains/main/blocks/%s/helpers/scripts/run_script_view", id) 423 return c.Post(ctx, u, body, resp) 424 } 425 426 // TraceCode simulates executing of code on the context of a contract at selected block and 427 // returns a full execution trace. 428 func (c *Client) TraceCode(ctx context.Context, id BlockID, body, resp interface{}) error { 429 u := fmt.Sprintf("chains/main/blocks/%s/helpers/scripts/trace_code", id) 430 return c.Post(ctx, u, body, resp) 431 } 432 433 // BroadcastOperation sends a signed operation to the network (injection). 434 // The call returns the operation hash on success. If theoperation was rejected 435 // by the node error is of type RPCError. 436 func (c *Client) BroadcastOperation(ctx context.Context, body []byte) (hash mavryk.OpHash, err error) { 437 err = c.Post(ctx, "injection/operation", hex.EncodeToString(body), &hash) 438 return 439 } 440 441 // ForgeOperation uses a remote node to serialize an operation to its binary format. 442 // The result of this call SHOULD NEVER be used for signing the operation, it is only 443 // meant for validating the locally generated serialized output. 444 func (c *Client) ForgeOperation(ctx context.Context, id BlockID, body, resp interface{}) error { 445 u := fmt.Sprintf("chains/main/blocks/%s/helpers/forge/operations", id) 446 return c.Post(ctx, u, body, resp) 447 } 448 449 // SimulateOperation simulates executing an operation without requiring a valid signature. 450 // The call returns the execution result as regular operation receipt with estimated 451 // future gas usage. 452 // 453 // Note gas consumption may differ based on whether a contract is cached inside a node 454 // at the time of operation inclusion in a block. The contract cache is dynamic 455 // so under rare circumstances the simulation can underestimates real gas cost 456 // and a contract call may fail. In such cases attempt to resend the transaction with a 457 // higher gas margin (CallOptions.ExtraGasMargin > ExtraSafetyMargin). 458 // 459 // For simulation purposes a future cache state is predicted. You can control the 460 // future simulation point via RunOperationRequest.Latency (in blocks). 461 func (c *Client) SimulateOperation(ctx context.Context, id BlockID, body, resp interface{}) error { 462 u := fmt.Sprintf("chains/main/blocks/%s/helpers/scripts/simulate_operation", id) 463 return c.Post(ctx, u, body, resp) 464 }