Skip to content

Commit 761d585

Browse files
committed
feat(upgrades): add read-state upgrade handler
Adds the x/ucallback store and reserves every system-contract address still unclaimed in the A/B/C ranges — 41 of 47 on donut, including 0xC2, which x/ucallback needs before it can ingest anything.
1 parent 4af77ab commit 761d585

4 files changed

Lines changed: 168 additions & 1 deletion

File tree

app/upgrades.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@ import (
1616
ethhashfix "github.com/pushchain/push-chain-node/app/upgrades/eth-hash-fix"
1717
evmblockscoutfix "github.com/pushchain/push-chain-node/app/upgrades/evm-blockscout-fix"
1818
evmchainidffix "github.com/pushchain/push-chain-node/app/upgrades/evm-chainid-fix"
19+
evmderivedgasprice "github.com/pushchain/push-chain-node/app/upgrades/evm-derived-gas-price"
1920
evmparamsmigration "github.com/pushchain/push-chain-node/app/upgrades/evm-params-migration"
2021
evmpreinstalls "github.com/pushchain/push-chain-node/app/upgrades/evm-preinstalls"
2122
evmrpcfix "github.com/pushchain/push-chain-node/app/upgrades/evm-rpc-fix"
2223
evmv040 "github.com/pushchain/push-chain-node/app/upgrades/evm-v0-4-0"
2324
evmv050 "github.com/pushchain/push-chain-node/app/upgrades/evm-v0-5-0"
24-
evmderivedgasprice "github.com/pushchain/push-chain-node/app/upgrades/evm-derived-gas-price"
2525
evmv060 "github.com/pushchain/push-chain-node/app/upgrades/evm-v0-6-0"
2626
feeabs "github.com/pushchain/push-chain-node/app/upgrades/fee-abs"
2727
gasoracle "github.com/pushchain/push-chain-node/app/upgrades/gas-oracle"
@@ -31,6 +31,7 @@ import (
3131
pc20 "github.com/pushchain/push-chain-node/app/upgrades/pc20"
3232
proxybytecodefix "github.com/pushchain/push-chain-node/app/upgrades/proxy-bytecode-fix"
3333
purgeexpiredoutbounds "github.com/pushchain/push-chain-node/app/upgrades/purge-expired-outbounds"
34+
readstate "github.com/pushchain/push-chain-node/app/upgrades/read-state"
3435
removefeeabsv1 "github.com/pushchain/push-chain-node/app/upgrades/remove-fee-abs-v1"
3536
removeutxverifier "github.com/pushchain/push-chain-node/app/upgrades/remove-utxverifier"
3637
sdkv053 "github.com/pushchain/push-chain-node/app/upgrades/sdk-v0-53"
@@ -94,6 +95,9 @@ var Upgrades = []upgrades.Upgrade{
9495
// evm-derived-gas-price — cosmos/evm bump for the derived-tx gas price fix;
9596
// no-op (JSON-RPC reporting only, no module ConsensusVersion changes)
9697
evmderivedgasprice.NewUpgrade(),
98+
// read-state — adds the x/ucallback store and reserves every system-contract
99+
// address still unclaimed in the A/B/C ranges (41 of 47 on donut, incl. 0xC2)
100+
readstate.NewUpgrade(),
97101
}
98102

99103
// RegisterUpgradeHandlers registers the chain upgrade handlers

app/upgrades/read-state/upgrade.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package readstate
2+
3+
import (
4+
"context"
5+
6+
storetypes "cosmossdk.io/store/types"
7+
upgradetypes "cosmossdk.io/x/upgrade/types"
8+
9+
sdk "github.com/cosmos/cosmos-sdk/types"
10+
"github.com/cosmos/cosmos-sdk/types/module"
11+
12+
"github.com/pushchain/push-chain-node/app/upgrades"
13+
ucallbacktypes "github.com/pushchain/push-chain-node/x/ucallback/types"
14+
)
15+
16+
const UpgradeName = "read-state"
17+
18+
// NewUpgrade constructs the upgrade definition.
19+
//
20+
// x/ucallback is a new module, so its store has to be added here — RunMigrations
21+
// alone will register the module's consensus version but cannot create a store that
22+
// the multistore was never told about, and the node fails to load at the upgrade
23+
// height without it.
24+
func NewUpgrade() upgrades.Upgrade {
25+
return upgrades.Upgrade{
26+
UpgradeName: UpgradeName,
27+
CreateUpgradeHandler: CreateUpgradeHandler,
28+
StoreUpgrades: storetypes.StoreUpgrades{
29+
Added: []string{ucallbacktypes.StoreKey},
30+
Deleted: []string{},
31+
},
32+
}
33+
}
34+
35+
func CreateUpgradeHandler(
36+
mm upgrades.ModuleManager,
37+
configurator module.Configurator,
38+
ak *upgrades.AppKeepers,
39+
) upgradetypes.UpgradeHandler {
40+
return func(ctx context.Context, plan upgradetypes.Plan, fromVM module.VersionMap) (module.VersionMap, error) {
41+
sdkCtx := sdk.UnwrapSDKContext(ctx)
42+
sdkCtx.Logger().Info("🔧 Running upgrade:", "name", UpgradeName)
43+
44+
// Reserve every system-contract address that is still empty.
45+
//
46+
// SYSTEM_CONTRACTS is not just the named contracts: constants.go's init()
47+
// fills the 0xA0-0xAF, 0xB0-0xBF and 0xC0-0xCF ranges with a full
48+
// proxy+admin+impl triple per slot, 47 entries in total. The genesis loop
49+
// runs only at InitGenesis, so every slot added after a chain launched is
50+
// unreserved on it — and an unreserved slot can be squatted by an ordinary
51+
// account, which is the squatting defence those ranges exist for.
52+
//
53+
// On donut that is 41 of the 47, all three addresses bare, including
54+
// UNIVERSAL_CALLBACK (0x…C2). x/ucallback only accepts ReadRequested logs
55+
// from that exact address, so until it holds the proxy the module is inert.
56+
// The remaining 6 are already deployed and the already-deployed guard skips
57+
// them, leaving their code untouched.
58+
if err := ak.URegistryKeeper.DeployMissingSystemContracts(sdkCtx); err != nil {
59+
return nil, err
60+
}
61+
sdkCtx.Logger().Info("Reserved any missing system contract addresses")
62+
63+
// RunMigrations registers x/ucallback at its current consensus version and
64+
// runs its InitGenesis, which seeds Params. There is no state to migrate:
65+
// the module is new, so it starts empty.
66+
return mm.RunMigrations(ctx, configurator, fromVM)
67+
}
68+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package integrationtest
2+
3+
import (
4+
"testing"
5+
6+
evmtypes "github.com/cosmos/evm/x/vm/types"
7+
"github.com/ethereum/go-ethereum/common"
8+
"github.com/stretchr/testify/require"
9+
10+
utils "github.com/pushchain/push-chain-node/test/utils"
11+
uregistrytypes "github.com/pushchain/push-chain-node/x/uregistry/types"
12+
)
13+
14+
// The read-state upgrade must reserve every system-contract slot that is still
15+
// empty, and leave the ones already deployed exactly as they are.
16+
//
17+
// UNIVERSAL_CALLBACK (0x…C2) was added to SYSTEM_CONTRACTS after donut launched, so
18+
// on that chain its proxy, admin and implementation are all bare — verified against
19+
// the live testnet. x/ucallback only accepts ReadRequested logs from that exact
20+
// address, so if the upgrade does not reserve it the module is inert.
21+
func TestReadStateUpgrade_ReservesMissingSystemContracts(t *testing.T) {
22+
chainApp, ctx, _ := utils.SetAppWithValidators(t)
23+
k := chainApp.UregistryKeeper
24+
25+
code := func(addr string) []byte {
26+
a := common.HexToAddress(addr)
27+
acct := chainApp.EVMKeeper.GetAccountOrEmpty(ctx, a)
28+
return chainApp.EVMKeeper.GetCode(ctx, common.BytesToHash(acct.CodeHash))
29+
}
30+
31+
// Simulate a chain that launched before the slot existed: clear the triple.
32+
cb := uregistrytypes.SYSTEM_CONTRACTS["UNIVERSAL_CALLBACK"]
33+
for _, a := range []string{cb.Address, cb.ProxyAdmin, cb.Implementation} {
34+
// Point the account back at the empty-code sentinel; that is exactly the
35+
// state the reservation guard treats as "not deployed".
36+
chainApp.EVMKeeper.SetCodeHash(ctx,
37+
common.HexToAddress(a).Bytes(), evmtypes.EmptyCodeHash)
38+
}
39+
require.Empty(t, code(cb.Address), "precondition: the slot must start bare")
40+
41+
// A slot that IS deployed must be left untouched.
42+
core := uregistrytypes.SYSTEM_CONTRACTS["UNIVERSAL_CORE"]
43+
coreBefore := code(core.Address)
44+
require.NotEmpty(t, coreBefore, "precondition: UNIVERSAL_CORE is deployed at genesis")
45+
46+
require.NoError(t, k.DeployMissingSystemContracts(ctx))
47+
48+
require.NotEmpty(t, code(cb.Address), "the callback proxy must be reserved")
49+
require.NotEmpty(t, code(cb.ProxyAdmin), "its ProxyAdmin must be reserved")
50+
require.NotEmpty(t, code(cb.Implementation), "its implementation must be reserved")
51+
require.Equal(t, coreBefore, code(core.Address),
52+
"an already-deployed contract must not be redeployed or overwritten")
53+
}
54+
55+
// Running the reservation twice must change nothing. The upgrade runs once, but the
56+
// same guard protects chains where the slot is already present, and a second pass is
57+
// the cheapest way to prove the guard actually holds.
58+
func TestReadStateUpgrade_ReservationIsIdempotent(t *testing.T) {
59+
chainApp, ctx, _ := utils.SetAppWithValidators(t)
60+
k := chainApp.UregistryKeeper
61+
62+
code := func(addr string) []byte {
63+
a := common.HexToAddress(addr)
64+
acct := chainApp.EVMKeeper.GetAccountOrEmpty(ctx, a)
65+
return chainApp.EVMKeeper.GetCode(ctx, common.BytesToHash(acct.CodeHash))
66+
}
67+
68+
require.NoError(t, k.DeployMissingSystemContracts(ctx))
69+
70+
before := map[string][]byte{}
71+
for name, c := range uregistrytypes.SYSTEM_CONTRACTS {
72+
before[name] = code(c.Address)
73+
require.NotEmpty(t, before[name], "%s must be reserved after the first pass", name)
74+
}
75+
76+
require.NoError(t, k.DeployMissingSystemContracts(ctx))
77+
78+
for name, c := range uregistrytypes.SYSTEM_CONTRACTS {
79+
require.Equal(t, before[name], code(c.Address), "%s changed on the second pass", name)
80+
}
81+
}

x/uregistry/keeper/keeper.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,3 +360,17 @@ func (k Keeper) FixReservedBytecode(ctx context.Context) error {
360360

361361
return nil
362362
}
363+
364+
// DeployMissingSystemContracts reserves every address in types.SYSTEM_CONTRACTS
365+
// that does not already hold code.
366+
//
367+
// The genesis loop only runs at InitGenesis, so a slot added to SYSTEM_CONTRACTS
368+
// after a chain launched is never reserved on that chain — an ordinary account can
369+
// take the address, and the module that expects a system contract there finds an
370+
// EOA. This is the upgrade-time counterpart: same map, same bytecode, same
371+
// deterministic order, and the same already-deployed guard, so it deploys only what
372+
// is genuinely missing and is safe to run on a chain where everything is present.
373+
func (k Keeper) DeployMissingSystemContracts(ctx context.Context) error {
374+
deploySystemContracts(ctx, k.evmKeeper, types.SYSTEM_CONTRACTS)
375+
return nil
376+
}

0 commit comments

Comments
 (0)