Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions contract_manager/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,16 @@
"types": "./dist/cjs/core/contracts/wormhole.d.ts"
}
},
"./core/pythnet-programs": {
"import": {
"default": "./dist/esm/core/pythnet-programs.mjs",
"types": "./dist/esm/core/pythnet-programs.d.ts"
},
"require": {
"default": "./dist/cjs/core/pythnet-programs.cjs",
"types": "./dist/cjs/core/pythnet-programs.d.ts"
}
},
"./core/token": {
"import": {
"default": "./dist/esm/core/token.mjs",
Expand Down
81 changes: 81 additions & 0 deletions contract_manager/scripts/PYTHNET_AUTHORITIES.md
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 contract_manager/scripts/__tests__/list_pythnet_authorities.test.ts
Copy link
Copy Markdown
Contributor

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 raw node:assert + console.log with a manual main() 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 Jest test()/expect() patterns for consistency and CI integration.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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();
Loading
Loading