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

[Applicaton] Provable past delegations & delayed undelegations #497

Closed
wants to merge 20 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
9e0b6e6
wip: full redelegation structure
red-0ne Apr 24, 2024
210c2ba
Merge remote-tracking branch 'origin/main' into feat/proof-on-redeleg…
red-0ne Apr 25, 2024
f4bf348
feat: complete provable past delegations
red-0ne Apr 25, 2024
6db9bf5
Merge remote-tracking branch 'origin/main' into feat/proof-on-redeleg…
red-0ne Apr 25, 2024
d94384e
fix: reword old comment
red-0ne Apr 25, 2024
264f51b
fix: Reword RingClient interface comment
red-0ne Apr 25, 2024
a3f8a2e
fix: access to ringByAddrAndBlock cache
red-0ne Apr 25, 2024
87acb41
chore: Add more comments to GetRingForAddress
red-0ne Apr 25, 2024
400a207
fix: rename proto message property with snake case
red-0ne Apr 25, 2024
47584b9
chore: fix more comments
red-0ne Apr 25, 2024
1828042
chore: fix more comments and rename some variables
red-0ne Apr 25, 2024
0a7873c
test: Initiate e2e tests
red-0ne Apr 26, 2024
9a662b7
Merge remote-tracking branch 'origin/main' into feat/proof-on-redeleg…
red-0ne Apr 26, 2024
378bca8
Merge remote-tracking branch 'origin/main' into feat/proof-on-redeleg…
red-0ne Apr 26, 2024
63b76b4
Merge remote-tracking branch 'origin/main' into feat/proof-on-redeleg…
red-0ne Apr 27, 2024
b6b81ef
test: working e2e tests
red-0ne Apr 27, 2024
2d50588
Merge remote-tracking branch 'origin/main' into feat/proof-on-redeleg…
red-0ne Apr 28, 2024
9f6c037
Merge remote-tracking branch 'origin/main' into feat/proof-on-redeleg…
red-0ne Apr 28, 2024
2c5d592
chore: tidy up e2e tests
red-0ne Apr 28, 2024
e17ae43
fix: Only used self rings if no archived delegations
red-0ne Apr 29, 2024
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
1,603 changes: 1,476 additions & 127 deletions api/poktroll/application/application.pulsar.go

Large diffs are not rendered by default.

652 changes: 652 additions & 0 deletions api/poktroll/application/undelegations.pulsar.go

Large diffs are not rendered by default.

61 changes: 61 additions & 0 deletions e2e/tests/delegate.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
Feature: Delegate Namespaces

Background:
Copy link
Member

Choose a reason for hiding this comment

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

TIL! Going to start using this.

Copy link
Member

Choose a reason for hiding this comment

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

Ended up going through [1] to learn more but not going to change anything now.

[1] https://cucumber.io/docs/gherkin/reference/#rule

# Setup Tx and NewBlock event query clients for each scenario
Given the poktroll chain is reachable
Comment on lines +4 to +5
Copy link
Member

Choose a reason for hiding this comment

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

Ideally we wouldn't need the comments and the step would be self explanatory

How about Given the poktroll binary is available and blockchain clients are configured


Scenario: User can delegate Application to Gateway
Given the user has the pocketd binary installed
And the actor type "application" with account "app1" is staked with enough uPOKT
Copy link
Member

Choose a reason for hiding this comment

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

I looked at the implementation of is staked with enough uPOKT and I think it's actually is restaked with one additional uPOKT.

I don't fully understand the intention - yet - but there's an opportunity to make it clearer.

And the actor type "gateway" with account "gateway1" is staked with enough uPOKT
Copy link
Member

Choose a reason for hiding this comment

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

Ditto

# Ensure that the application does not have any delegation
# This is to avoid any conflicts with previous delegations added by
# genesis transactions or previous tests.
And the application "app1" does not have any delegation
Copy link
Member

Choose a reason for hiding this comment

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

  1. This is clearer and shows that there's a side effect.
Suggested change
And the application "app1" does not have any delegation
And the application "app1" undelegates from all gateways it is delegated to

When the user delegates application "app1" to gateway "gateway1"
Then application "app1" is delegated to gateway "gateway1"

Scenario: User can undelegate Application from Gateway
Given the user has the pocketd binary installed
And the actor type "application" with account "app1" is staked with enough uPOKT
And the actor type "gateway" with account "gateway1" is staked with enough uPOKT
And the application "app1" does not have any delegation
When the user delegates application "app1" to gateway "gateway1"
# Ensure that the undelegation does not happen in the last block of the
# sesssion which could prevent observing that is only effective in the next session.
And the user undelegates application "app1" from gateway "gateway1" before the session end block
# Undelegation is not effective yet.
Then application "app1" is delegated to gateway "gateway1"
When the user waits until the start of the next session
# The undelegation becomes effective.
Then application "app1" is not delegated to gateway "gateway1"
And application "app1" has gateway "gateway1" address in the archived delegations

Scenario: Application can override undelegation from a Gateway
Given the user has the pocketd binary installed
And the actor type "application" with account "app1" is staked with enough uPOKT
And the actor type "gateway" with account "gateway1" is staked with enough uPOKT
And the application "app1" does not have any delegation
When the user delegates application "app1" to gateway "gateway1"
# Undelegate before the current session's end block so the next step can override it.
And the user undelegates application "app1" from gateway "gateway1" before the session end block
# The user redelegates to the same gateway before the next session.
# This should override the previous undelegation.
And the user delegates application "app1" to gateway "gateway1"
# Wait until the next session to see that the undelegation did not take effect.
And the user waits until the start of the next session
Then application "app1" is delegated to gateway "gateway1"

Scenario: Application gets its archived delegations pruned
Given the user has the pocketd binary installed
And the actor type "application" with account "app1" is staked with enough uPOKT
And the actor type "gateway" with account "gateway1" is staked with enough uPOKT
And the application "app1" does not have any delegation
When the user delegates application "app1" to gateway "gateway1"
And the user undelegates application "app1" from gateway "gateway1"
And the user waits until the start of the next session
Then application "app1" is not delegated to gateway "gateway1"
And application "app1" has gateway "gateway1" address in the archived delegations
When the user waits until archived delegations are pruned
Then application "app1" is not delegated to gateway "gateway1"
And application "app1" does not have gateway "gateway1" address in the archived delegations
249 changes: 249 additions & 0 deletions e2e/tests/delegate_steps_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
//go:build e2e

package e2e

import (
"context"
"fmt"
"strings"
"sync"

"cosmossdk.io/depinject"
"github.com/cometbft/cometbft/libs/json"
"github.com/stretchr/testify/require"
"golang.org/x/exp/slices"

"github.com/pokt-network/poktroll/pkg/client"
"github.com/pokt-network/poktroll/pkg/client/block"
"github.com/pokt-network/poktroll/pkg/client/events"
"github.com/pokt-network/poktroll/pkg/client/tx"
"github.com/pokt-network/poktroll/testutil/testclient"
appkeeper "github.com/pokt-network/poktroll/x/application/keeper"
"github.com/pokt-network/poktroll/x/application/types"
sessionkeeper "github.com/pokt-network/poktroll/x/session/keeper"
)

const (
// serviceId to stake for by applications.
serviceId = "anvil"
// defaultStakeAmount is the default amount to stake for applications and gateways.
Copy link
Member

Choose a reason for hiding this comment

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

NIT: Can we decouple app & gateway stakes? It'll be more "scalable" from a dec perspective.

defaultStakeAmount = 1000000
)

func (s *suite) TheActorTypeWithAccountIsStakedWithEnoughUpokt(accType, accName string) {
// Get the staked amount for account, if it exists.
// This is used to determine how much to stake for the actor and whether
// it is already staked or not.
stakedAmount, ok := s.getStakedAmount(accType, accName)
if !ok {
stakedAmount = defaultStakeAmount
}

// Fund the actor with enough upokt to stake for the service.
s.TheUserSendsUpoktFromAccountToAccount(int64(stakedAmount+1), "pnf", accName)
Copy link
Member

Choose a reason for hiding this comment

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

  1. Can you move pnf into a local fundingAccount variable?
  2. Can you try using faucet instead of pnf?

s.waitForTxResultEvent(
"transfer",
"recipient",
accNameToAddrMap[accName],
)

// Stake for the service with the
s.TheUserStakesAWithUpoktForServiceFromTheAccount(
accType,
int64(stakedAmount+1),
Copy link
Member

Choose a reason for hiding this comment

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

  1. Move stakedAmount+1 stakeAmount := stakedAmount + 1 on line 41
  2. Reuse it in both places
  3. #PUC why we're doing +1

serviceId,
accName,
)
s.TheUserShouldWaitForTheModuleMessageToBeSubmitted(
accType,
fmt.Sprintf("Stake%s", strings.Title(accType)),
)
}

func (s *suite) TheUserDelegatesApplicationToGateway(appName, gatewayName string) {
args := []string{
"tx",
"application",
"delegate-to-gateway",
accNameToAddrMap[gatewayName],
"--from",
appName,
keyRingFlag,
chainIdFlag,
"-y",
}
_, err := s.pocketd.RunCommandOnHost("", args...)
require.NoError(s, err)

s.TheUserShouldWaitForTheModuleMessageToBeSubmitted(
"application",
"DelegateToGateway",
)
}

func (s *suite) TheApplicationDoesNotHaveAnyDelegation(appName string) {
application := s.showApplication(appName)
undelegationWaitGroup := sync.WaitGroup{}
undelegationWaitGroup.Add(len(application.DelegateeGatewayAddresses))

// Concurrently undelegate the application from all gateways and wait for the
// transactions to be committed.
for _, gatewayAddress := range application.DelegateeGatewayAddresses {
go func(gatewayAddress string) {
s.TheUserUndelegatesApplicationFromGateway(appName, accAddrToNameMap[gatewayAddress])
s.TheUserWaitsUntilTheStartOfTheNextSession()
undelegationWaitGroup.Done()
}(gatewayAddress)
}
undelegationWaitGroup.Wait()
}

func (s *suite) ApplicationIsDelegatedToGateway(appName, gatewayName string) {
application := s.showApplication(appName)
require.Containsf(s,
application.DelegateeGatewayAddresses,
accNameToAddrMap[gatewayName],
"app %q is not delegated to gateway %q",
appName, gatewayName,
)
}

func (s *suite) ApplicationIsNotDelegatedToGateway(appName, gatewayName string) {
application := s.showApplication(appName)
require.NotContainsf(s,
application.DelegateeGatewayAddresses,
accNameToAddrMap[gatewayName],
"app %q is delegated to gateway %q",
appName, gatewayName,
)
}

func (s *suite) TheUserUndelegatesApplicationFromGatewayBeforeTheSessionEndBlock(appName, gatewayName string) {
s.TheUserWaitsUntilTheStartOfTheNextSession()
s.TheUserUndelegatesApplicationFromGateway(appName, gatewayName)
}

func (s *suite) TheUserUndelegatesApplicationFromGateway(appName, gatewayName string) {
args := []string{
"tx",
"application",
"undelegate-from-gateway",
accNameToAddrMap[gatewayName],
"--from",
appName,
keyRingFlag,
chainIdFlag,
"-y",
}
_, err := s.pocketd.RunCommandOnHost("", args...)
require.NoError(s, err)

s.TheUserShouldWaitForTheModuleMessageToBeSubmitted(
"application",
"UndelegateFromGateway",
)
}

func (s *suite) ApplicationHasGatewayAddressInTheArchivedDelegations(appName, gatewayName string) {
application := s.showApplication(appName)
require.Truef(s,
slices.ContainsFunc(application.ArchivedDelegations,
func(archivedDelegations types.ArchivedDelegations) bool {
return slices.Contains(archivedDelegations.GatewayAddresses, accNameToAddrMap[gatewayName])
}),
"app %q does not have gateway %q in its archived delegations",
appName, gatewayName,
)
}

func (s *suite) ApplicationDoesNotHaveGatewayAddressInTheArchivedDelegations(appName, gatewayName string) {
application := s.showApplication(appName)
require.Falsef(s,
slices.ContainsFunc(application.ArchivedDelegations,
func(archivedDelegations types.ArchivedDelegations) bool {
return slices.Contains(archivedDelegations.GatewayAddresses, accNameToAddrMap[gatewayName])
}),
"app %q has gateway %q in its archived delegations",
appName, gatewayName,
)
}

func (s *suite) ThePoktrollChainIsReachable() {
ctx := context.Background()

// Construct an events query client to listen for tx events from the supplier.
deps := depinject.Supply(events.NewEventsQueryClient(testclient.CometLocalWebsocketURL))
txEventsReplayClient, err := events.NewEventsReplayClient(
ctx,
deps,
"tm.event='Tx'",
tx.UnmarshalTxResult,
eventsReplayClientBufferSize,
)
require.NoError(s, err)
s.scenarioState[txResultEventsReplayClientKey] = txEventsReplayClient

// Construct an events query client to listen for claim settlement or expiration events on-chain.
blockEventsReplayClient, err := events.NewEventsReplayClient(
ctx,
deps,
newBlockEventSubscriptionQuery,
block.UnmarshalNewBlockEvent,
eventsReplayClientBufferSize,
)
require.NoError(s, err)
s.scenarioState[newBlockEventReplayClientKey] = blockEventsReplayClient
}

func (s *suite) TheUserWaitsUntilTheStartOfTheNextSession() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

blockReplayClient := s.scenarioState[newBlockEventReplayClientKey].(client.EventsReplayClient[*block.CometNewBlockEvent])
block := blockReplayClient.LastNEvents(ctx, 1)[0]
nextSessionStartHeight := sessionkeeper.GetSessionEndBlockHeight(block.Height()) + 1

blockObs := blockReplayClient.EventsSequence(ctx).Subscribe(ctx)
for newBlock := range blockObs.Ch() {
if newBlock.Height() >= nextSessionStartHeight {
break
}
}
}

func (s *suite) TheUserWaitsUntilArchivedDelegationsArePruned() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

blockReplayClient := s.scenarioState[newBlockEventReplayClientKey].(client.EventsReplayClient[*block.CometNewBlockEvent])
block := blockReplayClient.LastNEvents(ctx, 1)[0]
delegationPruningBlockHeight := block.Height() + appkeeper.ArchivedDelegationsRetentionBlocks

blockObs := blockReplayClient.EventsSequence(ctx).Subscribe(ctx)
for newBlock := range blockObs.Ch() {
if newBlock.Height() >= delegationPruningBlockHeight {
break
}
}
}

// showApplication issues a application show-application query with json output
// parses its content and return the application.
func (s *suite) showApplication(appName string) types.Application {
args := []string{
"q",
"application",
"show-application",
accNameToAddrMap[appName],
chainIdFlag,
"--output",
"json",
}
res, err := s.pocketd.RunCommandOnHost("", args...)
require.NoError(s, err)
var queryGetApplicationResponse types.QueryGetApplicationResponse
err = json.Unmarshal([]byte(res.Stdout), &queryGetApplicationResponse)
require.NoError(s, err)

return queryGetApplicationResponse.Application
}
4 changes: 4 additions & 0 deletions e2e/tests/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,10 @@ func (s *suite) getConfigFileContent(amount int64, actorType, serviceId string)
- publicly_exposed_url: http://relayminer:8545
rpc_type: json_rpc`,
amount, serviceId)
case "gateway":
configContent = fmt.Sprintf(`
stake_amount: %dupokt`,
amount)
default:
s.Fatalf("unknown actor type %s", actorType)
}
Expand Down
13 changes: 8 additions & 5 deletions e2e/tests/session_steps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ const (
)

func (s *suite) TheUserShouldWaitForTheModuleMessageToBeSubmitted(module, message string) {
s.waitForTxResultEvent(fmt.Sprintf("/poktroll.%s.Msg%s", module, message))
s.waitForTxResultEvent("message", "action", fmt.Sprintf("/poktroll.%s.Msg%s", module, message))
}

func (s *suite) TheUserShouldWaitForTheModuleEventToBeBroadcast(module, message string) {
Expand Down Expand Up @@ -200,7 +200,7 @@ func (s *suite) sendRelaysForSession(
}

// waitForTxResultEvent waits for an event to be observed which has the given message action.
func (s *suite) waitForTxResultEvent(targetAction string) {
func (s *suite) waitForTxResultEvent(eventType, targetKey, targetValue string) {
Copy link
Member

Choose a reason for hiding this comment

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

// waitForTxResultEvent waits for a TxResult event to be observed with the given attributes.
func (s *suite) waitForTxResultEvent(eventType, eventAttrKey, eventAttrValue string) {

ctx, done := context.WithCancel(context.Background())

txResultEventsReplayClientState, ok := s.scenarioState[txResultEventsReplayClientKey]
Expand All @@ -221,9 +221,12 @@ func (s *suite) waitForTxResultEvent(targetAction string) {
// Range over each event's attributes to find the "action" attribute
// and compare its value to that of the action provided.
for _, event := range txResult.Result.Events {
if event.Type != eventType {
continue
}
for _, attribute := range event.Attributes {
if attribute.Key == "action" {
if attribute.Value == targetAction {
if attribute.Key == targetKey {
if attribute.Value == targetValue {
done()
return
}
Expand All @@ -235,7 +238,7 @@ func (s *suite) waitForTxResultEvent(targetAction string) {

select {
case <-time.After(eventTimeout):
s.Fatalf("timed out waiting for message with action %q", targetAction)
s.Fatalf("timed out waiting for message with %q %q", targetKey, targetValue)
Copy link
Member

Choose a reason for hiding this comment

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

Add the type here as well

case <-ctx.Done():
s.Log("Success; message detected before timeout.")
}
Expand Down
6 changes: 3 additions & 3 deletions pkg/crypto/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ type RingCache interface {
// the addresses of the gateways the application delegated to, and converting
// them into their corresponding public key points on the secp256k1 curve.
type RingClient interface {
// GetRingForAddress returns the ring for the given application address if
// it exists.
GetRingForAddress(ctx context.Context, appAddress string) (*ring.Ring, error)
// GetRingForAddress returns the ring for the application address at a given
// block height if it exists.
GetRingForAddress(ctx context.Context, appAddress string, blockHeight int64) (*ring.Ring, error)

// VerifyRelayRequestSignature verifies the relay request signature against
// the ring for the application address in the relay request.
Expand Down
Loading
Loading