Skip to content

Commit

Permalink
[feat] Polynomial permutation (#59)
Browse files Browse the repository at this point in the history
* initial commit

* add unit test

* Update PRNG

* update permutation interface

* remove redundant print

* Retry only when timeout occurs
  • Loading branch information
tarassh authored Dec 29, 2024
1 parent 6558077 commit e573d36
Show file tree
Hide file tree
Showing 6 changed files with 113 additions and 24 deletions.
7 changes: 6 additions & 1 deletion internal/light-client/apihandler/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 8 additions & 4 deletions internal/light-client/poller/challenge.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down
4 changes: 2 additions & 2 deletions internal/light-client/poller/poll.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down
25 changes: 8 additions & 17 deletions internal/light-client/sampler/sampler.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"context"
"encoding/base64"
"fmt"
"math/rand"
"time"

"github.com/covalenthq/das-ipfs-pinner/common"
Expand All @@ -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"
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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.
Expand Down
48 changes: 48 additions & 0 deletions internal/light-client/utils/permutation.go
Original file line number Diff line number Diff line change
@@ -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
}
41 changes: 41 additions & 0 deletions internal/light-client/utils/permutation_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
}

0 comments on commit e573d36

Please sign in to comment.