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

feat: Batch hash verification #39

Merged
merged 2 commits into from
Jun 20, 2024
Merged
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: 2 additions & 1 deletion .github/workflows/actions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,5 +79,6 @@ jobs:
- name: Run holesky tests
env:
SIGNER_PRIVATE_KEY: ${{ secrets.SIGNER_PRIVATE_KEY }}
ETHEREUM_RPC: ${{ secrets.ETHEREUM_RPC }}
run: |
SIGNER_PRIVATE_KEY=$SIGNER_PRIVATE_KEY make holesky-test
SIGNER_PRIVATE_KEY=$SIGNER_PRIVATE_KEY ETHEREUM_RPC=$ETHEREUM_RPC make holesky-test
9 changes: 9 additions & 0 deletions e2e/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (

const (
privateKey = "SIGNER_PRIVATE_KEY"
ethRPC = "ETHEREUM_RPC"
transport = "http"
svcName = "eigenda_proxy"
host = "127.0.0.1"
Expand All @@ -43,6 +44,12 @@ func CreateTestSuite(t *testing.T, useMemory bool) (TestSuite, func()) {
t.Fatal("SIGNER_PRIVATE_KEY environment variable not set")
}

// load node url from environment
ethRPC := os.Getenv(ethRPC)
if ethRPC != "" && !useMemory {
t.Fatal("ETHEREUM_RPC environment variable is not set")
}

log := oplog.NewLogger(os.Stdout, oplog.CLIConfig{
Level: log.LevelDebug,
Format: oplog.FormatLogFmt,
Expand All @@ -57,6 +64,8 @@ func CreateTestSuite(t *testing.T, useMemory bool) (TestSuite, func()) {
DisableTLS: false,
SignerPrivateKeyHex: pk,
},
EthRPC: ethRPC,
SvcManagerAddr: "0xD4A7E1Bd8015057293f0D0A557088c286942e84b", // incompatible with non holeskly networks
CacheDir: "../operator-setup/resources/SRSTables",
G1Path: "../operator-setup/resources/g1_abbr.point",
G2Path: "../test/resources/kzg/g2.point", // do we need this?
Expand Down
39 changes: 37 additions & 2 deletions eigenda/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"time"

"github.com/Layr-Labs/eigenda-proxy/common"
"github.com/Layr-Labs/eigenda-proxy/verify"
"github.com/Layr-Labs/eigenda/api/clients"
"github.com/Layr-Labs/eigenda/api/clients/codecs"
"github.com/Layr-Labs/eigenda/encoding/kzg"
Expand All @@ -16,6 +17,8 @@ import (

const (
RPCFlagName = "eigenda-rpc"
EthRPCFlagName = "eigenda-eth-rpc"
SvcManagerAddrFlagName = "eigenda-svc-manager-addr"
StatusQueryRetryIntervalFlagName = "eigenda-status-query-retry-interval"
StatusQueryTimeoutFlagName = "eigenda-status-query-timeout"
DisableTlsFlagName = "eigenda-disable-tls"
Expand Down Expand Up @@ -44,6 +47,10 @@ type Config struct {
// The blob encoding version to use when writing blobs from the high level interface.
PutBlobEncodingVersion codecs.BlobEncodingVersion

// ETH vars
EthRPC string
SvcManagerAddr string
Copy link
Contributor

Choose a reason for hiding this comment

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

We should update the readme with a table of these addresses across Holesky and Mainnet, or even better link to the canonical EigenDA contract readme where those addresses are written down.


// KZG vars
CacheDir string

Expand Down Expand Up @@ -73,21 +80,37 @@ func (c *Config) GetMaxBlobLength() (uint64, error) {
return c.maxBlobLengthBytes, nil
}

func (c *Config) KzgConfig() *kzg.KzgConfig {
func (c *Config) VerificationCfg() *verify.Config {
Copy link
Contributor

@teddyknox teddyknox Jun 18, 2024

Choose a reason for hiding this comment

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

Nit: Never understood why people abbreviate 'config' to remove 3 letters. Feel free to ignore.

numBytes, err := c.GetMaxBlobLength()
if err != nil {
panic(fmt.Errorf("Check() was not called on config object, err is not nil: %w", err))
}

numPointsNeeded := uint64(math.Ceil(float64(numBytes) / BytesPerSymbol))
return &kzg.KzgConfig{

kzgCfg := &kzg.KzgConfig{
G1Path: c.G1Path,
G2PowerOf2Path: c.G2PowerOfTauPath,
CacheDir: c.CacheDir,
SRSOrder: numPointsNeeded,
SRSNumberToLoad: numPointsNeeded,
NumWorker: uint64(runtime.GOMAXPROCS(0)),
}

if c.EthRPC == "" || c.SvcManagerAddr == "" {
return &verify.Config{
Verify: false,
KzgConfig: kzgCfg,
}
}

return &verify.Config{
Verify: true,
RPCURL: c.EthRPC,
SvcManagerAddr: c.SvcManagerAddr,
KzgConfig: kzgCfg,
}

}

// NewConfig parses the Config from the provided flags or environment variables.
Expand All @@ -109,6 +132,8 @@ func ReadConfig(ctx *cli.Context) Config {
G2PowerOfTauPath: ctx.String(G2TauFlagName),
CacheDir: ctx.String(CachePathFlagName),
MaxBlobLength: ctx.String(MaxBlobLengthFlagName),
SvcManagerAddr: ctx.String(SvcManagerAddrFlagName),
EthRPC: ctx.String(EthRPCFlagName),
}
return cfg
}
Expand Down Expand Up @@ -199,5 +224,15 @@ func CLIFlags(envPrefix string) []cli.Flag {
Usage: "Directory path to SRS tables",
EnvVars: prefixEnvVars("TARGET_CACHE_PATH"),
},
&cli.StringFlag{
Name: EthRPCFlagName,
Usage: "JSON RPC node endpoint for the Ethereum network used for finalizing DA blobs.",
Copy link
Contributor

Choose a reason for hiding this comment

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

Thoughts on putting an example RPC url in the description and a default value for a public Holesky RPC?

EnvVars: prefixEnvVars("ETH_RPC"),
},
&cli.StringFlag{
Name: SvcManagerAddrFlagName,
Usage: "Deployed EigenDA service manager address.",
EnvVars: prefixEnvVars("SERVICE_MANAGER_ADDR"),
Copy link
Contributor

Choose a reason for hiding this comment

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

A default value for the Holesky address might be nice.

Copy link
Contributor

@teddyknox teddyknox Jun 18, 2024

Choose a reason for hiding this comment

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

I'm realizing it might be good to check into the repo certain "configuration profiles" representing good defaults for testnet and mainnet. Maybe networks/mainnet.env and networks/holesky.env. To make it the invocation of ./bin/eigenda-proxy solid state (a one liner) we would need to add a special command line flag --env-file <filepath> that let you use one of these files for settings.

},
}
}
11 changes: 9 additions & 2 deletions server/load_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,20 @@ import (
)

func LoadStore(cfg CLIConfig, ctx context.Context, log log.Logger) (store.Store, error) {
log.Info("Using eigenda backend")
daCfg := cfg.EigenDAConfig
vCfg := daCfg.VerificationCfg()

verifier, err := verify.NewVerifier(daCfg.KzgConfig())
verifier, err := verify.NewVerifier(vCfg, log)
if err != nil {
return nil, err
}

if vCfg.Verify {
log.Info("Certificate verification with Ethereum enabled")
} else {
log.Warn("Verification disabled")
}

maxBlobLength, err := daCfg.GetMaxBlobLength()
if err != nil {
return nil, err
Expand All @@ -28,6 +34,7 @@ func LoadStore(cfg CLIConfig, ctx context.Context, log log.Logger) (store.Store,
return store.NewMemStore(ctx, &cfg.MemStoreCfg, verifier, log, maxBlobLength)
}

log.Info("Using eigenda backend")
Copy link
Contributor

@teddyknox teddyknox Jun 18, 2024

Choose a reason for hiding this comment

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

nit: 'EigenDA' in any user-facing outputs

client, err := clients.NewEigenDAClient(log, daCfg.ClientConfig)
if err != nil {
return nil, err
Expand Down
13 changes: 9 additions & 4 deletions store/eigenda.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,15 @@ func (e EigenDAStore) Get(ctx context.Context, key []byte, domain common.DomainT
// reencode blob for verification
encodedBlob, err := e.client.GetCodec().EncodeBlob(decodedBlob)
if err != nil {
return nil, fmt.Errorf("EigenDA client failed to reencode blob: %w", err)
return nil, fmt.Errorf("EigenDA client failed to re-encode blob: %w", err)
}

err = e.verifier.Verify(cert.BlobHeader.Commitment, encodedBlob)
err = e.verifier.VerifyCommitment(cert.BlobHeader.Commitment, encodedBlob)
if err != nil {
return nil, err
}

err = e.verifier.VerifyCert(&cert)
if err != nil {
return nil, err
}
Expand All @@ -73,9 +78,9 @@ func (e EigenDAStore) Put(ctx context.Context, value []byte) (comm []byte, err e

encodedBlob, err := e.client.GetCodec().EncodeBlob(value)
if err != nil {
return nil, fmt.Errorf("EigenDA client failed to reencode blob: %w", err)
return nil, fmt.Errorf("EigenDA client failed to re-encode blob: %w", err)
}
err = e.verifier.Verify(cert.BlobHeader.Commitment, encodedBlob)
err = e.verifier.VerifyCommitment(cert.BlobHeader.Commitment, encodedBlob)
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion store/memory.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ func (e *MemStore) Get(ctx context.Context, commit []byte, domain eigendacommon.
}

// Don't need to do this really since it's a mock store
err = e.verifier.Verify(cert.BlobHeader.Commitment, encodedBlob)
err = e.verifier.VerifyCommitment(cert.BlobHeader.Commitment, encodedBlob)
Copy link
Contributor

@teddyknox teddyknox Jun 18, 2024

Choose a reason for hiding this comment

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

Observation, not urgent: The fact that there's a decent amount of code duplication between the EigenDA store and memory store suggests to me that we could do some refactoring.

if err != nil {
return nil, err
}
Expand Down
16 changes: 14 additions & 2 deletions store/memory_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,13 @@ func TestGetSet(t *testing.T) {
SRSNumberToLoad: 3000,
NumWorker: uint64(runtime.GOMAXPROCS(0)),
}
verifier, err := verify.NewVerifier(kzgConfig)

cfg := &verify.Config{
Verify: false,
KzgConfig: kzgConfig,
}

verifier, err := verify.NewVerifier(cfg, nil)
assert.NoError(t, err)

ms, err := NewMemStore(
Expand Down Expand Up @@ -68,7 +74,13 @@ func TestExpiration(t *testing.T) {
SRSNumberToLoad: 3000,
NumWorker: uint64(runtime.GOMAXPROCS(0)),
}
verifier, err := verify.NewVerifier(kzgConfig)

cfg := &verify.Config{
Verify: false,
KzgConfig: kzgConfig,
}

verifier, err := verify.NewVerifier(cfg, nil)
assert.NoError(t, err)

ms, err := NewMemStore(
Expand Down
72 changes: 72 additions & 0 deletions verify/cert.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package verify

import (
"fmt"

proxy_common "github.com/Layr-Labs/eigenda-proxy/common"
binding "github.com/Layr-Labs/eigenda/contracts/bindings/EigenDAServiceManager"
"github.com/ethereum/go-ethereum/common"

"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log"
)

// 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
}

func NewCertVerifier(cfg *Config, l log.Logger) (*CertVerifier, error) {
client, err := ethclient.Dial(cfg.RPCURL)
if err != nil {
return nil, fmt.Errorf("failed to dial ETH RPC node: %s", err.Error())
}

// construct binding
m, err := binding.NewContractEigenDAServiceManagerCaller(common.HexToAddress(cfg.SvcManagerAddr), client)
if err != nil {
return nil, err
}

return &CertVerifier{
manager: m,
}, nil
}

func (cv *CertVerifier) VerifyBatch(header *binding.IEigenDAServiceManagerBatchHeader,
id uint32, recordHash [32]byte, blockNum uint32) error {
// 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)
if err != nil {
return err
}

// 1.b - ensure that hash generated from local cert matches one stored on-chain

actualHash, err := HashBatchMetadata(header, recordHash, blockNum)

if err != nil {
return err
}

equal := proxy_common.EqualSlices(expectedHash[:], actualHash[:])
Copy link
Contributor

@teddyknox teddyknox Jun 18, 2024

Choose a reason for hiding this comment

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

Nit: "golang.org/x/exp/slices" gives you slices.Equal(). Again, can ignore.

https://chatgpt.com/share/79d6326a-d0e3-4192-829b-ee3064dccbc9

if !equal {
return fmt.Errorf("batch hash mismatch, expected: %x, got: %x", expectedHash, actualHash)
}

return nil
}

// 2 - (TODO) merkle proof verification

func (cv *CertVerifier) VerifyMerkleProof(inclusionProof []byte, rootHash []byte, leafHash []byte, index uint64) error {
return nil
}

// 3 - (TODO) verify blob security params
func (cv *CertVerifier) Verify(inclusionProof []byte, rootHash []byte, leafHash []byte, index uint64) error {
return nil
}
Loading
Loading