diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 06c83b08..7fbd8a00 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -16,11 +16,11 @@ jobs: go-version: '1.22' # The Go version to download (if necessary) and use. - run: go version - - name: Checkout EigenDA + - name: Checkout code uses: actions/checkout@v3 - name: golangci-lint uses: golangci/golangci-lint-action@v6 with: - version: v1.60 + version: v1.61 args: --timeout 3m diff --git a/flags/eigendaflags/cli.go b/flags/eigendaflags/cli.go index 29cf0907..9a13c772 100644 --- a/flags/eigendaflags/cli.go +++ b/flags/eigendaflags/cli.go @@ -1,6 +1,9 @@ package eigendaflags import ( + "fmt" + "log" + "strconv" "time" "github.com/Layr-Labs/eigenda/api/clients" @@ -21,6 +24,9 @@ var ( PutBlobEncodingVersionFlagName = withFlagPrefix("put-blob-encoding-version") DisablePointVerificationModeFlagName = withFlagPrefix("disable-point-verification-mode") WaitForFinalizationFlagName = withFlagPrefix("wait-for-finalization") + ConfirmationDepthFlagName = withFlagPrefix("confirmation-depth") + EthRPCURLFlagName = withFlagPrefix("eth-rpc") + SvcManagerAddrFlagName = withFlagPrefix("svc-manager-addr") ) func withFlagPrefix(s string) string { @@ -101,11 +107,41 @@ func CLIFlags(envPrefix, category string) []cli.Flag { EnvVars: []string{withEnvPrefix(envPrefix, "WAIT_FOR_FINALIZATION")}, Value: false, Category: category, + Hidden: true, + Action: func(_ *cli.Context, _ bool) error { + return fmt.Errorf("flag --%s is deprecated, instead use --%s finalized", WaitForFinalizationFlagName, ConfirmationDepthFlagName) + }, + }, + &cli.StringFlag{ + Name: ConfirmationDepthFlagName, + Usage: "Number of Ethereum blocks to wait after the blob's batch has been included on-chain, " + + "before returning from PutBlob calls. Can either be a number or 'finalized'.", + EnvVars: []string{withEnvPrefix(envPrefix, "CONFIRMATION_DEPTH")}, + Value: "0", + Category: category, + Action: func(_ *cli.Context, val string) error { + return validateConfirmationFlag(val) + }, + }, + &cli.StringFlag{ + Name: EthRPCURLFlagName, + Usage: "URL of the Ethereum RPC endpoint. Needed to confirm blobs landed onchain.", + EnvVars: []string{withEnvPrefix(envPrefix, "ETH_RPC")}, + Category: category, + Required: true, + }, + &cli.StringFlag{ + Name: SvcManagerAddrFlagName, + Usage: "Address of the EigenDAServiceManager contract. Required to confirm blobs landed onchain. See https://github.com/Layr-Labs/eigenlayer-middleware/?tab=readme-ov-file#current-mainnet-deployment", + EnvVars: []string{withEnvPrefix(envPrefix, "SERVICE_MANAGER_ADDR")}, + Category: category, + Required: true, }, } } func ReadConfig(ctx *cli.Context) clients.EigenDAClientConfig { + waitForFinalization, confirmationDepth := parseConfirmationFlag(ctx.String(ConfirmationDepthFlagName)) return clients.EigenDAClientConfig{ RPC: ctx.String(DisperserRPCFlagName), StatusQueryRetryInterval: ctx.Duration(StatusQueryRetryIntervalFlagName), @@ -116,6 +152,40 @@ func ReadConfig(ctx *cli.Context) clients.EigenDAClientConfig { SignerPrivateKeyHex: ctx.String(SignerPrivateKeyHexFlagName), PutBlobEncodingVersion: codecs.BlobEncodingVersion(ctx.Uint(PutBlobEncodingVersionFlagName)), DisablePointVerificationMode: ctx.Bool(DisablePointVerificationModeFlagName), - WaitForFinalization: ctx.Bool(WaitForFinalizationFlagName), + WaitForFinalization: waitForFinalization, + WaitForConfirmationDepth: confirmationDepth, + EthRpcUrl: ctx.String(EthRPCURLFlagName), + SvcManagerAddr: ctx.String(SvcManagerAddrFlagName), + } +} + +// parse the val (either "finalized" or a number) into waitForFinalization (bool) and confirmationDepth (uint64). +func parseConfirmationFlag(val string) (bool, uint64) { + if val == "finalized" { + return true, 0 } + + depth, err := strconv.ParseUint(val, 10, 64) + if err != nil { + panic("this should never happen, as the flag is validated before this point") + } + + return false, depth +} + +func validateConfirmationFlag(val string) error { + if val == "finalized" { + return nil + } + + depth, err := strconv.ParseUint(val, 10, 64) + if err != nil { + return fmt.Errorf("confirmation-depth must be either 'finalized' or a number, got: %s", val) + } + + if depth >= 64 { + log.Printf("Warning: confirmation depth set to %d, which is > 2 epochs (64). Consider using 'finalized' instead.\n", depth) + } + + return nil } diff --git a/go.mod b/go.mod index c11506e8..d012c3a0 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.22 toolchain go1.22.0 require ( - github.com/Layr-Labs/eigenda v0.8.4 + github.com/Layr-Labs/eigenda v0.8.5-0.20241028201743-5fe3e910a22d github.com/consensys/gnark-crypto v0.12.1 github.com/ethereum-optimism/optimism v1.9.4-0.20240927020138-a9c7f349d10b github.com/ethereum/go-ethereum v1.14.11 @@ -39,6 +39,7 @@ require ( github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect + github.com/aws/aws-sdk-go-v2/service/dynamodb v1.31.0 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7 // indirect github.com/aws/aws-sdk-go-v2/service/kms v1.31.0 // indirect @@ -173,7 +174,7 @@ require ( github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect github.com/miekg/dns v1.1.62 // indirect github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect diff --git a/go.sum b/go.sum index 3783f568..e3eb27f3 100644 --- a/go.sum +++ b/go.sum @@ -22,8 +22,8 @@ github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2 github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/DataDog/zstd v1.5.6-0.20230824185856-869dae002e5e h1:ZIWapoIRN1VqT8GR8jAwb1Ie9GyehWjVcGh32Y2MznE= github.com/DataDog/zstd v1.5.6-0.20230824185856-869dae002e5e/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= -github.com/Layr-Labs/eigenda v0.8.4 h1:6znMJcPLtchYixbUddCeKCKqwbpsAf/4CX/fBnWlIpc= -github.com/Layr-Labs/eigenda v0.8.4/go.mod h1:MzSFbxDQ1/tMcLlfxqz08YubB3rd+E2xme2p7hwP2YM= +github.com/Layr-Labs/eigenda v0.8.5-0.20241028201743-5fe3e910a22d h1:s5U1TaWhC1J2rwc9IQdU/COGvuXALCKMd9GbONUZMxc= +github.com/Layr-Labs/eigenda v0.8.5-0.20241028201743-5fe3e910a22d/go.mod h1:sqUNf9Ak+EfAX82jDxrb4QbT/g3DViWD3b7YIk36skk= 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.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= @@ -583,8 +583,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= -github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= diff --git a/server/config.go b/server/config.go index bfa2030e..3dc9d55c 100644 --- a/server/config.go +++ b/server/config.go @@ -37,11 +37,12 @@ type Config struct { // ReadConfig ... parses the Config from the provided flags or environment variables. func ReadConfig(ctx *cli.Context) Config { + edaClientConfig := eigendaflags.ReadConfig(ctx) return Config{ RedisConfig: redis.ReadConfig(ctx), S3Config: s3.ReadConfig(ctx), - EdaClientConfig: eigendaflags.ReadConfig(ctx), - VerifierConfig: verify.ReadConfig(ctx), + EdaClientConfig: edaClientConfig, + VerifierConfig: verify.ReadConfig(ctx, edaClientConfig), MemstoreEnabled: ctx.Bool(memstore.EnabledFlagName), MemstoreConfig: memstore.ReadConfig(ctx), FallbackTargets: ctx.StringSlice(flags.FallbackTargetsFlagName), @@ -80,7 +81,7 @@ func (cfg *Config) Check() error { // TODO: move this verification logic to verify/cli.go if cfg.VerifierConfig.VerifyCerts { if cfg.MemstoreEnabled { - return fmt.Errorf("cannot enable cert verification when memstore is enabled") + return fmt.Errorf("cannot enable cert verification when memstore is enabled. use --%s", verify.CertVerificationDisabledFlagName) } if cfg.VerifierConfig.RPCURL == "" { return fmt.Errorf("cert verification enabled but eth rpc is not set") diff --git a/store/generated_key/eigenda/eigenda.go b/store/generated_key/eigenda/eigenda.go index 56ace6c3..33f7ea36 100644 --- a/store/generated_key/eigenda/eigenda.go +++ b/store/generated_key/eigenda/eigenda.go @@ -2,7 +2,6 @@ package eigenda import ( "context" - "errors" "fmt" "time" @@ -66,46 +65,27 @@ func (e Store) Put(ctx context.Context, value []byte) ([]byte, error) { if err != nil { return nil, fmt.Errorf("EigenDA client failed to re-encode blob: %w", err) } + // TODO: We should move this length check inside PutBlob if uint64(len(encodedBlob)) > e.cfg.MaxBlobSizeBytes { return nil, fmt.Errorf("%w: blob length %d, max blob size %d", store.ErrProxyOversizedBlob, len(value), e.cfg.MaxBlobSizeBytes) } - dispersalStart := time.Now() blobInfo, err := e.client.PutBlob(ctx, value) if err != nil { + // TODO: we will want to filter for errors here and return a 503 when needed + // ie when dispersal itself failed, or that we timed out waiting for batch to land onchain return nil, err } cert := (*verify.Certificate)(blobInfo) err = e.verifier.VerifyCommitment(cert.BlobHeader.Commitment, encodedBlob) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to verify commitment: %w", err) } - dispersalDuration := time.Since(dispersalStart) - remainingTimeout := e.cfg.StatusQueryTimeout - dispersalDuration - - ticker := time.NewTicker(12 * time.Second) // avg. eth block time - defer ticker.Stop() - ctx, cancel := context.WithTimeout(context.Background(), remainingTimeout) - defer cancel() - - done := false - for !done { - select { - case <-ctx.Done(): - return nil, fmt.Errorf("timed out when trying to verify the DA certificate for a blob batch after dispersal") - case <-ticker.C: - err = e.verifier.VerifyCert(cert) - switch { - case err == nil: - done = true - case errors.Is(err, verify.ErrBatchMetadataHashNotFound): - e.log.Info("Blob confirmed, waiting for sufficient confirmation depth...", "targetDepth", e.cfg.EthConfirmationDepth) - default: - return nil, err - } - } + err = e.verifier.VerifyCert(ctx, cert) + if err != nil { + return nil, fmt.Errorf("failed to verify DA cert: %w", err) } bytes, err := rlp.EncodeToBytes(cert) @@ -128,7 +108,7 @@ func (e Store) BackendType() store.BackendType { // Key is used to recover certificate fields and that verifies blob // against commitment to ensure data is valid and non-tampered. -func (e Store) Verify(key []byte, value []byte) error { +func (e Store) Verify(ctx context.Context, key []byte, value []byte) error { var cert verify.Certificate err := rlp.DecodeBytes(key, &cert) if err != nil { @@ -148,5 +128,5 @@ func (e Store) Verify(key []byte, value []byte) error { } // verify DA certificate against EigenDA's batch metadata that's bridged to Ethereum - return e.verifier.VerifyCert(&cert) + return e.verifier.VerifyCert(ctx, &cert) } diff --git a/store/generated_key/memstore/memstore.go b/store/generated_key/memstore/memstore.go index b0dd7ab5..120be0ea 100644 --- a/store/generated_key/memstore/memstore.go +++ b/store/generated_key/memstore/memstore.go @@ -218,7 +218,7 @@ func (e *MemStore) Put(_ context.Context, value []byte) ([]byte, error) { return certBytes, nil } -func (e *MemStore) Verify(_, _ []byte) error { +func (e *MemStore) Verify(_ context.Context, _, _ []byte) error { return nil } diff --git a/store/manager.go b/store/manager.go index 79dfb9c8..c5db4852 100644 --- a/store/manager.go +++ b/store/manager.go @@ -56,7 +56,7 @@ func (m *Manager) Get(ctx context.Context, key []byte, cm commitments.Commitment } // 2 - verify blob hash against commitment key digest - err = m.s3.Verify(key, value) + err = m.s3.Verify(ctx, key, value) if err != nil { return nil, err } @@ -82,7 +82,7 @@ func (m *Manager) Get(ctx context.Context, key []byte, cm commitments.Commitment data, err := m.eigenda.Get(ctx, key) if err == nil { // verify - err = m.eigenda.Verify(key, data) + err = m.eigenda.Verify(ctx, key, data) if err != nil { return nil, err } @@ -158,7 +158,7 @@ func (m *Manager) putKeccak256Mode(ctx context.Context, key []byte, value []byte return nil, errors.New("S3 is disabled but is only supported for posting known commitment keys") } - err := m.s3.Verify(key, value) + err := m.s3.Verify(ctx, key, value) if err != nil { return nil, err } diff --git a/store/precomputed_key/redis/redis.go b/store/precomputed_key/redis/redis.go index a90b202f..9fea3b62 100644 --- a/store/precomputed_key/redis/redis.go +++ b/store/precomputed_key/redis/redis.go @@ -71,7 +71,7 @@ func (r *Store) Put(ctx context.Context, key []byte, value []byte) error { return r.client.Set(ctx, string(key), string(value), r.eviction).Err() } -func (r *Store) Verify(_ []byte, _ []byte) error { +func (r *Store) Verify(_ context.Context, _, _ []byte) error { return nil } diff --git a/store/precomputed_key/s3/s3.go b/store/precomputed_key/s3/s3.go index 5754dac8..0c0b161f 100644 --- a/store/precomputed_key/s3/s3.go +++ b/store/precomputed_key/s3/s3.go @@ -107,7 +107,7 @@ func (s *Store) Put(ctx context.Context, key []byte, value []byte) error { return nil } -func (s *Store) Verify(key []byte, value []byte) error { +func (s *Store) Verify(_ context.Context, key []byte, value []byte) error { h := crypto.Keccak256Hash(value) if !bytes.Equal(h[:], key) { return fmt.Errorf("key does not match value, expected: %s got: %s", hex.EncodeToString(key), h.Hex()) diff --git a/store/secondary.go b/store/secondary.go index f78b9020..d8fe8b16 100644 --- a/store/secondary.go +++ b/store/secondary.go @@ -28,7 +28,7 @@ type ISecondary interface { CachingEnabled() bool FallbackEnabled() bool HandleRedundantWrites(ctx context.Context, commitment []byte, value []byte) error - MultiSourceRead(context.Context, []byte, bool, func([]byte, []byte) error) ([]byte, error) + MultiSourceRead(context.Context, []byte, bool, func(context.Context, []byte, []byte) error) ([]byte, error) WriteSubscriptionLoop(ctx context.Context) } @@ -141,7 +141,7 @@ func (sm *SecondaryManager) WriteSubscriptionLoop(ctx context.Context) { // MultiSourceRead ... reads from a set of backends and returns the first successfully read blob // NOTE: - this can also be parallelized when reading from multiple sources and discarding connections that fail // - for complete optimization we can profile secondary storage backends to determine the fastest / most reliable and always rout to it first -func (sm *SecondaryManager) MultiSourceRead(ctx context.Context, commitment []byte, fallback bool, verify func([]byte, []byte) error) ([]byte, error) { +func (sm *SecondaryManager) MultiSourceRead(ctx context.Context, commitment []byte, fallback bool, verify func(context.Context, []byte, []byte) error) ([]byte, error) { var sources []PrecomputedKeyStore if fallback { sources = sm.fallbacks @@ -167,7 +167,7 @@ func (sm *SecondaryManager) MultiSourceRead(ctx context.Context, commitment []by // verify cert:data using provided verification function sm.verifyLock.Lock() - err = verify(commitment, data) + err = verify(ctx, commitment, data) if err != nil { cb(Failed) log.Warn("Failed to verify blob", "err", err, "backend", src.BackendType()) diff --git a/store/store.go b/store/store.go index 404fedcc..8c50e8f3 100644 --- a/store/store.go +++ b/store/store.go @@ -68,7 +68,7 @@ type Store interface { // Backend returns the backend type provider of the store. BackendType() BackendType // Verify verifies the given key-value pair. - Verify(key []byte, value []byte) error + Verify(ctx context.Context, key []byte, value []byte) error } type GeneratedKeyStore interface { diff --git a/utils/utils.go b/utils/utils.go index aa83aa95..9e7cb029 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -28,20 +28,6 @@ func Contains[P comparable](s []P, e P) bool { return false } -func EqualSlices[P comparable](s1, s2 []P) bool { - if len(s1) != len(s2) { - return false - } - - for i := 0; i < len(s1); i++ { - if s1[i] != s2[i] { - return false - } - } - - return true -} - func ParseBytesAmount(s string) (uint64, error) { s = strings.TrimSpace(s) diff --git a/verify/cert.go b/verify/cert.go index b4214b70..39c42230 100644 --- a/verify/cert.go +++ b/verify/cert.go @@ -3,26 +3,28 @@ package verify import ( "bytes" "context" - "errors" "fmt" "math/big" + "time" + "github.com/Layr-Labs/eigenda/api/grpc/disperser" binding "github.com/Layr-Labs/eigenda/contracts/bindings/EigenDAServiceManager" + + "github.com/ethereum-optimism/optimism/op-service/retry" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" - + "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/log" "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 { l log.Logger ethConfirmationDepth uint64 + waitForFinalization bool manager *binding.ContractEigenDAServiceManagerCaller ethClient *ethclient.Client } @@ -49,40 +51,67 @@ func NewCertVerifier(cfg *Config, l log.Logger) (*CertVerifier, error) { }, nil } -// verifies on-chain batch ID for equivalence to certificate batch header fields -func (cv *CertVerifier) VerifyBatch( - header *binding.IEigenDAServiceManagerBatchHeader, id uint32, recordHash [32]byte, confirmationNumber uint32, +// verifyBatchConfirmedOnChain verifies that batchMetadata (typically part of a received cert) +// matches the batch metadata hash stored on-chain +func (cv *CertVerifier) verifyBatchConfirmedOnChain( + ctx context.Context, batchID uint32, batchMetadata *disperser.BatchMetadata, ) error { - blockNumber, err := cv.getConfDeepBlockNumber() + // 1. Verify batch is actually onchain at the batchMetadata's state confirmedBlockNumber. + // This is super unlikely if the disperser is honest, but it could technically happen that a confirmed batch's block gets reorged out, + // yet the tx is included in an earlier or later block, making the batchMetadata received from the disperser + // no longer valid. The eigenda batcher does check for these reorgs and updates the batch's confirmation block number: + // https://github.com/Layr-Labs/eigenda/blob/bee55ed9207f16153c3fd8ebf73c219e68685def/disperser/batcher/finalizer.go#L198 + // TODO: We could require the disperser for the new batch, or try to reconstruct it ourselves by querying the chain, + // but for now we opt to simply fail the verification, which will force the batcher to resubmit the batch to eigenda. + confirmationBlockNumber := batchMetadata.GetConfirmationBlockNumber() + confirmationBlockNumberBigInt := big.NewInt(0).SetInt64(int64(confirmationBlockNumber)) + _, err := cv.retrieveBatchMetadataHash(ctx, batchID, confirmationBlockNumberBigInt) if err != nil { - return fmt.Errorf("failed to get context block: %w", err) + return fmt.Errorf("batch not found onchain at supposedly confirmed block %d: %w", confirmationBlockNumber, err) } - // 1. ensure that a batch hash can be looked up for a batch ID for a given block number - expectedHash, err := cv.manager.BatchIdToBatchMetadataHash(&bind.CallOpts{BlockNumber: blockNumber}, id) + // 2. Verify that the confirmation status has been reached. + // The eigenda-client already checks for this, but it is possible for either + // 1. a reorg to happen, causing the batch to be confirmed by fewer number of blocks than required + // 2. proxy's node is behind the eigenda_client's node that deemed the batch confirmed, or + // even if we use the same url, that the connection drops and we get load-balanced to a different eth node. + // We retry up to 60 seconds (allowing for reorgs up to 5 blocks deep), but we only wait 3 seconds between each retry, + // in case (2) is the case and the node simply needs to resync, which could happen fast. + onchainHash, err := retry.Do(ctx, 20, retry.Fixed(3*time.Second), func() ([32]byte, error) { + blockNumber, err := cv.getConfDeepBlockNumber(ctx) + if err != nil { + return [32]byte{}, fmt.Errorf("failed to get context block: %w", err) + } + return cv.retrieveBatchMetadataHash(ctx, batchID, blockNumber) + }) if err != nil { - return fmt.Errorf("failed to get batch metadata hash: %w", err) - } - if bytes.Equal(expectedHash[:], make([]byte, 32)) { - return ErrBatchMetadataHashNotFound + return fmt.Errorf("retrieving batch that was confirmed at block %v: %w", confirmationBlockNumber, err) } - // 2. ensure that hash generated from local cert matches one stored on-chain - actualHash, err := HashBatchMetadata(header, recordHash, confirmationNumber) + // 3. Compute the hash of the batch metadata received as argument. + header := &binding.IEigenDAServiceManagerBatchHeader{ + BlobHeadersRoot: [32]byte(batchMetadata.GetBatchHeader().GetBatchRoot()), + QuorumNumbers: batchMetadata.GetBatchHeader().GetQuorumNumbers(), + ReferenceBlockNumber: batchMetadata.GetBatchHeader().GetReferenceBlockNumber(), + SignedStakeForQuorums: batchMetadata.GetBatchHeader().GetQuorumSignedPercentages(), + } + recordHash := [32]byte(batchMetadata.GetSignatoryRecordHash()) + computedHash, err := HashBatchMetadata(header, recordHash, confirmationBlockNumber) if err != nil { return fmt.Errorf("failed to hash batch metadata: %w", err) } - equal := slices.Equal(expectedHash[:], actualHash[:]) + // 4. Ensure that hash generated from local cert matches one stored on-chain. + equal := slices.Equal(onchainHash[:], computedHash[:]) if !equal { - return fmt.Errorf("batch hash mismatch, expected: %x, got: %x", expectedHash, actualHash) + return fmt.Errorf("batch hash mismatch, onchain: %x, computed: %x", onchainHash, computedHash) } return nil } // verifies the blob batch inclusion proof against the blob root hash -func (cv *CertVerifier) VerifyMerkleProof(inclusionProof []byte, root []byte, +func (cv *CertVerifier) verifyMerkleProof(inclusionProof []byte, root []byte, blobIndex uint32, blobHeader BlobHeader) error { leafHash, err := HashEncodeBlobHeader(blobHeader) if err != nil { @@ -103,10 +132,37 @@ func (cv *CertVerifier) VerifyMerkleProof(inclusionProof []byte, root []byte, } // fetches a block number provided a subtraction of a user defined conf depth from latest block -func (cv *CertVerifier) getConfDeepBlockNumber() (*big.Int, error) { - blockNumber, err := cv.ethClient.BlockNumber(context.Background()) +func (cv *CertVerifier) getConfDeepBlockNumber(ctx context.Context) (*big.Int, error) { + if cv.waitForFinalization { + var header = types.Header{} + // We ask for the latest finalized block. The second parameter "hydrated txs" is set to false because we don't need full txs. + // See https://github.com/ethereum/execution-apis/blob/4140e528360fea53c34a766d86a000c6c039100e/src/eth/block.yaml#L61 + // This is equivalent to `cast block finalized`, as opposed to `cast block finalized --full`. + err := cv.ethClient.Client().CallContext(ctx, &header, "eth_getBlockByNumber", "finalized", false) + if err != nil { + return nil, fmt.Errorf("failed to get finalized block: %w", err) + } + return header.Number, nil + } + blockNumber, err := cv.ethClient.BlockNumber(ctx) if err != nil { return nil, fmt.Errorf("failed to get latest block number: %w", err) } - return new(big.Int).SetUint64(max(blockNumber-cv.ethConfirmationDepth, 0)), nil + if blockNumber < cv.ethConfirmationDepth { + return big.NewInt(0), nil + } + return new(big.Int).SetUint64(blockNumber - cv.ethConfirmationDepth), nil +} + +// retrieveBatchMetadataHash retrieves the batch metadata hash stored on-chain at a specific blockNumber for a given batchID +// returns an error if some problem calling the contract happens, or the hash is not found +func (cv *CertVerifier) retrieveBatchMetadataHash(ctx context.Context, batchID uint32, blockNumber *big.Int) ([32]byte, error) { + onchainHash, err := cv.manager.BatchIdToBatchMetadataHash(&bind.CallOpts{Context: ctx, BlockNumber: blockNumber}, batchID) + if err != nil { + return [32]byte{}, fmt.Errorf("calling EigenDAServiceManager.BatchIdToBatchMetadataHash: %w", err) + } + if bytes.Equal(onchainHash[:], make([]byte, 32)) { + return [32]byte{}, fmt.Errorf("BatchMetadataHash not found for BatchId %d at block %d", batchID, blockNumber.Uint64()) + } + return onchainHash, nil } diff --git a/verify/cli.go b/verify/cli.go index f41135f4..fb7e8a18 100644 --- a/verify/cli.go +++ b/verify/cli.go @@ -4,9 +4,11 @@ import ( "fmt" "runtime" + "github.com/urfave/cli/v2" + "github.com/Layr-Labs/eigenda-proxy/utils" + "github.com/Layr-Labs/eigenda/api/clients" "github.com/Layr-Labs/eigenda/encoding/kzg" - "github.com/urfave/cli/v2" ) var ( @@ -23,9 +25,6 @@ const srsOrder = 268435456 // 2 ^ 32 var ( // cert verification flags CertVerificationDisabledFlagName = withFlagPrefix("cert-verification-disabled") - EthRPCFlagName = withFlagPrefix("eth-rpc") - SvcManagerAddrFlagName = withFlagPrefix("svc-manager-addr") - EthConfirmationDepthFlagName = withFlagPrefix("eth-confirmation-depth") // kzg flags G1PathFlagName = withFlagPrefix("g1-path") @@ -55,25 +54,6 @@ func CLIFlags(envPrefix, category string) []cli.Flag { Value: false, Category: category, }, - &cli.StringFlag{ - Name: EthRPCFlagName, - Usage: "JSON RPC node endpoint for the Ethereum network used for finalizing DA blobs. See available list here: https://docs.eigenlayer.xyz/eigenda/networks/", - EnvVars: []string{withEnvPrefix(envPrefix, "ETH_RPC")}, - Category: category, - }, - &cli.StringFlag{ - Name: SvcManagerAddrFlagName, - 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: []string{withEnvPrefix(envPrefix, "SERVICE_MANAGER_ADDR")}, - Category: category, - }, - &cli.Uint64Flag{ - Name: EthConfirmationDepthFlagName, - Usage: "The number of Ethereum blocks to wait before considering a submitted blob's DA batch submission confirmed. `0` means wait for inclusion only.", - EnvVars: []string{withEnvPrefix(envPrefix, "ETH_CONFIRMATION_DEPTH")}, - Value: 0, - Category: category, - }, // kzg flags &cli.StringFlag{ Name: G1PathFlagName, @@ -137,7 +117,10 @@ func CLIFlags(envPrefix, category string) []cli.Flag { // this var is set by the action in the MaxBlobLengthFlagName flag var MaxBlobLengthBytes uint64 -func ReadConfig(ctx *cli.Context) Config { +// ReadConfig takes an eigendaClientConfig as input because the verifier config +// reuses some configs that are also used by the eigenda client. +// Not sure if urfave has a way to do flag aliases so opted for this approach. +func ReadConfig(ctx *cli.Context, edaClientConfig clients.EigenDAClientConfig) Config { kzgCfg := &kzg.KzgConfig{ G1Path: ctx.String(G1PathFlagName), G2PowerOf2Path: ctx.String(G2PowerOf2PathFlagName), @@ -148,10 +131,12 @@ func ReadConfig(ctx *cli.Context) Config { } return Config{ - KzgConfig: kzgCfg, - VerifyCerts: !ctx.Bool(CertVerificationDisabledFlagName), - RPCURL: ctx.String(EthRPCFlagName), - SvcManagerAddr: ctx.String(SvcManagerAddrFlagName), - EthConfirmationDepth: uint64(ctx.Int64(EthConfirmationDepthFlagName)), // #nosec G115 + KzgConfig: kzgCfg, + VerifyCerts: !ctx.Bool(CertVerificationDisabledFlagName), + // reuse some configs from the eigenda client + RPCURL: edaClientConfig.EthRpcUrl, + SvcManagerAddr: edaClientConfig.SvcManagerAddr, + EthConfirmationDepth: edaClientConfig.WaitForConfirmationDepth, + WaitForFinalization: edaClientConfig.WaitForFinalization, } } diff --git a/verify/deprecated_flags.go b/verify/deprecated_flags.go index 5fe78d8a..6c4f88c3 100644 --- a/verify/deprecated_flags.go +++ b/verify/deprecated_flags.go @@ -3,6 +3,7 @@ package verify import ( "fmt" + "github.com/Layr-Labs/eigenda-proxy/flags/eigendaflags" "github.com/urfave/cli/v2" ) @@ -41,7 +42,7 @@ func DeprecatedCLIFlags(envPrefix, category string) []cli.Flag { Action: func(_ *cli.Context, _ string) error { return fmt.Errorf("flag --%s (env var %s) is deprecated, use --%s (env var %s) instead", DeprecatedEthRPCFlagName, withDeprecatedEnvPrefix(envPrefix, "ETH_RPC"), - EthRPCFlagName, withEnvPrefix(envPrefix, "ETH_RPC")) + eigendaflags.EthRPCURLFlagName, withEnvPrefix(envPrefix, "ETH_RPC")) }, Category: category, }, @@ -52,7 +53,7 @@ func DeprecatedCLIFlags(envPrefix, category string) []cli.Flag { Action: func(_ *cli.Context, _ string) error { return fmt.Errorf("flag --%s (env var %s) is deprecated, use --%s (env var %s) instead", DeprecatedSvcManagerAddrFlagName, withDeprecatedEnvPrefix(envPrefix, "SERVICE_MANAGER_ADDR"), - SvcManagerAddrFlagName, withEnvPrefix(envPrefix, "SERVICE_MANAGER_ADDR")) + eigendaflags.SvcManagerAddrFlagName, withEnvPrefix(envPrefix, "SERVICE_MANAGER_ADDR")) }, Category: category, }, @@ -64,7 +65,7 @@ func DeprecatedCLIFlags(envPrefix, category string) []cli.Flag { Action: func(_ *cli.Context, _ uint64) error { return fmt.Errorf("flag --%s (env var %s) is deprecated, use --%s (env var %s) instead", DeprecatedEthConfirmationDepthFlagName, withDeprecatedEnvPrefix(envPrefix, "ETH_CONFIRMATION_DEPTH"), - EthConfirmationDepthFlagName, withEnvPrefix(envPrefix, "ETH_CONFIRMATION_DEPTH")) + eigendaflags.ConfirmationDepthFlagName, withEnvPrefix(envPrefix, "CONFIRMATION_DEPTH")) }, Category: category, }, diff --git a/verify/verifier.go b/verify/verifier.go index e42d3375..b825194d 100644 --- a/verify/verifier.go +++ b/verify/verifier.go @@ -1,6 +1,7 @@ package verify import ( + "context" "fmt" "math/big" @@ -10,9 +11,8 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/log" - binding "github.com/Layr-Labs/eigenda/contracts/bindings/EigenDAServiceManager" - "github.com/Layr-Labs/eigenda/api/grpc/common" + "github.com/Layr-Labs/eigenda/api/grpc/disperser" "github.com/Layr-Labs/eigenda/encoding/kzg" kzgverifier "github.com/Layr-Labs/eigenda/encoding/kzg/verifier" "github.com/Layr-Labs/eigenda/encoding/rs" @@ -25,6 +25,7 @@ type Config struct { RPCURL string SvcManagerAddr string EthConfirmationDepth uint64 + WaitForFinalization bool } // TODO: right now verification and confirmation depth are tightly coupled. we should decouple them @@ -60,32 +61,26 @@ func NewVerifier(cfg *Config, l log.Logger) (*Verifier, error) { } // verifies V0 eigenda certificate type -func (v *Verifier) VerifyCert(cert *Certificate) error { +func (v *Verifier) VerifyCert(ctx context.Context, cert *Certificate) error { if !v.verifyCerts { return nil } - // 1 - verify batch - header := binding.IEigenDAServiceManagerBatchHeader{ - BlobHeadersRoot: [32]byte(cert.Proof().GetBatchMetadata().GetBatchHeader().GetBatchRoot()), - QuorumNumbers: cert.Proof().GetBatchMetadata().GetBatchHeader().GetQuorumNumbers(), - ReferenceBlockNumber: cert.Proof().GetBatchMetadata().GetBatchHeader().GetReferenceBlockNumber(), - SignedStakeForQuorums: cert.Proof().GetBatchMetadata().GetBatchHeader().GetQuorumSignedPercentages(), - } - - err := v.cv.VerifyBatch(&header, cert.Proof().GetBatchId(), [32]byte(cert.Proof().BatchMetadata.GetSignatoryRecordHash()), cert.Proof().BatchMetadata.GetConfirmationBlockNumber()) + // 1 - verify batch in the cert is confirmed onchain + err := v.cv.verifyBatchConfirmedOnChain(ctx, cert.Proof().GetBatchId(), cert.Proof().GetBatchMetadata()) if err != nil { return fmt.Errorf("failed to verify batch: %w", err) } // 2 - verify merkle inclusion proof - err = v.cv.VerifyMerkleProof(cert.Proof().GetInclusionProof(), cert.BatchHeaderRoot(), cert.Proof().GetBlobIndex(), cert.ReadBlobHeader()) + err = v.cv.verifyMerkleProof(cert.Proof().GetInclusionProof(), cert.BatchHeaderRoot(), cert.Proof().GetBlobIndex(), cert.ReadBlobHeader()) if err != nil { return fmt.Errorf("failed to verify merkle proof: %w", err) } // 3 - verify security parameters - err = v.VerifySecurityParams(cert.ReadBlobHeader(), header) + batchHeader := cert.Proof().GetBatchMetadata().GetBatchHeader() + err = v.verifySecurityParams(cert.ReadBlobHeader(), batchHeader) if err != nil { return fmt.Errorf("failed to verify security parameters: %w", err) } @@ -138,8 +133,8 @@ func (v *Verifier) VerifyCommitment(expectedCommit *common.G1Commitment, blob [] return nil } -// VerifySecurityParams ensures that returned security parameters are valid -func (v *Verifier) VerifySecurityParams(blobHeader BlobHeader, batchHeader binding.IEigenDAServiceManagerBatchHeader) error { +// verifySecurityParams ensures that returned security parameters are valid +func (v *Verifier) verifySecurityParams(blobHeader BlobHeader, batchHeader *disperser.BatchHeader) error { confirmedQuorums := make(map[uint8]bool) // require that the security param in each blob is met @@ -163,7 +158,7 @@ func (v *Verifier) VerifySecurityParams(blobHeader BlobHeader, batchHeader bindi return fmt.Errorf("adversary threshold percentage must be greater than or equal to quorum adversary threshold percentage") } - if batchHeader.SignedStakeForQuorums[i] < blobHeader.QuorumBlobParams[i].ConfirmationThresholdPercentage { + if batchHeader.QuorumSignedPercentages[i] < blobHeader.QuorumBlobParams[i].ConfirmationThresholdPercentage { return fmt.Errorf("signed stake for quorum must be greater than or equal to confirmation threshold percentage") }