diff --git a/arbnode/blob_reader.go b/arbnode/blob_reader.go index 673df37b1f..d7560f47e4 100644 --- a/arbnode/blob_reader.go +++ b/arbnode/blob_reader.go @@ -5,24 +5,26 @@ package arbnode import ( "context" - "crypto/sha256" "encoding/json" "fmt" + "io" + "net/http" + "path" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/crypto/kzg4844" "github.com/offchainlabs/nitro/arbutil" - "github.com/offchainlabs/nitro/util/beaconclient" + "github.com/offchainlabs/nitro/util/blobs" "github.com/offchainlabs/nitro/util/pretty" - "github.com/pkg/errors" "github.com/spf13/pflag" ) type BlobClient struct { - bc *beaconclient.Client - ec arbutil.L1Interface + config BlobClientConfig + ec arbutil.L1Interface + httpClient *http.Client // The genesis time time won't change so only request it once. cachedGenesisTime uint64 @@ -40,8 +42,45 @@ func BlobClientAddOptions(prefix string, f *pflag.FlagSet) { f.String(prefix+".beacon-chain-url", DefaultBlobClientConfig.BeaconChainUrl, "Beacon Chain url to use for fetching blobs") } -func NewBlobClient(bc *beaconclient.Client, ec arbutil.L1Interface) *BlobClient { - return &BlobClient{bc: bc, ec: ec} +func NewBlobClient(config BlobClientConfig, ec arbutil.L1Interface) *BlobClient { + return &BlobClient{ + config: config, + ec: ec, + httpClient: &http.Client{}, + } +} + +type fullResult[T any] struct { + Data T `json:"data"` +} + +func beaconRequest[T interface{}](b *BlobClient, ctx context.Context, beaconPath string) (T, error) { + // Unfortunately, methods on a struct can't be generic. + + var empty T + + req, err := http.NewRequestWithContext(ctx, "GET", path.Join(b.config.BeaconChainUrl, beaconPath), http.NoBody) + if err != nil { + return empty, err + } + + resp, err := b.httpClient.Do(req) + if err != nil { + return empty, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return empty, err + } + + var full fullResult[T] + if err := json.Unmarshal(body, &full); err != nil { + return empty, err + } + + return full.Data, nil } // Get all the blobs associated with a particular block. @@ -62,58 +101,48 @@ func (b *BlobClient) GetBlobs(ctx context.Context, blockHash common.Hash, versio return b.blobSidecars(ctx, slot, versionedHashes) } -type blobResponse struct { - Data []blobResponseItem `json:"data"` -} type blobResponseItem struct { - BlockRoot string `json:"block_root"` - Index int `json:"index"` - Slot uint64 `json:"slot"` - BlockParentRoot string `json:"block_parent_root"` - ProposerIndex uint64 `json:"proposer_index"` - Blob string `json:"blob"` - KzgCommitment string `json:"kzg_commitment"` - KzgProof string `json:"kzg_proof"` + BlockRoot string `json:"block_root"` + Index int `json:"index"` + Slot uint64 `json:"slot"` + BlockParentRoot string `json:"block_parent_root"` + ProposerIndex uint64 `json:"proposer_index"` + Blob hexutil.Bytes `json:"blob"` + KzgCommitment hexutil.Bytes `json:"kzg_commitment"` + KzgProof hexutil.Bytes `json:"kzg_proof"` } func (b *BlobClient) blobSidecars(ctx context.Context, slot uint64, versionedHashes []common.Hash) ([]kzg4844.Blob, error) { - body, err := b.bc.Get(ctx, fmt.Sprintf("/eth/v1/beacon/blob_sidecars/%d", slot)) + response, err := beaconRequest[[]blobResponseItem](b, ctx, fmt.Sprintf("/eth/v1/beacon/blob_sidecars/%d", slot)) if err != nil { - return nil, errors.Wrap(err, "error calling beacon client in blobSidecars") + return nil, fmt.Errorf("error calling beacon client in blobSidecars: %w", err) } - br := &blobResponse{} - err = json.Unmarshal(body, br) - if err != nil { - return nil, errors.Wrap(err, "error decoding json response in blobSidecars") - } - - if len(br.Data) == 0 { - return nil, fmt.Errorf("no blobs found for slot %d", slot) + if len(response) < len(versionedHashes) { + return nil, fmt.Errorf("expected at least %d blobs for slot %d but only got %d", len(versionedHashes), slot, len(response)) } - blobs := make([]kzg4844.Blob, len(versionedHashes)) - var totalFound int + output := make([]kzg4844.Blob, len(versionedHashes)) + outputsFound := make([]bool, len(versionedHashes)) - for i := range blobs { - commitmentBytes, err := hexutil.Decode(br.Data[i].KzgCommitment) - if err != nil { - return nil, fmt.Errorf("couldn't decode commitment for slot(%d) at index(%d), commitment(%s)", slot, br.Data[i].Index, pretty.FirstFewChars(br.Data[i].KzgCommitment)) - } + for _, blobItem := range response { var commitment kzg4844.Commitment - copy(commitment[:], commitmentBytes) - versionedHash := kZGToVersionedHash(commitment) + copy(commitment[:], blobItem.KzgCommitment) + versionedHash := blobs.CommitmentToVersionedHash(commitment) // The versioned hashes of the blob commitments are produced in the by HASH_OPCODE_BYTE, // presumably in the order they were added to the tx. The spec is unclear if the blobs // need to be returned in any particular order from the beacon API, so we put them back in // the order from the tx. - var j int + var outputIdx int var found bool - for j = range versionedHashes { - if versionedHashes[j] == versionedHash { + for outputIdx = range versionedHashes { + if versionedHashes[outputIdx] == versionedHash { found = true - totalFound++ + if outputsFound[outputIdx] { + return nil, fmt.Errorf("found blob with versioned hash %v twice", versionedHash) + } + outputsFound[outputIdx] = true break } } @@ -121,30 +150,24 @@ func (b *BlobClient) blobSidecars(ctx context.Context, slot uint64, versionedHas continue } - blob, err := hexutil.Decode(br.Data[i].Blob) - if err != nil { - return nil, fmt.Errorf("couldn't decode blob for slot(%d) at index(%d), blob(%s)", slot, br.Data[i].Index, pretty.FirstFewChars(br.Data[i].Blob)) - } - copy(blobs[j][:], blob) + copy(output[outputIdx][:], blobItem.Blob) - proofBytes, err := hexutil.Decode(br.Data[i].KzgProof) - if err != nil { - return nil, fmt.Errorf("couldn't decode proof for slot(%d) at index(%d), proof(%s)", slot, br.Data[i].Index, pretty.FirstFewChars(br.Data[i].KzgProof)) - } var proof kzg4844.Proof - copy(proof[:], proofBytes) + copy(proof[:], blobItem.KzgProof) - err = kzg4844.VerifyBlobProof(blobs[j], commitment, proof) + err = kzg4844.VerifyBlobProof(output[outputIdx], commitment, proof) if err != nil { - return nil, fmt.Errorf("failed to verify blob proof for blob at slot(%d) at index(%d), blob(%s)", slot, br.Data[i].Index, pretty.FirstFewChars(br.Data[i].Blob)) + return nil, fmt.Errorf("failed to verify blob proof for blob at slot(%d) at index(%d), blob(%s)", slot, blobItem.Index, pretty.FirstFewChars(blobItem.Blob.String())) } } - if totalFound < len(versionedHashes) { - return nil, fmt.Errorf("not all of the requested blobs (%d/%d) were found at slot (%d), can't reconstruct batch payload", totalFound, len(versionedHashes), slot) + for i, found := range outputsFound { + if !found { + return nil, fmt.Errorf("missing blob %v in slot %v, can't reconstruct batch payload", versionedHashes[i], slot) + } } - return blobs, nil + return output, nil } type genesisResponse struct { @@ -157,29 +180,10 @@ func (b *BlobClient) genesisTime(ctx context.Context) (uint64, error) { return b.cachedGenesisTime, nil } - body, err := b.bc.Get(ctx, "/eth/v1/beacon/genesis") + gr, err := beaconRequest[genesisResponse](b, ctx, "/eth/v1/beacon/genesis") if err != nil { - return 0, errors.Wrap(err, "error calling beacon client in genesisTime") - } - - gr := &genesisResponse{} - dataWrapper := &struct{ Data *genesisResponse }{Data: gr} - err = json.Unmarshal(body, dataWrapper) - if err != nil { - return 0, errors.Wrap(err, "error decoding json response in genesisTime") + return 0, fmt.Errorf("error calling beacon client in genesisTime: %w", err) } return gr.GenesisTime, nil } - -// The following code is taken from core/vm/contracts.go -const ( - blobCommitmentVersionKZG uint8 = 0x01 // Version byte for the point evaluation precompile. -) - -func kZGToVersionedHash(kzg kzg4844.Commitment) common.Hash { - h := sha256.Sum256(kzg[:]) - h[0] = blobCommitmentVersionKZG - - return h -} diff --git a/arbnode/node.go b/arbnode/node.go index 99ecb541ee..9f5626bbaf 100644 --- a/arbnode/node.go +++ b/arbnode/node.go @@ -40,7 +40,6 @@ import ( "github.com/offchainlabs/nitro/solgen/go/rollupgen" "github.com/offchainlabs/nitro/staker" "github.com/offchainlabs/nitro/staker/validatorwallet" - "github.com/offchainlabs/nitro/util/beaconclient" "github.com/offchainlabs/nitro/util/contracts" "github.com/offchainlabs/nitro/util/headerreader" "github.com/offchainlabs/nitro/util/redisutil" @@ -518,12 +517,7 @@ func createNodeImpl( var blobReader arbstate.BlobReader if config.BlobClient.BeaconChainUrl != "" { - bc, err := beaconclient.NewClient(config.BlobClient.BeaconChainUrl) - if err != nil { - return nil, err - } - - blobReader = NewBlobClient(bc, l1client) + blobReader = NewBlobClient(config.BlobClient, l1client) } inboxTracker, err := NewInboxTracker(arbDb, txStreamer, daReader, blobReader) diff --git a/util/beaconclient/client.go b/util/beaconclient/client.go deleted file mode 100644 index e2dfd8e6bf..0000000000 --- a/util/beaconclient/client.go +++ /dev/null @@ -1,98 +0,0 @@ -package beaconclient - -import ( - "context" - "io" - "net" - "net/http" - "net/url" - - "github.com/pkg/errors" -) - -// Client is a wrapper object around the HTTP client. -// Taken from prysm/api/client. -type Client struct { - hc *http.Client - baseURL *url.URL - token string -} - -// NewClient constructs a new client with the provided options (ex WithTimeout). -// `host` is the base host + port used to construct request urls. This value can be -// a URL string, or NewClient will assume an http endpoint if just `host:port` is used. -func NewClient(host string, opts ...ClientOpt) (*Client, error) { - u, err := urlForHost(host) - if err != nil { - return nil, err - } - c := &Client{ - hc: &http.Client{}, - baseURL: u, - } - for _, o := range opts { - o(c) - } - return c, nil -} - -// Token returns the bearer token used for jwt authentication -func (c *Client) Token() string { - return c.token -} - -// BaseURL returns the base url of the client -func (c *Client) BaseURL() *url.URL { - return c.baseURL -} - -// Do execute the request against the http client -func (c *Client) Do(req *http.Request) (*http.Response, error) { - return c.hc.Do(req) -} - -func urlForHost(h string) (*url.URL, error) { - // try to parse as url (being permissive) - u, err := url.Parse(h) - if err == nil && u.Host != "" { - return u, nil - } - // try to parse as host:port - host, port, err := net.SplitHostPort(h) - if err != nil { - return nil, ErrMalformedHostname - } - return &url.URL{Host: net.JoinHostPort(host, port), Scheme: "http"}, nil -} - -// NodeURL returns a human-readable string representation of the beacon node base url. -func (c *Client) NodeURL() string { - return c.baseURL.String() -} - -// Get is a generic, opinionated GET function to reduce boilerplate amongst the getters in this package. -func (c *Client) Get(ctx context.Context, path string, opts ...ReqOption) ([]byte, error) { - u := c.baseURL.ResolveReference(&url.URL{Path: path}) - req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) - if err != nil { - return nil, err - } - for _, o := range opts { - o(req) - } - r, err := c.hc.Do(req) - if err != nil { - return nil, err - } - defer func() { - err = r.Body.Close() - }() - if r.StatusCode != http.StatusOK { - return nil, Non200Err(r) - } - b, err := io.ReadAll(r.Body) - if err != nil { - return nil, errors.Wrap(err, "error reading http response body") - } - return b, nil -} diff --git a/util/beaconclient/errors.go b/util/beaconclient/errors.go deleted file mode 100644 index 7ee88805cd..0000000000 --- a/util/beaconclient/errors.go +++ /dev/null @@ -1,40 +0,0 @@ -package beaconclient - -import ( - "fmt" - "io" - "net/http" - - "github.com/pkg/errors" -) - -// ErrMalformedHostname is used to indicate if a host name's format is incorrect. -var ErrMalformedHostname = errors.New("hostname must include port, separated by one colon, like example.com:3500") - -// ErrNotOK is used to indicate when an HTTP request to the API failed with any non-2xx response code. -// More specific errors may be returned, but an error in reaction to a non-2xx response will always wrap ErrNotOK. -var ErrNotOK = errors.New("did not receive 2xx response from API") - -// ErrNotFound specifically means that a '404 - NOT FOUND' response was received from the API. -var ErrNotFound = errors.Wrap(ErrNotOK, "recv 404 NotFound response from API") - -// ErrInvalidNodeVersion indicates that the /eth/v1/node/version API response format was not recognized. -var ErrInvalidNodeVersion = errors.New("invalid node version response") - -// Non200Err is a function that parses an HTTP response to handle responses that are not 200 with a formatted error. -func Non200Err(response *http.Response) error { - bodyBytes, err := io.ReadAll(response.Body) - var body string - if err != nil { - body = "(Unable to read response body.)" - } else { - body = "response body:\n" + string(bodyBytes) - } - msg := fmt.Sprintf("code=%d, url=%s, body=%s", response.StatusCode, response.Request.URL, body) - switch response.StatusCode { - case 404: - return errors.Wrap(ErrNotFound, msg) - default: - return errors.Wrap(ErrNotOK, msg) - } -} diff --git a/util/beaconclient/options.go b/util/beaconclient/options.go deleted file mode 100644 index 98a37e17a0..0000000000 --- a/util/beaconclient/options.go +++ /dev/null @@ -1,48 +0,0 @@ -package beaconclient - -import ( - "fmt" - "net/http" - "time" -) - -// ReqOption is a request functional option. -type ReqOption func(*http.Request) - -// WithSSZEncoding is a request functional option that adds SSZ encoding header. -func WithSSZEncoding() ReqOption { - return func(req *http.Request) { - req.Header.Set("Accept", "application/octet-stream") - } -} - -// WithAuthorizationToken is a request functional option that adds header for authorization token. -func WithAuthorizationToken(token string) ReqOption { - return func(req *http.Request) { - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) - } -} - -// ClientOpt is a functional option for the Client type (http.Client wrapper) -type ClientOpt func(*Client) - -// WithTimeout sets the .Timeout attribute of the wrapped http.Client. -func WithTimeout(timeout time.Duration) ClientOpt { - return func(c *Client) { - c.hc.Timeout = timeout - } -} - -// WithRoundTripper replaces the underlying HTTP's transport with a custom one. -func WithRoundTripper(t http.RoundTripper) ClientOpt { - return func(c *Client) { - c.hc.Transport = t - } -} - -// WithAuthenticationToken sets an oauth token to be used. -func WithAuthenticationToken(token string) ClientOpt { - return func(c *Client) { - c.token = token - } -} diff --git a/util/blobs/blobs.go b/util/blobs/blobs.go index c8025dc253..9f6c8d1303 100644 --- a/util/blobs/blobs.go +++ b/util/blobs/blobs.go @@ -51,6 +51,13 @@ func DecodeBlobs(blobs []kzg4844.Blob) ([]byte, error) { return outputData, err } +func CommitmentToVersionedHash(commitment kzg4844.Commitment) common.Hash { + // As per the EIP-4844 spec, the versioned hash is the SHA-256 hash of the commitment with the first byte set to 1. + hash := sha256.Sum256(commitment[:]) + hash[0] = 1 + return hash +} + // Return KZG commitments, proofs, and versioned hashes that corresponds to these blobs func ComputeCommitmentsProofsAndHashes(blobs []kzg4844.Blob) ([]kzg4844.Commitment, []kzg4844.Proof, []common.Hash, error) { commitments := make([]kzg4844.Commitment, len(blobs)) @@ -67,10 +74,7 @@ func ComputeCommitmentsProofsAndHashes(blobs []kzg4844.Blob) ([]kzg4844.Commitme if err != nil { return nil, nil, nil, err } - // As per the EIP-4844 spec, the versioned hash is the SHA-256 hash of the commitment with the first byte set to 1. - hash := sha256.Sum256(commitments[i][:]) - hash[0] = 1 - versionedHashes[i] = hash + versionedHashes[i] = CommitmentToVersionedHash(commitments[i]) } return commitments, proofs, versionedHashes, nil