From 34404754efa510606acb899cee3ae9f813204a3f Mon Sep 17 00:00:00 2001 From: fredo Date: Fri, 24 Jul 2026 16:20:36 +0200 Subject: [PATCH 01/11] feat(release): publish Rust contract bindings Keep Cargo consumers on the same canonical ABI and release train as the npm package. Relates to ENG-4070. --- .github/workflows/release.yml | 88 ++ .github/workflows/solidity-test.yml | 23 + .gitignore | 4 + README.md | 23 + bindings/rust/Cargo.toml | 17 + bindings/rust/README.md | 22 + bindings/rust/RELEASING.md | 30 + bindings/rust/abi/StateOracle.json | 1567 +++++++++++++++++++++++++ bindings/rust/src/lib.rs | 39 + shell/bump-version.sh | 17 +- shell/create_artifacts.sh | 8 + test/release/test_release_workflow.py | 45 +- 12 files changed, 1878 insertions(+), 5 deletions(-) create mode 100644 bindings/rust/Cargo.toml create mode 100644 bindings/rust/README.md create mode 100644 bindings/rust/RELEASING.md create mode 100644 bindings/rust/abi/StateOracle.json create mode 100644 bindings/rust/src/lib.rs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4f372f8..35e852d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,6 +13,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + submodules: recursive - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 with: @@ -52,6 +54,92 @@ jobs: - name: Publish to npm run: npm publish --access public --ignore-scripts=true + release-cargo: + needs: create-artifacts + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Rust + run: | + rustup toolchain install stable --profile minimal --component clippy,rustfmt --no-self-update + rustup default stable + + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: credible-layer-contracts-artifacts + path: artifacts/ + + - name: Verify release inputs + run: | + set -euo pipefail + crate_version=$(cargo metadata \ + --manifest-path bindings/rust/Cargo.toml \ + --format-version 1 \ + --no-deps | jq -r '.packages[0].version') + npm_version=$(jq -r '.version' package.json) + if [[ "$crate_version" != "$GITHUB_REF_NAME" ]]; then + echo "::error::Cargo version ${crate_version} does not match tag ${GITHUB_REF_NAME}" + exit 1 + fi + if [[ "$npm_version" != "$GITHUB_REF_NAME" ]]; then + echo "::error::npm version ${npm_version} does not match tag ${GITHUB_REF_NAME}" + exit 1 + fi + if ! cmp -s artifacts/StateOracle.json bindings/rust/abi/StateOracle.json; then + echo "::error::Committed Rust ABI differs from the release artifact" + diff -u bindings/rust/abi/StateOracle.json artifacts/StateOracle.json || true + exit 1 + fi + + - name: Verify Cargo package + run: | + cargo fmt --manifest-path bindings/rust/Cargo.toml -- --check + cargo clippy --manifest-path bindings/rust/Cargo.toml --all-targets -- -D warnings -D clippy::pedantic + cargo test --manifest-path bindings/rust/Cargo.toml + cargo publish --manifest-path bindings/rust/Cargo.toml --dry-run + + - name: Check whether version is already published + id: published + env: + CRATE_NAME: credible-layer-contracts + run: | + set -euo pipefail + crate_version=$(cargo metadata \ + --manifest-path bindings/rust/Cargo.toml \ + --format-version 1 \ + --no-deps | jq -r '.packages[0].version') + status=$(curl \ + --silent \ + --show-error \ + --output /dev/null \ + --write-out '%{http_code}' \ + --user-agent credible-layer-contracts-release \ + "https://crates.io/api/v1/crates/${CRATE_NAME}/${crate_version}") + case "$status" in + 200) echo "exists=true" >> "$GITHUB_OUTPUT" ;; + 404) echo "exists=false" >> "$GITHUB_OUTPUT" ;; + *) + echo "::error::Unexpected crates.io response: HTTP ${status}" + exit 1 + ;; + esac + + - name: Authenticate to crates.io + if: steps.published.outputs.exists != 'true' + id: crates-io-auth + uses: rust-lang/crates-io-auth-action@v1 + + - name: Publish to crates.io + if: steps.published.outputs.exists != 'true' + env: + CARGO_REGISTRY_TOKEN: ${{ steps.crates-io-auth.outputs.token }} + run: cargo publish --manifest-path bindings/rust/Cargo.toml release-github: needs: create-artifacts permissions: diff --git a/.github/workflows/solidity-test.yml b/.github/workflows/solidity-test.yml index cb8223b..40c0516 100644 --- a/.github/workflows/solidity-test.yml +++ b/.github/workflows/solidity-test.yml @@ -13,6 +13,29 @@ jobs: - name: Test release configuration run: python3 test/release/test_release_workflow.py + rust-bindings: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + - name: Setup Rust + run: | + rustup toolchain install stable --profile minimal --component clippy,rustfmt --no-self-update + rustup default stable + - name: Verify generated ABI and Cargo package + run: | + ./shell/create_artifacts.sh + git diff --exit-code -- bindings/rust/abi/StateOracle.json + cargo fmt --manifest-path bindings/rust/Cargo.toml -- --check + cargo clippy --manifest-path bindings/rust/Cargo.toml --all-targets -- -D warnings -D clippy::pedantic + cargo test --manifest-path bindings/rust/Cargo.toml + cargo publish --manifest-path bindings/rust/Cargo.toml --dry-run + solidity-base: uses: phylaxsystems/actions/.github/workflows/solidity-base.yaml@main with: diff --git a/.gitignore b/.gitignore index 5807c81..083a7ec 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,10 @@ docs/ artifacts/ +# The Rust bindings crate is a library; consumers resolve their own lockfile. +bindings/rust/Cargo.lock +bindings/rust/target/ + # macOS system files .DS_Store **/.DS_Store diff --git a/README.md b/README.md index aab0155..758d4ca 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,29 @@ State Oracle Proxy deployed at
## Installation +### Rust bindings + +Each tagged release publishes the StateOracle ABI as the +`credible-layer-contracts` Cargo crate. Registry consumers can install it with: + +```bash +cargo add credible-layer-contracts +``` + +For an unreleased revision, Cargo can resolve the crate directly from this +repository. Pin a commit rather than a branch or tag: + +```toml +[dependencies] +credible-layer-contracts = { git = "https://github.com/phylaxsystems/credible-layer-contracts.git", rev = "" } +``` + +Both forms expose the generated binding as +`credible_layer_contracts::StateOracle`. The committed ABI snapshot allows git +consumers to build without installing Foundry or initializing submodules. + +### Solidity development + 1. Clone the repository: ```bash diff --git a/bindings/rust/Cargo.toml b/bindings/rust/Cargo.toml new file mode 100644 index 0000000..741a4a5 --- /dev/null +++ b/bindings/rust/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "credible-layer-contracts" +version = "0.3.0" +edition = "2024" +rust-version = "1.85" +authors = ["Phylax Systems"] +license = "MIT OR Apache-2.0" +description = "Alloy Rust bindings for the Credible Layer protocol contracts" +repository = "https://github.com/phylaxsystems/credible-layer-contracts" +homepage = "https://github.com/phylaxsystems/credible-layer-contracts" +readme = "README.md" +keywords = ["ethereum", "solidity", "abi", "bindings", "credible-layer"] +publish = ["crates-io"] +include = ["src/**", "abi/StateOracle.json", "README.md", "RELEASING.md"] + +[dependencies] +alloy-sol-types = { version = "1.6.1", features = ["json"] } diff --git a/bindings/rust/README.md b/bindings/rust/README.md new file mode 100644 index 0000000..7e8ee9d --- /dev/null +++ b/bindings/rust/README.md @@ -0,0 +1,22 @@ +# Credible Layer contract bindings + +Alloy Rust bindings for the production contracts maintained in +[`credible-layer-contracts`](https://github.com/phylaxsystems/credible-layer-contracts). + +The crate version follows the repository's contract release version. Its ABI +snapshot is generated from the same Foundry artifact that is published in the +`@phylax-systems/credible-layer-contracts` npm package. + +```bash +cargo add credible-layer-contracts +``` + +```rust +use credible_layer_contracts::StateOracle; +``` + +Do not edit `abi/StateOracle.json` by hand. Run `./shell/create_artifacts.sh` +from the repository root and review the resulting ABI change. + +Maintainers should follow [`RELEASING.md`](RELEASING.md) for the one-time +crates.io bootstrap and subsequent tagged releases. diff --git a/bindings/rust/RELEASING.md b/bindings/rust/RELEASING.md new file mode 100644 index 0000000..f909ffd --- /dev/null +++ b/bindings/rust/RELEASING.md @@ -0,0 +1,30 @@ +# Releasing the Rust bindings + +Tagged releases publish the Cargo crate through crates.io trusted publishing. +The workflow requests a short-lived token with GitHub OIDC; do not add a +long-lived crates.io token to the repository's Actions secrets. + +## One-time crates.io bootstrap + +crates.io requires the crate to exist before a trusted publisher can be +registered. Bootstrap the first release from the same commit that will be +tagged: + +1. Sign in to crates.io with GitHub and verify the publishing account's email. +2. Create a narrowly scoped crates.io API token. +3. Run `./shell/bump-version.sh patch` to update both package versions and + create the local release tag. +4. Run `cargo login`, then + `cargo publish --manifest-path bindings/rust/Cargo.toml`. +5. In the new crate's trusted-publishing settings, register GitHub owner + `phylaxsystems`, repository `credible-layer-contracts`, workflow + `release.yml`, and no GitHub environment. +6. Revoke the bootstrap API token (and run `cargo logout` locally). +7. Push the version commit and its tag. The workflow recognizes that the + bootstrap version already exists; later tags publish automatically via OIDC. + +## Normal releases + +Use `./shell/bump-version.sh patch|minor|major`, review the resulting commit and +tag, then push both. The release fails before publishing if the npm version, +Cargo version, tag, or committed StateOracle ABI disagree. diff --git a/bindings/rust/abi/StateOracle.json b/bindings/rust/abi/StateOracle.json new file mode 100644 index 0000000..6d57535 --- /dev/null +++ b/bindings/rust/abi/StateOracle.json @@ -0,0 +1,1567 @@ +[ + { + "type": "constructor", + "inputs": [ + { + "name": "assertionTimelockBlocks", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "ASSERTION_TIMELOCK_BLOCKS", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "DEFAULT_ADMIN_ROLE", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "GOVERNANCE_ROLE", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "GUARDIAN_ADMIN_ROLE", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "GUARDIAN_ROLE", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "OPERATOR_ADMIN_ROLE", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "OPERATOR_ROLE", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "acceptManagerTransfer", + "inputs": [ + { + "name": "contractAddress", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "acceptOwnership", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "addAdminVerifier", + "inputs": [ + { + "name": "adminVerifier", + "type": "address", + "internalType": "contract IAdminVerifier" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "addAssertion", + "inputs": [ + { + "name": "contractAddress", + "type": "address", + "internalType": "address" + }, + { + "name": "assertionId", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "daVerifier", + "type": "address", + "internalType": "contract IDAVerifier" + }, + { + "name": "metadata", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "proof", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "addDAVerifier", + "inputs": [ + { + "name": "daVerifier", + "type": "address", + "internalType": "contract IDAVerifier" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "addToWhitelist", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "adminVerifiers", + "inputs": [ + { + "name": "adminVerifier", + "type": "address", + "internalType": "contract IAdminVerifier" + } + ], + "outputs": [ + { + "name": "isRegistered", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "assertionAdopters", + "inputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "manager", + "type": "address", + "internalType": "address" + }, + { + "name": "pendingManager", + "type": "address", + "internalType": "address" + }, + { + "name": "assertionCount", + "type": "uint16", + "internalType": "uint16" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "batch", + "inputs": [ + { + "name": "calls", + "type": "bytes[]", + "internalType": "bytes[]" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "daVerifiers", + "inputs": [ + { + "name": "daVerifier", + "type": "address", + "internalType": "contract IDAVerifier" + } + ], + "outputs": [ + { + "name": "isRegistered", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "disableWhitelist", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "enableWhitelist", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "getAssertionCount", + "inputs": [ + { + "name": "contractAddress", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "assertionCount", + "type": "uint16", + "internalType": "uint16" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getAssertionWindow", + "inputs": [ + { + "name": "contractAddress", + "type": "address", + "internalType": "address" + }, + { + "name": "assertionId", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [ + { + "name": "activationBlock", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "deactivationBlock", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getManager", + "inputs": [ + { + "name": "contractAddress", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "manager", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getPendingManager", + "inputs": [ + { + "name": "contractAddress", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "pendingManager", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getRoleAdmin", + "inputs": [ + { + "name": "role", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "grantGovernanceRole", + "inputs": [ + { + "name": "governance", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "grantGuardianAdminRole", + "inputs": [ + { + "name": "guardianAdmin", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "grantGuardianRole", + "inputs": [ + { + "name": "guardian", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "grantOperatorAdminRole", + "inputs": [ + { + "name": "operatorAdmin", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "grantOperatorRole", + "inputs": [ + { + "name": "operator", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "grantRole", + "inputs": [ + { + "name": "role", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "account", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "hasAssertion", + "inputs": [ + { + "name": "contractAddress", + "type": "address", + "internalType": "address" + }, + { + "name": "assertionId", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [ + { + "name": "isAssociated", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "hasRole", + "inputs": [ + { + "name": "role", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "account", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "initialize", + "inputs": [ + { + "name": "admin", + "type": "address", + "internalType": "address" + }, + { + "name": "_adminVerifiers", + "type": "address[]", + "internalType": "contract IAdminVerifier[]" + }, + { + "name": "_daVerifiers", + "type": "address[]", + "internalType": "contract IDAVerifier[]" + }, + { + "name": "_maxAssertionsPerAA", + "type": "uint16", + "internalType": "uint16" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "isAdminVerifierRegistered", + "inputs": [ + { + "name": "adminVerifier", + "type": "address", + "internalType": "contract IAdminVerifier" + } + ], + "outputs": [ + { + "name": "isRegistered", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "isDAVerifierRegistered", + "inputs": [ + { + "name": "daVerifier", + "type": "address", + "internalType": "contract IDAVerifier" + } + ], + "outputs": [ + { + "name": "isRegistered", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "isWhitelisted", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "maxAssertionsPerAA", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint16", + "internalType": "uint16" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "owner", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "pendingOwner", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "registerAssertionAdopter", + "inputs": [ + { + "name": "contractAddress", + "type": "address", + "internalType": "address" + }, + { + "name": "adminVerifier", + "type": "address", + "internalType": "contract IAdminVerifier" + }, + { + "name": "data", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "removeAdminVerifier", + "inputs": [ + { + "name": "adminVerifier", + "type": "address", + "internalType": "contract IAdminVerifier" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "removeAssertion", + "inputs": [ + { + "name": "contractAddress", + "type": "address", + "internalType": "address" + }, + { + "name": "assertionId", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "removeAssertionByGuardian", + "inputs": [ + { + "name": "contractAddress", + "type": "address", + "internalType": "address" + }, + { + "name": "assertionId", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "removeDAVerifier", + "inputs": [ + { + "name": "daVerifier", + "type": "address", + "internalType": "contract IDAVerifier" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "removeFromWhitelist", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "renounceOwnership", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "renounceRole", + "inputs": [ + { + "name": "role", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "callerConfirmation", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "resetStorage", + "inputs": [ + { + "name": "adopter", + "type": "address", + "internalType": "address" + }, + { + "name": "storageKey", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "revokeGovernanceRole", + "inputs": [ + { + "name": "governance", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "revokeGuardianAdminRole", + "inputs": [ + { + "name": "guardianAdmin", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "revokeGuardianRole", + "inputs": [ + { + "name": "guardian", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "revokeManager", + "inputs": [ + { + "name": "contractAddress", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "revokeOperatorAdminRole", + "inputs": [ + { + "name": "operatorAdmin", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "revokeOperatorRole", + "inputs": [ + { + "name": "operator", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "revokeRole", + "inputs": [ + { + "name": "role", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "account", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setMaxAssertionsPerAA", + "inputs": [ + { + "name": "_maxAssertionsPerAA", + "type": "uint16", + "internalType": "uint16" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "supportsInterface", + "inputs": [ + { + "name": "interfaceId", + "type": "bytes4", + "internalType": "bytes4" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "transferManager", + "inputs": [ + { + "name": "contractAddress", + "type": "address", + "internalType": "address" + }, + { + "name": "newManager", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "transferOwnership", + "inputs": [ + { + "name": "newOwner", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "whitelist", + "inputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "whitelistEnabled", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "event", + "name": "AddedToWhitelist", + "inputs": [ + { + "name": "account", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "AdminVerifierAdded", + "inputs": [ + { + "name": "adminVerifier", + "type": "address", + "indexed": false, + "internalType": "contract IAdminVerifier" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "AdminVerifierRemoved", + "inputs": [ + { + "name": "adminVerifier", + "type": "address", + "indexed": false, + "internalType": "contract IAdminVerifier" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "AssertionAdded", + "inputs": [ + { + "name": "assertionAdopter", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "assertionId", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "activationBlock", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "daVerifier", + "type": "address", + "indexed": true, + "internalType": "contract IDAVerifier" + }, + { + "name": "metadata", + "type": "bytes", + "indexed": false, + "internalType": "bytes" + }, + { + "name": "proof", + "type": "bytes", + "indexed": false, + "internalType": "bytes" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "AssertionAdopterAdded", + "inputs": [ + { + "name": "contractAddress", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "manager", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "adminVerifier", + "type": "address", + "indexed": false, + "internalType": "contract IAdminVerifier" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "AssertionRemoved", + "inputs": [ + { + "name": "assertionAdopter", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "assertionId", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "deactivationBlock", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "DAVerifierAdded", + "inputs": [ + { + "name": "daVerifier", + "type": "address", + "indexed": false, + "internalType": "contract IDAVerifier" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "DAVerifierRemoved", + "inputs": [ + { + "name": "daVerifier", + "type": "address", + "indexed": false, + "internalType": "contract IDAVerifier" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Initialized", + "inputs": [ + { + "name": "version", + "type": "uint64", + "indexed": false, + "internalType": "uint64" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "ManagerTransferRequested", + "inputs": [ + { + "name": "contractAddress", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "manager", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newManager", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "ManagerTransferred", + "inputs": [ + { + "name": "contractAddress", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newManager", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "OwnershipTransferStarted", + "inputs": [ + { + "name": "previousOwner", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newOwner", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "OwnershipTransferred", + "inputs": [ + { + "name": "previousOwner", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newOwner", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "RemovedFromWhitelist", + "inputs": [ + { + "name": "account", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "RoleAdminChanged", + "inputs": [ + { + "name": "role", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "previousAdminRole", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "newAdminRole", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "RoleGranted", + "inputs": [ + { + "name": "role", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "account", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "sender", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "RoleRevoked", + "inputs": [ + { + "name": "role", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "account", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "sender", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "StorageReset", + "inputs": [ + { + "name": "adopter", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "storageKey", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "resetBlock", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "WhitelistDisabled", + "inputs": [], + "anonymous": false + }, + { + "type": "event", + "name": "WhitelistEnabled", + "inputs": [], + "anonymous": false + }, + { + "type": "error", + "name": "AccessControlBadConfirmation", + "inputs": [] + }, + { + "type": "error", + "name": "AccessControlUnauthorizedAccount", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + }, + { + "name": "neededRole", + "type": "bytes32", + "internalType": "bytes32" + } + ] + }, + { + "type": "error", + "name": "AccountNotWhitelisted", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "AdminVerifierAlreadyRegistered", + "inputs": [] + }, + { + "type": "error", + "name": "AdminVerifierNotRegistered", + "inputs": [] + }, + { + "type": "error", + "name": "AlreadyWhitelisted", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "AssertionAdopterAlreadyRegistered", + "inputs": [] + }, + { + "type": "error", + "name": "AssertionAdopterNotRegistered", + "inputs": [] + }, + { + "type": "error", + "name": "AssertionAlreadyExists", + "inputs": [] + }, + { + "type": "error", + "name": "AssertionAlreadyRemoved", + "inputs": [] + }, + { + "type": "error", + "name": "AssertionDoesNotExist", + "inputs": [] + }, + { + "type": "error", + "name": "BatchError", + "inputs": [ + { + "name": "result", + "type": "bytes", + "internalType": "bytes" + } + ] + }, + { + "type": "error", + "name": "CannotGrantDefaultAdminRole", + "inputs": [] + }, + { + "type": "error", + "name": "CannotRenounceOwnerDefaultAdminRole", + "inputs": [] + }, + { + "type": "error", + "name": "CannotRevokeOwnerDefaultAdminRole", + "inputs": [] + }, + { + "type": "error", + "name": "DAVerifierAlreadyRegistered", + "inputs": [] + }, + { + "type": "error", + "name": "DAVerifierNotRegistered", + "inputs": [] + }, + { + "type": "error", + "name": "DAVerifierNotRegistered", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidAssertionTimelock", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidDAProof", + "inputs": [ + { + "name": "daVerifier", + "type": "address", + "internalType": "contract IDAVerifier" + } + ] + }, + { + "type": "error", + "name": "InvalidInitialization", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidManagerTransferRequest", + "inputs": [] + }, + { + "type": "error", + "name": "NoPendingManager", + "inputs": [] + }, + { + "type": "error", + "name": "NotInitializing", + "inputs": [] + }, + { + "type": "error", + "name": "NotWhitelisted", + "inputs": [] + }, + { + "type": "error", + "name": "OwnableInvalidOwner", + "inputs": [ + { + "name": "owner", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "OwnableUnauthorizedAccount", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "TooManyAssertions", + "inputs": [] + }, + { + "type": "error", + "name": "UnauthorizedManager", + "inputs": [] + }, + { + "type": "error", + "name": "UnauthorizedRegistrant", + "inputs": [] + }, + { + "type": "error", + "name": "WhitelistAlreadyDisabled", + "inputs": [] + }, + { + "type": "error", + "name": "WhitelistAlreadyEnabled", + "inputs": [] + } +] diff --git a/bindings/rust/src/lib.rs b/bindings/rust/src/lib.rs new file mode 100644 index 0000000..5e7bbd3 --- /dev/null +++ b/bindings/rust/src/lib.rs @@ -0,0 +1,39 @@ +//! Alloy bindings for the Credible Layer protocol contracts. +//! +//! The ABI snapshot is generated by the repository's artifact pipeline. It is +//! committed so registry and git consumers do not need Foundry or submodules. + +use alloy_sol_types::sol; + +sol!(StateOracle, "abi/StateOracle.json"); + +#[cfg(test)] +mod tests { + use alloy_sol_types::{SolCall, SolError, SolEvent, TopicList}; + + use super::StateOracle; + + #[test] + fn state_oracle_boundary_matches_representative_canonical_entries() { + assert_eq!( + StateOracle::registerAssertionAdopterCall::SIGNATURE, + "registerAssertionAdopter(address,address,bytes)" + ); + assert_eq!( + StateOracle::getAssertionWindowCall::SIGNATURE, + "getAssertionWindow(address,bytes32)" + ); + assert_eq!( + StateOracle::AssertionAdded::SIGNATURE, + "AssertionAdded(address,bytes32,uint256,address,bytes,bytes)" + ); + assert_eq!( + StateOracle::InvalidDAProof::SIGNATURE, + "InvalidDAProof(address)" + ); + assert_eq!( + <::TopicList as TopicList>::COUNT, + 4 + ); + } +} diff --git a/shell/bump-version.sh b/shell/bump-version.sh index 3324fd2..a0da557 100755 --- a/shell/bump-version.sh +++ b/shell/bump-version.sh @@ -1,5 +1,7 @@ #!/usr/bin/env bash +set -euo pipefail + # Check if an argument was provided if [ $# -ne 1 ]; then echo "Usage: $0 " @@ -26,8 +28,14 @@ NEW_VERSION=$(npm version "$VERSION_TYPE" --no-git-tag-version) # Strip the leading 'v' from the version TAG_NAME=${NEW_VERSION#v} -# Stage the package.json changes -git add package.json +# Keep the Cargo package on the same release train as the npm package and tag. +sed -i.bak \ + "s/^version = \"[0-9][0-9.]*\"$/version = \"${TAG_NAME}\"/" \ + bindings/rust/Cargo.toml +rm bindings/rust/Cargo.toml.bak + +# Stage the package metadata changes +git add package.json bindings/rust/Cargo.toml # Commit the version bump git commit -m "chore: bump version to ${TAG_NAME}" @@ -36,8 +44,9 @@ git commit -m "chore: bump version to ${TAG_NAME}" git tag "${TAG_NAME}" echo "✅ Successfully:" -echo " - Bumped npm version to ${TAG_NAME}" +echo " - Bumped npm and Cargo versions to ${TAG_NAME}" echo " - Created git tag ${TAG_NAME}" echo "" echo "To push changes:" -echo " git push origin ${TAG_NAME}" \ No newline at end of file +echo " git push origin HEAD" +echo " git push origin ${TAG_NAME}" diff --git a/shell/create_artifacts.sh b/shell/create_artifacts.sh index f59dae7..519140e 100755 --- a/shell/create_artifacts.sh +++ b/shell/create_artifacts.sh @@ -51,3 +51,11 @@ extract_abi "$ROOT_DIR/out/IAdminVerifier.sol/IAdminVerifier.json" "${INTERFACES # Extract ABIs for libraries LIBRARIES="${ARTIFACTS}/libraries" extract_abi "$ROOT_DIR/out/AdminVerifierRegistry.sol/AdminVerifierRegistry.json" "${LIBRARIES}" + +# Keep the committed Rust binding input byte-for-byte aligned with the ABI +# published to npm. This snapshot lets Cargo git dependencies build without +# Foundry or initialized submodules. +RUST_BINDINGS_ABI="${ROOT_DIR}/bindings/rust/abi" +mkdir -p "${RUST_BINDINGS_ABI}" +cp "${ARTIFACTS}/StateOracle.json" "${RUST_BINDINGS_ABI}/StateOracle.json" +echo "Synced StateOracle ABI to ${RUST_BINDINGS_ABI}/StateOracle.json" diff --git a/test/release/test_release_workflow.py b/test/release/test_release_workflow.py index 8c29e96..00273bd 100644 --- a/test/release/test_release_workflow.py +++ b/test/release/test_release_workflow.py @@ -1,4 +1,5 @@ import json +import tomllib import unittest from pathlib import Path @@ -10,7 +11,7 @@ class ReleaseWorkflowTest(unittest.TestCase): def test_npm_publish_runs_inline_in_the_trusted_workflow(self): workflow = (ROOT / ".github" / "workflows" / "release.yml").read_text() release_job = workflow.split(" release-npm:\n", 1)[1].split( - " release-github:\n", 1 + " release-cargo:\n", 1 )[0] self.assertIn("id-token: write", release_job) @@ -27,6 +28,48 @@ def test_npm_publish_runs_inline_in_the_trusted_workflow(self): ) self.assertNotIn("phylaxsystems/actions/release-npm", release_job) + def test_cargo_publish_runs_inline_with_trusted_publishing(self): + workflow = (ROOT / ".github" / "workflows" / "release.yml").read_text() + release_job = workflow.split(" release-cargo:\n", 1)[1].split( + " release-github:\n", 1 + )[0] + + self.assertIn("needs: create-artifacts", release_job) + self.assertIn("id-token: write", release_job) + self.assertIn("uses: actions/checkout@", release_job) + self.assertIn("uses: actions/download-artifact@", release_job) + self.assertIn("name: credible-layer-contracts-artifacts", release_job) + self.assertIn("cmp -s artifacts/StateOracle.json", release_job) + self.assertIn( + "cargo publish --manifest-path bindings/rust/Cargo.toml --dry-run", + release_job, + ) + self.assertIn("uses: rust-lang/crates-io-auth-action@v1", release_job) + self.assertIn( + "CARGO_REGISTRY_TOKEN: ${{ steps.crates-io-auth.outputs.token }}", + release_job, + ) + self.assertIn( + "run: cargo publish --manifest-path bindings/rust/Cargo.toml", + release_job, + ) + + def test_cargo_and_npm_packages_share_release_version(self): + package = json.loads((ROOT / "package.json").read_text()) + with (ROOT / "bindings" / "rust" / "Cargo.toml").open("rb") as manifest: + cargo_package = tomllib.load(manifest)["package"] + + self.assertEqual(cargo_package["name"], "credible-layer-contracts") + self.assertEqual(cargo_package["version"], package["version"]) + self.assertEqual(cargo_package["publish"], ["crates-io"]) + self.assertEqual(cargo_package["license"], "MIT OR Apache-2.0") + + def test_artifact_generation_updates_the_committed_rust_abi(self): + script = (ROOT / "shell" / "create_artifacts.sh").read_text() + + self.assertIn('bindings/rust/abi', script) + self.assertIn('cp "${ARTIFACTS}/StateOracle.json"', script) + def test_package_requests_provenance(self): package = json.loads((ROOT / "package.json").read_text()) From 973fa5aa35870f21bfa330fcc955170d26a69a71 Mon Sep 17 00:00:00 2001 From: fredo Date: Fri, 31 Jul 2026 14:52:31 +0200 Subject: [PATCH 02/11] fix(bindings): include license texts in crate --- bindings/rust/Cargo.toml | 9 +- bindings/rust/LICENSE-APACHE | 176 ++++++++++++++++++++++++++ bindings/rust/LICENSE-MIT | 23 ++++ test/release/test_release_workflow.py | 12 ++ 4 files changed, 219 insertions(+), 1 deletion(-) create mode 100644 bindings/rust/LICENSE-APACHE create mode 100644 bindings/rust/LICENSE-MIT diff --git a/bindings/rust/Cargo.toml b/bindings/rust/Cargo.toml index 741a4a5..4f89ec2 100644 --- a/bindings/rust/Cargo.toml +++ b/bindings/rust/Cargo.toml @@ -11,7 +11,14 @@ homepage = "https://github.com/phylaxsystems/credible-layer-contracts" readme = "README.md" keywords = ["ethereum", "solidity", "abi", "bindings", "credible-layer"] publish = ["crates-io"] -include = ["src/**", "abi/StateOracle.json", "README.md", "RELEASING.md"] +include = [ + "src/**", + "abi/StateOracle.json", + "README.md", + "RELEASING.md", + "LICENSE-MIT", + "LICENSE-APACHE", +] [dependencies] alloy-sol-types = { version = "1.6.1", features = ["json"] } diff --git a/bindings/rust/LICENSE-APACHE b/bindings/rust/LICENSE-APACHE new file mode 100644 index 0000000..1b5ec8b --- /dev/null +++ b/bindings/rust/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/bindings/rust/LICENSE-MIT b/bindings/rust/LICENSE-MIT new file mode 100644 index 0000000..31aa793 --- /dev/null +++ b/bindings/rust/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/test/release/test_release_workflow.py b/test/release/test_release_workflow.py index 00273bd..49275b0 100644 --- a/test/release/test_release_workflow.py +++ b/test/release/test_release_workflow.py @@ -64,6 +64,18 @@ def test_cargo_and_npm_packages_share_release_version(self): self.assertEqual(cargo_package["publish"], ["crates-io"]) self.assertEqual(cargo_package["license"], "MIT OR Apache-2.0") + def test_cargo_package_includes_repository_license_texts(self): + crate_root = ROOT / "bindings" / "rust" + with (crate_root / "Cargo.toml").open("rb") as manifest: + included_files = tomllib.load(manifest)["package"]["include"] + + for license_name in ("LICENSE-MIT", "LICENSE-APACHE"): + self.assertIn(license_name, included_files) + self.assertEqual( + (crate_root / license_name).read_text().splitlines(), + (ROOT / license_name).read_text().splitlines(), + ) + def test_artifact_generation_updates_the_committed_rust_abi(self): script = (ROOT / "shell" / "create_artifacts.sh").read_text() From 59c3e8d2e93e05972ce1a65623552beefc38b919 Mon Sep 17 00:00:00 2001 From: fredo Date: Fri, 31 Jul 2026 14:53:51 +0200 Subject: [PATCH 03/11] ci(bindings): enforce Rust 1.85 MSRV --- .github/workflows/solidity-test.yml | 9 +++++++++ test/release/test_release_workflow.py | 17 +++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/.github/workflows/solidity-test.yml b/.github/workflows/solidity-test.yml index 40c0516..7f06ec4 100644 --- a/.github/workflows/solidity-test.yml +++ b/.github/workflows/solidity-test.yml @@ -36,6 +36,15 @@ jobs: cargo test --manifest-path bindings/rust/Cargo.toml cargo publish --manifest-path bindings/rust/Cargo.toml --dry-run + rust-bindings-msrv: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup Rust 1.85 + run: rustup toolchain install 1.85.0 --profile minimal --no-self-update + - name: Test Rust bindings on the declared MSRV + run: cargo +1.85.0 test --manifest-path bindings/rust/Cargo.toml --all-targets + solidity-base: uses: phylaxsystems/actions/.github/workflows/solidity-base.yaml@main with: diff --git a/test/release/test_release_workflow.py b/test/release/test_release_workflow.py index 49275b0..3ddf2b1 100644 --- a/test/release/test_release_workflow.py +++ b/test/release/test_release_workflow.py @@ -76,6 +76,23 @@ def test_cargo_package_includes_repository_license_texts(self): (ROOT / license_name).read_text().splitlines(), ) + def test_ci_enforces_cargo_msrv(self): + workflow = (ROOT / ".github" / "workflows" / "solidity-test.yml").read_text() + msrv_job = workflow.split(" rust-bindings-msrv:\n", 1)[1].split( + " solidity-base:\n", 1 + )[0] + with (ROOT / "bindings" / "rust" / "Cargo.toml").open("rb") as manifest: + rust_version = tomllib.load(manifest)["package"]["rust-version"] + rust_toolchain = ( + f"{rust_version}.0" if rust_version.count(".") == 1 else rust_version + ) + + self.assertIn(f"rustup toolchain install {rust_toolchain}", msrv_job) + self.assertIn( + f"cargo +{rust_toolchain} test --manifest-path bindings/rust/Cargo.toml", + msrv_job, + ) + def test_artifact_generation_updates_the_committed_rust_abi(self): script = (ROOT / "shell" / "create_artifacts.sh").read_text() From 24bdc628736b9e734b9bb6d4e1c07e16e112459a Mon Sep 17 00:00:00 2001 From: fredo Date: Fri, 31 Jul 2026 14:55:11 +0200 Subject: [PATCH 04/11] ci(release): isolate Cargo publishing credentials --- .github/workflows/release.yml | 20 ++++++++--- test/release/test_release_workflow.py | 50 +++++++++++++++++++++------ 2 files changed, 54 insertions(+), 16 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 35e852d..6023627 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -54,12 +54,13 @@ jobs: - name: Publish to npm run: npm publish --access public --ignore-scripts=true - release-cargo: + release-cargo-verify: needs: create-artifacts runs-on: ubuntu-latest permissions: contents: read - id-token: write + outputs: + exists: ${{ steps.published.outputs.exists }} steps: - name: Checkout uses: actions/checkout@v4 @@ -130,16 +131,25 @@ jobs: ;; esac + release-cargo: + needs: release-cargo-verify + if: needs.release-cargo-verify.outputs.exists != 'true' + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Authenticate to crates.io - if: steps.published.outputs.exists != 'true' id: crates-io-auth uses: rust-lang/crates-io-auth-action@v1 - name: Publish to crates.io - if: steps.published.outputs.exists != 'true' env: CARGO_REGISTRY_TOKEN: ${{ steps.crates-io-auth.outputs.token }} - run: cargo publish --manifest-path bindings/rust/Cargo.toml + run: cargo publish --manifest-path bindings/rust/Cargo.toml --no-verify release-github: needs: create-artifacts permissions: diff --git a/test/release/test_release_workflow.py b/test/release/test_release_workflow.py index 3ddf2b1..83cc320 100644 --- a/test/release/test_release_workflow.py +++ b/test/release/test_release_workflow.py @@ -28,31 +28,59 @@ def test_npm_publish_runs_inline_in_the_trusted_workflow(self): ) self.assertNotIn("phylaxsystems/actions/release-npm", release_job) - def test_cargo_publish_runs_inline_with_trusted_publishing(self): + def test_cargo_verification_has_no_oidc_permission(self): + workflow = (ROOT / ".github" / "workflows" / "release.yml").read_text() + verify_job = workflow.split(" release-cargo-verify:\n", 1)[1].split( + " release-cargo:\n", 1 + )[0] + + self.assertIn("needs: create-artifacts", verify_job) + self.assertIn("contents: read", verify_job) + self.assertNotIn("id-token: write", verify_job) + self.assertIn("uses: actions/checkout@", verify_job) + self.assertIn("uses: actions/download-artifact@", verify_job) + self.assertIn("name: credible-layer-contracts-artifacts", verify_job) + self.assertIn("cmp -s artifacts/StateOracle.json", verify_job) + self.assertIn( + "cargo publish --manifest-path bindings/rust/Cargo.toml --dry-run", + verify_job, + ) + self.assertIn("id: published", verify_job) + self.assertIn("exists: ${{ steps.published.outputs.exists }}", verify_job) + self.assertNotIn("rust-lang/crates-io-auth-action", verify_job) + + def test_cargo_publish_job_is_minimal(self): workflow = (ROOT / ".github" / "workflows" / "release.yml").read_text() release_job = workflow.split(" release-cargo:\n", 1)[1].split( " release-github:\n", 1 )[0] - self.assertIn("needs: create-artifacts", release_job) - self.assertIn("id-token: write", release_job) - self.assertIn("uses: actions/checkout@", release_job) - self.assertIn("uses: actions/download-artifact@", release_job) - self.assertIn("name: credible-layer-contracts-artifacts", release_job) - self.assertIn("cmp -s artifacts/StateOracle.json", release_job) + self.assertIn("needs: release-cargo-verify", release_job) self.assertIn( - "cargo publish --manifest-path bindings/rust/Cargo.toml --dry-run", - release_job, + "if: needs.release-cargo-verify.outputs.exists != 'true'", release_job ) - self.assertIn("uses: rust-lang/crates-io-auth-action@v1", release_job) + self.assertIn("id-token: write", release_job) + self.assertIn("uses: actions/checkout@", release_job) + self.assertIn("uses: rust-lang/crates-io-auth-action@", release_job) self.assertIn( "CARGO_REGISTRY_TOKEN: ${{ steps.crates-io-auth.outputs.token }}", release_job, ) self.assertIn( - "run: cargo publish --manifest-path bindings/rust/Cargo.toml", + "run: cargo publish --manifest-path bindings/rust/Cargo.toml --no-verify", release_job, ) + for verification_step in ( + "rustup toolchain install", + "cargo fmt", + "cargo clippy", + "cargo test", + "--dry-run", + "actions/download-artifact", + "cmp -s", + "curl", + ): + self.assertNotIn(verification_step, release_job) def test_cargo_and_npm_packages_share_release_version(self): package = json.loads((ROOT / "package.json").read_text()) From 16db490c118ca455b5ce26d48f2fa6554728ce44 Mon Sep 17 00:00:00 2001 From: fredo Date: Fri, 31 Jul 2026 14:56:26 +0200 Subject: [PATCH 05/11] ci(release): pin Cargo publishing actions --- .github/workflows/release.yml | 8 ++++---- test/release/test_release_workflow.py | 22 ++++++++++++++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6023627..391be62 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -63,7 +63,7 @@ jobs: exists: ${{ steps.published.outputs.exists }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Setup Rust run: | @@ -71,7 +71,7 @@ jobs: rustup default stable - name: Download artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: credible-layer-contracts-artifacts path: artifacts/ @@ -140,11 +140,11 @@ jobs: id-token: write steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Authenticate to crates.io id: crates-io-auth - uses: rust-lang/crates-io-auth-action@v1 + uses: rust-lang/crates-io-auth-action@c6f97d42243bad5fab37ca0427f495c86d5b1a18 # v1.0.5 - name: Publish to crates.io env: diff --git a/test/release/test_release_workflow.py b/test/release/test_release_workflow.py index 83cc320..beaa0f5 100644 --- a/test/release/test_release_workflow.py +++ b/test/release/test_release_workflow.py @@ -1,4 +1,5 @@ import json +import re import tomllib import unittest from pathlib import Path @@ -82,6 +83,27 @@ def test_cargo_publish_job_is_minimal(self): ): self.assertNotIn(verification_step, release_job) + def test_cargo_release_actions_are_pinned_to_commit_shas(self): + workflow = (ROOT / ".github" / "workflows" / "release.yml").read_text() + cargo_jobs = workflow.split(" release-cargo-verify:\n", 1)[1].split( + " release-github:\n", 1 + )[0] + action_references = [ + line.strip() + for line in cargo_jobs.splitlines() + if line.strip().startswith("uses:") + ] + + self.assertTrue(action_references) + for action_reference in action_references: + self.assertIsNotNone( + re.fullmatch( + r"uses: [^@\s]+@[0-9a-f]{40}(?:\s+#\s+\S+)?", + action_reference, + ), + action_reference, + ) + def test_cargo_and_npm_packages_share_release_version(self): package = json.loads((ROOT / "package.json").read_text()) with (ROOT / "bindings" / "rust" / "Cargo.toml").open("rb") as manifest: From 17c215ee488b53e0b1c08da396abedfa69c0970d Mon Sep 17 00:00:00 2001 From: fredo Date: Fri, 31 Jul 2026 15:01:34 +0200 Subject: [PATCH 06/11] ci(release): isolate npm publishing credentials --- .github/workflows/release.yml | 69 ++++++++++++++++++++++----- test/release/test_release_workflow.py | 57 ++++++++++++++++++---- 2 files changed, 105 insertions(+), 21 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 391be62..f85d0d2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,34 +26,79 @@ jobs: with: name: credible-layer-contracts-artifacts path: artifacts/ - release-npm: + release-npm-verify: needs: create-artifacts runs-on: ubuntu-latest permissions: contents: read - id-token: write steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Setup Node - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: - node-version: 22 - registry-url: https://registry.npmjs.org - - # npm >= 11.5.1 is required for OIDC trusted publishing. - - name: Update npm - run: npm install -g npm@latest + node-version: 24.18.1 - name: Download artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: credible-layer-contracts-artifacts path: artifacts/ + - name: Build npm package + id: package + run: | + set -euo pipefail + npm_version=$(jq -r '.version' package.json) + if [[ "$npm_version" != "$GITHUB_REF_NAME" ]]; then + echo "::error::npm version ${npm_version} does not match tag ${GITHUB_REF_NAME}" + exit 1 + fi + package_file=$(npm pack --ignore-scripts=true --json | jq -r '.[0].filename') + if [[ ! -f "$package_file" ]]; then + echo "::error::npm pack did not create ${package_file}" + exit 1 + fi + echo "package_path=${package_file}" >> "$GITHUB_OUTPUT" + + - name: Upload verified npm package + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: credible-layer-contracts-npm-package + path: ${{ steps.package.outputs.package_path }} + if-no-files-found: error + retention-days: 1 + + release-npm: + needs: release-npm-verify + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - name: Setup Node + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: 24.18.1 + registry-url: https://registry.npmjs.org + + - name: Download verified npm package + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: credible-layer-contracts-npm-package + path: npm-package/ + - name: Publish to npm - run: npm publish --access public --ignore-scripts=true + run: | + set -euo pipefail + shopt -s nullglob + packages=(npm-package/*.tgz) + if [[ ${#packages[@]} -ne 1 ]]; then + echo "::error::Expected exactly one npm package, found ${#packages[@]}" + exit 1 + fi + npm publish "${packages[0]}" --access public --ignore-scripts=true release-cargo-verify: needs: create-artifacts runs-on: ubuntu-latest diff --git a/test/release/test_release_workflow.py b/test/release/test_release_workflow.py index beaa0f5..e23ab6e 100644 --- a/test/release/test_release_workflow.py +++ b/test/release/test_release_workflow.py @@ -9,25 +9,64 @@ class ReleaseWorkflowTest(unittest.TestCase): - def test_npm_publish_runs_inline_in_the_trusted_workflow(self): + def test_npm_package_verification_has_no_oidc_permission(self): + workflow = (ROOT / ".github" / "workflows" / "release.yml").read_text() + verify_job = workflow.split(" release-npm-verify:\n", 1)[1].split( + " release-npm:\n", 1 + )[0] + + self.assertIn("needs: create-artifacts", verify_job) + self.assertIn("contents: read", verify_job) + self.assertNotIn("id-token: write", verify_job) + self.assertIn("uses: actions/checkout@", verify_job) + self.assertIn("uses: actions/setup-node@", verify_job) + self.assertIn("uses: actions/download-artifact@", verify_job) + self.assertIn("name: credible-layer-contracts-artifacts", verify_job) + self.assertIn("npm pack --ignore-scripts=true --json", verify_job) + self.assertIn("uses: actions/upload-artifact@", verify_job) + self.assertIn("name: credible-layer-contracts-npm-package", verify_job) + + def test_npm_publish_job_only_publishes_verified_package(self): workflow = (ROOT / ".github" / "workflows" / "release.yml").read_text() release_job = workflow.split(" release-npm:\n", 1)[1].split( - " release-cargo:\n", 1 + " release-cargo-verify:\n", 1 )[0] + self.assertIn("needs: release-npm-verify", release_job) self.assertIn("id-token: write", release_job) - self.assertIn("runs-on: ubuntu-latest", release_job) - self.assertIn("uses: actions/checkout@", release_job) + self.assertNotIn("actions/checkout", release_job) self.assertIn("uses: actions/setup-node@", release_job) + self.assertIn("node-version: 24.18.1", release_job) self.assertIn("registry-url: https://registry.npmjs.org", release_job) - self.assertIn("run: npm install -g npm@latest", release_job) self.assertIn("uses: actions/download-artifact@", release_job) - self.assertIn("name: credible-layer-contracts-artifacts", release_job) - self.assertIn("path: artifacts/", release_job) + self.assertIn("name: credible-layer-contracts-npm-package", release_job) self.assertIn( - "run: npm publish --access public --ignore-scripts=true", release_job + 'npm publish "${packages[0]}" --access public --ignore-scripts=true', + release_job, ) - self.assertNotIn("phylaxsystems/actions/release-npm", release_job) + self.assertNotIn("npm install", release_job) + self.assertNotIn("npm pack --", release_job) + + def test_npm_release_actions_are_pinned_to_commit_shas(self): + workflow = (ROOT / ".github" / "workflows" / "release.yml").read_text() + npm_jobs = workflow.split(" release-npm-verify:\n", 1)[1].split( + " release-cargo-verify:\n", 1 + )[0] + action_references = [ + line.strip() + for line in npm_jobs.splitlines() + if line.strip().startswith("uses:") + ] + + self.assertTrue(action_references) + for action_reference in action_references: + self.assertIsNotNone( + re.fullmatch( + r"uses: [^@\s]+@[0-9a-f]{40}(?:\s+#\s+\S+)?", + action_reference, + ), + action_reference, + ) def test_cargo_verification_has_no_oidc_permission(self): workflow = (ROOT / ".github" / "workflows" / "release.yml").read_text() From 372cc9c55a93b2ac4ca8afff8395ee8b5e3908f9 Mon Sep 17 00:00:00 2001 From: fredo Date: Fri, 31 Jul 2026 17:52:19 +0200 Subject: [PATCH 07/11] ci(release): verify ABI before artifact upload --- .github/workflows/release.yml | 2 ++ test/release/test_release_workflow.py | 15 +++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f85d0d2..7b440f6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,6 +21,8 @@ jobs: version: nightly - name: Create artifacts run: ./shell/create_artifacts.sh + - name: Verify committed Rust ABI + run: git diff --exit-code -- bindings/rust/abi/StateOracle.json - name: Upload artifact directory uses: actions/upload-artifact@v4 with: diff --git a/test/release/test_release_workflow.py b/test/release/test_release_workflow.py index e23ab6e..d16ac55 100644 --- a/test/release/test_release_workflow.py +++ b/test/release/test_release_workflow.py @@ -9,6 +9,21 @@ class ReleaseWorkflowTest(unittest.TestCase): + def test_artifacts_are_verified_before_upload(self): + workflow = (ROOT / ".github" / "workflows" / "release.yml").read_text() + artifact_job = workflow.split(" create-artifacts:\n", 1)[1].split( + " release-npm-verify:\n", 1 + )[0] + + generation_index = artifact_job.index("run: ./shell/create_artifacts.sh") + verification_index = artifact_job.index( + "run: git diff --exit-code -- bindings/rust/abi/StateOracle.json" + ) + upload_index = artifact_job.index("uses: actions/upload-artifact@") + + self.assertLess(generation_index, verification_index) + self.assertLess(verification_index, upload_index) + def test_npm_package_verification_has_no_oidc_permission(self): workflow = (ROOT / ".github" / "workflows" / "release.yml").read_text() verify_job = workflow.split(" release-npm-verify:\n", 1)[1].split( From f17cedd9f7b163b972cee88c45e5b7927762cab7 Mon Sep 17 00:00:00 2001 From: fredo Date: Fri, 31 Jul 2026 17:53:43 +0200 Subject: [PATCH 08/11] ci(release): pin artifact generation inputs --- .github/workflows/release.yml | 8 ++++---- test/release/test_release_workflow.py | 24 ++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7b440f6..691d967 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,19 +12,19 @@ jobs: create-artifacts: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: submodules: recursive - name: Install Foundry - uses: foundry-rs/foundry-toolchain@v1 + uses: foundry-rs/foundry-toolchain@908c540300062bd5a7e473851cdb4282204cee09 # v1 with: - version: nightly + version: v1.7.1 - name: Create artifacts run: ./shell/create_artifacts.sh - name: Verify committed Rust ABI run: git diff --exit-code -- bindings/rust/abi/StateOracle.json - name: Upload artifact directory - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: credible-layer-contracts-artifacts path: artifacts/ diff --git a/test/release/test_release_workflow.py b/test/release/test_release_workflow.py index d16ac55..cb53813 100644 --- a/test/release/test_release_workflow.py +++ b/test/release/test_release_workflow.py @@ -24,6 +24,30 @@ def test_artifacts_are_verified_before_upload(self): self.assertLess(generation_index, verification_index) self.assertLess(verification_index, upload_index) + def test_artifact_generation_inputs_are_immutable(self): + workflow = (ROOT / ".github" / "workflows" / "release.yml").read_text() + artifact_job = workflow.split(" create-artifacts:\n", 1)[1].split( + " release-npm-verify:\n", 1 + )[0] + action_references = [ + line.strip() + for line in artifact_job.splitlines() + if line.strip().startswith("uses:") + ] + + self.assertTrue(action_references) + for action_reference in action_references: + self.assertIsNotNone( + re.fullmatch( + r"uses: [^@\s]+@[0-9a-f]{40}(?:\s+#\s+\S+)?", + action_reference, + ), + action_reference, + ) + self.assertRegex(artifact_job, r"version: v\d+\.\d+\.\d+") + self.assertNotIn("version: nightly", artifact_job) + self.assertNotIn("version: stable", artifact_job) + def test_npm_package_verification_has_no_oidc_permission(self): workflow = (ROOT / ".github" / "workflows" / "release.yml").read_text() verify_job = workflow.split(" release-npm-verify:\n", 1)[1].split( From ced999568ff13a356d929b937cf5d9fb1175a21a Mon Sep 17 00:00:00 2001 From: fredo Date: Fri, 31 Jul 2026 17:54:37 +0200 Subject: [PATCH 09/11] ci(release): inline pinned GitHub publishing --- .github/workflows/release.yml | 29 ++++++++++++++++++++----- test/release/test_release_workflow.py | 31 +++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 691d967..dc48825 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -199,10 +199,29 @@ jobs: run: cargo publish --manifest-path bindings/rust/Cargo.toml --no-verify release-github: needs: create-artifacts + runs-on: ubuntu-latest permissions: contents: write - uses: phylaxsystems/actions/.github/workflows/release-github.yaml@main - secrets: - SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} - with: - artifact_name: credible-layer-contracts-artifacts + steps: + - name: Download artifacts + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: credible-layer-contracts-artifacts + path: artifacts/ + + - name: Create GitHub release + env: + GH_REPO: ${{ github.repository }} + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + mapfile -d '' artifact_files < <(find artifacts -type f -print0) + if (( ${#artifact_files[@]} == 0 )); then + echo "::error::No release artifacts were downloaded" + exit 1 + fi + gh release create "$GITHUB_REF_NAME" \ + "${artifact_files[@]}" \ + --generate-notes \ + --title "$GITHUB_REF_NAME" \ + --verify-tag diff --git a/test/release/test_release_workflow.py b/test/release/test_release_workflow.py index cb53813..82379df 100644 --- a/test/release/test_release_workflow.py +++ b/test/release/test_release_workflow.py @@ -182,6 +182,37 @@ def test_cargo_release_actions_are_pinned_to_commit_shas(self): action_reference, ) + def test_github_release_is_local_and_uses_pinned_artifacts(self): + workflow = (ROOT / ".github" / "workflows" / "release.yml").read_text() + release_job = workflow.split(" release-github:\n", 1)[1] + action_references = [ + line.strip() + for line in release_job.splitlines() + if line.strip().startswith("uses:") + ] + + self.assertIn("runs-on: ubuntu-latest", release_job) + self.assertIn("contents: write", release_job) + self.assertNotIn("id-token: write", release_job) + self.assertNotIn("phylaxsystems/actions", release_job) + self.assertNotIn("SSH_PRIVATE_KEY", release_job) + self.assertNotIn("actions/checkout", release_job) + self.assertIn("uses: actions/download-artifact@", release_job) + self.assertIn("name: credible-layer-contracts-artifacts", release_job) + self.assertIn("gh release create", release_job) + self.assertIn('"$GITHUB_REF_NAME"', release_job) + self.assertIn("--generate-notes", release_job) + + self.assertTrue(action_references) + for action_reference in action_references: + self.assertIsNotNone( + re.fullmatch( + r"uses: [^@\s]+@[0-9a-f]{40}(?:\s+#\s+\S+)?", + action_reference, + ), + action_reference, + ) + def test_cargo_and_npm_packages_share_release_version(self): package = json.loads((ROOT / "package.json").read_text()) with (ROOT / "bindings" / "rust" / "Cargo.toml").open("rb") as manifest: From c4b3e71f3ac04916f54bec362f863508471594e5 Mon Sep 17 00:00:00 2001 From: fredo Date: Mon, 31 Aug 2026 16:18:44 +0200 Subject: [PATCH 10/11] feat(bindings): version StateOracle interface --- .github/workflows/release.yml | 6 +- .github/workflows/solidity-test.yml | 2 +- README.md | 11 +- bindings/rust/Cargo.toml | 2 +- bindings/rust/README.md | 23 ++- bindings/rust/RELEASING.md | 2 +- .../{StateOracle.json => IStateOracleV1.json} | 54 ++++-- bindings/rust/src/lib.rs | 27 +-- shell/create_artifacts.sh | 33 +++- src/interfaces/IStateOracleV1.sol | 161 ++++++++++++++++++ test/release/test_release_workflow.py | 6 +- 11 files changed, 276 insertions(+), 51 deletions(-) rename bindings/rust/abi/{StateOracle.json => IStateOracleV1.json} (97%) create mode 100644 src/interfaces/IStateOracleV1.sol diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dc48825..717c638 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,7 +22,7 @@ jobs: - name: Create artifacts run: ./shell/create_artifacts.sh - name: Verify committed Rust ABI - run: git diff --exit-code -- bindings/rust/abi/StateOracle.json + run: git diff --exit-code -- bindings/rust/abi/IStateOracleV1.json - name: Upload artifact directory uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: @@ -139,9 +139,9 @@ jobs: echo "::error::npm version ${npm_version} does not match tag ${GITHUB_REF_NAME}" exit 1 fi - if ! cmp -s artifacts/StateOracle.json bindings/rust/abi/StateOracle.json; then + if ! cmp -s artifacts/interfaces/IStateOracleV1.json bindings/rust/abi/IStateOracleV1.json; then echo "::error::Committed Rust ABI differs from the release artifact" - diff -u bindings/rust/abi/StateOracle.json artifacts/StateOracle.json || true + diff -u bindings/rust/abi/IStateOracleV1.json artifacts/interfaces/IStateOracleV1.json || true exit 1 fi diff --git a/.github/workflows/solidity-test.yml b/.github/workflows/solidity-test.yml index dd849f8..ebd44c9 100644 --- a/.github/workflows/solidity-test.yml +++ b/.github/workflows/solidity-test.yml @@ -30,7 +30,7 @@ jobs: - name: Verify generated ABI and Cargo package run: | ./shell/create_artifacts.sh - git diff --exit-code -- bindings/rust/abi/StateOracle.json + git diff --exit-code -- bindings/rust/abi/IStateOracleV1.json cargo fmt --manifest-path bindings/rust/Cargo.toml -- --check cargo clippy --manifest-path bindings/rust/Cargo.toml --all-targets -- -D warnings -D clippy::pedantic cargo test --manifest-path bindings/rust/Cargo.toml diff --git a/README.md b/README.md index 1e28829..86b0276 100644 --- a/README.md +++ b/README.md @@ -166,7 +166,7 @@ Every broadcast entrypoint in `DeployTestingAdminVerifiers.s.sol` likewise requi ### Rust bindings -Each tagged release publishes the StateOracle ABI as the +Each tagged release publishes versioned StateOracle interface bindings as the `credible-layer-contracts` Cargo crate. Registry consumers can install it with: ```bash @@ -181,9 +181,12 @@ repository. Pin a commit rather than a branch or tag: credible-layer-contracts = { git = "https://github.com/phylaxsystems/credible-layer-contracts.git", rev = "" } ``` -Both forms expose the generated binding as -`credible_layer_contracts::StateOracle`. The committed ABI snapshot allows git -consumers to build without installing Foundry or initializing submodules. +Both forms expose the first interface generation as +`credible_layer_contracts::state_oracle::v1::IStateOracleV1`. Interface +generations are independent from repository tags: `0.3.0` first publishes +`IStateOracleV1`, while a future breaking change will add `IStateOracleV2` +rather than replace V1. The committed ABI snapshots allow git consumers to +build without installing Foundry or initializing submodules. ### Solidity development diff --git a/bindings/rust/Cargo.toml b/bindings/rust/Cargo.toml index 4f89ec2..179e4ec 100644 --- a/bindings/rust/Cargo.toml +++ b/bindings/rust/Cargo.toml @@ -13,7 +13,7 @@ keywords = ["ethereum", "solidity", "abi", "bindings", "credible-layer"] publish = ["crates-io"] include = [ "src/**", - "abi/StateOracle.json", + "abi/IStateOracleV1.json", "README.md", "RELEASING.md", "LICENSE-MIT", diff --git a/bindings/rust/README.md b/bindings/rust/README.md index 7e8ee9d..b63b802 100644 --- a/bindings/rust/README.md +++ b/bindings/rust/README.md @@ -1,22 +1,33 @@ # Credible Layer contract bindings -Alloy Rust bindings for the production contracts maintained in +Alloy Rust bindings for the versioned consumer interfaces maintained in [`credible-layer-contracts`](https://github.com/phylaxsystems/credible-layer-contracts). The crate version follows the repository's contract release version. Its ABI -snapshot is generated from the same Foundry artifact that is published in the -`@phylax-systems/credible-layer-contracts` npm package. +snapshots are generated from the same canonical Solidity interfaces published +in the `@phylax-systems/credible-layer-contracts` npm package. ```bash cargo add credible-layer-contracts ``` ```rust -use credible_layer_contracts::StateOracle; +use credible_layer_contracts::state_oracle::v1::IStateOracleV1; ``` -Do not edit `abi/StateOracle.json` by hand. Run `./shell/create_artifacts.sh` -from the repository root and review the resulting ABI change. +## Interface compatibility + +Interface generations are independent from repository release versions. An +existing generation may grow additively, but a breaking ABI change introduces +a new Solidity interface and Rust module. Published generations remain +available so consumers can select the interface used by their deployment. + +| First release | Solidity interface | Rust module | +| --- | --- | --- | +| `0.3.0` | `IStateOracleV1` | `state_oracle::v1` | + +Do not edit files under `abi/` by hand. Run `./shell/create_artifacts.sh` from +the repository root and review the resulting ABI changes. Maintainers should follow [`RELEASING.md`](RELEASING.md) for the one-time crates.io bootstrap and subsequent tagged releases. diff --git a/bindings/rust/RELEASING.md b/bindings/rust/RELEASING.md index f909ffd..7376d05 100644 --- a/bindings/rust/RELEASING.md +++ b/bindings/rust/RELEASING.md @@ -27,4 +27,4 @@ tagged: Use `./shell/bump-version.sh patch|minor|major`, review the resulting commit and tag, then push both. The release fails before publishing if the npm version, -Cargo version, tag, or committed StateOracle ABI disagree. +Cargo version, tag, or committed versioned interface ABI disagree. diff --git a/bindings/rust/abi/StateOracle.json b/bindings/rust/abi/IStateOracleV1.json similarity index 97% rename from bindings/rust/abi/StateOracle.json rename to bindings/rust/abi/IStateOracleV1.json index 6d57535..f1f3716 100644 --- a/bindings/rust/abi/StateOracle.json +++ b/bindings/rust/abi/IStateOracleV1.json @@ -1,15 +1,4 @@ [ - { - "type": "constructor", - "inputs": [ - { - "name": "assertionTimelockBlocks", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "nonpayable" - }, { "type": "function", "name": "ASSERTION_TIMELOCK_BLOCKS", @@ -551,6 +540,44 @@ "outputs": [], "stateMutability": "nonpayable" }, + { + "type": "function", + "name": "initializeWithWhitelist", + "inputs": [ + { + "name": "admin", + "type": "address", + "internalType": "address" + }, + { + "name": "_adminVerifiers", + "type": "address[]", + "internalType": "contract IAdminVerifier[]" + }, + { + "name": "_daVerifiers", + "type": "address[]", + "internalType": "contract IDAVerifier[]" + }, + { + "name": "_maxAssertionsPerAA", + "type": "uint16", + "internalType": "uint16" + }, + { + "name": "_whitelistEnabled", + "type": "bool", + "internalType": "bool" + }, + { + "name": "_initialWhitelist", + "type": "address[]", + "internalType": "address[]" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, { "type": "function", "name": "isAdminVerifierRegistered", @@ -1471,11 +1498,6 @@ "name": "DAVerifierNotRegistered", "inputs": [] }, - { - "type": "error", - "name": "DAVerifierNotRegistered", - "inputs": [] - }, { "type": "error", "name": "InvalidAssertionTimelock", diff --git a/bindings/rust/src/lib.rs b/bindings/rust/src/lib.rs index 5e7bbd3..3e7e3ef 100644 --- a/bindings/rust/src/lib.rs +++ b/bindings/rust/src/lib.rs @@ -1,38 +1,43 @@ //! Alloy bindings for the Credible Layer protocol contracts. //! -//! The ABI snapshot is generated by the repository's artifact pipeline. It is -//! committed so registry and git consumers do not need Foundry or submodules. +//! Versioned ABI snapshots are generated from canonical Solidity interfaces by +//! the repository's artifact pipeline. They are committed so registry and git +//! consumers do not need Foundry or submodules. -use alloy_sol_types::sol; - -sol!(StateOracle, "abi/StateOracle.json"); +/// `StateOracle` bindings grouped by interface generation. +pub mod state_oracle { + /// First published `StateOracle` interface generation. + pub mod v1 { + alloy_sol_types::sol!(IStateOracleV1, "abi/IStateOracleV1.json"); + } +} #[cfg(test)] mod tests { use alloy_sol_types::{SolCall, SolError, SolEvent, TopicList}; - use super::StateOracle; + use super::state_oracle::v1::IStateOracleV1; #[test] fn state_oracle_boundary_matches_representative_canonical_entries() { assert_eq!( - StateOracle::registerAssertionAdopterCall::SIGNATURE, + IStateOracleV1::registerAssertionAdopterCall::SIGNATURE, "registerAssertionAdopter(address,address,bytes)" ); assert_eq!( - StateOracle::getAssertionWindowCall::SIGNATURE, + IStateOracleV1::getAssertionWindowCall::SIGNATURE, "getAssertionWindow(address,bytes32)" ); assert_eq!( - StateOracle::AssertionAdded::SIGNATURE, + IStateOracleV1::AssertionAdded::SIGNATURE, "AssertionAdded(address,bytes32,uint256,address,bytes,bytes)" ); assert_eq!( - StateOracle::InvalidDAProof::SIGNATURE, + IStateOracleV1::InvalidDAProof::SIGNATURE, "InvalidDAProof(address)" ); assert_eq!( - <::TopicList as TopicList>::COUNT, + <::TopicList as TopicList>::COUNT, 4 ); } diff --git a/shell/create_artifacts.sh b/shell/create_artifacts.sh index 519140e..4617a3e 100755 --- a/shell/create_artifacts.sh +++ b/shell/create_artifacts.sh @@ -22,12 +22,34 @@ extract_abi() { echo "Extracted ABI to $target_dir/$output_name" } +# Normalize an ABI for comparison while ignoring constructor entries and +# Solidity-only internal type annotations. Duplicate entries are collapsed +# because inherited errors can appear more than once in implementation ABIs. +normalize_public_abi() { + jq -S ' + [.abi[] + | select(.type != "constructor") + | walk(if type == "object" then del(.internalType) else . end)] + | unique + | sort_by(.type, (.name // ""), ((.inputs // []) | tostring), ((.outputs // []) | tostring)) + ' "$1" +} + # Change to the root directory before running forge cd "$ROOT_DIR" # Generate the artifacts with Forge forge build +# The versioned interface is the canonical consumer boundary used by the Rust +# crate. Refuse to publish if it drifts from the implementation surface. +if ! diff -u \ + <(normalize_public_abi "$ROOT_DIR/out/StateOracle.sol/StateOracle.json") \ + <(normalize_public_abi "$ROOT_DIR/out/IStateOracleV1.sol/IStateOracleV1.json"); then + echo "StateOracle implementation ABI differs from IStateOracleV1" >&2 + exit 1 +fi + # Delete the current artifacts ARTIFACTS="$ROOT_DIR/artifacts" rm -rf "$ARTIFACTS" @@ -47,15 +69,16 @@ INTERFACES="${ARTIFACTS}/interfaces" extract_abi "$ROOT_DIR/out/IBatch.sol/IBatch.json" "${INTERFACES}" extract_abi "$ROOT_DIR/out/IDAVerifier.sol/IDAVerifier.json" "${INTERFACES}" extract_abi "$ROOT_DIR/out/IAdminVerifier.sol/IAdminVerifier.json" "${INTERFACES}" +extract_abi "$ROOT_DIR/out/IStateOracleV1.sol/IStateOracleV1.json" "${INTERFACES}" # Extract ABIs for libraries LIBRARIES="${ARTIFACTS}/libraries" extract_abi "$ROOT_DIR/out/AdminVerifierRegistry.sol/AdminVerifierRegistry.json" "${LIBRARIES}" -# Keep the committed Rust binding input byte-for-byte aligned with the ABI -# published to npm. This snapshot lets Cargo git dependencies build without -# Foundry or initialized submodules. +# Keep the committed Rust binding input byte-for-byte aligned with the +# versioned interface ABI published to npm. This snapshot lets Cargo git +# dependencies build without Foundry or initialized submodules. RUST_BINDINGS_ABI="${ROOT_DIR}/bindings/rust/abi" mkdir -p "${RUST_BINDINGS_ABI}" -cp "${ARTIFACTS}/StateOracle.json" "${RUST_BINDINGS_ABI}/StateOracle.json" -echo "Synced StateOracle ABI to ${RUST_BINDINGS_ABI}/StateOracle.json" +cp "${INTERFACES}/IStateOracleV1.json" "${RUST_BINDINGS_ABI}/IStateOracleV1.json" +echo "Synced IStateOracleV1 ABI to ${RUST_BINDINGS_ABI}/IStateOracleV1.json" diff --git a/src/interfaces/IStateOracleV1.sol b/src/interfaces/IStateOracleV1.sol new file mode 100644 index 0000000..34382af --- /dev/null +++ b/src/interfaces/IStateOracleV1.sol @@ -0,0 +1,161 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.28; + +import {IAdminVerifier} from "./IAdminVerifier.sol"; +import {IDAVerifier} from "./IDAVerifier.sol"; + +/// @title IStateOracleV1 +/// @notice Canonical consumer interface for the first StateOracle ABI generation. +/// @dev This interface is the source for published bindings. Breaking changes require a new +/// interface generation; release tags are mapped to interface generations in the bindings README. +interface IStateOracleV1 { + error AccessControlBadConfirmation(); + error AccessControlUnauthorizedAccount(address account, bytes32 neededRole); + error AccountNotWhitelisted(address account); + error AdminVerifierAlreadyRegistered(); + error AdminVerifierNotRegistered(); + error AlreadyWhitelisted(address account); + error AssertionAdopterAlreadyRegistered(); + error AssertionAdopterNotRegistered(); + error AssertionAlreadyExists(); + error AssertionAlreadyRemoved(); + error AssertionDoesNotExist(); + error BatchError(bytes result); + error CannotGrantDefaultAdminRole(); + error CannotRenounceOwnerDefaultAdminRole(); + error CannotRevokeOwnerDefaultAdminRole(); + error DAVerifierAlreadyRegistered(); + error DAVerifierNotRegistered(); + error InvalidAssertionTimelock(); + error InvalidDAProof(IDAVerifier daVerifier); + error InvalidInitialization(); + error InvalidManagerTransferRequest(); + error NoPendingManager(); + error NotInitializing(); + error NotWhitelisted(); + error OwnableInvalidOwner(address owner); + error OwnableUnauthorizedAccount(address account); + error TooManyAssertions(); + error UnauthorizedManager(); + error UnauthorizedRegistrant(); + error WhitelistAlreadyDisabled(); + error WhitelistAlreadyEnabled(); + + event AddedToWhitelist(address indexed account); + event AdminVerifierAdded(IAdminVerifier adminVerifier); + event AdminVerifierRemoved(IAdminVerifier adminVerifier); + event AssertionAdded( + address indexed assertionAdopter, + bytes32 indexed assertionId, + uint256 activationBlock, + IDAVerifier indexed daVerifier, + bytes metadata, + bytes proof + ); + event AssertionAdopterAdded(address indexed contractAddress, address indexed manager, IAdminVerifier adminVerifier); + event AssertionRemoved(address indexed assertionAdopter, bytes32 indexed assertionId, uint256 deactivationBlock); + event DAVerifierAdded(IDAVerifier daVerifier); + event DAVerifierRemoved(IDAVerifier daVerifier); + event Initialized(uint64 version); + event ManagerTransferRequested( + address indexed contractAddress, address indexed manager, address indexed newManager + ); + event ManagerTransferred(address indexed contractAddress, address indexed newManager); + event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner); + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + event RemovedFromWhitelist(address indexed account); + event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole); + event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender); + event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender); + event StorageReset(address indexed adopter, bytes32 indexed storageKey, uint256 resetBlock); + event WhitelistDisabled(); + event WhitelistEnabled(); + + function ASSERTION_TIMELOCK_BLOCKS() external view returns (uint256); + function DEFAULT_ADMIN_ROLE() external view returns (bytes32); + function GOVERNANCE_ROLE() external view returns (bytes32); + function GUARDIAN_ADMIN_ROLE() external view returns (bytes32); + function GUARDIAN_ROLE() external view returns (bytes32); + function OPERATOR_ADMIN_ROLE() external view returns (bytes32); + function OPERATOR_ROLE() external view returns (bytes32); + function acceptManagerTransfer(address contractAddress) external; + function acceptOwnership() external; + function addAdminVerifier(IAdminVerifier adminVerifier) external; + function addAssertion( + address contractAddress, + bytes32 assertionId, + IDAVerifier daVerifier, + bytes calldata metadata, + bytes calldata proof + ) external; + function addDAVerifier(IDAVerifier daVerifier) external; + function addToWhitelist(address account) external; + function adminVerifiers(IAdminVerifier adminVerifier) external view returns (bool isRegistered); + function assertionAdopters(address) + external + view + returns (address manager, address pendingManager, uint16 assertionCount); + function batch(bytes[] calldata calls) external; + function daVerifiers(IDAVerifier daVerifier) external view returns (bool isRegistered); + function disableWhitelist() external; + function enableWhitelist() external; + function getAssertionCount(address contractAddress) external view returns (uint16 assertionCount); + function getAssertionWindow(address contractAddress, bytes32 assertionId) + external + view + returns (uint256 activationBlock, uint256 deactivationBlock); + function getManager(address contractAddress) external view returns (address manager); + function getPendingManager(address contractAddress) external view returns (address pendingManager); + function getRoleAdmin(bytes32 role) external view returns (bytes32); + function grantGovernanceRole(address governance) external; + function grantGuardianAdminRole(address guardianAdmin) external; + function grantGuardianRole(address guardian) external; + function grantOperatorAdminRole(address operatorAdmin) external; + function grantOperatorRole(address operator) external; + function grantRole(bytes32 role, address account) external; + function hasAssertion(address contractAddress, bytes32 assertionId) external view returns (bool isAssociated); + function hasRole(bytes32 role, address account) external view returns (bool); + function initialize( + address admin, + IAdminVerifier[] calldata _adminVerifiers, + IDAVerifier[] calldata _daVerifiers, + uint16 _maxAssertionsPerAA + ) external; + function initializeWithWhitelist( + address admin, + IAdminVerifier[] calldata _adminVerifiers, + IDAVerifier[] calldata _daVerifiers, + uint16 _maxAssertionsPerAA, + bool _whitelistEnabled, + address[] calldata _initialWhitelist + ) external; + function isAdminVerifierRegistered(IAdminVerifier adminVerifier) external view returns (bool isRegistered); + function isDAVerifierRegistered(IDAVerifier daVerifier) external view returns (bool isRegistered); + function isWhitelisted(address account) external view returns (bool); + function maxAssertionsPerAA() external view returns (uint16); + function owner() external view returns (address); + function pendingOwner() external view returns (address); + function registerAssertionAdopter(address contractAddress, IAdminVerifier adminVerifier, bytes calldata data) + external; + function removeAdminVerifier(IAdminVerifier adminVerifier) external; + function removeAssertion(address contractAddress, bytes32 assertionId) external; + function removeAssertionByGuardian(address contractAddress, bytes32 assertionId) external; + function removeDAVerifier(IDAVerifier daVerifier) external; + function removeFromWhitelist(address account) external; + function renounceOwnership() external; + function renounceRole(bytes32 role, address callerConfirmation) external; + function resetStorage(address adopter, bytes32 storageKey) external; + function revokeGovernanceRole(address governance) external; + function revokeGuardianAdminRole(address guardianAdmin) external; + function revokeGuardianRole(address guardian) external; + function revokeManager(address contractAddress) external; + function revokeOperatorAdminRole(address operatorAdmin) external; + function revokeOperatorRole(address operator) external; + function revokeRole(bytes32 role, address account) external; + function setMaxAssertionsPerAA(uint16 _maxAssertionsPerAA) external; + function supportsInterface(bytes4 interfaceId) external view returns (bool); + function transferManager(address contractAddress, address newManager) external; + function transferOwnership(address newOwner) external; + function whitelist(address) external view returns (bool); + function whitelistEnabled() external view returns (bool); +} diff --git a/test/release/test_release_workflow.py b/test/release/test_release_workflow.py index 82379df..0970570 100644 --- a/test/release/test_release_workflow.py +++ b/test/release/test_release_workflow.py @@ -17,7 +17,7 @@ def test_artifacts_are_verified_before_upload(self): generation_index = artifact_job.index("run: ./shell/create_artifacts.sh") verification_index = artifact_job.index( - "run: git diff --exit-code -- bindings/rust/abi/StateOracle.json" + "run: git diff --exit-code -- bindings/rust/abi/IStateOracleV1.json" ) upload_index = artifact_job.index("uses: actions/upload-artifact@") @@ -119,7 +119,7 @@ def test_cargo_verification_has_no_oidc_permission(self): self.assertIn("uses: actions/checkout@", verify_job) self.assertIn("uses: actions/download-artifact@", verify_job) self.assertIn("name: credible-layer-contracts-artifacts", verify_job) - self.assertIn("cmp -s artifacts/StateOracle.json", verify_job) + self.assertIn("cmp -s artifacts/interfaces/IStateOracleV1.json", verify_job) self.assertIn( "cargo publish --manifest-path bindings/rust/Cargo.toml --dry-run", verify_job, @@ -256,7 +256,7 @@ def test_artifact_generation_updates_the_committed_rust_abi(self): script = (ROOT / "shell" / "create_artifacts.sh").read_text() self.assertIn('bindings/rust/abi', script) - self.assertIn('cp "${ARTIFACTS}/StateOracle.json"', script) + self.assertIn('cp "${INTERFACES}/IStateOracleV1.json"', script) def test_package_requests_provenance(self): package = json.loads((ROOT / "package.json").read_text()) From ef7d30503d61e01b15076b1e3294f404e42dadcb Mon Sep 17 00:00:00 2001 From: fredo Date: Mon, 31 Aug 2026 16:58:40 +0200 Subject: [PATCH 11/11] fix(bindings): preserve StateOracle ABI generations --- .github/workflows/release.yml | 14 +- .github/workflows/solidity-test.yml | 21 +- README.md | 12 +- bindings/rust/Cargo.toml | 1 + bindings/rust/README.md | 15 +- bindings/rust/RELEASING.md | 2 +- bindings/rust/abi/IStateOracleV1.json | 256 +--- bindings/rust/abi/IStateOracleV2.json | 1589 +++++++++++++++++++++++++ bindings/rust/src/lib.rs | 40 +- foundry.toml | 1 + shell/create_artifacts.sh | 12 +- src/interfaces/IStateOracleV1.sol | 58 +- src/interfaces/IStateOracleV2.sol | 161 +++ test/release/test_release_workflow.py | 46 +- 14 files changed, 1920 insertions(+), 308 deletions(-) create mode 100644 bindings/rust/abi/IStateOracleV2.json create mode 100644 src/interfaces/IStateOracleV2.sol diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 717c638..a2a377a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,7 +22,7 @@ jobs: - name: Create artifacts run: ./shell/create_artifacts.sh - name: Verify committed Rust ABI - run: git diff --exit-code -- bindings/rust/abi/IStateOracleV1.json + run: git diff --exit-code -- bindings/rust/abi/IStateOracleV1.json bindings/rust/abi/IStateOracleV2.json - name: Upload artifact directory uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: @@ -139,11 +139,13 @@ jobs: echo "::error::npm version ${npm_version} does not match tag ${GITHUB_REF_NAME}" exit 1 fi - if ! cmp -s artifacts/interfaces/IStateOracleV1.json bindings/rust/abi/IStateOracleV1.json; then - echo "::error::Committed Rust ABI differs from the release artifact" - diff -u bindings/rust/abi/IStateOracleV1.json artifacts/interfaces/IStateOracleV1.json || true - exit 1 - fi + for interface in IStateOracleV1 IStateOracleV2; do + if ! cmp -s "artifacts/interfaces/${interface}.json" "bindings/rust/abi/${interface}.json"; then + echo "::error::Committed ${interface} Rust ABI differs from the release artifact" + diff -u "bindings/rust/abi/${interface}.json" "artifacts/interfaces/${interface}.json" || true + exit 1 + fi + done - name: Verify Cargo package run: | diff --git a/.github/workflows/solidity-test.yml b/.github/workflows/solidity-test.yml index ebd44c9..ac57b0f 100644 --- a/.github/workflows/solidity-test.yml +++ b/.github/workflows/solidity-test.yml @@ -22,7 +22,7 @@ jobs: - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 with: - version: nightly + version: v1.7.1 - name: Setup Rust run: | rustup toolchain install stable --profile minimal --component clippy,rustfmt --no-self-update @@ -30,7 +30,7 @@ jobs: - name: Verify generated ABI and Cargo package run: | ./shell/create_artifacts.sh - git diff --exit-code -- bindings/rust/abi/IStateOracleV1.json + git diff --exit-code -- bindings/rust/abi/IStateOracleV1.json bindings/rust/abi/IStateOracleV2.json cargo fmt --manifest-path bindings/rust/Cargo.toml -- --check cargo clippy --manifest-path bindings/rust/Cargo.toml --all-targets -- -D warnings -D clippy::pedantic cargo test --manifest-path bindings/rust/Cargo.toml @@ -48,8 +48,25 @@ jobs: solidity-base: uses: phylaxsystems/actions/.github/workflows/solidity-base.yaml@main with: + # Run this separately below with a pinned Foundry version. The reusable + # workflow otherwise installs latest and can invalidate snapshots without + # any contract change. + disable-gas-snapshot: true gas-diff-tolerance: 25 + gas-snapshot: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + with: + submodules: recursive + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@908c540300062bd5a7e473851cdb4282204cee09 # v1 + with: + version: v1.7.1 + - name: Compare gas reports + run: FOUNDRY_PROFILE=snapshot forge snapshot --check --silent --tolerance 25 + contract-compatibility: runs-on: ubuntu-latest steps: diff --git a/README.md b/README.md index 86b0276..bb1b7f5 100644 --- a/README.md +++ b/README.md @@ -181,12 +181,12 @@ repository. Pin a commit rather than a branch or tag: credible-layer-contracts = { git = "https://github.com/phylaxsystems/credible-layer-contracts.git", rev = "" } ``` -Both forms expose the first interface generation as -`credible_layer_contracts::state_oracle::v1::IStateOracleV1`. Interface -generations are independent from repository tags: `0.3.0` first publishes -`IStateOracleV1`, while a future breaking change will add `IStateOracleV2` -rather than replace V1. The committed ABI snapshots allow git consumers to -build without installing Foundry or initializing submodules. +Both forms expose the historical `0.2.0` interface as +`credible_layer_contracts::state_oracle::v1::IStateOracleV1` and the current +`0.3.0` interface as `credible_layer_contracts::state_oracle::v2::IStateOracleV2`. +Future breaking changes add another interface generation rather than replacing +an existing one. The committed ABI snapshots allow git consumers to build +without installing Foundry or initializing submodules. ### Solidity development diff --git a/bindings/rust/Cargo.toml b/bindings/rust/Cargo.toml index 179e4ec..87a3071 100644 --- a/bindings/rust/Cargo.toml +++ b/bindings/rust/Cargo.toml @@ -14,6 +14,7 @@ publish = ["crates-io"] include = [ "src/**", "abi/IStateOracleV1.json", + "abi/IStateOracleV2.json", "README.md", "RELEASING.md", "LICENSE-MIT", diff --git a/bindings/rust/README.md b/bindings/rust/README.md index b63b802..7d86b62 100644 --- a/bindings/rust/README.md +++ b/bindings/rust/README.md @@ -12,19 +12,20 @@ cargo add credible-layer-contracts ``` ```rust -use credible_layer_contracts::state_oracle::v1::IStateOracleV1; +use credible_layer_contracts::state_oracle::v2::IStateOracleV2; ``` ## Interface compatibility -Interface generations are independent from repository release versions. An -existing generation may grow additively, but a breaking ABI change introduces -a new Solidity interface and Rust module. Published generations remain -available so consumers can select the interface used by their deployment. +Interface generations are independent from repository release versions. The +active generation may grow additively, but a breaking ABI change introduces a +new Solidity interface and Rust module. Superseded generations are frozen and +remain available so consumers can select the interface used by their deployment. -| First release | Solidity interface | Rust module | +| Contract release | Solidity interface | Rust module | | --- | --- | --- | -| `0.3.0` | `IStateOracleV1` | `state_oracle::v1` | +| `0.2.0` | `IStateOracleV1` | `state_oracle::v1` | +| `0.3.0` | `IStateOracleV2` | `state_oracle::v2` | Do not edit files under `abi/` by hand. Run `./shell/create_artifacts.sh` from the repository root and review the resulting ABI changes. diff --git a/bindings/rust/RELEASING.md b/bindings/rust/RELEASING.md index 7376d05..8376dca 100644 --- a/bindings/rust/RELEASING.md +++ b/bindings/rust/RELEASING.md @@ -27,4 +27,4 @@ tagged: Use `./shell/bump-version.sh patch|minor|major`, review the resulting commit and tag, then push both. The release fails before publishing if the npm version, -Cargo version, tag, or committed versioned interface ABI disagree. +Cargo version, tag, or committed versioned interface ABIs disagree. diff --git a/bindings/rust/abi/IStateOracleV1.json b/bindings/rust/abi/IStateOracleV1.json index f1f3716..fba6225 100644 --- a/bindings/rust/abi/IStateOracleV1.json +++ b/bindings/rust/abi/IStateOracleV1.json @@ -6,8 +6,21 @@ "outputs": [ { "name": "", - "type": "uint256", - "internalType": "uint256" + "type": "uint128", + "internalType": "uint128" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "DA_VERIFIER", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract IDAVerifier" } ], "stateMutability": "view" @@ -137,11 +150,6 @@ "type": "bytes32", "internalType": "bytes32" }, - { - "name": "daVerifier", - "type": "address", - "internalType": "contract IDAVerifier" - }, { "name": "metadata", "type": "bytes", @@ -156,19 +164,6 @@ "outputs": [], "stateMutability": "nonpayable" }, - { - "type": "function", - "name": "addDAVerifier", - "inputs": [ - { - "name": "daVerifier", - "type": "address", - "internalType": "contract IDAVerifier" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, { "type": "function", "name": "addToWhitelist", @@ -243,25 +238,6 @@ "outputs": [], "stateMutability": "nonpayable" }, - { - "type": "function", - "name": "daVerifiers", - "inputs": [ - { - "name": "daVerifier", - "type": "address", - "internalType": "contract IDAVerifier" - } - ], - "outputs": [ - { - "name": "isRegistered", - "type": "bool", - "internalType": "bool" - } - ], - "stateMutability": "view" - }, { "type": "function", "name": "disableWhitelist", @@ -313,13 +289,13 @@ "outputs": [ { "name": "activationBlock", - "type": "uint256", - "internalType": "uint256" + "type": "uint128", + "internalType": "uint128" }, { "name": "deactivationBlock", - "type": "uint256", - "internalType": "uint256" + "type": "uint128", + "internalType": "uint128" } ], "stateMutability": "view" @@ -526,53 +502,10 @@ "type": "address[]", "internalType": "contract IAdminVerifier[]" }, - { - "name": "_daVerifiers", - "type": "address[]", - "internalType": "contract IDAVerifier[]" - }, - { - "name": "_maxAssertionsPerAA", - "type": "uint16", - "internalType": "uint16" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "initializeWithWhitelist", - "inputs": [ - { - "name": "admin", - "type": "address", - "internalType": "address" - }, - { - "name": "_adminVerifiers", - "type": "address[]", - "internalType": "contract IAdminVerifier[]" - }, - { - "name": "_daVerifiers", - "type": "address[]", - "internalType": "contract IDAVerifier[]" - }, { "name": "_maxAssertionsPerAA", "type": "uint16", "internalType": "uint16" - }, - { - "name": "_whitelistEnabled", - "type": "bool", - "internalType": "bool" - }, - { - "name": "_initialWhitelist", - "type": "address[]", - "internalType": "address[]" } ], "outputs": [], @@ -597,25 +530,6 @@ ], "stateMutability": "view" }, - { - "type": "function", - "name": "isDAVerifierRegistered", - "inputs": [ - { - "name": "daVerifier", - "type": "address", - "internalType": "contract IDAVerifier" - } - ], - "outputs": [ - { - "name": "isRegistered", - "type": "bool", - "internalType": "bool" - } - ], - "stateMutability": "view" - }, { "type": "function", "name": "isWhitelisted", @@ -746,19 +660,6 @@ "outputs": [], "stateMutability": "nonpayable" }, - { - "type": "function", - "name": "removeDAVerifier", - "inputs": [ - { - "name": "daVerifier", - "type": "address", - "internalType": "contract IDAVerifier" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, { "type": "function", "name": "removeFromWhitelist", @@ -797,24 +698,6 @@ "outputs": [], "stateMutability": "nonpayable" }, - { - "type": "function", - "name": "resetStorage", - "inputs": [ - { - "name": "adopter", - "type": "address", - "internalType": "address" - }, - { - "name": "storageKey", - "type": "bytes32", - "internalType": "bytes32" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, { "type": "function", "name": "revokeGovernanceRole", @@ -1052,13 +935,13 @@ { "name": "assertionAdopter", "type": "address", - "indexed": true, + "indexed": false, "internalType": "address" }, { "name": "assertionId", "type": "bytes32", - "indexed": true, + "indexed": false, "internalType": "bytes32" }, { @@ -1066,24 +949,6 @@ "type": "uint256", "indexed": false, "internalType": "uint256" - }, - { - "name": "daVerifier", - "type": "address", - "indexed": true, - "internalType": "contract IDAVerifier" - }, - { - "name": "metadata", - "type": "bytes", - "indexed": false, - "internalType": "bytes" - }, - { - "name": "proof", - "type": "bytes", - "indexed": false, - "internalType": "bytes" } ], "anonymous": false @@ -1120,13 +985,13 @@ { "name": "assertionAdopter", "type": "address", - "indexed": true, + "indexed": false, "internalType": "address" }, { "name": "assertionId", "type": "bytes32", - "indexed": true, + "indexed": false, "internalType": "bytes32" }, { @@ -1138,32 +1003,6 @@ ], "anonymous": false }, - { - "type": "event", - "name": "DAVerifierAdded", - "inputs": [ - { - "name": "daVerifier", - "type": "address", - "indexed": false, - "internalType": "contract IDAVerifier" - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "DAVerifierRemoved", - "inputs": [ - { - "name": "daVerifier", - "type": "address", - "indexed": false, - "internalType": "contract IDAVerifier" - } - ], - "anonymous": false - }, { "type": "event", "name": "Initialized", @@ -1347,31 +1186,6 @@ ], "anonymous": false }, - { - "type": "event", - "name": "StorageReset", - "inputs": [ - { - "name": "adopter", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "storageKey", - "type": "bytes32", - "indexed": true, - "internalType": "bytes32" - }, - { - "name": "resetBlock", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - } - ], - "anonymous": false - }, { "type": "event", "name": "WhitelistDisabled", @@ -1488,16 +1302,6 @@ "name": "CannotRevokeOwnerDefaultAdminRole", "inputs": [] }, - { - "type": "error", - "name": "DAVerifierAlreadyRegistered", - "inputs": [] - }, - { - "type": "error", - "name": "DAVerifierNotRegistered", - "inputs": [] - }, { "type": "error", "name": "InvalidAssertionTimelock", @@ -1505,23 +1309,17 @@ }, { "type": "error", - "name": "InvalidDAProof", - "inputs": [ - { - "name": "daVerifier", - "type": "address", - "internalType": "contract IDAVerifier" - } - ] + "name": "InvalidInitialization", + "inputs": [] }, { "type": "error", - "name": "InvalidInitialization", + "name": "InvalidManagerTransferRequest", "inputs": [] }, { "type": "error", - "name": "InvalidManagerTransferRequest", + "name": "InvalidProof", "inputs": [] }, { diff --git a/bindings/rust/abi/IStateOracleV2.json b/bindings/rust/abi/IStateOracleV2.json new file mode 100644 index 0000000..f1f3716 --- /dev/null +++ b/bindings/rust/abi/IStateOracleV2.json @@ -0,0 +1,1589 @@ +[ + { + "type": "function", + "name": "ASSERTION_TIMELOCK_BLOCKS", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "DEFAULT_ADMIN_ROLE", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "GOVERNANCE_ROLE", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "GUARDIAN_ADMIN_ROLE", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "GUARDIAN_ROLE", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "OPERATOR_ADMIN_ROLE", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "OPERATOR_ROLE", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "acceptManagerTransfer", + "inputs": [ + { + "name": "contractAddress", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "acceptOwnership", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "addAdminVerifier", + "inputs": [ + { + "name": "adminVerifier", + "type": "address", + "internalType": "contract IAdminVerifier" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "addAssertion", + "inputs": [ + { + "name": "contractAddress", + "type": "address", + "internalType": "address" + }, + { + "name": "assertionId", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "daVerifier", + "type": "address", + "internalType": "contract IDAVerifier" + }, + { + "name": "metadata", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "proof", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "addDAVerifier", + "inputs": [ + { + "name": "daVerifier", + "type": "address", + "internalType": "contract IDAVerifier" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "addToWhitelist", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "adminVerifiers", + "inputs": [ + { + "name": "adminVerifier", + "type": "address", + "internalType": "contract IAdminVerifier" + } + ], + "outputs": [ + { + "name": "isRegistered", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "assertionAdopters", + "inputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "manager", + "type": "address", + "internalType": "address" + }, + { + "name": "pendingManager", + "type": "address", + "internalType": "address" + }, + { + "name": "assertionCount", + "type": "uint16", + "internalType": "uint16" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "batch", + "inputs": [ + { + "name": "calls", + "type": "bytes[]", + "internalType": "bytes[]" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "daVerifiers", + "inputs": [ + { + "name": "daVerifier", + "type": "address", + "internalType": "contract IDAVerifier" + } + ], + "outputs": [ + { + "name": "isRegistered", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "disableWhitelist", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "enableWhitelist", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "getAssertionCount", + "inputs": [ + { + "name": "contractAddress", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "assertionCount", + "type": "uint16", + "internalType": "uint16" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getAssertionWindow", + "inputs": [ + { + "name": "contractAddress", + "type": "address", + "internalType": "address" + }, + { + "name": "assertionId", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [ + { + "name": "activationBlock", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "deactivationBlock", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getManager", + "inputs": [ + { + "name": "contractAddress", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "manager", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getPendingManager", + "inputs": [ + { + "name": "contractAddress", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "pendingManager", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getRoleAdmin", + "inputs": [ + { + "name": "role", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "grantGovernanceRole", + "inputs": [ + { + "name": "governance", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "grantGuardianAdminRole", + "inputs": [ + { + "name": "guardianAdmin", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "grantGuardianRole", + "inputs": [ + { + "name": "guardian", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "grantOperatorAdminRole", + "inputs": [ + { + "name": "operatorAdmin", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "grantOperatorRole", + "inputs": [ + { + "name": "operator", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "grantRole", + "inputs": [ + { + "name": "role", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "account", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "hasAssertion", + "inputs": [ + { + "name": "contractAddress", + "type": "address", + "internalType": "address" + }, + { + "name": "assertionId", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [ + { + "name": "isAssociated", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "hasRole", + "inputs": [ + { + "name": "role", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "account", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "initialize", + "inputs": [ + { + "name": "admin", + "type": "address", + "internalType": "address" + }, + { + "name": "_adminVerifiers", + "type": "address[]", + "internalType": "contract IAdminVerifier[]" + }, + { + "name": "_daVerifiers", + "type": "address[]", + "internalType": "contract IDAVerifier[]" + }, + { + "name": "_maxAssertionsPerAA", + "type": "uint16", + "internalType": "uint16" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "initializeWithWhitelist", + "inputs": [ + { + "name": "admin", + "type": "address", + "internalType": "address" + }, + { + "name": "_adminVerifiers", + "type": "address[]", + "internalType": "contract IAdminVerifier[]" + }, + { + "name": "_daVerifiers", + "type": "address[]", + "internalType": "contract IDAVerifier[]" + }, + { + "name": "_maxAssertionsPerAA", + "type": "uint16", + "internalType": "uint16" + }, + { + "name": "_whitelistEnabled", + "type": "bool", + "internalType": "bool" + }, + { + "name": "_initialWhitelist", + "type": "address[]", + "internalType": "address[]" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "isAdminVerifierRegistered", + "inputs": [ + { + "name": "adminVerifier", + "type": "address", + "internalType": "contract IAdminVerifier" + } + ], + "outputs": [ + { + "name": "isRegistered", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "isDAVerifierRegistered", + "inputs": [ + { + "name": "daVerifier", + "type": "address", + "internalType": "contract IDAVerifier" + } + ], + "outputs": [ + { + "name": "isRegistered", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "isWhitelisted", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "maxAssertionsPerAA", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint16", + "internalType": "uint16" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "owner", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "pendingOwner", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "registerAssertionAdopter", + "inputs": [ + { + "name": "contractAddress", + "type": "address", + "internalType": "address" + }, + { + "name": "adminVerifier", + "type": "address", + "internalType": "contract IAdminVerifier" + }, + { + "name": "data", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "removeAdminVerifier", + "inputs": [ + { + "name": "adminVerifier", + "type": "address", + "internalType": "contract IAdminVerifier" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "removeAssertion", + "inputs": [ + { + "name": "contractAddress", + "type": "address", + "internalType": "address" + }, + { + "name": "assertionId", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "removeAssertionByGuardian", + "inputs": [ + { + "name": "contractAddress", + "type": "address", + "internalType": "address" + }, + { + "name": "assertionId", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "removeDAVerifier", + "inputs": [ + { + "name": "daVerifier", + "type": "address", + "internalType": "contract IDAVerifier" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "removeFromWhitelist", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "renounceOwnership", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "renounceRole", + "inputs": [ + { + "name": "role", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "callerConfirmation", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "resetStorage", + "inputs": [ + { + "name": "adopter", + "type": "address", + "internalType": "address" + }, + { + "name": "storageKey", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "revokeGovernanceRole", + "inputs": [ + { + "name": "governance", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "revokeGuardianAdminRole", + "inputs": [ + { + "name": "guardianAdmin", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "revokeGuardianRole", + "inputs": [ + { + "name": "guardian", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "revokeManager", + "inputs": [ + { + "name": "contractAddress", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "revokeOperatorAdminRole", + "inputs": [ + { + "name": "operatorAdmin", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "revokeOperatorRole", + "inputs": [ + { + "name": "operator", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "revokeRole", + "inputs": [ + { + "name": "role", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "account", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setMaxAssertionsPerAA", + "inputs": [ + { + "name": "_maxAssertionsPerAA", + "type": "uint16", + "internalType": "uint16" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "supportsInterface", + "inputs": [ + { + "name": "interfaceId", + "type": "bytes4", + "internalType": "bytes4" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "transferManager", + "inputs": [ + { + "name": "contractAddress", + "type": "address", + "internalType": "address" + }, + { + "name": "newManager", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "transferOwnership", + "inputs": [ + { + "name": "newOwner", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "whitelist", + "inputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "whitelistEnabled", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "event", + "name": "AddedToWhitelist", + "inputs": [ + { + "name": "account", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "AdminVerifierAdded", + "inputs": [ + { + "name": "adminVerifier", + "type": "address", + "indexed": false, + "internalType": "contract IAdminVerifier" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "AdminVerifierRemoved", + "inputs": [ + { + "name": "adminVerifier", + "type": "address", + "indexed": false, + "internalType": "contract IAdminVerifier" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "AssertionAdded", + "inputs": [ + { + "name": "assertionAdopter", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "assertionId", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "activationBlock", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "daVerifier", + "type": "address", + "indexed": true, + "internalType": "contract IDAVerifier" + }, + { + "name": "metadata", + "type": "bytes", + "indexed": false, + "internalType": "bytes" + }, + { + "name": "proof", + "type": "bytes", + "indexed": false, + "internalType": "bytes" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "AssertionAdopterAdded", + "inputs": [ + { + "name": "contractAddress", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "manager", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "adminVerifier", + "type": "address", + "indexed": false, + "internalType": "contract IAdminVerifier" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "AssertionRemoved", + "inputs": [ + { + "name": "assertionAdopter", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "assertionId", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "deactivationBlock", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "DAVerifierAdded", + "inputs": [ + { + "name": "daVerifier", + "type": "address", + "indexed": false, + "internalType": "contract IDAVerifier" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "DAVerifierRemoved", + "inputs": [ + { + "name": "daVerifier", + "type": "address", + "indexed": false, + "internalType": "contract IDAVerifier" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Initialized", + "inputs": [ + { + "name": "version", + "type": "uint64", + "indexed": false, + "internalType": "uint64" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "ManagerTransferRequested", + "inputs": [ + { + "name": "contractAddress", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "manager", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newManager", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "ManagerTransferred", + "inputs": [ + { + "name": "contractAddress", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newManager", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "OwnershipTransferStarted", + "inputs": [ + { + "name": "previousOwner", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newOwner", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "OwnershipTransferred", + "inputs": [ + { + "name": "previousOwner", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newOwner", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "RemovedFromWhitelist", + "inputs": [ + { + "name": "account", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "RoleAdminChanged", + "inputs": [ + { + "name": "role", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "previousAdminRole", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "newAdminRole", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "RoleGranted", + "inputs": [ + { + "name": "role", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "account", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "sender", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "RoleRevoked", + "inputs": [ + { + "name": "role", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "account", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "sender", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "StorageReset", + "inputs": [ + { + "name": "adopter", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "storageKey", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "resetBlock", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "WhitelistDisabled", + "inputs": [], + "anonymous": false + }, + { + "type": "event", + "name": "WhitelistEnabled", + "inputs": [], + "anonymous": false + }, + { + "type": "error", + "name": "AccessControlBadConfirmation", + "inputs": [] + }, + { + "type": "error", + "name": "AccessControlUnauthorizedAccount", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + }, + { + "name": "neededRole", + "type": "bytes32", + "internalType": "bytes32" + } + ] + }, + { + "type": "error", + "name": "AccountNotWhitelisted", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "AdminVerifierAlreadyRegistered", + "inputs": [] + }, + { + "type": "error", + "name": "AdminVerifierNotRegistered", + "inputs": [] + }, + { + "type": "error", + "name": "AlreadyWhitelisted", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "AssertionAdopterAlreadyRegistered", + "inputs": [] + }, + { + "type": "error", + "name": "AssertionAdopterNotRegistered", + "inputs": [] + }, + { + "type": "error", + "name": "AssertionAlreadyExists", + "inputs": [] + }, + { + "type": "error", + "name": "AssertionAlreadyRemoved", + "inputs": [] + }, + { + "type": "error", + "name": "AssertionDoesNotExist", + "inputs": [] + }, + { + "type": "error", + "name": "BatchError", + "inputs": [ + { + "name": "result", + "type": "bytes", + "internalType": "bytes" + } + ] + }, + { + "type": "error", + "name": "CannotGrantDefaultAdminRole", + "inputs": [] + }, + { + "type": "error", + "name": "CannotRenounceOwnerDefaultAdminRole", + "inputs": [] + }, + { + "type": "error", + "name": "CannotRevokeOwnerDefaultAdminRole", + "inputs": [] + }, + { + "type": "error", + "name": "DAVerifierAlreadyRegistered", + "inputs": [] + }, + { + "type": "error", + "name": "DAVerifierNotRegistered", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidAssertionTimelock", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidDAProof", + "inputs": [ + { + "name": "daVerifier", + "type": "address", + "internalType": "contract IDAVerifier" + } + ] + }, + { + "type": "error", + "name": "InvalidInitialization", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidManagerTransferRequest", + "inputs": [] + }, + { + "type": "error", + "name": "NoPendingManager", + "inputs": [] + }, + { + "type": "error", + "name": "NotInitializing", + "inputs": [] + }, + { + "type": "error", + "name": "NotWhitelisted", + "inputs": [] + }, + { + "type": "error", + "name": "OwnableInvalidOwner", + "inputs": [ + { + "name": "owner", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "OwnableUnauthorizedAccount", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "TooManyAssertions", + "inputs": [] + }, + { + "type": "error", + "name": "UnauthorizedManager", + "inputs": [] + }, + { + "type": "error", + "name": "UnauthorizedRegistrant", + "inputs": [] + }, + { + "type": "error", + "name": "WhitelistAlreadyDisabled", + "inputs": [] + }, + { + "type": "error", + "name": "WhitelistAlreadyEnabled", + "inputs": [] + } +] diff --git a/bindings/rust/src/lib.rs b/bindings/rust/src/lib.rs index 3e7e3ef..b03a72d 100644 --- a/bindings/rust/src/lib.rs +++ b/bindings/rust/src/lib.rs @@ -6,20 +6,25 @@ /// `StateOracle` bindings grouped by interface generation. pub mod state_oracle { - /// First published `StateOracle` interface generation. + /// `StateOracle` interface used by contract release 0.2.0. pub mod v1 { alloy_sol_types::sol!(IStateOracleV1, "abi/IStateOracleV1.json"); } + + /// `StateOracle` interface introduced by contract release 0.3.0. + pub mod v2 { + alloy_sol_types::sol!(IStateOracleV2, "abi/IStateOracleV2.json"); + } } #[cfg(test)] mod tests { use alloy_sol_types::{SolCall, SolError, SolEvent, TopicList}; - use super::state_oracle::v1::IStateOracleV1; + use super::state_oracle::{v1::IStateOracleV1, v2::IStateOracleV2}; #[test] - fn state_oracle_boundary_matches_representative_canonical_entries() { + fn state_oracle_v1_matches_release_0_2_0() { assert_eq!( IStateOracleV1::registerAssertionAdopterCall::SIGNATURE, "registerAssertionAdopter(address,address,bytes)" @@ -28,16 +33,41 @@ mod tests { IStateOracleV1::getAssertionWindowCall::SIGNATURE, "getAssertionWindow(address,bytes32)" ); + assert_eq!( + IStateOracleV1::addAssertionCall::SIGNATURE, + "addAssertion(address,bytes32,bytes,bytes)" + ); assert_eq!( IStateOracleV1::AssertionAdded::SIGNATURE, + "AssertionAdded(address,bytes32,uint256)" + ); + assert_eq!(IStateOracleV1::InvalidProof::SIGNATURE, "InvalidProof()"); + assert_eq!( + <::TopicList as TopicList>::COUNT, + 1 + ); + } + + #[test] + fn state_oracle_v2_matches_release_0_3_0() { + assert_eq!( + IStateOracleV2::registerAssertionAdopterCall::SIGNATURE, + "registerAssertionAdopter(address,address,bytes)" + ); + assert_eq!( + IStateOracleV2::addAssertionCall::SIGNATURE, + "addAssertion(address,bytes32,address,bytes,bytes)" + ); + assert_eq!( + IStateOracleV2::AssertionAdded::SIGNATURE, "AssertionAdded(address,bytes32,uint256,address,bytes,bytes)" ); assert_eq!( - IStateOracleV1::InvalidDAProof::SIGNATURE, + IStateOracleV2::InvalidDAProof::SIGNATURE, "InvalidDAProof(address)" ); assert_eq!( - <::TopicList as TopicList>::COUNT, + <::TopicList as TopicList>::COUNT, 4 ); } diff --git a/foundry.toml b/foundry.toml index c1784c0..491f531 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,6 +2,7 @@ src = "src" out = "out" libs = ["lib"] +solc = "0.8.36" # Without this, `forge inspect ... storage-layout` adds storageLayout to the # output selection and invalidates the build cache, so the next `forge build` # recompiles every file (3.26s vs 0.11s). diff --git a/shell/create_artifacts.sh b/shell/create_artifacts.sh index 4617a3e..42f5b26 100755 --- a/shell/create_artifacts.sh +++ b/shell/create_artifacts.sh @@ -41,12 +41,13 @@ cd "$ROOT_DIR" # Generate the artifacts with Forge forge build -# The versioned interface is the canonical consumer boundary used by the Rust -# crate. Refuse to publish if it drifts from the implementation surface. +# The latest versioned interface is the canonical consumer boundary used by the +# Rust crate. Historical interfaces intentionally differ from the current +# implementation and remain immutable. Refuse to publish if V2 drifts. if ! diff -u \ <(normalize_public_abi "$ROOT_DIR/out/StateOracle.sol/StateOracle.json") \ - <(normalize_public_abi "$ROOT_DIR/out/IStateOracleV1.sol/IStateOracleV1.json"); then - echo "StateOracle implementation ABI differs from IStateOracleV1" >&2 + <(normalize_public_abi "$ROOT_DIR/out/IStateOracleV2.sol/IStateOracleV2.json"); then + echo "StateOracle implementation ABI differs from IStateOracleV2" >&2 exit 1 fi @@ -70,6 +71,7 @@ extract_abi "$ROOT_DIR/out/IBatch.sol/IBatch.json" "${INTERFACES}" extract_abi "$ROOT_DIR/out/IDAVerifier.sol/IDAVerifier.json" "${INTERFACES}" extract_abi "$ROOT_DIR/out/IAdminVerifier.sol/IAdminVerifier.json" "${INTERFACES}" extract_abi "$ROOT_DIR/out/IStateOracleV1.sol/IStateOracleV1.json" "${INTERFACES}" +extract_abi "$ROOT_DIR/out/IStateOracleV2.sol/IStateOracleV2.json" "${INTERFACES}" # Extract ABIs for libraries LIBRARIES="${ARTIFACTS}/libraries" @@ -82,3 +84,5 @@ RUST_BINDINGS_ABI="${ROOT_DIR}/bindings/rust/abi" mkdir -p "${RUST_BINDINGS_ABI}" cp "${INTERFACES}/IStateOracleV1.json" "${RUST_BINDINGS_ABI}/IStateOracleV1.json" echo "Synced IStateOracleV1 ABI to ${RUST_BINDINGS_ABI}/IStateOracleV1.json" +cp "${INTERFACES}/IStateOracleV2.json" "${RUST_BINDINGS_ABI}/IStateOracleV2.json" +echo "Synced IStateOracleV2 ABI to ${RUST_BINDINGS_ABI}/IStateOracleV2.json" diff --git a/src/interfaces/IStateOracleV1.sol b/src/interfaces/IStateOracleV1.sol index 34382af..efa580e 100644 --- a/src/interfaces/IStateOracleV1.sol +++ b/src/interfaces/IStateOracleV1.sol @@ -5,9 +5,9 @@ import {IAdminVerifier} from "./IAdminVerifier.sol"; import {IDAVerifier} from "./IDAVerifier.sol"; /// @title IStateOracleV1 -/// @notice Canonical consumer interface for the first StateOracle ABI generation. -/// @dev This interface is the source for published bindings. Breaking changes require a new -/// interface generation; release tags are mapped to interface generations in the bindings README. +/// @notice Historical StateOracle consumer interface released as 0.2.0. +/// @dev This interface is immutable. It remains available for consumers of 0.2.0 deployments; +/// breaking changes introduced in 0.3.0 are represented by IStateOracleV2. interface IStateOracleV1 { error AccessControlBadConfirmation(); error AccessControlUnauthorizedAccount(address account, bytes32 neededRole); @@ -24,12 +24,10 @@ interface IStateOracleV1 { error CannotGrantDefaultAdminRole(); error CannotRenounceOwnerDefaultAdminRole(); error CannotRevokeOwnerDefaultAdminRole(); - error DAVerifierAlreadyRegistered(); - error DAVerifierNotRegistered(); error InvalidAssertionTimelock(); - error InvalidDAProof(IDAVerifier daVerifier); error InvalidInitialization(); error InvalidManagerTransferRequest(); + error InvalidProof(); error NoPendingManager(); error NotInitializing(); error NotWhitelisted(); @@ -44,18 +42,9 @@ interface IStateOracleV1 { event AddedToWhitelist(address indexed account); event AdminVerifierAdded(IAdminVerifier adminVerifier); event AdminVerifierRemoved(IAdminVerifier adminVerifier); - event AssertionAdded( - address indexed assertionAdopter, - bytes32 indexed assertionId, - uint256 activationBlock, - IDAVerifier indexed daVerifier, - bytes metadata, - bytes proof - ); + event AssertionAdded(address assertionAdopter, bytes32 assertionId, uint256 activationBlock); event AssertionAdopterAdded(address indexed contractAddress, address indexed manager, IAdminVerifier adminVerifier); - event AssertionRemoved(address indexed assertionAdopter, bytes32 indexed assertionId, uint256 deactivationBlock); - event DAVerifierAdded(IDAVerifier daVerifier); - event DAVerifierRemoved(IDAVerifier daVerifier); + event AssertionRemoved(address assertionAdopter, bytes32 assertionId, uint256 deactivationBlock); event Initialized(uint64 version); event ManagerTransferRequested( address indexed contractAddress, address indexed manager, address indexed newManager @@ -67,11 +56,11 @@ interface IStateOracleV1 { event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole); event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender); event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender); - event StorageReset(address indexed adopter, bytes32 indexed storageKey, uint256 resetBlock); event WhitelistDisabled(); event WhitelistEnabled(); - function ASSERTION_TIMELOCK_BLOCKS() external view returns (uint256); + function ASSERTION_TIMELOCK_BLOCKS() external view returns (uint128); + function DA_VERIFIER() external view returns (IDAVerifier); function DEFAULT_ADMIN_ROLE() external view returns (bytes32); function GOVERNANCE_ROLE() external view returns (bytes32); function GUARDIAN_ADMIN_ROLE() external view returns (bytes32); @@ -81,14 +70,8 @@ interface IStateOracleV1 { function acceptManagerTransfer(address contractAddress) external; function acceptOwnership() external; function addAdminVerifier(IAdminVerifier adminVerifier) external; - function addAssertion( - address contractAddress, - bytes32 assertionId, - IDAVerifier daVerifier, - bytes calldata metadata, - bytes calldata proof - ) external; - function addDAVerifier(IDAVerifier daVerifier) external; + function addAssertion(address contractAddress, bytes32 assertionId, bytes calldata metadata, bytes calldata proof) + external; function addToWhitelist(address account) external; function adminVerifiers(IAdminVerifier adminVerifier) external view returns (bool isRegistered); function assertionAdopters(address) @@ -96,14 +79,13 @@ interface IStateOracleV1 { view returns (address manager, address pendingManager, uint16 assertionCount); function batch(bytes[] calldata calls) external; - function daVerifiers(IDAVerifier daVerifier) external view returns (bool isRegistered); function disableWhitelist() external; function enableWhitelist() external; function getAssertionCount(address contractAddress) external view returns (uint16 assertionCount); function getAssertionWindow(address contractAddress, bytes32 assertionId) external view - returns (uint256 activationBlock, uint256 deactivationBlock); + returns (uint128 activationBlock, uint128 deactivationBlock); function getManager(address contractAddress) external view returns (address manager); function getPendingManager(address contractAddress) external view returns (address pendingManager); function getRoleAdmin(bytes32 role) external view returns (bytes32); @@ -115,22 +97,8 @@ interface IStateOracleV1 { function grantRole(bytes32 role, address account) external; function hasAssertion(address contractAddress, bytes32 assertionId) external view returns (bool isAssociated); function hasRole(bytes32 role, address account) external view returns (bool); - function initialize( - address admin, - IAdminVerifier[] calldata _adminVerifiers, - IDAVerifier[] calldata _daVerifiers, - uint16 _maxAssertionsPerAA - ) external; - function initializeWithWhitelist( - address admin, - IAdminVerifier[] calldata _adminVerifiers, - IDAVerifier[] calldata _daVerifiers, - uint16 _maxAssertionsPerAA, - bool _whitelistEnabled, - address[] calldata _initialWhitelist - ) external; + function initialize(address admin, IAdminVerifier[] calldata _adminVerifiers, uint16 _maxAssertionsPerAA) external; function isAdminVerifierRegistered(IAdminVerifier adminVerifier) external view returns (bool isRegistered); - function isDAVerifierRegistered(IDAVerifier daVerifier) external view returns (bool isRegistered); function isWhitelisted(address account) external view returns (bool); function maxAssertionsPerAA() external view returns (uint16); function owner() external view returns (address); @@ -140,11 +108,9 @@ interface IStateOracleV1 { function removeAdminVerifier(IAdminVerifier adminVerifier) external; function removeAssertion(address contractAddress, bytes32 assertionId) external; function removeAssertionByGuardian(address contractAddress, bytes32 assertionId) external; - function removeDAVerifier(IDAVerifier daVerifier) external; function removeFromWhitelist(address account) external; function renounceOwnership() external; function renounceRole(bytes32 role, address callerConfirmation) external; - function resetStorage(address adopter, bytes32 storageKey) external; function revokeGovernanceRole(address governance) external; function revokeGuardianAdminRole(address guardianAdmin) external; function revokeGuardianRole(address guardian) external; diff --git a/src/interfaces/IStateOracleV2.sol b/src/interfaces/IStateOracleV2.sol new file mode 100644 index 0000000..bda24ad --- /dev/null +++ b/src/interfaces/IStateOracleV2.sol @@ -0,0 +1,161 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.28; + +import {IAdminVerifier} from "./IAdminVerifier.sol"; +import {IDAVerifier} from "./IDAVerifier.sol"; + +/// @title IStateOracleV2 +/// @notice Canonical consumer interface for the StateOracle ABI introduced in release 0.3.0. +/// @dev This interface is the source for published bindings. Breaking changes require a new +/// interface generation; release tags are mapped to interface generations in the bindings README. +interface IStateOracleV2 { + error AccessControlBadConfirmation(); + error AccessControlUnauthorizedAccount(address account, bytes32 neededRole); + error AccountNotWhitelisted(address account); + error AdminVerifierAlreadyRegistered(); + error AdminVerifierNotRegistered(); + error AlreadyWhitelisted(address account); + error AssertionAdopterAlreadyRegistered(); + error AssertionAdopterNotRegistered(); + error AssertionAlreadyExists(); + error AssertionAlreadyRemoved(); + error AssertionDoesNotExist(); + error BatchError(bytes result); + error CannotGrantDefaultAdminRole(); + error CannotRenounceOwnerDefaultAdminRole(); + error CannotRevokeOwnerDefaultAdminRole(); + error DAVerifierAlreadyRegistered(); + error DAVerifierNotRegistered(); + error InvalidAssertionTimelock(); + error InvalidDAProof(IDAVerifier daVerifier); + error InvalidInitialization(); + error InvalidManagerTransferRequest(); + error NoPendingManager(); + error NotInitializing(); + error NotWhitelisted(); + error OwnableInvalidOwner(address owner); + error OwnableUnauthorizedAccount(address account); + error TooManyAssertions(); + error UnauthorizedManager(); + error UnauthorizedRegistrant(); + error WhitelistAlreadyDisabled(); + error WhitelistAlreadyEnabled(); + + event AddedToWhitelist(address indexed account); + event AdminVerifierAdded(IAdminVerifier adminVerifier); + event AdminVerifierRemoved(IAdminVerifier adminVerifier); + event AssertionAdded( + address indexed assertionAdopter, + bytes32 indexed assertionId, + uint256 activationBlock, + IDAVerifier indexed daVerifier, + bytes metadata, + bytes proof + ); + event AssertionAdopterAdded(address indexed contractAddress, address indexed manager, IAdminVerifier adminVerifier); + event AssertionRemoved(address indexed assertionAdopter, bytes32 indexed assertionId, uint256 deactivationBlock); + event DAVerifierAdded(IDAVerifier daVerifier); + event DAVerifierRemoved(IDAVerifier daVerifier); + event Initialized(uint64 version); + event ManagerTransferRequested( + address indexed contractAddress, address indexed manager, address indexed newManager + ); + event ManagerTransferred(address indexed contractAddress, address indexed newManager); + event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner); + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + event RemovedFromWhitelist(address indexed account); + event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole); + event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender); + event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender); + event StorageReset(address indexed adopter, bytes32 indexed storageKey, uint256 resetBlock); + event WhitelistDisabled(); + event WhitelistEnabled(); + + function ASSERTION_TIMELOCK_BLOCKS() external view returns (uint256); + function DEFAULT_ADMIN_ROLE() external view returns (bytes32); + function GOVERNANCE_ROLE() external view returns (bytes32); + function GUARDIAN_ADMIN_ROLE() external view returns (bytes32); + function GUARDIAN_ROLE() external view returns (bytes32); + function OPERATOR_ADMIN_ROLE() external view returns (bytes32); + function OPERATOR_ROLE() external view returns (bytes32); + function acceptManagerTransfer(address contractAddress) external; + function acceptOwnership() external; + function addAdminVerifier(IAdminVerifier adminVerifier) external; + function addAssertion( + address contractAddress, + bytes32 assertionId, + IDAVerifier daVerifier, + bytes calldata metadata, + bytes calldata proof + ) external; + function addDAVerifier(IDAVerifier daVerifier) external; + function addToWhitelist(address account) external; + function adminVerifiers(IAdminVerifier adminVerifier) external view returns (bool isRegistered); + function assertionAdopters(address) + external + view + returns (address manager, address pendingManager, uint16 assertionCount); + function batch(bytes[] calldata calls) external; + function daVerifiers(IDAVerifier daVerifier) external view returns (bool isRegistered); + function disableWhitelist() external; + function enableWhitelist() external; + function getAssertionCount(address contractAddress) external view returns (uint16 assertionCount); + function getAssertionWindow(address contractAddress, bytes32 assertionId) + external + view + returns (uint256 activationBlock, uint256 deactivationBlock); + function getManager(address contractAddress) external view returns (address manager); + function getPendingManager(address contractAddress) external view returns (address pendingManager); + function getRoleAdmin(bytes32 role) external view returns (bytes32); + function grantGovernanceRole(address governance) external; + function grantGuardianAdminRole(address guardianAdmin) external; + function grantGuardianRole(address guardian) external; + function grantOperatorAdminRole(address operatorAdmin) external; + function grantOperatorRole(address operator) external; + function grantRole(bytes32 role, address account) external; + function hasAssertion(address contractAddress, bytes32 assertionId) external view returns (bool isAssociated); + function hasRole(bytes32 role, address account) external view returns (bool); + function initialize( + address admin, + IAdminVerifier[] calldata _adminVerifiers, + IDAVerifier[] calldata _daVerifiers, + uint16 _maxAssertionsPerAA + ) external; + function initializeWithWhitelist( + address admin, + IAdminVerifier[] calldata _adminVerifiers, + IDAVerifier[] calldata _daVerifiers, + uint16 _maxAssertionsPerAA, + bool _whitelistEnabled, + address[] calldata _initialWhitelist + ) external; + function isAdminVerifierRegistered(IAdminVerifier adminVerifier) external view returns (bool isRegistered); + function isDAVerifierRegistered(IDAVerifier daVerifier) external view returns (bool isRegistered); + function isWhitelisted(address account) external view returns (bool); + function maxAssertionsPerAA() external view returns (uint16); + function owner() external view returns (address); + function pendingOwner() external view returns (address); + function registerAssertionAdopter(address contractAddress, IAdminVerifier adminVerifier, bytes calldata data) + external; + function removeAdminVerifier(IAdminVerifier adminVerifier) external; + function removeAssertion(address contractAddress, bytes32 assertionId) external; + function removeAssertionByGuardian(address contractAddress, bytes32 assertionId) external; + function removeDAVerifier(IDAVerifier daVerifier) external; + function removeFromWhitelist(address account) external; + function renounceOwnership() external; + function renounceRole(bytes32 role, address callerConfirmation) external; + function resetStorage(address adopter, bytes32 storageKey) external; + function revokeGovernanceRole(address governance) external; + function revokeGuardianAdminRole(address guardianAdmin) external; + function revokeGuardianRole(address guardian) external; + function revokeManager(address contractAddress) external; + function revokeOperatorAdminRole(address operatorAdmin) external; + function revokeOperatorRole(address operator) external; + function revokeRole(bytes32 role, address account) external; + function setMaxAssertionsPerAA(uint16 _maxAssertionsPerAA) external; + function supportsInterface(bytes4 interfaceId) external view returns (bool); + function transferManager(address contractAddress, address newManager) external; + function transferOwnership(address newOwner) external; + function whitelist(address) external view returns (bool); + function whitelistEnabled() external view returns (bool); +} diff --git a/test/release/test_release_workflow.py b/test/release/test_release_workflow.py index 0970570..b6bb7a2 100644 --- a/test/release/test_release_workflow.py +++ b/test/release/test_release_workflow.py @@ -17,7 +17,7 @@ def test_artifacts_are_verified_before_upload(self): generation_index = artifact_job.index("run: ./shell/create_artifacts.sh") verification_index = artifact_job.index( - "run: git diff --exit-code -- bindings/rust/abi/IStateOracleV1.json" + "run: git diff --exit-code -- bindings/rust/abi/IStateOracleV1.json bindings/rust/abi/IStateOracleV2.json" ) upload_index = artifact_job.index("uses: actions/upload-artifact@") @@ -119,7 +119,8 @@ def test_cargo_verification_has_no_oidc_permission(self): self.assertIn("uses: actions/checkout@", verify_job) self.assertIn("uses: actions/download-artifact@", verify_job) self.assertIn("name: credible-layer-contracts-artifacts", verify_job) - self.assertIn("cmp -s artifacts/interfaces/IStateOracleV1.json", verify_job) + self.assertIn("for interface in IStateOracleV1 IStateOracleV2", verify_job) + self.assertIn('cmp -s "artifacts/interfaces/${interface}.json"', verify_job) self.assertIn( "cargo publish --manifest-path bindings/rust/Cargo.toml --dry-run", verify_job, @@ -257,6 +258,47 @@ def test_artifact_generation_updates_the_committed_rust_abi(self): self.assertIn('bindings/rust/abi', script) self.assertIn('cp "${INTERFACES}/IStateOracleV1.json"', script) + self.assertIn('cp "${INTERFACES}/IStateOracleV2.json"', script) + + def test_state_oracle_release_mapping_preserves_supported_abis(self): + readme = (ROOT / "bindings" / "rust" / "README.md").read_text() + rust_source = (ROOT / "bindings" / "rust" / "src" / "lib.rs").read_text() + + self.assertIn( + "| `0.2.0` | `IStateOracleV1` | `state_oracle::v1` |", readme + ) + self.assertIn( + "| `0.3.0` | `IStateOracleV2` | `state_oracle::v2` |", readme + ) + self.assertIn("pub mod v1", rust_source) + self.assertIn("pub mod v2", rust_source) + + def test_cargo_package_includes_every_supported_state_oracle_abi(self): + with (ROOT / "bindings" / "rust" / "Cargo.toml").open("rb") as manifest: + included_files = tomllib.load(manifest)["package"]["include"] + + self.assertIn("abi/IStateOracleV1.json", included_files) + self.assertIn("abi/IStateOracleV2.json", included_files) + + def test_gas_snapshot_uses_a_pinned_foundry_version(self): + workflow = (ROOT / ".github" / "workflows" / "solidity-test.yml").read_text() + solidity_job = workflow.split(" solidity-base:\n", 1)[1].split( + " gas-snapshot:\n", 1 + )[0] + gas_job = workflow.split(" gas-snapshot:\n", 1)[1].split( + " contract-compatibility:\n", 1 + )[0] + + self.assertIn("disable-gas-snapshot: true", solidity_job) + self.assertRegex(gas_job, r"version: v\d+\.\d+\.\d+") + self.assertNotIn("version: nightly", gas_job) + self.assertNotIn("version: stable", gas_job) + self.assertIn("forge snapshot --check --silent --tolerance 25", gas_job) + + def test_solidity_compiler_version_is_pinned(self): + foundry_config = (ROOT / "foundry.toml").read_text() + + self.assertRegex(foundry_config, r'(?m)^solc = "\d+\.\d+\.\d+"$') def test_package_requests_provenance(self): package = json.loads((ROOT / "package.json").read_text())