diff --git a/app/upgrades.go b/app/upgrades.go index 121e87e..44e29ee 100644 --- a/app/upgrades.go +++ b/app/upgrades.go @@ -13,6 +13,7 @@ import ( v102 "github.com/TacBuild/tacchain/app/upgrades/v1.0.2" v104 "github.com/TacBuild/tacchain/app/upgrades/v1.0.4" v160 "github.com/TacBuild/tacchain/app/upgrades/v1.6.0" + v160spbhotfix "github.com/TacBuild/tacchain/app/upgrades/v1.6.0-spb-hotfix" ) // Upgrades list of chain upgrades @@ -24,6 +25,7 @@ var Upgrades = []upgrades.Upgrade{ v102.Upgrade, // liquid stake v104.Upgrade, // ed25519 precompile v160.Upgrade, // upgrade to cosmos/evm v0.6.0 + v160spbhotfix.Upgrade, } // RegisterUpgradeHandlers registers the chain upgrade handlers diff --git a/app/upgrades/v1.6.0-spb-hotfix/upgrades.go b/app/upgrades/v1.6.0-spb-hotfix/upgrades.go new file mode 100644 index 0000000..006badd --- /dev/null +++ b/app/upgrades/v1.6.0-spb-hotfix/upgrades.go @@ -0,0 +1,41 @@ +package v160spbhotfix + +import ( + "context" + "fmt" + + storetypes "cosmossdk.io/store/types" + upgradetypes "cosmossdk.io/x/upgrade/types" + + "github.com/TacBuild/tacchain/app/upgrades" + v160 "github.com/TacBuild/tacchain/app/upgrades/v1.6.0" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" +) + +const UpgradeName = "v1.6.0-spb-hotfix" + +var Upgrade = upgrades.Upgrade{ + UpgradeName: UpgradeName, + CreateUpgradeHandler: CreateUpgradeHandler, + StoreUpgrades: storetypes.StoreUpgrades{}, +} + +func CreateUpgradeHandler( + _ upgrades.ModuleManager, + _ module.Configurator, + ak *upgrades.AppKeepers, +) upgradetypes.UpgradeHandler { + return func(ctx context.Context, _ upgradetypes.Plan, fromVM module.VersionMap) (module.VersionMap, error) { + sdkCtx := sdk.UnwrapSDKContext(ctx) + logger := sdkCtx.Logger() + + logger.Info("Starting v1.6.0 SPB hotfix upgrade") + if err := v160.MigrateHistoricalGovEVMParamProposals(sdkCtx, ak); err != nil { + return nil, fmt.Errorf("historical gov EVM params proposal migration failed: %w", err) + } + + logger.Info("v1.6.0 SPB hotfix upgrade complete") + return fromVM, nil + } +} diff --git a/app/upgrades/v1.6.0/gov_proposal_migration.go b/app/upgrades/v1.6.0/gov_proposal_migration.go new file mode 100644 index 0000000..e01b110 --- /dev/null +++ b/app/upgrades/v1.6.0/gov_proposal_migration.go @@ -0,0 +1,173 @@ +package v160 + +import ( + "fmt" + + storetypes "cosmossdk.io/store/types" + "github.com/cosmos/gogoproto/proto" + gogoany "github.com/cosmos/gogoproto/types/any" + "google.golang.org/protobuf/encoding/protowire" + + "github.com/TacBuild/tacchain/app/upgrades" + sdk "github.com/cosmos/cosmos-sdk/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + govv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" + evmvmtypes "github.com/cosmos/evm/x/vm/types" +) + +const evmMsgUpdateParamsTypeURL = "/cosmos.evm.vm.v1.MsgUpdateParams" + +// MigrateHistoricalGovEVMParamProposals rewrites old x/vm MsgUpdateParams +// payloads embedded in stored x/gov proposals without unpacking proposal +// interfaces. This repairs historical proposals that were submitted before the +// cosmos/evm Params protobuf field numbers changed. +func MigrateHistoricalGovEVMParamProposals(ctx sdk.Context, ak *upgrades.AppKeepers) error { + storeKey := ak.GetStoreKey(govtypes.StoreKey) + if storeKey == nil { + return fmt.Errorf("gov store key not found") + } + store := ctx.KVStore(storeKey) + + var scannedProposals, scannedMessages, rewrittenMessages uint64 + + iterator := storetypes.KVStorePrefixIterator(store, govtypes.ProposalsKeyPrefix.Bytes()) + defer iterator.Close() + + for ; iterator.Valid(); iterator.Next() { + scannedProposals++ + + var proposal govv1.Proposal + if err := proto.Unmarshal(iterator.Value(), &proposal); err != nil { + return fmt.Errorf("unmarshal gov proposal at key %x: %w", iterator.Key(), err) + } + + proposalRewritten := false + for i, msg := range proposal.Messages { + scannedMessages++ + + rewritten, err := migrateHistoricalGovEVMParamProposalMessage(msg) + if err != nil { + return fmt.Errorf("proposal %d message %d: %w", proposal.Id, i, err) + } + if rewritten { + proposalRewritten = true + rewrittenMessages++ + } + } + + if !proposalRewritten { + continue + } + + bz, err := proto.Marshal(&proposal) + if err != nil { + return fmt.Errorf("marshal repaired gov proposal %d: %w", proposal.Id, err) + } + store.Set(iterator.Key(), bz) + } + + ctx.Logger().Info( + "Historical gov proposal EVM params migration complete", + "proposals_scanned", scannedProposals, + "messages_scanned", scannedMessages, + "messages_rewritten", rewrittenMessages, + ) + + return nil +} + +func migrateHistoricalGovEVMParamProposalMessage(msg *gogoany.Any) (bool, error) { + if msg == nil || msg.TypeUrl != evmMsgUpdateParamsTypeURL { + return false, nil + } + + var current evmvmtypes.MsgUpdateParams + currentErr := proto.Unmarshal(msg.Value, ¤t) + hasOldWire, wireErr := msgUpdateParamsHasOldEVMParamsWire(msg.Value) + if wireErr != nil { + return false, fmt.Errorf("inspect MsgUpdateParams wire layout: %w", wireErr) + } + if currentErr == nil && !hasOldWire { + return false, nil + } + + var old oldEVMMsgUpdateParams + if err := proto.Unmarshal(msg.Value, &old); err != nil { + if currentErr != nil { + return false, fmt.Errorf("unmarshal current MsgUpdateParams: %w; unmarshal old MsgUpdateParams: %v", currentErr, err) + } + return false, fmt.Errorf("unmarshal old MsgUpdateParams: %w", err) + } + + newMsg := evmvmtypes.MsgUpdateParams{ + Authority: old.Authority, + Params: evmvmtypes.Params{ + EvmDenom: old.Params.EvmDenom, + ExtraEIPs: old.Params.ExtraEIPs, + EVMChannels: old.Params.EVMChannels, + AccessControl: old.Params.AccessControl, + ActiveStaticPrecompiles: old.Params.ActiveStaticPrecompiles, + HistoryServeWindow: evmvmtypes.DefaultHistoryServeWindow, + ExtendedDenomOptions: nil, + }, + } + + bz, err := proto.Marshal(&newMsg) + if err != nil { + return false, fmt.Errorf("marshal current MsgUpdateParams: %w", err) + } + msg.Value = bz + + return true, nil +} + +func msgUpdateParamsHasOldEVMParamsWire(bz []byte) (bool, error) { + for len(bz) > 0 { + num, typ, tagLen := protowire.ConsumeTag(bz) + if tagLen < 0 { + return false, protowire.ParseError(tagLen) + } + bz = bz[tagLen:] + + if num == 2 { + if typ != protowire.BytesType { + return false, fmt.Errorf("unexpected wire type %d for MsgUpdateParams.params", typ) + } + paramsBz, valueLen := protowire.ConsumeBytes(bz) + if valueLen < 0 { + return false, protowire.ParseError(valueLen) + } + return evmParamsHasOldLayoutWire(paramsBz), nil + } + + valueLen := protowire.ConsumeFieldValue(num, typ, bz) + if valueLen < 0 { + return false, protowire.ParseError(valueLen) + } + bz = bz[valueLen:] + } + + return false, nil +} + +func evmParamsHasOldLayoutWire(bz []byte) bool { + for len(bz) > 0 { + num, typ, tagLen := protowire.ConsumeTag(bz) + if tagLen < 0 { + return false + } + bz = bz[tagLen:] + + if num == 10 && typ == protowire.BytesType { + return true + } + + valueLen := protowire.ConsumeFieldValue(num, typ, bz) + if valueLen < 0 { + return false + } + bz = bz[valueLen:] + } + + return false +} diff --git a/app/upgrades/v1.6.0/gov_proposal_migration_test.go b/app/upgrades/v1.6.0/gov_proposal_migration_test.go new file mode 100644 index 0000000..895eacb --- /dev/null +++ b/app/upgrades/v1.6.0/gov_proposal_migration_test.go @@ -0,0 +1,222 @@ +package v160 + +import ( + "bytes" + "testing" + + "cosmossdk.io/collections" + storetypes "cosmossdk.io/store/types" + "github.com/cosmos/gogoproto/proto" + gogoany "github.com/cosmos/gogoproto/types/any" + "github.com/stretchr/testify/require" + + "github.com/TacBuild/tacchain/app/upgrades" + "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/testutil" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + govv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" + vm "github.com/cosmos/evm/x/vm" + evmvmtypes "github.com/cosmos/evm/x/vm/types" +) + +func TestMigrateHistoricalGovEVMParamProposalsRewritesOldPayload(t *testing.T) { + ctx, ak, cdc := newGovProposalMigrationTest(t) + + proposal := govv1.Proposal{ + Id: 7, + Messages: []*gogoany.Any{oldEVMMsgUpdateParamsAny(t)}, + Status: govv1.StatusPassed, + Title: "old evm params", + } + writeRawGovProposal(t, ctx, ak, proposal) + + var decodedBefore govv1.Proposal + err := cdc.Unmarshal(readRawGovProposal(t, ctx, ak, proposal.Id), &decodedBefore) + require.Error(t, err) + + require.NoError(t, MigrateHistoricalGovEVMParamProposals(ctx, ak)) + + var decodedAfter govv1.Proposal + require.NoError(t, cdc.Unmarshal(readRawGovProposal(t, ctx, ak, proposal.Id), &decodedAfter)) + + msgs, err := decodedAfter.GetMsgs() + require.NoError(t, err) + require.Len(t, msgs, 1) + + updateMsg, ok := msgs[0].(*evmvmtypes.MsgUpdateParams) + require.True(t, ok) + require.Equal(t, "gov-authority", updateMsg.Authority) + require.Equal(t, "utac", updateMsg.Params.EvmDenom) + require.Equal(t, []int64{3855}, updateMsg.Params.ExtraEIPs) + require.Equal(t, []string{"channel-0"}, updateMsg.Params.EVMChannels) + require.Equal(t, []string{"0x0000000000000000000000000000000000000800"}, updateMsg.Params.ActiveStaticPrecompiles) + require.Equal(t, uint64(evmvmtypes.DefaultHistoryServeWindow), updateMsg.Params.HistoryServeWindow) + require.Nil(t, updateMsg.Params.ExtendedDenomOptions) +} + +func TestMigrateHistoricalGovEVMParamProposalsLeavesUnrelatedProposalUnchanged(t *testing.T) { + ctx, ak, _ := newGovProposalMigrationTest(t) + + proposal := govv1.Proposal{ + Id: 1, + Messages: []*gogoany.Any{ + { + TypeUrl: "/cosmos.upgrade.v1beta1.MsgSoftwareUpgrade", + Value: []byte{0x0a, 0x03, 'v', '1', '6'}, + }, + }, + Status: govv1.StatusPassed, + Title: "software upgrade only", + } + writeRawGovProposal(t, ctx, ak, proposal) + before := readRawGovProposal(t, ctx, ak, proposal.Id) + + require.NoError(t, MigrateHistoricalGovEVMParamProposals(ctx, ak)) + + require.Equal(t, before, readRawGovProposal(t, ctx, ak, proposal.Id)) +} + +func TestMigrateHistoricalGovEVMParamProposalsLeavesCurrentPayloadUnchanged(t *testing.T) { + ctx, ak, _ := newGovProposalMigrationTest(t) + + currentAny, err := codectypes.NewAnyWithValue(&evmvmtypes.MsgUpdateParams{ + Authority: "gov-authority", + Params: evmvmtypes.Params{ + EvmDenom: "utac", + ExtraEIPs: []int64{3855}, + EVMChannels: []string{"channel-0"}, + AccessControl: testAccessControl(), + ActiveStaticPrecompiles: []string{"0x0000000000000000000000000000000000000800"}, + HistoryServeWindow: evmvmtypes.DefaultHistoryServeWindow, + }, + }) + require.NoError(t, err) + + proposal := govv1.Proposal{ + Id: 2, + Messages: []*gogoany.Any{currentAny}, + Status: govv1.StatusPassed, + Title: "current evm params", + } + writeRawGovProposal(t, ctx, ak, proposal) + before := readRawGovProposal(t, ctx, ak, proposal.Id) + + require.NoError(t, MigrateHistoricalGovEVMParamProposals(ctx, ak)) + + require.Equal(t, before, readRawGovProposal(t, ctx, ak, proposal.Id)) +} + +func TestMigrateHistoricalGovEVMParamProposalsIsIdempotent(t *testing.T) { + ctx, ak, cdc := newGovProposalMigrationTest(t) + + proposal := govv1.Proposal{ + Id: 3, + Messages: []*gogoany.Any{oldEVMMsgUpdateParamsAny(t)}, + Status: govv1.StatusPassed, + Title: "old evm params", + } + writeRawGovProposal(t, ctx, ak, proposal) + + require.NoError(t, MigrateHistoricalGovEVMParamProposals(ctx, ak)) + afterFirstRun := readRawGovProposal(t, ctx, ak, proposal.Id) + + require.NoError(t, MigrateHistoricalGovEVMParamProposals(ctx, ak)) + require.Equal(t, afterFirstRun, readRawGovProposal(t, ctx, ak, proposal.Id)) + + var decoded govv1.Proposal + require.NoError(t, cdc.Unmarshal(afterFirstRun, &decoded)) +} + +func TestMigrateHistoricalGovEVMParamProposalsRequiresGovStore(t *testing.T) { + ctx, ak, _ := newGovProposalMigrationTest(t) + ak.GetStoreKey = func(string) *storetypes.KVStoreKey { return nil } + + err := MigrateHistoricalGovEVMParamProposals(ctx, ak) + require.Error(t, err) + require.Contains(t, err.Error(), "gov store key not found") +} + +func newGovProposalMigrationTest(t *testing.T) (sdk.Context, *upgrades.AppKeepers, codec.Codec) { + t.Helper() + + govKey := storetypes.NewKVStoreKey(govtypes.StoreKey) + testCtx := testutil.DefaultContextWithDB(t, govKey, storetypes.NewTransientStoreKey("transient_test")) + encCfg := moduletestutil.MakeTestEncodingConfig(vm.AppModuleBasic{}) + + ak := &upgrades.AppKeepers{ + Codec: encCfg.Codec, + GetStoreKey: func(storeKey string) *storetypes.KVStoreKey { + if storeKey == govtypes.StoreKey { + return govKey + } + return nil + }, + } + + return testCtx.Ctx, ak, encCfg.Codec +} + +func writeRawGovProposal(t *testing.T, ctx sdk.Context, ak *upgrades.AppKeepers, proposal govv1.Proposal) { + t.Helper() + + bz, err := proto.Marshal(&proposal) + require.NoError(t, err) + ctx.KVStore(ak.GetStoreKey(govtypes.StoreKey)).Set(govProposalStoreKey(t, proposal.Id), bz) +} + +func readRawGovProposal(t *testing.T, ctx sdk.Context, ak *upgrades.AppKeepers, id uint64) []byte { + t.Helper() + + bz := ctx.KVStore(ak.GetStoreKey(govtypes.StoreKey)).Get(govProposalStoreKey(t, id)) + require.NotNil(t, bz) + return bytes.Clone(bz) +} + +func govProposalStoreKey(t *testing.T, id uint64) []byte { + t.Helper() + + key := append([]byte(nil), govtypes.ProposalsKeyPrefix.Bytes()...) + idKey := make([]byte, collections.Uint64Key.Size(id)) + _, err := collections.Uint64Key.Encode(idKey, id) + require.NoError(t, err) + return append(key, idKey...) +} + +func oldEVMMsgUpdateParamsAny(t *testing.T) *gogoany.Any { + t.Helper() + + msg := oldEVMMsgUpdateParams{ + Authority: "gov-authority", + Params: oldEVMParams{ + EvmDenom: "utac", + ExtraEIPs: []int64{3855}, + EVMChannels: []string{"channel-0"}, + AccessControl: testAccessControl(), + ActiveStaticPrecompiles: []string{"0x0000000000000000000000000000000000000800"}, + }, + } + bz, err := proto.Marshal(&msg) + require.NoError(t, err) + + hasOldWire, err := msgUpdateParamsHasOldEVMParamsWire(bz) + require.NoError(t, err) + require.True(t, hasOldWire) + + var current evmvmtypes.MsgUpdateParams + require.Error(t, proto.Unmarshal(bz, ¤t)) + + return &gogoany.Any{ + TypeUrl: evmMsgUpdateParamsTypeURL, + Value: bz, + } +} + +func testAccessControl() evmvmtypes.AccessControl { + return evmvmtypes.AccessControl{ + Create: evmvmtypes.AccessControlType{AccessType: evmvmtypes.AccessTypePermissionless}, + Call: evmvmtypes.AccessControlType{AccessType: evmvmtypes.AccessTypePermissionless}, + } +} diff --git a/app/upgrades/v1.6.0/old_evm_params.go b/app/upgrades/v1.6.0/old_evm_params.go index 75d5ac5..fafeec7 100644 --- a/app/upgrades/v1.6.0/old_evm_params.go +++ b/app/upgrades/v1.6.0/old_evm_params.go @@ -36,3 +36,15 @@ type oldEVMParams struct { func (m *oldEVMParams) Reset() { *m = oldEVMParams{} } func (m *oldEVMParams) String() string { return proto.CompactTextString(m) } func (m *oldEVMParams) ProtoMessage() {} + +// oldEVMMsgUpdateParams mirrors the old MsgUpdateParams payload embedded in +// historical x/gov proposals. The top-level message did not change, but its +// Params field did. +type oldEVMMsgUpdateParams struct { + Authority string `protobuf:"bytes,1,opt,name=authority,proto3"` + Params oldEVMParams `protobuf:"bytes,2,opt,name=params,proto3"` +} + +func (m *oldEVMMsgUpdateParams) Reset() { *m = oldEVMMsgUpdateParams{} } +func (m *oldEVMMsgUpdateParams) String() string { return proto.CompactTextString(m) } +func (m *oldEVMMsgUpdateParams) ProtoMessage() {} diff --git a/app/upgrades/v1.6.0/upgrades.go b/app/upgrades/v1.6.0/upgrades.go index 7b59039..d4fdb27 100644 --- a/app/upgrades/v1.6.0/upgrades.go +++ b/app/upgrades/v1.6.0/upgrades.go @@ -75,6 +75,15 @@ func CreateUpgradeHandler( return nil, fmt.Errorf("x/vm params migration failed: %w", err) } + // 2a1. Re-encode historical x/gov proposals that embed old x/vm + // MsgUpdateParams payloads. Runtime x/vm state is fixed above, but gov + // queries unpack proposal Any messages and would otherwise fail on the + // old Params field 10 wire type. + logger.Info("Migrating historical x/gov EVM MsgUpdateParams proposals") + if err := MigrateHistoricalGovEVMParamProposals(sdkCtx, ak); err != nil { + return nil, fmt.Errorf("historical gov EVM params proposal migration failed: %w", err) + } + // 2a2. Set history_serve_window to the default value (8192 / EIP-2935). // This is a new field in v0.6.0 that did not exist in v0.2.0, so it // stays 0 after the raw re-encoding above. Read → patch → write back. diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go index c51dd2e..8fc02a1 100644 --- a/tests/e2e/e2e_test.go +++ b/tests/e2e/e2e_test.go @@ -200,8 +200,9 @@ func (s *TacchainTestSuite) TestStakingAPR() { delegatedAmount := parseBalanceAmount(output) require.Contains(s.T(), delegatedAmount, delegationAmount, "Delegation amount should match") - // Verify delegation via dedicated query and dump rewards raw - delegRewardsOut, _ := ExecuteCommand(ctx, params, "q", "distribution", "rewards", delegatorAddr, validatorAddr) + // Verify delegation rewards via dedicated query and dump rewards raw. + delegRewardsOut, err := ExecuteCommand(ctx, params, "q", "distribution", "rewards-by-validator", delegatorAddr, validatorAddr) + require.NoError(s.T(), err, "Failed to query rewards for specific validator: %s", delegRewardsOut) fmt.Printf("Rewards for specific validator: %s\n", delegRewardsOut) // Wait for a few blocks to accumulate rewards before measurement diff --git a/tests/localnet/test-evm.sh b/tests/localnet/test-evm.sh index fcbd4d5..b48bac3 100755 --- a/tests/localnet/test-evm.sh +++ b/tests/localnet/test-evm.sh @@ -13,6 +13,11 @@ trap cleanup EXIT export HOMEDIR=.test-localnet-evm export CHAIN_ID=tacchain_2391-1 export MIN_GAS_PRICE=25000000000 +export TACCHAIND=${TACCHAIND:-$(command -v tacchaind 2>/dev/null || echo ./build/tacchaind)} + +tacchaind() { + "$TACCHAIND" "$@" +} EVM_CHAIN_ID=$(echo "$CHAIN_ID" | sed -E 's/.*_([0-9]+)-.*/\1/') if [[ -z "$EVM_CHAIN_ID" ]]; then diff --git a/tests/localnet/test-params.sh b/tests/localnet/test-params.sh index 24b2995..b0801e1 100755 --- a/tests/localnet/test-params.sh +++ b/tests/localnet/test-params.sh @@ -4,11 +4,16 @@ export GENESIS_ACC_1_ADDRESS=tac1zg69v7ys40x77y352eufp27daufrg4nckcjrx2 export GENESIS_ACC_2_ADDRESS=tac167a5p268zlj2tgmlrmhkcqyex07stu5k6s23lq export HOMEDIR=./.test-localnet-params export CHAIN_ID=tacchain_239-1 +export TACCHAIND=${TACCHAIND:-$(command -v tacchaind 2>/dev/null || echo ./build/tacchaind)} export VALIDATOR_1_MNEMONIC="spray retire festival globe nuclear festival install lunch deal bench unlock car solution vague witness weasel ankle rebel slush allow wing seek tobacco carbon" # tac1tg73cpsxxca3m2t6w09gezvcg37zrqqxglwsgv export VALIDATOR_2_MNEMONIC="coach deposit public fiction utility dentist course bread maple lawn dress bridge melody snake taxi suggest student vote actress shop man service bubble build" # tac137kh82tna99k9cdnvpga9jcme0tqn9up40f96g export VALIDATOR_3_MNEMONIC="brave name midnight glass story soda calm panel menu rescue check puzzle layer mango pull snake short spread virtual use already alone observe cream" # tac13gv56l9leqvdjj6y4cr0g8rtzudk5c65md003y export VALIDATOR_4_MNEMONIC="canal marble glimpse nurse afford medal film whale hockey defense mango visa romance plastic little cage balance special sibling clump machine wrestle energy acid" # tac1zh9dxqc28gx99gyeq6rfwmd623m77h00zykpvz +tacchaind() { + "$TACCHAIND" "$@" +} + # start new multi-validator network echo "Starting new multi-validator network with 4 nodes" echo y | make localnet-init-multi-node > /dev/null 2>&1