feat(kms): optional TCB UpToDate requirement for apps#498
feat(kms): optional TCB UpToDate requirement for apps#498
Conversation
90140f9 to
d660bc0
Compare
d660bc0 to
bfeaa5b
Compare
… remove KMS backward-compat factory DstackApp: - Extract shared init logic into _initializeCommon() internal function - Add old 5-param initialize(address,bool,bool,bytes32,bytes32) overload for upgrade compatibility with existing proxies - Keep 6-param initialize with requireTcbUpToDate for new deployments - Add version() pure function returning 2 for capability detection DstackKms: - Remove 5-param deployAndRegisterApp backward-compat overload - Remove _deployAndRegisterApp internal function - Keep only 6-param deployAndRegisterApp with inline logic, callers must explicitly specify requireTcbUpToDate
…task - deployContract() accepts optional initializer signature param, passed to hre.upgrades.deployProxy for overload disambiguation - estimateDeploymentCost() accepts optional initializer param, passed to encodeFunctionData - app:deploy task adds --requireTcbUpToDate flag and passes 6 args with explicit initializer signature
…tion - setup.ts / DstackApp.test.ts: pass explicit initializer signature to deployContract and hre.upgrades.deployProxy - DstackApp.test.ts: add version() test, add TCB-via-initialize tests - DstackApp.upgrade.test.ts (new): 9 cases covering: - Old 5-param proxy upgrade preserves storage - version()=2 available after upgrade - requireTcbUpToDate defaults false (no silent behavior change) - setRequireTcbUpToDate works post-upgrade with access control - isAppAllowed TCB enforcement after owner opt-in - KMS factory deploys with TCB on/off, end-to-end verification
Manual Upgrade Test Plan (anvil fork of Base)SetupTerminal 1 — start anvil: anvil --fork-url https://mainnet.base.orgTerminal 2 — all commands below: cd kms/auth-eth
export RPC=http://127.0.0.1:8545
export PK=0xdf57089febbacf7ba0bc227dafbffa9fc08a93fdc68e1e42411a14efcf23656e
export IMPL_SLOT=0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbcPhase 1: Deploy old version (master)git checkout master
npx hardhat clean && npx hardhat compile
npx hardhat kms:deploy --with-app-impl --network test
# Record output, set env vars:
export KMS_PROXY=<DstackKms proxy address>
export OLD_APP_IMPL=<DstackApp implementation address>
export KMS_CONTRACT_ADDRESS=$KMS_PROXY
npx hardhat kms:create-app \
--allow-any-device \
--hash 0x0000000000000000000000000000000000000000000000000000000000000001 \
--network test
# Record output, set env var:
export EXISTING_APP=<App proxy address>Phase 2: Record pre-upgrade baseline# Old version has no version(), expect revert
cast call $EXISTING_APP "version()(uint256)" --rpc-url $RPC
# Old version has no requireTcbUpToDate, expect revert
cast call $EXISTING_APP "requireTcbUpToDate()(bool)" --rpc-url $RPC
# Existing data should be present
cast call $EXISTING_APP "allowedComposeHashes(bytes32)(bool)" \
0x0000000000000000000000000000000000000000000000000000000000000001 \
--rpc-url $RPC
# Expected: true
cast call $EXISTING_APP "allowAnyDevice()(bool)" --rpc-url $RPC
# Expected: true
cast call $EXISTING_APP "owner()(address)" --rpc-url $RPC
# Expected: deployer address
# Record current implementation address (ERC1967 slot)
cast storage $EXISTING_APP $IMPL_SLOT --rpc-url $RPC
# Should match OLD_APP_IMPL (left-zero-padded to 32 bytes)Phase 3: Switch to new code and upgradegit checkout feature/app-tcb-toggle
npx hardhat clean && npx hardhat compile
# 3a. Deploy new DstackApp implementation
npx hardhat app:deploy-impl --network test
export NEW_APP_IMPL=<new DstackApp impl address>
# Verify old and new impl addresses differ
echo "OLD: $OLD_APP_IMPL"
echo "NEW: $NEW_APP_IMPL"
# These MUST be different; if identical, compilation is stale
# 3b. Deploy new DstackKms implementation and upgrade KMS proxy
npx hardhat kms:deploy-impl --network test
npx hardhat kms:upgrade --address $KMS_PROXY --network test
# 3c. Update KMS app implementation pointer
npx hardhat kms:set-app-implementation $NEW_APP_IMPL --network test
# 3d. Upgrade existing DstackApp proxy via direct UUPS call
# (bypasses hardhat-upgrades plugin manifest issues with factory-deployed proxies)
cast send $EXISTING_APP "upgradeToAndCall(address,bytes)" \
$NEW_APP_IMPL 0x \
--private-key $PK --rpc-url $RPCPhase 4: Post-upgrade verification# Implementation address updated
cast storage $EXISTING_APP $IMPL_SLOT --rpc-url $RPC
# Expected: NEW_APP_IMPL (left-zero-padded to 32 bytes)
# version() = 2
cast call $EXISTING_APP "version()(uint256)" --rpc-url $RPC
# Expected: 2
# requireTcbUpToDate defaults to false (no silent behavior change)
cast call $EXISTING_APP "requireTcbUpToDate()(bool)" --rpc-url $RPC
# Expected: false
# Old data preserved
cast call $EXISTING_APP "allowedComposeHashes(bytes32)(bool)" \
0x0000000000000000000000000000000000000000000000000000000000000001 \
--rpc-url $RPC
# Expected: true
cast call $EXISTING_APP "allowAnyDevice()(bool)" --rpc-url $RPC
# Expected: true
cast call $EXISTING_APP "owner()(address)" --rpc-url $RPC
# Expected: deployer address
# Enable TCB check
cast send $EXISTING_APP "setRequireTcbUpToDate(bool)" true \
--private-key $PK --rpc-url $RPC
cast call $EXISTING_APP "requireTcbUpToDate()(bool)" --rpc-url $RPC
# Expected: true
# Disable TCB check
cast send $EXISTING_APP "setRequireTcbUpToDate(bool)" false \
--private-key $PK --rpc-url $RPC
cast call $EXISTING_APP "requireTcbUpToDate()(bool)" --rpc-url $RPC
# Expected: falsePhase 5: Verify new factory path (6-param with TCB flag)# With TCB flag
npx hardhat kms:create-app \
--require-tcb-up-to-date \
--allow-any-device \
--hash 0x0000000000000000000000000000000000000000000000000000000000000002 \
--network test
export NEW_APP=<output address>
cast call $NEW_APP "version()(uint256)" --rpc-url $RPC
# Expected: 2
cast call $NEW_APP "requireTcbUpToDate()(bool)" --rpc-url $RPC
# Expected: true
# Without TCB flag
npx hardhat kms:create-app \
--allow-any-device \
--hash 0x0000000000000000000000000000000000000000000000000000000000000003 \
--network test
export NEW_APP2=<output address>
cast call $NEW_APP2 "requireTcbUpToDate()(bool)" --rpc-url $RPC
# Expected: falsePhase 6: Verify backward-compatible 5-param factory path (old SDK callers)This verifies that old SDK callers using the 5-param # Send tx via old 5-param overload, capture txhash with --json
TX_HASH=$(cast send $KMS_PROXY \
"deployAndRegisterApp(address,bool,bool,bytes32,bytes32)" \
$(cast wallet address --private-key $PK) \
false \
true \
0x0000000000000000000000000000000000000000000000000000000000000000 \
0x0000000000000000000000000000000000000000000000000000000000000005 \
--private-key $PK --rpc-url $RPC --json | jq -r '.transactionHash')
# Extract app address from AppDeployedViaFactory event (topics[1])
# Event signature: AppDeployedViaFactory(address indexed appId, address indexed deployer)
# Topic[0] = 0xfd86d7f6962eba3b7a3bf9129c06c0b2f885e1c61ef2c9f0dbb856be0deefdee
export OLD_SDK_APP=$(cast receipt $TX_HASH --json --rpc-url $RPC \
| jq -r '.logs[] | select(.topics[0] == "0xfd86d7f6962eba3b7a3bf9129c06c0b2f885e1c61ef2c9f0dbb856be0deefdee") | .topics[1]' \
| sed 's/0x000000000000000000000000/0x/')
echo "Old SDK app deployed at: $OLD_SDK_APP"
cast call $OLD_SDK_APP "version()(uint256)" --rpc-url $RPC
# Expected: 2
cast call $OLD_SDK_APP "requireTcbUpToDate()(bool)" --rpc-url $RPC
# Expected: false (5-param overload defaults to false)
cast call $OLD_SDK_APP "allowAnyDevice()(bool)" --rpc-url $RPC
# Expected: truePhase 7: Verify app:deploy standalone pathnpx hardhat app:deploy \
--require-tcb-up-to-date \
--allow-any-device \
--hash 0x0000000000000000000000000000000000000000000000000000000000000004 \
--network test
export STANDALONE_APP=<output address>
cast call $STANDALONE_APP "version()(uint256)" --rpc-url $RPC
# Expected: 2
cast call $STANDALONE_APP "requireTcbUpToDate()(bool)" --rpc-url $RPC
# Expected: trueChecklist
Notes
|
…rApp Keep the old 5-param deployAndRegisterApp(address,bool,bool,bytes32,bytes32) overload that defaults requireTcbUpToDate=false, so existing SDK callers (e.g. phala-cloud SDKs using viem) continue to work without changes. Fix ethers v6 ambiguous overload resolution by using explicit function signatures in kms:create-app task and upgrade tests.
Production Upgrade Safety Guide (Multisig)Owner operations go through the multisig. Implementation deployments can be done by any EOA independently. Pre-upgrade: Record all current implementation addressesexport IMPL_SLOT=0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc
# Record KMS current implementation
OLD_KMS_IMPL=$(cast storage $KMS_PROXY $IMPL_SLOT --rpc-url $RPC)
echo "KMS impl: $OLD_KMS_IMPL"
# Record each App proxy current implementation
OLD_APP_IMPL=$(cast storage $APP_PROXY $IMPL_SLOT --rpc-url $RPC)
echo "App impl: $OLD_APP_IMPL"
# Record current factory app implementation pointer
OLD_FACTORY_APP_IMPL=$(cast call $KMS_PROXY "appImplementation()(address)" --rpc-url $RPC)
echo "Factory app impl: $OLD_FACTORY_APP_IMPL"Save these values offline before proceeding. They are your rollback targets. Phase A: Deploy new implementations (any EOA, no multisig needed)Implementation contracts are just bytecode on chain — deploying them has zero effect on existing proxies. Any team member with an EOA and gas can do this independently, ahead of the multisig signing session. npx hardhat app:deploy-impl --network <network>
# Record: NEW_APP_IMPL=<address>
npx hardhat kms:deploy-impl --network <network>
# Record: NEW_KMS_IMPL=<address>Verify the deployed code is correct: # Should return 2
cast call $NEW_APP_IMPL "version()(uint256)" --rpc-url $RPC
# Confirm implementation addresses differ from current
echo "Old app impl: $OLD_APP_IMPL"
echo "New app impl: $NEW_APP_IMPL"These addresses can sit on chain indefinitely until the multisig is ready to proceed. Phase B: Full dry run on anvil fork (any team member)Run the complete Manual Upgrade Test Plan (see comment above) on a fork. This also requires no multisig. Phase C: Multisig operations (requires signature collection)Pre-encode ALL calldata (including rollback) before starting the signing session. Tx #1: Upgrade KMS proxycast calldata "upgradeToAndCall(address,bytes)" $NEW_KMS_IMPL 0xSubmit to Safe UI:
After execution, verify: cast storage $KMS_PROXY $IMPL_SLOT --rpc-url $RPC
# Must match NEW_KMS_IMPL
cast call $KMS_PROXY "appImplementation()(address)" --rpc-url $RPC
# Must still return OLD_FACTORY_APP_IMPL (unchanged)Tx #2: Update factory app implementation pointercast calldata "setAppImplementation(address)" $NEW_APP_IMPLSubmit to Safe UI:
This only affects NEW apps created via factory. Existing apps are untouched. Tx #3: Canary — upgrade ONE non-critical app proxycast calldata "upgradeToAndCall(address,bytes)" $NEW_APP_IMPL 0xSubmit to Safe UI:
Verify: cast call $CANARY_APP "version()(uint256)" --rpc-url $RPC
# Expected: 2
cast call $CANARY_APP "requireTcbUpToDate()(bool)" --rpc-url $RPC
# Expected: false
cast call $CANARY_APP "owner()(address)" --rpc-url $RPC
# Expected: multisig address (unchanged)Tx #4+: Upgrade remaining app proxiesRepeat tx #3 for each app. Can be batched in a single multisig approval using Safe Transaction Builder. Rollback commands (multisig transactions)
Pre-encode these before starting the upgrade so they're ready if needed: Rollback KMS proxy: cast calldata "upgradeToAndCall(address,bytes)" $OLD_KMS_IMPL 0x
# Submit to Safe: To=$KMS_PROXY, Value=0, Data=<output>Rollback App proxy: cast calldata "upgradeToAndCall(address,bytes)" $OLD_APP_IMPL 0x
# Submit to Safe: To=$APP_PROXY, Value=0, Data=<output>Rollback factory pointer: cast calldata "setAppImplementation(address)" $OLD_FACTORY_APP_IMPL
# Submit to Safe: To=$KMS_PROXY, Value=0, Data=<output>What cannot be rolled backIf a DstackApp owner has previously called Post-upgrade behavior guarantees
Checklist before signing session
|
Summary
requireTcbUpToDateflag to DstackApp (initializer + setter) and enforce it inisAppAllowedUpgrade notes (existing KMS deployments)
npx hardhat kms:deploy-impl).upgradeTo(address)on the existing KMS proxy.DstackAppimplementation, deploy a newDstackAppimplementation and callsetAppImplementation(address)on the KMS proxy. Existing app proxies can be upgraded separately via their ownupgradeTo(if upgrades are not disabled).Testing
cd kms/auth-eth && npm test