-
Notifications
You must be signed in to change notification settings - Fork 336
feat(contract-manager): Pythnet BPF upgrade authority audit tool #3740
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
jayantk
wants to merge
2
commits into
main
Choose a base branch
from
hydra/i-cmecdvqy/head
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| # Pythnet Authority Audit Tool | ||
|
|
||
| Lists the BPF upgrade authorities for known Pyth programs deployed on Pythnet. | ||
|
|
||
| ## Usage | ||
|
|
||
| ```bash | ||
| pnpm --filter @pythnetwork/contract-manager exec ts-node scripts/list_pythnet_authorities.ts | ||
| ``` | ||
|
|
||
| ### CLI Options | ||
|
|
||
| | Flag | Default | Description | | ||
| |------|---------|-------------| | ||
| | `--rpc <url>` | `https://pythnet.rpcpool.com` | Pythnet RPC endpoint | | ||
| | `--out <path>` | `pythnet-authorities.json` | Output JSON file path | | ||
| | `--programs <path>` | _(built-in list)_ | Custom program list JSON | | ||
|
|
||
| ### Custom Program List | ||
|
|
||
| Supply a JSON file with `--programs` to override the built-in list: | ||
|
|
||
| ```json | ||
| [ | ||
| { | ||
| "name": "My Program", | ||
| "program_id": "<base58 pubkey>", | ||
| "source": "description of where this ID comes from", | ||
| "is_validator_builtin": false | ||
| } | ||
| ] | ||
| ``` | ||
|
|
||
| ## Program Registry | ||
|
|
||
| The built-in program list is sourced from: | ||
| - **[Pyth documentation](https://docs.pyth.network/price-feeds/core/contract-addresses/pythnet)** for program IDs | ||
| - **Existing repo constants** (`@pythnetwork/xc-admin-common`) for `REMOTE_EXECUTOR_ADDRESS` and `MESSAGE_BUFFER_PROGRAM_ID` | ||
|
|
||
| Current programs: | ||
| | Name | Program ID | | ||
| |------|-----------| | ||
| | Oracle Program | `FsJ3A3u2vn5cTVofAjvy6y5kwABJAqYWpe4975bi2epH` | | ||
| | Remote Executor | `exe6S3AxPVNmy46L4Nj6HrnnAVQUhwyYzMSNcnRn3qq` | | ||
| | Message Buffer | `7Vbmv1jt4vyuqBZcpYPpnVhrqVe5e6ZPb6JxDcffRHUM` | | ||
|
|
||
| ## Validator-Built-in Investigation | ||
|
|
||
| The pythnet validator tree ([pyth-network/pythnet@pyth-v1.14.29/programs](https://github.com/pyth-network/pythnet/tree/pyth-v1.14.29/programs)) was investigated for Pyth oracle programs baked into the validator as builtin/native programs. | ||
|
|
||
| **Finding: No Pyth oracle programs are baked into the validator as builtins.** | ||
|
|
||
| The validator tree contains standard Solana system programs (vote, stake, system, etc.) but the Pyth oracle, Remote Executor, and Message Buffer programs are deployed as regular BPF upgradeable programs with ProgramData accounts. This means all three have inspectable upgrade authorities via the BPF Upgradeable Loader. | ||
|
|
||
| ## Output Schema | ||
|
|
||
| The JSON output follows this schema (fields `state_authorities` and `validators` are placeholders for future PRs): | ||
|
|
||
| ```json | ||
| { | ||
| "rpc": "https://pythnet.rpcpool.com", | ||
| "generated_at": "2024-01-01T00:00:00.000Z", | ||
| "programs": [ | ||
| { | ||
| "name": "Oracle Program", | ||
| "program_id": "FsJ3A3u2vn5cTVofAjvy6y5kwABJAqYWpe4975bi2epH", | ||
| "source": "docs.pyth.network/price-feeds/core/contract-addresses/pythnet", | ||
| "upgrade_authority": "<pubkey or null>", | ||
| "programdata_address": "<pubkey or null>", | ||
| "last_deploy_slot": 12345678, | ||
| "notes": "", | ||
| "state_authorities": [] | ||
| } | ||
| ], | ||
| "validators": [] | ||
| } | ||
| ``` | ||
|
|
||
| ## Extending | ||
|
|
||
| To add a new program, edit `contract_manager/src/core/pythnet-programs.ts` and add an entry to the `PYTHNET_PROGRAMS` array. If the program is a validator builtin (no ProgramData account), set `isValidatorBuiltin: true`. |
130 changes: 130 additions & 0 deletions
130
contract_manager/scripts/__tests__/list_pythnet_authorities.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,130 @@ | ||
| /** | ||
| * Fixture-based test for list_pythnet_authorities JSON output schema. | ||
| * | ||
| * Run: pnpm --filter @pythnetwork/contract-manager exec tsx scripts/__tests__/list_pythnet_authorities.test.ts | ||
| * | ||
| * Live RPC test (optional): TEST_LIVE_RPC=1 pnpm --filter @pythnetwork/contract-manager exec tsx scripts/__tests__/list_pythnet_authorities.test.ts | ||
| */ | ||
| /* eslint-disable no-console */ | ||
| import assert from "node:assert"; | ||
| import { PublicKey } from "@solana/web3.js"; | ||
| import { | ||
| decodeProgramAccount, | ||
| decodeProgramDataAccount, | ||
| } from "../../../governance/xc_admin/packages/xc_admin_common/src/bpf_upgradable_loader"; | ||
| import { PYTHNET_PROGRAMS } from "../../src/core/pythnet-programs"; | ||
|
|
||
| // -- Fixture: a recorded Program account (type=2) pointing to a programdata address -- | ||
| // Type 2 (Program) = 02 00 00 00 in LE, followed by 32-byte programdata pubkey | ||
| const FIXTURE_PROGRAMDATA_PUBKEY = new PublicKey( | ||
| "BJ3jrUzddfuSrZHXSCxMUUQsjKEyLmuuyZebPtaxyPBJ", | ||
| ); | ||
| const programAccountData = Buffer.alloc(36); | ||
| programAccountData.writeUInt32LE(2, 0); | ||
| FIXTURE_PROGRAMDATA_PUBKEY.toBuffer().copy(programAccountData, 4); | ||
|
|
||
| // -- Fixture: a recorded ProgramData account (type=3) with upgrade authority -- | ||
| const FIXTURE_AUTHORITY = new PublicKey( | ||
| "3HGMrhJx7GhZHZSgxVikvRG4iz2gmqiH8WPqMYdykeyN", | ||
| ); | ||
| const FIXTURE_SLOT = 123456789; | ||
| const programDataWithAuthority = Buffer.alloc(45); | ||
| programDataWithAuthority.writeUInt32LE(3, 0); | ||
| programDataWithAuthority.writeBigUInt64LE(BigInt(FIXTURE_SLOT), 4); | ||
| programDataWithAuthority[12] = 1; // Some(authority) | ||
| FIXTURE_AUTHORITY.toBuffer().copy(programDataWithAuthority, 13); | ||
|
|
||
| // -- Fixture: ProgramData with no authority (immutable) -- | ||
| const programDataNoAuthority = Buffer.alloc(45); | ||
| programDataNoAuthority.writeUInt32LE(3, 0); | ||
| programDataNoAuthority.writeBigUInt64LE(BigInt(999), 4); | ||
| programDataNoAuthority[12] = 0; // None | ||
|
|
||
| function testDecodeProgramAccount() { | ||
| const result = decodeProgramAccount(programAccountData); | ||
| assert.ok(result.equals(FIXTURE_PROGRAMDATA_PUBKEY), "decoded programdata address should match fixture"); | ||
| console.log(" PASS: decodeProgramAccount"); | ||
| } | ||
|
|
||
| function testDecodeProgramDataWithAuthority() { | ||
| const result = decodeProgramDataAccount(programDataWithAuthority); | ||
| assert.strictEqual(result.slot, FIXTURE_SLOT, "slot should match"); | ||
| assert.ok(result.upgradeAuthority !== null, "authority should not be null"); | ||
| assert.ok( | ||
| result.upgradeAuthority!.equals(FIXTURE_AUTHORITY), | ||
| "authority should match fixture", | ||
| ); | ||
| console.log(" PASS: decodeProgramDataAccount (with authority)"); | ||
| } | ||
|
|
||
| function testDecodeProgramDataNoAuthority() { | ||
| const result = decodeProgramDataAccount(programDataNoAuthority); | ||
| assert.strictEqual(result.slot, 999, "slot should be 999"); | ||
| assert.strictEqual(result.upgradeAuthority, null, "authority should be null for immutable program"); | ||
| console.log(" PASS: decodeProgramDataAccount (no authority / immutable)"); | ||
| } | ||
|
|
||
| function testInvalidAccountType() { | ||
| const badData = Buffer.alloc(36); | ||
| badData.writeUInt32LE(99, 0); | ||
| assert.throws(() => decodeProgramAccount(badData), /Expected Program account type/); | ||
| console.log(" PASS: decodeProgramAccount rejects invalid type"); | ||
| } | ||
|
|
||
| function testProgramRegistry() { | ||
| assert.ok(PYTHNET_PROGRAMS.length >= 3, "should have at least 3 programs"); | ||
| for (const p of PYTHNET_PROGRAMS) { | ||
| assert.ok(p.name, "program must have a name"); | ||
| assert.ok(p.programId instanceof PublicKey, "programId must be a PublicKey"); | ||
| assert.ok(p.source, "program must have a source"); | ||
| } | ||
| console.log(" PASS: PYTHNET_PROGRAMS registry is valid"); | ||
| } | ||
|
|
||
| function testOutputSchema() { | ||
| // Verify the expected JSON schema shape | ||
| const sampleOutput = { | ||
| rpc: "https://pythnet.rpcpool.com", | ||
| generated_at: new Date().toISOString(), | ||
| programs: [ | ||
| { | ||
| name: "Test", | ||
| program_id: "11111111111111111111111111111111", | ||
| source: "test", | ||
| upgrade_authority: FIXTURE_AUTHORITY.toBase58(), | ||
| programdata_address: FIXTURE_PROGRAMDATA_PUBKEY.toBase58(), | ||
| last_deploy_slot: FIXTURE_SLOT, | ||
| notes: "", | ||
| state_authorities: [], | ||
| }, | ||
| ], | ||
| validators: [], | ||
| }; | ||
|
|
||
| assert.ok(typeof sampleOutput.rpc === "string"); | ||
| assert.ok(typeof sampleOutput.generated_at === "string"); | ||
| assert.ok(Array.isArray(sampleOutput.programs)); | ||
| assert.ok(Array.isArray(sampleOutput.validators)); | ||
| const prog = sampleOutput.programs[0]; | ||
| assert.ok(typeof prog.name === "string"); | ||
| assert.ok(typeof prog.program_id === "string"); | ||
| assert.ok(typeof prog.source === "string"); | ||
| assert.ok(Array.isArray(prog.state_authorities)); | ||
| console.log(" PASS: JSON output schema is valid"); | ||
| } | ||
|
|
||
| async function main() { | ||
| console.log("Running fixture-based tests...\n"); | ||
|
|
||
| testDecodeProgramAccount(); | ||
| testDecodeProgramDataWithAuthority(); | ||
| testDecodeProgramDataNoAuthority(); | ||
| testInvalidAccountType(); | ||
| testProgramRegistry(); | ||
| testOutputSchema(); | ||
|
|
||
| console.log("\nAll fixture tests passed!"); | ||
| } | ||
|
|
||
| // eslint-disable-next-line @typescript-eslint/no-floating-promises, unicorn/prefer-top-level-await | ||
| main(); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚩 Test uses manual assert/console.log instead of the project's Jest framework
The existing tests in this area of the codebase use Jest (see
governance/xc_admin/packages/xc_admin_common/src/__tests__/BpfUpgradableLoaderInstruction.test.ts), but this new test uses rawnode:assert+console.logwith a manualmain()runner. This means the tests won't be picked up by the project's standard test runner and won't integrate with CI. Consider converting to Jesttest()/expect()patterns for consistency and CI integration.Was this helpful? React with 👍 or 👎 to provide feedback.