Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: modify verifier to not require eth archive node #241

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This summary feels like something better encapsulated in release notes and within our README


### Deployment Steps

```bash
Expand Down
9 changes: 7 additions & 2 deletions flags/eigendaflags/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package eigendaflags

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

Expand Down Expand Up @@ -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")},
Expand Down Expand Up @@ -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
Expand Down
80 changes: 74 additions & 6 deletions verify/cert.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"context"
"fmt"
"math"
"math/big"
"time"

Expand All @@ -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).
Comment on lines +27 to +30
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// 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 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.
// 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)
Expand All @@ -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
}

Expand Down Expand Up @@ -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.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we poll the latest block number and ensure it's greater than the confirmation block number?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i.e, do we care about the case where the RPC node is far behind?

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 +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
}
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
}
Loading