Skip to content

Commit

Permalink
chore: modify verifier to not require eth archive node (#241)
Browse files Browse the repository at this point in the history
* chore: force verifier's EthConfirmationDepth to be <64

We panic in the flag's action, as well as in the verifier's constructor when this condition is not respected.

This will make sure that an archival node is not required.

* chore: modify verifier to load quorum parameters only once at initialization

This removes the need for running with an eth archive node.

* style: fix minor lint issue

* docs: update README to mention that archival node is no longer needed

* docs: clean-up README archival node requirement explanation

* docs: fix verify/cert.go comment typo

Co-authored-by: Ethen <[email protected]>

* docs: for eg -> e.g.

* style(cert): remove unecessary bound checks from inside loop

* style: create consts package with EthHappyPathFinalizationDepthBlocks = 64

* style: change panic into error return

* docs: change op reference for eth reference

* docs: make flag comment simpler

* Update verify/cert.go

Co-authored-by: EthenNotEthan <[email protected]>

---------

Co-authored-by: Ethen <[email protected]>
  • Loading branch information
samlaf and ethenotethan authored Jan 23, 2025
1 parent 075b283 commit deac7be
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 39 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,12 @@ The following specs are recommended for running on a single production server:
* 4 GB RAM
* 1-2 cores CPU

### Ethereum Node Requirements

A normal (non-archival) Ethereum node is sufficient for running the proxy with [cert verification](#certificate-verification) turned on. This is because all parameters that are read from the chain are either:
1. immutable (eg: [securityThresholds](https://github.com/Layr-Labs/eigenda/blob/a6dd724acdf732af483fd2d9a86325febe7ebdcd/contracts/src/core/EigenDAThresholdRegistryStorage.sol#L30)), or
2. are upgradeable but have all the historical versions available in contract storage (eg: [versioninedBlobParams](https://github.com/Layr-Labs/eigenda/blob/a6dd724acdf732af483fd2d9a86325febe7ebdcd/contracts/src/core/EigenDAThresholdRegistryStorage.sol#L27))

### Deployment Steps

```bash
Expand Down
8 changes: 8 additions & 0 deletions common/consts/consts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package consts

// EthHappyPathFinalizationDepth is the number of blocks that must be included on top of a block for it to be considered "final",
// under happy-path aka normal network conditions.
//
// See https://www.alchemy.com/overviews/ethereum-commitment-levels for a quick TLDR explanation,
// or https://eth2book.info/capella/part3/transition/epoch/#finalisation for full details.
var EthHappyPathFinalizationDepthBlocks = uint8(64)
11 changes: 8 additions & 3 deletions flags/eigendaflags/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ package eigendaflags

import (
"fmt"
"log"
"strconv"
"time"

"github.com/Layr-Labs/eigenda-proxy/common/consts"
"github.com/Layr-Labs/eigenda/api/clients"
"github.com/Layr-Labs/eigenda/api/clients/codecs"
"github.com/urfave/cli/v2"
Expand Down Expand Up @@ -118,6 +118,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")},
Expand Down Expand Up @@ -209,8 +210,12 @@ func validateConfirmationFlag(val string) error {
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)
if depth >= uint64(consts.EthHappyPathFinalizationDepthBlocks) {
// 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 >64.
// But perhaps someone testing crazy reorg scenarios where finalization takes >2 epochs might want to set this to a higher number.
// 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.
return fmt.Errorf("confirmation depth set to %d, which is > 2 epochs (64). Use 'finalized' instead", depth)
}

return nil
Expand Down
78 changes: 72 additions & 6 deletions verify/cert.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import (
"bytes"
"context"
"fmt"
"math"
"math/big"
"time"

"github.com/Layr-Labs/eigenda-proxy/common/consts"
"github.com/Layr-Labs/eigenda/api/grpc/disperser"
binding "github.com/Layr-Labs/eigenda/contracts/bindings/EigenDAServiceManager"

Expand All @@ -22,14 +24,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 used to verify that a blob's batch commitment 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 (consts.EthHappyPathFinalizationDepthBlocks).
// 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 >= uint64(consts.EthHappyPathFinalizationDepthBlocks) {
// 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)
Expand All @@ -43,11 +58,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
}

Expand Down Expand Up @@ -155,7 +177,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 {
Expand All @@ -166,3 +191,44 @@ 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 (e.g. 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)
}
if len(thresholds) > math.MaxUint8 {
return nil, nil, fmt.Errorf("thresholds received from EigenDAServiceManager contains %d > 256 quorums, which isn't possible", len(thresholds))
}
var quorumAdversaryThresholds = make(map[uint8]uint8)
for quorumNum, threshold := range thresholds {
quorumAdversaryThresholds[uint8(quorumNum)] = threshold //nolint:gosec // disable G115 // We checked the length of thresholds above
}
// 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
}
36 changes: 6 additions & 30 deletions verify/verifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand Down Expand Up @@ -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")
}

Expand All @@ -186,32 +182,12 @@ 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)
}
}

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
}

0 comments on commit deac7be

Please sign in to comment.