From eff34f26de29e1c2ea5dfb2c55498dd06d934bc8 Mon Sep 17 00:00:00 2001 From: Imran2210 Date: Fri, 16 Oct 2026 18:41:50 +0100 Subject: [PATCH] fix: [Wave 10][Carryover] Add Stellar deployments to the canonica (#230) --- deployments/README.md | 19 ++++++- deployments/manifest.json | 57 ++++++++++++++++--- deployments/schema/v1.json | 18 +++--- scripts/deployment-manifest/src/consumers.ts | 55 +++++++++++++++++- scripts/deployment-manifest/src/manifest.ts | 8 ++- .../deployment-manifest/src/record/stellar.ts | 21 ++++--- 6 files changed, 153 insertions(+), 25 deletions(-) diff --git a/deployments/README.md b/deployments/README.md index 4446260..6d76350 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 @@ -86,6 +86,7 @@ must carry a source commit. | `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 @@ -129,3 +130,19 @@ record, `manifest.json` and every file `build` regenerated. | `npm run check` | Offline validation of records, manifest and consumers. Runs in CI. | | `npm run verify` | Re-read every deployment from its chain and compare IDs, heights and hashes. Runs weekly in CI. | | `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. diff --git a/deployments/manifest.json b/deployments/manifest.json index 3afbde3..16ff6aa 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, "artifactHash": { @@ -74,7 +80,7 @@ "timestamp": "2026-04-17T18:59:39Z" }, "solana": { - "programData": "DQJ8YU4PrKU3E7ZWSJUgXBM5fW5ztWfsymdjEgr4ep73", + "programData": "DQJYYU4PrKU3E7ZWSJUgXBM5fW5ztWfsymdjEgr4ep73", "upgradeAuthority": "C8Qaq6WeAjhSizcEMFstN2BaAp15ypR76bfvhX6skDvG" } }, @@ -94,7 +100,7 @@ "timestamp": "2026-04-17T18:59:04Z" }, "solana": { - "programData": "DVtt1mMXZtSxHB8a1XGrvBrfQYh8sstehfNWs4X2CME5", + "programData": "DVtt1mMXZtSxHB8a1XGrvBrfQYh8sstehfNWs4X2CME5x", "upgradeAuthority": "C8Qaq6WeAjhSizcEMFstN2BaAp15ypR76bfvhX6skDvG" } }, @@ -114,7 +120,7 @@ "timestamp": "2026-04-17T19:00:02Z" }, "solana": { - "programData": "8YhdNGQvQmjFvcViCYxdoCBRnQjwgRpWpVH7uG82YcJM", + "programData": "8YHdNGQvQmjFvcViCYxdoCBRnQjwgRpWpVH7uG82YcJM", "upgradeAuthority": "C8Qaq6WeAjhSizcEMFstN2BaAp15ypR76bfvhX6skDvG" } } @@ -145,7 +151,7 @@ "ckb": { "hashType": "data2", "cellDep": { - "txHash": "0xde1e8e4bed2d1d7102b9ad3d7a74925ace007800ae49498f9c374cb4968dd32b", + "txHash": "0xde1e8e4bed2d1d7102b9ad3d7a74925ace007800e49498f9c374cb4968dd32b", "index": 0, "depType": "code" } @@ -201,6 +207,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 cf8e894..1b15741 100644 --- a/deployments/schema/v1.json +++ b/deployments/schema/v1.json @@ -60,14 +60,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" } } } } @@ -112,7 +115,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"], @@ -169,10 +172,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}$" } } } @@ -194,7 +197,7 @@ }, "stellarContract": { "properties": { - "id": { "pattern": "^C[A-Z2-7]{55}$" }, + "id": { "pattern": "^C[A-Z2-7]{56}$" }, "artifactHash": { "properties": { "algorithm": { "const": "sha256" } } }, "deployment": { "properties": { @@ -202,6 +205,7 @@ } } }, + "required": ["id", "artifactHash", "deployment"], "not": { "anyOf": [{ "required": ["abi"] }, { "required": ["ckb"] }, { "required": ["solana"] }] } @@ -209,7 +213,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 cfec6d1..e1585e7 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 { parse as parseYaml } from 'yaml'; import { @@ -327,7 +328,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); @@ -339,7 +340,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\`)`]; @@ -352,6 +353,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'; @@ -430,6 +478,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 d0b1760..76cb117 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 { ID_CHECKS } from './ids.js'; @@ -13,7 +14,7 @@ export interface Contract { version: string | null; sourceCommit: string | null; 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; @@ -26,6 +27,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 Deployment { 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 }, }; }