diff --git a/builder/builder.go b/builder/builder.go index 44201bcdeb..4d23eaa475 100644 --- a/builder/builder.go +++ b/builder/builder.go @@ -15,6 +15,8 @@ import ( "github.com/attestantio/go-eth2-client/spec/bellatrix" "github.com/attestantio/go-eth2-client/spec/capella" "github.com/attestantio/go-eth2-client/spec/phase0" + utilbellatrix "github.com/attestantio/go-eth2-client/util/bellatrix" + utilcapella "github.com/attestantio/go-eth2-client/util/capella" "github.com/ethereum/go-ethereum/beacon/engine" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" @@ -48,6 +50,7 @@ type ValidatorData struct { type IRelay interface { SubmitBlock(msg *bellatrixapi.SubmitBlockRequest, vd ValidatorData) error SubmitBlockCapella(msg *capellaapi.SubmitBlockRequest, vd ValidatorData) error + SubmitV2BlockCapella(msg *common.SubmitBlockRequestV2Optimistic, vd ValidatorData) error GetValidatorForSlot(nextSlot uint64) (ValidatorData, error) Config() RelayConfig Start() error @@ -306,30 +309,69 @@ func (b *Builder) submitCapellaBlock(block *types.Block, blockValue *big.Int, or Value: value, } + /* --- makes a bad block hash. + log.Warn(fmt.Sprintf("*** current hash: %v, %v\n", blockBidMsg.BlockHash.String(), payload.BlockHash.String())) + modifiedHash := "0x0000" + blockBidMsg.BlockHash.String()[6:] + if err := payload.BlockHash.UnmarshalText([]byte(modifiedHash)); err != nil { + log.Error(fmt.Sprintf("unable to modify msg execution payload: %v", err)) + } + if err := blockBidMsg.BlockHash.UnmarshalText([]byte(modifiedHash)); err != nil { + log.Error(fmt.Sprintf("unable to modify msg block hash: %v", err)) + } + log.Warn(fmt.Sprintf("*** new hash: %v, %v\n", blockBidMsg.BlockHash.String(), payload.BlockHash.String())) + */ + signature, err := ssz.SignMessage(&blockBidMsg, b.builderSigningDomain, b.builderSecretKey) if err != nil { log.Error("could not sign builder bid", "err", err) return err } - blockSubmitReq := capellaapi.SubmitBlockRequest{ - Signature: signature, - Message: &blockBidMsg, - ExecutionPayload: payload, + transactions := utilbellatrix.ExecutionPayloadTransactions{Transactions: payload.Transactions} + transactionsRoot, err := transactions.HashTreeRoot() + if err != nil { + log.Error("could not calculate transactions root", "err", err) + return err } - if b.dryRun { - err = b.validator.ValidateBuilderSubmissionV2(&blockvalidation.BuilderBlockValidationRequestV2{SubmitBlockRequest: blockSubmitReq, RegisteredGasLimit: vd.GasLimit}) - if err != nil { - log.Error("could not validate block for capella", "err", err) - } - } else { - go b.ds.ConsumeBuiltBlock(block, blockValue, ordersClosedAt, sealedAt, commitedBundles, allBundles, usedSbundles, &blockBidMsg) - err = b.relay.SubmitBlockCapella(&blockSubmitReq, vd) - if err != nil { - log.Error("could not submit capella block", "err", err, "#commitedBundles", len(commitedBundles)) - return err - } + withdrawals := utilcapella.ExecutionPayloadWithdrawals{Withdrawals: payload.Withdrawals} + withdrawalsRoot, err := withdrawals.HashTreeRoot() + if err != nil { + log.Error("could not calculate withdrawals root", "err", err) + return err + } + + eph := capella.ExecutionPayloadHeader{ + ParentHash: payload.ParentHash, + FeeRecipient: payload.FeeRecipient, + StateRoot: payload.StateRoot, + ReceiptsRoot: payload.ReceiptsRoot, + LogsBloom: payload.LogsBloom, + PrevRandao: payload.PrevRandao, + BlockNumber: payload.BlockNumber, + GasLimit: payload.GasLimit, + GasUsed: payload.GasUsed, + Timestamp: payload.Timestamp, + ExtraData: payload.ExtraData, + BaseFeePerGas: payload.BaseFeePerGas, + BlockHash: payload.BlockHash, + TransactionsRoot: transactionsRoot, + WithdrawalsRoot: withdrawalsRoot, + } + + blockSubmitReq := common.SubmitBlockRequestV2Optimistic{ + Message: &blockBidMsg, + ExecutionPayloadHeader: &eph, + Signature: signature, + Transactions: payload.Transactions, + Withdrawals: payload.Withdrawals, + } + + go b.ds.ConsumeBuiltBlock(block, blockValue, ordersClosedAt, sealedAt, commitedBundles, allBundles, usedSbundles, &blockBidMsg) + err = b.relay.SubmitV2BlockCapella(&blockSubmitReq, vd) + if err != nil { + log.Error("could not submit capella block", "err", err, "#commitedBundles", len(commitedBundles)) + return err } log.Info("submitted capella block", "slot", blockBidMsg.Slot, "value", blockBidMsg.Value.String(), "parent", blockBidMsg.ParentHash, "hash", block.Hash(), "#commitedBundles", len(commitedBundles)) diff --git a/builder/local_relay.go b/builder/local_relay.go index 9c2abef78c..e8574e4108 100644 --- a/builder/local_relay.go +++ b/builder/local_relay.go @@ -22,6 +22,7 @@ import ( "github.com/attestantio/go-eth2-client/spec/bellatrix" "github.com/attestantio/go-eth2-client/spec/phase0" bellatrixutil "github.com/attestantio/go-eth2-client/util/bellatrix" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/log" "github.com/flashbots/go-boost-utils/bls" @@ -121,6 +122,10 @@ func (r *LocalRelay) SubmitBlockCapella(msg *capellaapi.SubmitBlockRequest, _ Va return r.submitBlockCapella(msg) } +func (r *LocalRelay) SubmitV2BlockCapella(msg *common.SubmitBlockRequestV2Optimistic, vd ValidatorData) error { + return fmt.Errorf("capella v2 not supported on local relay") +} + func (r *LocalRelay) Config() RelayConfig { // local relay does not need config as it is submitting to its own internal endpoint return RelayConfig{} diff --git a/builder/relay.go b/builder/relay.go index a28fe1e71c..e05a5672c9 100644 --- a/builder/relay.go +++ b/builder/relay.go @@ -11,6 +11,7 @@ import ( "github.com/attestantio/go-builder-client/api/bellatrix" "github.com/attestantio/go-builder-client/api/capella" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/log" "github.com/flashbots/go-boost-utils/utils" ) @@ -193,6 +194,30 @@ func (r *RemoteRelay) SubmitBlockCapella(msg *capella.SubmitBlockRequest, _ Vali return nil } +func (r *RemoteRelay) SubmitV2BlockCapella(msg *common.SubmitBlockRequestV2Optimistic, vd ValidatorData) error { + log.Info("submitting block to remote relay", "endpoint", r.config.Endpoint) + + endpoint := r.config.Endpoint + "/relay/v2/builder/blocks" + if r.cancellationsEnabled { + endpoint = endpoint + "?cancellations=true" + } + + bodyBytes, err := msg.MarshalSSZ() + if err != nil { + return fmt.Errorf("error marshaling ssz: %w", err) + } + log.Debug("submitting block to remote relay", "endpoint", r.config.Endpoint) + code, err := SendSSZRequest(context.TODO(), *http.DefaultClient, http.MethodPost, endpoint, bodyBytes, r.config.GzipEnabled) + if err != nil { + return fmt.Errorf("error sending http request to relay %s. err: %w", r.config.Endpoint, err) + } + if code > 299 { + return fmt.Errorf("non-ok response code %d from relay %s", code, r.config.Endpoint) + } + + return nil +} + func (r *RemoteRelay) getSlotValidatorMapFromRelay() (map[uint64]ValidatorData, error) { var dst GetValidatorRelayResponse code, err := SendHTTPRequest(context.TODO(), *http.DefaultClient, http.MethodGet, r.config.Endpoint+"/relay/v1/builder/validators", nil, &dst) diff --git a/builder/relay_aggregator.go b/builder/relay_aggregator.go index 12f6f62c13..3b1dc1390f 100644 --- a/builder/relay_aggregator.go +++ b/builder/relay_aggregator.go @@ -7,6 +7,7 @@ import ( "github.com/attestantio/go-builder-client/api/bellatrix" "github.com/attestantio/go-builder-client/api/capella" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/log" ) @@ -81,6 +82,26 @@ func (r *RemoteRelayAggregator) SubmitBlockCapella(msg *capella.SubmitBlockReque return nil } +func (r *RemoteRelayAggregator) SubmitV2BlockCapella(msg *common.SubmitBlockRequestV2Optimistic, registration ValidatorData) error { + r.registrationsCacheLock.RLock() + defer r.registrationsCacheLock.RUnlock() + + relays, found := r.registrationsCache[registration] + if !found { + return fmt.Errorf("no relays for registration %s", registration.Pubkey) + } + for _, relay := range relays { + go func(relay IRelay) { + err := relay.SubmitV2BlockCapella(msg, registration) + if err != nil { + log.Error("could not submit block", "err", err) + } + }(relay) + } + + return nil +} + type RelayValidatorRegistration struct { vd ValidatorData relayI int // index into relays array to preserve relative order diff --git a/builder/relay_aggregator_test.go b/builder/relay_aggregator_test.go index d46533e49a..a470dee294 100644 --- a/builder/relay_aggregator_test.go +++ b/builder/relay_aggregator_test.go @@ -7,6 +7,7 @@ import ( "github.com/attestantio/go-builder-client/api/bellatrix" "github.com/attestantio/go-builder-client/api/capella" + "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/require" ) @@ -68,6 +69,10 @@ func (r *testRelay) SubmitBlockCapella(msg *capella.SubmitBlockRequest, registra return r.sbError } +func (r *testRelay) SubmitV2BlockCapella(msg *common.SubmitBlockRequestV2Optimistic, vd ValidatorData) error { + return r.sbError +} + func (r *testRelay) GetValidatorForSlot(nextSlot uint64) (ValidatorData, error) { r.requestedSlot = nextSlot return r.gvsVd, r.gvsErr diff --git a/common/types.go b/common/types.go index 218ca0be4c..dbeffef8a6 100644 --- a/common/types.go +++ b/common/types.go @@ -28,7 +28,12 @@ import ( "reflect" "strings" + apiv1 "github.com/attestantio/go-builder-client/api/v1" + consensusbellatrix "github.com/attestantio/go-eth2-client/spec/bellatrix" + consensuscapella "github.com/attestantio/go-eth2-client/spec/capella" + "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/ethereum/go-ethereum/common/hexutil" + ssz "github.com/ferranbt/fastssz" "golang.org/x/crypto/sha3" ) @@ -429,3 +434,270 @@ func (ma *MixedcaseAddress) ValidChecksum() bool { func (ma *MixedcaseAddress) Original() string { return ma.original } + +/* +SubmitBlockRequestV2Optimistic is the v2 request from the builder to submit +a block. The message must be SSZ encoded. The first three fields are at most +944 bytes, which fit into a single 1500 MTU ethernet packet. The +`UnmarshalSSZHeaderOnly` function just parses the first three fields, +which is sufficient data to set the bid of the builder. The `Transactions` +and `Withdrawals` fields are required to construct the full SignedBeaconBlock +and are parsed asynchronously. + +Header only layout: +[000-236) = Message (236 bytes) +[236-240) = offset1 ( 4 bytes) +[240-336) = Signature ( 96 bytes) +[336-340) = offset2 ( 4 bytes) +[340-344) = offset3 ( 4 bytes) +[344-944) = EPH (600 bytes) +*/ +type SubmitBlockRequestV2Optimistic struct { + Message *apiv1.BidTrace + ExecutionPayloadHeader *consensuscapella.ExecutionPayloadHeader + Signature phase0.BLSSignature `ssz-size:"96"` + Transactions []consensusbellatrix.Transaction `ssz-max:"1048576,1073741824" ssz-size:"?,?"` + Withdrawals []*consensuscapella.Withdrawal `ssz-max:"16"` +} + +// MarshalSSZ ssz marshals the SubmitBlockRequestV2Optimistic object +func (s *SubmitBlockRequestV2Optimistic) MarshalSSZ() ([]byte, error) { + return ssz.MarshalSSZ(s) +} + +// UnmarshalSSZ ssz unmarshals the SubmitBlockRequestV2Optimistic object +func (s *SubmitBlockRequestV2Optimistic) UnmarshalSSZ(buf []byte) error { + var err error + size := uint64(len(buf)) + if size < 344 { + return ssz.ErrSize + } + + tail := buf + var o1, o3, o4 uint64 + + // Field (0) 'Message' + if s.Message == nil { + s.Message = new(apiv1.BidTrace) + } + if err = s.Message.UnmarshalSSZ(buf[0:236]); err != nil { + return err + } + + // Offset (1) 'ExecutionPayloadHeader' + if o1 = ssz.ReadOffset(buf[236:240]); o1 > size { + return ssz.ErrOffset + } + + if o1 < 344 { + return ssz.ErrInvalidVariableOffset + } + + // Field (2) 'Signature' + copy(s.Signature[:], buf[240:336]) + + // Offset (3) 'Transactions' + if o3 = ssz.ReadOffset(buf[336:340]); o3 > size || o1 > o3 { + return ssz.ErrOffset + } + + // Offset (4) 'Withdrawals' + if o4 = ssz.ReadOffset(buf[340:344]); o4 > size || o3 > o4 { + return ssz.ErrOffset + } + + // Field (1) 'ExecutionPayloadHeader' + { + buf = tail[o1:o3] + if s.ExecutionPayloadHeader == nil { + s.ExecutionPayloadHeader = new(consensuscapella.ExecutionPayloadHeader) + } + if err = s.ExecutionPayloadHeader.UnmarshalSSZ(buf); err != nil { + return err + } + } + + // Field (3) 'Transactions' + { + buf = tail[o3:o4] + num, err := ssz.DecodeDynamicLength(buf, 1073741824) + if err != nil { + return err + } + s.Transactions = make([]consensusbellatrix.Transaction, num) + err = ssz.UnmarshalDynamic(buf, num, func(indx int, buf []byte) (err error) { + if len(buf) > 1073741824 { + return ssz.ErrBytesLength + } + if cap(s.Transactions[indx]) == 0 { + s.Transactions[indx] = consensusbellatrix.Transaction(make([]byte, 0, len(buf))) + } + s.Transactions[indx] = append(s.Transactions[indx], buf...) + return nil + }) + if err != nil { + return err + } + } + + // Field (4) 'Withdrawals' + { + buf = tail[o4:] + num, err := ssz.DivideInt2(len(buf), 44, 16) + if err != nil { + return err + } + s.Withdrawals = make([]*consensuscapella.Withdrawal, num) + for ii := 0; ii < num; ii++ { + if s.Withdrawals[ii] == nil { + s.Withdrawals[ii] = new(consensuscapella.Withdrawal) + } + if err = s.Withdrawals[ii].UnmarshalSSZ(buf[ii*44 : (ii+1)*44]); err != nil { + return err + } + } + } + return err +} + +// UnmarshalSSZHeaderOnly ssz unmarshals the first 3 fields of the SubmitBlockRequestV2Optimistic object +func (s *SubmitBlockRequestV2Optimistic) UnmarshalSSZHeaderOnly(buf []byte) error { + var err error + size := uint64(len(buf)) + if size < 344 { + return ssz.ErrSize + } + + tail := buf + var o1, o3 uint64 + + // Field (0) 'Message' + if s.Message == nil { + s.Message = new(apiv1.BidTrace) + } + if err = s.Message.UnmarshalSSZ(buf[0:236]); err != nil { + return err + } + + // Offset (1) 'ExecutionPayloadHeader' + if o1 = ssz.ReadOffset(buf[236:240]); o1 > size { + return ssz.ErrOffset + } + + if o1 < 344 { + return ssz.ErrInvalidVariableOffset + } + + // Field (2) 'Signature' + copy(s.Signature[:], buf[240:336]) + + // Offset (3) 'Transactions' + if o3 = ssz.ReadOffset(buf[336:340]); o3 > size || o1 > o3 { + return ssz.ErrOffset + } + + // Field (1) 'ExecutionPayloadHeader' + { + buf = tail[o1:o3] + if s.ExecutionPayloadHeader == nil { + s.ExecutionPayloadHeader = new(consensuscapella.ExecutionPayloadHeader) + } + if err = s.ExecutionPayloadHeader.UnmarshalSSZ(buf); err != nil { + return err + } + } + return err +} + +// MarshalSSZTo ssz marshals the SubmitBlockRequestV2Optimistic object to a target array +func (s *SubmitBlockRequestV2Optimistic) MarshalSSZTo(buf []byte) (dst []byte, err error) { + dst = buf + offset := int(344) + + // Field (0) 'Message' + if s.Message == nil { + s.Message = new(apiv1.BidTrace) + } + if dst, err = s.Message.MarshalSSZTo(dst); err != nil { + return + } + + // Offset (1) 'ExecutionPayloadHeader' + dst = ssz.WriteOffset(dst, offset) + if s.ExecutionPayloadHeader == nil { + s.ExecutionPayloadHeader = new(consensuscapella.ExecutionPayloadHeader) + } + offset += s.ExecutionPayloadHeader.SizeSSZ() + + // Field (2) 'Signature' + dst = append(dst, s.Signature[:]...) + + // Offset (3) 'Transactions' + dst = ssz.WriteOffset(dst, offset) + for ii := 0; ii < len(s.Transactions); ii++ { + offset += 4 + offset += len(s.Transactions[ii]) + } + + // Offset (4) 'Withdrawals' + dst = ssz.WriteOffset(dst, offset) + + // Field (1) 'ExecutionPayloadHeader' + if dst, err = s.ExecutionPayloadHeader.MarshalSSZTo(dst); err != nil { + return + } + + // Field (3) 'Transactions' + if size := len(s.Transactions); size > 1073741824 { + err = ssz.ErrListTooBigFn("SubmitBlockRequestV2Optimistic.Transactions", size, 1073741824) + return + } + { + offset = 4 * len(s.Transactions) + for ii := 0; ii < len(s.Transactions); ii++ { + dst = ssz.WriteOffset(dst, offset) + offset += len(s.Transactions[ii]) + } + } + for ii := 0; ii < len(s.Transactions); ii++ { + if size := len(s.Transactions[ii]); size > 1073741824 { + err = ssz.ErrBytesLengthFn("SubmitBlockRequestV2Optimistic.Transactions[ii]", size, 1073741824) + return + } + dst = append(dst, s.Transactions[ii]...) + } + + // Field (4) 'Withdrawals' + if size := len(s.Withdrawals); size > 16 { + err = ssz.ErrListTooBigFn("SubmitBlockRequestV2Optimistic.Withdrawals", size, 16) + return + } + for ii := 0; ii < len(s.Withdrawals); ii++ { + if dst, err = s.Withdrawals[ii].MarshalSSZTo(dst); err != nil { + return + } + } + return dst, nil +} + +// SizeSSZ returns the ssz encoded size in bytes for the SubmitBlockRequestV2Optimistic object +func (s *SubmitBlockRequestV2Optimistic) SizeSSZ() (size int) { + size = 344 + + // Field (1) 'ExecutionPayloadHeader' + if s.ExecutionPayloadHeader == nil { + s.ExecutionPayloadHeader = new(consensuscapella.ExecutionPayloadHeader) + } + size += s.ExecutionPayloadHeader.SizeSSZ() + + // Field (3) 'Transactions' + for ii := 0; ii < len(s.Transactions); ii++ { + size += 4 + size += len(s.Transactions[ii]) + } + + // Field (4) 'Withdrawals' + size += len(s.Withdrawals) * 44 + + return +}