Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimize PoW #419

Merged
merged 1 commit into from
Oct 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion registration/pow_verifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ func (v *powVerifier) Verify(poetChallenge, nodeID []byte, nonce uint64) error {
return fmt.Errorf("%w: invalid nodeID length: %d", ErrInvalidPow, len(nodeID))
}

hash := shared.CalcSubmitPowHash(v.params.Challenge, poetChallenge, nodeID, nil, nonce)
hash := shared.NewPowHasher(v.params.Challenge, nodeID, poetChallenge).Hash(nonce, nil)

if !shared.CheckLeadingZeroBits(hash, v.params.Difficulty) {
return fmt.Errorf("%w: invalid leading zero bits", ErrInvalidPow)
Expand Down
3 changes: 2 additions & 1 deletion registration/pow_verifier_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@ func TestVerify(t *testing.T) {
nodeID := make([]byte, 32)
poetChallenge := []byte("some data to submit to poet")
challenge := []byte("challenge this")
difficulty := uint(4)
difficulty := uint(6)

nonce, err := shared.FindSubmitPowNonce(context.Background(), challenge, poetChallenge, nodeID, difficulty)
require.NoError(t, err)
require.EqualValues(t, 143, nonce)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: EqualValues ignores typing; I think this assertion should fail if someone accidentally changes the type of nonce:

Suggested change
require.EqualValues(t, 143, nonce)
require.Equal(t, uint64(143), nonce)


verifier := NewPowVerifier(NewPowParams(challenge, difficulty))
require.NoError(t, verifier.Verify(poetChallenge, nodeID, nonce))
Expand Down
75 changes: 75 additions & 0 deletions shared/pow.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package shared

import (
"context"
"encoding/binary"
"hash"

"github.com/c0mm4nd/go-ripemd"
)

// FindSubmitPowNonce finds the nonce that solves the PoW challenge.
func FindSubmitPowNonce(
ctx context.Context,
powChallenge, poetChallenge, nodeID []byte,
difficulty uint,
) (uint64, error) {
var hash []byte
p := NewPowHasher(powChallenge, nodeID, poetChallenge)

for nonce := uint64(0); ; nonce++ {
select {
case <-ctx.Done():
return 0, ctx.Err()

Check warning on line 23 in shared/pow.go

View check run for this annotation

Codecov / codecov/patch

shared/pow.go#L22-L23

Added lines #L22 - L23 were not covered by tests
default:
}

hash = p.Hash(nonce, hash[:0])

if CheckLeadingZeroBits(hash, difficulty) {
return nonce, nil
}
}
}

type powHasher struct {
h hash.Hash
input []byte
}

func NewPowHasher(inputs ...[]byte) *powHasher {
h := &powHasher{h: ripemd.New256(), input: []byte{}}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: h.input does not need to be initialized to append bytes to it

Suggested change
h := &powHasher{h: ripemd.New256(), input: []byte{}}
h := &powHasher{h: ripemd.New256()}

for _, in := range inputs {
h.input = append(h.input, in...)
}
h.input = append(h.input, make([]byte, 8)...) // placeholder for nonce
return h
}

func (p *powHasher) Hash(nonce uint64, output []byte) []byte {
nonceBytes := p.input[len(p.input)-8:]
binary.LittleEndian.PutUint64(nonceBytes, nonce)

p.h.Reset()
p.h.Write(p.input)
return p.h.Sum(output)
}

// CheckLeadingZeroBits checks if the first 'expected' bits of the byte array are all zero.
func CheckLeadingZeroBits(data []byte, expected uint) bool {
if len(data)*8 < int(expected) {
return false
}
for i := 0; i < int(expected/8); i++ {
if data[i] != 0 {
return false
}

Check warning on line 66 in shared/pow.go

View check run for this annotation

Codecov / codecov/patch

shared/pow.go#L65-L66

Added lines #L65 - L66 were not covered by tests
}
if expected%8 != 0 {
if data[expected/8]>>(8-expected%8) != 0 {
return false
}
}

return true
}
63 changes: 63 additions & 0 deletions shared/pow_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package shared_test

import (
"context"
"encoding/binary"
"fmt"
"testing"

"github.com/stretchr/testify/require"

"github.com/spacemeshos/poet/shared"
)

func TestCheckLeadingZeroBits(t *testing.T) {
r := require.New(t)

r.True(shared.CheckLeadingZeroBits([]byte{0x00}, 0))
r.True(shared.CheckLeadingZeroBits([]byte{0x00}, 8))

// Out of bounds
r.False(shared.CheckLeadingZeroBits([]byte{0x00}, 9))

r.True(shared.CheckLeadingZeroBits([]byte{0x0F}, 4))
r.False(shared.CheckLeadingZeroBits([]byte{0x0F}, 5))

r.True(shared.CheckLeadingZeroBits([]byte{0x00, 0x0F}, 5))
r.True(shared.CheckLeadingZeroBits([]byte{0x00, 0x0F}, 12))
r.False(shared.CheckLeadingZeroBits([]byte{0x00, 0x0F}, 13))
}

func BenchmarkPowHash(b *testing.B) {
powChallenge := make([]byte, 32)
poetChallenge := make([]byte, 32)
nodeID := make([]byte, 32)

var hash []byte
p := shared.NewPowHasher(powChallenge, poetChallenge, nodeID)
for i := 0; i < b.N; i++ {
hash = p.Hash(123678, hash[:0])
}
}

func BenchmarkFindSubmitPowNonce(b *testing.B) {
powChallenge := make([]byte, 32)
poetChallenge := make([]byte, 32)
nodeID := make([]byte, 32)
b.ResetTimer()
for difficulty := 20; difficulty <= 20; difficulty += 1 {
b.Run(fmt.Sprintf("difficulty=%v", difficulty), func(b *testing.B) {
for i := 0; i < b.N; i++ {
binary.LittleEndian.PutUint64(nodeID, uint64(i))
_, err := shared.FindSubmitPowNonce(
context.Background(),
powChallenge,
poetChallenge,
nodeID,
uint(difficulty),
)
require.NoError(b, err)
}
})
}
}
54 changes: 0 additions & 54 deletions shared/shared.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
package shared

import (
"context"
"encoding/binary"
"fmt"

"github.com/c0mm4nd/go-ripemd"
"github.com/spacemeshos/go-scale"
"github.com/spacemeshos/merkle-tree"
"github.com/spacemeshos/sha256-simd"
Expand Down Expand Up @@ -171,58 +169,6 @@ func DecodeSliceOfByteSliceWithLimit(d *scale.Decoder, outerLimit, innerLimit ui
return result, total, nil
}

// FindSubmitPowNonce finds the nonce that solves the PoW challenge.
func FindSubmitPowNonce(
ctx context.Context,
powChallenge, poetChallenge, nodeID []byte,
difficulty uint,
) (uint64, error) {
var hash []byte
for nonce := uint64(0); ; nonce++ {
select {
case <-ctx.Done():
return 0, ctx.Err()
default:
}

hash := CalcSubmitPowHash(powChallenge, poetChallenge, nodeID, hash, nonce)
if CheckLeadingZeroBits(hash, difficulty) {
return nonce, nil
}
}
}

// CalcSubmitPowHash calculates the hash for the Submit PoW.
// The hash is ripemd256(powChallenge || nodeID || poetChallenge || nonce).
func CalcSubmitPowHash(powChallenge, poetChallenge, nodeID, output []byte, nonce uint64) []byte {
md := ripemd.New256()
md.Write(powChallenge)
md.Write(nodeID)
md.Write(poetChallenge)
if err := binary.Write(md, binary.LittleEndian, nonce); err != nil {
panic(err)
}
return md.Sum(output)
}

// CheckLeadingZeroBits checks if the first 'expected' bits of the byte array are all zero.
func CheckLeadingZeroBits(data []byte, expected uint) bool {
if len(data)*8 < int(expected) {
return false
}
for i := 0; i < int(expected/8); i++ {
if data[i] != 0 {
return false
}
}
if expected%8 != 0 {
if data[expected/8]>>(8-expected%8) != 0 {
return false
}
}
return true
}

// HashMembershipTreeNode calculates internal node of
// the membership merkle tree.
func HashMembershipTreeNode(buf, lChild, rChild []byte) []byte {
Expand Down
41 changes: 0 additions & 41 deletions shared/shared_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
package shared

import (
"context"
"encoding/binary"
"encoding/hex"
"fmt"
"math"
"testing"

Expand Down Expand Up @@ -111,42 +109,3 @@ func BenchmarkFiatShamir(b *testing.B) {
FiatShamir([]byte("challenge this"), uint64(30000000000), T)
}
}

func TestCheckLeadingZeroBits(t *testing.T) {
r := require.New(t)

r.True(CheckLeadingZeroBits([]byte{0x00}, 0))
r.True(CheckLeadingZeroBits([]byte{0x00}, 8))

// Out of bounds
r.False(CheckLeadingZeroBits([]byte{0x00}, 9))

r.True(CheckLeadingZeroBits([]byte{0x0F}, 4))
r.False(CheckLeadingZeroBits([]byte{0x0F}, 5))

r.True(CheckLeadingZeroBits([]byte{0x00, 0x0F}, 5))
r.True(CheckLeadingZeroBits([]byte{0x00, 0x0F}, 12))
r.False(CheckLeadingZeroBits([]byte{0x00, 0x0F}, 13))
}

func BenchmarkFindSubmitPowNonce(b *testing.B) {
powChallenge := make([]byte, 32)
poetChallenge := make([]byte, 32)
nodeID := make([]byte, 32)
b.ResetTimer()
for difficulty := 0; difficulty <= 24; difficulty += 4 {
b.Run(fmt.Sprintf("difficulty=%v", difficulty), func(b *testing.B) {
for i := 0; i < b.N; i++ {
binary.LittleEndian.PutUint64(nodeID, uint64(i))
_, err := FindSubmitPowNonce(
context.Background(),
powChallenge,
poetChallenge,
nodeID,
uint(difficulty),
)
require.NoError(b, err)
}
})
}
}