diff --git a/core/assignment_test.go b/core/assignment_test.go index 656f03952..1d45a312d 100644 --- a/core/assignment_test.go +++ b/core/assignment_test.go @@ -56,22 +56,6 @@ func TestOperatorAssignments(t *testing.T) { StartIndex: 15, NumChunks: 6, }, - mock.MakeOperatorId(6): { - StartIndex: 21, - NumChunks: 3, - }, - mock.MakeOperatorId(7): { - StartIndex: 14, - NumChunks: 3, - }, - mock.MakeOperatorId(8): { - StartIndex: 17, - NumChunks: 4, - }, - mock.MakeOperatorId(9): { - StartIndex: 21, - NumChunks: 4, - }, } expectedInfo := core.AssignmentInfo{ TotalChunks: 21, diff --git a/core/assignment_v2.go b/core/assignment_v2.go new file mode 100644 index 000000000..b9268704c --- /dev/null +++ b/core/assignment_v2.go @@ -0,0 +1,159 @@ +package core + +import ( + "fmt" + "math" + "math/big" + "sort" +) + +type BlobParameters struct { + CodingRate uint + ReconstructionThreshold float64 + NumChunks uint +} + +func (p BlobParameters) MaxNumOperators() uint { + + return uint(math.Floor(float64(p.NumChunks) * (1 - 1/(p.ReconstructionThreshold*float64(p.CodingRate))))) + +} + +var ( + ParametersMap = map[byte]BlobParameters{ + 0: {CodingRate: 8, ReconstructionThreshold: 0.22, NumChunks: 8192}, + } +) + +// AssignmentCoordinator is responsible for assigning chunks to operators in a way that satisfies the security +// requirements of the protocol, as well as the constraints imposed by the specific blob version. +type AssignmentCoordinatorV2 interface { + + // GetAssignments calculates the full set of node assignments + GetAssignments(state *OperatorState, blobVersion byte, quorum QuorumID) (map[OperatorID]Assignment, error) + + // GetAssignment calculates the assignment for a specific operator + GetAssignment(state *OperatorState, blobVersion byte, quorum QuorumID, id OperatorID) (Assignment, error) + + // GetChunkLength determines the length of a chunk given the blob version and blob length + GetChunkLength(blobVersion byte, blobLength uint) (uint, error) +} + +type StdAssignmentCoordinatorV2 struct { +} + +var _ AssignmentCoordinatorV2 = (*StdAssignmentCoordinatorV2)(nil) + +func (c *StdAssignmentCoordinatorV2) GetAssignments(state *OperatorState, blobVersion byte, quorum QuorumID) (map[OperatorID]Assignment, error) { + + params, ok := ParametersMap[blobVersion] + if !ok { + return nil, fmt.Errorf("blob version %d not found", blobVersion) + } + + ops, ok := state.Operators[quorum] + if !ok { + return nil, fmt.Errorf("no operators found for quorum %d", quorum) + } + + if len(ops) > int(params.MaxNumOperators()) { + return nil, fmt.Errorf("too many operators for blob version %d", blobVersion) + } + + n := big.NewInt(int64(len(ops))) + m := big.NewInt(int64(params.NumChunks)) + + type assignment struct { + id OperatorID + index uint + chunks uint + stake *big.Int + } + + chunkAssignments := make([]assignment, 0, len(ops)) + for ID, r := range state.Operators[quorum] { + + num := new(big.Int).Mul(r.Stake, new(big.Int).Sub(m, n)) + denom := state.Totals[quorum].Stake + + chunks := roundUpDivideBig(num, denom) + + chunkAssignments = append(chunkAssignments, assignment{id: ID, index: r.Index, chunks: uint(chunks.Uint64()), stake: r.Stake}) + } + + // Sort chunk decreasing by stake or operator ID in case of a tie + sort.Slice(chunkAssignments, func(i, j int) bool { + if chunkAssignments[i].stake.Cmp(chunkAssignments[j].stake) == 0 { + return chunkAssignments[i].index < chunkAssignments[j].index + } + return chunkAssignments[i].stake.Cmp(chunkAssignments[j].stake) == 1 + }) + + mp := 0 + for _, a := range chunkAssignments { + mp += int(a.chunks) + } + + delta := int(params.NumChunks) - mp + if delta < 0 { + return nil, fmt.Errorf("total chunks %d exceeds maximum %d", mp, params.NumChunks) + } + + assignments := make(map[OperatorID]Assignment, len(chunkAssignments)) + index := uint(0) + for i, a := range chunkAssignments { + if i < delta { + a.chunks++ + } + + assignment := Assignment{ + StartIndex: index, + NumChunks: a.chunks, + } + + assignments[a.id] = assignment + index += a.chunks + } + + return assignments, nil + +} + +func (c *StdAssignmentCoordinatorV2) GetAssignment(state *OperatorState, blobVersion byte, quorum QuorumID, id OperatorID) (Assignment, error) { + + assignments, err := c.GetAssignments(state, blobVersion, quorum) + if err != nil { + return Assignment{}, err + } + + assignment, ok := assignments[id] + if !ok { + return Assignment{}, ErrNotFound + } + + return assignment, nil +} + +func (c *StdAssignmentCoordinatorV2) GetChunkLength(blobVersion byte, blobLength uint) (uint, error) { + + if blobLength == 0 { + return 0, fmt.Errorf("blob length must be greater than 0") + } + + // Check that the blob length is a power of 2 + if blobLength&(blobLength-1) != 0 { + return 0, fmt.Errorf("blob length %d is not a power of 2", blobLength) + } + + if _, ok := ParametersMap[blobVersion]; !ok { + return 0, fmt.Errorf("blob version %d not found", blobVersion) + } + + chunkLength := blobLength * ParametersMap[blobVersion].CodingRate / ParametersMap[blobVersion].NumChunks + if chunkLength == 0 { + chunkLength = 1 + } + + return chunkLength, nil + +} diff --git a/core/assignment_v2_test.go b/core/assignment_v2_test.go new file mode 100644 index 000000000..d0304f9a6 --- /dev/null +++ b/core/assignment_v2_test.go @@ -0,0 +1,227 @@ +package core_test + +import ( + "context" + "math/rand" + "testing" + + "github.com/Layr-Labs/eigenda/core" + "github.com/Layr-Labs/eigenda/core/mock" + "github.com/stretchr/testify/assert" +) + +const ( + maxNumOperators = 3537 +) + +func TestOperatorAssignmentsV2(t *testing.T) { + + state := dat.GetTotalOperatorState(context.Background(), 0) + operatorState := state.OperatorState + coordinator := &core.StdAssignmentCoordinatorV2{} + + blobVersion := byte(0) + + assignments, err := coordinator.GetAssignments(operatorState, blobVersion, 0) + assert.NoError(t, err) + expectedAssignments := map[core.OperatorID]core.Assignment{ + mock.MakeOperatorId(0): { + StartIndex: 7802, + NumChunks: 390, + }, + mock.MakeOperatorId(1): { + StartIndex: 7022, + NumChunks: 780, + }, + mock.MakeOperatorId(2): { + StartIndex: 5852, + NumChunks: 1170, + }, + mock.MakeOperatorId(3): { + StartIndex: 4291, + NumChunks: 1561, + }, + mock.MakeOperatorId(4): { + StartIndex: 2340, + NumChunks: 1951, + }, + mock.MakeOperatorId(5): { + StartIndex: 0, + NumChunks: 2340, + }, + } + + for operatorID, assignment := range assignments { + + assert.Equal(t, assignment, expectedAssignments[operatorID]) + + assignment, err := coordinator.GetAssignment(operatorState, blobVersion, 0, operatorID) + assert.NoError(t, err) + + assert.Equal(t, assignment, expectedAssignments[operatorID]) + + } + +} + +func TestMaxNumOperators(t *testing.T) { + + assert.Equal(t, core.ParametersMap[0].MaxNumOperators(), uint(maxNumOperators)) + +} + +func TestAssignmentWithTooManyOperators(t *testing.T) { + + asn := &core.StdAssignmentCoordinatorV2{} + + numOperators := maxNumOperators + 1 + + stakes := map[core.QuorumID]map[core.OperatorID]int{ + 0: {}, + } + for i := 0; i < numOperators; i++ { + stakes[0][mock.MakeOperatorId(i)] = rand.Intn(100) + 1 + } + + dat, err := mock.NewChainDataMock(stakes) + if err != nil { + t.Fatal(err) + } + + state := dat.GetTotalOperatorState(context.Background(), 0) + + assert.Equal(t, len(state.Operators[0]), numOperators) + + blobVersion := byte(0) + + _, err = asn.GetAssignments(state.OperatorState, blobVersion, 0) + assert.Error(t, err) + +} + +func FuzzOperatorAssignmentsV2(f *testing.F) { + + // Add distributions to fuzz + asn := &core.StdAssignmentCoordinatorV2{} + + for i := 1; i < 100; i++ { + f.Add(i) + } + + for i := 0; i < 100; i++ { + f.Add(rand.Intn(2048) + 100) + } + + for i := 0; i < 5; i++ { + f.Add(maxNumOperators) + } + + f.Fuzz(func(t *testing.T, numOperators int) { + + // Generate a random slice of integers of length n + + stakes := map[core.QuorumID]map[core.OperatorID]int{ + 0: {}, + } + for i := 0; i < numOperators; i++ { + stakes[0][mock.MakeOperatorId(i)] = rand.Intn(100) + 1 + } + + dat, err := mock.NewChainDataMock(stakes) + if err != nil { + t.Fatal(err) + } + + state := dat.GetTotalOperatorState(context.Background(), 0) + + blobVersion := byte(0) + + assignments, err := asn.GetAssignments(state.OperatorState, blobVersion, 0) + assert.NoError(t, err) + + // Check that the total number of chunks is correct + totalChunks := uint(0) + for _, assignment := range assignments { + totalChunks += assignment.NumChunks + } + assert.Equal(t, totalChunks, core.ParametersMap[blobVersion].NumChunks) + + // Check that each operator's assignment satisfies the security requirement + for operatorID, assignment := range assignments { + + totalStake := uint(state.Totals[0].Stake.Uint64()) + myStake := uint(state.Operators[0][operatorID].Stake.Uint64()) + + LHS := assignment.NumChunks * totalStake * core.ParametersMap[blobVersion].CodingRate * uint(core.ParametersMap[blobVersion].ReconstructionThreshold*100) + RHS := 100 * myStake * core.ParametersMap[blobVersion].NumChunks + + assert.GreaterOrEqual(t, LHS, RHS) + + } + + }) + +} + +func TestChunkLength(t *testing.T) { + + asn := &core.StdAssignmentCoordinatorV2{} + + blobVersion := byte(0) + + pairs := []struct { + blobLength uint + chunkLength uint + }{ + {512, 1}, + {1024, 1}, + {2048, 2}, + {4096, 4}, + {8192, 8}, + } + + for _, pair := range pairs { + + chunkLength, err := asn.GetChunkLength(blobVersion, pair.blobLength) + + assert.NoError(t, err) + + assert.Equal(t, pair.chunkLength, chunkLength) + } + +} + +func TestInvalidChunkLength(t *testing.T) { + + asn := &core.StdAssignmentCoordinatorV2{} + + blobVersion := byte(0) + + invalidLengths := []uint{ + 0, + 3, + 5, + 6, + 7, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 31, + 63, + 127, + 255, + 511, + 1023, + } + + for _, length := range invalidLengths { + + _, err := asn.GetChunkLength(blobVersion, length) + assert.Error(t, err) + } + +} diff --git a/core/mock/state.go b/core/mock/state.go index 6b566d7fc..7fb71c0c0 100644 --- a/core/mock/state.go +++ b/core/mock/state.go @@ -2,6 +2,7 @@ package mock import ( "context" + "encoding/binary" "fmt" "math/big" "sort" @@ -36,7 +37,11 @@ type PrivateOperatorState struct { } func MakeOperatorId(id int) core.OperatorID { - data := [32]byte{uint8(id)} + + // Initialize a [32]byte array + var data [32]byte + // Encode the integer into a byte slice + binary.LittleEndian.PutUint64(data[:8], uint64(id)) return data }