Skip to content

Commit

Permalink
feat: PoA to PoS migration (docs + test) (#241)
Browse files Browse the repository at this point in the history
* working migration with stake verification

* add docs

* 2 approaches

* nit
  • Loading branch information
Reecepbcups authored Feb 4, 2025
1 parent ee973a0 commit 34aee49
Show file tree
Hide file tree
Showing 9 changed files with 253 additions and 57 deletions.
8 changes: 5 additions & 3 deletions .github/workflows/e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,11 @@ jobs:
outputs: type=docker,dest=${{ env.TAR_PATH }}

- name: Upload host artifact
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4.6.0
with:
name: ${{ env.IMAGE_NAME }}
path: ${{ env.TAR_PATH }}
overwrite: true

test:
needs: build-docker
Expand All @@ -54,6 +55,7 @@ jobs:
- "ictest-jail"
- "ictest-val-add"
- "ictest-val-remove"
- "ictest-poa-to-pos"
fail-fast: false

steps:
Expand All @@ -66,7 +68,7 @@ jobs:
uses: actions/checkout@v4

- name: Download Host Artifact
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4.1.8
with:
name: ${{ env.IMAGE_NAME }}
path: /tmp
Expand All @@ -90,7 +92,7 @@ jobs:
go-version: ${{ env.GO_VERSION }}

- name: Download Host Artifact
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4.1.8
with:
name: ${{ env.IMAGE_NAME }}
path: /tmp
Expand Down
32 changes: 0 additions & 32 deletions CHANGELOG.md

This file was deleted.

36 changes: 33 additions & 3 deletions INTEGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* [Example integration of the PoA Module](#example-integration-of-the-poa-module)
* [Ante Handler Setup](#ante-handler-integration)
* [Network Considerations](#network-considerations)
* [PoA to PoS Migration](#migrating-to-pos-from-poa)

# Introduction

Expand Down Expand Up @@ -133,7 +134,7 @@ func NewAnteHandler(options HandlerOptions) (sdk.AnteHandler, error) {

### [Disable Withdraw Delegation Rewards](./ante/disable_withdraw_delegator_rewards.go)

This decorator blocks the `MsgWithdrawDelegatorReward` message from the CosmosSDK `x/distribution` module. The decorator acts as a preventive measure against a crash caused by an interaction between the POA module and the CosmosSDK `x/distribution` module.
This decorator blocks the `MsgWithdrawDelegatorReward` message from the CosmosSDK `x/distribution` module. The decorator acts as a preventive measure against a crash caused by an interaction between the POA module and the CosmosSDK `x/distribution` module.

While the crash has a low probability of occurring in the wild, it is a critical issue that can cause the chain to halt.

Expand Down Expand Up @@ -253,8 +254,37 @@ If you want a module's control not to be based on governance (e.g. x/upgrade for

## Migrating to PoS from PoA

A future optional upgrade will grant PoA networks the ability to migrate to PoS (Proof-of-Stake).
You can perform an upgrade to transition from this PoA module on your network, to the Cosmos SDK's native staking module with delegators. [poa_to_pos_test e2e](./e2e/poa_to_pos_test.go).

Reasons this may be desired:
### Desired Reasons
- The chain product has been successful and the network is ready to be decentralized.
- There is a new token use case that requires a PoS network for user delegations (ex: sharing platform rewards with stakers).

### Risk

Networks using IBC (07-tendermint light client) may break if too many new validators enter the set within the trusting period. This would require IBC clients be updated on counterparty chains with the 'new' validator set.

PoA safe guards this risk within the module itself by not allowing >33% of the validator set to be changed within a block *(technically you could still bypass this with the unsafe message flag on SetPower)*. This is a security limitation of IBC and how the 07-tendermint light client works, NOT a limit of proof of authority in any way. This could technically happen on any PoS network in the Cosmos ecosystem, but is more likely to happen on a PoA network with a small validator set and low stake.

We recommend you coordinate with either foundation delegations or a phased approach

#### Approach 1 - self delegations
- validators get tokens
- PoA is removed but a staking ante block is set so only validators can delegate
- validators self delegate
- then the ante staking whitelist is removed and open for all

#### Approach 2 - blocked new validators
- poa is removed
- a staking decorator is added to the ante blocking MsgNewValidator creation messages
- delegations must delegate to the current set.
- In a future upgrade this block can be removed once the set is in a stable place

This is up to your team to implement and is more operations than technical. It may not be an issue for teams but we do want to call out so you are aware.

### How to Upgrade
- Remove all references to the poa module in your application
- Any modules using the poa.ModuleName as the authority should be changed to something like govtypes.ModuleName (app.go)
- Create a NoOp upgrade handler that does a Store removal of the "poa" key namespace. ([example, PR #240](https://github.com/strangelove-ventures/poa/pull/240))


7 changes: 5 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ ictest-val-add:
ictest-val-remove:
$(MAKE) -C e2e/ ictest-val-remove

.PHONY: test ictest-poa ictest-jail ictest-val-add ictest-val-remove
ictest-poa-to-pos:
$(MAKE) -C e2e/ ictest-poa-to-pos

.PHONY: test ictest-poa ictest-jail ictest-val-add ictest-val-remove ictest-poa-to-pos

COV_ROOT="/tmp/poa-coverage"
COV_UNIT_E2E="${COV_ROOT}/unit-e2e"
Expand Down Expand Up @@ -194,4 +197,4 @@ sim-app-determinism:
sim-app-determinism-random:
$(MAKE) sim-app-determinism SIM_SEED=$$RANDOM

.PHONY: sim-full-app sim-full-app-random sim-import-export sim-after-import sim-app-determinism sim-import-export-random sim-after-import-random sim-app-determinism-random
.PHONY: sim-full-app sim-full-app-random sim-import-export sim-after-import sim-app-determinism sim-import-export-random sim-after-import-random sim-app-determinism-random
16 changes: 10 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@



The Proof of Authority (PoA) module allows for permissioned networks to be controlled by a predefined set of validators to verify transactions. This implementation extends the Cosmos-SDK's x/staking module to a set of administrators over the chain. These administrators gate keep the chain by whitelisting validators, updating consensus power, and removing validators from the network.
The Proof of Authority (PoA) module allows for permissioned networks to be controlled by a predefined set of validators to verify transactions. This implementation extends the Cosmos-SDK's x/staking module to a set of administrators over the chain. These administrators gate keep the chain by whitelisting validators, updating consensus power, and removing validators from the network. Networks can then easily transition to a standard Proof of Stake (PoS) module [with a simple upgrade](./INTEGRATION.md#migrating-to-pos-from-poa).

## Security

Expand All @@ -28,13 +28,17 @@ The default POA administrator is set to the `x/gov` module address. One can chan
```bash
# Override the default PoA admin address
POA_ADMIN_ADDRESS="cosmos1hj5fveer5cjtn4wd6wstzugjfdxzl0xpxvjjvr" poad start
````
```

## Migration

You can migrate from the PoA module to the standard x/staking module by following the [migration guide](./INTEGRATION.md#migrating-to-pos-from-poa). **READ** the risk that are involved with this migration if your network has live IBC (07-tendermint) connections.

## Configuration

After integrating the PoA module into your chain, read the [network considerations](./INTEGRATION.md#network-considerations) before launching the network.

This includes: parameters, full module control, migrating from PoA->PoS, and other useful information.
This includes: parameters, full module control, and other useful information.

## Concepts

Expand Down Expand Up @@ -68,9 +72,9 @@ For better UX, this is accomplished by wrapping the x/staking module's `create-v

**Flow**:
- Validator previous block set power is 9 (3 validators @ 3 power)
- The admin increases validator[0] to 4 power (+11%)
- The admin increases validator[1] to 4 power (+22%)
- The admin increases validator[2] to 4 power (+33%, error)
- The admin increases `validator[0]` to 4 power (+11%)
- The admin increases `validator[1]` to 4 power (+22%)
- The admin increases `validator[2]` to 4 power (+33%, error)

The `AbsoluteChangedPower` of +1 to each validator is 3, which is 33% of the previous block power (3/9). It can be bypassed with the use of the `--unsafe` flag in the CLI command.

Expand Down
5 changes: 4 additions & 1 deletion e2e/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,8 @@ ictest-jail:
ictest-val-add:
go clean -testcache && go test -race -v -run TestPOAAddValidator .

ictest-poa-to-pos:
go clean -testcache && go test -race -v -run TestPoAToPoSUpgrade .

ictest-val-remove:
go clean -testcache && go test -race -v -run TestPOARemoval .
go clean -testcache && go test -race -v -run TestPOARemoval .
191 changes: 191 additions & 0 deletions e2e/poa_to_pos_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
package e2e

import (
"context"
"fmt"
"strconv"
"testing"
"time"

sdkmath "cosmossdk.io/math"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
govv1beta1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1"
stakingttypes "github.com/cosmos/cosmos-sdk/x/staking/types"
"github.com/docker/docker/client"
"github.com/strangelove-ventures/interchaintest/v8"
"github.com/strangelove-ventures/interchaintest/v8/chain/cosmos"
"github.com/strangelove-ventures/interchaintest/v8/ibc"
"github.com/strangelove-ventures/interchaintest/v8/testutil"
"github.com/stretchr/testify/require"

upgradetypes "cosmossdk.io/x/upgrade/types"
)

const (
chainName = "poa"

/*
This upgrade handler is just noop with:
storeUpgrades := storetypes.StoreUpgrades{
Deleted: []string{
"poa",
},
}
*/
upgradeName = "v2-remove-poa"

// create a simapp that does not have PoA code in it, then you:
// make local-image && docker image tag poa:local ghcr.io/reecepbcups/poa:bare-pos-feb-4-2025 && docker push
upgradeRepo, upgradeVersion = "ghcr.io/reecepbcups/poa", "bare-pos-feb-4-2025"

haltHeightDelta = int64(9) // will propose upgrade this many blocks in the future
blocksAfterUpgrade = int64(3)
)

func TestPoAToPoSUpgrade(t *testing.T) {
if testing.Short() {
t.Skip("skipping in short mode")
}

t.Parallel()

t.Log(chainName, POAImage.Repository, POAImage.Version, upgradeName)

numVals, numNodes := 4, 0
cfg := POACfg

chains := interchaintest.CreateChainWithConfig(t, numVals, numNodes, "poa", "", cfg)
chain := chains[0].(*cosmos.CosmosChain)

ctx, ic, client, _ := interchaintest.BuildInitialChain(t, chains, false)
t.Cleanup(func() {
_ = ic.Close()
})

userFunds := sdkmath.NewInt(10_000_000_000)
users := interchaintest.GetAndFundTestUsers(t, ctx, t.Name(), userFunds, chain)
chainUser := users[0]

// upgrade
height, err := chain.Height(ctx)
require.NoError(t, err, "error fetching height before submit upgrade proposal")

haltHeight := height + haltHeightDelta

propIdStr := SubmitUpgradeProposal(t, ctx, chain, chainUser, upgradeName, haltHeight)

propId, err := strconv.ParseUint(propIdStr, 10, 64)
require.NoError(t, err, "failed to convert proposal ID to uint64")

err = chain.VoteOnProposalAllValidators(ctx, propIdStr, cosmos.ProposalVoteYes)
require.NoError(t, err, "failed to submit votes")

_, err = cosmos.PollForProposalStatus(ctx, chain, height, height+haltHeightDelta, propId, govv1beta1.StatusPassed)
require.NoError(t, err, "proposal status did not change to passed in expected number of blocks")

height, err = chain.Height(ctx)
require.NoError(t, err, "error fetching height before upgrade")

t.Logf("height before upgrade: %d", height)

timeoutCtx, timeoutCtxCancel := context.WithTimeout(ctx, time.Second*10)
defer timeoutCtxCancel()

// this should timeout due to chain halt at upgrade height.
_ = testutil.WaitForBlocks(timeoutCtx, int(haltHeight-height)+1, chain)

// bring down nodes to prepare for upgrade
err = chain.StopAllNodes(ctx)
require.NoError(t, err, "error stopping node(s)")

// upgrade version on all nodes
chain.UpgradeVersion(ctx, client, upgradeRepo, upgradeVersion)

// start all nodes back up. validators reach consensus on first block after upgrade height
// and chain block production resumes.
err = chain.StartAllNodes(ctx)
require.NoError(t, err, "error starting upgraded node(s)")

timeoutCtx, timeoutCtxCancel = context.WithTimeout(ctx, time.Second*30)
defer timeoutCtxCancel()

err = testutil.WaitForBlocks(timeoutCtx, int(blocksAfterUpgrade), chain)
require.NoError(t, err, "chain did not produce blocks after upgrade")

verifyStakingFunctions(t, ctx, chain)
}

func verifyStakingFunctions(t *testing.T, ctx context.Context, chain *cosmos.CosmosChain) {
vals, err := chain.StakingQueryValidators(ctx, stakingttypes.BondStatusBonded)
require.NoError(t, err, "error querying validators")

validator := vals[0]

val, err := chain.StakingQueryValidator(ctx, validator.OperatorAddress)
require.NoError(t, err, "error querying validator")
before := val.Tokens
t.Logf("before validator: %s", val)

err = chain.GetNode().StakingDelegate(ctx, "validator", validator.OperatorAddress, "7000000"+chain.Config().Denom)
require.NoError(t, err, "error delegating to validator")

val, err = chain.StakingQueryValidator(ctx, validator.OperatorAddress)
require.NoError(t, err, "error querying validator")
t.Logf("after validator: %s", val)

after := val.Tokens
require.True(t, after.GT(before), "after tokens is not greater than before")
}

func UpgradeNodes(t *testing.T, ctx context.Context, chain *cosmos.CosmosChain, client *client.Client, haltHeight int64, upgradeRepo, upgradeBranchVersion string) {
// bring down nodes to prepare for upgrade
t.Log("stopping node(s)")
err := chain.StopAllNodes(ctx)
require.NoError(t, err, "error stopping node(s)")

// upgrade version on all nodes
t.Log("upgrading node(s)")
chain.UpgradeVersion(ctx, client, upgradeRepo, upgradeBranchVersion)

// start all nodes back up.
// validators reach consensus on first block after upgrade height
// and chain block production resumes.
t.Log("starting node(s)")
err = chain.StartAllNodes(ctx)
require.NoError(t, err, "error starting upgraded node(s)")

timeoutCtx, timeoutCtxCancel := context.WithTimeout(ctx, time.Second*60)
defer timeoutCtxCancel()

err = testutil.WaitForBlocks(timeoutCtx, int(blocksAfterUpgrade), chain)
require.NoError(t, err, "chain did not produce blocks after upgrade")

height, err := chain.Height(ctx)
require.NoError(t, err, "error fetching height after upgrade")

require.GreaterOrEqual(t, height, haltHeight+blocksAfterUpgrade, "height did not increment enough after upgrade")
}

func SubmitUpgradeProposal(t *testing.T, ctx context.Context, chain *cosmos.CosmosChain, user ibc.Wallet, upgradeName string, haltHeight int64) string {
upgradeMsg := []cosmos.ProtoMessage{
&upgradetypes.MsgSoftwareUpgrade{
Authority: authtypes.NewModuleAddress(govtypes.ModuleName).String(),
Plan: upgradetypes.Plan{
Name: upgradeName,
Height: int64(haltHeight),
Info: "",
},
},
}

proposal, err := chain.BuildProposal(upgradeMsg, "Chain Upgrade 1", "Summary desc", "ipfs://CID", fmt.Sprintf(`500000000%s`, chain.Config().Denom), user.FormattedAddress(), false)
require.NoError(t, err, "error building proposal")

txProp, err := chain.SubmitProposal(ctx, user.KeyName(), proposal)
t.Log("txProp", txProp)
require.NoError(t, err, "error submitting proposal")

return txProp.ProposalID
}
Loading

0 comments on commit 34aee49

Please sign in to comment.