github.com/mavryk-network/mvgo@v1.19.9/internal/compose/alpha/task/double_bake.go (about)

     1  // Copyright (c) 2023 Blockwatch Data Inc.
     2  // Author: alex@blockwatch.cc, abdul@blockwatch.cc
     3  package task
     4  
     5  import (
     6  	"bytes"
     7  	"crypto/rand"
     8  	"encoding/binary"
     9  	"fmt"
    10  
    11  	"github.com/mavryk-network/mvgo/codec"
    12  	"github.com/mavryk-network/mvgo/internal/compose"
    13  	"github.com/mavryk-network/mvgo/internal/compose/alpha"
    14  	"github.com/mavryk-network/mvgo/mavryk"
    15  	"github.com/mavryk-network/mvgo/rpc"
    16  	"github.com/mavryk-network/mvgo/signer"
    17  
    18  	"github.com/pkg/errors"
    19  )
    20  
    21  var _ alpha.TaskBuilder = (*DoubleBakeTask)(nil)
    22  
    23  func init() {
    24  	alpha.RegisterTask("double_bake", NewDoubleBakeTask)
    25  }
    26  
    27  type DoubleBakeTask struct {
    28  	TargetTask
    29  	BakerKey mavryk.PrivateKey
    30  }
    31  
    32  func NewDoubleBakeTask() alpha.TaskBuilder {
    33  	return &DoubleBakeTask{}
    34  }
    35  
    36  func (t *DoubleBakeTask) Type() string {
    37  	return "double_bake"
    38  }
    39  
    40  func (t *DoubleBakeTask) Build(ctx compose.Context, task alpha.Task) (*codec.Op, *rpc.CallOptions, error) {
    41  	if err := t.parse(ctx, task); err != nil {
    42  		return nil, nil, errors.Wrap(err, "parse")
    43  	}
    44  
    45  	// wait for baking rights to appear for the next block, the block must
    46  	// actually be baked by this baker, so we look for round 0 only
    47  	// this means to test this on sandbox the source/baker must be a
    48  	// sandbox baker
    49  	ctx.Log.Infof("Waiting for round-0 baking rights for %s...", t.BakerKey.Address())
    50  	var (
    51  		round int
    52  		head  *rpc.BlockHeaderLogEntry
    53  	)
    54  	done := make(chan struct{})
    55  	_, err := ctx.SubscribeBlocks(func(h *rpc.BlockHeaderLogEntry, _ int64, _ int, _ int, _ bool) bool {
    56  		r, ok, err := t.fetchBakingRights(ctx, t.BakerKey.Address(), h.Hash)
    57  		if err != nil {
    58  			ctx.Log.Warnf("fetch baking rights: %v", err)
    59  		} else if ok && r == 0 {
    60  			head = h
    61  			round = r
    62  			close(done)
    63  			return true
    64  		}
    65  		ctx.Log.Debugf("No round-0 rights in block %d", h.Level)
    66  		return false
    67  	})
    68  	if err != nil {
    69  		return nil, nil, err
    70  	}
    71  	select {
    72  	case <-done:
    73  		ctx.Log.Infof("Found baking rights in block=%d round=%d", head.Level+1, round)
    74  	case <-ctx.Done():
    75  		return nil, nil, ctx.Err()
    76  	}
    77  
    78  	// produce random block headers (for NEXT block!)
    79  	b1 := t.randomBlock(ctx, head, round)
    80  	b2 := t.randomBlock(ctx, head, round)
    81  
    82  	// order blocks by hash
    83  	if bytes.Compare(b1.Hash().Bytes(), b2.Hash().Bytes()) > 0 {
    84  		b1, b2 = b2, b1
    85  	}
    86  
    87  	// pack into evidence op
    88  	op := codec.NewOp().
    89  		WithSource(t.Source). // required for compose simulation mode
    90  		WithContents(&codec.DoubleBakingEvidence{
    91  			Bh1: b1,
    92  			Bh2: b2,
    93  		})
    94  
    95  	// wait one block for sending
    96  	ctx.Log.Debug("Wait next block")
    97  	if err := ctx.WaitNumBlocks(1); err != nil {
    98  		return nil, nil, err
    99  	}
   100  
   101  	opts := rpc.NewCallOptions()
   102  	opts.Signer = signer.NewFromKey(t.Key)
   103  	opts.IgnoreLimits = true
   104  
   105  	return op, opts, nil
   106  }
   107  
   108  func (t *DoubleBakeTask) Validate(ctx compose.Context, task alpha.Task) error {
   109  	return t.parse(ctx, task)
   110  }
   111  
   112  func (t *DoubleBakeTask) parse(ctx compose.Context, task alpha.Task) (err error) {
   113  	if err = t.TargetTask.parse(ctx, task); err != nil {
   114  		return err
   115  	}
   116  	if t.BakerKey, err = ctx.ResolvePrivateKey(task.Destination); err != nil {
   117  		return errors.Wrap(err, "destination key")
   118  	}
   119  	return
   120  }
   121  
   122  func (t *DoubleBakeTask) randomBlock(ctx compose.Context, head *rpc.BlockHeaderLogEntry, round int) codec.BlockHeader {
   123  	// generate a random block header
   124  	h := codec.BlockHeader{
   125  		Level:            int32(head.Level + 1),
   126  		Proto:            byte(head.Proto),
   127  		Predecessor:      head.Hash,
   128  		Timestamp:        head.Timestamp,
   129  		ValidationPass:   byte(head.ValidationPass),
   130  		Fitness:          head.Fitness,
   131  		ProofOfWorkNonce: head.Pow(),
   132  		PayloadRound:     round,
   133  		OperationsHash:   head.OperationsHash,
   134  		Context:          head.Context,
   135  		LbVote:           mavryk.FeatureVotePass,
   136  		AiVote:           mavryk.FeatureVotePass,
   137  	}
   138  	// adjust fitness
   139  	f2 := make([]byte, len(h.Fitness[1]))
   140  	binary.BigEndian.PutUint32(f2, uint32(head.Level+1))
   141  	h.Fitness[1] = f2
   142  
   143  	// randomize payload hash
   144  	rand.Read(h.PayloadHash[:])
   145  	h.WithChainId(ctx.Params().ChainId) // Tenderbake block signing needs chain id
   146  
   147  	// sign the block
   148  	if err := h.Sign(t.BakerKey); err != nil {
   149  		ctx.Log.Errorf("signing random block: %v", err)
   150  	}
   151  	return h
   152  }
   153  
   154  func (t *DoubleBakeTask) fetchBakingRights(ctx compose.Context, addr mavryk.Address, id mavryk.BlockHash) (int, bool, error) {
   155  	u := fmt.Sprintf("/chains/main/blocks/%s/helpers/baking_rights?delegate=%s&max_round=64", id, addr)
   156  	var rights []struct {
   157  		Round int `json:"round"`
   158  	}
   159  	if err := ctx.Fetch(u, &rights); err != nil {
   160  		return 0, false, err
   161  	}
   162  	if len(rights) == 0 {
   163  		return 0, false, nil
   164  	}
   165  	return rights[0].Round, true, nil
   166  }