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

[Mining] refactor: difficulty in terms of target hash #690

Merged
merged 16 commits into from
Jul 24, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 92 additions & 74 deletions api/poktroll/proof/params.pulsar.go

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions e2e/tests/parse_params_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
package e2e

import (
"encoding/hex"
"fmt"
"strconv"

Expand Down Expand Up @@ -132,8 +133,8 @@ func (s *suite) newProofMsgUpdateParams(params paramsMap) cosmostypes.Msg {

for paramName, paramValue := range params {
switch paramName {
case prooftypes.ParamMinRelayDifficultyBits:
msgUpdateParams.Params.MinRelayDifficultyBits = uint64(paramValue.value.(int64))
case prooftypes.ParamRelayDifficultyTargetHash:
msgUpdateParams.Params.RelayDifficultyTargetHash, _ = hex.DecodeString(string(paramValue.value.([]byte)))
case prooftypes.ParamProofRequestProbability:
msgUpdateParams.Params.ProofRequestProbability = paramValue.value.(float32)
case prooftypes.ParamProofRequirementThreshold:
Expand Down
14 changes: 7 additions & 7 deletions e2e/tests/update_params.feature
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,11 @@ Feature: Params Namespace
And all "proof" module params are set to their default values
And an authz grant from the "gov" "module" account to the "pnf" "user" account for the "/poktroll.proof.MsgUpdateParams" message exists
When the "pnf" account sends an authz exec message to update all "proof" module params
| name | value | type |
| min_relay_difficulty_bits | 8 | int64 |
| proof_request_probability | 0.1 | float |
| proof_requirement_threshold | 100 | int64 |
| proof_missing_penalty | 500 | coin |
| name | value | type |
| relay_difficulty_target_hash | 00000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffff | bytes |
| proof_request_probability | 0.1 | float |
| proof_requirement_threshold | 100 | int64 |
| proof_missing_penalty | 500 | coin |
Then all "proof" module params should be updated

# NB: If you are reading this and the proof module has parameters
Expand Down Expand Up @@ -89,6 +89,6 @@ Feature: Params Namespace
And all "proof" module params are set to their default values
And an authz grant from the "gov" "module" account to the "pnf" "user" account for the "/poktroll.proof.MsgUpdateParams" message exists
When the "unauthorized" account sends an authz exec message to update "proof" the module param
| name | value | type |
| "min_relay_difficulty_bits | 666 | int64 |
| name | value | type |
| proof_request_probability | 0.1 | float |
Then the "proof" module param "min_relay_difficulty_bits" should be set to its default value
5 changes: 3 additions & 2 deletions e2e/tests/update_params_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
package e2e

import (
"encoding/hex"
"encoding/json"
"fmt"
"reflect"
Expand Down Expand Up @@ -370,9 +371,9 @@ func (s *suite) assertExpectedModuleParamsUpdated(moduleName string) {
params := prooftypes.DefaultParams()
paramsMap := s.expectedModuleParams[moduleName]

minRelayDifficultyBits, ok := paramsMap[prooftypes.ParamMinRelayDifficultyBits]
relayDifficultyTargetHash, ok := paramsMap[prooftypes.ParamRelayDifficultyTargetHash]
if ok {
params.MinRelayDifficultyBits = uint64(minRelayDifficultyBits.value.(int64))
params.RelayDifficultyTargetHash, _ = hex.DecodeString(string(relayDifficultyTargetHash.value.([]byte)))
}

proofRequestProbability, ok := paramsMap[prooftypes.ParamProofRequestProbability]
Expand Down
2 changes: 1 addition & 1 deletion pkg/client/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@ type BlockQueryClient interface {
// protobuf message. Since the generated go types don't include interface types, this
// is necessary to prevent dependency cycles.
type ProofParams interface {
GetMinRelayDifficultyBits() uint64
GetRelayDifficultyTargetHash() []byte
GetProofRequestProbability() float32
GetProofRequirementThreshold() uint64
GetProofMissingPenalty() *cosmostypes.Coin
Expand Down
35 changes: 21 additions & 14 deletions pkg/crypto/protocol/difficulty.go
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
package protocol

import (
"encoding/binary"
"math/bits"
"crypto/sha256"
"encoding/hex"
"math/big"
)

// CountHashDifficultyBits returns the number of leading zero bits in the given byte slice.
// TODO_MAINNET: Consider generalizing difficulty to a target hash. See:
// - https://bitcoin.stackexchange.com/questions/107976/bitcoin-difficulty-why-leading-0s
// - https://bitcoin.stackexchange.com/questions/121920/is-it-always-possible-to-find-a-number-whose-hash-starts-with-a-certain-number-o
// - https://github.com/pokt-network/poktroll/pull/656/files#r1666712528
func CountHashDifficultyBits(bz [32]byte) int {
// Using BigEndian for contiguous bit/byte ordering such leading zeros
// accumulate across adjacent bytes.
// E.g.: []byte{0, 0b00111111, 0x00, 0x00} has 10 leading zero bits. If
// LittleEndian were applied instead, it would have 18 leading zeros because it would
// look like []byte{0, 0, 0b00111111, 0}.
return bits.LeadingZeros64(binary.BigEndian.Uint64(bz[:]))
var (
// Difficulty1HashBz is the chosen "highest" (easiest) target hash, which
// corresponds to the lowest possible difficulty. It effectively calibrates
bryanchriswhite marked this conversation as resolved.
Show resolved Hide resolved
// the difficulty number (which is returned by GetDifficultyFromHash) by defining
// the hash which corresponds to difficulty 1.
Copy link
Member

Choose a reason for hiding this comment

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

#PUC I still don't fully understand what "difficulty 1" is.

Is it the first difficulty?
Is something here equal to / greater than / less than the number 1?
Is the difficulty itself 1?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We have to define a target hash which corresponds to the easiest difficulty so that we have a way to talk about the relative difficulty between it and any other target hash.

Copy link
Member

Choose a reason for hiding this comment

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

Why don't we call it BaseRelayDifficultyHash or MinRelayDifficultyHash?

// - https://bitcoin.stackexchange.com/questions/107976/bitcoin-difficulty-why-leading-0s
// - https://bitcoin.stackexchange.com/questions/121920/is-it-always-possible-to-find-a-number-whose-hash-starts-with-a-certain-number-o
Difficulty1HashBz, _ = hex.DecodeString("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")
)

// GetDifficultyFromHash returns the "difficulty" of the given hash, with respect
// to the "highest" (easiest) target hash, Difficulty1Hash.
func GetDifficultyFromHash(hashBz [sha256.Size]byte) int64 {
bryanchriswhite marked this conversation as resolved.
Show resolved Hide resolved
bryanchriswhite marked this conversation as resolved.
Show resolved Hide resolved
difficulty1HashInt := new(big.Int).SetBytes(Difficulty1HashBz)
hashInt := new(big.Int).SetBytes(hashBz[:])

// difficulty is the ratio of the highest target hash to the given hash.
return new(big.Int).Div(difficulty1HashInt, hashInt).Int64()
Olshansk marked this conversation as resolved.
Show resolved Hide resolved
}
60 changes: 32 additions & 28 deletions pkg/crypto/protocol/difficulty_test.go
Original file line number Diff line number Diff line change
@@ -1,51 +1,55 @@
package protocol_test
package protocol

import (
"fmt"
"crypto/sha256"
"encoding/hex"
"math/big"
"testing"

"github.com/stretchr/testify/require"

"github.com/pokt-network/poktroll/pkg/crypto/protocol"
)

func TestCountDifficultyBits(t *testing.T) {
func TestGetDifficultyFromHash(t *testing.T) {
tests := []struct {
bz []byte
difficulty int
desc string
hashHex string
expectedDifficulty int64
}{
{
bz: []byte{0b11111111},
difficulty: 0,
},
{
bz: []byte{0b01111111},
difficulty: 1,
desc: "Difficulty 1",
hashHex: "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
expectedDifficulty: 1,
},
{
bz: []byte{0, 255},
difficulty: 8,
desc: "Difficulty 2",
hashHex: "7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
expectedDifficulty: 2,
},
{
bz: []byte{0, 0b01111111},
difficulty: 9,
desc: "Difficulty 4",
hashHex: "3fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
expectedDifficulty: 4,
},
{
bz: []byte{0, 0b00111111},
difficulty: 10,
},
{
bz: []byte{0, 0, 255},
difficulty: 16,
desc: "Highest difficulty",
hashHex: "0000000000000000000000000000000000000000000000000000000000000001",
expectedDifficulty: new(big.Int).SetBytes(Difficulty1HashBz).Int64(),
},
}

for _, test := range tests {
t.Run(fmt.Sprintf("difficulty_%d_zero_bits", test.difficulty), func(t *testing.T) {
var bz [32]byte
copy(bz[:], test.bz)
actualDifficulty := protocol.CountHashDifficultyBits(bz)
require.Equal(t, test.difficulty, actualDifficulty)
t.Run(test.desc, func(t *testing.T) {
hashBytes, err := hex.DecodeString(test.hashHex)
if err != nil {
t.Fatalf("failed to decode hash: %v", err)
}

var hashBz [sha256.Size]byte
copy(hashBz[:], hashBytes)

difficulty := GetDifficultyFromHash(hashBz)
t.Logf("test: %s, difficulty: %d", test.desc, difficulty)
require.Equal(t, test.expectedDifficulty, difficulty)
})
}
}
10 changes: 10 additions & 0 deletions pkg/crypto/protocol/hash.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package protocol

import "crypto/sha256"

// GetHashFromBytes returns the hash of the relay (full, request or response) bytes.
// It is used as helper in the case that the relay is already marshaled and
// centralizes the hasher used.
func GetHashFromBytes(relayBz []byte) [sha256.Size]byte {
return sha256.Sum256(relayBz)
}
10 changes: 10 additions & 0 deletions pkg/crypto/rand/integer.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,16 @@ import (
func SeededInt63(seedParts ...[]byte) int64 {
seedHashInputBz := bytes.Join(append([][]byte{}, seedParts...), nil)
seedHash := crypto.Sha256(seedHashInputBz)

// TODO_MAINNET: To support other language implementations of the protocol, the
// pseudo-random number generator used here should be language-agnostic (i.e. not
// golang specific).
//
// Additionally, there is a precision loss here when converting the hash to an int64.
// Since the math/rand.Source interface only supports int64 seeds, we are forced to
// truncate the hash to 64 bits. This is not ideal, as it reduces the entropy of the
// seed. We should consider using a different random number generator that supports
// byte array seeds.
seed, _ := binary.Varint(seedHash)

return rand.NewSource(seed).Int63()
Expand Down
19 changes: 11 additions & 8 deletions pkg/relayer/miner/miner.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package miner

import (
"bytes"
"context"

"cosmossdk.io/depinject"
Expand All @@ -25,9 +26,11 @@ type miner struct {
// proofQueryClient is used to query for the minimum relay difficulty.
proofQueryClient client.ProofQueryClient

// relayDifficultyBits is the minimum difficulty that a relay must have to be
// volume / reward applicable.
relayDifficultyBits uint64
// relay_difficulty is the target hash which a relay hash must be less than to be volume/reward applicable.
//
// TODO_MAINNET(#543): This is populated by querying the corresponding on-chain parameter during construction.
// If this parameter is updated on-chain the relayminer will need to be restarted to query the new value.
relayDifficultyTargetHash []byte
}

// NewMiner creates a new miner from the given dependencies and options. It
Expand All @@ -37,7 +40,7 @@ type miner struct {
// - ProofQueryClient
//
// Available options:
// - WithDifficulty
// - WithRelayDifficultyTargetHash
Olshansk marked this conversation as resolved.
Show resolved Hide resolved
func NewMiner(
deps depinject.Config,
opts ...relayer.MinerOption,
Expand Down Expand Up @@ -91,8 +94,8 @@ func (mnr *miner) setDefaults() error {
return err
}

if mnr.relayDifficultyBits == 0 {
mnr.relayDifficultyBits = params.GetMinRelayDifficultyBits()
if len(mnr.relayDifficultyTargetHash) == 0 {
mnr.relayDifficultyTargetHash = params.GetRelayDifficultyTargetHash()
}
return nil
}
Expand All @@ -112,10 +115,10 @@ func (mnr *miner) mapMineRelay(
if err != nil {
return either.Error[*relayer.MinedRelay](err), false
}
relayHash := servicetypes.GetHashFromBytes(relayBz)
relayHash := protocol.GetHashFromBytes(relayBz)

// The relay IS NOT volume / reward applicable
if uint64(protocol.CountHashDifficultyBits(relayHash)) < mnr.relayDifficultyBits {
if bytes.Compare(relayHash[:], mnr.relayDifficultyTargetHash) == 1 {
Olshansk marked this conversation as resolved.
Show resolved Hide resolved
return either.Success[*relayer.MinedRelay](nil), true
}

Expand Down
9 changes: 5 additions & 4 deletions pkg/relayer/miner/miner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,19 @@ import (
"cosmossdk.io/depinject"
"github.com/stretchr/testify/require"

"github.com/pokt-network/poktroll/pkg/crypto/protocol"
"github.com/pokt-network/poktroll/pkg/observable/channel"
"github.com/pokt-network/poktroll/pkg/relayer"
"github.com/pokt-network/poktroll/pkg/relayer/miner"
"github.com/pokt-network/poktroll/testutil/testclient/testqueryclients"
servicetypes "github.com/pokt-network/poktroll/x/service/types"
)

const testDifficulty = uint64(16)
var testTargetHash, _ = hex.DecodeString("0000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")
Olshansk marked this conversation as resolved.
Show resolved Hide resolved

// TestMiner_MinedRelays constructs an observable of mined relays, through which
// it pipes pre-mined relay fixtures. It asserts that the observable only emits
// mined relays with difficulty equal to or greater than testDifficulty.
// mined relays with difficulty equal to or greater than testTargetHash.
//
// To regenerate all fixtures, use `make go_testgen_fixtures`; to regenerate only this
// test's fixtures run `go generate ./pkg/relayer/miner/miner_test.go`.
Expand All @@ -42,7 +43,7 @@ func TestMiner_MinedRelays(t *testing.T) {

proofQueryClientMock := testqueryclients.NewTestProofQueryClient(t)
deps := depinject.Supply(proofQueryClientMock)
mnr, err := miner.NewMiner(deps, miner.WithDifficulty(testDifficulty))
mnr, err := miner.NewMiner(deps, miner.WithRelayDifficultyTargetHash(testTargetHash))
require.NoError(t, err)

minedRelays := mnr.MinedRelays(ctx, mockRelaysObs)
Expand Down Expand Up @@ -134,7 +135,7 @@ func unmarshalHexMinedRelay(
require.NoError(t, err)

// TODO_TECHDEBT(@red-0ne, #446): Centralize the configuration for the SMT spec.
relayHashArr := servicetypes.GetHashFromBytes(relayBz)
relayHashArr := protocol.GetHashFromBytes(relayBz)
relayHash := relayHashArr[:]

return &relayer.MinedRelay{
Expand Down
7 changes: 3 additions & 4 deletions pkg/relayer/miner/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@ package miner

import "github.com/pokt-network/poktroll/pkg/relayer"

// WithDifficulty sets the difficulty of the miner, where difficultyBytes is the
// minimum number of leading zero bytes.
func WithDifficulty(difficultyBits uint64) relayer.MinerOption {
// WithRelayDifficultyTargetHash sets the relayDifficultyTargetHash of the miner.
func WithRelayDifficultyTargetHash(targetHash []byte) relayer.MinerOption {
return func(mnr relayer.Miner) {
mnr.(*miner).relayDifficultyBits = difficultyBits
mnr.(*miner).relayDifficultyTargetHash = targetHash
}
}
5 changes: 2 additions & 3 deletions proto/poktroll/proof/params.proto
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,8 @@ message Params {
option (amino.name) = "poktroll/x/proof/Params";
option (gogoproto.equal) = true;

// min_relay_difficulty_bits is the minimum difficulty in bits for a relay to
// be included in a Merkle proof.
uint64 min_relay_difficulty_bits = 1 [(gogoproto.jsontag) = "min_relay_difficulty_bits"];
// relay_difficulty_target_hash is the maximum value a relay hash must be less than to be volume/reward applicable.
bytes relay_difficulty_target_hash = 1 [(gogoproto.jsontag) = "relay_difficulty_target_hash"];

// proof_request_probability is the probability of a session requiring a proof
// if it's cost (i.e. compute unit consumption) is below the ProofRequirementThreshold.
Expand Down
10 changes: 5 additions & 5 deletions telemetry/event_counters.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,18 +147,18 @@ func ClaimCounter(
)
}

// RelayMiningDifficultyGauge sets a gauge which tracks the relay mining difficulty,
// which is represented by number of leading zero bits.
// The serviceId is used as a label to be able to track the difficulty for each service.
func RelayMiningDifficultyGauge(numbLeadingZeroBits int, serviceId string) {
// RelayMiningDifficultyGauge sets a gauge which tracks the integer representation
// of the relay mining difficulty. The serviceId is used as a label to be able to
// track the difficulty for each service.
func RelayMiningDifficultyGauge(difficulty int64, serviceId string) {
labels := []metrics.Label{
{Name: "type", Value: "relay_mining_difficulty"},
{Name: "service_id", Value: serviceId},
}

telemetry.SetGaugeWithLabels(
[]string{eventTypeMetricKeyGauge},
float32(numbLeadingZeroBits),
float32(difficulty),
labels,
)
}
Expand Down
4 changes: 2 additions & 2 deletions tests/integration/tokenomics/relay_mining_difficulty_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,8 @@ func TestUpdateRelayMiningDifficulty_NewServiceSeenForTheFirstTime(t *testing.T)
relayMiningEvent := relayMiningEvents[0]
require.Equal(t, "svc1", relayMiningEvent.ServiceId)
// The default difficulty)
require.Equal(t, "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", relayMiningEvent.PrevTargetHashHexEncoded)
require.Equal(t, "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", relayMiningEvent.NewTargetHashHexEncoded)
require.Equal(t, prooftypes.DefaultRelayDifficultyTargetHashHex, relayMiningEvent.PrevTargetHashHexEncoded)
require.Equal(t, prooftypes.DefaultRelayDifficultyTargetHashHex, relayMiningEvent.NewTargetHashHexEncoded)
// The previous EMA is the same as the current one if the service is new
require.Equal(t, uint64(1), relayMiningEvent.PrevNumRelaysEma)
require.Equal(t, uint64(1), relayMiningEvent.NewNumRelaysEma)
Expand Down
Loading
Loading