github.com/ethereum-optimism/optimism@v1.7.2/op-node/rollup/derive/engine_update.go (about) 1 package derive 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 8 "github.com/ethereum/go-ethereum/core/types" 9 "github.com/ethereum/go-ethereum/log" 10 11 "github.com/ethereum-optimism/optimism/op-node/rollup/async" 12 "github.com/ethereum-optimism/optimism/op-node/rollup/conductor" 13 "github.com/ethereum-optimism/optimism/op-service/eth" 14 ) 15 16 // isDepositTx checks an opaqueTx to determine if it is a Deposit Transaction 17 // It has to return an error in the case the transaction is empty 18 func isDepositTx(opaqueTx eth.Data) (bool, error) { 19 if len(opaqueTx) == 0 { 20 return false, errors.New("empty transaction") 21 } 22 return opaqueTx[0] == types.DepositTxType, nil 23 } 24 25 // lastDeposit finds the index of last deposit at the start of the transactions. 26 // It walks the transactions from the start until it finds a non-deposit tx. 27 // An error is returned if any looked at transaction cannot be decoded 28 func lastDeposit(txns []eth.Data) (int, error) { 29 var lastDeposit int 30 for i, tx := range txns { 31 deposit, err := isDepositTx(tx) 32 if err != nil { 33 return 0, fmt.Errorf("invalid transaction at idx %d", i) 34 } 35 if deposit { 36 lastDeposit = i 37 } else { 38 break 39 } 40 } 41 return lastDeposit, nil 42 } 43 44 func sanityCheckPayload(payload *eth.ExecutionPayload) error { 45 // Sanity check payload before inserting it 46 if len(payload.Transactions) == 0 { 47 return errors.New("no transactions in returned payload") 48 } 49 if payload.Transactions[0][0] != types.DepositTxType { 50 return fmt.Errorf("first transaction was not deposit tx. Got %v", payload.Transactions[0][0]) 51 } 52 // Ensure that the deposits are first 53 lastDeposit, err := lastDeposit(payload.Transactions) 54 if err != nil { 55 return fmt.Errorf("failed to find last deposit: %w", err) 56 } 57 // Ensure no deposits after last deposit 58 for i := lastDeposit + 1; i < len(payload.Transactions); i++ { 59 tx := payload.Transactions[i] 60 deposit, err := isDepositTx(tx) 61 if err != nil { 62 return fmt.Errorf("failed to decode transaction idx %d: %w", i, err) 63 } 64 if deposit { 65 return fmt.Errorf("deposit tx (%d) after other tx in l2 block with prev deposit at idx %d", i, lastDeposit) 66 } 67 } 68 return nil 69 } 70 71 type BlockInsertionErrType uint 72 73 const ( 74 // BlockInsertOK indicates that the payload was successfully executed and appended to the canonical chain. 75 BlockInsertOK BlockInsertionErrType = iota 76 // BlockInsertTemporaryErr indicates that the insertion failed but may succeed at a later time without changes to the payload. 77 BlockInsertTemporaryErr 78 // BlockInsertPrestateErr indicates that the pre-state to insert the payload could not be prepared, e.g. due to missing chain data. 79 BlockInsertPrestateErr 80 // BlockInsertPayloadErr indicates that the payload was invalid and cannot become canonical. 81 BlockInsertPayloadErr 82 ) 83 84 // startPayload starts an execution payload building process in the provided Engine, with the given attributes. 85 // The severity of the error is distinguished to determine whether the same payload attributes may be re-attempted later. 86 func startPayload(ctx context.Context, eng ExecEngine, fc eth.ForkchoiceState, attrs *eth.PayloadAttributes) (id eth.PayloadID, errType BlockInsertionErrType, err error) { 87 fcRes, err := eng.ForkchoiceUpdate(ctx, &fc, attrs) 88 if err != nil { 89 var inputErr eth.InputError 90 if errors.As(err, &inputErr) { 91 switch inputErr.Code { 92 case eth.InvalidForkchoiceState: 93 return eth.PayloadID{}, BlockInsertPrestateErr, fmt.Errorf("pre-block-creation forkchoice update was inconsistent with engine, need reset to resolve: %w", inputErr.Unwrap()) 94 case eth.InvalidPayloadAttributes: 95 return eth.PayloadID{}, BlockInsertPayloadErr, fmt.Errorf("payload attributes are not valid, cannot build block: %w", inputErr.Unwrap()) 96 default: 97 return eth.PayloadID{}, BlockInsertPrestateErr, fmt.Errorf("unexpected error code in forkchoice-updated response: %w", err) 98 } 99 } else { 100 return eth.PayloadID{}, BlockInsertTemporaryErr, fmt.Errorf("failed to create new block via forkchoice: %w", err) 101 } 102 } 103 104 switch fcRes.PayloadStatus.Status { 105 // TODO(proto): snap sync - specify explicit different error type if node is syncing 106 case eth.ExecutionInvalid, eth.ExecutionInvalidBlockHash: 107 return eth.PayloadID{}, BlockInsertPayloadErr, eth.ForkchoiceUpdateErr(fcRes.PayloadStatus) 108 case eth.ExecutionValid: 109 id := fcRes.PayloadID 110 if id == nil { 111 return eth.PayloadID{}, BlockInsertTemporaryErr, errors.New("nil id in forkchoice result when expecting a valid ID") 112 } 113 return *id, BlockInsertOK, nil 114 default: 115 return eth.PayloadID{}, BlockInsertTemporaryErr, eth.ForkchoiceUpdateErr(fcRes.PayloadStatus) 116 } 117 } 118 119 // confirmPayload ends an execution payload building process in the provided Engine, and persists the payload as the canonical head. 120 // If updateSafe is true, then the payload will also be recognized as safe-head at the same time. 121 // The severity of the error is distinguished to determine whether the payload was valid and can become canonical. 122 func confirmPayload( 123 ctx context.Context, 124 log log.Logger, 125 eng ExecEngine, 126 fc eth.ForkchoiceState, 127 payloadInfo eth.PayloadInfo, 128 updateSafe bool, 129 agossip async.AsyncGossiper, 130 sequencerConductor conductor.SequencerConductor, 131 ) (out *eth.ExecutionPayloadEnvelope, errTyp BlockInsertionErrType, err error) { 132 var envelope *eth.ExecutionPayloadEnvelope 133 // if the payload is available from the async gossiper, it means it was not yet imported, so we reuse it 134 if cached := agossip.Get(); cached != nil { 135 envelope = cached 136 // log a limited amount of information about the reused payload, more detailed logging happens later down 137 log.Debug("found uninserted payload from async gossiper, reusing it and bypassing engine", 138 "hash", envelope.ExecutionPayload.BlockHash, 139 "number", uint64(envelope.ExecutionPayload.BlockNumber), 140 "parent", envelope.ExecutionPayload.ParentHash, 141 "txs", len(envelope.ExecutionPayload.Transactions)) 142 } else { 143 envelope, err = eng.GetPayload(ctx, payloadInfo) 144 } 145 if err != nil { 146 // even if it is an input-error (unknown payload ID), it is temporary, since we will re-attempt the full payload building, not just the retrieval of the payload. 147 return nil, BlockInsertTemporaryErr, fmt.Errorf("failed to get execution payload: %w", err) 148 } 149 payload := envelope.ExecutionPayload 150 if err := sanityCheckPayload(payload); err != nil { 151 return nil, BlockInsertPayloadErr, err 152 } 153 if err := sequencerConductor.CommitUnsafePayload(ctx, envelope); err != nil { 154 return nil, BlockInsertTemporaryErr, fmt.Errorf("failed to commit unsafe payload to conductor: %w", err) 155 } 156 // begin gossiping as soon as possible 157 // agossip.Clear() will be called later if an non-temporary error is found, or if the payload is successfully inserted 158 agossip.Gossip(envelope) 159 160 status, err := eng.NewPayload(ctx, payload, envelope.ParentBeaconBlockRoot) 161 if err != nil { 162 return nil, BlockInsertTemporaryErr, fmt.Errorf("failed to insert execution payload: %w", err) 163 } 164 if status.Status == eth.ExecutionInvalid || status.Status == eth.ExecutionInvalidBlockHash { 165 agossip.Clear() 166 return nil, BlockInsertPayloadErr, eth.NewPayloadErr(payload, status) 167 } 168 if status.Status != eth.ExecutionValid { 169 return nil, BlockInsertTemporaryErr, eth.NewPayloadErr(payload, status) 170 } 171 172 fc.HeadBlockHash = payload.BlockHash 173 if updateSafe { 174 fc.SafeBlockHash = payload.BlockHash 175 } 176 fcRes, err := eng.ForkchoiceUpdate(ctx, &fc, nil) 177 if err != nil { 178 var inputErr eth.InputError 179 if errors.As(err, &inputErr) { 180 switch inputErr.Code { 181 case eth.InvalidForkchoiceState: 182 // if we succeed to update the forkchoice pre-payload, but fail post-payload, then it is a payload error 183 agossip.Clear() 184 return nil, BlockInsertPayloadErr, fmt.Errorf("post-block-creation forkchoice update was inconsistent with engine, need reset to resolve: %w", inputErr.Unwrap()) 185 default: 186 agossip.Clear() 187 return nil, BlockInsertPrestateErr, fmt.Errorf("unexpected error code in forkchoice-updated response: %w", err) 188 } 189 } else { 190 return nil, BlockInsertTemporaryErr, fmt.Errorf("failed to make the new L2 block canonical via forkchoice: %w", err) 191 } 192 } 193 agossip.Clear() 194 if fcRes.PayloadStatus.Status != eth.ExecutionValid { 195 return nil, BlockInsertPayloadErr, eth.ForkchoiceUpdateErr(fcRes.PayloadStatus) 196 } 197 log.Info("inserted block", "hash", payload.BlockHash, "number", uint64(payload.BlockNumber), 198 "state_root", payload.StateRoot, "timestamp", uint64(payload.Timestamp), "parent", payload.ParentHash, 199 "prev_randao", payload.PrevRandao, "fee_recipient", payload.FeeRecipient, 200 "txs", len(payload.Transactions), "update_safe", updateSafe) 201 return envelope, BlockInsertOK, nil 202 }