From e573d36550ef0a44f4ca843b4360791c2b8e5e2a Mon Sep 17 00:00:00 2001 From: tarassh Date: Sat, 28 Dec 2024 21:49:27 -0800 Subject: [PATCH] [feat] Polynomial permutation (#59) * initial commit * add unit test * Update PRNG * update permutation interface * remove redundant print * Retry only when timeout occurs --- internal/light-client/apihandler/handler.go | 7 ++- internal/light-client/poller/challenge.go | 12 +++-- internal/light-client/poller/poll.go | 4 +- internal/light-client/sampler/sampler.go | 25 ++++------ internal/light-client/utils/permutation.go | 48 +++++++++++++++++++ .../light-client/utils/permutation_test.go | 41 ++++++++++++++++ 6 files changed, 113 insertions(+), 24 deletions(-) create mode 100644 internal/light-client/utils/permutation.go create mode 100644 internal/light-client/utils/permutation_test.go diff --git a/internal/light-client/apihandler/handler.go b/internal/light-client/apihandler/handler.go index 7af9b48..0a462fd 100644 --- a/internal/light-client/apihandler/handler.go +++ b/internal/light-client/apihandler/handler.go @@ -149,8 +149,13 @@ func (p *ApiHandler) SendSampleVerifyRequest(request *pb.SampleVerifyRequest) er // Check the response status code if resp.StatusCode != http.StatusOK { + if resp.StatusCode == http.StatusRequestTimeout { + return fmt.Errorf("API request timed out") + } + responseBody, _ := io.ReadAll(resp.Body) - return fmt.Errorf("API request failed with status: %s, response: %s", resp.Status, responseBody) + log.Errorf("API request failed with status: %s, response: %s", resp.Status, responseBody) + return nil } return nil diff --git a/internal/light-client/poller/challenge.go b/internal/light-client/poller/challenge.go index aa63325..648465c 100644 --- a/internal/light-client/poller/challenge.go +++ b/internal/light-client/poller/challenge.go @@ -128,20 +128,24 @@ func readBigInt(reader *bytes.Reader) (*big.Int, error) { } // Solve solves a challenge for a given workload and identity -func (c *Challenge) Solve(workload *pb.Workload, identity *utils.Identity) (bool, error) { +func (c *Challenge) Solve(workload *pb.Workload, identity *utils.Identity) (bool, []byte, error) { // Calculate the target target, err := c.computeTarget(workload, identity) if err != nil { - return false, fmt.Errorf("failed to calculate target: %w", err) + return false, nil, fmt.Errorf("failed to calculate target: %w", err) } // Compare the target switch c.ClauseType.Type { case "Modulo": log.Infof("Solving Modulo challenge M=%s, K=%s", c.ClauseType.M, c.ClauseType.K) - return c.solveModulo(target, c.ClauseType.M, c.ClauseType.K) + eligible, err := c.solveModulo(target, c.ClauseType.M, c.ClauseType.K) + if err != nil { + return false, nil, fmt.Errorf("failed to solve Modulo challenge: %w", err) + } + return eligible, target, nil default: - return false, fmt.Errorf("unsupported clause type: %s", c.ClauseType.Type) + return false, nil, fmt.Errorf("unsupported clause type: %s", c.ClauseType.Type) } } diff --git a/internal/light-client/poller/poll.go b/internal/light-client/poller/poll.go index 8c9621e..783ae2d 100644 --- a/internal/light-client/poller/poll.go +++ b/internal/light-client/poller/poll.go @@ -55,14 +55,14 @@ func (p *WorkloadPoller) periodicPoll() { log.Errorf("failed to decode challenge: %s", err) } - eligible, err := challenge.Solve(workload.Workload, p.identity) + eligible, seed, err := challenge.Solve(workload.Workload, p.identity) if err != nil { log.Errorf("failed to solve challenge: %s", err) } log.Infof("workload is eligible: %v", eligible) if eligible { - p.sampler.ProcessEvent(workload) + p.sampler.ProcessEvent(workload, seed) } } diff --git a/internal/light-client/sampler/sampler.go b/internal/light-client/sampler/sampler.go index 0438e71..bfacd48 100644 --- a/internal/light-client/sampler/sampler.go +++ b/internal/light-client/sampler/sampler.go @@ -4,7 +4,6 @@ import ( "context" "encoding/base64" "fmt" - "math/rand" "time" "github.com/covalenthq/das-ipfs-pinner/common" @@ -13,6 +12,7 @@ import ( "github.com/covalenthq/das-ipfs-pinner/internal/light-client/apihandler" verifier "github.com/covalenthq/das-ipfs-pinner/internal/light-client/c-kzg-verifier" pb "github.com/covalenthq/das-ipfs-pinner/internal/light-client/schemapb" + "github.com/covalenthq/das-ipfs-pinner/internal/light-client/utils" ckzg4844 "github.com/ethereum/c-kzg-4844/v2/bindings/go" "github.com/ipfs/go-cid" ipfs "github.com/ipfs/go-ipfs-api" @@ -49,8 +49,8 @@ func NewSampler(ipfsAddr string, samplingDelay uint, pub *apihandler.ApiHandler) }, nil } -func (s *Sampler) ProcessEvent(workload *pb.SignedWorkload) { - go func(signedWorkload *pb.SignedWorkload) { +func (s *Sampler) ProcessEvent(workload *pb.SignedWorkload, seed []byte) { + go func(signedWorkload *pb.SignedWorkload, seed []byte) { log.Debugf("Processing workload: %+v", workload.GetWorkload().ReadableString()) workload := workload.Workload @@ -97,19 +97,10 @@ func (s *Sampler) ProcessEvent(workload *pb.SignedWorkload) { return } - // Track sampled column indices to avoid duplicates - sampledCols := make(map[int]bool) - - for i := 0; i < sampleIterations; i++ { - // Find a unique column index that hasn't been sampled yet - var colIndex int - for { - colIndex = rand.Intn(len(links)) - if !sampledCols[colIndex] { - sampledCols[colIndex] = true - break - } - } + // Use Polynomial Permutation to generate Indeces + rand := utils.NewPolynomialPermutation(seed, ckzg4844.CellsPerExtBlob) + for i := range sampleIterations { + colIndex := rand.Permute(i) / stackSize var data internal.DataMap if err := s.GetData(links[colIndex].CID, &data); err != nil { @@ -152,7 +143,7 @@ func (s *Sampler) ProcessEvent(workload *pb.SignedWorkload) { return } } - }(workload) + }(workload, seed) } // GetData tries to fetch data from both the IPFS node and gateways simultaneously. diff --git a/internal/light-client/utils/permutation.go b/internal/light-client/utils/permutation.go new file mode 100644 index 0000000..4c53205 --- /dev/null +++ b/internal/light-client/utils/permutation.go @@ -0,0 +1,48 @@ +package utils + +import ( + "crypto/sha256" +) + +func gcd(a, b int) int { + if b == 0 { + return a + } + return gcd(b, a%b) +} + +// Pseudo-random number generator based on a polynomial permutation +// `f(x) = (a*x + b) mod maxElements“ +// where `a` and `maxElements` are coprime, guarantees unique indices +// and `b` is a random integer - ofset +type PolynomialPermutation struct { + a int + b int + maxElements int +} + +// NewPolynomialPermutation creates a new PolynomialPermutation +func NewPolynomialPermutation(publicSeed []byte, maxElements int) *PolynomialPermutation { + // Hash the public seed to derive coefficients + hash := sha256.Sum256(publicSeed) + seed := int64(hash[0]) + int64(hash[1])<<8 + int64(hash[2])<<16 + int64(hash[3])<<24 + + // Derive coefficients a and b + a := int(seed%int64(maxElements)) + 1 + b := int((seed / int64(maxElements)) % int64(maxElements)) + + // Ensure a is coprime with maxElements + for gcd(a, maxElements) != 1 { + a = (a + 1) % maxElements + } + + return &PolynomialPermutation{ + a: a, + b: b, + maxElements: maxElements, + } +} + +func (p *PolynomialPermutation) Permute(index int) int { + return (p.a*index + p.b) % p.maxElements +} diff --git a/internal/light-client/utils/permutation_test.go b/internal/light-client/utils/permutation_test.go new file mode 100644 index 0000000..cce9b0f --- /dev/null +++ b/internal/light-client/utils/permutation_test.go @@ -0,0 +1,41 @@ +package utils + +import ( + "testing" + "time" +) + +// TestPermutationAlgorihm tests the GenerateIndices function. +func TestPermutationAlgorihm(t *testing.T) { + now := time.Now().UnixNano() + publicSeed := []byte{byte(now), byte(now >> 8), byte(now >> 16), byte(now >> 24)} + numSamples := 128 + totalIndices := 128 + + rand := NewPolynomialPermutation(publicSeed, totalIndices) + + // Check for uniqueness + uniqueIndices := make(map[int]int) + for idx := range numSamples { + uniqueIndices[rand.Permute(idx)] = +1 + + if uniqueIndices[rand.Permute(idx)] > 1 { + t.Fatalf("Index %d is not unique", idx) + } + } +} + +func TestPermutationWithSeed(t *testing.T) { + seed := []byte{51, 132, 112, 103, 0, 0, 0, 0} + expected := []int{78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77} + maxElements := 128 + numSamples := 128 + + rand := NewPolynomialPermutation(seed, maxElements) + + for idx := range numSamples { + if rand.Permute(idx) != expected[idx] { + t.Fatalf("Index %d is not unique", idx) + } + } +}