From 53d95c31b700d42fa7eb412d8f30801d0d385fae Mon Sep 17 00:00:00 2001 From: Teddy Knox Date: Wed, 26 Jun 2024 18:43:23 -0400 Subject: [PATCH] Add configurability for blob confirmation depth --- .env.example.holesky | 3 ++ .env.example.mainnet | 3 ++ README.md | 57 ++++++++++----------- e2e/setup.go | 1 + go.mod | 2 +- go.sum | 4 +- server/config.go | 23 ++++++--- server/eigenda_store.go | 66 +++++++++++++++++++------ server/load_store.go | 7 ++- verify/cert.go | 49 ++++++++++++++++-- verify/finalized_block_number_client.go | 63 +++++++++++++++++++++++ verify/verifier.go | 9 ++-- 12 files changed, 226 insertions(+), 61 deletions(-) create mode 100644 verify/finalized_block_number_client.go diff --git a/.env.example.holesky b/.env.example.holesky index 55706ed..210ab4b 100644 --- a/.env.example.holesky +++ b/.env.example.holesky @@ -18,6 +18,9 @@ EIGENDA_PROXY_SERVICE_MANAGER_ADDR=0xD4A7E1Bd8015057293f0D0A557088c286942e84b # Directory path to SRS tables # EIGENDA_PROXY_TARGET_CACHE_PATH=resources/SRSTables +# The number of Ethereum blocks of confirmation that the DA briging transaction must have before it is assumed by the proxy to be final. The value of `0` indicates that the proxy should wait for weak-subjectivity finalization (12-14 minutes). +# EIGENDA_PROXY_ETH_CONFIRMATION_DEPTH=6 + # Directory path to g1.point file # EIGENDA_PROXY_TARGET_KZG_G1_PATH=resources/g1.point diff --git a/.env.example.mainnet b/.env.example.mainnet index 8cab2da..53b016c 100644 --- a/.env.example.mainnet +++ b/.env.example.mainnet @@ -18,6 +18,9 @@ EIGENDA_PROXY_SERVICE_MANAGER_ADDR=0x870679E138bCdf293b7Ff14dD44b70FC97e12fc0 # Directory path to SRS tables # EIGENDA_PROXY_TARGET_CACHE_PATH=resources/SRSTables +# The number of Ethereum blocks of confirmation that the DA briging transaction must have before it is assumed by the proxy to be final. The value of `0` indicates that the proxy should wait for weak-subjectivity finalization (12-14 minutes). +# EIGENDA_PROXY_ETH_CONFIRMATION_DEPTH=6 + # Directory path to g1.point file # EIGENDA_PROXY_TARGET_KZG_G1_PATH=resources/g1.point diff --git a/README.md b/README.md index 8badd63..0a7d26e 100644 --- a/README.md +++ b/README.md @@ -17,34 +17,35 @@ In order to disperse to the EigenDA network in production, or at high throughput ## Configuration Options -| CLI Flag Name | Env Var Flag Name | Input Type | Default Value | Required | Description | -|----------------------------------------------|----------------------------------------------|------------|---------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `--eigenda-rpc` | `EIGENDA_PROXY_RPC` | string | None | Yes | RPC host of the EigenDA disperser service (e.g., on Holesky this is `disperser-holesky.eigenda.xyz:443`). Full network list available in the documentation. | -| `--eigenda-signer-private-key-hex` | `EIGENDA_PROXY_SIGNER_PRIVATE_KEY_HEX` | string | None | Yes | Hex-encoded signer private key. This key should not be associated with an Ethereum address holding any funds. | -| `--eigenda-eth-rpc` | `EIGENDA_PROXY_ETH_RPC` | string | None | Yes | JSON RPC node endpoint for the Ethereum network used for finalizing DA blobs. See available list here: | -| `--eigenda-svc-manager-addr` | `EIGENDA_PROXY_SERVICE_MANAGER_ADDR` | string | None | Yes | The deployed EigenDA service manager address. The list can be found here: | -| `--eigenda-g1-path` | `EIGENDA_PROXY_TARGET_KZG_G1_PATH` | string | None | Yes | Directory path to g1.point file. | -| `--eigenda-g2-tau-path` | `EIGENDA_PROXY_TARGET_G2_TAU_PATH` | string | None | Yes | Directory path to g2.point.powerOf2 file. | -| `--eigenda-cache-path` | `EIGENDA_PROXY_TARGET_CACHE_PATH` | string | None | Yes | Directory path to SRS tables for caching. | -| `--addr` | `EIGENDA_PROXY_ADDR` | string | "127.0.0.1" | No | Server listening address. | -| `--port` | `EIGENDA_PROXY_PORT` | int | 3100 | No | Server listening port. | -| `--eigenda-disable-tls` | `EIGENDA_PROXY_GRPC_DISABLE_TLS` | bool | false | No | Disable TLS for gRPC communication with the EigenDA disperser. | -| `--eigenda-custom-quorum-ids` | `EIGENDA_PROXY_CUSTOM_QUORUM_IDS` | string | None | No | Custom quorum IDs for writing blobs. Should not include default quorums 0 or 1. | -| `--eigenda-disable-point-verification-mode` | `EIGENDA_PROXY_DISABLE_POINT_VERIFICATION_MODE` | bool | false | No | Disable point verification mode. This mode performs IFFT on data before writing and FFT on data after reading. Disabling requires supplying the entire blob for verification against the KZG commitment. | -| `--eigenda-max-blob-length` | `EIGENDA_PROXY_MAX_BLOB_LENGTH` | string | "2MiB" | No | Maximum blob length to be written or read from EigenDA. Determines the number of SRS points loaded into memory for KZG commitments. Example units: '30MiB', '4Kb', '30MB'. Maximum size slightly exceeds 1GB. | -| `--eigenda-put-blob-encoding-version` | `EIGENDA_PROXY_PUT_BLOB_ENCODING_VERSION` | int | 0 | No | Blob encoding version to use when writing blobs from the high-level interface. | -| `--eigenda-status-query-retry-interval` | `EIGENDA_PROXY_STATUS_QUERY_INTERVAL` | duration | 5s | No | Interval between retries when awaiting network blob finalization. | -| `--eigenda-status-query-timeout` | `EIGENDA_PROXY_STATUS_QUERY_TIMEOUT` | duration | 30m0s | No | Duration to wait for a blob to finalize after being sent for dispersal. | -| `--eigenda-response-timeout` | `EIGENDA_PROXY_RESPONSE_TIMEOUT` | duration | 10s | No | Total time to wait for a response from the EigenDA disperser. | -| `--memstore.enabled` | `MEMSTORE_ENABLED` | bool | false | No | Whether to use mem-store for DA logic. | -| `--memstore.expiration` | `MEMSTORE_EXPIRATION` | duration | 25m0s | No | Duration that a blob/commitment pair are allowed to live. | -| `--metrics.addr` | `EIGENDA_PROXY_METRICS_ADDR` | string | "0.0.0.0" | No | Metrics listening address. | -| `--metrics.enabled` | `EIGENDA_PROXY_METRICS_ENABLED` | bool | false | No | Enable the metrics server. | -| `--metrics.port` | `EIGENDA_PROXY_METRICS_PORT` | int | 7300 | No | Metrics listening port. | -| `--log.color` | `EIGENDA_PROXY_LOG_COLOR` | bool | false | No | Color the log output if in terminal mode. | -| `--log.format` | `EIGENDA_PROXY_LOG_FORMAT` | string | text | No | Format the log output. Supported formats: 'text', 'terminal', 'logfmt', 'json', 'json-pretty'. | -| `--log.level` | `EIGENDA_PROXY_LOG_LEVEL` | string | INFO | No | The lowest log level that will be output. | -| `--log.pid` | `EIGENDA_PROXY_LOG_PID` | bool | false | No | Show pid in the log. | +| CLI Flag Name | Env Var Flag Name | Input Type | Default Value | Required | Description | +|----------------------------------------------|-------------------------------------------------|------------|---------------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `--addr` | None | string | None | Yes | Server listening address. | +| `--port` | None | int | None | Yes | Server listening port. | +| `--eigenda-disperser-rpc` | `EIGENDA_PROXY_EIGENDA_DISPERSER_RPC` | string | None | Yes | RPC host of the EigenDA disperser service (e.g., on Holesky this is `disperser-holesky.eigenda.xyz:443`). Full network list available in the documentation. | +| `--eigenda-signer-private-key-hex` | `EIGENDA_PROXY_SIGNER_PRIVATE_KEY_HEX` | string | None | Yes | Hex-encoded signer private key. This key should not be associated with an Ethereum address holding any funds. | +| `--eigenda-eth-rpc` | `EIGENDA_PROXY_ETH_RPC` | string | None | Yes | JSON RPC node endpoint for the Ethereum network used for finalizing DA blobs. See available list here: | +| `--eigenda-svc-manager-addr` | `EIGENDA_PROXY_SERVICE_MANAGER_ADDR` | string | None | Yes | The deployed EigenDA service manager address. The list can be found here: | +| `--eigenda-g1-path` | `EIGENDA_PROXY_TARGET_KZG_G1_PATH` | string | "resources/g1.point" | No | Directory path to g1.point file. | +| `--eigenda-g2-tau-path` | `EIGENDA_PROXY_TARGET_G2_TAU_PATH` | string | "resources/g2.point.powerOf2" | No | Directory path to g2.point.powerOf2 file. | +| `--eigenda-cache-path` | `EIGENDA_PROXY_TARGET_CACHE_PATH` | string | "resources/SRSTables/" | No | Directory path to SRS tables for caching. | +| `--eigenda-eth-confirmation-depth` | `EIGENDA_PROXY_ETH_CONFIRMATION_DEPTH` | int | 6 | No | The number of Ethereum blocks of confirmation that the DA briging transaction must have before it is assumed by the proxy to be final. The value of `0` indicates that the proxy should wait for weak-subjectivity finalization (12-14 minutes). | +| `--eigenda-disable-tls` | `EIGENDA_PROXY_GRPC_DISABLE_TLS` | bool | false | No | Disable TLS for gRPC communication with the EigenDA disperser. | +| `--eigenda-custom-quorum-ids` | `EIGENDA_PROXY_CUSTOM_QUORUM_IDS` | string | None | No | Custom quorum IDs for writing blobs. Should not include default quorums 0 or 1. | +| `--eigenda-disable-point-verification-mode` | `EIGENDA_PROXY_DISABLE_POINT_VERIFICATION_MODE` | bool | false | No | Disable point verification mode. This mode performs IFFT on data before writing and FFT on data after reading. Disabling requires supplying the entire blob for verification against the KZG commitment. | +| `--eigenda-max-blob-length` | `EIGENDA_PROXY_MAX_BLOB_LENGTH` | string | "2MiB" | No | Maximum blob length to be written or read from EigenDA. Determines the number of SRS points loaded into memory for KZG commitments. Example units: '30MiB', '4Kb', '30MB'. Maximum size slightly exceeds 1GB. | +| `--eigenda-put-blob-encoding-version` | `EIGENDA_PROXY_PUT_BLOB_ENCODING_VERSION` | int | 0 | No | Blob encoding version to use when writing blobs from the high-level interface. | +| `--eigenda-status-query-retry-interval` | `EIGENDA_PROXY_STATUS_QUERY_INTERVAL` | duration | 5s | No | Interval between retries when awaiting network blob finalization. | +| `--eigenda-status-query-timeout` | `EIGENDA_PROXY_STATUS_QUERY_TIMEOUT` | duration | 30m0s | No | Duration to wait for a blob to finalize after being sent for dispersal. | +| `--eigenda-response-timeout` | `EIGENDA_PROXY_RESPONSE_TIMEOUT` | duration | 10s | No | Total time to wait for a response from the EigenDA disperser. | +| `--memstore.enabled` | `MEMSTORE_ENABLED` | bool | false | No | Whether to use mem-store for DA logic. | +| `--memstore.expiration` | `MEMSTORE_EXPIRATION` | duration | 25m0s | No | Duration that a blob/commitment pair are allowed to live. | +| `--metrics.addr` | `EIGENDA_PROXY_METRICS_ADDR` | string | "0.0.0.0" | No | Metrics listening address. | +| `--metrics.enabled` | `EIGENDA_PROXY_METRICS_ENABLED` | bool | false | No | Enable the metrics server. | +| `--metrics.port` | `EIGENDA_PROXY_METRICS_PORT` | int | 7300 | No | Metrics listening port. | +| `--log.color` | `EIGENDA_PROXY_LOG_COLOR` | bool | false | No | Color the log output if in terminal mode. | +| `--log.format` | `EIGENDA_PROXY_LOG_FORMAT` | string | text | No | Format the log output. Supported formats: 'text', 'terminal', 'logfmt', 'json', 'json-pretty'. | +| `--log.level` | `EIGENDA_PROXY_LOG_LEVEL` | string | INFO | No | The lowest log level that will be output. | +| `--log.pid` | `EIGENDA_PROXY_LOG_PID` | bool | false | No | Show pid in the log. | ### Certificate verification diff --git a/e2e/setup.go b/e2e/setup.go index 8b3b484..9370b2f 100644 --- a/e2e/setup.go +++ b/e2e/setup.go @@ -72,6 +72,7 @@ func CreateTestSuite(t *testing.T, useMemory bool) (TestSuite, func()) { PutBlobEncodingVersion: 0x00, MemstoreEnabled: useMemory, MemstoreBlobExpiration: 14 * 24 * time.Hour, + EthConfirmationDepth: 6, } store, err := server.LoadStore( diff --git a/go.mod b/go.mod index fcd254c..c385c63 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/Layr-Labs/eigenda-proxy go 1.21 require ( - github.com/Layr-Labs/eigenda v0.7.2-0.20240606180508-e90cb7432ca5 + github.com/Layr-Labs/eigenda v0.7.5-0.20240626225853-1645ffe3489e github.com/consensys/gnark-crypto v0.12.1 github.com/ethereum-optimism/optimism v1.7.7 github.com/ethereum/go-ethereum v1.14.0 diff --git a/go.sum b/go.sum index d72d90e..f2b13cf 100644 --- a/go.sum +++ b/go.sum @@ -20,8 +20,8 @@ github.com/DataDog/datadog-go v2.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3 github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/DataDog/zstd v1.5.2 h1:vUG4lAyuPCXO0TLbXvPv7EB7cNK1QV/luu55UHLrrn8= github.com/DataDog/zstd v1.5.2/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= -github.com/Layr-Labs/eigenda v0.7.2-0.20240606180508-e90cb7432ca5 h1:PGcoSXnIlZYhwfrzqG1F2E/Sqc3ZGRqa5owryswax2s= -github.com/Layr-Labs/eigenda v0.7.2-0.20240606180508-e90cb7432ca5/go.mod h1:gG5KSp5gGY0lywj6aZwaK9ZEF8eEVX4ilo679pFpvAA= +github.com/Layr-Labs/eigenda v0.7.5-0.20240626225853-1645ffe3489e h1:vibQgDKVXvuKZKqdyvIuCx8MH1B5uOGrKITAUp3bzJg= +github.com/Layr-Labs/eigenda v0.7.5-0.20240626225853-1645ffe3489e/go.mod h1:gG5KSp5gGY0lywj6aZwaK9ZEF8eEVX4ilo679pFpvAA= github.com/Layr-Labs/eigensdk-go v0.1.7-0.20240507215523-7e4891d5099a h1:L/UsJFw9M31FD/WgXTPFB0oxbq9Cu4Urea1xWPMQS7Y= github.com/Layr-Labs/eigensdk-go v0.1.7-0.20240507215523-7e4891d5099a/go.mod h1:OF9lmS/57MKxS0xpSpX0qHZl0SKkDRpvJIvsGvMN1y8= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= diff --git a/server/config.go b/server/config.go index aebfc20..2f1b109 100644 --- a/server/config.go +++ b/server/config.go @@ -19,6 +19,7 @@ const ( EigenDADisperserRPCFlagName = "eigenda-disperser-rpc" EthRPCFlagName = "eigenda-eth-rpc" SvcManagerAddrFlagName = "eigenda-svc-manager-addr" + EthConfirmationDepthFlagName = "eigenda-eth-confirmation-depth" StatusQueryRetryIntervalFlagName = "eigenda-status-query-retry-interval" StatusQueryTimeoutFlagName = "eigenda-status-query-timeout" DisableTlsFlagName = "eigenda-disable-tls" @@ -50,8 +51,9 @@ type Config struct { PutBlobEncodingVersion codecs.BlobEncodingVersion // ETH vars - EthRPC string - SvcManagerAddr string + EthRPC string + SvcManagerAddr string + EthConfirmationDepth uint64 // KZG vars CacheDir string @@ -111,10 +113,11 @@ func (c *Config) VerificationCfg() *verify.Config { } return &verify.Config{ - Verify: true, - RPCURL: c.EthRPC, - SvcManagerAddr: c.SvcManagerAddr, - KzgConfig: kzgCfg, + Verify: true, + RPCURL: c.EthRPC, + SvcManagerAddr: c.SvcManagerAddr, + KzgConfig: kzgCfg, + EthConfirmationDepth: c.EthConfirmationDepth, } } @@ -140,9 +143,11 @@ func ReadConfig(ctx *cli.Context) Config { MaxBlobLength: ctx.String(MaxBlobLengthFlagName), SvcManagerAddr: ctx.String(SvcManagerAddrFlagName), EthRPC: ctx.String(EthRPCFlagName), + EthConfirmationDepth: ctx.Uint64(EthConfirmationDepthFlagName), MemstoreEnabled: ctx.Bool(MemstoreFlagName), MemstoreBlobExpiration: ctx.Duration(MemstoreExpirationFlagName), } + cfg.ClientConfig.WaitForFinalization = (cfg.EthConfirmationDepth != 0) return cfg } @@ -245,6 +250,12 @@ func CLIFlags(envPrefix string) []cli.Flag { Usage: "The deployed EigenDA service manager address. The list can be found here: https://github.com/Layr-Labs/eigenlayer-middleware/?tab=readme-ov-file#current-mainnet-deployment", EnvVars: prefixEnvVars("SERVICE_MANAGER_ADDR"), }, + &cli.Uint64Flag{ + Name: EthConfirmationDepthFlagName, + Usage: "The number of Ethereum blocks of confirmation that the DA briging transaction must have before it is assumed by the proxy to be final. The value of `0` indicates that the proxy should wait for weak-subjectivity finalization (12-14 minutes).", + EnvVars: prefixEnvVars("ETH_CONFIRMATION_DEPTH"), + Value: 6, + }, &cli.BoolFlag{ Name: MemstoreFlagName, Usage: "Whether to use mem-store for DA logic.", diff --git a/server/eigenda_store.go b/server/eigenda_store.go index 6b5066e..f4a2a04 100644 --- a/server/eigenda_store.go +++ b/server/eigenda_store.go @@ -2,27 +2,40 @@ package server import ( "context" + "errors" "fmt" + "time" "github.com/Layr-Labs/eigenda-proxy/verify" "github.com/Layr-Labs/eigenda/api/clients" + "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/rlp" ) +type EigenDAStoreConfig struct { + MaxBlobSizeBytes uint64 + EthConfirmationDepth uint64 + + // The total amount of time that the client will spend waiting for EigenDA to confirm a blob + StatusQueryTimeout time.Duration +} + // EigenDAStore does storage interactions and verifications for blobs with DA. type EigenDAStore struct { - client *clients.EigenDAClient - verifier *verify.Verifier - maxBlobSizeBytes uint64 + client *clients.EigenDAClient + verifier *verify.Verifier + cfg *EigenDAStoreConfig + log log.Logger } var _ Store = (*EigenDAStore)(nil) -func NewEigenDAStore(ctx context.Context, client *clients.EigenDAClient, v *verify.Verifier, maxBlobSizeBytes uint64) (*EigenDAStore, error) { +func NewEigenDAStore(ctx context.Context, client *clients.EigenDAClient, v *verify.Verifier, log log.Logger, cfg *EigenDAStoreConfig) (*EigenDAStore, error) { return &EigenDAStore{ - client: client, - verifier: v, - maxBlobSizeBytes: maxBlobSizeBytes, + client: client, + verifier: v, + log: log, + cfg: cfg, }, nil } @@ -50,11 +63,6 @@ func (e EigenDAStore) Get(ctx context.Context, key []byte, domain DomainType) ([ return nil, err } - err = e.verifier.VerifyCert(&cert) - if err != nil { - return nil, err - } - switch domain { case BinaryDomain: return decodedBlob, nil @@ -67,13 +75,16 @@ func (e EigenDAStore) Get(ctx context.Context, key []byte, domain DomainType) ([ // Put disperses a blob for some pre-image and returns the associated RLP encoded certificate commit. func (e EigenDAStore) Put(ctx context.Context, value []byte) (comm []byte, err error) { - if uint64(len(value)) > e.maxBlobSizeBytes { - return nil, fmt.Errorf("blob is larger than max blob size: blob length %d, max blob size %d", len(value), e.maxBlobSizeBytes) + if uint64(len(value)) > e.cfg.MaxBlobSizeBytes { + return nil, fmt.Errorf("blob is larger than max blob size: blob length %d, max blob size %d", len(value), e.cfg.MaxBlobSizeBytes) } - cert, err := e.client.PutBlob(ctx, value) + + dispersalStart := time.Now() + blobInfo, err := e.client.PutBlob(ctx, value) if err != nil { return nil, err } + cert := (*verify.Certificate)(blobInfo) encodedBlob, err := e.client.GetCodec().EncodeBlob(value) if err != nil { @@ -84,6 +95,31 @@ func (e EigenDAStore) Put(ctx context.Context, value []byte) (comm []byte, err e return nil, err } + dispersalDuration := time.Since(dispersalStart) + remainingTimeout := e.cfg.StatusQueryTimeout - dispersalDuration + + ticker := time.NewTicker(12 * time.Second) + defer ticker.Stop() + ctx, cancel := context.WithTimeout(context.Background(), remainingTimeout) + defer cancel() + + done := false + for !done { + select { + case <-ctx.Done(): + return nil, ctx.Err() + case <-ticker.C: + err = e.verifier.VerifyCert(cert) + if err == nil { + done = true + } else if !errors.Is(err, verify.ErrBatchMetadataHashNotFound) { + return nil, err + } else { + e.log.Info("Blob confirmed, waiting for sufficient confirmation depth...", "targetDepth", e.cfg.EthConfirmationDepth) + } + } + } + bytes, err := rlp.EncodeToBytes(cert) if err != nil { return nil, fmt.Errorf("failed to encode DA cert to RLP format: %w", err) diff --git a/server/load_store.go b/server/load_store.go index 41458c2..327cc3c 100644 --- a/server/load_store.go +++ b/server/load_store.go @@ -43,6 +43,11 @@ func LoadStore(cfg CLIConfig, ctx context.Context, log log.Logger) (Store, error ctx, client, verifier, - maxBlobLength, + log, + &EigenDAStoreConfig{ + MaxBlobSizeBytes: maxBlobLength, + EthConfirmationDepth: cfg.EigenDAConfig.EthConfirmationDepth, + StatusQueryTimeout: cfg.EigenDAConfig.ClientConfig.StatusQueryTimeout, + }, ) } diff --git a/verify/cert.go b/verify/cert.go index 6f55b43..48d5f2e 100644 --- a/verify/cert.go +++ b/verify/cert.go @@ -1,9 +1,14 @@ package verify import ( + "bytes" + "context" + "errors" "fmt" + "math/big" binding "github.com/Layr-Labs/eigenda/contracts/bindings/EigenDAServiceManager" + "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethclient" @@ -11,10 +16,15 @@ import ( "golang.org/x/exp/slices" ) +var ErrBatchMetadataHashNotFound = errors.New("BatchMetadataHash not found for BatchId") + // CertVerifier verifies the DA certificate against on-chain EigenDA contracts // to ensure disperser returned fields haven't been tampered with type CertVerifier struct { - manager *binding.ContractEigenDAServiceManagerCaller + ethConfirmationDepth uint64 + manager *binding.ContractEigenDAServiceManagerCaller + finalizedBlockClient *FinalizedBlockClient + ethClient *ethclient.Client } func NewCertVerifier(cfg *Config, l log.Logger) (*CertVerifier, error) { @@ -30,22 +40,33 @@ func NewCertVerifier(cfg *Config, l log.Logger) (*CertVerifier, error) { } return &CertVerifier{ - manager: m, + manager: m, + finalizedBlockClient: NewFinalizedBlockClient(client.Client()), + ethConfirmationDepth: cfg.EthConfirmationDepth, + ethClient: client, }, nil } func (cv *CertVerifier) VerifyBatch(header *binding.IEigenDAServiceManagerBatchHeader, id uint32, recordHash [32]byte, blockNum uint32) error { + // 0 - Determine block context number + blockNumber, err := cv.getContextBlock() + if err != nil { + return err + } + // 1 - Verify batch hash // 1.a - ensure that a batch hash can be looked up for a batch ID - expectedHash, err := cv.manager.BatchIdToBatchMetadataHash(nil, id) + expectedHash, err := cv.manager.BatchIdToBatchMetadataHash(&bind.CallOpts{BlockNumber: blockNumber}, id) if err != nil { return err } + if bytes.Equal(expectedHash[:], make([]byte, 32)) { + return ErrBatchMetadataHashNotFound + } // 1.b - ensure that hash generated from local cert matches one stored on-chain - actualHash, err := HashBatchMetadata(header, recordHash, blockNum) if err != nil { @@ -84,3 +105,23 @@ func (cv *CertVerifier) VerifyMerkleProof(inclusionProof []byte, root []byte, bl func (cv *CertVerifier) VerifyBlobParams(inclusionProof []byte, rootHash []byte, leafHash []byte, index uint64) error { return nil } + +func (cv *CertVerifier) getContextBlock() (*big.Int, error) { + var blockNumber *big.Int + if cv.ethConfirmationDepth == 0 { + // Get the latest finalized block + blockHeader, err := cv.finalizedBlockClient.GetBlock(context.Background(), "finalized", false) + if err != nil { + return nil, err + } + blockNumber = blockHeader.Number() + } else { + blockHeader, err := cv.ethClient.BlockByNumber(context.Background(), nil) + if err != nil { + return nil, err + } + blockNumber = new(big.Int) + blockNumber.Sub(blockHeader.Number(), big.NewInt(int64(cv.ethConfirmationDepth-1))) + } + return blockNumber, nil +} diff --git a/verify/finalized_block_number_client.go b/verify/finalized_block_number_client.go new file mode 100644 index 0000000..79760e1 --- /dev/null +++ b/verify/finalized_block_number_client.go @@ -0,0 +1,63 @@ +package verify + +import ( + "context" + "encoding/json" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/rpc" +) + +type FinalizedBlockClient struct { + c *rpc.Client +} + +// Dial connects a client to the given URL. +func Dial(rawurl string) (*FinalizedBlockClient, error) { + return DialContext(context.Background(), rawurl) +} + +// DialContext connects a client to the given URL with context. +func DialContext(ctx context.Context, rawurl string) (*FinalizedBlockClient, error) { + c, err := rpc.DialContext(ctx, rawurl) + if err != nil { + return nil, err + } + return NewFinalizedBlockClient(c), nil +} + +// NewFinalizedBlockClient creates a client that uses the given RPC client. +func NewFinalizedBlockClient(c *rpc.Client) *FinalizedBlockClient { + return &FinalizedBlockClient{c} +} + +// Close closes the underlying RPC connection. +func (ec *FinalizedBlockClient) Close() { + ec.c.Close() +} + +// Client gets the underlying RPC client. +func (ec *FinalizedBlockClient) Client() *rpc.Client { + return ec.c +} + +func (c *FinalizedBlockClient) GetBlock(ctx context.Context, method string, args ...interface{}) (*types.Block, error) { + var raw json.RawMessage + err := c.c.CallContext(ctx, &raw, method, args...) + if err != nil { + return nil, err + } + + // Decode header and transactions. + var head *types.Header + if err := json.Unmarshal(raw, &head); err != nil { + return nil, err + } + // When the block is not found, the API returns JSON null. + if head == nil { + return nil, ethereum.NotFound + } + + return types.NewBlockWithHeader(head), nil +} diff --git a/verify/verifier.go b/verify/verifier.go index 8b69d0b..a4dd28f 100644 --- a/verify/verifier.go +++ b/verify/verifier.go @@ -17,10 +17,11 @@ import ( ) type Config struct { - Verify bool - RPCURL string - SvcManagerAddr string - KzgConfig *kzg.KzgConfig + Verify bool + RPCURL string + SvcManagerAddr string + KzgConfig *kzg.KzgConfig + EthConfirmationDepth uint64 } type Verifier struct {