diff --git a/README.md b/README.md index bb0a5c8..b882a83 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,9 @@ The following specs are recommended for running on a single production server: * 4 GB RAM * 1-2 cores CPU +### Ethereum Node Requirements +The cert verification logic inside proxy used to require an archive node to fetch quorum information at reference block numbers in the past. We have removed this requirement by making the quorum parameters immutable in the EigenDAServiceManager contract. This means that a normal Ethereum node can now be used to run the proxy. See https://github.com/Layr-Labs/eigenda-proxy/issues/230 for more details. + ### Deployment Steps ```bash diff --git a/flags/eigendaflags/cli.go b/flags/eigendaflags/cli.go index 060f555..77bba37 100644 --- a/flags/eigendaflags/cli.go +++ b/flags/eigendaflags/cli.go @@ -2,7 +2,6 @@ package eigendaflags import ( "fmt" - "log" "strconv" "time" @@ -118,6 +117,7 @@ func CLIFlags(envPrefix, category string) []cli.Flag { Category: category, }, &cli.BoolFlag{ + // This flag is DEPRECATED. Use ConfirmationDepthFlagName, which accept "finalization" or a number <64. Name: WaitForFinalizationFlagName, Usage: "Wait for blob finalization before returning from PutBlob.", EnvVars: []string{withEnvPrefix(envPrefix, "WAIT_FOR_FINALIZATION")}, @@ -210,7 +210,12 @@ func validateConfirmationFlag(val string) error { } if depth >= 64 { - log.Printf("Warning: confirmation depth set to %d, which is > 2 epochs (64). Consider using 'finalized' instead.\n", depth) + // We keep this low (<128) to avoid requiring an archive node (see how this is used in CertVerifier). + // Note: assuming here that no sane person would ever need to set this to a number to something >64. + // But perhaps someone testing crazy reorg scenarios where finalization takes >2 epochs might want to set this to a higher number...? + // Let's deal with that case if and when it comes up (ideally never). Do keep in mind if you ever change this + // that it might affect a LOT of validators on your rollup who would now need an archival node. + panic(fmt.Sprintf("Warning: confirmation depth set to %d, which is > 2 epochs (64). Use 'finalized' instead.\n", depth)) } return nil diff --git a/verify/cert.go b/verify/cert.go index 39c4223..c533a33 100644 --- a/verify/cert.go +++ b/verify/cert.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "fmt" + "math" "math/big" "time" @@ -22,14 +23,27 @@ import ( // 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 + l log.Logger + // ethConfirmationDepth is using to verify that a blob's batch has been bridged to the EigenDAServiceManager contract at least + // this many blocks in the past. To do so we make an eth_call to the contract at the current block_number - ethConfirmationDepth. + // Hence in order to not require an archive node, this value should be kept low. We force it to be < 64. + // waitForFinalization should be used instead of ethConfirmationDepth if the user wants to wait for finality (typically 64 blocks in happy case). ethConfirmationDepth uint64 waitForFinalization bool manager *binding.ContractEigenDAServiceManagerCaller ethClient *ethclient.Client + // The two fields below are fetched from the EigenDAServiceManager contract in the constructor. + // They are used to verify the quorums in the received certificates. + // See getQuorumParametersAtLatestBlock for more details. + quorumsRequired []uint8 + quorumAdversaryThresholds map[uint8]uint8 } func NewCertVerifier(cfg *Config, l log.Logger) (*CertVerifier, error) { + if cfg.EthConfirmationDepth >= 64 { + // We keep this low (<128) to avoid requiring an archive node. + return nil, fmt.Errorf("confirmation depth must be less than 64; consider using cfg.WaitForFinalization=true instead") + } log.Info("Enabling certificate verification", "confirmation_depth", cfg.EthConfirmationDepth) client, err := ethclient.Dial(cfg.RPCURL) @@ -43,11 +57,18 @@ func NewCertVerifier(cfg *Config, l log.Logger) (*CertVerifier, error) { return nil, err } + quorumsRequired, quorumAdversaryThresholds, err := getQuorumParametersAtLatestBlock(m) + if err != nil { + return nil, fmt.Errorf("failed to fetch quorum parameters from EigenDAServiceManager: %w", err) + } + return &CertVerifier{ - l: l, - manager: m, - ethConfirmationDepth: cfg.EthConfirmationDepth, - ethClient: client, + l: l, + manager: m, + ethConfirmationDepth: cfg.EthConfirmationDepth, + ethClient: client, + quorumsRequired: quorumsRequired, + quorumAdversaryThresholds: quorumAdversaryThresholds, }, nil } @@ -155,7 +176,10 @@ func (cv *CertVerifier) getConfDeepBlockNumber(ctx context.Context) (*big.Int, e } // 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 +// returns an error if some problem calling the contract happens, or the hash is not found. +// We make an eth_call to the EigenDAServiceManager at the given blockNumber to retrieve the hash. +// Therefore, make sure that blockNumber is <128 blocks behind the latest block, to avoid requiring an archive node. +// This is currently enforced by having EthConfirmationDepth be <64. 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 { @@ -166,3 +190,47 @@ func (cv *CertVerifier) retrieveBatchMetadataHash(ctx context.Context, batchID u } return onchainHash, nil } + +// getQuorumParametersAtLatestBlock fetches the required quorums and quorum adversary thresholds +// from the EigenDAServiceManager contract at the latest block. +// We then cache these parameters and use them in the Verifier to verify the certificates. +// +// Note: this strategy (fetching once and caching) only works because these parameters are immutable. +// They might be different in different environments (for eg on a devnet or testnet), but they are fixed on a given network. +// We used to allow these parameters to change (via a setter function on the contract), but that then forced us here in the proxy +// to query for these parameters on every request, at the batch's reference block number (RBN). +// This in turn required rollup validators running this proxy to have an archive node, in case the RBN was >128 blocks in the past, +// which was not ideal. So we decided to make these parameters immutable, and cache them here. +func getQuorumParametersAtLatestBlock( + manager *binding.ContractEigenDAServiceManagerCaller, +) ([]uint8, map[uint8]uint8, error) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + requiredQuorums, err := manager.QuorumNumbersRequired(&bind.CallOpts{Context: ctx}) + if err != nil { + return nil, nil, fmt.Errorf("failed to fetch QuorumNumbersRequired from EigenDAServiceManager: %w", err) + } + ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + thresholds, err := manager.QuorumAdversaryThresholdPercentages(&bind.CallOpts{Context: ctx}) + if err != nil { + return nil, nil, fmt.Errorf("failed to fetch QuorumAdversaryThresholdPercentages from EigenDAServiceManager: %w", err) + } + var quorumAdversaryThresholds = make(map[uint8]uint8) + for quorumNum, threshold := range thresholds { + if quorumNum > math.MaxInt8 { + return nil, nil, fmt.Errorf("quorum number %d is too large to fit in int8", quorumNum) + } + if quorumNum < 0 { + return nil, nil, fmt.Errorf("quorum number %d cannot be negative", quorumNum) + } + quorumAdversaryThresholds[uint8(quorumNum)] = threshold + } + // Sanity check: ensure that the required quorums are a subset of the quorums for which we have adversary thresholds + for _, quorum := range requiredQuorums { + if _, ok := quorumAdversaryThresholds[quorum]; !ok { + return nil, nil, fmt.Errorf("required quorum %d does not have an adversary threshold. Was the EigenDAServiceManager properly deployed?", quorum) + } + } + return requiredQuorums, quorumAdversaryThresholds, nil +} diff --git a/verify/verifier.go b/verify/verifier.go index 84f3863..7454a84 100644 --- a/verify/verifier.go +++ b/verify/verifier.go @@ -4,12 +4,10 @@ import ( "context" "encoding/json" "fmt" - "math/big" "github.com/consensys/gnark-crypto/ecc" "github.com/consensys/gnark-crypto/ecc/bn254" "github.com/consensys/gnark-crypto/ecc/bn254/fp" - "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/log" "github.com/Layr-Labs/eigenda/api/grpc/common" @@ -22,7 +20,7 @@ import ( type Config struct { KzgConfig *kzg.KzgConfig VerifyCerts bool - // below 3 fields are only required if VerifyCerts is true + // below fields are only required if VerifyCerts is true RPCURL string SvcManagerAddr string EthConfirmationDepth uint64 @@ -170,12 +168,10 @@ func (v *Verifier) verifySecurityParams(blobHeader BlobHeader, batchHeader *disp // we get the quorum adversary threshold at the batch's reference block number. This is not strictly needed right now // since this threshold is hardcoded into the contract: https://github.com/Layr-Labs/eigenda/blob/master/contracts/src/core/EigenDAServiceManagerStorage.sol // but it is good practice in case the contract changes in the future - quorumAdversaryThreshold, err := v.getQuorumAdversaryThreshold(blobHeader.QuorumBlobParams[i].QuorumNumber, int64(batchHeader.ReferenceBlockNumber)) - if err != nil { - log.Warn("failed to get quorum adversary threshold", "err", err) - } - - if quorumAdversaryThreshold > 0 && blobHeader.QuorumBlobParams[i].AdversaryThresholdPercentage < quorumAdversaryThreshold { + quorumAdversaryThreshold, ok := v.cv.quorumAdversaryThresholds[blobHeader.QuorumBlobParams[i].QuorumNumber] + if !ok { + log.Warn("CertVerifier.quorumAdversaryThresholds map does not contain quorum number", "quorumNumber", blobHeader.QuorumBlobParams[i].QuorumNumber) + } else if blobHeader.QuorumBlobParams[i].AdversaryThresholdPercentage < quorumAdversaryThreshold { return fmt.Errorf("adversary threshold percentage must be greater than or equal to quorum adversary threshold percentage") } @@ -186,13 +182,8 @@ func (v *Verifier) verifySecurityParams(blobHeader BlobHeader, batchHeader *disp confirmedQuorums[blobHeader.QuorumBlobParams[i].QuorumNumber] = true } - requiredQuorums, err := v.cv.manager.QuorumNumbersRequired(&bind.CallOpts{BlockNumber: big.NewInt(int64(batchHeader.ReferenceBlockNumber))}) - if err != nil { - log.Warn("failed to get required quorum numbers at block number", "err", err, "referenceBlockNumber", batchHeader.ReferenceBlockNumber) - } - // ensure that required quorums are present in the confirmed ones - for _, quorum := range requiredQuorums { + for _, quorum := range v.cv.quorumsRequired { if !confirmedQuorums[quorum] { return fmt.Errorf("quorum %d is required but not present in confirmed quorums", quorum) } @@ -200,18 +191,3 @@ func (v *Verifier) verifySecurityParams(blobHeader BlobHeader, batchHeader *disp return nil } - -// getQuorumAdversaryThreshold reads the adversarial threshold percentage for a given quorum number, -// at a given block number. If the quorum number does not exist, it returns 0. -func (v *Verifier) getQuorumAdversaryThreshold(quorumNum uint8, blockNumber int64) (uint8, error) { - percentages, err := v.cv.manager.QuorumAdversaryThresholdPercentages(&bind.CallOpts{BlockNumber: big.NewInt(blockNumber)}) - if err != nil { - return 0, err - } - - if len(percentages) > int(quorumNum) { - return percentages[quorumNum], nil - } - - return 0, nil -}