Browse Source

[Sui 15/x] - Pyth upgrade script + bug fix (#936)

* upgrade script for Pyth, so far getting the dependencies and digest works

* upgrade_pyth

* made progress: works up until verify_vaa

* delete some comments

* upgrade almost works, on last step now

* comments

* edit

* upgrade process pretty much works (we got an IncompatibleUpgrade error)

* fix bug in pyth migrate process (handle_migrate threw error because take_digest was destroying non-empty cursor and not removing prefix bytes)

* comments and toml file

* revert change to Move.lock file

* remove migrate__v__0_1_1

* prettier
optke3 2 years ago
parent
commit
0ec7b69031

+ 1 - 1
target_chains/sui/contracts/Move.testnet.toml

@@ -12,7 +12,7 @@ local = "../../../../wormhole/sui/wormhole"
 
 [addresses]
 pyth = "0x0"
-wormhole = "0x80c60bff35fe5026e319cf3d66ae671f2b4e12923c92c45df75eaf4de79e3ce7"
+wormhole = "0xf47329f4344f3bf0f8e436e2f7b485466cff300f12a166563995d3888c296a94"
 
 [dev-addresses]
 pyth = "0x100"

+ 7 - 2
target_chains/sui/contracts/sources/governance/contract_upgrade.move

@@ -12,6 +12,7 @@ module pyth::contract_upgrade {
     use sui::object::{ID};
     use sui::package::{UpgradeReceipt, UpgradeTicket};
     use wormhole::bytes32::{Self, Bytes32};
+    use wormhole::bytes::{Self};
     use wormhole::cursor::{Self};
     use wormhole::governance_message::{Self, DecreeTicket, DecreeReceipt};
 
@@ -123,13 +124,17 @@ module pyth::contract_upgrade {
 
     fun deserialize(payload: vector<u8>): UpgradeContract {
         let cur = cursor::new(payload);
+        // Pyth upgrade governance message payloads are 40 bytes long. The breakdown looks like
+        // 4 (magic) + 1 (module name) + 1 (action) + 2 (target chain) + 32 (digest)
 
+        bytes::take_bytes(&mut cur, 8); // ignore the first 8 bytes here (they were used for verification in a different code path)
         // This amount cannot be greater than max u64.
         let digest = bytes32::take_bytes(&mut cur);
         assert!(bytes32::is_nonzero(&digest), E_DIGEST_ZERO_BYTES);
 
-        cursor::destroy_empty(cur);
-
+        // there might be additional appended to payload in the future,
+        // which is why we don't cursor::destroy_empty(&mut cur)
+        cursor::take_rest(cur);
         UpgradeContract { digest }
     }
 

+ 0 - 4
target_chains/sui/contracts/sources/migrate.move

@@ -26,8 +26,6 @@ module pyth::migrate {
         pyth_state: &mut State,
         receipt: DecreeReceipt<GovernanceWitness>
     ) {
-        // This should be removed in an upgrade from 0.1.1.
-        state::migrate__v__0_1_1(pyth_state);
 
         // Perform standard migrate.
         handle_migrate(pyth_state, receipt);
@@ -55,8 +53,6 @@ module pyth::migrate {
         pyth_state: &mut State,
         receipt: DecreeReceipt<GovernanceWitness>
     ) {
-        // Update the version first.
-        //
         // See `version_control` module for hard-coded configuration.
         state::migrate_version(pyth_state);
 

+ 3 - 3
target_chains/sui/scripts/pyth/pyth_deploy.ts

@@ -18,8 +18,8 @@ import { REGISTRY, NETWORK } from "../registry";
 dotenv.config({ path: "~/.env" });
 
 // Network dependent settings.
-let network = NETWORK.MAINNET; // <= NOTE: Update this when changing network
-const walletPrivateKey = process.env.SUI_MAINNET; // <= NOTE: Update this when changing network
+let network = NETWORK.TESTNET; // <= NOTE: Update this when changing network
+const walletPrivateKey = process.env.SUI_TESTNET_ALT_KEY; // <= NOTE: Update this when changing network
 
 const registry = REGISTRY[network];
 const provider = new JsonRpcProvider(
@@ -47,7 +47,7 @@ async function publishPackage(signer: RawSigner, packagePath: string) {
       dependencies: string[];
     } = JSON.parse(
       execSync(
-        `sui move build --dump-bytecode-as-base64 --path ${packagePath} 2> /dev/null`,
+        `sui move build --dump-bytecode-as-base64 --path ${__dirname}/${packagePath} 2> /dev/null`,
         {
           encoding: "utf-8",
         }

+ 4 - 4
target_chains/sui/scripts/pyth/pyth_init_state.ts

@@ -12,8 +12,8 @@ import { REGISTRY, NETWORK, INITIAL_DATA_SOURCES } from "../registry";
 dotenv.config({ path: "~/.env" });
 
 // Network dependent settings.
-let network = NETWORK.MAINNET; // <= NOTE: Update this when changing network
-let walletPrivateKey = process.env.SUI_MAINNET; // <= NOTE: Update this when changing network
+let network = NETWORK.TESTNET; // <= NOTE: Update this when changing network
+let walletPrivateKey = process.env.SUI_TESTNET_ALT_KEY; // <= NOTE: Update this when changing network
 
 const registry = REGISTRY[network];
 const initial_data_sources = INITIAL_DATA_SOURCES[network];
@@ -35,9 +35,9 @@ async function main() {
 
   // NOTE: Set these before calling init_pyth
   const upgradeCap =
-    "0x92d51150b762fd694877b23ecaba79a3fc1032bc24914d145a393b62e1e61894";
+    "0x3009604c845875def6c08d6bba8769f13e8fd4c600210063cdb5bf3b3c650eca";
   const deployerCap =
-    "0x645ba70c9087d54a3e5e6abed0d506516dddb71d987b0ee503593de2677caefe";
+    "0x6655c230a23dc642d97fa9f8ab2a23a92813f23930e33f3fe20e63acfe3b60e9";
 
   init_pyth(wallet, PYTH_PACKAGE, deployerCap, upgradeCap);
 }

+ 311 - 0
target_chains/sui/scripts/pyth/upgrade_pyth.ts

@@ -0,0 +1,311 @@
+import * as mock from "@certusone/wormhole-sdk/lib/cjs/mock";
+import dotenv from "dotenv";
+
+import {
+  RawSigner,
+  SUI_CLOCK_OBJECT_ID,
+  TransactionBlock,
+  fromB64,
+  normalizeSuiObjectId,
+  JsonRpcProvider,
+  Ed25519Keypair,
+  testnetConnection,
+  Connection,
+} from "@mysten/sui.js";
+import { execSync } from "child_process";
+import { resolve } from "path";
+import * as fs from "fs";
+
+import { REGISTRY, NETWORK } from "../registry";
+import { modifySignTransaction } from "@certusone/wormhole-sdk/lib/cjs/solana";
+
+dotenv.config({ path: "~/.env" });
+
+// Network dependent settings.
+let network = NETWORK.TESTNET; // <= NOTE: Update this when changing network
+const walletPrivateKey = process.env.SUI_TESTNET_ALT_KEY_BASE_64; // <= NOTE: Update this when changing network
+
+const guardianPrivateKey = process.env.WH_TESTNET_GUARDIAN_PRIVATE_KEY;
+
+const registry = REGISTRY[network];
+const provider = new JsonRpcProvider(
+  new Connection({ fullnode: registry["RPC_URL"] })
+);
+
+const PYTH_STATE_ID = registry["PYTH_STATE_ID"];
+const PYTH_PACKAGE_ID = registry["PYTH_PACKAGE_ID"];
+const WORMHOLE_STATE_ID = registry["WORMHOLE_STATE_ID"];
+const WORMHOLE_PACKAGE_ID = registry["WORMHOLE_PACKAGE_ID"];
+console.log("WORMHOLE_STATE_ID: ", WORMHOLE_STATE_ID);
+console.log("PYTH_STATE_ID: ", WORMHOLE_STATE_ID);
+
+const GOVERNANCE_EMITTER =
+  //"0000000000000000000000000000000000000000000000000000000000000004";
+  "63278d271099bfd491951b3e648f08b1c71631e4a53674ad43e8f9f98068c385";
+
+// To upgrade Pyth, take the following steps.
+// 0. Make contract changes in the "contracts" folder. These updated contracts will be posted on chain as an
+//    entirely new package. The old package will still be valid unless we "brick" its call-sites explicitly
+//    (this is done for you via the version control logic built into the Pyth contracts).
+// 1. Make sure that in version_control.move, you create a new struct for the new version and update the
+//    current_version() and previous_version() functions accordingly. The former should point to the new version,
+//    and the latter should point to the old version.
+// 2. Update the Move.toml file so that it points to a wormhole dependency whose Move.toml file has a "published-at" field
+//    specified at the top with the correct address.
+// 3. Execute this script!
+//
+async function main() {
+  if (guardianPrivateKey === undefined) {
+    throw new Error("TESTNET_GUARDIAN_PRIVATE_KEY unset in environment");
+  }
+  if (walletPrivateKey === undefined) {
+    throw new Error("TESTNET_WALLET_PRIVATE_KEY unset in environment");
+  }
+  console.log("priv key: ", walletPrivateKey);
+
+  const wallet = new RawSigner(
+    Ed25519Keypair.fromSecretKey(
+      network == "MAINNET"
+        ? Buffer.from(walletPrivateKey, "hex")
+        : Buffer.from(walletPrivateKey, "base64")
+    ),
+    provider
+  );
+
+  console.log("wallet address: ", wallet.getAddress());
+
+  const pythContractsPath = resolve(`${__dirname}/../../contracts`);
+
+  // Build for digest.
+  const { modules, dependencies, digest } =
+    buildForBytecodeAndDigest(pythContractsPath);
+  console.log("dependencies", dependencies);
+  console.log("digest", digest.toString("hex"));
+
+  // ===========================================================================================
+  // Construct VAA. We will use the signed VAA when we execute the upgrade.
+  // For a mainnet contract upgrade, we would not construct AND sign the VAA here. Instead, all
+  // the guardians would have to sign the upgrade VAA.
+  const guardians = new mock.MockGuardians(0, [guardianPrivateKey]);
+  const timestamp = 12345678;
+  const governance = new mock.GovernanceEmitter(GOVERNANCE_EMITTER);
+
+  const action = 0;
+  const chain = 21;
+
+  // construct VAA inner payload
+
+  const magic = Buffer.alloc(4);
+  magic.write("PTGM", 0); // magic
+  console.log("magic buffer: ", magic);
+
+  let inner_payload = Buffer.alloc(8); // 4 (magic) + 1 (module name) + 1 (action) + 2 (target chain) = 8
+  inner_payload.write(magic.toString(), 0); // magic = "PTGM"
+  inner_payload.writeUInt8(1, 4); // moduleName = 1
+  inner_payload.writeUInt8(0, 5); // action = 0
+  inner_payload.writeUInt16BE(21, 6); // target chain = 21
+  inner_payload = Buffer.concat([inner_payload, digest]);
+
+  console.log("digest: ", digest.toString("hex"));
+  console.log("inner payload: ", inner_payload.toString("hex"));
+
+  // create governance message
+  let msg = governance.publishGovernanceMessage(
+    timestamp,
+    "",
+    inner_payload,
+    action,
+    chain
+  );
+  msg.writeUInt8(0x1, 84 - 33 + 31); // here we insert an 0x1 in the right place to make the module name "0x00000000000000000000000000000001"
+
+  console.log("governance msg: ", msg.toString("hex"));
+
+  // sign governance message
+  const signedVaa = guardians.addSignatures(msg, [0]);
+  console.log("Upgrade VAA:", signedVaa.toString("hex"));
+  // ===========================================================================================
+
+  //Execute upgrade with signed governance VAA.
+  const upgradeResults = await upgradePyth(
+    wallet,
+    PYTH_STATE_ID,
+    WORMHOLE_STATE_ID,
+    modules,
+    dependencies,
+    signedVaa
+  );
+
+  console.log("tx digest", upgradeResults.digest);
+  console.log("tx effects", JSON.stringify(upgradeResults.effects!));
+  console.log("tx events", JSON.stringify(upgradeResults.events!));
+
+  const migrateResults = await migratePyth(
+    wallet,
+    PYTH_STATE_ID,
+    WORMHOLE_STATE_ID,
+    signedVaa
+  );
+  console.log("tx digest", migrateResults.digest);
+  console.log("tx effects", JSON.stringify(migrateResults.effects!));
+  console.log("tx events", JSON.stringify(migrateResults.events!));
+}
+
+main();
+
+function buildForBytecodeAndDigest(packagePath: string) {
+  const buildOutput: {
+    modules: string[];
+    dependencies: string[];
+    digest: number[];
+  } = JSON.parse(
+    execSync(
+      `sui move build --dump-bytecode-as-base64 -p ${packagePath} 2> /dev/null`,
+      { encoding: "utf-8" }
+    )
+  );
+  return {
+    modules: buildOutput.modules.map((m: string) => Array.from(fromB64(m))),
+    dependencies: buildOutput.dependencies.map((d: string) =>
+      normalizeSuiObjectId(d)
+    ),
+    digest: Buffer.from(buildOutput.digest),
+  };
+}
+
+async function getPackageId(
+  provider: JsonRpcProvider,
+  stateId: string
+): Promise<string> {
+  const state = await provider
+    .getObject({
+      id: stateId,
+      options: {
+        showContent: true,
+      },
+    })
+    .then((result) => {
+      if (result.data?.content?.dataType == "moveObject") {
+        return result.data.content.fields;
+      }
+
+      throw new Error("not move object");
+    });
+
+  if ("upgrade_cap" in state) {
+    return state.upgrade_cap.fields.package;
+  }
+
+  throw new Error("upgrade_cap not found");
+}
+
+async function upgradePyth(
+  signer: RawSigner,
+  pythStateId: string,
+  wormholeStateId: string,
+  modules: number[][],
+  dependencies: string[],
+  signedVaa: Buffer
+) {
+  const pythPackage = await getPackageId(signer.provider, pythStateId);
+  const wormholePackage = await getPackageId(signer.provider, wormholeStateId);
+
+  console.log("pythPackage: ", pythPackage);
+  console.log("wormholePackage: ", wormholePackage);
+
+  const tx = new TransactionBlock();
+
+  const [verifiedVaa] = tx.moveCall({
+    target: `${wormholePackage}::vaa::parse_and_verify`,
+    arguments: [
+      tx.object(wormholeStateId),
+      tx.pure(Array.from(signedVaa)),
+      tx.object(SUI_CLOCK_OBJECT_ID),
+    ],
+  });
+
+  const [decreeTicket] = tx.moveCall({
+    target: `${pythPackage}::contract_upgrade::authorize_governance`,
+    arguments: [tx.object(pythStateId)],
+  });
+
+  const [decreeReceipt] = tx.moveCall({
+    target: `${wormholePackage}::governance_message::verify_vaa`,
+    arguments: [tx.object(wormholeStateId), verifiedVaa, decreeTicket],
+    typeArguments: [`${pythPackage}::governance_witness::GovernanceWitness`],
+  });
+
+  // Authorize upgrade.
+  const [upgradeTicket] = tx.moveCall({
+    target: `${pythPackage}::contract_upgrade::authorize_upgrade`,
+    arguments: [tx.object(pythStateId), decreeReceipt],
+  });
+
+  // Build and generate modules and dependencies for upgrade.
+  const [upgradeReceipt] = tx.upgrade({
+    modules,
+    dependencies,
+    packageId: pythPackage,
+    ticket: upgradeTicket,
+  });
+
+  // Commit upgrade.
+  tx.moveCall({
+    target: `${pythPackage}::contract_upgrade::commit_upgrade`,
+    arguments: [tx.object(pythStateId), upgradeReceipt],
+  });
+
+  tx.setGasBudget(2_000_000_000n);
+
+  return signer.signAndExecuteTransactionBlock({
+    transactionBlock: tx,
+    options: {
+      showEffects: true,
+      showEvents: true,
+    },
+  });
+}
+
+async function migratePyth(
+  signer: RawSigner,
+  pythStateId: string,
+  wormholeStateId: string,
+  signedUpgradeVaa: Buffer
+) {
+  const pythPackage = await getPackageId(signer.provider, pythStateId);
+  const wormholePackage = await getPackageId(signer.provider, wormholeStateId);
+
+  const tx = new TransactionBlock();
+
+  const [verifiedVaa] = tx.moveCall({
+    target: `${wormholePackage}::vaa::parse_and_verify`,
+    arguments: [
+      tx.object(wormholeStateId),
+      tx.pure(Array.from(signedUpgradeVaa)),
+      tx.object(SUI_CLOCK_OBJECT_ID),
+    ],
+  });
+  const [decreeTicket] = tx.moveCall({
+    target: `${pythPackage}::contract_upgrade::authorize_governance`,
+    arguments: [tx.object(pythStateId)],
+  });
+  const [decreeReceipt] = tx.moveCall({
+    target: `${wormholePackage}::governance_message::verify_vaa`,
+    arguments: [tx.object(wormholeStateId), verifiedVaa, decreeTicket],
+    typeArguments: [`${pythPackage}::governance_witness::GovernanceWitness`],
+  });
+  tx.moveCall({
+    target: `${pythPackage}::migrate::migrate`,
+    arguments: [tx.object(pythStateId), decreeReceipt],
+  });
+
+  tx.setGasBudget(2_000_000_000n);
+
+  return signer.signAndExecuteTransactionBlock({
+    transactionBlock: tx,
+    options: {
+      showEffects: true,
+      showEvents: true,
+    },
+  });
+}

+ 4 - 4
target_chains/sui/scripts/registry.ts

@@ -18,13 +18,13 @@ export const REGISTRY = {
   },
   TESTNET: {
     PYTH_PACKAGE_ID:
-      "0x975e063f398f720af4f33ec06a927f14ea76ca24f7f8dd544aa62ab9d5d15f44",
+      "0x431c1cfb9a4da32c77810a1c48aa19cc2edb03010281e0fe411b4b3b38589df1",
     PYTH_STATE_ID:
-      "0xd8afde3a48b4ff7212bd6829a150f43f59043221200d63504d981f62bff2e27a",
+      "0xd3e79c2c083b934e78b3bd58a490ec6b092561954da6e7322e1e2b3c8abfddc0",
     WORMHOLE_PACKAGE_ID:
-      "0xcc029e2810f17f9f43f52262f40026a71fbdca40ed3803ad2884994361910b7e",
+      "0xf47329f4344f3bf0f8e436e2f7b485466cff300f12a166563995d3888c296a94",
     WORMHOLE_STATE_ID:
-      "0xebba4cc4d614f7a7cdbe883acc76d1cc767922bc96778e7b68be0d15fce27c02",
+      "0x31358d198147da50db32eda2562951d53973a0c0ad5ed738e9b17d88b213d790",
     RPC_URL: "https://fullnode.testnet.sui.io:443",
   },
   MAINNET: {