Skip to content

Commit 96f3154

Browse files
feat(host-contracts): add rust bindings (#1782)
* chore(host-contracts): add bindings script * chore(host-contracts): add bindings * chore(gateway-contracts): move bindings script * chore(host-contracts): add integrity check workflow * chore(common): use contracts_bindings_update name
1 parent 4a33646 commit 96f3154

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+100455
-36
lines changed
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# This workflow verifies that:
2+
# - The Rust bindings crate version and files are up-to-date
3+
# - Contract mocks and selectors are current
4+
# - Dependency licenses compliance
5+
name: host-contracts-integrity-checks
6+
7+
on:
8+
pull_request:
9+
push:
10+
branches:
11+
- main
12+
13+
permissions: {}
14+
15+
concurrency:
16+
group: host-contract-integrity-checks-${{ github.ref }}
17+
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
18+
19+
jobs:
20+
check-changes:
21+
name: host-contracts-integrity-checks/check-changes
22+
permissions:
23+
contents: 'read' # Required to checkout repository code
24+
runs-on: ubuntu-latest
25+
outputs:
26+
changes-host-contracts: ${{ steps.filter.outputs.host-contracts }}
27+
steps:
28+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
29+
with:
30+
persist-credentials: 'false'
31+
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
32+
id: filter
33+
with:
34+
filters: |
35+
host-contracts:
36+
- .github/workflows/host-contracts-integrity-checks.yml
37+
- host-contracts/**
38+
39+
contract-integrity-checks:
40+
name: host-contracts-integrity-checks/contract-integrity-checks (bpr)
41+
needs: check-changes
42+
if: ${{ needs.check-changes.outputs.changes-host-contracts == 'true' }}
43+
permissions:
44+
contents: 'read' # Required to checkout repository code
45+
runs-on: ubuntu-latest
46+
steps:
47+
- name: Checkout project
48+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
49+
with:
50+
persist-credentials: 'false'
51+
52+
- name: Cache npm
53+
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
54+
with:
55+
path: ~/.npm
56+
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
57+
58+
- name: Install Foundry
59+
uses: foundry-rs/foundry-toolchain@de808b1eea699e761c404bda44ba8f21aba30b2c # v1.3.1
60+
with:
61+
version: v1.3.1
62+
63+
- name: Install dependencies
64+
working-directory: host-contracts
65+
run: npm ci && forge soldeer install
66+
67+
- name: Check bindings are up-to-date
68+
working-directory: host-contracts
69+
run: make check-bindings
70+
71+
- name: Check contract selectors are up-to-date
72+
working-directory: host-contracts
73+
run: make check-selectors

gateway-contracts/scripts/bindings_update.py renamed to ci/contracts_bindings_update.py

Lines changed: 76 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,47 @@
1111
from enum import Enum
1212
from pathlib import Path
1313

14-
GW_ROOT_DIR = Path(os.path.dirname(__file__)).parent
15-
GW_CRATE_DIR = GW_ROOT_DIR.joinpath("rust_bindings")
16-
GW_CONTRACTS_DIR = GW_ROOT_DIR.joinpath("contracts")
17-
GW_MOCKS_DIR = GW_CONTRACTS_DIR.joinpath("mocks")
14+
CI_DIR = Path(os.path.dirname(__file__))
15+
REPO_ROOT = CI_DIR.parent
1816

1917
# To update forge to the latest version locally, run `foundryup`
2018
MIN_FORGE_VERSION = (1, 3, 1)
2119
MAX_FORGE_VERSION = (2, 0, 0) # Exclusive upper bound
2220

2321

22+
class ProjectConfig:
23+
"""Configuration for a specific project's bindings."""
24+
25+
def __init__(self, name: str, root_dir: Path, skip_patterns: list[str] = None):
26+
self.name = name
27+
self.root_dir = root_dir
28+
self.crate_dir = root_dir.joinpath("rust_bindings")
29+
self.contracts_dir = root_dir.joinpath("contracts")
30+
self.skip_patterns = skip_patterns or []
31+
32+
def get_skip_args(self) -> str:
33+
"""Returns forge bind skip arguments for this project."""
34+
return " ".join(f"--skip '{pattern}'" for pattern in self.skip_patterns)
35+
36+
37+
# Project configurations
38+
PROJECTS = {
39+
"gateway": ProjectConfig(
40+
name="Gateway",
41+
root_dir=REPO_ROOT.joinpath("gateway-contracts"),
42+
skip_patterns=[
43+
"Example",
44+
"contracts/mocks/*",
45+
],
46+
),
47+
"host": ProjectConfig(
48+
name="Host",
49+
root_dir=REPO_ROOT.joinpath("host-contracts"),
50+
skip_patterns=["fhevm-foundry/*", "test/*"],
51+
),
52+
}
53+
54+
2455
def parse_semver(version_str: str) -> tuple:
2556
"""Parses a semver string (e.g., '1.3.1') into a tuple of integers."""
2657
return tuple(int(x) for x in version_str.split("."))
@@ -30,9 +61,17 @@ def init_cli() -> ArgumentParser:
3061
"""Inits the CLI of the tool."""
3162
parser = ArgumentParser(
3263
description=(
33-
"A tool to check or update the bindings crate of the Gateway contracts."
64+
"A tool to check or update the bindings crate of the Gateway or Host contracts."
3465
)
3566
)
67+
68+
parser.add_argument(
69+
"--project",
70+
choices=["gateway", "host"],
71+
required=True,
72+
help="The project to check or update bindings for.",
73+
)
74+
3675
subparsers = parser.add_subparsers(dest="command", help="Subcommands")
3776

3877
subparsers.add_parser(
@@ -53,7 +92,8 @@ def main():
5392
if args.command not in ["check", "update"]:
5493
return cli.print_help()
5594

56-
bindings_updater = BindingsUpdater()
95+
project_config = PROJECTS[args.project]
96+
bindings_updater = BindingsUpdater(project_config)
5797

5898
if args.command == "check":
5999
bindings_updater.check_version()
@@ -74,21 +114,23 @@ class ExitStatus(Enum):
74114

75115
class BindingsUpdater:
76116
"""
77-
An object used to check if the binding crate of the Gateway contracts is
117+
An object used to check if the binding crate of the contracts is
78118
up-to-date.
79119
80120
Also takes care of updating this crate if requested.
81121
"""
82122

83123
tempdir: str
84-
gateway_repo_version: str
124+
repo_version: str
125+
config: ProjectConfig
85126

86-
def __init__(self):
127+
def __init__(self, config: ProjectConfig):
128+
self.config = config
87129
self.tempdir = tempfile.mkdtemp()
88130
BindingsUpdater._check_forge_installed()
89-
with open(f"{GW_ROOT_DIR}/package.json", "r") as package_json_fd:
131+
with open(f"{config.root_dir}/package.json", "r") as package_json_fd:
90132
package_json_content = json.load(package_json_fd)
91-
self.gateway_repo_version = package_json_content["version"]
133+
self.repo_version = package_json_content["version"]
92134

93135
def __del__(self):
94136
shutil.rmtree(self.tempdir)
@@ -131,14 +173,15 @@ def _check_forge_installed():
131173
sys.exit(ExitStatus.WRONG_FORGE_VERSION.value)
132174

133175
def check_bindings_up_to_date(self):
134-
"""Checks that the Gateway contracts' bindings are up-to-date."""
135-
log_info("Checking that the Gateway contracts' bindings are up-to-date...")
176+
"""Checks that the contracts' bindings are up-to-date."""
177+
log_info(f"Checking that the {self.config.name} contracts' bindings are up-to-date...")
136178

179+
skip_args = self.config.get_skip_args()
137180
# We need to include the --no-metadata flag to avoid updating many of the contracts' bytecode
138181
# when only updating one of them (since interfaces are included in many contracts)
139182
return_code = subprocess.call(
140-
f"forge bind --root {GW_ROOT_DIR} --module --skip-cargo-toml "
141-
f"--hh -b {GW_CRATE_DIR}/src -o {self.tempdir} --skip Example --skip {GW_MOCKS_DIR}/* "
183+
f"forge bind --root {self.config.root_dir} --module --skip-cargo-toml "
184+
f"--hh -b {self.config.crate_dir}/src -o {self.tempdir} {skip_args} "
142185
f"--no-metadata",
143186
shell=True,
144187
stdout=subprocess.DEVNULL,
@@ -152,28 +195,29 @@ def check_bindings_up_to_date(self):
152195
log_success("All binding files are up-to-date!")
153196

154197
def update_bindings(self):
155-
"""Updates the Gateway contracts' bindings."""
156-
log_info("Updating Gateway contracts' bindings...")
198+
"""Updates the contracts' bindings."""
199+
log_info(f"Updating {self.config.name} contracts' bindings...")
157200

201+
skip_args = self.config.get_skip_args()
158202
# We need to include the --no-metadata flag to avoid updating many of the contracts' bytecode
159203
# when only updating one of them (since interfaces are included in many contracts)
160204
subprocess.run(
161-
f"forge bind --root {GW_ROOT_DIR} --hh -b {GW_CRATE_DIR}/src "
162-
f"--module --overwrite -o {self.tempdir} --skip Example --skip {GW_MOCKS_DIR}/* "
205+
f"forge bind --root {self.config.root_dir} --hh -b {self.config.crate_dir}/src "
206+
f"--module --overwrite -o {self.tempdir} {skip_args} "
163207
"--no-metadata",
164208
shell=True,
165209
check=True,
166210
stdout=subprocess.DEVNULL,
167211
)
168212

169-
log_success("The Gateway contracts' bindings are now up-to-date!")
213+
log_success(f"The {self.config.name} contracts' bindings are now up-to-date!")
170214

171215
def check_version(self):
172216
"""
173-
Checks that the version of the crate matches the version of the Gateway.
217+
Checks that the version of the crate matches the version of the project.
174218
"""
175-
log_info("Checking that the crate's version match the Gateway version...")
176-
with open(f"{GW_CRATE_DIR}/Cargo.toml", "r") as cargo_toml_fd:
219+
log_info(f"Checking that the crate's version match the {self.config.name} version...")
220+
with open(f"{self.config.crate_dir}/Cargo.toml", "r") as cargo_toml_fd:
177221
cargo_toml_content = cargo_toml_fd.read()
178222

179223
# Find the version in the Cargo.toml
@@ -193,23 +237,23 @@ def check_version(self):
193237
# Extract the version from the matches: the first (and only) captured group from the regex.
194238
cargo_toml_version = matches.group(1)
195239

196-
if self.gateway_repo_version != cargo_toml_version:
240+
if self.repo_version != cargo_toml_version:
197241
log_error(
198-
"ERROR: Cargo.toml version does not match Gateway version!\n"
199-
f"Gateway version: {self.gateway_repo_version}\n"
242+
f"ERROR: Cargo.toml version does not match {self.config.name} version!\n"
243+
f"{self.config.name} version: {self.repo_version}\n"
200244
f"Cargo.toml version: {cargo_toml_version}\n"
201245
)
202246
log_info("Run `make update-bindings` to update the crate's version.")
203247
sys.exit(ExitStatus.CRATE_VERSION_NOT_UP_TO_DATE.value)
204248
log_success(
205-
f"The version of the crate match with the Gateway version: {self.gateway_repo_version}!\n"
249+
f"The version of the crate match with the {self.config.name} version: {self.repo_version}!\n"
206250
)
207251

208252
def update_crate_version(self):
209-
"""Updates the crate's version to match with the Gateway version."""
253+
"""Updates the crate's version to match with the project version."""
210254
log_info("Updating the crate's version...")
211255

212-
with open(f"{GW_CRATE_DIR}/Cargo.toml", "r") as cargo_toml_fd:
256+
with open(f"{self.config.crate_dir}/Cargo.toml", "r") as cargo_toml_fd:
213257
cargo_toml_content = cargo_toml_fd.read()
214258

215259
# Replace the version in the Cargo.toml
@@ -223,18 +267,18 @@ def update_crate_version(self):
223267
# make sure we do not alter the original format of the Cargo.toml.
224268
cargo_toml_content = re.sub(
225269
r'(\[package\].*?version\s*=\s*")[^"]+(")',
226-
lambda m: m.group(1) + self.gateway_repo_version + m.group(2),
270+
lambda m: m.group(1) + self.repo_version + m.group(2),
227271
cargo_toml_content,
228272
count=1,
229273
flags=re.DOTALL,
230274
)
231275

232-
with open(f"{GW_CRATE_DIR}/Cargo.toml", "w") as cargo_toml_fd:
276+
with open(f"{self.config.crate_dir}/Cargo.toml", "w") as cargo_toml_fd:
233277
cargo_toml_fd.write(cargo_toml_content)
234278

235279
log_success(
236280
f"The crate's version has been successfully updated to "
237-
f"{self.gateway_repo_version}!\n"
281+
f"{self.repo_version}!\n"
238282
)
239283

240284

gateway-contracts/Makefile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,10 @@ docker-compose-down:
4848
docker compose -vvv down -v --remove-orphans
4949

5050
check-bindings: ensure-addresses
51-
python3 scripts/bindings_update.py check
51+
python3 ../ci/contracts_bindings_update.py --project gateway check
5252

5353
update-bindings: ensure-addresses
54-
python3 scripts/bindings_update.py update
54+
python3 ../ci/contracts_bindings_update.py --project gateway update
5555

5656
lint-bindings:
5757
cd rust_bindings && cargo clippy -- -D warnings && cd ..
@@ -97,7 +97,7 @@ deploy-mocked-zama-oft:
9797
DOTENV_CONFIG_PATH=$(ENV_PATH) npx hardhat task:deployMockedZamaOFT
9898

9999
# Deploy the contracts needed for deploying the gateway contracts
100-
deploy-setup-contracts:
100+
deploy-setup-contracts:
101101
DOTENV_CONFIG_PATH=$(ENV_PATH) npx hardhat task:deploySetupContracts --deploy-mocked-zama-oft true
102102

103103
# Ensure that the empty proxy addresses exists as these are required for contract compilation.

host-contracts/Makefile

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,17 @@ check-selectors:
4848
update-selectors:
4949
DAPP_OUT=$(FORGE_DAPP_OUT) forge selectors list | tail -n +2 > ./docs/contract_selectors.txt
5050

51+
check-bindings: ensure-addresses
52+
python3 ../ci/contracts_bindings_update.py --project host check
53+
54+
update-bindings: ensure-addresses
55+
python3 ../ci/contracts_bindings_update.py --project host update
56+
57+
lint-bindings:
58+
cd rust_bindings && cargo clippy -- -D warnings && cd ..
59+
5160
# Update auto-generated files for conformance checks
52-
update-conformance: update-selectors
61+
update-conformance: update-bindings update-selectors
5362

5463
# Conform to pre-commit checks
5564
conformance: prettier update-conformance
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[package]
2+
authors = ["Zama"]
3+
name = "fhevm_host_bindings"
4+
publish = true
5+
edition = "2024"
6+
license = "BSD-3-Clause-Clear"
7+
version = "0.10.0"
8+
9+
[lints.rust]
10+
unused = "allow"
11+
12+
[dependencies]
13+
alloy = { version = "1.0", default-features = false, features = [
14+
"contract",
15+
"sol-types",
16+
] }
17+
serde = { version = "1.0", default-features = false, features = ["derive"] }
18+
19+
[lib]
20+
path = "src/mod.rs"

0 commit comments

Comments
 (0)