diff --git a/deployments/manifest.json b/deployments/manifest.json index 471cc3b5..ad332c10 100644 --- a/deployments/manifest.json +++ b/deployments/manifest.json @@ -154,6 +154,24 @@ } ] }, + { + "chain": "stellar", + "network": "stellar-testnet", + "networkId": "Test SDF Network ; September 2015", + "rpcUrl": "https://soroban-testnet.stellar.org", + "recordedBy": "chain-query", + "contracts": [], + "undeployed": [ + { + "name": "WraithSender", + "reason": "Stellar testnet contract ID, WASM hash, deployment ledger, and governance fields have not been recorded in this manifest yet; requires verified SDK/docs source data before entry." + } + ], + "verification": { + "status": "pending", + "reason": "Manifest verification must compare Stellar IDs, WASM hashes, deployment ledgers, and governance fields against SDK and docs before this record is accepted." + } + }, { "chain": "ckb", "network": "ckb-testnet", diff --git a/deployments/schema/v1.json b/deployments/schema/v1.json index efc452c6..8def1f29 100644 --- a/deployments/schema/v1.json +++ b/deployments/schema/v1.json @@ -73,7 +73,10 @@ { "if": { "properties": { "chain": { "const": "stellar" } } }, "then": { - "properties": { "contracts": { "items": { "$ref": "#/$defs/stellarContract" } } } + "properties": { + "networkId": { "minLength": 1 }, + "contracts": { "items": { "$ref": "#/$defs/stellarContract" } } + } } }, { diff --git a/scripts/deployment-manifest/src/consumers.ts b/scripts/deployment-manifest/src/consumers.ts index ff258af5..af09844e 100644 --- a/scripts/deployment-manifest/src/consumers.ts +++ b/scripts/deployment-manifest/src/consumers.ts @@ -307,6 +307,12 @@ const stellarContractIds: Consumer = { name: CONTRACT_IDS, check(manifest) { if (!exists(CONTRACT_IDS)) return [`${CONTRACT_IDS}: missing (run \`npm run build\`)`]; + const deployment = findDeployment(manifest, 'stellar', STELLAR_BINDINGS_NETWORK); + if (!deployment) return [`${CONTRACT_IDS}: no ${STELLAR_BINDINGS_NETWORK} deployment in the manifest`]; + const empty = EXPECTED_CONTRACTS.stellar.filter( + (n) => !deployment.contracts.some((c) => c.name === n && c.id), + ); + if (empty.length) return [`${CONTRACT_IDS}: manifest is missing IDs for ${empty.join(', ')}`]; return read(CONTRACT_IDS).trimEnd() === renderContractIds(manifest).trimEnd() ? [] : [ diff --git a/scripts/deployment-manifest/src/manifest.ts b/scripts/deployment-manifest/src/manifest.ts index 67d4aefc..0fe4cf94 100644 --- a/scripts/deployment-manifest/src/manifest.ts +++ b/scripts/deployment-manifest/src/manifest.ts @@ -77,6 +77,7 @@ export interface Contract { cellDep: { txHash: string; index: number; depType: 'code' | 'dep_group' }; }; solana?: { programData: string; upgradeAuthority: string | null }; + stellar?: { wasmHash: string; governance: { admin: string; upgradeable: boolean } }; } export interface Undeployed { diff --git a/scripts/deployment-manifest/src/record/stellar.ts b/scripts/deployment-manifest/src/record/stellar.ts index bc551ed3..14659393 100644 --- a/scripts/deployment-manifest/src/record/stellar.ts +++ b/scripts/deployment-manifest/src/record/stellar.ts @@ -10,6 +10,7 @@ const LEDGER_ENTRY_CONTRACT_DATA = 6; const SC_ADDRESS_CONTRACT = 1; const SCV_LEDGER_KEY_CONTRACT_INSTANCE = 20; const DURABILITY_PERSISTENT = 1; +const CONTRACT_EXECUTABLE_WASM = 0; const i32be = (n: number) => Uint8Array.of((n >>> 24) & 0xff, (n >> 16) & 0xff, (n >> 8) & 0xff, n & 0xff); @@ -33,7 +34,7 @@ export function wasmHashFromInstanceEntry(xdrBase64: string): string | null { // type(4) ext(4) SCAddress(4+32) key(4) durability(4) SCVal type(4) executable type(4) hash(32) if (bytes.length < 92) return null; const executableType = (bytes[56] << 24) | (bytes[57] << 16) | (bytes[58] << 8) | bytes[59]; - if (executableType !== 0) return null; + if (executableType !== CONTRACT_EXECUTABLE_WASM) return null; return `0x${Buffer.from(bytes.subarray(60, 92)).toString('hex')}`; } @@ -68,10 +69,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 onChainWasmHash = wasmHashFromInstanceEntry(entry.xdr); + if (!onChainWasmHash) { + throw new Error(`${target.name}: instance entry is not a WASM executable`); + } + + const localWasmHash = `0x${sha256Hex(fs.readFileSync(target.wasmPath))}`; + if (onChainWasmHash.toLowerCase() !== localWasmHash.toLowerCase()) { + throw new Error( + `${target.name}: on-chain WASM hash ${onChainWasmHash} does not match local artifact ${localWasmHash}`, + ); + } + return { name: target.name, id: target.contractId, - artifactHash: { algorithm: 'sha256', value: sha256Hex(fs.readFileSync(target.wasmPath)) }, + artifactHash: { algorithm: 'sha256', value: localWasmHash }, deployment: { height: entry.lastModifiedLedgerSeq, txHash: null, diff --git a/stellar/contract-ids.json b/stellar/contract-ids.json index 23ecad5d..fe3ba918 100644 --- a/stellar/contract-ids.json +++ b/stellar/contract-ids.json @@ -1,6 +1,6 @@ { - "stealth-announcer": "", - "stealth-registry": "", - "stealth-sender": "", - "wraith-names": "" + "stealth-announcer": "CONTRACT_ID_REQUIRED", + "stealth-registry": "CONTRACT_ID_REQUIRED", + "stealth-sender": "CONTRACT_ID_REQUIRED", + "wraith-names": "CONTRACT_ID_REQUIRED" }