From 9056e5339a7a08d17d69f069507666ec95e74f64 Mon Sep 17 00:00:00 2001 From: Redouane Lakrache Date: Sat, 7 Sep 2024 01:24:58 +0200 Subject: [PATCH] [SessionManager] Implement off-chain proof params usage (#765) ## Summary This PR introduces the proof params to the `RelayMiner` so it does not submit proofs when not required. The E2E and `SessionManager` tests have been updated to account for proof requirement. ## Issue The `RelayMiner`'s `SessionManager` was systematically submitting proofs on-chain whether they were required or not. This led to a state chain bloat since proofs are what takes most for the block space. Given the permissionless nature of the protocol, this causing scalability issues. - #758 ## Type of change Select one or more: - [x] New feature, functionality or library - [ ] Bug fix - [ ] Code health or cleanup - [ ] Documentation - [ ] Other (specify) ## Testing - [ ] **Documentation**: `make docusaurus_start`; only needed if you make doc changes - [x] **Unit Tests**: `make go_develop_and_test` - [x] **LocalNet E2E Tests**: `make test_e2e` - [ ] **DevNet E2E Tests**: Add the `devnet-test-e2e` label to the PR. ## Sanity Checklist - [x] I have tested my changes using the available tooling - [x] I have commented my code - [x] I have performed a self-review of my own code; both comments & source code - [ ] I create and reference any new tickets, if applicable - [x] I have left TODOs throughout the codebase, if applicable --------- Co-authored-by: Daniel Olshansky --- e2e/tests/0_settlement.feature | 40 ++++- e2e/tests/init_test.go | 2 +- e2e/tests/session.feature | 11 +- e2e/tests/session_steps_test.go | 18 +- pkg/relayer/cmd/cmd.go | 1 + pkg/relayer/session/claim.go | 9 +- pkg/relayer/session/proof.go | 168 +++++++++++++----- pkg/relayer/session/session.go | 7 + pkg/relayer/session/session_test.go | 66 ++++++- .../testqueryclients/proofquerier.go | 17 +- .../testqueryclients/servicequerier.go | 6 +- testutil/testclient/testsupplier/client.go | 8 +- x/proof/types/claim.go | 29 +++ x/proof/types/params.go | 3 + x/tokenomics/keeper/settle_pending_claims.go | 46 +++-- 15 files changed, 348 insertions(+), 83 deletions(-) diff --git a/e2e/tests/0_settlement.feature b/e2e/tests/0_settlement.feature index 358d2cd27..0b0e47692 100644 --- a/e2e/tests/0_settlement.feature +++ b/e2e/tests/0_settlement.feature @@ -7,7 +7,7 @@ Feature: Tokenomics Namespace - Scenario: Emissions equals burn when a claim is created and a valid proof is submitted and required + Scenario: Emissions equals burn when a claim is created and a valid proof is submitted and required via threshold # Baseline Given the user has the pocketd binary installed # Network preparation @@ -17,11 +17,47 @@ Feature: Tokenomics Namespace And the "application" account for "app1" is staked And the service "anvil" registered for application "app1" has a compute units per relay of "1" # Start servicing + # Set proof_requirement_threshold to 9 < num_relays (10) * compute_units_per_relay (1) + # to make sure a proof is required. + And the "proof" module parameters are set as follows + | name | value | type | + | relay_difficulty_target_hash | ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff | bytes | + | proof_request_probability | 0.25 | float | + | proof_requirement_threshold | 9 | int64 | + | proof_missing_penalty | 320 | coin | + | proof_submission_fee | 1000000 | coin | When the supplier "supplier1" has serviced a session with "10" relays for service "anvil" for application "app1" # Wait for the Claim & Proof lifecycle And the user should wait for the "proof" module "CreateClaim" Message to be submitted And the user should wait for the "proof" module "SubmitProof" Message to be submitted - And the user should wait for the "tokenomics" module "ClaimSettled" end block event to be broadcast + And the user should wait for the ClaimSettled event with "THRESHOLD" proof requirement to be broadcast + # Validate the results + Then the account balance of "supplier1" should be "420" uPOKT "more" than before + And the "application" stake of "app1" should be "420" uPOKT "less" than before + + Scenario: Emissions equals burn when a claim is created but a proof is not required + # Baseline + Given the user has the pocketd binary installed + # Network preparation + And an account exists for "supplier1" + And the "supplier" account for "supplier1" is staked + And an account exists for "app1" + And the "application" account for "app1" is staked + And the service "anvil" registered for application "app1" has a compute units per relay of "1" + # Set proof_request_probability to 0 and proof_requirement_threshold to 100 to make sure a proof is not required. + And the "proof" module parameters are set as follows + | name | value | type | + | relay_difficulty_target_hash | ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff | bytes | + | proof_request_probability | 0 | float | + | proof_requirement_threshold | 100 | int64 | + | proof_missing_penalty | 320 | coin | + | proof_submission_fee | 1000000 | coin | + # Start servicing + When the supplier "supplier1" has serviced a session with "10" relays for service "anvil" for application "app1" + # Wait for the Claim & Proof lifecycle + And the user should wait for the "proof" module "CreateClaim" Message to be submitted + # No proof should be submitted, don't wait for one. + And the user should wait for the ClaimSettled event with "NOT_REQUIRED" proof requirement to be broadcast # Validate the results Then the account balance of "supplier1" should be "420" uPOKT "more" than before And the "application" stake of "app1" should be "420" uPOKT "less" than before diff --git a/e2e/tests/init_test.go b/e2e/tests/init_test.go index 5e570d1ef..dad073c97 100644 --- a/e2e/tests/init_test.go +++ b/e2e/tests/init_test.go @@ -331,7 +331,7 @@ func (s *suite) getConfigFileContent( services: - service_id: %s endpoints: - - publicly_exposed_url: http://relayminer:8545 + - publicly_exposed_url: http://relayminer1:8545 rpc_type: json_rpc`, ownerAddress, operatorAddress, amount, serviceId) default: diff --git a/e2e/tests/session.feature b/e2e/tests/session.feature index 8fb0c4059..37d3e279c 100644 --- a/e2e/tests/session.feature +++ b/e2e/tests/session.feature @@ -2,7 +2,16 @@ Feature: Session Namespace Scenario: Supplier completes claim/proof lifecycle for a valid session Given the user has the pocketd binary installed - When the supplier "supplier1" has serviced a session with "5" relays for service "svc1" for application "app1" + # Set proof_requirement_threshold to 4 < num_relays (5) * compute_units_per_relay (1) + # to make sure a proof is required. + And the "proof" module parameters are set as follows + | name | value | type | + | relay_difficulty_target_hash | ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff | bytes | + | proof_request_probability | 0.25 | float | + | proof_requirement_threshold | 4 | int64 | + | proof_missing_penalty | 320 | coin | + | proof_submission_fee | 1000000 | coin | + When the supplier "supplier1" has serviced a session with "5" relays for service "anvil" for application "app1" And the user should wait for the "proof" module "CreateClaim" Message to be submitted And the user should wait for the "proof" module "ClaimCreated" tx event to be broadcast Then the claim created by supplier "supplier1" for service "svc1" for application "app1" should be persisted on-chain diff --git a/e2e/tests/session_steps_test.go b/e2e/tests/session_steps_test.go index a06b5f8aa..079452d7b 100644 --- a/e2e/tests/session_steps_test.go +++ b/e2e/tests/session_steps_test.go @@ -11,6 +11,7 @@ import ( abci "github.com/cometbft/cometbft/abci/types" cosmostypes "github.com/cosmos/cosmos-sdk/types" + "github.com/regen-network/gocuke" "github.com/stretchr/testify/require" "github.com/pokt-network/poktroll/pkg/client/block" @@ -65,11 +66,12 @@ func (s *suite) TheUserShouldWaitForTheModuleTxEventToBeBroadcast(module, eventT s.waitForTxResultEvent(newEventTypeMatchFn(module, eventType)) } -func (s *suite) TheUserShouldWaitForTheModuleEndBlockEventToBeBroadcast(module, eventType string) { +func (s *suite) TheUserShouldWaitForTheClaimsettledEventWithProofRequirementToBeBroadcast(proofRequirement string) { s.waitForNewBlockEvent( combineEventMatchFns( - newEventTypeMatchFn(module, eventType), + newEventTypeMatchFn("tokenomics", "ClaimSettled"), newEventModeMatchFn("EndBlock"), + newEventAttributeMatchFn("proof_requirement", fmt.Sprintf("%q", proofRequirement)), ), ) } @@ -167,6 +169,18 @@ func (s *suite) TheClaimCreatedBySupplierForServiceForApplicationShouldBeSuccess s.waitForNewBlockEvent(isValidClaimSettledEvent) } +func (suite *suite) TheModuleParametersAreSetAsFollows(moduleName string, params gocuke.DataTable) { + suite.AnAuthzGrantFromTheAccountToTheAccountForTheMessageExists( + "gov", + "module", + "pnf", + "user", + fmt.Sprintf("/poktroll.%s.MsgUpdateParams", moduleName), + ) + + suite.TheAccountSendsAnAuthzExecMessageToUpdateAllModuleParams("pnf", moduleName, params) +} + func (s *suite) sendRelaysForSession( appName string, supplierOperatorName string, diff --git a/pkg/relayer/cmd/cmd.go b/pkg/relayer/cmd/cmd.go index a3242e4da..acd31896b 100644 --- a/pkg/relayer/cmd/cmd.go +++ b/pkg/relayer/cmd/cmd.go @@ -201,6 +201,7 @@ func setupRelayerDependencies( config.NewSupplySupplierQuerierFn(), config.NewSupplySessionQuerierFn(), config.NewSupplyServiceQueryClientFn(), + config.NewSupplyProofQueryClientFn(), config.NewSupplyRingCacheFn(), supplyTxFactory, supplyTxContext, diff --git a/pkg/relayer/session/claim.go b/pkg/relayer/session/claim.go index 39cca4404..44ef4edc7 100644 --- a/pkg/relayer/session/claim.go +++ b/pkg/relayer/session/claim.go @@ -180,14 +180,13 @@ func (rs *relayerSessionsManager) newMapClaimSessionsFn( return either.Success(sessionTrees), false } - // Map key is the supplier operator address. - claimMsgs := make([]client.MsgCreateClaim, 0) - for _, sessionTree := range sessionTrees { - claimMsgs = append(claimMsgs, &prooftypes.MsgCreateClaim{ + claimMsgs := make([]client.MsgCreateClaim, len(sessionTrees)) + for idx, sessionTree := range sessionTrees { + claimMsgs[idx] = &prooftypes.MsgCreateClaim{ RootHash: sessionTree.GetClaimRoot(), SessionHeader: sessionTree.GetSessionHeader(), SupplierOperatorAddress: sessionTree.GetSupplierOperatorAddress().String(), - }) + } } // Create claims for each supplier operator address in `sessionTrees`. diff --git a/pkg/relayer/session/proof.go b/pkg/relayer/session/proof.go index ac321e853..da8347c0b 100644 --- a/pkg/relayer/session/proof.go +++ b/pkg/relayer/session/proof.go @@ -13,6 +13,7 @@ import ( "github.com/pokt-network/poktroll/pkg/observable/logging" "github.com/pokt-network/poktroll/pkg/relayer" "github.com/pokt-network/poktroll/x/proof/types" + prooftypes "github.com/pokt-network/poktroll/x/proof/types" "github.com/pokt-network/poktroll/x/shared" ) @@ -136,39 +137,19 @@ func (rs *relayerSessionsManager) waitForEarliestSubmitProofsHeightAndGeneratePr logger = logger.With("earliest_supplier_proof_commit_height", earliestSupplierProofsCommitHeight) logger.Info().Msg("waiting & blocking for proof path seed block height") - // proofWindowOpenHeight - 1 is the block that will have its hash used as the - // source of entropy for all the session trees in that batch, waiting for it to - // be received before proceeding. + // earliestSupplierProofsCommitHeight - 1 is the block that will have its hash + // used as the source of entropy for all the session trees in that batch, + // waiting for it to be received before proceeding. proofPathSeedBlockHeight := earliestSupplierProofsCommitHeight - 1 proofPathSeedBlock := rs.waitForBlock(ctx, proofPathSeedBlockHeight) - logger = logger.With("proof_path_bock_hash", fmt.Sprintf("%x", proofPathSeedBlock.Hash())) + logger = logger.With("proof_path_seed_block", fmt.Sprintf("%x", proofPathSeedBlock.Hash())) logger.Info().Msg("observed proof path seed block height") - // Generate proofs for all sessionTrees concurrently while waiting for the - // earliest submitProofsHeight (pseudorandom submission distribution) to be reached. - // Use a channel to block until all proofs for the sessionTrees have been generated. - proofsGeneratedCh := make(chan []relayer.SessionTree) - defer close(proofsGeneratedCh) - go rs.goProveClaims( - ctx, - sessionTrees, - proofPathSeedBlock, - proofsGeneratedCh, - failedSubmitProofsSessionsCh, - ) - - logger.Info().Msg("waiting & blocking for earliest supplier proof commit height") - - // Wait for the earliestSupplierProofsCommitHeight to be reached before proceeding. - _ = rs.waitForBlock(ctx, earliestSupplierProofsCommitHeight) + successProofs, failedProofs := rs.proveClaims(ctx, sessionTrees, proofPathSeedBlock) + failedSubmitProofsSessionsCh <- failedProofs - logger.Info().Msg("observed earliest supplier proof commit height") - - // Once the earliest submitProofsHeight has been reached, and all proofs have - // been generated, return the sessionTrees that have been successfully proven - // to be submitted on-chain. - return <-proofsGeneratedCh + return successProofs } // newMapProveSessionsFn returns a new MapFn that submits proofs on the given @@ -187,13 +168,13 @@ func (rs *relayerSessionsManager) newMapProveSessionsFn( } // Map key is the supplier operator address. - proofMsgs := make([]client.MsgSubmitProof, 0) - for _, session := range sessionTrees { - proofMsgs = append(proofMsgs, &types.MsgSubmitProof{ + proofMsgs := make([]client.MsgSubmitProof, len(sessionTrees)) + for idx, session := range sessionTrees { + proofMsgs[idx] = &types.MsgSubmitProof{ Proof: session.GetProofBz(), SessionHeader: session.GetSessionHeader(), SupplierOperatorAddress: session.GetSupplierOperatorAddress().String(), - }) + } } // Submit proofs for each supplier operator address in `sessionTrees`. @@ -216,32 +197,45 @@ func (rs *relayerSessionsManager) newMapProveSessionsFn( } } -// goProveClaims generates the proofs corresponding to the given sessionTrees, +// proveClaims generates the proofs corresponding to the given sessionTrees, // then sends the successful and failed proofs to their respective channels. -// This function MUST be run as a goroutine. -func (rs *relayerSessionsManager) goProveClaims( +func (rs *relayerSessionsManager) proveClaims( ctx context.Context, sessionTrees []relayer.SessionTree, - sessionPathBlock client.Block, - proofsGeneratedCh chan<- []relayer.SessionTree, - failSubmitProofsSessionsCh chan<- []relayer.SessionTree, -) { + // The hash of this block is used to determine which branch of the proof + // should be generated for. + proofPathSeedBlock client.Block, +) (successProofs []relayer.SessionTree, failedProofs []relayer.SessionTree) { logger := rs.logger.With("method", "goProveClaims") - // Separate the sessionTrees into those that failed to generate a proof - // and those that succeeded, then send them on their respective channels. - failedProofs := []relayer.SessionTree{} - successProofs := []relayer.SessionTree{} + // sessionTreesWithProofRequired will accumulate all the sessionTrees that + // will require a proof to be submitted. + sessionTreesWithProofRequired := make([]relayer.SessionTree, 0) for _, sessionTree := range sessionTrees { - select { - case <-ctx.Done(): - return - default: + isProofRequired, err := rs.isProofRequired(ctx, sessionTree, proofPathSeedBlock) + + // If an error is encountered while determining if a proof is required, + // do not create the claim since the proof requirement is unknown. + // WARNING: Creating a claim and not submitting a proof (if necessary) could lead to a stake burn!! + if err != nil { + failedProofs = append(failedProofs, sessionTree) + rs.logger.Error().Err(err).Msg("failed to determine if proof is required, skipping claim creation") + continue } + + // If a proof is required, add the session to the list of sessions that require a proof. + if isProofRequired { + sessionTreesWithProofRequired = append(sessionTreesWithProofRequired, sessionTree) + } + } + + // Separate the sessionTrees into those that failed to generate a proof + // and those that succeeded, before returning each of them. + for _, sessionTree := range sessionTreesWithProofRequired { // Generate the proof path for the sessionTree using the previously committed // sessionPathBlock hash. path := protocol.GetPathForProof( - sessionPathBlock.Hash(), + proofPathSeedBlock.Hash(), sessionTree.GetSessionHeader().GetSessionId(), ) @@ -258,6 +252,82 @@ func (rs *relayerSessionsManager) goProveClaims( successProofs = append(successProofs, sessionTree) } - failSubmitProofsSessionsCh <- failedProofs - proofsGeneratedCh <- successProofs + return successProofs, failedProofs +} + +// isProofRequired determines whether a proof is required for the given session's +// claim based on the current proof module governance parameters. +// TODO_TECHDEBT: Refactor the method to be static and used both on-chain and off-chain. +// TODO_INVESTIGATE: Passing a polylog.Logger should allow for on-chain/off-chain +// usage of this function but it is currently raising a type error. +func (rs *relayerSessionsManager) isProofRequired( + ctx context.Context, + sessionTree relayer.SessionTree, + // The hash of this block is used to determine whether the proof is required + // w.r.t. the probabilistic features. + proofRequirementSeedBlock client.Block, +) (isProofRequired bool, err error) { + logger := rs.logger.With( + "session_id", sessionTree.GetSessionHeader().GetSessionId(), + "claim_root", fmt.Sprintf("%x", sessionTree.GetClaimRoot()), + "supplier_operator_address", sessionTree.GetSupplierOperatorAddress().String(), + ) + + // Create the claim object and use its methods to determine if a proof is required. + claim := claimFromSessionTree(sessionTree) + + // Get the number of compute units accumulated through the given session. + numClaimComputeUnits, err := claim.GetNumComputeUnits() + if err != nil { + return false, err + } + + proofParams, err := rs.proofQueryClient.GetParams(ctx) + if err != nil { + return false, err + } + + logger = logger.With( + "num_claim_compute_units", numClaimComputeUnits, + "proof_requirement_threshold", proofParams.GetProofRequirementThreshold(), + ) + + // Require a proof if the claim's compute units meets or exceeds the threshold. + // TODO_MAINNET: This should be proportional to the supplier's stake as well. + if numClaimComputeUnits >= proofParams.GetProofRequirementThreshold() { + logger.Info().Msg("compute units is above threshold, claim requires proof") + + return true, nil + } + + proofRequirementSampleValue, err := claim.GetProofRequirementSampleValue(proofRequirementSeedBlock.Hash()) + if err != nil { + return false, err + } + + logger = logger.With( + "proof_requirement_sample_value", proofRequirementSampleValue, + "proof_request_probability", proofParams.GetProofRequestProbability(), + ) + + // Require a proof probabilistically based on the proof_request_probability param. + // NB: A random value between 0 and 1 will be less than or equal to proof_request_probability + // with probability equal to the proof_request_probability. + if proofRequirementSampleValue <= proofParams.GetProofRequestProbability() { + logger.Info().Msg("claim hash seed is below proof request probability, claim requires proof") + + return true, nil + } + + logger.Info().Msg("claim does not require proof") + return false, nil +} + +// claimFromSessionTree creates a Claim object from the given SessionTree. +func claimFromSessionTree(sessionTree relayer.SessionTree) prooftypes.Claim { + return prooftypes.Claim{ + SupplierOperatorAddress: sessionTree.GetSupplierOperatorAddress().String(), + SessionHeader: sessionTree.GetSessionHeader(), + RootHash: sessionTree.GetClaimRoot(), + } } diff --git a/pkg/relayer/session/session.go b/pkg/relayer/session/session.go index bb63746ca..e03a95a55 100644 --- a/pkg/relayer/session/session.go +++ b/pkg/relayer/session/session.go @@ -59,6 +59,11 @@ type relayerSessionsManager struct { // This is used to get the ComputeUnitsPerRelay, which is used as the weight of a mined relay // when adding a mined relay to a session's tree. serviceQueryClient client.ServiceQueryClient + + // proofQueryClient is used to query for the proof requirement threshold and + // requirement probability governance parameters to determine whether a submitted + // claim requires a proof. + proofQueryClient client.ProofQueryClient } // NewRelayerSessions creates a new relayerSessions. @@ -66,6 +71,7 @@ type relayerSessionsManager struct { // Required dependencies: // - client.BlockClient // - client.SupplierClientMap +// - client.ProofQueryClient // // Available options: // - WithStoresDirectory @@ -87,6 +93,7 @@ func NewRelayerSessions( &rs.supplierClients, &rs.sharedQueryClient, &rs.serviceQueryClient, + &rs.proofQueryClient, ); err != nil { return nil, err } diff --git a/pkg/relayer/session/session_test.go b/pkg/relayer/session/session_test.go index d9bbe56ba..e87f81576 100644 --- a/pkg/relayer/session/session_test.go +++ b/pkg/relayer/session/session_test.go @@ -3,6 +3,7 @@ package session_test import ( "context" "crypto/sha256" + "math" "testing" "time" @@ -25,6 +26,7 @@ import ( "github.com/pokt-network/poktroll/testutil/testclient/testsupplier" "github.com/pokt-network/poktroll/testutil/testpolylog" "github.com/pokt-network/poktroll/testutil/testrelayer" + prooftypes "github.com/pokt-network/poktroll/x/proof/types" sessiontypes "github.com/pokt-network/poktroll/x/session/types" "github.com/pokt-network/poktroll/x/shared" sharedtypes "github.com/pokt-network/poktroll/x/shared/types" @@ -32,22 +34,34 @@ import ( // TODO_TEST: Add a test case which simulates a cold-started relayminer with unclaimed relays. +// requireProofCountEqualsExpectedValueFromProofParams sets up the session manager +// along with its dependencies before starting it. +// It takes in the proofParams to configure the proof requirements and the proofCount +// to assert the number of proofs to be requested. // TODO_BETA(@red-0ne): Add a test case which verifies that the service's compute units per relay is used as // the weight of a relay when updating a session's SMT. - -func TestRelayerSessionsManager_Start(t *testing.T) { +func requireProofCountEqualsExpectedValueFromProofParams(t *testing.T, proofParams prooftypes.Params, proofCount int) { // TODO_TECHDEBT(#446): Centralize the configuration for the SMT spec. var ( _, ctx = testpolylog.NewLoggerWithCtx(context.Background(), polyzero.DebugLevel) spec = smt.NewTrieSpec(sha256.New(), true) emptyBlockHash = make([]byte, spec.PathHasherSize()) activeSession *sessiontypes.Session + service sharedtypes.Service ) + service = sharedtypes.Service{ + Id: "svc", + ComputeUnitsPerRelay: 2, + } + // Add the service to the existing services. + testqueryclients.AddToExistingServices(t, service) + activeSession = &sessiontypes.Session{ Header: &sessiontypes.SessionHeader{ SessionStartBlockHeight: 1, SessionEndBlockHeight: 2, + Service: &service, }, } sessionHeader := activeSession.GetHeader() @@ -56,7 +70,7 @@ func TestRelayerSessionsManager_Start(t *testing.T) { blocksObs, blockPublishCh := channel.NewReplayObservable[client.Block](ctx, 20) blockClient := testblock.NewAnyTimesCommittedBlocksSequenceBlockClient(t, emptyBlockHash, blocksObs) supplierOperatorAddress := sample.AccAddress() - supplierClientMap := testsupplier.NewOneTimeClaimProofSupplierClientMap(ctx, t, supplierOperatorAddress) + supplierClientMap := testsupplier.NewClaimProofSupplierClientMap(ctx, t, supplierOperatorAddress, proofCount) ctrl := gomock.NewController(t) blockQueryClientMock := mockclient.NewMockCometRPC(ctrl) @@ -88,6 +102,7 @@ func TestRelayerSessionsManager_Start(t *testing.T) { sharedQueryClientMock := testqueryclients.NewTestSharedQueryClient(t) serviceQueryClientMock := testqueryclients.NewTestServiceQueryClient(t) + proofQueryClientMock := testqueryclients.NewTestProofQueryClientWithParams(t, &proofParams) deps := depinject.Supply( blockClient, @@ -95,6 +110,7 @@ func TestRelayerSessionsManager_Start(t *testing.T) { supplierClientMap, sharedQueryClientMock, serviceQueryClientMock, + proofQueryClientMock, ) storesDirectoryOpt := testrelayer.WithTempStoresDirectory(t) @@ -172,6 +188,50 @@ func TestRelayerSessionsManager_Start(t *testing.T) { waitSimulateIO() } +func TestRelayerSessionsManager_ProofThresholdRequired(t *testing.T) { + proofParams := prooftypes.DefaultParams() + + // Set proof requirement threshold to a low enough value so a proof is always requested. + proofParams.ProofRequirementThreshold = 1 + + // The test is submitting a single claim. Having the proof requirement threshold + // set to 1 results in exactly 1 proof being requested. + numExpectedProofs := 1 + + requireProofCountEqualsExpectedValueFromProofParams(t, proofParams, numExpectedProofs) +} + +func TestRelayerSessionsManager_ProofProbabilityRequired(t *testing.T) { + proofParams := prooftypes.DefaultParams() + + // Set proof requirement threshold to max uint64 to skip the threshold check. + proofParams.ProofRequirementThreshold = math.MaxUint64 + // Set proof request probability to 1 so a proof is always requested. + proofParams.ProofRequestProbability = 1 + + // The test is submitting a single claim. Having the proof request probability + // set to 1 results in exactly 1 proof being requested. + numExpectedProofs := 1 + + requireProofCountEqualsExpectedValueFromProofParams(t, proofParams, numExpectedProofs) +} + +func TestRelayerSessionsManager_ProofNotRequired(t *testing.T) { + proofParams := prooftypes.DefaultParams() + + // Set proof requirement threshold to max uint64 to skip the threshold check. + proofParams.ProofRequirementThreshold = math.MaxUint64 + // Set proof request probability to 0 so a proof is never requested. + proofParams.ProofRequestProbability = 0 + + // The test is submitting a single claim. Having the proof request probability + // set to 0 and proof requirement threshold set to max uint64 results in no proofs + // being requested. + numExpectedProofs := 0 + + requireProofCountEqualsExpectedValueFromProofParams(t, proofParams, numExpectedProofs) +} + // waitSimulateIO sleeps for a bit to allow the relayer sessions manager to // process asynchronously. This effectively simulates I/O delays which would // normally be present. diff --git a/testutil/testclient/testqueryclients/proofquerier.go b/testutil/testclient/testqueryclients/proofquerier.go index efcc11342..ac8962814 100644 --- a/testutil/testclient/testqueryclients/proofquerier.go +++ b/testutil/testclient/testqueryclients/proofquerier.go @@ -5,20 +5,27 @@ import ( "github.com/golang/mock/gomock" + "github.com/pokt-network/poktroll/pkg/client" "github.com/pokt-network/poktroll/testutil/mockclient" prooftypes "github.com/pokt-network/poktroll/x/proof/types" ) -// NewTestProofQueryClient creates a mock of the ProofQueryClient which uses the -// default proof module params for its GetParams() method implementation. -func NewTestProofQueryClient(t *testing.T) *mockclient.MockProofQueryClient { +// NewTestProofQueryClientWithParams creates a mock of the ProofQueryClient that +// uses the provided proof module params for its GetParams() method implementation. +func NewTestProofQueryClientWithParams(t *testing.T, params client.ProofParams) *mockclient.MockProofQueryClient { ctrl := gomock.NewController(t) - defaultProofParams := prooftypes.DefaultParams() proofQueryClientMock := mockclient.NewMockProofQueryClient(ctrl) proofQueryClientMock.EXPECT(). GetParams(gomock.Any()). - Return(&defaultProofParams, nil). + Return(params, nil). AnyTimes() return proofQueryClientMock } + +// NewTestProofQueryClient creates a mock of the ProofQueryClient which uses the +// default proof module params for its GetParams() method implementation. +func NewTestProofQueryClient(t *testing.T) *mockclient.MockProofQueryClient { + defaultProofParams := prooftypes.DefaultParams() + return NewTestProofQueryClientWithParams(t, &defaultProofParams) +} diff --git a/testutil/testclient/testqueryclients/servicequerier.go b/testutil/testclient/testqueryclients/servicequerier.go index 394e5e75e..0557078c0 100644 --- a/testutil/testclient/testqueryclients/servicequerier.go +++ b/testutil/testclient/testqueryclients/servicequerier.go @@ -30,13 +30,13 @@ func NewTestServiceQueryClient( DoAndReturn(func( _ context.Context, serviceId string, - ) (*sharedtypes.Service, error) { + ) (sharedtypes.Service, error) { service, ok := services[serviceId] if !ok { - return nil, prooftypes.ErrProofServiceNotFound.Wrapf("service %s not found", serviceId) + return sharedtypes.Service{}, prooftypes.ErrProofServiceNotFound.Wrapf("service %s not found", serviceId) } - return &service, nil + return service, nil }). AnyTimes() diff --git a/testutil/testclient/testsupplier/client.go b/testutil/testclient/testsupplier/client.go index 838b24707..7b8e55d0c 100644 --- a/testutil/testclient/testsupplier/client.go +++ b/testutil/testclient/testsupplier/client.go @@ -40,10 +40,14 @@ func NewLocalnetClient( return supplierClient } -func NewOneTimeClaimProofSupplierClientMap( +// NewClaimProofSupplierClientMap creates and returns a map of supplier to supplier +// client mocks. Each supplier client is expected to submit exactly 1 claim and +// exactly proofCount proofs. +func NewClaimProofSupplierClientMap( ctx context.Context, t *testing.T, supplierOperatorAddress string, + proofCount int, ) *supplier.SupplierClientMap { t.Helper() @@ -70,7 +74,7 @@ func NewOneTimeClaimProofSupplierClientMap( gomock.AssignableToTypeOf(([]client.MsgSubmitProof)(nil)), ). Return(nil). - Times(1) + Times(proofCount) supplierClientMap := supplier.NewSupplierClientMap() supplierClientMap.SupplierClients[supplierOperatorAddress] = supplierClientMock diff --git a/x/proof/types/claim.go b/x/proof/types/claim.go index a9c9bad3a..614cc9cb5 100644 --- a/x/proof/types/claim.go +++ b/x/proof/types/claim.go @@ -3,6 +3,7 @@ package types import ( "github.com/cometbft/cometbft/crypto" + poktrand "github.com/pokt-network/poktroll/pkg/crypto/rand" "github.com/pokt-network/smt" ) @@ -27,3 +28,31 @@ func (claim *Claim) GetHash() ([]byte, error) { return crypto.Sha256(claimBz), nil } + +// GetProofRequirementSampleValue returns a pseudo-random value between 0 and 1 to +// determine if a proof is required probabilistically. +// IMPORTANT: It is assumed that the caller has ensured the hash of the block seed +func (claim *Claim) GetProofRequirementSampleValue( + proofRequirementSeedBlockHash []byte, +) (proofRequirementSampleValue float32, err error) { + // Get the hash of the claim to seed the random number generator. + var claimHash []byte + claimHash, err = claim.GetHash() + if err != nil { + return 0, err + } + + // Append the hash of the proofRequirementSeedBlockHash to the claim hash to seed + // the random number generator to ensure that the proof requirement probability + // is unknown until the proofRequirementSeedBlockHash is observed. + proofRequirementSeed := append(claimHash, proofRequirementSeedBlockHash...) + + // Sample a pseudo-random value between 0 and 1 to determine if a proof is + // required probabilistically. + proofRequirementSampleValue, err = poktrand.SeededFloat32(proofRequirementSeed) + if err != nil { + return 0, err + } + + return proofRequirementSampleValue, nil +} diff --git a/x/proof/types/params.go b/x/proof/types/params.go index b6da6324c..5de271094 100644 --- a/x/proof/types/params.go +++ b/x/proof/types/params.go @@ -22,6 +22,9 @@ var ( DefaultRelayDifficultyTargetHashHex = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" // all relays are payable DefaultRelayDifficultyTargetHash, _ = hex.DecodeString(DefaultRelayDifficultyTargetHashHex) + // TODO_BETA(@red-0ne): Iterate on the parameters below by adding unit suffixes and + // consider having the proof_requirement_threshold to be a function of the supplier's stake amount. + KeyProofRequestProbability = []byte("ProofRequestProbability") ParamProofRequestProbability = "proof_request_probability" DefaultProofRequestProbability float32 = 0.25 // See: https://github.com/pokt-network/pocket-core/blob/staging/docs/proposals/probabilistic_proofs.md diff --git a/x/tokenomics/keeper/settle_pending_claims.go b/x/tokenomics/keeper/settle_pending_claims.go index 1666f4d37..48b434ef8 100644 --- a/x/tokenomics/keeper/settle_pending_claims.go +++ b/x/tokenomics/keeper/settle_pending_claims.go @@ -1,14 +1,15 @@ package keeper import ( + "context" "fmt" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/query" - poktrand "github.com/pokt-network/poktroll/pkg/crypto/rand" "github.com/pokt-network/poktroll/telemetry" prooftypes "github.com/pokt-network/poktroll/x/proof/types" + "github.com/pokt-network/poktroll/x/shared" "github.com/pokt-network/poktroll/x/tokenomics/types" ) @@ -279,16 +280,12 @@ func (k Keeper) proofRequirementForClaim(ctx sdk.Context, claim *prooftypes.Clai return requirementReason, nil } - // Get the hash of the claim to seed the random number generator. - var claimHash []byte - claimHash, err = claim.GetHash() + earliestProofCommitBlockHash, err := k.getEarliestSupplierProofCommitBlockHash(ctx, claim) if err != nil { return requirementReason, err } - // Sample a pseudo-random value between 0 and 1 to determine if a proof is required probabilistically. - var randFloat float32 - randFloat, err = poktrand.SeededFloat32(claimHash[:]) + proofRequirementSampleValue, err := claim.GetProofRequirementSampleValue(earliestProofCommitBlockHash) if err != nil { return requirementReason, err } @@ -296,12 +293,12 @@ func (k Keeper) proofRequirementForClaim(ctx sdk.Context, claim *prooftypes.Clai // Require a proof probabilistically based on the proof_request_probability param. // NB: A random value between 0 and 1 will be less than or equal to proof_request_probability // with probability equal to the proof_request_probability. - if randFloat <= proofParams.GetProofRequestProbability() { + if proofRequirementSampleValue <= proofParams.GetProofRequestProbability() { requirementReason = prooftypes.ProofRequirementReason_PROBABILISTIC logger.Info(fmt.Sprintf( "claim requires proof due to random sample (%.2f) being less than or equal to probability (%.2f)", - randFloat, + proofRequirementSampleValue, proofParams.GetProofRequestProbability(), )) return requirementReason, nil @@ -311,8 +308,37 @@ func (k Keeper) proofRequirementForClaim(ctx sdk.Context, claim *prooftypes.Clai "claim does not require proof due to compute units (%d) being less than the threshold (%d) and random sample (%.2f) being greater than probability (%.2f)", numClaimComputeUnits, proofParams.GetProofRequirementThreshold(), - randFloat, + proofRequirementSampleValue, proofParams.GetProofRequestProbability(), )) return requirementReason, nil } + +// getEarliestSupplierProofCommitBlockHash returns the block hash of the earliest +// block at which a claim might have its proof committed. +func (k Keeper) getEarliestSupplierProofCommitBlockHash( + ctx context.Context, + claim *prooftypes.Claim, +) (blockHash []byte, err error) { + sharedParams, err := k.sharedQuerier.GetParams(ctx) + if err != nil { + return nil, err + } + + sessionEndHeight := claim.GetSessionHeader().GetSessionEndBlockHeight() + supplierOperatorAddress := claim.GetSupplierOperatorAddress() + + proofWindowOpenHeight := shared.GetProofWindowOpenHeight(sharedParams, sessionEndHeight) + proofWindowOpenBlockHash := k.sessionKeeper.GetBlockHash(ctx, proofWindowOpenHeight) + + // TODO_TECHDEBT: Update the method header of this function to accept (sharedParams, Claim, BlockHash). + // After doing so, please review all calling sites and simplify them accordingly. + earliestSupplierProofCommitHeight := shared.GetEarliestSupplierProofCommitHeight( + sharedParams, + sessionEndHeight, + proofWindowOpenBlockHash, + supplierOperatorAddress, + ) + + return k.sessionKeeper.GetBlockHash(ctx, earliestSupplierProofCommitHeight), nil +}