From ba05698af4ff3c829370ec3b720ee39eec344531 Mon Sep 17 00:00:00 2001 From: Ethen Pociask Date: Tue, 18 Jun 2024 04:53:57 -0400 Subject: [PATCH 1/2] feat: Batch hash verification --- .github/workflows/actions.yml | 1 + e2e/setup.go | 9 ++++ eigenda/config.go | 39 +++++++++++++- server/load_store.go | 11 +++- store/eigenda.go | 13 +++-- store/memory.go | 2 +- store/memory_test.go | 16 +++++- verify/cert.go | 72 ++++++++++++++++++++++++++ verify/hasher.go | 97 +++++++++++++++++++++++++++++++++++ verify/hasher_test.go | 59 +++++++++++++++++++++ verify/verifier.go | 61 ++++++++++++++++++++-- verify/verify_test.go | 13 +++-- 12 files changed, 373 insertions(+), 20 deletions(-) create mode 100644 verify/cert.go create mode 100644 verify/hasher.go create mode 100644 verify/hasher_test.go diff --git a/.github/workflows/actions.yml b/.github/workflows/actions.yml index 5fa4d668..15a43e04 100644 --- a/.github/workflows/actions.yml +++ b/.github/workflows/actions.yml @@ -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 \ No newline at end of file diff --git a/e2e/setup.go b/e2e/setup.go index 7e234e67..173df40c 100644 --- a/e2e/setup.go +++ b/e2e/setup.go @@ -22,6 +22,7 @@ import ( const ( privateKey = "SIGNER_PRIVATE_KEY" + ethRPC = "ETHEREUM_RPC" transport = "http" svcName = "eigenda_proxy" host = "127.0.0.1" @@ -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, @@ -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? diff --git a/eigenda/config.go b/eigenda/config.go index 4a3e8de5..c7c2ff0e 100644 --- a/eigenda/config.go +++ b/eigenda/config.go @@ -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" @@ -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" @@ -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 + // KZG vars CacheDir string @@ -73,14 +80,15 @@ func (c *Config) GetMaxBlobLength() (uint64, error) { return c.maxBlobLengthBytes, nil } -func (c *Config) KzgConfig() *kzg.KzgConfig { +func (c *Config) VerificationCfg() *verify.Config { 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, @@ -88,6 +96,21 @@ func (c *Config) KzgConfig() *kzg.KzgConfig { 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. @@ -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 } @@ -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.", + EnvVars: prefixEnvVars("ETH_RPC"), + }, + &cli.StringFlag{ + Name: SvcManagerAddrFlagName, + Usage: "Deployed EigenDA service manager address.", + EnvVars: prefixEnvVars("SERVICE_MANAGER_ADDR"), + }, } } diff --git a/server/load_store.go b/server/load_store.go index 1ac7bc8c..0947822e 100644 --- a/server/load_store.go +++ b/server/load_store.go @@ -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 @@ -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") client, err := clients.NewEigenDAClient(log, daCfg.ClientConfig) if err != nil { return nil, err diff --git a/store/eigenda.go b/store/eigenda.go index 8b4071e7..dd96ce07 100644 --- a/store/eigenda.go +++ b/store/eigenda.go @@ -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 } @@ -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 } diff --git a/store/memory.go b/store/memory.go index 9d80edae..9535c83b 100644 --- a/store/memory.go +++ b/store/memory.go @@ -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) if err != nil { return nil, err } diff --git a/store/memory_test.go b/store/memory_test.go index f3ce1783..3af5fb7f 100644 --- a/store/memory_test.go +++ b/store/memory_test.go @@ -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( @@ -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( diff --git a/verify/cert.go b/verify/cert.go new file mode 100644 index 00000000..37e40a1e --- /dev/null +++ b/verify/cert.go @@ -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[:]) + 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 +} diff --git a/verify/hasher.go b/verify/hasher.go new file mode 100644 index 00000000..840d2be4 --- /dev/null +++ b/verify/hasher.go @@ -0,0 +1,97 @@ +package verify + +import ( + "encoding/binary" + + binding "github.com/Layr-Labs/eigenda/contracts/bindings/EigenDAServiceManager" + "github.com/ethereum/go-ethereum/accounts/abi" + geth_common "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" +) + +// HashBatchMetadata regenerates a batch data hash +// replicates: https://github.com/Layr-Labs/eigenda-utils/blob/c4cbc9ec078aeca3e4a04bd278e2fb136bf3e6de/src/libraries/EigenDAHasher.sol#L46-L54 +func HashBatchMetadata(bh *binding.IEigenDAServiceManagerBatchHeader, sigHash [32]byte, blockNum uint32) (geth_common.Hash, error) { + batchHeaderType, err := abi.NewType("tuple", "", []abi.ArgumentMarshaling{ + { + Name: "blobHeadersRoot", + Type: "bytes32", + }, + { + Name: "quorumNumbers", + Type: "bytes", + }, + { + Name: "signedStakeForQuorums", + Type: "bytes", + }, + { + Name: "referenceBlockNumber", + Type: "uint32", + }, + }) + + if err != nil { + return [32]byte{}, err + } + + arguments := abi.Arguments{ + { + Type: batchHeaderType, + }, + } + + s := struct { + BlobHeadersRoot [32]byte + QuorumNumbers []byte + SignedStakeForQuorums []byte + ReferenceBlockNumber uint32 + }{ + BlobHeadersRoot: bh.BlobHeadersRoot, + QuorumNumbers: bh.QuorumNumbers, + SignedStakeForQuorums: bh.SignedStakeForQuorums, + ReferenceBlockNumber: bh.ReferenceBlockNumber, + } + + bytes, err := arguments.Pack(s) + if err != nil { + return [32]byte{}, nil + } + + headerHash := crypto.Keccak256Hash(bytes) + return HashBatchHashedMetadata(headerHash, sigHash, blockNum) +} + +// HashBatchHashedMetadata hashes the given metadata into the commitment that will be stored in the contract +// replicates: https://github.com/Layr-Labs/eigenda-utils/blob/c4cbc9ec078aeca3e4a04bd278e2fb136bf3e6de/src/libraries/EigenDAHasher.sol#L19-L25 +func HashBatchHashedMetadata(batchHeaderHash [32]byte, signatoryRecordHash [32]byte, blockNumber uint32) (geth_common.Hash, error) { + + // since the solidity function uses abi.encodePacked, we need to consolidate the byte space that + // blockNum occupies to only 4 bytes versus 28 or 256 bits when encoded to abi buffer + a := make([]byte, 4) + binary.BigEndian.PutUint32(a, blockNumber) + + bytes32Type, err := abi.NewType("bytes32", "bytes32", nil) + if err != nil { + return geth_common.BytesToHash([]byte{}), err + } + + arguments := abi.Arguments{ + { + Type: bytes32Type, + }, + { + Type: bytes32Type, + }, + } + + bytes, err := arguments.Pack(batchHeaderHash, signatoryRecordHash) + if err != nil { + return [32]byte{}, err + } + + bytes = append(bytes, a...) + headerHash := crypto.Keccak256Hash(bytes) + + return headerHash, nil +} diff --git a/verify/hasher_test.go b/verify/hasher_test.go new file mode 100644 index 00000000..3c06936e --- /dev/null +++ b/verify/hasher_test.go @@ -0,0 +1,59 @@ +package verify + +import ( + "testing" + + binding "github.com/Layr-Labs/eigenda/contracts/bindings/EigenDAServiceManager" + "github.com/ethereum/go-ethereum/crypto" + "github.com/stretchr/testify/require" +) + +func TestHashBatchHashedMetadata(t *testing.T) { + batchHeaderHash := crypto.Keccak256Hash([]byte("batchHeader")) + sigRecordHash := crypto.Keccak256Hash([]byte("signatoryRecord")) + + // 1 - Test using uint32 MAX + var blockNum uint32 = 4294967295 + + expected := "0x687b60d8b30b6aaddf6413728fb66fb7a7554601c2cc8e17a37fa94ad0818500" + actual, err := HashBatchHashedMetadata(batchHeaderHash, sigRecordHash, blockNum) + require.NoError(t, err) + + require.Equal(t, expected, actual.String()) + + // 2 - Test using uint32 value + blockNum = 4294967294 + + expected = "0x94d77be4d3d180d32d61ec8037e687b71e7996feded39b72a6dc3f9ff6406b30" + actual, err = HashBatchHashedMetadata(batchHeaderHash, sigRecordHash, blockNum) + require.NoError(t, err) + + require.Equal(t, expected, actual.String()) + + // 3 - Testing using uint32 0 value + blockNum = 0 + + expected = "0x482dfb1545a792b6d118a045033143d0cc28b0e5a4b2e1924decf27e4fc8c250" + actual, err = HashBatchHashedMetadata(batchHeaderHash, sigRecordHash, blockNum) + require.NoError(t, err) + + require.Equal(t, expected, actual.String()) +} + +func TestHashBatchMetadata(t *testing.T) { + testHash := crypto.Keccak256Hash([]byte("batchHeader")) + + header := &binding.IEigenDAServiceManagerBatchHeader{ + BlobHeadersRoot: testHash, + QuorumNumbers: testHash.Bytes(), + SignedStakeForQuorums: testHash.Bytes(), + ReferenceBlockNumber: 1, + } + + expected := "0x746f8a453586621d12e41d097eab089b1f25beca44c434281d68d4be0484b7e8" + + actual, err := HashBatchMetadata(header, testHash, 1) + require.NoError(t, err) + require.Equal(t, actual.String(), expected) + +} diff --git a/verify/verifier.go b/verify/verifier.go index 298db2b4..86a60138 100644 --- a/verify/verifier.go +++ b/verify/verifier.go @@ -7,27 +7,78 @@ import ( "github.com/Layr-Labs/eigenda/encoding" "github.com/consensys/gnark-crypto/ecc/bn254" "github.com/consensys/gnark-crypto/ecc/bn254/fp" + "github.com/ethereum/go-ethereum/log" + + proxy_common "github.com/Layr-Labs/eigenda-proxy/common" + + binding "github.com/Layr-Labs/eigenda/contracts/bindings/EigenDAServiceManager" "github.com/Layr-Labs/eigenda/encoding/kzg" "github.com/Layr-Labs/eigenda/encoding/kzg/prover" "github.com/Layr-Labs/eigenda/encoding/rs" ) +type Config struct { + Verify bool + RPCURL string + SvcManagerAddr string + KzgConfig *kzg.KzgConfig +} + type Verifier struct { - prover *prover.Prover + verifyCert bool + prover *prover.Prover + cv *CertVerifier } -func NewVerifier(cfg *kzg.KzgConfig) (*Verifier, error) { - prover, err := prover.NewProver(cfg, false) // don't load G2 points +func NewVerifier(cfg *Config, l log.Logger) (*Verifier, error) { + var cv *CertVerifier + var err error + + if cfg.Verify { + cv, err = NewCertVerifier(cfg, l) + if err != nil { + return nil, err + } + } + + prover, err := prover.NewProver(cfg.KzgConfig, false) // don't load G2 points if err != nil { return nil, err } return &Verifier{ - prover: prover, + verifyCert: cfg.Verify, + prover: prover, + cv: cv, }, nil } +func (v *Verifier) VerifyCert(cert *proxy_common.Certificate) error { + if !v.verifyCert { + return nil + } + + // 1 - verify batch + + header := binding.IEigenDAServiceManagerBatchHeader{ + BlobHeadersRoot: [32]byte(cert.GetBlobVerificationProof().GetBatchMetadata().GetBatchHeader().GetBatchRoot()), + QuorumNumbers: cert.GetBlobVerificationProof().GetBatchMetadata().GetBatchHeader().GetQuorumNumbers(), + ReferenceBlockNumber: cert.GetBlobVerificationProof().GetBatchMetadata().GetBatchHeader().GetReferenceBlockNumber(), + SignedStakeForQuorums: cert.GetBlobVerificationProof().GetBatchMetadata().GetBatchHeader().GetQuorumSignedPercentages(), + } + + err := v.cv.VerifyBatch(&header, cert.BlobVerificationProof.BatchId, [32]byte(cert.BlobVerificationProof.BatchMetadata.SignatoryRecordHash), cert.BlobVerificationProof.BatchMetadata.GetConfirmationBlockNumber()) + if err != nil { + return err + } + + // 2 - TODO: verify merkle proof + + // 3 - TODO: verify security params + return nil +} + func (v *Verifier) Commit(blob []byte) (*bn254.G1Affine, error) { // ChunkLength and TotalChunks aren't relevant for computing data // commitment which is why they're currently set arbitrarily @@ -54,7 +105,7 @@ func (v *Verifier) Commit(blob []byte) (*bn254.G1Affine, error) { // Verify regenerates a commitment from the blob and asserts equivalence // to the commitment in the certificate // TODO: Optimize implementation by opening a point on the commitment instead -func (v *Verifier) Verify(expectedCommit *common.G1Commitment, blob []byte) error { +func (v *Verifier) VerifyCommitment(expectedCommit *common.G1Commitment, blob []byte) error { actualCommit, err := v.Commit(blob) if err != nil { return err diff --git a/verify/verify_test.go b/verify/verify_test.go index 19ec5c8b..0e7ccdb3 100644 --- a/verify/verify_test.go +++ b/verify/verify_test.go @@ -11,7 +11,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestVerification(t *testing.T) { +func TestCommitmentVerification(t *testing.T) { t.Parallel() var data = []byte("inter-subjective and not objective!") @@ -36,19 +36,24 @@ func TestVerification(t *testing.T) { NumWorker: uint64(runtime.GOMAXPROCS(0)), } - v, err := NewVerifier(kzgConfig) + cfg := &Config{ + Verify: false, + KzgConfig: kzgConfig, + } + + v, err := NewVerifier(cfg, nil) assert.NoError(t, err) // Happy path verification codec := codecs.NewIFFTCodec(codecs.NewDefaultBlobCodec()) blob, err := codec.EncodeBlob(data) assert.NoError(t, err) - err = v.Verify(c, blob) + err = v.VerifyCommitment(c, blob) assert.NoError(t, err) // failure with wrong data fakeData, err := codec.EncodeBlob([]byte("I am an imposter!!")) assert.NoError(t, err) - err = v.Verify(c, fakeData) + err = v.VerifyCommitment(c, fakeData) assert.Error(t, err) } From 2221b6bb834d2b0c25f537e9410710c622572945 Mon Sep 17 00:00:00 2001 From: Ethen Pociask Date: Tue, 18 Jun 2024 05:44:40 -0400 Subject: [PATCH 2/2] feat: Batch hash verification - update actions --- .github/workflows/actions.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/actions.yml b/.github/workflows/actions.yml index 15a43e04..2c26cdad 100644 --- a/.github/workflows/actions.yml +++ b/.github/workflows/actions.yml @@ -81,4 +81,4 @@ jobs: SIGNER_PRIVATE_KEY: ${{ secrets.SIGNER_PRIVATE_KEY }} ETHEREUM_RPC: ${{ secrets.ETHEREUM_RPC }} run: | - SIGNER_PRIVATE_KEY=$SIGNER_PRIVATE_KEY make holesky-test \ No newline at end of file + SIGNER_PRIVATE_KEY=$SIGNER_PRIVATE_KEY ETHEREUM_RPC=$ETHEREUM_RPC make holesky-test \ No newline at end of file