github.com/mavryk-network/mvgo@v1.19.9/codec/op.go (about) 1 // Copyright (c) 2020-2023 Blockwatch Data Inc. 2 // Author: alex@blockwatch.cc 3 4 package codec 5 6 import ( 7 "bytes" 8 "encoding" 9 "encoding/binary" 10 "fmt" 11 "io" 12 "strconv" 13 14 "github.com/mavryk-network/mvgo/mavryk" 15 "github.com/mavryk-network/mvgo/micheline" 16 ) 17 18 const ( 19 EmmyBlockWatermark byte = 0x01 // deprecated 20 EmmyEndorsementWatermark byte = 0x02 // deprecated 21 OperationWatermark byte = 0x03 22 TenderbakeBlockWatermark byte = 0x11 23 TenderbakePreendorsementWatermark byte = 0x12 24 TenderbakeEndorsementWatermark byte = 0x13 25 ) 26 27 var ( 28 // enc defines the default wire encoding used for Tezos messages 29 enc = binary.BigEndian 30 ) 31 32 // Operation is a generic type used to handle different Tezos operation 33 // types inside an operation's contents list. 34 type Operation interface { 35 Kind() mavryk.OpType 36 Limits() mavryk.Limits 37 GetCounter() int64 38 WithSource(mavryk.Address) 39 WithCounter(int64) 40 WithLimits(mavryk.Limits) 41 encoding.BinaryMarshaler 42 encoding.BinaryUnmarshaler 43 MarshalJSON() ([]byte, error) 44 EncodeBuffer(buf *bytes.Buffer, p *mavryk.Params) error 45 DecodeBuffer(buf *bytes.Buffer, p *mavryk.Params) error 46 } 47 48 // Op is a container used to collect, serialize and sign Tezos operations. 49 // It serves as a low level building block for constructing and serializing 50 // operations, but is agnostic to the order/lifecycle in which data is added 51 // or updated. 52 type Op struct { 53 Branch mavryk.BlockHash `json:"branch"` // used for TTL handling 54 Contents []Operation `json:"contents"` // non-zero list of transactions 55 Signature mavryk.Signature `json:"signature"` // added during the lifecycle 56 ChainId *mavryk.ChainIdHash `json:"-"` // optional, used for remote signing only 57 TTL int64 `json:"-"` // optional, specify TTL in blocks 58 Params *mavryk.Params `json:"-"` // optional, define protocol to encode for 59 Source mavryk.Address `json:"-"` // optional, used as manager/sender 60 } 61 62 // NewOp creates a new empty operation that uses default params and a 63 // default operation TTL. 64 func NewOp() *Op { 65 return &Op{ 66 Params: mavryk.DefaultParams, 67 TTL: mavryk.DefaultParams.MaxOperationsTTL - 2, // Ithaca recommendation 68 } 69 } 70 71 // NeedCounter returns true if any of the contained operations has not assigned 72 // a valid counter value. 73 func (o Op) NeedCounter() bool { 74 for _, v := range o.Contents { 75 if v.GetCounter() == 0 { 76 return true 77 } 78 } 79 return false 80 } 81 82 // WithParams defines the protocol and other chain configuration params for which 83 // the operation will be encoded. If unset, defaults to mavryk.DefaultParams. 84 func (o *Op) WithParams(p *mavryk.Params) *Op { 85 o.Params = p 86 return o 87 } 88 89 // WithContents adds a Tezos operation to the end of the contents list. 90 func (o *Op) WithContents(op Operation) *Op { 91 o.Contents = append(o.Contents, op) 92 return o 93 } 94 95 // WithContentsFront adds a Tezos operation to the front of the contents list. 96 func (o *Op) WithContentsFront(op Operation) *Op { 97 o.Contents = append([]Operation{op}, o.Contents...) 98 return o 99 } 100 101 // WithSource sets the source for all manager operations to addr. It is required 102 // before calling other WithXXX functions. 103 func (o *Op) WithSource(addr mavryk.Address) *Op { 104 for _, v := range o.Contents { 105 v.WithSource(addr) 106 } 107 o.Source = addr 108 return o 109 } 110 111 // WithTransfer adds a simple value transfer transaction to the contents list. 112 // Source must be defined via WithSource() before calling this function. 113 func (o *Op) WithTransfer(to mavryk.Address, amount int64) *Op { 114 o.Contents = append(o.Contents, &Transaction{ 115 Manager: Manager{ 116 Source: o.Source, 117 Counter: 0, 118 }, 119 Amount: mavryk.N(amount), 120 Destination: to, 121 }) 122 return o 123 } 124 125 // WithCall adds a contract call transaction to the contents list. 126 // Source must be defined via WithSource() before calling this function. 127 func (o *Op) WithCall(to mavryk.Address, params micheline.Parameters) *Op { 128 o.Contents = append(o.Contents, &Transaction{ 129 Manager: Manager{ 130 Source: o.Source, 131 Counter: 0, 132 }, 133 Destination: to, 134 Parameters: ¶ms, 135 }) 136 return o 137 } 138 139 // WithCallExt adds a contract call with value transfer transaction to the contents list. 140 // Source must be defined via WithSource() before calling this function. 141 func (o *Op) WithCallExt(to mavryk.Address, params micheline.Parameters, amount int64) *Op { 142 o.Contents = append(o.Contents, &Transaction{ 143 Manager: Manager{ 144 Source: o.Source, 145 Counter: 0, 146 }, 147 Amount: mavryk.N(amount), 148 Destination: to, 149 Parameters: ¶ms, 150 }) 151 return o 152 } 153 154 // WithOrigination adds a contract origination transaction to the contents list. 155 // Source must be defined via WithSource() before calling this function. 156 func (o *Op) WithOrigination(script micheline.Script) *Op { 157 o.Contents = append(o.Contents, &Origination{ 158 Manager: Manager{ 159 Source: o.Source, 160 Counter: 0, 161 }, 162 Script: script, 163 }) 164 return o 165 } 166 167 // WithOriginationExt adds a contract origination transaction with optional delegation to 168 // baker and an optional value transfer to the contents list. 169 // Source must be defined via WithSource() before calling this function. 170 func (o *Op) WithOriginationExt(script micheline.Script, baker mavryk.Address, amount int64) *Op { 171 o.Contents = append(o.Contents, &Origination{ 172 Manager: Manager{ 173 Source: o.Source, 174 Counter: 0, 175 }, 176 Balance: mavryk.N(amount), 177 Delegate: baker, 178 Script: script, 179 }) 180 return o 181 } 182 183 // WithDelegation adds a delegation transaction to the contents list. 184 // Source must be defined via WithSource() before calling this function. 185 func (o *Op) WithDelegation(to mavryk.Address) *Op { 186 o.Contents = append(o.Contents, &Delegation{ 187 Manager: Manager{ 188 Source: o.Source, 189 Counter: 0, 190 }, 191 Delegate: to, 192 }) 193 return o 194 } 195 196 // WithUndelegation adds a delegation transaction that resets the callers baker to null 197 // to the contents list. 198 // Source must be defined via WithSource() before calling this function. 199 func (o *Op) WithUndelegation() *Op { 200 o.Contents = append(o.Contents, &Delegation{ 201 Manager: Manager{ 202 Source: o.Source, 203 Counter: 0, 204 }, 205 }) 206 return o 207 } 208 209 // WithRegisterBaker adds a delegation transaction that registers the caller 210 // as baker to the contents list. 211 // Source must be defined via WithSource() before calling this function. 212 func (o *Op) WithRegisterBaker() *Op { 213 o.Contents = append(o.Contents, &Delegation{ 214 Manager: Manager{ 215 Source: o.Source, 216 Counter: 0, 217 }, 218 Delegate: o.Source, 219 }) 220 return o 221 } 222 223 // WithSetBakerParams adds a set_delegate_parameters call where target is 224 // source. The caller must be a registered baker. 225 // Source must be defined via WithSource() before calling this function. 226 func (o *Op) WithSetBakerParams(edge, limit int64) *Op { 227 return o.WithCall( 228 o.Source, 229 micheline.Parameters{ 230 Entrypoint: micheline.SET_DELEGATE_PARAMETERS, 231 Value: micheline.NewCombPair( 232 micheline.NewInt64(edge), 233 micheline.NewInt64(limit), 234 micheline.Unit, 235 ), 236 }, 237 ) 238 } 239 240 // WithStake sends a stake pseudo call to source to lock tokens for staking. 241 // The caller must delegate to a baker and this baker is implictly chosen to 242 // stake with. 243 // Source must be defined via WithSource() before calling this function. 244 func (o *Op) WithStake(amount int64) *Op { 245 return o.WithCallExt( 246 o.Source, 247 micheline.Parameters{ 248 Entrypoint: micheline.STAKE, 249 Value: micheline.Unit, 250 }, 251 amount, 252 ) 253 } 254 255 // WithUnstake sends an unstake pseudo call to source which creates an 256 // unstake request for amount tokens. 257 // Source must be defined via WithSource() before calling this function. 258 func (o *Op) WithUnstake(amount int64) *Op { 259 return o.WithCallExt( 260 o.Source, 261 micheline.Parameters{ 262 Entrypoint: micheline.UNSTAKE, 263 Value: micheline.Unit, 264 }, 265 amount, 266 ) 267 } 268 269 // WithUnstakeAll sends an unstake pseudo call to source which creates an 270 // unstake request for all currently staked tokens. 271 // Source must be defined via WithSource() before calling this function. 272 func (o *Op) WithUnstakeAll(amount int64) *Op { 273 return o.WithCallExt( 274 o.Source, 275 micheline.Parameters{ 276 Entrypoint: micheline.UNSTAKE, 277 Value: micheline.Unit, 278 }, 279 9223372036854775807, 280 ) 281 } 282 283 // WithFinalizeUnstake sends a finalize_unstake pseudo call to source which 284 // moves all unfrozen unstaked tokens back to spendable balance. 285 // Source must be defined via WithSource() before calling this function. 286 func (o *Op) WithFinalizeUnstake() *Op { 287 return o.WithCall( 288 o.Source, 289 micheline.Parameters{ 290 Entrypoint: micheline.FINALIZE_UNSTAKE, 291 Value: micheline.Unit, 292 }, 293 ) 294 } 295 296 // WithRegisterConstant adds a global constant registration transaction to the contents list. 297 // Source must be defined via WithSource() before calling this function. 298 func (o *Op) WithRegisterConstant(value micheline.Prim) *Op { 299 o.Contents = append(o.Contents, &RegisterGlobalConstant{ 300 Manager: Manager{ 301 Source: o.Source, 302 Counter: 0, 303 }, 304 Value: value, 305 }) 306 return o 307 } 308 309 // WithTTL sets a time-to-live for the operation in number of blocks. This may be 310 // used as a convenience method instead of setting a branch directly, but requires 311 // to use an autocomplete handler, wallet or custom function that fetches the hash 312 // of block head~N as branch. Note that serialization will fail until a brach is set. 313 func (o *Op) WithTTL(n int64) *Op { 314 if n > o.Params.MaxOperationsTTL { 315 n = o.Params.MaxOperationsTTL - 2 // Ithaca adjusted 316 } else if n < 0 { 317 n = 1 318 } 319 o.TTL = n 320 return o 321 } 322 323 // WithBranch sets the branch for this operation to hash. 324 func (o *Op) WithBranch(hash mavryk.BlockHash) *Op { 325 o.Branch = hash 326 return o 327 } 328 329 // WithChainId sets chain_id for this operation to id. Use this only for remote signing 330 // of (pre)endorsements as it creates an invalid binary encoding otherwise. 331 func (o *Op) WithChainId(id mavryk.ChainIdHash) *Op { 332 clone := id.Clone() 333 o.ChainId = &clone 334 return o 335 } 336 337 // WithLimits sets the limits (fee, gas and storage limit) of each 338 // contained operation to provided limits. Use this to apply values from 339 // simulation with an optional safety margin on gas and storage. This will also 340 // calculate the minFee for each operation in the list and add the minFee 341 // for header bytes (branch and signature) to the first operation in a list. 342 // 343 // Setting a user-defined fee for each individual operation is only honored 344 // when its higher than minFee. Note that when sending batch operations all 345 // fees must be >= the individual minFee. Otherwise the minFee rule will 346 // apply to all zero/lower fee operations and the entire batch may overpay 347 // (e.g. if you have the first operation pay all fees for example and set 348 // remaining fees to zero). 349 func (o *Op) WithLimits(limits []mavryk.Limits, margin int64) *Op { 350 for i, v := range o.Contents { 351 if len(limits) < i { 352 continue 353 } 354 // apply simulated limit to get a better size estimate 355 v.WithLimits(limits[i]) 356 357 // re-calculate limits with safety margins 358 gas := limits[i].GasLimit + margin 359 storage := limits[i].StorageLimit 360 if storage > 0 { 361 storage += margin 362 } 363 adj := mavryk.Limits{ 364 GasLimit: gas, 365 StorageLimit: storage, 366 } 367 368 // Apply limits, and re-compute the fee if needed. 369 // This is required, because fee value has an impact on operation size. 370 var lastFee int64 = -1 371 for lastFee < adj.Fee { 372 lastFee = adj.Fee 373 374 adj.Fee = max64(limits[i].Fee, CalculateMinFee(v, gas, i == 0, o.Params)) 375 v.WithLimits(adj) 376 } 377 } 378 return o 379 } 380 381 func (o *Op) WithMinFee() *Op { 382 for i, v := range o.Contents { 383 // extend current limit with minimum fee estimate based on size + gas 384 lim := v.Limits() 385 386 adj := mavryk.Limits{ 387 GasLimit: lim.GasLimit, 388 StorageLimit: lim.StorageLimit, 389 Fee: max64(lim.Fee, CalculateMinFee(v, lim.GasLimit, i == 0, o.Params)), 390 } 391 392 // use adjusted limits 393 v.WithLimits(adj) 394 } 395 return o 396 } 397 398 // Limits returns the sum of all limits (fee, gas, storage limit) currently 399 // set for all contained operations. 400 func (o Op) Limits() mavryk.Limits { 401 var l mavryk.Limits 402 for _, v := range o.Contents { 403 l = l.Add(v.Limits()) 404 } 405 return l 406 } 407 408 // Bytes serializes the operation into binary form. When no signature is set, the 409 // result can be used as input for signing, if a signature is set the result is 410 // ready to be broadcast. Returns a nil slice when branch or contents are empty. 411 func (o *Op) Bytes() []byte { 412 if len(o.Contents) == 0 || !o.Branch.IsValid() { 413 return nil 414 } 415 p := o.Params 416 if p == nil { 417 p = mavryk.DefaultParams 418 } 419 buf := bytes.NewBuffer(nil) 420 buf.Write(o.Branch.Bytes()) 421 for _, v := range o.Contents { 422 _ = v.EncodeBuffer(buf, p) 423 } 424 switch o.Contents[0].Kind() { 425 case mavryk.OpTypeEndorsementWithSlot: 426 // no signature 427 default: 428 if o.Signature.IsValid() { 429 buf.Write(o.Signature.Data) // raw, without type (!) 430 } 431 } 432 return buf.Bytes() 433 } 434 435 // WatermarkedBytes serializes the operation and prefixes it with a watermark. 436 // This format is only used for signing. Watermarked data is not useful anywhere 437 // else. 438 func (o *Op) WatermarkedBytes() []byte { 439 if len(o.Contents) == 0 || !o.Branch.IsValid() { 440 return nil 441 } 442 p := o.Params 443 if p == nil { 444 p = mavryk.DefaultParams 445 } 446 buf := bytes.NewBuffer(nil) 447 switch o.Contents[0].Kind() { 448 case mavryk.OpTypeEndorsement, mavryk.OpTypeEndorsementWithSlot: 449 if p.OperationTagsVersion < 2 { 450 buf.WriteByte(EmmyEndorsementWatermark) 451 } else { 452 buf.WriteByte(TenderbakeEndorsementWatermark) 453 } 454 if o.ChainId != nil { 455 buf.Write(o.ChainId.Bytes()) 456 } 457 case mavryk.OpTypePreendorsement: 458 buf.WriteByte(TenderbakePreendorsementWatermark) 459 if o.ChainId != nil { 460 buf.Write(o.ChainId.Bytes()) 461 } 462 default: 463 buf.WriteByte(OperationWatermark) 464 } 465 buf.Write(o.Branch.Bytes()) 466 for _, v := range o.Contents { 467 _ = v.EncodeBuffer(buf, p) 468 } 469 return buf.Bytes() 470 } 471 472 // Digest returns a 32 byte blake2b hash for signing the operation. The pre-image 473 // is the binary serialized operation (without signature) prefixed with a 474 // type-dependent watermark byte. 475 func (o *Op) Digest() []byte { 476 d := mavryk.Digest(o.WatermarkedBytes()) 477 return d[:] 478 } 479 480 // WithSignature adds an externally created signature to the operation. 481 // No signature validation is performed, it is assumed the signature is correct. 482 func (o *Op) WithSignature(sig mavryk.Signature) *Op { 483 o.Signature = sig 484 return o 485 } 486 487 // Sign signs the operation using provided private key. If a valid signature 488 // already exists this function is a noop. Fails when either branch or contents 489 // are empty. 490 func (o *Op) Sign(key mavryk.PrivateKey) error { 491 if !o.Branch.IsValid() { 492 return fmt.Errorf("tezos: missing branch") 493 } 494 if len(o.Contents) == 0 { 495 return fmt.Errorf("tezos: empty operation contents") 496 } 497 sig, err := key.Sign(o.Digest()) 498 if err != nil { 499 return err 500 } 501 o.Signature = sig 502 return nil 503 } 504 505 // Hash calculates the operation hash. For the hash to be correct, the operation 506 // must contain a valid signature. 507 func (o *Op) Hash() (h mavryk.OpHash) { 508 d := mavryk.Digest(o.Bytes()) 509 copy(h[:], d[:]) 510 return 511 } 512 513 // MarshalJSON conditionally marshals the JSON format of the operation with checks 514 // for required fields. Omits signature for unsigned ops so that the encoding is 515 // compatible with remote forging. 516 func (o *Op) MarshalJSON() ([]byte, error) { 517 buf := bytes.NewBuffer(nil) 518 buf.WriteByte('{') 519 buf.WriteString(`"branch":`) 520 buf.WriteString(strconv.Quote(o.Branch.String())) 521 buf.WriteString(`,"contents":[`) 522 for i, op := range o.Contents { 523 if i > 0 { 524 buf.WriteByte(',') 525 } 526 if b, err := op.MarshalJSON(); err != nil { 527 return nil, err 528 } else { 529 buf.Write(b) 530 } 531 } 532 buf.WriteByte(']') 533 sig := o.Signature 534 if len(o.Contents) > 0 && o.Contents[0].Kind() == mavryk.OpTypeEndorsementWithSlot { 535 // no signature 536 sig = mavryk.InvalidSignature 537 } 538 if sig.IsValid() { 539 buf.WriteString(`,"signature":`) 540 buf.WriteString(strconv.Quote(sig.String())) 541 } 542 buf.WriteByte('}') 543 return buf.Bytes(), nil 544 } 545 546 // DecodeOp decodes an operation from its binary representation. The encoded 547 // data may or may not contain a signature. 548 func DecodeOp(data []byte) (*Op, error) { 549 // check for shortest message 550 if len(data) < 32+5 { 551 return nil, io.ErrShortBuffer 552 } 553 554 // decode 555 buf := bytes.NewBuffer(data) 556 o := &Op{ 557 Contents: make([]Operation, 0), 558 Params: mavryk.DefaultParams, 559 } 560 if err := o.Branch.UnmarshalBinary(buf.Next(32)); err != nil { 561 return nil, err 562 } 563 for buf.Len() > 0 { 564 var op Operation 565 tag, _ := buf.ReadByte() 566 buf.UnreadByte() 567 switch mavryk.ParseOpTag(tag) { 568 case mavryk.OpTypeEndorsement: 569 if o.Params.OperationTagsVersion < 2 { 570 op = new(Endorsement) 571 } else { 572 op = new(TenderbakeEndorsement) 573 } 574 case mavryk.OpTypePreattestation: 575 op = new(TenderbakePreendorsement) 576 case mavryk.OpTypeEndorsementWithSlot: 577 op = new(EndorsementWithSlot) 578 case mavryk.OpTypeSeedNonceRevelation: 579 op = new(SeedNonceRevelation) 580 case mavryk.OpTypeDoubleAttestationEvidence: 581 if o.Params.OperationTagsVersion < 2 { 582 op = new(DoubleEndorsementEvidence) 583 } else { 584 op = new(TenderbakeDoubleEndorsementEvidence) 585 } 586 case mavryk.OpTypeDoublePreattestationEvidence: 587 op = new(TenderbakeDoublePreendorsementEvidence) 588 case mavryk.OpTypeDoubleBakingEvidence: 589 op = new(DoubleBakingEvidence) 590 case mavryk.OpTypeActivateAccount: 591 op = new(ActivateAccount) 592 case mavryk.OpTypeProposals: 593 op = new(Proposals) 594 case mavryk.OpTypeBallot: 595 op = new(Ballot) 596 case mavryk.OpTypeReveal: 597 op = new(Reveal) 598 case mavryk.OpTypeTransaction: 599 op = new(Transaction) 600 case mavryk.OpTypeOrigination: 601 op = new(Origination) 602 case mavryk.OpTypeDelegation: 603 op = new(Delegation) 604 case mavryk.OpTypeFailingNoop: 605 op = new(FailingNoop) 606 case mavryk.OpTypeRegisterConstant: 607 op = new(RegisterGlobalConstant) 608 case mavryk.OpTypeSetDepositsLimit: 609 op = new(SetDepositsLimit) 610 case mavryk.OpTypeTransferTicket: 611 op = new(TransferTicket) 612 case mavryk.OpTypeVdfRevelation: 613 op = new(VdfRevelation) 614 case mavryk.OpTypeIncreasePaidStorage: 615 op = new(IncreasePaidStorage) 616 case mavryk.OpTypeDrainDelegate: 617 op = new(DrainDelegate) 618 case mavryk.OpTypeUpdateConsensusKey: 619 op = new(UpdateConsensusKey) 620 case mavryk.OpTypeSmartRollupOriginate: 621 op = new(SmartRollupOriginate) 622 case mavryk.OpTypeSmartRollupAddMessages: 623 op = new(SmartRollupAddMessages) 624 case mavryk.OpTypeSmartRollupCement: 625 op = new(SmartRollupCement) 626 case mavryk.OpTypeSmartRollupPublish: 627 op = new(SmartRollupPublish) 628 // TODO 629 // case mavryk.OpTypeSmartRollupRefute: 630 // op = new(SmartRollupRefute) 631 case mavryk.OpTypeSmartRollupTimeout: 632 op = new(SmartRollupTimeout) 633 case mavryk.OpTypeSmartRollupExecuteOutboxMessage: 634 op = new(SmartRollupExecuteOutboxMessage) 635 case mavryk.OpTypeSmartRollupRecoverBond: 636 op = new(SmartRollupRecoverBond) 637 case mavryk.OpTypeDalPublishCommitment: 638 op = new(DalPublishCommitment) 639 640 default: 641 // stop if rest looks like a signature 642 // FIXME: BLS sigs are 96 bytes, but accepting this here will 643 // collide with detecting valid operation types in a batch 644 if buf.Len() == 64 { 645 break 646 } 647 return nil, fmt.Errorf("tezos: unsupported operation tag %d", tag) 648 } 649 if err := op.DecodeBuffer(buf, mavryk.DefaultParams); err != nil { 650 return nil, err 651 } 652 o.Contents = append(o.Contents, op) 653 } 654 655 if buf.Len() > 0 { 656 // FIXME: BLS sigs are 96 byte 657 if err := o.Signature.UnmarshalBinary(buf.Next(64)); err != nil { 658 return nil, err 659 } 660 } 661 return o, nil 662 }