diff --git a/.github/actions/setup-rust-stellar/action.yml b/.github/actions/setup-rust-stellar/action.yml index 05f37c8..ed6ef66 100644 --- a/.github/actions/setup-rust-stellar/action.yml +++ b/.github/actions/setup-rust-stellar/action.yml @@ -7,9 +7,9 @@ runs: uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - name: Install Rust - uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # stable (rust 1.97.1) + uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c with: - toolchain: stable + toolchain: 1.93.0 targets: wasm32v1-none - name: Add wasm32v1-none target diff --git a/.github/workflows/rustfmt.yml b/.github/workflows/rustfmt.yml index 89cd95d..e0ff669 100644 --- a/.github/workflows/rustfmt.yml +++ b/.github/workflows/rustfmt.yml @@ -27,9 +27,9 @@ jobs: persist-credentials: false - name: Install Rust - uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # stable (rust 1.97.1) + uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c with: - toolchain: stable + toolchain: 1.93.0 components: rustfmt - name: Run rustfmt diff --git a/.github/workflows/verify-build.yml b/.github/workflows/verify-build.yml index 3db186d..48fcb7b 100644 --- a/.github/workflows/verify-build.yml +++ b/.github/workflows/verify-build.yml @@ -11,6 +11,8 @@ on: - 'Cargo.toml' - 'Cargo.lock' - 'rust-toolchain.toml' + - 'deploy_mainnet.sh' + - 'scripts/**' - '.github/actions/setup-rust-stellar/**' - '.github/workflows/verify-build.yml' # Use pull_request (not pull_request_target): the check must run the workflow @@ -28,6 +30,8 @@ on: - 'Cargo.toml' - 'Cargo.lock' - 'rust-toolchain.toml' + - 'deploy_mainnet.sh' + - 'scripts/**' - '.github/actions/setup-rust-stellar/**' - '.github/workflows/verify-build.yml' - '.github/workflows/rustfmt.yml' @@ -49,11 +53,19 @@ jobs: - name: Setup Rust and Stellar uses: ./.github/actions/setup-rust-stellar - - name: Build events contract - run: cd contracts/events && make build + - name: Check release script syntax + run: | + bash -n deploy_mainnet.sh + bash -n scripts/capture-mainnet-compat-snapshot.sh + bash -n scripts/test-sdk27-compat.sh + bash -n scripts/test-mainnet-upgrade-guards.sh + bash -n scripts/testdata/mock-stellar + + - name: Verify mainnet upgrade guards + run: ./scripts/test-mainnet-upgrade-guards.sh - - name: Build profile contract - run: cd contracts/profile && make build + - name: Build contracts reproducibly + run: stellar contract build --locked - name: Check WASM sizes (64 KB ceiling) run: | @@ -67,5 +79,10 @@ jobs: fi done + - name: Verify SDK 23 to SDK 27 storage compatibility + env: + VERIFY_SDK27_BUILD: "1" + run: ./scripts/test-sdk27-compat.sh + - name: Run tests run: cargo test --release diff --git a/BACKLOG.md b/BACKLOG.md index 3ca3188..a077ef9 100644 --- a/BACKLOG.md +++ b/BACKLOG.md @@ -8,6 +8,7 @@ Legend: `[ ]` open, `[~]` in flight, `[x]` done. ## P0 (blocking mainnet) +- [x] **SDK 23 to SDK 27 storage compatibility.** Exact mainnet and synthetic SDK 23 state upgrades through H6 to pinned SDK 27 artifacts. Tests cover every persisted layout; CI pins fixtures and snapshots; mainnet tooling verifies versions, admins, bindings, pause state, queued hashes, and live executable hashes. - [ ] Third-party security audit (events + profile). Cost ~$30-80k for a Soroban-experienced firm. Schedule blocker for the mainnet date. - [x] Non-USDC token end-to-end smoke. 2026-06-05: registered the testnet native XLM SAC (`CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC`) on the new events contract; ran `scripts/smoke/contract-non-usdc-token.ts`. Bounty published in XLM, alice applied + submitted + won + got paid; payout tx `a6c8daad…`. Smoke updated to tolerate native-asset tx fees (~143k stroops / 0.014 XLM for 3 alice-signed txs); for non-native tokens the assertion still demands exact-budget delta. - [ ] Mainnet admin multi-sig provisioned per `docs/admin-custody-policy.md` (3 signers, 2-of-3). 2026-06-05: launch baseline updated to software multi-sig (Freighter on 3 isolated machines) with a hardware-upgrade trigger codified in `admin-custody-policy.md` §10 (TVL $250k OR daily volume $50k/day for 7 days OR first signer-machine incident → mandatory Yubikey upgrade). Pre-flight gate codified for both paths — see `docs/multisig-preflight.md` (Section 1.A hardware, Section 1.B Freighter; everything else identical) and `scripts/admin/verify-multisig.sh` (chain-state gate, hardware-agnostic). The verify script was sanity-tested against a single-sig account and correctly fails with the per-check breakdown. @@ -58,7 +59,7 @@ See `docs/audit-2026-06-stellar-skill.md` for full findings. ## Done -- [x] 2026-07-17 — Cancellation liveness hardening. `add_funds` now maintains an O(1) per-event non-owner contribution total, `start_cancel` no longer scans up to 5,000 contributor rows, and `process_cancel_batch` / `finalize_cancel` are permissionless after cancellation starts. The guarded 1.1.0 → 1.2.0 mainnet flow pauses before proposal, proves the zero-event state, keeps it frozen through the timelock, and rechecks before apply. Older zero-contributor rows initialize the missing total lazily; a missing total with existing contributors fails closed. Tests cover a 220-contributor constant-footprint start and exact third-party-driven payout deltas. +- [x] 2026-07-17 — Cancellation liveness hardening. `add_funds` now maintains an O(1) per-event non-owner contribution total, `start_cancel` no longer scans up to 5,000 contributor rows, and `process_cancel_batch` / `finalize_cancel` are permissionless after cancellation starts. The original guarded events 1.1.0 → 1.2.0 plan introduced the pause and zero-event checks; the current coordinated SDK 27 flow carries those checks forward to events 1.5.0 and profile 1.2.0. Older zero-contributor rows initialize the missing total lazily; a missing total with existing contributors fails closed. Tests cover a 220-contributor constant-footprint start and exact third-party-driven payout deltas. - [x] 2026-06-03 — `fee_bps_override` per-event field + `effective_fee_bps` resolver. - [x] 2026-06-03 — `WinnersAlreadySelected` replay lock on `select_winners`. - [x] 2026-06-03 — Grant last-milestone sweep (G4). diff --git a/Cargo.lock b/Cargo.lock index 6623f3e..510c369 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -228,6 +228,13 @@ dependencies = [ "soroban-sdk", ] +[[package]] +name = "boundless-storage-compatibility" +version = "0.1.0" +dependencies = [ + "soroban-sdk", +] + [[package]] name = "bumpalo" version = "3.19.0" diff --git a/contracts/compatibility/Cargo.toml b/contracts/compatibility/Cargo.toml new file mode 100644 index 0000000..8920cbc --- /dev/null +++ b/contracts/compatibility/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "boundless-storage-compatibility" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +doctest = false + +[dependencies] +soroban-sdk = { workspace = true, features = ["testutils"] } diff --git a/contracts/compatibility/fixtures/events-1.3.0-sdk23.wasm b/contracts/compatibility/fixtures/events-1.3.0-sdk23.wasm new file mode 100644 index 0000000..b389736 Binary files /dev/null and b/contracts/compatibility/fixtures/events-1.3.0-sdk23.wasm differ diff --git a/contracts/compatibility/fixtures/events-1.5.0-sdk27.wasm b/contracts/compatibility/fixtures/events-1.5.0-sdk27.wasm new file mode 100644 index 0000000..f8453b6 Binary files /dev/null and b/contracts/compatibility/fixtures/events-1.5.0-sdk27.wasm differ diff --git a/contracts/compatibility/fixtures/mainnet-events-1.1.0-sdk23.wasm b/contracts/compatibility/fixtures/mainnet-events-1.1.0-sdk23.wasm new file mode 100644 index 0000000..0087d08 Binary files /dev/null and b/contracts/compatibility/fixtures/mainnet-events-1.1.0-sdk23.wasm differ diff --git a/contracts/compatibility/fixtures/mainnet-profile-1.1.0-sdk23.wasm b/contracts/compatibility/fixtures/mainnet-profile-1.1.0-sdk23.wasm new file mode 100644 index 0000000..c310961 Binary files /dev/null and b/contracts/compatibility/fixtures/mainnet-profile-1.1.0-sdk23.wasm differ diff --git a/contracts/compatibility/fixtures/mainnet-state-63617727.json b/contracts/compatibility/fixtures/mainnet-state-63617727.json new file mode 100644 index 0000000..f9f6b92 --- /dev/null +++ b/contracts/compatibility/fixtures/mainnet-state-63617727.json @@ -0,0 +1,619 @@ +{ + "protocol_version": 27, + "sequence_number": 63617727, + "timestamp": 1784850182, + "network_id": "7ac33997544e3175d266bd022439b22cdb16508c01163f26e5cb2a3e1045a979", + "base_reserve": 5000000, + "min_persistent_entry_ttl": 2073600, + "min_temp_entry_ttl": 17280, + "max_entry_ttl": 3110400, + "ledger_entries": [ + { + "entry": { + "last_modified_ledger_seq": 63315871, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CCFVEGOQJEM47LRAJU2LHEK4KTL5VYN7AOGZ2HH2GNHAMXTILNMMJGQZ", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "2a8789bf5dd392b91ddfe6591072a0cb9fae70045abf379fd1dbbc2db4af1de3" + }, + "storage": [ + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "GCVK72I6TVJVDTTY4UKU6MQT4QJ2T2AAG3NULNEUDM46L3UOQYDSO4O2" + } + }, + { + "key": { + "vec": [ + { + "symbol": "DeploymentSeq" + } + ] + }, + "val": { + "u32": 63222823 + } + }, + { + "key": { + "vec": [ + { + "symbol": "FeeAccount" + } + ] + }, + "val": { + "address": "GADSIP2HPINWTMEUNMVYA5NJRL372OV52HDEJTRTLDZXMHADIPQNYH55" + } + }, + { + "key": { + "vec": [ + { + "symbol": "FeeBps" + } + ] + }, + "val": { + "u32": 250 + } + }, + { + "key": { + "vec": [ + { + "symbol": "Paused" + } + ] + }, + "val": { + "bool": false + } + }, + { + "key": { + "vec": [ + { + "symbol": "ProfileContract" + } + ] + }, + "val": { + "address": "CD3KH4OE7HDHHHUYFX3U4L7NLIILMXAY6HM5FEH2UH6UBOKX4HDNE3PC" + } + }, + { + "key": { + "vec": [ + { + "symbol": "SupportedToken" + }, + { + "address": "CAS3J7GYLGXMF6TDJBBYYSE3HQ6BBSMLNUQ34T6TZMYMW2EVH34XOWMA" + } + ] + }, + "val": { + "bool": true + } + }, + { + "key": { + "vec": [ + { + "symbol": "SupportedToken" + }, + { + "address": "CCW67TSZV3SSS2HXMBQ5JFGCKJNXKZM7UQUWUZPUTHXSTZLEO7SJMI75" + } + ] + }, + "val": { + "bool": true + } + }, + { + "key": { + "vec": [ + { + "symbol": "SupportedTokenAt" + }, + { + "u32": 0 + } + ] + }, + "val": { + "address": "CCW67TSZV3SSS2HXMBQ5JFGCKJNXKZM7UQUWUZPUTHXSTZLEO7SJMI75" + } + }, + { + "key": { + "vec": [ + { + "symbol": "SupportedTokenAt" + }, + { + "u32": 1 + } + ] + }, + "val": { + "address": "CAS3J7GYLGXMF6TDJBBYYSE3HQ6BBSMLNUQ34T6TZMYMW2EVH34XOWMA" + } + }, + { + "key": { + "vec": [ + { + "symbol": "SupportedTokenCount" + } + ] + }, + "val": { + "u32": 2 + } + }, + { + "key": { + "vec": [ + { + "symbol": "SupportedTokenSlot" + }, + { + "address": "CAS3J7GYLGXMF6TDJBBYYSE3HQ6BBSMLNUQ34T6TZMYMW2EVH34XOWMA" + } + ] + }, + "val": { + "u32": 2 + } + }, + { + "key": { + "vec": [ + { + "symbol": "SupportedTokenSlot" + }, + { + "address": "CCW67TSZV3SSS2HXMBQ5JFGCKJNXKZM7UQUWUZPUTHXSTZLEO7SJMI75" + } + ] + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "vec": [ + { + "symbol": "Version" + } + ] + }, + "val": { + "string": "1.1.0" + } + } + ] + } + } + } + }, + "ext": "v0" + }, + "live_until": 4294967295 + }, + { + "entry": { + "last_modified_ledger_seq": 63315890, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CD3KH4OE7HDHHHUYFX3U4L7NLIILMXAY6HM5FEH2UH6UBOKX4HDNE3PC", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "b9e3500cfb559781e68359655f37775c78afffc5a8436daca9d1d1f8e3569b02" + }, + "storage": [ + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "GCVK72I6TVJVDTTY4UKU6MQT4QJ2T2AAG3NULNEUDM46L3UOQYDSO4O2" + } + }, + { + "key": { + "vec": [ + { + "symbol": "DefaultBootstrapCredits" + } + ] + }, + "val": { + "u32": 10 + } + }, + { + "key": { + "vec": [ + { + "symbol": "DeploymentSeq" + } + ] + }, + "val": { + "u32": 63222664 + } + }, + { + "key": { + "vec": [ + { + "symbol": "EventsContract" + } + ] + }, + "val": { + "address": "CCFVEGOQJEM47LRAJU2LHEK4KTL5VYN7AOGZ2HH2GNHAMXTILNMMJGQZ" + } + }, + { + "key": { + "vec": [ + { + "symbol": "Paused" + } + ] + }, + "val": { + "bool": false + } + }, + { + "key": { + "vec": [ + { + "symbol": "Version" + } + ] + }, + "val": { + "string": "1.1.0" + } + } + ] + } + } + } + }, + "ext": "v0" + }, + "live_until": 4294967295 + }, + { + "entry": { + "last_modified_ledger_seq": 63585475, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CD3KH4OE7HDHHHUYFX3U4L7NLIILMXAY6HM5FEH2UH6UBOKX4HDNE3PC", + "key": { + "vec": [ + { + "symbol": "OpSeen" + }, + { + "bytes": "22f9dff5b7184344e5bac394f9fb141af5b1da8e71ae36e7612fb1aa68f2f1bc" + } + ] + }, + "durability": "temporary", + "val": { + "bool": true + } + } + }, + "ext": "v0" + }, + "live_until": 4294967295 + }, + { + "entry": { + "last_modified_ledger_seq": 63597602, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CD3KH4OE7HDHHHUYFX3U4L7NLIILMXAY6HM5FEH2UH6UBOKX4HDNE3PC", + "key": { + "vec": [ + { + "symbol": "OpSeen" + }, + { + "bytes": "d01286fa654b4f924eb2a165e3d8386142917b07af9a22f222420ba8d2b01ad8" + } + ] + }, + "durability": "temporary", + "val": { + "bool": true + } + } + }, + "ext": "v0" + }, + "live_until": 4294967295 + }, + { + "entry": { + "last_modified_ledger_seq": 63593387, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CD3KH4OE7HDHHHUYFX3U4L7NLIILMXAY6HM5FEH2UH6UBOKX4HDNE3PC", + "key": { + "vec": [ + { + "symbol": "OpSeen" + }, + { + "bytes": "d456ce7dbe62e2f5949dc2cce2b6a99900ce9ed08ee44237219af22755fa71d5" + } + ] + }, + "durability": "temporary", + "val": { + "bool": true + } + } + }, + "ext": "v0" + }, + "live_until": 4294967295 + }, + { + "entry": { + "last_modified_ledger_seq": 63585475, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CD3KH4OE7HDHHHUYFX3U4L7NLIILMXAY6HM5FEH2UH6UBOKX4HDNE3PC", + "key": { + "vec": [ + { + "symbol": "Profile" + }, + { + "address": "GAHGDV4MWDLT3BXOUT5A2T774IUAE54DZ6BUCBH4O7PG5TJGWXSV3OJO" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "bootstrapped_at" + }, + "val": { + "u64": "1784668845" + } + }, + { + "key": { + "symbol": "reputation" + }, + "val": { + "u64": "0" + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4294967295 + }, + { + "entry": { + "last_modified_ledger_seq": 63593387, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CD3KH4OE7HDHHHUYFX3U4L7NLIILMXAY6HM5FEH2UH6UBOKX4HDNE3PC", + "key": { + "vec": [ + { + "symbol": "Profile" + }, + { + "address": "GBVQKBWHZGO6YED3EVXOZW76VPNYUMJLBLKE5IN4XE5LTT262UXCBIR7" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "bootstrapped_at" + }, + "val": { + "u64": "1784713278" + } + }, + { + "key": { + "symbol": "reputation" + }, + "val": { + "u64": "0" + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4294967295 + }, + { + "entry": { + "last_modified_ledger_seq": 63539164, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CD3KH4OE7HDHHHUYFX3U4L7NLIILMXAY6HM5FEH2UH6UBOKX4HDNE3PC", + "key": { + "vec": [ + { + "symbol": "Profile" + }, + { + "address": "GCCCPDSWYDVTD2VSXNXDKWWNENSRTA637CHVHIIXISTKSK4FV6T7G3HV" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "bootstrapped_at" + }, + "val": { + "u64": "1784408398" + } + }, + { + "key": { + "symbol": "reputation" + }, + "val": { + "u64": "0" + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4294967295 + }, + { + "entry": { + "last_modified_ledger_seq": 63519879, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CD3KH4OE7HDHHHUYFX3U4L7NLIILMXAY6HM5FEH2UH6UBOKX4HDNE3PC", + "key": { + "vec": [ + { + "symbol": "Profile" + }, + { + "address": "GCS5XWO3M72B6PSMZVGOSB3CLQVWB5OJ6Z7FTFAP5FAZVWS3MLO4Y3WH" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "bootstrapped_at" + }, + "val": { + "u64": "1784300085" + } + }, + { + "key": { + "symbol": "reputation" + }, + "val": { + "u64": "0" + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4294967295 + }, + { + "entry": { + "last_modified_ledger_seq": 63597602, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CD3KH4OE7HDHHHUYFX3U4L7NLIILMXAY6HM5FEH2UH6UBOKX4HDNE3PC", + "key": { + "vec": [ + { + "symbol": "Profile" + }, + { + "address": "GDCLE5VYYAZ5HYSBSG3PIKQEXHPYNSHF2W7PSW2R7WEIPP2AD275EL4S" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "bootstrapped_at" + }, + "val": { + "u64": "1784736975" + } + }, + { + "key": { + "symbol": "reputation" + }, + "val": { + "u64": "0" + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4294967295 + } + ] +} diff --git a/contracts/compatibility/fixtures/manifest.json b/contracts/compatibility/fixtures/manifest.json new file mode 100644 index 0000000..0c36476 --- /dev/null +++ b/contracts/compatibility/fixtures/manifest.json @@ -0,0 +1,62 @@ +{ + "snapshot": { + "file": "mainnet-state-63617727.json", + "network": "mainnet", + "network_id": "7ac33997544e3175d266bd022439b22cdb16508c01163f26e5cb2a3e1045a979", + "ledger": 63617727, + "timestamp": 1784850182, + "protocol": 27, + "events_contract": "CCFVEGOQJEM47LRAJU2LHEK4KTL5VYN7AOGZ2HH2GNHAMXTILNMMJGQZ", + "profile_contract": "CD3KH4OE7HDHHHUYFX3U4L7NLIILMXAY6HM5FEH2UH6UBOKX4HDNE3PC", + "capture_script": "scripts/capture-mainnet-compat-snapshot.sh", + "sha256": "c5cff0b8668c985adef50d9f24568fbd23d510ca3e96da9a3c7c4f6f779d38d0" + }, + "wasm": [ + { + "file": "mainnet-events-1.1.0-sdk23.wasm", + "sha256": "2a8789bf5dd392b91ddfe6591072a0cb9fae70045abf379fd1dbbc2db4af1de3", + "source": "mainnet contract CCFVEGOQJEM47LRAJU2LHEK4KTL5VYN7AOGZ2HH2GNHAMXTILNMMJGQZ", + "soroban_sdk": "23.5.3", + "protocol": 23, + "runtime_version": "1.1.0", + "contractmeta_version": "1.1.0" + }, + { + "file": "mainnet-profile-1.1.0-sdk23.wasm", + "sha256": "b9e3500cfb559781e68359655f37775c78afffc5a8436daca9d1d1f8e3569b02", + "source": "mainnet contract CD3KH4OE7HDHHHUYFX3U4L7NLIILMXAY6HM5FEH2UH6UBOKX4HDNE3PC", + "soroban_sdk": "23.5.3", + "protocol": 23, + "runtime_version": "1.1.0", + "contractmeta_version": "1.1.0" + }, + { + "file": "events-1.3.0-sdk23.wasm", + "sha256": "6566d9b29d3880bb31c5c5ce3a3a7b7f9d0a085da5d51de01cfd611e3614f2d7", + "source": "commit 57aa591", + "soroban_sdk": "23.5.3", + "protocol": 23, + "runtime_version": "1.3.0", + "contractmeta_version": "1.2.0", + "note": "The source commit changed the runtime version to 1.3.0 without updating contractmeta. The fixture is retained byte-for-byte and the runtime version is asserted by the compatibility test." + }, + { + "file": "events-1.5.0-sdk27.wasm", + "sha256": "31b27e9d5fe554e0710161dd9387693e166bdd755d985a9771b1ed71b9e28321", + "source": "GitHub Actions Linux x86_64 build from commit d5959d0", + "soroban_sdk": "27.0.0", + "protocol": 27, + "runtime_version": "1.5.0", + "contractmeta_version": "1.5.0" + }, + { + "file": "profile-1.2.0-sdk27.wasm", + "sha256": "d2d025d0f7c2c29e6dbd95401841cf62b8b61be608c48615cffb5934050890c0", + "source": "GitHub Actions Linux x86_64 build from commit d5959d0", + "soroban_sdk": "27.0.0", + "protocol": 27, + "runtime_version": "1.2.0", + "contractmeta_version": "1.2.0" + } + ] +} diff --git a/contracts/compatibility/fixtures/profile-1.2.0-sdk27.wasm b/contracts/compatibility/fixtures/profile-1.2.0-sdk27.wasm new file mode 100644 index 0000000..5154b31 Binary files /dev/null and b/contracts/compatibility/fixtures/profile-1.2.0-sdk27.wasm differ diff --git a/contracts/compatibility/src/lib.rs b/contracts/compatibility/src/lib.rs new file mode 100644 index 0000000..c9f5419 --- /dev/null +++ b/contracts/compatibility/src/lib.rs @@ -0,0 +1,770 @@ +#![cfg(test)] +#![allow(clippy::enum_variant_names)] + +use std::cell::Cell; + +use soroban_sdk::{ + contracttype, + testutils::{Address as _, Ledger as _}, + token, vec, Address, BytesN, Env, Map, String, +}; + +mod old_events { + soroban_sdk::contractimport!(file = "fixtures/mainnet-events-1.1.0-sdk23.wasm"); +} + +mod old_profile { + soroban_sdk::contractimport!(file = "fixtures/mainnet-profile-1.1.0-sdk23.wasm"); +} + +mod pre27_events { + soroban_sdk::contractimport!(file = "fixtures/events-1.3.0-sdk23.wasm"); +} + +mod new_events { + soroban_sdk::contractimport!(file = "fixtures/events-1.5.0-sdk27.wasm"); +} + +mod new_profile { + soroban_sdk::contractimport!(file = "fixtures/profile-1.2.0-sdk27.wasm"); +} + +const FEE_BPS: u32 = 250; +const BUDGET: i128 = 10_000_000_000; +const CONTRIBUTION: i128 = 100_000_000; +const TIMELOCK: u32 = 17_280; + +fn next_op(env: &Env, counter: &Cell) -> BytesN<32> { + let value = counter.get(); + counter.set(value + 1); + let mut bytes = [0; 32]; + bytes[28..].copy_from_slice(&value.to_be_bytes()); + BytesN::from_array(env, &bytes) +} + +#[contracttype] +enum LegacyProfileDataKey { + DefaultBootstrapCredits, + DeploymentSeq, + OpSeen(BytesN<32>), +} + +#[contracttype] +enum LegacyEventsDataKey { + EventApplicantSlot(u64, Address), + ContributorSlot(u64, Address), + SupportedTokenSlot(Address), +} + +fn distribution(env: &Env) -> Map { + let mut distribution = Map::new(env); + distribution.set(1, 100); + distribution +} + +fn params( + env: &Env, + owner: &Address, + token: &Address, + title: &str, +) -> old_events::CreateEventParams { + old_events::CreateEventParams { + content_uri: String::from_str(env, "ipfs://event"), + deadline: Some(env.ledger().timestamp() + 86_400), + fee_bps_override: None, + manager: None, + owner: owner.clone(), + pillar: old_events::Pillar::Hackathon, + release_kind: old_events::ReleaseKind::Single, + title: String::from_str(env, title), + token: token.clone(), + total_budget: BUDGET, + winner_distribution: distribution(env), + } +} + +fn pre27_params( + env: &Env, + owner: &Address, + token: &Address, + title: &str, +) -> pre27_events::CreateEventParams { + pre27_events::CreateEventParams { + content_uri: String::from_str(env, "ipfs://event"), + deadline: Some(env.ledger().timestamp() + 86_400), + fee_bps_override: None, + manager: None, + owner: owner.clone(), + pillar: pre27_events::Pillar::Hackathon, + release_kind: pre27_events::ReleaseKind::Single, + title: String::from_str(env, title), + token: token.clone(), + total_budget: BUDGET, + winner_distribution: distribution(env), + } +} + +fn new_params( + env: &Env, + owner: &Address, + token: &Address, + title: &str, +) -> new_events::CreateEventParams { + new_events::CreateEventParams { + content_uri: String::from_str(env, "ipfs://event"), + deadline: Some(env.ledger().timestamp() + 86_400), + fee_bps_override: None, + manager: None, + owner: owner.clone(), + pillar: new_events::Pillar::Hackathon, + release_kind: new_events::ReleaseKind::Single, + title: String::from_str(env, title), + token: token.clone(), + total_budget: BUDGET, + winner_distribution: distribution(env), + } +} + +#[test] +fn sdk23_v110_rows_survive_h6_upgrade_to_sdk27() { + let env = Env::default(); + let ops = Cell::new(1); + env.mock_all_auths_allowing_non_root_auth(); + env.ledger().with_mut(|ledger| { + ledger.sequence_number = 100; + ledger.timestamp = 1_000_000; + // Preserve legacy replay markers while the test advances through the timelock. + ledger.min_temp_entry_ttl = TIMELOCK + 100; + }); + + let admin = Address::generate(&env); + let fee_account = Address::generate(&env); + + let profile_id = env.register(old_profile::WASM, (&admin,)); + let profile_v1 = old_profile::Client::new(&env, &profile_id); + + let events_id = env.register( + old_events::WASM, + (&admin, &fee_account, &FEE_BPS, &profile_id), + ); + let events_v1 = old_events::Client::new(&env, &events_id); + profile_v1.set_events_contract(&events_id); + + let issuer = Address::generate(&env); + let asset = env.register_stellar_asset_contract_v2(issuer); + let token_id = asset.address(); + let token_admin = token::StellarAssetClient::new(&env, &token_id); + token_admin.mint(&fee_account, &0); + + let owner = Address::generate(&env); + let winner = Address::generate(&env); + let grant_recipient = Address::generate(&env); + let contributor = Address::generate(&env); + let manager = Address::generate(&env); + let next_events_admin = Address::generate(&env); + let next_profile_admin = Address::generate(&env); + let rotated_events_id = Address::generate(&env); + let legacy_profile_user = Address::generate(&env); + let legacy_profile_op = next_op(&env, &ops); + token_admin.mint(&owner, &(BUDGET * 6)); + token_admin.mint(&contributor, &(CONTRIBUTION * 4)); + events_v1.register_supported_token(&token_id); + let token = token::Client::new(&env, &token_id); + profile_v1.bootstrap_self(&legacy_profile_user, &legacy_profile_op); + + let mut completed_params = params(&env, &owner, &token_id, "completed"); + completed_params.pillar = old_events::Pillar::Bounty; + let legacy_event_create_op = next_op(&env, &ops); + let completed_id = events_v1.create_event(&completed_params, &legacy_event_create_op); + events_v1.apply_to_bounty(&completed_id, &winner, &next_op(&env, &ops)); + events_v1.submit( + &completed_id, + &winner, + &String::from_str(&env, "ipfs://submission"), + &next_op(&env, &ops), + ); + events_v1.select_winners( + &completed_id, + &vec![ + &env, + old_events::WinnerSpec { + position: 1, + recipient: winner.clone(), + reputation_bump: 7, + }, + ], + &next_op(&env, &ops), + ); + + let cancelling_id = events_v1.create_event( + ¶ms(&env, &owner, &token_id, "cancelling"), + &next_op(&env, &ops), + ); + events_v1.add_funds( + &cancelling_id, + &contributor, + &CONTRIBUTION, + &next_op(&env, &ops), + ); + events_v1.start_cancel(&cancelling_id, &next_op(&env, &ops)); + + let mut active_params = params(&env, &owner, &token_id, "active"); + active_params.manager = Some(manager.clone()); + let active_contributor_id = events_v1.create_event(&active_params, &next_op(&env, &ops)); + events_v1.add_funds( + &active_contributor_id, + &contributor, + &CONTRIBUTION, + &next_op(&env, &ops), + ); + + let mut grant_params = params(&env, &owner, &token_id, "grant"); + grant_params.pillar = old_events::Pillar::Grant; + grant_params.release_kind = old_events::ReleaseKind::Multi(2); + let grant_id = events_v1.create_event(&grant_params, &next_op(&env, &ops)); + events_v1.select_winners( + &grant_id, + &vec![ + &env, + old_events::WinnerSpec { + position: 1, + recipient: grant_recipient.clone(), + reputation_bump: 0, + }, + ], + &next_op(&env, &ops), + ); + let grant_balance_before = token.balance(&grant_recipient); + events_v1.claim_milestone(&grant_id, &grant_recipient, &0, &5, &next_op(&env, &ops)); + + let mut crowdfunding_params = params(&env, &owner, &token_id, "crowdfunding"); + crowdfunding_params.pillar = old_events::Pillar::Crowdfunding; + crowdfunding_params.release_kind = old_events::ReleaseKind::Multi(2); + let crowdfunding_id = events_v1.create_event(&crowdfunding_params, &next_op(&env, &ops)); + events_v1.add_funds( + &crowdfunding_id, + &contributor, + &CONTRIBUTION, + &next_op(&env, &ops), + ); + events_v1.claim_milestone(&crowdfunding_id, &owner, &0, &0, &next_op(&env, &ops)); + + profile_v1.migrate(); + events_v1.migrate(); + profile_v1.set_admin(&next_profile_admin); + events_v1.set_admin(&next_events_admin); + profile_v1.propose_events_contract(&rotated_events_id); + + let new_profile_hash = env.deployer().upload_contract_wasm(new_profile::WASM); + profile_v1.propose_upgrade(&new_profile_hash, &String::from_str(&env, "1.2.0")); + let new_events_hash = env.deployer().upload_contract_wasm(new_events::WASM); + events_v1.propose_upgrade(&new_events_hash, &String::from_str(&env, "1.5.0")); + env.ledger().with_mut(|ledger| { + ledger.sequence_number += TIMELOCK; + }); + profile_v1.apply_upgrade(); + + let profile_v2 = new_profile::Client::new(&env, &profile_id); + assert_eq!(profile_v2.version(), String::from_str(&env, "1.2.0")); + assert_eq!( + profile_v2.get_migrated_to_version(), + Some(String::from_str(&env, "1.1.0")) + ); + let pending_events_contract = profile_v2.get_pending_events_contract().unwrap(); + assert_eq!(pending_events_contract.target, rotated_events_id); + profile_v2.cancel_pending_events_contract(); + assert_eq!(profile_v2.get_pending_events_contract(), None); + profile_v2.accept_admin(); + assert_eq!(profile_v2.get_admin(), next_profile_admin); + let stored_profile = profile_v2.get_profile(&winner).unwrap(); + assert_eq!(stored_profile.reputation, 7); + assert_eq!(profile_v2.get_earnings(&winner, &token_id), BUDGET); + let profile_replay_error = profile_v2 + .try_bootstrap_self(&legacy_profile_user, &legacy_profile_op) + .expect_err("SDK-23 profile replay row must remain effective") + .unwrap(); + assert_eq!(profile_replay_error, new_profile::Error::OpAlreadySeen); + + events_v1.apply_upgrade(); + + let events_v2 = new_events::Client::new(&env, &events_id); + assert_eq!(events_v2.version(), String::from_str(&env, "1.5.0")); + assert_eq!( + events_v2.get_migrated_to_version(), + Some(String::from_str(&env, "1.1.0")) + ); + events_v2.accept_admin(); + assert_eq!(events_v2.get_admin(), next_events_admin); + assert_eq!(events_v2.get_fee_account(), fee_account); + assert_eq!(events_v2.get_fee_bps(), FEE_BPS); + assert_eq!(events_v2.get_profile_contract(), profile_id); + assert_eq!(events_v2.get_applicant_count(&completed_id), 1); + assert_eq!( + events_v2.get_applicant_at(&completed_id, &0), + Some(winner.clone()) + ); + assert_eq!( + env.as_contract(&events_id, || env.storage().persistent().get::<_, u32>( + &LegacyEventsDataKey::EventApplicantSlot(completed_id, winner.clone()) + )), + Some(1) + ); + + let completed = events_v2.get_event(&completed_id); + assert_eq!(completed.title, String::from_str(&env, "completed")); + assert_eq!(completed.status, new_events::EventStatus::Completed); + let submission = events_v2.get_submission(&completed_id, &winner); + assert_eq!( + submission.content_uri, + String::from_str(&env, "ipfs://submission") + ); + let winners = events_v2.get_winners(&completed_id); + assert_eq!(winners.len(), 1); + assert_eq!(winners.get(0).unwrap().recipient, winner); + + assert_eq!(events_v2.get_contributor_count(&cancelling_id), 1); + assert_eq!( + events_v2.get_contributor_amount(&cancelling_id, &contributor), + CONTRIBUTION + ); + assert_eq!( + events_v2.process_cancel_batch(&cancelling_id, &25, &next_op(&env, &ops)), + 0 + ); + events_v2.finalize_cancel(&cancelling_id, &next_op(&env, &ops)); + assert_eq!( + events_v2.get_event(&cancelling_id).status, + new_events::EventStatus::Cancelled + ); + + assert_eq!( + events_v2.get_contributor_amount(&active_contributor_id, &contributor), + CONTRIBUTION + ); + assert_eq!( + events_v2.get_contributor_at(&active_contributor_id, &0), + Some(contributor.clone()) + ); + assert_eq!( + env.as_contract(&events_id, || env.storage().persistent().get::<_, u32>( + &LegacyEventsDataKey::ContributorSlot(active_contributor_id, contributor.clone()) + )), + Some(1) + ); + assert_eq!(events_v2.get_manager(&active_contributor_id), manager); + assert!(events_v2 + .try_start_cancel(&active_contributor_id, &next_op(&env, &ops)) + .is_err()); + + let events_replay_error = events_v2 + .try_create_event( + &new_params(&env, &owner, &token_id, "legacy-op-replay"), + &legacy_event_create_op, + ) + .expect_err("SDK-23 events replay row must remain effective") + .unwrap(); + assert_eq!(events_replay_error, new_events::Error::OpAlreadySeen); + + let post_upgrade_id = events_v2.create_event( + &new_params(&env, &owner, &token_id, "post-upgrade"), + &next_op(&env, &ops), + ); + assert_eq!(post_upgrade_id, crowdfunding_id + 1); + + let grant_replay_error = events_v2 + .try_claim_milestone(&grant_id, &grant_recipient, &0, &5, &next_op(&env, &ops)) + .expect_err("SDK-23 milestone row must reject a duplicate claim") + .unwrap(); + assert_eq!( + grant_replay_error, + new_events::Error::MilestoneAlreadyClaimed + ); + let fee_before_grant_claim = token.balance(&fee_account); + events_v2.claim_milestone(&grant_id, &grant_recipient, &1, &5, &next_op(&env, &ops)); + assert_eq!( + token.balance(&grant_recipient) - grant_balance_before, + BUDGET + ); + assert_eq!(token.balance(&fee_account) - fee_before_grant_claim, 0); + assert_eq!( + events_v2.get_event(&grant_id).status, + new_events::EventStatus::Completed + ); + assert_eq!( + profile_v2.get_profile(&grant_recipient).unwrap().reputation, + 10 + ); + assert_eq!(profile_v2.get_earnings(&grant_recipient, &token_id), BUDGET); + + let crowdfunding_replay_error = events_v2 + .try_claim_milestone(&crowdfunding_id, &owner, &0, &0, &next_op(&env, &ops)) + .expect_err("SDK-23 crowdfunding milestone row must reject a duplicate claim") + .unwrap(); + assert_eq!( + crowdfunding_replay_error, + new_events::Error::MilestoneAlreadyClaimed + ); + let crowdfunding_remaining = events_v2.get_event(&crowdfunding_id).remaining_escrow; + let crowdfunding_balance_before = token.balance(&owner); + let fee_before_crowdfunding_claim = token.balance(&fee_account); + events_v2.claim_milestone(&crowdfunding_id, &owner, &1, &0, &next_op(&env, &ops)); + let crowdfunding_fee = crowdfunding_remaining * FEE_BPS as i128 / 10_000; + assert_eq!( + token.balance(&owner) - crowdfunding_balance_before, + crowdfunding_remaining - crowdfunding_fee + ); + assert_eq!( + token.balance(&fee_account) - fee_before_crowdfunding_claim, + crowdfunding_fee + ); + assert_eq!( + events_v2.get_event(&crowdfunding_id).status, + new_events::EventStatus::Completed + ); + + profile_v2.migrate(); + events_v2.migrate(); + assert_eq!( + profile_v2.get_migrated_to_version(), + Some(String::from_str(&env, "1.2.0")) + ); + assert_eq!( + events_v2.get_migrated_to_version(), + Some(String::from_str(&env, "1.5.0")) + ); +} + +#[test] +fn sdk23_pre27_prize_and_cancellation_keys_work_after_upgrade() { + let env = Env::default(); + let ops = Cell::new(1_000); + env.mock_all_auths_allowing_non_root_auth(); + env.ledger().with_mut(|ledger| { + ledger.sequence_number = 100; + ledger.timestamp = 1_000_000; + }); + + let admin = Address::generate(&env); + let fee_account = Address::generate(&env); + let profile_id = env.register(old_profile::WASM, (&admin,)); + let profile_v1 = old_profile::Client::new(&env, &profile_id); + let events_id = env.register( + pre27_events::WASM, + (&admin, &fee_account, &FEE_BPS, &profile_id), + ); + let events_v1 = pre27_events::Client::new(&env, &events_id); + assert_eq!(events_v1.version(), String::from_str(&env, "1.3.0")); + profile_v1.set_events_contract(&events_id); + + let issuer = Address::generate(&env); + let asset = env.register_stellar_asset_contract_v2(issuer); + let token_id = asset.address(); + let token_admin = token::StellarAssetClient::new(&env, &token_id); + token_admin.mint(&fee_account, &0); + + let owner = Address::generate(&env); + let winner = Address::generate(&env); + let second_winner = Address::generate(&env); + let contributor = Address::generate(&env); + token_admin.mint(&owner, &(BUDGET * 3)); + token_admin.mint(&contributor, &(CONTRIBUTION * 2)); + events_v1.register_supported_token(&token_id); + + let mut prize_params = pre27_params(&env, &owner, &token_id, "prize"); + let mut prize_distribution = Map::new(&env); + prize_distribution.set(1, 50); + prize_distribution.set(2, 50); + prize_params.winner_distribution = prize_distribution; + let prize_id = events_v1.create_event(&prize_params, &next_op(&env, &ops)); + events_v1.select_winners( + &prize_id, + &vec![ + &env, + pre27_events::WinnerSpec { + position: 1, + recipient: winner.clone(), + reputation_bump: 11, + }, + ], + &next_op(&env, &ops), + ); + + let cancellation_id = events_v1.create_event( + &pre27_params(&env, &owner, &token_id, "aggregate"), + &next_op(&env, &ops), + ); + events_v1.add_funds( + &cancellation_id, + &contributor, + &CONTRIBUTION, + &next_op(&env, &ops), + ); + + let new_profile_hash = env.deployer().upload_contract_wasm(new_profile::WASM); + profile_v1.propose_upgrade(&new_profile_hash, &String::from_str(&env, "1.2.0")); + let new_events_hash = env.deployer().upload_contract_wasm(new_events::WASM); + events_v1.propose_upgrade(&new_events_hash, &String::from_str(&env, "1.5.0")); + env.ledger().with_mut(|ledger| { + ledger.sequence_number += TIMELOCK; + }); + profile_v1.apply_upgrade(); + + events_v1.apply_upgrade(); + + let profile_v2 = new_profile::Client::new(&env, &profile_id); + let events_v2 = new_events::Client::new(&env, &events_id); + let token = token::Client::new(&env, &token_id); + + assert_eq!(token.balance(&winner), 0); + assert!(events_v2 + .try_start_cancel(&prize_id, &next_op(&env, &ops)) + .is_err()); + events_v2.select_winners( + &prize_id, + &vec![ + &env, + new_events::WinnerSpec { + position: 2, + recipient: second_winner.clone(), + reputation_bump: 13, + }, + ], + &next_op(&env, &ops), + ); + let fee_before_prize_claims = token.balance(&fee_account); + events_v2.claim_prize(&prize_id, &1, &next_op(&env, &ops)); + events_v2.claim_prize(&prize_id, &2, &next_op(&env, &ops)); + assert_eq!(token.balance(&winner), BUDGET / 2); + assert_eq!(token.balance(&second_winner), BUDGET / 2); + assert_eq!(token.balance(&fee_account) - fee_before_prize_claims, 0); + assert_eq!( + events_v2.get_event(&prize_id).status, + new_events::EventStatus::Completed + ); + assert_eq!(profile_v2.get_profile(&winner).unwrap().reputation, 11); + assert_eq!( + profile_v2.get_profile(&second_winner).unwrap().reputation, + 13 + ); + assert_eq!(profile_v2.get_earnings(&winner, &token_id), BUDGET / 2); + assert_eq!( + profile_v2.get_earnings(&second_winner, &token_id), + BUDGET / 2 + ); + + events_v2.start_cancel(&cancellation_id, &next_op(&env, &ops)); + assert_eq!( + events_v2.get_event(&cancellation_id).status, + new_events::EventStatus::Cancelling + ); + assert_eq!( + events_v2.process_cancel_batch(&cancellation_id, &25, &next_op(&env, &ops)), + 0 + ); + events_v2.finalize_cancel(&cancellation_id, &next_op(&env, &ops)); + assert_eq!( + events_v2.get_event(&cancellation_id).status, + new_events::EventStatus::Cancelled + ); +} + +#[test] +fn exact_mainnet_snapshot_survives_h6_upgrade_to_sdk27() { + let env = Env::from_ledger_snapshot_file(concat!( + env!("CARGO_MANIFEST_DIR"), + "/fixtures/mainnet-state-63617727.json" + )); + env.mock_all_auths_allowing_non_root_auth(); + + let events_id = Address::from_str( + &env, + "CCFVEGOQJEM47LRAJU2LHEK4KTL5VYN7AOGZ2HH2GNHAMXTILNMMJGQZ", + ); + let profile_id = Address::from_str( + &env, + "CD3KH4OE7HDHHHUYFX3U4L7NLIILMXAY6HM5FEH2UH6UBOKX4HDNE3PC", + ); + let admin = Address::from_str( + &env, + "GCVK72I6TVJVDTTY4UKU6MQT4QJ2T2AAG3NULNEUDM46L3UOQYDSO4O2", + ); + let fee_account = Address::from_str( + &env, + "GADSIP2HPINWTMEUNMVYA5NJRL372OV52HDEJTRTLDZXMHADIPQNYH55", + ); + let token_0 = Address::from_str( + &env, + "CCW67TSZV3SSS2HXMBQ5JFGCKJNXKZM7UQUWUZPUTHXSTZLEO7SJMI75", + ); + let token_1 = Address::from_str( + &env, + "CAS3J7GYLGXMF6TDJBBYYSE3HQ6BBSMLNUQ34T6TZMYMW2EVH34XOWMA", + ); + let profile_addresses = [ + "GDCLE5VYYAZ5HYSBSG3PIKQEXHPYNSHF2W7PSW2R7WEIPP2AD275EL4S", + "GAHGDV4MWDLT3BXOUT5A2T774IUAE54DZ6BUCBH4O7PG5TJGWXSV3OJO", + "GBVQKBWHZGO6YED3EVXOZW76VPNYUMJLBLKE5IN4XE5LTT262UXCBIR7", + "GCCCPDSWYDVTD2VSXNXDKWWNENSRTA637CHVHIIXISTKSK4FV6T7G3HV", + "GCS5XWO3M72B6PSMZVGOSB3CLQVWB5OJ6Z7FTFAP5FAZVWS3MLO4Y3WH", + ]; + let legacy_op_ids = [ + [ + 0xd0, 0x12, 0x86, 0xfa, 0x65, 0x4b, 0x4f, 0x92, 0x4e, 0xb2, 0xa1, 0x65, 0xe3, 0xd8, + 0x38, 0x61, 0x42, 0x91, 0x7b, 0x07, 0xaf, 0x9a, 0x22, 0xf2, 0x22, 0x42, 0x0b, 0xa8, + 0xd2, 0xb0, 0x1a, 0xd8, + ], + [ + 0x22, 0xf9, 0xdf, 0xf5, 0xb7, 0x18, 0x43, 0x44, 0xe5, 0xba, 0xc3, 0x94, 0xf9, 0xfb, + 0x14, 0x1a, 0xf5, 0xb1, 0xda, 0x8e, 0x71, 0xae, 0x36, 0xe7, 0x61, 0x2f, 0xb1, 0xaa, + 0x68, 0xf2, 0xf1, 0xbc, + ], + [ + 0xd4, 0x56, 0xce, 0x7d, 0xbe, 0x62, 0xe2, 0xf5, 0x94, 0x9d, 0xc2, 0xcc, 0xe2, 0xb6, + 0xa9, 0x99, 0x00, 0xce, 0x9e, 0xd0, 0x8e, 0xe4, 0x42, 0x37, 0x21, 0x9a, 0xf2, 0x27, + 0x55, 0xfa, 0x71, 0xd5, + ], + ]; + + env.deployer().upload_contract_wasm(old_events::WASM); + env.deployer().upload_contract_wasm(old_profile::WASM); + + let events_v1 = old_events::Client::new(&env, &events_id); + let profile_v1 = old_profile::Client::new(&env, &profile_id); + assert_eq!(events_v1.version(), String::from_str(&env, "1.1.0")); + assert_eq!(profile_v1.version(), String::from_str(&env, "1.1.0")); + let expected_event_id_base = events_v1.id_base(); + events_v1.pause(); + profile_v1.pause(); + assert!(events_v1.is_paused()); + assert!(profile_v1.is_paused()); + + for strkey in profile_addresses { + let address = Address::from_str(&env, strkey); + let profile = profile_v1.get_profile(&address).unwrap(); + assert_eq!(profile.reputation, 0); + } + assert_eq!( + env.as_contract(&profile_id, || env + .storage() + .instance() + .get::<_, u32>(&LegacyProfileDataKey::DefaultBootstrapCredits)), + Some(10) + ); + assert_eq!( + env.as_contract(&profile_id, || env + .storage() + .instance() + .get::<_, u32>(&LegacyProfileDataKey::DeploymentSeq)), + Some(63_222_664) + ); + for bytes in legacy_op_ids { + let op_id = BytesN::from_array(&env, &bytes); + assert_eq!( + env.as_contract(&profile_id, || env + .storage() + .temporary() + .get::<_, bool>(&LegacyProfileDataKey::OpSeen(op_id))), + Some(true) + ); + } + + let new_profile_hash = env.deployer().upload_contract_wasm(new_profile::WASM); + profile_v1.propose_upgrade(&new_profile_hash, &String::from_str(&env, "1.2.0")); + let new_events_hash = env.deployer().upload_contract_wasm(new_events::WASM); + events_v1.propose_upgrade(&new_events_hash, &String::from_str(&env, "1.5.0")); + env.ledger().with_mut(|ledger| { + ledger.sequence_number += TIMELOCK; + }); + profile_v1.apply_upgrade(); + let profile_v2 = new_profile::Client::new(&env, &profile_id); + assert!(profile_v2.is_paused()); + + events_v1.apply_upgrade(); + + let events_v2 = new_events::Client::new(&env, &events_id); + assert!(events_v2.is_paused()); + profile_v2.migrate(); + events_v2.migrate(); + profile_v2.unpause(); + events_v2.unpause(); + + assert_eq!(events_v2.version(), String::from_str(&env, "1.5.0")); + assert_eq!(profile_v2.version(), String::from_str(&env, "1.2.0")); + assert_eq!( + events_v2.get_migrated_to_version(), + Some(String::from_str(&env, "1.5.0")) + ); + assert_eq!( + profile_v2.get_migrated_to_version(), + Some(String::from_str(&env, "1.2.0")) + ); + assert!(!events_v2.is_paused()); + assert!(!profile_v2.is_paused()); + assert_eq!(events_v2.get_admin(), admin); + assert_eq!(events_v2.get_fee_account(), fee_account); + assert_eq!(events_v2.get_fee_bps(), 250); + assert_eq!(events_v2.get_profile_contract(), profile_id); + assert_eq!(events_v2.id_base(), expected_event_id_base); + assert!(events_v2.is_supported_token(&token_0)); + assert!(events_v2.is_supported_token(&token_1)); + assert_eq!(events_v2.supported_token_count(), 2); + assert_eq!(events_v2.supported_token_at(&0), Some(token_0.clone())); + assert_eq!(events_v2.supported_token_at(&1), Some(token_1.clone())); + assert_eq!( + env.as_contract(&events_id, || env.storage().instance().get::<_, u32>( + &LegacyEventsDataKey::SupportedTokenSlot(token_0.clone()) + )), + Some(1) + ); + assert_eq!( + env.as_contract(&events_id, || env.storage().instance().get::<_, u32>( + &LegacyEventsDataKey::SupportedTokenSlot(token_1.clone()) + )), + Some(2) + ); + events_v2.deregister_supported_token(&token_1); + assert_eq!(events_v2.supported_token_count(), 1); + assert!(!events_v2.is_supported_token(&token_1)); + events_v2.register_supported_token(&token_1); + assert_eq!(events_v2.supported_token_count(), 2); + assert_eq!(events_v2.supported_token_at(&1), Some(token_1.clone())); + assert_eq!(profile_v2.get_admin(), admin); + assert_eq!(profile_v2.get_events_contract(), Some(events_id)); + let legacy_replay_user = Address::from_str(&env, profile_addresses[0]); + let legacy_replay_op = BytesN::from_array(&env, &legacy_op_ids[0]); + let legacy_replay_error = profile_v2 + .try_bootstrap_self(&legacy_replay_user, &legacy_replay_op) + .expect_err("captured SDK-23 replay row must remain effective") + .unwrap(); + assert_eq!(legacy_replay_error, new_profile::Error::OpAlreadySeen); + + for strkey in profile_addresses { + let address = Address::from_str(&env, strkey); + let profile = profile_v2.get_profile(&address).unwrap(); + assert_eq!(profile.reputation, 0); + } + assert_eq!( + env.as_contract(&profile_id, || env + .storage() + .instance() + .get::<_, u32>(&LegacyProfileDataKey::DefaultBootstrapCredits)), + Some(10) + ); + assert_eq!( + env.as_contract(&profile_id, || env + .storage() + .instance() + .get::<_, u32>(&LegacyProfileDataKey::DeploymentSeq)), + Some(63_222_664) + ); + for bytes in legacy_op_ids { + let op_id = BytesN::from_array(&env, &bytes); + assert_eq!( + env.as_contract(&profile_id, || env + .storage() + .temporary() + .get::<_, bool>(&LegacyProfileDataKey::OpSeen(op_id))), + Some(true) + ); + } +} diff --git a/contracts/events/src/storage.rs b/contracts/events/src/storage.rs index b922567..f66e648 100644 --- a/contracts/events/src/storage.rs +++ b/contracts/events/src/storage.rs @@ -1,6 +1,6 @@ #![allow(dead_code)] -use soroban_sdk::{Address, BytesN, Env, Vec}; +use soroban_sdk::{contracttype, Address, BytesN, Env, Vec}; use soroban_sdk::String; @@ -19,6 +19,11 @@ const INSTANCE_TTL_BUMP: u32 = 518_400; const EVENT_TTL_THRESHOLD: u32 = 86_400; const EVENT_TTL_BUMP: u32 = 1_555_200; +#[contracttype(export = false)] +enum LegacyDataKey { + OpSeen(BytesN<32>), +} + pub fn touch_instance(env: &Env) { env.storage() .instance() @@ -755,10 +760,17 @@ pub fn clear_cancellation_state(env: &Env, id: u64) { // IDEMPOTENCY (temporary; auto-TTL) // ============================================================ pub fn is_op_seen(env: &Env, domain: &Address, op_id: &BytesN<32>) -> bool { - env.storage() + let scoped_seen = env + .storage() .temporary() .get(&DataKey::OpSeen(domain.clone(), op_id.clone())) - .unwrap_or(false) + .unwrap_or(false); + scoped_seen + || env + .storage() + .temporary() + .get(&LegacyDataKey::OpSeen(op_id.clone())) + .unwrap_or(false) } pub fn mark_op_seen(env: &Env, domain: &Address, op_id: &BytesN<32>) { diff --git a/contracts/events/src/tests/bounty_pillar.rs b/contracts/events/src/tests/bounty_pillar.rs index f2cf850..5eda20a 100644 --- a/contracts/events/src/tests/bounty_pillar.rs +++ b/contracts/events/src/tests/bounty_pillar.rs @@ -1,7 +1,7 @@ #![cfg(test)] use soroban_sdk::{ - testutils::{Address as _, BytesN as _, Ledger}, + testutils::{Address as _, BytesN as _}, token, Address, BytesN, Env, Map, String, }; diff --git a/contracts/events/src/tests/escrow_fee_math.rs b/contracts/events/src/tests/escrow_fee_math.rs index 33dadb4..bb39254 100644 --- a/contracts/events/src/tests/escrow_fee_math.rs +++ b/contracts/events/src/tests/escrow_fee_math.rs @@ -19,7 +19,6 @@ struct Ctx<'a> { #[allow(dead_code)] profile: ProfileContractClient<'a>, owner: Address, - admin: Address, token_addr: Address, fee_account: Address, token_admin: token::StellarAssetClient<'a>, @@ -63,7 +62,6 @@ fn setup_with_bps<'a>(fee_bps: u32) -> Ctx<'a> { events, profile, owner, - admin: events_admin, token_addr, fee_account, token_admin, diff --git a/contracts/events/src/tests/hackathon_pillar.rs b/contracts/events/src/tests/hackathon_pillar.rs index a56733d..6c51c1b 100644 --- a/contracts/events/src/tests/hackathon_pillar.rs +++ b/contracts/events/src/tests/hackathon_pillar.rs @@ -1,7 +1,7 @@ #![cfg(test)] use soroban_sdk::{ - testutils::{Address as _, BytesN as _, Ledger as _}, + testutils::{Address as _, BytesN as _}, token, Address, BytesN, Env, Map, String, }; diff --git a/contracts/profile/src/storage.rs b/contracts/profile/src/storage.rs index 5262252..9b094e5 100644 --- a/contracts/profile/src/storage.rs +++ b/contracts/profile/src/storage.rs @@ -1,6 +1,6 @@ #![allow(dead_code)] -use soroban_sdk::{Address, BytesN, Env}; +use soroban_sdk::{contracttype, Address, BytesN, Env}; use soroban_sdk::String; @@ -16,6 +16,11 @@ const INSTANCE_TTL_BUMP: u32 = 518_400; const PROFILE_TTL_THRESHOLD: u32 = 86_400; const PROFILE_TTL_BUMP: u32 = 1_555_200; +#[contracttype(export = false)] +enum LegacyDataKey { + OpSeen(BytesN<32>), +} + pub fn touch_instance(env: &Env) { env.storage() .instance() @@ -172,10 +177,17 @@ pub fn set_earnings(env: &Env, user: &Address, token: &Address, amount: i128) { // IDEMPOTENCY (temporary; auto-TTL) // ============================================================ pub fn is_op_seen(env: &Env, domain: &Address, op_id: &BytesN<32>) -> bool { - env.storage() + let scoped_seen = env + .storage() .temporary() .get(&DataKey::OpSeen(domain.clone(), op_id.clone())) - .unwrap_or(false) + .unwrap_or(false); + scoped_seen + || env + .storage() + .temporary() + .get(&LegacyDataKey::OpSeen(op_id.clone())) + .unwrap_or(false) } pub fn mark_op_seen(env: &Env, domain: &Address, op_id: &BytesN<32>) { diff --git a/deploy_and_upgrade.sh b/deploy_and_upgrade.sh index edd9bd9..6c050f4 100755 --- a/deploy_and_upgrade.sh +++ b/deploy_and_upgrade.sh @@ -72,9 +72,9 @@ build_contract() { # any non-testnet build) MUST omit it; the default keeps the full # audit-mandated timelock, so this fails safe. if [ "$NETWORK" = "testnet" ]; then - stellar contract build --package "boundless-$CONTRACT_KIND" --features testnet + stellar contract build --locked --package "boundless-$CONTRACT_KIND" --features testnet else - stellar contract build + stellar contract build --locked fi } diff --git a/deploy_mainnet.sh b/deploy_mainnet.sh index 181a324..a716e30 100755 --- a/deploy_mainnet.sh +++ b/deploy_mainnet.sh @@ -14,14 +14,23 @@ # ./deploy_mainnet.sh deploy-events # ./deploy_mainnet.sh register-token # ./deploy_mainnet.sh rotate-admin +# ./deploy_mainnet.sh upload-profile-wasm # ./deploy_mainnet.sh upload-events-wasm +# ./deploy_mainnet.sh prepare-pause-profile # ./deploy_mainnet.sh prepare-pause-events +# ./deploy_mainnet.sh prepare-propose-upgrade-profile # ./deploy_mainnet.sh prepare-propose-upgrade-events +# ./deploy_mainnet.sh verify-proposed-upgrade-profile # ./deploy_mainnet.sh verify-proposed-upgrade-events +# ./deploy_mainnet.sh prepare-apply-upgrade-profile # ./deploy_mainnet.sh prepare-apply-upgrade-events +# ./deploy_mainnet.sh prepare-migrate-upgrade-profile # ./deploy_mainnet.sh prepare-migrate-upgrade-events +# ./deploy_mainnet.sh prepare-cancel-upgrade-profile # ./deploy_mainnet.sh prepare-cancel-upgrade-events +# ./deploy_mainnet.sh prepare-unpause-profile # ./deploy_mainnet.sh prepare-unpause-events +# ./deploy_mainnet.sh verify-upgrade-profile # ./deploy_mainnet.sh verify-upgrade-events # ./deploy_mainnet.sh upgrade-status # ./deploy_mainnet.sh verify @@ -39,6 +48,11 @@ NC='\033[0m' NETWORK="${STELLAR_NETWORK:-}" NETWORK_PASSPHRASE="Public Global Stellar Network ; September 2015" +REQUIRED_STELLAR_CLI_VERSION="27.0.0" +SDK27_EVENTS_VERSION="1.5.0" +SDK27_EVENTS_WASM_HASH="31b27e9d5fe554e0710161dd9387693e166bdd755d985a9771b1ed71b9e28321" +SDK27_PROFILE_VERSION="1.2.0" +SDK27_PROFILE_WASM_HASH="d2d025d0f7c2c29e6dbd95401841cf62b8b61be608c48615cffb5934050890c0" DEPLOYMENTS_DIR="$(cd "$(dirname "$0")" && pwd)/deployments" DEPLOYMENT_FILE="$DEPLOYMENTS_DIR/mainnet.json" @@ -81,6 +95,10 @@ require_cli() { command -v awk >/dev/null 2>&1 || err "awk not on PATH" command -v sha256sum >/dev/null 2>&1 || command -v shasum >/dev/null 2>&1 || \ err "sha256sum or shasum not on PATH" + local stellar_cli_version + stellar_cli_version="$(stellar --version | awk 'NR == 1 { print $2 }')" + [ "$stellar_cli_version" = "$REQUIRED_STELLAR_CLI_VERSION" ] || \ + err "stellar CLI version mismatch: expected $REQUIRED_STELLAR_CLI_VERSION, got ${stellar_cli_version:-unknown}" if [ -n "${STELLAR_RPC_URL:-}" ] || [ -n "${STELLAR_NETWORK_PASSPHRASE:-}" ]; then err "unset STELLAR_RPC_URL and STELLAR_NETWORK_PASSPHRASE when using --network $NETWORK" fi @@ -114,6 +132,66 @@ hash_wasm() { fi } +deployed_wasm_hash() { + local contract_id="$1" + stellar contract info hash \ + --network "$NETWORK" \ + --contract-id "$contract_id" +} + +assert_local_wasm_hash() { + local label="$1" wasm="$2" expected="$3" + local actual + actual="$(hash_wasm "$wasm")" + [ "$actual" = "$expected" ] || \ + err "$label artifact hash mismatch: expected $expected, got $actual" +} + +assert_deployed_wasm_hash() { + local label="$1" contract_id="$2" expected="$3" + local actual + actual="$(deployed_wasm_hash "$contract_id")" || \ + err "could not read deployed $label WASM hash" + [ "$actual" = "$expected" ] || \ + err "deployed $label WASM hash mismatch: expected $expected, got $actual" +} + +assert_sdk27_profile_artifact() { + local wasm="$1" version="$2" + if [ "$version" = "$SDK27_PROFILE_VERSION" ]; then + assert_local_wasm_hash "profile SDK 27" "$wasm" "$SDK27_PROFILE_WASM_HASH" + fi +} + +assert_sdk27_events_artifact() { + local wasm="$1" version="$2" + if [ "$version" = "$SDK27_EVENTS_VERSION" ]; then + assert_local_wasm_hash "events SDK 27" "$wasm" "$SDK27_EVENTS_WASM_HASH" + fi +} + +assert_profile_sdk27_wasm_live() { + assert_deployed_wasm_hash "profile SDK 27" "$1" "$SDK27_PROFILE_WASM_HASH" +} + +assert_events_sdk27_wasm_live() { + assert_deployed_wasm_hash "events SDK 27" "$1" "$SDK27_EVENTS_WASM_HASH" +} + +assert_version_argument() { + local label="$1" actual="$2" expected="$3" + [ "$actual" = "$expected" ] || \ + err "$label version must be $expected for this pinned mainnet flow; got $actual" +} + +assert_unpause_version_argument() { + local label="$1" actual="$2" upgraded="$3" + case "$actual" in + "1.1.0"|"$upgraded") ;; + *) err "$label unpause version must be 1.1.0 for pre-apply recovery or $upgraded after upgrade; got $actual" ;; + esac +} + ensure_deployments_dir() { mkdir -p "$DEPLOYMENTS_DIR" if [ ! -f "$DEPLOYMENT_FILE" ]; then @@ -152,6 +230,16 @@ events_contract_id() { printf '%s\n' "$events_id" } +profile_contract_id() { + local profile_id="${PROFILE_ID:-}" + if [ -z "$profile_id" ] && [ -f "$DEPLOYMENT_FILE" ]; then + profile_id="$(deployment_get profile_contract)" + fi + [ -n "$profile_id" ] || \ + err "profile contract id missing; set PROFILE_ID or restore deployments/mainnet.json" + printf '%s\n' "$profile_id" +} + events_read() { local events_id="$1" shift @@ -163,6 +251,17 @@ events_read() { -- "$@" } +profile_read() { + local profile_id="$1" + shift + stellar contract invoke \ + --send no \ + --network "$NETWORK" \ + --source "$ADMIN_SOURCE" \ + --id "$profile_id" \ + -- "$@" +} + read_json_string() { jq -er 'if type == "string" then . else error("expected JSON string") end' } @@ -176,6 +275,15 @@ assert_events_version() { err "events version mismatch: expected $expected, got $actual" } +assert_profile_version() { + local profile_id="$1" expected="$2" + local actual + actual="$(profile_read "$profile_id" version | read_json_string)" || \ + err "could not read the current profile contract version" + [ "$actual" = "$expected" ] || \ + err "profile version mismatch: expected $expected, got $actual" +} + assert_admin_source_matches() { local events_id="$1" local actual_admin @@ -185,6 +293,41 @@ assert_admin_source_matches() { err "ADMIN_SOURCE mismatch: contract admin is $actual_admin, got $ADMIN_SOURCE" } +assert_profile_admin_source_matches() { + local profile_id="$1" + local actual_admin + actual_admin="$(profile_read "$profile_id" get_admin | read_json_string)" || \ + err "could not read the current profile admin" + [ "$actual_admin" = "$ADMIN_SOURCE" ] || \ + err "ADMIN_SOURCE mismatch: profile contract admin is $actual_admin, got $ADMIN_SOURCE" +} + +assert_profile_events_binding() { + local profile_id="$1" expected_events_id="$2" + local actual_events_id + actual_events_id="$(profile_read "$profile_id" get_events_contract | read_json_string)" || \ + err "could not read profile.get_events_contract" + [ "$actual_events_id" = "$expected_events_id" ] || \ + err "profile events-contract binding changed: expected $expected_events_id, got $actual_events_id" +} + +assert_no_pending_events_contract_rotation() { + local profile_id="$1" + local pending + pending="$(profile_read "$profile_id" get_pending_events_contract)" || \ + err "could not read profile.get_pending_events_contract" + [ "$pending" = "null" ] || \ + err "a profile events-contract rotation is pending; cancel or complete it before the upgrade" +} + +assert_upgrade_governance_stable() { + local profile_id="$1" events_id="$2" + assert_admin_source_matches "$events_id" + assert_profile_admin_source_matches "$profile_id" + assert_profile_events_binding "$profile_id" "$events_id" + assert_no_pending_events_contract_rotation "$profile_id" +} + assert_events_paused() { local events_id="$1" local paused @@ -203,6 +346,24 @@ assert_events_unpaused() { err "events contract is still paused after unpause" } +assert_profile_paused() { + local profile_id="$1" + local paused + paused="$(profile_read "$profile_id" is_paused)" || \ + err "could not read profile.is_paused" + [ "$paused" = "true" ] || \ + err "profile contract is not paused" +} + +assert_profile_unpaused() { + local profile_id="$1" + local paused + paused="$(profile_read "$profile_id" is_paused)" || \ + err "could not read profile.is_paused" + [ "$paused" = "false" ] || \ + err "profile contract is still paused after unpause" +} + assert_no_pending_events_upgrade() { local events_id="$1" local pending @@ -228,6 +389,87 @@ assert_pending_events_upgrade() { err "pending wasm mismatch: expected $expected_hash, got $actual_hash" } +assert_no_pending_profile_upgrade() { + local profile_id="$1" + local pending + pending="$(profile_read "$profile_id" get_pending_upgrade)" || \ + err "could not read pending profile upgrade" + [ "$pending" = "null" ] || \ + err "a profile upgrade is already pending; inspect it with upgrade-status" +} + +assert_pending_profile_upgrade() { + local profile_id="$1" expected_version="$2" expected_hash="$3" + local pending actual_version actual_hash + pending="$(profile_read "$profile_id" get_pending_upgrade)" || \ + err "could not read pending profile upgrade" + [ "$pending" != "null" ] || err "no profile upgrade is pending" + actual_version="$(printf '%s' "$pending" | jq -er '.new_version')" || \ + err "pending profile upgrade did not contain new_version" + actual_hash="$(printf '%s' "$pending" | jq -er '.wasm_hash')" || \ + err "pending profile upgrade did not contain wasm_hash" + [ "$actual_version" = "$expected_version" ] || \ + err "pending profile version mismatch: expected $expected_version, got $actual_version" + [ "$actual_hash" = "$expected_hash" ] || \ + err "pending profile wasm mismatch: expected $expected_hash, got $actual_hash" +} + +assert_pending_events_version() { + local events_id="$1" expected_version="$2" + local pending actual_version + pending="$(events_read "$events_id" get_pending_upgrade)" || \ + err "could not read pending events upgrade" + [ "$pending" != "null" ] || err "no events upgrade is pending" + actual_version="$(printf '%s' "$pending" | jq -er '.new_version')" || \ + err "pending events upgrade did not contain new_version" + [ "$actual_version" = "$expected_version" ] || \ + err "pending events version mismatch: expected $expected_version, got $actual_version" +} + +assert_profile_sdk27_staged_or_applied() { + local profile_id="$1" + local version pending + version="$(profile_read "$profile_id" version | read_json_string)" || \ + err "could not read the current profile contract version" + pending="$(profile_read "$profile_id" get_pending_upgrade)" || \ + err "could not read pending profile upgrade" + assert_profile_paused "$profile_id" + + if [ "$version" = "1.1.0" ]; then + assert_pending_profile_upgrade \ + "$profile_id" "$SDK27_PROFILE_VERSION" "$SDK27_PROFILE_WASM_HASH" + elif [ "$version" = "1.2.0" ]; then + [ "$pending" = "null" ] || \ + err "profile is 1.2.0 but still has a pending upgrade" + assert_profile_sdk27_wasm_live "$profile_id" + else + err "profile must be staged at 1.1.0 or applied at 1.2.0; got $version" + fi +} + +assert_profile_sdk27_applied_paused() { + local profile_id="$1" + assert_profile_version "$profile_id" "1.2.0" + assert_profile_sdk27_wasm_live "$profile_id" + assert_profile_paused "$profile_id" + assert_no_pending_profile_upgrade "$profile_id" +} + +assert_profile_sdk27_migrated_paused() { + local profile_id="$1" + assert_profile_sdk27_applied_paused "$profile_id" + assert_profile_migrated_version "$profile_id" "1.2.0" +} + +assert_profile_sdk27_ready() { + local profile_id="$1" + assert_profile_version "$profile_id" "1.2.0" + assert_profile_sdk27_wasm_live "$profile_id" + assert_profile_migrated_version "$profile_id" "1.2.0" + assert_no_pending_profile_upgrade "$profile_id" + assert_profile_unpaused "$profile_id" +} + assert_no_events_exist() { local events_id="$1" local base first_event_id output status @@ -244,7 +486,7 @@ assert_no_events_exist() { set -e if [ "$status" -eq 0 ]; then - err "event $first_event_id exists; abort 1.2.0 and implement a legacy-total migration" + err "event $first_event_id exists; abort the upgrade and implement a legacy-total migration" fi if [[ "$output" != *"Error(Contract, #30)"* ]]; then err "could not prove event $first_event_id is absent; refusing to continue: $output" @@ -253,6 +495,10 @@ assert_no_events_exist() { ok "Zero-event guard confirmed: $first_event_id returned EventNotFound." } +is_zero_event_guarded_target() { + [ "$1" = "$SDK27_EVENTS_VERSION" ] +} + assert_migrated_version() { local events_id="$1" expected="$2" local migrated_version @@ -262,6 +508,15 @@ assert_migrated_version() { err "migration marker mismatch: expected $expected, got $migrated_version" } +assert_profile_migrated_version() { + local profile_id="$1" expected="$2" + local migrated_version + migrated_version="$(profile_read "$profile_id" get_migrated_to_version | read_json_string)" || \ + err "could not read profile migrated-to version" + [ "$migrated_version" = "$expected" ] || \ + err "profile migration marker mismatch: expected $expected, got $migrated_version" +} + prepare_events_invoke() { local events_id="$1" output_file="$2" shift 2 @@ -295,6 +550,39 @@ prepare_events_invoke() { echo " stellar tx send \"\" --network $NETWORK" } +prepare_profile_invoke() { + local profile_id="$1" output_file="$2" + shift 2 + local output_dir + output_dir="$(dirname "$output_file")" + [ -d "$output_dir" ] || err "XDR output directory does not exist: $output_dir" + assert_profile_admin_source_matches "$profile_id" + + local tmp + tmp="$(mktemp)" + if ! stellar contract invoke \ + --network "$NETWORK" \ + --source-account "$ADMIN_SOURCE" \ + --id "$profile_id" \ + --build-only \ + -- "$@" \ + | stellar tx simulate \ + --network "$NETWORK" \ + --source-account "$ADMIN_SOURCE" \ + > "$tmp"; then + rm -f "$tmp" + err "could not build and simulate the profile multisig transaction" + fi + [ -s "$tmp" ] || { + rm -f "$tmp" + err "prepared profile transaction was empty" + } + mv "$tmp" "$output_file" + ok "Prepared simulated XDR: $output_file" + echo "Sign it sequentially with the required multi-sig quorum, then submit with:" + echo " stellar tx send \"\" --network $NETWORK" +} + append_upgrade_log() { local action="$1" version="$2" wasm_hash="${3:-}" local upgrades_log="$DEPLOYMENTS_DIR/mainnet-upgrades.jsonl" @@ -309,7 +597,7 @@ append_upgrade_log() { cmd_build_release() { info "Building contracts in release mode..." - stellar contract build + stellar contract build --locked ok "Build complete." } @@ -459,6 +747,285 @@ cmd_rotate_admin() { echo "land, run 'verify' to confirm get_admin returns the new address." } +cmd_upload_profile_wasm() { + local wasm="${1:-}" + [ -n "$wasm" ] || err "usage: upload-profile-wasm " + [ -f "$wasm" ] || err "wasm file not found: $wasm" + require_env UPLOAD_SOURCE + require_env ADMIN_SOURCE + require_cli + confirm_mainnet + ensure_deployments_dir + + local profile_id expected_hash uploaded_hash + profile_id="$(profile_contract_id)" + assert_profile_version "$profile_id" "1.1.0" + assert_profile_admin_source_matches "$profile_id" + expected_hash="$(hash_wasm "$wasm")" + assert_local_wasm_hash "profile SDK 27" "$wasm" "$SDK27_PROFILE_WASM_HASH" + uploaded_hash="$(stellar contract upload \ + --source-account "$UPLOAD_SOURCE" \ + --network "$NETWORK" \ + --optimize=false \ + --wasm "$wasm")" + [ "$uploaded_hash" = "$expected_hash" ] || \ + err "uploaded profile wasm hash mismatch: expected $expected_hash, got $uploaded_hash" + + append_upgrade_log "profile-uploaded" "" "$uploaded_hash" + ok "Uploaded exact profile WASM: $uploaded_hash" +} + +cmd_prepare_pause_profile() { + local output_file="${1:-}" + [ -n "$output_file" ] || err "usage: prepare-pause-profile " + require_env ADMIN_SOURCE + require_cli + + local profile_id events_id + profile_id="$(profile_contract_id)" + events_id="$(events_contract_id)" + assert_upgrade_governance_stable "$profile_id" "$events_id" + assert_events_version "$events_id" "1.1.0" + assert_events_paused "$events_id" + assert_no_events_exist "$events_id" + assert_no_pending_events_upgrade "$events_id" + assert_profile_version "$profile_id" "1.1.0" + assert_profile_unpaused "$profile_id" + assert_no_pending_profile_upgrade "$profile_id" + prepare_profile_invoke "$profile_id" "$output_file" pause +} + +cmd_prepare_propose_upgrade_profile() { + local wasm="${1:-}" new_version="${2:-}" output_file="${3:-}" + [ -n "$wasm" ] || \ + err "usage: prepare-propose-upgrade-profile " + [ -n "$new_version" ] || \ + err "usage: prepare-propose-upgrade-profile " + [ -n "$output_file" ] || \ + err "usage: prepare-propose-upgrade-profile " + [ -f "$wasm" ] || err "wasm file not found: $wasm" + assert_version_argument "profile upgrade" "$new_version" "$SDK27_PROFILE_VERSION" + require_env ADMIN_SOURCE + require_cli + + local profile_id events_id wasm_hash + profile_id="$(profile_contract_id)" + events_id="$(events_contract_id)" + assert_upgrade_governance_stable "$profile_id" "$events_id" + wasm_hash="$(hash_wasm "$wasm")" + assert_sdk27_profile_artifact "$wasm" "$new_version" + assert_events_version "$events_id" "1.1.0" + assert_events_paused "$events_id" + assert_no_events_exist "$events_id" + assert_no_pending_events_upgrade "$events_id" + assert_no_pending_profile_upgrade "$profile_id" + assert_profile_paused "$profile_id" + if [ "$new_version" = "1.2.0" ]; then + assert_profile_version "$profile_id" "1.1.0" + fi + + prepare_profile_invoke "$profile_id" "$output_file" propose_upgrade \ + --new_wasm_hash "$wasm_hash" \ + --new_version "$new_version" + echo "Expected profile version: $new_version" + echo "Expected profile WASM: $wasm_hash" +} + +cmd_verify_proposed_upgrade_profile() { + local wasm="${1:-}" expected_version="${2:-}" + [ -n "$wasm" ] || \ + err "usage: verify-proposed-upgrade-profile " + [ -n "$expected_version" ] || \ + err "usage: verify-proposed-upgrade-profile " + [ -f "$wasm" ] || err "wasm file not found: $wasm" + assert_version_argument "profile upgrade" "$expected_version" "$SDK27_PROFILE_VERSION" + require_env ADMIN_SOURCE + require_cli + ensure_deployments_dir + + local profile_id events_id expected_hash + profile_id="$(profile_contract_id)" + events_id="$(events_contract_id)" + assert_upgrade_governance_stable "$profile_id" "$events_id" + expected_hash="$(hash_wasm "$wasm")" + assert_sdk27_profile_artifact "$wasm" "$expected_version" + assert_events_version "$events_id" "1.1.0" + assert_events_paused "$events_id" + assert_no_events_exist "$events_id" + assert_no_pending_events_upgrade "$events_id" + assert_pending_profile_upgrade "$profile_id" "$expected_version" "$expected_hash" + assert_profile_paused "$profile_id" + if [ "$expected_version" = "1.2.0" ]; then + assert_profile_version "$profile_id" "1.1.0" + fi + + append_upgrade_log "profile-proposal-verified" "$expected_version" "$expected_hash" + ok "Queued profile upgrade matches the expected version and exact WASM." +} + +cmd_prepare_apply_upgrade_profile() { + local wasm="${1:-}" expected_version="${2:-}" output_file="${3:-}" + [ -n "$wasm" ] || \ + err "usage: prepare-apply-upgrade-profile " + [ -n "$expected_version" ] || \ + err "usage: prepare-apply-upgrade-profile " + [ -n "$output_file" ] || \ + err "usage: prepare-apply-upgrade-profile " + [ -f "$wasm" ] || err "wasm file not found: $wasm" + assert_version_argument "profile upgrade" "$expected_version" "$SDK27_PROFILE_VERSION" + require_env ADMIN_SOURCE + require_cli + + local profile_id events_id expected_hash + profile_id="$(profile_contract_id)" + events_id="$(events_contract_id)" + assert_upgrade_governance_stable "$profile_id" "$events_id" + expected_hash="$(hash_wasm "$wasm")" + assert_sdk27_profile_artifact "$wasm" "$expected_version" + assert_events_version "$events_id" "1.1.0" + assert_events_paused "$events_id" + assert_no_events_exist "$events_id" + assert_pending_events_upgrade \ + "$events_id" "$SDK27_EVENTS_VERSION" "$SDK27_EVENTS_WASM_HASH" + assert_pending_profile_upgrade "$profile_id" "$expected_version" "$expected_hash" + assert_profile_paused "$profile_id" + if [ "$expected_version" = "1.2.0" ]; then + assert_profile_version "$profile_id" "1.1.0" + fi + + prepare_profile_invoke "$profile_id" "$output_file" apply_upgrade +} + +cmd_prepare_migrate_upgrade_profile() { + local expected_version="${1:-}" output_file="${2:-}" + [ -n "$expected_version" ] || \ + err "usage: prepare-migrate-upgrade-profile " + [ -n "$output_file" ] || \ + err "usage: prepare-migrate-upgrade-profile " + assert_version_argument "profile upgrade" "$expected_version" "$SDK27_PROFILE_VERSION" + require_env ADMIN_SOURCE + require_cli + + local profile_id events_id migrated_version + profile_id="$(profile_contract_id)" + events_id="$(events_contract_id)" + assert_upgrade_governance_stable "$profile_id" "$events_id" + assert_events_version "$events_id" "1.5.0" + assert_events_sdk27_wasm_live "$events_id" + assert_events_paused "$events_id" + assert_no_pending_events_upgrade "$events_id" + assert_profile_version "$profile_id" "$expected_version" + if [ "$expected_version" = "$SDK27_PROFILE_VERSION" ]; then + assert_profile_sdk27_wasm_live "$profile_id" + fi + assert_profile_paused "$profile_id" + assert_no_pending_profile_upgrade "$profile_id" + + migrated_version="$(profile_read "$profile_id" get_migrated_to_version | jq -r '. // empty')" || \ + err "could not read profile migrated-to version" + [ "$migrated_version" != "$expected_version" ] || \ + err "profile migration marker is already $expected_version; do not replay migrate" + prepare_profile_invoke "$profile_id" "$output_file" migrate +} + +cmd_prepare_cancel_upgrade_profile() { + local output_file="${1:-}" + [ -n "$output_file" ] || \ + err "usage: prepare-cancel-upgrade-profile " + require_env ADMIN_SOURCE + require_cli + + local profile_id events_id pending_version + profile_id="$(profile_contract_id)" + events_id="$(events_contract_id)" + assert_upgrade_governance_stable "$profile_id" "$events_id" + pending_version="$(profile_read "$profile_id" get_pending_upgrade | jq -r '.new_version // empty')" || \ + err "could not read pending profile upgrade" + [ -n "$pending_version" ] || err "no profile upgrade is pending" + if [ "$pending_version" = "1.2.0" ]; then + assert_profile_version "$profile_id" "1.1.0" + assert_profile_paused "$profile_id" + assert_events_version "$events_id" "1.1.0" + assert_events_paused "$events_id" + assert_no_events_exist "$events_id" + fi + prepare_profile_invoke "$profile_id" "$output_file" cancel_pending_upgrade + echo "Cancelling queued profile version: $pending_version" + echo "This transaction does not unpause the profile contract." +} + +cmd_prepare_unpause_profile() { + local expected_version="${1:-}" output_file="${2:-}" + [ -n "$expected_version" ] || \ + err "usage: prepare-unpause-profile " + [ -n "$output_file" ] || \ + err "usage: prepare-unpause-profile " + assert_unpause_version_argument "profile" "$expected_version" "$SDK27_PROFILE_VERSION" + require_env ADMIN_SOURCE + require_cli + + local profile_id events_id + profile_id="$(profile_contract_id)" + events_id="$(events_contract_id)" + assert_upgrade_governance_stable "$profile_id" "$events_id" + assert_profile_version "$profile_id" "$expected_version" + assert_profile_paused "$profile_id" + assert_no_pending_profile_upgrade "$profile_id" + if [ "$expected_version" = "1.2.0" ]; then + assert_profile_sdk27_wasm_live "$profile_id" + assert_profile_migrated_version "$profile_id" "$expected_version" + assert_events_version "$events_id" "1.5.0" + assert_events_sdk27_wasm_live "$events_id" + assert_events_paused "$events_id" + assert_no_pending_events_upgrade "$events_id" + assert_migrated_version "$events_id" "1.5.0" + elif [ "$expected_version" = "1.1.0" ]; then + assert_events_version "$events_id" "1.1.0" + assert_events_paused "$events_id" + assert_no_pending_events_upgrade "$events_id" + assert_no_events_exist "$events_id" + fi + prepare_profile_invoke "$profile_id" "$output_file" unpause +} + +cmd_verify_upgrade_profile() { + local wasm="${1:-}" expected_version="${2:-}" + [ -n "$wasm" ] || \ + err "usage: verify-upgrade-profile " + [ -n "$expected_version" ] || \ + err "usage: verify-upgrade-profile " + [ -f "$wasm" ] || err "wasm file not found: $wasm" + assert_version_argument "profile upgrade" "$expected_version" "$SDK27_PROFILE_VERSION" + require_env ADMIN_SOURCE + require_cli + ensure_deployments_dir + + local profile_id events_id expected_hash + profile_id="$(profile_contract_id)" + events_id="$(events_contract_id)" + assert_upgrade_governance_stable "$profile_id" "$events_id" + expected_hash="$(hash_wasm "$wasm")" + assert_sdk27_profile_artifact "$wasm" "$expected_version" + assert_profile_version "$profile_id" "$expected_version" + if [ "$expected_version" = "$SDK27_PROFILE_VERSION" ]; then + assert_profile_sdk27_wasm_live "$profile_id" + fi + assert_profile_migrated_version "$profile_id" "$expected_version" + assert_no_pending_profile_upgrade "$profile_id" + assert_profile_unpaused "$profile_id" + assert_events_version "$events_id" "1.5.0" + assert_events_sdk27_wasm_live "$events_id" + assert_migrated_version "$events_id" "1.5.0" + assert_no_pending_events_upgrade "$events_id" + assert_events_unpaused "$events_id" + + deployment_set profile_contract "$profile_id" + deployment_set profile_wasm_hash "$expected_hash" + deployment_set profile_version "$expected_version" + append_upgrade_log "profile-upgrade-verified" "$expected_version" "$expected_hash" + ok "Profile $expected_version is migrated, has no pending upgrade, and is unpaused." +} + cmd_upload_events_wasm() { local wasm="${1:-}" [ -n "$wasm" ] || err "usage: upload-events-wasm " @@ -474,6 +1041,7 @@ cmd_upload_events_wasm() { assert_events_version "$events_id" "1.1.0" assert_admin_source_matches "$events_id" expected_hash="$(hash_wasm "$wasm")" + assert_local_wasm_hash "events SDK 27" "$wasm" "$SDK27_EVENTS_WASM_HASH" uploaded_hash="$(stellar contract upload \ --source-account "$UPLOAD_SOURCE" \ --network "$NETWORK" \ @@ -492,8 +1060,13 @@ cmd_prepare_pause_events() { require_env ADMIN_SOURCE require_cli - local events_id + local events_id profile_id events_id="$(events_contract_id)" + profile_id="$(profile_contract_id)" + assert_upgrade_governance_stable "$profile_id" "$events_id" + assert_profile_version "$profile_id" "1.1.0" + assert_profile_unpaused "$profile_id" + assert_no_pending_profile_upgrade "$profile_id" assert_events_version "$events_id" "1.1.0" assert_events_unpaused "$events_id" assert_no_pending_events_upgrade "$events_id" @@ -510,18 +1083,25 @@ cmd_prepare_propose_upgrade_events() { [ -n "$output_file" ] || \ err "usage: prepare-propose-upgrade-events " [ -f "$wasm" ] || err "wasm file not found: $wasm" + assert_version_argument "events upgrade" "$new_version" "$SDK27_EVENTS_VERSION" require_env ADMIN_SOURCE require_cli - local events_id wasm_hash + local events_id profile_id wasm_hash events_id="$(events_contract_id)" + profile_id="$(profile_contract_id)" + assert_upgrade_governance_stable "$profile_id" "$events_id" wasm_hash="$(hash_wasm "$wasm")" + assert_sdk27_events_artifact "$wasm" "$new_version" assert_no_pending_events_upgrade "$events_id" - if [ "$new_version" = "1.2.0" ]; then + if is_zero_event_guarded_target "$new_version"; then assert_events_version "$events_id" "1.1.0" assert_events_paused "$events_id" assert_no_events_exist "$events_id" + if [ "$new_version" = "1.5.0" ]; then + assert_profile_sdk27_staged_or_applied "$profile_id" + fi fi prepare_events_invoke "$events_id" "$output_file" propose_upgrade \ @@ -538,19 +1118,26 @@ cmd_verify_proposed_upgrade_events() { [ -n "$expected_version" ] || \ err "usage: verify-proposed-upgrade-events " [ -f "$wasm" ] || err "wasm file not found: $wasm" + assert_version_argument "events upgrade" "$expected_version" "$SDK27_EVENTS_VERSION" require_env ADMIN_SOURCE require_cli ensure_deployments_dir - local events_id expected_hash + local events_id profile_id expected_hash events_id="$(events_contract_id)" + profile_id="$(profile_contract_id)" + assert_upgrade_governance_stable "$profile_id" "$events_id" expected_hash="$(hash_wasm "$wasm")" + assert_sdk27_events_artifact "$wasm" "$expected_version" assert_pending_events_upgrade "$events_id" "$expected_version" "$expected_hash" - if [ "$expected_version" = "1.2.0" ]; then + if is_zero_event_guarded_target "$expected_version"; then assert_events_version "$events_id" "1.1.0" assert_events_paused "$events_id" assert_no_events_exist "$events_id" + if [ "$expected_version" = "1.5.0" ]; then + assert_profile_sdk27_staged_or_applied "$profile_id" + fi fi append_upgrade_log "proposal-verified" "$expected_version" "$expected_hash" @@ -566,18 +1153,25 @@ cmd_prepare_apply_upgrade_events() { [ -n "$output_file" ] || \ err "usage: prepare-apply-upgrade-events " [ -f "$wasm" ] || err "wasm file not found: $wasm" + assert_version_argument "events upgrade" "$expected_version" "$SDK27_EVENTS_VERSION" require_env ADMIN_SOURCE require_cli - local events_id expected_hash + local events_id profile_id expected_hash events_id="$(events_contract_id)" + profile_id="$(profile_contract_id)" + assert_upgrade_governance_stable "$profile_id" "$events_id" expected_hash="$(hash_wasm "$wasm")" + assert_sdk27_events_artifact "$wasm" "$expected_version" assert_pending_events_upgrade "$events_id" "$expected_version" "$expected_hash" - if [ "$expected_version" = "1.2.0" ]; then + if is_zero_event_guarded_target "$expected_version"; then assert_events_version "$events_id" "1.1.0" assert_events_paused "$events_id" assert_no_events_exist "$events_id" + if [ "$expected_version" = "1.5.0" ]; then + assert_profile_sdk27_applied_paused "$profile_id" + fi fi prepare_events_invoke "$events_id" "$output_file" apply_upgrade @@ -589,21 +1183,28 @@ cmd_prepare_migrate_upgrade_events() { err "usage: prepare-migrate-upgrade-events " [ -n "$output_file" ] || \ err "usage: prepare-migrate-upgrade-events " + assert_version_argument "events upgrade" "$expected_version" "$SDK27_EVENTS_VERSION" require_env ADMIN_SOURCE require_cli - local events_id migrated_version + local events_id profile_id migrated_version events_id="$(events_contract_id)" + profile_id="$(profile_contract_id)" + assert_upgrade_governance_stable "$profile_id" "$events_id" assert_events_version "$events_id" "$expected_version" - assert_no_pending_events_upgrade "$events_id" - if [ "$expected_version" = "1.2.0" ]; then - assert_events_paused "$events_id" + if [ "$expected_version" = "$SDK27_EVENTS_VERSION" ]; then + assert_events_sdk27_wasm_live "$events_id" fi + assert_no_pending_events_upgrade "$events_id" + assert_events_paused "$events_id" migrated_version="$(events_read "$events_id" get_migrated_to_version | jq -r '. // empty')" || \ err "could not read migrated-to version" [ "$migrated_version" != "$expected_version" ] || \ err "migration marker is already $expected_version; do not replay migrate" + if [ "$expected_version" = "1.5.0" ]; then + assert_profile_sdk27_migrated_paused "$profile_id" + fi prepare_events_invoke "$events_id" "$output_file" migrate } @@ -614,11 +1215,20 @@ cmd_prepare_cancel_upgrade_events() { require_env ADMIN_SOURCE require_cli - local events_id pending_version + local events_id profile_id pending_version events_id="$(events_contract_id)" + profile_id="$(profile_contract_id)" + assert_upgrade_governance_stable "$profile_id" "$events_id" pending_version="$(events_read "$events_id" get_pending_upgrade | jq -r '.new_version // empty')" || \ err "could not read pending events upgrade" [ -n "$pending_version" ] || err "no events upgrade is pending" + if [ "$pending_version" = "1.5.0" ]; then + assert_events_version "$events_id" "1.1.0" + assert_events_paused "$events_id" + assert_no_events_exist "$events_id" + assert_profile_version "$profile_id" "1.1.0" + assert_profile_paused "$profile_id" + fi prepare_events_invoke "$events_id" "$output_file" cancel_pending_upgrade echo "Cancelling queued version: $pending_version" echo "This transaction does not unpause the contract." @@ -630,17 +1240,31 @@ cmd_prepare_unpause_events() { err "usage: prepare-unpause-events " [ -n "$output_file" ] || \ err "usage: prepare-unpause-events " + assert_unpause_version_argument "events" "$expected_version" "$SDK27_EVENTS_VERSION" require_env ADMIN_SOURCE require_cli - local events_id + local events_id profile_id events_id="$(events_contract_id)" + profile_id="$(profile_contract_id)" + assert_upgrade_governance_stable "$profile_id" "$events_id" assert_events_version "$events_id" "$expected_version" + if [ "$expected_version" = "$SDK27_EVENTS_VERSION" ]; then + assert_events_sdk27_wasm_live "$events_id" + fi assert_events_paused "$events_id" assert_no_pending_events_upgrade "$events_id" - if [ "$expected_version" = "1.2.0" ]; then + if is_zero_event_guarded_target "$expected_version"; then assert_migrated_version "$events_id" "$expected_version" fi + if [ "$expected_version" = "1.5.0" ]; then + assert_profile_sdk27_ready "$profile_id" + elif [ "$expected_version" = "1.1.0" ]; then + assert_no_events_exist "$events_id" + assert_profile_version "$profile_id" "1.1.0" + assert_profile_unpaused "$profile_id" + assert_no_pending_profile_upgrade "$profile_id" + fi prepare_events_invoke "$events_id" "$output_file" unpause } @@ -651,17 +1275,27 @@ cmd_verify_upgrade_events() { [ -n "$expected_version" ] || \ err "usage: verify-upgrade-events " [ -f "$wasm" ] || err "wasm file not found: $wasm" + assert_version_argument "events upgrade" "$expected_version" "$SDK27_EVENTS_VERSION" require_env ADMIN_SOURCE require_cli ensure_deployments_dir - local events_id expected_hash + local events_id profile_id expected_hash events_id="$(events_contract_id)" + profile_id="$(profile_contract_id)" + assert_upgrade_governance_stable "$profile_id" "$events_id" expected_hash="$(hash_wasm "$wasm")" + assert_sdk27_events_artifact "$wasm" "$expected_version" assert_events_version "$events_id" "$expected_version" + if [ "$expected_version" = "$SDK27_EVENTS_VERSION" ]; then + assert_events_sdk27_wasm_live "$events_id" + fi assert_migrated_version "$events_id" "$expected_version" assert_no_pending_events_upgrade "$events_id" assert_events_unpaused "$events_id" + if [ "$expected_version" = "1.5.0" ]; then + assert_profile_sdk27_ready "$profile_id" + fi deployment_set events_contract "$events_id" deployment_set events_wasm_hash "$expected_hash" @@ -674,17 +1308,34 @@ cmd_upgrade_status() { require_env ADMIN_SOURCE require_cli - local events_id + local events_id profile_id events_id="$(events_contract_id)" + profile_id="$(profile_contract_id)" echo "events.version:" events_read "$events_id" version + echo "events.wasm_hash:" + deployed_wasm_hash "$events_id" echo "events.is_paused:" events_read "$events_id" is_paused echo "events.get_pending_upgrade:" events_read "$events_id" get_pending_upgrade echo "events.get_migrated_to_version:" events_read "$events_id" get_migrated_to_version + echo "profile.version:" + profile_read "$profile_id" version + echo "profile.wasm_hash:" + deployed_wasm_hash "$profile_id" + echo "profile.is_paused:" + profile_read "$profile_id" is_paused + echo "profile.get_pending_upgrade:" + profile_read "$profile_id" get_pending_upgrade + echo "profile.get_events_contract:" + profile_read "$profile_id" get_events_contract + echo "profile.get_pending_events_contract:" + profile_read "$profile_id" get_pending_events_contract + echo "profile.get_migrated_to_version:" + profile_read "$profile_id" get_migrated_to_version } cmd_verify() { @@ -730,30 +1381,57 @@ case "$ACTION" in "rotate-admin") cmd_rotate_admin "$@" ;; + "upload-profile-wasm") + cmd_upload_profile_wasm "$@" + ;; "upload-events-wasm") cmd_upload_events_wasm "$@" ;; + "prepare-pause-profile") + cmd_prepare_pause_profile "$@" + ;; "prepare-pause-events") cmd_prepare_pause_events "$@" ;; + "prepare-propose-upgrade-profile") + cmd_prepare_propose_upgrade_profile "$@" + ;; "prepare-propose-upgrade-events") cmd_prepare_propose_upgrade_events "$@" ;; + "verify-proposed-upgrade-profile") + cmd_verify_proposed_upgrade_profile "$@" + ;; "verify-proposed-upgrade-events") cmd_verify_proposed_upgrade_events "$@" ;; + "prepare-apply-upgrade-profile") + cmd_prepare_apply_upgrade_profile "$@" + ;; "prepare-apply-upgrade-events") cmd_prepare_apply_upgrade_events "$@" ;; + "prepare-migrate-upgrade-profile") + cmd_prepare_migrate_upgrade_profile "$@" + ;; "prepare-migrate-upgrade-events") cmd_prepare_migrate_upgrade_events "$@" ;; + "prepare-cancel-upgrade-profile") + cmd_prepare_cancel_upgrade_profile "$@" + ;; "prepare-cancel-upgrade-events") cmd_prepare_cancel_upgrade_events "$@" ;; + "prepare-unpause-profile") + cmd_prepare_unpause_profile "$@" + ;; "prepare-unpause-events") cmd_prepare_unpause_events "$@" ;; + "verify-upgrade-profile") + cmd_verify_upgrade_profile "$@" + ;; "verify-upgrade-events") cmd_verify_upgrade_events "$@" ;; @@ -763,7 +1441,8 @@ case "$ACTION" in "upgrade-events") err "upgrade-events is removed; use the guarded prepare/sign/send flow in mainnet-deploy-runbook.md" ;; - "propose-upgrade-events"|"apply-upgrade-events"|"migrate-upgrade-events"|"cancel-upgrade-events"|"unpause-events") + "propose-upgrade-events"|"apply-upgrade-events"|"migrate-upgrade-events"|"cancel-upgrade-events"|"unpause-events"|\ + "propose-upgrade-profile"|"apply-upgrade-profile"|"migrate-upgrade-profile"|"cancel-upgrade-profile"|"unpause-profile") err "$ACTION cannot safely submit as the 2-of-3 admin; use the matching prepare-* action, sign sequentially, and send" ;; "verify") diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index f91114e..234b447 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -40,8 +40,8 @@ stellar --version # confirm >= 26.0.0 rustup target add wasm32v1-none # verify build still passes locally -cd contracts/events && stellar contract build && cd ../.. -cd contracts/profile && stellar contract build && cd ../.. +cd contracts/events && stellar contract build --locked && cd ../.. +cd contracts/profile && stellar contract build --locked && cd ../.. ``` ### Create the admin identity (per network) diff --git a/docs/contract-ops-runbook.md b/docs/contract-ops-runbook.md index 2d1ec67..6bd52d4 100644 --- a/docs/contract-ops-runbook.md +++ b/docs/contract-ops-runbook.md @@ -69,7 +69,7 @@ This mirrors the mainnet sequence end to end on throwaway testnet contracts. Wor ### 4.1 Build ```bash cd boundless-contract -stellar contract build +stellar contract build --locked # → target/wasm32v1-none/release/boundless_{events,profile}.wasm ``` diff --git a/docs/mainnet-deploy-runbook.md b/docs/mainnet-deploy-runbook.md index 55a4d79..15c1011 100644 --- a/docs/mainnet-deploy-runbook.md +++ b/docs/mainnet-deploy-runbook.md @@ -1,7 +1,7 @@ # Mainnet deployment runbook **For:** the contracts engineer + the admin multi-sig signers. -**Last updated:** 2026-07-17 (revised for the 1.1.0 → 1.2.0 cancellation-liveness upgrade guard). +**Last updated:** 2026-07-24 (SDK 23 to SDK 27 storage compatibility result). This is the cold-deploy procedure for Boundless contracts on Stellar mainnet. Follow it in order. Do not skip the verification steps. Every transaction below is irreversible. @@ -93,7 +93,7 @@ export USDC_SAC=CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC ```bash cd boundless-contract -stellar contract build +stellar contract build --locked # Deploy boundless-profile first; events depends on its address. PROFILE_WASM=target/wasm32v1-none/release/boundless_profile.wasm @@ -166,7 +166,7 @@ Verify the version label was written by the constructor: ```bash stellar contract invoke --network "$STELLAR_NETWORK" --source $INITIAL_ADMIN_KEY --id $PROFILE_ID -- version stellar contract invoke --network "$STELLAR_NETWORK" --source $INITIAL_ADMIN_KEY --id $EVENTS_ID -- version -# For this tree: profile returns "1.1.0" and events returns "1.2.0". +# For this tree: profile returns "1.2.0" and events returns "1.5.0". # For a tagged release, compare each result with that package's INITIAL_VERSION. ``` @@ -330,7 +330,7 @@ Sign `/tmp/events-emergency-pause.xdr` sequentially with two signers, submit wit Pause stops event-lifecycle mutations guarded by `require_not_paused`, including `create_event`, `add_funds`, `propose_manager`, `accept_manager`, `cancel_pending_manager`, `start_cancel`, `process_cancel_batch`, `finalize_cancel`, `select_winners`, `claim_milestone`, `apply_to_bounty`, `withdraw_application`, `submit`, and `withdraw_submission`. Reads continue. -Pause intentionally does **not** block admin recovery and governance entrypoints such as `propose_upgrade`, `apply_upgrade`, `cancel_pending_upgrade`, `migrate`, configuration changes, token-list changes, admin rotation, or `unpause`. This is why the 1.1.0 → 1.2.0 upgrade can remain paused through its timelock. +Pause intentionally does **not** block admin recovery and governance entrypoints such as `propose_upgrade`, `apply_upgrade`, `cancel_pending_upgrade`, `migrate`, configuration changes, token-list changes, admin rotation, or `unpause`. This is why the coordinated SDK 27 upgrade can remain paused through its timelock. Unpausing requires full quorum (see `admin-custody-policy.md` Section 4). @@ -361,7 +361,7 @@ Unpausing requires full quorum (see `admin-custody-policy.md` Section 4). ## 7. Upgrades (timelocked; H6) -The 2026-06 audit replaced the immediate-effect `upgrade(wasm_hash)` admin call with a three-step timelock flow. Use `deploy_mainnet.sh` for mainnet; it verifies the queued version and exact WASM hash and provides the special 1.1.0 → 1.2.0 zero-event guard. Direct contract calls below are only for diagnosis. +The 2026-06 audit replaced the immediate-effect `upgrade(wasm_hash)` admin call with a three-step timelock flow. Use `deploy_mainnet.sh` for mainnet; it verifies the queued version and exact WASM hash for both contracts and preserves the zero-event guard through the SDK 27 timelock. Direct contract calls below are only for diagnosis. ### 7.1 Constants @@ -372,92 +372,200 @@ The 2026-06 audit replaced the immediate-effect `upgrade(wasm_hash)` admin call These are in `contracts/events/src/admin.rs` and `contracts/profile/src/admin.rs`. Verify against the live wasm before timing a window. -### 7.2 Propose +### SDK 27 compatibility gate -For the 1.1.0 → 1.2.0 upgrade, the zero-event assumption must remain true for the entire timelock. Set the live contract ID explicitly if `deployments/mainnet.json` is not present. `ADMIN_SOURCE` is the admin multi-sig G-address, not a secret or single-signer alias. `UPLOAD_SOURCE` is a separate funded, signable CLI identity; uploading WASM does not require contract-admin authority. +The suite upgrades the exact mainnet `1.1.0` WASMs and ledger `63617727` +snapshot, plus synthetic SDK 23 rows for every persisted layout, through H6 +to the pinned events `1.5.0` and profile `1.2.0` WASMs. It then exercises +configuration, governance, event reads, winners, submissions, contributions, +cancellation, prize claims, earnings, indexes, cursors, and legacy replay +markers. Fixture hashes, provenance, and regeneration are recorded in +`docs/sdk27-compatibility-fixtures.md`. + +Before any SDK 27 mainnet proposal: + +```bash +./scripts/test-sdk27-compat.sh +``` + +CI must have rebuilt the contracts and matched the pinned fixtures. Abort if a +fixture hash differs, a compatibility test fails, or the zero-event check no +longer returns `EventNotFound`. The SDK 27 contracts retain a read fallback for +legacy temporary `OpSeen(BytesN<32>)` rows; new markers use the domain-scoped +key. + +Sections 7.2 through 7.5 are the executable SDK 27 procedure for events `1.1.0` → `1.5.0` and profile `1.1.0` → `1.2.0`. +The mainnet script intentionally rejects other upgrade target versions. A +future release must update its reviewed version/hash pins and compatibility +fixtures before preparing a proposal. + +### 7.2 Freeze and propose SDK 27 + +This is a coordinated two-contract upgrade. Keep both contracts paused from before either proposal until both migrations and post-upgrade reads have succeeded. Set the live contract IDs explicitly if `deployments/mainnet.json` is unavailable. `ADMIN_SOURCE` is the admin multi-sig G-address, not a secret or single-signer alias. `UPLOAD_SOURCE` is a separate funded, signable CLI identity; uploading WASM does not require contract-admin authority. + +Freeze governance changes for the whole window: do not call `set_admin`, +`accept_admin`, `propose_events_contract`, `accept_events_contract`, or +`cancel_pending_events_contract` on either contract while upgrade XDRs are +being prepared, signed, or submitted. Before every signed submission, rerun +the corresponding preparation or verification command against current ledger +state. The script requires both current admins to equal `ADMIN_SOURCE`, the +profile's active events binding to equal `EVENTS_ID`, and +`get_pending_events_contract` to be `null`. ```bash export EVENTS_ID=CCFVEGOQJEM47LRAJU2LHEK4KTL5VYN7AOGZ2HH2GNHAMXTILNMMJGQZ +export PROFILE_ID=CD3KH4OE7HDHHHUYFX3U4L7NLIILMXAY6HM5FEH2UH6UBOKX4HDNE3PC export ADMIN_SOURCE=$MULTISIG_ADMIN_ADDRESS export UPLOAD_SOURCE=boundless-upgrade-uploader -stellar contract build -EVENTS_WASM=target/wasm32v1-none/release/boundless_events.wasm +./scripts/test-sdk27-compat.sh + +EVENTS_WASM=contracts/compatibility/fixtures/events-1.5.0-sdk27.wasm +PROFILE_WASM=contracts/compatibility/fixtures/profile-1.2.0-sdk27.wasm -# Upload the exact already-built artifact before starting downtime. +# Upload the exact compatibility-tested artifacts before downtime. +./deploy_mainnet.sh upload-profile-wasm "$PROFILE_WASM" ./deploy_mainnet.sh upload-events-wasm "$EVENTS_WASM" -# Guard the current 1.1.0 zero-event state and prepare the pause transaction. -./deploy_mainnet.sh prepare-pause-events /tmp/events-1.2.0-pause.xdr +# Freeze events first. This command only prepares XDR. +./deploy_mainnet.sh prepare-pause-events /tmp/events-1.5.0-pause.xdr ``` -Sign `/tmp/events-1.2.0-pause.xdr` sequentially with the required quorum and submit it with `stellar tx send`. Then: +Sign and submit the events pause XDR. Only after it lands, prepare, sign, and submit the profile pause: + +```bash +./deploy_mainnet.sh prepare-pause-profile /tmp/profile-1.2.0-pause.xdr +``` + +Never prepare the next admin XDR before the prior one lands; each uses the multi-sig source account's current sequence. Then: ```bash ./deploy_mainnet.sh upgrade-status -# is_paused must be true. +# Both is_paused values must be true, both versions must be 1.1.0, +# both pending-upgrade values must be null, profile.get_events_contract must +# equal EVENTS_ID, and profile.get_pending_events_contract must be null. +``` + +Prepare the profile proposal: + +```bash +./deploy_mainnet.sh prepare-propose-upgrade-profile \ + "$PROFILE_WASM" "1.2.0" /tmp/profile-1.2.0-propose.xdr +``` + +Sign and submit it, then verify what landed: -# Recheck version + pause + zero-event state, then prepare the proposal. +```bash +./deploy_mainnet.sh verify-proposed-upgrade-profile "$PROFILE_WASM" "1.2.0" +``` + +Only then prepare, sign, and submit the events proposal: + +```bash ./deploy_mainnet.sh prepare-propose-upgrade-events \ - "$EVENTS_WASM" "1.2.0" /tmp/events-1.2.0-propose.xdr + "$EVENTS_WASM" "1.5.0" /tmp/events-1.5.0-propose.xdr ``` -Sign and submit `/tmp/events-1.2.0-propose.xdr`, then verify what actually landed: +Verify the events proposal and inspect both contracts: ```bash -./deploy_mainnet.sh verify-proposed-upgrade-events "$EVENTS_WASM" "1.2.0" +./deploy_mainnet.sh verify-proposed-upgrade-events "$EVENTS_WASM" "1.5.0" ./deploy_mainnet.sh upgrade-status ``` The guarded preparation and verification: -- requires the live version to be `1.1.0` and no other upgrade to be pending; -- requires the pause transaction to have landed and `is_paused` to read back as `true`; -- derives the first possible event ID from `id_base` and requires `get_event` to fail specifically with `EventNotFound`; -- uploads without re-optimizing and requires the returned hash to equal the local file's SHA-256; -- builds and simulates the admin transaction without attempting to bypass the 2-of-3 signing flow; -- reads back both the queued version and exact WASM hash after the signed proposal lands. +- requires both live versions to be `1.1.0`, both contracts to be paused, and no conflicting upgrade to be pending; +- rechecks both active admins, the profile-to-events binding, and the absence of a pending profile events-contract rotation at every prepared or verified upgrade step; +- derives the first possible event ID from `id_base` and requires `get_event` to fail specifically with `EventNotFound` before the events pause, proposal, and apply; +- requires Stellar CLI `27.0.0` and pins the local events and profile files to the exact compatibility-tested SDK 27 hashes; +- uploads without re-optimizing and requires each returned hash to equal the local file's SHA-256; +- builds and simulates every admin transaction without bypassing the 2-of-3 signing flow; +- reads back each queued version and exact WASM hash after the signed proposals land; +- queries each live contract's executable hash after apply and rechecks it before migration, unpause, and final verification. -The contract remains paused after proposal. Do not unpause during the timelock: `is_paused == true` is what prevents a new event from invalidating the zero-event snapshot before apply. If the event check succeeds or returns any ambiguous RPC error, the script aborts fail-closed. Do not apply 1.2.0; implement a paginated legacy-total migration instead. +Do not unpause either contract during the timelock. If `get_event` finds an +event or returns anything other than `EventNotFound`, abort without applying +either upgrade and investigate. Publish both `proposed_at_ledger` and +`available_at_ledger` values. Apply only after the later +`available_at_ledger` value has been reached. -Publish the `proposed_at` ledger sequence and `available_at` ledger to the status page so the community has a window to inspect the new wasm before it lands. +### 7.3 Apply, migrate, verify, and unpause -### 7.3 Apply +Keep the exact proposed artifacts. Apply profile first while both contracts remain paused: -Wait for the current ledger sequence to reach `available_at_ledger`. Keep the exact proposed WASM artifact. On day-of: +```bash +./deploy_mainnet.sh upgrade-status +./deploy_mainnet.sh prepare-apply-upgrade-profile \ + "$PROFILE_WASM" "1.2.0" /tmp/profile-1.2.0-apply.xdr +``` + +Sign and submit the profile apply XDR. Confirm profile is `1.2.0`, has no pending proposal, and remains paused. Then prepare, sign, and submit events apply: ```bash ./deploy_mainnet.sh upgrade-status ./deploy_mainnet.sh prepare-apply-upgrade-events \ - "$EVENTS_WASM" "1.2.0" /tmp/events-1.2.0-apply.xdr + "$EVENTS_WASM" "1.5.0" /tmp/events-1.5.0-apply.xdr ``` -Before preparing apply, the script rechecks the local WASM hash against the queued hash, current version, `is_paused == true`, and the zero-event invariant. Sign and submit `/tmp/events-1.2.0-apply.xdr`, then read status. It must show version `1.2.0`, no pending proposal, and still paused. +Before preparing either apply transaction, the script rechecks the pinned +local artifact, queued hash, current versions, pause state, both admins, the +profile-to-events binding, and the absence of a pending events-contract +rotation. Before events apply it also rechecks the zero-event invariant and +the profile's live executable hash. After each signed apply lands, the next +guarded command queries the contract's live executable hash and requires the +exact pinned SDK 27 value. Status must show events `1.5.0`, profile `1.2.0`, +no pending proposals, the original profile-to-events binding, no pending +rotation, and both contracts still paused. + +Migrate profile first, then events. Prepare, sign, submit, and verify each transaction before moving to the next: + +```bash +./deploy_mainnet.sh prepare-migrate-upgrade-profile \ + "1.2.0" /tmp/profile-1.2.0-migrate.xdr +``` -Prepare, sign, and submit migration: +Sign and submit the profile migration. After it lands: ```bash ./deploy_mainnet.sh upgrade-status + ./deploy_mainnet.sh prepare-migrate-upgrade-events \ - "1.2.0" /tmp/events-1.2.0-migrate.xdr + "1.5.0" /tmp/events-1.5.0-migrate.xdr ``` -After migration lands, prepare, sign, and submit unpause: +Sign and submit the events migration. After it lands: ```bash ./deploy_mainnet.sh upgrade-status -# get_migrated_to_version must be "1.2.0". +# Migrated-to values must be profile 1.2.0 and events 1.5.0. +``` + +Unpause profile first so events cannot accept work while its profile dependency is frozen. After the profile unpause lands, unpause events: + +```bash +./deploy_mainnet.sh prepare-unpause-profile \ + "1.2.0" /tmp/profile-1.2.0-unpause.xdr +``` + +Sign and submit the profile unpause. After it lands: + +```bash ./deploy_mainnet.sh prepare-unpause-events \ - "1.2.0" /tmp/events-1.2.0-unpause.xdr + "1.5.0" /tmp/events-1.5.0-unpause.xdr ``` -Only after the signed unpause lands: +Sign and submit the events unpause. + +Only after both signed unpause transactions land: ```bash -./deploy_mainnet.sh verify-upgrade-events "$EVENTS_WASM" "1.2.0" +./deploy_mainnet.sh verify-upgrade-profile "$PROFILE_WASM" "1.2.0" +./deploy_mainnet.sh verify-upgrade-events "$EVENTS_WASM" "1.5.0" +./deploy_mainnet.sh upgrade-status ``` -The final verification requires version `1.2.0`, migration marker `1.2.0`, no pending proposal, and `is_paused == false` before updating the local deployment record. +Final verification requires the exact local and live executable hashes, expected versions and migration markers, no pending proposals or events-contract rotation, the original profile-to-events binding, both expected admins, and both pause flags to be `false` before updating the deployment records. Common contract errors: @@ -498,35 +606,33 @@ Expiry is 7 days (`PENDING_EVENTS_CONTRACT_TTL_LEDGERS = 120_960`). ### 7.5 Recovery -If a propose was wrong but not yet applied: +Before either apply lands, cancel each affected proposal in profile-then-events +order, signing and submitting each XDR before preparing the next: ```bash +./deploy_mainnet.sh prepare-cancel-upgrade-profile \ + /tmp/profile-1.2.0-cancel.xdr ./deploy_mainnet.sh prepare-cancel-upgrade-events \ - /tmp/events-1.2.0-cancel.xdr + /tmp/events-1.5.0-cancel.xdr ``` -Sign and submit the cancel XDR. Cancellation deliberately leaves the pause state unchanged. Either propose the corrected artifact while still paused, or inspect `upgrade-status` and, only when abandoning the upgrade, prepare/sign/send an unpause for the still-live version: +Only prepare a cancel for a contract with a pending proposal. Cancellation +leaves both contracts paused. Either restart the proposal sequence or, when +abandoning the upgrade, unpause profile and then events, again submitting each +XDR before preparing the next: ```bash +./deploy_mainnet.sh prepare-unpause-profile \ + "1.1.0" /tmp/profile-abandon-upgrade-unpause.xdr ./deploy_mainnet.sh prepare-unpause-events \ "1.1.0" /tmp/events-abandon-upgrade-unpause.xdr ``` -`apply_upgrade` itself is atomic: a failed apply leaves the old WASM and pending proposal in place. Regenerate the prepared apply XDR because simulated transactions have time bounds. Migration is a separate transaction. If apply lands but migration or final verification fails, 1.2.0 remains paused and the pending proposal is already cleared. Recover with: - -```bash -./deploy_mainnet.sh upgrade-status -./deploy_mainnet.sh prepare-migrate-upgrade-events \ - "1.2.0" /tmp/events-1.2.0-migrate-recovery.xdr -# Sign and submit the migration XDR. -./deploy_mainnet.sh upgrade-status -./deploy_mainnet.sh prepare-unpause-events \ - "1.2.0" /tmp/events-1.2.0-unpause-recovery.xdr -# Sign and submit the unpause XDR. -./deploy_mainnet.sh verify-upgrade-events "$EVENTS_WASM" "1.2.0" -``` - -`prepare-unpause-events` refuses to prepare a 1.2.0 unpause until its migration marker is exactly `1.2.0`. +`apply_upgrade` is atomic per contract: a failed apply leaves that contract's +old WASM and proposal in place. Once either apply lands, do not downgrade or +unpause. Run `upgrade-status`, complete the other apply, and resume Section 7.3 +from the first missing migration or verification step. The unpause commands +require the exact live WASM and migration markers. --- @@ -584,4 +690,4 @@ Cap math: at `MAX_REFUNDS_PER_BATCH = 25`, refunding 5_000 contributors takes 20 The paged-cancel automation worker lives in `boundless-nestjs` (see its own BACKLOG for the worker's status). The orchestrator's `beginStartCancel` kicks off step 1. Any account can sponsor steps 2 and 3, so an unavailable event manager cannot strand a cancellation already in progress. -For the 1.1.0 → 1.2.0 mainnet upgrade, `deploy_mainnet.sh` freezes the zero-event assumption: it pauses before proposal, verifies `is_paused == true` and `EventNotFound`, keeps the contract paused through the timelock, and repeats both checks immediately before apply. Any existing event or ambiguous RPC result aborts the upgrade. New events initialize `NonOwnerContributionTotal` at zero and `add_funds` maintains it in O(1). An older event with no contributors initializes the missing total lazily. If an older event already has contributors, `add_funds` and `start_cancel` fail with `CancellationTotalMissing` instead of using an incomplete total. +For the events `1.1.0` → `1.5.0` and profile `1.1.0` → `1.2.0` mainnet upgrade, `deploy_mainnet.sh` freezes the zero-event assumption: it pauses before proposal, verifies `is_paused == true` and `EventNotFound`, keeps the contract paused through the timelock, and repeats both checks immediately before apply. Any existing event or ambiguous RPC result aborts the upgrade. New events initialize `NonOwnerContributionTotal` at zero and `add_funds` maintains it in O(1). An older event with no contributors initializes the missing total lazily. If an older event already has contributors, `add_funds` and `start_cancel` fail with `CancellationTotalMissing` instead of using an incomplete total. diff --git a/docs/sdk27-compatibility-fixtures.md b/docs/sdk27-compatibility-fixtures.md new file mode 100644 index 0000000..f9b9c71 --- /dev/null +++ b/docs/sdk27-compatibility-fixtures.md @@ -0,0 +1,43 @@ +# SDK 27 compatibility fixture provenance + +Fixture hashes and source details are pinned in +`contracts/compatibility/fixtures/manifest.json`. + +## Mainnet ledger snapshot + +The snapshot is mainnet ledger `63617727`, timestamp `1784850182`, protocol +`27`. It contains all contract-data entries for the events and profile IDs in +the manifest: two contract instances, five profile rows, and three legacy +operation markers. Contract code is loaded separately. TTLs are normalized to +`u32::MAX` so rows do not expire while the host test advances the ledger. + +Recreate it with Stellar CLI 27.0.0: + +```bash +./scripts/capture-mainnet-compat-snapshot.sh /tmp/mainnet-state-63617727.json +cmp /tmp/mainnet-state-63617727.json \ + contracts/compatibility/fixtures/mainnet-state-63617727.json +``` + +The script uses SDF's mainnet history archive by default and rejects any +header, selection, count, or hash mismatch. Set `STELLAR_ARCHIVE_URL` to use +another complete mainnet archive. + +## WASM provenance and version checks + +The two `mainnet-*-1.1.0-sdk23.wasm` files were fetched from the live mainnet +contract IDs recorded in the manifest. Their SHA-256 values match the recorded +deployed hashes. + +The SDK 23 prize/cancellation fixture comes from commit `57aa591`. Its runtime +version is `1.3.0`, but its stale embedded contractmeta says `1.2.0`. Both are +recorded and asserted without changing the fixture. + +The SDK 27 events `1.5.0` and profile `1.2.0` files are the canonical release +artifacts built by GitHub Actions on Linux x86_64 with Rust and Stellar CLI +`27.0.0` pinned. CI rebuilds the source and requires exact hash matches. +Mainnet upgrades use these committed files, not a host-local rebuild. + +Synthetic compatibility cases use deterministic operation IDs so their +Soroban ledger snapshots are reproducible. CI reruns the suite and rejects any +snapshot diff. diff --git a/scripts/capture-mainnet-compat-snapshot.sh b/scripts/capture-mainnet-compat-snapshot.sh new file mode 100755 index 0000000..51f97a2 --- /dev/null +++ b/scripts/capture-mainnet-compat-snapshot.sh @@ -0,0 +1,111 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +FIXTURES="$ROOT/contracts/compatibility/fixtures" +MANIFEST="$FIXTURES/manifest.json" +OUTPUT="${1:-}" + +[ -n "$OUTPUT" ] || { + echo "usage: $0 " >&2 + exit 1 +} +[ ! -e "$OUTPUT" ] || { + echo "refusing to overwrite existing file: $OUTPUT" >&2 + exit 1 +} + +command -v stellar >/dev/null 2>&1 || { + echo "stellar CLI not on PATH" >&2 + exit 1 +} +command -v jq >/dev/null 2>&1 || { + echo "jq not on PATH" >&2 + exit 1 +} +stellar --version | head -n 1 | grep -q '^stellar 27\.0\.0 ' || { + echo "Stellar CLI 27.0.0 is required" >&2 + exit 1 +} + +LEDGER="$(jq -r '.snapshot.ledger' "$MANIFEST")" +TIMESTAMP="$(jq -r '.snapshot.timestamp' "$MANIFEST")" +PROTOCOL="$(jq -r '.snapshot.protocol' "$MANIFEST")" +NETWORK_ID="$(jq -r '.snapshot.network_id' "$MANIFEST")" +EVENTS_ID="$(jq -r '.snapshot.events_contract' "$MANIFEST")" +PROFILE_ID="$(jq -r '.snapshot.profile_contract' "$MANIFEST")" +EXPECTED_HASH="$(jq -r '.snapshot.sha256' "$MANIFEST")" +ARCHIVE_URL="${STELLAR_ARCHIVE_URL:-https://history.stellar.org/prd/core-live/core_live_001/}" +NETWORK_PASSPHRASE="Public Global Stellar Network ; September 2015" + +raw_snapshot="$(mktemp "${TMPDIR:-/tmp}/boundless-mainnet-snapshot.XXXXXX")" +normalized_snapshot="$(mktemp "${TMPDIR:-/tmp}/boundless-mainnet-normalized.XXXXXX")" +trap 'rm -f "$raw_snapshot" "$normalized_snapshot"' EXIT + +stellar snapshot create \ + --ledger "$LEDGER" \ + --output json \ + --out "$raw_snapshot" \ + --archive-url "$ARCHIVE_URL" \ + --network-passphrase "$NETWORK_PASSPHRASE" \ + --address "$EVENTS_ID" \ + --address "$PROFILE_ID" + +jq \ + --arg events "$EVENTS_ID" \ + --arg profile "$PROFILE_ID" \ + ' + .ledger_entries |= ( + map(select( + .entry.data.contract_data.contract == $events + or .entry.data.contract_data.contract == $profile + )) + | map(.live_until = 4294967295) + | sort_by( + .entry.data.contract_data.contract, + (.entry.data.contract_data.key | tojson) + ) + ) + ' "$raw_snapshot" > "$normalized_snapshot" + +jq -e \ + --argjson ledger "$LEDGER" \ + --argjson timestamp "$TIMESTAMP" \ + --argjson protocol "$PROTOCOL" \ + --arg network_id "$NETWORK_ID" \ + --arg events "$EVENTS_ID" \ + --arg profile "$PROFILE_ID" \ + ' + .sequence_number == $ledger + and .timestamp == $timestamp + and .protocol_version == $protocol + and .network_id == $network_id + and (.ledger_entries | length == 10) + and (.ledger_entries | all( + .live_until == 4294967295 + and (.entry.data.contract_data.contract == $events + or .entry.data.contract_data.contract == $profile) + )) + and ([.ledger_entries[] + | select(.entry.data.contract_data.key == "ledger_key_contract_instance")] + | length == 2) + ' "$normalized_snapshot" >/dev/null || { + echo "captured ledger does not match the pinned snapshot selection" >&2 + exit 1 +} + +mv "$normalized_snapshot" "$OUTPUT" + +if command -v sha256sum >/dev/null 2>&1; then + actual_hash="$(sha256sum "$OUTPUT" | awk '{print $1}')" +else + actual_hash="$(shasum -a 256 "$OUTPUT" | awk '{print $1}')" +fi + +echo "snapshot: $OUTPUT" +echo "sha256: $actual_hash" +[ "$actual_hash" = "$EXPECTED_HASH" ] || { + echo "expected: $EXPECTED_HASH" >&2 + echo "the regenerated snapshot differs from the reviewed fixture" >&2 + exit 1 +} diff --git a/scripts/deploy/deploy.sh b/scripts/deploy/deploy.sh index 9e90b9d..4f408f1 100755 --- a/scripts/deploy/deploy.sh +++ b/scripts/deploy/deploy.sh @@ -99,9 +99,9 @@ if [[ "$NETWORK" == "testnet" || "$NETWORK" == "futurenet" ]]; then BUILD_FEATURES="--features testnet" fi # shellcheck disable=SC2086 -( cd "$REPO_ROOT/contracts/events" && stellar contract build $BUILD_FEATURES ) +( cd "$REPO_ROOT/contracts/events" && stellar contract build --locked $BUILD_FEATURES ) # shellcheck disable=SC2086 -( cd "$REPO_ROOT/contracts/profile" && stellar contract build $BUILD_FEATURES ) +( cd "$REPO_ROOT/contracts/profile" && stellar contract build --locked $BUILD_FEATURES ) EVENTS_WASM="$REPO_ROOT/target/wasm32v1-none/release/boundless_events.wasm" PROFILE_WASM="$REPO_ROOT/target/wasm32v1-none/release/boundless_profile.wasm" diff --git a/scripts/deploy_and_upgrade.sh b/scripts/deploy_and_upgrade.sh index 016473c..bab5278 100755 --- a/scripts/deploy_and_upgrade.sh +++ b/scripts/deploy_and_upgrade.sh @@ -31,7 +31,7 @@ deploy_contract() { cd $CONTRACT_DIR || { echo -e "${RED}Failed to change directory to $CONTRACT_DIR${NC}"; exit 1; } # Use stellar contract build which handles the reference-types issue - stellar contract build + stellar contract build --locked # Deploy the contract echo "Deploying contract..." @@ -95,7 +95,7 @@ upgrade_contract() { cd $CONTRACT_DIR || { echo -e "${RED}Failed to change directory to $CONTRACT_DIR${NC}"; exit 1; } # Use stellar contract build which handles the reference-types issue - stellar contract build + stellar contract build --locked # Install the new WASM echo "Installing new WASM..." diff --git a/scripts/test-mainnet-upgrade-guards.sh b/scripts/test-mainnet-upgrade-guards.sh new file mode 100755 index 0000000..2b61371 --- /dev/null +++ b/scripts/test-mainnet-upgrade-guards.sh @@ -0,0 +1,222 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +TEST_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/boundless-mainnet-guards.XXXXXX")" +FAKE_BIN="$TEST_ROOT/bin" +SCRIPT="$TEST_ROOT/deploy_mainnet.sh" +EVENTS_WASM="$ROOT/contracts/compatibility/fixtures/events-1.5.0-sdk27.wasm" +PROFILE_WASM="$ROOT/contracts/compatibility/fixtures/profile-1.2.0-sdk27.wasm" +XDR_DIR="$TEST_ROOT/xdr" +OLD_EVENTS_HASH="2a8789bf5dd392b91ddfe6591072a0cb9fae70045abf379fd1dbbc2db4af1de3" +OLD_PROFILE_HASH="b9e3500cfb559781e68359655f37775c78afffc5a8436daca9d1d1f8e3569b02" +WRONG_HASH="0000000000000000000000000000000000000000000000000000000000000000" + +cleanup() { + rm -rf "$TEST_ROOT" +} +trap cleanup EXIT + +mkdir -p "$FAKE_BIN" "$XDR_DIR" +cp "$ROOT/scripts/testdata/mock-stellar" "$FAKE_BIN/stellar" +cp "$ROOT/deploy_mainnet.sh" "$SCRIPT" +chmod +x "$FAKE_BIN/stellar" "$SCRIPT" + +export PATH="$FAKE_BIN:$PATH" +export STELLAR_NETWORK=mainnet-review +export ADMIN_SOURCE=GADMIN +export EVENTS_ID=EVENTS +export PROFILE_ID=PROFILE + +hash_file() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | awk '{print $1}' + else + shasum -a 256 "$1" | awk '{print $1}' + fi +} + +events_wasm_hash="$(hash_file "$EVENTS_WASM")" +profile_wasm_hash="$(hash_file "$PROFILE_WASM")" +events_pending="{\"new_version\":\"1.5.0\",\"wasm_hash\":\"$events_wasm_hash\"}" +profile_pending="{\"new_version\":\"1.2.0\",\"wasm_hash\":\"$profile_wasm_hash\"}" +bad_events_pending="{\"new_version\":\"1.5.0\",\"wasm_hash\":\"$WRONG_HASH\"}" +bad_profile_pending="{\"new_version\":\"1.2.0\",\"wasm_hash\":\"$WRONG_HASH\"}" + +set_state() { + export FAKE_EVENTS_VERSION="$1" + export FAKE_EVENTS_PAUSED="$2" + export FAKE_EVENTS_PENDING="$3" + export FAKE_EVENTS_MIGRATED="$4" + export FAKE_PROFILE_VERSION="$5" + export FAKE_PROFILE_PAUSED="$6" + export FAKE_PROFILE_PENDING="$7" + export FAKE_PROFILE_MIGRATED="$8" + export FAKE_PROFILE_EVENTS_CONTRACT=EVENTS + export FAKE_PROFILE_EVENTS_PENDING=null + if [ "$FAKE_EVENTS_VERSION" = "1.5.0" ]; then + export FAKE_EVENTS_LIVE_HASH="$events_wasm_hash" + else + export FAKE_EVENTS_LIVE_HASH="$OLD_EVENTS_HASH" + fi + if [ "$FAKE_PROFILE_VERSION" = "1.2.0" ]; then + export FAKE_PROFILE_LIVE_HASH="$profile_wasm_hash" + else + export FAKE_PROFILE_LIVE_HASH="$OLD_PROFILE_HASH" + fi +} + +expect_ok() { + local label="$1" + shift + "$@" >/dev/null + printf 'PASS %s\n' "$label" +} + +expect_fail() { + local label="$1" + shift + if "$@" >/dev/null 2>&1; then + printf 'FAIL %s unexpectedly succeeded\n' "$label" >&2 + exit 1 + fi + printf 'PASS %s failed closed\n' "$label" +} + +set_state 1.1.0 false null null 1.1.0 false null null +export FAKE_PROFILE_EVENTS_PENDING='{"target":"ROTATED","proposed_at":100}' +expect_fail pause-events-with-pending-profile-rotation \ + "$SCRIPT" prepare-pause-events "$XDR_DIR/pending-rotation-pause-events.xdr" + +set_state 1.1.0 false null null 1.1.0 false null null +export FAKE_PROFILE_EVENTS_CONTRACT=OTHER_EVENTS +expect_fail pause-events-with-profile-binding-drift \ + "$SCRIPT" prepare-pause-events "$XDR_DIR/binding-drift-pause-events.xdr" + +set_state 1.1.0 false null null 1.1.0 false null null +expect_ok pause-events "$SCRIPT" prepare-pause-events "$XDR_DIR/pause-events.xdr" +expect_fail pause-profile-before-events \ + "$SCRIPT" prepare-pause-profile "$XDR_DIR/early-pause-profile.xdr" + +set_state 1.1.0 true null null 1.1.0 false null null +expect_ok pause-profile "$SCRIPT" prepare-pause-profile "$XDR_DIR/pause-profile.xdr" + +set_state 1.1.0 true null null 1.1.0 true null null +expect_ok propose-profile \ + "$SCRIPT" prepare-propose-upgrade-profile "$PROFILE_WASM" 1.2.0 "$XDR_DIR/propose-profile.xdr" +expect_fail propose-profile-with-events-artifact \ + "$SCRIPT" prepare-propose-upgrade-profile "$EVENTS_WASM" 1.2.0 "$XDR_DIR/wrong-profile-artifact.xdr" +expect_fail propose-profile-with-unpinned-version \ + "$SCRIPT" prepare-propose-upgrade-profile "$PROFILE_WASM" 9.9.9 "$XDR_DIR/wrong-profile-version.xdr" +expect_fail propose-events-before-profile-staged \ + "$SCRIPT" prepare-propose-upgrade-events "$EVENTS_WASM" 1.5.0 "$XDR_DIR/early-propose-events.xdr" + +set_state 1.1.0 true null null 1.1.0 true "$profile_pending" null +expect_ok verify-profile-proposal \ + "$SCRIPT" verify-proposed-upgrade-profile "$PROFILE_WASM" 1.2.0 +expect_ok propose-events \ + "$SCRIPT" prepare-propose-upgrade-events "$EVENTS_WASM" 1.5.0 "$XDR_DIR/propose-events.xdr" +expect_fail propose-events-with-profile-artifact \ + "$SCRIPT" prepare-propose-upgrade-events "$PROFILE_WASM" 1.5.0 "$XDR_DIR/wrong-events-artifact.xdr" +expect_fail propose-events-with-unpinned-version \ + "$SCRIPT" prepare-propose-upgrade-events "$EVENTS_WASM" 9.9.9 "$XDR_DIR/wrong-events-version.xdr" +set_state 1.1.0 true null null 1.1.0 true "$bad_profile_pending" null +expect_fail propose-events-with-wrong-pending-profile-wasm \ + "$SCRIPT" prepare-propose-upgrade-events "$EVENTS_WASM" 1.5.0 "$XDR_DIR/wrong-profile-pending-propose-events.xdr" + +set_state 1.1.0 true "$events_pending" null 1.1.0 true "$profile_pending" null +expect_ok verify-events-proposal \ + "$SCRIPT" verify-proposed-upgrade-events "$EVENTS_WASM" 1.5.0 +expect_ok apply-profile \ + "$SCRIPT" prepare-apply-upgrade-profile "$PROFILE_WASM" 1.2.0 "$XDR_DIR/apply-profile.xdr" +expect_fail apply-events-before-profile-applied \ + "$SCRIPT" prepare-apply-upgrade-events "$EVENTS_WASM" 1.5.0 "$XDR_DIR/early-apply-events.xdr" +set_state 1.1.0 true "$bad_events_pending" null 1.1.0 true "$profile_pending" null +expect_fail apply-profile-with-wrong-pending-events-wasm \ + "$SCRIPT" prepare-apply-upgrade-profile "$PROFILE_WASM" 1.2.0 "$XDR_DIR/wrong-events-pending-apply-profile.xdr" + +set_state 1.1.0 true "$events_pending" null 1.2.0 true null null +expect_ok apply-events \ + "$SCRIPT" prepare-apply-upgrade-events "$EVENTS_WASM" 1.5.0 "$XDR_DIR/apply-events.xdr" +export FAKE_PROFILE_LIVE_HASH="$WRONG_HASH" +expect_fail apply-events-with-wrong-live-profile-wasm \ + "$SCRIPT" prepare-apply-upgrade-events "$EVENTS_WASM" 1.5.0 "$XDR_DIR/wrong-profile-live-apply-events.xdr" + +set_state 1.5.0 true null null 1.2.0 true null null +expect_ok migrate-profile \ + "$SCRIPT" prepare-migrate-upgrade-profile 1.2.0 "$XDR_DIR/migrate-profile.xdr" +export FAKE_EVENTS_LIVE_HASH="$WRONG_HASH" +expect_fail migrate-profile-with-wrong-live-events-wasm \ + "$SCRIPT" prepare-migrate-upgrade-profile 1.2.0 "$XDR_DIR/wrong-events-live-migrate-profile.xdr" +set_state 1.5.0 true null null 1.2.0 true null null +export FAKE_PROFILE_LIVE_HASH="$WRONG_HASH" +expect_fail migrate-profile-with-wrong-live-profile-wasm \ + "$SCRIPT" prepare-migrate-upgrade-profile 1.2.0 "$XDR_DIR/wrong-profile-live-migrate-profile.xdr" +set_state 1.5.0 true null null 1.2.0 true null null +expect_fail migrate-events-before-profile-migrated \ + "$SCRIPT" prepare-migrate-upgrade-events 1.5.0 "$XDR_DIR/early-migrate-events.xdr" + +set_state 1.5.0 true null null 1.2.0 true null '"1.2.0"' +expect_ok migrate-events \ + "$SCRIPT" prepare-migrate-upgrade-events 1.5.0 "$XDR_DIR/migrate-events.xdr" + +set_state 1.5.0 true null '"1.5.0"' 1.2.0 true null '"1.2.0"' +expect_ok unpause-profile \ + "$SCRIPT" prepare-unpause-profile 1.2.0 "$XDR_DIR/unpause-profile.xdr" +expect_fail unpause-profile-with-unpinned-version \ + "$SCRIPT" prepare-unpause-profile 9.9.9 "$XDR_DIR/wrong-profile-unpause-version.xdr" +export FAKE_PROFILE_LIVE_HASH="$WRONG_HASH" +expect_fail unpause-profile-with-wrong-live-profile-wasm \ + "$SCRIPT" prepare-unpause-profile 1.2.0 "$XDR_DIR/wrong-profile-live-unpause-profile.xdr" +set_state 1.5.0 true null '"1.5.0"' 1.2.0 true null '"1.2.0"' +export FAKE_EVENTS_LIVE_HASH="$WRONG_HASH" +expect_fail unpause-profile-with-wrong-live-events-wasm \ + "$SCRIPT" prepare-unpause-profile 1.2.0 "$XDR_DIR/wrong-events-live-unpause-profile.xdr" +set_state 1.5.0 true null '"1.5.0"' 1.2.0 true null '"1.2.0"' +expect_fail unpause-events-before-profile-unpaused \ + "$SCRIPT" prepare-unpause-events 1.5.0 "$XDR_DIR/early-unpause-events.xdr" + +set_state 1.5.0 true null '"1.5.0"' 1.2.0 false null '"1.2.0"' +expect_ok unpause-events \ + "$SCRIPT" prepare-unpause-events 1.5.0 "$XDR_DIR/unpause-events.xdr" +expect_fail unpause-events-with-unpinned-version \ + "$SCRIPT" prepare-unpause-events 9.9.9 "$XDR_DIR/wrong-events-unpause-version.xdr" +export FAKE_EVENTS_LIVE_HASH="$WRONG_HASH" +expect_fail unpause-events-with-wrong-live-events-wasm \ + "$SCRIPT" prepare-unpause-events 1.5.0 "$XDR_DIR/wrong-events-live-unpause-events.xdr" +set_state 1.5.0 true null '"1.5.0"' 1.2.0 false null '"1.2.0"' +export FAKE_PROFILE_LIVE_HASH="$WRONG_HASH" +expect_fail unpause-events-with-wrong-live-profile-wasm \ + "$SCRIPT" prepare-unpause-events 1.5.0 "$XDR_DIR/wrong-profile-live-unpause-events.xdr" + +set_state 1.5.0 false null '"1.5.0"' 1.2.0 false null '"1.2.0"' +expect_ok upgrade-status "$SCRIPT" upgrade-status +expect_ok verify-profile "$SCRIPT" verify-upgrade-profile "$PROFILE_WASM" 1.2.0 +expect_ok verify-events "$SCRIPT" verify-upgrade-events "$EVENTS_WASM" 1.5.0 +export FAKE_EVENTS_LIVE_HASH="$WRONG_HASH" +expect_fail verify-events-with-wrong-live-wasm \ + "$SCRIPT" verify-upgrade-events "$EVENTS_WASM" 1.5.0 +set_state 1.5.0 false null '"1.5.0"' 1.2.0 false null '"1.2.0"' +export FAKE_PROFILE_LIVE_HASH="$WRONG_HASH" +expect_fail verify-profile-with-wrong-live-wasm \ + "$SCRIPT" verify-upgrade-profile "$PROFILE_WASM" 1.2.0 + +set_state 1.1.0 true "$events_pending" null 1.1.0 true "$profile_pending" null +expect_ok cancel-profile \ + "$SCRIPT" prepare-cancel-upgrade-profile "$XDR_DIR/cancel-profile.xdr" +expect_fail abandon-profile-before-events-cancel \ + "$SCRIPT" prepare-unpause-profile 1.1.0 "$XDR_DIR/early-abandon-profile.xdr" + +set_state 1.1.0 true "$events_pending" null 1.1.0 true null null +expect_ok cancel-events \ + "$SCRIPT" prepare-cancel-upgrade-events "$XDR_DIR/cancel-events.xdr" + +set_state 1.1.0 true null null 1.1.0 true null null +expect_ok abandon-unpause-profile \ + "$SCRIPT" prepare-unpause-profile 1.1.0 "$XDR_DIR/abandon-profile.xdr" +expect_fail abandon-events-before-profile-unpaused \ + "$SCRIPT" prepare-unpause-events 1.1.0 "$XDR_DIR/early-abandon-events.xdr" + +set_state 1.1.0 true null null 1.1.0 false null null +expect_ok abandon-unpause-events \ + "$SCRIPT" prepare-unpause-events 1.1.0 "$XDR_DIR/abandon-events.xdr" diff --git a/scripts/test-sdk27-compat.sh b/scripts/test-sdk27-compat.sh new file mode 100755 index 0000000..4f2194a --- /dev/null +++ b/scripts/test-sdk27-compat.sh @@ -0,0 +1,116 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +FIXTURES="$ROOT/contracts/compatibility/fixtures" +MANIFEST="$FIXTURES/manifest.json" + +hash_file() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | awk '{print $1}' + else + shasum -a 256 "$1" | awk '{print $1}' + fi +} + +check_hash() { + local file="$1" expected="$2" actual + actual="$(hash_file "$file")" + if [ "$actual" != "$expected" ]; then + echo "hash mismatch for $file" >&2 + echo "expected: $expected" >&2 + echo "actual: $actual" >&2 + exit 1 + fi +} + +rustc --version | grep -q '^rustc 1\.93\.0 ' || { + echo "Rust 1.93.0 is required" >&2 + exit 1 +} +stellar --version | head -n 1 | grep -q '^stellar 27\.0\.0 ' || { + echo "Stellar CLI 27.0.0 is required" >&2 + exit 1 +} + +while IFS=$'\t' read -r file expected; do + check_hash "$FIXTURES/$file" "$expected" +done < <(jq -r '.wasm[] | [.file, .sha256] | @tsv' "$MANIFEST") + +while IFS=$'\t' read -r file expected; do + actual="$( + stellar -q contract info meta --wasm "$FIXTURES/$file" --output json \ + | jq -er '.[] | select(.sc_meta_v0.key == "version") | .sc_meta_v0.val' + )" + if [ "$actual" != "$expected" ]; then + echo "contractmeta version mismatch for $file" >&2 + echo "expected: $expected" >&2 + echo "actual: $actual" >&2 + exit 1 + fi +done < <(jq -r '.wasm[] | [.file, .contractmeta_version] | @tsv' "$MANIFEST") + +snapshot_file="$(jq -r '.snapshot.file' "$MANIFEST")" +snapshot_hash="$(jq -r '.snapshot.sha256' "$MANIFEST")" +check_hash "$FIXTURES/$snapshot_file" "$snapshot_hash" + +snapshot_ledger="$(jq -r '.snapshot.ledger' "$MANIFEST")" +snapshot_timestamp="$(jq -r '.snapshot.timestamp' "$MANIFEST")" +snapshot_protocol="$(jq -r '.snapshot.protocol' "$MANIFEST")" +snapshot_network_id="$(jq -r '.snapshot.network_id' "$MANIFEST")" +events_contract="$(jq -r '.snapshot.events_contract' "$MANIFEST")" +profile_contract="$(jq -r '.snapshot.profile_contract' "$MANIFEST")" +jq -e \ + --argjson ledger "$snapshot_ledger" \ + --argjson timestamp "$snapshot_timestamp" \ + --argjson protocol "$snapshot_protocol" \ + --arg network_id "$snapshot_network_id" \ + --arg events "$events_contract" \ + --arg profile "$profile_contract" \ + ' + .sequence_number == $ledger + and .timestamp == $timestamp + and .protocol_version == $protocol + and .network_id == $network_id + and (.ledger_entries | length > 0) + and (.ledger_entries | all( + .live_until == 4294967295 + and (.entry.data.contract_data.contract == $events + or .entry.data.contract_data.contract == $profile) + )) + and ([.ledger_entries[] + | select(.entry.data.contract_data.key == "ledger_key_contract_instance")] + | length == 2) + ' "$FIXTURES/$snapshot_file" >/dev/null || { + echo "snapshot provenance or selection invariant failed" >&2 + exit 1 +} + +verify_build="${VERIFY_SDK27_BUILD:-0}" +case "$verify_build" in + 0) ;; + 1) + EVENTS_WASM="$ROOT/target/wasm32v1-none/release/boundless_events.wasm" + PROFILE_WASM="$ROOT/target/wasm32v1-none/release/boundless_profile.wasm" + [ -f "$EVENTS_WASM" ] || { + echo "missing $EVENTS_WASM; run stellar contract build --locked first" >&2 + exit 1 + } + [ -f "$PROFILE_WASM" ] || { + echo "missing $PROFILE_WASM; run stellar contract build --locked first" >&2 + exit 1 + } + + events_hash="$(jq -r '.wasm[] | select(.file == "events-1.5.0-sdk27.wasm") | .sha256' "$MANIFEST")" + profile_hash="$(jq -r '.wasm[] | select(.file == "profile-1.2.0-sdk27.wasm") | .sha256' "$MANIFEST")" + check_hash "$EVENTS_WASM" "$events_hash" + check_hash "$PROFILE_WASM" "$profile_hash" + ;; + *) + echo "VERIFY_SDK27_BUILD must be 0 or 1" >&2 + exit 1 + ;; +esac + +cd "$ROOT" +cargo test --locked -p boundless-storage-compatibility diff --git a/scripts/testdata/mock-stellar b/scripts/testdata/mock-stellar new file mode 100644 index 0000000..299fb54 --- /dev/null +++ b/scripts/testdata/mock-stellar @@ -0,0 +1,113 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Test-only CLI mock for test-mainnet-upgrade-guards.sh. It never uses a network. + +if [ "${1:-}" = "--version" ]; then + printf 'stellar 27.0.0 (test)\n' + exit 0 +fi + +if [ "${1:-}" = "network" ] && [ "${2:-}" = "ls" ]; then + cat <<'EOF' +Name: mainnet-review +RPC url: https://rpc.example.invalid +Network passphrase: Public Global Stellar Network ; September 2015 +EOF + exit 0 +fi + +if [ "${1:-}" = "contract" ] && [ "${2:-}" = "info" ] && [ "${3:-}" = "hash" ]; then + contract_id="" + previous="" + for arg in "$@"; do + if [ "$previous" = "--contract-id" ] || [ "$previous" = "--id" ]; then + contract_id="$arg" + break + fi + previous="$arg" + done + + case "$contract_id" in + EVENTS) printf '%s\n' "${FAKE_EVENTS_LIVE_HASH:?missing FAKE_EVENTS_LIVE_HASH}" ;; + PROFILE) printf '%s\n' "${FAKE_PROFILE_LIVE_HASH:?missing FAKE_PROFILE_LIVE_HASH}" ;; + *) + printf 'unknown fake contract id: %s\n' "$contract_id" >&2 + exit 1 + ;; + esac + exit 0 +fi + +if [ "${1:-}" = "tx" ] && [ "${2:-}" = "simulate" ]; then + payload="$(cat)" + [ -n "$payload" ] + printf 'SIMULATED-XDR\n' + exit 0 +fi + +if [ "${1:-}" = "contract" ] && [ "${2:-}" = "invoke" ]; then + contract_id="" + function_name="" + after_separator=false + previous="" + + for arg in "$@"; do + if [ "$arg" = "--build-only" ]; then + printf 'BUILT-XDR\n' + exit 0 + fi + if [ "$after_separator" = true ]; then + function_name="$arg" + break + fi + if [ "$arg" = "--" ]; then + after_separator=true + fi + done + + for arg in "$@"; do + if [ "$previous" = "--id" ]; then + contract_id="$arg" + break + fi + previous="$arg" + done + + if [ "$contract_id" = "EVENTS" ]; then + version="${FAKE_EVENTS_VERSION:-1.1.0}" + paused="${FAKE_EVENTS_PAUSED:-false}" + pending="${FAKE_EVENTS_PENDING:-null}" + migrated="${FAKE_EVENTS_MIGRATED:-null}" + elif [ "$contract_id" = "PROFILE" ]; then + version="${FAKE_PROFILE_VERSION:-1.1.0}" + paused="${FAKE_PROFILE_PAUSED:-false}" + pending="${FAKE_PROFILE_PENDING:-null}" + migrated="${FAKE_PROFILE_MIGRATED:-null}" + events_contract="${FAKE_PROFILE_EVENTS_CONTRACT:-EVENTS}" + pending_events_contract="${FAKE_PROFILE_EVENTS_PENDING:-null}" + else + printf 'unknown fake contract id: %s\n' "$contract_id" >&2 + exit 1 + fi + + case "$function_name" in + version) printf '"%s"\n' "$version" ;; + get_admin) printf '"GADMIN"\n' ;; + is_paused) printf '%s\n' "$paused" ;; + get_pending_upgrade) printf '%s\n' "$pending" ;; + get_migrated_to_version) printf '%s\n' "$migrated" ;; + get_events_contract) printf '"%s"\n' "$events_contract" ;; + get_pending_events_contract) printf '%s\n' "$pending_events_contract" ;; + id_base) printf '271539957145796608\n' ;; + get_event) + printf 'HostError: Error(Contract, #30)\n' >&2 + exit 1 + ;; + *) printf 'null\n' ;; + esac + exit 0 +fi + +printf 'unsupported fake stellar invocation: %s\n' "$*" >&2 +exit 1