From ba7bb66c5fb2989b8846bbb485a1b48d19b6ef92 Mon Sep 17 00:00:00 2001 From: hopeyen Date: Thu, 3 Oct 2024 14:56:34 -0700 Subject: [PATCH] refactor: accountant sign over blob header fields --- api/clients/accountant.go | 44 +++++++++- api/clients/accountant_test.go | 136 ++++++++++++++++++------------- api/clients/disperser_client.go | 5 +- api/clients/eigenda_client.go | 9 +- inabox/tests/integration_test.go | 2 +- inabox/tests/ratelimit_test.go | 2 +- tools/traffic/generator.go | 2 +- 7 files changed, 134 insertions(+), 66 deletions(-) diff --git a/api/clients/accountant.go b/api/clients/accountant.go index 6c8968bd4..428c6bd74 100644 --- a/api/clients/accountant.go +++ b/api/clients/accountant.go @@ -2,11 +2,15 @@ package clients import ( "context" + "crypto/ecdsa" "errors" "fmt" + "math/big" "time" + "github.com/Layr-Labs/eigenda/core" "github.com/Layr-Labs/eigenda/disperser/meterer" + "github.com/ethereum/go-ethereum/common" ) type IAccountant interface { @@ -26,9 +30,16 @@ type Accountant struct { binUsages []uint64 cumulativePayment uint64 stopRotation chan struct{} + + domainSigner *meterer.EIP712Signer + accountantSigner *ecdsa.PrivateKey } -func NewAccountant(reservation meterer.ActiveReservation, onDemand meterer.OnDemandPayment, reservationWindow uint32, pricePerChargeable uint32, minChargeableSize uint32) Accountant { +func NewAccountant(reservation meterer.ActiveReservation, onDemand meterer.OnDemandPayment, reservationWindow uint32, pricePerChargeable uint32, minChargeableSize uint32, accountantSigner *ecdsa.PrivateKey) Accountant { + //todo: refactor signing strategy; EIP712 -> serializer? private key -> Local blob request signer? + chainID := big.NewInt(17000) + verifyingContract := common.HexToAddress("0x1234000000000000000000000000000000000000") + a := Accountant{ reservation: reservation, onDemand: onDemand, @@ -38,6 +49,8 @@ func NewAccountant(reservation meterer.ActiveReservation, onDemand meterer.OnDem binUsages: []uint64{0, 0, 0}, cumulativePayment: 0, stopRotation: make(chan struct{}), + domainSigner: meterer.NewEIP712Signer(chainID, verifyingContract), + accountantSigner: accountantSigner, } go a.startBinRotation() return a @@ -68,8 +81,8 @@ func (a *Accountant) Stop() { close(a.stopRotation) } -// accountant provides and records payment information -func (a *Accountant) AccountBlob(ctx context.Context, dataLength uint64, quorums []uint8) (uint32, uint64, error) { +// accountant calculates and records payment information +func (a *Accountant) BlobPaymentInfo(ctx context.Context, dataLength uint64, quorums []uint8) (uint32, uint64, error) { //TODO: do we need to lock the binUsages here in case the rotation happens in the middle of the function? currentBinUsage := a.binUsages[0] currentBinIndex := meterer.GetCurrentBinIndex(a.reservationWindow) @@ -88,7 +101,6 @@ func (a *Accountant) AccountBlob(ctx context.Context, dataLength uint64, quorums return currentBinIndex, 0, nil } - fmt.Println("in ondemand:", currentBinUsage, dataLength, a.reservation.DataRate) // reservation not available, attempt on-demand //todo: rollback if disperser respond with some type of rejection? incrementRequired := uint64(max(uint32(dataLength), a.minChargeableSize)) * uint64(a.pricePerChargeable) / uint64(a.minChargeableSize) @@ -98,3 +110,27 @@ func (a *Accountant) AccountBlob(ctx context.Context, dataLength uint64, quorums } return 0, 0, errors.New("Accountant cannot approve payment for this blob") } + +// accountant provides and records payment information +func (a *Accountant) AccountBlob(ctx context.Context, dataLength uint64, quorums []uint8) (*meterer.BlobHeader, error) { + binIndex, cumulativePayment, err := a.BlobPaymentInfo(ctx, dataLength, quorums) + if err != nil { + return nil, err + } + + // TODO: have client first generate a commitment, then pass it here + commitment := core.NewG1Point(big.NewInt(0), big.NewInt(1)) + header, err := meterer.ConstructBlobHeader(a.domainSigner, binIndex, cumulativePayment, *commitment, uint32(dataLength), quorums, a.accountantSigner) + if err != nil { + return nil, err + } + return header, nil +} + +func (a *Accountant) signBlobHeader(header *meterer.BlobHeader) (*meterer.BlobHeader, error) { + signedHeader, err := meterer.ConstructBlobHeader(a.domainSigner, header.BinIndex, header.CumulativePayment, header.Commitment, header.DataLength, header.QuorumNumbers, a.accountantSigner) + if err != nil { + return nil, err + } + return signedHeader, nil +} diff --git a/api/clients/accountant_test.go b/api/clients/accountant_test.go index 8f9ff596b..63e6d3dea 100644 --- a/api/clients/accountant_test.go +++ b/api/clients/accountant_test.go @@ -7,6 +7,7 @@ import ( "time" "github.com/Layr-Labs/eigenda/disperser/meterer" + "github.com/ethereum/go-ethereum/crypto" "github.com/stretchr/testify/assert" ) @@ -25,7 +26,10 @@ func TestNewAccountant(t *testing.T) { pricePerChargeable := uint32(1) minChargeableSize := uint32(100) - accountant := NewAccountant(reservation, onDemand, reservationWindow, pricePerChargeable, minChargeableSize) + privateKey1, err := crypto.GenerateKey() + assert.NoError(t, err) + accountant := NewAccountant(reservation, onDemand, reservationWindow, pricePerChargeable, minChargeableSize, privateKey1) + defer accountant.Stop() assert.Equal(t, reservation, accountant.reservation) assert.Equal(t, onDemand, accountant.onDemand) @@ -51,33 +55,36 @@ func TestAccountBlob_Reservation(t *testing.T) { pricePerChargeable := uint32(1) minChargeableSize := uint32(100) - accountant := NewAccountant(reservation, onDemand, reservationWindow, pricePerChargeable, minChargeableSize) + privateKey1, err := crypto.GenerateKey() + assert.NoError(t, err) + accountant := NewAccountant(reservation, onDemand, reservationWindow, pricePerChargeable, minChargeableSize, privateKey1) + defer accountant.Stop() ctx := context.Background() dataLength := uint64(500) quorums := []uint8{0, 1} - binIndex, payment, err := accountant.AccountBlob(ctx, dataLength, quorums) + header, err := accountant.AccountBlob(ctx, dataLength, quorums) assert.NoError(t, err) - assert.Equal(t, meterer.GetCurrentBinIndex(reservationWindow), binIndex) - assert.Equal(t, uint64(0), payment) + assert.Equal(t, meterer.GetCurrentBinIndex(reservationWindow), header.BinIndex) + assert.Equal(t, uint64(0), header.CumulativePayment) assert.Equal(t, []uint64{500, 0, 0}, accountant.binUsages) dataLength = uint64(700) - binIndex, payment, err = accountant.AccountBlob(ctx, dataLength, quorums) + header, err = accountant.AccountBlob(ctx, dataLength, quorums) assert.NoError(t, err) - assert.NotEqual(t, 0, binIndex) - assert.Equal(t, uint64(0), payment) + assert.NotEqual(t, 0, header.BinIndex) + assert.Equal(t, uint64(0), header.CumulativePayment) assert.Equal(t, []uint64{1200, 0, 200}, accountant.binUsages) // Second call should use on-demand payment - binIndex, payment, err = accountant.AccountBlob(ctx, 300, quorums) + header, err = accountant.AccountBlob(ctx, 300, quorums) assert.NoError(t, err) - assert.Equal(t, uint32(0), binIndex) - assert.Equal(t, uint64(3), payment) + assert.Equal(t, uint32(0), header.BinIndex) + assert.Equal(t, uint64(3), header.CumulativePayment) } func TestAccountBlob_OnDemand(t *testing.T) { @@ -95,18 +102,21 @@ func TestAccountBlob_OnDemand(t *testing.T) { pricePerChargeable := uint32(1) minChargeableSize := uint32(100) - accountant := NewAccountant(reservation, onDemand, reservationWindow, pricePerChargeable, minChargeableSize) + privateKey1, err := crypto.GenerateKey() + assert.NoError(t, err) + accountant := NewAccountant(reservation, onDemand, reservationWindow, pricePerChargeable, minChargeableSize, privateKey1) + defer accountant.Stop() ctx := context.Background() dataLength := uint64(1500) quorums := []uint8{0, 1} - binIndex, payment, err := accountant.AccountBlob(ctx, dataLength, quorums) + header, err := accountant.AccountBlob(ctx, dataLength, quorums) expectedPayment := uint64(dataLength * uint64(pricePerChargeable) / uint64(minChargeableSize)) assert.NoError(t, err) - assert.Equal(t, uint32(0), binIndex) - assert.Equal(t, expectedPayment, payment) + assert.Equal(t, uint32(0), header.BinIndex) + assert.Equal(t, expectedPayment, header.CumulativePayment) assert.Equal(t, []uint64{0, 0, 0}, accountant.binUsages) assert.Equal(t, expectedPayment, accountant.cumulativePayment) } @@ -120,13 +130,16 @@ func TestAccountBlob_InsufficientOnDemand(t *testing.T) { pricePerChargeable := uint32(100) minChargeableSize := uint32(100) - accountant := NewAccountant(reservation, onDemand, reservationWindow, pricePerChargeable, minChargeableSize) + privateKey1, err := crypto.GenerateKey() + assert.NoError(t, err) + accountant := NewAccountant(reservation, onDemand, reservationWindow, pricePerChargeable, minChargeableSize, privateKey1) + defer accountant.Stop() ctx := context.Background() dataLength := uint64(2000) quorums := []uint8{0, 1} - _, _, err := accountant.AccountBlob(ctx, dataLength, quorums) + _, err = accountant.AccountBlob(ctx, dataLength, quorums) assert.Error(t, err) assert.Contains(t, err.Error(), "Accountant cannot approve payment for this blob") @@ -147,31 +160,34 @@ func TestAccountBlobCallSeries(t *testing.T) { pricePerChargeable := uint32(100) minChargeableSize := uint32(100) - accountant := NewAccountant(reservation, onDemand, reservationWindow, pricePerChargeable, minChargeableSize) + privateKey1, err := crypto.GenerateKey() + assert.NoError(t, err) + accountant := NewAccountant(reservation, onDemand, reservationWindow, pricePerChargeable, minChargeableSize, privateKey1) + defer accountant.Stop() ctx := context.Background() quorums := []uint8{0, 1} // First call: Use reservation - binIndex, payment, err := accountant.AccountBlob(ctx, 800, quorums) + header, err := accountant.AccountBlob(ctx, 800, quorums) assert.NoError(t, err) - assert.Equal(t, meterer.GetCurrentBinIndex(reservationWindow), binIndex) - assert.Equal(t, uint64(0), payment) + assert.Equal(t, meterer.GetCurrentBinIndex(reservationWindow), header.BinIndex) + assert.Equal(t, uint64(0), header.CumulativePayment) // Second call: Use remaining reservation + overflow - binIndex, payment, err = accountant.AccountBlob(ctx, 300, quorums) + header, err = accountant.AccountBlob(ctx, 300, quorums) assert.NoError(t, err) - assert.Equal(t, meterer.GetCurrentBinIndex(reservationWindow), binIndex) - assert.Equal(t, uint64(0), payment) + assert.Equal(t, meterer.GetCurrentBinIndex(reservationWindow), header.BinIndex) + assert.Equal(t, uint64(0), header.CumulativePayment) // Third call: Use on-demand - binIndex, payment, err = accountant.AccountBlob(ctx, 500, quorums) + header, err = accountant.AccountBlob(ctx, 500, quorums) assert.NoError(t, err) - assert.Equal(t, uint32(0), binIndex) - assert.Equal(t, uint64(500), payment) + assert.Equal(t, uint32(0), header.BinIndex) + assert.Equal(t, uint64(500), header.CumulativePayment) // Fourth call: Insufficient on-demand - _, _, err = accountant.AccountBlob(ctx, 600, quorums) + _, err = accountant.AccountBlob(ctx, 600, quorums) assert.Error(t, err) assert.Contains(t, err.Error(), "Accountant cannot approve payment for this blob") } @@ -190,15 +206,16 @@ func TestAccountBlob_BinRotation(t *testing.T) { reservationWindow := uint32(1) // Set to 1 second for testing pricePerChargeable := uint32(1) minChargeableSize := uint32(100) - - accountant := NewAccountant(reservation, onDemand, reservationWindow, pricePerChargeable, minChargeableSize) + privateKey1, err := crypto.GenerateKey() + assert.NoError(t, err) + accountant := NewAccountant(reservation, onDemand, reservationWindow, pricePerChargeable, minChargeableSize, privateKey1) defer accountant.Stop() ctx := context.Background() quorums := []uint8{0, 1} // First call - _, _, err := accountant.AccountBlob(ctx, 800, quorums) + _, err = accountant.AccountBlob(ctx, 800, quorums) assert.NoError(t, err) assert.Equal(t, []uint64{800, 0, 0}, accountant.binUsages) @@ -206,12 +223,12 @@ func TestAccountBlob_BinRotation(t *testing.T) { time.Sleep(2 * time.Second) // Second call after bin rotation - _, _, err = accountant.AccountBlob(ctx, 300, quorums) + _, err = accountant.AccountBlob(ctx, 300, quorums) assert.NoError(t, err) assert.Equal(t, []uint64{300, 0, 0}, accountant.binUsages) // Third call - _, _, err = accountant.AccountBlob(ctx, 500, quorums) + _, err = accountant.AccountBlob(ctx, 500, quorums) assert.NoError(t, err) assert.Equal(t, []uint64{800, 0, 0}, accountant.binUsages) } @@ -231,33 +248,35 @@ func TestBinRotation(t *testing.T) { pricePerChargeable := uint32(1) minChargeableSize := uint32(100) - accountant := NewAccountant(reservation, onDemand, reservationWindow, pricePerChargeable, minChargeableSize) + privateKey1, err := crypto.GenerateKey() + assert.NoError(t, err) + accountant := NewAccountant(reservation, onDemand, reservationWindow, pricePerChargeable, minChargeableSize, privateKey1) defer accountant.Stop() ctx := context.Background() quorums := []uint8{0, 1} // First call - _, _, err := accountant.AccountBlob(ctx, 800, quorums) + _, err = accountant.AccountBlob(ctx, 800, quorums) assert.NoError(t, err) assert.Equal(t, []uint64{800, 0, 0}, accountant.binUsages) // Second call for overflow - _, _, err = accountant.AccountBlob(ctx, 800, quorums) + _, err = accountant.AccountBlob(ctx, 800, quorums) assert.NoError(t, err) assert.Equal(t, []uint64{1600, 0, 600}, accountant.binUsages) // Wait for bin rotation time.Sleep(1200 * time.Millisecond) - _, _, err = accountant.AccountBlob(ctx, 300, quorums) + _, err = accountant.AccountBlob(ctx, 300, quorums) assert.NoError(t, err) assert.Equal(t, []uint64{300, 600, 0}, accountant.binUsages) // another bin rotation time.Sleep(1200 * time.Millisecond) - _, _, err = accountant.AccountBlob(ctx, 500, quorums) + _, err = accountant.AccountBlob(ctx, 500, quorums) assert.NoError(t, err) assert.Equal(t, []uint64{1100, 0, 100}, accountant.binUsages) } @@ -277,7 +296,9 @@ func TestConcurrentBinRotationAndAccountBlob(t *testing.T) { pricePerChargeable := uint32(1) minChargeableSize := uint32(100) - accountant := NewAccountant(reservation, onDemand, reservationWindow, pricePerChargeable, minChargeableSize) + privateKey1, err := crypto.GenerateKey() + assert.NoError(t, err) + accountant := NewAccountant(reservation, onDemand, reservationWindow, pricePerChargeable, minChargeableSize, privateKey1) defer accountant.Stop() ctx := context.Background() @@ -290,7 +311,7 @@ func TestConcurrentBinRotationAndAccountBlob(t *testing.T) { go func() { defer wg.Done() for j := 0; j < 5; j++ { - _, _, err := accountant.AccountBlob(ctx, 100, quorums) + _, err := accountant.AccountBlob(ctx, 100, quorums) assert.NoError(t, err) time.Sleep(500 * time.Millisecond) } @@ -319,29 +340,31 @@ func TestAccountBlob_ReservationWithOneOverflow(t *testing.T) { pricePerChargeable := uint32(1) minChargeableSize := uint32(100) - accountant := NewAccountant(reservation, onDemand, reservationWindow, pricePerChargeable, minChargeableSize) - + privateKey1, err := crypto.GenerateKey() + assert.NoError(t, err) + accountant := NewAccountant(reservation, onDemand, reservationWindow, pricePerChargeable, minChargeableSize, privateKey1) + defer accountant.Stop() ctx := context.Background() quorums := []uint8{0, 1} // Okay reservation - binIndex, payment, err := accountant.AccountBlob(ctx, 800, quorums) + header, err := accountant.AccountBlob(ctx, 800, quorums) assert.NoError(t, err) - assert.Equal(t, meterer.GetCurrentBinIndex(reservationWindow), binIndex) - assert.Equal(t, uint64(0), payment) + assert.Equal(t, meterer.GetCurrentBinIndex(reservationWindow), header.BinIndex) + assert.Equal(t, uint64(0), header.CumulativePayment) assert.Equal(t, []uint64{800, 0, 0}, accountant.binUsages) // Second call: Allow one overflow - _, payment, err = accountant.AccountBlob(ctx, 500, quorums) + header, err = accountant.AccountBlob(ctx, 500, quorums) assert.NoError(t, err) - assert.Equal(t, uint64(0), payment) + assert.Equal(t, uint64(0), header.CumulativePayment) assert.Equal(t, []uint64{1300, 0, 300}, accountant.binUsages) // Third call: Should use on-demand payment - binIndex, payment, err = accountant.AccountBlob(ctx, 200, quorums) + header, err = accountant.AccountBlob(ctx, 200, quorums) assert.NoError(t, err) - assert.Equal(t, uint32(0), binIndex) - assert.Equal(t, uint64(2), payment) + assert.Equal(t, uint32(0), header.BinIndex) + assert.Equal(t, uint64(2), header.CumulativePayment) assert.Equal(t, []uint64{1300, 0, 300}, accountant.binUsages) } @@ -360,27 +383,30 @@ func TestAccountBlob_ReservationOverflowReset(t *testing.T) { pricePerChargeable := uint32(1) minChargeableSize := uint32(100) - accountant := NewAccountant(reservation, onDemand, reservationWindow, pricePerChargeable, minChargeableSize) + privateKey1, err := crypto.GenerateKey() + assert.NoError(t, err) + accountant := NewAccountant(reservation, onDemand, reservationWindow, pricePerChargeable, minChargeableSize, privateKey1) defer accountant.Stop() ctx := context.Background() quorums := []uint8{0, 1} // full reservation - _, _, err := accountant.AccountBlob(ctx, 1000, quorums) + _, err = accountant.AccountBlob(ctx, 1000, quorums) assert.NoError(t, err) assert.Equal(t, []uint64{1000, 0, 0}, accountant.binUsages) // no overflow - _, payment, err := accountant.AccountBlob(ctx, 500, quorums) + header, err := accountant.AccountBlob(ctx, 500, quorums) + assert.NoError(t, err) assert.Equal(t, []uint64{1000, 0, 0}, accountant.binUsages) - assert.Equal(t, uint64(5), payment) + assert.Equal(t, uint64(5), header.CumulativePayment) // Wait for bin rotation time.Sleep(1500 * time.Millisecond) // Third call: Should use new bin and allow overflow again - _, _, err = accountant.AccountBlob(ctx, 500, quorums) + _, err = accountant.AccountBlob(ctx, 500, quorums) assert.NoError(t, err) assert.Equal(t, []uint64{500, 0, 0}, accountant.binUsages) } diff --git a/api/clients/disperser_client.go b/api/clients/disperser_client.go index ae8b66701..148edb5c1 100644 --- a/api/clients/disperser_client.go +++ b/api/clients/disperser_client.go @@ -69,10 +69,11 @@ func (c *disperserClient) getDialOptions() []grpc.DialOption { func (c *disperserClient) DisperseBlob(ctx context.Context, data []byte, quorums []uint8) (*disperser.BlobStatus, []byte, error) { // disperser client checks with the accountant; accountant provides payment fields // if cannot pay, error (or attempt anyway without payment info?) - binIndex, cumulativePayment, err := c.accountant.AccountBlob(ctx, uint64(len(data)), quorums) + header, err := c.accountant.AccountBlob(ctx, uint64(len(data)), quorums) if err != nil { return nil, nil, err } + fmt.Printf("header: %v\n", header) addr := fmt.Sprintf("%v:%v", c.config.Hostname, c.config.Port) @@ -100,8 +101,6 @@ func (c *disperserClient) DisperseBlob(ctx context.Context, data []byte, quorums request := &disperser_rpc.DisperseBlobRequest{ Data: data, CustomQuorumNumbers: quorumNumbers, - BinIndex: binIndex, - CumulativePayment: cumulativePayment, } reply, err := disperserClient.DisperseBlob(ctxTimeout, request) diff --git a/api/clients/eigenda_client.go b/api/clients/eigenda_client.go index 54c1212ad..48d6869f0 100644 --- a/api/clients/eigenda_client.go +++ b/api/clients/eigenda_client.go @@ -13,6 +13,7 @@ import ( "github.com/Layr-Labs/eigenda/core/auth" "github.com/Layr-Labs/eigenda/disperser" "github.com/Layr-Labs/eigenda/disperser/meterer" + "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/log" ) @@ -80,7 +81,13 @@ func NewEigenDAClient(log log.Logger, config EigenDAClientConfig) (*EigenDAClien onDemand := meterer.OnDemandPayment{ CumulativePayment: 1000, } - accountant := NewAccountant(reservation, onDemand, binInterval, pricePerChargeable, minChargeableSize) + + privateKey, err := crypto.HexToECDSA(config.SignerPrivateKeyHex) + if err != nil { + return nil, fmt.Errorf("failed to parse private key: %w", err) + } + + accountant := NewAccountant(reservation, onDemand, binInterval, pricePerChargeable, minChargeableSize, privateKey) llClient := NewDisperserClient(llConfig, signer, accountant) diff --git a/inabox/tests/integration_test.go b/inabox/tests/integration_test.go index 4bfdfa22f..ed9c7b0d9 100644 --- a/inabox/tests/integration_test.go +++ b/inabox/tests/integration_test.go @@ -40,7 +40,7 @@ var _ = Describe("Inabox Integration", func() { Hostname: "localhost", Port: "32003", Timeout: 10 * time.Second, - }, signer) + }, signer, clients.Accountant{}) Expect(disp).To(Not(BeNil())) diff --git a/inabox/tests/ratelimit_test.go b/inabox/tests/ratelimit_test.go index 47a93decb..42ee49961 100644 --- a/inabox/tests/ratelimit_test.go +++ b/inabox/tests/ratelimit_test.go @@ -111,7 +111,7 @@ func testRatelimit(t *testing.T, testConfig *deploy.Config, c ratelimitTestCase) Hostname: "localhost", Port: testConfig.Dispersers[0].DISPERSER_SERVER_GRPC_PORT, Timeout: 10 * time.Second, - }, nil) + }, nil, clients.Accountant{}) assert.NotNil(t, disp) data := make([]byte, c.blobSize) diff --git a/tools/traffic/generator.go b/tools/traffic/generator.go index 2f8731ffd..bd75fafdf 100644 --- a/tools/traffic/generator.go +++ b/tools/traffic/generator.go @@ -32,7 +32,7 @@ func NewTrafficGenerator(config *Config, signer core.BlobRequestSigner) (*Traffi return &TrafficGenerator{ Logger: logger, - DisperserClient: clients.NewDisperserClient(&config.Config, signer), + DisperserClient: clients.NewDisperserClient(&config.Config, signer, clients.Accountant{}), Config: config, }, nil }