Skip to content

Commit

Permalink
[SessionManager] Implement off-chain proof params usage (#765)
Browse files Browse the repository at this point in the history
## 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 <[email protected]>
  • Loading branch information
red-0ne and Olshansk authored Sep 6, 2024
1 parent 7949a11 commit 9056e53
Show file tree
Hide file tree
Showing 15 changed files with 348 additions and 83 deletions.
40 changes: 38 additions & 2 deletions e2e/tests/0_settlement.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion e2e/tests/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
11 changes: 10 additions & 1 deletion e2e/tests/session.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 16 additions & 2 deletions e2e/tests/session_steps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)),
),
)
}
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions pkg/relayer/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ func setupRelayerDependencies(
config.NewSupplySupplierQuerierFn(),
config.NewSupplySessionQuerierFn(),
config.NewSupplyServiceQueryClientFn(),
config.NewSupplyProofQueryClientFn(),
config.NewSupplyRingCacheFn(),
supplyTxFactory,
supplyTxContext,
Expand Down
9 changes: 4 additions & 5 deletions pkg/relayer/session/claim.go
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
168 changes: 119 additions & 49 deletions pkg/relayer/session/proof.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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
Expand All @@ -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`.
Expand All @@ -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(),
)

Expand All @@ -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(),
}
}
Loading

0 comments on commit 9056e53

Please sign in to comment.