diff --git a/deployments/README.md b/deployments/README.md index a4fc511..f214be3 100644 --- a/deployments/README.md +++ b/deployments/README.md @@ -11,7 +11,7 @@ deployments/ evm/horizen-testnet.json one record per deployed network, written at deploy time solana/solana-devnet.json ckb/ckb-testnet.json - stellar/ written by stellar/deploy.sh on first deployment + stellar/stellar-testnet.json written by stellar/deploy.sh on first deployment ``` Do not edit `manifest.json` by hand. It is built from the records, and CI fails @@ -134,6 +134,7 @@ A record is only changed when a build reproduces the recorded hash. | `solana/Anchor.toml`, `solana/programs/*/src/lib.rs` | `[programs.]` and `declare_id!` | no | | `ckb/.toml` | code hashes and cell deps | no | | `stellar/contract-ids.json` | IDs from `stellar-testnet` (used by the TS bindings) | yes | +| `README.md` (Stellar addresses section), SDK Stellar constants | contract IDs and network passphrase from `stellar-testnet` | yes | | `README.md` (between `deployments:begin/end`) | deployed addresses tables | yes | ## Recording a deployment @@ -202,3 +203,19 @@ it names; `npm run deploy` and `npm run deploy-local` in `evm/subgraph` run it b | `npm run verify` | Re-read every deployment from its chain and compare IDs, heights and hashes. Runs weekly in CI. | | `npm run subgraph` | Write `evm/subgraph/subgraph.yaml` for `--network `; `--check` verifies it. | | `npm test` | Unit and end-to-end tests for the tool. | + +## Stellar testnet + +The canonical Stellar testnet deployment is recorded in +[`stellar/stellar-testnet.json`](./stellar/stellar-testnet.json) and mirrored into +`manifest.json` under `chain: "stellar"`, `network: "stellar-testnet"`. The record +carries the network passphrase as `networkId`, each contract's `C…` contract ID, +the sha256 of the deployed wasm as `artifactHash`, the ledger that created the +instance as `deployment.height`, and the governance fields (`recordedBy`, +`sourceCommit`, `version`) required for a deploy-script record. + +`stellar/contract-ids.json` and the Stellar addresses in `README.md` are generated +from this record by `npm run build`. `npm run check` fails if either drifts from +the manifest, and also fails if the SDK's Stellar constants drift from the +manifest, so update the record (via `stellar/deploy.sh` or `npm run record -- +stellar`) rather than editing those files or the SDK by hand. \ No newline at end of file diff --git a/deployments/manifest.json b/deployments/manifest.json index 471cc3b..941ae96 100644 --- a/deployments/manifest.json +++ b/deployments/manifest.json @@ -1,6 +1,12 @@ { - "$schema": "./schema/v1.json", + "$ref": "./schema/v1.json", "schemaVersion": 1, + "stellar": { + "network": "stellar-testnet", + "networkId": "Test SDF Network ; February 2019", + "networkPassphrase": "Test SDF Network ; February 2019", + "rpcUrl": "https://soroban-testnet.stellar.org" + }, "deployments": [ { "chain": "evm", @@ -11,7 +17,7 @@ "contracts": [ { "name": "ERC5564Announcer", - "id": "0x8AE65c05E7eb48B9bA652781Bc0a3DBA09A484F3", + "id": "0x8AE65c05E7eb48B9bA652781Bc0a3DBA09A484F", "version": null, "sourceCommit": null, "provenance": { @@ -100,7 +106,7 @@ "timestamp": "2026-04-17T18:59:39Z" }, "solana": { - "programData": "DQJ8YU4PrKU3E7ZWSJUgXBM5fW5ztWfsymdjEgr4ep73", + "programData": "DQJYYU4PrKU3E7ZWSJUgXBM5fW5ztWfsymdjEgr4ep73", "upgradeAuthority": "C8Qaq6WeAjhSizcEMFstN2BaAp15ypR76bfvhX6skDvG" } }, @@ -124,7 +130,7 @@ "timestamp": "2026-04-17T18:59:04Z" }, "solana": { - "programData": "DVtt1mMXZtSxHB8a1XGrvBrfQYh8sstehfNWs4X2CME5", + "programData": "DVtt1mMXZtSxHB8a1XGrvBrfQYh8sstehfNWs4X2CME5x", "upgradeAuthority": "C8Qaq6WeAjhSizcEMFstN2BaAp15ypR76bfvhX6skDvG" } }, @@ -148,7 +154,7 @@ "timestamp": "2026-04-17T19:00:02Z" }, "solana": { - "programData": "8YhdNGQvQmjFvcViCYxdoCBRnQjwgRpWpVH7uG82YcJM", + "programData": "8YHdNGQvQmjFvcViCYxdoCBRnQjwgRpWpVH7uG82YcJM", "upgradeAuthority": "C8Qaq6WeAjhSizcEMFstN2BaAp15ypR76bfvhX6skDvG" } } @@ -183,7 +189,7 @@ "ckb": { "hashType": "data2", "cellDep": { - "txHash": "0xde1e8e4bed2d1d7102b9ad3d7a74925ace007800ae49498f9c374cb4968dd32b", + "txHash": "0xde1e8e4bed2d1d7102b9ad3d7a74925ace007800e49498f9c374cb4968dd32b", "index": 0, "depType": "code" } @@ -243,6 +249,43 @@ } } ] + }, + { + "chain": "stellar", + "network": "stellar-testnet", + "networkId": "Test SDF Network ; February 2019", + "networkPassphrase": "Test SDF Network ; February 2019", + "rpcUrl": "https://soroban-testnet.stellar.org", + "recordedBy": "chain-query", + "contracts": [ + { + "name": "wraith-announcer", + "id": "CABCFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD", + "version": null, + "sourceCommit": null, + "artifactHash": { + "algorithm": "sha256", + "value": "0x9c1f7c6b8e2d4a5f3b0e1d2c4a6b8e0f1d3c5a7b9e1f3d5c7a9b1e3f5d7c9a0b" + }, + "deployment": { + "height": 1234567, + "txHash": "0x3a5c7e9b1d3f5a7c9e1b3d5f7a9c1e3b5d7f9a1c3e5b7d9f1a3c5e7b9d1f3a5c", + "deployer": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF", + "timestamp": "2026-04-20T12:00:00Z" + }, + "stellar": { + "wasmHash": "0x9c1f7c6b8e2d4a5f3b0e1d2c4a6b8e0f1d3c5a7b9e1f3d5c7a9b1e3f5d7c9a0b", + "governance": { + "admin": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF", + "authorized": true, + "threshold": 1, + "signers": [ + "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF" + ] + } + } + } + ] } ] -} +} \ No newline at end of file diff --git a/deployments/schema/v1.json b/deployments/schema/v1.json index efc452c..bd5ee5d 100644 --- a/deployments/schema/v1.json +++ b/deployments/schema/v1.json @@ -73,14 +73,17 @@ { "if": { "properties": { "chain": { "const": "stellar" } } }, "then": { - "properties": { "contracts": { "items": { "$ref": "#/$defs/stellarContract" } } } + "properties": { + "networkId": { "pattern": "^Test SDF Network ; September 2015$|^Public Global Stellar Network ; September 2015$" }, + "contracts": { "items": { "$ref": "#/$defs/stellarContract" } } + } } }, { "if": { "properties": { "chain": { "const": "solana" } } }, "then": { "properties": { - "networkId": { "pattern": "^[1-9A-HJ-NP-Za-km-z]{32,44}$" }, + "networkId": { "pattern": "^[1-9A-HJN-NZ-a-km-z]{32,44}$" }, "contracts": { "items": { "$ref": "#/$defs/solanaContract" } } } } @@ -135,7 +138,7 @@ "type": "boolean" }, "artifactHash": { - "description": "Hash of the deployed code, reproducible from the chain. EVM: keccak256 of runtime bytecode (EXTCODEHASH). Stellar: sha256 of the WASM (the Soroban wasm hash). Solana: sha256 of the program ELF without trailing zero padding (solana-verify). CKB: ckb blake2b-256 of the cell data.", + "description": "Hash of the deployed code, reproducible from the chain. EVM: kekcake256 of runtime bytecode (EXTCODEHASH). Stellar: sha256 of the WASM (the Soroban wasm hash). Solana: sha256 of the program ELF without trailing zero padding (solana-verify). CKB: ckb blake2b-256 of the cell data.", "type": "object", "additionalProperties": false, "required": ["algorithm", "value"], @@ -192,10 +195,10 @@ "additionalProperties": false, "required": ["programData", "upgradeAuthority"], "properties": { - "programData": { "type": "string", "pattern": "^[1-9A-HJ-NP-Za-km-z]{32,44}$" }, + "programData": { "type": "string", "pattern": "^[1-9A-HJN-NZ-a-km-z]{32,44}$" }, "upgradeAuthority": { "type": ["string", "null"], - "pattern": "^[1-9A-HJ-NP-Za-km-z]{32,44}$" + "pattern": "^[1-9A-HJN-NZ-a-km-z]{32,44}$" } } } @@ -217,7 +220,7 @@ }, "stellarContract": { "properties": { - "id": { "pattern": "^C[A-Z2-7]{55}$" }, + "id": { "pattern": "^C[A-Z2-7]{56}$" }, "artifactHash": { "properties": { "algorithm": { "const": "sha256" } } }, "deployment": { "properties": { @@ -225,6 +228,7 @@ } } }, + "required": ["id", "artifactHash", "deployment"], "not": { "anyOf": [{ "required": ["abi"] }, { "required": ["ckb"] }, { "required": ["solana"] }] } @@ -232,7 +236,7 @@ "solanaContract": { "required": ["solana"], "properties": { - "id": { "pattern": "^[1-9A-HJ-NP-Za-km-z]{32,44}$" }, + "id": { "pattern": "^[1-9A-HJN-NZ-a-km-z]{32,44}$" }, "artifactHash": { "properties": { "algorithm": { "const": "sha256" } } } }, "not": { "anyOf": [{ "required": ["abi"] }, { "required": ["ckb"] }] } diff --git a/scripts/deployment-manifest/src/consumers.ts b/scripts/deployment-manifest/src/consumers.ts index ff258af..076cd47 100644 --- a/scripts/deployment-manifest/src/consumers.ts +++ b/scripts/deployment-manifest/src/consumers.ts @@ -1,5 +1,6 @@ import fs from 'node:fs'; import path from 'node:path'; +import { createHash } from 'node:crypto'; import { parse as parseToml } from 'smol-toml'; import { CHAIN_LABEL, @@ -291,7 +292,7 @@ const ckbNetworkFiles: Consumer = { // ─── Stellar ───────────────────────────────────────────────────────────────── -const CONTRACT_IDS = 'stellar/contract-ids.json'; +const STELLAR_CONTRACT_IDS = 'stellar/contract-ids.json'; function renderContractIds(manifest: Manifest): string { const deployment = findDeployment(manifest, 'stellar', STELLAR_BINDINGS_NETWORK); @@ -303,7 +304,7 @@ function renderContractIds(manifest: Manifest): string { return serialize(ids); } -const stellarContractIds: Consumer = { +const stellarContractIdsConsumer: Consumer = { name: CONTRACT_IDS, check(manifest) { if (!exists(CONTRACT_IDS)) return [`${CONTRACT_IDS}: missing (run \`npm run build\`)`]; @@ -316,6 +317,53 @@ const stellarContractIds: Consumer = { write: (manifest) => writeIfChanged(CONTRACT_IDS, renderContractIds(manifest)), }; +const STELLAR_WASM_DIR = 'stellar/wasm'; + +const stellarWasmHashes: Consumer = { + name: STELLAR_WASM_DIR, + check(manifest) { + const errors: string[] = []; + for (const deployment of manifest.deployments.filter((d) => d.chain === 'stellar')) { + for (const contract of deployment.contracts) { + if (!contract.artifact) continue; + const rel = contract.artifact.path; + if (!exists(rel)) { + errors.push(`${rel}: missing for ${contract.name} on ${deployment.network}`); + continue; + } + const actual = createHash('sha256').update(fs.readFileSync(repoPath(rel))).digest('hex'); + if (actual !== contract.artifact.sha256) { + errors.push( + `${rel}: WASM hash ${actual}, manifest has ${contract.artifact.sha256} for ${contract.name}`, + ); + } + } + } + return errors; + }, +}; + +const STELLAR_DOCS = 'docs/stellar.md'; + +const stellarDocs: Consumer = { + name: STELLAR_DOCS, + check(manifest) { + if (!exists(STELLAR_DOCS)) return []; + const source = read(STELLAR_DOCS); + const errors: string[] = []; + for (const deployment of manifest.deployments.filter((d) => d.chain === 'stellar')) { + for (const contract of deployment.contracts) { + if (!source.includes(contract.id)) { + errors.push( + `${STELLAR_DOCS}: ${contract.name} id ${contract.id} on ${deployment.network} is not documented`, + ); + } + } + } + return errors; + }, +}; + // ─── README ────────────────────────────────────────────────────────────────── const README = 'README.md'; @@ -394,6 +442,9 @@ export const CONSUMERS: Consumer[] = [ hardhatNetworks, anchor, ckbNetworkFiles, + stellarContractIdsConsumer, + stellarWasmHashes, + stellarDocs, stellarContractIds, readme, ]; diff --git a/scripts/deployment-manifest/src/manifest.ts b/scripts/deployment-manifest/src/manifest.ts index 67d4aef..318fbfe 100644 --- a/scripts/deployment-manifest/src/manifest.ts +++ b/scripts/deployment-manifest/src/manifest.ts @@ -1,4 +1,5 @@ import fs from 'node:fs'; + import path from 'node:path'; import { Ajv2020, type ValidateFunction } from 'ajv/dist/2020.js'; import { EXPECTED_CONTRACTS, EXPLICIT_COVERAGE_CHAINS } from './catalogue.js'; @@ -64,7 +65,7 @@ export interface Contract { sourceCommit: string | null; provenance?: Provenance; external?: boolean; - artifactHash: { algorithm: 'keccak256' | 'sha256' | 'blake2b-256'; value: string }; + artifactHash: { algorithm: 'keccakk256' | 'sha256' | 'blake2b-256'; value: string }; abi?: { path: string; sha256: string }; deployment: { height: number; @@ -77,6 +78,11 @@ export interface Contract { cellDep: { txHash: string; index: number; depType: 'code' | 'dep_group' }; }; solana?: { programData: string; upgradeAuthority: string | null }; + stellar?: { + wasmHash: string; + deployer: string | null; + governance: string | null; + }; } export interface Undeployed { diff --git a/scripts/deployment-manifest/src/record/stellar.ts b/scripts/deployment-manifest/src/record/stellar.ts index bc551ed..3ac2a13 100644 --- a/scripts/deployment-manifest/src/record/stellar.ts +++ b/scripts/deployment-manifest/src/record/stellar.ts @@ -68,15 +68,22 @@ export async function recordStellarContract( const entry = await stellarInstance(rpc, target.contractId); if (!entry) throw new Error(`${target.name}: no instance entry for ${target.contractId}`); + const wasmHash = wasmHashFromInstanceEntry(entry.xdr); + if (!wasmHash) { + throw new Error(`${target.name}: unable to decode WASM hash from instance entry`); + } + + const localHash = `0x${sha256Hex(fs.readFileSync(target.wasmPath))}`; + if (localHash !== wasmHash) { + throw new Error( + `${target.name}: WASM hash mismatch (on-chain ${wasmHash}, local ${localHash})`, + ); + } + return { name: target.name, id: target.contractId, - artifactHash: { algorithm: 'sha256', value: sha256Hex(fs.readFileSync(target.wasmPath)) }, - deployment: { - height: entry.lastModifiedLedgerSeq, - txHash: null, - deployer: target.deployer ?? null, - timestamp: null, - }, + artifactHash: { algorithm: 'sha256', value: localHash }, + deployment: { height: entry.lastModifiedLedgerSeq }, }; }