From bccf0989b3b56e5bf0e3a51142756a6671f9dc24 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 15 Apr 2025 21:17:40 -0500 Subject: [PATCH 001/322] bump midnight-js --- package.json | 24 ++--- yarn.lock | 278 +++++++++++++++++++++++++++++---------------------- 2 files changed, 168 insertions(+), 134 deletions(-) diff --git a/package.json b/package.json index 24105ba7..bfe904a3 100644 --- a/package.json +++ b/package.json @@ -18,18 +18,18 @@ "dependencies": { "@midnight-ntwrk/compact-runtime": "^0.7.0", "@midnight-ntwrk/dapp-connector-api": "^1.2.2", - "@midnight-ntwrk/ledger": "^3.0.2", - "@midnight-ntwrk/midnight-js-contracts": "0.2.5", - "@midnight-ntwrk/midnight-js-fetch-zk-config-provider": "0.2.5", - "@midnight-ntwrk/midnight-js-http-client-proof-provider": "0.2.5", - "@midnight-ntwrk/midnight-js-indexer-public-data-provider": "0.2.5", - "@midnight-ntwrk/midnight-js-level-private-state-provider": "0.2.5", - "@midnight-ntwrk/midnight-js-network-id": "0.2.5", - "@midnight-ntwrk/midnight-js-node-zk-config-provider": "0.2.5", - "@midnight-ntwrk/midnight-js-types": "0.2.5", - "@midnight-ntwrk/midnight-js-utils": "0.2.5", - "@midnight-ntwrk/wallet": "^3.7.3", - "@midnight-ntwrk/wallet-api": "^3.5.0", + "@midnight-ntwrk/ledger": "^3.0.6", + "@midnight-ntwrk/midnight-js-contracts": "^1.0.0", + "@midnight-ntwrk/midnight-js-fetch-zk-config-provider": "^1.0.0", + "@midnight-ntwrk/midnight-js-http-client-proof-provider": "^1.0.0", + "@midnight-ntwrk/midnight-js-indexer-public-data-provider": "^1.0.0", + "@midnight-ntwrk/midnight-js-level-private-state-provider": "^1.0.0", + "@midnight-ntwrk/midnight-js-network-id": "^1.0.0", + "@midnight-ntwrk/midnight-js-node-zk-config-provider": "^1.0.0", + "@midnight-ntwrk/midnight-js-types": "^1.0.0", + "@midnight-ntwrk/midnight-js-utils": "^1.0.0", + "@midnight-ntwrk/wallet": "^4.0.0", + "@midnight-ntwrk/wallet-api": "^4.0.0", "fp-ts": "^2.16.1", "io-ts": "^2.2.20", "pino": "^8.16.0", diff --git a/yarn.lock b/yarn.lock index 64ac2456..7eb3114a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -888,54 +888,55 @@ __metadata: languageName: node linkType: hard -"@midnight-ntwrk/ledger@npm:^3.0.2": +"@midnight-ntwrk/ledger@npm:^3.0.6": version: 3.0.6 resolution: "@midnight-ntwrk/ledger@npm:3.0.6" checksum: 10/644fac9ea7b47c8d0c8ea437e18e2362746701e2fedba3f6fa9a08dcea1f283c05336d9def60403e72055baa5a1e841f971a858ae3133b577a3f12518a48c753 languageName: node linkType: hard -"@midnight-ntwrk/midnight-js-contracts@npm:0.2.5": - version: 0.2.5 - resolution: "@midnight-ntwrk/midnight-js-contracts@npm:0.2.5" +"@midnight-ntwrk/midnight-js-contracts@npm:^1.0.0": + version: 1.0.0 + resolution: "@midnight-ntwrk/midnight-js-contracts@npm:1.0.0" dependencies: - "@midnight-ntwrk/midnight-js-network-id": "npm:0.2.5" - "@midnight-ntwrk/midnight-js-types": "npm:0.2.5" - "@midnight-ntwrk/midnight-js-utils": "npm:0.2.5" - checksum: 10/ad584d7874f7539c6ce2fc8057ab86bf4da8021c935c0e6be569218d2abd64f7e44ef68e7c98da86a87f1d8389fb14676b30a9c15e4e1b0a5daba3fe92cf9b29 + "@midnight-ntwrk/midnight-js-network-id": "npm:1.0.0" + "@midnight-ntwrk/midnight-js-types": "npm:1.0.0" + "@midnight-ntwrk/midnight-js-utils": "npm:1.0.0" + checksum: 10/41a33e55788f881222ebf764b70206b420986b56c911f9534a261dec4ad4554e79871ef751a5fddcf68e552f5248a5462cf410edc5bb4a88f991140eed5942df languageName: node linkType: hard -"@midnight-ntwrk/midnight-js-fetch-zk-config-provider@npm:0.2.5": - version: 0.2.5 - resolution: "@midnight-ntwrk/midnight-js-fetch-zk-config-provider@npm:0.2.5" +"@midnight-ntwrk/midnight-js-fetch-zk-config-provider@npm:^1.0.0": + version: 1.0.0 + resolution: "@midnight-ntwrk/midnight-js-fetch-zk-config-provider@npm:1.0.0" dependencies: - "@midnight-ntwrk/midnight-js-types": "npm:0.2.5" + "@midnight-ntwrk/midnight-js-types": "npm:1.0.0" cross-fetch: "npm:^4.0.0" - checksum: 10/aa867a7e57b1a854d7aa70fbdf6e27d3ccda6388da9e4c27d5430344b7fe0d10c6ae357a47e20e61c559539378114addba562cf24c503ee25f93048c416d5942 + checksum: 10/8756a60a59d92411239274bb2532c2344a3bba962faae87dab2193c237ee3499f3adbecfe7b8f06e3a2e85a6a435fda90b8a77523f92d936cd0101721d6151bc languageName: node linkType: hard -"@midnight-ntwrk/midnight-js-http-client-proof-provider@npm:0.2.5": - version: 0.2.5 - resolution: "@midnight-ntwrk/midnight-js-http-client-proof-provider@npm:0.2.5" +"@midnight-ntwrk/midnight-js-http-client-proof-provider@npm:^1.0.0": + version: 1.0.0 + resolution: "@midnight-ntwrk/midnight-js-http-client-proof-provider@npm:1.0.0" dependencies: "@dao-xyz/borsh": "npm:^5.1.5" - "@midnight-ntwrk/midnight-js-network-id": "npm:0.2.5" - "@midnight-ntwrk/midnight-js-types": "npm:0.2.5" + "@midnight-ntwrk/midnight-js-network-id": "npm:1.0.0" + "@midnight-ntwrk/midnight-js-types": "npm:1.0.0" cross-fetch: "npm:^4.0.0" + fetch-retry: "npm:^6.0.0" lodash: "npm:^4.17.21" - checksum: 10/8e51e01363f506e67001944f17c249fac8ac16756c2405002431d7591dba1a84442a4c086a92e3ffc3a9c68028ad54d28902e22f4399e16942214f0c95b1fb69 + checksum: 10/7469fa5b66b540aceca22ef5f4069e7dc0c5c5f776510fde798dcf2638a4a79dc188aec78fa378a50ca6d8e09a5efcff939696e86de339dcb3f2587e56530f9c languageName: node linkType: hard -"@midnight-ntwrk/midnight-js-indexer-public-data-provider@npm:0.2.5": - version: 0.2.5 - resolution: "@midnight-ntwrk/midnight-js-indexer-public-data-provider@npm:0.2.5" +"@midnight-ntwrk/midnight-js-indexer-public-data-provider@npm:^1.0.0": + version: 1.0.0 + resolution: "@midnight-ntwrk/midnight-js-indexer-public-data-provider@npm:1.0.0" dependencies: "@apollo/client": "npm:^3.8.2" - "@midnight-ntwrk/midnight-js-network-id": "npm:0.2.5" - "@midnight-ntwrk/midnight-js-types": "npm:0.2.5" + "@midnight-ntwrk/midnight-js-network-id": "npm:1.0.0" + "@midnight-ntwrk/midnight-js-types": "npm:1.0.0" buffer: "npm:^6.0.3" cross-fetch: "npm:^4.0.0" graphql: "npm:^16.8.0" @@ -944,55 +945,55 @@ __metadata: rxjs: "npm:^7.5.0" ws: "npm:^8.14.2" zen-observable-ts: "npm:^1.1.0" - checksum: 10/cc31fb2cd3f0940cc0d87c2acaf8da513a0255d703dc20e71d0c7f94cf4eafb4ff03980036983a9d19ca1a32cf10b2ff1ad2eb5f74b74b3bd8a297f3c6f3b3a3 + checksum: 10/a179af0cc0bf7d9ce2709193d399a54f9b6a6b78062acedd72059acedcd7630400e69d4b39d1a385d34c6faa558c77cf6813ea4956155c3bdc1c046ee33cfcd7 languageName: node linkType: hard -"@midnight-ntwrk/midnight-js-level-private-state-provider@npm:0.2.5": - version: 0.2.5 - resolution: "@midnight-ntwrk/midnight-js-level-private-state-provider@npm:0.2.5" +"@midnight-ntwrk/midnight-js-level-private-state-provider@npm:^1.0.0": + version: 1.0.0 + resolution: "@midnight-ntwrk/midnight-js-level-private-state-provider@npm:1.0.0" dependencies: - "@midnight-ntwrk/midnight-js-types": "npm:0.2.5" - abstract-level: "npm:^1.0.3" + "@midnight-ntwrk/midnight-js-types": "npm:1.0.0" + abstract-level: "npm:^2.0.0" buffer: "npm:^6.0.3" fp-ts: "npm:^2.16.1" io-ts: "npm:^2.2.20" - level: "npm:^8.0.0" + level: "npm:^9.0.0" lodash: "npm:^4.17.21" - superjson: "npm:^1.13.1" - checksum: 10/f58dc30fc8894afde7cdf23ed25cdf2b4c7de1fe8408b3163e40a86e4dda5e2b9d40741a38194915f33216f77f98d0a9250ca78f2287e251ad44481106e57027 + superjson: "npm:^2.0.0" + checksum: 10/cf909ff1ed397888abc06e4c282b9f522742264b3952c5ac7abbfbbb82f4c5d791e6eeea1fc8f849cb17519d468235c114def877f5079df96efd5a8cb6cf7885 languageName: node linkType: hard -"@midnight-ntwrk/midnight-js-network-id@npm:0.2.5": - version: 0.2.5 - resolution: "@midnight-ntwrk/midnight-js-network-id@npm:0.2.5" - checksum: 10/1cc6736f381eb2b4b368d2ac048f332dc935f4147dfa2e6065cf525ce3a43dd4cf24328117e64479007f35323904b952f9a39f28810318b630b92e6e8954b002 +"@midnight-ntwrk/midnight-js-network-id@npm:1.0.0, @midnight-ntwrk/midnight-js-network-id@npm:^1.0.0": + version: 1.0.0 + resolution: "@midnight-ntwrk/midnight-js-network-id@npm:1.0.0" + checksum: 10/3edcdb713737368150c122b4315324f9294ba4c06a8adb1e5582a598cacdd4cca004c06e8bbeca2035fe72bfcb31abc9f0906a4328972985f085c857f3df70e3 languageName: node linkType: hard -"@midnight-ntwrk/midnight-js-node-zk-config-provider@npm:0.2.5": - version: 0.2.5 - resolution: "@midnight-ntwrk/midnight-js-node-zk-config-provider@npm:0.2.5" +"@midnight-ntwrk/midnight-js-node-zk-config-provider@npm:^1.0.0": + version: 1.0.0 + resolution: "@midnight-ntwrk/midnight-js-node-zk-config-provider@npm:1.0.0" dependencies: - "@midnight-ntwrk/midnight-js-types": "npm:0.2.5" - checksum: 10/b34768defa91c3aae7275644fffbd9f502fe1fa2a6b6ed8fe9620eb1a03b420becf516544137a4dd46271159aed5b25865f978c3bb92741ce77034540ae26771 + "@midnight-ntwrk/midnight-js-types": "npm:1.0.0" + checksum: 10/1a4a5dd6539a6288d1920d88f6d0edc985d19029246ddbf5eff6e01f5cefa54582b515631ce6c736d06aea7c32de5f9db95447788df7af23ad15994491a2b813 languageName: node linkType: hard -"@midnight-ntwrk/midnight-js-types@npm:0.2.5": - version: 0.2.5 - resolution: "@midnight-ntwrk/midnight-js-types@npm:0.2.5" +"@midnight-ntwrk/midnight-js-types@npm:1.0.0, @midnight-ntwrk/midnight-js-types@npm:^1.0.0": + version: 1.0.0 + resolution: "@midnight-ntwrk/midnight-js-types@npm:1.0.0" dependencies: rxjs: "npm:^7.5.0" - checksum: 10/0c113dfe8ef8c707f2cab1e1bd9c0350c0aa31d50030b3c1a0ce00804ef1db9d5d652ba238bb4c7a5d24507e7d8c13b0d0a1ac926d55abb62c7c94a7e2083707 + checksum: 10/a295476b6a921c87e69e844c4a4dfbe82c640c4af6a19899bcb2b3ec6b30b5b734862f248c1d22dfff061ab3980b3de1cecfb4370e32fe73ee77a6596cdd2a97 languageName: node linkType: hard -"@midnight-ntwrk/midnight-js-utils@npm:0.2.5": - version: 0.2.5 - resolution: "@midnight-ntwrk/midnight-js-utils@npm:0.2.5" - checksum: 10/5449becaf8c39f732430b01e26fb8906281113f78441c93f9a9b17231aa9ae304da1244cea30ac36ec1bef6a597124b0ba59689b4557456ce958115538bd2a23 +"@midnight-ntwrk/midnight-js-utils@npm:1.0.0, @midnight-ntwrk/midnight-js-utils@npm:^1.0.0": + version: 1.0.0 + resolution: "@midnight-ntwrk/midnight-js-utils@npm:1.0.0" + checksum: 10/f56d42a8a927fc2e174b15ed72202e00be79950345ec54ca3a4665352ea76f05d4b01c58cf5afb522a227889526bdeac0181279cd2bcd735b38a3051dcca1a75 languageName: node linkType: hard @@ -1003,7 +1004,7 @@ __metadata: languageName: node linkType: hard -"@midnight-ntwrk/wallet-api@npm:^3.4.2, @midnight-ntwrk/wallet-api@npm:^3.5.0": +"@midnight-ntwrk/wallet-api@npm:^3.4.2": version: 3.5.0 resolution: "@midnight-ntwrk/wallet-api@npm:3.5.0" dependencies: @@ -1014,18 +1015,51 @@ __metadata: languageName: node linkType: hard -"@midnight-ntwrk/wallet@npm:^3.7.3": - version: 3.7.5 - resolution: "@midnight-ntwrk/wallet@npm:3.7.5" +"@midnight-ntwrk/wallet-api@npm:^4.0.0": + version: 4.0.0 + resolution: "@midnight-ntwrk/wallet-api@npm:4.0.0" + dependencies: + "@midnight-ntwrk/zswap": "npm:^3.0.2" + peerDependencies: + rxjs: 7.x + checksum: 10/9056b6a3e53680bd8dc1639798984509dda3e66bca1a6ce43c2aa2ab0ba282fa3d89c4e91887396868b80b41038ddce7e4225782c2f5e1253cc15216804fb400 + languageName: node + linkType: hard + +"@midnight-ntwrk/wallet-sdk-address-format@npm:^1.0.0": + version: 1.0.0 + resolution: "@midnight-ntwrk/wallet-sdk-address-format@npm:1.0.0" dependencies: - "@midnight-ntwrk/wallet-api": "npm:^3.5.0" + "@scure/base": "npm:^1.1.9" + peerDependencies: + "@midnight-ntwrk/zswap": ^3.0.6 + checksum: 10/53813ac256d5e38d91e3b0b0bf47392246e027072f021f5cfe6c96daf3316ee6b61f7d45fdad627a1fce0d4ae4b4ec5c7eb5712ad5399f4b26dfd52c6203971c + languageName: node + linkType: hard + +"@midnight-ntwrk/wallet-sdk-capabilities@npm:^1.0.0": + version: 1.0.0 + resolution: "@midnight-ntwrk/wallet-sdk-capabilities@npm:1.0.0" + peerDependencies: + "@midnight-ntwrk/zswap": ^3.0.6 + checksum: 10/1d733bb60fc35161e47fe503aa85de8c63b807d3bfc479e5a25e6764dd527725e5e4b2ee46ae568083f330c3c5a1e5919934f4fc9ab66c0f01ca7d0249b88a69 + languageName: node + linkType: hard + +"@midnight-ntwrk/wallet@npm:^4.0.0": + version: 4.0.0 + resolution: "@midnight-ntwrk/wallet@npm:4.0.0" + dependencies: + "@midnight-ntwrk/wallet-api": "npm:^4.0.0" + "@midnight-ntwrk/wallet-sdk-address-format": "npm:^1.0.0" + "@midnight-ntwrk/wallet-sdk-capabilities": "npm:^1.0.0" "@midnight-ntwrk/zswap": "npm:^3.0.6" isomorphic-ws: "npm:^5.0.0" node-fetch: "npm:3.3.2" rxjs: "npm:^7.5" scale-ts: "npm:^1.1.0" ws: "npm:^8.8.1" - checksum: 10/60f57e17dc84cd980d22f6842cd82e4d1c02af20c5435833e2d8ab57559b89ac22e2fa5028f4a9d0d9da76bfcd2aa24efa2d87eef047967bbecb2c4772b9c3a0 + checksum: 10/3edf549f88c2b528b1107c9c18ca2d56beb80e201f7d3b000cc89f46d7c45c76961daa09c16b64062d7d9b6d9fc6a44ed9d59e9fb0deda6401cbf400521ba5b1 languageName: node linkType: hard @@ -1154,6 +1188,13 @@ __metadata: languageName: node linkType: hard +"@scure/base@npm:^1.1.9": + version: 1.2.4 + resolution: "@scure/base@npm:1.2.4" + checksum: 10/4b61679209af40143b49ce7b7570e1d9157c19df311ea6f57cd212d764b0b82222dbe3707334f08bec181caf1f047aca31aa91193c678d6548312cb3f9c82ab1 + languageName: node + linkType: hard + "@sinclair/typebox@npm:^0.27.8": version: 0.27.8 resolution: "@sinclair/typebox@npm:0.27.8" @@ -1592,18 +1633,17 @@ __metadata: languageName: node linkType: hard -"abstract-level@npm:^1.0.2, abstract-level@npm:^1.0.3, abstract-level@npm:^1.0.4": - version: 1.0.4 - resolution: "abstract-level@npm:1.0.4" +"abstract-level@npm:^2.0.0, abstract-level@npm:^2.0.1": + version: 2.0.2 + resolution: "abstract-level@npm:2.0.2" dependencies: buffer: "npm:^6.0.3" - catering: "npm:^2.1.0" is-buffer: "npm:^2.0.5" - level-supports: "npm:^4.0.0" + level-supports: "npm:^6.0.0" level-transcoder: "npm:^1.0.1" + maybe-combine-errors: "npm:^1.0.0" module-error: "npm:^1.0.1" - queue-microtask: "npm:^1.2.3" - checksum: 10/8edf4cf55b7b66b653296f53a643bcf1501074be099d8c44351595cd33f769b7b2aed216d5fffe1c99ebea4acf14f5ae093e98baa60ea1d236ea8a3387350ebb + checksum: 10/b41de35219ec70accc693be4c61f2b006891a51d408d328d489e745ea465022f5a5ee6afd4773ed91545668457f7e84530918bce5044953928977f7fccbe44bc languageName: node linkType: hard @@ -2091,15 +2131,12 @@ __metadata: languageName: node linkType: hard -"browser-level@npm:^1.0.1": - version: 1.0.1 - resolution: "browser-level@npm:1.0.1" +"browser-level@npm:^2.0.0": + version: 2.0.0 + resolution: "browser-level@npm:2.0.0" dependencies: - abstract-level: "npm:^1.0.2" - catering: "npm:^2.1.1" - module-error: "npm:^1.0.2" - run-parallel-limit: "npm:^1.1.0" - checksum: 10/e712569111782da76853fecf648b43ff878ff2301c2830a9e7399685b646824a85f304dea5f023e02ee41a63a972f9aad734bd411069095adc9c79784fc649a5 + abstract-level: "npm:^2.0.1" + checksum: 10/47b82677b533717386c176f59c2ef3dd5032d083933808c6614f8122f31ce13cecb20d538130d0d47ac573e848e505163378f01e8962c43fa7e70739b3e6951a languageName: node linkType: hard @@ -2279,13 +2316,6 @@ __metadata: languageName: node linkType: hard -"catering@npm:^2.1.0, catering@npm:^2.1.1": - version: 2.1.1 - resolution: "catering@npm:2.1.1" - checksum: 10/4669c9fa5f3a73273535fb458a964d8aba12dc5102d8487049cf03623bef3cdff4b5d9f92ff04c00f1001057a7cc7df6e700752ac622c2a7baf7bcff34166683 - languageName: node - linkType: hard - "chalk@npm:^4.0.0, chalk@npm:^4.0.2": version: 4.1.2 resolution: "chalk@npm:4.1.2" @@ -2331,17 +2361,16 @@ __metadata: languageName: node linkType: hard -"classic-level@npm:^1.2.0": - version: 1.4.1 - resolution: "classic-level@npm:1.4.1" +"classic-level@npm:^2.0.0": + version: 2.0.0 + resolution: "classic-level@npm:2.0.0" dependencies: - abstract-level: "npm:^1.0.2" - catering: "npm:^2.1.0" + abstract-level: "npm:^2.0.0" module-error: "npm:^1.0.1" napi-macros: "npm:^2.2.2" node-gyp: "npm:latest" node-gyp-build: "npm:^4.3.0" - checksum: 10/11f9362301477cb5cf3b147e5846754e0e4296231e265145101403f4a5cb797a685b6a9b6b4c880a42b05772f846a222a5a7a563262ca15b5ca03e25e9a805db + checksum: 10/730efa24dfcfba769c0c4c4100e04fae76587e08ab7ac5e93811d01c7307fa2e96ade054cf3c00cf125f4dc4a1f6e5bdfb77d0b3196b786afac2607a1fdbe4b8 languageName: node linkType: hard @@ -3365,6 +3394,13 @@ __metadata: languageName: node linkType: hard +"fetch-retry@npm:^6.0.0": + version: 6.0.0 + resolution: "fetch-retry@npm:6.0.0" + checksum: 10/0c8d3082e2d76fff2df75adef6280bc854bc36fd3ef38506674f0216d0d819e2efd14da7477d3f1732415aea1d2cfde7cd3e1aeae46f45f2adbfc5133296e8de + languageName: node + linkType: hard + "file-entry-cache@npm:^6.0.1": version: 6.0.1 resolution: "file-entry-cache@npm:6.0.1" @@ -5027,10 +5063,10 @@ __metadata: languageName: node linkType: hard -"level-supports@npm:^4.0.0": - version: 4.0.1 - resolution: "level-supports@npm:4.0.1" - checksum: 10/e2f177af813a25af29d15406a14240e2e10e5efb1c35b03643c885ac5931af760b9337826506b6395f98cf6b1e68ba294bfc345a248a1ae3f9c69e08e81824b2 +"level-supports@npm:^6.0.0": + version: 6.2.0 + resolution: "level-supports@npm:6.2.0" + checksum: 10/450c04839cf42ac7c73085b4928f1c1c51d9ab179aac9102cc8ef2389faf2d06cebaf57df2d025da89d78465004ccf29bfd972a04b0b35d5d423fa3f4516f906 languageName: node linkType: hard @@ -5044,14 +5080,14 @@ __metadata: languageName: node linkType: hard -"level@npm:^8.0.0": - version: 8.0.1 - resolution: "level@npm:8.0.1" +"level@npm:^9.0.0": + version: 9.0.0 + resolution: "level@npm:9.0.0" dependencies: - abstract-level: "npm:^1.0.4" - browser-level: "npm:^1.0.1" - classic-level: "npm:^1.2.0" - checksum: 10/a9c6d1fc50e30b2cc80b3c975b34de0eb12daab7fb4f8a546a28303705a45685340a904544fcd32e9a380fae7c62474ebd9cdb0108021ddbc7b88dd9c913f126 + abstract-level: "npm:^2.0.1" + browser-level: "npm:^2.0.0" + classic-level: "npm:^2.0.0" + checksum: 10/6d9dc32300dc6d2680ab3f68cd6862c7db48840070f99ccd51a1f5a6999bf41ccb2e20eeb9fe99bcf9052ff4370d783e01522fdbebe8c981cb9afbc72a8e70f0 languageName: node linkType: hard @@ -5196,6 +5232,13 @@ __metadata: languageName: node linkType: hard +"maybe-combine-errors@npm:^1.0.0": + version: 1.0.0 + resolution: "maybe-combine-errors@npm:1.0.0" + checksum: 10/16bb6d3dcf79fc61f5a04abe948c4c81cae0da6ee5da9a1d8196f1723b069d6ab60f752bc208e18481e2b82de146e068bc462558c65ecdf96fed0d021a1aa6ab + languageName: node + linkType: hard + "merge-stream@npm:^2.0.0": version: 2.0.0 resolution: "merge-stream@npm:2.0.0" @@ -5372,7 +5415,7 @@ __metadata: languageName: node linkType: hard -"module-error@npm:^1.0.1, module-error@npm:^1.0.2": +"module-error@npm:^1.0.1": version: 1.0.2 resolution: "module-error@npm:1.0.2" checksum: 10/5d653e35bd55b3e95f8aee2cdac108082ea892e71b8f651be92cde43e4ee86abee4fa8bd7fc3fe5e68b63926d42f63c54cd17b87a560c31f18739295575a3962 @@ -6024,7 +6067,7 @@ __metadata: languageName: node linkType: hard -"queue-microtask@npm:^1.2.2, queue-microtask@npm:^1.2.3": +"queue-microtask@npm:^1.2.2": version: 1.2.3 resolution: "queue-microtask@npm:1.2.3" checksum: 10/72900df0616e473e824202113c3df6abae59150dfb73ed13273503127235320e9c8ca4aaaaccfd58cf417c6ca92a6e68ee9a5c3182886ae949a768639b388a7b @@ -6264,18 +6307,18 @@ __metadata: dependencies: "@midnight-ntwrk/compact-runtime": "npm:^0.7.0" "@midnight-ntwrk/dapp-connector-api": "npm:^1.2.2" - "@midnight-ntwrk/ledger": "npm:^3.0.2" - "@midnight-ntwrk/midnight-js-contracts": "npm:0.2.5" - "@midnight-ntwrk/midnight-js-fetch-zk-config-provider": "npm:0.2.5" - "@midnight-ntwrk/midnight-js-http-client-proof-provider": "npm:0.2.5" - "@midnight-ntwrk/midnight-js-indexer-public-data-provider": "npm:0.2.5" - "@midnight-ntwrk/midnight-js-level-private-state-provider": "npm:0.2.5" - "@midnight-ntwrk/midnight-js-network-id": "npm:0.2.5" - "@midnight-ntwrk/midnight-js-node-zk-config-provider": "npm:0.2.5" - "@midnight-ntwrk/midnight-js-types": "npm:0.2.5" - "@midnight-ntwrk/midnight-js-utils": "npm:0.2.5" - "@midnight-ntwrk/wallet": "npm:^3.7.3" - "@midnight-ntwrk/wallet-api": "npm:^3.5.0" + "@midnight-ntwrk/ledger": "npm:^3.0.6" + "@midnight-ntwrk/midnight-js-contracts": "npm:^1.0.0" + "@midnight-ntwrk/midnight-js-fetch-zk-config-provider": "npm:^1.0.0" + "@midnight-ntwrk/midnight-js-http-client-proof-provider": "npm:^1.0.0" + "@midnight-ntwrk/midnight-js-indexer-public-data-provider": "npm:^1.0.0" + "@midnight-ntwrk/midnight-js-level-private-state-provider": "npm:^1.0.0" + "@midnight-ntwrk/midnight-js-network-id": "npm:^1.0.0" + "@midnight-ntwrk/midnight-js-node-zk-config-provider": "npm:^1.0.0" + "@midnight-ntwrk/midnight-js-types": "npm:^1.0.0" + "@midnight-ntwrk/midnight-js-utils": "npm:^1.0.0" + "@midnight-ntwrk/wallet": "npm:^4.0.0" + "@midnight-ntwrk/wallet-api": "npm:^4.0.0" "@types/jest": "npm:^29.5.6" "@types/node": "npm:^18.18.6" "@typescript-eslint/eslint-plugin": "npm:^6.8.0" @@ -6306,15 +6349,6 @@ __metadata: languageName: unknown linkType: soft -"run-parallel-limit@npm:^1.1.0": - version: 1.1.0 - resolution: "run-parallel-limit@npm:1.1.0" - dependencies: - queue-microtask: "npm:^1.2.2" - checksum: 10/672c3b87e7f939c684b9965222b361421db0930223ed1e43ebf0e7e48ccc1a022ea4de080bef4d5468434e2577c33b7681e3f03b7593fdc49ad250a55381123c - languageName: node - linkType: hard - "run-parallel@npm:^1.1.9": version: 1.2.0 resolution: "run-parallel@npm:1.2.0" @@ -6831,12 +6865,12 @@ __metadata: languageName: node linkType: hard -"superjson@npm:^1.13.1": - version: 1.13.3 - resolution: "superjson@npm:1.13.3" +"superjson@npm:^2.0.0": + version: 2.2.2 + resolution: "superjson@npm:2.2.2" dependencies: copy-anything: "npm:^3.0.2" - checksum: 10/71a186c513a9821e58264c0563cd1b3cf07d3b5ba53a09cc5c1a604d8ffeacac976a6ba1b5d5b3c71b6ab5a1941dfba5a15e3f106ad3ef22fe8d5eee3e2be052 + checksum: 10/6fdc709db4f69d586a18379948e0ade8268c851c791701fea960e29cea12672d7561b4ca89c4049c2e787eb1cec08a51df51d357aa6852078bc0d71d7e17b401 languageName: node linkType: hard From 780132e79168952b773116a134d060285f0c47a5 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 15 Apr 2025 23:20:32 -0500 Subject: [PATCH 002/322] remove unused deps --- package.json | 19 +- yarn.lock | 711 +-------------------------------------------------- 2 files changed, 9 insertions(+), 721 deletions(-) diff --git a/package.json b/package.json index bfe904a3..aef03893 100644 --- a/package.json +++ b/package.json @@ -11,25 +11,8 @@ "build": "turbo run build", "lint": "turbo run lint" }, - "resolutions": { - "@midnight-ntwrk/compact-runtime/@midnight-ntwrk/onchain-runtime": "^0.2.2", - "@midnight-ntwrk/zswap": "^3.0.2" - }, "dependencies": { "@midnight-ntwrk/compact-runtime": "^0.7.0", - "@midnight-ntwrk/dapp-connector-api": "^1.2.2", - "@midnight-ntwrk/ledger": "^3.0.6", - "@midnight-ntwrk/midnight-js-contracts": "^1.0.0", - "@midnight-ntwrk/midnight-js-fetch-zk-config-provider": "^1.0.0", - "@midnight-ntwrk/midnight-js-http-client-proof-provider": "^1.0.0", - "@midnight-ntwrk/midnight-js-indexer-public-data-provider": "^1.0.0", - "@midnight-ntwrk/midnight-js-level-private-state-provider": "^1.0.0", - "@midnight-ntwrk/midnight-js-network-id": "^1.0.0", - "@midnight-ntwrk/midnight-js-node-zk-config-provider": "^1.0.0", - "@midnight-ntwrk/midnight-js-types": "^1.0.0", - "@midnight-ntwrk/midnight-js-utils": "^1.0.0", - "@midnight-ntwrk/wallet": "^4.0.0", - "@midnight-ntwrk/wallet-api": "^4.0.0", "fp-ts": "^2.16.1", "io-ts": "^2.2.20", "pino": "^8.16.0", @@ -37,6 +20,8 @@ "rxjs": "^7.8.1" }, "devDependencies": { + "@midnight-ntwrk/ledger": "^3.0.6", + "@midnight-ntwrk/zswap": "^3.0.6", "@types/jest": "^29.5.6", "@types/node": "^18.18.6", "@typescript-eslint/eslint-plugin": "^6.8.0", diff --git a/yarn.lock b/yarn.lock index 7eb3114a..64965d1e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15,42 +15,6 @@ __metadata: languageName: node linkType: hard -"@apollo/client@npm:^3.8.2": - version: 3.13.1 - resolution: "@apollo/client@npm:3.13.1" - dependencies: - "@graphql-typed-document-node/core": "npm:^3.1.1" - "@wry/caches": "npm:^1.0.0" - "@wry/equality": "npm:^0.5.6" - "@wry/trie": "npm:^0.5.0" - graphql-tag: "npm:^2.12.6" - hoist-non-react-statics: "npm:^3.3.2" - optimism: "npm:^0.18.0" - prop-types: "npm:^15.7.2" - rehackt: "npm:^0.1.0" - symbol-observable: "npm:^4.0.0" - ts-invariant: "npm:^0.10.3" - tslib: "npm:^2.3.0" - zen-observable-ts: "npm:^1.2.5" - peerDependencies: - graphql: ^15.0.0 || ^16.0.0 - graphql-ws: ^5.5.5 || ^6.0.3 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc - subscriptions-transport-ws: ^0.9.0 || ^0.11.0 - peerDependenciesMeta: - graphql-ws: - optional: true - react: - optional: true - react-dom: - optional: true - subscriptions-transport-ws: - optional: true - checksum: 10/d3744a5416c7ba33057b1ed247fa4b30da167a6b490898968e6e03870424906c3b4b1910829dc5b26622393e3f203b6ad26e7f6a2c2e9505dc0f9e915432482a - languageName: node - linkType: hard - "@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.26.2": version: 7.26.2 resolution: "@babel/code-frame@npm:7.26.2" @@ -436,16 +400,6 @@ __metadata: languageName: node linkType: hard -"@dao-xyz/borsh@npm:^5.1.5": - version: 5.2.3 - resolution: "@dao-xyz/borsh@npm:5.2.3" - dependencies: - "@protobufjs/float": "npm:^1.0.2" - "@protobufjs/utf8": "npm:^1.1.0" - checksum: 10/87480526dd501ee5726aa39dccb27018e82e00a0d21ec7eaed6f23dd80eb94662f2395b17b09b44cd34ff06ffde9458d7734940e42cf5e7b5daf2c7bc03cfe3a - languageName: node - linkType: hard - "@eslint-community/eslint-utils@npm:^4.1.2, @eslint-community/eslint-utils@npm:^4.2.0, @eslint-community/eslint-utils@npm:^4.4.0": version: 4.4.1 resolution: "@eslint-community/eslint-utils@npm:4.4.1" @@ -495,15 +449,6 @@ __metadata: languageName: node linkType: hard -"@graphql-typed-document-node/core@npm:^3.1.1": - version: 3.2.0 - resolution: "@graphql-typed-document-node/core@npm:3.2.0" - peerDependencies: - graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - checksum: 10/fa44443accd28c8cf4cb96aaaf39d144a22e8b091b13366843f4e97d19c7bfeaf609ce3c7603a4aeffe385081eaf8ea245d078633a7324c11c5ec4b2011bb76d - languageName: node - linkType: hard - "@humanwhocodes/config-array@npm:^0.13.0": version: 0.13.0 resolution: "@humanwhocodes/config-array@npm:0.13.0" @@ -877,17 +822,6 @@ __metadata: languageName: unknown linkType: soft -"@midnight-ntwrk/dapp-connector-api@npm:^1.2.2": - version: 1.2.3 - resolution: "@midnight-ntwrk/dapp-connector-api@npm:1.2.3" - dependencies: - "@midnight-ntwrk/wallet-api": "npm:^3.4.2" - "@midnight-ntwrk/zswap": "npm:^3.0.2" - ts-custom-error: "npm:^3.3.1" - checksum: 10/2ecc68a486fa535b42361ba89e1703485ed25c533ee4e312bcd16bde83e990ad37c4f928f08531954da172a9773d0561473154c4feb034915779a491f8816ce0 - languageName: node - linkType: hard - "@midnight-ntwrk/ledger@npm:^3.0.6": version: 3.0.6 resolution: "@midnight-ntwrk/ledger@npm:3.0.6" @@ -895,175 +829,14 @@ __metadata: languageName: node linkType: hard -"@midnight-ntwrk/midnight-js-contracts@npm:^1.0.0": - version: 1.0.0 - resolution: "@midnight-ntwrk/midnight-js-contracts@npm:1.0.0" - dependencies: - "@midnight-ntwrk/midnight-js-network-id": "npm:1.0.0" - "@midnight-ntwrk/midnight-js-types": "npm:1.0.0" - "@midnight-ntwrk/midnight-js-utils": "npm:1.0.0" - checksum: 10/41a33e55788f881222ebf764b70206b420986b56c911f9534a261dec4ad4554e79871ef751a5fddcf68e552f5248a5462cf410edc5bb4a88f991140eed5942df - languageName: node - linkType: hard - -"@midnight-ntwrk/midnight-js-fetch-zk-config-provider@npm:^1.0.0": - version: 1.0.0 - resolution: "@midnight-ntwrk/midnight-js-fetch-zk-config-provider@npm:1.0.0" - dependencies: - "@midnight-ntwrk/midnight-js-types": "npm:1.0.0" - cross-fetch: "npm:^4.0.0" - checksum: 10/8756a60a59d92411239274bb2532c2344a3bba962faae87dab2193c237ee3499f3adbecfe7b8f06e3a2e85a6a435fda90b8a77523f92d936cd0101721d6151bc - languageName: node - linkType: hard - -"@midnight-ntwrk/midnight-js-http-client-proof-provider@npm:^1.0.0": - version: 1.0.0 - resolution: "@midnight-ntwrk/midnight-js-http-client-proof-provider@npm:1.0.0" - dependencies: - "@dao-xyz/borsh": "npm:^5.1.5" - "@midnight-ntwrk/midnight-js-network-id": "npm:1.0.0" - "@midnight-ntwrk/midnight-js-types": "npm:1.0.0" - cross-fetch: "npm:^4.0.0" - fetch-retry: "npm:^6.0.0" - lodash: "npm:^4.17.21" - checksum: 10/7469fa5b66b540aceca22ef5f4069e7dc0c5c5f776510fde798dcf2638a4a79dc188aec78fa378a50ca6d8e09a5efcff939696e86de339dcb3f2587e56530f9c - languageName: node - linkType: hard - -"@midnight-ntwrk/midnight-js-indexer-public-data-provider@npm:^1.0.0": - version: 1.0.0 - resolution: "@midnight-ntwrk/midnight-js-indexer-public-data-provider@npm:1.0.0" - dependencies: - "@apollo/client": "npm:^3.8.2" - "@midnight-ntwrk/midnight-js-network-id": "npm:1.0.0" - "@midnight-ntwrk/midnight-js-types": "npm:1.0.0" - buffer: "npm:^6.0.3" - cross-fetch: "npm:^4.0.0" - graphql: "npm:^16.8.0" - graphql-ws: "npm:^5.14.0" - isomorphic-ws: "npm:^5.0.0" - rxjs: "npm:^7.5.0" - ws: "npm:^8.14.2" - zen-observable-ts: "npm:^1.1.0" - checksum: 10/a179af0cc0bf7d9ce2709193d399a54f9b6a6b78062acedd72059acedcd7630400e69d4b39d1a385d34c6faa558c77cf6813ea4956155c3bdc1c046ee33cfcd7 - languageName: node - linkType: hard - -"@midnight-ntwrk/midnight-js-level-private-state-provider@npm:^1.0.0": - version: 1.0.0 - resolution: "@midnight-ntwrk/midnight-js-level-private-state-provider@npm:1.0.0" - dependencies: - "@midnight-ntwrk/midnight-js-types": "npm:1.0.0" - abstract-level: "npm:^2.0.0" - buffer: "npm:^6.0.3" - fp-ts: "npm:^2.16.1" - io-ts: "npm:^2.2.20" - level: "npm:^9.0.0" - lodash: "npm:^4.17.21" - superjson: "npm:^2.0.0" - checksum: 10/cf909ff1ed397888abc06e4c282b9f522742264b3952c5ac7abbfbbb82f4c5d791e6eeea1fc8f849cb17519d468235c114def877f5079df96efd5a8cb6cf7885 - languageName: node - linkType: hard - -"@midnight-ntwrk/midnight-js-network-id@npm:1.0.0, @midnight-ntwrk/midnight-js-network-id@npm:^1.0.0": - version: 1.0.0 - resolution: "@midnight-ntwrk/midnight-js-network-id@npm:1.0.0" - checksum: 10/3edcdb713737368150c122b4315324f9294ba4c06a8adb1e5582a598cacdd4cca004c06e8bbeca2035fe72bfcb31abc9f0906a4328972985f085c857f3df70e3 - languageName: node - linkType: hard - -"@midnight-ntwrk/midnight-js-node-zk-config-provider@npm:^1.0.0": - version: 1.0.0 - resolution: "@midnight-ntwrk/midnight-js-node-zk-config-provider@npm:1.0.0" - dependencies: - "@midnight-ntwrk/midnight-js-types": "npm:1.0.0" - checksum: 10/1a4a5dd6539a6288d1920d88f6d0edc985d19029246ddbf5eff6e01f5cefa54582b515631ce6c736d06aea7c32de5f9db95447788df7af23ad15994491a2b813 - languageName: node - linkType: hard - -"@midnight-ntwrk/midnight-js-types@npm:1.0.0, @midnight-ntwrk/midnight-js-types@npm:^1.0.0": - version: 1.0.0 - resolution: "@midnight-ntwrk/midnight-js-types@npm:1.0.0" - dependencies: - rxjs: "npm:^7.5.0" - checksum: 10/a295476b6a921c87e69e844c4a4dfbe82c640c4af6a19899bcb2b3ec6b30b5b734862f248c1d22dfff061ab3980b3de1cecfb4370e32fe73ee77a6596cdd2a97 - languageName: node - linkType: hard - -"@midnight-ntwrk/midnight-js-utils@npm:1.0.0, @midnight-ntwrk/midnight-js-utils@npm:^1.0.0": - version: 1.0.0 - resolution: "@midnight-ntwrk/midnight-js-utils@npm:1.0.0" - checksum: 10/f56d42a8a927fc2e174b15ed72202e00be79950345ec54ca3a4665352ea76f05d4b01c58cf5afb522a227889526bdeac0181279cd2bcd735b38a3051dcca1a75 - languageName: node - linkType: hard - -"@midnight-ntwrk/onchain-runtime@npm:^0.2.2": +"@midnight-ntwrk/onchain-runtime@npm:^0.2.0": version: 0.2.6 resolution: "@midnight-ntwrk/onchain-runtime@npm:0.2.6" checksum: 10/6c7bf8a6d9dfd4560f1da67a0b0a2a89331eecb8110b07f7d2eab1c311cadc8f5fc42e46118649766490e534b504c0e7cad94cfdcccdbb1b09d9f69877707ebd languageName: node linkType: hard -"@midnight-ntwrk/wallet-api@npm:^3.4.2": - version: 3.5.0 - resolution: "@midnight-ntwrk/wallet-api@npm:3.5.0" - dependencies: - "@midnight-ntwrk/zswap": "npm:^3.0.2" - peerDependencies: - rxjs: 7.x - checksum: 10/56777aa3d5df442767c88c4839ae397b50c09057542635a52f276999e1c831fd138bd0baf519cd339948c420e040cd49908086599fa1417f8d61123aafb514ad - languageName: node - linkType: hard - -"@midnight-ntwrk/wallet-api@npm:^4.0.0": - version: 4.0.0 - resolution: "@midnight-ntwrk/wallet-api@npm:4.0.0" - dependencies: - "@midnight-ntwrk/zswap": "npm:^3.0.2" - peerDependencies: - rxjs: 7.x - checksum: 10/9056b6a3e53680bd8dc1639798984509dda3e66bca1a6ce43c2aa2ab0ba282fa3d89c4e91887396868b80b41038ddce7e4225782c2f5e1253cc15216804fb400 - languageName: node - linkType: hard - -"@midnight-ntwrk/wallet-sdk-address-format@npm:^1.0.0": - version: 1.0.0 - resolution: "@midnight-ntwrk/wallet-sdk-address-format@npm:1.0.0" - dependencies: - "@scure/base": "npm:^1.1.9" - peerDependencies: - "@midnight-ntwrk/zswap": ^3.0.6 - checksum: 10/53813ac256d5e38d91e3b0b0bf47392246e027072f021f5cfe6c96daf3316ee6b61f7d45fdad627a1fce0d4ae4b4ec5c7eb5712ad5399f4b26dfd52c6203971c - languageName: node - linkType: hard - -"@midnight-ntwrk/wallet-sdk-capabilities@npm:^1.0.0": - version: 1.0.0 - resolution: "@midnight-ntwrk/wallet-sdk-capabilities@npm:1.0.0" - peerDependencies: - "@midnight-ntwrk/zswap": ^3.0.6 - checksum: 10/1d733bb60fc35161e47fe503aa85de8c63b807d3bfc479e5a25e6764dd527725e5e4b2ee46ae568083f330c3c5a1e5919934f4fc9ab66c0f01ca7d0249b88a69 - languageName: node - linkType: hard - -"@midnight-ntwrk/wallet@npm:^4.0.0": - version: 4.0.0 - resolution: "@midnight-ntwrk/wallet@npm:4.0.0" - dependencies: - "@midnight-ntwrk/wallet-api": "npm:^4.0.0" - "@midnight-ntwrk/wallet-sdk-address-format": "npm:^1.0.0" - "@midnight-ntwrk/wallet-sdk-capabilities": "npm:^1.0.0" - "@midnight-ntwrk/zswap": "npm:^3.0.6" - isomorphic-ws: "npm:^5.0.0" - node-fetch: "npm:3.3.2" - rxjs: "npm:^7.5" - scale-ts: "npm:^1.1.0" - ws: "npm:^8.8.1" - checksum: 10/3edf549f88c2b528b1107c9c18ca2d56beb80e201f7d3b000cc89f46d7c45c76961daa09c16b64062d7d9b6d9fc6a44ed9d59e9fb0deda6401cbf400521ba5b1 - languageName: node - linkType: hard - -"@midnight-ntwrk/zswap@npm:^3.0.2": +"@midnight-ntwrk/zswap@npm:^3.0.6": version: 3.0.6 resolution: "@midnight-ntwrk/zswap@npm:3.0.6" checksum: 10/d095b9380d1ca9f0f94286ce31bd8aaee0ee6458d8681d5b021198c1125e9e9276ebf7bd6ca5221c800ec1d8ed27909faf866268e3673d36a5126c2c9b99b595 @@ -1167,20 +940,6 @@ __metadata: languageName: node linkType: hard -"@protobufjs/float@npm:^1.0.2": - version: 1.0.2 - resolution: "@protobufjs/float@npm:1.0.2" - checksum: 10/634c2c989da0ef2f4f19373d64187e2a79f598c5fb7991afb689d29a2ea17c14b796b29725945fa34b9493c17fb799e08ac0a7ccaae460ee1757d3083ed35187 - languageName: node - linkType: hard - -"@protobufjs/utf8@npm:^1.1.0": - version: 1.1.0 - resolution: "@protobufjs/utf8@npm:1.1.0" - checksum: 10/131e289c57534c1d73a0e55782d6751dd821db1583cb2f7f7e017c9d6747addaebe79f28120b2e0185395d990aad347fb14ffa73ef4096fa38508d61a0e64602 - languageName: node - linkType: hard - "@rtsao/scc@npm:^1.1.0": version: 1.1.0 resolution: "@rtsao/scc@npm:1.1.0" @@ -1188,13 +947,6 @@ __metadata: languageName: node linkType: hard -"@scure/base@npm:^1.1.9": - version: 1.2.4 - resolution: "@scure/base@npm:1.2.4" - checksum: 10/4b61679209af40143b49ce7b7570e1d9157c19df311ea6f57cd212d764b0b82222dbe3707334f08bec181caf1f047aca31aa91193c678d6548312cb3f9c82ab1 - languageName: node - linkType: hard - "@sinclair/typebox@npm:^0.27.8": version: 0.27.8 resolution: "@sinclair/typebox@npm:0.27.8" @@ -1581,42 +1333,6 @@ __metadata: languageName: node linkType: hard -"@wry/caches@npm:^1.0.0": - version: 1.0.1 - resolution: "@wry/caches@npm:1.0.1" - dependencies: - tslib: "npm:^2.3.0" - checksum: 10/055f592ee52b5fd9aa86e274e54e4a8b2650f619000bf6f61880ce14aaf47eb2ab34f3ada2eab964fe8b2f19bf8097ecacddcea4638fcc64c3d3a0a512aaa07c - languageName: node - linkType: hard - -"@wry/context@npm:^0.7.0": - version: 0.7.4 - resolution: "@wry/context@npm:0.7.4" - dependencies: - tslib: "npm:^2.3.0" - checksum: 10/70d648949a97a035b2be2d6ddb716d4162113e850ab2c4c86331b2da94a7e826204080ce04eee2a95665bd3a0b245bf2ea3aae9adfa57b004ae0d2d49bdb5c8f - languageName: node - linkType: hard - -"@wry/equality@npm:^0.5.6": - version: 0.5.7 - resolution: "@wry/equality@npm:0.5.7" - dependencies: - tslib: "npm:^2.3.0" - checksum: 10/69dccf33c0c41fd7ec5550f5703b857c6484a949412ad747001da941270ea436648c3ab988a2091765304249585ac30c7b417fad8be9a7ce19c1221f71548e35 - languageName: node - linkType: hard - -"@wry/trie@npm:^0.5.0": - version: 0.5.0 - resolution: "@wry/trie@npm:0.5.0" - dependencies: - tslib: "npm:^2.3.0" - checksum: 10/578a08f3a96256c9b163230337183d9511fd775bdfe147a30561ccaacedc9ce33b9731ee6e591bb1f5f53e41b26789e519b47dff5100c7bf4e1cd2df3062f797 - languageName: node - linkType: hard - "abbrev@npm:^3.0.0": version: 3.0.0 resolution: "abbrev@npm:3.0.0" @@ -1633,20 +1349,6 @@ __metadata: languageName: node linkType: hard -"abstract-level@npm:^2.0.0, abstract-level@npm:^2.0.1": - version: 2.0.2 - resolution: "abstract-level@npm:2.0.2" - dependencies: - buffer: "npm:^6.0.3" - is-buffer: "npm:^2.0.5" - level-supports: "npm:^6.0.0" - level-transcoder: "npm:^1.0.1" - maybe-combine-errors: "npm:^1.0.0" - module-error: "npm:^1.0.1" - checksum: 10/b41de35219ec70accc693be4c61f2b006891a51d408d328d489e745ea465022f5a5ee6afd4773ed91545668457f7e84530918bce5044953928977f7fccbe44bc - languageName: node - linkType: hard - "acorn-jsx@npm:^5.3.2": version: 5.3.2 resolution: "acorn-jsx@npm:5.3.2" @@ -2131,15 +1833,6 @@ __metadata: languageName: node linkType: hard -"browser-level@npm:^2.0.0": - version: 2.0.0 - resolution: "browser-level@npm:2.0.0" - dependencies: - abstract-level: "npm:^2.0.1" - checksum: 10/47b82677b533717386c176f59c2ef3dd5032d083933808c6614f8122f31ce13cecb20d538130d0d47ac573e848e505163378f01e8962c43fa7e70739b3e6951a - languageName: node - linkType: hard - "browserslist@npm:^4.24.0": version: 4.24.4 resolution: "browserslist@npm:4.24.4" @@ -2361,19 +2054,6 @@ __metadata: languageName: node linkType: hard -"classic-level@npm:^2.0.0": - version: 2.0.0 - resolution: "classic-level@npm:2.0.0" - dependencies: - abstract-level: "npm:^2.0.0" - module-error: "npm:^1.0.1" - napi-macros: "npm:^2.2.2" - node-gyp: "npm:latest" - node-gyp-build: "npm:^4.3.0" - checksum: 10/730efa24dfcfba769c0c4c4100e04fae76587e08ab7ac5e93811d01c7307fa2e96ade054cf3c00cf125f4dc4a1f6e5bdfb77d0b3196b786afac2607a1fdbe4b8 - languageName: node - linkType: hard - "cliui@npm:^8.0.1": version: 8.0.1 resolution: "cliui@npm:8.0.1" @@ -2449,15 +2129,6 @@ __metadata: languageName: node linkType: hard -"copy-anything@npm:^3.0.2": - version: 3.0.5 - resolution: "copy-anything@npm:3.0.5" - dependencies: - is-what: "npm:^4.1.8" - checksum: 10/4c41385a94a1cff6352a954f9b1c05b6bb1b70713a2d31f4c7b188ae7187ce00ddcc9c09bd58d24cd35b67fc6dd84df5954c0be86ea10700ff74e677db3cb09c - languageName: node - linkType: hard - "core-util-is@npm:~1.0.0": version: 1.0.3 resolution: "core-util-is@npm:1.0.3" @@ -2519,15 +2190,6 @@ __metadata: languageName: node linkType: hard -"cross-fetch@npm:^4.0.0": - version: 4.1.0 - resolution: "cross-fetch@npm:4.1.0" - dependencies: - node-fetch: "npm:^2.7.0" - checksum: 10/07624940607b64777d27ec9c668ddb6649e8c59ee0a5a10e63a51ce857e2bbb1294a45854a31c10eccb91b65909a5b199fcb0217339b44156f85900a7384f489 - languageName: node - linkType: hard - "cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3, cross-spawn@npm:^7.0.6": version: 7.0.6 resolution: "cross-spawn@npm:7.0.6" @@ -2539,13 +2201,6 @@ __metadata: languageName: node linkType: hard -"data-uri-to-buffer@npm:^4.0.0": - version: 4.0.1 - resolution: "data-uri-to-buffer@npm:4.0.1" - checksum: 10/0d0790b67ffec5302f204c2ccca4494f70b4e2d940fea3d36b09f0bb2b8539c2e86690429eb1f1dc4bcc9e4df0644193073e63d9ee48ac9fce79ec1506e4aa4c - languageName: node - linkType: hard - "data-view-buffer@npm:^1.0.2": version: 1.0.2 resolution: "data-view-buffer@npm:1.0.2" @@ -3384,23 +3039,6 @@ __metadata: languageName: node linkType: hard -"fetch-blob@npm:^3.1.2, fetch-blob@npm:^3.1.4": - version: 3.2.0 - resolution: "fetch-blob@npm:3.2.0" - dependencies: - node-domexception: "npm:^1.0.0" - web-streams-polyfill: "npm:^3.0.3" - checksum: 10/5264ecceb5fdc19eb51d1d0359921f12730941e333019e673e71eb73921146dceabcb0b8f534582be4497312d656508a439ad0f5edeec2b29ab2e10c72a1f86b - languageName: node - linkType: hard - -"fetch-retry@npm:^6.0.0": - version: 6.0.0 - resolution: "fetch-retry@npm:6.0.0" - checksum: 10/0c8d3082e2d76fff2df75adef6280bc854bc36fd3ef38506674f0216d0d819e2efd14da7477d3f1732415aea1d2cfde7cd3e1aeae46f45f2adbfc5133296e8de - languageName: node - linkType: hard - "file-entry-cache@npm:^6.0.1": version: 6.0.1 resolution: "file-entry-cache@npm:6.0.1" @@ -3485,15 +3123,6 @@ __metadata: languageName: node linkType: hard -"formdata-polyfill@npm:^4.0.10": - version: 4.0.10 - resolution: "formdata-polyfill@npm:4.0.10" - dependencies: - fetch-blob: "npm:^3.1.2" - checksum: 10/9b5001d2edef3c9449ac3f48bd4f8cc92e7d0f2e7c1a5c8ba555ad4e77535cc5cf621fabe49e97f304067037282dd9093b9160a3cb533e46420b446c4e6bc06f - languageName: node - linkType: hard - "fp-ts@npm:^2.16.1": version: 2.16.9 resolution: "fp-ts@npm:2.16.9" @@ -3774,33 +3403,6 @@ __metadata: languageName: node linkType: hard -"graphql-tag@npm:^2.12.6": - version: 2.12.6 - resolution: "graphql-tag@npm:2.12.6" - dependencies: - tslib: "npm:^2.1.0" - peerDependencies: - graphql: ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 - checksum: 10/23a2bc1d3fbeae86444204e0ac08522e09dc369559ba75768e47421a7321b59f352fb5b2c9a5c37d3cf6de890dca4e5ac47e740c7cc622e728572ecaa649089e - languageName: node - linkType: hard - -"graphql-ws@npm:^5.14.0": - version: 5.16.2 - resolution: "graphql-ws@npm:5.16.2" - peerDependencies: - graphql: ">=0.11 <=16" - checksum: 10/6647bfe640b467f27aaf5ee044c1d114fe266e82cda4ebbb4368d5a4e98df5d2de9d6be70d28eb5e821d87fbf8964c3a8a18abf87c76d4f148800fd8e0488c3d - languageName: node - linkType: hard - -"graphql@npm:^16.8.0": - version: 16.10.0 - resolution: "graphql@npm:16.10.0" - checksum: 10/d42cf81ddcf3a61dfb213217576bf33c326f15b02c4cee369b373dc74100cbdcdc4479b3b797e79b654dabd8fddf50ef65ff75420e9ce5596c02e21f24c9126a - languageName: node - linkType: hard - "has-bigints@npm:^1.0.2": version: 1.1.0 resolution: "has-bigints@npm:1.1.0" @@ -3865,15 +3467,6 @@ __metadata: languageName: node linkType: hard -"hoist-non-react-statics@npm:^3.3.2": - version: 3.3.2 - resolution: "hoist-non-react-statics@npm:3.3.2" - dependencies: - react-is: "npm:^16.7.0" - checksum: 10/1acbe85f33e5a39f90c822ad4d28b24daeb60f71c545279431dc98c312cd28a54f8d64788e477fe21dc502b0e3cf58589ebe5c1ad22af27245370391c2d24ea6 - languageName: node - linkType: hard - "html-escaper@npm:^2.0.0": version: 2.0.2 resolution: "html-escaper@npm:2.0.2" @@ -4064,13 +3657,6 @@ __metadata: languageName: node linkType: hard -"is-buffer@npm:^2.0.5": - version: 2.0.5 - resolution: "is-buffer@npm:2.0.5" - checksum: 10/3261a8b858edcc6c9566ba1694bf829e126faa88911d1c0a747ea658c5d81b14b6955e3a702d59dabadd58fdd440c01f321aa71d6547105fd21d03f94d0597e7 - languageName: node - linkType: hard - "is-builtin-module@npm:^3.2.1": version: 3.2.1 resolution: "is-builtin-module@npm:3.2.1" @@ -4299,13 +3885,6 @@ __metadata: languageName: node linkType: hard -"is-what@npm:^4.1.8": - version: 4.1.16 - resolution: "is-what@npm:4.1.16" - checksum: 10/f6400634bae77be6903365dc53817292e1c4d8db1b467515d0c842505b8388ee8e558326d5e6952cb2a9d74116eca2af0c6adb8aa7e9d5c845a130ce9328bf13 - languageName: node - linkType: hard - "is-wsl@npm:^2.2.0": version: 2.2.0 resolution: "is-wsl@npm:2.2.0" @@ -4343,15 +3922,6 @@ __metadata: languageName: node linkType: hard -"isomorphic-ws@npm:^5.0.0": - version: 5.0.0 - resolution: "isomorphic-ws@npm:5.0.0" - peerDependencies: - ws: "*" - checksum: 10/e20eb2aee09ba96247465fda40c6d22c1153394c0144fa34fe6609f341af4c8c564f60ea3ba762335a7a9c306809349f9b863c8beedf2beea09b299834ad5398 - languageName: node - linkType: hard - "istanbul-lib-coverage@npm:^3.0.0, istanbul-lib-coverage@npm:^3.2.0": version: 3.2.2 resolution: "istanbul-lib-coverage@npm:3.2.2" @@ -4931,7 +4501,7 @@ __metadata: languageName: node linkType: hard -"js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0": +"js-tokens@npm:^4.0.0": version: 4.0.0 resolution: "js-tokens@npm:4.0.0" checksum: 10/af37d0d913fb56aec6dc0074c163cc71cd23c0b8aad5c2350747b6721d37ba118af35abdd8b33c47ec2800de07dedb16a527ca9c530ee004093e04958bd0cbf2 @@ -5063,34 +4633,6 @@ __metadata: languageName: node linkType: hard -"level-supports@npm:^6.0.0": - version: 6.2.0 - resolution: "level-supports@npm:6.2.0" - checksum: 10/450c04839cf42ac7c73085b4928f1c1c51d9ab179aac9102cc8ef2389faf2d06cebaf57df2d025da89d78465004ccf29bfd972a04b0b35d5d423fa3f4516f906 - languageName: node - linkType: hard - -"level-transcoder@npm:^1.0.1": - version: 1.0.1 - resolution: "level-transcoder@npm:1.0.1" - dependencies: - buffer: "npm:^6.0.3" - module-error: "npm:^1.0.1" - checksum: 10/2fb41a1d8037fc279f851ead8cdc3852b738f1f935ac2895183cd606aae3e57008e085c7c2bd2b2d43cfd057333108cfaed604092e173ac2abdf5ab1b8333f9e - languageName: node - linkType: hard - -"level@npm:^9.0.0": - version: 9.0.0 - resolution: "level@npm:9.0.0" - dependencies: - abstract-level: "npm:^2.0.1" - browser-level: "npm:^2.0.0" - classic-level: "npm:^2.0.0" - checksum: 10/6d9dc32300dc6d2680ab3f68cd6862c7db48840070f99ccd51a1f5a6999bf41ccb2e20eeb9fe99bcf9052ff4370d783e01522fdbebe8c981cb9afbc72a8e70f0 - languageName: node - linkType: hard - "leven@npm:^3.1.0": version: 3.1.0 resolution: "leven@npm:3.1.0" @@ -5147,24 +4689,13 @@ __metadata: languageName: node linkType: hard -"lodash@npm:^4.17.15, lodash@npm:^4.17.21": +"lodash@npm:^4.17.15": version: 4.17.21 resolution: "lodash@npm:4.17.21" checksum: 10/c08619c038846ea6ac754abd6dd29d2568aa705feb69339e836dfa8d8b09abbb2f859371e86863eda41848221f9af43714491467b5b0299122431e202bb0c532 languageName: node linkType: hard -"loose-envify@npm:^1.4.0": - version: 1.4.0 - resolution: "loose-envify@npm:1.4.0" - dependencies: - js-tokens: "npm:^3.0.0 || ^4.0.0" - bin: - loose-envify: cli.js - checksum: 10/6517e24e0cad87ec9888f500c5b5947032cdfe6ef65e1c1936a0c48a524b81e65542c9c3edc91c97d5bddc806ee2a985dbc79be89215d613b1de5db6d1cfe6f4 - languageName: node - linkType: hard - "lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0": version: 10.4.3 resolution: "lru-cache@npm:10.4.3" @@ -5232,13 +4763,6 @@ __metadata: languageName: node linkType: hard -"maybe-combine-errors@npm:^1.0.0": - version: 1.0.0 - resolution: "maybe-combine-errors@npm:1.0.0" - checksum: 10/16bb6d3dcf79fc61f5a04abe948c4c81cae0da6ee5da9a1d8196f1723b069d6ab60f752bc208e18481e2b82de146e068bc462558c65ecdf96fed0d021a1aa6ab - languageName: node - linkType: hard - "merge-stream@npm:^2.0.0": version: 2.0.0 resolution: "merge-stream@npm:2.0.0" @@ -5415,13 +4939,6 @@ __metadata: languageName: node linkType: hard -"module-error@npm:^1.0.1": - version: 1.0.2 - resolution: "module-error@npm:1.0.2" - checksum: 10/5d653e35bd55b3e95f8aee2cdac108082ea892e71b8f651be92cde43e4ee86abee4fa8bd7fc3fe5e68b63926d42f63c54cd17b87a560c31f18739295575a3962 - languageName: node - linkType: hard - "ms@npm:^2.1.1, ms@npm:^2.1.3": version: 2.1.3 resolution: "ms@npm:2.1.3" @@ -5438,13 +4955,6 @@ __metadata: languageName: node linkType: hard -"napi-macros@npm:^2.2.2": - version: 2.2.2 - resolution: "napi-macros@npm:2.2.2" - checksum: 10/2cdb9c40ad4b424b14fbe5e13c5329559e2b511665acf41cdcda172fd2270202dc747a2d288b687c72bc70f654c797bc24a93adb67631128d62461588d7cc070 - languageName: node - linkType: hard - "natural-compare@npm:^1.4.0": version: 1.4.0 resolution: "natural-compare@npm:1.4.0" @@ -5459,49 +4969,6 @@ __metadata: languageName: node linkType: hard -"node-domexception@npm:^1.0.0": - version: 1.0.0 - resolution: "node-domexception@npm:1.0.0" - checksum: 10/e332522f242348c511640c25a6fc7da4f30e09e580c70c6b13cb0be83c78c3e71c8d4665af2527e869fc96848924a4316ae7ec9014c091e2156f41739d4fa233 - languageName: node - linkType: hard - -"node-fetch@npm:3.3.2": - version: 3.3.2 - resolution: "node-fetch@npm:3.3.2" - dependencies: - data-uri-to-buffer: "npm:^4.0.0" - fetch-blob: "npm:^3.1.4" - formdata-polyfill: "npm:^4.0.10" - checksum: 10/24207ca8c81231c7c59151840e3fded461d67a31cf3e3b3968e12201a42f89ce4a0b5fb7079b1fa0a4655957b1ca9257553200f03a9f668b45ebad265ca5593d - languageName: node - linkType: hard - -"node-fetch@npm:^2.7.0": - version: 2.7.0 - resolution: "node-fetch@npm:2.7.0" - dependencies: - whatwg-url: "npm:^5.0.0" - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true - checksum: 10/b24f8a3dc937f388192e59bcf9d0857d7b6940a2496f328381641cb616efccc9866e89ec43f2ec956bbd6c3d3ee05524ce77fe7b29ccd34692b3a16f237d6676 - languageName: node - linkType: hard - -"node-gyp-build@npm:^4.3.0": - version: 4.8.4 - resolution: "node-gyp-build@npm:4.8.4" - bin: - node-gyp-build: bin.js - node-gyp-build-optional: optional.js - node-gyp-build-test: build-test.js - checksum: 10/6a7d62289d1afc419fc8fc9bd00aa4e554369e50ca0acbc215cb91446148b75ff7e2a3b53c2c5b2c09a39d416d69f3d3237937860373104b5fe429bf30ad9ac5 - languageName: node - linkType: hard - "node-gyp@npm:latest": version: 11.1.0 resolution: "node-gyp@npm:11.1.0" @@ -5563,13 +5030,6 @@ __metadata: languageName: node linkType: hard -"object-assign@npm:^4.1.1": - version: 4.1.1 - resolution: "object-assign@npm:4.1.1" - checksum: 10/fcc6e4ea8c7fe48abfbb552578b1c53e0d194086e2e6bbbf59e0a536381a292f39943c6e9628af05b5528aa5e3318bb30d6b2e53cadaf5b8fe9e12c4b69af23f - languageName: node - linkType: hard - "object-inspect@npm:^1.12.3, object-inspect@npm:^1.13.3": version: 1.13.4 resolution: "object-inspect@npm:1.13.4" @@ -5669,18 +5129,6 @@ __metadata: languageName: node linkType: hard -"optimism@npm:^0.18.0": - version: 0.18.1 - resolution: "optimism@npm:0.18.1" - dependencies: - "@wry/caches": "npm:^1.0.0" - "@wry/context": "npm:^0.7.0" - "@wry/trie": "npm:^0.5.0" - tslib: "npm:^2.3.0" - checksum: 10/d805f5995d61a417d4fd49a923749db1aa310d1ae8de084ec3a5f589f8b185d9a41b7b4422d33ee75ce43115c264e14bca086f8be2bb182c76448ad08997213a - languageName: node - linkType: hard - "optionator@npm:^0.9.3": version: 0.9.4 resolution: "optionator@npm:0.9.4" @@ -6012,17 +5460,6 @@ __metadata: languageName: node linkType: hard -"prop-types@npm:^15.7.2": - version: 15.8.1 - resolution: "prop-types@npm:15.8.1" - dependencies: - loose-envify: "npm:^1.4.0" - object-assign: "npm:^4.1.1" - react-is: "npm:^16.13.1" - checksum: 10/7d959caec002bc964c86cdc461ec93108b27337dabe6192fb97d69e16a0c799a03462713868b40749bfc1caf5f57ef80ac3e4ffad3effa636ee667582a75e2c0 - languageName: node - linkType: hard - "proper-lockfile@npm:^4.1.2": version: 4.1.2 resolution: "proper-lockfile@npm:4.1.2" @@ -6081,13 +5518,6 @@ __metadata: languageName: node linkType: hard -"react-is@npm:^16.13.1, react-is@npm:^16.7.0": - version: 16.13.1 - resolution: "react-is@npm:16.13.1" - checksum: 10/5aa564a1cde7d391ac980bedee21202fc90bdea3b399952117f54fb71a932af1e5902020144fb354b4690b2414a0c7aafe798eb617b76a3d441d956db7726fdf - languageName: node - linkType: hard - "react-is@npm:^18.0.0": version: 18.3.1 resolution: "react-is@npm:18.3.1" @@ -6180,21 +5610,6 @@ __metadata: languageName: node linkType: hard -"rehackt@npm:^0.1.0": - version: 0.1.0 - resolution: "rehackt@npm:0.1.0" - peerDependencies: - "@types/react": "*" - react: "*" - peerDependenciesMeta: - "@types/react": - optional: true - react: - optional: true - checksum: 10/c81adead82c165dffc574cbf9e1de3605522782a56b48df48b68d53d45c4d8c9253df3790109335bf97072424e54ad2423bb9544ca3a985fa91995dda43452fc - languageName: node - linkType: hard - "require-directory@npm:^2.1.1": version: 2.1.1 resolution: "require-directory@npm:2.1.1" @@ -6306,19 +5721,8 @@ __metadata: resolution: "root-workspace-0b6124@workspace:." dependencies: "@midnight-ntwrk/compact-runtime": "npm:^0.7.0" - "@midnight-ntwrk/dapp-connector-api": "npm:^1.2.2" "@midnight-ntwrk/ledger": "npm:^3.0.6" - "@midnight-ntwrk/midnight-js-contracts": "npm:^1.0.0" - "@midnight-ntwrk/midnight-js-fetch-zk-config-provider": "npm:^1.0.0" - "@midnight-ntwrk/midnight-js-http-client-proof-provider": "npm:^1.0.0" - "@midnight-ntwrk/midnight-js-indexer-public-data-provider": "npm:^1.0.0" - "@midnight-ntwrk/midnight-js-level-private-state-provider": "npm:^1.0.0" - "@midnight-ntwrk/midnight-js-network-id": "npm:^1.0.0" - "@midnight-ntwrk/midnight-js-node-zk-config-provider": "npm:^1.0.0" - "@midnight-ntwrk/midnight-js-types": "npm:^1.0.0" - "@midnight-ntwrk/midnight-js-utils": "npm:^1.0.0" - "@midnight-ntwrk/wallet": "npm:^4.0.0" - "@midnight-ntwrk/wallet-api": "npm:^4.0.0" + "@midnight-ntwrk/zswap": "npm:^3.0.6" "@types/jest": "npm:^29.5.6" "@types/node": "npm:^18.18.6" "@typescript-eslint/eslint-plugin": "npm:^6.8.0" @@ -6358,7 +5762,7 @@ __metadata: languageName: node linkType: hard -"rxjs@npm:^7.5, rxjs@npm:^7.5.0, rxjs@npm:^7.8.1": +"rxjs@npm:^7.8.1": version: 7.8.2 resolution: "rxjs@npm:7.8.2" dependencies: @@ -6429,13 +5833,6 @@ __metadata: languageName: node linkType: hard -"scale-ts@npm:^1.1.0": - version: 1.6.1 - resolution: "scale-ts@npm:1.6.1" - checksum: 10/f1f9bf1d9abfcfcaf8ae2ae326270beca5c2456cc72f6b6b8230aa175a30bdcd6387678746a4d873c834efbba9c8e015698d42ee67bd71b70f7adfe2e0ba1d39 - languageName: node - linkType: hard - "secure-json-parse@npm:^2.4.0": version: 2.7.0 resolution: "secure-json-parse@npm:2.7.0" @@ -6865,15 +6262,6 @@ __metadata: languageName: node linkType: hard -"superjson@npm:^2.0.0": - version: 2.2.2 - resolution: "superjson@npm:2.2.2" - dependencies: - copy-anything: "npm:^3.0.2" - checksum: 10/6fdc709db4f69d586a18379948e0ade8268c851c791701fea960e29cea12672d7561b4ca89c4049c2e787eb1cec08a51df51d357aa6852078bc0d71d7e17b401 - languageName: node - linkType: hard - "supports-color@npm:^7.1.0": version: 7.2.0 resolution: "supports-color@npm:7.2.0" @@ -6899,13 +6287,6 @@ __metadata: languageName: node linkType: hard -"symbol-observable@npm:^4.0.0": - version: 4.0.0 - resolution: "symbol-observable@npm:4.0.0" - checksum: 10/983aef3912ad080fc834b9ad115d44bc2994074c57cea4fb008e9f7ab9bb4118b908c63d9edc861f51257bc0595025510bdf7263bb09d8953a6929f240165c24 - languageName: node - linkType: hard - "synckit@npm:^0.9.1": version: 0.9.2 resolution: "synckit@npm:0.9.2" @@ -7065,13 +6446,6 @@ __metadata: languageName: node linkType: hard -"tr46@npm:~0.0.3": - version: 0.0.3 - resolution: "tr46@npm:0.0.3" - checksum: 10/8f1f5aa6cb232f9e1bdc86f485f916b7aa38caee8a778b378ffec0b70d9307873f253f5cbadbe2955ece2ac5c83d0dc14a77513166ccd0a0c7fe197e21396695 - languageName: node - linkType: hard - "ts-api-utils@npm:^1.0.1": version: 1.4.3 resolution: "ts-api-utils@npm:1.4.3" @@ -7081,22 +6455,6 @@ __metadata: languageName: node linkType: hard -"ts-custom-error@npm:^3.3.1": - version: 3.3.1 - resolution: "ts-custom-error@npm:3.3.1" - checksum: 10/92e3a2c426bf6049579aeb889b6f9787e0cfb6bb715a1457e2571708be7fe739662ca9eb2a8c61b72a2d32189645f4fbcf1a370087e030d922e9e2a7b7c1c994 - languageName: node - linkType: hard - -"ts-invariant@npm:^0.10.3": - version: 0.10.3 - resolution: "ts-invariant@npm:0.10.3" - dependencies: - tslib: "npm:^2.1.0" - checksum: 10/bb07d56fe4aae69d8860e0301dfdee2d375281159054bc24bf1e49e513fb0835bf7f70a11351344d213a79199c5e695f37ebbf5a447188a377ce0cd81d91ddb5 - languageName: node - linkType: hard - "ts-jest@npm:^29.1.1": version: 29.2.6 resolution: "ts-jest@npm:29.2.6" @@ -7184,7 +6542,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.1.0, tslib@npm:^2.3.0, tslib@npm:^2.6.2": +"tslib@npm:^2.1.0, tslib@npm:^2.6.2": version: 2.8.1 resolution: "tslib@npm:2.8.1" checksum: 10/3e2e043d5c2316461cb54e5c7fe02c30ef6dccb3384717ca22ae5c6b5bc95232a6241df19c622d9c73b809bea33b187f6dbc73030963e29950c2141bc32a79f7 @@ -7498,30 +6856,6 @@ __metadata: languageName: node linkType: hard -"web-streams-polyfill@npm:^3.0.3": - version: 3.3.3 - resolution: "web-streams-polyfill@npm:3.3.3" - checksum: 10/8e7e13501b3834094a50abe7c0b6456155a55d7571312b89570012ef47ec2a46d766934768c50aabad10a9c30dd764a407623e8bfcc74fcb58495c29130edea9 - languageName: node - linkType: hard - -"webidl-conversions@npm:^3.0.0": - version: 3.0.1 - resolution: "webidl-conversions@npm:3.0.1" - checksum: 10/b65b9f8d6854572a84a5c69615152b63371395f0c5dcd6729c45789052296df54314db2bc3e977df41705eacb8bc79c247cee139a63fa695192f95816ed528ad - languageName: node - linkType: hard - -"whatwg-url@npm:^5.0.0": - version: 5.0.0 - resolution: "whatwg-url@npm:5.0.0" - dependencies: - tr46: "npm:~0.0.3" - webidl-conversions: "npm:^3.0.0" - checksum: 10/f95adbc1e80820828b45cc671d97da7cd5e4ef9deb426c31bcd5ab00dc7103042291613b3ef3caec0a2335ed09e0d5ed026c940755dbb6d404e2b27f940fdf07 - languageName: node - linkType: hard - "which-boxed-primitive@npm:^1.1.0, which-boxed-primitive@npm:^1.1.1": version: 1.1.1 resolution: "which-boxed-primitive@npm:1.1.1" @@ -7650,21 +6984,6 @@ __metadata: languageName: node linkType: hard -"ws@npm:^8.14.2, ws@npm:^8.8.1": - version: 8.18.1 - resolution: "ws@npm:8.18.1" - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ">=5.0.2" - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - checksum: 10/3f38e9594f2af5b6324138e86b74df7d77bbb8e310bf8188679dd80bac0d1f47e51536a1923ac3365f31f3d8b25ea0b03e4ade466aa8292a86cd5defca64b19b - languageName: node - linkType: hard - "xml@npm:^1.0.1": version: 1.0.1 resolution: "xml@npm:1.0.1" @@ -7745,22 +7064,6 @@ __metadata: languageName: node linkType: hard -"zen-observable-ts@npm:^1.1.0, zen-observable-ts@npm:^1.2.5": - version: 1.2.5 - resolution: "zen-observable-ts@npm:1.2.5" - dependencies: - zen-observable: "npm:0.8.15" - checksum: 10/2384cf92a60e39e7b9735a0696f119684fee0f8bcc81d71474c92d656eca1bc3e87b484a04e97546e56bd539f8756bf97cf21a28a933ff7a94b35a8d217848eb - languageName: node - linkType: hard - -"zen-observable@npm:0.8.15": - version: 0.8.15 - resolution: "zen-observable@npm:0.8.15" - checksum: 10/30eac3f4055d33f446b4cd075d3543da347c2c8e68fbc35c3f5a19fb43be67c6ed27ee136bc8f8933efa547be7ce04957809ad00ee7f1b00a964f199ae6fb514 - languageName: node - linkType: hard - "zip-stream@npm:^6.0.1": version: 6.0.1 resolution: "zip-stream@npm:6.0.1" From 3419a25f2106b3904aa0a269e7b2a2cdfd2c4e4e Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 16 Apr 2025 17:56:00 -0500 Subject: [PATCH 003/322] remove eslint and prettier, add biome --- .dockerignore | 21 - biome.json | 38 + compact/src/run-compactc.cjs | 2 +- contracts/erc20/.eslintignore | 1 - contracts/erc20/.eslintrc.cjs | 30 - contracts/erc20/package.json | 1 - contracts/initializable/.eslintignore | 1 - contracts/initializable/.eslintrc.cjs | 30 - contracts/utils/.eslintignore | 1 - contracts/utils/.eslintrc.cjs | 30 - contracts/utils/package.json | 1 - package.json | 10 +- prettierrc.json | 6 - yarn.lock | 1674 ++----------------------- 14 files changed, 148 insertions(+), 1698 deletions(-) delete mode 100644 .dockerignore create mode 100644 biome.json delete mode 100644 contracts/erc20/.eslintignore delete mode 100644 contracts/erc20/.eslintrc.cjs delete mode 100644 contracts/initializable/.eslintignore delete mode 100644 contracts/initializable/.eslintrc.cjs delete mode 100644 contracts/utils/.eslintignore delete mode 100644 contracts/utils/.eslintrc.cjs delete mode 100644 prettierrc.json diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 8ebba54f..00000000 --- a/.dockerignore +++ /dev/null @@ -1,21 +0,0 @@ -*.tsbuildinfo -result/ -.direnv/ -.idea/ -.env -*.log -examples/ -coverage/ -.github/ -.vscode/ -.yarn -flake.nix -flake.lock -renovate.json -*.Dockerfile -COMMIT_EDITMSG -FETCH_HEAD -ORIG_HEAD -NOTICE -LICENSE -HEAD diff --git a/biome.json b/biome.json new file mode 100644 index 00000000..a06c8fa6 --- /dev/null +++ b/biome.json @@ -0,0 +1,38 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "vcs": { + "enabled": false, + "clientKind": "git", + "useIgnoreFile": false + }, + "files": { + "ignoreUnknown": false, + "ignore": [ + "tsconfig.base.json", + "tsconfig*.json", + "*.compact", + "artifacts/*", + "coverage/*", + "dist/*", + "reports/*" + ] + }, + "formatter": { + "enabled": true, + "indentStyle": "tab" + }, + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "javascript": { + "formatter": { + "quoteStyle": "single" + } + } +} diff --git a/compact/src/run-compactc.cjs b/compact/src/run-compactc.cjs index 69ca0eb4..89a5929a 100755 --- a/compact/src/run-compactc.cjs +++ b/compact/src/run-compactc.cjs @@ -1,6 +1,6 @@ #!/usr/bin/env node -const childProcess = require('child_process'); +const childProcess = require('node:child_process'); const path = require('path'); const [_node, _script, ...args] = process.argv; diff --git a/contracts/erc20/.eslintignore b/contracts/erc20/.eslintignore deleted file mode 100644 index 327555cd..00000000 --- a/contracts/erc20/.eslintignore +++ /dev/null @@ -1 +0,0 @@ -src/artifacts diff --git a/contracts/erc20/.eslintrc.cjs b/contracts/erc20/.eslintrc.cjs deleted file mode 100644 index 581f1d49..00000000 --- a/contracts/erc20/.eslintrc.cjs +++ /dev/null @@ -1,30 +0,0 @@ -module.exports = { - env: { - browser: true, - es2021: true, - node: true, - jest: true, - }, - extends: [ - 'standard-with-typescript', - 'plugin:prettier/recommended', - 'plugin:@typescript-eslint/recommended-requiring-type-checking', - ], - overrides: [], - parserOptions: { - ecmaVersion: 'latest', - sourceType: 'module', - project: ['tsconfig.json'], - }, - rules: { - '@typescript-eslint/no-misused-promises': 'off', // https://github.com/typescript-eslint/typescript-eslint/issues/5807 - '@typescript-eslint/no-floating-promises': 'warn', - '@typescript-eslint/promise-function-async': 'off', - '@typescript-eslint/no-redeclare': 'off', - '@typescript-eslint/no-invalid-void-type': 'off', - '@typescript-eslint/no-unsafe-call': 'off', - '@typescript-eslint/no-unsafe-member-access': 'off', - '@typescript-eslint/explicit-function-return-type': 'off', - '@typescript-eslint/consistent-type-definitions': 'off' - }, -}; diff --git a/contracts/erc20/package.json b/contracts/erc20/package.json index d914daf0..77d0921d 100644 --- a/contracts/erc20/package.json +++ b/contracts/erc20/package.json @@ -25,7 +25,6 @@ }, "devDependencies": { "@midnight-ntwrk/compact": "workspace:*", - "eslint": "^8.52.0", "jest": "^29.7.0", "typescript": "^5.2.2" } diff --git a/contracts/initializable/.eslintignore b/contracts/initializable/.eslintignore deleted file mode 100644 index 327555cd..00000000 --- a/contracts/initializable/.eslintignore +++ /dev/null @@ -1 +0,0 @@ -src/artifacts diff --git a/contracts/initializable/.eslintrc.cjs b/contracts/initializable/.eslintrc.cjs deleted file mode 100644 index 581f1d49..00000000 --- a/contracts/initializable/.eslintrc.cjs +++ /dev/null @@ -1,30 +0,0 @@ -module.exports = { - env: { - browser: true, - es2021: true, - node: true, - jest: true, - }, - extends: [ - 'standard-with-typescript', - 'plugin:prettier/recommended', - 'plugin:@typescript-eslint/recommended-requiring-type-checking', - ], - overrides: [], - parserOptions: { - ecmaVersion: 'latest', - sourceType: 'module', - project: ['tsconfig.json'], - }, - rules: { - '@typescript-eslint/no-misused-promises': 'off', // https://github.com/typescript-eslint/typescript-eslint/issues/5807 - '@typescript-eslint/no-floating-promises': 'warn', - '@typescript-eslint/promise-function-async': 'off', - '@typescript-eslint/no-redeclare': 'off', - '@typescript-eslint/no-invalid-void-type': 'off', - '@typescript-eslint/no-unsafe-call': 'off', - '@typescript-eslint/no-unsafe-member-access': 'off', - '@typescript-eslint/explicit-function-return-type': 'off', - '@typescript-eslint/consistent-type-definitions': 'off' - }, -}; diff --git a/contracts/utils/.eslintignore b/contracts/utils/.eslintignore deleted file mode 100644 index 327555cd..00000000 --- a/contracts/utils/.eslintignore +++ /dev/null @@ -1 +0,0 @@ -src/artifacts diff --git a/contracts/utils/.eslintrc.cjs b/contracts/utils/.eslintrc.cjs deleted file mode 100644 index 581f1d49..00000000 --- a/contracts/utils/.eslintrc.cjs +++ /dev/null @@ -1,30 +0,0 @@ -module.exports = { - env: { - browser: true, - es2021: true, - node: true, - jest: true, - }, - extends: [ - 'standard-with-typescript', - 'plugin:prettier/recommended', - 'plugin:@typescript-eslint/recommended-requiring-type-checking', - ], - overrides: [], - parserOptions: { - ecmaVersion: 'latest', - sourceType: 'module', - project: ['tsconfig.json'], - }, - rules: { - '@typescript-eslint/no-misused-promises': 'off', // https://github.com/typescript-eslint/typescript-eslint/issues/5807 - '@typescript-eslint/no-floating-promises': 'warn', - '@typescript-eslint/promise-function-async': 'off', - '@typescript-eslint/no-redeclare': 'off', - '@typescript-eslint/no-invalid-void-type': 'off', - '@typescript-eslint/no-unsafe-call': 'off', - '@typescript-eslint/no-unsafe-member-access': 'off', - '@typescript-eslint/explicit-function-return-type': 'off', - '@typescript-eslint/consistent-type-definitions': 'off' - }, -}; diff --git a/contracts/utils/package.json b/contracts/utils/package.json index 40c57157..fc0fe810 100644 --- a/contracts/utils/package.json +++ b/contracts/utils/package.json @@ -22,7 +22,6 @@ }, "devDependencies": { "@midnight-ntwrk/compact": "workspace:*", - "eslint": "^8.52.0", "jest": "^29.7.0", "typescript": "^5.2.2" } diff --git a/package.json b/package.json index aef03893..128470a1 100644 --- a/package.json +++ b/package.json @@ -20,25 +20,17 @@ "rxjs": "^7.8.1" }, "devDependencies": { + "@biomejs/biome": "1.9.4", "@midnight-ntwrk/ledger": "^3.0.6", "@midnight-ntwrk/zswap": "^3.0.6", "@types/jest": "^29.5.6", "@types/node": "^18.18.6", - "@typescript-eslint/eslint-plugin": "^6.8.0", - "@typescript-eslint/parser": "^6.8.0", - "eslint": "^8.52.0", - "eslint-config-prettier": "^9.0.0", - "eslint-plugin-import": "^2.28.1", - "eslint-plugin-n": "^16.2.0", - "eslint-plugin-prettier": "^5.0.1", - "eslint-plugin-promise": "^6.1.1", "fast-check": "^3.15.0", "jest": "^29.7.0", "jest-fast-check": "^2.0.0", "jest-gh-md-reporter": "^0.0.2", "jest-html-reporters": "^3.1.4", "jest-junit": "^16.0.0", - "prettier": "^3.0.3", "testcontainers": "^10.3.2", "ts-jest": "^29.1.1", "ts-node": "^10.9.1", diff --git a/prettierrc.json b/prettierrc.json deleted file mode 100644 index 4eb15403..00000000 --- a/prettierrc.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "semi": true, - "trailingComma": "all", - "singleQuote": true, - "printWidth": 130 - } diff --git a/yarn.lock b/yarn.lock index 64965d1e..4d7515ad 100644 --- a/yarn.lock +++ b/yarn.lock @@ -391,6 +391,97 @@ __metadata: languageName: node linkType: hard +"@biomejs/biome@npm:1.9.4": + version: 1.9.4 + resolution: "@biomejs/biome@npm:1.9.4" + dependencies: + "@biomejs/cli-darwin-arm64": "npm:1.9.4" + "@biomejs/cli-darwin-x64": "npm:1.9.4" + "@biomejs/cli-linux-arm64": "npm:1.9.4" + "@biomejs/cli-linux-arm64-musl": "npm:1.9.4" + "@biomejs/cli-linux-x64": "npm:1.9.4" + "@biomejs/cli-linux-x64-musl": "npm:1.9.4" + "@biomejs/cli-win32-arm64": "npm:1.9.4" + "@biomejs/cli-win32-x64": "npm:1.9.4" + dependenciesMeta: + "@biomejs/cli-darwin-arm64": + optional: true + "@biomejs/cli-darwin-x64": + optional: true + "@biomejs/cli-linux-arm64": + optional: true + "@biomejs/cli-linux-arm64-musl": + optional: true + "@biomejs/cli-linux-x64": + optional: true + "@biomejs/cli-linux-x64-musl": + optional: true + "@biomejs/cli-win32-arm64": + optional: true + "@biomejs/cli-win32-x64": + optional: true + bin: + biome: bin/biome + checksum: 10/bd8ff8fb4dc0581bd60a9b9ac28d0cd03ba17c6a1de2ab6228b7fda582079594ceee774f47e41aac2fc6d35de1637def2e32ef2e58fa24e22d1b24ef9ee5cefa + languageName: node + linkType: hard + +"@biomejs/cli-darwin-arm64@npm:1.9.4": + version: 1.9.4 + resolution: "@biomejs/cli-darwin-arm64@npm:1.9.4" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@biomejs/cli-darwin-x64@npm:1.9.4": + version: 1.9.4 + resolution: "@biomejs/cli-darwin-x64@npm:1.9.4" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@biomejs/cli-linux-arm64-musl@npm:1.9.4": + version: 1.9.4 + resolution: "@biomejs/cli-linux-arm64-musl@npm:1.9.4" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@biomejs/cli-linux-arm64@npm:1.9.4": + version: 1.9.4 + resolution: "@biomejs/cli-linux-arm64@npm:1.9.4" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@biomejs/cli-linux-x64-musl@npm:1.9.4": + version: 1.9.4 + resolution: "@biomejs/cli-linux-x64-musl@npm:1.9.4" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@biomejs/cli-linux-x64@npm:1.9.4": + version: 1.9.4 + resolution: "@biomejs/cli-linux-x64@npm:1.9.4" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@biomejs/cli-win32-arm64@npm:1.9.4": + version: 1.9.4 + resolution: "@biomejs/cli-win32-arm64@npm:1.9.4" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@biomejs/cli-win32-x64@npm:1.9.4": + version: 1.9.4 + resolution: "@biomejs/cli-win32-x64@npm:1.9.4" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@cspotcode/source-map-support@npm:^0.8.0": version: 0.8.1 resolution: "@cspotcode/source-map-support@npm:0.8.1" @@ -400,7 +491,7 @@ __metadata: languageName: node linkType: hard -"@eslint-community/eslint-utils@npm:^4.1.2, @eslint-community/eslint-utils@npm:^4.2.0, @eslint-community/eslint-utils@npm:^4.4.0": +"@eslint-community/eslint-utils@npm:^4.2.0": version: 4.4.1 resolution: "@eslint-community/eslint-utils@npm:4.4.1" dependencies: @@ -411,7 +502,7 @@ __metadata: languageName: node linkType: hard -"@eslint-community/regexpp@npm:^4.11.0, @eslint-community/regexpp@npm:^4.5.1, @eslint-community/regexpp@npm:^4.6.1": +"@eslint-community/regexpp@npm:^4.6.1": version: 4.12.1 resolution: "@eslint-community/regexpp@npm:4.12.1" checksum: 10/c08f1dd7dd18fbb60bdd0d85820656d1374dd898af9be7f82cb00451313402a22d5e30569c150315b4385907cdbca78c22389b2a72ab78883b3173be317620cc @@ -853,14 +944,14 @@ __metadata: languageName: node linkType: hard -"@nodelib/fs.stat@npm:2.0.5, @nodelib/fs.stat@npm:^2.0.2": +"@nodelib/fs.stat@npm:2.0.5": version: 2.0.5 resolution: "@nodelib/fs.stat@npm:2.0.5" checksum: 10/012480b5ca9d97bff9261571dbbec7bbc6033f69cc92908bc1ecfad0792361a5a1994bc48674b9ef76419d056a03efadfce5a6cf6dbc0a36559571a7a483f6f0 languageName: node linkType: hard -"@nodelib/fs.walk@npm:^1.2.3, @nodelib/fs.walk@npm:^1.2.8": +"@nodelib/fs.walk@npm:^1.2.8": version: 1.2.8 resolution: "@nodelib/fs.walk@npm:1.2.8" dependencies: @@ -898,7 +989,6 @@ __metadata: dependencies: "@midnight-ntwrk/compact": "workspace:*" "@openzeppelin-midnight-contracts/utils-contract": "workspace:^" - eslint: "npm:^8.52.0" jest: "npm:^29.7.0" typescript: "npm:^5.2.2" languageName: unknown @@ -920,7 +1010,6 @@ __metadata: resolution: "@openzeppelin-midnight-contracts/utils-contract@workspace:contracts/utils" dependencies: "@midnight-ntwrk/compact": "workspace:*" - eslint: "npm:^8.52.0" jest: "npm:^29.7.0" typescript: "npm:^5.2.2" languageName: unknown @@ -933,20 +1022,6 @@ __metadata: languageName: node linkType: hard -"@pkgr/core@npm:^0.1.0": - version: 0.1.1 - resolution: "@pkgr/core@npm:0.1.1" - checksum: 10/6f25fd2e3008f259c77207ac9915b02f1628420403b2630c92a07ff963129238c9262afc9e84344c7a23b5cc1f3965e2cd17e3798219f5fd78a63d144d3cceba - languageName: node - linkType: hard - -"@rtsao/scc@npm:^1.1.0": - version: 1.1.0 - resolution: "@rtsao/scc@npm:1.1.0" - checksum: 10/17d04adf404e04c1e61391ed97bca5117d4c2767a76ae3e879390d6dec7b317fcae68afbf9e98badee075d0b64fa60f287729c4942021b4d19cd01db77385c01 - languageName: node - linkType: hard - "@sinclair/typebox@npm:^0.27.8": version: 0.27.8 resolution: "@sinclair/typebox@npm:0.27.8" @@ -1106,20 +1181,6 @@ __metadata: languageName: node linkType: hard -"@types/json-schema@npm:^7.0.12": - version: 7.0.15 - resolution: "@types/json-schema@npm:7.0.15" - checksum: 10/1a3c3e06236e4c4aab89499c428d585527ce50c24fe8259e8b3926d3df4cfbbbcf306cfc73ddfb66cbafc973116efd15967020b0f738f63e09e64c7d260519e7 - languageName: node - linkType: hard - -"@types/json5@npm:^0.0.29": - version: 0.0.29 - resolution: "@types/json5@npm:0.0.29" - checksum: 10/4e5aed58cabb2bbf6f725da13421aa50a49abb6bc17bfab6c31b8774b073fa7b50d557c61f961a09a85f6056151190f8ac95f13f5b48136ba5841f7d4484ec56 - languageName: node - linkType: hard - "@types/node@npm:*": version: 22.13.8 resolution: "@types/node@npm:22.13.8" @@ -1145,13 +1206,6 @@ __metadata: languageName: node linkType: hard -"@types/semver@npm:^7.5.0": - version: 7.5.8 - resolution: "@types/semver@npm:7.5.8" - checksum: 10/3496808818ddb36deabfe4974fd343a78101fa242c4690044ccdc3b95dcf8785b494f5d628f2f47f38a702f8db9c53c67f47d7818f2be1b79f2efb09692e1178 - languageName: node - linkType: hard - "@types/ssh2-streams@npm:*": version: 0.1.12 resolution: "@types/ssh2-streams@npm:0.1.12" @@ -1203,129 +1257,6 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/eslint-plugin@npm:^6.8.0": - version: 6.21.0 - resolution: "@typescript-eslint/eslint-plugin@npm:6.21.0" - dependencies: - "@eslint-community/regexpp": "npm:^4.5.1" - "@typescript-eslint/scope-manager": "npm:6.21.0" - "@typescript-eslint/type-utils": "npm:6.21.0" - "@typescript-eslint/utils": "npm:6.21.0" - "@typescript-eslint/visitor-keys": "npm:6.21.0" - debug: "npm:^4.3.4" - graphemer: "npm:^1.4.0" - ignore: "npm:^5.2.4" - natural-compare: "npm:^1.4.0" - semver: "npm:^7.5.4" - ts-api-utils: "npm:^1.0.1" - peerDependencies: - "@typescript-eslint/parser": ^6.0.0 || ^6.0.0-alpha - eslint: ^7.0.0 || ^8.0.0 - peerDependenciesMeta: - typescript: - optional: true - checksum: 10/a57de0f630789330204cc1531f86cfc68b391cafb1ba67c8992133f1baa2a09d629df66e71260b040de4c9a3ff1252952037093c4128b0d56c4dbb37720b4c1d - languageName: node - linkType: hard - -"@typescript-eslint/parser@npm:^6.8.0": - version: 6.21.0 - resolution: "@typescript-eslint/parser@npm:6.21.0" - dependencies: - "@typescript-eslint/scope-manager": "npm:6.21.0" - "@typescript-eslint/types": "npm:6.21.0" - "@typescript-eslint/typescript-estree": "npm:6.21.0" - "@typescript-eslint/visitor-keys": "npm:6.21.0" - debug: "npm:^4.3.4" - peerDependencies: - eslint: ^7.0.0 || ^8.0.0 - peerDependenciesMeta: - typescript: - optional: true - checksum: 10/4d51cdbc170e72275efc5ef5fce48a81ec431e4edde8374f4d0213d8d370a06823e1a61ae31d502a5f1b0d1f48fc4d29a1b1b5c2dcf809d66d3872ccf6e46ac7 - languageName: node - linkType: hard - -"@typescript-eslint/scope-manager@npm:6.21.0": - version: 6.21.0 - resolution: "@typescript-eslint/scope-manager@npm:6.21.0" - dependencies: - "@typescript-eslint/types": "npm:6.21.0" - "@typescript-eslint/visitor-keys": "npm:6.21.0" - checksum: 10/fe91ac52ca8e09356a71dc1a2f2c326480f3cccfec6b2b6d9154c1a90651ab8ea270b07c67df5678956c3bbf0bbe7113ab68f68f21b20912ea528b1214197395 - languageName: node - linkType: hard - -"@typescript-eslint/type-utils@npm:6.21.0": - version: 6.21.0 - resolution: "@typescript-eslint/type-utils@npm:6.21.0" - dependencies: - "@typescript-eslint/typescript-estree": "npm:6.21.0" - "@typescript-eslint/utils": "npm:6.21.0" - debug: "npm:^4.3.4" - ts-api-utils: "npm:^1.0.1" - peerDependencies: - eslint: ^7.0.0 || ^8.0.0 - peerDependenciesMeta: - typescript: - optional: true - checksum: 10/d03fb3ee1caa71f3ce053505f1866268d7ed79ffb7fed18623f4a1253f5b8f2ffc92636d6fd08fcbaf5bd265a6de77bf192c53105131e4724643dfc910d705fc - languageName: node - linkType: hard - -"@typescript-eslint/types@npm:6.21.0": - version: 6.21.0 - resolution: "@typescript-eslint/types@npm:6.21.0" - checksum: 10/e26da86d6f36ca5b6ef6322619f8ec55aabcd7d43c840c977ae13ae2c964c3091fc92eb33730d8be08927c9de38466c5323e78bfb270a9ff1d3611fe821046c5 - languageName: node - linkType: hard - -"@typescript-eslint/typescript-estree@npm:6.21.0": - version: 6.21.0 - resolution: "@typescript-eslint/typescript-estree@npm:6.21.0" - dependencies: - "@typescript-eslint/types": "npm:6.21.0" - "@typescript-eslint/visitor-keys": "npm:6.21.0" - debug: "npm:^4.3.4" - globby: "npm:^11.1.0" - is-glob: "npm:^4.0.3" - minimatch: "npm:9.0.3" - semver: "npm:^7.5.4" - ts-api-utils: "npm:^1.0.1" - peerDependenciesMeta: - typescript: - optional: true - checksum: 10/b32fa35fca2a229e0f5f06793e5359ff9269f63e9705e858df95d55ca2cd7fdb5b3e75b284095a992c48c5fc46a1431a1a4b6747ede2dd08929dc1cbacc589b8 - languageName: node - linkType: hard - -"@typescript-eslint/utils@npm:6.21.0": - version: 6.21.0 - resolution: "@typescript-eslint/utils@npm:6.21.0" - dependencies: - "@eslint-community/eslint-utils": "npm:^4.4.0" - "@types/json-schema": "npm:^7.0.12" - "@types/semver": "npm:^7.5.0" - "@typescript-eslint/scope-manager": "npm:6.21.0" - "@typescript-eslint/types": "npm:6.21.0" - "@typescript-eslint/typescript-estree": "npm:6.21.0" - semver: "npm:^7.5.4" - peerDependencies: - eslint: ^7.0.0 || ^8.0.0 - checksum: 10/b404a2c55a425a79d054346ae123087d30c7ecf7ed7abcf680c47bf70c1de4fabadc63434f3f460b2fa63df76bc9e4a0b9fa2383bb8a9fcd62733fb5c4e4f3e3 - languageName: node - linkType: hard - -"@typescript-eslint/visitor-keys@npm:6.21.0": - version: 6.21.0 - resolution: "@typescript-eslint/visitor-keys@npm:6.21.0" - dependencies: - "@typescript-eslint/types": "npm:6.21.0" - eslint-visitor-keys: "npm:^3.4.1" - checksum: 10/30422cdc1e2ffad203df40351a031254b272f9c6f2b7e02e9bfa39e3fc2c7b1c6130333b0057412968deda17a3a68a578a78929a8139c6acef44d9d841dc72e1 - languageName: node - linkType: hard - "@ungap/structured-clone@npm:^1.2.0": version: 1.3.0 resolution: "@ungap/structured-clone@npm:1.3.0" @@ -1504,90 +1435,6 @@ __metadata: languageName: node linkType: hard -"array-buffer-byte-length@npm:^1.0.1, array-buffer-byte-length@npm:^1.0.2": - version: 1.0.2 - resolution: "array-buffer-byte-length@npm:1.0.2" - dependencies: - call-bound: "npm:^1.0.3" - is-array-buffer: "npm:^3.0.5" - checksum: 10/0ae3786195c3211b423e5be8dd93357870e6fb66357d81da968c2c39ef43583ef6eece1f9cb1caccdae4806739c65dea832b44b8593414313cd76a89795fca63 - languageName: node - linkType: hard - -"array-includes@npm:^3.1.8": - version: 3.1.8 - resolution: "array-includes@npm:3.1.8" - dependencies: - call-bind: "npm:^1.0.7" - define-properties: "npm:^1.2.1" - es-abstract: "npm:^1.23.2" - es-object-atoms: "npm:^1.0.0" - get-intrinsic: "npm:^1.2.4" - is-string: "npm:^1.0.7" - checksum: 10/290b206c9451f181fb2b1f79a3bf1c0b66bb259791290ffbada760c79b284eef6f5ae2aeb4bcff450ebc9690edd25732c4c73a3c2b340fcc0f4563aed83bf488 - languageName: node - linkType: hard - -"array-union@npm:^2.1.0": - version: 2.1.0 - resolution: "array-union@npm:2.1.0" - checksum: 10/5bee12395cba82da674931df6d0fea23c4aa4660cb3b338ced9f828782a65caa232573e6bf3968f23e0c5eb301764a382cef2f128b170a9dc59de0e36c39f98d - languageName: node - linkType: hard - -"array.prototype.findlastindex@npm:^1.2.5": - version: 1.2.5 - resolution: "array.prototype.findlastindex@npm:1.2.5" - dependencies: - call-bind: "npm:^1.0.7" - define-properties: "npm:^1.2.1" - es-abstract: "npm:^1.23.2" - es-errors: "npm:^1.3.0" - es-object-atoms: "npm:^1.0.0" - es-shim-unscopables: "npm:^1.0.2" - checksum: 10/7c5c821f357cd53ab6cc305de8086430dd8d7a2485db87b13f843e868055e9582b1fd338f02338f67fc3a1603ceaf9610dd2a470b0b506f9d18934780f95b246 - languageName: node - linkType: hard - -"array.prototype.flat@npm:^1.3.2": - version: 1.3.3 - resolution: "array.prototype.flat@npm:1.3.3" - dependencies: - call-bind: "npm:^1.0.8" - define-properties: "npm:^1.2.1" - es-abstract: "npm:^1.23.5" - es-shim-unscopables: "npm:^1.0.2" - checksum: 10/f9b992fa0775d8f7c97abc91eb7f7b2f0ed8430dd9aeb9fdc2967ac4760cdd7fc2ef7ead6528fef40c7261e4d790e117808ce0d3e7e89e91514d4963a531cd01 - languageName: node - linkType: hard - -"array.prototype.flatmap@npm:^1.3.2": - version: 1.3.3 - resolution: "array.prototype.flatmap@npm:1.3.3" - dependencies: - call-bind: "npm:^1.0.8" - define-properties: "npm:^1.2.1" - es-abstract: "npm:^1.23.5" - es-shim-unscopables: "npm:^1.0.2" - checksum: 10/473534573aa4b37b1d80705d0ce642f5933cccf5617c9f3e8a56686e9815ba93d469138e86a1f25d2fe8af999c3d24f54d703ec1fc2db2e6778d46d0f4ac951e - languageName: node - linkType: hard - -"arraybuffer.prototype.slice@npm:^1.0.4": - version: 1.0.4 - resolution: "arraybuffer.prototype.slice@npm:1.0.4" - dependencies: - array-buffer-byte-length: "npm:^1.0.1" - call-bind: "npm:^1.0.8" - define-properties: "npm:^1.2.1" - es-abstract: "npm:^1.23.5" - es-errors: "npm:^1.3.0" - get-intrinsic: "npm:^1.2.6" - is-array-buffer: "npm:^3.0.4" - checksum: 10/4821ebdfe7d699f910c7f09bc9fa996f09b96b80bccb4f5dd4b59deae582f6ad6e505ecef6376f8beac1eda06df2dbc89b70e82835d104d6fcabd33c1aed1ae9 - languageName: node - linkType: hard - "asn1@npm:^0.2.6": version: 0.2.6 resolution: "asn1@npm:0.2.6" @@ -1597,13 +1444,6 @@ __metadata: languageName: node linkType: hard -"async-function@npm:^1.0.0": - version: 1.0.0 - resolution: "async-function@npm:1.0.0" - checksum: 10/1a09379937d846f0ce7614e75071c12826945d4e417db634156bf0e4673c495989302f52186dfa9767a1d9181794554717badd193ca2bbab046ef1da741d8efd - languageName: node - linkType: hard - "async-lock@npm:^1.4.1": version: 1.4.1 resolution: "async-lock@npm:1.4.1" @@ -1625,15 +1465,6 @@ __metadata: languageName: node linkType: hard -"available-typed-arrays@npm:^1.0.7": - version: 1.0.7 - resolution: "available-typed-arrays@npm:1.0.7" - dependencies: - possible-typed-array-names: "npm:^1.0.0" - checksum: 10/6c9da3a66caddd83c875010a1ca8ef11eac02ba15fb592dc9418b2b5e7b77b645fa7729380a92d9835c2f05f2ca1b6251f39b993e0feb3f1517c74fa1af02cab - languageName: node - linkType: hard - "b4a@npm:^1.6.4": version: 1.6.7 resolution: "b4a@npm:1.6.7" @@ -1906,22 +1737,6 @@ __metadata: languageName: node linkType: hard -"builtin-modules@npm:^3.3.0": - version: 3.3.0 - resolution: "builtin-modules@npm:3.3.0" - checksum: 10/62e063ab40c0c1efccbfa9ffa31873e4f9d57408cb396a2649981a0ecbce56aabc93c28feaccbc5658c95aab2703ad1d11980e62ec2e5e72637404e1eb60f39e - languageName: node - linkType: hard - -"builtins@npm:^5.0.1": - version: 5.1.0 - resolution: "builtins@npm:5.1.0" - dependencies: - semver: "npm:^7.0.0" - checksum: 10/60aa9969f69656bf6eab82cd74b23ab805f112ae46a54b912bccc1533875760f2d2ce95e0a7d13144e35ada9f0386f17ed4961908bc9434b5a5e21375b1902b2 - languageName: node - linkType: hard - "byline@npm:^5.0.0": version: 5.0.0 resolution: "byline@npm:5.0.0" @@ -1949,38 +1764,6 @@ __metadata: languageName: node linkType: hard -"call-bind-apply-helpers@npm:^1.0.0, call-bind-apply-helpers@npm:^1.0.1, call-bind-apply-helpers@npm:^1.0.2": - version: 1.0.2 - resolution: "call-bind-apply-helpers@npm:1.0.2" - dependencies: - es-errors: "npm:^1.3.0" - function-bind: "npm:^1.1.2" - checksum: 10/00482c1f6aa7cfb30fb1dbeb13873edf81cfac7c29ed67a5957d60635a56b2a4a480f1016ddbdb3395cc37900d46037fb965043a51c5c789ffeab4fc535d18b5 - languageName: node - linkType: hard - -"call-bind@npm:^1.0.7, call-bind@npm:^1.0.8": - version: 1.0.8 - resolution: "call-bind@npm:1.0.8" - dependencies: - call-bind-apply-helpers: "npm:^1.0.0" - es-define-property: "npm:^1.0.0" - get-intrinsic: "npm:^1.2.4" - set-function-length: "npm:^1.2.2" - checksum: 10/659b03c79bbfccf0cde3a79e7d52570724d7290209823e1ca5088f94b52192dc1836b82a324d0144612f816abb2f1734447438e38d9dafe0b3f82c2a1b9e3bce - languageName: node - linkType: hard - -"call-bound@npm:^1.0.2, call-bound@npm:^1.0.3": - version: 1.0.3 - resolution: "call-bound@npm:1.0.3" - dependencies: - call-bind-apply-helpers: "npm:^1.0.1" - get-intrinsic: "npm:^1.2.6" - checksum: 10/c39a8245f68cdb7c1f5eea7b3b1e3a7a90084ea6efebb78ebc454d698ade2c2bb42ec033abc35f1e596d62496b6100e9f4cdfad1956476c510130e2cda03266d - languageName: node - linkType: hard - "callsites@npm:^3.0.0": version: 3.1.0 resolution: "callsites@npm:3.1.0" @@ -2201,39 +1984,6 @@ __metadata: languageName: node linkType: hard -"data-view-buffer@npm:^1.0.2": - version: 1.0.2 - resolution: "data-view-buffer@npm:1.0.2" - dependencies: - call-bound: "npm:^1.0.3" - es-errors: "npm:^1.3.0" - is-data-view: "npm:^1.0.2" - checksum: 10/c10b155a4e93999d3a215d08c23eea95f865e1f510b2e7748fcae1882b776df1afe8c99f483ace7fc0e5a3193ab08da138abebc9829d12003746c5a338c4d644 - languageName: node - linkType: hard - -"data-view-byte-length@npm:^1.0.2": - version: 1.0.2 - resolution: "data-view-byte-length@npm:1.0.2" - dependencies: - call-bound: "npm:^1.0.3" - es-errors: "npm:^1.3.0" - is-data-view: "npm:^1.0.2" - checksum: 10/2a47055fcf1ab3ec41b00b6f738c6461a841391a643c9ed9befec1117c1765b4d492661d97fb7cc899200c328949dca6ff189d2c6537d96d60e8a02dfe3c95f7 - languageName: node - linkType: hard - -"data-view-byte-offset@npm:^1.0.1": - version: 1.0.1 - resolution: "data-view-byte-offset@npm:1.0.1" - dependencies: - call-bound: "npm:^1.0.2" - es-errors: "npm:^1.3.0" - is-data-view: "npm:^1.0.1" - checksum: 10/fa3bdfa0968bea6711ee50375094b39f561bce3f15f9e558df59de9c25f0bdd4cddc002d9c1d70ac7772ebd36854a7e22d1761e7302a934e6f1c2263bcf44aa2 - languageName: node - linkType: hard - "dateformat@npm:^4.6.3": version: 4.6.3 resolution: "dateformat@npm:4.6.3" @@ -2253,15 +2003,6 @@ __metadata: languageName: node linkType: hard -"debug@npm:^3.2.7": - version: 3.2.7 - resolution: "debug@npm:3.2.7" - dependencies: - ms: "npm:^2.1.1" - checksum: 10/d86fd7be2b85462297ea16f1934dc219335e802f629ca9a69b63ed8ed041dda492389bb2ee039217c02e5b54792b1c51aa96ae954cf28634d363a2360c7a1639 - languageName: node - linkType: hard - "dedent@npm:^1.0.0": version: 1.5.3 resolution: "dedent@npm:1.5.3" @@ -2288,17 +2029,6 @@ __metadata: languageName: node linkType: hard -"define-data-property@npm:^1.0.1, define-data-property@npm:^1.1.4": - version: 1.1.4 - resolution: "define-data-property@npm:1.1.4" - dependencies: - es-define-property: "npm:^1.0.0" - es-errors: "npm:^1.3.0" - gopd: "npm:^1.0.1" - checksum: 10/abdcb2505d80a53524ba871273e5da75e77e52af9e15b3aa65d8aad82b8a3a424dad7aee2cc0b71470ac7acf501e08defac362e8b6a73cdb4309f028061df4ae - languageName: node - linkType: hard - "define-lazy-prop@npm:^2.0.0": version: 2.0.0 resolution: "define-lazy-prop@npm:2.0.0" @@ -2306,17 +2036,6 @@ __metadata: languageName: node linkType: hard -"define-properties@npm:^1.2.1": - version: 1.2.1 - resolution: "define-properties@npm:1.2.1" - dependencies: - define-data-property: "npm:^1.0.1" - has-property-descriptors: "npm:^1.0.0" - object-keys: "npm:^1.1.1" - checksum: 10/b4ccd00597dd46cb2d4a379398f5b19fca84a16f3374e2249201992f36b30f6835949a9429669ee6b41b6e837205a163eadd745e472069e70dfc10f03e5fcc12 - languageName: node - linkType: hard - "detect-newline@npm:^3.0.0": version: 3.1.0 resolution: "detect-newline@npm:3.1.0" @@ -2338,15 +2057,6 @@ __metadata: languageName: node linkType: hard -"dir-glob@npm:^3.0.1": - version: 3.0.1 - resolution: "dir-glob@npm:3.0.1" - dependencies: - path-type: "npm:^4.0.0" - checksum: 10/fa05e18324510d7283f55862f3161c6759a3f2f8dbce491a2fc14c8324c498286c54282c1f0e933cb930da8419b30679389499b919122952a4f8592362ef4615 - languageName: node - linkType: hard - "docker-compose@npm:^0.24.8": version: 0.24.8 resolution: "docker-compose@npm:0.24.8" @@ -2379,15 +2089,6 @@ __metadata: languageName: node linkType: hard -"doctrine@npm:^2.1.0": - version: 2.1.0 - resolution: "doctrine@npm:2.1.0" - dependencies: - esutils: "npm:^2.0.2" - checksum: 10/555684f77e791b17173ea86e2eea45ef26c22219cb64670669c4f4bebd26dbc95cd90ec1f4159e9349a6bb9eb892ce4dde8cd0139e77bedd8bf4518238618474 - languageName: node - linkType: hard - "doctrine@npm:^3.0.0": version: 3.0.0 resolution: "doctrine@npm:3.0.0" @@ -2397,17 +2098,6 @@ __metadata: languageName: node linkType: hard -"dunder-proto@npm:^1.0.0, dunder-proto@npm:^1.0.1": - version: 1.0.1 - resolution: "dunder-proto@npm:1.0.1" - dependencies: - call-bind-apply-helpers: "npm:^1.0.1" - es-errors: "npm:^1.3.0" - gopd: "npm:^1.2.0" - checksum: 10/5add88a3d68d42d6e6130a0cac450b7c2edbe73364bbd2fc334564418569bea97c6943a8fcd70e27130bf32afc236f30982fc4905039b703f23e9e0433c29934 - languageName: node - linkType: hard - "eastasianwidth@npm:^0.2.0": version: 0.2.0 resolution: "eastasianwidth@npm:0.2.0" @@ -2495,120 +2185,6 @@ __metadata: languageName: node linkType: hard -"es-abstract@npm:^1.23.2, es-abstract@npm:^1.23.5, es-abstract@npm:^1.23.9": - version: 1.23.9 - resolution: "es-abstract@npm:1.23.9" - dependencies: - array-buffer-byte-length: "npm:^1.0.2" - arraybuffer.prototype.slice: "npm:^1.0.4" - available-typed-arrays: "npm:^1.0.7" - call-bind: "npm:^1.0.8" - call-bound: "npm:^1.0.3" - data-view-buffer: "npm:^1.0.2" - data-view-byte-length: "npm:^1.0.2" - data-view-byte-offset: "npm:^1.0.1" - es-define-property: "npm:^1.0.1" - es-errors: "npm:^1.3.0" - es-object-atoms: "npm:^1.0.0" - es-set-tostringtag: "npm:^2.1.0" - es-to-primitive: "npm:^1.3.0" - function.prototype.name: "npm:^1.1.8" - get-intrinsic: "npm:^1.2.7" - get-proto: "npm:^1.0.0" - get-symbol-description: "npm:^1.1.0" - globalthis: "npm:^1.0.4" - gopd: "npm:^1.2.0" - has-property-descriptors: "npm:^1.0.2" - has-proto: "npm:^1.2.0" - has-symbols: "npm:^1.1.0" - hasown: "npm:^2.0.2" - internal-slot: "npm:^1.1.0" - is-array-buffer: "npm:^3.0.5" - is-callable: "npm:^1.2.7" - is-data-view: "npm:^1.0.2" - is-regex: "npm:^1.2.1" - is-shared-array-buffer: "npm:^1.0.4" - is-string: "npm:^1.1.1" - is-typed-array: "npm:^1.1.15" - is-weakref: "npm:^1.1.0" - math-intrinsics: "npm:^1.1.0" - object-inspect: "npm:^1.13.3" - object-keys: "npm:^1.1.1" - object.assign: "npm:^4.1.7" - own-keys: "npm:^1.0.1" - regexp.prototype.flags: "npm:^1.5.3" - safe-array-concat: "npm:^1.1.3" - safe-push-apply: "npm:^1.0.0" - safe-regex-test: "npm:^1.1.0" - set-proto: "npm:^1.0.0" - string.prototype.trim: "npm:^1.2.10" - string.prototype.trimend: "npm:^1.0.9" - string.prototype.trimstart: "npm:^1.0.8" - typed-array-buffer: "npm:^1.0.3" - typed-array-byte-length: "npm:^1.0.3" - typed-array-byte-offset: "npm:^1.0.4" - typed-array-length: "npm:^1.0.7" - unbox-primitive: "npm:^1.1.0" - which-typed-array: "npm:^1.1.18" - checksum: 10/31a321966d760d88fc2ed984104841b42f4f24fc322b246002b9be0af162e03803ee41fcc3cf8be89e07a27ba3033168f877dd983703cb81422ffe5322a27582 - languageName: node - linkType: hard - -"es-define-property@npm:^1.0.0, es-define-property@npm:^1.0.1": - version: 1.0.1 - resolution: "es-define-property@npm:1.0.1" - checksum: 10/f8dc9e660d90919f11084db0a893128f3592b781ce967e4fccfb8f3106cb83e400a4032c559184ec52ee1dbd4b01e7776c7cd0b3327b1961b1a4a7008920fe78 - languageName: node - linkType: hard - -"es-errors@npm:^1.3.0": - version: 1.3.0 - resolution: "es-errors@npm:1.3.0" - checksum: 10/96e65d640156f91b707517e8cdc454dd7d47c32833aa3e85d79f24f9eb7ea85f39b63e36216ef0114996581969b59fe609a94e30316b08f5f4df1d44134cf8d5 - languageName: node - linkType: hard - -"es-object-atoms@npm:^1.0.0, es-object-atoms@npm:^1.1.1": - version: 1.1.1 - resolution: "es-object-atoms@npm:1.1.1" - dependencies: - es-errors: "npm:^1.3.0" - checksum: 10/54fe77de288451dae51c37bfbfe3ec86732dc3778f98f3eb3bdb4bf48063b2c0b8f9c93542656986149d08aa5be3204286e2276053d19582b76753f1a2728867 - languageName: node - linkType: hard - -"es-set-tostringtag@npm:^2.1.0": - version: 2.1.0 - resolution: "es-set-tostringtag@npm:2.1.0" - dependencies: - es-errors: "npm:^1.3.0" - get-intrinsic: "npm:^1.2.6" - has-tostringtag: "npm:^1.0.2" - hasown: "npm:^2.0.2" - checksum: 10/86814bf8afbcd8966653f731415888019d4bc4aca6b6c354132a7a75bb87566751e320369654a101d23a91c87a85c79b178bcf40332839bd347aff437c4fb65f - languageName: node - linkType: hard - -"es-shim-unscopables@npm:^1.0.2": - version: 1.1.0 - resolution: "es-shim-unscopables@npm:1.1.0" - dependencies: - hasown: "npm:^2.0.2" - checksum: 10/c351f586c30bbabc62355be49564b2435468b52c3532b8a1663672e3d10dc300197e69c247869dd173e56d86423ab95fc0c10b0939cdae597094e0fdca078cba - languageName: node - linkType: hard - -"es-to-primitive@npm:^1.3.0": - version: 1.3.0 - resolution: "es-to-primitive@npm:1.3.0" - dependencies: - is-callable: "npm:^1.2.7" - is-date-object: "npm:^1.0.5" - is-symbol: "npm:^1.0.4" - checksum: 10/17faf35c221aad59a16286cbf58ef6f080bf3c485dff202c490d074d8e74da07884e29b852c245d894eac84f73c58330ec956dfd6d02c0b449d75eb1012a3f9b - languageName: node - linkType: hard - "escalade@npm:^3.1.1, escalade@npm:^3.2.0": version: 3.2.0 resolution: "escalade@npm:3.2.0" @@ -2630,143 +2206,6 @@ __metadata: languageName: node linkType: hard -"eslint-compat-utils@npm:^0.5.1": - version: 0.5.1 - resolution: "eslint-compat-utils@npm:0.5.1" - dependencies: - semver: "npm:^7.5.4" - peerDependencies: - eslint: ">=6.0.0" - checksum: 10/ac65ac1c6107cf19f63f5fc17cea361c9cb1336be7356f23dbb0fac10979974b4622e13e950be43cbf431801f2c07f7dab448573181ccf6edc0b86d5b5304511 - languageName: node - linkType: hard - -"eslint-config-prettier@npm:^9.0.0": - version: 9.1.0 - resolution: "eslint-config-prettier@npm:9.1.0" - peerDependencies: - eslint: ">=7.0.0" - bin: - eslint-config-prettier: bin/cli.js - checksum: 10/411e3b3b1c7aa04e3e0f20d561271b3b909014956c4dba51c878bf1a23dbb8c800a3be235c46c4732c70827276e540b6eed4636d9b09b444fd0a8e07f0fcd830 - languageName: node - linkType: hard - -"eslint-import-resolver-node@npm:^0.3.9": - version: 0.3.9 - resolution: "eslint-import-resolver-node@npm:0.3.9" - dependencies: - debug: "npm:^3.2.7" - is-core-module: "npm:^2.13.0" - resolve: "npm:^1.22.4" - checksum: 10/d52e08e1d96cf630957272e4f2644dcfb531e49dcfd1edd2e07e43369eb2ec7a7d4423d417beee613201206ff2efa4eb9a582b5825ee28802fc7c71fcd53ca83 - languageName: node - linkType: hard - -"eslint-module-utils@npm:^2.12.0": - version: 2.12.0 - resolution: "eslint-module-utils@npm:2.12.0" - dependencies: - debug: "npm:^3.2.7" - peerDependenciesMeta: - eslint: - optional: true - checksum: 10/dd27791147eca17366afcb83f47d6825b6ce164abb256681e5de4ec1d7e87d8605641eb869298a0dbc70665e2446dbcc2f40d3e1631a9475dd64dd23d4ca5dee - languageName: node - linkType: hard - -"eslint-plugin-es-x@npm:^7.5.0": - version: 7.8.0 - resolution: "eslint-plugin-es-x@npm:7.8.0" - dependencies: - "@eslint-community/eslint-utils": "npm:^4.1.2" - "@eslint-community/regexpp": "npm:^4.11.0" - eslint-compat-utils: "npm:^0.5.1" - peerDependencies: - eslint: ">=8" - checksum: 10/1df8d52c4fadc06854ce801af05b05f2642aa2deb918fb7d37738596eabd70b7f21a22b150b78ec9104bac6a1b6b4fb796adea2364ede91b01d20964849ce5f7 - languageName: node - linkType: hard - -"eslint-plugin-import@npm:^2.28.1": - version: 2.31.0 - resolution: "eslint-plugin-import@npm:2.31.0" - dependencies: - "@rtsao/scc": "npm:^1.1.0" - array-includes: "npm:^3.1.8" - array.prototype.findlastindex: "npm:^1.2.5" - array.prototype.flat: "npm:^1.3.2" - array.prototype.flatmap: "npm:^1.3.2" - debug: "npm:^3.2.7" - doctrine: "npm:^2.1.0" - eslint-import-resolver-node: "npm:^0.3.9" - eslint-module-utils: "npm:^2.12.0" - hasown: "npm:^2.0.2" - is-core-module: "npm:^2.15.1" - is-glob: "npm:^4.0.3" - minimatch: "npm:^3.1.2" - object.fromentries: "npm:^2.0.8" - object.groupby: "npm:^1.0.3" - object.values: "npm:^1.2.0" - semver: "npm:^6.3.1" - string.prototype.trimend: "npm:^1.0.8" - tsconfig-paths: "npm:^3.15.0" - peerDependencies: - eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 - checksum: 10/6b76bd009ac2db0615d9019699d18e2a51a86cb8c1d0855a35fb1b418be23b40239e6debdc6e8c92c59f1468ed0ea8d7b85c817117a113d5cc225be8a02ad31c - languageName: node - linkType: hard - -"eslint-plugin-n@npm:^16.2.0": - version: 16.6.2 - resolution: "eslint-plugin-n@npm:16.6.2" - dependencies: - "@eslint-community/eslint-utils": "npm:^4.4.0" - builtins: "npm:^5.0.1" - eslint-plugin-es-x: "npm:^7.5.0" - get-tsconfig: "npm:^4.7.0" - globals: "npm:^13.24.0" - ignore: "npm:^5.2.4" - is-builtin-module: "npm:^3.2.1" - is-core-module: "npm:^2.12.1" - minimatch: "npm:^3.1.2" - resolve: "npm:^1.22.2" - semver: "npm:^7.5.3" - peerDependencies: - eslint: ">=7.0.0" - checksum: 10/e0f600d03d3a3df57e9a811648b1b534a6d67c90ea9406340ddf3763c2b87cf5ef910b390f787ca5cb27c8d8ff36aad42d70209b54e2a1cb4cc2507ca417229a - languageName: node - linkType: hard - -"eslint-plugin-prettier@npm:^5.0.1": - version: 5.2.3 - resolution: "eslint-plugin-prettier@npm:5.2.3" - dependencies: - prettier-linter-helpers: "npm:^1.0.0" - synckit: "npm:^0.9.1" - peerDependencies: - "@types/eslint": ">=8.0.0" - eslint: ">=8.0.0" - eslint-config-prettier: "*" - prettier: ">=3.0.0" - peerDependenciesMeta: - "@types/eslint": - optional: true - eslint-config-prettier: - optional: true - checksum: 10/6444a0b89f3e2a6b38adce69761133f8539487d797f1655b3fa24f93a398be132c4f68f87041a14740b79202368d5782aa1dffd2bd7a3ea659f263d6796acf15 - languageName: node - linkType: hard - -"eslint-plugin-promise@npm:^6.1.1": - version: 6.6.0 - resolution: "eslint-plugin-promise@npm:6.6.0" - peerDependencies: - eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 - checksum: 10/c2b5604efd7e1390c132fcbf06cb2f072c956ffa65c14a991cb74ba1e2327357797239cb5b9b292d5e4010301bb897bd85a6273d7873fb157edc46aa2d95cbd9 - languageName: node - linkType: hard - "eslint-scope@npm:^7.2.2": version: 7.2.2 resolution: "eslint-scope@npm:7.2.2" @@ -2966,13 +2405,6 @@ __metadata: languageName: node linkType: hard -"fast-diff@npm:^1.1.2": - version: 1.3.0 - resolution: "fast-diff@npm:1.3.0" - checksum: 10/9e57415bc69cd6efcc720b3b8fe9fdaf42dcfc06f86f0f45378b1fa512598a8aac48aa3928c8751d58e2f01bb4ba4f07e4f3d9bc0d57586d45f1bd1e872c6cde - languageName: node - linkType: hard - "fast-fifo@npm:^1.2.0, fast-fifo@npm:^1.3.2": version: 1.3.2 resolution: "fast-fifo@npm:1.3.2" @@ -2980,19 +2412,6 @@ __metadata: languageName: node linkType: hard -"fast-glob@npm:^3.2.9": - version: 3.3.3 - resolution: "fast-glob@npm:3.3.3" - dependencies: - "@nodelib/fs.stat": "npm:^2.0.2" - "@nodelib/fs.walk": "npm:^1.2.3" - glob-parent: "npm:^5.1.2" - merge2: "npm:^1.3.0" - micromatch: "npm:^4.0.8" - checksum: 10/dcc6432b269762dd47381d8b8358bf964d8f4f60286ac6aa41c01ade70bda459ff2001b516690b96d5365f68a49242966112b5d5cc9cd82395fa8f9d017c90ad - languageName: node - linkType: hard - "fast-json-stable-stringify@npm:2.x, fast-json-stable-stringify@npm:^2.0.0, fast-json-stable-stringify@npm:^2.1.0": version: 2.1.0 resolution: "fast-json-stable-stringify@npm:2.1.0" @@ -3104,15 +2523,6 @@ __metadata: languageName: node linkType: hard -"for-each@npm:^0.3.3": - version: 0.3.5 - resolution: "for-each@npm:0.3.5" - dependencies: - is-callable: "npm:^1.2.7" - checksum: 10/330cc2439f85c94f4609de3ee1d32c5693ae15cdd7fe3d112c4fd9efd4ce7143f2c64ef6c2c9e0cfdb0058437f33ef05b5bdae5b98fcc903fb2143fbaf0fea0f - languageName: node - linkType: hard - "foreground-child@npm:^3.1.0": version: 3.3.1 resolution: "foreground-child@npm:3.3.1" @@ -3190,27 +2600,6 @@ __metadata: languageName: node linkType: hard -"function.prototype.name@npm:^1.1.6, function.prototype.name@npm:^1.1.8": - version: 1.1.8 - resolution: "function.prototype.name@npm:1.1.8" - dependencies: - call-bind: "npm:^1.0.8" - call-bound: "npm:^1.0.3" - define-properties: "npm:^1.2.1" - functions-have-names: "npm:^1.2.3" - hasown: "npm:^2.0.2" - is-callable: "npm:^1.2.7" - checksum: 10/25b9e5bea936732a6f0c0c08db58cc0d609ac1ed458c6a07ead46b32e7b9bf3fe5887796c3f83d35994efbc4fdde81c08ac64135b2c399b8f2113968d44082bc - languageName: node - linkType: hard - -"functions-have-names@npm:^1.2.3": - version: 1.2.3 - resolution: "functions-have-names@npm:1.2.3" - checksum: 10/0ddfd3ed1066a55984aaecebf5419fbd9344a5c38dd120ffb0739fac4496758dcf371297440528b115e4367fc46e3abc86a2cc0ff44612181b175ae967a11a05 - languageName: node - linkType: hard - "gensync@npm:^1.0.0-beta.2": version: 1.0.0-beta.2 resolution: "gensync@npm:1.0.0-beta.2" @@ -3225,24 +2614,6 @@ __metadata: languageName: node linkType: hard -"get-intrinsic@npm:^1.2.4, get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.2.6, get-intrinsic@npm:^1.2.7": - version: 1.3.0 - resolution: "get-intrinsic@npm:1.3.0" - dependencies: - call-bind-apply-helpers: "npm:^1.0.2" - es-define-property: "npm:^1.0.1" - es-errors: "npm:^1.3.0" - es-object-atoms: "npm:^1.1.1" - function-bind: "npm:^1.1.2" - get-proto: "npm:^1.0.1" - gopd: "npm:^1.2.0" - has-symbols: "npm:^1.1.0" - hasown: "npm:^2.0.2" - math-intrinsics: "npm:^1.1.0" - checksum: 10/6e9dd920ff054147b6f44cb98104330e87caafae051b6d37b13384a45ba15e71af33c3baeac7cb630a0aaa23142718dcf25b45cfdd86c184c5dcb4e56d953a10 - languageName: node - linkType: hard - "get-package-type@npm:^0.1.0": version: 0.1.0 resolution: "get-package-type@npm:0.1.0" @@ -3257,16 +2628,6 @@ __metadata: languageName: node linkType: hard -"get-proto@npm:^1.0.0, get-proto@npm:^1.0.1": - version: 1.0.1 - resolution: "get-proto@npm:1.0.1" - dependencies: - dunder-proto: "npm:^1.0.1" - es-object-atoms: "npm:^1.0.0" - checksum: 10/4fc96afdb58ced9a67558698b91433e6b037aaa6f1493af77498d7c85b141382cf223c0e5946f334fb328ee85dfe6edd06d218eaf09556f4bc4ec6005d7f5f7b - languageName: node - linkType: hard - "get-stream@npm:^6.0.0": version: 6.0.1 resolution: "get-stream@npm:6.0.1" @@ -3274,35 +2635,6 @@ __metadata: languageName: node linkType: hard -"get-symbol-description@npm:^1.1.0": - version: 1.1.0 - resolution: "get-symbol-description@npm:1.1.0" - dependencies: - call-bound: "npm:^1.0.3" - es-errors: "npm:^1.3.0" - get-intrinsic: "npm:^1.2.6" - checksum: 10/a353e3a9595a74720b40fb5bae3ba4a4f826e186e83814d93375182384265676f59e49998b9cdfac4a2225ce95a3d32a68f502a2c5619303987f1c183ab80494 - languageName: node - linkType: hard - -"get-tsconfig@npm:^4.7.0": - version: 4.10.0 - resolution: "get-tsconfig@npm:4.10.0" - dependencies: - resolve-pkg-maps: "npm:^1.0.0" - checksum: 10/5259b5c99a1957114337d9d0603b4a305ec9e29fa6cac7d2fbf634ba6754a0cc88bfd281a02416ce64e604b637d3cb239185381a79a5842b17fb55c097b38c4b - languageName: node - linkType: hard - -"glob-parent@npm:^5.1.2": - version: 5.1.2 - resolution: "glob-parent@npm:5.1.2" - dependencies: - is-glob: "npm:^4.0.1" - checksum: 10/32cd106ce8c0d83731966d31517adb766d02c3812de49c30cfe0675c7c0ae6630c11214c54a5ae67aca882cf738d27fd7768f21aa19118b9245950554be07247 - languageName: node - linkType: hard - "glob-parent@npm:^6.0.2": version: 6.0.2 resolution: "glob-parent@npm:6.0.2" @@ -3349,7 +2681,7 @@ __metadata: languageName: node linkType: hard -"globals@npm:^13.19.0, globals@npm:^13.24.0": +"globals@npm:^13.19.0": version: 13.24.0 resolution: "globals@npm:13.24.0" dependencies: @@ -3358,37 +2690,6 @@ __metadata: languageName: node linkType: hard -"globalthis@npm:^1.0.4": - version: 1.0.4 - resolution: "globalthis@npm:1.0.4" - dependencies: - define-properties: "npm:^1.2.1" - gopd: "npm:^1.0.1" - checksum: 10/1f1fd078fb2f7296306ef9dd51019491044ccf17a59ed49d375b576ca108ff37e47f3d29aead7add40763574a992f16a5367dd1e2173b8634ef18556ab719ac4 - languageName: node - linkType: hard - -"globby@npm:^11.1.0": - version: 11.1.0 - resolution: "globby@npm:11.1.0" - dependencies: - array-union: "npm:^2.1.0" - dir-glob: "npm:^3.0.1" - fast-glob: "npm:^3.2.9" - ignore: "npm:^5.2.0" - merge2: "npm:^1.4.1" - slash: "npm:^3.0.0" - checksum: 10/288e95e310227bbe037076ea81b7c2598ccbc3122d87abc6dab39e1eec309aa14f0e366a98cdc45237ffcfcbad3db597778c0068217dcb1950fef6249104e1b1 - languageName: node - linkType: hard - -"gopd@npm:^1.0.1, gopd@npm:^1.2.0": - version: 1.2.0 - resolution: "gopd@npm:1.2.0" - checksum: 10/94e296d69f92dc1c0768fcfeecfb3855582ab59a7c75e969d5f96ce50c3d201fd86d5a2857c22565764d5bb8a816c7b1e58f133ec318cd56274da36c5e3fb1a1 - languageName: node - linkType: hard - "graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" @@ -3403,13 +2704,6 @@ __metadata: languageName: node linkType: hard -"has-bigints@npm:^1.0.2": - version: 1.1.0 - resolution: "has-bigints@npm:1.1.0" - checksum: 10/90fb1b24d40d2472bcd1c8bd9dd479037ec240215869bdbff97b2be83acef57d28f7e96bdd003a21bed218d058b49097f4acc8821c05b1629cc5d48dd7bfcccd - languageName: node - linkType: hard - "has-flag@npm:^4.0.0": version: 4.0.0 resolution: "has-flag@npm:4.0.0" @@ -3417,40 +2711,6 @@ __metadata: languageName: node linkType: hard -"has-property-descriptors@npm:^1.0.0, has-property-descriptors@npm:^1.0.2": - version: 1.0.2 - resolution: "has-property-descriptors@npm:1.0.2" - dependencies: - es-define-property: "npm:^1.0.0" - checksum: 10/2d8c9ab8cebb572e3362f7d06139a4592105983d4317e68f7adba320fe6ddfc8874581e0971e899e633fd5f72e262830edce36d5a0bc863dad17ad20572484b2 - languageName: node - linkType: hard - -"has-proto@npm:^1.2.0": - version: 1.2.0 - resolution: "has-proto@npm:1.2.0" - dependencies: - dunder-proto: "npm:^1.0.0" - checksum: 10/7eaed07728eaa28b77fadccabce53f30de467ff186a766872669a833ac2e87d8922b76a22cc58339d7e0277aefe98d6d00762113b27a97cdf65adcf958970935 - languageName: node - linkType: hard - -"has-symbols@npm:^1.0.3, has-symbols@npm:^1.1.0": - version: 1.1.0 - resolution: "has-symbols@npm:1.1.0" - checksum: 10/959385c98696ebbca51e7534e0dc723ada325efa3475350951363cce216d27373e0259b63edb599f72eb94d6cde8577b4b2375f080b303947e560f85692834fa - languageName: node - linkType: hard - -"has-tostringtag@npm:^1.0.2": - version: 1.0.2 - resolution: "has-tostringtag@npm:1.0.2" - dependencies: - has-symbols: "npm:^1.0.3" - checksum: 10/c74c5f5ceee3c8a5b8bc37719840dc3749f5b0306d818974141dda2471a1a2ca6c8e46b9d6ac222c5345df7a901c9b6f350b1e6d62763fec877e26609a401bfe - languageName: node - linkType: hard - "hasown@npm:^2.0.2": version: 2.0.2 resolution: "hasown@npm:2.0.2" @@ -3524,7 +2784,7 @@ __metadata: languageName: node linkType: hard -"ignore@npm:^5.2.0, ignore@npm:^5.2.4": +"ignore@npm:^5.2.0": version: 5.3.2 resolution: "ignore@npm:5.3.2" checksum: 10/cceb6a457000f8f6a50e1196429750d782afce5680dd878aa4221bd79972d68b3a55b4b1458fc682be978f4d3c6a249046aa0880637367216444ab7b014cfc98 @@ -3577,17 +2837,6 @@ __metadata: languageName: node linkType: hard -"internal-slot@npm:^1.1.0": - version: 1.1.0 - resolution: "internal-slot@npm:1.1.0" - dependencies: - es-errors: "npm:^1.3.0" - hasown: "npm:^2.0.2" - side-channel: "npm:^1.1.0" - checksum: 10/1d5219273a3dab61b165eddf358815eefc463207db33c20fcfca54717da02e3f492003757721f972fd0bf21e4b426cab389c5427b99ceea4b8b670dc88ee6d4a - languageName: node - linkType: hard - "io-ts@npm:^2.2.20": version: 2.2.22 resolution: "io-ts@npm:2.2.22" @@ -3607,17 +2856,6 @@ __metadata: languageName: node linkType: hard -"is-array-buffer@npm:^3.0.4, is-array-buffer@npm:^3.0.5": - version: 3.0.5 - resolution: "is-array-buffer@npm:3.0.5" - dependencies: - call-bind: "npm:^1.0.8" - call-bound: "npm:^1.0.3" - get-intrinsic: "npm:^1.2.6" - checksum: 10/ef1095c55b963cd0dcf6f88a113e44a0aeca91e30d767c475e7d746d28d1195b10c5076b94491a7a0cd85020ca6a4923070021d74651d093dc909e9932cf689b - languageName: node - linkType: hard - "is-arrayish@npm:^0.2.1": version: 0.2.1 resolution: "is-arrayish@npm:0.2.1" @@ -3625,55 +2863,7 @@ __metadata: languageName: node linkType: hard -"is-async-function@npm:^2.0.0": - version: 2.1.1 - resolution: "is-async-function@npm:2.1.1" - dependencies: - async-function: "npm:^1.0.0" - call-bound: "npm:^1.0.3" - get-proto: "npm:^1.0.1" - has-tostringtag: "npm:^1.0.2" - safe-regex-test: "npm:^1.1.0" - checksum: 10/7c2ac7efdf671e03265e74a043bcb1c0a32e226bc2a42dfc5ec8644667df668bbe14b91c08e6c1414f392f8cf86cd1d489b3af97756e2c7a49dd1ba63fd40ca6 - languageName: node - linkType: hard - -"is-bigint@npm:^1.1.0": - version: 1.1.0 - resolution: "is-bigint@npm:1.1.0" - dependencies: - has-bigints: "npm:^1.0.2" - checksum: 10/10cf327310d712fe227cfaa32d8b11814c214392b6ac18c827f157e1e85363cf9c8e2a22df526689bd5d25e53b58cc110894787afb54e138e7c504174dba15fd - languageName: node - linkType: hard - -"is-boolean-object@npm:^1.2.1": - version: 1.2.2 - resolution: "is-boolean-object@npm:1.2.2" - dependencies: - call-bound: "npm:^1.0.3" - has-tostringtag: "npm:^1.0.2" - checksum: 10/051fa95fdb99d7fbf653165a7e6b2cba5d2eb62f7ffa81e793a790f3fb5366c91c1b7b6af6820aa2937dd86c73aa3ca9d9ca98f500988457b1c59692c52ba911 - languageName: node - linkType: hard - -"is-builtin-module@npm:^3.2.1": - version: 3.2.1 - resolution: "is-builtin-module@npm:3.2.1" - dependencies: - builtin-modules: "npm:^3.3.0" - checksum: 10/e8f0ffc19a98240bda9c7ada84d846486365af88d14616e737d280d378695c8c448a621dcafc8332dbf0fcd0a17b0763b845400709963fa9151ddffece90ae88 - languageName: node - linkType: hard - -"is-callable@npm:^1.2.7": - version: 1.2.7 - resolution: "is-callable@npm:1.2.7" - checksum: 10/48a9297fb92c99e9df48706241a189da362bff3003354aea4048bd5f7b2eb0d823cd16d0a383cece3d76166ba16d85d9659165ac6fcce1ac12e6c649d66dbdb9 - languageName: node - linkType: hard - -"is-core-module@npm:^2.12.1, is-core-module@npm:^2.13.0, is-core-module@npm:^2.15.1, is-core-module@npm:^2.16.0": +"is-core-module@npm:^2.16.0": version: 2.16.1 resolution: "is-core-module@npm:2.16.1" dependencies: @@ -3682,27 +2872,6 @@ __metadata: languageName: node linkType: hard -"is-data-view@npm:^1.0.1, is-data-view@npm:^1.0.2": - version: 1.0.2 - resolution: "is-data-view@npm:1.0.2" - dependencies: - call-bound: "npm:^1.0.2" - get-intrinsic: "npm:^1.2.6" - is-typed-array: "npm:^1.1.13" - checksum: 10/357e9a48fa38f369fd6c4c3b632a3ab2b8adca14997db2e4b3fe94c4cd0a709af48e0fb61b02c64a90c0dd542fd489d49c2d03157b05ae6c07f5e4dec9e730a8 - languageName: node - linkType: hard - -"is-date-object@npm:^1.0.5, is-date-object@npm:^1.1.0": - version: 1.1.0 - resolution: "is-date-object@npm:1.1.0" - dependencies: - call-bound: "npm:^1.0.2" - has-tostringtag: "npm:^1.0.2" - checksum: 10/3a811b2c3176fb31abee1d23d3dc78b6c65fd9c07d591fcb67553cab9e7f272728c3dd077d2d738b53f9a2103255b0a6e8dfc9568a7805c56a78b2563e8d1dec - languageName: node - linkType: hard - "is-docker@npm:^2.0.0, is-docker@npm:^2.1.1": version: 2.2.1 resolution: "is-docker@npm:2.2.1" @@ -3719,15 +2888,6 @@ __metadata: languageName: node linkType: hard -"is-finalizationregistry@npm:^1.1.0": - version: 1.1.1 - resolution: "is-finalizationregistry@npm:1.1.1" - dependencies: - call-bound: "npm:^1.0.3" - checksum: 10/0bfb145e9a1ba852ddde423b0926d2169ae5fe9e37882cde9e8f69031281a986308df4d982283e152396e88b86562ed2256cbaa5e6390fb840a4c25ab54b8a80 - languageName: node - linkType: hard - "is-fullwidth-code-point@npm:^3.0.0": version: 3.0.0 resolution: "is-fullwidth-code-point@npm:3.0.0" @@ -3742,19 +2902,7 @@ __metadata: languageName: node linkType: hard -"is-generator-function@npm:^1.0.10": - version: 1.1.0 - resolution: "is-generator-function@npm:1.1.0" - dependencies: - call-bound: "npm:^1.0.3" - get-proto: "npm:^1.0.0" - has-tostringtag: "npm:^1.0.2" - safe-regex-test: "npm:^1.1.0" - checksum: 10/5906ff51a856a5fbc6b90a90fce32040b0a6870da905f98818f1350f9acadfc9884f7c3dec833fce04b83dd883937b86a190b6593ede82e8b1af8b6c4ecf7cbd - languageName: node - linkType: hard - -"is-glob@npm:^4.0.0, is-glob@npm:^4.0.1, is-glob@npm:^4.0.3": +"is-glob@npm:^4.0.0, is-glob@npm:^4.0.3": version: 4.0.3 resolution: "is-glob@npm:4.0.3" dependencies: @@ -3763,23 +2911,6 @@ __metadata: languageName: node linkType: hard -"is-map@npm:^2.0.3": - version: 2.0.3 - resolution: "is-map@npm:2.0.3" - checksum: 10/8de7b41715b08bcb0e5edb0fb9384b80d2d5bcd10e142188f33247d19ff078abaf8e9b6f858e2302d8d05376a26a55cd23a3c9f8ab93292b02fcd2cc9e4e92bb - languageName: node - linkType: hard - -"is-number-object@npm:^1.1.1": - version: 1.1.1 - resolution: "is-number-object@npm:1.1.1" - dependencies: - call-bound: "npm:^1.0.3" - has-tostringtag: "npm:^1.0.2" - checksum: 10/a5922fb8779ab1ea3b8a9c144522b3d0bea5d9f8f23f7a72470e61e1e4df47714e28e0154ac011998b709cce260c3c9447ad3cd24a96c2f2a0abfdb2cbdc76c8 - languageName: node - linkType: hard - "is-number@npm:^7.0.0": version: 7.0.0 resolution: "is-number@npm:7.0.0" @@ -3794,34 +2925,6 @@ __metadata: languageName: node linkType: hard -"is-regex@npm:^1.2.1": - version: 1.2.1 - resolution: "is-regex@npm:1.2.1" - dependencies: - call-bound: "npm:^1.0.2" - gopd: "npm:^1.2.0" - has-tostringtag: "npm:^1.0.2" - hasown: "npm:^2.0.2" - checksum: 10/c42b7efc5868a5c9a4d8e6d3e9816e8815c611b09535c00fead18a1138455c5cb5e1887f0023a467ad3f9c419d62ba4dc3d9ba8bafe55053914d6d6454a945d2 - languageName: node - linkType: hard - -"is-set@npm:^2.0.3": - version: 2.0.3 - resolution: "is-set@npm:2.0.3" - checksum: 10/5685df33f0a4a6098a98c72d94d67cad81b2bc72f1fb2091f3d9283c4a1c582123cd709145b02a9745f0ce6b41e3e43f1c944496d1d74d4ea43358be61308669 - languageName: node - linkType: hard - -"is-shared-array-buffer@npm:^1.0.4": - version: 1.0.4 - resolution: "is-shared-array-buffer@npm:1.0.4" - dependencies: - call-bound: "npm:^1.0.3" - checksum: 10/0380d7c60cc692856871526ffcd38a8133818a2ee42d47bb8008248a0cd2121d8c8b5f66b6da3cac24bc5784553cacb6faaf678f66bc88c6615b42af2825230e - languageName: node - linkType: hard - "is-stream@npm:^2.0.0, is-stream@npm:^2.0.1": version: 2.0.1 resolution: "is-stream@npm:2.0.1" @@ -3829,62 +2932,6 @@ __metadata: languageName: node linkType: hard -"is-string@npm:^1.0.7, is-string@npm:^1.1.1": - version: 1.1.1 - resolution: "is-string@npm:1.1.1" - dependencies: - call-bound: "npm:^1.0.3" - has-tostringtag: "npm:^1.0.2" - checksum: 10/5277cb9e225a7cc8a368a72623b44a99f2cfa139659c6b203553540681ad4276bfc078420767aad0e73eef5f0bd07d4abf39a35d37ec216917879d11cebc1f8b - languageName: node - linkType: hard - -"is-symbol@npm:^1.0.4, is-symbol@npm:^1.1.1": - version: 1.1.1 - resolution: "is-symbol@npm:1.1.1" - dependencies: - call-bound: "npm:^1.0.2" - has-symbols: "npm:^1.1.0" - safe-regex-test: "npm:^1.1.0" - checksum: 10/db495c0d8cd0a7a66b4f4ef7fccee3ab5bd954cb63396e8ac4d32efe0e9b12fdfceb851d6c501216a71f4f21e5ff20fc2ee845a3d52d455e021c466ac5eb2db2 - languageName: node - linkType: hard - -"is-typed-array@npm:^1.1.13, is-typed-array@npm:^1.1.14, is-typed-array@npm:^1.1.15": - version: 1.1.15 - resolution: "is-typed-array@npm:1.1.15" - dependencies: - which-typed-array: "npm:^1.1.16" - checksum: 10/e8cf60b9ea85667097a6ad68c209c9722cfe8c8edf04d6218366469e51944c5cc25bae45ffb845c23f811d262e4314d3b0168748eb16711aa34d12724cdf0735 - languageName: node - linkType: hard - -"is-weakmap@npm:^2.0.2": - version: 2.0.2 - resolution: "is-weakmap@npm:2.0.2" - checksum: 10/a7b7e23206c542dcf2fa0abc483142731788771527e90e7e24f658c0833a0d91948a4f7b30d78f7a65255a48512e41a0288b778ba7fc396137515c12e201fd11 - languageName: node - linkType: hard - -"is-weakref@npm:^1.0.2, is-weakref@npm:^1.1.0": - version: 1.1.1 - resolution: "is-weakref@npm:1.1.1" - dependencies: - call-bound: "npm:^1.0.3" - checksum: 10/543506fd8259038b371bb083aac25b16cb4fd8b12fc58053aa3d45ac28dfd001cd5c6dffbba7aeea4213c74732d46b6cb2cfb5b412eed11f2db524f3f97d09a0 - languageName: node - linkType: hard - -"is-weakset@npm:^2.0.3": - version: 2.0.4 - resolution: "is-weakset@npm:2.0.4" - dependencies: - call-bound: "npm:^1.0.3" - get-intrinsic: "npm:^1.2.6" - checksum: 10/1d5e1d0179beeed3661125a6faa2e59bfb48afda06fc70db807f178aa0ebebc3758fb6358d76b3d528090d5ef85148c345dcfbf90839592fe293e3e5e82f2134 - languageName: node - linkType: hard - "is-wsl@npm:^2.2.0": version: 2.2.0 resolution: "is-wsl@npm:2.2.0" @@ -3894,13 +2941,6 @@ __metadata: languageName: node linkType: hard -"isarray@npm:^2.0.5": - version: 2.0.5 - resolution: "isarray@npm:2.0.5" - checksum: 10/1d8bc7911e13bb9f105b1b3e0b396c787a9e63046af0b8fe0ab1414488ab06b2b099b87a2d8a9e31d21c9a6fad773c7fc8b257c4880f2d957274479d28ca3414 - languageName: node - linkType: hard - "isarray@npm:~1.0.0": version: 1.0.0 resolution: "isarray@npm:1.0.0" @@ -4575,17 +3615,6 @@ __metadata: languageName: node linkType: hard -"json5@npm:^1.0.2": - version: 1.0.2 - resolution: "json5@npm:1.0.2" - dependencies: - minimist: "npm:^1.2.0" - bin: - json5: lib/cli.js - checksum: 10/a78d812dbbd5642c4f637dd130954acfd231b074965871c3e28a5bbd571f099d623ecf9161f1960c4ddf68e0cc98dee8bebfdb94a71ad4551f85a1afc94b63f6 - languageName: node - linkType: hard - "json5@npm:^2.2.3": version: 2.2.3 resolution: "json5@npm:2.2.3" @@ -4756,13 +3785,6 @@ __metadata: languageName: node linkType: hard -"math-intrinsics@npm:^1.1.0": - version: 1.1.0 - resolution: "math-intrinsics@npm:1.1.0" - checksum: 10/11df2eda46d092a6035479632e1ec865b8134bdfc4bd9e571a656f4191525404f13a283a515938c3a8de934dbfd9c09674d9da9fa831e6eb7e22b50b197d2edd - languageName: node - linkType: hard - "merge-stream@npm:^2.0.0": version: 2.0.0 resolution: "merge-stream@npm:2.0.0" @@ -4770,14 +3792,7 @@ __metadata: languageName: node linkType: hard -"merge2@npm:^1.3.0, merge2@npm:^1.4.1": - version: 1.4.1 - resolution: "merge2@npm:1.4.1" - checksum: 10/7268db63ed5169466540b6fb947aec313200bcf6d40c5ab722c22e242f651994619bcd85601602972d3c85bd2cc45a358a4c61937e9f11a061919a1da569b0c2 - languageName: node - linkType: hard - -"micromatch@npm:^4.0.4, micromatch@npm:^4.0.8": +"micromatch@npm:^4.0.4": version: 4.0.8 resolution: "micromatch@npm:4.0.8" dependencies: @@ -4794,15 +3809,6 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:9.0.3": - version: 9.0.3 - resolution: "minimatch@npm:9.0.3" - dependencies: - brace-expansion: "npm:^2.0.1" - checksum: 10/c81b47d28153e77521877649f4bab48348d10938df9e8147a58111fe00ef89559a2938de9f6632910c4f7bf7bb5cd81191a546167e58d357f0cfb1e18cecc1c5 - languageName: node - linkType: hard - "minimatch@npm:^3.0.4, minimatch@npm:^3.0.5, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": version: 3.1.2 resolution: "minimatch@npm:3.1.2" @@ -4830,7 +3836,7 @@ __metadata: languageName: node linkType: hard -"minimist@npm:^1.2.0, minimist@npm:^1.2.6": +"minimist@npm:^1.2.6": version: 1.2.8 resolution: "minimist@npm:1.2.8" checksum: 10/908491b6cc15a6c440ba5b22780a0ba89b9810e1aea684e253e43c4e3b8d56ec1dcdd7ea96dde119c29df59c936cde16062159eae4225c691e19c70b432b6e6f @@ -4939,7 +3945,7 @@ __metadata: languageName: node linkType: hard -"ms@npm:^2.1.1, ms@npm:^2.1.3": +"ms@npm:^2.1.3": version: 2.1.3 resolution: "ms@npm:2.1.3" checksum: 10/aa92de608021b242401676e35cfa5aa42dd70cbdc082b916da7fb925c542173e36bce97ea3e804923fe92c0ad991434e4a38327e15a1b5b5f945d66df615ae6d @@ -5030,69 +4036,13 @@ __metadata: languageName: node linkType: hard -"object-inspect@npm:^1.12.3, object-inspect@npm:^1.13.3": +"object-inspect@npm:^1.12.3": version: 1.13.4 resolution: "object-inspect@npm:1.13.4" checksum: 10/aa13b1190ad3e366f6c83ad8a16ed37a19ed57d267385aa4bfdccda833d7b90465c057ff6c55d035a6b2e52c1a2295582b294217a0a3a1ae7abdd6877ef781fb languageName: node linkType: hard -"object-keys@npm:^1.1.1": - version: 1.1.1 - resolution: "object-keys@npm:1.1.1" - checksum: 10/3d81d02674115973df0b7117628ea4110d56042e5326413e4b4313f0bcdf7dd78d4a3acef2c831463fa3796a66762c49daef306f4a0ea1af44877d7086d73bde - languageName: node - linkType: hard - -"object.assign@npm:^4.1.7": - version: 4.1.7 - resolution: "object.assign@npm:4.1.7" - dependencies: - call-bind: "npm:^1.0.8" - call-bound: "npm:^1.0.3" - define-properties: "npm:^1.2.1" - es-object-atoms: "npm:^1.0.0" - has-symbols: "npm:^1.1.0" - object-keys: "npm:^1.1.1" - checksum: 10/3fe28cdd779f2a728a9a66bd688679ba231a2b16646cd1e46b528fe7c947494387dda4bc189eff3417f3717ef4f0a8f2439347cf9a9aa3cef722fbfd9f615587 - languageName: node - linkType: hard - -"object.fromentries@npm:^2.0.8": - version: 2.0.8 - resolution: "object.fromentries@npm:2.0.8" - dependencies: - call-bind: "npm:^1.0.7" - define-properties: "npm:^1.2.1" - es-abstract: "npm:^1.23.2" - es-object-atoms: "npm:^1.0.0" - checksum: 10/5b2e80f7af1778b885e3d06aeb335dcc86965e39464671adb7167ab06ac3b0f5dd2e637a90d8ebd7426d69c6f135a4753ba3dd7d0fe2a7030cf718dcb910fd92 - languageName: node - linkType: hard - -"object.groupby@npm:^1.0.3": - version: 1.0.3 - resolution: "object.groupby@npm:1.0.3" - dependencies: - call-bind: "npm:^1.0.7" - define-properties: "npm:^1.2.1" - es-abstract: "npm:^1.23.2" - checksum: 10/44cb86dd2c660434be65f7585c54b62f0425b0c96b5c948d2756be253ef06737da7e68d7106e35506ce4a44d16aa85a413d11c5034eb7ce5579ec28752eb42d0 - languageName: node - linkType: hard - -"object.values@npm:^1.2.0": - version: 1.2.1 - resolution: "object.values@npm:1.2.1" - dependencies: - call-bind: "npm:^1.0.8" - call-bound: "npm:^1.0.3" - define-properties: "npm:^1.2.1" - es-object-atoms: "npm:^1.0.0" - checksum: 10/f5ec9eccdefeaaa834b089c525663436812a65ff13de7964a1c3a9110f32054f2d58aa476a645bb14f75a79f3fe1154fb3e7bfdae7ac1e80affe171b2ef74bce - languageName: node - linkType: hard - "on-exit-leak-free@npm:^2.1.0": version: 2.1.2 resolution: "on-exit-leak-free@npm:2.1.2" @@ -5143,17 +4093,6 @@ __metadata: languageName: node linkType: hard -"own-keys@npm:^1.0.1": - version: 1.0.1 - resolution: "own-keys@npm:1.0.1" - dependencies: - get-intrinsic: "npm:^1.2.6" - object-keys: "npm:^1.1.1" - safe-push-apply: "npm:^1.0.0" - checksum: 10/ab4bb3b8636908554fc19bf899e225444195092864cb61503a0d048fdaf662b04be2605b636a4ffeaf6e8811f6fcfa8cbb210ec964c0eb1a41eb853e1d5d2f41 - languageName: node - linkType: hard - "p-limit@npm:^2.2.0": version: 2.3.0 resolution: "p-limit@npm:2.3.0" @@ -5270,13 +4209,6 @@ __metadata: languageName: node linkType: hard -"path-type@npm:^4.0.0": - version: 4.0.0 - resolution: "path-type@npm:4.0.0" - checksum: 10/5b1e2daa247062061325b8fdbfd1fb56dde0a448fb1455453276ea18c60685bdad23a445dc148cf87bc216be1573357509b7d4060494a6fd768c7efad833ee45 - languageName: node - linkType: hard - "picocolors@npm:^1.0.0, picocolors@npm:^1.1.1": version: 1.1.1 resolution: "picocolors@npm:1.1.1" @@ -5369,13 +4301,6 @@ __metadata: languageName: node linkType: hard -"possible-typed-array-names@npm:^1.0.0": - version: 1.1.0 - resolution: "possible-typed-array-names@npm:1.1.0" - checksum: 10/2f44137b8d3dd35f4a7ba7469eec1cd9cfbb46ec164b93a5bc1f4c3d68599c9910ee3b91da1d28b4560e9cc8414c3cd56fedc07259c67e52cc774476270d3302 - languageName: node - linkType: hard - "prelude-ls@npm:^1.2.1": version: 1.2.1 resolution: "prelude-ls@npm:1.2.1" @@ -5383,24 +4308,6 @@ __metadata: languageName: node linkType: hard -"prettier-linter-helpers@npm:^1.0.0": - version: 1.0.0 - resolution: "prettier-linter-helpers@npm:1.0.0" - dependencies: - fast-diff: "npm:^1.1.2" - checksum: 10/00ce8011cf6430158d27f9c92cfea0a7699405633f7f1d4a45f07e21bf78e99895911cbcdc3853db3a824201a7c745bd49bfea8abd5fb9883e765a90f74f8392 - languageName: node - linkType: hard - -"prettier@npm:^3.0.3": - version: 3.5.2 - resolution: "prettier@npm:3.5.2" - bin: - prettier: bin/prettier.cjs - checksum: 10/ac7a157c8ec76459b13d81a03ff65d228015992cb926b676b0f1c83edd47e5db8ba257336b400be20942fc671816f1afde377cffe94d9e4368762a3d3acbffe5 - languageName: node - linkType: hard - "pretty-format@npm:^29.0.0, pretty-format@npm:^29.7.0": version: 29.7.0 resolution: "pretty-format@npm:29.7.0" @@ -5580,36 +4487,6 @@ __metadata: languageName: node linkType: hard -"reflect.getprototypeof@npm:^1.0.6, reflect.getprototypeof@npm:^1.0.9": - version: 1.0.10 - resolution: "reflect.getprototypeof@npm:1.0.10" - dependencies: - call-bind: "npm:^1.0.8" - define-properties: "npm:^1.2.1" - es-abstract: "npm:^1.23.9" - es-errors: "npm:^1.3.0" - es-object-atoms: "npm:^1.0.0" - get-intrinsic: "npm:^1.2.7" - get-proto: "npm:^1.0.1" - which-builtin-type: "npm:^1.2.1" - checksum: 10/80a4e2be716f4fe46a89a08ccad0863b47e8ce0f49616cab2d65dab0fbd53c6fdba0f52935fd41d37a2e4e22355c272004f920d63070de849f66eea7aeb4a081 - languageName: node - linkType: hard - -"regexp.prototype.flags@npm:^1.5.3": - version: 1.5.4 - resolution: "regexp.prototype.flags@npm:1.5.4" - dependencies: - call-bind: "npm:^1.0.8" - define-properties: "npm:^1.2.1" - es-errors: "npm:^1.3.0" - get-proto: "npm:^1.0.1" - gopd: "npm:^1.2.0" - set-function-name: "npm:^2.0.2" - checksum: 10/8ab897ca445968e0b96f6237641510f3243e59c180ee2ee8d83889c52ff735dd1bf3657fcd36db053e35e1d823dd53f2565d0b8021ea282c9fe62401c6c3bd6d - languageName: node - linkType: hard - "require-directory@npm:^2.1.1": version: 2.1.1 resolution: "require-directory@npm:2.1.1" @@ -5640,13 +4517,6 @@ __metadata: languageName: node linkType: hard -"resolve-pkg-maps@npm:^1.0.0": - version: 1.0.0 - resolution: "resolve-pkg-maps@npm:1.0.0" - checksum: 10/0763150adf303040c304009231314d1e84c6e5ebfa2d82b7d94e96a6e82bacd1dcc0b58ae257315f3c8adb89a91d8d0f12928241cba2df1680fbe6f60bf99b0e - languageName: node - linkType: hard - "resolve.exports@npm:^2.0.0": version: 2.0.3 resolution: "resolve.exports@npm:2.0.3" @@ -5654,7 +4524,7 @@ __metadata: languageName: node linkType: hard -"resolve@npm:^1.20.0, resolve@npm:^1.22.2, resolve@npm:^1.22.4": +"resolve@npm:^1.20.0": version: 1.22.10 resolution: "resolve@npm:1.22.10" dependencies: @@ -5667,7 +4537,7 @@ __metadata: languageName: node linkType: hard -"resolve@patch:resolve@npm%3A^1.20.0#optional!builtin, resolve@patch:resolve@npm%3A^1.22.2#optional!builtin, resolve@patch:resolve@npm%3A^1.22.4#optional!builtin": +"resolve@patch:resolve@npm%3A^1.20.0#optional!builtin": version: 1.22.10 resolution: "resolve@patch:resolve@npm%3A1.22.10#optional!builtin::version=1.22.10&hash=c3c19d" dependencies: @@ -5720,19 +4590,12 @@ __metadata: version: 0.0.0-use.local resolution: "root-workspace-0b6124@workspace:." dependencies: + "@biomejs/biome": "npm:1.9.4" "@midnight-ntwrk/compact-runtime": "npm:^0.7.0" "@midnight-ntwrk/ledger": "npm:^3.0.6" "@midnight-ntwrk/zswap": "npm:^3.0.6" "@types/jest": "npm:^29.5.6" "@types/node": "npm:^18.18.6" - "@typescript-eslint/eslint-plugin": "npm:^6.8.0" - "@typescript-eslint/parser": "npm:^6.8.0" - eslint: "npm:^8.52.0" - eslint-config-prettier: "npm:^9.0.0" - eslint-plugin-import: "npm:^2.28.1" - eslint-plugin-n: "npm:^16.2.0" - eslint-plugin-prettier: "npm:^5.0.1" - eslint-plugin-promise: "npm:^6.1.1" fast-check: "npm:^3.15.0" fp-ts: "npm:^2.16.1" io-ts: "npm:^2.2.20" @@ -5743,7 +4606,6 @@ __metadata: jest-junit: "npm:^16.0.0" pino: "npm:^8.16.0" pino-pretty: "npm:^10.2.3" - prettier: "npm:^3.0.3" rxjs: "npm:^7.8.1" testcontainers: "npm:^10.3.2" ts-jest: "npm:^29.1.1" @@ -5771,19 +4633,6 @@ __metadata: languageName: node linkType: hard -"safe-array-concat@npm:^1.1.3": - version: 1.1.3 - resolution: "safe-array-concat@npm:1.1.3" - dependencies: - call-bind: "npm:^1.0.8" - call-bound: "npm:^1.0.2" - get-intrinsic: "npm:^1.2.6" - has-symbols: "npm:^1.1.0" - isarray: "npm:^2.0.5" - checksum: 10/fac4f40f20a3f7da024b54792fcc61059e814566dcbb04586bfefef4d3b942b2408933f25b7b3dd024affd3f2a6bbc916bef04807855e4f192413941369db864 - languageName: node - linkType: hard - "safe-buffer@npm:~5.1.0, safe-buffer@npm:~5.1.1": version: 5.1.2 resolution: "safe-buffer@npm:5.1.2" @@ -5798,27 +4647,6 @@ __metadata: languageName: node linkType: hard -"safe-push-apply@npm:^1.0.0": - version: 1.0.0 - resolution: "safe-push-apply@npm:1.0.0" - dependencies: - es-errors: "npm:^1.3.0" - isarray: "npm:^2.0.5" - checksum: 10/2bd4e53b6694f7134b9cf93631480e7fafc8637165f0ee91d5a4af5e7f33d37de9562d1af5021178dd4217d0230cde8d6530fa28cfa1ebff9a431bf8fff124b4 - languageName: node - linkType: hard - -"safe-regex-test@npm:^1.1.0": - version: 1.1.0 - resolution: "safe-regex-test@npm:1.1.0" - dependencies: - call-bound: "npm:^1.0.2" - es-errors: "npm:^1.3.0" - is-regex: "npm:^1.2.1" - checksum: 10/ebdb61f305bf4756a5b023ad86067df5a11b26898573afe9e52a548a63c3bd594825d9b0e2dde2eb3c94e57e0e04ac9929d4107c394f7b8e56a4613bed46c69a - languageName: node - linkType: hard - "safe-stable-stringify@npm:^2.3.1": version: 2.5.0 resolution: "safe-stable-stringify@npm:2.5.0" @@ -5849,7 +4677,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.0.0, semver@npm:^7.3.5, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.7.1": +"semver@npm:^7.3.5, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.7.1": version: 7.7.1 resolution: "semver@npm:7.7.1" bin: @@ -5858,43 +4686,6 @@ __metadata: languageName: node linkType: hard -"set-function-length@npm:^1.2.2": - version: 1.2.2 - resolution: "set-function-length@npm:1.2.2" - dependencies: - define-data-property: "npm:^1.1.4" - es-errors: "npm:^1.3.0" - function-bind: "npm:^1.1.2" - get-intrinsic: "npm:^1.2.4" - gopd: "npm:^1.0.1" - has-property-descriptors: "npm:^1.0.2" - checksum: 10/505d62b8e088468917ca4e3f8f39d0e29f9a563b97dbebf92f4bd2c3172ccfb3c5b8e4566d5fcd00784a00433900e7cb8fbc404e2dbd8c3818ba05bb9d4a8a6d - languageName: node - linkType: hard - -"set-function-name@npm:^2.0.2": - version: 2.0.2 - resolution: "set-function-name@npm:2.0.2" - dependencies: - define-data-property: "npm:^1.1.4" - es-errors: "npm:^1.3.0" - functions-have-names: "npm:^1.2.3" - has-property-descriptors: "npm:^1.0.2" - checksum: 10/c7614154a53ebf8c0428a6c40a3b0b47dac30587c1a19703d1b75f003803f73cdfa6a93474a9ba678fa565ef5fbddc2fae79bca03b7d22ab5fd5163dbe571a74 - languageName: node - linkType: hard - -"set-proto@npm:^1.0.0": - version: 1.0.0 - resolution: "set-proto@npm:1.0.0" - dependencies: - dunder-proto: "npm:^1.0.1" - es-errors: "npm:^1.3.0" - es-object-atoms: "npm:^1.0.0" - checksum: 10/b87f8187bca595ddc3c0721ece4635015fd9d7cb294e6dd2e394ce5186a71bbfa4dc8a35010958c65e43ad83cde09642660e61a952883c24fd6b45ead15f045c - languageName: node - linkType: hard - "shebang-command@npm:^2.0.0": version: 2.0.0 resolution: "shebang-command@npm:2.0.0" @@ -5911,54 +4702,6 @@ __metadata: languageName: node linkType: hard -"side-channel-list@npm:^1.0.0": - version: 1.0.0 - resolution: "side-channel-list@npm:1.0.0" - dependencies: - es-errors: "npm:^1.3.0" - object-inspect: "npm:^1.13.3" - checksum: 10/603b928997abd21c5a5f02ae6b9cc36b72e3176ad6827fab0417ead74580cc4fb4d5c7d0a8a2ff4ead34d0f9e35701ed7a41853dac8a6d1a664fcce1a044f86f - languageName: node - linkType: hard - -"side-channel-map@npm:^1.0.1": - version: 1.0.1 - resolution: "side-channel-map@npm:1.0.1" - dependencies: - call-bound: "npm:^1.0.2" - es-errors: "npm:^1.3.0" - get-intrinsic: "npm:^1.2.5" - object-inspect: "npm:^1.13.3" - checksum: 10/5771861f77feefe44f6195ed077a9e4f389acc188f895f570d56445e251b861754b547ea9ef73ecee4e01fdada6568bfe9020d2ec2dfc5571e9fa1bbc4a10615 - languageName: node - linkType: hard - -"side-channel-weakmap@npm:^1.0.2": - version: 1.0.2 - resolution: "side-channel-weakmap@npm:1.0.2" - dependencies: - call-bound: "npm:^1.0.2" - es-errors: "npm:^1.3.0" - get-intrinsic: "npm:^1.2.5" - object-inspect: "npm:^1.13.3" - side-channel-map: "npm:^1.0.1" - checksum: 10/a815c89bc78c5723c714ea1a77c938377ea710af20d4fb886d362b0d1f8ac73a17816a5f6640f354017d7e292a43da9c5e876c22145bac00b76cfb3468001736 - languageName: node - linkType: hard - -"side-channel@npm:^1.1.0": - version: 1.1.0 - resolution: "side-channel@npm:1.1.0" - dependencies: - es-errors: "npm:^1.3.0" - object-inspect: "npm:^1.13.3" - side-channel-list: "npm:^1.0.0" - side-channel-map: "npm:^1.0.1" - side-channel-weakmap: "npm:^1.0.2" - checksum: 10/7d53b9db292c6262f326b6ff3bc1611db84ece36c2c7dc0e937954c13c73185b0406c56589e2bb8d071d6fee468e14c39fb5d203ee39be66b7b8174f179afaba - languageName: node - linkType: hard - "signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.3, signal-exit@npm:^3.0.7": version: 3.0.7 resolution: "signal-exit@npm:3.0.7" @@ -6160,44 +4903,6 @@ __metadata: languageName: node linkType: hard -"string.prototype.trim@npm:^1.2.10": - version: 1.2.10 - resolution: "string.prototype.trim@npm:1.2.10" - dependencies: - call-bind: "npm:^1.0.8" - call-bound: "npm:^1.0.2" - define-data-property: "npm:^1.1.4" - define-properties: "npm:^1.2.1" - es-abstract: "npm:^1.23.5" - es-object-atoms: "npm:^1.0.0" - has-property-descriptors: "npm:^1.0.2" - checksum: 10/47bb63cd2470a64bc5e2da1e570d369c016ccaa85c918c3a8bb4ab5965120f35e66d1f85ea544496fac84b9207a6b722adf007e6c548acd0813e5f8a82f9712a - languageName: node - linkType: hard - -"string.prototype.trimend@npm:^1.0.8, string.prototype.trimend@npm:^1.0.9": - version: 1.0.9 - resolution: "string.prototype.trimend@npm:1.0.9" - dependencies: - call-bind: "npm:^1.0.8" - call-bound: "npm:^1.0.2" - define-properties: "npm:^1.2.1" - es-object-atoms: "npm:^1.0.0" - checksum: 10/140c73899b6747de9e499c7c2e7a83d549c47a26fa06045b69492be9cfb9e2a95187499a373983a08a115ecff8bc3bd7b0fb09b8ff72fb2172abe766849272ef - languageName: node - linkType: hard - -"string.prototype.trimstart@npm:^1.0.8": - version: 1.0.8 - resolution: "string.prototype.trimstart@npm:1.0.8" - dependencies: - call-bind: "npm:^1.0.7" - define-properties: "npm:^1.2.1" - es-object-atoms: "npm:^1.0.0" - checksum: 10/160167dfbd68e6f7cb9f51a16074eebfce1571656fc31d40c3738ca9e30e35496f2c046fe57b6ad49f65f238a152be8c86fd9a2dd58682b5eba39dad995b3674 - languageName: node - linkType: hard - "string_decoder@npm:^1.1.1, string_decoder@npm:^1.3.0": version: 1.3.0 resolution: "string_decoder@npm:1.3.0" @@ -6234,13 +4939,6 @@ __metadata: languageName: node linkType: hard -"strip-bom@npm:^3.0.0": - version: 3.0.0 - resolution: "strip-bom@npm:3.0.0" - checksum: 10/8d50ff27b7ebe5ecc78f1fe1e00fcdff7af014e73cf724b46fb81ef889eeb1015fc5184b64e81a2efe002180f3ba431bdd77e300da5c6685d702780fbf0c8d5b - languageName: node - linkType: hard - "strip-bom@npm:^4.0.0": version: 4.0.0 resolution: "strip-bom@npm:4.0.0" @@ -6287,16 +4985,6 @@ __metadata: languageName: node linkType: hard -"synckit@npm:^0.9.1": - version: 0.9.2 - resolution: "synckit@npm:0.9.2" - dependencies: - "@pkgr/core": "npm:^0.1.0" - tslib: "npm:^2.6.2" - checksum: 10/d45c4288be9c0232343650643892a7edafb79152c0c08d7ae5d33ca2c296b67a0e15f8cb5c9153969612c4ea5cd5686297542384aab977db23cfa6653fe02027 - languageName: node - linkType: hard - "tar-fs@npm:^3.0.6": version: 3.0.8 resolution: "tar-fs@npm:3.0.8" @@ -6446,15 +5134,6 @@ __metadata: languageName: node linkType: hard -"ts-api-utils@npm:^1.0.1": - version: 1.4.3 - resolution: "ts-api-utils@npm:1.4.3" - peerDependencies: - typescript: ">=4.2.0" - checksum: 10/713c51e7392323305bd4867422ba130fbf70873ef6edbf80ea6d7e9c8f41eeeb13e40e8e7fe7cd321d74e4864777329797077268c9f570464303a1723f1eed39 - languageName: node - linkType: hard - "ts-jest@npm:^29.1.1": version: 29.2.6 resolution: "ts-jest@npm:29.2.6" @@ -6530,19 +5209,7 @@ __metadata: languageName: node linkType: hard -"tsconfig-paths@npm:^3.15.0": - version: 3.15.0 - resolution: "tsconfig-paths@npm:3.15.0" - dependencies: - "@types/json5": "npm:^0.0.29" - json5: "npm:^1.0.2" - minimist: "npm:^1.2.6" - strip-bom: "npm:^3.0.0" - checksum: 10/2041beaedc6c271fc3bedd12e0da0cc553e65d030d4ff26044b771fac5752d0460944c0b5e680f670c2868c95c664a256cec960ae528888db6ded83524e33a14 - languageName: node - linkType: hard - -"tslib@npm:^2.1.0, tslib@npm:^2.6.2": +"tslib@npm:^2.1.0": version: 2.8.1 resolution: "tslib@npm:2.8.1" checksum: 10/3e2e043d5c2316461cb54e5c7fe02c30ef6dccb3384717ca22ae5c6b5bc95232a6241df19c622d9c73b809bea33b187f6dbc73030963e29950c2141bc32a79f7 @@ -6657,59 +5324,6 @@ __metadata: languageName: node linkType: hard -"typed-array-buffer@npm:^1.0.3": - version: 1.0.3 - resolution: "typed-array-buffer@npm:1.0.3" - dependencies: - call-bound: "npm:^1.0.3" - es-errors: "npm:^1.3.0" - is-typed-array: "npm:^1.1.14" - checksum: 10/3fb91f0735fb413b2bbaaca9fabe7b8fc14a3fa5a5a7546bab8a57e755be0e3788d893195ad9c2b842620592de0e68d4c077d4c2c41f04ec25b8b5bb82fa9a80 - languageName: node - linkType: hard - -"typed-array-byte-length@npm:^1.0.3": - version: 1.0.3 - resolution: "typed-array-byte-length@npm:1.0.3" - dependencies: - call-bind: "npm:^1.0.8" - for-each: "npm:^0.3.3" - gopd: "npm:^1.2.0" - has-proto: "npm:^1.2.0" - is-typed-array: "npm:^1.1.14" - checksum: 10/269dad101dda73e3110117a9b84db86f0b5c07dad3a9418116fd38d580cab7fc628a4fc167e29b6d7c39da2f53374b78e7cb578b3c5ec7a556689d985d193519 - languageName: node - linkType: hard - -"typed-array-byte-offset@npm:^1.0.4": - version: 1.0.4 - resolution: "typed-array-byte-offset@npm:1.0.4" - dependencies: - available-typed-arrays: "npm:^1.0.7" - call-bind: "npm:^1.0.8" - for-each: "npm:^0.3.3" - gopd: "npm:^1.2.0" - has-proto: "npm:^1.2.0" - is-typed-array: "npm:^1.1.15" - reflect.getprototypeof: "npm:^1.0.9" - checksum: 10/c2869aa584cdae24ecfd282f20a0f556b13a49a9d5bca1713370bb3c89dff0ccbc5ceb45cb5b784c98f4579e5e3e2a07e438c3a5b8294583e2bd4abbd5104fb5 - languageName: node - linkType: hard - -"typed-array-length@npm:^1.0.7": - version: 1.0.7 - resolution: "typed-array-length@npm:1.0.7" - dependencies: - call-bind: "npm:^1.0.7" - for-each: "npm:^0.3.3" - gopd: "npm:^1.0.1" - is-typed-array: "npm:^1.1.13" - possible-typed-array-names: "npm:^1.0.0" - reflect.getprototypeof: "npm:^1.0.6" - checksum: 10/d6b2f0e81161682d2726eb92b1dc2b0890890f9930f33f9bcf6fc7272895ce66bc368066d273e6677776de167608adc53fcf81f1be39a146d64b630edbf2081c - languageName: node - linkType: hard - "typescript@npm:^5.2.2": version: 5.8.2 resolution: "typescript@npm:5.8.2" @@ -6730,18 +5344,6 @@ __metadata: languageName: node linkType: hard -"unbox-primitive@npm:^1.1.0": - version: 1.1.0 - resolution: "unbox-primitive@npm:1.1.0" - dependencies: - call-bound: "npm:^1.0.3" - has-bigints: "npm:^1.0.2" - has-symbols: "npm:^1.1.0" - which-boxed-primitive: "npm:^1.1.1" - checksum: 10/fadb347020f66b2c8aeacf8b9a79826fa34cc5e5457af4eb0bbc4e79bd87fed0fa795949825df534320f7c13f199259516ad30abc55a6e7b91d8d996ca069e50 - languageName: node - linkType: hard - "undici-types@npm:~5.26.4": version: 5.26.5 resolution: "undici-types@npm:5.26.5" @@ -6856,66 +5458,6 @@ __metadata: languageName: node linkType: hard -"which-boxed-primitive@npm:^1.1.0, which-boxed-primitive@npm:^1.1.1": - version: 1.1.1 - resolution: "which-boxed-primitive@npm:1.1.1" - dependencies: - is-bigint: "npm:^1.1.0" - is-boolean-object: "npm:^1.2.1" - is-number-object: "npm:^1.1.1" - is-string: "npm:^1.1.1" - is-symbol: "npm:^1.1.1" - checksum: 10/a877c0667bc089518c83ad4d845cf8296b03efe3565c1de1940c646e00a2a1ae9ed8a185bcfa27cbf352de7906f0616d83b9d2f19ca500ee02a551fb5cf40740 - languageName: node - linkType: hard - -"which-builtin-type@npm:^1.2.1": - version: 1.2.1 - resolution: "which-builtin-type@npm:1.2.1" - dependencies: - call-bound: "npm:^1.0.2" - function.prototype.name: "npm:^1.1.6" - has-tostringtag: "npm:^1.0.2" - is-async-function: "npm:^2.0.0" - is-date-object: "npm:^1.1.0" - is-finalizationregistry: "npm:^1.1.0" - is-generator-function: "npm:^1.0.10" - is-regex: "npm:^1.2.1" - is-weakref: "npm:^1.0.2" - isarray: "npm:^2.0.5" - which-boxed-primitive: "npm:^1.1.0" - which-collection: "npm:^1.0.2" - which-typed-array: "npm:^1.1.16" - checksum: 10/22c81c5cb7a896c5171742cd30c90d992ff13fb1ea7693e6cf80af077791613fb3f89aa9b4b7f890bd47b6ce09c6322c409932359580a2a2a54057f7b52d1cbe - languageName: node - linkType: hard - -"which-collection@npm:^1.0.2": - version: 1.0.2 - resolution: "which-collection@npm:1.0.2" - dependencies: - is-map: "npm:^2.0.3" - is-set: "npm:^2.0.3" - is-weakmap: "npm:^2.0.2" - is-weakset: "npm:^2.0.3" - checksum: 10/674bf659b9bcfe4055f08634b48a8588e879161b9fefed57e9ec4ff5601e4d50a05ccd76cf10f698ef5873784e5df3223336d56c7ce88e13bcf52ebe582fc8d7 - languageName: node - linkType: hard - -"which-typed-array@npm:^1.1.16, which-typed-array@npm:^1.1.18": - version: 1.1.18 - resolution: "which-typed-array@npm:1.1.18" - dependencies: - available-typed-arrays: "npm:^1.0.7" - call-bind: "npm:^1.0.8" - call-bound: "npm:^1.0.3" - for-each: "npm:^0.3.3" - gopd: "npm:^1.2.0" - has-tostringtag: "npm:^1.0.2" - checksum: 10/11eed801b2bd08cdbaecb17aff381e0fb03526532f61acc06e6c7b9370e08062c33763a51f27825f13fdf34aabd0df6104007f4e8f96e6eaef7db0ce17a26d6e - languageName: node - linkType: hard - "which@npm:^2.0.1": version: 2.0.2 resolution: "which@npm:2.0.2" From 45a2563f60e18c3404057765ec56ee8000f061f4 Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 16 Apr 2025 18:03:47 -0500 Subject: [PATCH 004/322] run fmt --- biome.json | 72 +++++------ compact/.eslintrc.cjs | 2 +- compact/src/run-compactc.cjs | 12 +- contracts/erc20/jest.config.ts | 20 +-- contracts/erc20/js-resolver.cjs | 18 +-- contracts/erc20/src/test/erc20.test.ts | 60 ++++++--- .../src/test/simulators/ERC20Simulator.ts | 118 +++++++++++++----- contracts/erc20/src/test/types/index.ts | 6 +- contracts/erc20/src/test/types/test.ts | 5 +- contracts/erc20/src/test/utils/address.ts | 33 ++--- contracts/erc20/src/test/utils/test.ts | 21 ++-- contracts/initializable/jest.config.ts | 20 +-- contracts/initializable/js-resolver.cjs | 18 +-- .../src/test/InitializableSimulator.ts | 34 +++-- .../src/test/initializable.test.ts | 24 ++-- .../initializable/src/test/types/test.ts | 5 +- .../initializable/src/test/utils/test.ts | 9 +- contracts/utils/jest.config.ts | 20 +-- contracts/utils/js-resolver.cjs | 18 +-- contracts/utils/src/test/UtilsSimulator.ts | 17 +-- contracts/utils/src/test/types/test.ts | 5 +- contracts/utils/src/test/utils.test.ts | 7 +- contracts/utils/src/test/utils/address.ts | 33 ++--- contracts/utils/src/test/utils/index.ts | 8 +- contracts/utils/src/test/utils/test.ts | 21 ++-- package.json | 76 +++++------ turbo.json | 45 +++++-- 27 files changed, 451 insertions(+), 276 deletions(-) diff --git a/biome.json b/biome.json index a06c8fa6..656beeee 100644 --- a/biome.json +++ b/biome.json @@ -1,38 +1,38 @@ { - "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", - "vcs": { - "enabled": false, - "clientKind": "git", - "useIgnoreFile": false - }, - "files": { - "ignoreUnknown": false, - "ignore": [ - "tsconfig.base.json", - "tsconfig*.json", - "*.compact", - "artifacts/*", - "coverage/*", - "dist/*", - "reports/*" - ] - }, - "formatter": { - "enabled": true, - "indentStyle": "tab" - }, - "organizeImports": { - "enabled": true - }, - "linter": { - "enabled": true, - "rules": { - "recommended": true - } - }, - "javascript": { - "formatter": { - "quoteStyle": "single" - } - } + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "vcs": { + "enabled": false, + "clientKind": "git", + "useIgnoreFile": false + }, + "files": { + "ignoreUnknown": false, + "ignore": [ + "tsconfig.base.json", + "tsconfig*.json", + "*.compact", + "artifacts/*", + "coverage/*", + "dist/*", + "reports/*" + ] + }, + "formatter": { + "enabled": true, + "indentStyle": "space" + }, + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "javascript": { + "formatter": { + "quoteStyle": "single" + } + } } diff --git a/compact/.eslintrc.cjs b/compact/.eslintrc.cjs index fad1d320..32fa19b8 100644 --- a/compact/.eslintrc.cjs +++ b/compact/.eslintrc.cjs @@ -19,6 +19,6 @@ module.exports = { '@typescript-eslint/no-misused-promises': 'off', // https://github.com/typescript-eslint/typescript-eslint/issues/5807 '@typescript-eslint/no-floating-promises': 'warn', '@typescript-eslint/promise-function-async': 'off', - '@typescript-eslint/no-redeclare': 'off' + '@typescript-eslint/no-redeclare': 'off', }, }; diff --git a/compact/src/run-compactc.cjs b/compact/src/run-compactc.cjs index 89a5929a..0361d51d 100755 --- a/compact/src/run-compactc.cjs +++ b/compact/src/run-compactc.cjs @@ -9,15 +9,19 @@ const COMPACT_HOME_ENV = process.env.COMPACT_HOME; let compactPath; if (COMPACT_HOME_ENV != null) { compactPath = COMPACT_HOME_ENV; - console.log(`COMPACT_HOME env variable is set; using Compact from ${compactPath}`); + console.log( + `COMPACT_HOME env variable is set; using Compact from ${compactPath}`, + ); } else { compactPath = path.resolve(__dirname, '..', 'compactc'); - console.log(`COMPACT_HOME env variable is not set; using fetched compact from ${compactPath}`); + console.log( + `COMPACT_HOME env variable is not set; using fetched compact from ${compactPath}`, + ); } // yarn runs everything with node... const child = childProcess.spawn(path.resolve(compactPath, 'compactc'), args, { - stdio: 'inherit' + stdio: 'inherit', }); child.on('exit', (code, signal) => { if (code === 0) { @@ -25,4 +29,4 @@ child.on('exit', (code, signal) => { } else { process.exit(code ?? signal); } -}) +}); diff --git a/contracts/erc20/jest.config.ts b/contracts/erc20/jest.config.ts index 3cbccc1b..5d1dbd14 100644 --- a/contracts/erc20/jest.config.ts +++ b/contracts/erc20/jest.config.ts @@ -1,14 +1,14 @@ -import type { Config } from "@jest/types"; +import type { Config } from '@jest/types'; const config: Config.InitialOptions = { - preset: "ts-jest/presets/default-esm", - testEnvironment: "node", + preset: 'ts-jest/presets/default-esm', + testEnvironment: 'node', verbose: true, - roots: [""], - modulePaths: [""], + roots: [''], + modulePaths: [''], passWithNoTests: false, - testMatch: ["**/*.test.ts"], - extensionsToTreatAsEsm: [".ts"], + testMatch: ['**/*.test.ts'], + extensionsToTreatAsEsm: ['.ts'], collectCoverage: true, resolver: '/js-resolver.cjs', coverageThreshold: { @@ -19,9 +19,9 @@ const config: Config.InitialOptions = { }, }, reporters: [ - "default", - ["jest-junit", { outputDirectory: "reports", outputName: "report.xml" }], - ["jest-html-reporters", { publicPath: "reports", filename: "report.html" }], + 'default', + ['jest-junit', { outputDirectory: 'reports', outputName: 'report.xml' }], + ['jest-html-reporters', { publicPath: 'reports', filename: 'report.html' }], ], }; diff --git a/contracts/erc20/js-resolver.cjs b/contracts/erc20/js-resolver.cjs index cc9ed285..19b6f50c 100644 --- a/contracts/erc20/js-resolver.cjs +++ b/contracts/erc20/js-resolver.cjs @@ -1,16 +1,20 @@ const jsResolver = (path, options) => { - const jsExtRegex = /\.js$/i - const resolver = options.defaultResolver - if (jsExtRegex.test(path) && !options.basedir.includes('node_modules') && !path.includes('node_modules')) { + const jsExtRegex = /\.js$/i; + const resolver = options.defaultResolver; + if ( + jsExtRegex.test(path) && + !options.basedir.includes('node_modules') && + !path.includes('node_modules') + ) { const newPath = path.replace(jsExtRegex, '.ts'); try { - return resolver(newPath, options) + return resolver(newPath, options); } catch { // use default resolver } } - return resolver(path, options) -} + return resolver(path, options); +}; -module.exports = jsResolver +module.exports = jsResolver; diff --git a/contracts/erc20/src/test/erc20.test.ts b/contracts/erc20/src/test/erc20.test.ts index 939f1c2b..a31d04eb 100644 --- a/contracts/erc20/src/test/erc20.test.ts +++ b/contracts/erc20/src/test/erc20.test.ts @@ -5,24 +5,31 @@ import * as utils from './utils'; const NO_STRING: MaybeString = { is_some: false, - value: '' + value: '', }; const NAME: MaybeString = { is_some: true, - value: "NAME" + value: 'NAME', }; const SYMBOL: MaybeString = { is_some: true, - value: "SYMBOL" + value: 'SYMBOL', }; const DECIMALS: bigint = 18n; const AMOUNT: bigint = BigInt(250); -const MAX_UINT128 = BigInt(2**128) - BigInt(1); - -const OWNER = String(Buffer.from("OWNER", 'ascii').toString('hex')).padStart(64, '0'); -const SPENDER = String(Buffer.from("SPENDER", 'ascii').toString('hex')).padStart(64, '0'); -const UNAUTHORIZED = String(Buffer.from("UNAUTHORIZED", 'ascii').toString('hex')).padStart(64, '0'); +const MAX_UINT128 = BigInt(2 ** 128) - BigInt(1); + +const OWNER = String(Buffer.from('OWNER', 'ascii').toString('hex')).padStart( + 64, + '0', +); +const SPENDER = String( + Buffer.from('SPENDER', 'ascii').toString('hex'), +).padStart(64, '0'); +const UNAUTHORIZED = String( + Buffer.from('UNAUTHORIZED', 'ascii').toString('hex'), +).padStart(64, '0'); const ZERO = String().padStart(64, '0'); const Z_OWNER = utils.createEitherTestUser('OWNER'); const Z_RECIPIENT = utils.createEitherTestUser('RECIPIENT'); @@ -65,8 +72,8 @@ describe('ERC20', () => { it('returns the amount of existing tokens when there is a supply', () => { token._mint(Z_OWNER, AMOUNT); expect(token.totalSupply()).toEqual(AMOUNT); - }) - }) + }); + }); describe('balanceOf', () => { it('should return zero when requested account has no balance', () => { @@ -232,7 +239,12 @@ describe('ERC20', () => { caller = SPENDER; const partialAmt = AMOUNT - 1n; - const txSuccess = token.transferFrom(Z_OWNER, Z_RECIPIENT, partialAmt, caller); + const txSuccess = token.transferFrom( + Z_OWNER, + Z_RECIPIENT, + partialAmt, + caller, + ); expect(txSuccess).toBeTruthy(); // Check balances @@ -245,7 +257,12 @@ describe('ERC20', () => { it('should transferFrom spender (full)', () => { caller = SPENDER; - const txSuccess = token.transferFrom(Z_OWNER, Z_RECIPIENT, AMOUNT, caller); + const txSuccess = token.transferFrom( + Z_OWNER, + Z_RECIPIENT, + AMOUNT, + caller, + ); expect(txSuccess).toBeTruthy(); // Check balances @@ -260,7 +277,12 @@ describe('ERC20', () => { token.approve(Z_SPENDER, MAX_UINT128, caller); caller = SPENDER; - const txSuccess = token.transferFrom(Z_OWNER, Z_RECIPIENT, AMOUNT, caller); + const txSuccess = token.transferFrom( + Z_OWNER, + Z_RECIPIENT, + AMOUNT, + caller, + ); expect(txSuccess).toBeTruthy(); // Check balances @@ -270,7 +292,7 @@ describe('ERC20', () => { expect(token.allowance(Z_OWNER, Z_SPENDER)).toEqual(MAX_UINT128); }); - it ('should fail when transfer amount exceeds allowance', () => { + it('should fail when transfer amount exceeds allowance', () => { caller = SPENDER; expect(() => { @@ -278,7 +300,7 @@ describe('ERC20', () => { }).toThrow('ERC20: insufficient allowance'); }); - it ('should fail when transfer amount exceeds balance', () => { + it('should fail when transfer amount exceeds balance', () => { caller = OWNER; // Increase allowance > balance token.approve(Z_SPENDER, AMOUNT + 1n, caller); @@ -294,7 +316,7 @@ describe('ERC20', () => { expect(() => { token.transferFrom(Z_OWNER, Z_RECIPIENT, AMOUNT, caller); - }).toThrow("ERC20: insufficient allowance"); + }).toThrow('ERC20: insufficient allowance'); }); it('should fail to transferFrom zero address', () => { @@ -302,7 +324,7 @@ describe('ERC20', () => { expect(() => { token.transferFrom(Z_OWNER, Z_RECIPIENT, AMOUNT, caller); - }).toThrow("ERC20: insufficient allowance"); + }).toThrow('ERC20: insufficient allowance'); }); it('should fail to transferFrom to the zero address', () => { @@ -310,7 +332,7 @@ describe('ERC20', () => { expect(() => { token.transferFrom(Z_OWNER, utils.ZERO_ADDRESS, AMOUNT, caller); - }).toThrow("ERC20: invalid receiver"); + }).toThrow('ERC20: invalid receiver'); }); }); @@ -330,7 +352,7 @@ describe('ERC20', () => { expect(token.balanceOf(Z_OWNER)).toEqual(1n); expect(token.balanceOf(Z_RECIPIENT)).toEqual(partialAmt); }); - }) + }); describe('_mint', () => { it('should mint and update supply', () => { diff --git a/contracts/erc20/src/test/simulators/ERC20Simulator.ts b/contracts/erc20/src/test/simulators/ERC20Simulator.ts index a8e324da..2a5d01a6 100644 --- a/contracts/erc20/src/test/simulators/ERC20Simulator.ts +++ b/contracts/erc20/src/test/simulators/ERC20Simulator.ts @@ -40,15 +40,16 @@ export class ERC20Simulator * @description Initializes the mock contract. */ constructor(name: MaybeString, symbol: MaybeString, decimals: bigint) { - this.contract = new MockERC20( - ERC20Witnesses, - ); + this.contract = new MockERC20(ERC20Witnesses); const { currentPrivateState, currentContractState, currentZswapLocalState, } = this.contract.initialState( - constructorContext({}, '0'.repeat(64)), name, symbol, decimals, + constructorContext({}, '0'.repeat(64)), + name, + symbol, + decimals, ); this.circuitContext = { currentPrivateState, @@ -123,8 +124,11 @@ export class ERC20Simulator * @param account The public key or contract address to query. * @returns The account's token balance. */ - public balanceOf(account: Either): bigint { - return this.contract.impureCircuits.balanceOf(this.circuitContext, account).result; + public balanceOf( + account: Either, + ): bigint { + return this.contract.impureCircuits.balanceOf(this.circuitContext, account) + .result; } /** @@ -136,9 +140,13 @@ export class ERC20Simulator */ public allowance( owner: Either, - spender: Either + spender: Either, ): bigint { - return this.contract.impureCircuits.allowance(this.circuitContext, owner, spender).result; + return this.contract.impureCircuits.allowance( + this.circuitContext, + owner, + spender, + ).result; } /** @@ -148,13 +156,20 @@ export class ERC20Simulator * @param sender The simulated caller. * @returns As per the IERC20 spec, this MUST return true. */ - public transfer(to: Either, value: bigint, sender?: CoinPublicKey): boolean { - const res = this.contract.impureCircuits.transfer({ + public transfer( + to: Either, + value: bigint, + sender?: CoinPublicKey, + ): boolean { + const res = this.contract.impureCircuits.transfer( + { ...this.circuitContext, currentZswapLocalState: sender ? emptyZswapLocalState(sender) : this.circuitContext.currentZswapLocalState, - }, to, value + }, + to, + value, ); this.circuitContext = res.context; @@ -174,15 +189,18 @@ export class ERC20Simulator from: Either, to: Either, value: bigint, - sender?: CoinPublicKey + sender?: CoinPublicKey, ): boolean { - const res = this.contract.impureCircuits.transferFrom({ + const res = this.contract.impureCircuits.transferFrom( + { ...this.circuitContext, currentZswapLocalState: sender ? emptyZswapLocalState(sender) : this.circuitContext.currentZswapLocalState, - }, - from, to, value + }, + from, + to, + value, ); this.circuitContext = res.context; @@ -196,14 +214,20 @@ export class ERC20Simulator * @param sender The simulated caller. * @returns Returns a boolean value indicating whether the operation succeeded. */ - public approve(spender: Either, value: bigint, sender?: CoinPublicKey): boolean { - const res = this.contract.impureCircuits.approve({ + public approve( + spender: Either, + value: bigint, + sender?: CoinPublicKey, + ): boolean { + const res = this.contract.impureCircuits.approve( + { ...this.circuitContext, currentZswapLocalState: sender ? emptyZswapLocalState(sender) : this.circuitContext.currentZswapLocalState, - }, - spender, value + }, + spender, + value, ); this.circuitContext = res.context; @@ -226,9 +250,14 @@ export class ERC20Simulator public _approve( owner: Either, spender: Either, - value: bigint + value: bigint, ) { - this.circuitContext = this.contract.impureCircuits._approve(this.circuitContext, owner, spender, value).context; + this.circuitContext = this.contract.impureCircuits._approve( + this.circuitContext, + owner, + spender, + value, + ).context; } /** @@ -244,7 +273,12 @@ export class ERC20Simulator to: Either, value: bigint, ) { - this.circuitContext = this.contract.impureCircuits._transfer(this.circuitContext, from, to, value).context; + this.circuitContext = this.contract.impureCircuits._transfer( + this.circuitContext, + from, + to, + value, + ).context; } /** @@ -253,8 +287,15 @@ export class ERC20Simulator * @param account The recipient of tokens minted. * @param value The amount of tokens minted. */ - public _mint(account: Either, value: bigint) { - this.circuitContext = this.contract.impureCircuits._mint(this.circuitContext, account, value).context; + public _mint( + account: Either, + value: bigint, + ) { + this.circuitContext = this.contract.impureCircuits._mint( + this.circuitContext, + account, + value, + ).context; } /** @@ -263,8 +304,15 @@ export class ERC20Simulator * @param account The target owner of tokens to burn. * @param value The amount of tokens to burn. */ - public _burn(account: Either, value: bigint) { - this.circuitContext = this.contract.impureCircuits._burn(this.circuitContext, account, value).context; + public _burn( + account: Either, + value: bigint, + ) { + this.circuitContext = this.contract.impureCircuits._burn( + this.circuitContext, + account, + value, + ).context; } /** @@ -277,9 +325,14 @@ export class ERC20Simulator public _update( from: Either, to: Either, - value: bigint + value: bigint, ) { - this.circuitContext = this.contract.impureCircuits._update(this.circuitContext, from, to, value).context; + this.circuitContext = this.contract.impureCircuits._update( + this.circuitContext, + from, + to, + value, + ).context; } /** @@ -292,8 +345,13 @@ export class ERC20Simulator public _spendAllowance( owner: Either, spender: Either, - value: bigint + value: bigint, ) { - this.circuitContext = this.contract.impureCircuits._spendAllowance(this.circuitContext, owner, spender, value).context; + this.circuitContext = this.contract.impureCircuits._spendAllowance( + this.circuitContext, + owner, + spender, + value, + ).context; } } diff --git a/contracts/erc20/src/test/types/index.ts b/contracts/erc20/src/test/types/index.ts index dac4e694..6e704746 100644 --- a/contracts/erc20/src/test/types/index.ts +++ b/contracts/erc20/src/test/types/index.ts @@ -1,6 +1,6 @@ export type { IContractSimulator } from './test'; export type MaybeString = { - is_some: boolean, - value: string -} + is_some: boolean; + value: string; +}; diff --git a/contracts/erc20/src/test/types/test.ts b/contracts/erc20/src/test/types/test.ts index 10fb6c98..7a909543 100644 --- a/contracts/erc20/src/test/types/test.ts +++ b/contracts/erc20/src/test/types/test.ts @@ -1,4 +1,7 @@ -import type { CircuitContext, ContractState } from '@midnight-ntwrk/compact-runtime'; +import type { + CircuitContext, + ContractState, +} from '@midnight-ntwrk/compact-runtime'; /** * Generic interface for mock contract implementations. diff --git a/contracts/erc20/src/test/utils/address.ts b/contracts/erc20/src/test/utils/address.ts index ef9a2842..032f6d4e 100644 --- a/contracts/erc20/src/test/utils/address.ts +++ b/contracts/erc20/src/test/utils/address.ts @@ -1,8 +1,11 @@ import { encodeContractAddress } from '@midnight-ntwrk/ledger'; import * as Compact from '../../artifacts/MockERC20/contract/index.cjs'; -import { convert_bigint_to_Uint8Array, encodeCoinPublicKey } from '@midnight-ntwrk/compact-runtime'; +import { + convert_bigint_to_Uint8Array, + encodeCoinPublicKey, +} from '@midnight-ntwrk/compact-runtime'; -const PREFIX_ADDRESS = "0200"; +const PREFIX_ADDRESS = '0200'; export const pad = (s: string, n: number): Uint8Array => { const encoder = new TextEncoder(); @@ -13,7 +16,7 @@ export const pad = (s: string, n: number): Uint8Array => { const paddedArray = new Uint8Array(n); paddedArray.set(utf8Bytes); return paddedArray; -} +}; /** * @description Generates ZswapCoinPublicKey from `str` for testing purposes. @@ -23,7 +26,7 @@ export const pad = (s: string, n: number): Uint8Array => { export const encodeToPK = (str: string): Compact.ZswapCoinPublicKey => { const toHex = Buffer.from(str, 'ascii').toString('hex'); return { bytes: encodeCoinPublicKey(String(toHex).padStart(64, '0')) }; -} +}; /** * @description Generates ContractAddress from `str` for testing purposes. @@ -35,7 +38,7 @@ export const encodeToAddress = (str: string): Compact.ContractAddress => { const toHex = Buffer.from(str, 'ascii').toString('hex'); const fullAddress = PREFIX_ADDRESS + String(toHex).padStart(64, '0'); return { bytes: encodeContractAddress(fullAddress) }; -} +}; /** * @description Generates an Either object for ZswapCoinPublicKey for testing. @@ -47,9 +50,9 @@ export const createEitherTestUser = (str: string) => { return { is_left: true, left: encodeToPK(str), - right: encodeToAddress('') - } -} + right: encodeToAddress(''), + }; +}; /** * @description Generates an Either object for ContractAddress for testing. @@ -61,18 +64,18 @@ export const createEitherTestContractAddress = (str: string) => { return { is_left: false, left: encodeToPK(''), - right: encodeToAddress(str) - } -} + right: encodeToAddress(str), + }; +}; export const ZERO_KEY = { is_left: true, left: { bytes: convert_bigint_to_Uint8Array(32, BigInt(0)) }, - right: encodeToAddress('') -} + right: encodeToAddress(''), +}; export const ZERO_ADDRESS = { is_left: false, left: encodeToPK(''), - right: { bytes: convert_bigint_to_Uint8Array(32, BigInt(0)) } -} + right: { bytes: convert_bigint_to_Uint8Array(32, BigInt(0)) }, +}; diff --git a/contracts/erc20/src/test/utils/test.ts b/contracts/erc20/src/test/utils/test.ts index 940ed612..d5479fcc 100644 --- a/contracts/erc20/src/test/utils/test.ts +++ b/contracts/erc20/src/test/utils/test.ts @@ -1,10 +1,10 @@ import { - type CircuitContext, - type CoinPublicKey, - type ContractAddress, - type ContractState, - QueryContext, - emptyZswapLocalState, + type CircuitContext, + type CoinPublicKey, + type ContractAddress, + type ContractState, + QueryContext, + emptyZswapLocalState, } from '@midnight-ntwrk/compact-runtime'; import type { IContractSimulator } from '../types'; @@ -50,10 +50,11 @@ export function useCircuitContext

( * @returns A new `CircuitContext` with the sender and updated context values. * @todo TODO: Move this utility to a generic package for broader reuse across contracts. */ -export function useCircuitContextSender>( - contract: C, - sender: CoinPublicKey, -): CircuitContext

{ +export function useCircuitContextSender< + P, + L, + C extends IContractSimulator, +>(contract: C, sender: CoinPublicKey): CircuitContext

{ const currentPrivateState = contract.getCurrentPrivateState(); const originalState = contract.getCurrentContractState(); const contractAddress = contract.contractAddress; diff --git a/contracts/initializable/jest.config.ts b/contracts/initializable/jest.config.ts index 3cbccc1b..5d1dbd14 100644 --- a/contracts/initializable/jest.config.ts +++ b/contracts/initializable/jest.config.ts @@ -1,14 +1,14 @@ -import type { Config } from "@jest/types"; +import type { Config } from '@jest/types'; const config: Config.InitialOptions = { - preset: "ts-jest/presets/default-esm", - testEnvironment: "node", + preset: 'ts-jest/presets/default-esm', + testEnvironment: 'node', verbose: true, - roots: [""], - modulePaths: [""], + roots: [''], + modulePaths: [''], passWithNoTests: false, - testMatch: ["**/*.test.ts"], - extensionsToTreatAsEsm: [".ts"], + testMatch: ['**/*.test.ts'], + extensionsToTreatAsEsm: ['.ts'], collectCoverage: true, resolver: '/js-resolver.cjs', coverageThreshold: { @@ -19,9 +19,9 @@ const config: Config.InitialOptions = { }, }, reporters: [ - "default", - ["jest-junit", { outputDirectory: "reports", outputName: "report.xml" }], - ["jest-html-reporters", { publicPath: "reports", filename: "report.html" }], + 'default', + ['jest-junit', { outputDirectory: 'reports', outputName: 'report.xml' }], + ['jest-html-reporters', { publicPath: 'reports', filename: 'report.html' }], ], }; diff --git a/contracts/initializable/js-resolver.cjs b/contracts/initializable/js-resolver.cjs index cc9ed285..19b6f50c 100644 --- a/contracts/initializable/js-resolver.cjs +++ b/contracts/initializable/js-resolver.cjs @@ -1,16 +1,20 @@ const jsResolver = (path, options) => { - const jsExtRegex = /\.js$/i - const resolver = options.defaultResolver - if (jsExtRegex.test(path) && !options.basedir.includes('node_modules') && !path.includes('node_modules')) { + const jsExtRegex = /\.js$/i; + const resolver = options.defaultResolver; + if ( + jsExtRegex.test(path) && + !options.basedir.includes('node_modules') && + !path.includes('node_modules') + ) { const newPath = path.replace(jsExtRegex, '.ts'); try { - return resolver(newPath, options) + return resolver(newPath, options); } catch { // use default resolver } } - return resolver(path, options) -} + return resolver(path, options); +}; -module.exports = jsResolver +module.exports = jsResolver; diff --git a/contracts/initializable/src/test/InitializableSimulator.ts b/contracts/initializable/src/test/InitializableSimulator.ts index 89402a19..9b8b5687 100644 --- a/contracts/initializable/src/test/InitializableSimulator.ts +++ b/contracts/initializable/src/test/InitializableSimulator.ts @@ -1,7 +1,20 @@ -import { type CircuitContext, type ContractState, QueryContext, sampleContractAddress, constructorContext } from '@midnight-ntwrk/compact-runtime'; -import { Contract as MockInitializable, type Ledger, ledger } from '../artifacts/MockInitializable/contract/index.cjs'; +import { + type CircuitContext, + type ContractState, + QueryContext, + sampleContractAddress, + constructorContext, +} from '@midnight-ntwrk/compact-runtime'; +import { + Contract as MockInitializable, + type Ledger, + ledger, +} from '../artifacts/MockInitializable/contract/index.cjs'; import type { IContractSimulator } from './types'; -import { InitializablePrivateState, InitializableWitnesses } from '../witnesses'; +import { + InitializablePrivateState, + InitializableWitnesses, +} from '../witnesses'; /** * @description A simulator implementation of an utils contract for testing purposes. @@ -31,9 +44,7 @@ export class InitializableSimulator currentPrivateState, currentContractState, currentZswapLocalState, - } = this.contract.initialState( - constructorContext({}, '0'.repeat(64)) - ); + } = this.contract.initialState(constructorContext({}, '0'.repeat(64))); this.circuitContext = { currentPrivateState, currentZswapLocalState, @@ -70,19 +81,22 @@ export class InitializableSimulator return this.circuitContext.originalState; } - /** + /** * @description Initializes the state. * @returns None. */ public initialize() { - this.circuitContext = this.contract.impureCircuits.initialize(this.circuitContext).context; + this.circuitContext = this.contract.impureCircuits.initialize( + this.circuitContext, + ).context; } - /** + /** * @description Returns true if the state is initialized. * @returns Whether the contract has been initialized. */ public isInitialized(): boolean { - return this.contract.impureCircuits.isInitialized(this.circuitContext).result; + return this.contract.impureCircuits.isInitialized(this.circuitContext) + .result; } } diff --git a/contracts/initializable/src/test/initializable.test.ts b/contracts/initializable/src/test/initializable.test.ts index 15ee059c..96d5271e 100644 --- a/contracts/initializable/src/test/initializable.test.ts +++ b/contracts/initializable/src/test/initializable.test.ts @@ -7,26 +7,32 @@ const contract = new InitializableSimulator(); describe('Initializable', () => { it('should generate the initial ledger state deterministically', () => { const contract2 = new InitializableSimulator(); - expect(contract.getCurrentPublicState()).toEqual(contract2.getCurrentPublicState()); + expect(contract.getCurrentPublicState()).toEqual( + contract2.getCurrentPublicState(), + ); }); describe('initialize', () => { it('should not be initialized', () => { expect(contract.isInitialized()).toEqual(false); - expect(contract.getCurrentPublicState().initializableState).toEqual(STATE.uninitialized); + expect(contract.getCurrentPublicState().initializableState).toEqual( + STATE.uninitialized, + ); }); it('should initialize', () => { contract.initialize(); expect(contract.isInitialized()).toEqual(true); - expect(contract.getCurrentPublicState().initializableState).toEqual(STATE.initialized); - }); + expect(contract.getCurrentPublicState().initializableState).toEqual( + STATE.initialized, + ); }); + }); - it('should fail when re-initialized', () => { - expect(() => { - contract.initialize(); - contract.initialize(); - }).toThrow('Contract already initialized'); + it('should fail when re-initialized', () => { + expect(() => { + contract.initialize(); + contract.initialize(); + }).toThrow('Contract already initialized'); }); }); diff --git a/contracts/initializable/src/test/types/test.ts b/contracts/initializable/src/test/types/test.ts index 10fb6c98..7a909543 100644 --- a/contracts/initializable/src/test/types/test.ts +++ b/contracts/initializable/src/test/types/test.ts @@ -1,4 +1,7 @@ -import type { CircuitContext, ContractState } from '@midnight-ntwrk/compact-runtime'; +import type { + CircuitContext, + ContractState, +} from '@midnight-ntwrk/compact-runtime'; /** * Generic interface for mock contract implementations. diff --git a/contracts/initializable/src/test/utils/test.ts b/contracts/initializable/src/test/utils/test.ts index 5a9e5837..d5479fcc 100644 --- a/contracts/initializable/src/test/utils/test.ts +++ b/contracts/initializable/src/test/utils/test.ts @@ -50,10 +50,11 @@ export function useCircuitContext

( * @returns A new `CircuitContext` with the sender and updated context values. * @todo TODO: Move this utility to a generic package for broader reuse across contracts. */ -export function useCircuitContextSender>( - contract: C, - sender: CoinPublicKey, -): CircuitContext

{ +export function useCircuitContextSender< + P, + L, + C extends IContractSimulator, +>(contract: C, sender: CoinPublicKey): CircuitContext

{ const currentPrivateState = contract.getCurrentPrivateState(); const originalState = contract.getCurrentContractState(); const contractAddress = contract.contractAddress; diff --git a/contracts/utils/jest.config.ts b/contracts/utils/jest.config.ts index edbdaeba..f15dd79d 100644 --- a/contracts/utils/jest.config.ts +++ b/contracts/utils/jest.config.ts @@ -1,14 +1,14 @@ -import type { Config } from "@jest/types"; +import type { Config } from '@jest/types'; const config: Config.InitialOptions = { - preset: "ts-jest/presets/default-esm", - testEnvironment: "node", + preset: 'ts-jest/presets/default-esm', + testEnvironment: 'node', verbose: true, - roots: [""], - modulePaths: [""], + roots: [''], + modulePaths: [''], passWithNoTests: false, - testMatch: ["**/*.test.ts"], - extensionsToTreatAsEsm: [".ts"], + testMatch: ['**/*.test.ts'], + extensionsToTreatAsEsm: ['.ts'], collectCoverage: true, resolver: '/js-resolver.cjs', coverageThreshold: { @@ -19,9 +19,9 @@ const config: Config.InitialOptions = { }, }, reporters: [ - "default", - ["jest-junit", { outputDirectory: "reports", outputName: "report.xml" }], - ["jest-html-reporters", { publicPath: "reports", filename: "report.html" }], + 'default', + ['jest-junit', { outputDirectory: 'reports', outputName: 'report.xml' }], + ['jest-html-reporters', { publicPath: 'reports', filename: 'report.html' }], ], }; diff --git a/contracts/utils/js-resolver.cjs b/contracts/utils/js-resolver.cjs index cc9ed285..19b6f50c 100644 --- a/contracts/utils/js-resolver.cjs +++ b/contracts/utils/js-resolver.cjs @@ -1,16 +1,20 @@ const jsResolver = (path, options) => { - const jsExtRegex = /\.js$/i - const resolver = options.defaultResolver - if (jsExtRegex.test(path) && !options.basedir.includes('node_modules') && !path.includes('node_modules')) { + const jsExtRegex = /\.js$/i; + const resolver = options.defaultResolver; + if ( + jsExtRegex.test(path) && + !options.basedir.includes('node_modules') && + !path.includes('node_modules') + ) { const newPath = path.replace(jsExtRegex, '.ts'); try { - return resolver(newPath, options) + return resolver(newPath, options); } catch { // use default resolver } } - return resolver(path, options) -} + return resolver(path, options); +}; -module.exports = jsResolver +module.exports = jsResolver; diff --git a/contracts/utils/src/test/UtilsSimulator.ts b/contracts/utils/src/test/UtilsSimulator.ts index c55c802f..d0bb53e0 100644 --- a/contracts/utils/src/test/UtilsSimulator.ts +++ b/contracts/utils/src/test/UtilsSimulator.ts @@ -37,16 +37,12 @@ export class UtilsContractSimulator * @description Initializes the mock contract. */ constructor() { - this.contract = new MockUtils( - UtilsWitnesses, - ); + this.contract = new MockUtils(UtilsWitnesses); const { currentPrivateState, currentContractState, currentZswapLocalState, - } = this.contract.initialState( - constructorContext({}, '0'.repeat(64)) - ); + } = this.contract.initialState(constructorContext({}, '0'.repeat(64))); this.circuitContext = { currentPrivateState, currentZswapLocalState, @@ -88,7 +84,12 @@ export class UtilsContractSimulator * @param keyOrAddress The target value to check, either a ZswapCoinPublicKey or a ContractAddress. * @returns Returns true if `keyOrAddress` is zero. */ - public isKeyOrAddressZero(keyOrAddress: Either): boolean { - return this.contract.circuits.isKeyOrAddressZero(this.circuitContext, keyOrAddress).result; + public isKeyOrAddressZero( + keyOrAddress: Either, + ): boolean { + return this.contract.circuits.isKeyOrAddressZero( + this.circuitContext, + keyOrAddress, + ).result; } } diff --git a/contracts/utils/src/test/types/test.ts b/contracts/utils/src/test/types/test.ts index 10fb6c98..7a909543 100644 --- a/contracts/utils/src/test/types/test.ts +++ b/contracts/utils/src/test/types/test.ts @@ -1,4 +1,7 @@ -import type { CircuitContext, ContractState } from '@midnight-ntwrk/compact-runtime'; +import type { + CircuitContext, + ContractState, +} from '@midnight-ntwrk/compact-runtime'; /** * Generic interface for mock contract implementations. diff --git a/contracts/utils/src/test/utils.test.ts b/contracts/utils/src/test/utils.test.ts index 3f38952b..da0a8d37 100644 --- a/contracts/utils/src/test/utils.test.ts +++ b/contracts/utils/src/test/utils.test.ts @@ -2,7 +2,8 @@ import { UtilsContractSimulator } from './UtilsSimulator'; import * as contractUtils from './utils'; const Z_SOME_KEY = contractUtils.createEitherTestUser('SOME_KEY'); -const SOME_CONTRACT = contractUtils.createEitherTestContractAddress('SOME_CONTRACT'); +const SOME_CONTRACT = + contractUtils.createEitherTestContractAddress('SOME_CONTRACT'); let contract: UtilsContractSimulator; @@ -12,7 +13,9 @@ describe('Utils', () => { describe('isKeyOrAddressZero', () => { it('should return zero for the zero address', () => { expect(contract.isKeyOrAddressZero(contractUtils.ZERO_KEY)).toBeTruthy(); - expect(contract.isKeyOrAddressZero(contractUtils.ZERO_ADDRESS)).toBeTruthy(); + expect( + contract.isKeyOrAddressZero(contractUtils.ZERO_ADDRESS), + ).toBeTruthy(); }); it('should not return zero for nonzero addresses', () => { diff --git a/contracts/utils/src/test/utils/address.ts b/contracts/utils/src/test/utils/address.ts index f6255d60..218db5de 100644 --- a/contracts/utils/src/test/utils/address.ts +++ b/contracts/utils/src/test/utils/address.ts @@ -1,8 +1,11 @@ import { encodeContractAddress } from '@midnight-ntwrk/ledger'; import * as Compact from '../../artifacts/MockUtils/contract/index.cjs'; -import { convert_bigint_to_Uint8Array, encodeCoinPublicKey } from '@midnight-ntwrk/compact-runtime'; +import { + convert_bigint_to_Uint8Array, + encodeCoinPublicKey, +} from '@midnight-ntwrk/compact-runtime'; -const PREFIX_ADDRESS = "0200"; +const PREFIX_ADDRESS = '0200'; export const pad = (s: string, n: number): Uint8Array => { const encoder = new TextEncoder(); @@ -13,7 +16,7 @@ export const pad = (s: string, n: number): Uint8Array => { const paddedArray = new Uint8Array(n); paddedArray.set(utf8Bytes); return paddedArray; -} +}; /** * @description Generates ZswapCoinPublicKey from `str` for testing purposes. @@ -23,7 +26,7 @@ export const pad = (s: string, n: number): Uint8Array => { export const encodeToPK = (str: string): Compact.ZswapCoinPublicKey => { const toHex = Buffer.from(str, 'ascii').toString('hex'); return { bytes: encodeCoinPublicKey(String(toHex).padStart(64, '0')) }; -} +}; /** * @description Generates ContractAddress from `str` for testing purposes. @@ -35,7 +38,7 @@ export const encodeToAddress = (str: string): Compact.ContractAddress => { const toHex = Buffer.from(str, 'ascii').toString('hex'); const fullAddress = PREFIX_ADDRESS + String(toHex).padStart(64, '0'); return { bytes: encodeContractAddress(fullAddress) }; -} +}; /** * @description Generates an Either object for ZswapCoinPublicKey for testing. @@ -47,9 +50,9 @@ export const createEitherTestUser = (str: string) => { return { is_left: true, left: encodeToPK(str), - right: encodeToAddress('') - } -} + right: encodeToAddress(''), + }; +}; /** * @description Generates an Either object for ContractAddress for testing. @@ -61,18 +64,18 @@ export const createEitherTestContractAddress = (str: string) => { return { is_left: false, left: encodeToPK(''), - right: encodeToAddress(str) - } -} + right: encodeToAddress(str), + }; +}; export const ZERO_KEY = { is_left: true, left: { bytes: convert_bigint_to_Uint8Array(32, BigInt(0)) }, - right: encodeToAddress('') -} + right: encodeToAddress(''), +}; export const ZERO_ADDRESS = { is_left: false, left: encodeToPK(''), - right: { bytes: convert_bigint_to_Uint8Array(32, BigInt(0)) } -} + right: { bytes: convert_bigint_to_Uint8Array(32, BigInt(0)) }, +}; diff --git a/contracts/utils/src/test/utils/index.ts b/contracts/utils/src/test/utils/index.ts index 2668cc79..b8e9585d 100644 --- a/contracts/utils/src/test/utils/index.ts +++ b/contracts/utils/src/test/utils/index.ts @@ -1,4 +1,10 @@ export { useCircuitContext as circuitContext } from './test'; export { - pad, encodeToPK, encodeToAddress, createEitherTestUser, createEitherTestContractAddress, ZERO_KEY, ZERO_ADDRESS + pad, + encodeToPK, + encodeToAddress, + createEitherTestUser, + createEitherTestContractAddress, + ZERO_KEY, + ZERO_ADDRESS, } from './address'; diff --git a/contracts/utils/src/test/utils/test.ts b/contracts/utils/src/test/utils/test.ts index 940ed612..d5479fcc 100644 --- a/contracts/utils/src/test/utils/test.ts +++ b/contracts/utils/src/test/utils/test.ts @@ -1,10 +1,10 @@ import { - type CircuitContext, - type CoinPublicKey, - type ContractAddress, - type ContractState, - QueryContext, - emptyZswapLocalState, + type CircuitContext, + type CoinPublicKey, + type ContractAddress, + type ContractState, + QueryContext, + emptyZswapLocalState, } from '@midnight-ntwrk/compact-runtime'; import type { IContractSimulator } from '../types'; @@ -50,10 +50,11 @@ export function useCircuitContext

( * @returns A new `CircuitContext` with the sender and updated context values. * @todo TODO: Move this utility to a generic package for broader reuse across contracts. */ -export function useCircuitContextSender>( - contract: C, - sender: CoinPublicKey, -): CircuitContext

{ +export function useCircuitContextSender< + P, + L, + C extends IContractSimulator, +>(contract: C, sender: CoinPublicKey): CircuitContext

{ const currentPrivateState = contract.getCurrentPrivateState(); const originalState = contract.getCurrentContractState(); const contractAddress = contract.contractAddress; diff --git a/package.json b/package.json index 128470a1..18a86574 100644 --- a/package.json +++ b/package.json @@ -1,40 +1,40 @@ { - "packageManager": "yarn@4.1.0", - "workspaces": [ - "compact", - "contracts/erc20/", - "contracts/initializable/", - "contracts/utils/" - ], - "scripts": { - "compact": "turbo run compact", - "build": "turbo run build", - "lint": "turbo run lint" - }, - "dependencies": { - "@midnight-ntwrk/compact-runtime": "^0.7.0", - "fp-ts": "^2.16.1", - "io-ts": "^2.2.20", - "pino": "^8.16.0", - "pino-pretty": "^10.2.3", - "rxjs": "^7.8.1" - }, - "devDependencies": { - "@biomejs/biome": "1.9.4", - "@midnight-ntwrk/ledger": "^3.0.6", - "@midnight-ntwrk/zswap": "^3.0.6", - "@types/jest": "^29.5.6", - "@types/node": "^18.18.6", - "fast-check": "^3.15.0", - "jest": "^29.7.0", - "jest-fast-check": "^2.0.0", - "jest-gh-md-reporter": "^0.0.2", - "jest-html-reporters": "^3.1.4", - "jest-junit": "^16.0.0", - "testcontainers": "^10.3.2", - "ts-jest": "^29.1.1", - "ts-node": "^10.9.1", - "turbo": "^1.10.16", - "typescript": "^5.2.2" - } + "packageManager": "yarn@4.1.0", + "workspaces": [ + "compact", + "contracts/erc20/", + "contracts/initializable/", + "contracts/utils/" + ], + "scripts": { + "compact": "turbo run compact", + "build": "turbo run build", + "lint": "turbo run lint" + }, + "dependencies": { + "@midnight-ntwrk/compact-runtime": "^0.7.0", + "fp-ts": "^2.16.1", + "io-ts": "^2.2.20", + "pino": "^8.16.0", + "pino-pretty": "^10.2.3", + "rxjs": "^7.8.1" + }, + "devDependencies": { + "@biomejs/biome": "1.9.4", + "@midnight-ntwrk/ledger": "^3.0.6", + "@midnight-ntwrk/zswap": "^3.0.6", + "@types/jest": "^29.5.6", + "@types/node": "^18.18.6", + "fast-check": "^3.15.0", + "jest": "^29.7.0", + "jest-fast-check": "^2.0.0", + "jest-gh-md-reporter": "^0.0.2", + "jest-html-reporters": "^3.1.4", + "jest-junit": "^16.0.0", + "testcontainers": "^10.3.2", + "ts-jest": "^29.1.1", + "ts-node": "^10.9.1", + "turbo": "^1.10.16", + "typescript": "^5.2.2" + } } diff --git a/turbo.json b/turbo.json index b539bd83..770d717d 100644 --- a/turbo.json +++ b/turbo.json @@ -1,12 +1,15 @@ { "$schema": "https://turbo.build/schema.json", - "globalDependencies": [ - ".prettierrc.json" - ], + "globalDependencies": [".prettierrc.json"], "pipeline": { "typecheck": { "dependsOn": ["^build", "compact"], - "inputs": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.mts", "tsconfig.json"], + "inputs": [ + "src/**/*.ts", + "src/**/*.tsx", + "src/**/*.mts", + "tsconfig.json" + ], "outputMode": "new-only", "outputs": [] }, @@ -20,13 +23,33 @@ "build": { "dependsOn": ["^build", "compact", "typecheck"], "outputMode": "new-only", - "inputs": ["src/**/*.ts", "src/**/*.mts", "src/**/*.tsx", "!src/**/*.test.ts", "!tests/**/*.ts", "tsconfig.json", "tsconfig.build.json", ".env"], + "inputs": [ + "src/**/*.ts", + "src/**/*.mts", + "src/**/*.tsx", + "!src/**/*.test.ts", + "!tests/**/*.ts", + "tsconfig.json", + "tsconfig.build.json", + ".env" + ], "outputs": ["dist/**"] }, "build-storybook": { "dependsOn": ["^build", "typecheck"], "outputMode": "new-only", - "inputs": ["src/**/*.ts", "src/**/*.mts", "src/**/*.tsx", "!src/**/*.test.ts", "!tests/**/*.ts", "tsconfig.json", "tsconfig.build.json", ".env", "vite.config.ts", ".storybook/**"], + "inputs": [ + "src/**/*.ts", + "src/**/*.mts", + "src/**/*.tsx", + "!src/**/*.test.ts", + "!tests/**/*.ts", + "tsconfig.json", + "tsconfig.build.json", + ".env", + "vite.config.ts", + ".storybook/**" + ], "outputs": ["storybook-static/**"] }, "lint": { @@ -37,7 +60,15 @@ "test": { "outputMode": "new-only", "dependsOn": ["^build", "compact", "typecheck"], - "inputs": ["src/**/*.ts", "src/**/*.mts", "src/**/*.tsx", "jest.config.ts", "tsconfig.json", "tsconfig.test.json", "test-compose.yml"], + "inputs": [ + "src/**/*.ts", + "src/**/*.mts", + "src/**/*.tsx", + "jest.config.ts", + "tsconfig.json", + "tsconfig.test.json", + "test-compose.yml" + ], "outputs": ["reports/**"] }, "check": { From fec71f584835232853eafcd50b470f234f51001b Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 16 Apr 2025 18:05:17 -0500 Subject: [PATCH 005/322] run lint --- compact/src/run-compactc.cjs | 2 +- contracts/erc20/src/test/erc20.test.ts | 4 ++-- .../erc20/src/test/simulators/ERC20Simulator.ts | 12 ++++++------ contracts/erc20/src/test/utils/address.ts | 2 +- .../initializable/src/test/InitializableSimulator.ts | 2 +- contracts/utils/src/test/UtilsSimulator.ts | 8 ++++---- contracts/utils/src/test/utils/address.ts | 2 +- 7 files changed, 16 insertions(+), 16 deletions(-) diff --git a/compact/src/run-compactc.cjs b/compact/src/run-compactc.cjs index 0361d51d..dca2891f 100755 --- a/compact/src/run-compactc.cjs +++ b/compact/src/run-compactc.cjs @@ -1,7 +1,7 @@ #!/usr/bin/env node const childProcess = require('node:child_process'); -const path = require('path'); +const path = require('node:path'); const [_node, _script, ...args] = process.argv; const COMPACT_HOME_ENV = process.env.COMPACT_HOME; diff --git a/contracts/erc20/src/test/erc20.test.ts b/contracts/erc20/src/test/erc20.test.ts index a31d04eb..8942dc55 100644 --- a/contracts/erc20/src/test/erc20.test.ts +++ b/contracts/erc20/src/test/erc20.test.ts @@ -1,6 +1,6 @@ -import { CoinPublicKey } from '@midnight-ntwrk/compact-runtime'; +import type { CoinPublicKey } from '@midnight-ntwrk/compact-runtime'; import { ERC20Simulator } from './simulators'; -import { MaybeString } from './types'; +import type { MaybeString } from './types'; import * as utils from './utils'; const NO_STRING: MaybeString = { diff --git a/contracts/erc20/src/test/simulators/ERC20Simulator.ts b/contracts/erc20/src/test/simulators/ERC20Simulator.ts index 2a5d01a6..f2e7257a 100644 --- a/contracts/erc20/src/test/simulators/ERC20Simulator.ts +++ b/contracts/erc20/src/test/simulators/ERC20Simulator.ts @@ -1,6 +1,6 @@ import { type CircuitContext, - CoinPublicKey, + type CoinPublicKey, type ContractState, QueryContext, constructorContext, @@ -11,13 +11,13 @@ import { type Ledger, Contract as MockERC20, ledger, - Either, - ZswapCoinPublicKey, - ContractAddress, + type Either, + type ZswapCoinPublicKey, + type ContractAddress, } from '../../artifacts/MockERC20/contract/index.cjs'; // Combined imports -import { MaybeString } from '../types'; +import type { MaybeString } from '../types'; import type { IContractSimulator } from './../types'; -import { ERC20PrivateState, ERC20Witnesses } from '../../witnesses'; +import { type ERC20PrivateState, ERC20Witnesses } from '../../witnesses'; /** * @description A simulator implementation of an erc20 contract for testing purposes. diff --git a/contracts/erc20/src/test/utils/address.ts b/contracts/erc20/src/test/utils/address.ts index 032f6d4e..e775fee6 100644 --- a/contracts/erc20/src/test/utils/address.ts +++ b/contracts/erc20/src/test/utils/address.ts @@ -1,5 +1,5 @@ import { encodeContractAddress } from '@midnight-ntwrk/ledger'; -import * as Compact from '../../artifacts/MockERC20/contract/index.cjs'; +import type * as Compact from '../../artifacts/MockERC20/contract/index.cjs'; import { convert_bigint_to_Uint8Array, encodeCoinPublicKey, diff --git a/contracts/initializable/src/test/InitializableSimulator.ts b/contracts/initializable/src/test/InitializableSimulator.ts index 9b8b5687..783b17f4 100644 --- a/contracts/initializable/src/test/InitializableSimulator.ts +++ b/contracts/initializable/src/test/InitializableSimulator.ts @@ -12,7 +12,7 @@ import { } from '../artifacts/MockInitializable/contract/index.cjs'; import type { IContractSimulator } from './types'; import { - InitializablePrivateState, + type InitializablePrivateState, InitializableWitnesses, } from '../witnesses'; diff --git a/contracts/utils/src/test/UtilsSimulator.ts b/contracts/utils/src/test/UtilsSimulator.ts index d0bb53e0..abaff99e 100644 --- a/contracts/utils/src/test/UtilsSimulator.ts +++ b/contracts/utils/src/test/UtilsSimulator.ts @@ -9,12 +9,12 @@ import { type Ledger, Contract as MockUtils, ledger, - Either, - ZswapCoinPublicKey, - ContractAddress, + type Either, + type ZswapCoinPublicKey, + type ContractAddress, } from '../artifacts/MockUtils/contract/index.cjs'; // Combined imports import type { IContractSimulator } from './types'; -import { UtilsPrivateState, UtilsWitnesses } from '../witnesses'; +import { type UtilsPrivateState, UtilsWitnesses } from '../witnesses'; /** * @description A simulator implementation of an utils contract for testing purposes. diff --git a/contracts/utils/src/test/utils/address.ts b/contracts/utils/src/test/utils/address.ts index 218db5de..9690fd48 100644 --- a/contracts/utils/src/test/utils/address.ts +++ b/contracts/utils/src/test/utils/address.ts @@ -1,5 +1,5 @@ import { encodeContractAddress } from '@midnight-ntwrk/ledger'; -import * as Compact from '../../artifacts/MockUtils/contract/index.cjs'; +import type * as Compact from '../../artifacts/MockUtils/contract/index.cjs'; import { convert_bigint_to_Uint8Array, encodeCoinPublicKey, From 86565b361a2effe34b8ba0c2b61beef7bd077b76 Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 16 Apr 2025 18:06:25 -0500 Subject: [PATCH 006/322] organize imports --- contracts/erc20/src/test/simulators/ERC20Simulator.ts | 8 ++++---- contracts/erc20/src/test/utils/address.ts | 4 ++-- .../initializable/src/test/InitializableSimulator.ts | 6 +++--- contracts/initializable/src/test/initializable.test.ts | 4 ++-- contracts/utils/src/test/UtilsSimulator.ts | 8 ++++---- contracts/utils/src/test/utils/address.ts | 4 ++-- 6 files changed, 17 insertions(+), 17 deletions(-) diff --git a/contracts/erc20/src/test/simulators/ERC20Simulator.ts b/contracts/erc20/src/test/simulators/ERC20Simulator.ts index f2e7257a..6814f878 100644 --- a/contracts/erc20/src/test/simulators/ERC20Simulator.ts +++ b/contracts/erc20/src/test/simulators/ERC20Simulator.ts @@ -8,16 +8,16 @@ import { } from '@midnight-ntwrk/compact-runtime'; import { sampleContractAddress } from '@midnight-ntwrk/zswap'; import { + type ContractAddress, + type Either, type Ledger, Contract as MockERC20, - ledger, - type Either, type ZswapCoinPublicKey, - type ContractAddress, + ledger, } from '../../artifacts/MockERC20/contract/index.cjs'; // Combined imports +import { type ERC20PrivateState, ERC20Witnesses } from '../../witnesses'; import type { MaybeString } from '../types'; import type { IContractSimulator } from './../types'; -import { type ERC20PrivateState, ERC20Witnesses } from '../../witnesses'; /** * @description A simulator implementation of an erc20 contract for testing purposes. diff --git a/contracts/erc20/src/test/utils/address.ts b/contracts/erc20/src/test/utils/address.ts index e775fee6..3580e196 100644 --- a/contracts/erc20/src/test/utils/address.ts +++ b/contracts/erc20/src/test/utils/address.ts @@ -1,9 +1,9 @@ -import { encodeContractAddress } from '@midnight-ntwrk/ledger'; -import type * as Compact from '../../artifacts/MockERC20/contract/index.cjs'; import { convert_bigint_to_Uint8Array, encodeCoinPublicKey, } from '@midnight-ntwrk/compact-runtime'; +import { encodeContractAddress } from '@midnight-ntwrk/ledger'; +import type * as Compact from '../../artifacts/MockERC20/contract/index.cjs'; const PREFIX_ADDRESS = '0200'; diff --git a/contracts/initializable/src/test/InitializableSimulator.ts b/contracts/initializable/src/test/InitializableSimulator.ts index 783b17f4..6388989f 100644 --- a/contracts/initializable/src/test/InitializableSimulator.ts +++ b/contracts/initializable/src/test/InitializableSimulator.ts @@ -2,19 +2,19 @@ import { type CircuitContext, type ContractState, QueryContext, - sampleContractAddress, constructorContext, + sampleContractAddress, } from '@midnight-ntwrk/compact-runtime'; import { - Contract as MockInitializable, type Ledger, + Contract as MockInitializable, ledger, } from '../artifacts/MockInitializable/contract/index.cjs'; -import type { IContractSimulator } from './types'; import { type InitializablePrivateState, InitializableWitnesses, } from '../witnesses'; +import type { IContractSimulator } from './types'; /** * @description A simulator implementation of an utils contract for testing purposes. diff --git a/contracts/initializable/src/test/initializable.test.ts b/contracts/initializable/src/test/initializable.test.ts index 96d5271e..2760fbbd 100644 --- a/contracts/initializable/src/test/initializable.test.ts +++ b/contracts/initializable/src/test/initializable.test.ts @@ -1,6 +1,6 @@ -import { it, describe, expect } from '@jest/globals'; -import { InitializableSimulator } from './InitializableSimulator.js'; +import { describe, expect, it } from '@jest/globals'; import { Initializable_STATE as STATE } from '../artifacts/MockInitializable/contract/index.cjs'; +import { InitializableSimulator } from './InitializableSimulator.js'; const contract = new InitializableSimulator(); diff --git a/contracts/utils/src/test/UtilsSimulator.ts b/contracts/utils/src/test/UtilsSimulator.ts index abaff99e..894058c9 100644 --- a/contracts/utils/src/test/UtilsSimulator.ts +++ b/contracts/utils/src/test/UtilsSimulator.ts @@ -6,15 +6,15 @@ import { } from '@midnight-ntwrk/compact-runtime'; import { sampleContractAddress } from '@midnight-ntwrk/zswap'; import { + type ContractAddress, + type Either, type Ledger, Contract as MockUtils, - ledger, - type Either, type ZswapCoinPublicKey, - type ContractAddress, + ledger, } from '../artifacts/MockUtils/contract/index.cjs'; // Combined imports -import type { IContractSimulator } from './types'; import { type UtilsPrivateState, UtilsWitnesses } from '../witnesses'; +import type { IContractSimulator } from './types'; /** * @description A simulator implementation of an utils contract for testing purposes. diff --git a/contracts/utils/src/test/utils/address.ts b/contracts/utils/src/test/utils/address.ts index 9690fd48..d4ac78a7 100644 --- a/contracts/utils/src/test/utils/address.ts +++ b/contracts/utils/src/test/utils/address.ts @@ -1,9 +1,9 @@ -import { encodeContractAddress } from '@midnight-ntwrk/ledger'; -import type * as Compact from '../../artifacts/MockUtils/contract/index.cjs'; import { convert_bigint_to_Uint8Array, encodeCoinPublicKey, } from '@midnight-ntwrk/compact-runtime'; +import { encodeContractAddress } from '@midnight-ntwrk/ledger'; +import type * as Compact from '../../artifacts/MockUtils/contract/index.cjs'; const PREFIX_ADDRESS = '0200'; From 24c2e784ecf8872e47df0d6d734d0eacc8e8b658 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 17 Apr 2025 02:31:42 -0500 Subject: [PATCH 007/322] remove dev deps from contracts/ --- contracts/erc20/package.json | 5 ----- contracts/initializable/package.json | 6 ------ contracts/utils/package.json | 5 ----- yarn.lock | 14 +------------- 4 files changed, 1 insertion(+), 29 deletions(-) diff --git a/contracts/erc20/package.json b/contracts/erc20/package.json index 77d0921d..67cbfe99 100644 --- a/contracts/erc20/package.json +++ b/contracts/erc20/package.json @@ -22,10 +22,5 @@ }, "dependencies": { "@openzeppelin-midnight-contracts/utils-contract": "workspace:^" - }, - "devDependencies": { - "@midnight-ntwrk/compact": "workspace:*", - "jest": "^29.7.0", - "typescript": "^5.2.2" } } diff --git a/contracts/initializable/package.json b/contracts/initializable/package.json index a04f1d3a..e6868ab2 100644 --- a/contracts/initializable/package.json +++ b/contracts/initializable/package.json @@ -19,11 +19,5 @@ "build": "rm -rf dist && tsc --project tsconfig.build.json && cp -Rf ./src/artifacts ./dist/artifacts && cp ./src/Initializable.compact ./dist", "lint": "eslint src", "typecheck": "tsc -p tsconfig.json --noEmit" - }, - "devDependencies": { - "@midnight-ntwrk/compact": "workspace:*", - "eslint": "^8.52.0", - "jest": "^29.7.0", - "typescript": "^5.2.2" } } diff --git a/contracts/utils/package.json b/contracts/utils/package.json index fc0fe810..3fdcfcf5 100644 --- a/contracts/utils/package.json +++ b/contracts/utils/package.json @@ -19,10 +19,5 @@ "build": "rm -rf dist && tsc --project tsconfig.build.json && cp -Rf ./src/artifacts ./dist/artifacts && cp ./src/Utils.compact ./dist", "lint": "eslint src", "typecheck": "tsc -p tsconfig.json --noEmit" - }, - "devDependencies": { - "@midnight-ntwrk/compact": "workspace:*", - "jest": "^29.7.0", - "typescript": "^5.2.2" } } diff --git a/yarn.lock b/yarn.lock index 4d7515ad..21ea5a9c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -901,7 +901,7 @@ __metadata: languageName: node linkType: hard -"@midnight-ntwrk/compact@workspace:*, @midnight-ntwrk/compact@workspace:compact": +"@midnight-ntwrk/compact@workspace:compact": version: 0.0.0-use.local resolution: "@midnight-ntwrk/compact@workspace:compact" dependencies: @@ -987,31 +987,19 @@ __metadata: version: 0.0.0-use.local resolution: "@openzeppelin-midnight-contracts/erc20-contract@workspace:contracts/erc20" dependencies: - "@midnight-ntwrk/compact": "workspace:*" "@openzeppelin-midnight-contracts/utils-contract": "workspace:^" - jest: "npm:^29.7.0" - typescript: "npm:^5.2.2" languageName: unknown linkType: soft "@openzeppelin-midnight-contracts/initializable-contract@workspace:contracts/initializable": version: 0.0.0-use.local resolution: "@openzeppelin-midnight-contracts/initializable-contract@workspace:contracts/initializable" - dependencies: - "@midnight-ntwrk/compact": "workspace:*" - eslint: "npm:^8.52.0" - jest: "npm:^29.7.0" - typescript: "npm:^5.2.2" languageName: unknown linkType: soft "@openzeppelin-midnight-contracts/utils-contract@workspace:^, @openzeppelin-midnight-contracts/utils-contract@workspace:contracts/utils": version: 0.0.0-use.local resolution: "@openzeppelin-midnight-contracts/utils-contract@workspace:contracts/utils" - dependencies: - "@midnight-ntwrk/compact": "workspace:*" - jest: "npm:^29.7.0" - typescript: "npm:^5.2.2" languageName: unknown linkType: soft From 82e64e508984c51826b055c36d3676b573130b72 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 17 Apr 2025 15:27:48 -0500 Subject: [PATCH 008/322] remove unused deps --- package.json | 9 +- yarn.lock | 992 +-------------------------------------------------- 2 files changed, 13 insertions(+), 988 deletions(-) diff --git a/package.json b/package.json index 18a86574..5d3ea810 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,6 @@ "packageManager": "yarn@4.1.0", "workspaces": [ "compact", - "contracts/erc20/", "contracts/initializable/", "contracts/utils/" ], @@ -12,12 +11,7 @@ "lint": "turbo run lint" }, "dependencies": { - "@midnight-ntwrk/compact-runtime": "^0.7.0", - "fp-ts": "^2.16.1", - "io-ts": "^2.2.20", - "pino": "^8.16.0", - "pino-pretty": "^10.2.3", - "rxjs": "^7.8.1" + "@midnight-ntwrk/compact-runtime": "^0.7.0" }, "devDependencies": { "@biomejs/biome": "1.9.4", @@ -31,7 +25,6 @@ "jest-gh-md-reporter": "^0.0.2", "jest-html-reporters": "^3.1.4", "jest-junit": "^16.0.0", - "testcontainers": "^10.3.2", "ts-jest": "^29.1.1", "ts-node": "^10.9.1", "turbo": "^1.10.16", diff --git a/yarn.lock b/yarn.lock index 21ea5a9c..2be4f8ea 100644 --- a/yarn.lock +++ b/yarn.lock @@ -377,13 +377,6 @@ __metadata: languageName: node linkType: hard -"@balena/dockerignore@npm:^1.0.2": - version: 1.0.2 - resolution: "@balena/dockerignore@npm:1.0.2" - checksum: 10/13d654fdd725008577d32e721c720275bdc48f72bce612326363d5bed449febbed856c517a0b23c7c40d87cb531e63432804550b4ecc13e365d26fee38fb6c8a - languageName: node - linkType: hard - "@bcoe/v8-coverage@npm:^0.2.3": version: 0.2.3 resolution: "@bcoe/v8-coverage@npm:0.2.3" @@ -533,13 +526,6 @@ __metadata: languageName: node linkType: hard -"@fastify/busboy@npm:^2.0.0": - version: 2.1.1 - resolution: "@fastify/busboy@npm:2.1.1" - checksum: 10/2bb8a7eca8289ed14c9eb15239bc1019797454624e769b39a0b90ed204d032403adc0f8ed0d2aef8a18c772205fa7808cf5a1b91f21c7bfc7b6032150b1062c5 - languageName: node - linkType: hard - "@humanwhocodes/config-array@npm:^0.13.0": version: 0.13.0 resolution: "@humanwhocodes/config-array@npm:0.13.0" @@ -983,21 +969,13 @@ __metadata: languageName: node linkType: hard -"@openzeppelin-midnight-contracts/erc20-contract@workspace:contracts/erc20": - version: 0.0.0-use.local - resolution: "@openzeppelin-midnight-contracts/erc20-contract@workspace:contracts/erc20" - dependencies: - "@openzeppelin-midnight-contracts/utils-contract": "workspace:^" - languageName: unknown - linkType: soft - "@openzeppelin-midnight-contracts/initializable-contract@workspace:contracts/initializable": version: 0.0.0-use.local resolution: "@openzeppelin-midnight-contracts/initializable-contract@workspace:contracts/initializable" languageName: unknown linkType: soft -"@openzeppelin-midnight-contracts/utils-contract@workspace:^, @openzeppelin-midnight-contracts/utils-contract@workspace:contracts/utils": +"@openzeppelin-midnight-contracts/utils-contract@workspace:contracts/utils": version: 0.0.0-use.local resolution: "@openzeppelin-midnight-contracts/utils-contract@workspace:contracts/utils" languageName: unknown @@ -1104,27 +1082,6 @@ __metadata: languageName: node linkType: hard -"@types/docker-modem@npm:*": - version: 3.0.6 - resolution: "@types/docker-modem@npm:3.0.6" - dependencies: - "@types/node": "npm:*" - "@types/ssh2": "npm:*" - checksum: 10/cc58e8189f6ec5a2b8ca890207402178a97ddac8c80d125dc65d8ab29034b5db736de15e99b91b2d74e66d14e26e73b6b8b33216613dd15fd3aa6b82c11a83ed - languageName: node - linkType: hard - -"@types/dockerode@npm:^3.3.29": - version: 3.3.35 - resolution: "@types/dockerode@npm:3.3.35" - dependencies: - "@types/docker-modem": "npm:*" - "@types/node": "npm:*" - "@types/ssh2": "npm:*" - checksum: 10/9b1bc6ffc032c5fd76564c4b2c80724eddcba4c0deb885105b811f0a843464f3152e44ea850d91b614f234e35fa70002aa7350d109517460a7fc339800833ade - languageName: node - linkType: hard - "@types/graceful-fs@npm:^4.1.3": version: 4.1.9 resolution: "@types/graceful-fs@npm:4.1.9" @@ -1178,7 +1135,7 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:^18.11.18, @types/node@npm:^18.18.6": +"@types/node@npm:^18.18.6": version: 18.19.78 resolution: "@types/node@npm:18.19.78" dependencies: @@ -1194,34 +1151,6 @@ __metadata: languageName: node linkType: hard -"@types/ssh2-streams@npm:*": - version: 0.1.12 - resolution: "@types/ssh2-streams@npm:0.1.12" - dependencies: - "@types/node": "npm:*" - checksum: 10/377bfff70e6c13e42f7bf832209c916b9a80491bba611c21f4cbdc8c9f99553794e5583ee933fd02bb1b056dd9b97433195452f119104f592a5a2440806f3087 - languageName: node - linkType: hard - -"@types/ssh2@npm:*": - version: 1.15.4 - resolution: "@types/ssh2@npm:1.15.4" - dependencies: - "@types/node": "npm:^18.11.18" - checksum: 10/a4d37e28bf81c6bc41c785d78ee0208163af86294411f9662097f72bf91bb14647d4786f7a01a5c8e74594cfc1ccedcf9495bfdfb5541f2262a2cf433c94c5d9 - languageName: node - linkType: hard - -"@types/ssh2@npm:^0.5.48": - version: 0.5.52 - resolution: "@types/ssh2@npm:0.5.52" - dependencies: - "@types/node": "npm:*" - "@types/ssh2-streams": "npm:*" - checksum: 10/fc2584af091da49da9d6628dd8a5e851b217bb9b1b732b0361903894f2730ab3fdf8634f954be34c5a513f7eb0b2772d059d64062bcf6b4a0eb73bfc83c4b858 - languageName: node - linkType: hard - "@types/stack-utils@npm:^2.0.0": version: 2.0.3 resolution: "@types/stack-utils@npm:2.0.3" @@ -1259,15 +1188,6 @@ __metadata: languageName: node linkType: hard -"abort-controller@npm:^3.0.0": - version: 3.0.0 - resolution: "abort-controller@npm:3.0.0" - dependencies: - event-target-shim: "npm:^5.0.0" - checksum: 10/ed84af329f1828327798229578b4fe03a4dd2596ba304083ebd2252666bdc1d7647d66d0b18704477e1f8aa315f055944aa6e859afebd341f12d0a53c37b4b40 - languageName: node - linkType: hard - "acorn-jsx@npm:^5.3.2": version: 5.3.2 resolution: "acorn-jsx@npm:5.3.2" @@ -1370,36 +1290,6 @@ __metadata: languageName: node linkType: hard -"archiver-utils@npm:^5.0.0, archiver-utils@npm:^5.0.2": - version: 5.0.2 - resolution: "archiver-utils@npm:5.0.2" - dependencies: - glob: "npm:^10.0.0" - graceful-fs: "npm:^4.2.0" - is-stream: "npm:^2.0.1" - lazystream: "npm:^1.0.0" - lodash: "npm:^4.17.15" - normalize-path: "npm:^3.0.0" - readable-stream: "npm:^4.0.0" - checksum: 10/9dde4aa3f0cb1bdfe0b3d4c969f82e6cca9ae76338b7fee6f0071a14a2a38c0cdd1c41ecd3e362466585aa6cc5d07e9e435abea8c94fd9c7ace35f184abef9e4 - languageName: node - linkType: hard - -"archiver@npm:^7.0.1": - version: 7.0.1 - resolution: "archiver@npm:7.0.1" - dependencies: - archiver-utils: "npm:^5.0.2" - async: "npm:^3.2.4" - buffer-crc32: "npm:^1.0.0" - readable-stream: "npm:^4.0.0" - readdir-glob: "npm:^1.1.2" - tar-stream: "npm:^3.0.0" - zip-stream: "npm:^6.0.1" - checksum: 10/81c6102db99d7ffd5cb2aed02a678f551c6603991a059ca66ef59249942b835a651a3d3b5240af4f8bec4e61e13790357c9d1ad4a99982bd2cc4149575c31d67 - languageName: node - linkType: hard - "arg@npm:^4.1.0": version: 4.1.3 resolution: "arg@npm:4.1.3" @@ -1423,43 +1313,13 @@ __metadata: languageName: node linkType: hard -"asn1@npm:^0.2.6": - version: 0.2.6 - resolution: "asn1@npm:0.2.6" - dependencies: - safer-buffer: "npm:~2.1.0" - checksum: 10/cf629291fee6c1a6f530549939433ebf32200d7849f38b810ff26ee74235e845c0c12b2ed0f1607ac17383d19b219b69cefa009b920dab57924c5c544e495078 - languageName: node - linkType: hard - -"async-lock@npm:^1.4.1": - version: 1.4.1 - resolution: "async-lock@npm:1.4.1" - checksum: 10/80d55ac95f920e880a865968b799963014f6d987dd790dd08173fae6e1af509d8cd0ab45a25daaca82e3ef8e7c939f5d128cd1facfcc5c647da8ac2409e20ef9 - languageName: node - linkType: hard - -"async@npm:^3.2.3, async@npm:^3.2.4": +"async@npm:^3.2.3": version: 3.2.6 resolution: "async@npm:3.2.6" checksum: 10/cb6e0561a3c01c4b56a799cc8bab6ea5fef45f069ab32500b6e19508db270ef2dffa55e5aed5865c5526e9907b1f8be61b27530823b411ffafb5e1538c86c368 languageName: node linkType: hard -"atomic-sleep@npm:^1.0.0": - version: 1.0.0 - resolution: "atomic-sleep@npm:1.0.0" - checksum: 10/3ab6d2cf46b31394b4607e935ec5c1c3c4f60f3e30f0913d35ea74b51b3585e84f590d09e58067f11762eec71c87d25314ce859030983dc0e4397eed21daa12e - languageName: node - linkType: hard - -"b4a@npm:^1.6.4": - version: 1.6.7 - resolution: "b4a@npm:1.6.7" - checksum: 10/1ac056e3bce378d4d3e570e57319360a9d3125ab6916a1921b95bea33d9ee646698ebc75467561fd6fcc80ff697612124c89bb9b95e80db94c6dc23fcb977705 - languageName: node - linkType: hard - "babel-jest@npm:^29.7.0": version: 29.7.0 resolution: "babel-jest@npm:29.7.0" @@ -1546,84 +1406,6 @@ __metadata: languageName: node linkType: hard -"bare-events@npm:^2.0.0, bare-events@npm:^2.2.0": - version: 2.5.4 - resolution: "bare-events@npm:2.5.4" - checksum: 10/135ef380b13f554ca2c6905bdbcfac8edae08fce85b7f953fa01f09a9f5b0da6a25e414111659bc9a6118216f0dd1f732016acd11ce91517f2afb26ebeb4b721 - languageName: node - linkType: hard - -"bare-fs@npm:^4.0.1": - version: 4.0.1 - resolution: "bare-fs@npm:4.0.1" - dependencies: - bare-events: "npm:^2.0.0" - bare-path: "npm:^3.0.0" - bare-stream: "npm:^2.0.0" - checksum: 10/70951cf7d7522f0b6780bdfaf7969226db85370fa107b1eee71c58272573463388b40203595a8826cd55ca34e6359ca4b1ee91fd5d0b8ea64ab0d1f9979de262 - languageName: node - linkType: hard - -"bare-os@npm:^3.0.1": - version: 3.5.1 - resolution: "bare-os@npm:3.5.1" - checksum: 10/ff65328cb83bf8ed1f527f1bf46ec0a9990d76575bc3ab464f0299f9e94fea551af7c044c1471967fc220be5f0ddd420e8580bc4e7a1be9a1a16bf944b45f89e - languageName: node - linkType: hard - -"bare-path@npm:^3.0.0": - version: 3.0.0 - resolution: "bare-path@npm:3.0.0" - dependencies: - bare-os: "npm:^3.0.1" - checksum: 10/712d90e9cd8c3263cc11b0e0d386d1531a452706d7840c081ee586b34b00d72544e65df7a40013d47c1b177277495225deeede65cb2984db88a979cb65aaa2ff - languageName: node - linkType: hard - -"bare-stream@npm:^2.0.0": - version: 2.6.5 - resolution: "bare-stream@npm:2.6.5" - dependencies: - streamx: "npm:^2.21.0" - peerDependencies: - bare-buffer: "*" - bare-events: "*" - peerDependenciesMeta: - bare-buffer: - optional: true - bare-events: - optional: true - checksum: 10/0f5ca2167fbbccc118157bce7c53a933e21726268e03d751461211550d72b2d01c296b767ccf96aae8ab28e106b126407c6fe0d29f915734b844ffe6057f0a08 - languageName: node - linkType: hard - -"base64-js@npm:^1.3.1": - version: 1.5.1 - resolution: "base64-js@npm:1.5.1" - checksum: 10/669632eb3745404c2f822a18fc3a0122d2f9a7a13f7fb8b5823ee19d1d2ff9ee5b52c53367176ea4ad093c332fd5ab4bd0ebae5a8e27917a4105a4cfc86b1005 - languageName: node - linkType: hard - -"bcrypt-pbkdf@npm:^1.0.2": - version: 1.0.2 - resolution: "bcrypt-pbkdf@npm:1.0.2" - dependencies: - tweetnacl: "npm:^0.14.3" - checksum: 10/13a4cde058250dbf1fa77a4f1b9a07d32ae2e3b9e28e88a0c7a1827835bc3482f3e478c4a0cfd4da6ff0c46dae07da1061123a995372b32cc563d9975f975404 - languageName: node - linkType: hard - -"bl@npm:^4.0.3": - version: 4.1.0 - resolution: "bl@npm:4.1.0" - dependencies: - buffer: "npm:^5.5.0" - inherits: "npm:^2.0.4" - readable-stream: "npm:^3.4.0" - checksum: 10/b7904e66ed0bdfc813c06ea6c3e35eafecb104369dbf5356d0f416af90c1546de3b74e5b63506f0629acf5e16a6f87c3798f16233dcff086e9129383aa02ab55 - languageName: node - linkType: hard - "brace-expansion@npm:^1.1.7": version: 1.1.11 resolution: "brace-expansion@npm:1.1.11" @@ -1684,13 +1466,6 @@ __metadata: languageName: node linkType: hard -"buffer-crc32@npm:^1.0.0": - version: 1.0.0 - resolution: "buffer-crc32@npm:1.0.0" - checksum: 10/ef3b7c07622435085c04300c9a51e850ec34a27b2445f758eef69b859c7827848c2282f3840ca6c1eef3829145a1580ce540cab03ccf4433827a2b95d3b09ca7 - languageName: node - linkType: hard - "buffer-from@npm:^1.0.0": version: 1.1.2 resolution: "buffer-from@npm:1.1.2" @@ -1698,40 +1473,6 @@ __metadata: languageName: node linkType: hard -"buffer@npm:^5.5.0": - version: 5.7.1 - resolution: "buffer@npm:5.7.1" - dependencies: - base64-js: "npm:^1.3.1" - ieee754: "npm:^1.1.13" - checksum: 10/997434d3c6e3b39e0be479a80288875f71cd1c07d75a3855e6f08ef848a3c966023f79534e22e415ff3a5112708ce06127277ab20e527146d55c84566405c7c6 - languageName: node - linkType: hard - -"buffer@npm:^6.0.3": - version: 6.0.3 - resolution: "buffer@npm:6.0.3" - dependencies: - base64-js: "npm:^1.3.1" - ieee754: "npm:^1.2.1" - checksum: 10/b6bc68237ebf29bdacae48ce60e5e28fc53ae886301f2ad9496618efac49427ed79096750033e7eab1897a4f26ae374ace49106a5758f38fb70c78c9fda2c3b1 - languageName: node - linkType: hard - -"buildcheck@npm:~0.0.6": - version: 0.0.6 - resolution: "buildcheck@npm:0.0.6" - checksum: 10/194ee8d3b0926fd6f3e799732130ad7ab194882c56900b8670ad43c81326f64871f49b7d9f1e9baad91ca3070eb4e8b678797fe9ae78cf87dde86d8916eb25d2 - languageName: node - linkType: hard - -"byline@npm:^5.0.0": - version: 5.0.0 - resolution: "byline@npm:5.0.0" - checksum: 10/737ca83e8eda2976728dae62e68bc733aea095fab08db4c6f12d3cee3cf45b6f97dce45d1f6b6ff9c2c947736d10074985b4425b31ce04afa1985a4ef3d334a7 - languageName: node - linkType: hard - "cacache@npm:^19.0.1": version: 19.0.1 resolution: "cacache@npm:19.0.1" @@ -1797,13 +1538,6 @@ __metadata: languageName: node linkType: hard -"chownr@npm:^1.1.1": - version: 1.1.4 - resolution: "chownr@npm:1.1.4" - checksum: 10/115648f8eb38bac5e41c3857f3e663f9c39ed6480d1349977c4d96c95a47266fcacc5a5aabf3cb6c481e22d72f41992827db47301851766c4fd77ac21a4f081d - languageName: node - linkType: hard - "chownr@npm:^3.0.0": version: 3.0.0 resolution: "chownr@npm:3.0.0" @@ -1866,26 +1600,6 @@ __metadata: languageName: node linkType: hard -"colorette@npm:^2.0.7": - version: 2.0.20 - resolution: "colorette@npm:2.0.20" - checksum: 10/0b8de48bfa5d10afc160b8eaa2b9938f34a892530b2f7d7897e0458d9535a066e3998b49da9d21161c78225b272df19ae3a64d6df28b4c9734c0e55bbd02406f - languageName: node - linkType: hard - -"compress-commons@npm:^6.0.2": - version: 6.0.2 - resolution: "compress-commons@npm:6.0.2" - dependencies: - crc-32: "npm:^1.2.0" - crc32-stream: "npm:^6.0.0" - is-stream: "npm:^2.0.1" - normalize-path: "npm:^3.0.0" - readable-stream: "npm:^4.0.0" - checksum: 10/78e3ba10aeef919a1c5bbac21e120f3e1558a31b2defebbfa1635274fc7f7e8a3a0ee748a06249589acd0b33a0d58144b8238ff77afc3220f8d403a96fcc13aa - languageName: node - linkType: hard - "concat-map@npm:0.0.1": version: 0.0.1 resolution: "concat-map@npm:0.0.1" @@ -1900,43 +1614,6 @@ __metadata: languageName: node linkType: hard -"core-util-is@npm:~1.0.0": - version: 1.0.3 - resolution: "core-util-is@npm:1.0.3" - checksum: 10/9de8597363a8e9b9952491ebe18167e3b36e7707569eed0ebf14f8bba773611376466ae34575bca8cfe3c767890c859c74056084738f09d4e4a6f902b2ad7d99 - languageName: node - linkType: hard - -"cpu-features@npm:~0.0.10": - version: 0.0.10 - resolution: "cpu-features@npm:0.0.10" - dependencies: - buildcheck: "npm:~0.0.6" - nan: "npm:^2.19.0" - node-gyp: "npm:latest" - checksum: 10/941b828ffe77582b2bdc03e894c913e2e2eeb5c6043ccb01338c34446d026f6888dc480ecb85e684809f9c3889d245f3648c7907eb61a92bdfc6aed039fcda8d - languageName: node - linkType: hard - -"crc-32@npm:^1.2.0": - version: 1.2.2 - resolution: "crc-32@npm:1.2.2" - bin: - crc32: bin/crc32.njs - checksum: 10/824f696a5baaf617809aa9cd033313c8f94f12d15ebffa69f10202480396be44aef9831d900ab291638a8022ed91c360696dd5b1ba691eb3f34e60be8835b7c3 - languageName: node - linkType: hard - -"crc32-stream@npm:^6.0.0": - version: 6.0.0 - resolution: "crc32-stream@npm:6.0.0" - dependencies: - crc-32: "npm:^1.2.0" - readable-stream: "npm:^4.0.0" - checksum: 10/e6edc2f81bc387daef6d18b2ac18c2ffcb01b554d3b5c7d8d29b177505aafffba574658fdd23922767e8dab1183d1962026c98c17e17fb272794c33293ef607c - languageName: node - linkType: hard - "create-jest@npm:^29.7.0": version: 29.7.0 resolution: "create-jest@npm:29.7.0" @@ -1972,14 +1649,7 @@ __metadata: languageName: node linkType: hard -"dateformat@npm:^4.6.3": - version: 4.6.3 - resolution: "dateformat@npm:4.6.3" - checksum: 10/5c149c91bf9ce2142c89f84eee4c585f0cb1f6faf2536b1af89873f862666a28529d1ccafc44750aa01384da2197c4f76f4e149a3cc0c1cb2c46f5cc45f2bcb5 - languageName: node - linkType: hard - -"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.3.5": +"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4": version: 4.4.0 resolution: "debug@npm:4.4.0" dependencies: @@ -2045,38 +1715,6 @@ __metadata: languageName: node linkType: hard -"docker-compose@npm:^0.24.8": - version: 0.24.8 - resolution: "docker-compose@npm:0.24.8" - dependencies: - yaml: "npm:^2.2.2" - checksum: 10/2b8526f9797a55c819ff2d7dcea57085b012b3a3d77bc2e1a6b45c3fc9e82196312f5298cbe8299966462454a5ac8f68814bb407736b4385e0d226a2a39e877a - languageName: node - linkType: hard - -"docker-modem@npm:^3.0.0": - version: 3.0.8 - resolution: "docker-modem@npm:3.0.8" - dependencies: - debug: "npm:^4.1.1" - readable-stream: "npm:^3.5.0" - split-ca: "npm:^1.0.1" - ssh2: "npm:^1.11.0" - checksum: 10/a731d057b3da5a9da3dd9aff7e25bc33f2d29f3e0af947bd823d1361350071afb5b7cb0582af5bf012b08fca356520685bcff87bfcba08e85725576b32f264a2 - languageName: node - linkType: hard - -"dockerode@npm:^3.3.5": - version: 3.3.5 - resolution: "dockerode@npm:3.3.5" - dependencies: - "@balena/dockerignore": "npm:^1.0.2" - docker-modem: "npm:^3.0.0" - tar-fs: "npm:~2.0.1" - checksum: 10/1748e8d96f88fe71bb165a4c05726904937f5863b69eaeb4a3c1bb3bbf66940c7bef13b349ff757dc43664b4367611aab76f35c1ba468f07dcbaba567e6acd88 - languageName: node - linkType: hard - "doctrine@npm:^3.0.0": version: 3.0.0 resolution: "doctrine@npm:3.0.0" @@ -2141,15 +1779,6 @@ __metadata: languageName: node linkType: hard -"end-of-stream@npm:^1.1.0, end-of-stream@npm:^1.4.1": - version: 1.4.4 - resolution: "end-of-stream@npm:1.4.4" - dependencies: - once: "npm:^1.4.0" - checksum: 10/530a5a5a1e517e962854a31693dbb5c0b2fc40b46dad2a56a2deec656ca040631124f4795823acc68238147805f8b021abbe221f4afed5ef3c8e8efc2024908b - languageName: node - linkType: hard - "env-paths@npm:^2.2.0": version: 2.2.1 resolution: "env-paths@npm:2.2.1" @@ -2312,20 +1941,6 @@ __metadata: languageName: node linkType: hard -"event-target-shim@npm:^5.0.0": - version: 5.0.1 - resolution: "event-target-shim@npm:5.0.1" - checksum: 10/49ff46c3a7facbad3decb31f597063e761785d7fdb3920d4989d7b08c97a61c2f51183e2f3a03130c9088df88d4b489b1b79ab632219901f184f85158508f4c8 - languageName: node - linkType: hard - -"events@npm:^3.3.0": - version: 3.3.0 - resolution: "events@npm:3.3.0" - checksum: 10/a3d47e285e28d324d7180f1e493961a2bbb4cad6412090e4dec114f4db1f5b560c7696ee8e758f55e23913ede856e3689cd3aa9ae13c56b5d8314cd3b3ddd1be - languageName: node - linkType: hard - "execa@npm:^5.0.0": version: 5.1.1 resolution: "execa@npm:5.1.1" @@ -2379,13 +1994,6 @@ __metadata: languageName: node linkType: hard -"fast-copy@npm:^3.0.0": - version: 3.0.2 - resolution: "fast-copy@npm:3.0.2" - checksum: 10/97e1022e2aaa27acf4a986d679310bfd66bfb87fe8da9dd33b698e3e50189484001cf1eeb9670e19b59d9d299828ed86c8da354c954f125995ab2a6331c5f290 - languageName: node - linkType: hard - "fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3": version: 3.1.3 resolution: "fast-deep-equal@npm:3.1.3" @@ -2393,13 +2001,6 @@ __metadata: languageName: node linkType: hard -"fast-fifo@npm:^1.2.0, fast-fifo@npm:^1.3.2": - version: 1.3.2 - resolution: "fast-fifo@npm:1.3.2" - checksum: 10/6bfcba3e4df5af7be3332703b69a7898a8ed7020837ec4395bb341bd96cc3a6d86c3f6071dd98da289618cf2234c70d84b2a6f09a33dd6f988b1ff60d8e54275 - languageName: node - linkType: hard - "fast-json-stable-stringify@npm:2.x, fast-json-stable-stringify@npm:^2.0.0, fast-json-stable-stringify@npm:^2.1.0": version: 2.1.0 resolution: "fast-json-stable-stringify@npm:2.1.0" @@ -2414,20 +2015,6 @@ __metadata: languageName: node linkType: hard -"fast-redact@npm:^3.1.1": - version: 3.5.0 - resolution: "fast-redact@npm:3.5.0" - checksum: 10/24b27e2023bd5a62f908d97a753b1adb8d89206b260f97727728e00b693197dea2fc2aa3711147a385d0ec6e713569fd533df37a4ef947e08cb65af3019c7ad5 - languageName: node - linkType: hard - -"fast-safe-stringify@npm:^2.1.1": - version: 2.1.1 - resolution: "fast-safe-stringify@npm:2.1.1" - checksum: 10/dc1f063c2c6ac9533aee14d406441f86783a8984b2ca09b19c2fe281f9ff59d315298bc7bc22fd1f83d26fe19ef2f20e2ddb68e96b15040292e555c5ced0c1e4 - languageName: node - linkType: hard - "fastq@npm:^1.6.0": version: 1.19.1 resolution: "fastq@npm:1.19.1" @@ -2521,20 +2108,6 @@ __metadata: languageName: node linkType: hard -"fp-ts@npm:^2.16.1": - version: 2.16.9 - resolution: "fp-ts@npm:2.16.9" - checksum: 10/af5c3fa829456da60ed63b5288907868f0df01a7215acfc5697f621a21f76b0a0f7b7c08ac81f2bcf7ecae13a6c6d41047e739a79cf239db19cd49afd7e8e015 - languageName: node - linkType: hard - -"fs-constants@npm:^1.0.0": - version: 1.0.0 - resolution: "fs-constants@npm:1.0.0" - checksum: 10/18f5b718371816155849475ac36c7d0b24d39a11d91348cfcb308b4494824413e03572c403c86d3a260e049465518c4f0d5bd00f0371cdfcad6d4f30a85b350d - languageName: node - linkType: hard - "fs-extra@npm:^10.0.0": version: 10.1.0 resolution: "fs-extra@npm:10.1.0" @@ -2609,13 +2182,6 @@ __metadata: languageName: node linkType: hard -"get-port@npm:^5.1.1": - version: 5.1.1 - resolution: "get-port@npm:5.1.1" - checksum: 10/0162663ffe5c09e748cd79d97b74cd70e5a5c84b760a475ce5767b357fb2a57cb821cee412d646aa8a156ed39b78aab88974eddaa9e5ee926173c036c0713787 - languageName: node - linkType: hard - "get-stream@npm:^6.0.0": version: 6.0.1 resolution: "get-stream@npm:6.0.1" @@ -2632,7 +2198,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^10.0.0, glob@npm:^10.2.2, glob@npm:^10.3.10, glob@npm:^10.3.7": +"glob@npm:^10.2.2, glob@npm:^10.3.10, glob@npm:^10.3.7": version: 10.4.5 resolution: "glob@npm:10.4.5" dependencies: @@ -2678,7 +2244,7 @@ __metadata: languageName: node linkType: hard -"graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": +"graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" checksum: 10/bf152d0ed1dc159239db1ba1f74fdbc40cb02f626770dcd5815c427ce0688c2635a06ed69af364396da4636d0408fcf7d4afdf7881724c3307e46aff30ca49e2 @@ -2708,13 +2274,6 @@ __metadata: languageName: node linkType: hard -"help-me@npm:^5.0.0": - version: 5.0.0 - resolution: "help-me@npm:5.0.0" - checksum: 10/5f99bd91dae93d02867175c3856c561d7e3a24f16999b08f5fc79689044b938d7ed58457f4d8c8744c01403e6e0470b7896baa344d112b2355842fd935a75d69 - languageName: node - linkType: hard - "html-escaper@npm:^2.0.0": version: 2.0.2 resolution: "html-escaper@npm:2.0.2" @@ -2765,13 +2324,6 @@ __metadata: languageName: node linkType: hard -"ieee754@npm:^1.1.13, ieee754@npm:^1.2.1": - version: 1.2.1 - resolution: "ieee754@npm:1.2.1" - checksum: 10/d9f2557a59036f16c282aaeb107832dc957a93d73397d89bbad4eb1130560560eb695060145e8e6b3b498b15ab95510226649a0b8f52ae06583575419fe10fc4 - languageName: node - linkType: hard - "ignore@npm:^5.2.0": version: 5.3.2 resolution: "ignore@npm:5.3.2" @@ -2818,22 +2370,13 @@ __metadata: languageName: node linkType: hard -"inherits@npm:2, inherits@npm:^2.0.3, inherits@npm:^2.0.4, inherits@npm:~2.0.3": +"inherits@npm:2": version: 2.0.4 resolution: "inherits@npm:2.0.4" checksum: 10/cd45e923bee15186c07fa4c89db0aace24824c482fb887b528304694b2aa6ff8a898da8657046a5dcf3e46cd6db6c61629551f9215f208d7c3f157cf9b290521 languageName: node linkType: hard -"io-ts@npm:^2.2.20": - version: 2.2.22 - resolution: "io-ts@npm:2.2.22" - peerDependencies: - fp-ts: ^2.5.0 - checksum: 10/c5eb8ca848f6e9586b5430773c62c8577902a6ca621349339e4d238c9ac4aba8df8de3e4d4317ff6593dcf38eb804445e0a5ba87afd7a2b8d29344ea9b6dc151 - languageName: node - linkType: hard - "ip-address@npm:^9.0.5": version: 9.0.5 resolution: "ip-address@npm:9.0.5" @@ -2913,7 +2456,7 @@ __metadata: languageName: node linkType: hard -"is-stream@npm:^2.0.0, is-stream@npm:^2.0.1": +"is-stream@npm:^2.0.0": version: 2.0.1 resolution: "is-stream@npm:2.0.1" checksum: 10/b8e05ccdf96ac330ea83c12450304d4a591f9958c11fd17bed240af8d5ffe08aedafa4c0f4cfccd4d28dc9d4d129daca1023633d5c11601a6cbc77521f6fae66 @@ -2929,13 +2472,6 @@ __metadata: languageName: node linkType: hard -"isarray@npm:~1.0.0": - version: 1.0.0 - resolution: "isarray@npm:1.0.0" - checksum: 10/f032df8e02dce8ec565cf2eb605ea939bdccea528dbcf565cdf92bfa2da9110461159d86a537388ef1acef8815a330642d7885b29010e8f7eac967c9993b65ab - languageName: node - linkType: hard - "isexe@npm:^2.0.0": version: 2.0.0 resolution: "isexe@npm:2.0.0" @@ -3522,13 +3058,6 @@ __metadata: languageName: node linkType: hard -"joycon@npm:^3.1.1": - version: 3.1.1 - resolution: "joycon@npm:3.1.1" - checksum: 10/4b36e3479144ec196425f46b3618f8a96ce7e1b658f091a309cd4906215f5b7a402d7df331a3e0a09681381a658d0c5f039cb3cf6907e0a1e17ed847f5d37775 - languageName: node - linkType: hard - "js-tokens@npm:^4.0.0": version: 4.0.0 resolution: "js-tokens@npm:4.0.0" @@ -3641,15 +3170,6 @@ __metadata: languageName: node linkType: hard -"lazystream@npm:^1.0.0": - version: 1.0.1 - resolution: "lazystream@npm:1.0.1" - dependencies: - readable-stream: "npm:^2.0.5" - checksum: 10/35f8cf8b5799c76570b211b079d4d706a20cbf13a4936d44cc7dbdacab1de6b346ab339ed3e3805f4693155ee5bbebbda4050fa2b666d61956e89a573089e3d4 - languageName: node - linkType: hard - "leven@npm:^3.1.0": version: 3.1.0 resolution: "leven@npm:3.1.0" @@ -3706,13 +3226,6 @@ __metadata: languageName: node linkType: hard -"lodash@npm:^4.17.15": - version: 4.17.21 - resolution: "lodash@npm:4.17.21" - checksum: 10/c08619c038846ea6ac754abd6dd29d2568aa705feb69339e836dfa8d8b09abbb2f859371e86863eda41848221f9af43714491467b5b0299122431e202bb0c532 - languageName: node - linkType: hard - "lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0": version: 10.4.3 resolution: "lru-cache@npm:10.4.3" @@ -3806,7 +3319,7 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^5.0.1, minimatch@npm:^5.1.0": +"minimatch@npm:^5.0.1": version: 5.1.6 resolution: "minimatch@npm:5.1.6" dependencies: @@ -3824,13 +3337,6 @@ __metadata: languageName: node linkType: hard -"minimist@npm:^1.2.6": - version: 1.2.8 - resolution: "minimist@npm:1.2.8" - checksum: 10/908491b6cc15a6c440ba5b22780a0ba89b9810e1aea684e253e43c4e3b8d56ec1dcdd7ea96dde119c29df59c936cde16062159eae4225c691e19c70b432b6e6f - languageName: node - linkType: hard - "minipass-collect@npm:^2.0.1": version: 2.0.1 resolution: "minipass-collect@npm:2.0.1" @@ -3908,13 +3414,6 @@ __metadata: languageName: node linkType: hard -"mkdirp-classic@npm:^0.5.2": - version: 0.5.3 - resolution: "mkdirp-classic@npm:0.5.3" - checksum: 10/3f4e088208270bbcc148d53b73e9a5bd9eef05ad2cbf3b3d0ff8795278d50dd1d11a8ef1875ff5aea3fa888931f95bfcb2ad5b7c1061cfefd6284d199e6776ac - languageName: node - linkType: hard - "mkdirp@npm:^1.0.4": version: 1.0.4 resolution: "mkdirp@npm:1.0.4" @@ -3940,15 +3439,6 @@ __metadata: languageName: node linkType: hard -"nan@npm:^2.19.0, nan@npm:^2.20.0": - version: 2.22.2 - resolution: "nan@npm:2.22.2" - dependencies: - node-gyp: "npm:latest" - checksum: 10/bee49de633650213970596ffbdf036bfe2109ff283a40f7742c3aa6d1fc15b9836f62bfee82192b879f56ab5f9fa9a1e5c58a908a50e5c87d91fb2118ef70827 - languageName: node - linkType: hard - "natural-compare@npm:^1.4.0": version: 1.4.0 resolution: "natural-compare@npm:1.4.0" @@ -4031,14 +3521,7 @@ __metadata: languageName: node linkType: hard -"on-exit-leak-free@npm:^2.1.0": - version: 2.1.2 - resolution: "on-exit-leak-free@npm:2.1.2" - checksum: 10/f7b4b7200026a08f6e4a17ba6d72e6c5cbb41789ed9cf7deaf9d9e322872c7dc5a7898549a894651ee0ee9ae635d34a678115bf8acdfba8ebd2ba2af688b563c - languageName: node - linkType: hard - -"once@npm:^1.3.0, once@npm:^1.3.1, once@npm:^1.4.0": +"once@npm:^1.3.0": version: 1.4.0 resolution: "once@npm:1.4.0" dependencies: @@ -4211,68 +3694,6 @@ __metadata: languageName: node linkType: hard -"pino-abstract-transport@npm:^1.0.0, pino-abstract-transport@npm:^1.2.0": - version: 1.2.0 - resolution: "pino-abstract-transport@npm:1.2.0" - dependencies: - readable-stream: "npm:^4.0.0" - split2: "npm:^4.0.0" - checksum: 10/6ec1d19a7ff3347fd21576f744c31c3e38ca4463ae638818408f43698c936f96be6a0bc750af5f7c1ae81873183bfcb062b7a0d12dc159a1813ea900c388c693 - languageName: node - linkType: hard - -"pino-pretty@npm:^10.2.3": - version: 10.3.1 - resolution: "pino-pretty@npm:10.3.1" - dependencies: - colorette: "npm:^2.0.7" - dateformat: "npm:^4.6.3" - fast-copy: "npm:^3.0.0" - fast-safe-stringify: "npm:^2.1.1" - help-me: "npm:^5.0.0" - joycon: "npm:^3.1.1" - minimist: "npm:^1.2.6" - on-exit-leak-free: "npm:^2.1.0" - pino-abstract-transport: "npm:^1.0.0" - pump: "npm:^3.0.0" - readable-stream: "npm:^4.0.0" - secure-json-parse: "npm:^2.4.0" - sonic-boom: "npm:^3.0.0" - strip-json-comments: "npm:^3.1.1" - bin: - pino-pretty: bin.js - checksum: 10/4284f125f7e8a5a10e856c8fd591ba34c30c0a0071a0b265a9eda43c3e447ba11d40b06cc67108675586358a5d1213a6ac3a92f6abd2896abfbab9a5b4c17072 - languageName: node - linkType: hard - -"pino-std-serializers@npm:^6.0.0": - version: 6.2.2 - resolution: "pino-std-serializers@npm:6.2.2" - checksum: 10/a00cdff4e1fbc206da9bed047e6dc400b065f43e8b4cef1635b0192feab0e8f932cdeb0faaa38a5d93d2e777ba4cda939c2ed4c1a70f6839ff25f9aef97c27ff - languageName: node - linkType: hard - -"pino@npm:^8.16.0": - version: 8.21.0 - resolution: "pino@npm:8.21.0" - dependencies: - atomic-sleep: "npm:^1.0.0" - fast-redact: "npm:^3.1.1" - on-exit-leak-free: "npm:^2.1.0" - pino-abstract-transport: "npm:^1.2.0" - pino-std-serializers: "npm:^6.0.0" - process-warning: "npm:^3.0.0" - quick-format-unescaped: "npm:^4.0.3" - real-require: "npm:^0.2.0" - safe-stable-stringify: "npm:^2.3.1" - sonic-boom: "npm:^3.7.0" - thread-stream: "npm:^2.6.0" - bin: - pino: bin.js - checksum: 10/5a054eab533ab91b20f63497b86070f0a6b40e4688cde9de66d23e03d6046c4e95d69c3f526dea9f30bcbc5874c7fbf0f91660cded4753946fd02261ca8ac340 - languageName: node - linkType: hard - "pirates@npm:^4.0.4": version: 4.0.6 resolution: "pirates@npm:4.0.6" @@ -4314,27 +3735,6 @@ __metadata: languageName: node linkType: hard -"process-nextick-args@npm:~2.0.0": - version: 2.0.1 - resolution: "process-nextick-args@npm:2.0.1" - checksum: 10/1d38588e520dab7cea67cbbe2efdd86a10cc7a074c09657635e34f035277b59fbb57d09d8638346bf7090f8e8ebc070c96fa5fd183b777fff4f5edff5e9466cf - languageName: node - linkType: hard - -"process-warning@npm:^3.0.0": - version: 3.0.0 - resolution: "process-warning@npm:3.0.0" - checksum: 10/2d82fa641e50a5789eaf0f2b33453760996e373d4591aac576a22d696186ab7e240a0592db86c264d4f28a46c2abbe9b94689752017db7dadc90f169f12b0924 - languageName: node - linkType: hard - -"process@npm:^0.11.10": - version: 0.11.10 - resolution: "process@npm:0.11.10" - checksum: 10/dbaa7e8d1d5cf375c36963ff43116772a989ef2bb47c9bdee20f38fd8fc061119cf38140631cf90c781aca4d3f0f0d2c834711952b728953f04fd7d238f59f5b - languageName: node - linkType: hard - "promise-retry@npm:^2.0.1": version: 2.0.1 resolution: "promise-retry@npm:2.0.1" @@ -4355,36 +3755,6 @@ __metadata: languageName: node linkType: hard -"proper-lockfile@npm:^4.1.2": - version: 4.1.2 - resolution: "proper-lockfile@npm:4.1.2" - dependencies: - graceful-fs: "npm:^4.2.4" - retry: "npm:^0.12.0" - signal-exit: "npm:^3.0.2" - checksum: 10/000a4875f543f591872b36ca94531af8a6463ddb0174f41c0b004d19e231d7445268b422ff1ea595e43d238655c702250cd3d27f408e7b9d97b56f1533ba26bf - languageName: node - linkType: hard - -"properties-reader@npm:^2.3.0": - version: 2.3.0 - resolution: "properties-reader@npm:2.3.0" - dependencies: - mkdirp: "npm:^1.0.4" - checksum: 10/0b41eb4136dc278ae0d97968ccce8de2d48d321655b319192e31f2424f1c6e052182204671e65aa8967216360cb3e7cbd9129830062e058fe9d6a1d74964c29a - languageName: node - linkType: hard - -"pump@npm:^3.0.0": - version: 3.0.2 - resolution: "pump@npm:3.0.2" - dependencies: - end-of-stream: "npm:^1.1.0" - once: "npm:^1.3.1" - checksum: 10/e0c4216874b96bd25ddf31a0b61a5613e26cc7afa32379217cf39d3915b0509def3565f5f6968fafdad2894c8bbdbd67d340e84f3634b2a29b950cffb6442d9f - languageName: node - linkType: hard - "punycode@npm:^2.1.0": version: 2.3.1 resolution: "punycode@npm:2.3.1" @@ -4406,13 +3776,6 @@ __metadata: languageName: node linkType: hard -"quick-format-unescaped@npm:^4.0.3": - version: 4.0.4 - resolution: "quick-format-unescaped@npm:4.0.4" - checksum: 10/591eca457509a99368b623db05248c1193aa3cedafc9a077d7acab09495db1231017ba3ad1b5386e5633271edd0a03b312d8640a59ee585b8516a42e15438aa7 - languageName: node - linkType: hard - "react-is@npm:^18.0.0": version: 18.3.1 resolution: "react-is@npm:18.3.1" @@ -4420,61 +3783,6 @@ __metadata: languageName: node linkType: hard -"readable-stream@npm:^2.0.5": - version: 2.3.8 - resolution: "readable-stream@npm:2.3.8" - dependencies: - core-util-is: "npm:~1.0.0" - inherits: "npm:~2.0.3" - isarray: "npm:~1.0.0" - process-nextick-args: "npm:~2.0.0" - safe-buffer: "npm:~5.1.1" - string_decoder: "npm:~1.1.1" - util-deprecate: "npm:~1.0.1" - checksum: 10/8500dd3a90e391d6c5d889256d50ec6026c059fadee98ae9aa9b86757d60ac46fff24fafb7a39fa41d54cb39d8be56cc77be202ebd4cd8ffcf4cb226cbaa40d4 - languageName: node - linkType: hard - -"readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0, readable-stream@npm:^3.5.0": - version: 3.6.2 - resolution: "readable-stream@npm:3.6.2" - dependencies: - inherits: "npm:^2.0.3" - string_decoder: "npm:^1.1.1" - util-deprecate: "npm:^1.0.1" - checksum: 10/d9e3e53193adcdb79d8f10f2a1f6989bd4389f5936c6f8b870e77570853561c362bee69feca2bbb7b32368ce96a85504aa4cedf7cf80f36e6a9de30d64244048 - languageName: node - linkType: hard - -"readable-stream@npm:^4.0.0": - version: 4.7.0 - resolution: "readable-stream@npm:4.7.0" - dependencies: - abort-controller: "npm:^3.0.0" - buffer: "npm:^6.0.3" - events: "npm:^3.3.0" - process: "npm:^0.11.10" - string_decoder: "npm:^1.3.0" - checksum: 10/bdf096c8ff59452ce5d08f13da9597f9fcfe400b4facfaa88e74ec057e5ad1fdfa140ffe28e5ed806cf4d2055f0b812806e962bca91dce31bc4cef08e53be3a4 - languageName: node - linkType: hard - -"readdir-glob@npm:^1.1.2": - version: 1.1.3 - resolution: "readdir-glob@npm:1.1.3" - dependencies: - minimatch: "npm:^5.1.0" - checksum: 10/ca3a20aa1e715d671302d4ec785a32bf08e59d6d0dd25d5fc03e9e5a39f8c612cdf809ab3e638a79973db7ad6868492edf38504701e313328e767693671447d6 - languageName: node - linkType: hard - -"real-require@npm:^0.2.0": - version: 0.2.0 - resolution: "real-require@npm:0.2.0" - checksum: 10/ddf44ee76301c774e9c9f2826da8a3c5c9f8fc87310f4a364e803ef003aa1a43c378b4323051ced212097fff1af459070f4499338b36a7469df1d4f7e8c0ba4c - languageName: node - linkType: hard - "require-directory@npm:^2.1.1": version: 2.1.1 resolution: "require-directory@npm:2.1.1" @@ -4585,17 +3893,11 @@ __metadata: "@types/jest": "npm:^29.5.6" "@types/node": "npm:^18.18.6" fast-check: "npm:^3.15.0" - fp-ts: "npm:^2.16.1" - io-ts: "npm:^2.2.20" jest: "npm:^29.7.0" jest-fast-check: "npm:^2.0.0" jest-gh-md-reporter: "npm:^0.0.2" jest-html-reporters: "npm:^3.1.4" jest-junit: "npm:^16.0.0" - pino: "npm:^8.16.0" - pino-pretty: "npm:^10.2.3" - rxjs: "npm:^7.8.1" - testcontainers: "npm:^10.3.2" ts-jest: "npm:^29.1.1" ts-node: "npm:^10.9.1" turbo: "npm:^1.10.16" @@ -4612,50 +3914,13 @@ __metadata: languageName: node linkType: hard -"rxjs@npm:^7.8.1": - version: 7.8.2 - resolution: "rxjs@npm:7.8.2" - dependencies: - tslib: "npm:^2.1.0" - checksum: 10/03dff09191356b2b87d94fbc1e97c4e9eb3c09d4452399dddd451b09c2f1ba8d56925a40af114282d7bc0c6fe7514a2236ca09f903cf70e4bbf156650dddb49d - languageName: node - linkType: hard - -"safe-buffer@npm:~5.1.0, safe-buffer@npm:~5.1.1": - version: 5.1.2 - resolution: "safe-buffer@npm:5.1.2" - checksum: 10/7eb5b48f2ed9a594a4795677d5a150faa7eb54483b2318b568dc0c4fc94092a6cce5be02c7288a0500a156282f5276d5688bce7259299568d1053b2150ef374a - languageName: node - linkType: hard - -"safe-buffer@npm:~5.2.0": - version: 5.2.1 - resolution: "safe-buffer@npm:5.2.1" - checksum: 10/32872cd0ff68a3ddade7a7617b8f4c2ae8764d8b7d884c651b74457967a9e0e886267d3ecc781220629c44a865167b61c375d2da6c720c840ecd73f45d5d9451 - languageName: node - linkType: hard - -"safe-stable-stringify@npm:^2.3.1": - version: 2.5.0 - resolution: "safe-stable-stringify@npm:2.5.0" - checksum: 10/2697fa186c17c38c3ca5309637b4ac6de2f1c3d282da27cd5e1e3c88eca0fb1f9aea568a6aabdf284111592c8782b94ee07176f17126031be72ab1313ed46c5c - languageName: node - linkType: hard - -"safer-buffer@npm:>= 2.1.2 < 3.0.0, safer-buffer@npm:~2.1.0": +"safer-buffer@npm:>= 2.1.2 < 3.0.0": version: 2.1.2 resolution: "safer-buffer@npm:2.1.2" checksum: 10/7eaf7a0cf37cc27b42fb3ef6a9b1df6e93a1c6d98c6c6702b02fe262d5fcbd89db63320793b99b21cb5348097d0a53de81bd5f4e8b86e20cc9412e3f1cfb4e83 languageName: node linkType: hard -"secure-json-parse@npm:^2.4.0": - version: 2.7.0 - resolution: "secure-json-parse@npm:2.7.0" - checksum: 10/974386587060b6fc5b1ac06481b2f9dbbb0d63c860cc73dc7533f27835fdb67b0ef08762dbfef25625c15bc0a0c366899e00076cb0d556af06b71e22f1dede4c - languageName: node - linkType: hard - "semver@npm:^6.3.0, semver@npm:^6.3.1": version: 6.3.1 resolution: "semver@npm:6.3.1" @@ -4690,7 +3955,7 @@ __metadata: languageName: node linkType: hard -"signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.3, signal-exit@npm:^3.0.7": +"signal-exit@npm:^3.0.3, signal-exit@npm:^3.0.7": version: 3.0.7 resolution: "signal-exit@npm:3.0.7" checksum: 10/a2f098f247adc367dffc27845853e9959b9e88b01cb301658cfe4194352d8d2bb32e18467c786a7fe15f1d44b233ea35633d076d5e737870b7139949d1ab6318 @@ -4746,15 +4011,6 @@ __metadata: languageName: node linkType: hard -"sonic-boom@npm:^3.0.0, sonic-boom@npm:^3.7.0": - version: 3.8.1 - resolution: "sonic-boom@npm:3.8.1" - dependencies: - atomic-sleep: "npm:^1.0.0" - checksum: 10/e03c9611e43fa81132cd2ce0fe4eb7fbcf19db267e9dec20dc6c586f82465c9c906e91a02f72150c740463ad9335536ea2131850307aaa6686d1fb5d4cc4be3e - languageName: node - linkType: hard - "source-map-support@npm:0.5.13": version: 0.5.13 resolution: "source-map-support@npm:0.5.13" @@ -4772,20 +4028,6 @@ __metadata: languageName: node linkType: hard -"split-ca@npm:^1.0.1": - version: 1.0.1 - resolution: "split-ca@npm:1.0.1" - checksum: 10/1e7409938a95ee843fe2593156a5735e6ee63772748ee448ea8477a5a3e3abde193c3325b3696e56a5aff07c7dcf6b1f6a2f2a036895b4f3afe96abb366d893f - languageName: node - linkType: hard - -"split2@npm:^4.0.0": - version: 4.2.0 - resolution: "split2@npm:4.2.0" - checksum: 10/09bbefc11bcf03f044584c9764cd31a252d8e52cea29130950b26161287c11f519807c5e54bd9e5804c713b79c02cefe6a98f4688630993386be353e03f534ab - languageName: node - linkType: hard - "sprintf-js@npm:^1.1.3": version: 1.1.3 resolution: "sprintf-js@npm:1.1.3" @@ -4800,33 +4042,6 @@ __metadata: languageName: node linkType: hard -"ssh-remote-port-forward@npm:^1.0.4": - version: 1.0.4 - resolution: "ssh-remote-port-forward@npm:1.0.4" - dependencies: - "@types/ssh2": "npm:^0.5.48" - ssh2: "npm:^1.4.0" - checksum: 10/c6c04c5ddfde7cb06e9a8655a152bd28fe6771c6fe62ff0bc08be229491546c410f30b153c968b8d6817a57d38678a270c228f30143ec0fe1be546efc4f6b65a - languageName: node - linkType: hard - -"ssh2@npm:^1.11.0, ssh2@npm:^1.4.0": - version: 1.16.0 - resolution: "ssh2@npm:1.16.0" - dependencies: - asn1: "npm:^0.2.6" - bcrypt-pbkdf: "npm:^1.0.2" - cpu-features: "npm:~0.0.10" - nan: "npm:^2.20.0" - dependenciesMeta: - cpu-features: - optional: true - nan: - optional: true - checksum: 10/0951c22d9c5a0e3b89a8e5ae890ebcbce9f1f94dbed37d1490e4e48e26bc8b074fa81f202ee57b708e31b5f33033f4c870b92047f4f02b6bc26c32225b01d84c - languageName: node - linkType: hard - "ssri@npm:^12.0.0": version: 12.0.0 resolution: "ssri@npm:12.0.0" @@ -4845,20 +4060,6 @@ __metadata: languageName: node linkType: hard -"streamx@npm:^2.15.0, streamx@npm:^2.21.0": - version: 2.22.0 - resolution: "streamx@npm:2.22.0" - dependencies: - bare-events: "npm:^2.2.0" - fast-fifo: "npm:^1.3.2" - text-decoder: "npm:^1.1.0" - dependenciesMeta: - bare-events: - optional: true - checksum: 10/9c329bb316e2085e207e471ecd0da18b4ed5b1cfe5cf10e9e7fad3f8f50c6ca1a6a844bdfd9bc7521560b97f229890de82ca162a0e66115300b91a489b1cbefd - languageName: node - linkType: hard - "string-length@npm:^4.0.1": version: 4.0.2 resolution: "string-length@npm:4.0.2" @@ -4891,24 +4092,6 @@ __metadata: languageName: node linkType: hard -"string_decoder@npm:^1.1.1, string_decoder@npm:^1.3.0": - version: 1.3.0 - resolution: "string_decoder@npm:1.3.0" - dependencies: - safe-buffer: "npm:~5.2.0" - checksum: 10/54d23f4a6acae0e93f999a585e673be9e561b65cd4cca37714af1e893ab8cd8dfa52a9e4f58f48f87b4a44918d3a9254326cb80ed194bf2e4c226e2b21767e56 - languageName: node - linkType: hard - -"string_decoder@npm:~1.1.1": - version: 1.1.1 - resolution: "string_decoder@npm:1.1.1" - dependencies: - safe-buffer: "npm:~5.1.0" - checksum: 10/7c41c17ed4dea105231f6df208002ebddd732e8e9e2d619d133cecd8e0087ddfd9587d2feb3c8caf3213cbd841ada6d057f5142cae68a4e62d3540778d9819b4 - languageName: node - linkType: hard - "strip-ansi-cjs@npm:strip-ansi@^6.0.1, strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": version: 6.0.1 resolution: "strip-ansi@npm:6.0.1" @@ -4973,59 +4156,6 @@ __metadata: languageName: node linkType: hard -"tar-fs@npm:^3.0.6": - version: 3.0.8 - resolution: "tar-fs@npm:3.0.8" - dependencies: - bare-fs: "npm:^4.0.1" - bare-path: "npm:^3.0.0" - pump: "npm:^3.0.0" - tar-stream: "npm:^3.1.5" - dependenciesMeta: - bare-fs: - optional: true - bare-path: - optional: true - checksum: 10/fdcd1c66dc5e2cad5544ffe7eab9a470b419290b22300c344688df51bf06127963da07a1e3ae23cae80851cd9f60149e80b38e56485dd7a14aea701241ac2f81 - languageName: node - linkType: hard - -"tar-fs@npm:~2.0.1": - version: 2.0.1 - resolution: "tar-fs@npm:2.0.1" - dependencies: - chownr: "npm:^1.1.1" - mkdirp-classic: "npm:^0.5.2" - pump: "npm:^3.0.0" - tar-stream: "npm:^2.0.0" - checksum: 10/85ceac6fce0e9175b5b67c0eca8864b7d29a940cae8b7657c60b66e8a252319d701c3df12814162a6839e6120f9e1975757293bdeaf294ad5b15721d236c4d32 - languageName: node - linkType: hard - -"tar-stream@npm:^2.0.0": - version: 2.2.0 - resolution: "tar-stream@npm:2.2.0" - dependencies: - bl: "npm:^4.0.3" - end-of-stream: "npm:^1.4.1" - fs-constants: "npm:^1.0.0" - inherits: "npm:^2.0.3" - readable-stream: "npm:^3.1.1" - checksum: 10/1a52a51d240c118cbcd30f7368ea5e5baef1eac3e6b793fb1a41e6cd7319296c79c0264ccc5859f5294aa80f8f00b9239d519e627b9aade80038de6f966fec6a - languageName: node - linkType: hard - -"tar-stream@npm:^3.0.0, tar-stream@npm:^3.1.5": - version: 3.1.7 - resolution: "tar-stream@npm:3.1.7" - dependencies: - b4a: "npm:^1.6.4" - fast-fifo: "npm:^1.2.0" - streamx: "npm:^2.15.0" - checksum: 10/b21a82705a72792544697c410451a4846af1f744176feb0ff11a7c3dd0896961552e3def5e1c9a6bbee4f0ae298b8252a1f4c9381e9f991553b9e4847976f05c - languageName: node - linkType: hard - "tar@npm:^7.4.3": version: 7.4.3 resolution: "tar@npm:7.4.3" @@ -5051,38 +4181,6 @@ __metadata: languageName: node linkType: hard -"testcontainers@npm:^10.3.2": - version: 10.18.0 - resolution: "testcontainers@npm:10.18.0" - dependencies: - "@balena/dockerignore": "npm:^1.0.2" - "@types/dockerode": "npm:^3.3.29" - archiver: "npm:^7.0.1" - async-lock: "npm:^1.4.1" - byline: "npm:^5.0.0" - debug: "npm:^4.3.5" - docker-compose: "npm:^0.24.8" - dockerode: "npm:^3.3.5" - get-port: "npm:^5.1.1" - proper-lockfile: "npm:^4.1.2" - properties-reader: "npm:^2.3.0" - ssh-remote-port-forward: "npm:^1.0.4" - tar-fs: "npm:^3.0.6" - tmp: "npm:^0.2.3" - undici: "npm:^5.28.5" - checksum: 10/c15ab1071bcfb4c5713b299184c8431001c05c10a1fd6439e480a50b0721badd641939b1b09a6950d4de2f1ad68450afb824af0feaaf2e96afb41a282e804b88 - languageName: node - linkType: hard - -"text-decoder@npm:^1.1.0": - version: 1.2.3 - resolution: "text-decoder@npm:1.2.3" - dependencies: - b4a: "npm:^1.6.4" - checksum: 10/bcdec33c0f070aeac38e46e4cafdcd567a58473ed308bdf75260bfbd8f7dc76acbc0b13226afaec4a169d0cb44cec2ab89c57b6395ccf02e941eaebbe19e124a - languageName: node - linkType: hard - "text-table@npm:^0.2.0": version: 0.2.0 resolution: "text-table@npm:0.2.0" @@ -5090,22 +4188,6 @@ __metadata: languageName: node linkType: hard -"thread-stream@npm:^2.6.0": - version: 2.7.0 - resolution: "thread-stream@npm:2.7.0" - dependencies: - real-require: "npm:^0.2.0" - checksum: 10/03e743a2ccb2af5fa695d2e4369113336ee9b9f09c4453d50a222cbb4ae3af321bff658e0e5bf8bfbce9d7f5a7bf6262d12a2a365e160f4e76380ec624d32e7b - languageName: node - linkType: hard - -"tmp@npm:^0.2.3": - version: 0.2.3 - resolution: "tmp@npm:0.2.3" - checksum: 10/7b13696787f159c9754793a83aa79a24f1522d47b87462ddb57c18ee93ff26c74cbb2b8d9138f571d2e0e765c728fb2739863a672b280528512c6d83d511c6fa - languageName: node - linkType: hard - "tmpl@npm:1.0.5": version: 1.0.5 resolution: "tmpl@npm:1.0.5" @@ -5197,13 +4279,6 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.1.0": - version: 2.8.1 - resolution: "tslib@npm:2.8.1" - checksum: 10/3e2e043d5c2316461cb54e5c7fe02c30ef6dccb3384717ca22ae5c6b5bc95232a6241df19c622d9c73b809bea33b187f6dbc73030963e29950c2141bc32a79f7 - languageName: node - linkType: hard - "turbo-darwin-64@npm:1.13.4": version: 1.13.4 resolution: "turbo-darwin-64@npm:1.13.4" @@ -5275,13 +4350,6 @@ __metadata: languageName: node linkType: hard -"tweetnacl@npm:^0.14.3": - version: 0.14.5 - resolution: "tweetnacl@npm:0.14.5" - checksum: 10/04ee27901cde46c1c0a64b9584e04c96c5fe45b38c0d74930710751ea991408b405747d01dfae72f80fc158137018aea94f9c38c651cb9c318f0861a310c3679 - languageName: node - linkType: hard - "type-check@npm:^0.4.0, type-check@npm:~0.4.0": version: 0.4.0 resolution: "type-check@npm:0.4.0" @@ -5346,15 +4414,6 @@ __metadata: languageName: node linkType: hard -"undici@npm:^5.28.5": - version: 5.28.5 - resolution: "undici@npm:5.28.5" - dependencies: - "@fastify/busboy": "npm:^2.0.0" - checksum: 10/459cd84ab75fe90d696fa2634a8b5b23f9e1080b27236c6809bd74e51862be85df6d95b4a8fed3ee42554495008cb3c05f1bc9d4a1807478f433cca567003d70 - languageName: node - linkType: hard - "unique-filename@npm:^4.0.0": version: 4.0.0 resolution: "unique-filename@npm:4.0.0" @@ -5403,13 +4462,6 @@ __metadata: languageName: node linkType: hard -"util-deprecate@npm:^1.0.1, util-deprecate@npm:~1.0.1": - version: 1.0.2 - resolution: "util-deprecate@npm:1.0.2" - checksum: 10/474acf1146cb2701fe3b074892217553dfcf9a031280919ba1b8d651a068c9b15d863b7303cb15bd00a862b498e6cf4ad7b4a08fb134edd5a6f7641681cb54a2 - languageName: node - linkType: hard - "uuid@npm:^8.3.2": version: 8.3.2 resolution: "uuid@npm:8.3.2" @@ -5549,15 +4601,6 @@ __metadata: languageName: node linkType: hard -"yaml@npm:^2.2.2": - version: 2.7.0 - resolution: "yaml@npm:2.7.0" - bin: - yaml: bin.mjs - checksum: 10/c8c314c62fbd49244a6a51b06482f6d495b37ab10fa685fcafa1bbaae7841b7233ee7d12cab087bcca5a0b28adc92868b6e437322276430c28d00f1c1732eeec - languageName: node - linkType: hard - "yargs-parser@npm:^21.1.1": version: 21.1.1 resolution: "yargs-parser@npm:21.1.1" @@ -5593,14 +4636,3 @@ __metadata: checksum: 10/f77b3d8d00310def622123df93d4ee654fc6a0096182af8bd60679ddcdfb3474c56c6c7190817c84a2785648cdee9d721c0154eb45698c62176c322fb46fc700 languageName: node linkType: hard - -"zip-stream@npm:^6.0.1": - version: 6.0.1 - resolution: "zip-stream@npm:6.0.1" - dependencies: - archiver-utils: "npm:^5.0.0" - compress-commons: "npm:^6.0.2" - readable-stream: "npm:^4.0.0" - checksum: 10/aa5abd6a89590eadeba040afbc375f53337f12637e5e98330012a12d9886cde7a3ccc28bd91aafab50576035bbb1de39a9a316eecf2411c8b9009c9f94f0db27 - languageName: node - linkType: hard From ebb272817102818b84b4a785b9c3716165ccabbc Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 17 Apr 2025 17:09:58 -0500 Subject: [PATCH 009/322] update turbo and scripts --- compact/turbo.json | 4 +- contracts/erc20/package.json | 8 +- contracts/initializable/package.json | 7 +- contracts/utils/package.json | 7 +- package.json | 10 ++- turbo.json | 117 ++++++++++----------------- yarn.lock | 70 +++++++++------- 7 files changed, 107 insertions(+), 116 deletions(-) diff --git a/compact/turbo.json b/compact/turbo.json index 6d8bfaa0..9c9378a1 100644 --- a/compact/turbo.json +++ b/compact/turbo.json @@ -1,11 +1,11 @@ { "$schema": "https://turbo.build/schema.json", "extends": ["//"], - "pipeline": { + "tasks": { "build": { "outputs": ["compactc/**"], "env": ["COMPACT_HOME"], - "inputs": ["src/**"], + "inputs": ["src/**.cjs"], "cache": true } } diff --git a/contracts/erc20/package.json b/contracts/erc20/package.json index 67cbfe99..ec3c05ba 100644 --- a/contracts/erc20/package.json +++ b/contracts/erc20/package.json @@ -15,10 +15,12 @@ "scripts": { "compact": "find src -name '*.compact' -exec sh -c 'run-compactc \"{}\" \"src/artifacts/$(basename \"{}\" .compact)\"' \\;", "test": "NODE_OPTIONS=--experimental-vm-modules jest", - "prepack": "yarn build", "build": "rm -rf dist && tsc --project tsconfig.build.json && cp -Rf ./src/artifacts ./dist/artifacts && cp ./src/ERC20.compact ./dist", - "lint": "eslint src", - "typecheck": "tsc -p tsconfig.json --noEmit" + "types": "tsc -p tsconfig.json --noEmit", + "fmt": "biome format", + "fmt:fix": "biome format --write", + "lint": "biome lint", + "lint:fix": "biome check --write" }, "dependencies": { "@openzeppelin-midnight-contracts/utils-contract": "workspace:^" diff --git a/contracts/initializable/package.json b/contracts/initializable/package.json index e6868ab2..0ae68562 100644 --- a/contracts/initializable/package.json +++ b/contracts/initializable/package.json @@ -17,7 +17,10 @@ "test": "NODE_OPTIONS=--experimental-vm-modules jest", "prepack": "yarn build", "build": "rm -rf dist && tsc --project tsconfig.build.json && cp -Rf ./src/artifacts ./dist/artifacts && cp ./src/Initializable.compact ./dist", - "lint": "eslint src", - "typecheck": "tsc -p tsconfig.json --noEmit" + "types": "tsc -p tsconfig.json --noEmit", + "fmt": "biome format", + "fmt:fix": "biome format --write", + "lint": "biome lint", + "lint:fix": "biome check --write" } } diff --git a/contracts/utils/package.json b/contracts/utils/package.json index 3fdcfcf5..c08d085f 100644 --- a/contracts/utils/package.json +++ b/contracts/utils/package.json @@ -17,7 +17,10 @@ "test": "NODE_OPTIONS=--experimental-vm-modules jest", "prepack": "yarn build", "build": "rm -rf dist && tsc --project tsconfig.build.json && cp -Rf ./src/artifacts ./dist/artifacts && cp ./src/Utils.compact ./dist", - "lint": "eslint src", - "typecheck": "tsc -p tsconfig.json --noEmit" + "types": "tsc -p tsconfig.json --noEmit", + "fmt": "biome format", + "fmt:fix": "biome format --write", + "lint": "biome lint", + "lint:fix": "biome check --write" } } diff --git a/package.json b/package.json index 5d3ea810..67183a32 100644 --- a/package.json +++ b/package.json @@ -2,13 +2,19 @@ "packageManager": "yarn@4.1.0", "workspaces": [ "compact", + "contracts/erc20/", "contracts/initializable/", "contracts/utils/" ], "scripts": { "compact": "turbo run compact", "build": "turbo run build", - "lint": "turbo run lint" + "test": "turbo run test", + "fmt": "turbo run fmt", + "fmt:fix": "turbo run fmt:fix", + "lint": "turbo run lint", + "lint:fix": "turbo run lint:fix", + "types": "turbo run types" }, "dependencies": { "@midnight-ntwrk/compact-runtime": "^0.7.0" @@ -27,7 +33,7 @@ "jest-junit": "^16.0.0", "ts-jest": "^29.1.1", "ts-node": "^10.9.1", - "turbo": "^1.10.16", + "turbo": "^2.4.4", "typescript": "^5.2.2" } } diff --git a/turbo.json b/turbo.json index 770d717d..e590773b 100644 --- a/turbo.json +++ b/turbo.json @@ -1,97 +1,66 @@ { "$schema": "https://turbo.build/schema.json", - "globalDependencies": [".prettierrc.json"], - "pipeline": { - "typecheck": { - "dependsOn": ["^build", "compact"], + "tasks": { + "compact": { + "env": [ + "COMPACT_HOME" + ], "inputs": [ - "src/**/*.ts", - "src/**/*.tsx", - "src/**/*.mts", - "tsconfig.json" + "src/**/*.compact" ], - "outputMode": "new-only", - "outputs": [] + "outputLogs": "new-only", + "outputs": [ + "src/artifacts/**", + "src/gen/**", + "gen/**" + ] }, - "compact": { - "dependsOn": ["^build"], - "env": ["COMPACT_HOME"], - "inputs": ["src/**/*.compact"], - "outputMode": "new-only", - "outputs": ["src/artifacts/**", "src/gen/**", "gen/**"] + "test": { + "outputs": [], + "cache": false }, "build": { - "dependsOn": ["^build", "compact", "typecheck"], - "outputMode": "new-only", - "inputs": [ - "src/**/*.ts", - "src/**/*.mts", - "src/**/*.tsx", - "!src/**/*.test.ts", - "!tests/**/*.ts", - "tsconfig.json", - "tsconfig.build.json", - ".env" + "dependsOn": [ + "^build", + "compact" + ], + "env": [ + "COMPACT_HOME" ], - "outputs": ["dist/**"] - }, - "build-storybook": { - "dependsOn": ["^build", "typecheck"], - "outputMode": "new-only", "inputs": [ "src/**/*.ts", - "src/**/*.mts", - "src/**/*.tsx", "!src/**/*.test.ts", "!tests/**/*.ts", "tsconfig.json", "tsconfig.build.json", - ".env", - "vite.config.ts", - ".storybook/**" - ], - "outputs": ["storybook-static/**"] - }, - "lint": { - "outputMode": "new-only", - "dependsOn": ["compact", "^build", "typecheck"], - "inputs": ["src/**/*.ts", "src/**/*.mts", "src/**/*.tsx", ".eslintrc.cjs"] - }, - "test": { - "outputMode": "new-only", - "dependsOn": ["^build", "compact", "typecheck"], - "inputs": [ - "src/**/*.ts", - "src/**/*.mts", - "src/**/*.tsx", - "jest.config.ts", - "tsconfig.json", - "tsconfig.test.json", - "test-compose.yml" + ".env" ], - "outputs": ["reports/**"] + "outputs": [ + "dist/**" + ] }, - "check": { - "outputMode": "new-only", - "dependsOn": ["build", "lint", "test", "build-storybook"] + "types": { + "outputs": [], + "cache": false, + "dependsOn": [ + "compact" + ] }, - "test-e2e": { - "outputMode": "new-only", - "dependsOn": ["build", "compact"] + "fmt": { + "outputs": [], + "cache": false }, - "start": { - "cache": false, - "persistent": true, - "dependsOn": ["build"] + "fmt:fix": { + "outputs": [], + "cache": false }, - "dev": { - "cache": false, - "persistent": true + "lint": { + "outputs": [], + "cache": false }, - "storybook": { - "cache": false, - "persistent": true, - "dependsOn": ["build"] + "lint:fix": { + "outputs": [], + "cache": false } } } diff --git a/yarn.lock b/yarn.lock index 2be4f8ea..a747c230 100644 --- a/yarn.lock +++ b/yarn.lock @@ -969,13 +969,21 @@ __metadata: languageName: node linkType: hard +"@openzeppelin-midnight-contracts/erc20-contract@workspace:contracts/erc20": + version: 0.0.0-use.local + resolution: "@openzeppelin-midnight-contracts/erc20-contract@workspace:contracts/erc20" + dependencies: + "@openzeppelin-midnight-contracts/utils-contract": "workspace:^" + languageName: unknown + linkType: soft + "@openzeppelin-midnight-contracts/initializable-contract@workspace:contracts/initializable": version: 0.0.0-use.local resolution: "@openzeppelin-midnight-contracts/initializable-contract@workspace:contracts/initializable" languageName: unknown linkType: soft -"@openzeppelin-midnight-contracts/utils-contract@workspace:contracts/utils": +"@openzeppelin-midnight-contracts/utils-contract@workspace:^, @openzeppelin-midnight-contracts/utils-contract@workspace:contracts/utils": version: 0.0.0-use.local resolution: "@openzeppelin-midnight-contracts/utils-contract@workspace:contracts/utils" languageName: unknown @@ -3900,7 +3908,7 @@ __metadata: jest-junit: "npm:^16.0.0" ts-jest: "npm:^29.1.1" ts-node: "npm:^10.9.1" - turbo: "npm:^1.10.16" + turbo: "npm:^2.4.4" typescript: "npm:^5.2.2" languageName: unknown linkType: soft @@ -4279,58 +4287,58 @@ __metadata: languageName: node linkType: hard -"turbo-darwin-64@npm:1.13.4": - version: 1.13.4 - resolution: "turbo-darwin-64@npm:1.13.4" +"turbo-darwin-64@npm:2.5.0": + version: 2.5.0 + resolution: "turbo-darwin-64@npm:2.5.0" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"turbo-darwin-arm64@npm:1.13.4": - version: 1.13.4 - resolution: "turbo-darwin-arm64@npm:1.13.4" +"turbo-darwin-arm64@npm:2.5.0": + version: 2.5.0 + resolution: "turbo-darwin-arm64@npm:2.5.0" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"turbo-linux-64@npm:1.13.4": - version: 1.13.4 - resolution: "turbo-linux-64@npm:1.13.4" +"turbo-linux-64@npm:2.5.0": + version: 2.5.0 + resolution: "turbo-linux-64@npm:2.5.0" conditions: os=linux & cpu=x64 languageName: node linkType: hard -"turbo-linux-arm64@npm:1.13.4": - version: 1.13.4 - resolution: "turbo-linux-arm64@npm:1.13.4" +"turbo-linux-arm64@npm:2.5.0": + version: 2.5.0 + resolution: "turbo-linux-arm64@npm:2.5.0" conditions: os=linux & cpu=arm64 languageName: node linkType: hard -"turbo-windows-64@npm:1.13.4": - version: 1.13.4 - resolution: "turbo-windows-64@npm:1.13.4" +"turbo-windows-64@npm:2.5.0": + version: 2.5.0 + resolution: "turbo-windows-64@npm:2.5.0" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"turbo-windows-arm64@npm:1.13.4": - version: 1.13.4 - resolution: "turbo-windows-arm64@npm:1.13.4" +"turbo-windows-arm64@npm:2.5.0": + version: 2.5.0 + resolution: "turbo-windows-arm64@npm:2.5.0" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"turbo@npm:^1.10.16": - version: 1.13.4 - resolution: "turbo@npm:1.13.4" - dependencies: - turbo-darwin-64: "npm:1.13.4" - turbo-darwin-arm64: "npm:1.13.4" - turbo-linux-64: "npm:1.13.4" - turbo-linux-arm64: "npm:1.13.4" - turbo-windows-64: "npm:1.13.4" - turbo-windows-arm64: "npm:1.13.4" +"turbo@npm:^2.4.4": + version: 2.5.0 + resolution: "turbo@npm:2.5.0" + dependencies: + turbo-darwin-64: "npm:2.5.0" + turbo-darwin-arm64: "npm:2.5.0" + turbo-linux-64: "npm:2.5.0" + turbo-linux-arm64: "npm:2.5.0" + turbo-windows-64: "npm:2.5.0" + turbo-windows-arm64: "npm:2.5.0" dependenciesMeta: turbo-darwin-64: optional: true @@ -4346,7 +4354,7 @@ __metadata: optional: true bin: turbo: bin/turbo - checksum: 10/b8187def43760428e117313fc4a559af5c02b473d816b3efef406165c2c45cf3a256fbda4b15f0c1a48ccd188bd43cf93686f2aeab176a15a01553cd165071cc + checksum: 10/09a9a441bee424925444a9d3c071efe6139187257296a2b04a6474b719ce385a118c42677a67db1b77ca34f041a72a3fc152a60efcd96d407fd8d3a708c259a4 languageName: node linkType: hard From cd757c0f841cb861eb8bb3ad2f4ebe43130242ca Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 17 Apr 2025 17:15:40 -0500 Subject: [PATCH 010/322] add clean script --- contracts/erc20/package.json | 3 ++- contracts/initializable/package.json | 3 ++- contracts/utils/package.json | 4 ++-- package.json | 3 ++- turbo.json | 4 ++++ 5 files changed, 12 insertions(+), 5 deletions(-) diff --git a/contracts/erc20/package.json b/contracts/erc20/package.json index ec3c05ba..2a8ca839 100644 --- a/contracts/erc20/package.json +++ b/contracts/erc20/package.json @@ -20,7 +20,8 @@ "fmt": "biome format", "fmt:fix": "biome format --write", "lint": "biome lint", - "lint:fix": "biome check --write" + "lint:fix": "biome check --write", + "clean": "git clean -fXd" }, "dependencies": { "@openzeppelin-midnight-contracts/utils-contract": "workspace:^" diff --git a/contracts/initializable/package.json b/contracts/initializable/package.json index 0ae68562..9864007a 100644 --- a/contracts/initializable/package.json +++ b/contracts/initializable/package.json @@ -21,6 +21,7 @@ "fmt": "biome format", "fmt:fix": "biome format --write", "lint": "biome lint", - "lint:fix": "biome check --write" + "lint:fix": "biome check --write", + "clean": "git clean -fXd" } } diff --git a/contracts/utils/package.json b/contracts/utils/package.json index c08d085f..e197d1d5 100644 --- a/contracts/utils/package.json +++ b/contracts/utils/package.json @@ -15,12 +15,12 @@ "scripts": { "compact": "find src -name '*.compact' -exec sh -c 'run-compactc \"{}\" \"src/artifacts/$(basename \"{}\" .compact)\"' \\;", "test": "NODE_OPTIONS=--experimental-vm-modules jest", - "prepack": "yarn build", "build": "rm -rf dist && tsc --project tsconfig.build.json && cp -Rf ./src/artifacts ./dist/artifacts && cp ./src/Utils.compact ./dist", "types": "tsc -p tsconfig.json --noEmit", "fmt": "biome format", "fmt:fix": "biome format --write", "lint": "biome lint", - "lint:fix": "biome check --write" + "lint:fix": "biome check --write", + "clean": "git clean -fXd" } } diff --git a/package.json b/package.json index 67183a32..1005b4b3 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "fmt:fix": "turbo run fmt:fix", "lint": "turbo run lint", "lint:fix": "turbo run lint:fix", - "types": "turbo run types" + "types": "turbo run types", + "clean": "git clean -fXd -e \\\\!node_modules -e \\\\!node_modules/**/*" }, "dependencies": { "@midnight-ntwrk/compact-runtime": "^0.7.0" diff --git a/turbo.json b/turbo.json index e590773b..0940f33c 100644 --- a/turbo.json +++ b/turbo.json @@ -61,6 +61,10 @@ "lint:fix": { "outputs": [], "cache": false + }, + "clean": { + "outputs": [], + "cache": false } } } From a5fb60b666b956ab975bd2cad8853f0725929198 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 17 Apr 2025 17:22:23 -0500 Subject: [PATCH 011/322] add clean script --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1005b4b3..df03994a 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "lint": "turbo run lint", "lint:fix": "turbo run lint:fix", "types": "turbo run types", - "clean": "git clean -fXd -e \\\\!node_modules -e \\\\!node_modules/**/*" + "clean": "turbo run clean" }, "dependencies": { "@midnight-ntwrk/compact-runtime": "^0.7.0" From b4fb0dfe32d1e5c36a8b3945b9ba5a42b5d35fb5 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 17 Apr 2025 20:47:24 -0500 Subject: [PATCH 012/322] clean up test config --- contracts/erc20/jest.config.ts | 14 +------------- contracts/initializable/jest.config.ts | 14 +------------- contracts/utils/jest.config.ts | 14 +------------- 3 files changed, 3 insertions(+), 39 deletions(-) diff --git a/contracts/erc20/jest.config.ts b/contracts/erc20/jest.config.ts index 5d1dbd14..bde5bde1 100644 --- a/contracts/erc20/jest.config.ts +++ b/contracts/erc20/jest.config.ts @@ -9,20 +9,8 @@ const config: Config.InitialOptions = { passWithNoTests: false, testMatch: ['**/*.test.ts'], extensionsToTreatAsEsm: ['.ts'], - collectCoverage: true, + collectCoverage: false, resolver: '/js-resolver.cjs', - coverageThreshold: { - global: { - branches: 50, - functions: 50, - lines: 50, - }, - }, - reporters: [ - 'default', - ['jest-junit', { outputDirectory: 'reports', outputName: 'report.xml' }], - ['jest-html-reporters', { publicPath: 'reports', filename: 'report.html' }], - ], }; export default config; diff --git a/contracts/initializable/jest.config.ts b/contracts/initializable/jest.config.ts index 5d1dbd14..bde5bde1 100644 --- a/contracts/initializable/jest.config.ts +++ b/contracts/initializable/jest.config.ts @@ -9,20 +9,8 @@ const config: Config.InitialOptions = { passWithNoTests: false, testMatch: ['**/*.test.ts'], extensionsToTreatAsEsm: ['.ts'], - collectCoverage: true, + collectCoverage: false, resolver: '/js-resolver.cjs', - coverageThreshold: { - global: { - branches: 50, - functions: 50, - lines: 50, - }, - }, - reporters: [ - 'default', - ['jest-junit', { outputDirectory: 'reports', outputName: 'report.xml' }], - ['jest-html-reporters', { publicPath: 'reports', filename: 'report.html' }], - ], }; export default config; diff --git a/contracts/utils/jest.config.ts b/contracts/utils/jest.config.ts index f15dd79d..bde5bde1 100644 --- a/contracts/utils/jest.config.ts +++ b/contracts/utils/jest.config.ts @@ -9,20 +9,8 @@ const config: Config.InitialOptions = { passWithNoTests: false, testMatch: ['**/*.test.ts'], extensionsToTreatAsEsm: ['.ts'], - collectCoverage: true, + collectCoverage: false, resolver: '/js-resolver.cjs', - coverageThreshold: { - global: { - //branches: 60, - //functions: 75, - //lines: 70, - }, - }, - reporters: [ - 'default', - ['jest-junit', { outputDirectory: 'reports', outputName: 'report.xml' }], - ['jest-html-reporters', { publicPath: 'reports', filename: 'report.html' }], - ], }; export default config; From 3462103b1ad2bfeac1dc676f6c8d011fd4c0ed64 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 17 Apr 2025 20:49:55 -0500 Subject: [PATCH 013/322] remove jest reporters --- package.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/package.json b/package.json index df03994a..85492b3a 100644 --- a/package.json +++ b/package.json @@ -29,9 +29,6 @@ "fast-check": "^3.15.0", "jest": "^29.7.0", "jest-fast-check": "^2.0.0", - "jest-gh-md-reporter": "^0.0.2", - "jest-html-reporters": "^3.1.4", - "jest-junit": "^16.0.0", "ts-jest": "^29.1.1", "ts-node": "^10.9.1", "turbo": "^2.4.4", From de6ab59d7e2972284513353d5dfe2619d8296030 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 17 Apr 2025 22:26:37 -0500 Subject: [PATCH 014/322] bump turbo to 2.5 --- package.json | 2 +- yarn.lock | 134 ++------------------------------------------------- 2 files changed, 5 insertions(+), 131 deletions(-) diff --git a/package.json b/package.json index 85492b3a..99e81577 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "jest-fast-check": "^2.0.0", "ts-jest": "^29.1.1", "ts-node": "^10.9.1", - "turbo": "^2.4.4", + "turbo": "^2.5.0", "typescript": "^5.2.2" } } diff --git a/yarn.lock b/yarn.lock index a747c230..bdc16704 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1695,13 +1695,6 @@ __metadata: languageName: node linkType: hard -"define-lazy-prop@npm:^2.0.0": - version: 2.0.0 - resolution: "define-lazy-prop@npm:2.0.0" - checksum: 10/0115fdb065e0490918ba271d7339c42453d209d4cb619dfe635870d906731eff3e1ade8028bb461ea27ce8264ec5e22c6980612d332895977e89c1bbc80fcee2 - languageName: node - linkType: hard - "detect-newline@npm:^3.0.0": version: 3.1.0 resolution: "detect-newline@npm:3.1.0" @@ -1739,7 +1732,7 @@ __metadata: languageName: node linkType: hard -"ejs@npm:^3.1.10, ejs@npm:^3.1.7": +"ejs@npm:^3.1.10": version: 3.1.10 resolution: "ejs@npm:3.1.10" dependencies: @@ -2116,17 +2109,6 @@ __metadata: languageName: node linkType: hard -"fs-extra@npm:^10.0.0": - version: 10.1.0 - resolution: "fs-extra@npm:10.1.0" - dependencies: - graceful-fs: "npm:^4.2.0" - jsonfile: "npm:^6.0.1" - universalify: "npm:^2.0.0" - checksum: 10/05ce2c3b59049bcb7b52001acd000e44b3c4af4ec1f8839f383ef41ec0048e3cfa7fd8a637b1bddfefad319145db89be91f4b7c1db2908205d38bf91e7d1d3b7 - languageName: node - linkType: hard - "fs-minipass@npm:^3.0.0": version: 3.0.3 resolution: "fs-minipass@npm:3.0.3" @@ -2252,7 +2234,7 @@ __metadata: languageName: node linkType: hard -"graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": +"graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" checksum: 10/bf152d0ed1dc159239db1ba1f74fdbc40cb02f626770dcd5815c427ce0688c2635a06ed69af364396da4636d0408fcf7d4afdf7881724c3307e46aff30ca49e2 @@ -2411,15 +2393,6 @@ __metadata: languageName: node linkType: hard -"is-docker@npm:^2.0.0, is-docker@npm:^2.1.1": - version: 2.2.1 - resolution: "is-docker@npm:2.2.1" - bin: - is-docker: cli.js - checksum: 10/3fef7ddbf0be25958e8991ad941901bf5922ab2753c46980b60b05c1bf9c9c2402d35e6dc32e4380b980ef5e1970a5d9d5e5aa2e02d77727c3b6b5e918474c56 - languageName: node - linkType: hard - "is-extglob@npm:^2.1.1": version: 2.1.1 resolution: "is-extglob@npm:2.1.1" @@ -2471,15 +2444,6 @@ __metadata: languageName: node linkType: hard -"is-wsl@npm:^2.2.0": - version: 2.2.0 - resolution: "is-wsl@npm:2.2.0" - dependencies: - is-docker: "npm:^2.0.0" - checksum: 10/20849846ae414997d290b75e16868e5261e86ff5047f104027026fd61d8b5a9b0b3ade16239f35e1a067b3c7cc02f70183cb661010ed16f4b6c7c93dad1b19d8 - languageName: node - linkType: hard - "isexe@npm:^2.0.0": version: 2.0.0 resolution: "isexe@npm:2.0.0" @@ -2754,15 +2718,6 @@ __metadata: languageName: node linkType: hard -"jest-gh-md-reporter@npm:^0.0.2": - version: 0.0.2 - resolution: "jest-gh-md-reporter@npm:0.0.2" - dependencies: - ejs: "npm:^3.1.7" - checksum: 10/e61e63807a124184aeacef13816c313e8f2211aecd31974cc14f7d0dc65819286fc63969fd8e215504e2be7a3a5946fd023a231aca1d4981af14238c7dc41a7b - languageName: node - linkType: hard - "jest-haste-map@npm:^29.7.0": version: 29.7.0 resolution: "jest-haste-map@npm:29.7.0" @@ -2786,28 +2741,6 @@ __metadata: languageName: node linkType: hard -"jest-html-reporters@npm:^3.1.4": - version: 3.1.7 - resolution: "jest-html-reporters@npm:3.1.7" - dependencies: - fs-extra: "npm:^10.0.0" - open: "npm:^8.0.3" - checksum: 10/4aa780dd8cae9065e11fe0e5abb6e339eb9001f1745761a90f2e984b2dd7cf575be5cb5fef862e314b108d1602234de8ad5cf81aaf6142cf0b4eb3adc7797509 - languageName: node - linkType: hard - -"jest-junit@npm:^16.0.0": - version: 16.0.0 - resolution: "jest-junit@npm:16.0.0" - dependencies: - mkdirp: "npm:^1.0.4" - strip-ansi: "npm:^6.0.1" - uuid: "npm:^8.3.2" - xml: "npm:^1.0.1" - checksum: 10/2c33ee8bfd0c83b9aa1f8ba5905084890d5f519d294ccc2829d778ac860d5adffffec75d930f44f1d498aa8370c783e0aa6a632d947fb7e81205f0e7b926669d - languageName: node - linkType: hard - "jest-leak-detector@npm:^29.7.0": version: 29.7.0 resolution: "jest-leak-detector@npm:29.7.0" @@ -3149,19 +3082,6 @@ __metadata: languageName: node linkType: hard -"jsonfile@npm:^6.0.1": - version: 6.1.0 - resolution: "jsonfile@npm:6.1.0" - dependencies: - graceful-fs: "npm:^4.1.6" - universalify: "npm:^2.0.0" - dependenciesMeta: - graceful-fs: - optional: true - checksum: 10/03014769e7dc77d4cf05fa0b534907270b60890085dd5e4d60a382ff09328580651da0b8b4cdf44d91e4c8ae64d91791d965f05707beff000ed494a38b6fec85 - languageName: node - linkType: hard - "keyv@npm:^4.5.3": version: 4.5.4 resolution: "keyv@npm:4.5.4" @@ -3422,15 +3342,6 @@ __metadata: languageName: node linkType: hard -"mkdirp@npm:^1.0.4": - version: 1.0.4 - resolution: "mkdirp@npm:1.0.4" - bin: - mkdirp: bin/cmd.js - checksum: 10/d71b8dcd4b5af2fe13ecf3bd24070263489404fe216488c5ba7e38ece1f54daf219e72a833a3a2dc404331e870e9f44963a33399589490956bff003a3404d3b2 - languageName: node - linkType: hard - "mkdirp@npm:^3.0.1": version: 3.0.1 resolution: "mkdirp@npm:3.0.1" @@ -3547,17 +3458,6 @@ __metadata: languageName: node linkType: hard -"open@npm:^8.0.3": - version: 8.4.2 - resolution: "open@npm:8.4.2" - dependencies: - define-lazy-prop: "npm:^2.0.0" - is-docker: "npm:^2.1.1" - is-wsl: "npm:^2.2.0" - checksum: 10/acd81a1d19879c818acb3af2d2e8e9d81d17b5367561e623248133deb7dd3aefaed527531df2677d3e6aaf0199f84df57b6b2262babff8bf46ea0029aac536c9 - languageName: node - linkType: hard - "optionator@npm:^0.9.3": version: 0.9.4 resolution: "optionator@npm:0.9.4" @@ -3903,12 +3803,9 @@ __metadata: fast-check: "npm:^3.15.0" jest: "npm:^29.7.0" jest-fast-check: "npm:^2.0.0" - jest-gh-md-reporter: "npm:^0.0.2" - jest-html-reporters: "npm:^3.1.4" - jest-junit: "npm:^16.0.0" ts-jest: "npm:^29.1.1" ts-node: "npm:^10.9.1" - turbo: "npm:^2.4.4" + turbo: "npm:^2.5.0" typescript: "npm:^5.2.2" languageName: unknown linkType: soft @@ -4329,7 +4226,7 @@ __metadata: languageName: node linkType: hard -"turbo@npm:^2.4.4": +"turbo@npm:^2.5.0": version: 2.5.0 resolution: "turbo@npm:2.5.0" dependencies: @@ -4440,13 +4337,6 @@ __metadata: languageName: node linkType: hard -"universalify@npm:^2.0.0": - version: 2.0.1 - resolution: "universalify@npm:2.0.1" - checksum: 10/ecd8469fe0db28e7de9e5289d32bd1b6ba8f7183db34f3bfc4ca53c49891c2d6aa05f3fb3936a81285a905cc509fb641a0c3fc131ec786167eff41236ae32e60 - languageName: node - linkType: hard - "update-browserslist-db@npm:^1.1.1": version: 1.1.3 resolution: "update-browserslist-db@npm:1.1.3" @@ -4470,15 +4360,6 @@ __metadata: languageName: node linkType: hard -"uuid@npm:^8.3.2": - version: 8.3.2 - resolution: "uuid@npm:8.3.2" - bin: - uuid: dist/bin/uuid - checksum: 10/9a5f7aa1d6f56dd1e8d5f2478f855f25c645e64e26e347a98e98d95781d5ed20062d6cca2eecb58ba7c84bc3910be95c0451ef4161906abaab44f9cb68ffbdd1 - languageName: node - linkType: hard - "v8-compile-cache-lib@npm:^3.0.1": version: 3.0.1 resolution: "v8-compile-cache-lib@npm:3.0.1" @@ -4574,13 +4455,6 @@ __metadata: languageName: node linkType: hard -"xml@npm:^1.0.1": - version: 1.0.1 - resolution: "xml@npm:1.0.1" - checksum: 10/6c4c31a1308e45732e5ac6b50edbca0e8f7abe5cb5de10215d8e3c688819fe7c7706e056f6fb59b1a23fdf1000c2d7a8bba0a89e94aa1796cd2376d9a5ba401e - languageName: node - linkType: hard - "y18n@npm:^5.0.5": version: 5.0.8 resolution: "y18n@npm:5.0.8" From 7c211d7b9e2cbb168d8edcfddb44f0f69deaea98 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 24 Apr 2025 18:33:01 -0500 Subject: [PATCH 015/322] bump turbo --- package.json | 2 +- yarn.lock | 58 ++++++++++++++++++++++++++-------------------------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/package.json b/package.json index 99e81577..5bb71715 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "jest-fast-check": "^2.0.0", "ts-jest": "^29.1.1", "ts-node": "^10.9.1", - "turbo": "^2.5.0", + "turbo": "^2.5.1", "typescript": "^5.2.2" } } diff --git a/yarn.lock b/yarn.lock index bdc16704..9d0ba076 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3805,7 +3805,7 @@ __metadata: jest-fast-check: "npm:^2.0.0" ts-jest: "npm:^29.1.1" ts-node: "npm:^10.9.1" - turbo: "npm:^2.5.0" + turbo: "npm:^2.5.1" typescript: "npm:^5.2.2" languageName: unknown linkType: soft @@ -4184,58 +4184,58 @@ __metadata: languageName: node linkType: hard -"turbo-darwin-64@npm:2.5.0": - version: 2.5.0 - resolution: "turbo-darwin-64@npm:2.5.0" +"turbo-darwin-64@npm:2.5.1": + version: 2.5.1 + resolution: "turbo-darwin-64@npm:2.5.1" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"turbo-darwin-arm64@npm:2.5.0": - version: 2.5.0 - resolution: "turbo-darwin-arm64@npm:2.5.0" +"turbo-darwin-arm64@npm:2.5.1": + version: 2.5.1 + resolution: "turbo-darwin-arm64@npm:2.5.1" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"turbo-linux-64@npm:2.5.0": - version: 2.5.0 - resolution: "turbo-linux-64@npm:2.5.0" +"turbo-linux-64@npm:2.5.1": + version: 2.5.1 + resolution: "turbo-linux-64@npm:2.5.1" conditions: os=linux & cpu=x64 languageName: node linkType: hard -"turbo-linux-arm64@npm:2.5.0": - version: 2.5.0 - resolution: "turbo-linux-arm64@npm:2.5.0" +"turbo-linux-arm64@npm:2.5.1": + version: 2.5.1 + resolution: "turbo-linux-arm64@npm:2.5.1" conditions: os=linux & cpu=arm64 languageName: node linkType: hard -"turbo-windows-64@npm:2.5.0": - version: 2.5.0 - resolution: "turbo-windows-64@npm:2.5.0" +"turbo-windows-64@npm:2.5.1": + version: 2.5.1 + resolution: "turbo-windows-64@npm:2.5.1" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"turbo-windows-arm64@npm:2.5.0": - version: 2.5.0 - resolution: "turbo-windows-arm64@npm:2.5.0" +"turbo-windows-arm64@npm:2.5.1": + version: 2.5.1 + resolution: "turbo-windows-arm64@npm:2.5.1" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"turbo@npm:^2.5.0": - version: 2.5.0 - resolution: "turbo@npm:2.5.0" +"turbo@npm:^2.5.1": + version: 2.5.1 + resolution: "turbo@npm:2.5.1" dependencies: - turbo-darwin-64: "npm:2.5.0" - turbo-darwin-arm64: "npm:2.5.0" - turbo-linux-64: "npm:2.5.0" - turbo-linux-arm64: "npm:2.5.0" - turbo-windows-64: "npm:2.5.0" - turbo-windows-arm64: "npm:2.5.0" + turbo-darwin-64: "npm:2.5.1" + turbo-darwin-arm64: "npm:2.5.1" + turbo-linux-64: "npm:2.5.1" + turbo-linux-arm64: "npm:2.5.1" + turbo-windows-64: "npm:2.5.1" + turbo-windows-arm64: "npm:2.5.1" dependenciesMeta: turbo-darwin-64: optional: true @@ -4251,7 +4251,7 @@ __metadata: optional: true bin: turbo: bin/turbo - checksum: 10/09a9a441bee424925444a9d3c071efe6139187257296a2b04a6474b719ce385a118c42677a67db1b77ca34f041a72a3fc152a60efcd96d407fd8d3a708c259a4 + checksum: 10/54d7c16d7cc9b60098db62d37940bef4663a14344bada1cb521e3260b6c30ec4bebc88a3dbb4108e4a3436fdc32b43091323f9d841ad3198719513765a73ddd9 languageName: node linkType: hard From 5cfcf80bd497054194f14a9e8672f7ac4e8f72c5 Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 25 Apr 2025 01:45:09 -0500 Subject: [PATCH 016/322] fix package names --- contracts/erc20/package.json | 4 ++-- contracts/erc20/src/ERC20.compact | 2 +- contracts/initializable/package.json | 2 +- contracts/utils/package.json | 2 +- yarn.lock | 14 +++++++------- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/contracts/erc20/package.json b/contracts/erc20/package.json index 2a8ca839..c64d5517 100644 --- a/contracts/erc20/package.json +++ b/contracts/erc20/package.json @@ -1,5 +1,5 @@ { - "name": "@openzeppelin-midnight-contracts/erc20-contract", + "name": "@openzeppelin-midnight/erc20", "type": "module", "main": "dist/index.js", "module": "dist/index.js", @@ -24,6 +24,6 @@ "clean": "git clean -fXd" }, "dependencies": { - "@openzeppelin-midnight-contracts/utils-contract": "workspace:^" + "@openzeppelin-midnight/utils": "workspace:^" } } diff --git a/contracts/erc20/src/ERC20.compact b/contracts/erc20/src/ERC20.compact index 96bdab69..31ca0059 100644 --- a/contracts/erc20/src/ERC20.compact +++ b/contracts/erc20/src/ERC20.compact @@ -22,7 +22,7 @@ pragma language_version >= 0.14.0; */ module ERC20 { import CompactStandardLibrary; - import "../../node_modules/@openzeppelin-midnight-contracts/utils-contract/src/Utils" prefix Utils_; + import "../../node_modules/@openzeppelin-midnight/utils/src/Utils" prefix Utils_; /// Public state export sealed ledger _name: Maybe>; diff --git a/contracts/initializable/package.json b/contracts/initializable/package.json index 9864007a..ca84c452 100644 --- a/contracts/initializable/package.json +++ b/contracts/initializable/package.json @@ -1,5 +1,5 @@ { - "name": "@openzeppelin-midnight-contracts/initializable-contract", + "name": "@openzeppelin-midnight/initializable", "type": "module", "main": "dist/index.js", "module": "dist/index.js", diff --git a/contracts/utils/package.json b/contracts/utils/package.json index e197d1d5..e47de479 100644 --- a/contracts/utils/package.json +++ b/contracts/utils/package.json @@ -1,5 +1,5 @@ { - "name": "@openzeppelin-midnight-contracts/utils-contract", + "name": "@openzeppelin-midnight/utils", "type": "module", "main": "dist/index.js", "module": "dist/index.js", diff --git a/yarn.lock b/yarn.lock index 9d0ba076..cd1784ed 100644 --- a/yarn.lock +++ b/yarn.lock @@ -969,23 +969,23 @@ __metadata: languageName: node linkType: hard -"@openzeppelin-midnight-contracts/erc20-contract@workspace:contracts/erc20": +"@openzeppelin-midnight/erc20@workspace:contracts/erc20": version: 0.0.0-use.local - resolution: "@openzeppelin-midnight-contracts/erc20-contract@workspace:contracts/erc20" + resolution: "@openzeppelin-midnight/erc20@workspace:contracts/erc20" dependencies: - "@openzeppelin-midnight-contracts/utils-contract": "workspace:^" + "@openzeppelin-midnight/utils": "workspace:^" languageName: unknown linkType: soft -"@openzeppelin-midnight-contracts/initializable-contract@workspace:contracts/initializable": +"@openzeppelin-midnight/initializable@workspace:contracts/initializable": version: 0.0.0-use.local - resolution: "@openzeppelin-midnight-contracts/initializable-contract@workspace:contracts/initializable" + resolution: "@openzeppelin-midnight/initializable@workspace:contracts/initializable" languageName: unknown linkType: soft -"@openzeppelin-midnight-contracts/utils-contract@workspace:^, @openzeppelin-midnight-contracts/utils-contract@workspace:contracts/utils": +"@openzeppelin-midnight/utils@workspace:^, @openzeppelin-midnight/utils@workspace:contracts/utils": version: 0.0.0-use.local - resolution: "@openzeppelin-midnight-contracts/utils-contract@workspace:contracts/utils" + resolution: "@openzeppelin-midnight/utils@workspace:contracts/utils" languageName: unknown linkType: soft From f23d40bb633538ec095abf0270ed171e413a51f1 Mon Sep 17 00:00:00 2001 From: andrew Date: Sun, 27 Apr 2025 01:46:24 -0500 Subject: [PATCH 017/322] set up compact compiler and builder --- compact/.eslintrc.cjs | 24 ----- compact/package.json | 41 +++++++-- compact/src/Builder.ts | 155 ++++++++++++++++++++++++++++++++ compact/src/Compiler.ts | 170 +++++++++++++++++++++++++++++++++++ compact/src/run-compactc.cjs | 32 ------- compact/src/runBuilder.ts | 43 +++++++++ compact/src/runCompiler.ts | 40 +++++++++ compact/tsconfig.json | 21 +++++ 8 files changed, 461 insertions(+), 65 deletions(-) delete mode 100644 compact/.eslintrc.cjs create mode 100755 compact/src/Builder.ts create mode 100755 compact/src/Compiler.ts delete mode 100755 compact/src/run-compactc.cjs create mode 100644 compact/src/runBuilder.ts create mode 100644 compact/src/runCompiler.ts create mode 100644 compact/tsconfig.json diff --git a/compact/.eslintrc.cjs b/compact/.eslintrc.cjs deleted file mode 100644 index 32fa19b8..00000000 --- a/compact/.eslintrc.cjs +++ /dev/null @@ -1,24 +0,0 @@ -module.exports = { - env: { - browser: true, - es2021: true, - node: true, - jest: true, - }, - extends: [ - 'plugin:prettier/recommended', - 'plugin:@typescript-eslint/recommended-requiring-type-checking', - ], - overrides: [], - parserOptions: { - ecmaVersion: 'latest', - sourceType: 'module', - project: ['tsconfig.json'], - }, - rules: { - '@typescript-eslint/no-misused-promises': 'off', // https://github.com/typescript-eslint/typescript-eslint/issues/5807 - '@typescript-eslint/no-floating-promises': 'warn', - '@typescript-eslint/promise-function-async': 'off', - '@typescript-eslint/no-redeclare': 'off', - }, -}; diff --git a/compact/package.json b/compact/package.json index a2ab6849..15b7595a 100644 --- a/compact/package.json +++ b/compact/package.json @@ -1,17 +1,40 @@ { - "name": "@midnight-ntwrk/compact", + "packageManager": "yarn@4.1.0", + "name": "@openzeppelin-midnight/compact", + "version": "0.0.1", + "keywords": [ + "compact", + "compiler" + ], + "author": "OpenZeppelin Community ", + "license": "MIT", "description": "Compact fetcher", - "author": "IOG", - "license": "Apache-2.0", - "private": true, - "version": "0.21.0", "type": "module", + "main": "index.js", "bin": { - "run-compactc": "src/run-compactc.cjs" + "compact-builder": "dist/runBuilder.js", + "compact-compiler": "dist/runCompiler.js" + }, + "scripts": { + "types": "tsc -p tsconfig.json --noEmit", + "fmt": "biome format", + "fmt:fix": "biome format --write", + "lint": "biome lint", + "lint:fix": "biome check --write", + "clean": "git clean -fXd" }, "devDependencies": { - "eslint": "^8.52.0", - "ts-node": "^10.9.2", - "typescript": "^5.2.2" + "@types/jest": "^29.5.6", + "@types/node": "^22.13.10", + "fast-check": "^3.15.0", + "jest": "^29.7.0", + "jest-fast-check": "^2.0.0", + "ts-jest": "^29.1.1", + "typescript": "^5.8.2" + }, + "dependencies": { + "chalk": "^5.4.1", + "log-symbols": "^7.0.0", + "ora": "^8.2.0" } } diff --git a/compact/src/Builder.ts b/compact/src/Builder.ts new file mode 100755 index 00000000..638157fa --- /dev/null +++ b/compact/src/Builder.ts @@ -0,0 +1,155 @@ +#!/usr/bin/env node + +import { exec } from 'node:child_process'; +import { promisify } from 'node:util'; +import chalk from 'chalk'; +import ora, { type Ora } from 'ora'; +import { CompactCompiler } from './Compiler.js'; + +// Promisified exec for async execution +const execAsync = promisify(exec); + +/** + * A class to handle the build process for a project. + * Runs CompactCompiler as a prerequisite, then executes build steps (TypeScript compilation, + * artifact copying, etc.) + * with progress feedback and colored output for success and error states. + * + * @example + * ```typescript + * const builder = new ProjectBuilder('--skip-zk'); // Optional flags for compactc + * builder.build().catch(err => console.error(err)); + * ``` + * + * @example Successful Build Output + * ``` + * ℹ [COMPILE] Found 2 .compact file(s) to compile + * ✔ [COMPILE] [1/2] Compiled AccessControl.compact + * Compactc version: 0.22.0 + * ✔ [COMPILE] [2/2] Compiled MockAccessControl.compact + * Compactc version: 0.22.0 + * ✔ [BUILD] [1/3] Compiling TypeScript + * ✔ [BUILD] [2/3] Copying artifacts + * ✔ [BUILD] [3/3] Copying and cleaning .compact files + * ``` + * + * @example Failed Compilation Output + * ``` + * ℹ [COMPILE] Found 2 .compact file(s) to compile + * ✖ [COMPILE] [1/2] Failed AccessControl.compact + * Compactc version: 0.22.0 + * Error: Expected ';' at line 5 in AccessControl.compact + * ``` + * + * @example Failed Build Step Output + * ``` + * ℹ [COMPILE] Found 2 .compact file(s) to compile + * ✔ [COMPILE] [1/2] Compiled AccessControl.compact + * ✔ [COMPILE] [2/2] Compiled MockAccessControl.compact + * ✖ [BUILD] [1/3] Failed Compiling TypeScript + * error TS1005: ';' expected at line 10 in file.ts + * [BUILD] ❌ Build failed: Command failed: tsc --project tsconfig.build.json + * ``` + */ +export class CompactBuilder { + private readonly compilerFlags: string; + private readonly steps: Array<{ cmd: string; msg: string; shell?: string }> = + [ + { + cmd: 'tsc --project tsconfig.build.json', + msg: 'Compiling TypeScript', + }, + { + cmd: 'mkdir -p dist/artifacts && cp -Rf src/artifacts/* dist/artifacts/ 2>/dev/null || true', + msg: 'Copying artifacts', + shell: '/bin/bash', + }, + { + cmd: 'cp src/*.compact dist/ 2>/dev/null || true && rm dist/Mock*.compact 2>/dev/null || true', + msg: 'Copying and cleaning .compact files', + shell: '/bin/bash', + }, + ]; + + /** + * Constructs a new ProjectBuilder instance. + * @param compilerFlags - Optional space-separated string of `compactc` flags (e.g., "--skip-zk") + */ + constructor(compilerFlags = '') { + this.compilerFlags = compilerFlags; + } + + /** + * Executes the full build process: compiles .compact files first, then runs build steps. + * Displays progress with spinners and outputs results in color. + * + * @returns A promise that resolves when all steps complete successfully + * @throws Error if compilation or any build step fails + */ + public async build(): Promise { + // Run compact compilation as a prerequisite + const compiler = new CompactCompiler(this.compilerFlags); + await compiler.compile(); + + // Proceed with build steps + for (const [index, step] of this.steps.entries()) { + await this.executeStep(step, index, this.steps.length); + } + } + + /** + * Executes a single build step. + * Runs the command, shows a spinner, and prints output with indentation. + * + * @param step - The build step containing command and message + * @param index - Current step index (0-based) for progress display + * @param total - Total number of steps for progress display + * @returns A promise that resolves when the step completes successfully + * @throws Error if the step fails + */ + private async executeStep( + step: { cmd: string; msg: string; shell?: string }, + index: number, + total: number, + ): Promise { + const stepLabel: string = `[${index + 1}/${total}]`; + const spinner: Ora = ora(`[BUILD] ${stepLabel} ${step.msg}`).start(); + + try { + const { stdout, stderr }: { stdout: string; stderr: string } = + await execAsync(step.cmd, { + shell: step.shell, // Only pass shell where needed + }); + spinner.succeed(`[BUILD] ${stepLabel} ${step.msg}`); + this.printOutput(stdout, chalk.cyan); + this.printOutput(stderr, chalk.yellow); // Show stderr (warnings) in yellow if present + } catch (error: any) { + spinner.fail(`[BUILD] ${stepLabel} ${step.msg}`); + this.printOutput(error.stdout, chalk.cyan); + this.printOutput(error.stderr, chalk.red); + console.error(chalk.red('[BUILD] ❌ Build failed:', error.message)); + process.exit(1); + } + } + + /** + * Prints command output with indentation and specified color. + * Filters out empty lines and indents each line for readability. + * + * @param output - The command output string to print (stdout or stderr) + * @param colorFn - Chalk color function to style the output (e.g., `chalk.cyan` for success, `chalk.red` for errors) + */ + private printOutput( + output: string | undefined, + colorFn: (text: string) => string, + ): void { + if (output) { + const lines: string[] = output + .split('\n') + .filter((line: string): boolean => line.trim() !== '') + .map((line: string): string => ` ${line}`); + // biome-ignore lint/suspicious/noConsoleLog: needed for debugging + console.log(colorFn(lines.join('\n'))); + } + } +} diff --git a/compact/src/Compiler.ts b/compact/src/Compiler.ts new file mode 100755 index 00000000..1ae6659c --- /dev/null +++ b/compact/src/Compiler.ts @@ -0,0 +1,170 @@ +#!/usr/bin/env node + +import { exec as execCallback } from 'node:child_process'; +import { existsSync, readdirSync } from 'node:fs'; +import { basename, dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { promisify } from 'node:util'; +import chalk from 'chalk'; +import ora, { type Ora } from 'ora'; + +const DIRNAME: string = dirname(fileURLToPath(import.meta.url)); +const SRC_DIR: string = 'src'; +const ARTIFACTS_DIR: string = 'src/artifacts'; +const COMPACT_HOME: string = + process.env.COMPACT_HOME ?? resolve(DIRNAME, '../compactc'); +const COMPACTC_PATH: string = join(COMPACT_HOME, 'compactc'); + +/** + * A class to handle compilation of `.compact` files using the `compactc` compiler. + * Provides progress feedback and colored output for success and error states. + * + * @example + * ```typescript + * const compiler = new CompactCompiler('--skip-zk'); + * compiler.compile().catch(err => console.error(err)); + * ``` + * + * @example Successful Compilation Output + * ``` + * ℹ [COMPILE] Found 2 .compact file(s) to compile + * ✔ [COMPILE] [1/2] Compiled AccessControl.compact + * Compactc version: 0.22.0 + * ✔ [COMPILE] [2/2] Compiled MockAccessControl.compact + * Compactc version: 0.22.0 + * Compiling circuit "src/artifacts/MockAccessControl/zkir/grantRole.zkir"... (skipped proving keys) + * ``` + * + * @example Failed Compilation Output + * ``` + * ℹ [COMPILE] Found 2 .compact file(s) to compile + * ✖ [COMPILE] [1/2] Failed AccessControl.compact + * Compactc version: 0.22.0 + * Error: Expected ';' at line 5 in AccessControl.compact + * ``` + */ +export class CompactCompiler { + /** Stores the compiler flags passed via command-line arguments */ + private readonly flags: string; + + /** + * Constructs a new CompactCompiler instance, validating the `compactc` binary path. + * + * @param flags - Space-separated string of `compactc` flags (e.g., "--skip-zk --no-communications-commitment") + * @throws {Error} If the `compactc` binary is not found at the resolved path + */ + constructor(flags: string) { + this.flags = flags.trim(); + const spinner = ora(); + + spinner.info(chalk.blue(`[COMPILE] COMPACT_HOME: ${COMPACT_HOME}`)); + spinner.info(chalk.blue(`[COMPILE] COMPACTC_PATH: ${COMPACTC_PATH}`)); + + if (!existsSync(COMPACTC_PATH)) { + spinner.fail( + chalk.red( + `[COMPILE] Error: compactc not found at ${COMPACTC_PATH}. Set COMPACT_HOME to the compactc binary path.`, + ), + ); + throw new Error(`compactc not found at ${COMPACTC_PATH}`); + } + } + + /** + * Compiles all `.compact` files in the source directory and its subdirectories (e.g., `src/test/mock/`). + * Scans the `src` directory recursively for `.compact` files, compiles each one using `compactc`, + * and displays progress with a spinner and colored output. + * + * @returns A promise that resolves when all files are compiled successfully + * @throws {Error} If compilation fails for any file + */ + public async compile(): Promise { + const compactFiles: string[] = readdirSync(SRC_DIR, { + recursive: true, + withFileTypes: true, + }) + .filter((dirent): boolean => { + const filePath = join(dirent.path, dirent.name); + return dirent.isFile() && filePath.endsWith('.compact'); + }) + .map((dirent): string => + join(dirent.path, dirent.name).replace(`${SRC_DIR}/`, ''), + ); + + const spinner = ora(); + if (compactFiles.length === 0) { + spinner.warn(chalk.yellow('[COMPILE] No .compact files found.')); + return; + } + + spinner.info( + chalk.blue( + `[COMPILE] Found ${compactFiles.length} .compact file(s) to compile`, + ), + ); + + for (const [index, file] of compactFiles.entries()) { + await this.compileFile(file, index, compactFiles.length); + } + } + + /** + * Compiles a single `.compact` file. + * Executes the `compactc` compiler with the provided flags, input file, and output directory. + * + * @param file - Relative path of the `.compact` file to compile (e.g., "test/mock/MockFile.compact") + * @param index - Current file index (0-based) for progress display + * @param total - Total number of files to compile for progress display + * @returns A promise that resolves when the file is compiled successfully + * @throws {Error} If compilation fails + */ + private async compileFile( + file: string, + index: number, + total: number, + ): Promise { + const execAsync = promisify(execCallback); + const inputPath: string = join(SRC_DIR, file); + const outputDir: string = join(ARTIFACTS_DIR, basename(file, '.compact')); + const step: string = `[${index + 1}/${total}]`; + const spinner: Ora = ora( + chalk.blue(`[COMPILE] ${step} Compiling ${file}`), + ).start(); + + try { + const command: string = + `${COMPACTC_PATH} ${this.flags} "${inputPath}" "${outputDir}"`.trim(); + spinner.text = chalk.blue(`[COMPILE] ${step} Running: ${command}`); + const { stdout, stderr }: { stdout: string; stderr: string } = + await execAsync(command); + spinner.succeed(chalk.green(`[COMPILE] ${step} Compiled ${file}`)); + this.printOutput(stdout, chalk.cyan); + this.printOutput(stderr, chalk.yellow); + } catch (error: any) { + spinner.fail(chalk.red(`[COMPILE] ${step} Failed ${file}`)); + this.printOutput(error.stdout, chalk.cyan); + this.printOutput(error.stderr, chalk.red); + throw error; + } + } + + /** + * Prints compiler output with indentation and specified color. + * + * @param output - The compiler output string to print (stdout or stderr) + * @param colorFn - Chalk color function to style the output (e.g., `chalk.cyan` for success, `chalk.red` for errors) + */ + private printOutput( + output: string | undefined, + colorFn: (text: string) => string, + ): void { + if (output) { + const lines: string[] = output + .split('\n') + .filter((line: string): boolean => line.trim() !== '') + .map((line: string): string => ` ${line}`); + // biome-ignore lint/suspicious/noConsoleLog: needed for debugging + console.log(colorFn(lines.join('\n'))); + } + } +} diff --git a/compact/src/run-compactc.cjs b/compact/src/run-compactc.cjs deleted file mode 100755 index dca2891f..00000000 --- a/compact/src/run-compactc.cjs +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env node - -const childProcess = require('node:child_process'); -const path = require('node:path'); - -const [_node, _script, ...args] = process.argv; -const COMPACT_HOME_ENV = process.env.COMPACT_HOME; - -let compactPath; -if (COMPACT_HOME_ENV != null) { - compactPath = COMPACT_HOME_ENV; - console.log( - `COMPACT_HOME env variable is set; using Compact from ${compactPath}`, - ); -} else { - compactPath = path.resolve(__dirname, '..', 'compactc'); - console.log( - `COMPACT_HOME env variable is not set; using fetched compact from ${compactPath}`, - ); -} - -// yarn runs everything with node... -const child = childProcess.spawn(path.resolve(compactPath, 'compactc'), args, { - stdio: 'inherit', -}); -child.on('exit', (code, signal) => { - if (code === 0) { - process.exit(0); - } else { - process.exit(code ?? signal); - } -}); diff --git a/compact/src/runBuilder.ts b/compact/src/runBuilder.ts new file mode 100644 index 00000000..bf55d90d --- /dev/null +++ b/compact/src/runBuilder.ts @@ -0,0 +1,43 @@ +#!/usr/bin/env node + +import chalk from 'chalk'; +import ora from 'ora'; +import { CompactBuilder } from './Builder.js'; + +/** + * Executes the Compact builder CLI. + * Builds projects using the `CompactBuilder` class with provided flags, including compilation and additional steps. + * + * @example + * ```bash + * npx compact-builder --skip-zk + * ``` + * Expected output: + * ``` + * ℹ [BUILD] Compact builder started + * ℹ [COMPILE] COMPACT_HOME: /path/to/compactc + * ℹ [COMPILE] COMPACTC_PATH: /path/to/compactc/compactc + * ℹ [COMPILE] Found 1 .compact file(s) to compile + * ✔ [COMPILE] [1/1] Compiled Foo.compact + * Compactc version: 0.22.0 + * ✔ [BUILD] [1/3] Compiling TypeScript + * ✔ [BUILD] [2/3] Copying artifacts + * ✔ [BUILD] [3/3] Copying and cleaning .compact files + * ``` + */ +async function runBuilder(): Promise { + const spinner = ora(chalk.blue('[BUILD] Compact Builder started')).info(); + + try { + const compilerFlags = process.argv.slice(2).join(' '); + const builder = new CompactBuilder(compilerFlags); + await builder.build(); + } catch (err) { + spinner.fail( + chalk.red('[BUILD] Unexpected error:', (err as Error).message), + ); + process.exit(1); + } +} + +runBuilder(); diff --git a/compact/src/runCompiler.ts b/compact/src/runCompiler.ts new file mode 100644 index 00000000..27de79d0 --- /dev/null +++ b/compact/src/runCompiler.ts @@ -0,0 +1,40 @@ +#!/usr/bin/env node + +import chalk from 'chalk'; +import ora from 'ora'; +import { CompactCompiler } from './Compiler.js'; + +/** + * Executes the Compact compiler CLI. + * Compiles `.compact` files using the `CompactCompiler` class with provided flags. + * + * @example + * ```bash + * npx compact-compiler --skip-zk + * ``` + * Expected output: + * ``` + * ℹ [COMPILE] Compact compiler started + * ℹ [COMPILE] COMPACT_HOME: /path/to/compactc + * ℹ [COMPILE] COMPACTC_PATH: /path/to/compactc/compactc + * ℹ [COMPILE] Found 1 .compact file(s) to compile + * ✔ [COMPILE] [1/1] Compiled Foo.compact + * Compactc version: 0.22.0 + * ``` + */ +async function runCompiler(): Promise { + const spinner = ora(chalk.blue('[COMPILE] Compact Compiler started')).info(); + + try { + const compilerFlags = process.argv.slice(2).join(' '); + const compiler = new CompactCompiler(compilerFlags); + await compiler.compile(); + } catch (err) { + spinner.fail( + chalk.red('[COMPILE] Unexpected error:', (err as Error).message), + ); + process.exit(1); + } +} + +runCompiler(); diff --git a/compact/tsconfig.json b/compact/tsconfig.json new file mode 100644 index 00000000..c81efc66 --- /dev/null +++ b/compact/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "moduleResolution": "node", + "declaration": true, + "sourceMap": true + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "dist" + ] +} From 69e86c141ec0f09c9143deb20f8b153b129f45ed Mon Sep 17 00:00:00 2001 From: andrew Date: Sun, 27 Apr 2025 01:48:43 -0500 Subject: [PATCH 018/322] update scripts --- contracts/erc20/package.json | 6 +++--- contracts/utils/package.json | 7 +++++-- package.json | 3 ++- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/contracts/erc20/package.json b/contracts/erc20/package.json index c64d5517..922efaa5 100644 --- a/contracts/erc20/package.json +++ b/contracts/erc20/package.json @@ -13,9 +13,9 @@ } }, "scripts": { - "compact": "find src -name '*.compact' -exec sh -c 'run-compactc \"{}\" \"src/artifacts/$(basename \"{}\" .compact)\"' \\;", + "compact": "npx compact-compiler", + "build": "npx compact-builder && tsc", "test": "NODE_OPTIONS=--experimental-vm-modules jest", - "build": "rm -rf dist && tsc --project tsconfig.build.json && cp -Rf ./src/artifacts ./dist/artifacts && cp ./src/ERC20.compact ./dist", "types": "tsc -p tsconfig.json --noEmit", "fmt": "biome format", "fmt:fix": "biome format --write", @@ -24,6 +24,6 @@ "clean": "git clean -fXd" }, "dependencies": { - "@openzeppelin-midnight/utils": "workspace:^" + "@openzeppelin-midnight/compact": "workspace:^" } } diff --git a/contracts/utils/package.json b/contracts/utils/package.json index 8f5ecb26..8389ab1d 100644 --- a/contracts/utils/package.json +++ b/contracts/utils/package.json @@ -13,14 +13,17 @@ } }, "scripts": { - "compact": "find src -name '*.compact' -exec sh -c 'run-compactc \"{}\" \"src/artifacts/$(basename \"{}\" .compact)\"' \\;", + "compact": "npx compact-compiler", + "build": "npx compact-builder && tsc", "test": "NODE_OPTIONS=--experimental-vm-modules jest", - "build": "rm -rf dist && tsc --project tsconfig.build.json && cp -Rf ./src/artifacts ./dist/artifacts && cp ./src/{Initializable,Pausable,Utils}.compact ./dist", "types": "tsc -p tsconfig.json --noEmit", "fmt": "biome format", "fmt:fix": "biome format --write", "lint": "biome lint", "lint:fix": "biome check --write", "clean": "git clean -fXd" + }, + "dependencies": { + "@openzeppelin-midnight/compact": "workspace:^" } } diff --git a/package.json b/package.json index 93bbacc3..0a6f059f 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,12 @@ { "packageManager": "yarn@4.1.0", "workspaces": [ - "compact", + "compact/", "contracts/erc20/", "contracts/utils/" ], "scripts": { + "prepare": "npx tsc -p ./compact && yarn rebuild", "compact": "turbo run compact", "build": "turbo run build", "test": "turbo run test", From 065f48be753b7719ed93def188eef8bb23f94b92 Mon Sep 17 00:00:00 2001 From: andrew Date: Sun, 27 Apr 2025 01:49:01 -0500 Subject: [PATCH 019/322] update yarn.lock --- yarn.lock | 1055 +++++++++++++++++------------------------------------ 1 file changed, 329 insertions(+), 726 deletions(-) diff --git a/yarn.lock b/yarn.lock index 4b43ba7e..866086ec 100644 --- a/yarn.lock +++ b/yarn.lock @@ -26,7 +26,7 @@ __metadata: languageName: node linkType: hard -"@babel/compat-data@npm:^7.26.5": +"@babel/compat-data@npm:^7.26.8": version: 7.26.8 resolution: "@babel/compat-data@npm:7.26.8" checksum: 10/bdddf577f670e0e12996ef37e134856c8061032edb71a13418c3d4dae8135da28910b7cd6dec6e668ab3a41e42089ef7ee9c54ef52fe0860b54cb420b0d14948 @@ -34,51 +34,51 @@ __metadata: linkType: hard "@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.23.9": - version: 7.26.9 - resolution: "@babel/core@npm:7.26.9" + version: 7.26.10 + resolution: "@babel/core@npm:7.26.10" dependencies: "@ampproject/remapping": "npm:^2.2.0" "@babel/code-frame": "npm:^7.26.2" - "@babel/generator": "npm:^7.26.9" + "@babel/generator": "npm:^7.26.10" "@babel/helper-compilation-targets": "npm:^7.26.5" "@babel/helper-module-transforms": "npm:^7.26.0" - "@babel/helpers": "npm:^7.26.9" - "@babel/parser": "npm:^7.26.9" + "@babel/helpers": "npm:^7.26.10" + "@babel/parser": "npm:^7.26.10" "@babel/template": "npm:^7.26.9" - "@babel/traverse": "npm:^7.26.9" - "@babel/types": "npm:^7.26.9" + "@babel/traverse": "npm:^7.26.10" + "@babel/types": "npm:^7.26.10" convert-source-map: "npm:^2.0.0" debug: "npm:^4.1.0" gensync: "npm:^1.0.0-beta.2" json5: "npm:^2.2.3" semver: "npm:^6.3.1" - checksum: 10/ceed199dbe25f286a0a59a2ea7879aed37c1f3bb289375d061eda4752cab2ba365e7f9e969c7fd3b9b95c930493db6eeb5a6d6f017dd135fb5a4503449aad753 + checksum: 10/68f6707eebd6bb8beed7ceccf5153e35b86c323e40d11d796d75c626ac8f1cc4e1f795584c5ab5f886bc64150c22d5088123d68c069c63f29984c4fc054d1dab languageName: node linkType: hard -"@babel/generator@npm:^7.26.9, @babel/generator@npm:^7.7.2": - version: 7.26.9 - resolution: "@babel/generator@npm:7.26.9" +"@babel/generator@npm:^7.26.10, @babel/generator@npm:^7.27.0, @babel/generator@npm:^7.7.2": + version: 7.27.0 + resolution: "@babel/generator@npm:7.27.0" dependencies: - "@babel/parser": "npm:^7.26.9" - "@babel/types": "npm:^7.26.9" + "@babel/parser": "npm:^7.27.0" + "@babel/types": "npm:^7.27.0" "@jridgewell/gen-mapping": "npm:^0.3.5" "@jridgewell/trace-mapping": "npm:^0.3.25" jsesc: "npm:^3.0.2" - checksum: 10/95075dd6158a49efcc71d7f2c5d20194fcf245348de7723ca35e37cd5800587f1d4de2be6c4ba87b5f5fbb967c052543c109eaab14b43f6a73eb05ccd9a5bb44 + checksum: 10/5447c402b1d841132534a0a9715e89f4f28b6f2886a23e70aaa442150dba4a1e29e4e2351814f439ee1775294dccdef9ab0a4192b6e6a5ad44e24233b3611da2 languageName: node linkType: hard "@babel/helper-compilation-targets@npm:^7.26.5": - version: 7.26.5 - resolution: "@babel/helper-compilation-targets@npm:7.26.5" + version: 7.27.0 + resolution: "@babel/helper-compilation-targets@npm:7.27.0" dependencies: - "@babel/compat-data": "npm:^7.26.5" + "@babel/compat-data": "npm:^7.26.8" "@babel/helper-validator-option": "npm:^7.25.9" browserslist: "npm:^4.24.0" lru-cache: "npm:^5.1.1" semver: "npm:^6.3.1" - checksum: 10/f3b5f0bfcd7b6adf03be1a494b269782531c6e415afab2b958c077d570371cf1bfe001c442508092c50ed3711475f244c05b8f04457d8dea9c34df2b741522bf + checksum: 10/32224b512e813fc808539b4ca7fca8c224849487c365abcef8cb8b0eea635c65375b81429f82d076e9ec1f3f3b3db1d0d56aac4d482a413f58d5ad608f912155 languageName: node linkType: hard @@ -133,24 +133,24 @@ __metadata: languageName: node linkType: hard -"@babel/helpers@npm:^7.26.9": - version: 7.26.9 - resolution: "@babel/helpers@npm:7.26.9" +"@babel/helpers@npm:^7.26.10": + version: 7.27.0 + resolution: "@babel/helpers@npm:7.27.0" dependencies: - "@babel/template": "npm:^7.26.9" - "@babel/types": "npm:^7.26.9" - checksum: 10/267dfa7d04dff7720610497f466aa7b60652b7ec8dde5914527879350c9d655271e892117c5b2f0f083d92d2a8e5e2cf9832d4f98cd7fb72d78f796002af19a1 + "@babel/template": "npm:^7.27.0" + "@babel/types": "npm:^7.27.0" + checksum: 10/0dd40ba1e5ba4b72d1763bb381384585a56f21a61a19dc1b9a03381fe8e840207fdaa4da645d14dc028ad768087d41aad46347cc6573bd69d82f597f5a12dc6f languageName: node linkType: hard -"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.23.9, @babel/parser@npm:^7.26.9": - version: 7.26.9 - resolution: "@babel/parser@npm:7.26.9" +"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.23.9, @babel/parser@npm:^7.26.10, @babel/parser@npm:^7.27.0": + version: 7.27.0 + resolution: "@babel/parser@npm:7.27.0" dependencies: - "@babel/types": "npm:^7.26.9" + "@babel/types": "npm:^7.27.0" bin: parser: ./bin/babel-parser.js - checksum: 10/cb84fe3ba556d6a4360f3373cf7eb0901c46608c8d77330cc1ca021d60f5d6ebb4056a8e7f9dd0ef231923ef1fe69c87b11ce9e160d2252e089a20232a2b942b + checksum: 10/0fee9f05c6db753882ca9d10958301493443da9f6986d7020ebd7a696b35886240016899bc0b47d871aea2abcafd64632343719742e87432c8145e0ec2af2a03 languageName: node linkType: hard @@ -341,39 +341,39 @@ __metadata: languageName: node linkType: hard -"@babel/template@npm:^7.26.9, @babel/template@npm:^7.3.3": - version: 7.26.9 - resolution: "@babel/template@npm:7.26.9" +"@babel/template@npm:^7.26.9, @babel/template@npm:^7.27.0, @babel/template@npm:^7.3.3": + version: 7.27.0 + resolution: "@babel/template@npm:7.27.0" dependencies: "@babel/code-frame": "npm:^7.26.2" - "@babel/parser": "npm:^7.26.9" - "@babel/types": "npm:^7.26.9" - checksum: 10/240288cebac95b1cc1cb045ad143365643da0470e905e11731e63280e43480785bd259924f4aea83898ef68e9fa7c176f5f2d1e8b0a059b27966e8ca0b41a1b6 + "@babel/parser": "npm:^7.27.0" + "@babel/types": "npm:^7.27.0" + checksum: 10/7159ca1daea287ad34676d45a7146675444d42c7664aca3e617abc9b1d9548c8f377f35a36bb34cf956e1d3610dcb7acfcfe890aebf81880d35f91a7bd273ee5 languageName: node linkType: hard -"@babel/traverse@npm:^7.25.9, @babel/traverse@npm:^7.26.9": - version: 7.26.9 - resolution: "@babel/traverse@npm:7.26.9" +"@babel/traverse@npm:^7.25.9, @babel/traverse@npm:^7.26.10": + version: 7.27.0 + resolution: "@babel/traverse@npm:7.27.0" dependencies: "@babel/code-frame": "npm:^7.26.2" - "@babel/generator": "npm:^7.26.9" - "@babel/parser": "npm:^7.26.9" - "@babel/template": "npm:^7.26.9" - "@babel/types": "npm:^7.26.9" + "@babel/generator": "npm:^7.27.0" + "@babel/parser": "npm:^7.27.0" + "@babel/template": "npm:^7.27.0" + "@babel/types": "npm:^7.27.0" debug: "npm:^4.3.1" globals: "npm:^11.1.0" - checksum: 10/c16a79522eafa0a7e40eb556bf1e8a3d50dbb0ff943a80f2c06cee2ec7ff87baa0c5d040a5cff574d9bcb3bed05e7d8c6f13b238a931c97267674b02c6cf45b4 + checksum: 10/b0675bc16bd87187e8b090557b0650135de56a621692ad8614b20f32621350ae0fc2e1129b73b780d64a9ed4beab46849a17f90d5267b6ae6ce09ec8412a12c7 languageName: node linkType: hard -"@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.25.9, @babel/types@npm:^7.26.9, @babel/types@npm:^7.3.3": - version: 7.26.9 - resolution: "@babel/types@npm:7.26.9" +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.25.9, @babel/types@npm:^7.26.10, @babel/types@npm:^7.27.0, @babel/types@npm:^7.3.3": + version: 7.27.0 + resolution: "@babel/types@npm:7.27.0" dependencies: "@babel/helper-string-parser": "npm:^7.25.9" "@babel/helper-validator-identifier": "npm:^7.25.9" - checksum: 10/11b62ea7ed64ef7e39cc9b33852c1084064c3b970ae0eaa5db659241cfb776577d1e68cbff4de438bada885d3a827b52cc0f3746112d8e1bc672bb99a8eb5b56 + checksum: 10/2c322bce107c8a534dc4a23be60d570e6a4cc7ca2e44d4f0eee08c0b626104eb7e60ab8de03463bc5da1773a2f69f1e6edec1648d648d65461d6520a7f3b0770 languageName: node linkType: hard @@ -484,73 +484,6 @@ __metadata: languageName: node linkType: hard -"@eslint-community/eslint-utils@npm:^4.2.0": - version: 4.4.1 - resolution: "@eslint-community/eslint-utils@npm:4.4.1" - dependencies: - eslint-visitor-keys: "npm:^3.4.3" - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - checksum: 10/ae92a11412674329b4bd38422518601ec9ceae28e251104d1cad83715da9d38e321f68c817c39b64e66d0af7d98df6f9a10ad2dc638911254b47fb8932df00ef - languageName: node - linkType: hard - -"@eslint-community/regexpp@npm:^4.6.1": - version: 4.12.1 - resolution: "@eslint-community/regexpp@npm:4.12.1" - checksum: 10/c08f1dd7dd18fbb60bdd0d85820656d1374dd898af9be7f82cb00451313402a22d5e30569c150315b4385907cdbca78c22389b2a72ab78883b3173be317620cc - languageName: node - linkType: hard - -"@eslint/eslintrc@npm:^2.1.4": - version: 2.1.4 - resolution: "@eslint/eslintrc@npm:2.1.4" - dependencies: - ajv: "npm:^6.12.4" - debug: "npm:^4.3.2" - espree: "npm:^9.6.0" - globals: "npm:^13.19.0" - ignore: "npm:^5.2.0" - import-fresh: "npm:^3.2.1" - js-yaml: "npm:^4.1.0" - minimatch: "npm:^3.1.2" - strip-json-comments: "npm:^3.1.1" - checksum: 10/7a3b14f4b40fc1a22624c3f84d9f467a3d9ea1ca6e9a372116cb92507e485260359465b58e25bcb6c9981b155416b98c9973ad9b796053fd7b3f776a6946bce8 - languageName: node - linkType: hard - -"@eslint/js@npm:8.57.1": - version: 8.57.1 - resolution: "@eslint/js@npm:8.57.1" - checksum: 10/7562b21be10c2adbfa4aa5bb2eccec2cb9ac649a3569560742202c8d1cb6c931ce634937a2f0f551e078403a1c1285d6c2c0aa345dafc986149665cd69fe8b59 - languageName: node - linkType: hard - -"@humanwhocodes/config-array@npm:^0.13.0": - version: 0.13.0 - resolution: "@humanwhocodes/config-array@npm:0.13.0" - dependencies: - "@humanwhocodes/object-schema": "npm:^2.0.3" - debug: "npm:^4.3.1" - minimatch: "npm:^3.0.5" - checksum: 10/524df31e61a85392a2433bf5d03164e03da26c03d009f27852e7dcfdafbc4a23f17f021dacf88e0a7a9fe04ca032017945d19b57a16e2676d9114c22a53a9d11 - languageName: node - linkType: hard - -"@humanwhocodes/module-importer@npm:^1.0.1": - version: 1.0.1 - resolution: "@humanwhocodes/module-importer@npm:1.0.1" - checksum: 10/e993950e346331e5a32eefb27948ecdee2a2c4ab3f072b8f566cd213ef485dd50a3ca497050608db91006f5479e43f91a439aef68d2a313bd3ded06909c7c5b3 - languageName: node - linkType: hard - -"@humanwhocodes/object-schema@npm:^2.0.3": - version: 2.0.3 - resolution: "@humanwhocodes/object-schema@npm:2.0.3" - checksum: 10/05bb99ed06c16408a45a833f03a732f59bf6184795d4efadd33238ff8699190a8c871ad1121241bb6501589a9598dc83bf25b99dcbcf41e155cdf36e35e937a3 - languageName: node - linkType: hard - "@isaacs/cliui@npm:^8.0.2": version: 8.0.2 resolution: "@isaacs/cliui@npm:8.0.2" @@ -887,18 +820,6 @@ __metadata: languageName: node linkType: hard -"@midnight-ntwrk/compact@workspace:compact": - version: 0.0.0-use.local - resolution: "@midnight-ntwrk/compact@workspace:compact" - dependencies: - eslint: "npm:^8.52.0" - ts-node: "npm:^10.9.2" - typescript: "npm:^5.2.2" - bin: - run-compactc: src/run-compactc.cjs - languageName: unknown - linkType: soft - "@midnight-ntwrk/ledger@npm:^3.0.6": version: 3.0.6 resolution: "@midnight-ntwrk/ledger@npm:3.0.6" @@ -920,33 +841,6 @@ __metadata: languageName: node linkType: hard -"@nodelib/fs.scandir@npm:2.1.5": - version: 2.1.5 - resolution: "@nodelib/fs.scandir@npm:2.1.5" - dependencies: - "@nodelib/fs.stat": "npm:2.0.5" - run-parallel: "npm:^1.1.9" - checksum: 10/6ab2a9b8a1d67b067922c36f259e3b3dfd6b97b219c540877a4944549a4d49ea5ceba5663905ab5289682f1f3c15ff441d02f0447f620a42e1cb5e1937174d4b - languageName: node - linkType: hard - -"@nodelib/fs.stat@npm:2.0.5": - version: 2.0.5 - resolution: "@nodelib/fs.stat@npm:2.0.5" - checksum: 10/012480b5ca9d97bff9261571dbbec7bbc6033f69cc92908bc1ecfad0792361a5a1994bc48674b9ef76419d056a03efadfce5a6cf6dbc0a36559571a7a483f6f0 - languageName: node - linkType: hard - -"@nodelib/fs.walk@npm:^1.2.8": - version: 1.2.8 - resolution: "@nodelib/fs.walk@npm:1.2.8" - dependencies: - "@nodelib/fs.scandir": "npm:2.1.5" - fastq: "npm:^1.6.0" - checksum: 10/40033e33e96e97d77fba5a238e4bba4487b8284678906a9f616b5579ddaf868a18874c0054a75402c9fbaaa033a25ceae093af58c9c30278e35c23c9479e79b0 - languageName: node - linkType: hard - "@npmcli/agent@npm:^3.0.0": version: 3.0.0 resolution: "@npmcli/agent@npm:3.0.0" @@ -969,17 +863,39 @@ __metadata: languageName: node linkType: hard +"@openzeppelin-midnight/compact@workspace:^, @openzeppelin-midnight/compact@workspace:compact": + version: 0.0.0-use.local + resolution: "@openzeppelin-midnight/compact@workspace:compact" + dependencies: + "@types/jest": "npm:^29.5.6" + "@types/node": "npm:^22.13.10" + chalk: "npm:^5.4.1" + fast-check: "npm:^3.15.0" + jest: "npm:^29.7.0" + jest-fast-check: "npm:^2.0.0" + log-symbols: "npm:^7.0.0" + ora: "npm:^8.2.0" + ts-jest: "npm:^29.1.1" + typescript: "npm:^5.8.2" + bin: + compact-builder: dist/runBuilder.js + compact-compiler: dist/runCompiler.js + languageName: unknown + linkType: soft + "@openzeppelin-midnight/erc20@workspace:contracts/erc20": version: 0.0.0-use.local resolution: "@openzeppelin-midnight/erc20@workspace:contracts/erc20" dependencies: - "@openzeppelin-midnight/utils": "workspace:^" + "@openzeppelin-midnight/compact": "workspace:^" languageName: unknown linkType: soft -"@openzeppelin-midnight/utils@workspace:^, @openzeppelin-midnight/utils@workspace:contracts/utils": +"@openzeppelin-midnight/utils@workspace:contracts/utils": version: 0.0.0-use.local resolution: "@openzeppelin-midnight/utils@workspace:contracts/utils" + dependencies: + "@openzeppelin-midnight/compact": "workspace:^" languageName: unknown linkType: soft @@ -1057,11 +973,11 @@ __metadata: linkType: hard "@types/babel__generator@npm:*": - version: 7.6.8 - resolution: "@types/babel__generator@npm:7.6.8" + version: 7.27.0 + resolution: "@types/babel__generator@npm:7.27.0" dependencies: "@babel/types": "npm:^7.0.0" - checksum: 10/b53c215e9074c69d212402990b0ca8fa57595d09e10d94bda3130aa22b55d796e50449199867879e4ea0ee968f3a2099e009cfb21a726a53324483abbf25cd30 + checksum: 10/f572e67a9a39397664350a4437d8a7fbd34acc83ff4887a8cf08349e39f8aeb5ad2f70fb78a0a0a23a280affe3a5f4c25f50966abdce292bcf31237af1c27b1a languageName: node linkType: hard @@ -1076,11 +992,11 @@ __metadata: linkType: hard "@types/babel__traverse@npm:*, @types/babel__traverse@npm:^7.0.6": - version: 7.20.6 - resolution: "@types/babel__traverse@npm:7.20.6" + version: 7.20.7 + resolution: "@types/babel__traverse@npm:7.20.7" dependencies: "@babel/types": "npm:^7.20.7" - checksum: 10/63d13a3789aa1e783b87a8b03d9fb2c2c90078de7782422feff1631b8c2a25db626e63a63ac5a1465d47359201c73069dacb4b52149d17c568187625da3064ae + checksum: 10/d005b58e1c26bdafc1ce564f60db0ee938393c7fc586b1197bdb71a02f7f33f72bc10ae4165776b6cafc77c4b6f2e1a164dd20bc36518c471b1131b153b4baa6 languageName: node linkType: hard @@ -1128,21 +1044,21 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*": - version: 22.13.8 - resolution: "@types/node@npm:22.13.8" +"@types/node@npm:*, @types/node@npm:^22.13.10": + version: 22.15.2 + resolution: "@types/node@npm:22.15.2" dependencies: - undici-types: "npm:~6.20.0" - checksum: 10/b69de3caab80336747bf41b5063478d23b196b9594c6f2eb819791380cc571676087dceb0fde9531ef7a1293f3eae12a9aaf79d8de349378c29a17c5e657bc78 + undici-types: "npm:~6.21.0" + checksum: 10/e22071571205413518aa3710644ed9603d8f4a417fc59f0e180240e1c05aaf7fb8feecdf553a2da305247b3533d03b58eab6e333115f01f581b9139a6b1dcd47 languageName: node linkType: hard "@types/node@npm:^18.18.6": - version: 18.19.78 - resolution: "@types/node@npm:18.19.78" + version: 18.19.87 + resolution: "@types/node@npm:18.19.87" dependencies: undici-types: "npm:~5.26.4" - checksum: 10/c9b285b965c054d4ffd511fd31c18b3d1512dfdf2af0f0340b19a225716aedc2a26d87dadc2dd27b33e2fa6cbcdacff2bb8bf70f91978b2ef3b18dfa327afdfb + checksum: 10/1e71b6d16dedeaa1fd5ff55baf1f353ca1f9e673b2e482d7fe82fa685addea5159a36602a344784c989b5e07ca1be633d0c493adf5951dee5a29cee69d613e7f languageName: node linkType: hard @@ -1176,26 +1092,10 @@ __metadata: languageName: node linkType: hard -"@ungap/structured-clone@npm:^1.2.0": - version: 1.3.0 - resolution: "@ungap/structured-clone@npm:1.3.0" - checksum: 10/80d6910946f2b1552a2406650051c91bbd1f24a6bf854354203d84fe2714b3e8ce4618f49cc3410494173a1c1e8e9777372fe68dce74bd45faf0a7a1a6ccf448 - languageName: node - linkType: hard - "abbrev@npm:^3.0.0": - version: 3.0.0 - resolution: "abbrev@npm:3.0.0" - checksum: 10/2ceee14efdeda42ef7355178c1069499f183546ff7112b3efe79c1edef09d20ad9c17939752215fb8f7fcf48d10e6a7c0aa00136dc9cf4d293d963718bb1d200 - languageName: node - linkType: hard - -"acorn-jsx@npm:^5.3.2": - version: 5.3.2 - resolution: "acorn-jsx@npm:5.3.2" - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - checksum: 10/d4371eaef7995530b5b5ca4183ff6f062ca17901a6d3f673c9ac011b01ede37e7a1f7f61f8f5cfe709e88054757bb8f3277dc4061087cdf4f2a1f90ccbcdb977 + version: 3.0.1 + resolution: "abbrev@npm:3.0.1" + checksum: 10/ebd2c149dda6f543b66ce3779ea612151bb3aa9d0824f169773ee9876f1ca5a4e0adbcccc7eed048c04da7998e1825e2aa76fcca92d9e67dea50ac2b0a58dc2e languageName: node linkType: hard @@ -1208,12 +1108,12 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^8.11.0, acorn@npm:^8.4.1, acorn@npm:^8.9.0": - version: 8.14.0 - resolution: "acorn@npm:8.14.0" +"acorn@npm:^8.11.0, acorn@npm:^8.4.1": + version: 8.14.1 + resolution: "acorn@npm:8.14.1" bin: acorn: bin/acorn - checksum: 10/6df29c35556782ca9e632db461a7f97947772c6c1d5438a81f0c873a3da3a792487e83e404d1c6c25f70513e91aa18745f6eafb1fcc3a43ecd1920b21dd173d2 + checksum: 10/d1379bbee224e8d44c3c3946e6ba6973e999fbdd4e22e41c3455d7f9b6f72f7ce18d3dc218002e1e48eea789539cf1cb6d1430c81838c6744799c712fb557d92 languageName: node linkType: hard @@ -1224,18 +1124,6 @@ __metadata: languageName: node linkType: hard -"ajv@npm:^6.12.4": - version: 6.12.6 - resolution: "ajv@npm:6.12.6" - dependencies: - fast-deep-equal: "npm:^3.1.1" - fast-json-stable-stringify: "npm:^2.0.0" - json-schema-traverse: "npm:^0.4.1" - uri-js: "npm:^4.2.2" - checksum: 10/48d6ad21138d12eb4d16d878d630079a2bda25a04e745c07846a4ad768319533031e28872a9b3c5790fa1ec41aabdf2abed30a56e5a03ebc2cf92184b8ee306c - languageName: node - linkType: hard - "ansi-escapes@npm:^4.2.1": version: 4.3.2 resolution: "ansi-escapes@npm:4.3.2" @@ -1308,13 +1196,6 @@ __metadata: languageName: node linkType: hard -"argparse@npm:^2.0.1": - version: 2.0.1 - resolution: "argparse@npm:2.0.1" - checksum: 10/18640244e641a417ec75a9bd38b0b2b6b95af5199aa241b131d4b2fb206f334d7ecc600bd194861610a5579084978bfcbb02baa399dbe442d56d0ae5e60dbaef - languageName: node - linkType: hard - "async@npm:^3.2.3": version: 3.2.6 resolution: "async@npm:3.2.6" @@ -1517,9 +1398,9 @@ __metadata: linkType: hard "caniuse-lite@npm:^1.0.30001688": - version: 1.0.30001701 - resolution: "caniuse-lite@npm:1.0.30001701" - checksum: 10/d121607a96f9165128203a317d6aee6a4c7808d52a1f3b46ef5fb918abe9e9d4463e57b0bd5ffe2f4316292bd5b8d85a832b4456b7ca6f024f377b498911bfec + version: 1.0.30001715 + resolution: "caniuse-lite@npm:1.0.30001715" + checksum: 10/5608cdaf609eb5fe3a86ab6c1c2f3943dbdab813041725f4747f5432b05e6e19fc606faa8a9b75c329b37b772c91c47e8db483e76a6b715b59c289ce53dcba68 languageName: node linkType: hard @@ -1533,6 +1414,13 @@ __metadata: languageName: node linkType: hard +"chalk@npm:^5.3.0, chalk@npm:^5.4.1": + version: 5.4.1 + resolution: "chalk@npm:5.4.1" + checksum: 10/29df3ffcdf25656fed6e95962e2ef86d14dfe03cd50e7074b06bad9ffbbf6089adbb40f75c00744d843685c8d008adaf3aed31476780312553caf07fa86e5bc7 + languageName: node + linkType: hard + "char-regex@npm:^1.0.2": version: 1.0.2 resolution: "char-regex@npm:1.0.2" @@ -1561,6 +1449,22 @@ __metadata: languageName: node linkType: hard +"cli-cursor@npm:^5.0.0": + version: 5.0.0 + resolution: "cli-cursor@npm:5.0.0" + dependencies: + restore-cursor: "npm:^5.0.0" + checksum: 10/1eb9a3f878b31addfe8d82c6d915ec2330cec8447ab1f117f4aa34f0137fbb3137ec3466e1c9a65bcb7557f6e486d343f2da57f253a2f668d691372dfa15c090 + languageName: node + linkType: hard + +"cli-spinners@npm:^2.9.2": + version: 2.9.2 + resolution: "cli-spinners@npm:2.9.2" + checksum: 10/a0a863f442df35ed7294424f5491fa1756bd8d2e4ff0c8736531d886cec0ece4d85e8663b77a5afaf1d296e3cbbebff92e2e99f52bbea89b667cbe789b994794 + languageName: node + linkType: hard + "cliui@npm:^8.0.1": version: 8.0.1 resolution: "cliui@npm:8.0.1" @@ -1640,7 +1544,7 @@ __metadata: languageName: node linkType: hard -"cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3, cross-spawn@npm:^7.0.6": +"cross-spawn@npm:^7.0.3, cross-spawn@npm:^7.0.6": version: 7.0.6 resolution: "cross-spawn@npm:7.0.6" dependencies: @@ -1651,7 +1555,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4": +"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.4": version: 4.4.0 resolution: "debug@npm:4.4.0" dependencies: @@ -1675,13 +1579,6 @@ __metadata: languageName: node linkType: hard -"deep-is@npm:^0.1.3": - version: 0.1.4 - resolution: "deep-is@npm:0.1.4" - checksum: 10/ec12d074aef5ae5e81fa470b9317c313142c9e8e2afe3f8efa124db309720db96d1d222b82b84c834e5f87e7a614b44a4684b6683583118b87c833b3be40d4d8 - languageName: node - linkType: hard - "deepmerge@npm:^4.2.2": version: 4.3.1 resolution: "deepmerge@npm:4.3.1" @@ -1710,15 +1607,6 @@ __metadata: languageName: node linkType: hard -"doctrine@npm:^3.0.0": - version: 3.0.0 - resolution: "doctrine@npm:3.0.0" - dependencies: - esutils: "npm:^2.0.2" - checksum: 10/b4b28f1df5c563f7d876e7461254a4597b8cabe915abe94d7c5d1633fed263fcf9a85e8d3836591fc2d040108e822b0d32758e5ec1fe31c590dc7e08086e3e48 - languageName: node - linkType: hard - "eastasianwidth@npm:^0.2.0": version: 0.2.0 resolution: "eastasianwidth@npm:0.2.0" @@ -1738,9 +1626,9 @@ __metadata: linkType: hard "electron-to-chromium@npm:^1.5.73": - version: 1.5.109 - resolution: "electron-to-chromium@npm:1.5.109" - checksum: 10/4f6bd5963a2a55cbff97b2374cb0dbd6141f85e5cf8cb07267d91b0e56f3a4c8df72a7be905ddb1770b9277deef207567e97f94b9385c7cba3775620af17a932 + version: 1.5.143 + resolution: "electron-to-chromium@npm:1.5.143" + checksum: 10/91a7980f96da5ad33b77e95c7e628f468e6bb53eac41437612882810a7552514132728f7c34ee5c967c599557af491fcc4f75e9138f82d22c1b1cdfc63fb8d6b languageName: node linkType: hard @@ -1751,6 +1639,13 @@ __metadata: languageName: node linkType: hard +"emoji-regex@npm:^10.3.0": + version: 10.4.0 + resolution: "emoji-regex@npm:10.4.0" + checksum: 10/76bb92c5bcf0b6980d37e535156231e4a9d0aa6ab3b9f5eabf7690231d5aa5d5b8e516f36e6804cbdd0f1c23dfef2a60c40ab7bb8aedd890584281a565b97c50 + languageName: node + linkType: hard + "emoji-regex@npm:^8.0.0": version: 8.0.0 resolution: "emoji-regex@npm:8.0.0" @@ -1811,89 +1706,6 @@ __metadata: languageName: node linkType: hard -"escape-string-regexp@npm:^4.0.0": - version: 4.0.0 - resolution: "escape-string-regexp@npm:4.0.0" - checksum: 10/98b48897d93060f2322108bf29db0feba7dd774be96cd069458d1453347b25ce8682ecc39859d4bca2203cc0ab19c237bcc71755eff49a0f8d90beadeeba5cc5 - languageName: node - linkType: hard - -"eslint-scope@npm:^7.2.2": - version: 7.2.2 - resolution: "eslint-scope@npm:7.2.2" - dependencies: - esrecurse: "npm:^4.3.0" - estraverse: "npm:^5.2.0" - checksum: 10/5c660fb905d5883ad018a6fea2b49f3cb5b1cbf2cd4bd08e98646e9864f9bc2c74c0839bed2d292e90a4a328833accc197c8f0baed89cbe8d605d6f918465491 - languageName: node - linkType: hard - -"eslint-visitor-keys@npm:^3.4.1, eslint-visitor-keys@npm:^3.4.3": - version: 3.4.3 - resolution: "eslint-visitor-keys@npm:3.4.3" - checksum: 10/3f357c554a9ea794b094a09bd4187e5eacd1bc0d0653c3adeb87962c548e6a1ab8f982b86963ae1337f5d976004146536dcee5d0e2806665b193fbfbf1a9231b - languageName: node - linkType: hard - -"eslint@npm:^8.52.0": - version: 8.57.1 - resolution: "eslint@npm:8.57.1" - dependencies: - "@eslint-community/eslint-utils": "npm:^4.2.0" - "@eslint-community/regexpp": "npm:^4.6.1" - "@eslint/eslintrc": "npm:^2.1.4" - "@eslint/js": "npm:8.57.1" - "@humanwhocodes/config-array": "npm:^0.13.0" - "@humanwhocodes/module-importer": "npm:^1.0.1" - "@nodelib/fs.walk": "npm:^1.2.8" - "@ungap/structured-clone": "npm:^1.2.0" - ajv: "npm:^6.12.4" - chalk: "npm:^4.0.0" - cross-spawn: "npm:^7.0.2" - debug: "npm:^4.3.2" - doctrine: "npm:^3.0.0" - escape-string-regexp: "npm:^4.0.0" - eslint-scope: "npm:^7.2.2" - eslint-visitor-keys: "npm:^3.4.3" - espree: "npm:^9.6.1" - esquery: "npm:^1.4.2" - esutils: "npm:^2.0.2" - fast-deep-equal: "npm:^3.1.3" - file-entry-cache: "npm:^6.0.1" - find-up: "npm:^5.0.0" - glob-parent: "npm:^6.0.2" - globals: "npm:^13.19.0" - graphemer: "npm:^1.4.0" - ignore: "npm:^5.2.0" - imurmurhash: "npm:^0.1.4" - is-glob: "npm:^4.0.0" - is-path-inside: "npm:^3.0.3" - js-yaml: "npm:^4.1.0" - json-stable-stringify-without-jsonify: "npm:^1.0.1" - levn: "npm:^0.4.1" - lodash.merge: "npm:^4.6.2" - minimatch: "npm:^3.1.2" - natural-compare: "npm:^1.4.0" - optionator: "npm:^0.9.3" - strip-ansi: "npm:^6.0.1" - text-table: "npm:^0.2.0" - bin: - eslint: bin/eslint.js - checksum: 10/5504fa24879afdd9f9929b2fbfc2ee9b9441a3d464efd9790fbda5f05738858530182029f13323add68d19fec749d3ab4a70320ded091ca4432b1e9cc4ed104c - languageName: node - linkType: hard - -"espree@npm:^9.6.0, espree@npm:^9.6.1": - version: 9.6.1 - resolution: "espree@npm:9.6.1" - dependencies: - acorn: "npm:^8.9.0" - acorn-jsx: "npm:^5.3.2" - eslint-visitor-keys: "npm:^3.4.1" - checksum: 10/255ab260f0d711a54096bdeda93adff0eadf02a6f9b92f02b323e83a2b7fc258797919437ad331efec3930475feb0142c5ecaaf3cdab4befebd336d47d3f3134 - languageName: node - linkType: hard - "esprima@npm:^4.0.0": version: 4.0.1 resolution: "esprima@npm:4.0.1" @@ -1904,38 +1716,6 @@ __metadata: languageName: node linkType: hard -"esquery@npm:^1.4.2": - version: 1.6.0 - resolution: "esquery@npm:1.6.0" - dependencies: - estraverse: "npm:^5.1.0" - checksum: 10/c587fb8ec9ed83f2b1bc97cf2f6854cc30bf784a79d62ba08c6e358bf22280d69aee12827521cf38e69ae9761d23fb7fde593ce315610f85655c139d99b05e5a - languageName: node - linkType: hard - -"esrecurse@npm:^4.3.0": - version: 4.3.0 - resolution: "esrecurse@npm:4.3.0" - dependencies: - estraverse: "npm:^5.2.0" - checksum: 10/44ffcd89e714ea6b30143e7f119b104fc4d75e77ee913f34d59076b40ef2d21967f84e019f84e1fd0465b42cdbf725db449f232b5e47f29df29ed76194db8e16 - languageName: node - linkType: hard - -"estraverse@npm:^5.1.0, estraverse@npm:^5.2.0": - version: 5.3.0 - resolution: "estraverse@npm:5.3.0" - checksum: 10/37cbe6e9a68014d34dbdc039f90d0baf72436809d02edffcc06ba3c2a12eb298048f877511353b130153e532aac8d68ba78430c0dd2f44806ebc7c014b01585e - languageName: node - linkType: hard - -"esutils@npm:^2.0.2": - version: 2.0.3 - resolution: "esutils@npm:2.0.3" - checksum: 10/b23acd24791db11d8f65be5ea58fd9a6ce2df5120ae2da65c16cfc5331ff59d5ac4ef50af66cd4bde238881503ec839928a0135b99a036a9cdfa22d17fd56cdb - languageName: node - linkType: hard - "execa@npm:^5.0.0": version: 5.1.1 resolution: "execa@npm:5.1.1" @@ -1989,36 +1769,13 @@ __metadata: languageName: node linkType: hard -"fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3": - version: 3.1.3 - resolution: "fast-deep-equal@npm:3.1.3" - checksum: 10/e21a9d8d84f53493b6aa15efc9cfd53dd5b714a1f23f67fb5dc8f574af80df889b3bce25dc081887c6d25457cce704e636395333abad896ccdec03abaf1f3f9d - languageName: node - linkType: hard - -"fast-json-stable-stringify@npm:2.x, fast-json-stable-stringify@npm:^2.0.0, fast-json-stable-stringify@npm:^2.1.0": +"fast-json-stable-stringify@npm:2.x, fast-json-stable-stringify@npm:^2.1.0": version: 2.1.0 resolution: "fast-json-stable-stringify@npm:2.1.0" checksum: 10/2c20055c1fa43c922428f16ca8bb29f2807de63e5c851f665f7ac9790176c01c3b40335257736b299764a8d383388dabc73c8083b8e1bc3d99f0a941444ec60e languageName: node linkType: hard -"fast-levenshtein@npm:^2.0.6": - version: 2.0.6 - resolution: "fast-levenshtein@npm:2.0.6" - checksum: 10/eb7e220ecf2bab5159d157350b81d01f75726a4382f5a9266f42b9150c4523b9795f7f5d9fbbbeaeac09a441b2369f05ee02db48ea938584205530fe5693cfe1 - languageName: node - linkType: hard - -"fastq@npm:^1.6.0": - version: 1.19.1 - resolution: "fastq@npm:1.19.1" - dependencies: - reusify: "npm:^1.0.4" - checksum: 10/75679dc226316341c4f2a6b618571f51eac96779906faecd8921b984e844d6ae42fabb2df69b1071327d398d5716693ea9c9c8941f64ac9e89ec2032ce59d730 - languageName: node - linkType: hard - "fb-watchman@npm:^2.0.0": version: 2.0.2 resolution: "fb-watchman@npm:2.0.2" @@ -2028,12 +1785,15 @@ __metadata: languageName: node linkType: hard -"file-entry-cache@npm:^6.0.1": - version: 6.0.1 - resolution: "file-entry-cache@npm:6.0.1" - dependencies: - flat-cache: "npm:^3.0.4" - checksum: 10/099bb9d4ab332cb93c48b14807a6918a1da87c45dce91d4b61fd40e6505d56d0697da060cb901c729c90487067d93c9243f5da3dc9c41f0358483bfdebca736b +"fdir@npm:^6.4.4": + version: 6.4.4 + resolution: "fdir@npm:6.4.4" + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + checksum: 10/d0000d6b790059b35f4ed19acc8847a66452e0bc68b28766c929ffd523e5ec2083811fc8a545e4a1d4945ce70e887b3a610c145c681073b506143ae3076342ed languageName: node linkType: hard @@ -2065,34 +1825,6 @@ __metadata: languageName: node linkType: hard -"find-up@npm:^5.0.0": - version: 5.0.0 - resolution: "find-up@npm:5.0.0" - dependencies: - locate-path: "npm:^6.0.0" - path-exists: "npm:^4.0.0" - checksum: 10/07955e357348f34660bde7920783204ff5a26ac2cafcaa28bace494027158a97b9f56faaf2d89a6106211a8174db650dd9f503f9c0d526b1202d5554a00b9095 - languageName: node - linkType: hard - -"flat-cache@npm:^3.0.4": - version: 3.2.0 - resolution: "flat-cache@npm:3.2.0" - dependencies: - flatted: "npm:^3.2.9" - keyv: "npm:^4.5.3" - rimraf: "npm:^3.0.2" - checksum: 10/02381c6ece5e9fa5b826c9bbea481d7fd77645d96e4b0b1395238124d581d10e56f17f723d897b6d133970f7a57f0fab9148cbbb67237a0a0ffe794ba60c0c70 - languageName: node - linkType: hard - -"flatted@npm:^3.2.9": - version: 3.3.3 - resolution: "flatted@npm:3.3.3" - checksum: 10/8c96c02fbeadcf4e8ffd0fa24983241e27698b0781295622591fc13585e2f226609d95e422bcf2ef044146ffacb6b68b1f20871454eddf75ab3caa6ee5f4a1fe - languageName: node - linkType: hard - "foreground-child@npm:^3.1.0": version: 3.3.1 resolution: "foreground-child@npm:3.3.1" @@ -2159,6 +1891,13 @@ __metadata: languageName: node linkType: hard +"get-east-asian-width@npm:^1.0.0": + version: 1.3.0 + resolution: "get-east-asian-width@npm:1.3.0" + checksum: 10/8e8e779eb28701db7fdb1c8cab879e39e6ae23f52dadd89c8aed05869671cee611a65d4f8557b83e981428623247d8bc5d0c7a4ef3ea7a41d826e73600112ad8 + languageName: node + linkType: hard + "get-package-type@npm:^0.1.0": version: 0.1.0 resolution: "get-package-type@npm:0.1.0" @@ -2173,16 +1912,7 @@ __metadata: languageName: node linkType: hard -"glob-parent@npm:^6.0.2": - version: 6.0.2 - resolution: "glob-parent@npm:6.0.2" - dependencies: - is-glob: "npm:^4.0.3" - checksum: 10/c13ee97978bef4f55106b71e66428eb1512e71a7466ba49025fc2aec59a5bfb0954d5abd58fc5ee6c9b076eef4e1f6d3375c2e964b88466ca390da4419a786a8 - languageName: node - linkType: hard - -"glob@npm:^10.2.2, glob@npm:^10.3.10, glob@npm:^10.3.7": +"glob@npm:^10.2.2": version: 10.4.5 resolution: "glob@npm:10.4.5" dependencies: @@ -2219,15 +1949,6 @@ __metadata: languageName: node linkType: hard -"globals@npm:^13.19.0": - version: 13.24.0 - resolution: "globals@npm:13.24.0" - dependencies: - type-fest: "npm:^0.20.2" - checksum: 10/62c5b1997d06674fc7191d3e01e324d3eda4d65ac9cc4e78329fa3b5c4fd42a0e1c8722822497a6964eee075255ce21ccf1eec2d83f92ef3f06653af4d0ee28e - languageName: node - linkType: hard - "graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" @@ -2235,13 +1956,6 @@ __metadata: languageName: node linkType: hard -"graphemer@npm:^1.4.0": - version: 1.4.0 - resolution: "graphemer@npm:1.4.0" - checksum: 10/6dd60dba97007b21e3a829fab3f771803cc1292977fe610e240ea72afd67e5690ac9eeaafc4a99710e78962e5936ab5a460787c2a1180f1cb0ccfac37d29f897 - languageName: node - linkType: hard - "has-flag@npm:^4.0.0": version: 4.0.0 resolution: "has-flag@npm:4.0.0" @@ -2308,23 +2022,6 @@ __metadata: languageName: node linkType: hard -"ignore@npm:^5.2.0": - version: 5.3.2 - resolution: "ignore@npm:5.3.2" - checksum: 10/cceb6a457000f8f6a50e1196429750d782afce5680dd878aa4221bd79972d68b3a55b4b1458fc682be978f4d3c6a249046aa0880637367216444ab7b014cfc98 - languageName: node - linkType: hard - -"import-fresh@npm:^3.2.1": - version: 3.3.1 - resolution: "import-fresh@npm:3.3.1" - dependencies: - parent-module: "npm:^1.0.0" - resolve-from: "npm:^4.0.0" - checksum: 10/a06b19461b4879cc654d46f8a6244eb55eb053437afd4cbb6613cad6be203811849ed3e4ea038783092879487299fda24af932b86bdfff67c9055ba3612b8c87 - languageName: node - linkType: hard - "import-local@npm:^3.0.2": version: 3.2.0 resolution: "import-local@npm:3.2.0" @@ -2387,13 +2084,6 @@ __metadata: languageName: node linkType: hard -"is-extglob@npm:^2.1.1": - version: 2.1.1 - resolution: "is-extglob@npm:2.1.1" - checksum: 10/df033653d06d0eb567461e58a7a8c9f940bd8c22274b94bf7671ab36df5719791aae15eef6d83bbb5e23283967f2f984b8914559d4449efda578c775c4be6f85 - languageName: node - linkType: hard - "is-fullwidth-code-point@npm:^3.0.0": version: 3.0.0 resolution: "is-fullwidth-code-point@npm:3.0.0" @@ -2408,12 +2098,10 @@ __metadata: languageName: node linkType: hard -"is-glob@npm:^4.0.0, is-glob@npm:^4.0.3": - version: 4.0.3 - resolution: "is-glob@npm:4.0.3" - dependencies: - is-extglob: "npm:^2.1.1" - checksum: 10/3ed74f2b0cdf4f401f38edb0442ddfde3092d79d7d35c9919c86641efdbcbb32e45aa3c0f70ce5eecc946896cd5a0f26e4188b9f2b881876f7cb6c505b82da11 +"is-interactive@npm:^2.0.0": + version: 2.0.0 + resolution: "is-interactive@npm:2.0.0" + checksum: 10/e8d52ad490bed7ae665032c7675ec07732bbfe25808b0efbc4d5a76b1a1f01c165f332775c63e25e9a03d319ebb6b24f571a9e902669fc1e40b0a60b5be6e26c languageName: node linkType: hard @@ -2424,13 +2112,6 @@ __metadata: languageName: node linkType: hard -"is-path-inside@npm:^3.0.3": - version: 3.0.3 - resolution: "is-path-inside@npm:3.0.3" - checksum: 10/abd50f06186a052b349c15e55b182326f1936c89a78bf6c8f2b707412517c097ce04bc49a0ca221787bc44e1049f51f09a2ffb63d22899051988d3a618ba13e9 - languageName: node - linkType: hard - "is-stream@npm:^2.0.0": version: 2.0.1 resolution: "is-stream@npm:2.0.1" @@ -2438,6 +2119,20 @@ __metadata: languageName: node linkType: hard +"is-unicode-supported@npm:^1.3.0": + version: 1.3.0 + resolution: "is-unicode-supported@npm:1.3.0" + checksum: 10/20a1fc161afafaf49243551a5ac33b6c4cf0bbcce369fcd8f2951fbdd000c30698ce320de3ee6830497310a8f41880f8066d440aa3eb0a853e2aa4836dd89abc + languageName: node + linkType: hard + +"is-unicode-supported@npm:^2.0.0": + version: 2.1.0 + resolution: "is-unicode-supported@npm:2.1.0" + checksum: 10/f254e3da6b0ab1a57a94f7273a7798dd35d1d45b227759f600d0fa9d5649f9c07fa8d3c8a6360b0e376adf916d151ec24fc9a50c5295c58bae7ca54a76a063f9 + languageName: node + linkType: hard + "isexe@npm:^2.0.0": version: 2.0.0 resolution: "isexe@npm:2.0.0" @@ -3012,17 +2707,6 @@ __metadata: languageName: node linkType: hard -"js-yaml@npm:^4.1.0": - version: 4.1.0 - resolution: "js-yaml@npm:4.1.0" - dependencies: - argparse: "npm:^2.0.1" - bin: - js-yaml: bin/js-yaml.js - checksum: 10/c138a34a3fd0d08ebaf71273ad4465569a483b8a639e0b118ff65698d257c2791d3199e3f303631f2cb98213fa7b5f5d6a4621fd0fff819421b990d30d967140 - languageName: node - linkType: hard - "jsbn@npm:1.1.0": version: 1.1.0 resolution: "jsbn@npm:1.1.0" @@ -3039,13 +2723,6 @@ __metadata: languageName: node linkType: hard -"json-buffer@npm:3.0.1": - version: 3.0.1 - resolution: "json-buffer@npm:3.0.1" - checksum: 10/82876154521b7b68ba71c4f969b91572d1beabadd87bd3a6b236f85fbc7dc4695089191ed60bb59f9340993c51b33d479f45b6ba9f3548beb519705281c32c3c - languageName: node - linkType: hard - "json-parse-even-better-errors@npm:^2.3.0": version: 2.3.1 resolution: "json-parse-even-better-errors@npm:2.3.1" @@ -3053,20 +2730,6 @@ __metadata: languageName: node linkType: hard -"json-schema-traverse@npm:^0.4.1": - version: 0.4.1 - resolution: "json-schema-traverse@npm:0.4.1" - checksum: 10/7486074d3ba247769fda17d5181b345c9fb7d12e0da98b22d1d71a5db9698d8b4bd900a3ec1a4ffdd60846fc2556274a5c894d0c48795f14cb03aeae7b55260b - languageName: node - linkType: hard - -"json-stable-stringify-without-jsonify@npm:^1.0.1": - version: 1.0.1 - resolution: "json-stable-stringify-without-jsonify@npm:1.0.1" - checksum: 10/12786c2e2f22c27439e6db0532ba321f1d0617c27ad8cb1c352a0e9249a50182fd1ba8b52a18899291604b0c32eafa8afd09e51203f19109a0537f68db2b652d - languageName: node - linkType: hard - "json5@npm:^2.2.3": version: 2.2.3 resolution: "json5@npm:2.2.3" @@ -3076,15 +2739,6 @@ __metadata: languageName: node linkType: hard -"keyv@npm:^4.5.3": - version: 4.5.4 - resolution: "keyv@npm:4.5.4" - dependencies: - json-buffer: "npm:3.0.1" - checksum: 10/167eb6ef64cc84b6fa0780ee50c9de456b422a1e18802209234f7c2cf7eae648c7741f32e50d7e24ccb22b24c13154070b01563d642755b156c357431a191e75 - languageName: node - linkType: hard - "kleur@npm:^3.0.3": version: 3.0.3 resolution: "kleur@npm:3.0.3" @@ -3099,16 +2753,6 @@ __metadata: languageName: node linkType: hard -"levn@npm:^0.4.1": - version: 0.4.1 - resolution: "levn@npm:0.4.1" - dependencies: - prelude-ls: "npm:^1.2.1" - type-check: "npm:~0.4.0" - checksum: 10/2e4720ff79f21ae08d42374b0a5c2f664c5be8b6c8f565bb4e1315c96ed3a8acaa9de788ffed82d7f2378cf36958573de07ef92336cb5255ed74d08b8318c9ee - languageName: node - linkType: hard - "lines-and-columns@npm:^1.1.6": version: 1.2.4 resolution: "lines-and-columns@npm:1.2.4" @@ -3125,15 +2769,6 @@ __metadata: languageName: node linkType: hard -"locate-path@npm:^6.0.0": - version: 6.0.0 - resolution: "locate-path@npm:6.0.0" - dependencies: - p-locate: "npm:^5.0.0" - checksum: 10/72eb661788a0368c099a184c59d2fee760b3831c9c1c33955e8a19ae4a21b4116e53fa736dc086cdeb9fce9f7cc508f2f92d2d3aae516f133e16a2bb59a39f5a - languageName: node - linkType: hard - "lodash.memoize@npm:^4.1.2": version: 4.1.2 resolution: "lodash.memoize@npm:4.1.2" @@ -3141,10 +2776,23 @@ __metadata: languageName: node linkType: hard -"lodash.merge@npm:^4.6.2": - version: 4.6.2 - resolution: "lodash.merge@npm:4.6.2" - checksum: 10/d0ea2dd0097e6201be083865d50c3fb54fbfbdb247d9cc5950e086c991f448b7ab0cdab0d57eacccb43473d3f2acd21e134db39f22dac2d6c9ba6bf26978e3d6 +"log-symbols@npm:^6.0.0": + version: 6.0.0 + resolution: "log-symbols@npm:6.0.0" + dependencies: + chalk: "npm:^5.3.0" + is-unicode-supported: "npm:^1.3.0" + checksum: 10/510cdda36700cbcd87a2a691ea08d310a6c6b449084018f7f2ec4f732ca5e51b301ff1327aadd96f53c08318e616276c65f7fe22f2a16704fb0715d788bc3c33 + languageName: node + linkType: hard + +"log-symbols@npm:^7.0.0": + version: 7.0.0 + resolution: "log-symbols@npm:7.0.0" + dependencies: + is-unicode-supported: "npm:^2.0.0" + yoctocolors: "npm:^2.1.1" + checksum: 10/a6cb6e90bfe9f0774a09ff783e2035cd7e375a42757d7e401b391916a67f6da382f4966b57dda89430faaebe2ed13803ea867e104f8d67caf66082943a7153f0 languageName: node linkType: hard @@ -3232,7 +2880,14 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^3.0.4, minimatch@npm:^3.0.5, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": +"mimic-function@npm:^5.0.0": + version: 5.0.1 + resolution: "mimic-function@npm:5.0.1" + checksum: 10/eb5893c99e902ccebbc267c6c6b83092966af84682957f79313311edb95e8bb5f39fb048d77132b700474d1c86d90ccc211e99bae0935447a4834eb4c882982c + languageName: node + linkType: hard + +"minimatch@npm:^3.0.4, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": version: 3.1.2 resolution: "minimatch@npm:3.1.2" dependencies: @@ -3327,12 +2982,11 @@ __metadata: linkType: hard "minizlib@npm:^3.0.1": - version: 3.0.1 - resolution: "minizlib@npm:3.0.1" + version: 3.0.2 + resolution: "minizlib@npm:3.0.2" dependencies: - minipass: "npm:^7.0.4" - rimraf: "npm:^5.0.5" - checksum: 10/622cb85f51e5c206a080a62d20db0d7b4066f308cb6ce82a9644da112367c3416ae7062017e631eb7ac8588191cfa4a9a279b8651c399265202b298e98c4acef + minipass: "npm:^7.1.2" + checksum: 10/c075bed1594f68dcc8c35122333520112daefd4d070e5d0a228bd4cf5580e9eed3981b96c0ae1d62488e204e80fd27b2b9d0068ca9a5ef3993e9565faf63ca41 languageName: node linkType: hard @@ -3367,22 +3021,22 @@ __metadata: linkType: hard "node-gyp@npm:latest": - version: 11.1.0 - resolution: "node-gyp@npm:11.1.0" + version: 11.2.0 + resolution: "node-gyp@npm:11.2.0" dependencies: env-paths: "npm:^2.2.0" exponential-backoff: "npm:^3.1.1" - glob: "npm:^10.3.10" graceful-fs: "npm:^4.2.6" make-fetch-happen: "npm:^14.0.3" nopt: "npm:^8.0.0" proc-log: "npm:^5.0.0" semver: "npm:^7.3.5" tar: "npm:^7.4.3" + tinyglobby: "npm:^0.2.12" which: "npm:^5.0.0" bin: node-gyp: bin/node-gyp.js - checksum: 10/3314ebfeb99dbcdf9e8c810df1ee52294045399873d4ab1e6740608c4fbe63adaf6580c0610b23c6eda125e298536553f5bb6fb0df714016a5c721ed31095e42 + checksum: 10/806fd8e3adc9157e17bf0d4a2c899cf6b98a0bbe9f453f630094ce791866271f6cddcaf2133e6513715d934fcba2014d287c7053d5d7934937b3a34d5a3d84ad languageName: node linkType: hard @@ -3452,17 +3106,29 @@ __metadata: languageName: node linkType: hard -"optionator@npm:^0.9.3": - version: 0.9.4 - resolution: "optionator@npm:0.9.4" +"onetime@npm:^7.0.0": + version: 7.0.0 + resolution: "onetime@npm:7.0.0" dependencies: - deep-is: "npm:^0.1.3" - fast-levenshtein: "npm:^2.0.6" - levn: "npm:^0.4.1" - prelude-ls: "npm:^1.2.1" - type-check: "npm:^0.4.0" - word-wrap: "npm:^1.2.5" - checksum: 10/a8398559c60aef88d7f353a4f98dcdff6090a4e70f874c827302bf1213d9106a1c4d5fcb68dacb1feb3c30a04c4102f41047aa55d4c576b863d6fc876e001af6 + mimic-function: "npm:^5.0.0" + checksum: 10/eb08d2da9339819e2f9d52cab9caf2557d80e9af8c7d1ae86e1a0fef027d00a88e9f5bd67494d350df360f7c559fbb44e800b32f310fb989c860214eacbb561c + languageName: node + linkType: hard + +"ora@npm:^8.2.0": + version: 8.2.0 + resolution: "ora@npm:8.2.0" + dependencies: + chalk: "npm:^5.3.0" + cli-cursor: "npm:^5.0.0" + cli-spinners: "npm:^2.9.2" + is-interactive: "npm:^2.0.0" + is-unicode-supported: "npm:^2.0.0" + log-symbols: "npm:^6.0.0" + stdin-discarder: "npm:^0.2.2" + string-width: "npm:^7.2.0" + strip-ansi: "npm:^7.1.0" + checksum: 10/cea932fdcb29549cd7b5af81f427760986429cadc752b1dd4bf31bc6821f5ba137e1ef9a18cde7bdfbe5b4e3d3201e76b048765c51a27b15d18c57ac0e0a909a languageName: node linkType: hard @@ -3475,7 +3141,7 @@ __metadata: languageName: node linkType: hard -"p-limit@npm:^3.0.2, p-limit@npm:^3.1.0": +"p-limit@npm:^3.1.0": version: 3.1.0 resolution: "p-limit@npm:3.1.0" dependencies: @@ -3493,15 +3159,6 @@ __metadata: languageName: node linkType: hard -"p-locate@npm:^5.0.0": - version: 5.0.0 - resolution: "p-locate@npm:5.0.0" - dependencies: - p-limit: "npm:^3.0.2" - checksum: 10/1623088f36cf1cbca58e9b61c4e62bf0c60a07af5ae1ca99a720837356b5b6c5ba3eb1b2127e47a06865fee59dd0453cad7cc844cda9d5a62ac1a5a51b7c86d3 - languageName: node - linkType: hard - "p-map@npm:^7.0.2": version: 7.0.3 resolution: "p-map@npm:7.0.3" @@ -3523,15 +3180,6 @@ __metadata: languageName: node linkType: hard -"parent-module@npm:^1.0.0": - version: 1.0.1 - resolution: "parent-module@npm:1.0.1" - dependencies: - callsites: "npm:^3.0.0" - checksum: 10/6ba8b255145cae9470cf5551eb74be2d22281587af787a2626683a6c20fbb464978784661478dd2a3f1dad74d1e802d403e1b03c1a31fab310259eec8ac560ff - languageName: node - linkType: hard - "parse-json@npm:^5.2.0": version: 5.2.0 resolution: "parse-json@npm:5.2.0" @@ -3596,10 +3244,17 @@ __metadata: languageName: node linkType: hard +"picomatch@npm:^4.0.2": + version: 4.0.2 + resolution: "picomatch@npm:4.0.2" + checksum: 10/ce617b8da36797d09c0baacb96ca8a44460452c89362d7cb8f70ca46b4158ba8bc3606912de7c818eb4a939f7f9015cef3c766ec8a0c6bfc725fdc078e39c717 + languageName: node + linkType: hard + "pirates@npm:^4.0.4": - version: 4.0.6 - resolution: "pirates@npm:4.0.6" - checksum: 10/d02dda76f4fec1cbdf395c36c11cf26f76a644f9f9a1bfa84d3167d0d3154d5289aacc72677aa20d599bb4a6937a471de1b65c995e2aea2d8687cbcd7e43ea5f + version: 4.0.7 + resolution: "pirates@npm:4.0.7" + checksum: 10/2427f371366081ae42feb58214f04805d6b41d6b84d74480ebcc9e0ddbd7105a139f7c653daeaf83ad8a1a77214cf07f64178e76de048128fec501eab3305a96 languageName: node linkType: hard @@ -3612,13 +3267,6 @@ __metadata: languageName: node linkType: hard -"prelude-ls@npm:^1.2.1": - version: 1.2.1 - resolution: "prelude-ls@npm:1.2.1" - checksum: 10/0b9d2c76801ca652a7f64892dd37b7e3fab149a37d2424920099bf894acccc62abb4424af2155ab36dea8744843060a2d8ddc983518d0b1e22265a22324b72ed - languageName: node - linkType: hard - "pretty-format@npm:^29.0.0, pretty-format@npm:^29.7.0": version: 29.7.0 resolution: "pretty-format@npm:29.7.0" @@ -3657,13 +3305,6 @@ __metadata: languageName: node linkType: hard -"punycode@npm:^2.1.0": - version: 2.3.1 - resolution: "punycode@npm:2.3.1" - checksum: 10/febdc4362bead22f9e2608ff0171713230b57aff9dddc1c273aa2a651fbd366f94b7d6a71d78342a7c0819906750351ca7f2edd26ea41b626d87d6a13d1bd059 - languageName: node - linkType: hard - "pure-rand@npm:^6.0.0, pure-rand@npm:^6.1.0": version: 6.1.0 resolution: "pure-rand@npm:6.1.0" @@ -3671,13 +3312,6 @@ __metadata: languageName: node linkType: hard -"queue-microtask@npm:^1.2.2": - version: 1.2.3 - resolution: "queue-microtask@npm:1.2.3" - checksum: 10/72900df0616e473e824202113c3df6abae59150dfb73ed13273503127235320e9c8ca4aaaaccfd58cf417c6ca92a6e68ee9a5c3182886ae949a768639b388a7b - languageName: node - linkType: hard - "react-is@npm:^18.0.0": version: 18.3.1 resolution: "react-is@npm:18.3.1" @@ -3701,13 +3335,6 @@ __metadata: languageName: node linkType: hard -"resolve-from@npm:^4.0.0": - version: 4.0.0 - resolution: "resolve-from@npm:4.0.0" - checksum: 10/91eb76ce83621eea7bbdd9b55121a5c1c4a39e54a9ce04a9ad4517f102f8b5131c2cf07622c738a6683991bf54f2ce178f5a42803ecbd527ddc5105f362cc9e3 - languageName: node - linkType: hard - "resolve-from@npm:^5.0.0": version: 5.0.0 resolution: "resolve-from@npm:5.0.0" @@ -3748,39 +3375,20 @@ __metadata: languageName: node linkType: hard -"retry@npm:^0.12.0": - version: 0.12.0 - resolution: "retry@npm:0.12.0" - checksum: 10/1f914879f97e7ee931ad05fe3afa629bd55270fc6cf1c1e589b6a99fab96d15daad0fa1a52a00c729ec0078045fe3e399bd4fd0c93bcc906957bdc17f89cb8e6 - languageName: node - linkType: hard - -"reusify@npm:^1.0.4": - version: 1.1.0 - resolution: "reusify@npm:1.1.0" - checksum: 10/af47851b547e8a8dc89af144fceee17b80d5beaf5e6f57ed086432d79943434ff67ca526e92275be6f54b6189f6920a24eace75c2657eed32d02c400312b21ec - languageName: node - linkType: hard - -"rimraf@npm:^3.0.2": - version: 3.0.2 - resolution: "rimraf@npm:3.0.2" +"restore-cursor@npm:^5.0.0": + version: 5.1.0 + resolution: "restore-cursor@npm:5.1.0" dependencies: - glob: "npm:^7.1.3" - bin: - rimraf: bin.js - checksum: 10/063ffaccaaaca2cfd0ef3beafb12d6a03dd7ff1260d752d62a6077b5dfff6ae81bea571f655bb6b589d366930ec1bdd285d40d560c0dae9b12f125e54eb743d5 + onetime: "npm:^7.0.0" + signal-exit: "npm:^4.1.0" + checksum: 10/838dd54e458d89cfbc1a923b343c1b0f170a04100b4ce1733e97531842d7b440463967e521216e8ab6c6f8e89df877acc7b7f4c18ec76e99fb9bf5a60d358d2c languageName: node linkType: hard -"rimraf@npm:^5.0.5": - version: 5.0.10 - resolution: "rimraf@npm:5.0.10" - dependencies: - glob: "npm:^10.3.7" - bin: - rimraf: dist/esm/bin.mjs - checksum: 10/f3b8ce81eecbde4628b07bdf9e2fa8b684e0caea4999acb1e3b0402c695cd41f28cd075609a808e61ce2672f528ca079f675ab1d8e8d5f86d56643a03e0b8d2e +"retry@npm:^0.12.0": + version: 0.12.0 + resolution: "retry@npm:0.12.0" + checksum: 10/1f914879f97e7ee931ad05fe3afa629bd55270fc6cf1c1e589b6a99fab96d15daad0fa1a52a00c729ec0078045fe3e399bd4fd0c93bcc906957bdc17f89cb8e6 languageName: node linkType: hard @@ -3804,15 +3412,6 @@ __metadata: languageName: unknown linkType: soft -"run-parallel@npm:^1.1.9": - version: 1.2.0 - resolution: "run-parallel@npm:1.2.0" - dependencies: - queue-microtask: "npm:^1.2.2" - checksum: 10/cb4f97ad25a75ebc11a8ef4e33bb962f8af8516bb2001082ceabd8902e15b98f4b84b4f8a9b222e5d57fc3bd1379c483886ed4619367a7680dad65316993021d - languageName: node - linkType: hard - "safer-buffer@npm:>= 2.1.2 < 3.0.0": version: 2.1.2 resolution: "safer-buffer@npm:2.1.2" @@ -3861,7 +3460,7 @@ __metadata: languageName: node linkType: hard -"signal-exit@npm:^4.0.1": +"signal-exit@npm:^4.0.1, signal-exit@npm:^4.1.0": version: 4.1.0 resolution: "signal-exit@npm:4.1.0" checksum: 10/c9fa63bbbd7431066174a48ba2dd9986dfd930c3a8b59de9c29d7b6854ec1c12a80d15310869ea5166d413b99f041bfa3dd80a7947bcd44ea8e6eb3ffeabfa1f @@ -3959,6 +3558,13 @@ __metadata: languageName: node linkType: hard +"stdin-discarder@npm:^0.2.2": + version: 0.2.2 + resolution: "stdin-discarder@npm:0.2.2" + checksum: 10/642ffd05bd5b100819d6b24a613d83c6e3857c6de74eb02fc51506fa61dc1b0034665163831873868157c4538d71e31762bcf319be86cea04c3aba5336470478 + languageName: node + linkType: hard + "string-length@npm:^4.0.1": version: 4.0.2 resolution: "string-length@npm:4.0.2" @@ -3991,6 +3597,17 @@ __metadata: languageName: node linkType: hard +"string-width@npm:^7.2.0": + version: 7.2.0 + resolution: "string-width@npm:7.2.0" + dependencies: + emoji-regex: "npm:^10.3.0" + get-east-asian-width: "npm:^1.0.0" + strip-ansi: "npm:^7.1.0" + checksum: 10/42f9e82f61314904a81393f6ef75b832c39f39761797250de68c041d8ba4df2ef80db49ab6cd3a292923a6f0f409b8c9980d120f7d32c820b4a8a84a2598a295 + languageName: node + linkType: hard + "strip-ansi-cjs@npm:strip-ansi@^6.0.1, strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": version: 6.0.1 resolution: "strip-ansi@npm:6.0.1" @@ -4000,7 +3617,7 @@ __metadata: languageName: node linkType: hard -"strip-ansi@npm:^7.0.1": +"strip-ansi@npm:^7.0.1, strip-ansi@npm:^7.1.0": version: 7.1.0 resolution: "strip-ansi@npm:7.1.0" dependencies: @@ -4080,10 +3697,13 @@ __metadata: languageName: node linkType: hard -"text-table@npm:^0.2.0": - version: 0.2.0 - resolution: "text-table@npm:0.2.0" - checksum: 10/4383b5baaeffa9bb4cda2ac33a4aa2e6d1f8aaf811848bf73513a9b88fd76372dc461f6fd6d2e9cb5100f48b473be32c6f95bd983509b7d92bb4d92c10747452 +"tinyglobby@npm:^0.2.12": + version: 0.2.13 + resolution: "tinyglobby@npm:0.2.13" + dependencies: + fdir: "npm:^6.4.4" + picomatch: "npm:^4.0.2" + checksum: 10/b04557ee58ad2be5f2d2cbb4b441476436c92bb45ba2e1fc464d686b793392b305ed0bcb8b877429e9b5036bdd46770c161a08384c0720b6682b7cd6ac80e403 languageName: node linkType: hard @@ -4104,8 +3724,8 @@ __metadata: linkType: hard "ts-jest@npm:^29.1.1": - version: 29.2.6 - resolution: "ts-jest@npm:29.2.6" + version: 29.3.2 + resolution: "ts-jest@npm:29.3.2" dependencies: bs-logger: "npm:^0.2.6" ejs: "npm:^3.1.10" @@ -4115,6 +3735,7 @@ __metadata: lodash.memoize: "npm:^4.1.2" make-error: "npm:^1.3.6" semver: "npm:^7.7.1" + type-fest: "npm:^4.39.1" yargs-parser: "npm:^21.1.1" peerDependencies: "@babel/core": ">=7.0.0-beta.0 <8" @@ -4136,11 +3757,11 @@ __metadata: optional: true bin: ts-jest: cli.js - checksum: 10/9cb6804266be7c9384cecace346f65d2ab5a685d252c5275b53b5958f6545951328a5c4d48c49c5f92d1e04187ca31e348e5a3540d20cb365c33d1eb89371e22 + checksum: 10/62fb226a4df408174a3f28919c89440b2f5df4dec404bb49696591e61d75536b1c3be8ae726d187958a467654d82294d81d2dd70d9ec370542a30907183aaf61 languageName: node linkType: hard -"ts-node@npm:^10.9.1, ts-node@npm:^10.9.2": +"ts-node@npm:^10.9.1": version: 10.9.2 resolution: "ts-node@npm:10.9.2" dependencies: @@ -4178,58 +3799,58 @@ __metadata: languageName: node linkType: hard -"turbo-darwin-64@npm:2.5.1": - version: 2.5.1 - resolution: "turbo-darwin-64@npm:2.5.1" +"turbo-darwin-64@npm:2.5.2": + version: 2.5.2 + resolution: "turbo-darwin-64@npm:2.5.2" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"turbo-darwin-arm64@npm:2.5.1": - version: 2.5.1 - resolution: "turbo-darwin-arm64@npm:2.5.1" +"turbo-darwin-arm64@npm:2.5.2": + version: 2.5.2 + resolution: "turbo-darwin-arm64@npm:2.5.2" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"turbo-linux-64@npm:2.5.1": - version: 2.5.1 - resolution: "turbo-linux-64@npm:2.5.1" +"turbo-linux-64@npm:2.5.2": + version: 2.5.2 + resolution: "turbo-linux-64@npm:2.5.2" conditions: os=linux & cpu=x64 languageName: node linkType: hard -"turbo-linux-arm64@npm:2.5.1": - version: 2.5.1 - resolution: "turbo-linux-arm64@npm:2.5.1" +"turbo-linux-arm64@npm:2.5.2": + version: 2.5.2 + resolution: "turbo-linux-arm64@npm:2.5.2" conditions: os=linux & cpu=arm64 languageName: node linkType: hard -"turbo-windows-64@npm:2.5.1": - version: 2.5.1 - resolution: "turbo-windows-64@npm:2.5.1" +"turbo-windows-64@npm:2.5.2": + version: 2.5.2 + resolution: "turbo-windows-64@npm:2.5.2" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"turbo-windows-arm64@npm:2.5.1": - version: 2.5.1 - resolution: "turbo-windows-arm64@npm:2.5.1" +"turbo-windows-arm64@npm:2.5.2": + version: 2.5.2 + resolution: "turbo-windows-arm64@npm:2.5.2" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard "turbo@npm:^2.5.1": - version: 2.5.1 - resolution: "turbo@npm:2.5.1" - dependencies: - turbo-darwin-64: "npm:2.5.1" - turbo-darwin-arm64: "npm:2.5.1" - turbo-linux-64: "npm:2.5.1" - turbo-linux-arm64: "npm:2.5.1" - turbo-windows-64: "npm:2.5.1" - turbo-windows-arm64: "npm:2.5.1" + version: 2.5.2 + resolution: "turbo@npm:2.5.2" + dependencies: + turbo-darwin-64: "npm:2.5.2" + turbo-darwin-arm64: "npm:2.5.2" + turbo-linux-64: "npm:2.5.2" + turbo-linux-arm64: "npm:2.5.2" + turbo-windows-64: "npm:2.5.2" + turbo-windows-arm64: "npm:2.5.2" dependenciesMeta: turbo-darwin-64: optional: true @@ -4245,16 +3866,7 @@ __metadata: optional: true bin: turbo: bin/turbo - checksum: 10/54d7c16d7cc9b60098db62d37940bef4663a14344bada1cb521e3260b6c30ec4bebc88a3dbb4108e4a3436fdc32b43091323f9d841ad3198719513765a73ddd9 - languageName: node - linkType: hard - -"type-check@npm:^0.4.0, type-check@npm:~0.4.0": - version: 0.4.0 - resolution: "type-check@npm:0.4.0" - dependencies: - prelude-ls: "npm:^1.2.1" - checksum: 10/14687776479d048e3c1dbfe58a2409e00367810d6960c0f619b33793271ff2a27f81b52461f14a162f1f89a9b1d8da1b237fc7c99b0e1fdcec28ec63a86b1fec + checksum: 10/dee9047dbeeddd5584744e604620267749e278e71f9658fd58ca72f6f71d38c47132ea958aee7b7049e51c85bcfce35ca6efb1e8d180b03d7504d7427f05b026 languageName: node linkType: hard @@ -4265,13 +3877,6 @@ __metadata: languageName: node linkType: hard -"type-fest@npm:^0.20.2": - version: 0.20.2 - resolution: "type-fest@npm:0.20.2" - checksum: 10/8907e16284b2d6cfa4f4817e93520121941baba36b39219ea36acfe64c86b9dbc10c9941af450bd60832c8f43464974d51c0957f9858bc66b952b66b6914cbb9 - languageName: node - linkType: hard - "type-fest@npm:^0.21.3": version: 0.21.3 resolution: "type-fest@npm:0.21.3" @@ -4279,23 +3884,30 @@ __metadata: languageName: node linkType: hard -"typescript@npm:^5.2.2": - version: 5.8.2 - resolution: "typescript@npm:5.8.2" +"type-fest@npm:^4.39.1": + version: 4.40.1 + resolution: "type-fest@npm:4.40.1" + checksum: 10/907767cd7889c8f17d94f4a811ec27c33339a9134f6842a1a56b4d6ee87cb1d6b01332f366a3f03adc10923fd6d511d73b73076f7ab5256bf5c0b43a03ab6e8b + languageName: node + linkType: hard + +"typescript@npm:^5.2.2, typescript@npm:^5.8.2": + version: 5.8.3 + resolution: "typescript@npm:5.8.3" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 10/dbc2168a55d56771f4d581997be52bab5cbc09734fec976cfbaabd787e61fb4c6cf9125fd48c6f98054ce549c77ecedefc7f64252a830dd8e9c3381f61fbeb78 + checksum: 10/65c40944c51b513b0172c6710ee62e951b70af6f75d5a5da745cb7fab132c09ae27ffdf7838996e3ed603bb015dadd099006658046941bd0ba30340cc563ae92 languageName: node linkType: hard -"typescript@patch:typescript@npm%3A^5.2.2#optional!builtin": - version: 5.8.2 - resolution: "typescript@patch:typescript@npm%3A5.8.2#optional!builtin::version=5.8.2&hash=d69c25" +"typescript@patch:typescript@npm%3A^5.2.2#optional!builtin, typescript@patch:typescript@npm%3A^5.8.2#optional!builtin": + version: 5.8.3 + resolution: "typescript@patch:typescript@npm%3A5.8.3#optional!builtin::version=5.8.3&hash=d69c25" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 10/6ae9b2c4d3254ec2eaee6f26ed997e19c02177a212422993209f81e87092b2bb0a4738085549c5b0164982a5609364c047c72aeb281f6c8d802cd0d1c6f0d353 + checksum: 10/98470634034ec37fd9ea61cc82dcf9a27950d0117a4646146b767d085a2ec14b137aae9642a83d1c62732d7fdcdac19bb6288b0bb468a72f7a06ae4e1d2c72c9 languageName: node linkType: hard @@ -4306,10 +3918,10 @@ __metadata: languageName: node linkType: hard -"undici-types@npm:~6.20.0": - version: 6.20.0 - resolution: "undici-types@npm:6.20.0" - checksum: 10/583ac7bbf4ff69931d3985f4762cde2690bb607844c16a5e2fbb92ed312fe4fa1b365e953032d469fa28ba8b224e88a595f0b10a449332f83fa77c695e567dbe +"undici-types@npm:~6.21.0": + version: 6.21.0 + resolution: "undici-types@npm:6.21.0" + checksum: 10/ec8f41aa4359d50f9b59fa61fe3efce3477cc681908c8f84354d8567bb3701fafdddf36ef6bff307024d3feb42c837cf6f670314ba37fc8145e219560e473d14 languageName: node linkType: hard @@ -4345,15 +3957,6 @@ __metadata: languageName: node linkType: hard -"uri-js@npm:^4.2.2": - version: 4.4.1 - resolution: "uri-js@npm:4.4.1" - dependencies: - punycode: "npm:^2.1.0" - checksum: 10/b271ca7e3d46b7160222e3afa3e531505161c9a4e097febae9664e4b59912f4cbe94861361a4175edac3a03fee99d91e44b6a58c17a634bc5a664b19fc76fbcb - languageName: node - linkType: hard - "v8-compile-cache-lib@npm:^3.0.1": version: 3.0.1 resolution: "v8-compile-cache-lib@npm:3.0.1" @@ -4403,13 +4006,6 @@ __metadata: languageName: node linkType: hard -"word-wrap@npm:^1.2.5": - version: 1.2.5 - resolution: "word-wrap@npm:1.2.5" - checksum: 10/1ec6f6089f205f83037be10d0c4b34c9183b0b63fca0834a5b3cee55dd321429d73d40bb44c8fc8471b5203d6e8f8275717f49a8ff4b2b0ab41d7e1b563e0854 - languageName: node - linkType: hard - "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0, wrap-ansi@npm:^7.0.0": version: 7.0.0 resolution: "wrap-ansi@npm:7.0.0" @@ -4512,3 +4108,10 @@ __metadata: checksum: 10/f77b3d8d00310def622123df93d4ee654fc6a0096182af8bd60679ddcdfb3474c56c6c7190817c84a2785648cdee9d721c0154eb45698c62176c322fb46fc700 languageName: node linkType: hard + +"yoctocolors@npm:^2.1.1": + version: 2.1.1 + resolution: "yoctocolors@npm:2.1.1" + checksum: 10/563fbec88bce9716d1044bc98c96c329e1d7a7c503e6f1af68f1ff914adc3ba55ce953c871395e2efecad329f85f1632f51a99c362032940321ff80c42a6f74d + languageName: node + linkType: hard From dfef7ec079df130fe8d1cd9e565b700550cb8c91 Mon Sep 17 00:00:00 2001 From: andrew Date: Sun, 27 Apr 2025 01:49:14 -0500 Subject: [PATCH 020/322] update readme --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 90f64ec7..6ee7941c 100644 --- a/README.md +++ b/README.md @@ -16,12 +16,12 @@ Clone the repository: git clone git@github.com:OpenZeppelin/midnight-contracts.git ``` -`cd` into it and then install dependencies and build: +`cd` into it and then install dependencies, prepare compiler, and compile: ```bash -cd midnight-contracts -yarn -npx turbo build +yarn && \ +yarn run prepare && \ +npx turbo compact ``` ### Run tests From d4678fd196012de2999e218fa08862c5db75d470 Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 28 Apr 2025 00:40:23 -0500 Subject: [PATCH 021/322] fix fmt --- compact/package.json | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/compact/package.json b/compact/package.json index 15b7595a..84ede8f5 100644 --- a/compact/package.json +++ b/compact/package.json @@ -2,10 +2,7 @@ "packageManager": "yarn@4.1.0", "name": "@openzeppelin-midnight/compact", "version": "0.0.1", - "keywords": [ - "compact", - "compiler" - ], + "keywords": ["compact", "compiler"], "author": "OpenZeppelin Community ", "license": "MIT", "description": "Compact fetcher", From 6135d9c60a44cdd5a5329ca6279686dd87f0440f Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 29 Apr 2025 19:00:15 -0500 Subject: [PATCH 022/322] fix fmt --- compact/package.json | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/compact/package.json b/compact/package.json index 15b7595a..84ede8f5 100644 --- a/compact/package.json +++ b/compact/package.json @@ -2,10 +2,7 @@ "packageManager": "yarn@4.1.0", "name": "@openzeppelin-midnight/compact", "version": "0.0.1", - "keywords": [ - "compact", - "compiler" - ], + "keywords": ["compact", "compiler"], "author": "OpenZeppelin Community ", "license": "MIT", "description": "Compact fetcher", From 8bbfce30613f342c829a5fba060f9b94074b4b79 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 29 Apr 2025 19:10:45 -0500 Subject: [PATCH 023/322] remove lingering file --- compact/src/run-compactc.cjs | 32 -------------------------------- 1 file changed, 32 deletions(-) delete mode 100755 compact/src/run-compactc.cjs diff --git a/compact/src/run-compactc.cjs b/compact/src/run-compactc.cjs deleted file mode 100755 index dca2891f..00000000 --- a/compact/src/run-compactc.cjs +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env node - -const childProcess = require('node:child_process'); -const path = require('node:path'); - -const [_node, _script, ...args] = process.argv; -const COMPACT_HOME_ENV = process.env.COMPACT_HOME; - -let compactPath; -if (COMPACT_HOME_ENV != null) { - compactPath = COMPACT_HOME_ENV; - console.log( - `COMPACT_HOME env variable is set; using Compact from ${compactPath}`, - ); -} else { - compactPath = path.resolve(__dirname, '..', 'compactc'); - console.log( - `COMPACT_HOME env variable is not set; using fetched compact from ${compactPath}`, - ); -} - -// yarn runs everything with node... -const child = childProcess.spawn(path.resolve(compactPath, 'compactc'), args, { - stdio: 'inherit', -}); -child.on('exit', (code, signal) => { - if (code === 0) { - process.exit(0); - } else { - process.exit(code ?? signal); - } -}); From 7634d77b1004c1e965cccb22b0eaee4236a5f4b3 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 29 Apr 2025 19:23:10 -0500 Subject: [PATCH 024/322] update biome, fix fmt --- biome.json | 33 ++++++++++++++++++++++--- contracts/erc20/src/test/erc20.test.ts | 3 +-- contracts/erc20/src/test/utils/index.ts | 2 -- contracts/utils/src/test/utils/index.ts | 10 -------- 4 files changed, 31 insertions(+), 17 deletions(-) delete mode 100644 contracts/erc20/src/test/utils/index.ts delete mode 100644 contracts/utils/src/test/utils/index.ts diff --git a/biome.json b/biome.json index 656beeee..65873551 100644 --- a/biome.json +++ b/biome.json @@ -24,15 +24,42 @@ "organizeImports": { "enabled": true }, - "linter": { +"linter": { "enabled": true, "rules": { - "recommended": true + "recommended": true, + "a11y": { + "useButtonType": "off" + }, + "correctness": { + "noUnusedVariables": "error", + "useExhaustiveDependencies": "error", + "noUnusedImports": "error" + }, + "performance": { + "noBarrelFile": "error", + "noReExportAll": "error", + "noDelete": "off" + }, + "style": { + "noNonNullAssertion": "off", + "useShorthandArrayType": "error" + }, + "suspicious": { + "noArrayIndexKey": "off", + "noConfusingVoidType": "off", + "noConsoleLog": "error", + "noExplicitAny": "off" + } } }, "javascript": { "formatter": { - "quoteStyle": "single" + "quoteStyle": "single", + "trailingCommas": "all", + "semicolons": "always", + "indentStyle": "space", + "indentWidth": 2 } } } diff --git a/contracts/erc20/src/test/erc20.test.ts b/contracts/erc20/src/test/erc20.test.ts index 8e9a4596..414b3ea2 100644 --- a/contracts/erc20/src/test/erc20.test.ts +++ b/contracts/erc20/src/test/erc20.test.ts @@ -1,7 +1,7 @@ import type { CoinPublicKey } from '@midnight-ntwrk/compact-runtime'; import { ERC20Simulator } from './simulators/ERC20Simulator'; import type { MaybeString } from './types/string'; -import * as utils from './utils'; +import * as utils from './utils/address'; const NO_STRING: MaybeString = { is_some: false, @@ -35,7 +35,6 @@ const Z_OWNER = utils.createEitherTestUser('OWNER'); const Z_RECIPIENT = utils.createEitherTestUser('RECIPIENT'); const Z_SPENDER = utils.createEitherTestUser('SPENDER'); const Z_OTHER = utils.createEitherTestUser('OTHER'); -const SOME_CONTRACT = utils.createEitherTestContractAddress('SOME_CONTRACT'); let token: ERC20Simulator; let caller: CoinPublicKey; diff --git a/contracts/erc20/src/test/utils/index.ts b/contracts/erc20/src/test/utils/index.ts deleted file mode 100644 index 731fe1ec..00000000 --- a/contracts/erc20/src/test/utils/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { useCircuitContext as circuitContext } from './test'; -export * from './address'; diff --git a/contracts/utils/src/test/utils/index.ts b/contracts/utils/src/test/utils/index.ts deleted file mode 100644 index b8e9585d..00000000 --- a/contracts/utils/src/test/utils/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -export { useCircuitContext as circuitContext } from './test'; -export { - pad, - encodeToPK, - encodeToAddress, - createEitherTestUser, - createEitherTestContractAddress, - ZERO_KEY, - ZERO_ADDRESS, -} from './address'; From 18ab0f323911d008f31b16e41ff8b77fbcf391a0 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 29 Apr 2025 22:59:52 -0500 Subject: [PATCH 025/322] add requirements in dev section --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 6ee7941c..0fd030ef 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,12 @@ ## Development +> ### Requirements +> +> - [node](https://nodejs.org/) +> - [yarn](https://yarnpkg.com/getting-started/install) +> - [compact](https://docs.midnight.network/develop/tutorial/building/#midnight-compact-compiler) + Clone the repository: ```bash From bedff4fa76ac3295816f96d9fd07cba3595ef9a3 Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 30 Apr 2025 02:39:04 -0500 Subject: [PATCH 026/322] add devdeps to contracts packages --- compact/package.json | 5 ++++- contracts/erc20/package.json | 7 +++++++ contracts/utils/package.json | 7 +++++++ yarn.lock | 10 ++++++++++ 4 files changed, 28 insertions(+), 1 deletion(-) diff --git a/compact/package.json b/compact/package.json index 84ede8f5..15b7595a 100644 --- a/compact/package.json +++ b/compact/package.json @@ -2,7 +2,10 @@ "packageManager": "yarn@4.1.0", "name": "@openzeppelin-midnight/compact", "version": "0.0.1", - "keywords": ["compact", "compiler"], + "keywords": [ + "compact", + "compiler" + ], "author": "OpenZeppelin Community ", "license": "MIT", "description": "Compact fetcher", diff --git a/contracts/erc20/package.json b/contracts/erc20/package.json index 922efaa5..bb471653 100644 --- a/contracts/erc20/package.json +++ b/contracts/erc20/package.json @@ -25,5 +25,12 @@ }, "dependencies": { "@openzeppelin-midnight/compact": "workspace:^" + }, + "devDependencies": { + "@biomejs/biome": "1.9.4", + "@types/jest": "^29.5.6", + "@types/node": "^18.18.6", + "jest": "^29.7.0", + "typescript": "^5.2.2" } } diff --git a/contracts/utils/package.json b/contracts/utils/package.json index 8389ab1d..5205b373 100644 --- a/contracts/utils/package.json +++ b/contracts/utils/package.json @@ -25,5 +25,12 @@ }, "dependencies": { "@openzeppelin-midnight/compact": "workspace:^" + }, + "devDependencies": { + "@biomejs/biome": "1.9.4", + "@types/jest": "^29.5.6", + "@types/node": "^18.18.6", + "jest": "^29.7.0", + "typescript": "^5.2.2" } } diff --git a/yarn.lock b/yarn.lock index 866086ec..0bb1b54d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -887,7 +887,12 @@ __metadata: version: 0.0.0-use.local resolution: "@openzeppelin-midnight/erc20@workspace:contracts/erc20" dependencies: + "@biomejs/biome": "npm:1.9.4" "@openzeppelin-midnight/compact": "workspace:^" + "@types/jest": "npm:^29.5.6" + "@types/node": "npm:^18.18.6" + jest: "npm:^29.7.0" + typescript: "npm:^5.2.2" languageName: unknown linkType: soft @@ -895,7 +900,12 @@ __metadata: version: 0.0.0-use.local resolution: "@openzeppelin-midnight/utils@workspace:contracts/utils" dependencies: + "@biomejs/biome": "npm:1.9.4" "@openzeppelin-midnight/compact": "workspace:^" + "@types/jest": "npm:^29.5.6" + "@types/node": "npm:^18.18.6" + jest: "npm:^29.7.0" + typescript: "npm:^5.2.2" languageName: unknown linkType: soft From dc7ae8f1607e4287f6185b6bf86459d063b7924b Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 30 Apr 2025 22:42:31 -0500 Subject: [PATCH 027/322] simplify workspace --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index 0a6f059f..eef2ac9b 100644 --- a/package.json +++ b/package.json @@ -2,8 +2,7 @@ "packageManager": "yarn@4.1.0", "workspaces": [ "compact/", - "contracts/erc20/", - "contracts/utils/" + "contracts/*/" ], "scripts": { "prepare": "npx tsc -p ./compact && yarn rebuild", From ff967e5d970bc476ad97b11d6cad0b0d3bc29ce7 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 1 May 2025 13:53:16 -0500 Subject: [PATCH 028/322] remove unnecessary button rule --- biome.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/biome.json b/biome.json index 65873551..74a65acc 100644 --- a/biome.json +++ b/biome.json @@ -28,9 +28,6 @@ "enabled": true, "rules": { "recommended": true, - "a11y": { - "useButtonType": "off" - }, "correctness": { "noUnusedVariables": "error", "useExhaustiveDependencies": "error", From 8ce9ac0e1698a170044d023ba2eefc064e647704 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 1 May 2025 15:16:37 -0500 Subject: [PATCH 029/322] fix fmt --- compact/package.json | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/compact/package.json b/compact/package.json index 15b7595a..84ede8f5 100644 --- a/compact/package.json +++ b/compact/package.json @@ -2,10 +2,7 @@ "packageManager": "yarn@4.1.0", "name": "@openzeppelin-midnight/compact", "version": "0.0.1", - "keywords": [ - "compact", - "compiler" - ], + "keywords": ["compact", "compiler"], "author": "OpenZeppelin Community ", "license": "MIT", "description": "Compact fetcher", From f869fac5a89d95cbd25eace7c1bcb7be8643d63a Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 1 May 2025 15:17:02 -0500 Subject: [PATCH 030/322] remove useExhaustiveDeps rule --- biome.json | 1 - 1 file changed, 1 deletion(-) diff --git a/biome.json b/biome.json index 74a65acc..c41cd318 100644 --- a/biome.json +++ b/biome.json @@ -30,7 +30,6 @@ "recommended": true, "correctness": { "noUnusedVariables": "error", - "useExhaustiveDependencies": "error", "noUnusedImports": "error" }, "performance": { From d0995ce891f38c68a250dd6f5d58e5e938a57eee Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 2 May 2025 20:08:10 -0500 Subject: [PATCH 031/322] add contract template --- contracts/myContract/jest.config.ts | 16 +++ contracts/myContract/js-resolver.cjs | 20 ++++ contracts/myContract/package.json | 36 ++++++ contracts/myContract/src/MyContract.compact | 41 +++++++ .../myContract/src/test/MyContract.test.ts | 19 ++++ .../src/test/mocks/MockMyContract.compact | 21 ++++ .../test/simulators/MyContractSimulator.ts | 104 ++++++++++++++++++ contracts/myContract/src/test/types/string.ts | 4 + contracts/myContract/src/test/types/test.ts | 26 +++++ .../myContract/src/test/utils/address.ts | 81 ++++++++++++++ contracts/myContract/src/test/utils/test.ts | 68 ++++++++++++ .../src/witnesses/MyContractWitnesses.ts | 3 + contracts/myContract/tsconfig.build.json | 5 + contracts/myContract/tsconfig.json | 21 ++++ yarn.lock | 13 +++ 15 files changed, 478 insertions(+) create mode 100644 contracts/myContract/jest.config.ts create mode 100644 contracts/myContract/js-resolver.cjs create mode 100644 contracts/myContract/package.json create mode 100644 contracts/myContract/src/MyContract.compact create mode 100644 contracts/myContract/src/test/MyContract.test.ts create mode 100644 contracts/myContract/src/test/mocks/MockMyContract.compact create mode 100644 contracts/myContract/src/test/simulators/MyContractSimulator.ts create mode 100644 contracts/myContract/src/test/types/string.ts create mode 100644 contracts/myContract/src/test/types/test.ts create mode 100644 contracts/myContract/src/test/utils/address.ts create mode 100644 contracts/myContract/src/test/utils/test.ts create mode 100644 contracts/myContract/src/witnesses/MyContractWitnesses.ts create mode 100644 contracts/myContract/tsconfig.build.json create mode 100644 contracts/myContract/tsconfig.json diff --git a/contracts/myContract/jest.config.ts b/contracts/myContract/jest.config.ts new file mode 100644 index 00000000..bde5bde1 --- /dev/null +++ b/contracts/myContract/jest.config.ts @@ -0,0 +1,16 @@ +import type { Config } from '@jest/types'; + +const config: Config.InitialOptions = { + preset: 'ts-jest/presets/default-esm', + testEnvironment: 'node', + verbose: true, + roots: [''], + modulePaths: [''], + passWithNoTests: false, + testMatch: ['**/*.test.ts'], + extensionsToTreatAsEsm: ['.ts'], + collectCoverage: false, + resolver: '/js-resolver.cjs', +}; + +export default config; diff --git a/contracts/myContract/js-resolver.cjs b/contracts/myContract/js-resolver.cjs new file mode 100644 index 00000000..19b6f50c --- /dev/null +++ b/contracts/myContract/js-resolver.cjs @@ -0,0 +1,20 @@ +const jsResolver = (path, options) => { + const jsExtRegex = /\.js$/i; + const resolver = options.defaultResolver; + if ( + jsExtRegex.test(path) && + !options.basedir.includes('node_modules') && + !path.includes('node_modules') + ) { + const newPath = path.replace(jsExtRegex, '.ts'); + try { + return resolver(newPath, options); + } catch { + // use default resolver + } + } + + return resolver(path, options); +}; + +module.exports = jsResolver; diff --git a/contracts/myContract/package.json b/contracts/myContract/package.json new file mode 100644 index 00000000..7e991454 --- /dev/null +++ b/contracts/myContract/package.json @@ -0,0 +1,36 @@ +{ + "name": "@openzeppelin-midnight/myContract", + "type": "module", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "require": "./dist/index.js", + "import": "./dist/index.js", + "default": "./dist/index.js" + } + }, + "scripts": { + "compact": "npx compact-compiler", + "build": "npx compact-builder && tsc", + "test": "NODE_OPTIONS=--experimental-vm-modules jest", + "types": "tsc -p tsconfig.json --noEmit", + "fmt": "biome format", + "fmt:fix": "biome format --write", + "lint": "biome lint", + "lint:fix": "biome check --write", + "clean": "git clean -fXd" + }, + "dependencies": { + "@openzeppelin-midnight/compact": "workspace:^" + }, + "devDependencies": { + "@biomejs/biome": "1.9.4", + "@types/jest": "^29.5.6", + "@types/node": "^18.18.6", + "jest": "^29.7.0", + "typescript": "^5.2.2" + } +} diff --git a/contracts/myContract/src/MyContract.compact b/contracts/myContract/src/MyContract.compact new file mode 100644 index 00000000..25ddf50f --- /dev/null +++ b/contracts/myContract/src/MyContract.compact @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT + +pragma language_version >= 0.14.0; + +/** + * @module MyContract + * @description Get rekt, losers + */ +module MyContract { + import CompactStandardLibrary; + + /// Public state + export ledger name: Maybe>; + + /** + * @description Initializes MyContract's name. + */ + export circuit initializer( + _name: Maybe> + ): [] { + return setName(_name); + } + + /** + * @description Returns the contract name. + * + * @return {Maybe>} - The token name. + */ + export circuit getName(): Maybe> { + return name; + } + + /** + * @description Sets the contract name. + * + * @return {[]} - None. + */ + export circuit setName(newName: Maybe>): [] { + name = newName; + } +} diff --git a/contracts/myContract/src/test/MyContract.test.ts b/contracts/myContract/src/test/MyContract.test.ts new file mode 100644 index 00000000..575d867d --- /dev/null +++ b/contracts/myContract/src/test/MyContract.test.ts @@ -0,0 +1,19 @@ +import { MyContractSimulator } from './simulators/MyContractSimulator'; +import type { MaybeString } from './types/string'; + +const NAME: MaybeString = { + is_some: true, + value: 'NAME', +}; + +let contract: MyContractSimulator; + +describe('MyContract', () => { + describe('name', () => { + it('should return name', () => { + contract = new MyContractSimulator(NAME); + + expect(contract.getName()).toEqual(NAME); + }); + }); +}); diff --git a/contracts/myContract/src/test/mocks/MockMyContract.compact b/contracts/myContract/src/test/mocks/MockMyContract.compact new file mode 100644 index 00000000..8caabf35 --- /dev/null +++ b/contracts/myContract/src/test/mocks/MockMyContract.compact @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT + +pragma language_version >= 0.14.0; + +import CompactStandardLibrary; + +import "../../MyContract" prefix MyContract_; + +export { ZswapCoinPublicKey, ContractAddress, Either, Maybe }; + +constructor(name: Maybe>) { + MyContract_initializer(name); +} + +export circuit getName(): Maybe> { + return MyContract_getName(); +} + +export circuit setName(newName: Maybe>): [] { + return MyContract_setName(newName); +} diff --git a/contracts/myContract/src/test/simulators/MyContractSimulator.ts b/contracts/myContract/src/test/simulators/MyContractSimulator.ts new file mode 100644 index 00000000..329906cd --- /dev/null +++ b/contracts/myContract/src/test/simulators/MyContractSimulator.ts @@ -0,0 +1,104 @@ +import { + type CircuitContext, + type ContractState, + QueryContext, + constructorContext, +} from '@midnight-ntwrk/compact-runtime'; +import { sampleContractAddress } from '@midnight-ntwrk/zswap'; +import { + type Ledger, + Contract as MockMyContract, + ledger, +} from '../../artifacts/MockMyContract/contract/index.cjs'; // Combined imports +import { + type MyContractPrivateState, + MyContractWitnesses, +} from '../../witnesses/MyContractWitnesses'; +import type { MaybeString } from '../types/string'; +import type { IContractSimulator } from '../types/test'; + +/** + * @description A simulator implementation of a contract for testing purposes. + * @template P - The private state type, fixed to MyContractPrivateState. + * @template L - The ledger type, fixed to Contract.Ledger. + */ +export class MyContractSimulator + implements IContractSimulator +{ + /** @description The underlying contract instance managing contract logic. */ + readonly contract: MockMyContract; + + /** @description The deployed address of the contract. */ + readonly contractAddress: string; + + /** @description The current circuit context, updated by contract operations. */ + circuitContext: CircuitContext; + + /** + * @description Initializes the mock contract. + */ + constructor(name: MaybeString) { + this.contract = new MockMyContract( + MyContractWitnesses, + ); + const { + currentPrivateState, + currentContractState, + currentZswapLocalState, + } = this.contract.initialState( + constructorContext({}, '0'.repeat(64)), + name, + ); + this.circuitContext = { + currentPrivateState, + currentZswapLocalState, + originalState: currentContractState, + transactionContext: new QueryContext( + currentContractState.data, + sampleContractAddress(), + ), + }; + this.contractAddress = this.circuitContext.transactionContext.address; + } + + /** + * @description Retrieves the current public ledger state of the contract. + * @returns The ledger state as defined by the contract. + */ + public getCurrentPublicState(): Ledger { + return ledger(this.circuitContext.transactionContext.state); + } + + /** + * @description Retrieves the current private state of the contract. + * @returns The private state of type MyContractPrivateState. + */ + public getCurrentPrivateState(): MyContractPrivateState { + return this.circuitContext.currentPrivateState; + } + + /** + * @description Retrieves the current contract state. + * @returns The contract state object. + */ + public getCurrentContractState(): ContractState { + return this.circuitContext.originalState; + } + + /** + * @description Returns the contract name. + * @returns The contract name. + */ + public getName(): MaybeString { + return this.contract.impureCircuits.getName(this.circuitContext).result; + } + + /** + * @description Sets the contract name. + * @returns None. + */ + public setName(newName: MaybeString) { + return this.contract.impureCircuits.setName(this.circuitContext, newName) + .result; + } +} diff --git a/contracts/myContract/src/test/types/string.ts b/contracts/myContract/src/test/types/string.ts new file mode 100644 index 00000000..430a139e --- /dev/null +++ b/contracts/myContract/src/test/types/string.ts @@ -0,0 +1,4 @@ +export type MaybeString = { + is_some: boolean; + value: string; +}; diff --git a/contracts/myContract/src/test/types/test.ts b/contracts/myContract/src/test/types/test.ts new file mode 100644 index 00000000..7a909543 --- /dev/null +++ b/contracts/myContract/src/test/types/test.ts @@ -0,0 +1,26 @@ +import type { + CircuitContext, + ContractState, +} from '@midnight-ntwrk/compact-runtime'; + +/** + * Generic interface for mock contract implementations. + * @template P - The type of the contract's private state. + * @template L - The type of the contract's ledger (public state). + */ +export interface IContractSimulator { + /** The contract's deployed address. */ + readonly contractAddress: string; + + /** The current circuit context. */ + circuitContext: CircuitContext

; + + /** Retrieves the current ledger state. */ + getCurrentPublicState(): L; + + /** Retrieves the current private state. */ + getCurrentPrivateState(): P; + + /** Retrieves the current contract state. */ + getCurrentContractState(): ContractState; +} diff --git a/contracts/myContract/src/test/utils/address.ts b/contracts/myContract/src/test/utils/address.ts new file mode 100644 index 00000000..3580e196 --- /dev/null +++ b/contracts/myContract/src/test/utils/address.ts @@ -0,0 +1,81 @@ +import { + convert_bigint_to_Uint8Array, + encodeCoinPublicKey, +} from '@midnight-ntwrk/compact-runtime'; +import { encodeContractAddress } from '@midnight-ntwrk/ledger'; +import type * as Compact from '../../artifacts/MockERC20/contract/index.cjs'; + +const PREFIX_ADDRESS = '0200'; + +export const pad = (s: string, n: number): Uint8Array => { + const encoder = new TextEncoder(); + const utf8Bytes = encoder.encode(s); + if (n < utf8Bytes.length) { + throw new Error(`The padded length n must be at least ${utf8Bytes.length}`); + } + const paddedArray = new Uint8Array(n); + paddedArray.set(utf8Bytes); + return paddedArray; +}; + +/** + * @description Generates ZswapCoinPublicKey from `str` for testing purposes. + * @param str String to hexify and encode. + * @returns Encoded `ZswapCoinPublicKey`. + */ +export const encodeToPK = (str: string): Compact.ZswapCoinPublicKey => { + const toHex = Buffer.from(str, 'ascii').toString('hex'); + return { bytes: encodeCoinPublicKey(String(toHex).padStart(64, '0')) }; +}; + +/** + * @description Generates ContractAddress from `str` for testing purposes. + * Prepends 32-byte hex with PREFIX_ADDRESS before encoding. + * @param str String to hexify and encode. + * @returns Encoded `ZswapCoinPublicKey`. + */ +export const encodeToAddress = (str: string): Compact.ContractAddress => { + const toHex = Buffer.from(str, 'ascii').toString('hex'); + const fullAddress = PREFIX_ADDRESS + String(toHex).padStart(64, '0'); + return { bytes: encodeContractAddress(fullAddress) }; +}; + +/** + * @description Generates an Either object for ZswapCoinPublicKey for testing. + * For use when an Either argument is expected. + * @param str String to hexify and encode. + * @returns Defined Either object for ZswapCoinPublicKey. + */ +export const createEitherTestUser = (str: string) => { + return { + is_left: true, + left: encodeToPK(str), + right: encodeToAddress(''), + }; +}; + +/** + * @description Generates an Either object for ContractAddress for testing. + * For use when an Either argument is expected. + * @param str String to hexify and encode. + * @returns Defined Either object for ContractAddress. + */ +export const createEitherTestContractAddress = (str: string) => { + return { + is_left: false, + left: encodeToPK(''), + right: encodeToAddress(str), + }; +}; + +export const ZERO_KEY = { + is_left: true, + left: { bytes: convert_bigint_to_Uint8Array(32, BigInt(0)) }, + right: encodeToAddress(''), +}; + +export const ZERO_ADDRESS = { + is_left: false, + left: encodeToPK(''), + right: { bytes: convert_bigint_to_Uint8Array(32, BigInt(0)) }, +}; diff --git a/contracts/myContract/src/test/utils/test.ts b/contracts/myContract/src/test/utils/test.ts new file mode 100644 index 00000000..d467e572 --- /dev/null +++ b/contracts/myContract/src/test/utils/test.ts @@ -0,0 +1,68 @@ +import { + type CircuitContext, + type CoinPublicKey, + type ContractAddress, + type ContractState, + QueryContext, + emptyZswapLocalState, +} from '@midnight-ntwrk/compact-runtime'; +import type { IContractSimulator } from '../types/test'; + +/** + * Constructs a `CircuitContext` from the given state and sender information. + * + * This is typically used at runtime to provide the necessary context + * for executing circuits, including contract state, private state, + * sender identity, and transaction data. + * + * @template P - The type of the contract's private state. + * @param privateState - The current private state of the contract. + * @param contractState - The full contract state, including public and private data. + * @param sender - The public key of the sender (used in the circuit). + * @param contractAddress - The address of the deployed contract. + * @returns A fully populated `CircuitContext` for circuit execution. + * @todo TODO: Move this utility to a generic package for broader reuse across contracts. + */ +export function useCircuitContext

( + privateState: P, + contractState: ContractState, + sender: CoinPublicKey, + contractAddress: ContractAddress, +): CircuitContext

{ + return { + originalState: contractState, + currentPrivateState: privateState, + transactionContext: new QueryContext(contractState.data, contractAddress), + currentZswapLocalState: emptyZswapLocalState(sender), + }; +} + +/** + * Prepares a new `CircuitContext` using the given sender and contract. + * + * Useful for mocking or updating the circuit context with a custom sender. + * + * @template P - The type of the contract's private state. + * @template L - The type of the contract's ledger (public state). + * @template C - The specific type of the contract implementing `MockContract`. + * @param contract - The contract instance implementing `MockContract`. + * @param sender - The public key to set as the sender in the new circuit context. + * @returns A new `CircuitContext` with the sender and updated context values. + * @todo TODO: Move this utility to a generic package for broader reuse across contracts. + */ +export function useCircuitContextSender< + P, + L, + C extends IContractSimulator, +>(contract: C, sender: CoinPublicKey): CircuitContext

{ + const currentPrivateState = contract.getCurrentPrivateState(); + const originalState = contract.getCurrentContractState(); + const contractAddress = contract.contractAddress; + + return { + originalState, + currentPrivateState, + transactionContext: new QueryContext(originalState.data, contractAddress), + currentZswapLocalState: emptyZswapLocalState(sender), + }; +} diff --git a/contracts/myContract/src/witnesses/MyContractWitnesses.ts b/contracts/myContract/src/witnesses/MyContractWitnesses.ts new file mode 100644 index 00000000..9547c404 --- /dev/null +++ b/contracts/myContract/src/witnesses/MyContractWitnesses.ts @@ -0,0 +1,3 @@ +// This is how we type an empty object. +export type MyContractPrivateState = Record; +export const MyContractWitnesses = {}; diff --git a/contracts/myContract/tsconfig.build.json b/contracts/myContract/tsconfig.build.json new file mode 100644 index 00000000..f1132509 --- /dev/null +++ b/contracts/myContract/tsconfig.build.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["src/test/**/*.ts"], + "compilerOptions": {} +} diff --git a/contracts/myContract/tsconfig.json b/contracts/myContract/tsconfig.json new file mode 100644 index 00000000..3e90b0a9 --- /dev/null +++ b/contracts/myContract/tsconfig.json @@ -0,0 +1,21 @@ +{ + "include": ["src/**/*.ts"], + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "declaration": true, + "lib": ["ESNext"], + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "node", + "allowJs": true, + "forceConsistentCasingInFileNames": true, + "noImplicitAny": true, + "strict": true, + "isolatedModules": true, + "sourceMap": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "skipLibCheck": true + } +} diff --git a/yarn.lock b/yarn.lock index 0bb1b54d..25a6a3ae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -896,6 +896,19 @@ __metadata: languageName: unknown linkType: soft +"@openzeppelin-midnight/myContract@workspace:contracts/myContract": + version: 0.0.0-use.local + resolution: "@openzeppelin-midnight/myContract@workspace:contracts/myContract" + dependencies: + "@biomejs/biome": "npm:1.9.4" + "@openzeppelin-midnight/compact": "workspace:^" + "@types/jest": "npm:^29.5.6" + "@types/node": "npm:^18.18.6" + jest: "npm:^29.7.0" + typescript: "npm:^5.2.2" + languageName: unknown + linkType: soft + "@openzeppelin-midnight/utils@workspace:contracts/utils": version: 0.0.0-use.local resolution: "@openzeppelin-midnight/utils@workspace:contracts/utils" From efef77a6609e5b579e7c3d1d976fa932a0bccfea Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 6 May 2025 19:10:24 -0500 Subject: [PATCH 032/322] start ownable shielded --- compact/package.json | 5 +- contracts/myContract/src/MyContract.compact | 41 ----- .../myContract/src/test/MyContract.test.ts | 19 -- .../src/test/mocks/MockMyContract.compact | 21 --- .../test/simulators/MyContractSimulator.ts | 104 ----------- .../src/witnesses/MyContractWitnesses.ts | 3 - .../{myContract => ownable}/jest.config.ts | 0 .../{myContract => ownable}/js-resolver.cjs | 0 .../{myContract => ownable}/package.json | 2 +- contracts/ownable/src/Ownable.compact | 77 +++++++++ contracts/ownable/src/test/Ownable.test.ts | 33 ++++ .../src/test/mocks/MockOwnable.compact | 34 ++++ .../src/test/simulators/OwnableSimulator.ts | 162 ++++++++++++++++++ .../src/test/types/string.ts | 0 .../src/test/types/test.ts | 0 .../src/test/utils/address.ts | 0 .../src/test/utils/test.ts | 0 .../ownable/src/witnesses/OwnableWitnesses.ts | 45 +++++ contracts/ownable/src/witnesses/interface.ts | 17 ++ .../tsconfig.build.json | 0 .../{myContract => ownable}/tsconfig.json | 0 yarn.lock | 4 +- 22 files changed, 375 insertions(+), 192 deletions(-) delete mode 100644 contracts/myContract/src/MyContract.compact delete mode 100644 contracts/myContract/src/test/MyContract.test.ts delete mode 100644 contracts/myContract/src/test/mocks/MockMyContract.compact delete mode 100644 contracts/myContract/src/test/simulators/MyContractSimulator.ts delete mode 100644 contracts/myContract/src/witnesses/MyContractWitnesses.ts rename contracts/{myContract => ownable}/jest.config.ts (100%) rename contracts/{myContract => ownable}/js-resolver.cjs (100%) rename contracts/{myContract => ownable}/package.json (95%) create mode 100644 contracts/ownable/src/Ownable.compact create mode 100644 contracts/ownable/src/test/Ownable.test.ts create mode 100644 contracts/ownable/src/test/mocks/MockOwnable.compact create mode 100644 contracts/ownable/src/test/simulators/OwnableSimulator.ts rename contracts/{myContract => ownable}/src/test/types/string.ts (100%) rename contracts/{myContract => ownable}/src/test/types/test.ts (100%) rename contracts/{myContract => ownable}/src/test/utils/address.ts (100%) rename contracts/{myContract => ownable}/src/test/utils/test.ts (100%) create mode 100644 contracts/ownable/src/witnesses/OwnableWitnesses.ts create mode 100644 contracts/ownable/src/witnesses/interface.ts rename contracts/{myContract => ownable}/tsconfig.build.json (100%) rename contracts/{myContract => ownable}/tsconfig.json (100%) diff --git a/compact/package.json b/compact/package.json index 84ede8f5..15b7595a 100644 --- a/compact/package.json +++ b/compact/package.json @@ -2,7 +2,10 @@ "packageManager": "yarn@4.1.0", "name": "@openzeppelin-midnight/compact", "version": "0.0.1", - "keywords": ["compact", "compiler"], + "keywords": [ + "compact", + "compiler" + ], "author": "OpenZeppelin Community ", "license": "MIT", "description": "Compact fetcher", diff --git a/contracts/myContract/src/MyContract.compact b/contracts/myContract/src/MyContract.compact deleted file mode 100644 index 25ddf50f..00000000 --- a/contracts/myContract/src/MyContract.compact +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma language_version >= 0.14.0; - -/** - * @module MyContract - * @description Get rekt, losers - */ -module MyContract { - import CompactStandardLibrary; - - /// Public state - export ledger name: Maybe>; - - /** - * @description Initializes MyContract's name. - */ - export circuit initializer( - _name: Maybe> - ): [] { - return setName(_name); - } - - /** - * @description Returns the contract name. - * - * @return {Maybe>} - The token name. - */ - export circuit getName(): Maybe> { - return name; - } - - /** - * @description Sets the contract name. - * - * @return {[]} - None. - */ - export circuit setName(newName: Maybe>): [] { - name = newName; - } -} diff --git a/contracts/myContract/src/test/MyContract.test.ts b/contracts/myContract/src/test/MyContract.test.ts deleted file mode 100644 index 575d867d..00000000 --- a/contracts/myContract/src/test/MyContract.test.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { MyContractSimulator } from './simulators/MyContractSimulator'; -import type { MaybeString } from './types/string'; - -const NAME: MaybeString = { - is_some: true, - value: 'NAME', -}; - -let contract: MyContractSimulator; - -describe('MyContract', () => { - describe('name', () => { - it('should return name', () => { - contract = new MyContractSimulator(NAME); - - expect(contract.getName()).toEqual(NAME); - }); - }); -}); diff --git a/contracts/myContract/src/test/mocks/MockMyContract.compact b/contracts/myContract/src/test/mocks/MockMyContract.compact deleted file mode 100644 index 8caabf35..00000000 --- a/contracts/myContract/src/test/mocks/MockMyContract.compact +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma language_version >= 0.14.0; - -import CompactStandardLibrary; - -import "../../MyContract" prefix MyContract_; - -export { ZswapCoinPublicKey, ContractAddress, Either, Maybe }; - -constructor(name: Maybe>) { - MyContract_initializer(name); -} - -export circuit getName(): Maybe> { - return MyContract_getName(); -} - -export circuit setName(newName: Maybe>): [] { - return MyContract_setName(newName); -} diff --git a/contracts/myContract/src/test/simulators/MyContractSimulator.ts b/contracts/myContract/src/test/simulators/MyContractSimulator.ts deleted file mode 100644 index 329906cd..00000000 --- a/contracts/myContract/src/test/simulators/MyContractSimulator.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { - type CircuitContext, - type ContractState, - QueryContext, - constructorContext, -} from '@midnight-ntwrk/compact-runtime'; -import { sampleContractAddress } from '@midnight-ntwrk/zswap'; -import { - type Ledger, - Contract as MockMyContract, - ledger, -} from '../../artifacts/MockMyContract/contract/index.cjs'; // Combined imports -import { - type MyContractPrivateState, - MyContractWitnesses, -} from '../../witnesses/MyContractWitnesses'; -import type { MaybeString } from '../types/string'; -import type { IContractSimulator } from '../types/test'; - -/** - * @description A simulator implementation of a contract for testing purposes. - * @template P - The private state type, fixed to MyContractPrivateState. - * @template L - The ledger type, fixed to Contract.Ledger. - */ -export class MyContractSimulator - implements IContractSimulator -{ - /** @description The underlying contract instance managing contract logic. */ - readonly contract: MockMyContract; - - /** @description The deployed address of the contract. */ - readonly contractAddress: string; - - /** @description The current circuit context, updated by contract operations. */ - circuitContext: CircuitContext; - - /** - * @description Initializes the mock contract. - */ - constructor(name: MaybeString) { - this.contract = new MockMyContract( - MyContractWitnesses, - ); - const { - currentPrivateState, - currentContractState, - currentZswapLocalState, - } = this.contract.initialState( - constructorContext({}, '0'.repeat(64)), - name, - ); - this.circuitContext = { - currentPrivateState, - currentZswapLocalState, - originalState: currentContractState, - transactionContext: new QueryContext( - currentContractState.data, - sampleContractAddress(), - ), - }; - this.contractAddress = this.circuitContext.transactionContext.address; - } - - /** - * @description Retrieves the current public ledger state of the contract. - * @returns The ledger state as defined by the contract. - */ - public getCurrentPublicState(): Ledger { - return ledger(this.circuitContext.transactionContext.state); - } - - /** - * @description Retrieves the current private state of the contract. - * @returns The private state of type MyContractPrivateState. - */ - public getCurrentPrivateState(): MyContractPrivateState { - return this.circuitContext.currentPrivateState; - } - - /** - * @description Retrieves the current contract state. - * @returns The contract state object. - */ - public getCurrentContractState(): ContractState { - return this.circuitContext.originalState; - } - - /** - * @description Returns the contract name. - * @returns The contract name. - */ - public getName(): MaybeString { - return this.contract.impureCircuits.getName(this.circuitContext).result; - } - - /** - * @description Sets the contract name. - * @returns None. - */ - public setName(newName: MaybeString) { - return this.contract.impureCircuits.setName(this.circuitContext, newName) - .result; - } -} diff --git a/contracts/myContract/src/witnesses/MyContractWitnesses.ts b/contracts/myContract/src/witnesses/MyContractWitnesses.ts deleted file mode 100644 index 9547c404..00000000 --- a/contracts/myContract/src/witnesses/MyContractWitnesses.ts +++ /dev/null @@ -1,3 +0,0 @@ -// This is how we type an empty object. -export type MyContractPrivateState = Record; -export const MyContractWitnesses = {}; diff --git a/contracts/myContract/jest.config.ts b/contracts/ownable/jest.config.ts similarity index 100% rename from contracts/myContract/jest.config.ts rename to contracts/ownable/jest.config.ts diff --git a/contracts/myContract/js-resolver.cjs b/contracts/ownable/js-resolver.cjs similarity index 100% rename from contracts/myContract/js-resolver.cjs rename to contracts/ownable/js-resolver.cjs diff --git a/contracts/myContract/package.json b/contracts/ownable/package.json similarity index 95% rename from contracts/myContract/package.json rename to contracts/ownable/package.json index 7e991454..d1016a6a 100644 --- a/contracts/myContract/package.json +++ b/contracts/ownable/package.json @@ -1,5 +1,5 @@ { - "name": "@openzeppelin-midnight/myContract", + "name": "@openzeppelin-midnight/ownable", "type": "module", "main": "dist/index.js", "module": "dist/index.js", diff --git a/contracts/ownable/src/Ownable.compact b/contracts/ownable/src/Ownable.compact new file mode 100644 index 00000000..8f6952f6 --- /dev/null +++ b/contracts/ownable/src/Ownable.compact @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: MIT + +pragma language_version >= 0.14.0; + +/** + * @module Shielded Ownable module + * @description Get rekt, losers + */ +module Ownable { + import CompactStandardLibrary; + + /// Public state + export ledger _owner: Bytes<32>; + export ledger _pendingOwner: Bytes<32>; + export ledger _instance: Counter; + + /// Witnesses + witness localSecretKey(): Bytes<32>; + //witness setPendingOwner(currentOwner: Bytes<32>): Bytes<32>; + + /** + * @description Add me... + */ + export circuit initializer(): [] { + _instance.increment(1); + const initialOwner = disclose(publicKey(localSecretKey(), _instance as Field as Bytes<32>)); + _owner = initialOwner; + } + + /** + * @description Add me... + */ + export circuit owner(): Bytes<32> { + return _owner; + } + + /** + * @description Add me... + */ + //export circuit proposeNewOwner(): Bytes<32> { + // + //} + + /** + * @description Add me... + */ + export circuit renounceOwnership(): [] { + assertOnlyOwner(); + const zeroAddress = burn_address().left.bytes; + _transferOwnership(zeroAddress); + } + + /** + * @description Add me... + */ + export circuit assertOnlyOwner(): [] { + assert _owner == publicKey(localSecretKey(), _instance as Field as Bytes<32>) "Ownable: not owner"; + } + + /** + * @description Add me... + */ + export circuit publicKey(sk: Bytes<32>, instance: Bytes<32>): Bytes<32> { + // Using `self` ensures `_owner` will be unique even if the same `sk` is used in + // other contracts that use the `ownable:pk:` domain with the same `instance` + const self = kernel.self().bytes; + return persistent_hash>>([self, pad(32, "ownable:pk:"), instance, sk]); + } + + /** + * @description Add me... + */ + export circuit _transferOwnership(newOwner: Bytes<32>): [] { + _instance.increment(1); + _owner = newOwner; + } +} diff --git a/contracts/ownable/src/test/Ownable.test.ts b/contracts/ownable/src/test/Ownable.test.ts new file mode 100644 index 00000000..ce175d87 --- /dev/null +++ b/contracts/ownable/src/test/Ownable.test.ts @@ -0,0 +1,33 @@ +import { CoinPublicKey, encodeCoinPublicKey } from '@midnight-ntwrk/compact-runtime'; +import { OwnableSimulator } from './simulators/OwnableSimulator'; +import * as utils from './utils/address'; + +const OWNER = String(Buffer.from('OWNER', 'ascii').toString('hex')).padStart( + 64, + '0', +); +const SPENDER = String( + Buffer.from('SPENDER', 'ascii').toString('hex'), +).padStart(64, '0'); +const UNAUTHORIZED = String( + Buffer.from('UNAUTHORIZED', 'ascii').toString('hex'), +).padStart(64, '0'); +const ZERO = String().padStart(64, '0'); +const Z_OWNER = utils.createEitherTestUser('OWNER'); +const Z_RECIPIENT = utils.createEitherTestUser('RECIPIENT'); +const Z_SPENDER = utils.createEitherTestUser('SPENDER'); +const Z_OTHER = utils.createEitherTestUser('OTHER'); +const EMPTY_BYTES = utils.ZERO_KEY.left.bytes; + +let ownable: OwnableSimulator; +let caller: CoinPublicKey; + +describe('Ownable', () => { + describe('initializer', () => { + it('should initialize and set the caller as owner', () => { + caller = OWNER; + ownable = new OwnableSimulator(OWNER); + //expect(ownable.owner()).not.toEqual(utils.ZERO_ADDRESS); + }); + }); +}); diff --git a/contracts/ownable/src/test/mocks/MockOwnable.compact b/contracts/ownable/src/test/mocks/MockOwnable.compact new file mode 100644 index 00000000..d550c7dc --- /dev/null +++ b/contracts/ownable/src/test/mocks/MockOwnable.compact @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT + +pragma language_version >= 0.14.0; + +import CompactStandardLibrary; + +import "../../Ownable" prefix Ownable_; + +export { ZswapCoinPublicKey, ContractAddress, Either, Maybe }; +export { Ownable__owner, Ownable__pendingOwner, Ownable__instance }; + +constructor() { + Ownable_initializer(); +} + +export circuit owner(): Bytes<32> { + return Ownable_owner(); + } + +export circuit renounceOwnership(): [] { + return Ownable_renounceOwnership(); +} + +export circuit assertOnlyOwner(): [] { + return Ownable_assertOnlyOwner(); +} + +export circuit publicKey(sk: Bytes<32>, instance: Bytes<32>): Bytes<32> { + return Ownable_publicKey(sk, instance); +} + +export circuit _transferOwnership(newOwner: Bytes<32>): [] { + return Ownable__transferOwnership(newOwner); +} diff --git a/contracts/ownable/src/test/simulators/OwnableSimulator.ts b/contracts/ownable/src/test/simulators/OwnableSimulator.ts new file mode 100644 index 00000000..036e24e9 --- /dev/null +++ b/contracts/ownable/src/test/simulators/OwnableSimulator.ts @@ -0,0 +1,162 @@ +import { + type CircuitContext, + CoinPublicKey, + type ContractState, + QueryContext, + constructorContext, + emptyZswapLocalState, +} from '@midnight-ntwrk/compact-runtime'; +import { sampleContractAddress } from '@midnight-ntwrk/zswap'; +import { + type Ledger, + Contract as MockOwnable, + ledger, +} from '../../artifacts/MockOwnable/contract/index.cjs'; // Combined imports +import { + OwnablePrivateState, + OwnableWitnesses, +} from '../../witnesses/OwnableWitnesses'; +import type { MaybeString } from '../types/string'; +import type { IContractSimulator } from '../types/test'; + +/** + * @description A simulator implementation of a contract for testing purposes. + * @template P - The private state type, fixed to OwnablePrivateState. + * @template L - The ledger type, fixed to Contract.Ledger. + */ +export class OwnableSimulator + implements IContractSimulator +{ + /** @description The underlying contract instance managing contract logic. */ + readonly contract: MockOwnable; + + /** @description The deployed address of the contract. */ + readonly contractAddress: string; + + /** @description The deployer address of the contract. */ + readonly deployer: CoinPublicKey; + + /** @description The current circuit context, updated by contract operations. */ + circuitContext: CircuitContext; + + /** + * @description Initializes the mock contract. + */ + constructor(deployer: CoinPublicKey) { + this.contract = new MockOwnable( + OwnableWitnesses(), + ); + this.deployer = deployer; + const { + currentPrivateState, + currentContractState, + currentZswapLocalState, + } = this.contract.initialState( + constructorContext(OwnablePrivateState.generate(), deployer), + ); + this.circuitContext = { + currentPrivateState, + currentZswapLocalState, + originalState: currentContractState, + transactionContext: new QueryContext( + currentContractState.data, + sampleContractAddress(), + ), + }; + this.contractAddress = this.circuitContext.transactionContext.address; + } + + /** + * @description Retrieves the current public ledger state of the contract. + * @returns The ledger state as defined by the contract. + */ + public getCurrentPublicState(): Ledger { + return ledger(this.circuitContext.transactionContext.state); + } + + /** + * @description Retrieves the current private state of the contract. + * @returns The private state of type OwnablePrivateState. + */ + public getCurrentPrivateState(): OwnablePrivateState { + return this.circuitContext.currentPrivateState; + } + + /** + * @description Retrieves the current contract state. + * @returns The contract state object. + */ + public getCurrentContractState(): ContractState { + return this.circuitContext.originalState; + } + + /** + * @description Returns the contract name. + * @returns The contract name. + */ + //public getName(): MaybeString { + // return this.contract.impureCircuits.getName(this.circuitContext).result; + //} + + /** + * @description Sets the contract name. + * @returns None. + */ + //public setName(newName: MaybeString) { + // return this.contract.impureCircuits.setName(this.circuitContext, newName) + // .result; + //} + + public owner(): Uint8Array { + return this.contract.impureCircuits.owner(this.circuitContext).result; + } + + public renounceOwnership(sender: CoinPublicKey): CircuitContext { + const res = this.contract.impureCircuits.renounceOwnership( + { + ...this.circuitContext, + currentZswapLocalState: sender + ? emptyZswapLocalState(sender) + : this.circuitContext.currentZswapLocalState, + }, + ); + + this.circuitContext = res.context; + return this.circuitContext; + } + + public assertOnlyOwner(sender: CoinPublicKey): CircuitContext { + const res = this.contract.impureCircuits.renounceOwnership( + { + ...this.circuitContext, + currentZswapLocalState: sender + ? emptyZswapLocalState(sender) + : this.circuitContext.currentZswapLocalState, + }, + ); + + this.circuitContext = res.context; + return this.circuitContext; + } + + public publicKey(sk: Uint8Array, instance: Uint8Array, sender: CoinPublicKey): CircuitContext { + const res = this.contract.impureCircuits.publicKey( + { + ...this.circuitContext, + currentZswapLocalState: sender + ? emptyZswapLocalState(sender) + : this.circuitContext.currentZswapLocalState, + }, + sk, + instance + ); + + this.circuitContext = res.context; + return this.circuitContext; + } + + public _transferOwnership(newOwner: Uint8Array): CircuitContext { + this.circuitContext = this.contract.impureCircuits._transferOwnership(this.circuitContext, newOwner).context; + return this.circuitContext; + } +} diff --git a/contracts/myContract/src/test/types/string.ts b/contracts/ownable/src/test/types/string.ts similarity index 100% rename from contracts/myContract/src/test/types/string.ts rename to contracts/ownable/src/test/types/string.ts diff --git a/contracts/myContract/src/test/types/test.ts b/contracts/ownable/src/test/types/test.ts similarity index 100% rename from contracts/myContract/src/test/types/test.ts rename to contracts/ownable/src/test/types/test.ts diff --git a/contracts/myContract/src/test/utils/address.ts b/contracts/ownable/src/test/utils/address.ts similarity index 100% rename from contracts/myContract/src/test/utils/address.ts rename to contracts/ownable/src/test/utils/address.ts diff --git a/contracts/myContract/src/test/utils/test.ts b/contracts/ownable/src/test/utils/test.ts similarity index 100% rename from contracts/myContract/src/test/utils/test.ts rename to contracts/ownable/src/test/utils/test.ts diff --git a/contracts/ownable/src/witnesses/OwnableWitnesses.ts b/contracts/ownable/src/witnesses/OwnableWitnesses.ts new file mode 100644 index 00000000..9f737582 --- /dev/null +++ b/contracts/ownable/src/witnesses/OwnableWitnesses.ts @@ -0,0 +1,45 @@ +import { getRandomValues } from 'node:crypto'; +import { IOwnableWitnesses } from './interface'; +import type { WitnessContext } from '@midnight-ntwrk/compact-runtime'; +import { + type Ledger, + } from '../artifacts/MockOwnable/contract/index.cjs'; // Combined imports + +/** + * @description Represents the private state of an access control contract, storing a secret key and role assignments. + */ +export type OwnablePrivateState = { + /** @description A 32-byte secret key used for cryptographic operations, such as nullifier generation. */ + secretKey: Buffer; + }; + +/** + * @description Utility object for managing the private state of an ownable contract. + */ +export const OwnablePrivateState = { + /** + * @description Generates a new private state with a random secret key and empty roles. + * @returns A fresh OwnablePrivateState instance. + */ + generate: (): OwnablePrivateState => { + return { secretKey: getRandomValues(Buffer.alloc(32)) }; + }, +} + +/** + * @description Factory function creating witness implementations for access control operations. + * @returns An object implementing the Witnesses interface for AccessContractPrivateState. + */ +export const OwnableWitnesses = + (): IOwnableWitnesses => ({ + /** + * @description Retrieves the secret key from the private state. + * @param context - The witness context containing the private state. + * @returns A tuple of the unchanged private state and the secret key as a Uint8Array. + */ + localSecretKey( + context: WitnessContext, + ): [OwnablePrivateState, Uint8Array] { + return [context.privateState, context.privateState.secretKey]; + }, +}); \ No newline at end of file diff --git a/contracts/ownable/src/witnesses/interface.ts b/contracts/ownable/src/witnesses/interface.ts new file mode 100644 index 00000000..d264ee9d --- /dev/null +++ b/contracts/ownable/src/witnesses/interface.ts @@ -0,0 +1,17 @@ +import type { WitnessContext } from '@midnight-ntwrk/compact-runtime'; +import { + type Ledger, + } from '../artifacts/MockOwnable/contract/index.cjs'; // Combined imports + +/** + * @description Interface defining the witness methods for ownable operations. + * @template P - The private state type. + */ +export interface IOwnableWitnesses

{ + /** + * Retrieves the secret key from the private state. + * @param context - The witness context containing the private state. + * @returns A tuple of the private state and the secret key as a Uint8Array. + */ + localSecretKey(context: WitnessContext): [P, Uint8Array]; +} diff --git a/contracts/myContract/tsconfig.build.json b/contracts/ownable/tsconfig.build.json similarity index 100% rename from contracts/myContract/tsconfig.build.json rename to contracts/ownable/tsconfig.build.json diff --git a/contracts/myContract/tsconfig.json b/contracts/ownable/tsconfig.json similarity index 100% rename from contracts/myContract/tsconfig.json rename to contracts/ownable/tsconfig.json diff --git a/yarn.lock b/yarn.lock index 25a6a3ae..5214c131 100644 --- a/yarn.lock +++ b/yarn.lock @@ -896,9 +896,9 @@ __metadata: languageName: unknown linkType: soft -"@openzeppelin-midnight/myContract@workspace:contracts/myContract": +"@openzeppelin-midnight/ownable@workspace:contracts/ownable": version: 0.0.0-use.local - resolution: "@openzeppelin-midnight/myContract@workspace:contracts/myContract" + resolution: "@openzeppelin-midnight/ownable@workspace:contracts/ownable" dependencies: "@biomejs/biome": "npm:1.9.4" "@openzeppelin-midnight/compact": "workspace:^" From e13bdc83ef6091dfa8ef9ff0d3c7f769d7a47874 Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 7 May 2025 18:15:47 -0500 Subject: [PATCH 033/322] fix pk circuit --- contracts/ownable/src/Ownable.compact | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/contracts/ownable/src/Ownable.compact b/contracts/ownable/src/Ownable.compact index 8f6952f6..e501c055 100644 --- a/contracts/ownable/src/Ownable.compact +++ b/contracts/ownable/src/Ownable.compact @@ -61,10 +61,7 @@ module Ownable { * @description Add me... */ export circuit publicKey(sk: Bytes<32>, instance: Bytes<32>): Bytes<32> { - // Using `self` ensures `_owner` will be unique even if the same `sk` is used in - // other contracts that use the `ownable:pk:` domain with the same `instance` - const self = kernel.self().bytes; - return persistent_hash>>([self, pad(32, "ownable:pk:"), instance, sk]); + return persistent_hash>>([pad(32, "ownable:pk:"), instance, sk]); } /** From f41efd52d4a7e8c5b6824aaa26757cbc308407f1 Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 7 May 2025 18:16:10 -0500 Subject: [PATCH 034/322] add custom witness context --- .../ownable/src/witnesses/OwnableWitnesses.ts | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/contracts/ownable/src/witnesses/OwnableWitnesses.ts b/contracts/ownable/src/witnesses/OwnableWitnesses.ts index 9f737582..26350db3 100644 --- a/contracts/ownable/src/witnesses/OwnableWitnesses.ts +++ b/contracts/ownable/src/witnesses/OwnableWitnesses.ts @@ -6,10 +6,10 @@ import { } from '../artifacts/MockOwnable/contract/index.cjs'; // Combined imports /** - * @description Represents the private state of an access control contract, storing a secret key and role assignments. + * @description Represents the private state of an ownable contract, storing a secret key. */ export type OwnablePrivateState = { - /** @description A 32-byte secret key used for cryptographic operations, such as nullifier generation. */ + /** @description A 32-byte secret key used for cryptographic operations. */ secretKey: Buffer; }; @@ -18,7 +18,7 @@ export type OwnablePrivateState = { */ export const OwnablePrivateState = { /** - * @description Generates a new private state with a random secret key and empty roles. + * @description Generates a new private state with a random secret key. * @returns A fresh OwnablePrivateState instance. */ generate: (): OwnablePrivateState => { @@ -27,8 +27,8 @@ export const OwnablePrivateState = { } /** - * @description Factory function creating witness implementations for access control operations. - * @returns An object implementing the Witnesses interface for AccessContractPrivateState. + * @description Factory function creating witness implementations for ownable operations. + * @returns An object implementing the Witnesses interface for OwnablePrivateState. */ export const OwnableWitnesses = (): IOwnableWitnesses => ({ @@ -42,4 +42,18 @@ export const OwnableWitnesses = ): [OwnablePrivateState, Uint8Array] { return [context.privateState, context.privateState.secretKey]; }, -}); \ No newline at end of file +}); + +export const SetWitnessContext = + (sk: Uint8Array): IOwnableWitnesses => ({ + /** + * @description Retrieves the secret key from the private state. + * @param context - The witness context containing the private state. + * @returns A tuple of the unchanged private state and the passed `sk` as a Uint8Array. + */ + localSecretKey( + context: WitnessContext, + ): [OwnablePrivateState, Uint8Array] { + return [context.privateState, sk]; + }, +}); From 67b90351512e6b3c9d2858873e34174357a64d48 Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 7 May 2025 18:16:42 -0500 Subject: [PATCH 035/322] add setWitnessContext method --- .../src/test/simulators/OwnableSimulator.ts | 50 +++++-------------- 1 file changed, 12 insertions(+), 38 deletions(-) diff --git a/contracts/ownable/src/test/simulators/OwnableSimulator.ts b/contracts/ownable/src/test/simulators/OwnableSimulator.ts index 036e24e9..41cb3c27 100644 --- a/contracts/ownable/src/test/simulators/OwnableSimulator.ts +++ b/contracts/ownable/src/test/simulators/OwnableSimulator.ts @@ -15,9 +15,10 @@ import { import { OwnablePrivateState, OwnableWitnesses, + SetWitnessContext, } from '../../witnesses/OwnableWitnesses'; -import type { MaybeString } from '../types/string'; import type { IContractSimulator } from '../types/test'; +import { useCircuitContextSender } from '../utils/test'; /** * @description A simulator implementation of a contract for testing purposes. @@ -91,56 +92,29 @@ export class OwnableSimulator } /** - * @description Returns the contract name. - * @returns The contract name. - */ - //public getName(): MaybeString { - // return this.contract.impureCircuits.getName(this.circuitContext).result; - //} - - /** - * @description Sets the contract name. + * @description Changes the witness context by setting `sk` + * as the `secretKey`. * @returns None. */ - //public setName(newName: MaybeString) { - // return this.contract.impureCircuits.setName(this.circuitContext, newName) - // .result; - //} + public setWitnessContext(sk: Uint8Array) { + this.contract.witnesses = SetWitnessContext(sk); + } public owner(): Uint8Array { return this.contract.impureCircuits.owner(this.circuitContext).result; } - public renounceOwnership(sender: CoinPublicKey): CircuitContext { - const res = this.contract.impureCircuits.renounceOwnership( - { - ...this.circuitContext, - currentZswapLocalState: sender - ? emptyZswapLocalState(sender) - : this.circuitContext.currentZswapLocalState, - }, - ); - - this.circuitContext = res.context; + public renounceOwnership(): CircuitContext { + this.circuitContext = this.contract.impureCircuits.renounceOwnership(this.circuitContext).context; return this.circuitContext; } - public assertOnlyOwner(sender: CoinPublicKey): CircuitContext { - const res = this.contract.impureCircuits.renounceOwnership( - { - ...this.circuitContext, - currentZswapLocalState: sender - ? emptyZswapLocalState(sender) - : this.circuitContext.currentZswapLocalState, - }, - ); - - this.circuitContext = res.context; - return this.circuitContext; + public assertOnlyOwner(): CircuitContext { + return this.contract.impureCircuits.assertOnlyOwner(this.circuitContext).context; } public publicKey(sk: Uint8Array, instance: Uint8Array, sender: CoinPublicKey): CircuitContext { - const res = this.contract.impureCircuits.publicKey( + const res = this.contract.circuits.publicKey( { ...this.circuitContext, currentZswapLocalState: sender From 565449bda8377cdab38d7bd9bd519122458aa09a Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 7 May 2025 18:17:00 -0500 Subject: [PATCH 036/322] add ownable tests --- contracts/ownable/src/test/Ownable.test.ts | 96 +++++++++++++++++++++- 1 file changed, 94 insertions(+), 2 deletions(-) diff --git a/contracts/ownable/src/test/Ownable.test.ts b/contracts/ownable/src/test/Ownable.test.ts index ce175d87..f6e18c2c 100644 --- a/contracts/ownable/src/test/Ownable.test.ts +++ b/contracts/ownable/src/test/Ownable.test.ts @@ -1,4 +1,4 @@ -import { CoinPublicKey, encodeCoinPublicKey } from '@midnight-ntwrk/compact-runtime'; +import { CoinPublicKey, convert_bigint_to_Uint8Array, convert_Uint8Array_to_bigint } from '@midnight-ntwrk/compact-runtime'; import { OwnableSimulator } from './simulators/OwnableSimulator'; import * as utils from './utils/address'; @@ -18,16 +18,108 @@ const Z_RECIPIENT = utils.createEitherTestUser('RECIPIENT'); const Z_SPENDER = utils.createEitherTestUser('SPENDER'); const Z_OTHER = utils.createEitherTestUser('OTHER'); const EMPTY_BYTES = utils.ZERO_KEY.left.bytes; +const BAD_SECRET_KEY = convert_bigint_to_Uint8Array(32, 123456789n); let ownable: OwnableSimulator; let caller: CoinPublicKey; +let ownerSK: Uint8Array; describe('Ownable', () => { describe('initializer', () => { it('should initialize and set the caller as owner', () => { caller = OWNER; ownable = new OwnableSimulator(OWNER); - //expect(ownable.owner()).not.toEqual(utils.ZERO_ADDRESS); + expect(ownable.owner()).not.toEqual(utils.ZERO_ADDRESS); + expect(ownable.getCurrentPublicState().ownable_Instance).toEqual(1n); + }); + }); + + beforeEach(() => { + ownable = new OwnableSimulator(OWNER); + ownerSK = ownable.getCurrentPrivateState().secretKey; + }); + + describe('assertOnlyOwner', () => { + it('should allow owner to call', () => { + ownable.assertOnlyOwner(); + }); + + it('should fail with unauthorized caller', () => { + // Change secret key in witness context + ownable.setWitnessContext(BAD_SECRET_KEY); + + expect(() => { + ownable.assertOnlyOwner() + }).toThrow('Ownable: not owner'); + }); + + it('should handle owner → not-owner → owner calls', () => { + // Owner + ownable.assertOnlyOwner(); + + // Not owner + ownable.setWitnessContext(BAD_SECRET_KEY); + expect(() => { + ownable.assertOnlyOwner() + }).toThrow('Ownable: not owner'); + + // Owner + ownable.setWitnessContext(ownerSK); + ownable.assertOnlyOwner(); + }); + }); + + describe('renounceOwnership', () => { + it('should renounce ownership', () => { + ownable.renounceOwnership(); + expect(ownable.owner()).toEqual(EMPTY_BYTES); + + // Check that original owner can no longer call protected circuits + expect(() => { + ownable.assertOnlyOwner() + }).toThrow('Ownable: not owner'); + }); + + it('should not renounce ownership from non-owner', () => { + ownable.setWitnessContext(BAD_SECRET_KEY); + expect(() => { + ownable.renounceOwnership(); + }).toThrow('Ownable: not owner'); + }); + + it('should not renounce ownership from non-owner', () => { + ownable.setWitnessContext(BAD_SECRET_KEY); + expect(() => { + ownable.renounceOwnership(); + }).toThrow('Ownable: not owner'); + }); + + it('should not renounce ownership more than once', () => { + ownable.renounceOwnership(); + + expect(() => { + ownable.renounceOwnership(); + }).toThrow('Ownable: not owner'); }); }); }); + + +//const sk = ownable.getCurrentPrivateState().secretKey; +//console.log("skkkkkk", sk); +// +//ownable.setWitnessContext(BAD_SECRET_KEY); +////ownable.owner2(); +// +//expect(ownable.owner()).not.toEqual(utils.ZERO_ADDRESS); +//console.log("ownerrrr after", ownable.owner()); +// +//expect(() => { +// ownable.assertOnlyOwner() +//}).toThrow('Ownable: not owner'); +// +//ownable.setWitnessContext(sk); +//expect(() => { +// ownable.assertOnlyOwner() +//}).toThrow('Ownable: not owner'); +// \ No newline at end of file From 98ffd94dd6b21f878ddcbb93da6f9a39b22873ed Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 12 May 2025 00:36:39 -0500 Subject: [PATCH 037/322] add ownablePK --- contracts/ownable/src/OwnablePK.compact | 102 +++++++ contracts/ownable/src/test/OwnablePK.test.ts | 286 ++++++++++++++++++ .../src/test/mocks/MockOwnablePK.compact | 58 ++++ .../src/test/simulators/OwnablePKSimulator.ts | 183 +++++++++++ 4 files changed, 629 insertions(+) create mode 100644 contracts/ownable/src/OwnablePK.compact create mode 100644 contracts/ownable/src/test/OwnablePK.test.ts create mode 100644 contracts/ownable/src/test/mocks/MockOwnablePK.compact create mode 100644 contracts/ownable/src/test/simulators/OwnablePKSimulator.ts diff --git a/contracts/ownable/src/OwnablePK.compact b/contracts/ownable/src/OwnablePK.compact new file mode 100644 index 00000000..a9eea15d --- /dev/null +++ b/contracts/ownable/src/OwnablePK.compact @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: MIT + +pragma language_version >= 0.14.0; + +/** + * @module Shielded Ownable Public Key module + * @description Get rekt, losers + */ +module OwnablePK { + import CompactStandardLibrary; + + /// Public state + export ledger _owner: Bytes<32>; + export ledger _pendingOwner: Bytes<32>; + export ledger _instance: Counter; + + /** + * @description Add me... + */ + export circuit initializer(initOwner: ZswapCoinPublicKey): [] { + assert initOwner != burn_address().left "OwnablePK: new owner cannot be zero"; + const nextInstance = _instance + 1 as Field as Bytes<32>; + const shieldedOwner = shieldOwner(initOwner, nextInstance); + _transferOwnership(shieldedOwner); + } + + /** + * @description Add me... + */ + export circuit owner(): Bytes<32> { + return _owner; + } + + /** + * @description Add me... + */ + export circuit pendingOwner(): Bytes<32> { + return _pendingOwner; + } + + /** + * @description Add me... + */ + export circuit transferOwnership(newOwner: ZswapCoinPublicKey): [] { + assertOnlyOwner(); + _proposeOwner(newOwner); + } + + /** + * @description Add me... + */ + export circuit acceptOwnership(): [] { + const caller = own_public_key(); + const nextInstance = _instance + 1 as Field as Bytes<32>; + const shieldedOwner = shieldOwner(caller, nextInstance); + assert shieldedOwner == _pendingOwner "OwnablePK: caller is not pending owner"; + + // Reset pending owner and assign new owner + _transferOwnership(shieldedOwner); + } + + /** + * @description Add me... + */ + export circuit renounceOwnership(): [] { + assertOnlyOwner(); + _transferOwnership(default>); + } + + /** + * @description Add me... + */ + export circuit assertOnlyOwner(): [] { + const caller = own_public_key(); + assert _owner == shieldOwner(caller, _instance as Field as Bytes<32>) "OwnablePK: not owner"; + } + + /** + * @description Add me... + */ + export circuit shieldOwner(ownerPK: ZswapCoinPublicKey, instance: Bytes<32>): Bytes<32> { + return persistent_hash>>([pad(32, "OwnablePK:shield:"), instance, ownerPK.bytes]); + } + + /** + * @description Add me... + */ + export circuit _transferOwnership(newOwner: Bytes<32>): [] { + _pendingOwner = default>; + _instance.increment(1); + _owner = newOwner; + } + + /** + * @description Add me... + */ + export circuit _proposeOwner(newOwner: ZswapCoinPublicKey): [] { + assert newOwner != burn_address().left "OwnablePK: new owner cannot be zero"; + const nextInstance = _instance + 1 as Field as Bytes<32>; + _pendingOwner = shieldOwner(newOwner, nextInstance); + } +} diff --git a/contracts/ownable/src/test/OwnablePK.test.ts b/contracts/ownable/src/test/OwnablePK.test.ts new file mode 100644 index 00000000..6c1583bc --- /dev/null +++ b/contracts/ownable/src/test/OwnablePK.test.ts @@ -0,0 +1,286 @@ +import { CoinPublicKey, convert_bigint_to_Uint8Array, convert_Uint8Array_to_bigint } from '@midnight-ntwrk/compact-runtime'; +import { OwnablePKSimulator } from './simulators/OwnablePKSimulator'; +import * as utils from './utils/address'; + +const OWNER = String(Buffer.from('OWNER', 'ascii').toString('hex')).padStart( + 64, + '0', +); +const NEW_OWNER = String( + Buffer.from('NEW_OWNER', 'ascii').toString('hex'), +).padStart(64, '0'); +const UNAUTHORIZED = String( + Buffer.from('UNAUTHORIZED', 'ascii').toString('hex'), +).padStart(64, '0'); +const Z_ZERO = utils.encodeToPK(''); +const Z_OWNER = utils.encodeToPK('OWNER'); +const Z_NEW_OWNER = utils.encodeToPK('NEW_OWNER'); +const Z_NEW_NEW_OWNER = utils.encodeToPK('Z_NEW_NEW_OWNER'); +const EMPTY_BYTES = utils.ZERO_KEY.left.bytes; + +let ownable: OwnablePKSimulator; +let caller: CoinPublicKey; +let ownerSK: Uint8Array; + +describe('OwnablePK', () => { + describe('initializer', () => { + it('should initialize and set the shielded owner', () => { + ownable = new OwnablePKSimulator(Z_OWNER, OWNER); + + // Check instance + const instance = ownable.getCurrentPublicState().ownablePK_Instance; + expect(instance).toEqual(1n); + + // Check shielded owner + const expOwner = ownable.shieldOwner(Z_OWNER, convert_bigint_to_Uint8Array(32, instance)); + expect(ownable.owner()).toEqual(expOwner); + + // Check pending owner + const pendingOwner = ownable.getCurrentPublicState().ownablePK_PendingOwner; + expect(pendingOwner).toEqual(EMPTY_BYTES) + }); + + it('should fail when initializing owner as zero', () => { + expect(() => { + ownable = new OwnablePKSimulator(utils.ZERO_KEY.left, OWNER); + }).toThrow('OwnablePK: new owner cannot be zero'); + }); + }); + + describe('with owner set', () => { + beforeEach(() => { + ownable = new OwnablePKSimulator(Z_OWNER, OWNER); + }); + + describe('owner', () => { + it('should return correct owner', () => { + expect(ownable.owner()).toEqual(ownable.getCurrentPublicState().ownablePK_Owner); + }); + + it('should return no owner', () => { + // Set owner to zero + ownable._transferOwnership(EMPTY_BYTES); + expect(ownable.owner()).toEqual(EMPTY_BYTES); + }); + }); + + describe('pendingOwner', () => { + it('should return pending owner', () => { + const nextInstance = ownable.getCurrentPublicState().ownablePK_Instance + 1n; + const expPending = ownable.shieldOwner(Z_NEW_OWNER, convert_bigint_to_Uint8Array(32, nextInstance)); + ownable._proposeOwner(Z_NEW_OWNER); + expect(ownable.pendingOwner()).toEqual(expPending); + }); + + it('should return no pending owner', () => { + expect(ownable.pendingOwner()).toEqual(EMPTY_BYTES); + }); + }); + + describe('transferOwnership', () => { + it('should start two-step transfer', () => { + caller = OWNER; + + ownable.transferOwnership(Z_NEW_OWNER, caller); + + // Check pending owner + const nextInstance = ownable.getCurrentPublicState().ownablePK_Instance + 1n; + const expPending = ownable.shieldOwner(Z_NEW_OWNER, convert_bigint_to_Uint8Array(32, nextInstance)); + expect(ownable.pendingOwner()).toEqual(expPending); + + // Check current owner + const thisInstance = ownable.getCurrentPublicState().ownablePK_Instance; + const expOwner = ownable.shieldOwner(Z_OWNER, convert_bigint_to_Uint8Array(32, thisInstance)); + expect(ownable.owner()).toEqual(expOwner); + }); + + it('should not transfer zero as owner', () => { + caller = OWNER; + + expect(() => { + ownable.transferOwnership(Z_ZERO, caller); + }).toThrow('OwnablePK: new owner cannot be zero'); + }); + + it('should not transfer owner from unauthorized caller', () => { + caller = UNAUTHORIZED; + + expect(() => { + ownable.transferOwnership(Z_NEW_OWNER, caller); + }).toThrow('OwnablePK: not owner'); + }); + + it('should overwrite pending owner with new owner', () => { + caller = OWNER; + + ownable.transferOwnership(Z_NEW_OWNER, caller); + ownable.transferOwnership(Z_NEW_NEW_OWNER, caller); + + // Check new pending owner + const nextInstance = ownable.getCurrentPublicState().ownablePK_Instance + 1n; + const expPending = ownable.shieldOwner(Z_NEW_NEW_OWNER, convert_bigint_to_Uint8Array(32, nextInstance)); + expect(ownable.pendingOwner()).toEqual(expPending); + }); + }); + + describe('acceptOwnership', () => { + describe('when owner is pending', () => { + beforeEach(() => { + ownable._proposeOwner(Z_NEW_OWNER); + }); + + it('should accept ownership from pending owner', () => { + caller = NEW_OWNER; + const beforeInstance = ownable.getCurrentPublicState().ownablePK_Instance; + + ownable.acceptOwnership(caller); + + // Check instance is bumped + const afterInstance = ownable.getCurrentPublicState().ownablePK_Instance; + expect(afterInstance).toEqual(beforeInstance + 1n); + + // Check new owner + const expOwner = ownable.shieldOwner(Z_NEW_OWNER, convert_bigint_to_Uint8Array(32, afterInstance)); + expect(ownable.owner()).toEqual(expOwner); + + // Check pending owner is reset + expect(ownable.pendingOwner()).toEqual(EMPTY_BYTES); + }); + + it('should not accept ownership from unauthorized', () => { + caller = UNAUTHORIZED; + + expect(() => { + ownable.acceptOwnership(caller); + }).toThrow('OwnablePK: caller is not pending owner'); + }); + + it('should not accept ownership from current owner', () => { + caller = OWNER; + + expect(() => { + ownable.acceptOwnership(caller); + }).toThrow('OwnablePK: caller is not pending owner'); + }); + + it('should not accept ownership from previous owner', () => { + caller = NEW_OWNER; + // Sets new owner + ownable.acceptOwnership(caller); + + // New owner proposes another new owner + ownable.transferOwnership(Z_NEW_NEW_OWNER, caller); + + // Initial owner tries to accept + caller = OWNER; + expect(() => { + ownable.acceptOwnership(caller); + }).toThrow('OwnablePK: caller is not pending owner') + }); + }); + }); + + describe('renounceOwnership', () => { + it('should renounce ownership', () => { + caller = OWNER; + const beforeInstance = ownable.getCurrentPublicState().ownablePK_Instance; + ownable.renounceOwnership(caller); + + expect(ownable.owner()).toEqual(EMPTY_BYTES); + expect(ownable.pendingOwner()).toEqual(EMPTY_BYTES); + expect(ownable.getCurrentPublicState().ownablePK_Instance).toEqual(beforeInstance + 1n); + }); + + it('should not renounce from unauthorized', () => { + caller = UNAUTHORIZED; + expect(() => { + ownable.renounceOwnership(caller); + }).toThrow('OwnablePK: not owner'); + }); + }); + + describe('assertOnlyOwner', () => { + it('should allow owner to call', () => { + caller = OWNER; + + ownable.assertOnlyOwner(caller); + }); + + it('should not allow unauthorized to call', () => { + caller = UNAUTHORIZED; + expect(() => { + ownable.assertOnlyOwner(caller); + }).toThrow('OwnablePK: not owner'); + }); + + it('should update who can and cannot call', () => { + caller = OWNER; + ownable.assertOnlyOwner(caller); + + caller = NEW_OWNER; + expect(() => { + ownable.assertOnlyOwner(caller); + }).toThrow('OwnablePK: not owner'); + + // Transfer to new owner + const nextInstance = ownable.getCurrentPublicState().ownablePK_Instance + 1n; + const newOwner = ownable.shieldOwner(Z_NEW_OWNER, convert_bigint_to_Uint8Array(32, nextInstance)); + ownable._transferOwnership(newOwner); + + caller = NEW_OWNER; + ownable.assertOnlyOwner(caller); + + caller = OWNER; + expect(() => { + ownable.assertOnlyOwner(caller); + }).toThrow('OwnablePK: not owner'); + }); + }); + + describe('shieldOwner', () => { + it.skip('should hash owner correctly', () => { + const instance = convert_bigint_to_Uint8Array(32, 123n); + const expHash = ownable.shieldOwner(Z_OWNER, instance); + // TODO add matching algo in js + }); + }); + + describe('_transferOwnership', () => { + it('should transfer ownership', () => { + const beforeInstance = ownable.getCurrentPublicState().ownablePK_Instance; + ownable._proposeOwner(Z_NEW_NEW_OWNER); + + ownable._transferOwnership(Z_NEW_OWNER.bytes); + + // _transferownership does not shield the input so it should be a == a + expect(ownable.owner()).toEqual(Z_NEW_OWNER.bytes); + // Check instance is bumped + expect(ownable.getCurrentPublicState().ownablePK_Instance).toEqual(beforeInstance + 1n); + // Check pending owner is reset + expect(ownable.pendingOwner()).toEqual(EMPTY_BYTES); + }); + + it('should transfer ownership to zero', () => { + // _transfer does not shield the input so it should be a == a + ownable._transferOwnership(EMPTY_BYTES); + expect(ownable.owner()).toEqual(EMPTY_BYTES); + }); + }); + + describe('proposeOwner', () => { + it('should propose owner', () => { + ownable._proposeOwner(Z_NEW_OWNER); + + const nextInstance = ownable.getCurrentPublicState().ownablePK_Instance + 1n; + const expOwner = ownable.shieldOwner(Z_NEW_OWNER, convert_bigint_to_Uint8Array(32, nextInstance)); + expect(ownable.pendingOwner()).toEqual(expOwner); + }); + + it('should not propose zero as owner', () => { + expect(() => { + ownable._proposeOwner(utils.ZERO_KEY.left); + }).toThrow('OwnablePK: new owner cannot be zero'); + }); + }); + }); +}); diff --git a/contracts/ownable/src/test/mocks/MockOwnablePK.compact b/contracts/ownable/src/test/mocks/MockOwnablePK.compact new file mode 100644 index 00000000..5b39fc36 --- /dev/null +++ b/contracts/ownable/src/test/mocks/MockOwnablePK.compact @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT + +pragma language_version >= 0.14.0; + +import CompactStandardLibrary; +import "../../OwnablePK" prefix OwnablePK_; + +export { ZswapCoinPublicKey, ContractAddress, Either, Maybe }; +export { OwnablePK__owner, OwnablePK__pendingOwner, OwnablePK__instance }; + +constructor(initOwner: ZswapCoinPublicKey) { + OwnablePK_initializer(initOwner); +} + +export circuit owner(): Bytes<32> { + return OwnablePK_owner(); +} + +export circuit pendingOwner(): Bytes<32> { + return OwnablePK_pendingOwner(); +} + +export circuit transferOwnership(newOwner: ZswapCoinPublicKey): [] { + return OwnablePK_transferOwnership(newOwner); +} + +export circuit acceptOwnership(): [] { + return OwnablePK_acceptOwnership(); +} + +export circuit renounceOwnership(): [] { + return OwnablePK_renounceOwnership(); +} + +export circuit assertOnlyOwner(): [] { + return OwnablePK_assertOnlyOwner(); +} + +export circuit shieldOwner(ownerPK: ZswapCoinPublicKey, instance: Bytes<32>): Bytes<32> { + return OwnablePK_shieldOwner(ownerPK, instance); +} + +export circuit _transferOwnership(newOwner: Bytes<32>): [] { + return OwnablePK__transferOwnership(newOwner); +} + +export circuit _proposeOwner(newOwner: ZswapCoinPublicKey): [] { + return OwnablePK__proposeOwner(newOwner); +} + +export circuit thing(): Bytes<32> { + const f = kernel.self(); + return f.bytes; +} + +export circuit thing2(): ContractAddress { + return kernel.self(); +} diff --git a/contracts/ownable/src/test/simulators/OwnablePKSimulator.ts b/contracts/ownable/src/test/simulators/OwnablePKSimulator.ts new file mode 100644 index 00000000..64780a02 --- /dev/null +++ b/contracts/ownable/src/test/simulators/OwnablePKSimulator.ts @@ -0,0 +1,183 @@ +import { + type CircuitContext, + CoinPublicKey, + type ContractState, + QueryContext, + constructorContext, + emptyZswapLocalState, +} from '@midnight-ntwrk/compact-runtime'; +import { sampleContractAddress } from '@midnight-ntwrk/zswap'; +import { + type Ledger, + Contract as MockOwnable, + ledger, +} from '../../artifacts/MockOwnablePK/contract/index.cjs'; // Combined imports +import { + OwnablePrivateState, + OwnableWitnesses, + SetWitnessContext, +} from '../../witnesses/OwnableWitnesses'; +import type { IContractSimulator } from '../types/test'; +import { useCircuitContextSender } from '../utils/test'; +import { ZswapCoinPublicKey } from '../../artifacts/MockOwnable/contract/index.cjs'; + +/** + * @description A simulator implementation of a contract for testing purposes. + * @template P - The private state type, fixed to OwnablePrivateState. + * @template L - The ledger type, fixed to Contract.Ledger. + */ +export class OwnablePKSimulator + implements IContractSimulator +{ + /** @description The underlying contract instance managing contract logic. */ + readonly contract: MockOwnable; + + /** @description The deployed address of the contract. */ + readonly contractAddress: string; + + /** @description The deployer address of the contract. */ + readonly deployer: CoinPublicKey; + + /** @description The current circuit context, updated by contract operations. */ + circuitContext: CircuitContext; + + /** + * @description Initializes the mock contract. + */ + constructor(initOwner: ZswapCoinPublicKey, deployer: CoinPublicKey) { + this.contract = new MockOwnable( + OwnableWitnesses(), + ); + this.deployer = deployer; + const { + currentPrivateState, + currentContractState, + currentZswapLocalState, + } = this.contract.initialState( + constructorContext(OwnablePrivateState.generate(), deployer), + initOwner + ); + this.circuitContext = { + currentPrivateState, + currentZswapLocalState, + originalState: currentContractState, + transactionContext: new QueryContext( + currentContractState.data, + sampleContractAddress(), + ), + }; + this.contractAddress = this.circuitContext.transactionContext.address; + } + + /** + * @description Retrieves the current public ledger state of the contract. + * @returns The ledger state as defined by the contract. + */ + public getCurrentPublicState(): Ledger { + return ledger(this.circuitContext.transactionContext.state); + } + + /** + * @description Retrieves the current private state of the contract. + * @returns The private state of type OwnablePrivateState. + */ + public getCurrentPrivateState(): OwnablePrivateState { + return this.circuitContext.currentPrivateState; + } + + /** + * @description Retrieves the current contract state. + * @returns The contract state object. + */ + public getCurrentContractState(): ContractState { + return this.circuitContext.originalState; + } + + public owner(): Uint8Array { + return this.contract.impureCircuits.owner(this.circuitContext).result; + } + + public pendingOwner(): Uint8Array { + return this.contract.impureCircuits.pendingOwner(this.circuitContext).result; + } + + public transferOwnership(newOwner: ZswapCoinPublicKey, sender: CoinPublicKey): CircuitContext { + const res = this.contract.impureCircuits.transferOwnership( + { + ...this.circuitContext, + currentZswapLocalState: sender + ? emptyZswapLocalState(sender) + : this.circuitContext.currentZswapLocalState, + }, + newOwner, + ); + + this.circuitContext = res.context; + return this.circuitContext; + } + + public acceptOwnership(sender: CoinPublicKey): CircuitContext { + const res = this.contract.impureCircuits.acceptOwnership( + { + ...this.circuitContext, + currentZswapLocalState: sender + ? emptyZswapLocalState(sender) + : this.circuitContext.currentZswapLocalState, + }, + ); + + this.circuitContext = res.context; + return this.circuitContext; + } + + public renounceOwnership(sender: CoinPublicKey): CircuitContext { + const res = this.contract.impureCircuits.renounceOwnership( + { + ...this.circuitContext, + currentZswapLocalState: sender + ? emptyZswapLocalState(sender) + : this.circuitContext.currentZswapLocalState, + }, + ); + + this.circuitContext = res.context; + return this.circuitContext; + } + + public assertOnlyOwner(sender: CoinPublicKey): CircuitContext { + const res = this.contract.impureCircuits.assertOnlyOwner( + { + ...this.circuitContext, + currentZswapLocalState: sender + ? emptyZswapLocalState(sender) + : this.circuitContext.currentZswapLocalState, + }, + ); + + this.circuitContext = res.context; + return this.circuitContext; + } + + public shieldOwner(ownerPK: ZswapCoinPublicKey, instance: Uint8Array): Uint8Array { + return this.contract.circuits.shieldOwner(this.circuitContext, ownerPK, instance).result + } + + public _transferOwnership(newOwner: Uint8Array): CircuitContext { + this.circuitContext = this.contract.impureCircuits._transferOwnership(this.circuitContext, newOwner).context; + return this.circuitContext; + } + + public _proposeOwner(newOwner: ZswapCoinPublicKey): CircuitContext { + this.circuitContext = this.contract.impureCircuits._proposeOwner(this.circuitContext, newOwner).context; + return this.circuitContext; + } + + public thing(): Uint8Array { + return this.contract.impureCircuits.thing(this.circuitContext).result; + } + + public thing2() { + return this.contract.impureCircuits.thing2(this.circuitContext).result; + } + +} From 7a73e927deaf670ce46341c71059eeee6d614821 Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 12 May 2025 00:37:13 -0500 Subject: [PATCH 038/322] clean up mock and sim --- contracts/ownable/src/test/mocks/MockOwnablePK.compact | 9 --------- .../ownable/src/test/simulators/OwnablePKSimulator.ts | 9 --------- 2 files changed, 18 deletions(-) diff --git a/contracts/ownable/src/test/mocks/MockOwnablePK.compact b/contracts/ownable/src/test/mocks/MockOwnablePK.compact index 5b39fc36..1dcc9de1 100644 --- a/contracts/ownable/src/test/mocks/MockOwnablePK.compact +++ b/contracts/ownable/src/test/mocks/MockOwnablePK.compact @@ -47,12 +47,3 @@ export circuit _transferOwnership(newOwner: Bytes<32>): [] { export circuit _proposeOwner(newOwner: ZswapCoinPublicKey): [] { return OwnablePK__proposeOwner(newOwner); } - -export circuit thing(): Bytes<32> { - const f = kernel.self(); - return f.bytes; -} - -export circuit thing2(): ContractAddress { - return kernel.self(); -} diff --git a/contracts/ownable/src/test/simulators/OwnablePKSimulator.ts b/contracts/ownable/src/test/simulators/OwnablePKSimulator.ts index 64780a02..04934c46 100644 --- a/contracts/ownable/src/test/simulators/OwnablePKSimulator.ts +++ b/contracts/ownable/src/test/simulators/OwnablePKSimulator.ts @@ -171,13 +171,4 @@ export class OwnablePKSimulator this.circuitContext = this.contract.impureCircuits._proposeOwner(this.circuitContext, newOwner).context; return this.circuitContext; } - - public thing(): Uint8Array { - return this.contract.impureCircuits.thing(this.circuitContext).result; - } - - public thing2() { - return this.contract.impureCircuits.thing2(this.circuitContext).result; - } - } From 60de48e85fefdd20b5448118cba13f8b632d6947 Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 12 May 2025 01:06:30 -0500 Subject: [PATCH 039/322] fix import --- contracts/ownable/src/test/utils/address.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/ownable/src/test/utils/address.ts b/contracts/ownable/src/test/utils/address.ts index 3580e196..d061e4b2 100644 --- a/contracts/ownable/src/test/utils/address.ts +++ b/contracts/ownable/src/test/utils/address.ts @@ -3,7 +3,7 @@ import { encodeCoinPublicKey, } from '@midnight-ntwrk/compact-runtime'; import { encodeContractAddress } from '@midnight-ntwrk/ledger'; -import type * as Compact from '../../artifacts/MockERC20/contract/index.cjs'; +import type * as Compact from '../../artifacts/MockOwnablePK/contract/index.cjs'; const PREFIX_ADDRESS = '0200'; From bf94cde37f04a70a035b51e4d4eca950f84f30b0 Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 12 May 2025 01:06:57 -0500 Subject: [PATCH 040/322] fix fmt --- compact/package.json | 5 +- contracts/ownable/src/test/Ownable.test.ts | 15 +- contracts/ownable/src/test/OwnablePK.test.ts | 92 +++++++++--- .../src/test/simulators/OwnablePKSimulator.ts | 137 ++++++++++-------- .../src/test/simulators/OwnableSimulator.ts | 28 ++-- .../ownable/src/witnesses/OwnableWitnesses.ts | 74 +++++----- contracts/ownable/src/witnesses/interface.ts | 4 +- 7 files changed, 212 insertions(+), 143 deletions(-) diff --git a/compact/package.json b/compact/package.json index 15b7595a..84ede8f5 100644 --- a/compact/package.json +++ b/compact/package.json @@ -2,10 +2,7 @@ "packageManager": "yarn@4.1.0", "name": "@openzeppelin-midnight/compact", "version": "0.0.1", - "keywords": [ - "compact", - "compiler" - ], + "keywords": ["compact", "compiler"], "author": "OpenZeppelin Community ", "license": "MIT", "description": "Compact fetcher", diff --git a/contracts/ownable/src/test/Ownable.test.ts b/contracts/ownable/src/test/Ownable.test.ts index f6e18c2c..09c27268 100644 --- a/contracts/ownable/src/test/Ownable.test.ts +++ b/contracts/ownable/src/test/Ownable.test.ts @@ -1,4 +1,8 @@ -import { CoinPublicKey, convert_bigint_to_Uint8Array, convert_Uint8Array_to_bigint } from '@midnight-ntwrk/compact-runtime'; +import { + CoinPublicKey, + convert_bigint_to_Uint8Array, + convert_Uint8Array_to_bigint, +} from '@midnight-ntwrk/compact-runtime'; import { OwnableSimulator } from './simulators/OwnableSimulator'; import * as utils from './utils/address'; @@ -49,7 +53,7 @@ describe('Ownable', () => { ownable.setWitnessContext(BAD_SECRET_KEY); expect(() => { - ownable.assertOnlyOwner() + ownable.assertOnlyOwner(); }).toThrow('Ownable: not owner'); }); @@ -60,7 +64,7 @@ describe('Ownable', () => { // Not owner ownable.setWitnessContext(BAD_SECRET_KEY); expect(() => { - ownable.assertOnlyOwner() + ownable.assertOnlyOwner(); }).toThrow('Ownable: not owner'); // Owner @@ -76,7 +80,7 @@ describe('Ownable', () => { // Check that original owner can no longer call protected circuits expect(() => { - ownable.assertOnlyOwner() + ownable.assertOnlyOwner(); }).toThrow('Ownable: not owner'); }); @@ -104,7 +108,6 @@ describe('Ownable', () => { }); }); - //const sk = ownable.getCurrentPrivateState().secretKey; //console.log("skkkkkk", sk); // @@ -122,4 +125,4 @@ describe('Ownable', () => { //expect(() => { // ownable.assertOnlyOwner() //}).toThrow('Ownable: not owner'); -// \ No newline at end of file +// diff --git a/contracts/ownable/src/test/OwnablePK.test.ts b/contracts/ownable/src/test/OwnablePK.test.ts index 6c1583bc..640b79a3 100644 --- a/contracts/ownable/src/test/OwnablePK.test.ts +++ b/contracts/ownable/src/test/OwnablePK.test.ts @@ -1,4 +1,8 @@ -import { CoinPublicKey, convert_bigint_to_Uint8Array, convert_Uint8Array_to_bigint } from '@midnight-ntwrk/compact-runtime'; +import { + CoinPublicKey, + convert_bigint_to_Uint8Array, + convert_Uint8Array_to_bigint, +} from '@midnight-ntwrk/compact-runtime'; import { OwnablePKSimulator } from './simulators/OwnablePKSimulator'; import * as utils from './utils/address'; @@ -32,12 +36,16 @@ describe('OwnablePK', () => { expect(instance).toEqual(1n); // Check shielded owner - const expOwner = ownable.shieldOwner(Z_OWNER, convert_bigint_to_Uint8Array(32, instance)); + const expOwner = ownable.shieldOwner( + Z_OWNER, + convert_bigint_to_Uint8Array(32, instance), + ); expect(ownable.owner()).toEqual(expOwner); // Check pending owner - const pendingOwner = ownable.getCurrentPublicState().ownablePK_PendingOwner; - expect(pendingOwner).toEqual(EMPTY_BYTES) + const pendingOwner = + ownable.getCurrentPublicState().ownablePK_PendingOwner; + expect(pendingOwner).toEqual(EMPTY_BYTES); }); it('should fail when initializing owner as zero', () => { @@ -54,7 +62,9 @@ describe('OwnablePK', () => { describe('owner', () => { it('should return correct owner', () => { - expect(ownable.owner()).toEqual(ownable.getCurrentPublicState().ownablePK_Owner); + expect(ownable.owner()).toEqual( + ownable.getCurrentPublicState().ownablePK_Owner, + ); }); it('should return no owner', () => { @@ -66,8 +76,12 @@ describe('OwnablePK', () => { describe('pendingOwner', () => { it('should return pending owner', () => { - const nextInstance = ownable.getCurrentPublicState().ownablePK_Instance + 1n; - const expPending = ownable.shieldOwner(Z_NEW_OWNER, convert_bigint_to_Uint8Array(32, nextInstance)); + const nextInstance = + ownable.getCurrentPublicState().ownablePK_Instance + 1n; + const expPending = ownable.shieldOwner( + Z_NEW_OWNER, + convert_bigint_to_Uint8Array(32, nextInstance), + ); ownable._proposeOwner(Z_NEW_OWNER); expect(ownable.pendingOwner()).toEqual(expPending); }); @@ -84,13 +98,20 @@ describe('OwnablePK', () => { ownable.transferOwnership(Z_NEW_OWNER, caller); // Check pending owner - const nextInstance = ownable.getCurrentPublicState().ownablePK_Instance + 1n; - const expPending = ownable.shieldOwner(Z_NEW_OWNER, convert_bigint_to_Uint8Array(32, nextInstance)); + const nextInstance = + ownable.getCurrentPublicState().ownablePK_Instance + 1n; + const expPending = ownable.shieldOwner( + Z_NEW_OWNER, + convert_bigint_to_Uint8Array(32, nextInstance), + ); expect(ownable.pendingOwner()).toEqual(expPending); // Check current owner const thisInstance = ownable.getCurrentPublicState().ownablePK_Instance; - const expOwner = ownable.shieldOwner(Z_OWNER, convert_bigint_to_Uint8Array(32, thisInstance)); + const expOwner = ownable.shieldOwner( + Z_OWNER, + convert_bigint_to_Uint8Array(32, thisInstance), + ); expect(ownable.owner()).toEqual(expOwner); }); @@ -117,8 +138,12 @@ describe('OwnablePK', () => { ownable.transferOwnership(Z_NEW_NEW_OWNER, caller); // Check new pending owner - const nextInstance = ownable.getCurrentPublicState().ownablePK_Instance + 1n; - const expPending = ownable.shieldOwner(Z_NEW_NEW_OWNER, convert_bigint_to_Uint8Array(32, nextInstance)); + const nextInstance = + ownable.getCurrentPublicState().ownablePK_Instance + 1n; + const expPending = ownable.shieldOwner( + Z_NEW_NEW_OWNER, + convert_bigint_to_Uint8Array(32, nextInstance), + ); expect(ownable.pendingOwner()).toEqual(expPending); }); }); @@ -131,16 +156,21 @@ describe('OwnablePK', () => { it('should accept ownership from pending owner', () => { caller = NEW_OWNER; - const beforeInstance = ownable.getCurrentPublicState().ownablePK_Instance; + const beforeInstance = + ownable.getCurrentPublicState().ownablePK_Instance; ownable.acceptOwnership(caller); // Check instance is bumped - const afterInstance = ownable.getCurrentPublicState().ownablePK_Instance; + const afterInstance = + ownable.getCurrentPublicState().ownablePK_Instance; expect(afterInstance).toEqual(beforeInstance + 1n); // Check new owner - const expOwner = ownable.shieldOwner(Z_NEW_OWNER, convert_bigint_to_Uint8Array(32, afterInstance)); + const expOwner = ownable.shieldOwner( + Z_NEW_OWNER, + convert_bigint_to_Uint8Array(32, afterInstance), + ); expect(ownable.owner()).toEqual(expOwner); // Check pending owner is reset @@ -175,7 +205,7 @@ describe('OwnablePK', () => { caller = OWNER; expect(() => { ownable.acceptOwnership(caller); - }).toThrow('OwnablePK: caller is not pending owner') + }).toThrow('OwnablePK: caller is not pending owner'); }); }); }); @@ -183,12 +213,15 @@ describe('OwnablePK', () => { describe('renounceOwnership', () => { it('should renounce ownership', () => { caller = OWNER; - const beforeInstance = ownable.getCurrentPublicState().ownablePK_Instance; + const beforeInstance = + ownable.getCurrentPublicState().ownablePK_Instance; ownable.renounceOwnership(caller); expect(ownable.owner()).toEqual(EMPTY_BYTES); expect(ownable.pendingOwner()).toEqual(EMPTY_BYTES); - expect(ownable.getCurrentPublicState().ownablePK_Instance).toEqual(beforeInstance + 1n); + expect(ownable.getCurrentPublicState().ownablePK_Instance).toEqual( + beforeInstance + 1n, + ); }); it('should not renounce from unauthorized', () => { @@ -223,8 +256,12 @@ describe('OwnablePK', () => { }).toThrow('OwnablePK: not owner'); // Transfer to new owner - const nextInstance = ownable.getCurrentPublicState().ownablePK_Instance + 1n; - const newOwner = ownable.shieldOwner(Z_NEW_OWNER, convert_bigint_to_Uint8Array(32, nextInstance)); + const nextInstance = + ownable.getCurrentPublicState().ownablePK_Instance + 1n; + const newOwner = ownable.shieldOwner( + Z_NEW_OWNER, + convert_bigint_to_Uint8Array(32, nextInstance), + ); ownable._transferOwnership(newOwner); caller = NEW_OWNER; @@ -247,7 +284,8 @@ describe('OwnablePK', () => { describe('_transferOwnership', () => { it('should transfer ownership', () => { - const beforeInstance = ownable.getCurrentPublicState().ownablePK_Instance; + const beforeInstance = + ownable.getCurrentPublicState().ownablePK_Instance; ownable._proposeOwner(Z_NEW_NEW_OWNER); ownable._transferOwnership(Z_NEW_OWNER.bytes); @@ -255,7 +293,9 @@ describe('OwnablePK', () => { // _transferownership does not shield the input so it should be a == a expect(ownable.owner()).toEqual(Z_NEW_OWNER.bytes); // Check instance is bumped - expect(ownable.getCurrentPublicState().ownablePK_Instance).toEqual(beforeInstance + 1n); + expect(ownable.getCurrentPublicState().ownablePK_Instance).toEqual( + beforeInstance + 1n, + ); // Check pending owner is reset expect(ownable.pendingOwner()).toEqual(EMPTY_BYTES); }); @@ -271,8 +311,12 @@ describe('OwnablePK', () => { it('should propose owner', () => { ownable._proposeOwner(Z_NEW_OWNER); - const nextInstance = ownable.getCurrentPublicState().ownablePK_Instance + 1n; - const expOwner = ownable.shieldOwner(Z_NEW_OWNER, convert_bigint_to_Uint8Array(32, nextInstance)); + const nextInstance = + ownable.getCurrentPublicState().ownablePK_Instance + 1n; + const expOwner = ownable.shieldOwner( + Z_NEW_OWNER, + convert_bigint_to_Uint8Array(32, nextInstance), + ); expect(ownable.pendingOwner()).toEqual(expOwner); }); diff --git a/contracts/ownable/src/test/simulators/OwnablePKSimulator.ts b/contracts/ownable/src/test/simulators/OwnablePKSimulator.ts index 04934c46..9ce66143 100644 --- a/contracts/ownable/src/test/simulators/OwnablePKSimulator.ts +++ b/contracts/ownable/src/test/simulators/OwnablePKSimulator.ts @@ -45,9 +45,7 @@ export class OwnablePKSimulator * @description Initializes the mock contract. */ constructor(initOwner: ZswapCoinPublicKey, deployer: CoinPublicKey) { - this.contract = new MockOwnable( - OwnableWitnesses(), - ); + this.contract = new MockOwnable(OwnableWitnesses()); this.deployer = deployer; const { currentPrivateState, @@ -55,7 +53,7 @@ export class OwnablePKSimulator currentZswapLocalState, } = this.contract.initialState( constructorContext(OwnablePrivateState.generate(), deployer), - initOwner + initOwner, ); this.circuitContext = { currentPrivateState, @@ -98,77 +96,98 @@ export class OwnablePKSimulator } public pendingOwner(): Uint8Array { - return this.contract.impureCircuits.pendingOwner(this.circuitContext).result; + return this.contract.impureCircuits.pendingOwner(this.circuitContext) + .result; } - public transferOwnership(newOwner: ZswapCoinPublicKey, sender: CoinPublicKey): CircuitContext { + public transferOwnership( + newOwner: ZswapCoinPublicKey, + sender: CoinPublicKey, + ): CircuitContext { const res = this.contract.impureCircuits.transferOwnership( - { - ...this.circuitContext, - currentZswapLocalState: sender - ? emptyZswapLocalState(sender) - : this.circuitContext.currentZswapLocalState, - }, - newOwner, - ); - - this.circuitContext = res.context; - return this.circuitContext; + { + ...this.circuitContext, + currentZswapLocalState: sender + ? emptyZswapLocalState(sender) + : this.circuitContext.currentZswapLocalState, + }, + newOwner, + ); + + this.circuitContext = res.context; + return this.circuitContext; } - public acceptOwnership(sender: CoinPublicKey): CircuitContext { - const res = this.contract.impureCircuits.acceptOwnership( - { - ...this.circuitContext, - currentZswapLocalState: sender - ? emptyZswapLocalState(sender) - : this.circuitContext.currentZswapLocalState, - }, - ); - - this.circuitContext = res.context; - return this.circuitContext; + public acceptOwnership( + sender: CoinPublicKey, + ): CircuitContext { + const res = this.contract.impureCircuits.acceptOwnership({ + ...this.circuitContext, + currentZswapLocalState: sender + ? emptyZswapLocalState(sender) + : this.circuitContext.currentZswapLocalState, + }); + + this.circuitContext = res.context; + return this.circuitContext; } - public renounceOwnership(sender: CoinPublicKey): CircuitContext { - const res = this.contract.impureCircuits.renounceOwnership( - { - ...this.circuitContext, - currentZswapLocalState: sender - ? emptyZswapLocalState(sender) - : this.circuitContext.currentZswapLocalState, - }, - ); - - this.circuitContext = res.context; - return this.circuitContext; + public renounceOwnership( + sender: CoinPublicKey, + ): CircuitContext { + const res = this.contract.impureCircuits.renounceOwnership({ + ...this.circuitContext, + currentZswapLocalState: sender + ? emptyZswapLocalState(sender) + : this.circuitContext.currentZswapLocalState, + }); + + this.circuitContext = res.context; + return this.circuitContext; } - public assertOnlyOwner(sender: CoinPublicKey): CircuitContext { - const res = this.contract.impureCircuits.assertOnlyOwner( - { - ...this.circuitContext, - currentZswapLocalState: sender - ? emptyZswapLocalState(sender) - : this.circuitContext.currentZswapLocalState, - }, - ); - - this.circuitContext = res.context; - return this.circuitContext; + public assertOnlyOwner( + sender: CoinPublicKey, + ): CircuitContext { + const res = this.contract.impureCircuits.assertOnlyOwner({ + ...this.circuitContext, + currentZswapLocalState: sender + ? emptyZswapLocalState(sender) + : this.circuitContext.currentZswapLocalState, + }); + + this.circuitContext = res.context; + return this.circuitContext; } - public shieldOwner(ownerPK: ZswapCoinPublicKey, instance: Uint8Array): Uint8Array { - return this.contract.circuits.shieldOwner(this.circuitContext, ownerPK, instance).result + public shieldOwner( + ownerPK: ZswapCoinPublicKey, + instance: Uint8Array, + ): Uint8Array { + return this.contract.circuits.shieldOwner( + this.circuitContext, + ownerPK, + instance, + ).result; } - public _transferOwnership(newOwner: Uint8Array): CircuitContext { - this.circuitContext = this.contract.impureCircuits._transferOwnership(this.circuitContext, newOwner).context; + public _transferOwnership( + newOwner: Uint8Array, + ): CircuitContext { + this.circuitContext = this.contract.impureCircuits._transferOwnership( + this.circuitContext, + newOwner, + ).context; return this.circuitContext; } - public _proposeOwner(newOwner: ZswapCoinPublicKey): CircuitContext { - this.circuitContext = this.contract.impureCircuits._proposeOwner(this.circuitContext, newOwner).context; + public _proposeOwner( + newOwner: ZswapCoinPublicKey, + ): CircuitContext { + this.circuitContext = this.contract.impureCircuits._proposeOwner( + this.circuitContext, + newOwner, + ).context; return this.circuitContext; } } diff --git a/contracts/ownable/src/test/simulators/OwnableSimulator.ts b/contracts/ownable/src/test/simulators/OwnableSimulator.ts index 41cb3c27..5a013ff9 100644 --- a/contracts/ownable/src/test/simulators/OwnableSimulator.ts +++ b/contracts/ownable/src/test/simulators/OwnableSimulator.ts @@ -44,9 +44,7 @@ export class OwnableSimulator * @description Initializes the mock contract. */ constructor(deployer: CoinPublicKey) { - this.contract = new MockOwnable( - OwnableWitnesses(), - ); + this.contract = new MockOwnable(OwnableWitnesses()); this.deployer = deployer; const { currentPrivateState, @@ -105,15 +103,22 @@ export class OwnableSimulator } public renounceOwnership(): CircuitContext { - this.circuitContext = this.contract.impureCircuits.renounceOwnership(this.circuitContext).context; + this.circuitContext = this.contract.impureCircuits.renounceOwnership( + this.circuitContext, + ).context; return this.circuitContext; } public assertOnlyOwner(): CircuitContext { - return this.contract.impureCircuits.assertOnlyOwner(this.circuitContext).context; + return this.contract.impureCircuits.assertOnlyOwner(this.circuitContext) + .context; } - public publicKey(sk: Uint8Array, instance: Uint8Array, sender: CoinPublicKey): CircuitContext { + public publicKey( + sk: Uint8Array, + instance: Uint8Array, + sender: CoinPublicKey, + ): CircuitContext { const res = this.contract.circuits.publicKey( { ...this.circuitContext, @@ -122,15 +127,20 @@ export class OwnableSimulator : this.circuitContext.currentZswapLocalState, }, sk, - instance + instance, ); this.circuitContext = res.context; return this.circuitContext; } - public _transferOwnership(newOwner: Uint8Array): CircuitContext { - this.circuitContext = this.contract.impureCircuits._transferOwnership(this.circuitContext, newOwner).context; + public _transferOwnership( + newOwner: Uint8Array, + ): CircuitContext { + this.circuitContext = this.contract.impureCircuits._transferOwnership( + this.circuitContext, + newOwner, + ).context; return this.circuitContext; } } diff --git a/contracts/ownable/src/witnesses/OwnableWitnesses.ts b/contracts/ownable/src/witnesses/OwnableWitnesses.ts index 26350db3..80135c1e 100644 --- a/contracts/ownable/src/witnesses/OwnableWitnesses.ts +++ b/contracts/ownable/src/witnesses/OwnableWitnesses.ts @@ -1,59 +1,57 @@ import { getRandomValues } from 'node:crypto'; import { IOwnableWitnesses } from './interface'; import type { WitnessContext } from '@midnight-ntwrk/compact-runtime'; -import { - type Ledger, - } from '../artifacts/MockOwnable/contract/index.cjs'; // Combined imports +import { type Ledger } from '../artifacts/MockOwnable/contract/index.cjs'; // Combined imports /** * @description Represents the private state of an ownable contract, storing a secret key. */ export type OwnablePrivateState = { - /** @description A 32-byte secret key used for cryptographic operations. */ - secretKey: Buffer; - }; + /** @description A 32-byte secret key used for cryptographic operations. */ + secretKey: Buffer; +}; /** * @description Utility object for managing the private state of an ownable contract. */ export const OwnablePrivateState = { - /** - * @description Generates a new private state with a random secret key. - * @returns A fresh OwnablePrivateState instance. - */ - generate: (): OwnablePrivateState => { - return { secretKey: getRandomValues(Buffer.alloc(32)) }; - }, -} + /** + * @description Generates a new private state with a random secret key. + * @returns A fresh OwnablePrivateState instance. + */ + generate: (): OwnablePrivateState => { + return { secretKey: getRandomValues(Buffer.alloc(32)) }; + }, +}; /** * @description Factory function creating witness implementations for ownable operations. * @returns An object implementing the Witnesses interface for OwnablePrivateState. */ -export const OwnableWitnesses = - (): IOwnableWitnesses => ({ - /** - * @description Retrieves the secret key from the private state. - * @param context - The witness context containing the private state. - * @returns A tuple of the unchanged private state and the secret key as a Uint8Array. - */ - localSecretKey( - context: WitnessContext, - ): [OwnablePrivateState, Uint8Array] { - return [context.privateState, context.privateState.secretKey]; - }, +export const OwnableWitnesses = (): IOwnableWitnesses => ({ + /** + * @description Retrieves the secret key from the private state. + * @param context - The witness context containing the private state. + * @returns A tuple of the unchanged private state and the secret key as a Uint8Array. + */ + localSecretKey( + context: WitnessContext, + ): [OwnablePrivateState, Uint8Array] { + return [context.privateState, context.privateState.secretKey]; + }, }); -export const SetWitnessContext = - (sk: Uint8Array): IOwnableWitnesses => ({ - /** - * @description Retrieves the secret key from the private state. - * @param context - The witness context containing the private state. - * @returns A tuple of the unchanged private state and the passed `sk` as a Uint8Array. - */ - localSecretKey( - context: WitnessContext, - ): [OwnablePrivateState, Uint8Array] { - return [context.privateState, sk]; - }, +export const SetWitnessContext = ( + sk: Uint8Array, +): IOwnableWitnesses => ({ + /** + * @description Retrieves the secret key from the private state. + * @param context - The witness context containing the private state. + * @returns A tuple of the unchanged private state and the passed `sk` as a Uint8Array. + */ + localSecretKey( + context: WitnessContext, + ): [OwnablePrivateState, Uint8Array] { + return [context.privateState, sk]; + }, }); diff --git a/contracts/ownable/src/witnesses/interface.ts b/contracts/ownable/src/witnesses/interface.ts index d264ee9d..18f7635a 100644 --- a/contracts/ownable/src/witnesses/interface.ts +++ b/contracts/ownable/src/witnesses/interface.ts @@ -1,7 +1,5 @@ import type { WitnessContext } from '@midnight-ntwrk/compact-runtime'; -import { - type Ledger, - } from '../artifacts/MockOwnable/contract/index.cjs'; // Combined imports +import { type Ledger } from '../artifacts/MockOwnable/contract/index.cjs'; // Combined imports /** * @description Interface defining the witness methods for ownable operations. From 5c509387f43c4a6600b68c285de6cddb844fef1c Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 12 May 2025 01:10:48 -0500 Subject: [PATCH 041/322] fix lint --- contracts/ownable/src/test/Ownable.test.ts | 19 +------------------ contracts/ownable/src/test/OwnablePK.test.ts | 6 ++---- .../src/test/simulators/OwnablePKSimulator.ts | 6 ++---- .../src/test/simulators/OwnableSimulator.ts | 3 +-- .../ownable/src/witnesses/OwnableWitnesses.ts | 4 ++-- contracts/ownable/src/witnesses/interface.ts | 2 +- 6 files changed, 9 insertions(+), 31 deletions(-) diff --git a/contracts/ownable/src/test/Ownable.test.ts b/contracts/ownable/src/test/Ownable.test.ts index 09c27268..6ee3e8c2 100644 --- a/contracts/ownable/src/test/Ownable.test.ts +++ b/contracts/ownable/src/test/Ownable.test.ts @@ -1,8 +1,4 @@ -import { - CoinPublicKey, - convert_bigint_to_Uint8Array, - convert_Uint8Array_to_bigint, -} from '@midnight-ntwrk/compact-runtime'; +import { convert_bigint_to_Uint8Array } from '@midnight-ntwrk/compact-runtime'; import { OwnableSimulator } from './simulators/OwnableSimulator'; import * as utils from './utils/address'; @@ -10,28 +6,15 @@ const OWNER = String(Buffer.from('OWNER', 'ascii').toString('hex')).padStart( 64, '0', ); -const SPENDER = String( - Buffer.from('SPENDER', 'ascii').toString('hex'), -).padStart(64, '0'); -const UNAUTHORIZED = String( - Buffer.from('UNAUTHORIZED', 'ascii').toString('hex'), -).padStart(64, '0'); -const ZERO = String().padStart(64, '0'); -const Z_OWNER = utils.createEitherTestUser('OWNER'); -const Z_RECIPIENT = utils.createEitherTestUser('RECIPIENT'); -const Z_SPENDER = utils.createEitherTestUser('SPENDER'); -const Z_OTHER = utils.createEitherTestUser('OTHER'); const EMPTY_BYTES = utils.ZERO_KEY.left.bytes; const BAD_SECRET_KEY = convert_bigint_to_Uint8Array(32, 123456789n); let ownable: OwnableSimulator; -let caller: CoinPublicKey; let ownerSK: Uint8Array; describe('Ownable', () => { describe('initializer', () => { it('should initialize and set the caller as owner', () => { - caller = OWNER; ownable = new OwnableSimulator(OWNER); expect(ownable.owner()).not.toEqual(utils.ZERO_ADDRESS); expect(ownable.getCurrentPublicState().ownable_Instance).toEqual(1n); diff --git a/contracts/ownable/src/test/OwnablePK.test.ts b/contracts/ownable/src/test/OwnablePK.test.ts index 640b79a3..f96a0acf 100644 --- a/contracts/ownable/src/test/OwnablePK.test.ts +++ b/contracts/ownable/src/test/OwnablePK.test.ts @@ -1,7 +1,6 @@ import { - CoinPublicKey, + type CoinPublicKey, convert_bigint_to_Uint8Array, - convert_Uint8Array_to_bigint, } from '@midnight-ntwrk/compact-runtime'; import { OwnablePKSimulator } from './simulators/OwnablePKSimulator'; import * as utils from './utils/address'; @@ -24,7 +23,6 @@ const EMPTY_BYTES = utils.ZERO_KEY.left.bytes; let ownable: OwnablePKSimulator; let caller: CoinPublicKey; -let ownerSK: Uint8Array; describe('OwnablePK', () => { describe('initializer', () => { @@ -277,7 +275,7 @@ describe('OwnablePK', () => { describe('shieldOwner', () => { it.skip('should hash owner correctly', () => { const instance = convert_bigint_to_Uint8Array(32, 123n); - const expHash = ownable.shieldOwner(Z_OWNER, instance); + const _expHash = ownable.shieldOwner(Z_OWNER, instance); // TODO add matching algo in js }); }); diff --git a/contracts/ownable/src/test/simulators/OwnablePKSimulator.ts b/contracts/ownable/src/test/simulators/OwnablePKSimulator.ts index 9ce66143..7a6ba6f0 100644 --- a/contracts/ownable/src/test/simulators/OwnablePKSimulator.ts +++ b/contracts/ownable/src/test/simulators/OwnablePKSimulator.ts @@ -1,12 +1,13 @@ import { type CircuitContext, - CoinPublicKey, + type CoinPublicKey, type ContractState, QueryContext, constructorContext, emptyZswapLocalState, } from '@midnight-ntwrk/compact-runtime'; import { sampleContractAddress } from '@midnight-ntwrk/zswap'; +import type { ZswapCoinPublicKey } from '../../artifacts/MockOwnable/contract/index.cjs'; import { type Ledger, Contract as MockOwnable, @@ -15,11 +16,8 @@ import { import { OwnablePrivateState, OwnableWitnesses, - SetWitnessContext, } from '../../witnesses/OwnableWitnesses'; import type { IContractSimulator } from '../types/test'; -import { useCircuitContextSender } from '../utils/test'; -import { ZswapCoinPublicKey } from '../../artifacts/MockOwnable/contract/index.cjs'; /** * @description A simulator implementation of a contract for testing purposes. diff --git a/contracts/ownable/src/test/simulators/OwnableSimulator.ts b/contracts/ownable/src/test/simulators/OwnableSimulator.ts index 5a013ff9..29312b32 100644 --- a/contracts/ownable/src/test/simulators/OwnableSimulator.ts +++ b/contracts/ownable/src/test/simulators/OwnableSimulator.ts @@ -1,6 +1,6 @@ import { type CircuitContext, - CoinPublicKey, + type CoinPublicKey, type ContractState, QueryContext, constructorContext, @@ -18,7 +18,6 @@ import { SetWitnessContext, } from '../../witnesses/OwnableWitnesses'; import type { IContractSimulator } from '../types/test'; -import { useCircuitContextSender } from '../utils/test'; /** * @description A simulator implementation of a contract for testing purposes. diff --git a/contracts/ownable/src/witnesses/OwnableWitnesses.ts b/contracts/ownable/src/witnesses/OwnableWitnesses.ts index 80135c1e..e00ccd01 100644 --- a/contracts/ownable/src/witnesses/OwnableWitnesses.ts +++ b/contracts/ownable/src/witnesses/OwnableWitnesses.ts @@ -1,7 +1,7 @@ import { getRandomValues } from 'node:crypto'; -import { IOwnableWitnesses } from './interface'; import type { WitnessContext } from '@midnight-ntwrk/compact-runtime'; -import { type Ledger } from '../artifacts/MockOwnable/contract/index.cjs'; // Combined imports +import type { Ledger } from '../artifacts/MockOwnable/contract/index.cjs'; // Combined imports +import type { IOwnableWitnesses } from './interface'; /** * @description Represents the private state of an ownable contract, storing a secret key. diff --git a/contracts/ownable/src/witnesses/interface.ts b/contracts/ownable/src/witnesses/interface.ts index 18f7635a..af8bddf2 100644 --- a/contracts/ownable/src/witnesses/interface.ts +++ b/contracts/ownable/src/witnesses/interface.ts @@ -1,5 +1,5 @@ import type { WitnessContext } from '@midnight-ntwrk/compact-runtime'; -import { type Ledger } from '../artifacts/MockOwnable/contract/index.cjs'; // Combined imports +import type { Ledger } from '../artifacts/MockOwnable/contract/index.cjs'; // Combined imports /** * @description Interface defining the witness methods for ownable operations. From 10c92955325d65d430dd1e0f993b119fa308f9cc Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 12 May 2025 01:34:22 -0500 Subject: [PATCH 042/322] remove og ownable --- contracts/ownable/src/Ownable.compact | 74 --------- contracts/ownable/src/test/Ownable.test.ts | 111 -------------- .../src/test/mocks/MockOwnable.compact | 34 ---- .../src/test/simulators/OwnableSimulator.ts | 145 ------------------ .../ownable/src/witnesses/OwnableWitnesses.ts | 57 ------- 5 files changed, 421 deletions(-) delete mode 100644 contracts/ownable/src/Ownable.compact delete mode 100644 contracts/ownable/src/test/Ownable.test.ts delete mode 100644 contracts/ownable/src/test/mocks/MockOwnable.compact delete mode 100644 contracts/ownable/src/test/simulators/OwnableSimulator.ts delete mode 100644 contracts/ownable/src/witnesses/OwnableWitnesses.ts diff --git a/contracts/ownable/src/Ownable.compact b/contracts/ownable/src/Ownable.compact deleted file mode 100644 index e501c055..00000000 --- a/contracts/ownable/src/Ownable.compact +++ /dev/null @@ -1,74 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma language_version >= 0.14.0; - -/** - * @module Shielded Ownable module - * @description Get rekt, losers - */ -module Ownable { - import CompactStandardLibrary; - - /// Public state - export ledger _owner: Bytes<32>; - export ledger _pendingOwner: Bytes<32>; - export ledger _instance: Counter; - - /// Witnesses - witness localSecretKey(): Bytes<32>; - //witness setPendingOwner(currentOwner: Bytes<32>): Bytes<32>; - - /** - * @description Add me... - */ - export circuit initializer(): [] { - _instance.increment(1); - const initialOwner = disclose(publicKey(localSecretKey(), _instance as Field as Bytes<32>)); - _owner = initialOwner; - } - - /** - * @description Add me... - */ - export circuit owner(): Bytes<32> { - return _owner; - } - - /** - * @description Add me... - */ - //export circuit proposeNewOwner(): Bytes<32> { - // - //} - - /** - * @description Add me... - */ - export circuit renounceOwnership(): [] { - assertOnlyOwner(); - const zeroAddress = burn_address().left.bytes; - _transferOwnership(zeroAddress); - } - - /** - * @description Add me... - */ - export circuit assertOnlyOwner(): [] { - assert _owner == publicKey(localSecretKey(), _instance as Field as Bytes<32>) "Ownable: not owner"; - } - - /** - * @description Add me... - */ - export circuit publicKey(sk: Bytes<32>, instance: Bytes<32>): Bytes<32> { - return persistent_hash>>([pad(32, "ownable:pk:"), instance, sk]); - } - - /** - * @description Add me... - */ - export circuit _transferOwnership(newOwner: Bytes<32>): [] { - _instance.increment(1); - _owner = newOwner; - } -} diff --git a/contracts/ownable/src/test/Ownable.test.ts b/contracts/ownable/src/test/Ownable.test.ts deleted file mode 100644 index 6ee3e8c2..00000000 --- a/contracts/ownable/src/test/Ownable.test.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { convert_bigint_to_Uint8Array } from '@midnight-ntwrk/compact-runtime'; -import { OwnableSimulator } from './simulators/OwnableSimulator'; -import * as utils from './utils/address'; - -const OWNER = String(Buffer.from('OWNER', 'ascii').toString('hex')).padStart( - 64, - '0', -); -const EMPTY_BYTES = utils.ZERO_KEY.left.bytes; -const BAD_SECRET_KEY = convert_bigint_to_Uint8Array(32, 123456789n); - -let ownable: OwnableSimulator; -let ownerSK: Uint8Array; - -describe('Ownable', () => { - describe('initializer', () => { - it('should initialize and set the caller as owner', () => { - ownable = new OwnableSimulator(OWNER); - expect(ownable.owner()).not.toEqual(utils.ZERO_ADDRESS); - expect(ownable.getCurrentPublicState().ownable_Instance).toEqual(1n); - }); - }); - - beforeEach(() => { - ownable = new OwnableSimulator(OWNER); - ownerSK = ownable.getCurrentPrivateState().secretKey; - }); - - describe('assertOnlyOwner', () => { - it('should allow owner to call', () => { - ownable.assertOnlyOwner(); - }); - - it('should fail with unauthorized caller', () => { - // Change secret key in witness context - ownable.setWitnessContext(BAD_SECRET_KEY); - - expect(() => { - ownable.assertOnlyOwner(); - }).toThrow('Ownable: not owner'); - }); - - it('should handle owner → not-owner → owner calls', () => { - // Owner - ownable.assertOnlyOwner(); - - // Not owner - ownable.setWitnessContext(BAD_SECRET_KEY); - expect(() => { - ownable.assertOnlyOwner(); - }).toThrow('Ownable: not owner'); - - // Owner - ownable.setWitnessContext(ownerSK); - ownable.assertOnlyOwner(); - }); - }); - - describe('renounceOwnership', () => { - it('should renounce ownership', () => { - ownable.renounceOwnership(); - expect(ownable.owner()).toEqual(EMPTY_BYTES); - - // Check that original owner can no longer call protected circuits - expect(() => { - ownable.assertOnlyOwner(); - }).toThrow('Ownable: not owner'); - }); - - it('should not renounce ownership from non-owner', () => { - ownable.setWitnessContext(BAD_SECRET_KEY); - expect(() => { - ownable.renounceOwnership(); - }).toThrow('Ownable: not owner'); - }); - - it('should not renounce ownership from non-owner', () => { - ownable.setWitnessContext(BAD_SECRET_KEY); - expect(() => { - ownable.renounceOwnership(); - }).toThrow('Ownable: not owner'); - }); - - it('should not renounce ownership more than once', () => { - ownable.renounceOwnership(); - - expect(() => { - ownable.renounceOwnership(); - }).toThrow('Ownable: not owner'); - }); - }); -}); - -//const sk = ownable.getCurrentPrivateState().secretKey; -//console.log("skkkkkk", sk); -// -//ownable.setWitnessContext(BAD_SECRET_KEY); -////ownable.owner2(); -// -//expect(ownable.owner()).not.toEqual(utils.ZERO_ADDRESS); -//console.log("ownerrrr after", ownable.owner()); -// -//expect(() => { -// ownable.assertOnlyOwner() -//}).toThrow('Ownable: not owner'); -// -//ownable.setWitnessContext(sk); -//expect(() => { -// ownable.assertOnlyOwner() -//}).toThrow('Ownable: not owner'); -// diff --git a/contracts/ownable/src/test/mocks/MockOwnable.compact b/contracts/ownable/src/test/mocks/MockOwnable.compact deleted file mode 100644 index d550c7dc..00000000 --- a/contracts/ownable/src/test/mocks/MockOwnable.compact +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma language_version >= 0.14.0; - -import CompactStandardLibrary; - -import "../../Ownable" prefix Ownable_; - -export { ZswapCoinPublicKey, ContractAddress, Either, Maybe }; -export { Ownable__owner, Ownable__pendingOwner, Ownable__instance }; - -constructor() { - Ownable_initializer(); -} - -export circuit owner(): Bytes<32> { - return Ownable_owner(); - } - -export circuit renounceOwnership(): [] { - return Ownable_renounceOwnership(); -} - -export circuit assertOnlyOwner(): [] { - return Ownable_assertOnlyOwner(); -} - -export circuit publicKey(sk: Bytes<32>, instance: Bytes<32>): Bytes<32> { - return Ownable_publicKey(sk, instance); -} - -export circuit _transferOwnership(newOwner: Bytes<32>): [] { - return Ownable__transferOwnership(newOwner); -} diff --git a/contracts/ownable/src/test/simulators/OwnableSimulator.ts b/contracts/ownable/src/test/simulators/OwnableSimulator.ts deleted file mode 100644 index 29312b32..00000000 --- a/contracts/ownable/src/test/simulators/OwnableSimulator.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { - type CircuitContext, - type CoinPublicKey, - type ContractState, - QueryContext, - constructorContext, - emptyZswapLocalState, -} from '@midnight-ntwrk/compact-runtime'; -import { sampleContractAddress } from '@midnight-ntwrk/zswap'; -import { - type Ledger, - Contract as MockOwnable, - ledger, -} from '../../artifacts/MockOwnable/contract/index.cjs'; // Combined imports -import { - OwnablePrivateState, - OwnableWitnesses, - SetWitnessContext, -} from '../../witnesses/OwnableWitnesses'; -import type { IContractSimulator } from '../types/test'; - -/** - * @description A simulator implementation of a contract for testing purposes. - * @template P - The private state type, fixed to OwnablePrivateState. - * @template L - The ledger type, fixed to Contract.Ledger. - */ -export class OwnableSimulator - implements IContractSimulator -{ - /** @description The underlying contract instance managing contract logic. */ - readonly contract: MockOwnable; - - /** @description The deployed address of the contract. */ - readonly contractAddress: string; - - /** @description The deployer address of the contract. */ - readonly deployer: CoinPublicKey; - - /** @description The current circuit context, updated by contract operations. */ - circuitContext: CircuitContext; - - /** - * @description Initializes the mock contract. - */ - constructor(deployer: CoinPublicKey) { - this.contract = new MockOwnable(OwnableWitnesses()); - this.deployer = deployer; - const { - currentPrivateState, - currentContractState, - currentZswapLocalState, - } = this.contract.initialState( - constructorContext(OwnablePrivateState.generate(), deployer), - ); - this.circuitContext = { - currentPrivateState, - currentZswapLocalState, - originalState: currentContractState, - transactionContext: new QueryContext( - currentContractState.data, - sampleContractAddress(), - ), - }; - this.contractAddress = this.circuitContext.transactionContext.address; - } - - /** - * @description Retrieves the current public ledger state of the contract. - * @returns The ledger state as defined by the contract. - */ - public getCurrentPublicState(): Ledger { - return ledger(this.circuitContext.transactionContext.state); - } - - /** - * @description Retrieves the current private state of the contract. - * @returns The private state of type OwnablePrivateState. - */ - public getCurrentPrivateState(): OwnablePrivateState { - return this.circuitContext.currentPrivateState; - } - - /** - * @description Retrieves the current contract state. - * @returns The contract state object. - */ - public getCurrentContractState(): ContractState { - return this.circuitContext.originalState; - } - - /** - * @description Changes the witness context by setting `sk` - * as the `secretKey`. - * @returns None. - */ - public setWitnessContext(sk: Uint8Array) { - this.contract.witnesses = SetWitnessContext(sk); - } - - public owner(): Uint8Array { - return this.contract.impureCircuits.owner(this.circuitContext).result; - } - - public renounceOwnership(): CircuitContext { - this.circuitContext = this.contract.impureCircuits.renounceOwnership( - this.circuitContext, - ).context; - return this.circuitContext; - } - - public assertOnlyOwner(): CircuitContext { - return this.contract.impureCircuits.assertOnlyOwner(this.circuitContext) - .context; - } - - public publicKey( - sk: Uint8Array, - instance: Uint8Array, - sender: CoinPublicKey, - ): CircuitContext { - const res = this.contract.circuits.publicKey( - { - ...this.circuitContext, - currentZswapLocalState: sender - ? emptyZswapLocalState(sender) - : this.circuitContext.currentZswapLocalState, - }, - sk, - instance, - ); - - this.circuitContext = res.context; - return this.circuitContext; - } - - public _transferOwnership( - newOwner: Uint8Array, - ): CircuitContext { - this.circuitContext = this.contract.impureCircuits._transferOwnership( - this.circuitContext, - newOwner, - ).context; - return this.circuitContext; - } -} diff --git a/contracts/ownable/src/witnesses/OwnableWitnesses.ts b/contracts/ownable/src/witnesses/OwnableWitnesses.ts deleted file mode 100644 index e00ccd01..00000000 --- a/contracts/ownable/src/witnesses/OwnableWitnesses.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { getRandomValues } from 'node:crypto'; -import type { WitnessContext } from '@midnight-ntwrk/compact-runtime'; -import type { Ledger } from '../artifacts/MockOwnable/contract/index.cjs'; // Combined imports -import type { IOwnableWitnesses } from './interface'; - -/** - * @description Represents the private state of an ownable contract, storing a secret key. - */ -export type OwnablePrivateState = { - /** @description A 32-byte secret key used for cryptographic operations. */ - secretKey: Buffer; -}; - -/** - * @description Utility object for managing the private state of an ownable contract. - */ -export const OwnablePrivateState = { - /** - * @description Generates a new private state with a random secret key. - * @returns A fresh OwnablePrivateState instance. - */ - generate: (): OwnablePrivateState => { - return { secretKey: getRandomValues(Buffer.alloc(32)) }; - }, -}; - -/** - * @description Factory function creating witness implementations for ownable operations. - * @returns An object implementing the Witnesses interface for OwnablePrivateState. - */ -export const OwnableWitnesses = (): IOwnableWitnesses => ({ - /** - * @description Retrieves the secret key from the private state. - * @param context - The witness context containing the private state. - * @returns A tuple of the unchanged private state and the secret key as a Uint8Array. - */ - localSecretKey( - context: WitnessContext, - ): [OwnablePrivateState, Uint8Array] { - return [context.privateState, context.privateState.secretKey]; - }, -}); - -export const SetWitnessContext = ( - sk: Uint8Array, -): IOwnableWitnesses => ({ - /** - * @description Retrieves the secret key from the private state. - * @param context - The witness context containing the private state. - * @returns A tuple of the unchanged private state and the passed `sk` as a Uint8Array. - */ - localSecretKey( - context: WitnessContext, - ): [OwnablePrivateState, Uint8Array] { - return [context.privateState, sk]; - }, -}); From cba873b97d2c0c295d56778287468f32a92dabd4 Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 12 May 2025 01:34:34 -0500 Subject: [PATCH 043/322] fix name, fmt, lint --- .../src/test/simulators/OwnablePKSimulator.ts | 37 +++++++++---------- .../src/witnesses/OwnablePKWitnesses.ts | 3 ++ 2 files changed, 20 insertions(+), 20 deletions(-) create mode 100644 contracts/ownable/src/witnesses/OwnablePKWitnesses.ts diff --git a/contracts/ownable/src/test/simulators/OwnablePKSimulator.ts b/contracts/ownable/src/test/simulators/OwnablePKSimulator.ts index 7a6ba6f0..9a3deecc 100644 --- a/contracts/ownable/src/test/simulators/OwnablePKSimulator.ts +++ b/contracts/ownable/src/test/simulators/OwnablePKSimulator.ts @@ -14,21 +14,21 @@ import { ledger, } from '../../artifacts/MockOwnablePK/contract/index.cjs'; // Combined imports import { - OwnablePrivateState, - OwnableWitnesses, -} from '../../witnesses/OwnableWitnesses'; + type OwnablePKPrivateState, + OwnablePKWitnesses, +} from '../../witnesses/OwnablePKWitnesses'; import type { IContractSimulator } from '../types/test'; /** * @description A simulator implementation of a contract for testing purposes. - * @template P - The private state type, fixed to OwnablePrivateState. + * @template P - The private state type, fixed to OwnablePKPrivateState. * @template L - The ledger type, fixed to Contract.Ledger. */ export class OwnablePKSimulator - implements IContractSimulator + implements IContractSimulator { /** @description The underlying contract instance managing contract logic. */ - readonly contract: MockOwnable; + readonly contract: MockOwnable; /** @description The deployed address of the contract. */ readonly contractAddress: string; @@ -37,22 +37,19 @@ export class OwnablePKSimulator readonly deployer: CoinPublicKey; /** @description The current circuit context, updated by contract operations. */ - circuitContext: CircuitContext; + circuitContext: CircuitContext; /** * @description Initializes the mock contract. */ constructor(initOwner: ZswapCoinPublicKey, deployer: CoinPublicKey) { - this.contract = new MockOwnable(OwnableWitnesses()); + this.contract = new MockOwnable(OwnablePKWitnesses); this.deployer = deployer; const { currentPrivateState, currentContractState, currentZswapLocalState, - } = this.contract.initialState( - constructorContext(OwnablePrivateState.generate(), deployer), - initOwner, - ); + } = this.contract.initialState(constructorContext({}, deployer), initOwner); this.circuitContext = { currentPrivateState, currentZswapLocalState, @@ -75,9 +72,9 @@ export class OwnablePKSimulator /** * @description Retrieves the current private state of the contract. - * @returns The private state of type OwnablePrivateState. + * @returns The private state of type OwnablePKPrivateState. */ - public getCurrentPrivateState(): OwnablePrivateState { + public getCurrentPrivateState(): OwnablePKPrivateState { return this.circuitContext.currentPrivateState; } @@ -101,7 +98,7 @@ export class OwnablePKSimulator public transferOwnership( newOwner: ZswapCoinPublicKey, sender: CoinPublicKey, - ): CircuitContext { + ): CircuitContext { const res = this.contract.impureCircuits.transferOwnership( { ...this.circuitContext, @@ -118,7 +115,7 @@ export class OwnablePKSimulator public acceptOwnership( sender: CoinPublicKey, - ): CircuitContext { + ): CircuitContext { const res = this.contract.impureCircuits.acceptOwnership({ ...this.circuitContext, currentZswapLocalState: sender @@ -132,7 +129,7 @@ export class OwnablePKSimulator public renounceOwnership( sender: CoinPublicKey, - ): CircuitContext { + ): CircuitContext { const res = this.contract.impureCircuits.renounceOwnership({ ...this.circuitContext, currentZswapLocalState: sender @@ -146,7 +143,7 @@ export class OwnablePKSimulator public assertOnlyOwner( sender: CoinPublicKey, - ): CircuitContext { + ): CircuitContext { const res = this.contract.impureCircuits.assertOnlyOwner({ ...this.circuitContext, currentZswapLocalState: sender @@ -171,7 +168,7 @@ export class OwnablePKSimulator public _transferOwnership( newOwner: Uint8Array, - ): CircuitContext { + ): CircuitContext { this.circuitContext = this.contract.impureCircuits._transferOwnership( this.circuitContext, newOwner, @@ -181,7 +178,7 @@ export class OwnablePKSimulator public _proposeOwner( newOwner: ZswapCoinPublicKey, - ): CircuitContext { + ): CircuitContext { this.circuitContext = this.contract.impureCircuits._proposeOwner( this.circuitContext, newOwner, diff --git a/contracts/ownable/src/witnesses/OwnablePKWitnesses.ts b/contracts/ownable/src/witnesses/OwnablePKWitnesses.ts new file mode 100644 index 00000000..4976e327 --- /dev/null +++ b/contracts/ownable/src/witnesses/OwnablePKWitnesses.ts @@ -0,0 +1,3 @@ +// This is how we type an empty object. +export type OwnablePKPrivateState = Record; +export const OwnablePKWitnesses = {}; From 82bb3f701d56b7e531e06748a57772b611973d24 Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 14 May 2025 18:50:38 -0500 Subject: [PATCH 044/322] add documentation for ownablePK --- contracts/ownable/src/OwnablePK.compact | 88 +++++++++++++++++++++---- 1 file changed, 77 insertions(+), 11 deletions(-) diff --git a/contracts/ownable/src/OwnablePK.compact b/contracts/ownable/src/OwnablePK.compact index a9eea15d..1752529e 100644 --- a/contracts/ownable/src/OwnablePK.compact +++ b/contracts/ownable/src/OwnablePK.compact @@ -4,7 +4,27 @@ pragma language_version >= 0.14.0; /** * @module Shielded Ownable Public Key module - * @description Get rekt, losers + * @description The OwnablePK module provides a basic access control mechanism, + * where there is an account (an owner) that can be granted exclusive access + * to specific circuits. + * + * The initial owner can be set by using the `initializer` circuit during + * construction. The owner's public key will be obfuscated in the ledger + * (thus shielded) by the `shieldOwner` circuit. + * + * This module enforces a two-step ownership transfer mechanism. The mechanism + * flow starts with the current owner calling `transferOwnership` and passing + * the new owner's ZswapCoinPublicKey. The proposed owner's key is obfuscated + * similarly via the `shieldOwner` circuit. After the owner proposes the new + * owner, the new owner must accept ownership by calling `acceptOwnership`. + * This circuit validates that the caller is the proposed owner. Thereafter, + * the new owner may call `assertOnlyOwner` circuits. + * + * The reason this module enforces the two-step mechanism is for safety. + * If the owner transferred ownership to the wrong pubkey without the mechanism, + * it's likely that the ownership privileges will be lost for the contract forever. + * With the two-step mechanism, the current owner can overwrite the pending + * owner by calling `transferOwnership` with a different pubkey. */ module OwnablePK { import CompactStandardLibrary; @@ -15,7 +35,10 @@ module OwnablePK { export ledger _instance: Counter; /** - * @description Add me... + * @description Initializes the contract by setting `initOwner` as the + * (shielded) contract owner. + * + * @returns {[]} - None. */ export circuit initializer(initOwner: ZswapCoinPublicKey): [] { assert initOwner != burn_address().left "OwnablePK: new owner cannot be zero"; @@ -25,21 +48,31 @@ module OwnablePK { } /** - * @description Add me... + * @description Returns the shielded owner. + * + * @returns {Bytes<32>} - The shielded owner. */ export circuit owner(): Bytes<32> { return _owner; } /** - * @description Add me... + * @description Returns the shielded pending owner. + * + * @returns {Bytes<32>} - The shielded proposed owner. */ export circuit pendingOwner(): Bytes<32> { return _pendingOwner; } /** - * @description Add me... + * @description Initiates the two-step ownership transfer to `newOwner`. + * + * Requirements: + * + * - The caller must be the current contract owner. + * + * @returns {[]} - None. */ export circuit transferOwnership(newOwner: ZswapCoinPublicKey): [] { assertOnlyOwner(); @@ -47,7 +80,14 @@ module OwnablePK { } /** - * @description Add me... + * @description Finishes the two-step ownership transfer process by accepting + * the ownership. Can only be called by the pending owner. + * + * Requirements: + * + * - The caller is the pending owner. + * + * @returns {[]} - None. */ export circuit acceptOwnership(): [] { const caller = own_public_key(); @@ -60,7 +100,15 @@ module OwnablePK { } /** - * @description Add me... + * @description Leaves the contract without an owner. It will not be + * possible to call `assertOnlyOnwer` circuits anymore. Can only be + * called by the current owner. + * + * Requirements: + * + * - The caller is the contract owner. + * + * @returns {[]} - None. */ export circuit renounceOwnership(): [] { assertOnlyOwner(); @@ -68,7 +116,10 @@ module OwnablePK { } /** - * @description Add me... + * @description Throws if called by any account other than the owner. + * Use this to restrict access to sensitive circuits. + * + * @returns {[]} - None. */ export circuit assertOnlyOwner(): [] { const caller = own_public_key(); @@ -76,14 +127,22 @@ module OwnablePK { } /** - * @description Add me... + * @description Obfuscates the `ownerPK` be hashing it with a domain separator and + * the passed `instance`. + * + * @returns {Bytes<32>} - The shielded hash of the owner and instance. */ export circuit shieldOwner(ownerPK: ZswapCoinPublicKey, instance: Bytes<32>): Bytes<32> { return persistent_hash>>([pad(32, "OwnablePK:shield:"), instance, ownerPK.bytes]); } /** - * @description Add me... + * @description Internal circuit that transfers ownership of the contract to `newOwner`. + * This circuit does not have access control and thus should not be exposed. + * + * Be careful with this circuit. `newOwner` will be stored in the ledger as it's + * passed meaning that `newOwner` must be shielded via `shieldOwner` beforehand. + * Maybe include `shieldOwner()` in logic so it's difficult to misuse? */ export circuit _transferOwnership(newOwner: Bytes<32>): [] { _pendingOwner = default>; @@ -92,7 +151,14 @@ module OwnablePK { } /** - * @description Add me... + * @description Internal circuit that sets the pending owner. + * This circuit shields `newOwner` internally. + * + * Requirements: + * + * - `newOwner` can not be the zero address. + * + * @returns {[]} - None. */ export circuit _proposeOwner(newOwner: ZswapCoinPublicKey): [] { assert newOwner != burn_address().left "OwnablePK: new owner cannot be zero"; From 3ce5eec204634d76a8f7b6af8b6ef3f81c6668e6 Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 14 May 2025 22:09:03 -0500 Subject: [PATCH 045/322] add comments to simulator --- .../src/test/simulators/OwnablePKSimulator.ts | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/contracts/ownable/src/test/simulators/OwnablePKSimulator.ts b/contracts/ownable/src/test/simulators/OwnablePKSimulator.ts index 349be6e3..5d5dfc29 100644 --- a/contracts/ownable/src/test/simulators/OwnablePKSimulator.ts +++ b/contracts/ownable/src/test/simulators/OwnablePKSimulator.ts @@ -86,15 +86,26 @@ export class OwnablePKSimulator return this.circuitContext.originalState; } + /** + * @description Returns the shielded owner. + * @returns The shielded owner. + */ public owner(): Uint8Array { return this.contract.impureCircuits.owner(this.circuitContext).result; } + /** + * @description Returns the shielded pending owner. + * @returns The shielded proposed owner. + */ public pendingOwner(): Uint8Array { return this.contract.impureCircuits.pendingOwner(this.circuitContext) .result; } + /** + * @description Initiates the two-step ownership transfer to `newOwner`. + */ public transferOwnership( newOwner: ZswapCoinPublicKey, sender: CoinPublicKey, @@ -113,6 +124,10 @@ export class OwnablePKSimulator return this.circuitContext; } + /** + * @description Finishes the two-step ownership transfer process by accepting + * the ownership. Can only be called by the pending owner. + */ public acceptOwnership( sender: CoinPublicKey, ): CircuitContext { @@ -127,6 +142,11 @@ export class OwnablePKSimulator return this.circuitContext; } + /** + * @description Leaves the contract without an owner. It will not be + * possible to call `assertOnlyOnwer` circuits anymore. Can only be + * called by the current owner. + */ public renounceOwnership( sender: CoinPublicKey, ): CircuitContext { @@ -141,6 +161,10 @@ export class OwnablePKSimulator return this.circuitContext; } + /** + * @description Throws if called by any account other than the owner. + * Use this to restrict access to sensitive circuits. + */ public assertOnlyOwner( sender: CoinPublicKey, ): CircuitContext { @@ -155,6 +179,11 @@ export class OwnablePKSimulator return this.circuitContext; } + /** + * @description Obfuscates the `ownerPK` be hashing it with a domain separator and + * the passed `instance`. + * @returns The shielded hash of the owner and instance. + */ public shieldOwner( ownerPK: ZswapCoinPublicKey, instance: Uint8Array, @@ -166,6 +195,10 @@ export class OwnablePKSimulator ).result; } + + /** + * @description Internal circuit that transfers ownership of the contract to `newOwner`. + */ public _transferOwnership( newOwner: Uint8Array, ): CircuitContext { @@ -176,6 +209,9 @@ export class OwnablePKSimulator return this.circuitContext; } + /** + * @description Internal circuit that sets the pending owner. + */ public _proposeOwner( newOwner: ZswapCoinPublicKey, ): CircuitContext { From d4f36f5446c47886568e725cdf36b1d12ce9809b Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 15 May 2025 01:24:40 -0500 Subject: [PATCH 046/322] allow ownership proposals to zero (to cancel) --- contracts/ownable/src/OwnablePK.compact | 22 ++++++++++++-------- contracts/ownable/src/test/OwnablePK.test.ts | 18 +++++++++------- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/contracts/ownable/src/OwnablePK.compact b/contracts/ownable/src/OwnablePK.compact index 1752529e..1071f3e0 100644 --- a/contracts/ownable/src/OwnablePK.compact +++ b/contracts/ownable/src/OwnablePK.compact @@ -24,7 +24,8 @@ pragma language_version >= 0.14.0; * If the owner transferred ownership to the wrong pubkey without the mechanism, * it's likely that the ownership privileges will be lost for the contract forever. * With the two-step mechanism, the current owner can overwrite the pending - * owner by calling `transferOwnership` with a different pubkey. + * owner by calling `transferOwnership` with a different pubkey or passing + * zero to cancel the transfer. */ module OwnablePK { import CompactStandardLibrary; @@ -67,6 +68,8 @@ module OwnablePK { /** * @description Initiates the two-step ownership transfer to `newOwner`. + * To cancel an ownership transfer, the current owner can call this circuit + * and pass zero as `newOwner`. * * Requirements: * @@ -152,17 +155,18 @@ module OwnablePK { /** * @description Internal circuit that sets the pending owner. - * This circuit shields `newOwner` internally. - * - * Requirements: - * - * - `newOwner` can not be the zero address. + * Passing `newOwner` as zero cancels the two-step ownership + * transfer. Otherwise, this circuit shields `newOwner` and + * sets it in the ledger. * * @returns {[]} - None. */ export circuit _proposeOwner(newOwner: ZswapCoinPublicKey): [] { - assert newOwner != burn_address().left "OwnablePK: new owner cannot be zero"; - const nextInstance = _instance + 1 as Field as Bytes<32>; - _pendingOwner = shieldOwner(newOwner, nextInstance); + if (newOwner == burn_address().left) { + _pendingOwner = pad(32, ""); + } else { + const nextInstance = _instance + 1 as Field as Bytes<32>; + _pendingOwner = shieldOwner(newOwner, nextInstance); + } } } diff --git a/contracts/ownable/src/test/OwnablePK.test.ts b/contracts/ownable/src/test/OwnablePK.test.ts index f96a0acf..dbfad780 100644 --- a/contracts/ownable/src/test/OwnablePK.test.ts +++ b/contracts/ownable/src/test/OwnablePK.test.ts @@ -113,12 +113,14 @@ describe('OwnablePK', () => { expect(ownable.owner()).toEqual(expOwner); }); - it('should not transfer zero as owner', () => { + it('should cancel two-step transfer', () => { caller = OWNER; - expect(() => { - ownable.transferOwnership(Z_ZERO, caller); - }).toThrow('OwnablePK: new owner cannot be zero'); + // Start transfer process + ownable.transferOwnership(Z_NEW_OWNER, caller); + // Cancel transfer by transferring to zero + ownable.transferOwnership(Z_ZERO, caller); + expect(ownable.pendingOwner()).toEqual(Z_ZERO.bytes); }); it('should not transfer owner from unauthorized caller', () => { @@ -318,10 +320,10 @@ describe('OwnablePK', () => { expect(ownable.pendingOwner()).toEqual(expOwner); }); - it('should not propose zero as owner', () => { - expect(() => { - ownable._proposeOwner(utils.ZERO_KEY.left); - }).toThrow('OwnablePK: new owner cannot be zero'); + it('should propose owner and cancel', () => { + ownable._proposeOwner(Z_NEW_OWNER); + ownable._proposeOwner(utils.ZERO_KEY.left); + expect(ownable.pendingOwner()).toEqual(Z_ZERO.bytes); }); }); }); From 5741be712ebfff22ad9001b6e54a8f85768585f0 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 15 May 2025 01:32:50 -0500 Subject: [PATCH 047/322] fix fmt --- contracts/ownable/src/OwnablePK.compact | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/ownable/src/OwnablePK.compact b/contracts/ownable/src/OwnablePK.compact index 1071f3e0..7697c756 100644 --- a/contracts/ownable/src/OwnablePK.compact +++ b/contracts/ownable/src/OwnablePK.compact @@ -30,7 +30,7 @@ pragma language_version >= 0.14.0; module OwnablePK { import CompactStandardLibrary; - /// Public state + /** Public state */ export ledger _owner: Bytes<32>; export ledger _pendingOwner: Bytes<32>; export ledger _instance: Counter; From 7120d98786d4ec5f153e5bde095660c127b3819d Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 30 Jun 2025 15:35:40 -0300 Subject: [PATCH 048/322] migrate ownable to vitest, fix package, bump compact version --- contracts/ownable/jest.config.ts | 16 --------------- contracts/ownable/js-resolver.cjs | 20 ------------------- contracts/ownable/package.json | 20 ++++++++----------- contracts/ownable/src/OwnablePK.compact | 2 +- contracts/ownable/src/test/OwnablePK.test.ts | 1 + .../src/test/mocks/MockOwnablePK.compact | 2 +- contracts/ownable/vitest.config.ts | 10 ++++++++++ yarn.lock | 7 +++---- 8 files changed, 24 insertions(+), 54 deletions(-) delete mode 100644 contracts/ownable/jest.config.ts delete mode 100644 contracts/ownable/js-resolver.cjs create mode 100644 contracts/ownable/vitest.config.ts diff --git a/contracts/ownable/jest.config.ts b/contracts/ownable/jest.config.ts deleted file mode 100644 index bde5bde1..00000000 --- a/contracts/ownable/jest.config.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { Config } from '@jest/types'; - -const config: Config.InitialOptions = { - preset: 'ts-jest/presets/default-esm', - testEnvironment: 'node', - verbose: true, - roots: [''], - modulePaths: [''], - passWithNoTests: false, - testMatch: ['**/*.test.ts'], - extensionsToTreatAsEsm: ['.ts'], - collectCoverage: false, - resolver: '/js-resolver.cjs', -}; - -export default config; diff --git a/contracts/ownable/js-resolver.cjs b/contracts/ownable/js-resolver.cjs deleted file mode 100644 index 19b6f50c..00000000 --- a/contracts/ownable/js-resolver.cjs +++ /dev/null @@ -1,20 +0,0 @@ -const jsResolver = (path, options) => { - const jsExtRegex = /\.js$/i; - const resolver = options.defaultResolver; - if ( - jsExtRegex.test(path) && - !options.basedir.includes('node_modules') && - !path.includes('node_modules') - ) { - const newPath = path.replace(jsExtRegex, '.ts'); - try { - return resolver(newPath, options); - } catch { - // use default resolver - } - } - - return resolver(path, options); -}; - -module.exports = jsResolver; diff --git a/contracts/ownable/package.json b/contracts/ownable/package.json index d1016a6a..73f59507 100644 --- a/contracts/ownable/package.json +++ b/contracts/ownable/package.json @@ -1,5 +1,6 @@ { "name": "@openzeppelin-midnight/ownable", + "private": true, "type": "module", "main": "dist/index.js", "module": "dist/index.js", @@ -13,24 +14,19 @@ } }, "scripts": { - "compact": "npx compact-compiler", - "build": "npx compact-builder && tsc", - "test": "NODE_OPTIONS=--experimental-vm-modules jest", + "compact": "compact-compiler", + "build": "compact-builder && tsc", + "test": "vitest run", "types": "tsc -p tsconfig.json --noEmit", - "fmt": "biome format", - "fmt:fix": "biome format --write", - "lint": "biome lint", - "lint:fix": "biome check --write", "clean": "git clean -fXd" }, "dependencies": { "@openzeppelin-midnight/compact": "workspace:^" }, "devDependencies": { - "@biomejs/biome": "1.9.4", - "@types/jest": "^29.5.6", - "@types/node": "^18.18.6", - "jest": "^29.7.0", - "typescript": "^5.2.2" + "@types/node": "22.14.0", + "ts-node": "^10.9.2", + "typescript": "^5.2.2", + "vitest": "^3.1.3" } } diff --git a/contracts/ownable/src/OwnablePK.compact b/contracts/ownable/src/OwnablePK.compact index 7697c756..3c4ab389 100644 --- a/contracts/ownable/src/OwnablePK.compact +++ b/contracts/ownable/src/OwnablePK.compact @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma language_version >= 0.14.0; +pragma language_version >= 0.15.0; /** * @module Shielded Ownable Public Key module diff --git a/contracts/ownable/src/test/OwnablePK.test.ts b/contracts/ownable/src/test/OwnablePK.test.ts index dbfad780..8e0f2fa1 100644 --- a/contracts/ownable/src/test/OwnablePK.test.ts +++ b/contracts/ownable/src/test/OwnablePK.test.ts @@ -4,6 +4,7 @@ import { } from '@midnight-ntwrk/compact-runtime'; import { OwnablePKSimulator } from './simulators/OwnablePKSimulator'; import * as utils from './utils/address'; +import { beforeEach, describe, expect, it } from 'vitest'; const OWNER = String(Buffer.from('OWNER', 'ascii').toString('hex')).padStart( 64, diff --git a/contracts/ownable/src/test/mocks/MockOwnablePK.compact b/contracts/ownable/src/test/mocks/MockOwnablePK.compact index 1dcc9de1..85ebcc76 100644 --- a/contracts/ownable/src/test/mocks/MockOwnablePK.compact +++ b/contracts/ownable/src/test/mocks/MockOwnablePK.compact @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma language_version >= 0.14.0; +pragma language_version >= 0.15.0; import CompactStandardLibrary; import "../../OwnablePK" prefix OwnablePK_; diff --git a/contracts/ownable/vitest.config.ts b/contracts/ownable/vitest.config.ts new file mode 100644 index 00000000..785b792e --- /dev/null +++ b/contracts/ownable/vitest.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + include: ['src/test/**/*.test.ts'], + reporters: 'verbose', + }, +}); diff --git a/yarn.lock b/yarn.lock index ec5144d1..605724e7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -432,12 +432,11 @@ __metadata: version: 0.0.0-use.local resolution: "@openzeppelin-midnight/ownable@workspace:contracts/ownable" dependencies: - "@biomejs/biome": "npm:1.9.4" "@openzeppelin-midnight/compact": "workspace:^" - "@types/jest": "npm:^29.5.6" - "@types/node": "npm:^18.18.6" - jest: "npm:^29.7.0" + "@types/node": "npm:22.14.0" + ts-node: "npm:^10.9.2" typescript: "npm:^5.2.2" + vitest: "npm:^3.1.3" languageName: unknown linkType: soft From 5a0135af9757f2ea5fa1d10d7b7149a2242de989 Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 30 Jun 2025 15:36:09 -0300 Subject: [PATCH 049/322] fix fmt --- contracts/ownable/src/test/OwnablePK.test.ts | 2 +- contracts/ownable/src/test/simulators/OwnablePKSimulator.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/contracts/ownable/src/test/OwnablePK.test.ts b/contracts/ownable/src/test/OwnablePK.test.ts index 8e0f2fa1..1d8cfcb4 100644 --- a/contracts/ownable/src/test/OwnablePK.test.ts +++ b/contracts/ownable/src/test/OwnablePK.test.ts @@ -2,9 +2,9 @@ import { type CoinPublicKey, convert_bigint_to_Uint8Array, } from '@midnight-ntwrk/compact-runtime'; +import { beforeEach, describe, expect, it } from 'vitest'; import { OwnablePKSimulator } from './simulators/OwnablePKSimulator'; import * as utils from './utils/address'; -import { beforeEach, describe, expect, it } from 'vitest'; const OWNER = String(Buffer.from('OWNER', 'ascii').toString('hex')).padStart( 64, diff --git a/contracts/ownable/src/test/simulators/OwnablePKSimulator.ts b/contracts/ownable/src/test/simulators/OwnablePKSimulator.ts index 5d5dfc29..dff1a37b 100644 --- a/contracts/ownable/src/test/simulators/OwnablePKSimulator.ts +++ b/contracts/ownable/src/test/simulators/OwnablePKSimulator.ts @@ -195,7 +195,6 @@ export class OwnablePKSimulator ).result; } - /** * @description Internal circuit that transfers ownership of the contract to `newOwner`. */ From d3c7a6206524c899d6e4ca68c4ba2d34efcaefe3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Wed, 23 Jul 2025 19:09:40 -0400 Subject: [PATCH 050/322] Init Shielded AcessControl --- contracts/shieldedAccessControl/package.json | 32 ++ .../src/ShieldedAccessControl.compact | 313 ++++++++++++++++++ .../mocks/MockShieldedAccessControl.compact | 53 +++ .../shieldedAccessControl/tsconfig.build.json | 5 + contracts/shieldedAccessControl/tsconfig.json | 25 ++ .../shieldedAccessControl/vitest.config.ts | 10 + .../ROOT/pages/api/shieldedAccessControl.adoc | 0 .../ROOT/pages/shieldedAccessControl.adoc | 0 8 files changed, 438 insertions(+) create mode 100644 contracts/shieldedAccessControl/package.json create mode 100644 contracts/shieldedAccessControl/src/ShieldedAccessControl.compact create mode 100644 contracts/shieldedAccessControl/src/test/mocks/MockShieldedAccessControl.compact create mode 100644 contracts/shieldedAccessControl/tsconfig.build.json create mode 100644 contracts/shieldedAccessControl/tsconfig.json create mode 100644 contracts/shieldedAccessControl/vitest.config.ts create mode 100644 docs/modules/ROOT/pages/api/shieldedAccessControl.adoc create mode 100644 docs/modules/ROOT/pages/shieldedAccessControl.adoc diff --git a/contracts/shieldedAccessControl/package.json b/contracts/shieldedAccessControl/package.json new file mode 100644 index 00000000..65238178 --- /dev/null +++ b/contracts/shieldedAccessControl/package.json @@ -0,0 +1,32 @@ +{ + "name": "@openzeppelin-compact/shielded-access-control", + "private": true, + "type": "module", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "require": "./dist/index.js", + "import": "./dist/index.js", + "default": "./dist/index.js" + } + }, + "scripts": { + "compact": "compact-compiler", + "build": "compact-builder && tsc", + "test": "vitest run", + "types": "tsc -p tsconfig.json --noEmit", + "clean": "git clean -fXd" + }, + "dependencies": { + "@openzeppelin-compact/compact": "workspace:^" + }, + "devDependencies": { + "@types/node": "22.14.0", + "ts-node": "^10.9.2", + "typescript": "^5.2.2", + "vitest": "^3.1.3" + } +} \ No newline at end of file diff --git a/contracts/shieldedAccessControl/src/ShieldedAccessControl.compact b/contracts/shieldedAccessControl/src/ShieldedAccessControl.compact new file mode 100644 index 00000000..25aca710 --- /dev/null +++ b/contracts/shieldedAccessControl/src/ShieldedAccessControl.compact @@ -0,0 +1,313 @@ +// SPDX-License-Identifier: MIT + +pragma language_version >= 0.15.0; + +/** + * @module Shielded AccessControl + * @description A Shielded AccessControl library. + * This module provides a role-based access control mechanism, where roles can be used to + * represent a set of permissions. Roles are stored as MerkleTree commitments to avoid + * disclosing information regarding the roles an account may have. Commitments are created + * with SHA256(PublicKey | roleIdentifier | nonce). + * + * @notice Using the SHA256 hashing function comes at a significant performace cost. In the future, we + * plan on migrating to a ZK-friendly hashing function like Poseidon when an implementation is available. + * + * Roles are referred to by their `Bytes<32>` identifier. These should be exposed + * in the top-level contract and be unique. One way to achieve this is by + * using `export sealed ledger` hash digests that are initialized in the top-level contract: + * + * ```typescript + * import CompactStandardLibrary; + * import "./node_modules/@openzeppelin-compact/accessControl/src/ShieldedAccessControl" prefix ShieldedAccessControl_; + * + * export sealed ledger MY_ROLE: Bytes<32>; + * + * constructor() { + * MY_ROLE = persistent_hash>(pad(32, "MY_ROLE")); + * } + * ``` + * + * To restrict access to a circuit, use {assertOnlyRole}: + * + * ```typescript + * circuit foo(): [] { + * assertOnlyRole(MY_ROLE); + * ... + * } + * ``` + * + * Roles can be granted and revoked dynamically via the {grantRole} and + * {revokeRole} circuits. Each role has an associated admin role, and only + * accounts that have a role's admin role can call {grantRole} and {revokeRole}. + * + * By default, the admin role for all roles is `DEFAULT_ADMIN_ROLE`, which means + * that only accounts with this role will be able to grant or revoke other + * roles. More complex role relationships can be created by using + * {_setRoleAdmin}. To set a custom `DEFAULT_ADMIN_ROLE`, implement the `Initializable` + * module and set `DEFAULT_ADMIN_ROLE` in the `initialize()` circuit. + * + * WARNING: The `DEFAULT_ADMIN_ROLE` is also its own admin: it has permission to + * grant and revoke this role. Extra precautions should be taken to secure + * accounts that have been granted it. + * + * @notice Roles can only be granted to ZswapCoinPublicKeys + * through the main role approval circuits (`grantRole` and `_grantRole`). + * In other words, role approvals to contract addresses are disallowed through these + * circuits. + * This is because Compact currently does not support contract-to-contract calls which means + * if a contract is granted a role, the contract cannot directly call the protected + * circuit. + * + * @notice This module does offer an experimental circuit that allows roles to be granted + * to contract addresses (`_unsafeGrantRole`). + * Note that the circuit name is very explicit ("unsafe") with this experimental circuit. + * Until contract-to-contract calls are supported, + * there is no direct way for a contract to call protected circuits. + * + * @notice The unsafe circuits are planned to become deprecated once contract-to-contract calls + * are supported. + * + * @notice Missing Features and Improvements: + * + * - Role events + * - An ERC165-like interface + */ +module ShieldedAccessControl { + import CompactStandardLibrary; + import "../../node_modules/@openzeppelin-compact/utils/src/Utils" prefix Utils_; + + /** + * @description A MerkleTree of role commitments stored as H(PK | role | nonce) + * @type {Bytes<32>} roleCommitment - A roleCommitment created by the following hash: SHA256(PK | role | nonce). + * @type {MerkleTree<4, roleCommitment>} + * @type {MerkleTree<4, Bytes<32>>} _operatorRoles +  */ + export ledger _operatorRoles: MerkleTree<4, Bytes<32>; + + /** + * @description Mapping from a role identifier to an admin role identifier. + * @type {Bytes<32>} roleId - A hash representing a role identifier. + * @type {Bytes<32>} adminId - A hash representing an admin identifier. + * @type {Map} + * @type {Map, Bytes<32>>} _adminRoles +  */ + export ledger _adminRoles: Map, Bytes<32>>; + + export ledger DEFAULT_ADMIN_ROLE: Bytes<32>; + + /** + * @description Returns `true` if `account` has been granted `roleId`. + * + * @circuitInfo + * + * @param {Either} account - The account to query. + * @param {Bytes<32>} roleId - The role identifier. + * @param {Bytes<16>} nonce - The nonce - H(SK | "role-nonce" | role | PK) - used to generate + * the role commitment stored in `_operatorRoles` + * @return {Boolean} - Whether the account has the specified role. +   */ + export circuit hasRole(roleId: Bytes<32>, account: Either, nonce: Bytes<16>): Boolean { + const authPath = persistent_hash>>(roleId, account.left.bytes, pad(32, nonce)); + return _operatorRoles.checkRoot(merkleTreePathRoot<4, Bytes<32>>(authPath)) + } + + /** + * @description Reverts if `own_public_key()` is missing `roleId`. + * + * @circuitInfo + * + * Requirements: + * + * - The caller must have `roleId`. + * - The caller must not be a ContractAddress + * + * @param {Bytes<32>} roleId - The role identifier. + * @return {[]} - Empty tuple. + */ + export circuit assertOnlyRole(roleId: Bytes<32>, nonce: Bytes<16>): [] { + _checkRole( + roleId, + left(own_public_key(), + nonce + )); + } + + /** + * @description Reverts if `account` is missing `roleId`. + * + * @circuitInfo + * + * Requirements: + * + * - `account` must have `roleId`. + * + * @param {Bytes<32>} roleId - The role identifier. + * @param {Either} account - The account to query. + * @return {[]} - Empty tuple. + */ + export circuit _checkRole(roleId: Bytes<32>, account: Either, nonce: Bytes<16>): [] { + assert hasRole(roleId, account, nonce) "ShieldedAccessControl: unauthorized account"; + } + + /** + * @description Returns the admin role that controls `roleId` or + * a byte array with all zero bytes if `roleId` doesn't exist. See {grantRole} and {revokeRole}. + * + * To change a role’s admin use {_setRoleAdmin}. + * + * @circuitInfo + * + * @param {Bytes<32>} roleId - The role identifier. + * @return {Bytes<32>} roleAdmin - The admin role that controls `roleId`. + */ + export circuit getRoleAdmin(roleId: Bytes<32>): Bytes<32> { + if (_adminRoles.member(roleId)) { + return _adminRoles.lookup(roleId); + } + return default>; + } + + /** + * @description Grants `roleId` to `account`. + * + * @circuitInfo + * + * Requirements: + * + * - `account` must not be a ContractAddress. + * - The caller must have `roleId`'s admin role. + * + * @param {Bytes<32>} roleId - The role identifier. + * @param {Either} account - A ZswapCoinPublicKey or ContractAddress. + * @return {[]} - Empty tuple. + */ + export circuit grantRole(roleId: Bytes<32>, account: Either, nonce: Bytes<16>): [] { + assertOnlyRole(getRoleAdmin(roleId), nonce); + _grantRole(roleId, account); + } + + /** + * @description Revokes `roleId` from `account`. + * + * @circuitInfo + * + * Requirements: + * + * - The caller must have `roleId`'s admin role. + * + * @param {Bytes<32>} roleId - The role identifier. + * @param {Either} account - A ZswapCoinPublicKey or ContractAddress. + * @return {[]} - Empty tuple. + */ + export circuit revokeRole(roleId: Bytes<32>, account: Either, nonce: Bytes<16>): [] { + assertOnlyRole(getRoleAdmin(roleId), nonce); + _revokeRole(roleId, account, nonce); + } + + /** + * @description Revokes `roleId` from the calling account. + * + * @notice Roles are often managed via {grantRole} and {revokeRole}: this circuit's + * purpose is to provide a mechanism for accounts to lose their privileges + * if they are compromised (such as when a trusted device is misplaced). + * + * @circuitInfo + * + * Requirements: + * + * - The caller must be `callerConfirmation`. + * - The caller must not be a `ContractAddress`. + * + * @param {Bytes<32>} roleId - The role identifier. + * @param {Either} callerConfirmation - A ZswapCoinPublicKey or ContractAddress. + * @return {[]} - Empty tuple. + */ + export circuit renounceRole(roleId: Bytes<32>, callerConfirmation: Either): [] { + assert callerConfirmation == left(own_public_key()) "AccessControl: bad confirmation"; + + _revokeRole(roleId, callerConfirmation); + } + + /** + * @description Sets `adminRole` as `roleId`'s admin role. + * + * @circuitInfo + * + * @param {Bytes<32>} roleId - The role identifier. + * @param {Bytes<32>} adminRole - The admin role identifier. + * @return {[]} - Empty tuple. + */ + export circuit _setRoleAdmin(roleId: Bytes<32>, adminRole: Bytes<32>): [] { + _adminRoles.insert(roleId, adminRole); + } + + /** + * @description Attempts to grant `roleId` to `account` and returns a boolean indicating if `roleId` was granted. + * Internal circuit without access restriction. + * + * @circuitInfo + * + * Requirements: + * + * - `account` must not be a ContractAddress. + * + * @param {Bytes<32>} roleId - The role identifier. + * @param {Either} account - A ZswapCoinPublicKey or ContractAddress. + * @return {Boolean} roleGranted - A boolean indicating if `roleId` was granted. + */ + export circuit _grantRole(roleId: Bytes<32>, account: Either, nonce: Bytes<16>): Boolean { + assert !Utils_isContractAddress(account) "AccessControl: unsafe role approval"; + return _unsafeGrantRole(roleId, account, nonce); + } + + /** + * @description Attempts to grant `roleId` to `account` and returns a boolean indicating if `roleId` was granted. + * Internal circuit without access restriction. It does NOT check if the role is granted to a ContractAddress. + * + * @circuitInfo + * + * @notice External smart contracts cannot call the token contract at this time, so granting a role to an ContractAddress may + * render a circuit permanently inaccessible. + * + * @param {Bytes<32>} roleId - The role identifier. + * @param {Either} account - A ZswapCoinPublicKey or ContractAddress. + * @return {Boolean} roleGranted - A boolean indicating if `role` was granted. + */ + export circuit _unsafeGrantRole(roleId: Bytes<32>, account: Either): Boolean { + if (hasRole(roleId, account, nonce)) { + return false; + } + + if (account.isLeft) { + const commitment = persistent_hash>>([roleId, account.left.bytes, pad(32, nonce)]) + _operatorRoles.insertHash(commitment); + return true; + } + + const commitment = persistent_hash>>([roleId, account.right.bytes, pad(32, nonce)]) + _operatorRoles.insertHash(commitment); + return true; + } + + /** + * @description Attempts to revoke `roleId` from `account` and returns a boolean indicating if `roleId` was revoked. + * Internal circuit without access restriction. + * + * @circuitInfo + * + * @param {Bytes<32>} roleId - The role identifier. + * @param {Bytes<32>} adminRole - The admin role identifier. + * @return {Boolean} roleRevoked - A boolean indicating if `roleId` was revoked. + */ + export circuit _revokeRole(roleId: Bytes<32>, account: Either): Boolean { + if (!hasRole(roleId, account)) { + return false; + } + + _operatorRoles + .lookup(roleId) + .insert(account, false); + return true; + } +} diff --git a/contracts/shieldedAccessControl/src/test/mocks/MockShieldedAccessControl.compact b/contracts/shieldedAccessControl/src/test/mocks/MockShieldedAccessControl.compact new file mode 100644 index 00000000..2324c6a8 --- /dev/null +++ b/contracts/shieldedAccessControl/src/test/mocks/MockShieldedAccessControl.compact @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT + +pragma language_version >= 0.15.0; + +import CompactStandardLibrary; + +import "../../ShieldedAccessControl" prefix ShieldedAccessControl_; + +export { ZswapCoinPublicKey, ContractAddress, Either, Maybe, ShieldedAccessControl_DEFAULT_ADMIN_ROLE }; + +export circuit hasRole(roleId: Bytes<32>, account: Either, nonce: Bytes<16>): Boolean { + return ShieldedAccessControl_hasRole(roleId, account); +} + +export circuit assertOnlyRole(roleId: Bytes<32>, nonce: Bytes<16>): [] { + ShieldedAccessControl_assertOnlyRole(roleId, nonce); +} + +export circuit _checkRole(roleId: Bytes<32>, account: Either, nonce: Bytes<16>): [] { + ShieldedAccessControl__checkRole(roleId, account, nonce); +} + +export circuit getRoleAdmin(roleId: Bytes<32>): Bytes<32> { + return ShieldedAccessControl_getRoleAdmin(roleId); +} + +export circuit grantRole(roleId: Bytes<32>, account: Either, nonce: Bytes<16>): [] { + ShieldedAccessControl_grantRole(roleId, account, nonce); +} + +export circuit revokeRole(roleId: Bytes<32>, account: Either): [] { + ShieldedAccessControl_revokeRole(roleId, account); +} + +export circuit renounceRole(roleId: Bytes<32>, callerConfirmation: Either): [] { + ShieldedAccessControl_renounceRole(roleId, callerConfirmation); +} + +export circuit _setRoleAdmin(roleId: Bytes<32>, adminRole: Bytes<32>): [] { + ShieldedAccessControl__setRoleAdmin(roleId, adminRole); +} + +export circuit _grantRole(roleId: Bytes<32>, account: Either, nonce: Bytes<16>): Boolean { + return ShieldedAccessControl__grantRole(roleId, account, nonce); +} + +export circuit _unsafeGrantRole(roleId: Bytes<32>, account: Either, nonce: Bytes<16>): Boolean { + return ShieldedAccessControl__unsafeGrantRole(roleId, account, nonce); +} + +export circuit _revokeRole(roleId: Bytes<32>, account: Either): Boolean { + return ShieldedAccessControl__revokeRole(roleId, account); +} diff --git a/contracts/shieldedAccessControl/tsconfig.build.json b/contracts/shieldedAccessControl/tsconfig.build.json new file mode 100644 index 00000000..f1132509 --- /dev/null +++ b/contracts/shieldedAccessControl/tsconfig.build.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["src/test/**/*.ts"], + "compilerOptions": {} +} diff --git a/contracts/shieldedAccessControl/tsconfig.json b/contracts/shieldedAccessControl/tsconfig.json new file mode 100644 index 00000000..4ae082c4 --- /dev/null +++ b/contracts/shieldedAccessControl/tsconfig.json @@ -0,0 +1,25 @@ +{ + "include": [ + "src/**/*.ts" + ], + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "declaration": true, + "lib": [ + "ES2022" + ], + "target": "ES2022", + "module": "nodenext", + "moduleResolution": "nodenext", + "allowJs": true, + "forceConsistentCasingInFileNames": true, + "noImplicitAny": true, + "strict": true, + "isolatedModules": true, + "sourceMap": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "skipLibCheck": true + } +} diff --git a/contracts/shieldedAccessControl/vitest.config.ts b/contracts/shieldedAccessControl/vitest.config.ts new file mode 100644 index 00000000..785b792e --- /dev/null +++ b/contracts/shieldedAccessControl/vitest.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + include: ['src/test/**/*.test.ts'], + reporters: 'verbose', + }, +}); diff --git a/docs/modules/ROOT/pages/api/shieldedAccessControl.adoc b/docs/modules/ROOT/pages/api/shieldedAccessControl.adoc new file mode 100644 index 00000000..e69de29b diff --git a/docs/modules/ROOT/pages/shieldedAccessControl.adoc b/docs/modules/ROOT/pages/shieldedAccessControl.adoc new file mode 100644 index 00000000..e69de29b From 5f9268e2a4897abb016f5feafc5a49094a6f42e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Tue, 29 Jul 2025 19:50:17 -0400 Subject: [PATCH 051/322] Update ShieldedAccessControl to 0.24.0 --- .../src/ShieldedAccessControl.compact | 118 +++++++++++------- .../mocks/MockShieldedAccessControl.compact | 26 ++-- yarn.lock | 12 ++ 3 files changed, 96 insertions(+), 60 deletions(-) diff --git a/contracts/shieldedAccessControl/src/ShieldedAccessControl.compact b/contracts/shieldedAccessControl/src/ShieldedAccessControl.compact index 25aca710..e43f6380 100644 --- a/contracts/shieldedAccessControl/src/ShieldedAccessControl.compact +++ b/contracts/shieldedAccessControl/src/ShieldedAccessControl.compact @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma language_version >= 0.15.0; +pragma language_version >= 0.16.0; /** * @module Shielded AccessControl @@ -24,7 +24,7 @@ pragma language_version >= 0.15.0; * export sealed ledger MY_ROLE: Bytes<32>; * * constructor() { - * MY_ROLE = persistent_hash>(pad(32, "MY_ROLE")); + * MY_ROLE = persistentHash>(pad(32, "MY_ROLE")); * } * ``` * @@ -80,10 +80,17 @@ module ShieldedAccessControl { /** * @description A MerkleTree of role commitments stored as H(PK | role | nonce) * @type {Bytes<32>} roleCommitment - A roleCommitment created by the following hash: SHA256(PK | role | nonce). - * @type {MerkleTree<4, roleCommitment>} - * @type {MerkleTree<4, Bytes<32>>} _operatorRoles + * @type {MerkleTree<10, roleCommitment>} + * @type {MerkleTree<10, Bytes<32>>} _operatorRoles  */ - export ledger _operatorRoles: MerkleTree<4, Bytes<32>; + export ledger _operatorRoles: MerkleTree<10, Bytes<32>>; + + /** + * @description A set of nullifiers used to revoke permissions from a role + * @type {Bytes<32>} roleCommitment - A roleCommitment created by the following hash: SHA256(PK | role | nonce). + * @type {Set} _roleNullifiers +  */ + export ledger _roleNullifiers: Set>; /** * @description Mapping from a role identifier to an admin role identifier. @@ -96,24 +103,28 @@ module ShieldedAccessControl { export ledger DEFAULT_ADMIN_ROLE: Bytes<32>; - /** - * @description Returns `true` if `account` has been granted `roleId`. - * - * @circuitInfo - * - * @param {Either} account - The account to query. - * @param {Bytes<32>} roleId - The role identifier. - * @param {Bytes<16>} nonce - The nonce - H(SK | "role-nonce" | role | PK) - used to generate - * the role commitment stored in `_operatorRoles` - * @return {Boolean} - Whether the account has the specified role. -   */ - export circuit hasRole(roleId: Bytes<32>, account: Either, nonce: Bytes<16>): Boolean { - const authPath = persistent_hash>>(roleId, account.left.bytes, pad(32, nonce)); - return _operatorRoles.checkRoot(merkleTreePathRoot<4, Bytes<32>>(authPath)) + witness getRoleCommitmentPath(roleCommitment: Bytes<32>): MerkleTreePath<10, Bytes<32>>; + + /** + * @description Returns `true` if `account` has been granted `roleId`. + * + * @circuitInfo + * + * @param {Either} account - The account to query. + * @param {Bytes<32>} roleId - The role identifier. + * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK) + * @return {Boolean} - Whether the account has the specified role. +  */ + export circuit hasRole(roleId: Bytes<32>, account: Either, nonce: Bytes<32>): Boolean { + const roleCommitment = persistentHash>>([roleId, account.left.bytes, nonce]); + const authPath = getRoleCommitmentPath(roleCommitment); + const isNullified = _roleNullifiers.member(disclose(roleCommitment)); + return _operatorRoles + .checkRoot(merkleTreePathRoot<10, Bytes<32>>(disclose(authPath))) && !isNullified; } /** - * @description Reverts if `own_public_key()` is missing `roleId`. + * @description Reverts if `ownPublicKey()` is missing `roleId`. * * @circuitInfo * @@ -123,14 +134,15 @@ module ShieldedAccessControl { * - The caller must not be a ContractAddress * * @param {Bytes<32>} roleId - The role identifier. + * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK) * @return {[]} - Empty tuple. */ - export circuit assertOnlyRole(roleId: Bytes<32>, nonce: Bytes<16>): [] { + export circuit assertOnlyRole(roleId: Bytes<32>, nonce: Bytes<32>): [] { _checkRole( roleId, - left(own_public_key(), + left(ownPublicKey()), nonce - )); + ); } /** @@ -144,10 +156,11 @@ module ShieldedAccessControl { * * @param {Bytes<32>} roleId - The role identifier. * @param {Either} account - The account to query. + * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK) * @return {[]} - Empty tuple. */ - export circuit _checkRole(roleId: Bytes<32>, account: Either, nonce: Bytes<16>): [] { - assert hasRole(roleId, account, nonce) "ShieldedAccessControl: unauthorized account"; + export circuit _checkRole(roleId: Bytes<32>, account: Either, nonce: Bytes<32>): [] { + assert(hasRole(roleId, account, nonce), "ShieldedAccessControl: unauthorized account"); } /** @@ -162,8 +175,8 @@ module ShieldedAccessControl { * @return {Bytes<32>} roleAdmin - The admin role that controls `roleId`. */ export circuit getRoleAdmin(roleId: Bytes<32>): Bytes<32> { - if (_adminRoles.member(roleId)) { - return _adminRoles.lookup(roleId); + if (_adminRoles.member(disclose(roleId))) { + return _adminRoles.lookup(disclose(roleId)); } return default>; } @@ -180,11 +193,12 @@ module ShieldedAccessControl { * * @param {Bytes<32>} roleId - The role identifier. * @param {Either} account - A ZswapCoinPublicKey or ContractAddress. + * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK) * @return {[]} - Empty tuple. */ - export circuit grantRole(roleId: Bytes<32>, account: Either, nonce: Bytes<16>): [] { + export circuit grantRole(roleId: Bytes<32>, account: Either, nonce: Bytes<32>): [] { assertOnlyRole(getRoleAdmin(roleId), nonce); - _grantRole(roleId, account); + _grantRole(roleId, account, nonce); } /** @@ -198,9 +212,10 @@ module ShieldedAccessControl { * * @param {Bytes<32>} roleId - The role identifier. * @param {Either} account - A ZswapCoinPublicKey or ContractAddress. + * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK) * @return {[]} - Empty tuple. */ - export circuit revokeRole(roleId: Bytes<32>, account: Either, nonce: Bytes<16>): [] { + export circuit revokeRole(roleId: Bytes<32>, account: Either, nonce: Bytes<32>): [] { assertOnlyRole(getRoleAdmin(roleId), nonce); _revokeRole(roleId, account, nonce); } @@ -221,12 +236,13 @@ module ShieldedAccessControl { * * @param {Bytes<32>} roleId - The role identifier. * @param {Either} callerConfirmation - A ZswapCoinPublicKey or ContractAddress. + * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK) * @return {[]} - Empty tuple. */ - export circuit renounceRole(roleId: Bytes<32>, callerConfirmation: Either): [] { - assert callerConfirmation == left(own_public_key()) "AccessControl: bad confirmation"; + export circuit renounceRole(roleId: Bytes<32>, callerConfirmation: Either, nonce: Bytes<32>): [] { + assert(callerConfirmation == left(ownPublicKey()), "ShieldedAccessControl: bad confirmation"); - _revokeRole(roleId, callerConfirmation); + _revokeRole(roleId, callerConfirmation, nonce); } /** @@ -239,7 +255,7 @@ module ShieldedAccessControl { * @return {[]} - Empty tuple. */ export circuit _setRoleAdmin(roleId: Bytes<32>, adminRole: Bytes<32>): [] { - _adminRoles.insert(roleId, adminRole); + _adminRoles.insert(disclose(roleId), disclose(adminRole)); } /** @@ -254,10 +270,11 @@ module ShieldedAccessControl { * * @param {Bytes<32>} roleId - The role identifier. * @param {Either} account - A ZswapCoinPublicKey or ContractAddress. + * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK) * @return {Boolean} roleGranted - A boolean indicating if `roleId` was granted. */ - export circuit _grantRole(roleId: Bytes<32>, account: Either, nonce: Bytes<16>): Boolean { - assert !Utils_isContractAddress(account) "AccessControl: unsafe role approval"; + export circuit _grantRole(roleId: Bytes<32>, account: Either, nonce: Bytes<32>): Boolean { + assert(!Utils_isContractAddress(account), "ShieldedAccessControl: unsafe role approval"); return _unsafeGrantRole(roleId, account, nonce); } @@ -272,21 +289,22 @@ module ShieldedAccessControl { * * @param {Bytes<32>} roleId - The role identifier. * @param {Either} account - A ZswapCoinPublicKey or ContractAddress. + * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK) * @return {Boolean} roleGranted - A boolean indicating if `role` was granted. */ - export circuit _unsafeGrantRole(roleId: Bytes<32>, account: Either): Boolean { + export circuit _unsafeGrantRole(roleId: Bytes<32>, account: Either, nonce: Bytes<32>): Boolean { if (hasRole(roleId, account, nonce)) { return false; } - if (account.isLeft) { - const commitment = persistent_hash>>([roleId, account.left.bytes, pad(32, nonce)]) - _operatorRoles.insertHash(commitment); + if (!Utils_isContractAddress(account)) { + const roleCommitment = persistentHash>>([roleId, account.left.bytes, nonce]); + _operatorRoles.insertHash(disclose(roleCommitment)); return true; } - const commitment = persistent_hash>>([roleId, account.right.bytes, pad(32, nonce)]) - _operatorRoles.insertHash(commitment); + const roleCommitment = persistentHash>>([roleId, account.right.bytes, nonce]); + _operatorRoles.insertHash(disclose(roleCommitment)); return true; } @@ -298,16 +316,22 @@ module ShieldedAccessControl { * * @param {Bytes<32>} roleId - The role identifier. * @param {Bytes<32>} adminRole - The admin role identifier. + * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK) * @return {Boolean} roleRevoked - A boolean indicating if `roleId` was revoked. */ - export circuit _revokeRole(roleId: Bytes<32>, account: Either): Boolean { - if (!hasRole(roleId, account)) { + export circuit _revokeRole(roleId: Bytes<32>, account: Either, nonce: Bytes<32>): Boolean { + if (!hasRole(roleId, account, nonce)) { return false; } - _operatorRoles - .lookup(roleId) - .insert(account, false); + if(!Utils_isContractAddress(account)) { + const roleCommitment = persistentHash>>([roleId, account.left.bytes, nonce]); + _roleNullifiers.insert(disclose(roleCommitment)); + return true; + } + + const roleCommitment = persistentHash>>([roleId, account.right.bytes, nonce]); + _roleNullifiers.insert(disclose(roleCommitment)); return true; } } diff --git a/contracts/shieldedAccessControl/src/test/mocks/MockShieldedAccessControl.compact b/contracts/shieldedAccessControl/src/test/mocks/MockShieldedAccessControl.compact index 2324c6a8..406bca34 100644 --- a/contracts/shieldedAccessControl/src/test/mocks/MockShieldedAccessControl.compact +++ b/contracts/shieldedAccessControl/src/test/mocks/MockShieldedAccessControl.compact @@ -8,15 +8,15 @@ import "../../ShieldedAccessControl" prefix ShieldedAccessControl_; export { ZswapCoinPublicKey, ContractAddress, Either, Maybe, ShieldedAccessControl_DEFAULT_ADMIN_ROLE }; -export circuit hasRole(roleId: Bytes<32>, account: Either, nonce: Bytes<16>): Boolean { - return ShieldedAccessControl_hasRole(roleId, account); +export circuit hasRole(roleId: Bytes<32>, account: Either, nonce: Bytes<32>): Boolean { + return ShieldedAccessControl_hasRole(roleId, account, nonce); } -export circuit assertOnlyRole(roleId: Bytes<32>, nonce: Bytes<16>): [] { +export circuit assertOnlyRole(roleId: Bytes<32>, nonce: Bytes<32>): [] { ShieldedAccessControl_assertOnlyRole(roleId, nonce); } -export circuit _checkRole(roleId: Bytes<32>, account: Either, nonce: Bytes<16>): [] { +export circuit _checkRole(roleId: Bytes<32>, account: Either, nonce: Bytes<32>): [] { ShieldedAccessControl__checkRole(roleId, account, nonce); } @@ -24,30 +24,30 @@ export circuit getRoleAdmin(roleId: Bytes<32>): Bytes<32> { return ShieldedAccessControl_getRoleAdmin(roleId); } -export circuit grantRole(roleId: Bytes<32>, account: Either, nonce: Bytes<16>): [] { +export circuit grantRole(roleId: Bytes<32>, account: Either, nonce: Bytes<32>): [] { ShieldedAccessControl_grantRole(roleId, account, nonce); } -export circuit revokeRole(roleId: Bytes<32>, account: Either): [] { - ShieldedAccessControl_revokeRole(roleId, account); +export circuit revokeRole(roleId: Bytes<32>, account: Either, nonce: Bytes<32>): [] { + ShieldedAccessControl_revokeRole(roleId, account, nonce); } -export circuit renounceRole(roleId: Bytes<32>, callerConfirmation: Either): [] { - ShieldedAccessControl_renounceRole(roleId, callerConfirmation); +export circuit renounceRole(roleId: Bytes<32>, callerConfirmation: Either, nonce: Bytes<32>): [] { + ShieldedAccessControl_renounceRole(roleId, callerConfirmation, nonce); } export circuit _setRoleAdmin(roleId: Bytes<32>, adminRole: Bytes<32>): [] { ShieldedAccessControl__setRoleAdmin(roleId, adminRole); } -export circuit _grantRole(roleId: Bytes<32>, account: Either, nonce: Bytes<16>): Boolean { +export circuit _grantRole(roleId: Bytes<32>, account: Either, nonce: Bytes<32>): Boolean { return ShieldedAccessControl__grantRole(roleId, account, nonce); } -export circuit _unsafeGrantRole(roleId: Bytes<32>, account: Either, nonce: Bytes<16>): Boolean { +export circuit _unsafeGrantRole(roleId: Bytes<32>, account: Either, nonce: Bytes<32>): Boolean { return ShieldedAccessControl__unsafeGrantRole(roleId, account, nonce); } -export circuit _revokeRole(roleId: Bytes<32>, account: Either): Boolean { - return ShieldedAccessControl__revokeRole(roleId, account); +export circuit _revokeRole(roleId: Bytes<32>, account: Either, nonce: Bytes<32>): Boolean { + return ShieldedAccessControl__revokeRole(roleId, account, nonce); } diff --git a/yarn.lock b/yarn.lock index bfe8e4de..8510acb8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -483,6 +483,18 @@ __metadata: languageName: unknown linkType: soft +"@openzeppelin-compact/shielded-access-control@workspace:contracts/shieldedAccessControl": + version: 0.0.0-use.local + resolution: "@openzeppelin-compact/shielded-access-control@workspace:contracts/shieldedAccessControl" + dependencies: + "@openzeppelin-compact/compact": "workspace:^" + "@types/node": "npm:22.14.0" + ts-node: "npm:^10.9.2" + typescript: "npm:^5.2.2" + vitest: "npm:^3.1.3" + languageName: unknown + linkType: soft + "@openzeppelin-compact/utils@workspace:contracts/utils": version: 0.0.0-use.local resolution: "@openzeppelin-compact/utils@workspace:contracts/utils" From bd81f55d3ed56f1cb1542e2c3be7b8a791155b72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Tue, 29 Jul 2025 21:11:21 -0400 Subject: [PATCH 052/322] Add disclosure closer to disclosure point --- contracts/utils/src/Utils.compact | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/utils/src/Utils.compact b/contracts/utils/src/Utils.compact index 3528e10e..01e013a0 100644 --- a/contracts/utils/src/Utils.compact +++ b/contracts/utils/src/Utils.compact @@ -63,7 +63,7 @@ module Utils { * @return {Boolean} - Returns true if `keyOrAddress` is a ContractAddress. */ export pure circuit isContractAddress(keyOrAddress: Either): Boolean { - return !keyOrAddress.is_left; + return disclose(!keyOrAddress.is_left); } /** From 01c0b6a0d9e578acc7c09608817ce7b6476ef425 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Thu, 31 Jul 2025 19:26:48 -0400 Subject: [PATCH 053/322] Revert "Add disclosure closer to disclosure point" This reverts commit bd81f55d3ed56f1cb1542e2c3be7b8a791155b72. --- contracts/utils/src/Utils.compact | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/utils/src/Utils.compact b/contracts/utils/src/Utils.compact index 01e013a0..3528e10e 100644 --- a/contracts/utils/src/Utils.compact +++ b/contracts/utils/src/Utils.compact @@ -63,7 +63,7 @@ module Utils { * @return {Boolean} - Returns true if `keyOrAddress` is a ContractAddress. */ export pure circuit isContractAddress(keyOrAddress: Either): Boolean { - return disclose(!keyOrAddress.is_left); + return !keyOrAddress.is_left; } /** From 04ff4f41c5af9d9252371ac38dd299bdd404e167 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Tue, 5 Aug 2025 15:52:45 -0400 Subject: [PATCH 054/322] Update contract design --- .../src/ShieldedAccessControl.compact | 317 +++++++++++++++--- .../src/ShieldedAccessControlUtils.compact | 25 ++ .../mocks/MockShieldedAccessControl.compact | 6 +- 3 files changed, 306 insertions(+), 42 deletions(-) create mode 100644 contracts/shieldedAccessControl/src/ShieldedAccessControlUtils.compact diff --git a/contracts/shieldedAccessControl/src/ShieldedAccessControl.compact b/contracts/shieldedAccessControl/src/ShieldedAccessControl.compact index e43f6380..ee39d49c 100644 --- a/contracts/shieldedAccessControl/src/ShieldedAccessControl.compact +++ b/contracts/shieldedAccessControl/src/ShieldedAccessControl.compact @@ -75,11 +75,11 @@ pragma language_version >= 0.16.0; */ module ShieldedAccessControl { import CompactStandardLibrary; - import "../../node_modules/@openzeppelin-compact/utils/src/Utils" prefix Utils_; + import "ShieldedAccessControlUtils" prefix Utils_; /** - * @description A MerkleTree of role commitments stored as H(PK | role | nonce) - * @type {Bytes<32>} roleCommitment - A roleCommitment created by the following hash: SHA256(PK | role | nonce). + * @description A MerkleTree of role commitments stored as SHA256(PK | role | nonce | index) + * @type {Bytes<32>} finalRoleCommitment - A roleCommitment created by the following hash: SHA256(PK | role | nonce | index). * @type {MerkleTree<10, roleCommitment>} * @type {MerkleTree<10, Bytes<32>>} _operatorRoles  */ @@ -87,10 +87,10 @@ module ShieldedAccessControl { /** * @description A set of nullifiers used to revoke permissions from a role - * @type {Bytes<32>} roleCommitment - A roleCommitment created by the following hash: SHA256(PK | role | nonce). - * @type {Set} _roleNullifiers + * @type {Bytes<32> roleCommitment - A roleCommitment created by the following hash: SHA256(PK | role | nonce | index). + * @type {Set} _roleCommitmentNullifiers  */ - export ledger _roleNullifiers: Set>; + export ledger _roleCommitmentNullifiers: Set>; /** * @description Mapping from a role identifier to an admin role identifier. @@ -101,37 +101,80 @@ module ShieldedAccessControl {  */ export ledger _adminRoles: Map, Bytes<32>>; + /** + * @description Mapping from an intermediateRoleCommitment to an index in the `_operatorRoles` MerkleTree. + * @type {Bytes<32>} intermediateRoleCommitment - An intermediateRoleCommitment created by the following hash: SHA256(PK | role | nonce). + * @type {Uint<64>} index - The index of a finalRoleCommitment in the `_operatorRoles` MerkleTree created by the following hash: SHA256(PK | role | nonce | index). + * @type {Map} + * @type {Map, Uint<64>>} _roleCommitmentIndex +  */ + export ledger _roleCommitmentIndex: Map, Uint<64>>; + + export ledger _nextIndex: Counter; + export ledger DEFAULT_ADMIN_ROLE: Bytes<32>; - witness getRoleCommitmentPath(roleCommitment: Bytes<32>): MerkleTreePath<10, Bytes<32>>; + witness getRoleCommitmentPath(roleCommitment: Bytes<32>, index: Uint<64>): MerkleTreePath<10, Bytes<32>>; /** * @description Returns `true` if `account` has been granted `roleId`. * - * @circuitInfo + * @circuitInfo k=16, rows=60076 + * + * Requirements: + * + * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce) + * must exist in the `_roleCommitmentIndex` Map. + * - A nullifier for the role commitment produced by SHA256(roleId | account | nonce | index) + * must not exist in the `_roleCommitmentNullifiers` Set. + * - A path for the role commitment produced by SHA256(roleId | account | nonce | index) must + * exist at `index` in the `_operatorRoles` MerkleTree. + * + * Disclosures: + * + * - The intermediate role commitment produced by SHA256(roleId | account | nonce). + * - The role commitment produced by SHA256(roleId | account | nonce | index). + * - The MerkleTree path for the role commitment stored at `index` in the `_operatorRoles` + * MerkleTree. + * - The type data of `account` - a ZswapCoinPublicKey or ContractAddress. * - * @param {Either} account - The account to query. * @param {Bytes<32>} roleId - The role identifier. + * @param {Either} account - The account to check. * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK) - * @return {Boolean} - Whether the account has the specified role. + * @return {Boolean} - A boolean determining if the account has the specified role.  */ export circuit hasRole(roleId: Bytes<32>, account: Either, nonce: Bytes<32>): Boolean { - const roleCommitment = persistentHash>>([roleId, account.left.bytes, nonce]); - const authPath = getRoleCommitmentPath(roleCommitment); - const isNullified = _roleNullifiers.member(disclose(roleCommitment)); - return _operatorRoles - .checkRoot(merkleTreePathRoot<10, Bytes<32>>(disclose(authPath))) && !isNullified; + if (!Utils_isContractAddress(account)) { + const zswapPubKey = account.left.bytes; + return _checkMerkleTree(roleId, zswapPubKey, nonce); + } + + const contractAddress = account.right.bytes; + return _checkMerkleTree(roleId, contractAddress, nonce); } /** * @description Reverts if `ownPublicKey()` is missing `roleId`. * - * @circuitInfo + * @circuitInfo k=15, rows=29786 * * Requirements: * - * - The caller must have `roleId`. - * - The caller must not be a ContractAddress + * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce) + * must exist in the `_roleCommitmentIndex` Map. + * - A nullifier for the role commitment produced by SHA256(roleId | account | nonce | index) + * must not exist in the `_roleCommitmentNullifiers` Set. + * - A path for the role commitment produced by SHA256(roleId | account | nonce | index) must + * exist at `index` in the `_operatorRoles` MerkleTree. + * - The caller must not be a ContractAddress. + * + * Disclosures: + * + * - The intermediate role commitment produced by SHA256(roleId | account | nonce). + * - The role commitment produced by SHA256(roleId | account | nonce | index). + * - The MerkleTree path for the role commitment stored at `index` in the `_operatorRoles` + * MerkleTree. + * - The type data of `account` - a ZswapCoinPublicKey or ContractAddress. * * @param {Bytes<32>} roleId - The role identifier. * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK) @@ -140,7 +183,7 @@ module ShieldedAccessControl { export circuit assertOnlyRole(roleId: Bytes<32>, nonce: Bytes<32>): [] { _checkRole( roleId, - left(ownPublicKey()), + left(ownPublicKey()), nonce ); } @@ -148,14 +191,27 @@ module ShieldedAccessControl { /** * @description Reverts if `account` is missing `roleId`. * - * @circuitInfo + * @circuitInfo k=16, rows=60055 * * Requirements: * - * - `account` must have `roleId`. + * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce) + * must exist in the `_roleCommitmentIndex` Map. + * - A nullifier for the role commitment produced by SHA256(roleId | account | nonce | index) + * must not exist in the `_roleCommitmentNullifiers` Set. + * - A path for the role commitment produced by SHA256(roleId | account | nonce | index) must + * exist at `index` in the `_operatorRoles` MerkleTree. + * + * Disclosures: + * + * - The intermediate role commitment produced by SHA256(roleId | account | nonce). + * - The role commitment produced by SHA256(roleId | account | nonce | index). + * - The MerkleTree path for the role commitment stored at `index` in the `_operatorRoles` + * MerkleTree. + * - The type data of `account` - a ZswapCoinPublicKey or ContractAddress. * * @param {Bytes<32>} roleId - The role identifier. - * @param {Either} account - The account to query. + * @param {Either} account - The account to check. * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK) * @return {[]} - Empty tuple. */ @@ -163,13 +219,53 @@ module ShieldedAccessControl { assert(hasRole(roleId, account, nonce), "ShieldedAccessControl: unauthorized account"); } + /** + * @description Checks if a path exists for a role commitment. + * + * @circuitInfo k=15, rows=29807 + * + * Requirements: + * + * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce) + * must exist in the `_roleCommitmentIndex` Map. + * - A nullifier for the role commitment produced by SHA256(roleId | account | nonce | index) + * must not exist in the `_roleCommitmentNullifiers` Set. + * - A path for the role commitment produced by SHA256(roleId | account | nonce | index) must + * exist at `index` in the `_operatorRoles` MerkleTree. + * + * Disclosures: + * + * - The intermediate role commitment produced by SHA256(roleId | account | nonce). + * - The role commitment produced by SHA256(roleId | account | nonce | index). + * - The MerkleTree path for the role commitment stored at `index` in the `_operatorRoles` + * MerkleTree. + * + * @param {Bytes<32>} roleId - The role identifier. + * @param {Bytes<32>} account - The account to check represented as a Bytes<32>. + * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK) + * @return {Boolean} - A boolean determining if a path for for the role commitment + * produced by SHA256(roleId | account | nonce | index) exists in the `_operatorRoles` MerkleTree + */ + export circuit _checkMerkleTree(roleId: Bytes<32>, account: Bytes<32>, nonce: Bytes<32>): Boolean { + const intermediateRoleCommitment = persistentHash>>([roleId, account, nonce]); + assert(_roleCommitmentIndex.member(disclose(intermediateRoleCommitment)), "ShieldedAccessControl: role commitment index not found"); + + const index = _roleCommitmentIndex.lookup(disclose(intermediateRoleCommitment)); + const finalRoleCommitment = persistentHash>>([roleId, account, nonce, index as Field as Bytes<32>]); + assert(!_roleCommitmentNullifiers.member(disclose(finalRoleCommitment)), "ShieldedAccessControl: role commitment access revoked"); + + const authPath = getRoleCommitmentPath(finalRoleCommitment, index); + return _operatorRoles + .checkRoot(merkleTreePathRoot<10, Bytes<32>>(disclose(authPath))); + } + /** * @description Returns the admin role that controls `roleId` or * a byte array with all zero bytes if `roleId` doesn't exist. See {grantRole} and {revokeRole}. * * To change a role’s admin use {_setRoleAdmin}. * - * @circuitInfo + * @circuitInfo k=10, rows=212 * * @param {Bytes<32>} roleId - The role identifier. * @return {Bytes<32>} roleAdmin - The admin role that controls `roleId`. @@ -184,12 +280,25 @@ module ShieldedAccessControl { /** * @description Grants `roleId` to `account`. * - * @circuitInfo + * @circuitInfo k=18, rows=138635 * * Requirements: * * - `account` must not be a ContractAddress. - * - The caller must have `roleId`'s admin role. + * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce) + * must exist in the `_roleCommitmentIndex` Map. + * - A nullifier for the role commitment produced by SHA256(roleId | account | nonce | index) + * must not exist in the `_roleCommitmentNullifiers` Set. + * - A path for the role commitment produced by SHA256(roleId | account | nonce | index) must + * exist at `index` in the `_operatorRoles` MerkleTree. + * + * Disclosures: + * + * - The intermediate role commitment produced by SHA256(roleId | account | nonce). + * - The role commitment produced by SHA256(roleId | account | nonce | index). + * - The MerkleTree path for the role commitment stored at `index` in the `_operatorRoles` + * MerkleTree. + * - The type data of `account` - a ZswapCoinPublicKey or ContractAddress. * * @param {Bytes<32>} roleId - The role identifier. * @param {Either} account - A ZswapCoinPublicKey or ContractAddress. @@ -204,11 +313,25 @@ module ShieldedAccessControl { /** * @description Revokes `roleId` from `account`. * - * @circuitInfo + * @circuitInfo k=18, rows=138383 * * Requirements: * - * - The caller must have `roleId`'s admin role. + * - `account` must not be a ContractAddress. + * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce) + * must exist in the `_roleCommitmentIndex` Map. + * - A nullifier for the role commitment produced by SHA256(roleId | account | nonce | index) + * must not exist in the `_roleCommitmentNullifiers` Set. + * - A path for the role commitment produced by SHA256(roleId | account | nonce | index) must + * exist at `index` in the `_operatorRoles` MerkleTree. + * + * Disclosures: + * + * - The intermediate role commitment produced by SHA256(roleId | account | nonce). + * - The role commitment produced by SHA256(roleId | account | nonce | index). + * - The MerkleTree path for the role commitment stored at `index` in the `_operatorRoles` + * MerkleTree. + * - The type data of `account` - a ZswapCoinPublicKey or ContractAddress. * * @param {Bytes<32>} roleId - The role identifier. * @param {Either} account - A ZswapCoinPublicKey or ContractAddress. @@ -227,12 +350,26 @@ module ShieldedAccessControl { * purpose is to provide a mechanism for accounts to lose their privileges * if they are compromised (such as when a trusted device is misplaced). * - * @circuitInfo + * @circuitInfo k=17, rows=108846 * * Requirements: * * - The caller must be `callerConfirmation`. * - The caller must not be a `ContractAddress`. + * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce) + * must exist in the `_roleCommitmentIndex` Map. + * - A nullifier for the role commitment produced by SHA256(roleId | account | nonce | index) + * must not exist in the `_roleCommitmentNullifiers` Set. + * - A path for the role commitment produced by SHA256(roleId | account | nonce | index) must + * exist at `index` in the `_operatorRoles` MerkleTree. + * + * Disclosures: + * + * - The intermediate role commitment produced by SHA256(roleId | account | nonce). + * - The role commitment produced by SHA256(roleId | account | nonce | index). + * - The MerkleTree path for the role commitment stored at `index` in the `_operatorRoles` + * MerkleTree. + * - The type data of `callerConfirmation` - a ZswapCoinPublicKey or ContractAddress. * * @param {Bytes<32>} roleId - The role identifier. * @param {Either} callerConfirmation - A ZswapCoinPublicKey or ContractAddress. @@ -248,7 +385,7 @@ module ShieldedAccessControl { /** * @description Sets `adminRole` as `roleId`'s admin role. * - * @circuitInfo + * @circuitInfo k=10, rows=210 * * @param {Bytes<32>} roleId - The role identifier. * @param {Bytes<32>} adminRole - The admin role identifier. @@ -262,11 +399,25 @@ module ShieldedAccessControl { * @description Attempts to grant `roleId` to `account` and returns a boolean indicating if `roleId` was granted. * Internal circuit without access restriction. * - * @circuitInfo + * @circuitInfo k=17, rows=109025 * * Requirements: * * - `account` must not be a ContractAddress. + * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce) + * must exist in the `_roleCommitmentIndex` Map. + * - A nullifier for the role commitment produced by SHA256(roleId | account | nonce | index) + * must not exist in the `_roleCommitmentNullifiers` Set. + * - A path for the role commitment produced by SHA256(roleId | account | nonce | index) must + * exist at `index` in the `_operatorRoles` MerkleTree. + * + * Disclosures: + * + * - The intermediate role commitment produced by SHA256(roleId | account | nonce). + * - The role commitment produced by SHA256(roleId | account | nonce | index). + * - The MerkleTree path for the role commitment stored at `index` in the `_operatorRoles` + * MerkleTree. + * - The type data of `account` - a ZswapCoinPublicKey or ContractAddress. * * @param {Bytes<32>} roleId - The role identifier. * @param {Either} account - A ZswapCoinPublicKey or ContractAddress. @@ -282,11 +433,28 @@ module ShieldedAccessControl { * @description Attempts to grant `roleId` to `account` and returns a boolean indicating if `roleId` was granted. * Internal circuit without access restriction. It does NOT check if the role is granted to a ContractAddress. * - * @circuitInfo + * @circuitInfo k=17, rows=109024 * * @notice External smart contracts cannot call the token contract at this time, so granting a role to an ContractAddress may * render a circuit permanently inaccessible. * + * Requirements: + * + * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce) + * must exist in the `_roleCommitmentIndex` Map. + * - A nullifier for the role commitment produced by SHA256(roleId | account | nonce | index) + * must not exist in the `_roleCommitmentNullifiers` Set. + * - A path for the role commitment produced by SHA256(roleId | account | nonce | index) must + * exist at `index` in the `_operatorRoles` MerkleTree. + * + * Disclosures: + * + * - The intermediate role commitment produced by SHA256(roleId | account | nonce). + * - The role commitment produced by SHA256(roleId | account | nonce | index). + * - The MerkleTree path for the role commitment stored at `index` in the `_operatorRoles` + * MerkleTree. + * - The type data of `account` - a ZswapCoinPublicKey or ContractAddress. + * * @param {Bytes<32>} roleId - The role identifier. * @param {Either} account - A ZswapCoinPublicKey or ContractAddress. * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK) @@ -298,13 +466,13 @@ module ShieldedAccessControl { } if (!Utils_isContractAddress(account)) { - const roleCommitment = persistentHash>>([roleId, account.left.bytes, nonce]); - _operatorRoles.insertHash(disclose(roleCommitment)); + const zswapPubKey = account.left.bytes; + _addRoleCommitmentToLedger(roleId, zswapPubKey, nonce); return true; } - const roleCommitment = persistentHash>>([roleId, account.right.bytes, nonce]); - _operatorRoles.insertHash(disclose(roleCommitment)); + const contractAddress = account.right.bytes; + _addRoleCommitmentToLedger(roleId, contractAddress, nonce); return true; } @@ -312,10 +480,27 @@ module ShieldedAccessControl { * @description Attempts to revoke `roleId` from `account` and returns a boolean indicating if `roleId` was revoked. * Internal circuit without access restriction. * - * @circuitInfo + * @circuitInfo k=17, rows=108770 + * + * Requirements: + * + * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce) + * must exist in the `_roleCommitmentIndex` Map. + * - A nullifier for the role commitment produced by SHA256(roleId | account | nonce | index) + * must not exist in the `_roleCommitmentNullifiers` Set. + * - A path for the role commitment produced by SHA256(roleId | account | nonce | index) must + * exist at `index` in the `_operatorRoles` MerkleTree. + * + * Disclosures: + * + * - The intermediate role commitment produced by SHA256(roleId | account | nonce). + * - The role commitment produced by SHA256(roleId | account | nonce | index). + * - The MerkleTree path for the role commitment stored at `index` in the `_operatorRoles` + * MerkleTree. + * - The type data of `account` - a ZswapCoinPublicKey or ContractAddress. * * @param {Bytes<32>} roleId - The role identifier. - * @param {Bytes<32>} adminRole - The admin role identifier. + * @param {Either} account - A ZswapCoinPublicKey or ContractAddress. * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK) * @return {Boolean} roleRevoked - A boolean indicating if `roleId` was revoked. */ @@ -325,13 +510,63 @@ module ShieldedAccessControl { } if(!Utils_isContractAddress(account)) { - const roleCommitment = persistentHash>>([roleId, account.left.bytes, nonce]); - _roleNullifiers.insert(disclose(roleCommitment)); + const zswapPubKey = account.left.bytes; + _nullifyRoleCommitment(roleId, zswapPubKey, nonce); return true; } - const roleCommitment = persistentHash>>([roleId, account.right.bytes, nonce]); - _roleNullifiers.insert(disclose(roleCommitment)); + const contractAddress = account.right.bytes; + _nullifyRoleCommitment(roleId, contractAddress, nonce); return true; } + + /** + * @description Adds a role commitment to the `_operatorRoles` MerkleTree. + * + * WARNING: Exposing this circuit in the implementing contract would allow anyone to add roles. + * + * @circuitInfo k=15, rows=24571 + * + * Disclosures: + * + * - The role commitment produced by SHA256(roleId | account | nonce | index). + * + * @param {Bytes<32>} roleId - The role identifier. + * @param {Bytes<32>} account - The account to add represented as a Bytes<32>. + * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK) + * @return {Boolean} roleRevoked - A boolean indicating if `roleId` was revoked. + */ + circuit _addRoleCommitmentToLedger(roleId: Bytes<32>, account: Bytes<32>, nonce: Bytes<32>): [] { + const intermediateRoleCommitment = persistentHash>>([roleId, account, nonce]); + const index = _nextIndex.read(); + const finalRoleCommitment = persistentHash>>([roleId, account, nonce, index as Field as Bytes<32>]); + + _operatorRoles.insertHashIndex(disclose(finalRoleCommitment), index); + _roleCommitmentIndex.insert(disclose(finalRoleCommitment), index); + _nextIndex.increment(1); + } + + /** + * @description Adds a role commitment to the `_roleNullifiers` nullifer set. + * + * WARNING: Exposing this circuit in the implementing contract would allow anyone to revoke roles. + * + * @circuitInfo k=15, rows=24563 + * + * Disclosures: + * + * - The role commitment produced by SHA256(roleId | account | nonce | index). + * - The intermediate role commitment produced by SHA256(roleId | account | nonce). + * + * @param {Bytes<32>} roleId - The role identifier. + * @param {Bytes<32>} account - The account to add represented as a Bytes<32>. + * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK) + * @return {Boolean} roleRevoked - A boolean indicating if `roleId` was revoked. + */ + circuit _nullifyRoleCommitment(roleId: Bytes<32>, account: Bytes<32>, nonce: Bytes<32>): [] { + const intermediateRoleCommitment = persistentHash>>([roleId, account, nonce]); + const index = _roleCommitmentIndex.lookup(disclose(intermediateRoleCommitment)); + const finalRoleCommitment = persistentHash>>([roleId, account, nonce, index as Field as Bytes<32>]); + _roleCommitmentNullifiers.insert(disclose(finalRoleCommitment)); + } } diff --git a/contracts/shieldedAccessControl/src/ShieldedAccessControlUtils.compact b/contracts/shieldedAccessControl/src/ShieldedAccessControlUtils.compact new file mode 100644 index 00000000..5c08f0f6 --- /dev/null +++ b/contracts/shieldedAccessControl/src/ShieldedAccessControlUtils.compact @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT + +pragma language_version >= 0.16.0; + +/** + * @module ShieldedAccessControlUtils. + * @description A library for common utilities used in the Shielded Access Control module. + */ +module ShieldedAccessControlUtils { + import CompactStandardLibrary; + + /** + * @description Returns whether `keyOrAddress` is a ContractAddress type. + * + * Disclosures: + * + * - The type data of `keyOrAddress` - a ZswapCoinPublicKey or ContractAddress. + * + * @param {Either} keyOrAddress - The target value to check, either a ZswapCoinPublicKey or a ContractAddress. + * @return {Boolean} - Returns true if `keyOrAddress` is a ContractAddress. + */ + export pure circuit isContractAddress(keyOrAddress: Either): Boolean { + return disclose(!keyOrAddress.is_left); + } +} diff --git a/contracts/shieldedAccessControl/src/test/mocks/MockShieldedAccessControl.compact b/contracts/shieldedAccessControl/src/test/mocks/MockShieldedAccessControl.compact index 406bca34..85c25042 100644 --- a/contracts/shieldedAccessControl/src/test/mocks/MockShieldedAccessControl.compact +++ b/contracts/shieldedAccessControl/src/test/mocks/MockShieldedAccessControl.compact @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma language_version >= 0.15.0; +pragma language_version >= 0.16.0; import CompactStandardLibrary; @@ -20,6 +20,10 @@ export circuit _checkRole(roleId: Bytes<32>, account: Either, account: Bytes<32>, nonce: Bytes<32>): Boolean { + return ShieldedAccessControl__checkMerkleTree(roleId, account, nonce); +} + export circuit getRoleAdmin(roleId: Bytes<32>): Bytes<32> { return ShieldedAccessControl_getRoleAdmin(roleId); } From 8fca267c7d102788d926e4ca354fce857e6f406a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Tue, 5 Aug 2025 15:54:30 -0400 Subject: [PATCH 055/322] expose merkletree ledger var in TS --- .../src/test/mocks/MockShieldedAccessControl.compact | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/contracts/shieldedAccessControl/src/test/mocks/MockShieldedAccessControl.compact b/contracts/shieldedAccessControl/src/test/mocks/MockShieldedAccessControl.compact index 85c25042..f8710b91 100644 --- a/contracts/shieldedAccessControl/src/test/mocks/MockShieldedAccessControl.compact +++ b/contracts/shieldedAccessControl/src/test/mocks/MockShieldedAccessControl.compact @@ -6,7 +6,14 @@ import CompactStandardLibrary; import "../../ShieldedAccessControl" prefix ShieldedAccessControl_; -export { ZswapCoinPublicKey, ContractAddress, Either, Maybe, ShieldedAccessControl_DEFAULT_ADMIN_ROLE }; +export { + ZswapCoinPublicKey, + ContractAddress, + Either, + Maybe, + ShieldedAccessControl_DEFAULT_ADMIN_ROLE, + ShieldedAccessControl__operatorRoles +}; export circuit hasRole(roleId: Bytes<32>, account: Either, nonce: Bytes<32>): Boolean { return ShieldedAccessControl_hasRole(roleId, account, nonce); From b4cc0e026bf268835090a907ed1cc7ec4a35703b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Tue, 5 Aug 2025 19:51:31 -0400 Subject: [PATCH 056/322] Update contract interface --- .../src/ShieldedAccessControl.compact | 16 ++++++++++++++++ .../test/mocks/MockShieldedAccessControl.compact | 11 ++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/contracts/shieldedAccessControl/src/ShieldedAccessControl.compact b/contracts/shieldedAccessControl/src/ShieldedAccessControl.compact index ee39d49c..0e40989e 100644 --- a/contracts/shieldedAccessControl/src/ShieldedAccessControl.compact +++ b/contracts/shieldedAccessControl/src/ShieldedAccessControl.compact @@ -110,11 +110,15 @@ module ShieldedAccessControl {  */ export ledger _roleCommitmentIndex: Map, Uint<64>>; + export ledger _roleIds: Set>; + export ledger _nextIndex: Counter; export ledger DEFAULT_ADMIN_ROLE: Bytes<32>; witness getRoleCommitmentPath(roleCommitment: Bytes<32>, index: Uint<64>): MerkleTreePath<10, Bytes<32>>; + witness _requestRole(roleId: Bytes<32>): []; + witness _recoverNonce(): []; /** * @description Returns `true` if `account` has been granted `roleId`. @@ -537,6 +541,10 @@ module ShieldedAccessControl { * @return {Boolean} roleRevoked - A boolean indicating if `roleId` was revoked. */ circuit _addRoleCommitmentToLedger(roleId: Bytes<32>, account: Bytes<32>, nonce: Bytes<32>): [] { + if (!_roleIds.member(disclose(roleId))) { + _roleIds.insert(disclose(roleId)); + } + const intermediateRoleCommitment = persistentHash>>([roleId, account, nonce]); const index = _nextIndex.read(); const finalRoleCommitment = persistentHash>>([roleId, account, nonce, index as Field as Bytes<32>]); @@ -569,4 +577,12 @@ module ShieldedAccessControl { const finalRoleCommitment = persistentHash>>([roleId, account, nonce, index as Field as Bytes<32>]); _roleCommitmentNullifiers.insert(disclose(finalRoleCommitment)); } + + export circuit requestRole(roleId: Bytes<32>): [] { + _requestRole(roleId); + } + + export circuit recoverNonce(): [] { + _recoverNonce(); + } } diff --git a/contracts/shieldedAccessControl/src/test/mocks/MockShieldedAccessControl.compact b/contracts/shieldedAccessControl/src/test/mocks/MockShieldedAccessControl.compact index f8710b91..1f8c048b 100644 --- a/contracts/shieldedAccessControl/src/test/mocks/MockShieldedAccessControl.compact +++ b/contracts/shieldedAccessControl/src/test/mocks/MockShieldedAccessControl.compact @@ -12,7 +12,8 @@ export { Either, Maybe, ShieldedAccessControl_DEFAULT_ADMIN_ROLE, - ShieldedAccessControl__operatorRoles + ShieldedAccessControl__operatorRoles, + ShieldedAccessControl__roleIds }; export circuit hasRole(roleId: Bytes<32>, account: Either, nonce: Bytes<32>): Boolean { @@ -62,3 +63,11 @@ export circuit _unsafeGrantRole(roleId: Bytes<32>, account: Either, account: Either, nonce: Bytes<32>): Boolean { return ShieldedAccessControl__revokeRole(roleId, account, nonce); } + +export circuit recoverNonce(): [] { + ShieldedAccessControl_recoverNonce(); +} + +export circuit requestRole(roleId: Bytes<32>): [] { + ShieldedAccessControl_requestRole(roleId); +} From 73ffd2bdee8d270d06888a69755f5bbbcbad63a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Tue, 5 Aug 2025 19:56:25 -0400 Subject: [PATCH 057/322] Remove _roleIds --- .../shieldedAccessControl/src/ShieldedAccessControl.compact | 6 ------ 1 file changed, 6 deletions(-) diff --git a/contracts/shieldedAccessControl/src/ShieldedAccessControl.compact b/contracts/shieldedAccessControl/src/ShieldedAccessControl.compact index 0e40989e..7183d954 100644 --- a/contracts/shieldedAccessControl/src/ShieldedAccessControl.compact +++ b/contracts/shieldedAccessControl/src/ShieldedAccessControl.compact @@ -110,8 +110,6 @@ module ShieldedAccessControl {  */ export ledger _roleCommitmentIndex: Map, Uint<64>>; - export ledger _roleIds: Set>; - export ledger _nextIndex: Counter; export ledger DEFAULT_ADMIN_ROLE: Bytes<32>; @@ -541,10 +539,6 @@ module ShieldedAccessControl { * @return {Boolean} roleRevoked - A boolean indicating if `roleId` was revoked. */ circuit _addRoleCommitmentToLedger(roleId: Bytes<32>, account: Bytes<32>, nonce: Bytes<32>): [] { - if (!_roleIds.member(disclose(roleId))) { - _roleIds.insert(disclose(roleId)); - } - const intermediateRoleCommitment = persistentHash>>([roleId, account, nonce]); const index = _nextIndex.read(); const finalRoleCommitment = persistentHash>>([roleId, account, nonce, index as Field as Bytes<32>]); From 8af10e28f8c98c35642d76efaed9c66bfd3e7f2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Tue, 5 Aug 2025 20:27:26 -0400 Subject: [PATCH 058/322] revert changes --- .../src/ShieldedAccessControl.compact | 26 ++++++++++++------- .../mocks/MockShieldedAccessControl.compact | 13 ++-------- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/contracts/shieldedAccessControl/src/ShieldedAccessControl.compact b/contracts/shieldedAccessControl/src/ShieldedAccessControl.compact index 7183d954..4d321892 100644 --- a/contracts/shieldedAccessControl/src/ShieldedAccessControl.compact +++ b/contracts/shieldedAccessControl/src/ShieldedAccessControl.compact @@ -110,13 +110,27 @@ module ShieldedAccessControl {  */ export ledger _roleCommitmentIndex: Map, Uint<64>>; + /** + * @description A counter tracking the next available index in the `_operatorRoles` MerkleTree + */ export ledger _nextIndex: Counter; export ledger DEFAULT_ADMIN_ROLE: Bytes<32>; + /** + * @description Returns the Merkle path, given the knowledge that a `roleCommitment` is at the given index. + * + * Requirements: + * + * - It is an error to call this if this if `roleCommitment` is not contained at the given index. + * + * @circuitInfo + * + * @param {Bytes<32>} roleCommitment - The role identifier. + * @param {Uint<64>} index - An index in the `_operatorRoles` MerkleTree + * @return {MerkleTreePath<10, Bytes<32>>} - The Merkle path of `roleCommitment` in the `_operatorRoles` Merkle Tree +  */ witness getRoleCommitmentPath(roleCommitment: Bytes<32>, index: Uint<64>): MerkleTreePath<10, Bytes<32>>; - witness _requestRole(roleId: Bytes<32>): []; - witness _recoverNonce(): []; /** * @description Returns `true` if `account` has been granted `roleId`. @@ -571,12 +585,4 @@ module ShieldedAccessControl { const finalRoleCommitment = persistentHash>>([roleId, account, nonce, index as Field as Bytes<32>]); _roleCommitmentNullifiers.insert(disclose(finalRoleCommitment)); } - - export circuit requestRole(roleId: Bytes<32>): [] { - _requestRole(roleId); - } - - export circuit recoverNonce(): [] { - _recoverNonce(); - } } diff --git a/contracts/shieldedAccessControl/src/test/mocks/MockShieldedAccessControl.compact b/contracts/shieldedAccessControl/src/test/mocks/MockShieldedAccessControl.compact index 1f8c048b..e1fe345d 100644 --- a/contracts/shieldedAccessControl/src/test/mocks/MockShieldedAccessControl.compact +++ b/contracts/shieldedAccessControl/src/test/mocks/MockShieldedAccessControl.compact @@ -12,8 +12,7 @@ export { Either, Maybe, ShieldedAccessControl_DEFAULT_ADMIN_ROLE, - ShieldedAccessControl__operatorRoles, - ShieldedAccessControl__roleIds + ShieldedAccessControl__operatorRoles }; export circuit hasRole(roleId: Bytes<32>, account: Either, nonce: Bytes<32>): Boolean { @@ -62,12 +61,4 @@ export circuit _unsafeGrantRole(roleId: Bytes<32>, account: Either, account: Either, nonce: Bytes<32>): Boolean { return ShieldedAccessControl__revokeRole(roleId, account, nonce); -} - -export circuit recoverNonce(): [] { - ShieldedAccessControl_recoverNonce(); -} - -export circuit requestRole(roleId: Bytes<32>): [] { - ShieldedAccessControl_requestRole(roleId); -} +} \ No newline at end of file From ecdc1d83f24843791a1830f3c4c7c9e64d20c230 Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 6 Aug 2025 01:06:57 -0300 Subject: [PATCH 059/322] update syntax to compact 0.24.0 --- contracts/ownable/src/OwnablePK.compact | 18 +++++------ contracts/ownable/src/test/OwnablePK.test.ts | 30 +++++++++---------- .../src/test/mocks/MockOwnablePK.compact | 4 +-- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/contracts/ownable/src/OwnablePK.compact b/contracts/ownable/src/OwnablePK.compact index 3c4ab389..60352b47 100644 --- a/contracts/ownable/src/OwnablePK.compact +++ b/contracts/ownable/src/OwnablePK.compact @@ -42,9 +42,9 @@ module OwnablePK { * @returns {[]} - None. */ export circuit initializer(initOwner: ZswapCoinPublicKey): [] { - assert initOwner != burn_address().left "OwnablePK: new owner cannot be zero"; + assert(initOwner != burnAddress().left, "OwnablePK: new owner cannot be zero"); const nextInstance = _instance + 1 as Field as Bytes<32>; - const shieldedOwner = shieldOwner(initOwner, nextInstance); + const shieldedOwner = shieldOwner(disclose(initOwner), nextInstance); _transferOwnership(shieldedOwner); } @@ -93,10 +93,10 @@ module OwnablePK { * @returns {[]} - None. */ export circuit acceptOwnership(): [] { - const caller = own_public_key(); + const caller = ownPublicKey(); const nextInstance = _instance + 1 as Field as Bytes<32>; const shieldedOwner = shieldOwner(caller, nextInstance); - assert shieldedOwner == _pendingOwner "OwnablePK: caller is not pending owner"; + assert(shieldedOwner == _pendingOwner, "OwnablePK: caller is not pending owner"); // Reset pending owner and assign new owner _transferOwnership(shieldedOwner); @@ -125,8 +125,8 @@ module OwnablePK { * @returns {[]} - None. */ export circuit assertOnlyOwner(): [] { - const caller = own_public_key(); - assert _owner == shieldOwner(caller, _instance as Field as Bytes<32>) "OwnablePK: not owner"; + const caller = ownPublicKey(); + assert(_owner == shieldOwner(caller, _instance as Field as Bytes<32>), "OwnablePK: not owner"); } /** @@ -136,7 +136,7 @@ module OwnablePK { * @returns {Bytes<32>} - The shielded hash of the owner and instance. */ export circuit shieldOwner(ownerPK: ZswapCoinPublicKey, instance: Bytes<32>): Bytes<32> { - return persistent_hash>>([pad(32, "OwnablePK:shield:"), instance, ownerPK.bytes]); + return persistentHash>>([pad(32, "OwnablePK:shield:"), instance, ownerPK.bytes]); } /** @@ -150,7 +150,7 @@ module OwnablePK { export circuit _transferOwnership(newOwner: Bytes<32>): [] { _pendingOwner = default>; _instance.increment(1); - _owner = newOwner; + _owner = disclose(newOwner); } /** @@ -162,7 +162,7 @@ module OwnablePK { * @returns {[]} - None. */ export circuit _proposeOwner(newOwner: ZswapCoinPublicKey): [] { - if (newOwner == burn_address().left) { + if (newOwner == burnAddress().left) { _pendingOwner = pad(32, ""); } else { const nextInstance = _instance + 1 as Field as Bytes<32>; diff --git a/contracts/ownable/src/test/OwnablePK.test.ts b/contracts/ownable/src/test/OwnablePK.test.ts index a9fba560..fb2ab94d 100644 --- a/contracts/ownable/src/test/OwnablePK.test.ts +++ b/contracts/ownable/src/test/OwnablePK.test.ts @@ -31,7 +31,7 @@ describe('OwnablePK', () => { ownable = new OwnablePKSimulator(Z_OWNER, OWNER); // Check instance - const instance = ownable.getCurrentPublicState().ownablePK_Instance; + const instance = ownable.getCurrentPublicState().OwnablePK__instance; expect(instance).toEqual(1n); // Check shielded owner @@ -43,7 +43,7 @@ describe('OwnablePK', () => { // Check pending owner const pendingOwner = - ownable.getCurrentPublicState().ownablePK_PendingOwner; + ownable.getCurrentPublicState().OwnablePK__pendingOwner; expect(pendingOwner).toEqual(EMPTY_BYTES); }); @@ -62,7 +62,7 @@ describe('OwnablePK', () => { describe('owner', () => { it('should return correct owner', () => { expect(ownable.owner()).toEqual( - ownable.getCurrentPublicState().ownablePK_Owner, + ownable.getCurrentPublicState().OwnablePK__owner, ); }); @@ -76,7 +76,7 @@ describe('OwnablePK', () => { describe('pendingOwner', () => { it('should return pending owner', () => { const nextInstance = - ownable.getCurrentPublicState().ownablePK_Instance + 1n; + ownable.getCurrentPublicState().OwnablePK__instance + 1n; const expPending = ownable.shieldOwner( Z_NEW_OWNER, convert_bigint_to_Uint8Array(32, nextInstance), @@ -98,7 +98,7 @@ describe('OwnablePK', () => { // Check pending owner const nextInstance = - ownable.getCurrentPublicState().ownablePK_Instance + 1n; + ownable.getCurrentPublicState().OwnablePK__instance + 1n; const expPending = ownable.shieldOwner( Z_NEW_OWNER, convert_bigint_to_Uint8Array(32, nextInstance), @@ -106,7 +106,7 @@ describe('OwnablePK', () => { expect(ownable.pendingOwner()).toEqual(expPending); // Check current owner - const thisInstance = ownable.getCurrentPublicState().ownablePK_Instance; + const thisInstance = ownable.getCurrentPublicState().OwnablePK__instance; const expOwner = ownable.shieldOwner( Z_OWNER, convert_bigint_to_Uint8Array(32, thisInstance), @@ -140,7 +140,7 @@ describe('OwnablePK', () => { // Check new pending owner const nextInstance = - ownable.getCurrentPublicState().ownablePK_Instance + 1n; + ownable.getCurrentPublicState().OwnablePK__instance + 1n; const expPending = ownable.shieldOwner( Z_NEW_NEW_OWNER, convert_bigint_to_Uint8Array(32, nextInstance), @@ -158,13 +158,13 @@ describe('OwnablePK', () => { it('should accept ownership from pending owner', () => { caller = NEW_OWNER; const beforeInstance = - ownable.getCurrentPublicState().ownablePK_Instance; + ownable.getCurrentPublicState().OwnablePK__instance; ownable.acceptOwnership(caller); // Check instance is bumped const afterInstance = - ownable.getCurrentPublicState().ownablePK_Instance; + ownable.getCurrentPublicState().OwnablePK__instance; expect(afterInstance).toEqual(beforeInstance + 1n); // Check new owner @@ -215,12 +215,12 @@ describe('OwnablePK', () => { it('should renounce ownership', () => { caller = OWNER; const beforeInstance = - ownable.getCurrentPublicState().ownablePK_Instance; + ownable.getCurrentPublicState().OwnablePK__instance; ownable.renounceOwnership(caller); expect(ownable.owner()).toEqual(EMPTY_BYTES); expect(ownable.pendingOwner()).toEqual(EMPTY_BYTES); - expect(ownable.getCurrentPublicState().ownablePK_Instance).toEqual( + expect(ownable.getCurrentPublicState().OwnablePK__instance).toEqual( beforeInstance + 1n, ); }); @@ -258,7 +258,7 @@ describe('OwnablePK', () => { // Transfer to new owner const nextInstance = - ownable.getCurrentPublicState().ownablePK_Instance + 1n; + ownable.getCurrentPublicState().OwnablePK__instance + 1n; const newOwner = ownable.shieldOwner( Z_NEW_OWNER, convert_bigint_to_Uint8Array(32, nextInstance), @@ -286,7 +286,7 @@ describe('OwnablePK', () => { describe('_transferOwnership', () => { it('should transfer ownership', () => { const beforeInstance = - ownable.getCurrentPublicState().ownablePK_Instance; + ownable.getCurrentPublicState().OwnablePK__instance; ownable._proposeOwner(Z_NEW_NEW_OWNER); ownable._transferOwnership(Z_NEW_OWNER.bytes); @@ -294,7 +294,7 @@ describe('OwnablePK', () => { // _transferownership does not shield the input so it should be a == a expect(ownable.owner()).toEqual(Z_NEW_OWNER.bytes); // Check instance is bumped - expect(ownable.getCurrentPublicState().ownablePK_Instance).toEqual( + expect(ownable.getCurrentPublicState().OwnablePK__instance).toEqual( beforeInstance + 1n, ); // Check pending owner is reset @@ -313,7 +313,7 @@ describe('OwnablePK', () => { ownable._proposeOwner(Z_NEW_OWNER); const nextInstance = - ownable.getCurrentPublicState().ownablePK_Instance + 1n; + ownable.getCurrentPublicState().OwnablePK__instance + 1n; const expOwner = ownable.shieldOwner( Z_NEW_OWNER, convert_bigint_to_Uint8Array(32, nextInstance), diff --git a/contracts/ownable/src/test/mocks/MockOwnablePK.compact b/contracts/ownable/src/test/mocks/MockOwnablePK.compact index 85ebcc76..1ab40018 100644 --- a/contracts/ownable/src/test/mocks/MockOwnablePK.compact +++ b/contracts/ownable/src/test/mocks/MockOwnablePK.compact @@ -21,7 +21,7 @@ export circuit pendingOwner(): Bytes<32> { } export circuit transferOwnership(newOwner: ZswapCoinPublicKey): [] { - return OwnablePK_transferOwnership(newOwner); + return OwnablePK_transferOwnership(disclose(newOwner)); } export circuit acceptOwnership(): [] { @@ -45,5 +45,5 @@ export circuit _transferOwnership(newOwner: Bytes<32>): [] { } export circuit _proposeOwner(newOwner: ZswapCoinPublicKey): [] { - return OwnablePK__proposeOwner(newOwner); + return OwnablePK__proposeOwner(disclose(newOwner)); } From cdb21bcb5a1e0f53f00e73f7baba43dd99763452 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Thu, 7 Aug 2025 15:26:37 -0400 Subject: [PATCH 060/322] Circuit performance speedup --- .../src/ShieldedAccessControl.compact | 244 +++++++++--------- 1 file changed, 122 insertions(+), 122 deletions(-) diff --git a/contracts/shieldedAccessControl/src/ShieldedAccessControl.compact b/contracts/shieldedAccessControl/src/ShieldedAccessControl.compact index 4d321892..686c6f42 100644 --- a/contracts/shieldedAccessControl/src/ShieldedAccessControl.compact +++ b/contracts/shieldedAccessControl/src/ShieldedAccessControl.compact @@ -5,10 +5,10 @@ pragma language_version >= 0.16.0; /** * @module Shielded AccessControl * @description A Shielded AccessControl library. - * This module provides a role-based access control mechanism, where roles can be used to - * represent a set of permissions. Roles are stored as MerkleTree commitments to avoid - * disclosing information regarding the roles an account may have. Commitments are created - * with SHA256(PublicKey | roleIdentifier | nonce). + * This module provides a shielded role-based access control mechanism, where roles can be used to + * represent a set of permissions. Roles are stored as Merkle tree commitments to avoid + * disclosing information about role holder. Role commitments are created with the following + * hashing scheme SHA256( SHA256(PublicKey | roleIdentifier | nonce) | index). * * @notice Using the SHA256 hashing function comes at a significant performace cost. In the future, we * plan on migrating to a ZK-friendly hashing function like Poseidon when an implementation is available. @@ -78,20 +78,13 @@ module ShieldedAccessControl { import "ShieldedAccessControlUtils" prefix Utils_; /** - * @description A MerkleTree of role commitments stored as SHA256(PK | role | nonce | index) - * @type {Bytes<32>} finalRoleCommitment - A roleCommitment created by the following hash: SHA256(PK | role | nonce | index). + * @description A Merkle tree of role commitments stored as SHA256( SHA256(PK | role | nonce) | index) + * @type {Bytes<32>} roleCommitment - A commitment created by the following hash: SHA256( SHA256(PK | role | nonce) | index). * @type {MerkleTree<10, roleCommitment>} * @type {MerkleTree<10, Bytes<32>>} _operatorRoles  */ export ledger _operatorRoles: MerkleTree<10, Bytes<32>>; - /** - * @description A set of nullifiers used to revoke permissions from a role - * @type {Bytes<32> roleCommitment - A roleCommitment created by the following hash: SHA256(PK | role | nonce | index). - * @type {Set} _roleCommitmentNullifiers -  */ - export ledger _roleCommitmentNullifiers: Set>; - /** * @description Mapping from a role identifier to an admin role identifier. * @type {Bytes<32>} roleId - A hash representing a role identifier. @@ -102,9 +95,16 @@ module ShieldedAccessControl { export ledger _adminRoles: Map, Bytes<32>>; /** - * @description Mapping from an intermediateRoleCommitment to an index in the `_operatorRoles` MerkleTree. - * @type {Bytes<32>} intermediateRoleCommitment - An intermediateRoleCommitment created by the following hash: SHA256(PK | role | nonce). - * @type {Uint<64>} index - The index of a finalRoleCommitment in the `_operatorRoles` MerkleTree created by the following hash: SHA256(PK | role | nonce | index). + * @description A set of nullifiers used to revoke the permissions of a role + * @type {Bytes<32> roleCommitment - A roleCommitment created by the following hash: SHA256( SHA256(PK | role | nonce) | index). + * @type {Set} _roleCommitmentNullifiers +  */ + export ledger _roleCommitmentNullifiers: Set>; + + /** + * @description Mapping from an intermediate role commitment hash to an index in the `_operatorRoles` Merkle tree. + * @type {Bytes<32>} intermediateRoleCommitment - An intermediate role commitment hash created by the following hashing scheme: SHA256(PK | role | nonce). + * @type {Uint<64>} index - The index of a role commitment in the `_operatorRoles` Merkle tree. * @type {Map} * @type {Map, Uint<64>>} _roleCommitmentIndex  */ @@ -118,7 +118,7 @@ module ShieldedAccessControl { export ledger DEFAULT_ADMIN_ROLE: Bytes<32>; /** - * @description Returns the Merkle path, given the knowledge that a `roleCommitment` is at the given index. + * @description Returns a Merkle path in the `_operatorRoles` Merkle tree, given the knowledge that a `roleCommitment` is at the given index. * * Requirements: * @@ -126,37 +126,37 @@ module ShieldedAccessControl { * * @circuitInfo * - * @param {Bytes<32>} roleCommitment - The role identifier. - * @param {Uint<64>} index - An index in the `_operatorRoles` MerkleTree - * @return {MerkleTreePath<10, Bytes<32>>} - The Merkle path of `roleCommitment` in the `_operatorRoles` Merkle Tree + * @param {Bytes<32>} roleCommitment - A commitment created by the following hash: SHA256( SHA256(PK | role | nonce) | index). + * @param {Uint<64>} index - An index in the `_operatorRoles` Merkle tree + * @return {MerkleTreePath<10, Bytes<32>>} - The Merkle path of `roleCommitment` in the `_operatorRoles` Merkle tree  */ witness getRoleCommitmentPath(roleCommitment: Bytes<32>, index: Uint<64>): MerkleTreePath<10, Bytes<32>>; /** * @description Returns `true` if `account` has been granted `roleId`. * - * @circuitInfo k=16, rows=60076 + * @circuitInfo k=16, rows=50605 * * Requirements: * * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce) - * must exist in the `_roleCommitmentIndex` Map. - * - A nullifier for the role commitment produced by SHA256(roleId | account | nonce | index) - * must not exist in the `_roleCommitmentNullifiers` Set. - * - A path for the role commitment produced by SHA256(roleId | account | nonce | index) must - * exist at `index` in the `_operatorRoles` MerkleTree. + * must exist in the `_roleCommitmentIndex` map. + * - A nullifier for the role commitment produced by SHA256( SHA256(roleId | account | nonce) | index) + * must not exist in the `_roleCommitmentNullifiers` set. + * - A path for the role commitment produced by SHA256( SHA256(roleId | account | nonce) | index) must + * exist at `index` in the `_operatorRoles` Merkle tree. * * Disclosures: * * - The intermediate role commitment produced by SHA256(roleId | account | nonce). - * - The role commitment produced by SHA256(roleId | account | nonce | index). - * - The MerkleTree path for the role commitment stored at `index` in the `_operatorRoles` - * MerkleTree. + * - The role commitment produced by SHA256( SHA256(roleId | account | nonce) | index). + * - The Merkle tree path for the role commitment stored at `index` in the `_operatorRoles` + * Merkle tree. * - The type data of `account` - a ZswapCoinPublicKey or ContractAddress. * * @param {Bytes<32>} roleId - The role identifier. * @param {Either} account - The account to check. - * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK) + * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK). * @return {Boolean} - A boolean determining if the account has the specified role.  */ export circuit hasRole(roleId: Bytes<32>, account: Either, nonce: Bytes<32>): Boolean { @@ -172,24 +172,24 @@ module ShieldedAccessControl { /** * @description Reverts if `ownPublicKey()` is missing `roleId`. * - * @circuitInfo k=15, rows=29786 + * @circuitInfo k=15, rows=25046 * * Requirements: * * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce) - * must exist in the `_roleCommitmentIndex` Map. - * - A nullifier for the role commitment produced by SHA256(roleId | account | nonce | index) - * must not exist in the `_roleCommitmentNullifiers` Set. - * - A path for the role commitment produced by SHA256(roleId | account | nonce | index) must - * exist at `index` in the `_operatorRoles` MerkleTree. + * must exist in the `_roleCommitmentIndex` map. + * - A nullifier for the role commitment produced by SHA256( SHA256(roleId | account | nonce) | index) + * must not exist in the `_roleCommitmentNullifiers` set. + * - A path for the role commitment produced by SHA256( SHA256(roleId | account | nonce) | index) must + * exist at `index` in the `_operatorRoles` Merkle tree. * - The caller must not be a ContractAddress. * * Disclosures: * * - The intermediate role commitment produced by SHA256(roleId | account | nonce). - * - The role commitment produced by SHA256(roleId | account | nonce | index). - * - The MerkleTree path for the role commitment stored at `index` in the `_operatorRoles` - * MerkleTree. + * - The role commitment produced by SHA256( SHA256(roleId | account | nonce) | index). + * - The Merkle tree path for the role commitment stored at `index` in the `_operatorRoles` + * Merkle tree. * - The type data of `account` - a ZswapCoinPublicKey or ContractAddress. * * @param {Bytes<32>} roleId - The role identifier. @@ -207,23 +207,23 @@ module ShieldedAccessControl { /** * @description Reverts if `account` is missing `roleId`. * - * @circuitInfo k=16, rows=60055 + * @circuitInfo k=16, rows=50584 * * Requirements: * * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce) - * must exist in the `_roleCommitmentIndex` Map. - * - A nullifier for the role commitment produced by SHA256(roleId | account | nonce | index) - * must not exist in the `_roleCommitmentNullifiers` Set. - * - A path for the role commitment produced by SHA256(roleId | account | nonce | index) must - * exist at `index` in the `_operatorRoles` MerkleTree. + * must exist in the `_roleCommitmentIndex` map. + * - A nullifier for the role commitment produced by SHA256( SHA256(roleId | account | nonce) | index) + * must not exist in the `_roleCommitmentNullifiers` set. + * - A path for the role commitment produced by SHA256( SHA256(roleId | account | nonce) | index) must + * exist at `index` in the `_operatorRoles` Merkle tree. * * Disclosures: * * - The intermediate role commitment produced by SHA256(roleId | account | nonce). - * - The role commitment produced by SHA256(roleId | account | nonce | index). - * - The MerkleTree path for the role commitment stored at `index` in the `_operatorRoles` - * MerkleTree. + * - The role commitment produced by SHA256( SHA256(roleId | account | nonce) | index). + * - The Merkle tree path for the role commitment stored at `index` in the `_operatorRoles` + * Merkle tree. * - The type data of `account` - a ZswapCoinPublicKey or ContractAddress. * * @param {Bytes<32>} roleId - The role identifier. @@ -238,36 +238,36 @@ module ShieldedAccessControl { /** * @description Checks if a path exists for a role commitment. * - * @circuitInfo k=15, rows=29807 + * @circuitInfo k=15, rows=25067 * * Requirements: * * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce) - * must exist in the `_roleCommitmentIndex` Map. - * - A nullifier for the role commitment produced by SHA256(roleId | account | nonce | index) - * must not exist in the `_roleCommitmentNullifiers` Set. - * - A path for the role commitment produced by SHA256(roleId | account | nonce | index) must - * exist at `index` in the `_operatorRoles` MerkleTree. + * must exist in the `_roleCommitmentIndex` map. + * - A nullifier for the role commitment produced by SHA256( SHA256(roleId | account | nonce) | index) + * must not exist in the `_roleCommitmentNullifiers` set. + * - A path for the role commitment produced by SHA256( SHA256(roleId | account | nonce) | index) must + * exist at `index` in the `_operatorRoles` Merkle tree. * * Disclosures: * * - The intermediate role commitment produced by SHA256(roleId | account | nonce). - * - The role commitment produced by SHA256(roleId | account | nonce | index). - * - The MerkleTree path for the role commitment stored at `index` in the `_operatorRoles` - * MerkleTree. + * - The role commitment produced by SHA256( SHA256(roleId | account | nonce) | index). + * - The Merkle tree path for the role commitment stored at `index` in the `_operatorRoles` + * Merkle tree. * * @param {Bytes<32>} roleId - The role identifier. * @param {Bytes<32>} account - The account to check represented as a Bytes<32>. * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK) * @return {Boolean} - A boolean determining if a path for for the role commitment - * produced by SHA256(roleId | account | nonce | index) exists in the `_operatorRoles` MerkleTree + * produced by SHA256( SHA256(roleId | account | nonce) | index) exists in the `_operatorRoles` Merkle tree */ export circuit _checkMerkleTree(roleId: Bytes<32>, account: Bytes<32>, nonce: Bytes<32>): Boolean { const intermediateRoleCommitment = persistentHash>>([roleId, account, nonce]); assert(_roleCommitmentIndex.member(disclose(intermediateRoleCommitment)), "ShieldedAccessControl: role commitment index not found"); const index = _roleCommitmentIndex.lookup(disclose(intermediateRoleCommitment)); - const finalRoleCommitment = persistentHash>>([roleId, account, nonce, index as Field as Bytes<32>]); + const finalRoleCommitment = persistentHash>>([intermediateRoleCommitment, index as Field as Bytes<32>]); assert(!_roleCommitmentNullifiers.member(disclose(finalRoleCommitment)), "ShieldedAccessControl: role commitment access revoked"); const authPath = getRoleCommitmentPath(finalRoleCommitment, index); @@ -281,7 +281,7 @@ module ShieldedAccessControl { * * To change a role’s admin use {_setRoleAdmin}. * - * @circuitInfo k=10, rows=212 + * @circuitInfo k=10, rows=207 * * @param {Bytes<32>} roleId - The role identifier. * @return {Bytes<32>} roleAdmin - The admin role that controls `roleId`. @@ -296,24 +296,24 @@ module ShieldedAccessControl { /** * @description Grants `roleId` to `account`. * - * @circuitInfo k=18, rows=138635 + * @circuitInfo k=17, rows=114944 * * Requirements: * * - `account` must not be a ContractAddress. * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce) - * must exist in the `_roleCommitmentIndex` Map. - * - A nullifier for the role commitment produced by SHA256(roleId | account | nonce | index) - * must not exist in the `_roleCommitmentNullifiers` Set. - * - A path for the role commitment produced by SHA256(roleId | account | nonce | index) must - * exist at `index` in the `_operatorRoles` MerkleTree. + * must exist in the `_roleCommitmentIndex` map. + * - A nullifier for the role commitment produced by SHA256( SHA256(roleId | account | nonce) | index) + * must not exist in the `_roleCommitmentNullifiers` set. + * - A path for the role commitment produced by SHA256( SHA256(roleId | account | nonce) | index) must + * exist at `index` in the `_operatorRoles` Merkle tree. * * Disclosures: * * - The intermediate role commitment produced by SHA256(roleId | account | nonce). - * - The role commitment produced by SHA256(roleId | account | nonce | index). - * - The MerkleTree path for the role commitment stored at `index` in the `_operatorRoles` - * MerkleTree. + * - The role commitment produced by SHA256( SHA256(roleId | account | nonce) | index). + * - The Merkle tree path for the role commitment stored at `index` in the `_operatorRoles` + * Merkle tree. * - The type data of `account` - a ZswapCoinPublicKey or ContractAddress. * * @param {Bytes<32>} roleId - The role identifier. @@ -329,24 +329,24 @@ module ShieldedAccessControl { /** * @description Revokes `roleId` from `account`. * - * @circuitInfo k=18, rows=138383 + * @circuitInfo k=17, rows=114699 * * Requirements: * * - `account` must not be a ContractAddress. * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce) - * must exist in the `_roleCommitmentIndex` Map. - * - A nullifier for the role commitment produced by SHA256(roleId | account | nonce | index) - * must not exist in the `_roleCommitmentNullifiers` Set. - * - A path for the role commitment produced by SHA256(roleId | account | nonce | index) must - * exist at `index` in the `_operatorRoles` MerkleTree. + * must exist in the `_roleCommitmentIndex` map. + * - A nullifier for the role commitment produced by SHA256( SHA256(roleId | account | nonce) | index) + * must not exist in the `_roleCommitmentNullifiers` set. + * - A path for the role commitment produced by SHA256( SHA256(roleId | account | nonce) | index) must + * exist at `index` in the `_operatorRoles` Merkle tree. * * Disclosures: * * - The intermediate role commitment produced by SHA256(roleId | account | nonce). - * - The role commitment produced by SHA256(roleId | account | nonce | index). - * - The MerkleTree path for the role commitment stored at `index` in the `_operatorRoles` - * MerkleTree. + * - The role commitment produced by SHA256( SHA256(roleId | account | nonce) | index). + * - The Merkle tree path for the role commitment stored at `index` in the `_operatorRoles` + * Merkle tree. * - The type data of `account` - a ZswapCoinPublicKey or ContractAddress. * * @param {Bytes<32>} roleId - The role identifier. @@ -366,25 +366,25 @@ module ShieldedAccessControl { * purpose is to provide a mechanism for accounts to lose their privileges * if they are compromised (such as when a trusted device is misplaced). * - * @circuitInfo k=17, rows=108846 + * @circuitInfo k=17, rows=89905 * * Requirements: * * - The caller must be `callerConfirmation`. * - The caller must not be a `ContractAddress`. * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce) - * must exist in the `_roleCommitmentIndex` Map. - * - A nullifier for the role commitment produced by SHA256(roleId | account | nonce | index) - * must not exist in the `_roleCommitmentNullifiers` Set. - * - A path for the role commitment produced by SHA256(roleId | account | nonce | index) must - * exist at `index` in the `_operatorRoles` MerkleTree. + * must exist in the `_roleCommitmentIndex` map. + * - A nullifier for the role commitment produced by SHA256( SHA256(roleId | account | nonce) | index) + * must not exist in the `_roleCommitmentNullifiers` set. + * - A path for the role commitment produced by SHA256( SHA256(roleId | account | nonce) | index) must + * exist at `index` in the `_operatorRoles` Merkle tree. * * Disclosures: * * - The intermediate role commitment produced by SHA256(roleId | account | nonce). - * - The role commitment produced by SHA256(roleId | account | nonce | index). - * - The MerkleTree path for the role commitment stored at `index` in the `_operatorRoles` - * MerkleTree. + * - The role commitment produced by SHA256( SHA256(roleId | account | nonce) | index). + * - The Merkle tree path for the role commitment stored at `index` in the `_operatorRoles` + * Merkle tree. * - The type data of `callerConfirmation` - a ZswapCoinPublicKey or ContractAddress. * * @param {Bytes<32>} roleId - The role identifier. @@ -401,7 +401,7 @@ module ShieldedAccessControl { /** * @description Sets `adminRole` as `roleId`'s admin role. * - * @circuitInfo k=10, rows=210 + * @circuitInfo k=10, rows=209 * * @param {Bytes<32>} roleId - The role identifier. * @param {Bytes<32>} adminRole - The admin role identifier. @@ -415,24 +415,24 @@ module ShieldedAccessControl { * @description Attempts to grant `roleId` to `account` and returns a boolean indicating if `roleId` was granted. * Internal circuit without access restriction. * - * @circuitInfo k=17, rows=109025 + * @circuitInfo k=17, rows=90077 * * Requirements: * * - `account` must not be a ContractAddress. * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce) - * must exist in the `_roleCommitmentIndex` Map. - * - A nullifier for the role commitment produced by SHA256(roleId | account | nonce | index) - * must not exist in the `_roleCommitmentNullifiers` Set. - * - A path for the role commitment produced by SHA256(roleId | account | nonce | index) must - * exist at `index` in the `_operatorRoles` MerkleTree. + * must exist in the `_roleCommitmentIndex` map. + * - A nullifier for the role commitment produced by SHA256( SHA256(roleId | account | nonce) | index) + * must not exist in the `_roleCommitmentNullifiers` set. + * - A path for the role commitment produced by SHA256( SHA256(roleId | account | nonce) | index) must + * exist at `index` in the `_operatorRoles` Merkle tree. * * Disclosures: * * - The intermediate role commitment produced by SHA256(roleId | account | nonce). - * - The role commitment produced by SHA256(roleId | account | nonce | index). - * - The MerkleTree path for the role commitment stored at `index` in the `_operatorRoles` - * MerkleTree. + * - The role commitment produced by SHA256( SHA256(roleId | account | nonce) | index). + * - The Merkle tree path for the role commitment stored at `index` in the `_operatorRoles` + * Merkle tree. * - The type data of `account` - a ZswapCoinPublicKey or ContractAddress. * * @param {Bytes<32>} roleId - The role identifier. @@ -449,7 +449,7 @@ module ShieldedAccessControl { * @description Attempts to grant `roleId` to `account` and returns a boolean indicating if `roleId` was granted. * Internal circuit without access restriction. It does NOT check if the role is granted to a ContractAddress. * - * @circuitInfo k=17, rows=109024 + * @circuitInfo k=17, rows=90076 * * @notice External smart contracts cannot call the token contract at this time, so granting a role to an ContractAddress may * render a circuit permanently inaccessible. @@ -457,18 +457,18 @@ module ShieldedAccessControl { * Requirements: * * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce) - * must exist in the `_roleCommitmentIndex` Map. - * - A nullifier for the role commitment produced by SHA256(roleId | account | nonce | index) - * must not exist in the `_roleCommitmentNullifiers` Set. - * - A path for the role commitment produced by SHA256(roleId | account | nonce | index) must - * exist at `index` in the `_operatorRoles` MerkleTree. + * must exist in the `_roleCommitmentIndex` map. + * - A nullifier for the role commitment produced by SHA256( SHA256(roleId | account | nonce) | index) + * must not exist in the `_roleCommitmentNullifiers` set. + * - A path for the role commitment produced by SHA256( SHA256(roleId | account | nonce) | index) must + * exist at `index` in the `_operatorRoles` Merkle tree. * * Disclosures: * * - The intermediate role commitment produced by SHA256(roleId | account | nonce). - * - The role commitment produced by SHA256(roleId | account | nonce | index). - * - The MerkleTree path for the role commitment stored at `index` in the `_operatorRoles` - * MerkleTree. + * - The role commitment produced by SHA256( SHA256(roleId | account | nonce) | index). + * - The Merkle tree path for the role commitment stored at `index` in the `_operatorRoles` + * Merkle tree. * - The type data of `account` - a ZswapCoinPublicKey or ContractAddress. * * @param {Bytes<32>} roleId - The role identifier. @@ -496,23 +496,23 @@ module ShieldedAccessControl { * @description Attempts to revoke `roleId` from `account` and returns a boolean indicating if `roleId` was revoked. * Internal circuit without access restriction. * - * @circuitInfo k=17, rows=108770 + * @circuitInfo k=17, rows=89829 * * Requirements: * * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce) - * must exist in the `_roleCommitmentIndex` Map. - * - A nullifier for the role commitment produced by SHA256(roleId | account | nonce | index) - * must not exist in the `_roleCommitmentNullifiers` Set. - * - A path for the role commitment produced by SHA256(roleId | account | nonce | index) must - * exist at `index` in the `_operatorRoles` MerkleTree. + * must exist in the `_roleCommitmentIndex` map. + * - A nullifier for the role commitment produced by SHA256( SHA256(roleId | account | nonce) | index) + * must not exist in the `_roleCommitmentNullifiers` set. + * - A path for the role commitment produced by SHA256( SHA256(roleId | account | nonce) | index) must + * exist at `index` in the `_operatorRoles` Merkle tree. * * Disclosures: * * - The intermediate role commitment produced by SHA256(roleId | account | nonce). - * - The role commitment produced by SHA256(roleId | account | nonce | index). - * - The MerkleTree path for the role commitment stored at `index` in the `_operatorRoles` - * MerkleTree. + * - The role commitment produced by SHA256( SHA256(roleId | account | nonce) | index). + * - The Merkle tree path for the role commitment stored at `index` in the `_operatorRoles` + * Merkle tree. * - The type data of `account` - a ZswapCoinPublicKey or ContractAddress. * * @param {Bytes<32>} roleId - The role identifier. @@ -537,15 +537,15 @@ module ShieldedAccessControl { } /** - * @description Adds a role commitment to the `_operatorRoles` MerkleTree. + * @description Adds a role commitment to the `_operatorRoles` Merkle tree. * * WARNING: Exposing this circuit in the implementing contract would allow anyone to add roles. * - * @circuitInfo k=15, rows=24571 + * @circuitInfo k=15, rows=19832 * * Disclosures: * - * - The role commitment produced by SHA256(roleId | account | nonce | index). + * - The role commitment produced by SHA256( SHA256(roleId | account | nonce) | index). * * @param {Bytes<32>} roleId - The role identifier. * @param {Bytes<32>} account - The account to add represented as a Bytes<32>. @@ -555,7 +555,7 @@ module ShieldedAccessControl { circuit _addRoleCommitmentToLedger(roleId: Bytes<32>, account: Bytes<32>, nonce: Bytes<32>): [] { const intermediateRoleCommitment = persistentHash>>([roleId, account, nonce]); const index = _nextIndex.read(); - const finalRoleCommitment = persistentHash>>([roleId, account, nonce, index as Field as Bytes<32>]); + const finalRoleCommitment = persistentHash>>([intermediateRoleCommitment, index as Field as Bytes<32>]); _operatorRoles.insertHashIndex(disclose(finalRoleCommitment), index); _roleCommitmentIndex.insert(disclose(finalRoleCommitment), index); @@ -567,11 +567,11 @@ module ShieldedAccessControl { * * WARNING: Exposing this circuit in the implementing contract would allow anyone to revoke roles. * - * @circuitInfo k=15, rows=24563 + * @circuitInfo k=15, rows=19824 * * Disclosures: * - * - The role commitment produced by SHA256(roleId | account | nonce | index). + * - The role commitment produced by SHA256( SHA256(roleId | account | nonce) | index). * - The intermediate role commitment produced by SHA256(roleId | account | nonce). * * @param {Bytes<32>} roleId - The role identifier. @@ -582,7 +582,7 @@ module ShieldedAccessControl { circuit _nullifyRoleCommitment(roleId: Bytes<32>, account: Bytes<32>, nonce: Bytes<32>): [] { const intermediateRoleCommitment = persistentHash>>([roleId, account, nonce]); const index = _roleCommitmentIndex.lookup(disclose(intermediateRoleCommitment)); - const finalRoleCommitment = persistentHash>>([roleId, account, nonce, index as Field as Bytes<32>]); + const finalRoleCommitment = persistentHash>>([intermediateRoleCommitment, index as Field as Bytes<32>]); _roleCommitmentNullifiers.insert(disclose(finalRoleCommitment)); } } From 20b14b8ca94ce4c67355ec03ba2af5411fe20ba2 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 7 Aug 2025 23:46:20 -0300 Subject: [PATCH 061/322] add initial pk design with init tests, sim, and witnesses --- contracts/ownable/src/Z_OwnablePK.compact | 75 +++++++ .../ownable/src/test/Z_OwnablePK.test.ts | 95 +++++++++ .../src/test/mocks/MockZ_OwnablePK.compact | 37 ++++ .../test/simulators/Z_OwnablePKSimulator.ts | 190 ++++++++++++++++++ .../src/witnesses/Z_OwnablePKWitnesses.ts | 38 ++++ contracts/ownable/src/witnesses/interface.ts | 15 +- 6 files changed, 449 insertions(+), 1 deletion(-) create mode 100644 contracts/ownable/src/Z_OwnablePK.compact create mode 100644 contracts/ownable/src/test/Z_OwnablePK.test.ts create mode 100644 contracts/ownable/src/test/mocks/MockZ_OwnablePK.compact create mode 100644 contracts/ownable/src/test/simulators/Z_OwnablePKSimulator.ts create mode 100644 contracts/ownable/src/witnesses/Z_OwnablePKWitnesses.ts diff --git a/contracts/ownable/src/Z_OwnablePK.compact b/contracts/ownable/src/Z_OwnablePK.compact new file mode 100644 index 00000000..22a55e06 --- /dev/null +++ b/contracts/ownable/src/Z_OwnablePK.compact @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: MIT + +pragma language_version >= 0.16.0; + +module Z_OwnablePK { + import CompactStandardLibrary; + + export ledger _ownerCommitment: Bytes<32>; + export ledger _instance: Counter; + + export witness offchainNonce(): Bytes<32>; + + export circuit initialize(initCommitment: Bytes<32>): [] { + assert(initCommitment != default>, "Invalid parameters"); + _transferOwnership(initCommitment); + } + + export circuit owner(): Bytes<32> { + return _ownerCommitment; + } + + export circuit transferOwnership(newOwnerCommitment: Bytes<32>): [] { + assertOnlyOwner(); + assert(newOwnerCommitment != default>, "Invalid parameters"); + _transferOwnership(newOwnerCommitment); + } + + export circuit renounceOwnership(): [] { + assertOnlyOwner(); + _transferOwnership(default>); + } + + export circuit renounceOwnershipObfuscated(): [] { + assertOnlyOwner(); + const nonce = offchainNonce(); + const obfuscatedCommitment = persistentHash>>( + [ + pad(32, "Z_OwnablePK:renounced:"), + default>, + _instance as Field as Bytes<32>, + nonce + ] + ); + + _transferOwnership(obfuscatedCommitment); + } + + export circuit assertOnlyOwner(): [] { + const caller = ownPublicKey(); + const nonce = offchainNonce(); + assert( + _ownerCommitment == shieldPK(caller, _instance, nonce + ), "Forbidden"); + } + + export circuit shieldPK( + pk: ZswapCoinPublicKey, + instance: Uint<64>, + nonce: Bytes<32> + ): Bytes<32> { + return persistentHash>>( + [ + pad(32, "Z_OwnablePK:shield:"), + pk.bytes, + instance as Field as Bytes<32>, + nonce + ] + ); + } + + export circuit _transferOwnership(newOwnerCommitment: Bytes<32>): [] { + _instance.increment(1); + _ownerCommitment = disclose(newOwnerCommitment); + } +} diff --git a/contracts/ownable/src/test/Z_OwnablePK.test.ts b/contracts/ownable/src/test/Z_OwnablePK.test.ts new file mode 100644 index 00000000..e2bb0ab8 --- /dev/null +++ b/contracts/ownable/src/test/Z_OwnablePK.test.ts @@ -0,0 +1,95 @@ +import { + type CoinPublicKey, + convert_bigint_to_Uint8Array, + persistentHash, + CompactTypeVector, + CompactTypeBytes +} from '@midnight-ntwrk/compact-runtime'; +import { beforeEach, describe, expect, it } from 'vitest'; +import { Z_OwnablePKSimulator } from './simulators/Z_OwnablePKSimulator.js'; +import * as utils from './utils/address.js'; +import { ZswapCoinPublicKey } from '../artifacts/MockOwnable/contract/index.cjs'; + +const OWNER = String(Buffer.from('OWNER', 'ascii').toString('hex')).padStart( + 64, + '0', +); +const NEW_OWNER = String( + Buffer.from('NEW_OWNER', 'ascii').toString('hex'), +).padStart(64, '0'); +const UNAUTHORIZED = String( + Buffer.from('UNAUTHORIZED', 'ascii').toString('hex'), +).padStart(64, '0'); +const Z_ZERO = utils.encodeToPK(''); +const Z_OWNER = utils.encodeToPK('OWNER'); +const Z_NEW_OWNER = utils.encodeToPK('NEW_OWNER'); +const Z_NEW_NEW_OWNER = utils.encodeToPK('Z_NEW_NEW_OWNER'); +const EMPTY_BYTES = utils.ZERO_KEY.left.bytes; + +// Commitments +const DOMAIN = "Z_OwnablePK:shield:"; +const INIT_COUNTER = 1n; +const STATIC_NONCE = new Uint8Array(32).fill(0xab); + +let ownable: Z_OwnablePKSimulator; +let caller: CoinPublicKey; + +const createZPKCommitment = ( + domain: string, + pk: ZswapCoinPublicKey, + counter: bigint, + nonce: Uint8Array +): Uint8Array => { + const rt_type = new CompactTypeVector(4, new CompactTypeBytes(32)); + const encoder = new TextEncoder(); + const bDomain = encoder.encode(domain); + const bPK = pk.bytes; + const bCounter = convert_bigint_to_Uint8Array(32, counter); + return persistentHash(rt_type, [bDomain, bPK, bCounter, nonce]); +} + +describe('Z_OwnablePK', () => { + describe('before initialize', () => { + it('should fail when setting owner commitment as 0', () => { + expect(() => { + const badCommitment = new Uint8Array(32).fill(0); + new Z_OwnablePKSimulator(badCommitment, OWNER); + }).toThrow('Invalid parameters'); + }); + + it('should initialize with non-zero commitment', () => { + const nonZeroCommitment = new Uint8Array(32).fill(1); + ownable = new Z_OwnablePKSimulator(nonZeroCommitment, OWNER); + + expect(ownable.owner()).toEqual(nonZeroCommitment); + }); + }); + + describe('after initialization', () => { + beforeEach(() => { + const ownerCommitment = createZPKCommitment(DOMAIN, Z_OWNER, INIT_COUNTER, STATIC_NONCE); + ownable = new Z_OwnablePKSimulator(ownerCommitment, OWNER); + }); + + describe('owner', () => { + it('should return the correct owner commitment', () => { + const expCommitment = createZPKCommitment(DOMAIN, Z_OWNER, INIT_COUNTER, STATIC_NONCE); + expect(ownable.owner()).toEqual(expCommitment); + }); + }); + + describe('assertOnlyOwner', () => { + it('should allow the authorized caller with correct nonce to call', () => { + caller = OWNER; + expect(ownable.assertOnlyOwner(caller)).to.not.throw; + }); + + it('should fail when called by unauthorized with correct nonce', () => { + caller = UNAUTHORIZED; + expect(() => { + ownable.assertOnlyOwner(caller); + }).toThrow('Forbidden'); + }); + }); + }); +}); diff --git a/contracts/ownable/src/test/mocks/MockZ_OwnablePK.compact b/contracts/ownable/src/test/mocks/MockZ_OwnablePK.compact new file mode 100644 index 00000000..e719e6c0 --- /dev/null +++ b/contracts/ownable/src/test/mocks/MockZ_OwnablePK.compact @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT + +pragma language_version >= 0.15.0; + +import CompactStandardLibrary; +import "../../Z_OwnablePK" prefix Z_OwnablePK_; + +export { ZswapCoinPublicKey, ContractAddress, Either, Maybe }; +export { Z_OwnablePK__ownerCommitment, Z_OwnablePK__instance }; + +constructor(initOwnerCommitment: Bytes<32>) { + Z_OwnablePK_initialize(initOwnerCommitment); +} + +export circuit owner(): Bytes<32> { + return Z_OwnablePK_owner(); +} + +export circuit transferOwnership(newOwnerCommitment: Bytes<32>): [] { + return Z_OwnablePK_transferOwnership(disclose(newOwnerCommitment)); +} + +export circuit renounceOwnership(): [] { + return Z_OwnablePK_renounceOwnership(); +} + +export circuit assertOnlyOwner(): [] { + return Z_OwnablePK_assertOnlyOwner(); +} + +export circuit shieldPK(ownerPK: ZswapCoinPublicKey, instance: Uint<64>, nonce: Bytes<32>): Bytes<32> { + return Z_OwnablePK_shieldPK(ownerPK, instance, nonce); +} + +export circuit _transferOwnership(newOwnerCommitment: Bytes<32>): [] { + return Z_OwnablePK__transferOwnership(newOwnerCommitment); +} diff --git a/contracts/ownable/src/test/simulators/Z_OwnablePKSimulator.ts b/contracts/ownable/src/test/simulators/Z_OwnablePKSimulator.ts new file mode 100644 index 00000000..dd187759 --- /dev/null +++ b/contracts/ownable/src/test/simulators/Z_OwnablePKSimulator.ts @@ -0,0 +1,190 @@ +import { + type CircuitContext, + type CoinPublicKey, + type ContractState, + constructorContext, + emptyZswapLocalState, + QueryContext, +} from '@midnight-ntwrk/compact-runtime'; +import { sampleContractAddress } from '@midnight-ntwrk/zswap'; +import type { ZswapCoinPublicKey } from '../../artifacts/MockZ_OwnablePK/contract/index.cjs'; +import { + type Ledger, + ledger, + Contract as MockOwnable, +} from '../../artifacts/MockZ_OwnablePK/contract/index.cjs'; // Combined imports +import { +Z_OwnablePKPrivateState, + Z_OwnablePKWitnesses, +} from '../../witnesses/Z_OwnablePKWitnesses.js'; +import type { IContractSimulator } from '../types/test.js'; +//import { types, padBytes, fieldToBytes } from 'compact-runtime'; + +/** + * @description A simulator implementation of a contract for testing purposes. + * @template P - The private state type, fixed to Z_OwnablePKPrivateState. + * @template L - The ledger type, fixed to Contract.Ledger. + */ +export class Z_OwnablePKSimulator + implements IContractSimulator +{ + /** @description The underlying contract instance managing contract logic. */ + readonly contract: MockOwnable; + + /** @description The deployed address of the contract. */ + readonly contractAddress: string; + + /** @description The deployer address of the contract. */ + readonly deployer: CoinPublicKey; + + /** @description The current circuit context, updated by contract operations. */ + circuitContext: CircuitContext; + + /** + * @description Initializes the mock contract. + */ + constructor(initOwner: Uint8Array, deployer: CoinPublicKey) { + this.contract = new MockOwnable(Z_OwnablePKWitnesses()); + this.deployer = deployer; + + const { + currentPrivateState, + currentContractState, + currentZswapLocalState, + } = this.contract.initialState( + constructorContext(Z_OwnablePKPrivateState.generate(), deployer), + initOwner + ); + this.circuitContext = { + currentPrivateState, + currentZswapLocalState, + originalState: currentContractState, + transactionContext: new QueryContext( + currentContractState.data, + sampleContractAddress(), + ), + }; + this.contractAddress = this.circuitContext.transactionContext.address; + } + + /** + * @description Retrieves the current public ledger state of the contract. + * @returns The ledger state as defined by the contract. + */ + public getCurrentPublicState(): Ledger { + return ledger(this.circuitContext.transactionContext.state); + } + + /** + * @description Retrieves the current private state of the contract. + * @returns The private state of type Z_OwnablePKPrivateState. + */ + public getCurrentPrivateState(): Z_OwnablePKPrivateState { + return this.circuitContext.currentPrivateState; + } + + /** + * @description Retrieves the current contract state. + * @returns The contract state object. + */ + public getCurrentContractState(): ContractState { + return this.circuitContext.originalState; + } + + /** + * @description Returns the shielded owner. + * @returns The shielded owner. + */ + public owner(): Uint8Array { + return this.contract.impureCircuits.owner(this.circuitContext).result; + } + + /** + * @description Initiates the two-step ownership transfer to `newOwner`. + */ + public transferOwnership( + newOwner: Uint8Array, + sender: CoinPublicKey, + ): CircuitContext { + const res = this.contract.impureCircuits.transferOwnership( + { + ...this.circuitContext, + currentZswapLocalState: sender + ? emptyZswapLocalState(sender) + : this.circuitContext.currentZswapLocalState, + }, + newOwner, + ); + + this.circuitContext = res.context; + return this.circuitContext; + } + + /** + * @description Leaves the contract without an owner. It will not be + * possible to call `assertOnlyOnwer` circuits anymore. Can only be + * called by the current owner. + */ + public renounceOwnership( + sender: CoinPublicKey, + ): CircuitContext { + const res = this.contract.impureCircuits.renounceOwnership({ + ...this.circuitContext, + currentZswapLocalState: sender + ? emptyZswapLocalState(sender) + : this.circuitContext.currentZswapLocalState, + }); + + this.circuitContext = res.context; + return this.circuitContext; + } + + /** + * @description Throws if called by any account other than the owner. + * Use this to restrict access to sensitive circuits. + */ + public assertOnlyOwner( + sender: CoinPublicKey, + ): CircuitContext { + const res = this.contract.impureCircuits.assertOnlyOwner({ + ...this.circuitContext, + currentZswapLocalState: sender + ? emptyZswapLocalState(sender) + : this.circuitContext.currentZswapLocalState, + }); + + this.circuitContext = res.context; + return this.circuitContext; + } + + /** + * @description Obfuscates the `ownerPK` be hashing it with a domain separator and + * the passed `instance`. + * @returns The shielded hash of the owner and instance. + */ + public shieldPK( + ownerPK: ZswapCoinPublicKey, + instance: bigint, + nonce: Uint8Array + ): Uint8Array { + return this.contract.circuits.shieldPK( + this.circuitContext, + ownerPK, + instance, + nonce + ).result; + } + + /** + * @description Internal circuit that transfers ownership of the contract to `newOwner`. + */ + public _transferOwnership( + newOwner: Uint8Array, + ): CircuitContext { + this.circuitContext = this.contract.impureCircuits._transferOwnership( + this.circuitContext, + newOwner, + ).context; + return this.circuitContext; + } +} diff --git a/contracts/ownable/src/witnesses/Z_OwnablePKWitnesses.ts b/contracts/ownable/src/witnesses/Z_OwnablePKWitnesses.ts new file mode 100644 index 00000000..78a096a0 --- /dev/null +++ b/contracts/ownable/src/witnesses/Z_OwnablePKWitnesses.ts @@ -0,0 +1,38 @@ +import { getRandomValues } from 'node:crypto'; +import type { Ledger } from '../artifacts/MockZ_OwnablePK/contract/index.cjs'; +import type { WitnessContext } from '@midnight-ntwrk/compact-runtime'; +import { IZ_OwnablePKWitnesses } from './interface.js' + +/** + * @description Represents the private state of an ownable contract, storing a secret nonce. + */ +export type Z_OwnablePKPrivateState = { + /** @description A 32-byte secret nonce used as a privacy additive. */ + offchainNonce: Buffer; +}; + +/** + * @description Utility object for managing the private state of an Ownable contract. + */ +export const Z_OwnablePKPrivateState = { + /** + * @description Generates a new private state with a random secret nonce. + * @returns A fresh Z_OwnablePKPrivateState instance. + */ + generate: (): Z_OwnablePKPrivateState => { + //return { offchainNonce: getRandomValues(Buffer.alloc(32))}; + return { offchainNonce: Buffer.from(Array(32).fill(0xab))}; + } +}; + +/** + * @description Factory function creating witness implementations for Ownable operations. + * @returns An object implementing the Witnesses interface for Z_OwnablePKPrivateState. + */ +export const Z_OwnablePKWitnesses = (): IZ_OwnablePKWitnesses => ({ + offchainNonce( + context: WitnessContext, + ): [Z_OwnablePKPrivateState, Uint8Array] { + return [context.privateState, context.privateState.offchainNonce]; + }, +}); \ No newline at end of file diff --git a/contracts/ownable/src/witnesses/interface.ts b/contracts/ownable/src/witnesses/interface.ts index 8ce2f273..3faedeb8 100644 --- a/contracts/ownable/src/witnesses/interface.ts +++ b/contracts/ownable/src/witnesses/interface.ts @@ -1,5 +1,5 @@ import type { WitnessContext } from '@midnight-ntwrk/compact-runtime'; -import type { Ledger } from '../artifacts/MockOwnablePK/contract/index.cjs'; // Combined imports +import type { Ledger } from '../artifacts/MockZ_OwnablePK/contract/index.cjs'; // Combined imports /** * @description Interface defining the witness methods for ownable operations. @@ -13,3 +13,16 @@ export interface IOwnableWitnesses

{ */ localSecretKey(context: WitnessContext): [P, Uint8Array]; } + +/** + * @description Interface defining the witness methods for Ownable operations. + * @template P - The private state type. + */ +export interface IZ_OwnablePKWitnesses

{ + /** + * Retrieves the secret nonce from the private state. + * @param context - The witness context containing the private state. + * @returns A tuple of the private state and the secret nonce as a Uint8Array. + */ + offchainNonce(context: WitnessContext): [P, Uint8Array]; +} From 39df0c60b80ec484f34b6c8a4cc3f7351c6b1e81 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 7 Aug 2025 23:46:48 -0300 Subject: [PATCH 062/322] remove comment --- contracts/ownable/src/test/simulators/Z_OwnablePKSimulator.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/ownable/src/test/simulators/Z_OwnablePKSimulator.ts b/contracts/ownable/src/test/simulators/Z_OwnablePKSimulator.ts index dd187759..1ee73716 100644 --- a/contracts/ownable/src/test/simulators/Z_OwnablePKSimulator.ts +++ b/contracts/ownable/src/test/simulators/Z_OwnablePKSimulator.ts @@ -18,7 +18,6 @@ Z_OwnablePKPrivateState, Z_OwnablePKWitnesses, } from '../../witnesses/Z_OwnablePKWitnesses.js'; import type { IContractSimulator } from '../types/test.js'; -//import { types, padBytes, fieldToBytes } from 'compact-runtime'; /** * @description A simulator implementation of a contract for testing purposes. From 7d748703715bbc5e3c55c52bdcff8aa6b538b4d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Fri, 8 Aug 2025 18:22:32 -0400 Subject: [PATCH 063/322] Update design and create witness implementations --- .../src/ShieldedAccessControl.compact | 70 ++++++- .../mocks/MockShieldedAccessControl.compact | 9 + .../ShieldedAccessControlWitnesses.ts | 181 ++++++++++++++++++ 3 files changed, 259 insertions(+), 1 deletion(-) create mode 100644 contracts/shieldedAccessControl/src/witnesses/ShieldedAccessControlWitnesses.ts diff --git a/contracts/shieldedAccessControl/src/ShieldedAccessControl.compact b/contracts/shieldedAccessControl/src/ShieldedAccessControl.compact index 686c6f42..a46ee9d0 100644 --- a/contracts/shieldedAccessControl/src/ShieldedAccessControl.compact +++ b/contracts/shieldedAccessControl/src/ShieldedAccessControl.compact @@ -51,6 +51,12 @@ pragma language_version >= 0.16.0; * grant and revoke this role. Extra precautions should be taken to secure * accounts that have been granted it. * + * By default, the salt value used to generate nonce values in the `requestRole` witness + * is set to 0. The use of a random salt value adds significantly to the strength of the + * underlying HKDF function and is highly encouraged. A random salt value can be set + * by implementing the `Initializable` module and setting `_salt` in the `initialize() + * circuit. + * * @notice Roles can only be granted to ZswapCoinPublicKeys * through the main role approval circuits (`grantRole` and `_grantRole`). * In other words, role approvals to contract addresses are disallowed through these @@ -115,6 +121,11 @@ module ShieldedAccessControl { */ export ledger _nextIndex: Counter; + /** + * @description A random salt value used to strengthen the HKDF function used in the `requestRole` witness function. + */ + export ledger _salt: Bytes<32>; + export ledger DEFAULT_ADMIN_ROLE: Bytes<32>; /** @@ -122,7 +133,7 @@ module ShieldedAccessControl { * * Requirements: * - * - It is an error to call this if this if `roleCommitment` is not contained at the given index. + * - It is an error to call this if `roleCommitment` is not contained at the given index. * * @circuitInfo * @@ -132,6 +143,30 @@ module ShieldedAccessControl {  */ witness getRoleCommitmentPath(roleCommitment: Bytes<32>, index: Uint<64>): MerkleTreePath<10, Bytes<32>>; + /** + * @description Locally creates and stores a nonce value using the HKDF function and the associated role identifier. + * + * @dev Developers must provide an implementation to privately send the account's public key, roleId, and nonce to an admin. One + * possible solution is by using an HTTP API. + * + * @param {Bytes<32>} roleId - A hash representing a role identifier. + * @param {Bytes<32>} account - The account requesting a role. + * @param {Bytes<32>} salt - A salt value for the underlying HKDF function. + * @return {[]} - Empty tuple. +  */ + witness requestRole(roleId: Bytes<32>, account: Bytes<32>, salt: Bytes<32>): []; + + /** + * @description Used to recover roles in the event of data loss. + * + * @dev Developers must export publicly declared roles from the top-level contract to generate possible roles for each. + * + * @param {Bytes<32>} account - The account requesting a role. + * @param {Bytes<32>}salt - A salt value for the underlying HKDF function. + * @return {[]} - Empty tuple. +  */ + witness recoverRoles(account: Bytes<32>, salt: Bytes<32>): []; + /** * @description Returns `true` if `account` has been granted `roleId`. * @@ -585,4 +620,37 @@ module ShieldedAccessControl { const finalRoleCommitment = persistentHash>>([intermediateRoleCommitment, index as Field as Bytes<32>]); _roleCommitmentNullifiers.insert(disclose(finalRoleCommitment)); } + + /** + * @description A wrapper circuit for the `requestRole` witness. + * + * @circuitInfo k=10, rows=188 + * + * Requirements: + * + * - The caller must not be a ContractAddress. + * + * @param {Bytes<32>} roleId - A hash representing a role identifier. + * @return {[]} - Empty tuple. +  */ + export circuit _requestRole(roleId: Bytes<32>): [] { + const publicKey = left(ownPublicKey()); + requestRole(roleId, publicKey.left.bytes, _salt); + } + + /** + * @description A wrapper circuit for the `recoverRoles` witness. + * + * @circuitInfo k=10, rows=99 + * + * Requirements: + * + * - The caller must not be a ContractAddress. + * + * @return {[]} - Empty tuple. +  */ + export circuit _recoverRoles(): [] { + const publicKey = left(ownPublicKey()); + recoverRoles(publicKey.left.bytes, _salt); + } } diff --git a/contracts/shieldedAccessControl/src/test/mocks/MockShieldedAccessControl.compact b/contracts/shieldedAccessControl/src/test/mocks/MockShieldedAccessControl.compact index e1fe345d..24f996a0 100644 --- a/contracts/shieldedAccessControl/src/test/mocks/MockShieldedAccessControl.compact +++ b/contracts/shieldedAccessControl/src/test/mocks/MockShieldedAccessControl.compact @@ -12,6 +12,7 @@ export { Either, Maybe, ShieldedAccessControl_DEFAULT_ADMIN_ROLE, + ShieldedAccessControl__salt, ShieldedAccessControl__operatorRoles }; @@ -61,4 +62,12 @@ export circuit _unsafeGrantRole(roleId: Bytes<32>, account: Either, account: Either, nonce: Bytes<32>): Boolean { return ShieldedAccessControl__revokeRole(roleId, account, nonce); +} + +export circuit _requestRole(roleId: Bytes<32>): [] { + ShieldedAccessControl__requestRole(roleId); +} + +export circuit _recoverRoles(): [] { + ShieldedAccessControl__recoverRoles(); } \ No newline at end of file diff --git a/contracts/shieldedAccessControl/src/witnesses/ShieldedAccessControlWitnesses.ts b/contracts/shieldedAccessControl/src/witnesses/ShieldedAccessControlWitnesses.ts new file mode 100644 index 00000000..ec345fb2 --- /dev/null +++ b/contracts/shieldedAccessControl/src/witnesses/ShieldedAccessControlWitnesses.ts @@ -0,0 +1,181 @@ +import { + type WitnessContext, + type MerkleTreePath, + constructorContext, + decodeCoinPublicKey, + QueryContext +} from '@midnight-ntwrk/compact-runtime'; +import { encodeContractAddress } from '@midnight-ntwrk/ledger'; +import { + type Either, + type Ledger, + Contract as MockShieldedAccessControl, + type ZswapCoinPublicKey, + type ContractAddress +} from '../artifacts/MockShieldedAccessControl/contract/index.cjs'; // Combined imports +import { Buffer } from 'node:buffer'; +import { sampleContractAddress } from '@midnight-ntwrk/zswap'; + +const { + hkdfSync, +} = await import('node:crypto'); + +const KEYLEN = 32; + +/** + * @description The respective `nonce` value for a given `roleId` should be at the same index + * for each array of `Buffer`s + */ +export type ShieldedAccessControlPrivateState = { + secretKey: Buffer; + nonces: Buffer[]; + roleIds: Buffer[]; +}; + +/** + * @description Generates a nonce value using the following scheme: HKDF-SHA256(SK, "role-nonce" | roleId | PK) + * @param secretKey - The secret key associated with the contract. + * @param roleId - The role identifier. + * @param salt - A salt value. + * @param account - The public key of an account. + * + * @returns A unique nonce value for `roleId` + */ +function generateNonce(secretKey: Buffer, roleId: Buffer, salt: Buffer, account: Buffer): + Buffer { + const domainString = Buffer.from('role-nonce'); + const info = Buffer.concat([domainString, roleId, account]); + const nonce = hkdfSync('sha512', secretKey, salt, info, KEYLEN); + + return Buffer.from(nonce); +} + +/** + * @description A stub function that simulates a successful role approval + * @param account - The public key of an account. + * @param roleId - The role identifier. + * @param nonce - The nonce associated with `roleId`. + * + * @returns Whether the account was approved for a role + */ +function sendRoleRequestToAdmin(account: Buffer, roleId: Buffer, nonce: Buffer) { + return true; +} + +export const ShieldedAccessControlWitnesses = { + /** + * @description Typescript implementation of the `getRoleCommitmentPath` witness function. + * @param privateState - The current private state. + * @param ledger - A snapshot of the current ledger state. + * @param roleCommitment - The role commitment to query. + * @param index - The index of `roleCommitment`in the Merkle tree. + * + * @returns An array of the private state and the Merkle tree path of `roleCommitment` + * in the `_operatorRoles` Merkle tree. + */ + getRoleCommitmentPath: ({ + ledger, + privateState, + }: WitnessContext, roleCommitment: Uint8Array, index: bigint): [ + ShieldedAccessControlPrivateState, + MerkleTreePath, + ] => { + const merkleTreePath = ledger.ShieldedAccessControl__operatorRoles.pathForLeaf(index, roleCommitment); + return [privateState, merkleTreePath] + }, + /** + * @description Typescript implementation of the `recoverNonce` witness function. Simulates calls to the `hasRole` circuit + * to determine if the account has the specified role. Updates the private state with any found roles. + * @param privateState - The current private state. + * @param ledger - A snapshot of the current ledger state. + * @param contractAddress - The address of the contract. + * @param account - The public key associated with a role. + * @param salt - A salt value. + * + * @returns An array of the new private state and the empty tuple + */ + recoverRoles: ({ + ledger, + privateState, + contractAddress + }: WitnessContext, account: Uint8Array, salt: Uint8Array): [ + ShieldedAccessControlPrivateState, + [], + ] => { + const roles = [ledger.ShieldedAccessControl_DEFAULT_ADMIN_ROLE]; + const coinPubKey = decodeCoinPublicKey(account); + let newPrivateState: ShieldedAccessControlPrivateState = { + secretKey: privateState.secretKey, + roleIds: [], + nonces: [] + }; + + const contract = new MockShieldedAccessControl( + ShieldedAccessControlWitnesses, + ); + const { + currentPrivateState, + currentContractState, + currentZswapLocalState, + } = contract.initialState(constructorContext({ secretKey: privateState.secretKey, nonces: [], roleIds: [] }, coinPubKey)); + const circuitContext = { + currentPrivateState, + currentZswapLocalState, + originalState: currentContractState, + transactionContext: new QueryContext( + currentContractState.data, + contractAddress, + ), + }; + + for (let i = 0; i < roles.length; i++) { + let role = roles[i]; + const nonce = generateNonce(privateState.secretKey, Buffer.from(role), Buffer.from(salt), Buffer.from(account)); + const eitherAccount: Either = { + is_left: true, + left: { bytes: account }, + right: { bytes: encodeContractAddress(sampleContractAddress()) }, + } + + try { + const hasRole = contract.impureCircuits.hasRole(circuitContext, role, eitherAccount, nonce); + if (hasRole) { + newPrivateState.nonces.push(nonce); + newPrivateState.roleIds.push(Buffer.from(role)) + } + } catch (err) { + console.log(err); + } + } + + return [newPrivateState, []] + }, + /** + * @description Typescript implementation of the `requestRole` witness function. + * @param privateState - The current private state. + * @param roleId - The role identifier. + * @param account - The public key requesting a role. + * @param salt - A salt value. + * + * @returns An array of the new private state and an empty array + */ + requestRole: ({ + privateState, + }: WitnessContext, roleId: Uint8Array, account: Uint8Array, salt: Uint8Array): [ + ShieldedAccessControlPrivateState, + [], + ] => { + const saltBuff = Buffer.from(salt); + const roleIdBuff = Buffer.from(roleId); + const accountBuff = Buffer.from(account); + const nonce = generateNonce(privateState.secretKey, roleIdBuff, saltBuff, accountBuff); + const isApproved = sendRoleRequestToAdmin(accountBuff, roleIdBuff, nonce); + + if (isApproved) { + privateState.nonces.push(nonce); + privateState.roleIds.push(roleIdBuff); + } + + return [privateState, []] + }, +}; From b28361dea90f45ae8aa122324024da3293279fb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Fri, 8 Aug 2025 18:22:57 -0400 Subject: [PATCH 064/322] fmt file --- .../ShieldedAccessControlWitnesses.ts | 131 +++++++++++------- 1 file changed, 82 insertions(+), 49 deletions(-) diff --git a/contracts/shieldedAccessControl/src/witnesses/ShieldedAccessControlWitnesses.ts b/contracts/shieldedAccessControl/src/witnesses/ShieldedAccessControlWitnesses.ts index ec345fb2..80663f1e 100644 --- a/contracts/shieldedAccessControl/src/witnesses/ShieldedAccessControlWitnesses.ts +++ b/contracts/shieldedAccessControl/src/witnesses/ShieldedAccessControlWitnesses.ts @@ -1,24 +1,22 @@ +import { Buffer } from 'node:buffer'; import { - type WitnessContext, - type MerkleTreePath, constructorContext, decodeCoinPublicKey, - QueryContext + type MerkleTreePath, + QueryContext, + type WitnessContext, } from '@midnight-ntwrk/compact-runtime'; import { encodeContractAddress } from '@midnight-ntwrk/ledger'; +import { sampleContractAddress } from '@midnight-ntwrk/zswap'; import { + type ContractAddress, type Either, type Ledger, Contract as MockShieldedAccessControl, type ZswapCoinPublicKey, - type ContractAddress } from '../artifacts/MockShieldedAccessControl/contract/index.cjs'; // Combined imports -import { Buffer } from 'node:buffer'; -import { sampleContractAddress } from '@midnight-ntwrk/zswap'; -const { - hkdfSync, -} = await import('node:crypto'); +const { hkdfSync } = await import('node:crypto'); const KEYLEN = 32; @@ -41,8 +39,12 @@ export type ShieldedAccessControlPrivateState = { * * @returns A unique nonce value for `roleId` */ -function generateNonce(secretKey: Buffer, roleId: Buffer, salt: Buffer, account: Buffer): - Buffer { +function generateNonce( + secretKey: Buffer, + roleId: Buffer, + salt: Buffer, + account: Buffer, +): Buffer { const domainString = Buffer.from('role-nonce'); const info = Buffer.concat([domainString, roleId, account]); const nonce = hkdfSync('sha512', secretKey, salt, info, KEYLEN); @@ -58,7 +60,11 @@ function generateNonce(secretKey: Buffer, roleId: Buffer, salt: Buffer, account: * * @returns Whether the account was approved for a role */ -function sendRoleRequestToAdmin(account: Buffer, roleId: Buffer, nonce: Buffer) { +function sendRoleRequestToAdmin( + account: Buffer, + roleId: Buffer, + nonce: Buffer, +) { return true; } @@ -73,15 +79,20 @@ export const ShieldedAccessControlWitnesses = { * @returns An array of the private state and the Merkle tree path of `roleCommitment` * in the `_operatorRoles` Merkle tree. */ - getRoleCommitmentPath: ({ - ledger, - privateState, - }: WitnessContext, roleCommitment: Uint8Array, index: bigint): [ - ShieldedAccessControlPrivateState, - MerkleTreePath, - ] => { - const merkleTreePath = ledger.ShieldedAccessControl__operatorRoles.pathForLeaf(index, roleCommitment); - return [privateState, merkleTreePath] + getRoleCommitmentPath: ( + { + ledger, + privateState, + }: WitnessContext, + roleCommitment: Uint8Array, + index: bigint, + ): [ShieldedAccessControlPrivateState, MerkleTreePath] => { + const merkleTreePath = + ledger.ShieldedAccessControl__operatorRoles.pathForLeaf( + index, + roleCommitment, + ); + return [privateState, merkleTreePath]; }, /** * @description Typescript implementation of the `recoverNonce` witness function. Simulates calls to the `hasRole` circuit @@ -94,30 +105,37 @@ export const ShieldedAccessControlWitnesses = { * * @returns An array of the new private state and the empty tuple */ - recoverRoles: ({ - ledger, - privateState, - contractAddress - }: WitnessContext, account: Uint8Array, salt: Uint8Array): [ - ShieldedAccessControlPrivateState, - [], - ] => { + recoverRoles: ( + { + ledger, + privateState, + contractAddress, + }: WitnessContext, + account: Uint8Array, + salt: Uint8Array, + ): [ShieldedAccessControlPrivateState, []] => { const roles = [ledger.ShieldedAccessControl_DEFAULT_ADMIN_ROLE]; const coinPubKey = decodeCoinPublicKey(account); - let newPrivateState: ShieldedAccessControlPrivateState = { + const newPrivateState: ShieldedAccessControlPrivateState = { secretKey: privateState.secretKey, roleIds: [], - nonces: [] + nonces: [], }; - const contract = new MockShieldedAccessControl( - ShieldedAccessControlWitnesses, - ); + const contract = + new MockShieldedAccessControl( + ShieldedAccessControlWitnesses, + ); const { currentPrivateState, currentContractState, currentZswapLocalState, - } = contract.initialState(constructorContext({ secretKey: privateState.secretKey, nonces: [], roleIds: [] }, coinPubKey)); + } = contract.initialState( + constructorContext( + { secretKey: privateState.secretKey, nonces: [], roleIds: [] }, + coinPubKey, + ), + ); const circuitContext = { currentPrivateState, currentZswapLocalState, @@ -129,26 +147,36 @@ export const ShieldedAccessControlWitnesses = { }; for (let i = 0; i < roles.length; i++) { - let role = roles[i]; - const nonce = generateNonce(privateState.secretKey, Buffer.from(role), Buffer.from(salt), Buffer.from(account)); + const role = roles[i]; + const nonce = generateNonce( + privateState.secretKey, + Buffer.from(role), + Buffer.from(salt), + Buffer.from(account), + ); const eitherAccount: Either = { is_left: true, left: { bytes: account }, right: { bytes: encodeContractAddress(sampleContractAddress()) }, - } + }; try { - const hasRole = contract.impureCircuits.hasRole(circuitContext, role, eitherAccount, nonce); + const hasRole = contract.impureCircuits.hasRole( + circuitContext, + role, + eitherAccount, + nonce, + ); if (hasRole) { newPrivateState.nonces.push(nonce); - newPrivateState.roleIds.push(Buffer.from(role)) + newPrivateState.roleIds.push(Buffer.from(role)); } } catch (err) { console.log(err); } } - return [newPrivateState, []] + return [newPrivateState, []]; }, /** * @description Typescript implementation of the `requestRole` witness function. @@ -159,16 +187,21 @@ export const ShieldedAccessControlWitnesses = { * * @returns An array of the new private state and an empty array */ - requestRole: ({ - privateState, - }: WitnessContext, roleId: Uint8Array, account: Uint8Array, salt: Uint8Array): [ - ShieldedAccessControlPrivateState, - [], - ] => { + requestRole: ( + { privateState }: WitnessContext, + roleId: Uint8Array, + account: Uint8Array, + salt: Uint8Array, + ): [ShieldedAccessControlPrivateState, []] => { const saltBuff = Buffer.from(salt); const roleIdBuff = Buffer.from(roleId); const accountBuff = Buffer.from(account); - const nonce = generateNonce(privateState.secretKey, roleIdBuff, saltBuff, accountBuff); + const nonce = generateNonce( + privateState.secretKey, + roleIdBuff, + saltBuff, + accountBuff, + ); const isApproved = sendRoleRequestToAdmin(accountBuff, roleIdBuff, nonce); if (isApproved) { @@ -176,6 +209,6 @@ export const ShieldedAccessControlWitnesses = { privateState.roleIds.push(roleIdBuff); } - return [privateState, []] + return [privateState, []]; }, }; From 5acfaf8f6611cb049a0e354923f369b601421495 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Fri, 8 Aug 2025 18:24:32 -0400 Subject: [PATCH 065/322] Fix lints --- .../src/witnesses/ShieldedAccessControlWitnesses.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/shieldedAccessControl/src/witnesses/ShieldedAccessControlWitnesses.ts b/contracts/shieldedAccessControl/src/witnesses/ShieldedAccessControlWitnesses.ts index 80663f1e..88e1c589 100644 --- a/contracts/shieldedAccessControl/src/witnesses/ShieldedAccessControlWitnesses.ts +++ b/contracts/shieldedAccessControl/src/witnesses/ShieldedAccessControlWitnesses.ts @@ -61,9 +61,9 @@ function generateNonce( * @returns Whether the account was approved for a role */ function sendRoleRequestToAdmin( - account: Buffer, - roleId: Buffer, - nonce: Buffer, + _account: Buffer, + _roleId: Buffer, + _nonce: Buffer, ) { return true; } From 5f9a9eb6668d02be3cf26ccdcf3d0e365bdcf22a Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 8 Aug 2025 21:08:00 -0300 Subject: [PATCH 066/322] remove old pk module, add abstract class and state manager for simulator --- contracts/ownable/src/OwnablePK.compact | 172 --------- contracts/ownable/src/test/OwnablePK.test.ts | 331 ------------------ .../ownable/src/test/Z_OwnablePK.test.ts | 17 +- .../src/test/mocks/MockOwnablePK.compact | 49 --- .../src/test/simulators/OwnablePKSimulator.ts | 223 ------------ .../test/simulators/Z_OwnablePKSimulator.ts | 243 ++++++++----- .../test/types/AbstractContractSimulator.ts | 131 +++++++ .../src/test/types/SimualatorStateManager.ts | 114 ++++++ contracts/ownable/src/test/types/test.ts | 91 ++++- .../src/witnesses/OwnablePKWitnesses.ts | 3 - 10 files changed, 486 insertions(+), 888 deletions(-) delete mode 100644 contracts/ownable/src/OwnablePK.compact delete mode 100644 contracts/ownable/src/test/OwnablePK.test.ts delete mode 100644 contracts/ownable/src/test/mocks/MockOwnablePK.compact delete mode 100644 contracts/ownable/src/test/simulators/OwnablePKSimulator.ts create mode 100644 contracts/ownable/src/test/types/AbstractContractSimulator.ts create mode 100644 contracts/ownable/src/test/types/SimualatorStateManager.ts delete mode 100644 contracts/ownable/src/witnesses/OwnablePKWitnesses.ts diff --git a/contracts/ownable/src/OwnablePK.compact b/contracts/ownable/src/OwnablePK.compact deleted file mode 100644 index 60352b47..00000000 --- a/contracts/ownable/src/OwnablePK.compact +++ /dev/null @@ -1,172 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma language_version >= 0.15.0; - -/** - * @module Shielded Ownable Public Key module - * @description The OwnablePK module provides a basic access control mechanism, - * where there is an account (an owner) that can be granted exclusive access - * to specific circuits. - * - * The initial owner can be set by using the `initializer` circuit during - * construction. The owner's public key will be obfuscated in the ledger - * (thus shielded) by the `shieldOwner` circuit. - * - * This module enforces a two-step ownership transfer mechanism. The mechanism - * flow starts with the current owner calling `transferOwnership` and passing - * the new owner's ZswapCoinPublicKey. The proposed owner's key is obfuscated - * similarly via the `shieldOwner` circuit. After the owner proposes the new - * owner, the new owner must accept ownership by calling `acceptOwnership`. - * This circuit validates that the caller is the proposed owner. Thereafter, - * the new owner may call `assertOnlyOwner` circuits. - * - * The reason this module enforces the two-step mechanism is for safety. - * If the owner transferred ownership to the wrong pubkey without the mechanism, - * it's likely that the ownership privileges will be lost for the contract forever. - * With the two-step mechanism, the current owner can overwrite the pending - * owner by calling `transferOwnership` with a different pubkey or passing - * zero to cancel the transfer. - */ -module OwnablePK { - import CompactStandardLibrary; - - /** Public state */ - export ledger _owner: Bytes<32>; - export ledger _pendingOwner: Bytes<32>; - export ledger _instance: Counter; - - /** - * @description Initializes the contract by setting `initOwner` as the - * (shielded) contract owner. - * - * @returns {[]} - None. - */ - export circuit initializer(initOwner: ZswapCoinPublicKey): [] { - assert(initOwner != burnAddress().left, "OwnablePK: new owner cannot be zero"); - const nextInstance = _instance + 1 as Field as Bytes<32>; - const shieldedOwner = shieldOwner(disclose(initOwner), nextInstance); - _transferOwnership(shieldedOwner); - } - - /** - * @description Returns the shielded owner. - * - * @returns {Bytes<32>} - The shielded owner. - */ - export circuit owner(): Bytes<32> { - return _owner; - } - - /** - * @description Returns the shielded pending owner. - * - * @returns {Bytes<32>} - The shielded proposed owner. - */ - export circuit pendingOwner(): Bytes<32> { - return _pendingOwner; - } - - /** - * @description Initiates the two-step ownership transfer to `newOwner`. - * To cancel an ownership transfer, the current owner can call this circuit - * and pass zero as `newOwner`. - * - * Requirements: - * - * - The caller must be the current contract owner. - * - * @returns {[]} - None. - */ - export circuit transferOwnership(newOwner: ZswapCoinPublicKey): [] { - assertOnlyOwner(); - _proposeOwner(newOwner); - } - - /** - * @description Finishes the two-step ownership transfer process by accepting - * the ownership. Can only be called by the pending owner. - * - * Requirements: - * - * - The caller is the pending owner. - * - * @returns {[]} - None. - */ - export circuit acceptOwnership(): [] { - const caller = ownPublicKey(); - const nextInstance = _instance + 1 as Field as Bytes<32>; - const shieldedOwner = shieldOwner(caller, nextInstance); - assert(shieldedOwner == _pendingOwner, "OwnablePK: caller is not pending owner"); - - // Reset pending owner and assign new owner - _transferOwnership(shieldedOwner); - } - - /** - * @description Leaves the contract without an owner. It will not be - * possible to call `assertOnlyOnwer` circuits anymore. Can only be - * called by the current owner. - * - * Requirements: - * - * - The caller is the contract owner. - * - * @returns {[]} - None. - */ - export circuit renounceOwnership(): [] { - assertOnlyOwner(); - _transferOwnership(default>); - } - - /** - * @description Throws if called by any account other than the owner. - * Use this to restrict access to sensitive circuits. - * - * @returns {[]} - None. - */ - export circuit assertOnlyOwner(): [] { - const caller = ownPublicKey(); - assert(_owner == shieldOwner(caller, _instance as Field as Bytes<32>), "OwnablePK: not owner"); - } - - /** - * @description Obfuscates the `ownerPK` be hashing it with a domain separator and - * the passed `instance`. - * - * @returns {Bytes<32>} - The shielded hash of the owner and instance. - */ - export circuit shieldOwner(ownerPK: ZswapCoinPublicKey, instance: Bytes<32>): Bytes<32> { - return persistentHash>>([pad(32, "OwnablePK:shield:"), instance, ownerPK.bytes]); - } - - /** - * @description Internal circuit that transfers ownership of the contract to `newOwner`. - * This circuit does not have access control and thus should not be exposed. - * - * Be careful with this circuit. `newOwner` will be stored in the ledger as it's - * passed meaning that `newOwner` must be shielded via `shieldOwner` beforehand. - * Maybe include `shieldOwner()` in logic so it's difficult to misuse? - */ - export circuit _transferOwnership(newOwner: Bytes<32>): [] { - _pendingOwner = default>; - _instance.increment(1); - _owner = disclose(newOwner); - } - - /** - * @description Internal circuit that sets the pending owner. - * Passing `newOwner` as zero cancels the two-step ownership - * transfer. Otherwise, this circuit shields `newOwner` and - * sets it in the ledger. - * - * @returns {[]} - None. - */ - export circuit _proposeOwner(newOwner: ZswapCoinPublicKey): [] { - if (newOwner == burnAddress().left) { - _pendingOwner = pad(32, ""); - } else { - const nextInstance = _instance + 1 as Field as Bytes<32>; - _pendingOwner = shieldOwner(newOwner, nextInstance); - } - } -} diff --git a/contracts/ownable/src/test/OwnablePK.test.ts b/contracts/ownable/src/test/OwnablePK.test.ts deleted file mode 100644 index fb2ab94d..00000000 --- a/contracts/ownable/src/test/OwnablePK.test.ts +++ /dev/null @@ -1,331 +0,0 @@ -import { - type CoinPublicKey, - convert_bigint_to_Uint8Array, -} from '@midnight-ntwrk/compact-runtime'; -import { beforeEach, describe, expect, it } from 'vitest'; -import { OwnablePKSimulator } from './simulators/OwnablePKSimulator.js'; -import * as utils from './utils/address.js'; - -const OWNER = String(Buffer.from('OWNER', 'ascii').toString('hex')).padStart( - 64, - '0', -); -const NEW_OWNER = String( - Buffer.from('NEW_OWNER', 'ascii').toString('hex'), -).padStart(64, '0'); -const UNAUTHORIZED = String( - Buffer.from('UNAUTHORIZED', 'ascii').toString('hex'), -).padStart(64, '0'); -const Z_ZERO = utils.encodeToPK(''); -const Z_OWNER = utils.encodeToPK('OWNER'); -const Z_NEW_OWNER = utils.encodeToPK('NEW_OWNER'); -const Z_NEW_NEW_OWNER = utils.encodeToPK('Z_NEW_NEW_OWNER'); -const EMPTY_BYTES = utils.ZERO_KEY.left.bytes; - -let ownable: OwnablePKSimulator; -let caller: CoinPublicKey; - -describe('OwnablePK', () => { - describe('initializer', () => { - it('should initialize and set the shielded owner', () => { - ownable = new OwnablePKSimulator(Z_OWNER, OWNER); - - // Check instance - const instance = ownable.getCurrentPublicState().OwnablePK__instance; - expect(instance).toEqual(1n); - - // Check shielded owner - const expOwner = ownable.shieldOwner( - Z_OWNER, - convert_bigint_to_Uint8Array(32, instance), - ); - expect(ownable.owner()).toEqual(expOwner); - - // Check pending owner - const pendingOwner = - ownable.getCurrentPublicState().OwnablePK__pendingOwner; - expect(pendingOwner).toEqual(EMPTY_BYTES); - }); - - it('should fail when initializing owner as zero', () => { - expect(() => { - ownable = new OwnablePKSimulator(utils.ZERO_KEY.left, OWNER); - }).toThrow('OwnablePK: new owner cannot be zero'); - }); - }); - - describe('with owner set', () => { - beforeEach(() => { - ownable = new OwnablePKSimulator(Z_OWNER, OWNER); - }); - - describe('owner', () => { - it('should return correct owner', () => { - expect(ownable.owner()).toEqual( - ownable.getCurrentPublicState().OwnablePK__owner, - ); - }); - - it('should return no owner', () => { - // Set owner to zero - ownable._transferOwnership(EMPTY_BYTES); - expect(ownable.owner()).toEqual(EMPTY_BYTES); - }); - }); - - describe('pendingOwner', () => { - it('should return pending owner', () => { - const nextInstance = - ownable.getCurrentPublicState().OwnablePK__instance + 1n; - const expPending = ownable.shieldOwner( - Z_NEW_OWNER, - convert_bigint_to_Uint8Array(32, nextInstance), - ); - ownable._proposeOwner(Z_NEW_OWNER); - expect(ownable.pendingOwner()).toEqual(expPending); - }); - - it('should return no pending owner', () => { - expect(ownable.pendingOwner()).toEqual(EMPTY_BYTES); - }); - }); - - describe('transferOwnership', () => { - it('should start two-step transfer', () => { - caller = OWNER; - - ownable.transferOwnership(Z_NEW_OWNER, caller); - - // Check pending owner - const nextInstance = - ownable.getCurrentPublicState().OwnablePK__instance + 1n; - const expPending = ownable.shieldOwner( - Z_NEW_OWNER, - convert_bigint_to_Uint8Array(32, nextInstance), - ); - expect(ownable.pendingOwner()).toEqual(expPending); - - // Check current owner - const thisInstance = ownable.getCurrentPublicState().OwnablePK__instance; - const expOwner = ownable.shieldOwner( - Z_OWNER, - convert_bigint_to_Uint8Array(32, thisInstance), - ); - expect(ownable.owner()).toEqual(expOwner); - }); - - it('should cancel two-step transfer', () => { - caller = OWNER; - - // Start transfer process - ownable.transferOwnership(Z_NEW_OWNER, caller); - // Cancel transfer by transferring to zero - ownable.transferOwnership(Z_ZERO, caller); - expect(ownable.pendingOwner()).toEqual(Z_ZERO.bytes); - }); - - it('should not transfer owner from unauthorized caller', () => { - caller = UNAUTHORIZED; - - expect(() => { - ownable.transferOwnership(Z_NEW_OWNER, caller); - }).toThrow('OwnablePK: not owner'); - }); - - it('should overwrite pending owner with new owner', () => { - caller = OWNER; - - ownable.transferOwnership(Z_NEW_OWNER, caller); - ownable.transferOwnership(Z_NEW_NEW_OWNER, caller); - - // Check new pending owner - const nextInstance = - ownable.getCurrentPublicState().OwnablePK__instance + 1n; - const expPending = ownable.shieldOwner( - Z_NEW_NEW_OWNER, - convert_bigint_to_Uint8Array(32, nextInstance), - ); - expect(ownable.pendingOwner()).toEqual(expPending); - }); - }); - - describe('acceptOwnership', () => { - describe('when owner is pending', () => { - beforeEach(() => { - ownable._proposeOwner(Z_NEW_OWNER); - }); - - it('should accept ownership from pending owner', () => { - caller = NEW_OWNER; - const beforeInstance = - ownable.getCurrentPublicState().OwnablePK__instance; - - ownable.acceptOwnership(caller); - - // Check instance is bumped - const afterInstance = - ownable.getCurrentPublicState().OwnablePK__instance; - expect(afterInstance).toEqual(beforeInstance + 1n); - - // Check new owner - const expOwner = ownable.shieldOwner( - Z_NEW_OWNER, - convert_bigint_to_Uint8Array(32, afterInstance), - ); - expect(ownable.owner()).toEqual(expOwner); - - // Check pending owner is reset - expect(ownable.pendingOwner()).toEqual(EMPTY_BYTES); - }); - - it('should not accept ownership from unauthorized', () => { - caller = UNAUTHORIZED; - - expect(() => { - ownable.acceptOwnership(caller); - }).toThrow('OwnablePK: caller is not pending owner'); - }); - - it('should not accept ownership from current owner', () => { - caller = OWNER; - - expect(() => { - ownable.acceptOwnership(caller); - }).toThrow('OwnablePK: caller is not pending owner'); - }); - - it('should not accept ownership from previous owner', () => { - caller = NEW_OWNER; - // Sets new owner - ownable.acceptOwnership(caller); - - // New owner proposes another new owner - ownable.transferOwnership(Z_NEW_NEW_OWNER, caller); - - // Initial owner tries to accept - caller = OWNER; - expect(() => { - ownable.acceptOwnership(caller); - }).toThrow('OwnablePK: caller is not pending owner'); - }); - }); - }); - - describe('renounceOwnership', () => { - it('should renounce ownership', () => { - caller = OWNER; - const beforeInstance = - ownable.getCurrentPublicState().OwnablePK__instance; - ownable.renounceOwnership(caller); - - expect(ownable.owner()).toEqual(EMPTY_BYTES); - expect(ownable.pendingOwner()).toEqual(EMPTY_BYTES); - expect(ownable.getCurrentPublicState().OwnablePK__instance).toEqual( - beforeInstance + 1n, - ); - }); - - it('should not renounce from unauthorized', () => { - caller = UNAUTHORIZED; - expect(() => { - ownable.renounceOwnership(caller); - }).toThrow('OwnablePK: not owner'); - }); - }); - - describe('assertOnlyOwner', () => { - it('should allow owner to call', () => { - caller = OWNER; - - ownable.assertOnlyOwner(caller); - }); - - it('should not allow unauthorized to call', () => { - caller = UNAUTHORIZED; - expect(() => { - ownable.assertOnlyOwner(caller); - }).toThrow('OwnablePK: not owner'); - }); - - it('should update who can and cannot call', () => { - caller = OWNER; - ownable.assertOnlyOwner(caller); - - caller = NEW_OWNER; - expect(() => { - ownable.assertOnlyOwner(caller); - }).toThrow('OwnablePK: not owner'); - - // Transfer to new owner - const nextInstance = - ownable.getCurrentPublicState().OwnablePK__instance + 1n; - const newOwner = ownable.shieldOwner( - Z_NEW_OWNER, - convert_bigint_to_Uint8Array(32, nextInstance), - ); - ownable._transferOwnership(newOwner); - - caller = NEW_OWNER; - ownable.assertOnlyOwner(caller); - - caller = OWNER; - expect(() => { - ownable.assertOnlyOwner(caller); - }).toThrow('OwnablePK: not owner'); - }); - }); - - describe('shieldOwner', () => { - it.skip('should hash owner correctly', () => { - const instance = convert_bigint_to_Uint8Array(32, 123n); - const _expHash = ownable.shieldOwner(Z_OWNER, instance); - // TODO add matching algo in js - }); - }); - - describe('_transferOwnership', () => { - it('should transfer ownership', () => { - const beforeInstance = - ownable.getCurrentPublicState().OwnablePK__instance; - ownable._proposeOwner(Z_NEW_NEW_OWNER); - - ownable._transferOwnership(Z_NEW_OWNER.bytes); - - // _transferownership does not shield the input so it should be a == a - expect(ownable.owner()).toEqual(Z_NEW_OWNER.bytes); - // Check instance is bumped - expect(ownable.getCurrentPublicState().OwnablePK__instance).toEqual( - beforeInstance + 1n, - ); - // Check pending owner is reset - expect(ownable.pendingOwner()).toEqual(EMPTY_BYTES); - }); - - it('should transfer ownership to zero', () => { - // _transfer does not shield the input so it should be a == a - ownable._transferOwnership(EMPTY_BYTES); - expect(ownable.owner()).toEqual(EMPTY_BYTES); - }); - }); - - describe('proposeOwner', () => { - it('should propose owner', () => { - ownable._proposeOwner(Z_NEW_OWNER); - - const nextInstance = - ownable.getCurrentPublicState().OwnablePK__instance + 1n; - const expOwner = ownable.shieldOwner( - Z_NEW_OWNER, - convert_bigint_to_Uint8Array(32, nextInstance), - ); - expect(ownable.pendingOwner()).toEqual(expOwner); - }); - - it('should propose owner and cancel', () => { - ownable._proposeOwner(Z_NEW_OWNER); - ownable._proposeOwner(utils.ZERO_KEY.left); - expect(ownable.pendingOwner()).toEqual(Z_ZERO.bytes); - }); - }); - }); -}); diff --git a/contracts/ownable/src/test/Z_OwnablePK.test.ts b/contracts/ownable/src/test/Z_OwnablePK.test.ts index e2bb0ab8..72a8763b 100644 --- a/contracts/ownable/src/test/Z_OwnablePK.test.ts +++ b/contracts/ownable/src/test/Z_OwnablePK.test.ts @@ -32,7 +32,6 @@ const INIT_COUNTER = 1n; const STATIC_NONCE = new Uint8Array(32).fill(0xab); let ownable: Z_OwnablePKSimulator; -let caller: CoinPublicKey; const createZPKCommitment = ( domain: string, @@ -42,6 +41,7 @@ const createZPKCommitment = ( ): Uint8Array => { const rt_type = new CompactTypeVector(4, new CompactTypeBytes(32)); const encoder = new TextEncoder(); + const bDomain = encoder.encode(domain); const bPK = pk.bytes; const bCounter = convert_bigint_to_Uint8Array(32, counter); @@ -53,13 +53,13 @@ describe('Z_OwnablePK', () => { it('should fail when setting owner commitment as 0', () => { expect(() => { const badCommitment = new Uint8Array(32).fill(0); - new Z_OwnablePKSimulator(badCommitment, OWNER); + new Z_OwnablePKSimulator(badCommitment); }).toThrow('Invalid parameters'); }); it('should initialize with non-zero commitment', () => { const nonZeroCommitment = new Uint8Array(32).fill(1); - ownable = new Z_OwnablePKSimulator(nonZeroCommitment, OWNER); + ownable = new Z_OwnablePKSimulator(nonZeroCommitment); expect(ownable.owner()).toEqual(nonZeroCommitment); }); @@ -68,7 +68,7 @@ describe('Z_OwnablePK', () => { describe('after initialization', () => { beforeEach(() => { const ownerCommitment = createZPKCommitment(DOMAIN, Z_OWNER, INIT_COUNTER, STATIC_NONCE); - ownable = new Z_OwnablePKSimulator(ownerCommitment, OWNER); + ownable = new Z_OwnablePKSimulator(ownerCommitment); }); describe('owner', () => { @@ -80,14 +80,15 @@ describe('Z_OwnablePK', () => { describe('assertOnlyOwner', () => { it('should allow the authorized caller with correct nonce to call', () => { - caller = OWNER; - expect(ownable.assertOnlyOwner(caller)).to.not.throw; + ownable.setCaller(OWNER); + expect(ownable.assertOnlyOwner()).to.not.throw; }); it('should fail when called by unauthorized with correct nonce', () => { - caller = UNAUTHORIZED; + ownable.setCaller(UNAUTHORIZED); + expect(() => { - ownable.assertOnlyOwner(caller); + ownable.assertOnlyOwner(); }).toThrow('Forbidden'); }); }); diff --git a/contracts/ownable/src/test/mocks/MockOwnablePK.compact b/contracts/ownable/src/test/mocks/MockOwnablePK.compact deleted file mode 100644 index 1ab40018..00000000 --- a/contracts/ownable/src/test/mocks/MockOwnablePK.compact +++ /dev/null @@ -1,49 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma language_version >= 0.15.0; - -import CompactStandardLibrary; -import "../../OwnablePK" prefix OwnablePK_; - -export { ZswapCoinPublicKey, ContractAddress, Either, Maybe }; -export { OwnablePK__owner, OwnablePK__pendingOwner, OwnablePK__instance }; - -constructor(initOwner: ZswapCoinPublicKey) { - OwnablePK_initializer(initOwner); -} - -export circuit owner(): Bytes<32> { - return OwnablePK_owner(); -} - -export circuit pendingOwner(): Bytes<32> { - return OwnablePK_pendingOwner(); -} - -export circuit transferOwnership(newOwner: ZswapCoinPublicKey): [] { - return OwnablePK_transferOwnership(disclose(newOwner)); -} - -export circuit acceptOwnership(): [] { - return OwnablePK_acceptOwnership(); -} - -export circuit renounceOwnership(): [] { - return OwnablePK_renounceOwnership(); -} - -export circuit assertOnlyOwner(): [] { - return OwnablePK_assertOnlyOwner(); -} - -export circuit shieldOwner(ownerPK: ZswapCoinPublicKey, instance: Bytes<32>): Bytes<32> { - return OwnablePK_shieldOwner(ownerPK, instance); -} - -export circuit _transferOwnership(newOwner: Bytes<32>): [] { - return OwnablePK__transferOwnership(newOwner); -} - -export circuit _proposeOwner(newOwner: ZswapCoinPublicKey): [] { - return OwnablePK__proposeOwner(disclose(newOwner)); -} diff --git a/contracts/ownable/src/test/simulators/OwnablePKSimulator.ts b/contracts/ownable/src/test/simulators/OwnablePKSimulator.ts deleted file mode 100644 index 60731080..00000000 --- a/contracts/ownable/src/test/simulators/OwnablePKSimulator.ts +++ /dev/null @@ -1,223 +0,0 @@ -import { - type CircuitContext, - type CoinPublicKey, - type ContractState, - constructorContext, - emptyZswapLocalState, - QueryContext, -} from '@midnight-ntwrk/compact-runtime'; -import { sampleContractAddress } from '@midnight-ntwrk/zswap'; -import type { ZswapCoinPublicKey } from '../../artifacts/MockOwnablePK/contract/index.cjs'; -import { - type Ledger, - ledger, - Contract as MockOwnable, -} from '../../artifacts/MockOwnablePK/contract/index.cjs'; // Combined imports -import { - type OwnablePKPrivateState, - OwnablePKWitnesses, -} from '../../witnesses/OwnablePKWitnesses.js'; -import type { IContractSimulator } from '../types/test.js'; - -/** - * @description A simulator implementation of a contract for testing purposes. - * @template P - The private state type, fixed to OwnablePKPrivateState. - * @template L - The ledger type, fixed to Contract.Ledger. - */ -export class OwnablePKSimulator - implements IContractSimulator -{ - /** @description The underlying contract instance managing contract logic. */ - readonly contract: MockOwnable; - - /** @description The deployed address of the contract. */ - readonly contractAddress: string; - - /** @description The deployer address of the contract. */ - readonly deployer: CoinPublicKey; - - /** @description The current circuit context, updated by contract operations. */ - circuitContext: CircuitContext; - - /** - * @description Initializes the mock contract. - */ - constructor(initOwner: ZswapCoinPublicKey, deployer: CoinPublicKey) { - this.contract = new MockOwnable(OwnablePKWitnesses); - this.deployer = deployer; - const { - currentPrivateState, - currentContractState, - currentZswapLocalState, - } = this.contract.initialState(constructorContext({}, deployer), initOwner); - this.circuitContext = { - currentPrivateState, - currentZswapLocalState, - originalState: currentContractState, - transactionContext: new QueryContext( - currentContractState.data, - sampleContractAddress(), - ), - }; - this.contractAddress = this.circuitContext.transactionContext.address; - } - - /** - * @description Retrieves the current public ledger state of the contract. - * @returns The ledger state as defined by the contract. - */ - public getCurrentPublicState(): Ledger { - return ledger(this.circuitContext.transactionContext.state); - } - - /** - * @description Retrieves the current private state of the contract. - * @returns The private state of type OwnablePKPrivateState. - */ - public getCurrentPrivateState(): OwnablePKPrivateState { - return this.circuitContext.currentPrivateState; - } - - /** - * @description Retrieves the current contract state. - * @returns The contract state object. - */ - public getCurrentContractState(): ContractState { - return this.circuitContext.originalState; - } - - /** - * @description Returns the shielded owner. - * @returns The shielded owner. - */ - public owner(): Uint8Array { - return this.contract.impureCircuits.owner(this.circuitContext).result; - } - - /** - * @description Returns the shielded pending owner. - * @returns The shielded proposed owner. - */ - public pendingOwner(): Uint8Array { - return this.contract.impureCircuits.pendingOwner(this.circuitContext) - .result; - } - - /** - * @description Initiates the two-step ownership transfer to `newOwner`. - */ - public transferOwnership( - newOwner: ZswapCoinPublicKey, - sender: CoinPublicKey, - ): CircuitContext { - const res = this.contract.impureCircuits.transferOwnership( - { - ...this.circuitContext, - currentZswapLocalState: sender - ? emptyZswapLocalState(sender) - : this.circuitContext.currentZswapLocalState, - }, - newOwner, - ); - - this.circuitContext = res.context; - return this.circuitContext; - } - - /** - * @description Finishes the two-step ownership transfer process by accepting - * the ownership. Can only be called by the pending owner. - */ - public acceptOwnership( - sender: CoinPublicKey, - ): CircuitContext { - const res = this.contract.impureCircuits.acceptOwnership({ - ...this.circuitContext, - currentZswapLocalState: sender - ? emptyZswapLocalState(sender) - : this.circuitContext.currentZswapLocalState, - }); - - this.circuitContext = res.context; - return this.circuitContext; - } - - /** - * @description Leaves the contract without an owner. It will not be - * possible to call `assertOnlyOnwer` circuits anymore. Can only be - * called by the current owner. - */ - public renounceOwnership( - sender: CoinPublicKey, - ): CircuitContext { - const res = this.contract.impureCircuits.renounceOwnership({ - ...this.circuitContext, - currentZswapLocalState: sender - ? emptyZswapLocalState(sender) - : this.circuitContext.currentZswapLocalState, - }); - - this.circuitContext = res.context; - return this.circuitContext; - } - - /** - * @description Throws if called by any account other than the owner. - * Use this to restrict access to sensitive circuits. - */ - public assertOnlyOwner( - sender: CoinPublicKey, - ): CircuitContext { - const res = this.contract.impureCircuits.assertOnlyOwner({ - ...this.circuitContext, - currentZswapLocalState: sender - ? emptyZswapLocalState(sender) - : this.circuitContext.currentZswapLocalState, - }); - - this.circuitContext = res.context; - return this.circuitContext; - } - - /** - * @description Obfuscates the `ownerPK` be hashing it with a domain separator and - * the passed `instance`. - * @returns The shielded hash of the owner and instance. - */ - public shieldOwner( - ownerPK: ZswapCoinPublicKey, - instance: Uint8Array, - ): Uint8Array { - return this.contract.circuits.shieldOwner( - this.circuitContext, - ownerPK, - instance, - ).result; - } - - /** - * @description Internal circuit that transfers ownership of the contract to `newOwner`. - */ - public _transferOwnership( - newOwner: Uint8Array, - ): CircuitContext { - this.circuitContext = this.contract.impureCircuits._transferOwnership( - this.circuitContext, - newOwner, - ).context; - return this.circuitContext; - } - - /** - * @description Internal circuit that sets the pending owner. - */ - public _proposeOwner( - newOwner: ZswapCoinPublicKey, - ): CircuitContext { - this.circuitContext = this.contract.impureCircuits._proposeOwner( - this.circuitContext, - newOwner, - ).context; - return this.circuitContext; - } -} diff --git a/contracts/ownable/src/test/simulators/Z_OwnablePKSimulator.ts b/contracts/ownable/src/test/simulators/Z_OwnablePKSimulator.ts index 1ee73716..c2b6ecc0 100644 --- a/contracts/ownable/src/test/simulators/Z_OwnablePKSimulator.ts +++ b/contracts/ownable/src/test/simulators/Z_OwnablePKSimulator.ts @@ -2,9 +2,7 @@ import { type CircuitContext, type CoinPublicKey, type ContractState, - constructorContext, emptyZswapLocalState, - QueryContext, } from '@midnight-ntwrk/compact-runtime'; import { sampleContractAddress } from '@midnight-ntwrk/zswap'; import type { ZswapCoinPublicKey } from '../../artifacts/MockZ_OwnablePK/contract/index.cjs'; @@ -17,53 +15,159 @@ import { Z_OwnablePKPrivateState, Z_OwnablePKWitnesses, } from '../../witnesses/Z_OwnablePKWitnesses.js'; -import type { IContractSimulator } from '../types/test.js'; +import { AbstractContractSimulator } from '../types/AbstractContractSimulator.js'; +import { SimulatorStateManager } from '../types/SimualatorStateManager.js'; +import { ContextlessCircuits, ExtractImpureCircuits, ExtractPureCircuits } from '../types/test.js'; + /** * @description A simulator implementation of a contract for testing purposes. * @template P - The private state type, fixed to Z_OwnablePKPrivateState. * @template L - The ledger type, fixed to Contract.Ledger. */ -export class Z_OwnablePKSimulator - implements IContractSimulator -{ - /** @description The underlying contract instance managing contract logic. */ +export class Z_OwnablePKSimulator extends AbstractContractSimulator< + Z_OwnablePKPrivateState, + Ledger +> { readonly contract: MockOwnable; - - /** @description The deployed address of the contract. */ readonly contractAddress: string; + private stateManager: SimulatorStateManager; + private callerOverride: CoinPublicKey | null = null; + + private _pureCircuitProxy?: ContextlessCircuits< + ExtractPureCircuits>, + Z_OwnablePKPrivateState + >; + + private _impureCircuitProxy?: ContextlessCircuits< + ExtractImpureCircuits>, + Z_OwnablePKPrivateState + >; + + constructor(initOwner: Uint8Array) { + super(); + this.contract = new MockOwnable( + Z_OwnablePKWitnesses(), + ); + // Setup initial state + const privateState: Z_OwnablePKPrivateState = Z_OwnablePKPrivateState.generate(); + const coinPK = '0'.repeat(64); + const address = sampleContractAddress(); + const constructorArgs = [initOwner]; + + this.stateManager = new SimulatorStateManager( + this.contract, + privateState, + coinPK, + address, + ...constructorArgs, + ); + this.contractAddress = this.circuitContext.transactionContext.address; + } - /** @description The deployer address of the contract. */ - readonly deployer: CoinPublicKey; + get circuitContext() { + return this.stateManager.getContext(); + } + + set circuitContext(ctx) { + this.stateManager.setContext(ctx); + } - /** @description The current circuit context, updated by contract operations. */ - circuitContext: CircuitContext; + getPublicState(): Ledger { + return ledger(this.circuitContext.transactionContext.state); + } /** - * @description Initializes the mock contract. + * @description Constructs a caller-specific circuit context. + * If a caller override is present, it replaces the current Zswap local state with an empty one + * scoped to the overridden caller. Otherwise, the existing context is reused as-is. + * @returns A circuit context adjusted for the current simulated caller. */ - constructor(initOwner: Uint8Array, deployer: CoinPublicKey) { - this.contract = new MockOwnable(Z_OwnablePKWitnesses()); - this.deployer = deployer; - - const { - currentPrivateState, - currentContractState, - currentZswapLocalState, - } = this.contract.initialState( - constructorContext(Z_OwnablePKPrivateState.generate(), deployer), - initOwner - ); - this.circuitContext = { - currentPrivateState, - currentZswapLocalState, - originalState: currentContractState, - transactionContext: new QueryContext( - currentContractState.data, - sampleContractAddress(), - ), + protected getCallerContext(): CircuitContext { + return { + ...this.circuitContext, + currentZswapLocalState: this.callerOverride + ? emptyZswapLocalState(this.callerOverride) + : this.circuitContext.currentZswapLocalState, + }; + } + + /** + * @description Initializes and returns a proxy to pure contract circuits. + * The proxy automatically injects the current circuit context into each call, + * and returns only the result portion of each circuit's output. + * @notice The proxy is created only when first accessed a.k.a lazy initialization. + * This approach is efficient in cases where only pure or only impure circuits are used, + * avoiding unnecessary proxy creation. + * @returns A proxy object exposing pure circuit functions without requiring explicit context. + */ + protected get pureCircuit(): ContextlessCircuits< + ExtractPureCircuits>, + Z_OwnablePKPrivateState + > { + if (!this._pureCircuitProxy) { + this._pureCircuitProxy = this.createPureCircuitProxy< + MockOwnable['circuits'] + >(this.contract.circuits, () => this.circuitContext); + } + return this._pureCircuitProxy; + } + + /** + * @description Initializes and returns a proxy to impure contract circuits. + * The proxy automatically injects the current (possibly caller-modified) context into each call, + * and updates the circuit context with the one returned by the circuit after execution. + * @notice The proxy is created only when first accessed a.k.a. lazy initialization. + * This approach is efficient in cases where only pure or only impure circuits are used, + * avoiding unnecessary proxy creation. + * @returns A proxy object exposing impure circuit functions without requiring explicit context management. + */ + protected get impureCircuit(): ContextlessCircuits< + ExtractImpureCircuits>, + Z_OwnablePKPrivateState + > { + if (!this._impureCircuitProxy) { + this._impureCircuitProxy = this.createImpureCircuitProxy< + MockOwnable['impureCircuits'] + >( + this.contract.impureCircuits, + () => this.getCallerContext(), + (ctx: any) => { + this.circuitContext = ctx; + }, + ); + } + return this._impureCircuitProxy; + } + + /** + * @description Sets the caller context. + * @param caller The caller in context of the proceeding circuit calls. + */ + public setCaller(caller: CoinPublicKey | null): void { + this.callerOverride = caller; + } + + /** + * @description Resets the cached circuit proxy instances. + * This is useful if the underlying contract state or circuit context has changed, + * and you want to ensure the proxies are recreated with updated context on next access. + */ + public resetCircuitProxies(): void { + this._pureCircuitProxy = undefined; + this._impureCircuitProxy = undefined; + } + + /** + * @description Helper method that provides access to both pure and impure circuit proxies. + * These proxies automatically inject the appropriate circuit context when invoked. + * @returns An object containing `pure` and `impure` circuit proxy interfaces. + */ + public get circuits() { + return { + pure: this.pureCircuit, + impure: this.impureCircuit, }; - this.contractAddress = this.circuitContext.transactionContext.address; } /** @@ -95,28 +199,16 @@ export class Z_OwnablePKSimulator * @returns The shielded owner. */ public owner(): Uint8Array { - return this.contract.impureCircuits.owner(this.circuitContext).result; + return this.circuits.impure.owner(); } /** - * @description Initiates the two-step ownership transfer to `newOwner`. + * @description */ public transferOwnership( newOwner: Uint8Array, - sender: CoinPublicKey, - ): CircuitContext { - const res = this.contract.impureCircuits.transferOwnership( - { - ...this.circuitContext, - currentZswapLocalState: sender - ? emptyZswapLocalState(sender) - : this.circuitContext.currentZswapLocalState, - }, - newOwner, - ); - - this.circuitContext = res.context; - return this.circuitContext; + ) { + this.circuits.impure.transferOwnership(newOwner); } /** @@ -124,66 +216,35 @@ export class Z_OwnablePKSimulator * possible to call `assertOnlyOnwer` circuits anymore. Can only be * called by the current owner. */ - public renounceOwnership( - sender: CoinPublicKey, - ): CircuitContext { - const res = this.contract.impureCircuits.renounceOwnership({ - ...this.circuitContext, - currentZswapLocalState: sender - ? emptyZswapLocalState(sender) - : this.circuitContext.currentZswapLocalState, - }); - - this.circuitContext = res.context; - return this.circuitContext; + public renounceOwnership() { + this.circuits.impure.renounceOwnership(); } /** * @description Throws if called by any account other than the owner. * Use this to restrict access to sensitive circuits. */ - public assertOnlyOwner( - sender: CoinPublicKey, - ): CircuitContext { - const res = this.contract.impureCircuits.assertOnlyOwner({ - ...this.circuitContext, - currentZswapLocalState: sender - ? emptyZswapLocalState(sender) - : this.circuitContext.currentZswapLocalState, - }); - - this.circuitContext = res.context; - return this.circuitContext; + public assertOnlyOwner() { + this.circuits.impure.assertOnlyOwner(); } /** - * @description Obfuscates the `ownerPK` be hashing it with a domain separator and + * @description Obfuscates the `pk` be hashing it with a domain separator and * the passed `instance`. * @returns The shielded hash of the owner and instance. */ public shieldPK( - ownerPK: ZswapCoinPublicKey, + pk: ZswapCoinPublicKey, instance: bigint, nonce: Uint8Array ): Uint8Array { - return this.contract.circuits.shieldPK( - this.circuitContext, - ownerPK, - instance, - nonce - ).result; + return this.circuits.pure.shieldPK(pk, instance, nonce); } /** * @description Internal circuit that transfers ownership of the contract to `newOwner`. */ - public _transferOwnership( - newOwner: Uint8Array, - ): CircuitContext { - this.circuitContext = this.contract.impureCircuits._transferOwnership( - this.circuitContext, - newOwner, - ).context; - return this.circuitContext; + public _transferOwnership(newOwnerCommitment: Uint8Array) { + this.circuits.impure._transferOwnership(newOwnerCommitment); } } diff --git a/contracts/ownable/src/test/types/AbstractContractSimulator.ts b/contracts/ownable/src/test/types/AbstractContractSimulator.ts new file mode 100644 index 00000000..8aec6a28 --- /dev/null +++ b/contracts/ownable/src/test/types/AbstractContractSimulator.ts @@ -0,0 +1,131 @@ +import type { + CircuitContext, + ContractState, +} from '@midnight-ntwrk/compact-runtime'; +import type { ContextlessCircuits, IContractSimulator } from './test.js'; + +/** + * Abstract base class for simulating contract behavior. + * Provides common functionality for managing circuit contexts and creating proxies + * for pure and impure circuit functions. + * + * @template P - The type representing the private state of the contract. + * @template L - The type representing the public ledger (contract) state. + */ +export abstract class AbstractContractSimulator + implements IContractSimulator +{ + /** + * The deployed contract's address. + * Must be implemented by concrete subclasses. + */ + abstract readonly contractAddress: string; + + /** + * The current circuit context containing private state, contract state, and transaction context. + * Must be implemented by concrete subclasses. + */ + abstract circuitContext: CircuitContext

; + + /** + * Retrieves the current public ledger state of the contract. + * Must be implemented by concrete subclasses. + * + * @returns The current public ledger state. + */ + abstract getPublicState(): L; + + /** + * Retrieves the current private state from the circuit context. + * + * @returns The current private state of the contract. + */ + public getPrivateState(): P { + return this.circuitContext.currentPrivateState; + } + + /** + * Retrieves the original contract state from the circuit context. + * + * @returns The original contract state. + */ + public getContractState(): ContractState { + return this.circuitContext.originalState; + } + + /** + * Creates a proxy wrapper around pure circuits. + * Pure circuits do not modify contract state, so only the result is returned. + * + * @template Circuits - The type of the circuits object to proxy. + * @param circuits - The original circuits object containing functions accepting a CircuitContext. + * @param context - A function returning the current CircuitContext to pass to circuit functions. + * @returns A proxy with contextless circuits that accept the original arguments and return only results. + */ + protected createPureCircuitProxy( + circuits: Circuits, + context: () => CircuitContext

, + ): ContextlessCircuits { + return new Proxy(circuits, { + get(target, prop, receiver) { + const original = Reflect.get(target, prop, receiver); + + if (typeof original !== 'function') return original; + + return (...args: any[]) => { + const ctx = context(); + + const fn = original as ( + ctx: CircuitContext

, + ...args: any[] + ) => { result: any }; + + return fn(ctx, ...args).result; + }; + }, + }) as ContextlessCircuits; + } + + /** + * Creates a proxy wrapper around impure circuits. + * Impure circuits can modify contract state, so the circuit context is updated accordingly. + * + * @template Circuits - The type of the circuits object to proxy. + * @param circuits - The original circuits object containing functions accepting a CircuitContext. + * @param context - A function returning the current CircuitContext to pass to circuit functions. + * @param updateContext - A callback to update the circuit context with the new context returned by the circuit. + * @returns A proxy with contextless circuits that accept the original arguments, update context, and return results. + */ + protected createImpureCircuitProxy( + circuits: Circuits, + context: () => CircuitContext

, + updateContext: (ctx: CircuitContext

) => void, + ): ContextlessCircuits { + return new Proxy(circuits, { + get(target, prop, receiver) { + const original = Reflect.get(target, prop, receiver); + + if (typeof original !== 'function') return original; + + return (...args: any[]) => { + const ctx = context(); + + const fn = original as ( + ctx: CircuitContext

, + ...args: any[] + ) => { result: any; context: CircuitContext

}; + + const { result, context: newCtx } = fn(ctx, ...args); + updateContext(newCtx); + return result; + }; + }, + }) as ContextlessCircuits; + } + + /** + * Optional method to reset any cached circuit proxies. + * Concrete subclasses can override this to clear proxies if needed. + */ + public resetCircuitProxies?(): void {} +} diff --git a/contracts/ownable/src/test/types/SimualatorStateManager.ts b/contracts/ownable/src/test/types/SimualatorStateManager.ts new file mode 100644 index 00000000..943ecf35 --- /dev/null +++ b/contracts/ownable/src/test/types/SimualatorStateManager.ts @@ -0,0 +1,114 @@ +import { + type CircuitContext, + type ConstructorContext, + type ContractState, + constructorContext, + QueryContext, +} from '@midnight-ntwrk/compact-runtime'; + +/** + * A composable utility class for managing Compact contract state in simulations. + * + * This class handles initialization and lifecycle management of the `CircuitContext`, + * which includes private state, public (ledger) state, zswap local state, and transaction context. + * + * It is designed to be embedded compositionally inside contract simulator classes + * (e.g., `FooSimulator`), enabling better separation of concerns and easier test setup. + * + * @template P - The type of the contract's private state. + * + * ### Responsibilities + * - Initializes the contract state using the compiled contract's `.initialState` method. + * - Stores and exposes the `CircuitContext` via getters/setters. + * - Supports injection of private state and contract constructor arguments. + * - Allows the owning simulator to update private state manually during testing. + * + * ### Example Usage: + * ```ts + * const contract = new MyContract(witnesses); + * const manager = new SimulatorStateManager( + * contract, + * { foo: 1n }, // initial private state + * '0'.repeat(64), // coin public key + * undefined, // optional contract address + * arg1, arg2 // additional constructor args + * ); + * + * const context = manager.getContext(); + * ``` + */ +export class SimulatorStateManager

{ + private context: CircuitContext

; + + /** + * Creates an instance of `SimulatorStateManager`. + * + * @param contract - A compiled Compact contract instance (from artifacts), exposing `initialState()`. + * @param privateState - The initial private state to inject into the contract. + * @param coinPK - The caller's coin public key (used to create the constructor context and as default address). + * @param contractAddress - Optional override for the contract's address. Defaults to `coinPK` if not provided. + * @param contractArgs - Additional arguments to pass to the contract constructor (e.g., circuit params). + */ + constructor( + contract: { + initialState: ( + ctx: ConstructorContext

, + ...args: any[] + ) => { + currentPrivateState: P; + currentContractState: ContractState; + currentZswapLocalState: any; + }; + }, + privateState: P, + coinPK: string, + contractAddress?: string, + ...contractArgs: any[] + ) { + const initCtx = constructorContext(privateState, coinPK); + + const { + currentPrivateState, + currentContractState, + currentZswapLocalState, + } = contract.initialState(initCtx, ...contractArgs); + + this.context = { + currentPrivateState, + currentZswapLocalState, + originalState: currentContractState, + transactionContext: new QueryContext( + currentContractState.data, + contractAddress ?? coinPK, + ), + }; + } + + /** + * Retrieves the current `CircuitContext`, including private state, + * zswap state, contract state, and transaction context. + */ + getContext(): CircuitContext

{ + return this.context; + } + + /** + * Replaces the internal `CircuitContext` with a new one. + * + * Useful when circuits mutate state and return an updated context. + */ + setContext(newContext: CircuitContext

) { + this.context = newContext; + } + + /** + * Updates just the private state inside the existing context. + * + * This is a lightweight way to simulate local state changes without reconstructing the full context. + * + * @param newPrivateState - The new private state object to apply. + */ + updatePrivateState(newPrivateState: P) { + this.context.currentPrivateState = newPrivateState; + } +} diff --git a/contracts/ownable/src/test/types/test.ts b/contracts/ownable/src/test/types/test.ts index 7a909543..c5511715 100644 --- a/contracts/ownable/src/test/types/test.ts +++ b/contracts/ownable/src/test/types/test.ts @@ -4,23 +4,92 @@ import type { } from '@midnight-ntwrk/compact-runtime'; /** - * Generic interface for mock contract implementations. - * @template P - The type of the contract's private state. - * @template L - The type of the contract's ledger (public state). + * Interface defining a generic contract simulator. + * + * @template P - Type representing the private contract state. + * @template L - Type representing the public ledger state. */ export interface IContractSimulator { - /** The contract's deployed address. */ + /** + * The deployed contract's address. + */ readonly contractAddress: string; - /** The current circuit context. */ + /** + * The current circuit context holding the contract state. + */ circuitContext: CircuitContext

; - /** Retrieves the current ledger state. */ - getCurrentPublicState(): L; + /** + * Returns the current public ledger state. + * + * @returns The current ledger state of type L. + */ + getPublicState(): L; - /** Retrieves the current private state. */ - getCurrentPrivateState(): P; + /** + * Returns the current private contract state. + * + * @returns The current private state of type P. + */ + getPrivateState(): P; - /** Retrieves the current contract state. */ - getCurrentContractState(): ContractState; + /** + * Returns the original contract state. + * + * @returns The current contract state. + */ + getContractState(): ContractState; } + +/** + * Extracts pure circuits from a contract type. + * + * Pure circuits are those in `circuits` but not in `impureCircuits`. + * + * @template TContract - Contract type with `circuits` and `impureCircuits`. + */ +export type ExtractPureCircuits = TContract extends { + circuits: infer TCircuits; + impureCircuits: infer TImpureCircuits; +} + ? Omit + : never; + +/** + * Extracts impure circuits from a contract type. + * + * Impure circuits are those in `impureCircuits`. + * + * @template TContract - Contract type with `circuits` and `impureCircuits`. + */ +export type ExtractImpureCircuits = TContract extends { + impureCircuits: infer TImpureCircuits; +} + ? TImpureCircuits + : never; + +/** + * Transforms a collection of circuit functions by removing the explicit `CircuitContext` parameter, + * producing a version of each function that can be called without passing the context explicitly. + * + * Each original circuit function is expected to have the signature: + * `(ctx: CircuitContext, ...args) => { result: R; context: CircuitContext }` + * or a compatible shape. + * + * The transformed type maps each key `K` of the input `Circuits` type to a new function + * that takes the same parameters as the original, *except* the first `CircuitContext` argument, + * and returns the `result` part `R` directly. + * + * @template Circuits - An object type whose values are circuit functions accepting a `CircuitContext` + * and returning an object with `result` and optionally `context`. + * @template TState - The type representing the private or contract state passed inside `CircuitContext`. + */ +export type ContextlessCircuits = { + [K in keyof Circuits]: Circuits[K] extends ( + ctx: CircuitContext, + ...args: infer P + ) => { result: infer R; context: CircuitContext } + ? (...args: P) => R + : never; +}; diff --git a/contracts/ownable/src/witnesses/OwnablePKWitnesses.ts b/contracts/ownable/src/witnesses/OwnablePKWitnesses.ts deleted file mode 100644 index 4976e327..00000000 --- a/contracts/ownable/src/witnesses/OwnablePKWitnesses.ts +++ /dev/null @@ -1,3 +0,0 @@ -// This is how we type an empty object. -export type OwnablePKPrivateState = Record; -export const OwnablePKWitnesses = {}; From 97cc94ef48d9c19c894ff7719a459dca232953c9 Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 8 Aug 2025 21:41:20 -0300 Subject: [PATCH 067/322] move helper test files --- .../AbstractContractSimulator.ts | 2 +- .../SimualatorStateManager.ts | 0 .../src/test/utils/createCircuitProxies.ts | 49 +++++++++++++++++++ contracts/ownable/src/test/utils/test.ts | 4 +- 4 files changed, 52 insertions(+), 3 deletions(-) rename contracts/ownable/src/test/{types => utils}/AbstractContractSimulator.ts (98%) rename contracts/ownable/src/test/{types => utils}/SimualatorStateManager.ts (100%) create mode 100644 contracts/ownable/src/test/utils/createCircuitProxies.ts diff --git a/contracts/ownable/src/test/types/AbstractContractSimulator.ts b/contracts/ownable/src/test/utils/AbstractContractSimulator.ts similarity index 98% rename from contracts/ownable/src/test/types/AbstractContractSimulator.ts rename to contracts/ownable/src/test/utils/AbstractContractSimulator.ts index 8aec6a28..36ac5d73 100644 --- a/contracts/ownable/src/test/types/AbstractContractSimulator.ts +++ b/contracts/ownable/src/test/utils/AbstractContractSimulator.ts @@ -2,7 +2,7 @@ import type { CircuitContext, ContractState, } from '@midnight-ntwrk/compact-runtime'; -import type { ContextlessCircuits, IContractSimulator } from './test.js'; +import type { ContextlessCircuits, IContractSimulator } from '../types/test.js'; /** * Abstract base class for simulating contract behavior. diff --git a/contracts/ownable/src/test/types/SimualatorStateManager.ts b/contracts/ownable/src/test/utils/SimualatorStateManager.ts similarity index 100% rename from contracts/ownable/src/test/types/SimualatorStateManager.ts rename to contracts/ownable/src/test/utils/SimualatorStateManager.ts diff --git a/contracts/ownable/src/test/utils/createCircuitProxies.ts b/contracts/ownable/src/test/utils/createCircuitProxies.ts new file mode 100644 index 00000000..b4a14ab0 --- /dev/null +++ b/contracts/ownable/src/test/utils/createCircuitProxies.ts @@ -0,0 +1,49 @@ +import type { CircuitContext } from '@midnight-ntwrk/compact-runtime'; +import type { + ContextlessCircuits, + ExtractImpureCircuits, + ExtractPureCircuits +} from '../types/test.js'; + +/** + * Creates lazily-initialized circuit proxies for pure and impure contract functions. + */ +export function createCircuitProxies( + contract: ContractType, + getContext: () => CircuitContext

, + getCallerContext: () => CircuitContext

, + updateContext: (ctx: CircuitContext

) => void, + createPureProxy: ( + circuits: C, + context: () => CircuitContext

, + ) => ContextlessCircuits, + createImpureProxy: ( + circuits: C, + context: () => CircuitContext

, + updateContext: (ctx: CircuitContext

) => void, + ) => ContextlessCircuits, +) { + let pureProxy: ContextlessCircuits, P> | undefined; + let impureProxy: ContextlessCircuits, P> | undefined; + + return { + get circuits() { + return { + pure: + pureProxy ?? + (pureProxy = createPureProxy(contract.circuits, getContext)), + impure: + impureProxy ?? + (impureProxy = createImpureProxy( + contract.impureCircuits, + getCallerContext, + updateContext, + )), + }; + }, + resetProxies() { + pureProxy = undefined; + impureProxy = undefined; + }, + }; +} diff --git a/contracts/ownable/src/test/utils/test.ts b/contracts/ownable/src/test/utils/test.ts index 2fd5a504..52e92528 100644 --- a/contracts/ownable/src/test/utils/test.ts +++ b/contracts/ownable/src/test/utils/test.ts @@ -55,8 +55,8 @@ export function useCircuitContextSender< L, C extends IContractSimulator, >(contract: C, sender: CoinPublicKey): CircuitContext

{ - const currentPrivateState = contract.getCurrentPrivateState(); - const originalState = contract.getCurrentContractState(); + const currentPrivateState = contract.getPrivateState(); + const originalState = contract.getContractState(); const contractAddress = contract.contractAddress; return { From 74945e0bbd93b53914be0516654fb292468ab062 Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 8 Aug 2025 21:41:35 -0300 Subject: [PATCH 068/322] update simulators --- .../src/test/simulators/OwnableSimulator.ts | 6 ++-- .../test/simulators/Z_OwnablePKSimulator.ts | 28 ++----------------- 2 files changed, 5 insertions(+), 29 deletions(-) diff --git a/contracts/ownable/src/test/simulators/OwnableSimulator.ts b/contracts/ownable/src/test/simulators/OwnableSimulator.ts index 31e18127..1fbd380e 100644 --- a/contracts/ownable/src/test/simulators/OwnableSimulator.ts +++ b/contracts/ownable/src/test/simulators/OwnableSimulator.ts @@ -71,7 +71,7 @@ export class OwnableSimulator * @description Retrieves the current public ledger state of the contract. * @returns The ledger state as defined by the contract. */ - public getCurrentPublicState(): Ledger { + public getPublicState(): Ledger { return ledger(this.circuitContext.transactionContext.state); } @@ -79,7 +79,7 @@ export class OwnableSimulator * @description Retrieves the current private state of the contract. * @returns The private state of type OwnablePrivateState. */ - public getCurrentPrivateState(): OwnablePrivateState { + public getPrivateState(): OwnablePrivateState { return this.circuitContext.currentPrivateState; } @@ -87,7 +87,7 @@ export class OwnableSimulator * @description Retrieves the current contract state. * @returns The contract state object. */ - public getCurrentContractState(): ContractState { + public getContractState(): ContractState { return this.circuitContext.originalState; } diff --git a/contracts/ownable/src/test/simulators/Z_OwnablePKSimulator.ts b/contracts/ownable/src/test/simulators/Z_OwnablePKSimulator.ts index c2b6ecc0..e6fd0dfd 100644 --- a/contracts/ownable/src/test/simulators/Z_OwnablePKSimulator.ts +++ b/contracts/ownable/src/test/simulators/Z_OwnablePKSimulator.ts @@ -15,8 +15,8 @@ import { Z_OwnablePKPrivateState, Z_OwnablePKWitnesses, } from '../../witnesses/Z_OwnablePKWitnesses.js'; -import { AbstractContractSimulator } from '../types/AbstractContractSimulator.js'; -import { SimulatorStateManager } from '../types/SimualatorStateManager.js'; +import { AbstractContractSimulator } from '../utils/AbstractContractSimulator.js'; +import { SimulatorStateManager } from '../utils/SimualatorStateManager.js'; import { ContextlessCircuits, ExtractImpureCircuits, ExtractPureCircuits } from '../types/test.js'; @@ -170,30 +170,6 @@ export class Z_OwnablePKSimulator extends AbstractContractSimulator< }; } - /** - * @description Retrieves the current public ledger state of the contract. - * @returns The ledger state as defined by the contract. - */ - public getCurrentPublicState(): Ledger { - return ledger(this.circuitContext.transactionContext.state); - } - - /** - * @description Retrieves the current private state of the contract. - * @returns The private state of type Z_OwnablePKPrivateState. - */ - public getCurrentPrivateState(): Z_OwnablePKPrivateState { - return this.circuitContext.currentPrivateState; - } - - /** - * @description Retrieves the current contract state. - * @returns The contract state object. - */ - public getCurrentContractState(): ContractState { - return this.circuitContext.originalState; - } - /** * @description Returns the shielded owner. * @returns The shielded owner. From 6698dcccb8182427fdfde293aa588361eef8fdd5 Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 8 Aug 2025 22:24:07 -0300 Subject: [PATCH 069/322] fix import --- contracts/ownable/src/test/simulators/OwnableSimulator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/ownable/src/test/simulators/OwnableSimulator.ts b/contracts/ownable/src/test/simulators/OwnableSimulator.ts index 1fbd380e..82778ad6 100644 --- a/contracts/ownable/src/test/simulators/OwnableSimulator.ts +++ b/contracts/ownable/src/test/simulators/OwnableSimulator.ts @@ -19,7 +19,7 @@ import { type OwnablePrivateState, OwnableWitnesses, } from '../../witnesses/OwnableWitnesses.js'; -import type { IContractSimulator } from '../types/test.js'; +import { IContractSimulator } from '../types/test.js'; /** * @description A simulator implementation of a Ownable contract for testing purposes. From 0e657a1e0fa4d981fff33f54755334cb88379e8d Mon Sep 17 00:00:00 2001 From: andrew Date: Sat, 9 Aug 2025 16:01:37 -0300 Subject: [PATCH 070/322] add sim options type --- contracts/ownable/src/test/types/test.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/contracts/ownable/src/test/types/test.ts b/contracts/ownable/src/test/types/test.ts index c5511715..48da9c81 100644 --- a/contracts/ownable/src/test/types/test.ts +++ b/contracts/ownable/src/test/types/test.ts @@ -93,3 +93,10 @@ export type ContextlessCircuits = { ? (...args: P) => R : never; }; + +export type SimulatorOptions any> = { + address?: string; + coinPK?: string; + privateState?: PS; + witnesses?: ReturnType; +}; From 5eb449978e7ff617e8bf69fe128f62614f62ebec Mon Sep 17 00:00:00 2001 From: andrew Date: Sat, 9 Aug 2025 16:02:11 -0300 Subject: [PATCH 071/322] fix generate nonce in witness --- contracts/ownable/src/witnesses/Z_OwnablePKWitnesses.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/contracts/ownable/src/witnesses/Z_OwnablePKWitnesses.ts b/contracts/ownable/src/witnesses/Z_OwnablePKWitnesses.ts index 78a096a0..5203cf23 100644 --- a/contracts/ownable/src/witnesses/Z_OwnablePKWitnesses.ts +++ b/contracts/ownable/src/witnesses/Z_OwnablePKWitnesses.ts @@ -20,8 +20,7 @@ export const Z_OwnablePKPrivateState = { * @returns A fresh Z_OwnablePKPrivateState instance. */ generate: (): Z_OwnablePKPrivateState => { - //return { offchainNonce: getRandomValues(Buffer.alloc(32))}; - return { offchainNonce: Buffer.from(Array(32).fill(0xab))}; + return { offchainNonce: getRandomValues(Buffer.alloc(32))}; } }; From 2ddadc88190e2fbd050b72ac5197b69e2362cef4 Mon Sep 17 00:00:00 2001 From: andrew Date: Sat, 9 Aug 2025 16:02:45 -0300 Subject: [PATCH 072/322] improve simulator with options in constructor --- .../test/simulators/Z_OwnablePKSimulator.ts | 38 +++++++++++++++---- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/contracts/ownable/src/test/simulators/Z_OwnablePKSimulator.ts b/contracts/ownable/src/test/simulators/Z_OwnablePKSimulator.ts index e6fd0dfd..8fcaa473 100644 --- a/contracts/ownable/src/test/simulators/Z_OwnablePKSimulator.ts +++ b/contracts/ownable/src/test/simulators/Z_OwnablePKSimulator.ts @@ -17,8 +17,9 @@ Z_OwnablePKPrivateState, } from '../../witnesses/Z_OwnablePKWitnesses.js'; import { AbstractContractSimulator } from '../utils/AbstractContractSimulator.js'; import { SimulatorStateManager } from '../utils/SimualatorStateManager.js'; -import { ContextlessCircuits, ExtractImpureCircuits, ExtractPureCircuits } from '../types/test.js'; +import { ContextlessCircuits, ExtractImpureCircuits, ExtractPureCircuits, SimulatorOptions } from '../types/test.js'; +type OwnableSimOptions = SimulatorOptions; /** * @description A simulator implementation of a contract for testing purposes. @@ -44,17 +45,22 @@ export class Z_OwnablePKSimulator extends AbstractContractSimulator< Z_OwnablePKPrivateState >; - constructor(initOwner: Uint8Array) { + constructor(initOwner: Uint8Array, options: OwnableSimOptions = {}, ) { super(); - this.contract = new MockOwnable( - Z_OwnablePKWitnesses(), - ); + // Setup initial state - const privateState: Z_OwnablePKPrivateState = Z_OwnablePKPrivateState.generate(); - const coinPK = '0'.repeat(64); - const address = sampleContractAddress(); + const { + privateState = Z_OwnablePKPrivateState.generate(), + witnesses = Z_OwnablePKWitnesses(), + coinPK = '0'.repeat(64), + address = sampleContractAddress(), + } = options; const constructorArgs = [initOwner]; + this.contract = new MockOwnable( + witnesses, + ); + this.stateManager = new SimulatorStateManager( this.contract, privateState, @@ -223,4 +229,20 @@ export class Z_OwnablePKSimulator extends AbstractContractSimulator< public _transferOwnership(newOwnerCommitment: Uint8Array) { this.circuits.impure._transferOwnership(newOwnerCommitment); } + + public readonly privateState = { + /** + * @description Stubs a new nonce into the private state. + */ + injectSecretNonce: (newNonce: Buffer): Z_OwnablePKPrivateState => { + const currentState = this.stateManager.getContext().currentPrivateState; + const updatedState = { ...currentState, offchainNonce: newNonce }; + this.stateManager.updatePrivateState(updatedState); + return updatedState; + }, + + getCurrentSecretNonce: (): Uint8Array => { + return this.stateManager.getContext().currentPrivateState.offchainNonce; + } + } } From de93d4828cc6fdc86477051c27f0888c7bb0c799 Mon Sep 17 00:00:00 2001 From: andrew Date: Sat, 9 Aug 2025 16:03:01 -0300 Subject: [PATCH 073/322] add transferOwnership tests --- .../ownable/src/test/Z_OwnablePK.test.ts | 142 +++++++++++++++++- 1 file changed, 134 insertions(+), 8 deletions(-) diff --git a/contracts/ownable/src/test/Z_OwnablePK.test.ts b/contracts/ownable/src/test/Z_OwnablePK.test.ts index 72a8763b..dbfe9445 100644 --- a/contracts/ownable/src/test/Z_OwnablePK.test.ts +++ b/contracts/ownable/src/test/Z_OwnablePK.test.ts @@ -9,6 +9,7 @@ import { beforeEach, describe, expect, it } from 'vitest'; import { Z_OwnablePKSimulator } from './simulators/Z_OwnablePKSimulator.js'; import * as utils from './utils/address.js'; import { ZswapCoinPublicKey } from '../artifacts/MockOwnable/contract/index.cjs'; +import { Z_OwnablePKPrivateState } from '../witnesses/Z_OwnablePKWitnesses.js'; const OWNER = String(Buffer.from('OWNER', 'ascii').toString('hex')).padStart( 64, @@ -26,11 +27,10 @@ const Z_NEW_OWNER = utils.encodeToPK('NEW_OWNER'); const Z_NEW_NEW_OWNER = utils.encodeToPK('Z_NEW_NEW_OWNER'); const EMPTY_BYTES = utils.ZERO_KEY.left.bytes; -// Commitments const DOMAIN = "Z_OwnablePK:shield:"; const INIT_COUNTER = 1n; -const STATIC_NONCE = new Uint8Array(32).fill(0xab); +let secretNonce: Uint8Array; let ownable: Z_OwnablePKSimulator; const createZPKCommitment = ( @@ -67,30 +67,156 @@ describe('Z_OwnablePK', () => { describe('after initialization', () => { beforeEach(() => { - const ownerCommitment = createZPKCommitment(DOMAIN, Z_OWNER, INIT_COUNTER, STATIC_NONCE); - ownable = new Z_OwnablePKSimulator(ownerCommitment); + // Create private state object and generate nonce + const PS = Z_OwnablePKPrivateState.generate(); + // Bind nonce for convenience + secretNonce = PS.offchainNonce; + // Prepare initial owner commitment with gen nonce + const ownerCommitment = createZPKCommitment(DOMAIN, Z_OWNER, INIT_COUNTER, secretNonce); + // Deploy contract with derived owner commitment and PS + ownable = new Z_OwnablePKSimulator(ownerCommitment, {privateState: PS}); }); describe('owner', () => { it('should return the correct owner commitment', () => { - const expCommitment = createZPKCommitment(DOMAIN, Z_OWNER, INIT_COUNTER, STATIC_NONCE); + const expCommitment = createZPKCommitment(DOMAIN, Z_OWNER, INIT_COUNTER, secretNonce); expect(ownable.owner()).toEqual(expCommitment); }); }); describe('assertOnlyOwner', () => { - it('should allow the authorized caller with correct nonce to call', () => { + it('should allow authorized caller with correct nonce to call', () => { + // Check nonce is correct + expect(ownable.privateState.getCurrentSecretNonce()).toEqual(secretNonce); + ownable.setCaller(OWNER); expect(ownable.assertOnlyOwner()).to.not.throw; }); - it('should fail when called by unauthorized with correct nonce', () => { - ownable.setCaller(UNAUTHORIZED); + it('should fail when the authorized caller has the wrong nonce', () => { + // Inject bad nonce + const badNonce = Buffer.alloc(32, "badNonce"); + ownable.privateState.injectSecretNonce(badNonce); + // Check nonce does not match + expect(ownable.privateState.getCurrentSecretNonce()).not.toEqual(secretNonce); + + // Set caller and call circuit + ownable.setCaller(OWNER); expect(() => { ownable.assertOnlyOwner(); }).toThrow('Forbidden'); }); + + it('should fail when unauthorized caller has the correct nonce', () => { + // Check nonce is correct + expect(ownable.privateState.getCurrentSecretNonce()).toEqual(secretNonce); + + ownable.setCaller(UNAUTHORIZED); + expect(() => { + ownable.assertOnlyOwner() + }).toThrow('Forbidden'); + }); + + it('should fail when unauthorized caller has the wrong nonce', () => { + // Inject bad nonce + const badNonce = Buffer.alloc(32, "badNonce"); + ownable.privateState.injectSecretNonce(badNonce); + + // Check nonce does not match + expect(ownable.privateState.getCurrentSecretNonce()).not.toEqual(secretNonce); + + // Set unauthorized caller and call circuit + ownable.setCaller(UNAUTHORIZED); + expect(() => { + ownable.assertOnlyOwner() + }).toThrow('Forbidden'); + }); + }); + + describe('transferOwnership', () => { + let newOwnerCommitment: Uint8Array; + let newOwnerNonce: Uint8Array; + + beforeEach(() => { + // Prepare new owner commitment + newOwnerNonce = Z_OwnablePKPrivateState.generate().offchainNonce; + const newOwnerCounter = INIT_COUNTER + 1n; + newOwnerCommitment = createZPKCommitment(DOMAIN, Z_NEW_OWNER, newOwnerCounter, newOwnerNonce); + }); + + it('should transfer ownership', () => { + ownable.setCaller(OWNER); + ownable.transferOwnership(newOwnerCommitment); + expect(ownable.owner()).toEqual(newOwnerCommitment); + + // Old owner + ownable.setCaller(OWNER); + expect(() => { + ownable.assertOnlyOwner() + }).toThrow('Forbidden'); + + // Unauthorized + ownable.setCaller(UNAUTHORIZED); + expect(() => { + ownable.assertOnlyOwner() + }).toThrow('Forbidden'); + + // New owner + ownable.setCaller(NEW_OWNER); + ownable.privateState.injectSecretNonce(Buffer.from(newOwnerNonce)) + expect(ownable.assertOnlyOwner()).not.to.throw; + }); + + it('should fail when transferring to zero', () => { + ownable.setCaller(OWNER); + const badCommitment = new Uint8Array(32).fill(0); + expect(() => { + ownable.transferOwnership(badCommitment); + }).toThrow('Invalid parameters'); + }); + + it('should fail when unauthorized transfers ownership', () => { + ownable.setCaller(UNAUTHORIZED); + expect(() => { + ownable.transferOwnership(newOwnerCommitment); + }).toThrow('Forbidden'); + }); + + /** + * @description More thoroughly tested in `_transferOwnership` + * */ + it('should bump instance after transfer', () => { + let beforeInstance = ownable.getPublicState().Z_OwnablePK__instance; + + // Transfer + ownable.setCaller(OWNER); + ownable.transferOwnership(newOwnerCommitment); + + // Check counter + let afterInstance = ownable.getPublicState().Z_OwnablePK__instance; + expect(afterInstance).toEqual(beforeInstance + 1n); + }); + + it('should change hash when transferring ownership to commitment with same pk and nonce', () => { + // Confirm current commitment + const initCommitment = ownable.owner(); + const calcInitCommitment = createZPKCommitment(DOMAIN, Z_OWNER, INIT_COUNTER, secretNonce); + expect(initCommitment).toEqual(calcInitCommitment); + + // Create new commitment by bumping the counter + const bumpedCounter = INIT_COUNTER + 1n; + const newCommitment = createZPKCommitment(DOMAIN, Z_OWNER, bumpedCounter, secretNonce); + + // Transfer ownership to self + ownable.setCaller(OWNER); + ownable.transferOwnership(newCommitment); + + // Check owner and permissions + const newOwner = ownable.owner(); + expect(newOwner).toEqual(newCommitment); + ownable.assertOnlyOwner(); + }) }); }); }); From d7fe2a70d8a4367ad720dafe2becb7abe9181d3c Mon Sep 17 00:00:00 2001 From: andrew Date: Sun, 10 Aug 2025 00:54:21 -0300 Subject: [PATCH 074/322] start hash chaining --- contracts/ownable/src/Z_OwnablePK.compact | 41 +++--- .../ownable/src/test/Z_OwnablePK.test.ts | 123 +++++++++++++----- .../src/test/mocks/MockZ_OwnablePK.compact | 6 +- .../test/simulators/Z_OwnablePKSimulator.ts | 21 ++- 4 files changed, 123 insertions(+), 68 deletions(-) diff --git a/contracts/ownable/src/Z_OwnablePK.compact b/contracts/ownable/src/Z_OwnablePK.compact index 22a55e06..730b4db6 100644 --- a/contracts/ownable/src/Z_OwnablePK.compact +++ b/contracts/ownable/src/Z_OwnablePK.compact @@ -6,7 +6,7 @@ module Z_OwnablePK { import CompactStandardLibrary; export ledger _ownerCommitment: Bytes<32>; - export ledger _instance: Counter; + export ledger _counter: Counter; export witness offchainNonce(): Bytes<32>; @@ -19,10 +19,10 @@ module Z_OwnablePK { return _ownerCommitment; } - export circuit transferOwnership(newOwnerCommitment: Bytes<32>): [] { + export circuit transferOwnership(newOwnerIdHash: Bytes<32>): [] { assertOnlyOwner(); - assert(newOwnerCommitment != default>, "Invalid parameters"); - _transferOwnership(newOwnerCommitment); + assert(newOwnerIdHash != default>, "Invalid parameters"); + _transferOwnership(newOwnerIdHash); } export circuit renounceOwnership(): [] { @@ -37,7 +37,7 @@ module Z_OwnablePK { [ pad(32, "Z_OwnablePK:renounced:"), default>, - _instance as Field as Bytes<32>, + _counter as Field as Bytes<32>, nonce ] ); @@ -48,28 +48,23 @@ module Z_OwnablePK { export circuit assertOnlyOwner(): [] { const caller = ownPublicKey(); const nonce = offchainNonce(); - assert( - _ownerCommitment == shieldPK(caller, _instance, nonce - ), "Forbidden"); + const idHash = persistentHash>>([caller.bytes, nonce]); + assert(_ownerCommitment == hashCommitment(idHash, _counter), "Forbidden"); } - export circuit shieldPK( - pk: ZswapCoinPublicKey, - instance: Uint<64>, - nonce: Bytes<32> + export circuit hashCommitment( + idHash: Bytes<32>, + counter: Uint<64>, ): Bytes<32> { - return persistentHash>>( - [ - pad(32, "Z_OwnablePK:shield:"), - pk.bytes, - instance as Field as Bytes<32>, - nonce - ] - ); + //const contextHash = persistentHash>>([kernel.self().bytes, idHash]); + //const counterHash = persistentHash>>([counter as Field as Bytes<32>, contextHash]); + const counterHash = persistentHash>>([counter as Field as Bytes<32>, idHash]); + const commitment = persistentHash>>([pad(32, "Z_OwnablePK:shield:"), counterHash]); + return commitment; } - export circuit _transferOwnership(newOwnerCommitment: Bytes<32>): [] { - _instance.increment(1); - _ownerCommitment = disclose(newOwnerCommitment); + export circuit _transferOwnership(newOwnerIdHash: Bytes<32>): [] { + _counter.increment(1); + _ownerCommitment = hashCommitment(disclose(newOwnerIdHash), _counter); } } diff --git a/contracts/ownable/src/test/Z_OwnablePK.test.ts b/contracts/ownable/src/test/Z_OwnablePK.test.ts index dbfe9445..dc881e85 100644 --- a/contracts/ownable/src/test/Z_OwnablePK.test.ts +++ b/contracts/ownable/src/test/Z_OwnablePK.test.ts @@ -8,7 +8,7 @@ import { import { beforeEach, describe, expect, it } from 'vitest'; import { Z_OwnablePKSimulator } from './simulators/Z_OwnablePKSimulator.js'; import * as utils from './utils/address.js'; -import { ZswapCoinPublicKey } from '../artifacts/MockOwnable/contract/index.cjs'; +import { ZswapCoinPublicKey, ContractAddress } from '../artifacts/MockOwnable/contract/index.cjs'; import { Z_OwnablePKPrivateState } from '../witnesses/Z_OwnablePKWitnesses.js'; const OWNER = String(Buffer.from('OWNER', 'ascii').toString('hex')).padStart( @@ -32,20 +32,69 @@ const INIT_COUNTER = 1n; let secretNonce: Uint8Array; let ownable: Z_OwnablePKSimulator; +let bOwnableAddress: Uint8Array; + +//const createZPKCommitment = ( +// domain: string, +// pk: ZswapCoinPublicKey, +// counter: bigint, +// nonce: Uint8Array +//): Uint8Array => { +// const rt_type = new CompactTypeVector(4, new CompactTypeBytes(32)); +// const encoder = new TextEncoder(); +// +// const bDomain = encoder.encode(domain); +// const bPK = pk.bytes; +// const bCounter = convert_bigint_to_Uint8Array(32, counter); +// return persistentHash(rt_type, [bDomain, bPK, bCounter, nonce]); +//} + +const createIdHash = ( + pk: ZswapCoinPublicKey, + nonce: Uint8Array +): Uint8Array => { + const rt_type = new CompactTypeVector(2, new CompactTypeBytes(32)); + + const bPK = pk.bytes; + return persistentHash(rt_type, [bPK, nonce]); +} + +const buildCommitmentFromId = ( + id: Uint8Array, + counter: bigint +): Uint8Array => { + const rt_type = new CompactTypeVector(2, new CompactTypeBytes(32)); + const encoder = new TextEncoder(); + + //const contextHash = persistentHash(rt_type, [address, id]); + + const bCounter = convert_bigint_to_Uint8Array(32, counter); + const innerHash = persistentHash(rt_type, [bCounter, id]); + + const bDomain = encoder.encode(DOMAIN); + const outerHash = persistentHash(rt_type, [bDomain, innerHash]); + return outerHash; +} -const createZPKCommitment = ( +const buildCommitment = ( domain: string, pk: ZswapCoinPublicKey, counter: bigint, - nonce: Uint8Array + nonce: Uint8Array, ): Uint8Array => { - const rt_type = new CompactTypeVector(4, new CompactTypeBytes(32)); + const rt_type = new CompactTypeVector(2, new CompactTypeBytes(32)); const encoder = new TextEncoder(); - const bDomain = encoder.encode(domain); - const bPK = pk.bytes; + const idHash = createIdHash(pk, nonce); + //const contextHash = persistentHash(rt_type, [address, idHash]); + const bCounter = convert_bigint_to_Uint8Array(32, counter); - return persistentHash(rt_type, [bDomain, bPK, bCounter, nonce]); + const counterHash = persistentHash(rt_type, [bCounter, idHash]); + + const bDomain = encoder.encode(domain); + const outerHash = persistentHash(rt_type, [bDomain, counterHash]); + return outerHash; + //return innerHash; } describe('Z_OwnablePK', () => { @@ -58,9 +107,12 @@ describe('Z_OwnablePK', () => { }); it('should initialize with non-zero commitment', () => { - const nonZeroCommitment = new Uint8Array(32).fill(1); - ownable = new Z_OwnablePKSimulator(nonZeroCommitment); + const notZeroPK = utils.encodeToPK('NOT_ZERO'); + const notZeroNonce = new Uint8Array(32).fill(1); + const nonZeroId = createIdHash(notZeroPK, notZeroNonce); + ownable = new Z_OwnablePKSimulator(nonZeroId); + const nonZeroCommitment = buildCommitment(DOMAIN, notZeroPK, INIT_COUNTER, notZeroNonce); expect(ownable.owner()).toEqual(nonZeroCommitment); }); }); @@ -71,15 +123,18 @@ describe('Z_OwnablePK', () => { const PS = Z_OwnablePKPrivateState.generate(); // Bind nonce for convenience secretNonce = PS.offchainNonce; - // Prepare initial owner commitment with gen nonce - const ownerCommitment = createZPKCommitment(DOMAIN, Z_OWNER, INIT_COUNTER, secretNonce); + // Prepare owner ID with gen nonce + const ownerCommitment = createIdHash(Z_OWNER, secretNonce); // Deploy contract with derived owner commitment and PS ownable = new Z_OwnablePKSimulator(ownerCommitment, {privateState: PS}); + + const encoder = new TextEncoder(); + bOwnableAddress = encoder.encode(ownable.contractAddress); }); describe('owner', () => { it('should return the correct owner commitment', () => { - const expCommitment = createZPKCommitment(DOMAIN, Z_OWNER, INIT_COUNTER, secretNonce); + const expCommitment = buildCommitment(DOMAIN, Z_OWNER, INIT_COUNTER, secretNonce); expect(ownable.owner()).toEqual(expCommitment); }); }); @@ -137,17 +192,20 @@ describe('Z_OwnablePK', () => { describe('transferOwnership', () => { let newOwnerCommitment: Uint8Array; let newOwnerNonce: Uint8Array; + let newIdHash: Uint8Array; + let newCounter: bigint; beforeEach(() => { // Prepare new owner commitment newOwnerNonce = Z_OwnablePKPrivateState.generate().offchainNonce; - const newOwnerCounter = INIT_COUNTER + 1n; - newOwnerCommitment = createZPKCommitment(DOMAIN, Z_NEW_OWNER, newOwnerCounter, newOwnerNonce); + newCounter = INIT_COUNTER + 1n; + newIdHash = createIdHash(Z_NEW_OWNER, newOwnerNonce); + newOwnerCommitment = buildCommitment(DOMAIN, Z_NEW_OWNER, newCounter, newOwnerNonce); }); it('should transfer ownership', () => { ownable.setCaller(OWNER); - ownable.transferOwnership(newOwnerCommitment); + ownable.transferOwnership(newIdHash); expect(ownable.owner()).toEqual(newOwnerCommitment); // Old owner @@ -187,36 +245,41 @@ describe('Z_OwnablePK', () => { * @description More thoroughly tested in `_transferOwnership` * */ it('should bump instance after transfer', () => { - let beforeInstance = ownable.getPublicState().Z_OwnablePK__instance; + let beforeInstance = ownable.getPublicState().Z_OwnablePK__counter; // Transfer ownable.setCaller(OWNER); ownable.transferOwnership(newOwnerCommitment); // Check counter - let afterInstance = ownable.getPublicState().Z_OwnablePK__instance; + let afterInstance = ownable.getPublicState().Z_OwnablePK__counter; expect(afterInstance).toEqual(beforeInstance + 1n); }); - it('should change hash when transferring ownership to commitment with same pk and nonce', () => { + it('should change commitment when transferring ownership to self with same pk + nonce)', () => { // Confirm current commitment + const repeatedId = createIdHash(Z_OWNER, secretNonce); const initCommitment = ownable.owner(); - const calcInitCommitment = createZPKCommitment(DOMAIN, Z_OWNER, INIT_COUNTER, secretNonce); - expect(initCommitment).toEqual(calcInitCommitment); + const expInitCommitment = buildCommitmentFromId(repeatedId, INIT_COUNTER); + expect(initCommitment).toEqual(expInitCommitment); + + // Transfer ownership to self with the same id -> `H(pk, nonce)` + ownable.setCaller(OWNER); + ownable.transferOwnership(repeatedId); + + // Check commitments don't match + const newCommitment = ownable.owner(); + expect(initCommitment).not.toEqual(newCommitment); - // Create new commitment by bumping the counter + // Build commitment locally and validate new commitment == expected const bumpedCounter = INIT_COUNTER + 1n; - const newCommitment = createZPKCommitment(DOMAIN, Z_OWNER, bumpedCounter, secretNonce); + const expNewCommitment = buildCommitmentFromId(repeatedId, bumpedCounter); + expect(newCommitment).toEqual(expNewCommitment); - // Transfer ownership to self + // Check same owner maintains permissions after transfer ownable.setCaller(OWNER); - ownable.transferOwnership(newCommitment); - - // Check owner and permissions - const newOwner = ownable.owner(); - expect(newOwner).toEqual(newCommitment); - ownable.assertOnlyOwner(); - }) + expect(ownable.assertOnlyOwner()).not.to.throw; + }); }); }); }); diff --git a/contracts/ownable/src/test/mocks/MockZ_OwnablePK.compact b/contracts/ownable/src/test/mocks/MockZ_OwnablePK.compact index e719e6c0..92658e3e 100644 --- a/contracts/ownable/src/test/mocks/MockZ_OwnablePK.compact +++ b/contracts/ownable/src/test/mocks/MockZ_OwnablePK.compact @@ -6,7 +6,7 @@ import CompactStandardLibrary; import "../../Z_OwnablePK" prefix Z_OwnablePK_; export { ZswapCoinPublicKey, ContractAddress, Either, Maybe }; -export { Z_OwnablePK__ownerCommitment, Z_OwnablePK__instance }; +export { Z_OwnablePK__ownerCommitment, Z_OwnablePK__counter }; constructor(initOwnerCommitment: Bytes<32>) { Z_OwnablePK_initialize(initOwnerCommitment); @@ -28,8 +28,8 @@ export circuit assertOnlyOwner(): [] { return Z_OwnablePK_assertOnlyOwner(); } -export circuit shieldPK(ownerPK: ZswapCoinPublicKey, instance: Uint<64>, nonce: Bytes<32>): Bytes<32> { - return Z_OwnablePK_shieldPK(ownerPK, instance, nonce); +export circuit hashCommitment(idHash: Bytes<32>, counter: Uint<64>): Bytes<32> { + return Z_OwnablePK_hashCommitment(idHash, counter); } export circuit _transferOwnership(newOwnerCommitment: Bytes<32>): [] { diff --git a/contracts/ownable/src/test/simulators/Z_OwnablePKSimulator.ts b/contracts/ownable/src/test/simulators/Z_OwnablePKSimulator.ts index 8fcaa473..e441dfa6 100644 --- a/contracts/ownable/src/test/simulators/Z_OwnablePKSimulator.ts +++ b/contracts/ownable/src/test/simulators/Z_OwnablePKSimulator.ts @@ -188,9 +188,9 @@ export class Z_OwnablePKSimulator extends AbstractContractSimulator< * @description */ public transferOwnership( - newOwner: Uint8Array, + newOwnerId: Uint8Array, ) { - this.circuits.impure.transferOwnership(newOwner); + this.circuits.impure.transferOwnership(newOwnerId); } /** @@ -211,23 +211,20 @@ export class Z_OwnablePKSimulator extends AbstractContractSimulator< } /** - * @description Obfuscates the `pk` be hashing it with a domain separator and - * the passed `instance`. - * @returns The shielded hash of the owner and instance. + * @description */ - public shieldPK( - pk: ZswapCoinPublicKey, - instance: bigint, - nonce: Uint8Array + public hashCommitment( + idHash: Uint8Array, + counter: bigint, ): Uint8Array { - return this.circuits.pure.shieldPK(pk, instance, nonce); + return this.circuits.pure.hashCommitment(idHash, counter); } /** * @description Internal circuit that transfers ownership of the contract to `newOwner`. */ - public _transferOwnership(newOwnerCommitment: Uint8Array) { - this.circuits.impure._transferOwnership(newOwnerCommitment); + public _transferOwnership(newOwnerId: Uint8Array) { + this.circuits.impure._transferOwnership(newOwnerId); } public readonly privateState = { From 47d7480da96af5943f2b6910e9c66e33787150e7 Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 11 Aug 2025 02:47:40 -0300 Subject: [PATCH 075/322] integrate instance salt to hash --- contracts/ownable/src/Z_OwnablePK.compact | 72 +++++++++++++++++------ 1 file changed, 53 insertions(+), 19 deletions(-) diff --git a/contracts/ownable/src/Z_OwnablePK.compact b/contracts/ownable/src/Z_OwnablePK.compact index 730b4db6..aa03f457 100644 --- a/contracts/ownable/src/Z_OwnablePK.compact +++ b/contracts/ownable/src/Z_OwnablePK.compact @@ -2,69 +2,103 @@ pragma language_version >= 0.16.0; +/** + * @module Z_OwnablePK + * @description A shielded Ownable library. + */ module Z_OwnablePK { import CompactStandardLibrary; export ledger _ownerCommitment: Bytes<32>; export ledger _counter: Counter; + export sealed ledger _instanceSalt: Bytes<32>; export witness offchainNonce(): Bytes<32>; - export circuit initialize(initCommitment: Bytes<32>): [] { - assert(initCommitment != default>, "Invalid parameters"); - _transferOwnership(initCommitment); + /** + * @description Add me!!! + */ + export circuit initialize(ownerId: Bytes<32>, instanceSalt: Bytes<32>): [] { + assert(ownerId != default>, "Invalid parameters"); + _instanceSalt = disclose(instanceSalt); + _transferOwnership(ownerId); } + /** + * @description Add me!!! + */ export circuit owner(): Bytes<32> { return _ownerCommitment; } - export circuit transferOwnership(newOwnerIdHash: Bytes<32>): [] { + /** + * @description Add me!!! + */ + export circuit transferOwnership(newOwnerId: Bytes<32>): [] { assertOnlyOwner(); - assert(newOwnerIdHash != default>, "Invalid parameters"); - _transferOwnership(newOwnerIdHash); + assert(newOwnerId != default>, "Invalid parameters"); + _transferOwnership(newOwnerId); } + /** + * @description Add me!!! + */ export circuit renounceOwnership(): [] { assertOnlyOwner(); _transferOwnership(default>); } + /** + * @description Add me!!! + */ export circuit renounceOwnershipObfuscated(): [] { assertOnlyOwner(); const nonce = offchainNonce(); const obfuscatedCommitment = persistentHash>>( [ - pad(32, "Z_OwnablePK:renounced:"), - default>, + persistentHash>>([default>, nonce]), + _instanceSalt, _counter as Field as Bytes<32>, - nonce + pad(32, "Z_OwnablePK:renounced:"), ] ); _transferOwnership(obfuscatedCommitment); } + /** + * @description Add me!!! + */ export circuit assertOnlyOwner(): [] { const caller = ownPublicKey(); const nonce = offchainNonce(); - const idHash = persistentHash>>([caller.bytes, nonce]); - assert(_ownerCommitment == hashCommitment(idHash, _counter), "Forbidden"); + const id = persistentHash>>([caller.bytes, nonce]); + assert(_ownerCommitment == hashCommitment(id, _counter), "Forbidden"); } + // computePKCommitment || generateCommitment + /** + * @description Add me!!! + */ export circuit hashCommitment( - idHash: Bytes<32>, + id: Bytes<32>, counter: Uint<64>, ): Bytes<32> { - //const contextHash = persistentHash>>([kernel.self().bytes, idHash]); - //const counterHash = persistentHash>>([counter as Field as Bytes<32>, contextHash]); - const counterHash = persistentHash>>([counter as Field as Bytes<32>, idHash]); - const commitment = persistentHash>>([pad(32, "Z_OwnablePK:shield:"), counterHash]); - return commitment; + return persistentHash>>( + [ + id, + _instanceSalt, + counter as Field as Bytes<32>, + pad(32, "Z_OwnablePK:shield:") + ] + ); } - export circuit _transferOwnership(newOwnerIdHash: Bytes<32>): [] { + /** + * @description Add me!!! + */ + export circuit _transferOwnership(newOwnerId: Bytes<32>): [] { _counter.increment(1); - _ownerCommitment = hashCommitment(disclose(newOwnerIdHash), _counter); + _ownerCommitment = hashCommitment(disclose(newOwnerId), _counter); } } From cf582c0c887e7e2e86e0a2e5288515ef065071b8 Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 11 Aug 2025 02:48:06 -0300 Subject: [PATCH 076/322] integrate instance salt to hash --- contracts/ownable/src/test/mocks/MockZ_OwnablePK.compact | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/ownable/src/test/mocks/MockZ_OwnablePK.compact b/contracts/ownable/src/test/mocks/MockZ_OwnablePK.compact index 92658e3e..4d425dbf 100644 --- a/contracts/ownable/src/test/mocks/MockZ_OwnablePK.compact +++ b/contracts/ownable/src/test/mocks/MockZ_OwnablePK.compact @@ -8,8 +8,8 @@ import "../../Z_OwnablePK" prefix Z_OwnablePK_; export { ZswapCoinPublicKey, ContractAddress, Either, Maybe }; export { Z_OwnablePK__ownerCommitment, Z_OwnablePK__counter }; -constructor(initOwnerCommitment: Bytes<32>) { - Z_OwnablePK_initialize(initOwnerCommitment); +constructor(initOwnerCommitment: Bytes<32>, instanceSalt: Bytes<32>) { + Z_OwnablePK_initialize(initOwnerCommitment, instanceSalt); } export circuit owner(): Bytes<32> { @@ -28,8 +28,8 @@ export circuit assertOnlyOwner(): [] { return Z_OwnablePK_assertOnlyOwner(); } -export circuit hashCommitment(idHash: Bytes<32>, counter: Uint<64>): Bytes<32> { - return Z_OwnablePK_hashCommitment(idHash, counter); +export circuit hashCommitment(id: Bytes<32>, counter: Uint<64>): Bytes<32> { + return Z_OwnablePK_hashCommitment(id, counter); } export circuit _transferOwnership(newOwnerCommitment: Bytes<32>): [] { From f5f58d24d1ec940678b47e826caa6fbf8342e7df Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 11 Aug 2025 02:48:42 -0300 Subject: [PATCH 077/322] update constructor in sim --- .../ownable/src/test/simulators/Z_OwnablePKSimulator.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/ownable/src/test/simulators/Z_OwnablePKSimulator.ts b/contracts/ownable/src/test/simulators/Z_OwnablePKSimulator.ts index e441dfa6..fac56eb6 100644 --- a/contracts/ownable/src/test/simulators/Z_OwnablePKSimulator.ts +++ b/contracts/ownable/src/test/simulators/Z_OwnablePKSimulator.ts @@ -45,7 +45,7 @@ export class Z_OwnablePKSimulator extends AbstractContractSimulator< Z_OwnablePKPrivateState >; - constructor(initOwner: Uint8Array, options: OwnableSimOptions = {}, ) { + constructor(initOwner: Uint8Array, instanceSalt: Uint8Array, options: OwnableSimOptions = {}, ) { super(); // Setup initial state @@ -55,7 +55,7 @@ export class Z_OwnablePKSimulator extends AbstractContractSimulator< coinPK = '0'.repeat(64), address = sampleContractAddress(), } = options; - const constructorArgs = [initOwner]; + const constructorArgs = [initOwner, instanceSalt]; this.contract = new MockOwnable( witnesses, @@ -214,10 +214,10 @@ export class Z_OwnablePKSimulator extends AbstractContractSimulator< * @description */ public hashCommitment( - idHash: Uint8Array, + id: Uint8Array, counter: bigint, ): Uint8Array { - return this.circuits.pure.hashCommitment(idHash, counter); + return this.circuits.impure.hashCommitment(id, counter); } /** From 6f233b10fb5ee8bbd32ab66d0db6f1b76b089b33 Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 11 Aug 2025 02:49:07 -0300 Subject: [PATCH 078/322] update tests with salt --- .../ownable/src/test/Z_OwnablePK.test.ts | 108 ++++++++---------- 1 file changed, 50 insertions(+), 58 deletions(-) diff --git a/contracts/ownable/src/test/Z_OwnablePK.test.ts b/contracts/ownable/src/test/Z_OwnablePK.test.ts index dc881e85..eed5e4b8 100644 --- a/contracts/ownable/src/test/Z_OwnablePK.test.ts +++ b/contracts/ownable/src/test/Z_OwnablePK.test.ts @@ -1,14 +1,13 @@ import { - type CoinPublicKey, convert_bigint_to_Uint8Array, persistentHash, CompactTypeVector, - CompactTypeBytes + CompactTypeBytes, } from '@midnight-ntwrk/compact-runtime'; import { beforeEach, describe, expect, it } from 'vitest'; import { Z_OwnablePKSimulator } from './simulators/Z_OwnablePKSimulator.js'; import * as utils from './utils/address.js'; -import { ZswapCoinPublicKey, ContractAddress } from '../artifacts/MockOwnable/contract/index.cjs'; +import { ZswapCoinPublicKey } from '../artifacts/MockOwnable/contract/index.cjs'; import { Z_OwnablePKPrivateState } from '../witnesses/Z_OwnablePKWitnesses.js'; const OWNER = String(Buffer.from('OWNER', 'ascii').toString('hex')).padStart( @@ -21,34 +20,17 @@ const NEW_OWNER = String( const UNAUTHORIZED = String( Buffer.from('UNAUTHORIZED', 'ascii').toString('hex'), ).padStart(64, '0'); -const Z_ZERO = utils.encodeToPK(''); const Z_OWNER = utils.encodeToPK('OWNER'); const Z_NEW_OWNER = utils.encodeToPK('NEW_OWNER'); -const Z_NEW_NEW_OWNER = utils.encodeToPK('Z_NEW_NEW_OWNER'); -const EMPTY_BYTES = utils.ZERO_KEY.left.bytes; +const INSTANCE_SALT = new Uint8Array(32).fill(8675309); const DOMAIN = "Z_OwnablePK:shield:"; const INIT_COUNTER = 1n; let secretNonce: Uint8Array; let ownable: Z_OwnablePKSimulator; -let bOwnableAddress: Uint8Array; - -//const createZPKCommitment = ( -// domain: string, -// pk: ZswapCoinPublicKey, -// counter: bigint, -// nonce: Uint8Array -//): Uint8Array => { -// const rt_type = new CompactTypeVector(4, new CompactTypeBytes(32)); -// const encoder = new TextEncoder(); -// -// const bDomain = encoder.encode(domain); -// const bPK = pk.bytes; -// const bCounter = convert_bigint_to_Uint8Array(32, counter); -// return persistentHash(rt_type, [bDomain, bPK, bCounter, nonce]); -//} +/** Helpers */ const createIdHash = ( pk: ZswapCoinPublicKey, nonce: Uint8Array @@ -61,40 +43,32 @@ const createIdHash = ( const buildCommitmentFromId = ( id: Uint8Array, - counter: bigint + instanceSalt: Uint8Array, + counter: bigint, ): Uint8Array => { - const rt_type = new CompactTypeVector(2, new CompactTypeBytes(32)); - const encoder = new TextEncoder(); - - //const contextHash = persistentHash(rt_type, [address, id]); - + const rt_type = new CompactTypeVector(4, new CompactTypeBytes(32)); const bCounter = convert_bigint_to_Uint8Array(32, counter); - const innerHash = persistentHash(rt_type, [bCounter, id]); + const bDomain = new TextEncoder().encode(DOMAIN); - const bDomain = encoder.encode(DOMAIN); - const outerHash = persistentHash(rt_type, [bDomain, innerHash]); - return outerHash; + const commitment = persistentHash(rt_type, [id, instanceSalt, bCounter, bDomain]); + return commitment; } const buildCommitment = ( - domain: string, - pk: ZswapCoinPublicKey, - counter: bigint, - nonce: Uint8Array, + pk: ZswapCoinPublicKey, + nonce: Uint8Array, + instanceSalt: Uint8Array, + counter: bigint, + domain: string ): Uint8Array => { - const rt_type = new CompactTypeVector(2, new CompactTypeBytes(32)); - const encoder = new TextEncoder(); - - const idHash = createIdHash(pk, nonce); - //const contextHash = persistentHash(rt_type, [address, idHash]); + const id = createIdHash(pk, nonce); + const rt_type = new CompactTypeVector(4, new CompactTypeBytes(32)); const bCounter = convert_bigint_to_Uint8Array(32, counter); - const counterHash = persistentHash(rt_type, [bCounter, idHash]); + const bDomain = new TextEncoder().encode(domain); - const bDomain = encoder.encode(domain); - const outerHash = persistentHash(rt_type, [bDomain, counterHash]); - return outerHash; - //return innerHash; + const commitment = persistentHash(rt_type, [id, instanceSalt, bCounter, bDomain]); + return commitment; } describe('Z_OwnablePK', () => { @@ -102,7 +76,7 @@ describe('Z_OwnablePK', () => { it('should fail when setting owner commitment as 0', () => { expect(() => { const badCommitment = new Uint8Array(32).fill(0); - new Z_OwnablePKSimulator(badCommitment); + new Z_OwnablePKSimulator(badCommitment, INSTANCE_SALT); }).toThrow('Invalid parameters'); }); @@ -110,9 +84,9 @@ describe('Z_OwnablePK', () => { const notZeroPK = utils.encodeToPK('NOT_ZERO'); const notZeroNonce = new Uint8Array(32).fill(1); const nonZeroId = createIdHash(notZeroPK, notZeroNonce); - ownable = new Z_OwnablePKSimulator(nonZeroId); + ownable = new Z_OwnablePKSimulator(nonZeroId, INSTANCE_SALT); - const nonZeroCommitment = buildCommitment(DOMAIN, notZeroPK, INIT_COUNTER, notZeroNonce); + const nonZeroCommitment = buildCommitmentFromId(nonZeroId, INSTANCE_SALT, INIT_COUNTER); expect(ownable.owner()).toEqual(nonZeroCommitment); }); }); @@ -124,17 +98,35 @@ describe('Z_OwnablePK', () => { // Bind nonce for convenience secretNonce = PS.offchainNonce; // Prepare owner ID with gen nonce - const ownerCommitment = createIdHash(Z_OWNER, secretNonce); + const ownerId = createIdHash(Z_OWNER, secretNonce); // Deploy contract with derived owner commitment and PS - ownable = new Z_OwnablePKSimulator(ownerCommitment, {privateState: PS}); - - const encoder = new TextEncoder(); - bOwnableAddress = encoder.encode(ownable.contractAddress); + ownable = new Z_OwnablePKSimulator(ownerId, INSTANCE_SALT, {privateState: PS}); }); + /** + * @TODO parameterize + */ + describe('hashCommitment', () => { + it('should match local and contract commitment algorithms', () => { + const address = ownable.contractAddress; + const id = createIdHash(Z_OWNER, secretNonce); + const counter = INIT_COUNTER; + + // Check buildCommitmentFromId + const hashFromContract = ownable.hashCommitment(id, counter); + const hashFromHelper1 = buildCommitmentFromId(id, INSTANCE_SALT, counter); + expect(hashFromContract).toEqual(hashFromHelper1); + + // Check buildCommitment + const hashFromHelper2 = buildCommitment(Z_OWNER, secretNonce, INSTANCE_SALT, counter, DOMAIN); + expect(hashFromContract).toEqual(hashFromHelper1); + expect(hashFromHelper1).toEqual(hashFromHelper2); + }) + }) + describe('owner', () => { it('should return the correct owner commitment', () => { - const expCommitment = buildCommitment(DOMAIN, Z_OWNER, INIT_COUNTER, secretNonce); + const expCommitment = buildCommitment(Z_OWNER, secretNonce, INSTANCE_SALT, INIT_COUNTER, DOMAIN); expect(ownable.owner()).toEqual(expCommitment); }); }); @@ -200,7 +192,7 @@ describe('Z_OwnablePK', () => { newOwnerNonce = Z_OwnablePKPrivateState.generate().offchainNonce; newCounter = INIT_COUNTER + 1n; newIdHash = createIdHash(Z_NEW_OWNER, newOwnerNonce); - newOwnerCommitment = buildCommitment(DOMAIN, Z_NEW_OWNER, newCounter, newOwnerNonce); + newOwnerCommitment = buildCommitment(Z_NEW_OWNER, newOwnerNonce, INSTANCE_SALT, newCounter, DOMAIN); }); it('should transfer ownership', () => { @@ -260,7 +252,7 @@ describe('Z_OwnablePK', () => { // Confirm current commitment const repeatedId = createIdHash(Z_OWNER, secretNonce); const initCommitment = ownable.owner(); - const expInitCommitment = buildCommitmentFromId(repeatedId, INIT_COUNTER); + const expInitCommitment = buildCommitmentFromId(repeatedId, INSTANCE_SALT, INIT_COUNTER); expect(initCommitment).toEqual(expInitCommitment); // Transfer ownership to self with the same id -> `H(pk, nonce)` @@ -273,7 +265,7 @@ describe('Z_OwnablePK', () => { // Build commitment locally and validate new commitment == expected const bumpedCounter = INIT_COUNTER + 1n; - const expNewCommitment = buildCommitmentFromId(repeatedId, bumpedCounter); + const expNewCommitment = buildCommitmentFromId(repeatedId, INSTANCE_SALT, bumpedCounter); expect(newCommitment).toEqual(expNewCommitment); // Check same owner maintains permissions after transfer From 9a643145efacb0e96f4d14ea4c3cb628e6bbf18a Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 11 Aug 2025 02:59:26 -0300 Subject: [PATCH 079/322] fix fmt --- .../test/simulators/AccessControlSimulator.ts | 4 +- .../ownable/src/test/Z_OwnablePK.test.ts | 127 +++++++++++++----- .../src/test/simulators/OwnableSimulator.ts | 2 +- .../test/simulators/Z_OwnablePKSimulator.ts | 43 +++--- .../src/test/utils/createCircuitProxies.ts | 37 +++-- .../src/witnesses/Z_OwnablePKWitnesses.ts | 13 +- contracts/ownable/src/witnesses/interface.ts | 2 +- 7 files changed, 148 insertions(+), 80 deletions(-) diff --git a/contracts/accessControl/src/test/simulators/AccessControlSimulator.ts b/contracts/accessControl/src/test/simulators/AccessControlSimulator.ts index b1eac3b2..5566c3fa 100644 --- a/contracts/accessControl/src/test/simulators/AccessControlSimulator.ts +++ b/contracts/accessControl/src/test/simulators/AccessControlSimulator.ts @@ -2,18 +2,18 @@ import { type CircuitContext, type CoinPublicKey, type ContractState, - QueryContext, constructorContext, emptyZswapLocalState, + QueryContext, } from '@midnight-ntwrk/compact-runtime'; import { sampleContractAddress } from '@midnight-ntwrk/zswap'; import { type ContractAddress, type Either, type Ledger, + ledger, Contract as MockAccessControl, type ZswapCoinPublicKey, - ledger, } from '../../artifacts/MockAccessControl/contract/index.cjs'; // Combined imports import { type AccessControlPrivateState, diff --git a/contracts/ownable/src/test/Z_OwnablePK.test.ts b/contracts/ownable/src/test/Z_OwnablePK.test.ts index eed5e4b8..152ed3a0 100644 --- a/contracts/ownable/src/test/Z_OwnablePK.test.ts +++ b/contracts/ownable/src/test/Z_OwnablePK.test.ts @@ -1,14 +1,14 @@ import { + CompactTypeBytes, + CompactTypeVector, convert_bigint_to_Uint8Array, persistentHash, - CompactTypeVector, - CompactTypeBytes, } from '@midnight-ntwrk/compact-runtime'; import { beforeEach, describe, expect, it } from 'vitest'; +import type { ZswapCoinPublicKey } from '../artifacts/MockOwnable/contract/index.cjs'; +import { Z_OwnablePKPrivateState } from '../witnesses/Z_OwnablePKWitnesses.js'; import { Z_OwnablePKSimulator } from './simulators/Z_OwnablePKSimulator.js'; import * as utils from './utils/address.js'; -import { ZswapCoinPublicKey } from '../artifacts/MockOwnable/contract/index.cjs'; -import { Z_OwnablePKPrivateState } from '../witnesses/Z_OwnablePKWitnesses.js'; const OWNER = String(Buffer.from('OWNER', 'ascii').toString('hex')).padStart( 64, @@ -24,7 +24,7 @@ const Z_OWNER = utils.encodeToPK('OWNER'); const Z_NEW_OWNER = utils.encodeToPK('NEW_OWNER'); const INSTANCE_SALT = new Uint8Array(32).fill(8675309); -const DOMAIN = "Z_OwnablePK:shield:"; +const DOMAIN = 'Z_OwnablePK:shield:'; const INIT_COUNTER = 1n; let secretNonce: Uint8Array; @@ -32,14 +32,14 @@ let ownable: Z_OwnablePKSimulator; /** Helpers */ const createIdHash = ( - pk: ZswapCoinPublicKey, - nonce: Uint8Array + pk: ZswapCoinPublicKey, + nonce: Uint8Array, ): Uint8Array => { const rt_type = new CompactTypeVector(2, new CompactTypeBytes(32)); const bPK = pk.bytes; return persistentHash(rt_type, [bPK, nonce]); -} +}; const buildCommitmentFromId = ( id: Uint8Array, @@ -50,16 +50,21 @@ const buildCommitmentFromId = ( const bCounter = convert_bigint_to_Uint8Array(32, counter); const bDomain = new TextEncoder().encode(DOMAIN); - const commitment = persistentHash(rt_type, [id, instanceSalt, bCounter, bDomain]); + const commitment = persistentHash(rt_type, [ + id, + instanceSalt, + bCounter, + bDomain, + ]); return commitment; -} +}; const buildCommitment = ( pk: ZswapCoinPublicKey, nonce: Uint8Array, instanceSalt: Uint8Array, counter: bigint, - domain: string + domain: string, ): Uint8Array => { const id = createIdHash(pk, nonce); @@ -67,9 +72,14 @@ const buildCommitment = ( const bCounter = convert_bigint_to_Uint8Array(32, counter); const bDomain = new TextEncoder().encode(domain); - const commitment = persistentHash(rt_type, [id, instanceSalt, bCounter, bDomain]); + const commitment = persistentHash(rt_type, [ + id, + instanceSalt, + bCounter, + bDomain, + ]); return commitment; -} +}; describe('Z_OwnablePK', () => { describe('before initialize', () => { @@ -86,7 +96,11 @@ describe('Z_OwnablePK', () => { const nonZeroId = createIdHash(notZeroPK, notZeroNonce); ownable = new Z_OwnablePKSimulator(nonZeroId, INSTANCE_SALT); - const nonZeroCommitment = buildCommitmentFromId(nonZeroId, INSTANCE_SALT, INIT_COUNTER); + const nonZeroCommitment = buildCommitmentFromId( + nonZeroId, + INSTANCE_SALT, + INIT_COUNTER, + ); expect(ownable.owner()).toEqual(nonZeroCommitment); }); }); @@ -100,7 +114,9 @@ describe('Z_OwnablePK', () => { // Prepare owner ID with gen nonce const ownerId = createIdHash(Z_OWNER, secretNonce); // Deploy contract with derived owner commitment and PS - ownable = new Z_OwnablePKSimulator(ownerId, INSTANCE_SALT, {privateState: PS}); + ownable = new Z_OwnablePKSimulator(ownerId, INSTANCE_SALT, { + privateState: PS, + }); }); /** @@ -108,25 +124,40 @@ describe('Z_OwnablePK', () => { */ describe('hashCommitment', () => { it('should match local and contract commitment algorithms', () => { - const address = ownable.contractAddress; const id = createIdHash(Z_OWNER, secretNonce); const counter = INIT_COUNTER; // Check buildCommitmentFromId const hashFromContract = ownable.hashCommitment(id, counter); - const hashFromHelper1 = buildCommitmentFromId(id, INSTANCE_SALT, counter); + const hashFromHelper1 = buildCommitmentFromId( + id, + INSTANCE_SALT, + counter, + ); expect(hashFromContract).toEqual(hashFromHelper1); // Check buildCommitment - const hashFromHelper2 = buildCommitment(Z_OWNER, secretNonce, INSTANCE_SALT, counter, DOMAIN); + const hashFromHelper2 = buildCommitment( + Z_OWNER, + secretNonce, + INSTANCE_SALT, + counter, + DOMAIN, + ); expect(hashFromContract).toEqual(hashFromHelper1); expect(hashFromHelper1).toEqual(hashFromHelper2); - }) - }) + }); + }); describe('owner', () => { it('should return the correct owner commitment', () => { - const expCommitment = buildCommitment(Z_OWNER, secretNonce, INSTANCE_SALT, INIT_COUNTER, DOMAIN); + const expCommitment = buildCommitment( + Z_OWNER, + secretNonce, + INSTANCE_SALT, + INIT_COUNTER, + DOMAIN, + ); expect(ownable.owner()).toEqual(expCommitment); }); }); @@ -134,7 +165,9 @@ describe('Z_OwnablePK', () => { describe('assertOnlyOwner', () => { it('should allow authorized caller with correct nonce to call', () => { // Check nonce is correct - expect(ownable.privateState.getCurrentSecretNonce()).toEqual(secretNonce); + expect(ownable.privateState.getCurrentSecretNonce()).toEqual( + secretNonce, + ); ownable.setCaller(OWNER); expect(ownable.assertOnlyOwner()).to.not.throw; @@ -142,11 +175,13 @@ describe('Z_OwnablePK', () => { it('should fail when the authorized caller has the wrong nonce', () => { // Inject bad nonce - const badNonce = Buffer.alloc(32, "badNonce"); + const badNonce = Buffer.alloc(32, 'badNonce'); ownable.privateState.injectSecretNonce(badNonce); // Check nonce does not match - expect(ownable.privateState.getCurrentSecretNonce()).not.toEqual(secretNonce); + expect(ownable.privateState.getCurrentSecretNonce()).not.toEqual( + secretNonce, + ); // Set caller and call circuit ownable.setCaller(OWNER); @@ -157,26 +192,30 @@ describe('Z_OwnablePK', () => { it('should fail when unauthorized caller has the correct nonce', () => { // Check nonce is correct - expect(ownable.privateState.getCurrentSecretNonce()).toEqual(secretNonce); + expect(ownable.privateState.getCurrentSecretNonce()).toEqual( + secretNonce, + ); ownable.setCaller(UNAUTHORIZED); expect(() => { - ownable.assertOnlyOwner() + ownable.assertOnlyOwner(); }).toThrow('Forbidden'); }); it('should fail when unauthorized caller has the wrong nonce', () => { // Inject bad nonce - const badNonce = Buffer.alloc(32, "badNonce"); + const badNonce = Buffer.alloc(32, 'badNonce'); ownable.privateState.injectSecretNonce(badNonce); // Check nonce does not match - expect(ownable.privateState.getCurrentSecretNonce()).not.toEqual(secretNonce); + expect(ownable.privateState.getCurrentSecretNonce()).not.toEqual( + secretNonce, + ); // Set unauthorized caller and call circuit ownable.setCaller(UNAUTHORIZED); expect(() => { - ownable.assertOnlyOwner() + ownable.assertOnlyOwner(); }).toThrow('Forbidden'); }); }); @@ -192,7 +231,13 @@ describe('Z_OwnablePK', () => { newOwnerNonce = Z_OwnablePKPrivateState.generate().offchainNonce; newCounter = INIT_COUNTER + 1n; newIdHash = createIdHash(Z_NEW_OWNER, newOwnerNonce); - newOwnerCommitment = buildCommitment(Z_NEW_OWNER, newOwnerNonce, INSTANCE_SALT, newCounter, DOMAIN); + newOwnerCommitment = buildCommitment( + Z_NEW_OWNER, + newOwnerNonce, + INSTANCE_SALT, + newCounter, + DOMAIN, + ); }); it('should transfer ownership', () => { @@ -203,18 +248,18 @@ describe('Z_OwnablePK', () => { // Old owner ownable.setCaller(OWNER); expect(() => { - ownable.assertOnlyOwner() + ownable.assertOnlyOwner(); }).toThrow('Forbidden'); // Unauthorized ownable.setCaller(UNAUTHORIZED); expect(() => { - ownable.assertOnlyOwner() + ownable.assertOnlyOwner(); }).toThrow('Forbidden'); // New owner ownable.setCaller(NEW_OWNER); - ownable.privateState.injectSecretNonce(Buffer.from(newOwnerNonce)) + ownable.privateState.injectSecretNonce(Buffer.from(newOwnerNonce)); expect(ownable.assertOnlyOwner()).not.to.throw; }); @@ -237,14 +282,14 @@ describe('Z_OwnablePK', () => { * @description More thoroughly tested in `_transferOwnership` * */ it('should bump instance after transfer', () => { - let beforeInstance = ownable.getPublicState().Z_OwnablePK__counter; + const beforeInstance = ownable.getPublicState().Z_OwnablePK__counter; // Transfer ownable.setCaller(OWNER); ownable.transferOwnership(newOwnerCommitment); // Check counter - let afterInstance = ownable.getPublicState().Z_OwnablePK__counter; + const afterInstance = ownable.getPublicState().Z_OwnablePK__counter; expect(afterInstance).toEqual(beforeInstance + 1n); }); @@ -252,7 +297,11 @@ describe('Z_OwnablePK', () => { // Confirm current commitment const repeatedId = createIdHash(Z_OWNER, secretNonce); const initCommitment = ownable.owner(); - const expInitCommitment = buildCommitmentFromId(repeatedId, INSTANCE_SALT, INIT_COUNTER); + const expInitCommitment = buildCommitmentFromId( + repeatedId, + INSTANCE_SALT, + INIT_COUNTER, + ); expect(initCommitment).toEqual(expInitCommitment); // Transfer ownership to self with the same id -> `H(pk, nonce)` @@ -265,7 +314,11 @@ describe('Z_OwnablePK', () => { // Build commitment locally and validate new commitment == expected const bumpedCounter = INIT_COUNTER + 1n; - const expNewCommitment = buildCommitmentFromId(repeatedId, INSTANCE_SALT, bumpedCounter); + const expNewCommitment = buildCommitmentFromId( + repeatedId, + INSTANCE_SALT, + bumpedCounter, + ); expect(newCommitment).toEqual(expNewCommitment); // Check same owner maintains permissions after transfer diff --git a/contracts/ownable/src/test/simulators/OwnableSimulator.ts b/contracts/ownable/src/test/simulators/OwnableSimulator.ts index 82778ad6..1fbd380e 100644 --- a/contracts/ownable/src/test/simulators/OwnableSimulator.ts +++ b/contracts/ownable/src/test/simulators/OwnableSimulator.ts @@ -19,7 +19,7 @@ import { type OwnablePrivateState, OwnableWitnesses, } from '../../witnesses/OwnableWitnesses.js'; -import { IContractSimulator } from '../types/test.js'; +import type { IContractSimulator } from '../types/test.js'; /** * @description A simulator implementation of a Ownable contract for testing purposes. diff --git a/contracts/ownable/src/test/simulators/Z_OwnablePKSimulator.ts b/contracts/ownable/src/test/simulators/Z_OwnablePKSimulator.ts index fac56eb6..c8520211 100644 --- a/contracts/ownable/src/test/simulators/Z_OwnablePKSimulator.ts +++ b/contracts/ownable/src/test/simulators/Z_OwnablePKSimulator.ts @@ -1,25 +1,31 @@ import { type CircuitContext, type CoinPublicKey, - type ContractState, emptyZswapLocalState, } from '@midnight-ntwrk/compact-runtime'; import { sampleContractAddress } from '@midnight-ntwrk/zswap'; -import type { ZswapCoinPublicKey } from '../../artifacts/MockZ_OwnablePK/contract/index.cjs'; import { type Ledger, ledger, Contract as MockOwnable, } from '../../artifacts/MockZ_OwnablePK/contract/index.cjs'; // Combined imports import { -Z_OwnablePKPrivateState, + Z_OwnablePKPrivateState, Z_OwnablePKWitnesses, } from '../../witnesses/Z_OwnablePKWitnesses.js'; +import type { + ContextlessCircuits, + ExtractImpureCircuits, + ExtractPureCircuits, + SimulatorOptions, +} from '../types/test.js'; import { AbstractContractSimulator } from '../utils/AbstractContractSimulator.js'; import { SimulatorStateManager } from '../utils/SimualatorStateManager.js'; -import { ContextlessCircuits, ExtractImpureCircuits, ExtractPureCircuits, SimulatorOptions } from '../types/test.js'; -type OwnableSimOptions = SimulatorOptions; +type OwnableSimOptions = SimulatorOptions< + Z_OwnablePKPrivateState, + typeof Z_OwnablePKWitnesses +>; /** * @description A simulator implementation of a contract for testing purposes. @@ -45,7 +51,11 @@ export class Z_OwnablePKSimulator extends AbstractContractSimulator< Z_OwnablePKPrivateState >; - constructor(initOwner: Uint8Array, instanceSalt: Uint8Array, options: OwnableSimOptions = {}, ) { + constructor( + initOwner: Uint8Array, + instanceSalt: Uint8Array, + options: OwnableSimOptions = {}, + ) { super(); // Setup initial state @@ -57,9 +67,7 @@ export class Z_OwnablePKSimulator extends AbstractContractSimulator< } = options; const constructorArgs = [initOwner, instanceSalt]; - this.contract = new MockOwnable( - witnesses, - ); + this.contract = new MockOwnable(witnesses); this.stateManager = new SimulatorStateManager( this.contract, @@ -187,9 +195,7 @@ export class Z_OwnablePKSimulator extends AbstractContractSimulator< /** * @description */ - public transferOwnership( - newOwnerId: Uint8Array, - ) { + public transferOwnership(newOwnerId: Uint8Array) { this.circuits.impure.transferOwnership(newOwnerId); } @@ -213,10 +219,7 @@ export class Z_OwnablePKSimulator extends AbstractContractSimulator< /** * @description */ - public hashCommitment( - id: Uint8Array, - counter: bigint, - ): Uint8Array { + public hashCommitment(id: Uint8Array, counter: bigint): Uint8Array { return this.circuits.impure.hashCommitment(id, counter); } @@ -231,7 +234,9 @@ export class Z_OwnablePKSimulator extends AbstractContractSimulator< /** * @description Stubs a new nonce into the private state. */ - injectSecretNonce: (newNonce: Buffer): Z_OwnablePKPrivateState => { + injectSecretNonce: ( + newNonce: Buffer, + ): Z_OwnablePKPrivateState => { const currentState = this.stateManager.getContext().currentPrivateState; const updatedState = { ...currentState, offchainNonce: newNonce }; this.stateManager.updatePrivateState(updatedState); @@ -240,6 +245,6 @@ export class Z_OwnablePKSimulator extends AbstractContractSimulator< getCurrentSecretNonce: (): Uint8Array => { return this.stateManager.getContext().currentPrivateState.offchainNonce; - } - } + }, + }; } diff --git a/contracts/ownable/src/test/utils/createCircuitProxies.ts b/contracts/ownable/src/test/utils/createCircuitProxies.ts index b4a14ab0..ad4211b6 100644 --- a/contracts/ownable/src/test/utils/createCircuitProxies.ts +++ b/contracts/ownable/src/test/utils/createCircuitProxies.ts @@ -2,13 +2,16 @@ import type { CircuitContext } from '@midnight-ntwrk/compact-runtime'; import type { ContextlessCircuits, ExtractImpureCircuits, - ExtractPureCircuits + ExtractPureCircuits, } from '../types/test.js'; /** * Creates lazily-initialized circuit proxies for pure and impure contract functions. */ -export function createCircuitProxies( +export function createCircuitProxies< + P, + ContractType extends { circuits: any; impureCircuits: any }, +>( contract: ContractType, getContext: () => CircuitContext

, getCallerContext: () => CircuitContext

, @@ -23,22 +26,28 @@ export function createCircuitProxies) => void, ) => ContextlessCircuits, ) { - let pureProxy: ContextlessCircuits, P> | undefined; - let impureProxy: ContextlessCircuits, P> | undefined; + let pureProxy: + | ContextlessCircuits, P> + | undefined; + let impureProxy: + | ContextlessCircuits, P> + | undefined; return { get circuits() { + if (!pureProxy) { + pureProxy = createPureProxy(contract.circuits, getContext); + } + if (!impureProxy) { + impureProxy = createImpureProxy( + contract.impureCircuits, + getCallerContext, + updateContext, + ); + } return { - pure: - pureProxy ?? - (pureProxy = createPureProxy(contract.circuits, getContext)), - impure: - impureProxy ?? - (impureProxy = createImpureProxy( - contract.impureCircuits, - getCallerContext, - updateContext, - )), + pure: pureProxy, + impure: impureProxy, }; }, resetProxies() { diff --git a/contracts/ownable/src/witnesses/Z_OwnablePKWitnesses.ts b/contracts/ownable/src/witnesses/Z_OwnablePKWitnesses.ts index 5203cf23..4bfcf082 100644 --- a/contracts/ownable/src/witnesses/Z_OwnablePKWitnesses.ts +++ b/contracts/ownable/src/witnesses/Z_OwnablePKWitnesses.ts @@ -1,7 +1,7 @@ import { getRandomValues } from 'node:crypto'; -import type { Ledger } from '../artifacts/MockZ_OwnablePK/contract/index.cjs'; import type { WitnessContext } from '@midnight-ntwrk/compact-runtime'; -import { IZ_OwnablePKWitnesses } from './interface.js' +import type { Ledger } from '../artifacts/MockZ_OwnablePK/contract/index.cjs'; +import type { IZ_OwnablePKWitnesses } from './interface.js'; /** * @description Represents the private state of an ownable contract, storing a secret nonce. @@ -20,18 +20,19 @@ export const Z_OwnablePKPrivateState = { * @returns A fresh Z_OwnablePKPrivateState instance. */ generate: (): Z_OwnablePKPrivateState => { - return { offchainNonce: getRandomValues(Buffer.alloc(32))}; - } + return { offchainNonce: getRandomValues(Buffer.alloc(32)) }; + }, }; /** * @description Factory function creating witness implementations for Ownable operations. * @returns An object implementing the Witnesses interface for Z_OwnablePKPrivateState. */ -export const Z_OwnablePKWitnesses = (): IZ_OwnablePKWitnesses => ({ +export const Z_OwnablePKWitnesses = + (): IZ_OwnablePKWitnesses => ({ offchainNonce( context: WitnessContext, ): [Z_OwnablePKPrivateState, Uint8Array] { return [context.privateState, context.privateState.offchainNonce]; }, -}); \ No newline at end of file + }); diff --git a/contracts/ownable/src/witnesses/interface.ts b/contracts/ownable/src/witnesses/interface.ts index 3faedeb8..94431006 100644 --- a/contracts/ownable/src/witnesses/interface.ts +++ b/contracts/ownable/src/witnesses/interface.ts @@ -24,5 +24,5 @@ export interface IZ_OwnablePKWitnesses

{ * @param context - The witness context containing the private state. * @returns A tuple of the private state and the secret nonce as a Uint8Array. */ - offchainNonce(context: WitnessContext): [P, Uint8Array]; + offchainNonce(context: WitnessContext): [P, Uint8Array]; } From 0bafd7b6284c22db6055498614879564741da1db Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 11 Aug 2025 03:14:12 -0300 Subject: [PATCH 080/322] remove underscore in module name --- ...Z_OwnablePK.compact => ZOwnablePK.compact} | 8 +-- ...Z_OwnablePK.test.ts => ZOwnablePK.test.ts} | 24 ++++----- ...nablePK.compact => MockZOwnablePK.compact} | 18 +++---- ...ePKSimulator.ts => ZOwnablePKSimulator.ts} | 52 +++++++++---------- ...ePKWitnesses.ts => ZOwnablePKWitnesses.ts} | 22 ++++---- contracts/ownable/src/witnesses/interface.ts | 17 +----- 6 files changed, 64 insertions(+), 77 deletions(-) rename contracts/ownable/src/{Z_OwnablePK.compact => ZOwnablePK.compact} (94%) rename contracts/ownable/src/test/{Z_OwnablePK.test.ts => ZOwnablePK.test.ts} (92%) rename contracts/ownable/src/test/mocks/{MockZ_OwnablePK.compact => MockZOwnablePK.compact} (55%) rename contracts/ownable/src/test/simulators/{Z_OwnablePKSimulator.ts => ZOwnablePKSimulator.ts} (83%) rename contracts/ownable/src/witnesses/{Z_OwnablePKWitnesses.ts => ZOwnablePKWitnesses.ts} (60%) diff --git a/contracts/ownable/src/Z_OwnablePK.compact b/contracts/ownable/src/ZOwnablePK.compact similarity index 94% rename from contracts/ownable/src/Z_OwnablePK.compact rename to contracts/ownable/src/ZOwnablePK.compact index aa03f457..935bcb74 100644 --- a/contracts/ownable/src/Z_OwnablePK.compact +++ b/contracts/ownable/src/ZOwnablePK.compact @@ -3,10 +3,10 @@ pragma language_version >= 0.16.0; /** - * @module Z_OwnablePK + * @module ZOwnablePK * @description A shielded Ownable library. */ -module Z_OwnablePK { +module ZOwnablePK { import CompactStandardLibrary; export ledger _ownerCommitment: Bytes<32>; @@ -59,7 +59,7 @@ module Z_OwnablePK { persistentHash>>([default>, nonce]), _instanceSalt, _counter as Field as Bytes<32>, - pad(32, "Z_OwnablePK:renounced:"), + pad(32, "ZOwnablePK:renounced:"), ] ); @@ -89,7 +89,7 @@ module Z_OwnablePK { id, _instanceSalt, counter as Field as Bytes<32>, - pad(32, "Z_OwnablePK:shield:") + pad(32, "ZOwnablePK:shield:") ] ); } diff --git a/contracts/ownable/src/test/Z_OwnablePK.test.ts b/contracts/ownable/src/test/ZOwnablePK.test.ts similarity index 92% rename from contracts/ownable/src/test/Z_OwnablePK.test.ts rename to contracts/ownable/src/test/ZOwnablePK.test.ts index 152ed3a0..1995b651 100644 --- a/contracts/ownable/src/test/Z_OwnablePK.test.ts +++ b/contracts/ownable/src/test/ZOwnablePK.test.ts @@ -6,8 +6,8 @@ import { } from '@midnight-ntwrk/compact-runtime'; import { beforeEach, describe, expect, it } from 'vitest'; import type { ZswapCoinPublicKey } from '../artifacts/MockOwnable/contract/index.cjs'; -import { Z_OwnablePKPrivateState } from '../witnesses/Z_OwnablePKWitnesses.js'; -import { Z_OwnablePKSimulator } from './simulators/Z_OwnablePKSimulator.js'; +import { ZOwnablePKPrivateState } from '../witnesses/ZOwnablePKWitnesses.js'; +import { ZOwnablePKSimulator } from './simulators/ZOwnablePKSimulator.js'; import * as utils from './utils/address.js'; const OWNER = String(Buffer.from('OWNER', 'ascii').toString('hex')).padStart( @@ -24,11 +24,11 @@ const Z_OWNER = utils.encodeToPK('OWNER'); const Z_NEW_OWNER = utils.encodeToPK('NEW_OWNER'); const INSTANCE_SALT = new Uint8Array(32).fill(8675309); -const DOMAIN = 'Z_OwnablePK:shield:'; +const DOMAIN = 'ZOwnablePK:shield:'; const INIT_COUNTER = 1n; let secretNonce: Uint8Array; -let ownable: Z_OwnablePKSimulator; +let ownable: ZOwnablePKSimulator; /** Helpers */ const createIdHash = ( @@ -81,12 +81,12 @@ const buildCommitment = ( return commitment; }; -describe('Z_OwnablePK', () => { +describe('ZOwnablePK', () => { describe('before initialize', () => { it('should fail when setting owner commitment as 0', () => { expect(() => { const badCommitment = new Uint8Array(32).fill(0); - new Z_OwnablePKSimulator(badCommitment, INSTANCE_SALT); + new ZOwnablePKSimulator(badCommitment, INSTANCE_SALT); }).toThrow('Invalid parameters'); }); @@ -94,7 +94,7 @@ describe('Z_OwnablePK', () => { const notZeroPK = utils.encodeToPK('NOT_ZERO'); const notZeroNonce = new Uint8Array(32).fill(1); const nonZeroId = createIdHash(notZeroPK, notZeroNonce); - ownable = new Z_OwnablePKSimulator(nonZeroId, INSTANCE_SALT); + ownable = new ZOwnablePKSimulator(nonZeroId, INSTANCE_SALT); const nonZeroCommitment = buildCommitmentFromId( nonZeroId, @@ -108,13 +108,13 @@ describe('Z_OwnablePK', () => { describe('after initialization', () => { beforeEach(() => { // Create private state object and generate nonce - const PS = Z_OwnablePKPrivateState.generate(); + const PS = ZOwnablePKPrivateState.generate(); // Bind nonce for convenience secretNonce = PS.offchainNonce; // Prepare owner ID with gen nonce const ownerId = createIdHash(Z_OWNER, secretNonce); // Deploy contract with derived owner commitment and PS - ownable = new Z_OwnablePKSimulator(ownerId, INSTANCE_SALT, { + ownable = new ZOwnablePKSimulator(ownerId, INSTANCE_SALT, { privateState: PS, }); }); @@ -228,7 +228,7 @@ describe('Z_OwnablePK', () => { beforeEach(() => { // Prepare new owner commitment - newOwnerNonce = Z_OwnablePKPrivateState.generate().offchainNonce; + newOwnerNonce = ZOwnablePKPrivateState.generate().offchainNonce; newCounter = INIT_COUNTER + 1n; newIdHash = createIdHash(Z_NEW_OWNER, newOwnerNonce); newOwnerCommitment = buildCommitment( @@ -282,14 +282,14 @@ describe('Z_OwnablePK', () => { * @description More thoroughly tested in `_transferOwnership` * */ it('should bump instance after transfer', () => { - const beforeInstance = ownable.getPublicState().Z_OwnablePK__counter; + const beforeInstance = ownable.getPublicState().ZOwnablePK__counter; // Transfer ownable.setCaller(OWNER); ownable.transferOwnership(newOwnerCommitment); // Check counter - const afterInstance = ownable.getPublicState().Z_OwnablePK__counter; + const afterInstance = ownable.getPublicState().ZOwnablePK__counter; expect(afterInstance).toEqual(beforeInstance + 1n); }); diff --git a/contracts/ownable/src/test/mocks/MockZ_OwnablePK.compact b/contracts/ownable/src/test/mocks/MockZOwnablePK.compact similarity index 55% rename from contracts/ownable/src/test/mocks/MockZ_OwnablePK.compact rename to contracts/ownable/src/test/mocks/MockZOwnablePK.compact index 4d425dbf..f4cf6505 100644 --- a/contracts/ownable/src/test/mocks/MockZ_OwnablePK.compact +++ b/contracts/ownable/src/test/mocks/MockZOwnablePK.compact @@ -3,35 +3,35 @@ pragma language_version >= 0.15.0; import CompactStandardLibrary; -import "../../Z_OwnablePK" prefix Z_OwnablePK_; +import "../../ZOwnablePK" prefix ZOwnablePK_; export { ZswapCoinPublicKey, ContractAddress, Either, Maybe }; -export { Z_OwnablePK__ownerCommitment, Z_OwnablePK__counter }; +export { ZOwnablePK__ownerCommitment, ZOwnablePK__counter }; constructor(initOwnerCommitment: Bytes<32>, instanceSalt: Bytes<32>) { - Z_OwnablePK_initialize(initOwnerCommitment, instanceSalt); + ZOwnablePK_initialize(initOwnerCommitment, instanceSalt); } export circuit owner(): Bytes<32> { - return Z_OwnablePK_owner(); + return ZOwnablePK_owner(); } export circuit transferOwnership(newOwnerCommitment: Bytes<32>): [] { - return Z_OwnablePK_transferOwnership(disclose(newOwnerCommitment)); + return ZOwnablePK_transferOwnership(disclose(newOwnerCommitment)); } export circuit renounceOwnership(): [] { - return Z_OwnablePK_renounceOwnership(); + return ZOwnablePK_renounceOwnership(); } export circuit assertOnlyOwner(): [] { - return Z_OwnablePK_assertOnlyOwner(); + return ZOwnablePK_assertOnlyOwner(); } export circuit hashCommitment(id: Bytes<32>, counter: Uint<64>): Bytes<32> { - return Z_OwnablePK_hashCommitment(id, counter); + return ZOwnablePK_hashCommitment(id, counter); } export circuit _transferOwnership(newOwnerCommitment: Bytes<32>): [] { - return Z_OwnablePK__transferOwnership(newOwnerCommitment); + return ZOwnablePK__transferOwnership(newOwnerCommitment); } diff --git a/contracts/ownable/src/test/simulators/Z_OwnablePKSimulator.ts b/contracts/ownable/src/test/simulators/ZOwnablePKSimulator.ts similarity index 83% rename from contracts/ownable/src/test/simulators/Z_OwnablePKSimulator.ts rename to contracts/ownable/src/test/simulators/ZOwnablePKSimulator.ts index c8520211..abc9e1b7 100644 --- a/contracts/ownable/src/test/simulators/Z_OwnablePKSimulator.ts +++ b/contracts/ownable/src/test/simulators/ZOwnablePKSimulator.ts @@ -8,11 +8,11 @@ import { type Ledger, ledger, Contract as MockOwnable, -} from '../../artifacts/MockZ_OwnablePK/contract/index.cjs'; // Combined imports +} from '../../artifacts/MockZOwnablePK/contract/index.cjs'; // Combined imports import { - Z_OwnablePKPrivateState, - Z_OwnablePKWitnesses, -} from '../../witnesses/Z_OwnablePKWitnesses.js'; + ZOwnablePKPrivateState, + ZOwnablePKWitnesses, +} from '../../witnesses/ZOwnablePKWitnesses.js'; import type { ContextlessCircuits, ExtractImpureCircuits, @@ -23,32 +23,32 @@ import { AbstractContractSimulator } from '../utils/AbstractContractSimulator.js import { SimulatorStateManager } from '../utils/SimualatorStateManager.js'; type OwnableSimOptions = SimulatorOptions< - Z_OwnablePKPrivateState, - typeof Z_OwnablePKWitnesses + ZOwnablePKPrivateState, + typeof ZOwnablePKWitnesses >; /** * @description A simulator implementation of a contract for testing purposes. - * @template P - The private state type, fixed to Z_OwnablePKPrivateState. + * @template P - The private state type, fixed to ZOwnablePKPrivateState. * @template L - The ledger type, fixed to Contract.Ledger. */ -export class Z_OwnablePKSimulator extends AbstractContractSimulator< - Z_OwnablePKPrivateState, +export class ZOwnablePKSimulator extends AbstractContractSimulator< + ZOwnablePKPrivateState, Ledger > { - readonly contract: MockOwnable; + readonly contract: MockOwnable; readonly contractAddress: string; - private stateManager: SimulatorStateManager; + private stateManager: SimulatorStateManager; private callerOverride: CoinPublicKey | null = null; private _pureCircuitProxy?: ContextlessCircuits< - ExtractPureCircuits>, - Z_OwnablePKPrivateState + ExtractPureCircuits>, + ZOwnablePKPrivateState >; private _impureCircuitProxy?: ContextlessCircuits< - ExtractImpureCircuits>, - Z_OwnablePKPrivateState + ExtractImpureCircuits>, + ZOwnablePKPrivateState >; constructor( @@ -60,14 +60,14 @@ export class Z_OwnablePKSimulator extends AbstractContractSimulator< // Setup initial state const { - privateState = Z_OwnablePKPrivateState.generate(), - witnesses = Z_OwnablePKWitnesses(), + privateState = ZOwnablePKPrivateState.generate(), + witnesses = ZOwnablePKWitnesses(), coinPK = '0'.repeat(64), address = sampleContractAddress(), } = options; const constructorArgs = [initOwner, instanceSalt]; - this.contract = new MockOwnable(witnesses); + this.contract = new MockOwnable(witnesses); this.stateManager = new SimulatorStateManager( this.contract, @@ -97,7 +97,7 @@ export class Z_OwnablePKSimulator extends AbstractContractSimulator< * scoped to the overridden caller. Otherwise, the existing context is reused as-is. * @returns A circuit context adjusted for the current simulated caller. */ - protected getCallerContext(): CircuitContext { + protected getCallerContext(): CircuitContext { return { ...this.circuitContext, currentZswapLocalState: this.callerOverride @@ -116,12 +116,12 @@ export class Z_OwnablePKSimulator extends AbstractContractSimulator< * @returns A proxy object exposing pure circuit functions without requiring explicit context. */ protected get pureCircuit(): ContextlessCircuits< - ExtractPureCircuits>, - Z_OwnablePKPrivateState + ExtractPureCircuits>, + ZOwnablePKPrivateState > { if (!this._pureCircuitProxy) { this._pureCircuitProxy = this.createPureCircuitProxy< - MockOwnable['circuits'] + MockOwnable['circuits'] >(this.contract.circuits, () => this.circuitContext); } return this._pureCircuitProxy; @@ -137,12 +137,12 @@ export class Z_OwnablePKSimulator extends AbstractContractSimulator< * @returns A proxy object exposing impure circuit functions without requiring explicit context management. */ protected get impureCircuit(): ContextlessCircuits< - ExtractImpureCircuits>, - Z_OwnablePKPrivateState + ExtractImpureCircuits>, + ZOwnablePKPrivateState > { if (!this._impureCircuitProxy) { this._impureCircuitProxy = this.createImpureCircuitProxy< - MockOwnable['impureCircuits'] + MockOwnable['impureCircuits'] >( this.contract.impureCircuits, () => this.getCallerContext(), @@ -236,7 +236,7 @@ export class Z_OwnablePKSimulator extends AbstractContractSimulator< */ injectSecretNonce: ( newNonce: Buffer, - ): Z_OwnablePKPrivateState => { + ): ZOwnablePKPrivateState => { const currentState = this.stateManager.getContext().currentPrivateState; const updatedState = { ...currentState, offchainNonce: newNonce }; this.stateManager.updatePrivateState(updatedState); diff --git a/contracts/ownable/src/witnesses/Z_OwnablePKWitnesses.ts b/contracts/ownable/src/witnesses/ZOwnablePKWitnesses.ts similarity index 60% rename from contracts/ownable/src/witnesses/Z_OwnablePKWitnesses.ts rename to contracts/ownable/src/witnesses/ZOwnablePKWitnesses.ts index 4bfcf082..f4682cc6 100644 --- a/contracts/ownable/src/witnesses/Z_OwnablePKWitnesses.ts +++ b/contracts/ownable/src/witnesses/ZOwnablePKWitnesses.ts @@ -1,12 +1,12 @@ import { getRandomValues } from 'node:crypto'; import type { WitnessContext } from '@midnight-ntwrk/compact-runtime'; -import type { Ledger } from '../artifacts/MockZ_OwnablePK/contract/index.cjs'; -import type { IZ_OwnablePKWitnesses } from './interface.js'; +import type { Ledger } from '../artifacts/MockZOwnablePK/contract/index.cjs'; +import type { IZOwnablePKWitnesses } from './interface.js'; /** * @description Represents the private state of an ownable contract, storing a secret nonce. */ -export type Z_OwnablePKPrivateState = { +export type ZOwnablePKPrivateState = { /** @description A 32-byte secret nonce used as a privacy additive. */ offchainNonce: Buffer; }; @@ -14,25 +14,25 @@ export type Z_OwnablePKPrivateState = { /** * @description Utility object for managing the private state of an Ownable contract. */ -export const Z_OwnablePKPrivateState = { +export const ZOwnablePKPrivateState = { /** * @description Generates a new private state with a random secret nonce. - * @returns A fresh Z_OwnablePKPrivateState instance. + * @returns A fresh ZOwnablePKPrivateState instance. */ - generate: (): Z_OwnablePKPrivateState => { + generate: (): ZOwnablePKPrivateState => { return { offchainNonce: getRandomValues(Buffer.alloc(32)) }; }, }; /** * @description Factory function creating witness implementations for Ownable operations. - * @returns An object implementing the Witnesses interface for Z_OwnablePKPrivateState. + * @returns An object implementing the Witnesses interface for ZOwnablePKPrivateState. */ -export const Z_OwnablePKWitnesses = - (): IZ_OwnablePKWitnesses => ({ +export const ZOwnablePKWitnesses = + (): IZOwnablePKWitnesses => ({ offchainNonce( - context: WitnessContext, - ): [Z_OwnablePKPrivateState, Uint8Array] { + context: WitnessContext, + ): [ZOwnablePKPrivateState, Uint8Array] { return [context.privateState, context.privateState.offchainNonce]; }, }); diff --git a/contracts/ownable/src/witnesses/interface.ts b/contracts/ownable/src/witnesses/interface.ts index 94431006..7799c8ef 100644 --- a/contracts/ownable/src/witnesses/interface.ts +++ b/contracts/ownable/src/witnesses/interface.ts @@ -1,24 +1,11 @@ import type { WitnessContext } from '@midnight-ntwrk/compact-runtime'; -import type { Ledger } from '../artifacts/MockZ_OwnablePK/contract/index.cjs'; // Combined imports - -/** - * @description Interface defining the witness methods for ownable operations. - * @template P - The private state type. - */ -export interface IOwnableWitnesses

{ - /** - * Retrieves the secret key from the private state. - * @param context - The witness context containing the private state. - * @returns A tuple of the private state and the secret key as a Uint8Array. - */ - localSecretKey(context: WitnessContext): [P, Uint8Array]; -} +import type { Ledger } from '../artifacts/MockZOwnablePK/contract/index.cjs'; // Combined imports /** * @description Interface defining the witness methods for Ownable operations. * @template P - The private state type. */ -export interface IZ_OwnablePKWitnesses

{ +export interface IZOwnablePKWitnesses

{ /** * Retrieves the secret nonce from the private state. * @param context - The witness context containing the private state. From dee9254e0e22c96b4905542a037436511f77540d Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 11 Aug 2025 03:14:45 -0300 Subject: [PATCH 081/322] remove unused file --- contracts/ownable/src/test/types/string.ts | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 contracts/ownable/src/test/types/string.ts diff --git a/contracts/ownable/src/test/types/string.ts b/contracts/ownable/src/test/types/string.ts deleted file mode 100644 index 430a139e..00000000 --- a/contracts/ownable/src/test/types/string.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type MaybeString = { - is_some: boolean; - value: string; -}; From a47ea19499bdc7a50c251c222d37e3d5dbf4f2b0 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 12 Aug 2025 00:36:08 -0300 Subject: [PATCH 082/322] fix var names --- contracts/ownable/src/test/ZOwnablePK.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/contracts/ownable/src/test/ZOwnablePK.test.ts b/contracts/ownable/src/test/ZOwnablePK.test.ts index 1995b651..38554643 100644 --- a/contracts/ownable/src/test/ZOwnablePK.test.ts +++ b/contracts/ownable/src/test/ZOwnablePK.test.ts @@ -85,8 +85,8 @@ describe('ZOwnablePK', () => { describe('before initialize', () => { it('should fail when setting owner commitment as 0', () => { expect(() => { - const badCommitment = new Uint8Array(32).fill(0); - new ZOwnablePKSimulator(badCommitment, INSTANCE_SALT); + const badId = new Uint8Array(32).fill(0); + new ZOwnablePKSimulator(badId, INSTANCE_SALT); }).toThrow('Invalid parameters'); }); @@ -263,11 +263,11 @@ describe('ZOwnablePK', () => { expect(ownable.assertOnlyOwner()).not.to.throw; }); - it('should fail when transferring to zero', () => { + it('should fail when transferring to id zero', () => { ownable.setCaller(OWNER); - const badCommitment = new Uint8Array(32).fill(0); + const badId = new Uint8Array(32).fill(0); expect(() => { - ownable.transferOwnership(badCommitment); + ownable.transferOwnership(badId); }).toThrow('Invalid parameters'); }); From 9233b473d9ae5b4b687fdc33d166b16ed1ca0c92 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 12 Aug 2025 00:36:54 -0300 Subject: [PATCH 083/322] add witness injection to sim --- .../test/simulators/ZOwnablePKSimulator.ts | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/contracts/ownable/src/test/simulators/ZOwnablePKSimulator.ts b/contracts/ownable/src/test/simulators/ZOwnablePKSimulator.ts index abc9e1b7..5f3cd893 100644 --- a/contracts/ownable/src/test/simulators/ZOwnablePKSimulator.ts +++ b/contracts/ownable/src/test/simulators/ZOwnablePKSimulator.ts @@ -8,7 +8,7 @@ import { type Ledger, ledger, Contract as MockOwnable, -} from '../../artifacts/MockZOwnablePK/contract/index.cjs'; // Combined imports +} from '../../artifacts/MockZOwnablePK/contract/index.cjs'; import { ZOwnablePKPrivateState, ZOwnablePKWitnesses, @@ -36,10 +36,11 @@ export class ZOwnablePKSimulator extends AbstractContractSimulator< ZOwnablePKPrivateState, Ledger > { - readonly contract: MockOwnable; + contract: MockOwnable; readonly contractAddress: string; private stateManager: SimulatorStateManager; private callerOverride: CoinPublicKey | null = null; + private _witnesses: ReturnType; private _pureCircuitProxy?: ContextlessCircuits< ExtractPureCircuits>, @@ -77,6 +78,8 @@ export class ZOwnablePKSimulator extends AbstractContractSimulator< ...constructorArgs, ); this.contractAddress = this.circuitContext.transactionContext.address; + this._witnesses = witnesses; + this.contract = new MockOwnable(this._witnesses); } get circuitContext() { @@ -184,6 +187,25 @@ export class ZOwnablePKSimulator extends AbstractContractSimulator< }; } + public get witnesses(): ReturnType { + return this._witnesses; + } + + public set witnesses(newWitnesses: ReturnType) { + this._witnesses = newWitnesses; + this.contract = new MockOwnable(this._witnesses); + } + + public overrideWitness( + key: K, + fn: typeof this._witnesses[K], + ) { + this.witnesses = { + ...this._witnesses, + [key]: fn, + }; + } + /** * @description Returns the shielded owner. * @returns The shielded owner. From 4ed11233fe47a3308dbdba06418bfc4c3645e750 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 12 Aug 2025 00:49:38 -0300 Subject: [PATCH 084/322] add callerCtx --- contracts/ownable/src/test/ZOwnablePK.test.ts | 32 +++++++++++-------- .../test/simulators/ZOwnablePKSimulator.ts | 18 ++++++----- 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/contracts/ownable/src/test/ZOwnablePK.test.ts b/contracts/ownable/src/test/ZOwnablePK.test.ts index 38554643..2189b6b6 100644 --- a/contracts/ownable/src/test/ZOwnablePK.test.ts +++ b/contracts/ownable/src/test/ZOwnablePK.test.ts @@ -169,7 +169,7 @@ describe('ZOwnablePK', () => { secretNonce, ); - ownable.setCaller(OWNER); + ownable.callerCtx.setCaller(OWNER); expect(ownable.assertOnlyOwner()).to.not.throw; }); @@ -184,7 +184,7 @@ describe('ZOwnablePK', () => { ); // Set caller and call circuit - ownable.setCaller(OWNER); + ownable.callerCtx.setCaller(OWNER); expect(() => { ownable.assertOnlyOwner(); }).toThrow('Forbidden'); @@ -196,7 +196,7 @@ describe('ZOwnablePK', () => { secretNonce, ); - ownable.setCaller(UNAUTHORIZED); + ownable.callerCtx.setCaller(UNAUTHORIZED); expect(() => { ownable.assertOnlyOwner(); }).toThrow('Forbidden'); @@ -213,7 +213,7 @@ describe('ZOwnablePK', () => { ); // Set unauthorized caller and call circuit - ownable.setCaller(UNAUTHORIZED); + ownable.callerCtx.setCaller(UNAUTHORIZED); expect(() => { ownable.assertOnlyOwner(); }).toThrow('Forbidden'); @@ -241,30 +241,30 @@ describe('ZOwnablePK', () => { }); it('should transfer ownership', () => { - ownable.setCaller(OWNER); + ownable.callerCtx.setCaller(OWNER); ownable.transferOwnership(newIdHash); expect(ownable.owner()).toEqual(newOwnerCommitment); // Old owner - ownable.setCaller(OWNER); + ownable.callerCtx.setCaller(OWNER); expect(() => { ownable.assertOnlyOwner(); }).toThrow('Forbidden'); // Unauthorized - ownable.setCaller(UNAUTHORIZED); + ownable.callerCtx.setCaller(UNAUTHORIZED); expect(() => { ownable.assertOnlyOwner(); }).toThrow('Forbidden'); // New owner - ownable.setCaller(NEW_OWNER); + ownable.callerCtx.setCaller(NEW_OWNER); ownable.privateState.injectSecretNonce(Buffer.from(newOwnerNonce)); expect(ownable.assertOnlyOwner()).not.to.throw; }); it('should fail when transferring to id zero', () => { - ownable.setCaller(OWNER); + ownable.callerCtx.setCaller(OWNER); const badId = new Uint8Array(32).fill(0); expect(() => { ownable.transferOwnership(badId); @@ -272,7 +272,7 @@ describe('ZOwnablePK', () => { }); it('should fail when unauthorized transfers ownership', () => { - ownable.setCaller(UNAUTHORIZED); + ownable.callerCtx.setCaller(UNAUTHORIZED); expect(() => { ownable.transferOwnership(newOwnerCommitment); }).toThrow('Forbidden'); @@ -285,7 +285,7 @@ describe('ZOwnablePK', () => { const beforeInstance = ownable.getPublicState().ZOwnablePK__counter; // Transfer - ownable.setCaller(OWNER); + ownable.callerCtx.setCaller(OWNER); ownable.transferOwnership(newOwnerCommitment); // Check counter @@ -305,7 +305,7 @@ describe('ZOwnablePK', () => { expect(initCommitment).toEqual(expInitCommitment); // Transfer ownership to self with the same id -> `H(pk, nonce)` - ownable.setCaller(OWNER); + ownable.callerCtx.setCaller(OWNER); ownable.transferOwnership(repeatedId); // Check commitments don't match @@ -322,9 +322,15 @@ describe('ZOwnablePK', () => { expect(newCommitment).toEqual(expNewCommitment); // Check same owner maintains permissions after transfer - ownable.setCaller(OWNER); + ownable.callerCtx.setCaller(OWNER); expect(ownable.assertOnlyOwner()).not.to.throw; }); }); + + describe('renounceOwnership', () => { + it('should renounce ownership', () => { + + }) + }) }); }); diff --git a/contracts/ownable/src/test/simulators/ZOwnablePKSimulator.ts b/contracts/ownable/src/test/simulators/ZOwnablePKSimulator.ts index 5f3cd893..f55a3715 100644 --- a/contracts/ownable/src/test/simulators/ZOwnablePKSimulator.ts +++ b/contracts/ownable/src/test/simulators/ZOwnablePKSimulator.ts @@ -157,14 +157,6 @@ export class ZOwnablePKSimulator extends AbstractContractSimulator< return this._impureCircuitProxy; } - /** - * @description Sets the caller context. - * @param caller The caller in context of the proceeding circuit calls. - */ - public setCaller(caller: CoinPublicKey | null): void { - this.callerOverride = caller; - } - /** * @description Resets the cached circuit proxy instances. * This is useful if the underlying contract state or circuit context has changed, @@ -269,4 +261,14 @@ export class ZOwnablePKSimulator extends AbstractContractSimulator< return this.stateManager.getContext().currentPrivateState.offchainNonce; }, }; + + public callerCtx = { + /** + * @description Sets the caller context. + * @param caller The caller in context of the proceeding circuit calls. + */ + setCaller: (caller: CoinPublicKey) => { + this.callerOverride = caller; + }, + } } From 876f9efa9fa29aeee20a4bf43e700aa6a9d4b83c Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 12 Aug 2025 01:57:59 -0300 Subject: [PATCH 085/322] add revealKey to renounce obfuscated --- contracts/ownable/src/ZOwnablePK.compact | 7 ++- contracts/ownable/src/test/ZOwnablePK.test.ts | 50 ++++++++++++++++--- .../src/test/mocks/MockZOwnablePK.compact | 4 ++ .../test/simulators/ZOwnablePKSimulator.ts | 8 +++ 4 files changed, 59 insertions(+), 10 deletions(-) diff --git a/contracts/ownable/src/ZOwnablePK.compact b/contracts/ownable/src/ZOwnablePK.compact index 935bcb74..a8a2b392 100644 --- a/contracts/ownable/src/ZOwnablePK.compact +++ b/contracts/ownable/src/ZOwnablePK.compact @@ -45,18 +45,17 @@ module ZOwnablePK { */ export circuit renounceOwnership(): [] { assertOnlyOwner(); - _transferOwnership(default>); + _ownerCommitment.resetToDefault(); } /** * @description Add me!!! */ - export circuit renounceOwnershipObfuscated(): [] { + export circuit renounceOwnershipObfuscated(revealKey: Bytes<32>): [] { assertOnlyOwner(); - const nonce = offchainNonce(); const obfuscatedCommitment = persistentHash>>( [ - persistentHash>>([default>, nonce]), + persistentHash>>([default>, revealKey]), _instanceSalt, _counter as Field as Bytes<32>, pad(32, "ZOwnablePK:renounced:"), diff --git a/contracts/ownable/src/test/ZOwnablePK.test.ts b/contracts/ownable/src/test/ZOwnablePK.test.ts index 2189b6b6..8b57d86c 100644 --- a/contracts/ownable/src/test/ZOwnablePK.test.ts +++ b/contracts/ownable/src/test/ZOwnablePK.test.ts @@ -23,6 +23,7 @@ const UNAUTHORIZED = String( const Z_OWNER = utils.encodeToPK('OWNER'); const Z_NEW_OWNER = utils.encodeToPK('NEW_OWNER'); const INSTANCE_SALT = new Uint8Array(32).fill(8675309); +const BAD_NONCE = Buffer.from(Buffer.alloc(32, 'BAD_NONCE')); const DOMAIN = 'ZOwnablePK:shield:'; const INIT_COUNTER = 1n; @@ -175,8 +176,7 @@ describe('ZOwnablePK', () => { it('should fail when the authorized caller has the wrong nonce', () => { // Inject bad nonce - const badNonce = Buffer.alloc(32, 'badNonce'); - ownable.privateState.injectSecretNonce(badNonce); + ownable.privateState.injectSecretNonce(BAD_NONCE); // Check nonce does not match expect(ownable.privateState.getCurrentSecretNonce()).not.toEqual( @@ -204,8 +204,7 @@ describe('ZOwnablePK', () => { it('should fail when unauthorized caller has the wrong nonce', () => { // Inject bad nonce - const badNonce = Buffer.alloc(32, 'badNonce'); - ownable.privateState.injectSecretNonce(badNonce); + ownable.privateState.injectSecretNonce(BAD_NONCE); // Check nonce does not match expect(ownable.privateState.getCurrentSecretNonce()).not.toEqual( @@ -329,8 +328,47 @@ describe('ZOwnablePK', () => { describe('renounceOwnership', () => { it('should renounce ownership', () => { + ownable.callerCtx.setCaller(OWNER); + ownable.renounceOwnership(); + + // Check owner is reset + expect(ownable.owner()).toEqual(new Uint8Array(32).fill(0)); + + // Check revoked permissions + expect(() => { + ownable.assertOnlyOwner() + }).toThrow('Forbidden'); + }); + + it('should fail when renouncing from unauthorized', () => { + ownable.callerCtx.setCaller(UNAUTHORIZED); + expect(() => { + ownable.renounceOwnership(); + }); + }); - }) - }) + it('should fail when renouncing from authorized with bad nonce', () => { + ownable.callerCtx.setCaller(OWNER); + ownable.privateState.injectSecretNonce(BAD_NONCE); + expect(() => { + ownable.renounceOwnership(); + }); + }); + + it('should fail when renouncing from unauthorized with bad nonce', () => { + ownable.callerCtx.setCaller(UNAUTHORIZED); + ownable.privateState.injectSecretNonce(BAD_NONCE); + expect(() => { + ownable.renounceOwnership(); + }); + }); + + //describe('renounceOwnershipObfuscated', () => { + // it('should renounce and obfuscate', () => { + // ownable.callerCtx.setCaller(OWNER); + // + // }) + //}) + }); }); }); diff --git a/contracts/ownable/src/test/mocks/MockZOwnablePK.compact b/contracts/ownable/src/test/mocks/MockZOwnablePK.compact index f4cf6505..4a41eb69 100644 --- a/contracts/ownable/src/test/mocks/MockZOwnablePK.compact +++ b/contracts/ownable/src/test/mocks/MockZOwnablePK.compact @@ -24,6 +24,10 @@ export circuit renounceOwnership(): [] { return ZOwnablePK_renounceOwnership(); } +export circuit renounceOwnershipObfuscated(revealKey: Bytes<32>): [] { + return ZOwnablePK_renounceOwnershipObfuscated(revealKey); +} + export circuit assertOnlyOwner(): [] { return ZOwnablePK_assertOnlyOwner(); } diff --git a/contracts/ownable/src/test/simulators/ZOwnablePKSimulator.ts b/contracts/ownable/src/test/simulators/ZOwnablePKSimulator.ts index f55a3715..130ed0d3 100644 --- a/contracts/ownable/src/test/simulators/ZOwnablePKSimulator.ts +++ b/contracts/ownable/src/test/simulators/ZOwnablePKSimulator.ts @@ -222,6 +222,14 @@ export class ZOwnablePKSimulator extends AbstractContractSimulator< this.circuits.impure.renounceOwnership(); } + + /** + * @description + */ + public renounceOwnershipObfuscated(revealKey: Uint8Array) { + this.circuits.impure.renounceOwnershipObfuscated(revealKey); + } + /** * @description Throws if called by any account other than the owner. * Use this to restrict access to sensitive circuits. From f9c52d99753d576f119e1daf8b9e3ed4e4d7cb81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Tue, 12 Aug 2025 12:48:44 -0400 Subject: [PATCH 086/322] fmt docs --- .../shieldedAccessControl/src/ShieldedAccessControl.compact | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/shieldedAccessControl/src/ShieldedAccessControl.compact b/contracts/shieldedAccessControl/src/ShieldedAccessControl.compact index a46ee9d0..925c7ffb 100644 --- a/contracts/shieldedAccessControl/src/ShieldedAccessControl.compact +++ b/contracts/shieldedAccessControl/src/ShieldedAccessControl.compact @@ -162,7 +162,7 @@ module ShieldedAccessControl { * @dev Developers must export publicly declared roles from the top-level contract to generate possible roles for each. * * @param {Bytes<32>} account - The account requesting a role. - * @param {Bytes<32>}salt - A salt value for the underlying HKDF function. + * @param {Bytes<32>} salt - A salt value for the underlying HKDF function. * @return {[]} - Empty tuple.  */ witness recoverRoles(account: Bytes<32>, salt: Bytes<32>): []; From fb01e61c0416a09ff2091ba8704b3acb54841af5 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 12 Aug 2025 14:11:04 -0300 Subject: [PATCH 087/322] remove obfuscated renounce --- contracts/ownable/src/ZOwnablePK.compact | 17 ----------------- contracts/ownable/src/test/ZOwnablePK.test.ts | 7 ------- .../src/test/mocks/MockZOwnablePK.compact | 4 ---- .../src/test/simulators/ZOwnablePKSimulator.ts | 8 -------- 4 files changed, 36 deletions(-) diff --git a/contracts/ownable/src/ZOwnablePK.compact b/contracts/ownable/src/ZOwnablePK.compact index a8a2b392..d093f45b 100644 --- a/contracts/ownable/src/ZOwnablePK.compact +++ b/contracts/ownable/src/ZOwnablePK.compact @@ -48,23 +48,6 @@ module ZOwnablePK { _ownerCommitment.resetToDefault(); } - /** - * @description Add me!!! - */ - export circuit renounceOwnershipObfuscated(revealKey: Bytes<32>): [] { - assertOnlyOwner(); - const obfuscatedCommitment = persistentHash>>( - [ - persistentHash>>([default>, revealKey]), - _instanceSalt, - _counter as Field as Bytes<32>, - pad(32, "ZOwnablePK:renounced:"), - ] - ); - - _transferOwnership(obfuscatedCommitment); - } - /** * @description Add me!!! */ diff --git a/contracts/ownable/src/test/ZOwnablePK.test.ts b/contracts/ownable/src/test/ZOwnablePK.test.ts index 8b57d86c..e2b93d4b 100644 --- a/contracts/ownable/src/test/ZOwnablePK.test.ts +++ b/contracts/ownable/src/test/ZOwnablePK.test.ts @@ -362,13 +362,6 @@ describe('ZOwnablePK', () => { ownable.renounceOwnership(); }); }); - - //describe('renounceOwnershipObfuscated', () => { - // it('should renounce and obfuscate', () => { - // ownable.callerCtx.setCaller(OWNER); - // - // }) - //}) }); }); }); diff --git a/contracts/ownable/src/test/mocks/MockZOwnablePK.compact b/contracts/ownable/src/test/mocks/MockZOwnablePK.compact index 4a41eb69..f4cf6505 100644 --- a/contracts/ownable/src/test/mocks/MockZOwnablePK.compact +++ b/contracts/ownable/src/test/mocks/MockZOwnablePK.compact @@ -24,10 +24,6 @@ export circuit renounceOwnership(): [] { return ZOwnablePK_renounceOwnership(); } -export circuit renounceOwnershipObfuscated(revealKey: Bytes<32>): [] { - return ZOwnablePK_renounceOwnershipObfuscated(revealKey); -} - export circuit assertOnlyOwner(): [] { return ZOwnablePK_assertOnlyOwner(); } diff --git a/contracts/ownable/src/test/simulators/ZOwnablePKSimulator.ts b/contracts/ownable/src/test/simulators/ZOwnablePKSimulator.ts index 130ed0d3..f55a3715 100644 --- a/contracts/ownable/src/test/simulators/ZOwnablePKSimulator.ts +++ b/contracts/ownable/src/test/simulators/ZOwnablePKSimulator.ts @@ -222,14 +222,6 @@ export class ZOwnablePKSimulator extends AbstractContractSimulator< this.circuits.impure.renounceOwnership(); } - - /** - * @description - */ - public renounceOwnershipObfuscated(revealKey: Uint8Array) { - this.circuits.impure.renounceOwnershipObfuscated(revealKey); - } - /** * @description Throws if called by any account other than the owner. * Use this to restrict access to sensitive circuits. From c05509f41f157705b1dc2e432b9881adef3e6f04 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 12 Aug 2025 14:15:38 -0300 Subject: [PATCH 088/322] fix circuit name, fmt and lint --- contracts/ownable/src/ZOwnablePK.compact | 6 +++--- contracts/ownable/src/test/ZOwnablePK.test.ts | 6 +++--- contracts/ownable/src/test/mocks/MockZOwnablePK.compact | 4 ++-- .../ownable/src/test/simulators/ZOwnablePKSimulator.ts | 8 ++++---- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/contracts/ownable/src/ZOwnablePK.compact b/contracts/ownable/src/ZOwnablePK.compact index d093f45b..248eeb1a 100644 --- a/contracts/ownable/src/ZOwnablePK.compact +++ b/contracts/ownable/src/ZOwnablePK.compact @@ -55,14 +55,14 @@ module ZOwnablePK { const caller = ownPublicKey(); const nonce = offchainNonce(); const id = persistentHash>>([caller.bytes, nonce]); - assert(_ownerCommitment == hashCommitment(id, _counter), "Forbidden"); + assert(_ownerCommitment == computeOwnerCommitment(id, _counter), "Forbidden"); } // computePKCommitment || generateCommitment /** * @description Add me!!! */ - export circuit hashCommitment( + export circuit computeOwnerCommitment( id: Bytes<32>, counter: Uint<64>, ): Bytes<32> { @@ -81,6 +81,6 @@ module ZOwnablePK { */ export circuit _transferOwnership(newOwnerId: Bytes<32>): [] { _counter.increment(1); - _ownerCommitment = hashCommitment(disclose(newOwnerId), _counter); + _ownerCommitment = computeOwnerCommitment(disclose(newOwnerId), _counter); } } diff --git a/contracts/ownable/src/test/ZOwnablePK.test.ts b/contracts/ownable/src/test/ZOwnablePK.test.ts index e2b93d4b..62a4a0ae 100644 --- a/contracts/ownable/src/test/ZOwnablePK.test.ts +++ b/contracts/ownable/src/test/ZOwnablePK.test.ts @@ -123,13 +123,13 @@ describe('ZOwnablePK', () => { /** * @TODO parameterize */ - describe('hashCommitment', () => { + describe('computeOwnerCommitment', () => { it('should match local and contract commitment algorithms', () => { const id = createIdHash(Z_OWNER, secretNonce); const counter = INIT_COUNTER; // Check buildCommitmentFromId - const hashFromContract = ownable.hashCommitment(id, counter); + const hashFromContract = ownable.computeOwnerCommitment(id, counter); const hashFromHelper1 = buildCommitmentFromId( id, INSTANCE_SALT, @@ -336,7 +336,7 @@ describe('ZOwnablePK', () => { // Check revoked permissions expect(() => { - ownable.assertOnlyOwner() + ownable.assertOnlyOwner(); }).toThrow('Forbidden'); }); diff --git a/contracts/ownable/src/test/mocks/MockZOwnablePK.compact b/contracts/ownable/src/test/mocks/MockZOwnablePK.compact index f4cf6505..b7c3548f 100644 --- a/contracts/ownable/src/test/mocks/MockZOwnablePK.compact +++ b/contracts/ownable/src/test/mocks/MockZOwnablePK.compact @@ -28,8 +28,8 @@ export circuit assertOnlyOwner(): [] { return ZOwnablePK_assertOnlyOwner(); } -export circuit hashCommitment(id: Bytes<32>, counter: Uint<64>): Bytes<32> { - return ZOwnablePK_hashCommitment(id, counter); +export circuit computeOwnerCommitment(id: Bytes<32>, counter: Uint<64>): Bytes<32> { + return ZOwnablePK_computeOwnerCommitment(id, counter); } export circuit _transferOwnership(newOwnerCommitment: Bytes<32>): [] { diff --git a/contracts/ownable/src/test/simulators/ZOwnablePKSimulator.ts b/contracts/ownable/src/test/simulators/ZOwnablePKSimulator.ts index f55a3715..dc6543d6 100644 --- a/contracts/ownable/src/test/simulators/ZOwnablePKSimulator.ts +++ b/contracts/ownable/src/test/simulators/ZOwnablePKSimulator.ts @@ -190,7 +190,7 @@ export class ZOwnablePKSimulator extends AbstractContractSimulator< public overrideWitness( key: K, - fn: typeof this._witnesses[K], + fn: (typeof this._witnesses)[K], ) { this.witnesses = { ...this._witnesses, @@ -233,8 +233,8 @@ export class ZOwnablePKSimulator extends AbstractContractSimulator< /** * @description */ - public hashCommitment(id: Uint8Array, counter: bigint): Uint8Array { - return this.circuits.impure.hashCommitment(id, counter); + public computeOwnerCommitment(id: Uint8Array, counter: bigint): Uint8Array { + return this.circuits.impure.computeOwnerCommitment(id, counter); } /** @@ -270,5 +270,5 @@ export class ZOwnablePKSimulator extends AbstractContractSimulator< setCaller: (caller: CoinPublicKey) => { this.callerOverride = caller; }, - } + }; } From 249663b7246b962306d591ba00047d6c89fb8ab4 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 12 Aug 2025 15:56:22 -0300 Subject: [PATCH 089/322] fix error msg --- contracts/ownable/src/ZOwnablePK.compact | 7 +- contracts/ownable/src/test/ZOwnablePK.test.ts | 226 +++++++++++------- 2 files changed, 137 insertions(+), 96 deletions(-) diff --git a/contracts/ownable/src/ZOwnablePK.compact b/contracts/ownable/src/ZOwnablePK.compact index 248eeb1a..4f157372 100644 --- a/contracts/ownable/src/ZOwnablePK.compact +++ b/contracts/ownable/src/ZOwnablePK.compact @@ -19,7 +19,7 @@ module ZOwnablePK { * @description Add me!!! */ export circuit initialize(ownerId: Bytes<32>, instanceSalt: Bytes<32>): [] { - assert(ownerId != default>, "Invalid parameters"); + assert(ownerId != default>, "ZOwnablePK: invalid id"); _instanceSalt = disclose(instanceSalt); _transferOwnership(ownerId); } @@ -36,7 +36,7 @@ module ZOwnablePK { */ export circuit transferOwnership(newOwnerId: Bytes<32>): [] { assertOnlyOwner(); - assert(newOwnerId != default>, "Invalid parameters"); + assert(newOwnerId != default>, "ZOwnablePK: invalid id"); _transferOwnership(newOwnerId); } @@ -55,10 +55,9 @@ module ZOwnablePK { const caller = ownPublicKey(); const nonce = offchainNonce(); const id = persistentHash>>([caller.bytes, nonce]); - assert(_ownerCommitment == computeOwnerCommitment(id, _counter), "Forbidden"); + assert(_ownerCommitment == computeOwnerCommitment(id, _counter), "ZOwnablePK: caller is not the owner"); } - // computePKCommitment || generateCommitment /** * @description Add me!!! */ diff --git a/contracts/ownable/src/test/ZOwnablePK.test.ts b/contracts/ownable/src/test/ZOwnablePK.test.ts index 62a4a0ae..dbb63417 100644 --- a/contracts/ownable/src/test/ZOwnablePK.test.ts +++ b/contracts/ownable/src/test/ZOwnablePK.test.ts @@ -88,7 +88,7 @@ describe('ZOwnablePK', () => { expect(() => { const badId = new Uint8Array(32).fill(0); new ZOwnablePKSimulator(badId, INSTANCE_SALT); - }).toThrow('Invalid parameters'); + }).toThrow('ZOwnablePK: invalid id'); }); it('should initialize with non-zero commitment', () => { @@ -120,36 +120,6 @@ describe('ZOwnablePK', () => { }); }); - /** - * @TODO parameterize - */ - describe('computeOwnerCommitment', () => { - it('should match local and contract commitment algorithms', () => { - const id = createIdHash(Z_OWNER, secretNonce); - const counter = INIT_COUNTER; - - // Check buildCommitmentFromId - const hashFromContract = ownable.computeOwnerCommitment(id, counter); - const hashFromHelper1 = buildCommitmentFromId( - id, - INSTANCE_SALT, - counter, - ); - expect(hashFromContract).toEqual(hashFromHelper1); - - // Check buildCommitment - const hashFromHelper2 = buildCommitment( - Z_OWNER, - secretNonce, - INSTANCE_SALT, - counter, - DOMAIN, - ); - expect(hashFromContract).toEqual(hashFromHelper1); - expect(hashFromHelper1).toEqual(hashFromHelper2); - }); - }); - describe('owner', () => { it('should return the correct owner commitment', () => { const expCommitment = buildCommitment( @@ -163,62 +133,6 @@ describe('ZOwnablePK', () => { }); }); - describe('assertOnlyOwner', () => { - it('should allow authorized caller with correct nonce to call', () => { - // Check nonce is correct - expect(ownable.privateState.getCurrentSecretNonce()).toEqual( - secretNonce, - ); - - ownable.callerCtx.setCaller(OWNER); - expect(ownable.assertOnlyOwner()).to.not.throw; - }); - - it('should fail when the authorized caller has the wrong nonce', () => { - // Inject bad nonce - ownable.privateState.injectSecretNonce(BAD_NONCE); - - // Check nonce does not match - expect(ownable.privateState.getCurrentSecretNonce()).not.toEqual( - secretNonce, - ); - - // Set caller and call circuit - ownable.callerCtx.setCaller(OWNER); - expect(() => { - ownable.assertOnlyOwner(); - }).toThrow('Forbidden'); - }); - - it('should fail when unauthorized caller has the correct nonce', () => { - // Check nonce is correct - expect(ownable.privateState.getCurrentSecretNonce()).toEqual( - secretNonce, - ); - - ownable.callerCtx.setCaller(UNAUTHORIZED); - expect(() => { - ownable.assertOnlyOwner(); - }).toThrow('Forbidden'); - }); - - it('should fail when unauthorized caller has the wrong nonce', () => { - // Inject bad nonce - ownable.privateState.injectSecretNonce(BAD_NONCE); - - // Check nonce does not match - expect(ownable.privateState.getCurrentSecretNonce()).not.toEqual( - secretNonce, - ); - - // Set unauthorized caller and call circuit - ownable.callerCtx.setCaller(UNAUTHORIZED); - expect(() => { - ownable.assertOnlyOwner(); - }).toThrow('Forbidden'); - }); - }); - describe('transferOwnership', () => { let newOwnerCommitment: Uint8Array; let newOwnerNonce: Uint8Array; @@ -248,13 +162,13 @@ describe('ZOwnablePK', () => { ownable.callerCtx.setCaller(OWNER); expect(() => { ownable.assertOnlyOwner(); - }).toThrow('Forbidden'); + }).toThrow('ZOwnablePK: caller is not the owner'); // Unauthorized ownable.callerCtx.setCaller(UNAUTHORIZED); expect(() => { ownable.assertOnlyOwner(); - }).toThrow('Forbidden'); + }).toThrow('ZOwnablePK: caller is not the owner'); // New owner ownable.callerCtx.setCaller(NEW_OWNER); @@ -267,14 +181,14 @@ describe('ZOwnablePK', () => { const badId = new Uint8Array(32).fill(0); expect(() => { ownable.transferOwnership(badId); - }).toThrow('Invalid parameters'); + }).toThrow('ZOwnablePK: invalid id'); }); it('should fail when unauthorized transfers ownership', () => { ownable.callerCtx.setCaller(UNAUTHORIZED); expect(() => { ownable.transferOwnership(newOwnerCommitment); - }).toThrow('Forbidden'); + }).toThrow('ZOwnablePK: caller is not the owner'); }); /** @@ -337,7 +251,7 @@ describe('ZOwnablePK', () => { // Check revoked permissions expect(() => { ownable.assertOnlyOwner(); - }).toThrow('Forbidden'); + }).toThrow('ZOwnablePK: caller is not the owner'); }); it('should fail when renouncing from unauthorized', () => { @@ -363,5 +277,133 @@ describe('ZOwnablePK', () => { }); }); }); + + describe('assertOnlyOwner', () => { + it('should allow authorized caller with correct nonce to call', () => { + // Check nonce is correct + expect(ownable.privateState.getCurrentSecretNonce()).toEqual( + secretNonce, + ); + + ownable.callerCtx.setCaller(OWNER); + expect(ownable.assertOnlyOwner()).to.not.throw; + }); + + it('should fail when the authorized caller has the wrong nonce', () => { + // Inject bad nonce + ownable.privateState.injectSecretNonce(BAD_NONCE); + + // Check nonce does not match + expect(ownable.privateState.getCurrentSecretNonce()).not.toEqual( + secretNonce, + ); + + // Set caller and call circuit + ownable.callerCtx.setCaller(OWNER); + expect(() => { + ownable.assertOnlyOwner(); + }).toThrow('ZOwnablePK: caller is not the owner'); + }); + + it('should fail when unauthorized caller has the correct nonce', () => { + // Check nonce is correct + expect(ownable.privateState.getCurrentSecretNonce()).toEqual( + secretNonce, + ); + + ownable.callerCtx.setCaller(UNAUTHORIZED); + expect(() => { + ownable.assertOnlyOwner(); + }).toThrow('ZOwnablePK: caller is not the owner'); + }); + + it('should fail when unauthorized caller has the wrong nonce', () => { + // Inject bad nonce + ownable.privateState.injectSecretNonce(BAD_NONCE); + + // Check nonce does not match + expect(ownable.privateState.getCurrentSecretNonce()).not.toEqual( + secretNonce, + ); + + // Set unauthorized caller and call circuit + ownable.callerCtx.setCaller(UNAUTHORIZED); + expect(() => { + ownable.assertOnlyOwner(); + }).toThrow('ZOwnablePK: caller is not the owner'); + }); + }); + + /** + * @TODO parameterize + */ + describe('computeOwnerCommitment', () => { + it('should match local and contract commitment algorithms', () => { + const id = createIdHash(Z_OWNER, secretNonce); + const counter = INIT_COUNTER; + + // Check buildCommitmentFromId + const hashFromContract = ownable.computeOwnerCommitment(id, counter); + const hashFromHelper1 = buildCommitmentFromId( + id, + INSTANCE_SALT, + counter, + ); + expect(hashFromContract).toEqual(hashFromHelper1); + + // Check buildCommitment + const hashFromHelper2 = buildCommitment( + Z_OWNER, + secretNonce, + INSTANCE_SALT, + counter, + DOMAIN, + ); + expect(hashFromContract).toEqual(hashFromHelper1); + expect(hashFromHelper1).toEqual(hashFromHelper2); + }); + }); + + describe('_transferOwnership', () => { + it('should transfer ownership', () => { + const id = createIdHash(Z_OWNER, secretNonce); + ownable._transferOwnership(id); + + const nextCounter = INIT_COUNTER + 1n; + const expCommitment = buildCommitmentFromId(id, INSTANCE_SALT, nextCounter); + expect(ownable.owner()).toEqual(expCommitment); + }); + + it('should bump the counter with each transfer', () => { + const nTransfers = 10; + const counterStart = 2; // count starts at 2 bc the constructor bumps the count to 1 + for (let i = counterStart; i <= nTransfers; i++) { + const pk = utils.encodeToPK(`Id${i}`); + const nonce = new Uint8Array(32).fill(i) + const id = createIdHash(pk, nonce); + ownable._transferOwnership(id); + + expect(ownable.getPublicState().ZOwnablePK__counter).toEqual(BigInt(i)) + } + }); + + it('should allow transfer to all zeroes id', () => { + const zerosId = new Uint8Array(32).fill(0); + ownable._transferOwnership(zerosId); + + const nextCounter = INIT_COUNTER + 1n; + const expCommitment = buildCommitmentFromId(zerosId, INSTANCE_SALT, nextCounter); + expect(ownable.owner()).toEqual(expCommitment); + }); + + it('should allow anyone to transfer', () => { + ownable.callerCtx.setCaller(OWNER); + const id = createIdHash(Z_OWNER, secretNonce); + expect(ownable._transferOwnership(id)).not.to.throw; + + ownable.callerCtx.setCaller(UNAUTHORIZED); + expect(ownable._transferOwnership(id)).not.to.throw; + }); + }); }); }); From a5289fd89c9476ac14972ee3ab89544ed2af4fad Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 12 Aug 2025 16:18:10 -0300 Subject: [PATCH 090/322] tidy up test --- contracts/ownable/src/test/ZOwnablePK.test.ts | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/contracts/ownable/src/test/ZOwnablePK.test.ts b/contracts/ownable/src/test/ZOwnablePK.test.ts index dbb63417..d8cd5a0b 100644 --- a/contracts/ownable/src/test/ZOwnablePK.test.ts +++ b/contracts/ownable/src/test/ZOwnablePK.test.ts @@ -10,28 +10,24 @@ import { ZOwnablePKPrivateState } from '../witnesses/ZOwnablePKWitnesses.js'; import { ZOwnablePKSimulator } from './simulators/ZOwnablePKSimulator.js'; import * as utils from './utils/address.js'; -const OWNER = String(Buffer.from('OWNER', 'ascii').toString('hex')).padStart( - 64, - '0', -); -const NEW_OWNER = String( - Buffer.from('NEW_OWNER', 'ascii').toString('hex'), -).padStart(64, '0'); -const UNAUTHORIZED = String( - Buffer.from('UNAUTHORIZED', 'ascii').toString('hex'), -).padStart(64, '0'); +// Callers +const OWNER = utils.toHexPadded('OWNER'); +const NEW_OWNER = utils.toHexPadded('NEW_OWNER'); +const UNAUTHORIZED = utils.toHexPadded('UNAUTHORIZED'); + +// ZPKs const Z_OWNER = utils.encodeToPK('OWNER'); const Z_NEW_OWNER = utils.encodeToPK('NEW_OWNER'); + const INSTANCE_SALT = new Uint8Array(32).fill(8675309); const BAD_NONCE = Buffer.from(Buffer.alloc(32, 'BAD_NONCE')); - const DOMAIN = 'ZOwnablePK:shield:'; const INIT_COUNTER = 1n; let secretNonce: Uint8Array; let ownable: ZOwnablePKSimulator; -/** Helpers */ +// Helpers const createIdHash = ( pk: ZswapCoinPublicKey, nonce: Uint8Array, From 4adaf86c118a54e8d4eca358096c41e52cc6ec50 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 12 Aug 2025 16:45:56 -0300 Subject: [PATCH 091/322] add initialize checks, add underscore to hash circuit --- contracts/ownable/src/ZOwnablePK.compact | 19 ++++++++++++++++--- contracts/ownable/src/test/ZOwnablePK.test.ts | 4 ++-- .../src/test/mocks/MockZOwnablePK.compact | 4 ++-- .../test/simulators/ZOwnablePKSimulator.ts | 4 ++-- 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/contracts/ownable/src/ZOwnablePK.compact b/contracts/ownable/src/ZOwnablePK.compact index 4f157372..67193082 100644 --- a/contracts/ownable/src/ZOwnablePK.compact +++ b/contracts/ownable/src/ZOwnablePK.compact @@ -8,6 +8,7 @@ pragma language_version >= 0.16.0; */ module ZOwnablePK { import CompactStandardLibrary; + import "../../node_modules/@openzeppelin-compact/utils/src/Initializable" prefix Initializable_; export ledger _ownerCommitment: Bytes<32>; export ledger _counter: Counter; @@ -19,6 +20,8 @@ module ZOwnablePK { * @description Add me!!! */ export circuit initialize(ownerId: Bytes<32>, instanceSalt: Bytes<32>): [] { + Initializable_initialize(); + assert(ownerId != default>, "ZOwnablePK: invalid id"); _instanceSalt = disclose(instanceSalt); _transferOwnership(ownerId); @@ -28,6 +31,7 @@ module ZOwnablePK { * @description Add me!!! */ export circuit owner(): Bytes<32> { + Initializable_assertInitialized(); return _ownerCommitment; } @@ -35,6 +39,8 @@ module ZOwnablePK { * @description Add me!!! */ export circuit transferOwnership(newOwnerId: Bytes<32>): [] { + Initializable_assertInitialized(); + assertOnlyOwner(); assert(newOwnerId != default>, "ZOwnablePK: invalid id"); _transferOwnership(newOwnerId); @@ -44,6 +50,8 @@ module ZOwnablePK { * @description Add me!!! */ export circuit renounceOwnership(): [] { + Initializable_assertInitialized(); + assertOnlyOwner(); _ownerCommitment.resetToDefault(); } @@ -52,19 +60,22 @@ module ZOwnablePK { * @description Add me!!! */ export circuit assertOnlyOwner(): [] { + Initializable_assertInitialized(); + const caller = ownPublicKey(); const nonce = offchainNonce(); const id = persistentHash>>([caller.bytes, nonce]); - assert(_ownerCommitment == computeOwnerCommitment(id, _counter), "ZOwnablePK: caller is not the owner"); + assert(_ownerCommitment == _computeOwnerCommitment(id, _counter), "ZOwnablePK: caller is not the owner"); } /** * @description Add me!!! */ - export circuit computeOwnerCommitment( + export circuit _computeOwnerCommitment( id: Bytes<32>, counter: Uint<64>, ): Bytes<32> { + Initializable_assertInitialized(); return persistentHash>>( [ id, @@ -79,7 +90,9 @@ module ZOwnablePK { * @description Add me!!! */ export circuit _transferOwnership(newOwnerId: Bytes<32>): [] { + Initializable_assertInitialized(); + _counter.increment(1); - _ownerCommitment = computeOwnerCommitment(disclose(newOwnerId), _counter); + _ownerCommitment = _computeOwnerCommitment(disclose(newOwnerId), _counter); } } diff --git a/contracts/ownable/src/test/ZOwnablePK.test.ts b/contracts/ownable/src/test/ZOwnablePK.test.ts index d8cd5a0b..c3d6ff15 100644 --- a/contracts/ownable/src/test/ZOwnablePK.test.ts +++ b/contracts/ownable/src/test/ZOwnablePK.test.ts @@ -333,13 +333,13 @@ describe('ZOwnablePK', () => { /** * @TODO parameterize */ - describe('computeOwnerCommitment', () => { + describe('_computeOwnerCommitment', () => { it('should match local and contract commitment algorithms', () => { const id = createIdHash(Z_OWNER, secretNonce); const counter = INIT_COUNTER; // Check buildCommitmentFromId - const hashFromContract = ownable.computeOwnerCommitment(id, counter); + const hashFromContract = ownable._computeOwnerCommitment(id, counter); const hashFromHelper1 = buildCommitmentFromId( id, INSTANCE_SALT, diff --git a/contracts/ownable/src/test/mocks/MockZOwnablePK.compact b/contracts/ownable/src/test/mocks/MockZOwnablePK.compact index b7c3548f..5d64d43e 100644 --- a/contracts/ownable/src/test/mocks/MockZOwnablePK.compact +++ b/contracts/ownable/src/test/mocks/MockZOwnablePK.compact @@ -28,8 +28,8 @@ export circuit assertOnlyOwner(): [] { return ZOwnablePK_assertOnlyOwner(); } -export circuit computeOwnerCommitment(id: Bytes<32>, counter: Uint<64>): Bytes<32> { - return ZOwnablePK_computeOwnerCommitment(id, counter); +export circuit _computeOwnerCommitment(id: Bytes<32>, counter: Uint<64>): Bytes<32> { + return ZOwnablePK__computeOwnerCommitment(id, counter); } export circuit _transferOwnership(newOwnerCommitment: Bytes<32>): [] { diff --git a/contracts/ownable/src/test/simulators/ZOwnablePKSimulator.ts b/contracts/ownable/src/test/simulators/ZOwnablePKSimulator.ts index dc6543d6..6a1337a1 100644 --- a/contracts/ownable/src/test/simulators/ZOwnablePKSimulator.ts +++ b/contracts/ownable/src/test/simulators/ZOwnablePKSimulator.ts @@ -233,8 +233,8 @@ export class ZOwnablePKSimulator extends AbstractContractSimulator< /** * @description */ - public computeOwnerCommitment(id: Uint8Array, counter: bigint): Uint8Array { - return this.circuits.impure.computeOwnerCommitment(id, counter); + public _computeOwnerCommitment(id: Uint8Array, counter: bigint): Uint8Array { + return this.circuits.impure._computeOwnerCommitment(id, counter); } /** From 954d63fa8e5d59257bf444049f6ffebeececd70a Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 12 Aug 2025 16:50:28 -0300 Subject: [PATCH 092/322] change PS/witness val name to secretNonce --- contracts/ownable/src/ZOwnablePK.compact | 4 ++-- contracts/ownable/src/test/ZOwnablePK.test.ts | 22 ++++++++++++++----- .../test/simulators/ZOwnablePKSimulator.ts | 4 ++-- .../src/witnesses/ZOwnablePKWitnesses.ts | 8 +++---- contracts/ownable/src/witnesses/interface.ts | 2 +- 5 files changed, 25 insertions(+), 15 deletions(-) diff --git a/contracts/ownable/src/ZOwnablePK.compact b/contracts/ownable/src/ZOwnablePK.compact index 67193082..237ef6f6 100644 --- a/contracts/ownable/src/ZOwnablePK.compact +++ b/contracts/ownable/src/ZOwnablePK.compact @@ -14,7 +14,7 @@ module ZOwnablePK { export ledger _counter: Counter; export sealed ledger _instanceSalt: Bytes<32>; - export witness offchainNonce(): Bytes<32>; + export witness secretNonce(): Bytes<32>; /** * @description Add me!!! @@ -63,7 +63,7 @@ module ZOwnablePK { Initializable_assertInitialized(); const caller = ownPublicKey(); - const nonce = offchainNonce(); + const nonce = secretNonce(); const id = persistentHash>>([caller.bytes, nonce]); assert(_ownerCommitment == _computeOwnerCommitment(id, _counter), "ZOwnablePK: caller is not the owner"); } diff --git a/contracts/ownable/src/test/ZOwnablePK.test.ts b/contracts/ownable/src/test/ZOwnablePK.test.ts index c3d6ff15..5759a853 100644 --- a/contracts/ownable/src/test/ZOwnablePK.test.ts +++ b/contracts/ownable/src/test/ZOwnablePK.test.ts @@ -107,7 +107,7 @@ describe('ZOwnablePK', () => { // Create private state object and generate nonce const PS = ZOwnablePKPrivateState.generate(); // Bind nonce for convenience - secretNonce = PS.offchainNonce; + secretNonce = PS.secretNonce; // Prepare owner ID with gen nonce const ownerId = createIdHash(Z_OWNER, secretNonce); // Deploy contract with derived owner commitment and PS @@ -137,7 +137,7 @@ describe('ZOwnablePK', () => { beforeEach(() => { // Prepare new owner commitment - newOwnerNonce = ZOwnablePKPrivateState.generate().offchainNonce; + newOwnerNonce = ZOwnablePKPrivateState.generate().secretNonce; newCounter = INIT_COUNTER + 1n; newIdHash = createIdHash(Z_NEW_OWNER, newOwnerNonce); newOwnerCommitment = buildCommitment( @@ -366,7 +366,11 @@ describe('ZOwnablePK', () => { ownable._transferOwnership(id); const nextCounter = INIT_COUNTER + 1n; - const expCommitment = buildCommitmentFromId(id, INSTANCE_SALT, nextCounter); + const expCommitment = buildCommitmentFromId( + id, + INSTANCE_SALT, + nextCounter, + ); expect(ownable.owner()).toEqual(expCommitment); }); @@ -375,11 +379,13 @@ describe('ZOwnablePK', () => { const counterStart = 2; // count starts at 2 bc the constructor bumps the count to 1 for (let i = counterStart; i <= nTransfers; i++) { const pk = utils.encodeToPK(`Id${i}`); - const nonce = new Uint8Array(32).fill(i) + const nonce = new Uint8Array(32).fill(i); const id = createIdHash(pk, nonce); ownable._transferOwnership(id); - expect(ownable.getPublicState().ZOwnablePK__counter).toEqual(BigInt(i)) + expect(ownable.getPublicState().ZOwnablePK__counter).toEqual( + BigInt(i), + ); } }); @@ -388,7 +394,11 @@ describe('ZOwnablePK', () => { ownable._transferOwnership(zerosId); const nextCounter = INIT_COUNTER + 1n; - const expCommitment = buildCommitmentFromId(zerosId, INSTANCE_SALT, nextCounter); + const expCommitment = buildCommitmentFromId( + zerosId, + INSTANCE_SALT, + nextCounter, + ); expect(ownable.owner()).toEqual(expCommitment); }); diff --git a/contracts/ownable/src/test/simulators/ZOwnablePKSimulator.ts b/contracts/ownable/src/test/simulators/ZOwnablePKSimulator.ts index 6a1337a1..c4e58c25 100644 --- a/contracts/ownable/src/test/simulators/ZOwnablePKSimulator.ts +++ b/contracts/ownable/src/test/simulators/ZOwnablePKSimulator.ts @@ -252,13 +252,13 @@ export class ZOwnablePKSimulator extends AbstractContractSimulator< newNonce: Buffer, ): ZOwnablePKPrivateState => { const currentState = this.stateManager.getContext().currentPrivateState; - const updatedState = { ...currentState, offchainNonce: newNonce }; + const updatedState = { ...currentState, secretNonce: newNonce }; this.stateManager.updatePrivateState(updatedState); return updatedState; }, getCurrentSecretNonce: (): Uint8Array => { - return this.stateManager.getContext().currentPrivateState.offchainNonce; + return this.stateManager.getContext().currentPrivateState.secretNonce; }, }; diff --git a/contracts/ownable/src/witnesses/ZOwnablePKWitnesses.ts b/contracts/ownable/src/witnesses/ZOwnablePKWitnesses.ts index f4682cc6..2a3d2028 100644 --- a/contracts/ownable/src/witnesses/ZOwnablePKWitnesses.ts +++ b/contracts/ownable/src/witnesses/ZOwnablePKWitnesses.ts @@ -8,7 +8,7 @@ import type { IZOwnablePKWitnesses } from './interface.js'; */ export type ZOwnablePKPrivateState = { /** @description A 32-byte secret nonce used as a privacy additive. */ - offchainNonce: Buffer; + secretNonce: Buffer; }; /** @@ -20,7 +20,7 @@ export const ZOwnablePKPrivateState = { * @returns A fresh ZOwnablePKPrivateState instance. */ generate: (): ZOwnablePKPrivateState => { - return { offchainNonce: getRandomValues(Buffer.alloc(32)) }; + return { secretNonce: getRandomValues(Buffer.alloc(32)) }; }, }; @@ -30,9 +30,9 @@ export const ZOwnablePKPrivateState = { */ export const ZOwnablePKWitnesses = (): IZOwnablePKWitnesses => ({ - offchainNonce( + secretNonce( context: WitnessContext, ): [ZOwnablePKPrivateState, Uint8Array] { - return [context.privateState, context.privateState.offchainNonce]; + return [context.privateState, context.privateState.secretNonce]; }, }); diff --git a/contracts/ownable/src/witnesses/interface.ts b/contracts/ownable/src/witnesses/interface.ts index 7799c8ef..1898410c 100644 --- a/contracts/ownable/src/witnesses/interface.ts +++ b/contracts/ownable/src/witnesses/interface.ts @@ -11,5 +11,5 @@ export interface IZOwnablePKWitnesses

{ * @param context - The witness context containing the private state. * @returns A tuple of the private state and the secret nonce as a Uint8Array. */ - offchainNonce(context: WitnessContext): [P, Uint8Array]; + secretNonce(context: WitnessContext): [P, Uint8Array]; } From e1ee7e45e73e0bf038816fa3c4c4bcda6ae96bb5 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 12 Aug 2025 16:51:33 -0300 Subject: [PATCH 093/322] improve test description --- contracts/ownable/src/test/ZOwnablePK.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/ownable/src/test/ZOwnablePK.test.ts b/contracts/ownable/src/test/ZOwnablePK.test.ts index 5759a853..e628eceb 100644 --- a/contracts/ownable/src/test/ZOwnablePK.test.ts +++ b/contracts/ownable/src/test/ZOwnablePK.test.ts @@ -334,7 +334,7 @@ describe('ZOwnablePK', () => { * @TODO parameterize */ describe('_computeOwnerCommitment', () => { - it('should match local and contract commitment algorithms', () => { + it('should match local and contract commitment', () => { const id = createIdHash(Z_OWNER, secretNonce); const counter = INIT_COUNTER; From 6bbe434964742e6e65ccd3c3c00f7d590cc4eda2 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 12 Aug 2025 16:52:26 -0300 Subject: [PATCH 094/322] remove unnecessary line --- contracts/ownable/src/test/ZOwnablePK.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/ownable/src/test/ZOwnablePK.test.ts b/contracts/ownable/src/test/ZOwnablePK.test.ts index e628eceb..07daf67e 100644 --- a/contracts/ownable/src/test/ZOwnablePK.test.ts +++ b/contracts/ownable/src/test/ZOwnablePK.test.ts @@ -355,7 +355,6 @@ describe('ZOwnablePK', () => { counter, DOMAIN, ); - expect(hashFromContract).toEqual(hashFromHelper1); expect(hashFromHelper1).toEqual(hashFromHelper2); }); }); From 54116e29d3b33bd953b9f359047458c628683b1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Tue, 12 Aug 2025 19:52:00 -0400 Subject: [PATCH 095/322] Add contractAddress to nonce generation scheme --- .../ShieldedAccessControlWitnesses.ts | 44 ++++++++++++++----- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/contracts/shieldedAccessControl/src/witnesses/ShieldedAccessControlWitnesses.ts b/contracts/shieldedAccessControl/src/witnesses/ShieldedAccessControlWitnesses.ts index 88e1c589..08260719 100644 --- a/contracts/shieldedAccessControl/src/witnesses/ShieldedAccessControlWitnesses.ts +++ b/contracts/shieldedAccessControl/src/witnesses/ShieldedAccessControlWitnesses.ts @@ -7,7 +7,6 @@ import { type WitnessContext, } from '@midnight-ntwrk/compact-runtime'; import { encodeContractAddress } from '@midnight-ntwrk/ledger'; -import { sampleContractAddress } from '@midnight-ntwrk/zswap'; import { type ContractAddress, type Either, @@ -21,13 +20,14 @@ const { hkdfSync } = await import('node:crypto'); const KEYLEN = 32; /** - * @description The respective `nonce` value for a given `roleId` should be at the same index + * @description The respective nonce and contract value for a given `roleId` should be at the same index * for each array of `Buffer`s */ export type ShieldedAccessControlPrivateState = { secretKey: Buffer; nonces: Buffer[]; roleIds: Buffer[]; + contractAddress: Buffer; }; /** @@ -36,6 +36,7 @@ export type ShieldedAccessControlPrivateState = { * @param roleId - The role identifier. * @param salt - A salt value. * @param account - The public key of an account. + * @param contractAddress - The contract address of the contract being called. * * @returns A unique nonce value for `roleId` */ @@ -44,9 +45,10 @@ function generateNonce( roleId: Buffer, salt: Buffer, account: Buffer, + contractAddress: Buffer, ): Buffer { const domainString = Buffer.from('role-nonce'); - const info = Buffer.concat([domainString, roleId, account]); + const info = Buffer.concat([domainString, roleId, account, contractAddress]); const nonce = hkdfSync('sha512', secretKey, salt, info, KEYLEN); return Buffer.from(nonce); @@ -57,6 +59,7 @@ function generateNonce( * @param account - The public key of an account. * @param roleId - The role identifier. * @param nonce - The nonce associated with `roleId`. + * @param contractAddress - The address of the contract being called. * * @returns Whether the account was approved for a role */ @@ -64,6 +67,7 @@ function sendRoleRequestToAdmin( _account: Buffer, _roleId: Buffer, _nonce: Buffer, + _contractAddress: Buffer, ) { return true; } @@ -120,6 +124,7 @@ export const ShieldedAccessControlWitnesses = { secretKey: privateState.secretKey, roleIds: [], nonces: [], + contractAddress: privateState.contractAddress, }; const contract = @@ -132,7 +137,12 @@ export const ShieldedAccessControlWitnesses = { currentZswapLocalState, } = contract.initialState( constructorContext( - { secretKey: privateState.secretKey, nonces: [], roleIds: [] }, + { + secretKey: privateState.secretKey, + nonces: [], + roleIds: [], + contractAddress: privateState.contractAddress, + }, coinPubKey, ), ); @@ -147,17 +157,18 @@ export const ShieldedAccessControlWitnesses = { }; for (let i = 0; i < roles.length; i++) { - const role = roles[i]; + const role = Buffer.from(roles[i]); const nonce = generateNonce( privateState.secretKey, - Buffer.from(role), + role, Buffer.from(salt), Buffer.from(account), + privateState.contractAddress, ); const eitherAccount: Either = { is_left: true, left: { bytes: account }, - right: { bytes: encodeContractAddress(sampleContractAddress()) }, + right: { bytes: new Uint8Array(32) }, }; try { @@ -169,7 +180,7 @@ export const ShieldedAccessControlWitnesses = { ); if (hasRole) { newPrivateState.nonces.push(nonce); - newPrivateState.roleIds.push(Buffer.from(role)); + newPrivateState.roleIds.push(role); } } catch (err) { console.log(err); @@ -184,11 +195,15 @@ export const ShieldedAccessControlWitnesses = { * @param roleId - The role identifier. * @param account - The public key requesting a role. * @param salt - A salt value. + * @param contractAddress - The address of the contract being called. * * @returns An array of the new private state and an empty array */ requestRole: ( - { privateState }: WitnessContext, + { + privateState, + contractAddress, + }: WitnessContext, roleId: Uint8Array, account: Uint8Array, salt: Uint8Array, @@ -196,13 +211,22 @@ export const ShieldedAccessControlWitnesses = { const saltBuff = Buffer.from(salt); const roleIdBuff = Buffer.from(roleId); const accountBuff = Buffer.from(account); + const contractAddressBuff = Buffer.from( + encodeContractAddress(contractAddress), + ); const nonce = generateNonce( privateState.secretKey, roleIdBuff, saltBuff, accountBuff, + contractAddressBuff, + ); + const isApproved = sendRoleRequestToAdmin( + accountBuff, + roleIdBuff, + nonce, + contractAddressBuff, ); - const isApproved = sendRoleRequestToAdmin(accountBuff, roleIdBuff, nonce); if (isApproved) { privateState.nonces.push(nonce); From 44019818cdf260b0b3fffecaad744c1b0583c707 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 12 Aug 2025 23:11:40 -0300 Subject: [PATCH 096/322] add in-code docs, add _computeOwnerId --- contracts/ownable/src/ZOwnablePK.compact | 186 ++++++++++++++++++++++- 1 file changed, 178 insertions(+), 8 deletions(-) diff --git a/contracts/ownable/src/ZOwnablePK.compact b/contracts/ownable/src/ZOwnablePK.compact index 237ef6f6..7470abee 100644 --- a/contracts/ownable/src/ZOwnablePK.compact +++ b/contracts/ownable/src/ZOwnablePK.compact @@ -4,7 +4,42 @@ pragma language_version >= 0.16.0; /** * @module ZOwnablePK - * @description A shielded Ownable library. + * @description A shielded, public key-derived Ownable module. + * + * `ZOwnablePK` provides a privacy-preserving access control mechanism + * for contracts with a single administrative user. Unlike traditional + * `Ownable` implementations that store or expose the owner's public key + * on-chain, this module stores only a commitment to a hashed identifier + * derived from the owner's public key and a secret nonce. + * + * @notice This module explicitly supports commitments derived from public keys; + * however, it may be possible to use contract addresses when contract-to-contract + * calls become available. This will be revisited when it is know if/how witnesses + * are used from a contract address context. + * + * @dev Features: + * - Obfuscated owner identity: The owner's public key is never revealed on-chain. + * - Stateless verification: The contract never needs access to the full public key. + * - Built-in support for transfer and renounce functionality. + * - Instance-specific salts to prevent cross-contract correlation. + * - Deterministic hashing with `persistentHash` to support zero-knowledge verification. + * + * @dev Commitment structure: + * ``` + * id = H(pk, secretNonce) + * commitment = H(id, instanceSalt, counter, "ZOwnablePK:shield:") + * ``` + * The commitment changes on each transfer due to the incrementing `counter`, + * providing unlinkability across ownership changes. + * + * @dev Security Considerations: + * - The `secretNonce` must be kept private. Loss of the nonce prevents the + * owner from proving ownership or transferring it. + * - Ownership validation is entirely circuit-based using witness-provided values. + * - The `_instanceSalt` is immutable and used to differentiate deployments. + * + * @notice Best used for single-admin contracts with privacy requirements. + * It is not designed for multi-owner or role-based access control. */ module ZOwnablePK { import CompactStandardLibrary; @@ -17,7 +52,23 @@ module ZOwnablePK { export witness secretNonce(): Bytes<32>; /** - * @description Add me!!! + * @description Initializes the contract by setting the initial owner via `ownerId` + * and storing the `instanceSalt` that acts as a privacy additive for preventing + * duplicate commitments among other contracts implementing ZOwnablePK. + * + * @dev The `ownerId` must be calculated prior to contract deployment. + * + * @circuitInfo k=???, rows=??? + * + * Requirements: + * + * - Contract is not initialized. + * - `ownerId` is not all zeroes. + * + * @param {Bytes<32>} ownerId - The owner's unique identifier H(pk, nonce). + * @param {Bytes<32>} instanceSalt - Contract salt to prevent duplicate commitments if + * users reuse their PK and secretNonce witness (not recommended). + * @returns {[]} Empty tuple. */ export circuit initialize(ownerId: Bytes<32>, instanceSalt: Bytes<32>): [] { Initializable_initialize(); @@ -28,7 +79,16 @@ module ZOwnablePK { } /** - * @description Add me!!! + * @description Returns the current commitment representing the contract owner. + * The full commitment is: `H(H(pk, nonce), instanceSalt, counter, domain)`. + * + * @circuitInfo k=???, rows=??? + * + * Requirements: + * + * - Contract is initialized. + * + * @returns {Bytes<32>} The current owner's commitment. */ export circuit owner(): Bytes<32> { Initializable_assertInitialized(); @@ -36,7 +96,19 @@ module ZOwnablePK { } /** - * @description Add me!!! + * @description Transfers ownership to `newOwnerId`. + * `newOwnerId` must be precalculated and given to the current owner off chain. + * + * @circuitInfo k=???, rows=??? + * + * Requirements: + * + * - Contract is initialized. + * - Caller must be the current owner. + * - `newOwnerId` must not be all zeroes. + * + * @param {Bytes<32>} newOwnerId - The new owner's unique identifier (`H(pk, nonce)`). + * @returns {[]} Empty tuple. */ export circuit transferOwnership(newOwnerId: Bytes<32>): [] { Initializable_assertInitialized(); @@ -47,7 +119,18 @@ module ZOwnablePK { } /** - * @description Add me!!! + * @description Leaves the contract without an owner. + * It will not be possible to call `assertOnlyOnwer` circuits anymore. + * Can only be called by the current owner. + * + * @circuitInfo k=???, rows=??? + * + * Requirements: + * + * - Contract is initialized. + * - Caller must be the current owner. + * + * @returns {[]} Empty tuple. */ export circuit renounceOwnership(): [] { Initializable_assertInitialized(); @@ -57,7 +140,19 @@ module ZOwnablePK { } /** - * @description Add me!!! + * @description Throws if called by any account whose id hash `H(pk, nonce)` does not match + * the stored owner commitment. + * Use this to restrict access of specific circuits to the owner. + * + * @circuitInfo k=???, rows=??? + * + * Requirements: + * + * - Contract is initialized. + * - Caller's id (`H(pk, nonce)`) when used in `_computeOwnerCommitment` must equal + * the stored `_ownerCommitment`, thus verifying themselves as the owner. + * + * @returns {[]} Empty tuple. */ export circuit assertOnlyOwner(): [] { Initializable_assertInitialized(); @@ -69,7 +164,33 @@ module ZOwnablePK { } /** - * @description Add me!!! + * @description Computes the owner commitment from the given `id` and `counter`. + * + * ## Owner ID (`id`) + * The `id` is expected to be computed off-chain as: + * `id = H(pk, nonce)` + * + * - `pk`: The owner's public key. + * - `nonce`: A secret nonce scoped to the instance, ideally rotated with each transfer. + * + * ## Commitment Derivation + * `commitment = H(id, instanceSalt, counter, domain)` + * + * - `id`: See above. + * - `instanceSalt`: A unique per-deployment salt, stored during initialization. + * This prevents commitment collisions across deployments. + * - `counter`: Incremented with each ownership transfer, ensuring uniqueness + * even with repeated `id` values. + * - `domain`: A domain separator to prevent hash collisions when extending the module. + * + * Requirements: + * + * - Contract is initialized. + * + * @param {Bytes<32>} id - The unique identifier of the owner calculated by `H(pk, nonce)`. + * @param {Uint<64>} counter - The current counter or round. This increments by `1` + * after every transfer to prevent duplicate commitments given the same `id`. + * @returns {Bytes<32>} The commitment derived from `id` and `counter`. */ export circuit _computeOwnerCommitment( id: Bytes<32>, @@ -87,7 +208,56 @@ module ZOwnablePK { } /** - * @description Add me!!! + * @description Computes the unique identifier (`id`) of the owner from their + * public key and a secret nonce. + * + * ## ID Derivation + * `id = H(pk, nonce)` + * + * - `pk`: The public key of the caller. This is passed explicitly to allow + * for off-chain derivation, testing, or scenarios where the caller is + * different from the subject of the computation. + * - `nonce`: A secret nonce tied to the identity. This value should be + * randomly generated and kept private. It may be rotated periodically + * for enhanced unlinkability. + * + * The result is a 32-byte commitment that uniquely identifies the owner. + * This value is later used in owner commitment hashing, and acts as a privacy-preserving + * alternative to a raw public key. + * + * @notice This module allows ownership to be tied to an identity commitment derived + * from a public key and secret nonce. + * While typically used with user public keys, this mechanism may also + * support contract addresses as identifiers in future contract-to-contract + * interactions. Both are treated as 32-byte values (`Bytes<32>`). + * + * @circuitInfo k=???, rows=??? + * + * @param {Bytes<32>} pk - The public key of the identity being committed. + * @param {Bytes<32>} nonce - A private nonce to scope the commitment. + * @returns {Bytes<32>} The computed owner ID. + */ + export pure circuit _computeOwnerId(pk: Either, nonce: Bytes<32>): Bytes<32> { + if (!pk.is_left) { + assert(false, "ZOwnablePK: contract address owners are not yet supported"); + } + + return persistentHash>>([pk.left.bytes, nonce]); + } + + /** + * @description Transfers ownership to owner id `newOwnerId` without + * enforcing permission checks on the caller. + * + * @circuitInfo k=???, rows=??? + * + * Requirements: + * + * - Contract is initialized. + * + * @param {Bytes<32>} newOwnerId - The unique identifier of the new owner + * calculated by `H(pk, nonce)`. + * @returns {[]} Empty tuple. */ export circuit _transferOwnership(newOwnerId: Bytes<32>): [] { Initializable_assertInitialized(); From 8c220922384d44758694f1e8af638c7df7a9f02b Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 12 Aug 2025 23:12:03 -0300 Subject: [PATCH 097/322] add _computeOwnerId and test --- contracts/ownable/src/test/ZOwnablePK.test.ts | 17 +++++++++++++++++ .../src/test/mocks/MockZOwnablePK.compact | 4 ++++ .../src/test/simulators/ZOwnablePKSimulator.ts | 10 ++++++++++ 3 files changed, 31 insertions(+) diff --git a/contracts/ownable/src/test/ZOwnablePK.test.ts b/contracts/ownable/src/test/ZOwnablePK.test.ts index 07daf67e..684f43a5 100644 --- a/contracts/ownable/src/test/ZOwnablePK.test.ts +++ b/contracts/ownable/src/test/ZOwnablePK.test.ts @@ -359,6 +359,23 @@ describe('ZOwnablePK', () => { }); }); + describe('_computeOwnerId', () => { + it('should match local and contract owner id', () => { + const eitherOwner = utils.createEitherTestUser("OWNER"); + const ownerId = ownable._computeOwnerId(eitherOwner, secretNonce); + const expId = createIdHash(Z_OWNER, secretNonce); + + expect(ownerId).toEqual(expId); + }); + + it('should fail to compute ContractAddress id', () => { + const eitherContract = utils.createEitherTestContractAddress("CONTRACT"); + expect(() => { + ownable._computeOwnerId(eitherContract, secretNonce); + }).toThrow('ZOwnablePK: contract address owners are not yet supported') + }) + }); + describe('_transferOwnership', () => { it('should transfer ownership', () => { const id = createIdHash(Z_OWNER, secretNonce); diff --git a/contracts/ownable/src/test/mocks/MockZOwnablePK.compact b/contracts/ownable/src/test/mocks/MockZOwnablePK.compact index 5d64d43e..45858b8a 100644 --- a/contracts/ownable/src/test/mocks/MockZOwnablePK.compact +++ b/contracts/ownable/src/test/mocks/MockZOwnablePK.compact @@ -32,6 +32,10 @@ export circuit _computeOwnerCommitment(id: Bytes<32>, counter: Uint<64>): Bytes< return ZOwnablePK__computeOwnerCommitment(id, counter); } +export pure circuit _computeOwnerId(pk: Either, nonce: Bytes<32>): Bytes<32> { + return ZOwnablePK__computeOwnerId(pk, nonce); +} + export circuit _transferOwnership(newOwnerCommitment: Bytes<32>): [] { return ZOwnablePK__transferOwnership(newOwnerCommitment); } diff --git a/contracts/ownable/src/test/simulators/ZOwnablePKSimulator.ts b/contracts/ownable/src/test/simulators/ZOwnablePKSimulator.ts index c4e58c25..710b53fb 100644 --- a/contracts/ownable/src/test/simulators/ZOwnablePKSimulator.ts +++ b/contracts/ownable/src/test/simulators/ZOwnablePKSimulator.ts @@ -8,6 +8,9 @@ import { type Ledger, ledger, Contract as MockOwnable, + Either, + ZswapCoinPublicKey, + ContractAddress } from '../../artifacts/MockZOwnablePK/contract/index.cjs'; import { ZOwnablePKPrivateState, @@ -237,6 +240,13 @@ export class ZOwnablePKSimulator extends AbstractContractSimulator< return this.circuits.impure._computeOwnerCommitment(id, counter); } + /** + * @description + */ + public _computeOwnerId(pk: Either, nonce: Uint8Array): Uint8Array { + return this.circuits.pure._computeOwnerId(pk, nonce); + } + /** * @description Internal circuit that transfers ownership of the contract to `newOwner`. */ From a3ff1ad17a0a5cbe38856221040ee381784550e5 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 12 Aug 2025 23:13:26 -0300 Subject: [PATCH 098/322] add reqs to _computeOwnerId --- contracts/ownable/src/ZOwnablePK.compact | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/contracts/ownable/src/ZOwnablePK.compact b/contracts/ownable/src/ZOwnablePK.compact index 7470abee..91aa3771 100644 --- a/contracts/ownable/src/ZOwnablePK.compact +++ b/contracts/ownable/src/ZOwnablePK.compact @@ -233,6 +233,10 @@ module ZOwnablePK { * * @circuitInfo k=???, rows=??? * + * Requirements: + * + * - `pk` is not a ContractAddress. + * * @param {Bytes<32>} pk - The public key of the identity being committed. * @param {Bytes<32>} nonce - A private nonce to scope the commitment. * @returns {Bytes<32>} The computed owner ID. From 6325d0bc2318ab321443ab565b531f9d38a2d918 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 12 Aug 2025 23:43:43 -0300 Subject: [PATCH 099/322] add generatePubKeyPair util --- contracts/ownable/src/test/Ownable.test.ts | 14 ++++++-------- contracts/ownable/src/test/ZOwnablePK.test.ts | 12 ++++-------- contracts/ownable/src/test/utils/address.ts | 16 ++++++++++++++++ 3 files changed, 26 insertions(+), 16 deletions(-) diff --git a/contracts/ownable/src/test/Ownable.test.ts b/contracts/ownable/src/test/Ownable.test.ts index bde39230..948201f4 100644 --- a/contracts/ownable/src/test/Ownable.test.ts +++ b/contracts/ownable/src/test/Ownable.test.ts @@ -3,14 +3,12 @@ import { beforeEach, describe, expect, it } from 'vitest'; import { OwnableSimulator } from './simulators/OwnableSimulator.js'; import * as utils from './utils/address.js'; -// Callers -const OWNER = utils.toHexPadded('OWNER'); -const NEW_OWNER = utils.toHexPadded('NEW_OWNER'); -const UNAUTHORIZED = utils.toHexPadded('UNAUTHORIZED'); - -// Encoded PK/Addresses -const Z_OWNER = utils.createEitherTestUser('OWNER'); -const Z_NEW_OWNER = utils.createEitherTestUser('NEW_OWNER'); +// PKs +const [OWNER, Z_OWNER] = utils.generateEitherPubKeyPair("OWNER"); +const [NEW_OWNER, Z_NEW_OWNER] = utils.generateEitherPubKeyPair("NEW_OWNER"); +const [UNAUTHORIZED, _] = utils.generateEitherPubKeyPair('UNAUTHORIZED'); + +// Encoded contract addresses const Z_OWNER_CONTRACT = utils.createEitherTestContractAddress('OWNER_CONTRACT'); const Z_RECIPIENT_CONTRACT = diff --git a/contracts/ownable/src/test/ZOwnablePK.test.ts b/contracts/ownable/src/test/ZOwnablePK.test.ts index 684f43a5..998735fc 100644 --- a/contracts/ownable/src/test/ZOwnablePK.test.ts +++ b/contracts/ownable/src/test/ZOwnablePK.test.ts @@ -10,14 +10,10 @@ import { ZOwnablePKPrivateState } from '../witnesses/ZOwnablePKWitnesses.js'; import { ZOwnablePKSimulator } from './simulators/ZOwnablePKSimulator.js'; import * as utils from './utils/address.js'; -// Callers -const OWNER = utils.toHexPadded('OWNER'); -const NEW_OWNER = utils.toHexPadded('NEW_OWNER'); -const UNAUTHORIZED = utils.toHexPadded('UNAUTHORIZED'); - -// ZPKs -const Z_OWNER = utils.encodeToPK('OWNER'); -const Z_NEW_OWNER = utils.encodeToPK('NEW_OWNER'); +// PKs +const [OWNER, Z_OWNER] = utils.generatePubKeyPair("OWNER"); +const [NEW_OWNER, Z_NEW_OWNER] = utils.generatePubKeyPair("NEW_OWNER"); +const [UNAUTHORIZED, _] = utils.generatePubKeyPair('UNAUTHORIZED'); const INSTANCE_SALT = new Uint8Array(32).fill(8675309); const BAD_NONCE = Buffer.from(Buffer.alloc(32, 'BAD_NONCE')); diff --git a/contracts/ownable/src/test/utils/address.ts b/contracts/ownable/src/test/utils/address.ts index 82e0fe3e..086b323c 100644 --- a/contracts/ownable/src/test/utils/address.ts +++ b/contracts/ownable/src/test/utils/address.ts @@ -61,6 +61,22 @@ export const createEitherTestContractAddress = (str: string) => ({ right: encodeToAddress(str), }); +const baseGeneratePubKeyPair = ( + str: string, + asEither: boolean, +): [string, Compact.ZswapCoinPublicKey | Compact.Either] => { + const pk = toHexPadded(str); + const zpk = asEither ? createEitherTestUser(str) : encodeToPK(str); + return [pk, zpk]; +}; + +export const generatePubKeyPair = (str: string) => + baseGeneratePubKeyPair(str, false) as [string, Compact.ZswapCoinPublicKey]; + +export const generateEitherPubKeyPair = (str: string) => + baseGeneratePubKeyPair(str, true) as [string, Compact.Either]; + + export const zeroUint8Array = (length = 32) => convert_bigint_to_Uint8Array(length, 0n); From a1507ef24aacd400c350bfa2f8f201e79259e504 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 12 Aug 2025 23:46:47 -0300 Subject: [PATCH 100/322] remove line --- contracts/ownable/src/test/utils/address.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/ownable/src/test/utils/address.ts b/contracts/ownable/src/test/utils/address.ts index 086b323c..de4dcf86 100644 --- a/contracts/ownable/src/test/utils/address.ts +++ b/contracts/ownable/src/test/utils/address.ts @@ -76,7 +76,6 @@ export const generatePubKeyPair = (str: string) => export const generateEitherPubKeyPair = (str: string) => baseGeneratePubKeyPair(str, true) as [string, Compact.Either]; - export const zeroUint8Array = (length = 32) => convert_bigint_to_Uint8Array(length, 0n); From 771bfc0e7b0ff2031146a14caf3cd6520e86995b Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 13 Aug 2025 00:18:47 -0300 Subject: [PATCH 101/322] add init option to mock, test when not initialized --- contracts/ownable/src/test/ZOwnablePK.test.ts | 38 +++++++++++++++++-- .../src/test/mocks/MockZOwnablePK.compact | 13 ++++++- .../test/simulators/ZOwnablePKSimulator.ts | 3 +- 3 files changed, 48 insertions(+), 6 deletions(-) diff --git a/contracts/ownable/src/test/ZOwnablePK.test.ts b/contracts/ownable/src/test/ZOwnablePK.test.ts index 998735fc..feaaab4d 100644 --- a/contracts/ownable/src/test/ZOwnablePK.test.ts +++ b/contracts/ownable/src/test/ZOwnablePK.test.ts @@ -20,6 +20,7 @@ const BAD_NONCE = Buffer.from(Buffer.alloc(32, 'BAD_NONCE')); const DOMAIN = 'ZOwnablePK:shield:'; const INIT_COUNTER = 1n; +let isInit = true; let secretNonce: Uint8Array; let ownable: ZOwnablePKSimulator; @@ -79,7 +80,7 @@ describe('ZOwnablePK', () => { it('should fail when setting owner commitment as 0', () => { expect(() => { const badId = new Uint8Array(32).fill(0); - new ZOwnablePKSimulator(badId, INSTANCE_SALT); + new ZOwnablePKSimulator(badId, INSTANCE_SALT, isInit); }).toThrow('ZOwnablePK: invalid id'); }); @@ -87,7 +88,7 @@ describe('ZOwnablePK', () => { const notZeroPK = utils.encodeToPK('NOT_ZERO'); const notZeroNonce = new Uint8Array(32).fill(1); const nonZeroId = createIdHash(notZeroPK, notZeroNonce); - ownable = new ZOwnablePKSimulator(nonZeroId, INSTANCE_SALT); + ownable = new ZOwnablePKSimulator(nonZeroId, INSTANCE_SALT, isInit); const nonZeroCommitment = buildCommitmentFromId( nonZeroId, @@ -98,6 +99,37 @@ describe('ZOwnablePK', () => { }); }); + describe('when not initialized correctly', () => { + beforeEach(() => { + ownable = new ZOwnablePKSimulator(randomByteArray, INSTANCE_SALT, false); + }); + type FailingCircuits = [method: keyof ZOwnablePKSimulator, args: unknown[]]; + const randomByteArray = new Uint8Array(32).fill(123); + const randomCounter = 321n; + // Circuit calls should fail before the args are used + const circuitsToFail: FailingCircuits[] = [ + ['owner', []], + ['assertOnlyOwner', []], + ['transferOwnership', [randomByteArray]], + ['renounceOwnership', []], + ['_computeOwnerCommitment', [randomByteArray, randomCounter]], + ['_transferOwnership', [randomByteArray]], + ]; + it.each(circuitsToFail)('%s should fail', (circuitName, args) => { + expect(() => { + (ownable[circuitName] as (...args: unknown[]) => unknown)(...args); + }).toThrow('Initializable: contract not initialized'); + }); + + it('should allow pure computeOwnerId', () => { + const eitherOwner = utils.createEitherTestUser("OWNER"); + + expect(() => { + ownable._computeOwnerId(eitherOwner, randomByteArray); + }).not.toThrow(); + }); + }); + describe('after initialization', () => { beforeEach(() => { // Create private state object and generate nonce @@ -107,7 +139,7 @@ describe('ZOwnablePK', () => { // Prepare owner ID with gen nonce const ownerId = createIdHash(Z_OWNER, secretNonce); // Deploy contract with derived owner commitment and PS - ownable = new ZOwnablePKSimulator(ownerId, INSTANCE_SALT, { + ownable = new ZOwnablePKSimulator(ownerId, INSTANCE_SALT, isInit, { privateState: PS, }); }); diff --git a/contracts/ownable/src/test/mocks/MockZOwnablePK.compact b/contracts/ownable/src/test/mocks/MockZOwnablePK.compact index 45858b8a..ce9f7e0c 100644 --- a/contracts/ownable/src/test/mocks/MockZOwnablePK.compact +++ b/contracts/ownable/src/test/mocks/MockZOwnablePK.compact @@ -8,8 +8,17 @@ import "../../ZOwnablePK" prefix ZOwnablePK_; export { ZswapCoinPublicKey, ContractAddress, Either, Maybe }; export { ZOwnablePK__ownerCommitment, ZOwnablePK__counter }; -constructor(initOwnerCommitment: Bytes<32>, instanceSalt: Bytes<32>) { - ZOwnablePK_initialize(initOwnerCommitment, instanceSalt); +/** + * @description `isInit` is a param for testing. + * + * If `isInit` is false, the constructor will not initialize the contract. + * This behavior is to test that circuits are not callable unless the + * contract is initialized. +*/ +constructor(initOwnerCommitment: Bytes<32>, instanceSalt: Bytes<32>, isInit: Boolean) { + if (disclose(isInit)) { + ZOwnablePK_initialize(initOwnerCommitment, instanceSalt); + } } export circuit owner(): Bytes<32> { diff --git a/contracts/ownable/src/test/simulators/ZOwnablePKSimulator.ts b/contracts/ownable/src/test/simulators/ZOwnablePKSimulator.ts index 710b53fb..bd359375 100644 --- a/contracts/ownable/src/test/simulators/ZOwnablePKSimulator.ts +++ b/contracts/ownable/src/test/simulators/ZOwnablePKSimulator.ts @@ -58,6 +58,7 @@ export class ZOwnablePKSimulator extends AbstractContractSimulator< constructor( initOwner: Uint8Array, instanceSalt: Uint8Array, + isInit: boolean, options: OwnableSimOptions = {}, ) { super(); @@ -69,7 +70,7 @@ export class ZOwnablePKSimulator extends AbstractContractSimulator< coinPK = '0'.repeat(64), address = sampleContractAddress(), } = options; - const constructorArgs = [initOwner, instanceSalt]; + const constructorArgs = [initOwner, instanceSalt, isInit]; this.contract = new MockOwnable(witnesses); From e9c1de5146f8242eb229f1fa2fca7826d369bbc9 Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 13 Aug 2025 00:24:40 -0300 Subject: [PATCH 102/322] tidy up code --- contracts/ownable/src/test/ZOwnablePK.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/ownable/src/test/ZOwnablePK.test.ts b/contracts/ownable/src/test/ZOwnablePK.test.ts index feaaab4d..c6b7b634 100644 --- a/contracts/ownable/src/test/ZOwnablePK.test.ts +++ b/contracts/ownable/src/test/ZOwnablePK.test.ts @@ -434,12 +434,12 @@ describe('ZOwnablePK', () => { }); it('should allow transfer to all zeroes id', () => { - const zerosId = new Uint8Array(32).fill(0); - ownable._transferOwnership(zerosId); + const zeroId = utils.zeroUint8Array(); + ownable._transferOwnership(zeroId); const nextCounter = INIT_COUNTER + 1n; const expCommitment = buildCommitmentFromId( - zerosId, + zeroId, INSTANCE_SALT, nextCounter, ); From 323b66fc524a5327915028f2b4bab17790e13322 Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 13 Aug 2025 01:54:48 -0300 Subject: [PATCH 103/322] parameterize tests --- contracts/ownable/src/test/ZOwnablePK.test.ts | 119 ++++++++++++------ 1 file changed, 79 insertions(+), 40 deletions(-) diff --git a/contracts/ownable/src/test/ZOwnablePK.test.ts b/contracts/ownable/src/test/ZOwnablePK.test.ts index c6b7b634..cde94414 100644 --- a/contracts/ownable/src/test/ZOwnablePK.test.ts +++ b/contracts/ownable/src/test/ZOwnablePK.test.ts @@ -11,8 +11,8 @@ import { ZOwnablePKSimulator } from './simulators/ZOwnablePKSimulator.js'; import * as utils from './utils/address.js'; // PKs -const [OWNER, Z_OWNER] = utils.generatePubKeyPair("OWNER"); -const [NEW_OWNER, Z_NEW_OWNER] = utils.generatePubKeyPair("NEW_OWNER"); +const [OWNER, Z_OWNER] = utils.generatePubKeyPair('OWNER'); +const [NEW_OWNER, Z_NEW_OWNER] = utils.generatePubKeyPair('NEW_OWNER'); const [UNAUTHORIZED, _] = utils.generatePubKeyPair('UNAUTHORIZED'); const INSTANCE_SALT = new Uint8Array(32).fill(8675309); @@ -100,8 +100,10 @@ describe('ZOwnablePK', () => { }); describe('when not initialized correctly', () => { + let isNotInit = false; + beforeEach(() => { - ownable = new ZOwnablePKSimulator(randomByteArray, INSTANCE_SALT, false); + ownable = new ZOwnablePKSimulator(randomByteArray, INSTANCE_SALT, isNotInit); }); type FailingCircuits = [method: keyof ZOwnablePKSimulator, args: unknown[]]; const randomByteArray = new Uint8Array(32).fill(123); @@ -122,7 +124,7 @@ describe('ZOwnablePK', () => { }); it('should allow pure computeOwnerId', () => { - const eitherOwner = utils.createEitherTestUser("OWNER"); + const eitherOwner = utils.createEitherTestUser('OWNER'); expect(() => { ownable._computeOwnerId(eitherOwner, randomByteArray); @@ -358,50 +360,87 @@ describe('ZOwnablePK', () => { }); }); - /** - * @TODO parameterize - */ describe('_computeOwnerCommitment', () => { - it('should match local and contract commitment', () => { - const id = createIdHash(Z_OWNER, secretNonce); - const counter = INIT_COUNTER; - - // Check buildCommitmentFromId - const hashFromContract = ownable._computeOwnerCommitment(id, counter); - const hashFromHelper1 = buildCommitmentFromId( - id, - INSTANCE_SALT, - counter, - ); - expect(hashFromContract).toEqual(hashFromHelper1); - - // Check buildCommitment - const hashFromHelper2 = buildCommitment( - Z_OWNER, - secretNonce, - INSTANCE_SALT, - counter, - DOMAIN, - ); - expect(hashFromHelper1).toEqual(hashFromHelper2); - }); + const MAX_U64 = 2n ** 64n - 1n; + const testCases = [ + ...Array.from({ length: 10 }, (_, i) => ({ + label: `User${i}`, + ownerPK: utils.encodeToPK(`User${i}`), + counter: BigInt(Math.floor(Math.random() * 2 ** 64 - 1)), + })), + { + label: 'ZeroCounter', + ownerPK: utils.encodeToPK('ZeroCounter'), + counter: 0n, + }, + { + label: 'MaxCounter', + ownerPK: utils.encodeToPK('MaxUser'), + counter: MAX_U64, + }, + ]; + it.each(testCases)( + 'should match commitment for $label with counter $counter', + ({ ownerPK, counter }) => { + const id = createIdHash(ownerPK, secretNonce); + + // Check buildCommitmentFromId + const hashFromContract = ownable._computeOwnerCommitment(id, counter); + const hashFromHelper1 = buildCommitmentFromId( + id, + INSTANCE_SALT, + counter, + ); + expect(hashFromContract).toEqual(hashFromHelper1); + + // Check buildCommitment + const hashFromHelper2 = buildCommitment( + ownerPK, + secretNonce, + INSTANCE_SALT, + counter, + DOMAIN, + ); + expect(hashFromHelper1).toEqual(hashFromHelper2); + }, + ); }); describe('_computeOwnerId', () => { - it('should match local and contract owner id', () => { - const eitherOwner = utils.createEitherTestUser("OWNER"); - const ownerId = ownable._computeOwnerId(eitherOwner, secretNonce); - const expId = createIdHash(Z_OWNER, secretNonce); - - expect(ownerId).toEqual(expId); - }); + const testCases = [ + ...Array.from({ length: 10 }, (_, i) => ({ + label: `User${i}`, + eitherOwner: utils.createEitherTestUser(`User${i}`), + nonce: new Uint8Array(32).fill(i), + })), + { + label: 'All-zero nonce', + eitherOwner: utils.createEitherTestUser('ZeroUser'), + nonce: new Uint8Array(32).fill(0), + }, + { + label: 'Max nonce', + eitherOwner: utils.createEitherTestUser('MaxUser'), + nonce: new Uint8Array(32).fill(255), + }, + ]; + + it.each(testCases)( + 'should match local and contract owner id for $label', + ({ eitherOwner, nonce }) => { + const ownerId = ownable._computeOwnerId(eitherOwner, nonce); + const expId = createIdHash(eitherOwner.left, nonce); + expect(ownerId).toEqual(expId); + }, + ); it('should fail to compute ContractAddress id', () => { - const eitherContract = utils.createEitherTestContractAddress("CONTRACT"); + const eitherContract = + utils.createEitherTestContractAddress('CONTRACT'); expect(() => { ownable._computeOwnerId(eitherContract, secretNonce); - }).toThrow('ZOwnablePK: contract address owners are not yet supported') - }) + }).toThrow('ZOwnablePK: contract address owners are not yet supported'); + }); }); describe('_transferOwnership', () => { From 00efb15db3eceeb4c52553ded09a6a453cffeab4 Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 13 Aug 2025 01:55:27 -0300 Subject: [PATCH 104/322] fix fmt --- contracts/ownable/src/ZOwnablePK.compact | 5 ++++- contracts/ownable/src/test/Ownable.test.ts | 4 ++-- .../src/test/simulators/ZOwnablePKSimulator.ts | 11 +++++++---- contracts/ownable/src/test/utils/address.ts | 13 +++++++++++-- 4 files changed, 24 insertions(+), 9 deletions(-) diff --git a/contracts/ownable/src/ZOwnablePK.compact b/contracts/ownable/src/ZOwnablePK.compact index 91aa3771..4111f415 100644 --- a/contracts/ownable/src/ZOwnablePK.compact +++ b/contracts/ownable/src/ZOwnablePK.compact @@ -241,7 +241,10 @@ module ZOwnablePK { * @param {Bytes<32>} nonce - A private nonce to scope the commitment. * @returns {Bytes<32>} The computed owner ID. */ - export pure circuit _computeOwnerId(pk: Either, nonce: Bytes<32>): Bytes<32> { + export pure circuit _computeOwnerId( + pk: Either, + nonce: Bytes<32> + ): Bytes<32> { if (!pk.is_left) { assert(false, "ZOwnablePK: contract address owners are not yet supported"); } diff --git a/contracts/ownable/src/test/Ownable.test.ts b/contracts/ownable/src/test/Ownable.test.ts index 948201f4..60053cb7 100644 --- a/contracts/ownable/src/test/Ownable.test.ts +++ b/contracts/ownable/src/test/Ownable.test.ts @@ -4,8 +4,8 @@ import { OwnableSimulator } from './simulators/OwnableSimulator.js'; import * as utils from './utils/address.js'; // PKs -const [OWNER, Z_OWNER] = utils.generateEitherPubKeyPair("OWNER"); -const [NEW_OWNER, Z_NEW_OWNER] = utils.generateEitherPubKeyPair("NEW_OWNER"); +const [OWNER, Z_OWNER] = utils.generateEitherPubKeyPair('OWNER'); +const [NEW_OWNER, Z_NEW_OWNER] = utils.generateEitherPubKeyPair('NEW_OWNER'); const [UNAUTHORIZED, _] = utils.generateEitherPubKeyPair('UNAUTHORIZED'); // Encoded contract addresses diff --git a/contracts/ownable/src/test/simulators/ZOwnablePKSimulator.ts b/contracts/ownable/src/test/simulators/ZOwnablePKSimulator.ts index bd359375..adb7d270 100644 --- a/contracts/ownable/src/test/simulators/ZOwnablePKSimulator.ts +++ b/contracts/ownable/src/test/simulators/ZOwnablePKSimulator.ts @@ -5,12 +5,12 @@ import { } from '@midnight-ntwrk/compact-runtime'; import { sampleContractAddress } from '@midnight-ntwrk/zswap'; import { + type ContractAddress, + type Either, type Ledger, ledger, Contract as MockOwnable, - Either, - ZswapCoinPublicKey, - ContractAddress + type ZswapCoinPublicKey, } from '../../artifacts/MockZOwnablePK/contract/index.cjs'; import { ZOwnablePKPrivateState, @@ -244,7 +244,10 @@ export class ZOwnablePKSimulator extends AbstractContractSimulator< /** * @description */ - public _computeOwnerId(pk: Either, nonce: Uint8Array): Uint8Array { + public _computeOwnerId( + pk: Either, + nonce: Uint8Array, + ): Uint8Array { return this.circuits.pure._computeOwnerId(pk, nonce); } diff --git a/contracts/ownable/src/test/utils/address.ts b/contracts/ownable/src/test/utils/address.ts index de4dcf86..640dbb86 100644 --- a/contracts/ownable/src/test/utils/address.ts +++ b/contracts/ownable/src/test/utils/address.ts @@ -64,7 +64,13 @@ export const createEitherTestContractAddress = (str: string) => ({ const baseGeneratePubKeyPair = ( str: string, asEither: boolean, -): [string, Compact.ZswapCoinPublicKey | Compact.Either] => { +): [ + string, + ( + | Compact.ZswapCoinPublicKey + | Compact.Either + ), +] => { const pk = toHexPadded(str); const zpk = asEither ? createEitherTestUser(str) : encodeToPK(str); return [pk, zpk]; @@ -74,7 +80,10 @@ export const generatePubKeyPair = (str: string) => baseGeneratePubKeyPair(str, false) as [string, Compact.ZswapCoinPublicKey]; export const generateEitherPubKeyPair = (str: string) => - baseGeneratePubKeyPair(str, true) as [string, Compact.Either]; + baseGeneratePubKeyPair(str, true) as [ + string, + Compact.Either, + ]; export const zeroUint8Array = (length = 32) => convert_bigint_to_Uint8Array(length, 0n); From 81ebb9f5f033de5144bfc34a67b61a8416d70b27 Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 13 Aug 2025 02:06:38 -0300 Subject: [PATCH 105/322] add ledger and witness docs --- contracts/ownable/src/ZOwnablePK.compact | 30 ++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/contracts/ownable/src/ZOwnablePK.compact b/contracts/ownable/src/ZOwnablePK.compact index 4111f415..99f4a998 100644 --- a/contracts/ownable/src/ZOwnablePK.compact +++ b/contracts/ownable/src/ZOwnablePK.compact @@ -45,10 +45,40 @@ module ZOwnablePK { import CompactStandardLibrary; import "../../node_modules/@openzeppelin-compact/utils/src/Initializable" prefix Initializable_; + /** + * @ledger _ownerCommitment + * @description Stores the current hashed commitment representing the owner. + * This commitment is derived from the public identifier (e.g., `H(pk, nonce)`), + * the `instanceSalt`, the transfer `counter`, and a domain separator. + * + * A commitment of `default>` (i.e., zero) indicates the contract is unowned. + */ export ledger _ownerCommitment: Bytes<32>; + /** + * @ledger _counter + * @description Internal transfer counter used to prevent commitment reuse. + * + * Increments by 1 on every successful ownership transfer. Combined with `id` and + * `instanceSalt` to compute unique owner commitments over time. + */ export ledger _counter: Counter; + /** + * @sealed @ledger _instanceSalt + * @description A per-instance value provided at initialization used to namespace + * commitments for this contract instance. + * + * This salt prevents commitment collisions across contracts that might otherwise use + * the same owner identifiers or domain parameters. It is immutable after initialization. + */ export sealed ledger _instanceSalt: Bytes<32>; + /** + * @witness secretNonce + * @description A private per-user nonce used in deriving the shielded owner identifier. + * + * Combined with the user's public key as `H(pk, nonce)` to produce an obfuscated, + * unlinkable identity commitment. Users are encouraged to rotate this value on ownership changes. + */ export witness secretNonce(): Bytes<32>; /** From c5417c1a7ec28871f4a7904e237a6729110fd881 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Wed, 13 Aug 2025 13:05:34 -0400 Subject: [PATCH 106/322] Revert "Add contractAddress to nonce generation scheme" This reverts commit 54116e29d3b33bd953b9f359047458c628683b1b. --- .../ShieldedAccessControlWitnesses.ts | 44 +++++-------------- 1 file changed, 10 insertions(+), 34 deletions(-) diff --git a/contracts/shieldedAccessControl/src/witnesses/ShieldedAccessControlWitnesses.ts b/contracts/shieldedAccessControl/src/witnesses/ShieldedAccessControlWitnesses.ts index 08260719..88e1c589 100644 --- a/contracts/shieldedAccessControl/src/witnesses/ShieldedAccessControlWitnesses.ts +++ b/contracts/shieldedAccessControl/src/witnesses/ShieldedAccessControlWitnesses.ts @@ -7,6 +7,7 @@ import { type WitnessContext, } from '@midnight-ntwrk/compact-runtime'; import { encodeContractAddress } from '@midnight-ntwrk/ledger'; +import { sampleContractAddress } from '@midnight-ntwrk/zswap'; import { type ContractAddress, type Either, @@ -20,14 +21,13 @@ const { hkdfSync } = await import('node:crypto'); const KEYLEN = 32; /** - * @description The respective nonce and contract value for a given `roleId` should be at the same index + * @description The respective `nonce` value for a given `roleId` should be at the same index * for each array of `Buffer`s */ export type ShieldedAccessControlPrivateState = { secretKey: Buffer; nonces: Buffer[]; roleIds: Buffer[]; - contractAddress: Buffer; }; /** @@ -36,7 +36,6 @@ export type ShieldedAccessControlPrivateState = { * @param roleId - The role identifier. * @param salt - A salt value. * @param account - The public key of an account. - * @param contractAddress - The contract address of the contract being called. * * @returns A unique nonce value for `roleId` */ @@ -45,10 +44,9 @@ function generateNonce( roleId: Buffer, salt: Buffer, account: Buffer, - contractAddress: Buffer, ): Buffer { const domainString = Buffer.from('role-nonce'); - const info = Buffer.concat([domainString, roleId, account, contractAddress]); + const info = Buffer.concat([domainString, roleId, account]); const nonce = hkdfSync('sha512', secretKey, salt, info, KEYLEN); return Buffer.from(nonce); @@ -59,7 +57,6 @@ function generateNonce( * @param account - The public key of an account. * @param roleId - The role identifier. * @param nonce - The nonce associated with `roleId`. - * @param contractAddress - The address of the contract being called. * * @returns Whether the account was approved for a role */ @@ -67,7 +64,6 @@ function sendRoleRequestToAdmin( _account: Buffer, _roleId: Buffer, _nonce: Buffer, - _contractAddress: Buffer, ) { return true; } @@ -124,7 +120,6 @@ export const ShieldedAccessControlWitnesses = { secretKey: privateState.secretKey, roleIds: [], nonces: [], - contractAddress: privateState.contractAddress, }; const contract = @@ -137,12 +132,7 @@ export const ShieldedAccessControlWitnesses = { currentZswapLocalState, } = contract.initialState( constructorContext( - { - secretKey: privateState.secretKey, - nonces: [], - roleIds: [], - contractAddress: privateState.contractAddress, - }, + { secretKey: privateState.secretKey, nonces: [], roleIds: [] }, coinPubKey, ), ); @@ -157,18 +147,17 @@ export const ShieldedAccessControlWitnesses = { }; for (let i = 0; i < roles.length; i++) { - const role = Buffer.from(roles[i]); + const role = roles[i]; const nonce = generateNonce( privateState.secretKey, - role, + Buffer.from(role), Buffer.from(salt), Buffer.from(account), - privateState.contractAddress, ); const eitherAccount: Either = { is_left: true, left: { bytes: account }, - right: { bytes: new Uint8Array(32) }, + right: { bytes: encodeContractAddress(sampleContractAddress()) }, }; try { @@ -180,7 +169,7 @@ export const ShieldedAccessControlWitnesses = { ); if (hasRole) { newPrivateState.nonces.push(nonce); - newPrivateState.roleIds.push(role); + newPrivateState.roleIds.push(Buffer.from(role)); } } catch (err) { console.log(err); @@ -195,15 +184,11 @@ export const ShieldedAccessControlWitnesses = { * @param roleId - The role identifier. * @param account - The public key requesting a role. * @param salt - A salt value. - * @param contractAddress - The address of the contract being called. * * @returns An array of the new private state and an empty array */ requestRole: ( - { - privateState, - contractAddress, - }: WitnessContext, + { privateState }: WitnessContext, roleId: Uint8Array, account: Uint8Array, salt: Uint8Array, @@ -211,22 +196,13 @@ export const ShieldedAccessControlWitnesses = { const saltBuff = Buffer.from(salt); const roleIdBuff = Buffer.from(roleId); const accountBuff = Buffer.from(account); - const contractAddressBuff = Buffer.from( - encodeContractAddress(contractAddress), - ); const nonce = generateNonce( privateState.secretKey, roleIdBuff, saltBuff, accountBuff, - contractAddressBuff, - ); - const isApproved = sendRoleRequestToAdmin( - accountBuff, - roleIdBuff, - nonce, - contractAddressBuff, ); + const isApproved = sendRoleRequestToAdmin(accountBuff, roleIdBuff, nonce); if (isApproved) { privateState.nonces.push(nonce); From db5821f94cdf936d0f8f4fc4b73cdc4639b142fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Wed, 13 Aug 2025 14:32:10 -0400 Subject: [PATCH 107/322] Update hashing scheme --- .../src/ShieldedAccessControl.compact | 155 +++++++++--------- .../mocks/MockShieldedAccessControl.compact | 2 +- 2 files changed, 80 insertions(+), 77 deletions(-) diff --git a/contracts/shieldedAccessControl/src/ShieldedAccessControl.compact b/contracts/shieldedAccessControl/src/ShieldedAccessControl.compact index 925c7ffb..a36cd04e 100644 --- a/contracts/shieldedAccessControl/src/ShieldedAccessControl.compact +++ b/contracts/shieldedAccessControl/src/ShieldedAccessControl.compact @@ -8,7 +8,7 @@ pragma language_version >= 0.16.0; * This module provides a shielded role-based access control mechanism, where roles can be used to * represent a set of permissions. Roles are stored as Merkle tree commitments to avoid * disclosing information about role holder. Role commitments are created with the following - * hashing scheme SHA256( SHA256(PublicKey | roleIdentifier | nonce) | index). + * hashing scheme SHA256( SHA256(roleIdentifier | account | nonce | contractAddress) | index). * * @notice Using the SHA256 hashing function comes at a significant performace cost. In the future, we * plan on migrating to a ZK-friendly hashing function like Poseidon when an implementation is available. @@ -84,8 +84,8 @@ module ShieldedAccessControl { import "ShieldedAccessControlUtils" prefix Utils_; /** - * @description A Merkle tree of role commitments stored as SHA256( SHA256(PK | role | nonce) | index) - * @type {Bytes<32>} roleCommitment - A commitment created by the following hash: SHA256( SHA256(PK | role | nonce) | index). + * @description A Merkle tree of role commitments stored as SHA256( SHA256(roleId | account | nonce | contractAddress) | index) + * @type {Bytes<32>} roleCommitment - A commitment created by the following hash: SHA256( SHA256(roleId | account | nonce | contractAddress) | index). * @type {MerkleTree<10, roleCommitment>} * @type {MerkleTree<10, Bytes<32>>} _operatorRoles  */ @@ -102,14 +102,14 @@ module ShieldedAccessControl { /** * @description A set of nullifiers used to revoke the permissions of a role - * @type {Bytes<32> roleCommitment - A roleCommitment created by the following hash: SHA256( SHA256(PK | role | nonce) | index). + * @type {Bytes<32> roleCommitment - A roleCommitment created by the following hash: SHA256( SHA256(roleId | account | nonce | contractAddress) | index). * @type {Set} _roleCommitmentNullifiers  */ export ledger _roleCommitmentNullifiers: Set>; /** * @description Mapping from an intermediate role commitment hash to an index in the `_operatorRoles` Merkle tree. - * @type {Bytes<32>} intermediateRoleCommitment - An intermediate role commitment hash created by the following hashing scheme: SHA256(PK | role | nonce). + * @type {Bytes<32>} intermediateRoleCommitment - An intermediate role commitment hash created by the following hashing scheme: SHA256(roleId | account | nonce | contractAddress). * @type {Uint<64>} index - The index of a role commitment in the `_operatorRoles` Merkle tree. * @type {Map} * @type {Map, Uint<64>>} _roleCommitmentIndex @@ -137,7 +137,7 @@ module ShieldedAccessControl { * * @circuitInfo * - * @param {Bytes<32>} roleCommitment - A commitment created by the following hash: SHA256( SHA256(PK | role | nonce) | index). + * @param {Bytes<32>} roleCommitment - A commitment created by the following hash: SHA256( SHA256(roleId | account | nonce | contractAddress) | index). * @param {Uint<64>} index - An index in the `_operatorRoles` Merkle tree * @return {MerkleTreePath<10, Bytes<32>>} - The Merkle path of `roleCommitment` in the `_operatorRoles` Merkle tree  */ @@ -170,21 +170,21 @@ module ShieldedAccessControl { /** * @description Returns `true` if `account` has been granted `roleId`. * - * @circuitInfo k=16, rows=50605 + * @circuitInfo k=16, rows=60150 * * Requirements: * - * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce) + * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce | contractAddress) * must exist in the `_roleCommitmentIndex` map. - * - A nullifier for the role commitment produced by SHA256( SHA256(roleId | account | nonce) | index) + * - A nullifier for the role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index) * must not exist in the `_roleCommitmentNullifiers` set. - * - A path for the role commitment produced by SHA256( SHA256(roleId | account | nonce) | index) must + * - A path for the role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index) must * exist at `index` in the `_operatorRoles` Merkle tree. * * Disclosures: * - * - The intermediate role commitment produced by SHA256(roleId | account | nonce). - * - The role commitment produced by SHA256( SHA256(roleId | account | nonce) | index). + * - The intermediate role commitment produced by SHA256(roleId | account | nonce | contractAddress). + * - The role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index). * - The Merkle tree path for the role commitment stored at `index` in the `_operatorRoles` * Merkle tree. * - The type data of `account` - a ZswapCoinPublicKey or ContractAddress. @@ -205,24 +205,24 @@ module ShieldedAccessControl { } /** - * @description Reverts if `ownPublicKey()` is missing `roleId`. + * @description Reverts if caller is missing `roleId`. * - * @circuitInfo k=15, rows=25046 + * @circuitInfo k=15, rows=29780 * * Requirements: * - * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce) + * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce | contractAddress) * must exist in the `_roleCommitmentIndex` map. - * - A nullifier for the role commitment produced by SHA256( SHA256(roleId | account | nonce) | index) + * - A nullifier for the role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index) * must not exist in the `_roleCommitmentNullifiers` set. - * - A path for the role commitment produced by SHA256( SHA256(roleId | account | nonce) | index) must + * - A path for the role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index) must * exist at `index` in the `_operatorRoles` Merkle tree. * - The caller must not be a ContractAddress. * * Disclosures: * - * - The intermediate role commitment produced by SHA256(roleId | account | nonce). - * - The role commitment produced by SHA256( SHA256(roleId | account | nonce) | index). + * - The intermediate role commitment produced by SHA256(roleId | account | nonce | contractAddress). + * - The role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index). * - The Merkle tree path for the role commitment stored at `index` in the `_operatorRoles` * Merkle tree. * - The type data of `account` - a ZswapCoinPublicKey or ContractAddress. @@ -242,21 +242,21 @@ module ShieldedAccessControl { /** * @description Reverts if `account` is missing `roleId`. * - * @circuitInfo k=16, rows=50584 + * @circuitInfo k=16, rows=60129 * * Requirements: * - * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce) + * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce | contractAddress) * must exist in the `_roleCommitmentIndex` map. - * - A nullifier for the role commitment produced by SHA256( SHA256(roleId | account | nonce) | index) + * - A nullifier for the role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index) * must not exist in the `_roleCommitmentNullifiers` set. - * - A path for the role commitment produced by SHA256( SHA256(roleId | account | nonce) | index) must + * - A path for the role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index) must * exist at `index` in the `_operatorRoles` Merkle tree. * * Disclosures: * - * - The intermediate role commitment produced by SHA256(roleId | account | nonce). - * - The role commitment produced by SHA256( SHA256(roleId | account | nonce) | index). + * - The intermediate role commitment produced by SHA256(roleId | account | nonce | contractAddress). + * - The role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index). * - The Merkle tree path for the role commitment stored at `index` in the `_operatorRoles` * Merkle tree. * - The type data of `account` - a ZswapCoinPublicKey or ContractAddress. @@ -273,21 +273,21 @@ module ShieldedAccessControl { /** * @description Checks if a path exists for a role commitment. * - * @circuitInfo k=15, rows=25067 + * @circuitInfo k=15, rows=29801 * * Requirements: * - * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce) + * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce | contractAddress) * must exist in the `_roleCommitmentIndex` map. - * - A nullifier for the role commitment produced by SHA256( SHA256(roleId | account | nonce) | index) + * - A nullifier for the role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index) * must not exist in the `_roleCommitmentNullifiers` set. - * - A path for the role commitment produced by SHA256( SHA256(roleId | account | nonce) | index) must + * - A path for the role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index) must * exist at `index` in the `_operatorRoles` Merkle tree. * * Disclosures: * - * - The intermediate role commitment produced by SHA256(roleId | account | nonce). - * - The role commitment produced by SHA256( SHA256(roleId | account | nonce) | index). + * - The intermediate role commitment produced by SHA256(roleId | account | nonce | contractAddress). + * - The role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index). * - The Merkle tree path for the role commitment stored at `index` in the `_operatorRoles` * Merkle tree. * @@ -295,10 +295,11 @@ module ShieldedAccessControl { * @param {Bytes<32>} account - The account to check represented as a Bytes<32>. * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK) * @return {Boolean} - A boolean determining if a path for for the role commitment - * produced by SHA256( SHA256(roleId | account | nonce) | index) exists in the `_operatorRoles` Merkle tree + * produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index) exists in the `_operatorRoles` Merkle tree */ export circuit _checkMerkleTree(roleId: Bytes<32>, account: Bytes<32>, nonce: Bytes<32>): Boolean { - const intermediateRoleCommitment = persistentHash>>([roleId, account, nonce]); + const contractAddress = kernel.self().bytes; + const intermediateRoleCommitment = persistentHash>>([roleId, account, nonce, contractAddress]); assert(_roleCommitmentIndex.member(disclose(intermediateRoleCommitment)), "ShieldedAccessControl: role commitment index not found"); const index = _roleCommitmentIndex.lookup(disclose(intermediateRoleCommitment)); @@ -331,22 +332,22 @@ module ShieldedAccessControl { /** * @description Grants `roleId` to `account`. * - * @circuitInfo k=17, rows=114944 + * @circuitInfo k=18, rows=138761 * * Requirements: * * - `account` must not be a ContractAddress. - * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce) + * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce | contractAddress) * must exist in the `_roleCommitmentIndex` map. - * - A nullifier for the role commitment produced by SHA256( SHA256(roleId | account | nonce) | index) + * - A nullifier for the role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index) * must not exist in the `_roleCommitmentNullifiers` set. - * - A path for the role commitment produced by SHA256( SHA256(roleId | account | nonce) | index) must + * - A path for the role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index) must * exist at `index` in the `_operatorRoles` Merkle tree. * * Disclosures: * - * - The intermediate role commitment produced by SHA256(roleId | account | nonce). - * - The role commitment produced by SHA256( SHA256(roleId | account | nonce) | index). + * - The intermediate role commitment produced by SHA256(roleId | account | nonce | contractAddress). + * - The role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index). * - The Merkle tree path for the role commitment stored at `index` in the `_operatorRoles` * Merkle tree. * - The type data of `account` - a ZswapCoinPublicKey or ContractAddress. @@ -364,22 +365,22 @@ module ShieldedAccessControl { /** * @description Revokes `roleId` from `account`. * - * @circuitInfo k=17, rows=114699 + * @circuitInfo k=18, rows=138517 * * Requirements: * * - `account` must not be a ContractAddress. - * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce) + * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce | contractAddress) * must exist in the `_roleCommitmentIndex` map. - * - A nullifier for the role commitment produced by SHA256( SHA256(roleId | account | nonce) | index) + * - A nullifier for the role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index) * must not exist in the `_roleCommitmentNullifiers` set. - * - A path for the role commitment produced by SHA256( SHA256(roleId | account | nonce) | index) must + * - A path for the role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index) must * exist at `index` in the `_operatorRoles` Merkle tree. * * Disclosures: * - * - The intermediate role commitment produced by SHA256(roleId | account | nonce). - * - The role commitment produced by SHA256( SHA256(roleId | account | nonce) | index). + * - The intermediate role commitment produced by SHA256(roleId | account | nonce | contractAddress). + * - The role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index). * - The Merkle tree path for the role commitment stored at `index` in the `_operatorRoles` * Merkle tree. * - The type data of `account` - a ZswapCoinPublicKey or ContractAddress. @@ -401,23 +402,23 @@ module ShieldedAccessControl { * purpose is to provide a mechanism for accounts to lose their privileges * if they are compromised (such as when a trusted device is misplaced). * - * @circuitInfo k=17, rows=89905 + * @circuitInfo k=17, rows=108992 * * Requirements: * * - The caller must be `callerConfirmation`. * - The caller must not be a `ContractAddress`. - * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce) + * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce | contractAddress) * must exist in the `_roleCommitmentIndex` map. - * - A nullifier for the role commitment produced by SHA256( SHA256(roleId | account | nonce) | index) + * - A nullifier for the role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index) * must not exist in the `_roleCommitmentNullifiers` set. - * - A path for the role commitment produced by SHA256( SHA256(roleId | account | nonce) | index) must + * - A path for the role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index) must * exist at `index` in the `_operatorRoles` Merkle tree. * * Disclosures: * - * - The intermediate role commitment produced by SHA256(roleId | account | nonce). - * - The role commitment produced by SHA256( SHA256(roleId | account | nonce) | index). + * - The intermediate role commitment produced by SHA256(roleId | account | nonce | contractAddress). + * - The role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index). * - The Merkle tree path for the role commitment stored at `index` in the `_operatorRoles` * Merkle tree. * - The type data of `callerConfirmation` - a ZswapCoinPublicKey or ContractAddress. @@ -450,22 +451,22 @@ module ShieldedAccessControl { * @description Attempts to grant `roleId` to `account` and returns a boolean indicating if `roleId` was granted. * Internal circuit without access restriction. * - * @circuitInfo k=17, rows=90077 + * @circuitInfo k=17, rows=109163 * * Requirements: * * - `account` must not be a ContractAddress. - * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce) + * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce | contractAddress) * must exist in the `_roleCommitmentIndex` map. - * - A nullifier for the role commitment produced by SHA256( SHA256(roleId | account | nonce) | index) + * - A nullifier for the role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index) * must not exist in the `_roleCommitmentNullifiers` set. - * - A path for the role commitment produced by SHA256( SHA256(roleId | account | nonce) | index) must + * - A path for the role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index) must * exist at `index` in the `_operatorRoles` Merkle tree. * * Disclosures: * - * - The intermediate role commitment produced by SHA256(roleId | account | nonce). - * - The role commitment produced by SHA256( SHA256(roleId | account | nonce) | index). + * - The intermediate role commitment produced by SHA256(roleId | account | nonce | contractAddress). + * - The role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index). * - The Merkle tree path for the role commitment stored at `index` in the `_operatorRoles` * Merkle tree. * - The type data of `account` - a ZswapCoinPublicKey or ContractAddress. @@ -484,24 +485,24 @@ module ShieldedAccessControl { * @description Attempts to grant `roleId` to `account` and returns a boolean indicating if `roleId` was granted. * Internal circuit without access restriction. It does NOT check if the role is granted to a ContractAddress. * - * @circuitInfo k=17, rows=90076 + * @circuitInfo k=17, rows=109162 * * @notice External smart contracts cannot call the token contract at this time, so granting a role to an ContractAddress may * render a circuit permanently inaccessible. * * Requirements: * - * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce) + * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce | contractAddress) * must exist in the `_roleCommitmentIndex` map. - * - A nullifier for the role commitment produced by SHA256( SHA256(roleId | account | nonce) | index) + * - A nullifier for the role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index) * must not exist in the `_roleCommitmentNullifiers` set. - * - A path for the role commitment produced by SHA256( SHA256(roleId | account | nonce) | index) must + * - A path for the role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index) must * exist at `index` in the `_operatorRoles` Merkle tree. * * Disclosures: * - * - The intermediate role commitment produced by SHA256(roleId | account | nonce). - * - The role commitment produced by SHA256( SHA256(roleId | account | nonce) | index). + * - The intermediate role commitment produced by SHA256(roleId | account | nonce | contractAddress). + * - The role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index). * - The Merkle tree path for the role commitment stored at `index` in the `_operatorRoles` * Merkle tree. * - The type data of `account` - a ZswapCoinPublicKey or ContractAddress. @@ -531,21 +532,21 @@ module ShieldedAccessControl { * @description Attempts to revoke `roleId` from `account` and returns a boolean indicating if `roleId` was revoked. * Internal circuit without access restriction. * - * @circuitInfo k=17, rows=89829 + * @circuitInfo k=17, rows=108916 * * Requirements: * - * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce) + * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce | contractAddress) * must exist in the `_roleCommitmentIndex` map. - * - A nullifier for the role commitment produced by SHA256( SHA256(roleId | account | nonce) | index) + * - A nullifier for the role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index) * must not exist in the `_roleCommitmentNullifiers` set. - * - A path for the role commitment produced by SHA256( SHA256(roleId | account | nonce) | index) must + * - A path for the role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index) must * exist at `index` in the `_operatorRoles` Merkle tree. * * Disclosures: * - * - The intermediate role commitment produced by SHA256(roleId | account | nonce). - * - The role commitment produced by SHA256( SHA256(roleId | account | nonce) | index). + * - The intermediate role commitment produced by SHA256(roleId | account | nonce | contractAddress). + * - The role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index). * - The Merkle tree path for the role commitment stored at `index` in the `_operatorRoles` * Merkle tree. * - The type data of `account` - a ZswapCoinPublicKey or ContractAddress. @@ -576,11 +577,11 @@ module ShieldedAccessControl { * * WARNING: Exposing this circuit in the implementing contract would allow anyone to add roles. * - * @circuitInfo k=15, rows=19832 + * @circuitInfo k=15, rows=24565 * * Disclosures: * - * - The role commitment produced by SHA256( SHA256(roleId | account | nonce) | index). + * - The role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index). * * @param {Bytes<32>} roleId - The role identifier. * @param {Bytes<32>} account - The account to add represented as a Bytes<32>. @@ -588,7 +589,8 @@ module ShieldedAccessControl { * @return {Boolean} roleRevoked - A boolean indicating if `roleId` was revoked. */ circuit _addRoleCommitmentToLedger(roleId: Bytes<32>, account: Bytes<32>, nonce: Bytes<32>): [] { - const intermediateRoleCommitment = persistentHash>>([roleId, account, nonce]); + const contractAddress = kernel.self().bytes; + const intermediateRoleCommitment = persistentHash>>([roleId, account, nonce, contractAddress]); const index = _nextIndex.read(); const finalRoleCommitment = persistentHash>>([intermediateRoleCommitment, index as Field as Bytes<32>]); @@ -602,12 +604,12 @@ module ShieldedAccessControl { * * WARNING: Exposing this circuit in the implementing contract would allow anyone to revoke roles. * - * @circuitInfo k=15, rows=19824 + * @circuitInfo k=15, rows=24559 * * Disclosures: * - * - The role commitment produced by SHA256( SHA256(roleId | account | nonce) | index). - * - The intermediate role commitment produced by SHA256(roleId | account | nonce). + * - The role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index). + * - The intermediate role commitment produced by SHA256(roleId | account | nonce | contractAddress). * * @param {Bytes<32>} roleId - The role identifier. * @param {Bytes<32>} account - The account to add represented as a Bytes<32>. @@ -615,7 +617,8 @@ module ShieldedAccessControl { * @return {Boolean} roleRevoked - A boolean indicating if `roleId` was revoked. */ circuit _nullifyRoleCommitment(roleId: Bytes<32>, account: Bytes<32>, nonce: Bytes<32>): [] { - const intermediateRoleCommitment = persistentHash>>([roleId, account, nonce]); + const contractAddress = kernel.self().bytes; + const intermediateRoleCommitment = persistentHash>>([roleId, account, nonce, contractAddress]); const index = _roleCommitmentIndex.lookup(disclose(intermediateRoleCommitment)); const finalRoleCommitment = persistentHash>>([intermediateRoleCommitment, index as Field as Bytes<32>]); _roleCommitmentNullifiers.insert(disclose(finalRoleCommitment)); diff --git a/contracts/shieldedAccessControl/src/test/mocks/MockShieldedAccessControl.compact b/contracts/shieldedAccessControl/src/test/mocks/MockShieldedAccessControl.compact index 24f996a0..91a6e39f 100644 --- a/contracts/shieldedAccessControl/src/test/mocks/MockShieldedAccessControl.compact +++ b/contracts/shieldedAccessControl/src/test/mocks/MockShieldedAccessControl.compact @@ -70,4 +70,4 @@ export circuit _requestRole(roleId: Bytes<32>): [] { export circuit _recoverRoles(): [] { ShieldedAccessControl__recoverRoles(); -} \ No newline at end of file +} From 98c86dbe8d69d0e9bd6fd33d76be5bb53f0307c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Wed, 13 Aug 2025 15:31:57 -0400 Subject: [PATCH 108/322] Update hash function in witness --- .../src/witnesses/ShieldedAccessControlWitnesses.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/shieldedAccessControl/src/witnesses/ShieldedAccessControlWitnesses.ts b/contracts/shieldedAccessControl/src/witnesses/ShieldedAccessControlWitnesses.ts index 88e1c589..74ae49bd 100644 --- a/contracts/shieldedAccessControl/src/witnesses/ShieldedAccessControlWitnesses.ts +++ b/contracts/shieldedAccessControl/src/witnesses/ShieldedAccessControlWitnesses.ts @@ -47,7 +47,7 @@ function generateNonce( ): Buffer { const domainString = Buffer.from('role-nonce'); const info = Buffer.concat([domainString, roleId, account]); - const nonce = hkdfSync('sha512', secretKey, salt, info, KEYLEN); + const nonce = hkdfSync('sha256', secretKey, salt, info, KEYLEN); return Buffer.from(nonce); } From b556fad32fddb0af65ffbea709295fb90062a02f Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 15 Aug 2025 18:14:16 -0300 Subject: [PATCH 109/322] improve computeOwnerId assertion --- contracts/ownable/src/ZOwnablePK.compact | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/contracts/ownable/src/ZOwnablePK.compact b/contracts/ownable/src/ZOwnablePK.compact index 99f4a998..99aab56c 100644 --- a/contracts/ownable/src/ZOwnablePK.compact +++ b/contracts/ownable/src/ZOwnablePK.compact @@ -275,9 +275,7 @@ module ZOwnablePK { pk: Either, nonce: Bytes<32> ): Bytes<32> { - if (!pk.is_left) { - assert(false, "ZOwnablePK: contract address owners are not yet supported"); - } + assert(pk.is_left, "ZOwnablePK: contract address owners are not yet supported"); return persistentHash>>([pk.left.bytes, nonce]); } From a880f8dcd715604884f9fa4d059a250fcb82fcac Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 15 Aug 2025 19:45:23 -0300 Subject: [PATCH 110/322] use _computeOwnerId in assertOnlyOwner --- contracts/ownable/src/ZOwnablePK.compact | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/contracts/ownable/src/ZOwnablePK.compact b/contracts/ownable/src/ZOwnablePK.compact index 99aab56c..88e43e04 100644 --- a/contracts/ownable/src/ZOwnablePK.compact +++ b/contracts/ownable/src/ZOwnablePK.compact @@ -187,9 +187,13 @@ module ZOwnablePK { export circuit assertOnlyOwner(): [] { Initializable_assertInitialized(); - const caller = ownPublicKey(); const nonce = secretNonce(); - const id = persistentHash>>([caller.bytes, nonce]); + const callerAsEither = Either { + is_left: true, + left: ownPublicKey(), + right: ContractAddress { bytes: pad(32, "") } + }; + const id = _computeOwnerId(callerAsEither, nonce); assert(_ownerCommitment == _computeOwnerCommitment(id, _counter), "ZOwnablePK: caller is not the owner"); } @@ -267,7 +271,7 @@ module ZOwnablePK { * * - `pk` is not a ContractAddress. * - * @param {Bytes<32>} pk - The public key of the identity being committed. + * @param {Either} pk - The public key of the identity being committed. * @param {Bytes<32>} nonce - A private nonce to scope the commitment. * @returns {Bytes<32>} The computed owner ID. */ From de690186949c10dce94180e5fa69e2b9fb73f459 Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 15 Aug 2025 19:47:57 -0300 Subject: [PATCH 111/322] fix fmt --- contracts/ownable/src/test/ZOwnablePK.test.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/contracts/ownable/src/test/ZOwnablePK.test.ts b/contracts/ownable/src/test/ZOwnablePK.test.ts index cde94414..987a68fc 100644 --- a/contracts/ownable/src/test/ZOwnablePK.test.ts +++ b/contracts/ownable/src/test/ZOwnablePK.test.ts @@ -20,7 +20,7 @@ const BAD_NONCE = Buffer.from(Buffer.alloc(32, 'BAD_NONCE')); const DOMAIN = 'ZOwnablePK:shield:'; const INIT_COUNTER = 1n; -let isInit = true; +const isInit = true; let secretNonce: Uint8Array; let ownable: ZOwnablePKSimulator; @@ -100,10 +100,14 @@ describe('ZOwnablePK', () => { }); describe('when not initialized correctly', () => { - let isNotInit = false; + const isNotInit = false; beforeEach(() => { - ownable = new ZOwnablePKSimulator(randomByteArray, INSTANCE_SALT, isNotInit); + ownable = new ZOwnablePKSimulator( + randomByteArray, + INSTANCE_SALT, + isNotInit, + ); }); type FailingCircuits = [method: keyof ZOwnablePKSimulator, args: unknown[]]; const randomByteArray = new Uint8Array(32).fill(123); From 92b46a85182e04ba2f72b240cac7127b07c73d1e Mon Sep 17 00:00:00 2001 From: andrew Date: Sat, 16 Aug 2025 20:20:15 -0300 Subject: [PATCH 112/322] add wrapAsEither circuits --- contracts/src/utils/Utils.compact | 46 +++++++++++++++++++ .../src/utils/test/mocks/MockUtils.compact | 16 +++++++ .../utils/test/simulators/UtilsSimulator.ts | 28 +++++++++++ contracts/src/utils/test/utils.test.ts | 29 ++++++++++++ 4 files changed, 119 insertions(+) diff --git a/contracts/src/utils/Utils.compact b/contracts/src/utils/Utils.compact index 3528e10e..b7291176 100644 --- a/contracts/src/utils/Utils.compact +++ b/contracts/src/utils/Utils.compact @@ -66,6 +66,52 @@ module Utils { return !keyOrAddress.is_left; } + /** + * @description Wraps a value as the `L` variant of a disjoint union (`Either`), + * following Compact conventions. + * + * Sets `is_left` to true, assigns the provided value to `left`, and sets `right` to `default` per convention. + * + * This helper is useful when you already have a value of type `L`, but the circuit interface expects an `Either`. + * It avoids manually constructing the full struct and ensures consistent formatting. + * + * @template L - Type of the Left variant. + * @template R - Type of the Right variant. + * + * @param {L} left - The value to wrap as the Left variant. + * @returns {Either} A disjoint union (`Either`) with the value in the `L` variant. + */ + export pure circuit wrapAsEitherLeft(left: L): Either { + return Either { + is_left: true, + left: left, + right: default + }; + } + + /** + * @description Wraps a value as the `R` variant of a disjoint union (`Either`), + * following Compact conventions. + * + * Sets `is_left` to false, assigns the provided value to `right`, and sets `left` to `default` per convention. + * + * This helper is useful when you already have a value of type `R`, but the circuit interface expects an `Either`. + * It avoids manually constructing the full struct and ensures consistent formatting. + * + * @template L - Type of the Left variant. + * @template R - Type of the Right variant. + * + * @param {R} right - The value to wrap as the `R` variant. + * @returns {Either} A disjoint union (`Either`) with the value in the `R` variant. + */ + export pure circuit wrapAsEitherRight(right: R): Either { + return Either { + is_left: false, + left: default, + right: right + }; + } + /** * @description A helper function that returns the empty string: "". * diff --git a/contracts/src/utils/test/mocks/MockUtils.compact b/contracts/src/utils/test/mocks/MockUtils.compact index 54fd45f3..c3b66d64 100644 --- a/contracts/src/utils/test/mocks/MockUtils.compact +++ b/contracts/src/utils/test/mocks/MockUtils.compact @@ -25,6 +25,22 @@ export pure circuit isContractAddress(keyOrAddress: Either { + return Utils_wrapAsEitherLeft(pk); +} + +// Find a better way to test different combinations +// other than creating a circuit for each pair +export pure circuit wrapAsEitherPkOrAddressRight( + address: ContractAddress, +): Either { + return Utils_wrapAsEitherRight(address); +} + export pure circuit emptyString(): Opaque<"string"> { return Utils_emptyString(); } diff --git a/contracts/src/utils/test/simulators/UtilsSimulator.ts b/contracts/src/utils/test/simulators/UtilsSimulator.ts index 715569f8..5f46223e 100644 --- a/contracts/src/utils/test/simulators/UtilsSimulator.ts +++ b/contracts/src/utils/test/simulators/UtilsSimulator.ts @@ -139,6 +139,34 @@ export class UtilsSimulator ).result; } + /** + * @description Returns `pk` wrapped in an `Either` type. + * @param pk The target value to wrap. + * @returns `Either` with `pk` in the left position. + */ + public wrapAsEitherPkOrAddressLeft( + pk: ZswapCoinPublicKey, + ): Either { + return this.contract.circuits.wrapAsEitherPkOrAddressLeft( + this.circuitContext, + pk, + ).result; + } + + /** + * @description Returns `address` wrapped in an `Either` type. + * @param pk The target value to wrap. + * @returns `Either` with `address` in the right position. + */ + public wrapAsEitherPkOrAddressRight( + address: ContractAddress, + ): Either { + return this.contract.circuits.wrapAsEitherPkOrAddressRight( + this.circuitContext, + address, + ).result; + } + /** * @description A helper function that returns the empty string: "" * @returns The empty string: "" diff --git a/contracts/src/utils/test/utils.test.ts b/contracts/src/utils/test/utils.test.ts index 1398d4d9..52b78acc 100644 --- a/contracts/src/utils/test/utils.test.ts +++ b/contracts/src/utils/test/utils.test.ts @@ -1,4 +1,9 @@ import { describe, expect, it } from 'vitest'; +import type { + ContractAddress, + Either, + ZswapCoinPublicKey, +} from './../../../artifacts/MockUtils/contract/index.cjs'; // Combined imports import { UtilsSimulator } from './simulators/UtilsSimulator.js'; import * as contractUtils from './utils/address.js'; @@ -87,6 +92,30 @@ describe('Utils', () => { }); }); + describe('wrapAsEitherLeft', () => { + it('should wrap pk as left', () => { + const pk = contractUtils.encodeToPK('PK'); + const exp: Either = { + is_left: true, + left: pk, + right: { bytes: new Uint8Array(32).fill(0) }, + }; + expect(contract.wrapAsEitherPkOrAddressLeft(pk)).toEqual(exp); + }); + }); + + describe('wrapAsEitherRight', () => { + it('should wrap address as right', () => { + const address = contractUtils.encodeToPK('ADDRESS'); + const exp: Either = { + is_left: false, + left: { bytes: new Uint8Array(32).fill(0) }, + right: address, + }; + expect(contract.wrapAsEitherPkOrAddressRight(address)).toEqual(exp); + }); + }); + describe('emptyString', () => { it('should return the empty string', () => { expect(contract.emptyString()).toBe(EMPTY_STRING); From 9fc5bbee9cccb63e3c3e6eec6c979af959dfcec9 Mon Sep 17 00:00:00 2001 From: andrew Date: Sat, 16 Aug 2025 21:23:23 -0300 Subject: [PATCH 113/322] add option to compile directory in compact --- compact/src/Compiler.ts | 39 +++++++++++++++++++++++++++++++------- compact/src/runCompiler.ts | 37 ++++++++++++++++++++++++++++++++---- 2 files changed, 65 insertions(+), 11 deletions(-) diff --git a/compact/src/Compiler.ts b/compact/src/Compiler.ts index 53b292ae..e79bd48f 100755 --- a/compact/src/Compiler.ts +++ b/compact/src/Compiler.ts @@ -27,6 +27,12 @@ const COMPACTC_PATH: string = join(COMPACT_HOME, 'compactc'); * compiler.compile().catch(err => console.error(err)); * ``` * + * @example Compile specific directory + * ```typescript + * const compiler = new CompactCompiler('--skip-zk', 'security'); + * compiler.compile().catch(err => console.error(err)); + * ``` + * * @example Successful Compilation Output * ``` * ℹ [COMPILE] Found 2 .compact file(s) to compile @@ -48,19 +54,26 @@ const COMPACTC_PATH: string = join(COMPACT_HOME, 'compactc'); export class CompactCompiler { /** Stores the compiler flags passed via command-line arguments */ private readonly flags: string; + /** Optional target directory to limit compilation scope */ + private readonly targetDir?: string; /** * Constructs a new CompactCompiler instance, validating the `compactc` binary path. * * @param flags - Space-separated string of `compactc` flags (e.g., "--skip-zk --no-communications-commitment") + * @param targetDir - Optional subdirectory within src/ to limit compilation (e.g., "security", "utils") * @throws {Error} If the `compactc` binary is not found at the resolved path */ - constructor(flags: string) { + constructor(flags: string, targetDir?: string) { this.flags = flags.trim(); + this.targetDir = targetDir; const spinner = ora(); spinner.info(chalk.blue(`[COMPILE] COMPACT_HOME: ${COMPACT_HOME}`)); spinner.info(chalk.blue(`[COMPILE] COMPACTC_PATH: ${COMPACTC_PATH}`)); + if (this.targetDir) { + spinner.info(chalk.blue(`[COMPILE] TARGET_DIR: ${this.targetDir}`)); + } if (!existsSync(COMPACTC_PATH)) { spinner.fail( @@ -73,25 +86,36 @@ export class CompactCompiler { } /** - * Compiles all `.compact` files in the source directory and its subdirectories (e.g., `src/test/mock/`). - * Scans the `src` directory recursively for `.compact` files, compiles each one using `compactc`, - * and displays progress with a spinner and colored output. + * Compiles all `.compact` files in the source directory (or target subdirectory) and its subdirectories. + * Scans the `src` directory (or `src/{targetDir}`) recursively for `.compact` files, + * compiles each one using `compactc`, and displays progress with a spinner and colored output. * * @returns A promise that resolves when all files are compiled successfully * @throws {Error} If compilation fails for any file */ public async compile(): Promise { - const compactFiles: string[] = await this.getCompactFiles(SRC_DIR); + const searchDir = this.targetDir ? join(SRC_DIR, this.targetDir) : SRC_DIR; + + // Validate target directory exists + if (this.targetDir && !existsSync(searchDir)) { + const spinner = ora(); + spinner.fail(chalk.red(`[COMPILE] Error: Target directory ${searchDir} does not exist.`)); + throw new Error(`Target directory ${searchDir} does not exist`); + } + + const compactFiles: string[] = await this.getCompactFiles(searchDir); const spinner = ora(); if (compactFiles.length === 0) { - spinner.warn(chalk.yellow('[COMPILE] No .compact files found.')); + const searchLocation = this.targetDir ? `${this.targetDir}/` : ''; + spinner.warn(chalk.yellow(`[COMPILE] No .compact files found in ${searchLocation}.`)); return; } + const searchLocation = this.targetDir ? ` in ${this.targetDir}/` : ''; spinner.info( chalk.blue( - `[COMPILE] Found ${compactFiles.length} .compact file(s) to compile`, + `[COMPILE] Found ${compactFiles.length} .compact file(s) to compile${searchLocation}`, ), ); @@ -123,6 +147,7 @@ export class CompactCompiler { } if (entry.isFile() && fullPath.endsWith('.compact')) { + // Always return relative path from SRC_DIR, regardless of search directory return [relative(SRC_DIR, fullPath)]; } return []; diff --git a/compact/src/runCompiler.ts b/compact/src/runCompiler.ts index b7a84f24..9c6215ff 100644 --- a/compact/src/runCompiler.ts +++ b/compact/src/runCompiler.ts @@ -12,13 +12,20 @@ import { CompactCompiler } from './Compiler.js'; * ```bash * npx compact-compiler --skip-zk * ``` + * + * @example Compile specific directory + * ```bash + * npx compact-compiler --dir security --skip-zk + * ``` + * * Expected output: * ``` * ℹ [COMPILE] Compact compiler started * ℹ [COMPILE] COMPACT_HOME: /path/to/compactc * ℹ [COMPILE] COMPACTC_PATH: /path/to/compactc/compactc - * ℹ [COMPILE] Found 1 .compact file(s) to compile - * ✔ [COMPILE] [1/1] Compiled Foo.compact + * ℹ [COMPILE] TARGET_DIR: security + * ℹ [COMPILE] Found 1 .compact file(s) to compile in security/ + * ✔ [COMPILE] [1/1] Compiled security/AccessControl.compact * Compactc version: 0.24.0 * ``` */ @@ -26,8 +33,30 @@ async function runCompiler(): Promise { const spinner = ora(chalk.blue('[COMPILE] Compact Compiler started')).info(); try { - const compilerFlags = process.argv.slice(2).join(' '); - const compiler = new CompactCompiler(compilerFlags); + const args = process.argv.slice(2); + + // Parse arguments more robustly + let targetDir: string | undefined; + const compilerFlags: string[] = []; + + for (let i = 0; i < args.length; i++) { + if (args[i] === '--dir') { + if (i + 1 < args.length && !args[i + 1].startsWith('--')) { + targetDir = args[i + 1]; + i++; // Skip the next argument (directory name) + } else { + spinner.fail(chalk.red('[COMPILE] Error: --dir flag requires a directory name')); + console.log(chalk.yellow('Usage: compact-compiler --dir [other-flags]')); + console.log(chalk.yellow('Example: compact-compiler --dir security --skip-zk')); + process.exit(1); + } + } else { + // All other arguments are compiler flags + compilerFlags.push(args[i]); + } + } + + const compiler = new CompactCompiler(compilerFlags.join(' '), targetDir); await compiler.compile(); } catch (err) { spinner.fail( From 98b7a3210f04cb5f925fcc89b1259fda1a8c1a7f Mon Sep 17 00:00:00 2001 From: andrew Date: Sat, 16 Aug 2025 21:24:23 -0300 Subject: [PATCH 114/322] add granular compile scripts --- contracts/package.json | 5 ++++ turbo.json | 57 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/contracts/package.json b/contracts/package.json index 493433f8..fc36646d 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -15,6 +15,11 @@ }, "scripts": { "compact": "compact-compiler", + "compact:access": "compact-compiler --dir access", + "compact:archive": "compact-compiler --dir archive", + "compact:security": "compact-compiler --dir security", + "compact:token": "compact-compiler --dir token", + "compact:utils": "compact-compiler --dir utils", "build": "compact-builder && tsc", "test": "vitest run", "types": "tsc -p tsconfig.json --noEmit", diff --git a/turbo.json b/turbo.json index fc4bb12f..4b5acd9c 100644 --- a/turbo.json +++ b/turbo.json @@ -1,10 +1,63 @@ { "$schema": "https://turbo.build/schema.json", "tasks": { - "compact": { + "compact:security": { "dependsOn": ["^build"], "env": ["COMPACT_HOME"], - "inputs": ["contracts/src/**/*.compact"], + "inputs": ["src/security/**/*.compact"], + "outputLogs": "new-only", + "outputs": ["artifacts/**/"] + }, + "compact:utils": { + "dependsOn": ["^build"], + "env": ["COMPACT_HOME"], + "inputs": ["src/utils/**/*.compact"], + "outputLogs": "new-only", + "outputs": ["artifacts/**/"] + }, + "compact:access": { + "dependsOn": ["^build", "compact:security", "compact:utils"], + "env": ["COMPACT_HOME"], + "inputs": [ + "src/access/**/*.compact", + "artifacts/**" + ], + "outputLogs": "new-only", + "outputs": ["artifacts/**/"] + }, + "compact:archive": { + "dependsOn": ["^build", "compact:utils"], + "env": ["COMPACT_HOME"], + "inputs": [ + "src/archive/**/*.compact", + "artifacts/**" + ], + "outputLogs": "new-only", + "outputs": ["artifacts/**/"] + }, + "compact:token": { + "dependsOn": ["^build", "compact:security", "compact:utils"], + "env": ["COMPACT_HOME"], + "inputs": [ + "src/token/**/*.compact", + "artifacts/**" + ], + "outputLogs": "new-only", + "outputs": ["artifacts/**/"] + }, + "compact": { + "dependsOn": [ + "compact:security", + "compact:utils", + "compact:access", + "compact:archive", + "compact:token" + ], + "env": ["COMPACT_HOME"], + "inputs": [ + "src/**/*.compact", + "test/**/*.compact" + ], "outputLogs": "new-only", "outputs": ["artifacts/**"] }, From ae9e3118b8df28b02d847e4aed97d7e68ecf8bf9 Mon Sep 17 00:00:00 2001 From: andrew Date: Sat, 16 Aug 2025 21:36:15 -0300 Subject: [PATCH 115/322] fix fmt --- compact/src/Compiler.ts | 10 ++++++++-- compact/src/runCompiler.ts | 14 +++++++++++--- turbo.json | 20 ++++---------------- 3 files changed, 23 insertions(+), 21 deletions(-) diff --git a/compact/src/Compiler.ts b/compact/src/Compiler.ts index e79bd48f..af426286 100755 --- a/compact/src/Compiler.ts +++ b/compact/src/Compiler.ts @@ -99,7 +99,11 @@ export class CompactCompiler { // Validate target directory exists if (this.targetDir && !existsSync(searchDir)) { const spinner = ora(); - spinner.fail(chalk.red(`[COMPILE] Error: Target directory ${searchDir} does not exist.`)); + spinner.fail( + chalk.red( + `[COMPILE] Error: Target directory ${searchDir} does not exist.`, + ), + ); throw new Error(`Target directory ${searchDir} does not exist`); } @@ -108,7 +112,9 @@ export class CompactCompiler { const spinner = ora(); if (compactFiles.length === 0) { const searchLocation = this.targetDir ? `${this.targetDir}/` : ''; - spinner.warn(chalk.yellow(`[COMPILE] No .compact files found in ${searchLocation}.`)); + spinner.warn( + chalk.yellow(`[COMPILE] No .compact files found in ${searchLocation}.`), + ); return; } diff --git a/compact/src/runCompiler.ts b/compact/src/runCompiler.ts index 9c6215ff..a1e9e002 100644 --- a/compact/src/runCompiler.ts +++ b/compact/src/runCompiler.ts @@ -45,9 +45,17 @@ async function runCompiler(): Promise { targetDir = args[i + 1]; i++; // Skip the next argument (directory name) } else { - spinner.fail(chalk.red('[COMPILE] Error: --dir flag requires a directory name')); - console.log(chalk.yellow('Usage: compact-compiler --dir [other-flags]')); - console.log(chalk.yellow('Example: compact-compiler --dir security --skip-zk')); + spinner.fail( + chalk.red('[COMPILE] Error: --dir flag requires a directory name'), + ); + console.log( + chalk.yellow( + 'Usage: compact-compiler --dir [other-flags]', + ), + ); + console.log( + chalk.yellow('Example: compact-compiler --dir security --skip-zk'), + ); process.exit(1); } } else { diff --git a/turbo.json b/turbo.json index 4b5acd9c..fdc0cb74 100644 --- a/turbo.json +++ b/turbo.json @@ -18,30 +18,21 @@ "compact:access": { "dependsOn": ["^build", "compact:security", "compact:utils"], "env": ["COMPACT_HOME"], - "inputs": [ - "src/access/**/*.compact", - "artifacts/**" - ], + "inputs": ["src/access/**/*.compact", "artifacts/**"], "outputLogs": "new-only", "outputs": ["artifacts/**/"] }, "compact:archive": { "dependsOn": ["^build", "compact:utils"], "env": ["COMPACT_HOME"], - "inputs": [ - "src/archive/**/*.compact", - "artifacts/**" - ], + "inputs": ["src/archive/**/*.compact", "artifacts/**"], "outputLogs": "new-only", "outputs": ["artifacts/**/"] }, "compact:token": { "dependsOn": ["^build", "compact:security", "compact:utils"], "env": ["COMPACT_HOME"], - "inputs": [ - "src/token/**/*.compact", - "artifacts/**" - ], + "inputs": ["src/token/**/*.compact", "artifacts/**"], "outputLogs": "new-only", "outputs": ["artifacts/**/"] }, @@ -54,10 +45,7 @@ "compact:token" ], "env": ["COMPACT_HOME"], - "inputs": [ - "src/**/*.compact", - "test/**/*.compact" - ], + "inputs": ["src/**/*.compact", "test/**/*.compact"], "outputLogs": "new-only", "outputs": ["artifacts/**"] }, From 4a9885ba116b7303b11a014e057e6cb3e69675d4 Mon Sep 17 00:00:00 2001 From: andrew Date: Sat, 16 Aug 2025 22:53:48 -0300 Subject: [PATCH 116/322] use fast compilation prior to tests, cache tests --- contracts/package.json | 2 +- turbo.json | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/contracts/package.json b/contracts/package.json index fc36646d..acd41414 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -21,7 +21,7 @@ "compact:token": "compact-compiler --dir token", "compact:utils": "compact-compiler --dir utils", "build": "compact-builder && tsc", - "test": "vitest run", + "test": "compact-compiler --skip-zk && vitest run", "types": "tsc -p tsconfig.json --noEmit", "clean": "git clean -fXd" }, diff --git a/turbo.json b/turbo.json index fdc0cb74..86592d3e 100644 --- a/turbo.json +++ b/turbo.json @@ -50,9 +50,16 @@ "outputs": ["artifacts/**"] }, "test": { - "dependsOn": ["^build", "compact"], + "dependsOn": ["^build"], + "env": ["COMPACT_HOME"], + "inputs": [ + "src/**/*.ts", + "src/**/*.compact", + "vitest.config.ts", + "package.json" + ], "outputs": [], - "cache": false + "cache": true }, "build": { "dependsOn": ["^build"], From 6fb3f74f54a29bc988aa86175009ce2f1b7b4e67 Mon Sep 17 00:00:00 2001 From: andrew Date: Sun, 17 Aug 2025 01:53:05 -0300 Subject: [PATCH 117/322] add circuit tag --- contracts/ownable/src/ZOwnablePK.compact | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/ownable/src/ZOwnablePK.compact b/contracts/ownable/src/ZOwnablePK.compact index 88e43e04..bf06a4a3 100644 --- a/contracts/ownable/src/ZOwnablePK.compact +++ b/contracts/ownable/src/ZOwnablePK.compact @@ -217,6 +217,8 @@ module ZOwnablePK { * even with repeated `id` values. * - `domain`: A domain separator to prevent hash collisions when extending the module. * + * @circuitInfo k=???, rows=??? + * * Requirements: * * - Contract is initialized. From 2ac92b54d556bc9c9caaba1cb00dad663ae25f08 Mon Sep 17 00:00:00 2001 From: andrew Date: Sun, 17 Aug 2025 16:30:20 -0300 Subject: [PATCH 118/322] move ZOwnablePK witness interface --- .../src/access/witnesses/ZOwnablePKWitnesses.ts | 14 +++++++++++++- contracts/src/access/witnesses/interface.ts | 15 --------------- 2 files changed, 13 insertions(+), 16 deletions(-) delete mode 100644 contracts/src/access/witnesses/interface.ts diff --git a/contracts/src/access/witnesses/ZOwnablePKWitnesses.ts b/contracts/src/access/witnesses/ZOwnablePKWitnesses.ts index ce1e0aec..f034951b 100644 --- a/contracts/src/access/witnesses/ZOwnablePKWitnesses.ts +++ b/contracts/src/access/witnesses/ZOwnablePKWitnesses.ts @@ -1,7 +1,19 @@ import { getRandomValues } from 'node:crypto'; import type { WitnessContext } from '@midnight-ntwrk/compact-runtime'; import type { Ledger } from '../../../artifacts/MockZOwnablePK/contract/index.cjs'; -import type { IZOwnablePKWitnesses } from './interface.js'; + +/** + * @description Interface defining the witness methods for ZOwnablePK operations. + * @template P - The private state type. + */ +export interface IZOwnablePKWitnesses

{ + /** + * Retrieves the secret nonce from the private state. + * @param context - The witness context containing the private state. + * @returns A tuple of the private state and the secret nonce as a Uint8Array. + */ + secretNonce(context: WitnessContext): [P, Uint8Array]; +} /** * @description Represents the private state of an ownable contract, storing a secret nonce. diff --git a/contracts/src/access/witnesses/interface.ts b/contracts/src/access/witnesses/interface.ts deleted file mode 100644 index fb84e59d..00000000 --- a/contracts/src/access/witnesses/interface.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { WitnessContext } from '@midnight-ntwrk/compact-runtime'; -import type { Ledger } from '../../../artifacts/MockZOwnablePK/contract/index.cjs'; // Combined imports - -/** - * @description Interface defining the witness methods for Ownable operations. - * @template P - The private state type. - */ -export interface IZOwnablePKWitnesses

{ - /** - * Retrieves the secret nonce from the private state. - * @param context - The witness context containing the private state. - * @returns A tuple of the private state and the secret nonce as a Uint8Array. - */ - secretNonce(context: WitnessContext): [P, Uint8Array]; -} From db87258e357f3855b64405261392418d2c656f52 Mon Sep 17 00:00:00 2001 From: andrew Date: Sun, 17 Aug 2025 21:45:02 -0300 Subject: [PATCH 119/322] improve in-code docs --- contracts/src/access/ZOwnablePK.compact | 28 ++++++++++++------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/contracts/src/access/ZOwnablePK.compact b/contracts/src/access/ZOwnablePK.compact index 564e8093..65225663 100644 --- a/contracts/src/access/ZOwnablePK.compact +++ b/contracts/src/access/ZOwnablePK.compact @@ -93,7 +93,7 @@ module ZOwnablePK { * Requirements: * * - Contract is not initialized. - * - `ownerId` is not all zeroes. + * - `ownerId` is not an empty array. * * @param {Bytes<32>} ownerId - The owner's unique identifier H(pk, nonce). * @param {Bytes<32>} instanceSalt - Contract salt to prevent duplicate commitments if @@ -134,8 +134,8 @@ module ZOwnablePK { * Requirements: * * - Contract is initialized. - * - Caller must be the current owner. - * - `newOwnerId` must not be all zeroes. + * - Caller is the the current owner. + * - `newOwnerId` is not an empty array. * * @param {Bytes<32>} newOwnerId - The new owner's unique identifier (`H(pk, nonce)`). * @returns {[]} Empty tuple. @@ -158,7 +158,7 @@ module ZOwnablePK { * Requirements: * * - Contract is initialized. - * - Caller must be the current owner. + * - Caller is the the current owner. * * @returns {[]} Empty tuple. */ @@ -172,14 +172,14 @@ module ZOwnablePK { /** * @description Throws if called by any account whose id hash `H(pk, nonce)` does not match * the stored owner commitment. - * Use this to restrict access of specific circuits to the owner. + * Use this to only allow the owner to call specific circuits. * * @circuitInfo k=???, rows=??? * * Requirements: * * - Contract is initialized. - * - Caller's id (`H(pk, nonce)`) when used in `_computeOwnerCommitment` must equal + * - Caller's id (`H(pk, nonce)`) when used in `_computeOwnerCommitment` equals * the stored `_ownerCommitment`, thus verifying themselves as the owner. * * @returns {[]} Empty tuple. @@ -212,10 +212,11 @@ module ZOwnablePK { * * - `id`: See above. * - `instanceSalt`: A unique per-deployment salt, stored during initialization. - * This prevents commitment collisions across deployments. + * This prevents commitment collisions across deployments. * - `counter`: Incremented with each ownership transfer, ensuring uniqueness - * even with repeated `id` values. - * - `domain`: A domain separator to prevent hash collisions when extending the module. + * even with repeated `id` values. Cast to `Field` then `Bytes<32>` for hashing. + * - `domain`: Domain separator `"ZOwnablePK:shield:"` (padded to 32 bytes) to prevent + * hash collisions when extending the module or using similar commitment schemes. * * @circuitInfo k=???, rows=??? * @@ -253,13 +254,12 @@ module ZOwnablePK { * - `pk`: The public key of the caller. This is passed explicitly to allow * for off-chain derivation, testing, or scenarios where the caller is * different from the subject of the computation. - * - `nonce`: A secret nonce tied to the identity. This value should be - * randomly generated and kept private. It may be rotated periodically - * for enhanced unlinkability. + * - `nonce`: A secret nonce tied to the identity. The generation strategy is + * left to the user, offering different security/convenience trade-offs. * * The result is a 32-byte commitment that uniquely identifies the owner. - * This value is later used in owner commitment hashing, and acts as a privacy-preserving - * alternative to a raw public key. + * This value is later used in owner commitment hashing, + * and acts as a privacy-preserving alternative to a raw public key. * * @notice This module allows ownership to be tied to an identity commitment derived * from a public key and secret nonce. From 9d78542fb483729570ac95d43bb26e5ee929ec41 Mon Sep 17 00:00:00 2001 From: andrew Date: Sun, 17 Aug 2025 21:45:22 -0300 Subject: [PATCH 120/322] add ZOwnablePK api --- docs/modules/ROOT/pages/api/access.adoc | 200 ++++++++++++++++++++++++ 1 file changed, 200 insertions(+) diff --git a/docs/modules/ROOT/pages/api/access.adoc b/docs/modules/ROOT/pages/api/access.adoc index 7237e42b..7a8c1e39 100644 --- a/docs/modules/ROOT/pages/api/access.adoc +++ b/docs/modules/ROOT/pages/api/access.adoc @@ -399,3 +399,203 @@ Requirements: Constraints: - k=10, rows=216 + +[.contract] +[[ZOwnablePK]] +=== `++ZOwnablePK++` link:https://github.com/OpenZeppelin/compact-contracts/blob/main/contracts/ownable/src/ZOwnablePK.compact[{github-icon},role=heading-link] + +[.hljs-theme-dark] +```ts +import "./node_modules/@openzeppelin-compact/contracts/src/access/ZOwnablePK"; +``` + +`ZOwnablePK` provides a privacy-preserving access control mechanism for contracts with a single administrative user. Unlike traditional `Ownable` implementations that store or expose the owner's public key on-chain, +this module stores only a commitment to a hashed identifier derived from the owner's public key and a secret nonce. + +Ownable provides a basic access control mechanism where an account (an owner) can be granted exclusive access to specific circuits. + +This module includes <> to restrict a circuit to be used only by the owner. + +TIP: For an overview of the module, read the {ownable-guide}. + +[.contract-index] +.Circuits +-- + +[.sub-index#ZOwnablePKModule] +* xref:#ZOwnablePK-initialize[`++initialize(ownerId, instanceSalt)++`] +* xref:#ZOwnablePK-owner[`++owner()++`] +* xref:#ZOwnablePK-transferOwnership[`++transferOwnership(newOwnerId)++`] +* xref:#ZOwnablePK-renounceOwnership[`++renounceOwnership()++`] +* xref:#ZOwnablePK-assertOnlyOwner[`++assertOnlyOwner(operator, approved)++`] +* xref:#ZOwnablePK-_computeOwnerCommitment[`++_computeOwnerCommitment(id, counter)++`] +* xref:#ZOwnablePK-_computeOwnerId[`++_computeOwnerId(pk, nonce)++`] +* xref:#ZOwnablePK-_transferOwnership[`++_transferOwnership(newOwnerId)++`] +-- + +[.contract-item] +[[ZOwnablePK-initialize]] +==== `[.contract-item-name]#++initialize++#++(initialOwner: Either) → []++` [.item-kind]#circuit# + +Initializes the contract by setting the initial owner via `ownerId` +and storing the `instanceSalt` that acts as a privacy additive +for preventing duplicate commitments among other contracts implementing ZOwnablePK. + +NOTE: The `ownerId` must be calculated prior to contract deployment. +See <> + +Requirements: + +- Contract is not already initialized. +- `ownerId` is not an empty array. + +Constraints: + +- k=???, rows=??? + +[.contract-item] +[[ZOwnablePK-owner]] +==== `[.contract-item-name]#++owner++#++() → Bytes<32>++` [.item-kind]#circuit# + +Returns the current commitment representing the contract owner. +The full commitment is: `H(H(pk, nonce), instanceSalt, counter, domain)`. + +Requirements: + +- Contract is initialized. + +Constraints: + +- k=???, rows=??? + +[.contract-item] +[[ZOwnablePK-transferOwnership]] +==== `[.contract-item-name]#++transferOwnership++#++(newOwnerId: Bytes<32>) → []++` [.item-kind]#circuit# + +Transfers ownership of the contract to `newOwnerId`. +`newOwnerId` must be precalculated and given to the current owner off chain. + +Requirements: + +- Contract is initialized. +- Caller is the current contract owner. +- `newOwnerId` is not an empty array. + +Constraints: + +- k=???, rows=??? + +[.contract-item] +[[ZOwnablePK-renounceOwnership]] +==== `[.contract-item-name]#++renounceOwnership++#++() → []++` [.item-kind]#circuit# + +Leaves the contract without an owner. +It will not be possible to call <> circuits anymore. +Can only be called by the current owner. + +Requirements: + +- Contract is initialized. +- Caller is the current owner. + +Constraints: + +- k=???, rows=??? + +[.contract-item] +[[ZOwnablePK-assertOnlyOwner]] +==== `[.contract-item-name]#++assertOnlyOwner++#++() → []++` [.item-kind]#circuit# + +Throws if called by any account whose id hash `H(pk, nonce)` does not match the stored owner commitment. +Use this to only allow the owner to call specific circuits. + +Requirements: + +- Contract is initialized. +- Caller's id (`H(pk, nonce)`) when used in <> equals the stored `_ownerCommitment`, +thus verifying themselves as the owner. + +Constraints: + +- k=???, rows=??? + +[.contract-item] +[[ZOwnablePK-_computeOwnerCommitment]] +==== `[.contract-item-name]#++_computeOwnerCommitment++#++(id: Bytes<32>, counter: Uint<64>) → Bytes<32>++` [.item-kind]#circuit# + +Computes the owner commitment from the given `id` and `counter`. + +**Owner ID (`id`)** + +The `id` is expected to be computed off-chain as: `id = H(pk, nonce)` + +- `pk`: The owner's public key. +- `nonce`: A secret nonce scoped to the instance, ideally rotated with each transfer. + +**Commitment Derivation** + +`commitment = H(id, instanceSalt, counter, domain)` + +- `id`: See above. +- `instanceSalt`: A unique per-deployment salt, stored during initialization. +This prevents commitment collisions across deployments. +- `counter`: Incremented with each ownership transfer, ensuring uniqueness even with repeated `id` values. +Cast to `Field` then `Bytes<32>` for hashing. +- `domain`: Domain separator `"ZOwnablePK:shield:"` (padded to 32 bytes) to prevent hash collisions +when extending the module or using similar commitment schemes. + +Requirements: + +- Contract is initialized. + +Constraints: + +- k=???, rows=??? + +[.contract-item] +[[ZOwnablePK-_computeOwnerId]] +==== `[.contract-item-name]#++_computeOwnerId++#++(pk: Either, nonce: Bytes<32>) → Bytes<32>++` [.item-kind]#circuit# + +Computes the unique identifier (`id`) of the owner from their public key and a secret nonce. + +**ID Derivation** +`id = H(pk, nonce)` + +- `pk`: The public key of the caller. +This is passed explicitly to allow for off-chain derivation, testing, or scenarios +where the caller is different from the subject of the computation. +- `nonce`: A secret nonce tied to the identity. +This value should be randomly generated and kept private. +It may be rotated periodically for enhanced unlinkability. + +The result is a 32-byte commitment that uniquely identifies the owner. +This value is later used in owner commitment hashing, +and acts as a privacy-preserving alternative to a raw public key. + +NOTE: This module allows ownership to be tied to an identity commitment derived from a public key and secret nonce. +While typically used with user public keys, +this mechanism may also support contract addresses as identifiers in future contract-to-contract interactions. +Both are treated as 32-byte values (`Bytes<32>`). + +Requirements: + +- Contract is initialized. +- `newOwner` is not a ContractAddress. + +Constraints: + +- k=???, rows=??? + +[.contract-item] +[[ZOwnablePK-_transferOwnership]] +==== `[.contract-item-name]#++_transferOwnership++#++(newOwnerId: Bytes<32>) → []++` [.item-kind]#circuit# + +Transfers ownership to owner id `newOwnerId` without enforcing permission checks on the caller. + +Requirements: + +- Contract is initialized. + +Constraints: + +- k=???, rows=??? From 96f95c920f1c24f0d38a9f0139f7f963d209eaa2 Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 18 Aug 2025 01:10:16 -0300 Subject: [PATCH 121/322] add withNonce to PS --- .../access/witnesses/ZOwnablePKWitnesses.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/contracts/src/access/witnesses/ZOwnablePKWitnesses.ts b/contracts/src/access/witnesses/ZOwnablePKWitnesses.ts index f034951b..58098d02 100644 --- a/contracts/src/access/witnesses/ZOwnablePKWitnesses.ts +++ b/contracts/src/access/witnesses/ZOwnablePKWitnesses.ts @@ -34,6 +34,24 @@ export const ZOwnablePKPrivateState = { generate: (): ZOwnablePKPrivateState => { return { secretNonce: getRandomValues(Buffer.alloc(32)) }; }, + + /** + * @description Generates a new private state with a user-defined secret nonce. + * Useful for deterministic nonce generation or advanced use cases. + * + * @param nonce - The 32-byte secret nonce to use. + * @returns A fresh ZOwnablePKPrivateState instance with the provided nonce. + * + * @example + * ```typescript + * // For deterministic nonces (user-defined scheme) + * const deterministicNonce = myDeterministicScheme(...); + * const privateState = ZOwnablePKPrivateState.withNonce(deterministicNonce); + * ``` + */ + withNonce: (nonce: Buffer): ZOwnablePKPrivateState => { + return { secretNonce: nonce }; + }, }; /** From c16cd7db0e0ab57d943120eb9077c59d3d5da69f Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 18 Aug 2025 01:10:57 -0300 Subject: [PATCH 122/322] add ZOwnablePK docs (less setup) --- docs/modules/ROOT/pages/access.adoc | 125 ++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/docs/modules/ROOT/pages/access.adoc b/docs/modules/ROOT/pages/access.adoc index f39581d5..a104d887 100644 --- a/docs/modules/ROOT/pages/access.adoc +++ b/docs/modules/ROOT/pages/access.adoc @@ -133,6 +133,131 @@ there is no direct way for a contract to call circuits of other contracts or tra NOTE: The unsafe circuits are planned to become deprecated once contract-to-contract calls become available. +== Shielded Ownership and `ZOwnablePK` + +Privacy-preserving access control is a fundamental building block for confidential smart contracts on Midnight. +While traditional ownership patterns expose the owner's identity on-chain, +many applications require administrative control without revealing who holds that authority. + +=== Privacy-First Ownership + +The most common approach to access control in traditional smart contracts is ownership: +there's an account that is the owner of a contract and can perform administrative tasks. +However, this approach reveals the owner's identity to all observers, creating privacy and security risks. +In privacy-sensitive applications—such as confidential voting systems, private treasuries, or anonymous governance—revealing the administrator's identity may compromise the entire system's confidentiality. +This library provides the `ZOwnablePK` module that implements shielded ownership—administrative control without identity disclosure. +The owner's public key is never revealed on-chain; instead, +the contract stores only a cryptographic commitment that proves ownership without exposing the underlying identity. + +=== Commitment Scheme + +The `ZOwnablePK` module employs a two-layer cryptographic commitment scheme designed to provide privacy, +unlinkability, and collision resistance across deployments and ownership transfers. + +==== Owner ID Computation + +The foundation of the system is the owner identifier, computed as: + +```ts +id = H(pk, nonce) +``` + +Where `pk` is the owner's public key and `nonce` is a secret value that may be either randomly generated +for maximum privacy or deterministically derived for recoverability. +This identifier serves as a privacy-preserving alternative to exposing the raw public key, +ensuring the owner's identity remains confidential. + +==== Owner Commitment Computation + +The final ownership commitment stored on-chain is computed as: + +```ts +commitment = H(id, instanceSalt, counter, pad(32, "ZOwnablePK:shield:")) +``` + +This multi-element hash provides several security properties: + +- `id`: The privacy-preserving owner identifier described above. +- `instanceSalt`: A unique per-deployment salt that prevents commitment collisions across different contract instances, even when the same owner and nonce are used. +- `counter`: Incremented with each ownership transfer to ensure unlinkability—each transfer produces a completely different commitment even with the same underlying owner. +- `pad(32, "ZOwnablePK:shield:")`: A domain separator padded to 32 bytes that prevents hash collisions with other commitment schemes and enables safe protocol extensions. + +==== Security Properties + +This commitment scheme ensures that: + +- Owner identities remain completely private—public keys are never revealed on-chain. +- Ownership transfers are unlinkable—observers cannot correlate past and future ownership. +- Cross-contract attacks are prevented through instance-specific salting. + +=== Nonce Generation Strategies + +The choice of nonce generation strategy represents a fundamental trade-off between simplicity/security and recoverability. +Both approaches are valid, and the best choice depends on your specific threat model and operational requirements. + +==== Random Nonce + +Generating a cryptographically random nonce provides the strongest privacy guarantees: + +```typescript +const randomNonce = crypto.getRandomValues(new Uint8Array(32)); +const ownerId = ZOwnablePK._computeOwnerId(publicKey, randomNonce); +``` + +This approach is easy to generate and ensures maximum unlinkability—even with sophisticated analysis, +observers cannot correlate ownership across different contracts or time periods. +However, it requires secure backup of both the private key and the nonce. +*Loss of either component results in permanent, irrecoverable loss of ownership.* + +==== Deterministic Nonce + +Deriving the nonce deterministically enables recovery through derivation schemes. +Some examples: + +- `H(passphrase + context)` - recoverable from passphrase only, but passphrase becomes critical single point of failure. +- `H(publicKey + userPassphrase + context)` - requires both public key and passphrase. +- `H(signature + context)` where `signature = sign(context)` - leverages wallet without exposing private key. + +*Context-Dependent Derivations:* + +- Include contract address, deployment timestamp, user ID, etc. +- Trade-off: more context is more unique but harder to recreate. + +WARNING: Approaches that avoid private key exposure (public key + passphrase, signature-based) +are generally recommended for operational security. + +Deriving the nonce deterministically from the public key and user passphrase provides a balance of security and recoverability: + +```typescript +// Example: Scrypt-based derivation +import { scryptSync } from 'node:crypto'; + +const deterministicNonce = scryptSync( + userPassphrase + publicKey + ":ZOwnablePK:nonce:v1", + 32, + { N: 16384, r: 8, p: 1 } // Standard scrypt parameters +); +const recoverableOwnerId = ZOwnablePK._computeOwnerId(publicKey, deterministicNonce); +``` + +**Security Considerations** + +The `ZOwnablePK` module remains agnostic to nonce generation methods, placing the security/convenience decision entirely with the user. Key considerations include: + +- **Backup requirements**: Random nonces require additional secure storage. +- **Recovery scenarios**: Deterministic nonces enable recovery. +- **Cross-contract correlation**: Reusing nonce strategies may reduce privacy across deployments. +- **Rotation costs**: Changing nonces requires ownership transfer transactions with associated DUST costs. + +Users should carefully evaluate their threat model, operational requirements, +and privacy needs when selecting a nonce generation strategy, +as this choice cannot be easily changed without transferring ownership. + +=== Setup + +Add meee! + == Role-Based Access Control While the simplicity of _ownership_ can be useful for simple systems or quick prototyping, different levels of authorization are often needed. From 3586158a01a2212a45d534c93a51e6336e6773d9 Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 18 Aug 2025 01:14:38 -0300 Subject: [PATCH 123/322] improve sec prop section --- docs/modules/ROOT/pages/access.adoc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/modules/ROOT/pages/access.adoc b/docs/modules/ROOT/pages/access.adoc index a104d887..d7f180f1 100644 --- a/docs/modules/ROOT/pages/access.adoc +++ b/docs/modules/ROOT/pages/access.adoc @@ -186,9 +186,9 @@ This multi-element hash provides several security properties: This commitment scheme ensures that: -- Owner identities remain completely private—public keys are never revealed on-chain. -- Ownership transfers are unlinkable—observers cannot correlate past and future ownership. -- Cross-contract attacks are prevented through instance-specific salting. +- Public keys are never revealed on-chain. +- Observers cannot correlate past and future ownership. +- Cross-contract collisions are prevented through instance-specific salting. === Nonce Generation Strategies From 33148b06ba7766e7e86ab9f164983e657402ded0 Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 18 Aug 2025 01:17:40 -0300 Subject: [PATCH 124/322] fix fmt --- contracts/src/access/ZOwnablePK.compact | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/src/access/ZOwnablePK.compact b/contracts/src/access/ZOwnablePK.compact index 65225663..3de3e4f1 100644 --- a/contracts/src/access/ZOwnablePK.compact +++ b/contracts/src/access/ZOwnablePK.compact @@ -252,10 +252,10 @@ module ZOwnablePK { * `id = H(pk, nonce)` * * - `pk`: The public key of the caller. This is passed explicitly to allow - * for off-chain derivation, testing, or scenarios where the caller is - * different from the subject of the computation. + * for off-chain derivation, testing, or scenarios where the caller is + * different from the subject of the computation. * - `nonce`: A secret nonce tied to the identity. The generation strategy is - * left to the user, offering different security/convenience trade-offs. + * left to the user, offering different security/convenience trade-offs. * * The result is a 32-byte commitment that uniquely identifies the owner. * This value is later used in owner commitment hashing, From 71ad93fba0d089c6eb7ebd3ec45498073a3e3b58 Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 18 Aug 2025 02:47:31 -0300 Subject: [PATCH 125/322] add usage section --- docs/modules/ROOT/pages/access.adoc | 81 ++++++++++++++++++++++++++++- 1 file changed, 79 insertions(+), 2 deletions(-) diff --git a/docs/modules/ROOT/pages/access.adoc b/docs/modules/ROOT/pages/access.adoc index d7f180f1..7ebb095c 100644 --- a/docs/modules/ROOT/pages/access.adoc +++ b/docs/modules/ROOT/pages/access.adoc @@ -254,9 +254,86 @@ Users should carefully evaluate their threat model, operational requirements, and privacy needs when selecting a nonce generation strategy, as this choice cannot be easily changed without transferring ownership. -=== Setup +=== Usage + +Import the `ZOwnablePK` module into the implementing contract and expose the ownership-handling circuits. +It’s recommended to prefix the module with `ZOwnablePK_` to avoid circuit signature clashes. + +```typescript +// MyZOwnablePKContract.compact + +pragma language_version >= 0.16.0; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin-compact/contracts/src/access/ZOwnablePK" + prefix ZOwnablePK_; + +constructor( + initOwnerCommitment: Bytes<32>, + instanceSalt: Bytes<32>, +) { + ZOwnablePK_initialize(initOwnerCommitment, instanceSalt); +} + +export circuit owner(): Bytes<32> { + return ZOwnablePK_owner(); +} + +export circuit transferOwnership(newOwnerCommitment: Bytes<32>): [] { + return ZOwnablePK_transferOwnership(disclose(newOwnerCommitment)); +} + +export circuit renounceOwnership(): [] { + return ZOwnablePK_renounceOwnership(); +} +``` + +Similar to the Ownable module, +circuits can be protected so that only the contract owner may them by adding `assertOnlyOwner` +as the first line in the circuit body like this: + +```typescript +export circuit mySensitiveCircuit(): [] { + ZOwnablePK_assertOnlyOwner(); + + // Do something +} +``` + +This covers the basic for creating a contract, but before deploying the contract, +the owner's id must be derived for the commitment scheme because it's required to deploy the contract. + +First, the owner needs to generate a secret nonce that's stored in the owner's private state. +See <>. + +Once the owner has the secret nonce generated, they can insert their public key and nonce into the following: + +```typescript +import { + CompactTypeBytes, + CompactTypeVector, + persistentHash, +} from '@midnight-ntwrk/compact-runtime'; +import { getRandomValues } from 'node:crypto'; + +// Owner ID +const generateId = ( + pk: Uint8Array, + nonce: Uint8Array, +): Uint8Array => { + const rt_type = new CompactTypeVector(2, new CompactTypeBytes(32)); + return persistentHash(rt_type, [pk, nonce]); +}; + +// Instance salt for the constructor +const generateInstanceSalt = (): Uint8Array => { + return getRandomValues(new Uint8Array(32)); +} +``` -Add meee! +TIP: Another way to get the user ID is to expose `_computeOwnerId` in the contract +and call this circuit off chain through a contract simulator. +Be on the lookout for future tooling that makes this process easier. == Role-Based Access Control From d707d2837d2ad247495bc68d5f00d0308170870e Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 18 Aug 2025 02:54:47 -0300 Subject: [PATCH 126/322] revert changes --- contracts/src/utils/Utils.compact | 46 ------------------- .../src/utils/test/mocks/MockUtils.compact | 16 ------- .../utils/test/simulators/UtilsSimulator.ts | 28 ----------- contracts/src/utils/test/utils.test.ts | 29 ------------ 4 files changed, 119 deletions(-) diff --git a/contracts/src/utils/Utils.compact b/contracts/src/utils/Utils.compact index b7291176..3528e10e 100644 --- a/contracts/src/utils/Utils.compact +++ b/contracts/src/utils/Utils.compact @@ -66,52 +66,6 @@ module Utils { return !keyOrAddress.is_left; } - /** - * @description Wraps a value as the `L` variant of a disjoint union (`Either`), - * following Compact conventions. - * - * Sets `is_left` to true, assigns the provided value to `left`, and sets `right` to `default` per convention. - * - * This helper is useful when you already have a value of type `L`, but the circuit interface expects an `Either`. - * It avoids manually constructing the full struct and ensures consistent formatting. - * - * @template L - Type of the Left variant. - * @template R - Type of the Right variant. - * - * @param {L} left - The value to wrap as the Left variant. - * @returns {Either} A disjoint union (`Either`) with the value in the `L` variant. - */ - export pure circuit wrapAsEitherLeft(left: L): Either { - return Either { - is_left: true, - left: left, - right: default - }; - } - - /** - * @description Wraps a value as the `R` variant of a disjoint union (`Either`), - * following Compact conventions. - * - * Sets `is_left` to false, assigns the provided value to `right`, and sets `left` to `default` per convention. - * - * This helper is useful when you already have a value of type `R`, but the circuit interface expects an `Either`. - * It avoids manually constructing the full struct and ensures consistent formatting. - * - * @template L - Type of the Left variant. - * @template R - Type of the Right variant. - * - * @param {R} right - The value to wrap as the `R` variant. - * @returns {Either} A disjoint union (`Either`) with the value in the `R` variant. - */ - export pure circuit wrapAsEitherRight(right: R): Either { - return Either { - is_left: false, - left: default, - right: right - }; - } - /** * @description A helper function that returns the empty string: "". * diff --git a/contracts/src/utils/test/mocks/MockUtils.compact b/contracts/src/utils/test/mocks/MockUtils.compact index c3b66d64..54fd45f3 100644 --- a/contracts/src/utils/test/mocks/MockUtils.compact +++ b/contracts/src/utils/test/mocks/MockUtils.compact @@ -25,22 +25,6 @@ export pure circuit isContractAddress(keyOrAddress: Either { - return Utils_wrapAsEitherLeft(pk); -} - -// Find a better way to test different combinations -// other than creating a circuit for each pair -export pure circuit wrapAsEitherPkOrAddressRight( - address: ContractAddress, -): Either { - return Utils_wrapAsEitherRight(address); -} - export pure circuit emptyString(): Opaque<"string"> { return Utils_emptyString(); } diff --git a/contracts/src/utils/test/simulators/UtilsSimulator.ts b/contracts/src/utils/test/simulators/UtilsSimulator.ts index 5f46223e..715569f8 100644 --- a/contracts/src/utils/test/simulators/UtilsSimulator.ts +++ b/contracts/src/utils/test/simulators/UtilsSimulator.ts @@ -139,34 +139,6 @@ export class UtilsSimulator ).result; } - /** - * @description Returns `pk` wrapped in an `Either` type. - * @param pk The target value to wrap. - * @returns `Either` with `pk` in the left position. - */ - public wrapAsEitherPkOrAddressLeft( - pk: ZswapCoinPublicKey, - ): Either { - return this.contract.circuits.wrapAsEitherPkOrAddressLeft( - this.circuitContext, - pk, - ).result; - } - - /** - * @description Returns `address` wrapped in an `Either` type. - * @param pk The target value to wrap. - * @returns `Either` with `address` in the right position. - */ - public wrapAsEitherPkOrAddressRight( - address: ContractAddress, - ): Either { - return this.contract.circuits.wrapAsEitherPkOrAddressRight( - this.circuitContext, - address, - ).result; - } - /** * @description A helper function that returns the empty string: "" * @returns The empty string: "" diff --git a/contracts/src/utils/test/utils.test.ts b/contracts/src/utils/test/utils.test.ts index 52b78acc..1398d4d9 100644 --- a/contracts/src/utils/test/utils.test.ts +++ b/contracts/src/utils/test/utils.test.ts @@ -1,9 +1,4 @@ import { describe, expect, it } from 'vitest'; -import type { - ContractAddress, - Either, - ZswapCoinPublicKey, -} from './../../../artifacts/MockUtils/contract/index.cjs'; // Combined imports import { UtilsSimulator } from './simulators/UtilsSimulator.js'; import * as contractUtils from './utils/address.js'; @@ -92,30 +87,6 @@ describe('Utils', () => { }); }); - describe('wrapAsEitherLeft', () => { - it('should wrap pk as left', () => { - const pk = contractUtils.encodeToPK('PK'); - const exp: Either = { - is_left: true, - left: pk, - right: { bytes: new Uint8Array(32).fill(0) }, - }; - expect(contract.wrapAsEitherPkOrAddressLeft(pk)).toEqual(exp); - }); - }); - - describe('wrapAsEitherRight', () => { - it('should wrap address as right', () => { - const address = contractUtils.encodeToPK('ADDRESS'); - const exp: Either = { - is_left: false, - left: { bytes: new Uint8Array(32).fill(0) }, - right: address, - }; - expect(contract.wrapAsEitherPkOrAddressRight(address)).toEqual(exp); - }); - }); - describe('emptyString', () => { it('should return the empty string', () => { expect(contract.emptyString()).toBe(EMPTY_STRING); From e018e039b7e4b4a1f192f7cd5eaacc776f2a2933 Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 18 Aug 2025 03:00:13 -0300 Subject: [PATCH 127/322] fix typo --- contracts/src/access/ZOwnablePK.compact | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/src/access/ZOwnablePK.compact b/contracts/src/access/ZOwnablePK.compact index 3de3e4f1..38f1e233 100644 --- a/contracts/src/access/ZOwnablePK.compact +++ b/contracts/src/access/ZOwnablePK.compact @@ -14,7 +14,7 @@ pragma language_version >= 0.16.0; * * @notice This module explicitly supports commitments derived from public keys; * however, it may be possible to use contract addresses when contract-to-contract - * calls become available. This will be revisited when it is know if/how witnesses + * calls become available. This will be revisited when it's known if/how witnesses * are used from a contract address context. * * @dev Features: From d11380220b2e38b1266629bc7a3e7c35e0194dba Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 18 Aug 2025 03:05:44 -0300 Subject: [PATCH 128/322] update readme with targeted compilation --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0e730124..bc8d31ab 100644 --- a/README.md +++ b/README.md @@ -88,10 +88,10 @@ Cached: 0 cached, 2 total Time: 7.178s ``` -**Note:** Speed up the development process by skipping the prover and verifier key file generation: +**Note:** Speed up the development process by targeting a single directory and skipping the prover and verifier key file generation: ```bash -turbo compact -- --skip-zk +turbo compact:token -- --skip-zk ``` ### Run tests From b3a0bd4cb713a81f4181da6405fc01e6b90819e1 Mon Sep 17 00:00:00 2001 From: andrew Date: Sun, 24 Aug 2025 15:29:30 -0300 Subject: [PATCH 129/322] add k and rows --- contracts/src/access/ZOwnablePK.compact | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/contracts/src/access/ZOwnablePK.compact b/contracts/src/access/ZOwnablePK.compact index 38f1e233..f4c302c1 100644 --- a/contracts/src/access/ZOwnablePK.compact +++ b/contracts/src/access/ZOwnablePK.compact @@ -88,7 +88,7 @@ module ZOwnablePK { * * @dev The `ownerId` must be calculated prior to contract deployment. * - * @circuitInfo k=???, rows=??? + * @circuitInfo k=14, rows=14933 * * Requirements: * @@ -112,7 +112,7 @@ module ZOwnablePK { * @description Returns the current commitment representing the contract owner. * The full commitment is: `H(H(pk, nonce), instanceSalt, counter, domain)`. * - * @circuitInfo k=???, rows=??? + * @circuitInfo k=10, rows=57 * * Requirements: * @@ -129,7 +129,7 @@ module ZOwnablePK { * @description Transfers ownership to `newOwnerId`. * `newOwnerId` must be precalculated and given to the current owner off chain. * - * @circuitInfo k=???, rows=??? + * @circuitInfo k=16, rows=39240 * * Requirements: * @@ -153,7 +153,7 @@ module ZOwnablePK { * It will not be possible to call `assertOnlyOnwer` circuits anymore. * Can only be called by the current owner. * - * @circuitInfo k=???, rows=??? + * @circuitInfo k=24442, rows=24442 * * Requirements: * @@ -174,7 +174,7 @@ module ZOwnablePK { * the stored owner commitment. * Use this to only allow the owner to call specific circuits. * - * @circuitInfo k=???, rows=??? + * @circuitInfo k=15, rows=24437 * * Requirements: * @@ -218,7 +218,7 @@ module ZOwnablePK { * - `domain`: Domain separator `"ZOwnablePK:shield:"` (padded to 32 bytes) to prevent * hash collisions when extending the module or using similar commitment schemes. * - * @circuitInfo k=???, rows=??? + * @circuitInfo k=14, rows=14853 * * Requirements: * @@ -267,8 +267,6 @@ module ZOwnablePK { * support contract addresses as identifiers in future contract-to-contract * interactions. Both are treated as 32-byte values (`Bytes<32>`). * - * @circuitInfo k=???, rows=??? - * * Requirements: * * - `pk` is not a ContractAddress. @@ -290,7 +288,7 @@ module ZOwnablePK { * @description Transfers ownership to owner id `newOwnerId` without * enforcing permission checks on the caller. * - * @circuitInfo k=???, rows=??? + * @circuitInfo k=14, rows=14823 * * Requirements: * From f3e2872fdd040af0ca0802dd72d2a51b9640bfc5 Mon Sep 17 00:00:00 2001 From: andrew Date: Sun, 24 Aug 2025 15:32:36 -0300 Subject: [PATCH 130/322] add constraints to docs --- contracts/src/access/ZOwnablePK.compact | 2 +- docs/modules/ROOT/pages/api/access.adoc | 18 +++++++----------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/contracts/src/access/ZOwnablePK.compact b/contracts/src/access/ZOwnablePK.compact index f4c302c1..4ec23a05 100644 --- a/contracts/src/access/ZOwnablePK.compact +++ b/contracts/src/access/ZOwnablePK.compact @@ -153,7 +153,7 @@ module ZOwnablePK { * It will not be possible to call `assertOnlyOnwer` circuits anymore. * Can only be called by the current owner. * - * @circuitInfo k=24442, rows=24442 + * @circuitInfo k=15, rows=24442 * * Requirements: * diff --git a/docs/modules/ROOT/pages/api/access.adoc b/docs/modules/ROOT/pages/api/access.adoc index 7a8c1e39..8ed48e6e 100644 --- a/docs/modules/ROOT/pages/api/access.adoc +++ b/docs/modules/ROOT/pages/api/access.adoc @@ -451,7 +451,7 @@ Requirements: Constraints: -- k=???, rows=??? +- k=14, rows=14933 [.contract-item] [[ZOwnablePK-owner]] @@ -466,7 +466,7 @@ Requirements: Constraints: -- k=???, rows=??? +- k=10, rows=57 [.contract-item] [[ZOwnablePK-transferOwnership]] @@ -483,7 +483,7 @@ Requirements: Constraints: -- k=???, rows=??? +- k=16, rows=39240 [.contract-item] [[ZOwnablePK-renounceOwnership]] @@ -500,7 +500,7 @@ Requirements: Constraints: -- k=???, rows=??? +- k=15, rows=24442 [.contract-item] [[ZOwnablePK-assertOnlyOwner]] @@ -517,7 +517,7 @@ thus verifying themselves as the owner. Constraints: -- k=???, rows=??? +- k=15, rows=24437 [.contract-item] [[ZOwnablePK-_computeOwnerCommitment]] @@ -550,7 +550,7 @@ Requirements: Constraints: -- k=???, rows=??? +- k=14, rows=14853 [.contract-item] [[ZOwnablePK-_computeOwnerId]] @@ -582,10 +582,6 @@ Requirements: - Contract is initialized. - `newOwner` is not a ContractAddress. -Constraints: - -- k=???, rows=??? - [.contract-item] [[ZOwnablePK-_transferOwnership]] ==== `[.contract-item-name]#++_transferOwnership++#++(newOwnerId: Bytes<32>) → []++` [.item-kind]#circuit# @@ -598,4 +594,4 @@ Requirements: Constraints: -- k=???, rows=??? +- k=14, rows=14823 From 7b57e9330905fc676b487a046808d4410207e827 Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 25 Aug 2025 16:49:09 -0300 Subject: [PATCH 131/322] add SKIP_ZK env var to compile --- compact/src/runCompiler.ts | 49 ++++++++++++++++++++++++++++++++------ turbo.json | 12 +++++----- 2 files changed, 48 insertions(+), 13 deletions(-) diff --git a/compact/src/runCompiler.ts b/compact/src/runCompiler.ts index a1e9e002..79fef57b 100644 --- a/compact/src/runCompiler.ts +++ b/compact/src/runCompiler.ts @@ -8,24 +8,50 @@ import { CompactCompiler } from './Compiler.js'; * Executes the Compact compiler CLI. * Compiles `.compact` files using the `CompactCompiler` class with provided flags. * - * @example + * Supports both CLI flags and environment variables for common development flags. + * Environment variables take precedence and are useful when using with Turbo monorepo tasks. + * + * @example CLI usage with flags * ```bash * npx compact-compiler --skip-zk * ``` * - * @example Compile specific directory + * @example Compile specific directory with CLI flags + * ```bash + * npx compact-compiler --dir access --skip-zk + * ``` + * + * @example Environment variable usage (recommended with Turbo) + * ```bash + * SKIP_ZK=true npx compact-compiler --dir access + * ``` + * + * @example Turbo monorepo usage * ```bash - * npx compact-compiler --dir security --skip-zk + * # Compile specific module with skip-zk for development + * SKIP_ZK=true turbo compact:access + * + * # Full build with skip-zk + * SKIP_ZK=true turbo compact + * + * # Normal compilation without flags + * turbo compact:access * ``` * + * Environment Variables: + * - `SKIP_ZK=true`: Adds --skip-zk flag to compilation (skips zero-knowledge proof generation for faster development builds) + * * Expected output: * ``` * ℹ [COMPILE] Compact compiler started * ℹ [COMPILE] COMPACT_HOME: /path/to/compactc * ℹ [COMPILE] COMPACTC_PATH: /path/to/compactc/compactc - * ℹ [COMPILE] TARGET_DIR: security - * ℹ [COMPILE] Found 1 .compact file(s) to compile in security/ - * ✔ [COMPILE] [1/1] Compiled security/AccessControl.compact + * ℹ [COMPILE] TARGET_DIR: accesss:compact:access: + * ℹ [COMPILE] Found 4 .compact file(s) to compile in access/ + * ✔ [COMPILE] [1/4] Compiled access/AccessControl.compact + * ✔ [COMPILE] [2/4] Compiled access/Ownable.compact + * ✔ [COMPILE] [3/4] Compiled access/test/mocks/MockAccessControl.compact + * ✔ [COMPILE] [4/4] Compiled access/test/mocks/MockOwnable.compact * Compactc version: 0.24.0 * ``` */ @@ -39,6 +65,12 @@ async function runCompiler(): Promise { let targetDir: string | undefined; const compilerFlags: string[] = []; + // Handle common development flags via environment variables + // This is especially useful when using with Turbo monorepo tasks + if (process.env.SKIP_ZK === 'true') { + compilerFlags.push('--skip-zk'); + } + for (let i = 0; i < args.length; i++) { if (args[i] === '--dir') { if (i + 1 < args.length && !args[i + 1].startsWith('--')) { @@ -54,7 +86,10 @@ async function runCompiler(): Promise { ), ); console.log( - chalk.yellow('Example: compact-compiler --dir security --skip-zk'), + chalk.yellow('Example: compact-compiler --dir access --skip-zk'), + ); + console.log( + chalk.yellow('Example: SKIP_ZK=true compact-compiler --dir access'), ); process.exit(1); } diff --git a/turbo.json b/turbo.json index 86592d3e..57e1bbb5 100644 --- a/turbo.json +++ b/turbo.json @@ -3,35 +3,35 @@ "tasks": { "compact:security": { "dependsOn": ["^build"], - "env": ["COMPACT_HOME"], + "env": ["COMPACT_HOME", "SKIP_ZK"], "inputs": ["src/security/**/*.compact"], "outputLogs": "new-only", "outputs": ["artifacts/**/"] }, "compact:utils": { "dependsOn": ["^build"], - "env": ["COMPACT_HOME"], + "env": ["COMPACT_HOME", "SKIP_ZK"], "inputs": ["src/utils/**/*.compact"], "outputLogs": "new-only", "outputs": ["artifacts/**/"] }, "compact:access": { "dependsOn": ["^build", "compact:security", "compact:utils"], - "env": ["COMPACT_HOME"], + "env": ["COMPACT_HOME", "SKIP_ZK"], "inputs": ["src/access/**/*.compact", "artifacts/**"], "outputLogs": "new-only", "outputs": ["artifacts/**/"] }, "compact:archive": { "dependsOn": ["^build", "compact:utils"], - "env": ["COMPACT_HOME"], + "env": ["COMPACT_HOME", "SKIP_ZK"], "inputs": ["src/archive/**/*.compact", "artifacts/**"], "outputLogs": "new-only", "outputs": ["artifacts/**/"] }, "compact:token": { "dependsOn": ["^build", "compact:security", "compact:utils"], - "env": ["COMPACT_HOME"], + "env": ["COMPACT_HOME", "SKIP_ZK"], "inputs": ["src/token/**/*.compact", "artifacts/**"], "outputLogs": "new-only", "outputs": ["artifacts/**/"] @@ -44,7 +44,7 @@ "compact:archive", "compact:token" ], - "env": ["COMPACT_HOME"], + "env": ["COMPACT_HOME", "SKIP_ZK"], "inputs": ["src/**/*.compact", "test/**/*.compact"], "outputLogs": "new-only", "outputs": ["artifacts/**"] From 822fcd6cbd47def8e2b09bde9dbe695be9f864f2 Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 25 Aug 2025 16:54:52 -0300 Subject: [PATCH 132/322] improve docs --- compact/src/runCompiler.ts | 37 ++++++++++++++++--------------------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/compact/src/runCompiler.ts b/compact/src/runCompiler.ts index 79fef57b..e5a0ecf6 100644 --- a/compact/src/runCompiler.ts +++ b/compact/src/runCompiler.ts @@ -8,38 +8,33 @@ import { CompactCompiler } from './Compiler.js'; * Executes the Compact compiler CLI. * Compiles `.compact` files using the `CompactCompiler` class with provided flags. * - * Supports both CLI flags and environment variables for common development flags. - * Environment variables take precedence and are useful when using with Turbo monorepo tasks. + * For individual module compilation, CLI flags work directly. + * For full compilation with dependencies, use environment variables due to Turbo task orchestration. * - * @example CLI usage with flags + * @example Individual module compilation (CLI flags work directly) * ```bash - * npx compact-compiler --skip-zk + * npx compact-compiler --dir security --skip-zk + * turbo compact:access -- --skip-zk + * turbo compact:security -- --skip-zk --other-flag * ``` * - * @example Compile specific directory with CLI flags + * @example Full compilation (environment variables required) * ```bash - * npx compact-compiler --dir access --skip-zk - * ``` + * # Use environment variables for full builds due to task dependencies + * SKIP_ZK=true turbo compact * - * @example Environment variable usage (recommended with Turbo) - * ```bash - * SKIP_ZK=true npx compact-compiler --dir access + * # Normal full build + * turbo compact * ``` * - * @example Turbo monorepo usage + * @example Direct CLI usage * ```bash - * # Compile specific module with skip-zk for development - * SKIP_ZK=true turbo compact:access - * - * # Full build with skip-zk - * SKIP_ZK=true turbo compact - * - * # Normal compilation without flags - * turbo compact:access + * npx compact-compiler --skip-zk + * npx compact-compiler --dir security --skip-zk * ``` * - * Environment Variables: - * - `SKIP_ZK=true`: Adds --skip-zk flag to compilation (skips zero-knowledge proof generation for faster development builds) + * Environment Variables (only needed for full builds): + * - `SKIP_ZK=true`: Adds --skip-zk flag when running full compilation via `turbo compact` * * Expected output: * ``` From 1c36f67e7e20560f244258d081e78e43aa19b856 Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 25 Aug 2025 17:04:17 -0300 Subject: [PATCH 133/322] update README --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index bc8d31ab..71c1cec8 100644 --- a/README.md +++ b/README.md @@ -88,10 +88,15 @@ Cached: 0 cached, 2 total Time: 7.178s ``` -**Note:** Speed up the development process by targeting a single directory and skipping the prover and verifier key file generation: +Speed up the development process by targeting a single directory +and skipping the prover and verifier key file generation: ```bash +# Individual module compilation (recommended for development) turbo compact:token -- --skip-zk + +# Full compilation with skip-zk (use environment variable) +SKIP_ZK=true turbo compact ``` ### Run tests From c676fd02cf59f67c91a7aaeaaffc8bac96ae7301 Mon Sep 17 00:00:00 2001 From: Andrew Fleming Date: Mon, 25 Aug 2025 16:27:09 -0500 Subject: [PATCH 134/322] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: ⟣ €₥ℵ∪ℓ ⟢ <34749913+emnul@users.noreply.github.com> Signed-off-by: Andrew Fleming --- contracts/src/access/ZOwnablePK.compact | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/contracts/src/access/ZOwnablePK.compact b/contracts/src/access/ZOwnablePK.compact index 4ec23a05..6bf43273 100644 --- a/contracts/src/access/ZOwnablePK.compact +++ b/contracts/src/access/ZOwnablePK.compact @@ -27,10 +27,11 @@ pragma language_version >= 0.16.0; * @dev Commitment structure: * ``` * id = H(pk, secretNonce) - * commitment = H(id, instanceSalt, counter, "ZOwnablePK:shield:") + * commitment = H256(id, instanceSalt, counter, "ZOwnablePK:shield:") * ``` - * The commitment changes on each transfer due to the incrementing `counter`, - * providing unlinkability across ownership changes. + * Where `H()` is the SHA256 hash algorithm. The commitment changes + * on each transfer due to the incrementing `counter`, providing + * unlinkability across ownership changes. * * @dev Security Considerations: * - The `secretNonce` must be kept private. Loss of the nonce prevents the @@ -86,14 +87,15 @@ module ZOwnablePK { * and storing the `instanceSalt` that acts as a privacy additive for preventing * duplicate commitments among other contracts implementing ZOwnablePK. * - * @dev The `ownerId` must be calculated prior to contract deployment. + * @warning The `ownerId` must be calculated prior to contract deployment using the SHA256 hashing algorithm. + * Using any other algorithm will result in a permanent loss of contract access. * * @circuitInfo k=14, rows=14933 * * Requirements: * * - Contract is not initialized. - * - `ownerId` is not an empty array. + * - `ownerId` is not zero. * * @param {Bytes<32>} ownerId - The owner's unique identifier H(pk, nonce). * @param {Bytes<32>} instanceSalt - Contract salt to prevent duplicate commitments if From 0cd3c594646971d3f32b97077fb6603160583fbc Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 25 Aug 2025 18:43:49 -0300 Subject: [PATCH 135/322] change generic H to SHA256 in docs --- contracts/src/access/ZOwnablePK.compact | 35 ++++++++++++------------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/contracts/src/access/ZOwnablePK.compact b/contracts/src/access/ZOwnablePK.compact index 6bf43273..9f750dff 100644 --- a/contracts/src/access/ZOwnablePK.compact +++ b/contracts/src/access/ZOwnablePK.compact @@ -26,12 +26,11 @@ pragma language_version >= 0.16.0; * * @dev Commitment structure: * ``` - * id = H(pk, secretNonce) - * commitment = H256(id, instanceSalt, counter, "ZOwnablePK:shield:") + * id = SHA256(pk, secretNonce) + * commitment = SHA256(id, instanceSalt, counter, "ZOwnablePK:shield:") * ``` - * Where `H()` is the SHA256 hash algorithm. The commitment changes - * on each transfer due to the incrementing `counter`, providing - * unlinkability across ownership changes. + * The commitment changes on each transfer due to the incrementing `counter`, + * providing unlinkability across ownership changes. * * @dev Security Considerations: * - The `secretNonce` must be kept private. Loss of the nonce prevents the @@ -49,7 +48,7 @@ module ZOwnablePK { /** * @ledger _ownerCommitment * @description Stores the current hashed commitment representing the owner. - * This commitment is derived from the public identifier (e.g., `H(pk, nonce)`), + * This commitment is derived from the public identifier (e.g., `SHA256(pk, nonce)`), * the `instanceSalt`, the transfer `counter`, and a domain separator. * * A commitment of `default>` (i.e., zero) indicates the contract is unowned. @@ -77,7 +76,7 @@ module ZOwnablePK { * @witness secretNonce * @description A private per-user nonce used in deriving the shielded owner identifier. * - * Combined with the user's public key as `H(pk, nonce)` to produce an obfuscated, + * Combined with the user's public key as `SHA256(pk, nonce)` to produce an obfuscated, * unlinkable identity commitment. Users are encouraged to rotate this value on ownership changes. */ export witness secretNonce(): Bytes<32>; @@ -88,7 +87,7 @@ module ZOwnablePK { * duplicate commitments among other contracts implementing ZOwnablePK. * * @warning The `ownerId` must be calculated prior to contract deployment using the SHA256 hashing algorithm. - * Using any other algorithm will result in a permanent loss of contract access. + * Using any other algorithm will result in a permanent loss of contract access. * * @circuitInfo k=14, rows=14933 * @@ -97,7 +96,7 @@ module ZOwnablePK { * - Contract is not initialized. * - `ownerId` is not zero. * - * @param {Bytes<32>} ownerId - The owner's unique identifier H(pk, nonce). + * @param {Bytes<32>} ownerId - The owner's unique identifier SHA256(pk, nonce). * @param {Bytes<32>} instanceSalt - Contract salt to prevent duplicate commitments if * users reuse their PK and secretNonce witness (not recommended). * @returns {[]} Empty tuple. @@ -112,7 +111,7 @@ module ZOwnablePK { /** * @description Returns the current commitment representing the contract owner. - * The full commitment is: `H(H(pk, nonce), instanceSalt, counter, domain)`. + * The full commitment is: `SHA256(SHA256(pk, nonce), instanceSalt, counter, domain)`. * * @circuitInfo k=10, rows=57 * @@ -139,7 +138,7 @@ module ZOwnablePK { * - Caller is the the current owner. * - `newOwnerId` is not an empty array. * - * @param {Bytes<32>} newOwnerId - The new owner's unique identifier (`H(pk, nonce)`). + * @param {Bytes<32>} newOwnerId - The new owner's unique identifier (`SHA256(pk, nonce)`). * @returns {[]} Empty tuple. */ export circuit transferOwnership(newOwnerId: Bytes<32>): [] { @@ -172,7 +171,7 @@ module ZOwnablePK { } /** - * @description Throws if called by any account whose id hash `H(pk, nonce)` does not match + * @description Throws if called by any account whose id hash `SHA256(pk, nonce)` does not match * the stored owner commitment. * Use this to only allow the owner to call specific circuits. * @@ -181,7 +180,7 @@ module ZOwnablePK { * Requirements: * * - Contract is initialized. - * - Caller's id (`H(pk, nonce)`) when used in `_computeOwnerCommitment` equals + * - Caller's id (`SHA256(pk, nonce)`) when used in `_computeOwnerCommitment` equals * the stored `_ownerCommitment`, thus verifying themselves as the owner. * * @returns {[]} Empty tuple. @@ -204,13 +203,13 @@ module ZOwnablePK { * * ## Owner ID (`id`) * The `id` is expected to be computed off-chain as: - * `id = H(pk, nonce)` + * `id = SHA256(pk, nonce)` * * - `pk`: The owner's public key. * - `nonce`: A secret nonce scoped to the instance, ideally rotated with each transfer. * * ## Commitment Derivation - * `commitment = H(id, instanceSalt, counter, domain)` + * `commitment = SHA256(id, instanceSalt, counter, domain)` * * - `id`: See above. * - `instanceSalt`: A unique per-deployment salt, stored during initialization. @@ -226,7 +225,7 @@ module ZOwnablePK { * * - Contract is initialized. * - * @param {Bytes<32>} id - The unique identifier of the owner calculated by `H(pk, nonce)`. + * @param {Bytes<32>} id - The unique identifier of the owner calculated by `SHA256(pk, nonce)`. * @param {Uint<64>} counter - The current counter or round. This increments by `1` * after every transfer to prevent duplicate commitments given the same `id`. * @returns {Bytes<32>} The commitment derived from `id` and `counter`. @@ -251,7 +250,7 @@ module ZOwnablePK { * public key and a secret nonce. * * ## ID Derivation - * `id = H(pk, nonce)` + * `id = SHA256(pk, nonce)` * * - `pk`: The public key of the caller. This is passed explicitly to allow * for off-chain derivation, testing, or scenarios where the caller is @@ -297,7 +296,7 @@ module ZOwnablePK { * - Contract is initialized. * * @param {Bytes<32>} newOwnerId - The unique identifier of the new owner - * calculated by `H(pk, nonce)`. + * calculated by `SHA256(pk, nonce)`. * @returns {[]} Empty tuple. */ export circuit _transferOwnership(newOwnerId: Bytes<32>): [] { From 06f3c83e218737f512573d2e7be89410bd947be5 Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 25 Aug 2025 18:46:18 -0300 Subject: [PATCH 136/322] change generic H to SHA256 --- docs/modules/ROOT/pages/access.adoc | 4 ++-- docs/modules/ROOT/pages/api/access.adoc | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/modules/ROOT/pages/access.adoc b/docs/modules/ROOT/pages/access.adoc index 7ebb095c..44c1deeb 100644 --- a/docs/modules/ROOT/pages/access.adoc +++ b/docs/modules/ROOT/pages/access.adoc @@ -159,7 +159,7 @@ unlinkability, and collision resistance across deployments and ownership transfe The foundation of the system is the owner identifier, computed as: ```ts -id = H(pk, nonce) +id = SHA256(pk, nonce) ``` Where `pk` is the owner's public key and `nonce` is a secret value that may be either randomly generated @@ -172,7 +172,7 @@ ensuring the owner's identity remains confidential. The final ownership commitment stored on-chain is computed as: ```ts -commitment = H(id, instanceSalt, counter, pad(32, "ZOwnablePK:shield:")) +commitment = SHA256(id, instanceSalt, counter, pad(32, "ZOwnablePK:shield:")) ``` This multi-element hash provides several security properties: diff --git a/docs/modules/ROOT/pages/api/access.adoc b/docs/modules/ROOT/pages/api/access.adoc index 8ed48e6e..acc88d0b 100644 --- a/docs/modules/ROOT/pages/api/access.adoc +++ b/docs/modules/ROOT/pages/api/access.adoc @@ -458,7 +458,7 @@ Constraints: ==== `[.contract-item-name]#++owner++#++() → Bytes<32>++` [.item-kind]#circuit# Returns the current commitment representing the contract owner. -The full commitment is: `H(H(pk, nonce), instanceSalt, counter, domain)`. +The full commitment is: `SHA256(SHA256(pk, nonce), instanceSalt, counter, domain)`. Requirements: @@ -506,13 +506,13 @@ Constraints: [[ZOwnablePK-assertOnlyOwner]] ==== `[.contract-item-name]#++assertOnlyOwner++#++() → []++` [.item-kind]#circuit# -Throws if called by any account whose id hash `H(pk, nonce)` does not match the stored owner commitment. +Throws if called by any account whose id hash `SHA256(pk, nonce)` does not match the stored owner commitment. Use this to only allow the owner to call specific circuits. Requirements: - Contract is initialized. -- Caller's id (`H(pk, nonce)`) when used in <> equals the stored `_ownerCommitment`, +- Caller's id (`SHA256(pk, nonce)`) when used in <> equals the stored `_ownerCommitment`, thus verifying themselves as the owner. Constraints: @@ -527,14 +527,14 @@ Computes the owner commitment from the given `id` and `counter`. **Owner ID (`id`)** -The `id` is expected to be computed off-chain as: `id = H(pk, nonce)` +The `id` is expected to be computed off-chain as: `id = SHA256(pk, nonce)` - `pk`: The owner's public key. - `nonce`: A secret nonce scoped to the instance, ideally rotated with each transfer. **Commitment Derivation** -`commitment = H(id, instanceSalt, counter, domain)` +`commitment = SHA256(id, instanceSalt, counter, domain)` - `id`: See above. - `instanceSalt`: A unique per-deployment salt, stored during initialization. @@ -559,7 +559,7 @@ Constraints: Computes the unique identifier (`id`) of the owner from their public key and a secret nonce. **ID Derivation** -`id = H(pk, nonce)` +`id = SHA256(pk, nonce)` - `pk`: The public key of the caller. This is passed explicitly to allow for off-chain derivation, testing, or scenarios From 8e28bdae68d77838f1dfaaa67f5ed19bc84bb5ed Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 25 Aug 2025 18:47:01 -0300 Subject: [PATCH 137/322] fix lang version in mock --- contracts/src/access/test/mocks/MockZOwnablePK.compact | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/src/access/test/mocks/MockZOwnablePK.compact b/contracts/src/access/test/mocks/MockZOwnablePK.compact index ce9f7e0c..d769f30e 100644 --- a/contracts/src/access/test/mocks/MockZOwnablePK.compact +++ b/contracts/src/access/test/mocks/MockZOwnablePK.compact @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma language_version >= 0.15.0; +pragma language_version >= 0.16.0; import CompactStandardLibrary; import "../../ZOwnablePK" prefix ZOwnablePK_; From 9b3e97c844c74d5746e0c5c9e51814faa1c43c14 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 26 Aug 2025 01:37:48 -0300 Subject: [PATCH 138/322] add bad owner id hash scenario --- contracts/src/access/test/ZOwnablePK.test.ts | 34 +++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/contracts/src/access/test/ZOwnablePK.test.ts b/contracts/src/access/test/ZOwnablePK.test.ts index b8103142..0369705a 100644 --- a/contracts/src/access/test/ZOwnablePK.test.ts +++ b/contracts/src/access/test/ZOwnablePK.test.ts @@ -3,6 +3,8 @@ import { CompactTypeVector, convert_bigint_to_Uint8Array, persistentHash, + transientHash, + upgradeFromTransient, } from '@midnight-ntwrk/compact-runtime'; import { beforeEach, describe, expect, it } from 'vitest'; import type { ZswapCoinPublicKey } from '../../../artifacts/MockOwnable/contract/index.cjs'; @@ -99,7 +101,7 @@ describe('ZOwnablePK', () => { }); }); - describe('when not initialized correctly', () => { + describe('when not deployed and not initialized', () => { const isNotInit = false; beforeEach(() => { @@ -136,6 +138,36 @@ describe('ZOwnablePK', () => { }); }); + describe('when incorrect hashing algo (not SHA256) is used to generate initial owner id', () => { + // ZOwnablePK only supports sha256 for owner id calculation + // Obviously, using any other algo for the id will not work + const badHashAlgo = (pk: ZswapCoinPublicKey, nonce: Uint8Array) => { + const rt_type = new CompactTypeVector(2, new CompactTypeBytes(32)); + return upgradeFromTransient(transientHash(rt_type, [pk.bytes, nonce])); + }; + const secretNonce = ZOwnablePKPrivateState.generate().secretNonce; + const badOwnerId = badHashAlgo(Z_OWNER, secretNonce); + + beforeEach(() => { + ownable = new ZOwnablePKSimulator(badOwnerId, INSTANCE_SALT, isInit); + }); + // + type FailingCircuits = [method: keyof ZOwnablePKSimulator, args: unknown[]]; + const protectedCircuits: FailingCircuits[] = [ + ['assertOnlyOwner', []], + ['transferOwnership', [badOwnerId]], + ['renounceOwnership', []], + ]; + + it.each(protectedCircuits)('%s should fail', (circuitName, args) => { + ownable.callerCtx.setCaller(OWNER); + + expect(() => { + (ownable[circuitName] as (...args: unknown[]) => unknown)(...args); + }).toThrow('ZOwnablePK: caller is not the owner'); + }); + }); + describe('after initialization', () => { beforeEach(() => { // Create private state object and generate nonce From 490af83ab4d4117dd716c7ed20528d0c961b91c0 Mon Sep 17 00:00:00 2001 From: Andrew Fleming Date: Tue, 26 Aug 2025 13:42:02 -0500 Subject: [PATCH 139/322] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: ⟣ €₥ℵ∪ℓ ⟢ <34749913+emnul@users.noreply.github.com> Signed-off-by: Andrew Fleming --- contracts/src/access/test/types/test.ts | 4 ++-- contracts/src/access/test/utils/createCircuitProxies.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/src/access/test/types/test.ts b/contracts/src/access/test/types/test.ts index 48da9c81..643def10 100644 --- a/contracts/src/access/test/types/test.ts +++ b/contracts/src/access/test/types/test.ts @@ -50,8 +50,8 @@ export interface IContractSimulator { * @template TContract - Contract type with `circuits` and `impureCircuits`. */ export type ExtractPureCircuits = TContract extends { - circuits: infer TCircuits; - impureCircuits: infer TImpureCircuits; + circuits: infer TCircuits extends Record; + impureCircuits: infer TImpureCircuits extends Record; } ? Omit : never; diff --git a/contracts/src/access/test/utils/createCircuitProxies.ts b/contracts/src/access/test/utils/createCircuitProxies.ts index ad4211b6..334181d4 100644 --- a/contracts/src/access/test/utils/createCircuitProxies.ts +++ b/contracts/src/access/test/utils/createCircuitProxies.ts @@ -10,7 +10,7 @@ import type { */ export function createCircuitProxies< P, - ContractType extends { circuits: any; impureCircuits: any }, + ContractType extends { circuits: Record; impureCircuits: Record;}, >( contract: ContractType, getContext: () => CircuitContext

, From 018ba5ce34a46a1bfc9a4af7c34149698988044d Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 26 Aug 2025 15:48:24 -0300 Subject: [PATCH 140/322] improve create proxy constraints, cast circuits to the extracted type, fix fmt --- .../access/test/utils/createCircuitProxies.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/contracts/src/access/test/utils/createCircuitProxies.ts b/contracts/src/access/test/utils/createCircuitProxies.ts index 334181d4..8aa500da 100644 --- a/contracts/src/access/test/utils/createCircuitProxies.ts +++ b/contracts/src/access/test/utils/createCircuitProxies.ts @@ -10,17 +10,20 @@ import type { */ export function createCircuitProxies< P, - ContractType extends { circuits: Record; impureCircuits: Record;}, + ContractType extends { + circuits: Record; + impureCircuits: Record; + }, >( contract: ContractType, getContext: () => CircuitContext

, getCallerContext: () => CircuitContext

, updateContext: (ctx: CircuitContext

) => void, - createPureProxy: ( + createPureProxy: >( circuits: C, context: () => CircuitContext

, ) => ContextlessCircuits, - createImpureProxy: ( + createImpureProxy: >( circuits: C, context: () => CircuitContext

, updateContext: (ctx: CircuitContext

) => void, @@ -36,11 +39,14 @@ export function createCircuitProxies< return { get circuits() { if (!pureProxy) { - pureProxy = createPureProxy(contract.circuits, getContext); + pureProxy = createPureProxy( + contract.circuits as ExtractPureCircuits, + getContext, + ); } if (!impureProxy) { impureProxy = createImpureProxy( - contract.impureCircuits, + contract.impureCircuits as ExtractImpureCircuits, getCallerContext, updateContext, ); From 12a06c7f7075791f4b0e9b695c7c843df5a6a6d3 Mon Sep 17 00:00:00 2001 From: Andrew Fleming Date: Tue, 26 Aug 2025 14:14:41 -0500 Subject: [PATCH 141/322] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: ⟣ €₥ℵ∪ℓ ⟢ <34749913+emnul@users.noreply.github.com> Signed-off-by: Andrew Fleming --- docs/modules/ROOT/pages/access.adoc | 4 ++-- docs/modules/ROOT/pages/api/access.adoc | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/modules/ROOT/pages/access.adoc b/docs/modules/ROOT/pages/access.adoc index 44c1deeb..e8c1cf47 100644 --- a/docs/modules/ROOT/pages/access.adoc +++ b/docs/modules/ROOT/pages/access.adoc @@ -197,7 +197,7 @@ Both approaches are valid, and the best choice depends on your specific threat m ==== Random Nonce -Generating a cryptographically random nonce provides the strongest privacy guarantees: +Generating a cryptographically strong random nonce provides the strongest privacy guarantees: ```typescript const randomNonce = crypto.getRandomValues(new Uint8Array(32)); @@ -300,7 +300,7 @@ export circuit mySensitiveCircuit(): [] { } ``` -This covers the basic for creating a contract, but before deploying the contract, +This covers the basics for creating a contract, but before deploying the contract, the owner's id must be derived for the commitment scheme because it's required to deploy the contract. First, the owner needs to generate a secret nonce that's stored in the owner's private state. diff --git a/docs/modules/ROOT/pages/api/access.adoc b/docs/modules/ROOT/pages/api/access.adoc index acc88d0b..5b3b95f6 100644 --- a/docs/modules/ROOT/pages/api/access.adoc +++ b/docs/modules/ROOT/pages/api/access.adoc @@ -427,7 +427,7 @@ TIP: For an overview of the module, read the {ownable-guide}. * xref:#ZOwnablePK-owner[`++owner()++`] * xref:#ZOwnablePK-transferOwnership[`++transferOwnership(newOwnerId)++`] * xref:#ZOwnablePK-renounceOwnership[`++renounceOwnership()++`] -* xref:#ZOwnablePK-assertOnlyOwner[`++assertOnlyOwner(operator, approved)++`] +* xref:#ZOwnablePK-assertOnlyOwner[`++assertOnlyOwner()++`] * xref:#ZOwnablePK-_computeOwnerCommitment[`++_computeOwnerCommitment(id, counter)++`] * xref:#ZOwnablePK-_computeOwnerId[`++_computeOwnerId(pk, nonce)++`] * xref:#ZOwnablePK-_transferOwnership[`++_transferOwnership(newOwnerId)++`] @@ -580,7 +580,7 @@ Both are treated as 32-byte values (`Bytes<32>`). Requirements: - Contract is initialized. -- `newOwner` is not a ContractAddress. +- `pk` is not a ContractAddress. [.contract-item] [[ZOwnablePK-_transferOwnership]] From 1b55a09829bdf08eda697fbf7f4740854f004260 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Tue, 26 Aug 2025 16:59:17 -0400 Subject: [PATCH 142/322] Remove old dir, rename files --- contracts/shieldedAccessControl/package.json | 32 -- .../mocks/MockShieldedAccessControl.compact | 73 ----- .../shieldedAccessControl/tsconfig.build.json | 5 - contracts/shieldedAccessControl/tsconfig.json | 25 -- .../shieldedAccessControl/vitest.config.ts | 10 - .../access}/ShieldedAccessControl.compact | 305 ++++++------------ .../ShieldedAccessControlUtils.compact | 0 .../mocks/MockShieldedAccessControl.compact | 64 ++++ .../ShieldedAccessControlSimulator.ts | 297 +++++++++++++++++ .../ShieldedAccessControlWitnesses.ts | 2 +- 10 files changed, 469 insertions(+), 344 deletions(-) delete mode 100644 contracts/shieldedAccessControl/package.json delete mode 100644 contracts/shieldedAccessControl/src/test/mocks/MockShieldedAccessControl.compact delete mode 100644 contracts/shieldedAccessControl/tsconfig.build.json delete mode 100644 contracts/shieldedAccessControl/tsconfig.json delete mode 100644 contracts/shieldedAccessControl/vitest.config.ts rename contracts/{shieldedAccessControl/src => src/access}/ShieldedAccessControl.compact (60%) rename contracts/{shieldedAccessControl/src => src/access}/ShieldedAccessControlUtils.compact (100%) create mode 100644 contracts/src/access/test/mocks/MockShieldedAccessControl.compact create mode 100644 contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts rename contracts/{shieldedAccessControl/src => src/access}/witnesses/ShieldedAccessControlWitnesses.ts (98%) diff --git a/contracts/shieldedAccessControl/package.json b/contracts/shieldedAccessControl/package.json deleted file mode 100644 index 65238178..00000000 --- a/contracts/shieldedAccessControl/package.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "name": "@openzeppelin-compact/shielded-access-control", - "private": true, - "type": "module", - "main": "dist/index.js", - "module": "dist/index.js", - "types": "./dist/index.d.ts", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "require": "./dist/index.js", - "import": "./dist/index.js", - "default": "./dist/index.js" - } - }, - "scripts": { - "compact": "compact-compiler", - "build": "compact-builder && tsc", - "test": "vitest run", - "types": "tsc -p tsconfig.json --noEmit", - "clean": "git clean -fXd" - }, - "dependencies": { - "@openzeppelin-compact/compact": "workspace:^" - }, - "devDependencies": { - "@types/node": "22.14.0", - "ts-node": "^10.9.2", - "typescript": "^5.2.2", - "vitest": "^3.1.3" - } -} \ No newline at end of file diff --git a/contracts/shieldedAccessControl/src/test/mocks/MockShieldedAccessControl.compact b/contracts/shieldedAccessControl/src/test/mocks/MockShieldedAccessControl.compact deleted file mode 100644 index 91a6e39f..00000000 --- a/contracts/shieldedAccessControl/src/test/mocks/MockShieldedAccessControl.compact +++ /dev/null @@ -1,73 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma language_version >= 0.16.0; - -import CompactStandardLibrary; - -import "../../ShieldedAccessControl" prefix ShieldedAccessControl_; - -export { - ZswapCoinPublicKey, - ContractAddress, - Either, - Maybe, - ShieldedAccessControl_DEFAULT_ADMIN_ROLE, - ShieldedAccessControl__salt, - ShieldedAccessControl__operatorRoles -}; - -export circuit hasRole(roleId: Bytes<32>, account: Either, nonce: Bytes<32>): Boolean { - return ShieldedAccessControl_hasRole(roleId, account, nonce); -} - -export circuit assertOnlyRole(roleId: Bytes<32>, nonce: Bytes<32>): [] { - ShieldedAccessControl_assertOnlyRole(roleId, nonce); -} - -export circuit _checkRole(roleId: Bytes<32>, account: Either, nonce: Bytes<32>): [] { - ShieldedAccessControl__checkRole(roleId, account, nonce); -} - -export circuit _checkMerkleTree(roleId: Bytes<32>, account: Bytes<32>, nonce: Bytes<32>): Boolean { - return ShieldedAccessControl__checkMerkleTree(roleId, account, nonce); -} - -export circuit getRoleAdmin(roleId: Bytes<32>): Bytes<32> { - return ShieldedAccessControl_getRoleAdmin(roleId); -} - -export circuit grantRole(roleId: Bytes<32>, account: Either, nonce: Bytes<32>): [] { - ShieldedAccessControl_grantRole(roleId, account, nonce); -} - -export circuit revokeRole(roleId: Bytes<32>, account: Either, nonce: Bytes<32>): [] { - ShieldedAccessControl_revokeRole(roleId, account, nonce); -} - -export circuit renounceRole(roleId: Bytes<32>, callerConfirmation: Either, nonce: Bytes<32>): [] { - ShieldedAccessControl_renounceRole(roleId, callerConfirmation, nonce); -} - -export circuit _setRoleAdmin(roleId: Bytes<32>, adminRole: Bytes<32>): [] { - ShieldedAccessControl__setRoleAdmin(roleId, adminRole); -} - -export circuit _grantRole(roleId: Bytes<32>, account: Either, nonce: Bytes<32>): Boolean { - return ShieldedAccessControl__grantRole(roleId, account, nonce); -} - -export circuit _unsafeGrantRole(roleId: Bytes<32>, account: Either, nonce: Bytes<32>): Boolean { - return ShieldedAccessControl__unsafeGrantRole(roleId, account, nonce); -} - -export circuit _revokeRole(roleId: Bytes<32>, account: Either, nonce: Bytes<32>): Boolean { - return ShieldedAccessControl__revokeRole(roleId, account, nonce); -} - -export circuit _requestRole(roleId: Bytes<32>): [] { - ShieldedAccessControl__requestRole(roleId); -} - -export circuit _recoverRoles(): [] { - ShieldedAccessControl__recoverRoles(); -} diff --git a/contracts/shieldedAccessControl/tsconfig.build.json b/contracts/shieldedAccessControl/tsconfig.build.json deleted file mode 100644 index f1132509..00000000 --- a/contracts/shieldedAccessControl/tsconfig.build.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extends": "./tsconfig.json", - "exclude": ["src/test/**/*.ts"], - "compilerOptions": {} -} diff --git a/contracts/shieldedAccessControl/tsconfig.json b/contracts/shieldedAccessControl/tsconfig.json deleted file mode 100644 index 4ae082c4..00000000 --- a/contracts/shieldedAccessControl/tsconfig.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "include": [ - "src/**/*.ts" - ], - "compilerOptions": { - "rootDir": "src", - "outDir": "dist", - "declaration": true, - "lib": [ - "ES2022" - ], - "target": "ES2022", - "module": "nodenext", - "moduleResolution": "nodenext", - "allowJs": true, - "forceConsistentCasingInFileNames": true, - "noImplicitAny": true, - "strict": true, - "isolatedModules": true, - "sourceMap": true, - "resolveJsonModule": true, - "esModuleInterop": true, - "skipLibCheck": true - } -} diff --git a/contracts/shieldedAccessControl/vitest.config.ts b/contracts/shieldedAccessControl/vitest.config.ts deleted file mode 100644 index 785b792e..00000000 --- a/contracts/shieldedAccessControl/vitest.config.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { defineConfig } from 'vitest/config'; - -export default defineConfig({ - test: { - globals: true, - environment: 'node', - include: ['src/test/**/*.test.ts'], - reporters: 'verbose', - }, -}); diff --git a/contracts/shieldedAccessControl/src/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact similarity index 60% rename from contracts/shieldedAccessControl/src/ShieldedAccessControl.compact rename to contracts/src/access/ShieldedAccessControl.compact index a36cd04e..fd21521c 100644 --- a/contracts/shieldedAccessControl/src/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -8,10 +8,10 @@ pragma language_version >= 0.16.0; * This module provides a shielded role-based access control mechanism, where roles can be used to * represent a set of permissions. Roles are stored as Merkle tree commitments to avoid * disclosing information about role holder. Role commitments are created with the following - * hashing scheme SHA256( SHA256(roleIdentifier | account | nonce | contractAddress) | index). + * hashing scheme SHA256(roleId | account | nonce | merkleTreeIndex). * * @notice Using the SHA256 hashing function comes at a significant performace cost. In the future, we - * plan on migrating to a ZK-friendly hashing function like Poseidon when an implementation is available. + * plan on migrating to a ZK-friendly hashing function when an implementation is available. * * Roles are referred to by their `Bytes<32>` identifier. These should be exposed * in the top-level contract and be unique. One way to achieve this is by @@ -51,12 +51,6 @@ pragma language_version >= 0.16.0; * grant and revoke this role. Extra precautions should be taken to secure * accounts that have been granted it. * - * By default, the salt value used to generate nonce values in the `requestRole` witness - * is set to 0. The use of a random salt value adds significantly to the strength of the - * underlying HKDF function and is highly encouraged. A random salt value can be set - * by implementing the `Initializable` module and setting `_salt` in the `initialize() - * circuit. - * * @notice Roles can only be granted to ZswapCoinPublicKeys * through the main role approval circuits (`grantRole` and `_grantRole`). * In other words, role approvals to contract addresses are disallowed through these @@ -84,8 +78,8 @@ module ShieldedAccessControl { import "ShieldedAccessControlUtils" prefix Utils_; /** - * @description A Merkle tree of role commitments stored as SHA256( SHA256(roleId | account | nonce | contractAddress) | index) - * @type {Bytes<32>} roleCommitment - A commitment created by the following hash: SHA256( SHA256(roleId | account | nonce | contractAddress) | index). + * @description A Merkle tree of role commitments stored as SHA256(roleId | account | nonce | merkleTreeIndex) + * @type {Bytes<32>} roleCommitment - A role commitment created by the following hash: SHA256(roleId | account | nonce | merkleTreeIndex). * @type {MerkleTree<10, roleCommitment>} * @type {MerkleTree<10, Bytes<32>>} _operatorRoles  */ @@ -102,70 +96,27 @@ module ShieldedAccessControl { /** * @description A set of nullifiers used to revoke the permissions of a role - * @type {Bytes<32> roleCommitment - A roleCommitment created by the following hash: SHA256( SHA256(roleId | account | nonce | contractAddress) | index). + * @type {Bytes<32> roleCommitment - A role commitment created by the following hash: SHA256(roleId | account | nonce | merkleTreeIndex). * @type {Set} _roleCommitmentNullifiers  */ export ledger _roleCommitmentNullifiers: Set>; - /** - * @description Mapping from an intermediate role commitment hash to an index in the `_operatorRoles` Merkle tree. - * @type {Bytes<32>} intermediateRoleCommitment - An intermediate role commitment hash created by the following hashing scheme: SHA256(roleId | account | nonce | contractAddress). - * @type {Uint<64>} index - The index of a role commitment in the `_operatorRoles` Merkle tree. - * @type {Map} - * @type {Map, Uint<64>>} _roleCommitmentIndex -  */ - export ledger _roleCommitmentIndex: Map, Uint<64>>; - - /** - * @description A counter tracking the next available index in the `_operatorRoles` MerkleTree - */ - export ledger _nextIndex: Counter; - - /** - * @description A random salt value used to strengthen the HKDF function used in the `requestRole` witness function. - */ - export ledger _salt: Bytes<32>; - export ledger DEFAULT_ADMIN_ROLE: Bytes<32>; /** * @description Returns a Merkle path in the `_operatorRoles` Merkle tree, given the knowledge that a `roleCommitment` is at the given index. * - * Requirements: - * - * - It is an error to call this if `roleCommitment` is not contained at the given index. - * - * @circuitInfo - * - * @param {Bytes<32>} roleCommitment - A commitment created by the following hash: SHA256( SHA256(roleId | account | nonce | contractAddress) | index). + * @param {Bytes<32>} roleCommitment - A commitment created by the following hash: SHA256(roleId | account | nonce | merkleTreeIndex). * @param {Uint<64>} index - An index in the `_operatorRoles` Merkle tree * @return {MerkleTreePath<10, Bytes<32>>} - The Merkle path of `roleCommitment` in the `_operatorRoles` Merkle tree  */ - witness getRoleCommitmentPath(roleCommitment: Bytes<32>, index: Uint<64>): MerkleTreePath<10, Bytes<32>>; + witness getRoleCommitmentPath(roleCommitment: Bytes<32>): MerkleTreePath<10, Bytes<32>>; - /** - * @description Locally creates and stores a nonce value using the HKDF function and the associated role identifier. - * - * @dev Developers must provide an implementation to privately send the account's public key, roleId, and nonce to an admin. One - * possible solution is by using an HTTP API. - * - * @param {Bytes<32>} roleId - A hash representing a role identifier. - * @param {Bytes<32>} account - The account requesting a role. - * @param {Bytes<32>} salt - A salt value for the underlying HKDF function. - * @return {[]} - Empty tuple. -  */ - witness requestRole(roleId: Bytes<32>, account: Bytes<32>, salt: Bytes<32>): []; + witness secretNonce(roleId: Bytes<32>): Bytes<32>; - /** - * @description Used to recover roles in the event of data loss. - * - * @dev Developers must export publicly declared roles from the top-level contract to generate possible roles for each. - * - * @param {Bytes<32>} account - The account requesting a role. - * @param {Bytes<32>} salt - A salt value for the underlying HKDF function. - * @return {[]} - Empty tuple. -  */ - witness recoverRoles(account: Bytes<32>, salt: Bytes<32>): []; + witness merkleTreeIndex(roleId: Bytes<32>): Uint<64>; + + witness getFirstFreeMerkleTreeIndex(): Uint<64>; /** * @description Returns `true` if `account` has been granted `roleId`. @@ -174,17 +125,17 @@ module ShieldedAccessControl { * * Requirements: * - * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce | contractAddress) + * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce) * must exist in the `_roleCommitmentIndex` map. - * - A nullifier for the role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index) + * - A nullifier for the role commitment produced by SHA256(roleId | account | nonce) * must not exist in the `_roleCommitmentNullifiers` set. - * - A path for the role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index) must + * - A path for the role commitment produced by SHA256(roleId | account | nonce) must * exist at `index` in the `_operatorRoles` Merkle tree. * * Disclosures: * - * - The intermediate role commitment produced by SHA256(roleId | account | nonce | contractAddress). - * - The role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index). + * - The intermediate role commitment produced by SHA256(roleId | account | nonce). + * - The role commitment produced by SHA256(roleId | account | nonce). * - The Merkle tree path for the role commitment stored at `index` in the `_operatorRoles` * Merkle tree. * - The type data of `account` - a ZswapCoinPublicKey or ContractAddress. @@ -194,14 +145,14 @@ module ShieldedAccessControl { * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK). * @return {Boolean} - A boolean determining if the account has the specified role.  */ - export circuit hasRole(roleId: Bytes<32>, account: Either, nonce: Bytes<32>): Boolean { + export circuit hasRole(roleId: Bytes<32>, account: Either): Boolean { if (!Utils_isContractAddress(account)) { const zswapPubKey = account.left.bytes; - return _checkMerkleTree(roleId, zswapPubKey, nonce); + return _checkMerkleTree(roleId, zswapPubKey); } const contractAddress = account.right.bytes; - return _checkMerkleTree(roleId, contractAddress, nonce); + return _checkMerkleTree(roleId, contractAddress); } /** @@ -211,18 +162,18 @@ module ShieldedAccessControl { * * Requirements: * - * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce | contractAddress) + * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce) * must exist in the `_roleCommitmentIndex` map. - * - A nullifier for the role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index) + * - A nullifier for the role commitment produced by SHA256(roleId | account | nonce) * must not exist in the `_roleCommitmentNullifiers` set. - * - A path for the role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index) must + * - A path for the role commitment produced by SHA256(roleId | account | nonce) must * exist at `index` in the `_operatorRoles` Merkle tree. * - The caller must not be a ContractAddress. * * Disclosures: * - * - The intermediate role commitment produced by SHA256(roleId | account | nonce | contractAddress). - * - The role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index). + * - The intermediate role commitment produced by SHA256(roleId | account | nonce). + * - The role commitment produced by SHA256(roleId | account | nonce). * - The Merkle tree path for the role commitment stored at `index` in the `_operatorRoles` * Merkle tree. * - The type data of `account` - a ZswapCoinPublicKey or ContractAddress. @@ -231,11 +182,10 @@ module ShieldedAccessControl { * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK) * @return {[]} - Empty tuple. */ - export circuit assertOnlyRole(roleId: Bytes<32>, nonce: Bytes<32>): [] { + export circuit assertOnlyRole(roleId: Bytes<32>): [] { _checkRole( roleId, - left(ownPublicKey()), - nonce + left(ownPublicKey()) ); } @@ -246,28 +196,27 @@ module ShieldedAccessControl { * * Requirements: * - * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce | contractAddress) + * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce) * must exist in the `_roleCommitmentIndex` map. - * - A nullifier for the role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index) + * - A nullifier for the role commitment produced by SHA256(roleId | account | nonce) * must not exist in the `_roleCommitmentNullifiers` set. - * - A path for the role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index) must + * - A path for the role commitment produced by SHA256(roleId | account | nonce) must * exist at `index` in the `_operatorRoles` Merkle tree. * * Disclosures: * - * - The intermediate role commitment produced by SHA256(roleId | account | nonce | contractAddress). - * - The role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index). + * - The intermediate role commitment produced by SHA256(roleId | account | nonce). + * - The role commitment produced by SHA256(roleId | account | nonce). * - The Merkle tree path for the role commitment stored at `index` in the `_operatorRoles` * Merkle tree. * - The type data of `account` - a ZswapCoinPublicKey or ContractAddress. * * @param {Bytes<32>} roleId - The role identifier. * @param {Either} account - The account to check. - * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK) * @return {[]} - Empty tuple. */ - export circuit _checkRole(roleId: Bytes<32>, account: Either, nonce: Bytes<32>): [] { - assert(hasRole(roleId, account, nonce), "ShieldedAccessControl: unauthorized account"); + export circuit _checkRole(roleId: Bytes<32>, account: Either): [] { + assert(hasRole(roleId, account), "ShieldedAccessControl: unauthorized account"); } /** @@ -277,17 +226,17 @@ module ShieldedAccessControl { * * Requirements: * - * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce | contractAddress) + * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce) * must exist in the `_roleCommitmentIndex` map. - * - A nullifier for the role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index) + * - A nullifier for the role commitment produced by SHA256(roleId | account | nonce) * must not exist in the `_roleCommitmentNullifiers` set. - * - A path for the role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index) must + * - A path for the role commitment produced by SHA256(roleId | account | nonce) must * exist at `index` in the `_operatorRoles` Merkle tree. * * Disclosures: * - * - The intermediate role commitment produced by SHA256(roleId | account | nonce | contractAddress). - * - The role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index). + * - The intermediate role commitment produced by SHA256(roleId | account | nonce). + * - The role commitment produced by SHA256(roleId | account | nonce). * - The Merkle tree path for the role commitment stored at `index` in the `_operatorRoles` * Merkle tree. * @@ -295,18 +244,15 @@ module ShieldedAccessControl { * @param {Bytes<32>} account - The account to check represented as a Bytes<32>. * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK) * @return {Boolean} - A boolean determining if a path for for the role commitment - * produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index) exists in the `_operatorRoles` Merkle tree + * produced by SHA256(roleId | account | nonce) exists in the `_operatorRoles` Merkle tree */ - export circuit _checkMerkleTree(roleId: Bytes<32>, account: Bytes<32>, nonce: Bytes<32>): Boolean { - const contractAddress = kernel.self().bytes; - const intermediateRoleCommitment = persistentHash>>([roleId, account, nonce, contractAddress]); - assert(_roleCommitmentIndex.member(disclose(intermediateRoleCommitment)), "ShieldedAccessControl: role commitment index not found"); - - const index = _roleCommitmentIndex.lookup(disclose(intermediateRoleCommitment)); - const finalRoleCommitment = persistentHash>>([intermediateRoleCommitment, index as Field as Bytes<32>]); - assert(!_roleCommitmentNullifiers.member(disclose(finalRoleCommitment)), "ShieldedAccessControl: role commitment access revoked"); + export circuit _checkMerkleTree(roleId: Bytes<32>, account: Bytes<32>): Boolean { + const nonce = secretNonce(roleId); + const index = merkleTreeIndex(roleId); + const roleCommitment = persistentHash>>([roleId, account, nonce, index as Field as Bytes<32>]); + assert(!_roleCommitmentNullifiers.member(disclose(roleCommitment)), "ShieldedAccessControl: role commitment access revoked"); - const authPath = getRoleCommitmentPath(finalRoleCommitment, index); + const authPath = getRoleCommitmentPath(roleCommitment); return _operatorRoles .checkRoot(merkleTreePathRoot<10, Bytes<32>>(disclose(authPath))); } @@ -337,17 +283,17 @@ module ShieldedAccessControl { * Requirements: * * - `account` must not be a ContractAddress. - * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce | contractAddress) + * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce) * must exist in the `_roleCommitmentIndex` map. - * - A nullifier for the role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index) + * - A nullifier for the role commitment produced by SHA256(roleId | account | nonce) * must not exist in the `_roleCommitmentNullifiers` set. - * - A path for the role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index) must + * - A path for the role commitment produced by SHA256(roleId | account | nonce) must * exist at `index` in the `_operatorRoles` Merkle tree. * * Disclosures: * - * - The intermediate role commitment produced by SHA256(roleId | account | nonce | contractAddress). - * - The role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index). + * - The intermediate role commitment produced by SHA256(roleId | account | nonce). + * - The role commitment produced by SHA256(roleId | account | nonce). * - The Merkle tree path for the role commitment stored at `index` in the `_operatorRoles` * Merkle tree. * - The type data of `account` - a ZswapCoinPublicKey or ContractAddress. @@ -357,9 +303,9 @@ module ShieldedAccessControl { * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK) * @return {[]} - Empty tuple. */ - export circuit grantRole(roleId: Bytes<32>, account: Either, nonce: Bytes<32>): [] { - assertOnlyRole(getRoleAdmin(roleId), nonce); - _grantRole(roleId, account, nonce); + export circuit grantRole(roleId: Bytes<32>, account: Either): [] { + assertOnlyRole(getRoleAdmin(roleId)); + _grantRole(roleId, account); } /** @@ -370,17 +316,17 @@ module ShieldedAccessControl { * Requirements: * * - `account` must not be a ContractAddress. - * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce | contractAddress) + * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce) * must exist in the `_roleCommitmentIndex` map. - * - A nullifier for the role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index) + * - A nullifier for the role commitment produced by SHA256(roleId | account | nonce) * must not exist in the `_roleCommitmentNullifiers` set. - * - A path for the role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index) must + * - A path for the role commitment produced by SHA256(roleId | account | nonce) must * exist at `index` in the `_operatorRoles` Merkle tree. * * Disclosures: * - * - The intermediate role commitment produced by SHA256(roleId | account | nonce | contractAddress). - * - The role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index). + * - The intermediate role commitment produced by SHA256(roleId | account | nonce). + * - The role commitment produced by SHA256(roleId | account | nonce). * - The Merkle tree path for the role commitment stored at `index` in the `_operatorRoles` * Merkle tree. * - The type data of `account` - a ZswapCoinPublicKey or ContractAddress. @@ -390,9 +336,9 @@ module ShieldedAccessControl { * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK) * @return {[]} - Empty tuple. */ - export circuit revokeRole(roleId: Bytes<32>, account: Either, nonce: Bytes<32>): [] { - assertOnlyRole(getRoleAdmin(roleId), nonce); - _revokeRole(roleId, account, nonce); + export circuit revokeRole(roleId: Bytes<32>, account: Either): [] { + assertOnlyRole(getRoleAdmin(roleId)); + _revokeRole(roleId, account); } /** @@ -408,17 +354,17 @@ module ShieldedAccessControl { * * - The caller must be `callerConfirmation`. * - The caller must not be a `ContractAddress`. - * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce | contractAddress) + * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce) * must exist in the `_roleCommitmentIndex` map. - * - A nullifier for the role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index) + * - A nullifier for the role commitment produced by SHA256(roleId | account | nonce) * must not exist in the `_roleCommitmentNullifiers` set. - * - A path for the role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index) must + * - A path for the role commitment produced by SHA256(roleId | account | nonce) must * exist at `index` in the `_operatorRoles` Merkle tree. * * Disclosures: * - * - The intermediate role commitment produced by SHA256(roleId | account | nonce | contractAddress). - * - The role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index). + * - The intermediate role commitment produced by SHA256(roleId | account | nonce). + * - The role commitment produced by SHA256(roleId | account | nonce). * - The Merkle tree path for the role commitment stored at `index` in the `_operatorRoles` * Merkle tree. * - The type data of `callerConfirmation` - a ZswapCoinPublicKey or ContractAddress. @@ -428,10 +374,10 @@ module ShieldedAccessControl { * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK) * @return {[]} - Empty tuple. */ - export circuit renounceRole(roleId: Bytes<32>, callerConfirmation: Either, nonce: Bytes<32>): [] { + export circuit renounceRole(roleId: Bytes<32>, callerConfirmation: Either): [] { assert(callerConfirmation == left(ownPublicKey()), "ShieldedAccessControl: bad confirmation"); - _revokeRole(roleId, callerConfirmation, nonce); + _revokeRole(roleId, callerConfirmation); } /** @@ -456,17 +402,17 @@ module ShieldedAccessControl { * Requirements: * * - `account` must not be a ContractAddress. - * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce | contractAddress) + * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce) * must exist in the `_roleCommitmentIndex` map. - * - A nullifier for the role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index) + * - A nullifier for the role commitment produced by SHA256(roleId | account | nonce) * must not exist in the `_roleCommitmentNullifiers` set. - * - A path for the role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index) must + * - A path for the role commitment produced by SHA256(roleId | account | nonce) must * exist at `index` in the `_operatorRoles` Merkle tree. * * Disclosures: * - * - The intermediate role commitment produced by SHA256(roleId | account | nonce | contractAddress). - * - The role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index). + * - The intermediate role commitment produced by SHA256(roleId | account | nonce). + * - The role commitment produced by SHA256(roleId | account | nonce). * - The Merkle tree path for the role commitment stored at `index` in the `_operatorRoles` * Merkle tree. * - The type data of `account` - a ZswapCoinPublicKey or ContractAddress. @@ -476,9 +422,9 @@ module ShieldedAccessControl { * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK) * @return {Boolean} roleGranted - A boolean indicating if `roleId` was granted. */ - export circuit _grantRole(roleId: Bytes<32>, account: Either, nonce: Bytes<32>): Boolean { + export circuit _grantRole(roleId: Bytes<32>, account: Either): Boolean { assert(!Utils_isContractAddress(account), "ShieldedAccessControl: unsafe role approval"); - return _unsafeGrantRole(roleId, account, nonce); + return _unsafeGrantRole(roleId, account); } /** @@ -492,17 +438,17 @@ module ShieldedAccessControl { * * Requirements: * - * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce | contractAddress) + * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce) * must exist in the `_roleCommitmentIndex` map. - * - A nullifier for the role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index) + * - A nullifier for the role commitment produced by SHA256(roleId | account | nonce) * must not exist in the `_roleCommitmentNullifiers` set. - * - A path for the role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index) must + * - A path for the role commitment produced by SHA256(roleId | account | nonce) must * exist at `index` in the `_operatorRoles` Merkle tree. * * Disclosures: * - * - The intermediate role commitment produced by SHA256(roleId | account | nonce | contractAddress). - * - The role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index). + * - The intermediate role commitment produced by SHA256(roleId | account | nonce). + * - The role commitment produced by SHA256(roleId | account | nonce). * - The Merkle tree path for the role commitment stored at `index` in the `_operatorRoles` * Merkle tree. * - The type data of `account` - a ZswapCoinPublicKey or ContractAddress. @@ -512,19 +458,19 @@ module ShieldedAccessControl { * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK) * @return {Boolean} roleGranted - A boolean indicating if `role` was granted. */ - export circuit _unsafeGrantRole(roleId: Bytes<32>, account: Either, nonce: Bytes<32>): Boolean { - if (hasRole(roleId, account, nonce)) { + export circuit _unsafeGrantRole(roleId: Bytes<32>, account: Either): Boolean { + if (hasRole(roleId, account)) { return false; } if (!Utils_isContractAddress(account)) { const zswapPubKey = account.left.bytes; - _addRoleCommitmentToLedger(roleId, zswapPubKey, nonce); + _addRoleCommitmentToLedger(roleId, zswapPubKey); return true; } const contractAddress = account.right.bytes; - _addRoleCommitmentToLedger(roleId, contractAddress, nonce); + _addRoleCommitmentToLedger(roleId, contractAddress); return true; } @@ -536,17 +482,17 @@ module ShieldedAccessControl { * * Requirements: * - * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce | contractAddress) + * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce) * must exist in the `_roleCommitmentIndex` map. - * - A nullifier for the role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index) + * - A nullifier for the role commitment produced by SHA256(roleId | account | nonce) * must not exist in the `_roleCommitmentNullifiers` set. - * - A path for the role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index) must + * - A path for the role commitment produced by SHA256(roleId | account | nonce) must * exist at `index` in the `_operatorRoles` Merkle tree. * * Disclosures: * - * - The intermediate role commitment produced by SHA256(roleId | account | nonce | contractAddress). - * - The role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index). + * - The intermediate role commitment produced by SHA256(roleId | account | nonce). + * - The role commitment produced by SHA256(roleId | account | nonce). * - The Merkle tree path for the role commitment stored at `index` in the `_operatorRoles` * Merkle tree. * - The type data of `account` - a ZswapCoinPublicKey or ContractAddress. @@ -556,19 +502,19 @@ module ShieldedAccessControl { * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK) * @return {Boolean} roleRevoked - A boolean indicating if `roleId` was revoked. */ - export circuit _revokeRole(roleId: Bytes<32>, account: Either, nonce: Bytes<32>): Boolean { - if (!hasRole(roleId, account, nonce)) { + export circuit _revokeRole(roleId: Bytes<32>, account: Either): Boolean { + if (!hasRole(roleId, account)) { return false; } if(!Utils_isContractAddress(account)) { const zswapPubKey = account.left.bytes; - _nullifyRoleCommitment(roleId, zswapPubKey, nonce); + _nullifyRoleCommitment(roleId, zswapPubKey); return true; } const contractAddress = account.right.bytes; - _nullifyRoleCommitment(roleId, contractAddress, nonce); + _nullifyRoleCommitment(roleId, contractAddress); return true; } @@ -581,22 +527,19 @@ module ShieldedAccessControl { * * Disclosures: * - * - The role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index). + * - The role commitment produced by SHA256(roleId | account | nonce). * * @param {Bytes<32>} roleId - The role identifier. * @param {Bytes<32>} account - The account to add represented as a Bytes<32>. * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK) * @return {Boolean} roleRevoked - A boolean indicating if `roleId` was revoked. */ - circuit _addRoleCommitmentToLedger(roleId: Bytes<32>, account: Bytes<32>, nonce: Bytes<32>): [] { - const contractAddress = kernel.self().bytes; - const intermediateRoleCommitment = persistentHash>>([roleId, account, nonce, contractAddress]); - const index = _nextIndex.read(); - const finalRoleCommitment = persistentHash>>([intermediateRoleCommitment, index as Field as Bytes<32>]); - - _operatorRoles.insertHashIndex(disclose(finalRoleCommitment), index); - _roleCommitmentIndex.insert(disclose(finalRoleCommitment), index); - _nextIndex.increment(1); + circuit _addRoleCommitmentToLedger(roleId: Bytes<32>, account: Bytes<32>): [] { + const nonce = secretNonce(roleId); + const index = getFirstFreeMerkleTreeIndex(); + const roleCommitment = persistentHash>>([roleId, account, nonce, index as Field as Byes<32>]); + + _operatorRoles.insertHashIndex(disclose(roleCommitment), index); } /** @@ -608,52 +551,18 @@ module ShieldedAccessControl { * * Disclosures: * - * - The role commitment produced by SHA256( SHA256(roleId | account | nonce | contractAddress) | index). - * - The intermediate role commitment produced by SHA256(roleId | account | nonce | contractAddress). + * - The role commitment produced by SHA256(roleId | account | nonce). + * - The intermediate role commitment produced by SHA256(roleId | account | nonce). * * @param {Bytes<32>} roleId - The role identifier. * @param {Bytes<32>} account - The account to add represented as a Bytes<32>. * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK) * @return {Boolean} roleRevoked - A boolean indicating if `roleId` was revoked. */ - circuit _nullifyRoleCommitment(roleId: Bytes<32>, account: Bytes<32>, nonce: Bytes<32>): [] { - const contractAddress = kernel.self().bytes; - const intermediateRoleCommitment = persistentHash>>([roleId, account, nonce, contractAddress]); - const index = _roleCommitmentIndex.lookup(disclose(intermediateRoleCommitment)); - const finalRoleCommitment = persistentHash>>([intermediateRoleCommitment, index as Field as Bytes<32>]); - _roleCommitmentNullifiers.insert(disclose(finalRoleCommitment)); - } - - /** - * @description A wrapper circuit for the `requestRole` witness. - * - * @circuitInfo k=10, rows=188 - * - * Requirements: - * - * - The caller must not be a ContractAddress. - * - * @param {Bytes<32>} roleId - A hash representing a role identifier. - * @return {[]} - Empty tuple. -  */ - export circuit _requestRole(roleId: Bytes<32>): [] { - const publicKey = left(ownPublicKey()); - requestRole(roleId, publicKey.left.bytes, _salt); - } - - /** - * @description A wrapper circuit for the `recoverRoles` witness. - * - * @circuitInfo k=10, rows=99 - * - * Requirements: - * - * - The caller must not be a ContractAddress. - * - * @return {[]} - Empty tuple. -  */ - export circuit _recoverRoles(): [] { - const publicKey = left(ownPublicKey()); - recoverRoles(publicKey.left.bytes, _salt); + circuit _nullifyRoleCommitment(roleId: Bytes<32>, account: Bytes<32>): [] { + const nonce = secretNonce(roleId); + const index = merkleTreeIndex(roleId); + const roleCommitment = persistentHash>>([roleId, account, nonce, index as Field as Bytes<32>]); + _roleCommitmentNullifiers.insert(disclose(roleCommitment)); } } diff --git a/contracts/shieldedAccessControl/src/ShieldedAccessControlUtils.compact b/contracts/src/access/ShieldedAccessControlUtils.compact similarity index 100% rename from contracts/shieldedAccessControl/src/ShieldedAccessControlUtils.compact rename to contracts/src/access/ShieldedAccessControlUtils.compact diff --git a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact new file mode 100644 index 00000000..ac009ca3 --- /dev/null +++ b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: MIT + +pragma language_version >= 0.16.0; + +import CompactStandardLibrary; + +import "../../ShieldedAccessControl" prefix ShieldedAccessControl_; + +export { + ZswapCoinPublicKey, + ContractAddress, + Either, + Maybe, + ShieldedAccessControl_DEFAULT_ADMIN_ROLE, + ShieldedAccessControl__operatorRoles +}; + +export circuit hasRole(roleId: Bytes<32>, account: Either): Boolean { + return ShieldedAccessControl_hasRole(roleId, account); +} + +export circuit assertOnlyRole(roleId: Bytes<32>): [] { + ShieldedAccessControl_assertOnlyRole(roleId); +} + +export circuit _checkRole(roleId: Bytes<32>, account: Either): [] { + ShieldedAccessControl__checkRole(roleId, account); +} + +export circuit _checkMerkleTree(roleId: Bytes<32>, account: Bytes<32>): Boolean { + return ShieldedAccessControl__checkMerkleTree(roleId, account); +} + +export circuit getRoleAdmin(roleId: Bytes<32>): Bytes<32> { + return ShieldedAccessControl_getRoleAdmin(roleId); +} + +export circuit grantRole(roleId: Bytes<32>, account: Either): [] { + ShieldedAccessControl_grantRole(roleId, account); +} + +export circuit revokeRole(roleId: Bytes<32>, account: Either): [] { + ShieldedAccessControl_revokeRole(roleId, account); +} + +export circuit renounceRole(roleId: Bytes<32>, callerConfirmation: Either): [] { + ShieldedAccessControl_renounceRole(roleId, callerConfirmation); +} + +export circuit _setRoleAdmin(roleId: Bytes<32>, adminRole: Bytes<32>): [] { + ShieldedAccessControl__setRoleAdmin(roleId, adminRole); +} + +export circuit _grantRole(roleId: Bytes<32>, account: Either): Boolean { + return ShieldedAccessControl__grantRole(roleId, account); +} + +export circuit _unsafeGrantRole(roleId: Bytes<32>, account: Either): Boolean { + return ShieldedAccessControl__unsafeGrantRole(roleId, account); +} + +export circuit _revokeRole(roleId: Bytes<32>, account: Either): Boolean { + return ShieldedAccessControl__revokeRole(roleId, account); +} \ No newline at end of file diff --git a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts new file mode 100644 index 00000000..b71d4cf4 --- /dev/null +++ b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts @@ -0,0 +1,297 @@ +import { + type CircuitContext, + type CoinPublicKey, + type ContractState, + QueryContext, + constructorContext, + emptyZswapLocalState, +} from '@midnight-ntwrk/compact-runtime'; +import { sampleContractAddress } from '@midnight-ntwrk/zswap'; +import { + type ContractAddress, + type Either, + type Ledger, + Contract as MockShieldedAccessControl, + type ZswapCoinPublicKey, + ledger, +} from '../../shieldedAccessControl/src/artifacts/MockShieldedAccessControl/contract/index.cjs'; // Combined imports +import { + type ShieldedAccessControlPrivateState, + ShieldedAccessControlWitnesses, +} from '../../witnesses/ShieldedAccessControlWitnesses.js'; +import type { IContractSimulator } from '../../shieldedAccessControl/src/test/types/test.js'; + +/** + * @description A simulator implementation of a AccessControl contract for testing purposes. + * @template P - The private state type, fixed to ShieldedAccessControlPrivateState. + * @template L - The ledger type, fixed to Contract.Ledger. + */ +export class AccessControlSimulator + implements IContractSimulator { + /** @description The underlying contract instance managing contract logic. */ + readonly contract: MockShieldedAccessControl; + + /** @description The deployed address of the contract. */ + readonly contractAddress: string; + + /** @description The current circuit context, updated by contract operations. */ + circuitContext: CircuitContext; + + /** + * @description Initializes the mock contract. + */ + constructor() { + this.contract = new MockShieldedAccessControl( + ShieldedAccessControlWitnesses, + ); + const { + currentPrivateState, + currentContractState, + currentZswapLocalState, + } = this.contract.initialState(constructorContext({}, '0'.repeat(64))); + this.circuitContext = { + currentPrivateState, + currentZswapLocalState, + originalState: currentContractState, + transactionContext: new QueryContext( + currentContractState.data, + sampleContractAddress(), + ), + }; + this.contractAddress = this.circuitContext.transactionContext.address; + } + + /** + * @description Retrieves the current public ledger state of the contract. + * @returns The ledger state as defined by the contract. + */ + public getCurrentPublicState(): Ledger { + return ledger(this.circuitContext.transactionContext.state); + } + + /** + * @description Retrieves the current private state of the contract. + * @returns The private state of type ShieldedAccessControlPrivateState. + */ + public getCurrentPrivateState(): ShieldedAccessControlPrivateState { + return this.circuitContext.currentPrivateState; + } + + /** + * @description Retrieves the current contract state. + * @returns The contract state object. + */ + public getCurrentContractState(): ContractState { + return this.circuitContext.originalState; + } + + /** + * @description Retrieves an account's permission for `roleId`. + * @param roleId - The role identifier. + * @param account - A ZswapCoinPublicKey or a ContractAddress. + * @returns Whether an account has a specified role. + */ + public hasRole( + roleId: Uint8Array, + account: Either, + ): boolean { + return this.contract.impureCircuits.hasRole( + this.circuitContext, + roleId, + account, + ).result; + } + + /** + * @description Retrieves an account's permission for `roleId`. + * @param caller - Optional. Sets the caller context if provided. + * @param roleId - The role identifier. + */ + public assertOnlyRole(roleId: Uint8Array, caller?: CoinPublicKey) { + const res = this.contract.impureCircuits.assertOnlyRole( + { + ...this.circuitContext, + currentZswapLocalState: caller + ? emptyZswapLocalState(caller) + : this.circuitContext.currentZswapLocalState, + }, + roleId, + ); + + this.circuitContext = res.context; + } + + /** + * @description Retrieves an account's permission for `roleId`. + * @param roleId - The role identifier. + * @param account - A ZswapCoinPublicKey or a ContractAddress. + */ + public _checkRole( + roleId: Uint8Array, + account: Either, + ) { + this.circuitContext = this.contract.impureCircuits._checkRole( + this.circuitContext, + roleId, + account, + ).context; + } + + /** + * @description Retrieves `roleId`'s admin identifier. + * @param roleId - The role identifier. + * @returns The admin identifier for `roleId`. + */ + public getRoleAdmin(roleId: Uint8Array): Uint8Array { + return this.contract.impureCircuits.getRoleAdmin( + this.circuitContext, + roleId, + ).result; + } + + /** + * @description Grants an account permissions to use `roleId`. + * @param caller - Optional. Sets the caller context if provided. + * @param roleId - The role identifier. + * @param account - A ZswapCoinPublicKey or a ContractAddress. + */ + public grantRole( + roleId: Uint8Array, + account: Either, + caller?: CoinPublicKey, + ) { + const res = this.contract.impureCircuits.grantRole( + { + ...this.circuitContext, + currentZswapLocalState: caller + ? emptyZswapLocalState(caller) + : this.circuitContext.currentZswapLocalState, + }, + roleId, + account, + ); + + this.circuitContext = res.context; + } + + /** + * @description Revokes an account's permission to use `roleId`. + * @param caller - Optional. Sets the caller context if provided. + * @param roleId - The role identifier. + * @param account - A ZswapCoinPublicKey or a ContractAddress. + */ + public revokeRole( + roleId: Uint8Array, + account: Either, + caller?: CoinPublicKey, + ) { + const res = this.contract.impureCircuits.revokeRole( + { + ...this.circuitContext, + currentZswapLocalState: caller + ? emptyZswapLocalState(caller) + : this.circuitContext.currentZswapLocalState, + }, + roleId, + account, + ); + + this.circuitContext = res.context; + } + + /** + * @description Revokes `roleId` from the calling account. + * @param caller - Optional. Sets the caller context if provided. + * @param roleId - The role identifier. + * @param account - A ZswapCoinPublicKey or a ContractAddress. + */ + public renounceRole( + roleId: Uint8Array, + account: Either, + caller?: CoinPublicKey, + ) { + const res = this.contract.impureCircuits.renounceRole( + { + ...this.circuitContext, + currentZswapLocalState: caller + ? emptyZswapLocalState(caller) + : this.circuitContext.currentZswapLocalState, + }, + roleId, + account, + ); + + this.circuitContext = res.context; + } + + /** + * @description Sets the admin identifier for `roleId`. + * @param roleId - The role identifier. + * @param adminId - The admin role identifier. + */ + public _setRoleAdmin(roleId: Uint8Array, adminId: Uint8Array) { + this.circuitContext = this.contract.impureCircuits._setRoleAdmin( + this.circuitContext, + roleId, + adminId, + ).context; + } + + /** + * @description Grants an account permissions to use `roleId`. Internal function without access restriction. + * @param roleId - The role identifier. + * @param account - A ZswapCoinPublicKey or a ContractAddress. + */ + public _grantRole( + roleId: Uint8Array, + account: Either, + ): boolean { + const res = this.contract.impureCircuits._grantRole( + this.circuitContext, + roleId, + account, + ); + + this.circuitContext = res.context; + return res.result; + } + + /** + * @description Grants an account permissions to use `roleId`. Internal function without access restriction. + * DOES NOT restrict sending to a ContractAddress. + * @param roleId - The role identifier. + * @param account - A ZswapCoinPublicKey or a ContractAddress. + */ + public _unsafeGrantRole( + roleId: Uint8Array, + account: Either, + ): boolean { + const res = this.contract.impureCircuits._unsafeGrantRole( + this.circuitContext, + roleId, + account, + ); + + this.circuitContext = res.context; + return res.result; + } + + /** + * @description Revokes an account's permission to use `roleId`. Internal function without access restriction. + * @param roleId - The role identifier. + * @param account - A ZswapCoinPublicKey or a ContractAddress. + */ + public _revokeRole( + roleId: Uint8Array, + account: Either, + ): boolean { + const res = this.contract.impureCircuits._revokeRole( + this.circuitContext, + roleId, + account, + ); + + this.circuitContext = res.context; + return res.result; + } +} diff --git a/contracts/shieldedAccessControl/src/witnesses/ShieldedAccessControlWitnesses.ts b/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts similarity index 98% rename from contracts/shieldedAccessControl/src/witnesses/ShieldedAccessControlWitnesses.ts rename to contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts index 74ae49bd..4ad00147 100644 --- a/contracts/shieldedAccessControl/src/witnesses/ShieldedAccessControlWitnesses.ts +++ b/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts @@ -14,7 +14,7 @@ import { type Ledger, Contract as MockShieldedAccessControl, type ZswapCoinPublicKey, -} from '../artifacts/MockShieldedAccessControl/contract/index.cjs'; // Combined imports +} from '../shieldedAccessControl/src/artifacts/MockShieldedAccessControl/contract/index.cjs'; // Combined imports const { hkdfSync } = await import('node:crypto'); From 8e5dd6bdd2862da583258a7ba0be720562b9f6d9 Mon Sep 17 00:00:00 2001 From: Andrew Fleming Date: Tue, 26 Aug 2025 22:42:14 -0500 Subject: [PATCH 143/322] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: ⟣ €₥ℵ∪ℓ ⟢ <34749913+emnul@users.noreply.github.com> Signed-off-by: Andrew Fleming --- compact/src/runCompiler.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/compact/src/runCompiler.ts b/compact/src/runCompiler.ts index e5a0ecf6..31d69cea 100644 --- a/compact/src/runCompiler.ts +++ b/compact/src/runCompiler.ts @@ -41,7 +41,7 @@ import { CompactCompiler } from './Compiler.js'; * ℹ [COMPILE] Compact compiler started * ℹ [COMPILE] COMPACT_HOME: /path/to/compactc * ℹ [COMPILE] COMPACTC_PATH: /path/to/compactc/compactc - * ℹ [COMPILE] TARGET_DIR: accesss:compact:access: + * ℹ [COMPILE] TARGET_DIR: access:compact:access: * ℹ [COMPILE] Found 4 .compact file(s) to compile in access/ * ✔ [COMPILE] [1/4] Compiled access/AccessControl.compact * ✔ [COMPILE] [2/4] Compiled access/Ownable.compact @@ -68,7 +68,8 @@ async function runCompiler(): Promise { for (let i = 0; i < args.length; i++) { if (args[i] === '--dir') { - if (i + 1 < args.length && !args[i + 1].startsWith('--')) { + const dirNameExists = i + 1 < args.length && !args[i + 1].startsWith('--'); + if (dirNameExists) { targetDir = args[i + 1]; i++; // Skip the next argument (directory name) } else { From b100b9e506c6635aaf6684c8abbcad94b5a77823 Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 27 Aug 2025 12:57:30 -0300 Subject: [PATCH 144/322] add AGPK section --- docs/modules/ROOT/pages/access.adoc | 33 ++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/docs/modules/ROOT/pages/access.adoc b/docs/modules/ROOT/pages/access.adoc index e8c1cf47..0347b1f0 100644 --- a/docs/modules/ROOT/pages/access.adoc +++ b/docs/modules/ROOT/pages/access.adoc @@ -226,7 +226,7 @@ Some examples: WARNING: Approaches that avoid private key exposure (public key + passphrase, signature-based) are generally recommended for operational security. -Deriving the nonce deterministically from the public key and user passphrase provides a balance of security and recoverability: +Deriving the nonce deterministically from an <> and user passphrase provides a balance of security and recoverability: ```typescript // Example: Scrypt-based derivation @@ -254,6 +254,37 @@ Users should carefully evaluate their threat model, operational requirements, and privacy needs when selecting a nonce generation strategy, as this choice cannot be easily changed without transferring ownership. +=== Air-Gapped Public Key (AGPK) + +For maximum privacy guarantees, +users should employ an Air-Gapped Public Key (AGPK) exclusively for contract ownership and administrative circuits. +An AGPK is a public key that maintains complete isolation from all other on-chain activities, +similar to how air-gapped systems are isolated from networks to prevent data leakage. + +==== The Privacy Enhancement + +While `ZOwnablePK` provides cryptographic privacy through its commitment scheme, +operational security practices like using an AGPK provide an additional layer of protection against correlation attacks. Even with the strongest cryptographic commitments, +reusing a public key across different on-chain activities can potentially compromise privacy +through transaction pattern analysis. + +==== AGPK Principles + +An Air-Gapped Public Key must adhere to strict isolation principles: + +- *Never used before:* The public key has no prior transaction history on any blockchain network. +- *Never used elsewhere:* The key is exclusively reserved for the specific contract's administrative circuits i.e. `assertOnlyOwner`. +- *Never used again:* The private key is destroyed once ownership is renounced or transferred to another account. + +==== Best Practices Recommendation + +While neither required nor enforced by the `ZOwnablePK` module, +an Air-Gapped Public Key provides strong operational privacy hygiene for shielded contract administration. +Users should evaluate their threat model and privacy requirements when deciding whether to implement AGPK practices. + +WARNING: The effectiveness of an AGPK depends entirely on abiding by the AGPK principles. +A single transaction using the key outside the administrative context compromises all privacy benefits. + === Usage Import the `ZOwnablePK` module into the implementing contract and expose the ownership-handling circuits. From 15aff68740757c21c7b6fe0657c67f1a56d16783 Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 27 Aug 2025 13:16:05 -0300 Subject: [PATCH 145/322] fix guide links in access api, add agpk ref --- docs/modules/ROOT/pages/api/access.adoc | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/modules/ROOT/pages/api/access.adoc b/docs/modules/ROOT/pages/api/access.adoc index 5b3b95f6..adc19ac6 100644 --- a/docs/modules/ROOT/pages/api/access.adoc +++ b/docs/modules/ROOT/pages/api/access.adoc @@ -1,6 +1,8 @@ :github-icon: pass:[] -:accessControl-guide: xref:accessControl.adoc[AccessControl guide] -:ownable-guide: xref:ownable.adoc[Ownable guide] +:accessControl-guide: xref:access.adoc#role_based_access_control[AccessControl guide] +:ownable-guide: xref:access.adoc#ownership_and_ownable[Ownable guide] +:zownablepk-guide: xref:access.adoc#shielded_ownership_and_zownablepk[ZOwnablePK guide] +:agpk: xref:access.adoc#air_gapped_public_key_agpk[Air-Gapped Public Key] :grantRole: <> :revokeRole: <> @@ -12,6 +14,8 @@ This directory provides ways to restrict who can access the circuits of a contra - `<>` is a simpler mechanism with a single owner "role" that can be assigned to a single account. This simpler mechanism can be useful for quick tests but projects with production concerns are likely to outgrow it. +- `<>` provides a privacy-preserving single owner access control mechanism using cryptographic commitments. The owner's public key is never revealed on-chain, instead storing only a commitment that proves ownership without exposing identity, suitable for applications requiring administrative control with strong privacy guarantees. + == Core [.contract] @@ -411,12 +415,13 @@ import "./node_modules/@openzeppelin-compact/contracts/src/access/ZOwnablePK"; `ZOwnablePK` provides a privacy-preserving access control mechanism for contracts with a single administrative user. Unlike traditional `Ownable` implementations that store or expose the owner's public key on-chain, this module stores only a commitment to a hashed identifier derived from the owner's public key and a secret nonce. +For the strongest security guarantees, use an {agpk}. Ownable provides a basic access control mechanism where an account (an owner) can be granted exclusive access to specific circuits. This module includes <> to restrict a circuit to be used only by the owner. -TIP: For an overview of the module, read the {ownable-guide}. +TIP: For an overview of the module, read the {zownablepk-guide}. [.contract-index] .Circuits From 524dab91958be425310994ba21f59f0546f54867 Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 27 Aug 2025 13:31:56 -0300 Subject: [PATCH 146/322] add agpk recommendation --- contracts/src/access/ZOwnablePK.compact | 2 ++ docs/modules/ROOT/pages/api/access.adoc | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/contracts/src/access/ZOwnablePK.compact b/contracts/src/access/ZOwnablePK.compact index 9f750dff..c4704a9c 100644 --- a/contracts/src/access/ZOwnablePK.compact +++ b/contracts/src/access/ZOwnablePK.compact @@ -11,6 +11,7 @@ pragma language_version >= 0.16.0; * `Ownable` implementations that store or expose the owner's public key * on-chain, this module stores only a commitment to a hashed identifier * derived from the owner's public key and a secret nonce. + * For the strongest security guarantees, use an Air-Gapped Public Key. * * @notice This module explicitly supports commitments derived from public keys; * however, it may be possible to use contract addresses when contract-to-contract @@ -255,6 +256,7 @@ module ZOwnablePK { * - `pk`: The public key of the caller. This is passed explicitly to allow * for off-chain derivation, testing, or scenarios where the caller is * different from the subject of the computation. + * We recommend using an Air-Gapped Public Key. * - `nonce`: A secret nonce tied to the identity. The generation strategy is * left to the user, offering different security/convenience trade-offs. * diff --git a/docs/modules/ROOT/pages/api/access.adoc b/docs/modules/ROOT/pages/api/access.adoc index adc19ac6..dd900b8a 100644 --- a/docs/modules/ROOT/pages/api/access.adoc +++ b/docs/modules/ROOT/pages/api/access.adoc @@ -444,7 +444,7 @@ TIP: For an overview of the module, read the {zownablepk-guide}. Initializes the contract by setting the initial owner via `ownerId` and storing the `instanceSalt` that acts as a privacy additive -for preventing duplicate commitments among other contracts implementing ZOwnablePK. +for preventing duplicate commitments among other contracts implementing `ZOwnablePK`. NOTE: The `ownerId` must be calculated prior to contract deployment. See <> @@ -569,6 +569,7 @@ Computes the unique identifier (`id`) of the owner from their public key and a s - `pk`: The public key of the caller. This is passed explicitly to allow for off-chain derivation, testing, or scenarios where the caller is different from the subject of the computation. +We recommend using an {agpk}. - `nonce`: A secret nonce tied to the identity. This value should be randomly generated and kept private. It may be rotated periodically for enhanced unlinkability. From 7ba770dfcca6649c48bcb38abb867ce0413a6ae8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Wed, 27 Aug 2025 16:31:29 -0400 Subject: [PATCH 147/322] WIP Experimental re-design --- .../src/access/ShieldedAccessControl.compact | 86 ++++++------------- 1 file changed, 25 insertions(+), 61 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index fd21521c..d9870c24 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -101,6 +101,8 @@ module ShieldedAccessControl {  */ export ledger _roleCommitmentNullifiers: Set>; + export ledger _currentMerkleTreeIndex: Counter; + export ledger DEFAULT_ADMIN_ROLE: Bytes<32>; /** @@ -114,9 +116,12 @@ module ShieldedAccessControl { witness secretNonce(roleId: Bytes<32>): Bytes<32>; - witness merkleTreeIndex(roleId: Bytes<32>): Uint<64>; + witness getRoleCommitmentIndex(roleId: Bytes<32>): Uint<64>; - witness getFirstFreeMerkleTreeIndex(): Uint<64>; + struct Role { + hasRole: Boolean; + roleCommitment: Bytes<32>; + } /** * @description Returns `true` if `account` has been granted `roleId`. @@ -145,7 +150,7 @@ module ShieldedAccessControl { * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK). * @return {Boolean} - A boolean determining if the account has the specified role.  */ - export circuit hasRole(roleId: Bytes<32>, account: Either): Boolean { + export circuit hasRole(roleId: Bytes<32>, account: Either): Role { if (!Utils_isContractAddress(account)) { const zswapPubKey = account.left.bytes; return _checkMerkleTree(roleId, zswapPubKey); @@ -216,7 +221,8 @@ module ShieldedAccessControl { * @return {[]} - Empty tuple. */ export circuit _checkRole(roleId: Bytes<32>, account: Either): [] { - assert(hasRole(roleId, account), "ShieldedAccessControl: unauthorized account"); + const role = hasRole(roleId, account); + assert(role.hasRole, "ShieldedAccessControl: unauthorized account"); } /** @@ -246,15 +252,16 @@ module ShieldedAccessControl { * @return {Boolean} - A boolean determining if a path for for the role commitment * produced by SHA256(roleId | account | nonce) exists in the `_operatorRoles` Merkle tree */ - export circuit _checkMerkleTree(roleId: Bytes<32>, account: Bytes<32>): Boolean { + export circuit _checkMerkleTree(roleId: Bytes<32>, account: Bytes<32>): Role { const nonce = secretNonce(roleId); - const index = merkleTreeIndex(roleId); + const index = getMerkleTreeIndex(roleId); const roleCommitment = persistentHash>>([roleId, account, nonce, index as Field as Bytes<32>]); assert(!_roleCommitmentNullifiers.member(disclose(roleCommitment)), "ShieldedAccessControl: role commitment access revoked"); const authPath = getRoleCommitmentPath(roleCommitment); - return _operatorRoles + const hasRole = _operatorRoles .checkRoot(merkleTreePathRoot<10, Bytes<32>>(disclose(authPath))); + return Role {hasRole, roleCommitment}; } /** @@ -459,18 +466,22 @@ module ShieldedAccessControl { * @return {Boolean} roleGranted - A boolean indicating if `role` was granted. */ export circuit _unsafeGrantRole(roleId: Bytes<32>, account: Either): Boolean { - if (hasRole(roleId, account)) { + const role = hasRole(roleId, account); + if (role.hasRole) { return false; } if (!Utils_isContractAddress(account)) { const zswapPubKey = account.left.bytes; - _addRoleCommitmentToLedger(roleId, zswapPubKey); + _operatorRoles.insertHashIndex(disclose(role.roleCommitment), _currentMerkleTreeIndex); + _currentMerkleTreeIndex.increment(1); return true; } const contractAddress = account.right.bytes; - _addRoleCommitmentToLedger(roleId, contractAddress); + // Use ledger index as source of truth + _operatorRoles.insertHashIndex(disclose(role.roleCommitment), _currentMerkleTreeIndex); + _currentMerkleTreeIndex.increment(1); return true; } @@ -503,66 +514,19 @@ module ShieldedAccessControl { * @return {Boolean} roleRevoked - A boolean indicating if `roleId` was revoked. */ export circuit _revokeRole(roleId: Bytes<32>, account: Either): Boolean { - if (!hasRole(roleId, account)) { + const role = hasRole(roleId, account); + if (!role.hasRole) { return false; } if(!Utils_isContractAddress(account)) { const zswapPubKey = account.left.bytes; - _nullifyRoleCommitment(roleId, zswapPubKey); + _roleCommitmentNullifiers.insert(disclose(role.roleCommitment)); return true; } const contractAddress = account.right.bytes; - _nullifyRoleCommitment(roleId, contractAddress); + _roleCommitmentNullifiers.insert(disclose(role.roleCommitment)); return true; } - - /** - * @description Adds a role commitment to the `_operatorRoles` Merkle tree. - * - * WARNING: Exposing this circuit in the implementing contract would allow anyone to add roles. - * - * @circuitInfo k=15, rows=24565 - * - * Disclosures: - * - * - The role commitment produced by SHA256(roleId | account | nonce). - * - * @param {Bytes<32>} roleId - The role identifier. - * @param {Bytes<32>} account - The account to add represented as a Bytes<32>. - * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK) - * @return {Boolean} roleRevoked - A boolean indicating if `roleId` was revoked. - */ - circuit _addRoleCommitmentToLedger(roleId: Bytes<32>, account: Bytes<32>): [] { - const nonce = secretNonce(roleId); - const index = getFirstFreeMerkleTreeIndex(); - const roleCommitment = persistentHash>>([roleId, account, nonce, index as Field as Byes<32>]); - - _operatorRoles.insertHashIndex(disclose(roleCommitment), index); - } - - /** - * @description Adds a role commitment to the `_roleNullifiers` nullifer set. - * - * WARNING: Exposing this circuit in the implementing contract would allow anyone to revoke roles. - * - * @circuitInfo k=15, rows=24559 - * - * Disclosures: - * - * - The role commitment produced by SHA256(roleId | account | nonce). - * - The intermediate role commitment produced by SHA256(roleId | account | nonce). - * - * @param {Bytes<32>} roleId - The role identifier. - * @param {Bytes<32>} account - The account to add represented as a Bytes<32>. - * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK) - * @return {Boolean} roleRevoked - A boolean indicating if `roleId` was revoked. - */ - circuit _nullifyRoleCommitment(roleId: Bytes<32>, account: Bytes<32>): [] { - const nonce = secretNonce(roleId); - const index = merkleTreeIndex(roleId); - const roleCommitment = persistentHash>>([roleId, account, nonce, index as Field as Bytes<32>]); - _roleCommitmentNullifiers.insert(disclose(roleCommitment)); - } } From a3e6a3ad01e893ff189a7a2fdc260e7bc9077a11 Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 27 Aug 2025 17:57:16 -0300 Subject: [PATCH 148/322] add descriptions to circuits in sim --- .../test/simulators/ZOwnablePKSimulator.ts | 43 +++++++++++++------ 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/contracts/src/access/test/simulators/ZOwnablePKSimulator.ts b/contracts/src/access/test/simulators/ZOwnablePKSimulator.ts index 730ae0d4..e6e3f62b 100644 --- a/contracts/src/access/test/simulators/ZOwnablePKSimulator.ts +++ b/contracts/src/access/test/simulators/ZOwnablePKSimulator.ts @@ -203,46 +203,57 @@ export class ZOwnablePKSimulator extends AbstractContractSimulator< } /** - * @description Returns the shielded owner. - * @returns The shielded owner. + * @description Returns the current commitment representing the contract owner. + * The full commitment is: `SHA256(SHA256(pk, nonce), instanceSalt, counter, domain)`. + * @returns The current owner's commitment. */ public owner(): Uint8Array { return this.circuits.impure.owner(); } /** - * @description + * @description Transfers ownership to `newOwnerId`. + * `newOwnerId` must be precalculated and given to the current owner off chain. + * @param newOwnerId The new owner's unique identifier (`SHA256(pk, nonce)`). */ public transferOwnership(newOwnerId: Uint8Array) { this.circuits.impure.transferOwnership(newOwnerId); } /** - * @description Leaves the contract without an owner. It will not be - * possible to call `assertOnlyOnwer` circuits anymore. Can only be - * called by the current owner. + * @description Leaves the contract without an owner. + * It will not be possible to call `assertOnlyOnwer` circuits anymore. + * Can only be called by the current owner. */ public renounceOwnership() { this.circuits.impure.renounceOwnership(); } /** - * @description Throws if called by any account other than the owner. - * Use this to restrict access to sensitive circuits. + * @description Throws if called by any account whose id hash `SHA256(pk, nonce)` does not match + * the stored owner commitment. Use this to only allow the owner to call specific circuits. */ public assertOnlyOwner() { this.circuits.impure.assertOnlyOwner(); } /** - * @description + * @description Computes the owner commitment from the given `id` and `counter`. + * @param id - The unique identifier of the owner calculated by `SHA256(pk, nonce)`. + * @param counter - The current counter or round. This increments by `1` + * after every transfer to prevent duplicate commitments given the same `id`. + * @returns The commitment derived from `id` and `counter`. */ public _computeOwnerCommitment(id: Uint8Array, counter: bigint): Uint8Array { return this.circuits.impure._computeOwnerCommitment(id, counter); } /** - * @description + * @description Computes the unique identifier (`id`) of the owner from their + * public key and a secret nonce. + * @param pk - The public key of the identity being committed. + * @param nonce - A private nonce to scope the commitment. + * @returns The computed owner ID. */ public _computeOwnerId( pk: Either, @@ -252,7 +263,9 @@ export class ZOwnablePKSimulator extends AbstractContractSimulator< } /** - * @description Internal circuit that transfers ownership of the contract to `newOwner`. + * @description Transfers ownership to owner id `newOwnerId` without + * enforcing permission checks on the caller. + * @param newOwnerId - The unique identifier of the new owner calculated by `SHA256(pk, nonce)`. */ public _transferOwnership(newOwnerId: Uint8Array) { this.circuits.impure._transferOwnership(newOwnerId); @@ -260,7 +273,9 @@ export class ZOwnablePKSimulator extends AbstractContractSimulator< public readonly privateState = { /** - * @description Stubs a new nonce into the private state. + * @description Contextually sets a new nonce into the private state. + * @param newNonce The secret nonce. + * @returns The ZOwnablePK private state after setting the new nonce. */ injectSecretNonce: ( newNonce: Buffer, @@ -271,6 +286,10 @@ export class ZOwnablePKSimulator extends AbstractContractSimulator< return updatedState; }, + /** + * @description Returns the secret nonce given the context. + * @returns The secret nonce. + */ getCurrentSecretNonce: (): Uint8Array => { return this.stateManager.getContext().currentPrivateState.secretNonce; }, From a22ba6c68e722fb0fe6379fc2b7d0a1433309c62 Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 27 Aug 2025 17:57:51 -0300 Subject: [PATCH 149/322] fix sim state mngr, improve docs --- .../src/access/test/utils/SimualatorStateManager.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/contracts/src/access/test/utils/SimualatorStateManager.ts b/contracts/src/access/test/utils/SimualatorStateManager.ts index 943ecf35..def6174a 100644 --- a/contracts/src/access/test/utils/SimualatorStateManager.ts +++ b/contracts/src/access/test/utils/SimualatorStateManager.ts @@ -4,6 +4,7 @@ import { type ContractState, constructorContext, QueryContext, + sampleContractAddress } from '@midnight-ntwrk/compact-runtime'; /** @@ -28,9 +29,9 @@ import { * const contract = new MyContract(witnesses); * const manager = new SimulatorStateManager( * contract, - * { foo: 1n }, // initial private state + * { foo: 1n }, // initial private state * '0'.repeat(64), // coin public key - * undefined, // optional contract address + * sampleContractAddress(), // optional contract address * arg1, arg2 // additional constructor args * ); * @@ -45,8 +46,8 @@ export class SimulatorStateManager

{ * * @param contract - A compiled Compact contract instance (from artifacts), exposing `initialState()`. * @param privateState - The initial private state to inject into the contract. - * @param coinPK - The caller's coin public key (used to create the constructor context and as default address). - * @param contractAddress - Optional override for the contract's address. Defaults to `coinPK` if not provided. + * @param coinPK - The caller's coin public key (used to create the constructor context). + * @param contractAddress - Optional override for the contract's address. Defaults to `sampleContractAddress` if not provided. * @param contractArgs - Additional arguments to pass to the contract constructor (e.g., circuit params). */ constructor( @@ -79,7 +80,7 @@ export class SimulatorStateManager

{ originalState: currentContractState, transactionContext: new QueryContext( currentContractState.data, - contractAddress ?? coinPK, + contractAddress ?? sampleContractAddress(), ), }; } From 37bb7aa85efbf27078361e4bca4840c50e4090e2 Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 27 Aug 2025 17:58:37 -0300 Subject: [PATCH 150/322] fix fmt --- contracts/src/access/test/utils/SimualatorStateManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/src/access/test/utils/SimualatorStateManager.ts b/contracts/src/access/test/utils/SimualatorStateManager.ts index def6174a..ac06fdba 100644 --- a/contracts/src/access/test/utils/SimualatorStateManager.ts +++ b/contracts/src/access/test/utils/SimualatorStateManager.ts @@ -4,7 +4,7 @@ import { type ContractState, constructorContext, QueryContext, - sampleContractAddress + sampleContractAddress, } from '@midnight-ntwrk/compact-runtime'; /** From 02227ab2cfa624606d33227df575206008feea88 Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 27 Aug 2025 18:35:01 -0300 Subject: [PATCH 151/322] add non-deterministic sig warning --- docs/modules/ROOT/pages/access.adoc | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/modules/ROOT/pages/access.adoc b/docs/modules/ROOT/pages/access.adoc index 0347b1f0..2f3facbb 100644 --- a/docs/modules/ROOT/pages/access.adoc +++ b/docs/modules/ROOT/pages/access.adoc @@ -211,12 +211,20 @@ However, it requires secure backup of both the private key and the nonce. ==== Deterministic Nonce +:rfc6979: https://datatracker.ietf.org/doc/html/rfc6979[RFC 6979] +:ed25519: https://ed25519.cr.yp.to/[Ed25519] + Deriving the nonce deterministically enables recovery through derivation schemes. Some examples: - `H(passphrase + context)` - recoverable from passphrase only, but passphrase becomes critical single point of failure. - `H(publicKey + userPassphrase + context)` - requires both public key and passphrase. -- `H(signature + context)` where `signature = sign(context)` - leverages wallet without exposing private key. +- `H(signature + context)` where `signature = sign(context)` - leverages wallet without exposing private key + +WARNING: When using signature-based nonce derivation, +ensure the wallet/library uses deterministic signatures ({ed25519} or {rfc6979} for ECDSA). +Non-deterministic signatures will generate different nonces on each signing, making recovery impossible. +Test the implementation by signing the same message twice then verify that the signatures match. *Context-Dependent Derivations:* From b584c533cfc4cdcfbbddb44e0846143f270d3b52 Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 27 Aug 2025 18:35:50 -0300 Subject: [PATCH 152/322] add period --- docs/modules/ROOT/pages/access.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modules/ROOT/pages/access.adoc b/docs/modules/ROOT/pages/access.adoc index 2f3facbb..ecb1852a 100644 --- a/docs/modules/ROOT/pages/access.adoc +++ b/docs/modules/ROOT/pages/access.adoc @@ -219,7 +219,7 @@ Some examples: - `H(passphrase + context)` - recoverable from passphrase only, but passphrase becomes critical single point of failure. - `H(publicKey + userPassphrase + context)` - requires both public key and passphrase. -- `H(signature + context)` where `signature = sign(context)` - leverages wallet without exposing private key +- `H(signature + context)` where `signature = sign(context)` - leverages wallet without exposing private key. WARNING: When using signature-based nonce derivation, ensure the wallet/library uses deterministic signatures ({ed25519} or {rfc6979} for ECDSA). From 418a7b564ae5276aef53beeb1a2dc452b0bd55b3 Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 27 Aug 2025 19:21:56 -0300 Subject: [PATCH 153/322] improve agpk principles --- docs/modules/ROOT/pages/access.adoc | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/modules/ROOT/pages/access.adoc b/docs/modules/ROOT/pages/access.adoc index ecb1852a..556a5379 100644 --- a/docs/modules/ROOT/pages/access.adoc +++ b/docs/modules/ROOT/pages/access.adoc @@ -280,9 +280,16 @@ through transaction pattern analysis. An Air-Gapped Public Key must adhere to strict isolation principles: -- *Never used before:* The public key has no prior transaction history on any blockchain network. -- *Never used elsewhere:* The key is exclusively reserved for the specific contract's administrative circuits i.e. `assertOnlyOwner`. -- *Never used again:* The private key is destroyed once ownership is renounced or transferred to another account. +- *Never used before:* The private key material +(including any seed, parent key, or entropy source from which this key is derived) +has never generated any public key that appears in any on-chain transaction, across any blockchain network. +The key material must be cryptographically virgin. +- *Never used elsewhere:* From the moment of AGPK generation until its destruction, +the private key material is used exclusively for this contract's administrative functions (i.e. `assertOnlyOwner`). +No other public keys may be derived from or generated with the same key material during this period. +- *Never used again:* Users commit to destroying all copies of the private key material +upon ownership renunciation or transfer. +This relies entirely on user discipline and cannot be externally verified or enforced. ==== Best Practices Recommendation From bd18c19094c59ed12748700d206f2a011539689a Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 27 Aug 2025 19:28:07 -0300 Subject: [PATCH 154/322] improve clarity on 'never used elsewhere' --- docs/modules/ROOT/pages/access.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modules/ROOT/pages/access.adoc b/docs/modules/ROOT/pages/access.adoc index 556a5379..f9c2d52c 100644 --- a/docs/modules/ROOT/pages/access.adoc +++ b/docs/modules/ROOT/pages/access.adoc @@ -286,7 +286,7 @@ has never generated any public key that appears in any on-chain transaction, acr The key material must be cryptographically virgin. - *Never used elsewhere:* From the moment of AGPK generation until its destruction, the private key material is used exclusively for this contract's administrative functions (i.e. `assertOnlyOwner`). -No other public keys may be derived from or generated with the same key material during this period. +No other public keys may ever be derived from or generated with the same key material. - *Never used again:* Users commit to destroying all copies of the private key material upon ownership renunciation or transfer. This relies entirely on user discipline and cannot be externally verified or enforced. From acf33979a8413ee13a9ca4c941a2ff7f08465fa9 Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 27 Aug 2025 22:15:22 -0300 Subject: [PATCH 155/322] add wit_ prefix to witnesses --- contracts/src/access/ZOwnablePK.compact | 6 +++--- contracts/src/access/witnesses/ZOwnablePKWitnesses.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/contracts/src/access/ZOwnablePK.compact b/contracts/src/access/ZOwnablePK.compact index c4704a9c..37e4616f 100644 --- a/contracts/src/access/ZOwnablePK.compact +++ b/contracts/src/access/ZOwnablePK.compact @@ -74,13 +74,13 @@ module ZOwnablePK { export sealed ledger _instanceSalt: Bytes<32>; /** - * @witness secretNonce + * @witness wit_secretNonce * @description A private per-user nonce used in deriving the shielded owner identifier. * * Combined with the user's public key as `SHA256(pk, nonce)` to produce an obfuscated, * unlinkable identity commitment. Users are encouraged to rotate this value on ownership changes. */ - export witness secretNonce(): Bytes<32>; + export witness wit_secretNonce(): Bytes<32>; /** * @description Initializes the contract by setting the initial owner via `ownerId` @@ -189,7 +189,7 @@ module ZOwnablePK { export circuit assertOnlyOwner(): [] { Initializable_assertInitialized(); - const nonce = secretNonce(); + const nonce = wit_secretNonce(); const callerAsEither = Either { is_left: true, left: ownPublicKey(), diff --git a/contracts/src/access/witnesses/ZOwnablePKWitnesses.ts b/contracts/src/access/witnesses/ZOwnablePKWitnesses.ts index 58098d02..62cc3bba 100644 --- a/contracts/src/access/witnesses/ZOwnablePKWitnesses.ts +++ b/contracts/src/access/witnesses/ZOwnablePKWitnesses.ts @@ -12,7 +12,7 @@ export interface IZOwnablePKWitnesses

{ * @param context - The witness context containing the private state. * @returns A tuple of the private state and the secret nonce as a Uint8Array. */ - secretNonce(context: WitnessContext): [P, Uint8Array]; + wit_secretNonce(context: WitnessContext): [P, Uint8Array]; } /** @@ -60,7 +60,7 @@ export const ZOwnablePKPrivateState = { */ export const ZOwnablePKWitnesses = (): IZOwnablePKWitnesses => ({ - secretNonce( + wit_secretNonce( context: WitnessContext, ): [ZOwnablePKPrivateState, Uint8Array] { return [context.privateState, context.privateState.secretNonce]; From 84899249c29e4bf87aa0d05a1b6bc55079af6f27 Mon Sep 17 00:00:00 2001 From: Andrew Fleming Date: Wed, 27 Aug 2025 20:23:06 -0500 Subject: [PATCH 156/322] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: ⟣ €₥ℵ∪ℓ ⟢ <34749913+emnul@users.noreply.github.com> Signed-off-by: Andrew Fleming --- turbo.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/turbo.json b/turbo.json index 57e1bbb5..9d1bac99 100644 --- a/turbo.json +++ b/turbo.json @@ -18,7 +18,7 @@ "compact:access": { "dependsOn": ["^build", "compact:security", "compact:utils"], "env": ["COMPACT_HOME", "SKIP_ZK"], - "inputs": ["src/access/**/*.compact", "artifacts/**"], + "inputs": ["src/access/**/*.compact"], "outputLogs": "new-only", "outputs": ["artifacts/**/"] }, From 67972b935727d3b4fe3e71d585a28dc479b9f71c Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 27 Aug 2025 22:24:00 -0300 Subject: [PATCH 157/322] remove artifacts from inputs --- turbo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/turbo.json b/turbo.json index 9d1bac99..a293cf52 100644 --- a/turbo.json +++ b/turbo.json @@ -25,14 +25,14 @@ "compact:archive": { "dependsOn": ["^build", "compact:utils"], "env": ["COMPACT_HOME", "SKIP_ZK"], - "inputs": ["src/archive/**/*.compact", "artifacts/**"], + "inputs": ["src/archive/**/*.compact"], "outputLogs": "new-only", "outputs": ["artifacts/**/"] }, "compact:token": { "dependsOn": ["^build", "compact:security", "compact:utils"], "env": ["COMPACT_HOME", "SKIP_ZK"], - "inputs": ["src/token/**/*.compact", "artifacts/**"], + "inputs": ["src/token/**/*.compact"], "outputLogs": "new-only", "outputs": ["artifacts/**/"] }, From f23e56af213be809d4e8a65e4f8107df7e22f5a7 Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 27 Aug 2025 22:24:49 -0300 Subject: [PATCH 158/322] fix fmt --- compact/src/runCompiler.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/compact/src/runCompiler.ts b/compact/src/runCompiler.ts index 31d69cea..8d5b98aa 100644 --- a/compact/src/runCompiler.ts +++ b/compact/src/runCompiler.ts @@ -68,7 +68,8 @@ async function runCompiler(): Promise { for (let i = 0; i < args.length; i++) { if (args[i] === '--dir') { - const dirNameExists = i + 1 < args.length && !args[i + 1].startsWith('--'); + const dirNameExists = + i + 1 < args.length && !args[i + 1].startsWith('--'); if (dirNameExists) { targetDir = args[i + 1]; i++; // Skip the next argument (directory name) From efaa2a0d34f5784adc65991fdcce3994ec125dde Mon Sep 17 00:00:00 2001 From: Andrew Fleming Date: Wed, 27 Aug 2025 22:47:13 -0500 Subject: [PATCH 159/322] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: ⟣ €₥ℵ∪ℓ ⟢ <34749913+emnul@users.noreply.github.com> Signed-off-by: Andrew Fleming --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 71c1cec8..6b688a27 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ and skipping the prover and verifier key file generation: ```bash # Individual module compilation (recommended for development) -turbo compact:token -- --skip-zk +turbo compact:token --filter=@openzeppelin-compact/contracts -- --skip-zk # Full compilation with skip-zk (use environment variable) SKIP_ZK=true turbo compact From eb3d4bc4b9618fac2fa234190b1aa4f1eafd4aaa Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 28 Aug 2025 15:35:29 -0300 Subject: [PATCH 160/322] rebase --- contracts/src/access/ZOwnablePK.compact | 310 ++++++++++ contracts/src/access/test/Ownable.test.ts | 14 +- contracts/src/access/test/ZOwnablePK.test.ts | 534 ++++++++++++++++++ .../access/test/mocks/MockZOwnablePK.compact | 50 ++ .../test/simulators/AccessControlSimulator.ts | 6 +- .../test/simulators/OwnableSimulator.ts | 6 +- .../test/simulators/ZOwnablePKSimulator.ts | 307 ++++++++++ contracts/src/access/test/types/test.ts | 98 +++- .../test/utils/AbstractContractSimulator.ts | 131 +++++ .../test/utils/SimualatorStateManager.ts | 115 ++++ contracts/src/access/test/utils/address.ts | 24 + .../access/test/utils/createCircuitProxies.ts | 64 +++ contracts/src/access/test/utils/test.ts | 4 +- .../access/witnesses/ZOwnablePKWitnesses.ts | 68 +++ docs/modules/ROOT/pages/access.adoc | 248 ++++++++ docs/modules/ROOT/pages/api/access.adoc | 206 ++++++- 16 files changed, 2156 insertions(+), 29 deletions(-) create mode 100644 contracts/src/access/ZOwnablePK.compact create mode 100644 contracts/src/access/test/ZOwnablePK.test.ts create mode 100644 contracts/src/access/test/mocks/MockZOwnablePK.compact create mode 100644 contracts/src/access/test/simulators/ZOwnablePKSimulator.ts create mode 100644 contracts/src/access/test/utils/AbstractContractSimulator.ts create mode 100644 contracts/src/access/test/utils/SimualatorStateManager.ts create mode 100644 contracts/src/access/test/utils/createCircuitProxies.ts create mode 100644 contracts/src/access/witnesses/ZOwnablePKWitnesses.ts diff --git a/contracts/src/access/ZOwnablePK.compact b/contracts/src/access/ZOwnablePK.compact new file mode 100644 index 00000000..37e4616f --- /dev/null +++ b/contracts/src/access/ZOwnablePK.compact @@ -0,0 +1,310 @@ +// SPDX-License-Identifier: MIT + +pragma language_version >= 0.16.0; + +/** + * @module ZOwnablePK + * @description A shielded, public key-derived Ownable module. + * + * `ZOwnablePK` provides a privacy-preserving access control mechanism + * for contracts with a single administrative user. Unlike traditional + * `Ownable` implementations that store or expose the owner's public key + * on-chain, this module stores only a commitment to a hashed identifier + * derived from the owner's public key and a secret nonce. + * For the strongest security guarantees, use an Air-Gapped Public Key. + * + * @notice This module explicitly supports commitments derived from public keys; + * however, it may be possible to use contract addresses when contract-to-contract + * calls become available. This will be revisited when it's known if/how witnesses + * are used from a contract address context. + * + * @dev Features: + * - Obfuscated owner identity: The owner's public key is never revealed on-chain. + * - Stateless verification: The contract never needs access to the full public key. + * - Built-in support for transfer and renounce functionality. + * - Instance-specific salts to prevent cross-contract correlation. + * - Deterministic hashing with `persistentHash` to support zero-knowledge verification. + * + * @dev Commitment structure: + * ``` + * id = SHA256(pk, secretNonce) + * commitment = SHA256(id, instanceSalt, counter, "ZOwnablePK:shield:") + * ``` + * The commitment changes on each transfer due to the incrementing `counter`, + * providing unlinkability across ownership changes. + * + * @dev Security Considerations: + * - The `secretNonce` must be kept private. Loss of the nonce prevents the + * owner from proving ownership or transferring it. + * - Ownership validation is entirely circuit-based using witness-provided values. + * - The `_instanceSalt` is immutable and used to differentiate deployments. + * + * @notice Best used for single-admin contracts with privacy requirements. + * It is not designed for multi-owner or role-based access control. + */ +module ZOwnablePK { + import CompactStandardLibrary; + import "../security/Initializable" prefix Initializable_; + + /** + * @ledger _ownerCommitment + * @description Stores the current hashed commitment representing the owner. + * This commitment is derived from the public identifier (e.g., `SHA256(pk, nonce)`), + * the `instanceSalt`, the transfer `counter`, and a domain separator. + * + * A commitment of `default>` (i.e., zero) indicates the contract is unowned. + */ + export ledger _ownerCommitment: Bytes<32>; + /** + * @ledger _counter + * @description Internal transfer counter used to prevent commitment reuse. + * + * Increments by 1 on every successful ownership transfer. Combined with `id` and + * `instanceSalt` to compute unique owner commitments over time. + */ + export ledger _counter: Counter; + /** + * @sealed @ledger _instanceSalt + * @description A per-instance value provided at initialization used to namespace + * commitments for this contract instance. + * + * This salt prevents commitment collisions across contracts that might otherwise use + * the same owner identifiers or domain parameters. It is immutable after initialization. + */ + export sealed ledger _instanceSalt: Bytes<32>; + + /** + * @witness wit_secretNonce + * @description A private per-user nonce used in deriving the shielded owner identifier. + * + * Combined with the user's public key as `SHA256(pk, nonce)` to produce an obfuscated, + * unlinkable identity commitment. Users are encouraged to rotate this value on ownership changes. + */ + export witness wit_secretNonce(): Bytes<32>; + + /** + * @description Initializes the contract by setting the initial owner via `ownerId` + * and storing the `instanceSalt` that acts as a privacy additive for preventing + * duplicate commitments among other contracts implementing ZOwnablePK. + * + * @warning The `ownerId` must be calculated prior to contract deployment using the SHA256 hashing algorithm. + * Using any other algorithm will result in a permanent loss of contract access. + * + * @circuitInfo k=14, rows=14933 + * + * Requirements: + * + * - Contract is not initialized. + * - `ownerId` is not zero. + * + * @param {Bytes<32>} ownerId - The owner's unique identifier SHA256(pk, nonce). + * @param {Bytes<32>} instanceSalt - Contract salt to prevent duplicate commitments if + * users reuse their PK and secretNonce witness (not recommended). + * @returns {[]} Empty tuple. + */ + export circuit initialize(ownerId: Bytes<32>, instanceSalt: Bytes<32>): [] { + Initializable_initialize(); + + assert(ownerId != default>, "ZOwnablePK: invalid id"); + _instanceSalt = disclose(instanceSalt); + _transferOwnership(ownerId); + } + + /** + * @description Returns the current commitment representing the contract owner. + * The full commitment is: `SHA256(SHA256(pk, nonce), instanceSalt, counter, domain)`. + * + * @circuitInfo k=10, rows=57 + * + * Requirements: + * + * - Contract is initialized. + * + * @returns {Bytes<32>} The current owner's commitment. + */ + export circuit owner(): Bytes<32> { + Initializable_assertInitialized(); + return _ownerCommitment; + } + + /** + * @description Transfers ownership to `newOwnerId`. + * `newOwnerId` must be precalculated and given to the current owner off chain. + * + * @circuitInfo k=16, rows=39240 + * + * Requirements: + * + * - Contract is initialized. + * - Caller is the the current owner. + * - `newOwnerId` is not an empty array. + * + * @param {Bytes<32>} newOwnerId - The new owner's unique identifier (`SHA256(pk, nonce)`). + * @returns {[]} Empty tuple. + */ + export circuit transferOwnership(newOwnerId: Bytes<32>): [] { + Initializable_assertInitialized(); + + assertOnlyOwner(); + assert(newOwnerId != default>, "ZOwnablePK: invalid id"); + _transferOwnership(newOwnerId); + } + + /** + * @description Leaves the contract without an owner. + * It will not be possible to call `assertOnlyOnwer` circuits anymore. + * Can only be called by the current owner. + * + * @circuitInfo k=15, rows=24442 + * + * Requirements: + * + * - Contract is initialized. + * - Caller is the the current owner. + * + * @returns {[]} Empty tuple. + */ + export circuit renounceOwnership(): [] { + Initializable_assertInitialized(); + + assertOnlyOwner(); + _ownerCommitment.resetToDefault(); + } + + /** + * @description Throws if called by any account whose id hash `SHA256(pk, nonce)` does not match + * the stored owner commitment. + * Use this to only allow the owner to call specific circuits. + * + * @circuitInfo k=15, rows=24437 + * + * Requirements: + * + * - Contract is initialized. + * - Caller's id (`SHA256(pk, nonce)`) when used in `_computeOwnerCommitment` equals + * the stored `_ownerCommitment`, thus verifying themselves as the owner. + * + * @returns {[]} Empty tuple. + */ + export circuit assertOnlyOwner(): [] { + Initializable_assertInitialized(); + + const nonce = wit_secretNonce(); + const callerAsEither = Either { + is_left: true, + left: ownPublicKey(), + right: ContractAddress { bytes: pad(32, "") } + }; + const id = _computeOwnerId(callerAsEither, nonce); + assert(_ownerCommitment == _computeOwnerCommitment(id, _counter), "ZOwnablePK: caller is not the owner"); + } + + /** + * @description Computes the owner commitment from the given `id` and `counter`. + * + * ## Owner ID (`id`) + * The `id` is expected to be computed off-chain as: + * `id = SHA256(pk, nonce)` + * + * - `pk`: The owner's public key. + * - `nonce`: A secret nonce scoped to the instance, ideally rotated with each transfer. + * + * ## Commitment Derivation + * `commitment = SHA256(id, instanceSalt, counter, domain)` + * + * - `id`: See above. + * - `instanceSalt`: A unique per-deployment salt, stored during initialization. + * This prevents commitment collisions across deployments. + * - `counter`: Incremented with each ownership transfer, ensuring uniqueness + * even with repeated `id` values. Cast to `Field` then `Bytes<32>` for hashing. + * - `domain`: Domain separator `"ZOwnablePK:shield:"` (padded to 32 bytes) to prevent + * hash collisions when extending the module or using similar commitment schemes. + * + * @circuitInfo k=14, rows=14853 + * + * Requirements: + * + * - Contract is initialized. + * + * @param {Bytes<32>} id - The unique identifier of the owner calculated by `SHA256(pk, nonce)`. + * @param {Uint<64>} counter - The current counter or round. This increments by `1` + * after every transfer to prevent duplicate commitments given the same `id`. + * @returns {Bytes<32>} The commitment derived from `id` and `counter`. + */ + export circuit _computeOwnerCommitment( + id: Bytes<32>, + counter: Uint<64>, + ): Bytes<32> { + Initializable_assertInitialized(); + return persistentHash>>( + [ + id, + _instanceSalt, + counter as Field as Bytes<32>, + pad(32, "ZOwnablePK:shield:") + ] + ); + } + + /** + * @description Computes the unique identifier (`id`) of the owner from their + * public key and a secret nonce. + * + * ## ID Derivation + * `id = SHA256(pk, nonce)` + * + * - `pk`: The public key of the caller. This is passed explicitly to allow + * for off-chain derivation, testing, or scenarios where the caller is + * different from the subject of the computation. + * We recommend using an Air-Gapped Public Key. + * - `nonce`: A secret nonce tied to the identity. The generation strategy is + * left to the user, offering different security/convenience trade-offs. + * + * The result is a 32-byte commitment that uniquely identifies the owner. + * This value is later used in owner commitment hashing, + * and acts as a privacy-preserving alternative to a raw public key. + * + * @notice This module allows ownership to be tied to an identity commitment derived + * from a public key and secret nonce. + * While typically used with user public keys, this mechanism may also + * support contract addresses as identifiers in future contract-to-contract + * interactions. Both are treated as 32-byte values (`Bytes<32>`). + * + * Requirements: + * + * - `pk` is not a ContractAddress. + * + * @param {Either} pk - The public key of the identity being committed. + * @param {Bytes<32>} nonce - A private nonce to scope the commitment. + * @returns {Bytes<32>} The computed owner ID. + */ + export pure circuit _computeOwnerId( + pk: Either, + nonce: Bytes<32> + ): Bytes<32> { + assert(pk.is_left, "ZOwnablePK: contract address owners are not yet supported"); + + return persistentHash>>([pk.left.bytes, nonce]); + } + + /** + * @description Transfers ownership to owner id `newOwnerId` without + * enforcing permission checks on the caller. + * + * @circuitInfo k=14, rows=14823 + * + * Requirements: + * + * - Contract is initialized. + * + * @param {Bytes<32>} newOwnerId - The unique identifier of the new owner + * calculated by `SHA256(pk, nonce)`. + * @returns {[]} Empty tuple. + */ + export circuit _transferOwnership(newOwnerId: Bytes<32>): [] { + Initializable_assertInitialized(); + + _counter.increment(1); + _ownerCommitment = _computeOwnerCommitment(disclose(newOwnerId), _counter); + } +} diff --git a/contracts/src/access/test/Ownable.test.ts b/contracts/src/access/test/Ownable.test.ts index bde39230..60053cb7 100644 --- a/contracts/src/access/test/Ownable.test.ts +++ b/contracts/src/access/test/Ownable.test.ts @@ -3,14 +3,12 @@ import { beforeEach, describe, expect, it } from 'vitest'; import { OwnableSimulator } from './simulators/OwnableSimulator.js'; import * as utils from './utils/address.js'; -// Callers -const OWNER = utils.toHexPadded('OWNER'); -const NEW_OWNER = utils.toHexPadded('NEW_OWNER'); -const UNAUTHORIZED = utils.toHexPadded('UNAUTHORIZED'); - -// Encoded PK/Addresses -const Z_OWNER = utils.createEitherTestUser('OWNER'); -const Z_NEW_OWNER = utils.createEitherTestUser('NEW_OWNER'); +// PKs +const [OWNER, Z_OWNER] = utils.generateEitherPubKeyPair('OWNER'); +const [NEW_OWNER, Z_NEW_OWNER] = utils.generateEitherPubKeyPair('NEW_OWNER'); +const [UNAUTHORIZED, _] = utils.generateEitherPubKeyPair('UNAUTHORIZED'); + +// Encoded contract addresses const Z_OWNER_CONTRACT = utils.createEitherTestContractAddress('OWNER_CONTRACT'); const Z_RECIPIENT_CONTRACT = diff --git a/contracts/src/access/test/ZOwnablePK.test.ts b/contracts/src/access/test/ZOwnablePK.test.ts new file mode 100644 index 00000000..0369705a --- /dev/null +++ b/contracts/src/access/test/ZOwnablePK.test.ts @@ -0,0 +1,534 @@ +import { + CompactTypeBytes, + CompactTypeVector, + convert_bigint_to_Uint8Array, + persistentHash, + transientHash, + upgradeFromTransient, +} from '@midnight-ntwrk/compact-runtime'; +import { beforeEach, describe, expect, it } from 'vitest'; +import type { ZswapCoinPublicKey } from '../../../artifacts/MockOwnable/contract/index.cjs'; +import { ZOwnablePKPrivateState } from '../witnesses/ZOwnablePKWitnesses.js'; +import { ZOwnablePKSimulator } from './simulators/ZOwnablePKSimulator.js'; +import * as utils from './utils/address.js'; + +// PKs +const [OWNER, Z_OWNER] = utils.generatePubKeyPair('OWNER'); +const [NEW_OWNER, Z_NEW_OWNER] = utils.generatePubKeyPair('NEW_OWNER'); +const [UNAUTHORIZED, _] = utils.generatePubKeyPair('UNAUTHORIZED'); + +const INSTANCE_SALT = new Uint8Array(32).fill(8675309); +const BAD_NONCE = Buffer.from(Buffer.alloc(32, 'BAD_NONCE')); +const DOMAIN = 'ZOwnablePK:shield:'; +const INIT_COUNTER = 1n; + +const isInit = true; +let secretNonce: Uint8Array; +let ownable: ZOwnablePKSimulator; + +// Helpers +const createIdHash = ( + pk: ZswapCoinPublicKey, + nonce: Uint8Array, +): Uint8Array => { + const rt_type = new CompactTypeVector(2, new CompactTypeBytes(32)); + + const bPK = pk.bytes; + return persistentHash(rt_type, [bPK, nonce]); +}; + +const buildCommitmentFromId = ( + id: Uint8Array, + instanceSalt: Uint8Array, + counter: bigint, +): Uint8Array => { + const rt_type = new CompactTypeVector(4, new CompactTypeBytes(32)); + const bCounter = convert_bigint_to_Uint8Array(32, counter); + const bDomain = new TextEncoder().encode(DOMAIN); + + const commitment = persistentHash(rt_type, [ + id, + instanceSalt, + bCounter, + bDomain, + ]); + return commitment; +}; + +const buildCommitment = ( + pk: ZswapCoinPublicKey, + nonce: Uint8Array, + instanceSalt: Uint8Array, + counter: bigint, + domain: string, +): Uint8Array => { + const id = createIdHash(pk, nonce); + + const rt_type = new CompactTypeVector(4, new CompactTypeBytes(32)); + const bCounter = convert_bigint_to_Uint8Array(32, counter); + const bDomain = new TextEncoder().encode(domain); + + const commitment = persistentHash(rt_type, [ + id, + instanceSalt, + bCounter, + bDomain, + ]); + return commitment; +}; + +describe('ZOwnablePK', () => { + describe('before initialize', () => { + it('should fail when setting owner commitment as 0', () => { + expect(() => { + const badId = new Uint8Array(32).fill(0); + new ZOwnablePKSimulator(badId, INSTANCE_SALT, isInit); + }).toThrow('ZOwnablePK: invalid id'); + }); + + it('should initialize with non-zero commitment', () => { + const notZeroPK = utils.encodeToPK('NOT_ZERO'); + const notZeroNonce = new Uint8Array(32).fill(1); + const nonZeroId = createIdHash(notZeroPK, notZeroNonce); + ownable = new ZOwnablePKSimulator(nonZeroId, INSTANCE_SALT, isInit); + + const nonZeroCommitment = buildCommitmentFromId( + nonZeroId, + INSTANCE_SALT, + INIT_COUNTER, + ); + expect(ownable.owner()).toEqual(nonZeroCommitment); + }); + }); + + describe('when not deployed and not initialized', () => { + const isNotInit = false; + + beforeEach(() => { + ownable = new ZOwnablePKSimulator( + randomByteArray, + INSTANCE_SALT, + isNotInit, + ); + }); + type FailingCircuits = [method: keyof ZOwnablePKSimulator, args: unknown[]]; + const randomByteArray = new Uint8Array(32).fill(123); + const randomCounter = 321n; + // Circuit calls should fail before the args are used + const circuitsToFail: FailingCircuits[] = [ + ['owner', []], + ['assertOnlyOwner', []], + ['transferOwnership', [randomByteArray]], + ['renounceOwnership', []], + ['_computeOwnerCommitment', [randomByteArray, randomCounter]], + ['_transferOwnership', [randomByteArray]], + ]; + it.each(circuitsToFail)('%s should fail', (circuitName, args) => { + expect(() => { + (ownable[circuitName] as (...args: unknown[]) => unknown)(...args); + }).toThrow('Initializable: contract not initialized'); + }); + + it('should allow pure computeOwnerId', () => { + const eitherOwner = utils.createEitherTestUser('OWNER'); + + expect(() => { + ownable._computeOwnerId(eitherOwner, randomByteArray); + }).not.toThrow(); + }); + }); + + describe('when incorrect hashing algo (not SHA256) is used to generate initial owner id', () => { + // ZOwnablePK only supports sha256 for owner id calculation + // Obviously, using any other algo for the id will not work + const badHashAlgo = (pk: ZswapCoinPublicKey, nonce: Uint8Array) => { + const rt_type = new CompactTypeVector(2, new CompactTypeBytes(32)); + return upgradeFromTransient(transientHash(rt_type, [pk.bytes, nonce])); + }; + const secretNonce = ZOwnablePKPrivateState.generate().secretNonce; + const badOwnerId = badHashAlgo(Z_OWNER, secretNonce); + + beforeEach(() => { + ownable = new ZOwnablePKSimulator(badOwnerId, INSTANCE_SALT, isInit); + }); + // + type FailingCircuits = [method: keyof ZOwnablePKSimulator, args: unknown[]]; + const protectedCircuits: FailingCircuits[] = [ + ['assertOnlyOwner', []], + ['transferOwnership', [badOwnerId]], + ['renounceOwnership', []], + ]; + + it.each(protectedCircuits)('%s should fail', (circuitName, args) => { + ownable.callerCtx.setCaller(OWNER); + + expect(() => { + (ownable[circuitName] as (...args: unknown[]) => unknown)(...args); + }).toThrow('ZOwnablePK: caller is not the owner'); + }); + }); + + describe('after initialization', () => { + beforeEach(() => { + // Create private state object and generate nonce + const PS = ZOwnablePKPrivateState.generate(); + // Bind nonce for convenience + secretNonce = PS.secretNonce; + // Prepare owner ID with gen nonce + const ownerId = createIdHash(Z_OWNER, secretNonce); + // Deploy contract with derived owner commitment and PS + ownable = new ZOwnablePKSimulator(ownerId, INSTANCE_SALT, isInit, { + privateState: PS, + }); + }); + + describe('owner', () => { + it('should return the correct owner commitment', () => { + const expCommitment = buildCommitment( + Z_OWNER, + secretNonce, + INSTANCE_SALT, + INIT_COUNTER, + DOMAIN, + ); + expect(ownable.owner()).toEqual(expCommitment); + }); + }); + + describe('transferOwnership', () => { + let newOwnerCommitment: Uint8Array; + let newOwnerNonce: Uint8Array; + let newIdHash: Uint8Array; + let newCounter: bigint; + + beforeEach(() => { + // Prepare new owner commitment + newOwnerNonce = ZOwnablePKPrivateState.generate().secretNonce; + newCounter = INIT_COUNTER + 1n; + newIdHash = createIdHash(Z_NEW_OWNER, newOwnerNonce); + newOwnerCommitment = buildCommitment( + Z_NEW_OWNER, + newOwnerNonce, + INSTANCE_SALT, + newCounter, + DOMAIN, + ); + }); + + it('should transfer ownership', () => { + ownable.callerCtx.setCaller(OWNER); + ownable.transferOwnership(newIdHash); + expect(ownable.owner()).toEqual(newOwnerCommitment); + + // Old owner + ownable.callerCtx.setCaller(OWNER); + expect(() => { + ownable.assertOnlyOwner(); + }).toThrow('ZOwnablePK: caller is not the owner'); + + // Unauthorized + ownable.callerCtx.setCaller(UNAUTHORIZED); + expect(() => { + ownable.assertOnlyOwner(); + }).toThrow('ZOwnablePK: caller is not the owner'); + + // New owner + ownable.callerCtx.setCaller(NEW_OWNER); + ownable.privateState.injectSecretNonce(Buffer.from(newOwnerNonce)); + expect(ownable.assertOnlyOwner()).not.to.throw; + }); + + it('should fail when transferring to id zero', () => { + ownable.callerCtx.setCaller(OWNER); + const badId = new Uint8Array(32).fill(0); + expect(() => { + ownable.transferOwnership(badId); + }).toThrow('ZOwnablePK: invalid id'); + }); + + it('should fail when unauthorized transfers ownership', () => { + ownable.callerCtx.setCaller(UNAUTHORIZED); + expect(() => { + ownable.transferOwnership(newOwnerCommitment); + }).toThrow('ZOwnablePK: caller is not the owner'); + }); + + /** + * @description More thoroughly tested in `_transferOwnership` + * */ + it('should bump instance after transfer', () => { + const beforeInstance = ownable.getPublicState().ZOwnablePK__counter; + + // Transfer + ownable.callerCtx.setCaller(OWNER); + ownable.transferOwnership(newOwnerCommitment); + + // Check counter + const afterInstance = ownable.getPublicState().ZOwnablePK__counter; + expect(afterInstance).toEqual(beforeInstance + 1n); + }); + + it('should change commitment when transferring ownership to self with same pk + nonce)', () => { + // Confirm current commitment + const repeatedId = createIdHash(Z_OWNER, secretNonce); + const initCommitment = ownable.owner(); + const expInitCommitment = buildCommitmentFromId( + repeatedId, + INSTANCE_SALT, + INIT_COUNTER, + ); + expect(initCommitment).toEqual(expInitCommitment); + + // Transfer ownership to self with the same id -> `H(pk, nonce)` + ownable.callerCtx.setCaller(OWNER); + ownable.transferOwnership(repeatedId); + + // Check commitments don't match + const newCommitment = ownable.owner(); + expect(initCommitment).not.toEqual(newCommitment); + + // Build commitment locally and validate new commitment == expected + const bumpedCounter = INIT_COUNTER + 1n; + const expNewCommitment = buildCommitmentFromId( + repeatedId, + INSTANCE_SALT, + bumpedCounter, + ); + expect(newCommitment).toEqual(expNewCommitment); + + // Check same owner maintains permissions after transfer + ownable.callerCtx.setCaller(OWNER); + expect(ownable.assertOnlyOwner()).not.to.throw; + }); + }); + + describe('renounceOwnership', () => { + it('should renounce ownership', () => { + ownable.callerCtx.setCaller(OWNER); + ownable.renounceOwnership(); + + // Check owner is reset + expect(ownable.owner()).toEqual(new Uint8Array(32).fill(0)); + + // Check revoked permissions + expect(() => { + ownable.assertOnlyOwner(); + }).toThrow('ZOwnablePK: caller is not the owner'); + }); + + it('should fail when renouncing from unauthorized', () => { + ownable.callerCtx.setCaller(UNAUTHORIZED); + expect(() => { + ownable.renounceOwnership(); + }); + }); + + it('should fail when renouncing from authorized with bad nonce', () => { + ownable.callerCtx.setCaller(OWNER); + ownable.privateState.injectSecretNonce(BAD_NONCE); + expect(() => { + ownable.renounceOwnership(); + }); + }); + + it('should fail when renouncing from unauthorized with bad nonce', () => { + ownable.callerCtx.setCaller(UNAUTHORIZED); + ownable.privateState.injectSecretNonce(BAD_NONCE); + expect(() => { + ownable.renounceOwnership(); + }); + }); + }); + + describe('assertOnlyOwner', () => { + it('should allow authorized caller with correct nonce to call', () => { + // Check nonce is correct + expect(ownable.privateState.getCurrentSecretNonce()).toEqual( + secretNonce, + ); + + ownable.callerCtx.setCaller(OWNER); + expect(ownable.assertOnlyOwner()).to.not.throw; + }); + + it('should fail when the authorized caller has the wrong nonce', () => { + // Inject bad nonce + ownable.privateState.injectSecretNonce(BAD_NONCE); + + // Check nonce does not match + expect(ownable.privateState.getCurrentSecretNonce()).not.toEqual( + secretNonce, + ); + + // Set caller and call circuit + ownable.callerCtx.setCaller(OWNER); + expect(() => { + ownable.assertOnlyOwner(); + }).toThrow('ZOwnablePK: caller is not the owner'); + }); + + it('should fail when unauthorized caller has the correct nonce', () => { + // Check nonce is correct + expect(ownable.privateState.getCurrentSecretNonce()).toEqual( + secretNonce, + ); + + ownable.callerCtx.setCaller(UNAUTHORIZED); + expect(() => { + ownable.assertOnlyOwner(); + }).toThrow('ZOwnablePK: caller is not the owner'); + }); + + it('should fail when unauthorized caller has the wrong nonce', () => { + // Inject bad nonce + ownable.privateState.injectSecretNonce(BAD_NONCE); + + // Check nonce does not match + expect(ownable.privateState.getCurrentSecretNonce()).not.toEqual( + secretNonce, + ); + + // Set unauthorized caller and call circuit + ownable.callerCtx.setCaller(UNAUTHORIZED); + expect(() => { + ownable.assertOnlyOwner(); + }).toThrow('ZOwnablePK: caller is not the owner'); + }); + }); + + describe('_computeOwnerCommitment', () => { + const MAX_U64 = 2n ** 64n - 1n; + const testCases = [ + ...Array.from({ length: 10 }, (_, i) => ({ + label: `User${i}`, + ownerPK: utils.encodeToPK(`User${i}`), + counter: BigInt(Math.floor(Math.random() * 2 ** 64 - 1)), + })), + { + label: 'ZeroCounter', + ownerPK: utils.encodeToPK('ZeroCounter'), + counter: 0n, + }, + { + label: 'MaxCounter', + ownerPK: utils.encodeToPK('MaxUser'), + counter: MAX_U64, + }, + ]; + it.each(testCases)( + 'should match commitment for $label with counter $counter', + ({ ownerPK, counter }) => { + const id = createIdHash(ownerPK, secretNonce); + + // Check buildCommitmentFromId + const hashFromContract = ownable._computeOwnerCommitment(id, counter); + const hashFromHelper1 = buildCommitmentFromId( + id, + INSTANCE_SALT, + counter, + ); + expect(hashFromContract).toEqual(hashFromHelper1); + + // Check buildCommitment + const hashFromHelper2 = buildCommitment( + ownerPK, + secretNonce, + INSTANCE_SALT, + counter, + DOMAIN, + ); + expect(hashFromHelper1).toEqual(hashFromHelper2); + }, + ); + }); + + describe('_computeOwnerId', () => { + const testCases = [ + ...Array.from({ length: 10 }, (_, i) => ({ + label: `User${i}`, + eitherOwner: utils.createEitherTestUser(`User${i}`), + nonce: new Uint8Array(32).fill(i), + })), + { + label: 'All-zero nonce', + eitherOwner: utils.createEitherTestUser('ZeroUser'), + nonce: new Uint8Array(32).fill(0), + }, + { + label: 'Max nonce', + eitherOwner: utils.createEitherTestUser('MaxUser'), + nonce: new Uint8Array(32).fill(255), + }, + ]; + + it.each(testCases)( + 'should match local and contract owner id for $label', + ({ eitherOwner, nonce }) => { + const ownerId = ownable._computeOwnerId(eitherOwner, nonce); + const expId = createIdHash(eitherOwner.left, nonce); + expect(ownerId).toEqual(expId); + }, + ); + + it('should fail to compute ContractAddress id', () => { + const eitherContract = + utils.createEitherTestContractAddress('CONTRACT'); + expect(() => { + ownable._computeOwnerId(eitherContract, secretNonce); + }).toThrow('ZOwnablePK: contract address owners are not yet supported'); + }); + }); + + describe('_transferOwnership', () => { + it('should transfer ownership', () => { + const id = createIdHash(Z_OWNER, secretNonce); + ownable._transferOwnership(id); + + const nextCounter = INIT_COUNTER + 1n; + const expCommitment = buildCommitmentFromId( + id, + INSTANCE_SALT, + nextCounter, + ); + expect(ownable.owner()).toEqual(expCommitment); + }); + + it('should bump the counter with each transfer', () => { + const nTransfers = 10; + const counterStart = 2; // count starts at 2 bc the constructor bumps the count to 1 + for (let i = counterStart; i <= nTransfers; i++) { + const pk = utils.encodeToPK(`Id${i}`); + const nonce = new Uint8Array(32).fill(i); + const id = createIdHash(pk, nonce); + ownable._transferOwnership(id); + + expect(ownable.getPublicState().ZOwnablePK__counter).toEqual( + BigInt(i), + ); + } + }); + + it('should allow transfer to all zeroes id', () => { + const zeroId = utils.zeroUint8Array(); + ownable._transferOwnership(zeroId); + + const nextCounter = INIT_COUNTER + 1n; + const expCommitment = buildCommitmentFromId( + zeroId, + INSTANCE_SALT, + nextCounter, + ); + expect(ownable.owner()).toEqual(expCommitment); + }); + + it('should allow anyone to transfer', () => { + ownable.callerCtx.setCaller(OWNER); + const id = createIdHash(Z_OWNER, secretNonce); + expect(ownable._transferOwnership(id)).not.to.throw; + + ownable.callerCtx.setCaller(UNAUTHORIZED); + expect(ownable._transferOwnership(id)).not.to.throw; + }); + }); + }); +}); diff --git a/contracts/src/access/test/mocks/MockZOwnablePK.compact b/contracts/src/access/test/mocks/MockZOwnablePK.compact new file mode 100644 index 00000000..d769f30e --- /dev/null +++ b/contracts/src/access/test/mocks/MockZOwnablePK.compact @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MIT + +pragma language_version >= 0.16.0; + +import CompactStandardLibrary; +import "../../ZOwnablePK" prefix ZOwnablePK_; + +export { ZswapCoinPublicKey, ContractAddress, Either, Maybe }; +export { ZOwnablePK__ownerCommitment, ZOwnablePK__counter }; + +/** + * @description `isInit` is a param for testing. + * + * If `isInit` is false, the constructor will not initialize the contract. + * This behavior is to test that circuits are not callable unless the + * contract is initialized. +*/ +constructor(initOwnerCommitment: Bytes<32>, instanceSalt: Bytes<32>, isInit: Boolean) { + if (disclose(isInit)) { + ZOwnablePK_initialize(initOwnerCommitment, instanceSalt); + } +} + +export circuit owner(): Bytes<32> { + return ZOwnablePK_owner(); +} + +export circuit transferOwnership(newOwnerCommitment: Bytes<32>): [] { + return ZOwnablePK_transferOwnership(disclose(newOwnerCommitment)); +} + +export circuit renounceOwnership(): [] { + return ZOwnablePK_renounceOwnership(); +} + +export circuit assertOnlyOwner(): [] { + return ZOwnablePK_assertOnlyOwner(); +} + +export circuit _computeOwnerCommitment(id: Bytes<32>, counter: Uint<64>): Bytes<32> { + return ZOwnablePK__computeOwnerCommitment(id, counter); +} + +export pure circuit _computeOwnerId(pk: Either, nonce: Bytes<32>): Bytes<32> { + return ZOwnablePK__computeOwnerId(pk, nonce); +} + +export circuit _transferOwnership(newOwnerCommitment: Bytes<32>): [] { + return ZOwnablePK__transferOwnership(newOwnerCommitment); +} diff --git a/contracts/src/access/test/simulators/AccessControlSimulator.ts b/contracts/src/access/test/simulators/AccessControlSimulator.ts index b0309133..b9d0701c 100644 --- a/contracts/src/access/test/simulators/AccessControlSimulator.ts +++ b/contracts/src/access/test/simulators/AccessControlSimulator.ts @@ -66,7 +66,7 @@ export class AccessControlSimulator * @description Retrieves the current public ledger state of the contract. * @returns The ledger state as defined by the contract. */ - public getCurrentPublicState(): Ledger { + public getPublicState(): Ledger { return ledger(this.circuitContext.transactionContext.state); } @@ -74,7 +74,7 @@ export class AccessControlSimulator * @description Retrieves the current private state of the contract. * @returns The private state of type AccessControlPrivateState. */ - public getCurrentPrivateState(): AccessControlPrivateState { + public getPrivateState(): AccessControlPrivateState { return this.circuitContext.currentPrivateState; } @@ -82,7 +82,7 @@ export class AccessControlSimulator * @description Retrieves the current contract state. * @returns The contract state object. */ - public getCurrentContractState(): ContractState { + public getContractState(): ContractState { return this.circuitContext.originalState; } diff --git a/contracts/src/access/test/simulators/OwnableSimulator.ts b/contracts/src/access/test/simulators/OwnableSimulator.ts index de90adb4..f89c310a 100644 --- a/contracts/src/access/test/simulators/OwnableSimulator.ts +++ b/contracts/src/access/test/simulators/OwnableSimulator.ts @@ -71,7 +71,7 @@ export class OwnableSimulator * @description Retrieves the current public ledger state of the contract. * @returns The ledger state as defined by the contract. */ - public getCurrentPublicState(): Ledger { + public getPublicState(): Ledger { return ledger(this.circuitContext.transactionContext.state); } @@ -79,7 +79,7 @@ export class OwnableSimulator * @description Retrieves the current private state of the contract. * @returns The private state of type OwnablePrivateState. */ - public getCurrentPrivateState(): OwnablePrivateState { + public getPrivateState(): OwnablePrivateState { return this.circuitContext.currentPrivateState; } @@ -87,7 +87,7 @@ export class OwnableSimulator * @description Retrieves the current contract state. * @returns The contract state object. */ - public getCurrentContractState(): ContractState { + public getContractState(): ContractState { return this.circuitContext.originalState; } diff --git a/contracts/src/access/test/simulators/ZOwnablePKSimulator.ts b/contracts/src/access/test/simulators/ZOwnablePKSimulator.ts new file mode 100644 index 00000000..e6e3f62b --- /dev/null +++ b/contracts/src/access/test/simulators/ZOwnablePKSimulator.ts @@ -0,0 +1,307 @@ +import { + type CircuitContext, + type CoinPublicKey, + emptyZswapLocalState, +} from '@midnight-ntwrk/compact-runtime'; +import { sampleContractAddress } from '@midnight-ntwrk/zswap'; +import { + type ContractAddress, + type Either, + type Ledger, + ledger, + Contract as MockOwnable, + type ZswapCoinPublicKey, +} from '../../../../artifacts/MockZOwnablePK/contract/index.cjs'; +import { + ZOwnablePKPrivateState, + ZOwnablePKWitnesses, +} from '../../witnesses/ZOwnablePKWitnesses.js'; +import type { + ContextlessCircuits, + ExtractImpureCircuits, + ExtractPureCircuits, + SimulatorOptions, +} from '../types/test.js'; +import { AbstractContractSimulator } from '../utils/AbstractContractSimulator.js'; +import { SimulatorStateManager } from '../utils/SimualatorStateManager.js'; + +type OwnableSimOptions = SimulatorOptions< + ZOwnablePKPrivateState, + typeof ZOwnablePKWitnesses +>; + +/** + * @description A simulator implementation of a contract for testing purposes. + * @template P - The private state type, fixed to ZOwnablePKPrivateState. + * @template L - The ledger type, fixed to Contract.Ledger. + */ +export class ZOwnablePKSimulator extends AbstractContractSimulator< + ZOwnablePKPrivateState, + Ledger +> { + contract: MockOwnable; + readonly contractAddress: string; + private stateManager: SimulatorStateManager; + private callerOverride: CoinPublicKey | null = null; + private _witnesses: ReturnType; + + private _pureCircuitProxy?: ContextlessCircuits< + ExtractPureCircuits>, + ZOwnablePKPrivateState + >; + + private _impureCircuitProxy?: ContextlessCircuits< + ExtractImpureCircuits>, + ZOwnablePKPrivateState + >; + + constructor( + initOwner: Uint8Array, + instanceSalt: Uint8Array, + isInit: boolean, + options: OwnableSimOptions = {}, + ) { + super(); + + // Setup initial state + const { + privateState = ZOwnablePKPrivateState.generate(), + witnesses = ZOwnablePKWitnesses(), + coinPK = '0'.repeat(64), + address = sampleContractAddress(), + } = options; + const constructorArgs = [initOwner, instanceSalt, isInit]; + + this.contract = new MockOwnable(witnesses); + + this.stateManager = new SimulatorStateManager( + this.contract, + privateState, + coinPK, + address, + ...constructorArgs, + ); + this.contractAddress = this.circuitContext.transactionContext.address; + this._witnesses = witnesses; + this.contract = new MockOwnable(this._witnesses); + } + + get circuitContext() { + return this.stateManager.getContext(); + } + + set circuitContext(ctx) { + this.stateManager.setContext(ctx); + } + + getPublicState(): Ledger { + return ledger(this.circuitContext.transactionContext.state); + } + + /** + * @description Constructs a caller-specific circuit context. + * If a caller override is present, it replaces the current Zswap local state with an empty one + * scoped to the overridden caller. Otherwise, the existing context is reused as-is. + * @returns A circuit context adjusted for the current simulated caller. + */ + protected getCallerContext(): CircuitContext { + return { + ...this.circuitContext, + currentZswapLocalState: this.callerOverride + ? emptyZswapLocalState(this.callerOverride) + : this.circuitContext.currentZswapLocalState, + }; + } + + /** + * @description Initializes and returns a proxy to pure contract circuits. + * The proxy automatically injects the current circuit context into each call, + * and returns only the result portion of each circuit's output. + * @notice The proxy is created only when first accessed a.k.a lazy initialization. + * This approach is efficient in cases where only pure or only impure circuits are used, + * avoiding unnecessary proxy creation. + * @returns A proxy object exposing pure circuit functions without requiring explicit context. + */ + protected get pureCircuit(): ContextlessCircuits< + ExtractPureCircuits>, + ZOwnablePKPrivateState + > { + if (!this._pureCircuitProxy) { + this._pureCircuitProxy = this.createPureCircuitProxy< + MockOwnable['circuits'] + >(this.contract.circuits, () => this.circuitContext); + } + return this._pureCircuitProxy; + } + + /** + * @description Initializes and returns a proxy to impure contract circuits. + * The proxy automatically injects the current (possibly caller-modified) context into each call, + * and updates the circuit context with the one returned by the circuit after execution. + * @notice The proxy is created only when first accessed a.k.a. lazy initialization. + * This approach is efficient in cases where only pure or only impure circuits are used, + * avoiding unnecessary proxy creation. + * @returns A proxy object exposing impure circuit functions without requiring explicit context management. + */ + protected get impureCircuit(): ContextlessCircuits< + ExtractImpureCircuits>, + ZOwnablePKPrivateState + > { + if (!this._impureCircuitProxy) { + this._impureCircuitProxy = this.createImpureCircuitProxy< + MockOwnable['impureCircuits'] + >( + this.contract.impureCircuits, + () => this.getCallerContext(), + (ctx: any) => { + this.circuitContext = ctx; + }, + ); + } + return this._impureCircuitProxy; + } + + /** + * @description Resets the cached circuit proxy instances. + * This is useful if the underlying contract state or circuit context has changed, + * and you want to ensure the proxies are recreated with updated context on next access. + */ + public resetCircuitProxies(): void { + this._pureCircuitProxy = undefined; + this._impureCircuitProxy = undefined; + } + + /** + * @description Helper method that provides access to both pure and impure circuit proxies. + * These proxies automatically inject the appropriate circuit context when invoked. + * @returns An object containing `pure` and `impure` circuit proxy interfaces. + */ + public get circuits() { + return { + pure: this.pureCircuit, + impure: this.impureCircuit, + }; + } + + public get witnesses(): ReturnType { + return this._witnesses; + } + + public set witnesses(newWitnesses: ReturnType) { + this._witnesses = newWitnesses; + this.contract = new MockOwnable(this._witnesses); + } + + public overrideWitness( + key: K, + fn: (typeof this._witnesses)[K], + ) { + this.witnesses = { + ...this._witnesses, + [key]: fn, + }; + } + + /** + * @description Returns the current commitment representing the contract owner. + * The full commitment is: `SHA256(SHA256(pk, nonce), instanceSalt, counter, domain)`. + * @returns The current owner's commitment. + */ + public owner(): Uint8Array { + return this.circuits.impure.owner(); + } + + /** + * @description Transfers ownership to `newOwnerId`. + * `newOwnerId` must be precalculated and given to the current owner off chain. + * @param newOwnerId The new owner's unique identifier (`SHA256(pk, nonce)`). + */ + public transferOwnership(newOwnerId: Uint8Array) { + this.circuits.impure.transferOwnership(newOwnerId); + } + + /** + * @description Leaves the contract without an owner. + * It will not be possible to call `assertOnlyOnwer` circuits anymore. + * Can only be called by the current owner. + */ + public renounceOwnership() { + this.circuits.impure.renounceOwnership(); + } + + /** + * @description Throws if called by any account whose id hash `SHA256(pk, nonce)` does not match + * the stored owner commitment. Use this to only allow the owner to call specific circuits. + */ + public assertOnlyOwner() { + this.circuits.impure.assertOnlyOwner(); + } + + /** + * @description Computes the owner commitment from the given `id` and `counter`. + * @param id - The unique identifier of the owner calculated by `SHA256(pk, nonce)`. + * @param counter - The current counter or round. This increments by `1` + * after every transfer to prevent duplicate commitments given the same `id`. + * @returns The commitment derived from `id` and `counter`. + */ + public _computeOwnerCommitment(id: Uint8Array, counter: bigint): Uint8Array { + return this.circuits.impure._computeOwnerCommitment(id, counter); + } + + /** + * @description Computes the unique identifier (`id`) of the owner from their + * public key and a secret nonce. + * @param pk - The public key of the identity being committed. + * @param nonce - A private nonce to scope the commitment. + * @returns The computed owner ID. + */ + public _computeOwnerId( + pk: Either, + nonce: Uint8Array, + ): Uint8Array { + return this.circuits.pure._computeOwnerId(pk, nonce); + } + + /** + * @description Transfers ownership to owner id `newOwnerId` without + * enforcing permission checks on the caller. + * @param newOwnerId - The unique identifier of the new owner calculated by `SHA256(pk, nonce)`. + */ + public _transferOwnership(newOwnerId: Uint8Array) { + this.circuits.impure._transferOwnership(newOwnerId); + } + + public readonly privateState = { + /** + * @description Contextually sets a new nonce into the private state. + * @param newNonce The secret nonce. + * @returns The ZOwnablePK private state after setting the new nonce. + */ + injectSecretNonce: ( + newNonce: Buffer, + ): ZOwnablePKPrivateState => { + const currentState = this.stateManager.getContext().currentPrivateState; + const updatedState = { ...currentState, secretNonce: newNonce }; + this.stateManager.updatePrivateState(updatedState); + return updatedState; + }, + + /** + * @description Returns the secret nonce given the context. + * @returns The secret nonce. + */ + getCurrentSecretNonce: (): Uint8Array => { + return this.stateManager.getContext().currentPrivateState.secretNonce; + }, + }; + + public callerCtx = { + /** + * @description Sets the caller context. + * @param caller The caller in context of the proceeding circuit calls. + */ + setCaller: (caller: CoinPublicKey) => { + this.callerOverride = caller; + }, + }; +} diff --git a/contracts/src/access/test/types/test.ts b/contracts/src/access/test/types/test.ts index 7a909543..643def10 100644 --- a/contracts/src/access/test/types/test.ts +++ b/contracts/src/access/test/types/test.ts @@ -4,23 +4,99 @@ import type { } from '@midnight-ntwrk/compact-runtime'; /** - * Generic interface for mock contract implementations. - * @template P - The type of the contract's private state. - * @template L - The type of the contract's ledger (public state). + * Interface defining a generic contract simulator. + * + * @template P - Type representing the private contract state. + * @template L - Type representing the public ledger state. */ export interface IContractSimulator { - /** The contract's deployed address. */ + /** + * The deployed contract's address. + */ readonly contractAddress: string; - /** The current circuit context. */ + /** + * The current circuit context holding the contract state. + */ circuitContext: CircuitContext

; - /** Retrieves the current ledger state. */ - getCurrentPublicState(): L; + /** + * Returns the current public ledger state. + * + * @returns The current ledger state of type L. + */ + getPublicState(): L; - /** Retrieves the current private state. */ - getCurrentPrivateState(): P; + /** + * Returns the current private contract state. + * + * @returns The current private state of type P. + */ + getPrivateState(): P; - /** Retrieves the current contract state. */ - getCurrentContractState(): ContractState; + /** + * Returns the original contract state. + * + * @returns The current contract state. + */ + getContractState(): ContractState; } + +/** + * Extracts pure circuits from a contract type. + * + * Pure circuits are those in `circuits` but not in `impureCircuits`. + * + * @template TContract - Contract type with `circuits` and `impureCircuits`. + */ +export type ExtractPureCircuits = TContract extends { + circuits: infer TCircuits extends Record; + impureCircuits: infer TImpureCircuits extends Record; +} + ? Omit + : never; + +/** + * Extracts impure circuits from a contract type. + * + * Impure circuits are those in `impureCircuits`. + * + * @template TContract - Contract type with `circuits` and `impureCircuits`. + */ +export type ExtractImpureCircuits = TContract extends { + impureCircuits: infer TImpureCircuits; +} + ? TImpureCircuits + : never; + +/** + * Transforms a collection of circuit functions by removing the explicit `CircuitContext` parameter, + * producing a version of each function that can be called without passing the context explicitly. + * + * Each original circuit function is expected to have the signature: + * `(ctx: CircuitContext, ...args) => { result: R; context: CircuitContext }` + * or a compatible shape. + * + * The transformed type maps each key `K` of the input `Circuits` type to a new function + * that takes the same parameters as the original, *except* the first `CircuitContext` argument, + * and returns the `result` part `R` directly. + * + * @template Circuits - An object type whose values are circuit functions accepting a `CircuitContext` + * and returning an object with `result` and optionally `context`. + * @template TState - The type representing the private or contract state passed inside `CircuitContext`. + */ +export type ContextlessCircuits = { + [K in keyof Circuits]: Circuits[K] extends ( + ctx: CircuitContext, + ...args: infer P + ) => { result: infer R; context: CircuitContext } + ? (...args: P) => R + : never; +}; + +export type SimulatorOptions any> = { + address?: string; + coinPK?: string; + privateState?: PS; + witnesses?: ReturnType; +}; diff --git a/contracts/src/access/test/utils/AbstractContractSimulator.ts b/contracts/src/access/test/utils/AbstractContractSimulator.ts new file mode 100644 index 00000000..36ac5d73 --- /dev/null +++ b/contracts/src/access/test/utils/AbstractContractSimulator.ts @@ -0,0 +1,131 @@ +import type { + CircuitContext, + ContractState, +} from '@midnight-ntwrk/compact-runtime'; +import type { ContextlessCircuits, IContractSimulator } from '../types/test.js'; + +/** + * Abstract base class for simulating contract behavior. + * Provides common functionality for managing circuit contexts and creating proxies + * for pure and impure circuit functions. + * + * @template P - The type representing the private state of the contract. + * @template L - The type representing the public ledger (contract) state. + */ +export abstract class AbstractContractSimulator + implements IContractSimulator +{ + /** + * The deployed contract's address. + * Must be implemented by concrete subclasses. + */ + abstract readonly contractAddress: string; + + /** + * The current circuit context containing private state, contract state, and transaction context. + * Must be implemented by concrete subclasses. + */ + abstract circuitContext: CircuitContext

; + + /** + * Retrieves the current public ledger state of the contract. + * Must be implemented by concrete subclasses. + * + * @returns The current public ledger state. + */ + abstract getPublicState(): L; + + /** + * Retrieves the current private state from the circuit context. + * + * @returns The current private state of the contract. + */ + public getPrivateState(): P { + return this.circuitContext.currentPrivateState; + } + + /** + * Retrieves the original contract state from the circuit context. + * + * @returns The original contract state. + */ + public getContractState(): ContractState { + return this.circuitContext.originalState; + } + + /** + * Creates a proxy wrapper around pure circuits. + * Pure circuits do not modify contract state, so only the result is returned. + * + * @template Circuits - The type of the circuits object to proxy. + * @param circuits - The original circuits object containing functions accepting a CircuitContext. + * @param context - A function returning the current CircuitContext to pass to circuit functions. + * @returns A proxy with contextless circuits that accept the original arguments and return only results. + */ + protected createPureCircuitProxy( + circuits: Circuits, + context: () => CircuitContext

, + ): ContextlessCircuits { + return new Proxy(circuits, { + get(target, prop, receiver) { + const original = Reflect.get(target, prop, receiver); + + if (typeof original !== 'function') return original; + + return (...args: any[]) => { + const ctx = context(); + + const fn = original as ( + ctx: CircuitContext

, + ...args: any[] + ) => { result: any }; + + return fn(ctx, ...args).result; + }; + }, + }) as ContextlessCircuits; + } + + /** + * Creates a proxy wrapper around impure circuits. + * Impure circuits can modify contract state, so the circuit context is updated accordingly. + * + * @template Circuits - The type of the circuits object to proxy. + * @param circuits - The original circuits object containing functions accepting a CircuitContext. + * @param context - A function returning the current CircuitContext to pass to circuit functions. + * @param updateContext - A callback to update the circuit context with the new context returned by the circuit. + * @returns A proxy with contextless circuits that accept the original arguments, update context, and return results. + */ + protected createImpureCircuitProxy( + circuits: Circuits, + context: () => CircuitContext

, + updateContext: (ctx: CircuitContext

) => void, + ): ContextlessCircuits { + return new Proxy(circuits, { + get(target, prop, receiver) { + const original = Reflect.get(target, prop, receiver); + + if (typeof original !== 'function') return original; + + return (...args: any[]) => { + const ctx = context(); + + const fn = original as ( + ctx: CircuitContext

, + ...args: any[] + ) => { result: any; context: CircuitContext

}; + + const { result, context: newCtx } = fn(ctx, ...args); + updateContext(newCtx); + return result; + }; + }, + }) as ContextlessCircuits; + } + + /** + * Optional method to reset any cached circuit proxies. + * Concrete subclasses can override this to clear proxies if needed. + */ + public resetCircuitProxies?(): void {} +} diff --git a/contracts/src/access/test/utils/SimualatorStateManager.ts b/contracts/src/access/test/utils/SimualatorStateManager.ts new file mode 100644 index 00000000..ac06fdba --- /dev/null +++ b/contracts/src/access/test/utils/SimualatorStateManager.ts @@ -0,0 +1,115 @@ +import { + type CircuitContext, + type ConstructorContext, + type ContractState, + constructorContext, + QueryContext, + sampleContractAddress, +} from '@midnight-ntwrk/compact-runtime'; + +/** + * A composable utility class for managing Compact contract state in simulations. + * + * This class handles initialization and lifecycle management of the `CircuitContext`, + * which includes private state, public (ledger) state, zswap local state, and transaction context. + * + * It is designed to be embedded compositionally inside contract simulator classes + * (e.g., `FooSimulator`), enabling better separation of concerns and easier test setup. + * + * @template P - The type of the contract's private state. + * + * ### Responsibilities + * - Initializes the contract state using the compiled contract's `.initialState` method. + * - Stores and exposes the `CircuitContext` via getters/setters. + * - Supports injection of private state and contract constructor arguments. + * - Allows the owning simulator to update private state manually during testing. + * + * ### Example Usage: + * ```ts + * const contract = new MyContract(witnesses); + * const manager = new SimulatorStateManager( + * contract, + * { foo: 1n }, // initial private state + * '0'.repeat(64), // coin public key + * sampleContractAddress(), // optional contract address + * arg1, arg2 // additional constructor args + * ); + * + * const context = manager.getContext(); + * ``` + */ +export class SimulatorStateManager

{ + private context: CircuitContext

; + + /** + * Creates an instance of `SimulatorStateManager`. + * + * @param contract - A compiled Compact contract instance (from artifacts), exposing `initialState()`. + * @param privateState - The initial private state to inject into the contract. + * @param coinPK - The caller's coin public key (used to create the constructor context). + * @param contractAddress - Optional override for the contract's address. Defaults to `sampleContractAddress` if not provided. + * @param contractArgs - Additional arguments to pass to the contract constructor (e.g., circuit params). + */ + constructor( + contract: { + initialState: ( + ctx: ConstructorContext

, + ...args: any[] + ) => { + currentPrivateState: P; + currentContractState: ContractState; + currentZswapLocalState: any; + }; + }, + privateState: P, + coinPK: string, + contractAddress?: string, + ...contractArgs: any[] + ) { + const initCtx = constructorContext(privateState, coinPK); + + const { + currentPrivateState, + currentContractState, + currentZswapLocalState, + } = contract.initialState(initCtx, ...contractArgs); + + this.context = { + currentPrivateState, + currentZswapLocalState, + originalState: currentContractState, + transactionContext: new QueryContext( + currentContractState.data, + contractAddress ?? sampleContractAddress(), + ), + }; + } + + /** + * Retrieves the current `CircuitContext`, including private state, + * zswap state, contract state, and transaction context. + */ + getContext(): CircuitContext

{ + return this.context; + } + + /** + * Replaces the internal `CircuitContext` with a new one. + * + * Useful when circuits mutate state and return an updated context. + */ + setContext(newContext: CircuitContext

) { + this.context = newContext; + } + + /** + * Updates just the private state inside the existing context. + * + * This is a lightweight way to simulate local state changes without reconstructing the full context. + * + * @param newPrivateState - The new private state object to apply. + */ + updatePrivateState(newPrivateState: P) { + this.context.currentPrivateState = newPrivateState; + } +} diff --git a/contracts/src/access/test/utils/address.ts b/contracts/src/access/test/utils/address.ts index f89eab11..fb22e4be 100644 --- a/contracts/src/access/test/utils/address.ts +++ b/contracts/src/access/test/utils/address.ts @@ -61,6 +61,30 @@ export const createEitherTestContractAddress = (str: string) => ({ right: encodeToAddress(str), }); +const baseGeneratePubKeyPair = ( + str: string, + asEither: boolean, +): [ + string, + ( + | Compact.ZswapCoinPublicKey + | Compact.Either + ), +] => { + const pk = toHexPadded(str); + const zpk = asEither ? createEitherTestUser(str) : encodeToPK(str); + return [pk, zpk]; +}; + +export const generatePubKeyPair = (str: string) => + baseGeneratePubKeyPair(str, false) as [string, Compact.ZswapCoinPublicKey]; + +export const generateEitherPubKeyPair = (str: string) => + baseGeneratePubKeyPair(str, true) as [ + string, + Compact.Either, + ]; + export const zeroUint8Array = (length = 32) => convert_bigint_to_Uint8Array(length, 0n); diff --git a/contracts/src/access/test/utils/createCircuitProxies.ts b/contracts/src/access/test/utils/createCircuitProxies.ts new file mode 100644 index 00000000..8aa500da --- /dev/null +++ b/contracts/src/access/test/utils/createCircuitProxies.ts @@ -0,0 +1,64 @@ +import type { CircuitContext } from '@midnight-ntwrk/compact-runtime'; +import type { + ContextlessCircuits, + ExtractImpureCircuits, + ExtractPureCircuits, +} from '../types/test.js'; + +/** + * Creates lazily-initialized circuit proxies for pure and impure contract functions. + */ +export function createCircuitProxies< + P, + ContractType extends { + circuits: Record; + impureCircuits: Record; + }, +>( + contract: ContractType, + getContext: () => CircuitContext

, + getCallerContext: () => CircuitContext

, + updateContext: (ctx: CircuitContext

) => void, + createPureProxy: >( + circuits: C, + context: () => CircuitContext

, + ) => ContextlessCircuits, + createImpureProxy: >( + circuits: C, + context: () => CircuitContext

, + updateContext: (ctx: CircuitContext

) => void, + ) => ContextlessCircuits, +) { + let pureProxy: + | ContextlessCircuits, P> + | undefined; + let impureProxy: + | ContextlessCircuits, P> + | undefined; + + return { + get circuits() { + if (!pureProxy) { + pureProxy = createPureProxy( + contract.circuits as ExtractPureCircuits, + getContext, + ); + } + if (!impureProxy) { + impureProxy = createImpureProxy( + contract.impureCircuits as ExtractImpureCircuits, + getCallerContext, + updateContext, + ); + } + return { + pure: pureProxy, + impure: impureProxy, + }; + }, + resetProxies() { + pureProxy = undefined; + impureProxy = undefined; + }, + }; +} diff --git a/contracts/src/access/test/utils/test.ts b/contracts/src/access/test/utils/test.ts index 2fd5a504..52e92528 100644 --- a/contracts/src/access/test/utils/test.ts +++ b/contracts/src/access/test/utils/test.ts @@ -55,8 +55,8 @@ export function useCircuitContextSender< L, C extends IContractSimulator, >(contract: C, sender: CoinPublicKey): CircuitContext

{ - const currentPrivateState = contract.getCurrentPrivateState(); - const originalState = contract.getCurrentContractState(); + const currentPrivateState = contract.getPrivateState(); + const originalState = contract.getContractState(); const contractAddress = contract.contractAddress; return { diff --git a/contracts/src/access/witnesses/ZOwnablePKWitnesses.ts b/contracts/src/access/witnesses/ZOwnablePKWitnesses.ts new file mode 100644 index 00000000..62cc3bba --- /dev/null +++ b/contracts/src/access/witnesses/ZOwnablePKWitnesses.ts @@ -0,0 +1,68 @@ +import { getRandomValues } from 'node:crypto'; +import type { WitnessContext } from '@midnight-ntwrk/compact-runtime'; +import type { Ledger } from '../../../artifacts/MockZOwnablePK/contract/index.cjs'; + +/** + * @description Interface defining the witness methods for ZOwnablePK operations. + * @template P - The private state type. + */ +export interface IZOwnablePKWitnesses

{ + /** + * Retrieves the secret nonce from the private state. + * @param context - The witness context containing the private state. + * @returns A tuple of the private state and the secret nonce as a Uint8Array. + */ + wit_secretNonce(context: WitnessContext): [P, Uint8Array]; +} + +/** + * @description Represents the private state of an ownable contract, storing a secret nonce. + */ +export type ZOwnablePKPrivateState = { + /** @description A 32-byte secret nonce used as a privacy additive. */ + secretNonce: Buffer; +}; + +/** + * @description Utility object for managing the private state of an Ownable contract. + */ +export const ZOwnablePKPrivateState = { + /** + * @description Generates a new private state with a random secret nonce. + * @returns A fresh ZOwnablePKPrivateState instance. + */ + generate: (): ZOwnablePKPrivateState => { + return { secretNonce: getRandomValues(Buffer.alloc(32)) }; + }, + + /** + * @description Generates a new private state with a user-defined secret nonce. + * Useful for deterministic nonce generation or advanced use cases. + * + * @param nonce - The 32-byte secret nonce to use. + * @returns A fresh ZOwnablePKPrivateState instance with the provided nonce. + * + * @example + * ```typescript + * // For deterministic nonces (user-defined scheme) + * const deterministicNonce = myDeterministicScheme(...); + * const privateState = ZOwnablePKPrivateState.withNonce(deterministicNonce); + * ``` + */ + withNonce: (nonce: Buffer): ZOwnablePKPrivateState => { + return { secretNonce: nonce }; + }, +}; + +/** + * @description Factory function creating witness implementations for Ownable operations. + * @returns An object implementing the Witnesses interface for ZOwnablePKPrivateState. + */ +export const ZOwnablePKWitnesses = + (): IZOwnablePKWitnesses => ({ + wit_secretNonce( + context: WitnessContext, + ): [ZOwnablePKPrivateState, Uint8Array] { + return [context.privateState, context.privateState.secretNonce]; + }, + }); diff --git a/docs/modules/ROOT/pages/access.adoc b/docs/modules/ROOT/pages/access.adoc index f39581d5..7978c1fa 100644 --- a/docs/modules/ROOT/pages/access.adoc +++ b/docs/modules/ROOT/pages/access.adoc @@ -133,6 +133,254 @@ there is no direct way for a contract to call circuits of other contracts or tra NOTE: The unsafe circuits are planned to become deprecated once contract-to-contract calls become available. +== Shielded Ownership and `ZOwnablePK` + +Privacy-preserving access control is a fundamental building block for confidential smart contracts on Midnight. +While traditional ownership patterns expose the owner's identity on-chain, +many applications require administrative control without revealing who holds that authority. + +=== Privacy-First Ownership + +The most common approach to access control in traditional smart contracts is ownership: +there's an account that is the owner of a contract and can perform administrative tasks. +However, this approach reveals the owner's identity to all observers, creating privacy and security risks. +In privacy-sensitive applications—such as confidential voting systems, private treasuries, or anonymous governance—revealing the administrator's identity may compromise the entire system's confidentiality. +This library provides the `ZOwnablePK` module that implements shielded ownership—administrative control without identity disclosure. +The owner's public key is never revealed on-chain; instead, +the contract stores only a cryptographic commitment that proves ownership without exposing the underlying identity. + +=== Commitment Scheme + +The `ZOwnablePK` module employs a two-layer cryptographic commitment scheme designed to provide privacy, +unlinkability, and collision resistance across deployments and ownership transfers. + +==== Owner ID Computation + +The foundation of the system is the owner identifier, computed as: + +```ts +id = SHA256(pk, nonce) +``` + +Where `pk` is the owner's public key and `nonce` is a secret value that may be either randomly generated +for maximum privacy or deterministically derived for recoverability. +This identifier serves as a privacy-preserving alternative to exposing the raw public key, +ensuring the owner's identity remains confidential. + +==== Owner Commitment Computation + +The final ownership commitment stored on-chain is computed as: + +```ts +commitment = SHA256(id, instanceSalt, counter, pad(32, "ZOwnablePK:shield:")) +``` + +This multi-element hash provides several security properties: + +- `id`: The privacy-preserving owner identifier described above. +- `instanceSalt`: A unique per-deployment salt that prevents commitment collisions across different contract instances, even when the same owner and nonce are used. +- `counter`: Incremented with each ownership transfer to ensure unlinkability—each transfer produces a completely different commitment even with the same underlying owner. +- `pad(32, "ZOwnablePK:shield:")`: A domain separator padded to 32 bytes that prevents hash collisions with other commitment schemes and enables safe protocol extensions. + +==== Security Properties + +This commitment scheme ensures that: + +- Public keys are never revealed on-chain. +- Observers cannot correlate past and future ownership. +- Cross-contract collisions are prevented through instance-specific salting. + +=== Nonce Generation Strategies + +The choice of nonce generation strategy represents a fundamental trade-off between simplicity/security and recoverability. +Both approaches are valid, and the best choice depends on your specific threat model and operational requirements. + +==== Random Nonce + +Generating a cryptographically strong random nonce provides the strongest privacy guarantees: + +```typescript +const randomNonce = crypto.getRandomValues(new Uint8Array(32)); +const ownerId = ZOwnablePK._computeOwnerId(publicKey, randomNonce); +``` + +This approach is easy to generate and ensures maximum unlinkability—even with sophisticated analysis, +observers cannot correlate ownership across different contracts or time periods. +However, it requires secure backup of both the private key and the nonce. +*Loss of either component results in permanent, irrecoverable loss of ownership.* + +==== Deterministic Nonce + +:rfc6979: https://datatracker.ietf.org/doc/html/rfc6979[RFC 6979] +:ed25519: https://ed25519.cr.yp.to/[Ed25519] + +Deriving the nonce deterministically enables recovery through derivation schemes. +Some examples: + +- `H(passphrase + context)` - recoverable from passphrase only, but passphrase becomes critical single point of failure. +- `H(publicKey + userPassphrase + context)` - requires both public key and passphrase. +- `H(signature + context)` where `signature = sign(context)` - leverages wallet without exposing private key. + +WARNING: When using signature-based nonce derivation, +ensure the wallet/library uses deterministic signatures ({ed25519} or {rfc6979} for ECDSA). +Non-deterministic signatures will generate different nonces on each signing, making recovery impossible. +Test the implementation by signing the same message twice then verify that the signatures match. + +*Context-Dependent Derivations:* + +- Include contract address, deployment timestamp, user ID, etc. +- Trade-off: more context is more unique but harder to recreate. + +WARNING: Approaches that avoid private key exposure (public key + passphrase, signature-based) +are generally recommended for operational security. + +Deriving the nonce deterministically from an <> and user passphrase provides a balance of security and recoverability: + +```typescript +// Example: Scrypt-based derivation +import { scryptSync } from 'node:crypto'; + +const deterministicNonce = scryptSync( + userPassphrase + publicKey + ":ZOwnablePK:nonce:v1", + 32, + { N: 16384, r: 8, p: 1 } // Standard scrypt parameters +); +const recoverableOwnerId = ZOwnablePK._computeOwnerId(publicKey, deterministicNonce); +``` + +**Security Considerations** + +The `ZOwnablePK` module remains agnostic to nonce generation methods, placing the security/convenience decision entirely with the user. Key considerations include: + +- **Backup requirements**: Random nonces require additional secure storage. +- **Recovery scenarios**: Deterministic nonces enable recovery. +- **Cross-contract correlation**: Reusing nonce strategies may reduce privacy across deployments. +- **Rotation costs**: Changing nonces requires ownership transfer transactions with associated DUST costs. + +Users should carefully evaluate their threat model, operational requirements, +and privacy needs when selecting a nonce generation strategy, +as this choice cannot be easily changed without transferring ownership. + +=== Air-Gapped Public Key (AGPK) + +For maximum privacy guarantees, +users should employ an Air-Gapped Public Key (AGPK) exclusively for contract ownership and administrative circuits. +An AGPK is a public key that maintains complete isolation from all other on-chain activities, +similar to how air-gapped systems are isolated from networks to prevent data leakage. + +==== The Privacy Enhancement + +While `ZOwnablePK` provides cryptographic privacy through its commitment scheme, +operational security practices like using an AGPK provide an additional layer of protection against correlation attacks. Even with the strongest cryptographic commitments, +reusing a public key across different on-chain activities can potentially compromise privacy +through transaction pattern analysis. + +==== AGPK Principles + +An Air-Gapped Public Key must adhere to strict isolation principles: + +- *Never used before:* The private key material +(including any seed, parent key, or entropy source from which this key is derived) +has never generated any public key that appears in any on-chain transaction, across any blockchain network. +The key material must be cryptographically pure. +- *Never used elsewhere:* From the moment of AGPK generation until its destruction, +the private key material is used exclusively for this contract's administrative functions (i.e. `assertOnlyOwner`). +No other public keys may ever be derived from or generated with the same key material. +- *Never used again:* Users commit to destroying all copies of the private key material +upon ownership renunciation or transfer. +This relies entirely on user discipline and cannot be externally verified or enforced. + +==== Best Practices Recommendation + +While neither required nor enforced by the `ZOwnablePK` module, +an Air-Gapped Public Key provides strong operational privacy hygiene for shielded contract administration. +Users should evaluate their threat model and privacy requirements when deciding whether to implement AGPK practices. + +WARNING: The effectiveness of an AGPK depends entirely on abiding by the AGPK principles. +A single transaction using the key outside the administrative context compromises all privacy benefits. + +=== Usage + +Import the `ZOwnablePK` module into the implementing contract and expose the ownership-handling circuits. +It’s recommended to prefix the module with `ZOwnablePK_` to avoid circuit signature clashes. + +```typescript +// MyZOwnablePKContract.compact + +pragma language_version >= 0.16.0; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin-compact/contracts/src/access/ZOwnablePK" + prefix ZOwnablePK_; + +constructor( + initOwnerCommitment: Bytes<32>, + instanceSalt: Bytes<32>, +) { + ZOwnablePK_initialize(initOwnerCommitment, instanceSalt); +} + +export circuit owner(): Bytes<32> { + return ZOwnablePK_owner(); +} + +export circuit transferOwnership(newOwnerCommitment: Bytes<32>): [] { + return ZOwnablePK_transferOwnership(disclose(newOwnerCommitment)); +} + +export circuit renounceOwnership(): [] { + return ZOwnablePK_renounceOwnership(); +} +``` + +Similar to the Ownable module, +circuits can be protected so that only the contract owner may them by adding `assertOnlyOwner` +as the first line in the circuit body like this: + +```typescript +export circuit mySensitiveCircuit(): [] { + ZOwnablePK_assertOnlyOwner(); + + // Do something +} +``` + +This covers the basics for creating a contract, but before deploying the contract, +the owner's id must be derived for the commitment scheme because it's required to deploy the contract. + +First, the owner needs to generate a secret nonce that's stored in the owner's private state. +See <>. + +Once the owner has the secret nonce generated, they can insert their public key and nonce into the following: + +```typescript +import { + CompactTypeBytes, + CompactTypeVector, + persistentHash, +} from '@midnight-ntwrk/compact-runtime'; +import { getRandomValues } from 'node:crypto'; + +// Owner ID +const generateId = ( + pk: Uint8Array, + nonce: Uint8Array, +): Uint8Array => { + const rt_type = new CompactTypeVector(2, new CompactTypeBytes(32)); + return persistentHash(rt_type, [pk, nonce]); +}; + +// Instance salt for the constructor +const generateInstanceSalt = (): Uint8Array => { + return getRandomValues(new Uint8Array(32)); +} +``` + +TIP: Another way to get the user ID is to expose `_computeOwnerId` in the contract +and call this circuit off chain through a contract simulator. +Be on the lookout for future tooling that makes this process easier. + == Role-Based Access Control While the simplicity of _ownership_ can be useful for simple systems or quick prototyping, different levels of authorization are often needed. diff --git a/docs/modules/ROOT/pages/api/access.adoc b/docs/modules/ROOT/pages/api/access.adoc index 7237e42b..dd900b8a 100644 --- a/docs/modules/ROOT/pages/api/access.adoc +++ b/docs/modules/ROOT/pages/api/access.adoc @@ -1,6 +1,8 @@ :github-icon: pass:[] -:accessControl-guide: xref:accessControl.adoc[AccessControl guide] -:ownable-guide: xref:ownable.adoc[Ownable guide] +:accessControl-guide: xref:access.adoc#role_based_access_control[AccessControl guide] +:ownable-guide: xref:access.adoc#ownership_and_ownable[Ownable guide] +:zownablepk-guide: xref:access.adoc#shielded_ownership_and_zownablepk[ZOwnablePK guide] +:agpk: xref:access.adoc#air_gapped_public_key_agpk[Air-Gapped Public Key] :grantRole: <> :revokeRole: <> @@ -12,6 +14,8 @@ This directory provides ways to restrict who can access the circuits of a contra - `<>` is a simpler mechanism with a single owner "role" that can be assigned to a single account. This simpler mechanism can be useful for quick tests but projects with production concerns are likely to outgrow it. +- `<>` provides a privacy-preserving single owner access control mechanism using cryptographic commitments. The owner's public key is never revealed on-chain, instead storing only a commitment that proves ownership without exposing identity, suitable for applications requiring administrative control with strong privacy guarantees. + == Core [.contract] @@ -399,3 +403,201 @@ Requirements: Constraints: - k=10, rows=216 + +[.contract] +[[ZOwnablePK]] +=== `++ZOwnablePK++` link:https://github.com/OpenZeppelin/compact-contracts/blob/main/contracts/ownable/src/ZOwnablePK.compact[{github-icon},role=heading-link] + +[.hljs-theme-dark] +```ts +import "./node_modules/@openzeppelin-compact/contracts/src/access/ZOwnablePK"; +``` + +`ZOwnablePK` provides a privacy-preserving access control mechanism for contracts with a single administrative user. Unlike traditional `Ownable` implementations that store or expose the owner's public key on-chain, +this module stores only a commitment to a hashed identifier derived from the owner's public key and a secret nonce. +For the strongest security guarantees, use an {agpk}. + +Ownable provides a basic access control mechanism where an account (an owner) can be granted exclusive access to specific circuits. + +This module includes <> to restrict a circuit to be used only by the owner. + +TIP: For an overview of the module, read the {zownablepk-guide}. + +[.contract-index] +.Circuits +-- + +[.sub-index#ZOwnablePKModule] +* xref:#ZOwnablePK-initialize[`++initialize(ownerId, instanceSalt)++`] +* xref:#ZOwnablePK-owner[`++owner()++`] +* xref:#ZOwnablePK-transferOwnership[`++transferOwnership(newOwnerId)++`] +* xref:#ZOwnablePK-renounceOwnership[`++renounceOwnership()++`] +* xref:#ZOwnablePK-assertOnlyOwner[`++assertOnlyOwner()++`] +* xref:#ZOwnablePK-_computeOwnerCommitment[`++_computeOwnerCommitment(id, counter)++`] +* xref:#ZOwnablePK-_computeOwnerId[`++_computeOwnerId(pk, nonce)++`] +* xref:#ZOwnablePK-_transferOwnership[`++_transferOwnership(newOwnerId)++`] +-- + +[.contract-item] +[[ZOwnablePK-initialize]] +==== `[.contract-item-name]#++initialize++#++(initialOwner: Either) → []++` [.item-kind]#circuit# + +Initializes the contract by setting the initial owner via `ownerId` +and storing the `instanceSalt` that acts as a privacy additive +for preventing duplicate commitments among other contracts implementing `ZOwnablePK`. + +NOTE: The `ownerId` must be calculated prior to contract deployment. +See <> + +Requirements: + +- Contract is not already initialized. +- `ownerId` is not an empty array. + +Constraints: + +- k=14, rows=14933 + +[.contract-item] +[[ZOwnablePK-owner]] +==== `[.contract-item-name]#++owner++#++() → Bytes<32>++` [.item-kind]#circuit# + +Returns the current commitment representing the contract owner. +The full commitment is: `SHA256(SHA256(pk, nonce), instanceSalt, counter, domain)`. + +Requirements: + +- Contract is initialized. + +Constraints: + +- k=10, rows=57 + +[.contract-item] +[[ZOwnablePK-transferOwnership]] +==== `[.contract-item-name]#++transferOwnership++#++(newOwnerId: Bytes<32>) → []++` [.item-kind]#circuit# + +Transfers ownership of the contract to `newOwnerId`. +`newOwnerId` must be precalculated and given to the current owner off chain. + +Requirements: + +- Contract is initialized. +- Caller is the current contract owner. +- `newOwnerId` is not an empty array. + +Constraints: + +- k=16, rows=39240 + +[.contract-item] +[[ZOwnablePK-renounceOwnership]] +==== `[.contract-item-name]#++renounceOwnership++#++() → []++` [.item-kind]#circuit# + +Leaves the contract without an owner. +It will not be possible to call <> circuits anymore. +Can only be called by the current owner. + +Requirements: + +- Contract is initialized. +- Caller is the current owner. + +Constraints: + +- k=15, rows=24442 + +[.contract-item] +[[ZOwnablePK-assertOnlyOwner]] +==== `[.contract-item-name]#++assertOnlyOwner++#++() → []++` [.item-kind]#circuit# + +Throws if called by any account whose id hash `SHA256(pk, nonce)` does not match the stored owner commitment. +Use this to only allow the owner to call specific circuits. + +Requirements: + +- Contract is initialized. +- Caller's id (`SHA256(pk, nonce)`) when used in <> equals the stored `_ownerCommitment`, +thus verifying themselves as the owner. + +Constraints: + +- k=15, rows=24437 + +[.contract-item] +[[ZOwnablePK-_computeOwnerCommitment]] +==== `[.contract-item-name]#++_computeOwnerCommitment++#++(id: Bytes<32>, counter: Uint<64>) → Bytes<32>++` [.item-kind]#circuit# + +Computes the owner commitment from the given `id` and `counter`. + +**Owner ID (`id`)** + +The `id` is expected to be computed off-chain as: `id = SHA256(pk, nonce)` + +- `pk`: The owner's public key. +- `nonce`: A secret nonce scoped to the instance, ideally rotated with each transfer. + +**Commitment Derivation** + +`commitment = SHA256(id, instanceSalt, counter, domain)` + +- `id`: See above. +- `instanceSalt`: A unique per-deployment salt, stored during initialization. +This prevents commitment collisions across deployments. +- `counter`: Incremented with each ownership transfer, ensuring uniqueness even with repeated `id` values. +Cast to `Field` then `Bytes<32>` for hashing. +- `domain`: Domain separator `"ZOwnablePK:shield:"` (padded to 32 bytes) to prevent hash collisions +when extending the module or using similar commitment schemes. + +Requirements: + +- Contract is initialized. + +Constraints: + +- k=14, rows=14853 + +[.contract-item] +[[ZOwnablePK-_computeOwnerId]] +==== `[.contract-item-name]#++_computeOwnerId++#++(pk: Either, nonce: Bytes<32>) → Bytes<32>++` [.item-kind]#circuit# + +Computes the unique identifier (`id`) of the owner from their public key and a secret nonce. + +**ID Derivation** +`id = SHA256(pk, nonce)` + +- `pk`: The public key of the caller. +This is passed explicitly to allow for off-chain derivation, testing, or scenarios +where the caller is different from the subject of the computation. +We recommend using an {agpk}. +- `nonce`: A secret nonce tied to the identity. +This value should be randomly generated and kept private. +It may be rotated periodically for enhanced unlinkability. + +The result is a 32-byte commitment that uniquely identifies the owner. +This value is later used in owner commitment hashing, +and acts as a privacy-preserving alternative to a raw public key. + +NOTE: This module allows ownership to be tied to an identity commitment derived from a public key and secret nonce. +While typically used with user public keys, +this mechanism may also support contract addresses as identifiers in future contract-to-contract interactions. +Both are treated as 32-byte values (`Bytes<32>`). + +Requirements: + +- Contract is initialized. +- `pk` is not a ContractAddress. + +[.contract-item] +[[ZOwnablePK-_transferOwnership]] +==== `[.contract-item-name]#++_transferOwnership++#++(newOwnerId: Bytes<32>) → []++` [.item-kind]#circuit# + +Transfers ownership to owner id `newOwnerId` without enforcing permission checks on the caller. + +Requirements: + +- Contract is initialized. + +Constraints: + +- k=14, rows=14823 From c16a6beb813f2701e58bf2bdb12d150bb5fccecb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Fri, 29 Aug 2025 17:32:24 -0400 Subject: [PATCH 161/322] Construct simulator and witnesses for impl --- .../src/access/ShieldedAccessControl.compact | 17 +- .../mocks/MockShieldedAccessControl.compact | 9 +- .../ShieldedAccessControlSimulator.ts | 474 ++++++++++-------- .../ShieldedAccessControlWitnesses.ts | 296 ++++------- 4 files changed, 386 insertions(+), 410 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index d9870c24..79598fe6 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -112,13 +112,13 @@ module ShieldedAccessControl { * @param {Uint<64>} index - An index in the `_operatorRoles` Merkle tree * @return {MerkleTreePath<10, Bytes<32>>} - The Merkle path of `roleCommitment` in the `_operatorRoles` Merkle tree  */ - witness getRoleCommitmentPath(roleCommitment: Bytes<32>): MerkleTreePath<10, Bytes<32>>; + witness wit_getRoleCommitmentPath(roleCommitment: Bytes<32>): MerkleTreePath<10, Bytes<32>>; - witness secretNonce(roleId: Bytes<32>): Bytes<32>; + witness wit_secretNonce(roleId: Bytes<32>): Bytes<32>; - witness getRoleCommitmentIndex(roleId: Bytes<32>): Uint<64>; + witness wit_getRoleIndex(roleId: Bytes<32>): Uint<64>; - struct Role { + export struct Role { hasRole: Boolean; roleCommitment: Bytes<32>; } @@ -253,15 +253,15 @@ module ShieldedAccessControl { * produced by SHA256(roleId | account | nonce) exists in the `_operatorRoles` Merkle tree */ export circuit _checkMerkleTree(roleId: Bytes<32>, account: Bytes<32>): Role { - const nonce = secretNonce(roleId); - const index = getMerkleTreeIndex(roleId); + const nonce = wit_secretNonce(roleId); + const index = wit_getRoleIndex(roleId); const roleCommitment = persistentHash>>([roleId, account, nonce, index as Field as Bytes<32>]); assert(!_roleCommitmentNullifiers.member(disclose(roleCommitment)), "ShieldedAccessControl: role commitment access revoked"); - const authPath = getRoleCommitmentPath(roleCommitment); + const authPath = wit_getRoleCommitmentPath(roleCommitment); const hasRole = _operatorRoles .checkRoot(merkleTreePathRoot<10, Bytes<32>>(disclose(authPath))); - return Role {hasRole, roleCommitment}; + return Role {hasRole, disclose(roleCommitment)}; } /** @@ -473,6 +473,7 @@ module ShieldedAccessControl { if (!Utils_isContractAddress(account)) { const zswapPubKey = account.left.bytes; + // Use ledger index as source of truth _operatorRoles.insertHashIndex(disclose(role.roleCommitment), _currentMerkleTreeIndex); _currentMerkleTreeIndex.increment(1); return true; diff --git a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact index ac009ca3..78c36a96 100644 --- a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact +++ b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact @@ -11,11 +11,14 @@ export { ContractAddress, Either, Maybe, + MerkleTreePath, ShieldedAccessControl_DEFAULT_ADMIN_ROLE, - ShieldedAccessControl__operatorRoles + ShieldedAccessControl__operatorRoles, + ShieldedAccessControl__currentMerkleTreeIndex, + ShieldedAccessControl_Role }; -export circuit hasRole(roleId: Bytes<32>, account: Either): Boolean { +export circuit hasRole(roleId: Bytes<32>, account: Either): ShieldedAccessControl_Role { return ShieldedAccessControl_hasRole(roleId, account); } @@ -27,7 +30,7 @@ export circuit _checkRole(roleId: Bytes<32>, account: Either, account: Bytes<32>): Boolean { +export circuit _checkMerkleTree(roleId: Bytes<32>, account: Bytes<32>): ShieldedAccessControl_Role { return ShieldedAccessControl__checkMerkleTree(roleId, account); } diff --git a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts index b71d4cf4..d194a5a3 100644 --- a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts +++ b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts @@ -1,9 +1,6 @@ import { type CircuitContext, type CoinPublicKey, - type ContractState, - QueryContext, - constructorContext, emptyZswapLocalState, } from '@midnight-ntwrk/compact-runtime'; import { sampleContractAddress } from '@midnight-ntwrk/zswap'; @@ -11,287 +8,344 @@ import { type ContractAddress, type Either, type Ledger, + ledger, Contract as MockShieldedAccessControl, type ZswapCoinPublicKey, - ledger, -} from '../../shieldedAccessControl/src/artifacts/MockShieldedAccessControl/contract/index.cjs'; // Combined imports + type ShieldedAccessControl_Role as Role +} from '../../../../artifacts/MockShieldedAccessControl/contract/index.cjs'; import { - type ShieldedAccessControlPrivateState, + ShieldedAccessControlPrivateState, ShieldedAccessControlWitnesses, } from '../../witnesses/ShieldedAccessControlWitnesses.js'; -import type { IContractSimulator } from '../../shieldedAccessControl/src/test/types/test.js'; +import type { + ContextlessCircuits, + ExtractImpureCircuits, + ExtractPureCircuits, + SimulatorOptions, +} from '../types/test.js'; +import { AbstractContractSimulator } from '../utils/AbstractContractSimulator.js'; +import { SimulatorStateManager } from '../utils/SimualatorStateManager.js'; + +type ShieldedAccessControlSimOptions = SimulatorOptions< + ShieldedAccessControlPrivateState, + typeof ShieldedAccessControlWitnesses +>; /** - * @description A simulator implementation of a AccessControl contract for testing purposes. + * @description A simulator implementation of a contract for testing purposes. * @template P - The private state type, fixed to ShieldedAccessControlPrivateState. * @template L - The ledger type, fixed to Contract.Ledger. */ -export class AccessControlSimulator - implements IContractSimulator { - /** @description The underlying contract instance managing contract logic. */ - readonly contract: MockShieldedAccessControl; - - /** @description The deployed address of the contract. */ +export class ShieldedAccessControlSimulator extends AbstractContractSimulator< + ShieldedAccessControlPrivateState, + Ledger +> { + contract: MockShieldedAccessControl; readonly contractAddress: string; + private stateManager: SimulatorStateManager; + private callerOverride: CoinPublicKey | null = null; + private _witnesses: ReturnType; - /** @description The current circuit context, updated by contract operations. */ - circuitContext: CircuitContext; + private _pureCircuitProxy?: ContextlessCircuits< + ExtractPureCircuits>, + ShieldedAccessControlPrivateState + >; - /** - * @description Initializes the mock contract. - */ - constructor() { - this.contract = new MockShieldedAccessControl( - ShieldedAccessControlWitnesses, - ); + private _impureCircuitProxy?: ContextlessCircuits< + ExtractImpureCircuits>, + ShieldedAccessControlPrivateState + >; + + constructor( + initUser: Uint8Array, + options: ShieldedAccessControlSimOptions = {}, + ) { + super(); + + // Setup initial state const { - currentPrivateState, - currentContractState, - currentZswapLocalState, - } = this.contract.initialState(constructorContext({}, '0'.repeat(64))); - this.circuitContext = { - currentPrivateState, - currentZswapLocalState, - originalState: currentContractState, - transactionContext: new QueryContext( - currentContractState.data, - sampleContractAddress(), - ), - }; + privateState = ShieldedAccessControlPrivateState.generate(initUser), + witnesses = ShieldedAccessControlWitnesses(), + coinPK = '0'.repeat(64), + address = sampleContractAddress(), + } = options; + + this.contract = new MockShieldedAccessControl(witnesses); + + this.stateManager = new SimulatorStateManager( + this.contract, + privateState, + coinPK, + address, + [], + ); this.contractAddress = this.circuitContext.transactionContext.address; + this._witnesses = witnesses; + this.contract = new MockShieldedAccessControl(this._witnesses); } - /** - * @description Retrieves the current public ledger state of the contract. - * @returns The ledger state as defined by the contract. - */ - public getCurrentPublicState(): Ledger { + get circuitContext() { + return this.stateManager.getContext(); + } + + set circuitContext(ctx) { + this.stateManager.setContext(ctx); + } + + getPublicState(): Ledger { return ledger(this.circuitContext.transactionContext.state); } /** - * @description Retrieves the current private state of the contract. - * @returns The private state of type ShieldedAccessControlPrivateState. + * @description Constructs a caller-specific circuit context. + * If a caller override is present, it replaces the current Zswap local state with an empty one + * scoped to the overridden caller. Otherwise, the existing context is reused as-is. + * @returns A circuit context adjusted for the current simulated caller. */ - public getCurrentPrivateState(): ShieldedAccessControlPrivateState { - return this.circuitContext.currentPrivateState; + protected getCallerContext(): CircuitContext { + return { + ...this.circuitContext, + currentZswapLocalState: this.callerOverride + ? emptyZswapLocalState(this.callerOverride) + : this.circuitContext.currentZswapLocalState, + }; } /** - * @description Retrieves the current contract state. - * @returns The contract state object. + * @description Initializes and returns a proxy to pure contract circuits. + * The proxy automatically injects the current circuit context into each call, + * and returns only the result portion of each circuit's output. + * @notice The proxy is created only when first accessed a.k.a lazy initialization. + * This approach is efficient in cases where only pure or only impure circuits are used, + * avoiding unnecessary proxy creation. + * @returns A proxy object exposing pure circuit functions without requiring explicit context. */ - public getCurrentContractState(): ContractState { - return this.circuitContext.originalState; + protected get pureCircuit(): ContextlessCircuits< + ExtractPureCircuits>, + ShieldedAccessControlPrivateState + > { + if (!this._pureCircuitProxy) { + this._pureCircuitProxy = this.createPureCircuitProxy< + MockShieldedAccessControl['circuits'] + >(this.contract.circuits, () => this.circuitContext); + } + return this._pureCircuitProxy; } /** - * @description Retrieves an account's permission for `roleId`. - * @param roleId - The role identifier. - * @param account - A ZswapCoinPublicKey or a ContractAddress. - * @returns Whether an account has a specified role. + * @description Initializes and returns a proxy to impure contract circuits. + * The proxy automatically injects the current (possibly caller-modified) context into each call, + * and updates the circuit context with the one returned by the circuit after execution. + * @notice The proxy is created only when first accessed a.k.a. lazy initialization. + * This approach is efficient in cases where only pure or only impure circuits are used, + * avoiding unnecessary proxy creation. + * @returns A proxy object exposing impure circuit functions without requiring explicit context management. */ - public hasRole( - roleId: Uint8Array, - account: Either, - ): boolean { - return this.contract.impureCircuits.hasRole( - this.circuitContext, - roleId, - account, - ).result; + protected get impureCircuit(): ContextlessCircuits< + ExtractImpureCircuits>, + ShieldedAccessControlPrivateState + > { + if (!this._impureCircuitProxy) { + this._impureCircuitProxy = this.createImpureCircuitProxy< + MockShieldedAccessControl['impureCircuits'] + >( + this.contract.impureCircuits, + () => this.getCallerContext(), + (ctx: any) => { + this.circuitContext = ctx; + }, + ); + } + return this._impureCircuitProxy; } /** - * @description Retrieves an account's permission for `roleId`. - * @param caller - Optional. Sets the caller context if provided. - * @param roleId - The role identifier. + * @description Resets the cached circuit proxy instances. + * This is useful if the underlying contract state or circuit context has changed, + * and you want to ensure the proxies are recreated with updated context on next access. */ - public assertOnlyRole(roleId: Uint8Array, caller?: CoinPublicKey) { - const res = this.contract.impureCircuits.assertOnlyRole( - { - ...this.circuitContext, - currentZswapLocalState: caller - ? emptyZswapLocalState(caller) - : this.circuitContext.currentZswapLocalState, - }, - roleId, - ); - - this.circuitContext = res.context; + public resetCircuitProxies(): void { + this._pureCircuitProxy = undefined; + this._impureCircuitProxy = undefined; } /** - * @description Retrieves an account's permission for `roleId`. - * @param roleId - The role identifier. - * @param account - A ZswapCoinPublicKey or a ContractAddress. + * @description Helper method that provides access to both pure and impure circuit proxies. + * These proxies automatically inject the appropriate circuit context when invoked. + * @returns An object containing `pure` and `impure` circuit proxy interfaces. */ - public _checkRole( - roleId: Uint8Array, - account: Either, + public get circuits() { + return { + pure: this.pureCircuit, + impure: this.impureCircuit, + }; + } + + public get witnesses(): ReturnType { + return this._witnesses; + } + + public set witnesses(newWitnesses: ReturnType) { + this._witnesses = newWitnesses; + this.contract = new MockShieldedAccessControl(this._witnesses); + } + + public overrideWitness( + key: K, + fn: (typeof this._witnesses)[K], ) { - this.circuitContext = this.contract.impureCircuits._checkRole( - this.circuitContext, - roleId, - account, - ).context; + this.witnesses = { + ...this._witnesses, + [key]: fn, + }; } /** - * @description Retrieves `roleId`'s admin identifier. - * @param roleId - The role identifier. - * @returns The admin identifier for `roleId`. + * @description Returns the current commitment representing the contract owner. + * The full commitment is: `SHA256(SHA256(pk, nonce), instanceSalt, counter, domain)`. + * @returns The current owner's commitment. */ - public getRoleAdmin(roleId: Uint8Array): Uint8Array { - return this.contract.impureCircuits.getRoleAdmin( - this.circuitContext, - roleId, - ).result; + public hasRole(roleId: Uint8Array, account: Either): Role { + return this.circuits.impure.hasRole(roleId, account); } /** - * @description Grants an account permissions to use `roleId`. - * @param caller - Optional. Sets the caller context if provided. - * @param roleId - The role identifier. - * @param account - A ZswapCoinPublicKey or a ContractAddress. + * @description Transfers ownership to `newOwnerId`. + * `newOwnerId` must be precalculated and given to the current owner off chain. + * @param newOwnerId The new owner's unique identifier (`SHA256(pk, nonce)`). */ - public grantRole( - roleId: Uint8Array, - account: Either, - caller?: CoinPublicKey, - ) { - const res = this.contract.impureCircuits.grantRole( - { - ...this.circuitContext, - currentZswapLocalState: caller - ? emptyZswapLocalState(caller) - : this.circuitContext.currentZswapLocalState, - }, - roleId, - account, - ); - - this.circuitContext = res.context; + public assertOnlyRole(roleId: Uint8Array) { + this.circuits.impure.assertOnlyRole(roleId); } /** - * @description Revokes an account's permission to use `roleId`. - * @param caller - Optional. Sets the caller context if provided. - * @param roleId - The role identifier. - * @param account - A ZswapCoinPublicKey or a ContractAddress. + * @description Leaves the contract without an owner. + * It will not be possible to call `assertOnlyOnwer` circuits anymore. + * Can only be called by the current owner. */ - public revokeRole( - roleId: Uint8Array, - account: Either, - caller?: CoinPublicKey, - ) { - const res = this.contract.impureCircuits.revokeRole( - { - ...this.circuitContext, - currentZswapLocalState: caller - ? emptyZswapLocalState(caller) - : this.circuitContext.currentZswapLocalState, - }, - roleId, - account, - ); - - this.circuitContext = res.context; + public _checkRole(roleId: Uint8Array, account: Either) { + this.circuits.impure._checkRole(roleId, account); } /** - * @description Revokes `roleId` from the calling account. - * @param caller - Optional. Sets the caller context if provided. - * @param roleId - The role identifier. - * @param account - A ZswapCoinPublicKey or a ContractAddress. + * @description Throws if called by any account whose id hash `SHA256(pk, nonce)` does not match + * the stored owner commitment. Use this to only allow the owner to call specific circuits. */ - public renounceRole( - roleId: Uint8Array, - account: Either, - caller?: CoinPublicKey, - ) { - const res = this.contract.impureCircuits.renounceRole( - { - ...this.circuitContext, - currentZswapLocalState: caller - ? emptyZswapLocalState(caller) - : this.circuitContext.currentZswapLocalState, - }, - roleId, - account, - ); + public _checkMerkleTree(roleId: Uint8Array, account: Uint8Array): Role { + return this.circuits.impure._checkMerkleTree(roleId, account); + } - this.circuitContext = res.context; + /** + * @description Computes the owner commitment from the given `id` and `counter`. + * @param id - The unique identifier of the owner calculated by `SHA256(pk, nonce)`. + * @param counter - The current counter or round. This increments by `1` + * after every transfer to prevent duplicate commitments given the same `id`. + * @returns The commitment derived from `id` and `counter`. + */ + public getRoleAdmin(roleId: Uint8Array): Uint8Array { + return this.circuits.impure.getRoleAdmin(roleId); } /** - * @description Sets the admin identifier for `roleId`. - * @param roleId - The role identifier. - * @param adminId - The admin role identifier. + * @description Computes the unique identifier (`id`) of the owner from their + * public key and a secret nonce. + * @param pk - The public key of the identity being committed. + * @param nonce - A private nonce to scope the commitment. + * @returns The computed owner ID. */ - public _setRoleAdmin(roleId: Uint8Array, adminId: Uint8Array) { - this.circuitContext = this.contract.impureCircuits._setRoleAdmin( - this.circuitContext, - roleId, - adminId, - ).context; + public grantRole(roleId: Uint8Array, account: Either) { + this.circuits.impure.grantRole(roleId, account); } /** - * @description Grants an account permissions to use `roleId`. Internal function without access restriction. - * @param roleId - The role identifier. - * @param account - A ZswapCoinPublicKey or a ContractAddress. + * @description Transfers ownership to owner id `newOwnerId` without + * enforcing permission checks on the caller. + * @param newOwnerId - The unique identifier of the new owner calculated by `SHA256(pk, nonce)`. */ - public _grantRole( - roleId: Uint8Array, - account: Either, - ): boolean { - const res = this.contract.impureCircuits._grantRole( - this.circuitContext, - roleId, - account, - ); + public revokeRole(roleId: Uint8Array, account: Either) { + this.circuits.impure.revokeRole(roleId, account); + } - this.circuitContext = res.context; - return res.result; + /** + * @description Transfers ownership to owner id `newOwnerId` without + * enforcing permission checks on the caller. + * @param newOwnerId - The unique identifier of the new owner calculated by `SHA256(pk, nonce)`. + */ + public renounceRole(roleId: Uint8Array, callerConfirmation: Either) { + this.circuits.impure.renounceRole(roleId, callerConfirmation); } /** - * @description Grants an account permissions to use `roleId`. Internal function without access restriction. - * DOES NOT restrict sending to a ContractAddress. - * @param roleId - The role identifier. - * @param account - A ZswapCoinPublicKey or a ContractAddress. + * @description Transfers ownership to owner id `newOwnerId` without + * enforcing permission checks on the caller. + * @param newOwnerId - The unique identifier of the new owner calculated by `SHA256(pk, nonce)`. */ - public _unsafeGrantRole( - roleId: Uint8Array, - account: Either, - ): boolean { - const res = this.contract.impureCircuits._unsafeGrantRole( - this.circuitContext, - roleId, - account, - ); + public _setRoleAdmin(roleId: Uint8Array, adminRole: Uint8Array) { + this.circuits.impure._setRoleAdmin(roleId, adminRole); + } - this.circuitContext = res.context; - return res.result; + /** + * @description Transfers ownership to owner id `newOwnerId` without + * enforcing permission checks on the caller. + * @param newOwnerId - The unique identifier of the new owner calculated by `SHA256(pk, nonce)`. + */ + public _grantRole(roleId: Uint8Array, account: Either): Boolean { + return this.circuits.impure._grantRole(roleId, account); } /** - * @description Revokes an account's permission to use `roleId`. Internal function without access restriction. - * @param roleId - The role identifier. - * @param account - A ZswapCoinPublicKey or a ContractAddress. + * @description Transfers ownership to owner id `newOwnerId` without + * enforcing permission checks on the caller. + * @param newOwnerId - The unique identifier of the new owner calculated by `SHA256(pk, nonce)`. */ - public _revokeRole( - roleId: Uint8Array, - account: Either, - ): boolean { - const res = this.contract.impureCircuits._revokeRole( - this.circuitContext, - roleId, - account, - ); + public _unsafeGrantRole(roleId: Uint8Array, account: Either): Boolean { + return this.circuits.impure._unsafeGrantRole(roleId, account); + } - this.circuitContext = res.context; - return res.result; + /** + * @description Transfers ownership to owner id `newOwnerId` without + * enforcing permission checks on the caller. + * @param newOwnerId - The unique identifier of the new owner calculated by `SHA256(pk, nonce)`. + */ + public _revokeRole(roleId: Uint8Array, account: Either): Boolean { + return this.circuits.impure._revokeRole(roleId, account); } + + public readonly privateState = { + /** + * @description Contextually sets a new nonce into the private state. + * @param newNonce The secret nonce. + * @returns The ZOwnablePK private state after setting the new nonce. + */ + injectSecretNonce: ( + roleId: Buffer, + newNonce: Buffer, + ): ShieldedAccessControlPrivateState => { + const currentState = this.stateManager.getContext().currentPrivateState; + const updatedState = { ...currentState, roles: { ...currentState.roles } } + const roleString = roleId.toString('hex'); + updatedState.roles[roleString] = newNonce; + this.stateManager.updatePrivateState(updatedState); + return updatedState; + }, + + /** + * @description Returns the secret nonce given the context. + * @returns The secret nonce. + */ + getCurrentSecretNonce: (roleId: Buffer): Uint8Array => { + const roleString = roleId.toString('hex'); + return this.stateManager.getContext().currentPrivateState.roles[roleString]; + }, + }; + + public callerCtx = { + /** + * @description Sets the caller context. + * @param caller The caller in context of the proceeding circuit calls. + */ + setCaller: (caller: CoinPublicKey) => { + this.callerOverride = caller; + }, + }; } diff --git a/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts b/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts index 4ad00147..586dcb2e 100644 --- a/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts +++ b/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts @@ -1,214 +1,132 @@ -import { Buffer } from 'node:buffer'; -import { - constructorContext, - decodeCoinPublicKey, - type MerkleTreePath, - QueryContext, - type WitnessContext, -} from '@midnight-ntwrk/compact-runtime'; -import { encodeContractAddress } from '@midnight-ntwrk/ledger'; -import { sampleContractAddress } from '@midnight-ntwrk/zswap'; -import { - type ContractAddress, - type Either, - type Ledger, - Contract as MockShieldedAccessControl, - type ZswapCoinPublicKey, -} from '../shieldedAccessControl/src/artifacts/MockShieldedAccessControl/contract/index.cjs'; // Combined imports +import { getRandomValues } from 'node:crypto'; +import { CompactTypeVector, CompactTypeBytes, persistentHash, type WitnessContext, convert_bigint_to_Uint8Array } from '@midnight-ntwrk/compact-runtime'; +import type { Ledger, MerkleTreePath } from '../../../artifacts/MockShieldedAccessControl/contract/index.cjs'; -const { hkdfSync } = await import('node:crypto'); - -const KEYLEN = 32; +const MERKLE_TREE_DEPTH = 2 ** 10; /** - * @description The respective `nonce` value for a given `roleId` should be at the same index - * for each array of `Buffer`s + * @description Interface defining the witness methods for ShieldedAccessControl operations. + * @template P - The private state type. */ -export type ShieldedAccessControlPrivateState = { - secretKey: Buffer; - nonces: Buffer[]; - roleIds: Buffer[]; -}; +export interface IShieldedAccessControlWitnesses

{ + /** + * Retrieves the secret nonce from the private state. + * @param context - The witness context containing the private state. + * @returns A tuple of the private state and the secret nonce as a Uint8Array. + */ + wit_secretNonce(context: WitnessContext, roleId: Uint8Array): [P, Uint8Array]; + wit_getRoleCommitmentPath(context: WitnessContext, roleCommitment: Uint8Array): [P, MerkleTreePath]; + wit_getRoleIndex(context: WitnessContext, roleId: Uint8Array): [P, bigint]; +} + +type RoleId = string; +type SecretNonce = Uint8Array; /** - * @description Generates a nonce value using the following scheme: HKDF-SHA256(SK, "role-nonce" | roleId | PK) - * @param secretKey - The secret key associated with the contract. - * @param roleId - The role identifier. - * @param salt - A salt value. - * @param account - The public key of an account. - * - * @returns A unique nonce value for `roleId` + * @description Represents the private state of an ownable contract, storing a secret nonce. */ -function generateNonce( - secretKey: Buffer, - roleId: Buffer, - salt: Buffer, - account: Buffer, -): Buffer { - const domainString = Buffer.from('role-nonce'); - const info = Buffer.concat([domainString, roleId, account]); - const nonce = hkdfSync('sha256', secretKey, salt, info, KEYLEN); - - return Buffer.from(nonce); -} +export type ShieldedAccessControlPrivateState = { + /** @description A 32-byte secret nonce used as a privacy additive. */ + roles: Record, + account: Uint8Array +}; /** - * @description A stub function that simulates a successful role approval - * @param account - The public key of an account. - * @param roleId - The role identifier. - * @param nonce - The nonce associated with `roleId`. - * - * @returns Whether the account was approved for a role + * @description Utility object for managing the private state of a Shielded AccessControl contract. */ -function sendRoleRequestToAdmin( - _account: Buffer, - _roleId: Buffer, - _nonce: Buffer, -) { - return true; -} - -export const ShieldedAccessControlWitnesses = { +export const ShieldedAccessControlPrivateState = { /** - * @description Typescript implementation of the `getRoleCommitmentPath` witness function. - * @param privateState - The current private state. - * @param ledger - A snapshot of the current ledger state. - * @param roleCommitment - The role commitment to query. - * @param index - The index of `roleCommitment`in the Merkle tree. - * - * @returns An array of the private state and the Merkle tree path of `roleCommitment` - * in the `_operatorRoles` Merkle tree. + * @description Generates a new private state with a random secret nonce. + * @returns A fresh ShieldedAccessControlPrivateState instance. */ - getRoleCommitmentPath: ( - { - ledger, - privateState, - }: WitnessContext, - roleCommitment: Uint8Array, - index: bigint, - ): [ShieldedAccessControlPrivateState, MerkleTreePath] => { - const merkleTreePath = - ledger.ShieldedAccessControl__operatorRoles.pathForLeaf( - index, - roleCommitment, - ); - return [privateState, merkleTreePath]; + generate: (account: Uint8Array): ShieldedAccessControlPrivateState => { + const defaultRoleId: string = Buffer.alloc(32).toString('hex'); + const privateState: ShieldedAccessControlPrivateState = { roles: {}, account }; + privateState.roles[defaultRoleId] = getRandomValues(Buffer.alloc(32)); + return privateState; }, + /** - * @description Typescript implementation of the `recoverNonce` witness function. Simulates calls to the `hasRole` circuit - * to determine if the account has the specified role. Updates the private state with any found roles. - * @param privateState - The current private state. - * @param ledger - A snapshot of the current ledger state. - * @param contractAddress - The address of the contract. - * @param account - The public key associated with a role. - * @param salt - A salt value. + * @description Generates a new private state with a user-defined secret nonce. + * Useful for deterministic nonce generation or advanced use cases. * - * @returns An array of the new private state and the empty tuple + * @param nonce - The 32-byte secret nonce to use. + * @returns A fresh ShieldedAccessControlPrivateState instance with the provided nonce. + * + * @example + * ```typescript + * // For deterministic nonces (user-defined scheme) + * const deterministicNonce = myDeterministicScheme(...); + * const privateState = ShieldedAccessControlPrivateState.withNonce(deterministicNonce); + * ``` */ - recoverRoles: ( - { - ledger, - privateState, - contractAddress, - }: WitnessContext, - account: Uint8Array, - salt: Uint8Array, - ): [ShieldedAccessControlPrivateState, []] => { - const roles = [ledger.ShieldedAccessControl_DEFAULT_ADMIN_ROLE]; - const coinPubKey = decodeCoinPublicKey(account); - const newPrivateState: ShieldedAccessControlPrivateState = { - secretKey: privateState.secretKey, - roleIds: [], - nonces: [], - }; - - const contract = - new MockShieldedAccessControl( - ShieldedAccessControlWitnesses, - ); - const { - currentPrivateState, - currentContractState, - currentZswapLocalState, - } = contract.initialState( - constructorContext( - { secretKey: privateState.secretKey, nonces: [], roleIds: [] }, - coinPubKey, - ), - ); - const circuitContext = { - currentPrivateState, - currentZswapLocalState, - originalState: currentContractState, - transactionContext: new QueryContext( - currentContractState.data, - contractAddress, - ), - }; + withRoleAndNonce: (account: Uint8Array, roleId: Buffer, nonce: Buffer): ShieldedAccessControlPrivateState => { + const roleString = roleId.toString('hex'); + const privateState: ShieldedAccessControlPrivateState = { roles: {}, account }; + privateState.roles[roleString] = nonce; + return privateState; + }, - for (let i = 0; i < roles.length; i++) { - const role = roles[i]; - const nonce = generateNonce( - privateState.secretKey, - Buffer.from(role), - Buffer.from(salt), - Buffer.from(account), - ); - const eitherAccount: Either = { - is_left: true, - left: { bytes: account }, - right: { bytes: encodeContractAddress(sampleContractAddress()) }, - }; + setRole: (privateState: ShieldedAccessControlPrivateState, roleId: Buffer, nonce: Buffer): ShieldedAccessControlPrivateState => { + const roleString = roleId.toString('hex'); + privateState.roles[roleString] = nonce; + return privateState; + }, - try { - const hasRole = contract.impureCircuits.hasRole( - circuitContext, - role, - eitherAccount, - nonce, - ); - if (hasRole) { - newPrivateState.nonces.push(nonce); - newPrivateState.roleIds.push(Buffer.from(role)); - } - } catch (err) { - console.log(err); - } + getRoleCommitmentPath: (ledger: Ledger, roleCommitment: Uint8Array): MerkleTreePath => { + const path = ledger.ShieldedAccessControl__operatorRoles.findPathForLeaf(roleCommitment); + const defaultPath: MerkleTreePath = { + leaf: Buffer.alloc(32), + path: [] } - - return [newPrivateState, []]; + return path ? path : defaultPath; }, - /** - * @description Typescript implementation of the `requestRole` witness function. - * @param privateState - The current private state. - * @param roleId - The role identifier. - * @param account - The public key requesting a role. - * @param salt - A salt value. - * - * @returns An array of the new private state and an empty array - */ - requestRole: ( - { privateState }: WitnessContext, - roleId: Uint8Array, - account: Uint8Array, - salt: Uint8Array, - ): [ShieldedAccessControlPrivateState, []] => { - const saltBuff = Buffer.from(salt); - const roleIdBuff = Buffer.from(roleId); - const accountBuff = Buffer.from(account); - const nonce = generateNonce( - privateState.secretKey, - roleIdBuff, - saltBuff, - accountBuff, - ); - const isApproved = sendRoleRequestToAdmin(accountBuff, roleIdBuff, nonce); - if (isApproved) { - privateState.nonces.push(nonce); - privateState.roleIds.push(roleIdBuff); + // If index cannot be found in MT return _currentMTIndex + getRoleIndex: ({ ledger, privateState }: WitnessContext, roleId: Uint8Array): bigint => { + const roleIdString = Buffer.from(roleId).toString('hex'); + // Iterate over each MT to determine if commitment exists + for (let i = 0; i < MERKLE_TREE_DEPTH; i++) { + const rt_type = new CompactTypeVector(4, new CompactTypeBytes(32)); + const bIndex = convert_bigint_to_Uint8Array(32, BigInt(i)); + const commitment = persistentHash(rt_type, [roleId, privateState.account, privateState.roles[roleIdString], bIndex]); + try { + ledger.ShieldedAccessControl__operatorRoles.pathForLeaf(BigInt(i), commitment); + return BigInt(i); + } catch (e) { + console.error(e); + } } - return [privateState, []]; + // If commitment doesn't exist return currentMTIndex + // Used for adding roles or as a standard default + return ledger.ShieldedAccessControl__currentMerkleTreeIndex; }, }; + +/** + * @description Factory function creating witness implementations for Shielded AccessControl operations. + * @returns An object implementing the Witnesses interface for ShieldedAccessControlPrivateState. + */ +export const ShieldedAccessControlWitnesses = + (): IShieldedAccessControlWitnesses => ({ + wit_secretNonce( + context: WitnessContext, + roleId: Uint8Array + ): [ShieldedAccessControlPrivateState, Uint8Array] { + const roleString = Buffer.from(roleId).toString('hex'); + return [context.privateState, context.privateState.roles[roleString]]; + }, + wit_getRoleCommitmentPath( + context: WitnessContext, + roleCommitment: Uint8Array + ): [ShieldedAccessControlPrivateState, MerkleTreePath] { + return [context.privateState, ShieldedAccessControlPrivateState.getRoleCommitmentPath(context.ledger, roleCommitment)]; + }, + wit_getRoleIndex( + context: WitnessContext, + roleId: Uint8Array + ): [ShieldedAccessControlPrivateState, bigint] { + return [context.privateState, ShieldedAccessControlPrivateState.getRoleIndex(context, roleId)]; + }, + }); \ No newline at end of file From 9117fc698ba1abcddc4c43e68bc07e3984711ee1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Tue, 2 Sep 2025 13:52:48 -0400 Subject: [PATCH 162/322] Restrict usage to ZSwapCoinPubKeys, simplify logic --- .../src/access/ShieldedAccessControl.compact | 72 +++---------------- 1 file changed, 10 insertions(+), 62 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index 79598fe6..0d2c266b 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -151,13 +151,17 @@ module ShieldedAccessControl { * @return {Boolean} - A boolean determining if the account has the specified role.  */ export circuit hasRole(roleId: Bytes<32>, account: Either): Role { - if (!Utils_isContractAddress(account)) { - const zswapPubKey = account.left.bytes; - return _checkMerkleTree(roleId, zswapPubKey); - } + assert(!Utils_isContractAddress(account), "ShieldedAccessControl: contract address roles are not yet supported"); + + const nonce = wit_secretNonce(roleId); + const index = wit_getRoleIndex(roleId); + const roleCommitment = persistentHash>>([roleId, account.left.bytes, nonce, index as Field as Bytes<32>, pad(32, "ShieldedAccessControl:shield:")]); + assert(!_roleCommitmentNullifiers.member(disclose(roleCommitment)), "ShieldedAccessControl: role commitment access revoked"); - const contractAddress = account.right.bytes; - return _checkMerkleTree(roleId, contractAddress); + const authPath = wit_getRoleCommitmentPath(roleCommitment); + const hasRole = _operatorRoles + .checkRoot(merkleTreePathRoot<10, Bytes<32>>(disclose(authPath))); + return Role {hasRole, disclose(roleCommitment)}; } /** @@ -225,45 +229,6 @@ module ShieldedAccessControl { assert(role.hasRole, "ShieldedAccessControl: unauthorized account"); } - /** - * @description Checks if a path exists for a role commitment. - * - * @circuitInfo k=15, rows=29801 - * - * Requirements: - * - * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce) - * must exist in the `_roleCommitmentIndex` map. - * - A nullifier for the role commitment produced by SHA256(roleId | account | nonce) - * must not exist in the `_roleCommitmentNullifiers` set. - * - A path for the role commitment produced by SHA256(roleId | account | nonce) must - * exist at `index` in the `_operatorRoles` Merkle tree. - * - * Disclosures: - * - * - The intermediate role commitment produced by SHA256(roleId | account | nonce). - * - The role commitment produced by SHA256(roleId | account | nonce). - * - The Merkle tree path for the role commitment stored at `index` in the `_operatorRoles` - * Merkle tree. - * - * @param {Bytes<32>} roleId - The role identifier. - * @param {Bytes<32>} account - The account to check represented as a Bytes<32>. - * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK) - * @return {Boolean} - A boolean determining if a path for for the role commitment - * produced by SHA256(roleId | account | nonce) exists in the `_operatorRoles` Merkle tree - */ - export circuit _checkMerkleTree(roleId: Bytes<32>, account: Bytes<32>): Role { - const nonce = wit_secretNonce(roleId); - const index = wit_getRoleIndex(roleId); - const roleCommitment = persistentHash>>([roleId, account, nonce, index as Field as Bytes<32>]); - assert(!_roleCommitmentNullifiers.member(disclose(roleCommitment)), "ShieldedAccessControl: role commitment access revoked"); - - const authPath = wit_getRoleCommitmentPath(roleCommitment); - const hasRole = _operatorRoles - .checkRoot(merkleTreePathRoot<10, Bytes<32>>(disclose(authPath))); - return Role {hasRole, disclose(roleCommitment)}; - } - /** * @description Returns the admin role that controls `roleId` or * a byte array with all zero bytes if `roleId` doesn't exist. See {grantRole} and {revokeRole}. @@ -430,7 +395,6 @@ module ShieldedAccessControl { * @return {Boolean} roleGranted - A boolean indicating if `roleId` was granted. */ export circuit _grantRole(roleId: Bytes<32>, account: Either): Boolean { - assert(!Utils_isContractAddress(account), "ShieldedAccessControl: unsafe role approval"); return _unsafeGrantRole(roleId, account); } @@ -471,15 +435,6 @@ module ShieldedAccessControl { return false; } - if (!Utils_isContractAddress(account)) { - const zswapPubKey = account.left.bytes; - // Use ledger index as source of truth - _operatorRoles.insertHashIndex(disclose(role.roleCommitment), _currentMerkleTreeIndex); - _currentMerkleTreeIndex.increment(1); - return true; - } - - const contractAddress = account.right.bytes; // Use ledger index as source of truth _operatorRoles.insertHashIndex(disclose(role.roleCommitment), _currentMerkleTreeIndex); _currentMerkleTreeIndex.increment(1); @@ -520,13 +475,6 @@ module ShieldedAccessControl { return false; } - if(!Utils_isContractAddress(account)) { - const zswapPubKey = account.left.bytes; - _roleCommitmentNullifiers.insert(disclose(role.roleCommitment)); - return true; - } - - const contractAddress = account.right.bytes; _roleCommitmentNullifiers.insert(disclose(role.roleCommitment)); return true; } From e77d127e31e35a3369fc40b3872a9efb03b6d7ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Tue, 2 Sep 2025 13:53:25 -0400 Subject: [PATCH 163/322] Update *.compact testing dependencies --- .../test/mocks/MockShieldedAccessControl.compact | 4 ---- .../test/simulators/ShieldedAccessControlSimulator.ts | 10 +--------- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact index 78c36a96..361bab4a 100644 --- a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact +++ b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact @@ -30,10 +30,6 @@ export circuit _checkRole(roleId: Bytes<32>, account: Either, account: Bytes<32>): ShieldedAccessControl_Role { - return ShieldedAccessControl__checkMerkleTree(roleId, account); -} - export circuit getRoleAdmin(roleId: Bytes<32>): Bytes<32> { return ShieldedAccessControl_getRoleAdmin(roleId); } diff --git a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts index d194a5a3..38845ff1 100644 --- a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts +++ b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts @@ -57,7 +57,7 @@ export class ShieldedAccessControlSimulator extends AbstractContractSimulator< >; constructor( - initUser: Uint8Array, + initUser: Either, options: ShieldedAccessControlSimOptions = {}, ) { super(); @@ -227,14 +227,6 @@ export class ShieldedAccessControlSimulator extends AbstractContractSimulator< this.circuits.impure._checkRole(roleId, account); } - /** - * @description Throws if called by any account whose id hash `SHA256(pk, nonce)` does not match - * the stored owner commitment. Use this to only allow the owner to call specific circuits. - */ - public _checkMerkleTree(roleId: Uint8Array, account: Uint8Array): Role { - return this.circuits.impure._checkMerkleTree(roleId, account); - } - /** * @description Computes the owner commitment from the given `id` and `counter`. * @param id - The unique identifier of the owner calculated by `SHA256(pk, nonce)`. From 0c3361175f4ce402be39219aab94ae6ab6475d82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Tue, 2 Sep 2025 13:54:03 -0400 Subject: [PATCH 164/322] Add helper fn, update API for improved flexibility --- contracts/src/access/test/utils/address.ts | 37 +++++++++++++++------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/contracts/src/access/test/utils/address.ts b/contracts/src/access/test/utils/address.ts index fb22e4be..0cd6f6a2 100644 --- a/contracts/src/access/test/utils/address.ts +++ b/contracts/src/access/test/utils/address.ts @@ -64,23 +64,30 @@ export const createEitherTestContractAddress = (str: string) => ({ const baseGeneratePubKeyPair = ( str: string, asEither: boolean, + asPK: boolean ): [ - string, - ( - | Compact.ZswapCoinPublicKey - | Compact.Either - ), -] => { + string, + ( + | Compact.ZswapCoinPublicKey + | Compact.Either + ), + ] => { const pk = toHexPadded(str); - const zpk = asEither ? createEitherTestUser(str) : encodeToPK(str); - return [pk, zpk]; + + if (asEither && asPK) { + return [pk, createEitherTestUser(str)] + } else if (asEither && !asPK) { + return [pk, createEitherTestContractAddress(str)] + } + + return [pk, encodeToPK(str)]; }; export const generatePubKeyPair = (str: string) => - baseGeneratePubKeyPair(str, false) as [string, Compact.ZswapCoinPublicKey]; + baseGeneratePubKeyPair(str, false, false) as [string, Compact.ZswapCoinPublicKey]; -export const generateEitherPubKeyPair = (str: string) => - baseGeneratePubKeyPair(str, true) as [ +export const generateEitherPubKeyPair = (str: string, asPK = true) => + baseGeneratePubKeyPair(str, true, asPK) as [ string, Compact.Either, ]; @@ -99,3 +106,11 @@ export const ZERO_ADDRESS = { left: encodeToPK(''), right: { bytes: zeroUint8Array() }, }; + +export const eitherToBytes = (account: Compact.Either) => { + if (account.is_left) { + return account.left.bytes; + } + + return account.right.bytes; +} \ No newline at end of file From e3cb30db9827737f44c46259b3cb9e163339808d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Tue, 2 Sep 2025 13:54:30 -0400 Subject: [PATCH 165/322] Use helper in witness impl --- .../access/witnesses/ShieldedAccessControlWitnesses.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts b/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts index 586dcb2e..fc7fb5ee 100644 --- a/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts +++ b/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts @@ -1,6 +1,7 @@ import { getRandomValues } from 'node:crypto'; import { CompactTypeVector, CompactTypeBytes, persistentHash, type WitnessContext, convert_bigint_to_Uint8Array } from '@midnight-ntwrk/compact-runtime'; -import type { Ledger, MerkleTreePath } from '../../../artifacts/MockShieldedAccessControl/contract/index.cjs'; +import type { Ledger, MerkleTreePath, Either, ZswapCoinPublicKey, ContractAddress } from '../../../artifacts/MockShieldedAccessControl/contract/index.cjs'; +import { eitherToBytes } from '../test/utils/address'; const MERKLE_TREE_DEPTH = 2 ** 10; @@ -36,12 +37,13 @@ export type ShieldedAccessControlPrivateState = { */ export const ShieldedAccessControlPrivateState = { /** - * @description Generates a new private state with a random secret nonce. + * @description Generates a new private state with a random secret nonce and a default roleId of 0. * @returns A fresh ShieldedAccessControlPrivateState instance. */ - generate: (account: Uint8Array): ShieldedAccessControlPrivateState => { + generate: (account: Either): ShieldedAccessControlPrivateState => { const defaultRoleId: string = Buffer.alloc(32).toString('hex'); - const privateState: ShieldedAccessControlPrivateState = { roles: {}, account }; + const bAccount = eitherToBytes(account); + const privateState: ShieldedAccessControlPrivateState = { roles: {}, account: bAccount }; privateState.roles[defaultRoleId] = getRandomValues(Buffer.alloc(32)); return privateState; }, From 95821e05340f7c451b4ab95cefa22f6e32fe449f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Tue, 2 Sep 2025 13:54:42 -0400 Subject: [PATCH 166/322] Init tests --- .../access/test/ShieldedAccessControl.test.ts | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 contracts/src/access/test/ShieldedAccessControl.test.ts diff --git a/contracts/src/access/test/ShieldedAccessControl.test.ts b/contracts/src/access/test/ShieldedAccessControl.test.ts new file mode 100644 index 00000000..8a7eb8e0 --- /dev/null +++ b/contracts/src/access/test/ShieldedAccessControl.test.ts @@ -0,0 +1,90 @@ +import { + CompactTypeBytes, + CompactTypeVector, + convert_bigint_to_Uint8Array, + persistentHash, + transientHash, + upgradeFromTransient, +} from '@midnight-ntwrk/compact-runtime'; +import { beforeEach, describe, expect, it } from 'vitest'; +import type { ZswapCoinPublicKey, Either, ContractAddress } from '../../../artifacts/MockShieldedAccessControl/contract/index.cjs'; +import { ShieldedAccessControlPrivateState } from '../witnesses/ShieldedAccessControlWitnesses.js'; +import { ShieldedAccessControlSimulator } from './simulators/ShieldedAccessControlSimulator.js'; +import * as utils from './utils/address.js'; + +// PKs +const [ADMIN, Z_ADMIN] = utils.generateEitherPubKeyPair('ADMIN'); +const [CUSTOM_ADMIN, Z_CUSTOM_ADMIN] = utils.generateEitherPubKeyPair('CUSTOM_ADMIN'); +const [OPERATOR_1, Z_OPERATOR_1] = utils.generateEitherPubKeyPair('OPERATOR_1'); +const [OPERATOR_CONTRACT, Z_OPERATOR_CONTRACT] = utils.generateEitherPubKeyPair('OPERATOR_CONTRACT', false); + +// Constants +const INSTANCE_SALT = new Uint8Array(32).fill(8675309); +const BAD_NONCE = Buffer.from(Buffer.alloc(32, 'BAD_NONCE')); +const DOMAIN = 'ShieldedAccessControl:shield:'; +const INIT_COUNTER = 0n; + +// Roles +const DEFAULT_ADMIN_ROLE = utils.zeroUint8Array(); +const OPERATOR_ROLE_1 = convert_bigint_to_Uint8Array(32, 1n); +const OPERATOR_ROLE_2 = convert_bigint_to_Uint8Array(32, 2n); +const OPERATOR_ROLE_3 = convert_bigint_to_Uint8Array(32, 3n); +const CUSTOM_ADMIN_ROLE = convert_bigint_to_Uint8Array(32, 4n); +const UNINITIALIZED_ROLE = convert_bigint_to_Uint8Array(32, 5n); + +const operatorTypes = [ + ['contract', Z_OPERATOR_CONTRACT], + ['pubkey', Z_OPERATOR_1], +] as const; + +// Role to string +const DEFAULT_ADMIN_ROLE_TO_STRING = Buffer.from(DEFAULT_ADMIN_ROLE).toString('hex'); + +let secretNonce: Uint8Array; +let shieldedAccessControl: ShieldedAccessControlSimulator; + +// Helpers +const buildCommitment = ( + roleId: Uint8Array, + account: Either, + nonce: Uint8Array, + index: bigint, +): Uint8Array => { + const rt_type = new CompactTypeVector(5, new CompactTypeBytes(32)); + const bAccount = utils.eitherToBytes(account); + const bIndex = convert_bigint_to_Uint8Array(32, index); + const bDomain = new TextEncoder().encode(DOMAIN); + + const commitment = persistentHash(rt_type, [ + roleId, + bAccount, + nonce, + bIndex, + bDomain, + ]); + + return commitment; +}; + +describe('ShieldedAccessControl', () => { + beforeEach(() => { + // Create private state object and generate nonce + const PS = ShieldedAccessControlPrivateState.generate(Z_ADMIN); + // Bind nonce for convenience + secretNonce = PS.roles[DEFAULT_ADMIN_ROLE_TO_STRING]; + // Prepare owner ID with gen nonce + // Deploy contract with derived owner commitment and PS + shieldedAccessControl = new ShieldedAccessControlSimulator(Z_ADMIN, { + privateState: PS, + }); + + describe('hasRole', () => { + it('should throw if caller is contract address', () => { + shieldedAccessControl.callerCtx.setCaller(OPERATOR_CONTRACT); + expect(() => { + shieldedAccessControl.hasRole(UNINITIALIZED_ROLE, Z_OPERATOR_CONTRACT) + }).toThrow('ShieldedAccessControl: contract address roles are not yet supported'); + }) + }) + }); +}); From 52000a2026328834b9a2c8290da7f25082be6650 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Tue, 2 Sep 2025 15:44:03 -0400 Subject: [PATCH 167/322] Update simulator initialization --- .../src/access/test/simulators/ShieldedAccessControlSimulator.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts index 38845ff1..048bd107 100644 --- a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts +++ b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts @@ -77,7 +77,6 @@ export class ShieldedAccessControlSimulator extends AbstractContractSimulator< privateState, coinPK, address, - [], ); this.contractAddress = this.circuitContext.transactionContext.address; this._witnesses = witnesses; From 3af7b9a5bec6de807d62aa7d2780e9f7fcf6fe80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Wed, 3 Sep 2025 11:44:28 -0400 Subject: [PATCH 168/322] Update hashing scheme --- .../src/access/witnesses/ShieldedAccessControlWitnesses.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts b/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts index fc7fb5ee..e13f6472 100644 --- a/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts +++ b/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts @@ -4,6 +4,7 @@ import type { Ledger, MerkleTreePath, Either, ZswapCoinPublicKey, ContractAddres import { eitherToBytes } from '../test/utils/address'; const MERKLE_TREE_DEPTH = 2 ** 10; +const DOMAIN = new TextEncoder().encode("ShieldedAccessControl:shield:"); /** * @description Interface defining the witness methods for ShieldedAccessControl operations. @@ -89,9 +90,9 @@ export const ShieldedAccessControlPrivateState = { const roleIdString = Buffer.from(roleId).toString('hex'); // Iterate over each MT to determine if commitment exists for (let i = 0; i < MERKLE_TREE_DEPTH; i++) { - const rt_type = new CompactTypeVector(4, new CompactTypeBytes(32)); + const rt_type = new CompactTypeVector(5, new CompactTypeBytes(32)); const bIndex = convert_bigint_to_Uint8Array(32, BigInt(i)); - const commitment = persistentHash(rt_type, [roleId, privateState.account, privateState.roles[roleIdString], bIndex]); + const commitment = persistentHash(rt_type, [roleId, privateState.account, privateState.roles[roleIdString], bIndex, DOMAIN]); try { ledger.ShieldedAccessControl__operatorRoles.pathForLeaf(BigInt(i), commitment); return BigInt(i); From 8d2306192f627ecd1dcc8cd3eac32a6b61061d3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Wed, 3 Sep 2025 11:45:06 -0400 Subject: [PATCH 169/322] Fix incorrect default MerkleTreePath construction --- .../src/access/witnesses/ShieldedAccessControlWitnesses.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts b/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts index e13f6472..302be0a4 100644 --- a/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts +++ b/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts @@ -79,8 +79,11 @@ export const ShieldedAccessControlPrivateState = { getRoleCommitmentPath: (ledger: Ledger, roleCommitment: Uint8Array): MerkleTreePath => { const path = ledger.ShieldedAccessControl__operatorRoles.findPathForLeaf(roleCommitment); const defaultPath: MerkleTreePath = { - leaf: Buffer.alloc(32), - path: [] + leaf: new Uint8Array(32), + path: Array.from({ length: 10 }, () => ({ + sibling: { field: 0n }, + goes_left: false, + })) } return path ? path : defaultPath; }, From 47f5d3ea31a40e4ec1af6a3dbb1e0aee556715a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Wed, 3 Sep 2025 11:45:45 -0400 Subject: [PATCH 170/322] Improve typesafety of try catch block, add debugging logic --- .../witnesses/ShieldedAccessControlWitnesses.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts b/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts index 302be0a4..b4f1d7aa 100644 --- a/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts +++ b/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts @@ -99,13 +99,20 @@ export const ShieldedAccessControlPrivateState = { try { ledger.ShieldedAccessControl__operatorRoles.pathForLeaf(BigInt(i), commitment); return BigInt(i); - } catch (e) { - console.error(e); + } catch (e: unknown) { + if (e instanceof Error) { + const [msg, index] = e.message.split(":"); + if (msg === "invalid index into sparse merkle tree") { + // console.log(`${roleIdString} not found at index ${index}`); + } else { + throw e; + } + } } } // If commitment doesn't exist return currentMTIndex - // Used for adding roles or as a standard default + // Used for adding roles return ledger.ShieldedAccessControl__currentMerkleTreeIndex; }, }; From c607923fe33b9079ecf7bf1f309881f2a6b8fded Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Wed, 3 Sep 2025 11:46:21 -0400 Subject: [PATCH 171/322] Add initialization checks, correct role commitment checks --- .../access/test/ShieldedAccessControl.test.ts | 39 +++++++++++++++---- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/contracts/src/access/test/ShieldedAccessControl.test.ts b/contracts/src/access/test/ShieldedAccessControl.test.ts index 8a7eb8e0..33839b50 100644 --- a/contracts/src/access/test/ShieldedAccessControl.test.ts +++ b/contracts/src/access/test/ShieldedAccessControl.test.ts @@ -23,6 +23,7 @@ const INSTANCE_SALT = new Uint8Array(32).fill(8675309); const BAD_NONCE = Buffer.from(Buffer.alloc(32, 'BAD_NONCE')); const DOMAIN = 'ShieldedAccessControl:shield:'; const INIT_COUNTER = 0n; +const EMPTY_ROOT = { field: 0n }; // Roles const DEFAULT_ADMIN_ROLE = utils.zeroUint8Array(); @@ -77,14 +78,36 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl = new ShieldedAccessControlSimulator(Z_ADMIN, { privateState: PS, }); + }); + + describe('initialization checks', () => { + it('DEFAULT_ADMIN_ROLE should be 0', () => { + expect(shieldedAccessControl.getPublicState().ShieldedAccessControl_DEFAULT_ADMIN_ROLE).toEqual(DEFAULT_ADMIN_ROLE); + }); - describe('hasRole', () => { - it('should throw if caller is contract address', () => { - shieldedAccessControl.callerCtx.setCaller(OPERATOR_CONTRACT); - expect(() => { - shieldedAccessControl.hasRole(UNINITIALIZED_ROLE, Z_OPERATOR_CONTRACT) - }).toThrow('ShieldedAccessControl: contract address roles are not yet supported'); - }) - }) + it('Merkle tree root should be 0', () => { + expect(shieldedAccessControl.getPublicState().ShieldedAccessControl__operatorRoles.root()).toEqual(EMPTY_ROOT); + }); }); + + describe('hasRole', () => { + it('should throw if caller is contract address', () => { + shieldedAccessControl.callerCtx.setCaller(OPERATOR_CONTRACT); + expect(() => { + shieldedAccessControl.hasRole(UNINITIALIZED_ROLE, Z_OPERATOR_CONTRACT) + }).toThrow('ShieldedAccessControl: contract address roles are not yet supported'); + }); + + it('should return correct role commitment', () => { + const expCommitment = buildCommitment( + DEFAULT_ADMIN_ROLE, + Z_ADMIN, + secretNonce, + INIT_COUNTER, + ); + + const role = shieldedAccessControl.hasRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); + expect(role.roleCommitment).toEqual(expCommitment); + }); + }) }); From 4140f3b249634e487bd8754218e290c90bb064ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Thu, 4 Sep 2025 18:23:29 -0400 Subject: [PATCH 172/322] Use correct MT API --- contracts/src/access/ShieldedAccessControl.compact | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index 0d2c266b..01151441 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -436,7 +436,7 @@ module ShieldedAccessControl { } // Use ledger index as source of truth - _operatorRoles.insertHashIndex(disclose(role.roleCommitment), _currentMerkleTreeIndex); + _operatorRoles.insertIndex(disclose(role.roleCommitment), _currentMerkleTreeIndex); _currentMerkleTreeIndex.increment(1); return true; } From 67f36aa660a3f7a1f8f17c1407d484a34aed3334 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Thu, 4 Sep 2025 19:38:05 -0400 Subject: [PATCH 173/322] Add utility fn and improve logging --- .../witnesses/ShieldedAccessControlWitnesses.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts b/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts index b4f1d7aa..bda581a0 100644 --- a/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts +++ b/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts @@ -6,6 +6,15 @@ import { eitherToBytes } from '../test/utils/address'; const MERKLE_TREE_DEPTH = 2 ** 10; const DOMAIN = new TextEncoder().encode("ShieldedAccessControl:shield:"); +function fmtHexString(bytes: String | Uint8Array): string { + if (bytes instanceof String) { + return `${bytes.slice(0, 4)}...${bytes.slice(-4)}` + } else { + const buffStr = Buffer.from(bytes).toString('hex'); + return `${buffStr.slice(0, 4)}...${buffStr.slice(-4)}`; + } +} + /** * @description Interface defining the witness methods for ShieldedAccessControl operations. * @template P - The private state type. @@ -103,7 +112,7 @@ export const ShieldedAccessControlPrivateState = { if (e instanceof Error) { const [msg, index] = e.message.split(":"); if (msg === "invalid index into sparse merkle tree") { - // console.log(`${roleIdString} not found at index ${index}`); + // console.log(`role ${fmtHexString(roleIdString)} with commitment ${fmtHexString(commitment)} not found at index ${index}`); } else { throw e; } From 99fa0be551da2b80d2e27f77c3654ea93558777f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Thu, 4 Sep 2025 19:39:04 -0400 Subject: [PATCH 174/322] add test --- .../access/test/ShieldedAccessControl.test.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/contracts/src/access/test/ShieldedAccessControl.test.ts b/contracts/src/access/test/ShieldedAccessControl.test.ts index 33839b50..d4103fcc 100644 --- a/contracts/src/access/test/ShieldedAccessControl.test.ts +++ b/contracts/src/access/test/ShieldedAccessControl.test.ts @@ -80,17 +80,11 @@ describe('ShieldedAccessControl', () => { }); }); - describe('initialization checks', () => { - it('DEFAULT_ADMIN_ROLE should be 0', () => { - expect(shieldedAccessControl.getPublicState().ShieldedAccessControl_DEFAULT_ADMIN_ROLE).toEqual(DEFAULT_ADMIN_ROLE); - }); - - it('Merkle tree root should be 0', () => { - expect(shieldedAccessControl.getPublicState().ShieldedAccessControl__operatorRoles.root()).toEqual(EMPTY_ROOT); + describe('hasRole', () => { + beforeEach(() => { + shieldedAccessControl._grantRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); }); - }); - describe('hasRole', () => { it('should throw if caller is contract address', () => { shieldedAccessControl.callerCtx.setCaller(OPERATOR_CONTRACT); expect(() => { @@ -109,5 +103,10 @@ describe('ShieldedAccessControl', () => { const role = shieldedAccessControl.hasRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); expect(role.roleCommitment).toEqual(expCommitment); }); + + it('should return true when admin has role', () => { + const role = shieldedAccessControl.hasRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); + expect(role.hasRole).toEqual(true); + }); }) }); From 4b1ff86846898daf4f846cf3f5362681ac422164 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Mon, 8 Sep 2025 14:53:51 -0400 Subject: [PATCH 175/322] Fix typo in filename --- .../utils/{SimualatorStateManager.ts => SimulatorStateManager.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename contracts/src/access/test/utils/{SimualatorStateManager.ts => SimulatorStateManager.ts} (100%) diff --git a/contracts/src/access/test/utils/SimualatorStateManager.ts b/contracts/src/access/test/utils/SimulatorStateManager.ts similarity index 100% rename from contracts/src/access/test/utils/SimualatorStateManager.ts rename to contracts/src/access/test/utils/SimulatorStateManager.ts From 9a735b273ec91801682a61368cb144e4faadca3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Mon, 8 Sep 2025 14:54:08 -0400 Subject: [PATCH 176/322] Update imports --- contracts/src/access/test/simulators/ZOwnablePKSimulator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/src/access/test/simulators/ZOwnablePKSimulator.ts b/contracts/src/access/test/simulators/ZOwnablePKSimulator.ts index e6e3f62b..adfb67ce 100644 --- a/contracts/src/access/test/simulators/ZOwnablePKSimulator.ts +++ b/contracts/src/access/test/simulators/ZOwnablePKSimulator.ts @@ -23,7 +23,7 @@ import type { SimulatorOptions, } from '../types/test.js'; import { AbstractContractSimulator } from '../utils/AbstractContractSimulator.js'; -import { SimulatorStateManager } from '../utils/SimualatorStateManager.js'; +import { SimulatorStateManager } from '../utils/SimulatorStateManager.js'; type OwnableSimOptions = SimulatorOptions< ZOwnablePKPrivateState, From 2f375bac513b12ef5bb926b23a6288fecd7c5e9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Mon, 8 Sep 2025 14:54:51 -0400 Subject: [PATCH 177/322] Update witness fn signatures --- .../witnesses/ShieldedAccessControlWitnesses.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts b/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts index bda581a0..7de12adb 100644 --- a/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts +++ b/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts @@ -6,7 +6,7 @@ import { eitherToBytes } from '../test/utils/address'; const MERKLE_TREE_DEPTH = 2 ** 10; const DOMAIN = new TextEncoder().encode("ShieldedAccessControl:shield:"); -function fmtHexString(bytes: String | Uint8Array): string { +export function fmtHexString(bytes: String | Uint8Array): string { if (bytes instanceof String) { return `${bytes.slice(0, 4)}...${bytes.slice(-4)}` } else { @@ -39,7 +39,7 @@ type SecretNonce = Uint8Array; export type ShieldedAccessControlPrivateState = { /** @description A 32-byte secret nonce used as a privacy additive. */ roles: Record, - account: Uint8Array + account: Either }; /** @@ -52,8 +52,7 @@ export const ShieldedAccessControlPrivateState = { */ generate: (account: Either): ShieldedAccessControlPrivateState => { const defaultRoleId: string = Buffer.alloc(32).toString('hex'); - const bAccount = eitherToBytes(account); - const privateState: ShieldedAccessControlPrivateState = { roles: {}, account: bAccount }; + const privateState: ShieldedAccessControlPrivateState = { roles: {}, account }; privateState.roles[defaultRoleId] = getRandomValues(Buffer.alloc(32)); return privateState; }, @@ -72,7 +71,7 @@ export const ShieldedAccessControlPrivateState = { * const privateState = ShieldedAccessControlPrivateState.withNonce(deterministicNonce); * ``` */ - withRoleAndNonce: (account: Uint8Array, roleId: Buffer, nonce: Buffer): ShieldedAccessControlPrivateState => { + withRoleAndNonce: (account: Either, roleId: Buffer, nonce: Buffer): ShieldedAccessControlPrivateState => { const roleString = roleId.toString('hex'); const privateState: ShieldedAccessControlPrivateState = { roles: {}, account }; privateState.roles[roleString] = nonce; @@ -104,7 +103,9 @@ export const ShieldedAccessControlPrivateState = { for (let i = 0; i < MERKLE_TREE_DEPTH; i++) { const rt_type = new CompactTypeVector(5, new CompactTypeBytes(32)); const bIndex = convert_bigint_to_Uint8Array(32, BigInt(i)); - const commitment = persistentHash(rt_type, [roleId, privateState.account, privateState.roles[roleIdString], bIndex, DOMAIN]); + const bAccount = eitherToBytes(privateState.account); + const bNonce = privateState.roles[roleIdString]; + const commitment = persistentHash(rt_type, [roleId, bAccount, bNonce, bIndex, DOMAIN]); try { ledger.ShieldedAccessControl__operatorRoles.pathForLeaf(BigInt(i), commitment); return BigInt(i); From a5a176309aa0460af1685374318673dacf2bfc73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Mon, 8 Sep 2025 14:57:11 -0400 Subject: [PATCH 178/322] Update constructor, witnesses setter --- .../access/test/simulators/ShieldedAccessControlSimulator.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts index 048bd107..6666df91 100644 --- a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts +++ b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts @@ -24,7 +24,7 @@ import type { SimulatorOptions, } from '../types/test.js'; import { AbstractContractSimulator } from '../utils/AbstractContractSimulator.js'; -import { SimulatorStateManager } from '../utils/SimualatorStateManager.js'; +import { SimulatorStateManager } from '../utils/SimulatorStateManager.js'; type ShieldedAccessControlSimOptions = SimulatorOptions< ShieldedAccessControlPrivateState, @@ -64,7 +64,7 @@ export class ShieldedAccessControlSimulator extends AbstractContractSimulator< // Setup initial state const { - privateState = ShieldedAccessControlPrivateState.generate(initUser), + privateState = options.privateState ? options.privateState : ShieldedAccessControlPrivateState.generate(initUser), witnesses = ShieldedAccessControlWitnesses(), coinPK = '0'.repeat(64), address = sampleContractAddress(), @@ -185,6 +185,7 @@ export class ShieldedAccessControlSimulator extends AbstractContractSimulator< } public set witnesses(newWitnesses: ReturnType) { + this.resetCircuitProxies(); this._witnesses = newWitnesses; this.contract = new MockShieldedAccessControl(this._witnesses); } From 353b37939c421392adbd1c4b36724b7c8593277e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Mon, 8 Sep 2025 14:57:41 -0400 Subject: [PATCH 179/322] Add bad index tests --- .../access/test/ShieldedAccessControl.test.ts | 52 +++++++++++++++---- 1 file changed, 42 insertions(+), 10 deletions(-) diff --git a/contracts/src/access/test/ShieldedAccessControl.test.ts b/contracts/src/access/test/ShieldedAccessControl.test.ts index d4103fcc..34051905 100644 --- a/contracts/src/access/test/ShieldedAccessControl.test.ts +++ b/contracts/src/access/test/ShieldedAccessControl.test.ts @@ -3,11 +3,10 @@ import { CompactTypeVector, convert_bigint_to_Uint8Array, persistentHash, - transientHash, - upgradeFromTransient, + WitnessContext, } from '@midnight-ntwrk/compact-runtime'; import { beforeEach, describe, expect, it } from 'vitest'; -import type { ZswapCoinPublicKey, Either, ContractAddress } from '../../../artifacts/MockShieldedAccessControl/contract/index.cjs'; +import type { ZswapCoinPublicKey, Either, ContractAddress, Ledger } from '../../../artifacts/MockShieldedAccessControl/contract/index.cjs'; import { ShieldedAccessControlPrivateState } from '../witnesses/ShieldedAccessControlWitnesses.js'; import { ShieldedAccessControlSimulator } from './simulators/ShieldedAccessControlSimulator.js'; import * as utils from './utils/address.js'; @@ -20,7 +19,7 @@ const [OPERATOR_CONTRACT, Z_OPERATOR_CONTRACT] = utils.generateEitherPubKeyPair( // Constants const INSTANCE_SALT = new Uint8Array(32).fill(8675309); -const BAD_NONCE = Buffer.from(Buffer.alloc(32, 'BAD_NONCE')); +const BAD_NONCE = Buffer.alloc(32, 'BAD_NONCE'); const DOMAIN = 'ShieldedAccessControl:shield:'; const INIT_COUNTER = 0n; const EMPTY_ROOT = { field: 0n }; @@ -41,7 +40,7 @@ const operatorTypes = [ // Role to string const DEFAULT_ADMIN_ROLE_TO_STRING = Buffer.from(DEFAULT_ADMIN_ROLE).toString('hex'); -let secretNonce: Uint8Array; +const secretNonce = Buffer.alloc(32, "secretNonce"); let shieldedAccessControl: ShieldedAccessControlSimulator; // Helpers @@ -67,19 +66,50 @@ const buildCommitment = ( return commitment; }; +function RETURN_BAD_INDEX(context: WitnessContext, roleId: Uint8Array): [ShieldedAccessControlPrivateState, bigint] { + console.log("WIT RETURN BAD INDEX"); + return [context.privateState, 1023n] +} + describe('ShieldedAccessControl', () => { beforeEach(() => { // Create private state object and generate nonce - const PS = ShieldedAccessControlPrivateState.generate(Z_ADMIN); - // Bind nonce for convenience - secretNonce = PS.roles[DEFAULT_ADMIN_ROLE_TO_STRING]; - // Prepare owner ID with gen nonce - // Deploy contract with derived owner commitment and PS + const PS = ShieldedAccessControlPrivateState.withRoleAndNonce(Z_ADMIN, Buffer.from(DEFAULT_ADMIN_ROLE), secretNonce); + // Init contract for user with PS shieldedAccessControl = new ShieldedAccessControlSimulator(Z_ADMIN, { privateState: PS, }); }); + describe.only('should fail when incorrect witness values provided', () => { + beforeEach(() => { + shieldedAccessControl._grantRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); + }); + + type FailingCircuits = [method: keyof ShieldedAccessControlSimulator, args: unknown[]]; + const protectedCircuits: FailingCircuits[] = [ + ['assertOnlyRole', [DEFAULT_ADMIN_ROLE]], + ['_checkRole', [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ['grantRole', [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ['revokeRole', [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ]; + + it.each(protectedCircuits)('%s should fail with bad nonce', (circuitName, args) => { + shieldedAccessControl.privateState.injectSecretNonce(Buffer.from(DEFAULT_ADMIN_ROLE), BAD_NONCE); + + expect(() => { + (shieldedAccessControl[circuitName] as (...args: unknown[]) => unknown)(...args); + }).toThrow('ShieldedAccessControl: unauthorized account'); + }); + + it.each(protectedCircuits)('%s should fail with bad index', (circuitName, args) => { + shieldedAccessControl.overrideWitness("wit_getRoleIndex", RETURN_BAD_INDEX); + expect(() => { + (shieldedAccessControl[circuitName] as (...args: unknown[]) => unknown)(...args); + }).toThrow('ShieldedAccessControl: unauthorized account'); + }); + }); + describe('hasRole', () => { beforeEach(() => { shieldedAccessControl._grantRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); @@ -108,5 +138,7 @@ describe('ShieldedAccessControl', () => { const role = shieldedAccessControl.hasRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); expect(role.hasRole).toEqual(true); }); + + }) }); From 26d357634da5a188b5c203c4717dd624c95b156b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Mon, 8 Sep 2025 18:22:10 -0400 Subject: [PATCH 180/322] Update Role field name --- contracts/src/access/ShieldedAccessControl.compact | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index 01151441..116af121 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -119,7 +119,7 @@ module ShieldedAccessControl { witness wit_getRoleIndex(roleId: Bytes<32>): Uint<64>; export struct Role { - hasRole: Boolean; + isApproved: Boolean; roleCommitment: Bytes<32>; } @@ -156,12 +156,12 @@ module ShieldedAccessControl { const nonce = wit_secretNonce(roleId); const index = wit_getRoleIndex(roleId); const roleCommitment = persistentHash>>([roleId, account.left.bytes, nonce, index as Field as Bytes<32>, pad(32, "ShieldedAccessControl:shield:")]); - assert(!_roleCommitmentNullifiers.member(disclose(roleCommitment)), "ShieldedAccessControl: role commitment access revoked"); + assert(!_roleCommitmentNullifiers.member(disclose(roleCommitment)), "ShieldedAccessControl: role access has been revoked"); const authPath = wit_getRoleCommitmentPath(roleCommitment); - const hasRole = _operatorRoles + const isApproved = _operatorRoles .checkRoot(merkleTreePathRoot<10, Bytes<32>>(disclose(authPath))); - return Role {hasRole, disclose(roleCommitment)}; + return Role {isApproved, disclose(roleCommitment)}; } /** @@ -226,7 +226,7 @@ module ShieldedAccessControl { */ export circuit _checkRole(roleId: Bytes<32>, account: Either): [] { const role = hasRole(roleId, account); - assert(role.hasRole, "ShieldedAccessControl: unauthorized account"); + assert(role.isApproved, "ShieldedAccessControl: unauthorized account"); } /** @@ -431,7 +431,7 @@ module ShieldedAccessControl { */ export circuit _unsafeGrantRole(roleId: Bytes<32>, account: Either): Boolean { const role = hasRole(roleId, account); - if (role.hasRole) { + if (role.isApproved) { return false; } @@ -471,7 +471,7 @@ module ShieldedAccessControl { */ export circuit _revokeRole(roleId: Bytes<32>, account: Either): Boolean { const role = hasRole(roleId, account); - if (!role.hasRole) { + if (!role.isApproved) { return false; } From da274c429ac1c88eb8c096c77775a6a32727b07a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Mon, 8 Sep 2025 18:22:43 -0400 Subject: [PATCH 181/322] Add tests --- .../access/test/ShieldedAccessControl.test.ts | 43 ++++++++++++++++--- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/contracts/src/access/test/ShieldedAccessControl.test.ts b/contracts/src/access/test/ShieldedAccessControl.test.ts index 34051905..f95381a9 100644 --- a/contracts/src/access/test/ShieldedAccessControl.test.ts +++ b/contracts/src/access/test/ShieldedAccessControl.test.ts @@ -6,19 +6,19 @@ import { WitnessContext, } from '@midnight-ntwrk/compact-runtime'; import { beforeEach, describe, expect, it } from 'vitest'; -import type { ZswapCoinPublicKey, Either, ContractAddress, Ledger } from '../../../artifacts/MockShieldedAccessControl/contract/index.cjs'; +import type { ZswapCoinPublicKey, Either, ContractAddress, Ledger, MerkleTreePath } from '../../../artifacts/MockShieldedAccessControl/contract/index.cjs'; import { ShieldedAccessControlPrivateState } from '../witnesses/ShieldedAccessControlWitnesses.js'; import { ShieldedAccessControlSimulator } from './simulators/ShieldedAccessControlSimulator.js'; import * as utils from './utils/address.js'; // PKs const [ADMIN, Z_ADMIN] = utils.generateEitherPubKeyPair('ADMIN'); +const [UNAUTHORIZED, Z_UNAUTHORIZED] = utils.generateEitherPubKeyPair('UNAUTHORIZED'); const [CUSTOM_ADMIN, Z_CUSTOM_ADMIN] = utils.generateEitherPubKeyPair('CUSTOM_ADMIN'); const [OPERATOR_1, Z_OPERATOR_1] = utils.generateEitherPubKeyPair('OPERATOR_1'); const [OPERATOR_CONTRACT, Z_OPERATOR_CONTRACT] = utils.generateEitherPubKeyPair('OPERATOR_CONTRACT', false); // Constants -const INSTANCE_SALT = new Uint8Array(32).fill(8675309); const BAD_NONCE = Buffer.alloc(32, 'BAD_NONCE'); const DOMAIN = 'ShieldedAccessControl:shield:'; const INIT_COUNTER = 0n; @@ -67,10 +67,20 @@ const buildCommitment = ( }; function RETURN_BAD_INDEX(context: WitnessContext, roleId: Uint8Array): [ShieldedAccessControlPrivateState, bigint] { - console.log("WIT RETURN BAD INDEX"); return [context.privateState, 1023n] } +function RETURN_BAD_PATH(context: WitnessContext, roleCommitment: Uint8Array): [ShieldedAccessControlPrivateState, MerkleTreePath] { + const defaultPath: MerkleTreePath = { + leaf: new Uint8Array(32), + path: Array.from({ length: 10 }, () => ({ + sibling: { field: 0n }, + goes_left: false, + })) + }; + return [context.privateState, defaultPath]; +} + describe('ShieldedAccessControl', () => { beforeEach(() => { // Create private state object and generate nonce @@ -81,7 +91,7 @@ describe('ShieldedAccessControl', () => { }); }); - describe.only('should fail when incorrect witness values provided', () => { + describe('should fail with bad witness values', () => { beforeEach(() => { shieldedAccessControl._grantRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); }); @@ -108,6 +118,13 @@ describe('ShieldedAccessControl', () => { (shieldedAccessControl[circuitName] as (...args: unknown[]) => unknown)(...args); }).toThrow('ShieldedAccessControl: unauthorized account'); }); + + it.each(protectedCircuits)('%s should fail with bad role path', (circuitName, args) => { + shieldedAccessControl.overrideWitness("wit_getRoleCommitmentPath", RETURN_BAD_PATH); + expect(() => { + (shieldedAccessControl[circuitName] as (...args: unknown[]) => unknown)(...args); + }).toThrow('ShieldedAccessControl: unauthorized account'); + }); }); describe('hasRole', () => { @@ -136,9 +153,25 @@ describe('ShieldedAccessControl', () => { it('should return true when admin has role', () => { const role = shieldedAccessControl.hasRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); - expect(role.hasRole).toEqual(true); + expect(role.isApproved).toEqual(true); }); + it('should return false when unauthorized', () => { + const role = shieldedAccessControl.hasRole(DEFAULT_ADMIN_ROLE, Z_UNAUTHORIZED); + expect(role.isApproved).toEqual(false); + }) + + it('should return false when role does not exist', () => { + shieldedAccessControl.privateState.injectSecretNonce(Buffer.from(UNINITIALIZED_ROLE), Buffer.alloc(32)); + const role = shieldedAccessControl.hasRole(UNINITIALIZED_ROLE, Z_UNAUTHORIZED); + expect(role.isApproved).toBe(false); + }); + it('should fail when role access has been revoked', () => { + shieldedAccessControl._revokeRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); + expect(() => { + shieldedAccessControl.hasRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); + }).toThrow("ShieldedAccessControl: role access has been revoked"); + }); }) }); From 7ae6407b606c15ab2b16f1bf38ed8cd79dcdb624 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Mon, 8 Sep 2025 19:39:23 -0400 Subject: [PATCH 182/322] Remove _unsafeGrantRole --- .../src/access/ShieldedAccessControl.compact | 35 ------------------- .../mocks/MockShieldedAccessControl.compact | 4 --- .../ShieldedAccessControlSimulator.ts | 9 ----- 3 files changed, 48 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index 116af121..17a9f746 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -395,41 +395,6 @@ module ShieldedAccessControl { * @return {Boolean} roleGranted - A boolean indicating if `roleId` was granted. */ export circuit _grantRole(roleId: Bytes<32>, account: Either): Boolean { - return _unsafeGrantRole(roleId, account); - } - - /** - * @description Attempts to grant `roleId` to `account` and returns a boolean indicating if `roleId` was granted. - * Internal circuit without access restriction. It does NOT check if the role is granted to a ContractAddress. - * - * @circuitInfo k=17, rows=109162 - * - * @notice External smart contracts cannot call the token contract at this time, so granting a role to an ContractAddress may - * render a circuit permanently inaccessible. - * - * Requirements: - * - * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce) - * must exist in the `_roleCommitmentIndex` map. - * - A nullifier for the role commitment produced by SHA256(roleId | account | nonce) - * must not exist in the `_roleCommitmentNullifiers` set. - * - A path for the role commitment produced by SHA256(roleId | account | nonce) must - * exist at `index` in the `_operatorRoles` Merkle tree. - * - * Disclosures: - * - * - The intermediate role commitment produced by SHA256(roleId | account | nonce). - * - The role commitment produced by SHA256(roleId | account | nonce). - * - The Merkle tree path for the role commitment stored at `index` in the `_operatorRoles` - * Merkle tree. - * - The type data of `account` - a ZswapCoinPublicKey or ContractAddress. - * - * @param {Bytes<32>} roleId - The role identifier. - * @param {Either} account - A ZswapCoinPublicKey or ContractAddress. - * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK) - * @return {Boolean} roleGranted - A boolean indicating if `role` was granted. - */ - export circuit _unsafeGrantRole(roleId: Bytes<32>, account: Either): Boolean { const role = hasRole(roleId, account); if (role.isApproved) { return false; diff --git a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact index 361bab4a..12c4faa3 100644 --- a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact +++ b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact @@ -54,10 +54,6 @@ export circuit _grantRole(roleId: Bytes<32>, account: Either, account: Either): Boolean { - return ShieldedAccessControl__unsafeGrantRole(roleId, account); -} - export circuit _revokeRole(roleId: Bytes<32>, account: Either): Boolean { return ShieldedAccessControl__revokeRole(roleId, account); } \ No newline at end of file diff --git a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts index 6666df91..2c22cbe6 100644 --- a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts +++ b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts @@ -285,15 +285,6 @@ export class ShieldedAccessControlSimulator extends AbstractContractSimulator< return this.circuits.impure._grantRole(roleId, account); } - /** - * @description Transfers ownership to owner id `newOwnerId` without - * enforcing permission checks on the caller. - * @param newOwnerId - The unique identifier of the new owner calculated by `SHA256(pk, nonce)`. - */ - public _unsafeGrantRole(roleId: Uint8Array, account: Either): Boolean { - return this.circuits.impure._unsafeGrantRole(roleId, account); - } - /** * @description Transfers ownership to owner id `newOwnerId` without * enforcing permission checks on the caller. From 5a1b0b4e839fbb6a0a4fb1f4cb2ac5eb41abbb31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Mon, 8 Sep 2025 19:54:46 -0400 Subject: [PATCH 183/322] Improve tests --- .../access/test/ShieldedAccessControl.test.ts | 47 ++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/contracts/src/access/test/ShieldedAccessControl.test.ts b/contracts/src/access/test/ShieldedAccessControl.test.ts index f95381a9..44c685e2 100644 --- a/contracts/src/access/test/ShieldedAccessControl.test.ts +++ b/contracts/src/access/test/ShieldedAccessControl.test.ts @@ -94,6 +94,7 @@ describe('ShieldedAccessControl', () => { describe('should fail with bad witness values', () => { beforeEach(() => { shieldedAccessControl._grantRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); + shieldedAccessControl.callerCtx.setCaller(ADMIN); }); type FailingCircuits = [method: keyof ShieldedAccessControlSimulator, args: unknown[]]; @@ -107,20 +108,44 @@ describe('ShieldedAccessControl', () => { it.each(protectedCircuits)('%s should fail with bad nonce', (circuitName, args) => { shieldedAccessControl.privateState.injectSecretNonce(Buffer.from(DEFAULT_ADMIN_ROLE), BAD_NONCE); + // Check nonce does not match + expect(shieldedAccessControl.privateState.getCurrentSecretNonce(Buffer.from(DEFAULT_ADMIN_ROLE))).not.toEqual( + secretNonce, + ); expect(() => { (shieldedAccessControl[circuitName] as (...args: unknown[]) => unknown)(...args); }).toThrow('ShieldedAccessControl: unauthorized account'); }); it.each(protectedCircuits)('%s should fail with bad index', (circuitName, args) => { + const [, trueIndex] = shieldedAccessControl.witnesses.wit_getRoleIndex(shieldedAccessControl.getWitnessContext(), DEFAULT_ADMIN_ROLE); shieldedAccessControl.overrideWitness("wit_getRoleIndex", RETURN_BAD_INDEX); + const [, badIndex] = shieldedAccessControl.witnesses.wit_getRoleIndex(shieldedAccessControl.getWitnessContext(), DEFAULT_ADMIN_ROLE); + + // Check index does not match + expect(trueIndex).not.toBe( + badIndex + ); expect(() => { (shieldedAccessControl[circuitName] as (...args: unknown[]) => unknown)(...args); }).toThrow('ShieldedAccessControl: unauthorized account'); }); it.each(protectedCircuits)('%s should fail with bad role path', (circuitName, args) => { + const expCommitment = buildCommitment( + DEFAULT_ADMIN_ROLE, + Z_ADMIN, + secretNonce, + INIT_COUNTER, + ); + const [, truePath] = shieldedAccessControl.witnesses.wit_getRoleCommitmentPath(shieldedAccessControl.getWitnessContext(), expCommitment); shieldedAccessControl.overrideWitness("wit_getRoleCommitmentPath", RETURN_BAD_PATH); + const [, badPath] = shieldedAccessControl.witnesses.wit_getRoleCommitmentPath(shieldedAccessControl.getWitnessContext(), expCommitment); + + // Check path does not match + expect(truePath).not.toEqual( + badPath + ); expect(() => { (shieldedAccessControl[circuitName] as (...args: unknown[]) => unknown)(...args); }).toThrow('ShieldedAccessControl: unauthorized account'); @@ -173,5 +198,25 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl.hasRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); }).toThrow("ShieldedAccessControl: role access has been revoked"); }); - }) + }); + + describe('assertOnlyRole', () => { + beforeEach(() => { + shieldedAccessControl._grantRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); + shieldedAccessControl.callerCtx.setCaller(ADMIN); + }); + + it('should allow authorized caller with correct nonce to call', () => { + expect(() => + shieldedAccessControl.assertOnlyRole(DEFAULT_ADMIN_ROLE), + ).not.toThrow(); + }); + + it('should throw if caller is unauthorized', () => { + shieldedAccessControl.callerCtx.setCaller(UNAUTHORIZED); + expect(() => + shieldedAccessControl.assertOnlyRole(DEFAULT_ADMIN_ROLE), + ).toThrow('ShieldedAccessControl: unauthorized account'); + }); + }); }); From 923d779862031bd510d8ce555024886074d58e70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Mon, 8 Sep 2025 19:54:54 -0400 Subject: [PATCH 184/322] Add helper method --- .../test/simulators/ShieldedAccessControlSimulator.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts index 2c22cbe6..40344d2f 100644 --- a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts +++ b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts @@ -2,6 +2,7 @@ import { type CircuitContext, type CoinPublicKey, emptyZswapLocalState, + WitnessContext, } from '@midnight-ntwrk/compact-runtime'; import { sampleContractAddress } from '@midnight-ntwrk/zswap'; import { @@ -95,6 +96,14 @@ export class ShieldedAccessControlSimulator extends AbstractContractSimulator< return ledger(this.circuitContext.transactionContext.state); } + getWitnessContext(): WitnessContext { + return { + ledger: this.getPublicState(), + privateState: this.getPrivateState(), + contractAddress: this.contractAddress + } + } + /** * @description Constructs a caller-specific circuit context. * If a caller override is present, it replaces the current Zswap local state with an empty one From 07dd40bdb4c86773b8c90552b750f14bb53f31d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Tue, 9 Sep 2025 19:18:51 -0400 Subject: [PATCH 185/322] Change privateState fn signatures --- .../test/simulators/ShieldedAccessControlSimulator.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts index 40344d2f..707f0bc7 100644 --- a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts +++ b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts @@ -310,12 +310,12 @@ export class ShieldedAccessControlSimulator extends AbstractContractSimulator< * @returns The ZOwnablePK private state after setting the new nonce. */ injectSecretNonce: ( - roleId: Buffer, + roleId: Uint8Array, newNonce: Buffer, ): ShieldedAccessControlPrivateState => { const currentState = this.stateManager.getContext().currentPrivateState; const updatedState = { ...currentState, roles: { ...currentState.roles } } - const roleString = roleId.toString('hex'); + const roleString = Buffer.from(roleId).toString('hex'); updatedState.roles[roleString] = newNonce; this.stateManager.updatePrivateState(updatedState); return updatedState; @@ -325,8 +325,8 @@ export class ShieldedAccessControlSimulator extends AbstractContractSimulator< * @description Returns the secret nonce given the context. * @returns The secret nonce. */ - getCurrentSecretNonce: (roleId: Buffer): Uint8Array => { - const roleString = roleId.toString('hex'); + getCurrentSecretNonce: (roleId: Uint8Array): Uint8Array => { + const roleString = Buffer.from(roleId).toString('hex'); return this.stateManager.getContext().currentPrivateState.roles[roleString]; }, }; From fdcf13cc0717cc0f89cc64b2f8600f3ad596eba8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Tue, 9 Sep 2025 19:18:59 -0400 Subject: [PATCH 186/322] Add tests --- .../access/test/ShieldedAccessControl.test.ts | 368 +++++++++++++++--- 1 file changed, 314 insertions(+), 54 deletions(-) diff --git a/contracts/src/access/test/ShieldedAccessControl.test.ts b/contracts/src/access/test/ShieldedAccessControl.test.ts index 44c685e2..29af7e81 100644 --- a/contracts/src/access/test/ShieldedAccessControl.test.ts +++ b/contracts/src/access/test/ShieldedAccessControl.test.ts @@ -6,7 +6,7 @@ import { WitnessContext, } from '@midnight-ntwrk/compact-runtime'; import { beforeEach, describe, expect, it } from 'vitest'; -import type { ZswapCoinPublicKey, Either, ContractAddress, Ledger, MerkleTreePath } from '../../../artifacts/MockShieldedAccessControl/contract/index.cjs'; +import type { ZswapCoinPublicKey, Either, ContractAddress, Ledger, MerkleTreePath, ShieldedAccessControl_Role as Role } from '../../../artifacts/MockShieldedAccessControl/contract/index.cjs'; import { ShieldedAccessControlPrivateState } from '../witnesses/ShieldedAccessControlWitnesses.js'; import { ShieldedAccessControlSimulator } from './simulators/ShieldedAccessControlSimulator.js'; import * as utils from './utils/address.js'; @@ -22,6 +22,7 @@ const [OPERATOR_CONTRACT, Z_OPERATOR_CONTRACT] = utils.generateEitherPubKeyPair( const BAD_NONCE = Buffer.alloc(32, 'BAD_NONCE'); const DOMAIN = 'ShieldedAccessControl:shield:'; const INIT_COUNTER = 0n; + const EMPTY_ROOT = { field: 0n }; // Roles @@ -40,7 +41,10 @@ const operatorTypes = [ // Role to string const DEFAULT_ADMIN_ROLE_TO_STRING = Buffer.from(DEFAULT_ADMIN_ROLE).toString('hex'); -const secretNonce = Buffer.alloc(32, "secretNonce"); +const ADMIN_SECRET_NONCE = Buffer.alloc(32, "ADMIN_SECRET_NONCE"); +const OPERATOR_ROLE_1_SECRET_NONCE = Buffer.alloc(32, "OPERATOR_ROLE_1_SECRET_NONCE"); +const OPERATOR_ROLE_2_SECRET_NONCE = Buffer.alloc(32, "OPERATOR_ROLE_2_SECRET_NONCE"); +const OPERATOR_ROLE_3_SECRET_NONCE = Buffer.alloc(32, "OPERATOR_ROLE_3_SECRET_NONCE"); let shieldedAccessControl: ShieldedAccessControlSimulator; // Helpers @@ -66,6 +70,13 @@ const buildCommitment = ( return commitment; }; +const EXP_DEFAULT_ADMIN_COMMITMENT = buildCommitment( + DEFAULT_ADMIN_ROLE, + Z_ADMIN, + ADMIN_SECRET_NONCE, + INIT_COUNTER, +); + function RETURN_BAD_INDEX(context: WitnessContext, roleId: Uint8Array): [ShieldedAccessControlPrivateState, bigint] { return [context.privateState, 1023n] } @@ -81,74 +92,197 @@ function RETURN_BAD_PATH(context: WitnessContext { beforeEach(() => { // Create private state object and generate nonce - const PS = ShieldedAccessControlPrivateState.withRoleAndNonce(Z_ADMIN, Buffer.from(DEFAULT_ADMIN_ROLE), secretNonce); + const PS = ShieldedAccessControlPrivateState.withRoleAndNonce(Z_ADMIN, Buffer.from(DEFAULT_ADMIN_ROLE), ADMIN_SECRET_NONCE); // Init contract for user with PS shieldedAccessControl = new ShieldedAccessControlSimulator(Z_ADMIN, { privateState: PS, }); }); - describe('should fail with bad witness values', () => { + describe('checked circuits should fail for authorized caller with invalid witness values', () => { beforeEach(() => { shieldedAccessControl._grantRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); shieldedAccessControl.callerCtx.setCaller(ADMIN); }); - type FailingCircuits = [method: keyof ShieldedAccessControlSimulator, args: unknown[]]; - const protectedCircuits: FailingCircuits[] = [ - ['assertOnlyRole', [DEFAULT_ADMIN_ROLE]], - ['_checkRole', [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ['grantRole', [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ['revokeRole', [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + type FailingCircuits = [method: keyof ShieldedAccessControlSimulator, isValidNonce: boolean, isValidIndex: boolean, isValidPath: boolean, args: unknown[]]; + const checkedCircuits: FailingCircuits[] = [ + ['assertOnlyRole', false, true, true, [DEFAULT_ADMIN_ROLE]], + ['assertOnlyRole', true, false, true, [DEFAULT_ADMIN_ROLE]], + ['assertOnlyRole', true, true, false, [DEFAULT_ADMIN_ROLE]], + ['assertOnlyRole', false, false, true, [DEFAULT_ADMIN_ROLE]], + ['assertOnlyRole', true, false, false, [DEFAULT_ADMIN_ROLE]], + ['assertOnlyRole', false, true, false, [DEFAULT_ADMIN_ROLE]], + ['assertOnlyRole', false, false, false, [DEFAULT_ADMIN_ROLE]], + ['grantRole', false, true, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ['grantRole', true, false, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ['grantRole', true, true, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ['grantRole', false, false, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ['grantRole', true, false, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ['grantRole', false, true, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ['grantRole', false, false, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ['revokeRole', false, true, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ['revokeRole', true, false, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ['revokeRole', true, true, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ['revokeRole', false, false, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ['revokeRole', true, false, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ['revokeRole', false, true, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ['revokeRole', false, false, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], ]; - it.each(protectedCircuits)('%s should fail with bad nonce', (circuitName, args) => { - shieldedAccessControl.privateState.injectSecretNonce(Buffer.from(DEFAULT_ADMIN_ROLE), BAD_NONCE); + it.each(checkedCircuits)('%s should fail with isValidNonce(%s), isValidIndex(%s), isValidPath(%s)', (circuitName, isValidNonce, isValidIndex, isValidPath, args) => { + if (isValidNonce) { + // Check nonce matches + expect(shieldedAccessControl.privateState.getCurrentSecretNonce(DEFAULT_ADMIN_ROLE)).toEqual( + ADMIN_SECRET_NONCE, + ); + } else { + // Check nonce does not match + shieldedAccessControl.privateState.injectSecretNonce(DEFAULT_ADMIN_ROLE, BAD_NONCE); + expect(shieldedAccessControl.privateState.getCurrentSecretNonce(DEFAULT_ADMIN_ROLE)).not.toEqual( + ADMIN_SECRET_NONCE, + ); + } - // Check nonce does not match - expect(shieldedAccessControl.privateState.getCurrentSecretNonce(Buffer.from(DEFAULT_ADMIN_ROLE))).not.toEqual( - secretNonce, - ); + if (isValidIndex) { + // Check index matches + const [, witnessCalculatedIndex] = shieldedAccessControl.witnesses.wit_getRoleIndex(shieldedAccessControl.getWitnessContext(), DEFAULT_ADMIN_ROLE); + expect(witnessCalculatedIndex).toBe(INIT_COUNTER); + } else { + // Check index does not match + shieldedAccessControl.overrideWitness('wit_getRoleIndex', RETURN_BAD_INDEX); + const [, witnessCalculatedIndex] = shieldedAccessControl.witnesses.wit_getRoleIndex(shieldedAccessControl.getWitnessContext(), DEFAULT_ADMIN_ROLE); + expect(witnessCalculatedIndex).not.toBe(INIT_COUNTER); + } + + if (isValidPath) { + // Check path matches + const truePath = shieldedAccessControl.getPublicState().ShieldedAccessControl__operatorRoles.findPathForLeaf(EXP_DEFAULT_ADMIN_COMMITMENT); + const [, witnessCalculatedPath] = shieldedAccessControl.witnesses.wit_getRoleCommitmentPath(shieldedAccessControl.getWitnessContext(), EXP_DEFAULT_ADMIN_COMMITMENT); + expect(witnessCalculatedPath).toEqual(truePath); + } else { + // Check path does not match + const truePath = shieldedAccessControl.getPublicState().ShieldedAccessControl__operatorRoles.findPathForLeaf(EXP_DEFAULT_ADMIN_COMMITMENT); + shieldedAccessControl.overrideWitness('wit_getRoleCommitmentPath', RETURN_BAD_PATH); + const [, witnessCalculatedPath] = shieldedAccessControl.witnesses.wit_getRoleIndex(shieldedAccessControl.getWitnessContext(), DEFAULT_ADMIN_ROLE); + expect(witnessCalculatedPath).not.toEqual(truePath); + } + + // Test protected circuit expect(() => { (shieldedAccessControl[circuitName] as (...args: unknown[]) => unknown)(...args); }).toThrow('ShieldedAccessControl: unauthorized account'); }); + }); - it.each(protectedCircuits)('%s should fail with bad index', (circuitName, args) => { - const [, trueIndex] = shieldedAccessControl.witnesses.wit_getRoleIndex(shieldedAccessControl.getWitnessContext(), DEFAULT_ADMIN_ROLE); - shieldedAccessControl.overrideWitness("wit_getRoleIndex", RETURN_BAD_INDEX); - const [, badIndex] = shieldedAccessControl.witnesses.wit_getRoleIndex(shieldedAccessControl.getWitnessContext(), DEFAULT_ADMIN_ROLE); + describe('checked circuits should fail for unauthorized caller with any witness value', () => { + beforeEach(() => { + shieldedAccessControl._grantRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); + shieldedAccessControl.callerCtx.setCaller(UNAUTHORIZED); + }); - // Check index does not match - expect(trueIndex).not.toBe( - badIndex - ); + type FailingCircuits = [method: keyof ShieldedAccessControlSimulator, isValidNonce: boolean, isValidIndex: boolean, isValidPath: boolean, args: unknown[]]; + const checkedCircuits: FailingCircuits[] = [ + ['assertOnlyRole', false, true, true, [DEFAULT_ADMIN_ROLE]], + ['assertOnlyRole', true, false, true, [DEFAULT_ADMIN_ROLE]], + ['assertOnlyRole', true, true, false, [DEFAULT_ADMIN_ROLE]], + ['assertOnlyRole', false, false, true, [DEFAULT_ADMIN_ROLE]], + ['assertOnlyRole', true, false, false, [DEFAULT_ADMIN_ROLE]], + ['assertOnlyRole', false, true, false, [DEFAULT_ADMIN_ROLE]], + ['assertOnlyRole', false, false, false, [DEFAULT_ADMIN_ROLE]], + ['assertOnlyRole', true, true, true, [DEFAULT_ADMIN_ROLE]], + ['grantRole', false, true, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ['grantRole', true, false, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ['grantRole', true, true, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ['grantRole', false, false, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ['grantRole', true, false, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ['grantRole', false, true, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ['grantRole', false, false, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ['grantRole', true, true, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ['revokeRole', false, true, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ['revokeRole', true, false, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ['revokeRole', true, true, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ['revokeRole', false, false, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ['revokeRole', true, false, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ['revokeRole', false, true, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ['revokeRole', false, false, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ['revokeRole', true, true, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ]; + + it.each(checkedCircuits)('%s should fail with isValidNonce(%s), isValidIndex(%s), isValidPath(%s)', (circuitName, isValidNonce, isValidIndex, isValidPath, args) => { + if (isValidNonce) { + // Check nonce matches + expect(shieldedAccessControl.privateState.getCurrentSecretNonce(DEFAULT_ADMIN_ROLE)).toEqual( + ADMIN_SECRET_NONCE, + ); + } else { + // Check nonce does not match + shieldedAccessControl.privateState.injectSecretNonce(DEFAULT_ADMIN_ROLE, BAD_NONCE); + expect(shieldedAccessControl.privateState.getCurrentSecretNonce(DEFAULT_ADMIN_ROLE)).not.toEqual( + ADMIN_SECRET_NONCE, + ); + } + + if (isValidIndex) { + // Check index matches + const [, witnessCalculatedIndex] = shieldedAccessControl.witnesses.wit_getRoleIndex(shieldedAccessControl.getWitnessContext(), DEFAULT_ADMIN_ROLE); + expect(witnessCalculatedIndex).toBe(INIT_COUNTER); + } else { + // Check index does not match + shieldedAccessControl.overrideWitness('wit_getRoleIndex', RETURN_BAD_INDEX); + const [, witnessCalculatedIndex] = shieldedAccessControl.witnesses.wit_getRoleIndex(shieldedAccessControl.getWitnessContext(), DEFAULT_ADMIN_ROLE); + expect(witnessCalculatedIndex).not.toBe(INIT_COUNTER); + } + + if (isValidPath) { + // Check path matches + const truePath = shieldedAccessControl.getPublicState().ShieldedAccessControl__operatorRoles.findPathForLeaf(EXP_DEFAULT_ADMIN_COMMITMENT); + const [, witnessCalculatedPath] = shieldedAccessControl.witnesses.wit_getRoleCommitmentPath(shieldedAccessControl.getWitnessContext(), EXP_DEFAULT_ADMIN_COMMITMENT); + expect(witnessCalculatedPath).toEqual(truePath); + } else { + // Check path does not match + const truePath = shieldedAccessControl.getPublicState().ShieldedAccessControl__operatorRoles.findPathForLeaf(EXP_DEFAULT_ADMIN_COMMITMENT); + shieldedAccessControl.overrideWitness('wit_getRoleCommitmentPath', RETURN_BAD_PATH); + const [, witnessCalculatedPath] = shieldedAccessControl.witnesses.wit_getRoleIndex(shieldedAccessControl.getWitnessContext(), DEFAULT_ADMIN_ROLE); + expect(witnessCalculatedPath).not.toEqual(truePath); + } + + // Test protected circuit expect(() => { (shieldedAccessControl[circuitName] as (...args: unknown[]) => unknown)(...args); }).toThrow('ShieldedAccessControl: unauthorized account'); }); + }); - it.each(protectedCircuits)('%s should fail with bad role path', (circuitName, args) => { - const expCommitment = buildCommitment( - DEFAULT_ADMIN_ROLE, - Z_ADMIN, - secretNonce, - INIT_COUNTER, - ); - const [, truePath] = shieldedAccessControl.witnesses.wit_getRoleCommitmentPath(shieldedAccessControl.getWitnessContext(), expCommitment); - shieldedAccessControl.overrideWitness("wit_getRoleCommitmentPath", RETURN_BAD_PATH); - const [, badPath] = shieldedAccessControl.witnesses.wit_getRoleCommitmentPath(shieldedAccessControl.getWitnessContext(), expCommitment); + describe('unsupported contract address failure cases', () => { + beforeEach(() => { + shieldedAccessControl._grantRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); + shieldedAccessControl.callerCtx.setCaller(ADMIN); + }); - // Check path does not match - expect(truePath).not.toEqual( - badPath - ); + type FailingCircuits = [method: keyof ShieldedAccessControlSimulator, args: unknown[]]; + const circuitsWithContractAddressCheck: FailingCircuits[] = [ + ['hasRole', [DEFAULT_ADMIN_ROLE, Z_OPERATOR_CONTRACT]], + ['_checkRole', [DEFAULT_ADMIN_ROLE, Z_OPERATOR_CONTRACT]], + ['grantRole', [DEFAULT_ADMIN_ROLE, Z_OPERATOR_CONTRACT]], + ['revokeRole', [DEFAULT_ADMIN_ROLE, Z_OPERATOR_CONTRACT]], + ['_grantRole', [DEFAULT_ADMIN_ROLE, Z_OPERATOR_CONTRACT]], + ['_revokeRole', [DEFAULT_ADMIN_ROLE, Z_OPERATOR_CONTRACT]], + ]; + + it.each(circuitsWithContractAddressCheck)('%s fails if contract address is queried', (circuitName, args) => { + // Test protected circuit expect(() => { (shieldedAccessControl[circuitName] as (...args: unknown[]) => unknown)(...args); - }).toThrow('ShieldedAccessControl: unauthorized account'); + }).toThrow('ShieldedAccessControl: contract address roles are not yet supported'); }); }); @@ -157,6 +291,26 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl._grantRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); }); + type HasRoleTest = [isValidNonce: boolean, isValidIndex: boolean, isValidPath: boolean, args: unknown[]]; + const falseCases: HasRoleTest[] = [ + [false, true, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + [true, false, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + [true, true, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + [false, false, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + [true, false, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + [false, true, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + [false, false, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ]; + + const commitmentDoesNotMatchCases: HasRoleTest[] = [ + [false, true, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + [true, false, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + [false, false, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + [true, false, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + [false, true, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + [false, false, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ]; + it('should throw if caller is contract address', () => { shieldedAccessControl.callerCtx.setCaller(OPERATOR_CONTRACT); expect(() => { @@ -164,11 +318,18 @@ describe('ShieldedAccessControl', () => { }).toThrow('ShieldedAccessControl: contract address roles are not yet supported'); }); + it('should throw if role has been revoked', () => { + shieldedAccessControl._revokeRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); + expect(() => { + shieldedAccessControl.hasRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); + }).toThrow("ShieldedAccessControl: role access has been revoked"); + }); + it('should return correct role commitment', () => { const expCommitment = buildCommitment( DEFAULT_ADMIN_ROLE, Z_ADMIN, - secretNonce, + ADMIN_SECRET_NONCE, INIT_COUNTER, ); @@ -181,22 +342,101 @@ describe('ShieldedAccessControl', () => { expect(role.isApproved).toEqual(true); }); - it('should return false when unauthorized', () => { + it('should return false when unauthorized does not have role', () => { const role = shieldedAccessControl.hasRole(DEFAULT_ADMIN_ROLE, Z_UNAUTHORIZED); expect(role.isApproved).toEqual(false); - }) + }); it('should return false when role does not exist', () => { - shieldedAccessControl.privateState.injectSecretNonce(Buffer.from(UNINITIALIZED_ROLE), Buffer.alloc(32)); + shieldedAccessControl.privateState.injectSecretNonce(UNINITIALIZED_ROLE, Buffer.alloc(32)); const role = shieldedAccessControl.hasRole(UNINITIALIZED_ROLE, Z_UNAUTHORIZED); expect(role.isApproved).toBe(false); }); - it('should fail when role access has been revoked', () => { - shieldedAccessControl._revokeRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); - expect(() => { - shieldedAccessControl.hasRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); - }).toThrow("ShieldedAccessControl: role access has been revoked"); + it.each(falseCases)('should return false with any invalid witness value - isValidNonce(%s), isValidIndex(%s), isValidPath(%s)', (isValidNonce, isValidIndex, isValidPath, args) => { + if (isValidNonce) { + // Check nonce matches + expect(shieldedAccessControl.privateState.getCurrentSecretNonce(DEFAULT_ADMIN_ROLE)).toEqual( + ADMIN_SECRET_NONCE, + ); + } else { + // Check nonce does not match + shieldedAccessControl.privateState.injectSecretNonce(DEFAULT_ADMIN_ROLE, BAD_NONCE); + expect(shieldedAccessControl.privateState.getCurrentSecretNonce(DEFAULT_ADMIN_ROLE)).not.toEqual( + ADMIN_SECRET_NONCE, + ); + } + + if (isValidIndex) { + // Check index matches + const [, witnessCalculatedIndex] = shieldedAccessControl.witnesses.wit_getRoleIndex(shieldedAccessControl.getWitnessContext(), DEFAULT_ADMIN_ROLE); + expect(witnessCalculatedIndex).toBe(INIT_COUNTER); + } else { + // Check index does not match + shieldedAccessControl.overrideWitness('wit_getRoleIndex', RETURN_BAD_INDEX); + const [, witnessCalculatedIndex] = shieldedAccessControl.witnesses.wit_getRoleIndex(shieldedAccessControl.getWitnessContext(), DEFAULT_ADMIN_ROLE); + expect(witnessCalculatedIndex).not.toBe(INIT_COUNTER); + } + + if (isValidPath) { + // Check path matches + const truePath = shieldedAccessControl.getPublicState().ShieldedAccessControl__operatorRoles.findPathForLeaf(EXP_DEFAULT_ADMIN_COMMITMENT); + const [, witnessCalculatedPath] = shieldedAccessControl.witnesses.wit_getRoleCommitmentPath(shieldedAccessControl.getWitnessContext(), EXP_DEFAULT_ADMIN_COMMITMENT); + expect(witnessCalculatedPath).toEqual(truePath); + } else { + // Check path does not match + const truePath = shieldedAccessControl.getPublicState().ShieldedAccessControl__operatorRoles.findPathForLeaf(EXP_DEFAULT_ADMIN_COMMITMENT); + shieldedAccessControl.overrideWitness('wit_getRoleCommitmentPath', RETURN_BAD_PATH); + const [, witnessCalculatedPath] = shieldedAccessControl.witnesses.wit_getRoleIndex(shieldedAccessControl.getWitnessContext(), DEFAULT_ADMIN_ROLE); + expect(witnessCalculatedPath).not.toEqual(truePath); + } + + // Test false case circuit + const role = (shieldedAccessControl['hasRole'] as (...args: unknown[]) => Role)(...args); + expect(role.isApproved).toBe(false); + }); + + it.each(commitmentDoesNotMatchCases)('commitment should not match with invalid nonce or index - isValidNonce(%s), isValidIndex(%s), isValidPath(%s)', (isValidNonce, isValidIndex, isValidPath, args) => { + if (isValidNonce) { + // Check nonce matches + expect(shieldedAccessControl.privateState.getCurrentSecretNonce(DEFAULT_ADMIN_ROLE)).toEqual( + ADMIN_SECRET_NONCE, + ); + } else { + // Check nonce does not match + shieldedAccessControl.privateState.injectSecretNonce(DEFAULT_ADMIN_ROLE, BAD_NONCE); + expect(shieldedAccessControl.privateState.getCurrentSecretNonce(DEFAULT_ADMIN_ROLE)).not.toEqual( + ADMIN_SECRET_NONCE, + ); + } + + if (isValidIndex) { + // Check index matches + const [, witnessCalculatedIndex] = shieldedAccessControl.witnesses.wit_getRoleIndex(shieldedAccessControl.getWitnessContext(), DEFAULT_ADMIN_ROLE); + expect(witnessCalculatedIndex).toBe(INIT_COUNTER); + } else { + // Check index does not match + shieldedAccessControl.overrideWitness('wit_getRoleIndex', RETURN_BAD_INDEX); + const [, witnessCalculatedIndex] = shieldedAccessControl.witnesses.wit_getRoleIndex(shieldedAccessControl.getWitnessContext(), DEFAULT_ADMIN_ROLE); + expect(witnessCalculatedIndex).not.toBe(INIT_COUNTER); + } + + if (isValidPath) { + // Check path matches + const truePath = shieldedAccessControl.getPublicState().ShieldedAccessControl__operatorRoles.findPathForLeaf(EXP_DEFAULT_ADMIN_COMMITMENT); + const [, witnessCalculatedPath] = shieldedAccessControl.witnesses.wit_getRoleCommitmentPath(shieldedAccessControl.getWitnessContext(), EXP_DEFAULT_ADMIN_COMMITMENT); + expect(witnessCalculatedPath).toEqual(truePath); + } else { + // Check path does not match + const truePath = shieldedAccessControl.getPublicState().ShieldedAccessControl__operatorRoles.findPathForLeaf(EXP_DEFAULT_ADMIN_COMMITMENT); + shieldedAccessControl.overrideWitness('wit_getRoleCommitmentPath', RETURN_BAD_PATH); + const [, witnessCalculatedPath] = shieldedAccessControl.witnesses.wit_getRoleIndex(shieldedAccessControl.getWitnessContext(), DEFAULT_ADMIN_ROLE); + expect(witnessCalculatedPath).not.toEqual(truePath); + } + + // Test false case circuit + const role = (shieldedAccessControl['hasRole'] as (...args: unknown[]) => Role)(...args); + expect(role.roleCommitment).not.toEqual(EXP_DEFAULT_ADMIN_COMMITMENT); }); }); @@ -206,17 +446,37 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl.callerCtx.setCaller(ADMIN); }); - it('should allow authorized caller with correct nonce to call', () => { + it('should not fail when authorized caller has correct nonce, index, and path', () => { + // Check nonce is correct + expect(shieldedAccessControl.privateState.getCurrentSecretNonce(DEFAULT_ADMIN_ROLE)).toBe(ADMIN_SECRET_NONCE); + + // Check index matches + const [, witnessCalculatedIndex] = shieldedAccessControl.witnesses.wit_getRoleIndex(shieldedAccessControl.getWitnessContext(), DEFAULT_ADMIN_ROLE); + expect(witnessCalculatedIndex).toBe(INIT_COUNTER); + + // Check path matches + const truePath = shieldedAccessControl.getPublicState().ShieldedAccessControl__operatorRoles.findPathForLeaf(EXP_DEFAULT_ADMIN_COMMITMENT); + const [, witnessCalculatedPath] = shieldedAccessControl.witnesses.wit_getRoleCommitmentPath(shieldedAccessControl.getWitnessContext(), EXP_DEFAULT_ADMIN_COMMITMENT); + expect(witnessCalculatedPath).toEqual(truePath); + expect(() => shieldedAccessControl.assertOnlyRole(DEFAULT_ADMIN_ROLE), ).not.toThrow(); }); - it('should throw if caller is unauthorized', () => { - shieldedAccessControl.callerCtx.setCaller(UNAUTHORIZED); - expect(() => - shieldedAccessControl.assertOnlyRole(DEFAULT_ADMIN_ROLE), - ).toThrow('ShieldedAccessControl: unauthorized account'); + it('should not fail for admin with multiple roles', () => { + shieldedAccessControl.privateState.injectSecretNonce(OPERATOR_ROLE_1, OPERATOR_ROLE_1_SECRET_NONCE); + shieldedAccessControl.privateState.injectSecretNonce(OPERATOR_ROLE_2, OPERATOR_ROLE_2_SECRET_NONCE); + shieldedAccessControl.privateState.injectSecretNonce(OPERATOR_ROLE_3, OPERATOR_ROLE_3_SECRET_NONCE); + shieldedAccessControl._grantRole(OPERATOR_ROLE_1, Z_ADMIN); + shieldedAccessControl._grantRole(OPERATOR_ROLE_2, Z_ADMIN); + shieldedAccessControl._grantRole(OPERATOR_ROLE_3, Z_ADMIN); + expect(() => { + shieldedAccessControl.assertOnlyRole(DEFAULT_ADMIN_ROLE); + shieldedAccessControl.assertOnlyRole(OPERATOR_ROLE_1); + shieldedAccessControl.assertOnlyRole(OPERATOR_ROLE_2); + shieldedAccessControl.assertOnlyRole(OPERATOR_ROLE_3); + }).not.toThrow(); }); }); }); From 7e6a6a0610ba25e0749c6b152f1c1c9e7b977446 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Thu, 11 Sep 2025 14:39:14 -0400 Subject: [PATCH 187/322] Should not throw if commitment in nullifer set --- contracts/src/access/ShieldedAccessControl.compact | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index 17a9f746..38b3fe94 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -156,12 +156,16 @@ module ShieldedAccessControl { const nonce = wit_secretNonce(roleId); const index = wit_getRoleIndex(roleId); const roleCommitment = persistentHash>>([roleId, account.left.bytes, nonce, index as Field as Bytes<32>, pad(32, "ShieldedAccessControl:shield:")]); - assert(!_roleCommitmentNullifiers.member(disclose(roleCommitment)), "ShieldedAccessControl: role access has been revoked"); const authPath = wit_getRoleCommitmentPath(roleCommitment); - const isApproved = _operatorRoles + const rootMatches = _operatorRoles .checkRoot(merkleTreePathRoot<10, Bytes<32>>(disclose(authPath))); - return Role {isApproved, disclose(roleCommitment)}; + + if(!_roleCommitmentNullifiers.member(roleCommitment) && rootMatches) { + return Role {isApproved: true, disclose(roleCommitment)}; + } else { + return Role {isApproved: false, disclose(roleCommitment)} + } } /** From 8e5c26f7976bcc0758c9fbd4264ba0ae30c2fee8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Thu, 11 Sep 2025 19:44:29 -0400 Subject: [PATCH 188/322] Export nullifiers for testing --- .../src/access/test/mocks/MockShieldedAccessControl.compact | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact index 12c4faa3..59c24a52 100644 --- a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact +++ b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact @@ -15,6 +15,7 @@ export { ShieldedAccessControl_DEFAULT_ADMIN_ROLE, ShieldedAccessControl__operatorRoles, ShieldedAccessControl__currentMerkleTreeIndex, + ShieldedAccessControl__roleCommitmentNullifiers, ShieldedAccessControl_Role }; From dd0cd82d369d8aa1e943091f878079836e399eda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Thu, 11 Sep 2025 19:45:13 -0400 Subject: [PATCH 189/322] Rename var and change return behavior --- contracts/src/access/ShieldedAccessControl.compact | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index 38b3fe94..95abf9d2 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -155,16 +155,16 @@ module ShieldedAccessControl { const nonce = wit_secretNonce(roleId); const index = wit_getRoleIndex(roleId); - const roleCommitment = persistentHash>>([roleId, account.left.bytes, nonce, index as Field as Bytes<32>, pad(32, "ShieldedAccessControl:shield:")]); + const computedCommitment = persistentHash>>([roleId, account.left.bytes, nonce, index as Field as Bytes<32>, pad(32, "ShieldedAccessControl:shield:")]); - const authPath = wit_getRoleCommitmentPath(roleCommitment); + const authPath = wit_getRoleCommitmentPath(computedCommitment); const rootMatches = _operatorRoles .checkRoot(merkleTreePathRoot<10, Bytes<32>>(disclose(authPath))); - if(!_roleCommitmentNullifiers.member(roleCommitment) && rootMatches) { - return Role {isApproved: true, disclose(roleCommitment)}; + if(!_roleCommitmentNullifiers.member(disclose(computedCommitment)) && rootMatches) { + return Role {isApproved: true, roleCommitment: disclose(computedCommitment)}; } else { - return Role {isApproved: false, disclose(roleCommitment)} + return Role {isApproved: false, roleCommitment: disclose(computedCommitment)}; } } From f24eeb0f2bc95db537fd712ade8eb51fd623f158 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Thu, 11 Sep 2025 19:45:37 -0400 Subject: [PATCH 190/322] Add _checkRole, grantRole tests --- .../access/test/ShieldedAccessControl.test.ts | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/contracts/src/access/test/ShieldedAccessControl.test.ts b/contracts/src/access/test/ShieldedAccessControl.test.ts index 29af7e81..abf9bd35 100644 --- a/contracts/src/access/test/ShieldedAccessControl.test.ts +++ b/contracts/src/access/test/ShieldedAccessControl.test.ts @@ -16,7 +16,10 @@ const [ADMIN, Z_ADMIN] = utils.generateEitherPubKeyPair('ADMIN'); const [UNAUTHORIZED, Z_UNAUTHORIZED] = utils.generateEitherPubKeyPair('UNAUTHORIZED'); const [CUSTOM_ADMIN, Z_CUSTOM_ADMIN] = utils.generateEitherPubKeyPair('CUSTOM_ADMIN'); const [OPERATOR_1, Z_OPERATOR_1] = utils.generateEitherPubKeyPair('OPERATOR_1'); +const [OPERATOR_2, Z_OPERATOR_2] = utils.generateEitherPubKeyPair('OPERATOR_2'); +const [OPERATOR_3, Z_OPERATOR_3] = utils.generateEitherPubKeyPair('OPERATOR_3'); const [OPERATOR_CONTRACT, Z_OPERATOR_CONTRACT] = utils.generateEitherPubKeyPair('OPERATOR_CONTRACT', false); +const Z_OPERATOR_LIST = [Z_OPERATOR_1, Z_OPERATOR_2, Z_OPERATOR_3]; // Constants const BAD_NONCE = Buffer.alloc(32, 'BAD_NONCE'); @@ -32,6 +35,7 @@ const OPERATOR_ROLE_2 = convert_bigint_to_Uint8Array(32, 2n); const OPERATOR_ROLE_3 = convert_bigint_to_Uint8Array(32, 3n); const CUSTOM_ADMIN_ROLE = convert_bigint_to_Uint8Array(32, 4n); const UNINITIALIZED_ROLE = convert_bigint_to_Uint8Array(32, 5n); +const OPERATOR_ROLE_LIST = [OPERATOR_ROLE_1, OPERATOR_ROLE_2, OPERATOR_ROLE_3]; const operatorTypes = [ ['contract', Z_OPERATOR_CONTRACT], @@ -45,6 +49,7 @@ const ADMIN_SECRET_NONCE = Buffer.alloc(32, "ADMIN_SECRET_NONCE"); const OPERATOR_ROLE_1_SECRET_NONCE = Buffer.alloc(32, "OPERATOR_ROLE_1_SECRET_NONCE"); const OPERATOR_ROLE_2_SECRET_NONCE = Buffer.alloc(32, "OPERATOR_ROLE_2_SECRET_NONCE"); const OPERATOR_ROLE_3_SECRET_NONCE = Buffer.alloc(32, "OPERATOR_ROLE_3_SECRET_NONCE"); +const OPERATOR_ROLE_SECRET_NONCES = [OPERATOR_ROLE_1_SECRET_NONCE, OPERATOR_ROLE_2_SECRET_NONCE, OPERATOR_ROLE_3_SECRET_NONCE]; let shieldedAccessControl: ShieldedAccessControlSimulator; // Helpers @@ -479,4 +484,117 @@ describe('ShieldedAccessControl', () => { }).not.toThrow(); }); }); + + describe('_checkRole', () => { + beforeEach(() => { + shieldedAccessControl._grantRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); + shieldedAccessControl.callerCtx.setCaller(ADMIN); + }); + + it('should not throw if admin has role', () => { + expect(() => + shieldedAccessControl._checkRole(DEFAULT_ADMIN_ROLE, Z_ADMIN), + ).not.toThrow(); + }); + + it('should throw if unauthorized does not have role', () => { + expect(() => + shieldedAccessControl._checkRole(DEFAULT_ADMIN_ROLE, Z_UNAUTHORIZED), + ).toThrow('ShieldedAccessControl: unauthorized account'); + }); + }); + + describe('getRoleAdmin', () => { + it('should return default admin role if admin role not set', () => { + expect(shieldedAccessControl.getRoleAdmin(OPERATOR_ROLE_1)).toEqual( + DEFAULT_ADMIN_ROLE, + ); + }); + + it('should return custom admin role if set', () => { + shieldedAccessControl._setRoleAdmin(OPERATOR_ROLE_1, CUSTOM_ADMIN_ROLE); + expect(shieldedAccessControl.getRoleAdmin(OPERATOR_ROLE_1)).toEqual( + CUSTOM_ADMIN_ROLE, + ); + }); + }); + + describe('grantRole', () => { + beforeEach(() => { + shieldedAccessControl._grantRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); + shieldedAccessControl.callerCtx.setCaller(ADMIN); + }); + + it('admin should grant role', () => { + shieldedAccessControl.privateState.injectSecretNonce(OPERATOR_ROLE_1, OPERATOR_ROLE_1_SECRET_NONCE); + shieldedAccessControl.grantRole(OPERATOR_ROLE_1, Z_OPERATOR_1); + const role: Role = shieldedAccessControl.hasRole(OPERATOR_ROLE_1, Z_OPERATOR_1); + expect(role.isApproved).toBe( + true, + ); + }); + + it('path for role should exist in Merkle tree', () => { + expect(shieldedAccessControl.getPublicState().ShieldedAccessControl__operatorRoles.findPathForLeaf(EXP_DEFAULT_ADMIN_COMMITMENT)).toBeDefined(); + }); + + it('should update Merkle tree root', () => { + expect(shieldedAccessControl.getPublicState().ShieldedAccessControl__operatorRoles.root().field).toBeGreaterThan(0n); + }); + + it('_currentMerkleTreeIndex should increment', () => { + // Starts at 1 because we grant role to self in beforeEach + expect(shieldedAccessControl.getPublicState().ShieldedAccessControl__currentMerkleTreeIndex).toBe(1n); + + shieldedAccessControl.privateState.injectSecretNonce(OPERATOR_ROLE_1, OPERATOR_ROLE_1_SECRET_NONCE); + shieldedAccessControl.privateState.injectSecretNonce(OPERATOR_ROLE_2, OPERATOR_ROLE_2_SECRET_NONCE); + shieldedAccessControl.privateState.injectSecretNonce(OPERATOR_ROLE_3, OPERATOR_ROLE_3_SECRET_NONCE); + + shieldedAccessControl.grantRole(OPERATOR_ROLE_1, Z_OPERATOR_1); + expect(shieldedAccessControl.getPublicState().ShieldedAccessControl__currentMerkleTreeIndex).toBe(2n); + + shieldedAccessControl.grantRole(OPERATOR_ROLE_2, Z_OPERATOR_2); + expect(shieldedAccessControl.getPublicState().ShieldedAccessControl__currentMerkleTreeIndex).toBe(3n); + + shieldedAccessControl.grantRole(OPERATOR_ROLE_3, Z_OPERATOR_3); + expect(shieldedAccessControl.getPublicState().ShieldedAccessControl__currentMerkleTreeIndex).toBe(4n); + }); + + + + it('admin should grant multiple roles', () => { + for (let i = 0; i < OPERATOR_ROLE_LIST.length; i++) { + shieldedAccessControl.privateState.injectSecretNonce(OPERATOR_ROLE_LIST[i], OPERATOR_ROLE_SECRET_NONCES[i]); + for (let j = 0; j < Z_OPERATOR_LIST.length; j++) { + shieldedAccessControl.grantRole(OPERATOR_ROLE_LIST[i], Z_OPERATOR_LIST[j]); + const role: Role = shieldedAccessControl.hasRole(OPERATOR_ROLE_LIST[i], Z_OPERATOR_LIST[j]) + expect(role.isApproved).toBe( + true, + ); + } + } + }); + + it('should throw if non-admin operator grants role', () => { + shieldedAccessControl.privateState.injectSecretNonce(OPERATOR_ROLE_1, OPERATOR_ROLE_1_SECRET_NONCE); + shieldedAccessControl._grantRole(OPERATOR_ROLE_1, Z_OPERATOR_1); + + shieldedAccessControl.callerCtx.setCaller(OPERATOR_1); + expect(() => { + shieldedAccessControl.grantRole(OPERATOR_ROLE_1, Z_UNAUTHORIZED); + }).toThrow('ShieldedAccessControl: unauthorized account'); + }); + }); + + describe('revokeRole', () => { + beforeEach(() => { + shieldedAccessControl._grantRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); + shieldedAccessControl.privateState.injectSecretNonce(OPERATOR_ROLE_1, OPERATOR_ROLE_1_SECRET_NONCE); + shieldedAccessControl.callerCtx.setCaller(ADMIN); + }); + + it.todo('admin should revoke role', () => { + + }); + }); }); From 2cb227e66bd687f025628e0b26d2bdecb26f62e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Thu, 11 Sep 2025 19:47:15 -0400 Subject: [PATCH 191/322] fmt files --- .../access/test/ShieldedAccessControl.test.ts | 767 +++++++++++++----- .../ShieldedAccessControlSimulator.ts | 96 ++- contracts/src/access/test/utils/address.ts | 32 +- .../ShieldedAccessControlWitnesses.ts | 132 ++- 4 files changed, 733 insertions(+), 294 deletions(-) diff --git a/contracts/src/access/test/ShieldedAccessControl.test.ts b/contracts/src/access/test/ShieldedAccessControl.test.ts index abf9bd35..73b5e4e3 100644 --- a/contracts/src/access/test/ShieldedAccessControl.test.ts +++ b/contracts/src/access/test/ShieldedAccessControl.test.ts @@ -3,22 +3,34 @@ import { CompactTypeVector, convert_bigint_to_Uint8Array, persistentHash, - WitnessContext, + type WitnessContext, } from '@midnight-ntwrk/compact-runtime'; import { beforeEach, describe, expect, it } from 'vitest'; -import type { ZswapCoinPublicKey, Either, ContractAddress, Ledger, MerkleTreePath, ShieldedAccessControl_Role as Role } from '../../../artifacts/MockShieldedAccessControl/contract/index.cjs'; +import type { + ContractAddress, + Either, + Ledger, + MerkleTreePath, + ShieldedAccessControl_Role as Role, + ZswapCoinPublicKey, +} from '../../../artifacts/MockShieldedAccessControl/contract/index.cjs'; import { ShieldedAccessControlPrivateState } from '../witnesses/ShieldedAccessControlWitnesses.js'; import { ShieldedAccessControlSimulator } from './simulators/ShieldedAccessControlSimulator.js'; import * as utils from './utils/address.js'; // PKs const [ADMIN, Z_ADMIN] = utils.generateEitherPubKeyPair('ADMIN'); -const [UNAUTHORIZED, Z_UNAUTHORIZED] = utils.generateEitherPubKeyPair('UNAUTHORIZED'); -const [CUSTOM_ADMIN, Z_CUSTOM_ADMIN] = utils.generateEitherPubKeyPair('CUSTOM_ADMIN'); +const [UNAUTHORIZED, Z_UNAUTHORIZED] = + utils.generateEitherPubKeyPair('UNAUTHORIZED'); +const [CUSTOM_ADMIN, Z_CUSTOM_ADMIN] = + utils.generateEitherPubKeyPair('CUSTOM_ADMIN'); const [OPERATOR_1, Z_OPERATOR_1] = utils.generateEitherPubKeyPair('OPERATOR_1'); const [OPERATOR_2, Z_OPERATOR_2] = utils.generateEitherPubKeyPair('OPERATOR_2'); const [OPERATOR_3, Z_OPERATOR_3] = utils.generateEitherPubKeyPair('OPERATOR_3'); -const [OPERATOR_CONTRACT, Z_OPERATOR_CONTRACT] = utils.generateEitherPubKeyPair('OPERATOR_CONTRACT', false); +const [OPERATOR_CONTRACT, Z_OPERATOR_CONTRACT] = utils.generateEitherPubKeyPair( + 'OPERATOR_CONTRACT', + false, +); const Z_OPERATOR_LIST = [Z_OPERATOR_1, Z_OPERATOR_2, Z_OPERATOR_3]; // Constants @@ -43,13 +55,27 @@ const operatorTypes = [ ] as const; // Role to string -const DEFAULT_ADMIN_ROLE_TO_STRING = Buffer.from(DEFAULT_ADMIN_ROLE).toString('hex'); +const DEFAULT_ADMIN_ROLE_TO_STRING = + Buffer.from(DEFAULT_ADMIN_ROLE).toString('hex'); -const ADMIN_SECRET_NONCE = Buffer.alloc(32, "ADMIN_SECRET_NONCE"); -const OPERATOR_ROLE_1_SECRET_NONCE = Buffer.alloc(32, "OPERATOR_ROLE_1_SECRET_NONCE"); -const OPERATOR_ROLE_2_SECRET_NONCE = Buffer.alloc(32, "OPERATOR_ROLE_2_SECRET_NONCE"); -const OPERATOR_ROLE_3_SECRET_NONCE = Buffer.alloc(32, "OPERATOR_ROLE_3_SECRET_NONCE"); -const OPERATOR_ROLE_SECRET_NONCES = [OPERATOR_ROLE_1_SECRET_NONCE, OPERATOR_ROLE_2_SECRET_NONCE, OPERATOR_ROLE_3_SECRET_NONCE]; +const ADMIN_SECRET_NONCE = Buffer.alloc(32, 'ADMIN_SECRET_NONCE'); +const OPERATOR_ROLE_1_SECRET_NONCE = Buffer.alloc( + 32, + 'OPERATOR_ROLE_1_SECRET_NONCE', +); +const OPERATOR_ROLE_2_SECRET_NONCE = Buffer.alloc( + 32, + 'OPERATOR_ROLE_2_SECRET_NONCE', +); +const OPERATOR_ROLE_3_SECRET_NONCE = Buffer.alloc( + 32, + 'OPERATOR_ROLE_3_SECRET_NONCE', +); +const OPERATOR_ROLE_SECRET_NONCES = [ + OPERATOR_ROLE_1_SECRET_NONCE, + OPERATOR_ROLE_2_SECRET_NONCE, + OPERATOR_ROLE_3_SECRET_NONCE, +]; let shieldedAccessControl: ShieldedAccessControlSimulator; // Helpers @@ -82,17 +108,23 @@ const EXP_DEFAULT_ADMIN_COMMITMENT = buildCommitment( INIT_COUNTER, ); -function RETURN_BAD_INDEX(context: WitnessContext, roleId: Uint8Array): [ShieldedAccessControlPrivateState, bigint] { - return [context.privateState, 1023n] +function RETURN_BAD_INDEX( + context: WitnessContext, + roleId: Uint8Array, +): [ShieldedAccessControlPrivateState, bigint] { + return [context.privateState, 1023n]; } -function RETURN_BAD_PATH(context: WitnessContext, roleCommitment: Uint8Array): [ShieldedAccessControlPrivateState, MerkleTreePath] { +function RETURN_BAD_PATH( + context: WitnessContext, + roleCommitment: Uint8Array, +): [ShieldedAccessControlPrivateState, MerkleTreePath] { const defaultPath: MerkleTreePath = { leaf: new Uint8Array(32), path: Array.from({ length: 10 }, () => ({ sibling: { field: 0n }, goes_left: false, - })) + })), }; return [context.privateState, defaultPath]; } @@ -100,12 +132,16 @@ function RETURN_BAD_PATH(context: WitnessContext { beforeEach(() => { // Create private state object and generate nonce - const PS = ShieldedAccessControlPrivateState.withRoleAndNonce(Z_ADMIN, Buffer.from(DEFAULT_ADMIN_ROLE), ADMIN_SECRET_NONCE); + const PS = ShieldedAccessControlPrivateState.withRoleAndNonce( + Z_ADMIN, + Buffer.from(DEFAULT_ADMIN_ROLE), + ADMIN_SECRET_NONCE, + ); // Init contract for user with PS shieldedAccessControl = new ShieldedAccessControlSimulator(Z_ADMIN, { privateState: PS, @@ -118,7 +154,13 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl.callerCtx.setCaller(ADMIN); }); - type FailingCircuits = [method: keyof ShieldedAccessControlSimulator, isValidNonce: boolean, isValidIndex: boolean, isValidPath: boolean, args: unknown[]]; + type FailingCircuits = [ + method: keyof ShieldedAccessControlSimulator, + isValidNonce: boolean, + isValidIndex: boolean, + isValidPath: boolean, + args: unknown[], + ]; const checkedCircuits: FailingCircuits[] = [ ['assertOnlyRole', false, true, true, [DEFAULT_ADMIN_ROLE]], ['assertOnlyRole', true, false, true, [DEFAULT_ADMIN_ROLE]], @@ -143,49 +185,93 @@ describe('ShieldedAccessControl', () => { ['revokeRole', false, false, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], ]; - it.each(checkedCircuits)('%s should fail with isValidNonce(%s), isValidIndex(%s), isValidPath(%s)', (circuitName, isValidNonce, isValidIndex, isValidPath, args) => { - if (isValidNonce) { - // Check nonce matches - expect(shieldedAccessControl.privateState.getCurrentSecretNonce(DEFAULT_ADMIN_ROLE)).toEqual( - ADMIN_SECRET_NONCE, - ); - } else { - // Check nonce does not match - shieldedAccessControl.privateState.injectSecretNonce(DEFAULT_ADMIN_ROLE, BAD_NONCE); - expect(shieldedAccessControl.privateState.getCurrentSecretNonce(DEFAULT_ADMIN_ROLE)).not.toEqual( - ADMIN_SECRET_NONCE, - ); - } + it.each(checkedCircuits)( + '%s should fail with isValidNonce(%s), isValidIndex(%s), isValidPath(%s)', + (circuitName, isValidNonce, isValidIndex, isValidPath, args) => { + if (isValidNonce) { + // Check nonce matches + expect( + shieldedAccessControl.privateState.getCurrentSecretNonce( + DEFAULT_ADMIN_ROLE, + ), + ).toEqual(ADMIN_SECRET_NONCE); + } else { + // Check nonce does not match + shieldedAccessControl.privateState.injectSecretNonce( + DEFAULT_ADMIN_ROLE, + BAD_NONCE, + ); + expect( + shieldedAccessControl.privateState.getCurrentSecretNonce( + DEFAULT_ADMIN_ROLE, + ), + ).not.toEqual(ADMIN_SECRET_NONCE); + } - if (isValidIndex) { - // Check index matches - const [, witnessCalculatedIndex] = shieldedAccessControl.witnesses.wit_getRoleIndex(shieldedAccessControl.getWitnessContext(), DEFAULT_ADMIN_ROLE); - expect(witnessCalculatedIndex).toBe(INIT_COUNTER); - } else { - // Check index does not match - shieldedAccessControl.overrideWitness('wit_getRoleIndex', RETURN_BAD_INDEX); - const [, witnessCalculatedIndex] = shieldedAccessControl.witnesses.wit_getRoleIndex(shieldedAccessControl.getWitnessContext(), DEFAULT_ADMIN_ROLE); - expect(witnessCalculatedIndex).not.toBe(INIT_COUNTER); - } + if (isValidIndex) { + // Check index matches + const [, witnessCalculatedIndex] = + shieldedAccessControl.witnesses.wit_getRoleIndex( + shieldedAccessControl.getWitnessContext(), + DEFAULT_ADMIN_ROLE, + ); + expect(witnessCalculatedIndex).toBe(INIT_COUNTER); + } else { + // Check index does not match + shieldedAccessControl.overrideWitness( + 'wit_getRoleIndex', + RETURN_BAD_INDEX, + ); + const [, witnessCalculatedIndex] = + shieldedAccessControl.witnesses.wit_getRoleIndex( + shieldedAccessControl.getWitnessContext(), + DEFAULT_ADMIN_ROLE, + ); + expect(witnessCalculatedIndex).not.toBe(INIT_COUNTER); + } - if (isValidPath) { - // Check path matches - const truePath = shieldedAccessControl.getPublicState().ShieldedAccessControl__operatorRoles.findPathForLeaf(EXP_DEFAULT_ADMIN_COMMITMENT); - const [, witnessCalculatedPath] = shieldedAccessControl.witnesses.wit_getRoleCommitmentPath(shieldedAccessControl.getWitnessContext(), EXP_DEFAULT_ADMIN_COMMITMENT); - expect(witnessCalculatedPath).toEqual(truePath); - } else { - // Check path does not match - const truePath = shieldedAccessControl.getPublicState().ShieldedAccessControl__operatorRoles.findPathForLeaf(EXP_DEFAULT_ADMIN_COMMITMENT); - shieldedAccessControl.overrideWitness('wit_getRoleCommitmentPath', RETURN_BAD_PATH); - const [, witnessCalculatedPath] = shieldedAccessControl.witnesses.wit_getRoleIndex(shieldedAccessControl.getWitnessContext(), DEFAULT_ADMIN_ROLE); - expect(witnessCalculatedPath).not.toEqual(truePath); - } + if (isValidPath) { + // Check path matches + const truePath = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf( + EXP_DEFAULT_ADMIN_COMMITMENT, + ); + const [, witnessCalculatedPath] = + shieldedAccessControl.witnesses.wit_getRoleCommitmentPath( + shieldedAccessControl.getWitnessContext(), + EXP_DEFAULT_ADMIN_COMMITMENT, + ); + expect(witnessCalculatedPath).toEqual(truePath); + } else { + // Check path does not match + const truePath = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf( + EXP_DEFAULT_ADMIN_COMMITMENT, + ); + shieldedAccessControl.overrideWitness( + 'wit_getRoleCommitmentPath', + RETURN_BAD_PATH, + ); + const [, witnessCalculatedPath] = + shieldedAccessControl.witnesses.wit_getRoleIndex( + shieldedAccessControl.getWitnessContext(), + DEFAULT_ADMIN_ROLE, + ); + expect(witnessCalculatedPath).not.toEqual(truePath); + } - // Test protected circuit - expect(() => { - (shieldedAccessControl[circuitName] as (...args: unknown[]) => unknown)(...args); - }).toThrow('ShieldedAccessControl: unauthorized account'); - }); + // Test protected circuit + expect(() => { + ( + shieldedAccessControl[circuitName] as ( + ...args: unknown[] + ) => unknown + )(...args); + }).toThrow('ShieldedAccessControl: unauthorized account'); + }, + ); }); describe('checked circuits should fail for unauthorized caller with any witness value', () => { @@ -194,7 +280,13 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl.callerCtx.setCaller(UNAUTHORIZED); }); - type FailingCircuits = [method: keyof ShieldedAccessControlSimulator, isValidNonce: boolean, isValidIndex: boolean, isValidPath: boolean, args: unknown[]]; + type FailingCircuits = [ + method: keyof ShieldedAccessControlSimulator, + isValidNonce: boolean, + isValidIndex: boolean, + isValidPath: boolean, + args: unknown[], + ]; const checkedCircuits: FailingCircuits[] = [ ['assertOnlyRole', false, true, true, [DEFAULT_ADMIN_ROLE]], ['assertOnlyRole', true, false, true, [DEFAULT_ADMIN_ROLE]], @@ -222,49 +314,93 @@ describe('ShieldedAccessControl', () => { ['revokeRole', true, true, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], ]; - it.each(checkedCircuits)('%s should fail with isValidNonce(%s), isValidIndex(%s), isValidPath(%s)', (circuitName, isValidNonce, isValidIndex, isValidPath, args) => { - if (isValidNonce) { - // Check nonce matches - expect(shieldedAccessControl.privateState.getCurrentSecretNonce(DEFAULT_ADMIN_ROLE)).toEqual( - ADMIN_SECRET_NONCE, - ); - } else { - // Check nonce does not match - shieldedAccessControl.privateState.injectSecretNonce(DEFAULT_ADMIN_ROLE, BAD_NONCE); - expect(shieldedAccessControl.privateState.getCurrentSecretNonce(DEFAULT_ADMIN_ROLE)).not.toEqual( - ADMIN_SECRET_NONCE, - ); - } + it.each(checkedCircuits)( + '%s should fail with isValidNonce(%s), isValidIndex(%s), isValidPath(%s)', + (circuitName, isValidNonce, isValidIndex, isValidPath, args) => { + if (isValidNonce) { + // Check nonce matches + expect( + shieldedAccessControl.privateState.getCurrentSecretNonce( + DEFAULT_ADMIN_ROLE, + ), + ).toEqual(ADMIN_SECRET_NONCE); + } else { + // Check nonce does not match + shieldedAccessControl.privateState.injectSecretNonce( + DEFAULT_ADMIN_ROLE, + BAD_NONCE, + ); + expect( + shieldedAccessControl.privateState.getCurrentSecretNonce( + DEFAULT_ADMIN_ROLE, + ), + ).not.toEqual(ADMIN_SECRET_NONCE); + } - if (isValidIndex) { - // Check index matches - const [, witnessCalculatedIndex] = shieldedAccessControl.witnesses.wit_getRoleIndex(shieldedAccessControl.getWitnessContext(), DEFAULT_ADMIN_ROLE); - expect(witnessCalculatedIndex).toBe(INIT_COUNTER); - } else { - // Check index does not match - shieldedAccessControl.overrideWitness('wit_getRoleIndex', RETURN_BAD_INDEX); - const [, witnessCalculatedIndex] = shieldedAccessControl.witnesses.wit_getRoleIndex(shieldedAccessControl.getWitnessContext(), DEFAULT_ADMIN_ROLE); - expect(witnessCalculatedIndex).not.toBe(INIT_COUNTER); - } + if (isValidIndex) { + // Check index matches + const [, witnessCalculatedIndex] = + shieldedAccessControl.witnesses.wit_getRoleIndex( + shieldedAccessControl.getWitnessContext(), + DEFAULT_ADMIN_ROLE, + ); + expect(witnessCalculatedIndex).toBe(INIT_COUNTER); + } else { + // Check index does not match + shieldedAccessControl.overrideWitness( + 'wit_getRoleIndex', + RETURN_BAD_INDEX, + ); + const [, witnessCalculatedIndex] = + shieldedAccessControl.witnesses.wit_getRoleIndex( + shieldedAccessControl.getWitnessContext(), + DEFAULT_ADMIN_ROLE, + ); + expect(witnessCalculatedIndex).not.toBe(INIT_COUNTER); + } - if (isValidPath) { - // Check path matches - const truePath = shieldedAccessControl.getPublicState().ShieldedAccessControl__operatorRoles.findPathForLeaf(EXP_DEFAULT_ADMIN_COMMITMENT); - const [, witnessCalculatedPath] = shieldedAccessControl.witnesses.wit_getRoleCommitmentPath(shieldedAccessControl.getWitnessContext(), EXP_DEFAULT_ADMIN_COMMITMENT); - expect(witnessCalculatedPath).toEqual(truePath); - } else { - // Check path does not match - const truePath = shieldedAccessControl.getPublicState().ShieldedAccessControl__operatorRoles.findPathForLeaf(EXP_DEFAULT_ADMIN_COMMITMENT); - shieldedAccessControl.overrideWitness('wit_getRoleCommitmentPath', RETURN_BAD_PATH); - const [, witnessCalculatedPath] = shieldedAccessControl.witnesses.wit_getRoleIndex(shieldedAccessControl.getWitnessContext(), DEFAULT_ADMIN_ROLE); - expect(witnessCalculatedPath).not.toEqual(truePath); - } + if (isValidPath) { + // Check path matches + const truePath = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf( + EXP_DEFAULT_ADMIN_COMMITMENT, + ); + const [, witnessCalculatedPath] = + shieldedAccessControl.witnesses.wit_getRoleCommitmentPath( + shieldedAccessControl.getWitnessContext(), + EXP_DEFAULT_ADMIN_COMMITMENT, + ); + expect(witnessCalculatedPath).toEqual(truePath); + } else { + // Check path does not match + const truePath = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf( + EXP_DEFAULT_ADMIN_COMMITMENT, + ); + shieldedAccessControl.overrideWitness( + 'wit_getRoleCommitmentPath', + RETURN_BAD_PATH, + ); + const [, witnessCalculatedPath] = + shieldedAccessControl.witnesses.wit_getRoleIndex( + shieldedAccessControl.getWitnessContext(), + DEFAULT_ADMIN_ROLE, + ); + expect(witnessCalculatedPath).not.toEqual(truePath); + } - // Test protected circuit - expect(() => { - (shieldedAccessControl[circuitName] as (...args: unknown[]) => unknown)(...args); - }).toThrow('ShieldedAccessControl: unauthorized account'); - }); + // Test protected circuit + expect(() => { + ( + shieldedAccessControl[circuitName] as ( + ...args: unknown[] + ) => unknown + )(...args); + }).toThrow('ShieldedAccessControl: unauthorized account'); + }, + ); }); describe('unsupported contract address failure cases', () => { @@ -273,7 +409,10 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl.callerCtx.setCaller(ADMIN); }); - type FailingCircuits = [method: keyof ShieldedAccessControlSimulator, args: unknown[]]; + type FailingCircuits = [ + method: keyof ShieldedAccessControlSimulator, + args: unknown[], + ]; const circuitsWithContractAddressCheck: FailingCircuits[] = [ ['hasRole', [DEFAULT_ADMIN_ROLE, Z_OPERATOR_CONTRACT]], ['_checkRole', [DEFAULT_ADMIN_ROLE, Z_OPERATOR_CONTRACT]], @@ -283,12 +422,21 @@ describe('ShieldedAccessControl', () => { ['_revokeRole', [DEFAULT_ADMIN_ROLE, Z_OPERATOR_CONTRACT]], ]; - it.each(circuitsWithContractAddressCheck)('%s fails if contract address is queried', (circuitName, args) => { - // Test protected circuit - expect(() => { - (shieldedAccessControl[circuitName] as (...args: unknown[]) => unknown)(...args); - }).toThrow('ShieldedAccessControl: contract address roles are not yet supported'); - }); + it.each(circuitsWithContractAddressCheck)( + '%s fails if contract address is queried', + (circuitName, args) => { + // Test protected circuit + expect(() => { + ( + shieldedAccessControl[circuitName] as ( + ...args: unknown[] + ) => unknown + )(...args); + }).toThrow( + 'ShieldedAccessControl: contract address roles are not yet supported', + ); + }, + ); }); describe('hasRole', () => { @@ -296,7 +444,12 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl._grantRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); }); - type HasRoleTest = [isValidNonce: boolean, isValidIndex: boolean, isValidPath: boolean, args: unknown[]]; + type HasRoleTest = [ + isValidNonce: boolean, + isValidIndex: boolean, + isValidPath: boolean, + args: unknown[], + ]; const falseCases: HasRoleTest[] = [ [false, true, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], [true, false, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], @@ -319,15 +472,17 @@ describe('ShieldedAccessControl', () => { it('should throw if caller is contract address', () => { shieldedAccessControl.callerCtx.setCaller(OPERATOR_CONTRACT); expect(() => { - shieldedAccessControl.hasRole(UNINITIALIZED_ROLE, Z_OPERATOR_CONTRACT) - }).toThrow('ShieldedAccessControl: contract address roles are not yet supported'); + shieldedAccessControl.hasRole(UNINITIALIZED_ROLE, Z_OPERATOR_CONTRACT); + }).toThrow( + 'ShieldedAccessControl: contract address roles are not yet supported', + ); }); it('should throw if role has been revoked', () => { shieldedAccessControl._revokeRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); expect(() => { shieldedAccessControl.hasRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); - }).toThrow("ShieldedAccessControl: role access has been revoked"); + }).toThrow('ShieldedAccessControl: role access has been revoked'); }); it('should return correct role commitment', () => { @@ -348,101 +503,194 @@ describe('ShieldedAccessControl', () => { }); it('should return false when unauthorized does not have role', () => { - const role = shieldedAccessControl.hasRole(DEFAULT_ADMIN_ROLE, Z_UNAUTHORIZED); + const role = shieldedAccessControl.hasRole( + DEFAULT_ADMIN_ROLE, + Z_UNAUTHORIZED, + ); expect(role.isApproved).toEqual(false); }); it('should return false when role does not exist', () => { - shieldedAccessControl.privateState.injectSecretNonce(UNINITIALIZED_ROLE, Buffer.alloc(32)); - const role = shieldedAccessControl.hasRole(UNINITIALIZED_ROLE, Z_UNAUTHORIZED); + shieldedAccessControl.privateState.injectSecretNonce( + UNINITIALIZED_ROLE, + Buffer.alloc(32), + ); + const role = shieldedAccessControl.hasRole( + UNINITIALIZED_ROLE, + Z_UNAUTHORIZED, + ); expect(role.isApproved).toBe(false); }); - it.each(falseCases)('should return false with any invalid witness value - isValidNonce(%s), isValidIndex(%s), isValidPath(%s)', (isValidNonce, isValidIndex, isValidPath, args) => { - if (isValidNonce) { - // Check nonce matches - expect(shieldedAccessControl.privateState.getCurrentSecretNonce(DEFAULT_ADMIN_ROLE)).toEqual( - ADMIN_SECRET_NONCE, - ); - } else { - // Check nonce does not match - shieldedAccessControl.privateState.injectSecretNonce(DEFAULT_ADMIN_ROLE, BAD_NONCE); - expect(shieldedAccessControl.privateState.getCurrentSecretNonce(DEFAULT_ADMIN_ROLE)).not.toEqual( - ADMIN_SECRET_NONCE, - ); - } - - if (isValidIndex) { - // Check index matches - const [, witnessCalculatedIndex] = shieldedAccessControl.witnesses.wit_getRoleIndex(shieldedAccessControl.getWitnessContext(), DEFAULT_ADMIN_ROLE); - expect(witnessCalculatedIndex).toBe(INIT_COUNTER); - } else { - // Check index does not match - shieldedAccessControl.overrideWitness('wit_getRoleIndex', RETURN_BAD_INDEX); - const [, witnessCalculatedIndex] = shieldedAccessControl.witnesses.wit_getRoleIndex(shieldedAccessControl.getWitnessContext(), DEFAULT_ADMIN_ROLE); - expect(witnessCalculatedIndex).not.toBe(INIT_COUNTER); - } + it.each(falseCases)( + 'should return false with any invalid witness value - isValidNonce(%s), isValidIndex(%s), isValidPath(%s)', + (isValidNonce, isValidIndex, isValidPath, args) => { + if (isValidNonce) { + // Check nonce matches + expect( + shieldedAccessControl.privateState.getCurrentSecretNonce( + DEFAULT_ADMIN_ROLE, + ), + ).toEqual(ADMIN_SECRET_NONCE); + } else { + // Check nonce does not match + shieldedAccessControl.privateState.injectSecretNonce( + DEFAULT_ADMIN_ROLE, + BAD_NONCE, + ); + expect( + shieldedAccessControl.privateState.getCurrentSecretNonce( + DEFAULT_ADMIN_ROLE, + ), + ).not.toEqual(ADMIN_SECRET_NONCE); + } - if (isValidPath) { - // Check path matches - const truePath = shieldedAccessControl.getPublicState().ShieldedAccessControl__operatorRoles.findPathForLeaf(EXP_DEFAULT_ADMIN_COMMITMENT); - const [, witnessCalculatedPath] = shieldedAccessControl.witnesses.wit_getRoleCommitmentPath(shieldedAccessControl.getWitnessContext(), EXP_DEFAULT_ADMIN_COMMITMENT); - expect(witnessCalculatedPath).toEqual(truePath); - } else { - // Check path does not match - const truePath = shieldedAccessControl.getPublicState().ShieldedAccessControl__operatorRoles.findPathForLeaf(EXP_DEFAULT_ADMIN_COMMITMENT); - shieldedAccessControl.overrideWitness('wit_getRoleCommitmentPath', RETURN_BAD_PATH); - const [, witnessCalculatedPath] = shieldedAccessControl.witnesses.wit_getRoleIndex(shieldedAccessControl.getWitnessContext(), DEFAULT_ADMIN_ROLE); - expect(witnessCalculatedPath).not.toEqual(truePath); - } + if (isValidIndex) { + // Check index matches + const [, witnessCalculatedIndex] = + shieldedAccessControl.witnesses.wit_getRoleIndex( + shieldedAccessControl.getWitnessContext(), + DEFAULT_ADMIN_ROLE, + ); + expect(witnessCalculatedIndex).toBe(INIT_COUNTER); + } else { + // Check index does not match + shieldedAccessControl.overrideWitness( + 'wit_getRoleIndex', + RETURN_BAD_INDEX, + ); + const [, witnessCalculatedIndex] = + shieldedAccessControl.witnesses.wit_getRoleIndex( + shieldedAccessControl.getWitnessContext(), + DEFAULT_ADMIN_ROLE, + ); + expect(witnessCalculatedIndex).not.toBe(INIT_COUNTER); + } - // Test false case circuit - const role = (shieldedAccessControl['hasRole'] as (...args: unknown[]) => Role)(...args); - expect(role.isApproved).toBe(false); - }); + if (isValidPath) { + // Check path matches + const truePath = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf( + EXP_DEFAULT_ADMIN_COMMITMENT, + ); + const [, witnessCalculatedPath] = + shieldedAccessControl.witnesses.wit_getRoleCommitmentPath( + shieldedAccessControl.getWitnessContext(), + EXP_DEFAULT_ADMIN_COMMITMENT, + ); + expect(witnessCalculatedPath).toEqual(truePath); + } else { + // Check path does not match + const truePath = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf( + EXP_DEFAULT_ADMIN_COMMITMENT, + ); + shieldedAccessControl.overrideWitness( + 'wit_getRoleCommitmentPath', + RETURN_BAD_PATH, + ); + const [, witnessCalculatedPath] = + shieldedAccessControl.witnesses.wit_getRoleIndex( + shieldedAccessControl.getWitnessContext(), + DEFAULT_ADMIN_ROLE, + ); + expect(witnessCalculatedPath).not.toEqual(truePath); + } - it.each(commitmentDoesNotMatchCases)('commitment should not match with invalid nonce or index - isValidNonce(%s), isValidIndex(%s), isValidPath(%s)', (isValidNonce, isValidIndex, isValidPath, args) => { - if (isValidNonce) { - // Check nonce matches - expect(shieldedAccessControl.privateState.getCurrentSecretNonce(DEFAULT_ADMIN_ROLE)).toEqual( - ADMIN_SECRET_NONCE, - ); - } else { - // Check nonce does not match - shieldedAccessControl.privateState.injectSecretNonce(DEFAULT_ADMIN_ROLE, BAD_NONCE); - expect(shieldedAccessControl.privateState.getCurrentSecretNonce(DEFAULT_ADMIN_ROLE)).not.toEqual( - ADMIN_SECRET_NONCE, - ); - } + // Test false case circuit + const role = ( + shieldedAccessControl['hasRole'] as (...args: unknown[]) => Role + )(...args); + expect(role.isApproved).toBe(false); + }, + ); + + it.each(commitmentDoesNotMatchCases)( + 'commitment should not match with invalid nonce or index - isValidNonce(%s), isValidIndex(%s), isValidPath(%s)', + (isValidNonce, isValidIndex, isValidPath, args) => { + if (isValidNonce) { + // Check nonce matches + expect( + shieldedAccessControl.privateState.getCurrentSecretNonce( + DEFAULT_ADMIN_ROLE, + ), + ).toEqual(ADMIN_SECRET_NONCE); + } else { + // Check nonce does not match + shieldedAccessControl.privateState.injectSecretNonce( + DEFAULT_ADMIN_ROLE, + BAD_NONCE, + ); + expect( + shieldedAccessControl.privateState.getCurrentSecretNonce( + DEFAULT_ADMIN_ROLE, + ), + ).not.toEqual(ADMIN_SECRET_NONCE); + } - if (isValidIndex) { - // Check index matches - const [, witnessCalculatedIndex] = shieldedAccessControl.witnesses.wit_getRoleIndex(shieldedAccessControl.getWitnessContext(), DEFAULT_ADMIN_ROLE); - expect(witnessCalculatedIndex).toBe(INIT_COUNTER); - } else { - // Check index does not match - shieldedAccessControl.overrideWitness('wit_getRoleIndex', RETURN_BAD_INDEX); - const [, witnessCalculatedIndex] = shieldedAccessControl.witnesses.wit_getRoleIndex(shieldedAccessControl.getWitnessContext(), DEFAULT_ADMIN_ROLE); - expect(witnessCalculatedIndex).not.toBe(INIT_COUNTER); - } + if (isValidIndex) { + // Check index matches + const [, witnessCalculatedIndex] = + shieldedAccessControl.witnesses.wit_getRoleIndex( + shieldedAccessControl.getWitnessContext(), + DEFAULT_ADMIN_ROLE, + ); + expect(witnessCalculatedIndex).toBe(INIT_COUNTER); + } else { + // Check index does not match + shieldedAccessControl.overrideWitness( + 'wit_getRoleIndex', + RETURN_BAD_INDEX, + ); + const [, witnessCalculatedIndex] = + shieldedAccessControl.witnesses.wit_getRoleIndex( + shieldedAccessControl.getWitnessContext(), + DEFAULT_ADMIN_ROLE, + ); + expect(witnessCalculatedIndex).not.toBe(INIT_COUNTER); + } - if (isValidPath) { - // Check path matches - const truePath = shieldedAccessControl.getPublicState().ShieldedAccessControl__operatorRoles.findPathForLeaf(EXP_DEFAULT_ADMIN_COMMITMENT); - const [, witnessCalculatedPath] = shieldedAccessControl.witnesses.wit_getRoleCommitmentPath(shieldedAccessControl.getWitnessContext(), EXP_DEFAULT_ADMIN_COMMITMENT); - expect(witnessCalculatedPath).toEqual(truePath); - } else { - // Check path does not match - const truePath = shieldedAccessControl.getPublicState().ShieldedAccessControl__operatorRoles.findPathForLeaf(EXP_DEFAULT_ADMIN_COMMITMENT); - shieldedAccessControl.overrideWitness('wit_getRoleCommitmentPath', RETURN_BAD_PATH); - const [, witnessCalculatedPath] = shieldedAccessControl.witnesses.wit_getRoleIndex(shieldedAccessControl.getWitnessContext(), DEFAULT_ADMIN_ROLE); - expect(witnessCalculatedPath).not.toEqual(truePath); - } + if (isValidPath) { + // Check path matches + const truePath = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf( + EXP_DEFAULT_ADMIN_COMMITMENT, + ); + const [, witnessCalculatedPath] = + shieldedAccessControl.witnesses.wit_getRoleCommitmentPath( + shieldedAccessControl.getWitnessContext(), + EXP_DEFAULT_ADMIN_COMMITMENT, + ); + expect(witnessCalculatedPath).toEqual(truePath); + } else { + // Check path does not match + const truePath = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf( + EXP_DEFAULT_ADMIN_COMMITMENT, + ); + shieldedAccessControl.overrideWitness( + 'wit_getRoleCommitmentPath', + RETURN_BAD_PATH, + ); + const [, witnessCalculatedPath] = + shieldedAccessControl.witnesses.wit_getRoleIndex( + shieldedAccessControl.getWitnessContext(), + DEFAULT_ADMIN_ROLE, + ); + expect(witnessCalculatedPath).not.toEqual(truePath); + } - // Test false case circuit - const role = (shieldedAccessControl['hasRole'] as (...args: unknown[]) => Role)(...args); - expect(role.roleCommitment).not.toEqual(EXP_DEFAULT_ADMIN_COMMITMENT); - }); + // Test false case circuit + const role = ( + shieldedAccessControl['hasRole'] as (...args: unknown[]) => Role + )(...args); + expect(role.roleCommitment).not.toEqual(EXP_DEFAULT_ADMIN_COMMITMENT); + }, + ); }); describe('assertOnlyRole', () => { @@ -453,15 +701,31 @@ describe('ShieldedAccessControl', () => { it('should not fail when authorized caller has correct nonce, index, and path', () => { // Check nonce is correct - expect(shieldedAccessControl.privateState.getCurrentSecretNonce(DEFAULT_ADMIN_ROLE)).toBe(ADMIN_SECRET_NONCE); + expect( + shieldedAccessControl.privateState.getCurrentSecretNonce( + DEFAULT_ADMIN_ROLE, + ), + ).toBe(ADMIN_SECRET_NONCE); // Check index matches - const [, witnessCalculatedIndex] = shieldedAccessControl.witnesses.wit_getRoleIndex(shieldedAccessControl.getWitnessContext(), DEFAULT_ADMIN_ROLE); + const [, witnessCalculatedIndex] = + shieldedAccessControl.witnesses.wit_getRoleIndex( + shieldedAccessControl.getWitnessContext(), + DEFAULT_ADMIN_ROLE, + ); expect(witnessCalculatedIndex).toBe(INIT_COUNTER); // Check path matches - const truePath = shieldedAccessControl.getPublicState().ShieldedAccessControl__operatorRoles.findPathForLeaf(EXP_DEFAULT_ADMIN_COMMITMENT); - const [, witnessCalculatedPath] = shieldedAccessControl.witnesses.wit_getRoleCommitmentPath(shieldedAccessControl.getWitnessContext(), EXP_DEFAULT_ADMIN_COMMITMENT); + const truePath = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf( + EXP_DEFAULT_ADMIN_COMMITMENT, + ); + const [, witnessCalculatedPath] = + shieldedAccessControl.witnesses.wit_getRoleCommitmentPath( + shieldedAccessControl.getWitnessContext(), + EXP_DEFAULT_ADMIN_COMMITMENT, + ); expect(witnessCalculatedPath).toEqual(truePath); expect(() => @@ -470,9 +734,18 @@ describe('ShieldedAccessControl', () => { }); it('should not fail for admin with multiple roles', () => { - shieldedAccessControl.privateState.injectSecretNonce(OPERATOR_ROLE_1, OPERATOR_ROLE_1_SECRET_NONCE); - shieldedAccessControl.privateState.injectSecretNonce(OPERATOR_ROLE_2, OPERATOR_ROLE_2_SECRET_NONCE); - shieldedAccessControl.privateState.injectSecretNonce(OPERATOR_ROLE_3, OPERATOR_ROLE_3_SECRET_NONCE); + shieldedAccessControl.privateState.injectSecretNonce( + OPERATOR_ROLE_1, + OPERATOR_ROLE_1_SECRET_NONCE, + ); + shieldedAccessControl.privateState.injectSecretNonce( + OPERATOR_ROLE_2, + OPERATOR_ROLE_2_SECRET_NONCE, + ); + shieldedAccessControl.privateState.injectSecretNonce( + OPERATOR_ROLE_3, + OPERATOR_ROLE_3_SECRET_NONCE, + ); shieldedAccessControl._grantRole(OPERATOR_ROLE_1, Z_ADMIN); shieldedAccessControl._grantRole(OPERATOR_ROLE_2, Z_ADMIN); shieldedAccessControl._grantRole(OPERATOR_ROLE_3, Z_ADMIN); @@ -526,57 +799,100 @@ describe('ShieldedAccessControl', () => { }); it('admin should grant role', () => { - shieldedAccessControl.privateState.injectSecretNonce(OPERATOR_ROLE_1, OPERATOR_ROLE_1_SECRET_NONCE); + shieldedAccessControl.privateState.injectSecretNonce( + OPERATOR_ROLE_1, + OPERATOR_ROLE_1_SECRET_NONCE, + ); shieldedAccessControl.grantRole(OPERATOR_ROLE_1, Z_OPERATOR_1); - const role: Role = shieldedAccessControl.hasRole(OPERATOR_ROLE_1, Z_OPERATOR_1); - expect(role.isApproved).toBe( - true, + const role: Role = shieldedAccessControl.hasRole( + OPERATOR_ROLE_1, + Z_OPERATOR_1, ); + expect(role.isApproved).toBe(true); }); it('path for role should exist in Merkle tree', () => { - expect(shieldedAccessControl.getPublicState().ShieldedAccessControl__operatorRoles.findPathForLeaf(EXP_DEFAULT_ADMIN_COMMITMENT)).toBeDefined(); + expect( + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf( + EXP_DEFAULT_ADMIN_COMMITMENT, + ), + ).toBeDefined(); }); it('should update Merkle tree root', () => { - expect(shieldedAccessControl.getPublicState().ShieldedAccessControl__operatorRoles.root().field).toBeGreaterThan(0n); + expect( + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.root().field, + ).toBeGreaterThan(0n); }); it('_currentMerkleTreeIndex should increment', () => { // Starts at 1 because we grant role to self in beforeEach - expect(shieldedAccessControl.getPublicState().ShieldedAccessControl__currentMerkleTreeIndex).toBe(1n); - - shieldedAccessControl.privateState.injectSecretNonce(OPERATOR_ROLE_1, OPERATOR_ROLE_1_SECRET_NONCE); - shieldedAccessControl.privateState.injectSecretNonce(OPERATOR_ROLE_2, OPERATOR_ROLE_2_SECRET_NONCE); - shieldedAccessControl.privateState.injectSecretNonce(OPERATOR_ROLE_3, OPERATOR_ROLE_3_SECRET_NONCE); + expect( + shieldedAccessControl.getPublicState() + .ShieldedAccessControl__currentMerkleTreeIndex, + ).toBe(1n); + + shieldedAccessControl.privateState.injectSecretNonce( + OPERATOR_ROLE_1, + OPERATOR_ROLE_1_SECRET_NONCE, + ); + shieldedAccessControl.privateState.injectSecretNonce( + OPERATOR_ROLE_2, + OPERATOR_ROLE_2_SECRET_NONCE, + ); + shieldedAccessControl.privateState.injectSecretNonce( + OPERATOR_ROLE_3, + OPERATOR_ROLE_3_SECRET_NONCE, + ); shieldedAccessControl.grantRole(OPERATOR_ROLE_1, Z_OPERATOR_1); - expect(shieldedAccessControl.getPublicState().ShieldedAccessControl__currentMerkleTreeIndex).toBe(2n); + expect( + shieldedAccessControl.getPublicState() + .ShieldedAccessControl__currentMerkleTreeIndex, + ).toBe(2n); shieldedAccessControl.grantRole(OPERATOR_ROLE_2, Z_OPERATOR_2); - expect(shieldedAccessControl.getPublicState().ShieldedAccessControl__currentMerkleTreeIndex).toBe(3n); + expect( + shieldedAccessControl.getPublicState() + .ShieldedAccessControl__currentMerkleTreeIndex, + ).toBe(3n); shieldedAccessControl.grantRole(OPERATOR_ROLE_3, Z_OPERATOR_3); - expect(shieldedAccessControl.getPublicState().ShieldedAccessControl__currentMerkleTreeIndex).toBe(4n); + expect( + shieldedAccessControl.getPublicState() + .ShieldedAccessControl__currentMerkleTreeIndex, + ).toBe(4n); }); - - it('admin should grant multiple roles', () => { for (let i = 0; i < OPERATOR_ROLE_LIST.length; i++) { - shieldedAccessControl.privateState.injectSecretNonce(OPERATOR_ROLE_LIST[i], OPERATOR_ROLE_SECRET_NONCES[i]); + shieldedAccessControl.privateState.injectSecretNonce( + OPERATOR_ROLE_LIST[i], + OPERATOR_ROLE_SECRET_NONCES[i], + ); for (let j = 0; j < Z_OPERATOR_LIST.length; j++) { - shieldedAccessControl.grantRole(OPERATOR_ROLE_LIST[i], Z_OPERATOR_LIST[j]); - const role: Role = shieldedAccessControl.hasRole(OPERATOR_ROLE_LIST[i], Z_OPERATOR_LIST[j]) - expect(role.isApproved).toBe( - true, + shieldedAccessControl.grantRole( + OPERATOR_ROLE_LIST[i], + Z_OPERATOR_LIST[j], ); + const role: Role = shieldedAccessControl.hasRole( + OPERATOR_ROLE_LIST[i], + Z_OPERATOR_LIST[j], + ); + expect(role.isApproved).toBe(true); } } }); it('should throw if non-admin operator grants role', () => { - shieldedAccessControl.privateState.injectSecretNonce(OPERATOR_ROLE_1, OPERATOR_ROLE_1_SECRET_NONCE); + shieldedAccessControl.privateState.injectSecretNonce( + OPERATOR_ROLE_1, + OPERATOR_ROLE_1_SECRET_NONCE, + ); shieldedAccessControl._grantRole(OPERATOR_ROLE_1, Z_OPERATOR_1); shieldedAccessControl.callerCtx.setCaller(OPERATOR_1); @@ -589,12 +905,13 @@ describe('ShieldedAccessControl', () => { describe('revokeRole', () => { beforeEach(() => { shieldedAccessControl._grantRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); - shieldedAccessControl.privateState.injectSecretNonce(OPERATOR_ROLE_1, OPERATOR_ROLE_1_SECRET_NONCE); + shieldedAccessControl.privateState.injectSecretNonce( + OPERATOR_ROLE_1, + OPERATOR_ROLE_1_SECRET_NONCE, + ); shieldedAccessControl.callerCtx.setCaller(ADMIN); }); - it.todo('admin should revoke role', () => { - - }); + it.todo('admin should revoke role', () => {}); }); }); diff --git a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts index 707f0bc7..69e25938 100644 --- a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts +++ b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts @@ -2,7 +2,7 @@ import { type CircuitContext, type CoinPublicKey, emptyZswapLocalState, - WitnessContext, + type WitnessContext, } from '@midnight-ntwrk/compact-runtime'; import { sampleContractAddress } from '@midnight-ntwrk/zswap'; import { @@ -11,8 +11,8 @@ import { type Ledger, ledger, Contract as MockShieldedAccessControl, + type ShieldedAccessControl_Role as Role, type ZswapCoinPublicKey, - type ShieldedAccessControl_Role as Role } from '../../../../artifacts/MockShieldedAccessControl/contract/index.cjs'; import { ShieldedAccessControlPrivateState, @@ -48,12 +48,16 @@ export class ShieldedAccessControlSimulator extends AbstractContractSimulator< private _witnesses: ReturnType; private _pureCircuitProxy?: ContextlessCircuits< - ExtractPureCircuits>, + ExtractPureCircuits< + MockShieldedAccessControl + >, ShieldedAccessControlPrivateState >; private _impureCircuitProxy?: ContextlessCircuits< - ExtractImpureCircuits>, + ExtractImpureCircuits< + MockShieldedAccessControl + >, ShieldedAccessControlPrivateState >; @@ -65,13 +69,18 @@ export class ShieldedAccessControlSimulator extends AbstractContractSimulator< // Setup initial state const { - privateState = options.privateState ? options.privateState : ShieldedAccessControlPrivateState.generate(initUser), + privateState = options.privateState + ? options.privateState + : ShieldedAccessControlPrivateState.generate(initUser), witnesses = ShieldedAccessControlWitnesses(), coinPK = '0'.repeat(64), address = sampleContractAddress(), } = options; - this.contract = new MockShieldedAccessControl(witnesses); + this.contract = + new MockShieldedAccessControl( + witnesses, + ); this.stateManager = new SimulatorStateManager( this.contract, @@ -81,7 +90,10 @@ export class ShieldedAccessControlSimulator extends AbstractContractSimulator< ); this.contractAddress = this.circuitContext.transactionContext.address; this._witnesses = witnesses; - this.contract = new MockShieldedAccessControl(this._witnesses); + this.contract = + new MockShieldedAccessControl( + this._witnesses, + ); } get circuitContext() { @@ -96,12 +108,15 @@ export class ShieldedAccessControlSimulator extends AbstractContractSimulator< return ledger(this.circuitContext.transactionContext.state); } - getWitnessContext(): WitnessContext { + getWitnessContext(): WitnessContext< + Ledger, + ShieldedAccessControlPrivateState + > { return { ledger: this.getPublicState(), privateState: this.getPrivateState(), - contractAddress: this.contractAddress - } + contractAddress: this.contractAddress, + }; } /** @@ -129,7 +144,9 @@ export class ShieldedAccessControlSimulator extends AbstractContractSimulator< * @returns A proxy object exposing pure circuit functions without requiring explicit context. */ protected get pureCircuit(): ContextlessCircuits< - ExtractPureCircuits>, + ExtractPureCircuits< + MockShieldedAccessControl + >, ShieldedAccessControlPrivateState > { if (!this._pureCircuitProxy) { @@ -150,7 +167,9 @@ export class ShieldedAccessControlSimulator extends AbstractContractSimulator< * @returns A proxy object exposing impure circuit functions without requiring explicit context management. */ protected get impureCircuit(): ContextlessCircuits< - ExtractImpureCircuits>, + ExtractImpureCircuits< + MockShieldedAccessControl + >, ShieldedAccessControlPrivateState > { if (!this._impureCircuitProxy) { @@ -193,10 +212,15 @@ export class ShieldedAccessControlSimulator extends AbstractContractSimulator< return this._witnesses; } - public set witnesses(newWitnesses: ReturnType) { + public set witnesses(newWitnesses: ReturnType< + typeof ShieldedAccessControlWitnesses + >) { this.resetCircuitProxies(); this._witnesses = newWitnesses; - this.contract = new MockShieldedAccessControl(this._witnesses); + this.contract = + new MockShieldedAccessControl( + this._witnesses, + ); } public overrideWitness( @@ -214,7 +238,10 @@ export class ShieldedAccessControlSimulator extends AbstractContractSimulator< * The full commitment is: `SHA256(SHA256(pk, nonce), instanceSalt, counter, domain)`. * @returns The current owner's commitment. */ - public hasRole(roleId: Uint8Array, account: Either): Role { + public hasRole( + roleId: Uint8Array, + account: Either, + ): Role { return this.circuits.impure.hasRole(roleId, account); } @@ -232,7 +259,10 @@ export class ShieldedAccessControlSimulator extends AbstractContractSimulator< * It will not be possible to call `assertOnlyOnwer` circuits anymore. * Can only be called by the current owner. */ - public _checkRole(roleId: Uint8Array, account: Either) { + public _checkRole( + roleId: Uint8Array, + account: Either, + ) { this.circuits.impure._checkRole(roleId, account); } @@ -254,7 +284,10 @@ export class ShieldedAccessControlSimulator extends AbstractContractSimulator< * @param nonce - A private nonce to scope the commitment. * @returns The computed owner ID. */ - public grantRole(roleId: Uint8Array, account: Either) { + public grantRole( + roleId: Uint8Array, + account: Either, + ) { this.circuits.impure.grantRole(roleId, account); } @@ -263,7 +296,10 @@ export class ShieldedAccessControlSimulator extends AbstractContractSimulator< * enforcing permission checks on the caller. * @param newOwnerId - The unique identifier of the new owner calculated by `SHA256(pk, nonce)`. */ - public revokeRole(roleId: Uint8Array, account: Either) { + public revokeRole( + roleId: Uint8Array, + account: Either, + ) { this.circuits.impure.revokeRole(roleId, account); } @@ -272,7 +308,10 @@ export class ShieldedAccessControlSimulator extends AbstractContractSimulator< * enforcing permission checks on the caller. * @param newOwnerId - The unique identifier of the new owner calculated by `SHA256(pk, nonce)`. */ - public renounceRole(roleId: Uint8Array, callerConfirmation: Either) { + public renounceRole( + roleId: Uint8Array, + callerConfirmation: Either, + ) { this.circuits.impure.renounceRole(roleId, callerConfirmation); } @@ -290,7 +329,10 @@ export class ShieldedAccessControlSimulator extends AbstractContractSimulator< * enforcing permission checks on the caller. * @param newOwnerId - The unique identifier of the new owner calculated by `SHA256(pk, nonce)`. */ - public _grantRole(roleId: Uint8Array, account: Either): Boolean { + public _grantRole( + roleId: Uint8Array, + account: Either, + ): boolean { return this.circuits.impure._grantRole(roleId, account); } @@ -299,7 +341,10 @@ export class ShieldedAccessControlSimulator extends AbstractContractSimulator< * enforcing permission checks on the caller. * @param newOwnerId - The unique identifier of the new owner calculated by `SHA256(pk, nonce)`. */ - public _revokeRole(roleId: Uint8Array, account: Either): Boolean { + public _revokeRole( + roleId: Uint8Array, + account: Either, + ): boolean { return this.circuits.impure._revokeRole(roleId, account); } @@ -314,7 +359,10 @@ export class ShieldedAccessControlSimulator extends AbstractContractSimulator< newNonce: Buffer, ): ShieldedAccessControlPrivateState => { const currentState = this.stateManager.getContext().currentPrivateState; - const updatedState = { ...currentState, roles: { ...currentState.roles } } + const updatedState = { + ...currentState, + roles: { ...currentState.roles }, + }; const roleString = Buffer.from(roleId).toString('hex'); updatedState.roles[roleString] = newNonce; this.stateManager.updatePrivateState(updatedState); @@ -327,7 +375,9 @@ export class ShieldedAccessControlSimulator extends AbstractContractSimulator< */ getCurrentSecretNonce: (roleId: Uint8Array): Uint8Array => { const roleString = Buffer.from(roleId).toString('hex'); - return this.stateManager.getContext().currentPrivateState.roles[roleString]; + return this.stateManager.getContext().currentPrivateState.roles[ + roleString + ]; }, }; diff --git a/contracts/src/access/test/utils/address.ts b/contracts/src/access/test/utils/address.ts index 0cd6f6a2..b5f184f4 100644 --- a/contracts/src/access/test/utils/address.ts +++ b/contracts/src/access/test/utils/address.ts @@ -64,27 +64,31 @@ export const createEitherTestContractAddress = (str: string) => ({ const baseGeneratePubKeyPair = ( str: string, asEither: boolean, - asPK: boolean + asPK: boolean, ): [ - string, - ( - | Compact.ZswapCoinPublicKey - | Compact.Either - ), - ] => { + string, + ( + | Compact.ZswapCoinPublicKey + | Compact.Either + ), +] => { const pk = toHexPadded(str); if (asEither && asPK) { - return [pk, createEitherTestUser(str)] - } else if (asEither && !asPK) { - return [pk, createEitherTestContractAddress(str)] + return [pk, createEitherTestUser(str)]; + } + if (asEither && !asPK) { + return [pk, createEitherTestContractAddress(str)]; } return [pk, encodeToPK(str)]; }; export const generatePubKeyPair = (str: string) => - baseGeneratePubKeyPair(str, false, false) as [string, Compact.ZswapCoinPublicKey]; + baseGeneratePubKeyPair(str, false, false) as [ + string, + Compact.ZswapCoinPublicKey, + ]; export const generateEitherPubKeyPair = (str: string, asPK = true) => baseGeneratePubKeyPair(str, true, asPK) as [ @@ -107,10 +111,12 @@ export const ZERO_ADDRESS = { right: { bytes: zeroUint8Array() }, }; -export const eitherToBytes = (account: Compact.Either) => { +export const eitherToBytes = ( + account: Compact.Either, +) => { if (account.is_left) { return account.left.bytes; } return account.right.bytes; -} \ No newline at end of file +}; diff --git a/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts b/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts index 7de12adb..f36a1303 100644 --- a/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts +++ b/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts @@ -1,18 +1,29 @@ import { getRandomValues } from 'node:crypto'; -import { CompactTypeVector, CompactTypeBytes, persistentHash, type WitnessContext, convert_bigint_to_Uint8Array } from '@midnight-ntwrk/compact-runtime'; -import type { Ledger, MerkleTreePath, Either, ZswapCoinPublicKey, ContractAddress } from '../../../artifacts/MockShieldedAccessControl/contract/index.cjs'; +import { + CompactTypeBytes, + CompactTypeVector, + convert_bigint_to_Uint8Array, + persistentHash, + type WitnessContext, +} from '@midnight-ntwrk/compact-runtime'; +import type { + ContractAddress, + Either, + Ledger, + MerkleTreePath, + ZswapCoinPublicKey, +} from '../../../artifacts/MockShieldedAccessControl/contract/index.cjs'; import { eitherToBytes } from '../test/utils/address'; const MERKLE_TREE_DEPTH = 2 ** 10; -const DOMAIN = new TextEncoder().encode("ShieldedAccessControl:shield:"); +const DOMAIN = new TextEncoder().encode('ShieldedAccessControl:shield:'); -export function fmtHexString(bytes: String | Uint8Array): string { +export function fmtHexString(bytes: string | Uint8Array): string { if (bytes instanceof String) { - return `${bytes.slice(0, 4)}...${bytes.slice(-4)}` - } else { - const buffStr = Buffer.from(bytes).toString('hex'); - return `${buffStr.slice(0, 4)}...${buffStr.slice(-4)}`; + return `${bytes.slice(0, 4)}...${bytes.slice(-4)}`; } + const buffStr = Buffer.from(bytes).toString('hex'); + return `${buffStr.slice(0, 4)}...${buffStr.slice(-4)}`; } /** @@ -25,9 +36,18 @@ export interface IShieldedAccessControlWitnesses

{ * @param context - The witness context containing the private state. * @returns A tuple of the private state and the secret nonce as a Uint8Array. */ - wit_secretNonce(context: WitnessContext, roleId: Uint8Array): [P, Uint8Array]; - wit_getRoleCommitmentPath(context: WitnessContext, roleCommitment: Uint8Array): [P, MerkleTreePath]; - wit_getRoleIndex(context: WitnessContext, roleId: Uint8Array): [P, bigint]; + wit_secretNonce( + context: WitnessContext, + roleId: Uint8Array, + ): [P, Uint8Array]; + wit_getRoleCommitmentPath( + context: WitnessContext, + roleCommitment: Uint8Array, + ): [P, MerkleTreePath]; + wit_getRoleIndex( + context: WitnessContext, + roleId: Uint8Array, + ): [P, bigint]; } type RoleId = string; @@ -38,8 +58,8 @@ type SecretNonce = Uint8Array; */ export type ShieldedAccessControlPrivateState = { /** @description A 32-byte secret nonce used as a privacy additive. */ - roles: Record, - account: Either + roles: Record; + account: Either; }; /** @@ -50,9 +70,14 @@ export const ShieldedAccessControlPrivateState = { * @description Generates a new private state with a random secret nonce and a default roleId of 0. * @returns A fresh ShieldedAccessControlPrivateState instance. */ - generate: (account: Either): ShieldedAccessControlPrivateState => { + generate: ( + account: Either, + ): ShieldedAccessControlPrivateState => { const defaultRoleId: string = Buffer.alloc(32).toString('hex'); - const privateState: ShieldedAccessControlPrivateState = { roles: {}, account }; + const privateState: ShieldedAccessControlPrivateState = { + roles: {}, + account, + }; privateState.roles[defaultRoleId] = getRandomValues(Buffer.alloc(32)); return privateState; }, @@ -71,33 +96,56 @@ export const ShieldedAccessControlPrivateState = { * const privateState = ShieldedAccessControlPrivateState.withNonce(deterministicNonce); * ``` */ - withRoleAndNonce: (account: Either, roleId: Buffer, nonce: Buffer): ShieldedAccessControlPrivateState => { + withRoleAndNonce: ( + account: Either, + roleId: Buffer, + nonce: Buffer, + ): ShieldedAccessControlPrivateState => { const roleString = roleId.toString('hex'); - const privateState: ShieldedAccessControlPrivateState = { roles: {}, account }; + const privateState: ShieldedAccessControlPrivateState = { + roles: {}, + account, + }; privateState.roles[roleString] = nonce; return privateState; }, - setRole: (privateState: ShieldedAccessControlPrivateState, roleId: Buffer, nonce: Buffer): ShieldedAccessControlPrivateState => { + setRole: ( + privateState: ShieldedAccessControlPrivateState, + roleId: Buffer, + nonce: Buffer, + ): ShieldedAccessControlPrivateState => { const roleString = roleId.toString('hex'); privateState.roles[roleString] = nonce; return privateState; }, - getRoleCommitmentPath: (ledger: Ledger, roleCommitment: Uint8Array): MerkleTreePath => { - const path = ledger.ShieldedAccessControl__operatorRoles.findPathForLeaf(roleCommitment); + getRoleCommitmentPath: ( + ledger: Ledger, + roleCommitment: Uint8Array, + ): MerkleTreePath => { + const path = + ledger.ShieldedAccessControl__operatorRoles.findPathForLeaf( + roleCommitment, + ); const defaultPath: MerkleTreePath = { leaf: new Uint8Array(32), path: Array.from({ length: 10 }, () => ({ sibling: { field: 0n }, goes_left: false, - })) - } + })), + }; return path ? path : defaultPath; }, // If index cannot be found in MT return _currentMTIndex - getRoleIndex: ({ ledger, privateState }: WitnessContext, roleId: Uint8Array): bigint => { + getRoleIndex: ( + { + ledger, + privateState, + }: WitnessContext, + roleId: Uint8Array, + ): bigint => { const roleIdString = Buffer.from(roleId).toString('hex'); // Iterate over each MT to determine if commitment exists for (let i = 0; i < MERKLE_TREE_DEPTH; i++) { @@ -105,14 +153,23 @@ export const ShieldedAccessControlPrivateState = { const bIndex = convert_bigint_to_Uint8Array(32, BigInt(i)); const bAccount = eitherToBytes(privateState.account); const bNonce = privateState.roles[roleIdString]; - const commitment = persistentHash(rt_type, [roleId, bAccount, bNonce, bIndex, DOMAIN]); + const commitment = persistentHash(rt_type, [ + roleId, + bAccount, + bNonce, + bIndex, + DOMAIN, + ]); try { - ledger.ShieldedAccessControl__operatorRoles.pathForLeaf(BigInt(i), commitment); + ledger.ShieldedAccessControl__operatorRoles.pathForLeaf( + BigInt(i), + commitment, + ); return BigInt(i); } catch (e: unknown) { if (e instanceof Error) { - const [msg, index] = e.message.split(":"); - if (msg === "invalid index into sparse merkle tree") { + const [msg, index] = e.message.split(':'); + if (msg === 'invalid index into sparse merkle tree') { // console.log(`role ${fmtHexString(roleIdString)} with commitment ${fmtHexString(commitment)} not found at index ${index}`); } else { throw e; @@ -135,21 +192,30 @@ export const ShieldedAccessControlWitnesses = (): IShieldedAccessControlWitnesses => ({ wit_secretNonce( context: WitnessContext, - roleId: Uint8Array + roleId: Uint8Array, ): [ShieldedAccessControlPrivateState, Uint8Array] { const roleString = Buffer.from(roleId).toString('hex'); return [context.privateState, context.privateState.roles[roleString]]; }, wit_getRoleCommitmentPath( context: WitnessContext, - roleCommitment: Uint8Array + roleCommitment: Uint8Array, ): [ShieldedAccessControlPrivateState, MerkleTreePath] { - return [context.privateState, ShieldedAccessControlPrivateState.getRoleCommitmentPath(context.ledger, roleCommitment)]; + return [ + context.privateState, + ShieldedAccessControlPrivateState.getRoleCommitmentPath( + context.ledger, + roleCommitment, + ), + ]; }, wit_getRoleIndex( context: WitnessContext, - roleId: Uint8Array + roleId: Uint8Array, ): [ShieldedAccessControlPrivateState, bigint] { - return [context.privateState, ShieldedAccessControlPrivateState.getRoleIndex(context, roleId)]; + return [ + context.privateState, + ShieldedAccessControlPrivateState.getRoleIndex(context, roleId), + ]; }, - }); \ No newline at end of file + }); From 061ac4a124393a129c3df50f0b2adc152419f99e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Fri, 12 Sep 2025 16:00:40 -0400 Subject: [PATCH 192/322] WIP --- .../src/access/ShieldedAccessControl.compact | 4 +- .../access/test/ShieldedAccessControl.test.ts | 108 +++++++++++++++--- .../ShieldedAccessControlWitnesses.ts | 21 ++-- 3 files changed, 107 insertions(+), 26 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index 95abf9d2..a5a4c732 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -116,7 +116,7 @@ module ShieldedAccessControl { witness wit_secretNonce(roleId: Bytes<32>): Bytes<32>; - witness wit_getRoleIndex(roleId: Bytes<32>): Uint<64>; + witness wit_getRoleIndex(roleId: Bytes<32> , account: Either): Uint<64>; export struct Role { isApproved: Boolean; @@ -154,7 +154,7 @@ module ShieldedAccessControl { assert(!Utils_isContractAddress(account), "ShieldedAccessControl: contract address roles are not yet supported"); const nonce = wit_secretNonce(roleId); - const index = wit_getRoleIndex(roleId); + const index = wit_getRoleIndex(roleId, account); const computedCommitment = persistentHash>>([roleId, account.left.bytes, nonce, index as Field as Bytes<32>, pad(32, "ShieldedAccessControl:shield:")]); const authPath = wit_getRoleCommitmentPath(computedCommitment); diff --git a/contracts/src/access/test/ShieldedAccessControl.test.ts b/contracts/src/access/test/ShieldedAccessControl.test.ts index 73b5e4e3..ac20d8f1 100644 --- a/contracts/src/access/test/ShieldedAccessControl.test.ts +++ b/contracts/src/access/test/ShieldedAccessControl.test.ts @@ -35,7 +35,8 @@ const Z_OPERATOR_LIST = [Z_OPERATOR_1, Z_OPERATOR_2, Z_OPERATOR_3]; // Constants const BAD_NONCE = Buffer.alloc(32, 'BAD_NONCE'); -const DOMAIN = 'ShieldedAccessControl:shield:'; +const DOMAIN = new Uint8Array(32); +new TextEncoder().encodeInto('ShieldedAccessControl:shield:', DOMAIN); const INIT_COUNTER = 0n; const EMPTY_ROOT = { field: 0n }; @@ -49,11 +50,6 @@ const CUSTOM_ADMIN_ROLE = convert_bigint_to_Uint8Array(32, 4n); const UNINITIALIZED_ROLE = convert_bigint_to_Uint8Array(32, 5n); const OPERATOR_ROLE_LIST = [OPERATOR_ROLE_1, OPERATOR_ROLE_2, OPERATOR_ROLE_3]; -const operatorTypes = [ - ['contract', Z_OPERATOR_CONTRACT], - ['pubkey', Z_OPERATOR_1], -] as const; - // Role to string const DEFAULT_ADMIN_ROLE_TO_STRING = Buffer.from(DEFAULT_ADMIN_ROLE).toString('hex'); @@ -88,14 +84,13 @@ const buildCommitment = ( const rt_type = new CompactTypeVector(5, new CompactTypeBytes(32)); const bAccount = utils.eitherToBytes(account); const bIndex = convert_bigint_to_Uint8Array(32, index); - const bDomain = new TextEncoder().encode(DOMAIN); const commitment = persistentHash(rt_type, [ roleId, bAccount, nonce, bIndex, - bDomain, + DOMAIN, ]); return commitment; @@ -214,6 +209,7 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl.witnesses.wit_getRoleIndex( shieldedAccessControl.getWitnessContext(), DEFAULT_ADMIN_ROLE, + Z_ADMIN ); expect(witnessCalculatedIndex).toBe(INIT_COUNTER); } else { @@ -226,6 +222,7 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl.witnesses.wit_getRoleIndex( shieldedAccessControl.getWitnessContext(), DEFAULT_ADMIN_ROLE, + Z_ADMIN ); expect(witnessCalculatedIndex).not.toBe(INIT_COUNTER); } @@ -258,6 +255,7 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl.witnesses.wit_getRoleIndex( shieldedAccessControl.getWitnessContext(), DEFAULT_ADMIN_ROLE, + Z_ADMIN ); expect(witnessCalculatedPath).not.toEqual(truePath); } @@ -343,6 +341,7 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl.witnesses.wit_getRoleIndex( shieldedAccessControl.getWitnessContext(), DEFAULT_ADMIN_ROLE, + Z_ADMIN ); expect(witnessCalculatedIndex).toBe(INIT_COUNTER); } else { @@ -355,6 +354,7 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl.witnesses.wit_getRoleIndex( shieldedAccessControl.getWitnessContext(), DEFAULT_ADMIN_ROLE, + Z_ADMIN ); expect(witnessCalculatedIndex).not.toBe(INIT_COUNTER); } @@ -387,6 +387,7 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl.witnesses.wit_getRoleIndex( shieldedAccessControl.getWitnessContext(), DEFAULT_ADMIN_ROLE, + Z_ADMIN ); expect(witnessCalculatedPath).not.toEqual(truePath); } @@ -478,13 +479,6 @@ describe('ShieldedAccessControl', () => { ); }); - it('should throw if role has been revoked', () => { - shieldedAccessControl._revokeRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); - expect(() => { - shieldedAccessControl.hasRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); - }).toThrow('ShieldedAccessControl: role access has been revoked'); - }); - it('should return correct role commitment', () => { const expCommitment = buildCommitment( DEFAULT_ADMIN_ROLE, @@ -551,6 +545,7 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl.witnesses.wit_getRoleIndex( shieldedAccessControl.getWitnessContext(), DEFAULT_ADMIN_ROLE, + Z_ADMIN ); expect(witnessCalculatedIndex).toBe(INIT_COUNTER); } else { @@ -563,6 +558,7 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl.witnesses.wit_getRoleIndex( shieldedAccessControl.getWitnessContext(), DEFAULT_ADMIN_ROLE, + Z_ADMIN ); expect(witnessCalculatedIndex).not.toBe(INIT_COUNTER); } @@ -595,6 +591,7 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl.witnesses.wit_getRoleIndex( shieldedAccessControl.getWitnessContext(), DEFAULT_ADMIN_ROLE, + Z_ADMIN ); expect(witnessCalculatedPath).not.toEqual(truePath); } @@ -636,6 +633,7 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl.witnesses.wit_getRoleIndex( shieldedAccessControl.getWitnessContext(), DEFAULT_ADMIN_ROLE, + Z_ADMIN ); expect(witnessCalculatedIndex).toBe(INIT_COUNTER); } else { @@ -648,6 +646,7 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl.witnesses.wit_getRoleIndex( shieldedAccessControl.getWitnessContext(), DEFAULT_ADMIN_ROLE, + Z_ADMIN ); expect(witnessCalculatedIndex).not.toBe(INIT_COUNTER); } @@ -680,6 +679,7 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl.witnesses.wit_getRoleIndex( shieldedAccessControl.getWitnessContext(), DEFAULT_ADMIN_ROLE, + Z_ADMIN ); expect(witnessCalculatedPath).not.toEqual(truePath); } @@ -712,6 +712,7 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl.witnesses.wit_getRoleIndex( shieldedAccessControl.getWitnessContext(), DEFAULT_ADMIN_ROLE, + Z_ADMIN ); expect(witnessCalculatedIndex).toBe(INIT_COUNTER); @@ -884,6 +885,15 @@ describe('ShieldedAccessControl', () => { Z_OPERATOR_LIST[j], ); expect(role.isApproved).toBe(true); + + + expect( + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf( + EXP_DEFAULT_ADMIN_COMMITMENT, + ), + ).toBeDefined(); } } }); @@ -902,16 +912,80 @@ describe('ShieldedAccessControl', () => { }); }); - describe('revokeRole', () => { + describe.only('revokeRole', () => { beforeEach(() => { + console.log("TEST - Current MT Index", shieldedAccessControl.getPublicState().ShieldedAccessControl__currentMerkleTreeIndex.toString()); shieldedAccessControl._grantRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); + console.log("TEST - Current MT Index", shieldedAccessControl.getPublicState().ShieldedAccessControl__currentMerkleTreeIndex.toString()); shieldedAccessControl.privateState.injectSecretNonce( OPERATOR_ROLE_1, OPERATOR_ROLE_1_SECRET_NONCE, ); + shieldedAccessControl._grantRole(OPERATOR_ROLE_1, Z_OPERATOR_1); + console.log("TEST - Current MT Index", shieldedAccessControl.getPublicState().ShieldedAccessControl__currentMerkleTreeIndex.toString()); shieldedAccessControl.callerCtx.setCaller(ADMIN); }); - it.todo('admin should revoke role', () => {}); + it('admin should revoke role', () => { + expect(shieldedAccessControl.hasRole(OPERATOR_ROLE_1, Z_OPERATOR_1).isApproved).toBe(true); + shieldedAccessControl.revokeRole(OPERATOR_ROLE_1, Z_OPERATOR_1); + expect(shieldedAccessControl.hasRole(OPERATOR_ROLE_1, Z_OPERATOR_1).isApproved).toBe(false); + }); + + it.only('commitment should be in nullifier set', () => { + console.log("TEST - Current MT Index", shieldedAccessControl.getPublicState().ShieldedAccessControl__currentMerkleTreeIndex.toString()); + const [, opRoleIndex] = shieldedAccessControl.witnesses.wit_getRoleIndex(shieldedAccessControl.getWitnessContext(), OPERATOR_ROLE_1, Z_OPERATOR_1); + const [, adminRoleIndex] = shieldedAccessControl.witnesses.wit_getRoleIndex(shieldedAccessControl.getWitnessContext(), DEFAULT_ADMIN_ROLE, Z_ADMIN); + console.log("OPERATOR INDEX ", opRoleIndex.toString(10)); + console.log("ADMIN INDEX ", adminRoleIndex.toString(10)); + const expCommitmentOp = buildCommitment(OPERATOR_ROLE_1, Z_OPERATOR_1, OPERATOR_ROLE_1_SECRET_NONCE, opRoleIndex); + const pathToOp = shieldedAccessControl.getPublicState().ShieldedAccessControl__operatorRoles.findPathForLeaf(expCommitmentOp); + const pathToAdmin = shieldedAccessControl.getPublicState().ShieldedAccessControl__operatorRoles.findPathForLeaf(EXP_DEFAULT_ADMIN_COMMITMENT); + //console.log("PATH TO OP ", pathToOp); + //console.log("PATH TO ADMIN ", pathToAdmin); + + //console.log("EXPECTED COMMITMENT ", expCommitmentOp); + const contractCommit = shieldedAccessControl.hasRole(OPERATOR_ROLE_1, Z_OPERATOR_1).roleCommitment; + //console.log("CONTRACT COMMITMENT ", contractCommit); + + shieldedAccessControl.revokeRole(OPERATOR_ROLE_1, Z_OPERATOR_1); + expect(shieldedAccessControl.getPublicState().ShieldedAccessControl__roleCommitmentNullifiers.isEmpty()).toBe(false); + expect(shieldedAccessControl.getPublicState().ShieldedAccessControl__roleCommitmentNullifiers.member(expCommitmentOp)).toBe(true); + }); + + it('admin should revoke multiple roles', () => { + const expCommitment = buildCommitment(OPERATOR_ROLE_1, Z_OPERATOR_1, OPERATOR_ROLE_1_SECRET_NONCE, 1n); + shieldedAccessControl.revokeRole(OPERATOR_ROLE_1, Z_OPERATOR_1); + expect(shieldedAccessControl.getPublicState().ShieldedAccessControl__roleCommitmentNullifiers.member(expCommitment)).toBe(true); + + for (let i = 1; i < OPERATOR_ROLE_LIST.length; i++) { + shieldedAccessControl.privateState.injectSecretNonce( + OPERATOR_ROLE_LIST[i], + OPERATOR_ROLE_SECRET_NONCES[i], + ); + for (let j = 1; j < Z_OPERATOR_LIST.length; j++) { + shieldedAccessControl._grantRole( + OPERATOR_ROLE_LIST[i], + Z_OPERATOR_LIST[j], + ); + const expCommitment = buildCommitment(OPERATOR_ROLE_LIST[i], Z_OPERATOR_LIST[j], OPERATOR_ROLE_SECRET_NONCES[i], BigInt(1 + i)); + shieldedAccessControl.revokeRole(OPERATOR_ROLE_LIST[i], Z_OPERATOR_LIST[j]); + expect(shieldedAccessControl.getPublicState().ShieldedAccessControl__roleCommitmentNullifiers.member(expCommitment)).toBe(true); + } + } + }); + + it('should throw if non-admin operator revokes role', () => { + shieldedAccessControl.privateState.injectSecretNonce( + OPERATOR_ROLE_1, + OPERATOR_ROLE_1_SECRET_NONCE, + ); + shieldedAccessControl._grantRole(OPERATOR_ROLE_1, Z_OPERATOR_1); + + shieldedAccessControl.callerCtx.setCaller(OPERATOR_1); + expect(() => { + shieldedAccessControl.revokeRole(OPERATOR_ROLE_1, Z_UNAUTHORIZED); + }).toThrow('ShieldedAccessControl: unauthorized account'); + }); }); }); diff --git a/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts b/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts index f36a1303..425f3e29 100644 --- a/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts +++ b/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts @@ -16,14 +16,16 @@ import type { import { eitherToBytes } from '../test/utils/address'; const MERKLE_TREE_DEPTH = 2 ** 10; -const DOMAIN = new TextEncoder().encode('ShieldedAccessControl:shield:'); +const DOMAIN = new Uint8Array(32); +new TextEncoder().encodeInto('ShieldedAccessControl:shield:', DOMAIN); export function fmtHexString(bytes: string | Uint8Array): string { if (bytes instanceof String) { return `${bytes.slice(0, 4)}...${bytes.slice(-4)}`; + } else { + const buffStr = Buffer.from(bytes as Uint8Array).toString('hex'); + return `${buffStr.slice(0, 4)}...${buffStr.slice(-4)}`; } - const buffStr = Buffer.from(bytes).toString('hex'); - return `${buffStr.slice(0, 4)}...${buffStr.slice(-4)}`; } /** @@ -47,6 +49,7 @@ export interface IShieldedAccessControlWitnesses

{ wit_getRoleIndex( context: WitnessContext, roleId: Uint8Array, + account: Either ): [P, bigint]; } @@ -145,13 +148,14 @@ export const ShieldedAccessControlPrivateState = { privateState, }: WitnessContext, roleId: Uint8Array, + account: Either ): bigint => { const roleIdString = Buffer.from(roleId).toString('hex'); - // Iterate over each MT to determine if commitment exists + // Iterate over each MT index to determine if commitment exists for (let i = 0; i < MERKLE_TREE_DEPTH; i++) { const rt_type = new CompactTypeVector(5, new CompactTypeBytes(32)); const bIndex = convert_bigint_to_Uint8Array(32, BigInt(i)); - const bAccount = eitherToBytes(privateState.account); + const bAccount = eitherToBytes(account); const bNonce = privateState.roles[roleIdString]; const commitment = persistentHash(rt_type, [ roleId, @@ -170,7 +174,7 @@ export const ShieldedAccessControlPrivateState = { if (e instanceof Error) { const [msg, index] = e.message.split(':'); if (msg === 'invalid index into sparse merkle tree') { - // console.log(`role ${fmtHexString(roleIdString)} with commitment ${fmtHexString(commitment)} not found at index ${index}`); + //console.log(`role ${fmtHexString(roleIdString)} with commitment ${fmtHexString(commitment)} not found at index ${index}`); } else { throw e; } @@ -178,6 +182,8 @@ export const ShieldedAccessControlPrivateState = { } } + console.log("WIT - Commitment DNE, returing MT index ", ledger.ShieldedAccessControl__currentMerkleTreeIndex.toString()); + // If commitment doesn't exist return currentMTIndex // Used for adding roles return ledger.ShieldedAccessControl__currentMerkleTreeIndex; @@ -212,10 +218,11 @@ export const ShieldedAccessControlWitnesses = wit_getRoleIndex( context: WitnessContext, roleId: Uint8Array, + account: Either ): [ShieldedAccessControlPrivateState, bigint] { return [ context.privateState, - ShieldedAccessControlPrivateState.getRoleIndex(context, roleId), + ShieldedAccessControlPrivateState.getRoleIndex(context, roleId, account), ]; }, }); From a98144ef8d1ec992efa662609bdbb5cf5563e438 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Mon, 15 Sep 2025 13:50:23 -0400 Subject: [PATCH 193/322] Update contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Andrew Fleming Signed-off-by: ⟣ €₥ℵ∪ℓ ⟢ <34749913+emnul@users.noreply.github.com> --- .../src/access/witnesses/ShieldedAccessControlWitnesses.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts b/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts index 425f3e29..119ed504 100644 --- a/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts +++ b/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts @@ -152,7 +152,7 @@ export const ShieldedAccessControlPrivateState = { ): bigint => { const roleIdString = Buffer.from(roleId).toString('hex'); // Iterate over each MT index to determine if commitment exists - for (let i = 0; i < MERKLE_TREE_DEPTH; i++) { + for (let i = 0; i <= ledger.ShieldedAccessControl__currentMerkleTreeIndex; i++) { const rt_type = new CompactTypeVector(5, new CompactTypeBytes(32)); const bIndex = convert_bigint_to_Uint8Array(32, BigInt(i)); const bAccount = eitherToBytes(account); From 9d9c2565dba81eb162ed731c1b3b22228b03af3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Mon, 15 Sep 2025 14:04:26 -0400 Subject: [PATCH 194/322] Optimize loop --- .../access/witnesses/ShieldedAccessControlWitnesses.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts b/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts index 425f3e29..6a2a4f14 100644 --- a/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts +++ b/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts @@ -15,7 +15,7 @@ import type { } from '../../../artifacts/MockShieldedAccessControl/contract/index.cjs'; import { eitherToBytes } from '../test/utils/address'; -const MERKLE_TREE_DEPTH = 2 ** 10; +const MERKLE_TREE_DEPTH = 2 ** 11 - 1; const DOMAIN = new Uint8Array(32); new TextEncoder().encodeInto('ShieldedAccessControl:shield:', DOMAIN); @@ -151,12 +151,12 @@ export const ShieldedAccessControlPrivateState = { account: Either ): bigint => { const roleIdString = Buffer.from(roleId).toString('hex'); + const bNonce = privateState.roles[roleIdString]; + const rt_type = new CompactTypeVector(5, new CompactTypeBytes(32)); + const bAccount = eitherToBytes(account); // Iterate over each MT index to determine if commitment exists for (let i = 0; i < MERKLE_TREE_DEPTH; i++) { - const rt_type = new CompactTypeVector(5, new CompactTypeBytes(32)); const bIndex = convert_bigint_to_Uint8Array(32, BigInt(i)); - const bAccount = eitherToBytes(account); - const bNonce = privateState.roles[roleIdString]; const commitment = persistentHash(rt_type, [ roleId, bAccount, @@ -174,7 +174,7 @@ export const ShieldedAccessControlPrivateState = { if (e instanceof Error) { const [msg, index] = e.message.split(':'); if (msg === 'invalid index into sparse merkle tree') { - //console.log(`role ${fmtHexString(roleIdString)} with commitment ${fmtHexString(commitment)} not found at index ${index}`); + // console.log(`role ${fmtHexString(roleIdString)} with commitment ${fmtHexString(commitment)} not found at index ${index}`); } else { throw e; } From 5d2da061c36c98f2bc2202a4c888cb858549f765 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Tue, 16 Sep 2025 22:37:17 -0400 Subject: [PATCH 195/322] Refactor Shielded Design --- .../src/access/ShieldedAccessControl.compact | 181 ++++++++++++------ 1 file changed, 123 insertions(+), 58 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index a5a4c732..7700e0b0 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -116,13 +116,101 @@ module ShieldedAccessControl { witness wit_secretNonce(roleId: Bytes<32>): Bytes<32>; - witness wit_getRoleIndex(roleId: Bytes<32> , account: Either): Uint<64>; + witness wit_getRoleIndex(roleId: Bytes<32> , accountId: Bytes<32>): Uint<64>; export struct Role { isApproved: Boolean; roleCommitment: Bytes<32>; } + /** + * @description Computes the owner commitment from the given `id` and `counter`. + * + * ## Owner ID (`id`) + * The `id` is expected to be computed off-chain as: + * `id = SHA256(pk, nonce)` + * + * - `pk`: The owner's public key. + * - `nonce`: A secret nonce scoped to the instance, ideally rotated with each transfer. + * + * ## Commitment Derivation + * `commitment = SHA256(id, instanceSalt, counter, domain)` + * + * - `id`: See above. + * - `instanceSalt`: A unique per-deployment salt, stored during initialization. + * This prevents commitment collisions across deployments. + * - `counter`: Incremented with each ownership transfer, ensuring uniqueness + * even with repeated `id` values. Cast to `Field` then `Bytes<32>` for hashing. + * - `domain`: Domain separator `"ZOwnablePK:shield:"` (padded to 32 bytes) to prevent + * hash collisions when extending the module or using similar commitment schemes. + * + * @circuitInfo k=14, rows=14853 + * + * Requirements: + * + * - Contract is initialized. + * + * @param {Bytes<32>} id - The unique identifier of the owner calculated by `SHA256(pk, nonce)`. + * @param {Uint<64>} counter - The current counter or round. This increments by `1` + * after every transfer to prevent duplicate commitments given the same `id`. + * @returns {Bytes<32>} The commitment derived from `id` and `counter`. + */ + export circuit _computeRoleCommitment( + accountId: Bytes<32>, + roleId: Bytes<32>, + index: Uint<64>, + ): Bytes<32> { + return persistentHash>>( + [ + accountId, + roleId, + index as Field as Bytes<32>, + pad(32, "ShieldedAccessControl:shield:") + ] + ); + } + + /** + * @description Computes the unique identifier (`id`) of the owner from their + * public key and a secret nonce. + * + * ## ID Derivation + * `id = SHA256(pk, nonce)` + * + * - `pk`: The public key of the caller. This is passed explicitly to allow + * for off-chain derivation, testing, or scenarios where the caller is + * different from the subject of the computation. + * We recommend using an Air-Gapped Public Key. + * - `nonce`: A secret nonce tied to the identity. The generation strategy is + * left to the user, offering different security/convenience trade-offs. + * + * The result is a 32-byte commitment that uniquely identifies the owner. + * This value is later used in owner commitment hashing, + * and acts as a privacy-preserving alternative to a raw public key. + * + * @notice This module allows ownership to be tied to an identity commitment derived + * from a public key and secret nonce. + * While typically used with user public keys, this mechanism may also + * support contract addresses as identifiers in future contract-to-contract + * interactions. Both are treated as 32-byte values (`Bytes<32>`). + * + * Requirements: + * + * - `pk` is not a ContractAddress. + * + * @param {Either} pk - The public key of the identity being committed. + * @param {Bytes<32>} nonce - A private nonce to scope the commitment. + * @returns {Bytes<32>} The computed owner ID. + */ + export pure circuit _computeRoleId( + pk: Either, + nonce: Bytes<32> + ): Bytes<32> { + assert(pk.is_left, "ShieldedAccessControl: contract address owners are not yet supported"); + + return persistentHash>>([pk.left.bytes, nonce]); + } + /** * @description Returns `true` if `account` has been granted `roleId`. * @@ -150,22 +238,17 @@ module ShieldedAccessControl { * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK). * @return {Boolean} - A boolean determining if the account has the specified role.  */ - export circuit hasRole(roleId: Bytes<32>, account: Either): Role { - assert(!Utils_isContractAddress(account), "ShieldedAccessControl: contract address roles are not yet supported"); + export circuit callerHasRole(roleId: Bytes<32>): Role { + const account = ownPublicKey(); const nonce = wit_secretNonce(roleId); - const index = wit_getRoleIndex(roleId, account); - const computedCommitment = persistentHash>>([roleId, account.left.bytes, nonce, index as Field as Bytes<32>, pad(32, "ShieldedAccessControl:shield:")]); - - const authPath = wit_getRoleCommitmentPath(computedCommitment); - const rootMatches = _operatorRoles - .checkRoot(merkleTreePathRoot<10, Bytes<32>>(disclose(authPath))); + const accountId = _computeRoleId(account, nonce); + return getRole(roleId, accountId); + } - if(!_roleCommitmentNullifiers.member(disclose(computedCommitment)) && rootMatches) { - return Role {isApproved: true, roleCommitment: disclose(computedCommitment)}; - } else { - return Role {isApproved: false, roleCommitment: disclose(computedCommitment)}; - } + export hasRole(roleId: Bytes<32>, accountId: Bytes<32>): Boolean { + const roleInfo = getRoleInfo(); + return roleInfo.isApproved; } /** @@ -196,41 +279,22 @@ module ShieldedAccessControl { * @return {[]} - Empty tuple. */ export circuit assertOnlyRole(roleId: Bytes<32>): [] { - _checkRole( - roleId, - left(ownPublicKey()) - ); + const role = callerHasRole(roleId); + assert(role.isApproved, "ShieldedAccessControl: unauthorized account"); } - /** - * @description Reverts if `account` is missing `roleId`. - * - * @circuitInfo k=16, rows=60129 - * - * Requirements: - * - * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce) - * must exist in the `_roleCommitmentIndex` map. - * - A nullifier for the role commitment produced by SHA256(roleId | account | nonce) - * must not exist in the `_roleCommitmentNullifiers` set. - * - A path for the role commitment produced by SHA256(roleId | account | nonce) must - * exist at `index` in the `_operatorRoles` Merkle tree. - * - * Disclosures: - * - * - The intermediate role commitment produced by SHA256(roleId | account | nonce). - * - The role commitment produced by SHA256(roleId | account | nonce). - * - The Merkle tree path for the role commitment stored at `index` in the `_operatorRoles` - * Merkle tree. - * - The type data of `account` - a ZswapCoinPublicKey or ContractAddress. - * - * @param {Bytes<32>} roleId - The role identifier. - * @param {Either} account - The account to check. - * @return {[]} - Empty tuple. - */ - export circuit _checkRole(roleId: Bytes<32>, account: Either): [] { - const role = hasRole(roleId, account); - assert(role.isApproved, "ShieldedAccessControl: unauthorized account"); + export circuit getRole(roleId: Bytes<32>, accountId: Bytes<32>): Role { + const index = wit_getRoleIndex(roleId, accountId); + const commitment = _computeRoleCommitment(accountId, roleId, index); + const authPath = wit_getRoleCommitmentPath(commitment); + const rootMatches = _operatorRoles + .checkRoot(merkleTreePathRoot<10, Bytes<32>>(disclose(authPath))); + + if(!_roleCommitmentNullifiers.member(disclose(commitment)) && rootMatches) { + return Role {isApproved: true, roleCommitment: disclose(commitment)}; + } else { + return Role {isApproved: false, roleCommitment: disclose(commitment)}; + } } /** @@ -279,9 +343,9 @@ module ShieldedAccessControl { * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK) * @return {[]} - Empty tuple. */ - export circuit grantRole(roleId: Bytes<32>, account: Either): [] { + export circuit grantRole(roleId: Bytes<32>, accountId: Bytes<32>): [] { assertOnlyRole(getRoleAdmin(roleId)); - _grantRole(roleId, account); + _grantRole(roleId, accountId); } /** @@ -312,9 +376,9 @@ module ShieldedAccessControl { * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK) * @return {[]} - Empty tuple. */ - export circuit revokeRole(roleId: Bytes<32>, account: Either): [] { + export circuit revokeRole(roleId: Bytes<32>, accountId: Bytes<32>): [] { assertOnlyRole(getRoleAdmin(roleId)); - _revokeRole(roleId, account); + _revokeRole(roleId, accountId); } /** @@ -350,10 +414,11 @@ module ShieldedAccessControl { * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK) * @return {[]} - Empty tuple. */ - export circuit renounceRole(roleId: Bytes<32>, callerConfirmation: Either): [] { - assert(callerConfirmation == left(ownPublicKey()), "ShieldedAccessControl: bad confirmation"); + export circuit renounceRole(roleId: Bytes<32>, accountId: Bytes<32>): [] { + const nonce = wit_secretNonce(roleId); + assert(accountId, _computeRoleId(ownPublicKey(), nonce), "ShieldedAccessControl: bad confirmation"); - _revokeRole(roleId, callerConfirmation); + _revokeRole(roleId, accountId); } /** @@ -398,8 +463,8 @@ module ShieldedAccessControl { * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK) * @return {Boolean} roleGranted - A boolean indicating if `roleId` was granted. */ - export circuit _grantRole(roleId: Bytes<32>, account: Either): Boolean { - const role = hasRole(roleId, account); + export circuit _grantRole(roleId: Bytes<32>, accountId: Bytes<32>): Boolean { + const role = getRole(roleId, accountId); if (role.isApproved) { return false; } @@ -438,8 +503,8 @@ module ShieldedAccessControl { * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK) * @return {Boolean} roleRevoked - A boolean indicating if `roleId` was revoked. */ - export circuit _revokeRole(roleId: Bytes<32>, account: Either): Boolean { - const role = hasRole(roleId, account); + export circuit _revokeRole(roleId: Bytes<32>, accountId: Bytes<32>): Boolean { + const role = getRole(roleId, account); if (!role.isApproved) { return false; } From 713d3d426d20bd531ff479c02d8915127006d7ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Tue, 16 Sep 2025 22:38:25 -0400 Subject: [PATCH 196/322] Move resetProxy call to end of fn --- .../access/test/simulators/ShieldedAccessControlSimulator.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts index 69e25938..170c41ec 100644 --- a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts +++ b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts @@ -215,12 +215,12 @@ export class ShieldedAccessControlSimulator extends AbstractContractSimulator< public set witnesses(newWitnesses: ReturnType< typeof ShieldedAccessControlWitnesses >) { - this.resetCircuitProxies(); this._witnesses = newWitnesses; this.contract = new MockShieldedAccessControl( this._witnesses, ); + this.resetCircuitProxies(); } public overrideWitness( @@ -388,6 +388,6 @@ export class ShieldedAccessControlSimulator extends AbstractContractSimulator< */ setCaller: (caller: CoinPublicKey) => { this.callerOverride = caller; - }, + } }; } From 8a5fbba71e0d1f6ad7e448d3e36e3caf32b86537 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Tue, 16 Sep 2025 22:38:52 -0400 Subject: [PATCH 197/322] Fixes incorrect indexing bug --- .../witnesses/ShieldedAccessControlWitnesses.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts b/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts index 8f557a4c..36bf6ac2 100644 --- a/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts +++ b/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts @@ -15,7 +15,6 @@ import type { } from '../../../artifacts/MockShieldedAccessControl/contract/index.cjs'; import { eitherToBytes } from '../test/utils/address'; -const MERKLE_TREE_DEPTH = 2 ** 11 - 1; const DOMAIN = new Uint8Array(32); new TextEncoder().encodeInto('ShieldedAccessControl:shield:', DOMAIN); @@ -156,7 +155,6 @@ export const ShieldedAccessControlPrivateState = { const bAccount = eitherToBytes(account); // Iterate over each MT index to determine if commitment exists for (let i = 0; i <= ledger.ShieldedAccessControl__currentMerkleTreeIndex; i++) { - const rt_type = new CompactTypeVector(5, new CompactTypeBytes(32)); const bIndex = convert_bigint_to_Uint8Array(32, BigInt(i)); const commitment = persistentHash(rt_type, [ roleId, @@ -166,11 +164,14 @@ export const ShieldedAccessControlPrivateState = { DOMAIN, ]); try { - ledger.ShieldedAccessControl__operatorRoles.pathForLeaf( - BigInt(i), + const index = BigInt(i); + const pathForLeaf = ledger.ShieldedAccessControl__operatorRoles.pathForLeaf( + index, commitment, ); - return BigInt(i); + if (pathForLeaf.leaf === commitment) { + return index; + } } catch (e: unknown) { if (e instanceof Error) { const [msg, index] = e.message.split(':'); From 2fce76c8c73350dc8d492bb3869df1b51ff59a17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Tue, 16 Sep 2025 22:39:20 -0400 Subject: [PATCH 198/322] WIP refactor tests --- .../access/test/ShieldedAccessControl.test.ts | 991 ---------------- .../access/test/ShieldedAccessControl_OLD.ts | 1053 +++++++++++++++++ 2 files changed, 1053 insertions(+), 991 deletions(-) create mode 100644 contracts/src/access/test/ShieldedAccessControl_OLD.ts diff --git a/contracts/src/access/test/ShieldedAccessControl.test.ts b/contracts/src/access/test/ShieldedAccessControl.test.ts index ac20d8f1..e69de29b 100644 --- a/contracts/src/access/test/ShieldedAccessControl.test.ts +++ b/contracts/src/access/test/ShieldedAccessControl.test.ts @@ -1,991 +0,0 @@ -import { - CompactTypeBytes, - CompactTypeVector, - convert_bigint_to_Uint8Array, - persistentHash, - type WitnessContext, -} from '@midnight-ntwrk/compact-runtime'; -import { beforeEach, describe, expect, it } from 'vitest'; -import type { - ContractAddress, - Either, - Ledger, - MerkleTreePath, - ShieldedAccessControl_Role as Role, - ZswapCoinPublicKey, -} from '../../../artifacts/MockShieldedAccessControl/contract/index.cjs'; -import { ShieldedAccessControlPrivateState } from '../witnesses/ShieldedAccessControlWitnesses.js'; -import { ShieldedAccessControlSimulator } from './simulators/ShieldedAccessControlSimulator.js'; -import * as utils from './utils/address.js'; - -// PKs -const [ADMIN, Z_ADMIN] = utils.generateEitherPubKeyPair('ADMIN'); -const [UNAUTHORIZED, Z_UNAUTHORIZED] = - utils.generateEitherPubKeyPair('UNAUTHORIZED'); -const [CUSTOM_ADMIN, Z_CUSTOM_ADMIN] = - utils.generateEitherPubKeyPair('CUSTOM_ADMIN'); -const [OPERATOR_1, Z_OPERATOR_1] = utils.generateEitherPubKeyPair('OPERATOR_1'); -const [OPERATOR_2, Z_OPERATOR_2] = utils.generateEitherPubKeyPair('OPERATOR_2'); -const [OPERATOR_3, Z_OPERATOR_3] = utils.generateEitherPubKeyPair('OPERATOR_3'); -const [OPERATOR_CONTRACT, Z_OPERATOR_CONTRACT] = utils.generateEitherPubKeyPair( - 'OPERATOR_CONTRACT', - false, -); -const Z_OPERATOR_LIST = [Z_OPERATOR_1, Z_OPERATOR_2, Z_OPERATOR_3]; - -// Constants -const BAD_NONCE = Buffer.alloc(32, 'BAD_NONCE'); -const DOMAIN = new Uint8Array(32); -new TextEncoder().encodeInto('ShieldedAccessControl:shield:', DOMAIN); -const INIT_COUNTER = 0n; - -const EMPTY_ROOT = { field: 0n }; - -// Roles -const DEFAULT_ADMIN_ROLE = utils.zeroUint8Array(); -const OPERATOR_ROLE_1 = convert_bigint_to_Uint8Array(32, 1n); -const OPERATOR_ROLE_2 = convert_bigint_to_Uint8Array(32, 2n); -const OPERATOR_ROLE_3 = convert_bigint_to_Uint8Array(32, 3n); -const CUSTOM_ADMIN_ROLE = convert_bigint_to_Uint8Array(32, 4n); -const UNINITIALIZED_ROLE = convert_bigint_to_Uint8Array(32, 5n); -const OPERATOR_ROLE_LIST = [OPERATOR_ROLE_1, OPERATOR_ROLE_2, OPERATOR_ROLE_3]; - -// Role to string -const DEFAULT_ADMIN_ROLE_TO_STRING = - Buffer.from(DEFAULT_ADMIN_ROLE).toString('hex'); - -const ADMIN_SECRET_NONCE = Buffer.alloc(32, 'ADMIN_SECRET_NONCE'); -const OPERATOR_ROLE_1_SECRET_NONCE = Buffer.alloc( - 32, - 'OPERATOR_ROLE_1_SECRET_NONCE', -); -const OPERATOR_ROLE_2_SECRET_NONCE = Buffer.alloc( - 32, - 'OPERATOR_ROLE_2_SECRET_NONCE', -); -const OPERATOR_ROLE_3_SECRET_NONCE = Buffer.alloc( - 32, - 'OPERATOR_ROLE_3_SECRET_NONCE', -); -const OPERATOR_ROLE_SECRET_NONCES = [ - OPERATOR_ROLE_1_SECRET_NONCE, - OPERATOR_ROLE_2_SECRET_NONCE, - OPERATOR_ROLE_3_SECRET_NONCE, -]; -let shieldedAccessControl: ShieldedAccessControlSimulator; - -// Helpers -const buildCommitment = ( - roleId: Uint8Array, - account: Either, - nonce: Uint8Array, - index: bigint, -): Uint8Array => { - const rt_type = new CompactTypeVector(5, new CompactTypeBytes(32)); - const bAccount = utils.eitherToBytes(account); - const bIndex = convert_bigint_to_Uint8Array(32, index); - - const commitment = persistentHash(rt_type, [ - roleId, - bAccount, - nonce, - bIndex, - DOMAIN, - ]); - - return commitment; -}; - -const EXP_DEFAULT_ADMIN_COMMITMENT = buildCommitment( - DEFAULT_ADMIN_ROLE, - Z_ADMIN, - ADMIN_SECRET_NONCE, - INIT_COUNTER, -); - -function RETURN_BAD_INDEX( - context: WitnessContext, - roleId: Uint8Array, -): [ShieldedAccessControlPrivateState, bigint] { - return [context.privateState, 1023n]; -} - -function RETURN_BAD_PATH( - context: WitnessContext, - roleCommitment: Uint8Array, -): [ShieldedAccessControlPrivateState, MerkleTreePath] { - const defaultPath: MerkleTreePath = { - leaf: new Uint8Array(32), - path: Array.from({ length: 10 }, () => ({ - sibling: { field: 0n }, - goes_left: false, - })), - }; - return [context.privateState, defaultPath]; -} - -type RoleAndNonce = { - roleId: string; - nonce: Buffer; -}; - -describe('ShieldedAccessControl', () => { - beforeEach(() => { - // Create private state object and generate nonce - const PS = ShieldedAccessControlPrivateState.withRoleAndNonce( - Z_ADMIN, - Buffer.from(DEFAULT_ADMIN_ROLE), - ADMIN_SECRET_NONCE, - ); - // Init contract for user with PS - shieldedAccessControl = new ShieldedAccessControlSimulator(Z_ADMIN, { - privateState: PS, - }); - }); - - describe('checked circuits should fail for authorized caller with invalid witness values', () => { - beforeEach(() => { - shieldedAccessControl._grantRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); - shieldedAccessControl.callerCtx.setCaller(ADMIN); - }); - - type FailingCircuits = [ - method: keyof ShieldedAccessControlSimulator, - isValidNonce: boolean, - isValidIndex: boolean, - isValidPath: boolean, - args: unknown[], - ]; - const checkedCircuits: FailingCircuits[] = [ - ['assertOnlyRole', false, true, true, [DEFAULT_ADMIN_ROLE]], - ['assertOnlyRole', true, false, true, [DEFAULT_ADMIN_ROLE]], - ['assertOnlyRole', true, true, false, [DEFAULT_ADMIN_ROLE]], - ['assertOnlyRole', false, false, true, [DEFAULT_ADMIN_ROLE]], - ['assertOnlyRole', true, false, false, [DEFAULT_ADMIN_ROLE]], - ['assertOnlyRole', false, true, false, [DEFAULT_ADMIN_ROLE]], - ['assertOnlyRole', false, false, false, [DEFAULT_ADMIN_ROLE]], - ['grantRole', false, true, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ['grantRole', true, false, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ['grantRole', true, true, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ['grantRole', false, false, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ['grantRole', true, false, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ['grantRole', false, true, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ['grantRole', false, false, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ['revokeRole', false, true, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ['revokeRole', true, false, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ['revokeRole', true, true, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ['revokeRole', false, false, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ['revokeRole', true, false, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ['revokeRole', false, true, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ['revokeRole', false, false, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ]; - - it.each(checkedCircuits)( - '%s should fail with isValidNonce(%s), isValidIndex(%s), isValidPath(%s)', - (circuitName, isValidNonce, isValidIndex, isValidPath, args) => { - if (isValidNonce) { - // Check nonce matches - expect( - shieldedAccessControl.privateState.getCurrentSecretNonce( - DEFAULT_ADMIN_ROLE, - ), - ).toEqual(ADMIN_SECRET_NONCE); - } else { - // Check nonce does not match - shieldedAccessControl.privateState.injectSecretNonce( - DEFAULT_ADMIN_ROLE, - BAD_NONCE, - ); - expect( - shieldedAccessControl.privateState.getCurrentSecretNonce( - DEFAULT_ADMIN_ROLE, - ), - ).not.toEqual(ADMIN_SECRET_NONCE); - } - - if (isValidIndex) { - // Check index matches - const [, witnessCalculatedIndex] = - shieldedAccessControl.witnesses.wit_getRoleIndex( - shieldedAccessControl.getWitnessContext(), - DEFAULT_ADMIN_ROLE, - Z_ADMIN - ); - expect(witnessCalculatedIndex).toBe(INIT_COUNTER); - } else { - // Check index does not match - shieldedAccessControl.overrideWitness( - 'wit_getRoleIndex', - RETURN_BAD_INDEX, - ); - const [, witnessCalculatedIndex] = - shieldedAccessControl.witnesses.wit_getRoleIndex( - shieldedAccessControl.getWitnessContext(), - DEFAULT_ADMIN_ROLE, - Z_ADMIN - ); - expect(witnessCalculatedIndex).not.toBe(INIT_COUNTER); - } - - if (isValidPath) { - // Check path matches - const truePath = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.findPathForLeaf( - EXP_DEFAULT_ADMIN_COMMITMENT, - ); - const [, witnessCalculatedPath] = - shieldedAccessControl.witnesses.wit_getRoleCommitmentPath( - shieldedAccessControl.getWitnessContext(), - EXP_DEFAULT_ADMIN_COMMITMENT, - ); - expect(witnessCalculatedPath).toEqual(truePath); - } else { - // Check path does not match - const truePath = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.findPathForLeaf( - EXP_DEFAULT_ADMIN_COMMITMENT, - ); - shieldedAccessControl.overrideWitness( - 'wit_getRoleCommitmentPath', - RETURN_BAD_PATH, - ); - const [, witnessCalculatedPath] = - shieldedAccessControl.witnesses.wit_getRoleIndex( - shieldedAccessControl.getWitnessContext(), - DEFAULT_ADMIN_ROLE, - Z_ADMIN - ); - expect(witnessCalculatedPath).not.toEqual(truePath); - } - - // Test protected circuit - expect(() => { - ( - shieldedAccessControl[circuitName] as ( - ...args: unknown[] - ) => unknown - )(...args); - }).toThrow('ShieldedAccessControl: unauthorized account'); - }, - ); - }); - - describe('checked circuits should fail for unauthorized caller with any witness value', () => { - beforeEach(() => { - shieldedAccessControl._grantRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); - shieldedAccessControl.callerCtx.setCaller(UNAUTHORIZED); - }); - - type FailingCircuits = [ - method: keyof ShieldedAccessControlSimulator, - isValidNonce: boolean, - isValidIndex: boolean, - isValidPath: boolean, - args: unknown[], - ]; - const checkedCircuits: FailingCircuits[] = [ - ['assertOnlyRole', false, true, true, [DEFAULT_ADMIN_ROLE]], - ['assertOnlyRole', true, false, true, [DEFAULT_ADMIN_ROLE]], - ['assertOnlyRole', true, true, false, [DEFAULT_ADMIN_ROLE]], - ['assertOnlyRole', false, false, true, [DEFAULT_ADMIN_ROLE]], - ['assertOnlyRole', true, false, false, [DEFAULT_ADMIN_ROLE]], - ['assertOnlyRole', false, true, false, [DEFAULT_ADMIN_ROLE]], - ['assertOnlyRole', false, false, false, [DEFAULT_ADMIN_ROLE]], - ['assertOnlyRole', true, true, true, [DEFAULT_ADMIN_ROLE]], - ['grantRole', false, true, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ['grantRole', true, false, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ['grantRole', true, true, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ['grantRole', false, false, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ['grantRole', true, false, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ['grantRole', false, true, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ['grantRole', false, false, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ['grantRole', true, true, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ['revokeRole', false, true, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ['revokeRole', true, false, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ['revokeRole', true, true, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ['revokeRole', false, false, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ['revokeRole', true, false, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ['revokeRole', false, true, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ['revokeRole', false, false, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ['revokeRole', true, true, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ]; - - it.each(checkedCircuits)( - '%s should fail with isValidNonce(%s), isValidIndex(%s), isValidPath(%s)', - (circuitName, isValidNonce, isValidIndex, isValidPath, args) => { - if (isValidNonce) { - // Check nonce matches - expect( - shieldedAccessControl.privateState.getCurrentSecretNonce( - DEFAULT_ADMIN_ROLE, - ), - ).toEqual(ADMIN_SECRET_NONCE); - } else { - // Check nonce does not match - shieldedAccessControl.privateState.injectSecretNonce( - DEFAULT_ADMIN_ROLE, - BAD_NONCE, - ); - expect( - shieldedAccessControl.privateState.getCurrentSecretNonce( - DEFAULT_ADMIN_ROLE, - ), - ).not.toEqual(ADMIN_SECRET_NONCE); - } - - if (isValidIndex) { - // Check index matches - const [, witnessCalculatedIndex] = - shieldedAccessControl.witnesses.wit_getRoleIndex( - shieldedAccessControl.getWitnessContext(), - DEFAULT_ADMIN_ROLE, - Z_ADMIN - ); - expect(witnessCalculatedIndex).toBe(INIT_COUNTER); - } else { - // Check index does not match - shieldedAccessControl.overrideWitness( - 'wit_getRoleIndex', - RETURN_BAD_INDEX, - ); - const [, witnessCalculatedIndex] = - shieldedAccessControl.witnesses.wit_getRoleIndex( - shieldedAccessControl.getWitnessContext(), - DEFAULT_ADMIN_ROLE, - Z_ADMIN - ); - expect(witnessCalculatedIndex).not.toBe(INIT_COUNTER); - } - - if (isValidPath) { - // Check path matches - const truePath = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.findPathForLeaf( - EXP_DEFAULT_ADMIN_COMMITMENT, - ); - const [, witnessCalculatedPath] = - shieldedAccessControl.witnesses.wit_getRoleCommitmentPath( - shieldedAccessControl.getWitnessContext(), - EXP_DEFAULT_ADMIN_COMMITMENT, - ); - expect(witnessCalculatedPath).toEqual(truePath); - } else { - // Check path does not match - const truePath = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.findPathForLeaf( - EXP_DEFAULT_ADMIN_COMMITMENT, - ); - shieldedAccessControl.overrideWitness( - 'wit_getRoleCommitmentPath', - RETURN_BAD_PATH, - ); - const [, witnessCalculatedPath] = - shieldedAccessControl.witnesses.wit_getRoleIndex( - shieldedAccessControl.getWitnessContext(), - DEFAULT_ADMIN_ROLE, - Z_ADMIN - ); - expect(witnessCalculatedPath).not.toEqual(truePath); - } - - // Test protected circuit - expect(() => { - ( - shieldedAccessControl[circuitName] as ( - ...args: unknown[] - ) => unknown - )(...args); - }).toThrow('ShieldedAccessControl: unauthorized account'); - }, - ); - }); - - describe('unsupported contract address failure cases', () => { - beforeEach(() => { - shieldedAccessControl._grantRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); - shieldedAccessControl.callerCtx.setCaller(ADMIN); - }); - - type FailingCircuits = [ - method: keyof ShieldedAccessControlSimulator, - args: unknown[], - ]; - const circuitsWithContractAddressCheck: FailingCircuits[] = [ - ['hasRole', [DEFAULT_ADMIN_ROLE, Z_OPERATOR_CONTRACT]], - ['_checkRole', [DEFAULT_ADMIN_ROLE, Z_OPERATOR_CONTRACT]], - ['grantRole', [DEFAULT_ADMIN_ROLE, Z_OPERATOR_CONTRACT]], - ['revokeRole', [DEFAULT_ADMIN_ROLE, Z_OPERATOR_CONTRACT]], - ['_grantRole', [DEFAULT_ADMIN_ROLE, Z_OPERATOR_CONTRACT]], - ['_revokeRole', [DEFAULT_ADMIN_ROLE, Z_OPERATOR_CONTRACT]], - ]; - - it.each(circuitsWithContractAddressCheck)( - '%s fails if contract address is queried', - (circuitName, args) => { - // Test protected circuit - expect(() => { - ( - shieldedAccessControl[circuitName] as ( - ...args: unknown[] - ) => unknown - )(...args); - }).toThrow( - 'ShieldedAccessControl: contract address roles are not yet supported', - ); - }, - ); - }); - - describe('hasRole', () => { - beforeEach(() => { - shieldedAccessControl._grantRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); - }); - - type HasRoleTest = [ - isValidNonce: boolean, - isValidIndex: boolean, - isValidPath: boolean, - args: unknown[], - ]; - const falseCases: HasRoleTest[] = [ - [false, true, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - [true, false, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - [true, true, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - [false, false, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - [true, false, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - [false, true, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - [false, false, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ]; - - const commitmentDoesNotMatchCases: HasRoleTest[] = [ - [false, true, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - [true, false, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - [false, false, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - [true, false, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - [false, true, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - [false, false, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ]; - - it('should throw if caller is contract address', () => { - shieldedAccessControl.callerCtx.setCaller(OPERATOR_CONTRACT); - expect(() => { - shieldedAccessControl.hasRole(UNINITIALIZED_ROLE, Z_OPERATOR_CONTRACT); - }).toThrow( - 'ShieldedAccessControl: contract address roles are not yet supported', - ); - }); - - it('should return correct role commitment', () => { - const expCommitment = buildCommitment( - DEFAULT_ADMIN_ROLE, - Z_ADMIN, - ADMIN_SECRET_NONCE, - INIT_COUNTER, - ); - - const role = shieldedAccessControl.hasRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); - expect(role.roleCommitment).toEqual(expCommitment); - }); - - it('should return true when admin has role', () => { - const role = shieldedAccessControl.hasRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); - expect(role.isApproved).toEqual(true); - }); - - it('should return false when unauthorized does not have role', () => { - const role = shieldedAccessControl.hasRole( - DEFAULT_ADMIN_ROLE, - Z_UNAUTHORIZED, - ); - expect(role.isApproved).toEqual(false); - }); - - it('should return false when role does not exist', () => { - shieldedAccessControl.privateState.injectSecretNonce( - UNINITIALIZED_ROLE, - Buffer.alloc(32), - ); - const role = shieldedAccessControl.hasRole( - UNINITIALIZED_ROLE, - Z_UNAUTHORIZED, - ); - expect(role.isApproved).toBe(false); - }); - - it.each(falseCases)( - 'should return false with any invalid witness value - isValidNonce(%s), isValidIndex(%s), isValidPath(%s)', - (isValidNonce, isValidIndex, isValidPath, args) => { - if (isValidNonce) { - // Check nonce matches - expect( - shieldedAccessControl.privateState.getCurrentSecretNonce( - DEFAULT_ADMIN_ROLE, - ), - ).toEqual(ADMIN_SECRET_NONCE); - } else { - // Check nonce does not match - shieldedAccessControl.privateState.injectSecretNonce( - DEFAULT_ADMIN_ROLE, - BAD_NONCE, - ); - expect( - shieldedAccessControl.privateState.getCurrentSecretNonce( - DEFAULT_ADMIN_ROLE, - ), - ).not.toEqual(ADMIN_SECRET_NONCE); - } - - if (isValidIndex) { - // Check index matches - const [, witnessCalculatedIndex] = - shieldedAccessControl.witnesses.wit_getRoleIndex( - shieldedAccessControl.getWitnessContext(), - DEFAULT_ADMIN_ROLE, - Z_ADMIN - ); - expect(witnessCalculatedIndex).toBe(INIT_COUNTER); - } else { - // Check index does not match - shieldedAccessControl.overrideWitness( - 'wit_getRoleIndex', - RETURN_BAD_INDEX, - ); - const [, witnessCalculatedIndex] = - shieldedAccessControl.witnesses.wit_getRoleIndex( - shieldedAccessControl.getWitnessContext(), - DEFAULT_ADMIN_ROLE, - Z_ADMIN - ); - expect(witnessCalculatedIndex).not.toBe(INIT_COUNTER); - } - - if (isValidPath) { - // Check path matches - const truePath = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.findPathForLeaf( - EXP_DEFAULT_ADMIN_COMMITMENT, - ); - const [, witnessCalculatedPath] = - shieldedAccessControl.witnesses.wit_getRoleCommitmentPath( - shieldedAccessControl.getWitnessContext(), - EXP_DEFAULT_ADMIN_COMMITMENT, - ); - expect(witnessCalculatedPath).toEqual(truePath); - } else { - // Check path does not match - const truePath = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.findPathForLeaf( - EXP_DEFAULT_ADMIN_COMMITMENT, - ); - shieldedAccessControl.overrideWitness( - 'wit_getRoleCommitmentPath', - RETURN_BAD_PATH, - ); - const [, witnessCalculatedPath] = - shieldedAccessControl.witnesses.wit_getRoleIndex( - shieldedAccessControl.getWitnessContext(), - DEFAULT_ADMIN_ROLE, - Z_ADMIN - ); - expect(witnessCalculatedPath).not.toEqual(truePath); - } - - // Test false case circuit - const role = ( - shieldedAccessControl['hasRole'] as (...args: unknown[]) => Role - )(...args); - expect(role.isApproved).toBe(false); - }, - ); - - it.each(commitmentDoesNotMatchCases)( - 'commitment should not match with invalid nonce or index - isValidNonce(%s), isValidIndex(%s), isValidPath(%s)', - (isValidNonce, isValidIndex, isValidPath, args) => { - if (isValidNonce) { - // Check nonce matches - expect( - shieldedAccessControl.privateState.getCurrentSecretNonce( - DEFAULT_ADMIN_ROLE, - ), - ).toEqual(ADMIN_SECRET_NONCE); - } else { - // Check nonce does not match - shieldedAccessControl.privateState.injectSecretNonce( - DEFAULT_ADMIN_ROLE, - BAD_NONCE, - ); - expect( - shieldedAccessControl.privateState.getCurrentSecretNonce( - DEFAULT_ADMIN_ROLE, - ), - ).not.toEqual(ADMIN_SECRET_NONCE); - } - - if (isValidIndex) { - // Check index matches - const [, witnessCalculatedIndex] = - shieldedAccessControl.witnesses.wit_getRoleIndex( - shieldedAccessControl.getWitnessContext(), - DEFAULT_ADMIN_ROLE, - Z_ADMIN - ); - expect(witnessCalculatedIndex).toBe(INIT_COUNTER); - } else { - // Check index does not match - shieldedAccessControl.overrideWitness( - 'wit_getRoleIndex', - RETURN_BAD_INDEX, - ); - const [, witnessCalculatedIndex] = - shieldedAccessControl.witnesses.wit_getRoleIndex( - shieldedAccessControl.getWitnessContext(), - DEFAULT_ADMIN_ROLE, - Z_ADMIN - ); - expect(witnessCalculatedIndex).not.toBe(INIT_COUNTER); - } - - if (isValidPath) { - // Check path matches - const truePath = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.findPathForLeaf( - EXP_DEFAULT_ADMIN_COMMITMENT, - ); - const [, witnessCalculatedPath] = - shieldedAccessControl.witnesses.wit_getRoleCommitmentPath( - shieldedAccessControl.getWitnessContext(), - EXP_DEFAULT_ADMIN_COMMITMENT, - ); - expect(witnessCalculatedPath).toEqual(truePath); - } else { - // Check path does not match - const truePath = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.findPathForLeaf( - EXP_DEFAULT_ADMIN_COMMITMENT, - ); - shieldedAccessControl.overrideWitness( - 'wit_getRoleCommitmentPath', - RETURN_BAD_PATH, - ); - const [, witnessCalculatedPath] = - shieldedAccessControl.witnesses.wit_getRoleIndex( - shieldedAccessControl.getWitnessContext(), - DEFAULT_ADMIN_ROLE, - Z_ADMIN - ); - expect(witnessCalculatedPath).not.toEqual(truePath); - } - - // Test false case circuit - const role = ( - shieldedAccessControl['hasRole'] as (...args: unknown[]) => Role - )(...args); - expect(role.roleCommitment).not.toEqual(EXP_DEFAULT_ADMIN_COMMITMENT); - }, - ); - }); - - describe('assertOnlyRole', () => { - beforeEach(() => { - shieldedAccessControl._grantRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); - shieldedAccessControl.callerCtx.setCaller(ADMIN); - }); - - it('should not fail when authorized caller has correct nonce, index, and path', () => { - // Check nonce is correct - expect( - shieldedAccessControl.privateState.getCurrentSecretNonce( - DEFAULT_ADMIN_ROLE, - ), - ).toBe(ADMIN_SECRET_NONCE); - - // Check index matches - const [, witnessCalculatedIndex] = - shieldedAccessControl.witnesses.wit_getRoleIndex( - shieldedAccessControl.getWitnessContext(), - DEFAULT_ADMIN_ROLE, - Z_ADMIN - ); - expect(witnessCalculatedIndex).toBe(INIT_COUNTER); - - // Check path matches - const truePath = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.findPathForLeaf( - EXP_DEFAULT_ADMIN_COMMITMENT, - ); - const [, witnessCalculatedPath] = - shieldedAccessControl.witnesses.wit_getRoleCommitmentPath( - shieldedAccessControl.getWitnessContext(), - EXP_DEFAULT_ADMIN_COMMITMENT, - ); - expect(witnessCalculatedPath).toEqual(truePath); - - expect(() => - shieldedAccessControl.assertOnlyRole(DEFAULT_ADMIN_ROLE), - ).not.toThrow(); - }); - - it('should not fail for admin with multiple roles', () => { - shieldedAccessControl.privateState.injectSecretNonce( - OPERATOR_ROLE_1, - OPERATOR_ROLE_1_SECRET_NONCE, - ); - shieldedAccessControl.privateState.injectSecretNonce( - OPERATOR_ROLE_2, - OPERATOR_ROLE_2_SECRET_NONCE, - ); - shieldedAccessControl.privateState.injectSecretNonce( - OPERATOR_ROLE_3, - OPERATOR_ROLE_3_SECRET_NONCE, - ); - shieldedAccessControl._grantRole(OPERATOR_ROLE_1, Z_ADMIN); - shieldedAccessControl._grantRole(OPERATOR_ROLE_2, Z_ADMIN); - shieldedAccessControl._grantRole(OPERATOR_ROLE_3, Z_ADMIN); - expect(() => { - shieldedAccessControl.assertOnlyRole(DEFAULT_ADMIN_ROLE); - shieldedAccessControl.assertOnlyRole(OPERATOR_ROLE_1); - shieldedAccessControl.assertOnlyRole(OPERATOR_ROLE_2); - shieldedAccessControl.assertOnlyRole(OPERATOR_ROLE_3); - }).not.toThrow(); - }); - }); - - describe('_checkRole', () => { - beforeEach(() => { - shieldedAccessControl._grantRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); - shieldedAccessControl.callerCtx.setCaller(ADMIN); - }); - - it('should not throw if admin has role', () => { - expect(() => - shieldedAccessControl._checkRole(DEFAULT_ADMIN_ROLE, Z_ADMIN), - ).not.toThrow(); - }); - - it('should throw if unauthorized does not have role', () => { - expect(() => - shieldedAccessControl._checkRole(DEFAULT_ADMIN_ROLE, Z_UNAUTHORIZED), - ).toThrow('ShieldedAccessControl: unauthorized account'); - }); - }); - - describe('getRoleAdmin', () => { - it('should return default admin role if admin role not set', () => { - expect(shieldedAccessControl.getRoleAdmin(OPERATOR_ROLE_1)).toEqual( - DEFAULT_ADMIN_ROLE, - ); - }); - - it('should return custom admin role if set', () => { - shieldedAccessControl._setRoleAdmin(OPERATOR_ROLE_1, CUSTOM_ADMIN_ROLE); - expect(shieldedAccessControl.getRoleAdmin(OPERATOR_ROLE_1)).toEqual( - CUSTOM_ADMIN_ROLE, - ); - }); - }); - - describe('grantRole', () => { - beforeEach(() => { - shieldedAccessControl._grantRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); - shieldedAccessControl.callerCtx.setCaller(ADMIN); - }); - - it('admin should grant role', () => { - shieldedAccessControl.privateState.injectSecretNonce( - OPERATOR_ROLE_1, - OPERATOR_ROLE_1_SECRET_NONCE, - ); - shieldedAccessControl.grantRole(OPERATOR_ROLE_1, Z_OPERATOR_1); - const role: Role = shieldedAccessControl.hasRole( - OPERATOR_ROLE_1, - Z_OPERATOR_1, - ); - expect(role.isApproved).toBe(true); - }); - - it('path for role should exist in Merkle tree', () => { - expect( - shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.findPathForLeaf( - EXP_DEFAULT_ADMIN_COMMITMENT, - ), - ).toBeDefined(); - }); - - it('should update Merkle tree root', () => { - expect( - shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.root().field, - ).toBeGreaterThan(0n); - }); - - it('_currentMerkleTreeIndex should increment', () => { - // Starts at 1 because we grant role to self in beforeEach - expect( - shieldedAccessControl.getPublicState() - .ShieldedAccessControl__currentMerkleTreeIndex, - ).toBe(1n); - - shieldedAccessControl.privateState.injectSecretNonce( - OPERATOR_ROLE_1, - OPERATOR_ROLE_1_SECRET_NONCE, - ); - shieldedAccessControl.privateState.injectSecretNonce( - OPERATOR_ROLE_2, - OPERATOR_ROLE_2_SECRET_NONCE, - ); - shieldedAccessControl.privateState.injectSecretNonce( - OPERATOR_ROLE_3, - OPERATOR_ROLE_3_SECRET_NONCE, - ); - - shieldedAccessControl.grantRole(OPERATOR_ROLE_1, Z_OPERATOR_1); - expect( - shieldedAccessControl.getPublicState() - .ShieldedAccessControl__currentMerkleTreeIndex, - ).toBe(2n); - - shieldedAccessControl.grantRole(OPERATOR_ROLE_2, Z_OPERATOR_2); - expect( - shieldedAccessControl.getPublicState() - .ShieldedAccessControl__currentMerkleTreeIndex, - ).toBe(3n); - - shieldedAccessControl.grantRole(OPERATOR_ROLE_3, Z_OPERATOR_3); - expect( - shieldedAccessControl.getPublicState() - .ShieldedAccessControl__currentMerkleTreeIndex, - ).toBe(4n); - }); - - it('admin should grant multiple roles', () => { - for (let i = 0; i < OPERATOR_ROLE_LIST.length; i++) { - shieldedAccessControl.privateState.injectSecretNonce( - OPERATOR_ROLE_LIST[i], - OPERATOR_ROLE_SECRET_NONCES[i], - ); - for (let j = 0; j < Z_OPERATOR_LIST.length; j++) { - shieldedAccessControl.grantRole( - OPERATOR_ROLE_LIST[i], - Z_OPERATOR_LIST[j], - ); - const role: Role = shieldedAccessControl.hasRole( - OPERATOR_ROLE_LIST[i], - Z_OPERATOR_LIST[j], - ); - expect(role.isApproved).toBe(true); - - - expect( - shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.findPathForLeaf( - EXP_DEFAULT_ADMIN_COMMITMENT, - ), - ).toBeDefined(); - } - } - }); - - it('should throw if non-admin operator grants role', () => { - shieldedAccessControl.privateState.injectSecretNonce( - OPERATOR_ROLE_1, - OPERATOR_ROLE_1_SECRET_NONCE, - ); - shieldedAccessControl._grantRole(OPERATOR_ROLE_1, Z_OPERATOR_1); - - shieldedAccessControl.callerCtx.setCaller(OPERATOR_1); - expect(() => { - shieldedAccessControl.grantRole(OPERATOR_ROLE_1, Z_UNAUTHORIZED); - }).toThrow('ShieldedAccessControl: unauthorized account'); - }); - }); - - describe.only('revokeRole', () => { - beforeEach(() => { - console.log("TEST - Current MT Index", shieldedAccessControl.getPublicState().ShieldedAccessControl__currentMerkleTreeIndex.toString()); - shieldedAccessControl._grantRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); - console.log("TEST - Current MT Index", shieldedAccessControl.getPublicState().ShieldedAccessControl__currentMerkleTreeIndex.toString()); - shieldedAccessControl.privateState.injectSecretNonce( - OPERATOR_ROLE_1, - OPERATOR_ROLE_1_SECRET_NONCE, - ); - shieldedAccessControl._grantRole(OPERATOR_ROLE_1, Z_OPERATOR_1); - console.log("TEST - Current MT Index", shieldedAccessControl.getPublicState().ShieldedAccessControl__currentMerkleTreeIndex.toString()); - shieldedAccessControl.callerCtx.setCaller(ADMIN); - }); - - it('admin should revoke role', () => { - expect(shieldedAccessControl.hasRole(OPERATOR_ROLE_1, Z_OPERATOR_1).isApproved).toBe(true); - shieldedAccessControl.revokeRole(OPERATOR_ROLE_1, Z_OPERATOR_1); - expect(shieldedAccessControl.hasRole(OPERATOR_ROLE_1, Z_OPERATOR_1).isApproved).toBe(false); - }); - - it.only('commitment should be in nullifier set', () => { - console.log("TEST - Current MT Index", shieldedAccessControl.getPublicState().ShieldedAccessControl__currentMerkleTreeIndex.toString()); - const [, opRoleIndex] = shieldedAccessControl.witnesses.wit_getRoleIndex(shieldedAccessControl.getWitnessContext(), OPERATOR_ROLE_1, Z_OPERATOR_1); - const [, adminRoleIndex] = shieldedAccessControl.witnesses.wit_getRoleIndex(shieldedAccessControl.getWitnessContext(), DEFAULT_ADMIN_ROLE, Z_ADMIN); - console.log("OPERATOR INDEX ", opRoleIndex.toString(10)); - console.log("ADMIN INDEX ", adminRoleIndex.toString(10)); - const expCommitmentOp = buildCommitment(OPERATOR_ROLE_1, Z_OPERATOR_1, OPERATOR_ROLE_1_SECRET_NONCE, opRoleIndex); - const pathToOp = shieldedAccessControl.getPublicState().ShieldedAccessControl__operatorRoles.findPathForLeaf(expCommitmentOp); - const pathToAdmin = shieldedAccessControl.getPublicState().ShieldedAccessControl__operatorRoles.findPathForLeaf(EXP_DEFAULT_ADMIN_COMMITMENT); - //console.log("PATH TO OP ", pathToOp); - //console.log("PATH TO ADMIN ", pathToAdmin); - - //console.log("EXPECTED COMMITMENT ", expCommitmentOp); - const contractCommit = shieldedAccessControl.hasRole(OPERATOR_ROLE_1, Z_OPERATOR_1).roleCommitment; - //console.log("CONTRACT COMMITMENT ", contractCommit); - - shieldedAccessControl.revokeRole(OPERATOR_ROLE_1, Z_OPERATOR_1); - expect(shieldedAccessControl.getPublicState().ShieldedAccessControl__roleCommitmentNullifiers.isEmpty()).toBe(false); - expect(shieldedAccessControl.getPublicState().ShieldedAccessControl__roleCommitmentNullifiers.member(expCommitmentOp)).toBe(true); - }); - - it('admin should revoke multiple roles', () => { - const expCommitment = buildCommitment(OPERATOR_ROLE_1, Z_OPERATOR_1, OPERATOR_ROLE_1_SECRET_NONCE, 1n); - shieldedAccessControl.revokeRole(OPERATOR_ROLE_1, Z_OPERATOR_1); - expect(shieldedAccessControl.getPublicState().ShieldedAccessControl__roleCommitmentNullifiers.member(expCommitment)).toBe(true); - - for (let i = 1; i < OPERATOR_ROLE_LIST.length; i++) { - shieldedAccessControl.privateState.injectSecretNonce( - OPERATOR_ROLE_LIST[i], - OPERATOR_ROLE_SECRET_NONCES[i], - ); - for (let j = 1; j < Z_OPERATOR_LIST.length; j++) { - shieldedAccessControl._grantRole( - OPERATOR_ROLE_LIST[i], - Z_OPERATOR_LIST[j], - ); - const expCommitment = buildCommitment(OPERATOR_ROLE_LIST[i], Z_OPERATOR_LIST[j], OPERATOR_ROLE_SECRET_NONCES[i], BigInt(1 + i)); - shieldedAccessControl.revokeRole(OPERATOR_ROLE_LIST[i], Z_OPERATOR_LIST[j]); - expect(shieldedAccessControl.getPublicState().ShieldedAccessControl__roleCommitmentNullifiers.member(expCommitment)).toBe(true); - } - } - }); - - it('should throw if non-admin operator revokes role', () => { - shieldedAccessControl.privateState.injectSecretNonce( - OPERATOR_ROLE_1, - OPERATOR_ROLE_1_SECRET_NONCE, - ); - shieldedAccessControl._grantRole(OPERATOR_ROLE_1, Z_OPERATOR_1); - - shieldedAccessControl.callerCtx.setCaller(OPERATOR_1); - expect(() => { - shieldedAccessControl.revokeRole(OPERATOR_ROLE_1, Z_UNAUTHORIZED); - }).toThrow('ShieldedAccessControl: unauthorized account'); - }); - }); -}); diff --git a/contracts/src/access/test/ShieldedAccessControl_OLD.ts b/contracts/src/access/test/ShieldedAccessControl_OLD.ts new file mode 100644 index 00000000..ececb9aa --- /dev/null +++ b/contracts/src/access/test/ShieldedAccessControl_OLD.ts @@ -0,0 +1,1053 @@ +import { + CompactTypeBytes, + CompactTypeVector, + convert_bigint_to_Uint8Array, + persistentHash, + type WitnessContext, +} from '@midnight-ntwrk/compact-runtime'; +import { beforeEach, describe, expect, it } from 'vitest'; +import { + ContractAddress, + Either, + Ledger, + MerkleTreePath, + ShieldedAccessControl_Role as Role, + ZswapCoinPublicKey, + Contract as MyContract +} from '../../../artifacts/MockShieldedAccessControl/contract/index.cjs'; +import { fmtHexString, ShieldedAccessControlPrivateState, ShieldedAccessControlWitnesses } from '../witnesses/ShieldedAccessControlWitnesses.js'; +import { ShieldedAccessControlSimulator } from './simulators/ShieldedAccessControlSimulator.js'; +import * as utils from './utils/address.js'; + +// PKs +const [ADMIN, Z_ADMIN] = utils.generateEitherPubKeyPair('ADMIN'); +const [UNAUTHORIZED, Z_UNAUTHORIZED] = + utils.generateEitherPubKeyPair('UNAUTHORIZED'); +const [CUSTOM_ADMIN, Z_CUSTOM_ADMIN] = + utils.generateEitherPubKeyPair('CUSTOM_ADMIN'); +const [OPERATOR_1, Z_OPERATOR_1] = utils.generateEitherPubKeyPair('OPERATOR_1'); +const [OPERATOR_2, Z_OPERATOR_2] = utils.generateEitherPubKeyPair('OPERATOR_2'); +const [OPERATOR_3, Z_OPERATOR_3] = utils.generateEitherPubKeyPair('OPERATOR_3'); +const [OPERATOR_CONTRACT, Z_OPERATOR_CONTRACT] = utils.generateEitherPubKeyPair( + 'OPERATOR_CONTRACT', + false, +); +const Z_OPERATOR_LIST = [Z_OPERATOR_1, Z_OPERATOR_2, Z_OPERATOR_3]; + +// Constants +const BAD_NONCE = Buffer.alloc(32, 'BAD_NONCE'); +const DOMAIN = new Uint8Array(32); +new TextEncoder().encodeInto('ShieldedAccessControl:shield:', DOMAIN); +const INIT_COUNTER = 0n; + +const EMPTY_ROOT = { field: 0n }; +const getRoleIndex = ( + { + ledger, + privateState, + }: WitnessContext, + roleId: Uint8Array, + account: Either +): bigint => { + const roleIdString = Buffer.from(roleId).toString('hex'); + const bNonce = privateState.roles[roleIdString]; + const rt_type = new CompactTypeVector(5, new CompactTypeBytes(32)); + const bAccount = utils.eitherToBytes(account); + // Iterate over each MT index to determine if commitment exists + for (let i = 0; i < (2 ** 11 - 1); i++) { + const bIndex = convert_bigint_to_Uint8Array(32, BigInt(i)); + const commitment = persistentHash(rt_type, [ + roleId, + bAccount, + bNonce, + bIndex, + DOMAIN, + ]); + try { + ledger.ShieldedAccessControl__operatorRoles.pathForLeaf( + BigInt(i), + commitment, + ); + return BigInt(i); + } catch (e: unknown) { + if (e instanceof Error) { + const [msg, index] = e.message.split(':'); + if (msg === 'invalid index into sparse merkle tree') { + // console.log(`role ${fmtHexString(roleIdString)} with commitment ${fmtHexString(commitment)} not found at index ${index}`); + } else { + throw e; + } + } + } + } + + console.log("WIT - Commitment DNE, returing MT index ", ledger.ShieldedAccessControl__currentMerkleTreeIndex.toString()); + + // If commitment doesn't exist return currentMTIndex + // Used for adding roles + return ledger.ShieldedAccessControl__currentMerkleTreeIndex; +} + +// Roles +const DEFAULT_ADMIN_ROLE = utils.zeroUint8Array(); +const OPERATOR_ROLE_1 = convert_bigint_to_Uint8Array(32, 1n); +const OPERATOR_ROLE_2 = convert_bigint_to_Uint8Array(32, 2n); +const OPERATOR_ROLE_3 = convert_bigint_to_Uint8Array(32, 3n); +const CUSTOM_ADMIN_ROLE = convert_bigint_to_Uint8Array(32, 4n); +const UNINITIALIZED_ROLE = convert_bigint_to_Uint8Array(32, 5n); +const OPERATOR_ROLE_LIST = [OPERATOR_ROLE_1, OPERATOR_ROLE_2, OPERATOR_ROLE_3]; + +// Role to string +const DEFAULT_ADMIN_ROLE_TO_STRING = + Buffer.from(DEFAULT_ADMIN_ROLE).toString('hex'); + +const ADMIN_SECRET_NONCE = Buffer.alloc(32, 'ADMIN_SECRET_NONCE'); +const OPERATOR_ROLE_1_SECRET_NONCE = Buffer.alloc( + 32, + 'OPERATOR_ROLE_1_SECRET_NONCE', +); +const OPERATOR_ROLE_2_SECRET_NONCE = Buffer.alloc( + 32, + 'OPERATOR_ROLE_2_SECRET_NONCE', +); +const OPERATOR_ROLE_3_SECRET_NONCE = Buffer.alloc( + 32, + 'OPERATOR_ROLE_3_SECRET_NONCE', +); +const OPERATOR_ROLE_SECRET_NONCES = [ + OPERATOR_ROLE_1_SECRET_NONCE, + OPERATOR_ROLE_2_SECRET_NONCE, + OPERATOR_ROLE_3_SECRET_NONCE, +]; +let shieldedAccessControl: ShieldedAccessControlSimulator; + +// Helpers +const buildCommitment = ( + roleId: Uint8Array, + account: Either, + nonce: Uint8Array, + index: bigint, +): Uint8Array => { + const rt_type = new CompactTypeVector(5, new CompactTypeBytes(32)); + const bAccount = utils.eitherToBytes(account); + const bIndex = convert_bigint_to_Uint8Array(32, index); + + const commitment = persistentHash(rt_type, [ + roleId, + bAccount, + nonce, + bIndex, + DOMAIN, + ]); + + return commitment; +}; + +const EXP_DEFAULT_ADMIN_COMMITMENT = buildCommitment( + DEFAULT_ADMIN_ROLE, + Z_ADMIN, + ADMIN_SECRET_NONCE, + INIT_COUNTER, +); + +function RETURN_BAD_INDEX( + context: WitnessContext, + roleId: Uint8Array, +): [ShieldedAccessControlPrivateState, bigint] { + return [context.privateState, 1023n]; +} + +function RETURN_BAD_PATH( + context: WitnessContext, + roleCommitment: Uint8Array, +): [ShieldedAccessControlPrivateState, MerkleTreePath] { + const defaultPath: MerkleTreePath = { + leaf: new Uint8Array(32), + path: Array.from({ length: 10 }, () => ({ + sibling: { field: 0n }, + goes_left: false, + })), + }; + return [context.privateState, defaultPath]; +} + +type RoleAndNonce = { + roleId: string; + nonce: Buffer; +}; + +describe('ShieldedAccessControl', () => { + beforeEach(() => { + // Create private state object and generate nonce + const PS = ShieldedAccessControlPrivateState.withRoleAndNonce( + Z_ADMIN, + Buffer.from(DEFAULT_ADMIN_ROLE), + ADMIN_SECRET_NONCE, + ); + // Init contract for user with PS + shieldedAccessControl = new ShieldedAccessControlSimulator(Z_ADMIN, { + privateState: PS, + }); + }); + + describe('checked circuits should fail for authorized caller with invalid witness values', () => { + beforeEach(() => { + shieldedAccessControl._grantRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); + shieldedAccessControl.callerCtx.setCaller(ADMIN); + }); + + type FailingCircuits = [ + method: keyof ShieldedAccessControlSimulator, + isValidNonce: boolean, + isValidIndex: boolean, + isValidPath: boolean, + args: unknown[], + ]; + const checkedCircuits: FailingCircuits[] = [ + ['assertOnlyRole', false, true, true, [DEFAULT_ADMIN_ROLE]], + ['assertOnlyRole', true, false, true, [DEFAULT_ADMIN_ROLE]], + ['assertOnlyRole', true, true, false, [DEFAULT_ADMIN_ROLE]], + ['assertOnlyRole', false, false, true, [DEFAULT_ADMIN_ROLE]], + ['assertOnlyRole', true, false, false, [DEFAULT_ADMIN_ROLE]], + ['assertOnlyRole', false, true, false, [DEFAULT_ADMIN_ROLE]], + ['assertOnlyRole', false, false, false, [DEFAULT_ADMIN_ROLE]], + ['grantRole', false, true, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ['grantRole', true, false, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ['grantRole', true, true, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ['grantRole', false, false, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ['grantRole', true, false, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ['grantRole', false, true, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ['grantRole', false, false, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ['revokeRole', false, true, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ['revokeRole', true, false, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ['revokeRole', true, true, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ['revokeRole', false, false, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ['revokeRole', true, false, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ['revokeRole', false, true, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ['revokeRole', false, false, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ]; + + it.each(checkedCircuits)( + '%s should fail with isValidNonce(%s), isValidIndex(%s), isValidPath(%s)', + (circuitName, isValidNonce, isValidIndex, isValidPath, args) => { + if (isValidNonce) { + // Check nonce matches + expect( + shieldedAccessControl.privateState.getCurrentSecretNonce( + DEFAULT_ADMIN_ROLE, + ), + ).toEqual(ADMIN_SECRET_NONCE); + } else { + // Check nonce does not match + shieldedAccessControl.privateState.injectSecretNonce( + DEFAULT_ADMIN_ROLE, + BAD_NONCE, + ); + expect( + shieldedAccessControl.privateState.getCurrentSecretNonce( + DEFAULT_ADMIN_ROLE, + ), + ).not.toEqual(ADMIN_SECRET_NONCE); + } + + if (isValidIndex) { + // Check index matches + const [, witnessCalculatedIndex] = + shieldedAccessControl.witnesses.wit_getRoleIndex( + shieldedAccessControl.getWitnessContext(), + DEFAULT_ADMIN_ROLE, + Z_ADMIN + ); + expect(witnessCalculatedIndex).toBe(INIT_COUNTER); + } else { + // Check index does not match + shieldedAccessControl.overrideWitness( + 'wit_getRoleIndex', + RETURN_BAD_INDEX, + ); + const [, witnessCalculatedIndex] = + shieldedAccessControl.witnesses.wit_getRoleIndex( + shieldedAccessControl.getWitnessContext(), + DEFAULT_ADMIN_ROLE, + Z_ADMIN + ); + expect(witnessCalculatedIndex).not.toBe(INIT_COUNTER); + } + + if (isValidPath) { + // Check path matches + const truePath = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf( + EXP_DEFAULT_ADMIN_COMMITMENT, + ); + const [, witnessCalculatedPath] = + shieldedAccessControl.witnesses.wit_getRoleCommitmentPath( + shieldedAccessControl.getWitnessContext(), + EXP_DEFAULT_ADMIN_COMMITMENT, + ); + expect(witnessCalculatedPath).toEqual(truePath); + } else { + // Check path does not match + const truePath = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf( + EXP_DEFAULT_ADMIN_COMMITMENT, + ); + shieldedAccessControl.overrideWitness( + 'wit_getRoleCommitmentPath', + RETURN_BAD_PATH, + ); + const [, witnessCalculatedPath] = + shieldedAccessControl.witnesses.wit_getRoleIndex( + shieldedAccessControl.getWitnessContext(), + DEFAULT_ADMIN_ROLE, + Z_ADMIN + ); + expect(witnessCalculatedPath).not.toEqual(truePath); + } + + // Test protected circuit + expect(() => { + ( + shieldedAccessControl[circuitName] as ( + ...args: unknown[] + ) => unknown + )(...args); + }).toThrow('ShieldedAccessControl: unauthorized account'); + }, + ); + }); + + describe('checked circuits should fail for unauthorized caller with any witness value', () => { + beforeEach(() => { + shieldedAccessControl._grantRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); + shieldedAccessControl.callerCtx.setCaller(UNAUTHORIZED); + }); + + type FailingCircuits = [ + method: keyof ShieldedAccessControlSimulator, + isValidNonce: boolean, + isValidIndex: boolean, + isValidPath: boolean, + args: unknown[], + ]; + const checkedCircuits: FailingCircuits[] = [ + ['assertOnlyRole', false, true, true, [DEFAULT_ADMIN_ROLE]], + ['assertOnlyRole', true, false, true, [DEFAULT_ADMIN_ROLE]], + ['assertOnlyRole', true, true, false, [DEFAULT_ADMIN_ROLE]], + ['assertOnlyRole', false, false, true, [DEFAULT_ADMIN_ROLE]], + ['assertOnlyRole', true, false, false, [DEFAULT_ADMIN_ROLE]], + ['assertOnlyRole', false, true, false, [DEFAULT_ADMIN_ROLE]], + ['assertOnlyRole', false, false, false, [DEFAULT_ADMIN_ROLE]], + ['assertOnlyRole', true, true, true, [DEFAULT_ADMIN_ROLE]], + ['grantRole', false, true, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ['grantRole', true, false, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ['grantRole', true, true, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ['grantRole', false, false, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ['grantRole', true, false, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ['grantRole', false, true, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ['grantRole', false, false, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ['grantRole', true, true, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ['revokeRole', false, true, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ['revokeRole', true, false, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ['revokeRole', true, true, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ['revokeRole', false, false, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ['revokeRole', true, false, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ['revokeRole', false, true, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ['revokeRole', false, false, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ['revokeRole', true, true, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ]; + + it.each(checkedCircuits)( + '%s should fail with isValidNonce(%s), isValidIndex(%s), isValidPath(%s)', + (circuitName, isValidNonce, isValidIndex, isValidPath, args) => { + if (isValidNonce) { + // Check nonce matches + expect( + shieldedAccessControl.privateState.getCurrentSecretNonce( + DEFAULT_ADMIN_ROLE, + ), + ).toEqual(ADMIN_SECRET_NONCE); + } else { + // Check nonce does not match + shieldedAccessControl.privateState.injectSecretNonce( + DEFAULT_ADMIN_ROLE, + BAD_NONCE, + ); + expect( + shieldedAccessControl.privateState.getCurrentSecretNonce( + DEFAULT_ADMIN_ROLE, + ), + ).not.toEqual(ADMIN_SECRET_NONCE); + } + + if (isValidIndex) { + // Check index matches + const [, witnessCalculatedIndex] = + shieldedAccessControl.witnesses.wit_getRoleIndex( + shieldedAccessControl.getWitnessContext(), + DEFAULT_ADMIN_ROLE, + Z_ADMIN + ); + expect(witnessCalculatedIndex).toBe(INIT_COUNTER); + } else { + // Check index does not match + shieldedAccessControl.overrideWitness( + 'wit_getRoleIndex', + RETURN_BAD_INDEX, + ); + const [, witnessCalculatedIndex] = + shieldedAccessControl.witnesses.wit_getRoleIndex( + shieldedAccessControl.getWitnessContext(), + DEFAULT_ADMIN_ROLE, + Z_ADMIN + ); + expect(witnessCalculatedIndex).not.toBe(INIT_COUNTER); + } + + if (isValidPath) { + // Check path matches + const truePath = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf( + EXP_DEFAULT_ADMIN_COMMITMENT, + ); + const [, witnessCalculatedPath] = + shieldedAccessControl.witnesses.wit_getRoleCommitmentPath( + shieldedAccessControl.getWitnessContext(), + EXP_DEFAULT_ADMIN_COMMITMENT, + ); + expect(witnessCalculatedPath).toEqual(truePath); + } else { + // Check path does not match + const truePath = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf( + EXP_DEFAULT_ADMIN_COMMITMENT, + ); + shieldedAccessControl.overrideWitness( + 'wit_getRoleCommitmentPath', + RETURN_BAD_PATH, + ); + const [, witnessCalculatedPath] = + shieldedAccessControl.witnesses.wit_getRoleIndex( + shieldedAccessControl.getWitnessContext(), + DEFAULT_ADMIN_ROLE, + Z_ADMIN + ); + expect(witnessCalculatedPath).not.toEqual(truePath); + } + + // Test protected circuit + expect(() => { + ( + shieldedAccessControl[circuitName] as ( + ...args: unknown[] + ) => unknown + )(...args); + }).toThrow('ShieldedAccessControl: unauthorized account'); + }, + ); + }); + + describe('unsupported contract address failure cases', () => { + beforeEach(() => { + shieldedAccessControl._grantRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); + shieldedAccessControl.callerCtx.setCaller(ADMIN); + }); + + type FailingCircuits = [ + method: keyof ShieldedAccessControlSimulator, + args: unknown[], + ]; + const circuitsWithContractAddressCheck: FailingCircuits[] = [ + ['hasRole', [DEFAULT_ADMIN_ROLE, Z_OPERATOR_CONTRACT]], + ['_checkRole', [DEFAULT_ADMIN_ROLE, Z_OPERATOR_CONTRACT]], + ['grantRole', [DEFAULT_ADMIN_ROLE, Z_OPERATOR_CONTRACT]], + ['revokeRole', [DEFAULT_ADMIN_ROLE, Z_OPERATOR_CONTRACT]], + ['_grantRole', [DEFAULT_ADMIN_ROLE, Z_OPERATOR_CONTRACT]], + ['_revokeRole', [DEFAULT_ADMIN_ROLE, Z_OPERATOR_CONTRACT]], + ]; + + it.each(circuitsWithContractAddressCheck)( + '%s fails if contract address is queried', + (circuitName, args) => { + // Test protected circuit + expect(() => { + ( + shieldedAccessControl[circuitName] as ( + ...args: unknown[] + ) => unknown + )(...args); + }).toThrow( + 'ShieldedAccessControl: contract address roles are not yet supported', + ); + }, + ); + }); + + describe('hasRole', () => { + beforeEach(() => { + shieldedAccessControl._grantRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); + }); + + type HasRoleTest = [ + isValidNonce: boolean, + isValidIndex: boolean, + isValidPath: boolean, + args: unknown[], + ]; + const falseCases: HasRoleTest[] = [ + [false, true, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + [true, false, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + [true, true, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + [false, false, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + [true, false, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + [false, true, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + [false, false, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ]; + + const commitmentDoesNotMatchCases: HasRoleTest[] = [ + [false, true, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + [true, false, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + [false, false, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + [true, false, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + [false, true, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + [false, false, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], + ]; + + it('should throw if caller is contract address', () => { + shieldedAccessControl.callerCtx.setCaller(OPERATOR_CONTRACT); + expect(() => { + shieldedAccessControl.hasRole(UNINITIALIZED_ROLE, Z_OPERATOR_CONTRACT); + }).toThrow( + 'ShieldedAccessControl: contract address roles are not yet supported', + ); + }); + + it('should return correct role commitment', () => { + const expCommitment = buildCommitment( + DEFAULT_ADMIN_ROLE, + Z_ADMIN, + ADMIN_SECRET_NONCE, + INIT_COUNTER, + ); + + const role = shieldedAccessControl.hasRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); + expect(role.roleCommitment).toEqual(expCommitment); + }); + + it('should return true when admin has role', () => { + const role = shieldedAccessControl.hasRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); + expect(role.isApproved).toEqual(true); + }); + + it('should return false when unauthorized does not have role', () => { + const role = shieldedAccessControl.hasRole( + DEFAULT_ADMIN_ROLE, + Z_UNAUTHORIZED, + ); + expect(role.isApproved).toEqual(false); + }); + + it('should return false when role does not exist', () => { + shieldedAccessControl.privateState.injectSecretNonce( + UNINITIALIZED_ROLE, + Buffer.alloc(32), + ); + const role = shieldedAccessControl.hasRole( + UNINITIALIZED_ROLE, + Z_UNAUTHORIZED, + ); + expect(role.isApproved).toBe(false); + }); + + it.each(falseCases)( + 'should return false with any invalid witness value - isValidNonce(%s), isValidIndex(%s), isValidPath(%s)', + (isValidNonce, isValidIndex, isValidPath, args) => { + if (isValidNonce) { + // Check nonce matches + expect( + shieldedAccessControl.privateState.getCurrentSecretNonce( + DEFAULT_ADMIN_ROLE, + ), + ).toEqual(ADMIN_SECRET_NONCE); + } else { + // Check nonce does not match + shieldedAccessControl.privateState.injectSecretNonce( + DEFAULT_ADMIN_ROLE, + BAD_NONCE, + ); + expect( + shieldedAccessControl.privateState.getCurrentSecretNonce( + DEFAULT_ADMIN_ROLE, + ), + ).not.toEqual(ADMIN_SECRET_NONCE); + } + + if (isValidIndex) { + // Check index matches + const [, witnessCalculatedIndex] = + shieldedAccessControl.witnesses.wit_getRoleIndex( + shieldedAccessControl.getWitnessContext(), + DEFAULT_ADMIN_ROLE, + Z_ADMIN + ); + expect(witnessCalculatedIndex).toBe(INIT_COUNTER); + } else { + // Check index does not match + shieldedAccessControl.overrideWitness( + 'wit_getRoleIndex', + RETURN_BAD_INDEX, + ); + const [, witnessCalculatedIndex] = + shieldedAccessControl.witnesses.wit_getRoleIndex( + shieldedAccessControl.getWitnessContext(), + DEFAULT_ADMIN_ROLE, + Z_ADMIN + ); + expect(witnessCalculatedIndex).not.toBe(INIT_COUNTER); + } + + if (isValidPath) { + // Check path matches + const truePath = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf( + EXP_DEFAULT_ADMIN_COMMITMENT, + ); + const [, witnessCalculatedPath] = + shieldedAccessControl.witnesses.wit_getRoleCommitmentPath( + shieldedAccessControl.getWitnessContext(), + EXP_DEFAULT_ADMIN_COMMITMENT, + ); + expect(witnessCalculatedPath).toEqual(truePath); + } else { + // Check path does not match + const truePath = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf( + EXP_DEFAULT_ADMIN_COMMITMENT, + ); + shieldedAccessControl.overrideWitness( + 'wit_getRoleCommitmentPath', + RETURN_BAD_PATH, + ); + const [, witnessCalculatedPath] = + shieldedAccessControl.witnesses.wit_getRoleIndex( + shieldedAccessControl.getWitnessContext(), + DEFAULT_ADMIN_ROLE, + Z_ADMIN + ); + expect(witnessCalculatedPath).not.toEqual(truePath); + } + + // Test false case circuit + const role = ( + shieldedAccessControl['hasRole'] as (...args: unknown[]) => Role + )(...args); + expect(role.isApproved).toBe(false); + }, + ); + + it.each(commitmentDoesNotMatchCases)( + 'commitment should not match with invalid nonce or index - isValidNonce(%s), isValidIndex(%s), isValidPath(%s)', + (isValidNonce, isValidIndex, isValidPath, args) => { + if (isValidNonce) { + // Check nonce matches + expect( + shieldedAccessControl.privateState.getCurrentSecretNonce( + DEFAULT_ADMIN_ROLE, + ), + ).toEqual(ADMIN_SECRET_NONCE); + } else { + // Check nonce does not match + shieldedAccessControl.privateState.injectSecretNonce( + DEFAULT_ADMIN_ROLE, + BAD_NONCE, + ); + expect( + shieldedAccessControl.privateState.getCurrentSecretNonce( + DEFAULT_ADMIN_ROLE, + ), + ).not.toEqual(ADMIN_SECRET_NONCE); + } + + if (isValidIndex) { + // Check index matches + const [, witnessCalculatedIndex] = + shieldedAccessControl.witnesses.wit_getRoleIndex( + shieldedAccessControl.getWitnessContext(), + DEFAULT_ADMIN_ROLE, + Z_ADMIN + ); + expect(witnessCalculatedIndex).toBe(INIT_COUNTER); + } else { + // Check index does not match + shieldedAccessControl.overrideWitness( + 'wit_getRoleIndex', + RETURN_BAD_INDEX, + ); + const [, witnessCalculatedIndex] = + shieldedAccessControl.witnesses.wit_getRoleIndex( + shieldedAccessControl.getWitnessContext(), + DEFAULT_ADMIN_ROLE, + Z_ADMIN + ); + expect(witnessCalculatedIndex).not.toBe(INIT_COUNTER); + } + + if (isValidPath) { + // Check path matches + const truePath = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf( + EXP_DEFAULT_ADMIN_COMMITMENT, + ); + const [, witnessCalculatedPath] = + shieldedAccessControl.witnesses.wit_getRoleCommitmentPath( + shieldedAccessControl.getWitnessContext(), + EXP_DEFAULT_ADMIN_COMMITMENT, + ); + expect(witnessCalculatedPath).toEqual(truePath); + } else { + // Check path does not match + const truePath = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf( + EXP_DEFAULT_ADMIN_COMMITMENT, + ); + shieldedAccessControl.overrideWitness( + 'wit_getRoleCommitmentPath', + RETURN_BAD_PATH, + ); + const [, witnessCalculatedPath] = + shieldedAccessControl.witnesses.wit_getRoleIndex( + shieldedAccessControl.getWitnessContext(), + DEFAULT_ADMIN_ROLE, + Z_ADMIN + ); + expect(witnessCalculatedPath).not.toEqual(truePath); + } + + // Test false case circuit + const role = ( + shieldedAccessControl['hasRole'] as (...args: unknown[]) => Role + )(...args); + expect(role.roleCommitment).not.toEqual(EXP_DEFAULT_ADMIN_COMMITMENT); + }, + ); + }); + + describe('assertOnlyRole', () => { + beforeEach(() => { + shieldedAccessControl._grantRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); + shieldedAccessControl.callerCtx.setCaller(ADMIN); + }); + + it('should not fail when authorized caller has correct nonce, index, and path', () => { + shieldedAccessControl.callerCtx.setCaller(OPERATOR_1); + shieldedAccessControl.assertOnlyRole(new Uint8Array(32).fill(1)); + // Check nonce is correct + expect( + shieldedAccessControl.privateState.getCurrentSecretNonce( + DEFAULT_ADMIN_ROLE, + ), + ).toBe(ADMIN_SECRET_NONCE); + + // Check index matches + const [, witnessCalculatedIndex] = + shieldedAccessControl.witnesses.wit_getRoleIndex( + shieldedAccessControl.getWitnessContext(), + DEFAULT_ADMIN_ROLE, + Z_ADMIN + ); + expect(witnessCalculatedIndex).toBe(INIT_COUNTER); + + // Check path matches + const truePath = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf( + EXP_DEFAULT_ADMIN_COMMITMENT, + ); + const [, witnessCalculatedPath] = + shieldedAccessControl.witnesses.wit_getRoleCommitmentPath( + shieldedAccessControl.getWitnessContext(), + EXP_DEFAULT_ADMIN_COMMITMENT, + ); + expect(witnessCalculatedPath).toEqual(truePath); + + expect(() => + shieldedAccessControl.assertOnlyRole(DEFAULT_ADMIN_ROLE), + ).not.toThrow(); + }); + + it('should not fail for admin with multiple roles', () => { + shieldedAccessControl.privateState.injectSecretNonce( + OPERATOR_ROLE_1, + OPERATOR_ROLE_1_SECRET_NONCE, + ); + shieldedAccessControl.privateState.injectSecretNonce( + OPERATOR_ROLE_2, + OPERATOR_ROLE_2_SECRET_NONCE, + ); + shieldedAccessControl.privateState.injectSecretNonce( + OPERATOR_ROLE_3, + OPERATOR_ROLE_3_SECRET_NONCE, + ); + shieldedAccessControl._grantRole(OPERATOR_ROLE_1, Z_ADMIN); + shieldedAccessControl._grantRole(OPERATOR_ROLE_2, Z_ADMIN); + shieldedAccessControl._grantRole(OPERATOR_ROLE_3, Z_ADMIN); + expect(() => { + shieldedAccessControl.assertOnlyRole(DEFAULT_ADMIN_ROLE); + shieldedAccessControl.assertOnlyRole(OPERATOR_ROLE_1); + shieldedAccessControl.assertOnlyRole(OPERATOR_ROLE_2); + shieldedAccessControl.assertOnlyRole(OPERATOR_ROLE_3); + }).not.toThrow(); + }); + }); + + describe('_checkRole', () => { + beforeEach(() => { + shieldedAccessControl._grantRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); + }); + + it('should not throw if admin has role', () => { + shieldedAccessControl.callerCtx.setCaller(OPERATOR_1); + console.log("ZswapState", shieldedAccessControl.circuitContext.currentZswapLocalState) + expect(() => + shieldedAccessControl._checkRole(DEFAULT_ADMIN_ROLE, Z_ADMIN), + ).not.toThrow(); + }); + + it('should throw if unauthorized does not have role', () => { + expect(() => + shieldedAccessControl._checkRole(DEFAULT_ADMIN_ROLE, Z_UNAUTHORIZED), + ).toThrow('ShieldedAccessControl: unauthorized account'); + }); + }); + + describe('getRoleAdmin', () => { + it('should return default admin role if admin role not set', () => { + expect(shieldedAccessControl.getRoleAdmin(OPERATOR_ROLE_1)).toEqual( + DEFAULT_ADMIN_ROLE, + ); + }); + + it('should return custom admin role if set', () => { + shieldedAccessControl._setRoleAdmin(OPERATOR_ROLE_1, CUSTOM_ADMIN_ROLE); + expect(shieldedAccessControl.getRoleAdmin(OPERATOR_ROLE_1)).toEqual( + CUSTOM_ADMIN_ROLE, + ); + }); + }); + + describe('grantRole', () => { + beforeEach(() => { + shieldedAccessControl._grantRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); + shieldedAccessControl.callerCtx.setCaller(ADMIN); + }); + + it('admin should grant role', () => { + shieldedAccessControl.privateState.injectSecretNonce( + OPERATOR_ROLE_1, + OPERATOR_ROLE_1_SECRET_NONCE, + ); + shieldedAccessControl.grantRole(OPERATOR_ROLE_1, Z_OPERATOR_1); + const role: Role = shieldedAccessControl.hasRole( + OPERATOR_ROLE_1, + Z_OPERATOR_1, + ); + expect(role.isApproved).toBe(true); + }); + + it('path for role should exist in Merkle tree', () => { + expect( + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf( + EXP_DEFAULT_ADMIN_COMMITMENT, + ), + ).toBeDefined(); + }); + + it('should update Merkle tree root', () => { + expect( + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.root().field, + ).toBeGreaterThan(0n); + }); + + it('_currentMerkleTreeIndex should increment', () => { + // Starts at 1 because we grant role to self in beforeEach + expect( + shieldedAccessControl.getPublicState() + .ShieldedAccessControl__currentMerkleTreeIndex, + ).toBe(1n); + + shieldedAccessControl.privateState.injectSecretNonce( + OPERATOR_ROLE_1, + OPERATOR_ROLE_1_SECRET_NONCE, + ); + shieldedAccessControl.privateState.injectSecretNonce( + OPERATOR_ROLE_2, + OPERATOR_ROLE_2_SECRET_NONCE, + ); + shieldedAccessControl.privateState.injectSecretNonce( + OPERATOR_ROLE_3, + OPERATOR_ROLE_3_SECRET_NONCE, + ); + + shieldedAccessControl.grantRole(OPERATOR_ROLE_1, Z_OPERATOR_1); + expect( + shieldedAccessControl.getPublicState() + .ShieldedAccessControl__currentMerkleTreeIndex, + ).toBe(2n); + + shieldedAccessControl.grantRole(OPERATOR_ROLE_2, Z_OPERATOR_2); + expect( + shieldedAccessControl.getPublicState() + .ShieldedAccessControl__currentMerkleTreeIndex, + ).toBe(3n); + + shieldedAccessControl.grantRole(OPERATOR_ROLE_3, Z_OPERATOR_3); + expect( + shieldedAccessControl.getPublicState() + .ShieldedAccessControl__currentMerkleTreeIndex, + ).toBe(4n); + }); + + it('admin should grant multiple roles', () => { + for (let i = 0; i < OPERATOR_ROLE_LIST.length; i++) { + shieldedAccessControl.privateState.injectSecretNonce( + OPERATOR_ROLE_LIST[i], + OPERATOR_ROLE_SECRET_NONCES[i], + ); + for (let j = 0; j < Z_OPERATOR_LIST.length; j++) { + shieldedAccessControl.grantRole( + OPERATOR_ROLE_LIST[i], + Z_OPERATOR_LIST[j], + ); + const role: Role = shieldedAccessControl.hasRole( + OPERATOR_ROLE_LIST[i], + Z_OPERATOR_LIST[j], + ); + expect(role.isApproved).toBe(true); + + + expect( + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf( + EXP_DEFAULT_ADMIN_COMMITMENT, + ), + ).toBeDefined(); + } + } + }); + + it('should throw if non-admin operator grants role', () => { + shieldedAccessControl.privateState.injectSecretNonce( + OPERATOR_ROLE_1, + OPERATOR_ROLE_1_SECRET_NONCE, + ); + shieldedAccessControl._grantRole(OPERATOR_ROLE_1, Z_OPERATOR_1); + + shieldedAccessControl.callerCtx.setCaller(OPERATOR_1); + expect(() => { + shieldedAccessControl.grantRole(OPERATOR_ROLE_1, Z_UNAUTHORIZED); + }).toThrow('ShieldedAccessControl: unauthorized account'); + }); + }); + + describe('revokeRole', () => { + beforeEach(() => { + shieldedAccessControl.callerCtx.setCaller(ADMIN); + console.log("TEST - Current MT Index", shieldedAccessControl.getPublicState().ShieldedAccessControl__currentMerkleTreeIndex.toString()); + console.log(shieldedAccessControl._grantRole(DEFAULT_ADMIN_ROLE, Z_ADMIN)); + console.log(shieldedAccessControl._grantRole(DEFAULT_ADMIN_ROLE, Z_ADMIN)); + console.log("TEST - ADMIN NONCE ", fmtHexString(ADMIN_SECRET_NONCE)); + console.log("TEST - OP NONCE ", fmtHexString(OPERATOR_ROLE_1_SECRET_NONCE)); + console.log("TEST - Current MT Index", shieldedAccessControl.getPublicState().ShieldedAccessControl__currentMerkleTreeIndex.toString()); + shieldedAccessControl.privateState.injectSecretNonce( + OPERATOR_ROLE_1, + OPERATOR_ROLE_1_SECRET_NONCE, + ); + shieldedAccessControl.grantRole(OPERATOR_ROLE_1, Z_OPERATOR_1); + console.log("TEST - Current MT Index", shieldedAccessControl.getPublicState().ShieldedAccessControl__currentMerkleTreeIndex.toString()); + + }); + + it('admin should revoke role', () => { + expect(shieldedAccessControl.hasRole(OPERATOR_ROLE_1, Z_OPERATOR_1).isApproved).toBe(true); + shieldedAccessControl.revokeRole(OPERATOR_ROLE_1, Z_OPERATOR_1); + expect(shieldedAccessControl.hasRole(OPERATOR_ROLE_1, Z_OPERATOR_1).isApproved).toBe(false); + }); + + it('commitment should be in nullifier set', () => { + console.log("TEST - Current MT Index", shieldedAccessControl.getPublicState().ShieldedAccessControl__currentMerkleTreeIndex.toString()); + const opRoleIndex = getRoleIndex({ ledger: shieldedAccessControl.getPublicState(), privateState: shieldedAccessControl.getPrivateState(), contractAddress: shieldedAccessControl.contractAddress }, OPERATOR_ROLE_1, Z_OPERATOR_1); + const adminRoleIndex = getRoleIndex(shieldedAccessControl.getWitnessContext(), DEFAULT_ADMIN_ROLE, Z_ADMIN); + console.log("OPERATOR INDEX ", opRoleIndex.toString(10)); + console.log("ADMIN INDEX ", adminRoleIndex.toString(10)); + const expCommitmentOp = buildCommitment(OPERATOR_ROLE_1, Z_OPERATOR_1, OPERATOR_ROLE_1_SECRET_NONCE, 0n); + const expCommitmentOp2 = buildCommitment(OPERATOR_ROLE_1, Z_OPERATOR_1, OPERATOR_ROLE_1_SECRET_NONCE, 0n); + const pathToOp = shieldedAccessControl.getPublicState().ShieldedAccessControl__operatorRoles.findPathForLeaf(expCommitmentOp); + const pathToAdmin = shieldedAccessControl.getPublicState().ShieldedAccessControl__operatorRoles.findPathForLeaf(EXP_DEFAULT_ADMIN_COMMITMENT); + //console.log("PATH TO OP ", pathToOp); + //console.log("PATH TO ADMIN ", pathToAdmin); + + //console.log("EXPECTED COMMITMENT ", expCommitmentOp); + const contractCommit = shieldedAccessControl.hasRole(OPERATOR_ROLE_1, Z_OPERATOR_1).roleCommitment; + //console.log("CONTRACT COMMITMENT ", contractCommit); + + shieldedAccessControl.revokeRole(OPERATOR_ROLE_1, Z_OPERATOR_1); + const it = shieldedAccessControl.getPublicState().ShieldedAccessControl_sanity[Symbol.iterator](); + console.log(EXP_DEFAULT_ADMIN_COMMITMENT); + console.log(it.next()); + console.log(shieldedAccessControl.getPublicState().ShieldedAccessControl_sanity.member(EXP_DEFAULT_ADMIN_COMMITMENT)); + console.log(expCommitmentOp) + console.log(it.next()); + console.log(shieldedAccessControl.getPublicState().ShieldedAccessControl_sanity.member(expCommitmentOp)); + expect(shieldedAccessControl.getPublicState().ShieldedAccessControl__roleCommitmentNullifiers.isEmpty()).toBe(false); + expect(shieldedAccessControl.getPublicState().ShieldedAccessControl__roleCommitmentNullifiers.member(expCommitmentOp)).toBe(true); + }); + + it('admin should revoke multiple roles', () => { + const expCommitment = buildCommitment(OPERATOR_ROLE_1, Z_OPERATOR_1, OPERATOR_ROLE_1_SECRET_NONCE, 1n); + shieldedAccessControl.revokeRole(OPERATOR_ROLE_1, Z_OPERATOR_1); + expect(shieldedAccessControl.getPublicState().ShieldedAccessControl__roleCommitmentNullifiers.member(expCommitment)).toBe(true); + + for (let i = 1; i < OPERATOR_ROLE_LIST.length; i++) { + shieldedAccessControl.privateState.injectSecretNonce( + OPERATOR_ROLE_LIST[i], + OPERATOR_ROLE_SECRET_NONCES[i], + ); + for (let j = 1; j < Z_OPERATOR_LIST.length; j++) { + shieldedAccessControl._grantRole( + OPERATOR_ROLE_LIST[i], + Z_OPERATOR_LIST[j], + ); + const expCommitment = buildCommitment(OPERATOR_ROLE_LIST[i], Z_OPERATOR_LIST[j], OPERATOR_ROLE_SECRET_NONCES[i], BigInt(1 + i)); + shieldedAccessControl.revokeRole(OPERATOR_ROLE_LIST[i], Z_OPERATOR_LIST[j]); + expect(shieldedAccessControl.getPublicState().ShieldedAccessControl__roleCommitmentNullifiers.member(expCommitment)).toBe(true); + } + } + }); + + it('should throw if non-admin operator revokes role', () => { + shieldedAccessControl.privateState.injectSecretNonce( + OPERATOR_ROLE_1, + OPERATOR_ROLE_1_SECRET_NONCE, + ); + shieldedAccessControl._grantRole(OPERATOR_ROLE_1, Z_OPERATOR_1); + + shieldedAccessControl.callerCtx.setCaller(OPERATOR_1); + expect(() => { + shieldedAccessControl.revokeRole(OPERATOR_ROLE_1, Z_UNAUTHORIZED); + }).toThrow('ShieldedAccessControl: unauthorized account'); + }); + }); +}); From 2fb86e7d930d9ec09392a6659c7ca63a3a4e373a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Wed, 17 Sep 2025 17:24:27 -0400 Subject: [PATCH 199/322] Fix compiler errors, refactor mock and witnesses --- .../src/access/ShieldedAccessControl.compact | 52 +++++++++++++------ .../mocks/MockShieldedAccessControl.compact | 49 ++++++++++++----- .../ShieldedAccessControlWitnesses.ts | 31 ++++++----- 3 files changed, 88 insertions(+), 44 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index 7700e0b0..fff6831c 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -121,6 +121,7 @@ module ShieldedAccessControl { export struct Role { isApproved: Boolean; roleCommitment: Bytes<32>; + commitmentNullifier: Bytes<32>; } /** @@ -155,7 +156,7 @@ module ShieldedAccessControl { * after every transfer to prevent duplicate commitments given the same `id`. * @returns {Bytes<32>} The commitment derived from `id` and `counter`. */ - export circuit _computeRoleCommitment( + export pure circuit _computeRoleCommitment( accountId: Bytes<32>, roleId: Bytes<32>, index: Uint<64>, @@ -165,11 +166,15 @@ module ShieldedAccessControl { accountId, roleId, index as Field as Bytes<32>, - pad(32, "ShieldedAccessControl:shield:") + pad(32, "ShieldedAccessControl:commitment") ] ); } + export pure circuit _computeNullifier(roleCommitment: Bytes<32>): Bytes<32> { + return persistentHash>>([roleCommitment, pad(32, "ShieldedAccessControl:nullifier")]); + } + /** * @description Computes the unique identifier (`id`) of the owner from their * public key and a secret nonce. @@ -239,15 +244,18 @@ module ShieldedAccessControl { * @return {Boolean} - A boolean determining if the account has the specified role.  */ export circuit callerHasRole(roleId: Bytes<32>): Role { - const account = ownPublicKey(); - + const callerAsEither = Either { + is_left: true, + left: ownPublicKey(), + right: ContractAddress { bytes: pad(32, "") } + }; const nonce = wit_secretNonce(roleId); - const accountId = _computeRoleId(account, nonce); + const accountId = _computeRoleId(callerAsEither, nonce); return getRole(roleId, accountId); } - export hasRole(roleId: Bytes<32>, accountId: Bytes<32>): Boolean { - const roleInfo = getRoleInfo(); + export circuit hasRole(roleId: Bytes<32>, accountId: Bytes<32>): Boolean { + const roleInfo = getRole(roleId, accountId); return roleInfo.isApproved; } @@ -286,14 +294,23 @@ module ShieldedAccessControl { export circuit getRole(roleId: Bytes<32>, accountId: Bytes<32>): Role { const index = wit_getRoleIndex(roleId, accountId); const commitment = _computeRoleCommitment(accountId, roleId, index); + const commitmentNullifier = _computeNullifier(commitment); const authPath = wit_getRoleCommitmentPath(commitment); const rootMatches = _operatorRoles .checkRoot(merkleTreePathRoot<10, Bytes<32>>(disclose(authPath))); - if(!_roleCommitmentNullifiers.member(disclose(commitment)) && rootMatches) { - return Role {isApproved: true, roleCommitment: disclose(commitment)}; + if(!_roleCommitmentNullifiers.member(disclose(commitmentNullifier)) && rootMatches) { + return Role { + isApproved: true, + roleCommitment: disclose(commitment), + commitmentNullifier: disclose(commitmentNullifier) + }; } else { - return Role {isApproved: false, roleCommitment: disclose(commitment)}; + return Role { + isApproved: false, + roleCommitment: disclose(commitment), + commitmentNullifier: disclose(commitmentNullifier) + }; } } @@ -414,11 +431,16 @@ module ShieldedAccessControl { * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK) * @return {[]} - Empty tuple. */ - export circuit renounceRole(roleId: Bytes<32>, accountId: Bytes<32>): [] { + export circuit renounceRole(roleId: Bytes<32>, callerConfirmation: Bytes<32>): [] { const nonce = wit_secretNonce(roleId); - assert(accountId, _computeRoleId(ownPublicKey(), nonce), "ShieldedAccessControl: bad confirmation"); + const callerAsEither = Either { + is_left: true, + left: ownPublicKey(), + right: ContractAddress { bytes: pad(32, "") } + }; + assert(callerConfirmation == _computeRoleId(callerAsEither, nonce), "ShieldedAccessControl: bad confirmation"); - _revokeRole(roleId, accountId); + _revokeRole(roleId, callerConfirmation); } /** @@ -504,12 +526,12 @@ module ShieldedAccessControl { * @return {Boolean} roleRevoked - A boolean indicating if `roleId` was revoked. */ export circuit _revokeRole(roleId: Bytes<32>, accountId: Bytes<32>): Boolean { - const role = getRole(roleId, account); + const role = getRole(roleId, accountId); if (!role.isApproved) { return false; } - _roleCommitmentNullifiers.insert(disclose(role.roleCommitment)); + _roleCommitmentNullifiers.insert(disclose(role.commitmentNullifier)); return true; } } diff --git a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact index 59c24a52..05c17395 100644 --- a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact +++ b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact @@ -19,31 +19,54 @@ export { ShieldedAccessControl_Role }; -export circuit hasRole(roleId: Bytes<32>, account: Either): ShieldedAccessControl_Role { - return ShieldedAccessControl_hasRole(roleId, account); +export pure circuit _computeRoleCommitment( + accountId: Bytes<32>, + roleId: Bytes<32>, + index: Uint<64>, +): Bytes<32> { + return ShieldedAccessControl__computeRoleCommitment(accountId, roleId, index); +} + +export pure circuit _computeRoleId( + pk: Either, + nonce: Bytes<32> +): Bytes<32> { + return ShieldedAccessControl__computeRoleId(pk, nonce); +} + +export pure circuit _computeNullifier(commitment: Bytes<32>): Bytes<32> { + return ShieldedAccessControl__computeNullifier(commitment); +} + +export circuit callerHasRole(roleId: Bytes<32>): ShieldedAccessControl_Role { + return ShieldedAccessControl_callerHasRole(roleId); +} + +export circuit hasRole(roleId: Bytes<32>, accountId: Bytes<32>): Boolean { + return ShieldedAccessControl_hasRole(roleId, accountId); } export circuit assertOnlyRole(roleId: Bytes<32>): [] { ShieldedAccessControl_assertOnlyRole(roleId); } -export circuit _checkRole(roleId: Bytes<32>, account: Either): [] { - ShieldedAccessControl__checkRole(roleId, account); +export circuit getRole(roleId: Bytes<32>, accountId: Bytes<32>): ShieldedAccessControl_Role { + return ShieldedAccessControl_getRole(roleId, accountId); } export circuit getRoleAdmin(roleId: Bytes<32>): Bytes<32> { return ShieldedAccessControl_getRoleAdmin(roleId); } -export circuit grantRole(roleId: Bytes<32>, account: Either): [] { - ShieldedAccessControl_grantRole(roleId, account); +export circuit grantRole(roleId: Bytes<32>, accountId: Bytes<32>): [] { + ShieldedAccessControl_grantRole(roleId, accountId); } -export circuit revokeRole(roleId: Bytes<32>, account: Either): [] { - ShieldedAccessControl_revokeRole(roleId, account); +export circuit revokeRole(roleId: Bytes<32>, accountId: Bytes<32>): [] { + ShieldedAccessControl_revokeRole(roleId, accountId); } -export circuit renounceRole(roleId: Bytes<32>, callerConfirmation: Either): [] { +export circuit renounceRole(roleId: Bytes<32>, callerConfirmation: Bytes<32>): [] { ShieldedAccessControl_renounceRole(roleId, callerConfirmation); } @@ -51,10 +74,10 @@ export circuit _setRoleAdmin(roleId: Bytes<32>, adminRole: Bytes<32>): [] { ShieldedAccessControl__setRoleAdmin(roleId, adminRole); } -export circuit _grantRole(roleId: Bytes<32>, account: Either): Boolean { - return ShieldedAccessControl__grantRole(roleId, account); +export circuit _grantRole(roleId: Bytes<32>, accountId: Bytes<32>): Boolean { + return ShieldedAccessControl__grantRole(roleId, accountId); } -export circuit _revokeRole(roleId: Bytes<32>, account: Either): Boolean { - return ShieldedAccessControl__revokeRole(roleId, account); +export circuit _revokeRole(roleId: Bytes<32>, accountId: Bytes<32>): Boolean { + return ShieldedAccessControl__revokeRole(roleId, accountId); } \ No newline at end of file diff --git a/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts b/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts index 36bf6ac2..f5266101 100644 --- a/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts +++ b/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts @@ -27,6 +27,12 @@ export function fmtHexString(bytes: string | Uint8Array): string { } } +export function createAccountId(account: Either, secretNonce: Uint8Array): Uint8Array { + const rt_type = new CompactTypeVector(2, new CompactTypeBytes(32)); + const bAccount = eitherToBytes(account); + return persistentHash(rt_type, [secretNonce, bAccount]); +} + /** * @description Interface defining the witness methods for ShieldedAccessControl operations. * @template P - The private state type. @@ -48,7 +54,7 @@ export interface IShieldedAccessControlWitnesses

{ wit_getRoleIndex( context: WitnessContext, roleId: Uint8Array, - account: Either + accountId: Uint8Array ): [P, bigint]; } @@ -61,7 +67,6 @@ type SecretNonce = Uint8Array; export type ShieldedAccessControlPrivateState = { /** @description A 32-byte secret nonce used as a privacy additive. */ roles: Record; - account: Either; }; /** @@ -73,14 +78,14 @@ export const ShieldedAccessControlPrivateState = { * @returns A fresh ShieldedAccessControlPrivateState instance. */ generate: ( - account: Either, ): ShieldedAccessControlPrivateState => { const defaultRoleId: string = Buffer.alloc(32).toString('hex'); + const secretNonce = new Uint8Array(getRandomValues(Buffer.alloc(32))); + const privateState: ShieldedAccessControlPrivateState = { roles: {}, - account, }; - privateState.roles[defaultRoleId] = getRandomValues(Buffer.alloc(32)); + privateState.roles[defaultRoleId] = secretNonce; return privateState; }, @@ -99,14 +104,12 @@ export const ShieldedAccessControlPrivateState = { * ``` */ withRoleAndNonce: ( - account: Either, roleId: Buffer, nonce: Buffer, ): ShieldedAccessControlPrivateState => { const roleString = roleId.toString('hex'); const privateState: ShieldedAccessControlPrivateState = { roles: {}, - account, }; privateState.roles[roleString] = nonce; return privateState; @@ -147,19 +150,15 @@ export const ShieldedAccessControlPrivateState = { privateState, }: WitnessContext, roleId: Uint8Array, - account: Either + accountId: Uint8Array ): bigint => { - const roleIdString = Buffer.from(roleId).toString('hex'); - const bNonce = privateState.roles[roleIdString]; - const rt_type = new CompactTypeVector(5, new CompactTypeBytes(32)); - const bAccount = eitherToBytes(account); + const rt_type = new CompactTypeVector(4, new CompactTypeBytes(32)); // Iterate over each MT index to determine if commitment exists for (let i = 0; i <= ledger.ShieldedAccessControl__currentMerkleTreeIndex; i++) { const bIndex = convert_bigint_to_Uint8Array(32, BigInt(i)); const commitment = persistentHash(rt_type, [ + accountId, roleId, - bAccount, - bNonce, bIndex, DOMAIN, ]); @@ -220,11 +219,11 @@ export const ShieldedAccessControlWitnesses = wit_getRoleIndex( context: WitnessContext, roleId: Uint8Array, - account: Either + accountId: Uint8Array ): [ShieldedAccessControlPrivateState, bigint] { return [ context.privateState, - ShieldedAccessControlPrivateState.getRoleIndex(context, roleId, account), + ShieldedAccessControlPrivateState.getRoleIndex(context, roleId, accountId), ]; }, }); From a2e644d99d8803d0f6ec2833923633889a52dc6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Thu, 18 Sep 2025 22:49:13 -0400 Subject: [PATCH 200/322] Refactor test suite --- .../access/test/ShieldedAccessControl.test.ts | 199 ++++++++++++++++++ 1 file changed, 199 insertions(+) diff --git a/contracts/src/access/test/ShieldedAccessControl.test.ts b/contracts/src/access/test/ShieldedAccessControl.test.ts index e69de29b..232fb37d 100644 --- a/contracts/src/access/test/ShieldedAccessControl.test.ts +++ b/contracts/src/access/test/ShieldedAccessControl.test.ts @@ -0,0 +1,199 @@ +import { + CompactTypeBytes, + CompactTypeVector, + convert_bigint_to_Uint8Array, + persistentHash, + type WitnessContext, +} from '@midnight-ntwrk/compact-runtime'; +import { beforeEach, describe, expect, it } from 'vitest'; +import { + ContractAddress, + Either, + Ledger, + MerkleTreePath, + ShieldedAccessControl_Role as Role, + ZswapCoinPublicKey, + Contract as MyContract +} from '../../../artifacts/MockShieldedAccessControl/contract/index.cjs'; +import { fmtHexString, ShieldedAccessControlPrivateState, ShieldedAccessControlWitnesses } from '../witnesses/ShieldedAccessControlWitnesses.js'; +import { ShieldedAccessControlSimulator } from './simulators/ShieldedAccessControlSimulator.js'; +import * as utils from './utils/address.js'; + +// Helpers +const buildCommitment = ( + accountId: Uint8Array, + roleId: Uint8Array, + index: bigint, +): Uint8Array => { + const rt_type = new CompactTypeVector(4, new CompactTypeBytes(32)); + const bIndex = convert_bigint_to_Uint8Array(32, index); + + const commitment = persistentHash(rt_type, [ + accountId, + roleId, + bIndex, + COMMITMENT_DOMAIN, + ]); + + return commitment; +}; + +const buildNullifier = ( + roleCommitment: Uint8Array, +): Uint8Array => { + const rt_type = new CompactTypeVector(2, new CompactTypeBytes(32)); + + const nullifier = persistentHash(rt_type, [ + roleCommitment, + NULLIFIER_DOMAIN, + ]); + + return nullifier; +}; + +const createIdHash = ( + pk: ZswapCoinPublicKey, + nonce: Uint8Array, +): Uint8Array => { + const rt_type = new CompactTypeVector(2, new CompactTypeBytes(32)); + + const bPK = pk.bytes; + return persistentHash(rt_type, [bPK, nonce]); +}; + +// PKs +const [ADMIN, Z_ADMIN] = utils.generatePubKeyPair('ADMIN'); +const [UNAUTHORIZED, Z_UNAUTHORIZED] = utils.generatePubKeyPair('UNAUTHORIZED'); + +// Roles +const DEFAULT_ADMIN_ROLE = utils.zeroUint8Array(); +const BAD_ROLE = convert_bigint_to_Uint8Array(32, 99999999n); + +// Nonces +const ADMIN_SECRET_NONCE = Buffer.alloc(32, 'ADMIN_SECRET_NONCE'); +const BAD_NONCE = Buffer.alloc(32, 'BAD_NONCE'); + +// Constants +const COMMITMENT_DOMAIN = new Uint8Array(32); +new TextEncoder().encodeInto('ShieldedAccessControl:commitment', COMMITMENT_DOMAIN); +const NULLIFIER_DOMAIN = new Uint8Array(32); +new TextEncoder().encodeInto('ShieldedAccessControl:nullifier', NULLIFIER_DOMAIN); + +const ADMIN_ID = createIdHash(Z_ADMIN, ADMIN_SECRET_NONCE); +const ADMIN_COMMITMENT = buildCommitment(ADMIN_ID, DEFAULT_ADMIN_ROLE, 0n); +const ADMIN_NULLIFIER = buildNullifier(ADMIN_COMMITMENT); + +const BAD_ID = createIdHash(Z_UNAUTHORIZED, new Uint8Array(32)); +const BAD_INDEX = 99999999n; +const BAD_COMMITMENT = buildCommitment(BAD_ID, BAD_ROLE, BAD_INDEX); + +let shieldedAccessControl: ShieldedAccessControlSimulator; + + +describe('ShieldedAccessControl', () => { + beforeEach(() => { + // Create private state object and generate nonce + const PS = ShieldedAccessControlPrivateState.withRoleAndNonce( + Buffer.from(DEFAULT_ADMIN_ROLE), + ADMIN_SECRET_NONCE, + ); + // Init contract for user with PS + shieldedAccessControl = new ShieldedAccessControlSimulator({ + privateState: PS, + coinPK: ADMIN + }); + }); + + describe('_computeRoleCommitment', () => { + it('computed commitment should match', () => { + expect(shieldedAccessControl._computeRoleCommitment(ADMIN_ID, DEFAULT_ADMIN_ROLE, 0n)).toEqual(ADMIN_COMMITMENT); + }); + + type ComputeRoleCommitmentCases = [ + method: keyof ShieldedAccessControlSimulator, + isValidId: boolean, + isValidRole: boolean, + isValidIndex: boolean, + args: unknown[], + ]; + + const checkedCircuits: ComputeRoleCommitmentCases[] = [ + ['_computeRoleCommitment', false, true, true, [BAD_ID, DEFAULT_ADMIN_ROLE, 0n]], + ['_computeRoleCommitment', true, false, true, [ADMIN_ID, BAD_ROLE, 0n]], + ['_computeRoleCommitment', true, true, false, [ADMIN_ID, DEFAULT_ADMIN_ROLE, BAD_INDEX]], + ['_computeRoleCommitment', false, true, false, [BAD_ID, DEFAULT_ADMIN_ROLE, BAD_INDEX]], + ['_computeRoleCommitment', false, false, false, [BAD_ID, BAD_ROLE, BAD_INDEX]], + ['_computeRoleCommitment', true, false, false, [ADMIN_ID, BAD_ROLE, BAD_INDEX]], + ['_computeRoleCommitment', false, false, true, [BAD_ID, BAD_ROLE, 0n]], + ] + + it.each(checkedCircuits)( + '%s should not match with isValidNonce(%s), isValidIndex(%s), isValidPath(%s)', + (circuitName, isValidId, isValidRole, isValidIndex, args) => { + // Test protected circuit + expect(() => { + ( + shieldedAccessControl[circuitName] as ( + ...args: unknown[] + ) => unknown + )(...args); + }).not.toEqual(ADMIN); + } + ) + }); + + describe('_computeNullifier', () => { + it('should match nullifier', () => { + expect(shieldedAccessControl._computeNullifier(ADMIN_COMMITMENT)).toEqual(ADMIN_NULLIFIER); + }); + + it('should not match with bad commitment', () => { + expect(shieldedAccessControl._computeNullifier(BAD_COMMITMENT)).not.toEqual(ADMIN_NULLIFIER); + }); + }); + + describe('_computeRoleId', () => { + const eitherAdmin = utils.createEitherTestUser('ADMIN'); + const eitherUnauthorized = utils.createEitherTestUser('UNAUTHORIZED'); + + it('should match role id', () => { + expect(shieldedAccessControl._computeRoleId(eitherAdmin, ADMIN_SECRET_NONCE)).toEqual(ADMIN_ID); + }); + + it('should fail for contract address', () => { + const eitherContract = utils.createEitherTestContractAddress('CONTRACT') + expect(() => { + shieldedAccessControl._computeRoleId(eitherContract, ADMIN_SECRET_NONCE); + }).toThrow('ShieldedAccessControl: contract address owners are not yet supported'); + }); + + type ComputeRoleIdCases = [ + method: keyof ShieldedAccessControlSimulator, + isValidAccount: boolean, + isValidNonce: boolean, + args: unknown[], + ]; + + const checkedCircuits: ComputeRoleIdCases[] = [ + ['_computeRoleId', true, false, [eitherAdmin, BAD_NONCE]], + ['_computeRoleId', false, true, [eitherUnauthorized, ADMIN_SECRET_NONCE]], + ['_computeRoleId', false, false, [eitherUnauthorized, BAD_NONCE]], + ]; + + it.each(checkedCircuits)( + '%s should not match role id with invalidAccount=%s or invalidNonce=%s', + (circuitName, isValidAccount, isValidNonce, args) => { + // Test circuit + expect(() => { + ( + shieldedAccessControl[circuitName] as ( + ...args: unknown[] + ) => unknown + )(...args); + }).not.toEqual(ADMIN_ID); + } + ) + }); + + +}); \ No newline at end of file From 16928c308c92f4758abfd398a043bfe1ab1d2d5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Thu, 18 Sep 2025 22:49:50 -0400 Subject: [PATCH 201/322] Refactor simulator for new design --- .../ShieldedAccessControlSimulator.ts | 67 +++++++++++-------- 1 file changed, 40 insertions(+), 27 deletions(-) diff --git a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts index 170c41ec..79df3512 100644 --- a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts +++ b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts @@ -62,7 +62,6 @@ export class ShieldedAccessControlSimulator extends AbstractContractSimulator< >; constructor( - initUser: Either, options: ShieldedAccessControlSimOptions = {}, ) { super(); @@ -71,9 +70,9 @@ export class ShieldedAccessControlSimulator extends AbstractContractSimulator< const { privateState = options.privateState ? options.privateState - : ShieldedAccessControlPrivateState.generate(initUser), + : ShieldedAccessControlPrivateState.generate(), witnesses = ShieldedAccessControlWitnesses(), - coinPK = '0'.repeat(64), + coinPK = options.coinPK ? options.coinPK : '0'.repeat(64), address = sampleContractAddress(), } = options; @@ -233,6 +232,29 @@ export class ShieldedAccessControlSimulator extends AbstractContractSimulator< }; } + public _computeRoleCommitment( + accountId: Uint8Array, + roleId: Uint8Array, + index: bigint, + ): Uint8Array { + return this.circuits.pure._computeRoleCommitment(accountId, roleId, index); + } + + public _computeRoleId( + pk: Either, + nonce: Uint8Array + ): Uint8Array { + return this.circuits.pure._computeRoleId(pk, nonce); + } + + public _computeNullifier(commitment: Uint8Array): Uint8Array { + return this.circuits.pure._computeNullifier(commitment); + } + + public callerHasRole(roleId: Uint8Array): Role { + return this.circuits.impure.callerHasRole(roleId); + } + /** * @description Returns the current commitment representing the contract owner. * The full commitment is: `SHA256(SHA256(pk, nonce), instanceSalt, counter, domain)`. @@ -240,9 +262,9 @@ export class ShieldedAccessControlSimulator extends AbstractContractSimulator< */ public hasRole( roleId: Uint8Array, - account: Either, - ): Role { - return this.circuits.impure.hasRole(roleId, account); + accountId: Uint8Array, + ): Boolean { + return this.circuits.impure.hasRole(roleId, accountId); } /** @@ -254,16 +276,8 @@ export class ShieldedAccessControlSimulator extends AbstractContractSimulator< this.circuits.impure.assertOnlyRole(roleId); } - /** - * @description Leaves the contract without an owner. - * It will not be possible to call `assertOnlyOnwer` circuits anymore. - * Can only be called by the current owner. - */ - public _checkRole( - roleId: Uint8Array, - account: Either, - ) { - this.circuits.impure._checkRole(roleId, account); + public getRole(roleId: Uint8Array, accountId: Uint8Array): Role { + return this.circuits.impure.getRole(roleId, accountId); } /** @@ -286,9 +300,9 @@ export class ShieldedAccessControlSimulator extends AbstractContractSimulator< */ public grantRole( roleId: Uint8Array, - account: Either, + accountId: Uint8Array ) { - this.circuits.impure.grantRole(roleId, account); + this.circuits.impure.grantRole(roleId, accountId); } /** @@ -298,9 +312,9 @@ export class ShieldedAccessControlSimulator extends AbstractContractSimulator< */ public revokeRole( roleId: Uint8Array, - account: Either, + accountId: Uint8Array ) { - this.circuits.impure.revokeRole(roleId, account); + this.circuits.impure.revokeRole(roleId, accountId); } /** @@ -310,7 +324,7 @@ export class ShieldedAccessControlSimulator extends AbstractContractSimulator< */ public renounceRole( roleId: Uint8Array, - callerConfirmation: Either, + callerConfirmation: Uint8Array ) { this.circuits.impure.renounceRole(roleId, callerConfirmation); } @@ -331,9 +345,9 @@ export class ShieldedAccessControlSimulator extends AbstractContractSimulator< */ public _grantRole( roleId: Uint8Array, - account: Either, + accountId: Uint8Array ): boolean { - return this.circuits.impure._grantRole(roleId, account); + return this.circuits.impure._grantRole(roleId, accountId); } /** @@ -343,9 +357,9 @@ export class ShieldedAccessControlSimulator extends AbstractContractSimulator< */ public _revokeRole( roleId: Uint8Array, - account: Either, + accountId: Uint8Array ): boolean { - return this.circuits.impure._revokeRole(roleId, account); + return this.circuits.impure._revokeRole(roleId, accountId); } public readonly privateState = { @@ -360,7 +374,6 @@ export class ShieldedAccessControlSimulator extends AbstractContractSimulator< ): ShieldedAccessControlPrivateState => { const currentState = this.stateManager.getContext().currentPrivateState; const updatedState = { - ...currentState, roles: { ...currentState.roles }, }; const roleString = Buffer.from(roleId).toString('hex'); @@ -370,7 +383,7 @@ export class ShieldedAccessControlSimulator extends AbstractContractSimulator< }, /** - * @description Returns the secret nonce given the context. + * @description Returns the secret nonce for a given roleId. * @returns The secret nonce. */ getCurrentSecretNonce: (roleId: Uint8Array): Uint8Array => { From 740bb831439d2a3a4329607193c2df33bdc6499b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Thu, 25 Sep 2025 13:56:37 -0400 Subject: [PATCH 202/322] WIP --- .../src/access/ShieldedAccessControl.compact | 4 +- .../access/test/ShieldedAccessControl.test.ts | 93 ++++++++++++++++++- .../ShieldedAccessControlSimulator.ts | 7 +- .../ShieldedAccessControlWitnesses.ts | 17 ++-- 4 files changed, 104 insertions(+), 17 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index fff6831c..2986cda6 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -297,7 +297,7 @@ module ShieldedAccessControl { const commitmentNullifier = _computeNullifier(commitment); const authPath = wit_getRoleCommitmentPath(commitment); const rootMatches = _operatorRoles - .checkRoot(merkleTreePathRoot<10, Bytes<32>>(disclose(authPath))); + .checkRoot(merkleTreePathRootNoLeafHash<10>(disclose(authPath))); if(!_roleCommitmentNullifiers.member(disclose(commitmentNullifier)) && rootMatches) { return Role { @@ -492,7 +492,7 @@ module ShieldedAccessControl { } // Use ledger index as source of truth - _operatorRoles.insertIndex(disclose(role.roleCommitment), _currentMerkleTreeIndex); + _operatorRoles.insertHashIndex(role.roleCommitment, _currentMerkleTreeIndex); _currentMerkleTreeIndex.increment(1); return true; } diff --git a/contracts/src/access/test/ShieldedAccessControl.test.ts b/contracts/src/access/test/ShieldedAccessControl.test.ts index 232fb37d..c6dc0891 100644 --- a/contracts/src/access/test/ShieldedAccessControl.test.ts +++ b/contracts/src/access/test/ShieldedAccessControl.test.ts @@ -5,7 +5,7 @@ import { persistentHash, type WitnessContext, } from '@midnight-ntwrk/compact-runtime'; -import { beforeEach, describe, expect, it } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; import { ContractAddress, Either, @@ -13,7 +13,7 @@ import { MerkleTreePath, ShieldedAccessControl_Role as Role, ZswapCoinPublicKey, - Contract as MyContract + Contract as MockShieldedAccessControl } from '../../../artifacts/MockShieldedAccessControl/contract/index.cjs'; import { fmtHexString, ShieldedAccessControlPrivateState, ShieldedAccessControlWitnesses } from '../witnesses/ShieldedAccessControlWitnesses.js'; import { ShieldedAccessControlSimulator } from './simulators/ShieldedAccessControlSimulator.js'; @@ -63,14 +63,24 @@ const createIdHash = ( // PKs const [ADMIN, Z_ADMIN] = utils.generatePubKeyPair('ADMIN'); +const [OPERATOR_1, Z_OPERATOR_1] = utils.generatePubKeyPair('OPERATOR_1'); +const [OPERATOR_2, Z_OPERATOR_2] = utils.generatePubKeyPair('OPERATOR_2'); +const [OPERATOR_3, Z_OPERATOR_3] = utils.generatePubKeyPair('OPERATOR_3'); const [UNAUTHORIZED, Z_UNAUTHORIZED] = utils.generatePubKeyPair('UNAUTHORIZED'); // Roles const DEFAULT_ADMIN_ROLE = utils.zeroUint8Array(); +const OPERATOR_1_ROLE = convert_bigint_to_Uint8Array(32, 1n); +const OPERATOR_2_ROLE = convert_bigint_to_Uint8Array(32, 2n); +const OPERATOR_3_ROLE = convert_bigint_to_Uint8Array(32, 3n); +const UNINITIALIZED_ROLE = convert_bigint_to_Uint8Array(32, 555n); const BAD_ROLE = convert_bigint_to_Uint8Array(32, 99999999n); // Nonces const ADMIN_SECRET_NONCE = Buffer.alloc(32, 'ADMIN_SECRET_NONCE'); +const OPERATOR_1_SECRET_NONCE = Buffer.alloc(32, 'OPERATOR_1_NONCE'); +const OPERATOR_2_SECRET_NONCE = Buffer.alloc(32, 'OPERATOR_2_NONCE'); +const OPERATOR_3_SECRET_NONCE = Buffer.alloc(32, 'OPERATOR_3_NONCE'); const BAD_NONCE = Buffer.alloc(32, 'BAD_NONCE'); // Constants @@ -83,6 +93,10 @@ const ADMIN_ID = createIdHash(Z_ADMIN, ADMIN_SECRET_NONCE); const ADMIN_COMMITMENT = buildCommitment(ADMIN_ID, DEFAULT_ADMIN_ROLE, 0n); const ADMIN_NULLIFIER = buildNullifier(ADMIN_COMMITMENT); +const OPERATOR_1_ID = createIdHash(Z_OPERATOR_1, OPERATOR_1_SECRET_NONCE); +const OPERATOR_2_ID = createIdHash(Z_OPERATOR_2, OPERATOR_2_SECRET_NONCE); +const OPERATOR_3_ID = createIdHash(Z_OPERATOR_3, OPERATOR_3_SECRET_NONCE); + const BAD_ID = createIdHash(Z_UNAUTHORIZED, new Uint8Array(32)); const BAD_INDEX = 99999999n; const BAD_COMMITMENT = buildCommitment(BAD_ID, BAD_ROLE, BAD_INDEX); @@ -195,5 +209,80 @@ describe('ShieldedAccessControl', () => { ) }); + // Complete testing once issue with pathForLeaf is resolved + describe.todo('wit_getRoleIndex', () => { + it.todo('should return 0 if no roles granted', () => { + const [_, index] = shieldedAccessControl.witnesses.wit_getRoleIndex(shieldedAccessControl.getWitnessContext(), UNINITIALIZED_ROLE, ADMIN_ID); + expect(index).toBe(0n); + }); + + it.todo('should return correct index', () => { + let granted = shieldedAccessControl._grantRole(DEFAULT_ADMIN_ROLE, ADMIN_ID); + expect(granted).toBe(true); + let [, adminIndex] = shieldedAccessControl.witnesses.wit_getRoleIndex(shieldedAccessControl.getWitnessContext(), DEFAULT_ADMIN_ROLE, ADMIN_ID); + expect(adminIndex).toBe(0n); + + shieldedAccessControl.privateState.injectSecretNonce(OPERATOR_1_ROLE, OPERATOR_1_SECRET_NONCE); + granted = shieldedAccessControl._grantRole(OPERATOR_1_ROLE, OPERATOR_1_ID); + expect(granted).toBe(true); + const [, operatorIndex] = shieldedAccessControl.witnesses.wit_getRoleIndex(shieldedAccessControl.getWitnessContext(), OPERATOR_1_ROLE, OPERATOR_1_ID); + expect(operatorIndex).toBe(1n); + + shieldedAccessControl.privateState.injectSecretNonce(OPERATOR_2_ROLE, OPERATOR_2_SECRET_NONCE); + granted = shieldedAccessControl._grantRole(OPERATOR_2_ROLE, OPERATOR_2_ID); + expect(granted).toBe(true); + shieldedAccessControl._grantRole(OPERATOR_2_ROLE, OPERATOR_2_ID); + const [, operatorIndex2] = shieldedAccessControl.witnesses.wit_getRoleIndex(shieldedAccessControl.getWitnessContext(), OPERATOR_2_ROLE, OPERATOR_2_ID); + expect(operatorIndex2).toBe(2n); + + shieldedAccessControl.privateState.injectSecretNonce(OPERATOR_3_ROLE, OPERATOR_3_SECRET_NONCE); + shieldedAccessControl._grantRole(OPERATOR_3_ROLE, OPERATOR_3_ID); + const [_, operatorIndex3] = shieldedAccessControl.witnesses.wit_getRoleIndex(shieldedAccessControl.getWitnessContext(), OPERATOR_3_ROLE, OPERATOR_3_ID); + expect(operatorIndex3).toBe(3n); + + let [, adminIndex2] = shieldedAccessControl.witnesses.wit_getRoleIndex(shieldedAccessControl.getWitnessContext(), DEFAULT_ADMIN_ROLE, ADMIN_ID); + expect(adminIndex2).toBe(0n); + }); + + it.todo('should return current Merkle tree index if role does not exist') + }); + + describe('wit_getRoleCommitmentPath', () => { + it('should return a Merkle tree path if one exists', () => { + + }); + }); + + describe('getRole', () => { + it('should return unapproved if role does not exist', () => { + expect(shieldedAccessControl.getRole(UNINITIALIZED_ROLE, ADMIN_ID).isApproved).toBe(false); + }); + + it('should return correct commitment', () => { + expect(shieldedAccessControl.getRole(DEFAULT_ADMIN_ROLE, ADMIN_ID).roleCommitment).toEqual(ADMIN_COMMITMENT); + }); + + it('should return correct nullifier', () => { + expect(shieldedAccessControl.getRole(DEFAULT_ADMIN_ROLE, ADMIN_ID).commitmentNullifier).toEqual(ADMIN_NULLIFIER); + }); + + it('should return approved role', () => { + shieldedAccessControl._grantRole(DEFAULT_ADMIN_ROLE, ADMIN_ID); + expect(shieldedAccessControl.getRole(DEFAULT_ADMIN_ROLE, ADMIN_ID).isApproved).toBe(true); + }); + }); + + describe('_grantRole', () => { + it('should return true for new role', () => { + expect(shieldedAccessControl._grantRole(DEFAULT_ADMIN_ROLE, ADMIN_ID)).toBe(true); + }); + + it('should return false if role already granted', () => { + shieldedAccessControl._grantRole(DEFAULT_ADMIN_ROLE, ADMIN_ID); + expect(shieldedAccessControl._grantRole(DEFAULT_ADMIN_ROLE, ADMIN_ID)).toBe(false); + }); + }); + + describe('') }); \ No newline at end of file diff --git a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts index 79df3512..2118e91e 100644 --- a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts +++ b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts @@ -2,6 +2,7 @@ import { type CircuitContext, type CoinPublicKey, emptyZswapLocalState, + witnessContext, type WitnessContext, } from '@midnight-ntwrk/compact-runtime'; import { sampleContractAddress } from '@midnight-ntwrk/zswap'; @@ -111,11 +112,7 @@ export class ShieldedAccessControlSimulator extends AbstractContractSimulator< Ledger, ShieldedAccessControlPrivateState > { - return { - ledger: this.getPublicState(), - privateState: this.getPrivateState(), - contractAddress: this.contractAddress, - }; + return witnessContext(this.getPublicState(), this.getPrivateState(), this.contractAddress); } /** diff --git a/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts b/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts index f5266101..cd905647 100644 --- a/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts +++ b/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts @@ -15,8 +15,8 @@ import type { } from '../../../artifacts/MockShieldedAccessControl/contract/index.cjs'; import { eitherToBytes } from '../test/utils/address'; -const DOMAIN = new Uint8Array(32); -new TextEncoder().encodeInto('ShieldedAccessControl:shield:', DOMAIN); +const COMMITMENT_DOMAIN = new Uint8Array(32); +new TextEncoder().encodeInto('ShieldedAccessControl:commitment', COMMITMENT_DOMAIN); export function fmtHexString(bytes: string | Uint8Array): string { if (bytes instanceof String) { @@ -154,28 +154,29 @@ export const ShieldedAccessControlPrivateState = { ): bigint => { const rt_type = new CompactTypeVector(4, new CompactTypeBytes(32)); // Iterate over each MT index to determine if commitment exists + console.log("current MT index ", ledger.ShieldedAccessControl__currentMerkleTreeIndex); for (let i = 0; i <= ledger.ShieldedAccessControl__currentMerkleTreeIndex; i++) { - const bIndex = convert_bigint_to_Uint8Array(32, BigInt(i)); + const index = BigInt(i); + const bIndex = convert_bigint_to_Uint8Array(32, index); const commitment = persistentHash(rt_type, [ accountId, roleId, bIndex, - DOMAIN, + COMMITMENT_DOMAIN, ]); try { - const index = BigInt(i); const pathForLeaf = ledger.ShieldedAccessControl__operatorRoles.pathForLeaf( index, commitment, ); - if (pathForLeaf.leaf === commitment) { + if (Buffer.from(pathForLeaf.leaf).compare(Buffer.from(commitment)) === 0) { return index; } } catch (e: unknown) { if (e instanceof Error) { const [msg, index] = e.message.split(':'); if (msg === 'invalid index into sparse merkle tree') { - // console.log(`role ${fmtHexString(roleIdString)} with commitment ${fmtHexString(commitment)} not found at index ${index}`); + // console.log(`accountId ${fmtHexString(accountId)} with commitment ${fmtHexString(commitment)} not found at index ${index}`); } else { throw e; } @@ -183,7 +184,7 @@ export const ShieldedAccessControlPrivateState = { } } - console.log("WIT - Commitment DNE, returing MT index ", ledger.ShieldedAccessControl__currentMerkleTreeIndex.toString()); + console.log("WIT - Commitment DNE, returning MT index ", ledger.ShieldedAccessControl__currentMerkleTreeIndex.toString()); // If commitment doesn't exist return currentMTIndex // Used for adding roles From a6feefffcb07611bd192255bb416d0a9d4836ac0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Tue, 24 Feb 2026 12:02:21 -0500 Subject: [PATCH 203/322] Remove old --- compact/package.json | 37 - compact/src/Builder.ts | 160 ---- compact/src/Compiler.ts | 713 -------------- compact/src/runBuilder.ts | 43 - compact/src/runCompiler.ts | 227 ----- compact/src/types/errors.ts | 97 -- compact/test/Compiler.test.ts | 880 ------------------ compact/test/runCompiler.test.ts | 478 ---------- compact/tsconfig.json | 27 - compact/turbo.json | 12 - compact/vitest.config.ts | 10 - docs/antora.yml | 8 - docs/modules/ROOT/nav.adoc | 26 - docs/modules/ROOT/pages/access.adoc | 573 ------------ docs/modules/ROOT/pages/api/access.adoc | 603 ------------ .../modules/ROOT/pages/api/fungibleToken.adoc | 389 -------- docs/modules/ROOT/pages/api/multitoken.adoc | 313 ------- .../ROOT/pages/api/nonFungibleToken.adoc | 489 ---------- docs/modules/ROOT/pages/api/security.adoc | 161 ---- .../ROOT/pages/api/shieldedAccessControl.adoc | 0 docs/modules/ROOT/pages/api/utils.adoc | 67 -- docs/modules/ROOT/pages/extensibility.adoc | 165 ---- docs/modules/ROOT/pages/fungibleToken.adoc | 155 --- docs/modules/ROOT/pages/index.adoc | 90 -- docs/modules/ROOT/pages/multitoken.adoc | 155 --- docs/modules/ROOT/pages/nonFungibleToken.adoc | 164 ---- docs/modules/ROOT/pages/security.adoc | 95 -- .../ROOT/pages/shieldedAccessControl.adoc | 0 docs/modules/ROOT/pages/utils.adoc | 30 - docs/modules/ROOT/pages/zkCircuits101.adoc | 17 - docs/package.json | 15 - 31 files changed, 6199 deletions(-) delete mode 100644 compact/package.json delete mode 100755 compact/src/Builder.ts delete mode 100755 compact/src/Compiler.ts delete mode 100644 compact/src/runBuilder.ts delete mode 100644 compact/src/runCompiler.ts delete mode 100644 compact/src/types/errors.ts delete mode 100644 compact/test/Compiler.test.ts delete mode 100644 compact/test/runCompiler.test.ts delete mode 100644 compact/tsconfig.json delete mode 100644 compact/turbo.json delete mode 100644 compact/vitest.config.ts delete mode 100644 docs/antora.yml delete mode 100644 docs/modules/ROOT/nav.adoc delete mode 100644 docs/modules/ROOT/pages/access.adoc delete mode 100644 docs/modules/ROOT/pages/api/access.adoc delete mode 100644 docs/modules/ROOT/pages/api/fungibleToken.adoc delete mode 100644 docs/modules/ROOT/pages/api/multitoken.adoc delete mode 100644 docs/modules/ROOT/pages/api/nonFungibleToken.adoc delete mode 100644 docs/modules/ROOT/pages/api/security.adoc delete mode 100644 docs/modules/ROOT/pages/api/shieldedAccessControl.adoc delete mode 100644 docs/modules/ROOT/pages/api/utils.adoc delete mode 100644 docs/modules/ROOT/pages/extensibility.adoc delete mode 100644 docs/modules/ROOT/pages/fungibleToken.adoc delete mode 100644 docs/modules/ROOT/pages/index.adoc delete mode 100644 docs/modules/ROOT/pages/multitoken.adoc delete mode 100644 docs/modules/ROOT/pages/nonFungibleToken.adoc delete mode 100644 docs/modules/ROOT/pages/security.adoc delete mode 100644 docs/modules/ROOT/pages/shieldedAccessControl.adoc delete mode 100644 docs/modules/ROOT/pages/utils.adoc delete mode 100644 docs/modules/ROOT/pages/zkCircuits101.adoc delete mode 100644 docs/package.json diff --git a/compact/package.json b/compact/package.json deleted file mode 100644 index 1b38245c..00000000 --- a/compact/package.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "name": "@openzeppelin-compact/compact", - "private": true, - "version": "0.0.1", - "keywords": [ - "compact", - "compiler" - ], - "author": "", - "license": "MIT", - "description": "Compact fetcher", - "type": "module", - "exports": "./index.js", - "engines": { - "node": ">=20" - }, - "bin": { - "compact-builder": "dist/runBuilder.js", - "compact-compiler": "dist/runCompiler.js" - }, - "scripts": { - "build": "tsc -p .", - "types": "tsc -p tsconfig.json --noEmit", - "test": "yarn vitest run", - "clean": "git clean -fXd" - }, - "devDependencies": { - "@types/node": "22.18.0", - "typescript": "^5.8.2", - "vitest": "^3.1.3" - }, - "dependencies": { - "chalk": "^5.6.2", - "log-symbols": "^7.0.0", - "ora": "^9.0.0" - } -} diff --git a/compact/src/Builder.ts b/compact/src/Builder.ts deleted file mode 100755 index b8f05197..00000000 --- a/compact/src/Builder.ts +++ /dev/null @@ -1,160 +0,0 @@ -#!/usr/bin/env node - -import { exec } from 'node:child_process'; -import { promisify } from 'node:util'; -import chalk from 'chalk'; -import ora, { type Ora } from 'ora'; -import { CompactCompiler } from './Compiler.js'; -import { isPromisifiedChildProcessError } from './types/errors.js'; - -// Promisified exec for async execution -const execAsync = promisify(exec); - -/** - * A class to handle the build process for a project. - * Runs CompactCompiler as a prerequisite, then executes build steps (TypeScript compilation, - * artifact copying, etc.) - * with progress feedback and colored output for success and error states. - * - * @notice `cmd` scripts discard `stderr` output and fail silently because this is - * handled in `executeStep`. - * - * @example - * ```typescript - * const builder = new ProjectBuilder('--skip-zk'); // Optional flags for compactc - * builder.build().catch(err => console.error(err)); - * ``` - * - * @example Successful Build Output - * ``` - * ℹ [COMPILE] Found 2 .compact file(s) to compile - * ✔ [COMPILE] [1/2] Compiled AccessControl.compact - * Compactc version: 0.24.0 - * ✔ [COMPILE] [2/2] Compiled MockAccessControl.compact - * Compactc version: 0.24.0 - * ✔ [BUILD] [1/3] Compiling TypeScript - * ✔ [BUILD] [2/3] Copying artifacts - * ✔ [BUILD] [3/3] Copying and cleaning .compact files - * ``` - * - * @example Failed Compilation Output - * ``` - * ℹ [COMPILE] Found 2 .compact file(s) to compile - * ✖ [COMPILE] [1/2] Failed AccessControl.compact - * Compactc version: 0.24.0 - * Error: Expected ';' at line 5 in AccessControl.compact - * ``` - * - * @example Failed Build Step Output - * ``` - * ℹ [COMPILE] Found 2 .compact file(s) to compile - * ✔ [COMPILE] [1/2] Compiled AccessControl.compact - * ✔ [COMPILE] [2/2] Compiled MockAccessControl.compact - * ✖ [BUILD] [1/3] Failed Compiling TypeScript - * error TS1005: ';' expected at line 10 in file.ts - * [BUILD] ❌ Build failed: Command failed: tsc --project tsconfig.build.json - * ``` - */ -export class CompactBuilder { - private readonly compilerFlags: string; - private readonly steps: Array<{ cmd: string; msg: string; shell?: string }> = - [ - { - cmd: 'tsc --project tsconfig.build.json', - msg: 'Compiling TypeScript', - }, - { - cmd: 'mkdir -p dist/artifacts && cp -Rf src/artifacts/* dist/artifacts/ 2>/dev/null || true', - msg: 'Copying artifacts', - shell: '/bin/bash', - }, - { - cmd: 'mkdir -p dist && find src -type f -name "*.compact" -exec cp {} dist/ \\; 2>/dev/null && rm dist/Mock*.compact 2>/dev/null || true', - msg: 'Copying and cleaning .compact files', - shell: '/bin/bash', - }, - ]; - - /** - * Constructs a new ProjectBuilder instance. - * @param compilerFlags - Optional space-separated string of `compactc` flags (e.g., "--skip-zk") - */ - constructor(compilerFlags = '') { - this.compilerFlags = compilerFlags; - } - - /** - * Executes the full build process: compiles .compact files first, then runs build steps. - * Displays progress with spinners and outputs results in color. - * - * @returns A promise that resolves when all steps complete successfully - * @throws Error if compilation or any build step fails - */ - public async build(): Promise { - // Run compact compilation as a prerequisite - const compiler = new CompactCompiler(this.compilerFlags); - await compiler.compile(); - - // Proceed with build steps - for (const [index, step] of this.steps.entries()) { - await this.executeStep(step, index, this.steps.length); - } - } - - /** - * Executes a single build step. - * Runs the command, shows a spinner, and prints output with indentation. - * - * @param step - The build step containing command and message - * @param index - Current step index (0-based) for progress display - * @param total - Total number of steps for progress display - * @returns A promise that resolves when the step completes successfully - * @throws Error if the step fails - */ - private async executeStep( - step: { cmd: string; msg: string; shell?: string }, - index: number, - total: number, - ): Promise { - const stepLabel: string = `[${index + 1}/${total}]`; - const spinner: Ora = ora(`[BUILD] ${stepLabel} ${step.msg}`).start(); - - try { - const { stdout, stderr }: { stdout: string; stderr: string } = - await execAsync(step.cmd, { - shell: step.shell, // Only pass shell where needed - }); - spinner.succeed(`[BUILD] ${stepLabel} ${step.msg}`); - this.printOutput(stdout, chalk.cyan); - this.printOutput(stderr, chalk.yellow); // Show stderr (warnings) in yellow if present - } catch (error: unknown) { - spinner.fail(`[BUILD] ${stepLabel} ${step.msg}`); - if (isPromisifiedChildProcessError(error)) { - this.printOutput(error.stdout, chalk.cyan); - this.printOutput(error.stderr, chalk.red); - // biome-ignore lint/suspicious/noConsole: Needed to display build failure reason - console.error(chalk.red('[BUILD] ❌ Build failed:', error.message)); - } else if (error instanceof Error) { - // biome-ignore lint/suspicious/noConsole: Needed to display build failure reason - console.error(chalk.red('[BUILD] ❌ Build failed:', error.message)); - } - - process.exit(1); - } - } - - /** - * Prints command output with indentation and specified color. - * Filters out empty lines and indents each line for readability. - * - * @param output - The command output string to print (stdout or stderr) - * @param colorFn - Chalk color function to style the output (e.g., `chalk.cyan` for success, `chalk.red` for errors) - */ - private printOutput(output: string, colorFn: (text: string) => string): void { - const lines: string[] = output - .split('\n') - .filter((line: string): boolean => line.trim() !== '') - .map((line: string): string => ` ${line}`); - console.log(colorFn(lines.join('\n'))); - } -} diff --git a/compact/src/Compiler.ts b/compact/src/Compiler.ts deleted file mode 100755 index fe1adcf7..00000000 --- a/compact/src/Compiler.ts +++ /dev/null @@ -1,713 +0,0 @@ -#!/usr/bin/env node - -import { exec as execCallback } from 'node:child_process'; -import { existsSync } from 'node:fs'; -import { readdir } from 'node:fs/promises'; -import { basename, join, relative } from 'node:path'; -import { promisify } from 'node:util'; -import chalk from 'chalk'; -import ora from 'ora'; -import { - CompactCliNotFoundError, - CompilationError, - DirectoryNotFoundError, - isPromisifiedChildProcessError, -} from './types/errors.ts'; - -/** Source directory containing .compact files */ -const SRC_DIR: string = 'src'; -/** Output directory for compiled artifacts */ -const ARTIFACTS_DIR: string = 'artifacts'; - -/** - * Function type for executing shell commands. - * Allows dependency injection for testing and customization. - * - * @param command - The shell command to execute - * @returns Promise resolving to command output - */ -export type ExecFunction = ( - command: string, -) => Promise<{ stdout: string; stderr: string }>; - -/** - * Service responsible for validating the Compact CLI environment. - * Checks CLI availability, retrieves version information, and ensures - * the toolchain is properly configured before compilation. - * - * @class EnvironmentValidator - * @example - * ```typescript - * const validator = new EnvironmentValidator(); - * await validator.validate('0.24.0'); - * const version = await validator.getDevToolsVersion(); - * ``` - */ -export class EnvironmentValidator { - private execFn: ExecFunction; - - /** - * Creates a new EnvironmentValidator instance. - * - * @param execFn - Function to execute shell commands (defaults to promisified child_process.exec) - */ - constructor(execFn: ExecFunction = promisify(execCallback)) { - this.execFn = execFn; - } - - /** - * Checks if the Compact CLI is available in the system PATH. - * - * @returns Promise resolving to true if CLI is available, false otherwise - * @example - * ```typescript - * const isAvailable = await validator.checkCompactAvailable(); - * if (!isAvailable) { - * throw new Error('Compact CLI not found'); - * } - * ``` - */ - async checkCompactAvailable(): Promise { - try { - await this.execFn('compact --version'); - return true; - } catch { - return false; - } - } - - /** - * Retrieves the version of the Compact developer tools. - * - * @returns Promise resolving to the version string - * @throws {Error} If the CLI is not available or command fails - * @example - * ```typescript - * const version = await validator.getDevToolsVersion(); - * console.log(`Using Compact ${version}`); - * ``` - */ - async getDevToolsVersion(): Promise { - const { stdout } = await this.execFn('compact --version'); - return stdout.trim(); - } - - /** - * Retrieves the version of the Compact toolchain/compiler. - * - * @param version - Optional specific toolchain version to query - * @returns Promise resolving to the toolchain version string - * @throws {Error} If the CLI is not available or command fails - * @example - * ```typescript - * const toolchainVersion = await validator.getToolchainVersion('0.24.0'); - * console.log(`Toolchain: ${toolchainVersion}`); - * ``` - */ - async getToolchainVersion(version?: string): Promise { - const versionFlag = version ? `+${version}` : ''; - const { stdout } = await this.execFn( - `compact compile ${versionFlag} --version`, - ); - return stdout.trim(); - } - - /** - * Validates the entire Compact environment and ensures it's ready for compilation. - * Checks CLI availability and retrieves version information. - * - * @param version - Optional specific toolchain version to validate - * @throws {CompactCliNotFoundError} If the Compact CLI is not available - * @throws {Error} If version commands fail - * @example - * ```typescript - * try { - * await validator.validate('0.24.0'); - * console.log('Environment validated successfully'); - * } catch (error) { - * if (error instanceof CompactCliNotFoundError) { - * console.error('Please install Compact CLI'); - * } - * } - * ``` - */ - async validate( - version?: string, - ): Promise<{ devToolsVersion: string; toolchainVersion: string }> { - const isAvailable = await this.checkCompactAvailable(); - if (!isAvailable) { - throw new CompactCliNotFoundError( - "'compact' CLI not found in PATH. Please install the Compact developer tools.", - ); - } - - const devToolsVersion = await this.getDevToolsVersion(); - const toolchainVersion = await this.getToolchainVersion(version); - - return { devToolsVersion, toolchainVersion }; - } -} - -/** - * Service responsible for discovering .compact files in the source directory. - * Recursively scans directories and filters for .compact file extensions. - * - * @class FileDiscovery - * @example - * ```typescript - * const discovery = new FileDiscovery(); - * const files = await discovery.getCompactFiles('src/security'); - * console.log(`Found ${files.length} .compact files`); - * ``` - */ -export class FileDiscovery { - /** - * Recursively discovers all .compact files in a directory. - * Returns relative paths from the SRC_DIR for consistent processing. - * - * @param dir - Directory path to search (relative or absolute) - * @returns Promise resolving to array of relative file paths - * @example - * ```typescript - * const files = await discovery.getCompactFiles('src'); - * // Returns: ['contracts/Token.compact', 'security/AccessControl.compact'] - * ``` - */ - async getCompactFiles(dir: string): Promise { - try { - const dirents = await readdir(dir, { withFileTypes: true }); - const filePromises = dirents.map(async (entry) => { - const fullPath = join(dir, entry.name); - try { - if (entry.isDirectory()) { - return await this.getCompactFiles(fullPath); - } - - if (entry.isFile() && fullPath.endsWith('.compact')) { - return [relative(SRC_DIR, fullPath)]; - } - return []; - } catch (err) { - // biome-ignore lint/suspicious/noConsole: Needed to display error and file path - console.warn(`Error accessing ${fullPath}:`, err); - return []; - } - }); - - const results = await Promise.all(filePromises); - return results.flat(); - } catch (err) { - // biome-ignore lint/suspicious/noConsole: Needed to display error and dir path - console.error(`Failed to read dir: ${dir}`, err); - return []; - } - } -} - -/** - * Service responsible for compiling individual .compact files. - * Handles command construction, execution, and error processing. - * - * @class CompilerService - * @example - * ```typescript - * const compiler = new CompilerService(); - * const result = await compiler.compileFile( - * 'contracts/Token.compact', - * '--skip-zk --verbose', - * '0.24.0' - * ); - * console.log('Compilation output:', result.stdout); - * ``` - */ -export class CompilerService { - private execFn: ExecFunction; - - /** - * Creates a new CompilerService instance. - * - * @param execFn - Function to execute shell commands (defaults to promisified child_process.exec) - */ - constructor(execFn: ExecFunction = promisify(execCallback)) { - this.execFn = execFn; - } - - /** - * Compiles a single .compact file using the Compact CLI. - * Constructs the appropriate command with flags and version, then executes it. - * - * @param file - Relative path to the .compact file from SRC_DIR - * @param flags - Space-separated compiler flags (e.g., '--skip-zk --verbose') - * @param version - Optional specific toolchain version to use - * @returns Promise resolving to compilation output (stdout/stderr) - * @throws {CompilationError} If compilation fails for any reason - * @example - * ```typescript - * try { - * const result = await compiler.compileFile( - * 'security/AccessControl.compact', - * '--skip-zk', - * '0.24.0' - * ); - * console.log('Success:', result.stdout); - * } catch (error) { - * if (error instanceof CompilationError) { - * console.error('Compilation failed for', error.file); - * } - * } - * ``` - */ - async compileFile( - file: string, - flags: string, - version?: string, - ): Promise<{ stdout: string; stderr: string }> { - const inputPath = join(SRC_DIR, file); - const outputDir = join(ARTIFACTS_DIR, basename(file, '.compact')); - - const versionFlag = version ? `+${version}` : ''; - const flagsStr = flags ? ` ${flags}` : ''; - const command = `compact compile${versionFlag ? ` ${versionFlag}` : ''}${flagsStr} "${inputPath}" "${outputDir}"`; - - try { - return await this.execFn(command); - } catch (error: unknown) { - let message: string; - - if (error instanceof Error) { - message = error.message; - } else { - message = String(error); // fallback for strings, objects, numbers, etc. - } - - throw new CompilationError( - `Failed to compile ${file}: ${message}`, - file, - error, - ); - } - } -} - -/** - * Utility service for handling user interface output and formatting. - * Provides consistent styling and formatting for compiler messages and output. - * - * @class UIService - * @example - * ```typescript - * UIService.displayEnvInfo('compact 0.1.0', 'Compactc 0.24.0', 'security'); - * UIService.printOutput('Compilation successful', chalk.green); - * ``` - */ -export const UIService = { - /** - * Prints formatted output with consistent indentation and coloring. - * Filters empty lines and adds consistent indentation for readability. - * - * @param output - Raw output text to format - * @param colorFn - Chalk color function for styling - * @example - * ```typescript - * UIService.printOutput(stdout, chalk.cyan); - * UIService.printOutput(stderr, chalk.red); - * ``` - */ - printOutput(output: string, colorFn: (text: string) => string): void { - const lines = output - .split('\n') - .filter((line) => line.trim() !== '') - .map((line) => ` ${line}`); - console.log(colorFn(lines.join('\n'))); - }, - - /** - * Displays environment information including tool versions and configuration. - * Shows developer tools version, toolchain version, and optional settings. - * - * @param devToolsVersion - Version string of the Compact developer tools - * @param toolchainVersion - Version string of the Compact toolchain/compiler - * @param targetDir - Optional target directory being compiled - * @param version - Optional specific version being used - * @example - * ```typescript - * UIService.displayEnvInfo( - * 'compact 0.1.0', - * 'Compactc version: 0.24.0', - * 'security', - * '0.24.0' - * ); - * ``` - */ - displayEnvInfo( - devToolsVersion: string, - toolchainVersion: string, - targetDir?: string, - version?: string, - ): void { - const spinner = ora(); - - if (targetDir) { - spinner.info(chalk.blue(`[COMPILE] TARGET_DIR: ${targetDir}`)); - } - - spinner.info( - chalk.blue(`[COMPILE] Compact developer tools: ${devToolsVersion}`), - ); - spinner.info( - chalk.blue(`[COMPILE] Compact toolchain: ${toolchainVersion}`), - ); - - if (version) { - spinner.info(chalk.blue(`[COMPILE] Using toolchain version: ${version}`)); - } - }, - - /** - * Displays compilation start message with file count and optional location. - * - * @param fileCount - Number of files to be compiled - * @param targetDir - Optional target directory being compiled - * @example - * ```typescript - * UIService.showCompilationStart(5, 'security'); - * // Output: "Found 5 .compact file(s) to compile in security/" - * ``` - */ - showCompilationStart(fileCount: number, targetDir?: string): void { - const searchLocation = targetDir ? ` in ${targetDir}/` : ''; - const spinner = ora(); - spinner.info( - chalk.blue( - `[COMPILE] Found ${fileCount} .compact file(s) to compile${searchLocation}`, - ), - ); - }, - - /** - * Displays a warning message when no .compact files are found. - * - * @param targetDir - Optional target directory that was searched - * @example - * ```typescript - * UIService.showNoFiles('security'); - * // Output: "No .compact files found in security/." - * ``` - */ - showNoFiles(targetDir?: string): void { - const searchLocation = targetDir ? `${targetDir}/` : ''; - const spinner = ora(); - spinner.warn( - chalk.yellow(`[COMPILE] No .compact files found in ${searchLocation}.`), - ); - }, -}; - -/** - * Main compiler class that orchestrates the compilation process. - * Coordinates environment validation, file discovery, and compilation services - * to provide a complete .compact file compilation solution. - * - * Features: - * - Dependency injection for testability - * - Structured error propagation with custom error types - * - Progress reporting and user feedback - * - Support for compiler flags and toolchain versions - * - Environment variable integration - * - * @class CompactCompiler - * @example - * ```typescript - * // Basic usage - * const compiler = new CompactCompiler('--skip-zk', 'security', '0.24.0'); - * await compiler.compile(); - * - * // Factory method usage - * const compiler = CompactCompiler.fromArgs(['--dir', 'security', '--skip-zk']); - * await compiler.compile(); - * - * // With environment variables - * process.env.SKIP_ZK = 'true'; - * const compiler = CompactCompiler.fromArgs(['--dir', 'token']); - * await compiler.compile(); - * ``` - */ -export class CompactCompiler { - /** Environment validation service */ - private readonly environmentValidator: EnvironmentValidator; - /** File discovery service */ - private readonly fileDiscovery: FileDiscovery; - /** Compilation execution service */ - private readonly compilerService: CompilerService; - - /** Compiler flags to pass to the Compact CLI */ - private readonly flags: string; - /** Optional target directory to limit compilation scope */ - private readonly targetDir?: string; - /** Optional specific toolchain version to use */ - private readonly version?: string; - - /** - * Creates a new CompactCompiler instance with specified configuration. - * - * @param flags - Space-separated compiler flags (e.g., '--skip-zk --verbose') - * @param targetDir - Optional subdirectory within src/ to compile (e.g., 'security', 'token') - * @param version - Optional toolchain version to use (e.g., '0.24.0') - * @param execFn - Optional custom exec function for dependency injection - * @example - * ```typescript - * // Compile all files with flags - * const compiler = new CompactCompiler('--skip-zk --verbose'); - * - * // Compile specific directory - * const compiler = new CompactCompiler('', 'security'); - * - * // Compile with specific version - * const compiler = new CompactCompiler('--skip-zk', undefined, '0.24.0'); - * - * // For testing with custom exec function - * const mockExec = vi.fn(); - * const compiler = new CompactCompiler('', undefined, undefined, mockExec); - * ``` - */ - constructor( - flags = '', - targetDir?: string, - version?: string, - execFn?: ExecFunction, - ) { - this.flags = flags.trim(); - this.targetDir = targetDir; - this.version = version; - this.environmentValidator = new EnvironmentValidator(execFn); - this.fileDiscovery = new FileDiscovery(); - this.compilerService = new CompilerService(execFn); - } - - /** - * Factory method to create a CompactCompiler from command-line arguments. - * Parses various argument formats including flags, directories, versions, and environment variables. - * - * Supported argument patterns: - * - `--dir ` - Target specific directory - * - `+` - Use specific toolchain version - * - Other arguments - Treated as compiler flags - * - `SKIP_ZK=true` environment variable - Adds --skip-zk flag - * - * @param args - Array of command-line arguments - * @param env - Environment variables (defaults to process.env) - * @returns New CompactCompiler instance configured from arguments - * @throws {Error} If --dir flag is provided without a directory name - * @example - * ```typescript - * // Parse command line: compact-compiler --dir security --skip-zk +0.24.0 - * const compiler = CompactCompiler.fromArgs([ - * '--dir', 'security', - * '--skip-zk', - * '+0.24.0' - * ]); - * - * // With environment variable - * const compiler = CompactCompiler.fromArgs( - * ['--dir', 'token'], - * { SKIP_ZK: 'true' } - * ); - * - * // Empty args with environment - * const compiler = CompactCompiler.fromArgs([], { SKIP_ZK: 'true' }); - * ``` - */ - static fromArgs( - args: string[], - env: NodeJS.ProcessEnv = process.env, - ): CompactCompiler { - let targetDir: string | undefined; - const flags: string[] = []; - let version: string | undefined; - - if (env.SKIP_ZK === 'true') { - flags.push('--skip-zk'); - } - - for (let i = 0; i < args.length; i++) { - if (args[i] === '--dir') { - const dirNameExists = - i + 1 < args.length && !args[i + 1].startsWith('--'); - if (dirNameExists) { - targetDir = args[i + 1]; - i++; - } else { - throw new Error('--dir flag requires a directory name'); - } - } else if (args[i].startsWith('+')) { - version = args[i].slice(1); - } else { - // Only add flag if it's not already present - if (!flags.includes(args[i])) { - flags.push(args[i]); - } - } - } - - return new CompactCompiler(flags.join(' '), targetDir, version); - } - - /** - * Validates the compilation environment and displays version information. - * Performs environment validation, retrieves toolchain versions, and shows configuration details. - * - * Process: - * - * 1. Validates CLI availability and toolchain compatibility - * 2. Retrieves developer tools and compiler versions - * 3. Displays environment configuration information - * - * @throws {CompactCliNotFoundError} If Compact CLI is not available in PATH - * @throws {Error} If version retrieval or other validation steps fail - * @example - * ```typescript - * try { - * await compiler.validateEnvironment(); - * console.log('Environment ready for compilation'); - * } catch (error) { - * if (error instanceof CompactCliNotFoundError) { - * console.error('Please install Compact CLI'); - * } - * } - * ``` - */ - async validateEnvironment(): Promise { - const { devToolsVersion, toolchainVersion } = - await this.environmentValidator.validate(this.version); - UIService.displayEnvInfo( - devToolsVersion, - toolchainVersion, - this.targetDir, - this.version, - ); - } - - /** - * Main compilation method that orchestrates the entire compilation process. - * - * Process flow: - * 1. Validates environment and shows configuration - * 2. Discovers .compact files in target directory - * 3. Compiles each file with progress reporting - * 4. Handles errors and provides user feedback - * - * @throws {CompactCliNotFoundError} If Compact CLI is not available - * @throws {DirectoryNotFoundError} If target directory doesn't exist - * @throws {CompilationError} If any file compilation fails - * @example - * ```typescript - * const compiler = new CompactCompiler('--skip-zk', 'security'); - * - * try { - * await compiler.compile(); - * console.log('All files compiled successfully'); - * } catch (error) { - * if (error instanceof DirectoryNotFoundError) { - * console.error(`Directory not found: ${error.directory}`); - * } else if (error instanceof CompilationError) { - * console.error(`Failed to compile: ${error.file}`); - * } - * } - * ``` - */ - async compile(): Promise { - await this.validateEnvironment(); - - const searchDir = this.targetDir ? join(SRC_DIR, this.targetDir) : SRC_DIR; - - // Validate target directory exists - if (this.targetDir && !existsSync(searchDir)) { - throw new DirectoryNotFoundError( - `Target directory ${searchDir} does not exist`, - searchDir, - ); - } - - const compactFiles = await this.fileDiscovery.getCompactFiles(searchDir); - - if (compactFiles.length === 0) { - UIService.showNoFiles(this.targetDir); - return; - } - - UIService.showCompilationStart(compactFiles.length, this.targetDir); - - for (const [index, file] of compactFiles.entries()) { - await this.compileFile(file, index, compactFiles.length); - } - } - - /** - * Compiles a single file with progress reporting and error handling. - * Private method used internally by the main compile() method. - * - * @param file - Relative path to the .compact file - * @param index - Current file index (0-based) for progress tracking - * @param total - Total number of files being compiled - * @throws {CompilationError} If compilation fails - * @private - */ - private async compileFile( - file: string, - index: number, - total: number, - ): Promise { - const step = `[${index + 1}/${total}]`; - const spinner = ora( - chalk.blue(`[COMPILE] ${step} Compiling ${file}`), - ).start(); - - try { - const result = await this.compilerService.compileFile( - file, - this.flags, - this.version, - ); - - spinner.succeed(chalk.green(`[COMPILE] ${step} Compiled ${file}`)); - // Filter out compactc version output from compact compile - const filteredOutput = result.stdout.split('\n').slice(1).join('\n'); - - if (filteredOutput) { - UIService.printOutput(filteredOutput, chalk.cyan); - } - UIService.printOutput(result.stderr, chalk.yellow); - } catch (error) { - spinner.fail(chalk.red(`[COMPILE] ${step} Failed ${file}`)); - - if ( - error instanceof CompilationError && - isPromisifiedChildProcessError(error) - ) { - const execError = error; - // Filter out compactc version output from compact compile - const filteredOutput = execError.stdout.split('\n').slice(1).join('\n'); - - if (filteredOutput) { - UIService.printOutput(filteredOutput, chalk.cyan); - } - UIService.printOutput(execError.stderr, chalk.red); - } - - throw error; - } - } - - /** - * For testing - */ - get testFlags(): string { - return this.flags; - } - get testTargetDir(): string | undefined { - return this.targetDir; - } - get testVersion(): string | undefined { - return this.version; - } -} diff --git a/compact/src/runBuilder.ts b/compact/src/runBuilder.ts deleted file mode 100644 index ebcf5a8f..00000000 --- a/compact/src/runBuilder.ts +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node - -import chalk from 'chalk'; -import ora from 'ora'; -import { CompactBuilder } from './Builder.js'; - -/** - * Executes the Compact builder CLI. - * Builds projects using the `CompactBuilder` class with provided flags, including compilation and additional steps. - * - * @example - * ```bash - * npx compact-builder --skip-zk - * ``` - * Expected output: - * ``` - * ℹ [BUILD] Compact builder started - * ℹ [COMPILE] COMPACT_HOME: /path/to/compactc - * ℹ [COMPILE] COMPACTC_PATH: /path/to/compactc/compactc - * ℹ [COMPILE] Found 1 .compact file(s) to compile - * ✔ [COMPILE] [1/1] Compiled Foo.compact - * Compactc version: 0.24.0 - * ✔ [BUILD] [1/3] Compiling TypeScript - * ✔ [BUILD] [2/3] Copying artifacts - * ✔ [BUILD] [3/3] Copying and cleaning .compact files - * ``` - */ -async function runBuilder(): Promise { - const spinner = ora(chalk.blue('[BUILD] Compact Builder started')).info(); - - try { - const compilerFlags = process.argv.slice(2).join(' '); - const builder = new CompactBuilder(compilerFlags); - await builder.build(); - } catch (err) { - spinner.fail( - chalk.red('[BUILD] Unexpected error:', (err as Error).message), - ); - process.exit(1); - } -} - -runBuilder(); diff --git a/compact/src/runCompiler.ts b/compact/src/runCompiler.ts deleted file mode 100644 index ce3cdc6c..00000000 --- a/compact/src/runCompiler.ts +++ /dev/null @@ -1,227 +0,0 @@ -#!/usr/bin/env node - -import chalk from 'chalk'; -import ora, { type Ora } from 'ora'; -import { CompactCompiler } from './Compiler.js'; -import { - type CompilationError, - isPromisifiedChildProcessError, -} from './types/errors.js'; - -/** - * Executes the Compact compiler CLI with improved error handling and user feedback. - * - * Error Handling Architecture: - * - * This CLI follows a layered error handling approach: - * - * - Business logic (Compiler.ts) throws structured errors with context. - * - CLI layer (runCompiler.ts) handles all user-facing error presentation. - * - Custom error types (types/errors.ts) provide semantic meaning and context. - * - * Benefits: Better testability, consistent UI, separation of concerns. - * - * Note: This compiler uses fail-fast error handling. - * Compilation stops on the first error encountered. - * This provides immediate feedback but doesn't attempt to compile remaining files after a failure. - * - * @example Individual module compilation - * ```bash - * npx compact-compiler --dir security --skip-zk - * turbo compact:access -- --skip-zk - * turbo compact:security -- --skip-zk --other-flag - * ``` - * - * @example Full compilation with environment variables - * ```bash - * SKIP_ZK=true turbo compact - * turbo compact - * ``` - * - * @example Version specification - * ```bash - * npx compact-compiler --dir security --skip-zk +0.24.0 - * ``` - */ -async function runCompiler(): Promise { - const spinner = ora(chalk.blue('[COMPILE] Compact compiler started')).info(); - - try { - const args = process.argv.slice(2); - const compiler = CompactCompiler.fromArgs(args); - await compiler.compile(); - } catch (error) { - handleError(error, spinner); - process.exit(1); - } -} - -/** - * Centralized error handling with specific error types and user-friendly messages. - * - * Handles different error types with appropriate user feedback: - * - * - `CompactCliNotFoundError`: Shows installation instructions. - * - `DirectoryNotFoundError`: Shows available directories. - * - `CompilationError`: Shows file-specific error details with context. - * - Environment validation errors: Shows troubleshooting tips. - * - Argument parsing errors: Shows usage help. - * - Generic errors: Shows general troubleshooting guidance. - * - * @param error - The error that occurred during compilation - * @param spinner - Ora spinner instance for consistent UI messaging - */ -function handleError(error: unknown, spinner: Ora): void { - // CompactCliNotFoundError - if (error instanceof Error && error.name === 'CompactCliNotFoundError') { - spinner.fail(chalk.red(`[COMPILE] Error: ${error.message}`)); - spinner.info( - chalk.blue( - `[COMPILE] Install with: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/midnightntwrk/compact/releases/latest/download/compact-installer.sh | sh`, - ), - ); - return; - } - - // DirectoryNotFoundError - if (error instanceof Error && error.name === 'DirectoryNotFoundError') { - spinner.fail(chalk.red(`[COMPILE] Error: ${error.message}`)); - showAvailableDirectories(); - return; - } - - // CompilationError - if (error instanceof Error && error.name === 'CompilationError') { - // The compilation error details (file name, stdout/stderr) are already displayed - // by `compileFile`; therefore, this just handles the final err state - const compilationError = error as CompilationError; - spinner.fail( - chalk.red( - `[COMPILE] Compilation failed for file: ${compilationError.file || 'unknown'}`, - ), - ); - - if (isPromisifiedChildProcessError(compilationError.cause)) { - const execError = compilationError.cause; - if ( - execError.stderr && - !execError.stderr.includes('stdout') && - !execError.stderr.includes('stderr') - ) { - console.log( - chalk.red(` Additional error details: ${execError.stderr}`), - ); - } - } - return; - } - - // Env validation errors (non-CLI errors) - if (isPromisifiedChildProcessError(error)) { - spinner.fail( - chalk.red(`[COMPILE] Environment validation failed: ${error.message}`), - ); - console.log(chalk.gray('\nTroubleshooting:')); - console.log( - chalk.gray(' • Check that Compact CLI is installed and in PATH'), - ); - console.log(chalk.gray(' • Verify the specified Compact version exists')); - console.log(chalk.gray(' • Ensure you have proper permissions')); - return; - } - - // Arg parsing - const errorMessage = error instanceof Error ? error.message : String(error); - if (errorMessage.includes('--dir flag requires a directory name')) { - spinner.fail( - chalk.red('[COMPILE] Error: --dir flag requires a directory name'), - ); - showUsageHelp(); - return; - } - - // Unexpected errors - spinner.fail(chalk.red(`[COMPILE] Unexpected error: ${errorMessage}`)); - console.log(chalk.gray('\nIf this error persists, please check:')); - console.log(chalk.gray(' • Compact CLI is installed and in PATH')); - console.log(chalk.gray(' • Source files exist and are readable')); - console.log(chalk.gray(' • Specified Compact version exists')); - console.log(chalk.gray(' • File system permissions are correct')); -} - -/** - * Shows available directories when `DirectoryNotFoundError` occurs. - */ -function showAvailableDirectories(): void { - console.log(chalk.yellow('\nAvailable directories:')); - console.log( - chalk.yellow(' --dir access # Compile access control contracts'), - ); - console.log(chalk.yellow(' --dir archive # Compile archive contracts')); - console.log(chalk.yellow(' --dir security # Compile security contracts')); - console.log(chalk.yellow(' --dir token # Compile token contracts')); - console.log(chalk.yellow(' --dir utils # Compile utility contracts')); -} - -/** - * Shows usage help with examples for different scenarios. - */ -function showUsageHelp(): void { - console.log(chalk.yellow('\nUsage: compact-compiler [options]')); - console.log(chalk.yellow('\nOptions:')); - console.log( - chalk.yellow( - ' --dir Compile specific directory (access, archive, security, token, utils)', - ), - ); - console.log( - chalk.yellow(' --skip-zk Skip zero-knowledge proof generation'), - ); - console.log( - chalk.yellow( - ' + Use specific toolchain version (e.g., +0.24.0)', - ), - ); - console.log(chalk.yellow('\nExamples:')); - console.log( - chalk.yellow( - ' compact-compiler # Compile all files', - ), - ); - console.log( - chalk.yellow( - ' compact-compiler --dir security # Compile security directory', - ), - ); - console.log( - chalk.yellow( - ' compact-compiler --dir access --skip-zk # Compile access with flags', - ), - ); - console.log( - chalk.yellow( - ' SKIP_ZK=true compact-compiler --dir token # Use environment variable', - ), - ); - console.log( - chalk.yellow( - ' compact-compiler --skip-zk +0.24.0 # Use specific version', - ), - ); - console.log(chalk.yellow('\nTurbo integration:')); - console.log( - chalk.yellow(' turbo compact # Full build'), - ); - console.log( - chalk.yellow( - ' turbo compact:security -- --skip-zk # Directory with flags', - ), - ); - console.log( - chalk.yellow( - ' SKIP_ZK=true turbo compact # Environment variables', - ), - ); -} - -runCompiler(); diff --git a/compact/src/types/errors.ts b/compact/src/types/errors.ts deleted file mode 100644 index 877a61f2..00000000 --- a/compact/src/types/errors.ts +++ /dev/null @@ -1,97 +0,0 @@ -/** - * A custom error that describes the shape of an error returned from a promisfied - * child_process.exec - * - * @interface PromisifiedChildProcessError - * @typedef {PromisifiedChildProcessError} - * @extends {Error} - * - * @prop {string} stdout stdout of a child process - * @prop {string} stderr stderr of a child process - */ -export interface PromisifiedChildProcessError extends Error { - stdout: string; - stderr: string; -} - -/** - * A type guard function for PromisifiedChildProcessError - * - * @param {unknown} error - An error caught in a try catch block - * @returns {error is PromisifiedChildProcessError} - Informs TS compiler if the understood - * type is a PromisifiedChildProcessError - */ -export function isPromisifiedChildProcessError( - error: unknown, -): error is PromisifiedChildProcessError { - return error instanceof Error && 'stdout' in error && 'stderr' in error; -} - -/** - * Custom error thrown when the Compact CLI is not found in the system PATH. - * This error indicates that the Compact developer tools are not installed - * or not properly configured in the environment. - * - * @class CompactCliNotFoundError - * @extends Error - */ -export class CompactCliNotFoundError extends Error { - /** - * Creates a new CompactCliNotFoundError instance. - * - * @param message - Error message describing the CLI availability issue - */ - constructor(message: string) { - super(message); - this.name = 'CompactCliNotFoundError'; - } -} - -/** - * Custom error thrown when compilation of a .compact file fails. - * Contains additional context about which file failed to compile, - * making it easier to identify and debug compilation issues. - * - * @class CompilationError - * @extends Error - */ -export class CompilationError extends Error { - public readonly file?: string; - - /** - * Creates a new CompilationError instance. - * - * @param message - Error message describing the compilation failure - * @param file - Optional relative path to the file that failed to compile - */ - constructor(message: string, file?: string, cause?: unknown) { - super(message, { cause }); - - this.file = file; - this.name = 'CompilationError'; - } -} - -/** - * Custom error thrown when a specified target directory does not exist. - * Provides specific information about which directory was not found, - * helping users correct path-related issues. - * - * @class DirectoryNotFoundError - * @extends Error - */ -export class DirectoryNotFoundError extends Error { - public readonly directory: string; - - /** - * Creates a new DirectoryNotFoundError instance. - * - * @param message - Error message describing the directory issue - * @param directory - The directory path that was not found - */ - constructor(message: string, directory: string) { - super(message); - this.directory = directory; - this.name = 'DirectoryNotFoundError'; - } -} diff --git a/compact/test/Compiler.test.ts b/compact/test/Compiler.test.ts deleted file mode 100644 index 115c8a4d..00000000 --- a/compact/test/Compiler.test.ts +++ /dev/null @@ -1,880 +0,0 @@ -import { existsSync } from 'node:fs'; -import { readdir } from 'node:fs/promises'; -import { - beforeEach, - describe, - expect, - it, - type MockedFunction, - vi, -} from 'vitest'; -import { - CompactCompiler, - CompilerService, - EnvironmentValidator, - type ExecFunction, - FileDiscovery, - UIService, -} from '../src/Compiler.js'; -import { - CompactCliNotFoundError, - CompilationError, - DirectoryNotFoundError, -} from '../src/types/errors.js'; - -// Mock Node.js modules -vi.mock('node:fs'); -vi.mock('node:fs/promises'); -vi.mock('chalk', () => ({ - default: { - blue: (text: string) => text, - green: (text: string) => text, - red: (text: string) => text, - yellow: (text: string) => text, - cyan: (text: string) => text, - gray: (text: string) => text, - }, -})); - -// Mock spinner -const mockSpinner = { - start: () => ({ succeed: vi.fn(), fail: vi.fn(), text: '' }), - info: vi.fn(), - warn: vi.fn(), - fail: vi.fn(), - succeed: vi.fn(), -}; - -vi.mock('ora', () => ({ - default: () => mockSpinner, -})); - -const mockExistsSync = vi.mocked(existsSync); -const mockReaddir = vi.mocked(readdir); - -describe('EnvironmentValidator', () => { - let mockExec: MockedFunction; - let validator: EnvironmentValidator; - - beforeEach(() => { - vi.clearAllMocks(); - mockExec = vi.fn(); - validator = new EnvironmentValidator(mockExec); - }); - - describe('checkCompactAvailable', () => { - it('should return true when compact CLI is available', async () => { - mockExec.mockResolvedValue({ stdout: 'compact 0.1.0', stderr: '' }); - - const result = await validator.checkCompactAvailable(); - - expect(result).toBe(true); - expect(mockExec).toHaveBeenCalledWith('compact --version'); - }); - - it('should return false when compact CLI is not available', async () => { - mockExec.mockRejectedValue(new Error('Command not found')); - - const result = await validator.checkCompactAvailable(); - - expect(result).toBe(false); - expect(mockExec).toHaveBeenCalledWith('compact --version'); - }); - }); - - describe('getDevToolsVersion', () => { - it('should return trimmed version string', async () => { - mockExec.mockResolvedValue({ stdout: ' compact 0.1.0 \n', stderr: '' }); - - const version = await validator.getDevToolsVersion(); - - expect(version).toBe('compact 0.1.0'); - expect(mockExec).toHaveBeenCalledWith('compact --version'); - }); - - it('should throw error when command fails', async () => { - mockExec.mockRejectedValue(new Error('Command failed')); - - await expect(validator.getDevToolsVersion()).rejects.toThrow( - 'Command failed', - ); - }); - }); - - describe('getToolchainVersion', () => { - it('should get version without specific version flag', async () => { - mockExec.mockResolvedValue({ - stdout: 'Compactc version: 0.24.0', - stderr: '', - }); - - const version = await validator.getToolchainVersion(); - - expect(version).toBe('Compactc version: 0.24.0'); - expect(mockExec).toHaveBeenCalledWith('compact compile --version'); - }); - - it('should get version with specific version flag', async () => { - mockExec.mockResolvedValue({ - stdout: 'Compactc version: 0.24.0', - stderr: '', - }); - - const version = await validator.getToolchainVersion('0.24.0'); - - expect(version).toBe('Compactc version: 0.24.0'); - expect(mockExec).toHaveBeenCalledWith( - 'compact compile +0.24.0 --version', - ); - }); - }); - - describe('validate', () => { - it('should validate successfully when CLI is available', async () => { - mockExec.mockResolvedValue({ stdout: 'compact 0.1.0', stderr: '' }); - - await expect(validator.validate()).resolves.not.toThrow(); - }); - - it('should throw CompactCliNotFoundError when CLI is not available', async () => { - mockExec.mockRejectedValue(new Error('Command not found')); - - await expect(validator.validate()).rejects.toThrow( - CompactCliNotFoundError, - ); - }); - }); -}); - -describe('FileDiscovery', () => { - let discovery: FileDiscovery; - - beforeEach(() => { - vi.clearAllMocks(); - discovery = new FileDiscovery(); - }); - - describe('getCompactFiles', () => { - it('should find .compact files in directory', async () => { - const mockDirents = [ - { - name: 'MyToken.compact', - isFile: () => true, - isDirectory: () => false, - }, - { - name: 'Ownable.compact', - isFile: () => true, - isDirectory: () => false, - }, - { name: 'README.md', isFile: () => true, isDirectory: () => false }, - { name: 'utils', isFile: () => false, isDirectory: () => true }, - ]; - - mockReaddir - .mockResolvedValueOnce(mockDirents as any) - .mockResolvedValueOnce([ - { - name: 'Utils.compact', - isFile: () => true, - isDirectory: () => false, - }, - ] as any); - - const files = await discovery.getCompactFiles('src'); - - expect(files).toEqual([ - 'MyToken.compact', - 'Ownable.compact', - 'utils/Utils.compact', - ]); - }); - - it('should handle empty directories', async () => { - mockReaddir.mockResolvedValue([]); - - const files = await discovery.getCompactFiles('src'); - - expect(files).toEqual([]); - }); - - it('should handle directory read errors gracefully', async () => { - const consoleSpy = vi - .spyOn(console, 'error') - .mockImplementation(() => {}); - - mockReaddir.mockRejectedValueOnce(new Error('Permission denied')); - - const files = await discovery.getCompactFiles('src'); - - expect(files).toEqual([]); - expect(consoleSpy).toHaveBeenCalledWith( - 'Failed to read dir: src', - expect.any(Error), - ); - consoleSpy.mockRestore(); - }); - - it('should handle file access errors gracefully', async () => { - const mockDirents = [ - { - name: 'MyToken.compact', - isFile: () => { - throw new Error('Access denied'); - }, - isDirectory: () => false, - }, - { - name: 'Ownable.compact', - isFile: () => true, - isDirectory: () => false, - }, - ]; - - mockReaddir.mockResolvedValue(mockDirents as any); - - const files = await discovery.getCompactFiles('src'); - - expect(files).toEqual(['Ownable.compact']); - }); - }); -}); - -describe('CompilerService', () => { - let mockExec: MockedFunction; - let service: CompilerService; - - beforeEach(() => { - vi.clearAllMocks(); - mockExec = vi.fn(); - service = new CompilerService(mockExec); - }); - - describe('compileFile', () => { - it('should compile file successfully with basic flags', async () => { - mockExec.mockResolvedValue({ - stdout: 'Compilation successful', - stderr: '', - }); - - const result = await service.compileFile('MyToken.compact', '--skip-zk'); - - expect(result).toEqual({ stdout: 'Compilation successful', stderr: '' }); - expect(mockExec).toHaveBeenCalledWith( - 'compact compile --skip-zk "src/MyToken.compact" "artifacts/MyToken"', - ); - }); - - it('should compile file with version flag', async () => { - mockExec.mockResolvedValue({ - stdout: 'Compilation successful', - stderr: '', - }); - - const result = await service.compileFile( - 'MyToken.compact', - '--skip-zk', - '0.24.0', - ); - - expect(result).toEqual({ stdout: 'Compilation successful', stderr: '' }); - expect(mockExec).toHaveBeenCalledWith( - 'compact compile +0.24.0 --skip-zk "src/MyToken.compact" "artifacts/MyToken"', - ); - }); - - it('should handle empty flags', async () => { - mockExec.mockResolvedValue({ - stdout: 'Compilation successful', - stderr: '', - }); - - const result = await service.compileFile('MyToken.compact', ''); - - expect(result).toEqual({ stdout: 'Compilation successful', stderr: '' }); - expect(mockExec).toHaveBeenCalledWith( - 'compact compile "src/MyToken.compact" "artifacts/MyToken"', - ); - }); - - it('should throw CompilationError when compilation fails', async () => { - mockExec.mockRejectedValue(new Error('Syntax error on line 10')); - - await expect( - service.compileFile('MyToken.compact', '--skip-zk'), - ).rejects.toThrow(CompilationError); - }); - - it('should include file path in CompilationError', async () => { - mockExec.mockRejectedValue(new Error('Syntax error')); - - try { - await service.compileFile('MyToken.compact', '--skip-zk'); - } catch (error) { - expect(error).toBeInstanceOf(CompilationError); - expect((error as CompilationError).file).toBe('MyToken.compact'); - } - }); - - it('should include cause in CompilationError', async () => { - const mockError = new Error('Syntax error'); - mockExec.mockRejectedValue(mockError); - - try { - await service.compileFile('MyToken.compact', '--skip-zk'); - } catch (error) { - expect(error).toBeInstanceOf(CompilationError); - expect((error as CompilationError).cause).toEqual(mockError); - } - }); - }); -}); - -describe('UIService', () => { - beforeEach(() => { - vi.clearAllMocks(); - vi.spyOn(console, 'log').mockImplementation(() => {}); - }); - - describe('printOutput', () => { - it('should format output with indentation', () => { - const mockColorFn = vi.fn((text: string) => `colored(${text})`); - - UIService.printOutput('line 1\nline 2\n\nline 3', mockColorFn); - - expect(mockColorFn).toHaveBeenCalledWith( - ' line 1\n line 2\n line 3', - ); - expect(console.log).toHaveBeenCalledWith( - 'colored( line 1\n line 2\n line 3)', - ); - }); - - it('should handle empty output', () => { - const mockColorFn = vi.fn((text: string) => `colored(${text})`); - - UIService.printOutput('', mockColorFn); - - expect(mockColorFn).toHaveBeenCalledWith(''); - expect(console.log).toHaveBeenCalledWith('colored()'); - }); - }); - - describe('displayEnvInfo', () => { - it('should display environment information with all parameters', () => { - UIService.displayEnvInfo( - 'compact 0.1.0', - 'Compactc 0.24.0', - 'security', - '0.24.0', - ); - - expect(mockSpinner.info).toHaveBeenCalledWith( - '[COMPILE] TARGET_DIR: security', - ); - expect(mockSpinner.info).toHaveBeenCalledWith( - '[COMPILE] Compact developer tools: compact 0.1.0', - ); - expect(mockSpinner.info).toHaveBeenCalledWith( - '[COMPILE] Compact toolchain: Compactc 0.24.0', - ); - expect(mockSpinner.info).toHaveBeenCalledWith( - '[COMPILE] Using toolchain version: 0.24.0', - ); - }); - - it('should display environment information without optional parameters', () => { - UIService.displayEnvInfo('compact 0.1.0', 'Compactc 0.24.0'); - - expect(mockSpinner.info).toHaveBeenCalledWith( - '[COMPILE] Compact developer tools: compact 0.1.0', - ); - expect(mockSpinner.info).toHaveBeenCalledWith( - '[COMPILE] Compact toolchain: Compactc 0.24.0', - ); - expect(mockSpinner.info).not.toHaveBeenCalledWith( - expect.stringContaining('TARGET_DIR'), - ); - expect(mockSpinner.info).not.toHaveBeenCalledWith( - expect.stringContaining('Using toolchain version'), - ); - }); - }); - - describe('showCompilationStart', () => { - it('should show file count without target directory', () => { - UIService.showCompilationStart(5); - - expect(mockSpinner.info).toHaveBeenCalledWith( - '[COMPILE] Found 5 .compact file(s) to compile', - ); - }); - - it('should show file count with target directory', () => { - UIService.showCompilationStart(3, 'security'); - - expect(mockSpinner.info).toHaveBeenCalledWith( - '[COMPILE] Found 3 .compact file(s) to compile in security/', - ); - }); - }); - - describe('showNoFiles', () => { - it('should show no files message with target directory', () => { - UIService.showNoFiles('security'); - - expect(mockSpinner.warn).toHaveBeenCalledWith( - '[COMPILE] No .compact files found in security/.', - ); - }); - - it('should show no files message without target directory', () => { - UIService.showNoFiles(); - - expect(mockSpinner.warn).toHaveBeenCalledWith( - '[COMPILE] No .compact files found in .', - ); - }); - }); -}); - -describe('CompactCompiler', () => { - let mockExec: MockedFunction; - let compiler: CompactCompiler; - - beforeEach(() => { - vi.clearAllMocks(); - mockExec = vi.fn().mockResolvedValue({ stdout: 'success', stderr: '' }); - mockExistsSync.mockReturnValue(true); - mockReaddir.mockResolvedValue([]); - }); - - describe('constructor', () => { - it('should create instance with default parameters', () => { - compiler = new CompactCompiler(); - - expect(compiler).toBeInstanceOf(CompactCompiler); - }); - - it('should create instance with all parameters', () => { - compiler = new CompactCompiler( - '--skip-zk', - 'security', - '0.24.0', - mockExec, - ); - - expect(compiler).toBeInstanceOf(CompactCompiler); - }); - - it('should trim flags', () => { - compiler = new CompactCompiler(' --skip-zk --verbose '); - expect(compiler.testFlags).toBe('--skip-zk --verbose'); - }); - }); - - describe('fromArgs', () => { - it('should parse empty arguments', () => { - compiler = CompactCompiler.fromArgs([]); - - expect(compiler.testFlags).toBe(''); - expect(compiler.testTargetDir).toBeUndefined(); - expect(compiler.testVersion).toBeUndefined(); - }); - - it('should handle SKIP_ZK environment variable', () => { - compiler = CompactCompiler.fromArgs([], { SKIP_ZK: 'true' }); - - expect(compiler.testFlags).toBe('--skip-zk'); - }); - - it('should ignore SKIP_ZK when not "true"', () => { - compiler = CompactCompiler.fromArgs([], { SKIP_ZK: 'false' }); - - expect(compiler.testFlags).toBe(''); - }); - - it('should parse --dir flag', () => { - compiler = CompactCompiler.fromArgs(['--dir', 'security']); - - expect(compiler.testTargetDir).toBe('security'); - expect(compiler.testFlags).toBe(''); - }); - - it('should parse --dir flag with additional flags', () => { - compiler = CompactCompiler.fromArgs([ - '--dir', - 'security', - '--skip-zk', - '--verbose', - ]); - - expect(compiler.testTargetDir).toBe('security'); - expect(compiler.testFlags).toBe('--skip-zk --verbose'); - }); - - it('should parse version flag', () => { - compiler = CompactCompiler.fromArgs(['+0.24.0']); - - expect(compiler.testVersion).toBe('0.24.0'); - expect(compiler.testFlags).toBe(''); - }); - - it('should parse complex arguments', () => { - compiler = CompactCompiler.fromArgs([ - '--dir', - 'security', - '--skip-zk', - '--verbose', - '+0.24.0', - ]); - - expect(compiler.testTargetDir).toBe('security'); - expect(compiler.testFlags).toBe('--skip-zk --verbose'); - expect(compiler.testVersion).toBe('0.24.0'); - }); - - it('should combine environment variables with CLI flags', () => { - compiler = CompactCompiler.fromArgs(['--dir', 'access', '--verbose'], { - SKIP_ZK: 'true', - }); - - expect(compiler.testTargetDir).toBe('access'); - expect(compiler.testFlags).toBe('--skip-zk --verbose'); - }); - - it('should deduplicate flags when both env var and CLI flag are present', () => { - compiler = CompactCompiler.fromArgs(['--skip-zk', '--verbose'], { - SKIP_ZK: 'true', - }); - - expect(compiler.testFlags).toBe('--skip-zk --verbose'); - }); - - it('should throw error for --dir without argument', () => { - expect(() => CompactCompiler.fromArgs(['--dir'])).toThrow( - '--dir flag requires a directory name', - ); - }); - - it('should throw error for --dir followed by another flag', () => { - expect(() => CompactCompiler.fromArgs(['--dir', '--skip-zk'])).toThrow( - '--dir flag requires a directory name', - ); - }); - }); - - describe('validateEnvironment', () => { - it('should validate successfully and display environment info', async () => { - mockExec - .mockResolvedValueOnce({ stdout: 'compact 0.1.0', stderr: '' }) // checkCompactAvailable - .mockResolvedValueOnce({ stdout: 'compact 0.1.0', stderr: '' }) // getDevToolsVersion - .mockResolvedValueOnce({ - stdout: 'Compactc version: 0.24.0', - stderr: '', - }); // getToolchainVersion - - compiler = new CompactCompiler( - '--skip-zk', - 'security', - '0.24.0', - mockExec, - ); - const displaySpy = vi - .spyOn(UIService, 'displayEnvInfo') - .mockImplementation(() => {}); - - await expect(compiler.validateEnvironment()).resolves.not.toThrow(); - - // Check steps - expect(mockExec).toHaveBeenCalledTimes(3); - expect(mockExec).toHaveBeenNthCalledWith(1, 'compact --version'); // validate() calls - expect(mockExec).toHaveBeenNthCalledWith(2, 'compact --version'); // getDevToolsVersion() - expect(mockExec).toHaveBeenNthCalledWith( - 3, - 'compact compile +0.24.0 --version', - ); // getToolchainVersion() - - // Verify passed args - expect(displaySpy).toHaveBeenCalledWith( - 'compact 0.1.0', - 'Compactc version: 0.24.0', - 'security', - '0.24.0', - ); - - displaySpy.mockRestore(); - }); - - it('should handle CompactCliNotFoundError with installation instructions', async () => { - mockExec.mockRejectedValue(new Error('Command not found')); - compiler = new CompactCompiler('', undefined, undefined, mockExec); - - await expect(compiler.validateEnvironment()).rejects.toThrow( - CompactCliNotFoundError, - ); - }); - - it('should handle version retrieval failures after successful CLI check', async () => { - mockExec - .mockResolvedValueOnce({ stdout: 'compact 0.1.0', stderr: '' }) // validate() succeeds - .mockRejectedValueOnce(new Error('Version command failed')); // getDevToolsVersion() fails - - compiler = new CompactCompiler('', undefined, undefined, mockExec); - - await expect(compiler.validateEnvironment()).rejects.toThrow( - 'Version command failed', - ); - }); - - it('should handle PromisifiedChildProcessError specifically', async () => { - const childProcessError = new Error('Command execution failed') as any; - childProcessError.stdout = 'some output'; - childProcessError.stderr = 'some error'; - - mockExec.mockRejectedValue(childProcessError); - compiler = new CompactCompiler('', undefined, undefined, mockExec); - - await expect(compiler.validateEnvironment()).rejects.toThrow( - "'compact' CLI not found in PATH. Please install the Compact developer tools.", - ); - }); - - it('should handle non-Error exceptions gracefully', async () => { - mockExec.mockRejectedValue('String error message'); - compiler = new CompactCompiler('', undefined, undefined, mockExec); - - await expect(compiler.validateEnvironment()).rejects.toThrow( - CompactCliNotFoundError, - ); - }); - - it('should validate with specific version flag', async () => { - mockExec - .mockResolvedValueOnce({ stdout: 'compact 0.1.0', stderr: '' }) - .mockResolvedValueOnce({ stdout: 'compact 0.1.0', stderr: '' }) - .mockResolvedValueOnce({ - stdout: 'Compactc version: 0.25.0', - stderr: '', - }); - - compiler = new CompactCompiler('', undefined, '0.25.0', mockExec); - const displaySpy = vi - .spyOn(UIService, 'displayEnvInfo') - .mockImplementation(() => {}); - - await compiler.validateEnvironment(); - - // Verify version-specific toolchain call - expect(mockExec).toHaveBeenNthCalledWith( - 3, - 'compact compile +0.25.0 --version', - ); - expect(displaySpy).toHaveBeenCalledWith( - 'compact 0.1.0', - 'Compactc version: 0.25.0', - undefined, // no targetDir - '0.25.0', - ); - - displaySpy.mockRestore(); - }); - - it('should validate without target directory or version', async () => { - mockExec - .mockResolvedValueOnce({ stdout: 'compact 0.1.0', stderr: '' }) - .mockResolvedValueOnce({ stdout: 'compact 0.1.0', stderr: '' }) - .mockResolvedValueOnce({ - stdout: 'Compactc version: 0.24.0', - stderr: '', - }); - - compiler = new CompactCompiler('', undefined, undefined, mockExec); - const displaySpy = vi - .spyOn(UIService, 'displayEnvInfo') - .mockImplementation(() => {}); - - await compiler.validateEnvironment(); - - // Verify default toolchain call (no version flag) - expect(mockExec).toHaveBeenNthCalledWith(3, 'compact compile --version'); - expect(displaySpy).toHaveBeenCalledWith( - 'compact 0.1.0', - 'Compactc version: 0.24.0', - undefined, - undefined, - ); - - displaySpy.mockRestore(); - }); - }); - - describe('compile', () => { - it('should handle empty source directory', async () => { - mockReaddir.mockResolvedValue([]); - compiler = new CompactCompiler('', undefined, undefined, mockExec); - - await expect(compiler.compile()).resolves.not.toThrow(); - }); - - it('should throw error if target directory does not exist', async () => { - mockExistsSync.mockReturnValue(false); - compiler = new CompactCompiler('', 'nonexistent', undefined, mockExec); - - await expect(compiler.compile()).rejects.toThrow(DirectoryNotFoundError); - }); - - it('should compile files successfully', async () => { - const mockDirents = [ - { - name: 'MyToken.compact', - isFile: () => true, - isDirectory: () => false, - }, - { - name: 'Ownable.compact', - isFile: () => true, - isDirectory: () => false, - }, - ]; - mockReaddir.mockResolvedValue(mockDirents as any); - compiler = new CompactCompiler( - '--skip-zk', - undefined, - undefined, - mockExec, - ); - - await compiler.compile(); - - expect(mockExec).toHaveBeenCalledWith( - expect.stringContaining('compact compile --skip-zk'), - ); - }); - - it('should handle compilation errors gracefully', async () => { - const brokenDirent = { - name: 'Broken.compact', - isFile: () => true, - isDirectory: () => false, - }; - - const mockDirents = [brokenDirent]; - mockReaddir.mockResolvedValue(mockDirents as any); - mockExistsSync.mockReturnValue(true); - - const testMockExec = vi - .fn() - .mockResolvedValueOnce({ stdout: 'compact 0.1.0', stderr: '' }) // checkCompactAvailable - .mockResolvedValueOnce({ stdout: 'compact 0.1.0', stderr: '' }) // getDevToolsVersion - .mockResolvedValueOnce({ stdout: 'Compactc 0.24.0', stderr: '' }) // getToolchainVersion - .mockRejectedValueOnce(new Error('Compilation failed')); // compileFile execution - - compiler = new CompactCompiler('', undefined, undefined, testMockExec); - - // Test that compilation errors are properly propagated - let thrownError: unknown; - try { - await compiler.compile(); - expect.fail('Expected compilation to throw an error'); - } catch (error) { - thrownError = error; - } - - expect(thrownError).toBeInstanceOf(Error); - expect((thrownError as Error).message).toBe( - `Failed to compile ${brokenDirent.name}: Compilation failed`, - ); - expect(testMockExec).toHaveBeenCalledTimes(4); - }); - }); - - describe('Real-world scenarios', () => { - beforeEach(() => { - const mockDirents = [ - { - name: 'AccessControl.compact', - isFile: () => true, - isDirectory: () => false, - }, - ]; - mockReaddir.mockResolvedValue(mockDirents as any); - }); - - it('should handle turbo compact command', () => { - compiler = CompactCompiler.fromArgs([]); - - expect(compiler.testFlags).toBe(''); - expect(compiler.testTargetDir).toBeUndefined(); - }); - - it('should handle SKIP_ZK=true turbo compact command', () => { - compiler = CompactCompiler.fromArgs([], { SKIP_ZK: 'true' }); - - expect(compiler.testFlags).toBe('--skip-zk'); - }); - - it('should handle turbo compact:access command', () => { - compiler = CompactCompiler.fromArgs(['--dir', 'access']); - - expect(compiler.testFlags).toBe(''); - expect(compiler.testTargetDir).toBe('access'); - }); - - it('should handle turbo compact:security -- --skip-zk command', () => { - compiler = CompactCompiler.fromArgs(['--dir', 'security', '--skip-zk']); - - expect(compiler.testFlags).toBe('--skip-zk'); - expect(compiler.testTargetDir).toBe('security'); - }); - - it('should handle version specification', () => { - compiler = CompactCompiler.fromArgs(['+0.24.0']); - - expect(compiler.testVersion).toBe('0.24.0'); - }); - - it.each([ - { - name: 'with skip zk env var only', - args: [ - '--dir', - 'security', - '--no-communications-commitment', - '+0.24.0', - ], - env: { SKIP_ZK: 'true' }, - }, - { - name: 'with skip-zk flag only', - args: [ - '--dir', - 'security', - '--skip-zk', - '--no-communications-commitment', - '+0.24.0', - ], - env: { SKIP_ZK: 'false' }, - }, - { - name: 'with both skip-zk flag and env var', - args: [ - '--dir', - 'security', - '--skip-zk', - '--no-communications-commitment', - '+0.24.0', - ], - env: { SKIP_ZK: 'true' }, - }, - ])('should handle complex command $name', ({ args, env }) => { - compiler = CompactCompiler.fromArgs(args, env); - - expect(compiler.testFlags).toBe( - '--skip-zk --no-communications-commitment', - ); - expect(compiler.testTargetDir).toBe('security'); - expect(compiler.testVersion).toBe('0.24.0'); - }); - }); -}); diff --git a/compact/test/runCompiler.test.ts b/compact/test/runCompiler.test.ts deleted file mode 100644 index f4bbefed..00000000 --- a/compact/test/runCompiler.test.ts +++ /dev/null @@ -1,478 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { CompactCompiler } from '../src/Compiler.js'; -import { - CompactCliNotFoundError, - CompilationError, - DirectoryNotFoundError, - isPromisifiedChildProcessError, - type PromisifiedChildProcessError, -} from '../src/types/errors.js'; - -// Mock CompactCompiler -vi.mock('../src/Compiler.js', () => ({ - CompactCompiler: { - fromArgs: vi.fn(), - }, -})); - -// Mock error utilities -vi.mock('../src/types/errors.js', async () => { - const actual = await vi.importActual('../src/types/errors.js'); - return { - ...actual, - isPromisifiedChildProcessError: vi.fn(), - }; -}); - -// Mock chalk -vi.mock('chalk', () => ({ - default: { - blue: (text: string) => text, - red: (text: string) => text, - yellow: (text: string) => text, - gray: (text: string) => text, - }, -})); - -// Mock ora -const mockSpinner = { - info: vi.fn().mockReturnThis(), - fail: vi.fn().mockReturnThis(), - succeed: vi.fn().mockReturnThis(), -}; -vi.mock('ora', () => ({ - default: vi.fn(() => mockSpinner), -})); - -// Mock process.exit -const mockExit = vi - .spyOn(process, 'exit') - .mockImplementation(() => undefined as never); - -// Mock console methods -const mockConsoleLog = vi.spyOn(console, 'log').mockImplementation(() => {}); - -describe('runCompiler CLI', () => { - let mockCompile: ReturnType; - let mockFromArgs: ReturnType; - let originalArgv: string[]; - - beforeEach(() => { - // Store original argv - originalArgv = [...process.argv]; - - vi.clearAllMocks(); - vi.resetModules(); - - mockCompile = vi.fn(); - mockFromArgs = vi.mocked(CompactCompiler.fromArgs); - - // Mock CompactCompiler instance - mockFromArgs.mockReturnValue({ - compile: mockCompile, - } as any); - - // Clear all mock calls - mockSpinner.info.mockClear(); - mockSpinner.fail.mockClear(); - mockSpinner.succeed.mockClear(); - mockConsoleLog.mockClear(); - mockExit.mockClear(); - }); - - afterEach(() => { - // Restore original argv - process.argv = originalArgv; - }); - - describe('successful compilation', () => { - it('should compile successfully with no arguments', async () => { - mockCompile.mockResolvedValue(undefined); - - // Import and run the CLI - await import('../src/runCompiler.js'); - - expect(mockFromArgs).toHaveBeenCalledWith([]); - expect(mockCompile).toHaveBeenCalled(); - expect(mockExit).not.toHaveBeenCalled(); - }); - - it('should compile successfully with arguments', async () => { - process.argv = [ - 'node', - 'runCompiler.js', - '--dir', - 'security', - '--skip-zk', - ]; - mockCompile.mockResolvedValue(undefined); - - await import('../src/runCompiler.js'); - - expect(mockFromArgs).toHaveBeenCalledWith([ - '--dir', - 'security', - '--skip-zk', - ]); - expect(mockCompile).toHaveBeenCalled(); - expect(mockExit).not.toHaveBeenCalled(); - }); - }); - - describe('error handling', () => { - it('should handle CompactCliNotFoundError with installation instructions', async () => { - const error = new CompactCliNotFoundError('CLI not found'); - mockCompile.mockRejectedValue(error); - - await import('../src/runCompiler.js'); - - expect(mockSpinner.fail).toHaveBeenCalledWith( - '[COMPILE] Error: CLI not found', - ); - expect(mockSpinner.info).toHaveBeenCalledWith( - "[COMPILE] Install with: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/midnightntwrk/compact/releases/latest/download/compact-installer.sh | sh", - ); - expect(mockExit).toHaveBeenCalledWith(1); - }); - - it('should handle DirectoryNotFoundError with helpful message', async () => { - const error = new DirectoryNotFoundError( - 'Directory not found', - 'src/nonexistent', - ); - mockCompile.mockRejectedValue(error); - - await import('../src/runCompiler.js'); - - expect(mockSpinner.fail).toHaveBeenCalledWith( - '[COMPILE] Error: Directory not found', - ); - expect(mockConsoleLog).toHaveBeenCalledWith('\nAvailable directories:'); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' --dir access # Compile access control contracts', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' --dir archive # Compile archive contracts', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' --dir security # Compile security contracts', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' --dir token # Compile token contracts', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' --dir utils # Compile utility contracts', - ); - expect(mockExit).toHaveBeenCalledWith(1); - }); - - it('should handle CompilationError with file context and cause', async () => { - const mockIsPromisifiedChildProcessError = vi.mocked( - isPromisifiedChildProcessError, - ); - - const childProcessError = { - message: 'Syntax error', - stdout: 'some output', - stderr: 'error details', - }; - - // Return true for this specific error - mockIsPromisifiedChildProcessError.mockImplementation( - (err) => err === childProcessError, - ); - - const error = new CompilationError( - 'Compilation failed', - 'MyToken.compact', - childProcessError, - ); - mockCompile.mockRejectedValue(error); - - await import('../src/runCompiler.js'); - - expect(mockSpinner.fail).toHaveBeenCalledWith( - '[COMPILE] Compilation failed for file: MyToken.compact', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ` Additional error details: ${(error.cause as PromisifiedChildProcessError).stderr}`, - ); - expect(mockExit).toHaveBeenCalledWith(1); - }); - - it('should handle CompilationError with unknown file', async () => { - const error = new CompilationError('Compilation failed'); - mockCompile.mockRejectedValue(error); - - await import('../src/runCompiler.js'); - - expect(mockSpinner.fail).toHaveBeenCalledWith( - '[COMPILE] Compilation failed for file: unknown', - ); - expect(mockExit).toHaveBeenCalledWith(1); - }); - - it('should handle argument parsing errors', async () => { - const error = new Error('--dir flag requires a directory name'); - mockFromArgs.mockImplementation(() => { - throw error; - }); - - await import('../src/runCompiler.js'); - - expect(mockSpinner.fail).toHaveBeenCalledWith( - '[COMPILE] Error: --dir flag requires a directory name', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - '\nUsage: compact-compiler [options]', - ); - expect(mockExit).toHaveBeenCalledWith(1); - }); - - it('should handle unexpected errors', async () => { - const msg = 'Something unexpected happened'; - const error = new Error(msg); - mockCompile.mockRejectedValue(error); - - await import('../src/runCompiler.js'); - - expect(mockSpinner.fail).toHaveBeenCalledWith( - `[COMPILE] Unexpected error: ${msg}`, - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - '\nIf this error persists, please check:', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' • Compact CLI is installed and in PATH', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' • Source files exist and are readable', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' • Specified Compact version exists', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' • File system permissions are correct', - ); - expect(mockExit).toHaveBeenCalledWith(1); - }); - - it('should handle non-Error exceptions', async () => { - const msg = 'String error'; - mockCompile.mockRejectedValue(msg); - - await import('../src/runCompiler.js'); - - expect(mockSpinner.fail).toHaveBeenCalledWith( - `[COMPILE] Unexpected error: ${msg}`, - ); - expect(mockExit).toHaveBeenCalledWith(1); - }); - }); - - describe('environment validation errors', () => { - it('should handle promisified child process errors', async () => { - const mockIsPromisifiedChildProcessError = vi.mocked( - isPromisifiedChildProcessError, - ); - - const error = { - message: 'Command failed', - stdout: 'some output', - stderr: 'error details', - }; - - // Return true for this specific error - mockIsPromisifiedChildProcessError.mockImplementation( - (err) => err === error, - ); - mockCompile.mockRejectedValue(error); - - await import('../src/runCompiler.js'); - - expect(mockIsPromisifiedChildProcessError).toHaveBeenCalledWith(error); - expect(mockSpinner.fail).toHaveBeenCalledWith( - '[COMPILE] Environment validation failed: Command failed', - ); - expect(mockConsoleLog).toHaveBeenCalledWith('\nTroubleshooting:'); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' • Check that Compact CLI is installed and in PATH', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' • Verify the specified Compact version exists', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' • Ensure you have proper permissions', - ); - expect(mockExit).toHaveBeenCalledWith(1); - }); - }); - - describe('usage help', () => { - it('should show complete usage help for argument parsing errors', async () => { - const error = new Error('--dir flag requires a directory name'); - mockFromArgs.mockImplementation(() => { - throw error; - }); - - await import('../src/runCompiler.js'); - - // Verify all sections of help are shown - expect(mockConsoleLog).toHaveBeenCalledWith( - '\nUsage: compact-compiler [options]', - ); - expect(mockConsoleLog).toHaveBeenCalledWith('\nOptions:'); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' --dir Compile specific directory (access, archive, security, token, utils)', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' --skip-zk Skip zero-knowledge proof generation', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' + Use specific toolchain version (e.g., +0.24.0)', - ); - expect(mockConsoleLog).toHaveBeenCalledWith('\nExamples:'); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' compact-compiler # Compile all files', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' compact-compiler --dir security # Compile security directory', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' compact-compiler --dir access --skip-zk # Compile access with flags', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' SKIP_ZK=true compact-compiler --dir token # Use environment variable', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' compact-compiler --skip-zk +0.24.0 # Use specific version', - ); - expect(mockConsoleLog).toHaveBeenCalledWith('\nTurbo integration:'); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' turbo compact # Full build', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' turbo compact:security -- --skip-zk # Directory with flags', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' SKIP_ZK=true turbo compact # Environment variables', - ); - }); - }); - - describe('directory error help', () => { - it('should show all available directories', async () => { - const error = new DirectoryNotFoundError( - 'Directory not found', - 'src/invalid', - ); - mockCompile.mockRejectedValue(error); - - await import('../src/runCompiler.js'); - - expect(mockConsoleLog).toHaveBeenCalledWith('\nAvailable directories:'); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' --dir access # Compile access control contracts', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' --dir archive # Compile archive contracts', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' --dir security # Compile security contracts', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' --dir token # Compile token contracts', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' --dir utils # Compile utility contracts', - ); - }); - }); - - describe('real-world command scenarios', () => { - beforeEach(() => { - mockCompile.mockResolvedValue(undefined); - }); - - it('should handle turbo compact', async () => { - process.argv = ['node', 'runCompiler.js']; - - await import('../src/runCompiler.js'); - - expect(mockFromArgs).toHaveBeenCalledWith([]); - }); - - it('should handle turbo compact:security', async () => { - process.argv = ['node', 'runCompiler.js', '--dir', 'security']; - - await import('../src/runCompiler.js'); - - expect(mockFromArgs).toHaveBeenCalledWith(['--dir', 'security']); - }); - - it('should handle turbo compact:access -- --skip-zk', async () => { - process.argv = ['node', 'runCompiler.js', '--dir', 'access', '--skip-zk']; - - await import('../src/runCompiler.js'); - - expect(mockFromArgs).toHaveBeenCalledWith([ - '--dir', - 'access', - '--skip-zk', - ]); - }); - - it('should handle version specification', async () => { - process.argv = ['node', 'runCompiler.js', '+0.24.0', '--skip-zk']; - - await import('../src/runCompiler.js'); - - expect(mockFromArgs).toHaveBeenCalledWith(['+0.24.0', '--skip-zk']); - }); - - it('should handle complex command', async () => { - process.argv = [ - 'node', - 'runCompiler.js', - '--dir', - 'security', - '--skip-zk', - '--verbose', - '+0.24.0', - ]; - - await import('../src/runCompiler.js'); - - expect(mockFromArgs).toHaveBeenCalledWith([ - '--dir', - 'security', - '--skip-zk', - '--verbose', - '+0.24.0', - ]); - }); - }); - - describe('integration with CompactCompiler', () => { - it('should pass arguments correctly to CompactCompiler.fromArgs', async () => { - const args = ['--dir', 'token', '--skip-zk', '+0.24.0']; - process.argv = ['node', 'runCompiler.js', ...args]; - mockCompile.mockResolvedValue(undefined); - - await import('../src/runCompiler.js'); - - expect(mockFromArgs).toHaveBeenCalledWith(args); - expect(mockFromArgs).toHaveBeenCalledTimes(1); - expect(mockCompile).toHaveBeenCalledTimes(1); - }); - - it('should handle empty arguments', async () => { - process.argv = ['node', 'runCompiler.js']; - mockCompile.mockResolvedValue(undefined); - - await import('../src/runCompiler.js'); - - expect(mockFromArgs).toHaveBeenCalledWith([]); - }); - }); -}); diff --git a/compact/tsconfig.json b/compact/tsconfig.json deleted file mode 100644 index 18a3366a..00000000 --- a/compact/tsconfig.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "compilerOptions": { - "outDir": "dist", - "rootDir": "src", - "declaration": true, - "lib": [ - "es2022" - ], - "module": "nodenext", - "target": "es2022", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "moduleResolution": "nodenext", - "sourceMap": true, - "rewriteRelativeImportExtensions": true, - "erasableSyntaxOnly": true, - "verbatimModuleSyntax": true - }, - "include": [ - "src/**/*" - ], - "exclude": [ - "node_modules", - "dist" - ] -} diff --git a/compact/turbo.json b/compact/turbo.json deleted file mode 100644 index 5d208997..00000000 --- a/compact/turbo.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "$schema": "https://turbo.build/schema.json", - "extends": ["//"], - "tasks": { - "build": { - "outputs": ["dist/**"], - "inputs": ["src/**/*.ts", "tsconfig.json"], - "env": ["COMPACT_HOME"], - "cache": true - } - } -} diff --git a/compact/vitest.config.ts b/compact/vitest.config.ts deleted file mode 100644 index d57e53a7..00000000 --- a/compact/vitest.config.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { defineConfig } from 'vitest/config'; - -export default defineConfig({ - test: { - globals: true, - environment: 'node', - include: ['test/**/*.test.ts'], - reporters: 'verbose', - }, -}); diff --git a/docs/antora.yml b/docs/antora.yml deleted file mode 100644 index c1059d1e..00000000 --- a/docs/antora.yml +++ /dev/null @@ -1,8 +0,0 @@ -name: contracts-compact -title: Contracts for Compact -version: 0.0.1 -nav: - - modules/ROOT/nav.adoc -asciidoc: - attributes: - page-sidebar-collapse-default: 'Access,Security,FungibleToken,NonFungibleToken,MultiToken,Utils' diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc deleted file mode 100644 index 6ae94fde..00000000 --- a/docs/modules/ROOT/nav.adoc +++ /dev/null @@ -1,26 +0,0 @@ -* xref:index.adoc[Overview] - -* Learn - -** xref:zkCircuits101.adoc[ZK Circuits 101] - -** xref:extensibility.adoc[Extensibility] - -* Modules - -** xref:access.adoc[Access] -*** xref:api/access.adoc[API Reference] - -** xref:security.adoc[Security] -*** xref:api/security.adoc[API Reference] - -** Tokens -*** xref:fungibleToken.adoc[FungibleToken] -**** xref:/api/fungibleToken.adoc[API Reference] -*** xref:nonFungibleToken.adoc[NonFungibleToken] -**** xref:/api/nonFungibleToken.adoc[API Reference] -*** xref:multitoken.adoc[MultiToken] -**** xref:api/multitoken.adoc[API Reference] - -** xref:utils.adoc[Utils] -*** xref:api/utils.adoc[API Reference] diff --git a/docs/modules/ROOT/pages/access.adoc b/docs/modules/ROOT/pages/access.adoc deleted file mode 100644 index 7978c1fa..00000000 --- a/docs/modules/ROOT/pages/access.adoc +++ /dev/null @@ -1,573 +0,0 @@ -:accessControl-guide: xref:accessControl.adoc[AccessControl guide] -:role-based-access: https://en.wikipedia.org/wiki/Role-based_access_control[Role-Based Access Control (RBAC)] - -= Access Control -:steals-system: https://blog.openzeppelin.com/on-the-parity-wallet-multisig-hack-405a8c12e8f7[steals your whole system] - -Access control—that is, "who is allowed to do this thing"—is incredibly important in the world of smart contracts. -The access control of your contract may govern who can mint tokens, vote on proposals, freeze transfers, and many other things. -It is therefore critical to understand how you implement it, lest someone else {steals-system}. - -== Ownership and `Ownable` - -The most common and basic form of access control is the concept of ownership: -there’s an account that is the owner of a contract and can do administrative tasks on it. -This approach is perfectly reasonable for contracts that have a single administrative user. - -OpenZeppelin Contracts for Compact provides an Ownable module for implementing ownership in your contracts. -The initial owner must be set by using the xref:api/ownable.adoc#Ownable-initialize[initialize] circuit during construction. -This can later be changed with xref:api/ownable.adoc#Ownable-transferOwnership[transferOwnership]. - -=== Usage - -Import the Ownable module into the implementing contract. -It's recommended to prefix the module with `Ownable_` to avoid circuit signature clashes. - -```ts -// MyOwnableContract.compact - -pragma language_version >= 0.16.0; - -import CompactStandardLibrary; -import "./node_modules/@openzeppelin-compact/contracts/src/access/Ownable" - prefix Ownable_; - -constructor( - initialOwner: Either -) { - Ownable_initialize(initialOwner); -} -``` - -To protect a circuit so that only the contract owner may call it, -insert the `assertOnlyOwner` circuit in the beginning of the circuit body like this: - -```ts -export circuit mySensitiveCircuit(): [] { - Ownable_assertOnlyOwner(); - - // Do something -} -``` - -Contracts may expose xref:api/ownable.adoc#Ownable-transferOwnership[transferOwnership] to allow the owner to transfer ownership. - -```ts -export circuit transferOwnership( - newOwner: Either -): [] { - Ownable_transferOwnership(newOwner); -} -``` - -Here's a complete contract showcasing how to integrate the Ownable module and protect sensitive circuits. - -```ts -// SimpleOwnable.compact - -pragma language_version >= 0.16.0; - -import CompactStandardLibrary; -import "./node_modules/@openzeppelin-compact/contracts/src/access/Ownable" - prefix Ownable_; - -/** - * Set `initialOwner` as the owner of the contract. -*/ -constructor(initialOwner: Either) { - Ownable_initialize(initialOwner); -} - -/** - * The current owner of the contact. - */ -export circuit owner(): Either { - return Ownable_owner(); -} - -/** - * Transfers ownership of the contract. - * Can only be called by the current owner. - */ -export circuit transferOwnership( - newOwner: Either -): [] { - Ownable_transferOwnership(newOwner); -} - -/** - * Leaves the contract without an owner. - * Can only be called by the current owner. - * Renouncing ownership means `mySensitiveCircuit` can never be called again. - */ -export circuit renounceOwnership(): [] { - Ownable_renounceOwnership(); -} - -/** - * This is the protected circuit that only the current owner can call. - */ -export circuit mySensitiveCircuit(): [] { - // Protects the circuit - Ownable_assertOnlyOwner(); - - // Do something -} -``` - -TIP: For more complex logic, contracts may transfer ownership to another user irrespective of the caller by leveraging xref:api/ownable.adoc#Ownable-_transferOwnership[_transferOwnership]. -This is generally more useful when contract addresses are the owner or when a contract has a unique deployment process. - -=== Ownership transfers - -Ownership can only be transferred to `ZswapCoinPublicKeys` through the main transfer circuits (xref:api/ownable.adoc#Ownable-transferOwnership[transferOwnership] and xref:api/ownable.adoc#Ownable-_transferOwnership[_transferOwnership]). -In other words, ownership transfers to contract addresses are disallowed through these circuits. -This is because Compact currently does not support contract-to-contract calls which means if a contract is granted ownership, the owner contract cannot directly call the protected circuit. - -=== Experimental features - -This module offers experimental circuits that allow ownership to be granted to contract addresses (xref:api/ownable.adoc#Ownable-_unsafeTransferOwnership[_unsafeTransferOwnership] and xref:api/ownable.adoc#Ownable-_unsafeUncheckedTransferOwnership[_unsafeUncheckedTransferOwnership]). -Note that the circuit names are very explicit ("unsafe") with these experimental circuits. -Until contract-to-contract calls are supported, -there is no direct way for a contract to call circuits of other contracts or transfer ownership back to a user. - -NOTE: The unsafe circuits are planned to become deprecated once contract-to-contract calls become available. - -== Shielded Ownership and `ZOwnablePK` - -Privacy-preserving access control is a fundamental building block for confidential smart contracts on Midnight. -While traditional ownership patterns expose the owner's identity on-chain, -many applications require administrative control without revealing who holds that authority. - -=== Privacy-First Ownership - -The most common approach to access control in traditional smart contracts is ownership: -there's an account that is the owner of a contract and can perform administrative tasks. -However, this approach reveals the owner's identity to all observers, creating privacy and security risks. -In privacy-sensitive applications—such as confidential voting systems, private treasuries, or anonymous governance—revealing the administrator's identity may compromise the entire system's confidentiality. -This library provides the `ZOwnablePK` module that implements shielded ownership—administrative control without identity disclosure. -The owner's public key is never revealed on-chain; instead, -the contract stores only a cryptographic commitment that proves ownership without exposing the underlying identity. - -=== Commitment Scheme - -The `ZOwnablePK` module employs a two-layer cryptographic commitment scheme designed to provide privacy, -unlinkability, and collision resistance across deployments and ownership transfers. - -==== Owner ID Computation - -The foundation of the system is the owner identifier, computed as: - -```ts -id = SHA256(pk, nonce) -``` - -Where `pk` is the owner's public key and `nonce` is a secret value that may be either randomly generated -for maximum privacy or deterministically derived for recoverability. -This identifier serves as a privacy-preserving alternative to exposing the raw public key, -ensuring the owner's identity remains confidential. - -==== Owner Commitment Computation - -The final ownership commitment stored on-chain is computed as: - -```ts -commitment = SHA256(id, instanceSalt, counter, pad(32, "ZOwnablePK:shield:")) -``` - -This multi-element hash provides several security properties: - -- `id`: The privacy-preserving owner identifier described above. -- `instanceSalt`: A unique per-deployment salt that prevents commitment collisions across different contract instances, even when the same owner and nonce are used. -- `counter`: Incremented with each ownership transfer to ensure unlinkability—each transfer produces a completely different commitment even with the same underlying owner. -- `pad(32, "ZOwnablePK:shield:")`: A domain separator padded to 32 bytes that prevents hash collisions with other commitment schemes and enables safe protocol extensions. - -==== Security Properties - -This commitment scheme ensures that: - -- Public keys are never revealed on-chain. -- Observers cannot correlate past and future ownership. -- Cross-contract collisions are prevented through instance-specific salting. - -=== Nonce Generation Strategies - -The choice of nonce generation strategy represents a fundamental trade-off between simplicity/security and recoverability. -Both approaches are valid, and the best choice depends on your specific threat model and operational requirements. - -==== Random Nonce - -Generating a cryptographically strong random nonce provides the strongest privacy guarantees: - -```typescript -const randomNonce = crypto.getRandomValues(new Uint8Array(32)); -const ownerId = ZOwnablePK._computeOwnerId(publicKey, randomNonce); -``` - -This approach is easy to generate and ensures maximum unlinkability—even with sophisticated analysis, -observers cannot correlate ownership across different contracts or time periods. -However, it requires secure backup of both the private key and the nonce. -*Loss of either component results in permanent, irrecoverable loss of ownership.* - -==== Deterministic Nonce - -:rfc6979: https://datatracker.ietf.org/doc/html/rfc6979[RFC 6979] -:ed25519: https://ed25519.cr.yp.to/[Ed25519] - -Deriving the nonce deterministically enables recovery through derivation schemes. -Some examples: - -- `H(passphrase + context)` - recoverable from passphrase only, but passphrase becomes critical single point of failure. -- `H(publicKey + userPassphrase + context)` - requires both public key and passphrase. -- `H(signature + context)` where `signature = sign(context)` - leverages wallet without exposing private key. - -WARNING: When using signature-based nonce derivation, -ensure the wallet/library uses deterministic signatures ({ed25519} or {rfc6979} for ECDSA). -Non-deterministic signatures will generate different nonces on each signing, making recovery impossible. -Test the implementation by signing the same message twice then verify that the signatures match. - -*Context-Dependent Derivations:* - -- Include contract address, deployment timestamp, user ID, etc. -- Trade-off: more context is more unique but harder to recreate. - -WARNING: Approaches that avoid private key exposure (public key + passphrase, signature-based) -are generally recommended for operational security. - -Deriving the nonce deterministically from an <> and user passphrase provides a balance of security and recoverability: - -```typescript -// Example: Scrypt-based derivation -import { scryptSync } from 'node:crypto'; - -const deterministicNonce = scryptSync( - userPassphrase - publicKey + ":ZOwnablePK:nonce:v1", - 32, - { N: 16384, r: 8, p: 1 } // Standard scrypt parameters -); -const recoverableOwnerId = ZOwnablePK._computeOwnerId(publicKey, deterministicNonce); -``` - -**Security Considerations** - -The `ZOwnablePK` module remains agnostic to nonce generation methods, placing the security/convenience decision entirely with the user. Key considerations include: - -- **Backup requirements**: Random nonces require additional secure storage. -- **Recovery scenarios**: Deterministic nonces enable recovery. -- **Cross-contract correlation**: Reusing nonce strategies may reduce privacy across deployments. -- **Rotation costs**: Changing nonces requires ownership transfer transactions with associated DUST costs. - -Users should carefully evaluate their threat model, operational requirements, -and privacy needs when selecting a nonce generation strategy, -as this choice cannot be easily changed without transferring ownership. - -=== Air-Gapped Public Key (AGPK) - -For maximum privacy guarantees, -users should employ an Air-Gapped Public Key (AGPK) exclusively for contract ownership and administrative circuits. -An AGPK is a public key that maintains complete isolation from all other on-chain activities, -similar to how air-gapped systems are isolated from networks to prevent data leakage. - -==== The Privacy Enhancement - -While `ZOwnablePK` provides cryptographic privacy through its commitment scheme, -operational security practices like using an AGPK provide an additional layer of protection against correlation attacks. Even with the strongest cryptographic commitments, -reusing a public key across different on-chain activities can potentially compromise privacy -through transaction pattern analysis. - -==== AGPK Principles - -An Air-Gapped Public Key must adhere to strict isolation principles: - -- *Never used before:* The private key material -(including any seed, parent key, or entropy source from which this key is derived) -has never generated any public key that appears in any on-chain transaction, across any blockchain network. -The key material must be cryptographically pure. -- *Never used elsewhere:* From the moment of AGPK generation until its destruction, -the private key material is used exclusively for this contract's administrative functions (i.e. `assertOnlyOwner`). -No other public keys may ever be derived from or generated with the same key material. -- *Never used again:* Users commit to destroying all copies of the private key material -upon ownership renunciation or transfer. -This relies entirely on user discipline and cannot be externally verified or enforced. - -==== Best Practices Recommendation - -While neither required nor enforced by the `ZOwnablePK` module, -an Air-Gapped Public Key provides strong operational privacy hygiene for shielded contract administration. -Users should evaluate their threat model and privacy requirements when deciding whether to implement AGPK practices. - -WARNING: The effectiveness of an AGPK depends entirely on abiding by the AGPK principles. -A single transaction using the key outside the administrative context compromises all privacy benefits. - -=== Usage - -Import the `ZOwnablePK` module into the implementing contract and expose the ownership-handling circuits. -It’s recommended to prefix the module with `ZOwnablePK_` to avoid circuit signature clashes. - -```typescript -// MyZOwnablePKContract.compact - -pragma language_version >= 0.16.0; - -import CompactStandardLibrary; -import "./node_modules/@openzeppelin-compact/contracts/src/access/ZOwnablePK" - prefix ZOwnablePK_; - -constructor( - initOwnerCommitment: Bytes<32>, - instanceSalt: Bytes<32>, -) { - ZOwnablePK_initialize(initOwnerCommitment, instanceSalt); -} - -export circuit owner(): Bytes<32> { - return ZOwnablePK_owner(); -} - -export circuit transferOwnership(newOwnerCommitment: Bytes<32>): [] { - return ZOwnablePK_transferOwnership(disclose(newOwnerCommitment)); -} - -export circuit renounceOwnership(): [] { - return ZOwnablePK_renounceOwnership(); -} -``` - -Similar to the Ownable module, -circuits can be protected so that only the contract owner may them by adding `assertOnlyOwner` -as the first line in the circuit body like this: - -```typescript -export circuit mySensitiveCircuit(): [] { - ZOwnablePK_assertOnlyOwner(); - - // Do something -} -``` - -This covers the basics for creating a contract, but before deploying the contract, -the owner's id must be derived for the commitment scheme because it's required to deploy the contract. - -First, the owner needs to generate a secret nonce that's stored in the owner's private state. -See <>. - -Once the owner has the secret nonce generated, they can insert their public key and nonce into the following: - -```typescript -import { - CompactTypeBytes, - CompactTypeVector, - persistentHash, -} from '@midnight-ntwrk/compact-runtime'; -import { getRandomValues } from 'node:crypto'; - -// Owner ID -const generateId = ( - pk: Uint8Array, - nonce: Uint8Array, -): Uint8Array => { - const rt_type = new CompactTypeVector(2, new CompactTypeBytes(32)); - return persistentHash(rt_type, [pk, nonce]); -}; - -// Instance salt for the constructor -const generateInstanceSalt = (): Uint8Array => { - return getRandomValues(new Uint8Array(32)); -} -``` - -TIP: Another way to get the user ID is to expose `_computeOwnerId` in the contract -and call this circuit off chain through a contract simulator. -Be on the lookout for future tooling that makes this process easier. - -== Role-Based Access Control - -While the simplicity of _ownership_ can be useful for simple systems or quick prototyping, different levels of authorization are often needed. -You may want for an account to have permission to ban users from a system, but not create new tokens. -{role-based-access} offers flexibility in this regard. - -In essence, we will be defining multiple _roles_, each allowed to perform different sets of actions. -An account may have, for example, 'moderator', 'minter' or 'admin' roles, which you will then check for instead of simply using `assertOnlyOwner`. -This check can be enforced through the `assertOnlyRole` circuit. -Separately, you will be able to define rules for how accounts can be granted a role, have it revoked, and more. - -Most software uses access control systems that are role-based: some users are regular users, some may be supervisors or managers, and a few will often have administrative privileges. - -=== Using `AccessControl` - -The Compact contracts library provides `AccessControl` for implementing role-based access control. -Its usage is straightforward: for each role that you want to define, -you will create a new role identifier that is used to grant, revoke, and check if an account has that role. - -Here’s a simple example of using `AccessControl` with xref:fungibleToken.adoc[FungibleToken] to define a 'minter' role, which allows accounts that have this role to create new tokens: - -```ts -// AccessControlMinter.compact - -pragma language_version >= 0.16.0; - -import CompactStandardLibrary; -import "./node_modules/@openzeppelin-compact/contracts/src/access/AccessControl" - prefix AccessControl_; -import "./node_modules/@openzeppelin-compact/contracts/src/token/FungibleToken" - prefix FungibleToken_; - -export sealed ledger MINTER_ROLE: Bytes<32>; - -/** - * Initialize FungibleToken and MINTER_ROLE - */ -constructor( - name: Opaque<"string">, - symbol: Opaque<"string">, - decimals: Uint<8>, - minter: Either -) { - FungibleToken_initialize(name, symbol, decimals); - MINTER_ROLE = persistentHash>(pad(32, "MINTER_ROLE")); - AccessControl__grantRole(MINTER_ROLE, minter); -} - -export circuit mint( - recipient: Either, - value: Uint<128>, -): [] { - AccessControl_assertOnlyRole(MINTER_ROLE); - FungibleToken__mint(recipient, value); -} -``` - -NOTE: Make sure you fully understand how xref:api/accessControl.adoc#accessControl[AccessControl] works before using it on your system, or copy-pasting the examples from this guide. - -While clear and explicit, this isn’t anything we wouldn’t have been able to achieve with xref:ownable.adoc[Ownable]. Indeed, where `AccessControl` shines is in scenarios where granular permissions are required, which can be implemented by defining _multiple_ roles. - -Let’s augment our FungibleToken example by also defining a 'burner' role, which lets accounts destroy tokens. - -```ts -// AccessControlMinter.compact - -pragma language_version >= 0.16.0; - -import CompactStandardLibrary; -import "./node_modules/@openzeppelin-compact/contracts/src/access/AccessControl" - prefix AccessControl_; -import "./node_modules/@openzeppelin-compact/contracts/src/token/FungibleToken" - prefix FungibleToken_; - -export sealed ledger MINTER_ROLE: Bytes<32>; -export sealed ledger BURNER_ROLE: Bytes<32>; - -/** - * Initialize FungibleToken and MINTER_ROLE - */ -constructor( - name: Opaque<"string">, - symbol: Opaque<"string">, - decimals: Uint<8>, - minter: Either, - burner: Either -) { - FungibleToken_initialize(name, symbol, decimals); - MINTER_ROLE = persistentHash>(pad(32, "MINTER_ROLE")); - BURNER_ROLE = persistentHash>(pad(32, "BURNER_ROLE")); - AccessControl__grantRole(MINTER_ROLE, minter); - AccessControl__grantRole(BURNER_ROLE, burner); -} - -export circuit mint( - recipient: Either, - value: Uint<128>, -): [] { - AccessControl_assertOnlyRole(MINTER_ROLE); - FungibleToken__mint(recipient, value); -} - -export circuit burn( - recipient: Either, - value: Uint<128>, -): [] { - AccessControl_assertOnlyRole(BURNER_ROLE); - FungibleToken__burn(recipient, value); -} -``` - -So clean! By splitting concerns this way, more granular levels of permission may be implemented than were possible with the simpler _ownership_ approach to access control. -Limiting what each component of a system is able to do is known as the https://en.wikipedia.org/wiki/Principle_of_least_privilege[principle of least privilege], and is a good security practice. -Note that each account may still have more than one role, if so desired. - -=== Granting and Revoking Roles - -The FungibleToken example above uses `_grantRole`, an internal circuit that is useful when programmatically assigning roles (such as during construction). But what if we later want to grant the 'minter' role to additional accounts? - -By default, *accounts with a role cannot grant it or revoke it from other accounts*: all having a role does is making the `hasRole` check pass. To grant and revoke roles dynamically, you will need help from the _role’s admin_. - -Every role has an associated admin role, which grants permission to call the `grantRole` and `revokeRole` circuits. A role can be granted or revoked by using these if the calling account has the corresponding admin role. Multiple roles may have the same admin role to make management easier. A role’s admin can even be the same role itself, which would cause accounts with that role to be able to also grant and revoke it. - -This mechanism can be used to create complex permissioning structures resembling organizational charts, but it also provides an easy way to manage simpler applications. `AccessControl` includes a special role, called `DEFAULT_ADMIN_ROLE`, which acts as the *default admin role for all roles*. An account with this role will be able to manage any other role, unless `_setRoleAdmin` is used to select a new admin role. - -Since it is the admin for all roles by default, and in fact it is also its own admin, this role carries significant risk. - -Let’s take a look at the FungibleToken example, this time taking advantage of the default admin role: - -```ts -// AccessControlMinter.compact - -pragma language_version >= 0.16.0; - -import CompactStandardLibrary; -import "./node_modules/@openzeppelin-compact/contracts/src/access/AccessControl" - prefix AccessControl_; -import "./node_modules/@openzeppelin-compact/contracts/src/token/FungibleToken" - prefix FungibleToken_; - -export sealed ledger MINTER_ROLE: Bytes<32>; -export sealed ledger BURNER_ROLE: Bytes<32>; - -/** - * Initialize FungibleToken and MINTER_ROLE - */ -constructor( - name: Opaque<"string">, - symbol: Opaque<"string">, - decimals: Uint<8>, -) { - FungibleToken_initialize(name, symbol, decimals); - MINTER_ROLE = persistentHash>(pad(32, "MINTER_ROLE")); - BURNER_ROLE = persistentHash>(pad(32, "BURNER_ROLE")); - // Grant the contract deployer the default admin role: it will be able - // to grant and revoke any roles - AccessControl__grantRole( - AccessControl_DEFAULT_ADMIN_ROLE, - left(ownPublicKey()), - ); -} - -export circuit mint( - recipient: Either, - value: Uint<128>, - ): [] { - AccessControl_assertOnlyRole(MINTER_ROLE); - FungibleToken__mint(recipient, value); -} - -export circuit burn( - recipient: Either, - value: Uint<128>, - ): [] { - AccessControl_assertOnlyRole(BURNER_ROLE); - FungibleToken__burn(recipient, value); -} -``` - -Note that, unlike the previous examples, no accounts are granted the 'minter' or 'burner' roles. However, because those roles' admin role is the default admin role, and _that_ role was granted to `ownPublicKey()`, that same account can call `grantRole` to give minting or burning permission, and `revokeRole` to remove it. - -Dynamic role allocation is often a desirable property, for example in systems where trust in a participant may vary over time. It can also be used to support use cases such as KYC, where the list of role-bearers may not be known up-front, or may be prohibitively expensive to include in a single transaction. - -=== Experimental features - -This module offers an experimental circuit that allow access control permissions to be granted to contract addresses xref:api/accessControl.adoc#AccessControl-_unsafeGrantRole[_unsafeGrantRole]. -Note that the circuit name is very explicit ("unsafe") with this experimental circuit. -Until contract-to-contract calls are supported, there is no direct way for a contract to call permissioned circuits of other contracts or grant/revoke role permissions. - -NOTE: The unsafe circuits are planned to become deprecated once contract-to-contract calls become available. \ No newline at end of file diff --git a/docs/modules/ROOT/pages/api/access.adoc b/docs/modules/ROOT/pages/api/access.adoc deleted file mode 100644 index dd900b8a..00000000 --- a/docs/modules/ROOT/pages/api/access.adoc +++ /dev/null @@ -1,603 +0,0 @@ -:github-icon: pass:[] -:accessControl-guide: xref:access.adoc#role_based_access_control[AccessControl guide] -:ownable-guide: xref:access.adoc#ownership_and_ownable[Ownable guide] -:zownablepk-guide: xref:access.adoc#shielded_ownership_and_zownablepk[ZOwnablePK guide] -:agpk: xref:access.adoc#air_gapped_public_key_agpk[Air-Gapped Public Key] -:grantRole: <> -:revokeRole: <> - -= Access Control - -This directory provides ways to restrict who can access the circuits of a contract or when they can do it. - -- `<>` provides a per-contract role based access control mechanism. Multiple hierarchical roles can be created and assigned each to multiple accounts within the same instance. - -- `<>` is a simpler mechanism with a single owner "role" that can be assigned to a single account. This simpler mechanism can be useful for quick tests but projects with production concerns are likely to outgrow it. - -- `<>` provides a privacy-preserving single owner access control mechanism using cryptographic commitments. The owner's public key is never revealed on-chain, instead storing only a commitment that proves ownership without exposing identity, suitable for applications requiring administrative control with strong privacy guarantees. - -== Core - -[.contract] -[[AccessControl]] -=== `++AccessControl++` link:https://github.com/OpenZeppelin/compact-contracts/tree/main/contracts/accessControl/src/AccessControl.compact[{github-icon},role=heading-link] - -[.hljs-theme-dark] -```ts -import "./node_modules/@openzeppelin-compact/contracts/src/access/AccessControl"; -``` - -Roles are referred to by their `Bytes<32>` identifier. These should be exposed in the top-level contract and be unique. The best way to achieve this is by using `export sealed ledger` hash digests that are initialized in the top-level contract: - -```typescript -import CompactStandardLibrary; -import "./node_modules/@openzeppelin-compact/contracts/src/access/AccessControl" - prefix AccessControl_; - -export sealed ledger MY_ROLE: Bytes<32>; - -constructor() { - MY_ROLE = persistentHash>(pad(32, "MY_ROLE")); -} -``` - -To restrict access to a circuit, use <>: -```typescript -circuit foo(): [] { - assertOnlyRole(MY_ROLE); - ... -} -``` - -Roles can be granted and revoked dynamically via the {grantRole} and {revokeRole} functions. Each role has an associated admin role, and only accounts that have a role's admin role can call {grantRole} and {revokeRole}. - -By default, the admin role for all roles is `DEFAULT_ADMIN_ROLE`, which means that only accounts with this role will be able to grant or revoke other roles. More complex role relationships can be created by using <>. To set a custom `DEFAULT_ADMIN_ROLE`, implement the `Initializable` module and set `DEFAULT_ADMIN_ROLE` in the `initialize()` function. - -WARNING: The `DEFAULT_ADMIN_ROLE` is also its own admin: it has permission to grant and revoke this role. Extra precautions should be taken to secure accounts that have been granted it. - -TIP: For an overview of the module, read the {accessControl-guide}. - -[.contract-index] -.Circuits --- - -[.sub-index#AccessControlModule] -* xref:#AccessControl-hasRole[`++hasRole(roleId, account)++`] -* xref:#AccessControl-assertOnlyRole[`++assertOnlyRole(roleId)++`] -* xref:#AccessControl-_checkRole[`++_checkRole(roleId, account)++`] -* xref:#AccessControl-getRoleAdmin[`++getRoleAdmin(roleId)++`] -* xref:#AccessControl-grantRole[`++grantRole(roleId, account)++`] -* xref:#AccessControl-revokeRole[`++revokeRole(roleId, account)++`] -* xref:#AccessControl-renounceRole[`++renounceRole(roleId, callerConfirmation)++`] -* xref:#AccessControl-_setRoleAdmin[`++_setRoleAdmin(roleId, adminRole)++`] -* xref:#AccessControl-_grantRole[`++_grantRole(roleId, account)++`] -* xref:#AccessControl-_unsafeGrantRole[`++_unsafeGrantRole(roleId, account)++`] -* xref:#AccessControl-_revokeRole[`++_revokeRole(roleId, account)++`] --- - -[.contract-item] -[[AccessControl-hasRole]] -==== `[.contract-item-name]#++hasRole++#++(roleId: Bytes<32>, account: Either) → Boolean++` [.item-kind]#circuit# - -Returns `true` if `account` has been granted `roleId`. - -Constraints: - -- k=10, rows=487 - -[.contract-item] -[[AccessControl-assertOnlyRole]] -==== `[.contract-item-name]#++assertOnlyRole++#++(roleId: Bytes<32>) → []++` [.item-kind]#circuit# - -Reverts if caller is missing `roleId`. - -Requirements: - -- The caller must have `roleId`. -- The caller must not be a `ContractAddress`. - -Constraints: - -- k=10, rows=345 - -[.contract-item] -[[AccessControl-_checkRole]] -==== `[.contract-item-name]#++_checkRole++#++(roleId: Bytes<32>, account: Either) → []++` [.item-kind]#circuit# - -Reverts if `account` is missing `roleId`. - -Requirements: - -- `account` must have `roleId`. - -Constraints: - -- k=10, rows=467 - -[.contract-item] -[[AccessControl-getRoleAdmin]] -==== `[.contract-item-name]#++getRoleAdmin++#++(roleId: Bytes<32>) → Bytes<32>++` [.item-kind]#circuit# - -Returns the admin role that controls `roleId` or a byte array with all zero bytes if `roleId` doesn't exist. See {grantRole} and {revokeRole}. - -To change a role's admin use <>. - -Constraints: - -- k=10, rows=207 - -[.contract-item] -[[AccessControl-grantRole]] -==== `[.contract-item-name]#++grantRole++#++(roleId: Bytes<32>, account: Either) → []++` [.item-kind]#circuit# - -Grants `roleId` to `account`. - -NOTE: Granting roles to contract addresses is currently disallowed until contract-to-contract interactions are supported in Compact. -This restriction prevents permanently disabling access to a circuit. - -Requirements: - -- `account` must not be a ContractAddress. -- The caller must have ``roleId``'s admin role. - -Constraints: - -- k=10, rows=994 - -[.contract-item] -[[AccessControl-revokeRole]] -==== `[.contract-item-name]#++revokeRole++#++(roleId: Bytes<32>, account: Either) → []++` [.item-kind]#circuit# - -Revokes `roleId` from `account`. - -Requirements: - -- The caller must have ``roleId``'s admin role. - -Constraints: - -- k=10, rows=827 - -[.contract-item] -[[AccessControl-renounceRole]] -==== `[.contract-item-name]#++renounceRole++#++(roleId: Bytes<32>, callerConfirmation: Either) → []++` [.item-kind]#circuit# - -Revokes `roleId` from the calling account. - -Roles are often managed via {grantRole} and {revokeRole}: this circuit's -purpose is to provide a mechanism for accounts to lose their privileges -if they are compromised (such as when a trusted device is misplaced). - -NOTE: We do not provide functionality for smart contracts to renounce roles because self-executing transactions are not supported on Midnight at this time. We may revisit this in future if this feature is made available in Compact. - -Requirements: - -- The caller must be `callerConfirmation`. -- The caller must not be a `ContractAddress`. - -Constraints: - -- k=10, rows=640 - -[.contract-item] -[[AccessControl-_setRoleAdmin]] -==== `[.contract-item-name]#++_setRoleAdmin++#++(roleId: Bytes<32>, adminRole: Bytes<32>) → []++` [.item-kind]#circuit# - -Sets `adminRole` as ``roleId``'s admin role. - -Constraints: - -- k=10, rows=209 - -[.contract-item] -[[AccessControl-_grantRole]] -==== `[.contract-item-name]#++_grantRole++#++(roleId: Bytes<32>, adminRole: Bytes<32>) → Boolean++` [.item-kind]#circuit# - -Attempts to grant `roleId` to `account` and returns a boolean indicating if `roleId` was granted. - -Internal circuit without access restriction. - -NOTE: Granting roles to contract addresses is currently disallowed in this circuit until contract-to-contract interactions are supported in Compact. -This restriction prevents permanently disabling access to a circuit. - -Requirements: - -- `account` must not be a ContractAddress. - -Constraints: - -- k=10, rows=734 - -[.contract-item] -[[AccessControl-_unsafeGrantRole]] -==== `[.contract-item-name]#++_unsafeGrantRole++#++(roleId: Bytes<32>, account: Either) → Boolean++` [.item-kind]#circuit# - -Unsafe variant of <>. - -WARNING: Granting roles to contract addresses is considered unsafe because contract-to-contract calls are not currently supported. -Granting a role to a smart contract may render a circuit permanently inaccessible. -Once contract-to-contract calls are supported, this circuit may be deprecated. - -Constraints: - -- k=10, rows=733 - -[.contract-item] -[[AccessControl-_revokeRole]] -==== `[.contract-item-name]#++_revokeRole++#++(roleId: Bytes<32>, account: Either) → Boolean++` [.item-kind]#circuit# - -Attempts to revoke `roleId` from `account` and returns a boolean indicating if `roleId` was revoked. - -Internal circuit without access restriction. - -Constraints: - -- k=10, rows=563 - -[.contract] -[[Ownable]] -=== `++Ownable++` link:https://github.com/OpenZeppelin/compact-contracts/blob/main/contracts/ownable/src/Ownable.compact[{github-icon},role=heading-link] - -[.hljs-theme-dark] -```ts -import "./node_modules/@openzeppelin-compact/contracts/src/access/Ownable"; -``` - -Ownable provides a basic access control mechanism where an account (an owner) can be granted exclusive access to specific circuits. - -This module includes <> to restrict a circuit to be used only by the owner. - -TIP: For an overview of the module, read the {ownable-guide}. - -[.contract-index] -.Circuits --- - -[.sub-index#OwnableModule] -* xref:#Ownable-initialize[`++initialize(initialOwner)++`] -* xref:#Ownable-owner[`++owner()++`] -* xref:#Ownable-transferOwnership[`++transferOwnership(newOwner)++`] -* xref:#Ownable-_unsafeTransferOwnership[`++_unsafeTransferOwnership(newOwner)++`] -* xref:#Ownable-renounceOwnership[`++renounceOwnership()++`] -* xref:#Ownable-assertOnlyOwner[`++assertOnlyOwner(operator, approved)++`] -* xref:#Ownable-_transferOwnership[`++_transferOwnership(newOwner)++`] -* xref:#Ownable-_unsafeUncheckedTransferOwnership[`++_unsafeUncheckedTransferOwnership(newOwner)++`] --- - -[.contract-item] -[[Ownable-initialize]] -==== `[.contract-item-name]#++initialize++#++(initialOwner: Either) → []++` [.item-kind]#circuit# - -Initializes the contract by setting the `initialOwner`. -This must be called in the contract's constructor. - -Requirements: - -- Contract is not already initialized. -- `initialOwner` is not a ContractAddress. -- `initialOwner` is not the zero address. - -Constraints: - -- k=10, rows=258 - -[.contract-item] -[[Ownable-owner]] -==== `[.contract-item-name]#++owner++#++() → Either++` [.item-kind]#circuit# - -Returns the current contract owner. - -Requirements: - -- Contract is initialized. - -Constraints: - -- k=10, rows=84 - -[.contract-item] -[[Ownable-transferOwnership]] -==== `[.contract-item-name]#++transferOwnership++#++(newOwner: Either) → []++` [.item-kind]#circuit# - -Transfers ownership of the contract to `newOwner`. - -NOTE: Ownership transfers to contract addresses are currently disallowed until contract-to-contract interactions are supported in Compact. -This restriction prevents permanently disabling access to a circuit. - -Requirements: - -- Contract is initialized. -- The caller is the current contract owner. -- `newOwner` is not a ContractAddress. -- `newOwner` is not the zero address. - -Constraints: - -- k=10, rows=338 - -[.contract-item] -[[Ownable-_unsafeTransferOwnership]] -==== `[.contract-item-name]#++_unsafeTransferOwnership++#++(newOwner: Either) → []++` [.item-kind]#circuit# - -Unsafe variant of <>. - -WARNING: Ownership transfers to contract addresses are considered unsafe because contract-to-contract calls are not currently supported. -Ownership privileges sent to a contract address may become uncallable. -Once contract-to-contract calls are supported, this circuit may be deprecated. - -Requirements: - -- Contract is initialized. -- The caller is the current contract owner. -- `newOwner` is not the zero address. - -Constraints: - -- k=10, rows=335 - -[.contract-item] -[[Ownable-renounceOwnership]] -==== `[.contract-item-name]#++renounceOwnership++#++() → []++` [.item-kind]#circuit# - -Leaves the contract without an owner. -It will not be possible to call <> circuits anymore. -Can only be called by the current owner. - -Requirements: - -- Contract is initialized. -- The caller is the current contract owner. - -Constraints: - -- k=10, rows=124 - -[.contract-item] -[[Ownable-assertOnlyOwner]] -==== `[.contract-item-name]#++assertOnlyOwner++#++() → []++` [.item-kind]#circuit# - -Throws if called by any account other than the owner. -Use this to restrict access of specific circuits to the owner. - -Requirements: - -- Contract is initialized. -- The caller is the current contract owner. - -Constraints: - -- k=10, rows=115 - -[.contract-item] -[[Ownable-_transferOwnership]] -==== `[.contract-item-name]#++_transferOwnership++#++(newOwner: Either) → []++` [.item-kind]#circuit# - -Transfers ownership of the contract to a `newOwner` without enforcing permission checks on the caller. - -NOTE: Ownership transfers to contract addresses are currently disallowed until contract-to-contract interactions are supported in Compact. -This restriction prevents permanently disabling access to a circuit. - -Requirements: - -- Contract is initialized. -- `newOwner` is not a ContractAddress. - -Constraints: - -- k=10, rows=219 - -[.contract-item] -[[Ownable-_unsafeUncheckedTransferOwnership]] -==== `[.contract-item-name]#++_unsafeUncheckedTransferOwnership++#++(newOwner: Either) → []++` [.item-kind]#circuit# - -Unsafe variant of <>. - -WARNING: Ownership transfers to contract addresses are considered unsafe because contract-to-contract calls are not currently supported. -Ownership privileges sent to a contract address may become uncallable. -Once contract-to-contract calls are supported, this circuit may be deprecated. - -Requirements: - -- Contract is initialized. - -Constraints: - -- k=10, rows=216 - -[.contract] -[[ZOwnablePK]] -=== `++ZOwnablePK++` link:https://github.com/OpenZeppelin/compact-contracts/blob/main/contracts/ownable/src/ZOwnablePK.compact[{github-icon},role=heading-link] - -[.hljs-theme-dark] -```ts -import "./node_modules/@openzeppelin-compact/contracts/src/access/ZOwnablePK"; -``` - -`ZOwnablePK` provides a privacy-preserving access control mechanism for contracts with a single administrative user. Unlike traditional `Ownable` implementations that store or expose the owner's public key on-chain, -this module stores only a commitment to a hashed identifier derived from the owner's public key and a secret nonce. -For the strongest security guarantees, use an {agpk}. - -Ownable provides a basic access control mechanism where an account (an owner) can be granted exclusive access to specific circuits. - -This module includes <> to restrict a circuit to be used only by the owner. - -TIP: For an overview of the module, read the {zownablepk-guide}. - -[.contract-index] -.Circuits --- - -[.sub-index#ZOwnablePKModule] -* xref:#ZOwnablePK-initialize[`++initialize(ownerId, instanceSalt)++`] -* xref:#ZOwnablePK-owner[`++owner()++`] -* xref:#ZOwnablePK-transferOwnership[`++transferOwnership(newOwnerId)++`] -* xref:#ZOwnablePK-renounceOwnership[`++renounceOwnership()++`] -* xref:#ZOwnablePK-assertOnlyOwner[`++assertOnlyOwner()++`] -* xref:#ZOwnablePK-_computeOwnerCommitment[`++_computeOwnerCommitment(id, counter)++`] -* xref:#ZOwnablePK-_computeOwnerId[`++_computeOwnerId(pk, nonce)++`] -* xref:#ZOwnablePK-_transferOwnership[`++_transferOwnership(newOwnerId)++`] --- - -[.contract-item] -[[ZOwnablePK-initialize]] -==== `[.contract-item-name]#++initialize++#++(initialOwner: Either) → []++` [.item-kind]#circuit# - -Initializes the contract by setting the initial owner via `ownerId` -and storing the `instanceSalt` that acts as a privacy additive -for preventing duplicate commitments among other contracts implementing `ZOwnablePK`. - -NOTE: The `ownerId` must be calculated prior to contract deployment. -See <> - -Requirements: - -- Contract is not already initialized. -- `ownerId` is not an empty array. - -Constraints: - -- k=14, rows=14933 - -[.contract-item] -[[ZOwnablePK-owner]] -==== `[.contract-item-name]#++owner++#++() → Bytes<32>++` [.item-kind]#circuit# - -Returns the current commitment representing the contract owner. -The full commitment is: `SHA256(SHA256(pk, nonce), instanceSalt, counter, domain)`. - -Requirements: - -- Contract is initialized. - -Constraints: - -- k=10, rows=57 - -[.contract-item] -[[ZOwnablePK-transferOwnership]] -==== `[.contract-item-name]#++transferOwnership++#++(newOwnerId: Bytes<32>) → []++` [.item-kind]#circuit# - -Transfers ownership of the contract to `newOwnerId`. -`newOwnerId` must be precalculated and given to the current owner off chain. - -Requirements: - -- Contract is initialized. -- Caller is the current contract owner. -- `newOwnerId` is not an empty array. - -Constraints: - -- k=16, rows=39240 - -[.contract-item] -[[ZOwnablePK-renounceOwnership]] -==== `[.contract-item-name]#++renounceOwnership++#++() → []++` [.item-kind]#circuit# - -Leaves the contract without an owner. -It will not be possible to call <> circuits anymore. -Can only be called by the current owner. - -Requirements: - -- Contract is initialized. -- Caller is the current owner. - -Constraints: - -- k=15, rows=24442 - -[.contract-item] -[[ZOwnablePK-assertOnlyOwner]] -==== `[.contract-item-name]#++assertOnlyOwner++#++() → []++` [.item-kind]#circuit# - -Throws if called by any account whose id hash `SHA256(pk, nonce)` does not match the stored owner commitment. -Use this to only allow the owner to call specific circuits. - -Requirements: - -- Contract is initialized. -- Caller's id (`SHA256(pk, nonce)`) when used in <> equals the stored `_ownerCommitment`, -thus verifying themselves as the owner. - -Constraints: - -- k=15, rows=24437 - -[.contract-item] -[[ZOwnablePK-_computeOwnerCommitment]] -==== `[.contract-item-name]#++_computeOwnerCommitment++#++(id: Bytes<32>, counter: Uint<64>) → Bytes<32>++` [.item-kind]#circuit# - -Computes the owner commitment from the given `id` and `counter`. - -**Owner ID (`id`)** - -The `id` is expected to be computed off-chain as: `id = SHA256(pk, nonce)` - -- `pk`: The owner's public key. -- `nonce`: A secret nonce scoped to the instance, ideally rotated with each transfer. - -**Commitment Derivation** - -`commitment = SHA256(id, instanceSalt, counter, domain)` - -- `id`: See above. -- `instanceSalt`: A unique per-deployment salt, stored during initialization. -This prevents commitment collisions across deployments. -- `counter`: Incremented with each ownership transfer, ensuring uniqueness even with repeated `id` values. -Cast to `Field` then `Bytes<32>` for hashing. -- `domain`: Domain separator `"ZOwnablePK:shield:"` (padded to 32 bytes) to prevent hash collisions -when extending the module or using similar commitment schemes. - -Requirements: - -- Contract is initialized. - -Constraints: - -- k=14, rows=14853 - -[.contract-item] -[[ZOwnablePK-_computeOwnerId]] -==== `[.contract-item-name]#++_computeOwnerId++#++(pk: Either, nonce: Bytes<32>) → Bytes<32>++` [.item-kind]#circuit# - -Computes the unique identifier (`id`) of the owner from their public key and a secret nonce. - -**ID Derivation** -`id = SHA256(pk, nonce)` - -- `pk`: The public key of the caller. -This is passed explicitly to allow for off-chain derivation, testing, or scenarios -where the caller is different from the subject of the computation. -We recommend using an {agpk}. -- `nonce`: A secret nonce tied to the identity. -This value should be randomly generated and kept private. -It may be rotated periodically for enhanced unlinkability. - -The result is a 32-byte commitment that uniquely identifies the owner. -This value is later used in owner commitment hashing, -and acts as a privacy-preserving alternative to a raw public key. - -NOTE: This module allows ownership to be tied to an identity commitment derived from a public key and secret nonce. -While typically used with user public keys, -this mechanism may also support contract addresses as identifiers in future contract-to-contract interactions. -Both are treated as 32-byte values (`Bytes<32>`). - -Requirements: - -- Contract is initialized. -- `pk` is not a ContractAddress. - -[.contract-item] -[[ZOwnablePK-_transferOwnership]] -==== `[.contract-item-name]#++_transferOwnership++#++(newOwnerId: Bytes<32>) → []++` [.item-kind]#circuit# - -Transfers ownership to owner id `newOwnerId` without enforcing permission checks on the caller. - -Requirements: - -- Contract is initialized. - -Constraints: - -- k=14, rows=14823 diff --git a/docs/modules/ROOT/pages/api/fungibleToken.adoc b/docs/modules/ROOT/pages/api/fungibleToken.adoc deleted file mode 100644 index 8fea2149..00000000 --- a/docs/modules/ROOT/pages/api/fungibleToken.adoc +++ /dev/null @@ -1,389 +0,0 @@ -:github-icon: pass:[] -:fungible-guide: xref:fungibleToken.adoc[FungibleToken guide] - -= FungibleToken - -This module provides the full FungibleToken module API. - -TIP: For an overview of the module, read the {fungible-guide}. - -== Core - -[.contract] -[[FungibleToken]] -=== `++FungibleToken++` link:https://github.com/OpenZeppelin/compact-contracts/blob/main/contracts/fungibleToken/src/FungibleToken.compact[{github-icon},role=heading-link] - -[.hljs-theme-dark] -```ts -import "./node_modules/@openzeppelin-compact/contracts/src/token/FungibleToken"; -``` - -[.contract-index] -.Circuits --- -[.sub-index#FungibleTokenModule] -* xref:#FungibleTokenModule-initialize[`++initialize(name_, symbol_, decimals_)++`] -* xref:#FungibleTokenModule-name[`++name()++`] -* xref:#FungibleTokenModule-symbol[`++symbol()++`] -* xref:#FungibleTokenModule-decimals[`++decimals()++`] -* xref:#FungibleTokenModule-totalSupply[`++totalSupply()++`] -* xref:#FungibleTokenModule-balanceOf[`++balanceOf(account)++`] -* xref:#FungibleTokenModule-transfer[`++transfer(to, value)++`] -* xref:#FungibleTokenModule-_unsafeTransfer[`++_unsafeTransfer(to, value)++`] -* xref:#FungibleTokenModule-allowance[`++allowance(owner, spender)++`] -* xref:#FungibleTokenModule-approve[`++approve(spender, value)++`] -* xref:#FungibleTokenModule-transferFrom[`++transferFrom(from, to, value)++`] -* xref:#FungibleTokenModule-_unsafeTransferFrom[`++_unsafeTransferFrom(from, to, value)++`] -* xref:#FungibleTokenModule-_transfer[`++_transfer(from, to, value)++`] -* xref:#FungibleTokenModule-_unsafeUncheckedTransfer[`++_unsafeUncheckedTransfer(from, to, value)++`] -* xref:#FungibleTokenModule-_update[`++_update(from, to, value)++`] -* xref:#FungibleTokenModule-_mint[`++_mint(account, value)++`] -* xref:#FungibleTokenModule-_unsafeMint[`++_unsafeMint(account, value)++`] -* xref:#FungibleTokenModule-_burn[`++_burn(account, value)++`] -* xref:#FungibleTokenModule-_approve[`++_approve(owner, spender, value)++`] -* xref:#FungibleTokenModule-_spendAllowance[`++_spendAllowance(owner, spender, value)++`] --- - -[.contract-item] -[[FungibleTokenModule-initialize]] -==== `[.contract-item-name]#++initialize++#++(name_: Opaque<"string">, symbol_: Opaque<"string">, decimals_: Uint<8>) → []++` [.item-kind]#circuit# - -Initializes the contract by setting the name, symbol, and decimals. - -This MUST be called in the implementing contract's constructor. -Failure to do so can lead to an irreparable contract. - -Requirements: - -- Contract is not initialized. - -Constraints: - -- k=10, rows=71 - -[.contract-item] -[[FungibleTokenModule-name]] -==== `[.contract-item-name]#++name++#++() → Opaque<"string">++` [.item-kind]#circuit# - -Returns the token name. - -Requirements: - -- Contract is initialized. - -Constraints: - -- k=10, rows=37 - -[.contract-item] -[[FungibleTokenModule-symbol]] -==== `[.contract-item-name]#++symbol++#++() → Opaque<"string">++` [.item-kind]#circuit# - -Returns the symbol of the token. - -Requirements: - -- Contract is initialized. - -Constraints: - -- k=10, rows=37 - -[.contract-item] -[[FungibleTokenModule-decimals]] -==== `[.contract-item-name]#++decimals++#++() → Uint<8>++` [.item-kind]#circuit# - -Returns the number of decimals used to get its user representation. - -Requirements: - -- Contract is initialized. - -Constraints: - -- k=10, rows=36 - -[.contract-item] -[[FungibleTokenModule-totalSupply]] -==== `[.contract-item-name]#++totalSupply++#++() → Uint<128>++` [.item-kind]#circuit# - -Returns the value of tokens in existence. - -Requirements: - -- Contract is initialized. - -Constraints: - -- k=10, rows=36 - -[.contract-item] -[[FungibleTokenModule-balanceOf]] -==== `[.contract-item-name]#++balanceOf++#++(account: Either) → Uint<128>++` [.item-kind]#circuit# - -Returns the value of tokens owned by `account`. - -Requirements: - -- Contract is initialized. - -Constraints: - -- k=10, rows=310 - -[.contract-item] -[[FungibleTokenModule-transfer]] -==== `[.contract-item-name]#++transfer++#++(to: Either, value: Uint<128>) → Boolean++` [.item-kind]#circuit# - -Moves a `value` amount of tokens from the caller's account to `to`. - -NOTE: Transfers to contract addresses are currently disallowed until contract-to-contract interactions are supported in Compact. -This restriction prevents assets from being inadvertently locked in contracts that cannot currently handle token receipt. - -Requirements: - -- Contract is initialized. -- `to` is not a ContractAddress. -- `to` is not the zero address. -- The caller has a balance of at least `value`. - -Constraints: - -- k=11, rows=1173 - -[.contract-item] -[[FungibleTokenModule-_unsafeTransfer]] -==== `[.contract-item-name]#++_unsafeTransfer++#++(to: Either, value: Uint<128>) → Boolean++` [.item-kind]#circuit# - -Unsafe variant of <> which allows transfers to contract addresses. - -WARNING: Transfers to contract addresses are considered unsafe because contract-to-contract calls are not currently supported. Tokens sent to a contract address may become irretrievable. -Once contract-to-contract calls are supported, this circuit may be deprecated. - -Requirements: - -- Contract is initialized. -- `to` is not the zero address. -- The caller has a balance of at least `value`. - -Constraints: - -- k=11, rows=1170 - -[.contract-item] -[[FungibleTokenModule-allowance]] -==== `[.contract-item-name]#++allowance++#++(owner: Either, spender: Either) → Uint<128>++` [.item-kind]#circuit# - -Returns the remaining number of tokens that `spender` will be allowed to spend on behalf of `owner` through <>. -This value changes when <> or <> are called. - -Requirements: - -- Contract is initialized. - -Constraints: - -- k=10, rows=624 - -[.contract-item] -[[FungibleTokenModule-approve]] -==== `[.contract-item-name]#++approve++#++(spender: Either, value: Uint<128>) → Boolean++` [.item-kind]#circuit# - -Sets a `value` amount of tokens as allowance of `spender` over the caller's tokens. - -Requirements: - -- Contract is initialized. -- `spender` is not the zero address. - -Constraints: - -- k=10, rows=452 - -[.contract-item] -[[FungibleTokenModule-transferFrom]] -==== `[.contract-item-name]#++transferFrom++#++(from: Either, to: Either, value: Uint<128>) → Boolean++` [.item-kind]#circuit# - -Moves `value` tokens from `from` to `to` using the allowance mechanism. -`value` is the deducted from the caller's allowance. - -NOTE: Transfers to contract addresses are currently disallowed until contract-to-contract interactions are supported in Compact. -This restriction prevents assets from being inadvertently locked in contracts that cannot currently handle token receipt. - -Requirements: - -- Contract is initialized. -- `from` is not the zero address. -- `from` must have a balance of at least `value`. -- `to` is not the zero address. -- `to` is not a ContractAddress. -- The caller has an allowance of ``from``'s tokens of at least `value`. - -Constraints: - -- k=11, rows=1821 - -[.contract-item] -[[FungibleTokenModule-_unsafeTransferFrom]] -==== `[.contract-item-name]#++_unsafeTransferFrom++#++(from: Either, to: Either, value: Uint<128>) → Boolean++` [.item-kind]#circuit# - -Unsafe variant of <> which allows transfers to contract addresses. - -WARNING: Transfers to contract addresses are considered unsafe because contract-to-contract calls are not currently supported. -Tokens sent to a contract address may become irretrievable. -Once contract-to-contract calls are supported, this circuit may be deprecated. - -Requirements: - -- Contract is initialized. -- `from` is not the zero address. -- `from` must have a balance of at least `value`. -- `to` is not the zero address. -- The caller has an allowance of ``from``'s tokens of at least `value`. - -Constraints: - -- k=11, rows=1818 - -[.contract-item] -[[FungibleTokenModule-_transfer]] -==== `[.contract-item-name]#++_transfer++#++(from: Either, to: Either, value: Uint<128>) → []++` [.item-kind]#circuit# - -Moves a `value` amount of tokens from `from` to `to`. -This circuit is equivalent to <>, and can be used to e.g. -implement automatic token fees, slashing mechanisms, etc. - -NOTE: Transfers to contract addresses are currently disallowed until contract-to-contract interactions are supported in Compact. -This restriction prevents assets from being inadvertently locked in contracts that cannot currently handle token receipt. - -Requirements: - -- Contract is initialized. -- `from` is not be the zero address. -- `from` must have at least a balance of `value`. -- `to` must not be the zero address. -- `to` must not be a ContractAddress. - -Constraints: - -- k=11, rows=1312 - -[.contract-item] -[[FungibleTokenModule-_unsafeUncheckedTransfer]] -==== `[.contract-item-name]#++_unsafeUncheckedTransfer++#++(from: Either, to: Either, value: Uint<128>) → []++` [.item-kind]#circuit# - -Unsafe variant of <> which allows transfers to contract addresses. - -WARNING: Transfers to contract addresses are considered unsafe because contract-to-contract calls are not currently supported. Tokens sent to a contract address may become irretrievable. -Once contract-to-contract calls are supported, this circuit may be deprecated. - -Requirements: - -- Contract is initialized. -- `from` is not the zero address. -- `to` is not the zero address. - -Constraints: - -- k=11, rows=1309 - -[.contract-item] -[[FungibleTokenModule-_update]] -==== `[.contract-item-name]#++_update++#++(from: Either, to: Either, value: Uint<128>) → []++` [.item-kind]#circuit# - -Transfers a `value` amount of tokens from `from` to `to`, -or alternatively mints (or burns) if `from` (or `to`) is the zero address. - -Requirements: - -- Contract is initialized. - -Constraints: - -- k=11, rows=1305 - -[.contract-item] -[[FungibleTokenModule-_mint]] -==== `[.contract-item-name]#++_mint++#++(account: Either, value: Uint<128>) → []++` [.item-kind]#circuit# - -Creates a `value` amount of tokens and assigns them to `account`, by transferring it from the zero address. -Relies on the `update` mechanism. - -Requirements: - -- Contract is initialized. -- `to` is not a ContractAddress. -- `account` is not the zero address. - -Constraints: - -- k=10, rows=752 - -[.contract-item] -[[FungibleTokenModule-_unsafeMint]] -==== `[.contract-item-name]#++_unsafeMint++#++(account: Either, value: Uint<128>) → []++` [.item-kind]#circuit# - -Unsafe variant of <> which allows transfers to contract addresses. - -WARNING: Transfers to contract addresses are considered unsafe because contract-to-contract calls are not currently supported. -Tokens sent to a contract address may become irretrievable. -Once contract-to-contract calls are supported, this circuit may be deprecated. - -Requirements: - -- Contract is initialized. -- `account` is not the zero address. - -Constraints: - -- k=10, rows=749 - -[.contract-item] -[[FungibleTokenModule-_burn]] -==== `[.contract-item-name]#++_burn++#++(account: Either, value: Uint<128>) → []++` [.item-kind]#circuit# - -Destroys a `value` amount of tokens from `account`, lowering the total supply. -Relies on the `_update` mechanism. - -Requirements: - -- Contract is initialized. -- `account` is not the zero address. -- `account` must have at least a balance of `value`. - -Constraints: - -- k=10, rows=773 - -[.contract-item] -[[FungibleTokenModule-_approve]] -==== `[.contract-item-name]#++_approve++#++(owner: Either, spender: Either, value: Uint<128>) → []++` [.item-kind]#circuit# - -Sets `value` as the allowance of `spender` over the ``owner``'s tokens. -This circuit is equivalent to `approve`, and can be used to e.g. set automatic allowances for certain subsystems, etc. - -Requirements: - -- Contract is initialized. -- `owner` is not the zero address. -- `spender` is not the zero address. - -Constraints: - -- k=10, rows=583 - -[.contract-item] -[[FungibleTokenModule-_spendAllowance]] -==== `[.contract-item-name]#++_spendAllowance++#++(owner: Either, spender: Either, value: Uint<128>) → []++` [.item-kind]#circuit# - -Updates ``owner``'s allowance for `spender` based on spent `value`. -Does not update the allowance value in case of infinite allowance. - -Requirements: - -- Contract is initialized. -- `spender` must have at least an allowance of `value` from `owner`. - -Constraints: - -- k=10, rows=931 diff --git a/docs/modules/ROOT/pages/api/multitoken.adoc b/docs/modules/ROOT/pages/api/multitoken.adoc deleted file mode 100644 index 05cd810c..00000000 --- a/docs/modules/ROOT/pages/api/multitoken.adoc +++ /dev/null @@ -1,313 +0,0 @@ -:github-icon: pass:[] -:multiToken-guide: xref:multitoken.adoc[MultiToken guide] -:erc1155-metadata: https://eips.ethereum.org/EIPS/eip-1155#metadata[ERC1155-Metadata] - -= MultiToken - -This module provides the full MultiToken module API. - -TIP: For an overview of the module, read the {multiToken-guide}. - -== Core - -[.contract] -[[MultiToken]] -=== `++MultiToken++` link:https://github.com/OpenZeppelin/compact-contracts/blob/main/contracts/multiToken/src/MultiToken.compact[{github-icon},role=heading-link] - -[.hljs-theme-dark] -```ts -import "./node_modules/@openzeppelin-compact/contracts/src/token/MultiToken"; -``` - -[.contract-index] -.Circuits --- - -[.sub-index#MultiTokenModule] -* xref:#MultiTokenModule-initialize[`++initialize(uri_)++`] -* xref:#MultiTokenModule-uri[`++uri(id)++`] -* xref:#MultiTokenModule-balanceOf[`++balanceOf(account, id)++`] -* xref:#MultiTokenModule-setApprovalForAll[`++setApprovalForAll(operator, approved)++`] -* xref:#MultiTokenModule-isApprovedForAll[`++isApprovedForAll(account, operator)++`] -* xref:#MultiTokenModule-transferFrom[`++transferFrom(from, to, id, value)++`] -* xref:#MultiTokenModule-_transfer[`++_transfer(from, to, id, value)++`] -* xref:#MultiTokenModule-_update[`++_update(from, to, id, value)++`] -* xref:#MultiTokenModule-_unsafeTransferFrom[`++_unsafeTransferFrom(from, to, id, value)++`] -* xref:#MultiTokenModule-_unsafeTransfer[`++_unsafeTransfer(from, to, id, value)++`] -* xref:#MultiTokenModule-_setURI[`++_setURI(newURI)++`] -* xref:#MultiTokenModule-_mint[`++_mint(to, id, value)++`] -* xref:#MultiTokenModule-_unsafeMint[`++_unsafeMint(to, id, value)++`] -* xref:#MultiTokenModule-_burn[`++_burn(from, id, value)++`] -* xref:#MultiTokenModule-_setApprovalForAll[`++_setApprovalForAll(owner, operator, approved)++`] --- - -[.contract-item] -[[MultiTokenModule-initialize]] -==== `[.contract-item-name]#++initialize++#++(uri_: Opaque<"string">) → []++` [.item-kind]#circuit# - -Initializes the contract by setting the base URI for all tokens. - -This MUST be called in the implementing contract's constructor. -Failure to do so can lead to an irreparable contract. - -Requirements: - -- Contract is not initialized. - -Constraints: - -- k=10, rows=45 - -[.contract-item] -[[MultiTokenModule-uri]] -==== `[.contract-item-name]#++uri++#++(id: Uint<128>) → Opaque<"string">++` [.item-kind]#circuit# - -This implementation returns the same URI for *all* token types. -It relies on the token type ID substitution mechanism defined in the EIP: {erc1155-metadata}. -Clients calling this function must replace the `\{id\}` substring with the actual token type ID. - -Requirements: - -- Contract is initialized. - -Constraints: - -- k=10, rows=90 - -[.contract-item] -[[MultiTokenModule-balanceOf]] -==== `[.contract-item-name]#++balanceOf++#++(account: Either, id: Uint<128>) → Uint<128>++` [.item-kind]#circuit# - -Returns the amount of `id` tokens owned by `account`. - -Requirements: - -- Contract is initialized. - -Constraints: - -- k=10, rows=439 - -[.contract-item] -[[MultiTokenModule-setApprovalForAll]] -==== `[.contract-item-name]#++setApprovalForAll++#++(operator: Either, approved: Boolean) → []++` [.item-kind]#circuit# - -Enables or disables approval for `operator` to manage all of the caller's assets. - -Requirements: - -- Contract is initialized. -- `operator` is not the zero address. - -Constraints: - -- k=10, rows=404 - -[.contract-item] -[[MultiTokenModule-isApprovedForAll]] -==== `[.contract-item-name]#++balanceOf++#++(account: Either, operator: Either) → Boolean++` [.item-kind]#circuit# - -Queries if `operator` is an authorized operator for `owner`. - -Requirements: - -- Contract is initialized. - -Constraints: - -- k=10, rows=619 - -[.contract-item] -[[MultiTokenModule-transferFrom]] -==== `[.contract-item-name]#++transferFrom++#++(from: Either, to: Either, id: Uint<128>, value: Uint<128>) → []++` [.item-kind]#circuit# - -Transfers ownership of `value` amount of `id` tokens from `from` to `to`. -The caller must be `from` or approved to transfer on their behalf. - -NOTE: Transfers to contract addresses are currently disallowed until contract-to-contract interactions are supported in Compact. -This restriction prevents assets from being inadvertently locked in contracts that cannot currently handle token receipt. - -Requirements: - -- Contract is initialized. -- `to` is not a ContractAddress. -- `to` is not the zero address. -- `from` is not the zero address. -- Caller must be `from` or approved via `setApprovalForAll`. -- `from` must have an `id` balance of at least `value`. - -Constraints: - -- k=11, rows=1882 - -[.contract-item] -[[MultiTokenModule-_transfer]] -==== `[.contract-item-name]#++_transfer++#++(from: Either, to: Either, id: Uint<128>, value: Uint<128>)++` [.item-kind]#circuit# - -Transfers ownership of `value` amount of `id` tokens from `from` to `to`. -Does not impose restrictions on the caller, making it suitable for composition in higher-level contract logic. - -NOTE: Transfers to contract addresses are currently disallowed until contract-to-contract interactions are supported in Compact. -This restriction prevents assets from being inadvertently locked in contracts that cannot currently handle token receipt. - -Requirements: - -- Contract is initialized. -- `to` is not a ContractAddress. -- `to` is not the zero address. -- `from` is not the zero address. -- `from` must have an `id` balance of at least `value`. - -Constraints: - -- k=11, rows=1487 - -[.contract-item] -[[MultiTokenModule-_update]] -==== `[.contract-item-name]#++_update++#++(from: Either, to: Either, id: Uint<128>, value: Uint<128>)++` [.item-kind]#internal# - -Transfers a value amount of tokens of type id from from to to. -This circuit will mint (or burn) if `from` (or `to`) is the zero address. - -Requirements: - -- Contract is initialized. -- If `from` is not zero, the balance of `id` of `from` must be >= `value`. - -Constraints: - -- k=11, rows=1482 - -[.contract-item] -[[MultiTokenModule-_unsafeTransferFrom]] -==== `[.contract-item-name]#++_unsafeTransferFrom++#++(from: Either, to: Either, id: Uint<128>, value: Uint<128>) → []++` [.item-kind]#circuit# - -Unsafe variant of <> which allows transfers to contract addresses. -The caller must be `from` or approved to transfer on their behalf. - -WARNING: Transfers to contract addresses are considered unsafe because contract-to-contract calls are not currently supported. Tokens sent to a contract address may become irretrievable. -Once contract-to-contract calls are supported, this circuit may be deprecated. - -Requirements: - -- Contract is initialized. -- `to` is not the zero address. -- `from` is not the zero address. -- Caller must be `from` or approved via `setApprovalForAll`. -- `from` must have an `id` balance of at least `value`. - -Constraints: - -- k=11, rows=1881 - -[.contract-item] -[[MultiTokenModule-_unsafeTransfer]] -==== `[.contract-item-name]#++_unsafeTransfer++#++(from: Either, to: Either, id: Uint<128>, value: Uint<128>) → []++` [.item-kind]#circuit# - -Unsafe variant of <> which allows transfers to contract addresses. -Does not impose restrictions on the caller, making it suitable as a low-level building block for advanced contract logic. - -WARNING: Transfers to contract addresses are considered unsafe because contract-to-contract calls are not currently supported. Tokens sent to a contract address may become irretrievable. -Once contract-to-contract calls are supported, this circuit may be deprecated. - -Requirements: - -- Contract is initialized. -- `from` is not the zero address. -- `to` is not the zero address. -- `from` must have an `id` balance of at least `value`. - -Constraints: - -- k=11, rows=1486 - -[.contract-item] -[[MultiTokenModule-_setURI]] -==== `[.contract-item-name]#++_setURI++#++(newURI: Opaque<"string">) → []++` [.item-kind]#circuit# - -Sets a new URI for all token types, by relying on the token type ID substitution mechanism defined in the MultiToken standard. -See https://eips.ethereum.org/EIPS/eip-1155#metadata. - -By this mechanism, any occurrence of the `\{id\}` substring -in either the URI or any of the values in the JSON file at said URI will be replaced by clients with the token type ID. - -For example, the `https://token-cdn-domain/\{id\}.json` URI would be interpreted by clients as -`https://token-cdn-domain/000000000000000000000000000000000000000000000000000000000004cce0.json` for token type ID 0x4cce0. - -Requirements: - -- Contract is initialized. - -Constraints: - -- k=10, rows=39 - -[.contract-item] -[[MultiTokenModule-_mint]] -==== `[.contract-item-name]#++_mint++#++(to: Either, id: Uint<128>, value: Uint<128>) → []++` [.item-kind]#circuit# - -Creates a `value` amount of tokens of type `token_id`, and assigns them to `to`. - -NOTE: Transfers to contract addresses are currently disallowed until contract-to-contract interactions are supported in Compact. -This restriction prevents assets from being inadvertently locked in contracts that cannot currently handle token receipt. - -Requirements: - -- Contract is initialized. -- `to` is not the zero address. -- `to` is not a ContractAddress - -Constraints: - -- k=10, rows=912 - -[.contract-item] -[[MultiTokenModule-_unsafeMint]] -==== `[.contract-item-name]#++_unsafeMint++#++(to: Either, id: Uint<128>, value: Uint<128>) → []++` [.item-kind]#circuit# - -Unsafe variant of `_mint` which allows transfers to contract addresses. - -WARNING: Transfers to contract addresses are considered unsafe because contract-to-contract calls are not currently supported. -Tokens sent to a contract address may become irretrievable. -Once contract-to-contract calls are supported, this circuit may be deprecated. - -Requirements: - -- Contract is initialized. -- `to` is not the zero address. - -Constraints: - -- k=10, rows=911 - -[.contract-item] -[[MultiTokenModule-_burn]] -==== `[.contract-item-name]#++_burn++#++(from: Either, id: Uint<128>, value: Uint<128>) → []++` [.item-kind]#circuit# - -Destroys a `value` amount of tokens of type `token_id` from `from`. - -Requirements: - -- Contract is initialized. -- `from` is not the zero address. -- `from` must have an `id` balance of at least `value`. - -Constraints: - -- k=10, rows=688 - -[.contract-item] -[[MultiTokenModule-_setApprovalForAll]] -==== `[.contract-item-name]#++_setApprovalForAll++#++(owner: Either, operator: Either, approved: Boolean) → []++` [.item-kind]#circuit# - -Enables or disables approval for `operator` to manage all of the caller's assets. -This circuit does not check for access permissions but can be useful as a building block for more complex contract logic. - -Requirements: - -- Contract is initialized. -- `operator` is not the zero address. - -Constraints: - -- k=10, rows=518 diff --git a/docs/modules/ROOT/pages/api/nonFungibleToken.adoc b/docs/modules/ROOT/pages/api/nonFungibleToken.adoc deleted file mode 100644 index df015904..00000000 --- a/docs/modules/ROOT/pages/api/nonFungibleToken.adoc +++ /dev/null @@ -1,489 +0,0 @@ -:github-icon: pass:[] -:nonfungible-guide: xref:nonFungibleToken.adoc[NonFungibleToken guide] - -= NonFungibleToken - -This module provides the full NonFungibleToken module API. - -TIP: For an overview of the module, read the {nonfungible-guide}. - -== Core - -[.contract] -[[NonFungibleToken]] -=== `++NonFungibleToken++` link:https://github.com/OpenZeppelin/compact-contracts/blob/main/contracts/nonFungibleToken/src/NonFungibleToken.compact[{github-icon},role=heading-link] - -[.hljs-theme-dark] -```ts -import "./node_modules/@openzeppelin-compact/contracts/src/token/NonFungibleToken"; -``` - -[.contract-index] -.Circuits --- -[.sub-index#NonFungibleTokenModule] -* xref:#NonFungibleTokenModule-initialize[`++initialize(name_, symbol_)++`] -* xref:#NonFungibleTokenModule-balanceOf[`++balanceOf(owner)++`] -* xref:#NonFungibleTokenModule-ownerOf[`++ownerOf(tokenId)++`] -* xref:#NonFungibleTokenModule-name[`++name()++`] -* xref:#NonFungibleTokenModule-symbol[`++symbol()++`] -* xref:#NonFungibleTokenModule-tokenURI[`++tokenURI(tokenId)++`] -* xref:#NonFungibleTokenModule-_setTokenURI[`++_setTokenURI(tokenId, tokenURI)++`] -* xref:#NonFungibleTokenModule-approve[`++approve(to, tokenId)++`] -* xref:#NonFungibleTokenModule-getApproved[`++getApproved(tokenId)++`] -* xref:#NonFungibleTokenModule-setApprovalForAll[`++setApprovalForAll(operator, approved)++`] -* xref:#NonFungibleTokenModule-isApprovedForAll[`++isApprovedForAll(owner, operator)++`] -* xref:#NonFungibleTokenModule-transferFrom[`++transferFrom(from, to, tokenId)++`] -* xref:#NonFungibleTokenModule-_unsafeTransferFrom[`++_unsafeTransferFrom(from, to, tokenId)++`] -* xref:#NonFungibleTokenModule-_ownerOf[`++_ownerOf(tokenId)++`] -* xref:#NonFungibleTokenModule-_getApproved[`++_getApproved(tokenId)++`] -* xref:#NonFungibleTokenModule-_isAuthorized[`++_isAuthorized(owner, spender, tokenId)++`] -* xref:#NonFungibleTokenModule-_checkAuthorized[`++_checkAuthorized(owner, spender, tokenId)++`] -* xref:#NonFungibleTokenModule-_update[`++_update(to, tokenId, auth)++`] -* xref:#NonFungibleTokenModule-_mint[`++_mint(to, tokenId)++`] -* xref:#NonFungibleTokenModule-_unsafeMint[`++_unsafeMint(to, tokenId)++`] -* xref:#NonFungibleTokenModule-_burn[`++_burn(tokenId)++`] -* xref:#NonFungibleTokenModule-_transfer[`++_transfer(from, to, tokenId)++`] -* xref:#NonFungibleTokenModule-_unsafeTransfer[`++_unsafeTransfer(from, to, tokenId)++`] -* xref:#NonFungibleTokenModule-_approve[`++_approve(to, tokenId, auth)++`] -* xref:#NonFungibleTokenModule-_setApprovalForAll[`++_setApprovalForAll(owner, operator, approved)++`] -* xref:#NonFungibleTokenModule-_requireOwned[`++_requireOwned(tokenId)++`] --- - -[.contract-item] -[[NonFungibleTokenModule-initialize]] -==== `[.contract-item-name]#++initialize++#++(name_: Opaque<"string">, symbol_: Opaque<"string">) → []++` [.item-kind]#circuit# - -Initializes the contract by setting the name and symbol. - -This MUST be called in the implementing contract's constructor. -Failure to do so can lead to an irreparable contract. - -Requirements: - -- Contract is not initialized. - -Constraints: - -- k=10, rows=65 - -[.contract-item] -[[NonFungibleTokenModule-balanceOf]] -==== `[.contract-item-name]#++balanceOf++#++(owner: Either) → Uint<128>++` [.item-kind]#circuit# - -Returns the number of tokens in ``owner``'s account. - -Requirements: - -- Contract is initialized. - -Constraints: - -- k=10, rows=309 - -[.contract-item] -[[NonFungibleTokenModule-ownerOf]] -==== `[.contract-item-name]#++ownerOf++#++(tokenId: Uint<128>) → Either++` [.item-kind]#circuit# - -Returns the owner of the `tokenId` token. - -Requirements: - -- The contract is initialized. -- The `tokenId` must exist. - -Constraints: - -- k=10, rows=290 - -[.contract-item] -[[NonFungibleTokenModule-name]] -==== `[.contract-item-name]#++name++#++() → Opaque<"string">++` [.item-kind]#circuit# - -Returns the token name. - -Requirements: - -- Contract is initialized. - -Constraints: - -- k=10, rows=36 - -[.contract-item] -[[NonFungibleTokenModule-symbol]] -==== `[.contract-item-name]#++symbol++#++() → Opaque<"string">++` [.item-kind]#circuit# - -Returns the symbol of the token. - -Requirements: - -- Contract is initialized. - -Constraints: - -- k=10, rows=36 - -[.contract-item] -[[NonFungibleTokenModule-tokenURI]] -==== `[.contract-item-name]#++tokenURI++#++(tokenId: Uint<128>) → Opaque<"string">++` [.item-kind]#circuit# - -Returns the token URI for the given `tokenId`. -Returns an empty string if a tokenURI does not exist. - -Requirements: - -- The contract is initialized. -- The `tokenId` must exist. - -NOTE: Native strings and string operations aren't supported within the Compact language, e.g. concatenating a base URI + token ID is not possible like in other NFT implementations. -Therefore, we propose the URI storage approach; whereby, NFTs may or may not have unique "base" URIs. -It's up to the implementation to decide on how to handle this. - -Constraints: - -- k=10, rows=296 - -[.contract-item] -[[NonFungibleTokenModule-_setTokenURI]] -==== `[.contract-item-name]#++_setTokenURI++#++(tokenId: Uint<128>, tokenURI: Opaque<"string">) → []++` [.item-kind]#circuit# - -Sets the the URI as `tokenURI` for the given `tokenId`. - -Requirements: - -- The contract is initialized. -- The `tokenId` must exist. - -NOTE: The URI for a given NFT is usually set when the NFT is minted. - -Constraints: - -- k=10, rows=253 - -[.contract-item] -[[NonFungibleTokenModule-approve]] -==== `[.contract-item-name]#++approve++#++(to: Either, tokenId: Uint<128>) → []++` [.item-kind]#circuit# - -Gives permission to `to` to transfer `tokenId` token to another account. -The approval is cleared when the token is transferred. - -Only a single account can be approved at a time, so approving the zero address clears previous approvals. - - -Requirements: - -- The contract is initialized. -- The caller must either own the token or be an approved operator. -- `tokenId` must exist. - -Constraints: - -- k=10, rows=966 - -[.contract-item] -[[NonFungibleTokenModule-getApproved]] -==== `[.contract-item-name]#++getApproved++#++(tokenId: Uint<128>) → Either++` [.item-kind]#circuit# - -Returns the account approved for `tokenId` token. - -Requirements: - -- The contract is initialized. -- `tokenId` must exist. - -Constraints: - -- k=10, rows=409 - -[.contract-item] -[[NonFungibleTokenModule-setApprovalForAll]] -==== `[.contract-item-name]#++setApprovalForAll++#++(operator: Either, approved: Boolean) → []++` [.item-kind]#circuit# - -Approve or remove `operator` as an operator for the caller. -Operators can call <> for any token owned by the caller. - -Requirements: - -- The contract is initialized. -- The `operator` cannot be the zero address. - -Constraints: - -- k=10, rows=409 - -[.contract-item] -[[NonFungibleTokenModule-isApprovedForAll]] -==== `[.contract-item-name]#++isApprovedForAll++#++(owner: Either, operator: Either) → Boolean++` [.item-kind]#circuit# - -Returns if the `operator` is allowed to manage all of the assets of `owner`. - -Requirements: - -- The contract must have been initialized. - -Constraints: - -- k=10, rows=621 - -[.contract-item] -[[NonFungibleTokenModule-transferFrom]] -==== `[.contract-item-name]#++transferFrom++#++(from: Either, to: Either, tokenId: Uint<128>) → []++` [.item-kind]#circuit# - -Transfers `tokenId` token from `from` to `to`. - -NOTE: Transfers to contract addresses are currently disallowed until contract-to-contract interactions are supported in Compact. -This restriction prevents assets from being inadvertently locked in contracts that cannot currently handle token receipt. - -Requirements: - -- The contract is initialized. -- `from` is not the zero address. -- `to` is not the zero address. -- `to` is not a ContractAddress. -- `tokenId` token must be owned by `from`. -- If the caller is not `from`, it must be approved to move this token by either <> or <>. - -Constraints: - -- k=11, rows=1966 - -[.contract-item] -[[NonFungibleTokenModule-_unsafeTransferFrom]] -==== `[.contract-item-name]#++_unsafeTransferFrom++#++(from: Either, to: Either, tokenId: Uint<128>) → []++` [.item-kind]#circuit# - -Unsafe variant of <> which allows transfers to contract addresses. - -WARNING: Transfers to contract addresses are considered unsafe because contract-to-contract calls are not currently supported. -Tokens sent to a contract address may become irretrievable. -Once contract-to-contract calls are supported, this circuit may be deprecated. - -Requirements: - -- The contract is initialized. -- `from` is not the zero address. -- `to` is not the zero address. -- `tokenId` token must be owned by `from`. -- If the caller is not `from`, it must be approved to move this token by either <> or <>. - -Constraints: - -- k=11, rows=1963 - -[.contract-item] -[[NonFungibleTokenModule-_ownerOf]] -==== `[.contract-item-name]#++_ownerOf++#++(tokenId: Uint<128>) → Either++` [.item-kind]#circuit# - -Returns the owner of the `tokenId`. Does NOT revert if token doesn't exist - -Requirements: - -- The contract is initialized. - -Constraints: - -- k=10, rows=253 - -[.contract-item] -[[NonFungibleTokenModule-_getApproved]] -==== `[.contract-item-name]#++_getApproved++#++(tokenId: Uint<128>) → Either++` [.item-kind]#circuit# - -Returns the approved address for `tokenId`. Returns the zero address if `tokenId` is not minted. - -Requirements: - -- The contract is initialized. - -Constraints: - -- k=10, rows=253 - -[.contract-item] -[[NonFungibleTokenModule-_isAuthorized]] -==== `[.contract-item-name]#++_isAuthorized++#++(owner: Either, spender: Either, tokenId: Uint<128> ) → Boolean++` [.item-kind]#circuit# - -Returns whether `spender` is allowed to manage ``owner``'s tokens, or `tokenId` in particular (ignoring whether it is owned by `owner`). - -Requirements: - -- The contract is initialized. - -WARNING: This function assumes that `owner` is the actual owner of `tokenId` and does not verify this assumption. - -Constraints: - -- k=11, rows=1098 - -[.contract-item] -[[NonFungibleTokenModule-_checkAuthorized]] -==== `[.contract-item-name]#++_checkAuthorized++#++(owner: Either, spender: Either, tokenId: Uint<128> ) → []++` [.item-kind]#circuit# - -Checks if `spender` can operate on `tokenId`, assuming the provided `owner` is the actual owner. - -Requirements: - -- The contract is initialized. -- `spender` has approval from `owner` for `tokenId` OR `spender` has approval to manage all of `owner`'s assets. - -WARNING: This function assumes that `owner` is the actual owner of `tokenId` and does not verify this assumption. - -Constraints: - -- k=11, rows=1121 - -[.contract-item] -[[NonFungibleTokenModule-_update]] -==== `[.contract-item-name]#++_update++#++(to: Either, tokenId: Uint<128>, auth: Either) → Either++` [.item-kind]#internal# - -Transfers `tokenId` from its current owner to `to`, or alternatively mints (or burns) if the current owner (or `to`) is the zero address. -Returns the owner of the `tokenId` before the update. - -Requirements: - -- The contract is initialized. -- If `auth` is non 0, then this function will check that `auth` is either the owner of the token, or approved to operate on the token (by the owner). - -Constraints: - -- k=12, rows=2049 - -[.contract-item] -[[NonFungibleTokenModule-_mint]] -==== `[.contract-item-name]#++_mint++#++(to: Either, tokenId: Uint<128>) → []++` [.item-kind]#circuit# - -Mints `tokenId` and transfers it to `to`. - -Requirements: - -- The contract is initialized. -- `tokenId` must not exist. -- `to` is not the zero address. -- `to` is not a ContractAddress. - -Constraints: - -- k=10, rows=1013 - -[.contract-item] -[[NonFungibleTokenModule-_unsafeMint]] -==== `[.contract-item-name]#++_unsafeMint++#++(account: Either, value: Uint<128>) → []++` [.item-kind]#circuit# - -Unsafe variant of <> which allows transfers to contract addresses. - -Requirements: - -- Contract is initialized. -- `tokenId` must not exist. -- `to` is not the zero address. - -WARNING: Transfers to contract addresses are considered unsafe because contract-to-contract calls are not currently supported. -Tokens sent to a contract address may become irretrievable. -Once contract-to-contract calls are supported, this circuit may be deprecated. - -Constraints: - -- k=10, rows=1010 - -[.contract-item] -[[NonFungibleTokenModule-_burn]] -==== `[.contract-item-name]#++_burn++#++(tokenId: Uint<128>) → []++` [.item-kind]#circuit# - -Destroys `tokenId`. -The approval is cleared when the token is burned. -This circuit does not check if the sender is authorized to operate on the token. - -Requirements: - -- The contract is initialized. -- `tokenId` must exist. - -Constraints: - -- k=10, rows=479 - -[.contract-item] -[[NonFungibleTokenModule-_transfer]] -==== `[.contract-item-name]#++_transfer++#++(from: Either, to: Either, tokenId: Uint<128>) → []++` [.item-kind]#circuit# - -Transfers `tokenId` from `from` to `to`. As opposed to <>, this imposes no restrictions on `ownPublicKey()`. - -NOTE: Transfers to contract addresses are currently disallowed until contract-to-contract interactions are supported in Compact. -This restriction prevents assets from being inadvertently locked in contracts that cannot currently handle token receipt. - -Requirements: - -- The contract is initialized. -- `to` is not the zero address. -- `to` is not a ContractAddress. -- `tokenId` token must be owned by `from`. - -Constraints: - -- k=11, rows=1224 - -[.contract-item] -[[NonFungibleTokenModule-_unsafeTransfer]] -==== `[.contract-item-name]#++_unsafeTransfer++#++(from: Either, to: Either, tokenId: Uint<128>) → []++` [.item-kind]#circuit# - -Unsafe variant of <> which allows transfers to contract addresses. - -Transfers `tokenId` from `from` to `to`. As opposed to <>, this imposes no restrictions on `ownPublicKey()`. It does NOT check if the recipient is a `ContractAddress`. - -WARNING: Transfers to contract addresses are considered unsafe because contract-to-contract calls are not currently supported. Tokens sent to a contract address may become irretrievable. -Once contract-to-contract calls are supported, this circuit may be deprecated. - -Requirements: - -- Contract is initialized. -- `to` is not the zero address. -- `tokenId` token must be owned by `from`. - -Constraints: - -- k=11, rows=1221 - -[.contract-item] -[[NonFungibleTokenModule-_approve]] -==== `[.contract-item-name]#++_approve++#++(to: Either, tokenId: Uint<128>, auth: Either) → []++` [.item-kind]#circuit# - -Approve `to` to operate on `tokenId` - -Requirements: - -- The contract is initialized. -- If `auth` is non 0, then this function will check that `auth` is either the owner of the token, or approved to operate on the token (by the owner). - -Constraints: - -- k=11, rows=1109 - -[.contract-item] -[[NonFungibleTokenModule-_setApprovalForAll]] -==== `[.contract-item-name]#++_setApprovalForAll++#++(owner: Either, operator: Either, approved: Boolean) → []++` [.item-kind]#circuit# - -Approve `operator` to operate on all of `owner` tokens - -Requirements: - -- The contract is initialized. -- `operator` is not the zero address. - -Constraints: - -- k=10, rows=524 - -[.contract-item] -[[NonFungibleTokenModule-_requireOwned]] -==== `[.contract-item-name]#++_requireOwned++#++(tokenId: Uint<128>) → Either++` [.item-kind]#circuit# - -Reverts if the `tokenId` doesn't have a current owner (it hasn't been minted, or it has been burned). -Returns the owner. - -Requirements: - -- The contract is initialized. -- `tokenId` must exist. - -Constraints: - -- k=10, rows=288 diff --git a/docs/modules/ROOT/pages/api/security.adoc b/docs/modules/ROOT/pages/api/security.adoc deleted file mode 100644 index 3d4e0e18..00000000 --- a/docs/modules/ROOT/pages/api/security.adoc +++ /dev/null @@ -1,161 +0,0 @@ -:github-icon: pass:[] -:security-guide: xref:security.adoc[Security guide] - -= Security - -This directory provides the API for all Security modules. - -TIP: For an overview of the module, read the {security-guide}. - -== Initializable - -[.contract] -[[Initializable]] -=== `++Initializable++` link:https://github.com/OpenZeppelin/compact-contracts/blob/main/contracts/utils/src/Initializable.compact[{github-icon},role=heading-link] - -[.hljs-theme-dark] -```ts -import "./node_modules/@openzeppelin-compact/contracts/src/security/Initializable"; -``` - -[.contract-index] -.Circuits --- - -[.sub-index#InitializableModule] -* xref:#InitializableModule-initialize[`++initialize()++`] -* xref:#InitializableModule-assertInitialized[`++assertInitialized()++`] -* xref:#InitializableModule-assertNotInitialized[`++assertNotInitialized()++`] --- - -[.contract-item] -[[InitializableModule-initialize]] -==== `[.contract-item-name]#++initialize++#++() → []++` [.item-kind]#circuit# - -Initializes the state thus ensuring the calling circuit can only be called once. - -Requirements: - -- Contract must not be initialized. - -Constraints: - -- k=10, rows=38 - -[.contract-item] -[[InitializableModule-assertInitialized]] -==== `[.contract-item-name]#++assertInitialized++#++() → []++` [.item-kind]#circuit# - -Asserts that the contract has been initialized, throwing an error if not. - -Requirements: - -- Contract must be initialized. - -Constraints: - -- k=10, rows=31 - -[.contract-item] -[[InitializableModule-assertNotInitialized]] -==== `[.contract-item-name]#++assertNotInitialized++#++() → []++` [.item-kind]#circuit# - -Asserts that the contract has not been initialized, throwing an error if it has. - -Requirements: - -- Contract must not be initialized. - -Constraints: - -- k=10, rows=35 - -== Pausable - -[.contract] -[[Pausable]] -=== `++Pausable++` link:https://github.com/OpenZeppelin/compact-contracts/blob/main/contracts/utils/src/Pausable.compact[{github-icon},role=heading-link] - -[.hljs-theme-dark] -```ts -import "./node_modules/@openzeppelin-compact/contracts/src/security/Pausable"; - -``` - -[.contract-index] -.Circuits --- - -[.sub-index#PausableModule] -* xref:#PausableModule-isPaused[`++isPaused()++`] -* xref:#PausableModule-assertPaused[`++assertPaused()++`] -* xref:#PausableModule-assertNotPaused[`++assertNotPaused()++`] -* xref:#PausableModule-_pause[`++_pause()++`] -* xref:#PausableModule-_unpause[`++_unpause()++`] --- - -[.contract-item] -[[PausableModule-isPaused]] -==== `[.contract-item-name]#++isPaused++#++() → Boolean++` [.item-kind]#circuit# - -Returns true if the contract is paused, and false otherwise. - -Constraints: - -- k=10, rows=32 - -[.contract-item] -[[PausableModule-assertPaused]] -==== `[.contract-item-name]#++assertPaused++#++() → []++` [.item-kind]#circuit# - -Makes a circuit only callable when the contract is paused. - -Requirements: - -- Contract must be paused. - -Constraints: - -- k=10, rows=31 - -[.contract-item] -[[PausableModule-assertNotPaused]] -==== `[.contract-item-name]#++assertNotPaused++#++() → []++` [.item-kind]#circuit# - -Makes a circuit only callable when the contract is not paused. - -Requirements: - -- Contract must not be paused. - -Constraints: - -- k=10, rows=35 - -[.contract-item] -[[PausableModule-_pause]] -==== `[.contract-item-name]#++_pause++#++() → []++` [.item-kind]#circuit# - -Triggers a stopped state. - -Requirements: - -- Contract must not be paused. - -Constraints: - -- k=10, rows=38 - -[.contract-item] -[[PausableModule-_unpause]] -==== `[.contract-item-name]#++_unpause++#++() → []++` [.item-kind]#circuit# - -Lifts the pause on the contract. - -Requirements: - -- Contract must be paused. - -Constraints: - -- k=10, rows=34 diff --git a/docs/modules/ROOT/pages/api/shieldedAccessControl.adoc b/docs/modules/ROOT/pages/api/shieldedAccessControl.adoc deleted file mode 100644 index e69de29b..00000000 diff --git a/docs/modules/ROOT/pages/api/utils.adoc b/docs/modules/ROOT/pages/api/utils.adoc deleted file mode 100644 index 5e123aee..00000000 --- a/docs/modules/ROOT/pages/api/utils.adoc +++ /dev/null @@ -1,67 +0,0 @@ -:github-icon: pass:[] -:utils-guide: xref:utils.adoc[Utils guide] - -= Utils - -This directory provides the API for all Utils modules. - -TIP: For an overview of the module, read the {utils-guide}. - -== Utils - -[.hljs-theme-dark] -```ts -import "./node_modules/@openzeppelin-compact/contracts/src/utils/Utils" prefix Utils_; -``` - -[.contract] -[[Utils]] -=== `++Utils++` link:https://github.com/OpenZeppelin/compact-contracts/blob/main/contracts/utils/src/Utils.compact[{github-icon},role=heading-link] - -NOTE: There's no easy way to get the constraints of pure circuits at this time so the constraints of the circuits listed below have been omitted. - -[.contract-index] -.Circuits --- - -[.sub-index#UtilsModule] -* xref:#UtilsModule-isKeyOrAddressZero[`++isKeyOrAddressZero(keyOrAddress)++`] -* xref:#UtilsModule-isKeyZero[`++isKeyZero(key)++`] -* xref:#UtilsModule-isKeyOrAddressEqual[`++isKeyOrAddressEqual(keyOrAddress, other)++`] -* xref:#UtilsModule-isContractAddress[`++isContractAddress(keyOrAddress)++`] -* xref:#UtilsModule-emptyString[`++emptyString()++`] --- - -[.contract-item] -[[UtilsModule-isKeyOrAddressZero]] -==== `[.contract-item-name]#++isKeyOrAddressZero++#++(keyOrAddress: Either) → Boolean++` [.item-kind]#circuit# - -Returns whether `keyOrAddress` is the zero address. - -NOTE: Midnight's burn address is represented as `left(default)` in Compact, -so we've chosen to represent the zero address as this structure as well - -[.contract-item] -[[UtilsModule-isKeyZero]] -==== `[.contract-item-name]#++isKeyZero++#++(key: ZswapCoinPublicKey) → Boolean++` [.item-kind]#circuit# - -Returns whether `key` is the zero address. - -[.contract-item] -[[UtilsModule-isKeyOrAddressEqual]] -==== `[.contract-item-name]#++isKeyOrAddressEqual++#++(keyOrAddress: Either, other: Either) → Boolean++` [.item-kind]#circuit# - -Returns whether `keyOrAddress` is equal to `other`. -Assumes that a `ZswapCoinPublicKey` and a `ContractAddress` can never be equal - -[.contract-item] -[[UtilsModule-isContractAddress]] -==== `[.contract-item-name]#++isContractAddress++#++(keyOrAddress: Either) → Boolean++` [.item-kind]#circuit# - -Returns whether `keyOrAddress` is a `ContractAddress` type. - -[.contract-item] -[[UtilsModule-emptyString]] -==== `[.contract-item-name]#++emptyString++#++() → Opaque<"string">++` [.item-kind]#circuit# - -A helper function that returns the empty string: "". diff --git a/docs/modules/ROOT/pages/extensibility.adoc b/docs/modules/ROOT/pages/extensibility.adoc deleted file mode 100644 index 3f3faaab..00000000 --- a/docs/modules/ROOT/pages/extensibility.adoc +++ /dev/null @@ -1,165 +0,0 @@ -# Extensibility - -[id="the_module_contract_pattern"] -## The Module/Contract Pattern - -We use the term *modular composition by delegation* to describe the practice of having contracts call into module-defined circuits to implement behavior. Rather than inheriting or overriding functionality, a contract delegates responsibility to the module by explicitly invoking its exported circuits. - -The idea is that there are two types of compact files: modules and contracts. To minimize risk, boilerplate, and avoid naming clashes, we follow these rules: - -### Modules - -Modules expose functionality through three circuit types: - -1. `internal`: private helpers → used to break up logic within the module. -2. `public`: composable building blocks → intended for contracts to use in complex flows (`_mint`, `_burn`). -3. `external`: standalone circuits → safe to expose as-is (`transfer`, `approve`). - -Modules must: - -- Export only `public` and `external` circuits. -- Prefix `public` circuits with `_` (e.g., `FungibleToken._mint`). -- Avoid `_` prefix for `external` circuits (e.g., `FungibleToken.transfer`). -- Avoid defining or calling constructors or `initialize()` directly. -- Optionally define an `initialize()` circuit for internal setup—but execution must be delegated to the contract. - -**Note**: Compact files must contain only one top-level module and all logic must be defined *inside* the module declaration. - -### Contracts - -Contracts compose behavior by explicitly invoking the relevant circuits from imported modules. Therefore, contracts: - -- Can import from modules. -- Should add prefix to imports (`import "FungibleToken" prefix FungibleToken_;`). -- Should re-expose external module circuits through wrapper circuits to control naming and layering. Avoid raw re-exports to prevent name clashes. -- Should implement constructor that calls `initialize` from imported modules. -- Must not call initializers outside of the constructor. - -This pattern balances modularity with local control, avoids tight coupling, and works within Compact’s language constraints. As Compact matures, this pattern will likely evolve as well. - -### Example contract implementing modules - -```ts -/** FungibleTokenMintablePausableOwnableContract */ - -pragma language_version >= 0.16.0; - -import CompactStandardLibrary; -import "./node_modules/@openzeppelin-compact/contracts/src/token/FungibleToken" - prefix FungibleToken_; -import "./node_modules/@openzeppelin-compact/contracts/src/security/Pausable" - prefix Pausable_; -import "./node_modules/@openzeppelin-compact/contracts/src/access/Ownable" - prefix Ownable_; - -constructor( - _name: Opaque<"string">, - _symbol: Opaque<"string">, - _decimals: Uint<8>, - _owner: Either -) { - FungibleToken_initialize(_name, _symbol, _decimals); - Ownable_initialize(_owner); -} - -/** IFungibleTokenMetadata */ - -export circuit name(): Opaque<"string"> { - return FungibleToken_name(); -} - -export circuit symbol(): Opaque<"string"> { - return FungibleToken_symbol(); -} - -export circuit decimals(): Uint<8> { - return FungibleToken_decimals(); -} - -/** IFungibleToken */ - -export circuit totalSupply(): Uint<128> { - return FungibleToken_totalSupply(); -} - -export circuit balanceOf( - account: Either -): Uint<128> { - return FungibleToken_balanceOf(account); -} - -export circuit allowance( - owner: Either, - spender: Either -): Uint<128> { - return FungibleToken_allowance(owner, spender); -} - -export circuit transfer( - to: Either, - value: Uint<128>, -): Boolean { - Pausable_assertNotPaused(); - return FungibleToken_transfer(to, value); -} - -export circuit transferFrom( - from: Either, - to: Either, - value: Uint<128>, -): Boolean { - Pausable_assertNotPaused(); - return FungibleToken_transferFrom(from, to, value); -} - -export circuit approve( - spender: Either, - value: Uint<128>, -): Boolean { - Pausable_assertNotPaused(); - return FungibleToken_approve(spender, value); -} - -/** IMintable */ - -export circuit mint( - account: Either, - value: Uint<128>, -): [] { - Pausable_assertNotPaused(); - Ownable_assertOnlyOwner(); - return FungibleToken__mint(account, value); -} - -/** IPausable */ - -export circuit isPaused(): Boolean { - return Pausable_isPaused(); -} - -export circuit pause(): [] { - Ownable_assertOnlyOwner(); - return Pausable__pause(); -} - -export circuit unpause(): [] { - Ownable_assertOnlyOwner(); - return Pausable__unpause(); -} - -/** IOwnable */ - -export circuit owner(): Either { - return Ownable_owner(); -} - -export circuit transferOwnership( - newOwner: Either -): [] { - return Ownable_transferOwnership(newOwner); -} - -export circuit renounceOwnership(): [] { - return Ownable_renounceOwnership(); -} -``` \ No newline at end of file diff --git a/docs/modules/ROOT/pages/fungibleToken.adoc b/docs/modules/ROOT/pages/fungibleToken.adoc deleted file mode 100644 index b56c42b2..00000000 --- a/docs/modules/ROOT/pages/fungibleToken.adoc +++ /dev/null @@ -1,155 +0,0 @@ -:fungible-tokens: https://docs.openzeppelin.com/contracts/5.x/tokens#different-kinds-of-tokens[fungible tokens] -:eip-20: https://eips.ethereum.org/EIPS/eip-20[EIP-20] - -= FungibleToken - -FungibleToken is a specification for {fungible-tokens}, -a type of token where all the units are exactly equal to each other. -This module is an approximation of {eip-20} written in the Compact programming language for the Midnight network. - -== ERC20 Compatbility - -Even though Midnight is not EVM-compatible, this implementation attempts to be an approximation of the standard. -Some features and behaviors are either not possible, not possible yet, -or changed because of the vastly different tech stack and Compact language constraints. - -**Notable changes** - -- **Uint<128> as value type** - Since 256-bit unsigned integers are not supported, the library uses the Compact type `Uint<128>`. - -**Features and specifications NOT supported** - -- **Events** - Midnight does not currently support events, but this is planned on being supported in the future. -- **Uint256 type** - There's ongoing research on ways to support uint256 in the future. -- **Interface** - Compact currently does not have a way to define a contract interface. -This library offers modules of contracts with free floating circuits; -nevertheless, there are no means of enforcing that all circuits are provided. - -== Contract-to-contract calls - -Contract-to-contract calls are currently not supported in the Compact language. -Due to this limitation, the current iteration of FungibleToken disallows transfers and mints to the `ContractAddress` type. -Transferring tokens to a contract may result in those tokens being locked forever. -The FungibleToken module, however, does provide `unsafe` circuit variants for users who wish to experiment with sending tokens to contracts. - -WARNING: The `unsafe` circuits will eventually be deprecated after Compact supports contract-to-contract calls—meaning -`transfer`, `_mint`, etc. are planned to eventually allow the recipients to be of the `ContractAddress` type. - -== Usage - -:extensibility-pattern: xref:extensibility.adoc#the_module_contract_pattern[Module/Contract Pattern] -:fungible-mint: xref:/api/fungibleToken.adoc#FungibleTokenModule-_mint[_mint] - -Import the FungibleToken module into the implementing contract. -It's recommended to prefix the module with `FungibleToken_` to avoid circuit signature clashes. - -```typescript -pragma language_version >= 0.16.0; - -import CompactStandardLibrary; -import "./node_modules/@openzeppelin-compact/contracts/src/token/FungibleToken" - prefix FungibleToken_; - -constructor( - name: Opaque<"string">, - symbol: Opaque<"string">, - decimals: Uint<8>, -) { - FungibleToken_initialize(name, symbol, decimals); -} -``` - -Next, expose the ciruits that users may call in the contract. -This library enables extensibility by following the rules of the {extensibility-pattern}. -Note that circuits with a preceding underscore (`_likeThis`) are meant to be building blocks for implementing contracts. -Exposing {fungible-mint} without some sort of access control, for example, would allow ANYONE to mint tokens. - -```typescript -export circuit name(): Opaque<"string"> { - return FungibleToken_name(); -} - -export circuit symbol(): Opaque<"string"> { - return FungibleToken_symbol(); -} - -export circuit decimals(): Uint<8> { - return FungibleToken_decimals(); -} - -(...) -``` - -The following example is a simple token contract with a fixed supply that's minted to the passed recipient upon construction. - -```typescript -// FungibleTokenFixedSupply.compact - -pragma language_version >= 0.16.0; - -import CompactStandardLibrary; -import "./node_modules/@openzeppelin-compact/contracts/src/token/FungibleToken" - prefix FungibleToken_; - -constructor( - name: Opaque<"string">, - symbol: Opaque<"string">, - decimals: Uint<8>, - recipient: Either, - fixedSupply: Uint<128>, -) { - FungibleToken_initialize(name, symbol, decimals); - FungibleToken__mint(recipient, fixedSupply); -} - -export circuit name(): Opaque<"string"> { - return FungibleToken_name(); -} - -export circuit symbol(): Opaque<"string"> { - return FungibleToken_symbol(); -} - -export circuit decimals(): Uint<8> { - return FungibleToken_decimals(); -} - -export circuit totalSupply(): Uint<128> { - return FungibleToken_totalSupply(); -} - -export circuit balanceOf( - account: Either, -): Uint<128> { - return FungibleToken_balanceOf(account); -} - -export circuit allowance( - owner: Either, - spender: Either, -): Uint<128> { - return FungibleToken_allowance(owner, spender); -} - -export circuit transfer( - to: Either, - value: Uint<128>, -): Boolean { - return FungibleToken_transfer(to, value); -} - -export circuit transferFrom( - from: Either, - to: Either, - value: Uint<128>, -): Boolean { - return FungibleToken_transferFrom(from, to, value); -} - -export circuit approve( - spender: Either, - value: Uint<128>, -): Boolean { - return FungibleToken_approve(spender, value); -} -``` diff --git a/docs/modules/ROOT/pages/index.adoc b/docs/modules/ROOT/pages/index.adoc deleted file mode 100644 index 05a2a246..00000000 --- a/docs/modules/ROOT/pages/index.adoc +++ /dev/null @@ -1,90 +0,0 @@ -:midnight: https://midnight.network/[Midnight] -:nvm: https://github.com/nvm-sh/nvm[nvm] -:yarn: https://yarnpkg.com/getting-started/install[yarn] -:turbo: https://turborepo.com/docs/getting-started/installation[turbo] -:compact-dev-tools: https://docs.midnight.network/blog/compact-developer-tools[Compact Developer Tools] - -= Contracts for Compact - -*A library for secure smart contract development* written in Compact for {midnight}. -This library consists of modules to build custom smart contracts. - -WARNING: This repo contains highly experimental code. Expect rapid iteration. *Use at your own risk.* - -== Installation - -Make sure you have {nvm}, {yarn}, and {turbo} installed on your machine. - -Follow Midnight's {compact-dev-tools} installation guide and confirm that `compact` is in the `PATH` env variable. - -```bash -$ compact compile --version - -Compactc version: 0.24.0 -0.24.0 -``` - -=== Set up the project - -Clone the repository: - -```bash -git clone git@github.com:OpenZeppelin/midnight-contracts.git -``` - -`cd` into it and then install dependencies and prepare the environment: - -```bash -nvm install && \ -yarn && \ -turbo compact -``` - -== Usage - -Compile the contracts: - -```bash -$ turbo compact - -(...) -✔ [COMPILE] [1/2] Compiled FungibleToken.compact -@openzeppelin-compact/fungible-token:compact: Compactc version: 0.24.0 -@openzeppelin-compact/fungible-token:compact: -✔ [COMPILE] [1/6] Compiled Initializable.compact -@openzeppelin-compact/utils:compact: Compactc version: 0.24.0 -@openzeppelin-compact/utils:compact: -✔ [COMPILE] [2/6] Compiled Pausable.compact -@openzeppelin-compact/utils:compact: Compactc version: 0.24.0 -@openzeppelin-compact/utils:compact: -✔ [COMPILE] [3/6] Compiled Utils.compact -@openzeppelin-compact/utils:compact: Compactc version: 0.24.0 -@openzeppelin-compact/utils:compact: -✔ [COMPILE] [4/6] Compiled test/mocks/MockInitializable.compact -@openzeppelin-compact/utils:compact: Compactc version: 0.24.0 -@openzeppelin-compact/utils:compact: Compiling 3 circuits: -✔ [COMPILE] [5/6] Compiled test/mocks/MockPausable.compact -@openzeppelin-compact/utils:compact: Compactc version: 0.24.0 -@openzeppelin-compact/utils:compact: Compiling 5 circuits: -✔ [COMPILE] [6/6] Compiled test/mocks/MockUtils.compact -@openzeppelin-compact/utils:compact: Compactc version: 0.24.0 -@openzeppelin-compact/utils:compact: - -✔ [COMPILE] [2/2] Compiled test/mocks/MockFungibleToken.compact -@openzeppelin-compact/fungible-token:compact: Compactc version: 0.24.0 -@openzeppelin-compact/fungible-token:compact: Compiling 15 circuits: - - - Tasks: 2 successful, 2 total -Cached: 0 cached, 2 total - Time: 7.178s -``` - -NOTE: Speed up the development process by skipping the prover and verifier key file generation: + -`SKIP_ZK=true turbo compact` - -Run tests: - -```bash -turbo test -``` diff --git a/docs/modules/ROOT/pages/multitoken.adoc b/docs/modules/ROOT/pages/multitoken.adoc deleted file mode 100644 index 49420db8..00000000 --- a/docs/modules/ROOT/pages/multitoken.adoc +++ /dev/null @@ -1,155 +0,0 @@ -:eip-1155: https://eips.ethereum.org/EIPS/eip-1155[EIP-1155] -:erc165: https://eips.ethereum.org/EIPS/eip-165[ERC165] - -= MultiToken - -MultiToken is a specification for contracts that manage multiple token types. -This module is an approximation of {eip-1155} written in the Compact programming language for the Midnight network. - -== ERC1155 Compatbility - -Even though Midnight is not EVM-compatible, this implementation attempts to be an approximation of the standard. -Some features and behaviors are either not possible, not possible yet, or changed because of the vastly different tech stack -and Compact language constraints. - -**Notable changes** - -- **Uint<128> as value and id type** - Since 256-bit unsigned integers are not supported, the library uses the Compact type `Uint<128>`. - -**Features and specifications NOT supported** - -- **Events** - Midnight does not currently support events, but this is planned on being supported in the future. -- **Uint256 type** - There's ongoing research on ways to support uint256 in the future. -- **Interface** - Compact currently does not have a way to define a contract interface. -This library offers modules of contracts with free floating circuits; nevertheless, there's no means of enforcing that all circuits are provided. -- **Batch mint, burn, transfer** - Without support for dynamic arrays, batching transfers is difficult to do without a hacky solution. -For instance, we could change the `to` and `from` parameters to be vectors. -This would change the signature and would be both difficult to use and easy to misuse. -- **Querying batched balances** - This can be somewhat supported. -The issue, without dynamic arrays, is that the module circuit must use Vector for accounts and ids; -therefore, the implementing contract must explicitly define the number of balances to query in the circuit i.e. - -> ```ts -> balanceOfBatch_10( -> accounts: Vector<10, Either>, -> ids: Vector<10, Uint<128>> -> ): Vector<10, Uint<128>> -> ``` -> Since this module does not offer mint or transfer batching, balance batching is also not included at this time. - -- **Introspection** - Compact currently cannot support contract-to-contract queries for introspection. -{erc165} (or an equivalent thereof) is NOT included in the contract. -- **Safe transfers** - The lack of an introspection mechanism means safe transfers of any kind can not be supported. - -== Contract-to-contract calls - -Contract-to-contract calls are currently not supported in the Compact language. -Due to this limitation, the current iteration of MultiToken disallows transfers and mints to the `ContractAddress` type. -Transferring tokens to a contract may result in those tokens being locked forever. -The MultiToken module, however, does provide `unsafe` circuit variants for users who wish to experiment with sending tokens to contracts. - -WARNING: The `unsafe` circuits will eventually be deprecated after Compact supports contract-to-contract calls—meaning -`transferFrom`, `_mint`, etc. are planned to eventually allow the recipients to be of the `ContractAddress` type. - -== Usage -:extensibility-pattern: xref:extensibility.adoc#the_module_contract_pattern[Module/Contract Pattern] -:multitoken-mint: xref:/api/multitoken.adoc#MultiTokenModule-_mint[_mint] - -Import the MultiToken module into the implementing contract. -It's recommended to prefix the module with `MultiToken_` to avoid circuit signature clashes. - -```typescript -pragma language_version >= 0.16.0; - -import CompactStandardLibrary; -import "./node_modules/@openzeppelin-compact/contracts/src/token/MultiToken" - prefix MultiToken_; - -constructor( - uri: Opaque<"string">, -) { - MultiToken_initialize(uri); -} -``` - -Next, expose the ciruits that users may call in the contract. -This library enables extensibility by following the rules of the {extensibility-pattern}. -Note that circuits with a preceding underscore (`_likeThis`) are meant to be building blocks for implementing contracts. -Exposing {multitoken-mint} without some sort of access control, for example, would allow ANYONE to mint tokens. - -```typescript -export circuit uri(id: Uint<128>): Opaque<"string"> { - return MultiToken_uri(); -} - -export circuit balanceOf( - account: Either -): Uint<128> { - return MultiToken_balanceOf(account); -} - -(...) -``` - -The following example is a simple multi-token contract that creates both a fixed-supply fungible token and an NFT using the same contract. - -```typescript -// MultiTokenTwoTokenTypes.compact - -pragma language_version >= 0.16.0; - -import CompactStandardLibrary; -import "./node_modules/@openzeppelin-compact/contracts/src/token/MultiToken" - prefix MultiToken_; - -constructor( - _uri: Opaque<"string">, - recipient: Either, - fungibleFixedSupply: Uint<128>, -) { - // `initialize` sets the URI (base) for all tokens minted from this contract - MultiToken_initialize(_uri); - - // Token id `123` is a fungible token (with a fixed supply of `fungibleFixedSupply`) - const fungibleTokenId = 123; - MultiToken__mint(recipient, fungibleTokenId, fungibleFixedSupply); - - // Token id `987` is a non-fungible token (so the supply is only 1) - const nonFungibleTokenId = 987; - MultiToken__mint(recipient, nonFungibleTokenId, 1); -} - -export circuit uri(id: Uint<128>): Opaque<"string"> { - return MultiToken_uri(id); -} - -export circuit balanceOf( - account: Either, - id: Uint<128>, -): Uint<128> { - return MultiToken_balanceOf(account, id); -} - -export circuit setApprovalForAll( - operator: Either, - approved: Boolean -): [] { - return MultiToken_setApprovalForAll(operator, approved); -} - -export circuit isApprovedForAll( - account: Either, - operator: Either -): Boolean { - return MultiToken_isApprovedForAll(account, operator); -} - -export circuit transferFrom( - from: Either, - to: Either, - id: Uint<128>, - value: Uint<128>, -): [] { - return MultiToken_transferFrom(from, to, id, value); -} -``` diff --git a/docs/modules/ROOT/pages/nonFungibleToken.adoc b/docs/modules/ROOT/pages/nonFungibleToken.adoc deleted file mode 100644 index c4286402..00000000 --- a/docs/modules/ROOT/pages/nonFungibleToken.adoc +++ /dev/null @@ -1,164 +0,0 @@ -:non-fungible-tokens: https://docs.openzeppelin.com/contracts/5.x/tokens#different-kinds-of-tokens[non-fungible tokens] -:eip-721: https://eips.ethereum.org/EIPS/eip-721[EIP-721] - -= NonFungibleToken - -FungibleToken is a specification for {non-fungible-tokens}, -a type of token where all the units are unique and distinct from each other. -This module is an approximation of {eip-721} written in the Compact programming language for the Midnight network. - -== ERC721 Compatbility - -Even though Midnight is not EVM-compatible, this implementation attempts to be an approximation of the standard. -Some features and behaviors are either not possible, not possible yet, -or changed because of the vastly different tech stack and Compact language constraints. - -**Notable changes** - -- **Uint<128> tokenIds** - Since 256-bit unsigned integers are not supported, the library uses the Compact type `Uint<128>`. -- **No _baseURI() support** - Native strings and string operations are not supported within the Compact language, so concatenating a base URI + token ID is not possible like in other NFT implementations. Therefore, we propose the URI storage approach; whereby, NFTs may or may not have unique "base" URIs. It's up to the implementation to decide on how to handle this. - -**Features and specifications NOT supported** - -- **Events** - Midnight does not currently support events, but this is planned on being supported in the future. -- **Uint256 type** - There's ongoing research on ways to support uint256 in the future. -- **Interface** - Compact currently does not have a way to define a contract interface. -This library offers modules of contracts with free floating circuits; -nevertheless, there are no means of enforcing that all circuits are provided. -- **ERC-165 Standard** - Since Compact doesn't provide a way to define a contract interace, -it's not possible to implement an https://eips.ethereum.org/EIPS/eip-165[ERC-165] like interface standard at this time. -- **Safe Transfers** - It's not possible to implement safe transfers without an https://eips.ethereum.org/EIPS/eip-165[ERC-165] like -interface standard at this time. - -== Contract-to-contract calls - -Contract-to-contract calls are currently not supported in the Compact language. -Due to this limitation, the current iteration of NonFungibleToken disallows transfers and mints to the `ContractAddress` type. -Transferring tokens to a contract may result in those tokens being locked forever. -The NonFungibleToken module, however, does provide `unsafe` circuit variants for users who wish to experiment with sending tokens to contracts. - -WARNING: The `unsafe` circuits will eventually be deprecated after Compact supports contract-to-contract calls—meaning -`_transfer`, `_mint`, etc. are planned to eventually allow the recipients to be of the `ContractAddress` type. - -== Usage - -:extensibility-pattern: xref:extensibility.adoc#the_module_contract_pattern[Module/Contract Pattern] -:nonfungible-mint: xref:/api/nonFungibleToken.adoc#NonFungibleTokenModule-_mint[_mint] - -Import the NonFungibleToken module into the implementing contract. -It's recommended to prefix the module with `NonFungibleToken_` to avoid circuit signature clashes. - -```typescript -pragma language_version >= 0.16.0; - -import CompactStandardLibrary; -import "./node_modules/@openzeppelin-compact/contracts/src/token/NonFungibleToken" - prefix NonFungibleToken_; - -constructor( - name: Opaque<"string">, - symbol: Opaque<"string">, -) { - NonFungibleToken_initialize(name, symbol); -} -``` - -Next, expose the ciruits that users may call in the contract. -This library enables extensibility by following the rules of the {extensibility-pattern}. -Note that circuits with a preceding underscore (`_likeThis`) are meant to be building blocks for implementing contracts. -Exposing {nonfungible-mint} without some sort of access control, for example, would allow ANYONE to mint tokens. - -```typescript -export circuit name(): Opaque<"string"> { - return NonFungibleToken_name(); -} - -export circuit symbol(): Opaque<"string"> { - return NonFungibleToken_symbol(); -} - -(...) -``` - -The following example is a simple non-fungible token contract that mints an NFT to the passed recipient upon construction. - -```typescript -// SimpleNonFungibleToken.compact - -pragma language_version >= 0.16.0; - -import CompactStandardLibrary; -import "./node_modules/@openzeppelin-compact/contracts/src/token/NonFungibleToken" - prefix NonFungibleToken_; - -constructor( - name: Opaque<"string">, - symbol: Opaque<"string">, - recipient: Either, - tokenURI: Opaque<"string">, -) { - const tokenId = 1 as Uint<128>; - NonFungibleToken_initialize(name, symbol); - NonFungibleToken__mint(recipient, tokenId); - NonFungibleToken__setTokenURI(tokenId, tokenURI); -} - -export circuit balanceOf( - owner: Either -): Uint<128> { - return NonFungibleToken_balanceOf(owner); -} - -export circuit ownerOf( - tokenId: Uint<128>, -): Either { - return NonFungibleToken_ownerOf(tokenId); -} - -export circuit name(): Opaque<"string"> { - return NonFungibleToken_name(); -} - -export circuit symbol(): Opaque<"string"> { - return NonFungibleToken_symbol(); -} - -export circuit tokenURI(tokenId: Uint<128>): Opaque<"string"> { - return NonFungibleToken_tokenURI(tokenId); -} - -export circuit approve( - to: Either, - tokenId: Uint<128>, -): [] { - NonFungibleToken_approve(to, tokenId); -} - -export circuit getApproved( - tokenId: Uint<128>, -): Either { - return NonFungibleToken_getApproved(tokenId); -} - -export circuit setApprovalForAll( - operator: Either, - approved: Boolean -): [] { - NonFungibleToken_setApprovalForAll(operator, approved); -} - -export circuit isApprovedForAll( - owner: Either, - operator: Either -): Boolean { - return NonFungibleToken_isApprovedForAll(owner, operator); -} - -export circuit transferFrom( - from: Either, - to: Either - tokenId: Uint<128>, -): [] { - NonFungibleToken_transferFrom(from, to, tokenId); -} -``` \ No newline at end of file diff --git a/docs/modules/ROOT/pages/security.adoc b/docs/modules/ROOT/pages/security.adoc deleted file mode 100644 index 038e7bc2..00000000 --- a/docs/modules/ROOT/pages/security.adoc +++ /dev/null @@ -1,95 +0,0 @@ -= Security - -The following documentation provides context, reasoning, and examples of modules found in the Security directory. - -== Initializable - -The Initializable module provides a simple mechanism that mimics the functionality of a constructor. -More specifically, it enables logic to be performed once and only once which is useful to set up a contract’s initial state when a constructor cannot be used, for example when there are circular dependencies at construction time. - -Many modules also use the initializable pattern which ensures that implementing contracts: - -- Don't allow circuit calls until the contract is initialized. -- Can only initialize the contract once. - -=== Usage - -```typescript -// CustomContractStateSetup.compact - -pragma language_version >= 0.16.0; - -import CompactStandardLibrary; -import './node_modules/@openzeppelin-compact/contracts/src/security/Initializable'; - -export ledger _fieldAfterDeployment: Field; - -export circuit doSomethingBeforeInitialized(): [] { - // Ensure action can only be done prior to custom initialization - Initializable_assertNotInitialized(); - // Do the thing -} - -export circuit setFieldAfterDeployment(f: Field): [] { - // Initialize so the circuit cannot be called again - Initializable_initialize(); - _fieldAfterDeployment = f; -} - -export circuit checkFieldAfterDeployment(): Field { - // Can only be called after the contract is initialized - Initializable_assertInitialized(); - return _fieldAfterDeployment; -} -``` - -== Pausable - -:ownable: xref:ownable.adoc[Ownable] -:assertPaused: xref:api/utils.adoc#PausableModule-assertPaused[assertPaused] -:assertNotPaused: xref:api/utils.adoc#PausableModule-assertNotPaused[assertNotPaused] - -The Pausable module allows contracts to implement an emergency stop mechanism. -This can be useful for scenarios such as preventing trades until the end of an evaluation period or having an emergency switch to freeze all transactions in the event of a large bug. - -To become pausable, the contract should include `pause` and `unpause` circuits (which should be protected). -For circuits that should be available only when paused or not, -insert calls to {assertPaused} and {assertNotPaused} respectively. - -=== Usage - -For example (using the {ownable} module for access control): - -```typescript -// OwnablePausable.compact - -pragma language_version >= 0.16.0; - -import CompactStandardLibrary; -import './node_modules/@openzeppelin-compact/contracts/src/security/Initializable' prefix Initializable_; -import './node_modules/@openzeppelin-compact/contracts/src/access/Ownable' prefix Ownable_; - -constructor(initOwner: Either) { - Ownable_initialize(initOwner); -} - -export circuit pause(): [] { - Ownable_assertOnlyOwner(); - Pausable__pause(); -} - -export circuit unpause(): [] { - Ownable_assertOnlyOwner(); - Pausable__unpause(); -} - -export circuit whenNotPaused(): [] { - Pausable_assertNotPaused(); - // Do something -} - -export circuit whenPaused(): [] { - Pausable_assertPaused(); - // Do something -} -``` diff --git a/docs/modules/ROOT/pages/shieldedAccessControl.adoc b/docs/modules/ROOT/pages/shieldedAccessControl.adoc deleted file mode 100644 index e69de29b..00000000 diff --git a/docs/modules/ROOT/pages/utils.adoc b/docs/modules/ROOT/pages/utils.adoc deleted file mode 100644 index 5884664d..00000000 --- a/docs/modules/ROOT/pages/utils.adoc +++ /dev/null @@ -1,30 +0,0 @@ -= Utils - -The following documentation provides context, reasoning, and examples of modules found in the Utils directory. - -== Utils - -The Utils module provides miscellaneous circuits and common utilities for Compact contract development. - -=== Usage - -```typescript -// UtilsExample.compact - -pragma language_version >= 0.16.0; - -import CompactStandardLibrary; -import './node_modules/@openzeppelin-compact/contracts/src/utils/Utils'; - -export circuit performActionWhenEqual( - a: Either, - b: Either, -): [] { - const isEqual = Utils_isKeyOrAddressEqual(a, b); - if (isEqual) { - // Do something - } else { - // Do something else - } -} -``` diff --git a/docs/modules/ROOT/pages/zkCircuits101.adoc b/docs/modules/ROOT/pages/zkCircuits101.adoc deleted file mode 100644 index bc3bbebc..00000000 --- a/docs/modules/ROOT/pages/zkCircuits101.adoc +++ /dev/null @@ -1,17 +0,0 @@ -= ZK Circuits 101 - -Compact code is compiled into arithmetic circuits, which are mathematical representations of the contract's logic. -These circuits are made up of arithmetic gates. The gates enforce the rules and constraints defined in the Compact program. -At a high level, ZK circuits can be thought of as a magic puzzle board that proves a series of steps was followed correctly - like a recipe. -The puzzle board is laid out in a grid like a giant sheet of graph paper with a certain number of rows. -Each row is a space where a step or rule that needs to be followed is written. -These steps/rules correspond to the gates that make up the arithmetic circuit. - -The size of the board is called the **domain size** which is referred to as `k` in the documentation. It’s always a power of 2 (like 256, 512, 1024, etc.), because https://zkhack.dev/whiteboard/module-three/[the math behind the scenes needs it that way]. -Now, just because the board has 1024 rows doesn’t mean it uses all of them. Maybe the recipe takes only 563 steps, so only 563 rows are filled and the rest are left blank. These filled-in rows are called **used rows**. -So "k = 10, rows = 563" in the API Reference documentation that means "this circuit has a size of 2^10 = 1024 rows and only uses 563 rows". - -Why is this important? Well, when writing ZK circuits the size and number of rules to follow as should be as small as possible. -The number of rules in a zero-knowledge circuit directly impacts both the prover time (how long it takes to generate a proof) and, to a lesser extent, the proof size and verifier time. -Larger circuits with more rules require more computation to generate a proof, which can make proof generation slower and more resource-intensive. -This is especially relevant for privacy-preserving blockchains like Midnight, where proof generation is often the most computationally expensive part of a transaction. diff --git a/docs/package.json b/docs/package.json deleted file mode 100644 index 02410b35..00000000 --- a/docs/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "docs", - "version": "0.0.1", - "scripts": { - "docs": "oz-docs -c .", - "docs:watch": "npm run docs watch", - "prepare-docs": "" - }, - "keywords": [], - "author": "", - "license": "ISC", - "devDependencies": { - "@openzeppelin/docs-utils": "^0.1.5" - } -} From 7df037892d6cbd5ee75574500345f9f36d3bf9f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Tue, 24 Feb 2026 18:33:20 -0500 Subject: [PATCH 204/322] Remove unused file --- .../access/ShieldedAccessControlUtils.compact | 25 ------------------- 1 file changed, 25 deletions(-) delete mode 100644 contracts/src/access/ShieldedAccessControlUtils.compact diff --git a/contracts/src/access/ShieldedAccessControlUtils.compact b/contracts/src/access/ShieldedAccessControlUtils.compact deleted file mode 100644 index 5c08f0f6..00000000 --- a/contracts/src/access/ShieldedAccessControlUtils.compact +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma language_version >= 0.16.0; - -/** - * @module ShieldedAccessControlUtils. - * @description A library for common utilities used in the Shielded Access Control module. - */ -module ShieldedAccessControlUtils { - import CompactStandardLibrary; - - /** - * @description Returns whether `keyOrAddress` is a ContractAddress type. - * - * Disclosures: - * - * - The type data of `keyOrAddress` - a ZswapCoinPublicKey or ContractAddress. - * - * @param {Either} keyOrAddress - The target value to check, either a ZswapCoinPublicKey or a ContractAddress. - * @return {Boolean} - Returns true if `keyOrAddress` is a ContractAddress. - */ - export pure circuit isContractAddress(keyOrAddress: Either): Boolean { - return disclose(!keyOrAddress.is_left); - } -} From a6f0eaa93f3f9c52ff6143769dcdcdb006d60c25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Tue, 24 Feb 2026 18:34:13 -0500 Subject: [PATCH 205/322] WIP --- .../src/access/ShieldedAccessControl.compact | 206 +++++++------ .../access/test/ShieldedAccessControl.test.ts | 54 +--- .../mocks/MockShieldedAccessControl.compact | 12 +- .../ShieldedAccessControlSimulator.ts | 272 +++--------------- .../ShieldedAccessControlWitnesses.ts | 99 ++----- contracts/test-utils/address.ts | 6 +- 6 files changed, 175 insertions(+), 474 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index 2986cda6..ec1239a3 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma language_version >= 0.16.0; +pragma language_version >= 0.21.0; /** * @module Shielded AccessControl @@ -8,16 +8,18 @@ pragma language_version >= 0.16.0; * This module provides a shielded role-based access control mechanism, where roles can be used to * represent a set of permissions. Roles are stored as Merkle tree commitments to avoid * disclosing information about role holder. Role commitments are created with the following - * hashing scheme SHA256(roleId | account | nonce | merkleTreeIndex). + * hashing scheme SHA256(accountId | roleId | instanceSalt | "ShieldedAccessControl:commitment"). + * Where `accountId` is a unique SHA256 hash of a user's ZswapCoinPublicKey and a 32 byte secret nonce value + * held in a user's local private state. `roleId` is a unique `Bytes<32>` identifier * - * @notice Using the SHA256 hashing function comes at a significant performace cost. In the future, we + * @notice Using the SHA256 hashing function comes at a significant performance cost. In the future, we * plan on migrating to a ZK-friendly hashing function when an implementation is available. * * Roles are referred to by their `Bytes<32>` identifier. These should be exposed * in the top-level contract and be unique. One way to achieve this is by * using `export sealed ledger` hash digests that are initialized in the top-level contract: * - * ```typescript + * ```compact * import CompactStandardLibrary; * import "./node_modules/@openzeppelin-compact/accessControl/src/ShieldedAccessControl" prefix ShieldedAccessControl_; * @@ -30,7 +32,7 @@ pragma language_version >= 0.16.0; * * To restrict access to a circuit, use {assertOnlyRole}: * - * ```typescript + * ```compact * circuit foo(): [] { * assertOnlyRole(MY_ROLE); * ... @@ -75,11 +77,11 @@ pragma language_version >= 0.16.0; */ module ShieldedAccessControl { import CompactStandardLibrary; - import "ShieldedAccessControlUtils" prefix Utils_; + import "../utils/Utils" prefix Utils_; /** - * @description A Merkle tree of role commitments stored as SHA256(roleId | account | nonce | merkleTreeIndex) - * @type {Bytes<32>} roleCommitment - A role commitment created by the following hash: SHA256(roleId | account | nonce | merkleTreeIndex). + * @description A Merkle tree of role commitments stored as SHA256(accountId | roleId | instanceSalt | "ShieldedAccessControl:commitment") + * @type {Bytes<32>} roleCommitment - A role commitment created by the following hash: SHA256(accountId | roleId | instanceSalt | "ShieldedAccessControl:commitment"). * @type {MerkleTree<10, roleCommitment>} * @type {MerkleTree<10, Bytes<32>>} _operatorRoles  */ @@ -96,19 +98,17 @@ module ShieldedAccessControl { /** * @description A set of nullifiers used to revoke the permissions of a role - * @type {Bytes<32> roleCommitment - A role commitment created by the following hash: SHA256(roleId | account | nonce | merkleTreeIndex). + * @type {Bytes<32> roleCommitment - A role commitment created by the following hash: SHA256(accountId | roleId | instanceSalt | "ShieldedAccessControl:commitment"). * @type {Set} _roleCommitmentNullifiers  */ export ledger _roleCommitmentNullifiers: Set>; - export ledger _currentMerkleTreeIndex: Counter; - export ledger DEFAULT_ADMIN_ROLE: Bytes<32>; /** * @description Returns a Merkle path in the `_operatorRoles` Merkle tree, given the knowledge that a `roleCommitment` is at the given index. * - * @param {Bytes<32>} roleCommitment - A commitment created by the following hash: SHA256(roleId | account | nonce | merkleTreeIndex). + * @param {Bytes<32>} roleCommitment - A commitment created by the following hash: SHA256(accountId | roleId | instanceSalt | "ShieldedAccessControl:commitment"). * @param {Uint<64>} index - An index in the `_operatorRoles` Merkle tree * @return {MerkleTreePath<10, Bytes<32>>} - The Merkle path of `roleCommitment` in the `_operatorRoles` Merkle tree  */ @@ -116,7 +116,6 @@ module ShieldedAccessControl { witness wit_secretNonce(roleId: Bytes<32>): Bytes<32>; - witness wit_getRoleIndex(roleId: Bytes<32> , accountId: Bytes<32>): Uint<64>; export struct Role { isApproved: Boolean; @@ -125,24 +124,23 @@ module ShieldedAccessControl { } /** - * @description Computes the owner commitment from the given `id` and `counter`. + * @description Computes the role commitment from the given `accountId` and `roleId`. * - * ## Owner ID (`id`) - * The `id` is expected to be computed off-chain as: - * `id = SHA256(pk, nonce)` + * ## Account ID (`accountId`) + * The `accountId` is expected to be computed off-chain as: + * `accountId = SHA256(zcpk, nonce)` * - * - `pk`: The owner's public key. - * - `nonce`: A secret nonce scoped to the instance, ideally rotated with each transfer. + * - `zcpk`: The account's ZswapCoinPublicKey. + * - `nonce`: A secret nonce scoped to the role. * * ## Commitment Derivation - * `commitment = SHA256(id, instanceSalt, counter, domain)` + * `commitment = SHA256(accountId, roleId, instanceSalt, domain)` * - * - `id`: See above. + * - `accountId`: See above. + * - `roleId`: A unique role identifier. * - `instanceSalt`: A unique per-deployment salt, stored during initialization. * This prevents commitment collisions across deployments. - * - `counter`: Incremented with each ownership transfer, ensuring uniqueness - * even with repeated `id` values. Cast to `Field` then `Bytes<32>` for hashing. - * - `domain`: Domain separator `"ZOwnablePK:shield:"` (padded to 32 bytes) to prevent + * - `domain`: Domain separator `"ShieldedAccessControl:commitment"` (padded to 32 bytes) to prevent * hash collisions when extending the module or using similar commitment schemes. * * @circuitInfo k=14, rows=14853 @@ -151,28 +149,25 @@ module ShieldedAccessControl { * * - Contract is initialized. * - * @param {Bytes<32>} id - The unique identifier of the owner calculated by `SHA256(pk, nonce)`. - * @param {Uint<64>} counter - The current counter or round. This increments by `1` - * after every transfer to prevent duplicate commitments given the same `id`. - * @returns {Bytes<32>} The commitment derived from `id` and `counter`. + * @param {Bytes<32>} roleId - The unique identifier of a role. + * @param {Bytes<32>} accountId - The unique identifier of the account calculated by `SHA256(zcpk, nonce)`. + * @returns {Bytes<32>} The commitment derived from `accountId` and `roleId`. */ export pure circuit _computeRoleCommitment( - accountId: Bytes<32>, - roleId: Bytes<32>, - index: Uint<64>, - ): Bytes<32> { - return persistentHash>>( - [ - accountId, - roleId, - index as Field as Bytes<32>, - pad(32, "ShieldedAccessControl:commitment") - ] - ); + roleId: Bytes<32>, + accountId: Bytes<32>, + ): Bytes<32> { + return persistentHash>>( + [roleId, + accountId, + pad(32, "ShieldedAccessControl:commitment")] + ); } export pure circuit _computeNullifier(roleCommitment: Bytes<32>): Bytes<32> { - return persistentHash>>([roleCommitment, pad(32, "ShieldedAccessControl:nullifier")]); + return persistentHash>>( + [roleCommitment, pad(32, "ShieldedAccessControl:nullifier")] + ); } /** @@ -205,12 +200,12 @@ module ShieldedAccessControl { * * @param {Either} pk - The public key of the identity being committed. * @param {Bytes<32>} nonce - A private nonce to scope the commitment. - * @returns {Bytes<32>} The computed owner ID. + * @returns {Bytes<32>} The computed account ID. */ - export pure circuit _computeRoleId( - pk: Either, - nonce: Bytes<32> - ): Bytes<32> { + export pure circuit _computeAccountId( + pk: Either, + nonce: Bytes<32> + ): Bytes<32> { assert(pk.is_left, "ShieldedAccessControl: contract address owners are not yet supported"); return persistentHash>>([pk.left.bytes, nonce]); @@ -244,13 +239,12 @@ module ShieldedAccessControl { * @return {Boolean} - A boolean determining if the account has the specified role.  */ export circuit callerHasRole(roleId: Bytes<32>): Role { - const callerAsEither = Either { - is_left: true, - left: ownPublicKey(), - right: ContractAddress { bytes: pad(32, "") } - }; + const callerAsEither = + Either { is_left: true, + left: ownPublicKey(), + right: ContractAddress { bytes: pad(32, "") } }; const nonce = wit_secretNonce(roleId); - const accountId = _computeRoleId(callerAsEither, nonce); + const accountId = _computeAccountId(callerAsEither, nonce); return getRole(roleId, accountId); } @@ -292,25 +286,20 @@ module ShieldedAccessControl { } export circuit getRole(roleId: Bytes<32>, accountId: Bytes<32>): Role { - const index = wit_getRoleIndex(roleId, accountId); - const commitment = _computeRoleCommitment(accountId, roleId, index); + const commitment = _computeRoleCommitment(roleId, accountId); const commitmentNullifier = _computeNullifier(commitment); const authPath = wit_getRoleCommitmentPath(commitment); - const rootMatches = _operatorRoles - .checkRoot(merkleTreePathRootNoLeafHash<10>(disclose(authPath))); + const rootMatches = + _operatorRoles.checkRoot(merkleTreePathRootNoLeafHash<10>(disclose(authPath))); - if(!_roleCommitmentNullifiers.member(disclose(commitmentNullifier)) && rootMatches) { - return Role { - isApproved: true, - roleCommitment: disclose(commitment), - commitmentNullifier: disclose(commitmentNullifier) - }; + if (!_roleCommitmentNullifiers.member(disclose(commitmentNullifier)) && rootMatches) { + return Role { isApproved: true, + roleCommitment: disclose(commitment), + commitmentNullifier: disclose(commitmentNullifier) }; } else { - return Role { - isApproved: false, - roleCommitment: disclose(commitment), - commitmentNullifier: disclose(commitmentNullifier) - }; + return Role { isApproved: false, + roleCommitment: disclose(commitment), + commitmentNullifier: disclose(commitmentNullifier) }; } } @@ -398,47 +387,48 @@ module ShieldedAccessControl { _revokeRole(roleId, accountId); } - /** - * @description Revokes `roleId` from the calling account. - * - * @notice Roles are often managed via {grantRole} and {revokeRole}: this circuit's - * purpose is to provide a mechanism for accounts to lose their privileges - * if they are compromised (such as when a trusted device is misplaced). - * - * @circuitInfo k=17, rows=108992 - * - * Requirements: - * - * - The caller must be `callerConfirmation`. - * - The caller must not be a `ContractAddress`. - * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce) - * must exist in the `_roleCommitmentIndex` map. - * - A nullifier for the role commitment produced by SHA256(roleId | account | nonce) - * must not exist in the `_roleCommitmentNullifiers` set. - * - A path for the role commitment produced by SHA256(roleId | account | nonce) must - * exist at `index` in the `_operatorRoles` Merkle tree. - * - * Disclosures: - * - * - The intermediate role commitment produced by SHA256(roleId | account | nonce). - * - The role commitment produced by SHA256(roleId | account | nonce). - * - The Merkle tree path for the role commitment stored at `index` in the `_operatorRoles` - * Merkle tree. - * - The type data of `callerConfirmation` - a ZswapCoinPublicKey or ContractAddress. - * - * @param {Bytes<32>} roleId - The role identifier. - * @param {Either} callerConfirmation - A ZswapCoinPublicKey or ContractAddress. - * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK) - * @return {[]} - Empty tuple. - */ + /** + * @description Revokes `roleId` from the calling account. + * + * @notice Roles are often managed via {grantRole} and {revokeRole}: this circuit's + * purpose is to provide a mechanism for accounts to lose their privileges + * if they are compromised (such as when a trusted device is misplaced). + * + * @circuitInfo k=17, rows=108992 + * + * Requirements: + * + * - The caller must be `callerConfirmation`. + * - The caller must not be a `ContractAddress`. + * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce) + * must exist in the `_roleCommitmentIndex` map. + * - A nullifier for the role commitment produced by SHA256(roleId | account | nonce) + * must not exist in the `_roleCommitmentNullifiers` set. + * - A path for the role commitment produced by SHA256(roleId | account | nonce) must + * exist at `index` in the `_operatorRoles` Merkle tree. + * + * Disclosures: + * + * - The intermediate role commitment produced by SHA256(roleId | account | nonce). + * - The role commitment produced by SHA256(roleId | account | nonce). + * - The Merkle tree path for the role commitment stored at `index` in the `_operatorRoles` + * Merkle tree. + * - The type data of `callerConfirmation` - a ZswapCoinPublicKey or ContractAddress. + * + * @param {Bytes<32>} roleId - The role identifier. + * @param {Either} callerConfirmation - A ZswapCoinPublicKey or ContractAddress. + * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK) + * @return {[]} - Empty tuple. + */ export circuit renounceRole(roleId: Bytes<32>, callerConfirmation: Bytes<32>): [] { const nonce = wit_secretNonce(roleId); - const callerAsEither = Either { - is_left: true, - left: ownPublicKey(), - right: ContractAddress { bytes: pad(32, "") } - }; - assert(callerConfirmation == _computeRoleId(callerAsEither, nonce), "ShieldedAccessControl: bad confirmation"); + const callerAsEither = + Either { is_left: true, + left: ownPublicKey(), + right: ContractAddress { bytes: pad(32, "") } }; + assert(callerConfirmation == _computeAccountId(callerAsEither, nonce), + "ShieldedAccessControl: bad confirmation" + ); _revokeRole(roleId, callerConfirmation); } @@ -491,9 +481,7 @@ module ShieldedAccessControl { return false; } - // Use ledger index as source of truth - _operatorRoles.insertHashIndex(role.roleCommitment, _currentMerkleTreeIndex); - _currentMerkleTreeIndex.increment(1); + _operatorRoles.insertHash(role.roleCommitment); return true; } diff --git a/contracts/src/access/test/ShieldedAccessControl.test.ts b/contracts/src/access/test/ShieldedAccessControl.test.ts index c6dc0891..16961190 100644 --- a/contracts/src/access/test/ShieldedAccessControl.test.ts +++ b/contracts/src/access/test/ShieldedAccessControl.test.ts @@ -1,8 +1,8 @@ import { CompactTypeBytes, CompactTypeVector, - convert_bigint_to_Uint8Array, persistentHash, + convertFieldToBytes, type WitnessContext, } from '@midnight-ntwrk/compact-runtime'; import { beforeEach, describe, expect, it, vi } from 'vitest'; @@ -17,7 +17,7 @@ import { } from '../../../artifacts/MockShieldedAccessControl/contract/index.cjs'; import { fmtHexString, ShieldedAccessControlPrivateState, ShieldedAccessControlWitnesses } from '../witnesses/ShieldedAccessControlWitnesses.js'; import { ShieldedAccessControlSimulator } from './simulators/ShieldedAccessControlSimulator.js'; -import * as utils from './utils/address.js'; +import * as utils from '#test-utils/address.js'; // Helpers const buildCommitment = ( @@ -26,7 +26,7 @@ const buildCommitment = ( index: bigint, ): Uint8Array => { const rt_type = new CompactTypeVector(4, new CompactTypeBytes(32)); - const bIndex = convert_bigint_to_Uint8Array(32, index); + const bIndex = convertFieldToBytes(32, index, ''); const commitment = persistentHash(rt_type, [ accountId, @@ -70,11 +70,11 @@ const [UNAUTHORIZED, Z_UNAUTHORIZED] = utils.generatePubKeyPair('UNAUTHORIZED'); // Roles const DEFAULT_ADMIN_ROLE = utils.zeroUint8Array(); -const OPERATOR_1_ROLE = convert_bigint_to_Uint8Array(32, 1n); -const OPERATOR_2_ROLE = convert_bigint_to_Uint8Array(32, 2n); -const OPERATOR_3_ROLE = convert_bigint_to_Uint8Array(32, 3n); -const UNINITIALIZED_ROLE = convert_bigint_to_Uint8Array(32, 555n); -const BAD_ROLE = convert_bigint_to_Uint8Array(32, 99999999n); +const OPERATOR_1_ROLE = convertFieldToBytes(32, 1n, ''); +const OPERATOR_2_ROLE = convertFieldToBytes(32, 2n, ''); +const OPERATOR_3_ROLE = convertFieldToBytes(32, 3n, ''); +const UNINITIALIZED_ROLE = convertFieldToBytes(32, 555n, ''); +const BAD_ROLE = convertFieldToBytes(32, 99999999n, ''); // Nonces const ADMIN_SECRET_NONCE = Buffer.alloc(32, 'ADMIN_SECRET_NONCE'); @@ -209,44 +209,6 @@ describe('ShieldedAccessControl', () => { ) }); - // Complete testing once issue with pathForLeaf is resolved - describe.todo('wit_getRoleIndex', () => { - it.todo('should return 0 if no roles granted', () => { - const [_, index] = shieldedAccessControl.witnesses.wit_getRoleIndex(shieldedAccessControl.getWitnessContext(), UNINITIALIZED_ROLE, ADMIN_ID); - expect(index).toBe(0n); - }); - - it.todo('should return correct index', () => { - let granted = shieldedAccessControl._grantRole(DEFAULT_ADMIN_ROLE, ADMIN_ID); - expect(granted).toBe(true); - let [, adminIndex] = shieldedAccessControl.witnesses.wit_getRoleIndex(shieldedAccessControl.getWitnessContext(), DEFAULT_ADMIN_ROLE, ADMIN_ID); - expect(adminIndex).toBe(0n); - - shieldedAccessControl.privateState.injectSecretNonce(OPERATOR_1_ROLE, OPERATOR_1_SECRET_NONCE); - granted = shieldedAccessControl._grantRole(OPERATOR_1_ROLE, OPERATOR_1_ID); - expect(granted).toBe(true); - const [, operatorIndex] = shieldedAccessControl.witnesses.wit_getRoleIndex(shieldedAccessControl.getWitnessContext(), OPERATOR_1_ROLE, OPERATOR_1_ID); - expect(operatorIndex).toBe(1n); - - shieldedAccessControl.privateState.injectSecretNonce(OPERATOR_2_ROLE, OPERATOR_2_SECRET_NONCE); - granted = shieldedAccessControl._grantRole(OPERATOR_2_ROLE, OPERATOR_2_ID); - expect(granted).toBe(true); - shieldedAccessControl._grantRole(OPERATOR_2_ROLE, OPERATOR_2_ID); - const [, operatorIndex2] = shieldedAccessControl.witnesses.wit_getRoleIndex(shieldedAccessControl.getWitnessContext(), OPERATOR_2_ROLE, OPERATOR_2_ID); - expect(operatorIndex2).toBe(2n); - - shieldedAccessControl.privateState.injectSecretNonce(OPERATOR_3_ROLE, OPERATOR_3_SECRET_NONCE); - shieldedAccessControl._grantRole(OPERATOR_3_ROLE, OPERATOR_3_ID); - const [_, operatorIndex3] = shieldedAccessControl.witnesses.wit_getRoleIndex(shieldedAccessControl.getWitnessContext(), OPERATOR_3_ROLE, OPERATOR_3_ID); - expect(operatorIndex3).toBe(3n); - - let [, adminIndex2] = shieldedAccessControl.witnesses.wit_getRoleIndex(shieldedAccessControl.getWitnessContext(), DEFAULT_ADMIN_ROLE, ADMIN_ID); - expect(adminIndex2).toBe(0n); - }); - - it.todo('should return current Merkle tree index if role does not exist') - }); - describe('wit_getRoleCommitmentPath', () => { it('should return a Merkle tree path if one exists', () => { diff --git a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact index 05c17395..9d0f79cb 100644 --- a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact +++ b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma language_version >= 0.16.0; +pragma language_version >= 0.21.0; import CompactStandardLibrary; @@ -14,24 +14,22 @@ export { MerkleTreePath, ShieldedAccessControl_DEFAULT_ADMIN_ROLE, ShieldedAccessControl__operatorRoles, - ShieldedAccessControl__currentMerkleTreeIndex, ShieldedAccessControl__roleCommitmentNullifiers, ShieldedAccessControl_Role }; export pure circuit _computeRoleCommitment( - accountId: Bytes<32>, roleId: Bytes<32>, - index: Uint<64>, + accountId: Bytes<32> ): Bytes<32> { - return ShieldedAccessControl__computeRoleCommitment(accountId, roleId, index); + return ShieldedAccessControl__computeRoleCommitment(roleId, accountId); } -export pure circuit _computeRoleId( +export pure circuit _computeAccountId( pk: Either, nonce: Bytes<32> ): Bytes<32> { - return ShieldedAccessControl__computeRoleId(pk, nonce); + return ShieldedAccessControl__computeAccountId(pk, nonce); } export pure circuit _computeNullifier(commitment: Bytes<32>): Bytes<32> { diff --git a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts index 2118e91e..f0358f0e 100644 --- a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts +++ b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts @@ -1,247 +1,77 @@ import { - type CircuitContext, - type CoinPublicKey, - emptyZswapLocalState, - witnessContext, - type WitnessContext, -} from '@midnight-ntwrk/compact-runtime'; -import { sampleContractAddress } from '@midnight-ntwrk/zswap'; + type BaseSimulatorOptions, + createSimulator, +} from '@openzeppelin-compact/contracts-simulator'; import { type ContractAddress, type Either, - type Ledger, + type ShieldedAccessControl_Role as Role, ledger, Contract as MockShieldedAccessControl, - type ShieldedAccessControl_Role as Role, type ZswapCoinPublicKey, -} from '../../../../artifacts/MockShieldedAccessControl/contract/index.cjs'; +} from '../../../../artifacts/MockShieldedAccessControl/contract/index.js'; import { ShieldedAccessControlPrivateState, ShieldedAccessControlWitnesses, } from '../../witnesses/ShieldedAccessControlWitnesses.js'; -import type { - ContextlessCircuits, - ExtractImpureCircuits, - ExtractPureCircuits, - SimulatorOptions, -} from '../types/test.js'; -import { AbstractContractSimulator } from '../utils/AbstractContractSimulator.js'; -import { SimulatorStateManager } from '../utils/SimulatorStateManager.js'; -type ShieldedAccessControlSimOptions = SimulatorOptions< - ShieldedAccessControlPrivateState, - typeof ShieldedAccessControlWitnesses ->; +/** + * Type constructor args + */ +type ShieldedAccessControlArgs = readonly []; /** - * @description A simulator implementation of a contract for testing purposes. - * @template P - The private state type, fixed to ShieldedAccessControlPrivateState. - * @template L - The ledger type, fixed to Contract.Ledger. + * Base simulator + * @dev We deliberately use `any` as the base simulator type. + * This workaround is necessary due to type inference and declaration filegen + * in a monorepo environment. Attempting to fully preserve type information + * turns into type gymnastics. + * + * `any` can be safely removed once the contract simulator is consumed + * as a properly packaged dependency (outside the monorepo). */ -export class ShieldedAccessControlSimulator extends AbstractContractSimulator< +const ShieldedAccessControlSimulatorBase: any = createSimulator< ShieldedAccessControlPrivateState, - Ledger -> { - contract: MockShieldedAccessControl; - readonly contractAddress: string; - private stateManager: SimulatorStateManager; - private callerOverride: CoinPublicKey | null = null; - private _witnesses: ReturnType; - - private _pureCircuitProxy?: ContextlessCircuits< - ExtractPureCircuits< - MockShieldedAccessControl - >, - ShieldedAccessControlPrivateState - >; - - private _impureCircuitProxy?: ContextlessCircuits< - ExtractImpureCircuits< - MockShieldedAccessControl - >, - ShieldedAccessControlPrivateState - >; + ReturnType, + ReturnType, + MockShieldedAccessControl, + ShieldedAccessControlArgs +>({ + contractFactory: (witnesses) => + new MockShieldedAccessControl(witnesses), + defaultPrivateState: () => ShieldedAccessControlPrivateState.generate(), + contractArgs: () => { + return []; + }, + ledgerExtractor: (state) => ledger(state), + witnessesFactory: () => ShieldedAccessControlWitnesses(), +}); +/** + * ShieldedAccessControlSimulator + */ +export class ShieldedAccessControlSimulator extends ShieldedAccessControlSimulatorBase { constructor( - options: ShieldedAccessControlSimOptions = {}, + options: BaseSimulatorOptions< + ShieldedAccessControlPrivateState, + ReturnType + > = {}, ) { - super(); - - // Setup initial state - const { - privateState = options.privateState - ? options.privateState - : ShieldedAccessControlPrivateState.generate(), - witnesses = ShieldedAccessControlWitnesses(), - coinPK = options.coinPK ? options.coinPK : '0'.repeat(64), - address = sampleContractAddress(), - } = options; - - this.contract = - new MockShieldedAccessControl( - witnesses, - ); - - this.stateManager = new SimulatorStateManager( - this.contract, - privateState, - coinPK, - address, - ); - this.contractAddress = this.circuitContext.transactionContext.address; - this._witnesses = witnesses; - this.contract = - new MockShieldedAccessControl( - this._witnesses, - ); - } - - get circuitContext() { - return this.stateManager.getContext(); - } - - set circuitContext(ctx) { - this.stateManager.setContext(ctx); - } - - getPublicState(): Ledger { - return ledger(this.circuitContext.transactionContext.state); - } - - getWitnessContext(): WitnessContext< - Ledger, - ShieldedAccessControlPrivateState - > { - return witnessContext(this.getPublicState(), this.getPrivateState(), this.contractAddress); - } - - /** - * @description Constructs a caller-specific circuit context. - * If a caller override is present, it replaces the current Zswap local state with an empty one - * scoped to the overridden caller. Otherwise, the existing context is reused as-is. - * @returns A circuit context adjusted for the current simulated caller. - */ - protected getCallerContext(): CircuitContext { - return { - ...this.circuitContext, - currentZswapLocalState: this.callerOverride - ? emptyZswapLocalState(this.callerOverride) - : this.circuitContext.currentZswapLocalState, - }; - } - - /** - * @description Initializes and returns a proxy to pure contract circuits. - * The proxy automatically injects the current circuit context into each call, - * and returns only the result portion of each circuit's output. - * @notice The proxy is created only when first accessed a.k.a lazy initialization. - * This approach is efficient in cases where only pure or only impure circuits are used, - * avoiding unnecessary proxy creation. - * @returns A proxy object exposing pure circuit functions without requiring explicit context. - */ - protected get pureCircuit(): ContextlessCircuits< - ExtractPureCircuits< - MockShieldedAccessControl - >, - ShieldedAccessControlPrivateState - > { - if (!this._pureCircuitProxy) { - this._pureCircuitProxy = this.createPureCircuitProxy< - MockShieldedAccessControl['circuits'] - >(this.contract.circuits, () => this.circuitContext); - } - return this._pureCircuitProxy; - } - - /** - * @description Initializes and returns a proxy to impure contract circuits. - * The proxy automatically injects the current (possibly caller-modified) context into each call, - * and updates the circuit context with the one returned by the circuit after execution. - * @notice The proxy is created only when first accessed a.k.a. lazy initialization. - * This approach is efficient in cases where only pure or only impure circuits are used, - * avoiding unnecessary proxy creation. - * @returns A proxy object exposing impure circuit functions without requiring explicit context management. - */ - protected get impureCircuit(): ContextlessCircuits< - ExtractImpureCircuits< - MockShieldedAccessControl - >, - ShieldedAccessControlPrivateState - > { - if (!this._impureCircuitProxy) { - this._impureCircuitProxy = this.createImpureCircuitProxy< - MockShieldedAccessControl['impureCircuits'] - >( - this.contract.impureCircuits, - () => this.getCallerContext(), - (ctx: any) => { - this.circuitContext = ctx; - }, - ); - } - return this._impureCircuitProxy; - } - - /** - * @description Resets the cached circuit proxy instances. - * This is useful if the underlying contract state or circuit context has changed, - * and you want to ensure the proxies are recreated with updated context on next access. - */ - public resetCircuitProxies(): void { - this._pureCircuitProxy = undefined; - this._impureCircuitProxy = undefined; - } - - /** - * @description Helper method that provides access to both pure and impure circuit proxies. - * These proxies automatically inject the appropriate circuit context when invoked. - * @returns An object containing `pure` and `impure` circuit proxy interfaces. - */ - public get circuits() { - return { - pure: this.pureCircuit, - impure: this.impureCircuit, - }; - } - - public get witnesses(): ReturnType { - return this._witnesses; - } - - public set witnesses(newWitnesses: ReturnType< - typeof ShieldedAccessControlWitnesses - >) { - this._witnesses = newWitnesses; - this.contract = - new MockShieldedAccessControl( - this._witnesses, - ); - this.resetCircuitProxies(); - } - - public overrideWitness( - key: K, - fn: (typeof this._witnesses)[K], - ) { - this.witnesses = { - ...this._witnesses, - [key]: fn, - }; + super([], options); } public _computeRoleCommitment( - accountId: Uint8Array, roleId: Uint8Array, - index: bigint, + accountId: Uint8Array, ): Uint8Array { - return this.circuits.pure._computeRoleCommitment(accountId, roleId, index); + return this.circuits.pure._computeRoleCommitment(roleId, accountId); } - public _computeRoleId( + public _computeAccountId( pk: Either, nonce: Uint8Array ): Uint8Array { - return this.circuits.pure._computeRoleId(pk, nonce); + return this.circuits.pure._computeAccountId(pk, nonce); } public _computeNullifier(commitment: Uint8Array): Uint8Array { @@ -363,7 +193,7 @@ export class ShieldedAccessControlSimulator extends AbstractContractSimulator< /** * @description Contextually sets a new nonce into the private state. * @param newNonce The secret nonce. - * @returns The ZOwnablePK private state after setting the new nonce. + * @returns The ShieldedAccessControlPK private state after setting the new nonce. */ injectSecretNonce: ( roleId: Uint8Array, @@ -390,14 +220,4 @@ export class ShieldedAccessControlSimulator extends AbstractContractSimulator< ]; }, }; - - public callerCtx = { - /** - * @description Sets the caller context. - * @param caller The caller in context of the proceeding circuit calls. - */ - setCaller: (caller: CoinPublicKey) => { - this.callerOverride = caller; - } - }; } diff --git a/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts b/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts index cd905647..d42aae43 100644 --- a/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts +++ b/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts @@ -2,7 +2,7 @@ import { getRandomValues } from 'node:crypto'; import { CompactTypeBytes, CompactTypeVector, - convert_bigint_to_Uint8Array, + convertFieldToBytes, persistentHash, type WitnessContext, } from '@midnight-ntwrk/compact-runtime'; @@ -13,11 +13,20 @@ import type { MerkleTreePath, ZswapCoinPublicKey, } from '../../../artifacts/MockShieldedAccessControl/contract/index.cjs'; -import { eitherToBytes } from '../test/utils/address'; const COMMITMENT_DOMAIN = new Uint8Array(32); new TextEncoder().encodeInto('ShieldedAccessControl:commitment', COMMITMENT_DOMAIN); +const eitherToBytes = ( + account: Either, +) => { + if (account.is_left) { + return account.left.bytes; + } + + return account.right.bytes; +}; + export function fmtHexString(bytes: string | Uint8Array): string { if (bytes instanceof String) { return `${bytes.slice(0, 4)}...${bytes.slice(-4)}`; @@ -27,12 +36,6 @@ export function fmtHexString(bytes: string | Uint8Array): string { } } -export function createAccountId(account: Either, secretNonce: Uint8Array): Uint8Array { - const rt_type = new CompactTypeVector(2, new CompactTypeBytes(32)); - const bAccount = eitherToBytes(account); - return persistentHash(rt_type, [secretNonce, bAccount]); -} - /** * @description Interface defining the witness methods for ShieldedAccessControl operations. * @template P - The private state type. @@ -51,18 +54,14 @@ export interface IShieldedAccessControlWitnesses

{ context: WitnessContext, roleCommitment: Uint8Array, ): [P, MerkleTreePath]; - wit_getRoleIndex( - context: WitnessContext, - roleId: Uint8Array, - accountId: Uint8Array - ): [P, bigint]; } type RoleId = string; type SecretNonce = Uint8Array; /** - * @description Represents the private state of an ownable contract, storing a secret nonce. + * @description Represents the private state of a Shielded AccessControl contract, storing + * mappings from a 32 byte hex string to a 32 byte secret nonce. */ export type ShieldedAccessControlPrivateState = { /** @description A 32-byte secret nonce used as a privacy additive. */ @@ -82,11 +81,7 @@ export const ShieldedAccessControlPrivateState = { const defaultRoleId: string = Buffer.alloc(32).toString('hex'); const secretNonce = new Uint8Array(getRandomValues(Buffer.alloc(32))); - const privateState: ShieldedAccessControlPrivateState = { - roles: {}, - }; - privateState.roles[defaultRoleId] = secretNonce; - return privateState; + return { roles: { [defaultRoleId]: secretNonce } }; }, /** @@ -108,11 +103,7 @@ export const ShieldedAccessControlPrivateState = { nonce: Buffer, ): ShieldedAccessControlPrivateState => { const roleString = roleId.toString('hex'); - const privateState: ShieldedAccessControlPrivateState = { - roles: {}, - }; - privateState.roles[roleString] = nonce; - return privateState; + return { roles: { [roleString]: nonce } }; }, setRole: ( @@ -142,55 +133,7 @@ export const ShieldedAccessControlPrivateState = { }; return path ? path : defaultPath; }, - - // If index cannot be found in MT return _currentMTIndex - getRoleIndex: ( - { - ledger, - privateState, - }: WitnessContext, - roleId: Uint8Array, - accountId: Uint8Array - ): bigint => { - const rt_type = new CompactTypeVector(4, new CompactTypeBytes(32)); - // Iterate over each MT index to determine if commitment exists - console.log("current MT index ", ledger.ShieldedAccessControl__currentMerkleTreeIndex); - for (let i = 0; i <= ledger.ShieldedAccessControl__currentMerkleTreeIndex; i++) { - const index = BigInt(i); - const bIndex = convert_bigint_to_Uint8Array(32, index); - const commitment = persistentHash(rt_type, [ - accountId, - roleId, - bIndex, - COMMITMENT_DOMAIN, - ]); - try { - const pathForLeaf = ledger.ShieldedAccessControl__operatorRoles.pathForLeaf( - index, - commitment, - ); - if (Buffer.from(pathForLeaf.leaf).compare(Buffer.from(commitment)) === 0) { - return index; - } - } catch (e: unknown) { - if (e instanceof Error) { - const [msg, index] = e.message.split(':'); - if (msg === 'invalid index into sparse merkle tree') { - // console.log(`accountId ${fmtHexString(accountId)} with commitment ${fmtHexString(commitment)} not found at index ${index}`); - } else { - throw e; - } - } - } - } - - console.log("WIT - Commitment DNE, returning MT index ", ledger.ShieldedAccessControl__currentMerkleTreeIndex.toString()); - - // If commitment doesn't exist return currentMTIndex - // Used for adding roles - return ledger.ShieldedAccessControl__currentMerkleTreeIndex; - }, -}; +} /** * @description Factory function creating witness implementations for Shielded AccessControl operations. @@ -217,14 +160,4 @@ export const ShieldedAccessControlWitnesses = ), ]; }, - wit_getRoleIndex( - context: WitnessContext, - roleId: Uint8Array, - accountId: Uint8Array - ): [ShieldedAccessControlPrivateState, bigint] { - return [ - context.privateState, - ShieldedAccessControlPrivateState.getRoleIndex(context, roleId, accountId), - ]; - }, }); diff --git a/contracts/test-utils/address.ts b/contracts/test-utils/address.ts index 38dae723..974ec1df 100644 --- a/contracts/test-utils/address.ts +++ b/contracts/test-utils/address.ts @@ -78,9 +78,9 @@ const baseGeneratePubKeyPair = ( str: string, asEither: boolean, ): [ - string, - ZswapCoinPublicKey | Either, -] => { + string, + ZswapCoinPublicKey | Either, + ] => { const pk = toHexPadded(str); const zpk = asEither ? createEitherTestUser(str) : encodeToPK(str); return [pk, zpk]; From dc5001f380a981d5e9512251b07cb3b22dc64aa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Wed, 25 Feb 2026 18:24:53 -0500 Subject: [PATCH 206/322] Use standard insert method over insertHash --- contracts/src/access/ShieldedAccessControl.compact | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index ec1239a3..738b646b 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -8,7 +8,7 @@ pragma language_version >= 0.21.0; * This module provides a shielded role-based access control mechanism, where roles can be used to * represent a set of permissions. Roles are stored as Merkle tree commitments to avoid * disclosing information about role holder. Role commitments are created with the following - * hashing scheme SHA256(accountId | roleId | instanceSalt | "ShieldedAccessControl:commitment"). + * hashing scheme SHA256(roleId | accountId | instanceSalt | "ShieldedAccessControl:commitment"). * Where `accountId` is a unique SHA256 hash of a user's ZswapCoinPublicKey and a 32 byte secret nonce value * held in a user's local private state. `roleId` is a unique `Bytes<32>` identifier * @@ -80,8 +80,8 @@ module ShieldedAccessControl { import "../utils/Utils" prefix Utils_; /** - * @description A Merkle tree of role commitments stored as SHA256(accountId | roleId | instanceSalt | "ShieldedAccessControl:commitment") - * @type {Bytes<32>} roleCommitment - A role commitment created by the following hash: SHA256(accountId | roleId | instanceSalt | "ShieldedAccessControl:commitment"). + * @description A Merkle tree of role commitments stored as SHA256(roleId | accountId | instanceSalt | "ShieldedAccessControl:commitment") + * @type {Bytes<32>} roleCommitment - A role commitment created by the following hash: SHA256(roleId | accountId | instanceSalt | "ShieldedAccessControl:commitment"). * @type {MerkleTree<10, roleCommitment>} * @type {MerkleTree<10, Bytes<32>>} _operatorRoles  */ @@ -134,7 +134,7 @@ module ShieldedAccessControl { * - `nonce`: A secret nonce scoped to the role. * * ## Commitment Derivation - * `commitment = SHA256(accountId, roleId, instanceSalt, domain)` + * `commitment = SHA256(roleId, accountId, instanceSalt, domain)` * * - `accountId`: See above. * - `roleId`: A unique role identifier. @@ -290,7 +290,7 @@ module ShieldedAccessControl { const commitmentNullifier = _computeNullifier(commitment); const authPath = wit_getRoleCommitmentPath(commitment); const rootMatches = - _operatorRoles.checkRoot(merkleTreePathRootNoLeafHash<10>(disclose(authPath))); + _operatorRoles.checkRoot(merkleTreePathRoot<10, Bytes<32>>(disclose(authPath))); if (!_roleCommitmentNullifiers.member(disclose(commitmentNullifier)) && rootMatches) { return Role { isApproved: true, @@ -481,7 +481,7 @@ module ShieldedAccessControl { return false; } - _operatorRoles.insertHash(role.roleCommitment); + _operatorRoles.insert(role.roleCommitment); return true; } From 5a817c9991cd970fa6231c0691e96623bd469fa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Wed, 25 Feb 2026 20:01:54 -0500 Subject: [PATCH 207/322] Update circuit docs --- .../src/access/ShieldedAccessControl.compact | 61 ++++++------------- 1 file changed, 20 insertions(+), 41 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index 738b646b..081c2687 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -7,10 +7,13 @@ pragma language_version >= 0.21.0; * @description A Shielded AccessControl library. * This module provides a shielded role-based access control mechanism, where roles can be used to * represent a set of permissions. Roles are stored as Merkle tree commitments to avoid - * disclosing information about role holder. Role commitments are created with the following - * hashing scheme SHA256(roleId | accountId | instanceSalt | "ShieldedAccessControl:commitment"). - * Where `accountId` is a unique SHA256 hash of a user's ZswapCoinPublicKey and a 32 byte secret nonce value - * held in a user's local private state. `roleId` is a unique `Bytes<32>` identifier + * disclosing information about role holders. Role commitments are created with the following + * hashing scheme SHA256(roleId | accountId | instanceSalt | commitmentDomain). Where + * - `accountId` is a unique SHA256 hash of a user's ZswapCoinPublicKey and a 32 byte secret nonce value + * held in a user's local private state + * - `roleId` is a unique `Bytes<32>` identifier + * - `instanceSalt` is an immutable, cryptographically strong random value provided on deployment + * - `commitmentDomain` is the following zero padded 32 byte string: "ShieldedAccessControl:commitment" * * @notice Using the SHA256 hashing function comes at a significant performance cost. In the future, we * plan on migrating to a ZK-friendly hashing function when an implementation is available. @@ -46,29 +49,16 @@ pragma language_version >= 0.21.0; * By default, the admin role for all roles is `DEFAULT_ADMIN_ROLE`, which means * that only accounts with this role will be able to grant or revoke other * roles. More complex role relationships can be created by using - * {_setRoleAdmin}. To set a custom `DEFAULT_ADMIN_ROLE`, implement the `Initializable` - * module and set `DEFAULT_ADMIN_ROLE` in the `initialize()` circuit. + * {_setRoleAdmin}. To set a custom `DEFAULT_ADMIN_ROLE` set `DEFAULT_ADMIN_ROLE` + * in the `initialize()` circuit. * * WARNING: The `DEFAULT_ADMIN_ROLE` is also its own admin: it has permission to * grant and revoke this role. Extra precautions should be taken to secure * accounts that have been granted it. * - * @notice Roles can only be granted to ZswapCoinPublicKeys - * through the main role approval circuits (`grantRole` and `_grantRole`). - * In other words, role approvals to contract addresses are disallowed through these - * circuits. - * This is because Compact currently does not support contract-to-contract calls which means - * if a contract is granted a role, the contract cannot directly call the protected - * circuit. - * - * @notice This module does offer an experimental circuit that allows roles to be granted - * to contract addresses (`_unsafeGrantRole`). - * Note that the circuit name is very explicit ("unsafe") with this experimental circuit. - * Until contract-to-contract calls are supported, - * there is no direct way for a contract to call protected circuits. - * - * @notice The unsafe circuits are planned to become deprecated once contract-to-contract calls - * are supported. + * @dev In this system, adversaries will have knowledge of all role and admin + * identifiers. They will know when an admin is added and how many admins exist + * in the system. * * @notice Missing Features and Improvements: * @@ -80,10 +70,10 @@ module ShieldedAccessControl { import "../utils/Utils" prefix Utils_; /** - * @description A Merkle tree of role commitments stored as SHA256(roleId | accountId | instanceSalt | "ShieldedAccessControl:commitment") - * @type {Bytes<32>} roleCommitment - A role commitment created by the following hash: SHA256(roleId | accountId | instanceSalt | "ShieldedAccessControl:commitment"). - * @type {MerkleTree<10, roleCommitment>} - * @type {MerkleTree<10, Bytes<32>>} _operatorRoles + * @ledger _operatorRoles + * @description A Merkle tree of role commitments stored as SHA256(roleId | accountId | instanceSalt | commitmentDomain) + * Role commitments are derived from a public role identifier (e.g., `persistentHash>(pad(32, "MY_ROLE")`), + * an account identifier (e.g., `SHA256(pk, nonce)`), the `instanceSalt`, and a domain separator.  */ export ledger _operatorRoles: MerkleTree<10, Bytes<32>>; @@ -447,32 +437,21 @@ module ShieldedAccessControl { } /** - * @description Attempts to grant `roleId` to `account` and returns a boolean indicating if `roleId` was granted. + * @description Attempts to grant `roleId` to `accountId` and returns a boolean indicating if `roleId` was granted. * Internal circuit without access restriction. * * @circuitInfo k=17, rows=109163 * * Requirements: * - * - `account` must not be a ContractAddress. - * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce) - * must exist in the `_roleCommitmentIndex` map. - * - A nullifier for the role commitment produced by SHA256(roleId | account | nonce) - * must not exist in the `_roleCommitmentNullifiers` set. - * - A path for the role commitment produced by SHA256(roleId | account | nonce) must - * exist at `index` in the `_operatorRoles` Merkle tree. * * Disclosures: * - * - The intermediate role commitment produced by SHA256(roleId | account | nonce). - * - The role commitment produced by SHA256(roleId | account | nonce). - * - The Merkle tree path for the role commitment stored at `index` in the `_operatorRoles` - * Merkle tree. - * - The type data of `account` - a ZswapCoinPublicKey or ContractAddress. + * - The role commitment produced by SHA256(roleId | accountId | instanceSalt | commitmentDomain ). + * - The role nullifier produced by SHA256(roleCommitment | nullifierDomain ). * * @param {Bytes<32>} roleId - The role identifier. - * @param {Either} account - A ZswapCoinPublicKey or ContractAddress. - * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK) + * @param {Bytes<32>} accountId - The account identifier. * @return {Boolean} roleGranted - A boolean indicating if `roleId` was granted. */ export circuit _grantRole(roleId: Bytes<32>, accountId: Bytes<32>): Boolean { From 88355c81740e8ba7fb0ee7682e3cbc56864a77ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Wed, 25 Feb 2026 20:04:07 -0500 Subject: [PATCH 208/322] Improve naming, remove dead code, narrow circuit responsibility --- .../src/access/ShieldedAccessControl.compact | 38 ++++++++----------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index 081c2687..0746583b 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -108,7 +108,7 @@ module ShieldedAccessControl { export struct Role { - isApproved: Boolean; + hasRole: Boolean; roleCommitment: Bytes<32>; commitmentNullifier: Bytes<32>; } @@ -228,19 +228,14 @@ module ShieldedAccessControl { * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK). * @return {Boolean} - A boolean determining if the account has the specified role.  */ - export circuit callerHasRole(roleId: Bytes<32>): Role { + export circuit callerHasRole(roleId: Bytes<32>): Boolean { const callerAsEither = Either { is_left: true, left: ownPublicKey(), right: ContractAddress { bytes: pad(32, "") } }; const nonce = wit_secretNonce(roleId); const accountId = _computeAccountId(callerAsEither, nonce); - return getRole(roleId, accountId); - } - - export circuit hasRole(roleId: Bytes<32>, accountId: Bytes<32>): Boolean { - const roleInfo = getRole(roleId, accountId); - return roleInfo.isApproved; + return computeRole(roleId, accountId).hasRole; } /** @@ -271,23 +266,22 @@ module ShieldedAccessControl { * @return {[]} - Empty tuple. */ export circuit assertOnlyRole(roleId: Bytes<32>): [] { - const role = callerHasRole(roleId); - assert(role.isApproved, "ShieldedAccessControl: unauthorized account"); + assert(callerHasRole(roleId), "ShieldedAccessControl: unauthorized account"); } - export circuit getRole(roleId: Bytes<32>, accountId: Bytes<32>): Role { + export circuit computeRole(roleId: Bytes<32>, accountId: Bytes<32>): Role { const commitment = _computeRoleCommitment(roleId, accountId); const commitmentNullifier = _computeNullifier(commitment); - const authPath = wit_getRoleCommitmentPath(commitment); + const roleCommitmentPath = wit_getRoleCommitmentPath(commitment); const rootMatches = - _operatorRoles.checkRoot(merkleTreePathRoot<10, Bytes<32>>(disclose(authPath))); + _operatorRoles.checkRoot(merkleTreePathRoot<10, Bytes<32>>(disclose(roleCommitmentPath))); if (!_roleCommitmentNullifiers.member(disclose(commitmentNullifier)) && rootMatches) { - return Role { isApproved: true, + return Role { hasRole: true, roleCommitment: disclose(commitment), commitmentNullifier: disclose(commitmentNullifier) }; } else { - return Role { isApproved: false, + return Role { hasRole: false, roleCommitment: disclose(commitment), commitmentNullifier: disclose(commitmentNullifier) }; } @@ -410,17 +404,17 @@ module ShieldedAccessControl { * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK) * @return {[]} - Empty tuple. */ - export circuit renounceRole(roleId: Bytes<32>, callerConfirmation: Bytes<32>): [] { + export circuit renounceRole(roleId: Bytes<32>, accountConfirmation: Bytes<32>): [] { const nonce = wit_secretNonce(roleId); const callerAsEither = Either { is_left: true, left: ownPublicKey(), right: ContractAddress { bytes: pad(32, "") } }; - assert(callerConfirmation == _computeAccountId(callerAsEither, nonce), + assert(accountConfirmation == _computeAccountId(callerAsEither, nonce), "ShieldedAccessControl: bad confirmation" ); - _revokeRole(roleId, callerConfirmation); + _revokeRole(roleId, accountConfirmation); } /** @@ -455,8 +449,8 @@ module ShieldedAccessControl { * @return {Boolean} roleGranted - A boolean indicating if `roleId` was granted. */ export circuit _grantRole(roleId: Bytes<32>, accountId: Bytes<32>): Boolean { - const role = getRole(roleId, accountId); - if (role.isApproved) { + const role = computeRole(roleId, accountId); + if (role.hasRole) { return false; } @@ -493,8 +487,8 @@ module ShieldedAccessControl { * @return {Boolean} roleRevoked - A boolean indicating if `roleId` was revoked. */ export circuit _revokeRole(roleId: Bytes<32>, accountId: Bytes<32>): Boolean { - const role = getRole(roleId, accountId); - if (!role.isApproved) { + const role = computeRole(roleId, accountId); + if (!role.hasRole) { return false; } From 266e0fd592dd5d1df62d539001f80f363090a7c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Thu, 26 Feb 2026 16:09:20 -0500 Subject: [PATCH 209/322] Update module docs --- .../src/access/ShieldedAccessControl.compact | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index 0746583b..da94952e 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -1,4 +1,5 @@ // SPDX-License-Identifier: MIT +// OpenZeppelin Compact Contracts v0.0.1-alpha.1 (access/ShieldedAccessControl.compact) pragma language_version >= 0.21.0; @@ -10,7 +11,7 @@ pragma language_version >= 0.21.0; * disclosing information about role holders. Role commitments are created with the following * hashing scheme SHA256(roleId | accountId | instanceSalt | commitmentDomain). Where * - `accountId` is a unique SHA256 hash of a user's ZswapCoinPublicKey and a 32 byte secret nonce value - * held in a user's local private state + * held in a user's local private state * - `roleId` is a unique `Bytes<32>` identifier * - `instanceSalt` is an immutable, cryptographically strong random value provided on deployment * - `commitmentDomain` is the following zero padded 32 byte string: "ShieldedAccessControl:commitment" @@ -56,9 +57,21 @@ pragma language_version >= 0.21.0; * grant and revoke this role. Extra precautions should be taken to secure * accounts that have been granted it. * - * @dev In this system, adversaries will have knowledge of all role and admin - * identifiers. They will know when an admin is added and how many admins exist - * in the system. + * @dev Privacy Assumptions + * - Outside observers will know when an admin is added and how many admins exist. + * - Outside observers will have knowledge of all role identifiers. + * - Outside observers will have knowledge of role additions and revocations. + * - Outside observers will NOT be able to identify the public address of any role holder + * so long as secret nonce values are kept private and generated using cryptographically + * secure random values. + * + * @dev Security Considerations: + * - The `secretNonce` must be kept private. Loss of the nonce prevents role holders + * and admins from proving access or transferring it. + * - Role validation is entirely circuit-based using witness-provided values. + * - It's strongly recommended to use cryptographically secure random values for the `_instanceSalt`. + * Failure to do so could lead to the exposure of public keys. + * - The `_instanceSalt` is immutable and used to differentiate deployments. * * @notice Missing Features and Improvements: * From efdf636d907fa3f7b4122a6db4e7d7d4d5a0668b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Thu, 26 Feb 2026 16:09:41 -0500 Subject: [PATCH 210/322] Add nominal type aliases --- .../src/access/ShieldedAccessControl.compact | 111 ++++++++++-------- .../mocks/MockShieldedAccessControl.compact | 88 +++++++------- 2 files changed, 101 insertions(+), 98 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index da94952e..2c9868c4 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -82,29 +82,33 @@ module ShieldedAccessControl { import CompactStandardLibrary; import "../utils/Utils" prefix Utils_; + export new type RoleCommitment = Bytes<32>; + export new type RoleIdentifier = Bytes<32>; + export new type AccountIdentifier = Bytes<32>; + export new type AdminIdentifier = Bytes<32>; + export new type RoleNullifer = Bytes<32>; + type ZcpkOrContractAddress = Either; + /** * @ledger _operatorRoles * @description A Merkle tree of role commitments stored as SHA256(roleId | accountId | instanceSalt | commitmentDomain) * Role commitments are derived from a public role identifier (e.g., `persistentHash>(pad(32, "MY_ROLE")`), * an account identifier (e.g., `SHA256(pk, nonce)`), the `instanceSalt`, and a domain separator.  */ - export ledger _operatorRoles: MerkleTree<10, Bytes<32>>; + export ledger _operatorRoles: MerkleTree<10, RoleCommitment>; /** + * @ledger _adminRoles * @description Mapping from a role identifier to an admin role identifier. - * @type {Bytes<32>} roleId - A hash representing a role identifier. - * @type {Bytes<32>} adminId - A hash representing an admin identifier. - * @type {Map} - * @type {Map, Bytes<32>>} _adminRoles  */ - export ledger _adminRoles: Map, Bytes<32>>; + export ledger _adminRoles: Map; /** * @description A set of nullifiers used to revoke the permissions of a role * @type {Bytes<32> roleCommitment - A role commitment created by the following hash: SHA256(accountId | roleId | instanceSalt | "ShieldedAccessControl:commitment"). * @type {Set} _roleCommitmentNullifiers  */ - export ledger _roleCommitmentNullifiers: Set>; + export ledger _roleCommitmentNullifiers: Set; export ledger DEFAULT_ADMIN_ROLE: Bytes<32>; @@ -115,15 +119,16 @@ module ShieldedAccessControl { * @param {Uint<64>} index - An index in the `_operatorRoles` Merkle tree * @return {MerkleTreePath<10, Bytes<32>>} - The Merkle path of `roleCommitment` in the `_operatorRoles` Merkle tree  */ - witness wit_getRoleCommitmentPath(roleCommitment: Bytes<32>): MerkleTreePath<10, Bytes<32>>; - - witness wit_secretNonce(roleId: Bytes<32>): Bytes<32>; + witness wit_getRoleCommitmentPath( + roleCommitment: RoleCommitment + ): MerkleTreePath<10, RoleCommitment>; + witness wit_secretNonce(roleId: RoleIdentifier): Bytes<32>; export struct Role { hasRole: Boolean; - roleCommitment: Bytes<32>; - commitmentNullifier: Bytes<32>; + roleCommitment: RoleCommitment; + roleNullifier: RoleNullifer; } /** @@ -157,20 +162,22 @@ module ShieldedAccessControl { * @returns {Bytes<32>} The commitment derived from `accountId` and `roleId`. */ export pure circuit _computeRoleCommitment( - roleId: Bytes<32>, - accountId: Bytes<32>, - ): Bytes<32> { + roleId: RoleIdentifier, + accountId: AccountIdentifier, + ): RoleCommitment { return persistentHash>>( - [roleId, - accountId, + [roleId as Bytes<32>, + accountId as Bytes<32>, pad(32, "ShieldedAccessControl:commitment")] - ); + ) + as RoleCommitment; } - export pure circuit _computeNullifier(roleCommitment: Bytes<32>): Bytes<32> { + export pure circuit _computeNullifier(roleCommitment: RoleCommitment): RoleNullifer { return persistentHash>>( - [roleCommitment, pad(32, "ShieldedAccessControl:nullifier")] - ); + [roleCommitment as Bytes<32>, pad(32, "ShieldedAccessControl:nullifier")] + ) + as RoleNullifer; } /** @@ -206,12 +213,12 @@ module ShieldedAccessControl { * @returns {Bytes<32>} The computed account ID. */ export pure circuit _computeAccountId( - pk: Either, + pk: ZcpkOrContractAddress, nonce: Bytes<32> - ): Bytes<32> { + ): AccountIdentifier { assert(pk.is_left, "ShieldedAccessControl: contract address owners are not yet supported"); - return persistentHash>>([pk.left.bytes, nonce]); + return persistentHash>>([pk.left.bytes, nonce]) as AccountIdentifier; } /** @@ -241,11 +248,11 @@ module ShieldedAccessControl { * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK). * @return {Boolean} - A boolean determining if the account has the specified role.  */ - export circuit callerHasRole(roleId: Bytes<32>): Boolean { + export circuit callerHasRole(roleId: RoleIdentifier): Boolean { const callerAsEither = - Either { is_left: true, - left: ownPublicKey(), - right: ContractAddress { bytes: pad(32, "") } }; + ZcpkOrContractAddress { is_left: true, + left: ownPublicKey(), + right: ContractAddress { bytes: pad(32, "") } }; const nonce = wit_secretNonce(roleId); const accountId = _computeAccountId(callerAsEither, nonce); return computeRole(roleId, accountId).hasRole; @@ -278,25 +285,27 @@ module ShieldedAccessControl { * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK) * @return {[]} - Empty tuple. */ - export circuit assertOnlyRole(roleId: Bytes<32>): [] { + export circuit assertOnlyRole(roleId: RoleIdentifier): [] { assert(callerHasRole(roleId), "ShieldedAccessControl: unauthorized account"); } - export circuit computeRole(roleId: Bytes<32>, accountId: Bytes<32>): Role { + export circuit computeRole(roleId: RoleIdentifier, accountId: AccountIdentifier): Role { const commitment = _computeRoleCommitment(roleId, accountId); - const commitmentNullifier = _computeNullifier(commitment); + const roleNullifier = _computeNullifier(commitment); const roleCommitmentPath = wit_getRoleCommitmentPath(commitment); const rootMatches = - _operatorRoles.checkRoot(merkleTreePathRoot<10, Bytes<32>>(disclose(roleCommitmentPath))); + _operatorRoles.checkRoot( + merkleTreePathRoot<10, RoleCommitment>(disclose(roleCommitmentPath)) + ); - if (!_roleCommitmentNullifiers.member(disclose(commitmentNullifier)) && rootMatches) { + if (!_roleCommitmentNullifiers.member(disclose(roleNullifier)) && rootMatches) { return Role { hasRole: true, roleCommitment: disclose(commitment), - commitmentNullifier: disclose(commitmentNullifier) }; + roleNullifier: disclose(roleNullifier) }; } else { return Role { hasRole: false, roleCommitment: disclose(commitment), - commitmentNullifier: disclose(commitmentNullifier) }; + roleNullifier: disclose(roleNullifier) }; } } @@ -311,11 +320,11 @@ module ShieldedAccessControl { * @param {Bytes<32>} roleId - The role identifier. * @return {Bytes<32>} roleAdmin - The admin role that controls `roleId`. */ - export circuit getRoleAdmin(roleId: Bytes<32>): Bytes<32> { + export circuit getRoleAdmin(roleId: RoleIdentifier): AdminIdentifier { if (_adminRoles.member(disclose(roleId))) { return _adminRoles.lookup(disclose(roleId)); } - return default>; + return default> as AdminIdentifier; } /** @@ -346,8 +355,8 @@ module ShieldedAccessControl { * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK) * @return {[]} - Empty tuple. */ - export circuit grantRole(roleId: Bytes<32>, accountId: Bytes<32>): [] { - assertOnlyRole(getRoleAdmin(roleId)); + export circuit grantRole(roleId: RoleIdentifier, accountId: AccountIdentifier): [] { + assertOnlyRole(getRoleAdmin(roleId) as RoleIdentifier); _grantRole(roleId, accountId); } @@ -379,8 +388,8 @@ module ShieldedAccessControl { * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK) * @return {[]} - Empty tuple. */ - export circuit revokeRole(roleId: Bytes<32>, accountId: Bytes<32>): [] { - assertOnlyRole(getRoleAdmin(roleId)); + export circuit revokeRole(roleId: RoleIdentifier, accountId: AccountIdentifier): [] { + assertOnlyRole(getRoleAdmin(roleId) as RoleIdentifier); _revokeRole(roleId, accountId); } @@ -417,17 +426,17 @@ module ShieldedAccessControl { * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK) * @return {[]} - Empty tuple. */ - export circuit renounceRole(roleId: Bytes<32>, accountConfirmation: Bytes<32>): [] { + export circuit renounceRole(roleId: RoleIdentifier, accountIdConfirmation: AccountIdentifier): [] { const nonce = wit_secretNonce(roleId); const callerAsEither = - Either { is_left: true, - left: ownPublicKey(), - right: ContractAddress { bytes: pad(32, "") } }; - assert(accountConfirmation == _computeAccountId(callerAsEither, nonce), + ZcpkOrContractAddress { is_left: true, + left: ownPublicKey(), + right: ContractAddress { bytes: pad(32, "") } }; + assert(accountIdConfirmation == _computeAccountId(callerAsEither, nonce), "ShieldedAccessControl: bad confirmation" ); - _revokeRole(roleId, accountConfirmation); + _revokeRole(roleId, accountIdConfirmation); } /** @@ -439,7 +448,7 @@ module ShieldedAccessControl { * @param {Bytes<32>} adminRole - The admin role identifier. * @return {[]} - Empty tuple. */ - export circuit _setRoleAdmin(roleId: Bytes<32>, adminRole: Bytes<32>): [] { + export circuit _setRoleAdmin(roleId: RoleIdentifier, adminRole: AdminIdentifier): [] { _adminRoles.insert(disclose(roleId), disclose(adminRole)); } @@ -461,7 +470,7 @@ module ShieldedAccessControl { * @param {Bytes<32>} accountId - The account identifier. * @return {Boolean} roleGranted - A boolean indicating if `roleId` was granted. */ - export circuit _grantRole(roleId: Bytes<32>, accountId: Bytes<32>): Boolean { + export circuit _grantRole(roleId: RoleIdentifier, accountId: AccountIdentifier): Boolean { const role = computeRole(roleId, accountId); if (role.hasRole) { return false; @@ -499,13 +508,13 @@ module ShieldedAccessControl { * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK) * @return {Boolean} roleRevoked - A boolean indicating if `roleId` was revoked. */ - export circuit _revokeRole(roleId: Bytes<32>, accountId: Bytes<32>): Boolean { + export circuit _revokeRole(roleId: RoleIdentifier, accountId: AccountIdentifier): Boolean { const role = computeRole(roleId, accountId); if (!role.hasRole) { return false; } - _roleCommitmentNullifiers.insert(disclose(role.commitmentNullifier)); + _roleCommitmentNullifiers.insert(disclose(role.roleNullifier)); return true; } } diff --git a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact index 9d0f79cb..5bab32da 100644 --- a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact +++ b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact @@ -4,78 +4,72 @@ pragma language_version >= 0.21.0; import CompactStandardLibrary; -import "../../ShieldedAccessControl" prefix ShieldedAccessControl_; - -export { - ZswapCoinPublicKey, - ContractAddress, - Either, - Maybe, - MerkleTreePath, - ShieldedAccessControl_DEFAULT_ADMIN_ROLE, - ShieldedAccessControl__operatorRoles, - ShieldedAccessControl__roleCommitmentNullifiers, - ShieldedAccessControl_Role -}; +import "../../ShieldedAccessControl" prefix $; + +export { ZswapCoinPublicKey, + ContractAddress, + Either, + Maybe, + MerkleTreePath, + $DEFAULT_ADMIN_ROLE, + $_operatorRoles, + $_roleCommitmentNullifiers, + $Role }; export pure circuit _computeRoleCommitment( - roleId: Bytes<32>, - accountId: Bytes<32> -): Bytes<32> { - return ShieldedAccessControl__computeRoleCommitment(roleId, accountId); + roleId: $RoleIdentifier, + accountId: $AccountIdentifier + ): $RoleCommitment { + return $_computeRoleCommitment(roleId, accountId); } export pure circuit _computeAccountId( - pk: Either, - nonce: Bytes<32> -): Bytes<32> { - return ShieldedAccessControl__computeAccountId(pk, nonce); + pk: Either, + nonce: Bytes<32> + ): $AccountIdentifier { + return $_computeAccountId(pk, nonce); } -export pure circuit _computeNullifier(commitment: Bytes<32>): Bytes<32> { - return ShieldedAccessControl__computeNullifier(commitment); +export pure circuit _computeNullifier(roleCommitment: $RoleCommitment): $RoleNullifer { + return $_computeNullifier(roleCommitment); } -export circuit callerHasRole(roleId: Bytes<32>): ShieldedAccessControl_Role { - return ShieldedAccessControl_callerHasRole(roleId); +export circuit callerHasRole(roleId: $RoleIdentifier): Boolean { + return $callerHasRole(roleId); } -export circuit hasRole(roleId: Bytes<32>, accountId: Bytes<32>): Boolean { - return ShieldedAccessControl_hasRole(roleId, accountId); +export circuit assertOnlyRole(roleId: $RoleIdentifier): [] { + $assertOnlyRole(roleId); } -export circuit assertOnlyRole(roleId: Bytes<32>): [] { - ShieldedAccessControl_assertOnlyRole(roleId); +export circuit computeRole(roleId: $RoleIdentifier, accountId: $AccountIdentifier): $Role { + return $computeRole(roleId, accountId); } -export circuit getRole(roleId: Bytes<32>, accountId: Bytes<32>): ShieldedAccessControl_Role { - return ShieldedAccessControl_getRole(roleId, accountId); +export circuit getRoleAdmin(roleId: $RoleIdentifier): $AdminIdentifier { + return $getRoleAdmin(roleId); } -export circuit getRoleAdmin(roleId: Bytes<32>): Bytes<32> { - return ShieldedAccessControl_getRoleAdmin(roleId); +export circuit grantRole(roleId: $RoleIdentifier, accountId: $AccountIdentifier): [] { + $grantRole(roleId, accountId); } -export circuit grantRole(roleId: Bytes<32>, accountId: Bytes<32>): [] { - ShieldedAccessControl_grantRole(roleId, accountId); +export circuit revokeRole(roleId: $RoleIdentifier, accountId: $AccountIdentifier): [] { + $revokeRole(roleId, accountId); } -export circuit revokeRole(roleId: Bytes<32>, accountId: Bytes<32>): [] { - ShieldedAccessControl_revokeRole(roleId, accountId); +export circuit renounceRole(roleId: $RoleIdentifier, accountIdConfirmation: $AccountIdentifier): [] { + $renounceRole(roleId, accountIdConfirmation); } -export circuit renounceRole(roleId: Bytes<32>, callerConfirmation: Bytes<32>): [] { - ShieldedAccessControl_renounceRole(roleId, callerConfirmation); +export circuit _setRoleAdmin(roleId: $RoleIdentifier, adminRole: $AdminIdentifier): [] { + $_setRoleAdmin(roleId, adminRole); } -export circuit _setRoleAdmin(roleId: Bytes<32>, adminRole: Bytes<32>): [] { - ShieldedAccessControl__setRoleAdmin(roleId, adminRole); +export circuit _grantRole(roleId: $RoleIdentifier, accountId: $AccountIdentifier): Boolean { + return $_grantRole(roleId, accountId); } -export circuit _grantRole(roleId: Bytes<32>, accountId: Bytes<32>): Boolean { - return ShieldedAccessControl__grantRole(roleId, accountId); +export circuit _revokeRole(roleId: $RoleIdentifier, accountId: $AccountIdentifier): Boolean { + return $_revokeRole(roleId, accountId); } - -export circuit _revokeRole(roleId: Bytes<32>, accountId: Bytes<32>): Boolean { - return ShieldedAccessControl__revokeRole(roleId, accountId); -} \ No newline at end of file From 1186e518d11993f792618a0dac79776d07248f5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Thu, 26 Feb 2026 16:16:03 -0500 Subject: [PATCH 211/322] Refactor witness file --- .../ShieldedAccessControlWitnesses.ts | 28 +------------------ 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts b/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts index d42aae43..7a1ca973 100644 --- a/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts +++ b/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts @@ -1,41 +1,15 @@ import { getRandomValues } from 'node:crypto'; import { - CompactTypeBytes, - CompactTypeVector, - convertFieldToBytes, - persistentHash, type WitnessContext, } from '@midnight-ntwrk/compact-runtime'; import type { - ContractAddress, - Either, Ledger, MerkleTreePath, - ZswapCoinPublicKey, -} from '../../../artifacts/MockShieldedAccessControl/contract/index.cjs'; +} from '../../../artifacts/MockShieldedAccessControl/contract/index.js'; const COMMITMENT_DOMAIN = new Uint8Array(32); new TextEncoder().encodeInto('ShieldedAccessControl:commitment', COMMITMENT_DOMAIN); -const eitherToBytes = ( - account: Either, -) => { - if (account.is_left) { - return account.left.bytes; - } - - return account.right.bytes; -}; - -export function fmtHexString(bytes: string | Uint8Array): string { - if (bytes instanceof String) { - return `${bytes.slice(0, 4)}...${bytes.slice(-4)}`; - } else { - const buffStr = Buffer.from(bytes as Uint8Array).toString('hex'); - return `${buffStr.slice(0, 4)}...${buffStr.slice(-4)}`; - } -} - /** * @description Interface defining the witness methods for ShieldedAccessControl operations. * @template P - The private state type. From e819d8d9a147bab3eb4093ce9390f9935138ad46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Thu, 26 Feb 2026 16:16:15 -0500 Subject: [PATCH 212/322] Change prefix --- .../mocks/MockShieldedAccessControl.compact | 91 ++++++++++++------- 1 file changed, 58 insertions(+), 33 deletions(-) diff --git a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact index 5bab32da..ff4ffe9a 100644 --- a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact +++ b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact @@ -4,72 +4,97 @@ pragma language_version >= 0.21.0; import CompactStandardLibrary; -import "../../ShieldedAccessControl" prefix $; +import "../../ShieldedAccessControl" prefix ShieldedAccessControl_; export { ZswapCoinPublicKey, ContractAddress, Either, Maybe, MerkleTreePath, - $DEFAULT_ADMIN_ROLE, - $_operatorRoles, - $_roleCommitmentNullifiers, - $Role }; + ShieldedAccessControl_DEFAULT_ADMIN_ROLE, + ShieldedAccessControl__operatorRoles, + ShieldedAccessControl__roleCommitmentNullifiers, + ShieldedAccessControl_Role }; export pure circuit _computeRoleCommitment( - roleId: $RoleIdentifier, - accountId: $AccountIdentifier - ): $RoleCommitment { - return $_computeRoleCommitment(roleId, accountId); + roleId: ShieldedAccessControl_RoleIdentifier, + accountId: ShieldedAccessControl_AccountIdentifier + ): ShieldedAccessControl_RoleCommitment { + return ShieldedAccessControl__computeRoleCommitment(roleId, accountId); } export pure circuit _computeAccountId( pk: Either, nonce: Bytes<32> - ): $AccountIdentifier { - return $_computeAccountId(pk, nonce); + ): ShieldedAccessControl_AccountIdentifier { + return ShieldedAccessControl__computeAccountId(pk, nonce); } -export pure circuit _computeNullifier(roleCommitment: $RoleCommitment): $RoleNullifer { - return $_computeNullifier(roleCommitment); +export pure circuit _computeNullifier( + roleCommitment: ShieldedAccessControl_RoleCommitment + ): ShieldedAccessControl_RoleNullifer { + return ShieldedAccessControl__computeNullifier(roleCommitment); } -export circuit callerHasRole(roleId: $RoleIdentifier): Boolean { - return $callerHasRole(roleId); +export circuit callerHasRole(roleId: ShieldedAccessControl_RoleIdentifier): Boolean { + return ShieldedAccessControl_callerHasRole(roleId); } -export circuit assertOnlyRole(roleId: $RoleIdentifier): [] { - $assertOnlyRole(roleId); +export circuit assertOnlyRole(roleId: ShieldedAccessControl_RoleIdentifier): [] { + ShieldedAccessControl_assertOnlyRole(roleId); } -export circuit computeRole(roleId: $RoleIdentifier, accountId: $AccountIdentifier): $Role { - return $computeRole(roleId, accountId); +export circuit computeRole( + roleId: ShieldedAccessControl_RoleIdentifier, + accountId: ShieldedAccessControl_AccountIdentifier + ): ShieldedAccessControl_Role { + return ShieldedAccessControl_computeRole(roleId, accountId); } -export circuit getRoleAdmin(roleId: $RoleIdentifier): $AdminIdentifier { - return $getRoleAdmin(roleId); +export circuit getRoleAdmin( + roleId: ShieldedAccessControl_RoleIdentifier + ): ShieldedAccessControl_AdminIdentifier { + return ShieldedAccessControl_getRoleAdmin(roleId); } -export circuit grantRole(roleId: $RoleIdentifier, accountId: $AccountIdentifier): [] { - $grantRole(roleId, accountId); +export circuit grantRole( + roleId: ShieldedAccessControl_RoleIdentifier, + accountId: ShieldedAccessControl_AccountIdentifier + ): [] { + ShieldedAccessControl_grantRole(roleId, accountId); } -export circuit revokeRole(roleId: $RoleIdentifier, accountId: $AccountIdentifier): [] { - $revokeRole(roleId, accountId); +export circuit revokeRole( + roleId: ShieldedAccessControl_RoleIdentifier, + accountId: ShieldedAccessControl_AccountIdentifier + ): [] { + ShieldedAccessControl_revokeRole(roleId, accountId); } -export circuit renounceRole(roleId: $RoleIdentifier, accountIdConfirmation: $AccountIdentifier): [] { - $renounceRole(roleId, accountIdConfirmation); +export circuit renounceRole( + roleId: ShieldedAccessControl_RoleIdentifier, + accountIdConfirmation: ShieldedAccessControl_AccountIdentifier + ): [] { + ShieldedAccessControl_renounceRole(roleId, accountIdConfirmation); } -export circuit _setRoleAdmin(roleId: $RoleIdentifier, adminRole: $AdminIdentifier): [] { - $_setRoleAdmin(roleId, adminRole); +export circuit _setRoleAdmin( + roleId: ShieldedAccessControl_RoleIdentifier, + adminRole: ShieldedAccessControl_AdminIdentifier + ): [] { + ShieldedAccessControl__setRoleAdmin(roleId, adminRole); } -export circuit _grantRole(roleId: $RoleIdentifier, accountId: $AccountIdentifier): Boolean { - return $_grantRole(roleId, accountId); +export circuit _grantRole( + roleId: ShieldedAccessControl_RoleIdentifier, + accountId: ShieldedAccessControl_AccountIdentifier + ): Boolean { + return ShieldedAccessControl__grantRole(roleId, accountId); } -export circuit _revokeRole(roleId: $RoleIdentifier, accountId: $AccountIdentifier): Boolean { - return $_revokeRole(roleId, accountId); +export circuit _revokeRole( + roleId: ShieldedAccessControl_RoleIdentifier, + accountId: ShieldedAccessControl_AccountIdentifier + ): Boolean { + return ShieldedAccessControl__revokeRole(roleId, accountId); } From 1b59084d2d20054348aea5f4e4581c94f3c54467 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Thu, 26 Feb 2026 16:20:25 -0500 Subject: [PATCH 213/322] Refactor simulator --- .../ShieldedAccessControlSimulator.ts | 40 +++++-------------- 1 file changed, 9 insertions(+), 31 deletions(-) diff --git a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts index f0358f0e..114edb5e 100644 --- a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts +++ b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts @@ -20,17 +20,7 @@ import { */ type ShieldedAccessControlArgs = readonly []; -/** - * Base simulator - * @dev We deliberately use `any` as the base simulator type. - * This workaround is necessary due to type inference and declaration filegen - * in a monorepo environment. Attempting to fully preserve type information - * turns into type gymnastics. - * - * `any` can be safely removed once the contract simulator is consumed - * as a properly packaged dependency (outside the monorepo). - */ -const ShieldedAccessControlSimulatorBase: any = createSimulator< +const ShieldedAccessControlSimulatorBase = createSimulator< ShieldedAccessControlPrivateState, ReturnType, ReturnType, @@ -78,22 +68,10 @@ export class ShieldedAccessControlSimulator extends ShieldedAccessControlSimulat return this.circuits.pure._computeNullifier(commitment); } - public callerHasRole(roleId: Uint8Array): Role { + public callerHasRole(roleId: Uint8Array): Boolean { return this.circuits.impure.callerHasRole(roleId); } - /** - * @description Returns the current commitment representing the contract owner. - * The full commitment is: `SHA256(SHA256(pk, nonce), instanceSalt, counter, domain)`. - * @returns The current owner's commitment. - */ - public hasRole( - roleId: Uint8Array, - accountId: Uint8Array, - ): Boolean { - return this.circuits.impure.hasRole(roleId, accountId); - } - /** * @description Transfers ownership to `newOwnerId`. * `newOwnerId` must be precalculated and given to the current owner off chain. @@ -103,12 +81,12 @@ export class ShieldedAccessControlSimulator extends ShieldedAccessControlSimulat this.circuits.impure.assertOnlyRole(roleId); } - public getRole(roleId: Uint8Array, accountId: Uint8Array): Role { - return this.circuits.impure.getRole(roleId, accountId); + public computeRole(roleId: Uint8Array, accountId: Uint8Array): Role { + return this.circuits.impure.computeRole(roleId, accountId); } /** - * @description Computes the owner commitment from the given `id` and `counter`. + * @description Computes the role commitment from the given `id` and `counter`. * @param id - The unique identifier of the owner calculated by `SHA256(pk, nonce)`. * @param counter - The current counter or round. This increments by `1` * after every transfer to prevent duplicate commitments given the same `id`. @@ -193,19 +171,19 @@ export class ShieldedAccessControlSimulator extends ShieldedAccessControlSimulat /** * @description Contextually sets a new nonce into the private state. * @param newNonce The secret nonce. - * @returns The ShieldedAccessControlPK private state after setting the new nonce. + * @returns The ShieldedAccessControl private state after setting the new nonce. */ injectSecretNonce: ( roleId: Uint8Array, newNonce: Buffer, ): ShieldedAccessControlPrivateState => { - const currentState = this.stateManager.getContext().currentPrivateState; + const currentState = this.getPrivateState(); const updatedState = { roles: { ...currentState.roles }, }; const roleString = Buffer.from(roleId).toString('hex'); updatedState.roles[roleString] = newNonce; - this.stateManager.updatePrivateState(updatedState); + this.circuitContextManager.updatePrivateState(updatedState); return updatedState; }, @@ -215,7 +193,7 @@ export class ShieldedAccessControlSimulator extends ShieldedAccessControlSimulat */ getCurrentSecretNonce: (roleId: Uint8Array): Uint8Array => { const roleString = Buffer.from(roleId).toString('hex'); - return this.stateManager.getContext().currentPrivateState.roles[ + return this.getPrivateState().roles[ roleString ]; }, From aef1ed3bb7646a766b1952ec60ac99642ac8fdfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Thu, 26 Feb 2026 17:56:26 -0500 Subject: [PATCH 214/322] Update error message --- contracts/src/access/ShieldedAccessControl.compact | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index 2c9868c4..0156021d 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -216,7 +216,7 @@ module ShieldedAccessControl { pk: ZcpkOrContractAddress, nonce: Bytes<32> ): AccountIdentifier { - assert(pk.is_left, "ShieldedAccessControl: contract address owners are not yet supported"); + assert(pk.is_left, "ShieldedAccessControl: contract address roles are not yet supported"); return persistentHash>>([pk.left.bytes, nonce]) as AccountIdentifier; } From 1f3349db883f187cd03976e98968c5210bc16929 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Thu, 26 Feb 2026 17:57:04 -0500 Subject: [PATCH 215/322] rename param --- .../access/test/simulators/ShieldedAccessControlSimulator.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts index 114edb5e..574de03a 100644 --- a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts +++ b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts @@ -64,8 +64,8 @@ export class ShieldedAccessControlSimulator extends ShieldedAccessControlSimulat return this.circuits.pure._computeAccountId(pk, nonce); } - public _computeNullifier(commitment: Uint8Array): Uint8Array { - return this.circuits.pure._computeNullifier(commitment); + public _computeNullifier(roleCommitment: Uint8Array): Uint8Array { + return this.circuits.pure._computeNullifier(roleCommitment); } public callerHasRole(roleId: Uint8Array): Boolean { From d4f6b8d7b3c564915108ad6587ea0a34123a2d53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Thu, 26 Feb 2026 18:05:10 -0500 Subject: [PATCH 216/322] Refactor tests --- .../access/test/ShieldedAccessControl.test.ts | 265 +++++++++++------- 1 file changed, 168 insertions(+), 97 deletions(-) diff --git a/contracts/src/access/test/ShieldedAccessControl.test.ts b/contracts/src/access/test/ShieldedAccessControl.test.ts index 16961190..ff1fc48a 100644 --- a/contracts/src/access/test/ShieldedAccessControl.test.ts +++ b/contracts/src/access/test/ShieldedAccessControl.test.ts @@ -1,38 +1,42 @@ import { CompactTypeBytes, CompactTypeVector, - persistentHash, convertFieldToBytes, - type WitnessContext, + MerkleTreePath, + persistentHash, } from '@midnight-ntwrk/compact-runtime'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it } from 'vitest'; import { - ContractAddress, - Either, - Ledger, - MerkleTreePath, ShieldedAccessControl_Role as Role, ZswapCoinPublicKey, - Contract as MockShieldedAccessControl -} from '../../../artifacts/MockShieldedAccessControl/contract/index.cjs'; -import { fmtHexString, ShieldedAccessControlPrivateState, ShieldedAccessControlWitnesses } from '../witnesses/ShieldedAccessControlWitnesses.js'; +} from '../../../artifacts/MockShieldedAccessControl/contract/index.js'; +import { ShieldedAccessControlPrivateState, ShieldedAccessControlWitnesses } from '../witnesses/ShieldedAccessControlWitnesses.js'; import { ShieldedAccessControlSimulator } from './simulators/ShieldedAccessControlSimulator.js'; import * as utils from '#test-utils/address.js'; +const COMMITMENT_DOMAIN = "ShieldedAccessControl:commitment"; +const NULLIFIER_DOMAIN = "ShieldedAccessControl:nullifier"; + +const DEFAULT_MT_PATH: MerkleTreePath = { + leaf: new Uint8Array(32), + path: Array.from({ length: 10 }, () => ({ + sibling: { field: 0n }, + goes_left: false, + })), +}; + // Helpers const buildCommitment = ( - accountId: Uint8Array, roleId: Uint8Array, - index: bigint, + accountId: Uint8Array ): Uint8Array => { - const rt_type = new CompactTypeVector(4, new CompactTypeBytes(32)); - const bIndex = convertFieldToBytes(32, index, ''); + const rt_type = new CompactTypeVector(3, new CompactTypeBytes(32)); + const bDomain = new TextEncoder().encode(COMMITMENT_DOMAIN); const commitment = persistentHash(rt_type, [ - accountId, roleId, - bIndex, - COMMITMENT_DOMAIN, + accountId, + bDomain, ]); return commitment; @@ -42,16 +46,17 @@ const buildNullifier = ( roleCommitment: Uint8Array, ): Uint8Array => { const rt_type = new CompactTypeVector(2, new CompactTypeBytes(32)); + const bDomain = new TextEncoder().encode(NULLIFIER_DOMAIN); const nullifier = persistentHash(rt_type, [ roleCommitment, - NULLIFIER_DOMAIN, + bDomain, ]); return nullifier; }; -const createIdHash = ( +const buildAccountId = ( pk: ZswapCoinPublicKey, nonce: Uint8Array, ): Uint8Array => { @@ -61,45 +66,34 @@ const createIdHash = ( return persistentHash(rt_type, [bPK, nonce]); }; +class ShieldedAccessControlConstant { + publicKey: string; + zPublicKey: ZswapCoinPublicKey; + roleId: Uint8Array; + accountId: Uint8Array; + roleNullifier: Uint8Array; + roleCommitment: Uint8Array; + secretNonce: Buffer; + + constructor(baseString: string, roleIdentifier: bigint) { + [this.publicKey, this.zPublicKey] = utils.generatePubKeyPair(baseString); + this.secretNonce = Buffer.alloc(32, baseString + "_NONCE"); + this.accountId = buildAccountId(this.zPublicKey, this.secretNonce); + this.roleId = convertFieldToBytes(32, roleIdentifier, ''); + this.roleCommitment = buildCommitment(this.roleId, this.accountId); + this.roleNullifier = buildNullifier(this.roleCommitment); + }; +} + + // PKs -const [ADMIN, Z_ADMIN] = utils.generatePubKeyPair('ADMIN'); -const [OPERATOR_1, Z_OPERATOR_1] = utils.generatePubKeyPair('OPERATOR_1'); -const [OPERATOR_2, Z_OPERATOR_2] = utils.generatePubKeyPair('OPERATOR_2'); -const [OPERATOR_3, Z_OPERATOR_3] = utils.generatePubKeyPair('OPERATOR_3'); -const [UNAUTHORIZED, Z_UNAUTHORIZED] = utils.generatePubKeyPair('UNAUTHORIZED'); - -// Roles -const DEFAULT_ADMIN_ROLE = utils.zeroUint8Array(); -const OPERATOR_1_ROLE = convertFieldToBytes(32, 1n, ''); -const OPERATOR_2_ROLE = convertFieldToBytes(32, 2n, ''); -const OPERATOR_3_ROLE = convertFieldToBytes(32, 3n, ''); -const UNINITIALIZED_ROLE = convertFieldToBytes(32, 555n, ''); -const BAD_ROLE = convertFieldToBytes(32, 99999999n, ''); - -// Nonces -const ADMIN_SECRET_NONCE = Buffer.alloc(32, 'ADMIN_SECRET_NONCE'); -const OPERATOR_1_SECRET_NONCE = Buffer.alloc(32, 'OPERATOR_1_NONCE'); -const OPERATOR_2_SECRET_NONCE = Buffer.alloc(32, 'OPERATOR_2_NONCE'); -const OPERATOR_3_SECRET_NONCE = Buffer.alloc(32, 'OPERATOR_3_NONCE'); -const BAD_NONCE = Buffer.alloc(32, 'BAD_NONCE'); - -// Constants -const COMMITMENT_DOMAIN = new Uint8Array(32); -new TextEncoder().encodeInto('ShieldedAccessControl:commitment', COMMITMENT_DOMAIN); -const NULLIFIER_DOMAIN = new Uint8Array(32); -new TextEncoder().encodeInto('ShieldedAccessControl:nullifier', NULLIFIER_DOMAIN); - -const ADMIN_ID = createIdHash(Z_ADMIN, ADMIN_SECRET_NONCE); -const ADMIN_COMMITMENT = buildCommitment(ADMIN_ID, DEFAULT_ADMIN_ROLE, 0n); -const ADMIN_NULLIFIER = buildNullifier(ADMIN_COMMITMENT); - -const OPERATOR_1_ID = createIdHash(Z_OPERATOR_1, OPERATOR_1_SECRET_NONCE); -const OPERATOR_2_ID = createIdHash(Z_OPERATOR_2, OPERATOR_2_SECRET_NONCE); -const OPERATOR_3_ID = createIdHash(Z_OPERATOR_3, OPERATOR_3_SECRET_NONCE); - -const BAD_ID = createIdHash(Z_UNAUTHORIZED, new Uint8Array(32)); -const BAD_INDEX = 99999999n; -const BAD_COMMITMENT = buildCommitment(BAD_ID, BAD_ROLE, BAD_INDEX); +const ADMIN = new ShieldedAccessControlConstant('ADMIN', 0n); +const OPERATOR_1 = new ShieldedAccessControlConstant('OPERATOR_1', 1n); +const OPERATOR_2 = new ShieldedAccessControlConstant('OPERATOR_2', 2n); +const OPERATOR_3 = new ShieldedAccessControlConstant('OPERATOR_3', 3n); +const UNAUTHORIZED = new ShieldedAccessControlConstant('UNAUTHORIZED', 99999999n); +const UNINITIALIZED = new ShieldedAccessControlConstant('UNINITIALIZED', 555n); +const BAD_INPUT = new ShieldedAccessControlConstant('BAD_INPUT', 666n); let shieldedAccessControl: ShieldedAccessControlSimulator; @@ -108,42 +102,123 @@ describe('ShieldedAccessControl', () => { beforeEach(() => { // Create private state object and generate nonce const PS = ShieldedAccessControlPrivateState.withRoleAndNonce( - Buffer.from(DEFAULT_ADMIN_ROLE), - ADMIN_SECRET_NONCE, + Buffer.from(ADMIN.roleId), + ADMIN.secretNonce, ); // Init contract for user with PS shieldedAccessControl = new ShieldedAccessControlSimulator({ privateState: PS, - coinPK: ADMIN + witnesses: ShieldedAccessControlWitnesses() + }); + }); + + describe('checked circuits should fail for authorized caller with invalid witness values', () => { + beforeEach(() => { + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl.as(ADMIN.publicKey); }); + + type FailingCircuits = [ + method: keyof ShieldedAccessControlSimulator, + isValidNonce: boolean, + isValidPath: boolean, + args: unknown[], + ]; + const checkedCircuits: FailingCircuits[] = [ + ['assertOnlyRole', false, true, [ADMIN.roleId]], + ['assertOnlyRole', true, false, [ADMIN.roleId]], + ['assertOnlyRole', false, false, [ADMIN.roleId]], + ['grantRole', false, true, [ADMIN.roleId, ADMIN.accountId]], + ['grantRole', true, false, [ADMIN.roleId, ADMIN.accountId]], + ['grantRole', false, false, [ADMIN.roleId, ADMIN.accountId]], + ['revokeRole', true, false, [ADMIN.roleId, ADMIN.accountId]], + ['revokeRole', false, true, [ADMIN.roleId, ADMIN.accountId]], + ['revokeRole', false, false, [ADMIN.roleId, ADMIN.accountId]], + ]; + + it.each(checkedCircuits)( + '%s should fail with isValidNonce(%s), isValidPath(%s)', + (circuitName, isValidNonce, isValidPath, args) => { + if (isValidNonce) { + // Check nonce matches + expect( + shieldedAccessControl.privateState.getCurrentSecretNonce( + ADMIN.roleId, + ), + ).toEqual(ADMIN.secretNonce); + } else { + // Check nonce does not match + shieldedAccessControl.privateState.injectSecretNonce( + ADMIN.roleId, + UNAUTHORIZED.secretNonce, + ); + expect( + shieldedAccessControl.privateState.getCurrentSecretNonce( + ADMIN.roleId, + ), + ).not.toEqual(ADMIN.secretNonce); + } + + if (isValidPath) { + // Check path matches + const truePath = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf( + ADMIN.roleCommitment, + ); + const [, witnessCalculatedPath] = + shieldedAccessControl.witnesses.wit_getRoleCommitmentPath( + shieldedAccessControl.getWitnessContext(), + ADMIN.roleCommitment, + ); + expect(witnessCalculatedPath).toEqual(truePath); + } else { + // Check path does not match + const truePath = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf( + ADMIN.roleCommitment, + ); + shieldedAccessControl.overrideWitness('wit_getRoleCommitmentPath', (ctx) => { + return [ctx.privateState, DEFAULT_MT_PATH]; + }); + const [, witnessCalculatedPath] = shieldedAccessControl.witnesses.wit_getRoleCommitmentPath(shieldedAccessControl.getWitnessContext(), BAD_INPUT.roleCommitment) + expect(witnessCalculatedPath).not.toEqual(truePath); + } + + // Test protected circuit + expect(() => { + ( + shieldedAccessControl[circuitName] as ( + ...args: unknown[] + ) => unknown + )(...args); + }).toThrow('ShieldedAccessControl: unauthorized account'); + }, + ); }); describe('_computeRoleCommitment', () => { it('computed commitment should match', () => { - expect(shieldedAccessControl._computeRoleCommitment(ADMIN_ID, DEFAULT_ADMIN_ROLE, 0n)).toEqual(ADMIN_COMMITMENT); + expect(shieldedAccessControl._computeRoleCommitment(ADMIN.roleId, ADMIN.accountId)).toEqual(ADMIN.roleCommitment); }); type ComputeRoleCommitmentCases = [ method: keyof ShieldedAccessControlSimulator, isValidId: boolean, isValidRole: boolean, - isValidIndex: boolean, args: unknown[], ]; const checkedCircuits: ComputeRoleCommitmentCases[] = [ - ['_computeRoleCommitment', false, true, true, [BAD_ID, DEFAULT_ADMIN_ROLE, 0n]], - ['_computeRoleCommitment', true, false, true, [ADMIN_ID, BAD_ROLE, 0n]], - ['_computeRoleCommitment', true, true, false, [ADMIN_ID, DEFAULT_ADMIN_ROLE, BAD_INDEX]], - ['_computeRoleCommitment', false, true, false, [BAD_ID, DEFAULT_ADMIN_ROLE, BAD_INDEX]], - ['_computeRoleCommitment', false, false, false, [BAD_ID, BAD_ROLE, BAD_INDEX]], - ['_computeRoleCommitment', true, false, false, [ADMIN_ID, BAD_ROLE, BAD_INDEX]], - ['_computeRoleCommitment', false, false, true, [BAD_ID, BAD_ROLE, 0n]], + ['_computeRoleCommitment', false, true, [BAD_INPUT.roleId, ADMIN.accountId]], + ['_computeRoleCommitment', true, false, [ADMIN.roleId, BAD_INPUT.accountId]], + ['_computeRoleCommitment', false, false, [BAD_INPUT.roleId, BAD_INPUT.accountId]], ] it.each(checkedCircuits)( - '%s should not match with isValidNonce(%s), isValidIndex(%s), isValidPath(%s)', - (circuitName, isValidId, isValidRole, isValidIndex, args) => { + '%s should not match with isValidNonce(%s), isValidPath(%s)', + (circuitName, isValidId, isValidRole, args) => { // Test protected circuit expect(() => { ( @@ -158,27 +233,27 @@ describe('ShieldedAccessControl', () => { describe('_computeNullifier', () => { it('should match nullifier', () => { - expect(shieldedAccessControl._computeNullifier(ADMIN_COMMITMENT)).toEqual(ADMIN_NULLIFIER); + expect(shieldedAccessControl._computeNullifier(ADMIN.roleCommitment)).toEqual(ADMIN.roleNullifier); }); - it('should not match with bad commitment', () => { - expect(shieldedAccessControl._computeNullifier(BAD_COMMITMENT)).not.toEqual(ADMIN_NULLIFIER); + it('should not match bad commitment inputs', () => { + expect(shieldedAccessControl._computeNullifier(BAD_INPUT.roleCommitment)).not.toEqual(ADMIN.roleNullifier); }); }); - describe('_computeRoleId', () => { + describe('_computeAccountId', () => { const eitherAdmin = utils.createEitherTestUser('ADMIN'); const eitherUnauthorized = utils.createEitherTestUser('UNAUTHORIZED'); it('should match role id', () => { - expect(shieldedAccessControl._computeRoleId(eitherAdmin, ADMIN_SECRET_NONCE)).toEqual(ADMIN_ID); + expect(shieldedAccessControl._computeAccountId(eitherAdmin, ADMIN.secretNonce)).toEqual(ADMIN.accountId); }); it('should fail for contract address', () => { const eitherContract = utils.createEitherTestContractAddress('CONTRACT') expect(() => { - shieldedAccessControl._computeRoleId(eitherContract, ADMIN_SECRET_NONCE); - }).toThrow('ShieldedAccessControl: contract address owners are not yet supported'); + shieldedAccessControl._computeAccountId(eitherContract, ADMIN.secretNonce); + }).toThrow('ShieldedAccessControl: contract address roles are not yet supported'); }); type ComputeRoleIdCases = [ @@ -189,9 +264,9 @@ describe('ShieldedAccessControl', () => { ]; const checkedCircuits: ComputeRoleIdCases[] = [ - ['_computeRoleId', true, false, [eitherAdmin, BAD_NONCE]], - ['_computeRoleId', false, true, [eitherUnauthorized, ADMIN_SECRET_NONCE]], - ['_computeRoleId', false, false, [eitherUnauthorized, BAD_NONCE]], + ['_computeAccountId', true, false, [eitherAdmin, UNAUTHORIZED.secretNonce]], + ['_computeAccountId', false, true, [eitherUnauthorized, ADMIN.secretNonce]], + ['_computeAccountId', false, false, [eitherUnauthorized, UNAUTHORIZED.secretNonce]], ]; it.each(checkedCircuits)( @@ -204,47 +279,43 @@ describe('ShieldedAccessControl', () => { ...args: unknown[] ) => unknown )(...args); - }).not.toEqual(ADMIN_ID); + }).not.toEqual(ADMIN.accountId); } ) }); - describe('wit_getRoleCommitmentPath', () => { - it('should return a Merkle tree path if one exists', () => { + describe('computeRole', () => { + beforeEach(() => { + shieldedAccessControl.as(ADMIN.publicKey); }); - }); - describe('getRole', () => { it('should return unapproved if role does not exist', () => { - expect(shieldedAccessControl.getRole(UNINITIALIZED_ROLE, ADMIN_ID).isApproved).toBe(false); + expect(shieldedAccessControl.computeRole(UNINITIALIZED.roleId, ADMIN.accountId).hasRole).toBe(false); }); it('should return correct commitment', () => { - expect(shieldedAccessControl.getRole(DEFAULT_ADMIN_ROLE, ADMIN_ID).roleCommitment).toEqual(ADMIN_COMMITMENT); + expect(shieldedAccessControl.computeRole(ADMIN.roleId, ADMIN.accountId).roleCommitment).toEqual(ADMIN.roleCommitment); }); it('should return correct nullifier', () => { - expect(shieldedAccessControl.getRole(DEFAULT_ADMIN_ROLE, ADMIN_ID).commitmentNullifier).toEqual(ADMIN_NULLIFIER); + expect(shieldedAccessControl.computeRole(ADMIN.roleId, ADMIN.accountId).roleNullifier).toEqual(ADMIN.roleNullifier); }); it('should return approved role', () => { - shieldedAccessControl._grantRole(DEFAULT_ADMIN_ROLE, ADMIN_ID); - expect(shieldedAccessControl.getRole(DEFAULT_ADMIN_ROLE, ADMIN_ID).isApproved).toBe(true); + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + expect(shieldedAccessControl.computeRole(ADMIN.roleId, ADMIN.accountId).hasRole).toBe(true); }); }); describe('_grantRole', () => { it('should return true for new role', () => { - expect(shieldedAccessControl._grantRole(DEFAULT_ADMIN_ROLE, ADMIN_ID)).toBe(true); + expect(shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId)).toBe(true); }); it('should return false if role already granted', () => { - shieldedAccessControl._grantRole(DEFAULT_ADMIN_ROLE, ADMIN_ID); - expect(shieldedAccessControl._grantRole(DEFAULT_ADMIN_ROLE, ADMIN_ID)).toBe(false); + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + expect(shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId)).toBe(false); }); }); - - describe('') - }); \ No newline at end of file From e67fc4aa10d17ed04dc9222024973c8e8a77baba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Fri, 27 Feb 2026 14:24:24 -0500 Subject: [PATCH 217/322] Add Initializable --- .../src/access/ShieldedAccessControl.compact | 67 +++++++++++++++++-- .../mocks/MockShieldedAccessControl.compact | 21 ++++-- 2 files changed, 79 insertions(+), 9 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index 0156021d..fb7d01fc 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -81,6 +81,7 @@ pragma language_version >= 0.21.0; module ShieldedAccessControl { import CompactStandardLibrary; import "../utils/Utils" prefix Utils_; + import "../security/Initializable" prefix Initializable_; export new type RoleCommitment = Bytes<32>; export new type RoleIdentifier = Bytes<32>; @@ -112,6 +113,16 @@ module ShieldedAccessControl { export ledger DEFAULT_ADMIN_ROLE: Bytes<32>; + /** + * @sealed @ledger _instanceSalt + * @description A per-instance value provided at initialization used to namespace + * commitments for this contract instance. + * + * This salt prevents commitment collisions across contracts that might otherwise use + * the same identifiers or domain parameters. It is immutable after initialization. + */ + export sealed ledger _instanceSalt: Bytes<32>; + /** * @description Returns a Merkle path in the `_operatorRoles` Merkle tree, given the knowledge that a `roleCommitment` is at the given index. * @@ -131,6 +142,29 @@ module ShieldedAccessControl { roleNullifier: RoleNullifer; } + /** + * @description Initializes the contract by storing the `instanceSalt` that acts as a privacy additive + * for preventing duplicate commitments among other contracts implementing ShieldedAccessControl. + * + * @warning The `instanceSalt` must be calculated prior to contract deployment using a cryptographically + * secure random number generator e.g. crypto.getRandomValues() to maintain strong privacy guarantees + * + * @circuitInfo k=14, rows=14933 + * + * Requirements: + * + * - Contract is not initialized. + * + * @param {Bytes<32>} instanceSalt - Contract salt to prevent duplicate commitments if + * users reuse their PK and secretNonce witness across different contracts (not recommended). + * @returns {[]} Empty tuple. + */ + export circuit initialize(instanceSalt: Bytes<32>): [] { + Initializable_initialize(); + + _instanceSalt = disclose(instanceSalt); + } + /** * @description Computes the role commitment from the given `accountId` and `roleId`. * @@ -161,13 +195,16 @@ module ShieldedAccessControl { * @param {Bytes<32>} accountId - The unique identifier of the account calculated by `SHA256(zcpk, nonce)`. * @returns {Bytes<32>} The commitment derived from `accountId` and `roleId`. */ - export pure circuit _computeRoleCommitment( - roleId: RoleIdentifier, - accountId: AccountIdentifier, - ): RoleCommitment { - return persistentHash>>( + export circuit _computeRoleCommitment( + roleId: RoleIdentifier, + accountId: AccountIdentifier, + ): RoleCommitment { + Initializable_assertInitialized(); + + return persistentHash>>( [roleId as Bytes<32>, accountId as Bytes<32>, + _instanceSalt, pad(32, "ShieldedAccessControl:commitment")] ) as RoleCommitment; @@ -249,6 +286,8 @@ module ShieldedAccessControl { * @return {Boolean} - A boolean determining if the account has the specified role.  */ export circuit callerHasRole(roleId: RoleIdentifier): Boolean { + Initializable_assertInitialized(); + const callerAsEither = ZcpkOrContractAddress { is_left: true, left: ownPublicKey(), @@ -286,10 +325,14 @@ module ShieldedAccessControl { * @return {[]} - Empty tuple. */ export circuit assertOnlyRole(roleId: RoleIdentifier): [] { + Initializable_assertInitialized(); + assert(callerHasRole(roleId), "ShieldedAccessControl: unauthorized account"); } export circuit computeRole(roleId: RoleIdentifier, accountId: AccountIdentifier): Role { + Initializable_assertInitialized(); + const commitment = _computeRoleCommitment(roleId, accountId); const roleNullifier = _computeNullifier(commitment); const roleCommitmentPath = wit_getRoleCommitmentPath(commitment); @@ -321,6 +364,8 @@ module ShieldedAccessControl { * @return {Bytes<32>} roleAdmin - The admin role that controls `roleId`. */ export circuit getRoleAdmin(roleId: RoleIdentifier): AdminIdentifier { + Initializable_assertInitialized(); + if (_adminRoles.member(disclose(roleId))) { return _adminRoles.lookup(disclose(roleId)); } @@ -356,6 +401,8 @@ module ShieldedAccessControl { * @return {[]} - Empty tuple. */ export circuit grantRole(roleId: RoleIdentifier, accountId: AccountIdentifier): [] { + Initializable_assertInitialized(); + assertOnlyRole(getRoleAdmin(roleId) as RoleIdentifier); _grantRole(roleId, accountId); } @@ -389,6 +436,8 @@ module ShieldedAccessControl { * @return {[]} - Empty tuple. */ export circuit revokeRole(roleId: RoleIdentifier, accountId: AccountIdentifier): [] { + Initializable_assertInitialized(); + assertOnlyRole(getRoleAdmin(roleId) as RoleIdentifier); _revokeRole(roleId, accountId); } @@ -427,6 +476,8 @@ module ShieldedAccessControl { * @return {[]} - Empty tuple. */ export circuit renounceRole(roleId: RoleIdentifier, accountIdConfirmation: AccountIdentifier): [] { + Initializable_assertInitialized(); + const nonce = wit_secretNonce(roleId); const callerAsEither = ZcpkOrContractAddress { is_left: true, @@ -449,6 +500,8 @@ module ShieldedAccessControl { * @return {[]} - Empty tuple. */ export circuit _setRoleAdmin(roleId: RoleIdentifier, adminRole: AdminIdentifier): [] { + Initializable_assertInitialized(); + _adminRoles.insert(disclose(roleId), disclose(adminRole)); } @@ -471,6 +524,8 @@ module ShieldedAccessControl { * @return {Boolean} roleGranted - A boolean indicating if `roleId` was granted. */ export circuit _grantRole(roleId: RoleIdentifier, accountId: AccountIdentifier): Boolean { + Initializable_assertInitialized(); + const role = computeRole(roleId, accountId); if (role.hasRole) { return false; @@ -509,6 +564,8 @@ module ShieldedAccessControl { * @return {Boolean} roleRevoked - A boolean indicating if `roleId` was revoked. */ export circuit _revokeRole(roleId: RoleIdentifier, accountId: AccountIdentifier): Boolean { + Initializable_assertInitialized(); + const role = computeRole(roleId, accountId); if (!role.hasRole) { return false; diff --git a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact index ff4ffe9a..d5c4e30d 100644 --- a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact +++ b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact @@ -16,10 +16,23 @@ export { ZswapCoinPublicKey, ShieldedAccessControl__roleCommitmentNullifiers, ShieldedAccessControl_Role }; -export pure circuit _computeRoleCommitment( - roleId: ShieldedAccessControl_RoleIdentifier, - accountId: ShieldedAccessControl_AccountIdentifier - ): ShieldedAccessControl_RoleCommitment { +/** + * @description `isInit` is a param for testing. + * + * If `isInit` is false, the constructor will not initialize the contract. + * This behavior is to test that circuits are not callable unless the + * contract is initialized. +*/ +constructor(instanceSalt: Bytes<32>, isInit: Boolean) { + if (disclose(isInit)) { + ShieldedAccessControl_initialize(instanceSalt); + } +} + +export circuit _computeRoleCommitment( + roleId: ShieldedAccessControl_RoleIdentifier, + accountId: ShieldedAccessControl_AccountIdentifier + ): ShieldedAccessControl_RoleCommitment { return ShieldedAccessControl__computeRoleCommitment(roleId, accountId); } From 01153f0c332f41372ee1816d9c5cebd0c0a6b59e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Fri, 27 Feb 2026 14:27:25 -0500 Subject: [PATCH 218/322] Refactor sim --- .../simulators/ShieldedAccessControlSimulator.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts index 574de03a..72fda901 100644 --- a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts +++ b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts @@ -18,7 +18,10 @@ import { /** * Type constructor args */ -type ShieldedAccessControlArgs = readonly []; +type ShieldedAccessControlArgs = readonly [ + instanceSalt: Uint8Array, + isInit: boolean, +]; const ShieldedAccessControlSimulatorBase = createSimulator< ShieldedAccessControlPrivateState, @@ -30,8 +33,8 @@ const ShieldedAccessControlSimulatorBase = createSimulator< contractFactory: (witnesses) => new MockShieldedAccessControl(witnesses), defaultPrivateState: () => ShieldedAccessControlPrivateState.generate(), - contractArgs: () => { - return []; + contractArgs: (instanceSalt, isInit) => { + return [instanceSalt, isInit]; }, ledgerExtractor: (state) => ledger(state), witnessesFactory: () => ShieldedAccessControlWitnesses(), @@ -42,19 +45,21 @@ const ShieldedAccessControlSimulatorBase = createSimulator< */ export class ShieldedAccessControlSimulator extends ShieldedAccessControlSimulatorBase { constructor( + instanceSalt: Uint8Array, + isInit: boolean, options: BaseSimulatorOptions< ShieldedAccessControlPrivateState, ReturnType > = {}, ) { - super([], options); + super([instanceSalt, isInit], options); } public _computeRoleCommitment( roleId: Uint8Array, accountId: Uint8Array, ): Uint8Array { - return this.circuits.pure._computeRoleCommitment(roleId, accountId); + return this.circuits.impure._computeRoleCommitment(roleId, accountId); } public _computeAccountId( From 1a3bb3def87e07c7553f7eaa151129c9b9846607 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Fri, 27 Feb 2026 18:09:57 -0500 Subject: [PATCH 219/322] improve naming --- contracts/src/access/ShieldedAccessControl.compact | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index fb7d01fc..c822cb7a 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -333,9 +333,9 @@ module ShieldedAccessControl { export circuit computeRole(roleId: RoleIdentifier, accountId: AccountIdentifier): Role { Initializable_assertInitialized(); - const commitment = _computeRoleCommitment(roleId, accountId); - const roleNullifier = _computeNullifier(commitment); - const roleCommitmentPath = wit_getRoleCommitmentPath(commitment); + const roleCommitment = _computeRoleCommitment(roleId, accountId); + const roleNullifier = _computeNullifier(roleCommitment); + const roleCommitmentPath = wit_getRoleCommitmentPath(roleCommitment); const rootMatches = _operatorRoles.checkRoot( merkleTreePathRoot<10, RoleCommitment>(disclose(roleCommitmentPath)) @@ -343,11 +343,11 @@ module ShieldedAccessControl { if (!_roleCommitmentNullifiers.member(disclose(roleNullifier)) && rootMatches) { return Role { hasRole: true, - roleCommitment: disclose(commitment), + roleCommitment: disclose(roleCommitment), roleNullifier: disclose(roleNullifier) }; } else { return Role { hasRole: false, - roleCommitment: disclose(commitment), + roleCommitment: disclose(roleCommitment), roleNullifier: disclose(roleNullifier) }; } } From bf99e94d7c33790bab2269f8b7dc965a63b431d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Fri, 27 Feb 2026 18:10:21 -0500 Subject: [PATCH 220/322] WIP refactor tests --- .../access/test/ShieldedAccessControl.test.ts | 499 +++++++++--------- 1 file changed, 258 insertions(+), 241 deletions(-) diff --git a/contracts/src/access/test/ShieldedAccessControl.test.ts b/contracts/src/access/test/ShieldedAccessControl.test.ts index ff1fc48a..a00964fb 100644 --- a/contracts/src/access/test/ShieldedAccessControl.test.ts +++ b/contracts/src/access/test/ShieldedAccessControl.test.ts @@ -1,21 +1,17 @@ import { - CompactTypeBytes, - CompactTypeVector, convertFieldToBytes, MerkleTreePath, - persistentHash, } from '@midnight-ntwrk/compact-runtime'; import { beforeEach, describe, expect, it } from 'vitest'; import { ShieldedAccessControl_Role as Role, ZswapCoinPublicKey, } from '../../../artifacts/MockShieldedAccessControl/contract/index.js'; -import { ShieldedAccessControlPrivateState, ShieldedAccessControlWitnesses } from '../witnesses/ShieldedAccessControlWitnesses.js'; +import { ShieldedAccessControlPrivateState } from '../witnesses/ShieldedAccessControlWitnesses.js'; import { ShieldedAccessControlSimulator } from './simulators/ShieldedAccessControlSimulator.js'; import * as utils from '#test-utils/address.js'; -const COMMITMENT_DOMAIN = "ShieldedAccessControl:commitment"; -const NULLIFIER_DOMAIN = "ShieldedAccessControl:nullifier"; +const INSTANCE_SALT = new Uint8Array(32).fill(48473095); const DEFAULT_MT_PATH: MerkleTreePath = { leaf: new Uint8Array(32), @@ -25,67 +21,26 @@ const DEFAULT_MT_PATH: MerkleTreePath = { })), }; -// Helpers -const buildCommitment = ( - roleId: Uint8Array, - accountId: Uint8Array -): Uint8Array => { - const rt_type = new CompactTypeVector(3, new CompactTypeBytes(32)); - const bDomain = new TextEncoder().encode(COMMITMENT_DOMAIN); - - const commitment = persistentHash(rt_type, [ - roleId, - accountId, - bDomain, - ]); - - return commitment; -}; - -const buildNullifier = ( - roleCommitment: Uint8Array, -): Uint8Array => { - const rt_type = new CompactTypeVector(2, new CompactTypeBytes(32)); - const bDomain = new TextEncoder().encode(NULLIFIER_DOMAIN); - - const nullifier = persistentHash(rt_type, [ - roleCommitment, - bDomain, - ]); - - return nullifier; -}; - -const buildAccountId = ( - pk: ZswapCoinPublicKey, - nonce: Uint8Array, -): Uint8Array => { - const rt_type = new CompactTypeVector(2, new CompactTypeBytes(32)); - - const bPK = pk.bytes; - return persistentHash(rt_type, [bPK, nonce]); -}; - class ShieldedAccessControlConstant { publicKey: string; zPublicKey: ZswapCoinPublicKey; - roleId: Uint8Array; + roleId: Buffer; accountId: Uint8Array; roleNullifier: Uint8Array; roleCommitment: Uint8Array; secretNonce: Buffer; + shieldedAccessControl = new ShieldedAccessControlSimulator(INSTANCE_SALT, true); constructor(baseString: string, roleIdentifier: bigint) { [this.publicKey, this.zPublicKey] = utils.generatePubKeyPair(baseString); this.secretNonce = Buffer.alloc(32, baseString + "_NONCE"); - this.accountId = buildAccountId(this.zPublicKey, this.secretNonce); - this.roleId = convertFieldToBytes(32, roleIdentifier, ''); - this.roleCommitment = buildCommitment(this.roleId, this.accountId); - this.roleNullifier = buildNullifier(this.roleCommitment); + this.accountId = this.shieldedAccessControl._computeAccountId(utils.createEitherTestUser(baseString), this.secretNonce); + this.roleId = Buffer.from(convertFieldToBytes(32, roleIdentifier, '')); + this.roleCommitment = this.shieldedAccessControl._computeRoleCommitment(this.roleId, this.accountId); + this.roleNullifier = this.shieldedAccessControl._computeNullifier(this.roleCommitment); }; } - // PKs const ADMIN = new ShieldedAccessControlConstant('ADMIN', 0n); const OPERATOR_1 = new ShieldedAccessControlConstant('OPERATOR_1', 1n); @@ -99,223 +54,285 @@ let shieldedAccessControl: ShieldedAccessControlSimulator; describe('ShieldedAccessControl', () => { - beforeEach(() => { - // Create private state object and generate nonce - const PS = ShieldedAccessControlPrivateState.withRoleAndNonce( - Buffer.from(ADMIN.roleId), - ADMIN.secretNonce, - ); - // Init contract for user with PS - shieldedAccessControl = new ShieldedAccessControlSimulator({ - privateState: PS, - witnesses: ShieldedAccessControlWitnesses() - }); - }); + describe('when not initialized correctly', () => { + const isNotInit = false; - describe('checked circuits should fail for authorized caller with invalid witness values', () => { beforeEach(() => { - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); - shieldedAccessControl.as(ADMIN.publicKey); + shieldedAccessControl = new ShieldedAccessControlSimulator( + INSTANCE_SALT, + isNotInit, + ); }); + type FailingCircuits = [method: keyof ShieldedAccessControlSimulator, args: unknown[]]; + // Circuit calls should fail before the args are used + const circuitsToFail: FailingCircuits[] = [ + ['callerHasRole', [UNINITIALIZED.roleId]], + ['assertOnlyRole', [UNINITIALIZED.roleId]], + ['computeRole', [UNINITIALIZED.roleId, UNINITIALIZED.accountId]], + ['getRoleAdmin', [UNINITIALIZED.roleId]], + ['grantRole', [UNINITIALIZED.roleId, UNINITIALIZED.accountId]], + ['revokeRole', [UNINITIALIZED.roleId, UNINITIALIZED.accountId]], + ['renounceRole', [UNINITIALIZED.roleId, UNINITIALIZED.accountId]], + ['_setRoleAdmin', [UNINITIALIZED.roleId, UNINITIALIZED.roleId]], + ['_grantRole', [UNINITIALIZED.roleId, UNINITIALIZED.accountId]], + ['_revokeRole', [UNINITIALIZED.roleId, UNINITIALIZED.accountId]], + ['_computeRoleCommitment', [UNINITIALIZED.roleId, UNINITIALIZED.accountId]], - type FailingCircuits = [ - method: keyof ShieldedAccessControlSimulator, - isValidNonce: boolean, - isValidPath: boolean, - args: unknown[], - ]; - const checkedCircuits: FailingCircuits[] = [ - ['assertOnlyRole', false, true, [ADMIN.roleId]], - ['assertOnlyRole', true, false, [ADMIN.roleId]], - ['assertOnlyRole', false, false, [ADMIN.roleId]], - ['grantRole', false, true, [ADMIN.roleId, ADMIN.accountId]], - ['grantRole', true, false, [ADMIN.roleId, ADMIN.accountId]], - ['grantRole', false, false, [ADMIN.roleId, ADMIN.accountId]], - ['revokeRole', true, false, [ADMIN.roleId, ADMIN.accountId]], - ['revokeRole', false, true, [ADMIN.roleId, ADMIN.accountId]], - ['revokeRole', false, false, [ADMIN.roleId, ADMIN.accountId]], ]; - - it.each(checkedCircuits)( - '%s should fail with isValidNonce(%s), isValidPath(%s)', - (circuitName, isValidNonce, isValidPath, args) => { - if (isValidNonce) { - // Check nonce matches - expect( - shieldedAccessControl.privateState.getCurrentSecretNonce( - ADMIN.roleId, - ), - ).toEqual(ADMIN.secretNonce); - } else { - // Check nonce does not match - shieldedAccessControl.privateState.injectSecretNonce( - ADMIN.roleId, - UNAUTHORIZED.secretNonce, - ); - expect( - shieldedAccessControl.privateState.getCurrentSecretNonce( - ADMIN.roleId, - ), - ).not.toEqual(ADMIN.secretNonce); - } - - if (isValidPath) { - // Check path matches - const truePath = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.findPathForLeaf( - ADMIN.roleCommitment, - ); - const [, witnessCalculatedPath] = - shieldedAccessControl.witnesses.wit_getRoleCommitmentPath( - shieldedAccessControl.getWitnessContext(), - ADMIN.roleCommitment, - ); - expect(witnessCalculatedPath).toEqual(truePath); - } else { - // Check path does not match - const truePath = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.findPathForLeaf( - ADMIN.roleCommitment, - ); - shieldedAccessControl.overrideWitness('wit_getRoleCommitmentPath', (ctx) => { - return [ctx.privateState, DEFAULT_MT_PATH]; - }); - const [, witnessCalculatedPath] = shieldedAccessControl.witnesses.wit_getRoleCommitmentPath(shieldedAccessControl.getWitnessContext(), BAD_INPUT.roleCommitment) - expect(witnessCalculatedPath).not.toEqual(truePath); - } - - // Test protected circuit - expect(() => { - ( - shieldedAccessControl[circuitName] as ( - ...args: unknown[] - ) => unknown - )(...args); - }).toThrow('ShieldedAccessControl: unauthorized account'); - }, - ); - }); - - describe('_computeRoleCommitment', () => { - it('computed commitment should match', () => { - expect(shieldedAccessControl._computeRoleCommitment(ADMIN.roleId, ADMIN.accountId)).toEqual(ADMIN.roleCommitment); + it.each(circuitsToFail)('%s should fail', (circuitName, args) => { + expect(() => { + (shieldedAccessControl[circuitName] as (...args: unknown[]) => unknown)(...args); + }).toThrow('Initializable: contract not initialized'); }); - type ComputeRoleCommitmentCases = [ - method: keyof ShieldedAccessControlSimulator, - isValidId: boolean, - isValidRole: boolean, - args: unknown[], - ]; - - const checkedCircuits: ComputeRoleCommitmentCases[] = [ - ['_computeRoleCommitment', false, true, [BAD_INPUT.roleId, ADMIN.accountId]], - ['_computeRoleCommitment', true, false, [ADMIN.roleId, BAD_INPUT.accountId]], - ['_computeRoleCommitment', false, false, [BAD_INPUT.roleId, BAD_INPUT.accountId]], - ] + it('should allow pure computeAccountId', () => { + const eitherAdmin = utils.createEitherTestUser('ADMIN'); - it.each(checkedCircuits)( - '%s should not match with isValidNonce(%s), isValidPath(%s)', - (circuitName, isValidId, isValidRole, args) => { - // Test protected circuit - expect(() => { - ( - shieldedAccessControl[circuitName] as ( - ...args: unknown[] - ) => unknown - )(...args); - }).not.toEqual(ADMIN); - } - ) - }); - - describe('_computeNullifier', () => { - it('should match nullifier', () => { - expect(shieldedAccessControl._computeNullifier(ADMIN.roleCommitment)).toEqual(ADMIN.roleNullifier); + expect(() => { + shieldedAccessControl._computeAccountId(eitherAdmin, ADMIN.secretNonce); + }).not.toThrow(); }); - it('should not match bad commitment inputs', () => { - expect(shieldedAccessControl._computeNullifier(BAD_INPUT.roleCommitment)).not.toEqual(ADMIN.roleNullifier); + it('should allow pure computeNullifier', () => { + expect(() => { + shieldedAccessControl._computeNullifier(ADMIN.roleCommitment); + }).not.toThrow(); }); }); - describe('_computeAccountId', () => { - const eitherAdmin = utils.createEitherTestUser('ADMIN'); - const eitherUnauthorized = utils.createEitherTestUser('UNAUTHORIZED'); + describe('after initialization', () => { + const isInit = true; - it('should match role id', () => { - expect(shieldedAccessControl._computeAccountId(eitherAdmin, ADMIN.secretNonce)).toEqual(ADMIN.accountId); + beforeEach(() => { + // Create private state object and generate nonce + const PS = ShieldedAccessControlPrivateState.withRoleAndNonce(ADMIN.roleId, ADMIN.secretNonce); + // Deploy contract with derived owner commitment and PS + shieldedAccessControl = new ShieldedAccessControlSimulator(INSTANCE_SALT, isInit, { + privateState: PS, + }); }); - it('should fail for contract address', () => { - const eitherContract = utils.createEitherTestContractAddress('CONTRACT') - expect(() => { - shieldedAccessControl._computeAccountId(eitherContract, ADMIN.secretNonce); - }).toThrow('ShieldedAccessControl: contract address roles are not yet supported'); + describe('checked circuits should fail for authorized caller with invalid witness values', () => { + beforeEach(() => { + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl.as(ADMIN.publicKey); + }); + + type FailingCircuits = [ + method: keyof ShieldedAccessControlSimulator, + isValidNonce: boolean, + isValidPath: boolean, + args: unknown[], + ]; + const checkedCircuits: FailingCircuits[] = [ + ['assertOnlyRole', false, true, [ADMIN.roleId]], + ['assertOnlyRole', true, false, [ADMIN.roleId]], + ['assertOnlyRole', false, false, [ADMIN.roleId]], + ['grantRole', false, true, [ADMIN.roleId, ADMIN.accountId]], + ['grantRole', true, false, [ADMIN.roleId, ADMIN.accountId]], + ['grantRole', false, false, [ADMIN.roleId, ADMIN.accountId]], + ['revokeRole', true, false, [ADMIN.roleId, ADMIN.accountId]], + ['revokeRole', false, true, [ADMIN.roleId, ADMIN.accountId]], + ['revokeRole', false, false, [ADMIN.roleId, ADMIN.accountId]], + ]; + + it.each(checkedCircuits)( + '%s should fail with isValidNonce(%s), isValidPath(%s)', + (circuitName, isValidNonce, isValidPath, args) => { + + if (isValidPath) { + // Check path matches + const truePath = shieldedAccessControl.privateState.getPathWithFindForLeaf(ADMIN.roleCommitment); + const witnessCalculatedPath = shieldedAccessControl.privateState.getPathWithWitnessImpl(ADMIN.roleCommitment); + expect(witnessCalculatedPath).toEqual(truePath); + } else { + // Check path does not match + const truePath = shieldedAccessControl.privateState.getPathWithFindForLeaf(ADMIN.roleCommitment); + + shieldedAccessControl.overrideWitness('wit_getRoleCommitmentPath', (ctx) => { + return [ctx.privateState, DEFAULT_MT_PATH]; + }); + const witnessCalculatedPath = shieldedAccessControl.privateState.getPathWithWitnessImpl(ADMIN.roleCommitment); + + expect(witnessCalculatedPath).not.toEqual(truePath); + } + + if (isValidNonce) { + // Check nonce matches + expect( + shieldedAccessControl.privateState.getCurrentSecretNonce( + ADMIN.roleId, + ), + ).toEqual(ADMIN.secretNonce); + } else { + // Check nonce does not match + shieldedAccessControl.privateState.injectSecretNonce( + ADMIN.roleId, + UNAUTHORIZED.secretNonce, + ); + expect( + shieldedAccessControl.privateState.getCurrentSecretNonce( + ADMIN.roleId, + ), + ).not.toEqual(ADMIN.secretNonce); + } + + // Test protected circuit + expect(() => { + ( + shieldedAccessControl[circuitName] as ( + ...args: unknown[] + ) => unknown + )(...args); + }).toThrow('ShieldedAccessControl: unauthorized account'); + }, + ); }); - type ComputeRoleIdCases = [ - method: keyof ShieldedAccessControlSimulator, - isValidAccount: boolean, - isValidNonce: boolean, - args: unknown[], - ]; - - const checkedCircuits: ComputeRoleIdCases[] = [ - ['_computeAccountId', true, false, [eitherAdmin, UNAUTHORIZED.secretNonce]], - ['_computeAccountId', false, true, [eitherUnauthorized, ADMIN.secretNonce]], - ['_computeAccountId', false, false, [eitherUnauthorized, UNAUTHORIZED.secretNonce]], - ]; - - it.each(checkedCircuits)( - '%s should not match role id with invalidAccount=%s or invalidNonce=%s', - (circuitName, isValidAccount, isValidNonce, args) => { - // Test circuit - expect(() => { - ( - shieldedAccessControl[circuitName] as ( - ...args: unknown[] - ) => unknown - )(...args); - }).not.toEqual(ADMIN.accountId); - } - ) - }); + describe('_computeRoleCommitment', () => { + it('should match computed commitment', () => { + expect(shieldedAccessControl._computeRoleCommitment(ADMIN.roleId, ADMIN.accountId)).toEqual(ADMIN.roleCommitment); + }); + + type ComputeRoleCommitmentCases = [ + method: keyof ShieldedAccessControlSimulator, + isValidId: boolean, + isValidRole: boolean, + args: unknown[], + ]; + + const checkedCircuits: ComputeRoleCommitmentCases[] = [ + ['_computeRoleCommitment', false, true, [BAD_INPUT.roleId, ADMIN.accountId]], + ['_computeRoleCommitment', true, false, [ADMIN.roleId, BAD_INPUT.accountId]], + ['_computeRoleCommitment', false, false, [BAD_INPUT.roleId, BAD_INPUT.accountId]], + ] + + it.each(checkedCircuits)( + '%s should not recompute commitment with isValidRoleId(%s), isValidAccountId(%s)', + (circuitName, isValidRoleId, isValidAccountId, args) => { + // Test protected circuit + expect(() => { + ( + shieldedAccessControl[circuitName] as ( + ...args: unknown[] + ) => unknown + )(...args); + }).not.toEqual(ADMIN.roleCommitment); + } + ) + }); + describe('_computeNullifier', () => { + it('should match nullifier', () => { + expect(shieldedAccessControl._computeNullifier(ADMIN.roleCommitment)).toEqual(ADMIN.roleNullifier); + }); - describe('computeRole', () => { - beforeEach(() => { - shieldedAccessControl.as(ADMIN.publicKey); + it('should not match bad commitment inputs', () => { + expect(shieldedAccessControl._computeNullifier(BAD_INPUT.roleCommitment)).not.toEqual(ADMIN.roleNullifier); + }); }); - it('should return unapproved if role does not exist', () => { - expect(shieldedAccessControl.computeRole(UNINITIALIZED.roleId, ADMIN.accountId).hasRole).toBe(false); - }); + describe('_computeAccountId', () => { + const eitherAdmin = utils.createEitherTestUser('ADMIN'); + const eitherUnauthorized = utils.createEitherTestUser('UNAUTHORIZED'); - it('should return correct commitment', () => { - expect(shieldedAccessControl.computeRole(ADMIN.roleId, ADMIN.accountId).roleCommitment).toEqual(ADMIN.roleCommitment); + it('should match role id', () => { + expect(shieldedAccessControl._computeAccountId(eitherAdmin, ADMIN.secretNonce)).toEqual(ADMIN.accountId); + }); + + it('should fail for contract address', () => { + const eitherContract = utils.createEitherTestContractAddress('CONTRACT') + expect(() => { + shieldedAccessControl._computeAccountId(eitherContract, ADMIN.secretNonce); + }).toThrow('ShieldedAccessControl: contract address roles are not yet supported'); + }); + + type ComputeRoleIdCases = [ + method: keyof ShieldedAccessControlSimulator, + isValidAccount: boolean, + isValidNonce: boolean, + args: unknown[], + ]; + + const checkedCircuits: ComputeRoleIdCases[] = [ + ['_computeAccountId', true, false, [eitherAdmin, UNAUTHORIZED.secretNonce]], + ['_computeAccountId', false, true, [eitherUnauthorized, ADMIN.secretNonce]], + ['_computeAccountId', false, false, [eitherUnauthorized, UNAUTHORIZED.secretNonce]], + ]; + + it.each(checkedCircuits)( + '%s should not match role id with invalidAccount=%s or invalidNonce=%s', + (circuitName, isValidAccount, isValidNonce, args) => { + // Test circuit + expect(() => { + ( + shieldedAccessControl[circuitName] as ( + ...args: unknown[] + ) => unknown + )(...args); + }).not.toEqual(ADMIN.accountId); + } + ) }); - it('should return correct nullifier', () => { - expect(shieldedAccessControl.computeRole(ADMIN.roleId, ADMIN.accountId).roleNullifier).toEqual(ADMIN.roleNullifier); + describe('computeRole', () => { + beforeEach(() => { + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId) + shieldedAccessControl.as(ADMIN.publicKey); + }); + + it('hasRole should return false if role does not exist', () => { + expect(shieldedAccessControl.computeRole(UNINITIALIZED.roleId, ADMIN.accountId).hasRole).toBe(false); + }); + + it('hasRole should return true for granted role', () => { + expect(shieldedAccessControl.computeRole(ADMIN.roleId, ADMIN.accountId).hasRole).toBe(true); + }); + + it('hasRole should return false for revoked role', () => { + shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + expect(shieldedAccessControl.computeRole(ADMIN.roleId, ADMIN.accountId).hasRole).toBe(false); + }); + + it('hasRole should return false when revoked role is re-granted', () => { + shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + expect(shieldedAccessControl.computeRole(ADMIN.roleId, ADMIN.accountId).hasRole).toBe(false); + }); + + it('should return correct commitment', () => { + expect(shieldedAccessControl.computeRole(ADMIN.roleId, ADMIN.accountId).roleCommitment).toEqual(ADMIN.roleCommitment); + }); + + it('should return correct nullifier', () => { + expect(shieldedAccessControl.computeRole(ADMIN.roleId, ADMIN.accountId).roleNullifier).toEqual(ADMIN.roleNullifier); + }); }); - it('should return approved role', () => { - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); - expect(shieldedAccessControl.computeRole(ADMIN.roleId, ADMIN.accountId).hasRole).toBe(true); + describe('_grantRole', () => { + it('should return true for new role', () => { + expect(shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId)).toBe(true); + }); + + it('should return false if role already granted', () => { + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + expect(shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId)).toBe(false); + }); + + it('should update Merkle tree root', () => { + const initialMtRoot = shieldedAccessControl.getPublicState().ShieldedAccessControl__operatorRoles.root(); + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + const updatedMtRoot = shieldedAccessControl.getPublicState().ShieldedAccessControl__operatorRoles.root(); + expect(initialMtRoot).not.toEqual(updatedMtRoot); + }); + + it('path for role commitment should exist', () => { + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + const path = shieldedAccessControl.privateState.getPathWithFindForLeaf(ADMIN.roleCommitment); + expect(path).not.toBe(undefined); + }); }); }); - describe('_grantRole', () => { - it('should return true for new role', () => { - expect(shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId)).toBe(true); - }); - it('should return false if role already granted', () => { - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); - expect(shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId)).toBe(false); - }); - }); }); \ No newline at end of file From 16b0ecde3d9d0e427ee1b972e5abb354339af264 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Fri, 27 Feb 2026 18:10:44 -0500 Subject: [PATCH 221/322] Add helper methods to sim --- .../test/simulators/ShieldedAccessControlSimulator.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts index 72fda901..4f3147a6 100644 --- a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts +++ b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts @@ -14,6 +14,7 @@ import { ShieldedAccessControlPrivateState, ShieldedAccessControlWitnesses, } from '../../witnesses/ShieldedAccessControlWitnesses.js'; +import { MerkleTreePath } from '@midnight-ntwrk/compact-runtime'; /** * Type constructor args @@ -202,5 +203,11 @@ export class ShieldedAccessControlSimulator extends ShieldedAccessControlSimulat roleString ]; }, + getPathWithFindForLeaf: (roleCommitment: Uint8Array): MerkleTreePath | undefined => { + return this.getPublicState().ShieldedAccessControl__operatorRoles.findPathForLeaf(roleCommitment); + }, + getPathWithWitnessImpl: (roleCommitment: Uint8Array): MerkleTreePath => { + return this.witnesses.wit_getRoleCommitmentPath(this.getWitnessContext(), roleCommitment)[1]; + } }; } From 5ddbe1e2bd4dca2616835331835b50a949e0e54d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Fri, 27 Feb 2026 20:49:59 -0500 Subject: [PATCH 222/322] Enforce single use role commitments --- .../src/access/ShieldedAccessControl.compact | 32 ++++++++----------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index c822cb7a..b321f0c9 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -513,21 +513,24 @@ module ShieldedAccessControl { * * Requirements: * + * - A role commitment not exist for the `roleId` | `accountId` pairing. + * - A role nullifier must not exist for role commitment. * * Disclosures: * * - The role commitment produced by SHA256(roleId | accountId | instanceSalt | commitmentDomain ). * - The role nullifier produced by SHA256(roleCommitment | nullifierDomain ). + * - The Merkle tree path for the role commitment stored in the `_operatorRoles` Merkle tree. * * @param {Bytes<32>} roleId - The role identifier. * @param {Bytes<32>} accountId - The account identifier. - * @return {Boolean} roleGranted - A boolean indicating if `roleId` was granted. + * @return {Boolean} isGranted - A boolean indicating if `roleId` was granted to `accountId`. */ export circuit _grantRole(roleId: RoleIdentifier, accountId: AccountIdentifier): Boolean { Initializable_assertInitialized(); const role = computeRole(roleId, accountId); - if (role.hasRole) { + if (role.hasRole || _roleCommitmentNullifiers.member(disclose(role.roleNullifier))) { return false; } @@ -536,38 +539,31 @@ module ShieldedAccessControl { } /** - * @description Attempts to revoke `roleId` from `account` and returns a boolean indicating if `roleId` was revoked. + * @description Attempts to revoke `roleId` from `accountId` and returns a boolean indicating if `roleId` was revoked. * Internal circuit without access restriction. * * @circuitInfo k=17, rows=108916 * * Requirements: * - * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce) - * must exist in the `_roleCommitmentIndex` map. - * - A nullifier for the role commitment produced by SHA256(roleId | account | nonce) - * must not exist in the `_roleCommitmentNullifiers` set. - * - A path for the role commitment produced by SHA256(roleId | account | nonce) must - * exist at `index` in the `_operatorRoles` Merkle tree. + * - A role commitment must exist for the `roleId` | `accountId` pairing. + * - A role nullifier must not exist for role commitment. * * Disclosures: * - * - The intermediate role commitment produced by SHA256(roleId | account | nonce). - * - The role commitment produced by SHA256(roleId | account | nonce). - * - The Merkle tree path for the role commitment stored at `index` in the `_operatorRoles` - * Merkle tree. - * - The type data of `account` - a ZswapCoinPublicKey or ContractAddress. + * - The role commitment produced by SHA256(roleId | accountId | instanceSalt | commitmentDomain ). + * - The role nullifier produced by SHA256(roleCommitment | nullifierDomain). + * - The Merkle tree path for the role commitment stored in the `_operatorRoles` Merkle tree. * * @param {Bytes<32>} roleId - The role identifier. - * @param {Either} account - A ZswapCoinPublicKey or ContractAddress. - * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK) - * @return {Boolean} roleRevoked - A boolean indicating if `roleId` was revoked. + * @param {Bytes<32>} accountId - The account identifier. + * @return {Boolean} isRevoked - A boolean indicating if `roleId` was revoked for `accountId` */ export circuit _revokeRole(roleId: RoleIdentifier, accountId: AccountIdentifier): Boolean { Initializable_assertInitialized(); const role = computeRole(roleId, accountId); - if (!role.hasRole) { + if (!role.hasRole || _roleCommitmentNullifiers.member(disclose(role.roleNullifier))) { return false; } From f91d346db4f52517b9fffaef5a3fbe7f93cfd620 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Fri, 27 Feb 2026 20:50:18 -0500 Subject: [PATCH 223/322] WIP tests --- .../access/test/ShieldedAccessControl.test.ts | 241 ++++++++++++++++-- 1 file changed, 213 insertions(+), 28 deletions(-) diff --git a/contracts/src/access/test/ShieldedAccessControl.test.ts b/contracts/src/access/test/ShieldedAccessControl.test.ts index a00964fb..4c471a2d 100644 --- a/contracts/src/access/test/ShieldedAccessControl.test.ts +++ b/contracts/src/access/test/ShieldedAccessControl.test.ts @@ -1,9 +1,11 @@ import { convertFieldToBytes, MerkleTreePath, + WitnessContext, } from '@midnight-ntwrk/compact-runtime'; import { beforeEach, describe, expect, it } from 'vitest'; import { + Ledger, ShieldedAccessControl_Role as Role, ZswapCoinPublicKey, } from '../../../artifacts/MockShieldedAccessControl/contract/index.js'; @@ -21,6 +23,10 @@ const DEFAULT_MT_PATH: MerkleTreePath = { })), }; +const RETURN_BAD_PATH = (ctx: WitnessContext, commitment: Uint8Array): [ShieldedAccessControlPrivateState, MerkleTreePath] => { + return [ctx.privateState, DEFAULT_MT_PATH]; +} + class ShieldedAccessControlConstant { publicKey: string; zPublicKey: ZswapCoinPublicKey; @@ -112,19 +118,19 @@ describe('ShieldedAccessControl', () => { }); }); - describe('checked circuits should fail for authorized caller with invalid witness values', () => { + describe('circuits should fail for authorized caller with invalid witness values', () => { beforeEach(() => { shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); shieldedAccessControl.as(ADMIN.publicKey); }); - type FailingCircuits = [ + type CheckedCircuitCases = [ method: keyof ShieldedAccessControlSimulator, isValidNonce: boolean, isValidPath: boolean, args: unknown[], ]; - const checkedCircuits: FailingCircuits[] = [ + const checkedCircuits: CheckedCircuitCases[] = [ ['assertOnlyRole', false, true, [ADMIN.roleId]], ['assertOnlyRole', true, false, [ADMIN.roleId]], ['assertOnlyRole', false, false, [ADMIN.roleId]], @@ -137,7 +143,7 @@ describe('ShieldedAccessControl', () => { ]; it.each(checkedCircuits)( - '%s should fail with isValidNonce(%s), isValidPath(%s)', + '%s should fail with isValidNonce=%s, isValidPath=%s', (circuitName, isValidNonce, isValidPath, args) => { if (isValidPath) { @@ -149,9 +155,7 @@ describe('ShieldedAccessControl', () => { // Check path does not match const truePath = shieldedAccessControl.privateState.getPathWithFindForLeaf(ADMIN.roleCommitment); - shieldedAccessControl.overrideWitness('wit_getRoleCommitmentPath', (ctx) => { - return [ctx.privateState, DEFAULT_MT_PATH]; - }); + shieldedAccessControl.overrideWitness('wit_getRoleCommitmentPath', RETURN_BAD_PATH); const witnessCalculatedPath = shieldedAccessControl.privateState.getPathWithWitnessImpl(ADMIN.roleCommitment); expect(witnessCalculatedPath).not.toEqual(truePath); @@ -195,27 +199,26 @@ describe('ShieldedAccessControl', () => { }); type ComputeRoleCommitmentCases = [ - method: keyof ShieldedAccessControlSimulator, isValidId: boolean, isValidRole: boolean, args: unknown[], ]; const checkedCircuits: ComputeRoleCommitmentCases[] = [ - ['_computeRoleCommitment', false, true, [BAD_INPUT.roleId, ADMIN.accountId]], - ['_computeRoleCommitment', true, false, [ADMIN.roleId, BAD_INPUT.accountId]], - ['_computeRoleCommitment', false, false, [BAD_INPUT.roleId, BAD_INPUT.accountId]], + [false, true, [BAD_INPUT.roleId, ADMIN.accountId]], + [true, false, [ADMIN.roleId, BAD_INPUT.accountId]], + [false, false, [BAD_INPUT.roleId, BAD_INPUT.accountId]], ] it.each(checkedCircuits)( - '%s should not recompute commitment with isValidRoleId(%s), isValidAccountId(%s)', - (circuitName, isValidRoleId, isValidAccountId, args) => { + '%s should not compute commitment with isValidRoleId=%s, isValidAccountId=%s', + (isValidRoleId, isValidAccountId, args) => { // Test protected circuit expect(() => { ( - shieldedAccessControl[circuitName] as ( + shieldedAccessControl['_computeRoleCommitment'] as ( ...args: unknown[] - ) => unknown + ) => Uint8Array )(...args); }).not.toEqual(ADMIN.roleCommitment); } @@ -236,7 +239,7 @@ describe('ShieldedAccessControl', () => { const eitherAdmin = utils.createEitherTestUser('ADMIN'); const eitherUnauthorized = utils.createEitherTestUser('UNAUTHORIZED'); - it('should match role id', () => { + it('should match account id', () => { expect(shieldedAccessControl._computeAccountId(eitherAdmin, ADMIN.secretNonce)).toEqual(ADMIN.accountId); }); @@ -247,28 +250,27 @@ describe('ShieldedAccessControl', () => { }).toThrow('ShieldedAccessControl: contract address roles are not yet supported'); }); - type ComputeRoleIdCases = [ - method: keyof ShieldedAccessControlSimulator, + type ComputeAccountIdCases = [ isValidAccount: boolean, isValidNonce: boolean, args: unknown[], ]; - const checkedCircuits: ComputeRoleIdCases[] = [ - ['_computeAccountId', true, false, [eitherAdmin, UNAUTHORIZED.secretNonce]], - ['_computeAccountId', false, true, [eitherUnauthorized, ADMIN.secretNonce]], - ['_computeAccountId', false, false, [eitherUnauthorized, UNAUTHORIZED.secretNonce]], + const checkedCircuits: ComputeAccountIdCases[] = [ + [true, false, [eitherAdmin, UNAUTHORIZED.secretNonce]], + [false, true, [eitherUnauthorized, ADMIN.secretNonce]], + [false, false, [eitherUnauthorized, UNAUTHORIZED.secretNonce]], ]; it.each(checkedCircuits)( - '%s should not match role id with invalidAccount=%s or invalidNonce=%s', - (circuitName, isValidAccount, isValidNonce, args) => { + '%s should not match account id with invalidAccount=%s or invalidNonce=%s', + (isValidAccount, isValidNonce, args) => { // Test circuit expect(() => { ( - shieldedAccessControl[circuitName] as ( + shieldedAccessControl['_computeAccountId'] as ( ...args: unknown[] - ) => unknown + ) => Uint8Array )(...args); }).not.toEqual(ADMIN.accountId); } @@ -281,6 +283,59 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl.as(ADMIN.publicKey); }); + type ComputeRoleCases = [ + isBadRoleId: boolean, + isBadAccountId: boolean, + args: unknown[], + ]; + const checkedCircuits: ComputeRoleCases[] = [ + [false, true, [ADMIN.roleId, BAD_INPUT.accountId]], + [true, false, [BAD_INPUT.roleId, ADMIN.accountId]], + [false, false, [BAD_INPUT.roleId, BAD_INPUT.accountId]], + ]; + + it.each(checkedCircuits)( + 'hasRole should be false with isBadRoleId(%s), isBadAccountId(%s)', + (isBadRoleId, isBadAccountId, args) => { + // Test protected circuit + expect( + ( + shieldedAccessControl['computeRole'] as ( + ...args: unknown[] + ) => Role + )(...args).hasRole + ).toBe(false); + }, + ); + + it.each(checkedCircuits)( + 'should return incorrect roleCommitment with isBadRoleId(%s), isBadAccountId(%s)', + (isBadRoleId, isBadAccountId, args) => { + // Test protected circuit + expect( + ( + shieldedAccessControl['computeRole'] as ( + ...args: unknown[] + ) => Role + )(...args).roleCommitment + ).not.toEqual(ADMIN.roleCommitment); + }, + ); + + it.each(checkedCircuits)( + 'should return incorrect roleNullifier with isBadRoleId(%s), isBadAccountId(%s)', + (isBadRoleId, isBadAccountId, args) => { + // Test protected circuit + expect( + ( + shieldedAccessControl['computeRole'] as ( + ...args: unknown[] + ) => Role + )(...args).roleNullifier + ).not.toEqual(ADMIN.roleNullifier); + }, + ); + it('hasRole should return false if role does not exist', () => { expect(shieldedAccessControl.computeRole(UNINITIALIZED.roleId, ADMIN.accountId).hasRole).toBe(false); }); @@ -289,6 +344,17 @@ describe('ShieldedAccessControl', () => { expect(shieldedAccessControl.computeRole(ADMIN.roleId, ADMIN.accountId).hasRole).toBe(true); }); + it('hasRole should return true for accountId with multiple roles', () => { + shieldedAccessControl._grantRole(OPERATOR_1.roleId, ADMIN.accountId); + shieldedAccessControl._grantRole(OPERATOR_2.roleId, ADMIN.accountId); + shieldedAccessControl._grantRole(OPERATOR_3.roleId, ADMIN.accountId); + + expect(shieldedAccessControl.computeRole(ADMIN.roleId, ADMIN.accountId).hasRole).toBe(true); + expect(shieldedAccessControl.computeRole(OPERATOR_1.roleId, ADMIN.accountId).hasRole).toBe(true); + expect(shieldedAccessControl.computeRole(OPERATOR_2.roleId, ADMIN.accountId).hasRole).toBe(true); + expect(shieldedAccessControl.computeRole(OPERATOR_3.roleId, ADMIN.accountId).hasRole).toBe(true); + }); + it('hasRole should return false for revoked role', () => { shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); expect(shieldedAccessControl.computeRole(ADMIN.roleId, ADMIN.accountId).hasRole).toBe(false); @@ -300,6 +366,11 @@ describe('ShieldedAccessControl', () => { expect(shieldedAccessControl.computeRole(ADMIN.roleId, ADMIN.accountId).hasRole).toBe(false); }); + it('hasRole should return false for bad path', () => { + shieldedAccessControl.overrideWitness('wit_getRoleCommitmentPath', RETURN_BAD_PATH); + expect(shieldedAccessControl.computeRole(ADMIN.roleId, ADMIN.accountId).hasRole).toBe(false); + }); + it('should return correct commitment', () => { expect(shieldedAccessControl.computeRole(ADMIN.roleId, ADMIN.accountId).roleCommitment).toEqual(ADMIN.roleCommitment); }); @@ -309,14 +380,88 @@ describe('ShieldedAccessControl', () => { }); }); + describe('assertOnlyRole', () => { + beforeEach(() => { + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl.as(ADMIN.publicKey); + }); + + it('should not fail when authorized caller has correct nonce, and path', () => { + // Check nonce is correct + expect( + shieldedAccessControl.privateState.getCurrentSecretNonce( + ADMIN.roleId, + ), + ).toBe(ADMIN.secretNonce); + + // Check path matches + const truePath = shieldedAccessControl.privateState.getPathWithFindForLeaf(ADMIN.roleCommitment); + const witnessCalculatedPath = shieldedAccessControl.privateState.getPathWithWitnessImpl(ADMIN.roleCommitment); + expect(witnessCalculatedPath).toEqual(truePath); + + expect(() => + shieldedAccessControl.assertOnlyRole(ADMIN.roleId), + ).not.toThrow(); + }); + + it('should fail for revoked role', () => { + shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + expect(() => + shieldedAccessControl.assertOnlyRole(ADMIN.roleId), + ).toThrow("ShieldedAccessControl: unauthorized account"); + }); + + it('should fail for revoked role with re-approval', () => { + shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + expect(() => + shieldedAccessControl.assertOnlyRole(ADMIN.roleId), + ).toThrow("ShieldedAccessControl: unauthorized account"); + }); + + it('should not fail for admin with multiple roles', () => { + // shieldedAccessControl.privateState.injectSecretNonce( + // OPERATOR_ROLE_1, + // OPERATOR_ROLE_1_SECRET_NONCE, + // ); + // shieldedAccessControl.privateState.injectSecretNonce( + // OPERATOR_ROLE_2, + // OPERATOR_ROLE_2_SECRET_NONCE, + // ); + // shieldedAccessControl.privateState.injectSecretNonce( + // OPERATOR_ROLE_3, + // OPERATOR_ROLE_3_SECRET_NONCE, + // ); + // shieldedAccessControl._grantRole(OPERATOR_ROLE_1, Z_ADMIN); + // shieldedAccessControl._grantRole(OPERATOR_ROLE_2, Z_ADMIN); + // shieldedAccessControl._grantRole(OPERATOR_ROLE_3, Z_ADMIN); + // expect(() => { + // shieldedAccessControl.assertOnlyRole(DEFAULT_ADMIN_ROLE); + // shieldedAccessControl.assertOnlyRole(OPERATOR_ROLE_1); + // shieldedAccessControl.assertOnlyRole(OPERATOR_ROLE_2); + // shieldedAccessControl.assertOnlyRole(OPERATOR_ROLE_3); + // }).not.toThrow(); + }); + }); + describe('_grantRole', () => { - it('should return true for new role', () => { + it('should grant role', () => { expect(shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId)).toBe(true); }); - it('should return false if role already granted', () => { + it('should not re-grant role', () => { shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + const merkleRoot = shieldedAccessControl.getPublicState().ShieldedAccessControl__operatorRoles.root(); expect(shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId)).toBe(false); + expect(shieldedAccessControl.getPublicState().ShieldedAccessControl__operatorRoles.root()).toEqual(merkleRoot); + }); + + it('should not re-grant revoked role', () => { + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + expect(shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId)).toBe(false); + const merkleRoot = shieldedAccessControl.getPublicState().ShieldedAccessControl__operatorRoles.root(); + expect(shieldedAccessControl.getPublicState().ShieldedAccessControl__operatorRoles.root()).toEqual(merkleRoot); }); it('should update Merkle tree root', () => { @@ -332,6 +477,46 @@ describe('ShieldedAccessControl', () => { expect(path).not.toBe(undefined); }); }); + + describe('_revokeRole', () => { + it('should not revoke role that does not exist', () => { + expect(shieldedAccessControl._revokeRole(UNINITIALIZED.roleId, ADMIN.accountId)).toBe(false); + }); + + it('should not re-revoke role', () => { + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + expect(shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId)).toBe(false); + }); + + it('should revoke role', () => { + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + expect(shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId)).toBe(true); + }); + + it('should update nullifier set on revoke', () => { + const initialSize = shieldedAccessControl.getPublicState().ShieldedAccessControl__roleCommitmentNullifiers.size(); + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + const isRevoked = shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + expect(isRevoked).toBe(true); + + const updatedSize = shieldedAccessControl.getPublicState().ShieldedAccessControl__roleCommitmentNullifiers.size(); + const isEmpty = shieldedAccessControl.getPublicState().ShieldedAccessControl__roleCommitmentNullifiers.isEmpty(); + expect(initialSize).not.toEqual(updatedSize); + expect(isEmpty).toBe(false); + }); + + it('should not update nullifier set on failed revoke', () => { + const initialSize = shieldedAccessControl.getPublicState().ShieldedAccessControl__roleCommitmentNullifiers.size(); + const isRevoked = shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + expect(isRevoked).toBe(false); + + const updatedSize = shieldedAccessControl.getPublicState().ShieldedAccessControl__roleCommitmentNullifiers.size(); + const isEmpty = shieldedAccessControl.getPublicState().ShieldedAccessControl__roleCommitmentNullifiers.isEmpty(); + expect(initialSize).toEqual(updatedSize); + expect(isEmpty).toBe(true); + }); + }); }); From b54159382fba021ae0383969b9a8d3c5c6601124 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Sat, 28 Feb 2026 16:42:40 -0500 Subject: [PATCH 224/322] Refactor docs --- .../src/access/ShieldedAccessControl.compact | 248 +++++++++--------- 1 file changed, 129 insertions(+), 119 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index b321f0c9..d6d13715 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -6,7 +6,7 @@ pragma language_version >= 0.21.0; /** * @module Shielded AccessControl * @description A Shielded AccessControl library. - * This module provides a shielded role-based access control mechanism, where roles can be used to + * This module provides a shielded role-based access control (RBAC) mechanism, where roles can be used to * represent a set of permissions. Roles are stored as Merkle tree commitments to avoid * disclosing information about role holders. Role commitments are created with the following * hashing scheme SHA256(roleId | accountId | instanceSalt | commitmentDomain). Where @@ -16,6 +16,11 @@ pragma language_version >= 0.21.0; * - `instanceSalt` is an immutable, cryptographically strong random value provided on deployment * - `commitmentDomain` is the following zero padded 32 byte string: "ShieldedAccessControl:commitment" * + * In this RBAC model, role commitments behave like private bearer tokens. Possession of a valid, non-revoked role + * commitment grants authorization. Revocation permanently burns the role instance, requiring explicit new issuance + * under a new account identifier. Users must rotate their identity (new nonce) to be re-authorized. This creates + * stronger security invariants over traditional RBAC systems and enables privacy-preserving identity rotation. + * * @notice Using the SHA256 hashing function comes at a significant performance cost. In the future, we * plan on migrating to a ZK-friendly hashing function when an implementation is available. * @@ -59,6 +64,7 @@ pragma language_version >= 0.21.0; * * @dev Privacy Assumptions * - Outside observers will know when an admin is added and how many admins exist. + * - Outside observers will know which role identifiers are admin identifiers. * - Outside observers will have knowledge of all role identifiers. * - Outside observers will have knowledge of role additions and revocations. * - Outside observers will NOT be able to identify the public address of any role holder @@ -94,7 +100,8 @@ module ShieldedAccessControl { * @ledger _operatorRoles * @description A Merkle tree of role commitments stored as SHA256(roleId | accountId | instanceSalt | commitmentDomain) * Role commitments are derived from a public role identifier (e.g., `persistentHash>(pad(32, "MY_ROLE")`), - * an account identifier (e.g., `SHA256(pk, nonce)`), the `instanceSalt`, and a domain separator. + * an account identifier (e.g., `SHA256(zcpk, nonce)`), the `instanceSalt`, and a domain separator. + * @type {Bytes<32>} RoleCommitment - A role commitment created by the following hash: SHA256( roleId | accountId | instanceSalt | commitmentDomain).  */ export ledger _operatorRoles: MerkleTree<10, RoleCommitment>; @@ -106,11 +113,17 @@ module ShieldedAccessControl { /** * @description A set of nullifiers used to revoke the permissions of a role - * @type {Bytes<32> roleCommitment - A role commitment created by the following hash: SHA256(accountId | roleId | instanceSalt | "ShieldedAccessControl:commitment"). + * @type {Bytes<32>} RoleNullifier - A role nullifier created by the following hash: SHA256( roleCommitment | nullifierDomain). * @type {Set} _roleCommitmentNullifiers  */ export ledger _roleCommitmentNullifiers: Set; + /** + * @description The default admin role for all roles. Only accounts with this role will be able to grant or revoke other roles + * unless custom admin roles are created. + * + * @default 0x0000000000000000000000000000000000000000000000000000000000000000 +  */ export ledger DEFAULT_ADMIN_ROLE: Bytes<32>; /** @@ -124,18 +137,34 @@ module ShieldedAccessControl { export sealed ledger _instanceSalt: Bytes<32>; /** - * @description Returns a Merkle path in the `_operatorRoles` Merkle tree, given the knowledge that a `roleCommitment` is at the given index. + * @witness wit_getRoleCommitmentPath + * @description Returns a path to a role commitment in the `_operatorRoles` Merkle tree if one exists. Otherwise, returns an invalid path. * - * @param {Bytes<32>} roleCommitment - A commitment created by the following hash: SHA256(accountId | roleId | instanceSalt | "ShieldedAccessControl:commitment"). - * @param {Uint<64>} index - An index in the `_operatorRoles` Merkle tree - * @return {MerkleTreePath<10, Bytes<32>>} - The Merkle path of `roleCommitment` in the `_operatorRoles` Merkle tree + * @param {Bytes<32>} RoleCommitment - A commitment created by the following hash: SHA256( roleId | accountId | instanceSalt | commitmentDomain). + * @return {MerkleTreePath<10, Bytes<32>>} - The Merkle tree path to `roleCommitment` in the `_operatorRoles` Merkle tree  */ witness wit_getRoleCommitmentPath( roleCommitment: RoleCommitment ): MerkleTreePath<10, RoleCommitment>; + /** + * @witness wit_secretNonce + * @description A private per-accountId nonce used in deriving the shielded account identifier. + * + * Combined with the user's ZswapCoinPublicKey as `SHA256(zcpk, nonce)` to produce an obfuscated, + * unlinkable identity commitment. + * + * @param {Bytes<32>} roleId - The unique identifier of a role. + */ witness wit_secretNonce(roleId: RoleIdentifier): Bytes<32>; + /** + * @description A struct containing information for a particular `roleId` | `accountId` pairing. + * + * @type {Boolean} hasRole - A boolean flag indicating an `accountId` is authorized for a `roleId` + * @type {} roleCommitment - The role commitment for a particular `roleId` | `accountId` pairing. + * @type {} roleNullifier - The associated role nullifier for `roleCommitment`. + */ export struct Role { hasRole: Boolean; roleCommitment: RoleCommitment; @@ -175,14 +204,14 @@ module ShieldedAccessControl { * - `zcpk`: The account's ZswapCoinPublicKey. * - `nonce`: A secret nonce scoped to the role. * - * ## Commitment Derivation - * `commitment = SHA256(roleId, accountId, instanceSalt, domain)` + * ## Role Commitment Derivation + * `roleCommitment = SHA256(roleId, accountId, instanceSalt, commitmentDomain)` * * - `accountId`: See above. * - `roleId`: A unique role identifier. * - `instanceSalt`: A unique per-deployment salt, stored during initialization. * This prevents commitment collisions across deployments. - * - `domain`: Domain separator `"ShieldedAccessControl:commitment"` (padded to 32 bytes) to prevent + * - `commitmentDomain`: Domain separator `"ShieldedAccessControl:commitment"` (padded to 32 bytes) to prevent * hash collisions when extending the module or using similar commitment schemes. * * @circuitInfo k=14, rows=14853 @@ -210,6 +239,21 @@ module ShieldedAccessControl { as RoleCommitment; } + /** + * @description Computes the role nullifier for a given `roleCommitment`. + * + * ## Role Nullifier Derivation + * `roleNullifier = SHA256(roleCommitment, nullifierDomain)` + * + * - `roleCommitment`: See `_computeRoleCommitment`. + * - `nullifierDomain`: Domain separator `"ShieldedAccessControl:nullifier"` (padded to 32 bytes) to prevent + * hash collisions when extending the module or using similar commitment schemes. + * + * @circuitInfo k=14, rows=14853 + * + * @param {} roleCommitment - The role commitment for a particular `roleId` | `accountId` pairing. + * @returns {Bytes<32>} roleNullifier - The associated nullifier for `roleCommitment`. + */ export pure circuit _computeNullifier(roleCommitment: RoleCommitment): RoleNullifer { return persistentHash>>( [roleCommitment as Bytes<32>, pad(32, "ShieldedAccessControl:nullifier")] @@ -218,24 +262,23 @@ module ShieldedAccessControl { } /** - * @description Computes the unique identifier (`id`) of the owner from their - * public key and a secret nonce. + * @description Computes the unique identifier (`accountId`) of an account from their + * ZswapCoinPublicKey and a secret nonce. * * ## ID Derivation - * `id = SHA256(pk, nonce)` + * `accountId = SHA256(zcpk, nonce)` * - * - `pk`: The public key of the caller. This is passed explicitly to allow + * - `zcpk`: The ZswapCoinPublicKey of the caller. This is passed explicitly to allow * for off-chain derivation, testing, or scenarios where the caller is - * different from the subject of the computation. - * We recommend using an Air-Gapped Public Key. + * different from the subject of the computation. We recommend using an Air-Gapped Public Key. * - `nonce`: A secret nonce tied to the identity. The generation strategy is * left to the user, offering different security/convenience trade-offs. * - * The result is a 32-byte commitment that uniquely identifies the owner. - * This value is later used in owner commitment hashing, + * The result is a 32-byte commitment that uniquely identifies the account. + * This value is later used in role commitment hashing, * and acts as a privacy-preserving alternative to a raw public key. * - * @notice This module allows ownership to be tied to an identity commitment derived + * @notice This module allows access to be tied to an identity commitment derived * from a public key and secret nonce. * While typically used with user public keys, this mechanism may also * support contract addresses as identifiers in future contract-to-contract @@ -243,11 +286,11 @@ module ShieldedAccessControl { * * Requirements: * - * - `pk` is not a ContractAddress. + * - `zcpk` is not a ContractAddress. * - * @param {Either} pk - The public key of the identity being committed. + * @param {Either} zcpk - The public key of the identity being committed. * @param {Bytes<32>} nonce - A private nonce to scope the commitment. - * @returns {Bytes<32>} The computed account ID. + * @returns {Bytes<32>} accountId - The computed account ID. */ export pure circuit _computeAccountId( pk: ZcpkOrContractAddress, @@ -259,31 +302,18 @@ module ShieldedAccessControl { } /** - * @description Returns `true` if `account` has been granted `roleId`. + * @description Returns `true` if a caller is authorized for `roleId`. * * @circuitInfo k=16, rows=60150 * - * Requirements: - * - * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce) - * must exist in the `_roleCommitmentIndex` map. - * - A nullifier for the role commitment produced by SHA256(roleId | account | nonce) - * must not exist in the `_roleCommitmentNullifiers` set. - * - A path for the role commitment produced by SHA256(roleId | account | nonce) must - * exist at `index` in the `_operatorRoles` Merkle tree. - * * Disclosures: * - * - The intermediate role commitment produced by SHA256(roleId | account | nonce). - * - The role commitment produced by SHA256(roleId | account | nonce). - * - The Merkle tree path for the role commitment stored at `index` in the `_operatorRoles` - * Merkle tree. - * - The type data of `account` - a ZswapCoinPublicKey or ContractAddress. + * - The role commitment produced by SHA256(roleId | accountId | instanceSalt | commitmentDomain ). + * - The role nullifier produced by SHA256(roleCommitment | nullifierDomain). + * - The Merkle tree path for the role commitment stored in the `_operatorRoles` Merkle tree. * * @param {Bytes<32>} roleId - The role identifier. - * @param {Either} account - The account to check. - * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK). - * @return {Boolean} - A boolean determining if the account has the specified role. + * @return {Boolean} - A boolean determining if a caller has the specified role.  */ export circuit callerHasRole(roleId: RoleIdentifier): Boolean { Initializable_assertInitialized(); @@ -298,30 +328,21 @@ module ShieldedAccessControl { } /** - * @description Reverts if caller is missing `roleId`. + * @description Reverts if caller is not authorized for `roleId`. * * @circuitInfo k=15, rows=29780 * * Requirements: * - * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce) - * must exist in the `_roleCommitmentIndex` map. - * - A nullifier for the role commitment produced by SHA256(roleId | account | nonce) - * must not exist in the `_roleCommitmentNullifiers` set. - * - A path for the role commitment produced by SHA256(roleId | account | nonce) must - * exist at `index` in the `_operatorRoles` Merkle tree. - * - The caller must not be a ContractAddress. + * - caller must have authorization for `roleId`. * * Disclosures: * - * - The intermediate role commitment produced by SHA256(roleId | account | nonce). - * - The role commitment produced by SHA256(roleId | account | nonce). - * - The Merkle tree path for the role commitment stored at `index` in the `_operatorRoles` - * Merkle tree. - * - The type data of `account` - a ZswapCoinPublicKey or ContractAddress. + * - The role commitment produced by SHA256(roleId | accountId | instanceSalt | commitmentDomain ). + * - The role nullifier produced by SHA256(roleCommitment | nullifierDomain). + * - The Merkle tree path for the role commitment stored in the `_operatorRoles` Merkle tree. * * @param {Bytes<32>} roleId - The role identifier. - * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK) * @return {[]} - Empty tuple. */ export circuit assertOnlyRole(roleId: RoleIdentifier): [] { @@ -330,6 +351,24 @@ module ShieldedAccessControl { assert(callerHasRole(roleId), "ShieldedAccessControl: unauthorized account"); } + /** + * @description Computes the role commitment and associated role nullifier for a `roleId` | `accountId` + * pairing. A role commitment is valid for a `roleId` | `accountId` pairing if and only if a valid + * path exists in the `_operatorRoles` Merkle tree for the role commitment and a role nullifier doesn't + * exist for the role commitment in the `_roleNullifiers` set. + * + * @circuitInfo k=15, rows=29780 + * + * Disclosures: + * + * - The role commitment produced by SHA256(roleId | accountId | instanceSalt | commitmentDomain ). + * - The role nullifier produced by SHA256(roleCommitment | nullifierDomain). + * - The Merkle tree path for the role commitment stored in the `_operatorRoles` Merkle tree. + * + * @param {Bytes<32>} roleId - The role identifier. + * @param {Bytes<32>} accountId - The unique identifier of the account. + * @return {[]} - Empty tuple. + */ export circuit computeRole(roleId: RoleIdentifier, accountId: AccountIdentifier): Role { Initializable_assertInitialized(); @@ -353,8 +392,8 @@ module ShieldedAccessControl { } /** - * @description Returns the admin role that controls `roleId` or - * a byte array with all zero bytes if `roleId` doesn't exist. See {grantRole} and {revokeRole}. + * @description Returns the admin role that controls `roleId` or a zero + * byte array if `roleId` doesn't exist. See {grantRole} and {revokeRole}. * * To change a role’s admin use {_setRoleAdmin}. * @@ -373,31 +412,25 @@ module ShieldedAccessControl { } /** - * @description Grants `roleId` to `account`. - * - * @circuitInfo k=18, rows=138761 + * @description Grants `roleId` to `accountId` by inserting a role commitment unique to the + * `roleId` | `accountId` pairing into the `_operatorRoles` Merkle tree. `roleId` can only be + * granted to `accountId` once. A new `accountId` must be generated to be re-authorized for + * `roleId` * * Requirements: * - * - `account` must not be a ContractAddress. - * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce) - * must exist in the `_roleCommitmentIndex` map. - * - A nullifier for the role commitment produced by SHA256(roleId | account | nonce) - * must not exist in the `_roleCommitmentNullifiers` set. - * - A path for the role commitment produced by SHA256(roleId | account | nonce) must - * exist at `index` in the `_operatorRoles` Merkle tree. + * - caller must be admin for `roleId` + * + * @circuitInfo k=18, rows=138761 * * Disclosures: * - * - The intermediate role commitment produced by SHA256(roleId | account | nonce). - * - The role commitment produced by SHA256(roleId | account | nonce). - * - The Merkle tree path for the role commitment stored at `index` in the `_operatorRoles` - * Merkle tree. - * - The type data of `account` - a ZswapCoinPublicKey or ContractAddress. + * - The role commitment produced by SHA256(roleId | accountId | instanceSalt | commitmentDomain ). + * - The role nullifier produced by SHA256(roleCommitment | nullifierDomain). + * - The Merkle tree path for the role commitment stored in the `_operatorRoles` Merkle tree. * * @param {Bytes<32>} roleId - The role identifier. - * @param {Either} account - A ZswapCoinPublicKey or ContractAddress. - * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK) + * @param {Bytes<32>} accountId - The unique identifier of the account. * @return {[]} - Empty tuple. */ export circuit grantRole(roleId: RoleIdentifier, accountId: AccountIdentifier): [] { @@ -408,31 +441,26 @@ module ShieldedAccessControl { } /** - * @description Revokes `roleId` from `account`. + * @description Revokes `roleId` from `accountId` by inserting a role nullifier into the + * `_roleNullifiers` set. A `roleId` can only be revoked from `accountId` once to avoid duplicate + * insertions into the `_roleNullifiers` set. * * @circuitInfo k=18, rows=138517 * * Requirements: * - * - `account` must not be a ContractAddress. - * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce) - * must exist in the `_roleCommitmentIndex` map. - * - A nullifier for the role commitment produced by SHA256(roleId | account | nonce) - * must not exist in the `_roleCommitmentNullifiers` set. - * - A path for the role commitment produced by SHA256(roleId | account | nonce) must - * exist at `index` in the `_operatorRoles` Merkle tree. + * - caller must be admin for `roleId` + * + * @circuitInfo k=18, rows=138761 * * Disclosures: * - * - The intermediate role commitment produced by SHA256(roleId | account | nonce). - * - The role commitment produced by SHA256(roleId | account | nonce). - * - The Merkle tree path for the role commitment stored at `index` in the `_operatorRoles` - * Merkle tree. - * - The type data of `account` - a ZswapCoinPublicKey or ContractAddress. + * - The role commitment produced by SHA256(roleId | accountId | instanceSalt | commitmentDomain ). + * - The role nullifier produced by SHA256(roleCommitment | nullifierDomain). + * - The Merkle tree path for the role commitment stored in the `_operatorRoles` Merkle tree. * * @param {Bytes<32>} roleId - The role identifier. - * @param {Either} account - A ZswapCoinPublicKey or ContractAddress. - * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK) + * @param {Bytes<32>} accountId - The unique identifier of the account. * @return {[]} - Empty tuple. */ export circuit revokeRole(roleId: RoleIdentifier, accountId: AccountIdentifier): [] { @@ -453,26 +481,16 @@ module ShieldedAccessControl { * * Requirements: * - * - The caller must be `callerConfirmation`. - * - The caller must not be a `ContractAddress`. - * - An index for the intermediate role commitment produced by SHA256(roleId | account | nonce) - * must exist in the `_roleCommitmentIndex` map. - * - A nullifier for the role commitment produced by SHA256(roleId | account | nonce) - * must not exist in the `_roleCommitmentNullifiers` set. - * - A path for the role commitment produced by SHA256(roleId | account | nonce) must - * exist at `index` in the `_operatorRoles` Merkle tree. + * - The caller must provide a valid `accountId` for a `roleId` * * Disclosures: * - * - The intermediate role commitment produced by SHA256(roleId | account | nonce). - * - The role commitment produced by SHA256(roleId | account | nonce). - * - The Merkle tree path for the role commitment stored at `index` in the `_operatorRoles` - * Merkle tree. - * - The type data of `callerConfirmation` - a ZswapCoinPublicKey or ContractAddress. + * - The role commitment produced by SHA256(roleId | accountId | instanceSalt | commitmentDomain ). + * - The role nullifier produced by SHA256(roleCommitment | nullifierDomain). + * - The Merkle tree path for the role commitment stored in the `_operatorRoles` Merkle tree. * * @param {Bytes<32>} roleId - The role identifier. - * @param {Either} callerConfirmation - A ZswapCoinPublicKey or ContractAddress. - * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK) + * @param {Bytes<32>} accountId - The unique identifier of the account. * @return {[]} - Empty tuple. */ export circuit renounceRole(roleId: RoleIdentifier, accountIdConfirmation: AccountIdentifier): [] { @@ -491,31 +509,27 @@ module ShieldedAccessControl { } /** - * @description Sets `adminRole` as `roleId`'s admin role. + * @description Sets `adminID` as `roleId`'s admin identifier. Internal circuit without access restriction. * * @circuitInfo k=10, rows=209 * * @param {Bytes<32>} roleId - The role identifier. - * @param {Bytes<32>} adminRole - The admin role identifier. + * @param {Bytes<32>} adminId - The admin role identifier. * @return {[]} - Empty tuple. */ - export circuit _setRoleAdmin(roleId: RoleIdentifier, adminRole: AdminIdentifier): [] { + export circuit _setRoleAdmin(roleId: RoleIdentifier, adminId: AdminIdentifier): [] { Initializable_assertInitialized(); - _adminRoles.insert(disclose(roleId), disclose(adminRole)); + _adminRoles.insert(disclose(roleId), disclose(adminId)); } /** * @description Attempts to grant `roleId` to `accountId` and returns a boolean indicating if `roleId` was granted. - * Internal circuit without access restriction. + * Internal circuit without access restriction. Returns true if a role commitment doesn't exist for the + * `roleId` | `accountId` pairing and a role nullifier doesn't exist for the computed role commitment. * * @circuitInfo k=17, rows=109163 * - * Requirements: - * - * - A role commitment not exist for the `roleId` | `accountId` pairing. - * - A role nullifier must not exist for role commitment. - * * Disclosures: * * - The role commitment produced by SHA256(roleId | accountId | instanceSalt | commitmentDomain ). @@ -540,15 +554,11 @@ module ShieldedAccessControl { /** * @description Attempts to revoke `roleId` from `accountId` and returns a boolean indicating if `roleId` was revoked. - * Internal circuit without access restriction. + * Internal circuit without access restriction. Returns true if a role commitment exists for the + * `roleId` | `accountId` pairing and a role nullifier doesn't exist for the computed role commitment. * * @circuitInfo k=17, rows=108916 * - * Requirements: - * - * - A role commitment must exist for the `roleId` | `accountId` pairing. - * - A role nullifier must not exist for role commitment. - * * Disclosures: * * - The role commitment produced by SHA256(roleId | accountId | instanceSalt | commitmentDomain ). From 13d142ccdabb6dab14ec81bf07a2357bac413d7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Sun, 1 Mar 2026 14:37:27 -0500 Subject: [PATCH 225/322] move disclosure closer to disclosure point --- contracts/src/access/ShieldedAccessControl.compact | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index d6d13715..d8cc7928 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -382,12 +382,12 @@ module ShieldedAccessControl { if (!_roleCommitmentNullifiers.member(disclose(roleNullifier)) && rootMatches) { return Role { hasRole: true, - roleCommitment: disclose(roleCommitment), - roleNullifier: disclose(roleNullifier) }; + roleCommitment: roleCommitment, + roleNullifier: roleNullifier }; } else { return Role { hasRole: false, - roleCommitment: disclose(roleCommitment), - roleNullifier: disclose(roleNullifier) }; + roleCommitment: roleCommitment, + roleNullifier: roleNullifier }; } } @@ -548,7 +548,7 @@ module ShieldedAccessControl { return false; } - _operatorRoles.insert(role.roleCommitment); + _operatorRoles.insert(disclose(role.roleCommitment)); return true; } From a5268f6e26aed72bfbac7bb1b9ca2cbf69d35627 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Sun, 1 Mar 2026 21:00:29 -0500 Subject: [PATCH 226/322] Reduce metadata leakage --- .../src/access/ShieldedAccessControl.compact | 165 +++++++++++------- .../mocks/MockShieldedAccessControl.compact | 94 ++++++++-- 2 files changed, 182 insertions(+), 77 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index d8cc7928..2cc6d6a7 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -67,6 +67,8 @@ pragma language_version >= 0.21.0; * - Outside observers will know which role identifiers are admin identifiers. * - Outside observers will have knowledge of all role identifiers. * - Outside observers will have knowledge of role additions and revocations. + * - Outside observers can link calls made by the same role instance across time. + * - If a user ever reveals their nonce or reuses it poorly, they can be deanonymized retroactively. * - Outside observers will NOT be able to identify the public address of any role holder * so long as secret nonce values are kept private and generated using cryptographically * secure random values. @@ -93,17 +95,17 @@ module ShieldedAccessControl { export new type RoleIdentifier = Bytes<32>; export new type AccountIdentifier = Bytes<32>; export new type AdminIdentifier = Bytes<32>; - export new type RoleNullifer = Bytes<32>; + export new type RoleNullifier = Bytes<32>; type ZcpkOrContractAddress = Either; /** * @ledger _operatorRoles * @description A Merkle tree of role commitments stored as SHA256(roleId | accountId | instanceSalt | commitmentDomain) * Role commitments are derived from a public role identifier (e.g., `persistentHash>(pad(32, "MY_ROLE")`), - * an account identifier (e.g., `SHA256(zcpk, nonce)`), the `instanceSalt`, and a domain separator. + * an account identifier (e.g., `SHA256(zcpk, nonce, instanceSalt, accountDomain)`), the `instanceSalt`, and a domain separator. * @type {Bytes<32>} RoleCommitment - A role commitment created by the following hash: SHA256( roleId | accountId | instanceSalt | commitmentDomain).  */ - export ledger _operatorRoles: MerkleTree<10, RoleCommitment>; + export ledger _operatorRoles: MerkleTree<20, RoleCommitment>; /** * @ledger _adminRoles @@ -112,11 +114,11 @@ module ShieldedAccessControl { export ledger _adminRoles: Map; /** - * @description A set of nullifiers used to revoke the permissions of a role - * @type {Bytes<32>} RoleNullifier - A role nullifier created by the following hash: SHA256( roleCommitment | nullifierDomain). + * @description A Merkle tree of nullifiers used to prove a role has been revoked + * @type {Bytes<32>} RoleNullifier - A role nullifier created by the following hash: SHA256(roleCommitment | nullifierDomain). * @type {Set} _roleCommitmentNullifiers  */ - export ledger _roleCommitmentNullifiers: Set; + export ledger _roleCommitmentNullifiers: MerkleTree<20, RoleNullifier>; /** * @description The default admin role for all roles. Only accounts with this role will be able to grant or revoke other roles @@ -141,34 +143,43 @@ module ShieldedAccessControl { * @description Returns a path to a role commitment in the `_operatorRoles` Merkle tree if one exists. Otherwise, returns an invalid path. * * @param {Bytes<32>} RoleCommitment - A commitment created by the following hash: SHA256( roleId | accountId | instanceSalt | commitmentDomain). - * @return {MerkleTreePath<10, Bytes<32>>} - The Merkle tree path to `roleCommitment` in the `_operatorRoles` Merkle tree + * @return {MerkleTreePath<20, Bytes<32>>} - The Merkle tree path to `roleCommitment` in the `_operatorRoles` Merkle tree  */ witness wit_getRoleCommitmentPath( roleCommitment: RoleCommitment - ): MerkleTreePath<10, RoleCommitment>; + ): MerkleTreePath<20, RoleCommitment>; + + /** + * @witness wit_getCommitmentNullifierPath + * @description Returns a path to a role nullifier in the `_roleCommitmentNullifiers` Merkle tree if one exists. Otherwise, returns an invalid path. + * + * @param {Bytes<32>} RoleNullifier - A nullifier created by the following hash: SHA256( roleCommitment | commitmentDomain). + * @return {MerkleTreePath<20, Bytes<32>>} - The Merkle tree path to `roleNullifier` in the `_roleCommitmentNullifiers` Merkle tree +  */ + witness wit_getCommitmentNullifierPath( + roleNullifier: RoleNullifier + ): MerkleTreePath<20, RoleNullifier>; /** * @witness wit_secretNonce * @description A private per-accountId nonce used in deriving the shielded account identifier. * - * Combined with the user's ZswapCoinPublicKey as `SHA256(zcpk, nonce)` to produce an obfuscated, - * unlinkable identity commitment. + * Combined with the user's ZswapCoinPublicKey as `SHA256(zcpk, nonce, instanceSalt, accountDomain)` to produce an obfuscated, + * unlinkable identity commitment. Nonce MUST be unique per role to avoid cross-role linking. * * @param {Bytes<32>} roleId - The unique identifier of a role. */ witness wit_secretNonce(roleId: RoleIdentifier): Bytes<32>; /** - * @description A struct containing information for a particular `roleId` | `accountId` pairing. + * @description A struct containing auth information for a particular `roleId` | `accountId` pairing. * - * @type {Boolean} hasRole - A boolean flag indicating an `accountId` is authorized for a `roleId` - * @type {} roleCommitment - The role commitment for a particular `roleId` | `accountId` pairing. - * @type {} roleNullifier - The associated role nullifier for `roleCommitment`. + * @type {Boolean} hasRole - A boolean flag indicating an `accountId` is authorized for a `roleId`. + * @type {Boolean} isRevoked - A boolean flag indicating if a nullifier exists for `roleCommitment`. */ - export struct Role { + export struct RoleCheck { hasRole: Boolean; - roleCommitment: RoleCommitment; - roleNullifier: RoleNullifer; + isRevoked: Boolean; } /** @@ -199,7 +210,7 @@ module ShieldedAccessControl { * * ## Account ID (`accountId`) * The `accountId` is expected to be computed off-chain as: - * `accountId = SHA256(zcpk, nonce)` + * `accountId = SHA256(zcpk, nonce, instanceSalt, accountDomain)` * * - `zcpk`: The account's ZswapCoinPublicKey. * - `nonce`: A secret nonce scoped to the role. @@ -221,13 +232,13 @@ module ShieldedAccessControl { * - Contract is initialized. * * @param {Bytes<32>} roleId - The unique identifier of a role. - * @param {Bytes<32>} accountId - The unique identifier of the account calculated by `SHA256(zcpk, nonce)`. + * @param {Bytes<32>} accountId - The unique identifier of the account calculated by `SHA256(zcpk, nonce, instanceSalt, accountDomain)`. * @returns {Bytes<32>} The commitment derived from `accountId` and `roleId`. */ - export circuit _computeRoleCommitment( - roleId: RoleIdentifier, - accountId: AccountIdentifier, - ): RoleCommitment { + circuit _computeRoleCommitment( + roleId: RoleIdentifier, + accountId: AccountIdentifier, + ): RoleCommitment { Initializable_assertInitialized(); return persistentHash>>( @@ -254,11 +265,11 @@ module ShieldedAccessControl { * @param {} roleCommitment - The role commitment for a particular `roleId` | `accountId` pairing. * @returns {Bytes<32>} roleNullifier - The associated nullifier for `roleCommitment`. */ - export pure circuit _computeNullifier(roleCommitment: RoleCommitment): RoleNullifer { + pure circuit _computeNullifier(roleCommitment: RoleCommitment): RoleNullifier { return persistentHash>>( [roleCommitment as Bytes<32>, pad(32, "ShieldedAccessControl:nullifier")] ) - as RoleNullifer; + as RoleNullifier; } /** @@ -266,13 +277,17 @@ module ShieldedAccessControl { * ZswapCoinPublicKey and a secret nonce. * * ## ID Derivation - * `accountId = SHA256(zcpk, nonce)` + * `accountId = SHA256(zcpk, nonce, instanceSalt, accountDomain)` * * - `zcpk`: The ZswapCoinPublicKey of the caller. This is passed explicitly to allow * for off-chain derivation, testing, or scenarios where the caller is * different from the subject of the computation. We recommend using an Air-Gapped Public Key. * - `nonce`: A secret nonce tied to the identity. The generation strategy is * left to the user, offering different security/convenience trade-offs. + * - `instanceSalt`: A unique per-deployment salt, stored during initialization. + * This prevents commitment collisions across deployments. + * - `accountIdDomain`: Domain separator `"ShieldedAccessControl:accountId"` (padded to 32 bytes) to prevent + * hash collisions when extending the module or using similar commitment schemes. * * The result is a 32-byte commitment that uniquely identifies the account. * This value is later used in role commitment hashing, @@ -292,13 +307,13 @@ module ShieldedAccessControl { * @param {Bytes<32>} nonce - A private nonce to scope the commitment. * @returns {Bytes<32>} accountId - The computed account ID. */ - export pure circuit _computeAccountId( - pk: ZcpkOrContractAddress, - nonce: Bytes<32> - ): AccountIdentifier { + circuit _computeAccountId(pk: ZcpkOrContractAddress, nonce: Bytes<32>): AccountIdentifier { assert(pk.is_left, "ShieldedAccessControl: contract address roles are not yet supported"); - return persistentHash>>([pk.left.bytes, nonce]) as AccountIdentifier; + return persistentHash>>( + [pk.left.bytes, nonce, _instanceSalt, pad(32, "ShieldedAccessControl:accountId")] + ) + as AccountIdentifier; } /** @@ -308,9 +323,8 @@ module ShieldedAccessControl { * * Disclosures: * - * - The role commitment produced by SHA256(roleId | accountId | instanceSalt | commitmentDomain ). - * - The role nullifier produced by SHA256(roleCommitment | nullifierDomain). * - The Merkle tree path for the role commitment stored in the `_operatorRoles` Merkle tree. + * - The Merkle tree path for the nullifier commitment stored in the `_roleCommitmentNullifiers` Merkle tree. * * @param {Bytes<32>} roleId - The role identifier. * @return {Boolean} - A boolean determining if a caller has the specified role. @@ -324,7 +338,7 @@ module ShieldedAccessControl { right: ContractAddress { bytes: pad(32, "") } }; const nonce = wit_secretNonce(roleId); const accountId = _computeAccountId(callerAsEither, nonce); - return computeRole(roleId, accountId).hasRole; + return checkRole(roleId, accountId).hasRole; } /** @@ -338,9 +352,8 @@ module ShieldedAccessControl { * * Disclosures: * - * - The role commitment produced by SHA256(roleId | accountId | instanceSalt | commitmentDomain ). - * - The role nullifier produced by SHA256(roleCommitment | nullifierDomain). * - The Merkle tree path for the role commitment stored in the `_operatorRoles` Merkle tree. + * - The Merkle tree path for the nullifier commitment stored in the `_roleCommitmentNullifiers` Merkle tree. * * @param {Bytes<32>} roleId - The role identifier. * @return {[]} - Empty tuple. @@ -355,39 +368,49 @@ module ShieldedAccessControl { * @description Computes the role commitment and associated role nullifier for a `roleId` | `accountId` * pairing. A role commitment is valid for a `roleId` | `accountId` pairing if and only if a valid * path exists in the `_operatorRoles` Merkle tree for the role commitment and a role nullifier doesn't - * exist for the role commitment in the `_roleNullifiers` set. + * exist for the role commitment in the `_roleCommitmentNullifiers` set. * * @circuitInfo k=15, rows=29780 * * Disclosures: * - * - The role commitment produced by SHA256(roleId | accountId | instanceSalt | commitmentDomain ). - * - The role nullifier produced by SHA256(roleCommitment | nullifierDomain). * - The Merkle tree path for the role commitment stored in the `_operatorRoles` Merkle tree. + * - The Merkle tree path for the nullifier commitment stored in the `_roleCommitmentNullifiers` Merkle tree. * * @param {Bytes<32>} roleId - The role identifier. * @param {Bytes<32>} accountId - The unique identifier of the account. * @return {[]} - Empty tuple. */ - export circuit computeRole(roleId: RoleIdentifier, accountId: AccountIdentifier): Role { + circuit checkRole(roleId: RoleIdentifier, accountId: AccountIdentifier): RoleCheck { Initializable_assertInitialized(); const roleCommitment = _computeRoleCommitment(roleId, accountId); - const roleNullifier = _computeNullifier(roleCommitment); const roleCommitmentPath = wit_getRoleCommitmentPath(roleCommitment); - const rootMatches = + assert(roleCommitment == roleCommitmentPath.leaf, + "ShieldedAccessControl: Path must contain queried role commitment" + ); + + const roleNullifier = _computeNullifier(roleCommitment); + const nullifierCommitmentPath = wit_getCommitmentNullifierPath(roleNullifier); + assert(roleNullifier == nullifierCommitmentPath.leaf, + "ShieldedAccessControl: Path must contain queried nullifier commitment" + ); + + const hasCommitment = _operatorRoles.checkRoot( - merkleTreePathRoot<10, RoleCommitment>(disclose(roleCommitmentPath)) + merkleTreePathRoot<20, RoleCommitment>(disclose(roleCommitmentPath)) + ); + const isRevoked = + _roleCommitmentNullifiers.checkRoot( + merkleTreePathRoot<20, RoleNullifier>(disclose(nullifierCommitmentPath)) ); - if (!_roleCommitmentNullifiers.member(disclose(roleNullifier)) && rootMatches) { - return Role { hasRole: true, - roleCommitment: roleCommitment, - roleNullifier: roleNullifier }; + const hasRole = hasCommitment && !isRevoked; + + if (hasRole) { + return RoleCheck { hasRole, isRevoked }; } else { - return Role { hasRole: false, - roleCommitment: roleCommitment, - roleNullifier: roleNullifier }; + return RoleCheck { hasRole, isRevoked }; } } @@ -399,6 +422,10 @@ module ShieldedAccessControl { * * @circuitInfo k=10, rows=207 * + * Disclosures: + * + * - The role identifier + * * @param {Bytes<32>} roleId - The role identifier. * @return {Bytes<32>} roleAdmin - The admin role that controls `roleId`. */ @@ -426,8 +453,9 @@ module ShieldedAccessControl { * Disclosures: * * - The role commitment produced by SHA256(roleId | accountId | instanceSalt | commitmentDomain ). - * - The role nullifier produced by SHA256(roleCommitment | nullifierDomain). + * - The role identifier * - The Merkle tree path for the role commitment stored in the `_operatorRoles` Merkle tree. + * - The Merkle tree path for the nullifier commitment stored in the `_roleCommitmentNullifiers` Merkle tree. * * @param {Bytes<32>} roleId - The role identifier. * @param {Bytes<32>} accountId - The unique identifier of the account. @@ -455,9 +483,10 @@ module ShieldedAccessControl { * * Disclosures: * - * - The role commitment produced by SHA256(roleId | accountId | instanceSalt | commitmentDomain ). * - The role nullifier produced by SHA256(roleCommitment | nullifierDomain). + * - The role identifier * - The Merkle tree path for the role commitment stored in the `_operatorRoles` Merkle tree. + * - The Merkle tree path for the nullifier commitment stored in the `_roleCommitmentNullifiers` Merkle tree. * * @param {Bytes<32>} roleId - The role identifier. * @param {Bytes<32>} accountId - The unique identifier of the account. @@ -485,9 +514,9 @@ module ShieldedAccessControl { * * Disclosures: * - * - The role commitment produced by SHA256(roleId | accountId | instanceSalt | commitmentDomain ). * - The role nullifier produced by SHA256(roleCommitment | nullifierDomain). * - The Merkle tree path for the role commitment stored in the `_operatorRoles` Merkle tree. + * - The Merkle tree path for the nullifier commitment stored in the `_roleCommitmentNullifiers` Merkle tree. * * @param {Bytes<32>} roleId - The role identifier. * @param {Bytes<32>} accountId - The unique identifier of the account. @@ -509,10 +538,15 @@ module ShieldedAccessControl { } /** - * @description Sets `adminID` as `roleId`'s admin identifier. Internal circuit without access restriction. + * @description Sets `adminId` as `roleId`'s admin identifier. Internal circuit without access restriction. * * @circuitInfo k=10, rows=209 * + * Disclosures: + * + * - The role identifier + * - The account identifier + * * @param {Bytes<32>} roleId - The role identifier. * @param {Bytes<32>} adminId - The admin role identifier. * @return {[]} - Empty tuple. @@ -532,9 +566,9 @@ module ShieldedAccessControl { * * Disclosures: * - * - The role commitment produced by SHA256(roleId | accountId | instanceSalt | commitmentDomain ). - * - The role nullifier produced by SHA256(roleCommitment | nullifierDomain ). + * - The role commitment produced by SHA256(roleId | accountId | instanceSalt | commitmentDomain). * - The Merkle tree path for the role commitment stored in the `_operatorRoles` Merkle tree. + * - The Merkle tree path for the nullifier commitment stored in the `_roleCommitmentNullifiers` Merkle tree. * * @param {Bytes<32>} roleId - The role identifier. * @param {Bytes<32>} accountId - The account identifier. @@ -543,12 +577,14 @@ module ShieldedAccessControl { export circuit _grantRole(roleId: RoleIdentifier, accountId: AccountIdentifier): Boolean { Initializable_assertInitialized(); - const role = computeRole(roleId, accountId); - if (role.hasRole || _roleCommitmentNullifiers.member(disclose(role.roleNullifier))) { + const roleCheck = checkRole(roleId, accountId); + if (roleCheck.hasRole || roleCheck.isRevoked) { return false; } - _operatorRoles.insert(disclose(role.roleCommitment)); + const roleCommitment = _computeRoleCommitment(roleId, accountId); + + _operatorRoles.insert(disclose(roleCommitment)); return true; } @@ -561,9 +597,9 @@ module ShieldedAccessControl { * * Disclosures: * - * - The role commitment produced by SHA256(roleId | accountId | instanceSalt | commitmentDomain ). * - The role nullifier produced by SHA256(roleCommitment | nullifierDomain). * - The Merkle tree path for the role commitment stored in the `_operatorRoles` Merkle tree. + * - The Merkle tree path for the nullifier commitment stored in the `_roleCommitmentNullifiers` Merkle tree. * * @param {Bytes<32>} roleId - The role identifier. * @param {Bytes<32>} accountId - The account identifier. @@ -572,12 +608,15 @@ module ShieldedAccessControl { export circuit _revokeRole(roleId: RoleIdentifier, accountId: AccountIdentifier): Boolean { Initializable_assertInitialized(); - const role = computeRole(roleId, accountId); - if (!role.hasRole || _roleCommitmentNullifiers.member(disclose(role.roleNullifier))) { + const roleCheck = checkRole(roleId, accountId); + if (!roleCheck.hasRole || roleCheck.isRevoked) { return false; } - _roleCommitmentNullifiers.insert(disclose(role.roleNullifier)); + const roleCommitment = _computeRoleCommitment(roleId, accountId); + const roleNullifier = _computeNullifier(roleCommitment); + + _roleCommitmentNullifiers.insert(disclose(roleNullifier)); return true; } } diff --git a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact index d5c4e30d..629b1af8 100644 --- a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact +++ b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact @@ -6,6 +6,8 @@ import CompactStandardLibrary; import "../../ShieldedAccessControl" prefix ShieldedAccessControl_; +import "../../../security/Initializable" prefix Initializable_; + export { ZswapCoinPublicKey, ContractAddress, Either, @@ -14,7 +16,17 @@ export { ZswapCoinPublicKey, ShieldedAccessControl_DEFAULT_ADMIN_ROLE, ShieldedAccessControl__operatorRoles, ShieldedAccessControl__roleCommitmentNullifiers, - ShieldedAccessControl_Role }; + ShieldedAccessControl_RoleCheck, }; + +// witness is re-implemented in the Mock contract for testing +witness wit_getRoleCommitmentPath( + roleCommitment: ShieldedAccessControl_RoleCommitment + ): MerkleTreePath<10, ShieldedAccessControl_RoleCommitment>; + +// witness is re-implemented in the Mock contract for testing +witness wit_getCommitmentNullifierPath( + roleNullifier: ShieldedAccessControl_RoleNullifier + ): MerkleTreePath<10, ShieldedAccessControl_RoleNullifier>; /** * @description `isInit` is a param for testing. @@ -29,24 +41,46 @@ constructor(instanceSalt: Bytes<32>, isInit: Boolean) { } } +// circuit is reimplemented in the Mock contract for testing export circuit _computeRoleCommitment( roleId: ShieldedAccessControl_RoleIdentifier, accountId: ShieldedAccessControl_AccountIdentifier ): ShieldedAccessControl_RoleCommitment { - return ShieldedAccessControl__computeRoleCommitment(roleId, accountId); + Initializable_assertInitialized(); + + return persistentHash>>( + [roleId as Bytes<32>, + accountId as Bytes<32>, + ShieldedAccessControl__instanceSalt, + pad(32, "ShieldedAccessControl:commitment")] + ) + as ShieldedAccessControl_RoleCommitment; } -export pure circuit _computeAccountId( - pk: Either, - nonce: Bytes<32> - ): ShieldedAccessControl_AccountIdentifier { - return ShieldedAccessControl__computeAccountId(pk, nonce); +// circuit is reimplemented in the Mock contract for testing +export circuit _computeAccountId( + pk: Either, + nonce: Bytes<32> + ): ShieldedAccessControl_AccountIdentifier { + assert(pk.is_left, "ShieldedAccessControl: contract address roles are not yet supported"); + + return persistentHash>>( + [pk.left.bytes, + nonce, + ShieldedAccessControl__instanceSalt, + pad(32, "ShieldedAccessControl:accountId")] + ) + as ShieldedAccessControl_AccountIdentifier; } +// circuit is reimplemented in the Mock contract for testing export pure circuit _computeNullifier( roleCommitment: ShieldedAccessControl_RoleCommitment - ): ShieldedAccessControl_RoleNullifer { - return ShieldedAccessControl__computeNullifier(roleCommitment); + ): ShieldedAccessControl_RoleNullifier { + return persistentHash>>( + [roleCommitment as Bytes<32>, pad(32, "ShieldedAccessControl:nullifier")] + ) + as ShieldedAccessControl_RoleNullifier; } export circuit callerHasRole(roleId: ShieldedAccessControl_RoleIdentifier): Boolean { @@ -57,11 +91,43 @@ export circuit assertOnlyRole(roleId: ShieldedAccessControl_RoleIdentifier): [] ShieldedAccessControl_assertOnlyRole(roleId); } -export circuit computeRole( - roleId: ShieldedAccessControl_RoleIdentifier, - accountId: ShieldedAccessControl_AccountIdentifier - ): ShieldedAccessControl_Role { - return ShieldedAccessControl_computeRole(roleId, accountId); +// checkRole is re-implemented in the Mock contract for testing +circuit checkRole( + roleId: ShieldedAccessControl_RoleIdentifier, + accountId: ShieldedAccessControl_AccountIdentifier + ): ShieldedAccessControl_RoleCheck { + Initializable_assertInitialized(); + + const roleCommitment = _computeRoleCommitment(roleId, accountId); + const roleCommitmentPath = wit_getRoleCommitmentPath(roleCommitment); + assert(roleCommitment == roleCommitmentPath.leaf, + "ShieldedAccessControl: Path must contain queried role commitment" + ); + + const roleNullifier = _computeNullifier(roleCommitment); + const nullifierCommitmentPath = wit_getCommitmentNullifierPath(roleNullifier); + assert(roleNullifier == nullifierCommitmentPath.leaf, + "ShieldedAccessControl: Path must contain queried nullifier commitment" + ); + + const isValidCommitmentPath = + ShieldedAccessControl__operatorRoles.checkRoot( + merkleTreePathRoot<10, ShieldedAccessControl_RoleCommitment>( + disclose(roleCommitmentPath) + ) + ); + const isValidNullifierPath = + ShieldedAccessControl__roleCommitmentNullifiers.checkRoot( + merkleTreePathRoot<10, ShieldedAccessControl_RoleNullifier>( + disclose(nullifierCommitmentPath) + ) + ); + + if (!isValidNullifierPath && isValidCommitmentPath) { + return ShieldedAccessControl_RoleCheck { hasRole: true, isRevoked: isValidNullifierPath, }; + } else { + return ShieldedAccessControl_RoleCheck { hasRole: false, isRevoked: isValidNullifierPath, }; + } } export circuit getRoleAdmin( From 6a4d55ed19cc88a7bd2bfb84e3f2a13b5044015e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Sun, 1 Mar 2026 21:04:31 -0500 Subject: [PATCH 227/322] Update witness file --- .../ShieldedAccessControlWitnesses.ts | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts b/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts index 7a1ca973..52fcaf18 100644 --- a/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts +++ b/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts @@ -28,6 +28,10 @@ export interface IShieldedAccessControlWitnesses

{ context: WitnessContext, roleCommitment: Uint8Array, ): [P, MerkleTreePath]; + wit_getCommitmentNullifierPath( + context: WitnessContext, + nullifierCommitment: Uint8Array, + ): [P, MerkleTreePath]; } type RoleId = string; @@ -107,6 +111,23 @@ export const ShieldedAccessControlPrivateState = { }; return path ? path : defaultPath; }, + getCommitmentNullifierPath: ( + ledger: Ledger, + nullifierCommitment: Uint8Array, + ): MerkleTreePath => { + const path = + ledger.ShieldedAccessControl__roleCommitmentNullifiers.findPathForLeaf( + nullifierCommitment, + ); + const defaultPath: MerkleTreePath = { + leaf: new Uint8Array(32), + path: Array.from({ length: 10 }, () => ({ + sibling: { field: 0n }, + goes_left: false, + })), + }; + return path ? path : defaultPath; + }, } /** @@ -134,4 +155,16 @@ export const ShieldedAccessControlWitnesses = ), ]; }, + wit_getCommitmentNullifierPath( + context: WitnessContext, + nullifierCommitment: Uint8Array, + ): [ShieldedAccessControlPrivateState, MerkleTreePath] { + return [ + context.privateState, + ShieldedAccessControlPrivateState.getCommitmentNullifierPath( + context.ledger, + nullifierCommitment, + ), + ]; + }, }); From 82170b881730ff0734afe7b157caffa47b690756 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Sun, 1 Mar 2026 21:11:20 -0500 Subject: [PATCH 228/322] Export circuit from Mock --- .../src/access/test/mocks/MockShieldedAccessControl.compact | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact index 629b1af8..3d344640 100644 --- a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact +++ b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact @@ -92,7 +92,7 @@ export circuit assertOnlyRole(roleId: ShieldedAccessControl_RoleIdentifier): [] } // checkRole is re-implemented in the Mock contract for testing -circuit checkRole( +export circuit checkRole( roleId: ShieldedAccessControl_RoleIdentifier, accountId: ShieldedAccessControl_AccountIdentifier ): ShieldedAccessControl_RoleCheck { From f3279b49c0541b52116d098c68123b6e0aad900b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Sun, 1 Mar 2026 21:11:41 -0500 Subject: [PATCH 229/322] Update simulator --- .../ShieldedAccessControlSimulator.ts | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts index 4f3147a6..57529918 100644 --- a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts +++ b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts @@ -5,7 +5,7 @@ import { import { type ContractAddress, type Either, - type ShieldedAccessControl_Role as Role, + type ShieldedAccessControl_RoleCheck as RoleCheck, ledger, Contract as MockShieldedAccessControl, type ZswapCoinPublicKey, @@ -67,7 +67,7 @@ export class ShieldedAccessControlSimulator extends ShieldedAccessControlSimulat pk: Either, nonce: Uint8Array ): Uint8Array { - return this.circuits.pure._computeAccountId(pk, nonce); + return this.circuits.impure._computeAccountId(pk, nonce); } public _computeNullifier(roleCommitment: Uint8Array): Uint8Array { @@ -87,12 +87,12 @@ export class ShieldedAccessControlSimulator extends ShieldedAccessControlSimulat this.circuits.impure.assertOnlyRole(roleId); } - public computeRole(roleId: Uint8Array, accountId: Uint8Array): Role { - return this.circuits.impure.computeRole(roleId, accountId); + public checkRole(roleId: Uint8Array, accountId: Uint8Array): RoleCheck { + return this.circuits.impure.checkRole(roleId, accountId); } /** - * @description Computes the role commitment from the given `id` and `counter`. + * @description Computes the RoleCheck commitment from the given `id` and `counter`. * @param id - The unique identifier of the owner calculated by `SHA256(pk, nonce)`. * @param counter - The current counter or round. This increments by `1` * after every transfer to prevent duplicate commitments given the same `id`. @@ -203,11 +203,17 @@ export class ShieldedAccessControlSimulator extends ShieldedAccessControlSimulat roleString ]; }, - getPathWithFindForLeaf: (roleCommitment: Uint8Array): MerkleTreePath | undefined => { + getCommitmentPathWithFindForLeaf: (roleCommitment: Uint8Array): MerkleTreePath | undefined => { return this.getPublicState().ShieldedAccessControl__operatorRoles.findPathForLeaf(roleCommitment); }, - getPathWithWitnessImpl: (roleCommitment: Uint8Array): MerkleTreePath => { + getCommitmentPathWithWitnessImpl: (roleCommitment: Uint8Array): MerkleTreePath => { return this.witnesses.wit_getRoleCommitmentPath(this.getWitnessContext(), roleCommitment)[1]; + }, + getNullifierPathWithFindForLeaf: (nullifierCommitment: Uint8Array): MerkleTreePath | undefined => { + return this.getPublicState().ShieldedAccessControl__roleCommitmentNullifiers.findPathForLeaf(nullifierCommitment); + }, + getNullifierPathWithWitnessImpl: (nullifierCommitment: Uint8Array): MerkleTreePath => { + return this.witnesses.wit_getCommitmentNullifierPath(this.getWitnessContext(), nullifierCommitment)[1]; } }; } From 0be3e5c486bdcd3aee2de936b72038e32fc651a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Sun, 1 Mar 2026 21:37:21 -0500 Subject: [PATCH 230/322] Update method name, assert Initialized in computeAccountId --- contracts/src/access/ShieldedAccessControl.compact | 9 +++++---- .../access/test/mocks/MockShieldedAccessControl.compact | 2 +- .../test/simulators/ShieldedAccessControlSimulator.ts | 4 ++-- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index 2cc6d6a7..5e8cf41e 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -308,6 +308,7 @@ module ShieldedAccessControl { * @returns {Bytes<32>} accountId - The computed account ID. */ circuit _computeAccountId(pk: ZcpkOrContractAddress, nonce: Bytes<32>): AccountIdentifier { + Initializable_assertInitialized(); assert(pk.is_left, "ShieldedAccessControl: contract address roles are not yet supported"); return persistentHash>>( @@ -338,7 +339,7 @@ module ShieldedAccessControl { right: ContractAddress { bytes: pad(32, "") } }; const nonce = wit_secretNonce(roleId); const accountId = _computeAccountId(callerAsEither, nonce); - return checkRole(roleId, accountId).hasRole; + return _checkRole(roleId, accountId).hasRole; } /** @@ -381,7 +382,7 @@ module ShieldedAccessControl { * @param {Bytes<32>} accountId - The unique identifier of the account. * @return {[]} - Empty tuple. */ - circuit checkRole(roleId: RoleIdentifier, accountId: AccountIdentifier): RoleCheck { + circuit _checkRole(roleId: RoleIdentifier, accountId: AccountIdentifier): RoleCheck { Initializable_assertInitialized(); const roleCommitment = _computeRoleCommitment(roleId, accountId); @@ -577,7 +578,7 @@ module ShieldedAccessControl { export circuit _grantRole(roleId: RoleIdentifier, accountId: AccountIdentifier): Boolean { Initializable_assertInitialized(); - const roleCheck = checkRole(roleId, accountId); + const roleCheck = _checkRole(roleId, accountId); if (roleCheck.hasRole || roleCheck.isRevoked) { return false; } @@ -608,7 +609,7 @@ module ShieldedAccessControl { export circuit _revokeRole(roleId: RoleIdentifier, accountId: AccountIdentifier): Boolean { Initializable_assertInitialized(); - const roleCheck = checkRole(roleId, accountId); + const roleCheck = _checkRole(roleId, accountId); if (!roleCheck.hasRole || roleCheck.isRevoked) { return false; } diff --git a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact index 3d344640..284e4cd4 100644 --- a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact +++ b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact @@ -92,7 +92,7 @@ export circuit assertOnlyRole(roleId: ShieldedAccessControl_RoleIdentifier): [] } // checkRole is re-implemented in the Mock contract for testing -export circuit checkRole( +export circuit _checkRole( roleId: ShieldedAccessControl_RoleIdentifier, accountId: ShieldedAccessControl_AccountIdentifier ): ShieldedAccessControl_RoleCheck { diff --git a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts index 57529918..5d3db100 100644 --- a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts +++ b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts @@ -87,8 +87,8 @@ export class ShieldedAccessControlSimulator extends ShieldedAccessControlSimulat this.circuits.impure.assertOnlyRole(roleId); } - public checkRole(roleId: Uint8Array, accountId: Uint8Array): RoleCheck { - return this.circuits.impure.checkRole(roleId, accountId); + public _checkRole(roleId: Uint8Array, accountId: Uint8Array): RoleCheck { + return this.circuits.impure._checkRole(roleId, accountId); } /** From 5c67ac833756dee446fea6e1fb6619a2d88ee264 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Mon, 2 Mar 2026 10:13:40 -0500 Subject: [PATCH 231/322] fmt file --- .../src/access/test/mocks/MockShieldedAccessControl.compact | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact index 284e4cd4..3f8fa8bf 100644 --- a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact +++ b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact @@ -93,9 +93,9 @@ export circuit assertOnlyRole(roleId: ShieldedAccessControl_RoleIdentifier): [] // checkRole is re-implemented in the Mock contract for testing export circuit _checkRole( - roleId: ShieldedAccessControl_RoleIdentifier, - accountId: ShieldedAccessControl_AccountIdentifier - ): ShieldedAccessControl_RoleCheck { + roleId: ShieldedAccessControl_RoleIdentifier, + accountId: ShieldedAccessControl_AccountIdentifier + ): ShieldedAccessControl_RoleCheck { Initializable_assertInitialized(); const roleCommitment = _computeRoleCommitment(roleId, accountId); From 9601c4d969326633611b423f26c798fdb874b6dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Mon, 2 Mar 2026 12:58:58 -0500 Subject: [PATCH 232/322] Add assertion to _checkRole --- .../src/access/ShieldedAccessControl.compact | 25 ++++++---- .../mocks/MockShieldedAccessControl.compact | 48 +++++++++++-------- 2 files changed, 45 insertions(+), 28 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index 5e8cf41e..c4e58748 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -387,25 +387,32 @@ module ShieldedAccessControl { const roleCommitment = _computeRoleCommitment(roleId, accountId); const roleCommitmentPath = wit_getRoleCommitmentPath(roleCommitment); - assert(roleCommitment == roleCommitmentPath.leaf, + const hasCommitment = + _operatorRoles.checkRoot( + merkleTreePathRoot<20, RoleCommitment>(disclose(roleCommitmentPath)) + ); + + // If the path is valid we assert it’s a path for the queried leaf (not some other leaf). + if (hasCommitment) { + assert(roleCommitmentPath.leaf == roleCommitment, "ShieldedAccessControl: Path must contain queried role commitment" ); + } const roleNullifier = _computeNullifier(roleCommitment); const nullifierCommitmentPath = wit_getCommitmentNullifierPath(roleNullifier); - assert(roleNullifier == nullifierCommitmentPath.leaf, - "ShieldedAccessControl: Path must contain queried nullifier commitment" - ); - - const hasCommitment = - _operatorRoles.checkRoot( - merkleTreePathRoot<20, RoleCommitment>(disclose(roleCommitmentPath)) - ); const isRevoked = _roleCommitmentNullifiers.checkRoot( merkleTreePathRoot<20, RoleNullifier>(disclose(nullifierCommitmentPath)) ); + // If the path is valid we assert it’s a path for the queried leaf (not some other leaf). + if (isRevoked) { + assert(nullifierCommitmentPath.leaf == roleNullifier, + "ShieldedAccessControl: Path must contain queried role nullifier" + ); + } + const hasRole = hasCommitment && !isRevoked; if (hasRole) { diff --git a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact index 3f8fa8bf..e3e7f0eb 100644 --- a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact +++ b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact @@ -21,12 +21,12 @@ export { ZswapCoinPublicKey, // witness is re-implemented in the Mock contract for testing witness wit_getRoleCommitmentPath( roleCommitment: ShieldedAccessControl_RoleCommitment - ): MerkleTreePath<10, ShieldedAccessControl_RoleCommitment>; + ): MerkleTreePath<20, ShieldedAccessControl_RoleCommitment>; // witness is re-implemented in the Mock contract for testing witness wit_getCommitmentNullifierPath( roleNullifier: ShieldedAccessControl_RoleNullifier - ): MerkleTreePath<10, ShieldedAccessControl_RoleNullifier>; + ): MerkleTreePath<20, ShieldedAccessControl_RoleNullifier>; /** * @description `isInit` is a param for testing. @@ -62,6 +62,7 @@ export circuit _computeAccountId( pk: Either, nonce: Bytes<32> ): ShieldedAccessControl_AccountIdentifier { + Initializable_assertInitialized(); assert(pk.is_left, "ShieldedAccessControl: contract address roles are not yet supported"); return persistentHash>>( @@ -100,33 +101,42 @@ export circuit _checkRole( const roleCommitment = _computeRoleCommitment(roleId, accountId); const roleCommitmentPath = wit_getRoleCommitmentPath(roleCommitment); - assert(roleCommitment == roleCommitmentPath.leaf, - "ShieldedAccessControl: Path must contain queried role commitment" - ); - - const roleNullifier = _computeNullifier(roleCommitment); - const nullifierCommitmentPath = wit_getCommitmentNullifierPath(roleNullifier); - assert(roleNullifier == nullifierCommitmentPath.leaf, - "ShieldedAccessControl: Path must contain queried nullifier commitment" - ); - - const isValidCommitmentPath = + const hasCommitment = ShieldedAccessControl__operatorRoles.checkRoot( - merkleTreePathRoot<10, ShieldedAccessControl_RoleCommitment>( + merkleTreePathRoot<20, ShieldedAccessControl_RoleCommitment>( disclose(roleCommitmentPath) ) ); - const isValidNullifierPath = + + // If the path is valid we assert it’s a path for the queried leaf (not some other leaf). + if (hasCommitment) { + assert(roleCommitmentPath.leaf == roleCommitment, + "ShieldedAccessControl: Path must contain queried role commitment" + ); + } + + const roleNullifier = _computeNullifier(roleCommitment); + const nullifierCommitmentPath = wit_getCommitmentNullifierPath(roleNullifier); + const isRevoked = ShieldedAccessControl__roleCommitmentNullifiers.checkRoot( - merkleTreePathRoot<10, ShieldedAccessControl_RoleNullifier>( + merkleTreePathRoot<20, ShieldedAccessControl_RoleNullifier>( disclose(nullifierCommitmentPath) ) ); - if (!isValidNullifierPath && isValidCommitmentPath) { - return ShieldedAccessControl_RoleCheck { hasRole: true, isRevoked: isValidNullifierPath, }; + // If the path is valid we assert it’s a path for the queried leaf (not some other leaf). + if (isRevoked) { + assert(nullifierCommitmentPath.leaf == roleNullifier, + "ShieldedAccessControl: Path must contain queried role nullifier" + ); + } + + const hasRole = hasCommitment && !isRevoked; + + if (hasRole) { + return ShieldedAccessControl_RoleCheck { hasRole, isRevoked }; } else { - return ShieldedAccessControl_RoleCheck { hasRole: false, isRevoked: isValidNullifierPath, }; + return ShieldedAccessControl_RoleCheck { hasRole, isRevoked }; } } From 8053d174a43b30b7d5c4d4d1a961d7b88b63a434 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Mon, 2 Mar 2026 12:59:27 -0500 Subject: [PATCH 233/322] fmt file --- contracts/src/access/ShieldedAccessControl.compact | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index c4e58748..dd82340d 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -394,9 +394,9 @@ module ShieldedAccessControl { // If the path is valid we assert it’s a path for the queried leaf (not some other leaf). if (hasCommitment) { - assert(roleCommitmentPath.leaf == roleCommitment, - "ShieldedAccessControl: Path must contain queried role commitment" - ); + assert(roleCommitmentPath.leaf == roleCommitment, + "ShieldedAccessControl: Path must contain queried role commitment" + ); } const roleNullifier = _computeNullifier(roleCommitment); @@ -408,9 +408,9 @@ module ShieldedAccessControl { // If the path is valid we assert it’s a path for the queried leaf (not some other leaf). if (isRevoked) { - assert(nullifierCommitmentPath.leaf == roleNullifier, - "ShieldedAccessControl: Path must contain queried role nullifier" - ); + assert(nullifierCommitmentPath.leaf == roleNullifier, + "ShieldedAccessControl: Path must contain queried role nullifier" + ); } const hasRole = hasCommitment && !isRevoked; From 287f7bb14c501b83e20dc05775741dd18bde64b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Mon, 2 Mar 2026 13:01:23 -0500 Subject: [PATCH 234/322] Update default Merkle tree value --- .../src/access/witnesses/ShieldedAccessControlWitnesses.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts b/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts index 52fcaf18..a9896fbe 100644 --- a/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts +++ b/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts @@ -7,9 +7,6 @@ import type { MerkleTreePath, } from '../../../artifacts/MockShieldedAccessControl/contract/index.js'; -const COMMITMENT_DOMAIN = new Uint8Array(32); -new TextEncoder().encodeInto('ShieldedAccessControl:commitment', COMMITMENT_DOMAIN); - /** * @description Interface defining the witness methods for ShieldedAccessControl operations. * @template P - The private state type. @@ -104,7 +101,7 @@ export const ShieldedAccessControlPrivateState = { ); const defaultPath: MerkleTreePath = { leaf: new Uint8Array(32), - path: Array.from({ length: 10 }, () => ({ + path: Array.from({ length: 20 }, () => ({ sibling: { field: 0n }, goes_left: false, })), @@ -121,7 +118,7 @@ export const ShieldedAccessControlPrivateState = { ); const defaultPath: MerkleTreePath = { leaf: new Uint8Array(32), - path: Array.from({ length: 10 }, () => ({ + path: Array.from({ length: 20 }, () => ({ sibling: { field: 0n }, goes_left: false, })), From 8aedd8806d828c9a3731a36d887d7ceef1ce9f42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Mon, 2 Mar 2026 20:03:18 -0500 Subject: [PATCH 235/322] Simplify return statement --- contracts/src/access/ShieldedAccessControl.compact | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index dd82340d..a3315d70 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -415,11 +415,7 @@ module ShieldedAccessControl { const hasRole = hasCommitment && !isRevoked; - if (hasRole) { - return RoleCheck { hasRole, isRevoked }; - } else { - return RoleCheck { hasRole, isRevoked }; - } + return RoleCheck { hasRole, isRevoked }; } /** From d80091be660421f57f0cdd95e39d688181e290e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Mon, 2 Mar 2026 20:17:57 -0500 Subject: [PATCH 236/322] fmt files --- .../access/test/ShieldedAccessControl.test.ts | 597 ++++++++++-------- .../ShieldedAccessControlSimulator.ts | 73 +-- .../ShieldedAccessControlWitnesses.ts | 9 +- contracts/test-utils/address.ts | 10 +- 4 files changed, 370 insertions(+), 319 deletions(-) diff --git a/contracts/src/access/test/ShieldedAccessControl.test.ts b/contracts/src/access/test/ShieldedAccessControl.test.ts index 4c471a2d..8c1ad129 100644 --- a/contracts/src/access/test/ShieldedAccessControl.test.ts +++ b/contracts/src/access/test/ShieldedAccessControl.test.ts @@ -1,33 +1,79 @@ import { + CompactTypeBytes, + CompactTypeVector, convertFieldToBytes, - MerkleTreePath, - WitnessContext, + type MerkleTreePath, + persistentHash, + type WitnessContext, } from '@midnight-ntwrk/compact-runtime'; import { beforeEach, describe, expect, it } from 'vitest'; -import { +import * as utils from '#test-utils/address.js'; +import type { Ledger, - ShieldedAccessControl_Role as Role, + ShieldedAccessControl_RoleCheck as RoleCheck, ZswapCoinPublicKey, } from '../../../artifacts/MockShieldedAccessControl/contract/index.js'; import { ShieldedAccessControlPrivateState } from '../witnesses/ShieldedAccessControlWitnesses.js'; import { ShieldedAccessControlSimulator } from './simulators/ShieldedAccessControlSimulator.js'; -import * as utils from '#test-utils/address.js'; const INSTANCE_SALT = new Uint8Array(32).fill(48473095); +const COMMITMENT_DOMAIN = 'ShieldedAccessControl:commitment'; +const NULLIFIER_DOMAIN = 'ShieldedAccessControl:nullifier'; +const ACCOUNT_DOMAIN = 'ShieldedAccessControl:accountId'; const DEFAULT_MT_PATH: MerkleTreePath = { leaf: new Uint8Array(32), - path: Array.from({ length: 10 }, () => ({ + path: Array.from({ length: 20 }, () => ({ sibling: { field: 0n }, goes_left: false, })), }; -const RETURN_BAD_PATH = (ctx: WitnessContext, commitment: Uint8Array): [ShieldedAccessControlPrivateState, MerkleTreePath] => { +const RETURN_BAD_PATH = ( + ctx: WitnessContext, + _commitment: Uint8Array, +): [ShieldedAccessControlPrivateState, MerkleTreePath] => { return [ctx.privateState, DEFAULT_MT_PATH]; -} +}; + +// Helpers +const buildAccountIdHash = ( + pk: ZswapCoinPublicKey, + nonce: Uint8Array, +): Uint8Array => { + const rt_type = new CompactTypeVector(4, new CompactTypeBytes(32)); + + const bPK = pk.bytes; + const bDomain = new TextEncoder().encode(ACCOUNT_DOMAIN); + return persistentHash(rt_type, [bPK, nonce, INSTANCE_SALT, bDomain]); +}; + +const buildRoleCommitmentHash = ( + roleId: Uint8Array, + accountId: Uint8Array, +): Uint8Array => { + const rt_type = new CompactTypeVector(4, new CompactTypeBytes(32)); + const bDomain = new TextEncoder().encode(COMMITMENT_DOMAIN); + + const commitment = persistentHash(rt_type, [ + roleId, + accountId, + INSTANCE_SALT, + bDomain, + ]); + return commitment; +}; + +const buildNullifierHash = (commitment: Uint8Array): Uint8Array => { + const rt_type = new CompactTypeVector(2, new CompactTypeBytes(32)); + const bDomain = new TextEncoder().encode(NULLIFIER_DOMAIN); + + const nullifier = persistentHash(rt_type, [commitment, bDomain]); + return nullifier; +}; class ShieldedAccessControlConstant { + baseString: string; publicKey: string; zPublicKey: ZswapCoinPublicKey; roleId: Buffer; @@ -35,16 +81,16 @@ class ShieldedAccessControlConstant { roleNullifier: Uint8Array; roleCommitment: Uint8Array; secretNonce: Buffer; - shieldedAccessControl = new ShieldedAccessControlSimulator(INSTANCE_SALT, true); constructor(baseString: string, roleIdentifier: bigint) { + this.baseString = baseString; [this.publicKey, this.zPublicKey] = utils.generatePubKeyPair(baseString); - this.secretNonce = Buffer.alloc(32, baseString + "_NONCE"); - this.accountId = this.shieldedAccessControl._computeAccountId(utils.createEitherTestUser(baseString), this.secretNonce); + this.secretNonce = Buffer.alloc(32, `${baseString}_NONCE`); + this.accountId = buildAccountIdHash(this.zPublicKey, this.secretNonce); this.roleId = Buffer.from(convertFieldToBytes(32, roleIdentifier, '')); - this.roleCommitment = this.shieldedAccessControl._computeRoleCommitment(this.roleId, this.accountId); - this.roleNullifier = this.shieldedAccessControl._computeNullifier(this.roleCommitment); - }; + this.roleCommitment = buildRoleCommitmentHash(this.roleId, this.accountId); + this.roleNullifier = buildNullifierHash(this.roleCommitment); + } } // PKs @@ -52,13 +98,15 @@ const ADMIN = new ShieldedAccessControlConstant('ADMIN', 0n); const OPERATOR_1 = new ShieldedAccessControlConstant('OPERATOR_1', 1n); const OPERATOR_2 = new ShieldedAccessControlConstant('OPERATOR_2', 2n); const OPERATOR_3 = new ShieldedAccessControlConstant('OPERATOR_3', 3n); -const UNAUTHORIZED = new ShieldedAccessControlConstant('UNAUTHORIZED', 99999999n); +const UNAUTHORIZED = new ShieldedAccessControlConstant( + 'UNAUTHORIZED', + 99999999n, +); const UNINITIALIZED = new ShieldedAccessControlConstant('UNINITIALIZED', 555n); const BAD_INPUT = new ShieldedAccessControlConstant('BAD_INPUT', 666n); let shieldedAccessControl: ShieldedAccessControlSimulator; - describe('ShieldedAccessControl', () => { describe('when not initialized correctly', () => { const isNotInit = false; @@ -69,12 +117,15 @@ describe('ShieldedAccessControl', () => { isNotInit, ); }); - type FailingCircuits = [method: keyof ShieldedAccessControlSimulator, args: unknown[]]; + type FailingCircuits = [ + method: keyof ShieldedAccessControlSimulator, + args: unknown[], + ]; // Circuit calls should fail before the args are used const circuitsToFail: FailingCircuits[] = [ ['callerHasRole', [UNINITIALIZED.roleId]], ['assertOnlyRole', [UNINITIALIZED.roleId]], - ['computeRole', [UNINITIALIZED.roleId, UNINITIALIZED.accountId]], + ['_checkRole', [UNINITIALIZED.roleId, UNINITIALIZED.accountId]], ['getRoleAdmin', [UNINITIALIZED.roleId]], ['grantRole', [UNINITIALIZED.roleId, UNINITIALIZED.accountId]], ['revokeRole', [UNINITIALIZED.roleId, UNINITIALIZED.accountId]], @@ -82,24 +133,27 @@ describe('ShieldedAccessControl', () => { ['_setRoleAdmin', [UNINITIALIZED.roleId, UNINITIALIZED.roleId]], ['_grantRole', [UNINITIALIZED.roleId, UNINITIALIZED.accountId]], ['_revokeRole', [UNINITIALIZED.roleId, UNINITIALIZED.accountId]], - ['_computeRoleCommitment', [UNINITIALIZED.roleId, UNINITIALIZED.accountId]], - + [ + '_computeRoleCommitment', + [UNINITIALIZED.roleId, UNINITIALIZED.accountId], + ], + [ + '_computeAccountId', + [ + utils.createEitherTestUser(UNINITIALIZED.baseString), + UNINITIALIZED.accountId, + ], + ], ]; it.each(circuitsToFail)('%s should fail', (circuitName, args) => { expect(() => { - (shieldedAccessControl[circuitName] as (...args: unknown[]) => unknown)(...args); + (shieldedAccessControl[circuitName] as (...args: unknown[]) => unknown)( + ...args, + ); }).toThrow('Initializable: contract not initialized'); }); - it('should allow pure computeAccountId', () => { - const eitherAdmin = utils.createEitherTestUser('ADMIN'); - - expect(() => { - shieldedAccessControl._computeAccountId(eitherAdmin, ADMIN.secretNonce); - }).not.toThrow(); - }); - - it('should allow pure computeNullifier', () => { + it('should allow pure _computeNullifier', () => { expect(() => { shieldedAccessControl._computeNullifier(ADMIN.roleCommitment); }).not.toThrow(); @@ -111,127 +165,67 @@ describe('ShieldedAccessControl', () => { beforeEach(() => { // Create private state object and generate nonce - const PS = ShieldedAccessControlPrivateState.withRoleAndNonce(ADMIN.roleId, ADMIN.secretNonce); + const PS = ShieldedAccessControlPrivateState.withRoleAndNonce( + ADMIN.roleId, + ADMIN.secretNonce, + ); // Deploy contract with derived owner commitment and PS - shieldedAccessControl = new ShieldedAccessControlSimulator(INSTANCE_SALT, isInit, { - privateState: PS, - }); - }); - - describe('circuits should fail for authorized caller with invalid witness values', () => { - beforeEach(() => { - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); - shieldedAccessControl.as(ADMIN.publicKey); - }); - - type CheckedCircuitCases = [ - method: keyof ShieldedAccessControlSimulator, - isValidNonce: boolean, - isValidPath: boolean, - args: unknown[], - ]; - const checkedCircuits: CheckedCircuitCases[] = [ - ['assertOnlyRole', false, true, [ADMIN.roleId]], - ['assertOnlyRole', true, false, [ADMIN.roleId]], - ['assertOnlyRole', false, false, [ADMIN.roleId]], - ['grantRole', false, true, [ADMIN.roleId, ADMIN.accountId]], - ['grantRole', true, false, [ADMIN.roleId, ADMIN.accountId]], - ['grantRole', false, false, [ADMIN.roleId, ADMIN.accountId]], - ['revokeRole', true, false, [ADMIN.roleId, ADMIN.accountId]], - ['revokeRole', false, true, [ADMIN.roleId, ADMIN.accountId]], - ['revokeRole', false, false, [ADMIN.roleId, ADMIN.accountId]], - ]; - - it.each(checkedCircuits)( - '%s should fail with isValidNonce=%s, isValidPath=%s', - (circuitName, isValidNonce, isValidPath, args) => { - - if (isValidPath) { - // Check path matches - const truePath = shieldedAccessControl.privateState.getPathWithFindForLeaf(ADMIN.roleCommitment); - const witnessCalculatedPath = shieldedAccessControl.privateState.getPathWithWitnessImpl(ADMIN.roleCommitment); - expect(witnessCalculatedPath).toEqual(truePath); - } else { - // Check path does not match - const truePath = shieldedAccessControl.privateState.getPathWithFindForLeaf(ADMIN.roleCommitment); - - shieldedAccessControl.overrideWitness('wit_getRoleCommitmentPath', RETURN_BAD_PATH); - const witnessCalculatedPath = shieldedAccessControl.privateState.getPathWithWitnessImpl(ADMIN.roleCommitment); - - expect(witnessCalculatedPath).not.toEqual(truePath); - } - - if (isValidNonce) { - // Check nonce matches - expect( - shieldedAccessControl.privateState.getCurrentSecretNonce( - ADMIN.roleId, - ), - ).toEqual(ADMIN.secretNonce); - } else { - // Check nonce does not match - shieldedAccessControl.privateState.injectSecretNonce( - ADMIN.roleId, - UNAUTHORIZED.secretNonce, - ); - expect( - shieldedAccessControl.privateState.getCurrentSecretNonce( - ADMIN.roleId, - ), - ).not.toEqual(ADMIN.secretNonce); - } - - // Test protected circuit - expect(() => { - ( - shieldedAccessControl[circuitName] as ( - ...args: unknown[] - ) => unknown - )(...args); - }).toThrow('ShieldedAccessControl: unauthorized account'); + shieldedAccessControl = new ShieldedAccessControlSimulator( + INSTANCE_SALT, + isInit, + { + privateState: PS, }, ); }); describe('_computeRoleCommitment', () => { it('should match computed commitment', () => { - expect(shieldedAccessControl._computeRoleCommitment(ADMIN.roleId, ADMIN.accountId)).toEqual(ADMIN.roleCommitment); + expect( + shieldedAccessControl._computeRoleCommitment( + ADMIN.roleId, + ADMIN.accountId, + ), + ).toEqual(ADMIN.roleCommitment); }); - type ComputeRoleCommitmentCases = [ - isValidId: boolean, - isValidRole: boolean, + type ComputeCommitmentCases = [ + isValidRoleId: boolean, + isValidAccountId: boolean, args: unknown[], ]; - const checkedCircuits: ComputeRoleCommitmentCases[] = [ + const checkedCircuits: ComputeCommitmentCases[] = [ [false, true, [BAD_INPUT.roleId, ADMIN.accountId]], [true, false, [ADMIN.roleId, BAD_INPUT.accountId]], [false, false, [BAD_INPUT.roleId, BAD_INPUT.accountId]], - ] - - it.each(checkedCircuits)( - '%s should not compute commitment with isValidRoleId=%s, isValidAccountId=%s', - (isValidRoleId, isValidAccountId, args) => { - // Test protected circuit - expect(() => { - ( - shieldedAccessControl['_computeRoleCommitment'] as ( - ...args: unknown[] - ) => Uint8Array - )(...args); - }).not.toEqual(ADMIN.roleCommitment); - } - ) + ]; + + it.each( + checkedCircuits, + )('should not compute commitment with isValidRoleId=%s, isValidAccountId=%s', (_isValidRoleId, _isValidAccountId, args) => { + // Test protected circuit + expect(() => { + ( + shieldedAccessControl._computeRoleCommitment as ( + ...args: unknown[] + ) => Uint8Array + )(...args); + }).not.toEqual(ADMIN.roleCommitment); + }); }); describe('_computeNullifier', () => { it('should match nullifier', () => { - expect(shieldedAccessControl._computeNullifier(ADMIN.roleCommitment)).toEqual(ADMIN.roleNullifier); + expect( + shieldedAccessControl._computeNullifier(ADMIN.roleCommitment), + ).toEqual(ADMIN.roleNullifier); }); it('should not match bad commitment inputs', () => { - expect(shieldedAccessControl._computeNullifier(BAD_INPUT.roleCommitment)).not.toEqual(ADMIN.roleNullifier); + expect( + shieldedAccessControl._computeNullifier(BAD_INPUT.roleCommitment), + ).not.toEqual(ADMIN.roleNullifier); }); }); @@ -240,14 +234,25 @@ describe('ShieldedAccessControl', () => { const eitherUnauthorized = utils.createEitherTestUser('UNAUTHORIZED'); it('should match account id', () => { - expect(shieldedAccessControl._computeAccountId(eitherAdmin, ADMIN.secretNonce)).toEqual(ADMIN.accountId); + expect( + shieldedAccessControl._computeAccountId( + eitherAdmin, + ADMIN.secretNonce, + ), + ).toEqual(ADMIN.accountId); }); it('should fail for contract address', () => { - const eitherContract = utils.createEitherTestContractAddress('CONTRACT') + const eitherContract = + utils.createEitherTestContractAddress('CONTRACT'); expect(() => { - shieldedAccessControl._computeAccountId(eitherContract, ADMIN.secretNonce); - }).toThrow('ShieldedAccessControl: contract address roles are not yet supported'); + shieldedAccessControl._computeAccountId( + eitherContract, + ADMIN.secretNonce, + ); + }).toThrow( + 'ShieldedAccessControl: contract address roles are not yet supported', + ); }); type ComputeAccountIdCases = [ @@ -262,86 +267,64 @@ describe('ShieldedAccessControl', () => { [false, false, [eitherUnauthorized, UNAUTHORIZED.secretNonce]], ]; - it.each(checkedCircuits)( - '%s should not match account id with invalidAccount=%s or invalidNonce=%s', - (isValidAccount, isValidNonce, args) => { - // Test circuit - expect(() => { - ( - shieldedAccessControl['_computeAccountId'] as ( - ...args: unknown[] - ) => Uint8Array - )(...args); - }).not.toEqual(ADMIN.accountId); - } - ) + it.each( + checkedCircuits, + )('should not match account id with isValidAccount=%s or isValidNonce=%s', (_isValidAccount, _isValidNonce, args) => { + // Test circuit + expect(() => { + ( + shieldedAccessControl._computeAccountId as ( + ...args: unknown[] + ) => Uint8Array + )(...args); + }).not.toEqual(ADMIN.accountId); + }); }); - describe('computeRole', () => { + describe('_checkRole', () => { beforeEach(() => { - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId) - shieldedAccessControl.as(ADMIN.publicKey); + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl.setPersistentCaller(ADMIN.publicKey); }); - type ComputeRoleCases = [ - isBadRoleId: boolean, - isBadAccountId: boolean, + type CheckRoleCases = [ + isValidRoleId: boolean, + isValidAccountId: boolean, args: unknown[], ]; - const checkedCircuits: ComputeRoleCases[] = [ + const checkedCircuits: CheckRoleCases[] = [ [false, true, [ADMIN.roleId, BAD_INPUT.accountId]], [true, false, [BAD_INPUT.roleId, ADMIN.accountId]], [false, false, [BAD_INPUT.roleId, BAD_INPUT.accountId]], ]; - it.each(checkedCircuits)( - 'hasRole should be false with isBadRoleId(%s), isBadAccountId(%s)', - (isBadRoleId, isBadAccountId, args) => { - // Test protected circuit - expect( - ( - shieldedAccessControl['computeRole'] as ( - ...args: unknown[] - ) => Role - )(...args).hasRole - ).toBe(false); - }, - ); - - it.each(checkedCircuits)( - 'should return incorrect roleCommitment with isBadRoleId(%s), isBadAccountId(%s)', - (isBadRoleId, isBadAccountId, args) => { - // Test protected circuit - expect( - ( - shieldedAccessControl['computeRole'] as ( - ...args: unknown[] - ) => Role - )(...args).roleCommitment - ).not.toEqual(ADMIN.roleCommitment); - }, - ); - - it.each(checkedCircuits)( - 'should return incorrect roleNullifier with isBadRoleId(%s), isBadAccountId(%s)', - (isBadRoleId, isBadAccountId, args) => { - // Test protected circuit - expect( - ( - shieldedAccessControl['computeRole'] as ( - ...args: unknown[] - ) => Role - )(...args).roleNullifier - ).not.toEqual(ADMIN.roleNullifier); - }, - ); + it.each( + checkedCircuits, + )('hasRole should be false with isValidRoleId=%s isValidAccountId=%s', (_isValidRoleId, _isValidAccountId, args) => { + // Test protected circuit + expect( + ( + shieldedAccessControl._checkRole as ( + ...args: unknown[] + ) => RoleCheck + )(...args).hasRole, + ).toBe(false); + }); it('hasRole should return false if role does not exist', () => { - expect(shieldedAccessControl.computeRole(UNINITIALIZED.roleId, ADMIN.accountId).hasRole).toBe(false); + expect( + shieldedAccessControl._checkRole( + UNINITIALIZED.roleId, + ADMIN.accountId, + ).hasRole, + ).toBe(false); }); it('hasRole should return true for granted role', () => { - expect(shieldedAccessControl.computeRole(ADMIN.roleId, ADMIN.accountId).hasRole).toBe(true); + expect( + shieldedAccessControl._checkRole(ADMIN.roleId, ADMIN.accountId) + .hasRole, + ).toBe(true); }); it('hasRole should return true for accountId with multiple roles', () => { @@ -349,41 +332,57 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl._grantRole(OPERATOR_2.roleId, ADMIN.accountId); shieldedAccessControl._grantRole(OPERATOR_3.roleId, ADMIN.accountId); - expect(shieldedAccessControl.computeRole(ADMIN.roleId, ADMIN.accountId).hasRole).toBe(true); - expect(shieldedAccessControl.computeRole(OPERATOR_1.roleId, ADMIN.accountId).hasRole).toBe(true); - expect(shieldedAccessControl.computeRole(OPERATOR_2.roleId, ADMIN.accountId).hasRole).toBe(true); - expect(shieldedAccessControl.computeRole(OPERATOR_3.roleId, ADMIN.accountId).hasRole).toBe(true); + expect( + shieldedAccessControl._checkRole(ADMIN.roleId, ADMIN.accountId) + .hasRole, + ).toBe(true); + expect( + shieldedAccessControl._checkRole(OPERATOR_1.roleId, ADMIN.accountId) + .hasRole, + ).toBe(true); + expect( + shieldedAccessControl._checkRole(OPERATOR_2.roleId, ADMIN.accountId) + .hasRole, + ).toBe(true); + expect( + shieldedAccessControl._checkRole(OPERATOR_3.roleId, ADMIN.accountId) + .hasRole, + ).toBe(true); }); it('hasRole should return false for revoked role', () => { shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); - expect(shieldedAccessControl.computeRole(ADMIN.roleId, ADMIN.accountId).hasRole).toBe(false); + expect( + shieldedAccessControl._checkRole(ADMIN.roleId, ADMIN.accountId) + .hasRole, + ).toBe(false); }); it('hasRole should return false when revoked role is re-granted', () => { shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); - expect(shieldedAccessControl.computeRole(ADMIN.roleId, ADMIN.accountId).hasRole).toBe(false); + expect( + shieldedAccessControl._checkRole(ADMIN.roleId, ADMIN.accountId) + .hasRole, + ).toBe(false); }); it('hasRole should return false for bad path', () => { - shieldedAccessControl.overrideWitness('wit_getRoleCommitmentPath', RETURN_BAD_PATH); - expect(shieldedAccessControl.computeRole(ADMIN.roleId, ADMIN.accountId).hasRole).toBe(false); - }); - - it('should return correct commitment', () => { - expect(shieldedAccessControl.computeRole(ADMIN.roleId, ADMIN.accountId).roleCommitment).toEqual(ADMIN.roleCommitment); - }); - - it('should return correct nullifier', () => { - expect(shieldedAccessControl.computeRole(ADMIN.roleId, ADMIN.accountId).roleNullifier).toEqual(ADMIN.roleNullifier); + shieldedAccessControl.overrideWitness( + 'wit_getRoleCommitmentPath', + RETURN_BAD_PATH, + ); + expect( + shieldedAccessControl._checkRole(ADMIN.roleId, ADMIN.accountId) + .hasRole, + ).toBe(false); }); }); describe('assertOnlyRole', () => { beforeEach(() => { shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); - shieldedAccessControl.as(ADMIN.publicKey); + shieldedAccessControl.setPersistentCaller(ADMIN.publicKey); }); it('should not fail when authorized caller has correct nonce, and path', () => { @@ -395,8 +394,14 @@ describe('ShieldedAccessControl', () => { ).toBe(ADMIN.secretNonce); // Check path matches - const truePath = shieldedAccessControl.privateState.getPathWithFindForLeaf(ADMIN.roleCommitment); - const witnessCalculatedPath = shieldedAccessControl.privateState.getPathWithWitnessImpl(ADMIN.roleCommitment); + const truePath = + shieldedAccessControl.privateState.getCommitmentPathWithFindForLeaf( + ADMIN.roleCommitment, + ); + const witnessCalculatedPath = + shieldedAccessControl.privateState.getCommitmentPathWithWitnessImpl( + ADMIN.roleCommitment, + ); expect(witnessCalculatedPath).toEqual(truePath); expect(() => @@ -408,7 +413,7 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); expect(() => shieldedAccessControl.assertOnlyRole(ADMIN.roleId), - ).toThrow("ShieldedAccessControl: unauthorized account"); + ).toThrow('ShieldedAccessControl: unauthorized account'); }); it('should fail for revoked role with re-approval', () => { @@ -416,108 +421,154 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); expect(() => shieldedAccessControl.assertOnlyRole(ADMIN.roleId), - ).toThrow("ShieldedAccessControl: unauthorized account"); + ).toThrow('ShieldedAccessControl: unauthorized account'); }); it('should not fail for admin with multiple roles', () => { - // shieldedAccessControl.privateState.injectSecretNonce( - // OPERATOR_ROLE_1, - // OPERATOR_ROLE_1_SECRET_NONCE, - // ); - // shieldedAccessControl.privateState.injectSecretNonce( - // OPERATOR_ROLE_2, - // OPERATOR_ROLE_2_SECRET_NONCE, - // ); - // shieldedAccessControl.privateState.injectSecretNonce( - // OPERATOR_ROLE_3, - // OPERATOR_ROLE_3_SECRET_NONCE, - // ); - // shieldedAccessControl._grantRole(OPERATOR_ROLE_1, Z_ADMIN); - // shieldedAccessControl._grantRole(OPERATOR_ROLE_2, Z_ADMIN); - // shieldedAccessControl._grantRole(OPERATOR_ROLE_3, Z_ADMIN); - // expect(() => { - // shieldedAccessControl.assertOnlyRole(DEFAULT_ADMIN_ROLE); - // shieldedAccessControl.assertOnlyRole(OPERATOR_ROLE_1); - // shieldedAccessControl.assertOnlyRole(OPERATOR_ROLE_2); - // shieldedAccessControl.assertOnlyRole(OPERATOR_ROLE_3); - // }).not.toThrow(); + shieldedAccessControl.privateState.injectSecretNonce( + OPERATOR_1.roleId, + OPERATOR_1.secretNonce, + ); + // A unique accountId must be constructed for each new role using its associated secretNonce + const operator1AccountId = buildAccountIdHash( + ADMIN.zPublicKey, + OPERATOR_1.secretNonce, + ); + + shieldedAccessControl.privateState.injectSecretNonce( + OPERATOR_2.roleId, + OPERATOR_2.secretNonce, + ); + const operator2AccountId = buildAccountIdHash( + ADMIN.zPublicKey, + OPERATOR_2.secretNonce, + ); + + shieldedAccessControl.privateState.injectSecretNonce( + OPERATOR_3.roleId, + OPERATOR_3.secretNonce, + ); + const operator3AccountId = buildAccountIdHash( + ADMIN.zPublicKey, + OPERATOR_3.secretNonce, + ); + + shieldedAccessControl._grantRole(OPERATOR_1.roleId, operator1AccountId); + shieldedAccessControl._grantRole(OPERATOR_2.roleId, operator2AccountId); + shieldedAccessControl._grantRole(OPERATOR_3.roleId, operator3AccountId); + expect(() => { + shieldedAccessControl.assertOnlyRole(ADMIN.roleId); + shieldedAccessControl.assertOnlyRole(OPERATOR_1.roleId); + shieldedAccessControl.assertOnlyRole(OPERATOR_2.roleId); + shieldedAccessControl.assertOnlyRole(OPERATOR_3.roleId); + }).not.toThrow(); }); }); describe('_grantRole', () => { it('should grant role', () => { - expect(shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId)).toBe(true); + expect( + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId), + ).toBe(true); }); it('should not re-grant role', () => { shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); - const merkleRoot = shieldedAccessControl.getPublicState().ShieldedAccessControl__operatorRoles.root(); - expect(shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId)).toBe(false); - expect(shieldedAccessControl.getPublicState().ShieldedAccessControl__operatorRoles.root()).toEqual(merkleRoot); + const merkleRoot = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.root(); + expect( + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId), + ).toBe(false); + expect( + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.root(), + ).toEqual(merkleRoot); }); it('should not re-grant revoked role', () => { shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); - expect(shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId)).toBe(false); - const merkleRoot = shieldedAccessControl.getPublicState().ShieldedAccessControl__operatorRoles.root(); - expect(shieldedAccessControl.getPublicState().ShieldedAccessControl__operatorRoles.root()).toEqual(merkleRoot); + expect( + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId), + ).toBe(false); + const merkleRoot = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.root(); + expect( + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.root(), + ).toEqual(merkleRoot); }); it('should update Merkle tree root', () => { - const initialMtRoot = shieldedAccessControl.getPublicState().ShieldedAccessControl__operatorRoles.root(); + const initialMtRoot = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.root(); shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); - const updatedMtRoot = shieldedAccessControl.getPublicState().ShieldedAccessControl__operatorRoles.root(); + const updatedMtRoot = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.root(); expect(initialMtRoot).not.toEqual(updatedMtRoot); }); it('path for role commitment should exist', () => { shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); - const path = shieldedAccessControl.privateState.getPathWithFindForLeaf(ADMIN.roleCommitment); + const path = + shieldedAccessControl.privateState.getCommitmentPathWithFindForLeaf( + ADMIN.roleCommitment, + ); expect(path).not.toBe(undefined); }); }); describe('_revokeRole', () => { it('should not revoke role that does not exist', () => { - expect(shieldedAccessControl._revokeRole(UNINITIALIZED.roleId, ADMIN.accountId)).toBe(false); + expect( + shieldedAccessControl._revokeRole( + UNINITIALIZED.roleId, + ADMIN.accountId, + ), + ).toBe(false); }); it('should not re-revoke role', () => { shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); - expect(shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId)).toBe(false); + expect( + shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId), + ).toBe(false); }); it('should revoke role', () => { shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); - expect(shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId)).toBe(true); + expect( + shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId), + ).toBe(true); }); - it('should update nullifier set on revoke', () => { - const initialSize = shieldedAccessControl.getPublicState().ShieldedAccessControl__roleCommitmentNullifiers.size(); - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); - const isRevoked = shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); - expect(isRevoked).toBe(true); - - const updatedSize = shieldedAccessControl.getPublicState().ShieldedAccessControl__roleCommitmentNullifiers.size(); - const isEmpty = shieldedAccessControl.getPublicState().ShieldedAccessControl__roleCommitmentNullifiers.isEmpty(); - expect(initialSize).not.toEqual(updatedSize); - expect(isEmpty).toBe(false); + it('should update nullifier root on revoke', () => { + // const initialSize = shieldedAccessControl.getPublicState().ShieldedAccessControl__roleCommitmentNullifiers.size(); + // shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + // const isRevoked = shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + // expect(isRevoked).toBe(true); + // const updatedSize = shieldedAccessControl.getPublicState().ShieldedAccessControl__roleCommitmentNullifiers.size(); + // const isEmpty = shieldedAccessControl.getPublicState().ShieldedAccessControl__roleCommitmentNullifiers.isEmpty(); + // expect(initialSize).not.toEqual(updatedSize); + // expect(isEmpty).toBe(false); }); - it('should not update nullifier set on failed revoke', () => { - const initialSize = shieldedAccessControl.getPublicState().ShieldedAccessControl__roleCommitmentNullifiers.size(); - const isRevoked = shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); - expect(isRevoked).toBe(false); - - const updatedSize = shieldedAccessControl.getPublicState().ShieldedAccessControl__roleCommitmentNullifiers.size(); - const isEmpty = shieldedAccessControl.getPublicState().ShieldedAccessControl__roleCommitmentNullifiers.isEmpty(); - expect(initialSize).toEqual(updatedSize); - expect(isEmpty).toBe(true); + it('should not update nullifier root on failed revoke', () => { + // const initialSize = shieldedAccessControl.getPublicState().ShieldedAccessControl__roleCommitmentNullifiers.size(); + // const isRevoked = shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + // expect(isRevoked).toBe(false); + // const updatedSize = shieldedAccessControl.getPublicState().ShieldedAccessControl__roleCommitmentNullifiers.size(); + // const isEmpty = shieldedAccessControl.getPublicState().ShieldedAccessControl__roleCommitmentNullifiers.isEmpty(); + // expect(initialSize).toEqual(updatedSize); + // expect(isEmpty).toBe(true); }); }); }); - - -}); \ No newline at end of file +}); diff --git a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts index 5d3db100..6794c61d 100644 --- a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts +++ b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts @@ -1,3 +1,4 @@ +import type { MerkleTreePath } from '@midnight-ntwrk/compact-runtime'; import { type BaseSimulatorOptions, createSimulator, @@ -5,16 +6,15 @@ import { import { type ContractAddress, type Either, - type ShieldedAccessControl_RoleCheck as RoleCheck, ledger, Contract as MockShieldedAccessControl, + type ShieldedAccessControl_RoleCheck as RoleCheck, type ZswapCoinPublicKey, } from '../../../../artifacts/MockShieldedAccessControl/contract/index.js'; import { ShieldedAccessControlPrivateState, ShieldedAccessControlWitnesses, } from '../../witnesses/ShieldedAccessControlWitnesses.js'; -import { MerkleTreePath } from '@midnight-ntwrk/compact-runtime'; /** * Type constructor args @@ -65,7 +65,7 @@ export class ShieldedAccessControlSimulator extends ShieldedAccessControlSimulat public _computeAccountId( pk: Either, - nonce: Uint8Array + nonce: Uint8Array, ): Uint8Array { return this.circuits.impure._computeAccountId(pk, nonce); } @@ -74,7 +74,7 @@ export class ShieldedAccessControlSimulator extends ShieldedAccessControlSimulat return this.circuits.pure._computeNullifier(roleCommitment); } - public callerHasRole(roleId: Uint8Array): Boolean { + public callerHasRole(roleId: Uint8Array): boolean { return this.circuits.impure.callerHasRole(roleId); } @@ -109,10 +109,7 @@ export class ShieldedAccessControlSimulator extends ShieldedAccessControlSimulat * @param nonce - A private nonce to scope the commitment. * @returns The computed owner ID. */ - public grantRole( - roleId: Uint8Array, - accountId: Uint8Array - ) { + public grantRole(roleId: Uint8Array, accountId: Uint8Array) { this.circuits.impure.grantRole(roleId, accountId); } @@ -121,10 +118,7 @@ export class ShieldedAccessControlSimulator extends ShieldedAccessControlSimulat * enforcing permission checks on the caller. * @param newOwnerId - The unique identifier of the new owner calculated by `SHA256(pk, nonce)`. */ - public revokeRole( - roleId: Uint8Array, - accountId: Uint8Array - ) { + public revokeRole(roleId: Uint8Array, accountId: Uint8Array) { this.circuits.impure.revokeRole(roleId, accountId); } @@ -133,10 +127,7 @@ export class ShieldedAccessControlSimulator extends ShieldedAccessControlSimulat * enforcing permission checks on the caller. * @param newOwnerId - The unique identifier of the new owner calculated by `SHA256(pk, nonce)`. */ - public renounceRole( - roleId: Uint8Array, - callerConfirmation: Uint8Array - ) { + public renounceRole(roleId: Uint8Array, callerConfirmation: Uint8Array) { this.circuits.impure.renounceRole(roleId, callerConfirmation); } @@ -154,10 +145,7 @@ export class ShieldedAccessControlSimulator extends ShieldedAccessControlSimulat * enforcing permission checks on the caller. * @param newOwnerId - The unique identifier of the new owner calculated by `SHA256(pk, nonce)`. */ - public _grantRole( - roleId: Uint8Array, - accountId: Uint8Array - ): boolean { + public _grantRole(roleId: Uint8Array, accountId: Uint8Array): boolean { return this.circuits.impure._grantRole(roleId, accountId); } @@ -166,10 +154,7 @@ export class ShieldedAccessControlSimulator extends ShieldedAccessControlSimulat * enforcing permission checks on the caller. * @param newOwnerId - The unique identifier of the new owner calculated by `SHA256(pk, nonce)`. */ - public _revokeRole( - roleId: Uint8Array, - accountId: Uint8Array - ): boolean { + public _revokeRole(roleId: Uint8Array, accountId: Uint8Array): boolean { return this.circuits.impure._revokeRole(roleId, accountId); } @@ -199,21 +184,37 @@ export class ShieldedAccessControlSimulator extends ShieldedAccessControlSimulat */ getCurrentSecretNonce: (roleId: Uint8Array): Uint8Array => { const roleString = Buffer.from(roleId).toString('hex'); - return this.getPrivateState().roles[ - roleString - ]; + return this.getPrivateState().roles[roleString]; + }, + getCommitmentPathWithFindForLeaf: ( + roleCommitment: Uint8Array, + ): MerkleTreePath | undefined => { + return this.getPublicState().ShieldedAccessControl__operatorRoles.findPathForLeaf( + roleCommitment, + ); }, - getCommitmentPathWithFindForLeaf: (roleCommitment: Uint8Array): MerkleTreePath | undefined => { - return this.getPublicState().ShieldedAccessControl__operatorRoles.findPathForLeaf(roleCommitment); + getCommitmentPathWithWitnessImpl: ( + roleCommitment: Uint8Array, + ): MerkleTreePath => { + return this.witnesses.wit_getRoleCommitmentPath( + this.getWitnessContext(), + roleCommitment, + )[1]; }, - getCommitmentPathWithWitnessImpl: (roleCommitment: Uint8Array): MerkleTreePath => { - return this.witnesses.wit_getRoleCommitmentPath(this.getWitnessContext(), roleCommitment)[1]; + getNullifierPathWithFindForLeaf: ( + nullifierCommitment: Uint8Array, + ): MerkleTreePath | undefined => { + return this.getPublicState().ShieldedAccessControl__roleCommitmentNullifiers.findPathForLeaf( + nullifierCommitment, + ); }, - getNullifierPathWithFindForLeaf: (nullifierCommitment: Uint8Array): MerkleTreePath | undefined => { - return this.getPublicState().ShieldedAccessControl__roleCommitmentNullifiers.findPathForLeaf(nullifierCommitment); + getNullifierPathWithWitnessImpl: ( + nullifierCommitment: Uint8Array, + ): MerkleTreePath => { + return this.witnesses.wit_getCommitmentNullifierPath( + this.getWitnessContext(), + nullifierCommitment, + )[1]; }, - getNullifierPathWithWitnessImpl: (nullifierCommitment: Uint8Array): MerkleTreePath => { - return this.witnesses.wit_getCommitmentNullifierPath(this.getWitnessContext(), nullifierCommitment)[1]; - } }; } diff --git a/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts b/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts index a9896fbe..128327c0 100644 --- a/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts +++ b/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts @@ -1,7 +1,5 @@ import { getRandomValues } from 'node:crypto'; -import { - type WitnessContext, -} from '@midnight-ntwrk/compact-runtime'; +import type { WitnessContext } from '@midnight-ntwrk/compact-runtime'; import type { Ledger, MerkleTreePath, @@ -51,8 +49,7 @@ export const ShieldedAccessControlPrivateState = { * @description Generates a new private state with a random secret nonce and a default roleId of 0. * @returns A fresh ShieldedAccessControlPrivateState instance. */ - generate: ( - ): ShieldedAccessControlPrivateState => { + generate: (): ShieldedAccessControlPrivateState => { const defaultRoleId: string = Buffer.alloc(32).toString('hex'); const secretNonce = new Uint8Array(getRandomValues(Buffer.alloc(32))); @@ -125,7 +122,7 @@ export const ShieldedAccessControlPrivateState = { }; return path ? path : defaultPath; }, -} +}; /** * @description Factory function creating witness implementations for Shielded AccessControl operations. diff --git a/contracts/test-utils/address.ts b/contracts/test-utils/address.ts index 974ec1df..648e5511 100644 --- a/contracts/test-utils/address.ts +++ b/contracts/test-utils/address.ts @@ -56,7 +56,9 @@ export const encodeToAddress = (str: string): EncodedContractAddress => { * @param str String to hexify and encode. * @returns Defined Either object for ZswapCoinPublicKey. */ -export const createEitherTestUser = (str: string) => ({ +export const createEitherTestUser = ( + str: string, +): Either => ({ is_left: true, left: encodeToPK(str), right: encodeToAddress(''), @@ -78,9 +80,9 @@ const baseGeneratePubKeyPair = ( str: string, asEither: boolean, ): [ - string, - ZswapCoinPublicKey | Either, - ] => { + string, + ZswapCoinPublicKey | Either, +] => { const pk = toHexPadded(str); const zpk = asEither ? createEitherTestUser(str) : encodeToPK(str); return [pk, zpk]; From d3fa7f7d1a538c9508a8199c2edb920f48389abc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Tue, 3 Mar 2026 15:52:40 -0500 Subject: [PATCH 237/322] reorganize code, inline auth logic in _grant/revokeRole --- .../src/access/ShieldedAccessControl.compact | 404 ++++++++++-------- 1 file changed, 230 insertions(+), 174 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index a3315d70..9b4b2ac8 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -75,7 +75,8 @@ pragma language_version >= 0.21.0; * * @dev Security Considerations: * - The `secretNonce` must be kept private. Loss of the nonce prevents role holders - * and admins from proving access or transferring it. + * and admins from proving access or transferring it. Public exposure, poor nonce selection or nonce + reuse may weaken privacy guarantees and allow retroactive deanonymization. * - Role validation is entirely circuit-based using witness-provided values. * - It's strongly recommended to use cryptographically secure random values for the `_instanceSalt`. * Failure to do so could lead to the exposure of public keys. @@ -205,118 +206,6 @@ module ShieldedAccessControl { _instanceSalt = disclose(instanceSalt); } - /** - * @description Computes the role commitment from the given `accountId` and `roleId`. - * - * ## Account ID (`accountId`) - * The `accountId` is expected to be computed off-chain as: - * `accountId = SHA256(zcpk, nonce, instanceSalt, accountDomain)` - * - * - `zcpk`: The account's ZswapCoinPublicKey. - * - `nonce`: A secret nonce scoped to the role. - * - * ## Role Commitment Derivation - * `roleCommitment = SHA256(roleId, accountId, instanceSalt, commitmentDomain)` - * - * - `accountId`: See above. - * - `roleId`: A unique role identifier. - * - `instanceSalt`: A unique per-deployment salt, stored during initialization. - * This prevents commitment collisions across deployments. - * - `commitmentDomain`: Domain separator `"ShieldedAccessControl:commitment"` (padded to 32 bytes) to prevent - * hash collisions when extending the module or using similar commitment schemes. - * - * @circuitInfo k=14, rows=14853 - * - * Requirements: - * - * - Contract is initialized. - * - * @param {Bytes<32>} roleId - The unique identifier of a role. - * @param {Bytes<32>} accountId - The unique identifier of the account calculated by `SHA256(zcpk, nonce, instanceSalt, accountDomain)`. - * @returns {Bytes<32>} The commitment derived from `accountId` and `roleId`. - */ - circuit _computeRoleCommitment( - roleId: RoleIdentifier, - accountId: AccountIdentifier, - ): RoleCommitment { - Initializable_assertInitialized(); - - return persistentHash>>( - [roleId as Bytes<32>, - accountId as Bytes<32>, - _instanceSalt, - pad(32, "ShieldedAccessControl:commitment")] - ) - as RoleCommitment; - } - - /** - * @description Computes the role nullifier for a given `roleCommitment`. - * - * ## Role Nullifier Derivation - * `roleNullifier = SHA256(roleCommitment, nullifierDomain)` - * - * - `roleCommitment`: See `_computeRoleCommitment`. - * - `nullifierDomain`: Domain separator `"ShieldedAccessControl:nullifier"` (padded to 32 bytes) to prevent - * hash collisions when extending the module or using similar commitment schemes. - * - * @circuitInfo k=14, rows=14853 - * - * @param {} roleCommitment - The role commitment for a particular `roleId` | `accountId` pairing. - * @returns {Bytes<32>} roleNullifier - The associated nullifier for `roleCommitment`. - */ - pure circuit _computeNullifier(roleCommitment: RoleCommitment): RoleNullifier { - return persistentHash>>( - [roleCommitment as Bytes<32>, pad(32, "ShieldedAccessControl:nullifier")] - ) - as RoleNullifier; - } - - /** - * @description Computes the unique identifier (`accountId`) of an account from their - * ZswapCoinPublicKey and a secret nonce. - * - * ## ID Derivation - * `accountId = SHA256(zcpk, nonce, instanceSalt, accountDomain)` - * - * - `zcpk`: The ZswapCoinPublicKey of the caller. This is passed explicitly to allow - * for off-chain derivation, testing, or scenarios where the caller is - * different from the subject of the computation. We recommend using an Air-Gapped Public Key. - * - `nonce`: A secret nonce tied to the identity. The generation strategy is - * left to the user, offering different security/convenience trade-offs. - * - `instanceSalt`: A unique per-deployment salt, stored during initialization. - * This prevents commitment collisions across deployments. - * - `accountIdDomain`: Domain separator `"ShieldedAccessControl:accountId"` (padded to 32 bytes) to prevent - * hash collisions when extending the module or using similar commitment schemes. - * - * The result is a 32-byte commitment that uniquely identifies the account. - * This value is later used in role commitment hashing, - * and acts as a privacy-preserving alternative to a raw public key. - * - * @notice This module allows access to be tied to an identity commitment derived - * from a public key and secret nonce. - * While typically used with user public keys, this mechanism may also - * support contract addresses as identifiers in future contract-to-contract - * interactions. Both are treated as 32-byte values (`Bytes<32>`). - * - * Requirements: - * - * - `zcpk` is not a ContractAddress. - * - * @param {Either} zcpk - The public key of the identity being committed. - * @param {Bytes<32>} nonce - A private nonce to scope the commitment. - * @returns {Bytes<32>} accountId - The computed account ID. - */ - circuit _computeAccountId(pk: ZcpkOrContractAddress, nonce: Bytes<32>): AccountIdentifier { - Initializable_assertInitialized(); - assert(pk.is_left, "ShieldedAccessControl: contract address roles are not yet supported"); - - return persistentHash>>( - [pk.left.bytes, nonce, _instanceSalt, pad(32, "ShieldedAccessControl:accountId")] - ) - as AccountIdentifier; - } - /** * @description Returns `true` if a caller is authorized for `roleId`. * @@ -365,59 +254,6 @@ module ShieldedAccessControl { assert(callerHasRole(roleId), "ShieldedAccessControl: unauthorized account"); } - /** - * @description Computes the role commitment and associated role nullifier for a `roleId` | `accountId` - * pairing. A role commitment is valid for a `roleId` | `accountId` pairing if and only if a valid - * path exists in the `_operatorRoles` Merkle tree for the role commitment and a role nullifier doesn't - * exist for the role commitment in the `_roleCommitmentNullifiers` set. - * - * @circuitInfo k=15, rows=29780 - * - * Disclosures: - * - * - The Merkle tree path for the role commitment stored in the `_operatorRoles` Merkle tree. - * - The Merkle tree path for the nullifier commitment stored in the `_roleCommitmentNullifiers` Merkle tree. - * - * @param {Bytes<32>} roleId - The role identifier. - * @param {Bytes<32>} accountId - The unique identifier of the account. - * @return {[]} - Empty tuple. - */ - circuit _checkRole(roleId: RoleIdentifier, accountId: AccountIdentifier): RoleCheck { - Initializable_assertInitialized(); - - const roleCommitment = _computeRoleCommitment(roleId, accountId); - const roleCommitmentPath = wit_getRoleCommitmentPath(roleCommitment); - const hasCommitment = - _operatorRoles.checkRoot( - merkleTreePathRoot<20, RoleCommitment>(disclose(roleCommitmentPath)) - ); - - // If the path is valid we assert it’s a path for the queried leaf (not some other leaf). - if (hasCommitment) { - assert(roleCommitmentPath.leaf == roleCommitment, - "ShieldedAccessControl: Path must contain queried role commitment" - ); - } - - const roleNullifier = _computeNullifier(roleCommitment); - const nullifierCommitmentPath = wit_getCommitmentNullifierPath(roleNullifier); - const isRevoked = - _roleCommitmentNullifiers.checkRoot( - merkleTreePathRoot<20, RoleNullifier>(disclose(nullifierCommitmentPath)) - ); - - // If the path is valid we assert it’s a path for the queried leaf (not some other leaf). - if (isRevoked) { - assert(nullifierCommitmentPath.leaf == roleNullifier, - "ShieldedAccessControl: Path must contain queried role nullifier" - ); - } - - const hasRole = hasCommitment && !isRevoked; - - return RoleCheck { hasRole, isRevoked }; - } - /** * @description Returns the admin role that controls `roleId` or a zero * byte array if `roleId` doesn't exist. See {grantRole} and {revokeRole}. @@ -566,6 +402,8 @@ module ShieldedAccessControl { * Internal circuit without access restriction. Returns true if a role commitment doesn't exist for the * `roleId` | `accountId` pairing and a role nullifier doesn't exist for the computed role commitment. * + * @dev Auth logic is duplicated in this circuit to avoid an expensive re-computation of the role commitment + * * @circuitInfo k=17, rows=109163 * * Disclosures: @@ -581,12 +419,38 @@ module ShieldedAccessControl { export circuit _grantRole(roleId: RoleIdentifier, accountId: AccountIdentifier): Boolean { Initializable_assertInitialized(); - const roleCheck = _checkRole(roleId, accountId); - if (roleCheck.hasRole || roleCheck.isRevoked) { - return false; + const roleCommitment = _computeRoleCommitment(roleId, accountId); + const roleCommitmentPath = wit_getRoleCommitmentPath(roleCommitment); + const hasCommitment = + _operatorRoles.checkRoot( + merkleTreePathRoot<20, RoleCommitment>(disclose(roleCommitmentPath)) + ); + + // If the path is valid we assert it’s a path for the queried leaf (not some other leaf). + if (hasCommitment) { + assert(roleCommitmentPath.leaf == roleCommitment, + "ShieldedAccessControl: Path must contain queried role commitment" + ); } - const roleCommitment = _computeRoleCommitment(roleId, accountId); + const roleNullifier = _computeNullifier(roleCommitment); + const nullifierCommitmentPath = wit_getCommitmentNullifierPath(roleNullifier); + const isRevoked = + _roleCommitmentNullifiers.checkRoot( + merkleTreePathRoot<20, RoleNullifier>(disclose(nullifierCommitmentPath)) + ); + + // If the path is valid we assert it’s a path for the queried leaf (not some other leaf). + if (isRevoked) { + assert(nullifierCommitmentPath.leaf == roleNullifier, + "ShieldedAccessControl: Path must contain queried role nullifier" + ); + } + + const hasRole = hasCommitment && !isRevoked; + if (hasRole || isRevoked) { + return false; + } _operatorRoles.insert(disclose(roleCommitment)); return true; @@ -597,6 +461,8 @@ module ShieldedAccessControl { * Internal circuit without access restriction. Returns true if a role commitment exists for the * `roleId` | `accountId` pairing and a role nullifier doesn't exist for the computed role commitment. * + * @dev Auth logic is duplicated in this circuit to avoid an expensive re-computation of the role commitment and nullifier + * * @circuitInfo k=17, rows=108916 * * Disclosures: @@ -612,15 +478,205 @@ module ShieldedAccessControl { export circuit _revokeRole(roleId: RoleIdentifier, accountId: AccountIdentifier): Boolean { Initializable_assertInitialized(); - const roleCheck = _checkRole(roleId, accountId); - if (!roleCheck.hasRole || roleCheck.isRevoked) { - return false; + const roleCommitment = _computeRoleCommitment(roleId, accountId); + const roleCommitmentPath = wit_getRoleCommitmentPath(roleCommitment); + const hasCommitment = + _operatorRoles.checkRoot( + merkleTreePathRoot<20, RoleCommitment>(disclose(roleCommitmentPath)) + ); + + // If the path is valid we assert it’s a path for the queried leaf (not some other leaf). + if (hasCommitment) { + assert(roleCommitmentPath.leaf == roleCommitment, + "ShieldedAccessControl: Path must contain queried role commitment" + ); } - const roleCommitment = _computeRoleCommitment(roleId, accountId); const roleNullifier = _computeNullifier(roleCommitment); + const nullifierCommitmentPath = wit_getCommitmentNullifierPath(roleNullifier); + const isRevoked = + _roleCommitmentNullifiers.checkRoot( + merkleTreePathRoot<20, RoleNullifier>(disclose(nullifierCommitmentPath)) + ); + + // If the path is valid we assert it’s a path for the queried leaf (not some other leaf). + if (isRevoked) { + assert(nullifierCommitmentPath.leaf == roleNullifier, + "ShieldedAccessControl: Path must contain queried role nullifier" + ); + } + + const hasRole = hasCommitment && !isRevoked; + if (!hasRole || isRevoked) { + return false; + } _roleCommitmentNullifiers.insert(disclose(roleNullifier)); return true; } + + /** + * @description Computes the role commitment and associated role nullifier for a `roleId` | `accountId` + * pairing. A role commitment is valid for a `roleId` | `accountId` pairing if and only if a valid + * path exists in the `_operatorRoles` Merkle tree for the role commitment and a role nullifier doesn't + * exist for the role commitment in the `_roleCommitmentNullifiers` set. + * + * @circuitInfo k=15, rows=29780 + * + * Disclosures: + * + * - The Merkle tree path for the role commitment stored in the `_operatorRoles` Merkle tree. + * - The Merkle tree path for the nullifier commitment stored in the `_roleCommitmentNullifiers` Merkle tree. + * + * @param {Bytes<32>} roleId - The role identifier. + * @param {Bytes<32>} accountId - The unique identifier of the account. + * @return {[]} - Empty tuple. + */ + circuit _checkRole(roleId: RoleIdentifier, accountId: AccountIdentifier): RoleCheck { + Initializable_assertInitialized(); + + const roleCommitment = _computeRoleCommitment(roleId, accountId); + const roleCommitmentPath = wit_getRoleCommitmentPath(roleCommitment); + const hasCommitment = + _operatorRoles.checkRoot( + merkleTreePathRoot<20, RoleCommitment>(disclose(roleCommitmentPath)) + ); + + // If the path is valid we assert it’s a path for the queried leaf (not some other leaf). + if (hasCommitment) { + assert(roleCommitmentPath.leaf == roleCommitment, + "ShieldedAccessControl: Path must contain queried role commitment" + ); + } + + const roleNullifier = _computeNullifier(roleCommitment); + const nullifierCommitmentPath = wit_getCommitmentNullifierPath(roleNullifier); + const isRevoked = + _roleCommitmentNullifiers.checkRoot( + merkleTreePathRoot<20, RoleNullifier>(disclose(nullifierCommitmentPath)) + ); + + // If the path is valid we assert it’s a path for the queried leaf (not some other leaf). + if (isRevoked) { + assert(nullifierCommitmentPath.leaf == roleNullifier, + "ShieldedAccessControl: Path must contain queried role nullifier" + ); + } + + const hasRole = hasCommitment && !isRevoked; + + return RoleCheck { hasRole, isRevoked }; + } + + /** + * @description Computes the role commitment from the given `accountId` and `roleId`. + * + * ## Account ID (`accountId`) + * The `accountId` is expected to be computed off-chain as: + * `accountId = SHA256(zcpk, nonce, instanceSalt, accountDomain)` + * + * - `zcpk`: The account's ZswapCoinPublicKey. + * - `nonce`: A secret nonce scoped to the role. + * + * ## Role Commitment Derivation + * `roleCommitment = SHA256(roleId, accountId, instanceSalt, commitmentDomain)` + * + * - `accountId`: See above. + * - `roleId`: A unique role identifier. + * - `instanceSalt`: A unique per-deployment salt, stored during initialization. + * This prevents commitment collisions across deployments. + * - `commitmentDomain`: Domain separator `"ShieldedAccessControl:commitment"` (padded to 32 bytes) to prevent + * hash collisions when extending the module or using similar commitment schemes. + * + * @circuitInfo k=14, rows=14853 + * + * Requirements: + * + * - Contract is initialized. + * + * @param {Bytes<32>} roleId - The unique identifier of a role. + * @param {Bytes<32>} accountId - The unique identifier of the account calculated by `SHA256(zcpk, nonce, instanceSalt, accountDomain)`. + * @returns {Bytes<32>} The commitment derived from `accountId` and `roleId`. + */ + circuit _computeRoleCommitment( + roleId: RoleIdentifier, + accountId: AccountIdentifier, + ): RoleCommitment { + Initializable_assertInitialized(); + + return persistentHash>>( + [roleId as Bytes<32>, + accountId as Bytes<32>, + _instanceSalt, + pad(32, "ShieldedAccessControl:commitment")] + ) + as RoleCommitment; + } + + /** + * @description Computes the role nullifier for a given `roleCommitment`. + * + * ## Role Nullifier Derivation + * `roleNullifier = SHA256(roleCommitment, nullifierDomain)` + * + * - `roleCommitment`: See `_computeRoleCommitment`. + * - `nullifierDomain`: Domain separator `"ShieldedAccessControl:nullifier"` (padded to 32 bytes) to prevent + * hash collisions when extending the module or using similar commitment schemes. + * + * @circuitInfo k=14, rows=14853 + * + * @param {} roleCommitment - The role commitment for a particular `roleId` | `accountId` pairing. + * @returns {Bytes<32>} roleNullifier - The associated nullifier for `roleCommitment`. + */ + pure circuit _computeNullifier(roleCommitment: RoleCommitment): RoleNullifier { + return persistentHash>>( + [roleCommitment as Bytes<32>, pad(32, "ShieldedAccessControl:nullifier")] + ) + as RoleNullifier; + } + + /** + * @description Computes the unique identifier (`accountId`) of an account from their + * ZswapCoinPublicKey and a secret nonce. + * + * ## ID Derivation + * `accountId = SHA256(zcpk, nonce, instanceSalt, accountDomain)` + * + * - `zcpk`: The ZswapCoinPublicKey of the caller. This is passed explicitly to allow + * for off-chain derivation, testing, or scenarios where the caller is + * different from the subject of the computation. We recommend using an Air-Gapped Public Key. + * - `nonce`: A secret nonce tied to the identity. The generation strategy is + * left to the user, offering different security/convenience trade-offs. + * - `instanceSalt`: A unique per-deployment salt, stored during initialization. + * This prevents commitment collisions across deployments. + * - `accountIdDomain`: Domain separator `"ShieldedAccessControl:accountId"` (padded to 32 bytes) to prevent + * hash collisions when extending the module or using similar commitment schemes. + * + * The result is a 32-byte commitment that uniquely identifies the account. + * This value is later used in role commitment hashing, + * and acts as a privacy-preserving alternative to a raw public key. + * + * @notice This module allows access to be tied to an identity commitment derived + * from a public key and secret nonce. + * While typically used with user public keys, this mechanism may also + * support contract addresses as identifiers in future contract-to-contract + * interactions. Both are treated as 32-byte values (`Bytes<32>`). + * + * Requirements: + * + * - `zcpk` is not a ContractAddress. + * + * @param {Either} zcpk - The public key of the identity being committed. + * @param {Bytes<32>} nonce - A private nonce to scope the commitment. + * @returns {Bytes<32>} accountId - The computed account ID. + */ + circuit _computeAccountId(pk: ZcpkOrContractAddress, nonce: Bytes<32>): AccountIdentifier { + Initializable_assertInitialized(); + assert(pk.is_left, "ShieldedAccessControl: contract address roles are not yet supported"); + + return persistentHash>>( + [pk.left.bytes, nonce, _instanceSalt, pad(32, "ShieldedAccessControl:accountId")] + ) + as AccountIdentifier; + } } From 3a016b1b2f5b3ff4dc136a711252e124827ae7dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Wed, 4 Mar 2026 13:01:43 -0500 Subject: [PATCH 238/322] Update circuit info --- .../src/access/ShieldedAccessControl.compact | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index 9b4b2ac8..9983a4d0 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -209,7 +209,7 @@ module ShieldedAccessControl { /** * @description Returns `true` if a caller is authorized for `roleId`. * - * @circuitInfo k=16, rows=60150 + * @circuitInfo k=15, rows=22128 * * Disclosures: * @@ -234,7 +234,7 @@ module ShieldedAccessControl { /** * @description Reverts if caller is not authorized for `roleId`. * - * @circuitInfo k=15, rows=29780 + * @circuitInfo k=15, rows=22130 * * Requirements: * @@ -260,7 +260,7 @@ module ShieldedAccessControl { * * To change a role’s admin use {_setRoleAdmin}. * - * @circuitInfo k=10, rows=207 + * @circuitInfo k=9, rows=375 * * Disclosures: * @@ -288,7 +288,7 @@ module ShieldedAccessControl { * * - caller must be admin for `roleId` * - * @circuitInfo k=18, rows=138761 + * @circuitInfo k=16, rows=39993 * * Disclosures: * @@ -380,7 +380,7 @@ module ShieldedAccessControl { /** * @description Sets `adminId` as `roleId`'s admin identifier. Internal circuit without access restriction. * - * @circuitInfo k=10, rows=209 + * @circuitInfo k=10, rows=583 * * Disclosures: * @@ -402,9 +402,9 @@ module ShieldedAccessControl { * Internal circuit without access restriction. Returns true if a role commitment doesn't exist for the * `roleId` | `accountId` pairing and a role nullifier doesn't exist for the computed role commitment. * - * @dev Auth logic is duplicated in this circuit to avoid an expensive re-computation of the role commitment + * @dev Commitment and nullifier checks are inlined in this circuit to avoid an expensive re-computation of the role commitment and nullifier * - * @circuitInfo k=17, rows=109163 + * @circuitInfo k=15, rows=18115 * * Disclosures: * @@ -461,9 +461,9 @@ module ShieldedAccessControl { * Internal circuit without access restriction. Returns true if a role commitment exists for the * `roleId` | `accountId` pairing and a role nullifier doesn't exist for the computed role commitment. * - * @dev Auth logic is duplicated in this circuit to avoid an expensive re-computation of the role commitment and nullifier + * @dev Commitment and nullifier checks are inlined in this circuit to avoid an expensive re-computation of the role commitment and nullifier * - * @circuitInfo k=17, rows=108916 + * @circuitInfo k=15, rows=18115 * * Disclosures: * @@ -521,7 +521,7 @@ module ShieldedAccessControl { * path exists in the `_operatorRoles` Merkle tree for the role commitment and a role nullifier doesn't * exist for the role commitment in the `_roleCommitmentNullifiers` set. * - * @circuitInfo k=15, rows=29780 + * @circuitInfo k=14, rows=16089 * * Disclosures: * @@ -588,7 +588,7 @@ module ShieldedAccessControl { * - `commitmentDomain`: Domain separator `"ShieldedAccessControl:commitment"` (padded to 32 bytes) to prevent * hash collisions when extending the module or using similar commitment schemes. * - * @circuitInfo k=14, rows=14853 + * @circuitInfo k=13, rows=6423 * * Requirements: * @@ -623,8 +623,6 @@ module ShieldedAccessControl { * - `nullifierDomain`: Domain separator `"ShieldedAccessControl:nullifier"` (padded to 32 bytes) to prevent * hash collisions when extending the module or using similar commitment schemes. * - * @circuitInfo k=14, rows=14853 - * * @param {} roleCommitment - The role commitment for a particular `roleId` | `accountId` pairing. * @returns {Bytes<32>} roleNullifier - The associated nullifier for `roleCommitment`. */ @@ -662,6 +660,8 @@ module ShieldedAccessControl { * support contract addresses as identifiers in future contract-to-contract * interactions. Both are treated as 32-byte values (`Bytes<32>`). * + * @circuitInfo k=13, rows=6705 + * * Requirements: * * - `zcpk` is not a ContractAddress. From 3bb9e4c499865f4f74e70bdb653ccaacc8b63968 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Thu, 5 Mar 2026 11:33:44 -0500 Subject: [PATCH 239/322] WIP commit --- .../test/ShieldedAccessControl.e2e.test.ts | 46 +++++++++++++++++++ .../access/test/ShieldedAccessControl.test.ts | 1 + 2 files changed, 47 insertions(+) create mode 100644 contracts/src/access/test/ShieldedAccessControl.e2e.test.ts diff --git a/contracts/src/access/test/ShieldedAccessControl.e2e.test.ts b/contracts/src/access/test/ShieldedAccessControl.e2e.test.ts new file mode 100644 index 00000000..54879768 --- /dev/null +++ b/contracts/src/access/test/ShieldedAccessControl.e2e.test.ts @@ -0,0 +1,46 @@ +import { beforeAll, describe, it } from 'vitest' +import { createLogger } from '@midnight-ntwrk/testkit-js' +import { createTestContext } from '#test-utils/e2e-environment.js' +import { deployContract } from '@midnight-ntwrk/midnight-js-contracts'; +import { CompiledContract } from '@midnight-ntwrk/compact-js' +import { + type ContractAddress, + type Either, + ledger, + Contract as MockShieldedAccessControl, + type ShieldedAccessControl_RoleCheck as RoleCheck, + type ZswapCoinPublicKey, +} from '../../../artifacts/MockShieldedAccessControl/contract/index.js'; +import { + ShieldedAccessControlPrivateState, + ShieldedAccessControlWitnesses, +} from '../witnesses/ShieldedAccessControlWitnesses.js'; + + +const logger = createLogger('shielded_access_control_e2e') +let ctx: Awaited> + +beforeAll(async () => { + ctx = await createTestContext(logger, { + privateStateStoreName: `shielded-access-control-${Date.now()}`, + zkConfigPath: './artifacts/MockShieldedAccessControl', + }) +}) + +describe('ShieldedAccessControl e2e', () => { + it('should deploy contract [@slow]', async () => { + logger.info('Deploying ShieldedAccessControl contract...'); + const compiledShieldedAccessControl = CompiledContract.make('ShieldedAccessControl', MockShieldedAccessControl).pipe( + CompiledContract.withWitnesses(ShieldedAccessControlWitnesses()), + CompiledContract.withCompiledFileAssets('./artifacts/MockShieldedAccessControl') + ) + const counterContract = await deployContract(ctx.providers, { + compiledContract: compiledShieldedAccessControl, + args: [new Uint8Array(32).fill(48473095)], + privateStateId: 'shielded-access-control', + initialPrivateState: ShieldedAccessControlPrivateState.generate() + }); + logger.info(`Deployed contract at address: ${counterContract.deployTxData.public.contractAddress}`); + return counterContract; + }); +}) \ No newline at end of file diff --git a/contracts/src/access/test/ShieldedAccessControl.test.ts b/contracts/src/access/test/ShieldedAccessControl.test.ts index 8c1ad129..402f5231 100644 --- a/contracts/src/access/test/ShieldedAccessControl.test.ts +++ b/contracts/src/access/test/ShieldedAccessControl.test.ts @@ -331,6 +331,7 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl._grantRole(OPERATOR_1.roleId, ADMIN.accountId); shieldedAccessControl._grantRole(OPERATOR_2.roleId, ADMIN.accountId); shieldedAccessControl._grantRole(OPERATOR_3.roleId, ADMIN.accountId); + shieldedAccessControl.getContractState() expect( shieldedAccessControl._checkRole(ADMIN.roleId, ADMIN.accountId) From e3cf7e9cd71269fa52c27938cc824965b90338c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Thu, 5 Mar 2026 11:34:18 -0500 Subject: [PATCH 240/322] Revert "WIP commit" This reverts commit 3bb9e4c499865f4f74e70bdb653ccaacc8b63968. --- .../test/ShieldedAccessControl.e2e.test.ts | 46 ------------------- .../access/test/ShieldedAccessControl.test.ts | 1 - 2 files changed, 47 deletions(-) delete mode 100644 contracts/src/access/test/ShieldedAccessControl.e2e.test.ts diff --git a/contracts/src/access/test/ShieldedAccessControl.e2e.test.ts b/contracts/src/access/test/ShieldedAccessControl.e2e.test.ts deleted file mode 100644 index 54879768..00000000 --- a/contracts/src/access/test/ShieldedAccessControl.e2e.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { beforeAll, describe, it } from 'vitest' -import { createLogger } from '@midnight-ntwrk/testkit-js' -import { createTestContext } from '#test-utils/e2e-environment.js' -import { deployContract } from '@midnight-ntwrk/midnight-js-contracts'; -import { CompiledContract } from '@midnight-ntwrk/compact-js' -import { - type ContractAddress, - type Either, - ledger, - Contract as MockShieldedAccessControl, - type ShieldedAccessControl_RoleCheck as RoleCheck, - type ZswapCoinPublicKey, -} from '../../../artifacts/MockShieldedAccessControl/contract/index.js'; -import { - ShieldedAccessControlPrivateState, - ShieldedAccessControlWitnesses, -} from '../witnesses/ShieldedAccessControlWitnesses.js'; - - -const logger = createLogger('shielded_access_control_e2e') -let ctx: Awaited> - -beforeAll(async () => { - ctx = await createTestContext(logger, { - privateStateStoreName: `shielded-access-control-${Date.now()}`, - zkConfigPath: './artifacts/MockShieldedAccessControl', - }) -}) - -describe('ShieldedAccessControl e2e', () => { - it('should deploy contract [@slow]', async () => { - logger.info('Deploying ShieldedAccessControl contract...'); - const compiledShieldedAccessControl = CompiledContract.make('ShieldedAccessControl', MockShieldedAccessControl).pipe( - CompiledContract.withWitnesses(ShieldedAccessControlWitnesses()), - CompiledContract.withCompiledFileAssets('./artifacts/MockShieldedAccessControl') - ) - const counterContract = await deployContract(ctx.providers, { - compiledContract: compiledShieldedAccessControl, - args: [new Uint8Array(32).fill(48473095)], - privateStateId: 'shielded-access-control', - initialPrivateState: ShieldedAccessControlPrivateState.generate() - }); - logger.info(`Deployed contract at address: ${counterContract.deployTxData.public.contractAddress}`); - return counterContract; - }); -}) \ No newline at end of file diff --git a/contracts/src/access/test/ShieldedAccessControl.test.ts b/contracts/src/access/test/ShieldedAccessControl.test.ts index 402f5231..8c1ad129 100644 --- a/contracts/src/access/test/ShieldedAccessControl.test.ts +++ b/contracts/src/access/test/ShieldedAccessControl.test.ts @@ -331,7 +331,6 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl._grantRole(OPERATOR_1.roleId, ADMIN.accountId); shieldedAccessControl._grantRole(OPERATOR_2.roleId, ADMIN.accountId); shieldedAccessControl._grantRole(OPERATOR_3.roleId, ADMIN.accountId); - shieldedAccessControl.getContractState() expect( shieldedAccessControl._checkRole(ADMIN.roleId, ADMIN.accountId) From 1c5f803498186aa2f3d2519254fd57f5e098dca1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Thu, 5 Mar 2026 11:35:09 -0500 Subject: [PATCH 241/322] Reapply "WIP commit" This reverts commit e3cf7e9cd71269fa52c27938cc824965b90338c0. --- .../test/ShieldedAccessControl.e2e.test.ts | 46 +++++++++++++++++++ .../access/test/ShieldedAccessControl.test.ts | 1 + 2 files changed, 47 insertions(+) create mode 100644 contracts/src/access/test/ShieldedAccessControl.e2e.test.ts diff --git a/contracts/src/access/test/ShieldedAccessControl.e2e.test.ts b/contracts/src/access/test/ShieldedAccessControl.e2e.test.ts new file mode 100644 index 00000000..54879768 --- /dev/null +++ b/contracts/src/access/test/ShieldedAccessControl.e2e.test.ts @@ -0,0 +1,46 @@ +import { beforeAll, describe, it } from 'vitest' +import { createLogger } from '@midnight-ntwrk/testkit-js' +import { createTestContext } from '#test-utils/e2e-environment.js' +import { deployContract } from '@midnight-ntwrk/midnight-js-contracts'; +import { CompiledContract } from '@midnight-ntwrk/compact-js' +import { + type ContractAddress, + type Either, + ledger, + Contract as MockShieldedAccessControl, + type ShieldedAccessControl_RoleCheck as RoleCheck, + type ZswapCoinPublicKey, +} from '../../../artifacts/MockShieldedAccessControl/contract/index.js'; +import { + ShieldedAccessControlPrivateState, + ShieldedAccessControlWitnesses, +} from '../witnesses/ShieldedAccessControlWitnesses.js'; + + +const logger = createLogger('shielded_access_control_e2e') +let ctx: Awaited> + +beforeAll(async () => { + ctx = await createTestContext(logger, { + privateStateStoreName: `shielded-access-control-${Date.now()}`, + zkConfigPath: './artifacts/MockShieldedAccessControl', + }) +}) + +describe('ShieldedAccessControl e2e', () => { + it('should deploy contract [@slow]', async () => { + logger.info('Deploying ShieldedAccessControl contract...'); + const compiledShieldedAccessControl = CompiledContract.make('ShieldedAccessControl', MockShieldedAccessControl).pipe( + CompiledContract.withWitnesses(ShieldedAccessControlWitnesses()), + CompiledContract.withCompiledFileAssets('./artifacts/MockShieldedAccessControl') + ) + const counterContract = await deployContract(ctx.providers, { + compiledContract: compiledShieldedAccessControl, + args: [new Uint8Array(32).fill(48473095)], + privateStateId: 'shielded-access-control', + initialPrivateState: ShieldedAccessControlPrivateState.generate() + }); + logger.info(`Deployed contract at address: ${counterContract.deployTxData.public.contractAddress}`); + return counterContract; + }); +}) \ No newline at end of file diff --git a/contracts/src/access/test/ShieldedAccessControl.test.ts b/contracts/src/access/test/ShieldedAccessControl.test.ts index 8c1ad129..402f5231 100644 --- a/contracts/src/access/test/ShieldedAccessControl.test.ts +++ b/contracts/src/access/test/ShieldedAccessControl.test.ts @@ -331,6 +331,7 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl._grantRole(OPERATOR_1.roleId, ADMIN.accountId); shieldedAccessControl._grantRole(OPERATOR_2.roleId, ADMIN.accountId); shieldedAccessControl._grantRole(OPERATOR_3.roleId, ADMIN.accountId); + shieldedAccessControl.getContractState() expect( shieldedAccessControl._checkRole(ADMIN.roleId, ADMIN.accountId) From 8d9713a74216aa50f6f8f4297852cce90badf957 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Thu, 5 Mar 2026 11:35:35 -0500 Subject: [PATCH 242/322] WIP commit --- .../access/test/ShieldedAccessControl_OLD.ts | 897 ++++++++++-------- 1 file changed, 497 insertions(+), 400 deletions(-) diff --git a/contracts/src/access/test/ShieldedAccessControl_OLD.ts b/contracts/src/access/test/ShieldedAccessControl_OLD.ts index ececb9aa..59607ddf 100644 --- a/contracts/src/access/test/ShieldedAccessControl_OLD.ts +++ b/contracts/src/access/test/ShieldedAccessControl_OLD.ts @@ -1,3 +1,5 @@ +// biome-ignore-all lint: will delete later + import { CompactTypeBytes, CompactTypeVector, @@ -7,15 +9,19 @@ import { } from '@midnight-ntwrk/compact-runtime'; import { beforeEach, describe, expect, it } from 'vitest'; import { - ContractAddress, - Either, - Ledger, - MerkleTreePath, - ShieldedAccessControl_Role as Role, - ZswapCoinPublicKey, - Contract as MyContract + type ContractAddress, + type Either, + type Ledger, + type MerkleTreePath, + Contract as MyContract, + type ShieldedAccessControl_Role as Role, + type ZswapCoinPublicKey, } from '../../../artifacts/MockShieldedAccessControl/contract/index.cjs'; -import { fmtHexString, ShieldedAccessControlPrivateState, ShieldedAccessControlWitnesses } from '../witnesses/ShieldedAccessControlWitnesses.js'; +import { + fmtHexString, + ShieldedAccessControlPrivateState, + ShieldedAccessControlWitnesses, +} from '../witnesses/ShieldedAccessControlWitnesses.js'; import { ShieldedAccessControlSimulator } from './simulators/ShieldedAccessControlSimulator.js'; import * as utils from './utils/address.js'; @@ -47,14 +53,14 @@ const getRoleIndex = ( privateState, }: WitnessContext, roleId: Uint8Array, - account: Either + account: Either, ): bigint => { const roleIdString = Buffer.from(roleId).toString('hex'); const bNonce = privateState.roles[roleIdString]; const rt_type = new CompactTypeVector(5, new CompactTypeBytes(32)); const bAccount = utils.eitherToBytes(account); // Iterate over each MT index to determine if commitment exists - for (let i = 0; i < (2 ** 11 - 1); i++) { + for (let i = 0; i < 2 ** 11 - 1; i++) { const bIndex = convert_bigint_to_Uint8Array(32, BigInt(i)); const commitment = persistentHash(rt_type, [ roleId, @@ -81,12 +87,15 @@ const getRoleIndex = ( } } - console.log("WIT - Commitment DNE, returing MT index ", ledger.ShieldedAccessControl__currentMerkleTreeIndex.toString()); + console.log( + 'WIT - Commitment DNE, returing MT index ', + ledger.ShieldedAccessControl__currentMerkleTreeIndex.toString(), + ); // If commitment doesn't exist return currentMTIndex // Used for adding roles return ledger.ShieldedAccessControl__currentMerkleTreeIndex; -} +}; // Roles const DEFAULT_ADMIN_ROLE = utils.zeroUint8Array(); @@ -126,19 +135,11 @@ const buildCommitment = ( roleId: Uint8Array, account: Either, nonce: Uint8Array, - index: bigint, ): Uint8Array => { const rt_type = new CompactTypeVector(5, new CompactTypeBytes(32)); const bAccount = utils.eitherToBytes(account); - const bIndex = convert_bigint_to_Uint8Array(32, index); - const commitment = persistentHash(rt_type, [ - roleId, - bAccount, - nonce, - bIndex, - DOMAIN, - ]); + const commitment = persistentHash(rt_type, [roleId, bAccount, nonce, DOMAIN]); return commitment; }; @@ -147,7 +148,6 @@ const EXP_DEFAULT_ADMIN_COMMITMENT = buildCommitment( DEFAULT_ADMIN_ROLE, Z_ADMIN, ADMIN_SECRET_NONCE, - INIT_COUNTER, ); function RETURN_BAD_INDEX( @@ -227,96 +227,93 @@ describe('ShieldedAccessControl', () => { ['revokeRole', false, false, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], ]; - it.each(checkedCircuits)( - '%s should fail with isValidNonce(%s), isValidIndex(%s), isValidPath(%s)', - (circuitName, isValidNonce, isValidIndex, isValidPath, args) => { - if (isValidNonce) { - // Check nonce matches - expect( - shieldedAccessControl.privateState.getCurrentSecretNonce( - DEFAULT_ADMIN_ROLE, - ), - ).toEqual(ADMIN_SECRET_NONCE); - } else { - // Check nonce does not match - shieldedAccessControl.privateState.injectSecretNonce( + it.each( + checkedCircuits, + )('%s should fail with isValidNonce(%s), isValidIndex(%s), isValidPath(%s)', (circuitName, isValidNonce, isValidIndex, isValidPath, args) => { + if (isValidNonce) { + // Check nonce matches + expect( + shieldedAccessControl.privateState.getCurrentSecretNonce( DEFAULT_ADMIN_ROLE, - BAD_NONCE, - ); - expect( - shieldedAccessControl.privateState.getCurrentSecretNonce( - DEFAULT_ADMIN_ROLE, - ), - ).not.toEqual(ADMIN_SECRET_NONCE); - } + ), + ).toEqual(ADMIN_SECRET_NONCE); + } else { + // Check nonce does not match + shieldedAccessControl.privateState.injectSecretNonce( + DEFAULT_ADMIN_ROLE, + BAD_NONCE, + ); + expect( + shieldedAccessControl.privateState.getCurrentSecretNonce( + DEFAULT_ADMIN_ROLE, + ), + ).not.toEqual(ADMIN_SECRET_NONCE); + } - if (isValidIndex) { - // Check index matches - const [, witnessCalculatedIndex] = - shieldedAccessControl.witnesses.wit_getRoleIndex( - shieldedAccessControl.getWitnessContext(), - DEFAULT_ADMIN_ROLE, - Z_ADMIN - ); - expect(witnessCalculatedIndex).toBe(INIT_COUNTER); - } else { - // Check index does not match - shieldedAccessControl.overrideWitness( - 'wit_getRoleIndex', - RETURN_BAD_INDEX, + if (isValidIndex) { + // Check index matches + const [, witnessCalculatedIndex] = + shieldedAccessControl.witnesses.wit_getRoleIndex( + shieldedAccessControl.getWitnessContext(), + DEFAULT_ADMIN_ROLE, + Z_ADMIN, ); - const [, witnessCalculatedIndex] = - shieldedAccessControl.witnesses.wit_getRoleIndex( - shieldedAccessControl.getWitnessContext(), - DEFAULT_ADMIN_ROLE, - Z_ADMIN - ); - expect(witnessCalculatedIndex).not.toBe(INIT_COUNTER); - } + expect(witnessCalculatedIndex).toBe(INIT_COUNTER); + } else { + // Check index does not match + shieldedAccessControl.overrideWitness( + 'wit_getRoleIndex', + RETURN_BAD_INDEX, + ); + const [, witnessCalculatedIndex] = + shieldedAccessControl.witnesses.wit_getRoleIndex( + shieldedAccessControl.getWitnessContext(), + DEFAULT_ADMIN_ROLE, + Z_ADMIN, + ); + expect(witnessCalculatedIndex).not.toBe(INIT_COUNTER); + } - if (isValidPath) { - // Check path matches - const truePath = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.findPathForLeaf( - EXP_DEFAULT_ADMIN_COMMITMENT, - ); - const [, witnessCalculatedPath] = - shieldedAccessControl.witnesses.wit_getRoleCommitmentPath( - shieldedAccessControl.getWitnessContext(), - EXP_DEFAULT_ADMIN_COMMITMENT, - ); - expect(witnessCalculatedPath).toEqual(truePath); - } else { - // Check path does not match - const truePath = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.findPathForLeaf( - EXP_DEFAULT_ADMIN_COMMITMENT, - ); - shieldedAccessControl.overrideWitness( - 'wit_getRoleCommitmentPath', - RETURN_BAD_PATH, + if (isValidPath) { + // Check path matches + const truePath = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf( + EXP_DEFAULT_ADMIN_COMMITMENT, ); - const [, witnessCalculatedPath] = - shieldedAccessControl.witnesses.wit_getRoleIndex( - shieldedAccessControl.getWitnessContext(), - DEFAULT_ADMIN_ROLE, - Z_ADMIN - ); - expect(witnessCalculatedPath).not.toEqual(truePath); - } + const [, witnessCalculatedPath] = + shieldedAccessControl.witnesses.wit_getRoleCommitmentPath( + shieldedAccessControl.getWitnessContext(), + EXP_DEFAULT_ADMIN_COMMITMENT, + ); + expect(witnessCalculatedPath).toEqual(truePath); + } else { + // Check path does not match + const truePath = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf( + EXP_DEFAULT_ADMIN_COMMITMENT, + ); + shieldedAccessControl.overrideWitness( + 'wit_getRoleCommitmentPath', + RETURN_BAD_PATH, + ); + const [, witnessCalculatedPath] = + shieldedAccessControl.witnesses.wit_getRoleIndex( + shieldedAccessControl.getWitnessContext(), + DEFAULT_ADMIN_ROLE, + Z_ADMIN, + ); + expect(witnessCalculatedPath).not.toEqual(truePath); + } - // Test protected circuit - expect(() => { - ( - shieldedAccessControl[circuitName] as ( - ...args: unknown[] - ) => unknown - )(...args); - }).toThrow('ShieldedAccessControl: unauthorized account'); - }, - ); + // Test protected circuit + expect(() => { + (shieldedAccessControl[circuitName] as (...args: unknown[]) => unknown)( + ...args, + ); + }).toThrow('ShieldedAccessControl: unauthorized account'); + }); }); describe('checked circuits should fail for unauthorized caller with any witness value', () => { @@ -359,96 +356,93 @@ describe('ShieldedAccessControl', () => { ['revokeRole', true, true, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], ]; - it.each(checkedCircuits)( - '%s should fail with isValidNonce(%s), isValidIndex(%s), isValidPath(%s)', - (circuitName, isValidNonce, isValidIndex, isValidPath, args) => { - if (isValidNonce) { - // Check nonce matches - expect( - shieldedAccessControl.privateState.getCurrentSecretNonce( - DEFAULT_ADMIN_ROLE, - ), - ).toEqual(ADMIN_SECRET_NONCE); - } else { - // Check nonce does not match - shieldedAccessControl.privateState.injectSecretNonce( + it.each( + checkedCircuits, + )('%s should fail with isValidNonce(%s), isValidIndex(%s), isValidPath(%s)', (circuitName, isValidNonce, isValidIndex, isValidPath, args) => { + if (isValidNonce) { + // Check nonce matches + expect( + shieldedAccessControl.privateState.getCurrentSecretNonce( DEFAULT_ADMIN_ROLE, - BAD_NONCE, - ); - expect( - shieldedAccessControl.privateState.getCurrentSecretNonce( - DEFAULT_ADMIN_ROLE, - ), - ).not.toEqual(ADMIN_SECRET_NONCE); - } + ), + ).toEqual(ADMIN_SECRET_NONCE); + } else { + // Check nonce does not match + shieldedAccessControl.privateState.injectSecretNonce( + DEFAULT_ADMIN_ROLE, + BAD_NONCE, + ); + expect( + shieldedAccessControl.privateState.getCurrentSecretNonce( + DEFAULT_ADMIN_ROLE, + ), + ).not.toEqual(ADMIN_SECRET_NONCE); + } - if (isValidIndex) { - // Check index matches - const [, witnessCalculatedIndex] = - shieldedAccessControl.witnesses.wit_getRoleIndex( - shieldedAccessControl.getWitnessContext(), - DEFAULT_ADMIN_ROLE, - Z_ADMIN - ); - expect(witnessCalculatedIndex).toBe(INIT_COUNTER); - } else { - // Check index does not match - shieldedAccessControl.overrideWitness( - 'wit_getRoleIndex', - RETURN_BAD_INDEX, + if (isValidIndex) { + // Check index matches + const [, witnessCalculatedIndex] = + shieldedAccessControl.witnesses.wit_getRoleIndex( + shieldedAccessControl.getWitnessContext(), + DEFAULT_ADMIN_ROLE, + Z_ADMIN, ); - const [, witnessCalculatedIndex] = - shieldedAccessControl.witnesses.wit_getRoleIndex( - shieldedAccessControl.getWitnessContext(), - DEFAULT_ADMIN_ROLE, - Z_ADMIN - ); - expect(witnessCalculatedIndex).not.toBe(INIT_COUNTER); - } + expect(witnessCalculatedIndex).toBe(INIT_COUNTER); + } else { + // Check index does not match + shieldedAccessControl.overrideWitness( + 'wit_getRoleIndex', + RETURN_BAD_INDEX, + ); + const [, witnessCalculatedIndex] = + shieldedAccessControl.witnesses.wit_getRoleIndex( + shieldedAccessControl.getWitnessContext(), + DEFAULT_ADMIN_ROLE, + Z_ADMIN, + ); + expect(witnessCalculatedIndex).not.toBe(INIT_COUNTER); + } - if (isValidPath) { - // Check path matches - const truePath = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.findPathForLeaf( - EXP_DEFAULT_ADMIN_COMMITMENT, - ); - const [, witnessCalculatedPath] = - shieldedAccessControl.witnesses.wit_getRoleCommitmentPath( - shieldedAccessControl.getWitnessContext(), - EXP_DEFAULT_ADMIN_COMMITMENT, - ); - expect(witnessCalculatedPath).toEqual(truePath); - } else { - // Check path does not match - const truePath = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.findPathForLeaf( - EXP_DEFAULT_ADMIN_COMMITMENT, - ); - shieldedAccessControl.overrideWitness( - 'wit_getRoleCommitmentPath', - RETURN_BAD_PATH, + if (isValidPath) { + // Check path matches + const truePath = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf( + EXP_DEFAULT_ADMIN_COMMITMENT, ); - const [, witnessCalculatedPath] = - shieldedAccessControl.witnesses.wit_getRoleIndex( - shieldedAccessControl.getWitnessContext(), - DEFAULT_ADMIN_ROLE, - Z_ADMIN - ); - expect(witnessCalculatedPath).not.toEqual(truePath); - } + const [, witnessCalculatedPath] = + shieldedAccessControl.witnesses.wit_getRoleCommitmentPath( + shieldedAccessControl.getWitnessContext(), + EXP_DEFAULT_ADMIN_COMMITMENT, + ); + expect(witnessCalculatedPath).toEqual(truePath); + } else { + // Check path does not match + const truePath = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf( + EXP_DEFAULT_ADMIN_COMMITMENT, + ); + shieldedAccessControl.overrideWitness( + 'wit_getRoleCommitmentPath', + RETURN_BAD_PATH, + ); + const [, witnessCalculatedPath] = + shieldedAccessControl.witnesses.wit_getRoleIndex( + shieldedAccessControl.getWitnessContext(), + DEFAULT_ADMIN_ROLE, + Z_ADMIN, + ); + expect(witnessCalculatedPath).not.toEqual(truePath); + } - // Test protected circuit - expect(() => { - ( - shieldedAccessControl[circuitName] as ( - ...args: unknown[] - ) => unknown - )(...args); - }).toThrow('ShieldedAccessControl: unauthorized account'); - }, - ); + // Test protected circuit + expect(() => { + (shieldedAccessControl[circuitName] as (...args: unknown[]) => unknown)( + ...args, + ); + }).toThrow('ShieldedAccessControl: unauthorized account'); + }); }); describe('unsupported contract address failure cases', () => { @@ -470,21 +464,18 @@ describe('ShieldedAccessControl', () => { ['_revokeRole', [DEFAULT_ADMIN_ROLE, Z_OPERATOR_CONTRACT]], ]; - it.each(circuitsWithContractAddressCheck)( - '%s fails if contract address is queried', - (circuitName, args) => { - // Test protected circuit - expect(() => { - ( - shieldedAccessControl[circuitName] as ( - ...args: unknown[] - ) => unknown - )(...args); - }).toThrow( - 'ShieldedAccessControl: contract address roles are not yet supported', + it.each( + circuitsWithContractAddressCheck, + )('%s fails if contract address is queried', (circuitName, args) => { + // Test protected circuit + expect(() => { + (shieldedAccessControl[circuitName] as (...args: unknown[]) => unknown)( + ...args, ); - }, - ); + }).toThrow( + 'ShieldedAccessControl: contract address roles are not yet supported', + ); + }); }); describe('hasRole', () => { @@ -563,181 +554,179 @@ describe('ShieldedAccessControl', () => { expect(role.isApproved).toBe(false); }); - it.each(falseCases)( - 'should return false with any invalid witness value - isValidNonce(%s), isValidIndex(%s), isValidPath(%s)', - (isValidNonce, isValidIndex, isValidPath, args) => { - if (isValidNonce) { - // Check nonce matches - expect( - shieldedAccessControl.privateState.getCurrentSecretNonce( - DEFAULT_ADMIN_ROLE, - ), - ).toEqual(ADMIN_SECRET_NONCE); - } else { - // Check nonce does not match - shieldedAccessControl.privateState.injectSecretNonce( + it.each( + falseCases, + )('should return false with any invalid witness value - isValidNonce(%s), isValidIndex(%s), isValidPath(%s)', (isValidNonce, isValidIndex, isValidPath, args) => { + if (isValidNonce) { + // Check nonce matches + expect( + shieldedAccessControl.privateState.getCurrentSecretNonce( DEFAULT_ADMIN_ROLE, - BAD_NONCE, - ); - expect( - shieldedAccessControl.privateState.getCurrentSecretNonce( - DEFAULT_ADMIN_ROLE, - ), - ).not.toEqual(ADMIN_SECRET_NONCE); - } + ), + ).toEqual(ADMIN_SECRET_NONCE); + } else { + // Check nonce does not match + shieldedAccessControl.privateState.injectSecretNonce( + DEFAULT_ADMIN_ROLE, + BAD_NONCE, + ); + expect( + shieldedAccessControl.privateState.getCurrentSecretNonce( + DEFAULT_ADMIN_ROLE, + ), + ).not.toEqual(ADMIN_SECRET_NONCE); + } - if (isValidIndex) { - // Check index matches - const [, witnessCalculatedIndex] = - shieldedAccessControl.witnesses.wit_getRoleIndex( - shieldedAccessControl.getWitnessContext(), - DEFAULT_ADMIN_ROLE, - Z_ADMIN - ); - expect(witnessCalculatedIndex).toBe(INIT_COUNTER); - } else { - // Check index does not match - shieldedAccessControl.overrideWitness( - 'wit_getRoleIndex', - RETURN_BAD_INDEX, + if (isValidIndex) { + // Check index matches + const [, witnessCalculatedIndex] = + shieldedAccessControl.witnesses.wit_getRoleIndex( + shieldedAccessControl.getWitnessContext(), + DEFAULT_ADMIN_ROLE, + Z_ADMIN, ); - const [, witnessCalculatedIndex] = - shieldedAccessControl.witnesses.wit_getRoleIndex( - shieldedAccessControl.getWitnessContext(), - DEFAULT_ADMIN_ROLE, - Z_ADMIN - ); - expect(witnessCalculatedIndex).not.toBe(INIT_COUNTER); - } + expect(witnessCalculatedIndex).toBe(INIT_COUNTER); + } else { + // Check index does not match + shieldedAccessControl.overrideWitness( + 'wit_getRoleIndex', + RETURN_BAD_INDEX, + ); + const [, witnessCalculatedIndex] = + shieldedAccessControl.witnesses.wit_getRoleIndex( + shieldedAccessControl.getWitnessContext(), + DEFAULT_ADMIN_ROLE, + Z_ADMIN, + ); + expect(witnessCalculatedIndex).not.toBe(INIT_COUNTER); + } - if (isValidPath) { - // Check path matches - const truePath = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.findPathForLeaf( - EXP_DEFAULT_ADMIN_COMMITMENT, - ); - const [, witnessCalculatedPath] = - shieldedAccessControl.witnesses.wit_getRoleCommitmentPath( - shieldedAccessControl.getWitnessContext(), - EXP_DEFAULT_ADMIN_COMMITMENT, - ); - expect(witnessCalculatedPath).toEqual(truePath); - } else { - // Check path does not match - const truePath = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.findPathForLeaf( - EXP_DEFAULT_ADMIN_COMMITMENT, - ); - shieldedAccessControl.overrideWitness( - 'wit_getRoleCommitmentPath', - RETURN_BAD_PATH, + if (isValidPath) { + // Check path matches + const truePath = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf( + EXP_DEFAULT_ADMIN_COMMITMENT, ); - const [, witnessCalculatedPath] = - shieldedAccessControl.witnesses.wit_getRoleIndex( - shieldedAccessControl.getWitnessContext(), - DEFAULT_ADMIN_ROLE, - Z_ADMIN - ); - expect(witnessCalculatedPath).not.toEqual(truePath); - } + const [, witnessCalculatedPath] = + shieldedAccessControl.witnesses.wit_getRoleCommitmentPath( + shieldedAccessControl.getWitnessContext(), + EXP_DEFAULT_ADMIN_COMMITMENT, + ); + expect(witnessCalculatedPath).toEqual(truePath); + } else { + // Check path does not match + const truePath = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf( + EXP_DEFAULT_ADMIN_COMMITMENT, + ); + shieldedAccessControl.overrideWitness( + 'wit_getRoleCommitmentPath', + RETURN_BAD_PATH, + ); + const [, witnessCalculatedPath] = + shieldedAccessControl.witnesses.wit_getRoleIndex( + shieldedAccessControl.getWitnessContext(), + DEFAULT_ADMIN_ROLE, + Z_ADMIN, + ); + expect(witnessCalculatedPath).not.toEqual(truePath); + } - // Test false case circuit - const role = ( - shieldedAccessControl['hasRole'] as (...args: unknown[]) => Role - )(...args); - expect(role.isApproved).toBe(false); - }, - ); + // Test false case circuit + const role = ( + shieldedAccessControl['hasRole'] as (...args: unknown[]) => Role + )(...args); + expect(role.isApproved).toBe(false); + }); - it.each(commitmentDoesNotMatchCases)( - 'commitment should not match with invalid nonce or index - isValidNonce(%s), isValidIndex(%s), isValidPath(%s)', - (isValidNonce, isValidIndex, isValidPath, args) => { - if (isValidNonce) { - // Check nonce matches - expect( - shieldedAccessControl.privateState.getCurrentSecretNonce( - DEFAULT_ADMIN_ROLE, - ), - ).toEqual(ADMIN_SECRET_NONCE); - } else { - // Check nonce does not match - shieldedAccessControl.privateState.injectSecretNonce( + it.each( + commitmentDoesNotMatchCases, + )('commitment should not match with invalid nonce or index - isValidNonce(%s), isValidIndex(%s), isValidPath(%s)', (isValidNonce, isValidIndex, isValidPath, args) => { + if (isValidNonce) { + // Check nonce matches + expect( + shieldedAccessControl.privateState.getCurrentSecretNonce( DEFAULT_ADMIN_ROLE, - BAD_NONCE, - ); - expect( - shieldedAccessControl.privateState.getCurrentSecretNonce( - DEFAULT_ADMIN_ROLE, - ), - ).not.toEqual(ADMIN_SECRET_NONCE); - } + ), + ).toEqual(ADMIN_SECRET_NONCE); + } else { + // Check nonce does not match + shieldedAccessControl.privateState.injectSecretNonce( + DEFAULT_ADMIN_ROLE, + BAD_NONCE, + ); + expect( + shieldedAccessControl.privateState.getCurrentSecretNonce( + DEFAULT_ADMIN_ROLE, + ), + ).not.toEqual(ADMIN_SECRET_NONCE); + } - if (isValidIndex) { - // Check index matches - const [, witnessCalculatedIndex] = - shieldedAccessControl.witnesses.wit_getRoleIndex( - shieldedAccessControl.getWitnessContext(), - DEFAULT_ADMIN_ROLE, - Z_ADMIN - ); - expect(witnessCalculatedIndex).toBe(INIT_COUNTER); - } else { - // Check index does not match - shieldedAccessControl.overrideWitness( - 'wit_getRoleIndex', - RETURN_BAD_INDEX, + if (isValidIndex) { + // Check index matches + const [, witnessCalculatedIndex] = + shieldedAccessControl.witnesses.wit_getRoleIndex( + shieldedAccessControl.getWitnessContext(), + DEFAULT_ADMIN_ROLE, + Z_ADMIN, ); - const [, witnessCalculatedIndex] = - shieldedAccessControl.witnesses.wit_getRoleIndex( - shieldedAccessControl.getWitnessContext(), - DEFAULT_ADMIN_ROLE, - Z_ADMIN - ); - expect(witnessCalculatedIndex).not.toBe(INIT_COUNTER); - } + expect(witnessCalculatedIndex).toBe(INIT_COUNTER); + } else { + // Check index does not match + shieldedAccessControl.overrideWitness( + 'wit_getRoleIndex', + RETURN_BAD_INDEX, + ); + const [, witnessCalculatedIndex] = + shieldedAccessControl.witnesses.wit_getRoleIndex( + shieldedAccessControl.getWitnessContext(), + DEFAULT_ADMIN_ROLE, + Z_ADMIN, + ); + expect(witnessCalculatedIndex).not.toBe(INIT_COUNTER); + } - if (isValidPath) { - // Check path matches - const truePath = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.findPathForLeaf( - EXP_DEFAULT_ADMIN_COMMITMENT, - ); - const [, witnessCalculatedPath] = - shieldedAccessControl.witnesses.wit_getRoleCommitmentPath( - shieldedAccessControl.getWitnessContext(), - EXP_DEFAULT_ADMIN_COMMITMENT, - ); - expect(witnessCalculatedPath).toEqual(truePath); - } else { - // Check path does not match - const truePath = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.findPathForLeaf( - EXP_DEFAULT_ADMIN_COMMITMENT, - ); - shieldedAccessControl.overrideWitness( - 'wit_getRoleCommitmentPath', - RETURN_BAD_PATH, + if (isValidPath) { + // Check path matches + const truePath = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf( + EXP_DEFAULT_ADMIN_COMMITMENT, ); - const [, witnessCalculatedPath] = - shieldedAccessControl.witnesses.wit_getRoleIndex( - shieldedAccessControl.getWitnessContext(), - DEFAULT_ADMIN_ROLE, - Z_ADMIN - ); - expect(witnessCalculatedPath).not.toEqual(truePath); - } + const [, witnessCalculatedPath] = + shieldedAccessControl.witnesses.wit_getRoleCommitmentPath( + shieldedAccessControl.getWitnessContext(), + EXP_DEFAULT_ADMIN_COMMITMENT, + ); + expect(witnessCalculatedPath).toEqual(truePath); + } else { + // Check path does not match + const truePath = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf( + EXP_DEFAULT_ADMIN_COMMITMENT, + ); + shieldedAccessControl.overrideWitness( + 'wit_getRoleCommitmentPath', + RETURN_BAD_PATH, + ); + const [, witnessCalculatedPath] = + shieldedAccessControl.witnesses.wit_getRoleIndex( + shieldedAccessControl.getWitnessContext(), + DEFAULT_ADMIN_ROLE, + Z_ADMIN, + ); + expect(witnessCalculatedPath).not.toEqual(truePath); + } - // Test false case circuit - const role = ( - shieldedAccessControl['hasRole'] as (...args: unknown[]) => Role - )(...args); - expect(role.roleCommitment).not.toEqual(EXP_DEFAULT_ADMIN_COMMITMENT); - }, - ); + // Test false case circuit + const role = ( + shieldedAccessControl['hasRole'] as (...args: unknown[]) => Role + )(...args); + expect(role.roleCommitment).not.toEqual(EXP_DEFAULT_ADMIN_COMMITMENT); + }); }); describe('assertOnlyRole', () => { @@ -761,7 +750,7 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl.witnesses.wit_getRoleIndex( shieldedAccessControl.getWitnessContext(), DEFAULT_ADMIN_ROLE, - Z_ADMIN + Z_ADMIN, ); expect(witnessCalculatedIndex).toBe(INIT_COUNTER); @@ -815,7 +804,10 @@ describe('ShieldedAccessControl', () => { it('should not throw if admin has role', () => { shieldedAccessControl.callerCtx.setCaller(OPERATOR_1); - console.log("ZswapState", shieldedAccessControl.circuitContext.currentZswapLocalState) + console.log( + 'ZswapState', + shieldedAccessControl.circuitContext.currentZswapLocalState, + ); expect(() => shieldedAccessControl._checkRole(DEFAULT_ADMIN_ROLE, Z_ADMIN), ).not.toThrow(); @@ -936,7 +928,6 @@ describe('ShieldedAccessControl', () => { ); expect(role.isApproved).toBe(true); - expect( shieldedAccessControl .getPublicState() @@ -965,60 +956,152 @@ describe('ShieldedAccessControl', () => { describe('revokeRole', () => { beforeEach(() => { shieldedAccessControl.callerCtx.setCaller(ADMIN); - console.log("TEST - Current MT Index", shieldedAccessControl.getPublicState().ShieldedAccessControl__currentMerkleTreeIndex.toString()); - console.log(shieldedAccessControl._grantRole(DEFAULT_ADMIN_ROLE, Z_ADMIN)); - console.log(shieldedAccessControl._grantRole(DEFAULT_ADMIN_ROLE, Z_ADMIN)); - console.log("TEST - ADMIN NONCE ", fmtHexString(ADMIN_SECRET_NONCE)); - console.log("TEST - OP NONCE ", fmtHexString(OPERATOR_ROLE_1_SECRET_NONCE)); - console.log("TEST - Current MT Index", shieldedAccessControl.getPublicState().ShieldedAccessControl__currentMerkleTreeIndex.toString()); + console.log( + 'TEST - Current MT Index', + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__currentMerkleTreeIndex.toString(), + ); + console.log( + shieldedAccessControl._grantRole(DEFAULT_ADMIN_ROLE, Z_ADMIN), + ); + console.log( + shieldedAccessControl._grantRole(DEFAULT_ADMIN_ROLE, Z_ADMIN), + ); + console.log('TEST - ADMIN NONCE ', fmtHexString(ADMIN_SECRET_NONCE)); + console.log( + 'TEST - OP NONCE ', + fmtHexString(OPERATOR_ROLE_1_SECRET_NONCE), + ); + console.log( + 'TEST - Current MT Index', + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__currentMerkleTreeIndex.toString(), + ); shieldedAccessControl.privateState.injectSecretNonce( OPERATOR_ROLE_1, OPERATOR_ROLE_1_SECRET_NONCE, ); shieldedAccessControl.grantRole(OPERATOR_ROLE_1, Z_OPERATOR_1); - console.log("TEST - Current MT Index", shieldedAccessControl.getPublicState().ShieldedAccessControl__currentMerkleTreeIndex.toString()); - + console.log( + 'TEST - Current MT Index', + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__currentMerkleTreeIndex.toString(), + ); }); it('admin should revoke role', () => { - expect(shieldedAccessControl.hasRole(OPERATOR_ROLE_1, Z_OPERATOR_1).isApproved).toBe(true); + expect( + shieldedAccessControl.hasRole(OPERATOR_ROLE_1, Z_OPERATOR_1).isApproved, + ).toBe(true); shieldedAccessControl.revokeRole(OPERATOR_ROLE_1, Z_OPERATOR_1); - expect(shieldedAccessControl.hasRole(OPERATOR_ROLE_1, Z_OPERATOR_1).isApproved).toBe(false); + expect( + shieldedAccessControl.hasRole(OPERATOR_ROLE_1, Z_OPERATOR_1).isApproved, + ).toBe(false); }); it('commitment should be in nullifier set', () => { - console.log("TEST - Current MT Index", shieldedAccessControl.getPublicState().ShieldedAccessControl__currentMerkleTreeIndex.toString()); - const opRoleIndex = getRoleIndex({ ledger: shieldedAccessControl.getPublicState(), privateState: shieldedAccessControl.getPrivateState(), contractAddress: shieldedAccessControl.contractAddress }, OPERATOR_ROLE_1, Z_OPERATOR_1); - const adminRoleIndex = getRoleIndex(shieldedAccessControl.getWitnessContext(), DEFAULT_ADMIN_ROLE, Z_ADMIN); - console.log("OPERATOR INDEX ", opRoleIndex.toString(10)); - console.log("ADMIN INDEX ", adminRoleIndex.toString(10)); - const expCommitmentOp = buildCommitment(OPERATOR_ROLE_1, Z_OPERATOR_1, OPERATOR_ROLE_1_SECRET_NONCE, 0n); - const expCommitmentOp2 = buildCommitment(OPERATOR_ROLE_1, Z_OPERATOR_1, OPERATOR_ROLE_1_SECRET_NONCE, 0n); - const pathToOp = shieldedAccessControl.getPublicState().ShieldedAccessControl__operatorRoles.findPathForLeaf(expCommitmentOp); - const pathToAdmin = shieldedAccessControl.getPublicState().ShieldedAccessControl__operatorRoles.findPathForLeaf(EXP_DEFAULT_ADMIN_COMMITMENT); + console.log( + 'TEST - Current MT Index', + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__currentMerkleTreeIndex.toString(), + ); + const opRoleIndex = getRoleIndex( + { + ledger: shieldedAccessControl.getPublicState(), + privateState: shieldedAccessControl.getPrivateState(), + contractAddress: shieldedAccessControl.contractAddress, + }, + OPERATOR_ROLE_1, + Z_OPERATOR_1, + ); + const adminRoleIndex = getRoleIndex( + shieldedAccessControl.getWitnessContext(), + DEFAULT_ADMIN_ROLE, + Z_ADMIN, + ); + console.log('OPERATOR INDEX ', opRoleIndex.toString(10)); + console.log('ADMIN INDEX ', adminRoleIndex.toString(10)); + const expCommitmentOp = buildCommitment( + OPERATOR_ROLE_1, + Z_OPERATOR_1, + OPERATOR_ROLE_1_SECRET_NONCE, + 0n, + ); + const expCommitmentOp2 = buildCommitment( + OPERATOR_ROLE_1, + Z_OPERATOR_1, + OPERATOR_ROLE_1_SECRET_NONCE, + 0n, + ); + const pathToOp = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf(expCommitmentOp); + const pathToAdmin = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf( + EXP_DEFAULT_ADMIN_COMMITMENT, + ); //console.log("PATH TO OP ", pathToOp); //console.log("PATH TO ADMIN ", pathToAdmin); //console.log("EXPECTED COMMITMENT ", expCommitmentOp); - const contractCommit = shieldedAccessControl.hasRole(OPERATOR_ROLE_1, Z_OPERATOR_1).roleCommitment; + const contractCommit = shieldedAccessControl.hasRole( + OPERATOR_ROLE_1, + Z_OPERATOR_1, + ).roleCommitment; //console.log("CONTRACT COMMITMENT ", contractCommit); shieldedAccessControl.revokeRole(OPERATOR_ROLE_1, Z_OPERATOR_1); - const it = shieldedAccessControl.getPublicState().ShieldedAccessControl_sanity[Symbol.iterator](); + const it = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl_sanity[Symbol.iterator](); console.log(EXP_DEFAULT_ADMIN_COMMITMENT); console.log(it.next()); - console.log(shieldedAccessControl.getPublicState().ShieldedAccessControl_sanity.member(EXP_DEFAULT_ADMIN_COMMITMENT)); - console.log(expCommitmentOp) + console.log( + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl_sanity.member(EXP_DEFAULT_ADMIN_COMMITMENT), + ); + console.log(expCommitmentOp); console.log(it.next()); - console.log(shieldedAccessControl.getPublicState().ShieldedAccessControl_sanity.member(expCommitmentOp)); - expect(shieldedAccessControl.getPublicState().ShieldedAccessControl__roleCommitmentNullifiers.isEmpty()).toBe(false); - expect(shieldedAccessControl.getPublicState().ShieldedAccessControl__roleCommitmentNullifiers.member(expCommitmentOp)).toBe(true); + console.log( + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl_sanity.member(expCommitmentOp), + ); + expect( + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.isEmpty(), + ).toBe(false); + expect( + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.member( + expCommitmentOp, + ), + ).toBe(true); }); it('admin should revoke multiple roles', () => { - const expCommitment = buildCommitment(OPERATOR_ROLE_1, Z_OPERATOR_1, OPERATOR_ROLE_1_SECRET_NONCE, 1n); + const expCommitment = buildCommitment( + OPERATOR_ROLE_1, + Z_OPERATOR_1, + OPERATOR_ROLE_1_SECRET_NONCE, + 1n, + ); shieldedAccessControl.revokeRole(OPERATOR_ROLE_1, Z_OPERATOR_1); - expect(shieldedAccessControl.getPublicState().ShieldedAccessControl__roleCommitmentNullifiers.member(expCommitment)).toBe(true); + expect( + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.member( + expCommitment, + ), + ).toBe(true); for (let i = 1; i < OPERATOR_ROLE_LIST.length; i++) { shieldedAccessControl.privateState.injectSecretNonce( @@ -1030,9 +1113,23 @@ describe('ShieldedAccessControl', () => { OPERATOR_ROLE_LIST[i], Z_OPERATOR_LIST[j], ); - const expCommitment = buildCommitment(OPERATOR_ROLE_LIST[i], Z_OPERATOR_LIST[j], OPERATOR_ROLE_SECRET_NONCES[i], BigInt(1 + i)); - shieldedAccessControl.revokeRole(OPERATOR_ROLE_LIST[i], Z_OPERATOR_LIST[j]); - expect(shieldedAccessControl.getPublicState().ShieldedAccessControl__roleCommitmentNullifiers.member(expCommitment)).toBe(true); + const expCommitment = buildCommitment( + OPERATOR_ROLE_LIST[i], + Z_OPERATOR_LIST[j], + OPERATOR_ROLE_SECRET_NONCES[i], + BigInt(1 + i), + ); + shieldedAccessControl.revokeRole( + OPERATOR_ROLE_LIST[i], + Z_OPERATOR_LIST[j], + ); + expect( + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.member( + expCommitment, + ), + ).toBe(true); } } }); From ecbe0daa0941cd561d14688140282a1362771dc3 Mon Sep 17 00:00:00 2001 From: Pepe Blasco Date: Fri, 6 Mar 2026 10:53:20 +0100 Subject: [PATCH 243/322] Add tests for shielded access control --- .../access/test/ShieldedAccessControl.test.ts | 379 +++++++++++++++++- 1 file changed, 364 insertions(+), 15 deletions(-) diff --git a/contracts/src/access/test/ShieldedAccessControl.test.ts b/contracts/src/access/test/ShieldedAccessControl.test.ts index 8c1ad129..31c45be9 100644 --- a/contracts/src/access/test/ShieldedAccessControl.test.ts +++ b/contracts/src/access/test/ShieldedAccessControl.test.ts @@ -550,24 +550,373 @@ describe('ShieldedAccessControl', () => { }); it('should update nullifier root on revoke', () => { - // const initialSize = shieldedAccessControl.getPublicState().ShieldedAccessControl__roleCommitmentNullifiers.size(); - // shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); - // const isRevoked = shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); - // expect(isRevoked).toBe(true); - // const updatedSize = shieldedAccessControl.getPublicState().ShieldedAccessControl__roleCommitmentNullifiers.size(); - // const isEmpty = shieldedAccessControl.getPublicState().ShieldedAccessControl__roleCommitmentNullifiers.isEmpty(); - // expect(initialSize).not.toEqual(updatedSize); - // expect(isEmpty).toBe(false); + const initialNullifierRoot = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.root(); + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + const isRevoked = shieldedAccessControl._revokeRole( + ADMIN.roleId, + ADMIN.accountId, + ); + expect(isRevoked).toBe(true); + const updatedNullifierRoot = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.root(); + expect(initialNullifierRoot).not.toEqual(updatedNullifierRoot); }); it('should not update nullifier root on failed revoke', () => { - // const initialSize = shieldedAccessControl.getPublicState().ShieldedAccessControl__roleCommitmentNullifiers.size(); - // const isRevoked = shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); - // expect(isRevoked).toBe(false); - // const updatedSize = shieldedAccessControl.getPublicState().ShieldedAccessControl__roleCommitmentNullifiers.size(); - // const isEmpty = shieldedAccessControl.getPublicState().ShieldedAccessControl__roleCommitmentNullifiers.isEmpty(); - // expect(initialSize).toEqual(updatedSize); - // expect(isEmpty).toBe(true); + const initialNullifierRoot = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.root(); + const isRevoked = shieldedAccessControl._revokeRole( + ADMIN.roleId, + ADMIN.accountId, + ); + expect(isRevoked).toBe(false); + const updatedNullifierRoot = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.root(); + expect(initialNullifierRoot).toEqual(updatedNullifierRoot); + }); + + it('path for role nullifier should exist after revoke', () => { + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + const path = + shieldedAccessControl.privateState.getNullifierPathWithFindForLeaf( + ADMIN.roleNullifier, + ); + expect(path).not.toBe(undefined); + }); + }); + + describe('callerHasRole', () => { + beforeEach(() => { + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl.setPersistentCaller(ADMIN.publicKey); + }); + + it('should return true for caller with granted role', () => { + expect(shieldedAccessControl.callerHasRole(ADMIN.roleId)).toBe(true); + }); + + it('should return false for caller without role', () => { + // The witness requires a nonce entry for the queried roleId to exist in + // private state (the runtime cannot call the circuit without it). + // Inject a nonce that was never used to grant a role, so the derived + // accountId will not match any commitment in the tree. + shieldedAccessControl.privateState.injectSecretNonce( + OPERATOR_1.roleId, + OPERATOR_1.secretNonce, + ); + expect(shieldedAccessControl.callerHasRole(OPERATOR_1.roleId)).toBe( + false, + ); + }); + + it('should return false for caller with revoked role', () => { + shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + expect(shieldedAccessControl.callerHasRole(ADMIN.roleId)).toBe(false); + }); + + it('should return false for revoked role after re-grant attempt', () => { + shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + expect(shieldedAccessControl.callerHasRole(ADMIN.roleId)).toBe(false); + }); + + it('should return false for a different caller sharing the same private state', () => { + // UNAUTHORIZED uses the same private state (ADMIN.secretNonce for ADMIN.roleId), + // so their derived accountId won't match the committed one. + shieldedAccessControl.setPersistentCaller(UNAUTHORIZED.publicKey); + expect(shieldedAccessControl.callerHasRole(ADMIN.roleId)).toBe(false); + }); + }); + + describe('assertOnlyRole — unauthorized caller', () => { + beforeEach(() => { + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + }); + + it('should fail for caller who was never granted the role', () => { + shieldedAccessControl.setPersistentCaller(UNAUTHORIZED.publicKey); + expect(() => + shieldedAccessControl.assertOnlyRole(ADMIN.roleId), + ).toThrow('ShieldedAccessControl: unauthorized account'); + }); + }); + + describe('_checkRole — isRevoked field', () => { + beforeEach(() => { + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl.setPersistentCaller(ADMIN.publicKey); + }); + + it('isRevoked should be false when role is active', () => { + expect( + shieldedAccessControl._checkRole(ADMIN.roleId, ADMIN.accountId) + .isRevoked, + ).toBe(false); + }); + + it('isRevoked should be true when role is revoked', () => { + shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + expect( + shieldedAccessControl._checkRole(ADMIN.roleId, ADMIN.accountId) + .isRevoked, + ).toBe(true); + }); + + it('isRevoked should be false when role has never been granted', () => { + expect( + shieldedAccessControl._checkRole( + UNINITIALIZED.roleId, + ADMIN.accountId, + ).isRevoked, + ).toBe(false); + }); + + it('should appear active (hasRole=true, isRevoked=false) when nullifier path witness returns a bad path', () => { + // SIMULATOR LIMITATION: in the simulator, a bad nullifier path causes + // _roleCommitmentNullifiers.checkRoot() to return false, so isRevoked=false + // and the role appears active even though it was revoked. + // In production ZK this cannot happen: an incorrect path produces an + // invalid proof that the verifier rejects. + shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl.overrideWitness( + 'wit_getCommitmentNullifierPath', + RETURN_BAD_PATH, + ); + const roleCheck = shieldedAccessControl._checkRole( + ADMIN.roleId, + ADMIN.accountId, + ); + expect(roleCheck.isRevoked).toBe(false); + expect(roleCheck.hasRole).toBe(true); + }); + }); + + describe('getRoleAdmin', () => { + it('should return zero bytes (DEFAULT_ADMIN_ROLE) for a role with no admin set', () => { + expect( + shieldedAccessControl.getRoleAdmin(OPERATOR_1.roleId), + ).toEqual(new Uint8Array(32)); + }); + + it('should return the admin role after _setRoleAdmin', () => { + shieldedAccessControl._setRoleAdmin(OPERATOR_1.roleId, ADMIN.roleId); + expect( + shieldedAccessControl.getRoleAdmin(OPERATOR_1.roleId), + ).toEqual(new Uint8Array(ADMIN.roleId)); + }); + }); + + describe('_setRoleAdmin', () => { + it('should set admin role retrievable by getRoleAdmin', () => { + shieldedAccessControl._setRoleAdmin(OPERATOR_1.roleId, ADMIN.roleId); + expect( + shieldedAccessControl.getRoleAdmin(OPERATOR_1.roleId), + ).toEqual(new Uint8Array(ADMIN.roleId)); + }); + + it('should override an existing admin role', () => { + shieldedAccessControl._setRoleAdmin(OPERATOR_1.roleId, ADMIN.roleId); + shieldedAccessControl._setRoleAdmin( + OPERATOR_1.roleId, + OPERATOR_2.roleId, + ); + expect( + shieldedAccessControl.getRoleAdmin(OPERATOR_1.roleId), + ).toEqual(new Uint8Array(OPERATOR_2.roleId)); + }); + }); + + describe('grantRole', () => { + beforeEach(() => { + // Give ADMIN the DEFAULT_ADMIN_ROLE (ADMIN.roleId === all-zero bytes === DEFAULT_ADMIN_ROLE). + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl.setPersistentCaller(ADMIN.publicKey); + }); + + it('should grant role when caller has the admin role', () => { + // DEFAULT_ADMIN_ROLE is admin of every role by default. + expect(() => + shieldedAccessControl.grantRole( + OPERATOR_1.roleId, + OPERATOR_1.accountId, + ), + ).not.toThrow(); + expect( + shieldedAccessControl._checkRole( + OPERATOR_1.roleId, + OPERATOR_1.accountId, + ).hasRole, + ).toBe(true); + }); + + it('should fail when caller does not have the admin role', () => { + shieldedAccessControl.setPersistentCaller(UNAUTHORIZED.publicKey); + expect(() => + shieldedAccessControl.grantRole( + OPERATOR_1.roleId, + OPERATOR_1.accountId, + ), + ).toThrow('ShieldedAccessControl: unauthorized account'); + }); + + it('should not re-grant role', () => { + shieldedAccessControl.grantRole(OPERATOR_1.roleId, OPERATOR_1.accountId); + const treeRoot = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.root(); + expect(() => + shieldedAccessControl.grantRole( + OPERATOR_1.roleId, + OPERATOR_1.accountId, + ), + ).not.toThrow(); + expect( + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.root(), + ).toEqual(treeRoot); + }); + + it('should grant role using a custom admin role', () => { + // Make OPERATOR_1.roleId the admin of OPERATOR_2.roleId. + shieldedAccessControl._setRoleAdmin( + OPERATOR_2.roleId, + OPERATOR_1.roleId, + ); + // Grant OPERATOR_1.roleId to OPERATOR_1 (ADMIN has DEFAULT_ADMIN_ROLE + // which is the admin of OPERATOR_1.roleId by default). + shieldedAccessControl.grantRole(OPERATOR_1.roleId, OPERATOR_1.accountId); + + // Switch to OPERATOR_1 as caller and inject their nonce for their role. + shieldedAccessControl.privateState.injectSecretNonce( + OPERATOR_1.roleId, + OPERATOR_1.secretNonce, + ); + shieldedAccessControl.setPersistentCaller(OPERATOR_1.publicKey); + + // OPERATOR_1 (who holds OPERATOR_1.roleId) can now grant OPERATOR_2.roleId. + expect(() => + shieldedAccessControl.grantRole( + OPERATOR_2.roleId, + OPERATOR_2.accountId, + ), + ).not.toThrow(); + expect( + shieldedAccessControl._checkRole( + OPERATOR_2.roleId, + OPERATOR_2.accountId, + ).hasRole, + ).toBe(true); + }); + }); + + describe('revokeRole', () => { + beforeEach(() => { + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._grantRole( + OPERATOR_1.roleId, + OPERATOR_1.accountId, + ); + shieldedAccessControl.setPersistentCaller(ADMIN.publicKey); + }); + + it('should revoke role when caller has the admin role', () => { + expect(() => + shieldedAccessControl.revokeRole( + OPERATOR_1.roleId, + OPERATOR_1.accountId, + ), + ).not.toThrow(); + expect( + shieldedAccessControl._checkRole( + OPERATOR_1.roleId, + OPERATOR_1.accountId, + ).hasRole, + ).toBe(false); + }); + + it('should fail when caller does not have the admin role', () => { + shieldedAccessControl.setPersistentCaller(UNAUTHORIZED.publicKey); + expect(() => + shieldedAccessControl.revokeRole( + OPERATOR_1.roleId, + OPERATOR_1.accountId, + ), + ).toThrow('ShieldedAccessControl: unauthorized account'); + }); + + it('should not re-revoke role', () => { + shieldedAccessControl.revokeRole(OPERATOR_1.roleId, OPERATOR_1.accountId); + const nullifierRoot = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.root(); + expect(() => + shieldedAccessControl.revokeRole( + OPERATOR_1.roleId, + OPERATOR_1.accountId, + ), + ).not.toThrow(); + expect( + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.root(), + ).toEqual(nullifierRoot); + }); + }); + + describe('renounceRole', () => { + beforeEach(() => { + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl.setPersistentCaller(ADMIN.publicKey); + }); + + it('should allow caller to renounce their own role', () => { + expect(() => + shieldedAccessControl.renounceRole(ADMIN.roleId, ADMIN.accountId), + ).not.toThrow(); + expect( + shieldedAccessControl._checkRole(ADMIN.roleId, ADMIN.accountId) + .hasRole, + ).toBe(false); + }); + + it('should fail with wrong accountId confirmation', () => { + expect(() => + shieldedAccessControl.renounceRole( + ADMIN.roleId, + OPERATOR_1.accountId, + ), + ).toThrow('ShieldedAccessControl: bad confirmation'); + }); + + it('should be a no-op when role is already revoked', () => { + shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + // renounceRole calls _revokeRole internally which silently returns false + // when the role is already revoked — no assertion, so no throw. + expect(() => + shieldedAccessControl.renounceRole(ADMIN.roleId, ADMIN.accountId), + ).not.toThrow(); + expect( + shieldedAccessControl._checkRole(ADMIN.roleId, ADMIN.accountId) + .hasRole, + ).toBe(false); + }); + + it('should update nullifier root on successful renounce', () => { + const initialNullifierRoot = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.root(); + shieldedAccessControl.renounceRole(ADMIN.roleId, ADMIN.accountId); + const updatedNullifierRoot = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.root(); + expect(initialNullifierRoot).not.toEqual(updatedNullifierRoot); }); }); }); From d08134222493197b15eb5cac4cb720c64d001c6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Fri, 6 Mar 2026 22:02:22 -0500 Subject: [PATCH 244/322] Assert instance salt is not 0 --- .../src/access/ShieldedAccessControl.compact | 1 + .../test/ShieldedAccessControl.e2e.test.ts | 46 ------------------- 2 files changed, 1 insertion(+), 46 deletions(-) delete mode 100644 contracts/src/access/test/ShieldedAccessControl.e2e.test.ts diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index 9983a4d0..e2638740 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -201,6 +201,7 @@ module ShieldedAccessControl { * @returns {[]} Empty tuple. */ export circuit initialize(instanceSalt: Bytes<32>): [] { + assert(instanceSalt != default>, "ShieldedAccessControl: Instance salt must not be 0"); Initializable_initialize(); _instanceSalt = disclose(instanceSalt); diff --git a/contracts/src/access/test/ShieldedAccessControl.e2e.test.ts b/contracts/src/access/test/ShieldedAccessControl.e2e.test.ts deleted file mode 100644 index 54879768..00000000 --- a/contracts/src/access/test/ShieldedAccessControl.e2e.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { beforeAll, describe, it } from 'vitest' -import { createLogger } from '@midnight-ntwrk/testkit-js' -import { createTestContext } from '#test-utils/e2e-environment.js' -import { deployContract } from '@midnight-ntwrk/midnight-js-contracts'; -import { CompiledContract } from '@midnight-ntwrk/compact-js' -import { - type ContractAddress, - type Either, - ledger, - Contract as MockShieldedAccessControl, - type ShieldedAccessControl_RoleCheck as RoleCheck, - type ZswapCoinPublicKey, -} from '../../../artifacts/MockShieldedAccessControl/contract/index.js'; -import { - ShieldedAccessControlPrivateState, - ShieldedAccessControlWitnesses, -} from '../witnesses/ShieldedAccessControlWitnesses.js'; - - -const logger = createLogger('shielded_access_control_e2e') -let ctx: Awaited> - -beforeAll(async () => { - ctx = await createTestContext(logger, { - privateStateStoreName: `shielded-access-control-${Date.now()}`, - zkConfigPath: './artifacts/MockShieldedAccessControl', - }) -}) - -describe('ShieldedAccessControl e2e', () => { - it('should deploy contract [@slow]', async () => { - logger.info('Deploying ShieldedAccessControl contract...'); - const compiledShieldedAccessControl = CompiledContract.make('ShieldedAccessControl', MockShieldedAccessControl).pipe( - CompiledContract.withWitnesses(ShieldedAccessControlWitnesses()), - CompiledContract.withCompiledFileAssets('./artifacts/MockShieldedAccessControl') - ) - const counterContract = await deployContract(ctx.providers, { - compiledContract: compiledShieldedAccessControl, - args: [new Uint8Array(32).fill(48473095)], - privateStateId: 'shielded-access-control', - initialPrivateState: ShieldedAccessControlPrivateState.generate() - }); - logger.info(`Deployed contract at address: ${counterContract.deployTxData.public.contractAddress}`); - return counterContract; - }); -}) \ No newline at end of file From 154938ebea51545f3aa7c49494d8ea237e55a5b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Sun, 8 Mar 2026 16:17:20 -0400 Subject: [PATCH 245/322] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Improve module documentation Co-authored-by: 0xisk Signed-off-by: ⟣ €₥ℵ∪ℓ ⟢ <34749913+emnul@users.noreply.github.com> --- .../src/access/ShieldedAccessControl.compact | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index e2638740..63b40cb7 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -6,6 +6,7 @@ pragma language_version >= 0.21.0; /** * @module Shielded AccessControl * @description A Shielded AccessControl library. + * * This module provides a shielded role-based access control (RBAC) mechanism, where roles can be used to * represent a set of permissions. Roles are stored as Merkle tree commitments to avoid * disclosing information about role holders. Role commitments are created with the following @@ -30,7 +31,7 @@ pragma language_version >= 0.21.0; * * ```compact * import CompactStandardLibrary; - * import "./node_modules/@openzeppelin-compact/accessControl/src/ShieldedAccessControl" prefix ShieldedAccessControl_; + * import "./node_modules/@openzeppelin/compact-contracts/src/access/ShieldedAccessControl" prefix ShieldedAccessControl_; * * export sealed ledger MY_ROLE: Bytes<32>; * @@ -43,8 +44,8 @@ pragma language_version >= 0.21.0; * * ```compact * circuit foo(): [] { - * assertOnlyRole(MY_ROLE); - * ... + * ShieldedAccessControl_assertOnlyRole(MY_ROLE); + * // ... rest of circuit logic * } * ``` * @@ -83,9 +84,9 @@ pragma language_version >= 0.21.0; * - The `_instanceSalt` is immutable and used to differentiate deployments. * * @notice Missing Features and Improvements: - * * - Role events * - An ERC165-like interface + * - Migrate from SHA256 to a ZK-friendly hashing function when an implementation is available. */ module ShieldedAccessControl { import CompactStandardLibrary; @@ -103,7 +104,7 @@ module ShieldedAccessControl { * @ledger _operatorRoles * @description A Merkle tree of role commitments stored as SHA256(roleId | accountId | instanceSalt | commitmentDomain) * Role commitments are derived from a public role identifier (e.g., `persistentHash>(pad(32, "MY_ROLE")`), - * an account identifier (e.g., `SHA256(zcpk, nonce, instanceSalt, accountDomain)`), the `instanceSalt`, and a domain separator. + * an account identifier (e.g., `SHA256(zcpk, nonce, instanceSalt, accountIdDomain)`), the `instanceSalt`, and a domain separator. * @type {Bytes<32>} RoleCommitment - A role commitment created by the following hash: SHA256( roleId | accountId | instanceSalt | commitmentDomain).  */ export ledger _operatorRoles: MerkleTree<20, RoleCommitment>; @@ -154,7 +155,7 @@ module ShieldedAccessControl { * @witness wit_getCommitmentNullifierPath * @description Returns a path to a role nullifier in the `_roleCommitmentNullifiers` Merkle tree if one exists. Otherwise, returns an invalid path. * - * @param {Bytes<32>} RoleNullifier - A nullifier created by the following hash: SHA256( roleCommitment | commitmentDomain). + * @param {Bytes<32>} RoleNullifier - A nullifier created by the following hash: SHA256( roleCommitment | nullifierDomain). * @return {MerkleTreePath<20, Bytes<32>>} - The Merkle tree path to `roleNullifier` in the `_roleCommitmentNullifiers` Merkle tree  */ witness wit_getCommitmentNullifierPath( @@ -165,7 +166,7 @@ module ShieldedAccessControl { * @witness wit_secretNonce * @description A private per-accountId nonce used in deriving the shielded account identifier. * - * Combined with the user's ZswapCoinPublicKey as `SHA256(zcpk, nonce, instanceSalt, accountDomain)` to produce an obfuscated, + * Combined with the user's ZswapCoinPublicKey as `SHA256(zcpk, nonce, instanceSalt, accountIdDomain)` to produce an obfuscated, * unlinkable identity commitment. Nonce MUST be unique per role to avoid cross-role linking. * * @param {Bytes<32>} roleId - The unique identifier of a role. From 3cfa7af2f609c006506ec47979c13808867d113a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Sun, 8 Mar 2026 18:02:21 -0400 Subject: [PATCH 246/322] strange compact compiler bug --- .../src/access/ShieldedAccessControl.compact | 14 ++-- .../access/test/ShieldedAccessControl.test.ts | 77 ++++++++++++++++--- 2 files changed, 74 insertions(+), 17 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index 63b40cb7..7d5ff885 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -431,7 +431,7 @@ module ShieldedAccessControl { // If the path is valid we assert it’s a path for the queried leaf (not some other leaf). if (hasCommitment) { assert(roleCommitmentPath.leaf == roleCommitment, - "ShieldedAccessControl: Path must contain queried role commitment" + "ShieldedAccessControl: Path must contain leaf matching computed role commitment for the provided roleId, accountId pairing" ); } @@ -445,7 +445,7 @@ module ShieldedAccessControl { // If the path is valid we assert it’s a path for the queried leaf (not some other leaf). if (isRevoked) { assert(nullifierCommitmentPath.leaf == roleNullifier, - "ShieldedAccessControl: Path must contain queried role nullifier" + "ShieldedAccessControl: Path must contain leaf matching computed role nullifier for the computed role commitment" ); } @@ -490,7 +490,7 @@ module ShieldedAccessControl { // If the path is valid we assert it’s a path for the queried leaf (not some other leaf). if (hasCommitment) { assert(roleCommitmentPath.leaf == roleCommitment, - "ShieldedAccessControl: Path must contain queried role commitment" + "ShieldedAccessControl: Path must contain leaf matching computed role commitment for the provided roleId, accountId pairing" ); } @@ -504,7 +504,7 @@ module ShieldedAccessControl { // If the path is valid we assert it’s a path for the queried leaf (not some other leaf). if (isRevoked) { assert(nullifierCommitmentPath.leaf == roleNullifier, - "ShieldedAccessControl: Path must contain queried role nullifier" + "ShieldedAccessControl: Path must contain leaf matching computed role nullifier for the computed role commitment" ); } @@ -521,7 +521,7 @@ module ShieldedAccessControl { * @description Computes the role commitment and associated role nullifier for a `roleId` | `accountId` * pairing. A role commitment is valid for a `roleId` | `accountId` pairing if and only if a valid * path exists in the `_operatorRoles` Merkle tree for the role commitment and a role nullifier doesn't - * exist for the role commitment in the `_roleCommitmentNullifiers` set. + * exist for the role commitment in the `_roleCommitmentNullifiers` Merkle tree. * * @circuitInfo k=14, rows=16089 * @@ -547,7 +547,7 @@ module ShieldedAccessControl { // If the path is valid we assert it’s a path for the queried leaf (not some other leaf). if (hasCommitment) { assert(roleCommitmentPath.leaf == roleCommitment, - "ShieldedAccessControl: Path must contain queried role commitment" + "ShieldedAccessControl: Path must contain leaf matching computed role commitment for the provided roleId, accountId pairing" ); } @@ -561,7 +561,7 @@ module ShieldedAccessControl { // If the path is valid we assert it’s a path for the queried leaf (not some other leaf). if (isRevoked) { assert(nullifierCommitmentPath.leaf == roleNullifier, - "ShieldedAccessControl: Path must contain queried role nullifier" + "ShieldedAccessControl: Path must contain leaf matching computed role nullifier for the computed role commitment" ); } diff --git a/contracts/src/access/test/ShieldedAccessControl.test.ts b/contracts/src/access/test/ShieldedAccessControl.test.ts index f1b91171..2acbea28 100644 --- a/contracts/src/access/test/ShieldedAccessControl.test.ts +++ b/contracts/src/access/test/ShieldedAccessControl.test.ts @@ -109,12 +109,12 @@ let shieldedAccessControl: ShieldedAccessControlSimulator; describe('ShieldedAccessControl', () => { describe('when not initialized correctly', () => { - const isNotInit = false; + const isInit = false; beforeEach(() => { shieldedAccessControl = new ShieldedAccessControlSimulator( INSTANCE_SALT, - isNotInit, + isInit, ); }); type FailingCircuits = [ @@ -158,6 +158,16 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl._computeNullifier(ADMIN.roleCommitment); }).not.toThrow(); }); + + it('should fail with 0 instanceSalt', () => { + const isInit = true; + expect(() => { + new ShieldedAccessControlSimulator( + new Uint8Array(32), + isInit, + ); + }).toThrow('ShieldedAccessControl: Instance salt must not be 0'); + }); }); describe('after initialization', () => { @@ -169,7 +179,7 @@ describe('ShieldedAccessControl', () => { ADMIN.roleId, ADMIN.secretNonce, ); - // Deploy contract with derived owner commitment and PS + // Create contract simulator with PS shieldedAccessControl = new ShieldedAccessControlSimulator( INSTANCE_SALT, isInit, @@ -288,8 +298,8 @@ describe('ShieldedAccessControl', () => { }); type CheckRoleCases = [ - isValidRoleId: boolean, - isValidAccountId: boolean, + badRoleId: boolean, + badAccountId: boolean, args: unknown[], ]; const checkedCircuits: CheckRoleCases[] = [ @@ -300,7 +310,7 @@ describe('ShieldedAccessControl', () => { it.each( checkedCircuits, - )('hasRole should be false with isValidRoleId=%s isValidAccountId=%s', (_isValidRoleId, _isValidAccountId, args) => { + )('hasRole should be false with badRoleId=%s badAccountId=%s', (_badRoleId, _badAccountId, args) => { // Test protected circuit expect( ( @@ -320,6 +330,15 @@ describe('ShieldedAccessControl', () => { ).toBe(false); }); + it('isRevoked should return false if role does not exist', () => { + expect( + shieldedAccessControl._checkRole( + UNINITIALIZED.roleId, + ADMIN.accountId, + ).isRevoked, + ).toBe(false); + }); + it('hasRole should return true for granted role', () => { expect( shieldedAccessControl._checkRole(ADMIN.roleId, ADMIN.accountId) @@ -327,11 +346,17 @@ describe('ShieldedAccessControl', () => { ).toBe(true); }); + it('isRevoked should return false for un-revoked granted role', () => { + expect( + shieldedAccessControl._checkRole(ADMIN.roleId, ADMIN.accountId) + .isRevoked, + ).toBe(false); + }); + it('hasRole should return true for accountId with multiple roles', () => { shieldedAccessControl._grantRole(OPERATOR_1.roleId, ADMIN.accountId); shieldedAccessControl._grantRole(OPERATOR_2.roleId, ADMIN.accountId); shieldedAccessControl._grantRole(OPERATOR_3.roleId, ADMIN.accountId); - shieldedAccessControl.getContractState() expect( shieldedAccessControl._checkRole(ADMIN.roleId, ADMIN.accountId) @@ -351,14 +376,22 @@ describe('ShieldedAccessControl', () => { ).toBe(true); }); - it('hasRole should return false for revoked role', () => { + it('hasRole should return false for revoked role, ', () => { shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + const roleCheck = shieldedAccessControl._checkRole(ADMIN.roleId, ADMIN.accountId); expect( - shieldedAccessControl._checkRole(ADMIN.roleId, ADMIN.accountId) - .hasRole, + roleCheck.hasRole ).toBe(false); }); + it('isRevoked should return true for revoked role, ', () => { + shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + const roleCheck = shieldedAccessControl._checkRole(ADMIN.roleId, ADMIN.accountId); + expect( + roleCheck.isRevoked + ).toBe(true); + }); + it('hasRole should return false when revoked role is re-granted', () => { shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); @@ -368,6 +401,15 @@ describe('ShieldedAccessControl', () => { ).toBe(false); }); + it('isRevoked should return true when revoked role is re-granted', () => { + shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + expect( + shieldedAccessControl._checkRole(ADMIN.roleId, ADMIN.accountId) + .isRevoked, + ).toBe(true); + }); + it('hasRole should return false for bad path', () => { shieldedAccessControl.overrideWitness( 'wit_getRoleCommitmentPath', @@ -378,6 +420,21 @@ describe('ShieldedAccessControl', () => { .hasRole, ).toBe(false); }); + + it('should fail when wit_getRoleCommitmentPath returns valid path for a different roleId, accountId pairing', () => { + shieldedAccessControl._grantRole(OPERATOR_1.roleId, OPERATOR_1.accountId); + // Override witness to return valid path for OPERATOR_1 role commitment + shieldedAccessControl.overrideWitness( + 'wit_getRoleCommitmentPath', + () => { + const privateState = shieldedAccessControl.getPrivateState(); + const operator1MtPath = shieldedAccessControl.getPublicState().ShieldedAccessControl__operatorRoles.findPathForLeaf(OPERATOR_1.roleCommitment); + if (operator1MtPath) return [privateState, operator1MtPath] + else throw new Error('Merkle tree path should be defined'); + }, + ); + expect(() => { shieldedAccessControl._checkRole(ADMIN.roleId, ADMIN.accountId) }).toThrow('ShieldedAccessControl: Path must contain leaf matching computed role commitment for the provided roleId, accountId pairing') + }); }); describe('assertOnlyRole', () => { From a5fe6d1465de6835b259be87899b4a63680bb466 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Sun, 8 Mar 2026 19:11:13 -0400 Subject: [PATCH 247/322] add tests --- .../access/test/ShieldedAccessControl.test.ts | 501 +++++++++++------- 1 file changed, 299 insertions(+), 202 deletions(-) diff --git a/contracts/src/access/test/ShieldedAccessControl.test.ts b/contracts/src/access/test/ShieldedAccessControl.test.ts index 2acbea28..572e0735 100644 --- a/contracts/src/access/test/ShieldedAccessControl.test.ts +++ b/contracts/src/access/test/ShieldedAccessControl.test.ts @@ -162,10 +162,7 @@ describe('ShieldedAccessControl', () => { it('should fail with 0 instanceSalt', () => { const isInit = true; expect(() => { - new ShieldedAccessControlSimulator( - new Uint8Array(32), - isInit, - ); + new ShieldedAccessControlSimulator(new Uint8Array(32), isInit); }).toThrow('ShieldedAccessControl: Instance salt must not be 0'); }); }); @@ -297,153 +294,303 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl.setPersistentCaller(ADMIN.publicKey); }); - type CheckRoleCases = [ - badRoleId: boolean, - badAccountId: boolean, - args: unknown[], - ]; - const checkedCircuits: CheckRoleCases[] = [ - [false, true, [ADMIN.roleId, BAD_INPUT.accountId]], - [true, false, [BAD_INPUT.roleId, ADMIN.accountId]], - [false, false, [BAD_INPUT.roleId, BAD_INPUT.accountId]], - ]; + // Test skipped due to compiler bug not updating assertion text + it.skip('should fail when wit_getRoleCommitmentPath returns valid path for a different roleId, accountId pairing', () => { + shieldedAccessControl._grantRole( + OPERATOR_1.roleId, + OPERATOR_1.accountId, + ); + // Override witness to return valid path for OPERATOR_1 role commitment + shieldedAccessControl.overrideWitness( + 'wit_getRoleCommitmentPath', + () => { + const privateState = shieldedAccessControl.getPrivateState(); + const operator1MtPath = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf( + OPERATOR_1.roleCommitment, + ); + if (operator1MtPath) return [privateState, operator1MtPath]; + throw new Error('Merkle tree path should be defined'); + }, + ); + expect(() => { + shieldedAccessControl._checkRole(ADMIN.roleId, ADMIN.accountId); + }).toThrow( + 'ShieldedAccessControl: Path must contain leaf matching computed role commitment for the provided roleId, accountId pairing', + ); + }); - it.each( - checkedCircuits, - )('hasRole should be false with badRoleId=%s badAccountId=%s', (_badRoleId, _badAccountId, args) => { - // Test protected circuit - expect( - ( - shieldedAccessControl._checkRole as ( - ...args: unknown[] - ) => RoleCheck - )(...args).hasRole, - ).toBe(false); + // Test skipped due to compiler bug not updating assertion text + it.skip('should fail when wit_getCommitmentNullifierPath returns valid path for a different nullifier', () => { + shieldedAccessControl._grantRole( + OPERATOR_1.roleId, + OPERATOR_1.accountId, + ); + shieldedAccessControl._revokeRole( + OPERATOR_1.roleId, + OPERATOR_1.accountId, + ); + + // Override witness to return valid path for OPERATOR_1 role nullifier + shieldedAccessControl.overrideWitness( + 'wit_getCommitmentNullifierPath', + () => { + const privateState = shieldedAccessControl.getPrivateState(); + const operator1MtPath = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.findPathForLeaf( + OPERATOR_1.roleNullifier, + ); + if (operator1MtPath) return [privateState, operator1MtPath]; + throw new Error('Merkle tree path should be defined'); + }, + ); + expect(() => { + shieldedAccessControl._checkRole(ADMIN.roleId, ADMIN.accountId); + }).toThrow( + 'ShieldedAccessControl: Path must contain leaf matching role nullifier for the computed role commitment', + ); }); - it('hasRole should return false if role does not exist', () => { - expect( - shieldedAccessControl._checkRole( - UNINITIALIZED.roleId, + describe('hasRole field', () => { + type CheckRoleCases = [ + badRoleId: boolean, + badAccountId: boolean, + args: unknown[], + ]; + const checkedCircuits: CheckRoleCases[] = [ + [false, true, [ADMIN.roleId, BAD_INPUT.accountId]], + [true, false, [BAD_INPUT.roleId, ADMIN.accountId]], + [false, false, [BAD_INPUT.roleId, BAD_INPUT.accountId]], + ]; + + it.each( + checkedCircuits, + )('hasRole should be false with badRoleId=%s badAccountId=%s', (_badRoleId, _badAccountId, args) => { + // Test protected circuit + expect( + ( + shieldedAccessControl._checkRole as ( + ...args: unknown[] + ) => RoleCheck + )(...args).hasRole, + ).toBe(false); + }); + + it('hasRole should return false if role does not exist', () => { + expect( + shieldedAccessControl._checkRole( + UNINITIALIZED.roleId, + ADMIN.accountId, + ).hasRole, + ).toBe(false); + }); + + it('hasRole should return true for granted role', () => { + expect( + shieldedAccessControl._checkRole(ADMIN.roleId, ADMIN.accountId) + .hasRole, + ).toBe(true); + }); + + it('hasRole should return false when revoked role is re-granted', () => { + shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + expect( + shieldedAccessControl._checkRole(ADMIN.roleId, ADMIN.accountId) + .hasRole, + ).toBe(false); + }); + + it('hasRole should return true for accountId with multiple roles', () => { + shieldedAccessControl._grantRole(OPERATOR_1.roleId, ADMIN.accountId); + shieldedAccessControl._grantRole(OPERATOR_2.roleId, ADMIN.accountId); + shieldedAccessControl._grantRole(OPERATOR_3.roleId, ADMIN.accountId); + + expect( + shieldedAccessControl._checkRole(ADMIN.roleId, ADMIN.accountId) + .hasRole, + ).toBe(true); + expect( + shieldedAccessControl._checkRole(OPERATOR_1.roleId, ADMIN.accountId) + .hasRole, + ).toBe(true); + expect( + shieldedAccessControl._checkRole(OPERATOR_2.roleId, ADMIN.accountId) + .hasRole, + ).toBe(true); + expect( + shieldedAccessControl._checkRole(OPERATOR_3.roleId, ADMIN.accountId) + .hasRole, + ).toBe(true); + }); + + it('hasRole should return false for revoked role, ', () => { + shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + const roleCheck = shieldedAccessControl._checkRole( + ADMIN.roleId, ADMIN.accountId, - ).hasRole, - ).toBe(false); - }); + ); + expect(roleCheck.hasRole).toBe(false); + }); - it('isRevoked should return false if role does not exist', () => { - expect( - shieldedAccessControl._checkRole( - UNINITIALIZED.roleId, + it('hasRole should return false for bad _operatorRoles path', () => { + shieldedAccessControl.overrideWitness( + 'wit_getRoleCommitmentPath', + RETURN_BAD_PATH, + ); + expect( + shieldedAccessControl._checkRole(ADMIN.roleId, ADMIN.accountId) + .hasRole, + ).toBe(false); + }); + }); + + describe('isRevoked field', () => { + it('isRevoked should be false when role is active', () => { + expect( + shieldedAccessControl._checkRole(ADMIN.roleId, ADMIN.accountId) + .isRevoked, + ).toBe(false); + }); + + it('isRevoked should be true when role is revoked', () => { + shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + expect( + shieldedAccessControl._checkRole(ADMIN.roleId, ADMIN.accountId) + .isRevoked, + ).toBe(true); + }); + + it('isRevoked should be false when role has never been granted', () => { + expect( + shieldedAccessControl._checkRole( + UNINITIALIZED.roleId, + ADMIN.accountId, + ).isRevoked, + ).toBe(false); + }); + + it('should appear active (hasRole=true, isRevoked=false) when nullifier path witness returns a bad path', () => { + // SIMULATOR LIMITATION: in the simulator, a bad nullifier path causes + // _roleCommitmentNullifiers.checkRoot() to return false, so isRevoked=false + // and the role appears active even though it was revoked. + // In production ZK this cannot happen: an incorrect path produces an + // invalid proof that the verifier rejects. + shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl.overrideWitness( + 'wit_getCommitmentNullifierPath', + RETURN_BAD_PATH, + ); + const roleCheck = shieldedAccessControl._checkRole( + ADMIN.roleId, ADMIN.accountId, - ).isRevoked, - ).toBe(false); + ); + expect(roleCheck.isRevoked).toBe(false); + expect(roleCheck.hasRole).toBe(true); + }); + + it('isRevoked should return true when revoked role is re-granted', () => { + shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + expect( + shieldedAccessControl._checkRole(ADMIN.roleId, ADMIN.accountId) + .isRevoked, + ).toBe(true); + }); }); + }); - it('hasRole should return true for granted role', () => { - expect( - shieldedAccessControl._checkRole(ADMIN.roleId, ADMIN.accountId) - .hasRole, - ).toBe(true); + describe('assertOnlyRole', () => { + beforeEach(() => { + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl.setPersistentCaller(ADMIN.publicKey); }); - it('isRevoked should return false for un-revoked granted role', () => { - expect( - shieldedAccessControl._checkRole(ADMIN.roleId, ADMIN.accountId) - .isRevoked, - ).toBe(false); + it('should fail for caller who was never granted the role', () => { + shieldedAccessControl.as(UNAUTHORIZED.publicKey); + expect(() => + shieldedAccessControl.assertOnlyRole(ADMIN.roleId), + ).toThrow('ShieldedAccessControl: unauthorized account'); }); - it('hasRole should return true for accountId with multiple roles', () => { - shieldedAccessControl._grantRole(OPERATOR_1.roleId, ADMIN.accountId); - shieldedAccessControl._grantRole(OPERATOR_2.roleId, ADMIN.accountId); - shieldedAccessControl._grantRole(OPERATOR_3.roleId, ADMIN.accountId); - - expect( - shieldedAccessControl._checkRole(ADMIN.roleId, ADMIN.accountId) - .hasRole, - ).toBe(true); - expect( - shieldedAccessControl._checkRole(OPERATOR_1.roleId, ADMIN.accountId) - .hasRole, - ).toBe(true); - expect( - shieldedAccessControl._checkRole(OPERATOR_2.roleId, ADMIN.accountId) - .hasRole, - ).toBe(true); + it('should not fail when authorized caller has correct nonce, and path', () => { + // Check nonce is correct expect( - shieldedAccessControl._checkRole(OPERATOR_3.roleId, ADMIN.accountId) - .hasRole, - ).toBe(true); - }); + shieldedAccessControl.privateState.getCurrentSecretNonce( + ADMIN.roleId, + ), + ).toBe(ADMIN.secretNonce); - it('hasRole should return false for revoked role, ', () => { - shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); - const roleCheck = shieldedAccessControl._checkRole(ADMIN.roleId, ADMIN.accountId); - expect( - roleCheck.hasRole - ).toBe(false); - }); + // Check path matches + const truePath = + shieldedAccessControl.privateState.getCommitmentPathWithFindForLeaf( + ADMIN.roleCommitment, + ); + const witnessCalculatedPath = + shieldedAccessControl.privateState.getCommitmentPathWithWitnessImpl( + ADMIN.roleCommitment, + ); + expect(witnessCalculatedPath).toEqual(truePath); - it('isRevoked should return true for revoked role, ', () => { - shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); - const roleCheck = shieldedAccessControl._checkRole(ADMIN.roleId, ADMIN.accountId); - expect( - roleCheck.isRevoked - ).toBe(true); + expect(() => + shieldedAccessControl.assertOnlyRole(ADMIN.roleId), + ).not.toThrow(); }); - it('hasRole should return false when revoked role is re-granted', () => { - shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + it('should fail when authorized caller has incorrect path', () => { + // Check nonce is correct expect( - shieldedAccessControl._checkRole(ADMIN.roleId, ADMIN.accountId) - .hasRole, - ).toBe(false); - }); + shieldedAccessControl.privateState.getCurrentSecretNonce( + ADMIN.roleId, + ), + ).toBe(ADMIN.secretNonce); - it('isRevoked should return true when revoked role is re-granted', () => { - shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); - expect( - shieldedAccessControl._checkRole(ADMIN.roleId, ADMIN.accountId) - .isRevoked, - ).toBe(true); + // Check path does not match + const truePath = + shieldedAccessControl.privateState.getCommitmentPathWithFindForLeaf( + ADMIN.roleCommitment, + ); + shieldedAccessControl.overrideWitness('wit_getRoleCommitmentPath', RETURN_BAD_PATH); + const witnessCalculatedPath = + shieldedAccessControl.privateState.getCommitmentPathWithWitnessImpl( + ADMIN.roleCommitment, + ); + expect(witnessCalculatedPath).not.toEqual(truePath); + + expect(() => + shieldedAccessControl.assertOnlyRole(ADMIN.roleId), + ).toThrow('ShieldedAccessControl: unauthorized account'); }); - it('hasRole should return false for bad path', () => { - shieldedAccessControl.overrideWitness( - 'wit_getRoleCommitmentPath', - RETURN_BAD_PATH, - ); + it('should fail when authorized caller has incorrect nonce', () => { + shieldedAccessControl.privateState.injectSecretNonce(ADMIN.roleId, UNAUTHORIZED.secretNonce); + + // Check nonce is incorrect expect( - shieldedAccessControl._checkRole(ADMIN.roleId, ADMIN.accountId) - .hasRole, - ).toBe(false); - }); + shieldedAccessControl.privateState.getCurrentSecretNonce( + ADMIN.roleId, + ), + ).not.toBe(ADMIN.secretNonce); - it('should fail when wit_getRoleCommitmentPath returns valid path for a different roleId, accountId pairing', () => { - shieldedAccessControl._grantRole(OPERATOR_1.roleId, OPERATOR_1.accountId); - // Override witness to return valid path for OPERATOR_1 role commitment - shieldedAccessControl.overrideWitness( - 'wit_getRoleCommitmentPath', - () => { - const privateState = shieldedAccessControl.getPrivateState(); - const operator1MtPath = shieldedAccessControl.getPublicState().ShieldedAccessControl__operatorRoles.findPathForLeaf(OPERATOR_1.roleCommitment); - if (operator1MtPath) return [privateState, operator1MtPath] - else throw new Error('Merkle tree path should be defined'); - }, - ); - expect(() => { shieldedAccessControl._checkRole(ADMIN.roleId, ADMIN.accountId) }).toThrow('ShieldedAccessControl: Path must contain leaf matching computed role commitment for the provided roleId, accountId pairing') - }); - }); + // Check path matches + const truePath = + shieldedAccessControl.privateState.getCommitmentPathWithFindForLeaf( + ADMIN.roleCommitment, + ); + const witnessCalculatedPath = + shieldedAccessControl.privateState.getCommitmentPathWithWitnessImpl( + ADMIN.roleCommitment, + ); + expect(witnessCalculatedPath).toEqual(truePath); - describe('assertOnlyRole', () => { - beforeEach(() => { - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); - shieldedAccessControl.setPersistentCaller(ADMIN.publicKey); + expect(() => + shieldedAccessControl.assertOnlyRole(ADMIN.roleId), + ).toThrow('ShieldedAccessControl: unauthorized account'); }); - it('should not fail when authorized caller has correct nonce, and path', () => { + it('should fail when unauthorized caller has correct nonce, and path', () => { // Check nonce is correct expect( shieldedAccessControl.privateState.getCurrentSecretNonce( @@ -462,9 +609,11 @@ describe('ShieldedAccessControl', () => { ); expect(witnessCalculatedPath).toEqual(truePath); + shieldedAccessControl.as(UNAUTHORIZED.publicKey); + expect(() => shieldedAccessControl.assertOnlyRole(ADMIN.roleId), - ).not.toThrow(); + ).toThrow('ShieldedAccessControl: unauthorized account'); }); it('should fail for revoked role', () => { @@ -692,90 +841,29 @@ describe('ShieldedAccessControl', () => { }); }); - describe('assertOnlyRole — unauthorized caller', () => { - beforeEach(() => { - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); - }); - - it('should fail for caller who was never granted the role', () => { - shieldedAccessControl.setPersistentCaller(UNAUTHORIZED.publicKey); - expect(() => - shieldedAccessControl.assertOnlyRole(ADMIN.roleId), - ).toThrow('ShieldedAccessControl: unauthorized account'); - }); - }); - describe('_checkRole — isRevoked field', () => { - beforeEach(() => { - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); - shieldedAccessControl.setPersistentCaller(ADMIN.publicKey); - }); - - it('isRevoked should be false when role is active', () => { - expect( - shieldedAccessControl._checkRole(ADMIN.roleId, ADMIN.accountId) - .isRevoked, - ).toBe(false); - }); - - it('isRevoked should be true when role is revoked', () => { - shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); - expect( - shieldedAccessControl._checkRole(ADMIN.roleId, ADMIN.accountId) - .isRevoked, - ).toBe(true); - }); - - it('isRevoked should be false when role has never been granted', () => { - expect( - shieldedAccessControl._checkRole( - UNINITIALIZED.roleId, - ADMIN.accountId, - ).isRevoked, - ).toBe(false); - }); - - it('should appear active (hasRole=true, isRevoked=false) when nullifier path witness returns a bad path', () => { - // SIMULATOR LIMITATION: in the simulator, a bad nullifier path causes - // _roleCommitmentNullifiers.checkRoot() to return false, so isRevoked=false - // and the role appears active even though it was revoked. - // In production ZK this cannot happen: an incorrect path produces an - // invalid proof that the verifier rejects. - shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); - shieldedAccessControl.overrideWitness( - 'wit_getCommitmentNullifierPath', - RETURN_BAD_PATH, - ); - const roleCheck = shieldedAccessControl._checkRole( - ADMIN.roleId, - ADMIN.accountId, - ); - expect(roleCheck.isRevoked).toBe(false); - expect(roleCheck.hasRole).toBe(true); - }); - }); describe('getRoleAdmin', () => { it('should return zero bytes (DEFAULT_ADMIN_ROLE) for a role with no admin set', () => { - expect( - shieldedAccessControl.getRoleAdmin(OPERATOR_1.roleId), - ).toEqual(new Uint8Array(32)); + expect(shieldedAccessControl.getRoleAdmin(OPERATOR_1.roleId)).toEqual( + new Uint8Array(32), + ); }); it('should return the admin role after _setRoleAdmin', () => { shieldedAccessControl._setRoleAdmin(OPERATOR_1.roleId, ADMIN.roleId); - expect( - shieldedAccessControl.getRoleAdmin(OPERATOR_1.roleId), - ).toEqual(new Uint8Array(ADMIN.roleId)); + expect(shieldedAccessControl.getRoleAdmin(OPERATOR_1.roleId)).toEqual( + new Uint8Array(ADMIN.roleId), + ); }); }); describe('_setRoleAdmin', () => { it('should set admin role retrievable by getRoleAdmin', () => { shieldedAccessControl._setRoleAdmin(OPERATOR_1.roleId, ADMIN.roleId); - expect( - shieldedAccessControl.getRoleAdmin(OPERATOR_1.roleId), - ).toEqual(new Uint8Array(ADMIN.roleId)); + expect(shieldedAccessControl.getRoleAdmin(OPERATOR_1.roleId)).toEqual( + new Uint8Array(ADMIN.roleId), + ); }); it('should override an existing admin role', () => { @@ -784,9 +872,9 @@ describe('ShieldedAccessControl', () => { OPERATOR_1.roleId, OPERATOR_2.roleId, ); - expect( - shieldedAccessControl.getRoleAdmin(OPERATOR_1.roleId), - ).toEqual(new Uint8Array(OPERATOR_2.roleId)); + expect(shieldedAccessControl.getRoleAdmin(OPERATOR_1.roleId)).toEqual( + new Uint8Array(OPERATOR_2.roleId), + ); }); }); @@ -824,7 +912,10 @@ describe('ShieldedAccessControl', () => { }); it('should not re-grant role', () => { - shieldedAccessControl.grantRole(OPERATOR_1.roleId, OPERATOR_1.accountId); + shieldedAccessControl.grantRole( + OPERATOR_1.roleId, + OPERATOR_1.accountId, + ); const treeRoot = shieldedAccessControl .getPublicState() .ShieldedAccessControl__operatorRoles.root(); @@ -849,7 +940,10 @@ describe('ShieldedAccessControl', () => { ); // Grant OPERATOR_1.roleId to OPERATOR_1 (ADMIN has DEFAULT_ADMIN_ROLE // which is the admin of OPERATOR_1.roleId by default). - shieldedAccessControl.grantRole(OPERATOR_1.roleId, OPERATOR_1.accountId); + shieldedAccessControl.grantRole( + OPERATOR_1.roleId, + OPERATOR_1.accountId, + ); // Switch to OPERATOR_1 as caller and inject their nonce for their role. shieldedAccessControl.privateState.injectSecretNonce( @@ -910,7 +1004,10 @@ describe('ShieldedAccessControl', () => { }); it('should not re-revoke role', () => { - shieldedAccessControl.revokeRole(OPERATOR_1.roleId, OPERATOR_1.accountId); + shieldedAccessControl.revokeRole( + OPERATOR_1.roleId, + OPERATOR_1.accountId, + ); const nullifierRoot = shieldedAccessControl .getPublicState() .ShieldedAccessControl__roleCommitmentNullifiers.root(); From ac33519747e97d218b87df1d0c2e1053465c51ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Sun, 8 Mar 2026 19:11:36 -0400 Subject: [PATCH 248/322] update assertion message --- contracts/src/access/ShieldedAccessControl.compact | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index 7d5ff885..2713bc3e 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -445,7 +445,7 @@ module ShieldedAccessControl { // If the path is valid we assert it’s a path for the queried leaf (not some other leaf). if (isRevoked) { assert(nullifierCommitmentPath.leaf == roleNullifier, - "ShieldedAccessControl: Path must contain leaf matching computed role nullifier for the computed role commitment" + "ShieldedAccessControl: Path must contain leaf matching role nullifier for the computed role commitment" ); } @@ -504,7 +504,7 @@ module ShieldedAccessControl { // If the path is valid we assert it’s a path for the queried leaf (not some other leaf). if (isRevoked) { assert(nullifierCommitmentPath.leaf == roleNullifier, - "ShieldedAccessControl: Path must contain leaf matching computed role nullifier for the computed role commitment" + "ShieldedAccessControl: Path must contain leaf matching role nullifier for the computed role commitment" ); } @@ -561,7 +561,7 @@ module ShieldedAccessControl { // If the path is valid we assert it’s a path for the queried leaf (not some other leaf). if (isRevoked) { assert(nullifierCommitmentPath.leaf == roleNullifier, - "ShieldedAccessControl: Path must contain leaf matching computed role nullifier for the computed role commitment" + "ShieldedAccessControl: Path must contain leaf matching role nullifier for the computed role commitment" ); } From 12818f83a2af6e7849c5ce5e1b8417379e98d1d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Mon, 9 Mar 2026 13:48:16 -0400 Subject: [PATCH 249/322] Sync mock implementation --- .../src/access/test/mocks/MockShieldedAccessControl.compact | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact index e3e7f0eb..41f6dd4d 100644 --- a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact +++ b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact @@ -111,7 +111,7 @@ export circuit _checkRole( // If the path is valid we assert it’s a path for the queried leaf (not some other leaf). if (hasCommitment) { assert(roleCommitmentPath.leaf == roleCommitment, - "ShieldedAccessControl: Path must contain queried role commitment" + "ShieldedAccessControl: Path must contain leaf matching computed role commitment for the provided roleId, accountId pairing" ); } @@ -127,7 +127,7 @@ export circuit _checkRole( // If the path is valid we assert it’s a path for the queried leaf (not some other leaf). if (isRevoked) { assert(nullifierCommitmentPath.leaf == roleNullifier, - "ShieldedAccessControl: Path must contain queried role nullifier" + "ShieldedAccessControl: Path must contain leaf matching role nullifier for the computed role commitment" ); } From e5b402d80e2da425755c2f6b9282d75f2d4e10e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Mon, 9 Mar 2026 13:51:12 -0400 Subject: [PATCH 250/322] update tests --- .../access/test/ShieldedAccessControl.test.ts | 414 ++++++++++-------- 1 file changed, 240 insertions(+), 174 deletions(-) diff --git a/contracts/src/access/test/ShieldedAccessControl.test.ts b/contracts/src/access/test/ShieldedAccessControl.test.ts index 572e0735..84efb346 100644 --- a/contracts/src/access/test/ShieldedAccessControl.test.ts +++ b/contracts/src/access/test/ShieldedAccessControl.test.ts @@ -295,7 +295,7 @@ describe('ShieldedAccessControl', () => { }); // Test skipped due to compiler bug not updating assertion text - it.skip('should fail when wit_getRoleCommitmentPath returns valid path for a different roleId, accountId pairing', () => { + it('should fail when wit_getRoleCommitmentPath returns valid path for a different roleId, accountId pairing', () => { shieldedAccessControl._grantRole( OPERATOR_1.roleId, OPERATOR_1.accountId, @@ -322,7 +322,7 @@ describe('ShieldedAccessControl', () => { }); // Test skipped due to compiler bug not updating assertion text - it.skip('should fail when wit_getCommitmentNullifierPath returns valid path for a different nullifier', () => { + it('should fail when wit_getCommitmentNullifierPath returns valid path for a different nullifier', () => { shieldedAccessControl._grantRole( OPERATOR_1.roleId, OPERATOR_1.accountId, @@ -508,207 +508,273 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl.setPersistentCaller(ADMIN.publicKey); }); - it('should fail for caller who was never granted the role', () => { - shieldedAccessControl.as(UNAUTHORIZED.publicKey); - expect(() => - shieldedAccessControl.assertOnlyRole(ADMIN.roleId), - ).toThrow('ShieldedAccessControl: unauthorized account'); - }); + describe('should fail', () => { + it('for caller who was never granted the role', () => { + shieldedAccessControl.as(UNAUTHORIZED.publicKey); + expect(() => + shieldedAccessControl.assertOnlyRole(ADMIN.roleId), + ).toThrow('ShieldedAccessControl: unauthorized account'); + }); - it('should not fail when authorized caller has correct nonce, and path', () => { - // Check nonce is correct - expect( - shieldedAccessControl.privateState.getCurrentSecretNonce( - ADMIN.roleId, - ), - ).toBe(ADMIN.secretNonce); + it('when authorized caller has incorrect path', () => { + // Check nonce is correct + expect( + shieldedAccessControl.privateState.getCurrentSecretNonce( + ADMIN.roleId, + ), + ).toBe(ADMIN.secretNonce); + + // Check path does not match + const truePath = + shieldedAccessControl.privateState.getCommitmentPathWithFindForLeaf( + ADMIN.roleCommitment, + ); + shieldedAccessControl.overrideWitness('wit_getRoleCommitmentPath', RETURN_BAD_PATH); + const witnessCalculatedPath = + shieldedAccessControl.privateState.getCommitmentPathWithWitnessImpl( + ADMIN.roleCommitment, + ); + expect(witnessCalculatedPath).not.toEqual(truePath); + + expect(() => + shieldedAccessControl.assertOnlyRole(ADMIN.roleId), + ).toThrow('ShieldedAccessControl: unauthorized account'); + }); - // Check path matches - const truePath = - shieldedAccessControl.privateState.getCommitmentPathWithFindForLeaf( - ADMIN.roleCommitment, - ); - const witnessCalculatedPath = - shieldedAccessControl.privateState.getCommitmentPathWithWitnessImpl( - ADMIN.roleCommitment, - ); - expect(witnessCalculatedPath).toEqual(truePath); + it('when authorized caller has incorrect nonce', () => { + shieldedAccessControl.privateState.injectSecretNonce(ADMIN.roleId, UNAUTHORIZED.secretNonce); - expect(() => - shieldedAccessControl.assertOnlyRole(ADMIN.roleId), - ).not.toThrow(); - }); + // Check nonce is incorrect + expect( + shieldedAccessControl.privateState.getCurrentSecretNonce( + ADMIN.roleId, + ), + ).not.toBe(ADMIN.secretNonce); + + // Check path matches + const truePath = + shieldedAccessControl.privateState.getCommitmentPathWithFindForLeaf( + ADMIN.roleCommitment, + ); + const witnessCalculatedPath = + shieldedAccessControl.privateState.getCommitmentPathWithWitnessImpl( + ADMIN.roleCommitment, + ); + expect(witnessCalculatedPath).toEqual(truePath); + + expect(() => + shieldedAccessControl.assertOnlyRole(ADMIN.roleId), + ).toThrow('ShieldedAccessControl: unauthorized account'); + }); - it('should fail when authorized caller has incorrect path', () => { - // Check nonce is correct - expect( - shieldedAccessControl.privateState.getCurrentSecretNonce( - ADMIN.roleId, - ), - ).toBe(ADMIN.secretNonce); + it('when unauthorized caller has correct nonce, and path', () => { + // Check nonce is correct + expect( + shieldedAccessControl.privateState.getCurrentSecretNonce( + ADMIN.roleId, + ), + ).toBe(ADMIN.secretNonce); + + // Check path matches + const truePath = + shieldedAccessControl.privateState.getCommitmentPathWithFindForLeaf( + ADMIN.roleCommitment, + ); + const witnessCalculatedPath = + shieldedAccessControl.privateState.getCommitmentPathWithWitnessImpl( + ADMIN.roleCommitment, + ); + expect(witnessCalculatedPath).toEqual(truePath); + + shieldedAccessControl.as(UNAUTHORIZED.publicKey); + + expect(() => + shieldedAccessControl.assertOnlyRole(ADMIN.roleId), + ).toThrow('ShieldedAccessControl: unauthorized account'); + }); - // Check path does not match - const truePath = - shieldedAccessControl.privateState.getCommitmentPathWithFindForLeaf( - ADMIN.roleCommitment, - ); - shieldedAccessControl.overrideWitness('wit_getRoleCommitmentPath', RETURN_BAD_PATH); - const witnessCalculatedPath = - shieldedAccessControl.privateState.getCommitmentPathWithWitnessImpl( - ADMIN.roleCommitment, - ); - expect(witnessCalculatedPath).not.toEqual(truePath); + it('for revoked role', () => { + shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + expect(() => + shieldedAccessControl.assertOnlyRole(ADMIN.roleId), + ).toThrow('ShieldedAccessControl: unauthorized account'); + }); - expect(() => - shieldedAccessControl.assertOnlyRole(ADMIN.roleId), - ).toThrow('ShieldedAccessControl: unauthorized account'); + it('for revoked role with re-approval', () => { + shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + expect(() => + shieldedAccessControl.assertOnlyRole(ADMIN.roleId), + ).toThrow('ShieldedAccessControl: unauthorized account'); + }); }); - it('should fail when authorized caller has incorrect nonce', () => { - shieldedAccessControl.privateState.injectSecretNonce(ADMIN.roleId, UNAUTHORIZED.secretNonce); + describe('should not fail', () => { + it('for admin with multiple roles', () => { + shieldedAccessControl.privateState.injectSecretNonce( + OPERATOR_1.roleId, + OPERATOR_1.secretNonce, + ); + // A unique accountId must be constructed for each new role using its associated secretNonce + const operator1AccountId = buildAccountIdHash( + ADMIN.zPublicKey, + OPERATOR_1.secretNonce, + ); - // Check nonce is incorrect - expect( - shieldedAccessControl.privateState.getCurrentSecretNonce( - ADMIN.roleId, - ), - ).not.toBe(ADMIN.secretNonce); + shieldedAccessControl.privateState.injectSecretNonce( + OPERATOR_2.roleId, + OPERATOR_2.secretNonce, + ); + const operator2AccountId = buildAccountIdHash( + ADMIN.zPublicKey, + OPERATOR_2.secretNonce, + ); - // Check path matches - const truePath = - shieldedAccessControl.privateState.getCommitmentPathWithFindForLeaf( - ADMIN.roleCommitment, + shieldedAccessControl.privateState.injectSecretNonce( + OPERATOR_3.roleId, + OPERATOR_3.secretNonce, ); - const witnessCalculatedPath = - shieldedAccessControl.privateState.getCommitmentPathWithWitnessImpl( - ADMIN.roleCommitment, + const operator3AccountId = buildAccountIdHash( + ADMIN.zPublicKey, + OPERATOR_3.secretNonce, ); - expect(witnessCalculatedPath).toEqual(truePath); - expect(() => - shieldedAccessControl.assertOnlyRole(ADMIN.roleId), - ).toThrow('ShieldedAccessControl: unauthorized account'); - }); + shieldedAccessControl._grantRole(OPERATOR_1.roleId, operator1AccountId); + shieldedAccessControl._grantRole(OPERATOR_2.roleId, operator2AccountId); + shieldedAccessControl._grantRole(OPERATOR_3.roleId, operator3AccountId); + expect(() => { + shieldedAccessControl.assertOnlyRole(ADMIN.roleId); + shieldedAccessControl.assertOnlyRole(OPERATOR_1.roleId); + shieldedAccessControl.assertOnlyRole(OPERATOR_2.roleId); + shieldedAccessControl.assertOnlyRole(OPERATOR_3.roleId); + }).not.toThrow(); + }); - it('should fail when unauthorized caller has correct nonce, and path', () => { - // Check nonce is correct - expect( - shieldedAccessControl.privateState.getCurrentSecretNonce( - ADMIN.roleId, - ), - ).toBe(ADMIN.secretNonce); + it('when authorized ADMIN has correct nonce, and path', () => { + // Check nonce is correct + expect( + shieldedAccessControl.privateState.getCurrentSecretNonce( + ADMIN.roleId, + ), + ).toBe(ADMIN.secretNonce); + + // Check path matches + const truePath = + shieldedAccessControl.privateState.getCommitmentPathWithFindForLeaf( + ADMIN.roleCommitment, + ); + const witnessCalculatedPath = + shieldedAccessControl.privateState.getCommitmentPathWithWitnessImpl( + ADMIN.roleCommitment, + ); + expect(witnessCalculatedPath).toEqual(truePath); + + expect(() => + shieldedAccessControl.assertOnlyRole(ADMIN.roleId), + ).not.toThrow(); + }); - // Check path matches - const truePath = - shieldedAccessControl.privateState.getCommitmentPathWithFindForLeaf( - ADMIN.roleCommitment, + it('for multiple users with the same role', () => { + // All users will use OPERATOR_1.secretNonce as their nonce value + // when generating their accountId for simplicity + shieldedAccessControl.privateState.injectSecretNonce( + OPERATOR_1.roleId, + OPERATOR_1.secretNonce, ); - const witnessCalculatedPath = - shieldedAccessControl.privateState.getCommitmentPathWithWitnessImpl( - ADMIN.roleCommitment, + // A unique accountId must be constructed for each new role using its associated secretNonce + const operator1AdminAccountId = buildAccountIdHash( + ADMIN.zPublicKey, + OPERATOR_1.secretNonce, ); - expect(witnessCalculatedPath).toEqual(truePath); + shieldedAccessControl._grantRole(OPERATOR_1.roleId, operator1AdminAccountId); + shieldedAccessControl.as(ADMIN.publicKey); // assert ADMIN has OP_1 roleId + expect(shieldedAccessControl.assertOnlyRole(OPERATOR_1.roleId)); - shieldedAccessControl.as(UNAUTHORIZED.publicKey); - - expect(() => - shieldedAccessControl.assertOnlyRole(ADMIN.roleId), - ).toThrow('ShieldedAccessControl: unauthorized account'); - }); + const operator1Op2AccountId = buildAccountIdHash( + OPERATOR_2.zPublicKey, + OPERATOR_1.secretNonce, + ); + shieldedAccessControl._grantRole(OPERATOR_1.roleId, operator1Op2AccountId); + shieldedAccessControl.as(OPERATOR_2.publicKey); // assert OP_2 has OP_1 roleId + expect(shieldedAccessControl.assertOnlyRole(OPERATOR_1.roleId)); - it('should fail for revoked role', () => { - shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); - expect(() => - shieldedAccessControl.assertOnlyRole(ADMIN.roleId), - ).toThrow('ShieldedAccessControl: unauthorized account'); - }); + const operator1Op3AccountId = buildAccountIdHash( + OPERATOR_3.zPublicKey, + OPERATOR_1.secretNonce, + ); + shieldedAccessControl._grantRole(OPERATOR_1.roleId, operator1Op3AccountId); + shieldedAccessControl.as(OPERATOR_3.publicKey); // assert OP_3 has OP_1 roleId + expect(shieldedAccessControl.assertOnlyRole(OPERATOR_1.roleId)); + }); + }) + }); - it('should fail for revoked role with re-approval', () => { - shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); - expect(() => - shieldedAccessControl.assertOnlyRole(ADMIN.roleId), - ).toThrow('ShieldedAccessControl: unauthorized account'); + describe('_grantRole', () => { + describe('should return true', () => { + it('when granting a new role', () => { + expect( + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId), + ).toBe(true); + }); }); - it('should not fail for admin with multiple roles', () => { - shieldedAccessControl.privateState.injectSecretNonce( - OPERATOR_1.roleId, - OPERATOR_1.secretNonce, - ); - // A unique accountId must be constructed for each new role using its associated secretNonce - const operator1AccountId = buildAccountIdHash( - ADMIN.zPublicKey, - OPERATOR_1.secretNonce, - ); - - shieldedAccessControl.privateState.injectSecretNonce( - OPERATOR_2.roleId, - OPERATOR_2.secretNonce, - ); - const operator2AccountId = buildAccountIdHash( - ADMIN.zPublicKey, - OPERATOR_2.secretNonce, - ); - - shieldedAccessControl.privateState.injectSecretNonce( - OPERATOR_3.roleId, - OPERATOR_3.secretNonce, - ); - const operator3AccountId = buildAccountIdHash( - ADMIN.zPublicKey, - OPERATOR_3.secretNonce, - ); + describe('should update _operatorRoles merkle tree', () => { + it('when granting a new role', () => { + // check merkle tree is empty + let merkleRoot = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.root(); + expect(merkleRoot.field).toBe(0n); - shieldedAccessControl._grantRole(OPERATOR_1.roleId, operator1AccountId); - shieldedAccessControl._grantRole(OPERATOR_2.roleId, operator2AccountId); - shieldedAccessControl._grantRole(OPERATOR_3.roleId, operator3AccountId); - expect(() => { - shieldedAccessControl.assertOnlyRole(ADMIN.roleId); - shieldedAccessControl.assertOnlyRole(OPERATOR_1.roleId); - shieldedAccessControl.assertOnlyRole(OPERATOR_2.roleId); - shieldedAccessControl.assertOnlyRole(OPERATOR_3.roleId); - }).not.toThrow(); - }); - }); + // check merkle tree is updated + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + merkleRoot = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.root(); + expect(merkleRoot).not.toBe(0n); - describe('_grantRole', () => { - it('should grant role', () => { - expect( - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId), - ).toBe(true); + // check path exists for new role + const merkleTreePath = shieldedAccessControl.getPublicState().ShieldedAccessControl__operatorRoles.findPathForLeaf(ADMIN.roleCommitment); + expect(merkleTreePath).toBeDefined(); + expect(merkleTreePath?.leaf).toStrictEqual(ADMIN.roleCommitment); + }); }); - it('should not re-grant role', () => { - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); - const merkleRoot = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.root(); - expect( - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId), - ).toBe(false); - expect( - shieldedAccessControl + describe('should return false', () => { + it('when re-granting a role', () => { + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + const merkleRoot = shieldedAccessControl .getPublicState() - .ShieldedAccessControl__operatorRoles.root(), - ).toEqual(merkleRoot); - }); + .ShieldedAccessControl__operatorRoles.root(); + expect( + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId), + ).toBe(false); + expect( + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.root(), + ).toEqual(merkleRoot); + }); - it('should not re-grant revoked role', () => { - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); - shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); - expect( - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId), - ).toBe(false); - const merkleRoot = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.root(); - expect( - shieldedAccessControl + it('should not re-grant revoked role', () => { + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + expect( + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId), + ).toBe(false); + const merkleRoot = shieldedAccessControl .getPublicState() - .ShieldedAccessControl__operatorRoles.root(), - ).toEqual(merkleRoot); - }); + .ShieldedAccessControl__operatorRoles.root(); + expect( + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.root(), + ).toEqual(merkleRoot); + }); + }) + + + it('should update Merkle tree root', () => { const initialMtRoot = shieldedAccessControl From a10b9bb6ef1a0771538e6241279a8f6cc8ffed9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Mon, 9 Mar 2026 20:22:29 -0400 Subject: [PATCH 251/322] big refactor --- .../src/access/ShieldedAccessControl.compact | 257 +++++++++++------- .../mocks/MockShieldedAccessControl.compact | 38 +-- 2 files changed, 165 insertions(+), 130 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index 2713bc3e..01d3d36e 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -6,7 +6,7 @@ pragma language_version >= 0.21.0; /** * @module Shielded AccessControl * @description A Shielded AccessControl library. - * + * * This module provides a shielded role-based access control (RBAC) mechanism, where roles can be used to * represent a set of permissions. Roles are stored as Merkle tree commitments to avoid * disclosing information about role holders. Role commitments are created with the following @@ -67,7 +67,9 @@ pragma language_version >= 0.21.0; * - Outside observers will know when an admin is added and how many admins exist. * - Outside observers will know which role identifiers are admin identifiers. * - Outside observers will have knowledge of all role identifiers. + * - Outside observers will have knowledge of all role nullifiers and can link every nullifier to its corresponding commitment. * - Outside observers will have knowledge of role additions and revocations. + * - Outside observers can infer the total number of role grants made across all roles — not per-role counts, but the cumulative total. * - Outside observers can link calls made by the same role instance across time. * - If a user ever reveals their nonce or reuses it poorly, they can be deanonymized retroactively. * - Outside observers will NOT be able to identify the public address of any role holder @@ -77,11 +79,19 @@ pragma language_version >= 0.21.0; * @dev Security Considerations: * - The `secretNonce` must be kept private. Loss of the nonce prevents role holders * and admins from proving access or transferring it. Public exposure, poor nonce selection or nonce - reuse may weaken privacy guarantees and allow retroactive deanonymization. - * - Role validation is entirely circuit-based using witness-provided values. + * reuse may weaken privacy guarantees and allow retroactive deanonymization. + * - Role authorization depends on two mechanisms with different trust levels: + * commitment presence is verified via witness-supplied Merkle paths and is + * subject to honest-path limitations; revocation status is verified via direct + * Set membership and provides a cryptographic guarantee regardless of witness behavior. * - It's strongly recommended to use cryptographically secure random values for the `_instanceSalt`. * Failure to do so could lead to the exposure of public keys. * - The `_instanceSalt` is immutable and used to differentiate deployments. + * - The `_operatorRoles` Merkle tree has a fixed capacity of 2^20 leaf slots. + * Deployers should monitor slot consumption off-chain. A malicious or + * careless admin can exhaust capacity through repeated grants of the same + * roleId | accountId pairing, as the commitment existence check provides + * no cryptographic guarantee against duplicate insertions. * * @notice Missing Features and Improvements: * - Role events @@ -98,6 +108,7 @@ module ShieldedAccessControl { export new type AccountIdentifier = Bytes<32>; export new type AdminIdentifier = Bytes<32>; export new type RoleNullifier = Bytes<32>; + export new type HonestPathIndicator = Boolean; type ZcpkOrContractAddress = Either; /** @@ -116,11 +127,11 @@ module ShieldedAccessControl { export ledger _adminRoles: Map; /** - * @description A Merkle tree of nullifiers used to prove a role has been revoked + * @description A set of nullifiers used to prove a role has been revoked * @type {Bytes<32>} RoleNullifier - A role nullifier created by the following hash: SHA256(roleCommitment | nullifierDomain). * @type {Set} _roleCommitmentNullifiers  */ - export ledger _roleCommitmentNullifiers: MerkleTree<20, RoleNullifier>; + export ledger _roleCommitmentNullifiers: Set; /** * @description The default admin role for all roles. Only accounts with this role will be able to grant or revoke other roles @@ -151,17 +162,6 @@ module ShieldedAccessControl { roleCommitment: RoleCommitment ): MerkleTreePath<20, RoleCommitment>; - /** - * @witness wit_getCommitmentNullifierPath - * @description Returns a path to a role nullifier in the `_roleCommitmentNullifiers` Merkle tree if one exists. Otherwise, returns an invalid path. - * - * @param {Bytes<32>} RoleNullifier - A nullifier created by the following hash: SHA256( roleCommitment | nullifierDomain). - * @return {MerkleTreePath<20, Bytes<32>>} - The Merkle tree path to `roleNullifier` in the `_roleCommitmentNullifiers` Merkle tree -  */ - witness wit_getCommitmentNullifierPath( - roleNullifier: RoleNullifier - ): MerkleTreePath<20, RoleNullifier>; - /** * @witness wit_secretNonce * @description A private per-accountId nonce used in deriving the shielded account identifier. @@ -176,11 +176,16 @@ module ShieldedAccessControl { /** * @description A struct containing auth information for a particular `roleId` | `accountId` pairing. * - * @type {Boolean} hasRole - A boolean flag indicating an `accountId` is authorized for a `roleId`. - * @type {Boolean} isRevoked - A boolean flag indicating if a nullifier exists for `roleCommitment`. + * @type {Boolean} observedHasRole - Honest-path indicator derived from a witness-supplied Merkle path and can be spoofed by a + * malicious prover supplying a garbage path, producing a false negative (observedHasRole = false for a legitimately credentialed holder). + * It MUST NOT be used as a security gate in calling circuits. Its failure mode is a liveness concern only — a spoofed absence + * cannot grant access, it can only deny it. + * + * @type {Boolean} isRevoked - A boolean flag indicating if a nullifier exists for `roleCommitment`. It is derived from a direct + * Set membership lookup requiring no witness. It cannot be spoofed by a malicious prover and MAY be used as a security gate in calling circuits. */ export struct RoleCheck { - hasRole: Boolean; + observedHasRole: HonestPathIndicator; isRevoked: Boolean; } @@ -198,7 +203,7 @@ module ShieldedAccessControl { * - Contract is not initialized. * * @param {Bytes<32>} instanceSalt - Contract salt to prevent duplicate commitments if - * users reuse their PK and secretNonce witness across different contracts (not recommended). + * users reuse their PK and secretNonce witness across different contracts (not recommended). Must not be zero. * @returns {[]} Empty tuple. */ export circuit initialize(instanceSalt: Bytes<32>): [] { @@ -211,17 +216,22 @@ module ShieldedAccessControl { /** * @description Returns `true` if a caller is authorized for `roleId`. * + * @notice Completeness is not guaranteed — this circuit may fail for a + * legitimately credentialed caller if the proving environment supplies + * an invalid Merkle path. Soundness is guaranteed — this circuit will + * never return true for an unauthorized caller. + * * @circuitInfo k=15, rows=22128 * * Disclosures: * - * - The Merkle tree path for the role commitment stored in the `_operatorRoles` Merkle tree. - * - The Merkle tree path for the nullifier commitment stored in the `_roleCommitmentNullifiers` Merkle tree. + * - A Merkle tree path to a role commitment stored in the `_operatorRoles` Merkle tree. + * - The nullifier commitment stored in the `_roleCommitmentNullifiers` set. * * @param {Bytes<32>} roleId - The role identifier. - * @return {Boolean} - A boolean determining if a caller has the specified role. + * @return {Boolean} - An honest-path indicator determining if a caller has the specified role.  */ - export circuit callerHasRole(roleId: RoleIdentifier): Boolean { + export circuit unverifiedCallerHasRole(roleId: RoleIdentifier): HonestPathIndicator { Initializable_assertInitialized(); const callerAsEither = @@ -230,12 +240,17 @@ module ShieldedAccessControl { right: ContractAddress { bytes: pad(32, "") } }; const nonce = wit_secretNonce(roleId); const accountId = _computeAccountId(callerAsEither, nonce); - return _checkRole(roleId, accountId).hasRole; + return _checkRole(roleId, accountId).observedHasRole; } /** * @description Reverts if caller is not authorized for `roleId`. * + * @notice Completeness is not guaranteed — this circuit may fail for a + * legitimately credentialed caller if the proving environment supplies + * an invalid Merkle path. Soundness is guaranteed — this circuit will + * never return true for an unauthorized caller. + * * @circuitInfo k=15, rows=22130 * * Requirements: @@ -244,8 +259,8 @@ module ShieldedAccessControl { * * Disclosures: * - * - The Merkle tree path for the role commitment stored in the `_operatorRoles` Merkle tree. - * - The Merkle tree path for the nullifier commitment stored in the `_roleCommitmentNullifiers` Merkle tree. + * - A Merkle tree path to a role commitment stored in the `_operatorRoles` Merkle tree. + * - The nullifier commitment stored in the `_roleCommitmentNullifiers` set. * * @param {Bytes<32>} roleId - The role identifier. * @return {[]} - Empty tuple. @@ -253,7 +268,7 @@ module ShieldedAccessControl { export circuit assertOnlyRole(roleId: RoleIdentifier): [] { Initializable_assertInitialized(); - assert(callerHasRole(roleId), "ShieldedAccessControl: unauthorized account"); + assert(unverifiedCallerHasRole(roleId), "ShieldedAccessControl: unauthorized account"); } /** @@ -282,10 +297,15 @@ module ShieldedAccessControl { /** * @description Grants `roleId` to `accountId` by inserting a role commitment unique to the - * `roleId` | `accountId` pairing into the `_operatorRoles` Merkle tree. `roleId` can only be - * granted to `accountId` once. A new `accountId` must be generated to be re-authorized for + * `roleId` | `accountId` pairing into the `_operatorRoles` Merkle tree. A valid `roleId` can only be + * issued to `accountId` once. Once revoked, a new `accountId` must be generated to be re-authorized for * `roleId` * + * @notice Completeness is not guaranteed — this circuit may fail for a + * legitimately credentialed admin if the proving environment supplies + * an invalid Merkle path. Soundness is guaranteed — this circuit will never grant a role to an + * `accountId` unless the caller is authorized as admin for `roleId`. + * * Requirements: * * - caller must be admin for `roleId` @@ -296,8 +316,8 @@ module ShieldedAccessControl { * * - The role commitment produced by SHA256(roleId | accountId | instanceSalt | commitmentDomain ). * - The role identifier - * - The Merkle tree path for the role commitment stored in the `_operatorRoles` Merkle tree. - * - The Merkle tree path for the nullifier commitment stored in the `_roleCommitmentNullifiers` Merkle tree. + * - A Merkle tree path to a role commitment stored in the `_operatorRoles` Merkle tree. + * - The nullifier commitment stored in the `_roleCommitmentNullifiers` set. * * @param {Bytes<32>} roleId - The role identifier. * @param {Bytes<32>} accountId - The unique identifier of the account. @@ -312,8 +332,13 @@ module ShieldedAccessControl { /** * @description Revokes `roleId` from `accountId` by inserting a role nullifier into the - * `_roleNullifiers` set. A `roleId` can only be revoked from `accountId` once to avoid duplicate - * insertions into the `_roleNullifiers` set. + * `_roleNullifiers` set. Once revoked, a new `accountId` must be generated to be re-authorized for + * `roleId` + * + * @notice Completeness is not guaranteed — this circuit may fail for a + * legitimately credentialed admin if the proving environment supplies + * an invalid Merkle path. Soundness is guaranteed — this circuit will never revoke a role from an + * `accountId` unless the caller is authorized as admin for `roleId`. * * @circuitInfo k=18, rows=138517 * @@ -325,10 +350,10 @@ module ShieldedAccessControl { * * Disclosures: * - * - The role nullifier produced by SHA256(roleCommitment | nullifierDomain). + * - The role commitment produced by SHA256(roleId | accountId | instanceSalt | commitmentDomain ). * - The role identifier - * - The Merkle tree path for the role commitment stored in the `_operatorRoles` Merkle tree. - * - The Merkle tree path for the nullifier commitment stored in the `_roleCommitmentNullifiers` Merkle tree. + * - A Merkle tree path to a role commitment stored in the `_operatorRoles` Merkle tree. + * - The nullifier commitment stored in the `_roleCommitmentNullifiers` set. * * @param {Bytes<32>} roleId - The role identifier. * @param {Bytes<32>} accountId - The unique identifier of the account. @@ -348,6 +373,14 @@ module ShieldedAccessControl { * purpose is to provide a mechanism for accounts to lose their privileges * if they are compromised (such as when a trusted device is misplaced). * + * @warning Outside observers may be able to use timing and pattern analysis to weaken pseudonymity + * guarantees + * + * @notice Completeness is not guaranteed — this circuit may fail to renounce + * a legitimately held role if the proving environment supplies an invalid + * Merkle path. Soundness is guaranteed — this circuit will never revoke + * a role the caller does not hold. + * * @circuitInfo k=17, rows=108992 * * Requirements: @@ -357,8 +390,8 @@ module ShieldedAccessControl { * Disclosures: * * - The role nullifier produced by SHA256(roleCommitment | nullifierDomain). - * - The Merkle tree path for the role commitment stored in the `_operatorRoles` Merkle tree. - * - The Merkle tree path for the nullifier commitment stored in the `_roleCommitmentNullifiers` Merkle tree. + * - A Merkle tree path to a role commitment stored in the `_operatorRoles` Merkle tree. + * - The computed role nullifier for a role commitment * * @param {Bytes<32>} roleId - The role identifier. * @param {Bytes<32>} accountId - The unique identifier of the account. @@ -400,9 +433,40 @@ module ShieldedAccessControl { } /** - * @description Attempts to grant `roleId` to `accountId` and returns a boolean indicating if `roleId` was granted. - * Internal circuit without access restriction. Returns true if a role commitment doesn't exist for the - * `roleId` | `accountId` pairing and a role nullifier doesn't exist for the computed role commitment. + * @description Grants `roleId` to `accountId` by inserting a role commitment unique to the + * `roleId` | `accountId` pairing into the `_operatorRoles` Merkle tree. A valid `roleId` can only be + * issued to `accountId` once. Once revoked, a new `accountId` must be generated to be re-authorized for + * `roleId`. Revoked roles cannot be re-granted. + * + * Internal circuit without access restriction. + * + * @notice Completeness is not guaranteed — this circuit MAY insert duplicate + * commitments if the proving environment supplies an invalid Merkle path. + * Soundness is guaranteed — this circuit will never insert a commitment + * for a `roleId` | `accountId` pairing whose nullifier exists in + * `_roleCommitmentNullifiers`. + * + * ## Storage Caveat + * + * `_operatorRoles` is a fixed-depth Merkle tree with a maximum capacity of + * 2^20 = 1,048,576 leaf slots. Without the commitment existence check, + * repeated calls to `_grantRole` for the same `roleId` | `accountId` pairing + * will consume a new slot on each call, granting no additional authority but + * permanently exhausting tree capacity. This creates two risks: + * + * 1. Accidental duplicate grants through administrative error. + * 2. A deliberate griefing attack by a malicious admin exhausting the tree. + * + * The commitment existence check cannot provide a hard security guarantee + * against either risk — a prover supplying a garbage path will always receive + * hasCommitment = false and bypass the check. It therefore functions only as + * a best-effort, honest-path guard that prevents duplicate insertions when + * the admin and proving environment are both operating correctly. + * + * Tree capacity should be treated as an operational concern and slot consumption + * should be monitored off-chain. The TypeScript witness layer should additionally + * validate commitment absence before submitting a grant transaction as a + * defence-in-depth measure against accidental exhaustion. * * @dev Commitment and nullifier checks are inlined in this circuit to avoid an expensive re-computation of the role commitment and nullifier * @@ -411,17 +475,28 @@ module ShieldedAccessControl { * Disclosures: * * - The role commitment produced by SHA256(roleId | accountId | instanceSalt | commitmentDomain). - * - The Merkle tree path for the role commitment stored in the `_operatorRoles` Merkle tree. - * - The Merkle tree path for the nullifier commitment stored in the `_roleCommitmentNullifiers` Merkle tree. + * - A Merkle tree path to a role commitment stored in the `_operatorRoles` Merkle tree. + * - The computed role nullifier for a role commitment * * @param {Bytes<32>} roleId - The role identifier. * @param {Bytes<32>} accountId - The account identifier. - * @return {Boolean} isGranted - A boolean indicating if `roleId` was granted to `accountId`. + * @return {Boolean} observedIsGranted - Honest-path indicator only. Returns true if the operation was observed to succeed by + * the proving environment. This value MUST NOT be used as a security gate in calling circuits — it can be spoofed by + * a malicious prover and carries no on-chain guarantee. */ - export circuit _grantRole(roleId: RoleIdentifier, accountId: AccountIdentifier): Boolean { + export circuit _grantRole( + roleId: RoleIdentifier, + accountId: AccountIdentifier + ): HonestPathIndicator { Initializable_assertInitialized(); const roleCommitment = _computeRoleCommitment(roleId, accountId); + + // Best-effort honest-path guard against duplicate insertions. + // This check is NOT a security guarantee — a prover supplying a garbage + // path will always receive hasCommitment = false and bypass it. Its sole + // purpose is to conserve _operatorRoles tree capacity under normal + // operation. See the Storage Caveat in the circuit documentation above. const roleCommitmentPath = wit_getRoleCommitmentPath(roleCommitment); const hasCommitment = _operatorRoles.checkRoot( @@ -436,32 +511,24 @@ module ShieldedAccessControl { } const roleNullifier = _computeNullifier(roleCommitment); - const nullifierCommitmentPath = wit_getCommitmentNullifierPath(roleNullifier); - const isRevoked = - _roleCommitmentNullifiers.checkRoot( - merkleTreePathRoot<20, RoleNullifier>(disclose(nullifierCommitmentPath)) - ); - - // If the path is valid we assert it’s a path for the queried leaf (not some other leaf). - if (isRevoked) { - assert(nullifierCommitmentPath.leaf == roleNullifier, - "ShieldedAccessControl: Path must contain leaf matching role nullifier for the computed role commitment" - ); - } + const isRevoked = _roleCommitmentNullifiers.member(disclose(roleNullifier)); const hasRole = hasCommitment && !isRevoked; if (hasRole || isRevoked) { - return false; + return false as HonestPathIndicator; } _operatorRoles.insert(disclose(roleCommitment)); - return true; + return true as HonestPathIndicator; } /** - * @description Attempts to revoke `roleId` from `accountId` and returns a boolean indicating if `roleId` was revoked. - * Internal circuit without access restriction. Returns true if a role commitment exists for the - * `roleId` | `accountId` pairing and a role nullifier doesn't exist for the computed role commitment. + * @description Revokes `roleId` from `accountId` by inserting a role nullifier into `_roleCommitmentNullifiers`. + * Internal circuit without access restriction. A nullifier cannot be inserted for a commitment that doesn't exist. + * + * @notice Completeness is not guaranteed — this circuit MAY return false negative values and fail to revoke an active role + * if the proving environment supplies an invalid Merkle path. Soundness is guaranteed — this circuit will + * never revoke a role that does not exist or is already inactive. * * @dev Commitment and nullifier checks are inlined in this circuit to avoid an expensive re-computation of the role commitment and nullifier * @@ -471,13 +538,18 @@ module ShieldedAccessControl { * * - The role nullifier produced by SHA256(roleCommitment | nullifierDomain). * - The Merkle tree path for the role commitment stored in the `_operatorRoles` Merkle tree. - * - The Merkle tree path for the nullifier commitment stored in the `_roleCommitmentNullifiers` Merkle tree. + * - The computed role nullifier for a role commitment * * @param {Bytes<32>} roleId - The role identifier. * @param {Bytes<32>} accountId - The account identifier. - * @return {Boolean} isRevoked - A boolean indicating if `roleId` was revoked for `accountId` + * @return {Boolean} observedIsRevoked - Honest-path indicator only. Returns true if the operation was observed to succeed by + * the proving environment. This value MUST NOT be used as a security gate in calling circuits — it can be spoofed by + * a malicious prover and carries no on-chain guarantee. */ - export circuit _revokeRole(roleId: RoleIdentifier, accountId: AccountIdentifier): Boolean { + export circuit _revokeRole( + roleId: RoleIdentifier, + accountId: AccountIdentifier + ): HonestPathIndicator { Initializable_assertInitialized(); const roleCommitment = _computeRoleCommitment(roleId, accountId); @@ -495,44 +567,38 @@ module ShieldedAccessControl { } const roleNullifier = _computeNullifier(roleCommitment); - const nullifierCommitmentPath = wit_getCommitmentNullifierPath(roleNullifier); - const isRevoked = - _roleCommitmentNullifiers.checkRoot( - merkleTreePathRoot<20, RoleNullifier>(disclose(nullifierCommitmentPath)) - ); - - // If the path is valid we assert it’s a path for the queried leaf (not some other leaf). - if (isRevoked) { - assert(nullifierCommitmentPath.leaf == roleNullifier, - "ShieldedAccessControl: Path must contain leaf matching role nullifier for the computed role commitment" - ); - } + const isRevoked = _roleCommitmentNullifiers.member(disclose(roleNullifier)); const hasRole = hasCommitment && !isRevoked; if (!hasRole || isRevoked) { - return false; + return false as HonestPathIndicator; } _roleCommitmentNullifiers.insert(disclose(roleNullifier)); - return true; + return true as HonestPathIndicator; } /** - * @description Computes the role commitment and associated role nullifier for a `roleId` | `accountId` - * pairing. A role commitment is valid for a `roleId` | `accountId` pairing if and only if a valid - * path exists in the `_operatorRoles` Merkle tree for the role commitment and a role nullifier doesn't - * exist for the role commitment in the `_roleCommitmentNullifiers` Merkle tree. + * @description Checks whether `accountId` holds `roleId` by verifying the role commitment exists in + * `_operatorRoles` and its nullifier is absent from `_roleCommitmentNullifiers`. + * + * @notice Completeness is not guaranteed for `observedHasRole` — a legitimately + * credentialed account may observe hasRole = false if the proving + * environment supplies an invalid Merkle path. Soundness is guaranteed + * for `isRevoked` — it is derived from a direct Set membership lookup + * and cannot be forged. * * @circuitInfo k=14, rows=16089 * * Disclosures: * - * - The Merkle tree path for the role commitment stored in the `_operatorRoles` Merkle tree. - * - The Merkle tree path for the nullifier commitment stored in the `_roleCommitmentNullifiers` Merkle tree. + * - A Merkle tree path to a role commitment stored in the `_operatorRoles` Merkle tree. + * - The computed role nullifier for a role commitment * * @param {Bytes<32>} roleId - The role identifier. * @param {Bytes<32>} accountId - The unique identifier of the account. - * @return {[]} - Empty tuple. + * @return {{observedHasRole: Boolean, isRevoked: Boolean}} roleCheck - A struct containing authorization information for a + * `roleId` | `accountId` pairing. */ circuit _checkRole(roleId: RoleIdentifier, accountId: AccountIdentifier): RoleCheck { Initializable_assertInitialized(); @@ -552,22 +618,11 @@ module ShieldedAccessControl { } const roleNullifier = _computeNullifier(roleCommitment); - const nullifierCommitmentPath = wit_getCommitmentNullifierPath(roleNullifier); - const isRevoked = - _roleCommitmentNullifiers.checkRoot( - merkleTreePathRoot<20, RoleNullifier>(disclose(nullifierCommitmentPath)) - ); - - // If the path is valid we assert it’s a path for the queried leaf (not some other leaf). - if (isRevoked) { - assert(nullifierCommitmentPath.leaf == roleNullifier, - "ShieldedAccessControl: Path must contain leaf matching role nullifier for the computed role commitment" - ); - } + const isRevoked = _roleCommitmentNullifiers.member(disclose(roleNullifier)); - const hasRole = hasCommitment && !isRevoked; + const observedHasRole = hasCommitment && !isRevoked; - return RoleCheck { hasRole, isRevoked }; + return RoleCheck { observedHasRole as HonestPathIndicator, isRevoked }; } /** diff --git a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact index 41f6dd4d..25b118ef 100644 --- a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact +++ b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact @@ -23,11 +23,6 @@ witness wit_getRoleCommitmentPath( roleCommitment: ShieldedAccessControl_RoleCommitment ): MerkleTreePath<20, ShieldedAccessControl_RoleCommitment>; -// witness is re-implemented in the Mock contract for testing -witness wit_getCommitmentNullifierPath( - roleNullifier: ShieldedAccessControl_RoleNullifier - ): MerkleTreePath<20, ShieldedAccessControl_RoleNullifier>; - /** * @description `isInit` is a param for testing. * @@ -84,8 +79,10 @@ export pure circuit _computeNullifier( as ShieldedAccessControl_RoleNullifier; } -export circuit callerHasRole(roleId: ShieldedAccessControl_RoleIdentifier): Boolean { - return ShieldedAccessControl_callerHasRole(roleId); +export circuit unverifiedCallerHasRole( + roleId: ShieldedAccessControl_RoleIdentifier + ): ShieldedAccessControl_HonestPathIndicator { + return ShieldedAccessControl_unverifiedCallerHasRole(roleId); } export circuit assertOnlyRole(roleId: ShieldedAccessControl_RoleIdentifier): [] { @@ -116,28 +113,11 @@ export circuit _checkRole( } const roleNullifier = _computeNullifier(roleCommitment); - const nullifierCommitmentPath = wit_getCommitmentNullifierPath(roleNullifier); - const isRevoked = - ShieldedAccessControl__roleCommitmentNullifiers.checkRoot( - merkleTreePathRoot<20, ShieldedAccessControl_RoleNullifier>( - disclose(nullifierCommitmentPath) - ) - ); + const isRevoked = ShieldedAccessControl__roleCommitmentNullifiers.member(disclose(roleNullifier)); - // If the path is valid we assert it’s a path for the queried leaf (not some other leaf). - if (isRevoked) { - assert(nullifierCommitmentPath.leaf == roleNullifier, - "ShieldedAccessControl: Path must contain leaf matching role nullifier for the computed role commitment" - ); - } + const observedHasRole = hasCommitment && !isRevoked; - const hasRole = hasCommitment && !isRevoked; - - if (hasRole) { - return ShieldedAccessControl_RoleCheck { hasRole, isRevoked }; - } else { - return ShieldedAccessControl_RoleCheck { hasRole, isRevoked }; - } + return ShieldedAccessControl_RoleCheck { observedHasRole as ShieldedAccessControl_HonestPathIndicator, isRevoked }; } export circuit getRoleAdmin( @@ -177,13 +157,13 @@ export circuit _setRoleAdmin( export circuit _grantRole( roleId: ShieldedAccessControl_RoleIdentifier, accountId: ShieldedAccessControl_AccountIdentifier - ): Boolean { + ): ShieldedAccessControl_HonestPathIndicator { return ShieldedAccessControl__grantRole(roleId, accountId); } export circuit _revokeRole( roleId: ShieldedAccessControl_RoleIdentifier, accountId: ShieldedAccessControl_AccountIdentifier - ): Boolean { + ): ShieldedAccessControl_HonestPathIndicator { return ShieldedAccessControl__revokeRole(roleId, accountId); } From 09e4a216000d8aeb46f7e272c81711be7cf1b403 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Mon, 9 Mar 2026 20:24:17 -0400 Subject: [PATCH 252/322] Remove unused witness --- .../ShieldedAccessControlWitnesses.ts | 33 ------------------- 1 file changed, 33 deletions(-) diff --git a/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts b/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts index 128327c0..98960dda 100644 --- a/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts +++ b/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts @@ -23,10 +23,6 @@ export interface IShieldedAccessControlWitnesses

{ context: WitnessContext, roleCommitment: Uint8Array, ): [P, MerkleTreePath]; - wit_getCommitmentNullifierPath( - context: WitnessContext, - nullifierCommitment: Uint8Array, - ): [P, MerkleTreePath]; } type RoleId = string; @@ -105,23 +101,6 @@ export const ShieldedAccessControlPrivateState = { }; return path ? path : defaultPath; }, - getCommitmentNullifierPath: ( - ledger: Ledger, - nullifierCommitment: Uint8Array, - ): MerkleTreePath => { - const path = - ledger.ShieldedAccessControl__roleCommitmentNullifiers.findPathForLeaf( - nullifierCommitment, - ); - const defaultPath: MerkleTreePath = { - leaf: new Uint8Array(32), - path: Array.from({ length: 20 }, () => ({ - sibling: { field: 0n }, - goes_left: false, - })), - }; - return path ? path : defaultPath; - }, }; /** @@ -149,16 +128,4 @@ export const ShieldedAccessControlWitnesses = ), ]; }, - wit_getCommitmentNullifierPath( - context: WitnessContext, - nullifierCommitment: Uint8Array, - ): [ShieldedAccessControlPrivateState, MerkleTreePath] { - return [ - context.privateState, - ShieldedAccessControlPrivateState.getCommitmentNullifierPath( - context.ledger, - nullifierCommitment, - ), - ]; - }, }); From 813fa9924264bfc80f85a2ada089e0478e700b32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Mon, 9 Mar 2026 20:26:07 -0400 Subject: [PATCH 253/322] update simulator --- .../ShieldedAccessControlSimulator.ts | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts index 6794c61d..d99b1d6a 100644 --- a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts +++ b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts @@ -74,8 +74,8 @@ export class ShieldedAccessControlSimulator extends ShieldedAccessControlSimulat return this.circuits.pure._computeNullifier(roleCommitment); } - public callerHasRole(roleId: Uint8Array): boolean { - return this.circuits.impure.callerHasRole(roleId); + public unverifiedCallerHasRole(roleId: Uint8Array): boolean { + return this.circuits.impure.unverifiedCallerHasRole(roleId); } /** @@ -201,20 +201,5 @@ export class ShieldedAccessControlSimulator extends ShieldedAccessControlSimulat roleCommitment, )[1]; }, - getNullifierPathWithFindForLeaf: ( - nullifierCommitment: Uint8Array, - ): MerkleTreePath | undefined => { - return this.getPublicState().ShieldedAccessControl__roleCommitmentNullifiers.findPathForLeaf( - nullifierCommitment, - ); - }, - getNullifierPathWithWitnessImpl: ( - nullifierCommitment: Uint8Array, - ): MerkleTreePath => { - return this.witnesses.wit_getCommitmentNullifierPath( - this.getWitnessContext(), - nullifierCommitment, - )[1]; - }, }; } From 36a6d0a09823427378165014557d1c48f64a48ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Mon, 9 Mar 2026 20:36:24 -0400 Subject: [PATCH 254/322] refactor tests --- .../access/test/ShieldedAccessControl.test.ts | 287 +++++++++--------- 1 file changed, 150 insertions(+), 137 deletions(-) diff --git a/contracts/src/access/test/ShieldedAccessControl.test.ts b/contracts/src/access/test/ShieldedAccessControl.test.ts index 84efb346..979c5cff 100644 --- a/contracts/src/access/test/ShieldedAccessControl.test.ts +++ b/contracts/src/access/test/ShieldedAccessControl.test.ts @@ -123,7 +123,7 @@ describe('ShieldedAccessControl', () => { ]; // Circuit calls should fail before the args are used const circuitsToFail: FailingCircuits[] = [ - ['callerHasRole', [UNINITIALIZED.roleId]], + ['unverifiedCallerHasRole', [UNINITIALIZED.roleId]], ['assertOnlyRole', [UNINITIALIZED.roleId]], ['_checkRole', [UNINITIALIZED.roleId, UNINITIALIZED.accountId]], ['getRoleAdmin', [UNINITIALIZED.roleId]], @@ -294,7 +294,6 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl.setPersistentCaller(ADMIN.publicKey); }); - // Test skipped due to compiler bug not updating assertion text it('should fail when wit_getRoleCommitmentPath returns valid path for a different roleId, accountId pairing', () => { shieldedAccessControl._grantRole( OPERATOR_1.roleId, @@ -321,39 +320,7 @@ describe('ShieldedAccessControl', () => { ); }); - // Test skipped due to compiler bug not updating assertion text - it('should fail when wit_getCommitmentNullifierPath returns valid path for a different nullifier', () => { - shieldedAccessControl._grantRole( - OPERATOR_1.roleId, - OPERATOR_1.accountId, - ); - shieldedAccessControl._revokeRole( - OPERATOR_1.roleId, - OPERATOR_1.accountId, - ); - - // Override witness to return valid path for OPERATOR_1 role nullifier - shieldedAccessControl.overrideWitness( - 'wit_getCommitmentNullifierPath', - () => { - const privateState = shieldedAccessControl.getPrivateState(); - const operator1MtPath = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.findPathForLeaf( - OPERATOR_1.roleNullifier, - ); - if (operator1MtPath) return [privateState, operator1MtPath]; - throw new Error('Merkle tree path should be defined'); - }, - ); - expect(() => { - shieldedAccessControl._checkRole(ADMIN.roleId, ADMIN.accountId); - }).toThrow( - 'ShieldedAccessControl: Path must contain leaf matching role nullifier for the computed role commitment', - ); - }); - - describe('hasRole field', () => { + describe('observedHasRole field', () => { type CheckRoleCases = [ badRoleId: boolean, badAccountId: boolean, @@ -367,72 +334,72 @@ describe('ShieldedAccessControl', () => { it.each( checkedCircuits, - )('hasRole should be false with badRoleId=%s badAccountId=%s', (_badRoleId, _badAccountId, args) => { + )('observedHasRole should be false with badRoleId=%s badAccountId=%s', (_badRoleId, _badAccountId, args) => { // Test protected circuit expect( ( shieldedAccessControl._checkRole as ( ...args: unknown[] ) => RoleCheck - )(...args).hasRole, + )(...args).observedHasRole, ).toBe(false); }); - it('hasRole should return false if role does not exist', () => { + it('observedHasRole should return false if role does not exist', () => { expect( shieldedAccessControl._checkRole( UNINITIALIZED.roleId, ADMIN.accountId, - ).hasRole, + ).observedHasRole, ).toBe(false); }); - it('hasRole should return true for granted role', () => { + it('observedHasRole should return true for granted role', () => { expect( shieldedAccessControl._checkRole(ADMIN.roleId, ADMIN.accountId) - .hasRole, + .observedHasRole, ).toBe(true); }); - it('hasRole should return false when revoked role is re-granted', () => { + it('observedHasRole should return false when revoked role is re-granted', () => { shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); expect( shieldedAccessControl._checkRole(ADMIN.roleId, ADMIN.accountId) - .hasRole, + .observedHasRole, ).toBe(false); }); - it('hasRole should return true for accountId with multiple roles', () => { + it('observedHasRole should return true for accountId with multiple roles', () => { shieldedAccessControl._grantRole(OPERATOR_1.roleId, ADMIN.accountId); shieldedAccessControl._grantRole(OPERATOR_2.roleId, ADMIN.accountId); shieldedAccessControl._grantRole(OPERATOR_3.roleId, ADMIN.accountId); expect( shieldedAccessControl._checkRole(ADMIN.roleId, ADMIN.accountId) - .hasRole, + .observedHasRole, ).toBe(true); expect( shieldedAccessControl._checkRole(OPERATOR_1.roleId, ADMIN.accountId) - .hasRole, + .observedHasRole, ).toBe(true); expect( shieldedAccessControl._checkRole(OPERATOR_2.roleId, ADMIN.accountId) - .hasRole, + .observedHasRole, ).toBe(true); expect( shieldedAccessControl._checkRole(OPERATOR_3.roleId, ADMIN.accountId) - .hasRole, + .observedHasRole, ).toBe(true); }); - it('hasRole should return false for revoked role, ', () => { + it('observedHasRole should return false for revoked role, ', () => { shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); const roleCheck = shieldedAccessControl._checkRole( ADMIN.roleId, ADMIN.accountId, ); - expect(roleCheck.hasRole).toBe(false); + expect(roleCheck.observedHasRole).toBe(false); }); it('hasRole should return false for bad _operatorRoles path', () => { @@ -442,7 +409,7 @@ describe('ShieldedAccessControl', () => { ); expect( shieldedAccessControl._checkRole(ADMIN.roleId, ADMIN.accountId) - .hasRole, + .observedHasRole, ).toBe(false); }); }); @@ -472,25 +439,6 @@ describe('ShieldedAccessControl', () => { ).toBe(false); }); - it('should appear active (hasRole=true, isRevoked=false) when nullifier path witness returns a bad path', () => { - // SIMULATOR LIMITATION: in the simulator, a bad nullifier path causes - // _roleCommitmentNullifiers.checkRoot() to return false, so isRevoked=false - // and the role appears active even though it was revoked. - // In production ZK this cannot happen: an incorrect path produces an - // invalid proof that the verifier rejects. - shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); - shieldedAccessControl.overrideWitness( - 'wit_getCommitmentNullifierPath', - RETURN_BAD_PATH, - ); - const roleCheck = shieldedAccessControl._checkRole( - ADMIN.roleId, - ADMIN.accountId, - ); - expect(roleCheck.isRevoked).toBe(false); - expect(roleCheck.hasRole).toBe(true); - }); - it('isRevoked should return true when revoked role is re-granted', () => { shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); @@ -710,16 +658,69 @@ describe('ShieldedAccessControl', () => { }); describe('_grantRole', () => { + describe('should fail', () => { + it('when valid merkle tree path in _operatorRoles does not contain matching leaf', () => { + shieldedAccessControl._grantRole( + OPERATOR_1.roleId, + OPERATOR_1.accountId, + ); + // Override witness to return valid path for OPERATOR_1 role commitment + shieldedAccessControl.overrideWitness( + 'wit_getRoleCommitmentPath', + () => { + const privateState = shieldedAccessControl.getPrivateState(); + const operator1MtPath = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf( + OPERATOR_1.roleCommitment, + ); + if (operator1MtPath) return [privateState, operator1MtPath]; + throw new Error('Merkle tree path should be defined'); + }, + ); + expect(() => { + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + }).toThrow( + 'ShieldedAccessControl: Path must contain leaf matching computed role commitment for the provided roleId, accountId pairing', + ); + }); + }); + describe('should return true', () => { - it('when granting a new role', () => { + it('when authorized user grants a new role', () => { + shieldedAccessControl.as(ADMIN.publicKey); + expect( + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId), + ).toBe(true); + }); + + it('when unauthorized user grants role', () => { + shieldedAccessControl.as(UNAUTHORIZED.publicKey) expect( shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId), ).toBe(true); }); + + it('when witnesses return a bad path', () => { + // SIMULATOR LIMITATION: in the simulator, a bad nullifier path causes + // _roleCommitmentNullifiers.checkRoot() to return false, so isRevoked=false + // and the role appears active even though it was revoked. + // In production ZK this cannot happen: an incorrect path produces an + // invalid proof that the verifier rejects. + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl.overrideWitness('wit_getRoleCommitmentPath', RETURN_BAD_PATH); + + const isGranted = shieldedAccessControl._grantRole( + ADMIN.roleId, + ADMIN.accountId, + ); + expect(isGranted).toBe(true); + }); }); describe('should update _operatorRoles merkle tree', () => { - it('when granting a new role', () => { + it('when authorized user grants a new role', () => { // check merkle tree is empty let merkleRoot = shieldedAccessControl .getPublicState() @@ -727,6 +728,28 @@ describe('ShieldedAccessControl', () => { expect(merkleRoot.field).toBe(0n); // check merkle tree is updated + shieldedAccessControl.as(ADMIN.publicKey); + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + merkleRoot = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.root(); + expect(merkleRoot).not.toBe(0n); + + // check path exists for new role + const merkleTreePath = shieldedAccessControl.getPublicState().ShieldedAccessControl__operatorRoles.findPathForLeaf(ADMIN.roleCommitment); + expect(merkleTreePath).toBeDefined(); + expect(merkleTreePath?.leaf).toStrictEqual(ADMIN.roleCommitment); + }); + + it('when unauthorized user grants a new role', () => { + // check merkle tree is empty + let merkleRoot = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.root(); + expect(merkleRoot.field).toBe(0n); + + // check merkle tree is updated + shieldedAccessControl.as(UNAUTHORIZED.publicKey); shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); merkleRoot = shieldedAccessControl .getPublicState() @@ -743,57 +766,47 @@ describe('ShieldedAccessControl', () => { describe('should return false', () => { it('when re-granting a role', () => { shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); - const merkleRoot = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.root(); expect( shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId), ).toBe(false); - expect( - shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.root(), - ).toEqual(merkleRoot); }); - it('should not re-grant revoked role', () => { + it('when re-granting revoked role', () => { shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); expect( shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId), ).toBe(false); + }); + }); + + describe('should not update _operatorRoles merkle tree', () => { + it('when re-granting a role', () => { + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); const merkleRoot = shieldedAccessControl .getPublicState() .ShieldedAccessControl__operatorRoles.root(); - expect( - shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.root(), - ).toEqual(merkleRoot); - }); - }) - - + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId) + const newMerkleRoot = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.root(); + expect(merkleRoot).toEqual(newMerkleRoot); + }); - it('should update Merkle tree root', () => { - const initialMtRoot = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.root(); - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); - const updatedMtRoot = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.root(); - expect(initialMtRoot).not.toEqual(updatedMtRoot); - }); + it('when re-granting revoked role', () => { + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + const merkleRoot = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.root(); - it('path for role commitment should exist', () => { - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); - const path = - shieldedAccessControl.privateState.getCommitmentPathWithFindForLeaf( - ADMIN.roleCommitment, - ); - expect(path).not.toBe(undefined); + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId) + const newMerkleRoot = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.root(); + expect(merkleRoot).toEqual(newMerkleRoot); + }); }); }); @@ -822,56 +835,58 @@ describe('ShieldedAccessControl', () => { ).toBe(true); }); - it('should update nullifier root on revoke', () => { - const initialNullifierRoot = shieldedAccessControl + it('should update nullifier set on revoke', () => { + const initialSetSize = shieldedAccessControl .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.root(); + .ShieldedAccessControl__roleCommitmentNullifiers.size(); + expect(initialSetSize).toBe(0n); + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); const isRevoked = shieldedAccessControl._revokeRole( ADMIN.roleId, ADMIN.accountId, ); expect(isRevoked).toBe(true); - const updatedNullifierRoot = shieldedAccessControl + + const updatedSetSize = shieldedAccessControl .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.root(); - expect(initialNullifierRoot).not.toEqual(updatedNullifierRoot); + .ShieldedAccessControl__roleCommitmentNullifiers.size(); + expect(updatedSetSize).toBe(1n); + expect(shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.member(ADMIN.roleNullifier)).toBe(true); }); - it('should not update nullifier root on failed revoke', () => { - const initialNullifierRoot = shieldedAccessControl + it('should not update nullifier set on failed revoke', () => { + const initialSetSize = shieldedAccessControl .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.root(); + .ShieldedAccessControl__roleCommitmentNullifiers.size(); + expect(initialSetSize).toBe(0n); + const isRevoked = shieldedAccessControl._revokeRole( ADMIN.roleId, ADMIN.accountId, ); expect(isRevoked).toBe(false); - const updatedNullifierRoot = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.root(); - expect(initialNullifierRoot).toEqual(updatedNullifierRoot); - }); - it('path for role nullifier should exist after revoke', () => { - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); - shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); - const path = - shieldedAccessControl.privateState.getNullifierPathWithFindForLeaf( - ADMIN.roleNullifier, - ); - expect(path).not.toBe(undefined); + const updatedSetSize = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.size(); + expect(updatedSetSize).toEqual(initialSetSize); + expect(shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.member(ADMIN.roleNullifier)).toBe(false) }); }); - describe('callerHasRole', () => { + describe('unverifiedCallerHasRole', () => { beforeEach(() => { shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); shieldedAccessControl.setPersistentCaller(ADMIN.publicKey); }); it('should return true for caller with granted role', () => { - expect(shieldedAccessControl.callerHasRole(ADMIN.roleId)).toBe(true); + expect(shieldedAccessControl.unverifiedCallerHasRole(ADMIN.roleId)).toBe(true); }); it('should return false for caller without role', () => { @@ -883,32 +898,30 @@ describe('ShieldedAccessControl', () => { OPERATOR_1.roleId, OPERATOR_1.secretNonce, ); - expect(shieldedAccessControl.callerHasRole(OPERATOR_1.roleId)).toBe( + expect(shieldedAccessControl.unverifiedCallerHasRole(OPERATOR_1.roleId)).toBe( false, ); }); it('should return false for caller with revoked role', () => { shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); - expect(shieldedAccessControl.callerHasRole(ADMIN.roleId)).toBe(false); + expect(shieldedAccessControl.unverifiedCallerHasRole(ADMIN.roleId)).toBe(false); }); it('should return false for revoked role after re-grant attempt', () => { shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); - expect(shieldedAccessControl.callerHasRole(ADMIN.roleId)).toBe(false); + expect(shieldedAccessControl.unverifiedCallerHasRole(ADMIN.roleId)).toBe(false); }); it('should return false for a different caller sharing the same private state', () => { // UNAUTHORIZED uses the same private state (ADMIN.secretNonce for ADMIN.roleId), // so their derived accountId won't match the committed one. shieldedAccessControl.setPersistentCaller(UNAUTHORIZED.publicKey); - expect(shieldedAccessControl.callerHasRole(ADMIN.roleId)).toBe(false); + expect(shieldedAccessControl.unverifiedCallerHasRole(ADMIN.roleId)).toBe(false); }); }); - - describe('getRoleAdmin', () => { it('should return zero bytes (DEFAULT_ADMIN_ROLE) for a role with no admin set', () => { expect(shieldedAccessControl.getRoleAdmin(OPERATOR_1.roleId)).toEqual( From f2ee664ebad81596648a4cad0c083e2c03d27db4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Mon, 9 Mar 2026 22:36:22 -0400 Subject: [PATCH 255/322] Update description --- contracts/src/access/ShieldedAccessControl.compact | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index 01d3d36e..e417850d 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -374,7 +374,7 @@ module ShieldedAccessControl { * if they are compromised (such as when a trusted device is misplaced). * * @warning Outside observers may be able to use timing and pattern analysis to weaken pseudonymity - * guarantees + * guarantees if renounceRole is used in tandem with other on-chain actions. * * @notice Completeness is not guaranteed — this circuit may fail to renounce * a legitimately held role if the proving environment supplies an invalid From 1a7476192085634209ceb98c7ec7a13d93775c8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Mon, 9 Mar 2026 23:06:21 -0400 Subject: [PATCH 256/322] Update test --- .../access/test/ShieldedAccessControl.test.ts | 74 +++++++++++-------- 1 file changed, 42 insertions(+), 32 deletions(-) diff --git a/contracts/src/access/test/ShieldedAccessControl.test.ts b/contracts/src/access/test/ShieldedAccessControl.test.ts index 979c5cff..28cf73d8 100644 --- a/contracts/src/access/test/ShieldedAccessControl.test.ts +++ b/contracts/src/access/test/ShieldedAccessControl.test.ts @@ -13,7 +13,7 @@ import type { ShieldedAccessControl_RoleCheck as RoleCheck, ZswapCoinPublicKey, } from '../../../artifacts/MockShieldedAccessControl/contract/index.js'; -import { ShieldedAccessControlPrivateState } from '../witnesses/ShieldedAccessControlWitnesses.js'; +import { ShieldedAccessControlPrivateState, ShieldedAccessControlWitnesses } from '../witnesses/ShieldedAccessControlWitnesses.js'; import { ShieldedAccessControlSimulator } from './simulators/ShieldedAccessControlSimulator.js'; const INSTANCE_SALT = new Uint8Array(32).fill(48473095); @@ -700,23 +700,6 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId), ).toBe(true); }); - - it('when witnesses return a bad path', () => { - // SIMULATOR LIMITATION: in the simulator, a bad nullifier path causes - // _roleCommitmentNullifiers.checkRoot() to return false, so isRevoked=false - // and the role appears active even though it was revoked. - // In production ZK this cannot happen: an incorrect path produces an - // invalid proof that the verifier rejects. - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); - shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); - shieldedAccessControl.overrideWitness('wit_getRoleCommitmentPath', RETURN_BAD_PATH); - - const isGranted = shieldedAccessControl._grantRole( - ADMIN.roleId, - ADMIN.accountId, - ); - expect(isGranted).toBe(true); - }); }); describe('should update _operatorRoles merkle tree', () => { @@ -778,6 +761,29 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId), ).toBe(false); }); + + it('when witness returns a bad path', () => { + // a bad role commitment path causes _operatorRoles.checkRoot() to return false, so observedHasRole=false + // isRevoked=false because the role has not been revoked yet so this will allow a duplicate role + // commitment to be added to the merkle tree. However, duplicate role commitments do not + // violate our security invariant + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl.overrideWitness('wit_getRoleCommitmentPath', RETURN_BAD_PATH); + + const isGranted = shieldedAccessControl._grantRole( + ADMIN.roleId, + ADMIN.accountId, + ); + expect(isGranted).toBe(true); + + // Reset witness back to the default implementation + shieldedAccessControl.overrideWitness('wit_getRoleCommitmentPath', ShieldedAccessControlWitnesses().wit_getRoleCommitmentPath); + shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + expect(shieldedAccessControl.getPublicState().ShieldedAccessControl__roleCommitmentNullifiers.member(ADMIN.roleNullifier)).toBe(true); + + const roleCheck = shieldedAccessControl._checkRole(ADMIN.roleId, ADMIN.accountId); + expect(roleCheck.isRevoked).toBe(true); + }); }); describe('should not update _operatorRoles merkle tree', () => { @@ -976,7 +982,7 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl._checkRole( OPERATOR_1.roleId, OPERATOR_1.accountId, - ).hasRole, + ).observedHasRole, ).toBe(true); }); @@ -1042,7 +1048,7 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl._checkRole( OPERATOR_2.roleId, OPERATOR_2.accountId, - ).hasRole, + ).observedHasRole, ).toBe(true); }); }); @@ -1068,7 +1074,7 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl._checkRole( OPERATOR_1.roleId, OPERATOR_1.accountId, - ).hasRole, + ).observedHasRole, ).toBe(false); }); @@ -1087,9 +1093,9 @@ describe('ShieldedAccessControl', () => { OPERATOR_1.roleId, OPERATOR_1.accountId, ); - const nullifierRoot = shieldedAccessControl + const nullifierSetSize = shieldedAccessControl .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.root(); + .ShieldedAccessControl__roleCommitmentNullifiers.size(); expect(() => shieldedAccessControl.revokeRole( OPERATOR_1.roleId, @@ -1099,8 +1105,8 @@ describe('ShieldedAccessControl', () => { expect( shieldedAccessControl .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.root(), - ).toEqual(nullifierRoot); + .ShieldedAccessControl__roleCommitmentNullifiers.size(), + ).toEqual(nullifierSetSize); }); }); @@ -1116,7 +1122,7 @@ describe('ShieldedAccessControl', () => { ).not.toThrow(); expect( shieldedAccessControl._checkRole(ADMIN.roleId, ADMIN.accountId) - .hasRole, + .observedHasRole, ).toBe(false); }); @@ -1138,19 +1144,23 @@ describe('ShieldedAccessControl', () => { ).not.toThrow(); expect( shieldedAccessControl._checkRole(ADMIN.roleId, ADMIN.accountId) - .hasRole, + .observedHasRole, ).toBe(false); }); it('should update nullifier root on successful renounce', () => { - const initialNullifierRoot = shieldedAccessControl + const nullifierSetSize = shieldedAccessControl .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.root(); + .ShieldedAccessControl__roleCommitmentNullifiers.size(); + expect(nullifierSetSize).toBe(0n); shieldedAccessControl.renounceRole(ADMIN.roleId, ADMIN.accountId); - const updatedNullifierRoot = shieldedAccessControl + const updatedSetSize = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.size(); + expect(updatedSetSize).toEqual(1n); + expect(shieldedAccessControl .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.root(); - expect(initialNullifierRoot).not.toEqual(updatedNullifierRoot); + .ShieldedAccessControl__roleCommitmentNullifiers.member(ADMIN.roleNullifier)) }); }); }); From 67b85d1842dea9ad9a7403650c7d480fa49cce1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Tue, 10 Mar 2026 23:51:05 -0400 Subject: [PATCH 257/322] Refactor contract --- .../src/access/ShieldedAccessControl.compact | 570 ++++++++---------- .../mocks/MockShieldedAccessControl.compact | 62 +- 2 files changed, 273 insertions(+), 359 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index e417850d..68cd5fd1 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -10,21 +10,40 @@ pragma language_version >= 0.21.0; * This module provides a shielded role-based access control (RBAC) mechanism, where roles can be used to * represent a set of permissions. Roles are stored as Merkle tree commitments to avoid * disclosing information about role holders. Role commitments are created with the following - * hashing scheme SHA256(roleId | accountId | instanceSalt | commitmentDomain). Where - * - `accountId` is a unique SHA256 hash of a user's ZswapCoinPublicKey and a 32 byte secret nonce value - * held in a user's local private state - * - `roleId` is a unique `Bytes<32>` identifier + * hashing scheme, where `‖` denotes concatenation and all values are `Bytes<32>`: + * + * ``` + * roleCommitment := SHA256( role ‖ accountId ‖ instanceSalt ‖ commitmentDomain ) where + * + * accountId := SHA256( zcpk ‖ nonce ‖ instanceSalt ‖ accountIdDomain ) + * + * roleNullifier := SHA256( roleCommitment ‖ nullifierDomain ) + * + * commitmentDomain := pad(32, "ShieldedAccessControl:commitment") + * accountIdDomain := pad(32, "ShieldedAccessControl:accountId") + * nullifierDomain := pad(32, "ShieldedAccessControl:nullifier") + * ``` + * + * - `roleCommitment` is a Merkle tree leaf committing a `(roleId, accountId)` pairing, inserted + * into `_operatorRoles` on grant. The `instanceSalt` prevents commitment collisions across + * deployments that share the same role identifiers. + * - `accountId` is a privacy-preserving identity commitment. `zcpk` is the user's + * `ZswapCoinPublicKey`; `nonce` is a per-role secret held in local private state + * (supplied by `wit_secretNonce`); `instanceSalt` ensures the same key and nonce + * cannot be correlated across contracts. + * - `roleNullifier` is a one-time burn token inserted into `_roleCommitmentNullifiers` on + * revocation. Its presence permanently invalidates the corresponding role commitment, + * making re-grant under the same `accountId` impossible without generating a new identity. * - `instanceSalt` is an immutable, cryptographically strong random value provided on deployment * - `commitmentDomain` is the following zero padded 32 byte string: "ShieldedAccessControl:commitment" + * - `accountIdDomain` is the following zero padded 32 byte string: "ShieldedAccessControl:accountId" + * - `commitmentDomain` is the following zero padded 32 byte string: "ShieldedAccessControl:nullifier" * * In this RBAC model, role commitments behave like private bearer tokens. Possession of a valid, non-revoked role * commitment grants authorization. Revocation permanently burns the role instance, requiring explicit new issuance * under a new account identifier. Users must rotate their identity (new nonce) to be re-authorized. This creates * stronger security invariants over traditional RBAC systems and enables privacy-preserving identity rotation. * - * @notice Using the SHA256 hashing function comes at a significant performance cost. In the future, we - * plan on migrating to a ZK-friendly hashing function when an implementation is available. - * * Roles are referred to by their `Bytes<32>` identifier. These should be exposed * in the top-level contract and be unique. One way to achieve this is by * using `export sealed ledger` hash digests that are initialized in the top-level contract: @@ -68,30 +87,27 @@ pragma language_version >= 0.21.0; * - Outside observers will know which role identifiers are admin identifiers. * - Outside observers will have knowledge of all role identifiers. * - Outside observers will have knowledge of all role nullifiers and can link every nullifier to its corresponding commitment. - * - Outside observers will have knowledge of role additions and revocations. + * - Outside observers will know when roles are a granted and revoked. * - Outside observers can infer the total number of role grants made across all roles — not per-role counts, but the cumulative total. * - Outside observers can link calls made by the same role instance across time. - * - If a user ever reveals their nonce or reuses it poorly, they can be deanonymized retroactively. + * - Users can be retroactively deanonymized if their nonce is exposed or reused poorly. * - Outside observers will NOT be able to identify the public address of any role holder * so long as secret nonce values are kept private and generated using cryptographically * secure random values. * * @dev Security Considerations: * - The `secretNonce` must be kept private. Loss of the nonce prevents role holders - * and admins from proving access or transferring it. Public exposure, poor nonce selection or nonce + * and admins from proving access or transferring it. Nonce exposure, poor nonce selection and nonce * reuse may weaken privacy guarantees and allow retroactive deanonymization. - * - Role authorization depends on two mechanisms with different trust levels: - * commitment presence is verified via witness-supplied Merkle paths and is - * subject to honest-path limitations; revocation status is verified via direct - * Set membership and provides a cryptographic guarantee regardless of witness behavior. * - It's strongly recommended to use cryptographically secure random values for the `_instanceSalt`. - * Failure to do so could lead to the exposure of public keys. + * Failure to do so may weaken privacy guarantees. * - The `_instanceSalt` is immutable and used to differentiate deployments. * - The `_operatorRoles` Merkle tree has a fixed capacity of 2^20 leaf slots. - * Deployers should monitor slot consumption off-chain. A malicious or - * careless admin can exhaust capacity through repeated grants of the same - * roleId | accountId pairing, as the commitment existence check provides - * no cryptographic guarantee against duplicate insertions. + * Deployers should monitor slot consumption off-chain. A careless admin can exhaust + * capacity through repeated grants of the same active (role, accountId) pairing. + * + * @notice Using the SHA256 hashing function comes at a significant performance cost. In the future, we + * plan on migrating to a ZK-friendly hashing function when an implementation is available. * * @notice Missing Features and Improvements: * - Role events @@ -103,20 +119,19 @@ module ShieldedAccessControl { import "../utils/Utils" prefix Utils_; import "../security/Initializable" prefix Initializable_; + // TODO: Standardize types across contracts https://github.com/OpenZeppelin/compact-contracts/issues/368 export new type RoleCommitment = Bytes<32>; export new type RoleIdentifier = Bytes<32>; export new type AccountIdentifier = Bytes<32>; export new type AdminIdentifier = Bytes<32>; export new type RoleNullifier = Bytes<32>; - export new type HonestPathIndicator = Boolean; - type ZcpkOrContractAddress = Either; /** * @ledger _operatorRoles - * @description A Merkle tree of role commitments stored as SHA256(roleId | accountId | instanceSalt | commitmentDomain) + * @description A Merkle tree of role commitments stored as SHA256(role | accountId | instanceSalt | commitmentDomain) * Role commitments are derived from a public role identifier (e.g., `persistentHash>(pad(32, "MY_ROLE")`), * an account identifier (e.g., `SHA256(zcpk, nonce, instanceSalt, accountIdDomain)`), the `instanceSalt`, and a domain separator. - * @type {Bytes<32>} RoleCommitment - A role commitment created by the following hash: SHA256( roleId | accountId | instanceSalt | commitmentDomain). + * @type {Bytes<32>} RoleCommitment - A role commitment created by the following hash: SHA256( role | accountId | instanceSalt | commitmentDomain).  */ export ledger _operatorRoles: MerkleTree<20, RoleCommitment>; @@ -155,7 +170,8 @@ module ShieldedAccessControl { * @witness wit_getRoleCommitmentPath * @description Returns a path to a role commitment in the `_operatorRoles` Merkle tree if one exists. Otherwise, returns an invalid path. * - * @param {Bytes<32>} RoleCommitment - A commitment created by the following hash: SHA256( roleId | accountId | instanceSalt | commitmentDomain). + * @param {Bytes<32>} roleCommitment - A commitment created by the following hash: SHA256( role | accountId | instanceSalt | commitmentDomain). + * * @return {MerkleTreePath<20, Bytes<32>>} - The Merkle tree path to `roleCommitment` in the `_operatorRoles` Merkle tree  */ witness wit_getRoleCommitmentPath( @@ -164,30 +180,16 @@ module ShieldedAccessControl { /** * @witness wit_secretNonce - * @description A private per-accountId nonce used in deriving the shielded account identifier. + * @description Returns a private per-role nonce used in deriving the shielded account identifier for a role. * * Combined with the user's ZswapCoinPublicKey as `SHA256(zcpk, nonce, instanceSalt, accountIdDomain)` to produce an obfuscated, * unlinkable identity commitment. Nonce MUST be unique per role to avoid cross-role linking. * - * @param {Bytes<32>} roleId - The unique identifier of a role. - */ - witness wit_secretNonce(roleId: RoleIdentifier): Bytes<32>; - - /** - * @description A struct containing auth information for a particular `roleId` | `accountId` pairing. - * - * @type {Boolean} observedHasRole - Honest-path indicator derived from a witness-supplied Merkle path and can be spoofed by a - * malicious prover supplying a garbage path, producing a false negative (observedHasRole = false for a legitimately credentialed holder). - * It MUST NOT be used as a security gate in calling circuits. Its failure mode is a liveness concern only — a spoofed absence - * cannot grant access, it can only deny it. + * @param {Bytes<32>} role - The unique identifier of a role. * - * @type {Boolean} isRevoked - A boolean flag indicating if a nullifier exists for `roleCommitment`. It is derived from a direct - * Set membership lookup requiring no witness. It cannot be spoofed by a malicious prover and MAY be used as a security gate in calling circuits. + * @returns {Bytes<32>} secretNonce - A private per-role nonce used in deriving the shielded account identifier. */ - export struct RoleCheck { - observedHasRole: HonestPathIndicator; - isRevoked: Boolean; - } + witness wit_secretNonce(role: RoleIdentifier): Bytes<32>; /** * @description Initializes the contract by storing the `instanceSalt` that acts as a privacy additive @@ -204,6 +206,7 @@ module ShieldedAccessControl { * * @param {Bytes<32>} instanceSalt - Contract salt to prevent duplicate commitments if * users reuse their PK and secretNonce witness across different contracts (not recommended). Must not be zero. + * * @returns {[]} Empty tuple. */ export circuit initialize(instanceSalt: Bytes<32>): [] { @@ -214,66 +217,63 @@ module ShieldedAccessControl { } /** - * @description Returns `true` if a caller is authorized for `roleId`. - * - * @notice Completeness is not guaranteed — this circuit may fail for a - * legitimately credentialed caller if the proving environment supplies - * an invalid Merkle path. Soundness is guaranteed — this circuit will - * never return true for an unauthorized caller. + * @description Returns `true` if a caller proves ownership of `role` and is not revoked. MAY return false for a legitimately credentialed + * caller if the proving environment supplies an invalid Merkle path. This circuit will never return true for an + * unauthorized caller. * * @circuitInfo k=15, rows=22128 * + * Requirements: + * + * - caller must not provide valid Merkle tree path for a different `(role, accountId)` pairing. + * * Disclosures: * - * - A Merkle tree path to a role commitment stored in the `_operatorRoles` Merkle tree. - * - The nullifier commitment stored in the `_roleCommitmentNullifiers` set. + * - A Merkle tree path to a role commitment. + * - A role commitment corresponding to a `(role, accountId)` pairing. + * - A nullifier for the respective role commitment. + * + * @param {Bytes<32>} role - The role identifier. * - * @param {Bytes<32>} roleId - The role identifier. - * @return {Boolean} - An honest-path indicator determining if a caller has the specified role. + * @return {Boolean} - A boolean determining if a caller successfully proved ownership of `role`  */ - export circuit unverifiedCallerHasRole(roleId: RoleIdentifier): HonestPathIndicator { + export circuit proveCallerRole(role: RoleIdentifier): Boolean { Initializable_assertInitialized(); - const callerAsEither = - ZcpkOrContractAddress { is_left: true, - left: ownPublicKey(), - right: ContractAddress { bytes: pad(32, "") } }; - const nonce = wit_secretNonce(roleId); - const accountId = _computeAccountId(callerAsEither, nonce); - return _checkRole(roleId, accountId).observedHasRole; + const nonce = wit_secretNonce(role); + const accountId = _computeAccountId(ownPublicKey(), nonce); + return _validateRole(role, accountId); } /** - * @description Reverts if caller is not authorized for `roleId`. - * - * @notice Completeness is not guaranteed — this circuit may fail for a - * legitimately credentialed caller if the proving environment supplies - * an invalid Merkle path. Soundness is guaranteed — this circuit will - * never return true for an unauthorized caller. + * @description Reverts if caller cannot provide a valid proof of ownership for `role`. * * @circuitInfo k=15, rows=22130 * * Requirements: * - * - caller must have authorization for `roleId`. + * - caller must prove ownership of `role`. + * - caller must not provide valid Merkle tree path for a different `(role, accountId)` pairing. * * Disclosures: * - * - A Merkle tree path to a role commitment stored in the `_operatorRoles` Merkle tree. - * - The nullifier commitment stored in the `_roleCommitmentNullifiers` set. + * - A Merkle tree path to a role commitment. + * - A role commitment corresponding to a `(role, accountId)` pairing. + * - A nullifier for the respective role commitment. + * + * @param {Bytes<32>} role - The role identifier. * - * @param {Bytes<32>} roleId - The role identifier. * @return {[]} - Empty tuple. */ - export circuit assertOnlyRole(roleId: RoleIdentifier): [] { + export circuit assertOnlyRole(role: RoleIdentifier): [] { Initializable_assertInitialized(); - assert(unverifiedCallerHasRole(roleId), "ShieldedAccessControl: unauthorized account"); + assert(proveCallerRole(role), "ShieldedAccessControl: unauthorized account"); } /** - * @description Returns the admin role that controls `roleId` or a zero - * byte array if `roleId` doesn't exist. See {grantRole} and {revokeRole}. + * @description Returns the admin role that controls `role` or a zero + * byte array if `role` doesn't exist. See {grantRole} and {revokeRole}. * * To change a role’s admin use {_setRoleAdmin}. * @@ -281,365 +281,295 @@ module ShieldedAccessControl { * * Disclosures: * - * - The role identifier + * - A role identifier. + * + * @param {Bytes<32>} role - The role identifier. * - * @param {Bytes<32>} roleId - The role identifier. - * @return {Bytes<32>} roleAdmin - The admin role that controls `roleId`. + * @return {Bytes<32>} roleAdmin - The admin role that controls `role`. */ - export circuit getRoleAdmin(roleId: RoleIdentifier): AdminIdentifier { + export circuit getRoleAdmin(role: RoleIdentifier): AdminIdentifier { Initializable_assertInitialized(); - if (_adminRoles.member(disclose(roleId))) { - return _adminRoles.lookup(disclose(roleId)); + if (_adminRoles.member(disclose(role))) { + return _adminRoles.lookup(disclose(role)); } return default> as AdminIdentifier; } /** - * @description Grants `roleId` to `accountId` by inserting a role commitment unique to the - * `roleId` | `accountId` pairing into the `_operatorRoles` Merkle tree. A valid `roleId` can only be - * issued to `accountId` once. Once revoked, a new `accountId` must be generated to be re-authorized for - * `roleId` - * - * @notice Completeness is not guaranteed — this circuit may fail for a - * legitimately credentialed admin if the proving environment supplies - * an invalid Merkle path. Soundness is guaranteed — this circuit will never grant a role to an - * `accountId` unless the caller is authorized as admin for `roleId`. + * @description Grants `role` to `accountId` by inserting a role commitment unique to the + * `(role, accountId)` pairing into the `_operatorRoles` Merkle tree. Duplicate role commitments can be issued + * so long as they remain unrevoked. This does not yield any additional authority and simply wastes + * limited Merkle tree storage slots. Once revoked, a role cannot be re-granted. A new `accountId` must be + * generated to be re-authorized for a revoked `role`. * * Requirements: * - * - caller must be admin for `roleId` + * - caller must prove they're an admin for `role`. + * - caller must not provide valid Merkle tree path for a different `(role, accountId)` pairing. * * @circuitInfo k=16, rows=39993 * * Disclosures: * - * - The role commitment produced by SHA256(roleId | accountId | instanceSalt | commitmentDomain ). - * - The role identifier - * - A Merkle tree path to a role commitment stored in the `_operatorRoles` Merkle tree. - * - The nullifier commitment stored in the `_roleCommitmentNullifiers` set. + * - A Merkle tree path to a role commitment. + * - A role commitment corresponding to a `(role, accountId)` pairing. + * - A nullifier for the respective role commitment. + * - A role identifier. * - * @param {Bytes<32>} roleId - The role identifier. + * @param {Bytes<32>} role - The role identifier. * @param {Bytes<32>} accountId - The unique identifier of the account. + * * @return {[]} - Empty tuple. */ - export circuit grantRole(roleId: RoleIdentifier, accountId: AccountIdentifier): [] { + export circuit grantRole(role: RoleIdentifier, accountId: AccountIdentifier): [] { Initializable_assertInitialized(); - assertOnlyRole(getRoleAdmin(roleId) as RoleIdentifier); - _grantRole(roleId, accountId); + assertOnlyRole(getRoleAdmin(role) as RoleIdentifier); + _grantRole(role, accountId); } /** - * @description Revokes `roleId` from `accountId` by inserting a role nullifier into the - * `_roleNullifiers` set. Once revoked, a new `accountId` must be generated to be re-authorized for - * `roleId` + * @description Grants `role` to `accountId` by inserting a role commitment unique to the + * `(role, accountId)` pairing into the `_operatorRoles` Merkle tree. Duplicate role commitments can be issued + * so long as they remain unrevoked. This does not yield any additional authority and simply wastes + * limited Merkle tree storage slots. Once revoked, a role cannot be re-granted to the same `accountId`. A new `accountId` must be + * generated to be re-authorized for a revoked `role`. * - * @notice Completeness is not guaranteed — this circuit may fail for a - * legitimately credentialed admin if the proving environment supplies - * an invalid Merkle path. Soundness is guaranteed — this circuit will never revoke a role from an - * `accountId` unless the caller is authorized as admin for `roleId`. + * Internal circuit without access restriction. * - * @circuitInfo k=18, rows=138517 + * ## Storage Caveat * - * Requirements: + * `_operatorRoles` is a fixed-depth Merkle tree with a maximum capacity of + * 2^20 = 1,048,576 leaf slots. At the time of writing, it's not possible to check non-membership of a value + * without using un-trusted witness inputs. This creates two risks: * - * - caller must be admin for `roleId` + * 1. Accidental duplicate grants through administrative error. + * 2. A deliberate griefing attack by a malicious admin exhausting the tree. * - * @circuitInfo k=18, rows=138761 + * Tree capacity should be treated as an operational concern and slot consumption + * should be monitored off-chain. The TypeScript layer should additionally + * validate commitment absence before submitting a grant transaction as a + * defence-in-depth measure against accidental exhaustion. + * + * @dev Commitment and nullifier checks are inlined in this circuit to avoid an expensive re-computation of the role commitment and nullifier + * + * @circuitInfo k=15, rows=18115 * * Disclosures: * - * - The role commitment produced by SHA256(roleId | accountId | instanceSalt | commitmentDomain ). - * - The role identifier - * - A Merkle tree path to a role commitment stored in the `_operatorRoles` Merkle tree. - * - The nullifier commitment stored in the `_roleCommitmentNullifiers` set. + * - A role commitment corresponding to a `(role, accountId)` pairing. + * - A nullifier for the respective role commitment. * - * @param {Bytes<32>} roleId - The role identifier. - * @param {Bytes<32>} accountId - The unique identifier of the account. - * @return {[]} - Empty tuple. + * @param {Bytes<32>} role - The role identifier. + * @param {Bytes<32>} accountId - The account identifier. + * + * @return {Boolean} isGranted - Returns true if a role was granted successfully. */ - export circuit revokeRole(roleId: RoleIdentifier, accountId: AccountIdentifier): [] { + export circuit _grantRole( + role: RoleIdentifier, + accountId: AccountIdentifier + ): Boolean { Initializable_assertInitialized(); - assertOnlyRole(getRoleAdmin(roleId) as RoleIdentifier); - _revokeRole(roleId, accountId); + const roleCommitment = _computeRoleCommitment(role, accountId); + const roleNullifier = _computeNullifier(roleCommitment); + const isRevoked = _roleCommitmentNullifiers.member(disclose(roleNullifier)); + + if (isRevoked) { + return false; + } + + _operatorRoles.insert(disclose(roleCommitment)); + return true; } /** - * @description Revokes `roleId` from the calling account. - * - * @notice Roles are often managed via {grantRole} and {revokeRole}: this circuit's - * purpose is to provide a mechanism for accounts to lose their privileges - * if they are compromised (such as when a trusted device is misplaced). - * - * @warning Outside observers may be able to use timing and pattern analysis to weaken pseudonymity - * guarantees if renounceRole is used in tandem with other on-chain actions. + * @description Permanently revokes `role` from `accountId` by inserting a role nullifier into the + * `_roleCommitmentNullifiers` set. Once revoked, a new `accountId` must be generated to be re-authorized for + * `role`. At this time, proofs of non-membership on values in the Merkle tree LDT are not possible + * so a `(role, accountId)` pairing that does not exist can still be revoked. * - * @notice Completeness is not guaranteed — this circuit may fail to renounce - * a legitimately held role if the proving environment supplies an invalid - * Merkle path. Soundness is guaranteed — this circuit will never revoke - * a role the caller does not hold. - * - * @circuitInfo k=17, rows=108992 + * @circuitInfo k=18, rows=138517 * * Requirements: * - * - The caller must provide a valid `accountId` for a `roleId` + * - caller must prove they're an admin for `role`. + * - caller must not provide valid Merkle tree path for a different `(role, accountId)` pairing. + * + * @circuitInfo k=18, rows=138761 * * Disclosures: * - * - The role nullifier produced by SHA256(roleCommitment | nullifierDomain). - * - A Merkle tree path to a role commitment stored in the `_operatorRoles` Merkle tree. - * - The computed role nullifier for a role commitment + * - A Merkle tree path to a role commitment. + * - A role commitment corresponding to a `(role, accountId)` pairing. + * - A nullifier for the respective role commitment. + * - A role identifier. * - * @param {Bytes<32>} roleId - The role identifier. + * @param {Bytes<32>} role - The role identifier. * @param {Bytes<32>} accountId - The unique identifier of the account. + * * @return {[]} - Empty tuple. */ - export circuit renounceRole(roleId: RoleIdentifier, accountIdConfirmation: AccountIdentifier): [] { + export circuit revokeRole(role: RoleIdentifier, accountId: AccountIdentifier): [] { Initializable_assertInitialized(); - const nonce = wit_secretNonce(roleId); - const callerAsEither = - ZcpkOrContractAddress { is_left: true, - left: ownPublicKey(), - right: ContractAddress { bytes: pad(32, "") } }; - assert(accountIdConfirmation == _computeAccountId(callerAsEither, nonce), - "ShieldedAccessControl: bad confirmation" - ); - - _revokeRole(roleId, accountIdConfirmation); + assertOnlyRole(getRoleAdmin(role) as RoleIdentifier); + _revokeRole(role, accountId); } - /** - * @description Sets `adminId` as `roleId`'s admin identifier. Internal circuit without access restriction. + /** + * @description Permanently revokes `role` from `accountId` by inserting a role nullifier into the + * `_roleCommitmentNullifiers` set. Once revoked, a new `accountId` must be generated to be re-authorized for + * `role`. At this time, proofs of non-membership on values in the Merkle tree LDT are not possible + * so a `(role, accountId)` pairing that does not exist can still be revoked. Returns false if a role is already + * revoked. Internal circuit without access restriction. * - * @circuitInfo k=10, rows=583 + * @circuitInfo k=15, rows=18115 * * Disclosures: * - * - The role identifier - * - The account identifier + * - A nullifier for the respective role commitment. * - * @param {Bytes<32>} roleId - The role identifier. - * @param {Bytes<32>} adminId - The admin role identifier. - * @return {[]} - Empty tuple. + * @param {Bytes<32>} role - The role identifier. + * @param {Bytes<32>} accountId - The account identifier. + * + * @return {Boolean} isRevoked - Returns true if operation completes successfully. */ - export circuit _setRoleAdmin(roleId: RoleIdentifier, adminId: AdminIdentifier): [] { + export circuit _revokeRole( + role: RoleIdentifier, + accountId: AccountIdentifier + ): Boolean { Initializable_assertInitialized(); - _adminRoles.insert(disclose(roleId), disclose(adminId)); + const roleCommitment = _computeRoleCommitment(role, accountId); + const roleNullifier = _computeNullifier(roleCommitment); + const isRevoked = _roleCommitmentNullifiers.member(disclose(roleNullifier)); + + if (isRevoked) { + return false; + } + + _roleCommitmentNullifiers.insert(disclose(roleNullifier)); + return true; } /** - * @description Grants `roleId` to `accountId` by inserting a role commitment unique to the - * `roleId` | `accountId` pairing into the `_operatorRoles` Merkle tree. A valid `roleId` can only be - * issued to `accountId` once. Once revoked, a new `accountId` must be generated to be re-authorized for - * `roleId`. Revoked roles cannot be re-granted. - * - * Internal circuit without access restriction. - * - * @notice Completeness is not guaranteed — this circuit MAY insert duplicate - * commitments if the proving environment supplies an invalid Merkle path. - * Soundness is guaranteed — this circuit will never insert a commitment - * for a `roleId` | `accountId` pairing whose nullifier exists in - * `_roleCommitmentNullifiers`. + * @description Revokes `role` from the calling account. * - * ## Storage Caveat - * - * `_operatorRoles` is a fixed-depth Merkle tree with a maximum capacity of - * 2^20 = 1,048,576 leaf slots. Without the commitment existence check, - * repeated calls to `_grantRole` for the same `roleId` | `accountId` pairing - * will consume a new slot on each call, granting no additional authority but - * permanently exhausting tree capacity. This creates two risks: + * @notice Roles are often managed via {grantRole} and {revokeRole}: this circuit's + * purpose is to provide a mechanism for accounts to lose their privileges + * if they are compromised (such as when a trusted device is misplaced). * - * 1. Accidental duplicate grants through administrative error. - * 2. A deliberate griefing attack by a malicious admin exhausting the tree. + * @warning Outside observers may be able to use timing and pattern analysis to weaken pseudonymity + * guarantees if renounceRole is used in tandem with other on-chain actions. * - * The commitment existence check cannot provide a hard security guarantee - * against either risk — a prover supplying a garbage path will always receive - * hasCommitment = false and bypass the check. It therefore functions only as - * a best-effort, honest-path guard that prevents duplicate insertions when - * the admin and proving environment are both operating correctly. + * @circuitInfo k=17, rows=108992 * - * Tree capacity should be treated as an operational concern and slot consumption - * should be monitored off-chain. The TypeScript witness layer should additionally - * validate commitment absence before submitting a grant transaction as a - * defence-in-depth measure against accidental exhaustion. + * Requirements: * - * @dev Commitment and nullifier checks are inlined in this circuit to avoid an expensive re-computation of the role commitment and nullifier - * - * @circuitInfo k=15, rows=18115 + * - The caller must provide a valid `accountId` for a `role`. * * Disclosures: * - * - The role commitment produced by SHA256(roleId | accountId | instanceSalt | commitmentDomain). - * - A Merkle tree path to a role commitment stored in the `_operatorRoles` Merkle tree. - * - The computed role nullifier for a role commitment + * - A nullifier for the respective role commitment. * - * @param {Bytes<32>} roleId - The role identifier. - * @param {Bytes<32>} accountId - The account identifier. - * @return {Boolean} observedIsGranted - Honest-path indicator only. Returns true if the operation was observed to succeed by - * the proving environment. This value MUST NOT be used as a security gate in calling circuits — it can be spoofed by - * a malicious prover and carries no on-chain guarantee. + * @param {Bytes<32>} role - The role identifier. + * @param {Bytes<32>} accountId - The unique identifier of the account. + * + * @return {[]} - Empty tuple. */ - export circuit _grantRole( - roleId: RoleIdentifier, - accountId: AccountIdentifier - ): HonestPathIndicator { + export circuit renounceRole(role: RoleIdentifier, accountIdConfirmation: AccountIdentifier): [] { Initializable_assertInitialized(); - const roleCommitment = _computeRoleCommitment(roleId, accountId); - - // Best-effort honest-path guard against duplicate insertions. - // This check is NOT a security guarantee — a prover supplying a garbage - // path will always receive hasCommitment = false and bypass it. Its sole - // purpose is to conserve _operatorRoles tree capacity under normal - // operation. See the Storage Caveat in the circuit documentation above. - const roleCommitmentPath = wit_getRoleCommitmentPath(roleCommitment); - const hasCommitment = - _operatorRoles.checkRoot( - merkleTreePathRoot<20, RoleCommitment>(disclose(roleCommitmentPath)) - ); - - // If the path is valid we assert it’s a path for the queried leaf (not some other leaf). - if (hasCommitment) { - assert(roleCommitmentPath.leaf == roleCommitment, - "ShieldedAccessControl: Path must contain leaf matching computed role commitment for the provided roleId, accountId pairing" - ); - } - - const roleNullifier = _computeNullifier(roleCommitment); - const isRevoked = _roleCommitmentNullifiers.member(disclose(roleNullifier)); - - const hasRole = hasCommitment && !isRevoked; - if (hasRole || isRevoked) { - return false as HonestPathIndicator; - } + const nonce = wit_secretNonce(role); + assert(accountIdConfirmation == _computeAccountId(ownPublicKey(), nonce), + "ShieldedAccessControl: bad confirmation" + ); - _operatorRoles.insert(disclose(roleCommitment)); - return true as HonestPathIndicator; + _revokeRole(role, accountIdConfirmation); } /** - * @description Revokes `roleId` from `accountId` by inserting a role nullifier into `_roleCommitmentNullifiers`. - * Internal circuit without access restriction. A nullifier cannot be inserted for a commitment that doesn't exist. - * - * @notice Completeness is not guaranteed — this circuit MAY return false negative values and fail to revoke an active role - * if the proving environment supplies an invalid Merkle path. Soundness is guaranteed — this circuit will - * never revoke a role that does not exist or is already inactive. - * - * @dev Commitment and nullifier checks are inlined in this circuit to avoid an expensive re-computation of the role commitment and nullifier + * @description Sets `adminId` as `role`'s admin identifier. Internal circuit without access restriction. * - * @circuitInfo k=15, rows=18115 + * @circuitInfo k=10, rows=583 * * Disclosures: * - * - The role nullifier produced by SHA256(roleCommitment | nullifierDomain). - * - The Merkle tree path for the role commitment stored in the `_operatorRoles` Merkle tree. - * - The computed role nullifier for a role commitment + * - The role identifier + * - The admin identifier * - * @param {Bytes<32>} roleId - The role identifier. - * @param {Bytes<32>} accountId - The account identifier. - * @return {Boolean} observedIsRevoked - Honest-path indicator only. Returns true if the operation was observed to succeed by - * the proving environment. This value MUST NOT be used as a security gate in calling circuits — it can be spoofed by - * a malicious prover and carries no on-chain guarantee. + * @param {Bytes<32>} role - The role identifier. + * @param {Bytes<32>} adminId - The admin role identifier. + * + * @return {[]} - Empty tuple. */ - export circuit _revokeRole( - roleId: RoleIdentifier, - accountId: AccountIdentifier - ): HonestPathIndicator { + export circuit _setRoleAdmin(role: RoleIdentifier, adminId: AdminIdentifier): [] { Initializable_assertInitialized(); - const roleCommitment = _computeRoleCommitment(roleId, accountId); - const roleCommitmentPath = wit_getRoleCommitmentPath(roleCommitment); - const hasCommitment = - _operatorRoles.checkRoot( - merkleTreePathRoot<20, RoleCommitment>(disclose(roleCommitmentPath)) - ); - - // If the path is valid we assert it’s a path for the queried leaf (not some other leaf). - if (hasCommitment) { - assert(roleCommitmentPath.leaf == roleCommitment, - "ShieldedAccessControl: Path must contain leaf matching computed role commitment for the provided roleId, accountId pairing" - ); - } - - const roleNullifier = _computeNullifier(roleCommitment); - const isRevoked = _roleCommitmentNullifiers.member(disclose(roleNullifier)); - - const hasRole = hasCommitment && !isRevoked; - if (!hasRole || isRevoked) { - return false as HonestPathIndicator; - } - - _roleCommitmentNullifiers.insert(disclose(roleNullifier)); - return true as HonestPathIndicator; + _adminRoles.insert(disclose(role), disclose(adminId)); } /** - * @description Checks whether `accountId` holds `roleId` by verifying the role commitment exists in - * `_operatorRoles` and its nullifier is absent from `_roleCommitmentNullifiers`. - * - * @notice Completeness is not guaranteed for `observedHasRole` — a legitimately - * credentialed account may observe hasRole = false if the proving - * environment supplies an invalid Merkle path. Soundness is guaranteed - * for `isRevoked` — it is derived from a direct Set membership lookup - * and cannot be forged. + * @description Verifies whether `accountId` holds `role`. This circuit MAY return false for a + * legitimately credentialed account if the proving environment supplies an invalid Merkle path. * * @circuitInfo k=14, rows=16089 * + * Requirements: + * + * - caller must not provide valid Merkle tree path for a different `(role, accountId)` pairing. + * * Disclosures: * - * - A Merkle tree path to a role commitment stored in the `_operatorRoles` Merkle tree. - * - The computed role nullifier for a role commitment + * - A Merkle tree path to a role commitment. + * - A role commitment corresponding to a `(role, accountId)` pairing. + * - A nullifier for the respective role commitment. * - * @param {Bytes<32>} roleId - The role identifier. + * @param {Bytes<32>} role - The role identifier. * @param {Bytes<32>} accountId - The unique identifier of the account. - * @return {{observedHasRole: Boolean, isRevoked: Boolean}} roleCheck - A struct containing authorization information for a - * `roleId` | `accountId` pairing. + * + * @return {Boolean} isValidRole - A boolean indicating whether `accountId` has a valid role */ - circuit _checkRole(roleId: RoleIdentifier, accountId: AccountIdentifier): RoleCheck { + circuit _validateRole(role: RoleIdentifier, accountId: AccountIdentifier): Boolean { Initializable_assertInitialized(); - const roleCommitment = _computeRoleCommitment(roleId, accountId); + const roleCommitment = _computeRoleCommitment(role, accountId); const roleCommitmentPath = wit_getRoleCommitmentPath(roleCommitment); - const hasCommitment = + const isValidPath = _operatorRoles.checkRoot( merkleTreePathRoot<20, RoleCommitment>(disclose(roleCommitmentPath)) ); // If the path is valid we assert it’s a path for the queried leaf (not some other leaf). - if (hasCommitment) { + if (isValidPath) { assert(roleCommitmentPath.leaf == roleCommitment, - "ShieldedAccessControl: Path must contain leaf matching computed role commitment for the provided roleId, accountId pairing" + "ShieldedAccessControl: Path must contain leaf matching computed role commitment for the provided role, accountId pairing" ); } const roleNullifier = _computeNullifier(roleCommitment); const isRevoked = _roleCommitmentNullifiers.member(disclose(roleNullifier)); - const observedHasRole = hasCommitment && !isRevoked; - - return RoleCheck { observedHasRole as HonestPathIndicator, isRevoked }; + return isValidPath && !isRevoked; } /** - * @description Computes the role commitment from the given `accountId` and `roleId`. + * @description Computes the role commitment from the given `accountId` and `role`. * * ## Account ID (`accountId`) * The `accountId` is expected to be computed off-chain as: - * `accountId = SHA256(zcpk, nonce, instanceSalt, accountDomain)` + * `accountId = SHA256(zcpk, nonce, instanceSalt, accountIdDomain)` * * - `zcpk`: The account's ZswapCoinPublicKey. * - `nonce`: A secret nonce scoped to the role. * * ## Role Commitment Derivation - * `roleCommitment = SHA256(roleId, accountId, instanceSalt, commitmentDomain)` + * `roleCommitment = SHA256(role, accountId, instanceSalt, commitmentDomain)` * * - `accountId`: See above. - * - `roleId`: A unique role identifier. + * - `role`: A unique role identifier. * - `instanceSalt`: A unique per-deployment salt, stored during initialization. * This prevents commitment collisions across deployments. * - `commitmentDomain`: Domain separator `"ShieldedAccessControl:commitment"` (padded to 32 bytes) to prevent @@ -651,18 +581,19 @@ module ShieldedAccessControl { * * - Contract is initialized. * - * @param {Bytes<32>} roleId - The unique identifier of a role. - * @param {Bytes<32>} accountId - The unique identifier of the account calculated by `SHA256(zcpk, nonce, instanceSalt, accountDomain)`. - * @returns {Bytes<32>} The commitment derived from `accountId` and `roleId`. + * @param {Bytes<32>} role - The unique identifier of a role. + * @param {Bytes<32>} accountId - The unique identifier of the account calculated by `SHA256(zcpk, nonce, instanceSalt, accountIdDomain)`. + * + * @returns {Bytes<32>} The commitment derived from `accountId` and `role`. */ circuit _computeRoleCommitment( - roleId: RoleIdentifier, + role: RoleIdentifier, accountId: AccountIdentifier, ): RoleCommitment { Initializable_assertInitialized(); return persistentHash>>( - [roleId as Bytes<32>, + [role as Bytes<32>, accountId as Bytes<32>, _instanceSalt, pad(32, "ShieldedAccessControl:commitment")] @@ -680,7 +611,8 @@ module ShieldedAccessControl { * - `nullifierDomain`: Domain separator `"ShieldedAccessControl:nullifier"` (padded to 32 bytes) to prevent * hash collisions when extending the module or using similar commitment schemes. * - * @param {} roleCommitment - The role commitment for a particular `roleId` | `accountId` pairing. + * @param {} roleCommitment - The role commitment for a particular `(role, accountId)` pairing. + * * @returns {Bytes<32>} roleNullifier - The associated nullifier for `roleCommitment`. */ pure circuit _computeNullifier(roleCommitment: RoleCommitment): RoleNullifier { @@ -695,7 +627,7 @@ module ShieldedAccessControl { * ZswapCoinPublicKey and a secret nonce. * * ## ID Derivation - * `accountId = SHA256(zcpk, nonce, instanceSalt, accountDomain)` + * `accountId = SHA256(zcpk, nonce, instanceSalt, accountIdDomain)` * * - `zcpk`: The ZswapCoinPublicKey of the caller. This is passed explicitly to allow * for off-chain derivation, testing, or scenarios where the caller is @@ -711,28 +643,18 @@ module ShieldedAccessControl { * This value is later used in role commitment hashing, * and acts as a privacy-preserving alternative to a raw public key. * - * @notice This module allows access to be tied to an identity commitment derived - * from a public key and secret nonce. - * While typically used with user public keys, this mechanism may also - * support contract addresses as identifiers in future contract-to-contract - * interactions. Both are treated as 32-byte values (`Bytes<32>`). - * * @circuitInfo k=13, rows=6705 * - * Requirements: - * - * - `zcpk` is not a ContractAddress. - * - * @param {Either} zcpk - The public key of the identity being committed. + * @param {ZswapCoinPublicKey} zcpk - The public key of the identity being committed. * @param {Bytes<32>} nonce - A private nonce to scope the commitment. + * * @returns {Bytes<32>} accountId - The computed account ID. */ - circuit _computeAccountId(pk: ZcpkOrContractAddress, nonce: Bytes<32>): AccountIdentifier { + circuit _computeAccountId(zcpk: ZswapCoinPublicKey, nonce: Bytes<32>): AccountIdentifier { Initializable_assertInitialized(); - assert(pk.is_left, "ShieldedAccessControl: contract address roles are not yet supported"); return persistentHash>>( - [pk.left.bytes, nonce, _instanceSalt, pad(32, "ShieldedAccessControl:accountId")] + [zcpk.bytes, nonce, _instanceSalt, pad(32, "ShieldedAccessControl:accountId")] ) as AccountIdentifier; } diff --git a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact index 25b118ef..bfcc697c 100644 --- a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact +++ b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact @@ -16,7 +16,7 @@ export { ZswapCoinPublicKey, ShieldedAccessControl_DEFAULT_ADMIN_ROLE, ShieldedAccessControl__operatorRoles, ShieldedAccessControl__roleCommitmentNullifiers, - ShieldedAccessControl_RoleCheck, }; + }; // witness is re-implemented in the Mock contract for testing witness wit_getRoleCommitmentPath( @@ -54,14 +54,13 @@ export circuit _computeRoleCommitment( // circuit is reimplemented in the Mock contract for testing export circuit _computeAccountId( - pk: Either, + zcpk: ZswapCoinPublicKey, nonce: Bytes<32> ): ShieldedAccessControl_AccountIdentifier { Initializable_assertInitialized(); - assert(pk.is_left, "ShieldedAccessControl: contract address roles are not yet supported"); return persistentHash>>( - [pk.left.bytes, + [zcpk.bytes, nonce, ShieldedAccessControl__instanceSalt, pad(32, "ShieldedAccessControl:accountId")] @@ -79,46 +78,39 @@ export pure circuit _computeNullifier( as ShieldedAccessControl_RoleNullifier; } -export circuit unverifiedCallerHasRole( +export circuit proveCallerRole( roleId: ShieldedAccessControl_RoleIdentifier - ): ShieldedAccessControl_HonestPathIndicator { - return ShieldedAccessControl_unverifiedCallerHasRole(roleId); + ): Boolean { + return ShieldedAccessControl_proveCallerRole(roleId); } export circuit assertOnlyRole(roleId: ShieldedAccessControl_RoleIdentifier): [] { ShieldedAccessControl_assertOnlyRole(roleId); } -// checkRole is re-implemented in the Mock contract for testing -export circuit _checkRole( - roleId: ShieldedAccessControl_RoleIdentifier, - accountId: ShieldedAccessControl_AccountIdentifier - ): ShieldedAccessControl_RoleCheck { - Initializable_assertInitialized(); +// _validateRole is re-implemented in the Mock contract for testing +export circuit _validateRole(role: ShieldedAccessControl_RoleIdentifier, accountId: ShieldedAccessControl_AccountIdentifier): Boolean { + Initializable_assertInitialized(); - const roleCommitment = _computeRoleCommitment(roleId, accountId); - const roleCommitmentPath = wit_getRoleCommitmentPath(roleCommitment); - const hasCommitment = - ShieldedAccessControl__operatorRoles.checkRoot( - merkleTreePathRoot<20, ShieldedAccessControl_RoleCommitment>( - disclose(roleCommitmentPath) - ) - ); - - // If the path is valid we assert it’s a path for the queried leaf (not some other leaf). - if (hasCommitment) { - assert(roleCommitmentPath.leaf == roleCommitment, - "ShieldedAccessControl: Path must contain leaf matching computed role commitment for the provided roleId, accountId pairing" - ); - } + const roleCommitment = _computeRoleCommitment(role, accountId); + const roleCommitmentPath = wit_getRoleCommitmentPath(roleCommitment); + const isValidPath = + ShieldedAccessControl__operatorRoles.checkRoot( + merkleTreePathRoot<20, ShieldedAccessControl_RoleCommitment>(disclose(roleCommitmentPath)) + ); - const roleNullifier = _computeNullifier(roleCommitment); - const isRevoked = ShieldedAccessControl__roleCommitmentNullifiers.member(disclose(roleNullifier)); + // If the path is valid we assert it’s a path for the queried leaf (not some other leaf). + if (isValidPath) { + assert(roleCommitmentPath.leaf == roleCommitment, + "ShieldedAccessControl: Path must contain leaf matching computed role commitment for the provided role, accountId pairing" + ); + } - const observedHasRole = hasCommitment && !isRevoked; + const roleNullifier = _computeNullifier(roleCommitment); + const isRevoked = ShieldedAccessControl__roleCommitmentNullifiers.member(disclose(roleNullifier)); - return ShieldedAccessControl_RoleCheck { observedHasRole as ShieldedAccessControl_HonestPathIndicator, isRevoked }; -} + return isValidPath && !isRevoked; + } export circuit getRoleAdmin( roleId: ShieldedAccessControl_RoleIdentifier @@ -157,13 +149,13 @@ export circuit _setRoleAdmin( export circuit _grantRole( roleId: ShieldedAccessControl_RoleIdentifier, accountId: ShieldedAccessControl_AccountIdentifier - ): ShieldedAccessControl_HonestPathIndicator { + ): Boolean { return ShieldedAccessControl__grantRole(roleId, accountId); } export circuit _revokeRole( roleId: ShieldedAccessControl_RoleIdentifier, accountId: ShieldedAccessControl_AccountIdentifier - ): ShieldedAccessControl_HonestPathIndicator { + ): Boolean { return ShieldedAccessControl__revokeRole(roleId, accountId); } From 8fefb4eedc075b3973e2e54cb8e0f199f4bea397 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Tue, 10 Mar 2026 23:53:31 -0400 Subject: [PATCH 258/322] Refactor simulator --- .../simulators/ShieldedAccessControlSimulator.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts index d99b1d6a..3f3a0f8c 100644 --- a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts +++ b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts @@ -8,7 +8,6 @@ import { type Either, ledger, Contract as MockShieldedAccessControl, - type ShieldedAccessControl_RoleCheck as RoleCheck, type ZswapCoinPublicKey, } from '../../../../artifacts/MockShieldedAccessControl/contract/index.js'; import { @@ -64,18 +63,18 @@ export class ShieldedAccessControlSimulator extends ShieldedAccessControlSimulat } public _computeAccountId( - pk: Either, + zcpk: ZswapCoinPublicKey, nonce: Uint8Array, ): Uint8Array { - return this.circuits.impure._computeAccountId(pk, nonce); + return this.circuits.impure._computeAccountId(zcpk, nonce); } public _computeNullifier(roleCommitment: Uint8Array): Uint8Array { return this.circuits.pure._computeNullifier(roleCommitment); } - public unverifiedCallerHasRole(roleId: Uint8Array): boolean { - return this.circuits.impure.unverifiedCallerHasRole(roleId); + public proveCallerRole(roleId: Uint8Array): boolean { + return this.circuits.impure.proveCallerRole(roleId); } /** @@ -87,8 +86,8 @@ export class ShieldedAccessControlSimulator extends ShieldedAccessControlSimulat this.circuits.impure.assertOnlyRole(roleId); } - public _checkRole(roleId: Uint8Array, accountId: Uint8Array): RoleCheck { - return this.circuits.impure._checkRole(roleId, accountId); + public _validateRole(roleId: Uint8Array, accountId: Uint8Array): boolean { + return this.circuits.impure._validateRole(roleId, accountId); } /** From e8ba943fb791497869fbd3207e24124bdbfc1e34 Mon Sep 17 00:00:00 2001 From: 0xisk Date: Wed, 11 Mar 2026 18:22:22 +0100 Subject: [PATCH 259/322] refactor: harden the shieldedaccesscontrol lib by some improvements and fixing bugs --- .../src/access/ShieldedAccessControl.compact | 524 +++++++++++------- .../mocks/MockShieldedAccessControl.compact | 152 +++-- 2 files changed, 391 insertions(+), 285 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index 68cd5fd1..ca2ed497 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -37,7 +37,7 @@ pragma language_version >= 0.21.0; * - `instanceSalt` is an immutable, cryptographically strong random value provided on deployment * - `commitmentDomain` is the following zero padded 32 byte string: "ShieldedAccessControl:commitment" * - `accountIdDomain` is the following zero padded 32 byte string: "ShieldedAccessControl:accountId" - * - `commitmentDomain` is the following zero padded 32 byte string: "ShieldedAccessControl:nullifier" + * - `nullifierDomain` is the following zero padded 32 byte string: "ShieldedAccessControl:nullifier" * * In this RBAC model, role commitments behave like private bearer tokens. Possession of a valid, non-revoked role * commitment grants authorization. Revocation permanently burns the role instance, requiring explicit new issuance @@ -59,11 +59,11 @@ pragma language_version >= 0.21.0; * } * ``` * - * To restrict access to a circuit, use {assertOnlyRole}: + * To restrict access to a circuit, use {_checkRole}: * * ```compact * circuit foo(): [] { - * ShieldedAccessControl_assertOnlyRole(MY_ROLE); + * ShieldedAccessControl__checkRole(MY_ROLE); * // ... rest of circuit logic * } * ``` @@ -119,12 +119,7 @@ module ShieldedAccessControl { import "../utils/Utils" prefix Utils_; import "../security/Initializable" prefix Initializable_; - // TODO: Standardize types across contracts https://github.com/OpenZeppelin/compact-contracts/issues/368 export new type RoleCommitment = Bytes<32>; - export new type RoleIdentifier = Bytes<32>; - export new type AccountIdentifier = Bytes<32>; - export new type AdminIdentifier = Bytes<32>; - export new type RoleNullifier = Bytes<32>; /** * @ledger _operatorRoles @@ -139,14 +134,14 @@ module ShieldedAccessControl { * @ledger _adminRoles * @description Mapping from a role identifier to an admin role identifier.  */ - export ledger _adminRoles: Map; + export ledger _adminRoles: Map, Bytes<32>>; /** * @description A set of nullifiers used to prove a role has been revoked - * @type {Bytes<32>} RoleNullifier - A role nullifier created by the following hash: SHA256(roleCommitment | nullifierDomain). + * @type {Bytes<32>} Bytes<32> - A role nullifier created by the following hash: SHA256(roleCommitment | nullifierDomain). * @type {Set} _roleCommitmentNullifiers  */ - export ledger _roleCommitmentNullifiers: Set; + export ledger _roleCommitmentNullifiers: Set>; /** * @description The default admin role for all roles. Only accounts with this role will be able to grant or revoke other roles @@ -189,7 +184,7 @@ module ShieldedAccessControl { * * @returns {Bytes<32>} secretNonce - A private per-role nonce used in deriving the shielded account identifier. */ - witness wit_secretNonce(role: RoleIdentifier): Bytes<32>; + witness wit_secretNonce(role: Bytes<32>): Bytes<32>; /** * @description Initializes the contract by storing the `instanceSalt` that acts as a privacy additive @@ -221,7 +216,7 @@ module ShieldedAccessControl { * caller if the proving environment supplies an invalid Merkle path. This circuit will never return true for an * unauthorized caller. * - * @circuitInfo k=15, rows=22128 + * @circuitInfo k=15, rows=19239 * * Requirements: * @@ -237,18 +232,201 @@ module ShieldedAccessControl { * * @return {Boolean} - A boolean determining if a caller successfully proved ownership of `role`  */ - export circuit proveCallerRole(role: RoleIdentifier): Boolean { + export circuit hasRole(role: Bytes<32>): Boolean { Initializable_assertInitialized(); + return _hasRole(role, _computeAccountId(role, ownPublicKey())); + } + + /** + * @description Returns `true` if `account` holds `role` and is not revoked. The account identifier + * is derived internally from the provided `ZswapCoinPublicKey` and the caller's secret nonce. + * MAY return false for a legitimately credentialed account if the proving environment supplies + * an invalid Merkle path. This circuit will never return true for an unauthorized account. + * + * @circuitInfo k=15, rows=19259 + * + * Requirements: + * + * - caller must not provide valid Merkle tree path for a different `(role, accountId)` pairing. + * + * Disclosures: + * + * - A Merkle tree path to a role commitment. + * - A role commitment corresponding to a `(role, accountId)` pairing. + * - A nullifier for the respective role commitment. + * + * @param {Bytes<32>} role - The role identifier. + * @param {ZswapCoinPublicKey} account - The public key of the account to check. + * + * @return {Boolean} - A boolean determining if `account` holds `role`. + */ + export circuit hasRole(role: Bytes<32>, account: ZswapCoinPublicKey): Boolean { + Initializable_assertInitialized(); + + return _hasRole(role, _computeAccountId(role, account)); + } + + /** + * @description Computes the unique identifier (`accountId`) of an account from their + * ZswapCoinPublicKey and a secret nonce. + * + * ## ID Derivation + * `accountId = SHA256(zcpk, nonce, instanceSalt, accountIdDomain)` + * + * - `zcpk`: The ZswapCoinPublicKey of the caller. This is passed explicitly to allow + * for off-chain derivation, testing, or scenarios where the caller is + * different from the subject of the computation. We recommend using an Air-Gapped Public Key. + * - `nonce`: A secret nonce tied to the identity. The generation strategy is + * left to the user, offering different security/convenience trade-offs. + * - `instanceSalt`: A unique per-deployment salt, stored during initialization. + * This prevents commitment collisions across deployments. + * - `accountIdDomain`: Domain separator `"ShieldedAccessControl:accountId"` (padded to 32 bytes) to prevent + * hash collisions when extending the module or using similar commitment schemes. + * + * The result is a 32-byte commitment that uniquely identifies the account. + * This value is later used in role commitment hashing, + * and acts as a privacy-preserving alternative to a raw public key. + * + * @circuitInfo k=13, rows=6684 + * + * @param {Bytes<32>} role - The role identifier used to derive the per-role secret nonce. + * @param {ZswapCoinPublicKey} account - The public key of the identity being committed. + * + * @returns {Bytes<32>} accountId - The computed account ID. + */ + circuit _computeAccountId(role: Bytes<32>, account: ZswapCoinPublicKey): Bytes<32> { const nonce = wit_secretNonce(role); - const accountId = _computeAccountId(ownPublicKey(), nonce); - return _validateRole(role, accountId); + return persistentHash>>( + [account.bytes, nonce, _instanceSalt, pad(32, "ShieldedAccessControl:accountId")] + ); + } + + /** + * @description Public wrapper for {_computeAccountId}. Computes the unique identifier (`accountId`) + * of an account from a role identifier and a `ZswapCoinPublicKey`. The secret nonce is derived + * internally via {wit_secretNonce}. + * + * @circuitInfo k=13, rows=6684 + * + * @param {Bytes<32>} role - The role identifier used to derive the per-role secret nonce. + * @param {ZswapCoinPublicKey} account - The public key of the identity being committed. + * + * @returns {Bytes<32>} accountId - The computed account ID. + */ + export circuit computeAccountId(role: Bytes<32>, account: ZswapCoinPublicKey): Bytes<32> { + Initializable_assertInitialized(); + + return _computeAccountId(role, account); + } + + /** + * @description Verifies whether `accountId` holds `role`. This circuit MAY return false for a + * legitimately credentialed account if the proving environment supplies an invalid Merkle path. + * + * @circuitInfo k=14, rows=13183 + * + * Requirements: + * + * - caller must not provide valid Merkle tree path for a different `(role, accountId)` pairing. + * + * Disclosures: + * + * - A Merkle tree path to a role commitment. + * - A role commitment corresponding to a `(role, accountId)` pairing. + * - A nullifier for the respective role commitment. + * + * @param {Bytes<32>} role - The role identifier. + * @param {Bytes<32>} accountId - The unique identifier of the account. + * + * @return {Boolean} isValidRole - A boolean indicating whether `accountId` has a valid role + */ + circuit _hasRole(role: Bytes<32>, accountId: Bytes<32>): Boolean { + const roleCommitment = _computeRoleCommitment(role, accountId); + const roleCommitmentPath = wit_getRoleCommitmentPath(roleCommitment); + const isValidPath = + _operatorRoles.checkRoot( + merkleTreePathRoot<20, RoleCommitment>(disclose(roleCommitmentPath)) + ); + + // If the path is valid we assert it’s a path for the queried leaf (not some other leaf). + if (isValidPath) { + assert(roleCommitmentPath.leaf == roleCommitment, + "ShieldedAccessControl: Path must contain leaf matching computed role commitment for the provided role, accountId pairing" + ); + } + + const roleNullifier = _computeNullifier(roleCommitment); + const isRevoked = _roleCommitmentNullifiers.member(disclose(roleNullifier)); + + return isValidPath && !isRevoked; + } + + /** + * @description Computes the role commitment from the given `accountId` and `role`. + * + * ## Account ID (`accountId`) + * The `accountId` is expected to be computed off-chain as: + * `accountId = SHA256(zcpk, nonce, instanceSalt, accountIdDomain)` + * + * - `zcpk`: The account's ZswapCoinPublicKey. + * - `nonce`: A secret nonce scoped to the role. + * + * ## Role Commitment Derivation + * `roleCommitment = SHA256(role, accountId, instanceSalt, commitmentDomain)` + * + * - `accountId`: See above. + * - `role`: A unique role identifier. + * - `instanceSalt`: A unique per-deployment salt, stored during initialization. + * This prevents commitment collisions across deployments. + * - `commitmentDomain`: Domain separator `"ShieldedAccessControl:commitment"` (padded to 32 bytes) to prevent + * hash collisions when extending the module or using similar commitment schemes. + * + * @circuitInfo k=13, rows=6423 + * + * Requirements: + * + * - Contract is initialized. + * + * @param {Bytes<32>} role - The unique identifier of a role. + * @param {Bytes<32>} accountId - The unique identifier of the account calculated by `SHA256(zcpk, nonce, instanceSalt, accountIdDomain)`. + * + * @returns {Bytes<32>} The commitment derived from `accountId` and `role`. + */ + circuit _computeRoleCommitment(role: Bytes<32>, accountId: Bytes<32>,): RoleCommitment { + return persistentHash>>( + [role as Bytes<32>, + accountId as Bytes<32>, + _instanceSalt, + pad(32, "ShieldedAccessControl:commitment")] + ) + as RoleCommitment; + } + + /** + * @description Computes the role nullifier for a given `roleCommitment`. + * + * ## Role Nullifier Derivation + * `roleNullifier = SHA256(roleCommitment, nullifierDomain)` + * + * - `roleCommitment`: See `_computeRoleCommitment`. + * - `nullifierDomain`: Domain separator `"ShieldedAccessControl:nullifier"` (padded to 32 bytes) to prevent + * hash collisions when extending the module or using similar commitment schemes. + * + * @param {Bytes<32>} roleCommitment - The role commitment for a particular `(role, accountId)` pairing. + * + * @returns {Bytes<32>} roleNullifier - The associated nullifier for `roleCommitment`. + */ + pure circuit _computeNullifier(roleCommitment: RoleCommitment): Bytes<32> { + return persistentHash>>( + [roleCommitment as Bytes<32>, pad(32, "ShieldedAccessControl:nullifier")] + ); } /** * @description Reverts if caller cannot provide a valid proof of ownership for `role`. * - * @circuitInfo k=15, rows=22130 + * @circuitInfo k=15, rows=19239 * * Requirements: * @@ -265,10 +443,62 @@ module ShieldedAccessControl { * * @return {[]} - Empty tuple. */ - export circuit assertOnlyRole(role: RoleIdentifier): [] { + export circuit _checkRole(role: Bytes<32>): [] { + Initializable_assertInitialized(); + + _onlyRole(role, _computeAccountId(role, ownPublicKey())); + } + + /** + * @description Reverts if `account` does not hold `role`. The account identifier is derived + * internally from the provided `ZswapCoinPublicKey` and the caller's secret nonce. + * + * @circuitInfo k=15, rows=19259 + * + * Requirements: + * + * - `account` must hold `role`. + * - caller must not provide valid Merkle tree path for a different `(role, accountId)` pairing. + * + * Disclosures: + * + * - A Merkle tree path to a role commitment. + * - A role commitment corresponding to a `(role, accountId)` pairing. + * - A nullifier for the respective role commitment. + * + * @param {Bytes<32>} role - The role identifier. + * @param {ZswapCoinPublicKey} account - The public key of the account to check. + * + * @return {[]} - Empty tuple. + */ + export circuit _checkRole(role: Bytes<32>, account: ZswapCoinPublicKey): [] { Initializable_assertInitialized(); - assert(proveCallerRole(role), "ShieldedAccessControl: unauthorized account"); + _onlyRole(role, _computeAccountId(role, account)); + } + + /** + * @description Reverts if `accountId` does not hold `role` or has been revoked. Internal circuit + * without access restriction. Used as the shared assertion primitive for {_checkRole} overloads. + * + * Requirements: + * + * - `accountId` must hold `role`. + * - caller must not provide valid Merkle tree path for a different `(role, accountId)` pairing. + * + * Disclosures: + * + * - A Merkle tree path to a role commitment. + * - A role commitment corresponding to a `(role, accountId)` pairing. + * - A nullifier for the respective role commitment. + * + * @param {Bytes<32>} role - The role identifier. + * @param {Bytes<32>} accountId - The unique identifier of the account. + * + * @return {[]} - Empty tuple. + */ + circuit _onlyRole(role: Bytes<32>, accountId: Bytes<32>): [] { + assert(_hasRole(role, accountId), "ShieldedAccessControl: unauthorized account"); } /** @@ -287,13 +517,30 @@ module ShieldedAccessControl { * * @return {Bytes<32>} roleAdmin - The admin role that controls `role`. */ - export circuit getRoleAdmin(role: RoleIdentifier): AdminIdentifier { + export circuit getRoleAdmin(role: Bytes<32>): Bytes<32> { Initializable_assertInitialized(); + return _getRoleAdmin(role); + } + + /** + * @description Returns the admin role that controls `role` or a zero byte array if no admin + * has been set. Internal circuit without initialization check, used by {getRoleAdmin}, + * {grantRole}, and {revokeRole}. + * + * Disclosures: + * + * - A role identifier. + * + * @param {Bytes<32>} role - The role identifier. + * + * @return {Bytes<32>} roleAdmin - The admin role that controls `role`. + */ + circuit _getRoleAdmin(role: Bytes<32>): Bytes<32> { if (_adminRoles.member(disclose(role))) { return _adminRoles.lookup(disclose(role)); } - return default> as AdminIdentifier; + return default>; } /** @@ -308,7 +555,7 @@ module ShieldedAccessControl { * - caller must prove they're an admin for `role`. * - caller must not provide valid Merkle tree path for a different `(role, accountId)` pairing. * - * @circuitInfo k=16, rows=39993 + * @circuitInfo k=15, rows=31316 * * Disclosures: * @@ -322,11 +569,12 @@ module ShieldedAccessControl { * * @return {[]} - Empty tuple. */ - export circuit grantRole(role: RoleIdentifier, accountId: AccountIdentifier): [] { + export circuit grantRole(role: Bytes<32>, accountId: Bytes<32>): [] { Initializable_assertInitialized(); - assertOnlyRole(getRoleAdmin(role) as RoleIdentifier); - _grantRole(role, accountId); + const adminRole = _getRoleAdmin(role); + _onlyRole(adminRole, _computeAccountId(adminRole, ownPublicKey())); + _uncheckedGrantRole(role, accountId); } /** @@ -354,7 +602,7 @@ module ShieldedAccessControl { * * @dev Commitment and nullifier checks are inlined in this circuit to avoid an expensive re-computation of the role commitment and nullifier * - * @circuitInfo k=15, rows=18115 + * @circuitInfo k=14, rows=12335 * * Disclosures: * @@ -366,12 +614,28 @@ module ShieldedAccessControl { * * @return {Boolean} isGranted - Returns true if a role was granted successfully. */ - export circuit _grantRole( - role: RoleIdentifier, - accountId: AccountIdentifier - ): Boolean { + export circuit _grantRole(role: Bytes<32>, accountId: Bytes<32>): Boolean { Initializable_assertInitialized(); + return _uncheckedGrantRole(role, accountId); + } + + /** + * @description Grants `role` to `accountId` without an initialization or access control check. + * Returns `false` if the role commitment has been nullified (revoked), preventing re-grant + * under the same identity. Used by {grantRole} and {_grantRole} to avoid redundant init checks. + * + * Disclosures: + * + * - A role commitment corresponding to a `(role, accountId)` pairing. + * - A nullifier for the respective role commitment. + * + * @param {Bytes<32>} role - The role identifier. + * @param {Bytes<32>} accountId - The account identifier. + * + * @return {Boolean} isGranted - Returns `true` if the role was granted, `false` if already revoked. + */ + circuit _uncheckedGrantRole(role: Bytes<32>, accountId: Bytes<32>): Boolean { const roleCommitment = _computeRoleCommitment(role, accountId); const roleNullifier = _computeNullifier(roleCommitment); const isRevoked = _roleCommitmentNullifiers.member(disclose(roleNullifier)); @@ -390,15 +654,13 @@ module ShieldedAccessControl { * `role`. At this time, proofs of non-membership on values in the Merkle tree LDT are not possible * so a `(role, accountId)` pairing that does not exist can still be revoked. * - * @circuitInfo k=18, rows=138517 + * @circuitInfo k=15, rows=29305 * * Requirements: * * - caller must prove they're an admin for `role`. * - caller must not provide valid Merkle tree path for a different `(role, accountId)` pairing. * - * @circuitInfo k=18, rows=138761 - * * Disclosures: * * - A Merkle tree path to a role commitment. @@ -411,21 +673,22 @@ module ShieldedAccessControl { * * @return {[]} - Empty tuple. */ - export circuit revokeRole(role: RoleIdentifier, accountId: AccountIdentifier): [] { + export circuit revokeRole(role: Bytes<32>, accountId: Bytes<32>): [] { Initializable_assertInitialized(); - assertOnlyRole(getRoleAdmin(role) as RoleIdentifier); - _revokeRole(role, accountId); + const adminRole = _getRoleAdmin(role); + _onlyRole(adminRole, _computeAccountId(adminRole, ownPublicKey())); + _uncheckedRevokeRole(role, accountId); } - /** + /** * @description Permanently revokes `role` from `accountId` by inserting a role nullifier into the * `_roleCommitmentNullifiers` set. Once revoked, a new `accountId` must be generated to be re-authorized for * `role`. At this time, proofs of non-membership on values in the Merkle tree LDT are not possible * so a `(role, accountId)` pairing that does not exist can still be revoked. Returns false if a role is already * revoked. Internal circuit without access restriction. * - * @circuitInfo k=15, rows=18115 + * @circuitInfo k=14, rows=10324 * * Disclosures: * @@ -436,12 +699,28 @@ module ShieldedAccessControl { * * @return {Boolean} isRevoked - Returns true if operation completes successfully. */ - export circuit _revokeRole( - role: RoleIdentifier, - accountId: AccountIdentifier - ): Boolean { + export circuit _revokeRole(role: Bytes<32>, accountId: Bytes<32>): Boolean { Initializable_assertInitialized(); + return _uncheckedRevokeRole(role, accountId); + } + + /** + * @description Permanently revokes `role` from `accountId` without an initialization or access + * control check. Inserts the role nullifier into `_roleCommitmentNullifiers`. Returns `false` + * if the role is already revoked. Used by {revokeRole}, {_revokeRole}, and {renounceRole} to + * avoid redundant init checks. + * + * Disclosures: + * + * - A nullifier for the respective role commitment. + * + * @param {Bytes<32>} role - The role identifier. + * @param {Bytes<32>} accountId - The account identifier. + * + * @return {Boolean} isRevoked - Returns `true` if the role was revoked, `false` if already revoked. + */ + circuit _uncheckedRevokeRole(role: Bytes<32>, accountId: Bytes<32>): Boolean { const roleCommitment = _computeRoleCommitment(role, accountId); const roleNullifier = _computeNullifier(roleCommitment); const isRevoked = _roleCommitmentNullifiers.member(disclose(roleNullifier)); @@ -464,7 +743,7 @@ module ShieldedAccessControl { * @warning Outside observers may be able to use timing and pattern analysis to weaken pseudonymity * guarantees if renounceRole is used in tandem with other on-chain actions. * - * @circuitInfo k=17, rows=108992 + * @circuitInfo k=15, rows=16670 * * Requirements: * @@ -475,19 +754,18 @@ module ShieldedAccessControl { * - A nullifier for the respective role commitment. * * @param {Bytes<32>} role - The role identifier. - * @param {Bytes<32>} accountId - The unique identifier of the account. + * @param {Bytes<32>} accountIdConfirmation - The caller's account identifier, must match the internally computed value. * * @return {[]} - Empty tuple. */ - export circuit renounceRole(role: RoleIdentifier, accountIdConfirmation: AccountIdentifier): [] { + export circuit renounceRole(role: Bytes<32>, accountIdConfirmation: Bytes<32>): [] { Initializable_assertInitialized(); - const nonce = wit_secretNonce(role); - assert(accountIdConfirmation == _computeAccountId(ownPublicKey(), nonce), + assert(accountIdConfirmation == _computeAccountId(role, ownPublicKey()), "ShieldedAccessControl: bad confirmation" ); - _revokeRole(role, accountIdConfirmation); + _uncheckedRevokeRole(role, accountIdConfirmation); } /** @@ -505,157 +783,9 @@ module ShieldedAccessControl { * * @return {[]} - Empty tuple. */ - export circuit _setRoleAdmin(role: RoleIdentifier, adminId: AdminIdentifier): [] { + export circuit _setRoleAdmin(role: Bytes<32>, adminId: Bytes<32>): [] { Initializable_assertInitialized(); _adminRoles.insert(disclose(role), disclose(adminId)); } - - /** - * @description Verifies whether `accountId` holds `role`. This circuit MAY return false for a - * legitimately credentialed account if the proving environment supplies an invalid Merkle path. - * - * @circuitInfo k=14, rows=16089 - * - * Requirements: - * - * - caller must not provide valid Merkle tree path for a different `(role, accountId)` pairing. - * - * Disclosures: - * - * - A Merkle tree path to a role commitment. - * - A role commitment corresponding to a `(role, accountId)` pairing. - * - A nullifier for the respective role commitment. - * - * @param {Bytes<32>} role - The role identifier. - * @param {Bytes<32>} accountId - The unique identifier of the account. - * - * @return {Boolean} isValidRole - A boolean indicating whether `accountId` has a valid role - */ - circuit _validateRole(role: RoleIdentifier, accountId: AccountIdentifier): Boolean { - Initializable_assertInitialized(); - - const roleCommitment = _computeRoleCommitment(role, accountId); - const roleCommitmentPath = wit_getRoleCommitmentPath(roleCommitment); - const isValidPath = - _operatorRoles.checkRoot( - merkleTreePathRoot<20, RoleCommitment>(disclose(roleCommitmentPath)) - ); - - // If the path is valid we assert it’s a path for the queried leaf (not some other leaf). - if (isValidPath) { - assert(roleCommitmentPath.leaf == roleCommitment, - "ShieldedAccessControl: Path must contain leaf matching computed role commitment for the provided role, accountId pairing" - ); - } - - const roleNullifier = _computeNullifier(roleCommitment); - const isRevoked = _roleCommitmentNullifiers.member(disclose(roleNullifier)); - - return isValidPath && !isRevoked; - } - - /** - * @description Computes the role commitment from the given `accountId` and `role`. - * - * ## Account ID (`accountId`) - * The `accountId` is expected to be computed off-chain as: - * `accountId = SHA256(zcpk, nonce, instanceSalt, accountIdDomain)` - * - * - `zcpk`: The account's ZswapCoinPublicKey. - * - `nonce`: A secret nonce scoped to the role. - * - * ## Role Commitment Derivation - * `roleCommitment = SHA256(role, accountId, instanceSalt, commitmentDomain)` - * - * - `accountId`: See above. - * - `role`: A unique role identifier. - * - `instanceSalt`: A unique per-deployment salt, stored during initialization. - * This prevents commitment collisions across deployments. - * - `commitmentDomain`: Domain separator `"ShieldedAccessControl:commitment"` (padded to 32 bytes) to prevent - * hash collisions when extending the module or using similar commitment schemes. - * - * @circuitInfo k=13, rows=6423 - * - * Requirements: - * - * - Contract is initialized. - * - * @param {Bytes<32>} role - The unique identifier of a role. - * @param {Bytes<32>} accountId - The unique identifier of the account calculated by `SHA256(zcpk, nonce, instanceSalt, accountIdDomain)`. - * - * @returns {Bytes<32>} The commitment derived from `accountId` and `role`. - */ - circuit _computeRoleCommitment( - role: RoleIdentifier, - accountId: AccountIdentifier, - ): RoleCommitment { - Initializable_assertInitialized(); - - return persistentHash>>( - [role as Bytes<32>, - accountId as Bytes<32>, - _instanceSalt, - pad(32, "ShieldedAccessControl:commitment")] - ) - as RoleCommitment; - } - - /** - * @description Computes the role nullifier for a given `roleCommitment`. - * - * ## Role Nullifier Derivation - * `roleNullifier = SHA256(roleCommitment, nullifierDomain)` - * - * - `roleCommitment`: See `_computeRoleCommitment`. - * - `nullifierDomain`: Domain separator `"ShieldedAccessControl:nullifier"` (padded to 32 bytes) to prevent - * hash collisions when extending the module or using similar commitment schemes. - * - * @param {} roleCommitment - The role commitment for a particular `(role, accountId)` pairing. - * - * @returns {Bytes<32>} roleNullifier - The associated nullifier for `roleCommitment`. - */ - pure circuit _computeNullifier(roleCommitment: RoleCommitment): RoleNullifier { - return persistentHash>>( - [roleCommitment as Bytes<32>, pad(32, "ShieldedAccessControl:nullifier")] - ) - as RoleNullifier; - } - - /** - * @description Computes the unique identifier (`accountId`) of an account from their - * ZswapCoinPublicKey and a secret nonce. - * - * ## ID Derivation - * `accountId = SHA256(zcpk, nonce, instanceSalt, accountIdDomain)` - * - * - `zcpk`: The ZswapCoinPublicKey of the caller. This is passed explicitly to allow - * for off-chain derivation, testing, or scenarios where the caller is - * different from the subject of the computation. We recommend using an Air-Gapped Public Key. - * - `nonce`: A secret nonce tied to the identity. The generation strategy is - * left to the user, offering different security/convenience trade-offs. - * - `instanceSalt`: A unique per-deployment salt, stored during initialization. - * This prevents commitment collisions across deployments. - * - `accountIdDomain`: Domain separator `"ShieldedAccessControl:accountId"` (padded to 32 bytes) to prevent - * hash collisions when extending the module or using similar commitment schemes. - * - * The result is a 32-byte commitment that uniquely identifies the account. - * This value is later used in role commitment hashing, - * and acts as a privacy-preserving alternative to a raw public key. - * - * @circuitInfo k=13, rows=6705 - * - * @param {ZswapCoinPublicKey} zcpk - The public key of the identity being committed. - * @param {Bytes<32>} nonce - A private nonce to scope the commitment. - * - * @returns {Bytes<32>} accountId - The computed account ID. - */ - circuit _computeAccountId(zcpk: ZswapCoinPublicKey, nonce: Bytes<32>): AccountIdentifier { - Initializable_assertInitialized(); - - return persistentHash>>( - [zcpk.bytes, nonce, _instanceSalt, pad(32, "ShieldedAccessControl:accountId")] - ) - as AccountIdentifier; - } } diff --git a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact index bfcc697c..765ba1b5 100644 --- a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact +++ b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact @@ -15,8 +15,7 @@ export { ZswapCoinPublicKey, MerkleTreePath, ShieldedAccessControl_DEFAULT_ADMIN_ROLE, ShieldedAccessControl__operatorRoles, - ShieldedAccessControl__roleCommitmentNullifiers, - }; + ShieldedAccessControl__roleCommitmentNullifiers, }; // witness is re-implemented in the Mock contract for testing witness wit_getRoleCommitmentPath( @@ -36,15 +35,55 @@ constructor(instanceSalt: Bytes<32>, isInit: Boolean) { } } +// Follows the order of circuits in ShieldedAccessControl.compact + +export circuit hasRole(role: Bytes<32>): Boolean { + return ShieldedAccessControl_hasRole(role); +} + +export circuit hasRoleFor(role: Bytes<32>, account: ZswapCoinPublicKey): Boolean { + return ShieldedAccessControl_hasRole(role, account); +} + +export circuit computeAccountId(role: Bytes<32>, account: ZswapCoinPublicKey): Bytes<32> { + return disclose(ShieldedAccessControl_computeAccountId(role, account)); +} + +// _hasRole is re-implemented in the Mock contract for testing +export circuit _hasRole(role: Bytes<32>, accountId: Bytes<32>): Boolean { + Initializable_assertInitialized(); + + const roleCommitment = _computeRoleCommitment(role, accountId); + const roleCommitmentPath = wit_getRoleCommitmentPath(roleCommitment); + const isValidPath = + ShieldedAccessControl__operatorRoles.checkRoot( + merkleTreePathRoot<20, ShieldedAccessControl_RoleCommitment>( + disclose(roleCommitmentPath) + ) + ); + + // If the path is valid we assert it's a path for the queried leaf (not some other leaf). + if (isValidPath) { + assert(roleCommitmentPath.leaf == roleCommitment, + "ShieldedAccessControl: Path must contain leaf matching computed role commitment for the provided role, accountId pairing" + ); + } + + const roleNullifier = _computeNullifier(roleCommitment); + const isRevoked = ShieldedAccessControl__roleCommitmentNullifiers.member(disclose(roleNullifier)); + + return isValidPath && !isRevoked; +} + // circuit is reimplemented in the Mock contract for testing export circuit _computeRoleCommitment( - roleId: ShieldedAccessControl_RoleIdentifier, - accountId: ShieldedAccessControl_AccountIdentifier + role: Bytes<32>, + accountId: Bytes<32> ): ShieldedAccessControl_RoleCommitment { Initializable_assertInitialized(); return persistentHash>>( - [roleId as Bytes<32>, + [role as Bytes<32>, accountId as Bytes<32>, ShieldedAccessControl__instanceSalt, pad(32, "ShieldedAccessControl:commitment")] @@ -52,110 +91,47 @@ export circuit _computeRoleCommitment( as ShieldedAccessControl_RoleCommitment; } -// circuit is reimplemented in the Mock contract for testing -export circuit _computeAccountId( - zcpk: ZswapCoinPublicKey, - nonce: Bytes<32> - ): ShieldedAccessControl_AccountIdentifier { - Initializable_assertInitialized(); - - return persistentHash>>( - [zcpk.bytes, - nonce, - ShieldedAccessControl__instanceSalt, - pad(32, "ShieldedAccessControl:accountId")] - ) - as ShieldedAccessControl_AccountIdentifier; -} - // circuit is reimplemented in the Mock contract for testing export pure circuit _computeNullifier( roleCommitment: ShieldedAccessControl_RoleCommitment - ): ShieldedAccessControl_RoleNullifier { + ): Bytes<32> { return persistentHash>>( [roleCommitment as Bytes<32>, pad(32, "ShieldedAccessControl:nullifier")] - ) - as ShieldedAccessControl_RoleNullifier; + ); } -export circuit proveCallerRole( - roleId: ShieldedAccessControl_RoleIdentifier - ): Boolean { - return ShieldedAccessControl_proveCallerRole(roleId); +export circuit _checkRole(role: Bytes<32>): [] { + ShieldedAccessControl__checkRole(role); } -export circuit assertOnlyRole(roleId: ShieldedAccessControl_RoleIdentifier): [] { - ShieldedAccessControl_assertOnlyRole(roleId); +export circuit _checkRoleFor(role: Bytes<32>, account: ZswapCoinPublicKey): [] { + ShieldedAccessControl__checkRole(role, account); } -// _validateRole is re-implemented in the Mock contract for testing -export circuit _validateRole(role: ShieldedAccessControl_RoleIdentifier, accountId: ShieldedAccessControl_AccountIdentifier): Boolean { - Initializable_assertInitialized(); - - const roleCommitment = _computeRoleCommitment(role, accountId); - const roleCommitmentPath = wit_getRoleCommitmentPath(roleCommitment); - const isValidPath = - ShieldedAccessControl__operatorRoles.checkRoot( - merkleTreePathRoot<20, ShieldedAccessControl_RoleCommitment>(disclose(roleCommitmentPath)) - ); - - // If the path is valid we assert it’s a path for the queried leaf (not some other leaf). - if (isValidPath) { - assert(roleCommitmentPath.leaf == roleCommitment, - "ShieldedAccessControl: Path must contain leaf matching computed role commitment for the provided role, accountId pairing" - ); - } - - const roleNullifier = _computeNullifier(roleCommitment); - const isRevoked = ShieldedAccessControl__roleCommitmentNullifiers.member(disclose(roleNullifier)); - - return isValidPath && !isRevoked; - } - -export circuit getRoleAdmin( - roleId: ShieldedAccessControl_RoleIdentifier - ): ShieldedAccessControl_AdminIdentifier { - return ShieldedAccessControl_getRoleAdmin(roleId); +export circuit getRoleAdmin(role: Bytes<32>): Bytes<32> { + return ShieldedAccessControl_getRoleAdmin(role); } -export circuit grantRole( - roleId: ShieldedAccessControl_RoleIdentifier, - accountId: ShieldedAccessControl_AccountIdentifier - ): [] { - ShieldedAccessControl_grantRole(roleId, accountId); +export circuit grantRole(role: Bytes<32>, accountId: Bytes<32>): [] { + ShieldedAccessControl_grantRole(role, accountId); } -export circuit revokeRole( - roleId: ShieldedAccessControl_RoleIdentifier, - accountId: ShieldedAccessControl_AccountIdentifier - ): [] { - ShieldedAccessControl_revokeRole(roleId, accountId); +export circuit _grantRole(role: Bytes<32>, accountId: Bytes<32>): Boolean { + return ShieldedAccessControl__grantRole(role, accountId); } -export circuit renounceRole( - roleId: ShieldedAccessControl_RoleIdentifier, - accountIdConfirmation: ShieldedAccessControl_AccountIdentifier - ): [] { - ShieldedAccessControl_renounceRole(roleId, accountIdConfirmation); +export circuit revokeRole(role: Bytes<32>, accountId: Bytes<32>): [] { + ShieldedAccessControl_revokeRole(role, accountId); } -export circuit _setRoleAdmin( - roleId: ShieldedAccessControl_RoleIdentifier, - adminRole: ShieldedAccessControl_AdminIdentifier - ): [] { - ShieldedAccessControl__setRoleAdmin(roleId, adminRole); +export circuit _revokeRole(role: Bytes<32>, accountId: Bytes<32>): Boolean { + return ShieldedAccessControl__revokeRole(role, accountId); } -export circuit _grantRole( - roleId: ShieldedAccessControl_RoleIdentifier, - accountId: ShieldedAccessControl_AccountIdentifier - ): Boolean { - return ShieldedAccessControl__grantRole(roleId, accountId); +export circuit renounceRole(role: Bytes<32>, accountIdConfirmation: Bytes<32>): [] { + ShieldedAccessControl_renounceRole(role, accountIdConfirmation); } -export circuit _revokeRole( - roleId: ShieldedAccessControl_RoleIdentifier, - accountId: ShieldedAccessControl_AccountIdentifier - ): Boolean { - return ShieldedAccessControl__revokeRole(roleId, accountId); +export circuit _setRoleAdmin(role: Bytes<32>, adminRole: Bytes<32>): [] { + ShieldedAccessControl__setRoleAdmin(role, adminRole); } From 81002bf593adab566177726c137f682fa231d50d Mon Sep 17 00:00:00 2001 From: 0xisk Date: Wed, 11 Mar 2026 18:31:20 +0100 Subject: [PATCH 260/322] Revert "refactor: harden the shieldedaccesscontrol lib by some improvements and fixing bugs" This reverts commit e8ba943fb791497869fbd3207e24124bdbfc1e34. --- .../src/access/ShieldedAccessControl.compact | 524 +++++++----------- .../mocks/MockShieldedAccessControl.compact | 152 ++--- 2 files changed, 285 insertions(+), 391 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index ca2ed497..68cd5fd1 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -37,7 +37,7 @@ pragma language_version >= 0.21.0; * - `instanceSalt` is an immutable, cryptographically strong random value provided on deployment * - `commitmentDomain` is the following zero padded 32 byte string: "ShieldedAccessControl:commitment" * - `accountIdDomain` is the following zero padded 32 byte string: "ShieldedAccessControl:accountId" - * - `nullifierDomain` is the following zero padded 32 byte string: "ShieldedAccessControl:nullifier" + * - `commitmentDomain` is the following zero padded 32 byte string: "ShieldedAccessControl:nullifier" * * In this RBAC model, role commitments behave like private bearer tokens. Possession of a valid, non-revoked role * commitment grants authorization. Revocation permanently burns the role instance, requiring explicit new issuance @@ -59,11 +59,11 @@ pragma language_version >= 0.21.0; * } * ``` * - * To restrict access to a circuit, use {_checkRole}: + * To restrict access to a circuit, use {assertOnlyRole}: * * ```compact * circuit foo(): [] { - * ShieldedAccessControl__checkRole(MY_ROLE); + * ShieldedAccessControl_assertOnlyRole(MY_ROLE); * // ... rest of circuit logic * } * ``` @@ -119,7 +119,12 @@ module ShieldedAccessControl { import "../utils/Utils" prefix Utils_; import "../security/Initializable" prefix Initializable_; + // TODO: Standardize types across contracts https://github.com/OpenZeppelin/compact-contracts/issues/368 export new type RoleCommitment = Bytes<32>; + export new type RoleIdentifier = Bytes<32>; + export new type AccountIdentifier = Bytes<32>; + export new type AdminIdentifier = Bytes<32>; + export new type RoleNullifier = Bytes<32>; /** * @ledger _operatorRoles @@ -134,14 +139,14 @@ module ShieldedAccessControl { * @ledger _adminRoles * @description Mapping from a role identifier to an admin role identifier.  */ - export ledger _adminRoles: Map, Bytes<32>>; + export ledger _adminRoles: Map; /** * @description A set of nullifiers used to prove a role has been revoked - * @type {Bytes<32>} Bytes<32> - A role nullifier created by the following hash: SHA256(roleCommitment | nullifierDomain). + * @type {Bytes<32>} RoleNullifier - A role nullifier created by the following hash: SHA256(roleCommitment | nullifierDomain). * @type {Set} _roleCommitmentNullifiers  */ - export ledger _roleCommitmentNullifiers: Set>; + export ledger _roleCommitmentNullifiers: Set; /** * @description The default admin role for all roles. Only accounts with this role will be able to grant or revoke other roles @@ -184,7 +189,7 @@ module ShieldedAccessControl { * * @returns {Bytes<32>} secretNonce - A private per-role nonce used in deriving the shielded account identifier. */ - witness wit_secretNonce(role: Bytes<32>): Bytes<32>; + witness wit_secretNonce(role: RoleIdentifier): Bytes<32>; /** * @description Initializes the contract by storing the `instanceSalt` that acts as a privacy additive @@ -216,7 +221,7 @@ module ShieldedAccessControl { * caller if the proving environment supplies an invalid Merkle path. This circuit will never return true for an * unauthorized caller. * - * @circuitInfo k=15, rows=19239 + * @circuitInfo k=15, rows=22128 * * Requirements: * @@ -232,201 +237,18 @@ module ShieldedAccessControl { * * @return {Boolean} - A boolean determining if a caller successfully proved ownership of `role`  */ - export circuit hasRole(role: Bytes<32>): Boolean { + export circuit proveCallerRole(role: RoleIdentifier): Boolean { Initializable_assertInitialized(); - return _hasRole(role, _computeAccountId(role, ownPublicKey())); - } - - /** - * @description Returns `true` if `account` holds `role` and is not revoked. The account identifier - * is derived internally from the provided `ZswapCoinPublicKey` and the caller's secret nonce. - * MAY return false for a legitimately credentialed account if the proving environment supplies - * an invalid Merkle path. This circuit will never return true for an unauthorized account. - * - * @circuitInfo k=15, rows=19259 - * - * Requirements: - * - * - caller must not provide valid Merkle tree path for a different `(role, accountId)` pairing. - * - * Disclosures: - * - * - A Merkle tree path to a role commitment. - * - A role commitment corresponding to a `(role, accountId)` pairing. - * - A nullifier for the respective role commitment. - * - * @param {Bytes<32>} role - The role identifier. - * @param {ZswapCoinPublicKey} account - The public key of the account to check. - * - * @return {Boolean} - A boolean determining if `account` holds `role`. - */ - export circuit hasRole(role: Bytes<32>, account: ZswapCoinPublicKey): Boolean { - Initializable_assertInitialized(); - - return _hasRole(role, _computeAccountId(role, account)); - } - - /** - * @description Computes the unique identifier (`accountId`) of an account from their - * ZswapCoinPublicKey and a secret nonce. - * - * ## ID Derivation - * `accountId = SHA256(zcpk, nonce, instanceSalt, accountIdDomain)` - * - * - `zcpk`: The ZswapCoinPublicKey of the caller. This is passed explicitly to allow - * for off-chain derivation, testing, or scenarios where the caller is - * different from the subject of the computation. We recommend using an Air-Gapped Public Key. - * - `nonce`: A secret nonce tied to the identity. The generation strategy is - * left to the user, offering different security/convenience trade-offs. - * - `instanceSalt`: A unique per-deployment salt, stored during initialization. - * This prevents commitment collisions across deployments. - * - `accountIdDomain`: Domain separator `"ShieldedAccessControl:accountId"` (padded to 32 bytes) to prevent - * hash collisions when extending the module or using similar commitment schemes. - * - * The result is a 32-byte commitment that uniquely identifies the account. - * This value is later used in role commitment hashing, - * and acts as a privacy-preserving alternative to a raw public key. - * - * @circuitInfo k=13, rows=6684 - * - * @param {Bytes<32>} role - The role identifier used to derive the per-role secret nonce. - * @param {ZswapCoinPublicKey} account - The public key of the identity being committed. - * - * @returns {Bytes<32>} accountId - The computed account ID. - */ - circuit _computeAccountId(role: Bytes<32>, account: ZswapCoinPublicKey): Bytes<32> { const nonce = wit_secretNonce(role); - return persistentHash>>( - [account.bytes, nonce, _instanceSalt, pad(32, "ShieldedAccessControl:accountId")] - ); - } - - /** - * @description Public wrapper for {_computeAccountId}. Computes the unique identifier (`accountId`) - * of an account from a role identifier and a `ZswapCoinPublicKey`. The secret nonce is derived - * internally via {wit_secretNonce}. - * - * @circuitInfo k=13, rows=6684 - * - * @param {Bytes<32>} role - The role identifier used to derive the per-role secret nonce. - * @param {ZswapCoinPublicKey} account - The public key of the identity being committed. - * - * @returns {Bytes<32>} accountId - The computed account ID. - */ - export circuit computeAccountId(role: Bytes<32>, account: ZswapCoinPublicKey): Bytes<32> { - Initializable_assertInitialized(); - - return _computeAccountId(role, account); - } - - /** - * @description Verifies whether `accountId` holds `role`. This circuit MAY return false for a - * legitimately credentialed account if the proving environment supplies an invalid Merkle path. - * - * @circuitInfo k=14, rows=13183 - * - * Requirements: - * - * - caller must not provide valid Merkle tree path for a different `(role, accountId)` pairing. - * - * Disclosures: - * - * - A Merkle tree path to a role commitment. - * - A role commitment corresponding to a `(role, accountId)` pairing. - * - A nullifier for the respective role commitment. - * - * @param {Bytes<32>} role - The role identifier. - * @param {Bytes<32>} accountId - The unique identifier of the account. - * - * @return {Boolean} isValidRole - A boolean indicating whether `accountId` has a valid role - */ - circuit _hasRole(role: Bytes<32>, accountId: Bytes<32>): Boolean { - const roleCommitment = _computeRoleCommitment(role, accountId); - const roleCommitmentPath = wit_getRoleCommitmentPath(roleCommitment); - const isValidPath = - _operatorRoles.checkRoot( - merkleTreePathRoot<20, RoleCommitment>(disclose(roleCommitmentPath)) - ); - - // If the path is valid we assert it’s a path for the queried leaf (not some other leaf). - if (isValidPath) { - assert(roleCommitmentPath.leaf == roleCommitment, - "ShieldedAccessControl: Path must contain leaf matching computed role commitment for the provided role, accountId pairing" - ); - } - - const roleNullifier = _computeNullifier(roleCommitment); - const isRevoked = _roleCommitmentNullifiers.member(disclose(roleNullifier)); - - return isValidPath && !isRevoked; - } - - /** - * @description Computes the role commitment from the given `accountId` and `role`. - * - * ## Account ID (`accountId`) - * The `accountId` is expected to be computed off-chain as: - * `accountId = SHA256(zcpk, nonce, instanceSalt, accountIdDomain)` - * - * - `zcpk`: The account's ZswapCoinPublicKey. - * - `nonce`: A secret nonce scoped to the role. - * - * ## Role Commitment Derivation - * `roleCommitment = SHA256(role, accountId, instanceSalt, commitmentDomain)` - * - * - `accountId`: See above. - * - `role`: A unique role identifier. - * - `instanceSalt`: A unique per-deployment salt, stored during initialization. - * This prevents commitment collisions across deployments. - * - `commitmentDomain`: Domain separator `"ShieldedAccessControl:commitment"` (padded to 32 bytes) to prevent - * hash collisions when extending the module or using similar commitment schemes. - * - * @circuitInfo k=13, rows=6423 - * - * Requirements: - * - * - Contract is initialized. - * - * @param {Bytes<32>} role - The unique identifier of a role. - * @param {Bytes<32>} accountId - The unique identifier of the account calculated by `SHA256(zcpk, nonce, instanceSalt, accountIdDomain)`. - * - * @returns {Bytes<32>} The commitment derived from `accountId` and `role`. - */ - circuit _computeRoleCommitment(role: Bytes<32>, accountId: Bytes<32>,): RoleCommitment { - return persistentHash>>( - [role as Bytes<32>, - accountId as Bytes<32>, - _instanceSalt, - pad(32, "ShieldedAccessControl:commitment")] - ) - as RoleCommitment; - } - - /** - * @description Computes the role nullifier for a given `roleCommitment`. - * - * ## Role Nullifier Derivation - * `roleNullifier = SHA256(roleCommitment, nullifierDomain)` - * - * - `roleCommitment`: See `_computeRoleCommitment`. - * - `nullifierDomain`: Domain separator `"ShieldedAccessControl:nullifier"` (padded to 32 bytes) to prevent - * hash collisions when extending the module or using similar commitment schemes. - * - * @param {Bytes<32>} roleCommitment - The role commitment for a particular `(role, accountId)` pairing. - * - * @returns {Bytes<32>} roleNullifier - The associated nullifier for `roleCommitment`. - */ - pure circuit _computeNullifier(roleCommitment: RoleCommitment): Bytes<32> { - return persistentHash>>( - [roleCommitment as Bytes<32>, pad(32, "ShieldedAccessControl:nullifier")] - ); + const accountId = _computeAccountId(ownPublicKey(), nonce); + return _validateRole(role, accountId); } /** * @description Reverts if caller cannot provide a valid proof of ownership for `role`. * - * @circuitInfo k=15, rows=19239 + * @circuitInfo k=15, rows=22130 * * Requirements: * @@ -443,62 +265,10 @@ module ShieldedAccessControl { * * @return {[]} - Empty tuple. */ - export circuit _checkRole(role: Bytes<32>): [] { - Initializable_assertInitialized(); - - _onlyRole(role, _computeAccountId(role, ownPublicKey())); - } - - /** - * @description Reverts if `account` does not hold `role`. The account identifier is derived - * internally from the provided `ZswapCoinPublicKey` and the caller's secret nonce. - * - * @circuitInfo k=15, rows=19259 - * - * Requirements: - * - * - `account` must hold `role`. - * - caller must not provide valid Merkle tree path for a different `(role, accountId)` pairing. - * - * Disclosures: - * - * - A Merkle tree path to a role commitment. - * - A role commitment corresponding to a `(role, accountId)` pairing. - * - A nullifier for the respective role commitment. - * - * @param {Bytes<32>} role - The role identifier. - * @param {ZswapCoinPublicKey} account - The public key of the account to check. - * - * @return {[]} - Empty tuple. - */ - export circuit _checkRole(role: Bytes<32>, account: ZswapCoinPublicKey): [] { + export circuit assertOnlyRole(role: RoleIdentifier): [] { Initializable_assertInitialized(); - _onlyRole(role, _computeAccountId(role, account)); - } - - /** - * @description Reverts if `accountId` does not hold `role` or has been revoked. Internal circuit - * without access restriction. Used as the shared assertion primitive for {_checkRole} overloads. - * - * Requirements: - * - * - `accountId` must hold `role`. - * - caller must not provide valid Merkle tree path for a different `(role, accountId)` pairing. - * - * Disclosures: - * - * - A Merkle tree path to a role commitment. - * - A role commitment corresponding to a `(role, accountId)` pairing. - * - A nullifier for the respective role commitment. - * - * @param {Bytes<32>} role - The role identifier. - * @param {Bytes<32>} accountId - The unique identifier of the account. - * - * @return {[]} - Empty tuple. - */ - circuit _onlyRole(role: Bytes<32>, accountId: Bytes<32>): [] { - assert(_hasRole(role, accountId), "ShieldedAccessControl: unauthorized account"); + assert(proveCallerRole(role), "ShieldedAccessControl: unauthorized account"); } /** @@ -517,30 +287,13 @@ module ShieldedAccessControl { * * @return {Bytes<32>} roleAdmin - The admin role that controls `role`. */ - export circuit getRoleAdmin(role: Bytes<32>): Bytes<32> { + export circuit getRoleAdmin(role: RoleIdentifier): AdminIdentifier { Initializable_assertInitialized(); - return _getRoleAdmin(role); - } - - /** - * @description Returns the admin role that controls `role` or a zero byte array if no admin - * has been set. Internal circuit without initialization check, used by {getRoleAdmin}, - * {grantRole}, and {revokeRole}. - * - * Disclosures: - * - * - A role identifier. - * - * @param {Bytes<32>} role - The role identifier. - * - * @return {Bytes<32>} roleAdmin - The admin role that controls `role`. - */ - circuit _getRoleAdmin(role: Bytes<32>): Bytes<32> { if (_adminRoles.member(disclose(role))) { return _adminRoles.lookup(disclose(role)); } - return default>; + return default> as AdminIdentifier; } /** @@ -555,7 +308,7 @@ module ShieldedAccessControl { * - caller must prove they're an admin for `role`. * - caller must not provide valid Merkle tree path for a different `(role, accountId)` pairing. * - * @circuitInfo k=15, rows=31316 + * @circuitInfo k=16, rows=39993 * * Disclosures: * @@ -569,12 +322,11 @@ module ShieldedAccessControl { * * @return {[]} - Empty tuple. */ - export circuit grantRole(role: Bytes<32>, accountId: Bytes<32>): [] { + export circuit grantRole(role: RoleIdentifier, accountId: AccountIdentifier): [] { Initializable_assertInitialized(); - const adminRole = _getRoleAdmin(role); - _onlyRole(adminRole, _computeAccountId(adminRole, ownPublicKey())); - _uncheckedGrantRole(role, accountId); + assertOnlyRole(getRoleAdmin(role) as RoleIdentifier); + _grantRole(role, accountId); } /** @@ -602,7 +354,7 @@ module ShieldedAccessControl { * * @dev Commitment and nullifier checks are inlined in this circuit to avoid an expensive re-computation of the role commitment and nullifier * - * @circuitInfo k=14, rows=12335 + * @circuitInfo k=15, rows=18115 * * Disclosures: * @@ -614,28 +366,12 @@ module ShieldedAccessControl { * * @return {Boolean} isGranted - Returns true if a role was granted successfully. */ - export circuit _grantRole(role: Bytes<32>, accountId: Bytes<32>): Boolean { + export circuit _grantRole( + role: RoleIdentifier, + accountId: AccountIdentifier + ): Boolean { Initializable_assertInitialized(); - return _uncheckedGrantRole(role, accountId); - } - - /** - * @description Grants `role` to `accountId` without an initialization or access control check. - * Returns `false` if the role commitment has been nullified (revoked), preventing re-grant - * under the same identity. Used by {grantRole} and {_grantRole} to avoid redundant init checks. - * - * Disclosures: - * - * - A role commitment corresponding to a `(role, accountId)` pairing. - * - A nullifier for the respective role commitment. - * - * @param {Bytes<32>} role - The role identifier. - * @param {Bytes<32>} accountId - The account identifier. - * - * @return {Boolean} isGranted - Returns `true` if the role was granted, `false` if already revoked. - */ - circuit _uncheckedGrantRole(role: Bytes<32>, accountId: Bytes<32>): Boolean { const roleCommitment = _computeRoleCommitment(role, accountId); const roleNullifier = _computeNullifier(roleCommitment); const isRevoked = _roleCommitmentNullifiers.member(disclose(roleNullifier)); @@ -654,13 +390,15 @@ module ShieldedAccessControl { * `role`. At this time, proofs of non-membership on values in the Merkle tree LDT are not possible * so a `(role, accountId)` pairing that does not exist can still be revoked. * - * @circuitInfo k=15, rows=29305 + * @circuitInfo k=18, rows=138517 * * Requirements: * * - caller must prove they're an admin for `role`. * - caller must not provide valid Merkle tree path for a different `(role, accountId)` pairing. * + * @circuitInfo k=18, rows=138761 + * * Disclosures: * * - A Merkle tree path to a role commitment. @@ -673,22 +411,21 @@ module ShieldedAccessControl { * * @return {[]} - Empty tuple. */ - export circuit revokeRole(role: Bytes<32>, accountId: Bytes<32>): [] { + export circuit revokeRole(role: RoleIdentifier, accountId: AccountIdentifier): [] { Initializable_assertInitialized(); - const adminRole = _getRoleAdmin(role); - _onlyRole(adminRole, _computeAccountId(adminRole, ownPublicKey())); - _uncheckedRevokeRole(role, accountId); + assertOnlyRole(getRoleAdmin(role) as RoleIdentifier); + _revokeRole(role, accountId); } - /** + /** * @description Permanently revokes `role` from `accountId` by inserting a role nullifier into the * `_roleCommitmentNullifiers` set. Once revoked, a new `accountId` must be generated to be re-authorized for * `role`. At this time, proofs of non-membership on values in the Merkle tree LDT are not possible * so a `(role, accountId)` pairing that does not exist can still be revoked. Returns false if a role is already * revoked. Internal circuit without access restriction. * - * @circuitInfo k=14, rows=10324 + * @circuitInfo k=15, rows=18115 * * Disclosures: * @@ -699,28 +436,12 @@ module ShieldedAccessControl { * * @return {Boolean} isRevoked - Returns true if operation completes successfully. */ - export circuit _revokeRole(role: Bytes<32>, accountId: Bytes<32>): Boolean { + export circuit _revokeRole( + role: RoleIdentifier, + accountId: AccountIdentifier + ): Boolean { Initializable_assertInitialized(); - return _uncheckedRevokeRole(role, accountId); - } - - /** - * @description Permanently revokes `role` from `accountId` without an initialization or access - * control check. Inserts the role nullifier into `_roleCommitmentNullifiers`. Returns `false` - * if the role is already revoked. Used by {revokeRole}, {_revokeRole}, and {renounceRole} to - * avoid redundant init checks. - * - * Disclosures: - * - * - A nullifier for the respective role commitment. - * - * @param {Bytes<32>} role - The role identifier. - * @param {Bytes<32>} accountId - The account identifier. - * - * @return {Boolean} isRevoked - Returns `true` if the role was revoked, `false` if already revoked. - */ - circuit _uncheckedRevokeRole(role: Bytes<32>, accountId: Bytes<32>): Boolean { const roleCommitment = _computeRoleCommitment(role, accountId); const roleNullifier = _computeNullifier(roleCommitment); const isRevoked = _roleCommitmentNullifiers.member(disclose(roleNullifier)); @@ -743,7 +464,7 @@ module ShieldedAccessControl { * @warning Outside observers may be able to use timing and pattern analysis to weaken pseudonymity * guarantees if renounceRole is used in tandem with other on-chain actions. * - * @circuitInfo k=15, rows=16670 + * @circuitInfo k=17, rows=108992 * * Requirements: * @@ -754,18 +475,19 @@ module ShieldedAccessControl { * - A nullifier for the respective role commitment. * * @param {Bytes<32>} role - The role identifier. - * @param {Bytes<32>} accountIdConfirmation - The caller's account identifier, must match the internally computed value. + * @param {Bytes<32>} accountId - The unique identifier of the account. * * @return {[]} - Empty tuple. */ - export circuit renounceRole(role: Bytes<32>, accountIdConfirmation: Bytes<32>): [] { + export circuit renounceRole(role: RoleIdentifier, accountIdConfirmation: AccountIdentifier): [] { Initializable_assertInitialized(); - assert(accountIdConfirmation == _computeAccountId(role, ownPublicKey()), + const nonce = wit_secretNonce(role); + assert(accountIdConfirmation == _computeAccountId(ownPublicKey(), nonce), "ShieldedAccessControl: bad confirmation" ); - _uncheckedRevokeRole(role, accountIdConfirmation); + _revokeRole(role, accountIdConfirmation); } /** @@ -783,9 +505,157 @@ module ShieldedAccessControl { * * @return {[]} - Empty tuple. */ - export circuit _setRoleAdmin(role: Bytes<32>, adminId: Bytes<32>): [] { + export circuit _setRoleAdmin(role: RoleIdentifier, adminId: AdminIdentifier): [] { Initializable_assertInitialized(); _adminRoles.insert(disclose(role), disclose(adminId)); } + + /** + * @description Verifies whether `accountId` holds `role`. This circuit MAY return false for a + * legitimately credentialed account if the proving environment supplies an invalid Merkle path. + * + * @circuitInfo k=14, rows=16089 + * + * Requirements: + * + * - caller must not provide valid Merkle tree path for a different `(role, accountId)` pairing. + * + * Disclosures: + * + * - A Merkle tree path to a role commitment. + * - A role commitment corresponding to a `(role, accountId)` pairing. + * - A nullifier for the respective role commitment. + * + * @param {Bytes<32>} role - The role identifier. + * @param {Bytes<32>} accountId - The unique identifier of the account. + * + * @return {Boolean} isValidRole - A boolean indicating whether `accountId` has a valid role + */ + circuit _validateRole(role: RoleIdentifier, accountId: AccountIdentifier): Boolean { + Initializable_assertInitialized(); + + const roleCommitment = _computeRoleCommitment(role, accountId); + const roleCommitmentPath = wit_getRoleCommitmentPath(roleCommitment); + const isValidPath = + _operatorRoles.checkRoot( + merkleTreePathRoot<20, RoleCommitment>(disclose(roleCommitmentPath)) + ); + + // If the path is valid we assert it’s a path for the queried leaf (not some other leaf). + if (isValidPath) { + assert(roleCommitmentPath.leaf == roleCommitment, + "ShieldedAccessControl: Path must contain leaf matching computed role commitment for the provided role, accountId pairing" + ); + } + + const roleNullifier = _computeNullifier(roleCommitment); + const isRevoked = _roleCommitmentNullifiers.member(disclose(roleNullifier)); + + return isValidPath && !isRevoked; + } + + /** + * @description Computes the role commitment from the given `accountId` and `role`. + * + * ## Account ID (`accountId`) + * The `accountId` is expected to be computed off-chain as: + * `accountId = SHA256(zcpk, nonce, instanceSalt, accountIdDomain)` + * + * - `zcpk`: The account's ZswapCoinPublicKey. + * - `nonce`: A secret nonce scoped to the role. + * + * ## Role Commitment Derivation + * `roleCommitment = SHA256(role, accountId, instanceSalt, commitmentDomain)` + * + * - `accountId`: See above. + * - `role`: A unique role identifier. + * - `instanceSalt`: A unique per-deployment salt, stored during initialization. + * This prevents commitment collisions across deployments. + * - `commitmentDomain`: Domain separator `"ShieldedAccessControl:commitment"` (padded to 32 bytes) to prevent + * hash collisions when extending the module or using similar commitment schemes. + * + * @circuitInfo k=13, rows=6423 + * + * Requirements: + * + * - Contract is initialized. + * + * @param {Bytes<32>} role - The unique identifier of a role. + * @param {Bytes<32>} accountId - The unique identifier of the account calculated by `SHA256(zcpk, nonce, instanceSalt, accountIdDomain)`. + * + * @returns {Bytes<32>} The commitment derived from `accountId` and `role`. + */ + circuit _computeRoleCommitment( + role: RoleIdentifier, + accountId: AccountIdentifier, + ): RoleCommitment { + Initializable_assertInitialized(); + + return persistentHash>>( + [role as Bytes<32>, + accountId as Bytes<32>, + _instanceSalt, + pad(32, "ShieldedAccessControl:commitment")] + ) + as RoleCommitment; + } + + /** + * @description Computes the role nullifier for a given `roleCommitment`. + * + * ## Role Nullifier Derivation + * `roleNullifier = SHA256(roleCommitment, nullifierDomain)` + * + * - `roleCommitment`: See `_computeRoleCommitment`. + * - `nullifierDomain`: Domain separator `"ShieldedAccessControl:nullifier"` (padded to 32 bytes) to prevent + * hash collisions when extending the module or using similar commitment schemes. + * + * @param {} roleCommitment - The role commitment for a particular `(role, accountId)` pairing. + * + * @returns {Bytes<32>} roleNullifier - The associated nullifier for `roleCommitment`. + */ + pure circuit _computeNullifier(roleCommitment: RoleCommitment): RoleNullifier { + return persistentHash>>( + [roleCommitment as Bytes<32>, pad(32, "ShieldedAccessControl:nullifier")] + ) + as RoleNullifier; + } + + /** + * @description Computes the unique identifier (`accountId`) of an account from their + * ZswapCoinPublicKey and a secret nonce. + * + * ## ID Derivation + * `accountId = SHA256(zcpk, nonce, instanceSalt, accountIdDomain)` + * + * - `zcpk`: The ZswapCoinPublicKey of the caller. This is passed explicitly to allow + * for off-chain derivation, testing, or scenarios where the caller is + * different from the subject of the computation. We recommend using an Air-Gapped Public Key. + * - `nonce`: A secret nonce tied to the identity. The generation strategy is + * left to the user, offering different security/convenience trade-offs. + * - `instanceSalt`: A unique per-deployment salt, stored during initialization. + * This prevents commitment collisions across deployments. + * - `accountIdDomain`: Domain separator `"ShieldedAccessControl:accountId"` (padded to 32 bytes) to prevent + * hash collisions when extending the module or using similar commitment schemes. + * + * The result is a 32-byte commitment that uniquely identifies the account. + * This value is later used in role commitment hashing, + * and acts as a privacy-preserving alternative to a raw public key. + * + * @circuitInfo k=13, rows=6705 + * + * @param {ZswapCoinPublicKey} zcpk - The public key of the identity being committed. + * @param {Bytes<32>} nonce - A private nonce to scope the commitment. + * + * @returns {Bytes<32>} accountId - The computed account ID. + */ + circuit _computeAccountId(zcpk: ZswapCoinPublicKey, nonce: Bytes<32>): AccountIdentifier { + Initializable_assertInitialized(); + + return persistentHash>>( + [zcpk.bytes, nonce, _instanceSalt, pad(32, "ShieldedAccessControl:accountId")] + ) + as AccountIdentifier; + } } diff --git a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact index 765ba1b5..bfcc697c 100644 --- a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact +++ b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact @@ -15,7 +15,8 @@ export { ZswapCoinPublicKey, MerkleTreePath, ShieldedAccessControl_DEFAULT_ADMIN_ROLE, ShieldedAccessControl__operatorRoles, - ShieldedAccessControl__roleCommitmentNullifiers, }; + ShieldedAccessControl__roleCommitmentNullifiers, + }; // witness is re-implemented in the Mock contract for testing witness wit_getRoleCommitmentPath( @@ -35,55 +36,15 @@ constructor(instanceSalt: Bytes<32>, isInit: Boolean) { } } -// Follows the order of circuits in ShieldedAccessControl.compact - -export circuit hasRole(role: Bytes<32>): Boolean { - return ShieldedAccessControl_hasRole(role); -} - -export circuit hasRoleFor(role: Bytes<32>, account: ZswapCoinPublicKey): Boolean { - return ShieldedAccessControl_hasRole(role, account); -} - -export circuit computeAccountId(role: Bytes<32>, account: ZswapCoinPublicKey): Bytes<32> { - return disclose(ShieldedAccessControl_computeAccountId(role, account)); -} - -// _hasRole is re-implemented in the Mock contract for testing -export circuit _hasRole(role: Bytes<32>, accountId: Bytes<32>): Boolean { - Initializable_assertInitialized(); - - const roleCommitment = _computeRoleCommitment(role, accountId); - const roleCommitmentPath = wit_getRoleCommitmentPath(roleCommitment); - const isValidPath = - ShieldedAccessControl__operatorRoles.checkRoot( - merkleTreePathRoot<20, ShieldedAccessControl_RoleCommitment>( - disclose(roleCommitmentPath) - ) - ); - - // If the path is valid we assert it's a path for the queried leaf (not some other leaf). - if (isValidPath) { - assert(roleCommitmentPath.leaf == roleCommitment, - "ShieldedAccessControl: Path must contain leaf matching computed role commitment for the provided role, accountId pairing" - ); - } - - const roleNullifier = _computeNullifier(roleCommitment); - const isRevoked = ShieldedAccessControl__roleCommitmentNullifiers.member(disclose(roleNullifier)); - - return isValidPath && !isRevoked; -} - // circuit is reimplemented in the Mock contract for testing export circuit _computeRoleCommitment( - role: Bytes<32>, - accountId: Bytes<32> + roleId: ShieldedAccessControl_RoleIdentifier, + accountId: ShieldedAccessControl_AccountIdentifier ): ShieldedAccessControl_RoleCommitment { Initializable_assertInitialized(); return persistentHash>>( - [role as Bytes<32>, + [roleId as Bytes<32>, accountId as Bytes<32>, ShieldedAccessControl__instanceSalt, pad(32, "ShieldedAccessControl:commitment")] @@ -91,47 +52,110 @@ export circuit _computeRoleCommitment( as ShieldedAccessControl_RoleCommitment; } +// circuit is reimplemented in the Mock contract for testing +export circuit _computeAccountId( + zcpk: ZswapCoinPublicKey, + nonce: Bytes<32> + ): ShieldedAccessControl_AccountIdentifier { + Initializable_assertInitialized(); + + return persistentHash>>( + [zcpk.bytes, + nonce, + ShieldedAccessControl__instanceSalt, + pad(32, "ShieldedAccessControl:accountId")] + ) + as ShieldedAccessControl_AccountIdentifier; +} + // circuit is reimplemented in the Mock contract for testing export pure circuit _computeNullifier( roleCommitment: ShieldedAccessControl_RoleCommitment - ): Bytes<32> { + ): ShieldedAccessControl_RoleNullifier { return persistentHash>>( [roleCommitment as Bytes<32>, pad(32, "ShieldedAccessControl:nullifier")] - ); + ) + as ShieldedAccessControl_RoleNullifier; } -export circuit _checkRole(role: Bytes<32>): [] { - ShieldedAccessControl__checkRole(role); +export circuit proveCallerRole( + roleId: ShieldedAccessControl_RoleIdentifier + ): Boolean { + return ShieldedAccessControl_proveCallerRole(roleId); } -export circuit _checkRoleFor(role: Bytes<32>, account: ZswapCoinPublicKey): [] { - ShieldedAccessControl__checkRole(role, account); +export circuit assertOnlyRole(roleId: ShieldedAccessControl_RoleIdentifier): [] { + ShieldedAccessControl_assertOnlyRole(roleId); } -export circuit getRoleAdmin(role: Bytes<32>): Bytes<32> { - return ShieldedAccessControl_getRoleAdmin(role); +// _validateRole is re-implemented in the Mock contract for testing +export circuit _validateRole(role: ShieldedAccessControl_RoleIdentifier, accountId: ShieldedAccessControl_AccountIdentifier): Boolean { + Initializable_assertInitialized(); + + const roleCommitment = _computeRoleCommitment(role, accountId); + const roleCommitmentPath = wit_getRoleCommitmentPath(roleCommitment); + const isValidPath = + ShieldedAccessControl__operatorRoles.checkRoot( + merkleTreePathRoot<20, ShieldedAccessControl_RoleCommitment>(disclose(roleCommitmentPath)) + ); + + // If the path is valid we assert it’s a path for the queried leaf (not some other leaf). + if (isValidPath) { + assert(roleCommitmentPath.leaf == roleCommitment, + "ShieldedAccessControl: Path must contain leaf matching computed role commitment for the provided role, accountId pairing" + ); + } + + const roleNullifier = _computeNullifier(roleCommitment); + const isRevoked = ShieldedAccessControl__roleCommitmentNullifiers.member(disclose(roleNullifier)); + + return isValidPath && !isRevoked; + } + +export circuit getRoleAdmin( + roleId: ShieldedAccessControl_RoleIdentifier + ): ShieldedAccessControl_AdminIdentifier { + return ShieldedAccessControl_getRoleAdmin(roleId); } -export circuit grantRole(role: Bytes<32>, accountId: Bytes<32>): [] { - ShieldedAccessControl_grantRole(role, accountId); +export circuit grantRole( + roleId: ShieldedAccessControl_RoleIdentifier, + accountId: ShieldedAccessControl_AccountIdentifier + ): [] { + ShieldedAccessControl_grantRole(roleId, accountId); } -export circuit _grantRole(role: Bytes<32>, accountId: Bytes<32>): Boolean { - return ShieldedAccessControl__grantRole(role, accountId); +export circuit revokeRole( + roleId: ShieldedAccessControl_RoleIdentifier, + accountId: ShieldedAccessControl_AccountIdentifier + ): [] { + ShieldedAccessControl_revokeRole(roleId, accountId); } -export circuit revokeRole(role: Bytes<32>, accountId: Bytes<32>): [] { - ShieldedAccessControl_revokeRole(role, accountId); +export circuit renounceRole( + roleId: ShieldedAccessControl_RoleIdentifier, + accountIdConfirmation: ShieldedAccessControl_AccountIdentifier + ): [] { + ShieldedAccessControl_renounceRole(roleId, accountIdConfirmation); } -export circuit _revokeRole(role: Bytes<32>, accountId: Bytes<32>): Boolean { - return ShieldedAccessControl__revokeRole(role, accountId); +export circuit _setRoleAdmin( + roleId: ShieldedAccessControl_RoleIdentifier, + adminRole: ShieldedAccessControl_AdminIdentifier + ): [] { + ShieldedAccessControl__setRoleAdmin(roleId, adminRole); } -export circuit renounceRole(role: Bytes<32>, accountIdConfirmation: Bytes<32>): [] { - ShieldedAccessControl_renounceRole(role, accountIdConfirmation); +export circuit _grantRole( + roleId: ShieldedAccessControl_RoleIdentifier, + accountId: ShieldedAccessControl_AccountIdentifier + ): Boolean { + return ShieldedAccessControl__grantRole(roleId, accountId); } -export circuit _setRoleAdmin(role: Bytes<32>, adminRole: Bytes<32>): [] { - ShieldedAccessControl__setRoleAdmin(role, adminRole); +export circuit _revokeRole( + roleId: ShieldedAccessControl_RoleIdentifier, + accountId: ShieldedAccessControl_AccountIdentifier + ): Boolean { + return ShieldedAccessControl__revokeRole(roleId, accountId); } From d67a369fc0aa5d67275f53fb661030db9f50e366 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Wed, 11 Mar 2026 17:20:59 -0400 Subject: [PATCH 261/322] fmt files, add export to mock --- .../src/access/ShieldedAccessControl.compact | 46 +++++++--------- .../mocks/MockShieldedAccessControl.compact | 53 ++++++++++--------- 2 files changed, 48 insertions(+), 51 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index 68cd5fd1..57dc7ab6 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -366,10 +366,7 @@ module ShieldedAccessControl { * * @return {Boolean} isGranted - Returns true if a role was granted successfully. */ - export circuit _grantRole( - role: RoleIdentifier, - accountId: AccountIdentifier - ): Boolean { + export circuit _grantRole(role: RoleIdentifier, accountId: AccountIdentifier): Boolean { Initializable_assertInitialized(); const roleCommitment = _computeRoleCommitment(role, accountId); @@ -418,28 +415,25 @@ module ShieldedAccessControl { _revokeRole(role, accountId); } - /** - * @description Permanently revokes `role` from `accountId` by inserting a role nullifier into the - * `_roleCommitmentNullifiers` set. Once revoked, a new `accountId` must be generated to be re-authorized for - * `role`. At this time, proofs of non-membership on values in the Merkle tree LDT are not possible - * so a `(role, accountId)` pairing that does not exist can still be revoked. Returns false if a role is already - * revoked. Internal circuit without access restriction. - * - * @circuitInfo k=15, rows=18115 - * - * Disclosures: - * - * - A nullifier for the respective role commitment. - * - * @param {Bytes<32>} role - The role identifier. - * @param {Bytes<32>} accountId - The account identifier. - * - * @return {Boolean} isRevoked - Returns true if operation completes successfully. - */ - export circuit _revokeRole( - role: RoleIdentifier, - accountId: AccountIdentifier - ): Boolean { + /** + * @description Permanently revokes `role` from `accountId` by inserting a role nullifier into the + * `_roleCommitmentNullifiers` set. Once revoked, a new `accountId` must be generated to be re-authorized for + * `role`. At this time, proofs of non-membership on values in the Merkle tree LDT are not possible + * so a `(role, accountId)` pairing that does not exist can still be revoked. Returns false if a role is already + * revoked. Internal circuit without access restriction. + * + * @circuitInfo k=15, rows=18115 + * + * Disclosures: + * + * - A nullifier for the respective role commitment. + * + * @param {Bytes<32>} role - The role identifier. + * @param {Bytes<32>} accountId - The account identifier. + * + * @return {Boolean} isRevoked - Returns true if operation completes successfully. + */ + export circuit _revokeRole(role: RoleIdentifier, accountId: AccountIdentifier): Boolean { Initializable_assertInitialized(); const roleCommitment = _computeRoleCommitment(role, accountId); diff --git a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact index bfcc697c..2540134a 100644 --- a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact +++ b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact @@ -16,7 +16,7 @@ export { ZswapCoinPublicKey, ShieldedAccessControl_DEFAULT_ADMIN_ROLE, ShieldedAccessControl__operatorRoles, ShieldedAccessControl__roleCommitmentNullifiers, - }; + ShieldedAccessControl__adminRoles }; // witness is re-implemented in the Mock contract for testing witness wit_getRoleCommitmentPath( @@ -78,9 +78,7 @@ export pure circuit _computeNullifier( as ShieldedAccessControl_RoleNullifier; } -export circuit proveCallerRole( - roleId: ShieldedAccessControl_RoleIdentifier - ): Boolean { +export circuit proveCallerRole(roleId: ShieldedAccessControl_RoleIdentifier): Boolean { return ShieldedAccessControl_proveCallerRole(roleId); } @@ -89,29 +87,34 @@ export circuit assertOnlyRole(roleId: ShieldedAccessControl_RoleIdentifier): [] } // _validateRole is re-implemented in the Mock contract for testing -export circuit _validateRole(role: ShieldedAccessControl_RoleIdentifier, accountId: ShieldedAccessControl_AccountIdentifier): Boolean { - Initializable_assertInitialized(); - - const roleCommitment = _computeRoleCommitment(role, accountId); - const roleCommitmentPath = wit_getRoleCommitmentPath(roleCommitment); - const isValidPath = - ShieldedAccessControl__operatorRoles.checkRoot( - merkleTreePathRoot<20, ShieldedAccessControl_RoleCommitment>(disclose(roleCommitmentPath)) - ); - - // If the path is valid we assert it’s a path for the queried leaf (not some other leaf). - if (isValidPath) { - assert(roleCommitmentPath.leaf == roleCommitment, - "ShieldedAccessControl: Path must contain leaf matching computed role commitment for the provided role, accountId pairing" - ); - } - - const roleNullifier = _computeNullifier(roleCommitment); - const isRevoked = ShieldedAccessControl__roleCommitmentNullifiers.member(disclose(roleNullifier)); - - return isValidPath && !isRevoked; +export circuit _validateRole( + role: ShieldedAccessControl_RoleIdentifier, + accountId: ShieldedAccessControl_AccountIdentifier + ): Boolean { + Initializable_assertInitialized(); + + const roleCommitment = _computeRoleCommitment(role, accountId); + const roleCommitmentPath = wit_getRoleCommitmentPath(roleCommitment); + const isValidPath = + ShieldedAccessControl__operatorRoles.checkRoot( + merkleTreePathRoot<20, ShieldedAccessControl_RoleCommitment>( + disclose(roleCommitmentPath) + ) + ); + + // If the path is valid we assert it’s a path for the queried leaf (not some other leaf). + if (isValidPath) { + assert(roleCommitmentPath.leaf == roleCommitment, + "ShieldedAccessControl: Path must contain leaf matching computed role commitment for the provided role, accountId pairing" + ); } + const roleNullifier = _computeNullifier(roleCommitment); + const isRevoked = ShieldedAccessControl__roleCommitmentNullifiers.member(disclose(roleNullifier)); + + return isValidPath && !isRevoked; +} + export circuit getRoleAdmin( roleId: ShieldedAccessControl_RoleIdentifier ): ShieldedAccessControl_AdminIdentifier { From 7546ffd046e8a184d23e4413242e172fa2e89be3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Wed, 11 Mar 2026 17:26:32 -0400 Subject: [PATCH 262/322] Add additional documentation --- contracts/src/access/ShieldedAccessControl.compact | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index 57dc7ab6..d9993702 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -449,7 +449,8 @@ module ShieldedAccessControl { } /** - * @description Revokes `role` from the calling account. + * @description Revokes `role` from the calling account. Fails silently if role is already revoked. + * `role` existence is not checked, so a caller can renounce roles they don't own or don't exist. * * @notice Roles are often managed via {grantRole} and {revokeRole}: this circuit's * purpose is to provide a mechanism for accounts to lose their privileges From 5f654fcc9824bbe823dfccb34949356742946138 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Wed, 11 Mar 2026 17:26:53 -0400 Subject: [PATCH 263/322] remove unused types --- .../access/test/simulators/ShieldedAccessControlSimulator.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts index 3f3a0f8c..7430b1dd 100644 --- a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts +++ b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts @@ -4,8 +4,6 @@ import { createSimulator, } from '@openzeppelin-compact/contracts-simulator'; import { - type ContractAddress, - type Either, ledger, Contract as MockShieldedAccessControl, type ZswapCoinPublicKey, From ac1077a6c1e9ac5b28e123ee742aa70df65eed67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Wed, 11 Mar 2026 17:28:20 -0400 Subject: [PATCH 264/322] Add tests and lint files --- .../access/test/ShieldedAccessControl.test.ts | 1699 +++++++++++++---- 1 file changed, 1304 insertions(+), 395 deletions(-) diff --git a/contracts/src/access/test/ShieldedAccessControl.test.ts b/contracts/src/access/test/ShieldedAccessControl.test.ts index 28cf73d8..89373b1c 100644 --- a/contracts/src/access/test/ShieldedAccessControl.test.ts +++ b/contracts/src/access/test/ShieldedAccessControl.test.ts @@ -10,10 +10,9 @@ import { beforeEach, describe, expect, it } from 'vitest'; import * as utils from '#test-utils/address.js'; import type { Ledger, - ShieldedAccessControl_RoleCheck as RoleCheck, ZswapCoinPublicKey, } from '../../../artifacts/MockShieldedAccessControl/contract/index.js'; -import { ShieldedAccessControlPrivateState, ShieldedAccessControlWitnesses } from '../witnesses/ShieldedAccessControlWitnesses.js'; +import { ShieldedAccessControlPrivateState } from '../witnesses/ShieldedAccessControlWitnesses.js'; import { ShieldedAccessControlSimulator } from './simulators/ShieldedAccessControlSimulator.js'; const INSTANCE_SALT = new Uint8Array(32).fill(48473095); @@ -123,9 +122,9 @@ describe('ShieldedAccessControl', () => { ]; // Circuit calls should fail before the args are used const circuitsToFail: FailingCircuits[] = [ - ['unverifiedCallerHasRole', [UNINITIALIZED.roleId]], + ['proveCallerRole', [UNINITIALIZED.roleId]], ['assertOnlyRole', [UNINITIALIZED.roleId]], - ['_checkRole', [UNINITIALIZED.roleId, UNINITIALIZED.accountId]], + ['_validateRole', [UNINITIALIZED.roleId, UNINITIALIZED.accountId]], ['getRoleAdmin', [UNINITIALIZED.roleId]], ['grantRole', [UNINITIALIZED.roleId, UNINITIALIZED.accountId]], ['revokeRole', [UNINITIALIZED.roleId, UNINITIALIZED.accountId]], @@ -139,10 +138,7 @@ describe('ShieldedAccessControl', () => { ], [ '_computeAccountId', - [ - utils.createEitherTestUser(UNINITIALIZED.baseString), - UNINITIALIZED.accountId, - ], + [UNINITIALIZED.zPublicKey, UNINITIALIZED.accountId], ], ]; it.each(circuitsToFail)('%s should fail', (circuitName, args) => { @@ -237,31 +233,15 @@ describe('ShieldedAccessControl', () => { }); describe('_computeAccountId', () => { - const eitherAdmin = utils.createEitherTestUser('ADMIN'); - const eitherUnauthorized = utils.createEitherTestUser('UNAUTHORIZED'); - it('should match account id', () => { expect( shieldedAccessControl._computeAccountId( - eitherAdmin, + ADMIN.zPublicKey, ADMIN.secretNonce, ), ).toEqual(ADMIN.accountId); }); - it('should fail for contract address', () => { - const eitherContract = - utils.createEitherTestContractAddress('CONTRACT'); - expect(() => { - shieldedAccessControl._computeAccountId( - eitherContract, - ADMIN.secretNonce, - ); - }).toThrow( - 'ShieldedAccessControl: contract address roles are not yet supported', - ); - }); - type ComputeAccountIdCases = [ isValidAccount: boolean, isValidNonce: boolean, @@ -269,9 +249,9 @@ describe('ShieldedAccessControl', () => { ]; const checkedCircuits: ComputeAccountIdCases[] = [ - [true, false, [eitherAdmin, UNAUTHORIZED.secretNonce]], - [false, true, [eitherUnauthorized, ADMIN.secretNonce]], - [false, false, [eitherUnauthorized, UNAUTHORIZED.secretNonce]], + [true, false, [ADMIN.zPublicKey, UNAUTHORIZED.secretNonce]], + [false, true, [UNAUTHORIZED.zPublicKey, ADMIN.secretNonce]], + [false, false, [UNAUTHORIZED.zPublicKey, UNAUTHORIZED.secretNonce]], ]; it.each( @@ -288,13 +268,13 @@ describe('ShieldedAccessControl', () => { }); }); - describe('_checkRole', () => { + describe('_validateRole', () => { beforeEach(() => { shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); shieldedAccessControl.setPersistentCaller(ADMIN.publicKey); }); - it('should fail when wit_getRoleCommitmentPath returns valid path for a different roleId, accountId pairing', () => { + it('should fail when wit_getRoleCommitmentPath returns a valid path for a different roleId, accountId pairing', () => { shieldedAccessControl._grantRole( OPERATOR_1.roleId, OPERATOR_1.accountId, @@ -314,13 +294,13 @@ describe('ShieldedAccessControl', () => { }, ); expect(() => { - shieldedAccessControl._checkRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._validateRole(ADMIN.roleId, ADMIN.accountId); }).toThrow( - 'ShieldedAccessControl: Path must contain leaf matching computed role commitment for the provided roleId, accountId pairing', + 'ShieldedAccessControl: Path must contain leaf matching computed role commitment for the provided role, accountId pairing', ); }); - describe('observedHasRole field', () => { + describe('should return false', () => { type CheckRoleCases = [ badRoleId: boolean, badAccountId: boolean, @@ -334,117 +314,180 @@ describe('ShieldedAccessControl', () => { it.each( checkedCircuits, - )('observedHasRole should be false with badRoleId=%s badAccountId=%s', (_badRoleId, _badAccountId, args) => { + )('when badRoleId=%s badAccountId=%s', (_badRoleId, _badAccountId, args) => { // Test protected circuit expect( ( - shieldedAccessControl._checkRole as ( + shieldedAccessControl._validateRole as ( ...args: unknown[] - ) => RoleCheck - )(...args).observedHasRole, + ) => boolean + )(...args), ).toBe(false); }); - it('observedHasRole should return false if role does not exist', () => { + it('when role does not exist', () => { expect( - shieldedAccessControl._checkRole( + shieldedAccessControl._validateRole( UNINITIALIZED.roleId, ADMIN.accountId, - ).observedHasRole, + ), ).toBe(false); }); - it('observedHasRole should return true for granted role', () => { + it('when revoked role is re-issued to the same accountId', () => { + shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); expect( - shieldedAccessControl._checkRole(ADMIN.roleId, ADMIN.accountId) - .observedHasRole, - ).toBe(true); + shieldedAccessControl._validateRole(ADMIN.roleId, ADMIN.accountId), + ).toBe(false); }); - it('observedHasRole should return false when revoked role is re-granted', () => { + it('when role is revoked, ', () => { shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + const roleCheck = shieldedAccessControl._validateRole( + ADMIN.roleId, + ADMIN.accountId, + ); + expect(roleCheck).toBe(false); + }); + + it('when invalid witness is provided for a legitimately credentialed user', () => { + shieldedAccessControl.overrideWitness( + 'wit_getRoleCommitmentPath', + RETURN_BAD_PATH, + ); + expect( + shieldedAccessControl._validateRole(ADMIN.roleId, ADMIN.accountId), + ).toBe(false); + }); + + // an invalid witness should not violate the security invariant: revoked roles + // are permanent + it('when an invalid witness is provided for a revoked role', () => { + shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl.overrideWitness( + 'wit_getRoleCommitmentPath', + RETURN_BAD_PATH, + ); expect( - shieldedAccessControl._checkRole(ADMIN.roleId, ADMIN.accountId) - .observedHasRole, + shieldedAccessControl._validateRole(ADMIN.roleId, ADMIN.accountId), ).toBe(false); }); + }); + + describe('should return true', () => { + it('when role is granted', () => { + expect( + shieldedAccessControl._validateRole(ADMIN.roleId, ADMIN.accountId), + ).toBe(true); + }); - it('observedHasRole should return true for accountId with multiple roles', () => { + it('when accountId has multiple roles', () => { shieldedAccessControl._grantRole(OPERATOR_1.roleId, ADMIN.accountId); shieldedAccessControl._grantRole(OPERATOR_2.roleId, ADMIN.accountId); shieldedAccessControl._grantRole(OPERATOR_3.roleId, ADMIN.accountId); expect( - shieldedAccessControl._checkRole(ADMIN.roleId, ADMIN.accountId) - .observedHasRole, + shieldedAccessControl._validateRole(ADMIN.roleId, ADMIN.accountId), ).toBe(true); expect( - shieldedAccessControl._checkRole(OPERATOR_1.roleId, ADMIN.accountId) - .observedHasRole, + shieldedAccessControl._validateRole( + OPERATOR_1.roleId, + ADMIN.accountId, + ), ).toBe(true); expect( - shieldedAccessControl._checkRole(OPERATOR_2.roleId, ADMIN.accountId) - .observedHasRole, + shieldedAccessControl._validateRole( + OPERATOR_2.roleId, + ADMIN.accountId, + ), ).toBe(true); expect( - shieldedAccessControl._checkRole(OPERATOR_3.roleId, ADMIN.accountId) - .observedHasRole, + shieldedAccessControl._validateRole( + OPERATOR_3.roleId, + ADMIN.accountId, + ), ).toBe(true); }); - it('observedHasRole should return false for revoked role, ', () => { + it('when role is revoked and re-issued with a different accountId', () => { shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); - const roleCheck = shieldedAccessControl._checkRole( + + shieldedAccessControl.privateState.injectSecretNonce( ADMIN.roleId, - ADMIN.accountId, + Buffer.alloc(32, 'NEW_ADMIN_NONCE'), ); - expect(roleCheck.observedHasRole).toBe(false); - }); - - it('hasRole should return false for bad _operatorRoles path', () => { - shieldedAccessControl.overrideWitness( - 'wit_getRoleCommitmentPath', - RETURN_BAD_PATH, + const newAdminAccountId = buildAccountIdHash( + ADMIN.zPublicKey, + shieldedAccessControl.privateState.getCurrentSecretNonce( + ADMIN.roleId, + ), ); - expect( - shieldedAccessControl._checkRole(ADMIN.roleId, ADMIN.accountId) - .observedHasRole, - ).toBe(false); - }); - }); + expect(newAdminAccountId).not.toEqual(ADMIN.accountId); - describe('isRevoked field', () => { - it('isRevoked should be false when role is active', () => { + shieldedAccessControl._grantRole(ADMIN.roleId, newAdminAccountId); expect( - shieldedAccessControl._checkRole(ADMIN.roleId, ADMIN.accountId) - .isRevoked, - ).toBe(false); + shieldedAccessControl._validateRole( + ADMIN.roleId, + newAdminAccountId, + ), + ).toBe(true); }); - it('isRevoked should be true when role is revoked', () => { - shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + it('when multiple users have the same role', () => { + // All users will use OPERATOR_1.secretNonce as their nonce value + // when generating their accountId for simplicity + shieldedAccessControl.privateState.injectSecretNonce( + OPERATOR_1.roleId, + OPERATOR_1.secretNonce, + ); + // A unique accountId must be constructed for each new role using its associated secretNonce + const operator1AdminAccountId = buildAccountIdHash( + ADMIN.zPublicKey, + OPERATOR_1.secretNonce, + ); + shieldedAccessControl._grantRole( + OPERATOR_1.roleId, + operator1AdminAccountId, + ); + shieldedAccessControl.as(ADMIN.publicKey); // assert ADMIN has OP_1 roleId expect( - shieldedAccessControl._checkRole(ADMIN.roleId, ADMIN.accountId) - .isRevoked, + shieldedAccessControl._validateRole( + OPERATOR_1.roleId, + operator1AdminAccountId, + ), ).toBe(true); - }); - it('isRevoked should be false when role has never been granted', () => { + const operator1Op2AccountId = buildAccountIdHash( + OPERATOR_2.zPublicKey, + OPERATOR_1.secretNonce, + ); + shieldedAccessControl._grantRole( + OPERATOR_1.roleId, + operator1Op2AccountId, + ); + shieldedAccessControl.as(OPERATOR_2.publicKey); // assert OP_2 has OP_1 roleId expect( - shieldedAccessControl._checkRole( - UNINITIALIZED.roleId, - ADMIN.accountId, - ).isRevoked, - ).toBe(false); - }); + shieldedAccessControl._validateRole( + OPERATOR_1.roleId, + operator1Op2AccountId, + ), + ).toBe(true); - it('isRevoked should return true when revoked role is re-granted', () => { - shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + const operator1Op3AccountId = buildAccountIdHash( + OPERATOR_3.zPublicKey, + OPERATOR_1.secretNonce, + ); + shieldedAccessControl._grantRole( + OPERATOR_1.roleId, + operator1Op3AccountId, + ); + shieldedAccessControl.as(OPERATOR_3.publicKey); // assert OP_3 has OP_1 roleId expect( - shieldedAccessControl._checkRole(ADMIN.roleId, ADMIN.accountId) - .isRevoked, + shieldedAccessControl._validateRole( + OPERATOR_1.roleId, + operator1Op3AccountId, + ), ).toBe(true); }); }); @@ -457,7 +500,33 @@ describe('ShieldedAccessControl', () => { }); describe('should fail', () => { - it('for caller who was never granted the role', () => { + it('when wit_getRoleCommitmentPath returns a valid path for a different roleId, accountId pairing', () => { + shieldedAccessControl._grantRole( + OPERATOR_1.roleId, + OPERATOR_1.accountId, + ); + // Override witness to return valid path for OPERATOR_1 role commitment + shieldedAccessControl.overrideWitness( + 'wit_getRoleCommitmentPath', + () => { + const privateState = shieldedAccessControl.getPrivateState(); + const operator1MtPath = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf( + OPERATOR_1.roleCommitment, + ); + if (operator1MtPath) return [privateState, operator1MtPath]; + throw new Error('Merkle tree path should be defined'); + }, + ); + expect(() => { + shieldedAccessControl.assertOnlyRole(ADMIN.roleId); + }).toThrow( + 'ShieldedAccessControl: Path must contain leaf matching computed role commitment for the provided role, accountId pairing', + ); + }); + + it('when caller was never granted the role', () => { shieldedAccessControl.as(UNAUTHORIZED.publicKey); expect(() => shieldedAccessControl.assertOnlyRole(ADMIN.roleId), @@ -465,6 +534,18 @@ describe('ShieldedAccessControl', () => { }); it('when authorized caller has incorrect path', () => { + // Check caller is admin, has admin role + expect( + shieldedAccessControl.getCallerContext().currentZswapLocalState + .coinPublicKey, + ).toEqual(ADMIN.zPublicKey); + expect(shieldedAccessControl.getRoleAdmin(ADMIN.roleId)).toEqual( + new Uint8Array(ADMIN.roleId), + ); + expect( + shieldedAccessControl._validateRole(ADMIN.roleId, ADMIN.accountId), + ).toBe(true); + // Check nonce is correct expect( shieldedAccessControl.privateState.getCurrentSecretNonce( @@ -477,7 +558,10 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl.privateState.getCommitmentPathWithFindForLeaf( ADMIN.roleCommitment, ); - shieldedAccessControl.overrideWitness('wit_getRoleCommitmentPath', RETURN_BAD_PATH); + shieldedAccessControl.overrideWitness( + 'wit_getRoleCommitmentPath', + RETURN_BAD_PATH, + ); const witnessCalculatedPath = shieldedAccessControl.privateState.getCommitmentPathWithWitnessImpl( ADMIN.roleCommitment, @@ -490,7 +574,22 @@ describe('ShieldedAccessControl', () => { }); it('when authorized caller has incorrect nonce', () => { - shieldedAccessControl.privateState.injectSecretNonce(ADMIN.roleId, UNAUTHORIZED.secretNonce); + // Check caller is admin, has admin role + expect( + shieldedAccessControl.getCallerContext().currentZswapLocalState + .coinPublicKey, + ).toEqual(ADMIN.zPublicKey); + expect(shieldedAccessControl.getRoleAdmin(ADMIN.roleId)).toEqual( + new Uint8Array(ADMIN.roleId), + ); + expect( + shieldedAccessControl._validateRole(ADMIN.roleId, ADMIN.accountId), + ).toBe(true); + + shieldedAccessControl.privateState.injectSecretNonce( + ADMIN.roleId, + UNAUTHORIZED.secretNonce, + ); // Check nonce is incorrect expect( @@ -516,6 +615,17 @@ describe('ShieldedAccessControl', () => { }); it('when unauthorized caller has correct nonce, and path', () => { + // Check UNAUTHORIZED user is not admin, doesnt have admin role + expect(shieldedAccessControl.getRoleAdmin(ADMIN.roleId)).not.toEqual( + new Uint8Array(UNAUTHORIZED.roleId), + ); + expect( + shieldedAccessControl._validateRole( + ADMIN.roleId, + UNAUTHORIZED.accountId, + ), + ).toBe(false); + // Check nonce is correct expect( shieldedAccessControl.privateState.getCurrentSecretNonce( @@ -535,20 +645,25 @@ describe('ShieldedAccessControl', () => { expect(witnessCalculatedPath).toEqual(truePath); shieldedAccessControl.as(UNAUTHORIZED.publicKey); + // Check caller is UNAUTHORIZED user + expect( + shieldedAccessControl.getCallerContext().currentZswapLocalState + .coinPublicKey, + ).toEqual(UNAUTHORIZED.zPublicKey); expect(() => shieldedAccessControl.assertOnlyRole(ADMIN.roleId), ).toThrow('ShieldedAccessControl: unauthorized account'); }); - it('for revoked role', () => { + it('when role is revoked', () => { shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); expect(() => shieldedAccessControl.assertOnlyRole(ADMIN.roleId), ).toThrow('ShieldedAccessControl: unauthorized account'); }); - it('for revoked role with re-approval', () => { + it('when role is revoked and re-issued to the same accountId', () => { shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); expect(() => @@ -558,7 +673,7 @@ describe('ShieldedAccessControl', () => { }); describe('should not fail', () => { - it('for admin with multiple roles', () => { + it('when accountId has multiple roles', () => { shieldedAccessControl.privateState.injectSecretNonce( OPERATOR_1.roleId, OPERATOR_1.secretNonce, @@ -587,9 +702,18 @@ describe('ShieldedAccessControl', () => { OPERATOR_3.secretNonce, ); - shieldedAccessControl._grantRole(OPERATOR_1.roleId, operator1AccountId); - shieldedAccessControl._grantRole(OPERATOR_2.roleId, operator2AccountId); - shieldedAccessControl._grantRole(OPERATOR_3.roleId, operator3AccountId); + shieldedAccessControl._grantRole( + OPERATOR_1.roleId, + operator1AccountId, + ); + shieldedAccessControl._grantRole( + OPERATOR_2.roleId, + operator2AccountId, + ); + shieldedAccessControl._grantRole( + OPERATOR_3.roleId, + operator3AccountId, + ); expect(() => { shieldedAccessControl.assertOnlyRole(ADMIN.roleId); shieldedAccessControl.assertOnlyRole(OPERATOR_1.roleId); @@ -598,7 +722,7 @@ describe('ShieldedAccessControl', () => { }).not.toThrow(); }); - it('when authorized ADMIN has correct nonce, and path', () => { + it('when authorized caller has correct nonce, and path', () => { // Check nonce is correct expect( shieldedAccessControl.privateState.getCurrentSecretNonce( @@ -622,7 +746,7 @@ describe('ShieldedAccessControl', () => { ).not.toThrow(); }); - it('for multiple users with the same role', () => { + it('when multiple users have the same role', () => { // All users will use OPERATOR_1.secretNonce as their nonce value // when generating their accountId for simplicity shieldedAccessControl.privateState.injectSecretNonce( @@ -634,7 +758,10 @@ describe('ShieldedAccessControl', () => { ADMIN.zPublicKey, OPERATOR_1.secretNonce, ); - shieldedAccessControl._grantRole(OPERATOR_1.roleId, operator1AdminAccountId); + shieldedAccessControl._grantRole( + OPERATOR_1.roleId, + operator1AdminAccountId, + ); shieldedAccessControl.as(ADMIN.publicKey); // assert ADMIN has OP_1 roleId expect(shieldedAccessControl.assertOnlyRole(OPERATOR_1.roleId)); @@ -642,7 +769,10 @@ describe('ShieldedAccessControl', () => { OPERATOR_2.zPublicKey, OPERATOR_1.secretNonce, ); - shieldedAccessControl._grantRole(OPERATOR_1.roleId, operator1Op2AccountId); + shieldedAccessControl._grantRole( + OPERATOR_1.roleId, + operator1Op2AccountId, + ); shieldedAccessControl.as(OPERATOR_2.publicKey); // assert OP_2 has OP_1 roleId expect(shieldedAccessControl.assertOnlyRole(OPERATOR_1.roleId)); @@ -650,16 +780,34 @@ describe('ShieldedAccessControl', () => { OPERATOR_3.zPublicKey, OPERATOR_1.secretNonce, ); - shieldedAccessControl._grantRole(OPERATOR_1.roleId, operator1Op3AccountId); + shieldedAccessControl._grantRole( + OPERATOR_1.roleId, + operator1Op3AccountId, + ); shieldedAccessControl.as(OPERATOR_3.publicKey); // assert OP_3 has OP_1 roleId expect(shieldedAccessControl.assertOnlyRole(OPERATOR_1.roleId)); }); - }) + }); }); - describe('_grantRole', () => { + describe('grantRole', () => { + beforeEach(() => { + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl.setPersistentCaller(ADMIN.publicKey); + }); + describe('should fail', () => { - it('when valid merkle tree path in _operatorRoles does not contain matching leaf', () => { + it('when caller does not have the admin role', () => { + shieldedAccessControl.as(UNAUTHORIZED.publicKey); + expect(() => + shieldedAccessControl.grantRole( + OPERATOR_1.roleId, + OPERATOR_1.accountId, + ), + ).toThrow('ShieldedAccessControl: unauthorized account'); + }); + + it('when wit_getRoleCommitmentPath returns a valid path for a different roleId, accountId pairing', () => { shieldedAccessControl._grantRole( OPERATOR_1.roleId, OPERATOR_1.accountId, @@ -679,13 +827,104 @@ describe('ShieldedAccessControl', () => { }, ); expect(() => { - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl.grantRole(ADMIN.roleId, ADMIN.accountId); }).toThrow( - 'ShieldedAccessControl: Path must contain leaf matching computed role commitment for the provided roleId, accountId pairing', + 'ShieldedAccessControl: Path must contain leaf matching computed role commitment for the provided role, accountId pairing', + ); + }); + + it.todo('when role is revoked and re-issued to the same accountId'); + it.todo('when role is revoked'); + it.todo('when non-admin caller has role'); + it.todo('when admin provides incorrect nonce'); + it.todo('when admin provides bad witness path'); + }); + + describe('should not update _operatorRoles Merkle tree', () => { + it.todo('when re-granting revoked role', () => {}); + it.todo('when role is revoked and re-issued to the same accountId'); + it.todo('when role is revoked'); + it.todo('when non-admin caller has role'); + it.todo('when admin provides incorrect nonce'); + it.todo('when admin provides bad witness path'); + }); + + describe('should grant role', () => { + it('when caller has the admin role', () => { + expect(() => + shieldedAccessControl.grantRole( + OPERATOR_1.roleId, + OPERATOR_1.accountId, + ), + ).not.toThrow(); + expect( + shieldedAccessControl._validateRole( + OPERATOR_1.roleId, + OPERATOR_1.accountId, + ), + ).toBe(true); + }); + + it('when caller has custom admin role', () => { + // Make OPERATOR_1.roleId the admin of OPERATOR_2.roleId. + shieldedAccessControl._setRoleAdmin( + OPERATOR_2.roleId, + OPERATOR_1.roleId, + ); + // Grant OPERATOR_1.roleId to OPERATOR_1.accountId + shieldedAccessControl.grantRole( + OPERATOR_1.roleId, + OPERATOR_1.accountId, + ); + + // Switch to OPERATOR_1 as caller and inject their nonce for their role. + shieldedAccessControl.privateState.injectSecretNonce( + OPERATOR_1.roleId, + OPERATOR_1.secretNonce, ); + shieldedAccessControl.setPersistentCaller(OPERATOR_1.publicKey); + + // OPERATOR_1.accountId (who holds OPERATOR_1.roleId) can now grant OPERATOR_2.roleId. + expect(() => + shieldedAccessControl.grantRole( + OPERATOR_2.roleId, + OPERATOR_2.accountId, + ), + ).not.toThrow(); + expect( + shieldedAccessControl._validateRole( + OPERATOR_2.roleId, + OPERATOR_2.accountId, + ), + ).toBe(true); }); + + it.todo( + 'when admin role is revoked and re-issued with a different accountId', + ); + + it.todo('when multiple admins of the same role exist'); + it.todo('when admin has multiple roles'); + it.todo('when re-granting active role'); + it.todo('when granting role that does not exist'); + it.todo('when granting role with bad accountId'); + }); + + describe('should update _operatorRoles Merkle tree', () => { + it.todo( + 'when admin role is revoked and re-issued with a different accountId', + ); + it.todo('when caller has admin role'); + it.todo('when caller has custom admin role'); + it.todo('when multiple admins of the same role exist'); + it.todo('when admin has multiple roles'); + it.todo('when re-granting active role'); + it.todo('when granting role that does not exist'); + it.todo('when granting role with bad accountId'); }); + }); + describe('_grantRole', () => { describe('should return true', () => { it('when authorized user grants a new role', () => { shieldedAccessControl.as(ADMIN.publicKey); @@ -695,15 +934,48 @@ describe('ShieldedAccessControl', () => { }); it('when unauthorized user grants role', () => { - shieldedAccessControl.as(UNAUTHORIZED.publicKey) + shieldedAccessControl.as(UNAUTHORIZED.publicKey); expect( shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId), ).toBe(true); }); + + it('when re-granting active role ', () => { + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + + expect( + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId), + ).toBe(true); + }); + + it('when granting role that does not exist', () => { + expect( + shieldedAccessControl._grantRole( + UNINITIALIZED.roleId, + ADMIN.accountId, + ), + ).toBe(true); + }); + + it('when granting role with bad accountId', () => { + expect( + shieldedAccessControl._grantRole(ADMIN.roleId, BAD_INPUT.accountId), + ).toBe(true); + }); }); describe('should update _operatorRoles merkle tree', () => { it('when authorized user grants a new role', () => { + shieldedAccessControl.setPersistentCaller(ADMIN.publicKey); + // Check caller is admin, has admin role + expect( + shieldedAccessControl.getCallerContext().currentZswapLocalState + .coinPublicKey, + ).toEqual(ADMIN.zPublicKey); + expect(shieldedAccessControl.getRoleAdmin(ADMIN.roleId)).toEqual( + new Uint8Array(ADMIN.roleId), + ); + // check merkle tree is empty let merkleRoot = shieldedAccessControl .getPublicState() @@ -719,20 +991,41 @@ describe('ShieldedAccessControl', () => { expect(merkleRoot).not.toBe(0n); // check path exists for new role - const merkleTreePath = shieldedAccessControl.getPublicState().ShieldedAccessControl__operatorRoles.findPathForLeaf(ADMIN.roleCommitment); + const merkleTreePath = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf( + ADMIN.roleCommitment, + ); expect(merkleTreePath).toBeDefined(); expect(merkleTreePath?.leaf).toStrictEqual(ADMIN.roleCommitment); }); it('when unauthorized user grants a new role', () => { + // Check UNAUTHORIZED is not admin + expect(shieldedAccessControl.getRoleAdmin(ADMIN.roleId)).not.toEqual( + new Uint8Array(UNAUTHORIZED.roleId), + ); + expect( + shieldedAccessControl._validateRole( + ADMIN.roleId, + UNAUTHORIZED.accountId, + ), + ).toBe(false); + // check merkle tree is empty let merkleRoot = shieldedAccessControl .getPublicState() .ShieldedAccessControl__operatorRoles.root(); expect(merkleRoot.field).toBe(0n); - // check merkle tree is updated + // check caller is UNAUTHORIZED user shieldedAccessControl.as(UNAUTHORIZED.publicKey); + expect( + shieldedAccessControl.getCallerContext().currentZswapLocalState + .coinPublicKey, + ).toEqual(UNAUTHORIZED.zPublicKey); + + // check merkle tree is updated shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); merkleRoot = shieldedAccessControl .getPublicState() @@ -740,191 +1033,889 @@ describe('ShieldedAccessControl', () => { expect(merkleRoot).not.toBe(0n); // check path exists for new role - const merkleTreePath = shieldedAccessControl.getPublicState().ShieldedAccessControl__operatorRoles.findPathForLeaf(ADMIN.roleCommitment); + const merkleTreePath = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf( + ADMIN.roleCommitment, + ); expect(merkleTreePath).toBeDefined(); expect(merkleTreePath?.leaf).toStrictEqual(ADMIN.roleCommitment); }); + + it('when granting role that does not exist', () => { + // check merkle tree is empty + let merkleRoot = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.root(); + expect(merkleRoot.field).toBe(0n); + + // check merkle tree is updated + shieldedAccessControl._grantRole( + UNINITIALIZED.roleId, + UNINITIALIZED.accountId, + ); + merkleRoot = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.root(); + expect(merkleRoot).not.toBe(0n); + + // check path exists for new role + const merkleTreePath = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf( + UNINITIALIZED.roleCommitment, + ); + expect(merkleTreePath).toBeDefined(); + expect(merkleTreePath?.leaf).toStrictEqual( + UNINITIALIZED.roleCommitment, + ); + }); + + it('when granting role with bad accountId', () => { + // check merkle tree is empty + let merkleRoot = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.root(); + expect(merkleRoot.field).toBe(0n); + + // check merkle tree is updated + shieldedAccessControl._grantRole(ADMIN.roleId, BAD_INPUT.accountId); + merkleRoot = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.root(); + expect(merkleRoot).not.toBe(0n); + + // check path exists for new role + const adminRoleBadAccountCommitment = buildRoleCommitmentHash( + ADMIN.roleId, + BAD_INPUT.accountId, + ); + const merkleTreePath = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf( + adminRoleBadAccountCommitment, + ); + expect(merkleTreePath).toBeDefined(); + expect(merkleTreePath?.leaf).toStrictEqual( + adminRoleBadAccountCommitment, + ); + }); }); describe('should return false', () => { - it('when re-granting a role', () => { + it('when re-granting revoked role', () => { shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); expect( shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId), ).toBe(false); }); + }); + describe('should not update _operatorRoles merkle tree', () => { it('when re-granting revoked role', () => { shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + const merkleRoot = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.root(); + + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + const newMerkleRoot = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.root(); + expect(merkleRoot).toEqual(newMerkleRoot); + }); + }); + }); + + describe('revokeRole', () => { + beforeEach(() => { + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._grantRole( + OPERATOR_1.roleId, + OPERATOR_1.accountId, + ); + shieldedAccessControl.setPersistentCaller(ADMIN.publicKey); + }); + + describe('should fail', () => { + it('when caller does not have the admin role', () => { + shieldedAccessControl.as(UNAUTHORIZED.publicKey); + expect(() => + shieldedAccessControl.revokeRole( + OPERATOR_1.roleId, + OPERATOR_1.accountId, + ), + ).toThrow('ShieldedAccessControl: unauthorized account'); + }); + it.todo('when admin role is revoked from caller'); + it.todo('when caller is admin of a different role'); + it.todo('when admin provides invalid Merkle tree path'); + + it('when authorized caller provides bad nonce', () => { + shieldedAccessControl.privateState.injectSecretNonce( + ADMIN.roleId, + BAD_INPUT.secretNonce, + ); + expect(() => + shieldedAccessControl.revokeRole(ADMIN.roleId, ADMIN.accountId), + ).toThrow('ShieldedAccessControl: unauthorized account'); + }); + }); + + describe('should not update _roleCommitmentNullifiers set', () => { + it('when role is re-revoked', () => { + shieldedAccessControl.revokeRole( + OPERATOR_1.roleId, + OPERATOR_1.accountId, + ); + const nullifierSetSize = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.size(); + expect(() => + shieldedAccessControl.revokeRole( + OPERATOR_1.roleId, + OPERATOR_1.accountId, + ), + ).not.toThrow(); expect( - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId), + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.size(), + ).toEqual(nullifierSetSize); + }); + }); + + describe('should revoke role', () => { + it('when caller has the admin role', () => { + expect(() => + shieldedAccessControl.revokeRole( + OPERATOR_1.roleId, + OPERATOR_1.accountId, + ), + ).not.toThrow(); + expect( + shieldedAccessControl._validateRole( + OPERATOR_1.roleId, + OPERATOR_1.accountId, + ), ).toBe(false); }); - it('when witness returns a bad path', () => { - // a bad role commitment path causes _operatorRoles.checkRoot() to return false, so observedHasRole=false - // isRevoked=false because the role has not been revoked yet so this will allow a duplicate role - // commitment to be added to the merkle tree. However, duplicate role commitments do not - // violate our security invariant - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); - shieldedAccessControl.overrideWitness('wit_getRoleCommitmentPath', RETURN_BAD_PATH); + it('when caller has custom admin role', () => { + // setup test + shieldedAccessControl._grantRole( + OPERATOR_2.roleId, + OPERATOR_3.accountId, + ); + shieldedAccessControl.privateState.injectSecretNonce( + OPERATOR_1.roleId, + OPERATOR_1.secretNonce, + ); + // OP_1 is admin of OP_2 role + shieldedAccessControl._setRoleAdmin( + OPERATOR_2.roleId, + OPERATOR_1.roleId, + ); + shieldedAccessControl.as(OPERATOR_1.publicKey); - const isGranted = shieldedAccessControl._grantRole( + expect(() => + shieldedAccessControl.revokeRole( + OPERATOR_2.roleId, + OPERATOR_3.accountId, + ), + ).not.toThrow(); + expect( + shieldedAccessControl._validateRole( + OPERATOR_2.roleId, + OPERATOR_3.accountId, + ), + ).toBe(false); + }); + + it('when role does not exist', () => { + // create role commitment that doesn't exist + const commitment = buildRoleCommitmentHash( + UNINITIALIZED.roleId, + ADMIN.accountId, + ); + + // confirm role commitment not in Merkle tree + const path = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf(commitment); + expect(path).toBeUndefined(); + + expect(() => + shieldedAccessControl.revokeRole( + UNINITIALIZED.roleId, + ADMIN.accountId, + ), + ).not.toThrow(); + + expect( + shieldedAccessControl._validateRole( + UNINITIALIZED.roleId, + ADMIN.accountId, + ), + ).toBe(false); + }); + + it('when revoking role with bad accountId', () => { + expect(() => + shieldedAccessControl.revokeRole(ADMIN.roleId, BAD_INPUT.accountId), + ).not.toThrow(); + + expect( + shieldedAccessControl._validateRole( + ADMIN.roleId, + BAD_INPUT.accountId, + ), + ).toBe(false); + }); + + it.todo('when multiple admins of the same role exist'); + it.todo('when admin has multiple roles'); + it.todo( + 'when admin role is revoked and re-issued with a different accountId', + ); + }); + + describe('should update _roleCommitmentNullifiers set', () => { + it.todo('when caller has admin role'); + it.todo('when caller has custom admin role'); + it.todo('when role does not exist'); + it.todo('when multiple admins of the same role exist'); + it.todo('when admin has multiple roles'); + }); + }); + + describe('_revokeRole', () => { + beforeEach(() => { + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl.setPersistentCaller(ADMIN.publicKey); + }); + + describe('should return true', () => { + it('when active role is revoked', () => { + // confirm role is active + const isValidRole = shieldedAccessControl._validateRole( + ADMIN.roleId, + ADMIN.accountId, + ); + expect(isValidRole).toBe(true); + + expect( + shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId), + ).toBe(true); + }); + + it('when an authorized user revokes role', () => { + // Check caller is admin, has admin role + expect( + shieldedAccessControl.getCallerContext().currentZswapLocalState + .coinPublicKey, + ).toEqual(ADMIN.zPublicKey); + expect(shieldedAccessControl.getRoleAdmin(ADMIN.roleId)).toEqual( + new Uint8Array(ADMIN.roleId), + ); + expect( + shieldedAccessControl._validateRole(ADMIN.roleId, ADMIN.accountId), + ).toBe(true); + + expect( + shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId), + ).toBe(true); + }); + + it('when unauthorized user revokes role', () => { + // Check UNAUTHORIZED is not admin + expect(shieldedAccessControl.getRoleAdmin(ADMIN.roleId)).not.toEqual( + new Uint8Array(UNAUTHORIZED.roleId), + ); + expect( + shieldedAccessControl._validateRole( + ADMIN.roleId, + UNAUTHORIZED.accountId, + ), + ).toBe(false); + + // check caller is UNAUTHORIZED user + shieldedAccessControl.as(UNAUTHORIZED.publicKey); + expect( + shieldedAccessControl.getCallerContext().currentZswapLocalState + .coinPublicKey, + ).toEqual(UNAUTHORIZED.zPublicKey); + expect( + shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId), + ).toBe(true); + }); + + it('when revoking role that does not exist', () => { + // create role commitment that doesn't exist + const commitment = buildRoleCommitmentHash( + UNINITIALIZED.roleId, + ADMIN.accountId, + ); + + // confirm role commitment not in Merkle tree + const path = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf(commitment); + expect(path).toBeUndefined(); + + expect( + shieldedAccessControl._revokeRole( + UNINITIALIZED.roleId, + ADMIN.accountId, + ), + ).toBe(true); + }); + + it('when revoking role with bad accountId', () => { + expect( + shieldedAccessControl._revokeRole( + ADMIN.roleId, + BAD_INPUT.accountId, + ), + ).toBe(true); + }); + }); + + describe('should update nullifier set', () => { + it('when active role is revoked', () => { + // confirm role is active + const isValidRole = shieldedAccessControl._validateRole( ADMIN.roleId, ADMIN.accountId, ); - expect(isGranted).toBe(true); + expect(isValidRole).toBe(true); + + const initialSetSize = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.size(); + expect(initialSetSize).toBe(0n); + + shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + + const updatedSetSize = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.size(); + expect(updatedSetSize).toBe(1n); + expect( + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.member( + ADMIN.roleNullifier, + ), + ).toBe(true); + }); + + it('when an authorized user revokes role', () => { + // Check caller is admin, has admin role + expect( + shieldedAccessControl.getCallerContext().currentZswapLocalState + .coinPublicKey, + ).toEqual(ADMIN.zPublicKey); + expect(shieldedAccessControl.getRoleAdmin(ADMIN.roleId)).toEqual( + new Uint8Array(ADMIN.roleId), + ); + expect( + shieldedAccessControl._validateRole(ADMIN.roleId, ADMIN.accountId), + ).toBe(true); + + const initialSetSize = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.size(); + expect(initialSetSize).toBe(0n); + + shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + + const updatedSetSize = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.size(); + expect(updatedSetSize).toBe(1n); + expect( + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.member( + ADMIN.roleNullifier, + ), + ).toBe(true); + }); + + it('when unauthorized user revokes role', () => { + // Check UNAUTHORIZED is not admin + expect(shieldedAccessControl.getRoleAdmin(ADMIN.roleId)).not.toEqual( + new Uint8Array(UNAUTHORIZED.roleId), + ); + expect( + shieldedAccessControl._validateRole( + ADMIN.roleId, + UNAUTHORIZED.accountId, + ), + ).toBe(false); + + const initialSetSize = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.size(); + expect(initialSetSize).toBe(0n); + + // check caller is UNAUTHORIZED user + shieldedAccessControl.as(UNAUTHORIZED.publicKey); + expect( + shieldedAccessControl.getCallerContext().currentZswapLocalState + .coinPublicKey, + ).toEqual(UNAUTHORIZED.zPublicKey); + + shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + + const updatedSetSize = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.size(); + expect(updatedSetSize).toBe(1n); + expect( + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.member( + ADMIN.roleNullifier, + ), + ).toBe(true); + }); + + it('when revoking role that does not exist', () => { + // create role commitment that doesn't exist + const commitment = buildRoleCommitmentHash( + UNINITIALIZED.roleId, + ADMIN.accountId, + ); + + // confirm role commitment not in Merkle tree + const path = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf(commitment); + expect(path).toBeUndefined(); + + const initialSetSize = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.size(); + expect(initialSetSize).toBe(0n); + + shieldedAccessControl._revokeRole( + UNINITIALIZED.roleId, + ADMIN.accountId, + ); + + const updatedSetSize = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.size(); + expect(updatedSetSize).toBe(1n); + + const nullifier = buildNullifierHash(commitment); + + expect( + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.member( + nullifier, + ), + ).toBe(true); + }); + + it('when revoking role with bad accountId', () => { + const initialSetSize = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.size(); + expect(initialSetSize).toBe(0n); + + shieldedAccessControl._revokeRole(ADMIN.roleId, BAD_INPUT.accountId); + + const updatedSetSize = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.size(); + expect(updatedSetSize).toBe(1n); + + const commitment = buildRoleCommitmentHash( + ADMIN.roleId, + BAD_INPUT.accountId, + ); + const nullifier = buildNullifierHash(commitment); + expect( + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.member( + nullifier, + ), + ).toBe(true); + }); + }); + + describe('should return false', () => { + it('when authorized user re-revokes role', () => { + // Check caller is admin, has admin role + expect( + shieldedAccessControl.getCallerContext().currentZswapLocalState + .coinPublicKey, + ).toEqual(ADMIN.zPublicKey); + expect(shieldedAccessControl.getRoleAdmin(ADMIN.roleId)).toEqual( + new Uint8Array(ADMIN.roleId), + ); + expect( + shieldedAccessControl._validateRole(ADMIN.roleId, ADMIN.accountId), + ).toBe(true); - // Reset witness back to the default implementation - shieldedAccessControl.overrideWitness('wit_getRoleCommitmentPath', ShieldedAccessControlWitnesses().wit_getRoleCommitmentPath); shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); - expect(shieldedAccessControl.getPublicState().ShieldedAccessControl__roleCommitmentNullifiers.member(ADMIN.roleNullifier)).toBe(true); + expect( + shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId), + ).toBe(false); + }); - const roleCheck = shieldedAccessControl._checkRole(ADMIN.roleId, ADMIN.accountId); - expect(roleCheck.isRevoked).toBe(true); + it('when unauthorized user re-revokes role', () => { + // Check UNAUTHORIZED is not admin + expect(shieldedAccessControl.getRoleAdmin(ADMIN.roleId)).not.toEqual( + new Uint8Array(UNAUTHORIZED.roleId), + ); + expect( + shieldedAccessControl._validateRole( + ADMIN.roleId, + UNAUTHORIZED.accountId, + ), + ).toBe(false); + + // revoke as ADMIN + shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + + // check caller is UNAUTHORIZED user + shieldedAccessControl.as(UNAUTHORIZED.publicKey); + expect( + shieldedAccessControl.getCallerContext().currentZswapLocalState + .coinPublicKey, + ).toEqual(UNAUTHORIZED.zPublicKey); + expect( + shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId), + ).toBe(false); }); }); - describe('should not update _operatorRoles merkle tree', () => { - it('when re-granting a role', () => { - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); - const merkleRoot = shieldedAccessControl + describe('should not update nullifier set', () => { + it('when authorized user re-revokes role', () => { + shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + const initialSetSize = shieldedAccessControl .getPublicState() - .ShieldedAccessControl__operatorRoles.root(); + .ShieldedAccessControl__roleCommitmentNullifiers.size(); + expect(initialSetSize).toBe(1n); - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId) - const newMerkleRoot = shieldedAccessControl + // Check caller is admin, doesn't have admin role + expect( + shieldedAccessControl.getCallerContext().currentZswapLocalState + .coinPublicKey, + ).toEqual(ADMIN.zPublicKey); + expect( + shieldedAccessControl._validateRole(ADMIN.roleId, ADMIN.accountId), + ).toBe(false); + + shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + + const updatedSetSize = shieldedAccessControl .getPublicState() - .ShieldedAccessControl__operatorRoles.root(); - expect(merkleRoot).toEqual(newMerkleRoot); + .ShieldedAccessControl__roleCommitmentNullifiers.size(); + expect(updatedSetSize).toEqual(initialSetSize); }); - it('when re-granting revoked role', () => { - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + it('when unauthorized user re-revokes role', () => { shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); - const merkleRoot = shieldedAccessControl + const initialSetSize = shieldedAccessControl .getPublicState() - .ShieldedAccessControl__operatorRoles.root(); + .ShieldedAccessControl__roleCommitmentNullifiers.size(); + expect(initialSetSize).toBe(1n); + + // Check UNAUTHORIZED is not admin + expect(shieldedAccessControl.getRoleAdmin(ADMIN.roleId)).not.toEqual( + new Uint8Array(UNAUTHORIZED.roleId), + ); + expect( + shieldedAccessControl._validateRole( + ADMIN.roleId, + UNAUTHORIZED.accountId, + ), + ).toBe(false); + + // re-revoke as UNAUTHORIZED + shieldedAccessControl.as(UNAUTHORIZED.publicKey); + shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + + const updatedSetSize = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.size(); + expect(updatedSetSize).toEqual(initialSetSize); + }); + }); + }); + + describe('proveCallerRole', () => { + beforeEach(() => { + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl.setPersistentCaller(ADMIN.publicKey); + }); + + it('should fail when caller provides valid path for a different roleId, accountId pairing', () => { + shieldedAccessControl._grantRole( + OPERATOR_1.roleId, + OPERATOR_1.accountId, + ); + // Override witness to return valid path for OPERATOR_1 role commitment + shieldedAccessControl.overrideWitness( + 'wit_getRoleCommitmentPath', + () => { + const privateState = shieldedAccessControl.getPrivateState(); + const operator1MtPath = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf( + OPERATOR_1.roleCommitment, + ); + if (operator1MtPath) return [privateState, operator1MtPath]; + throw new Error('Merkle tree path should be defined'); + }, + ); + expect(() => { + shieldedAccessControl.proveCallerRole(ADMIN.roleId); + }).toThrow( + 'ShieldedAccessControl: Path must contain leaf matching computed role commitment for the provided role, accountId pairing', + ); + }); + + describe('should return true', () => { + it('when caller has role', () => { + // Check caller is admin, has admin role + expect( + shieldedAccessControl.getCallerContext().currentZswapLocalState + .coinPublicKey, + ).toEqual(ADMIN.zPublicKey); + expect( + shieldedAccessControl._validateRole(ADMIN.roleId, ADMIN.accountId), + ).toBe(true); + + expect(shieldedAccessControl.proveCallerRole(ADMIN.roleId)).toBe( + true, + ); + }); + + it('when caller has multiple roles', () => { + // setup test + shieldedAccessControl.privateState.injectSecretNonce( + OPERATOR_1.roleId, + OPERATOR_1.secretNonce, + ); + shieldedAccessControl.privateState.injectSecretNonce( + OPERATOR_2.roleId, + OPERATOR_2.secretNonce, + ); + shieldedAccessControl.privateState.injectSecretNonce( + OPERATOR_3.roleId, + OPERATOR_3.secretNonce, + ); + const account1 = buildAccountIdHash( + ADMIN.zPublicKey, + OPERATOR_1.secretNonce, + ); + const account2 = buildAccountIdHash( + ADMIN.zPublicKey, + OPERATOR_2.secretNonce, + ); + const account3 = buildAccountIdHash( + ADMIN.zPublicKey, + OPERATOR_3.secretNonce, + ); + shieldedAccessControl._grantRole(OPERATOR_1.roleId, account1); + shieldedAccessControl._grantRole(OPERATOR_2.roleId, account2); + shieldedAccessControl._grantRole(OPERATOR_3.roleId, account3); + + expect(shieldedAccessControl.proveCallerRole(ADMIN.roleId)).toBe( + true, + ); + expect(shieldedAccessControl.proveCallerRole(OPERATOR_1.roleId)).toBe( + true, + ); + expect(shieldedAccessControl.proveCallerRole(OPERATOR_2.roleId)).toBe( + true, + ); + expect(shieldedAccessControl.proveCallerRole(OPERATOR_3.roleId)).toBe( + true, + ); + }); + + it('when role is revoked and re-issued with a different accountId', () => { + shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + + shieldedAccessControl.privateState.injectSecretNonce( + ADMIN.roleId, + Buffer.alloc(32, 'NEW_ADMIN_NONCE'), + ); + const newAdminAccountId = buildAccountIdHash( + ADMIN.zPublicKey, + shieldedAccessControl.privateState.getCurrentSecretNonce( + ADMIN.roleId, + ), + ); + expect(newAdminAccountId).not.toEqual(ADMIN.accountId); + + shieldedAccessControl._grantRole(ADMIN.roleId, newAdminAccountId); + expect(shieldedAccessControl.proveCallerRole(ADMIN.roleId)).toBe( + true, + ); + }); + + it('when multiple users have the same role', () => { + // All users will use OPERATOR_1.secretNonce as their nonce value + // when generating their accountId for simplicity + shieldedAccessControl.privateState.injectSecretNonce( + OPERATOR_1.roleId, + OPERATOR_1.secretNonce, + ); + // A unique accountId must be constructed for each new role using its associated secretNonce + const operator1AdminAccountId = buildAccountIdHash( + ADMIN.zPublicKey, + OPERATOR_1.secretNonce, + ); + shieldedAccessControl._grantRole( + OPERATOR_1.roleId, + operator1AdminAccountId, + ); + shieldedAccessControl.as(ADMIN.publicKey); // prove ADMIN has OP_1 roleId + expect(shieldedAccessControl.proveCallerRole(OPERATOR_1.roleId)).toBe( + true, + ); + + const operator1Op2AccountId = buildAccountIdHash( + OPERATOR_2.zPublicKey, + OPERATOR_1.secretNonce, + ); + shieldedAccessControl._grantRole( + OPERATOR_1.roleId, + operator1Op2AccountId, + ); + shieldedAccessControl.as(OPERATOR_2.publicKey); // prove OP_2 has OP_1 roleId + expect(shieldedAccessControl.proveCallerRole(OPERATOR_1.roleId)).toBe( + true, + ); - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId) - const newMerkleRoot = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.root(); - expect(merkleRoot).toEqual(newMerkleRoot); + const operator1Op3AccountId = buildAccountIdHash( + OPERATOR_3.zPublicKey, + OPERATOR_1.secretNonce, + ); + shieldedAccessControl._grantRole( + OPERATOR_1.roleId, + operator1Op3AccountId, + ); + shieldedAccessControl.as(OPERATOR_3.publicKey); // prove OP_3 has OP_1 roleId + expect(shieldedAccessControl.proveCallerRole(OPERATOR_1.roleId)).toBe( + true, + ); }); }); - }); - - describe('_revokeRole', () => { - it('should not revoke role that does not exist', () => { - expect( - shieldedAccessControl._revokeRole( - UNINITIALIZED.roleId, - ADMIN.accountId, - ), - ).toBe(false); - }); - it('should not re-revoke role', () => { - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); - shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); - expect( - shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId), - ).toBe(false); - }); + describe('should return false', () => { + it('when caller does not have role', () => { + // setup test + shieldedAccessControl.privateState.injectSecretNonce( + OPERATOR_1.roleId, + OPERATOR_1.secretNonce, + ); + const accountId = buildAccountIdHash( + ADMIN.zPublicKey, + OPERATOR_1.secretNonce, + ); - it('should revoke role', () => { - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); - expect( - shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId), - ).toBe(true); - }); + // Check does not have OPERATOR role + expect( + shieldedAccessControl._validateRole(OPERATOR_1.roleId, accountId), + ).toBe(false); - it('should update nullifier set on revoke', () => { - const initialSetSize = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.size(); - expect(initialSetSize).toBe(0n); + expect(shieldedAccessControl.proveCallerRole(OPERATOR_1.roleId)).toBe( + false, + ); + }); - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); - const isRevoked = shieldedAccessControl._revokeRole( - ADMIN.roleId, - ADMIN.accountId, - ); - expect(isRevoked).toBe(true); + it('when caller has revoked role', () => { + shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); - const updatedSetSize = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.size(); - expect(updatedSetSize).toBe(1n); - expect(shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.member(ADMIN.roleNullifier)).toBe(true); - }); + // check role revoked + expect( + shieldedAccessControl._validateRole(ADMIN.roleId, ADMIN.accountId), + ).toBe(false); - it('should not update nullifier set on failed revoke', () => { - const initialSetSize = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.size(); - expect(initialSetSize).toBe(0n); + expect(shieldedAccessControl.proveCallerRole(ADMIN.roleId)).toBe( + false, + ); + }); - const isRevoked = shieldedAccessControl._revokeRole( - ADMIN.roleId, - ADMIN.accountId, - ); - expect(isRevoked).toBe(false); + it('when revoked role is re-granted', () => { + shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + // check role revoked + expect( + shieldedAccessControl._validateRole(ADMIN.roleId, ADMIN.accountId), + ).toBe(false); - const updatedSetSize = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.size(); - expect(updatedSetSize).toEqual(initialSetSize); - expect(shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.member(ADMIN.roleNullifier)).toBe(false) - }); - }); + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + expect(shieldedAccessControl.proveCallerRole(ADMIN.roleId)).toBe( + false, + ); + }); - describe('unverifiedCallerHasRole', () => { - beforeEach(() => { - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); - shieldedAccessControl.setPersistentCaller(ADMIN.publicKey); - }); + it('when an unauthorized caller has valid nonce', () => { + // UNAUTHORIZED uses the same private state (ADMIN.secretNonce for ADMIN.roleId), + // so their derived accountId won't match the committed one. + shieldedAccessControl.as(UNAUTHORIZED.publicKey); + expect(shieldedAccessControl.proveCallerRole(ADMIN.roleId)).toBe( + false, + ); + }); - it('should return true for caller with granted role', () => { - expect(shieldedAccessControl.unverifiedCallerHasRole(ADMIN.roleId)).toBe(true); - }); + it('when an authorized caller provides invalid nonce', () => { + // Check caller is admin, has admin role + expect( + shieldedAccessControl.getCallerContext().currentZswapLocalState + .coinPublicKey, + ).toEqual(ADMIN.zPublicKey); + expect( + shieldedAccessControl._validateRole(ADMIN.roleId, ADMIN.accountId), + ).toBe(true); - it('should return false for caller without role', () => { - // The witness requires a nonce entry for the queried roleId to exist in - // private state (the runtime cannot call the circuit without it). - // Inject a nonce that was never used to grant a role, so the derived - // accountId will not match any commitment in the tree. - shieldedAccessControl.privateState.injectSecretNonce( - OPERATOR_1.roleId, - OPERATOR_1.secretNonce, - ); - expect(shieldedAccessControl.unverifiedCallerHasRole(OPERATOR_1.roleId)).toBe( - false, - ); - }); + shieldedAccessControl.privateState.injectSecretNonce( + ADMIN.roleId, + BAD_INPUT.secretNonce, + ); + // nonce should not match + expect(ADMIN.secretNonce).not.toEqual( + shieldedAccessControl.privateState.getCurrentSecretNonce( + ADMIN.roleId, + ), + ); - it('should return false for caller with revoked role', () => { - shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); - expect(shieldedAccessControl.unverifiedCallerHasRole(ADMIN.roleId)).toBe(false); - }); + expect(shieldedAccessControl.proveCallerRole(ADMIN.roleId)).toBe( + false, + ); + }); - it('should return false for revoked role after re-grant attempt', () => { - shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); - expect(shieldedAccessControl.unverifiedCallerHasRole(ADMIN.roleId)).toBe(false); - }); + it('when an authorized caller provides invalid witness path', () => { + // Check caller is admin, has admin role + expect( + shieldedAccessControl.getCallerContext().currentZswapLocalState + .coinPublicKey, + ).toEqual(ADMIN.zPublicKey); + expect( + shieldedAccessControl._validateRole(ADMIN.roleId, ADMIN.accountId), + ).toBe(true); - it('should return false for a different caller sharing the same private state', () => { - // UNAUTHORIZED uses the same private state (ADMIN.secretNonce for ADMIN.roleId), - // so their derived accountId won't match the committed one. - shieldedAccessControl.setPersistentCaller(UNAUTHORIZED.publicKey); - expect(shieldedAccessControl.unverifiedCallerHasRole(ADMIN.roleId)).toBe(false); + shieldedAccessControl.overrideWitness( + 'wit_getRoleCommitmentPath', + RETURN_BAD_PATH, + ); + expect(shieldedAccessControl.proveCallerRole(ADMIN.roleId)).toBe( + false, + ); + }); }); }); @@ -944,169 +1935,85 @@ describe('ShieldedAccessControl', () => { }); describe('_setRoleAdmin', () => { - it('should set admin role retrievable by getRoleAdmin', () => { + it('should set admin role', () => { shieldedAccessControl._setRoleAdmin(OPERATOR_1.roleId, ADMIN.roleId); expect(shieldedAccessControl.getRoleAdmin(OPERATOR_1.roleId)).toEqual( new Uint8Array(ADMIN.roleId), ); }); - it('should override an existing admin role', () => { - shieldedAccessControl._setRoleAdmin(OPERATOR_1.roleId, ADMIN.roleId); - shieldedAccessControl._setRoleAdmin( - OPERATOR_1.roleId, - OPERATOR_2.roleId, - ); - expect(shieldedAccessControl.getRoleAdmin(OPERATOR_1.roleId)).toEqual( - new Uint8Array(OPERATOR_2.roleId), - ); - }); - }); - - describe('grantRole', () => { - beforeEach(() => { - // Give ADMIN the DEFAULT_ADMIN_ROLE (ADMIN.roleId === all-zero bytes === DEFAULT_ADMIN_ROLE). - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); - shieldedAccessControl.setPersistentCaller(ADMIN.publicKey); - }); - - it('should grant role when caller has the admin role', () => { - // DEFAULT_ADMIN_ROLE is admin of every role by default. - expect(() => - shieldedAccessControl.grantRole( - OPERATOR_1.roleId, - OPERATOR_1.accountId, - ), - ).not.toThrow(); + it('should update _adminRoles map', () => { expect( - shieldedAccessControl._checkRole( - OPERATOR_1.roleId, - OPERATOR_1.accountId, - ).observedHasRole, + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__adminRoles.isEmpty(), ).toBe(true); - }); - it('should fail when caller does not have the admin role', () => { - shieldedAccessControl.setPersistentCaller(UNAUTHORIZED.publicKey); - expect(() => - shieldedAccessControl.grantRole( - OPERATOR_1.roleId, - OPERATOR_1.accountId, - ), - ).toThrow('ShieldedAccessControl: unauthorized account'); - }); + // setup test + shieldedAccessControl._setRoleAdmin(OPERATOR_1.roleId, ADMIN.roleId); + shieldedAccessControl._setRoleAdmin(OPERATOR_2.roleId, ADMIN.roleId); + shieldedAccessControl._setRoleAdmin(OPERATOR_3.roleId, ADMIN.roleId); - it('should not re-grant role', () => { - shieldedAccessControl.grantRole( - OPERATOR_1.roleId, - OPERATOR_1.accountId, - ); - const treeRoot = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.root(); - expect(() => - shieldedAccessControl.grantRole( - OPERATOR_1.roleId, - OPERATOR_1.accountId, - ), - ).not.toThrow(); + // check updated state expect( shieldedAccessControl .getPublicState() - .ShieldedAccessControl__operatorRoles.root(), - ).toEqual(treeRoot); - }); - - it('should grant role using a custom admin role', () => { - // Make OPERATOR_1.roleId the admin of OPERATOR_2.roleId. - shieldedAccessControl._setRoleAdmin( - OPERATOR_2.roleId, - OPERATOR_1.roleId, - ); - // Grant OPERATOR_1.roleId to OPERATOR_1 (ADMIN has DEFAULT_ADMIN_ROLE - // which is the admin of OPERATOR_1.roleId by default). - shieldedAccessControl.grantRole( - OPERATOR_1.roleId, - OPERATOR_1.accountId, - ); - - // Switch to OPERATOR_1 as caller and inject their nonce for their role. - shieldedAccessControl.privateState.injectSecretNonce( - OPERATOR_1.roleId, - OPERATOR_1.secretNonce, - ); - shieldedAccessControl.setPersistentCaller(OPERATOR_1.publicKey); + .ShieldedAccessControl__adminRoles.isEmpty(), + ).toBe(false); + expect( + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__adminRoles.size(), + ).toBe(3n); - // OPERATOR_1 (who holds OPERATOR_1.roleId) can now grant OPERATOR_2.roleId. - expect(() => - shieldedAccessControl.grantRole( - OPERATOR_2.roleId, - OPERATOR_2.accountId, - ), - ).not.toThrow(); + // check new values exist expect( - shieldedAccessControl._checkRole( - OPERATOR_2.roleId, - OPERATOR_2.accountId, - ).observedHasRole, + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__adminRoles.member(OPERATOR_1.roleId), ).toBe(true); - }); - }); + expect( + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__adminRoles.lookup(OPERATOR_1.roleId), + ).toEqual(new Uint8Array(ADMIN.roleId)); - describe('revokeRole', () => { - beforeEach(() => { - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); - shieldedAccessControl._grantRole( - OPERATOR_1.roleId, - OPERATOR_1.accountId, - ); - shieldedAccessControl.setPersistentCaller(ADMIN.publicKey); - }); + expect( + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__adminRoles.member(OPERATOR_2.roleId), + ).toBe(true); + expect( + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__adminRoles.lookup(OPERATOR_2.roleId), + ).toEqual(new Uint8Array(ADMIN.roleId)); - it('should revoke role when caller has the admin role', () => { - expect(() => - shieldedAccessControl.revokeRole( - OPERATOR_1.roleId, - OPERATOR_1.accountId, - ), - ).not.toThrow(); expect( - shieldedAccessControl._checkRole( - OPERATOR_1.roleId, - OPERATOR_1.accountId, - ).observedHasRole, - ).toBe(false); + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__adminRoles.member(OPERATOR_3.roleId), + ).toBe(true); + expect( + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__adminRoles.lookup(OPERATOR_3.roleId), + ).toEqual(new Uint8Array(ADMIN.roleId)); }); - it('should fail when caller does not have the admin role', () => { - shieldedAccessControl.setPersistentCaller(UNAUTHORIZED.publicKey); - expect(() => - shieldedAccessControl.revokeRole( - OPERATOR_1.roleId, - OPERATOR_1.accountId, - ), - ).toThrow('ShieldedAccessControl: unauthorized account'); - }); + it('should override an existing admin role', () => { + shieldedAccessControl._setRoleAdmin(OPERATOR_1.roleId, ADMIN.roleId); + expect(shieldedAccessControl.getRoleAdmin(OPERATOR_1.roleId)).toEqual( + new Uint8Array(ADMIN.roleId), + ); - it('should not re-revoke role', () => { - shieldedAccessControl.revokeRole( + shieldedAccessControl._setRoleAdmin( OPERATOR_1.roleId, - OPERATOR_1.accountId, + OPERATOR_2.roleId, + ); + expect(shieldedAccessControl.getRoleAdmin(OPERATOR_1.roleId)).toEqual( + new Uint8Array(OPERATOR_2.roleId), ); - const nullifierSetSize = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.size(); - expect(() => - shieldedAccessControl.revokeRole( - OPERATOR_1.roleId, - OPERATOR_1.accountId, - ), - ).not.toThrow(); - expect( - shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.size(), - ).toEqual(nullifierSetSize); }); }); @@ -1121,8 +2028,7 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl.renounceRole(ADMIN.roleId, ADMIN.accountId), ).not.toThrow(); expect( - shieldedAccessControl._checkRole(ADMIN.roleId, ADMIN.accountId) - .observedHasRole, + shieldedAccessControl._validateRole(ADMIN.roleId, ADMIN.accountId), ).toBe(false); }); @@ -1143,8 +2049,7 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl.renounceRole(ADMIN.roleId, ADMIN.accountId), ).not.toThrow(); expect( - shieldedAccessControl._checkRole(ADMIN.roleId, ADMIN.accountId) - .observedHasRole, + shieldedAccessControl._validateRole(ADMIN.roleId, ADMIN.accountId), ).toBe(false); }); @@ -1158,9 +2063,13 @@ describe('ShieldedAccessControl', () => { .getPublicState() .ShieldedAccessControl__roleCommitmentNullifiers.size(); expect(updatedSetSize).toEqual(1n); - expect(shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.member(ADMIN.roleNullifier)) + expect( + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.member( + ADMIN.roleNullifier, + ), + ); }); }); }); From 475709604bfadd7b815f1ed62ffb964cbd3b8eb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Wed, 11 Mar 2026 19:40:05 -0400 Subject: [PATCH 265/322] fmt file --- .../access/test/ShieldedAccessControl.test.ts | 507 ++++++++++++++++-- 1 file changed, 458 insertions(+), 49 deletions(-) diff --git a/contracts/src/access/test/ShieldedAccessControl.test.ts b/contracts/src/access/test/ShieldedAccessControl.test.ts index 89373b1c..a3a80030 100644 --- a/contracts/src/access/test/ShieldedAccessControl.test.ts +++ b/contracts/src/access/test/ShieldedAccessControl.test.ts @@ -833,20 +833,91 @@ describe('ShieldedAccessControl', () => { ); }); - it.todo('when role is revoked and re-issued to the same accountId'); - it.todo('when role is revoked'); - it.todo('when non-admin caller has role'); - it.todo('when admin provides incorrect nonce'); - it.todo('when admin provides bad witness path'); + it('when admin with duplicate roles is revoked', () => { + // create duplicate roles + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + + shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + expect(() => + shieldedAccessControl.grantRole(ADMIN.roleId, ADMIN.accountId), + ).toThrow('ShieldedAccessControl: unauthorized account'); + }); + + it('when admin role is revoked', () => { + shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + expect(() => + shieldedAccessControl.grantRole(ADMIN.roleId, ADMIN.accountId), + ).toThrow('ShieldedAccessControl: unauthorized account'); + }); + + it('when admin provides incorrect nonce', () => { + shieldedAccessControl.privateState.injectSecretNonce( + ADMIN.roleId, + BAD_INPUT.secretNonce, + ); + expect( + shieldedAccessControl.privateState.getCurrentSecretNonce( + ADMIN.roleId, + ), + ).not.toEqual(ADMIN.secretNonce); + expect(() => + shieldedAccessControl.grantRole(ADMIN.roleId, ADMIN.accountId), + ).toThrow('ShieldedAccessControl: unauthorized account'); + }); + + it('when admin provides bad witness path', () => { + shieldedAccessControl.overrideWitness( + 'wit_getRoleCommitmentPath', + RETURN_BAD_PATH, + ); + expect(() => + shieldedAccessControl.grantRole(ADMIN.roleId, ADMIN.accountId), + ).toThrow('ShieldedAccessControl: unauthorized account'); + }); + + it('when non-admin caller has role', () => { + shieldedAccessControl._grantRole( + OPERATOR_1.roleId, + OPERATOR_1.accountId, + ); + + shieldedAccessControl.as(OPERATOR_1.publicKey); + // OP_1 has role but is not authorized to grant roles to other users + expect(() => + shieldedAccessControl.grantRole( + OPERATOR_1.roleId, + OPERATOR_2.accountId, + ), + ).toThrow('ShieldedAccessControl: unauthorized account'); + }); }); describe('should not update _operatorRoles Merkle tree', () => { - it.todo('when re-granting revoked role', () => {}); - it.todo('when role is revoked and re-issued to the same accountId'); - it.todo('when role is revoked'); - it.todo('when non-admin caller has role'); - it.todo('when admin provides incorrect nonce'); - it.todo('when admin provides bad witness path'); + it('when role is revoked', () => { + // setup test + shieldedAccessControl._grantRole( + OPERATOR_1.roleId, + OPERATOR_1.accountId, + ); + shieldedAccessControl._revokeRole( + OPERATOR_1.roleId, + OPERATOR_1.accountId, + ); + + const initialRoot = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.root(); + shieldedAccessControl.grantRole( + OPERATOR_1.roleId, + OPERATOR_1.accountId, + ); + + const updatedRoot = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.root(); + expect(initialRoot).toEqual(updatedRoot); + }); }); describe('should grant role', () => { @@ -899,28 +970,136 @@ describe('ShieldedAccessControl', () => { ).toBe(true); }); - it.todo( - 'when admin role is revoked and re-issued with a different accountId', - ); + it('when admin role is revoked and re-issued with a different accountId', () => { + // setup test + shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + const newNonce = Buffer.alloc(32, 'NEW_ADMIN_NONCE'); + shieldedAccessControl.privateState.injectSecretNonce( + ADMIN.roleId, + newNonce, + ); + const newAccountId = buildAccountIdHash(ADMIN.zPublicKey, newNonce); + shieldedAccessControl._grantRole(ADMIN.roleId, newAccountId); - it.todo('when multiple admins of the same role exist'); - it.todo('when admin has multiple roles'); - it.todo('when re-granting active role'); - it.todo('when granting role that does not exist'); - it.todo('when granting role with bad accountId'); - }); + expect(() => { + shieldedAccessControl.grantRole( + OPERATOR_1.roleId, + OPERATOR_1.accountId, + ); + }).not.toThrow(); + expect( + shieldedAccessControl._validateRole( + OPERATOR_1.roleId, + OPERATOR_1.accountId, + ), + ).toBe(true); + expect( + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf( + OPERATOR_1.roleCommitment, + ), + ).toBeDefined(); + }); - describe('should update _operatorRoles Merkle tree', () => { - it.todo( - 'when admin role is revoked and re-issued with a different accountId', - ); - it.todo('when caller has admin role'); - it.todo('when caller has custom admin role'); - it.todo('when multiple admins of the same role exist'); - it.todo('when admin has multiple roles'); - it.todo('when re-granting active role'); - it.todo('when granting role that does not exist'); - it.todo('when granting role with bad accountId'); + it('when multiple admins of the same role exist', () => { + // setup test + const account1 = buildAccountIdHash( + OPERATOR_1.zPublicKey, + ADMIN.secretNonce, + ); + const account2 = buildAccountIdHash( + OPERATOR_2.zPublicKey, + ADMIN.secretNonce, + ); + const account3 = buildAccountIdHash( + OPERATOR_3.zPublicKey, + ADMIN.secretNonce, + ); + shieldedAccessControl._grantRole(ADMIN.roleId, account1); + shieldedAccessControl._grantRole(ADMIN.roleId, account2); + shieldedAccessControl._grantRole(ADMIN.roleId, account3); + + // check grant role succeeds as OP and role is valid + shieldedAccessControl.as(OPERATOR_1.publicKey); + expect(() => + shieldedAccessControl.grantRole(ADMIN.roleId, account1), + ).not.toThrow(); + expect( + shieldedAccessControl._validateRole(ADMIN.roleId, account1), + ).toBe(true); + + shieldedAccessControl.as(OPERATOR_2.publicKey); + expect(() => + shieldedAccessControl.grantRole(ADMIN.roleId, ADMIN.accountId), + ).not.toThrow(); + expect( + shieldedAccessControl._validateRole(ADMIN.roleId, account2), + ).toBe(true); + + shieldedAccessControl.as(OPERATOR_3.publicKey); + expect(() => + shieldedAccessControl.grantRole(ADMIN.roleId, ADMIN.accountId), + ).not.toThrow(); + expect( + shieldedAccessControl._validateRole(ADMIN.roleId, account3), + ).toBe(true); + }); + + it('when admin has multiple roles', () => { + shieldedAccessControl._grantRole(OPERATOR_1.roleId, ADMIN.accountId); + shieldedAccessControl._grantRole(OPERATOR_2.roleId, ADMIN.accountId); + shieldedAccessControl._grantRole(OPERATOR_3.roleId, ADMIN.accountId); + + expect(() => + shieldedAccessControl.grantRole( + OPERATOR_1.roleId, + OPERATOR_1.accountId, + ), + ).not.toThrow(); + expect( + shieldedAccessControl._validateRole( + OPERATOR_1.roleId, + OPERATOR_1.accountId, + ), + ).toBe(true); + }); + + it('when re-granting active role', () => { + expect(() => + shieldedAccessControl.grantRole(ADMIN.roleId, ADMIN.accountId), + ).not.toThrow(); + expect( + shieldedAccessControl._validateRole(ADMIN.roleId, ADMIN.accountId), + ).toBe(true); + }); + + it('when granting role that does not exist', () => { + expect(() => + shieldedAccessControl.grantRole( + UNINITIALIZED.roleId, + UNINITIALIZED.accountId, + ), + ).not.toThrow(); + expect( + shieldedAccessControl._validateRole( + UNINITIALIZED.roleId, + UNINITIALIZED.accountId, + ), + ).toBe(true); + }); + + it('when granting role with bad accountId', () => { + expect(() => + shieldedAccessControl.grantRole(ADMIN.roleId, BAD_INPUT.accountId), + ).not.toThrow(); + expect( + shieldedAccessControl._validateRole( + ADMIN.roleId, + BAD_INPUT.accountId, + ), + ).toBe(true); + }); }); }); @@ -1149,11 +1328,89 @@ describe('ShieldedAccessControl', () => { ), ).toThrow('ShieldedAccessControl: unauthorized account'); }); - it.todo('when admin role is revoked from caller'); - it.todo('when caller is admin of a different role'); - it.todo('when admin provides invalid Merkle tree path'); - it('when authorized caller provides bad nonce', () => { + it('when wit_getRoleCommitmentPath returns a valid path for a different roleId, accountId pairing', () => { + shieldedAccessControl._grantRole( + OPERATOR_1.roleId, + OPERATOR_1.accountId, + ); + // Override witness to return valid path for OPERATOR_1 role commitment + shieldedAccessControl.overrideWitness( + 'wit_getRoleCommitmentPath', + () => { + const privateState = shieldedAccessControl.getPrivateState(); + const operator1MtPath = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf( + OPERATOR_1.roleCommitment, + ); + if (operator1MtPath) return [privateState, operator1MtPath]; + throw new Error('Merkle tree path should be defined'); + }, + ); + expect(() => { + shieldedAccessControl.revokeRole(ADMIN.roleId, ADMIN.accountId); + }).toThrow( + 'ShieldedAccessControl: Path must contain leaf matching computed role commitment for the provided role, accountId pairing', + ); + }); + + it('when admin with duplicate roles is revoked', () => { + // create duplicate roles + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + + shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + expect(() => + shieldedAccessControl.revokeRole(ADMIN.roleId, ADMIN.accountId), + ).toThrow('ShieldedAccessControl: unauthorized account'); + }); + + it('when admin role is revoked', () => { + shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + expect(() => + shieldedAccessControl.grantRole(ADMIN.roleId, ADMIN.accountId), + ).toThrow('ShieldedAccessControl: unauthorized account'); + }); + + it('when admin provides bad witness path', () => { + shieldedAccessControl.overrideWitness( + 'wit_getRoleCommitmentPath', + RETURN_BAD_PATH, + ); + expect(() => + shieldedAccessControl.revokeRole(ADMIN.roleId, ADMIN.accountId), + ).toThrow('ShieldedAccessControl: unauthorized account'); + }); + + it('when non-admin caller has role', () => { + shieldedAccessControl._grantRole( + OPERATOR_1.roleId, + OPERATOR_1.accountId, + ); + + shieldedAccessControl.as(OPERATOR_1.publicKey); + // OP_1 has role but is not authorized to grant roles to other users + expect(() => + shieldedAccessControl.revokeRole( + OPERATOR_1.roleId, + OPERATOR_2.accountId, + ), + ).toThrow('ShieldedAccessControl: unauthorized account'); + }); + + it('when caller is admin of a different role', () => { + shieldedAccessControl._setRoleAdmin( + OPERATOR_1.roleId, + OPERATOR_1.accountId, + ); + shieldedAccessControl.as(OPERATOR_1.publicKey); + expect(() => + shieldedAccessControl.revokeRole(ADMIN.roleId, ADMIN.accountId), + ).toThrow('ShieldedAccessControl: unauthorized account'); + }); + + it('when admin provides bad nonce', () => { shieldedAccessControl.privateState.injectSecretNonce( ADMIN.roleId, BAD_INPUT.secretNonce, @@ -1275,19 +1532,108 @@ describe('ShieldedAccessControl', () => { ).toBe(false); }); - it.todo('when multiple admins of the same role exist'); - it.todo('when admin has multiple roles'); - it.todo( - 'when admin role is revoked and re-issued with a different accountId', - ); - }); + it('when multiple admins of the same role exist', () => { + // setup test + const account1 = buildAccountIdHash( + OPERATOR_1.zPublicKey, + ADMIN.secretNonce, + ); + const account2 = buildAccountIdHash( + OPERATOR_2.zPublicKey, + ADMIN.secretNonce, + ); + const account3 = buildAccountIdHash( + OPERATOR_3.zPublicKey, + ADMIN.secretNonce, + ); + shieldedAccessControl._grantRole(ADMIN.roleId, account1); + shieldedAccessControl._grantRole(ADMIN.roleId, account2); + shieldedAccessControl._grantRole(ADMIN.roleId, account3); + + // check revoke role succeeds as OP and role is valid + shieldedAccessControl.as(OPERATOR_1.publicKey); + expect(() => + shieldedAccessControl.revokeRole(ADMIN.roleId, account1), + ).not.toThrow(); + expect( + shieldedAccessControl._validateRole(ADMIN.roleId, account1), + ).toBe(false); + + shieldedAccessControl.as(OPERATOR_2.publicKey); + expect(() => + shieldedAccessControl.revokeRole(ADMIN.roleId, account2), + ).not.toThrow(); + expect( + shieldedAccessControl._validateRole(ADMIN.roleId, account2), + ).toBe(false); + + shieldedAccessControl.as(OPERATOR_3.publicKey); + expect(() => + shieldedAccessControl.revokeRole(ADMIN.roleId, account3), + ).not.toThrow(); + expect( + shieldedAccessControl._validateRole(ADMIN.roleId, account3), + ).toBe(false); + }); + + it('when admin has multiple roles', () => { + shieldedAccessControl._grantRole(OPERATOR_1.roleId, ADMIN.accountId); + shieldedAccessControl._grantRole(OPERATOR_2.roleId, ADMIN.accountId); + shieldedAccessControl._grantRole(OPERATOR_3.roleId, ADMIN.accountId); + + expect(() => + shieldedAccessControl.revokeRole( + OPERATOR_1.roleId, + OPERATOR_1.accountId, + ), + ).not.toThrow(); + expect( + shieldedAccessControl._validateRole( + OPERATOR_1.roleId, + OPERATOR_1.accountId, + ), + ).toBe(false); + }); + + it('when revoking role that does not exist', () => { + expect(() => + shieldedAccessControl.revokeRole( + UNINITIALIZED.roleId, + UNINITIALIZED.accountId, + ), + ).not.toThrow(); + expect( + shieldedAccessControl._validateRole( + UNINITIALIZED.roleId, + UNINITIALIZED.accountId, + ), + ).toBe(false); + }); + + it('when admin role is revoked and re-issued with a different accountId', () => { + // setup test + shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + const newNonce = Buffer.alloc(32, 'NEW_ADMIN_NONCE'); + shieldedAccessControl.privateState.injectSecretNonce( + ADMIN.roleId, + newNonce, + ); + const newAccountId = buildAccountIdHash(ADMIN.zPublicKey, newNonce); + shieldedAccessControl._grantRole(ADMIN.roleId, newAccountId); - describe('should update _roleCommitmentNullifiers set', () => { - it.todo('when caller has admin role'); - it.todo('when caller has custom admin role'); - it.todo('when role does not exist'); - it.todo('when multiple admins of the same role exist'); - it.todo('when admin has multiple roles'); + expect(() => { + shieldedAccessControl.revokeRole( + OPERATOR_1.roleId, + OPERATOR_1.accountId, + ); + }).not.toThrow(); + expect( + shieldedAccessControl._validateRole( + OPERATOR_1.roleId, + OPERATOR_1.accountId, + ), + ).toBe(false); + }); }); }); @@ -2032,12 +2378,74 @@ describe('ShieldedAccessControl', () => { ).toBe(false); }); - it('should fail with wrong accountId confirmation', () => { + it('should allow caller to renounce role that does not exist', () => { + // Set ADMIN.secretNonce for UNINITIALIZED role so circuit computes ADMIN.accountId + shieldedAccessControl.privateState.injectSecretNonce( + UNINITIALIZED.roleId, + ADMIN.secretNonce, + ); expect(() => shieldedAccessControl.renounceRole( + UNINITIALIZED.roleId, + ADMIN.accountId, + ), + ).not.toThrow(); + expect( + shieldedAccessControl._validateRole( + UNINITIALIZED.roleId, + ADMIN.accountId, + ), + ).toBe(false); + }); + + it('should allow caller to renounce a role they do not have', () => { + // Set ADMIN.secretNonce for OPERATOR_1 role so circuit computes ADMIN.accountId + shieldedAccessControl.privateState.injectSecretNonce( + OPERATOR_1.roleId, + ADMIN.secretNonce, + ); + expect(() => + shieldedAccessControl.renounceRole( + OPERATOR_1.roleId, + ADMIN.accountId, + ), + ).not.toThrow(); + expect( + shieldedAccessControl._validateRole( + OPERATOR_1.roleId, + ADMIN.accountId, + ), + ).toBe(false); + }); + + it('should fail when caller provides bad nonce', () => { + shieldedAccessControl.privateState.injectSecretNonce( + ADMIN.roleId, + BAD_INPUT.secretNonce, + ); + + expect(() => + shieldedAccessControl.renounceRole(ADMIN.roleId, ADMIN.accountId), + ).toThrow('ShieldedAccessControl: bad confirmation'); + }); + + it('should fail when caller provides bad accountId', () => { + expect(() => + shieldedAccessControl.renounceRole(ADMIN.roleId, BAD_INPUT.accountId), + ).toThrow('ShieldedAccessControl: bad confirmation'); + }); + + it('should fail when unauthorized caller provides valid nonce, and accountId', () => { + // check we have valid secret nonce in private state + expect( + shieldedAccessControl.privateState.getCurrentSecretNonce( ADMIN.roleId, - OPERATOR_1.accountId, ), + ).toEqual(ADMIN.secretNonce); + + shieldedAccessControl.as(UNAUTHORIZED.publicKey); + expect(() => + shieldedAccessControl.renounceRole(ADMIN.roleId, ADMIN.accountId), ).toThrow('ShieldedAccessControl: bad confirmation'); }); @@ -2058,6 +2466,7 @@ describe('ShieldedAccessControl', () => { .getPublicState() .ShieldedAccessControl__roleCommitmentNullifiers.size(); expect(nullifierSetSize).toBe(0n); + shieldedAccessControl.renounceRole(ADMIN.roleId, ADMIN.accountId); const updatedSetSize = shieldedAccessControl .getPublicState() From 187f23f192d99f9ae102aae1a2ff819891b0a6e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Wed, 11 Mar 2026 20:28:31 -0400 Subject: [PATCH 266/322] add test --- .../access/test/ShieldedAccessControl.test.ts | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/contracts/src/access/test/ShieldedAccessControl.test.ts b/contracts/src/access/test/ShieldedAccessControl.test.ts index a3a80030..68c70c2f 100644 --- a/contracts/src/access/test/ShieldedAccessControl.test.ts +++ b/contracts/src/access/test/ShieldedAccessControl.test.ts @@ -746,6 +746,29 @@ describe('ShieldedAccessControl', () => { ).not.toThrow(); }); + it('when role is revoked and re-issued with a different accountId', () => { + shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + + shieldedAccessControl.privateState.injectSecretNonce( + ADMIN.roleId, + Buffer.alloc(32, 'NEW_ADMIN_NONCE'), + ); + const newAdminAccountId = buildAccountIdHash( + ADMIN.zPublicKey, + shieldedAccessControl.privateState.getCurrentSecretNonce( + ADMIN.roleId, + ), + ); + expect(newAdminAccountId).not.toEqual(ADMIN.accountId); + + shieldedAccessControl._grantRole(ADMIN.roleId, newAdminAccountId); + expect(() => + shieldedAccessControl.assertOnlyRole( + ADMIN.roleId, + ) + ).not.toThrow(); + }); + it('when multiple users have the same role', () => { // All users will use OPERATOR_1.secretNonce as their nonce value // when generating their accountId for simplicity From 593bcc080536ebd655bf007ed3160f76c01c1799 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Wed, 11 Mar 2026 20:30:51 -0400 Subject: [PATCH 267/322] refactor files: roleId -> role --- .../access/test/ShieldedAccessControl.test.ts | 770 +++++++++--------- .../ShieldedAccessControlSimulator.ts | 54 +- .../ShieldedAccessControlWitnesses.ts | 20 +- 3 files changed, 422 insertions(+), 422 deletions(-) diff --git a/contracts/src/access/test/ShieldedAccessControl.test.ts b/contracts/src/access/test/ShieldedAccessControl.test.ts index 68c70c2f..e90a8712 100644 --- a/contracts/src/access/test/ShieldedAccessControl.test.ts +++ b/contracts/src/access/test/ShieldedAccessControl.test.ts @@ -48,14 +48,14 @@ const buildAccountIdHash = ( }; const buildRoleCommitmentHash = ( - roleId: Uint8Array, + role: Uint8Array, accountId: Uint8Array, ): Uint8Array => { const rt_type = new CompactTypeVector(4, new CompactTypeBytes(32)); const bDomain = new TextEncoder().encode(COMMITMENT_DOMAIN); const commitment = persistentHash(rt_type, [ - roleId, + role, accountId, INSTANCE_SALT, bDomain, @@ -75,7 +75,7 @@ class ShieldedAccessControlConstant { baseString: string; publicKey: string; zPublicKey: ZswapCoinPublicKey; - roleId: Buffer; + role: Buffer; accountId: Uint8Array; roleNullifier: Uint8Array; roleCommitment: Uint8Array; @@ -86,8 +86,8 @@ class ShieldedAccessControlConstant { [this.publicKey, this.zPublicKey] = utils.generatePubKeyPair(baseString); this.secretNonce = Buffer.alloc(32, `${baseString}_NONCE`); this.accountId = buildAccountIdHash(this.zPublicKey, this.secretNonce); - this.roleId = Buffer.from(convertFieldToBytes(32, roleIdentifier, '')); - this.roleCommitment = buildRoleCommitmentHash(this.roleId, this.accountId); + this.role = Buffer.from(convertFieldToBytes(32, roleIdentifier, '')); + this.roleCommitment = buildRoleCommitmentHash(this.role, this.accountId); this.roleNullifier = buildNullifierHash(this.roleCommitment); } } @@ -122,19 +122,19 @@ describe('ShieldedAccessControl', () => { ]; // Circuit calls should fail before the args are used const circuitsToFail: FailingCircuits[] = [ - ['proveCallerRole', [UNINITIALIZED.roleId]], - ['assertOnlyRole', [UNINITIALIZED.roleId]], - ['_validateRole', [UNINITIALIZED.roleId, UNINITIALIZED.accountId]], - ['getRoleAdmin', [UNINITIALIZED.roleId]], - ['grantRole', [UNINITIALIZED.roleId, UNINITIALIZED.accountId]], - ['revokeRole', [UNINITIALIZED.roleId, UNINITIALIZED.accountId]], - ['renounceRole', [UNINITIALIZED.roleId, UNINITIALIZED.accountId]], - ['_setRoleAdmin', [UNINITIALIZED.roleId, UNINITIALIZED.roleId]], - ['_grantRole', [UNINITIALIZED.roleId, UNINITIALIZED.accountId]], - ['_revokeRole', [UNINITIALIZED.roleId, UNINITIALIZED.accountId]], + ['proveCallerRole', [UNINITIALIZED.role]], + ['assertOnlyRole', [UNINITIALIZED.role]], + ['_validateRole', [UNINITIALIZED.role, UNINITIALIZED.accountId]], + ['getRoleAdmin', [UNINITIALIZED.role]], + ['grantRole', [UNINITIALIZED.role, UNINITIALIZED.accountId]], + ['revokeRole', [UNINITIALIZED.role, UNINITIALIZED.accountId]], + ['renounceRole', [UNINITIALIZED.role, UNINITIALIZED.accountId]], + ['_setRoleAdmin', [UNINITIALIZED.role, UNINITIALIZED.role]], + ['_grantRole', [UNINITIALIZED.role, UNINITIALIZED.accountId]], + ['_revokeRole', [UNINITIALIZED.role, UNINITIALIZED.accountId]], [ '_computeRoleCommitment', - [UNINITIALIZED.roleId, UNINITIALIZED.accountId], + [UNINITIALIZED.role, UNINITIALIZED.accountId], ], [ '_computeAccountId', @@ -169,7 +169,7 @@ describe('ShieldedAccessControl', () => { beforeEach(() => { // Create private state object and generate nonce const PS = ShieldedAccessControlPrivateState.withRoleAndNonce( - ADMIN.roleId, + ADMIN.role, ADMIN.secretNonce, ); // Create contract simulator with PS @@ -186,7 +186,7 @@ describe('ShieldedAccessControl', () => { it('should match computed commitment', () => { expect( shieldedAccessControl._computeRoleCommitment( - ADMIN.roleId, + ADMIN.role, ADMIN.accountId, ), ).toEqual(ADMIN.roleCommitment); @@ -199,9 +199,9 @@ describe('ShieldedAccessControl', () => { ]; const checkedCircuits: ComputeCommitmentCases[] = [ - [false, true, [BAD_INPUT.roleId, ADMIN.accountId]], - [true, false, [ADMIN.roleId, BAD_INPUT.accountId]], - [false, false, [BAD_INPUT.roleId, BAD_INPUT.accountId]], + [false, true, [BAD_INPUT.role, ADMIN.accountId]], + [true, false, [ADMIN.role, BAD_INPUT.accountId]], + [false, false, [BAD_INPUT.role, BAD_INPUT.accountId]], ]; it.each( @@ -270,13 +270,13 @@ describe('ShieldedAccessControl', () => { describe('_validateRole', () => { beforeEach(() => { - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); shieldedAccessControl.setPersistentCaller(ADMIN.publicKey); }); - it('should fail when wit_getRoleCommitmentPath returns a valid path for a different roleId, accountId pairing', () => { + it('should fail when wit_getRoleCommitmentPath returns a valid path for a different role, accountId pairing', () => { shieldedAccessControl._grantRole( - OPERATOR_1.roleId, + OPERATOR_1.role, OPERATOR_1.accountId, ); // Override witness to return valid path for OPERATOR_1 role commitment @@ -294,7 +294,7 @@ describe('ShieldedAccessControl', () => { }, ); expect(() => { - shieldedAccessControl._validateRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId); }).toThrow( 'ShieldedAccessControl: Path must contain leaf matching computed role commitment for the provided role, accountId pairing', ); @@ -307,9 +307,9 @@ describe('ShieldedAccessControl', () => { args: unknown[], ]; const checkedCircuits: CheckRoleCases[] = [ - [false, true, [ADMIN.roleId, BAD_INPUT.accountId]], - [true, false, [BAD_INPUT.roleId, ADMIN.accountId]], - [false, false, [BAD_INPUT.roleId, BAD_INPUT.accountId]], + [false, true, [ADMIN.role, BAD_INPUT.accountId]], + [true, false, [BAD_INPUT.role, ADMIN.accountId]], + [false, false, [BAD_INPUT.role, BAD_INPUT.accountId]], ]; it.each( @@ -328,24 +328,24 @@ describe('ShieldedAccessControl', () => { it('when role does not exist', () => { expect( shieldedAccessControl._validateRole( - UNINITIALIZED.roleId, + UNINITIALIZED.role, ADMIN.accountId, ), ).toBe(false); }); it('when revoked role is re-issued to the same accountId', () => { - shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); + shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); expect( - shieldedAccessControl._validateRole(ADMIN.roleId, ADMIN.accountId), + shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), ).toBe(false); }); it('when role is revoked, ', () => { - shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); const roleCheck = shieldedAccessControl._validateRole( - ADMIN.roleId, + ADMIN.role, ADMIN.accountId, ); expect(roleCheck).toBe(false); @@ -357,20 +357,20 @@ describe('ShieldedAccessControl', () => { RETURN_BAD_PATH, ); expect( - shieldedAccessControl._validateRole(ADMIN.roleId, ADMIN.accountId), + shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), ).toBe(false); }); // an invalid witness should not violate the security invariant: revoked roles // are permanent it('when an invalid witness is provided for a revoked role', () => { - shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); shieldedAccessControl.overrideWitness( 'wit_getRoleCommitmentPath', RETURN_BAD_PATH, ); expect( - shieldedAccessControl._validateRole(ADMIN.roleId, ADMIN.accountId), + shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), ).toBe(false); }); }); @@ -378,57 +378,57 @@ describe('ShieldedAccessControl', () => { describe('should return true', () => { it('when role is granted', () => { expect( - shieldedAccessControl._validateRole(ADMIN.roleId, ADMIN.accountId), + shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), ).toBe(true); }); it('when accountId has multiple roles', () => { - shieldedAccessControl._grantRole(OPERATOR_1.roleId, ADMIN.accountId); - shieldedAccessControl._grantRole(OPERATOR_2.roleId, ADMIN.accountId); - shieldedAccessControl._grantRole(OPERATOR_3.roleId, ADMIN.accountId); + shieldedAccessControl._grantRole(OPERATOR_1.role, ADMIN.accountId); + shieldedAccessControl._grantRole(OPERATOR_2.role, ADMIN.accountId); + shieldedAccessControl._grantRole(OPERATOR_3.role, ADMIN.accountId); expect( - shieldedAccessControl._validateRole(ADMIN.roleId, ADMIN.accountId), + shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), ).toBe(true); expect( shieldedAccessControl._validateRole( - OPERATOR_1.roleId, + OPERATOR_1.role, ADMIN.accountId, ), ).toBe(true); expect( shieldedAccessControl._validateRole( - OPERATOR_2.roleId, + OPERATOR_2.role, ADMIN.accountId, ), ).toBe(true); expect( shieldedAccessControl._validateRole( - OPERATOR_3.roleId, + OPERATOR_3.role, ADMIN.accountId, ), ).toBe(true); }); it('when role is revoked and re-issued with a different accountId', () => { - shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); shieldedAccessControl.privateState.injectSecretNonce( - ADMIN.roleId, + ADMIN.role, Buffer.alloc(32, 'NEW_ADMIN_NONCE'), ); const newAdminAccountId = buildAccountIdHash( ADMIN.zPublicKey, shieldedAccessControl.privateState.getCurrentSecretNonce( - ADMIN.roleId, + ADMIN.role, ), ); expect(newAdminAccountId).not.toEqual(ADMIN.accountId); - shieldedAccessControl._grantRole(ADMIN.roleId, newAdminAccountId); + shieldedAccessControl._grantRole(ADMIN.role, newAdminAccountId); expect( shieldedAccessControl._validateRole( - ADMIN.roleId, + ADMIN.role, newAdminAccountId, ), ).toBe(true); @@ -438,7 +438,7 @@ describe('ShieldedAccessControl', () => { // All users will use OPERATOR_1.secretNonce as their nonce value // when generating their accountId for simplicity shieldedAccessControl.privateState.injectSecretNonce( - OPERATOR_1.roleId, + OPERATOR_1.role, OPERATOR_1.secretNonce, ); // A unique accountId must be constructed for each new role using its associated secretNonce @@ -447,13 +447,13 @@ describe('ShieldedAccessControl', () => { OPERATOR_1.secretNonce, ); shieldedAccessControl._grantRole( - OPERATOR_1.roleId, + OPERATOR_1.role, operator1AdminAccountId, ); - shieldedAccessControl.as(ADMIN.publicKey); // assert ADMIN has OP_1 roleId + shieldedAccessControl.as(ADMIN.publicKey); // assert ADMIN has OP_1 role expect( shieldedAccessControl._validateRole( - OPERATOR_1.roleId, + OPERATOR_1.role, operator1AdminAccountId, ), ).toBe(true); @@ -463,13 +463,13 @@ describe('ShieldedAccessControl', () => { OPERATOR_1.secretNonce, ); shieldedAccessControl._grantRole( - OPERATOR_1.roleId, + OPERATOR_1.role, operator1Op2AccountId, ); - shieldedAccessControl.as(OPERATOR_2.publicKey); // assert OP_2 has OP_1 roleId + shieldedAccessControl.as(OPERATOR_2.publicKey); // assert OP_2 has OP_1 role expect( shieldedAccessControl._validateRole( - OPERATOR_1.roleId, + OPERATOR_1.role, operator1Op2AccountId, ), ).toBe(true); @@ -479,13 +479,13 @@ describe('ShieldedAccessControl', () => { OPERATOR_1.secretNonce, ); shieldedAccessControl._grantRole( - OPERATOR_1.roleId, + OPERATOR_1.role, operator1Op3AccountId, ); - shieldedAccessControl.as(OPERATOR_3.publicKey); // assert OP_3 has OP_1 roleId + shieldedAccessControl.as(OPERATOR_3.publicKey); // assert OP_3 has OP_1 role expect( shieldedAccessControl._validateRole( - OPERATOR_1.roleId, + OPERATOR_1.role, operator1Op3AccountId, ), ).toBe(true); @@ -495,14 +495,14 @@ describe('ShieldedAccessControl', () => { describe('assertOnlyRole', () => { beforeEach(() => { - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); shieldedAccessControl.setPersistentCaller(ADMIN.publicKey); }); describe('should fail', () => { - it('when wit_getRoleCommitmentPath returns a valid path for a different roleId, accountId pairing', () => { + it('when wit_getRoleCommitmentPath returns a valid path for a different role, accountId pairing', () => { shieldedAccessControl._grantRole( - OPERATOR_1.roleId, + OPERATOR_1.role, OPERATOR_1.accountId, ); // Override witness to return valid path for OPERATOR_1 role commitment @@ -520,7 +520,7 @@ describe('ShieldedAccessControl', () => { }, ); expect(() => { - shieldedAccessControl.assertOnlyRole(ADMIN.roleId); + shieldedAccessControl.assertOnlyRole(ADMIN.role); }).toThrow( 'ShieldedAccessControl: Path must contain leaf matching computed role commitment for the provided role, accountId pairing', ); @@ -529,7 +529,7 @@ describe('ShieldedAccessControl', () => { it('when caller was never granted the role', () => { shieldedAccessControl.as(UNAUTHORIZED.publicKey); expect(() => - shieldedAccessControl.assertOnlyRole(ADMIN.roleId), + shieldedAccessControl.assertOnlyRole(ADMIN.role), ).toThrow('ShieldedAccessControl: unauthorized account'); }); @@ -539,17 +539,17 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl.getCallerContext().currentZswapLocalState .coinPublicKey, ).toEqual(ADMIN.zPublicKey); - expect(shieldedAccessControl.getRoleAdmin(ADMIN.roleId)).toEqual( - new Uint8Array(ADMIN.roleId), + expect(shieldedAccessControl.getRoleAdmin(ADMIN.role)).toEqual( + new Uint8Array(ADMIN.role), ); expect( - shieldedAccessControl._validateRole(ADMIN.roleId, ADMIN.accountId), + shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), ).toBe(true); // Check nonce is correct expect( shieldedAccessControl.privateState.getCurrentSecretNonce( - ADMIN.roleId, + ADMIN.role, ), ).toBe(ADMIN.secretNonce); @@ -569,7 +569,7 @@ describe('ShieldedAccessControl', () => { expect(witnessCalculatedPath).not.toEqual(truePath); expect(() => - shieldedAccessControl.assertOnlyRole(ADMIN.roleId), + shieldedAccessControl.assertOnlyRole(ADMIN.role), ).toThrow('ShieldedAccessControl: unauthorized account'); }); @@ -579,22 +579,22 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl.getCallerContext().currentZswapLocalState .coinPublicKey, ).toEqual(ADMIN.zPublicKey); - expect(shieldedAccessControl.getRoleAdmin(ADMIN.roleId)).toEqual( - new Uint8Array(ADMIN.roleId), + expect(shieldedAccessControl.getRoleAdmin(ADMIN.role)).toEqual( + new Uint8Array(ADMIN.role), ); expect( - shieldedAccessControl._validateRole(ADMIN.roleId, ADMIN.accountId), + shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), ).toBe(true); shieldedAccessControl.privateState.injectSecretNonce( - ADMIN.roleId, + ADMIN.role, UNAUTHORIZED.secretNonce, ); // Check nonce is incorrect expect( shieldedAccessControl.privateState.getCurrentSecretNonce( - ADMIN.roleId, + ADMIN.role, ), ).not.toBe(ADMIN.secretNonce); @@ -610,18 +610,18 @@ describe('ShieldedAccessControl', () => { expect(witnessCalculatedPath).toEqual(truePath); expect(() => - shieldedAccessControl.assertOnlyRole(ADMIN.roleId), + shieldedAccessControl.assertOnlyRole(ADMIN.role), ).toThrow('ShieldedAccessControl: unauthorized account'); }); it('when unauthorized caller has correct nonce, and path', () => { // Check UNAUTHORIZED user is not admin, doesnt have admin role - expect(shieldedAccessControl.getRoleAdmin(ADMIN.roleId)).not.toEqual( - new Uint8Array(UNAUTHORIZED.roleId), + expect(shieldedAccessControl.getRoleAdmin(ADMIN.role)).not.toEqual( + new Uint8Array(UNAUTHORIZED.role), ); expect( shieldedAccessControl._validateRole( - ADMIN.roleId, + ADMIN.role, UNAUTHORIZED.accountId, ), ).toBe(false); @@ -629,7 +629,7 @@ describe('ShieldedAccessControl', () => { // Check nonce is correct expect( shieldedAccessControl.privateState.getCurrentSecretNonce( - ADMIN.roleId, + ADMIN.role, ), ).toBe(ADMIN.secretNonce); @@ -652,22 +652,22 @@ describe('ShieldedAccessControl', () => { ).toEqual(UNAUTHORIZED.zPublicKey); expect(() => - shieldedAccessControl.assertOnlyRole(ADMIN.roleId), + shieldedAccessControl.assertOnlyRole(ADMIN.role), ).toThrow('ShieldedAccessControl: unauthorized account'); }); it('when role is revoked', () => { - shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); expect(() => - shieldedAccessControl.assertOnlyRole(ADMIN.roleId), + shieldedAccessControl.assertOnlyRole(ADMIN.role), ).toThrow('ShieldedAccessControl: unauthorized account'); }); it('when role is revoked and re-issued to the same accountId', () => { - shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); + shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); expect(() => - shieldedAccessControl.assertOnlyRole(ADMIN.roleId), + shieldedAccessControl.assertOnlyRole(ADMIN.role), ).toThrow('ShieldedAccessControl: unauthorized account'); }); }); @@ -675,7 +675,7 @@ describe('ShieldedAccessControl', () => { describe('should not fail', () => { it('when accountId has multiple roles', () => { shieldedAccessControl.privateState.injectSecretNonce( - OPERATOR_1.roleId, + OPERATOR_1.role, OPERATOR_1.secretNonce, ); // A unique accountId must be constructed for each new role using its associated secretNonce @@ -685,7 +685,7 @@ describe('ShieldedAccessControl', () => { ); shieldedAccessControl.privateState.injectSecretNonce( - OPERATOR_2.roleId, + OPERATOR_2.role, OPERATOR_2.secretNonce, ); const operator2AccountId = buildAccountIdHash( @@ -694,7 +694,7 @@ describe('ShieldedAccessControl', () => { ); shieldedAccessControl.privateState.injectSecretNonce( - OPERATOR_3.roleId, + OPERATOR_3.role, OPERATOR_3.secretNonce, ); const operator3AccountId = buildAccountIdHash( @@ -703,22 +703,22 @@ describe('ShieldedAccessControl', () => { ); shieldedAccessControl._grantRole( - OPERATOR_1.roleId, + OPERATOR_1.role, operator1AccountId, ); shieldedAccessControl._grantRole( - OPERATOR_2.roleId, + OPERATOR_2.role, operator2AccountId, ); shieldedAccessControl._grantRole( - OPERATOR_3.roleId, + OPERATOR_3.role, operator3AccountId, ); expect(() => { - shieldedAccessControl.assertOnlyRole(ADMIN.roleId); - shieldedAccessControl.assertOnlyRole(OPERATOR_1.roleId); - shieldedAccessControl.assertOnlyRole(OPERATOR_2.roleId); - shieldedAccessControl.assertOnlyRole(OPERATOR_3.roleId); + shieldedAccessControl.assertOnlyRole(ADMIN.role); + shieldedAccessControl.assertOnlyRole(OPERATOR_1.role); + shieldedAccessControl.assertOnlyRole(OPERATOR_2.role); + shieldedAccessControl.assertOnlyRole(OPERATOR_3.role); }).not.toThrow(); }); @@ -726,7 +726,7 @@ describe('ShieldedAccessControl', () => { // Check nonce is correct expect( shieldedAccessControl.privateState.getCurrentSecretNonce( - ADMIN.roleId, + ADMIN.role, ), ).toBe(ADMIN.secretNonce); @@ -742,29 +742,29 @@ describe('ShieldedAccessControl', () => { expect(witnessCalculatedPath).toEqual(truePath); expect(() => - shieldedAccessControl.assertOnlyRole(ADMIN.roleId), + shieldedAccessControl.assertOnlyRole(ADMIN.role), ).not.toThrow(); }); it('when role is revoked and re-issued with a different accountId', () => { - shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); shieldedAccessControl.privateState.injectSecretNonce( - ADMIN.roleId, + ADMIN.role, Buffer.alloc(32, 'NEW_ADMIN_NONCE'), ); const newAdminAccountId = buildAccountIdHash( ADMIN.zPublicKey, shieldedAccessControl.privateState.getCurrentSecretNonce( - ADMIN.roleId, + ADMIN.role, ), ); expect(newAdminAccountId).not.toEqual(ADMIN.accountId); - shieldedAccessControl._grantRole(ADMIN.roleId, newAdminAccountId); + shieldedAccessControl._grantRole(ADMIN.role, newAdminAccountId); expect(() => shieldedAccessControl.assertOnlyRole( - ADMIN.roleId, + ADMIN.role, ) ).not.toThrow(); }); @@ -773,7 +773,7 @@ describe('ShieldedAccessControl', () => { // All users will use OPERATOR_1.secretNonce as their nonce value // when generating their accountId for simplicity shieldedAccessControl.privateState.injectSecretNonce( - OPERATOR_1.roleId, + OPERATOR_1.role, OPERATOR_1.secretNonce, ); // A unique accountId must be constructed for each new role using its associated secretNonce @@ -782,40 +782,40 @@ describe('ShieldedAccessControl', () => { OPERATOR_1.secretNonce, ); shieldedAccessControl._grantRole( - OPERATOR_1.roleId, + OPERATOR_1.role, operator1AdminAccountId, ); - shieldedAccessControl.as(ADMIN.publicKey); // assert ADMIN has OP_1 roleId - expect(shieldedAccessControl.assertOnlyRole(OPERATOR_1.roleId)); + shieldedAccessControl.as(ADMIN.publicKey); // assert ADMIN has OP_1 role + expect(shieldedAccessControl.assertOnlyRole(OPERATOR_1.role)); const operator1Op2AccountId = buildAccountIdHash( OPERATOR_2.zPublicKey, OPERATOR_1.secretNonce, ); shieldedAccessControl._grantRole( - OPERATOR_1.roleId, + OPERATOR_1.role, operator1Op2AccountId, ); - shieldedAccessControl.as(OPERATOR_2.publicKey); // assert OP_2 has OP_1 roleId - expect(shieldedAccessControl.assertOnlyRole(OPERATOR_1.roleId)); + shieldedAccessControl.as(OPERATOR_2.publicKey); // assert OP_2 has OP_1 role + expect(shieldedAccessControl.assertOnlyRole(OPERATOR_1.role)); const operator1Op3AccountId = buildAccountIdHash( OPERATOR_3.zPublicKey, OPERATOR_1.secretNonce, ); shieldedAccessControl._grantRole( - OPERATOR_1.roleId, + OPERATOR_1.role, operator1Op3AccountId, ); - shieldedAccessControl.as(OPERATOR_3.publicKey); // assert OP_3 has OP_1 roleId - expect(shieldedAccessControl.assertOnlyRole(OPERATOR_1.roleId)); + shieldedAccessControl.as(OPERATOR_3.publicKey); // assert OP_3 has OP_1 role + expect(shieldedAccessControl.assertOnlyRole(OPERATOR_1.role)); }); }); }); describe('grantRole', () => { beforeEach(() => { - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); shieldedAccessControl.setPersistentCaller(ADMIN.publicKey); }); @@ -824,15 +824,15 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl.as(UNAUTHORIZED.publicKey); expect(() => shieldedAccessControl.grantRole( - OPERATOR_1.roleId, + OPERATOR_1.role, OPERATOR_1.accountId, ), ).toThrow('ShieldedAccessControl: unauthorized account'); }); - it('when wit_getRoleCommitmentPath returns a valid path for a different roleId, accountId pairing', () => { + it('when wit_getRoleCommitmentPath returns a valid path for a different role, accountId pairing', () => { shieldedAccessControl._grantRole( - OPERATOR_1.roleId, + OPERATOR_1.role, OPERATOR_1.accountId, ); // Override witness to return valid path for OPERATOR_1 role commitment @@ -850,7 +850,7 @@ describe('ShieldedAccessControl', () => { }, ); expect(() => { - shieldedAccessControl.grantRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl.grantRole(ADMIN.role, ADMIN.accountId); }).toThrow( 'ShieldedAccessControl: Path must contain leaf matching computed role commitment for the provided role, accountId pairing', ); @@ -858,34 +858,34 @@ describe('ShieldedAccessControl', () => { it('when admin with duplicate roles is revoked', () => { // create duplicate roles - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); + shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); - shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); expect(() => - shieldedAccessControl.grantRole(ADMIN.roleId, ADMIN.accountId), + shieldedAccessControl.grantRole(ADMIN.role, ADMIN.accountId), ).toThrow('ShieldedAccessControl: unauthorized account'); }); it('when admin role is revoked', () => { - shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); expect(() => - shieldedAccessControl.grantRole(ADMIN.roleId, ADMIN.accountId), + shieldedAccessControl.grantRole(ADMIN.role, ADMIN.accountId), ).toThrow('ShieldedAccessControl: unauthorized account'); }); it('when admin provides incorrect nonce', () => { shieldedAccessControl.privateState.injectSecretNonce( - ADMIN.roleId, + ADMIN.role, BAD_INPUT.secretNonce, ); expect( shieldedAccessControl.privateState.getCurrentSecretNonce( - ADMIN.roleId, + ADMIN.role, ), ).not.toEqual(ADMIN.secretNonce); expect(() => - shieldedAccessControl.grantRole(ADMIN.roleId, ADMIN.accountId), + shieldedAccessControl.grantRole(ADMIN.role, ADMIN.accountId), ).toThrow('ShieldedAccessControl: unauthorized account'); }); @@ -895,13 +895,13 @@ describe('ShieldedAccessControl', () => { RETURN_BAD_PATH, ); expect(() => - shieldedAccessControl.grantRole(ADMIN.roleId, ADMIN.accountId), + shieldedAccessControl.grantRole(ADMIN.role, ADMIN.accountId), ).toThrow('ShieldedAccessControl: unauthorized account'); }); it('when non-admin caller has role', () => { shieldedAccessControl._grantRole( - OPERATOR_1.roleId, + OPERATOR_1.role, OPERATOR_1.accountId, ); @@ -909,7 +909,7 @@ describe('ShieldedAccessControl', () => { // OP_1 has role but is not authorized to grant roles to other users expect(() => shieldedAccessControl.grantRole( - OPERATOR_1.roleId, + OPERATOR_1.role, OPERATOR_2.accountId, ), ).toThrow('ShieldedAccessControl: unauthorized account'); @@ -920,11 +920,11 @@ describe('ShieldedAccessControl', () => { it('when role is revoked', () => { // setup test shieldedAccessControl._grantRole( - OPERATOR_1.roleId, + OPERATOR_1.role, OPERATOR_1.accountId, ); shieldedAccessControl._revokeRole( - OPERATOR_1.roleId, + OPERATOR_1.role, OPERATOR_1.accountId, ); @@ -932,7 +932,7 @@ describe('ShieldedAccessControl', () => { .getPublicState() .ShieldedAccessControl__operatorRoles.root(); shieldedAccessControl.grantRole( - OPERATOR_1.roleId, + OPERATOR_1.role, OPERATOR_1.accountId, ); @@ -947,47 +947,47 @@ describe('ShieldedAccessControl', () => { it('when caller has the admin role', () => { expect(() => shieldedAccessControl.grantRole( - OPERATOR_1.roleId, + OPERATOR_1.role, OPERATOR_1.accountId, ), ).not.toThrow(); expect( shieldedAccessControl._validateRole( - OPERATOR_1.roleId, + OPERATOR_1.role, OPERATOR_1.accountId, ), ).toBe(true); }); it('when caller has custom admin role', () => { - // Make OPERATOR_1.roleId the admin of OPERATOR_2.roleId. + // Make OPERATOR_1.role the admin of OPERATOR_2.role. shieldedAccessControl._setRoleAdmin( - OPERATOR_2.roleId, - OPERATOR_1.roleId, + OPERATOR_2.role, + OPERATOR_1.role, ); - // Grant OPERATOR_1.roleId to OPERATOR_1.accountId + // Grant OPERATOR_1.role to OPERATOR_1.accountId shieldedAccessControl.grantRole( - OPERATOR_1.roleId, + OPERATOR_1.role, OPERATOR_1.accountId, ); // Switch to OPERATOR_1 as caller and inject their nonce for their role. shieldedAccessControl.privateState.injectSecretNonce( - OPERATOR_1.roleId, + OPERATOR_1.role, OPERATOR_1.secretNonce, ); shieldedAccessControl.setPersistentCaller(OPERATOR_1.publicKey); - // OPERATOR_1.accountId (who holds OPERATOR_1.roleId) can now grant OPERATOR_2.roleId. + // OPERATOR_1.accountId (who holds OPERATOR_1.role) can now grant OPERATOR_2.role. expect(() => shieldedAccessControl.grantRole( - OPERATOR_2.roleId, + OPERATOR_2.role, OPERATOR_2.accountId, ), ).not.toThrow(); expect( shieldedAccessControl._validateRole( - OPERATOR_2.roleId, + OPERATOR_2.role, OPERATOR_2.accountId, ), ).toBe(true); @@ -995,24 +995,24 @@ describe('ShieldedAccessControl', () => { it('when admin role is revoked and re-issued with a different accountId', () => { // setup test - shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); const newNonce = Buffer.alloc(32, 'NEW_ADMIN_NONCE'); shieldedAccessControl.privateState.injectSecretNonce( - ADMIN.roleId, + ADMIN.role, newNonce, ); const newAccountId = buildAccountIdHash(ADMIN.zPublicKey, newNonce); - shieldedAccessControl._grantRole(ADMIN.roleId, newAccountId); + shieldedAccessControl._grantRole(ADMIN.role, newAccountId); expect(() => { shieldedAccessControl.grantRole( - OPERATOR_1.roleId, + OPERATOR_1.role, OPERATOR_1.accountId, ); }).not.toThrow(); expect( shieldedAccessControl._validateRole( - OPERATOR_1.roleId, + OPERATOR_1.role, OPERATOR_1.accountId, ), ).toBe(true); @@ -1039,50 +1039,50 @@ describe('ShieldedAccessControl', () => { OPERATOR_3.zPublicKey, ADMIN.secretNonce, ); - shieldedAccessControl._grantRole(ADMIN.roleId, account1); - shieldedAccessControl._grantRole(ADMIN.roleId, account2); - shieldedAccessControl._grantRole(ADMIN.roleId, account3); + shieldedAccessControl._grantRole(ADMIN.role, account1); + shieldedAccessControl._grantRole(ADMIN.role, account2); + shieldedAccessControl._grantRole(ADMIN.role, account3); // check grant role succeeds as OP and role is valid shieldedAccessControl.as(OPERATOR_1.publicKey); expect(() => - shieldedAccessControl.grantRole(ADMIN.roleId, account1), + shieldedAccessControl.grantRole(ADMIN.role, account1), ).not.toThrow(); expect( - shieldedAccessControl._validateRole(ADMIN.roleId, account1), + shieldedAccessControl._validateRole(ADMIN.role, account1), ).toBe(true); shieldedAccessControl.as(OPERATOR_2.publicKey); expect(() => - shieldedAccessControl.grantRole(ADMIN.roleId, ADMIN.accountId), + shieldedAccessControl.grantRole(ADMIN.role, ADMIN.accountId), ).not.toThrow(); expect( - shieldedAccessControl._validateRole(ADMIN.roleId, account2), + shieldedAccessControl._validateRole(ADMIN.role, account2), ).toBe(true); shieldedAccessControl.as(OPERATOR_3.publicKey); expect(() => - shieldedAccessControl.grantRole(ADMIN.roleId, ADMIN.accountId), + shieldedAccessControl.grantRole(ADMIN.role, ADMIN.accountId), ).not.toThrow(); expect( - shieldedAccessControl._validateRole(ADMIN.roleId, account3), + shieldedAccessControl._validateRole(ADMIN.role, account3), ).toBe(true); }); it('when admin has multiple roles', () => { - shieldedAccessControl._grantRole(OPERATOR_1.roleId, ADMIN.accountId); - shieldedAccessControl._grantRole(OPERATOR_2.roleId, ADMIN.accountId); - shieldedAccessControl._grantRole(OPERATOR_3.roleId, ADMIN.accountId); + shieldedAccessControl._grantRole(OPERATOR_1.role, ADMIN.accountId); + shieldedAccessControl._grantRole(OPERATOR_2.role, ADMIN.accountId); + shieldedAccessControl._grantRole(OPERATOR_3.role, ADMIN.accountId); expect(() => shieldedAccessControl.grantRole( - OPERATOR_1.roleId, + OPERATOR_1.role, OPERATOR_1.accountId, ), ).not.toThrow(); expect( shieldedAccessControl._validateRole( - OPERATOR_1.roleId, + OPERATOR_1.role, OPERATOR_1.accountId, ), ).toBe(true); @@ -1090,23 +1090,23 @@ describe('ShieldedAccessControl', () => { it('when re-granting active role', () => { expect(() => - shieldedAccessControl.grantRole(ADMIN.roleId, ADMIN.accountId), + shieldedAccessControl.grantRole(ADMIN.role, ADMIN.accountId), ).not.toThrow(); expect( - shieldedAccessControl._validateRole(ADMIN.roleId, ADMIN.accountId), + shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), ).toBe(true); }); it('when granting role that does not exist', () => { expect(() => shieldedAccessControl.grantRole( - UNINITIALIZED.roleId, + UNINITIALIZED.role, UNINITIALIZED.accountId, ), ).not.toThrow(); expect( shieldedAccessControl._validateRole( - UNINITIALIZED.roleId, + UNINITIALIZED.role, UNINITIALIZED.accountId, ), ).toBe(true); @@ -1114,11 +1114,11 @@ describe('ShieldedAccessControl', () => { it('when granting role with bad accountId', () => { expect(() => - shieldedAccessControl.grantRole(ADMIN.roleId, BAD_INPUT.accountId), + shieldedAccessControl.grantRole(ADMIN.role, BAD_INPUT.accountId), ).not.toThrow(); expect( shieldedAccessControl._validateRole( - ADMIN.roleId, + ADMIN.role, BAD_INPUT.accountId, ), ).toBe(true); @@ -1131,29 +1131,29 @@ describe('ShieldedAccessControl', () => { it('when authorized user grants a new role', () => { shieldedAccessControl.as(ADMIN.publicKey); expect( - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId), + shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId), ).toBe(true); }); it('when unauthorized user grants role', () => { shieldedAccessControl.as(UNAUTHORIZED.publicKey); expect( - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId), + shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId), ).toBe(true); }); it('when re-granting active role ', () => { - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); expect( - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId), + shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId), ).toBe(true); }); it('when granting role that does not exist', () => { expect( shieldedAccessControl._grantRole( - UNINITIALIZED.roleId, + UNINITIALIZED.role, ADMIN.accountId, ), ).toBe(true); @@ -1161,7 +1161,7 @@ describe('ShieldedAccessControl', () => { it('when granting role with bad accountId', () => { expect( - shieldedAccessControl._grantRole(ADMIN.roleId, BAD_INPUT.accountId), + shieldedAccessControl._grantRole(ADMIN.role, BAD_INPUT.accountId), ).toBe(true); }); }); @@ -1174,8 +1174,8 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl.getCallerContext().currentZswapLocalState .coinPublicKey, ).toEqual(ADMIN.zPublicKey); - expect(shieldedAccessControl.getRoleAdmin(ADMIN.roleId)).toEqual( - new Uint8Array(ADMIN.roleId), + expect(shieldedAccessControl.getRoleAdmin(ADMIN.role)).toEqual( + new Uint8Array(ADMIN.role), ); // check merkle tree is empty @@ -1186,7 +1186,7 @@ describe('ShieldedAccessControl', () => { // check merkle tree is updated shieldedAccessControl.as(ADMIN.publicKey); - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); merkleRoot = shieldedAccessControl .getPublicState() .ShieldedAccessControl__operatorRoles.root(); @@ -1204,12 +1204,12 @@ describe('ShieldedAccessControl', () => { it('when unauthorized user grants a new role', () => { // Check UNAUTHORIZED is not admin - expect(shieldedAccessControl.getRoleAdmin(ADMIN.roleId)).not.toEqual( - new Uint8Array(UNAUTHORIZED.roleId), + expect(shieldedAccessControl.getRoleAdmin(ADMIN.role)).not.toEqual( + new Uint8Array(UNAUTHORIZED.role), ); expect( shieldedAccessControl._validateRole( - ADMIN.roleId, + ADMIN.role, UNAUTHORIZED.accountId, ), ).toBe(false); @@ -1228,7 +1228,7 @@ describe('ShieldedAccessControl', () => { ).toEqual(UNAUTHORIZED.zPublicKey); // check merkle tree is updated - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); merkleRoot = shieldedAccessControl .getPublicState() .ShieldedAccessControl__operatorRoles.root(); @@ -1253,7 +1253,7 @@ describe('ShieldedAccessControl', () => { // check merkle tree is updated shieldedAccessControl._grantRole( - UNINITIALIZED.roleId, + UNINITIALIZED.role, UNINITIALIZED.accountId, ); merkleRoot = shieldedAccessControl @@ -1281,7 +1281,7 @@ describe('ShieldedAccessControl', () => { expect(merkleRoot.field).toBe(0n); // check merkle tree is updated - shieldedAccessControl._grantRole(ADMIN.roleId, BAD_INPUT.accountId); + shieldedAccessControl._grantRole(ADMIN.role, BAD_INPUT.accountId); merkleRoot = shieldedAccessControl .getPublicState() .ShieldedAccessControl__operatorRoles.root(); @@ -1289,7 +1289,7 @@ describe('ShieldedAccessControl', () => { // check path exists for new role const adminRoleBadAccountCommitment = buildRoleCommitmentHash( - ADMIN.roleId, + ADMIN.role, BAD_INPUT.accountId, ); const merkleTreePath = shieldedAccessControl @@ -1306,23 +1306,23 @@ describe('ShieldedAccessControl', () => { describe('should return false', () => { it('when re-granting revoked role', () => { - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); - shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); expect( - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId), + shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId), ).toBe(false); }); }); describe('should not update _operatorRoles merkle tree', () => { it('when re-granting revoked role', () => { - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); - shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); const merkleRoot = shieldedAccessControl .getPublicState() .ShieldedAccessControl__operatorRoles.root(); - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); const newMerkleRoot = shieldedAccessControl .getPublicState() .ShieldedAccessControl__operatorRoles.root(); @@ -1333,9 +1333,9 @@ describe('ShieldedAccessControl', () => { describe('revokeRole', () => { beforeEach(() => { - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); shieldedAccessControl._grantRole( - OPERATOR_1.roleId, + OPERATOR_1.role, OPERATOR_1.accountId, ); shieldedAccessControl.setPersistentCaller(ADMIN.publicKey); @@ -1346,15 +1346,15 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl.as(UNAUTHORIZED.publicKey); expect(() => shieldedAccessControl.revokeRole( - OPERATOR_1.roleId, + OPERATOR_1.role, OPERATOR_1.accountId, ), ).toThrow('ShieldedAccessControl: unauthorized account'); }); - it('when wit_getRoleCommitmentPath returns a valid path for a different roleId, accountId pairing', () => { + it('when wit_getRoleCommitmentPath returns a valid path for a different role, accountId pairing', () => { shieldedAccessControl._grantRole( - OPERATOR_1.roleId, + OPERATOR_1.role, OPERATOR_1.accountId, ); // Override witness to return valid path for OPERATOR_1 role commitment @@ -1372,7 +1372,7 @@ describe('ShieldedAccessControl', () => { }, ); expect(() => { - shieldedAccessControl.revokeRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl.revokeRole(ADMIN.role, ADMIN.accountId); }).toThrow( 'ShieldedAccessControl: Path must contain leaf matching computed role commitment for the provided role, accountId pairing', ); @@ -1380,19 +1380,19 @@ describe('ShieldedAccessControl', () => { it('when admin with duplicate roles is revoked', () => { // create duplicate roles - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); + shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); - shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); expect(() => - shieldedAccessControl.revokeRole(ADMIN.roleId, ADMIN.accountId), + shieldedAccessControl.revokeRole(ADMIN.role, ADMIN.accountId), ).toThrow('ShieldedAccessControl: unauthorized account'); }); it('when admin role is revoked', () => { - shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); expect(() => - shieldedAccessControl.grantRole(ADMIN.roleId, ADMIN.accountId), + shieldedAccessControl.grantRole(ADMIN.role, ADMIN.accountId), ).toThrow('ShieldedAccessControl: unauthorized account'); }); @@ -1402,13 +1402,13 @@ describe('ShieldedAccessControl', () => { RETURN_BAD_PATH, ); expect(() => - shieldedAccessControl.revokeRole(ADMIN.roleId, ADMIN.accountId), + shieldedAccessControl.revokeRole(ADMIN.role, ADMIN.accountId), ).toThrow('ShieldedAccessControl: unauthorized account'); }); it('when non-admin caller has role', () => { shieldedAccessControl._grantRole( - OPERATOR_1.roleId, + OPERATOR_1.role, OPERATOR_1.accountId, ); @@ -1416,7 +1416,7 @@ describe('ShieldedAccessControl', () => { // OP_1 has role but is not authorized to grant roles to other users expect(() => shieldedAccessControl.revokeRole( - OPERATOR_1.roleId, + OPERATOR_1.role, OPERATOR_2.accountId, ), ).toThrow('ShieldedAccessControl: unauthorized account'); @@ -1424,22 +1424,22 @@ describe('ShieldedAccessControl', () => { it('when caller is admin of a different role', () => { shieldedAccessControl._setRoleAdmin( - OPERATOR_1.roleId, + OPERATOR_1.role, OPERATOR_1.accountId, ); shieldedAccessControl.as(OPERATOR_1.publicKey); expect(() => - shieldedAccessControl.revokeRole(ADMIN.roleId, ADMIN.accountId), + shieldedAccessControl.revokeRole(ADMIN.role, ADMIN.accountId), ).toThrow('ShieldedAccessControl: unauthorized account'); }); it('when admin provides bad nonce', () => { shieldedAccessControl.privateState.injectSecretNonce( - ADMIN.roleId, + ADMIN.role, BAD_INPUT.secretNonce, ); expect(() => - shieldedAccessControl.revokeRole(ADMIN.roleId, ADMIN.accountId), + shieldedAccessControl.revokeRole(ADMIN.role, ADMIN.accountId), ).toThrow('ShieldedAccessControl: unauthorized account'); }); }); @@ -1447,7 +1447,7 @@ describe('ShieldedAccessControl', () => { describe('should not update _roleCommitmentNullifiers set', () => { it('when role is re-revoked', () => { shieldedAccessControl.revokeRole( - OPERATOR_1.roleId, + OPERATOR_1.role, OPERATOR_1.accountId, ); const nullifierSetSize = shieldedAccessControl @@ -1455,7 +1455,7 @@ describe('ShieldedAccessControl', () => { .ShieldedAccessControl__roleCommitmentNullifiers.size(); expect(() => shieldedAccessControl.revokeRole( - OPERATOR_1.roleId, + OPERATOR_1.role, OPERATOR_1.accountId, ), ).not.toThrow(); @@ -1471,13 +1471,13 @@ describe('ShieldedAccessControl', () => { it('when caller has the admin role', () => { expect(() => shieldedAccessControl.revokeRole( - OPERATOR_1.roleId, + OPERATOR_1.role, OPERATOR_1.accountId, ), ).not.toThrow(); expect( shieldedAccessControl._validateRole( - OPERATOR_1.roleId, + OPERATOR_1.role, OPERATOR_1.accountId, ), ).toBe(false); @@ -1486,29 +1486,29 @@ describe('ShieldedAccessControl', () => { it('when caller has custom admin role', () => { // setup test shieldedAccessControl._grantRole( - OPERATOR_2.roleId, + OPERATOR_2.role, OPERATOR_3.accountId, ); shieldedAccessControl.privateState.injectSecretNonce( - OPERATOR_1.roleId, + OPERATOR_1.role, OPERATOR_1.secretNonce, ); // OP_1 is admin of OP_2 role shieldedAccessControl._setRoleAdmin( - OPERATOR_2.roleId, - OPERATOR_1.roleId, + OPERATOR_2.role, + OPERATOR_1.role, ); shieldedAccessControl.as(OPERATOR_1.publicKey); expect(() => shieldedAccessControl.revokeRole( - OPERATOR_2.roleId, + OPERATOR_2.role, OPERATOR_3.accountId, ), ).not.toThrow(); expect( shieldedAccessControl._validateRole( - OPERATOR_2.roleId, + OPERATOR_2.role, OPERATOR_3.accountId, ), ).toBe(false); @@ -1517,7 +1517,7 @@ describe('ShieldedAccessControl', () => { it('when role does not exist', () => { // create role commitment that doesn't exist const commitment = buildRoleCommitmentHash( - UNINITIALIZED.roleId, + UNINITIALIZED.role, ADMIN.accountId, ); @@ -1529,14 +1529,14 @@ describe('ShieldedAccessControl', () => { expect(() => shieldedAccessControl.revokeRole( - UNINITIALIZED.roleId, + UNINITIALIZED.role, ADMIN.accountId, ), ).not.toThrow(); expect( shieldedAccessControl._validateRole( - UNINITIALIZED.roleId, + UNINITIALIZED.role, ADMIN.accountId, ), ).toBe(false); @@ -1544,12 +1544,12 @@ describe('ShieldedAccessControl', () => { it('when revoking role with bad accountId', () => { expect(() => - shieldedAccessControl.revokeRole(ADMIN.roleId, BAD_INPUT.accountId), + shieldedAccessControl.revokeRole(ADMIN.role, BAD_INPUT.accountId), ).not.toThrow(); expect( shieldedAccessControl._validateRole( - ADMIN.roleId, + ADMIN.role, BAD_INPUT.accountId, ), ).toBe(false); @@ -1569,50 +1569,50 @@ describe('ShieldedAccessControl', () => { OPERATOR_3.zPublicKey, ADMIN.secretNonce, ); - shieldedAccessControl._grantRole(ADMIN.roleId, account1); - shieldedAccessControl._grantRole(ADMIN.roleId, account2); - shieldedAccessControl._grantRole(ADMIN.roleId, account3); + shieldedAccessControl._grantRole(ADMIN.role, account1); + shieldedAccessControl._grantRole(ADMIN.role, account2); + shieldedAccessControl._grantRole(ADMIN.role, account3); // check revoke role succeeds as OP and role is valid shieldedAccessControl.as(OPERATOR_1.publicKey); expect(() => - shieldedAccessControl.revokeRole(ADMIN.roleId, account1), + shieldedAccessControl.revokeRole(ADMIN.role, account1), ).not.toThrow(); expect( - shieldedAccessControl._validateRole(ADMIN.roleId, account1), + shieldedAccessControl._validateRole(ADMIN.role, account1), ).toBe(false); shieldedAccessControl.as(OPERATOR_2.publicKey); expect(() => - shieldedAccessControl.revokeRole(ADMIN.roleId, account2), + shieldedAccessControl.revokeRole(ADMIN.role, account2), ).not.toThrow(); expect( - shieldedAccessControl._validateRole(ADMIN.roleId, account2), + shieldedAccessControl._validateRole(ADMIN.role, account2), ).toBe(false); shieldedAccessControl.as(OPERATOR_3.publicKey); expect(() => - shieldedAccessControl.revokeRole(ADMIN.roleId, account3), + shieldedAccessControl.revokeRole(ADMIN.role, account3), ).not.toThrow(); expect( - shieldedAccessControl._validateRole(ADMIN.roleId, account3), + shieldedAccessControl._validateRole(ADMIN.role, account3), ).toBe(false); }); it('when admin has multiple roles', () => { - shieldedAccessControl._grantRole(OPERATOR_1.roleId, ADMIN.accountId); - shieldedAccessControl._grantRole(OPERATOR_2.roleId, ADMIN.accountId); - shieldedAccessControl._grantRole(OPERATOR_3.roleId, ADMIN.accountId); + shieldedAccessControl._grantRole(OPERATOR_1.role, ADMIN.accountId); + shieldedAccessControl._grantRole(OPERATOR_2.role, ADMIN.accountId); + shieldedAccessControl._grantRole(OPERATOR_3.role, ADMIN.accountId); expect(() => shieldedAccessControl.revokeRole( - OPERATOR_1.roleId, + OPERATOR_1.role, OPERATOR_1.accountId, ), ).not.toThrow(); expect( shieldedAccessControl._validateRole( - OPERATOR_1.roleId, + OPERATOR_1.role, OPERATOR_1.accountId, ), ).toBe(false); @@ -1621,13 +1621,13 @@ describe('ShieldedAccessControl', () => { it('when revoking role that does not exist', () => { expect(() => shieldedAccessControl.revokeRole( - UNINITIALIZED.roleId, + UNINITIALIZED.role, UNINITIALIZED.accountId, ), ).not.toThrow(); expect( shieldedAccessControl._validateRole( - UNINITIALIZED.roleId, + UNINITIALIZED.role, UNINITIALIZED.accountId, ), ).toBe(false); @@ -1635,24 +1635,24 @@ describe('ShieldedAccessControl', () => { it('when admin role is revoked and re-issued with a different accountId', () => { // setup test - shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); const newNonce = Buffer.alloc(32, 'NEW_ADMIN_NONCE'); shieldedAccessControl.privateState.injectSecretNonce( - ADMIN.roleId, + ADMIN.role, newNonce, ); const newAccountId = buildAccountIdHash(ADMIN.zPublicKey, newNonce); - shieldedAccessControl._grantRole(ADMIN.roleId, newAccountId); + shieldedAccessControl._grantRole(ADMIN.role, newAccountId); expect(() => { shieldedAccessControl.revokeRole( - OPERATOR_1.roleId, + OPERATOR_1.role, OPERATOR_1.accountId, ); }).not.toThrow(); expect( shieldedAccessControl._validateRole( - OPERATOR_1.roleId, + OPERATOR_1.role, OPERATOR_1.accountId, ), ).toBe(false); @@ -1662,7 +1662,7 @@ describe('ShieldedAccessControl', () => { describe('_revokeRole', () => { beforeEach(() => { - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); shieldedAccessControl.setPersistentCaller(ADMIN.publicKey); }); @@ -1670,13 +1670,13 @@ describe('ShieldedAccessControl', () => { it('when active role is revoked', () => { // confirm role is active const isValidRole = shieldedAccessControl._validateRole( - ADMIN.roleId, + ADMIN.role, ADMIN.accountId, ); expect(isValidRole).toBe(true); expect( - shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId), + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId), ).toBe(true); }); @@ -1686,26 +1686,26 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl.getCallerContext().currentZswapLocalState .coinPublicKey, ).toEqual(ADMIN.zPublicKey); - expect(shieldedAccessControl.getRoleAdmin(ADMIN.roleId)).toEqual( - new Uint8Array(ADMIN.roleId), + expect(shieldedAccessControl.getRoleAdmin(ADMIN.role)).toEqual( + new Uint8Array(ADMIN.role), ); expect( - shieldedAccessControl._validateRole(ADMIN.roleId, ADMIN.accountId), + shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), ).toBe(true); expect( - shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId), + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId), ).toBe(true); }); it('when unauthorized user revokes role', () => { // Check UNAUTHORIZED is not admin - expect(shieldedAccessControl.getRoleAdmin(ADMIN.roleId)).not.toEqual( - new Uint8Array(UNAUTHORIZED.roleId), + expect(shieldedAccessControl.getRoleAdmin(ADMIN.role)).not.toEqual( + new Uint8Array(UNAUTHORIZED.role), ); expect( shieldedAccessControl._validateRole( - ADMIN.roleId, + ADMIN.role, UNAUTHORIZED.accountId, ), ).toBe(false); @@ -1717,14 +1717,14 @@ describe('ShieldedAccessControl', () => { .coinPublicKey, ).toEqual(UNAUTHORIZED.zPublicKey); expect( - shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId), + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId), ).toBe(true); }); it('when revoking role that does not exist', () => { // create role commitment that doesn't exist const commitment = buildRoleCommitmentHash( - UNINITIALIZED.roleId, + UNINITIALIZED.role, ADMIN.accountId, ); @@ -1736,7 +1736,7 @@ describe('ShieldedAccessControl', () => { expect( shieldedAccessControl._revokeRole( - UNINITIALIZED.roleId, + UNINITIALIZED.role, ADMIN.accountId, ), ).toBe(true); @@ -1745,7 +1745,7 @@ describe('ShieldedAccessControl', () => { it('when revoking role with bad accountId', () => { expect( shieldedAccessControl._revokeRole( - ADMIN.roleId, + ADMIN.role, BAD_INPUT.accountId, ), ).toBe(true); @@ -1756,7 +1756,7 @@ describe('ShieldedAccessControl', () => { it('when active role is revoked', () => { // confirm role is active const isValidRole = shieldedAccessControl._validateRole( - ADMIN.roleId, + ADMIN.role, ADMIN.accountId, ); expect(isValidRole).toBe(true); @@ -1766,7 +1766,7 @@ describe('ShieldedAccessControl', () => { .ShieldedAccessControl__roleCommitmentNullifiers.size(); expect(initialSetSize).toBe(0n); - shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); const updatedSetSize = shieldedAccessControl .getPublicState() @@ -1787,11 +1787,11 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl.getCallerContext().currentZswapLocalState .coinPublicKey, ).toEqual(ADMIN.zPublicKey); - expect(shieldedAccessControl.getRoleAdmin(ADMIN.roleId)).toEqual( - new Uint8Array(ADMIN.roleId), + expect(shieldedAccessControl.getRoleAdmin(ADMIN.role)).toEqual( + new Uint8Array(ADMIN.role), ); expect( - shieldedAccessControl._validateRole(ADMIN.roleId, ADMIN.accountId), + shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), ).toBe(true); const initialSetSize = shieldedAccessControl @@ -1799,7 +1799,7 @@ describe('ShieldedAccessControl', () => { .ShieldedAccessControl__roleCommitmentNullifiers.size(); expect(initialSetSize).toBe(0n); - shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); const updatedSetSize = shieldedAccessControl .getPublicState() @@ -1816,12 +1816,12 @@ describe('ShieldedAccessControl', () => { it('when unauthorized user revokes role', () => { // Check UNAUTHORIZED is not admin - expect(shieldedAccessControl.getRoleAdmin(ADMIN.roleId)).not.toEqual( - new Uint8Array(UNAUTHORIZED.roleId), + expect(shieldedAccessControl.getRoleAdmin(ADMIN.role)).not.toEqual( + new Uint8Array(UNAUTHORIZED.role), ); expect( shieldedAccessControl._validateRole( - ADMIN.roleId, + ADMIN.role, UNAUTHORIZED.accountId, ), ).toBe(false); @@ -1838,7 +1838,7 @@ describe('ShieldedAccessControl', () => { .coinPublicKey, ).toEqual(UNAUTHORIZED.zPublicKey); - shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); const updatedSetSize = shieldedAccessControl .getPublicState() @@ -1856,7 +1856,7 @@ describe('ShieldedAccessControl', () => { it('when revoking role that does not exist', () => { // create role commitment that doesn't exist const commitment = buildRoleCommitmentHash( - UNINITIALIZED.roleId, + UNINITIALIZED.role, ADMIN.accountId, ); @@ -1872,7 +1872,7 @@ describe('ShieldedAccessControl', () => { expect(initialSetSize).toBe(0n); shieldedAccessControl._revokeRole( - UNINITIALIZED.roleId, + UNINITIALIZED.role, ADMIN.accountId, ); @@ -1898,7 +1898,7 @@ describe('ShieldedAccessControl', () => { .ShieldedAccessControl__roleCommitmentNullifiers.size(); expect(initialSetSize).toBe(0n); - shieldedAccessControl._revokeRole(ADMIN.roleId, BAD_INPUT.accountId); + shieldedAccessControl._revokeRole(ADMIN.role, BAD_INPUT.accountId); const updatedSetSize = shieldedAccessControl .getPublicState() @@ -1906,7 +1906,7 @@ describe('ShieldedAccessControl', () => { expect(updatedSetSize).toBe(1n); const commitment = buildRoleCommitmentHash( - ADMIN.roleId, + ADMIN.role, BAD_INPUT.accountId, ); const nullifier = buildNullifierHash(commitment); @@ -1927,33 +1927,33 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl.getCallerContext().currentZswapLocalState .coinPublicKey, ).toEqual(ADMIN.zPublicKey); - expect(shieldedAccessControl.getRoleAdmin(ADMIN.roleId)).toEqual( - new Uint8Array(ADMIN.roleId), + expect(shieldedAccessControl.getRoleAdmin(ADMIN.role)).toEqual( + new Uint8Array(ADMIN.role), ); expect( - shieldedAccessControl._validateRole(ADMIN.roleId, ADMIN.accountId), + shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), ).toBe(true); - shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); expect( - shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId), + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId), ).toBe(false); }); it('when unauthorized user re-revokes role', () => { // Check UNAUTHORIZED is not admin - expect(shieldedAccessControl.getRoleAdmin(ADMIN.roleId)).not.toEqual( - new Uint8Array(UNAUTHORIZED.roleId), + expect(shieldedAccessControl.getRoleAdmin(ADMIN.role)).not.toEqual( + new Uint8Array(UNAUTHORIZED.role), ); expect( shieldedAccessControl._validateRole( - ADMIN.roleId, + ADMIN.role, UNAUTHORIZED.accountId, ), ).toBe(false); // revoke as ADMIN - shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); // check caller is UNAUTHORIZED user shieldedAccessControl.as(UNAUTHORIZED.publicKey); @@ -1962,14 +1962,14 @@ describe('ShieldedAccessControl', () => { .coinPublicKey, ).toEqual(UNAUTHORIZED.zPublicKey); expect( - shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId), + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId), ).toBe(false); }); }); describe('should not update nullifier set', () => { it('when authorized user re-revokes role', () => { - shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); const initialSetSize = shieldedAccessControl .getPublicState() .ShieldedAccessControl__roleCommitmentNullifiers.size(); @@ -1981,10 +1981,10 @@ describe('ShieldedAccessControl', () => { .coinPublicKey, ).toEqual(ADMIN.zPublicKey); expect( - shieldedAccessControl._validateRole(ADMIN.roleId, ADMIN.accountId), + shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), ).toBe(false); - shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); const updatedSetSize = shieldedAccessControl .getPublicState() @@ -1993,26 +1993,26 @@ describe('ShieldedAccessControl', () => { }); it('when unauthorized user re-revokes role', () => { - shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); const initialSetSize = shieldedAccessControl .getPublicState() .ShieldedAccessControl__roleCommitmentNullifiers.size(); expect(initialSetSize).toBe(1n); // Check UNAUTHORIZED is not admin - expect(shieldedAccessControl.getRoleAdmin(ADMIN.roleId)).not.toEqual( - new Uint8Array(UNAUTHORIZED.roleId), + expect(shieldedAccessControl.getRoleAdmin(ADMIN.role)).not.toEqual( + new Uint8Array(UNAUTHORIZED.role), ); expect( shieldedAccessControl._validateRole( - ADMIN.roleId, + ADMIN.role, UNAUTHORIZED.accountId, ), ).toBe(false); // re-revoke as UNAUTHORIZED shieldedAccessControl.as(UNAUTHORIZED.publicKey); - shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); const updatedSetSize = shieldedAccessControl .getPublicState() @@ -2024,13 +2024,13 @@ describe('ShieldedAccessControl', () => { describe('proveCallerRole', () => { beforeEach(() => { - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); shieldedAccessControl.setPersistentCaller(ADMIN.publicKey); }); - it('should fail when caller provides valid path for a different roleId, accountId pairing', () => { + it('should fail when caller provides valid path for a different role, accountId pairing', () => { shieldedAccessControl._grantRole( - OPERATOR_1.roleId, + OPERATOR_1.role, OPERATOR_1.accountId, ); // Override witness to return valid path for OPERATOR_1 role commitment @@ -2048,7 +2048,7 @@ describe('ShieldedAccessControl', () => { }, ); expect(() => { - shieldedAccessControl.proveCallerRole(ADMIN.roleId); + shieldedAccessControl.proveCallerRole(ADMIN.role); }).toThrow( 'ShieldedAccessControl: Path must contain leaf matching computed role commitment for the provided role, accountId pairing', ); @@ -2062,10 +2062,10 @@ describe('ShieldedAccessControl', () => { .coinPublicKey, ).toEqual(ADMIN.zPublicKey); expect( - shieldedAccessControl._validateRole(ADMIN.roleId, ADMIN.accountId), + shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), ).toBe(true); - expect(shieldedAccessControl.proveCallerRole(ADMIN.roleId)).toBe( + expect(shieldedAccessControl.proveCallerRole(ADMIN.role)).toBe( true, ); }); @@ -2073,15 +2073,15 @@ describe('ShieldedAccessControl', () => { it('when caller has multiple roles', () => { // setup test shieldedAccessControl.privateState.injectSecretNonce( - OPERATOR_1.roleId, + OPERATOR_1.role, OPERATOR_1.secretNonce, ); shieldedAccessControl.privateState.injectSecretNonce( - OPERATOR_2.roleId, + OPERATOR_2.role, OPERATOR_2.secretNonce, ); shieldedAccessControl.privateState.injectSecretNonce( - OPERATOR_3.roleId, + OPERATOR_3.role, OPERATOR_3.secretNonce, ); const account1 = buildAccountIdHash( @@ -2096,41 +2096,41 @@ describe('ShieldedAccessControl', () => { ADMIN.zPublicKey, OPERATOR_3.secretNonce, ); - shieldedAccessControl._grantRole(OPERATOR_1.roleId, account1); - shieldedAccessControl._grantRole(OPERATOR_2.roleId, account2); - shieldedAccessControl._grantRole(OPERATOR_3.roleId, account3); + shieldedAccessControl._grantRole(OPERATOR_1.role, account1); + shieldedAccessControl._grantRole(OPERATOR_2.role, account2); + shieldedAccessControl._grantRole(OPERATOR_3.role, account3); - expect(shieldedAccessControl.proveCallerRole(ADMIN.roleId)).toBe( + expect(shieldedAccessControl.proveCallerRole(ADMIN.role)).toBe( true, ); - expect(shieldedAccessControl.proveCallerRole(OPERATOR_1.roleId)).toBe( + expect(shieldedAccessControl.proveCallerRole(OPERATOR_1.role)).toBe( true, ); - expect(shieldedAccessControl.proveCallerRole(OPERATOR_2.roleId)).toBe( + expect(shieldedAccessControl.proveCallerRole(OPERATOR_2.role)).toBe( true, ); - expect(shieldedAccessControl.proveCallerRole(OPERATOR_3.roleId)).toBe( + expect(shieldedAccessControl.proveCallerRole(OPERATOR_3.role)).toBe( true, ); }); it('when role is revoked and re-issued with a different accountId', () => { - shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); shieldedAccessControl.privateState.injectSecretNonce( - ADMIN.roleId, + ADMIN.role, Buffer.alloc(32, 'NEW_ADMIN_NONCE'), ); const newAdminAccountId = buildAccountIdHash( ADMIN.zPublicKey, shieldedAccessControl.privateState.getCurrentSecretNonce( - ADMIN.roleId, + ADMIN.role, ), ); expect(newAdminAccountId).not.toEqual(ADMIN.accountId); - shieldedAccessControl._grantRole(ADMIN.roleId, newAdminAccountId); - expect(shieldedAccessControl.proveCallerRole(ADMIN.roleId)).toBe( + shieldedAccessControl._grantRole(ADMIN.role, newAdminAccountId); + expect(shieldedAccessControl.proveCallerRole(ADMIN.role)).toBe( true, ); }); @@ -2139,7 +2139,7 @@ describe('ShieldedAccessControl', () => { // All users will use OPERATOR_1.secretNonce as their nonce value // when generating their accountId for simplicity shieldedAccessControl.privateState.injectSecretNonce( - OPERATOR_1.roleId, + OPERATOR_1.role, OPERATOR_1.secretNonce, ); // A unique accountId must be constructed for each new role using its associated secretNonce @@ -2148,11 +2148,11 @@ describe('ShieldedAccessControl', () => { OPERATOR_1.secretNonce, ); shieldedAccessControl._grantRole( - OPERATOR_1.roleId, + OPERATOR_1.role, operator1AdminAccountId, ); - shieldedAccessControl.as(ADMIN.publicKey); // prove ADMIN has OP_1 roleId - expect(shieldedAccessControl.proveCallerRole(OPERATOR_1.roleId)).toBe( + shieldedAccessControl.as(ADMIN.publicKey); // prove ADMIN has OP_1 role + expect(shieldedAccessControl.proveCallerRole(OPERATOR_1.role)).toBe( true, ); @@ -2161,11 +2161,11 @@ describe('ShieldedAccessControl', () => { OPERATOR_1.secretNonce, ); shieldedAccessControl._grantRole( - OPERATOR_1.roleId, + OPERATOR_1.role, operator1Op2AccountId, ); - shieldedAccessControl.as(OPERATOR_2.publicKey); // prove OP_2 has OP_1 roleId - expect(shieldedAccessControl.proveCallerRole(OPERATOR_1.roleId)).toBe( + shieldedAccessControl.as(OPERATOR_2.publicKey); // prove OP_2 has OP_1 role + expect(shieldedAccessControl.proveCallerRole(OPERATOR_1.role)).toBe( true, ); @@ -2174,11 +2174,11 @@ describe('ShieldedAccessControl', () => { OPERATOR_1.secretNonce, ); shieldedAccessControl._grantRole( - OPERATOR_1.roleId, + OPERATOR_1.role, operator1Op3AccountId, ); - shieldedAccessControl.as(OPERATOR_3.publicKey); // prove OP_3 has OP_1 roleId - expect(shieldedAccessControl.proveCallerRole(OPERATOR_1.roleId)).toBe( + shieldedAccessControl.as(OPERATOR_3.publicKey); // prove OP_3 has OP_1 role + expect(shieldedAccessControl.proveCallerRole(OPERATOR_1.role)).toBe( true, ); }); @@ -2188,7 +2188,7 @@ describe('ShieldedAccessControl', () => { it('when caller does not have role', () => { // setup test shieldedAccessControl.privateState.injectSecretNonce( - OPERATOR_1.roleId, + OPERATOR_1.role, OPERATOR_1.secretNonce, ); const accountId = buildAccountIdHash( @@ -2198,45 +2198,45 @@ describe('ShieldedAccessControl', () => { // Check does not have OPERATOR role expect( - shieldedAccessControl._validateRole(OPERATOR_1.roleId, accountId), + shieldedAccessControl._validateRole(OPERATOR_1.role, accountId), ).toBe(false); - expect(shieldedAccessControl.proveCallerRole(OPERATOR_1.roleId)).toBe( + expect(shieldedAccessControl.proveCallerRole(OPERATOR_1.role)).toBe( false, ); }); it('when caller has revoked role', () => { - shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); // check role revoked expect( - shieldedAccessControl._validateRole(ADMIN.roleId, ADMIN.accountId), + shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), ).toBe(false); - expect(shieldedAccessControl.proveCallerRole(ADMIN.roleId)).toBe( + expect(shieldedAccessControl.proveCallerRole(ADMIN.role)).toBe( false, ); }); it('when revoked role is re-granted', () => { - shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); // check role revoked expect( - shieldedAccessControl._validateRole(ADMIN.roleId, ADMIN.accountId), + shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), ).toBe(false); - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); - expect(shieldedAccessControl.proveCallerRole(ADMIN.roleId)).toBe( + shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); + expect(shieldedAccessControl.proveCallerRole(ADMIN.role)).toBe( false, ); }); it('when an unauthorized caller has valid nonce', () => { - // UNAUTHORIZED uses the same private state (ADMIN.secretNonce for ADMIN.roleId), + // UNAUTHORIZED uses the same private state (ADMIN.secretNonce for ADMIN.role), // so their derived accountId won't match the committed one. shieldedAccessControl.as(UNAUTHORIZED.publicKey); - expect(shieldedAccessControl.proveCallerRole(ADMIN.roleId)).toBe( + expect(shieldedAccessControl.proveCallerRole(ADMIN.role)).toBe( false, ); }); @@ -2248,21 +2248,21 @@ describe('ShieldedAccessControl', () => { .coinPublicKey, ).toEqual(ADMIN.zPublicKey); expect( - shieldedAccessControl._validateRole(ADMIN.roleId, ADMIN.accountId), + shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), ).toBe(true); shieldedAccessControl.privateState.injectSecretNonce( - ADMIN.roleId, + ADMIN.role, BAD_INPUT.secretNonce, ); // nonce should not match expect(ADMIN.secretNonce).not.toEqual( shieldedAccessControl.privateState.getCurrentSecretNonce( - ADMIN.roleId, + ADMIN.role, ), ); - expect(shieldedAccessControl.proveCallerRole(ADMIN.roleId)).toBe( + expect(shieldedAccessControl.proveCallerRole(ADMIN.role)).toBe( false, ); }); @@ -2274,14 +2274,14 @@ describe('ShieldedAccessControl', () => { .coinPublicKey, ).toEqual(ADMIN.zPublicKey); expect( - shieldedAccessControl._validateRole(ADMIN.roleId, ADMIN.accountId), + shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), ).toBe(true); shieldedAccessControl.overrideWitness( 'wit_getRoleCommitmentPath', RETURN_BAD_PATH, ); - expect(shieldedAccessControl.proveCallerRole(ADMIN.roleId)).toBe( + expect(shieldedAccessControl.proveCallerRole(ADMIN.role)).toBe( false, ); }); @@ -2290,24 +2290,24 @@ describe('ShieldedAccessControl', () => { describe('getRoleAdmin', () => { it('should return zero bytes (DEFAULT_ADMIN_ROLE) for a role with no admin set', () => { - expect(shieldedAccessControl.getRoleAdmin(OPERATOR_1.roleId)).toEqual( + expect(shieldedAccessControl.getRoleAdmin(OPERATOR_1.role)).toEqual( new Uint8Array(32), ); }); it('should return the admin role after _setRoleAdmin', () => { - shieldedAccessControl._setRoleAdmin(OPERATOR_1.roleId, ADMIN.roleId); - expect(shieldedAccessControl.getRoleAdmin(OPERATOR_1.roleId)).toEqual( - new Uint8Array(ADMIN.roleId), + shieldedAccessControl._setRoleAdmin(OPERATOR_1.role, ADMIN.role); + expect(shieldedAccessControl.getRoleAdmin(OPERATOR_1.role)).toEqual( + new Uint8Array(ADMIN.role), ); }); }); describe('_setRoleAdmin', () => { it('should set admin role', () => { - shieldedAccessControl._setRoleAdmin(OPERATOR_1.roleId, ADMIN.roleId); - expect(shieldedAccessControl.getRoleAdmin(OPERATOR_1.roleId)).toEqual( - new Uint8Array(ADMIN.roleId), + shieldedAccessControl._setRoleAdmin(OPERATOR_1.role, ADMIN.role); + expect(shieldedAccessControl.getRoleAdmin(OPERATOR_1.role)).toEqual( + new Uint8Array(ADMIN.role), ); }); @@ -2319,9 +2319,9 @@ describe('ShieldedAccessControl', () => { ).toBe(true); // setup test - shieldedAccessControl._setRoleAdmin(OPERATOR_1.roleId, ADMIN.roleId); - shieldedAccessControl._setRoleAdmin(OPERATOR_2.roleId, ADMIN.roleId); - shieldedAccessControl._setRoleAdmin(OPERATOR_3.roleId, ADMIN.roleId); + shieldedAccessControl._setRoleAdmin(OPERATOR_1.role, ADMIN.role); + shieldedAccessControl._setRoleAdmin(OPERATOR_2.role, ADMIN.role); + shieldedAccessControl._setRoleAdmin(OPERATOR_3.role, ADMIN.role); // check updated state expect( @@ -2339,83 +2339,83 @@ describe('ShieldedAccessControl', () => { expect( shieldedAccessControl .getPublicState() - .ShieldedAccessControl__adminRoles.member(OPERATOR_1.roleId), + .ShieldedAccessControl__adminRoles.member(OPERATOR_1.role), ).toBe(true); expect( shieldedAccessControl .getPublicState() - .ShieldedAccessControl__adminRoles.lookup(OPERATOR_1.roleId), - ).toEqual(new Uint8Array(ADMIN.roleId)); + .ShieldedAccessControl__adminRoles.lookup(OPERATOR_1.role), + ).toEqual(new Uint8Array(ADMIN.role)); expect( shieldedAccessControl .getPublicState() - .ShieldedAccessControl__adminRoles.member(OPERATOR_2.roleId), + .ShieldedAccessControl__adminRoles.member(OPERATOR_2.role), ).toBe(true); expect( shieldedAccessControl .getPublicState() - .ShieldedAccessControl__adminRoles.lookup(OPERATOR_2.roleId), - ).toEqual(new Uint8Array(ADMIN.roleId)); + .ShieldedAccessControl__adminRoles.lookup(OPERATOR_2.role), + ).toEqual(new Uint8Array(ADMIN.role)); expect( shieldedAccessControl .getPublicState() - .ShieldedAccessControl__adminRoles.member(OPERATOR_3.roleId), + .ShieldedAccessControl__adminRoles.member(OPERATOR_3.role), ).toBe(true); expect( shieldedAccessControl .getPublicState() - .ShieldedAccessControl__adminRoles.lookup(OPERATOR_3.roleId), - ).toEqual(new Uint8Array(ADMIN.roleId)); + .ShieldedAccessControl__adminRoles.lookup(OPERATOR_3.role), + ).toEqual(new Uint8Array(ADMIN.role)); }); it('should override an existing admin role', () => { - shieldedAccessControl._setRoleAdmin(OPERATOR_1.roleId, ADMIN.roleId); - expect(shieldedAccessControl.getRoleAdmin(OPERATOR_1.roleId)).toEqual( - new Uint8Array(ADMIN.roleId), + shieldedAccessControl._setRoleAdmin(OPERATOR_1.role, ADMIN.role); + expect(shieldedAccessControl.getRoleAdmin(OPERATOR_1.role)).toEqual( + new Uint8Array(ADMIN.role), ); shieldedAccessControl._setRoleAdmin( - OPERATOR_1.roleId, - OPERATOR_2.roleId, + OPERATOR_1.role, + OPERATOR_2.role, ); - expect(shieldedAccessControl.getRoleAdmin(OPERATOR_1.roleId)).toEqual( - new Uint8Array(OPERATOR_2.roleId), + expect(shieldedAccessControl.getRoleAdmin(OPERATOR_1.role)).toEqual( + new Uint8Array(OPERATOR_2.role), ); }); }); describe('renounceRole', () => { beforeEach(() => { - shieldedAccessControl._grantRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); shieldedAccessControl.setPersistentCaller(ADMIN.publicKey); }); it('should allow caller to renounce their own role', () => { expect(() => - shieldedAccessControl.renounceRole(ADMIN.roleId, ADMIN.accountId), + shieldedAccessControl.renounceRole(ADMIN.role, ADMIN.accountId), ).not.toThrow(); expect( - shieldedAccessControl._validateRole(ADMIN.roleId, ADMIN.accountId), + shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), ).toBe(false); }); it('should allow caller to renounce role that does not exist', () => { // Set ADMIN.secretNonce for UNINITIALIZED role so circuit computes ADMIN.accountId shieldedAccessControl.privateState.injectSecretNonce( - UNINITIALIZED.roleId, + UNINITIALIZED.role, ADMIN.secretNonce, ); expect(() => shieldedAccessControl.renounceRole( - UNINITIALIZED.roleId, + UNINITIALIZED.role, ADMIN.accountId, ), ).not.toThrow(); expect( shieldedAccessControl._validateRole( - UNINITIALIZED.roleId, + UNINITIALIZED.role, ADMIN.accountId, ), ).toBe(false); @@ -2424,18 +2424,18 @@ describe('ShieldedAccessControl', () => { it('should allow caller to renounce a role they do not have', () => { // Set ADMIN.secretNonce for OPERATOR_1 role so circuit computes ADMIN.accountId shieldedAccessControl.privateState.injectSecretNonce( - OPERATOR_1.roleId, + OPERATOR_1.role, ADMIN.secretNonce, ); expect(() => shieldedAccessControl.renounceRole( - OPERATOR_1.roleId, + OPERATOR_1.role, ADMIN.accountId, ), ).not.toThrow(); expect( shieldedAccessControl._validateRole( - OPERATOR_1.roleId, + OPERATOR_1.role, ADMIN.accountId, ), ).toBe(false); @@ -2443,18 +2443,18 @@ describe('ShieldedAccessControl', () => { it('should fail when caller provides bad nonce', () => { shieldedAccessControl.privateState.injectSecretNonce( - ADMIN.roleId, + ADMIN.role, BAD_INPUT.secretNonce, ); expect(() => - shieldedAccessControl.renounceRole(ADMIN.roleId, ADMIN.accountId), + shieldedAccessControl.renounceRole(ADMIN.role, ADMIN.accountId), ).toThrow('ShieldedAccessControl: bad confirmation'); }); it('should fail when caller provides bad accountId', () => { expect(() => - shieldedAccessControl.renounceRole(ADMIN.roleId, BAD_INPUT.accountId), + shieldedAccessControl.renounceRole(ADMIN.role, BAD_INPUT.accountId), ).toThrow('ShieldedAccessControl: bad confirmation'); }); @@ -2462,25 +2462,25 @@ describe('ShieldedAccessControl', () => { // check we have valid secret nonce in private state expect( shieldedAccessControl.privateState.getCurrentSecretNonce( - ADMIN.roleId, + ADMIN.role, ), ).toEqual(ADMIN.secretNonce); shieldedAccessControl.as(UNAUTHORIZED.publicKey); expect(() => - shieldedAccessControl.renounceRole(ADMIN.roleId, ADMIN.accountId), + shieldedAccessControl.renounceRole(ADMIN.role, ADMIN.accountId), ).toThrow('ShieldedAccessControl: bad confirmation'); }); it('should be a no-op when role is already revoked', () => { - shieldedAccessControl._revokeRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); // renounceRole calls _revokeRole internally which silently returns false // when the role is already revoked — no assertion, so no throw. expect(() => - shieldedAccessControl.renounceRole(ADMIN.roleId, ADMIN.accountId), + shieldedAccessControl.renounceRole(ADMIN.role, ADMIN.accountId), ).not.toThrow(); expect( - shieldedAccessControl._validateRole(ADMIN.roleId, ADMIN.accountId), + shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), ).toBe(false); }); @@ -2490,7 +2490,7 @@ describe('ShieldedAccessControl', () => { .ShieldedAccessControl__roleCommitmentNullifiers.size(); expect(nullifierSetSize).toBe(0n); - shieldedAccessControl.renounceRole(ADMIN.roleId, ADMIN.accountId); + shieldedAccessControl.renounceRole(ADMIN.role, ADMIN.accountId); const updatedSetSize = shieldedAccessControl .getPublicState() .ShieldedAccessControl__roleCommitmentNullifiers.size(); diff --git a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts index 7430b1dd..c27285d4 100644 --- a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts +++ b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts @@ -54,10 +54,10 @@ export class ShieldedAccessControlSimulator extends ShieldedAccessControlSimulat } public _computeRoleCommitment( - roleId: Uint8Array, + role: Uint8Array, accountId: Uint8Array, ): Uint8Array { - return this.circuits.impure._computeRoleCommitment(roleId, accountId); + return this.circuits.impure._computeRoleCommitment(role, accountId); } public _computeAccountId( @@ -71,8 +71,8 @@ export class ShieldedAccessControlSimulator extends ShieldedAccessControlSimulat return this.circuits.pure._computeNullifier(roleCommitment); } - public proveCallerRole(roleId: Uint8Array): boolean { - return this.circuits.impure.proveCallerRole(roleId); + public proveCallerRole(role: Uint8Array): boolean { + return this.circuits.impure.proveCallerRole(role); } /** @@ -80,12 +80,12 @@ export class ShieldedAccessControlSimulator extends ShieldedAccessControlSimulat * `newOwnerId` must be precalculated and given to the current owner off chain. * @param newOwnerId The new owner's unique identifier (`SHA256(pk, nonce)`). */ - public assertOnlyRole(roleId: Uint8Array) { - this.circuits.impure.assertOnlyRole(roleId); + public assertOnlyRole(role: Uint8Array) { + this.circuits.impure.assertOnlyRole(role); } - public _validateRole(roleId: Uint8Array, accountId: Uint8Array): boolean { - return this.circuits.impure._validateRole(roleId, accountId); + public _validateRole(role: Uint8Array, accountId: Uint8Array): boolean { + return this.circuits.impure._validateRole(role, accountId); } /** @@ -95,8 +95,8 @@ export class ShieldedAccessControlSimulator extends ShieldedAccessControlSimulat * after every transfer to prevent duplicate commitments given the same `id`. * @returns The commitment derived from `id` and `counter`. */ - public getRoleAdmin(roleId: Uint8Array): Uint8Array { - return this.circuits.impure.getRoleAdmin(roleId); + public getRoleAdmin(role: Uint8Array): Uint8Array { + return this.circuits.impure.getRoleAdmin(role); } /** @@ -106,8 +106,8 @@ export class ShieldedAccessControlSimulator extends ShieldedAccessControlSimulat * @param nonce - A private nonce to scope the commitment. * @returns The computed owner ID. */ - public grantRole(roleId: Uint8Array, accountId: Uint8Array) { - this.circuits.impure.grantRole(roleId, accountId); + public grantRole(role: Uint8Array, accountId: Uint8Array) { + this.circuits.impure.grantRole(role, accountId); } /** @@ -115,8 +115,8 @@ export class ShieldedAccessControlSimulator extends ShieldedAccessControlSimulat * enforcing permission checks on the caller. * @param newOwnerId - The unique identifier of the new owner calculated by `SHA256(pk, nonce)`. */ - public revokeRole(roleId: Uint8Array, accountId: Uint8Array) { - this.circuits.impure.revokeRole(roleId, accountId); + public revokeRole(role: Uint8Array, accountId: Uint8Array) { + this.circuits.impure.revokeRole(role, accountId); } /** @@ -124,8 +124,8 @@ export class ShieldedAccessControlSimulator extends ShieldedAccessControlSimulat * enforcing permission checks on the caller. * @param newOwnerId - The unique identifier of the new owner calculated by `SHA256(pk, nonce)`. */ - public renounceRole(roleId: Uint8Array, callerConfirmation: Uint8Array) { - this.circuits.impure.renounceRole(roleId, callerConfirmation); + public renounceRole(role: Uint8Array, callerConfirmation: Uint8Array) { + this.circuits.impure.renounceRole(role, callerConfirmation); } /** @@ -133,8 +133,8 @@ export class ShieldedAccessControlSimulator extends ShieldedAccessControlSimulat * enforcing permission checks on the caller. * @param newOwnerId - The unique identifier of the new owner calculated by `SHA256(pk, nonce)`. */ - public _setRoleAdmin(roleId: Uint8Array, adminRole: Uint8Array) { - this.circuits.impure._setRoleAdmin(roleId, adminRole); + public _setRoleAdmin(role: Uint8Array, adminRole: Uint8Array) { + this.circuits.impure._setRoleAdmin(role, adminRole); } /** @@ -142,8 +142,8 @@ export class ShieldedAccessControlSimulator extends ShieldedAccessControlSimulat * enforcing permission checks on the caller. * @param newOwnerId - The unique identifier of the new owner calculated by `SHA256(pk, nonce)`. */ - public _grantRole(roleId: Uint8Array, accountId: Uint8Array): boolean { - return this.circuits.impure._grantRole(roleId, accountId); + public _grantRole(role: Uint8Array, accountId: Uint8Array): boolean { + return this.circuits.impure._grantRole(role, accountId); } /** @@ -151,8 +151,8 @@ export class ShieldedAccessControlSimulator extends ShieldedAccessControlSimulat * enforcing permission checks on the caller. * @param newOwnerId - The unique identifier of the new owner calculated by `SHA256(pk, nonce)`. */ - public _revokeRole(roleId: Uint8Array, accountId: Uint8Array): boolean { - return this.circuits.impure._revokeRole(roleId, accountId); + public _revokeRole(role: Uint8Array, accountId: Uint8Array): boolean { + return this.circuits.impure._revokeRole(role, accountId); } public readonly privateState = { @@ -162,25 +162,25 @@ export class ShieldedAccessControlSimulator extends ShieldedAccessControlSimulat * @returns The ShieldedAccessControl private state after setting the new nonce. */ injectSecretNonce: ( - roleId: Uint8Array, + role: Uint8Array, newNonce: Buffer, ): ShieldedAccessControlPrivateState => { const currentState = this.getPrivateState(); const updatedState = { roles: { ...currentState.roles }, }; - const roleString = Buffer.from(roleId).toString('hex'); + const roleString = Buffer.from(role).toString('hex'); updatedState.roles[roleString] = newNonce; this.circuitContextManager.updatePrivateState(updatedState); return updatedState; }, /** - * @description Returns the secret nonce for a given roleId. + * @description Returns the secret nonce for a given role. * @returns The secret nonce. */ - getCurrentSecretNonce: (roleId: Uint8Array): Uint8Array => { - const roleString = Buffer.from(roleId).toString('hex'); + getCurrentSecretNonce: (role: Uint8Array): Uint8Array => { + const roleString = Buffer.from(role).toString('hex'); return this.getPrivateState().roles[roleString]; }, getCommitmentPathWithFindForLeaf: ( diff --git a/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts b/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts index 98960dda..e6dfcdaa 100644 --- a/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts +++ b/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts @@ -17,7 +17,7 @@ export interface IShieldedAccessControlWitnesses

{ */ wit_secretNonce( context: WitnessContext, - roleId: Uint8Array, + role: Uint8Array, ): [P, Uint8Array]; wit_getRoleCommitmentPath( context: WitnessContext, @@ -25,7 +25,7 @@ export interface IShieldedAccessControlWitnesses

{ ): [P, MerkleTreePath]; } -type RoleId = string; +type role = string; type SecretNonce = Uint8Array; /** @@ -34,7 +34,7 @@ type SecretNonce = Uint8Array; */ export type ShieldedAccessControlPrivateState = { /** @description A 32-byte secret nonce used as a privacy additive. */ - roles: Record; + roles: Record; }; /** @@ -42,7 +42,7 @@ export type ShieldedAccessControlPrivateState = { */ export const ShieldedAccessControlPrivateState = { /** - * @description Generates a new private state with a random secret nonce and a default roleId of 0. + * @description Generates a new private state with a random secret nonce and a default role of 0. * @returns A fresh ShieldedAccessControlPrivateState instance. */ generate: (): ShieldedAccessControlPrivateState => { @@ -67,19 +67,19 @@ export const ShieldedAccessControlPrivateState = { * ``` */ withRoleAndNonce: ( - roleId: Buffer, + role: Buffer, nonce: Buffer, ): ShieldedAccessControlPrivateState => { - const roleString = roleId.toString('hex'); + const roleString = role.toString('hex'); return { roles: { [roleString]: nonce } }; }, setRole: ( privateState: ShieldedAccessControlPrivateState, - roleId: Buffer, + role: Buffer, nonce: Buffer, ): ShieldedAccessControlPrivateState => { - const roleString = roleId.toString('hex'); + const roleString = role.toString('hex'); privateState.roles[roleString] = nonce; return privateState; }, @@ -111,9 +111,9 @@ export const ShieldedAccessControlWitnesses = (): IShieldedAccessControlWitnesses => ({ wit_secretNonce( context: WitnessContext, - roleId: Uint8Array, + role: Uint8Array, ): [ShieldedAccessControlPrivateState, Uint8Array] { - const roleString = Buffer.from(roleId).toString('hex'); + const roleString = Buffer.from(role).toString('hex'); return [context.privateState, context.privateState.roles[roleString]]; }, wit_getRoleCommitmentPath( From b77599345fc2b41d1bd85c4e28e5367792567014 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Wed, 11 Mar 2026 20:31:16 -0400 Subject: [PATCH 268/322] rm old tests --- .../access/test/ShieldedAccessControl_OLD.ts | 1150 ----------------- 1 file changed, 1150 deletions(-) delete mode 100644 contracts/src/access/test/ShieldedAccessControl_OLD.ts diff --git a/contracts/src/access/test/ShieldedAccessControl_OLD.ts b/contracts/src/access/test/ShieldedAccessControl_OLD.ts deleted file mode 100644 index 59607ddf..00000000 --- a/contracts/src/access/test/ShieldedAccessControl_OLD.ts +++ /dev/null @@ -1,1150 +0,0 @@ -// biome-ignore-all lint: will delete later - -import { - CompactTypeBytes, - CompactTypeVector, - convert_bigint_to_Uint8Array, - persistentHash, - type WitnessContext, -} from '@midnight-ntwrk/compact-runtime'; -import { beforeEach, describe, expect, it } from 'vitest'; -import { - type ContractAddress, - type Either, - type Ledger, - type MerkleTreePath, - Contract as MyContract, - type ShieldedAccessControl_Role as Role, - type ZswapCoinPublicKey, -} from '../../../artifacts/MockShieldedAccessControl/contract/index.cjs'; -import { - fmtHexString, - ShieldedAccessControlPrivateState, - ShieldedAccessControlWitnesses, -} from '../witnesses/ShieldedAccessControlWitnesses.js'; -import { ShieldedAccessControlSimulator } from './simulators/ShieldedAccessControlSimulator.js'; -import * as utils from './utils/address.js'; - -// PKs -const [ADMIN, Z_ADMIN] = utils.generateEitherPubKeyPair('ADMIN'); -const [UNAUTHORIZED, Z_UNAUTHORIZED] = - utils.generateEitherPubKeyPair('UNAUTHORIZED'); -const [CUSTOM_ADMIN, Z_CUSTOM_ADMIN] = - utils.generateEitherPubKeyPair('CUSTOM_ADMIN'); -const [OPERATOR_1, Z_OPERATOR_1] = utils.generateEitherPubKeyPair('OPERATOR_1'); -const [OPERATOR_2, Z_OPERATOR_2] = utils.generateEitherPubKeyPair('OPERATOR_2'); -const [OPERATOR_3, Z_OPERATOR_3] = utils.generateEitherPubKeyPair('OPERATOR_3'); -const [OPERATOR_CONTRACT, Z_OPERATOR_CONTRACT] = utils.generateEitherPubKeyPair( - 'OPERATOR_CONTRACT', - false, -); -const Z_OPERATOR_LIST = [Z_OPERATOR_1, Z_OPERATOR_2, Z_OPERATOR_3]; - -// Constants -const BAD_NONCE = Buffer.alloc(32, 'BAD_NONCE'); -const DOMAIN = new Uint8Array(32); -new TextEncoder().encodeInto('ShieldedAccessControl:shield:', DOMAIN); -const INIT_COUNTER = 0n; - -const EMPTY_ROOT = { field: 0n }; -const getRoleIndex = ( - { - ledger, - privateState, - }: WitnessContext, - roleId: Uint8Array, - account: Either, -): bigint => { - const roleIdString = Buffer.from(roleId).toString('hex'); - const bNonce = privateState.roles[roleIdString]; - const rt_type = new CompactTypeVector(5, new CompactTypeBytes(32)); - const bAccount = utils.eitherToBytes(account); - // Iterate over each MT index to determine if commitment exists - for (let i = 0; i < 2 ** 11 - 1; i++) { - const bIndex = convert_bigint_to_Uint8Array(32, BigInt(i)); - const commitment = persistentHash(rt_type, [ - roleId, - bAccount, - bNonce, - bIndex, - DOMAIN, - ]); - try { - ledger.ShieldedAccessControl__operatorRoles.pathForLeaf( - BigInt(i), - commitment, - ); - return BigInt(i); - } catch (e: unknown) { - if (e instanceof Error) { - const [msg, index] = e.message.split(':'); - if (msg === 'invalid index into sparse merkle tree') { - // console.log(`role ${fmtHexString(roleIdString)} with commitment ${fmtHexString(commitment)} not found at index ${index}`); - } else { - throw e; - } - } - } - } - - console.log( - 'WIT - Commitment DNE, returing MT index ', - ledger.ShieldedAccessControl__currentMerkleTreeIndex.toString(), - ); - - // If commitment doesn't exist return currentMTIndex - // Used for adding roles - return ledger.ShieldedAccessControl__currentMerkleTreeIndex; -}; - -// Roles -const DEFAULT_ADMIN_ROLE = utils.zeroUint8Array(); -const OPERATOR_ROLE_1 = convert_bigint_to_Uint8Array(32, 1n); -const OPERATOR_ROLE_2 = convert_bigint_to_Uint8Array(32, 2n); -const OPERATOR_ROLE_3 = convert_bigint_to_Uint8Array(32, 3n); -const CUSTOM_ADMIN_ROLE = convert_bigint_to_Uint8Array(32, 4n); -const UNINITIALIZED_ROLE = convert_bigint_to_Uint8Array(32, 5n); -const OPERATOR_ROLE_LIST = [OPERATOR_ROLE_1, OPERATOR_ROLE_2, OPERATOR_ROLE_3]; - -// Role to string -const DEFAULT_ADMIN_ROLE_TO_STRING = - Buffer.from(DEFAULT_ADMIN_ROLE).toString('hex'); - -const ADMIN_SECRET_NONCE = Buffer.alloc(32, 'ADMIN_SECRET_NONCE'); -const OPERATOR_ROLE_1_SECRET_NONCE = Buffer.alloc( - 32, - 'OPERATOR_ROLE_1_SECRET_NONCE', -); -const OPERATOR_ROLE_2_SECRET_NONCE = Buffer.alloc( - 32, - 'OPERATOR_ROLE_2_SECRET_NONCE', -); -const OPERATOR_ROLE_3_SECRET_NONCE = Buffer.alloc( - 32, - 'OPERATOR_ROLE_3_SECRET_NONCE', -); -const OPERATOR_ROLE_SECRET_NONCES = [ - OPERATOR_ROLE_1_SECRET_NONCE, - OPERATOR_ROLE_2_SECRET_NONCE, - OPERATOR_ROLE_3_SECRET_NONCE, -]; -let shieldedAccessControl: ShieldedAccessControlSimulator; - -// Helpers -const buildCommitment = ( - roleId: Uint8Array, - account: Either, - nonce: Uint8Array, -): Uint8Array => { - const rt_type = new CompactTypeVector(5, new CompactTypeBytes(32)); - const bAccount = utils.eitherToBytes(account); - - const commitment = persistentHash(rt_type, [roleId, bAccount, nonce, DOMAIN]); - - return commitment; -}; - -const EXP_DEFAULT_ADMIN_COMMITMENT = buildCommitment( - DEFAULT_ADMIN_ROLE, - Z_ADMIN, - ADMIN_SECRET_NONCE, -); - -function RETURN_BAD_INDEX( - context: WitnessContext, - roleId: Uint8Array, -): [ShieldedAccessControlPrivateState, bigint] { - return [context.privateState, 1023n]; -} - -function RETURN_BAD_PATH( - context: WitnessContext, - roleCommitment: Uint8Array, -): [ShieldedAccessControlPrivateState, MerkleTreePath] { - const defaultPath: MerkleTreePath = { - leaf: new Uint8Array(32), - path: Array.from({ length: 10 }, () => ({ - sibling: { field: 0n }, - goes_left: false, - })), - }; - return [context.privateState, defaultPath]; -} - -type RoleAndNonce = { - roleId: string; - nonce: Buffer; -}; - -describe('ShieldedAccessControl', () => { - beforeEach(() => { - // Create private state object and generate nonce - const PS = ShieldedAccessControlPrivateState.withRoleAndNonce( - Z_ADMIN, - Buffer.from(DEFAULT_ADMIN_ROLE), - ADMIN_SECRET_NONCE, - ); - // Init contract for user with PS - shieldedAccessControl = new ShieldedAccessControlSimulator(Z_ADMIN, { - privateState: PS, - }); - }); - - describe('checked circuits should fail for authorized caller with invalid witness values', () => { - beforeEach(() => { - shieldedAccessControl._grantRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); - shieldedAccessControl.callerCtx.setCaller(ADMIN); - }); - - type FailingCircuits = [ - method: keyof ShieldedAccessControlSimulator, - isValidNonce: boolean, - isValidIndex: boolean, - isValidPath: boolean, - args: unknown[], - ]; - const checkedCircuits: FailingCircuits[] = [ - ['assertOnlyRole', false, true, true, [DEFAULT_ADMIN_ROLE]], - ['assertOnlyRole', true, false, true, [DEFAULT_ADMIN_ROLE]], - ['assertOnlyRole', true, true, false, [DEFAULT_ADMIN_ROLE]], - ['assertOnlyRole', false, false, true, [DEFAULT_ADMIN_ROLE]], - ['assertOnlyRole', true, false, false, [DEFAULT_ADMIN_ROLE]], - ['assertOnlyRole', false, true, false, [DEFAULT_ADMIN_ROLE]], - ['assertOnlyRole', false, false, false, [DEFAULT_ADMIN_ROLE]], - ['grantRole', false, true, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ['grantRole', true, false, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ['grantRole', true, true, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ['grantRole', false, false, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ['grantRole', true, false, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ['grantRole', false, true, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ['grantRole', false, false, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ['revokeRole', false, true, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ['revokeRole', true, false, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ['revokeRole', true, true, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ['revokeRole', false, false, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ['revokeRole', true, false, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ['revokeRole', false, true, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ['revokeRole', false, false, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ]; - - it.each( - checkedCircuits, - )('%s should fail with isValidNonce(%s), isValidIndex(%s), isValidPath(%s)', (circuitName, isValidNonce, isValidIndex, isValidPath, args) => { - if (isValidNonce) { - // Check nonce matches - expect( - shieldedAccessControl.privateState.getCurrentSecretNonce( - DEFAULT_ADMIN_ROLE, - ), - ).toEqual(ADMIN_SECRET_NONCE); - } else { - // Check nonce does not match - shieldedAccessControl.privateState.injectSecretNonce( - DEFAULT_ADMIN_ROLE, - BAD_NONCE, - ); - expect( - shieldedAccessControl.privateState.getCurrentSecretNonce( - DEFAULT_ADMIN_ROLE, - ), - ).not.toEqual(ADMIN_SECRET_NONCE); - } - - if (isValidIndex) { - // Check index matches - const [, witnessCalculatedIndex] = - shieldedAccessControl.witnesses.wit_getRoleIndex( - shieldedAccessControl.getWitnessContext(), - DEFAULT_ADMIN_ROLE, - Z_ADMIN, - ); - expect(witnessCalculatedIndex).toBe(INIT_COUNTER); - } else { - // Check index does not match - shieldedAccessControl.overrideWitness( - 'wit_getRoleIndex', - RETURN_BAD_INDEX, - ); - const [, witnessCalculatedIndex] = - shieldedAccessControl.witnesses.wit_getRoleIndex( - shieldedAccessControl.getWitnessContext(), - DEFAULT_ADMIN_ROLE, - Z_ADMIN, - ); - expect(witnessCalculatedIndex).not.toBe(INIT_COUNTER); - } - - if (isValidPath) { - // Check path matches - const truePath = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.findPathForLeaf( - EXP_DEFAULT_ADMIN_COMMITMENT, - ); - const [, witnessCalculatedPath] = - shieldedAccessControl.witnesses.wit_getRoleCommitmentPath( - shieldedAccessControl.getWitnessContext(), - EXP_DEFAULT_ADMIN_COMMITMENT, - ); - expect(witnessCalculatedPath).toEqual(truePath); - } else { - // Check path does not match - const truePath = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.findPathForLeaf( - EXP_DEFAULT_ADMIN_COMMITMENT, - ); - shieldedAccessControl.overrideWitness( - 'wit_getRoleCommitmentPath', - RETURN_BAD_PATH, - ); - const [, witnessCalculatedPath] = - shieldedAccessControl.witnesses.wit_getRoleIndex( - shieldedAccessControl.getWitnessContext(), - DEFAULT_ADMIN_ROLE, - Z_ADMIN, - ); - expect(witnessCalculatedPath).not.toEqual(truePath); - } - - // Test protected circuit - expect(() => { - (shieldedAccessControl[circuitName] as (...args: unknown[]) => unknown)( - ...args, - ); - }).toThrow('ShieldedAccessControl: unauthorized account'); - }); - }); - - describe('checked circuits should fail for unauthorized caller with any witness value', () => { - beforeEach(() => { - shieldedAccessControl._grantRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); - shieldedAccessControl.callerCtx.setCaller(UNAUTHORIZED); - }); - - type FailingCircuits = [ - method: keyof ShieldedAccessControlSimulator, - isValidNonce: boolean, - isValidIndex: boolean, - isValidPath: boolean, - args: unknown[], - ]; - const checkedCircuits: FailingCircuits[] = [ - ['assertOnlyRole', false, true, true, [DEFAULT_ADMIN_ROLE]], - ['assertOnlyRole', true, false, true, [DEFAULT_ADMIN_ROLE]], - ['assertOnlyRole', true, true, false, [DEFAULT_ADMIN_ROLE]], - ['assertOnlyRole', false, false, true, [DEFAULT_ADMIN_ROLE]], - ['assertOnlyRole', true, false, false, [DEFAULT_ADMIN_ROLE]], - ['assertOnlyRole', false, true, false, [DEFAULT_ADMIN_ROLE]], - ['assertOnlyRole', false, false, false, [DEFAULT_ADMIN_ROLE]], - ['assertOnlyRole', true, true, true, [DEFAULT_ADMIN_ROLE]], - ['grantRole', false, true, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ['grantRole', true, false, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ['grantRole', true, true, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ['grantRole', false, false, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ['grantRole', true, false, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ['grantRole', false, true, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ['grantRole', false, false, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ['grantRole', true, true, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ['revokeRole', false, true, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ['revokeRole', true, false, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ['revokeRole', true, true, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ['revokeRole', false, false, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ['revokeRole', true, false, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ['revokeRole', false, true, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ['revokeRole', false, false, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ['revokeRole', true, true, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ]; - - it.each( - checkedCircuits, - )('%s should fail with isValidNonce(%s), isValidIndex(%s), isValidPath(%s)', (circuitName, isValidNonce, isValidIndex, isValidPath, args) => { - if (isValidNonce) { - // Check nonce matches - expect( - shieldedAccessControl.privateState.getCurrentSecretNonce( - DEFAULT_ADMIN_ROLE, - ), - ).toEqual(ADMIN_SECRET_NONCE); - } else { - // Check nonce does not match - shieldedAccessControl.privateState.injectSecretNonce( - DEFAULT_ADMIN_ROLE, - BAD_NONCE, - ); - expect( - shieldedAccessControl.privateState.getCurrentSecretNonce( - DEFAULT_ADMIN_ROLE, - ), - ).not.toEqual(ADMIN_SECRET_NONCE); - } - - if (isValidIndex) { - // Check index matches - const [, witnessCalculatedIndex] = - shieldedAccessControl.witnesses.wit_getRoleIndex( - shieldedAccessControl.getWitnessContext(), - DEFAULT_ADMIN_ROLE, - Z_ADMIN, - ); - expect(witnessCalculatedIndex).toBe(INIT_COUNTER); - } else { - // Check index does not match - shieldedAccessControl.overrideWitness( - 'wit_getRoleIndex', - RETURN_BAD_INDEX, - ); - const [, witnessCalculatedIndex] = - shieldedAccessControl.witnesses.wit_getRoleIndex( - shieldedAccessControl.getWitnessContext(), - DEFAULT_ADMIN_ROLE, - Z_ADMIN, - ); - expect(witnessCalculatedIndex).not.toBe(INIT_COUNTER); - } - - if (isValidPath) { - // Check path matches - const truePath = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.findPathForLeaf( - EXP_DEFAULT_ADMIN_COMMITMENT, - ); - const [, witnessCalculatedPath] = - shieldedAccessControl.witnesses.wit_getRoleCommitmentPath( - shieldedAccessControl.getWitnessContext(), - EXP_DEFAULT_ADMIN_COMMITMENT, - ); - expect(witnessCalculatedPath).toEqual(truePath); - } else { - // Check path does not match - const truePath = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.findPathForLeaf( - EXP_DEFAULT_ADMIN_COMMITMENT, - ); - shieldedAccessControl.overrideWitness( - 'wit_getRoleCommitmentPath', - RETURN_BAD_PATH, - ); - const [, witnessCalculatedPath] = - shieldedAccessControl.witnesses.wit_getRoleIndex( - shieldedAccessControl.getWitnessContext(), - DEFAULT_ADMIN_ROLE, - Z_ADMIN, - ); - expect(witnessCalculatedPath).not.toEqual(truePath); - } - - // Test protected circuit - expect(() => { - (shieldedAccessControl[circuitName] as (...args: unknown[]) => unknown)( - ...args, - ); - }).toThrow('ShieldedAccessControl: unauthorized account'); - }); - }); - - describe('unsupported contract address failure cases', () => { - beforeEach(() => { - shieldedAccessControl._grantRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); - shieldedAccessControl.callerCtx.setCaller(ADMIN); - }); - - type FailingCircuits = [ - method: keyof ShieldedAccessControlSimulator, - args: unknown[], - ]; - const circuitsWithContractAddressCheck: FailingCircuits[] = [ - ['hasRole', [DEFAULT_ADMIN_ROLE, Z_OPERATOR_CONTRACT]], - ['_checkRole', [DEFAULT_ADMIN_ROLE, Z_OPERATOR_CONTRACT]], - ['grantRole', [DEFAULT_ADMIN_ROLE, Z_OPERATOR_CONTRACT]], - ['revokeRole', [DEFAULT_ADMIN_ROLE, Z_OPERATOR_CONTRACT]], - ['_grantRole', [DEFAULT_ADMIN_ROLE, Z_OPERATOR_CONTRACT]], - ['_revokeRole', [DEFAULT_ADMIN_ROLE, Z_OPERATOR_CONTRACT]], - ]; - - it.each( - circuitsWithContractAddressCheck, - )('%s fails if contract address is queried', (circuitName, args) => { - // Test protected circuit - expect(() => { - (shieldedAccessControl[circuitName] as (...args: unknown[]) => unknown)( - ...args, - ); - }).toThrow( - 'ShieldedAccessControl: contract address roles are not yet supported', - ); - }); - }); - - describe('hasRole', () => { - beforeEach(() => { - shieldedAccessControl._grantRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); - }); - - type HasRoleTest = [ - isValidNonce: boolean, - isValidIndex: boolean, - isValidPath: boolean, - args: unknown[], - ]; - const falseCases: HasRoleTest[] = [ - [false, true, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - [true, false, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - [true, true, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - [false, false, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - [true, false, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - [false, true, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - [false, false, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ]; - - const commitmentDoesNotMatchCases: HasRoleTest[] = [ - [false, true, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - [true, false, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - [false, false, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - [true, false, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - [false, true, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - [false, false, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], - ]; - - it('should throw if caller is contract address', () => { - shieldedAccessControl.callerCtx.setCaller(OPERATOR_CONTRACT); - expect(() => { - shieldedAccessControl.hasRole(UNINITIALIZED_ROLE, Z_OPERATOR_CONTRACT); - }).toThrow( - 'ShieldedAccessControl: contract address roles are not yet supported', - ); - }); - - it('should return correct role commitment', () => { - const expCommitment = buildCommitment( - DEFAULT_ADMIN_ROLE, - Z_ADMIN, - ADMIN_SECRET_NONCE, - INIT_COUNTER, - ); - - const role = shieldedAccessControl.hasRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); - expect(role.roleCommitment).toEqual(expCommitment); - }); - - it('should return true when admin has role', () => { - const role = shieldedAccessControl.hasRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); - expect(role.isApproved).toEqual(true); - }); - - it('should return false when unauthorized does not have role', () => { - const role = shieldedAccessControl.hasRole( - DEFAULT_ADMIN_ROLE, - Z_UNAUTHORIZED, - ); - expect(role.isApproved).toEqual(false); - }); - - it('should return false when role does not exist', () => { - shieldedAccessControl.privateState.injectSecretNonce( - UNINITIALIZED_ROLE, - Buffer.alloc(32), - ); - const role = shieldedAccessControl.hasRole( - UNINITIALIZED_ROLE, - Z_UNAUTHORIZED, - ); - expect(role.isApproved).toBe(false); - }); - - it.each( - falseCases, - )('should return false with any invalid witness value - isValidNonce(%s), isValidIndex(%s), isValidPath(%s)', (isValidNonce, isValidIndex, isValidPath, args) => { - if (isValidNonce) { - // Check nonce matches - expect( - shieldedAccessControl.privateState.getCurrentSecretNonce( - DEFAULT_ADMIN_ROLE, - ), - ).toEqual(ADMIN_SECRET_NONCE); - } else { - // Check nonce does not match - shieldedAccessControl.privateState.injectSecretNonce( - DEFAULT_ADMIN_ROLE, - BAD_NONCE, - ); - expect( - shieldedAccessControl.privateState.getCurrentSecretNonce( - DEFAULT_ADMIN_ROLE, - ), - ).not.toEqual(ADMIN_SECRET_NONCE); - } - - if (isValidIndex) { - // Check index matches - const [, witnessCalculatedIndex] = - shieldedAccessControl.witnesses.wit_getRoleIndex( - shieldedAccessControl.getWitnessContext(), - DEFAULT_ADMIN_ROLE, - Z_ADMIN, - ); - expect(witnessCalculatedIndex).toBe(INIT_COUNTER); - } else { - // Check index does not match - shieldedAccessControl.overrideWitness( - 'wit_getRoleIndex', - RETURN_BAD_INDEX, - ); - const [, witnessCalculatedIndex] = - shieldedAccessControl.witnesses.wit_getRoleIndex( - shieldedAccessControl.getWitnessContext(), - DEFAULT_ADMIN_ROLE, - Z_ADMIN, - ); - expect(witnessCalculatedIndex).not.toBe(INIT_COUNTER); - } - - if (isValidPath) { - // Check path matches - const truePath = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.findPathForLeaf( - EXP_DEFAULT_ADMIN_COMMITMENT, - ); - const [, witnessCalculatedPath] = - shieldedAccessControl.witnesses.wit_getRoleCommitmentPath( - shieldedAccessControl.getWitnessContext(), - EXP_DEFAULT_ADMIN_COMMITMENT, - ); - expect(witnessCalculatedPath).toEqual(truePath); - } else { - // Check path does not match - const truePath = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.findPathForLeaf( - EXP_DEFAULT_ADMIN_COMMITMENT, - ); - shieldedAccessControl.overrideWitness( - 'wit_getRoleCommitmentPath', - RETURN_BAD_PATH, - ); - const [, witnessCalculatedPath] = - shieldedAccessControl.witnesses.wit_getRoleIndex( - shieldedAccessControl.getWitnessContext(), - DEFAULT_ADMIN_ROLE, - Z_ADMIN, - ); - expect(witnessCalculatedPath).not.toEqual(truePath); - } - - // Test false case circuit - const role = ( - shieldedAccessControl['hasRole'] as (...args: unknown[]) => Role - )(...args); - expect(role.isApproved).toBe(false); - }); - - it.each( - commitmentDoesNotMatchCases, - )('commitment should not match with invalid nonce or index - isValidNonce(%s), isValidIndex(%s), isValidPath(%s)', (isValidNonce, isValidIndex, isValidPath, args) => { - if (isValidNonce) { - // Check nonce matches - expect( - shieldedAccessControl.privateState.getCurrentSecretNonce( - DEFAULT_ADMIN_ROLE, - ), - ).toEqual(ADMIN_SECRET_NONCE); - } else { - // Check nonce does not match - shieldedAccessControl.privateState.injectSecretNonce( - DEFAULT_ADMIN_ROLE, - BAD_NONCE, - ); - expect( - shieldedAccessControl.privateState.getCurrentSecretNonce( - DEFAULT_ADMIN_ROLE, - ), - ).not.toEqual(ADMIN_SECRET_NONCE); - } - - if (isValidIndex) { - // Check index matches - const [, witnessCalculatedIndex] = - shieldedAccessControl.witnesses.wit_getRoleIndex( - shieldedAccessControl.getWitnessContext(), - DEFAULT_ADMIN_ROLE, - Z_ADMIN, - ); - expect(witnessCalculatedIndex).toBe(INIT_COUNTER); - } else { - // Check index does not match - shieldedAccessControl.overrideWitness( - 'wit_getRoleIndex', - RETURN_BAD_INDEX, - ); - const [, witnessCalculatedIndex] = - shieldedAccessControl.witnesses.wit_getRoleIndex( - shieldedAccessControl.getWitnessContext(), - DEFAULT_ADMIN_ROLE, - Z_ADMIN, - ); - expect(witnessCalculatedIndex).not.toBe(INIT_COUNTER); - } - - if (isValidPath) { - // Check path matches - const truePath = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.findPathForLeaf( - EXP_DEFAULT_ADMIN_COMMITMENT, - ); - const [, witnessCalculatedPath] = - shieldedAccessControl.witnesses.wit_getRoleCommitmentPath( - shieldedAccessControl.getWitnessContext(), - EXP_DEFAULT_ADMIN_COMMITMENT, - ); - expect(witnessCalculatedPath).toEqual(truePath); - } else { - // Check path does not match - const truePath = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.findPathForLeaf( - EXP_DEFAULT_ADMIN_COMMITMENT, - ); - shieldedAccessControl.overrideWitness( - 'wit_getRoleCommitmentPath', - RETURN_BAD_PATH, - ); - const [, witnessCalculatedPath] = - shieldedAccessControl.witnesses.wit_getRoleIndex( - shieldedAccessControl.getWitnessContext(), - DEFAULT_ADMIN_ROLE, - Z_ADMIN, - ); - expect(witnessCalculatedPath).not.toEqual(truePath); - } - - // Test false case circuit - const role = ( - shieldedAccessControl['hasRole'] as (...args: unknown[]) => Role - )(...args); - expect(role.roleCommitment).not.toEqual(EXP_DEFAULT_ADMIN_COMMITMENT); - }); - }); - - describe('assertOnlyRole', () => { - beforeEach(() => { - shieldedAccessControl._grantRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); - shieldedAccessControl.callerCtx.setCaller(ADMIN); - }); - - it('should not fail when authorized caller has correct nonce, index, and path', () => { - shieldedAccessControl.callerCtx.setCaller(OPERATOR_1); - shieldedAccessControl.assertOnlyRole(new Uint8Array(32).fill(1)); - // Check nonce is correct - expect( - shieldedAccessControl.privateState.getCurrentSecretNonce( - DEFAULT_ADMIN_ROLE, - ), - ).toBe(ADMIN_SECRET_NONCE); - - // Check index matches - const [, witnessCalculatedIndex] = - shieldedAccessControl.witnesses.wit_getRoleIndex( - shieldedAccessControl.getWitnessContext(), - DEFAULT_ADMIN_ROLE, - Z_ADMIN, - ); - expect(witnessCalculatedIndex).toBe(INIT_COUNTER); - - // Check path matches - const truePath = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.findPathForLeaf( - EXP_DEFAULT_ADMIN_COMMITMENT, - ); - const [, witnessCalculatedPath] = - shieldedAccessControl.witnesses.wit_getRoleCommitmentPath( - shieldedAccessControl.getWitnessContext(), - EXP_DEFAULT_ADMIN_COMMITMENT, - ); - expect(witnessCalculatedPath).toEqual(truePath); - - expect(() => - shieldedAccessControl.assertOnlyRole(DEFAULT_ADMIN_ROLE), - ).not.toThrow(); - }); - - it('should not fail for admin with multiple roles', () => { - shieldedAccessControl.privateState.injectSecretNonce( - OPERATOR_ROLE_1, - OPERATOR_ROLE_1_SECRET_NONCE, - ); - shieldedAccessControl.privateState.injectSecretNonce( - OPERATOR_ROLE_2, - OPERATOR_ROLE_2_SECRET_NONCE, - ); - shieldedAccessControl.privateState.injectSecretNonce( - OPERATOR_ROLE_3, - OPERATOR_ROLE_3_SECRET_NONCE, - ); - shieldedAccessControl._grantRole(OPERATOR_ROLE_1, Z_ADMIN); - shieldedAccessControl._grantRole(OPERATOR_ROLE_2, Z_ADMIN); - shieldedAccessControl._grantRole(OPERATOR_ROLE_3, Z_ADMIN); - expect(() => { - shieldedAccessControl.assertOnlyRole(DEFAULT_ADMIN_ROLE); - shieldedAccessControl.assertOnlyRole(OPERATOR_ROLE_1); - shieldedAccessControl.assertOnlyRole(OPERATOR_ROLE_2); - shieldedAccessControl.assertOnlyRole(OPERATOR_ROLE_3); - }).not.toThrow(); - }); - }); - - describe('_checkRole', () => { - beforeEach(() => { - shieldedAccessControl._grantRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); - }); - - it('should not throw if admin has role', () => { - shieldedAccessControl.callerCtx.setCaller(OPERATOR_1); - console.log( - 'ZswapState', - shieldedAccessControl.circuitContext.currentZswapLocalState, - ); - expect(() => - shieldedAccessControl._checkRole(DEFAULT_ADMIN_ROLE, Z_ADMIN), - ).not.toThrow(); - }); - - it('should throw if unauthorized does not have role', () => { - expect(() => - shieldedAccessControl._checkRole(DEFAULT_ADMIN_ROLE, Z_UNAUTHORIZED), - ).toThrow('ShieldedAccessControl: unauthorized account'); - }); - }); - - describe('getRoleAdmin', () => { - it('should return default admin role if admin role not set', () => { - expect(shieldedAccessControl.getRoleAdmin(OPERATOR_ROLE_1)).toEqual( - DEFAULT_ADMIN_ROLE, - ); - }); - - it('should return custom admin role if set', () => { - shieldedAccessControl._setRoleAdmin(OPERATOR_ROLE_1, CUSTOM_ADMIN_ROLE); - expect(shieldedAccessControl.getRoleAdmin(OPERATOR_ROLE_1)).toEqual( - CUSTOM_ADMIN_ROLE, - ); - }); - }); - - describe('grantRole', () => { - beforeEach(() => { - shieldedAccessControl._grantRole(DEFAULT_ADMIN_ROLE, Z_ADMIN); - shieldedAccessControl.callerCtx.setCaller(ADMIN); - }); - - it('admin should grant role', () => { - shieldedAccessControl.privateState.injectSecretNonce( - OPERATOR_ROLE_1, - OPERATOR_ROLE_1_SECRET_NONCE, - ); - shieldedAccessControl.grantRole(OPERATOR_ROLE_1, Z_OPERATOR_1); - const role: Role = shieldedAccessControl.hasRole( - OPERATOR_ROLE_1, - Z_OPERATOR_1, - ); - expect(role.isApproved).toBe(true); - }); - - it('path for role should exist in Merkle tree', () => { - expect( - shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.findPathForLeaf( - EXP_DEFAULT_ADMIN_COMMITMENT, - ), - ).toBeDefined(); - }); - - it('should update Merkle tree root', () => { - expect( - shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.root().field, - ).toBeGreaterThan(0n); - }); - - it('_currentMerkleTreeIndex should increment', () => { - // Starts at 1 because we grant role to self in beforeEach - expect( - shieldedAccessControl.getPublicState() - .ShieldedAccessControl__currentMerkleTreeIndex, - ).toBe(1n); - - shieldedAccessControl.privateState.injectSecretNonce( - OPERATOR_ROLE_1, - OPERATOR_ROLE_1_SECRET_NONCE, - ); - shieldedAccessControl.privateState.injectSecretNonce( - OPERATOR_ROLE_2, - OPERATOR_ROLE_2_SECRET_NONCE, - ); - shieldedAccessControl.privateState.injectSecretNonce( - OPERATOR_ROLE_3, - OPERATOR_ROLE_3_SECRET_NONCE, - ); - - shieldedAccessControl.grantRole(OPERATOR_ROLE_1, Z_OPERATOR_1); - expect( - shieldedAccessControl.getPublicState() - .ShieldedAccessControl__currentMerkleTreeIndex, - ).toBe(2n); - - shieldedAccessControl.grantRole(OPERATOR_ROLE_2, Z_OPERATOR_2); - expect( - shieldedAccessControl.getPublicState() - .ShieldedAccessControl__currentMerkleTreeIndex, - ).toBe(3n); - - shieldedAccessControl.grantRole(OPERATOR_ROLE_3, Z_OPERATOR_3); - expect( - shieldedAccessControl.getPublicState() - .ShieldedAccessControl__currentMerkleTreeIndex, - ).toBe(4n); - }); - - it('admin should grant multiple roles', () => { - for (let i = 0; i < OPERATOR_ROLE_LIST.length; i++) { - shieldedAccessControl.privateState.injectSecretNonce( - OPERATOR_ROLE_LIST[i], - OPERATOR_ROLE_SECRET_NONCES[i], - ); - for (let j = 0; j < Z_OPERATOR_LIST.length; j++) { - shieldedAccessControl.grantRole( - OPERATOR_ROLE_LIST[i], - Z_OPERATOR_LIST[j], - ); - const role: Role = shieldedAccessControl.hasRole( - OPERATOR_ROLE_LIST[i], - Z_OPERATOR_LIST[j], - ); - expect(role.isApproved).toBe(true); - - expect( - shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.findPathForLeaf( - EXP_DEFAULT_ADMIN_COMMITMENT, - ), - ).toBeDefined(); - } - } - }); - - it('should throw if non-admin operator grants role', () => { - shieldedAccessControl.privateState.injectSecretNonce( - OPERATOR_ROLE_1, - OPERATOR_ROLE_1_SECRET_NONCE, - ); - shieldedAccessControl._grantRole(OPERATOR_ROLE_1, Z_OPERATOR_1); - - shieldedAccessControl.callerCtx.setCaller(OPERATOR_1); - expect(() => { - shieldedAccessControl.grantRole(OPERATOR_ROLE_1, Z_UNAUTHORIZED); - }).toThrow('ShieldedAccessControl: unauthorized account'); - }); - }); - - describe('revokeRole', () => { - beforeEach(() => { - shieldedAccessControl.callerCtx.setCaller(ADMIN); - console.log( - 'TEST - Current MT Index', - shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__currentMerkleTreeIndex.toString(), - ); - console.log( - shieldedAccessControl._grantRole(DEFAULT_ADMIN_ROLE, Z_ADMIN), - ); - console.log( - shieldedAccessControl._grantRole(DEFAULT_ADMIN_ROLE, Z_ADMIN), - ); - console.log('TEST - ADMIN NONCE ', fmtHexString(ADMIN_SECRET_NONCE)); - console.log( - 'TEST - OP NONCE ', - fmtHexString(OPERATOR_ROLE_1_SECRET_NONCE), - ); - console.log( - 'TEST - Current MT Index', - shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__currentMerkleTreeIndex.toString(), - ); - shieldedAccessControl.privateState.injectSecretNonce( - OPERATOR_ROLE_1, - OPERATOR_ROLE_1_SECRET_NONCE, - ); - shieldedAccessControl.grantRole(OPERATOR_ROLE_1, Z_OPERATOR_1); - console.log( - 'TEST - Current MT Index', - shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__currentMerkleTreeIndex.toString(), - ); - }); - - it('admin should revoke role', () => { - expect( - shieldedAccessControl.hasRole(OPERATOR_ROLE_1, Z_OPERATOR_1).isApproved, - ).toBe(true); - shieldedAccessControl.revokeRole(OPERATOR_ROLE_1, Z_OPERATOR_1); - expect( - shieldedAccessControl.hasRole(OPERATOR_ROLE_1, Z_OPERATOR_1).isApproved, - ).toBe(false); - }); - - it('commitment should be in nullifier set', () => { - console.log( - 'TEST - Current MT Index', - shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__currentMerkleTreeIndex.toString(), - ); - const opRoleIndex = getRoleIndex( - { - ledger: shieldedAccessControl.getPublicState(), - privateState: shieldedAccessControl.getPrivateState(), - contractAddress: shieldedAccessControl.contractAddress, - }, - OPERATOR_ROLE_1, - Z_OPERATOR_1, - ); - const adminRoleIndex = getRoleIndex( - shieldedAccessControl.getWitnessContext(), - DEFAULT_ADMIN_ROLE, - Z_ADMIN, - ); - console.log('OPERATOR INDEX ', opRoleIndex.toString(10)); - console.log('ADMIN INDEX ', adminRoleIndex.toString(10)); - const expCommitmentOp = buildCommitment( - OPERATOR_ROLE_1, - Z_OPERATOR_1, - OPERATOR_ROLE_1_SECRET_NONCE, - 0n, - ); - const expCommitmentOp2 = buildCommitment( - OPERATOR_ROLE_1, - Z_OPERATOR_1, - OPERATOR_ROLE_1_SECRET_NONCE, - 0n, - ); - const pathToOp = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.findPathForLeaf(expCommitmentOp); - const pathToAdmin = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.findPathForLeaf( - EXP_DEFAULT_ADMIN_COMMITMENT, - ); - //console.log("PATH TO OP ", pathToOp); - //console.log("PATH TO ADMIN ", pathToAdmin); - - //console.log("EXPECTED COMMITMENT ", expCommitmentOp); - const contractCommit = shieldedAccessControl.hasRole( - OPERATOR_ROLE_1, - Z_OPERATOR_1, - ).roleCommitment; - //console.log("CONTRACT COMMITMENT ", contractCommit); - - shieldedAccessControl.revokeRole(OPERATOR_ROLE_1, Z_OPERATOR_1); - const it = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl_sanity[Symbol.iterator](); - console.log(EXP_DEFAULT_ADMIN_COMMITMENT); - console.log(it.next()); - console.log( - shieldedAccessControl - .getPublicState() - .ShieldedAccessControl_sanity.member(EXP_DEFAULT_ADMIN_COMMITMENT), - ); - console.log(expCommitmentOp); - console.log(it.next()); - console.log( - shieldedAccessControl - .getPublicState() - .ShieldedAccessControl_sanity.member(expCommitmentOp), - ); - expect( - shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.isEmpty(), - ).toBe(false); - expect( - shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.member( - expCommitmentOp, - ), - ).toBe(true); - }); - - it('admin should revoke multiple roles', () => { - const expCommitment = buildCommitment( - OPERATOR_ROLE_1, - Z_OPERATOR_1, - OPERATOR_ROLE_1_SECRET_NONCE, - 1n, - ); - shieldedAccessControl.revokeRole(OPERATOR_ROLE_1, Z_OPERATOR_1); - expect( - shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.member( - expCommitment, - ), - ).toBe(true); - - for (let i = 1; i < OPERATOR_ROLE_LIST.length; i++) { - shieldedAccessControl.privateState.injectSecretNonce( - OPERATOR_ROLE_LIST[i], - OPERATOR_ROLE_SECRET_NONCES[i], - ); - for (let j = 1; j < Z_OPERATOR_LIST.length; j++) { - shieldedAccessControl._grantRole( - OPERATOR_ROLE_LIST[i], - Z_OPERATOR_LIST[j], - ); - const expCommitment = buildCommitment( - OPERATOR_ROLE_LIST[i], - Z_OPERATOR_LIST[j], - OPERATOR_ROLE_SECRET_NONCES[i], - BigInt(1 + i), - ); - shieldedAccessControl.revokeRole( - OPERATOR_ROLE_LIST[i], - Z_OPERATOR_LIST[j], - ); - expect( - shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.member( - expCommitment, - ), - ).toBe(true); - } - } - }); - - it('should throw if non-admin operator revokes role', () => { - shieldedAccessControl.privateState.injectSecretNonce( - OPERATOR_ROLE_1, - OPERATOR_ROLE_1_SECRET_NONCE, - ); - shieldedAccessControl._grantRole(OPERATOR_ROLE_1, Z_OPERATOR_1); - - shieldedAccessControl.callerCtx.setCaller(OPERATOR_1); - expect(() => { - shieldedAccessControl.revokeRole(OPERATOR_ROLE_1, Z_UNAUTHORIZED); - }).toThrow('ShieldedAccessControl: unauthorized account'); - }); - }); -}); From 196843e41ce70f4cc51aaeb23814fa6623edd00c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Thu, 12 Mar 2026 14:23:06 -0400 Subject: [PATCH 269/322] Simplify internal circuit signature --- .../src/access/ShieldedAccessControl.compact | 18 +++----- .../access/test/ShieldedAccessControl.test.ts | 44 ++++++++----------- .../mocks/MockShieldedAccessControl.compact | 18 ++++---- .../ShieldedAccessControlSimulator.ts | 5 +-- 4 files changed, 36 insertions(+), 49 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index d9993702..affc8004 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -240,8 +240,7 @@ module ShieldedAccessControl { export circuit proveCallerRole(role: RoleIdentifier): Boolean { Initializable_assertInitialized(); - const nonce = wit_secretNonce(role); - const accountId = _computeAccountId(ownPublicKey(), nonce); + const accountId = _computeAccountId(role); return _validateRole(role, accountId); } @@ -477,8 +476,7 @@ module ShieldedAccessControl { export circuit renounceRole(role: RoleIdentifier, accountIdConfirmation: AccountIdentifier): [] { Initializable_assertInitialized(); - const nonce = wit_secretNonce(role); - assert(accountIdConfirmation == _computeAccountId(ownPublicKey(), nonce), + assert(accountIdConfirmation == _computeAccountId(role), "ShieldedAccessControl: bad confirmation" ); @@ -618,15 +616,13 @@ module ShieldedAccessControl { } /** - * @description Computes the unique identifier (`accountId`) of an account from their + * @description Computes the unique identifier (`accountId`) of a caller from their * ZswapCoinPublicKey and a secret nonce. * * ## ID Derivation * `accountId = SHA256(zcpk, nonce, instanceSalt, accountIdDomain)` * - * - `zcpk`: The ZswapCoinPublicKey of the caller. This is passed explicitly to allow - * for off-chain derivation, testing, or scenarios where the caller is - * different from the subject of the computation. We recommend using an Air-Gapped Public Key. + * - `zcpk`: The ZswapCoinPublicKey of the caller. We recommend using an Air-Gapped Public Key. * - `nonce`: A secret nonce tied to the identity. The generation strategy is * left to the user, offering different security/convenience trade-offs. * - `instanceSalt`: A unique per-deployment salt, stored during initialization. @@ -645,11 +641,11 @@ module ShieldedAccessControl { * * @returns {Bytes<32>} accountId - The computed account ID. */ - circuit _computeAccountId(zcpk: ZswapCoinPublicKey, nonce: Bytes<32>): AccountIdentifier { + circuit _computeAccountId(role: RoleIdentifier): AccountIdentifier { Initializable_assertInitialized(); - return persistentHash>>( - [zcpk.bytes, nonce, _instanceSalt, pad(32, "ShieldedAccessControl:accountId")] + return persistentHash>>( + [ownPublicKey().bytes, wit_secretNonce(role), _instanceSalt, pad(32, "ShieldedAccessControl:accountId")] ) as AccountIdentifier; } diff --git a/contracts/src/access/test/ShieldedAccessControl.test.ts b/contracts/src/access/test/ShieldedAccessControl.test.ts index e90a8712..dd4de193 100644 --- a/contracts/src/access/test/ShieldedAccessControl.test.ts +++ b/contracts/src/access/test/ShieldedAccessControl.test.ts @@ -138,7 +138,7 @@ describe('ShieldedAccessControl', () => { ], [ '_computeAccountId', - [UNINITIALIZED.zPublicKey, UNINITIALIZED.accountId], + [UNINITIALIZED.role], ], ]; it.each(circuitsToFail)('%s should fail', (circuitName, args) => { @@ -233,38 +233,30 @@ describe('ShieldedAccessControl', () => { }); describe('_computeAccountId', () => { - it('should match account id', () => { + beforeEach(() => { + shieldedAccessControl.setPersistentCaller(ADMIN.publicKey); + }); + + it('should match when authorized caller with correct nonce', () => { expect( shieldedAccessControl._computeAccountId( - ADMIN.zPublicKey, - ADMIN.secretNonce, + ADMIN.role, ), ).toEqual(ADMIN.accountId); }); - type ComputeAccountIdCases = [ - isValidAccount: boolean, - isValidNonce: boolean, - args: unknown[], - ]; - - const checkedCircuits: ComputeAccountIdCases[] = [ - [true, false, [ADMIN.zPublicKey, UNAUTHORIZED.secretNonce]], - [false, true, [UNAUTHORIZED.zPublicKey, ADMIN.secretNonce]], - [false, false, [UNAUTHORIZED.zPublicKey, UNAUTHORIZED.secretNonce]], - ]; + it('should not match when authorized caller with bad nonce', () => { + shieldedAccessControl.privateState.injectSecretNonce(ADMIN.role, BAD_INPUT.secretNonce) + const computedAccountId = shieldedAccessControl._computeAccountId(ADMIN.role) + expect(computedAccountId).not.toEqual(ADMIN.accountId); + expect(computedAccountId).toEqual(buildAccountIdHash(ADMIN.zPublicKey, BAD_INPUT.secretNonce)); + }); - it.each( - checkedCircuits, - )('should not match account id with isValidAccount=%s or isValidNonce=%s', (_isValidAccount, _isValidNonce, args) => { - // Test circuit - expect(() => { - ( - shieldedAccessControl._computeAccountId as ( - ...args: unknown[] - ) => Uint8Array - )(...args); - }).not.toEqual(ADMIN.accountId); + it('should not match when unauthorized caller with correct nonce', () => { + shieldedAccessControl.as(UNAUTHORIZED.publicKey); + const computedAccountId = shieldedAccessControl._computeAccountId(ADMIN.role); + expect(computedAccountId).not.toEqual(ADMIN.accountId); + expect(computedAccountId).toEqual(buildAccountIdHash(UNAUTHORIZED.zPublicKey, ADMIN.secretNonce)); }); }); diff --git a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact index 2540134a..8b2b8fb7 100644 --- a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact +++ b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact @@ -18,11 +18,13 @@ export { ZswapCoinPublicKey, ShieldedAccessControl__roleCommitmentNullifiers, ShieldedAccessControl__adminRoles }; -// witness is re-implemented in the Mock contract for testing +// witnesses are re-implemented in the Mock contract for testing witness wit_getRoleCommitmentPath( roleCommitment: ShieldedAccessControl_RoleCommitment ): MerkleTreePath<20, ShieldedAccessControl_RoleCommitment>; +witness wit_secretNonce(role: ShieldedAccessControl_RoleIdentifier): Bytes<32>; + /** * @description `isInit` is a param for testing. * @@ -53,18 +55,16 @@ export circuit _computeRoleCommitment( } // circuit is reimplemented in the Mock contract for testing -export circuit _computeAccountId( - zcpk: ZswapCoinPublicKey, - nonce: Bytes<32> - ): ShieldedAccessControl_AccountIdentifier { +export circuit _computeAccountId(role: ShieldedAccessControl_RoleIdentifier): ShieldedAccessControl_AccountIdentifier { Initializable_assertInitialized(); - return persistentHash>>( - [zcpk.bytes, - nonce, + // disclosure required here for testing + return disclose(persistentHash>>( + [ownPublicKey().bytes, + wit_secretNonce(role), ShieldedAccessControl__instanceSalt, pad(32, "ShieldedAccessControl:accountId")] - ) + )) as ShieldedAccessControl_AccountIdentifier; } diff --git a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts index c27285d4..3d5f393b 100644 --- a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts +++ b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts @@ -61,10 +61,9 @@ export class ShieldedAccessControlSimulator extends ShieldedAccessControlSimulat } public _computeAccountId( - zcpk: ZswapCoinPublicKey, - nonce: Uint8Array, + role: Uint8Array, ): Uint8Array { - return this.circuits.impure._computeAccountId(zcpk, nonce); + return this.circuits.impure._computeAccountId(role); } public _computeNullifier(roleCommitment: Uint8Array): Uint8Array { From 1900f045b5a6371956a1ff945d9eac24392333fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Thu, 12 Mar 2026 18:00:13 -0400 Subject: [PATCH 270/322] Remove initialization checks from internal circuits --- .../src/access/ShieldedAccessControl.compact | 29 ++++++++++++++----- .../access/test/ShieldedAccessControl.test.ts | 27 +++++++++++------ .../mocks/MockShieldedAccessControl.compact | 18 +++++------- 3 files changed, 48 insertions(+), 26 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index affc8004..e384e707 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -508,6 +508,11 @@ module ShieldedAccessControl { * @description Verifies whether `accountId` holds `role`. This circuit MAY return false for a * legitimately credentialed account if the proving environment supplies an invalid Merkle path. * + * @warning This circuit does not perform an initialization check. It's only meant to be used as + * an internal helper in the Shielded Access Control module. Using this circuit outside of the + * module may cause undefined behavior and break security guarantees. + * + * * @circuitInfo k=14, rows=16089 * * Requirements: @@ -526,8 +531,6 @@ module ShieldedAccessControl { * @return {Boolean} isValidRole - A boolean indicating whether `accountId` has a valid role */ circuit _validateRole(role: RoleIdentifier, accountId: AccountIdentifier): Boolean { - Initializable_assertInitialized(); - const roleCommitment = _computeRoleCommitment(role, accountId); const roleCommitmentPath = wit_getRoleCommitmentPath(roleCommitment); const isValidPath = @@ -551,6 +554,10 @@ module ShieldedAccessControl { /** * @description Computes the role commitment from the given `accountId` and `role`. * + * @warning This circuit does not perform an initialization check. It's only meant to be used as + * an internal helper in the Shielded Access Control module. Using this circuit outside of the + * module may cause undefined behavior and break security guarantees. + * * ## Account ID (`accountId`) * The `accountId` is expected to be computed off-chain as: * `accountId = SHA256(zcpk, nonce, instanceSalt, accountIdDomain)` @@ -583,7 +590,6 @@ module ShieldedAccessControl { role: RoleIdentifier, accountId: AccountIdentifier, ): RoleCommitment { - Initializable_assertInitialized(); return persistentHash>>( [role as Bytes<32>, @@ -597,6 +603,10 @@ module ShieldedAccessControl { /** * @description Computes the role nullifier for a given `roleCommitment`. * + * @warning This circuit does not perform an initialization check. It's only meant to be used as + * an internal helper in the Shielded Access Control module. Using this circuit outside of the + * module may cause undefined behavior and break security guarantees. + * * ## Role Nullifier Derivation * `roleNullifier = SHA256(roleCommitment, nullifierDomain)` * @@ -619,6 +629,10 @@ module ShieldedAccessControl { * @description Computes the unique identifier (`accountId`) of a caller from their * ZswapCoinPublicKey and a secret nonce. * + * @warning This circuit does not perform an initialization check. It's only meant to be used as + * an internal helper in the Shielded Access Control module. Using this circuit outside of the + * module may cause undefined behavior and break security guarantees. + * * ## ID Derivation * `accountId = SHA256(zcpk, nonce, instanceSalt, accountIdDomain)` * @@ -642,10 +656,11 @@ module ShieldedAccessControl { * @returns {Bytes<32>} accountId - The computed account ID. */ circuit _computeAccountId(role: RoleIdentifier): AccountIdentifier { - Initializable_assertInitialized(); - - return persistentHash>>( - [ownPublicKey().bytes, wit_secretNonce(role), _instanceSalt, pad(32, "ShieldedAccessControl:accountId")] + return persistentHash>>( + [ownPublicKey().bytes, + wit_secretNonce(role), + _instanceSalt, + pad(32, "ShieldedAccessControl:accountId")] ) as AccountIdentifier; } diff --git a/contracts/src/access/test/ShieldedAccessControl.test.ts b/contracts/src/access/test/ShieldedAccessControl.test.ts index dd4de193..31df9e0a 100644 --- a/contracts/src/access/test/ShieldedAccessControl.test.ts +++ b/contracts/src/access/test/ShieldedAccessControl.test.ts @@ -124,7 +124,6 @@ describe('ShieldedAccessControl', () => { const circuitsToFail: FailingCircuits[] = [ ['proveCallerRole', [UNINITIALIZED.role]], ['assertOnlyRole', [UNINITIALIZED.role]], - ['_validateRole', [UNINITIALIZED.role, UNINITIALIZED.accountId]], ['getRoleAdmin', [UNINITIALIZED.role]], ['grantRole', [UNINITIALIZED.role, UNINITIALIZED.accountId]], ['revokeRole', [UNINITIALIZED.role, UNINITIALIZED.accountId]], @@ -132,14 +131,6 @@ describe('ShieldedAccessControl', () => { ['_setRoleAdmin', [UNINITIALIZED.role, UNINITIALIZED.role]], ['_grantRole', [UNINITIALIZED.role, UNINITIALIZED.accountId]], ['_revokeRole', [UNINITIALIZED.role, UNINITIALIZED.accountId]], - [ - '_computeRoleCommitment', - [UNINITIALIZED.role, UNINITIALIZED.accountId], - ], - [ - '_computeAccountId', - [UNINITIALIZED.role], - ], ]; it.each(circuitsToFail)('%s should fail', (circuitName, args) => { expect(() => { @@ -155,6 +146,24 @@ describe('ShieldedAccessControl', () => { }).not.toThrow(); }); + it('should allow unchecked _computeAccountId', () => { + expect(() => { + shieldedAccessControl._computeAccountId(ADMIN.role); + }).not.toThrow(); + }) + + it('should allow unchecked _computeRoleCommitment', () => { + expect(() => { + shieldedAccessControl._computeRoleCommitment(ADMIN.role, ADMIN.accountId); + }).not.toThrow(); + }); + + it('should allow unchecked _validateRole', () => { + expect(() => { + shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId); + }).not.toThrow(); + }) + it('should fail with 0 instanceSalt', () => { const isInit = true; expect(() => { diff --git a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact index 8b2b8fb7..6e52b207 100644 --- a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact +++ b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact @@ -43,7 +43,6 @@ export circuit _computeRoleCommitment( roleId: ShieldedAccessControl_RoleIdentifier, accountId: ShieldedAccessControl_AccountIdentifier ): ShieldedAccessControl_RoleCommitment { - Initializable_assertInitialized(); return persistentHash>>( [roleId as Bytes<32>, @@ -55,16 +54,17 @@ export circuit _computeRoleCommitment( } // circuit is reimplemented in the Mock contract for testing -export circuit _computeAccountId(role: ShieldedAccessControl_RoleIdentifier): ShieldedAccessControl_AccountIdentifier { - Initializable_assertInitialized(); +export circuit _computeAccountId( + role: ShieldedAccessControl_RoleIdentifier + ): ShieldedAccessControl_AccountIdentifier { // disclosure required here for testing return disclose(persistentHash>>( - [ownPublicKey().bytes, - wit_secretNonce(role), - ShieldedAccessControl__instanceSalt, - pad(32, "ShieldedAccessControl:accountId")] - )) + [ownPublicKey().bytes, + wit_secretNonce(role), + ShieldedAccessControl__instanceSalt, + pad(32, "ShieldedAccessControl:accountId")] + )) as ShieldedAccessControl_AccountIdentifier; } @@ -91,8 +91,6 @@ export circuit _validateRole( role: ShieldedAccessControl_RoleIdentifier, accountId: ShieldedAccessControl_AccountIdentifier ): Boolean { - Initializable_assertInitialized(); - const roleCommitment = _computeRoleCommitment(role, accountId); const roleCommitmentPath = wit_getRoleCommitmentPath(roleCommitment); const isValidPath = From fcb9a16c8319defc6c733dbc6d94368f28c8d93e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Thu, 12 Mar 2026 18:12:01 -0400 Subject: [PATCH 271/322] Revert "Remove initialization checks from internal circuits" This reverts commit 1900f045b5a6371956a1ff945d9eac24392333fe. --- .../src/access/ShieldedAccessControl.compact | 29 +++++-------------- .../access/test/ShieldedAccessControl.test.ts | 27 ++++++----------- .../mocks/MockShieldedAccessControl.compact | 18 +++++++----- 3 files changed, 26 insertions(+), 48 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index e384e707..affc8004 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -508,11 +508,6 @@ module ShieldedAccessControl { * @description Verifies whether `accountId` holds `role`. This circuit MAY return false for a * legitimately credentialed account if the proving environment supplies an invalid Merkle path. * - * @warning This circuit does not perform an initialization check. It's only meant to be used as - * an internal helper in the Shielded Access Control module. Using this circuit outside of the - * module may cause undefined behavior and break security guarantees. - * - * * @circuitInfo k=14, rows=16089 * * Requirements: @@ -531,6 +526,8 @@ module ShieldedAccessControl { * @return {Boolean} isValidRole - A boolean indicating whether `accountId` has a valid role */ circuit _validateRole(role: RoleIdentifier, accountId: AccountIdentifier): Boolean { + Initializable_assertInitialized(); + const roleCommitment = _computeRoleCommitment(role, accountId); const roleCommitmentPath = wit_getRoleCommitmentPath(roleCommitment); const isValidPath = @@ -554,10 +551,6 @@ module ShieldedAccessControl { /** * @description Computes the role commitment from the given `accountId` and `role`. * - * @warning This circuit does not perform an initialization check. It's only meant to be used as - * an internal helper in the Shielded Access Control module. Using this circuit outside of the - * module may cause undefined behavior and break security guarantees. - * * ## Account ID (`accountId`) * The `accountId` is expected to be computed off-chain as: * `accountId = SHA256(zcpk, nonce, instanceSalt, accountIdDomain)` @@ -590,6 +583,7 @@ module ShieldedAccessControl { role: RoleIdentifier, accountId: AccountIdentifier, ): RoleCommitment { + Initializable_assertInitialized(); return persistentHash>>( [role as Bytes<32>, @@ -603,10 +597,6 @@ module ShieldedAccessControl { /** * @description Computes the role nullifier for a given `roleCommitment`. * - * @warning This circuit does not perform an initialization check. It's only meant to be used as - * an internal helper in the Shielded Access Control module. Using this circuit outside of the - * module may cause undefined behavior and break security guarantees. - * * ## Role Nullifier Derivation * `roleNullifier = SHA256(roleCommitment, nullifierDomain)` * @@ -629,10 +619,6 @@ module ShieldedAccessControl { * @description Computes the unique identifier (`accountId`) of a caller from their * ZswapCoinPublicKey and a secret nonce. * - * @warning This circuit does not perform an initialization check. It's only meant to be used as - * an internal helper in the Shielded Access Control module. Using this circuit outside of the - * module may cause undefined behavior and break security guarantees. - * * ## ID Derivation * `accountId = SHA256(zcpk, nonce, instanceSalt, accountIdDomain)` * @@ -656,11 +642,10 @@ module ShieldedAccessControl { * @returns {Bytes<32>} accountId - The computed account ID. */ circuit _computeAccountId(role: RoleIdentifier): AccountIdentifier { - return persistentHash>>( - [ownPublicKey().bytes, - wit_secretNonce(role), - _instanceSalt, - pad(32, "ShieldedAccessControl:accountId")] + Initializable_assertInitialized(); + + return persistentHash>>( + [ownPublicKey().bytes, wit_secretNonce(role), _instanceSalt, pad(32, "ShieldedAccessControl:accountId")] ) as AccountIdentifier; } diff --git a/contracts/src/access/test/ShieldedAccessControl.test.ts b/contracts/src/access/test/ShieldedAccessControl.test.ts index 31df9e0a..dd4de193 100644 --- a/contracts/src/access/test/ShieldedAccessControl.test.ts +++ b/contracts/src/access/test/ShieldedAccessControl.test.ts @@ -124,6 +124,7 @@ describe('ShieldedAccessControl', () => { const circuitsToFail: FailingCircuits[] = [ ['proveCallerRole', [UNINITIALIZED.role]], ['assertOnlyRole', [UNINITIALIZED.role]], + ['_validateRole', [UNINITIALIZED.role, UNINITIALIZED.accountId]], ['getRoleAdmin', [UNINITIALIZED.role]], ['grantRole', [UNINITIALIZED.role, UNINITIALIZED.accountId]], ['revokeRole', [UNINITIALIZED.role, UNINITIALIZED.accountId]], @@ -131,6 +132,14 @@ describe('ShieldedAccessControl', () => { ['_setRoleAdmin', [UNINITIALIZED.role, UNINITIALIZED.role]], ['_grantRole', [UNINITIALIZED.role, UNINITIALIZED.accountId]], ['_revokeRole', [UNINITIALIZED.role, UNINITIALIZED.accountId]], + [ + '_computeRoleCommitment', + [UNINITIALIZED.role, UNINITIALIZED.accountId], + ], + [ + '_computeAccountId', + [UNINITIALIZED.role], + ], ]; it.each(circuitsToFail)('%s should fail', (circuitName, args) => { expect(() => { @@ -146,24 +155,6 @@ describe('ShieldedAccessControl', () => { }).not.toThrow(); }); - it('should allow unchecked _computeAccountId', () => { - expect(() => { - shieldedAccessControl._computeAccountId(ADMIN.role); - }).not.toThrow(); - }) - - it('should allow unchecked _computeRoleCommitment', () => { - expect(() => { - shieldedAccessControl._computeRoleCommitment(ADMIN.role, ADMIN.accountId); - }).not.toThrow(); - }); - - it('should allow unchecked _validateRole', () => { - expect(() => { - shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId); - }).not.toThrow(); - }) - it('should fail with 0 instanceSalt', () => { const isInit = true; expect(() => { diff --git a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact index 6e52b207..8b2b8fb7 100644 --- a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact +++ b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact @@ -43,6 +43,7 @@ export circuit _computeRoleCommitment( roleId: ShieldedAccessControl_RoleIdentifier, accountId: ShieldedAccessControl_AccountIdentifier ): ShieldedAccessControl_RoleCommitment { + Initializable_assertInitialized(); return persistentHash>>( [roleId as Bytes<32>, @@ -54,17 +55,16 @@ export circuit _computeRoleCommitment( } // circuit is reimplemented in the Mock contract for testing -export circuit _computeAccountId( - role: ShieldedAccessControl_RoleIdentifier - ): ShieldedAccessControl_AccountIdentifier { +export circuit _computeAccountId(role: ShieldedAccessControl_RoleIdentifier): ShieldedAccessControl_AccountIdentifier { + Initializable_assertInitialized(); // disclosure required here for testing return disclose(persistentHash>>( - [ownPublicKey().bytes, - wit_secretNonce(role), - ShieldedAccessControl__instanceSalt, - pad(32, "ShieldedAccessControl:accountId")] - )) + [ownPublicKey().bytes, + wit_secretNonce(role), + ShieldedAccessControl__instanceSalt, + pad(32, "ShieldedAccessControl:accountId")] + )) as ShieldedAccessControl_AccountIdentifier; } @@ -91,6 +91,8 @@ export circuit _validateRole( role: ShieldedAccessControl_RoleIdentifier, accountId: ShieldedAccessControl_AccountIdentifier ): Boolean { + Initializable_assertInitialized(); + const roleCommitment = _computeRoleCommitment(role, accountId); const roleCommitmentPath = wit_getRoleCommitmentPath(roleCommitment); const isValidPath = From fbe1df83aa6e16678b6132fe4972ab037c536540 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Thu, 12 Mar 2026 18:24:57 -0400 Subject: [PATCH 272/322] Reapply "Remove initialization checks from internal circuits" This reverts commit fcb9a16c8319defc6c733dbc6d94368f28c8d93e. --- .../src/access/ShieldedAccessControl.compact | 29 ++++++++++++++----- .../access/test/ShieldedAccessControl.test.ts | 27 +++++++++++------ .../mocks/MockShieldedAccessControl.compact | 18 +++++------- 3 files changed, 48 insertions(+), 26 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index affc8004..e384e707 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -508,6 +508,11 @@ module ShieldedAccessControl { * @description Verifies whether `accountId` holds `role`. This circuit MAY return false for a * legitimately credentialed account if the proving environment supplies an invalid Merkle path. * + * @warning This circuit does not perform an initialization check. It's only meant to be used as + * an internal helper in the Shielded Access Control module. Using this circuit outside of the + * module may cause undefined behavior and break security guarantees. + * + * * @circuitInfo k=14, rows=16089 * * Requirements: @@ -526,8 +531,6 @@ module ShieldedAccessControl { * @return {Boolean} isValidRole - A boolean indicating whether `accountId` has a valid role */ circuit _validateRole(role: RoleIdentifier, accountId: AccountIdentifier): Boolean { - Initializable_assertInitialized(); - const roleCommitment = _computeRoleCommitment(role, accountId); const roleCommitmentPath = wit_getRoleCommitmentPath(roleCommitment); const isValidPath = @@ -551,6 +554,10 @@ module ShieldedAccessControl { /** * @description Computes the role commitment from the given `accountId` and `role`. * + * @warning This circuit does not perform an initialization check. It's only meant to be used as + * an internal helper in the Shielded Access Control module. Using this circuit outside of the + * module may cause undefined behavior and break security guarantees. + * * ## Account ID (`accountId`) * The `accountId` is expected to be computed off-chain as: * `accountId = SHA256(zcpk, nonce, instanceSalt, accountIdDomain)` @@ -583,7 +590,6 @@ module ShieldedAccessControl { role: RoleIdentifier, accountId: AccountIdentifier, ): RoleCommitment { - Initializable_assertInitialized(); return persistentHash>>( [role as Bytes<32>, @@ -597,6 +603,10 @@ module ShieldedAccessControl { /** * @description Computes the role nullifier for a given `roleCommitment`. * + * @warning This circuit does not perform an initialization check. It's only meant to be used as + * an internal helper in the Shielded Access Control module. Using this circuit outside of the + * module may cause undefined behavior and break security guarantees. + * * ## Role Nullifier Derivation * `roleNullifier = SHA256(roleCommitment, nullifierDomain)` * @@ -619,6 +629,10 @@ module ShieldedAccessControl { * @description Computes the unique identifier (`accountId`) of a caller from their * ZswapCoinPublicKey and a secret nonce. * + * @warning This circuit does not perform an initialization check. It's only meant to be used as + * an internal helper in the Shielded Access Control module. Using this circuit outside of the + * module may cause undefined behavior and break security guarantees. + * * ## ID Derivation * `accountId = SHA256(zcpk, nonce, instanceSalt, accountIdDomain)` * @@ -642,10 +656,11 @@ module ShieldedAccessControl { * @returns {Bytes<32>} accountId - The computed account ID. */ circuit _computeAccountId(role: RoleIdentifier): AccountIdentifier { - Initializable_assertInitialized(); - - return persistentHash>>( - [ownPublicKey().bytes, wit_secretNonce(role), _instanceSalt, pad(32, "ShieldedAccessControl:accountId")] + return persistentHash>>( + [ownPublicKey().bytes, + wit_secretNonce(role), + _instanceSalt, + pad(32, "ShieldedAccessControl:accountId")] ) as AccountIdentifier; } diff --git a/contracts/src/access/test/ShieldedAccessControl.test.ts b/contracts/src/access/test/ShieldedAccessControl.test.ts index dd4de193..31df9e0a 100644 --- a/contracts/src/access/test/ShieldedAccessControl.test.ts +++ b/contracts/src/access/test/ShieldedAccessControl.test.ts @@ -124,7 +124,6 @@ describe('ShieldedAccessControl', () => { const circuitsToFail: FailingCircuits[] = [ ['proveCallerRole', [UNINITIALIZED.role]], ['assertOnlyRole', [UNINITIALIZED.role]], - ['_validateRole', [UNINITIALIZED.role, UNINITIALIZED.accountId]], ['getRoleAdmin', [UNINITIALIZED.role]], ['grantRole', [UNINITIALIZED.role, UNINITIALIZED.accountId]], ['revokeRole', [UNINITIALIZED.role, UNINITIALIZED.accountId]], @@ -132,14 +131,6 @@ describe('ShieldedAccessControl', () => { ['_setRoleAdmin', [UNINITIALIZED.role, UNINITIALIZED.role]], ['_grantRole', [UNINITIALIZED.role, UNINITIALIZED.accountId]], ['_revokeRole', [UNINITIALIZED.role, UNINITIALIZED.accountId]], - [ - '_computeRoleCommitment', - [UNINITIALIZED.role, UNINITIALIZED.accountId], - ], - [ - '_computeAccountId', - [UNINITIALIZED.role], - ], ]; it.each(circuitsToFail)('%s should fail', (circuitName, args) => { expect(() => { @@ -155,6 +146,24 @@ describe('ShieldedAccessControl', () => { }).not.toThrow(); }); + it('should allow unchecked _computeAccountId', () => { + expect(() => { + shieldedAccessControl._computeAccountId(ADMIN.role); + }).not.toThrow(); + }) + + it('should allow unchecked _computeRoleCommitment', () => { + expect(() => { + shieldedAccessControl._computeRoleCommitment(ADMIN.role, ADMIN.accountId); + }).not.toThrow(); + }); + + it('should allow unchecked _validateRole', () => { + expect(() => { + shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId); + }).not.toThrow(); + }) + it('should fail with 0 instanceSalt', () => { const isInit = true; expect(() => { diff --git a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact index 8b2b8fb7..6e52b207 100644 --- a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact +++ b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact @@ -43,7 +43,6 @@ export circuit _computeRoleCommitment( roleId: ShieldedAccessControl_RoleIdentifier, accountId: ShieldedAccessControl_AccountIdentifier ): ShieldedAccessControl_RoleCommitment { - Initializable_assertInitialized(); return persistentHash>>( [roleId as Bytes<32>, @@ -55,16 +54,17 @@ export circuit _computeRoleCommitment( } // circuit is reimplemented in the Mock contract for testing -export circuit _computeAccountId(role: ShieldedAccessControl_RoleIdentifier): ShieldedAccessControl_AccountIdentifier { - Initializable_assertInitialized(); +export circuit _computeAccountId( + role: ShieldedAccessControl_RoleIdentifier + ): ShieldedAccessControl_AccountIdentifier { // disclosure required here for testing return disclose(persistentHash>>( - [ownPublicKey().bytes, - wit_secretNonce(role), - ShieldedAccessControl__instanceSalt, - pad(32, "ShieldedAccessControl:accountId")] - )) + [ownPublicKey().bytes, + wit_secretNonce(role), + ShieldedAccessControl__instanceSalt, + pad(32, "ShieldedAccessControl:accountId")] + )) as ShieldedAccessControl_AccountIdentifier; } @@ -91,8 +91,6 @@ export circuit _validateRole( role: ShieldedAccessControl_RoleIdentifier, accountId: ShieldedAccessControl_AccountIdentifier ): Boolean { - Initializable_assertInitialized(); - const roleCommitment = _computeRoleCommitment(role, accountId); const roleCommitmentPath = wit_getRoleCommitmentPath(roleCommitment); const isValidPath = From 80dd34daefb9e2a2ce50c29e7d7f7e91c9f333ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Thu, 12 Mar 2026 20:07:08 -0400 Subject: [PATCH 273/322] Update docs reorganize code --- .../src/access/ShieldedAccessControl.compact | 160 +++++++++--------- 1 file changed, 80 insertions(+), 80 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index e384e707..ca9bc944 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -37,7 +37,7 @@ pragma language_version >= 0.21.0; * - `instanceSalt` is an immutable, cryptographically strong random value provided on deployment * - `commitmentDomain` is the following zero padded 32 byte string: "ShieldedAccessControl:commitment" * - `accountIdDomain` is the following zero padded 32 byte string: "ShieldedAccessControl:accountId" - * - `commitmentDomain` is the following zero padded 32 byte string: "ShieldedAccessControl:nullifier" + * - `nullifierDomain` is the following zero padded 32 byte string: "ShieldedAccessControl:nullifier" * * In this RBAC model, role commitments behave like private bearer tokens. Possession of a valid, non-revoked role * commitment grants authorization. Revocation permanently burns the role instance, requiring explicit new issuance @@ -216,34 +216,6 @@ module ShieldedAccessControl { _instanceSalt = disclose(instanceSalt); } - /** - * @description Returns `true` if a caller proves ownership of `role` and is not revoked. MAY return false for a legitimately credentialed - * caller if the proving environment supplies an invalid Merkle path. This circuit will never return true for an - * unauthorized caller. - * - * @circuitInfo k=15, rows=22128 - * - * Requirements: - * - * - caller must not provide valid Merkle tree path for a different `(role, accountId)` pairing. - * - * Disclosures: - * - * - A Merkle tree path to a role commitment. - * - A role commitment corresponding to a `(role, accountId)` pairing. - * - A nullifier for the respective role commitment. - * - * @param {Bytes<32>} role - The role identifier. - * - * @return {Boolean} - A boolean determining if a caller successfully proved ownership of `role` -  */ - export circuit proveCallerRole(role: RoleIdentifier): Boolean { - Initializable_assertInitialized(); - - const accountId = _computeAccountId(role); - return _validateRole(role, accountId); - } - /** * @description Reverts if caller cannot provide a valid proof of ownership for `role`. * @@ -271,28 +243,31 @@ module ShieldedAccessControl { } /** - * @description Returns the admin role that controls `role` or a zero - * byte array if `role` doesn't exist. See {grantRole} and {revokeRole}. + * @description Returns `true` if a caller proves ownership of `role` and is not revoked. MAY return false for a legitimately credentialed + * caller if the proving environment supplies an invalid Merkle path. This circuit will never return true for an + * unauthorized caller. * - * To change a role’s admin use {_setRoleAdmin}. + * @circuitInfo k=15, rows=22128 * - * @circuitInfo k=9, rows=375 + * Requirements: + * + * - caller must not provide valid Merkle tree path for a different `(role, accountId)` pairing. * * Disclosures: * - * - A role identifier. + * - A Merkle tree path to a role commitment. + * - A role commitment corresponding to a `(role, accountId)` pairing. + * - A nullifier for the respective role commitment. * * @param {Bytes<32>} role - The role identifier. * - * @return {Bytes<32>} roleAdmin - The admin role that controls `role`. - */ - export circuit getRoleAdmin(role: RoleIdentifier): AdminIdentifier { + * @return {Boolean} - A boolean determining if a caller successfully proved ownership of `role` +  */ + export circuit proveCallerRole(role: RoleIdentifier): Boolean { Initializable_assertInitialized(); - if (_adminRoles.member(disclose(role))) { - return _adminRoles.lookup(disclose(role)); - } - return default> as AdminIdentifier; + const accountId = _computeAccountId(role); + return _validateRole(role, accountId); } /** @@ -380,6 +355,42 @@ module ShieldedAccessControl { return true; } + /** + * @description Revokes `role` from the calling account. Fails silently if role is already revoked. + * `role` existence is not checked, so a caller can renounce roles they don't own or don't exist. + * + * @notice Roles are often managed via {grantRole} and {revokeRole}: this circuit's + * purpose is to provide a mechanism for accounts to lose their privileges + * if they are compromised (such as when a trusted device is misplaced). + * + * @warning Outside observers may be able to use timing and pattern analysis to weaken pseudonymity + * guarantees if renounceRole is used in tandem with other on-chain actions. + * + * @circuitInfo k=17, rows=108992 + * + * Requirements: + * + * - The caller must provide a valid `accountId` for a `role`. + * + * Disclosures: + * + * - A nullifier for the respective role commitment. + * + * @param {Bytes<32>} role - The role identifier. + * @param {Bytes<32>} accountIdConfirmation - The caller's account identifier, must match the internally computed value. + * + * @return {[]} - Empty tuple. + */ + export circuit renounceRole(role: RoleIdentifier, accountIdConfirmation: AccountIdentifier): [] { + Initializable_assertInitialized(); + + assert(accountIdConfirmation == _computeAccountId(role), + "ShieldedAccessControl: bad confirmation" + ); + + _revokeRole(role, accountIdConfirmation); + } + /** * @description Permanently revokes `role` from `accountId` by inserting a role nullifier into the * `_roleCommitmentNullifiers` set. Once revoked, a new `accountId` must be generated to be re-authorized for @@ -415,23 +426,23 @@ module ShieldedAccessControl { } /** - * @description Permanently revokes `role` from `accountId` by inserting a role nullifier into the - * `_roleCommitmentNullifiers` set. Once revoked, a new `accountId` must be generated to be re-authorized for - * `role`. At this time, proofs of non-membership on values in the Merkle tree LDT are not possible - * so a `(role, accountId)` pairing that does not exist can still be revoked. Returns false if a role is already - * revoked. Internal circuit without access restriction. - * - * @circuitInfo k=15, rows=18115 - * - * Disclosures: - * - * - A nullifier for the respective role commitment. - * - * @param {Bytes<32>} role - The role identifier. - * @param {Bytes<32>} accountId - The account identifier. - * - * @return {Boolean} isRevoked - Returns true if operation completes successfully. - */ + * @description Permanently revokes `role` from `accountId` by inserting a role nullifier into the + * `_roleCommitmentNullifiers` set. Once revoked, a new `accountId` must be generated to be re-authorized for + * `role`. At this time, proofs of non-membership on values in the Merkle tree LDT are not possible + * so a `(role, accountId)` pairing that does not exist can still be revoked. Returns false if a role is already + * revoked. Internal circuit without access restriction. + * + * @circuitInfo k=15, rows=18115 + * + * Disclosures: + * + * - A nullifier for the respective role commitment. + * + * @param {Bytes<32>} role - The role identifier. + * @param {Bytes<32>} accountId - The account identifier. + * + * @return {Boolean} isRevoked - Returns true if operation completes successfully. + */ export circuit _revokeRole(role: RoleIdentifier, accountId: AccountIdentifier): Boolean { Initializable_assertInitialized(); @@ -448,39 +459,28 @@ module ShieldedAccessControl { } /** - * @description Revokes `role` from the calling account. Fails silently if role is already revoked. - * `role` existence is not checked, so a caller can renounce roles they don't own or don't exist. - * - * @notice Roles are often managed via {grantRole} and {revokeRole}: this circuit's - * purpose is to provide a mechanism for accounts to lose their privileges - * if they are compromised (such as when a trusted device is misplaced). - * - * @warning Outside observers may be able to use timing and pattern analysis to weaken pseudonymity - * guarantees if renounceRole is used in tandem with other on-chain actions. - * - * @circuitInfo k=17, rows=108992 + * @description Returns the admin role that controls `role` or a zero + * byte array if `role` doesn't exist. See {grantRole} and {revokeRole}. * - * Requirements: + * To change a role’s admin use {_setRoleAdmin}. * - * - The caller must provide a valid `accountId` for a `role`. + * @circuitInfo k=9, rows=375 * * Disclosures: * - * - A nullifier for the respective role commitment. + * - A role identifier. * * @param {Bytes<32>} role - The role identifier. - * @param {Bytes<32>} accountId - The unique identifier of the account. * - * @return {[]} - Empty tuple. + * @return {Bytes<32>} roleAdmin - The admin role that controls `role`. */ - export circuit renounceRole(role: RoleIdentifier, accountIdConfirmation: AccountIdentifier): [] { + export circuit getRoleAdmin(role: RoleIdentifier): AdminIdentifier { Initializable_assertInitialized(); - assert(accountIdConfirmation == _computeAccountId(role), - "ShieldedAccessControl: bad confirmation" - ); - - _revokeRole(role, accountIdConfirmation); + if (_adminRoles.member(disclose(role))) { + return _adminRoles.lookup(disclose(role)); + } + return default> as AdminIdentifier; } /** From d38c6dc0debabe5bb0f8b29494aa45ea560727a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Fri, 13 Mar 2026 12:20:34 -0400 Subject: [PATCH 274/322] Add unchecked circuits --- .../src/access/ShieldedAccessControl.compact | 144 +++++++++++++++++- 1 file changed, 137 insertions(+), 7 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index ca9bc944..943e9eb9 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -239,7 +239,31 @@ module ShieldedAccessControl { export circuit assertOnlyRole(role: RoleIdentifier): [] { Initializable_assertInitialized(); - assert(proveCallerRole(role), "ShieldedAccessControl: unauthorized account"); + assert(_uncheckedProveCallerRole(role), "ShieldedAccessControl: unauthorized account"); + } + + /** + * @description Reverts if caller cannot provide a valid proof of ownership for `role`. + * + * @circuitInfo k=15, rows=22130 + * + * Requirements: + * + * - caller must prove ownership of `role`. + * - caller must not provide valid Merkle tree path for a different `(role, accountId)` pairing. + * + * Disclosures: + * + * - A Merkle tree path to a role commitment. + * - A role commitment corresponding to a `(role, accountId)` pairing. + * - A nullifier for the respective role commitment. + * + * @param {Bytes<32>} role - The role identifier. + * + * @return {[]} - Empty tuple. + */ + circuit _uncheckedAssertOnlyRole(role: RoleIdentifier): [] { + assert(_proveCallerRole(role), "ShieldedAccessControl: unauthorized account"); } /** @@ -270,6 +294,36 @@ module ShieldedAccessControl { return _validateRole(role, accountId); } + /** + * @description Returns `true` if a caller proves ownership of `role` and is not revoked. MAY return false for a legitimately credentialed + * caller if the proving environment supplies an invalid Merkle path. This circuit will never return true for an + * unauthorized caller. + * + * @warning This circuit does not perform an initialization check. It's only meant to be used as + * an internal helper in the Shielded Access Control module. Using this circuit outside of the + * module may cause undefined behavior and break security guarantees. + * + * @circuitInfo k=15, rows=22128 + * + * Requirements: + * + * - caller must not provide valid Merkle tree path for a different `(role, accountId)` pairing. + * + * Disclosures: + * + * - A Merkle tree path to a role commitment. + * - A role commitment corresponding to a `(role, accountId)` pairing. + * - A nullifier for the respective role commitment. + * + * @param {Bytes<32>} role - The role identifier. + * + * @return {Boolean} - A boolean determining if a caller successfully proved ownership of `role` +  */ + circuit _uncheckedProveCallerRole(role: RoleIdentifier): Boolean { + const accountId = _computeAccountId(role); + return _validateRole(role, accountId); + } + /** * @description Grants `role` to `accountId` by inserting a role commitment unique to the * `(role, accountId)` pairing into the `_operatorRoles` Merkle tree. Duplicate role commitments can be issued @@ -299,8 +353,8 @@ module ShieldedAccessControl { export circuit grantRole(role: RoleIdentifier, accountId: AccountIdentifier): [] { Initializable_assertInitialized(); - assertOnlyRole(getRoleAdmin(role) as RoleIdentifier); - _grantRole(role, accountId); + _uncheckedAssertOnlyRole(getRoleAdmin(role) as RoleIdentifier); + _uncheckedGrantRole(role, accountId); } /** @@ -355,6 +409,48 @@ module ShieldedAccessControl { return true; } + /** + * @description Grants `role` to `accountId` by inserting a role commitment unique to the + * `(role, accountId)` pairing into the `_operatorRoles` Merkle tree. Duplicate role commitments can be issued + * so long as they remain unrevoked. This does not yield any additional authority and simply wastes + * limited Merkle tree storage slots. Once revoked, a role cannot be re-granted to the same `accountId`. A new `accountId` must be + * generated to be re-authorized for a revoked `role`. + * + * Internal circuit without access restriction. + * + * See Storage Caveat in {_grantRole} + * + * @warning This circuit does not perform an initialization check. It's only meant to be used as + * an internal helper in the Shielded Access Control module. Using this circuit outside of the + * module may cause undefined behavior and break security guarantees. + * + * @dev Commitment and nullifier checks are inlined in this circuit to avoid an expensive re-computation of the role commitment and nullifier + * + * @circuitInfo k=15, rows=18115 + * + * Disclosures: + * + * - A role commitment corresponding to a `(role, accountId)` pairing. + * - A nullifier for the respective role commitment. + * + * @param {Bytes<32>} role - The role identifier. + * @param {Bytes<32>} accountId - The account identifier. + * + * @return {Boolean} isGranted - Returns true if a role was granted successfully. + */ + circuit _uncheckedGrantRole(role: RoleIdentifier, accountId: AccountIdentifier): Boolean { + const roleCommitment = _computeRoleCommitment(role, accountId); + const roleNullifier = _computeNullifier(roleCommitment); + const isRevoked = _roleCommitmentNullifiers.member(disclose(roleNullifier)); + + if (isRevoked) { + return false; + } + + _operatorRoles.insert(disclose(roleCommitment)); + return true; + } + /** * @description Revokes `role` from the calling account. Fails silently if role is already revoked. * `role` existence is not checked, so a caller can renounce roles they don't own or don't exist. @@ -388,7 +484,7 @@ module ShieldedAccessControl { "ShieldedAccessControl: bad confirmation" ); - _revokeRole(role, accountIdConfirmation); + _uncheckedRevokeRole(role, accountIdConfirmation); } /** @@ -421,8 +517,8 @@ module ShieldedAccessControl { export circuit revokeRole(role: RoleIdentifier, accountId: AccountIdentifier): [] { Initializable_assertInitialized(); - assertOnlyRole(getRoleAdmin(role) as RoleIdentifier); - _revokeRole(role, accountId); + _uncheckedAssertOnlyRole(getRoleAdmin(role) as RoleIdentifier); + _uncheckedRevokeRole(role, accountId); } /** @@ -458,6 +554,41 @@ module ShieldedAccessControl { return true; } + /** + * @description Permanently revokes `role` from `accountId` by inserting a role nullifier into the + * `_roleCommitmentNullifiers` set. Once revoked, a new `accountId` must be generated to be re-authorized for + * `role`. At this time, proofs of non-membership on values in the Merkle tree LDT are not possible + * so a `(role, accountId)` pairing that does not exist can still be revoked. Returns false if a role is already + * revoked. Internal circuit without access restriction. + * + * @warning This circuit does not perform an initialization check. It's only meant to be used as + * an internal helper in the Shielded Access Control module. Using this circuit outside of the + * module may cause undefined behavior and break security guarantees. + * + * @circuitInfo k=15, rows=18115 + * + * Disclosures: + * + * - A nullifier for the respective role commitment. + * + * @param {Bytes<32>} role - The role identifier. + * @param {Bytes<32>} accountId - The account identifier. + * + * @return {Boolean} isRevoked - Returns true if operation completes successfully. + */ + export circuit _uncheckedRevokeRole(role: RoleIdentifier, accountId: AccountIdentifier): Boolean { + const roleCommitment = _computeRoleCommitment(role, accountId); + const roleNullifier = _computeNullifier(roleCommitment); + const isRevoked = _roleCommitmentNullifiers.member(disclose(roleNullifier)); + + if (isRevoked) { + return false; + } + + _roleCommitmentNullifiers.insert(disclose(roleNullifier)); + return true; + } + /** * @description Returns the admin role that controls `role` or a zero * byte array if `role` doesn't exist. See {grantRole} and {revokeRole}. @@ -512,7 +643,6 @@ module ShieldedAccessControl { * an internal helper in the Shielded Access Control module. Using this circuit outside of the * module may cause undefined behavior and break security guarantees. * - * * @circuitInfo k=14, rows=16089 * * Requirements: From 1b6c2a5ae513684d2b5b4576ddaa1101c6aeb1b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Fri, 13 Mar 2026 12:20:52 -0400 Subject: [PATCH 275/322] remove init checks from get / set admin circuits --- contracts/src/access/ShieldedAccessControl.compact | 4 ---- 1 file changed, 4 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index 943e9eb9..d33652ba 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -606,8 +606,6 @@ module ShieldedAccessControl { * @return {Bytes<32>} roleAdmin - The admin role that controls `role`. */ export circuit getRoleAdmin(role: RoleIdentifier): AdminIdentifier { - Initializable_assertInitialized(); - if (_adminRoles.member(disclose(role))) { return _adminRoles.lookup(disclose(role)); } @@ -630,8 +628,6 @@ module ShieldedAccessControl { * @return {[]} - Empty tuple. */ export circuit _setRoleAdmin(role: RoleIdentifier, adminId: AdminIdentifier): [] { - Initializable_assertInitialized(); - _adminRoles.insert(disclose(role), disclose(adminId)); } From 819c8214f0999ec8d0fa36fb73e72b1fc68278e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Fri, 13 Mar 2026 12:28:59 -0400 Subject: [PATCH 276/322] fix circuit def --- contracts/src/access/ShieldedAccessControl.compact | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index d33652ba..c37db3bd 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -263,7 +263,7 @@ module ShieldedAccessControl { * @return {[]} - Empty tuple. */ circuit _uncheckedAssertOnlyRole(role: RoleIdentifier): [] { - assert(_proveCallerRole(role), "ShieldedAccessControl: unauthorized account"); + assert(_uncheckedProveCallerRole(role), "ShieldedAccessControl: unauthorized account"); } /** @@ -576,7 +576,7 @@ module ShieldedAccessControl { * * @return {Boolean} isRevoked - Returns true if operation completes successfully. */ - export circuit _uncheckedRevokeRole(role: RoleIdentifier, accountId: AccountIdentifier): Boolean { + circuit _uncheckedRevokeRole(role: RoleIdentifier, accountId: AccountIdentifier): Boolean { const roleCommitment = _computeRoleCommitment(role, accountId); const roleNullifier = _computeNullifier(roleCommitment); const isRevoked = _roleCommitmentNullifiers.member(disclose(roleNullifier)); From 15a113b6eb4b8758f3ccde775d7772b27a55ccc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Fri, 13 Mar 2026 12:29:15 -0400 Subject: [PATCH 277/322] Add unexported circuits to mock --- .../mocks/MockShieldedAccessControl.compact | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact index 6e52b207..f188ba82 100644 --- a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact +++ b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact @@ -82,10 +82,19 @@ export circuit proveCallerRole(roleId: ShieldedAccessControl_RoleIdentifier): Bo return ShieldedAccessControl_proveCallerRole(roleId); } +export circuit _uncheckedProveCallerRole(role: RoleIdentifier): Boolean { + const accountId = _computeAccountId(role); + return _validateRole(role, accountId); +} + export circuit assertOnlyRole(roleId: ShieldedAccessControl_RoleIdentifier): [] { ShieldedAccessControl_assertOnlyRole(roleId); } +export circuit _uncheckedAssertOnlyRole(role: RoleIdentifier): [] { + assert(_uncheckedProveCallerRole(role), "ShieldedAccessControl: unauthorized account"); +} + // _validateRole is re-implemented in the Mock contract for testing export circuit _validateRole( role: ShieldedAccessControl_RoleIdentifier, @@ -126,6 +135,19 @@ export circuit grantRole( ShieldedAccessControl_grantRole(roleId, accountId); } +circuit _uncheckedGrantRole(role: RoleIdentifier, accountId: AccountIdentifier): Boolean { + const roleCommitment = _computeRoleCommitment(role, accountId); + const roleNullifier = _computeNullifier(roleCommitment); + const isRevoked = _roleCommitmentNullifiers.member(disclose(roleNullifier)); + + if (isRevoked) { + return false; + } + + _operatorRoles.insert(disclose(roleCommitment)); + return true; +} + export circuit revokeRole( roleId: ShieldedAccessControl_RoleIdentifier, accountId: ShieldedAccessControl_AccountIdentifier @@ -133,6 +155,19 @@ export circuit revokeRole( ShieldedAccessControl_revokeRole(roleId, accountId); } +circuit _uncheckedRevokeRole(role: RoleIdentifier, accountId: AccountIdentifier): Boolean { + const roleCommitment = _computeRoleCommitment(role, accountId); + const roleNullifier = _computeNullifier(roleCommitment); + const isRevoked = _roleCommitmentNullifiers.member(disclose(roleNullifier)); + + if (isRevoked) { + return false; + } + + _roleCommitmentNullifiers.insert(disclose(roleNullifier)); + return true; +} + export circuit renounceRole( roleId: ShieldedAccessControl_RoleIdentifier, accountIdConfirmation: ShieldedAccessControl_AccountIdentifier From 8f536e0b1d5df935f01b284928759864a18df3a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Fri, 13 Mar 2026 12:32:28 -0400 Subject: [PATCH 278/322] Fmt Mock --- .../mocks/MockShieldedAccessControl.compact | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact index f188ba82..cc5025a2 100644 --- a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact +++ b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact @@ -82,7 +82,7 @@ export circuit proveCallerRole(roleId: ShieldedAccessControl_RoleIdentifier): Bo return ShieldedAccessControl_proveCallerRole(roleId); } -export circuit _uncheckedProveCallerRole(role: RoleIdentifier): Boolean { +export circuit _uncheckedProveCallerRole(role: ShieldedAccessControl_RoleIdentifier): Boolean { const accountId = _computeAccountId(role); return _validateRole(role, accountId); } @@ -91,7 +91,7 @@ export circuit assertOnlyRole(roleId: ShieldedAccessControl_RoleIdentifier): [] ShieldedAccessControl_assertOnlyRole(roleId); } -export circuit _uncheckedAssertOnlyRole(role: RoleIdentifier): [] { +export circuit _uncheckedAssertOnlyRole(role: ShieldedAccessControl_RoleIdentifier): [] { assert(_uncheckedProveCallerRole(role), "ShieldedAccessControl: unauthorized account"); } @@ -135,16 +135,19 @@ export circuit grantRole( ShieldedAccessControl_grantRole(roleId, accountId); } -circuit _uncheckedGrantRole(role: RoleIdentifier, accountId: AccountIdentifier): Boolean { +circuit _uncheckedGrantRole( + role: ShieldedAccessControl_RoleIdentifier, + accountId: ShieldedAccessControl_AccountIdentifier + ): Boolean { const roleCommitment = _computeRoleCommitment(role, accountId); const roleNullifier = _computeNullifier(roleCommitment); - const isRevoked = _roleCommitmentNullifiers.member(disclose(roleNullifier)); + const isRevoked = ShieldedAccessControl__roleCommitmentNullifiers.member(disclose(roleNullifier)); if (isRevoked) { return false; } - _operatorRoles.insert(disclose(roleCommitment)); + ShieldedAccessControl__operatorRoles.insert(disclose(roleCommitment)); return true; } @@ -155,16 +158,19 @@ export circuit revokeRole( ShieldedAccessControl_revokeRole(roleId, accountId); } -circuit _uncheckedRevokeRole(role: RoleIdentifier, accountId: AccountIdentifier): Boolean { +circuit _uncheckedRevokeRole( + role: ShieldedAccessControl_RoleIdentifier, + accountId: ShieldedAccessControl_AccountIdentifier + ): Boolean { const roleCommitment = _computeRoleCommitment(role, accountId); const roleNullifier = _computeNullifier(roleCommitment); - const isRevoked = _roleCommitmentNullifiers.member(disclose(roleNullifier)); + const isRevoked = ShieldedAccessControl__roleCommitmentNullifiers.member(disclose(roleNullifier)); if (isRevoked) { return false; } - _roleCommitmentNullifiers.insert(disclose(roleNullifier)); + ShieldedAccessControl__roleCommitmentNullifiers.insert(disclose(roleNullifier)); return true; } From 452c2d9caf32b441d13ceccb74fd6328e3b95daf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Fri, 13 Mar 2026 12:56:06 -0400 Subject: [PATCH 279/322] Add _uncheckedCircuits to sim, tests, fix mock export --- .../access/test/ShieldedAccessControl.test.ts | 2335 ++++++++++++----- .../mocks/MockShieldedAccessControl.compact | 4 +- .../ShieldedAccessControlSimulator.ts | 60 +- 3 files changed, 1764 insertions(+), 635 deletions(-) diff --git a/contracts/src/access/test/ShieldedAccessControl.test.ts b/contracts/src/access/test/ShieldedAccessControl.test.ts index 31df9e0a..a83fe7eb 100644 --- a/contracts/src/access/test/ShieldedAccessControl.test.ts +++ b/contracts/src/access/test/ShieldedAccessControl.test.ts @@ -116,6 +116,7 @@ describe('ShieldedAccessControl', () => { isInit, ); }); + type FailingCircuits = [ method: keyof ShieldedAccessControlSimulator, args: unknown[], @@ -124,11 +125,9 @@ describe('ShieldedAccessControl', () => { const circuitsToFail: FailingCircuits[] = [ ['proveCallerRole', [UNINITIALIZED.role]], ['assertOnlyRole', [UNINITIALIZED.role]], - ['getRoleAdmin', [UNINITIALIZED.role]], ['grantRole', [UNINITIALIZED.role, UNINITIALIZED.accountId]], ['revokeRole', [UNINITIALIZED.role, UNINITIALIZED.accountId]], ['renounceRole', [UNINITIALIZED.role, UNINITIALIZED.accountId]], - ['_setRoleAdmin', [UNINITIALIZED.role, UNINITIALIZED.role]], ['_grantRole', [UNINITIALIZED.role, UNINITIALIZED.accountId]], ['_revokeRole', [UNINITIALIZED.role, UNINITIALIZED.accountId]], ]; @@ -140,30 +139,31 @@ describe('ShieldedAccessControl', () => { }).toThrow('Initializable: contract not initialized'); }); - it('should allow pure _computeNullifier', () => { - expect(() => { - shieldedAccessControl._computeNullifier(ADMIN.roleCommitment); - }).not.toThrow(); - }); - - it('should allow unchecked _computeAccountId', () => { - expect(() => { - shieldedAccessControl._computeAccountId(ADMIN.role); - }).not.toThrow(); - }) - - it('should allow unchecked _computeRoleCommitment', () => { + type UncheckedCircuits = [ + method: keyof ShieldedAccessControlSimulator, + args: unknown[], + ]; + // Circuit calls should succeed + const circuitsToSucceed: UncheckedCircuits[] = [ + ['_uncheckedProveCallerRole', [UNINITIALIZED.role]], + ['_uncheckedAssertOnlyRole', [UNINITIALIZED.role]], + ['getRoleAdmin', [UNINITIALIZED.role]], + ['_uncheckedGrantRole', [UNINITIALIZED.role, UNINITIALIZED.accountId]], + ['_uncheckedRevokeRole', [UNINITIALIZED.role, UNINITIALIZED.accountId]], + ['_setRoleAdmin', [UNINITIALIZED.role, UNINITIALIZED.role]], + ['_computeAccountId', [UNINITIALIZED.role]], + ['_computeRoleCommitment', [UNINITIALIZED.role, UNINITIALIZED.accountId]], + ['_computeNullifier', [UNINITIALIZED.roleCommitment]], + ['_validateRole', [UNINITIALIZED.roleCommitment]], + ]; + it.each(circuitsToSucceed)('%s should succeed', (circuitName, args) => { expect(() => { - shieldedAccessControl._computeRoleCommitment(ADMIN.role, ADMIN.accountId); - }).not.toThrow(); + (shieldedAccessControl[circuitName] as (...args: unknown[]) => unknown)( + ...args, + ); + }).not.toThrow('Initializable: contract not initialized'); }); - it('should allow unchecked _validateRole', () => { - expect(() => { - shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId); - }).not.toThrow(); - }) - it('should fail with 0 instanceSalt', () => { const isInit = true; expect(() => { @@ -814,23 +814,14 @@ describe('ShieldedAccessControl', () => { }); }); - describe('grantRole', () => { + // TODO refactor to test _uncheckAssertOnlyRole + describe.skip('_uncheckAssertOnlyRole', () => { beforeEach(() => { shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); shieldedAccessControl.setPersistentCaller(ADMIN.publicKey); }); describe('should fail', () => { - it('when caller does not have the admin role', () => { - shieldedAccessControl.as(UNAUTHORIZED.publicKey); - expect(() => - shieldedAccessControl.grantRole( - OPERATOR_1.role, - OPERATOR_1.accountId, - ), - ).toThrow('ShieldedAccessControl: unauthorized account'); - }); - it('when wit_getRoleCommitmentPath returns a valid path for a different role, accountId pairing', () => { shieldedAccessControl._grantRole( OPERATOR_1.role, @@ -851,285 +842,615 @@ describe('ShieldedAccessControl', () => { }, ); expect(() => { - shieldedAccessControl.grantRole(ADMIN.role, ADMIN.accountId); + shieldedAccessControl.assertOnlyRole(ADMIN.role); }).toThrow( 'ShieldedAccessControl: Path must contain leaf matching computed role commitment for the provided role, accountId pairing', ); }); - it('when admin with duplicate roles is revoked', () => { - // create duplicate roles - shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); - shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); - - shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); + it('when caller was never granted the role', () => { + shieldedAccessControl.as(UNAUTHORIZED.publicKey); expect(() => - shieldedAccessControl.grantRole(ADMIN.role, ADMIN.accountId), + shieldedAccessControl.assertOnlyRole(ADMIN.role), ).toThrow('ShieldedAccessControl: unauthorized account'); }); - it('when admin role is revoked', () => { - shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); + it('when authorized caller has incorrect path', () => { + // Check caller is admin, has admin role + expect( + shieldedAccessControl.getCallerContext().currentZswapLocalState + .coinPublicKey, + ).toEqual(ADMIN.zPublicKey); + expect(shieldedAccessControl.getRoleAdmin(ADMIN.role)).toEqual( + new Uint8Array(ADMIN.role), + ); + expect( + shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), + ).toBe(true); + + // Check nonce is correct + expect( + shieldedAccessControl.privateState.getCurrentSecretNonce( + ADMIN.role, + ), + ).toBe(ADMIN.secretNonce); + + // Check path does not match + const truePath = + shieldedAccessControl.privateState.getCommitmentPathWithFindForLeaf( + ADMIN.roleCommitment, + ); + shieldedAccessControl.overrideWitness( + 'wit_getRoleCommitmentPath', + RETURN_BAD_PATH, + ); + const witnessCalculatedPath = + shieldedAccessControl.privateState.getCommitmentPathWithWitnessImpl( + ADMIN.roleCommitment, + ); + expect(witnessCalculatedPath).not.toEqual(truePath); + expect(() => - shieldedAccessControl.grantRole(ADMIN.role, ADMIN.accountId), + shieldedAccessControl.assertOnlyRole(ADMIN.role), ).toThrow('ShieldedAccessControl: unauthorized account'); }); - it('when admin provides incorrect nonce', () => { + it('when authorized caller has incorrect nonce', () => { + // Check caller is admin, has admin role + expect( + shieldedAccessControl.getCallerContext().currentZswapLocalState + .coinPublicKey, + ).toEqual(ADMIN.zPublicKey); + expect(shieldedAccessControl.getRoleAdmin(ADMIN.role)).toEqual( + new Uint8Array(ADMIN.role), + ); + expect( + shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), + ).toBe(true); + shieldedAccessControl.privateState.injectSecretNonce( ADMIN.role, - BAD_INPUT.secretNonce, + UNAUTHORIZED.secretNonce, ); + + // Check nonce is incorrect expect( shieldedAccessControl.privateState.getCurrentSecretNonce( ADMIN.role, ), - ).not.toEqual(ADMIN.secretNonce); + ).not.toBe(ADMIN.secretNonce); + + // Check path matches + const truePath = + shieldedAccessControl.privateState.getCommitmentPathWithFindForLeaf( + ADMIN.roleCommitment, + ); + const witnessCalculatedPath = + shieldedAccessControl.privateState.getCommitmentPathWithWitnessImpl( + ADMIN.roleCommitment, + ); + expect(witnessCalculatedPath).toEqual(truePath); + expect(() => - shieldedAccessControl.grantRole(ADMIN.role, ADMIN.accountId), + shieldedAccessControl.assertOnlyRole(ADMIN.role), ).toThrow('ShieldedAccessControl: unauthorized account'); }); - it('when admin provides bad witness path', () => { - shieldedAccessControl.overrideWitness( - 'wit_getRoleCommitmentPath', - RETURN_BAD_PATH, + it('when unauthorized caller has correct nonce, and path', () => { + // Check UNAUTHORIZED user is not admin, doesnt have admin role + expect(shieldedAccessControl.getRoleAdmin(ADMIN.role)).not.toEqual( + new Uint8Array(UNAUTHORIZED.role), ); + expect( + shieldedAccessControl._validateRole( + ADMIN.role, + UNAUTHORIZED.accountId, + ), + ).toBe(false); + + // Check nonce is correct + expect( + shieldedAccessControl.privateState.getCurrentSecretNonce( + ADMIN.role, + ), + ).toBe(ADMIN.secretNonce); + + // Check path matches + const truePath = + shieldedAccessControl.privateState.getCommitmentPathWithFindForLeaf( + ADMIN.roleCommitment, + ); + const witnessCalculatedPath = + shieldedAccessControl.privateState.getCommitmentPathWithWitnessImpl( + ADMIN.roleCommitment, + ); + expect(witnessCalculatedPath).toEqual(truePath); + + shieldedAccessControl.as(UNAUTHORIZED.publicKey); + // Check caller is UNAUTHORIZED user + expect( + shieldedAccessControl.getCallerContext().currentZswapLocalState + .coinPublicKey, + ).toEqual(UNAUTHORIZED.zPublicKey); + expect(() => - shieldedAccessControl.grantRole(ADMIN.role, ADMIN.accountId), + shieldedAccessControl.assertOnlyRole(ADMIN.role), ).toThrow('ShieldedAccessControl: unauthorized account'); }); - it('when non-admin caller has role', () => { - shieldedAccessControl._grantRole( - OPERATOR_1.role, - OPERATOR_1.accountId, - ); + it('when role is revoked', () => { + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); + expect(() => + shieldedAccessControl.assertOnlyRole(ADMIN.role), + ).toThrow('ShieldedAccessControl: unauthorized account'); + }); - shieldedAccessControl.as(OPERATOR_1.publicKey); - // OP_1 has role but is not authorized to grant roles to other users + it('when role is revoked and re-issued to the same accountId', () => { + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); + shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); expect(() => - shieldedAccessControl.grantRole( - OPERATOR_1.role, - OPERATOR_2.accountId, - ), + shieldedAccessControl.assertOnlyRole(ADMIN.role), ).toThrow('ShieldedAccessControl: unauthorized account'); }); }); - describe('should not update _operatorRoles Merkle tree', () => { - it('when role is revoked', () => { - // setup test - shieldedAccessControl._grantRole( - OPERATOR_1.role, - OPERATOR_1.accountId, - ); - shieldedAccessControl._revokeRole( + describe('should not fail', () => { + it('when accountId has multiple roles', () => { + shieldedAccessControl.privateState.injectSecretNonce( OPERATOR_1.role, - OPERATOR_1.accountId, + OPERATOR_1.secretNonce, ); - - const initialRoot = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.root(); - shieldedAccessControl.grantRole( - OPERATOR_1.role, - OPERATOR_1.accountId, + // A unique accountId must be constructed for each new role using its associated secretNonce + const operator1AccountId = buildAccountIdHash( + ADMIN.zPublicKey, + OPERATOR_1.secretNonce, ); - const updatedRoot = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.root(); - expect(initialRoot).toEqual(updatedRoot); - }); - }); - - describe('should grant role', () => { - it('when caller has the admin role', () => { - expect(() => - shieldedAccessControl.grantRole( - OPERATOR_1.role, - OPERATOR_1.accountId, - ), - ).not.toThrow(); - expect( - shieldedAccessControl._validateRole( - OPERATOR_1.role, - OPERATOR_1.accountId, - ), - ).toBe(true); - }); - - it('when caller has custom admin role', () => { - // Make OPERATOR_1.role the admin of OPERATOR_2.role. - shieldedAccessControl._setRoleAdmin( + shieldedAccessControl.privateState.injectSecretNonce( OPERATOR_2.role, - OPERATOR_1.role, + OPERATOR_2.secretNonce, ); - // Grant OPERATOR_1.role to OPERATOR_1.accountId - shieldedAccessControl.grantRole( - OPERATOR_1.role, - OPERATOR_1.accountId, + const operator2AccountId = buildAccountIdHash( + ADMIN.zPublicKey, + OPERATOR_2.secretNonce, ); - // Switch to OPERATOR_1 as caller and inject their nonce for their role. shieldedAccessControl.privateState.injectSecretNonce( - OPERATOR_1.role, - OPERATOR_1.secretNonce, + OPERATOR_3.role, + OPERATOR_3.secretNonce, + ); + const operator3AccountId = buildAccountIdHash( + ADMIN.zPublicKey, + OPERATOR_3.secretNonce, ); - shieldedAccessControl.setPersistentCaller(OPERATOR_1.publicKey); - // OPERATOR_1.accountId (who holds OPERATOR_1.role) can now grant OPERATOR_2.role. - expect(() => - shieldedAccessControl.grantRole( - OPERATOR_2.role, - OPERATOR_2.accountId, - ), - ).not.toThrow(); - expect( - shieldedAccessControl._validateRole( - OPERATOR_2.role, - OPERATOR_2.accountId, + shieldedAccessControl._grantRole( + OPERATOR_1.role, + operator1AccountId, + ); + shieldedAccessControl._grantRole( + OPERATOR_2.role, + operator2AccountId, + ); + shieldedAccessControl._grantRole( + OPERATOR_3.role, + operator3AccountId, + ); + expect(() => { + shieldedAccessControl.assertOnlyRole(ADMIN.role); + shieldedAccessControl.assertOnlyRole(OPERATOR_1.role); + shieldedAccessControl.assertOnlyRole(OPERATOR_2.role); + shieldedAccessControl.assertOnlyRole(OPERATOR_3.role); + }).not.toThrow(); + }); + + it('when authorized caller has correct nonce, and path', () => { + // Check nonce is correct + expect( + shieldedAccessControl.privateState.getCurrentSecretNonce( + ADMIN.role, ), - ).toBe(true); + ).toBe(ADMIN.secretNonce); + + // Check path matches + const truePath = + shieldedAccessControl.privateState.getCommitmentPathWithFindForLeaf( + ADMIN.roleCommitment, + ); + const witnessCalculatedPath = + shieldedAccessControl.privateState.getCommitmentPathWithWitnessImpl( + ADMIN.roleCommitment, + ); + expect(witnessCalculatedPath).toEqual(truePath); + + expect(() => + shieldedAccessControl.assertOnlyRole(ADMIN.role), + ).not.toThrow(); }); - it('when admin role is revoked and re-issued with a different accountId', () => { - // setup test + it('when role is revoked and re-issued with a different accountId', () => { shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); - const newNonce = Buffer.alloc(32, 'NEW_ADMIN_NONCE'); + shieldedAccessControl.privateState.injectSecretNonce( ADMIN.role, - newNonce, + Buffer.alloc(32, 'NEW_ADMIN_NONCE'), ); - const newAccountId = buildAccountIdHash(ADMIN.zPublicKey, newNonce); - shieldedAccessControl._grantRole(ADMIN.role, newAccountId); - - expect(() => { - shieldedAccessControl.grantRole( - OPERATOR_1.role, - OPERATOR_1.accountId, - ); - }).not.toThrow(); - expect( - shieldedAccessControl._validateRole( - OPERATOR_1.role, - OPERATOR_1.accountId, + const newAdminAccountId = buildAccountIdHash( + ADMIN.zPublicKey, + shieldedAccessControl.privateState.getCurrentSecretNonce( + ADMIN.role, ), - ).toBe(true); - expect( - shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.findPathForLeaf( - OPERATOR_1.roleCommitment, - ), - ).toBeDefined(); + ); + expect(newAdminAccountId).not.toEqual(ADMIN.accountId); + + shieldedAccessControl._grantRole(ADMIN.role, newAdminAccountId); + expect(() => + shieldedAccessControl.assertOnlyRole( + ADMIN.role, + ) + ).not.toThrow(); }); - it('when multiple admins of the same role exist', () => { - // setup test - const account1 = buildAccountIdHash( - OPERATOR_1.zPublicKey, - ADMIN.secretNonce, + it('when multiple users have the same role', () => { + // All users will use OPERATOR_1.secretNonce as their nonce value + // when generating their accountId for simplicity + shieldedAccessControl.privateState.injectSecretNonce( + OPERATOR_1.role, + OPERATOR_1.secretNonce, ); - const account2 = buildAccountIdHash( - OPERATOR_2.zPublicKey, - ADMIN.secretNonce, + // A unique accountId must be constructed for each new role using its associated secretNonce + const operator1AdminAccountId = buildAccountIdHash( + ADMIN.zPublicKey, + OPERATOR_1.secretNonce, ); - const account3 = buildAccountIdHash( - OPERATOR_3.zPublicKey, - ADMIN.secretNonce, + shieldedAccessControl._grantRole( + OPERATOR_1.role, + operator1AdminAccountId, ); - shieldedAccessControl._grantRole(ADMIN.role, account1); - shieldedAccessControl._grantRole(ADMIN.role, account2); - shieldedAccessControl._grantRole(ADMIN.role, account3); - - // check grant role succeeds as OP and role is valid - shieldedAccessControl.as(OPERATOR_1.publicKey); - expect(() => - shieldedAccessControl.grantRole(ADMIN.role, account1), - ).not.toThrow(); - expect( - shieldedAccessControl._validateRole(ADMIN.role, account1), - ).toBe(true); + shieldedAccessControl.as(ADMIN.publicKey); // assert ADMIN has OP_1 role + expect(shieldedAccessControl.assertOnlyRole(OPERATOR_1.role)); - shieldedAccessControl.as(OPERATOR_2.publicKey); - expect(() => - shieldedAccessControl.grantRole(ADMIN.role, ADMIN.accountId), - ).not.toThrow(); - expect( - shieldedAccessControl._validateRole(ADMIN.role, account2), - ).toBe(true); + const operator1Op2AccountId = buildAccountIdHash( + OPERATOR_2.zPublicKey, + OPERATOR_1.secretNonce, + ); + shieldedAccessControl._grantRole( + OPERATOR_1.role, + operator1Op2AccountId, + ); + shieldedAccessControl.as(OPERATOR_2.publicKey); // assert OP_2 has OP_1 role + expect(shieldedAccessControl.assertOnlyRole(OPERATOR_1.role)); - shieldedAccessControl.as(OPERATOR_3.publicKey); - expect(() => - shieldedAccessControl.grantRole(ADMIN.role, ADMIN.accountId), - ).not.toThrow(); - expect( - shieldedAccessControl._validateRole(ADMIN.role, account3), - ).toBe(true); + const operator1Op3AccountId = buildAccountIdHash( + OPERATOR_3.zPublicKey, + OPERATOR_1.secretNonce, + ); + shieldedAccessControl._grantRole( + OPERATOR_1.role, + operator1Op3AccountId, + ); + shieldedAccessControl.as(OPERATOR_3.publicKey); // assert OP_3 has OP_1 role + expect(shieldedAccessControl.assertOnlyRole(OPERATOR_1.role)); }); + }); + }); - it('when admin has multiple roles', () => { - shieldedAccessControl._grantRole(OPERATOR_1.role, ADMIN.accountId); - shieldedAccessControl._grantRole(OPERATOR_2.role, ADMIN.accountId); - shieldedAccessControl._grantRole(OPERATOR_3.role, ADMIN.accountId); + describe('grantRole', () => { + beforeEach(() => { + shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); + shieldedAccessControl.setPersistentCaller(ADMIN.publicKey); + }); + describe('should fail', () => { + it('when caller does not have the admin role', () => { + shieldedAccessControl.as(UNAUTHORIZED.publicKey); expect(() => shieldedAccessControl.grantRole( OPERATOR_1.role, OPERATOR_1.accountId, ), - ).not.toThrow(); - expect( - shieldedAccessControl._validateRole( - OPERATOR_1.role, - OPERATOR_1.accountId, - ), - ).toBe(true); + ).toThrow('ShieldedAccessControl: unauthorized account'); }); - it('when re-granting active role', () => { + it('when wit_getRoleCommitmentPath returns a valid path for a different role, accountId pairing', () => { + shieldedAccessControl._grantRole( + OPERATOR_1.role, + OPERATOR_1.accountId, + ); + // Override witness to return valid path for OPERATOR_1 role commitment + shieldedAccessControl.overrideWitness( + 'wit_getRoleCommitmentPath', + () => { + const privateState = shieldedAccessControl.getPrivateState(); + const operator1MtPath = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf( + OPERATOR_1.roleCommitment, + ); + if (operator1MtPath) return [privateState, operator1MtPath]; + throw new Error('Merkle tree path should be defined'); + }, + ); + expect(() => { + shieldedAccessControl.grantRole(ADMIN.role, ADMIN.accountId); + }).toThrow( + 'ShieldedAccessControl: Path must contain leaf matching computed role commitment for the provided role, accountId pairing', + ); + }); + + it('when admin with duplicate roles is revoked', () => { + // create duplicate roles + shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); + shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); + + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); expect(() => shieldedAccessControl.grantRole(ADMIN.role, ADMIN.accountId), - ).not.toThrow(); - expect( - shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), - ).toBe(true); + ).toThrow('ShieldedAccessControl: unauthorized account'); }); - it('when granting role that does not exist', () => { + it('when admin role is revoked', () => { + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); expect(() => - shieldedAccessControl.grantRole( - UNINITIALIZED.role, - UNINITIALIZED.accountId, - ), - ).not.toThrow(); - expect( - shieldedAccessControl._validateRole( - UNINITIALIZED.role, - UNINITIALIZED.accountId, - ), - ).toBe(true); + shieldedAccessControl.grantRole(ADMIN.role, ADMIN.accountId), + ).toThrow('ShieldedAccessControl: unauthorized account'); }); - it('when granting role with bad accountId', () => { - expect(() => - shieldedAccessControl.grantRole(ADMIN.role, BAD_INPUT.accountId), - ).not.toThrow(); + it('when admin provides incorrect nonce', () => { + shieldedAccessControl.privateState.injectSecretNonce( + ADMIN.role, + BAD_INPUT.secretNonce, + ); expect( - shieldedAccessControl._validateRole( + shieldedAccessControl.privateState.getCurrentSecretNonce( ADMIN.role, - BAD_INPUT.accountId, ), - ).toBe(true); + ).not.toEqual(ADMIN.secretNonce); + expect(() => + shieldedAccessControl.grantRole(ADMIN.role, ADMIN.accountId), + ).toThrow('ShieldedAccessControl: unauthorized account'); }); - }); - }); - describe('_grantRole', () => { - describe('should return true', () => { - it('when authorized user grants a new role', () => { + it('when admin provides bad witness path', () => { + shieldedAccessControl.overrideWitness( + 'wit_getRoleCommitmentPath', + RETURN_BAD_PATH, + ); + expect(() => + shieldedAccessControl.grantRole(ADMIN.role, ADMIN.accountId), + ).toThrow('ShieldedAccessControl: unauthorized account'); + }); + + it('when non-admin caller has role', () => { + shieldedAccessControl._grantRole( + OPERATOR_1.role, + OPERATOR_1.accountId, + ); + + shieldedAccessControl.as(OPERATOR_1.publicKey); + // OP_1 has role but is not authorized to grant roles to other users + expect(() => + shieldedAccessControl.grantRole( + OPERATOR_1.role, + OPERATOR_2.accountId, + ), + ).toThrow('ShieldedAccessControl: unauthorized account'); + }); + }); + + describe('should not update _operatorRoles Merkle tree', () => { + it('when role is revoked', () => { + // setup test + shieldedAccessControl._grantRole( + OPERATOR_1.role, + OPERATOR_1.accountId, + ); + shieldedAccessControl._revokeRole( + OPERATOR_1.role, + OPERATOR_1.accountId, + ); + + const initialRoot = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.root(); + shieldedAccessControl.grantRole( + OPERATOR_1.role, + OPERATOR_1.accountId, + ); + + const updatedRoot = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.root(); + expect(initialRoot).toEqual(updatedRoot); + }); + }); + + describe('should grant role', () => { + it('when caller has the admin role', () => { + expect(() => + shieldedAccessControl.grantRole( + OPERATOR_1.role, + OPERATOR_1.accountId, + ), + ).not.toThrow(); + expect( + shieldedAccessControl._validateRole( + OPERATOR_1.role, + OPERATOR_1.accountId, + ), + ).toBe(true); + }); + + it('when caller has custom admin role', () => { + // Make OPERATOR_1.role the admin of OPERATOR_2.role. + shieldedAccessControl._setRoleAdmin( + OPERATOR_2.role, + OPERATOR_1.role, + ); + // Grant OPERATOR_1.role to OPERATOR_1.accountId + shieldedAccessControl.grantRole( + OPERATOR_1.role, + OPERATOR_1.accountId, + ); + + // Switch to OPERATOR_1 as caller and inject their nonce for their role. + shieldedAccessControl.privateState.injectSecretNonce( + OPERATOR_1.role, + OPERATOR_1.secretNonce, + ); + shieldedAccessControl.setPersistentCaller(OPERATOR_1.publicKey); + + // OPERATOR_1.accountId (who holds OPERATOR_1.role) can now grant OPERATOR_2.role. + expect(() => + shieldedAccessControl.grantRole( + OPERATOR_2.role, + OPERATOR_2.accountId, + ), + ).not.toThrow(); + expect( + shieldedAccessControl._validateRole( + OPERATOR_2.role, + OPERATOR_2.accountId, + ), + ).toBe(true); + }); + + it('when admin role is revoked and re-issued with a different accountId', () => { + // setup test + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); + const newNonce = Buffer.alloc(32, 'NEW_ADMIN_NONCE'); + shieldedAccessControl.privateState.injectSecretNonce( + ADMIN.role, + newNonce, + ); + const newAccountId = buildAccountIdHash(ADMIN.zPublicKey, newNonce); + shieldedAccessControl._grantRole(ADMIN.role, newAccountId); + + expect(() => { + shieldedAccessControl.grantRole( + OPERATOR_1.role, + OPERATOR_1.accountId, + ); + }).not.toThrow(); + expect( + shieldedAccessControl._validateRole( + OPERATOR_1.role, + OPERATOR_1.accountId, + ), + ).toBe(true); + expect( + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf( + OPERATOR_1.roleCommitment, + ), + ).toBeDefined(); + }); + + it('when multiple admins of the same role exist', () => { + // setup test + const account1 = buildAccountIdHash( + OPERATOR_1.zPublicKey, + ADMIN.secretNonce, + ); + const account2 = buildAccountIdHash( + OPERATOR_2.zPublicKey, + ADMIN.secretNonce, + ); + const account3 = buildAccountIdHash( + OPERATOR_3.zPublicKey, + ADMIN.secretNonce, + ); + shieldedAccessControl._grantRole(ADMIN.role, account1); + shieldedAccessControl._grantRole(ADMIN.role, account2); + shieldedAccessControl._grantRole(ADMIN.role, account3); + + // check grant role succeeds as OP and role is valid + shieldedAccessControl.as(OPERATOR_1.publicKey); + expect(() => + shieldedAccessControl.grantRole(ADMIN.role, account1), + ).not.toThrow(); + expect( + shieldedAccessControl._validateRole(ADMIN.role, account1), + ).toBe(true); + + shieldedAccessControl.as(OPERATOR_2.publicKey); + expect(() => + shieldedAccessControl.grantRole(ADMIN.role, ADMIN.accountId), + ).not.toThrow(); + expect( + shieldedAccessControl._validateRole(ADMIN.role, account2), + ).toBe(true); + + shieldedAccessControl.as(OPERATOR_3.publicKey); + expect(() => + shieldedAccessControl.grantRole(ADMIN.role, ADMIN.accountId), + ).not.toThrow(); + expect( + shieldedAccessControl._validateRole(ADMIN.role, account3), + ).toBe(true); + }); + + it('when admin has multiple roles', () => { + shieldedAccessControl._grantRole(OPERATOR_1.role, ADMIN.accountId); + shieldedAccessControl._grantRole(OPERATOR_2.role, ADMIN.accountId); + shieldedAccessControl._grantRole(OPERATOR_3.role, ADMIN.accountId); + + expect(() => + shieldedAccessControl.grantRole( + OPERATOR_1.role, + OPERATOR_1.accountId, + ), + ).not.toThrow(); + expect( + shieldedAccessControl._validateRole( + OPERATOR_1.role, + OPERATOR_1.accountId, + ), + ).toBe(true); + }); + + it('when re-granting active role', () => { + expect(() => + shieldedAccessControl.grantRole(ADMIN.role, ADMIN.accountId), + ).not.toThrow(); + expect( + shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), + ).toBe(true); + }); + + it('when granting role that does not exist', () => { + expect(() => + shieldedAccessControl.grantRole( + UNINITIALIZED.role, + UNINITIALIZED.accountId, + ), + ).not.toThrow(); + expect( + shieldedAccessControl._validateRole( + UNINITIALIZED.role, + UNINITIALIZED.accountId, + ), + ).toBe(true); + }); + + it('when granting role with bad accountId', () => { + expect(() => + shieldedAccessControl.grantRole(ADMIN.role, BAD_INPUT.accountId), + ).not.toThrow(); + expect( + shieldedAccessControl._validateRole( + ADMIN.role, + BAD_INPUT.accountId, + ), + ).toBe(true); + }); + }); + }); + + describe('_grantRole', () => { + describe('should return true', () => { + it('when authorized user grants a new role', () => { shieldedAccessControl.as(ADMIN.publicKey); expect( shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId), @@ -1332,15 +1653,221 @@ describe('ShieldedAccessControl', () => { }); }); - describe('revokeRole', () => { - beforeEach(() => { - shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); - shieldedAccessControl._grantRole( - OPERATOR_1.role, - OPERATOR_1.accountId, - ); - shieldedAccessControl.setPersistentCaller(ADMIN.publicKey); - }); + // TODO refactor to test _uncheckedGrantRole + describe.skip('_uncheckedGrantRole', () => { + describe('should return true', () => { + it('when authorized user grants a new role', () => { + shieldedAccessControl.as(ADMIN.publicKey); + expect( + shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId), + ).toBe(true); + }); + + it('when unauthorized user grants role', () => { + shieldedAccessControl.as(UNAUTHORIZED.publicKey); + expect( + shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId), + ).toBe(true); + }); + + it('when re-granting active role ', () => { + shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); + + expect( + shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId), + ).toBe(true); + }); + + it('when granting role that does not exist', () => { + expect( + shieldedAccessControl._grantRole( + UNINITIALIZED.role, + ADMIN.accountId, + ), + ).toBe(true); + }); + + it('when granting role with bad accountId', () => { + expect( + shieldedAccessControl._grantRole(ADMIN.role, BAD_INPUT.accountId), + ).toBe(true); + }); + }); + + describe('should update _operatorRoles merkle tree', () => { + it('when authorized user grants a new role', () => { + shieldedAccessControl.setPersistentCaller(ADMIN.publicKey); + // Check caller is admin, has admin role + expect( + shieldedAccessControl.getCallerContext().currentZswapLocalState + .coinPublicKey, + ).toEqual(ADMIN.zPublicKey); + expect(shieldedAccessControl.getRoleAdmin(ADMIN.role)).toEqual( + new Uint8Array(ADMIN.role), + ); + + // check merkle tree is empty + let merkleRoot = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.root(); + expect(merkleRoot.field).toBe(0n); + + // check merkle tree is updated + shieldedAccessControl.as(ADMIN.publicKey); + shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); + merkleRoot = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.root(); + expect(merkleRoot).not.toBe(0n); + + // check path exists for new role + const merkleTreePath = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf( + ADMIN.roleCommitment, + ); + expect(merkleTreePath).toBeDefined(); + expect(merkleTreePath?.leaf).toStrictEqual(ADMIN.roleCommitment); + }); + + it('when unauthorized user grants a new role', () => { + // Check UNAUTHORIZED is not admin + expect(shieldedAccessControl.getRoleAdmin(ADMIN.role)).not.toEqual( + new Uint8Array(UNAUTHORIZED.role), + ); + expect( + shieldedAccessControl._validateRole( + ADMIN.role, + UNAUTHORIZED.accountId, + ), + ).toBe(false); + + // check merkle tree is empty + let merkleRoot = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.root(); + expect(merkleRoot.field).toBe(0n); + + // check caller is UNAUTHORIZED user + shieldedAccessControl.as(UNAUTHORIZED.publicKey); + expect( + shieldedAccessControl.getCallerContext().currentZswapLocalState + .coinPublicKey, + ).toEqual(UNAUTHORIZED.zPublicKey); + + // check merkle tree is updated + shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); + merkleRoot = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.root(); + expect(merkleRoot).not.toBe(0n); + + // check path exists for new role + const merkleTreePath = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf( + ADMIN.roleCommitment, + ); + expect(merkleTreePath).toBeDefined(); + expect(merkleTreePath?.leaf).toStrictEqual(ADMIN.roleCommitment); + }); + + it('when granting role that does not exist', () => { + // check merkle tree is empty + let merkleRoot = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.root(); + expect(merkleRoot.field).toBe(0n); + + // check merkle tree is updated + shieldedAccessControl._grantRole( + UNINITIALIZED.role, + UNINITIALIZED.accountId, + ); + merkleRoot = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.root(); + expect(merkleRoot).not.toBe(0n); + + // check path exists for new role + const merkleTreePath = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf( + UNINITIALIZED.roleCommitment, + ); + expect(merkleTreePath).toBeDefined(); + expect(merkleTreePath?.leaf).toStrictEqual( + UNINITIALIZED.roleCommitment, + ); + }); + + it('when granting role with bad accountId', () => { + // check merkle tree is empty + let merkleRoot = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.root(); + expect(merkleRoot.field).toBe(0n); + + // check merkle tree is updated + shieldedAccessControl._grantRole(ADMIN.role, BAD_INPUT.accountId); + merkleRoot = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.root(); + expect(merkleRoot).not.toBe(0n); + + // check path exists for new role + const adminRoleBadAccountCommitment = buildRoleCommitmentHash( + ADMIN.role, + BAD_INPUT.accountId, + ); + const merkleTreePath = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf( + adminRoleBadAccountCommitment, + ); + expect(merkleTreePath).toBeDefined(); + expect(merkleTreePath?.leaf).toStrictEqual( + adminRoleBadAccountCommitment, + ); + }); + }); + + describe('should return false', () => { + it('when re-granting revoked role', () => { + shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); + expect( + shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId), + ).toBe(false); + }); + }); + + describe('should not update _operatorRoles merkle tree', () => { + it('when re-granting revoked role', () => { + shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); + const merkleRoot = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.root(); + + shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); + const newMerkleRoot = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.root(); + expect(merkleRoot).toEqual(newMerkleRoot); + }); + }); + }); + + describe('revokeRole', () => { + beforeEach(() => { + shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); + shieldedAccessControl._grantRole( + OPERATOR_1.role, + OPERATOR_1.accountId, + ); + shieldedAccessControl.setPersistentCaller(ADMIN.publicKey); + }); describe('should fail', () => { it('when caller does not have the admin role', () => { @@ -1501,21 +2028,591 @@ describe('ShieldedAccessControl', () => { ); shieldedAccessControl.as(OPERATOR_1.publicKey); - expect(() => - shieldedAccessControl.revokeRole( - OPERATOR_2.role, - OPERATOR_3.accountId, - ), - ).not.toThrow(); + expect(() => + shieldedAccessControl.revokeRole( + OPERATOR_2.role, + OPERATOR_3.accountId, + ), + ).not.toThrow(); + expect( + shieldedAccessControl._validateRole( + OPERATOR_2.role, + OPERATOR_3.accountId, + ), + ).toBe(false); + }); + + it('when role does not exist', () => { + // create role commitment that doesn't exist + const commitment = buildRoleCommitmentHash( + UNINITIALIZED.role, + ADMIN.accountId, + ); + + // confirm role commitment not in Merkle tree + const path = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf(commitment); + expect(path).toBeUndefined(); + + expect(() => + shieldedAccessControl.revokeRole( + UNINITIALIZED.role, + ADMIN.accountId, + ), + ).not.toThrow(); + + expect( + shieldedAccessControl._validateRole( + UNINITIALIZED.role, + ADMIN.accountId, + ), + ).toBe(false); + }); + + it('when revoking role with bad accountId', () => { + expect(() => + shieldedAccessControl.revokeRole(ADMIN.role, BAD_INPUT.accountId), + ).not.toThrow(); + + expect( + shieldedAccessControl._validateRole( + ADMIN.role, + BAD_INPUT.accountId, + ), + ).toBe(false); + }); + + it('when multiple admins of the same role exist', () => { + // setup test + const account1 = buildAccountIdHash( + OPERATOR_1.zPublicKey, + ADMIN.secretNonce, + ); + const account2 = buildAccountIdHash( + OPERATOR_2.zPublicKey, + ADMIN.secretNonce, + ); + const account3 = buildAccountIdHash( + OPERATOR_3.zPublicKey, + ADMIN.secretNonce, + ); + shieldedAccessControl._grantRole(ADMIN.role, account1); + shieldedAccessControl._grantRole(ADMIN.role, account2); + shieldedAccessControl._grantRole(ADMIN.role, account3); + + // check revoke role succeeds as OP and role is valid + shieldedAccessControl.as(OPERATOR_1.publicKey); + expect(() => + shieldedAccessControl.revokeRole(ADMIN.role, account1), + ).not.toThrow(); + expect( + shieldedAccessControl._validateRole(ADMIN.role, account1), + ).toBe(false); + + shieldedAccessControl.as(OPERATOR_2.publicKey); + expect(() => + shieldedAccessControl.revokeRole(ADMIN.role, account2), + ).not.toThrow(); + expect( + shieldedAccessControl._validateRole(ADMIN.role, account2), + ).toBe(false); + + shieldedAccessControl.as(OPERATOR_3.publicKey); + expect(() => + shieldedAccessControl.revokeRole(ADMIN.role, account3), + ).not.toThrow(); + expect( + shieldedAccessControl._validateRole(ADMIN.role, account3), + ).toBe(false); + }); + + it('when admin has multiple roles', () => { + shieldedAccessControl._grantRole(OPERATOR_1.role, ADMIN.accountId); + shieldedAccessControl._grantRole(OPERATOR_2.role, ADMIN.accountId); + shieldedAccessControl._grantRole(OPERATOR_3.role, ADMIN.accountId); + + expect(() => + shieldedAccessControl.revokeRole( + OPERATOR_1.role, + OPERATOR_1.accountId, + ), + ).not.toThrow(); + expect( + shieldedAccessControl._validateRole( + OPERATOR_1.role, + OPERATOR_1.accountId, + ), + ).toBe(false); + }); + + it('when revoking role that does not exist', () => { + expect(() => + shieldedAccessControl.revokeRole( + UNINITIALIZED.role, + UNINITIALIZED.accountId, + ), + ).not.toThrow(); + expect( + shieldedAccessControl._validateRole( + UNINITIALIZED.role, + UNINITIALIZED.accountId, + ), + ).toBe(false); + }); + + it('when admin role is revoked and re-issued with a different accountId', () => { + // setup test + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); + const newNonce = Buffer.alloc(32, 'NEW_ADMIN_NONCE'); + shieldedAccessControl.privateState.injectSecretNonce( + ADMIN.role, + newNonce, + ); + const newAccountId = buildAccountIdHash(ADMIN.zPublicKey, newNonce); + shieldedAccessControl._grantRole(ADMIN.role, newAccountId); + + expect(() => { + shieldedAccessControl.revokeRole( + OPERATOR_1.role, + OPERATOR_1.accountId, + ); + }).not.toThrow(); + expect( + shieldedAccessControl._validateRole( + OPERATOR_1.role, + OPERATOR_1.accountId, + ), + ).toBe(false); + }); + }); + }); + + describe('_revokeRole', () => { + beforeEach(() => { + shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); + shieldedAccessControl.setPersistentCaller(ADMIN.publicKey); + }); + + describe('should return true', () => { + it('when active role is revoked', () => { + // confirm role is active + const isValidRole = shieldedAccessControl._validateRole( + ADMIN.role, + ADMIN.accountId, + ); + expect(isValidRole).toBe(true); + + expect( + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId), + ).toBe(true); + }); + + it('when an authorized user revokes role', () => { + // Check caller is admin, has admin role + expect( + shieldedAccessControl.getCallerContext().currentZswapLocalState + .coinPublicKey, + ).toEqual(ADMIN.zPublicKey); + expect(shieldedAccessControl.getRoleAdmin(ADMIN.role)).toEqual( + new Uint8Array(ADMIN.role), + ); + expect( + shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), + ).toBe(true); + + expect( + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId), + ).toBe(true); + }); + + it('when unauthorized user revokes role', () => { + // Check UNAUTHORIZED is not admin + expect(shieldedAccessControl.getRoleAdmin(ADMIN.role)).not.toEqual( + new Uint8Array(UNAUTHORIZED.role), + ); + expect( + shieldedAccessControl._validateRole( + ADMIN.role, + UNAUTHORIZED.accountId, + ), + ).toBe(false); + + // check caller is UNAUTHORIZED user + shieldedAccessControl.as(UNAUTHORIZED.publicKey); + expect( + shieldedAccessControl.getCallerContext().currentZswapLocalState + .coinPublicKey, + ).toEqual(UNAUTHORIZED.zPublicKey); + expect( + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId), + ).toBe(true); + }); + + it('when revoking role that does not exist', () => { + // create role commitment that doesn't exist + const commitment = buildRoleCommitmentHash( + UNINITIALIZED.role, + ADMIN.accountId, + ); + + // confirm role commitment not in Merkle tree + const path = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf(commitment); + expect(path).toBeUndefined(); + + expect( + shieldedAccessControl._revokeRole( + UNINITIALIZED.role, + ADMIN.accountId, + ), + ).toBe(true); + }); + + it('when revoking role with bad accountId', () => { + expect( + shieldedAccessControl._revokeRole( + ADMIN.role, + BAD_INPUT.accountId, + ), + ).toBe(true); + }); + }); + + describe('should update nullifier set', () => { + it('when active role is revoked', () => { + // confirm role is active + const isValidRole = shieldedAccessControl._validateRole( + ADMIN.role, + ADMIN.accountId, + ); + expect(isValidRole).toBe(true); + + const initialSetSize = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.size(); + expect(initialSetSize).toBe(0n); + + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); + + const updatedSetSize = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.size(); + expect(updatedSetSize).toBe(1n); + expect( + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.member( + ADMIN.roleNullifier, + ), + ).toBe(true); + }); + + it('when an authorized user revokes role', () => { + // Check caller is admin, has admin role + expect( + shieldedAccessControl.getCallerContext().currentZswapLocalState + .coinPublicKey, + ).toEqual(ADMIN.zPublicKey); + expect(shieldedAccessControl.getRoleAdmin(ADMIN.role)).toEqual( + new Uint8Array(ADMIN.role), + ); + expect( + shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), + ).toBe(true); + + const initialSetSize = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.size(); + expect(initialSetSize).toBe(0n); + + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); + + const updatedSetSize = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.size(); + expect(updatedSetSize).toBe(1n); + expect( + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.member( + ADMIN.roleNullifier, + ), + ).toBe(true); + }); + + it('when unauthorized user revokes role', () => { + // Check UNAUTHORIZED is not admin + expect(shieldedAccessControl.getRoleAdmin(ADMIN.role)).not.toEqual( + new Uint8Array(UNAUTHORIZED.role), + ); + expect( + shieldedAccessControl._validateRole( + ADMIN.role, + UNAUTHORIZED.accountId, + ), + ).toBe(false); + + const initialSetSize = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.size(); + expect(initialSetSize).toBe(0n); + + // check caller is UNAUTHORIZED user + shieldedAccessControl.as(UNAUTHORIZED.publicKey); + expect( + shieldedAccessControl.getCallerContext().currentZswapLocalState + .coinPublicKey, + ).toEqual(UNAUTHORIZED.zPublicKey); + + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); + + const updatedSetSize = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.size(); + expect(updatedSetSize).toBe(1n); + expect( + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.member( + ADMIN.roleNullifier, + ), + ).toBe(true); + }); + + it('when revoking role that does not exist', () => { + // create role commitment that doesn't exist + const commitment = buildRoleCommitmentHash( + UNINITIALIZED.role, + ADMIN.accountId, + ); + + // confirm role commitment not in Merkle tree + const path = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf(commitment); + expect(path).toBeUndefined(); + + const initialSetSize = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.size(); + expect(initialSetSize).toBe(0n); + + shieldedAccessControl._revokeRole( + UNINITIALIZED.role, + ADMIN.accountId, + ); + + const updatedSetSize = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.size(); + expect(updatedSetSize).toBe(1n); + + const nullifier = buildNullifierHash(commitment); + + expect( + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.member( + nullifier, + ), + ).toBe(true); + }); + + it('when revoking role with bad accountId', () => { + const initialSetSize = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.size(); + expect(initialSetSize).toBe(0n); + + shieldedAccessControl._revokeRole(ADMIN.role, BAD_INPUT.accountId); + + const updatedSetSize = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.size(); + expect(updatedSetSize).toBe(1n); + + const commitment = buildRoleCommitmentHash( + ADMIN.role, + BAD_INPUT.accountId, + ); + const nullifier = buildNullifierHash(commitment); + expect( + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.member( + nullifier, + ), + ).toBe(true); + }); + }); + + describe('should return false', () => { + it('when authorized user re-revokes role', () => { + // Check caller is admin, has admin role + expect( + shieldedAccessControl.getCallerContext().currentZswapLocalState + .coinPublicKey, + ).toEqual(ADMIN.zPublicKey); + expect(shieldedAccessControl.getRoleAdmin(ADMIN.role)).toEqual( + new Uint8Array(ADMIN.role), + ); + expect( + shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), + ).toBe(true); + + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); + expect( + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId), + ).toBe(false); + }); + + it('when unauthorized user re-revokes role', () => { + // Check UNAUTHORIZED is not admin + expect(shieldedAccessControl.getRoleAdmin(ADMIN.role)).not.toEqual( + new Uint8Array(UNAUTHORIZED.role), + ); + expect( + shieldedAccessControl._validateRole( + ADMIN.role, + UNAUTHORIZED.accountId, + ), + ).toBe(false); + + // revoke as ADMIN + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); + + // check caller is UNAUTHORIZED user + shieldedAccessControl.as(UNAUTHORIZED.publicKey); + expect( + shieldedAccessControl.getCallerContext().currentZswapLocalState + .coinPublicKey, + ).toEqual(UNAUTHORIZED.zPublicKey); + expect( + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId), + ).toBe(false); + }); + }); + + describe('should not update nullifier set', () => { + it('when authorized user re-revokes role', () => { + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); + const initialSetSize = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.size(); + expect(initialSetSize).toBe(1n); + + // Check caller is admin, doesn't have admin role + expect( + shieldedAccessControl.getCallerContext().currentZswapLocalState + .coinPublicKey, + ).toEqual(ADMIN.zPublicKey); + expect( + shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), + ).toBe(false); + + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); + + const updatedSetSize = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.size(); + expect(updatedSetSize).toEqual(initialSetSize); + }); + + it('when unauthorized user re-revokes role', () => { + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); + const initialSetSize = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.size(); + expect(initialSetSize).toBe(1n); + + // Check UNAUTHORIZED is not admin + expect(shieldedAccessControl.getRoleAdmin(ADMIN.role)).not.toEqual( + new Uint8Array(UNAUTHORIZED.role), + ); + expect( + shieldedAccessControl._validateRole( + ADMIN.role, + UNAUTHORIZED.accountId, + ), + ).toBe(false); + + // re-revoke as UNAUTHORIZED + shieldedAccessControl.as(UNAUTHORIZED.publicKey); + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); + + const updatedSetSize = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.size(); + expect(updatedSetSize).toEqual(initialSetSize); + }); + }); + }); + + // TODO refactor to test _uncheckedGrantRole + describe.skip('_uncheckedRevokeRole', () => { + beforeEach(() => { + shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); + shieldedAccessControl.setPersistentCaller(ADMIN.publicKey); + }); + + describe('should return true', () => { + it('when active role is revoked', () => { + // confirm role is active + const isValidRole = shieldedAccessControl._validateRole( + ADMIN.role, + ADMIN.accountId, + ); + expect(isValidRole).toBe(true); + + expect( + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId), + ).toBe(true); + }); + + it('when an authorized user revokes role', () => { + // Check caller is admin, has admin role + expect( + shieldedAccessControl.getCallerContext().currentZswapLocalState + .coinPublicKey, + ).toEqual(ADMIN.zPublicKey); + expect(shieldedAccessControl.getRoleAdmin(ADMIN.role)).toEqual( + new Uint8Array(ADMIN.role), + ); + expect( + shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), + ).toBe(true); + + expect( + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId), + ).toBe(true); + }); + + it('when unauthorized user revokes role', () => { + // Check UNAUTHORIZED is not admin + expect(shieldedAccessControl.getRoleAdmin(ADMIN.role)).not.toEqual( + new Uint8Array(UNAUTHORIZED.role), + ); expect( shieldedAccessControl._validateRole( - OPERATOR_2.role, - OPERATOR_3.accountId, + ADMIN.role, + UNAUTHORIZED.accountId, ), ).toBe(false); + + // check caller is UNAUTHORIZED user + shieldedAccessControl.as(UNAUTHORIZED.publicKey); + expect( + shieldedAccessControl.getCallerContext().currentZswapLocalState + .coinPublicKey, + ).toEqual(UNAUTHORIZED.zPublicKey); + expect( + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId), + ).toBe(true); }); - it('when role does not exist', () => { + it('when revoking role that does not exist', () => { // create role commitment that doesn't exist const commitment = buildRoleCommitmentHash( UNINITIALIZED.role, @@ -1528,160 +2625,194 @@ describe('ShieldedAccessControl', () => { .ShieldedAccessControl__operatorRoles.findPathForLeaf(commitment); expect(path).toBeUndefined(); - expect(() => - shieldedAccessControl.revokeRole( - UNINITIALIZED.role, - ADMIN.accountId, - ), - ).not.toThrow(); - expect( - shieldedAccessControl._validateRole( + shieldedAccessControl._revokeRole( UNINITIALIZED.role, ADMIN.accountId, ), - ).toBe(false); + ).toBe(true); }); it('when revoking role with bad accountId', () => { - expect(() => - shieldedAccessControl.revokeRole(ADMIN.role, BAD_INPUT.accountId), - ).not.toThrow(); - expect( - shieldedAccessControl._validateRole( + shieldedAccessControl._revokeRole( ADMIN.role, BAD_INPUT.accountId, ), - ).toBe(false); + ).toBe(true); }); + }); - it('when multiple admins of the same role exist', () => { - // setup test - const account1 = buildAccountIdHash( - OPERATOR_1.zPublicKey, - ADMIN.secretNonce, - ); - const account2 = buildAccountIdHash( - OPERATOR_2.zPublicKey, - ADMIN.secretNonce, - ); - const account3 = buildAccountIdHash( - OPERATOR_3.zPublicKey, - ADMIN.secretNonce, + describe('should update nullifier set', () => { + it('when active role is revoked', () => { + // confirm role is active + const isValidRole = shieldedAccessControl._validateRole( + ADMIN.role, + ADMIN.accountId, ); - shieldedAccessControl._grantRole(ADMIN.role, account1); - shieldedAccessControl._grantRole(ADMIN.role, account2); - shieldedAccessControl._grantRole(ADMIN.role, account3); + expect(isValidRole).toBe(true); - // check revoke role succeeds as OP and role is valid - shieldedAccessControl.as(OPERATOR_1.publicKey); - expect(() => - shieldedAccessControl.revokeRole(ADMIN.role, account1), - ).not.toThrow(); - expect( - shieldedAccessControl._validateRole(ADMIN.role, account1), - ).toBe(false); + const initialSetSize = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.size(); + expect(initialSetSize).toBe(0n); - shieldedAccessControl.as(OPERATOR_2.publicKey); - expect(() => - shieldedAccessControl.revokeRole(ADMIN.role, account2), - ).not.toThrow(); - expect( - shieldedAccessControl._validateRole(ADMIN.role, account2), - ).toBe(false); + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); - shieldedAccessControl.as(OPERATOR_3.publicKey); - expect(() => - shieldedAccessControl.revokeRole(ADMIN.role, account3), - ).not.toThrow(); + const updatedSetSize = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.size(); + expect(updatedSetSize).toBe(1n); expect( - shieldedAccessControl._validateRole(ADMIN.role, account3), - ).toBe(false); + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.member( + ADMIN.roleNullifier, + ), + ).toBe(true); }); - it('when admin has multiple roles', () => { - shieldedAccessControl._grantRole(OPERATOR_1.role, ADMIN.accountId); - shieldedAccessControl._grantRole(OPERATOR_2.role, ADMIN.accountId); - shieldedAccessControl._grantRole(OPERATOR_3.role, ADMIN.accountId); + it('when an authorized user revokes role', () => { + // Check caller is admin, has admin role + expect( + shieldedAccessControl.getCallerContext().currentZswapLocalState + .coinPublicKey, + ).toEqual(ADMIN.zPublicKey); + expect(shieldedAccessControl.getRoleAdmin(ADMIN.role)).toEqual( + new Uint8Array(ADMIN.role), + ); + expect( + shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), + ).toBe(true); - expect(() => - shieldedAccessControl.revokeRole( - OPERATOR_1.role, - OPERATOR_1.accountId, - ), - ).not.toThrow(); + const initialSetSize = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.size(); + expect(initialSetSize).toBe(0n); + + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); + + const updatedSetSize = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.size(); + expect(updatedSetSize).toBe(1n); expect( - shieldedAccessControl._validateRole( - OPERATOR_1.role, - OPERATOR_1.accountId, - ), - ).toBe(false); + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.member( + ADMIN.roleNullifier, + ), + ).toBe(true); }); - it('when revoking role that does not exist', () => { - expect(() => - shieldedAccessControl.revokeRole( - UNINITIALIZED.role, - UNINITIALIZED.accountId, - ), - ).not.toThrow(); + it('when unauthorized user revokes role', () => { + // Check UNAUTHORIZED is not admin + expect(shieldedAccessControl.getRoleAdmin(ADMIN.role)).not.toEqual( + new Uint8Array(UNAUTHORIZED.role), + ); expect( shieldedAccessControl._validateRole( - UNINITIALIZED.role, - UNINITIALIZED.accountId, + ADMIN.role, + UNAUTHORIZED.accountId, ), ).toBe(false); - }); - it('when admin role is revoked and re-issued with a different accountId', () => { - // setup test + const initialSetSize = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.size(); + expect(initialSetSize).toBe(0n); + + // check caller is UNAUTHORIZED user + shieldedAccessControl.as(UNAUTHORIZED.publicKey); + expect( + shieldedAccessControl.getCallerContext().currentZswapLocalState + .coinPublicKey, + ).toEqual(UNAUTHORIZED.zPublicKey); + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); - const newNonce = Buffer.alloc(32, 'NEW_ADMIN_NONCE'); - shieldedAccessControl.privateState.injectSecretNonce( - ADMIN.role, - newNonce, + + const updatedSetSize = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.size(); + expect(updatedSetSize).toBe(1n); + expect( + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.member( + ADMIN.roleNullifier, + ), + ).toBe(true); + }); + + it('when revoking role that does not exist', () => { + // create role commitment that doesn't exist + const commitment = buildRoleCommitmentHash( + UNINITIALIZED.role, + ADMIN.accountId, ); - const newAccountId = buildAccountIdHash(ADMIN.zPublicKey, newNonce); - shieldedAccessControl._grantRole(ADMIN.role, newAccountId); - expect(() => { - shieldedAccessControl.revokeRole( - OPERATOR_1.role, - OPERATOR_1.accountId, - ); - }).not.toThrow(); + // confirm role commitment not in Merkle tree + const path = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf(commitment); + expect(path).toBeUndefined(); + + const initialSetSize = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.size(); + expect(initialSetSize).toBe(0n); + + shieldedAccessControl._revokeRole( + UNINITIALIZED.role, + ADMIN.accountId, + ); + + const updatedSetSize = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.size(); + expect(updatedSetSize).toBe(1n); + + const nullifier = buildNullifierHash(commitment); + expect( - shieldedAccessControl._validateRole( - OPERATOR_1.role, - OPERATOR_1.accountId, - ), - ).toBe(false); + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.member( + nullifier, + ), + ).toBe(true); }); - }); - }); - describe('_revokeRole', () => { - beforeEach(() => { - shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); - shieldedAccessControl.setPersistentCaller(ADMIN.publicKey); - }); + it('when revoking role with bad accountId', () => { + const initialSetSize = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.size(); + expect(initialSetSize).toBe(0n); + + shieldedAccessControl._revokeRole(ADMIN.role, BAD_INPUT.accountId); + + const updatedSetSize = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.size(); + expect(updatedSetSize).toBe(1n); - describe('should return true', () => { - it('when active role is revoked', () => { - // confirm role is active - const isValidRole = shieldedAccessControl._validateRole( + const commitment = buildRoleCommitmentHash( ADMIN.role, - ADMIN.accountId, + BAD_INPUT.accountId, ); - expect(isValidRole).toBe(true); - + const nullifier = buildNullifierHash(commitment); expect( - shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId), + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.member( + nullifier, + ), ).toBe(true); }); + }); - it('when an authorized user revokes role', () => { + describe('should return false', () => { + it('when authorized user re-revokes role', () => { // Check caller is admin, has admin role expect( shieldedAccessControl.getCallerContext().currentZswapLocalState @@ -1694,12 +2825,13 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), ).toBe(true); + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); expect( shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId), - ).toBe(true); + ).toBe(false); }); - it('when unauthorized user revokes role', () => { + it('when unauthorized user re-revokes role', () => { // Check UNAUTHORIZED is not admin expect(shieldedAccessControl.getRoleAdmin(ADMIN.role)).not.toEqual( new Uint8Array(UNAUTHORIZED.role), @@ -1711,6 +2843,9 @@ describe('ShieldedAccessControl', () => { ), ).toBe(false); + // revoke as ADMIN + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); + // check caller is UNAUTHORIZED user shieldedAccessControl.as(UNAUTHORIZED.publicKey); expect( @@ -1719,311 +2854,333 @@ describe('ShieldedAccessControl', () => { ).toEqual(UNAUTHORIZED.zPublicKey); expect( shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId), - ).toBe(true); + ).toBe(false); }); + }); - it('when revoking role that does not exist', () => { - // create role commitment that doesn't exist - const commitment = buildRoleCommitmentHash( - UNINITIALIZED.role, - ADMIN.accountId, - ); - - // confirm role commitment not in Merkle tree - const path = shieldedAccessControl + describe('should not update nullifier set', () => { + it('when authorized user re-revokes role', () => { + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); + const initialSetSize = shieldedAccessControl .getPublicState() - .ShieldedAccessControl__operatorRoles.findPathForLeaf(commitment); - expect(path).toBeUndefined(); + .ShieldedAccessControl__roleCommitmentNullifiers.size(); + expect(initialSetSize).toBe(1n); + // Check caller is admin, doesn't have admin role expect( - shieldedAccessControl._revokeRole( - UNINITIALIZED.role, - ADMIN.accountId, - ), - ).toBe(true); - }); - - it('when revoking role with bad accountId', () => { + shieldedAccessControl.getCallerContext().currentZswapLocalState + .coinPublicKey, + ).toEqual(ADMIN.zPublicKey); expect( - shieldedAccessControl._revokeRole( - ADMIN.role, - BAD_INPUT.accountId, - ), - ).toBe(true); - }); - }); + shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), + ).toBe(false); - describe('should update nullifier set', () => { - it('when active role is revoked', () => { - // confirm role is active - const isValidRole = shieldedAccessControl._validateRole( - ADMIN.role, - ADMIN.accountId, - ); - expect(isValidRole).toBe(true); + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); + const updatedSetSize = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.size(); + expect(updatedSetSize).toEqual(initialSetSize); + }); + + it('when unauthorized user re-revokes role', () => { + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); const initialSetSize = shieldedAccessControl .getPublicState() .ShieldedAccessControl__roleCommitmentNullifiers.size(); - expect(initialSetSize).toBe(0n); + expect(initialSetSize).toBe(1n); + + // Check UNAUTHORIZED is not admin + expect(shieldedAccessControl.getRoleAdmin(ADMIN.role)).not.toEqual( + new Uint8Array(UNAUTHORIZED.role), + ); + expect( + shieldedAccessControl._validateRole( + ADMIN.role, + UNAUTHORIZED.accountId, + ), + ).toBe(false); + // re-revoke as UNAUTHORIZED + shieldedAccessControl.as(UNAUTHORIZED.publicKey); shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); const updatedSetSize = shieldedAccessControl .getPublicState() .ShieldedAccessControl__roleCommitmentNullifiers.size(); - expect(updatedSetSize).toBe(1n); - expect( - shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.member( - ADMIN.roleNullifier, - ), - ).toBe(true); + expect(updatedSetSize).toEqual(initialSetSize); }); + }); + }); - it('when an authorized user revokes role', () => { + describe('proveCallerRole', () => { + beforeEach(() => { + shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); + shieldedAccessControl.setPersistentCaller(ADMIN.publicKey); + }); + + it('should fail when caller provides valid path for a different role, accountId pairing', () => { + shieldedAccessControl._grantRole( + OPERATOR_1.role, + OPERATOR_1.accountId, + ); + // Override witness to return valid path for OPERATOR_1 role commitment + shieldedAccessControl.overrideWitness( + 'wit_getRoleCommitmentPath', + () => { + const privateState = shieldedAccessControl.getPrivateState(); + const operator1MtPath = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf( + OPERATOR_1.roleCommitment, + ); + if (operator1MtPath) return [privateState, operator1MtPath]; + throw new Error('Merkle tree path should be defined'); + }, + ); + expect(() => { + shieldedAccessControl.proveCallerRole(ADMIN.role); + }).toThrow( + 'ShieldedAccessControl: Path must contain leaf matching computed role commitment for the provided role, accountId pairing', + ); + }); + + describe('should return true', () => { + it('when caller has role', () => { // Check caller is admin, has admin role expect( shieldedAccessControl.getCallerContext().currentZswapLocalState .coinPublicKey, ).toEqual(ADMIN.zPublicKey); - expect(shieldedAccessControl.getRoleAdmin(ADMIN.role)).toEqual( - new Uint8Array(ADMIN.role), - ); expect( shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), ).toBe(true); - const initialSetSize = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.size(); - expect(initialSetSize).toBe(0n); + expect(shieldedAccessControl.proveCallerRole(ADMIN.role)).toBe( + true, + ); + }); + + it('when caller has multiple roles', () => { + // setup test + shieldedAccessControl.privateState.injectSecretNonce( + OPERATOR_1.role, + OPERATOR_1.secretNonce, + ); + shieldedAccessControl.privateState.injectSecretNonce( + OPERATOR_2.role, + OPERATOR_2.secretNonce, + ); + shieldedAccessControl.privateState.injectSecretNonce( + OPERATOR_3.role, + OPERATOR_3.secretNonce, + ); + const account1 = buildAccountIdHash( + ADMIN.zPublicKey, + OPERATOR_1.secretNonce, + ); + const account2 = buildAccountIdHash( + ADMIN.zPublicKey, + OPERATOR_2.secretNonce, + ); + const account3 = buildAccountIdHash( + ADMIN.zPublicKey, + OPERATOR_3.secretNonce, + ); + shieldedAccessControl._grantRole(OPERATOR_1.role, account1); + shieldedAccessControl._grantRole(OPERATOR_2.role, account2); + shieldedAccessControl._grantRole(OPERATOR_3.role, account3); + + expect(shieldedAccessControl.proveCallerRole(ADMIN.role)).toBe( + true, + ); + expect(shieldedAccessControl.proveCallerRole(OPERATOR_1.role)).toBe( + true, + ); + expect(shieldedAccessControl.proveCallerRole(OPERATOR_2.role)).toBe( + true, + ); + expect(shieldedAccessControl.proveCallerRole(OPERATOR_3.role)).toBe( + true, + ); + }); + it('when role is revoked and re-issued with a different accountId', () => { shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); - const updatedSetSize = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.size(); - expect(updatedSetSize).toBe(1n); - expect( - shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.member( - ADMIN.roleNullifier, - ), - ).toBe(true); + shieldedAccessControl.privateState.injectSecretNonce( + ADMIN.role, + Buffer.alloc(32, 'NEW_ADMIN_NONCE'), + ); + const newAdminAccountId = buildAccountIdHash( + ADMIN.zPublicKey, + shieldedAccessControl.privateState.getCurrentSecretNonce( + ADMIN.role, + ), + ); + expect(newAdminAccountId).not.toEqual(ADMIN.accountId); + + shieldedAccessControl._grantRole(ADMIN.role, newAdminAccountId); + expect(shieldedAccessControl.proveCallerRole(ADMIN.role)).toBe( + true, + ); + }); + + it('when multiple users have the same role', () => { + // All users will use OPERATOR_1.secretNonce as their nonce value + // when generating their accountId for simplicity + shieldedAccessControl.privateState.injectSecretNonce( + OPERATOR_1.role, + OPERATOR_1.secretNonce, + ); + // A unique accountId must be constructed for each new role using its associated secretNonce + const operator1AdminAccountId = buildAccountIdHash( + ADMIN.zPublicKey, + OPERATOR_1.secretNonce, + ); + shieldedAccessControl._grantRole( + OPERATOR_1.role, + operator1AdminAccountId, + ); + shieldedAccessControl.as(ADMIN.publicKey); // prove ADMIN has OP_1 role + expect(shieldedAccessControl.proveCallerRole(OPERATOR_1.role)).toBe( + true, + ); + + const operator1Op2AccountId = buildAccountIdHash( + OPERATOR_2.zPublicKey, + OPERATOR_1.secretNonce, + ); + shieldedAccessControl._grantRole( + OPERATOR_1.role, + operator1Op2AccountId, + ); + shieldedAccessControl.as(OPERATOR_2.publicKey); // prove OP_2 has OP_1 role + expect(shieldedAccessControl.proveCallerRole(OPERATOR_1.role)).toBe( + true, + ); + + const operator1Op3AccountId = buildAccountIdHash( + OPERATOR_3.zPublicKey, + OPERATOR_1.secretNonce, + ); + shieldedAccessControl._grantRole( + OPERATOR_1.role, + operator1Op3AccountId, + ); + shieldedAccessControl.as(OPERATOR_3.publicKey); // prove OP_3 has OP_1 role + expect(shieldedAccessControl.proveCallerRole(OPERATOR_1.role)).toBe( + true, + ); }); + }); - it('when unauthorized user revokes role', () => { - // Check UNAUTHORIZED is not admin - expect(shieldedAccessControl.getRoleAdmin(ADMIN.role)).not.toEqual( - new Uint8Array(UNAUTHORIZED.role), + describe('should return false', () => { + it('when caller does not have role', () => { + // setup test + shieldedAccessControl.privateState.injectSecretNonce( + OPERATOR_1.role, + OPERATOR_1.secretNonce, + ); + const accountId = buildAccountIdHash( + ADMIN.zPublicKey, + OPERATOR_1.secretNonce, ); + + // Check does not have OPERATOR role expect( - shieldedAccessControl._validateRole( - ADMIN.role, - UNAUTHORIZED.accountId, - ), + shieldedAccessControl._validateRole(OPERATOR_1.role, accountId), ).toBe(false); - const initialSetSize = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.size(); - expect(initialSetSize).toBe(0n); - - // check caller is UNAUTHORIZED user - shieldedAccessControl.as(UNAUTHORIZED.publicKey); - expect( - shieldedAccessControl.getCallerContext().currentZswapLocalState - .coinPublicKey, - ).toEqual(UNAUTHORIZED.zPublicKey); + expect(shieldedAccessControl.proveCallerRole(OPERATOR_1.role)).toBe( + false, + ); + }); + it('when caller has revoked role', () => { shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); - const updatedSetSize = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.size(); - expect(updatedSetSize).toBe(1n); + // check role revoked expect( - shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.member( - ADMIN.roleNullifier, - ), - ).toBe(true); - }); + shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), + ).toBe(false); - it('when revoking role that does not exist', () => { - // create role commitment that doesn't exist - const commitment = buildRoleCommitmentHash( - UNINITIALIZED.role, - ADMIN.accountId, + expect(shieldedAccessControl.proveCallerRole(ADMIN.role)).toBe( + false, ); + }); - // confirm role commitment not in Merkle tree - const path = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.findPathForLeaf(commitment); - expect(path).toBeUndefined(); - - const initialSetSize = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.size(); - expect(initialSetSize).toBe(0n); + it('when revoked role is re-granted', () => { + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); + // check role revoked + expect( + shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), + ).toBe(false); - shieldedAccessControl._revokeRole( - UNINITIALIZED.role, - ADMIN.accountId, + shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); + expect(shieldedAccessControl.proveCallerRole(ADMIN.role)).toBe( + false, ); - - const updatedSetSize = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.size(); - expect(updatedSetSize).toBe(1n); - - const nullifier = buildNullifierHash(commitment); - - expect( - shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.member( - nullifier, - ), - ).toBe(true); }); - it('when revoking role with bad accountId', () => { - const initialSetSize = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.size(); - expect(initialSetSize).toBe(0n); - - shieldedAccessControl._revokeRole(ADMIN.role, BAD_INPUT.accountId); - - const updatedSetSize = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.size(); - expect(updatedSetSize).toBe(1n); - - const commitment = buildRoleCommitmentHash( - ADMIN.role, - BAD_INPUT.accountId, + it('when an unauthorized caller has valid nonce', () => { + // UNAUTHORIZED uses the same private state (ADMIN.secretNonce for ADMIN.role), + // so their derived accountId won't match the committed one. + shieldedAccessControl.as(UNAUTHORIZED.publicKey); + expect(shieldedAccessControl.proveCallerRole(ADMIN.role)).toBe( + false, ); - const nullifier = buildNullifierHash(commitment); - expect( - shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.member( - nullifier, - ), - ).toBe(true); }); - }); - describe('should return false', () => { - it('when authorized user re-revokes role', () => { + it('when an authorized caller provides invalid nonce', () => { // Check caller is admin, has admin role expect( shieldedAccessControl.getCallerContext().currentZswapLocalState .coinPublicKey, ).toEqual(ADMIN.zPublicKey); - expect(shieldedAccessControl.getRoleAdmin(ADMIN.role)).toEqual( - new Uint8Array(ADMIN.role), - ); expect( shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), ).toBe(true); - shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); - expect( - shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId), - ).toBe(false); - }); - - it('when unauthorized user re-revokes role', () => { - // Check UNAUTHORIZED is not admin - expect(shieldedAccessControl.getRoleAdmin(ADMIN.role)).not.toEqual( - new Uint8Array(UNAUTHORIZED.role), + shieldedAccessControl.privateState.injectSecretNonce( + ADMIN.role, + BAD_INPUT.secretNonce, ); - expect( - shieldedAccessControl._validateRole( + // nonce should not match + expect(ADMIN.secretNonce).not.toEqual( + shieldedAccessControl.privateState.getCurrentSecretNonce( ADMIN.role, - UNAUTHORIZED.accountId, ), - ).toBe(false); - - // revoke as ADMIN - shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); + ); - // check caller is UNAUTHORIZED user - shieldedAccessControl.as(UNAUTHORIZED.publicKey); - expect( - shieldedAccessControl.getCallerContext().currentZswapLocalState - .coinPublicKey, - ).toEqual(UNAUTHORIZED.zPublicKey); - expect( - shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId), - ).toBe(false); + expect(shieldedAccessControl.proveCallerRole(ADMIN.role)).toBe( + false, + ); }); - }); - - describe('should not update nullifier set', () => { - it('when authorized user re-revokes role', () => { - shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); - const initialSetSize = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.size(); - expect(initialSetSize).toBe(1n); - // Check caller is admin, doesn't have admin role + it('when an authorized caller provides invalid witness path', () => { + // Check caller is admin, has admin role expect( shieldedAccessControl.getCallerContext().currentZswapLocalState .coinPublicKey, ).toEqual(ADMIN.zPublicKey); expect( shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), - ).toBe(false); - - shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); - - const updatedSetSize = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.size(); - expect(updatedSetSize).toEqual(initialSetSize); - }); - - it('when unauthorized user re-revokes role', () => { - shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); - const initialSetSize = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.size(); - expect(initialSetSize).toBe(1n); + ).toBe(true); - // Check UNAUTHORIZED is not admin - expect(shieldedAccessControl.getRoleAdmin(ADMIN.role)).not.toEqual( - new Uint8Array(UNAUTHORIZED.role), + shieldedAccessControl.overrideWitness( + 'wit_getRoleCommitmentPath', + RETURN_BAD_PATH, + ); + expect(shieldedAccessControl.proveCallerRole(ADMIN.role)).toBe( + false, ); - expect( - shieldedAccessControl._validateRole( - ADMIN.role, - UNAUTHORIZED.accountId, - ), - ).toBe(false); - - // re-revoke as UNAUTHORIZED - shieldedAccessControl.as(UNAUTHORIZED.publicKey); - shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); - - const updatedSetSize = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.size(); - expect(updatedSetSize).toEqual(initialSetSize); }); }); }); - describe('proveCallerRole', () => { + // TODO refactor to test _uncheckedProveCallerRole + describe.skip('_uncheckedProveCallerRole', () => { beforeEach(() => { shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); shieldedAccessControl.setPersistentCaller(ADMIN.publicKey); diff --git a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact index cc5025a2..ceccd434 100644 --- a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact +++ b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact @@ -135,7 +135,7 @@ export circuit grantRole( ShieldedAccessControl_grantRole(roleId, accountId); } -circuit _uncheckedGrantRole( +export circuit _uncheckedGrantRole( role: ShieldedAccessControl_RoleIdentifier, accountId: ShieldedAccessControl_AccountIdentifier ): Boolean { @@ -158,7 +158,7 @@ export circuit revokeRole( ShieldedAccessControl_revokeRole(roleId, accountId); } -circuit _uncheckedRevokeRole( +export circuit _uncheckedRevokeRole( role: ShieldedAccessControl_RoleIdentifier, accountId: ShieldedAccessControl_AccountIdentifier ): Boolean { diff --git a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts index 3d5f393b..754f751e 100644 --- a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts +++ b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts @@ -74,82 +74,54 @@ export class ShieldedAccessControlSimulator extends ShieldedAccessControlSimulat return this.circuits.impure.proveCallerRole(role); } - /** - * @description Transfers ownership to `newOwnerId`. - * `newOwnerId` must be precalculated and given to the current owner off chain. - * @param newOwnerId The new owner's unique identifier (`SHA256(pk, nonce)`). - */ + public _uncheckedProveCallerRole(role: Uint8Array): boolean { + return this.circuits.impure._uncheckedProveCallerRole(role); + } + public assertOnlyRole(role: Uint8Array) { this.circuits.impure.assertOnlyRole(role); } + public _uncheckedAssertOnlyRole(role: Uint8Array) { + this.circuits.impure._uncheckedAssertOnlyRole(role); + } + public _validateRole(role: Uint8Array, accountId: Uint8Array): boolean { return this.circuits.impure._validateRole(role, accountId); } - /** - * @description Computes the RoleCheck commitment from the given `id` and `counter`. - * @param id - The unique identifier of the owner calculated by `SHA256(pk, nonce)`. - * @param counter - The current counter or round. This increments by `1` - * after every transfer to prevent duplicate commitments given the same `id`. - * @returns The commitment derived from `id` and `counter`. - */ public getRoleAdmin(role: Uint8Array): Uint8Array { return this.circuits.impure.getRoleAdmin(role); } - /** - * @description Computes the unique identifier (`id`) of the owner from their - * public key and a secret nonce. - * @param pk - The public key of the identity being committed. - * @param nonce - A private nonce to scope the commitment. - * @returns The computed owner ID. - */ public grantRole(role: Uint8Array, accountId: Uint8Array) { this.circuits.impure.grantRole(role, accountId); } - /** - * @description Transfers ownership to owner id `newOwnerId` without - * enforcing permission checks on the caller. - * @param newOwnerId - The unique identifier of the new owner calculated by `SHA256(pk, nonce)`. - */ + public _uncheckedGrantRole(role: Uint8Array, accountId: Uint8Array) { + this.circuits.impure._uncheckedGrantRole(role, accountId); + } + public revokeRole(role: Uint8Array, accountId: Uint8Array) { this.circuits.impure.revokeRole(role, accountId); } - /** - * @description Transfers ownership to owner id `newOwnerId` without - * enforcing permission checks on the caller. - * @param newOwnerId - The unique identifier of the new owner calculated by `SHA256(pk, nonce)`. - */ + public _uncheckedRevokeRole(role: Uint8Array, accountId: Uint8Array) { + this.circuits.impure._uncheckedRevokeRole(role, accountId); + } + public renounceRole(role: Uint8Array, callerConfirmation: Uint8Array) { this.circuits.impure.renounceRole(role, callerConfirmation); } - /** - * @description Transfers ownership to owner id `newOwnerId` without - * enforcing permission checks on the caller. - * @param newOwnerId - The unique identifier of the new owner calculated by `SHA256(pk, nonce)`. - */ public _setRoleAdmin(role: Uint8Array, adminRole: Uint8Array) { this.circuits.impure._setRoleAdmin(role, adminRole); } - /** - * @description Transfers ownership to owner id `newOwnerId` without - * enforcing permission checks on the caller. - * @param newOwnerId - The unique identifier of the new owner calculated by `SHA256(pk, nonce)`. - */ public _grantRole(role: Uint8Array, accountId: Uint8Array): boolean { return this.circuits.impure._grantRole(role, accountId); } - /** - * @description Transfers ownership to owner id `newOwnerId` without - * enforcing permission checks on the caller. - * @param newOwnerId - The unique identifier of the new owner calculated by `SHA256(pk, nonce)`. - */ public _revokeRole(role: Uint8Array, accountId: Uint8Array): boolean { return this.circuits.impure._revokeRole(role, accountId); } From d989f3448f5e6cce2260c68c7b5ddba5000ecefd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Fri, 13 Mar 2026 13:32:17 -0400 Subject: [PATCH 280/322] Remove admin identifier --- .../src/access/ShieldedAccessControl.compact | 95 +++++++++---------- .../mocks/MockShieldedAccessControl.compact | 16 ++-- 2 files changed, 55 insertions(+), 56 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index c37db3bd..c553606b 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -34,7 +34,7 @@ pragma language_version >= 0.21.0; * - `roleNullifier` is a one-time burn token inserted into `_roleCommitmentNullifiers` on * revocation. Its presence permanently invalidates the corresponding role commitment, * making re-grant under the same `accountId` impossible without generating a new identity. - * - `instanceSalt` is an immutable, cryptographically strong random value provided on deployment + * - `instanceSalt` should be an immutable, cryptographically strong random value provided on deployment * - `commitmentDomain` is the following zero padded 32 byte string: "ShieldedAccessControl:commitment" * - `accountIdDomain` is the following zero padded 32 byte string: "ShieldedAccessControl:accountId" * - `nullifierDomain` is the following zero padded 32 byte string: "ShieldedAccessControl:nullifier" @@ -63,7 +63,7 @@ pragma language_version >= 0.21.0; * * ```compact * circuit foo(): [] { - * ShieldedAccessControl_assertOnlyRole(MY_ROLE); + * ShieldedAccessControl_assertOnlyRole(MY_ROLE as ShieldedAccessControl_RoleIdentifier); * // ... rest of circuit logic * } * ``` @@ -123,7 +123,6 @@ module ShieldedAccessControl { export new type RoleCommitment = Bytes<32>; export new type RoleIdentifier = Bytes<32>; export new type AccountIdentifier = Bytes<32>; - export new type AdminIdentifier = Bytes<32>; export new type RoleNullifier = Bytes<32>; /** @@ -131,7 +130,7 @@ module ShieldedAccessControl { * @description A Merkle tree of role commitments stored as SHA256(role | accountId | instanceSalt | commitmentDomain) * Role commitments are derived from a public role identifier (e.g., `persistentHash>(pad(32, "MY_ROLE")`), * an account identifier (e.g., `SHA256(zcpk, nonce, instanceSalt, accountIdDomain)`), the `instanceSalt`, and a domain separator. - * @type {Bytes<32>} RoleCommitment - A role commitment created by the following hash: SHA256( role | accountId | instanceSalt | commitmentDomain). + * @type {RoleCommitment} roleCommitment - A role commitment created by the following hash: SHA256( role | accountId | instanceSalt | commitmentDomain).  */ export ledger _operatorRoles: MerkleTree<20, RoleCommitment>; @@ -139,12 +138,12 @@ module ShieldedAccessControl { * @ledger _adminRoles * @description Mapping from a role identifier to an admin role identifier.  */ - export ledger _adminRoles: Map; + export ledger _adminRoles: Map; /** * @description A set of nullifiers used to prove a role has been revoked - * @type {Bytes<32>} RoleNullifier - A role nullifier created by the following hash: SHA256(roleCommitment | nullifierDomain). - * @type {Set} _roleCommitmentNullifiers + * @type {RoleNullifier} roleNullifier - A role nullifier created by the following hash: SHA256(roleCommitment | nullifierDomain). + * @type {Set} _roleCommitmentNullifiers  */ export ledger _roleCommitmentNullifiers: Set; @@ -162,7 +161,8 @@ module ShieldedAccessControl { * commitments for this contract instance. * * This salt prevents commitment collisions across contracts that might otherwise use - * the same identifiers or domain parameters. It is immutable after initialization. + * the same identifiers or domain parameters. It should be should be a cryptographically strong random value + * It is immutable after initialization. */ export sealed ledger _instanceSalt: Bytes<32>; @@ -170,9 +170,9 @@ module ShieldedAccessControl { * @witness wit_getRoleCommitmentPath * @description Returns a path to a role commitment in the `_operatorRoles` Merkle tree if one exists. Otherwise, returns an invalid path. * - * @param {Bytes<32>} roleCommitment - A commitment created by the following hash: SHA256( role | accountId | instanceSalt | commitmentDomain). + * @param {RoleCommitment} roleCommitment - A commitment created by the following hash: SHA256( role | accountId | instanceSalt | commitmentDomain). * - * @return {MerkleTreePath<20, Bytes<32>>} - The Merkle tree path to `roleCommitment` in the `_operatorRoles` Merkle tree + * @return {MerkleTreePath<20, RoleCommitment>} - The Merkle tree path to `roleCommitment` in the `_operatorRoles` Merkle tree  */ witness wit_getRoleCommitmentPath( roleCommitment: RoleCommitment @@ -185,7 +185,7 @@ module ShieldedAccessControl { * Combined with the user's ZswapCoinPublicKey as `SHA256(zcpk, nonce, instanceSalt, accountIdDomain)` to produce an obfuscated, * unlinkable identity commitment. Nonce MUST be unique per role to avoid cross-role linking. * - * @param {Bytes<32>} role - The unique identifier of a role. + * @param {RoleIdentifier} role - The unique identifier of a role. * * @returns {Bytes<32>} secretNonce - A private per-role nonce used in deriving the shielded account identifier. */ @@ -232,7 +232,7 @@ module ShieldedAccessControl { * - A role commitment corresponding to a `(role, accountId)` pairing. * - A nullifier for the respective role commitment. * - * @param {Bytes<32>} role - The role identifier. + * @param {RoleIdentifier} role - The role identifier. * * @return {[]} - Empty tuple. */ @@ -258,7 +258,7 @@ module ShieldedAccessControl { * - A role commitment corresponding to a `(role, accountId)` pairing. * - A nullifier for the respective role commitment. * - * @param {Bytes<32>} role - The role identifier. + * @param {RoleIdentifier} role - The role identifier. * * @return {[]} - Empty tuple. */ @@ -283,7 +283,7 @@ module ShieldedAccessControl { * - A role commitment corresponding to a `(role, accountId)` pairing. * - A nullifier for the respective role commitment. * - * @param {Bytes<32>} role - The role identifier. + * @param {RoleIdentifier} role - The role identifier. * * @return {Boolean} - A boolean determining if a caller successfully proved ownership of `role`  */ @@ -315,7 +315,7 @@ module ShieldedAccessControl { * - A role commitment corresponding to a `(role, accountId)` pairing. * - A nullifier for the respective role commitment. * - * @param {Bytes<32>} role - The role identifier. + * @param {RoleIdentifier} role - The role identifier. * * @return {Boolean} - A boolean determining if a caller successfully proved ownership of `role`  */ @@ -345,15 +345,15 @@ module ShieldedAccessControl { * - A nullifier for the respective role commitment. * - A role identifier. * - * @param {Bytes<32>} role - The role identifier. - * @param {Bytes<32>} accountId - The unique identifier of the account. + * @param {RoleIdentifier} role - The role identifier. + * @param {AccountIdentifier} accountId - The unique identifier of the account. * * @return {[]} - Empty tuple. */ export circuit grantRole(role: RoleIdentifier, accountId: AccountIdentifier): [] { Initializable_assertInitialized(); - _uncheckedAssertOnlyRole(getRoleAdmin(role) as RoleIdentifier); + _uncheckedAssertOnlyRole(getRoleAdmin(role)); _uncheckedGrantRole(role, accountId); } @@ -389,8 +389,8 @@ module ShieldedAccessControl { * - A role commitment corresponding to a `(role, accountId)` pairing. * - A nullifier for the respective role commitment. * - * @param {Bytes<32>} role - The role identifier. - * @param {Bytes<32>} accountId - The account identifier. + * @param {RoleIdentifier} role - The role identifier. + * @param {AccountIdentifier} accountId - The unique identifier of the account. * * @return {Boolean} isGranted - Returns true if a role was granted successfully. */ @@ -433,8 +433,8 @@ module ShieldedAccessControl { * - A role commitment corresponding to a `(role, accountId)` pairing. * - A nullifier for the respective role commitment. * - * @param {Bytes<32>} role - The role identifier. - * @param {Bytes<32>} accountId - The account identifier. + * @param {RoleIdentifier} role - The role identifier. + * @param {AccountIdentifier} accountId - The unique identifier of the account. * * @return {Boolean} isGranted - Returns true if a role was granted successfully. */ @@ -472,8 +472,8 @@ module ShieldedAccessControl { * * - A nullifier for the respective role commitment. * - * @param {Bytes<32>} role - The role identifier. - * @param {Bytes<32>} accountIdConfirmation - The caller's account identifier, must match the internally computed value. + * @param {RoleIdentifier} role - The role identifier. + * @param {AccountIdentifier} accountIdConfirmation - The caller's account identifier, must match the internally computed value. * * @return {[]} - Empty tuple. */ @@ -509,15 +509,15 @@ module ShieldedAccessControl { * - A nullifier for the respective role commitment. * - A role identifier. * - * @param {Bytes<32>} role - The role identifier. - * @param {Bytes<32>} accountId - The unique identifier of the account. + * @param {RoleIdentifier} role - The role identifier. + * @param {AccountIdentifier} accountId - The unique identifier of the account. * * @return {[]} - Empty tuple. */ export circuit revokeRole(role: RoleIdentifier, accountId: AccountIdentifier): [] { Initializable_assertInitialized(); - _uncheckedAssertOnlyRole(getRoleAdmin(role) as RoleIdentifier); + _uncheckedAssertOnlyRole(getRoleAdmin(role)); _uncheckedRevokeRole(role, accountId); } @@ -534,8 +534,8 @@ module ShieldedAccessControl { * * - A nullifier for the respective role commitment. * - * @param {Bytes<32>} role - The role identifier. - * @param {Bytes<32>} accountId - The account identifier. + * @param {RoleIdentifier} role - The role identifier. + * @param {AccountIdentifier} accountId - The unique identifier of the account. * * @return {Boolean} isRevoked - Returns true if operation completes successfully. */ @@ -571,8 +571,8 @@ module ShieldedAccessControl { * * - A nullifier for the respective role commitment. * - * @param {Bytes<32>} role - The role identifier. - * @param {Bytes<32>} accountId - The account identifier. + * @param {RoleIdentifier} role - The role identifier. + * @param {AccountIdentifier} accountId - The unique identifier of the account. * * @return {Boolean} isRevoked - Returns true if operation completes successfully. */ @@ -601,15 +601,15 @@ module ShieldedAccessControl { * * - A role identifier. * - * @param {Bytes<32>} role - The role identifier. + * @param {RoleIdentifier} role - The role identifier. * - * @return {Bytes<32>} roleAdmin - The admin role that controls `role`. + * @return {RoleIdentifier} roleAdmin - The admin role that controls `role`. */ - export circuit getRoleAdmin(role: RoleIdentifier): AdminIdentifier { + export circuit getRoleAdmin(role: RoleIdentifier): RoleIdentifier { if (_adminRoles.member(disclose(role))) { return _adminRoles.lookup(disclose(role)); } - return default> as AdminIdentifier; + return default> as RoleIdentifier; } /** @@ -622,12 +622,12 @@ module ShieldedAccessControl { * - The role identifier * - The admin identifier * - * @param {Bytes<32>} role - The role identifier. - * @param {Bytes<32>} adminId - The admin role identifier. + * @param {RoleIdentifier} role - The role identifier. + * @param {AccountIdentifier} accountId - The unique identifier of the account. * * @return {[]} - Empty tuple. */ - export circuit _setRoleAdmin(role: RoleIdentifier, adminId: AdminIdentifier): [] { + export circuit _setRoleAdmin(role: RoleIdentifier, adminId: RoleIdentifier): [] { _adminRoles.insert(disclose(role), disclose(adminId)); } @@ -651,8 +651,8 @@ module ShieldedAccessControl { * - A role commitment corresponding to a `(role, accountId)` pairing. * - A nullifier for the respective role commitment. * - * @param {Bytes<32>} role - The role identifier. - * @param {Bytes<32>} accountId - The unique identifier of the account. + * @param {RoleIdentifier} role - The role identifier. + * @param {AccountIdentifier} accountId - The unique identifier of the account. * * @return {Boolean} isValidRole - A boolean indicating whether `accountId` has a valid role */ @@ -707,10 +707,10 @@ module ShieldedAccessControl { * * - Contract is initialized. * - * @param {Bytes<32>} role - The unique identifier of a role. - * @param {Bytes<32>} accountId - The unique identifier of the account calculated by `SHA256(zcpk, nonce, instanceSalt, accountIdDomain)`. + * @param {RoleIdentifier} role - The role identifier. + * @param {AccountIdentifier} accountId - The unique identifier of the account. * - * @returns {Bytes<32>} The commitment derived from `accountId` and `role`. + * @returns {RoleCommitment} The commitment derived from `accountId` and `role`. */ circuit _computeRoleCommitment( role: RoleIdentifier, @@ -740,9 +740,9 @@ module ShieldedAccessControl { * - `nullifierDomain`: Domain separator `"ShieldedAccessControl:nullifier"` (padded to 32 bytes) to prevent * hash collisions when extending the module or using similar commitment schemes. * - * @param {} roleCommitment - The role commitment for a particular `(role, accountId)` pairing. + * @param {RoleCommitment} roleCommitment - The role commitment for a particular `(role, accountId)` pairing. * - * @returns {Bytes<32>} roleNullifier - The associated nullifier for `roleCommitment`. + * @returns {RoleNullifier} roleNullifier - The associated nullifier for `roleCommitment`. */ pure circuit _computeNullifier(roleCommitment: RoleCommitment): RoleNullifier { return persistentHash>>( @@ -776,10 +776,9 @@ module ShieldedAccessControl { * * @circuitInfo k=13, rows=6705 * - * @param {ZswapCoinPublicKey} zcpk - The public key of the identity being committed. - * @param {Bytes<32>} nonce - A private nonce to scope the commitment. + * @param {RoleIdentifier} role - A private nonce to scope the commitment. * - * @returns {Bytes<32>} accountId - The computed account ID. + * @returns {AccountIdentifier} accountId - The computed account ID. */ circuit _computeAccountId(role: RoleIdentifier): AccountIdentifier { return persistentHash>>( diff --git a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact index ceccd434..e03a6b2b 100644 --- a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact +++ b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact @@ -124,7 +124,7 @@ export circuit _validateRole( export circuit getRoleAdmin( roleId: ShieldedAccessControl_RoleIdentifier - ): ShieldedAccessControl_AdminIdentifier { + ): ShieldedAccessControl_RoleIdentifier { return ShieldedAccessControl_getRoleAdmin(roleId); } @@ -136,9 +136,9 @@ export circuit grantRole( } export circuit _uncheckedGrantRole( - role: ShieldedAccessControl_RoleIdentifier, - accountId: ShieldedAccessControl_AccountIdentifier - ): Boolean { + role: ShieldedAccessControl_RoleIdentifier, + accountId: ShieldedAccessControl_AccountIdentifier + ): Boolean { const roleCommitment = _computeRoleCommitment(role, accountId); const roleNullifier = _computeNullifier(roleCommitment); const isRevoked = ShieldedAccessControl__roleCommitmentNullifiers.member(disclose(roleNullifier)); @@ -159,9 +159,9 @@ export circuit revokeRole( } export circuit _uncheckedRevokeRole( - role: ShieldedAccessControl_RoleIdentifier, - accountId: ShieldedAccessControl_AccountIdentifier - ): Boolean { + role: ShieldedAccessControl_RoleIdentifier, + accountId: ShieldedAccessControl_AccountIdentifier + ): Boolean { const roleCommitment = _computeRoleCommitment(role, accountId); const roleNullifier = _computeNullifier(roleCommitment); const isRevoked = ShieldedAccessControl__roleCommitmentNullifiers.member(disclose(roleNullifier)); @@ -183,7 +183,7 @@ export circuit renounceRole( export circuit _setRoleAdmin( roleId: ShieldedAccessControl_RoleIdentifier, - adminRole: ShieldedAccessControl_AdminIdentifier + adminRole: ShieldedAccessControl_RoleIdentifier ): [] { ShieldedAccessControl__setRoleAdmin(roleId, adminRole); } From ac4853d3e6fe575431c108bc5aee65aaf188391a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Fri, 13 Mar 2026 19:07:19 -0400 Subject: [PATCH 281/322] Update docs --- contracts/src/access/ShieldedAccessControl.compact | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index c553606b..07f8adf5 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -151,6 +151,9 @@ module ShieldedAccessControl { * @description The default admin role for all roles. Only accounts with this role will be able to grant or revoke other roles * unless custom admin roles are created. * + * @remarks Treat as a constant — this value should never be written to. The Compact language does not support constant declarations, + * so this is implemented as a ledger variable by necessity. + * * @default 0x0000000000000000000000000000000000000000000000000000000000000000  */ export ledger DEFAULT_ADMIN_ROLE: Bytes<32>; From c7d06c0feb933b9b06048947fe0249c0cb96f627 Mon Sep 17 00:00:00 2001 From: andrew Date: Sat, 14 Mar 2026 10:58:15 -0300 Subject: [PATCH 282/322] remove ledger artifact from wit, update sim --- .../witnesses/SampleZOwnableWitnesses.ts | 23 ++++++++++--------- .../witnesses/WitnessWitnesses.ts | 20 ++++++++-------- .../integration/SampleZOwnableSimulator.ts | 9 ++++---- .../test/integration/WitnessSimulator.ts | 9 ++++---- 4 files changed, 33 insertions(+), 28 deletions(-) diff --git a/packages/simulator/test/fixtures/sample-contracts/witnesses/SampleZOwnableWitnesses.ts b/packages/simulator/test/fixtures/sample-contracts/witnesses/SampleZOwnableWitnesses.ts index 464007f1..ba764d5e 100644 --- a/packages/simulator/test/fixtures/sample-contracts/witnesses/SampleZOwnableWitnesses.ts +++ b/packages/simulator/test/fixtures/sample-contracts/witnesses/SampleZOwnableWitnesses.ts @@ -1,18 +1,17 @@ import { getRandomValues } from 'node:crypto'; import type { WitnessContext } from '@midnight-ntwrk/compact-runtime'; -import type { Ledger } from '../../artifacts/SampleZOwnable/contract/index.js'; /** * @description Interface defining the witness methods for SampleZOwnable operations. * @template P - The private state type. */ -export interface ISampleZOwnableWitnesses

{ +export interface ISampleZOwnableWitnesses { /** * Retrieves the secret nonce from the private state. * @param context - The witness context containing the private state. * @returns A tuple of the private state and the secret nonce as a Uint8Array. */ - secretNonce(context: WitnessContext): [P, Uint8Array]; + secretNonce(context: WitnessContext): [P, Uint8Array]; } /** @@ -58,11 +57,13 @@ export const SampleZOwnablePrivateState = { * @description Factory function creating witness implementations for Ownable operations. * @returns An object implementing the Witnesses interface for SampleZOwnablePrivateState. */ -export const SampleZOwnableWitnesses = - (): ISampleZOwnableWitnesses => ({ - secretNonce( - context: WitnessContext, - ): [SampleZOwnablePrivateState, Uint8Array] { - return [context.privateState, context.privateState.secretNonce]; - }, - }); +export const SampleZOwnableWitnesses = (): ISampleZOwnableWitnesses< + L, + SampleZOwnablePrivateState +> => ({ + secretNonce( + context: WitnessContext, + ): [SampleZOwnablePrivateState, Uint8Array] { + return [context.privateState, context.privateState.secretNonce]; + }, +}); diff --git a/packages/simulator/test/fixtures/sample-contracts/witnesses/WitnessWitnesses.ts b/packages/simulator/test/fixtures/sample-contracts/witnesses/WitnessWitnesses.ts index 7795cdfc..47c8009f 100644 --- a/packages/simulator/test/fixtures/sample-contracts/witnesses/WitnessWitnesses.ts +++ b/packages/simulator/test/fixtures/sample-contracts/witnesses/WitnessWitnesses.ts @@ -1,6 +1,5 @@ import { getRandomValues } from 'node:crypto'; import type { WitnessContext } from '@midnight-ntwrk/compact-runtime'; -import type { Ledger } from '../../artifacts/Witness/contract/index.js'; const randomBigInt = (bits: number): bigint => { const bytes = Math.ceil(bits / 8); @@ -16,14 +15,14 @@ const randomBigInt = (bits: number): bigint => { return result % max; }; -export interface IWitnessWitnesses

{ - wit_secretBytes(context: WitnessContext): [P, Uint8Array]; +export interface IWitnessWitnesses { + wit_secretBytes(context: WitnessContext): [P, Uint8Array]; wit_secretFieldPlusArg( - context: WitnessContext, + context: WitnessContext, arg: bigint, ): [P, bigint]; wit_secretUintPlusArgs( - context: WitnessContext, + context: WitnessContext, arg1: bigint, arg2: bigint, ): [P, bigint]; @@ -45,20 +44,23 @@ export const WitnessPrivateState = { }, }; -export const WitnessWitnesses = (): IWitnessWitnesses => ({ +export const WitnessWitnesses = (): IWitnessWitnesses< + L, + WitnessPrivateState +> => ({ wit_secretBytes( - context: WitnessContext, + context: WitnessContext, ): [WitnessPrivateState, Uint8Array] { return [context.privateState, context.privateState.secretBytes]; }, wit_secretFieldPlusArg( - context: WitnessContext, + context: WitnessContext, arg: bigint, ): [WitnessPrivateState, bigint] { return [context.privateState, context.privateState.secretField + arg]; }, wit_secretUintPlusArgs( - context: WitnessContext, + context: WitnessContext, arg1: bigint, arg2: bigint, ): [WitnessPrivateState, bigint] { diff --git a/packages/simulator/test/integration/SampleZOwnableSimulator.ts b/packages/simulator/test/integration/SampleZOwnableSimulator.ts index c288e259..e2e26f2f 100644 --- a/packages/simulator/test/integration/SampleZOwnableSimulator.ts +++ b/packages/simulator/test/integration/SampleZOwnableSimulator.ts @@ -11,14 +11,15 @@ import { SampleZOwnableWitnesses, } from '../fixtures/sample-contracts/witnesses/SampleZOwnableWitnesses'; -/** - * Type constructor args - */ +/** Type constructor args */ type SampleZOwnableArgs = readonly [ owner: Uint8Array, instanceSalt: Uint8Array, ]; +/** Concrete ledger type extracted from the generated artifact */ +type SampleZOwnableLedger = ReturnType; + /** * Base simulator */ @@ -36,7 +37,7 @@ const SampleZOwnableSimulatorBase = createSimulator< return [owner, instanceSalt]; }, ledgerExtractor: (state) => ledger(state), - witnessesFactory: () => SampleZOwnableWitnesses(), + witnessesFactory: () => SampleZOwnableWitnesses(), }); /** diff --git a/packages/simulator/test/integration/WitnessSimulator.ts b/packages/simulator/test/integration/WitnessSimulator.ts index c5b07041..e03ac46f 100644 --- a/packages/simulator/test/integration/WitnessSimulator.ts +++ b/packages/simulator/test/integration/WitnessSimulator.ts @@ -8,11 +8,12 @@ import { WitnessWitnesses, } from '../fixtures/sample-contracts/witnesses/WitnessWitnesses'; -/** - * Type constructor args - */ +/** Type constructor args */ type WitnessArgs = readonly []; +/** Concrete ledger type extracted from the generated artifact */ +type WitnessLedger = ReturnType; + /** * Base simulator */ @@ -30,7 +31,7 @@ const WitnessSimulatorBase = createSimulator< return []; }, ledgerExtractor: (state) => ledger(state), - witnessesFactory: () => WitnessWitnesses(), + witnessesFactory: () => WitnessWitnesses(), }); /** From 7e27f80fa20926a09de73819e9717a17b57ee2aa Mon Sep 17 00:00:00 2001 From: andrew Date: Sat, 14 Mar 2026 11:05:13 -0300 Subject: [PATCH 283/322] remove ledger artifact from wit, update sim in zownable --- .../test/simulators/ZOwnablePKSimulator.ts | 9 ++++---- .../access/witnesses/ZOwnablePKWitnesses.ts | 23 ++++++++++--------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/contracts/src/access/test/simulators/ZOwnablePKSimulator.ts b/contracts/src/access/test/simulators/ZOwnablePKSimulator.ts index 5dd24cf9..b1227ee1 100644 --- a/contracts/src/access/test/simulators/ZOwnablePKSimulator.ts +++ b/contracts/src/access/test/simulators/ZOwnablePKSimulator.ts @@ -14,15 +14,16 @@ import { ZOwnablePKWitnesses, } from '../../witnesses/ZOwnablePKWitnesses.js'; -/** - * Type constructor args - */ +/** Type constructor args */ type ZOwnablePKArgs = readonly [ owner: Uint8Array, instanceSalt: Uint8Array, isInit: boolean, ]; +/** Concrete ledger type extracted from the generated artifact */ +type ZOwnablePKLedger = ReturnType; + /** * Base simulator * @dev We deliberately use `any` as the base simulator type. @@ -47,7 +48,7 @@ const ZOwnablePKSimulatorBase: any = createSimulator< return [owner, instanceSalt, isInit]; }, ledgerExtractor: (state) => ledger(state), - witnessesFactory: () => ZOwnablePKWitnesses(), + witnessesFactory: () => ZOwnablePKWitnesses(), }); /** diff --git a/contracts/src/access/witnesses/ZOwnablePKWitnesses.ts b/contracts/src/access/witnesses/ZOwnablePKWitnesses.ts index 6453011d..ba273af8 100644 --- a/contracts/src/access/witnesses/ZOwnablePKWitnesses.ts +++ b/contracts/src/access/witnesses/ZOwnablePKWitnesses.ts @@ -1,18 +1,17 @@ import { getRandomValues } from 'node:crypto'; import type { WitnessContext } from '@midnight-ntwrk/compact-runtime'; -import type { Ledger } from '../../../artifacts/MockZOwnablePK/contract/index.js'; /** * @description Interface defining the witness methods for ZOwnablePK operations. * @template P - The private state type. */ -export interface IZOwnablePKWitnesses

{ +export interface IZOwnablePKWitnesses { /** * Retrieves the secret nonce from the private state. * @param context - The witness context containing the private state. * @returns A tuple of the private state and the secret nonce as a Uint8Array. */ - wit_secretNonce(context: WitnessContext): [P, Uint8Array]; + wit_secretNonce(context: WitnessContext): [P, Uint8Array]; } /** @@ -58,11 +57,13 @@ export const ZOwnablePKPrivateState = { * @description Factory function creating witness implementations for Ownable operations. * @returns An object implementing the Witnesses interface for ZOwnablePKPrivateState. */ -export const ZOwnablePKWitnesses = - (): IZOwnablePKWitnesses => ({ - wit_secretNonce( - context: WitnessContext, - ): [ZOwnablePKPrivateState, Uint8Array] { - return [context.privateState, context.privateState.secretNonce]; - }, - }); +export const ZOwnablePKWitnesses = (): IZOwnablePKWitnesses< + L, + ZOwnablePKPrivateState +> => ({ + wit_secretNonce( + context: WitnessContext, + ): [ZOwnablePKPrivateState, Uint8Array] { + return [context.privateState, context.privateState.secretNonce]; + }, +}); From 7ea82e9c25328e15a0a3627a40bffd605d2e00d1 Mon Sep 17 00:00:00 2001 From: andrew Date: Sat, 14 Mar 2026 11:15:12 -0300 Subject: [PATCH 284/322] update sim readme --- packages/simulator/README.md | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/simulator/README.md b/packages/simulator/README.md index fcfd3ec0..48b2b496 100644 --- a/packages/simulator/README.md +++ b/packages/simulator/README.md @@ -20,7 +20,10 @@ import { Contract, ledger } from './artifacts/MyContract/contract/index.js'; // 1. Define your contract arguments type type MyContractArgs = readonly [owner: Uint8Array, value: bigint]; -// 2. Create the simulator +// 2. Define the extracted ledger type +type MyContractLedger = ReturnType; + +// 3. Create the simulator const MySimulator = createSimulator< MyPrivateState, ReturnType, @@ -31,10 +34,10 @@ const MySimulator = createSimulator< defaultPrivateState: () => MyPrivateState.generate(), contractArgs: (owner, value) => [owner, value], ledgerExtractor: (state) => ledger(state), - witnessesFactory: () => MyWitnesses(), + witnessesFactory: () => MyWitnesses(), }); -// 3. Use it! +// 4. Use it! const sim = new MySimulator([ownerAddress, 100n], { coinPK: deployerPK }); ``` @@ -52,6 +55,9 @@ import { MyContractWitnesses, MyContractPrivateState } from './MyContractWitness // Define contract constructor arguments as a tuple type type MyContractArgs = readonly [arg1: bigint, arg2: string]; +// Define the extracted ledger type +type MyContractLedger = ReturnType; + // Create the base simulator with full type information const MyContractSimulatorBase = createSimulator< MyContractPrivateState, // Private state type @@ -63,7 +69,7 @@ const MyContractSimulatorBase = createSimulator< defaultPrivateState: () => MyContractPrivateState.generate(), contractArgs: (arg1, arg2) => [arg1, arg2], ledgerExtractor: (state) => ledger(state), - witnessesFactory: () => MyContractWitnesses(), // Note: Must be a function! + witnessesFactory: () => MyContractWitnesses(), // Note: Must be a function! }); ``` @@ -76,7 +82,7 @@ If the Compact contract has no witnesses: // Some Compact contract examples use: export const MyContractWitnesses = {}; -// But for the simulator, wrap it in a function: +// But for the simulator, wrap it in a generic function: export const MyContractWitnesses = () => ({}); ``` From c74f42f0f35555a467b8971e210e2f9c1c85dbad Mon Sep 17 00:00:00 2001 From: andrew Date: Sat, 14 Mar 2026 11:19:41 -0300 Subject: [PATCH 285/322] add changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc74a2d3..d4f290c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changes +- Use generic ledger type in ZOwnablePKWitnesses (#) - Bump compact compiler to v0.29.0 (#366) ## 0.0.1-alpha.1 (2025-12-2) From f802a548a7c6f04f4b7e5dd22b6153424ff208f9 Mon Sep 17 00:00:00 2001 From: andrew Date: Sat, 14 Mar 2026 11:31:38 -0300 Subject: [PATCH 286/322] improve witness docs --- contracts/src/access/witnesses/ZOwnablePKWitnesses.ts | 4 +++- .../sample-contracts/witnesses/SampleZOwnableWitnesses.ts | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/contracts/src/access/witnesses/ZOwnablePKWitnesses.ts b/contracts/src/access/witnesses/ZOwnablePKWitnesses.ts index ba273af8..61269a2a 100644 --- a/contracts/src/access/witnesses/ZOwnablePKWitnesses.ts +++ b/contracts/src/access/witnesses/ZOwnablePKWitnesses.ts @@ -3,12 +3,13 @@ import type { WitnessContext } from '@midnight-ntwrk/compact-runtime'; /** * @description Interface defining the witness methods for ZOwnablePK operations. + * @template L - The ledger type. * @template P - The private state type. */ export interface IZOwnablePKWitnesses { /** * Retrieves the secret nonce from the private state. - * @param context - The witness context containing the private state. + * @param context - The witness context containing the ledger and private state. * @returns A tuple of the private state and the secret nonce as a Uint8Array. */ wit_secretNonce(context: WitnessContext): [P, Uint8Array]; @@ -55,6 +56,7 @@ export const ZOwnablePKPrivateState = { /** * @description Factory function creating witness implementations for Ownable operations. + * @template L - The ledger type, supplied by the simulator. * @returns An object implementing the Witnesses interface for ZOwnablePKPrivateState. */ export const ZOwnablePKWitnesses = (): IZOwnablePKWitnesses< diff --git a/packages/simulator/test/fixtures/sample-contracts/witnesses/SampleZOwnableWitnesses.ts b/packages/simulator/test/fixtures/sample-contracts/witnesses/SampleZOwnableWitnesses.ts index ba764d5e..af447156 100644 --- a/packages/simulator/test/fixtures/sample-contracts/witnesses/SampleZOwnableWitnesses.ts +++ b/packages/simulator/test/fixtures/sample-contracts/witnesses/SampleZOwnableWitnesses.ts @@ -3,12 +3,13 @@ import type { WitnessContext } from '@midnight-ntwrk/compact-runtime'; /** * @description Interface defining the witness methods for SampleZOwnable operations. + * @template L - The ledger type. * @template P - The private state type. */ export interface ISampleZOwnableWitnesses { /** * Retrieves the secret nonce from the private state. - * @param context - The witness context containing the private state. + * @param context - The witness context containing the ledger and private state. * @returns A tuple of the private state and the secret nonce as a Uint8Array. */ secretNonce(context: WitnessContext): [P, Uint8Array]; @@ -55,6 +56,7 @@ export const SampleZOwnablePrivateState = { /** * @description Factory function creating witness implementations for Ownable operations. + * @template L - The ledger type, supplied by the simulator. * @returns An object implementing the Witnesses interface for SampleZOwnablePrivateState. */ export const SampleZOwnableWitnesses = (): ISampleZOwnableWitnesses< From bcb4ce19da5eb1a85178771fa415493eba68e786 Mon Sep 17 00:00:00 2001 From: andrew Date: Sat, 14 Mar 2026 11:33:32 -0300 Subject: [PATCH 287/322] update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4f290c9..c32538bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changes -- Use generic ledger type in ZOwnablePKWitnesses (#) +- Use generic ledger type in ZOwnablePKWitnesses (#389) - Bump compact compiler to v0.29.0 (#366) ## 0.0.1-alpha.1 (2025-12-2) From 72ed9a5395983f12acfb20737b258db4717e111b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Mon, 16 Mar 2026 14:54:18 -0400 Subject: [PATCH 288/322] refactor: test should compare circuit result not callback --- contracts/src/access/test/ShieldedAccessControl.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/src/access/test/ShieldedAccessControl.test.ts b/contracts/src/access/test/ShieldedAccessControl.test.ts index a83fe7eb..cf0dc8af 100644 --- a/contracts/src/access/test/ShieldedAccessControl.test.ts +++ b/contracts/src/access/test/ShieldedAccessControl.test.ts @@ -217,13 +217,13 @@ describe('ShieldedAccessControl', () => { checkedCircuits, )('should not compute commitment with isValidRoleId=%s, isValidAccountId=%s', (_isValidRoleId, _isValidAccountId, args) => { // Test protected circuit - expect(() => { + expect( ( shieldedAccessControl._computeRoleCommitment as ( ...args: unknown[] ) => Uint8Array - )(...args); - }).not.toEqual(ADMIN.roleCommitment); + )(...args) + ).not.toEqual(ADMIN.roleCommitment); }); }); From bbbdba9095dd85e5705c29cc53734d4fd69883de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Mon, 16 Mar 2026 14:54:52 -0400 Subject: [PATCH 289/322] refactor: test should compare root value not object --- .../access/test/ShieldedAccessControl.test.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/contracts/src/access/test/ShieldedAccessControl.test.ts b/contracts/src/access/test/ShieldedAccessControl.test.ts index cf0dc8af..50aa52b5 100644 --- a/contracts/src/access/test/ShieldedAccessControl.test.ts +++ b/contracts/src/access/test/ShieldedAccessControl.test.ts @@ -1512,7 +1512,7 @@ describe('ShieldedAccessControl', () => { merkleRoot = shieldedAccessControl .getPublicState() .ShieldedAccessControl__operatorRoles.root(); - expect(merkleRoot).not.toBe(0n); + expect(merkleRoot.field).not.toBe(0n); // check path exists for new role const merkleTreePath = shieldedAccessControl @@ -1554,7 +1554,7 @@ describe('ShieldedAccessControl', () => { merkleRoot = shieldedAccessControl .getPublicState() .ShieldedAccessControl__operatorRoles.root(); - expect(merkleRoot).not.toBe(0n); + expect(merkleRoot.field).not.toBe(0n); // check path exists for new role const merkleTreePath = shieldedAccessControl @@ -1581,7 +1581,7 @@ describe('ShieldedAccessControl', () => { merkleRoot = shieldedAccessControl .getPublicState() .ShieldedAccessControl__operatorRoles.root(); - expect(merkleRoot).not.toBe(0n); + expect(merkleRoot.field).not.toBe(0n); // check path exists for new role const merkleTreePath = shieldedAccessControl @@ -1607,7 +1607,7 @@ describe('ShieldedAccessControl', () => { merkleRoot = shieldedAccessControl .getPublicState() .ShieldedAccessControl__operatorRoles.root(); - expect(merkleRoot).not.toBe(0n); + expect(merkleRoot.field).not.toBe(0n); // check path exists for new role const adminRoleBadAccountCommitment = buildRoleCommitmentHash( @@ -1718,7 +1718,7 @@ describe('ShieldedAccessControl', () => { merkleRoot = shieldedAccessControl .getPublicState() .ShieldedAccessControl__operatorRoles.root(); - expect(merkleRoot).not.toBe(0n); + expect(merkleRoot.field).not.toBe(0n); // check path exists for new role const merkleTreePath = shieldedAccessControl @@ -1760,7 +1760,7 @@ describe('ShieldedAccessControl', () => { merkleRoot = shieldedAccessControl .getPublicState() .ShieldedAccessControl__operatorRoles.root(); - expect(merkleRoot).not.toBe(0n); + expect(merkleRoot.field).not.toBe(0n); // check path exists for new role const merkleTreePath = shieldedAccessControl @@ -1787,7 +1787,7 @@ describe('ShieldedAccessControl', () => { merkleRoot = shieldedAccessControl .getPublicState() .ShieldedAccessControl__operatorRoles.root(); - expect(merkleRoot).not.toBe(0n); + expect(merkleRoot.field).not.toBe(0n); // check path exists for new role const merkleTreePath = shieldedAccessControl @@ -1813,7 +1813,7 @@ describe('ShieldedAccessControl', () => { merkleRoot = shieldedAccessControl .getPublicState() .ShieldedAccessControl__operatorRoles.root(); - expect(merkleRoot).not.toBe(0n); + expect(merkleRoot.field).not.toBe(0n); // check path exists for new role const adminRoleBadAccountCommitment = buildRoleCommitmentHash( From 9ed5fecd3fef31aca32cf5504c13627cb4301958 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Mon, 16 Mar 2026 14:55:23 -0400 Subject: [PATCH 290/322] refactor: DEFAULT_ADMIN_ROLE into pure circuit --- .../src/access/ShieldedAccessControl.compact | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index 07f8adf5..d9aac0d5 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -147,17 +147,6 @@ module ShieldedAccessControl {  */ export ledger _roleCommitmentNullifiers: Set; - /** - * @description The default admin role for all roles. Only accounts with this role will be able to grant or revoke other roles - * unless custom admin roles are created. - * - * @remarks Treat as a constant — this value should never be written to. The Compact language does not support constant declarations, - * so this is implemented as a ledger variable by necessity. - * - * @default 0x0000000000000000000000000000000000000000000000000000000000000000 -  */ - export ledger DEFAULT_ADMIN_ROLE: Bytes<32>; - /** * @sealed @ledger _instanceSalt * @description A per-instance value provided at initialization used to namespace @@ -219,6 +208,17 @@ module ShieldedAccessControl { _instanceSalt = disclose(instanceSalt); } + /** + * @description The default admin role for all roles. Only accounts with this role will be able to grant or revoke other roles + * unless custom admin roles are created. + * + * @remarks The Compact language does not support constant declarations, + * so DEFAULT_ADMIN_ROLE is implemented as a circuit that returns a constant value by necessity. +  */ + export pure circuit DEFAULT_ADMIN_ROLE(): Bytes<32> { + return default>; + } + /** * @description Reverts if caller cannot provide a valid proof of ownership for `role`. * From 85c71d9a23b0a522d1487e1bd577a52f018b9fc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Mon, 16 Mar 2026 15:07:56 -0400 Subject: [PATCH 291/322] Update mock, sim, and tests with DEFAULT_ADMIN_ROLE circuit --- .../src/access/test/ShieldedAccessControl.test.ts | 12 +++++++++++- .../test/mocks/MockShieldedAccessControl.compact | 4 ++++ .../simulators/ShieldedAccessControlSimulator.ts | 4 ++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/contracts/src/access/test/ShieldedAccessControl.test.ts b/contracts/src/access/test/ShieldedAccessControl.test.ts index 50aa52b5..739e56cd 100644 --- a/contracts/src/access/test/ShieldedAccessControl.test.ts +++ b/contracts/src/access/test/ShieldedAccessControl.test.ts @@ -154,6 +154,7 @@ describe('ShieldedAccessControl', () => { ['_computeAccountId', [UNINITIALIZED.role]], ['_computeRoleCommitment', [UNINITIALIZED.role, UNINITIALIZED.accountId]], ['_computeNullifier', [UNINITIALIZED.roleCommitment]], + ['DEFAULT_ADMIN_ROLE', []], ['_validateRole', [UNINITIALIZED.roleCommitment]], ]; it.each(circuitsToSucceed)('%s should succeed', (circuitName, args) => { @@ -191,6 +192,12 @@ describe('ShieldedAccessControl', () => { ); }); + describe('DEFAULT_ADMIN_ROLE', () => { + it('should return 0', () => { + expect(shieldedAccessControl.DEFAULT_ADMIN_ROLE()).toStrictEqual(new Uint8Array(32)); + }); + }); + describe('_computeRoleCommitment', () => { it('should match computed commitment', () => { expect( @@ -3448,9 +3455,12 @@ describe('ShieldedAccessControl', () => { describe('getRoleAdmin', () => { it('should return zero bytes (DEFAULT_ADMIN_ROLE) for a role with no admin set', () => { - expect(shieldedAccessControl.getRoleAdmin(OPERATOR_1.role)).toEqual( + expect(shieldedAccessControl.getRoleAdmin(OPERATOR_1.role)).toStrictEqual( new Uint8Array(32), ); + expect(shieldedAccessControl.getRoleAdmin(OPERATOR_1.role)).toStrictEqual( + shieldedAccessControl.DEFAULT_ADMIN_ROLE(), + ); }); it('should return the admin role after _setRoleAdmin', () => { diff --git a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact index e03a6b2b..1bc73392 100644 --- a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact +++ b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact @@ -53,6 +53,10 @@ export circuit _computeRoleCommitment( as ShieldedAccessControl_RoleCommitment; } +export pure circuit DEFAULT_ADMIN_ROLE(): Bytes<32> { + return ShieldedAccessControl_DEFAULT_ADMIN_ROLE(); +} + // circuit is reimplemented in the Mock contract for testing export circuit _computeAccountId( role: ShieldedAccessControl_RoleIdentifier diff --git a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts index 754f751e..b28f6c16 100644 --- a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts +++ b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts @@ -70,6 +70,10 @@ export class ShieldedAccessControlSimulator extends ShieldedAccessControlSimulat return this.circuits.pure._computeNullifier(roleCommitment); } + public DEFAULT_ADMIN_ROLE(): Uint8Array { + return this.circuits.pure.DEFAULT_ADMIN_ROLE(); + } + public proveCallerRole(role: Uint8Array): boolean { return this.circuits.impure.proveCallerRole(role); } From bc2d635d55b7fe62f99fe9c73559a0d63c1c0759 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Mon, 16 Mar 2026 15:15:47 -0400 Subject: [PATCH 292/322] Update type --- contracts/src/access/ShieldedAccessControl.compact | 9 ++++----- .../access/test/mocks/MockShieldedAccessControl.compact | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index d9aac0d5..f30e8fb1 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -75,8 +75,7 @@ pragma language_version >= 0.21.0; * By default, the admin role for all roles is `DEFAULT_ADMIN_ROLE`, which means * that only accounts with this role will be able to grant or revoke other * roles. More complex role relationships can be created by using - * {_setRoleAdmin}. To set a custom `DEFAULT_ADMIN_ROLE` set `DEFAULT_ADMIN_ROLE` - * in the `initialize()` circuit. + * {_setRoleAdmin}. * * WARNING: The `DEFAULT_ADMIN_ROLE` is also its own admin: it has permission to * grant and revoke this role. Extra precautions should be taken to secure @@ -215,8 +214,8 @@ module ShieldedAccessControl { * @remarks The Compact language does not support constant declarations, * so DEFAULT_ADMIN_ROLE is implemented as a circuit that returns a constant value by necessity.  */ - export pure circuit DEFAULT_ADMIN_ROLE(): Bytes<32> { - return default>; + export pure circuit DEFAULT_ADMIN_ROLE(): RoleIdentifier { + return default> as RoleIdentifier; } /** @@ -612,7 +611,7 @@ module ShieldedAccessControl { if (_adminRoles.member(disclose(role))) { return _adminRoles.lookup(disclose(role)); } - return default> as RoleIdentifier; + return DEFAULT_ADMIN_ROLE(); } /** diff --git a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact index 1bc73392..9509114a 100644 --- a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact +++ b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact @@ -53,7 +53,7 @@ export circuit _computeRoleCommitment( as ShieldedAccessControl_RoleCommitment; } -export pure circuit DEFAULT_ADMIN_ROLE(): Bytes<32> { +export pure circuit DEFAULT_ADMIN_ROLE(): ShieldedAccessControl_RoleIdentifier { return ShieldedAccessControl_DEFAULT_ADMIN_ROLE(); } From dbdff4cc1450152c6dbdda69ad212f1c39a0a1db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Mon, 16 Mar 2026 19:46:34 -0400 Subject: [PATCH 293/322] Update contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Andrew Fleming Signed-off-by: ⟣ €₥ℵ∪ℓ ⟢ <34749913+emnul@users.noreply.github.com> --- .../access/witnesses/ShieldedAccessControlWitnesses.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts b/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts index e6dfcdaa..7a97740c 100644 --- a/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts +++ b/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts @@ -80,8 +80,14 @@ export const ShieldedAccessControlPrivateState = { nonce: Buffer, ): ShieldedAccessControlPrivateState => { const roleString = role.toString('hex'); - privateState.roles[roleString] = nonce; - return privateState; + const roles: Record = {}; + + for (const [k, v] of Object.entries(privateState.roles)) { + roles[k] = new Uint8Array(v); + } + + roles[roleString] = new Uint8Array(nonce); + return { roles } }, getRoleCommitmentPath: ( From b407b7c7d8ac40b325e11efddb0265c6ca20085f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Mon, 16 Mar 2026 19:53:00 -0400 Subject: [PATCH 294/322] Add matcher for assertion --- contracts/src/access/test/ShieldedAccessControl.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/src/access/test/ShieldedAccessControl.test.ts b/contracts/src/access/test/ShieldedAccessControl.test.ts index 739e56cd..0d3c7ff5 100644 --- a/contracts/src/access/test/ShieldedAccessControl.test.ts +++ b/contracts/src/access/test/ShieldedAccessControl.test.ts @@ -3652,7 +3652,7 @@ describe('ShieldedAccessControl', () => { ).toBe(false); }); - it('should update nullifier root on successful renounce', () => { + it('should update nullifier set on successful renounce', () => { const nullifierSetSize = shieldedAccessControl .getPublicState() .ShieldedAccessControl__roleCommitmentNullifiers.size(); @@ -3669,7 +3669,7 @@ describe('ShieldedAccessControl', () => { .ShieldedAccessControl__roleCommitmentNullifiers.member( ADMIN.roleNullifier, ), - ); + ).toBe(true); }); }); }); From 6d6e064f24946c26f0928210448f051490637836 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Mon, 16 Mar 2026 20:06:43 -0400 Subject: [PATCH 295/322] Throw error if role nonce is undefined --- .../simulators/ShieldedAccessControlSimulator.ts | 6 +++++- .../witnesses/ShieldedAccessControlWitnesses.ts | 13 ++++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts index b28f6c16..7f29704d 100644 --- a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts +++ b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts @@ -156,7 +156,11 @@ export class ShieldedAccessControlSimulator extends ShieldedAccessControlSimulat */ getCurrentSecretNonce: (role: Uint8Array): Uint8Array => { const roleString = Buffer.from(role).toString('hex'); - return this.getPrivateState().roles[roleString]; + const roleNonce = this.getPrivateState().roles[roleString]; + if (typeof roleNonce === "undefined") { + throw new Error(`Missing secret nonce for role ${roleNonce}`) + } + return roleNonce; }, getCommitmentPathWithFindForLeaf: ( roleCommitment: Uint8Array, diff --git a/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts b/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts index 7a97740c..dc7b315b 100644 --- a/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts +++ b/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts @@ -25,7 +25,7 @@ export interface IShieldedAccessControlWitnesses

{ ): [P, MerkleTreePath]; } -type role = string; +type Role = string; type SecretNonce = Uint8Array; /** @@ -34,7 +34,7 @@ type SecretNonce = Uint8Array; */ export type ShieldedAccessControlPrivateState = { /** @description A 32-byte secret nonce used as a privacy additive. */ - roles: Record; + roles: Record; }; /** @@ -83,6 +83,9 @@ export const ShieldedAccessControlPrivateState = { const roles: Record = {}; for (const [k, v] of Object.entries(privateState.roles)) { + if (typeof v === "undefined") { + throw new Error(`Missing secret nonce for role ${k}`); + } roles[k] = new Uint8Array(v); } @@ -120,7 +123,11 @@ export const ShieldedAccessControlWitnesses = role: Uint8Array, ): [ShieldedAccessControlPrivateState, Uint8Array] { const roleString = Buffer.from(role).toString('hex'); - return [context.privateState, context.privateState.roles[roleString]]; + const roleNonce = context.privateState.roles[roleString]; + if (typeof roleNonce === "undefined") { + throw new Error(`Missing secret nonce for role ${roleString}`); + } + return [context.privateState, roleNonce]; }, wit_getRoleCommitmentPath( context: WitnessContext, From 7e5f144534753100c9df61dfef2a7e275c220d93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Mon, 16 Mar 2026 20:38:32 -0400 Subject: [PATCH 296/322] Add README to contracts directory --- contracts/README.md | 93 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 contracts/README.md diff --git a/contracts/README.md b/contracts/README.md new file mode 100644 index 00000000..21e5fa08 --- /dev/null +++ b/contracts/README.md @@ -0,0 +1,93 @@ +# Contracts README + +This package contains the Compact smart contract source files, compiled artifacts, witness implementations, and test infrastructure for OpenZeppelin Contracts for Compact. + +## Directory Structure + +``` +contracts/ +├── src/ # Source files +│ ├── access/ # Access control contracts +│ ├── security/ # Security utility contracts +│ ├── token/ # Token standard contracts +│ ├── utils/ # General utility contracts +│ ├── archive/ # Archived/deprecated contracts +│ └── test-utils/ # Shared test helpers +├── artifacts/ # Compiled contract outputs (generated) +└── dist/ # Compiled TypeScript witness outputs (generated) +``` + +## src/ + +The `src/` directory is organized by module category. Each module follows the same internal layout: + +``` +/ +├── .compact # Contract source +├── witnesses/ # TypeScript witness implementations +└── test/ + ├── .test.ts # Test suite + ├── mocks/ # Mock contracts (test-only — see warning below) + └── simulators/ # Simulator helpers for testing +``` + +### src/access/ + +Access control primitives for restricting who can call contract circuits. + +| File | Description | +|------|-------------| +| `AccessControl.compact` | Role-based access control | +| `Ownable.compact` | Single-owner access control | +| `ShieldedAccessControl.compact` | Role-based access control with shielded (private) role assignments | +| `ZOwnablePK.compact` | Single-owner access control with shielded ownership | + +### src/security/ + +Contracts that add common security patterns on top of other modules. + +| File | Description | +|------|-------------| +| `Initializable.compact` | One-time initialization mechanism | +| `Pausable.compact` | Emergency pause/unpause mechanism | + +### src/token/ + +Implementations of standard token interfaces. + +| File | Description | +|------|-------------| +| `FungibleToken.compact` | ERC-20-style fungible token | +| `NonFungibleToken.compact` | ERC-721-style non-fungible token | +| `MultiToken.compact` | ERC-1155-style multi-token | + +### src/utils/ + +Low-level utilities shared across modules. + +| File | Description | +|------|-------------| +| `Utils.compact` | Common helper circuits | + +### src/archive/ + +Contracts that are no longer actively maintained. Do not use in new projects. + +### src/test-utils/ + +Shared TypeScript helpers used across test suites (e.g. address utilities). Not part of the public API. + +--- + +## > ⚠️ Mock Contracts Are For Testing Only + +Each module's `test/mocks/` directory contains `Mock*.compact` files (e.g. `MockFungibleToken.compact`, `MockOwnable.compact`, `MockAccessControl.compact`). + +**These contracts exist solely to expose internal state and circuits for testing purposes. They must never be used in production.** + +Mock contracts typically: +- Expose internal or protected circuits publicly for direct testing +- Skip access control or safety checks to isolate specific behaviors +- Introduce additional state that makes testing easier but is unsafe in deployment + +**Using a Mock contract in production would undermine the security guarantees the corresponding production contract is designed to provide.** From e001b1009bfdb67cfd956ffb8303b55f2d74eb5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Mon, 16 Mar 2026 20:43:10 -0400 Subject: [PATCH 297/322] Add disclaimer to all Mock files --- contracts/src/access/test/mocks/MockAccessControl.compact | 5 +++++ contracts/src/access/test/mocks/MockOwnable.compact | 5 +++++ .../access/test/mocks/MockShieldedAccessControl.compact | 5 +++++ contracts/src/access/test/mocks/MockZOwnablePK.compact | 5 +++++ contracts/src/archive/test/mocks/MockShieldedToken.compact | 5 +++++ .../src/security/test/mocks/MockInitializable.compact | 7 +++++++ contracts/src/security/test/mocks/MockPausable.compact | 7 +++++++ contracts/src/token/test/mocks/MockFungibleToken.compact | 5 +++++ contracts/src/token/test/mocks/MockMultiToken.compact | 7 +++++++ .../src/token/test/mocks/MockNonFungibleToken.compact | 7 +++++++ contracts/src/utils/test/mocks/MockUtils.compact | 7 +++++++ 11 files changed, 65 insertions(+) diff --git a/contracts/src/access/test/mocks/MockAccessControl.compact b/contracts/src/access/test/mocks/MockAccessControl.compact index 273121cc..57695020 100644 --- a/contracts/src/access/test/mocks/MockAccessControl.compact +++ b/contracts/src/access/test/mocks/MockAccessControl.compact @@ -1,5 +1,10 @@ // SPDX-License-Identifier: MIT +// WARNING: FOR TESTING PURPOSES ONLY. +// This contract exposes internal circuits and bypasses safety checks that the +// corresponding production contract relies on. DO NOT deploy or use this +// contract in any production application. + pragma language_version >= 0.21.0; import CompactStandardLibrary; diff --git a/contracts/src/access/test/mocks/MockOwnable.compact b/contracts/src/access/test/mocks/MockOwnable.compact index ebbc6110..85ac844f 100644 --- a/contracts/src/access/test/mocks/MockOwnable.compact +++ b/contracts/src/access/test/mocks/MockOwnable.compact @@ -1,5 +1,10 @@ // SPDX-License-Identifier: MIT +// WARNING: FOR TESTING PURPOSES ONLY. +// This contract exposes internal circuits and bypasses safety checks that the +// corresponding production contract relies on. DO NOT deploy or use this +// contract in any production application. + pragma language_version >= 0.21.0; import CompactStandardLibrary; diff --git a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact index 9509114a..1dd7cffb 100644 --- a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact +++ b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact @@ -1,5 +1,10 @@ // SPDX-License-Identifier: MIT +// WARNING: FOR TESTING PURPOSES ONLY. +// This contract exposes internal circuits and bypasses safety checks that the +// corresponding production contract relies on. DO NOT deploy or use this +// contract in any production application. + pragma language_version >= 0.21.0; import CompactStandardLibrary; diff --git a/contracts/src/access/test/mocks/MockZOwnablePK.compact b/contracts/src/access/test/mocks/MockZOwnablePK.compact index e0e5e18a..41657f1d 100644 --- a/contracts/src/access/test/mocks/MockZOwnablePK.compact +++ b/contracts/src/access/test/mocks/MockZOwnablePK.compact @@ -1,5 +1,10 @@ // SPDX-License-Identifier: MIT +// WARNING: FOR TESTING PURPOSES ONLY. +// This contract exposes internal circuits and bypasses safety checks that the +// corresponding production contract relies on. DO NOT deploy or use this +// contract in any production application. + pragma language_version >= 0.21.0; import CompactStandardLibrary; diff --git a/contracts/src/archive/test/mocks/MockShieldedToken.compact b/contracts/src/archive/test/mocks/MockShieldedToken.compact index 68c0fc35..4310501b 100644 --- a/contracts/src/archive/test/mocks/MockShieldedToken.compact +++ b/contracts/src/archive/test/mocks/MockShieldedToken.compact @@ -1,5 +1,10 @@ // SPDX-License-Identifier: MIT +// WARNING: FOR TESTING PURPOSES ONLY. +// This contract exposes internal circuits and bypasses safety checks that the +// corresponding production contract relies on. DO NOT deploy or use this +// contract in any production application. + pragma language_version >= 0.21.0; import CompactStandardLibrary; diff --git a/contracts/src/security/test/mocks/MockInitializable.compact b/contracts/src/security/test/mocks/MockInitializable.compact index ca5dd3fc..d8a9daf9 100644 --- a/contracts/src/security/test/mocks/MockInitializable.compact +++ b/contracts/src/security/test/mocks/MockInitializable.compact @@ -1,3 +1,10 @@ +// SPDX-License-Identifier: MIT + +// WARNING: FOR TESTING PURPOSES ONLY. +// This contract exposes internal circuits and bypasses safety checks that the +// corresponding production contract relies on. DO NOT deploy or use this +// contract in any production application. + pragma language_version >= 0.21.0; import CompactStandardLibrary; diff --git a/contracts/src/security/test/mocks/MockPausable.compact b/contracts/src/security/test/mocks/MockPausable.compact index da9d79a9..4eed6cbf 100644 --- a/contracts/src/security/test/mocks/MockPausable.compact +++ b/contracts/src/security/test/mocks/MockPausable.compact @@ -1,3 +1,10 @@ +// SPDX-License-Identifier: MIT + +// WARNING: FOR TESTING PURPOSES ONLY. +// This contract exposes internal circuits and bypasses safety checks that the +// corresponding production contract relies on. DO NOT deploy or use this +// contract in any production application. + pragma language_version >= 0.21.0; import CompactStandardLibrary; diff --git a/contracts/src/token/test/mocks/MockFungibleToken.compact b/contracts/src/token/test/mocks/MockFungibleToken.compact index 2b86c588..7e23d955 100644 --- a/contracts/src/token/test/mocks/MockFungibleToken.compact +++ b/contracts/src/token/test/mocks/MockFungibleToken.compact @@ -1,5 +1,10 @@ // SPDX-License-Identifier: MIT +// WARNING: FOR TESTING PURPOSES ONLY. +// This contract exposes internal circuits and bypasses safety checks that the +// corresponding production contract relies on. DO NOT deploy or use this +// contract in any production application. + pragma language_version >= 0.21.0; import CompactStandardLibrary; diff --git a/contracts/src/token/test/mocks/MockMultiToken.compact b/contracts/src/token/test/mocks/MockMultiToken.compact index 37d89fd1..e66f2884 100644 --- a/contracts/src/token/test/mocks/MockMultiToken.compact +++ b/contracts/src/token/test/mocks/MockMultiToken.compact @@ -1,3 +1,10 @@ +// SPDX-License-Identifier: MIT + +// WARNING: FOR TESTING PURPOSES ONLY. +// This contract exposes internal circuits and bypasses safety checks that the +// corresponding production contract relies on. DO NOT deploy or use this +// contract in any production application. + pragma language_version >= 0.21.0; import CompactStandardLibrary; diff --git a/contracts/src/token/test/mocks/MockNonFungibleToken.compact b/contracts/src/token/test/mocks/MockNonFungibleToken.compact index a7515486..05a9071e 100644 --- a/contracts/src/token/test/mocks/MockNonFungibleToken.compact +++ b/contracts/src/token/test/mocks/MockNonFungibleToken.compact @@ -1,3 +1,10 @@ +// SPDX-License-Identifier: MIT + +// WARNING: FOR TESTING PURPOSES ONLY. +// This contract exposes internal circuits and bypasses safety checks that the +// corresponding production contract relies on. DO NOT deploy or use this +// contract in any production application. + pragma language_version >= 0.21.0; import CompactStandardLibrary; diff --git a/contracts/src/utils/test/mocks/MockUtils.compact b/contracts/src/utils/test/mocks/MockUtils.compact index 3f13ffac..649a90ef 100644 --- a/contracts/src/utils/test/mocks/MockUtils.compact +++ b/contracts/src/utils/test/mocks/MockUtils.compact @@ -1,3 +1,10 @@ +// SPDX-License-Identifier: MIT + +// WARNING: FOR TESTING PURPOSES ONLY. +// This contract exposes internal circuits and bypasses safety checks that the +// corresponding production contract relies on. DO NOT deploy or use this +// contract in any production application. + pragma language_version >= 0.21.0; import CompactStandardLibrary; From dc3737e7d4bc29d664a85a390905d604ebcca470 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Mon, 16 Mar 2026 20:44:14 -0400 Subject: [PATCH 298/322] Update contracts/src/access/ShieldedAccessControl.compact MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Andrew Fleming Signed-off-by: ⟣ €₥ℵ∪ℓ ⟢ <34749913+emnul@users.noreply.github.com> --- contracts/src/access/ShieldedAccessControl.compact | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index f30e8fb1..2343d6b6 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -291,9 +291,7 @@ module ShieldedAccessControl {  */ export circuit proveCallerRole(role: RoleIdentifier): Boolean { Initializable_assertInitialized(); - - const accountId = _computeAccountId(role); - return _validateRole(role, accountId); + return _uncheckedProveCallerRole(role); } /** From 7c1427a036183670e5970d1cc6ab26f808c4d8fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Mon, 16 Mar 2026 20:54:04 -0400 Subject: [PATCH 299/322] Update circuit requirements --- .../src/access/ShieldedAccessControl.compact | 51 +++++++------------ 1 file changed, 17 insertions(+), 34 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index 2343d6b6..b0470f64 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -227,6 +227,7 @@ module ShieldedAccessControl { * * - caller must prove ownership of `role`. * - caller must not provide valid Merkle tree path for a different `(role, accountId)` pairing. + * - Contract is initialized. * * Disclosures: * @@ -244,30 +245,6 @@ module ShieldedAccessControl { assert(_uncheckedProveCallerRole(role), "ShieldedAccessControl: unauthorized account"); } - /** - * @description Reverts if caller cannot provide a valid proof of ownership for `role`. - * - * @circuitInfo k=15, rows=22130 - * - * Requirements: - * - * - caller must prove ownership of `role`. - * - caller must not provide valid Merkle tree path for a different `(role, accountId)` pairing. - * - * Disclosures: - * - * - A Merkle tree path to a role commitment. - * - A role commitment corresponding to a `(role, accountId)` pairing. - * - A nullifier for the respective role commitment. - * - * @param {RoleIdentifier} role - The role identifier. - * - * @return {[]} - Empty tuple. - */ - circuit _uncheckedAssertOnlyRole(role: RoleIdentifier): [] { - assert(_uncheckedProveCallerRole(role), "ShieldedAccessControl: unauthorized account"); - } - /** * @description Returns `true` if a caller proves ownership of `role` and is not revoked. MAY return false for a legitimately credentialed * caller if the proving environment supplies an invalid Merkle path. This circuit will never return true for an @@ -278,6 +255,7 @@ module ShieldedAccessControl { * Requirements: * * - caller must not provide valid Merkle tree path for a different `(role, accountId)` pairing. + * - Contract is initialized. * * Disclosures: * @@ -335,6 +313,7 @@ module ShieldedAccessControl { * * - caller must prove they're an admin for `role`. * - caller must not provide valid Merkle tree path for a different `(role, accountId)` pairing. + * - Contract is initialized. * * @circuitInfo k=16, rows=39993 * @@ -351,9 +330,8 @@ module ShieldedAccessControl { * @return {[]} - Empty tuple. */ export circuit grantRole(role: RoleIdentifier, accountId: AccountIdentifier): [] { - Initializable_assertInitialized(); - - _uncheckedAssertOnlyRole(getRoleAdmin(role)); + // Initialization check performed in assertOnlyRole + assertOnlyRole(getRoleAdmin(role)); _uncheckedGrantRole(role, accountId); } @@ -382,6 +360,10 @@ module ShieldedAccessControl { * * @dev Commitment and nullifier checks are inlined in this circuit to avoid an expensive re-computation of the role commitment and nullifier * + * Requirements: + * + * - Contract is initialized. + * * @circuitInfo k=15, rows=18115 * * Disclosures: @@ -467,6 +449,7 @@ module ShieldedAccessControl { * Requirements: * * - The caller must provide a valid `accountId` for a `role`. + * - Contract is initialized. * * Disclosures: * @@ -499,6 +482,7 @@ module ShieldedAccessControl { * * - caller must prove they're an admin for `role`. * - caller must not provide valid Merkle tree path for a different `(role, accountId)` pairing. + * - Contract is initialized. * * @circuitInfo k=18, rows=138761 * @@ -515,9 +499,8 @@ module ShieldedAccessControl { * @return {[]} - Empty tuple. */ export circuit revokeRole(role: RoleIdentifier, accountId: AccountIdentifier): [] { - Initializable_assertInitialized(); - - _uncheckedAssertOnlyRole(getRoleAdmin(role)); + // Initialization check performed in assertOnlyRole + assertOnlyRole(getRoleAdmin(role)); _uncheckedRevokeRole(role, accountId); } @@ -528,6 +511,10 @@ module ShieldedAccessControl { * so a `(role, accountId)` pairing that does not exist can still be revoked. Returns false if a role is already * revoked. Internal circuit without access restriction. * + * Requirements: + * + * - Contract is initialized. + * * @circuitInfo k=15, rows=18115 * * Disclosures: @@ -703,10 +690,6 @@ module ShieldedAccessControl { * * @circuitInfo k=13, rows=6423 * - * Requirements: - * - * - Contract is initialized. - * * @param {RoleIdentifier} role - The role identifier. * @param {AccountIdentifier} accountId - The unique identifier of the account. * From 99b18c67a518985cf9aefe8cd5256e72fff03378 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Mon, 16 Mar 2026 21:02:40 -0400 Subject: [PATCH 300/322] Refactor: Remove _uncheckedAssert, keep _grant / _revoke DRY --- .../src/access/ShieldedAccessControl.compact | 25 ++----------------- .../mocks/MockShieldedAccessControl.compact | 4 --- 2 files changed, 2 insertions(+), 27 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index b0470f64..26ec0f04 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -241,7 +241,6 @@ module ShieldedAccessControl { */ export circuit assertOnlyRole(role: RoleIdentifier): [] { Initializable_assertInitialized(); - assert(_uncheckedProveCallerRole(role), "ShieldedAccessControl: unauthorized account"); } @@ -378,17 +377,7 @@ module ShieldedAccessControl { */ export circuit _grantRole(role: RoleIdentifier, accountId: AccountIdentifier): Boolean { Initializable_assertInitialized(); - - const roleCommitment = _computeRoleCommitment(role, accountId); - const roleNullifier = _computeNullifier(roleCommitment); - const isRevoked = _roleCommitmentNullifiers.member(disclose(roleNullifier)); - - if (isRevoked) { - return false; - } - - _operatorRoles.insert(disclose(roleCommitment)); - return true; + return _uncheckedGrantRole(role, accountId); } /** @@ -528,17 +517,7 @@ module ShieldedAccessControl { */ export circuit _revokeRole(role: RoleIdentifier, accountId: AccountIdentifier): Boolean { Initializable_assertInitialized(); - - const roleCommitment = _computeRoleCommitment(role, accountId); - const roleNullifier = _computeNullifier(roleCommitment); - const isRevoked = _roleCommitmentNullifiers.member(disclose(roleNullifier)); - - if (isRevoked) { - return false; - } - - _roleCommitmentNullifiers.insert(disclose(roleNullifier)); - return true; + return _uncheckedRevokeRole(role, accountId); } /** diff --git a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact index 1dd7cffb..c87e4b34 100644 --- a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact +++ b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact @@ -100,10 +100,6 @@ export circuit assertOnlyRole(roleId: ShieldedAccessControl_RoleIdentifier): [] ShieldedAccessControl_assertOnlyRole(roleId); } -export circuit _uncheckedAssertOnlyRole(role: ShieldedAccessControl_RoleIdentifier): [] { - assert(_uncheckedProveCallerRole(role), "ShieldedAccessControl: unauthorized account"); -} - // _validateRole is re-implemented in the Mock contract for testing export circuit _validateRole( role: ShieldedAccessControl_RoleIdentifier, From 75e6dcb17986a28406482a495ba8c887d355d3ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Mon, 16 Mar 2026 21:03:47 -0400 Subject: [PATCH 301/322] Refactor: remove references to _uncheckedAssert --- contracts/src/access/test/ShieldedAccessControl.test.ts | 1 - .../access/test/simulators/ShieldedAccessControlSimulator.ts | 4 ---- 2 files changed, 5 deletions(-) diff --git a/contracts/src/access/test/ShieldedAccessControl.test.ts b/contracts/src/access/test/ShieldedAccessControl.test.ts index 0d3c7ff5..cfc4dbe0 100644 --- a/contracts/src/access/test/ShieldedAccessControl.test.ts +++ b/contracts/src/access/test/ShieldedAccessControl.test.ts @@ -146,7 +146,6 @@ describe('ShieldedAccessControl', () => { // Circuit calls should succeed const circuitsToSucceed: UncheckedCircuits[] = [ ['_uncheckedProveCallerRole', [UNINITIALIZED.role]], - ['_uncheckedAssertOnlyRole', [UNINITIALIZED.role]], ['getRoleAdmin', [UNINITIALIZED.role]], ['_uncheckedGrantRole', [UNINITIALIZED.role, UNINITIALIZED.accountId]], ['_uncheckedRevokeRole', [UNINITIALIZED.role, UNINITIALIZED.accountId]], diff --git a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts index 7f29704d..109e32fe 100644 --- a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts +++ b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts @@ -86,10 +86,6 @@ export class ShieldedAccessControlSimulator extends ShieldedAccessControlSimulat this.circuits.impure.assertOnlyRole(role); } - public _uncheckedAssertOnlyRole(role: Uint8Array) { - this.circuits.impure._uncheckedAssertOnlyRole(role); - } - public _validateRole(role: Uint8Array, accountId: Uint8Array): boolean { return this.circuits.impure._validateRole(role, accountId); } From 72fa7b30960e95cb349637f2439ce37112f4f254 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Mon, 16 Mar 2026 21:06:47 -0400 Subject: [PATCH 302/322] Remove warning from pure circuit --- contracts/src/access/ShieldedAccessControl.compact | 4 ---- 1 file changed, 4 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index 26ec0f04..e0d1ca25 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -691,10 +691,6 @@ module ShieldedAccessControl { /** * @description Computes the role nullifier for a given `roleCommitment`. * - * @warning This circuit does not perform an initialization check. It's only meant to be used as - * an internal helper in the Shielded Access Control module. Using this circuit outside of the - * module may cause undefined behavior and break security guarantees. - * * ## Role Nullifier Derivation * `roleNullifier = SHA256(roleCommitment, nullifierDomain)` * From b2defa9ef67b8d5e7619b9dfd095b69d42ebec03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Mon, 16 Mar 2026 21:11:11 -0400 Subject: [PATCH 303/322] revert changes to simulator test utils --- .../simulator/test/fixtures/utils/address.ts | 45 +++++-------------- 1 file changed, 12 insertions(+), 33 deletions(-) diff --git a/packages/simulator/test/fixtures/utils/address.ts b/packages/simulator/test/fixtures/utils/address.ts index a9578120..d99fde3f 100644 --- a/packages/simulator/test/fixtures/utils/address.ts +++ b/packages/simulator/test/fixtures/utils/address.ts @@ -70,34 +70,23 @@ export const createEitherTestContractAddress = (str: string) => ({ const baseGeneratePubKeyPair = ( str: string, asEither: boolean, - asPK: boolean, ): [ - string, - ( - | Compact.ZswapCoinPublicKey - | Compact.Either - ), -] => { + string, + ( + | Compact.ZswapCoinPublicKey + | Compact.Either + ), + ] => { const pk = toHexPadded(str); - - if (asEither && asPK) { - return [pk, createEitherTestUser(str)]; - } - if (asEither && !asPK) { - return [pk, createEitherTestContractAddress(str)]; - } - - return [pk, encodeToPK(str)]; + const zpk = asEither ? createEitherTestUser(str) : encodeToPK(str); + return [pk, zpk]; }; export const generatePubKeyPair = (str: string) => - baseGeneratePubKeyPair(str, false, false) as [ - string, - Compact.ZswapCoinPublicKey, - ]; + baseGeneratePubKeyPair(str, false) as [string, Compact.ZswapCoinPublicKey]; -export const generateEitherPubKeyPair = (str: string, asPK = true) => - baseGeneratePubKeyPair(str, true, asPK) as [ +export const generateEitherPubKeyPair = (str: string) => + baseGeneratePubKeyPair(str, true) as [ string, Compact.Either, ]; @@ -115,14 +104,4 @@ export const ZERO_ADDRESS = { is_left: false, left: encodeToPK(''), right: { bytes: zeroUint8Array() }, -}; - -export const eitherToBytes = ( - account: Compact.Either, -) => { - if (account.is_left) { - return account.left.bytes; - } - - return account.right.bytes; -}; +}; \ No newline at end of file From 79ad5a36b8fa2727611dd5f03af38bb89999bc84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Tue, 17 Mar 2026 17:05:33 -0400 Subject: [PATCH 304/322] rename circuit --- .../src/access/ShieldedAccessControl.compact | 8 +- .../access/test/ShieldedAccessControl.test.ts | 74 +++++++++---------- .../mocks/MockShieldedAccessControl.compact | 6 +- .../ShieldedAccessControlSimulator.ts | 8 +- 4 files changed, 48 insertions(+), 48 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index e0d1ca25..df625a78 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -241,7 +241,7 @@ module ShieldedAccessControl { */ export circuit assertOnlyRole(role: RoleIdentifier): [] { Initializable_assertInitialized(); - assert(_uncheckedProveCallerRole(role), "ShieldedAccessControl: unauthorized account"); + assert(_uncheckedCanProveRole(role), "ShieldedAccessControl: unauthorized account"); } /** @@ -266,9 +266,9 @@ module ShieldedAccessControl { * * @return {Boolean} - A boolean determining if a caller successfully proved ownership of `role`  */ - export circuit proveCallerRole(role: RoleIdentifier): Boolean { + export circuit canProveRole(role: RoleIdentifier): Boolean { Initializable_assertInitialized(); - return _uncheckedProveCallerRole(role); + return _uncheckedCanProveRole(role); } /** @@ -296,7 +296,7 @@ module ShieldedAccessControl { * * @return {Boolean} - A boolean determining if a caller successfully proved ownership of `role`  */ - circuit _uncheckedProveCallerRole(role: RoleIdentifier): Boolean { + circuit _uncheckedCanProveRole(role: RoleIdentifier): Boolean { const accountId = _computeAccountId(role); return _validateRole(role, accountId); } diff --git a/contracts/src/access/test/ShieldedAccessControl.test.ts b/contracts/src/access/test/ShieldedAccessControl.test.ts index cfc4dbe0..11418d9d 100644 --- a/contracts/src/access/test/ShieldedAccessControl.test.ts +++ b/contracts/src/access/test/ShieldedAccessControl.test.ts @@ -123,7 +123,7 @@ describe('ShieldedAccessControl', () => { ]; // Circuit calls should fail before the args are used const circuitsToFail: FailingCircuits[] = [ - ['proveCallerRole', [UNINITIALIZED.role]], + ['canProveRole', [UNINITIALIZED.role]], ['assertOnlyRole', [UNINITIALIZED.role]], ['grantRole', [UNINITIALIZED.role, UNINITIALIZED.accountId]], ['revokeRole', [UNINITIALIZED.role, UNINITIALIZED.accountId]], @@ -145,7 +145,7 @@ describe('ShieldedAccessControl', () => { ]; // Circuit calls should succeed const circuitsToSucceed: UncheckedCircuits[] = [ - ['_uncheckedProveCallerRole', [UNINITIALIZED.role]], + ['_uncheckedCanProveRole', [UNINITIALIZED.role]], ['getRoleAdmin', [UNINITIALIZED.role]], ['_uncheckedGrantRole', [UNINITIALIZED.role, UNINITIALIZED.accountId]], ['_uncheckedRevokeRole', [UNINITIALIZED.role, UNINITIALIZED.accountId]], @@ -2919,7 +2919,7 @@ describe('ShieldedAccessControl', () => { }); }); - describe('proveCallerRole', () => { + describe('canProveRole', () => { beforeEach(() => { shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); shieldedAccessControl.setPersistentCaller(ADMIN.publicKey); @@ -2945,7 +2945,7 @@ describe('ShieldedAccessControl', () => { }, ); expect(() => { - shieldedAccessControl.proveCallerRole(ADMIN.role); + shieldedAccessControl.canProveRole(ADMIN.role); }).toThrow( 'ShieldedAccessControl: Path must contain leaf matching computed role commitment for the provided role, accountId pairing', ); @@ -2962,7 +2962,7 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), ).toBe(true); - expect(shieldedAccessControl.proveCallerRole(ADMIN.role)).toBe( + expect(shieldedAccessControl.canProveRole(ADMIN.role)).toBe( true, ); }); @@ -2997,16 +2997,16 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl._grantRole(OPERATOR_2.role, account2); shieldedAccessControl._grantRole(OPERATOR_3.role, account3); - expect(shieldedAccessControl.proveCallerRole(ADMIN.role)).toBe( + expect(shieldedAccessControl.canProveRole(ADMIN.role)).toBe( true, ); - expect(shieldedAccessControl.proveCallerRole(OPERATOR_1.role)).toBe( + expect(shieldedAccessControl.canProveRole(OPERATOR_1.role)).toBe( true, ); - expect(shieldedAccessControl.proveCallerRole(OPERATOR_2.role)).toBe( + expect(shieldedAccessControl.canProveRole(OPERATOR_2.role)).toBe( true, ); - expect(shieldedAccessControl.proveCallerRole(OPERATOR_3.role)).toBe( + expect(shieldedAccessControl.canProveRole(OPERATOR_3.role)).toBe( true, ); }); @@ -3027,7 +3027,7 @@ describe('ShieldedAccessControl', () => { expect(newAdminAccountId).not.toEqual(ADMIN.accountId); shieldedAccessControl._grantRole(ADMIN.role, newAdminAccountId); - expect(shieldedAccessControl.proveCallerRole(ADMIN.role)).toBe( + expect(shieldedAccessControl.canProveRole(ADMIN.role)).toBe( true, ); }); @@ -3049,7 +3049,7 @@ describe('ShieldedAccessControl', () => { operator1AdminAccountId, ); shieldedAccessControl.as(ADMIN.publicKey); // prove ADMIN has OP_1 role - expect(shieldedAccessControl.proveCallerRole(OPERATOR_1.role)).toBe( + expect(shieldedAccessControl.canProveRole(OPERATOR_1.role)).toBe( true, ); @@ -3062,7 +3062,7 @@ describe('ShieldedAccessControl', () => { operator1Op2AccountId, ); shieldedAccessControl.as(OPERATOR_2.publicKey); // prove OP_2 has OP_1 role - expect(shieldedAccessControl.proveCallerRole(OPERATOR_1.role)).toBe( + expect(shieldedAccessControl.canProveRole(OPERATOR_1.role)).toBe( true, ); @@ -3075,7 +3075,7 @@ describe('ShieldedAccessControl', () => { operator1Op3AccountId, ); shieldedAccessControl.as(OPERATOR_3.publicKey); // prove OP_3 has OP_1 role - expect(shieldedAccessControl.proveCallerRole(OPERATOR_1.role)).toBe( + expect(shieldedAccessControl.canProveRole(OPERATOR_1.role)).toBe( true, ); }); @@ -3098,7 +3098,7 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl._validateRole(OPERATOR_1.role, accountId), ).toBe(false); - expect(shieldedAccessControl.proveCallerRole(OPERATOR_1.role)).toBe( + expect(shieldedAccessControl.canProveRole(OPERATOR_1.role)).toBe( false, ); }); @@ -3111,7 +3111,7 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), ).toBe(false); - expect(shieldedAccessControl.proveCallerRole(ADMIN.role)).toBe( + expect(shieldedAccessControl.canProveRole(ADMIN.role)).toBe( false, ); }); @@ -3124,7 +3124,7 @@ describe('ShieldedAccessControl', () => { ).toBe(false); shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); - expect(shieldedAccessControl.proveCallerRole(ADMIN.role)).toBe( + expect(shieldedAccessControl.canProveRole(ADMIN.role)).toBe( false, ); }); @@ -3133,7 +3133,7 @@ describe('ShieldedAccessControl', () => { // UNAUTHORIZED uses the same private state (ADMIN.secretNonce for ADMIN.role), // so their derived accountId won't match the committed one. shieldedAccessControl.as(UNAUTHORIZED.publicKey); - expect(shieldedAccessControl.proveCallerRole(ADMIN.role)).toBe( + expect(shieldedAccessControl.canProveRole(ADMIN.role)).toBe( false, ); }); @@ -3159,7 +3159,7 @@ describe('ShieldedAccessControl', () => { ), ); - expect(shieldedAccessControl.proveCallerRole(ADMIN.role)).toBe( + expect(shieldedAccessControl.canProveRole(ADMIN.role)).toBe( false, ); }); @@ -3178,15 +3178,15 @@ describe('ShieldedAccessControl', () => { 'wit_getRoleCommitmentPath', RETURN_BAD_PATH, ); - expect(shieldedAccessControl.proveCallerRole(ADMIN.role)).toBe( + expect(shieldedAccessControl.canProveRole(ADMIN.role)).toBe( false, ); }); }); }); - // TODO refactor to test _uncheckedProveCallerRole - describe.skip('_uncheckedProveCallerRole', () => { + // TODO refactor to test _uncheckedCanProveRole + describe.skip('_uncheckedCanProveRole', () => { beforeEach(() => { shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); shieldedAccessControl.setPersistentCaller(ADMIN.publicKey); @@ -3212,7 +3212,7 @@ describe('ShieldedAccessControl', () => { }, ); expect(() => { - shieldedAccessControl.proveCallerRole(ADMIN.role); + shieldedAccessControl.canProveRole(ADMIN.role); }).toThrow( 'ShieldedAccessControl: Path must contain leaf matching computed role commitment for the provided role, accountId pairing', ); @@ -3229,7 +3229,7 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), ).toBe(true); - expect(shieldedAccessControl.proveCallerRole(ADMIN.role)).toBe( + expect(shieldedAccessControl.canProveRole(ADMIN.role)).toBe( true, ); }); @@ -3264,16 +3264,16 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl._grantRole(OPERATOR_2.role, account2); shieldedAccessControl._grantRole(OPERATOR_3.role, account3); - expect(shieldedAccessControl.proveCallerRole(ADMIN.role)).toBe( + expect(shieldedAccessControl.canProveRole(ADMIN.role)).toBe( true, ); - expect(shieldedAccessControl.proveCallerRole(OPERATOR_1.role)).toBe( + expect(shieldedAccessControl.canProveRole(OPERATOR_1.role)).toBe( true, ); - expect(shieldedAccessControl.proveCallerRole(OPERATOR_2.role)).toBe( + expect(shieldedAccessControl.canProveRole(OPERATOR_2.role)).toBe( true, ); - expect(shieldedAccessControl.proveCallerRole(OPERATOR_3.role)).toBe( + expect(shieldedAccessControl.canProveRole(OPERATOR_3.role)).toBe( true, ); }); @@ -3294,7 +3294,7 @@ describe('ShieldedAccessControl', () => { expect(newAdminAccountId).not.toEqual(ADMIN.accountId); shieldedAccessControl._grantRole(ADMIN.role, newAdminAccountId); - expect(shieldedAccessControl.proveCallerRole(ADMIN.role)).toBe( + expect(shieldedAccessControl.canProveRole(ADMIN.role)).toBe( true, ); }); @@ -3316,7 +3316,7 @@ describe('ShieldedAccessControl', () => { operator1AdminAccountId, ); shieldedAccessControl.as(ADMIN.publicKey); // prove ADMIN has OP_1 role - expect(shieldedAccessControl.proveCallerRole(OPERATOR_1.role)).toBe( + expect(shieldedAccessControl.canProveRole(OPERATOR_1.role)).toBe( true, ); @@ -3329,7 +3329,7 @@ describe('ShieldedAccessControl', () => { operator1Op2AccountId, ); shieldedAccessControl.as(OPERATOR_2.publicKey); // prove OP_2 has OP_1 role - expect(shieldedAccessControl.proveCallerRole(OPERATOR_1.role)).toBe( + expect(shieldedAccessControl.canProveRole(OPERATOR_1.role)).toBe( true, ); @@ -3342,7 +3342,7 @@ describe('ShieldedAccessControl', () => { operator1Op3AccountId, ); shieldedAccessControl.as(OPERATOR_3.publicKey); // prove OP_3 has OP_1 role - expect(shieldedAccessControl.proveCallerRole(OPERATOR_1.role)).toBe( + expect(shieldedAccessControl.canProveRole(OPERATOR_1.role)).toBe( true, ); }); @@ -3365,7 +3365,7 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl._validateRole(OPERATOR_1.role, accountId), ).toBe(false); - expect(shieldedAccessControl.proveCallerRole(OPERATOR_1.role)).toBe( + expect(shieldedAccessControl.canProveRole(OPERATOR_1.role)).toBe( false, ); }); @@ -3378,7 +3378,7 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), ).toBe(false); - expect(shieldedAccessControl.proveCallerRole(ADMIN.role)).toBe( + expect(shieldedAccessControl.canProveRole(ADMIN.role)).toBe( false, ); }); @@ -3391,7 +3391,7 @@ describe('ShieldedAccessControl', () => { ).toBe(false); shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); - expect(shieldedAccessControl.proveCallerRole(ADMIN.role)).toBe( + expect(shieldedAccessControl.canProveRole(ADMIN.role)).toBe( false, ); }); @@ -3400,7 +3400,7 @@ describe('ShieldedAccessControl', () => { // UNAUTHORIZED uses the same private state (ADMIN.secretNonce for ADMIN.role), // so their derived accountId won't match the committed one. shieldedAccessControl.as(UNAUTHORIZED.publicKey); - expect(shieldedAccessControl.proveCallerRole(ADMIN.role)).toBe( + expect(shieldedAccessControl.canProveRole(ADMIN.role)).toBe( false, ); }); @@ -3426,7 +3426,7 @@ describe('ShieldedAccessControl', () => { ), ); - expect(shieldedAccessControl.proveCallerRole(ADMIN.role)).toBe( + expect(shieldedAccessControl.canProveRole(ADMIN.role)).toBe( false, ); }); @@ -3445,7 +3445,7 @@ describe('ShieldedAccessControl', () => { 'wit_getRoleCommitmentPath', RETURN_BAD_PATH, ); - expect(shieldedAccessControl.proveCallerRole(ADMIN.role)).toBe( + expect(shieldedAccessControl.canProveRole(ADMIN.role)).toBe( false, ); }); diff --git a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact index c87e4b34..16de18e7 100644 --- a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact +++ b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact @@ -87,11 +87,11 @@ export pure circuit _computeNullifier( as ShieldedAccessControl_RoleNullifier; } -export circuit proveCallerRole(roleId: ShieldedAccessControl_RoleIdentifier): Boolean { - return ShieldedAccessControl_proveCallerRole(roleId); +export circuit canProveRole(roleId: ShieldedAccessControl_RoleIdentifier): Boolean { + return ShieldedAccessControl_canProveRole(roleId); } -export circuit _uncheckedProveCallerRole(role: ShieldedAccessControl_RoleIdentifier): Boolean { +export circuit _uncheckedCanProveRole(role: ShieldedAccessControl_RoleIdentifier): Boolean { const accountId = _computeAccountId(role); return _validateRole(role, accountId); } diff --git a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts index 109e32fe..6132ba54 100644 --- a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts +++ b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts @@ -74,12 +74,12 @@ export class ShieldedAccessControlSimulator extends ShieldedAccessControlSimulat return this.circuits.pure.DEFAULT_ADMIN_ROLE(); } - public proveCallerRole(role: Uint8Array): boolean { - return this.circuits.impure.proveCallerRole(role); + public canProveRole(role: Uint8Array): boolean { + return this.circuits.impure.canProveRole(role); } - public _uncheckedProveCallerRole(role: Uint8Array): boolean { - return this.circuits.impure._uncheckedProveCallerRole(role); + public _uncheckedCanProveRole(role: Uint8Array): boolean { + return this.circuits.impure._uncheckedCanProveRole(role); } public assertOnlyRole(role: Uint8Array) { From 944d251348ee5271139f1acd01af50f557d9f4d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Tue, 17 Mar 2026 17:21:43 -0400 Subject: [PATCH 305/322] Update circuitInfo --- .../src/access/ShieldedAccessControl.compact | 32 +++++++++---------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index df625a78..8451fa4e 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -221,7 +221,7 @@ module ShieldedAccessControl { /** * @description Reverts if caller cannot provide a valid proof of ownership for `role`. * - * @circuitInfo k=15, rows=22130 + * @circuitInfo k=15, rows=19237 * * Requirements: * @@ -249,7 +249,7 @@ module ShieldedAccessControl { * caller if the proving environment supplies an invalid Merkle path. This circuit will never return true for an * unauthorized caller. * - * @circuitInfo k=15, rows=22128 + * @circuitInfo k=15, rows=19237 * * Requirements: * @@ -280,7 +280,7 @@ module ShieldedAccessControl { * an internal helper in the Shielded Access Control module. Using this circuit outside of the * module may cause undefined behavior and break security guarantees. * - * @circuitInfo k=15, rows=22128 + * @circuitInfo k=15, rows=19235 * * Requirements: * @@ -314,7 +314,7 @@ module ShieldedAccessControl { * - caller must not provide valid Merkle tree path for a different `(role, accountId)` pairing. * - Contract is initialized. * - * @circuitInfo k=16, rows=39993 + * @circuitInfo k=15, rows=31312 * * Disclosures: * @@ -363,7 +363,7 @@ module ShieldedAccessControl { * * - Contract is initialized. * - * @circuitInfo k=15, rows=18115 + * @circuitInfo k=14, rows=12333 * * Disclosures: * @@ -397,7 +397,7 @@ module ShieldedAccessControl { * * @dev Commitment and nullifier checks are inlined in this circuit to avoid an expensive re-computation of the role commitment and nullifier * - * @circuitInfo k=15, rows=18115 + * @circuitInfo k=14, rows=12331 * * Disclosures: * @@ -433,7 +433,7 @@ module ShieldedAccessControl { * @warning Outside observers may be able to use timing and pattern analysis to weaken pseudonymity * guarantees if renounceRole is used in tandem with other on-chain actions. * - * @circuitInfo k=17, rows=108992 + * @circuitInfo k=15, rows=16663 * * Requirements: * @@ -465,7 +465,7 @@ module ShieldedAccessControl { * `role`. At this time, proofs of non-membership on values in the Merkle tree LDT are not possible * so a `(role, accountId)` pairing that does not exist can still be revoked. * - * @circuitInfo k=18, rows=138517 + * @circuitInfo k=15, rows=29301 * * Requirements: * @@ -473,8 +473,6 @@ module ShieldedAccessControl { * - caller must not provide valid Merkle tree path for a different `(role, accountId)` pairing. * - Contract is initialized. * - * @circuitInfo k=18, rows=138761 - * * Disclosures: * * - A Merkle tree path to a role commitment. @@ -504,7 +502,7 @@ module ShieldedAccessControl { * * - Contract is initialized. * - * @circuitInfo k=15, rows=18115 + * @circuitInfo k=14, rows=10322 * * Disclosures: * @@ -531,7 +529,7 @@ module ShieldedAccessControl { * an internal helper in the Shielded Access Control module. Using this circuit outside of the * module may cause undefined behavior and break security guarantees. * - * @circuitInfo k=15, rows=18115 + * @circuitInfo k=14, rows=10320 * * Disclosures: * @@ -561,7 +559,7 @@ module ShieldedAccessControl { * * To change a role’s admin use {_setRoleAdmin}. * - * @circuitInfo k=9, rows=375 + * @circuitInfo k=9, rows=373 * * Disclosures: * @@ -581,7 +579,7 @@ module ShieldedAccessControl { /** * @description Sets `adminId` as `role`'s admin identifier. Internal circuit without access restriction. * - * @circuitInfo k=10, rows=583 + * @circuitInfo k=10, rows=581 * * Disclosures: * @@ -605,7 +603,7 @@ module ShieldedAccessControl { * an internal helper in the Shielded Access Control module. Using this circuit outside of the * module may cause undefined behavior and break security guarantees. * - * @circuitInfo k=14, rows=16089 + * @circuitInfo k=14, rows=13179 * * Requirements: * @@ -667,7 +665,7 @@ module ShieldedAccessControl { * - `commitmentDomain`: Domain separator `"ShieldedAccessControl:commitment"` (padded to 32 bytes) to prevent * hash collisions when extending the module or using similar commitment schemes. * - * @circuitInfo k=13, rows=6423 + * @circuitInfo k=13, rows=6421 * * @param {RoleIdentifier} role - The role identifier. * @param {AccountIdentifier} accountId - The unique identifier of the account. @@ -732,7 +730,7 @@ module ShieldedAccessControl { * This value is later used in role commitment hashing, * and acts as a privacy-preserving alternative to a raw public key. * - * @circuitInfo k=13, rows=6705 + * @circuitInfo k=13, rows=6659 * * @param {RoleIdentifier} role - A private nonce to scope the commitment. * From 9c86e1aba2534d3c09f8731365d673fcb570f518 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Tue, 17 Mar 2026 18:23:41 -0400 Subject: [PATCH 306/322] Remove tests --- .../src/access/ShieldedAccessControl.compact | 85 +-- .../access/test/ShieldedAccessControl.test.ts | 575 +----------------- .../mocks/MockShieldedAccessControl.compact | 100 ++- .../ShieldedAccessControlSimulator.ts | 9 +- 4 files changed, 79 insertions(+), 690 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index 8451fa4e..61c620eb 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -118,6 +118,11 @@ module ShieldedAccessControl { import "../utils/Utils" prefix Utils_; import "../security/Initializable" prefix Initializable_; + export enum UpdateType { + Grant, + Revoke + }; + // TODO: Standardize types across contracts https://github.com/OpenZeppelin/compact-contracts/issues/368 export new type RoleCommitment = Bytes<32>; export new type RoleIdentifier = Bytes<32>; @@ -331,7 +336,7 @@ module ShieldedAccessControl { export circuit grantRole(role: RoleIdentifier, accountId: AccountIdentifier): [] { // Initialization check performed in assertOnlyRole assertOnlyRole(getRoleAdmin(role)); - _uncheckedGrantRole(role, accountId); + _updateRole(role, accountId, UpdateType.Grant); } /** @@ -377,49 +382,7 @@ module ShieldedAccessControl { */ export circuit _grantRole(role: RoleIdentifier, accountId: AccountIdentifier): Boolean { Initializable_assertInitialized(); - return _uncheckedGrantRole(role, accountId); - } - - /** - * @description Grants `role` to `accountId` by inserting a role commitment unique to the - * `(role, accountId)` pairing into the `_operatorRoles` Merkle tree. Duplicate role commitments can be issued - * so long as they remain unrevoked. This does not yield any additional authority and simply wastes - * limited Merkle tree storage slots. Once revoked, a role cannot be re-granted to the same `accountId`. A new `accountId` must be - * generated to be re-authorized for a revoked `role`. - * - * Internal circuit without access restriction. - * - * See Storage Caveat in {_grantRole} - * - * @warning This circuit does not perform an initialization check. It's only meant to be used as - * an internal helper in the Shielded Access Control module. Using this circuit outside of the - * module may cause undefined behavior and break security guarantees. - * - * @dev Commitment and nullifier checks are inlined in this circuit to avoid an expensive re-computation of the role commitment and nullifier - * - * @circuitInfo k=14, rows=12331 - * - * Disclosures: - * - * - A role commitment corresponding to a `(role, accountId)` pairing. - * - A nullifier for the respective role commitment. - * - * @param {RoleIdentifier} role - The role identifier. - * @param {AccountIdentifier} accountId - The unique identifier of the account. - * - * @return {Boolean} isGranted - Returns true if a role was granted successfully. - */ - circuit _uncheckedGrantRole(role: RoleIdentifier, accountId: AccountIdentifier): Boolean { - const roleCommitment = _computeRoleCommitment(role, accountId); - const roleNullifier = _computeNullifier(roleCommitment); - const isRevoked = _roleCommitmentNullifiers.member(disclose(roleNullifier)); - - if (isRevoked) { - return false; - } - - _operatorRoles.insert(disclose(roleCommitment)); - return true; + return _updateRole(role, accountId, UpdateType.Grant); } /** @@ -456,7 +419,7 @@ module ShieldedAccessControl { "ShieldedAccessControl: bad confirmation" ); - _uncheckedRevokeRole(role, accountIdConfirmation); + _updateRole(role, accountIdConfirmation, UpdateType.Revoke); } /** @@ -488,7 +451,7 @@ module ShieldedAccessControl { export circuit revokeRole(role: RoleIdentifier, accountId: AccountIdentifier): [] { // Initialization check performed in assertOnlyRole assertOnlyRole(getRoleAdmin(role)); - _uncheckedRevokeRole(role, accountId); + _updateRole(role, accountId, UpdateType.Revoke); } /** @@ -515,32 +478,31 @@ module ShieldedAccessControl { */ export circuit _revokeRole(role: RoleIdentifier, accountId: AccountIdentifier): Boolean { Initializable_assertInitialized(); - return _uncheckedRevokeRole(role, accountId); + return _updateRole(role, accountId, UpdateType.Revoke); } /** - * @description Permanently revokes `role` from `accountId` by inserting a role nullifier into the - * `_roleCommitmentNullifiers` set. Once revoked, a new `accountId` must be generated to be re-authorized for - * `role`. At this time, proofs of non-membership on values in the Merkle tree LDT are not possible - * so a `(role, accountId)` pairing that does not exist can still be revoked. Returns false if a role is already - * revoked. Internal circuit without access restriction. + * @description Core business logic for the grant/revoke role circuits. Returns false if a role is revoked. + * Otherwise, dispatches on `updateType`: a `Grant` inserts the role commitment into `_operatorRoles`, and + * a `Revoke` inserts the nullifier into `_roleCommitmentNullifiers`. Returns true on success. * - * @warning This circuit does not perform an initialization check. It's only meant to be used as - * an internal helper in the Shielded Access Control module. Using this circuit outside of the - * module may cause undefined behavior and break security guarantees. - * - * @circuitInfo k=14, rows=10320 + * @circuitInfo k=14, rows=12391 * * Disclosures: * * - A nullifier for the respective role commitment. + * - A role commitment on success * * @param {RoleIdentifier} role - The role identifier. * @param {AccountIdentifier} accountId - The unique identifier of the account. * * @return {Boolean} isRevoked - Returns true if operation completes successfully. */ - circuit _uncheckedRevokeRole(role: RoleIdentifier, accountId: AccountIdentifier): Boolean { + circuit _updateRole( + role: RoleIdentifier, + accountId: AccountIdentifier, + updateType: UpdateType + ): Boolean { const roleCommitment = _computeRoleCommitment(role, accountId); const roleNullifier = _computeNullifier(roleCommitment); const isRevoked = _roleCommitmentNullifiers.member(disclose(roleNullifier)); @@ -549,7 +511,12 @@ module ShieldedAccessControl { return false; } - _roleCommitmentNullifiers.insert(disclose(roleNullifier)); + if (updateType == UpdateType.Grant) { + _operatorRoles.insert(disclose(roleCommitment)); + } else { + _roleCommitmentNullifiers.insert(disclose(roleNullifier)); + } + return true; } diff --git a/contracts/src/access/test/ShieldedAccessControl.test.ts b/contracts/src/access/test/ShieldedAccessControl.test.ts index 11418d9d..1f1f95ba 100644 --- a/contracts/src/access/test/ShieldedAccessControl.test.ts +++ b/contracts/src/access/test/ShieldedAccessControl.test.ts @@ -8,9 +8,10 @@ import { } from '@midnight-ntwrk/compact-runtime'; import { beforeEach, describe, expect, it } from 'vitest'; import * as utils from '#test-utils/address.js'; -import type { +import { Ledger, ZswapCoinPublicKey, + ShieldedAccessControl_UpdateType as UpdateType } from '../../../artifacts/MockShieldedAccessControl/contract/index.js'; import { ShieldedAccessControlPrivateState } from '../witnesses/ShieldedAccessControlWitnesses.js'; import { ShieldedAccessControlSimulator } from './simulators/ShieldedAccessControlSimulator.js'; @@ -147,14 +148,13 @@ describe('ShieldedAccessControl', () => { const circuitsToSucceed: UncheckedCircuits[] = [ ['_uncheckedCanProveRole', [UNINITIALIZED.role]], ['getRoleAdmin', [UNINITIALIZED.role]], - ['_uncheckedGrantRole', [UNINITIALIZED.role, UNINITIALIZED.accountId]], - ['_uncheckedRevokeRole', [UNINITIALIZED.role, UNINITIALIZED.accountId]], ['_setRoleAdmin', [UNINITIALIZED.role, UNINITIALIZED.role]], ['_computeAccountId', [UNINITIALIZED.role]], ['_computeRoleCommitment', [UNINITIALIZED.role, UNINITIALIZED.accountId]], ['_computeNullifier', [UNINITIALIZED.roleCommitment]], ['DEFAULT_ADMIN_ROLE', []], ['_validateRole', [UNINITIALIZED.roleCommitment]], + ['_updateRole', [UNINITIALIZED.roleCommitment, UNINITIALIZED.accountId, UpdateType.Grant]], ]; it.each(circuitsToSucceed)('%s should succeed', (circuitName, args) => { expect(() => { @@ -1659,212 +1659,6 @@ describe('ShieldedAccessControl', () => { }); }); - // TODO refactor to test _uncheckedGrantRole - describe.skip('_uncheckedGrantRole', () => { - describe('should return true', () => { - it('when authorized user grants a new role', () => { - shieldedAccessControl.as(ADMIN.publicKey); - expect( - shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId), - ).toBe(true); - }); - - it('when unauthorized user grants role', () => { - shieldedAccessControl.as(UNAUTHORIZED.publicKey); - expect( - shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId), - ).toBe(true); - }); - - it('when re-granting active role ', () => { - shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); - - expect( - shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId), - ).toBe(true); - }); - - it('when granting role that does not exist', () => { - expect( - shieldedAccessControl._grantRole( - UNINITIALIZED.role, - ADMIN.accountId, - ), - ).toBe(true); - }); - - it('when granting role with bad accountId', () => { - expect( - shieldedAccessControl._grantRole(ADMIN.role, BAD_INPUT.accountId), - ).toBe(true); - }); - }); - - describe('should update _operatorRoles merkle tree', () => { - it('when authorized user grants a new role', () => { - shieldedAccessControl.setPersistentCaller(ADMIN.publicKey); - // Check caller is admin, has admin role - expect( - shieldedAccessControl.getCallerContext().currentZswapLocalState - .coinPublicKey, - ).toEqual(ADMIN.zPublicKey); - expect(shieldedAccessControl.getRoleAdmin(ADMIN.role)).toEqual( - new Uint8Array(ADMIN.role), - ); - - // check merkle tree is empty - let merkleRoot = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.root(); - expect(merkleRoot.field).toBe(0n); - - // check merkle tree is updated - shieldedAccessControl.as(ADMIN.publicKey); - shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); - merkleRoot = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.root(); - expect(merkleRoot.field).not.toBe(0n); - - // check path exists for new role - const merkleTreePath = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.findPathForLeaf( - ADMIN.roleCommitment, - ); - expect(merkleTreePath).toBeDefined(); - expect(merkleTreePath?.leaf).toStrictEqual(ADMIN.roleCommitment); - }); - - it('when unauthorized user grants a new role', () => { - // Check UNAUTHORIZED is not admin - expect(shieldedAccessControl.getRoleAdmin(ADMIN.role)).not.toEqual( - new Uint8Array(UNAUTHORIZED.role), - ); - expect( - shieldedAccessControl._validateRole( - ADMIN.role, - UNAUTHORIZED.accountId, - ), - ).toBe(false); - - // check merkle tree is empty - let merkleRoot = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.root(); - expect(merkleRoot.field).toBe(0n); - - // check caller is UNAUTHORIZED user - shieldedAccessControl.as(UNAUTHORIZED.publicKey); - expect( - shieldedAccessControl.getCallerContext().currentZswapLocalState - .coinPublicKey, - ).toEqual(UNAUTHORIZED.zPublicKey); - - // check merkle tree is updated - shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); - merkleRoot = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.root(); - expect(merkleRoot.field).not.toBe(0n); - - // check path exists for new role - const merkleTreePath = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.findPathForLeaf( - ADMIN.roleCommitment, - ); - expect(merkleTreePath).toBeDefined(); - expect(merkleTreePath?.leaf).toStrictEqual(ADMIN.roleCommitment); - }); - - it('when granting role that does not exist', () => { - // check merkle tree is empty - let merkleRoot = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.root(); - expect(merkleRoot.field).toBe(0n); - - // check merkle tree is updated - shieldedAccessControl._grantRole( - UNINITIALIZED.role, - UNINITIALIZED.accountId, - ); - merkleRoot = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.root(); - expect(merkleRoot.field).not.toBe(0n); - - // check path exists for new role - const merkleTreePath = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.findPathForLeaf( - UNINITIALIZED.roleCommitment, - ); - expect(merkleTreePath).toBeDefined(); - expect(merkleTreePath?.leaf).toStrictEqual( - UNINITIALIZED.roleCommitment, - ); - }); - - it('when granting role with bad accountId', () => { - // check merkle tree is empty - let merkleRoot = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.root(); - expect(merkleRoot.field).toBe(0n); - - // check merkle tree is updated - shieldedAccessControl._grantRole(ADMIN.role, BAD_INPUT.accountId); - merkleRoot = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.root(); - expect(merkleRoot.field).not.toBe(0n); - - // check path exists for new role - const adminRoleBadAccountCommitment = buildRoleCommitmentHash( - ADMIN.role, - BAD_INPUT.accountId, - ); - const merkleTreePath = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.findPathForLeaf( - adminRoleBadAccountCommitment, - ); - expect(merkleTreePath).toBeDefined(); - expect(merkleTreePath?.leaf).toStrictEqual( - adminRoleBadAccountCommitment, - ); - }); - }); - - describe('should return false', () => { - it('when re-granting revoked role', () => { - shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); - shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); - expect( - shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId), - ).toBe(false); - }); - }); - - describe('should not update _operatorRoles merkle tree', () => { - it('when re-granting revoked role', () => { - shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); - shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); - const merkleRoot = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.root(); - - shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); - const newMerkleRoot = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.root(); - expect(merkleRoot).toEqual(newMerkleRoot); - }); - }); - }); - describe('revokeRole', () => { beforeEach(() => { shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); @@ -2556,369 +2350,6 @@ describe('ShieldedAccessControl', () => { }); }); - // TODO refactor to test _uncheckedGrantRole - describe.skip('_uncheckedRevokeRole', () => { - beforeEach(() => { - shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); - shieldedAccessControl.setPersistentCaller(ADMIN.publicKey); - }); - - describe('should return true', () => { - it('when active role is revoked', () => { - // confirm role is active - const isValidRole = shieldedAccessControl._validateRole( - ADMIN.role, - ADMIN.accountId, - ); - expect(isValidRole).toBe(true); - - expect( - shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId), - ).toBe(true); - }); - - it('when an authorized user revokes role', () => { - // Check caller is admin, has admin role - expect( - shieldedAccessControl.getCallerContext().currentZswapLocalState - .coinPublicKey, - ).toEqual(ADMIN.zPublicKey); - expect(shieldedAccessControl.getRoleAdmin(ADMIN.role)).toEqual( - new Uint8Array(ADMIN.role), - ); - expect( - shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), - ).toBe(true); - - expect( - shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId), - ).toBe(true); - }); - - it('when unauthorized user revokes role', () => { - // Check UNAUTHORIZED is not admin - expect(shieldedAccessControl.getRoleAdmin(ADMIN.role)).not.toEqual( - new Uint8Array(UNAUTHORIZED.role), - ); - expect( - shieldedAccessControl._validateRole( - ADMIN.role, - UNAUTHORIZED.accountId, - ), - ).toBe(false); - - // check caller is UNAUTHORIZED user - shieldedAccessControl.as(UNAUTHORIZED.publicKey); - expect( - shieldedAccessControl.getCallerContext().currentZswapLocalState - .coinPublicKey, - ).toEqual(UNAUTHORIZED.zPublicKey); - expect( - shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId), - ).toBe(true); - }); - - it('when revoking role that does not exist', () => { - // create role commitment that doesn't exist - const commitment = buildRoleCommitmentHash( - UNINITIALIZED.role, - ADMIN.accountId, - ); - - // confirm role commitment not in Merkle tree - const path = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.findPathForLeaf(commitment); - expect(path).toBeUndefined(); - - expect( - shieldedAccessControl._revokeRole( - UNINITIALIZED.role, - ADMIN.accountId, - ), - ).toBe(true); - }); - - it('when revoking role with bad accountId', () => { - expect( - shieldedAccessControl._revokeRole( - ADMIN.role, - BAD_INPUT.accountId, - ), - ).toBe(true); - }); - }); - - describe('should update nullifier set', () => { - it('when active role is revoked', () => { - // confirm role is active - const isValidRole = shieldedAccessControl._validateRole( - ADMIN.role, - ADMIN.accountId, - ); - expect(isValidRole).toBe(true); - - const initialSetSize = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.size(); - expect(initialSetSize).toBe(0n); - - shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); - - const updatedSetSize = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.size(); - expect(updatedSetSize).toBe(1n); - expect( - shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.member( - ADMIN.roleNullifier, - ), - ).toBe(true); - }); - - it('when an authorized user revokes role', () => { - // Check caller is admin, has admin role - expect( - shieldedAccessControl.getCallerContext().currentZswapLocalState - .coinPublicKey, - ).toEqual(ADMIN.zPublicKey); - expect(shieldedAccessControl.getRoleAdmin(ADMIN.role)).toEqual( - new Uint8Array(ADMIN.role), - ); - expect( - shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), - ).toBe(true); - - const initialSetSize = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.size(); - expect(initialSetSize).toBe(0n); - - shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); - - const updatedSetSize = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.size(); - expect(updatedSetSize).toBe(1n); - expect( - shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.member( - ADMIN.roleNullifier, - ), - ).toBe(true); - }); - - it('when unauthorized user revokes role', () => { - // Check UNAUTHORIZED is not admin - expect(shieldedAccessControl.getRoleAdmin(ADMIN.role)).not.toEqual( - new Uint8Array(UNAUTHORIZED.role), - ); - expect( - shieldedAccessControl._validateRole( - ADMIN.role, - UNAUTHORIZED.accountId, - ), - ).toBe(false); - - const initialSetSize = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.size(); - expect(initialSetSize).toBe(0n); - - // check caller is UNAUTHORIZED user - shieldedAccessControl.as(UNAUTHORIZED.publicKey); - expect( - shieldedAccessControl.getCallerContext().currentZswapLocalState - .coinPublicKey, - ).toEqual(UNAUTHORIZED.zPublicKey); - - shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); - - const updatedSetSize = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.size(); - expect(updatedSetSize).toBe(1n); - expect( - shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.member( - ADMIN.roleNullifier, - ), - ).toBe(true); - }); - - it('when revoking role that does not exist', () => { - // create role commitment that doesn't exist - const commitment = buildRoleCommitmentHash( - UNINITIALIZED.role, - ADMIN.accountId, - ); - - // confirm role commitment not in Merkle tree - const path = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.findPathForLeaf(commitment); - expect(path).toBeUndefined(); - - const initialSetSize = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.size(); - expect(initialSetSize).toBe(0n); - - shieldedAccessControl._revokeRole( - UNINITIALIZED.role, - ADMIN.accountId, - ); - - const updatedSetSize = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.size(); - expect(updatedSetSize).toBe(1n); - - const nullifier = buildNullifierHash(commitment); - - expect( - shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.member( - nullifier, - ), - ).toBe(true); - }); - - it('when revoking role with bad accountId', () => { - const initialSetSize = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.size(); - expect(initialSetSize).toBe(0n); - - shieldedAccessControl._revokeRole(ADMIN.role, BAD_INPUT.accountId); - - const updatedSetSize = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.size(); - expect(updatedSetSize).toBe(1n); - - const commitment = buildRoleCommitmentHash( - ADMIN.role, - BAD_INPUT.accountId, - ); - const nullifier = buildNullifierHash(commitment); - expect( - shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.member( - nullifier, - ), - ).toBe(true); - }); - }); - - describe('should return false', () => { - it('when authorized user re-revokes role', () => { - // Check caller is admin, has admin role - expect( - shieldedAccessControl.getCallerContext().currentZswapLocalState - .coinPublicKey, - ).toEqual(ADMIN.zPublicKey); - expect(shieldedAccessControl.getRoleAdmin(ADMIN.role)).toEqual( - new Uint8Array(ADMIN.role), - ); - expect( - shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), - ).toBe(true); - - shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); - expect( - shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId), - ).toBe(false); - }); - - it('when unauthorized user re-revokes role', () => { - // Check UNAUTHORIZED is not admin - expect(shieldedAccessControl.getRoleAdmin(ADMIN.role)).not.toEqual( - new Uint8Array(UNAUTHORIZED.role), - ); - expect( - shieldedAccessControl._validateRole( - ADMIN.role, - UNAUTHORIZED.accountId, - ), - ).toBe(false); - - // revoke as ADMIN - shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); - - // check caller is UNAUTHORIZED user - shieldedAccessControl.as(UNAUTHORIZED.publicKey); - expect( - shieldedAccessControl.getCallerContext().currentZswapLocalState - .coinPublicKey, - ).toEqual(UNAUTHORIZED.zPublicKey); - expect( - shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId), - ).toBe(false); - }); - }); - - describe('should not update nullifier set', () => { - it('when authorized user re-revokes role', () => { - shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); - const initialSetSize = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.size(); - expect(initialSetSize).toBe(1n); - - // Check caller is admin, doesn't have admin role - expect( - shieldedAccessControl.getCallerContext().currentZswapLocalState - .coinPublicKey, - ).toEqual(ADMIN.zPublicKey); - expect( - shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), - ).toBe(false); - - shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); - - const updatedSetSize = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.size(); - expect(updatedSetSize).toEqual(initialSetSize); - }); - - it('when unauthorized user re-revokes role', () => { - shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); - const initialSetSize = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.size(); - expect(initialSetSize).toBe(1n); - - // Check UNAUTHORIZED is not admin - expect(shieldedAccessControl.getRoleAdmin(ADMIN.role)).not.toEqual( - new Uint8Array(UNAUTHORIZED.role), - ); - expect( - shieldedAccessControl._validateRole( - ADMIN.role, - UNAUTHORIZED.accountId, - ), - ).toBe(false); - - // re-revoke as UNAUTHORIZED - shieldedAccessControl.as(UNAUTHORIZED.publicKey); - shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); - - const updatedSetSize = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.size(); - expect(updatedSetSize).toEqual(initialSetSize); - }); - }); - }); - describe('canProveRole', () => { beforeEach(() => { shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); diff --git a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact index 16de18e7..778c0faf 100644 --- a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact +++ b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact @@ -21,7 +21,8 @@ export { ZswapCoinPublicKey, ShieldedAccessControl_DEFAULT_ADMIN_ROLE, ShieldedAccessControl__operatorRoles, ShieldedAccessControl__roleCommitmentNullifiers, - ShieldedAccessControl__adminRoles }; + ShieldedAccessControl__adminRoles, + ShieldedAccessControl_UpdateType }; // witnesses are re-implemented in the Mock contract for testing witness wit_getRoleCommitmentPath( @@ -45,12 +46,12 @@ constructor(instanceSalt: Bytes<32>, isInit: Boolean) { // circuit is reimplemented in the Mock contract for testing export circuit _computeRoleCommitment( - roleId: ShieldedAccessControl_RoleIdentifier, + role: ShieldedAccessControl_RoleIdentifier, accountId: ShieldedAccessControl_AccountIdentifier ): ShieldedAccessControl_RoleCommitment { return persistentHash>>( - [roleId as Bytes<32>, + [role as Bytes<32>, accountId as Bytes<32>, ShieldedAccessControl__instanceSalt, pad(32, "ShieldedAccessControl:commitment")] @@ -87,17 +88,18 @@ export pure circuit _computeNullifier( as ShieldedAccessControl_RoleNullifier; } -export circuit canProveRole(roleId: ShieldedAccessControl_RoleIdentifier): Boolean { - return ShieldedAccessControl_canProveRole(roleId); +export circuit canProveRole(role: ShieldedAccessControl_RoleIdentifier): Boolean { + return ShieldedAccessControl_canProveRole(role); } +// _uncheckCanProveRole is re-implemented in the Mock contract for testing export circuit _uncheckedCanProveRole(role: ShieldedAccessControl_RoleIdentifier): Boolean { const accountId = _computeAccountId(role); return _validateRole(role, accountId); } -export circuit assertOnlyRole(roleId: ShieldedAccessControl_RoleIdentifier): [] { - ShieldedAccessControl_assertOnlyRole(roleId); +export circuit assertOnlyRole(role: ShieldedAccessControl_RoleIdentifier): [] { + ShieldedAccessControl_assertOnlyRole(role); } // _validateRole is re-implemented in the Mock contract for testing @@ -128,81 +130,73 @@ export circuit _validateRole( } export circuit getRoleAdmin( - roleId: ShieldedAccessControl_RoleIdentifier + role: ShieldedAccessControl_RoleIdentifier ): ShieldedAccessControl_RoleIdentifier { - return ShieldedAccessControl_getRoleAdmin(roleId); + return ShieldedAccessControl_getRoleAdmin(role); } export circuit grantRole( - roleId: ShieldedAccessControl_RoleIdentifier, - accountId: ShieldedAccessControl_AccountIdentifier - ): [] { - ShieldedAccessControl_grantRole(roleId, accountId); -} - -export circuit _uncheckedGrantRole( role: ShieldedAccessControl_RoleIdentifier, accountId: ShieldedAccessControl_AccountIdentifier - ): Boolean { - const roleCommitment = _computeRoleCommitment(role, accountId); - const roleNullifier = _computeNullifier(roleCommitment); - const isRevoked = ShieldedAccessControl__roleCommitmentNullifiers.member(disclose(roleNullifier)); - - if (isRevoked) { - return false; - } - - ShieldedAccessControl__operatorRoles.insert(disclose(roleCommitment)); - return true; -} - -export circuit revokeRole( - roleId: ShieldedAccessControl_RoleIdentifier, - accountId: ShieldedAccessControl_AccountIdentifier ): [] { - ShieldedAccessControl_revokeRole(roleId, accountId); + ShieldedAccessControl_grantRole(role, accountId); } -export circuit _uncheckedRevokeRole( +export circuit revokeRole( role: ShieldedAccessControl_RoleIdentifier, accountId: ShieldedAccessControl_AccountIdentifier - ): Boolean { - const roleCommitment = _computeRoleCommitment(role, accountId); - const roleNullifier = _computeNullifier(roleCommitment); - const isRevoked = ShieldedAccessControl__roleCommitmentNullifiers.member(disclose(roleNullifier)); - - if (isRevoked) { - return false; - } - - ShieldedAccessControl__roleCommitmentNullifiers.insert(disclose(roleNullifier)); - return true; + ): [] { + ShieldedAccessControl_revokeRole(role, accountId); } export circuit renounceRole( - roleId: ShieldedAccessControl_RoleIdentifier, + role: ShieldedAccessControl_RoleIdentifier, accountIdConfirmation: ShieldedAccessControl_AccountIdentifier ): [] { - ShieldedAccessControl_renounceRole(roleId, accountIdConfirmation); + ShieldedAccessControl_renounceRole(role, accountIdConfirmation); } export circuit _setRoleAdmin( - roleId: ShieldedAccessControl_RoleIdentifier, + role: ShieldedAccessControl_RoleIdentifier, adminRole: ShieldedAccessControl_RoleIdentifier ): [] { - ShieldedAccessControl__setRoleAdmin(roleId, adminRole); + ShieldedAccessControl__setRoleAdmin(role, adminRole); } export circuit _grantRole( - roleId: ShieldedAccessControl_RoleIdentifier, + role: ShieldedAccessControl_RoleIdentifier, accountId: ShieldedAccessControl_AccountIdentifier ): Boolean { - return ShieldedAccessControl__grantRole(roleId, accountId); + return ShieldedAccessControl__grantRole(role, accountId); } export circuit _revokeRole( - roleId: ShieldedAccessControl_RoleIdentifier, + role: ShieldedAccessControl_RoleIdentifier, accountId: ShieldedAccessControl_AccountIdentifier ): Boolean { - return ShieldedAccessControl__revokeRole(roleId, accountId); + return ShieldedAccessControl__revokeRole(role, accountId); +} + +// _updateRole is re-implemented in the Mock contract for testing +export circuit _updateRole( + role: ShieldedAccessControl_RoleIdentifier, + accountId: ShieldedAccessControl_AccountIdentifier, + updateType: ShieldedAccessControl_UpdateType + ): Boolean { + const roleCommitment = _computeRoleCommitment(role, accountId); + const roleNullifier = _computeNullifier(roleCommitment); + const isRevoked = ShieldedAccessControl__roleCommitmentNullifiers.member(disclose(roleNullifier)); + + if (isRevoked) { + return false; + } + + // disclosure only necessary here because we're exporting the circuit for testing + if (disclose(updateType) == ShieldedAccessControl_UpdateType.Grant) { + ShieldedAccessControl__operatorRoles.insert(disclose(roleCommitment)); + } else { + ShieldedAccessControl__roleCommitmentNullifiers.insert(disclose(roleNullifier)); + } + + return true; } diff --git a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts index 6132ba54..a9506612 100644 --- a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts +++ b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts @@ -6,6 +6,7 @@ import { import { ledger, Contract as MockShieldedAccessControl, + type ShieldedAccessControl_UpdateType as UpdateType, type ZswapCoinPublicKey, } from '../../../../artifacts/MockShieldedAccessControl/contract/index.js'; import { @@ -98,16 +99,12 @@ export class ShieldedAccessControlSimulator extends ShieldedAccessControlSimulat this.circuits.impure.grantRole(role, accountId); } - public _uncheckedGrantRole(role: Uint8Array, accountId: Uint8Array) { - this.circuits.impure._uncheckedGrantRole(role, accountId); - } - public revokeRole(role: Uint8Array, accountId: Uint8Array) { this.circuits.impure.revokeRole(role, accountId); } - public _uncheckedRevokeRole(role: Uint8Array, accountId: Uint8Array) { - this.circuits.impure._uncheckedRevokeRole(role, accountId); + public _updateRole(role: Uint8Array, accountId: Uint8Array, updateType: UpdateType) { + return this.circuits.impure._updateRole(role, accountId, updateType); } public renounceRole(role: Uint8Array, callerConfirmation: Uint8Array) { From bc1ded29c58fa3a4e6b3bec1cc3218d903545ef4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Tue, 17 Mar 2026 19:18:44 -0400 Subject: [PATCH 307/322] Use generic witness pattern --- .../ShieldedAccessControlSimulator.ts | 5 +-- .../ShieldedAccessControlWitnesses.ts | 33 +++++++++---------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts index a9506612..b4789d7c 100644 --- a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts +++ b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts @@ -7,13 +7,14 @@ import { ledger, Contract as MockShieldedAccessControl, type ShieldedAccessControl_UpdateType as UpdateType, - type ZswapCoinPublicKey, } from '../../../../artifacts/MockShieldedAccessControl/contract/index.js'; import { ShieldedAccessControlPrivateState, ShieldedAccessControlWitnesses, } from '../../witnesses/ShieldedAccessControlWitnesses.js'; +type ShieldedAccessControlLedger = ReturnType; + /** * Type constructor args */ @@ -36,7 +37,7 @@ const ShieldedAccessControlSimulatorBase = createSimulator< return [instanceSalt, isInit]; }, ledgerExtractor: (state) => ledger(state), - witnessesFactory: () => ShieldedAccessControlWitnesses(), + witnessesFactory: () => ShieldedAccessControlWitnesses(), }); /** diff --git a/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts b/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts index dc7b315b..8ffde118 100644 --- a/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts +++ b/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts @@ -1,26 +1,24 @@ import { getRandomValues } from 'node:crypto'; -import type { WitnessContext } from '@midnight-ntwrk/compact-runtime'; -import type { - Ledger, - MerkleTreePath, -} from '../../../artifacts/MockShieldedAccessControl/contract/index.js'; +import type { WitnessContext, MerkleTreePath } from '@midnight-ntwrk/compact-runtime'; + /** - * @description Interface defining the witness methods for ShieldedAccessControl operations. + * @description Interface defining the witness methods for ShieldedAccessControl operations + * @template L - The ledger type. * @template P - The private state type. */ -export interface IShieldedAccessControlWitnesses

{ +export interface IShieldedAccessControlWitnesses { /** * Retrieves the secret nonce from the private state. * @param context - The witness context containing the private state. * @returns A tuple of the private state and the secret nonce as a Uint8Array. */ wit_secretNonce( - context: WitnessContext, + context: WitnessContext, role: Uint8Array, ): [P, Uint8Array]; wit_getRoleCommitmentPath( - context: WitnessContext, + context: WitnessContext, roleCommitment: Uint8Array, ): [P, MerkleTreePath]; } @@ -93,15 +91,16 @@ export const ShieldedAccessControlPrivateState = { return { roles } }, - getRoleCommitmentPath: ( - ledger: Ledger, + getRoleCommitmentPath: ( + ledger: L, roleCommitment: Uint8Array, ): MerkleTreePath => { const path = - ledger.ShieldedAccessControl__operatorRoles.findPathForLeaf( + // cast ledger as any to avoid type gymnastics + (ledger as any).ShieldedAccessControl__operatorRoles.findPathForLeaf( roleCommitment, ); - const defaultPath: MerkleTreePath = { + const defaultPath = { leaf: new Uint8Array(32), path: Array.from({ length: 20 }, () => ({ sibling: { field: 0n }, @@ -117,9 +116,9 @@ export const ShieldedAccessControlPrivateState = { * @returns An object implementing the Witnesses interface for ShieldedAccessControlPrivateState. */ export const ShieldedAccessControlWitnesses = - (): IShieldedAccessControlWitnesses => ({ + (): IShieldedAccessControlWitnesses => ({ wit_secretNonce( - context: WitnessContext, + context: WitnessContext, role: Uint8Array, ): [ShieldedAccessControlPrivateState, Uint8Array] { const roleString = Buffer.from(role).toString('hex'); @@ -130,12 +129,12 @@ export const ShieldedAccessControlWitnesses = return [context.privateState, roleNonce]; }, wit_getRoleCommitmentPath( - context: WitnessContext, + context: WitnessContext, roleCommitment: Uint8Array, ): [ShieldedAccessControlPrivateState, MerkleTreePath] { return [ context.privateState, - ShieldedAccessControlPrivateState.getRoleCommitmentPath( + ShieldedAccessControlPrivateState.getRoleCommitmentPath( context.ledger, roleCommitment, ), From 748ee624d4bf6e9436bfb900924819e91c012b43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Tue, 17 Mar 2026 21:17:02 -0400 Subject: [PATCH 308/322] Add interface to computeAccountId locally, export _computeNullifier and _computeRoleCommitment circuits --- .../src/access/ShieldedAccessControl.compact | 46 ++++++++++++++++++- .../mocks/MockShieldedAccessControl.compact | 40 ++++++++-------- 2 files changed, 63 insertions(+), 23 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index 61c620eb..e24b725e 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -639,7 +639,7 @@ module ShieldedAccessControl { * * @returns {RoleCommitment} The commitment derived from `accountId` and `role`. */ - circuit _computeRoleCommitment( + export circuit _computeRoleCommitment( role: RoleIdentifier, accountId: AccountIdentifier, ): RoleCommitment { @@ -667,7 +667,7 @@ module ShieldedAccessControl { * * @returns {RoleNullifier} roleNullifier - The associated nullifier for `roleCommitment`. */ - pure circuit _computeNullifier(roleCommitment: RoleCommitment): RoleNullifier { + export pure circuit _computeNullifier(roleCommitment: RoleCommitment): RoleNullifier { return persistentHash>>( [roleCommitment as Bytes<32>, pad(32, "ShieldedAccessControl:nullifier")] ) @@ -712,4 +712,46 @@ module ShieldedAccessControl { ) as AccountIdentifier; } + + /** + * @description Computes an `accountId` locally without on-chain state, allowing a user to derive + * their shielded identity commitment before submitting it in a grant or revoke operation. + * This is the off-chain counterpart to {_computeAccountId} and produces an identical result + * given the same inputs. + * + * @warning OpSec: The `secretNonce` parameter is a sensitive secret. Mishandling it can + * permanently compromise the privacy guarantees of this system: + * + * - **Never log or persist** the `secretNonce` in plaintext — avoid browser devtools, + * application logs, analytics pipelines, or any observable side-channel. + * - **Store offline or in secure enclaves** — hardware security modules (HSMs), + * air-gapped devices, or encrypted vaults are strongly preferred over hot storage. + * - **Never reuse a nonce across roles** — reuse may allow an observer to correlate your + * identity across different role commitments, weakening privacy. + * - **Use cryptographically secure randomness** — generate nonces with `crypto.getRandomValues()` + * or equivalent; weak or predictable nonces can be brute-forced to reveal your identity. + * - **Treat nonce loss as identity loss** — a lost nonce cannot be recovered. Back up + * nonces securely before using them in role commitments. + * - **Avoid calling this circuit in untrusted environments** — executing this in an + * unverified browser extension, compromised runtime, or shared machine may expose + * the nonce to a malicious observer. + * + * ## ID Derivation + * See {_computeAccountId} for further details. + * + * @param {ZswapCoinPublicKey} account - The user's ZswapCoinPublicKey + * @param {Bytes<32>} secretNonce - A private nonce scoped to a particular role. + * @param {Bytes<32>} instanceSalt - The unique per-deployment salt for the contract instance. + * + * @returns {AccountIdentifier} accountId - The computed account ID. + */ + export pure circuit _computeAccountIdLocally(account: ZswapCoinPublicKey, secretNonce: Bytes<32>, instanceSalt: Bytes<32>): AccountIdentifier { + return persistentHash>>( + [account.bytes, + secretNonce, + instanceSalt, + pad(32, "ShieldedAccessControl:accountId")] + ) + as AccountIdentifier; + } } diff --git a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact index 778c0faf..09e963b9 100644 --- a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact +++ b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact @@ -44,19 +44,12 @@ constructor(instanceSalt: Bytes<32>, isInit: Boolean) { } } -// circuit is reimplemented in the Mock contract for testing export circuit _computeRoleCommitment( role: ShieldedAccessControl_RoleIdentifier, accountId: ShieldedAccessControl_AccountIdentifier ): ShieldedAccessControl_RoleCommitment { - return persistentHash>>( - [role as Bytes<32>, - accountId as Bytes<32>, - ShieldedAccessControl__instanceSalt, - pad(32, "ShieldedAccessControl:commitment")] - ) - as ShieldedAccessControl_RoleCommitment; + return ShieldedAccessControl__computeRoleCommitment(role, accountId); } export pure circuit DEFAULT_ADMIN_ROLE(): ShieldedAccessControl_RoleIdentifier { @@ -68,24 +61,29 @@ export circuit _computeAccountId( role: ShieldedAccessControl_RoleIdentifier ): ShieldedAccessControl_AccountIdentifier { - // disclosure required here for testing - return disclose(persistentHash>>( - [ownPublicKey().bytes, - wit_secretNonce(role), - ShieldedAccessControl__instanceSalt, - pad(32, "ShieldedAccessControl:accountId")] - )) - as ShieldedAccessControl_AccountIdentifier; + // value must be disclosed for testing + return disclose(persistentHash>>( + [ownPublicKey().bytes, + wit_secretNonce(role), + ShieldedAccessControl__instanceSalt, + pad(32, "ShieldedAccessControl:accountId")] + )) + as ShieldedAccessControl_AccountIdentifier; +} + +export pure circuit _computeAccountIdLocally( + account: ZswapCoinPublicKey, + secretNonce: Bytes<32>, + instanceSalt: Bytes<32> + ): ShieldedAccessControl_AccountIdentifier { + + return ShieldedAccessControl__computeAccountIdLocally(account, secretNonce, instanceSalt); } -// circuit is reimplemented in the Mock contract for testing export pure circuit _computeNullifier( roleCommitment: ShieldedAccessControl_RoleCommitment ): ShieldedAccessControl_RoleNullifier { - return persistentHash>>( - [roleCommitment as Bytes<32>, pad(32, "ShieldedAccessControl:nullifier")] - ) - as ShieldedAccessControl_RoleNullifier; + return ShieldedAccessControl__computeNullifier(roleCommitment); } export circuit canProveRole(role: ShieldedAccessControl_RoleIdentifier): Boolean { From a7566b1aa51c64c085a7f1b525ecfd7da2a61f78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Tue, 17 Mar 2026 21:38:34 -0400 Subject: [PATCH 309/322] Update tests and simulator --- .../access/test/ShieldedAccessControl.test.ts | 520 ++++++------------ .../ShieldedAccessControlSimulator.ts | 9 + 2 files changed, 190 insertions(+), 339 deletions(-) diff --git a/contracts/src/access/test/ShieldedAccessControl.test.ts b/contracts/src/access/test/ShieldedAccessControl.test.ts index 1f1f95ba..f4392390 100644 --- a/contracts/src/access/test/ShieldedAccessControl.test.ts +++ b/contracts/src/access/test/ShieldedAccessControl.test.ts @@ -155,6 +155,7 @@ describe('ShieldedAccessControl', () => { ['DEFAULT_ADMIN_ROLE', []], ['_validateRole', [UNINITIALIZED.roleCommitment]], ['_updateRole', [UNINITIALIZED.roleCommitment, UNINITIALIZED.accountId, UpdateType.Grant]], + ['_computeAccountIdLocally', [UNINITIALIZED.zPublicKey, UNINITIALIZED.secretNonce, UNINITIALIZED.role]], ]; it.each(circuitsToSucceed)('%s should succeed', (circuitName, args) => { expect(() => { @@ -275,6 +276,38 @@ describe('ShieldedAccessControl', () => { }); }); + describe('_computeAccountIdLocally', () => { + it('should match when given correct account and nonce', () => { + expect( + shieldedAccessControl._computeAccountIdLocally( + ADMIN.zPublicKey, + ADMIN.secretNonce, + INSTANCE_SALT, + ), + ).toEqual(ADMIN.accountId); + }); + + it('should not match when given correct account with bad nonce', () => { + const computedAccountId = shieldedAccessControl._computeAccountIdLocally( + ADMIN.zPublicKey, + BAD_INPUT.secretNonce, + INSTANCE_SALT, + ); + expect(computedAccountId).not.toEqual(ADMIN.accountId); + expect(computedAccountId).toEqual(buildAccountIdHash(ADMIN.zPublicKey, BAD_INPUT.secretNonce)); + }); + + it('should not match when given unauthorized account with correct nonce', () => { + const computedAccountId = shieldedAccessControl._computeAccountIdLocally( + UNAUTHORIZED.zPublicKey, + ADMIN.secretNonce, + INSTANCE_SALT, + ); + expect(computedAccountId).not.toEqual(ADMIN.accountId); + expect(computedAccountId).toEqual(buildAccountIdHash(UNAUTHORIZED.zPublicKey, ADMIN.secretNonce)); + }); + }); + describe('_validateRole', () => { beforeEach(() => { shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); @@ -820,327 +853,6 @@ describe('ShieldedAccessControl', () => { }); }); - // TODO refactor to test _uncheckAssertOnlyRole - describe.skip('_uncheckAssertOnlyRole', () => { - beforeEach(() => { - shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); - shieldedAccessControl.setPersistentCaller(ADMIN.publicKey); - }); - - describe('should fail', () => { - it('when wit_getRoleCommitmentPath returns a valid path for a different role, accountId pairing', () => { - shieldedAccessControl._grantRole( - OPERATOR_1.role, - OPERATOR_1.accountId, - ); - // Override witness to return valid path for OPERATOR_1 role commitment - shieldedAccessControl.overrideWitness( - 'wit_getRoleCommitmentPath', - () => { - const privateState = shieldedAccessControl.getPrivateState(); - const operator1MtPath = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.findPathForLeaf( - OPERATOR_1.roleCommitment, - ); - if (operator1MtPath) return [privateState, operator1MtPath]; - throw new Error('Merkle tree path should be defined'); - }, - ); - expect(() => { - shieldedAccessControl.assertOnlyRole(ADMIN.role); - }).toThrow( - 'ShieldedAccessControl: Path must contain leaf matching computed role commitment for the provided role, accountId pairing', - ); - }); - - it('when caller was never granted the role', () => { - shieldedAccessControl.as(UNAUTHORIZED.publicKey); - expect(() => - shieldedAccessControl.assertOnlyRole(ADMIN.role), - ).toThrow('ShieldedAccessControl: unauthorized account'); - }); - - it('when authorized caller has incorrect path', () => { - // Check caller is admin, has admin role - expect( - shieldedAccessControl.getCallerContext().currentZswapLocalState - .coinPublicKey, - ).toEqual(ADMIN.zPublicKey); - expect(shieldedAccessControl.getRoleAdmin(ADMIN.role)).toEqual( - new Uint8Array(ADMIN.role), - ); - expect( - shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), - ).toBe(true); - - // Check nonce is correct - expect( - shieldedAccessControl.privateState.getCurrentSecretNonce( - ADMIN.role, - ), - ).toBe(ADMIN.secretNonce); - - // Check path does not match - const truePath = - shieldedAccessControl.privateState.getCommitmentPathWithFindForLeaf( - ADMIN.roleCommitment, - ); - shieldedAccessControl.overrideWitness( - 'wit_getRoleCommitmentPath', - RETURN_BAD_PATH, - ); - const witnessCalculatedPath = - shieldedAccessControl.privateState.getCommitmentPathWithWitnessImpl( - ADMIN.roleCommitment, - ); - expect(witnessCalculatedPath).not.toEqual(truePath); - - expect(() => - shieldedAccessControl.assertOnlyRole(ADMIN.role), - ).toThrow('ShieldedAccessControl: unauthorized account'); - }); - - it('when authorized caller has incorrect nonce', () => { - // Check caller is admin, has admin role - expect( - shieldedAccessControl.getCallerContext().currentZswapLocalState - .coinPublicKey, - ).toEqual(ADMIN.zPublicKey); - expect(shieldedAccessControl.getRoleAdmin(ADMIN.role)).toEqual( - new Uint8Array(ADMIN.role), - ); - expect( - shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), - ).toBe(true); - - shieldedAccessControl.privateState.injectSecretNonce( - ADMIN.role, - UNAUTHORIZED.secretNonce, - ); - - // Check nonce is incorrect - expect( - shieldedAccessControl.privateState.getCurrentSecretNonce( - ADMIN.role, - ), - ).not.toBe(ADMIN.secretNonce); - - // Check path matches - const truePath = - shieldedAccessControl.privateState.getCommitmentPathWithFindForLeaf( - ADMIN.roleCommitment, - ); - const witnessCalculatedPath = - shieldedAccessControl.privateState.getCommitmentPathWithWitnessImpl( - ADMIN.roleCommitment, - ); - expect(witnessCalculatedPath).toEqual(truePath); - - expect(() => - shieldedAccessControl.assertOnlyRole(ADMIN.role), - ).toThrow('ShieldedAccessControl: unauthorized account'); - }); - - it('when unauthorized caller has correct nonce, and path', () => { - // Check UNAUTHORIZED user is not admin, doesnt have admin role - expect(shieldedAccessControl.getRoleAdmin(ADMIN.role)).not.toEqual( - new Uint8Array(UNAUTHORIZED.role), - ); - expect( - shieldedAccessControl._validateRole( - ADMIN.role, - UNAUTHORIZED.accountId, - ), - ).toBe(false); - - // Check nonce is correct - expect( - shieldedAccessControl.privateState.getCurrentSecretNonce( - ADMIN.role, - ), - ).toBe(ADMIN.secretNonce); - - // Check path matches - const truePath = - shieldedAccessControl.privateState.getCommitmentPathWithFindForLeaf( - ADMIN.roleCommitment, - ); - const witnessCalculatedPath = - shieldedAccessControl.privateState.getCommitmentPathWithWitnessImpl( - ADMIN.roleCommitment, - ); - expect(witnessCalculatedPath).toEqual(truePath); - - shieldedAccessControl.as(UNAUTHORIZED.publicKey); - // Check caller is UNAUTHORIZED user - expect( - shieldedAccessControl.getCallerContext().currentZswapLocalState - .coinPublicKey, - ).toEqual(UNAUTHORIZED.zPublicKey); - - expect(() => - shieldedAccessControl.assertOnlyRole(ADMIN.role), - ).toThrow('ShieldedAccessControl: unauthorized account'); - }); - - it('when role is revoked', () => { - shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); - expect(() => - shieldedAccessControl.assertOnlyRole(ADMIN.role), - ).toThrow('ShieldedAccessControl: unauthorized account'); - }); - - it('when role is revoked and re-issued to the same accountId', () => { - shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); - shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); - expect(() => - shieldedAccessControl.assertOnlyRole(ADMIN.role), - ).toThrow('ShieldedAccessControl: unauthorized account'); - }); - }); - - describe('should not fail', () => { - it('when accountId has multiple roles', () => { - shieldedAccessControl.privateState.injectSecretNonce( - OPERATOR_1.role, - OPERATOR_1.secretNonce, - ); - // A unique accountId must be constructed for each new role using its associated secretNonce - const operator1AccountId = buildAccountIdHash( - ADMIN.zPublicKey, - OPERATOR_1.secretNonce, - ); - - shieldedAccessControl.privateState.injectSecretNonce( - OPERATOR_2.role, - OPERATOR_2.secretNonce, - ); - const operator2AccountId = buildAccountIdHash( - ADMIN.zPublicKey, - OPERATOR_2.secretNonce, - ); - - shieldedAccessControl.privateState.injectSecretNonce( - OPERATOR_3.role, - OPERATOR_3.secretNonce, - ); - const operator3AccountId = buildAccountIdHash( - ADMIN.zPublicKey, - OPERATOR_3.secretNonce, - ); - - shieldedAccessControl._grantRole( - OPERATOR_1.role, - operator1AccountId, - ); - shieldedAccessControl._grantRole( - OPERATOR_2.role, - operator2AccountId, - ); - shieldedAccessControl._grantRole( - OPERATOR_3.role, - operator3AccountId, - ); - expect(() => { - shieldedAccessControl.assertOnlyRole(ADMIN.role); - shieldedAccessControl.assertOnlyRole(OPERATOR_1.role); - shieldedAccessControl.assertOnlyRole(OPERATOR_2.role); - shieldedAccessControl.assertOnlyRole(OPERATOR_3.role); - }).not.toThrow(); - }); - - it('when authorized caller has correct nonce, and path', () => { - // Check nonce is correct - expect( - shieldedAccessControl.privateState.getCurrentSecretNonce( - ADMIN.role, - ), - ).toBe(ADMIN.secretNonce); - - // Check path matches - const truePath = - shieldedAccessControl.privateState.getCommitmentPathWithFindForLeaf( - ADMIN.roleCommitment, - ); - const witnessCalculatedPath = - shieldedAccessControl.privateState.getCommitmentPathWithWitnessImpl( - ADMIN.roleCommitment, - ); - expect(witnessCalculatedPath).toEqual(truePath); - - expect(() => - shieldedAccessControl.assertOnlyRole(ADMIN.role), - ).not.toThrow(); - }); - - it('when role is revoked and re-issued with a different accountId', () => { - shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); - - shieldedAccessControl.privateState.injectSecretNonce( - ADMIN.role, - Buffer.alloc(32, 'NEW_ADMIN_NONCE'), - ); - const newAdminAccountId = buildAccountIdHash( - ADMIN.zPublicKey, - shieldedAccessControl.privateState.getCurrentSecretNonce( - ADMIN.role, - ), - ); - expect(newAdminAccountId).not.toEqual(ADMIN.accountId); - - shieldedAccessControl._grantRole(ADMIN.role, newAdminAccountId); - expect(() => - shieldedAccessControl.assertOnlyRole( - ADMIN.role, - ) - ).not.toThrow(); - }); - - it('when multiple users have the same role', () => { - // All users will use OPERATOR_1.secretNonce as their nonce value - // when generating their accountId for simplicity - shieldedAccessControl.privateState.injectSecretNonce( - OPERATOR_1.role, - OPERATOR_1.secretNonce, - ); - // A unique accountId must be constructed for each new role using its associated secretNonce - const operator1AdminAccountId = buildAccountIdHash( - ADMIN.zPublicKey, - OPERATOR_1.secretNonce, - ); - shieldedAccessControl._grantRole( - OPERATOR_1.role, - operator1AdminAccountId, - ); - shieldedAccessControl.as(ADMIN.publicKey); // assert ADMIN has OP_1 role - expect(shieldedAccessControl.assertOnlyRole(OPERATOR_1.role)); - - const operator1Op2AccountId = buildAccountIdHash( - OPERATOR_2.zPublicKey, - OPERATOR_1.secretNonce, - ); - shieldedAccessControl._grantRole( - OPERATOR_1.role, - operator1Op2AccountId, - ); - shieldedAccessControl.as(OPERATOR_2.publicKey); // assert OP_2 has OP_1 role - expect(shieldedAccessControl.assertOnlyRole(OPERATOR_1.role)); - - const operator1Op3AccountId = buildAccountIdHash( - OPERATOR_3.zPublicKey, - OPERATOR_1.secretNonce, - ); - shieldedAccessControl._grantRole( - OPERATOR_1.role, - operator1Op3AccountId, - ); - shieldedAccessControl.as(OPERATOR_3.publicKey); // assert OP_3 has OP_1 role - expect(shieldedAccessControl.assertOnlyRole(OPERATOR_1.role)); - }); - }); - }); - describe('grantRole', () => { beforeEach(() => { shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); @@ -2350,6 +2062,137 @@ describe('ShieldedAccessControl', () => { }); }); + describe('_updateRole', () => { + beforeEach(() => { + shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); + }); + + describe(`UpdateType.Grant`, () => { + describe('should return true', () => { + it('when granting a new role', () => { + expect( + shieldedAccessControl._updateRole(OPERATOR_1.role, OPERATOR_1.accountId, UpdateType.Grant), + ).toBe(true); + }); + + it('when re-granting an active role', () => { + expect( + shieldedAccessControl._updateRole(ADMIN.role, ADMIN.accountId, UpdateType.Grant), + ).toBe(true); + }); + }); + + describe('should update _operatorRoles merkle tree', () => { + it('when granting a new role', () => { + const initialRoot = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.root(); + + shieldedAccessControl._updateRole(OPERATOR_1.role, OPERATOR_1.accountId, UpdateType.Grant); + + const updatedRoot = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.root(); + expect(updatedRoot).not.toEqual(initialRoot); + + const path = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf(OPERATOR_1.roleCommitment); + expect(path).toBeDefined(); + expect(path?.leaf).toStrictEqual(OPERATOR_1.roleCommitment); + }); + }); + + describe('should return false', () => { + it('when granting a revoked role', () => { + shieldedAccessControl._updateRole(ADMIN.role, ADMIN.accountId, UpdateType.Revoke); + expect( + shieldedAccessControl._updateRole(ADMIN.role, ADMIN.accountId, UpdateType.Grant), + ).toBe(false); + }); + }); + + describe('should not update _operatorRoles merkle tree', () => { + it('when granting a revoked role', () => { + shieldedAccessControl._updateRole(ADMIN.role, ADMIN.accountId, UpdateType.Revoke); + const merkleRoot = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.root(); + + shieldedAccessControl._updateRole(ADMIN.role, ADMIN.accountId, UpdateType.Grant); + + const newMerkleRoot = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.root(); + expect(merkleRoot).toEqual(newMerkleRoot); + }); + }); + }); + + describe('UpdateType.Revoke', () => { + describe('should return true', () => { + it('when revoking an active role', () => { + expect( + shieldedAccessControl._updateRole(ADMIN.role, ADMIN.accountId, UpdateType.Revoke), + ).toBe(true); + }); + + it('when revoking a role that does not exist', () => { + expect( + shieldedAccessControl._updateRole(UNINITIALIZED.role, UNINITIALIZED.accountId, UpdateType.Revoke), + ).toBe(true); + }); + }); + + describe('should update nullifier set', () => { + it('when revoking an active role', () => { + const initialSetSize = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.size(); + expect(initialSetSize).toBe(0n); + + shieldedAccessControl._updateRole(ADMIN.role, ADMIN.accountId, UpdateType.Revoke); + + const updatedSetSize = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.size(); + expect(updatedSetSize).toBe(1n); + expect( + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.member(ADMIN.roleNullifier), + ).toBe(true); + }); + }); + + describe('should return false', () => { + it('when re-revoking an already revoked role', () => { + shieldedAccessControl._updateRole(ADMIN.role, ADMIN.accountId, UpdateType.Revoke); + expect( + shieldedAccessControl._updateRole(ADMIN.role, ADMIN.accountId, UpdateType.Revoke), + ).toBe(false); + }); + }); + + describe('should not update nullifier set', () => { + it('when re-revoking an already revoked role', () => { + shieldedAccessControl._updateRole(ADMIN.role, ADMIN.accountId, UpdateType.Revoke); + const initialSetSize = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.size(); + expect(initialSetSize).toBe(1n); + + shieldedAccessControl._updateRole(ADMIN.role, ADMIN.accountId, UpdateType.Revoke); + + const updatedSetSize = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.size(); + expect(updatedSetSize).toEqual(initialSetSize); + }); + }); + }); + }); + describe('canProveRole', () => { beforeEach(() => { shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); @@ -2616,8 +2459,7 @@ describe('ShieldedAccessControl', () => { }); }); - // TODO refactor to test _uncheckedCanProveRole - describe.skip('_uncheckedCanProveRole', () => { + describe('_uncheckedCanProveRole', () => { beforeEach(() => { shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); shieldedAccessControl.setPersistentCaller(ADMIN.publicKey); @@ -2643,7 +2485,7 @@ describe('ShieldedAccessControl', () => { }, ); expect(() => { - shieldedAccessControl.canProveRole(ADMIN.role); + shieldedAccessControl._uncheckedCanProveRole(ADMIN.role); }).toThrow( 'ShieldedAccessControl: Path must contain leaf matching computed role commitment for the provided role, accountId pairing', ); @@ -2660,7 +2502,7 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), ).toBe(true); - expect(shieldedAccessControl.canProveRole(ADMIN.role)).toBe( + expect(shieldedAccessControl._uncheckedCanProveRole(ADMIN.role)).toBe( true, ); }); @@ -2695,16 +2537,16 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl._grantRole(OPERATOR_2.role, account2); shieldedAccessControl._grantRole(OPERATOR_3.role, account3); - expect(shieldedAccessControl.canProveRole(ADMIN.role)).toBe( + expect(shieldedAccessControl._uncheckedCanProveRole(ADMIN.role)).toBe( true, ); - expect(shieldedAccessControl.canProveRole(OPERATOR_1.role)).toBe( + expect(shieldedAccessControl._uncheckedCanProveRole(OPERATOR_1.role)).toBe( true, ); - expect(shieldedAccessControl.canProveRole(OPERATOR_2.role)).toBe( + expect(shieldedAccessControl._uncheckedCanProveRole(OPERATOR_2.role)).toBe( true, ); - expect(shieldedAccessControl.canProveRole(OPERATOR_3.role)).toBe( + expect(shieldedAccessControl._uncheckedCanProveRole(OPERATOR_3.role)).toBe( true, ); }); @@ -2725,7 +2567,7 @@ describe('ShieldedAccessControl', () => { expect(newAdminAccountId).not.toEqual(ADMIN.accountId); shieldedAccessControl._grantRole(ADMIN.role, newAdminAccountId); - expect(shieldedAccessControl.canProveRole(ADMIN.role)).toBe( + expect(shieldedAccessControl._uncheckedCanProveRole(ADMIN.role)).toBe( true, ); }); @@ -2747,7 +2589,7 @@ describe('ShieldedAccessControl', () => { operator1AdminAccountId, ); shieldedAccessControl.as(ADMIN.publicKey); // prove ADMIN has OP_1 role - expect(shieldedAccessControl.canProveRole(OPERATOR_1.role)).toBe( + expect(shieldedAccessControl._uncheckedCanProveRole(OPERATOR_1.role)).toBe( true, ); @@ -2760,7 +2602,7 @@ describe('ShieldedAccessControl', () => { operator1Op2AccountId, ); shieldedAccessControl.as(OPERATOR_2.publicKey); // prove OP_2 has OP_1 role - expect(shieldedAccessControl.canProveRole(OPERATOR_1.role)).toBe( + expect(shieldedAccessControl._uncheckedCanProveRole(OPERATOR_1.role)).toBe( true, ); @@ -2773,7 +2615,7 @@ describe('ShieldedAccessControl', () => { operator1Op3AccountId, ); shieldedAccessControl.as(OPERATOR_3.publicKey); // prove OP_3 has OP_1 role - expect(shieldedAccessControl.canProveRole(OPERATOR_1.role)).toBe( + expect(shieldedAccessControl._uncheckedCanProveRole(OPERATOR_1.role)).toBe( true, ); }); @@ -2796,7 +2638,7 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl._validateRole(OPERATOR_1.role, accountId), ).toBe(false); - expect(shieldedAccessControl.canProveRole(OPERATOR_1.role)).toBe( + expect(shieldedAccessControl._uncheckedCanProveRole(OPERATOR_1.role)).toBe( false, ); }); @@ -2809,7 +2651,7 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), ).toBe(false); - expect(shieldedAccessControl.canProveRole(ADMIN.role)).toBe( + expect(shieldedAccessControl._uncheckedCanProveRole(ADMIN.role)).toBe( false, ); }); @@ -2822,7 +2664,7 @@ describe('ShieldedAccessControl', () => { ).toBe(false); shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); - expect(shieldedAccessControl.canProveRole(ADMIN.role)).toBe( + expect(shieldedAccessControl._uncheckedCanProveRole(ADMIN.role)).toBe( false, ); }); @@ -2831,7 +2673,7 @@ describe('ShieldedAccessControl', () => { // UNAUTHORIZED uses the same private state (ADMIN.secretNonce for ADMIN.role), // so their derived accountId won't match the committed one. shieldedAccessControl.as(UNAUTHORIZED.publicKey); - expect(shieldedAccessControl.canProveRole(ADMIN.role)).toBe( + expect(shieldedAccessControl._uncheckedCanProveRole(ADMIN.role)).toBe( false, ); }); @@ -2857,7 +2699,7 @@ describe('ShieldedAccessControl', () => { ), ); - expect(shieldedAccessControl.canProveRole(ADMIN.role)).toBe( + expect(shieldedAccessControl._uncheckedCanProveRole(ADMIN.role)).toBe( false, ); }); @@ -2876,7 +2718,7 @@ describe('ShieldedAccessControl', () => { 'wit_getRoleCommitmentPath', RETURN_BAD_PATH, ); - expect(shieldedAccessControl.canProveRole(ADMIN.role)).toBe( + expect(shieldedAccessControl._uncheckedCanProveRole(ADMIN.role)).toBe( false, ); }); diff --git a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts index b4789d7c..64cf29e9 100644 --- a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts +++ b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts @@ -6,6 +6,7 @@ import { import { ledger, Contract as MockShieldedAccessControl, + ZswapCoinPublicKey, type ShieldedAccessControl_UpdateType as UpdateType, } from '../../../../artifacts/MockShieldedAccessControl/contract/index.js'; import { @@ -68,6 +69,14 @@ export class ShieldedAccessControlSimulator extends ShieldedAccessControlSimulat return this.circuits.impure._computeAccountId(role); } + public _computeAccountIdLocally( + account: ZswapCoinPublicKey, + secretNonce: Uint8Array, + instanceSalt: Uint8Array + ): Uint8Array { + return this.circuits.pure._computeAccountIdLocally(account, secretNonce, instanceSalt); + } + public _computeNullifier(roleCommitment: Uint8Array): Uint8Array { return this.circuits.pure._computeNullifier(roleCommitment); } From d24eae933867d14140168d4cba07a617945052da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Wed, 18 Mar 2026 11:42:25 -0400 Subject: [PATCH 310/322] Remove README --- contracts/README.md | 93 --------------------------------------------- 1 file changed, 93 deletions(-) delete mode 100644 contracts/README.md diff --git a/contracts/README.md b/contracts/README.md deleted file mode 100644 index 21e5fa08..00000000 --- a/contracts/README.md +++ /dev/null @@ -1,93 +0,0 @@ -# Contracts README - -This package contains the Compact smart contract source files, compiled artifacts, witness implementations, and test infrastructure for OpenZeppelin Contracts for Compact. - -## Directory Structure - -``` -contracts/ -├── src/ # Source files -│ ├── access/ # Access control contracts -│ ├── security/ # Security utility contracts -│ ├── token/ # Token standard contracts -│ ├── utils/ # General utility contracts -│ ├── archive/ # Archived/deprecated contracts -│ └── test-utils/ # Shared test helpers -├── artifacts/ # Compiled contract outputs (generated) -└── dist/ # Compiled TypeScript witness outputs (generated) -``` - -## src/ - -The `src/` directory is organized by module category. Each module follows the same internal layout: - -``` -/ -├── .compact # Contract source -├── witnesses/ # TypeScript witness implementations -└── test/ - ├── .test.ts # Test suite - ├── mocks/ # Mock contracts (test-only — see warning below) - └── simulators/ # Simulator helpers for testing -``` - -### src/access/ - -Access control primitives for restricting who can call contract circuits. - -| File | Description | -|------|-------------| -| `AccessControl.compact` | Role-based access control | -| `Ownable.compact` | Single-owner access control | -| `ShieldedAccessControl.compact` | Role-based access control with shielded (private) role assignments | -| `ZOwnablePK.compact` | Single-owner access control with shielded ownership | - -### src/security/ - -Contracts that add common security patterns on top of other modules. - -| File | Description | -|------|-------------| -| `Initializable.compact` | One-time initialization mechanism | -| `Pausable.compact` | Emergency pause/unpause mechanism | - -### src/token/ - -Implementations of standard token interfaces. - -| File | Description | -|------|-------------| -| `FungibleToken.compact` | ERC-20-style fungible token | -| `NonFungibleToken.compact` | ERC-721-style non-fungible token | -| `MultiToken.compact` | ERC-1155-style multi-token | - -### src/utils/ - -Low-level utilities shared across modules. - -| File | Description | -|------|-------------| -| `Utils.compact` | Common helper circuits | - -### src/archive/ - -Contracts that are no longer actively maintained. Do not use in new projects. - -### src/test-utils/ - -Shared TypeScript helpers used across test suites (e.g. address utilities). Not part of the public API. - ---- - -## > ⚠️ Mock Contracts Are For Testing Only - -Each module's `test/mocks/` directory contains `Mock*.compact` files (e.g. `MockFungibleToken.compact`, `MockOwnable.compact`, `MockAccessControl.compact`). - -**These contracts exist solely to expose internal state and circuits for testing purposes. They must never be used in production.** - -Mock contracts typically: -- Expose internal or protected circuits publicly for direct testing -- Skip access control or safety checks to isolate specific behaviors -- Introduce additional state that makes testing easier but is unsafe in deployment - -**Using a Mock contract in production would undermine the security guarantees the corresponding production contract is designed to provide.** From 8c8ea726b0c377d3ad2473512e92fbe11290002d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Wed, 18 Mar 2026 13:12:20 -0400 Subject: [PATCH 311/322] Update contracts/src/access/ShieldedAccessControl.compact MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 0xisk Signed-off-by: ⟣ €₥ℵ∪ℓ ⟢ <34749913+emnul@users.noreply.github.com> --- contracts/src/access/ShieldedAccessControl.compact | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index e24b725e..7528e5cb 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -639,7 +639,7 @@ module ShieldedAccessControl { * * @returns {RoleCommitment} The commitment derived from `accountId` and `role`. */ - export circuit _computeRoleCommitment( + export circuit computeRoleCommitment( role: RoleIdentifier, accountId: AccountIdentifier, ): RoleCommitment { From c5b34f66a448561ff1af3eb7e9830bdb4f6bdf8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Wed, 18 Mar 2026 13:12:36 -0400 Subject: [PATCH 312/322] Update contracts/src/access/ShieldedAccessControl.compact MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 0xisk Signed-off-by: ⟣ €₥ℵ∪ℓ ⟢ <34749913+emnul@users.noreply.github.com> --- contracts/src/access/ShieldedAccessControl.compact | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index 7528e5cb..5df70475 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -667,7 +667,7 @@ module ShieldedAccessControl { * * @returns {RoleNullifier} roleNullifier - The associated nullifier for `roleCommitment`. */ - export pure circuit _computeNullifier(roleCommitment: RoleCommitment): RoleNullifier { + export pure circuit computeNullifier(roleCommitment: RoleCommitment): RoleNullifier { return persistentHash>>( [roleCommitment as Bytes<32>, pad(32, "ShieldedAccessControl:nullifier")] ) From 38b4696e2c652357c4e53d582cc14dd1ba6e1083 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Wed, 18 Mar 2026 13:12:52 -0400 Subject: [PATCH 313/322] Update contracts/src/access/ShieldedAccessControl.compact MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 0xisk Signed-off-by: ⟣ €₥ℵ∪ℓ ⟢ <34749913+emnul@users.noreply.github.com> --- contracts/src/access/ShieldedAccessControl.compact | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index 5df70475..5e809fd4 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -745,7 +745,7 @@ module ShieldedAccessControl { * * @returns {AccountIdentifier} accountId - The computed account ID. */ - export pure circuit _computeAccountIdLocally(account: ZswapCoinPublicKey, secretNonce: Bytes<32>, instanceSalt: Bytes<32>): AccountIdentifier { + export pure circuit computeAccountIdLocally(account: ZswapCoinPublicKey, secretNonce: Bytes<32>, instanceSalt: Bytes<32>): AccountIdentifier { return persistentHash>>( [account.bytes, secretNonce, From cc56b3907fd5bb9e14dec24686fbfbdbb4ab6298 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Wed, 18 Mar 2026 13:18:58 -0400 Subject: [PATCH 314/322] Update contracts/src/access/ShieldedAccessControl.compact MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 0xisk Signed-off-by: ⟣ €₥ℵ∪ℓ ⟢ <34749913+emnul@users.noreply.github.com> --- contracts/src/access/ShieldedAccessControl.compact | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index 5e809fd4..1710bea8 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -157,7 +157,7 @@ module ShieldedAccessControl { * commitments for this contract instance. * * This salt prevents commitment collisions across contracts that might otherwise use - * the same identifiers or domain parameters. It should be should be a cryptographically strong random value + * the same identifiers or domain parameters. It should be a cryptographically strong random value * It is immutable after initialization. */ export sealed ledger _instanceSalt: Bytes<32>; From dbc85f4c93dfc09db357cf53f35a58d2e6664487 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Wed, 18 Mar 2026 13:19:52 -0400 Subject: [PATCH 315/322] update _setRoleAdmin docs --- contracts/src/access/ShieldedAccessControl.compact | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index e24b725e..ee908bd3 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -544,7 +544,8 @@ module ShieldedAccessControl { } /** - * @description Sets `adminId` as `role`'s admin identifier. Internal circuit without access restriction. + * @description Sets `adminId` as `role`'s admin identifier. Users with valid admin identifiers + * may grant and revoke access to the specified `role`.Internal circuit without access restriction. * * @circuitInfo k=10, rows=581 * @@ -554,7 +555,7 @@ module ShieldedAccessControl { * - The admin identifier * * @param {RoleIdentifier} role - The role identifier. - * @param {AccountIdentifier} accountId - The unique identifier of the account. + * @param {RoleIdentifier} adminId - The admin identifier for `role`. * * @return {[]} - Empty tuple. */ From 7729e675be28f8acb3e358364cbca27d08679e6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Wed, 18 Mar 2026 13:25:41 -0400 Subject: [PATCH 316/322] Update circuit _computeNull and _computeRoleCom names --- .../src/access/ShieldedAccessControl.compact | 27 ++++++------ .../mocks/MockShieldedAccessControl.compact | 44 +++++++++---------- 2 files changed, 36 insertions(+), 35 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index c074574d..d440b30e 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -503,8 +503,8 @@ module ShieldedAccessControl { accountId: AccountIdentifier, updateType: UpdateType ): Boolean { - const roleCommitment = _computeRoleCommitment(role, accountId); - const roleNullifier = _computeNullifier(roleCommitment); + const roleCommitment = computeRoleCommitment(role, accountId); + const roleNullifier = computeNullifier(roleCommitment); const isRevoked = _roleCommitmentNullifiers.member(disclose(roleNullifier)); if (isRevoked) { @@ -589,7 +589,7 @@ module ShieldedAccessControl { * @return {Boolean} isValidRole - A boolean indicating whether `accountId` has a valid role */ circuit _validateRole(role: RoleIdentifier, accountId: AccountIdentifier): Boolean { - const roleCommitment = _computeRoleCommitment(role, accountId); + const roleCommitment = computeRoleCommitment(role, accountId); const roleCommitmentPath = wit_getRoleCommitmentPath(roleCommitment); const isValidPath = _operatorRoles.checkRoot( @@ -603,7 +603,7 @@ module ShieldedAccessControl { ); } - const roleNullifier = _computeNullifier(roleCommitment); + const roleNullifier = computeNullifier(roleCommitment); const isRevoked = _roleCommitmentNullifiers.member(disclose(roleNullifier)); return isValidPath && !isRevoked; @@ -641,9 +641,9 @@ module ShieldedAccessControl { * @returns {RoleCommitment} The commitment derived from `accountId` and `role`. */ export circuit computeRoleCommitment( - role: RoleIdentifier, - accountId: AccountIdentifier, - ): RoleCommitment { + role: RoleIdentifier, + accountId: AccountIdentifier, + ): RoleCommitment { return persistentHash>>( [role as Bytes<32>, @@ -660,7 +660,7 @@ module ShieldedAccessControl { * ## Role Nullifier Derivation * `roleNullifier = SHA256(roleCommitment, nullifierDomain)` * - * - `roleCommitment`: See `_computeRoleCommitment`. + * - `roleCommitment`: See `computeRoleCommitment`. * - `nullifierDomain`: Domain separator `"ShieldedAccessControl:nullifier"` (padded to 32 bytes) to prevent * hash collisions when extending the module or using similar commitment schemes. * @@ -746,12 +746,13 @@ module ShieldedAccessControl { * * @returns {AccountIdentifier} accountId - The computed account ID. */ - export pure circuit computeAccountIdLocally(account: ZswapCoinPublicKey, secretNonce: Bytes<32>, instanceSalt: Bytes<32>): AccountIdentifier { + export pure circuit computeAccountIdLocally( + account: ZswapCoinPublicKey, + secretNonce: Bytes<32>, + instanceSalt: Bytes<32> + ): AccountIdentifier { return persistentHash>>( - [account.bytes, - secretNonce, - instanceSalt, - pad(32, "ShieldedAccessControl:accountId")] + [account.bytes, secretNonce, instanceSalt, pad(32, "ShieldedAccessControl:accountId")] ) as AccountIdentifier; } diff --git a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact index 09e963b9..94330934 100644 --- a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact +++ b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact @@ -44,12 +44,12 @@ constructor(instanceSalt: Bytes<32>, isInit: Boolean) { } } -export circuit _computeRoleCommitment( +export circuit computeRoleCommitment( role: ShieldedAccessControl_RoleIdentifier, accountId: ShieldedAccessControl_AccountIdentifier ): ShieldedAccessControl_RoleCommitment { - return ShieldedAccessControl__computeRoleCommitment(role, accountId); + return ShieldedAccessControl_computeRoleCommitment(role, accountId); } export pure circuit DEFAULT_ADMIN_ROLE(): ShieldedAccessControl_RoleIdentifier { @@ -61,29 +61,29 @@ export circuit _computeAccountId( role: ShieldedAccessControl_RoleIdentifier ): ShieldedAccessControl_AccountIdentifier { - // value must be disclosed for testing - return disclose(persistentHash>>( - [ownPublicKey().bytes, - wit_secretNonce(role), - ShieldedAccessControl__instanceSalt, - pad(32, "ShieldedAccessControl:accountId")] - )) - as ShieldedAccessControl_AccountIdentifier; + // value must be disclosed for testing + return disclose(persistentHash>>( + [ownPublicKey().bytes, + wit_secretNonce(role), + ShieldedAccessControl__instanceSalt, + pad(32, "ShieldedAccessControl:accountId")] + )) + as ShieldedAccessControl_AccountIdentifier; } -export pure circuit _computeAccountIdLocally( - account: ZswapCoinPublicKey, - secretNonce: Bytes<32>, - instanceSalt: Bytes<32> - ): ShieldedAccessControl_AccountIdentifier { +export pure circuit computeAccountIdLocally( + account: ZswapCoinPublicKey, + secretNonce: Bytes<32>, + instanceSalt: Bytes<32> + ): ShieldedAccessControl_AccountIdentifier { - return ShieldedAccessControl__computeAccountIdLocally(account, secretNonce, instanceSalt); + return ShieldedAccessControl_computeAccountIdLocally(account, secretNonce, instanceSalt); } -export pure circuit _computeNullifier( +export pure circuit computeNullifier( roleCommitment: ShieldedAccessControl_RoleCommitment ): ShieldedAccessControl_RoleNullifier { - return ShieldedAccessControl__computeNullifier(roleCommitment); + return ShieldedAccessControl_computeNullifier(roleCommitment); } export circuit canProveRole(role: ShieldedAccessControl_RoleIdentifier): Boolean { @@ -105,7 +105,7 @@ export circuit _validateRole( role: ShieldedAccessControl_RoleIdentifier, accountId: ShieldedAccessControl_AccountIdentifier ): Boolean { - const roleCommitment = _computeRoleCommitment(role, accountId); + const roleCommitment = computeRoleCommitment(role, accountId); const roleCommitmentPath = wit_getRoleCommitmentPath(roleCommitment); const isValidPath = ShieldedAccessControl__operatorRoles.checkRoot( @@ -121,7 +121,7 @@ export circuit _validateRole( ); } - const roleNullifier = _computeNullifier(roleCommitment); + const roleNullifier = computeNullifier(roleCommitment); const isRevoked = ShieldedAccessControl__roleCommitmentNullifiers.member(disclose(roleNullifier)); return isValidPath && !isRevoked; @@ -181,8 +181,8 @@ export circuit _updateRole( accountId: ShieldedAccessControl_AccountIdentifier, updateType: ShieldedAccessControl_UpdateType ): Boolean { - const roleCommitment = _computeRoleCommitment(role, accountId); - const roleNullifier = _computeNullifier(roleCommitment); + const roleCommitment = computeRoleCommitment(role, accountId); + const roleNullifier = computeNullifier(roleCommitment); const isRevoked = ShieldedAccessControl__roleCommitmentNullifiers.member(disclose(roleNullifier)); if (isRevoked) { From 9bd47eaa28549d2f6acf8b4ab6c3e533e0a05aa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Wed, 18 Mar 2026 13:47:50 -0400 Subject: [PATCH 317/322] Refactor names in tests / sim --- .../access/test/ShieldedAccessControl.test.ts | 16 ++++++++-------- .../simulators/ShieldedAccessControlSimulator.ts | 8 ++++---- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/contracts/src/access/test/ShieldedAccessControl.test.ts b/contracts/src/access/test/ShieldedAccessControl.test.ts index f4392390..b55e5cb4 100644 --- a/contracts/src/access/test/ShieldedAccessControl.test.ts +++ b/contracts/src/access/test/ShieldedAccessControl.test.ts @@ -150,8 +150,8 @@ describe('ShieldedAccessControl', () => { ['getRoleAdmin', [UNINITIALIZED.role]], ['_setRoleAdmin', [UNINITIALIZED.role, UNINITIALIZED.role]], ['_computeAccountId', [UNINITIALIZED.role]], - ['_computeRoleCommitment', [UNINITIALIZED.role, UNINITIALIZED.accountId]], - ['_computeNullifier', [UNINITIALIZED.roleCommitment]], + ['computeRoleCommitment', [UNINITIALIZED.role, UNINITIALIZED.accountId]], + ['computeNullifier', [UNINITIALIZED.roleCommitment]], ['DEFAULT_ADMIN_ROLE', []], ['_validateRole', [UNINITIALIZED.roleCommitment]], ['_updateRole', [UNINITIALIZED.roleCommitment, UNINITIALIZED.accountId, UpdateType.Grant]], @@ -198,10 +198,10 @@ describe('ShieldedAccessControl', () => { }); }); - describe('_computeRoleCommitment', () => { + describe('computeRoleCommitment', () => { it('should match computed commitment', () => { expect( - shieldedAccessControl._computeRoleCommitment( + shieldedAccessControl.computeRoleCommitment( ADMIN.role, ADMIN.accountId, ), @@ -226,7 +226,7 @@ describe('ShieldedAccessControl', () => { // Test protected circuit expect( ( - shieldedAccessControl._computeRoleCommitment as ( + shieldedAccessControl.computeRoleCommitment as ( ...args: unknown[] ) => Uint8Array )(...args) @@ -234,16 +234,16 @@ describe('ShieldedAccessControl', () => { }); }); - describe('_computeNullifier', () => { + describe('computeNullifier', () => { it('should match nullifier', () => { expect( - shieldedAccessControl._computeNullifier(ADMIN.roleCommitment), + shieldedAccessControl.computeNullifier(ADMIN.roleCommitment), ).toEqual(ADMIN.roleNullifier); }); it('should not match bad commitment inputs', () => { expect( - shieldedAccessControl._computeNullifier(BAD_INPUT.roleCommitment), + shieldedAccessControl.computeNullifier(BAD_INPUT.roleCommitment), ).not.toEqual(ADMIN.roleNullifier); }); }); diff --git a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts index 64cf29e9..f6eae1ed 100644 --- a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts +++ b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts @@ -56,11 +56,11 @@ export class ShieldedAccessControlSimulator extends ShieldedAccessControlSimulat super([instanceSalt, isInit], options); } - public _computeRoleCommitment( + public computeRoleCommitment( role: Uint8Array, accountId: Uint8Array, ): Uint8Array { - return this.circuits.impure._computeRoleCommitment(role, accountId); + return this.circuits.impure.computeRoleCommitment(role, accountId); } public _computeAccountId( @@ -77,8 +77,8 @@ export class ShieldedAccessControlSimulator extends ShieldedAccessControlSimulat return this.circuits.pure._computeAccountIdLocally(account, secretNonce, instanceSalt); } - public _computeNullifier(roleCommitment: Uint8Array): Uint8Array { - return this.circuits.pure._computeNullifier(roleCommitment); + public computeNullifier(roleCommitment: Uint8Array): Uint8Array { + return this.circuits.pure.computeNullifier(roleCommitment); } public DEFAULT_ADMIN_ROLE(): Uint8Array { From 28cc91e4e4c07064190d8ba893cdee7b74b29381 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Wed, 18 Mar 2026 13:48:32 -0400 Subject: [PATCH 318/322] Refactor _computeAccountId to depend on pure variant, rename pure variant --- .../src/access/ShieldedAccessControl.compact | 10 ++-------- .../access/test/ShieldedAccessControl.test.ts | 10 +++++----- .../mocks/MockShieldedAccessControl.compact | 18 ++++-------------- .../ShieldedAccessControlSimulator.ts | 4 ++-- 4 files changed, 13 insertions(+), 29 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index d440b30e..55198f2c 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -705,13 +705,7 @@ module ShieldedAccessControl { * @returns {AccountIdentifier} accountId - The computed account ID. */ circuit _computeAccountId(role: RoleIdentifier): AccountIdentifier { - return persistentHash>>( - [ownPublicKey().bytes, - wit_secretNonce(role), - _instanceSalt, - pad(32, "ShieldedAccessControl:accountId")] - ) - as AccountIdentifier; + return computeAccountId(ownPublicKey(), wit_secretNonce(role), _instanceSalt); } /** @@ -746,7 +740,7 @@ module ShieldedAccessControl { * * @returns {AccountIdentifier} accountId - The computed account ID. */ - export pure circuit computeAccountIdLocally( + export pure circuit computeAccountId( account: ZswapCoinPublicKey, secretNonce: Bytes<32>, instanceSalt: Bytes<32> diff --git a/contracts/src/access/test/ShieldedAccessControl.test.ts b/contracts/src/access/test/ShieldedAccessControl.test.ts index b55e5cb4..d4e4c7a2 100644 --- a/contracts/src/access/test/ShieldedAccessControl.test.ts +++ b/contracts/src/access/test/ShieldedAccessControl.test.ts @@ -155,7 +155,7 @@ describe('ShieldedAccessControl', () => { ['DEFAULT_ADMIN_ROLE', []], ['_validateRole', [UNINITIALIZED.roleCommitment]], ['_updateRole', [UNINITIALIZED.roleCommitment, UNINITIALIZED.accountId, UpdateType.Grant]], - ['_computeAccountIdLocally', [UNINITIALIZED.zPublicKey, UNINITIALIZED.secretNonce, UNINITIALIZED.role]], + ['computeAccountId', [UNINITIALIZED.zPublicKey, UNINITIALIZED.secretNonce, UNINITIALIZED.role]], ]; it.each(circuitsToSucceed)('%s should succeed', (circuitName, args) => { expect(() => { @@ -276,10 +276,10 @@ describe('ShieldedAccessControl', () => { }); }); - describe('_computeAccountIdLocally', () => { + describe('computeAccountId', () => { it('should match when given correct account and nonce', () => { expect( - shieldedAccessControl._computeAccountIdLocally( + shieldedAccessControl.computeAccountId( ADMIN.zPublicKey, ADMIN.secretNonce, INSTANCE_SALT, @@ -288,7 +288,7 @@ describe('ShieldedAccessControl', () => { }); it('should not match when given correct account with bad nonce', () => { - const computedAccountId = shieldedAccessControl._computeAccountIdLocally( + const computedAccountId = shieldedAccessControl.computeAccountId( ADMIN.zPublicKey, BAD_INPUT.secretNonce, INSTANCE_SALT, @@ -298,7 +298,7 @@ describe('ShieldedAccessControl', () => { }); it('should not match when given unauthorized account with correct nonce', () => { - const computedAccountId = shieldedAccessControl._computeAccountIdLocally( + const computedAccountId = shieldedAccessControl.computeAccountId( UNAUTHORIZED.zPublicKey, ADMIN.secretNonce, INSTANCE_SALT, diff --git a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact index 94330934..116c3b14 100644 --- a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact +++ b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact @@ -57,27 +57,17 @@ export pure circuit DEFAULT_ADMIN_ROLE(): ShieldedAccessControl_RoleIdentifier { } // circuit is reimplemented in the Mock contract for testing -export circuit _computeAccountId( - role: ShieldedAccessControl_RoleIdentifier - ): ShieldedAccessControl_AccountIdentifier { - - // value must be disclosed for testing - return disclose(persistentHash>>( - [ownPublicKey().bytes, - wit_secretNonce(role), - ShieldedAccessControl__instanceSalt, - pad(32, "ShieldedAccessControl:accountId")] - )) - as ShieldedAccessControl_AccountIdentifier; +export circuit _computeAccountId(role: ShieldedAccessControl_RoleIdentifier): ShieldedAccessControl_AccountIdentifier { + return disclose(ShieldedAccessControl_computeAccountId(ownPublicKey(), wit_secretNonce(role), ShieldedAccessControl__instanceSalt)); } -export pure circuit computeAccountIdLocally( +export pure circuit computeAccountId( account: ZswapCoinPublicKey, secretNonce: Bytes<32>, instanceSalt: Bytes<32> ): ShieldedAccessControl_AccountIdentifier { - return ShieldedAccessControl_computeAccountIdLocally(account, secretNonce, instanceSalt); + return ShieldedAccessControl_computeAccountId(account, secretNonce, instanceSalt); } export pure circuit computeNullifier( diff --git a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts index f6eae1ed..1c03b3ff 100644 --- a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts +++ b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts @@ -69,12 +69,12 @@ export class ShieldedAccessControlSimulator extends ShieldedAccessControlSimulat return this.circuits.impure._computeAccountId(role); } - public _computeAccountIdLocally( + public computeAccountId( account: ZswapCoinPublicKey, secretNonce: Uint8Array, instanceSalt: Uint8Array ): Uint8Array { - return this.circuits.pure._computeAccountIdLocally(account, secretNonce, instanceSalt); + return this.circuits.pure.computeAccountId(account, secretNonce, instanceSalt); } public computeNullifier(roleCommitment: Uint8Array): Uint8Array { From ea50c073619f1833f737479a869de148ee9a1d4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9F=A3=20=E2=82=AC=E2=82=A5=E2=84=B5=E2=88=AA=E2=84=93?= =?UTF-8?q?=20=E2=9F=A2?= <34749913+emnul@users.noreply.github.com> Date: Wed, 18 Mar 2026 20:16:11 -0400 Subject: [PATCH 319/322] Update error message --- .../access/test/simulators/ShieldedAccessControlSimulator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts index 1c03b3ff..b8a1664b 100644 --- a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts +++ b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts @@ -161,7 +161,7 @@ export class ShieldedAccessControlSimulator extends ShieldedAccessControlSimulat const roleString = Buffer.from(role).toString('hex'); const roleNonce = this.getPrivateState().roles[roleString]; if (typeof roleNonce === "undefined") { - throw new Error(`Missing secret nonce for role ${roleNonce}`) + throw new Error(`Missing secret nonce for role ${roleString}`) } return roleNonce; }, From 00d521be4ecdea0df15d0b09d7c0a62897150c8d Mon Sep 17 00:00:00 2001 From: 0xisk Date: Mon, 23 Mar 2026 17:06:16 +0100 Subject: [PATCH 320/322] chore: add warning for the exported grant and revoke functions --- .../src/access/ShieldedAccessControl.compact | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/contracts/src/access/ShieldedAccessControl.compact b/contracts/src/access/ShieldedAccessControl.compact index 55198f2c..f4ec503c 100644 --- a/contracts/src/access/ShieldedAccessControl.compact +++ b/contracts/src/access/ShieldedAccessControl.compact @@ -348,6 +348,9 @@ module ShieldedAccessControl { * * Internal circuit without access restriction. * + * @warning Exposing this circuit directly in an implementing contract would allow anyone to grant + * roles without authorization. It must be wrapped with appropriate access control. + * * ## Storage Caveat * * `_operatorRoles` is a fixed-depth Merkle tree with a maximum capacity of @@ -461,6 +464,9 @@ module ShieldedAccessControl { * so a `(role, accountId)` pairing that does not exist can still be revoked. Returns false if a role is already * revoked. Internal circuit without access restriction. * + * @warning Exposing this circuit directly in an implementing contract would allow anyone to revoke + * roles without authorization. It must be wrapped with appropriate access control. + * * Requirements: * * - Contract is initialized. @@ -521,8 +527,10 @@ module ShieldedAccessControl { } /** - * @description Returns the admin role that controls `role` or a zero - * byte array if `role` doesn't exist. See {grantRole} and {revokeRole}. + * @description Returns the admin role that controls `role`. Returns `DEFAULT_ADMIN_ROLE` for + * roles with no explicitly set admin. Since `DEFAULT_ADMIN_ROLE` is the zero byte array, + * there is no distinction between a nonexistent `role` and one whose admin is `DEFAULT_ADMIN_ROLE`. + * See {grantRole} and {revokeRole}. * * To change a role’s admin use {_setRoleAdmin}. * @@ -545,7 +553,10 @@ module ShieldedAccessControl { /** * @description Sets `adminId` as `role`'s admin identifier. Users with valid admin identifiers - * may grant and revoke access to the specified `role`.Internal circuit without access restriction. + * may grant and revoke access to the specified `role`. Internal circuit without access restriction. + * + * @warning Exposing this circuit directly in an implementing contract would allow anyone to assign + * arbitrary admin roles without authorization. It must be wrapped with appropriate access control. * * @circuitInfo k=10, rows=581 * From dcf6e7e51283a2bca710b5c73ee6251d202a1d00 Mon Sep 17 00:00:00 2001 From: 0xisk Date: Fri, 27 Mar 2026 15:39:06 +0100 Subject: [PATCH 321/322] chore(access): same ordering everywhere (#410) --- .../access/test/ShieldedAccessControl.test.ts | 2459 +++++++++-------- .../mocks/MockShieldedAccessControl.compact | 154 +- .../ShieldedAccessControlSimulator.ts | 99 +- .../ShieldedAccessControlWitnesses.ts | 63 +- .../simulator/test/fixtures/utils/address.ts | 14 +- 5 files changed, 1413 insertions(+), 1376 deletions(-) diff --git a/contracts/src/access/test/ShieldedAccessControl.test.ts b/contracts/src/access/test/ShieldedAccessControl.test.ts index d4e4c7a2..0f0da7c7 100644 --- a/contracts/src/access/test/ShieldedAccessControl.test.ts +++ b/contracts/src/access/test/ShieldedAccessControl.test.ts @@ -9,9 +9,9 @@ import { import { beforeEach, describe, expect, it } from 'vitest'; import * as utils from '#test-utils/address.js'; import { - Ledger, - ZswapCoinPublicKey, - ShieldedAccessControl_UpdateType as UpdateType + type Ledger, + ShieldedAccessControl_UpdateType as UpdateType, + type ZswapCoinPublicKey, } from '../../../artifacts/MockShieldedAccessControl/contract/index.js'; import { ShieldedAccessControlPrivateState } from '../witnesses/ShieldedAccessControlWitnesses.js'; import { ShieldedAccessControlSimulator } from './simulators/ShieldedAccessControlSimulator.js'; @@ -154,8 +154,22 @@ describe('ShieldedAccessControl', () => { ['computeNullifier', [UNINITIALIZED.roleCommitment]], ['DEFAULT_ADMIN_ROLE', []], ['_validateRole', [UNINITIALIZED.roleCommitment]], - ['_updateRole', [UNINITIALIZED.roleCommitment, UNINITIALIZED.accountId, UpdateType.Grant]], - ['computeAccountId', [UNINITIALIZED.zPublicKey, UNINITIALIZED.secretNonce, UNINITIALIZED.role]], + [ + '_updateRole', + [ + UNINITIALIZED.roleCommitment, + UNINITIALIZED.accountId, + UpdateType.Grant, + ], + ], + [ + 'computeAccountId', + [ + UNINITIALIZED.zPublicKey, + UNINITIALIZED.secretNonce, + UNINITIALIZED.role, + ], + ], ]; it.each(circuitsToSucceed)('%s should succeed', (circuitName, args) => { expect(() => { @@ -194,437 +208,104 @@ describe('ShieldedAccessControl', () => { describe('DEFAULT_ADMIN_ROLE', () => { it('should return 0', () => { - expect(shieldedAccessControl.DEFAULT_ADMIN_ROLE()).toStrictEqual(new Uint8Array(32)); - }); - }); - - describe('computeRoleCommitment', () => { - it('should match computed commitment', () => { - expect( - shieldedAccessControl.computeRoleCommitment( - ADMIN.role, - ADMIN.accountId, - ), - ).toEqual(ADMIN.roleCommitment); - }); - - type ComputeCommitmentCases = [ - isValidRoleId: boolean, - isValidAccountId: boolean, - args: unknown[], - ]; - - const checkedCircuits: ComputeCommitmentCases[] = [ - [false, true, [BAD_INPUT.role, ADMIN.accountId]], - [true, false, [ADMIN.role, BAD_INPUT.accountId]], - [false, false, [BAD_INPUT.role, BAD_INPUT.accountId]], - ]; - - it.each( - checkedCircuits, - )('should not compute commitment with isValidRoleId=%s, isValidAccountId=%s', (_isValidRoleId, _isValidAccountId, args) => { - // Test protected circuit - expect( - ( - shieldedAccessControl.computeRoleCommitment as ( - ...args: unknown[] - ) => Uint8Array - )(...args) - ).not.toEqual(ADMIN.roleCommitment); - }); - }); - - describe('computeNullifier', () => { - it('should match nullifier', () => { - expect( - shieldedAccessControl.computeNullifier(ADMIN.roleCommitment), - ).toEqual(ADMIN.roleNullifier); - }); - - it('should not match bad commitment inputs', () => { - expect( - shieldedAccessControl.computeNullifier(BAD_INPUT.roleCommitment), - ).not.toEqual(ADMIN.roleNullifier); - }); - }); - - describe('_computeAccountId', () => { - beforeEach(() => { - shieldedAccessControl.setPersistentCaller(ADMIN.publicKey); - }); - - it('should match when authorized caller with correct nonce', () => { - expect( - shieldedAccessControl._computeAccountId( - ADMIN.role, - ), - ).toEqual(ADMIN.accountId); - }); - - it('should not match when authorized caller with bad nonce', () => { - shieldedAccessControl.privateState.injectSecretNonce(ADMIN.role, BAD_INPUT.secretNonce) - const computedAccountId = shieldedAccessControl._computeAccountId(ADMIN.role) - expect(computedAccountId).not.toEqual(ADMIN.accountId); - expect(computedAccountId).toEqual(buildAccountIdHash(ADMIN.zPublicKey, BAD_INPUT.secretNonce)); - }); - - it('should not match when unauthorized caller with correct nonce', () => { - shieldedAccessControl.as(UNAUTHORIZED.publicKey); - const computedAccountId = shieldedAccessControl._computeAccountId(ADMIN.role); - expect(computedAccountId).not.toEqual(ADMIN.accountId); - expect(computedAccountId).toEqual(buildAccountIdHash(UNAUTHORIZED.zPublicKey, ADMIN.secretNonce)); - }); - }); - - describe('computeAccountId', () => { - it('should match when given correct account and nonce', () => { - expect( - shieldedAccessControl.computeAccountId( - ADMIN.zPublicKey, - ADMIN.secretNonce, - INSTANCE_SALT, - ), - ).toEqual(ADMIN.accountId); - }); - - it('should not match when given correct account with bad nonce', () => { - const computedAccountId = shieldedAccessControl.computeAccountId( - ADMIN.zPublicKey, - BAD_INPUT.secretNonce, - INSTANCE_SALT, - ); - expect(computedAccountId).not.toEqual(ADMIN.accountId); - expect(computedAccountId).toEqual(buildAccountIdHash(ADMIN.zPublicKey, BAD_INPUT.secretNonce)); - }); - - it('should not match when given unauthorized account with correct nonce', () => { - const computedAccountId = shieldedAccessControl.computeAccountId( - UNAUTHORIZED.zPublicKey, - ADMIN.secretNonce, - INSTANCE_SALT, + expect(shieldedAccessControl.DEFAULT_ADMIN_ROLE()).toStrictEqual( + new Uint8Array(32), ); - expect(computedAccountId).not.toEqual(ADMIN.accountId); - expect(computedAccountId).toEqual(buildAccountIdHash(UNAUTHORIZED.zPublicKey, ADMIN.secretNonce)); }); }); - describe('_validateRole', () => { + describe('assertOnlyRole', () => { beforeEach(() => { shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); shieldedAccessControl.setPersistentCaller(ADMIN.publicKey); }); - it('should fail when wit_getRoleCommitmentPath returns a valid path for a different role, accountId pairing', () => { - shieldedAccessControl._grantRole( - OPERATOR_1.role, - OPERATOR_1.accountId, - ); - // Override witness to return valid path for OPERATOR_1 role commitment - shieldedAccessControl.overrideWitness( - 'wit_getRoleCommitmentPath', - () => { - const privateState = shieldedAccessControl.getPrivateState(); - const operator1MtPath = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.findPathForLeaf( - OPERATOR_1.roleCommitment, - ); - if (operator1MtPath) return [privateState, operator1MtPath]; - throw new Error('Merkle tree path should be defined'); - }, - ); - expect(() => { - shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId); - }).toThrow( - 'ShieldedAccessControl: Path must contain leaf matching computed role commitment for the provided role, accountId pairing', - ); - }); - - describe('should return false', () => { - type CheckRoleCases = [ - badRoleId: boolean, - badAccountId: boolean, - args: unknown[], - ]; - const checkedCircuits: CheckRoleCases[] = [ - [false, true, [ADMIN.role, BAD_INPUT.accountId]], - [true, false, [BAD_INPUT.role, ADMIN.accountId]], - [false, false, [BAD_INPUT.role, BAD_INPUT.accountId]], - ]; - - it.each( - checkedCircuits, - )('when badRoleId=%s badAccountId=%s', (_badRoleId, _badAccountId, args) => { - // Test protected circuit - expect( - ( - shieldedAccessControl._validateRole as ( - ...args: unknown[] - ) => boolean - )(...args), - ).toBe(false); + describe('should fail', () => { + it('when wit_getRoleCommitmentPath returns a valid path for a different role, accountId pairing', () => { + shieldedAccessControl._grantRole( + OPERATOR_1.role, + OPERATOR_1.accountId, + ); + // Override witness to return valid path for OPERATOR_1 role commitment + shieldedAccessControl.overrideWitness( + 'wit_getRoleCommitmentPath', + () => { + const privateState = shieldedAccessControl.getPrivateState(); + const operator1MtPath = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf( + OPERATOR_1.roleCommitment, + ); + if (operator1MtPath) return [privateState, operator1MtPath]; + throw new Error('Merkle tree path should be defined'); + }, + ); + expect(() => { + shieldedAccessControl.assertOnlyRole(ADMIN.role); + }).toThrow( + 'ShieldedAccessControl: Path must contain leaf matching computed role commitment for the provided role, accountId pairing', + ); }); - it('when role does not exist', () => { - expect( - shieldedAccessControl._validateRole( - UNINITIALIZED.role, - ADMIN.accountId, - ), - ).toBe(false); + it('when caller was never granted the role', () => { + shieldedAccessControl.as(UNAUTHORIZED.publicKey); + expect(() => + shieldedAccessControl.assertOnlyRole(ADMIN.role), + ).toThrow('ShieldedAccessControl: unauthorized account'); }); - it('when revoked role is re-issued to the same accountId', () => { - shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); - shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); + it('when authorized caller has incorrect path', () => { + // Check caller is admin, has admin role + expect( + shieldedAccessControl.getCallerContext().currentZswapLocalState + .coinPublicKey, + ).toEqual(ADMIN.zPublicKey); + expect(shieldedAccessControl.getRoleAdmin(ADMIN.role)).toEqual( + new Uint8Array(ADMIN.role), + ); expect( shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), - ).toBe(false); - }); + ).toBe(true); - it('when role is revoked, ', () => { - shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); - const roleCheck = shieldedAccessControl._validateRole( - ADMIN.role, - ADMIN.accountId, - ); - expect(roleCheck).toBe(false); - }); + // Check nonce is correct + expect( + shieldedAccessControl.privateState.getCurrentSecretNonce( + ADMIN.role, + ), + ).toBe(ADMIN.secretNonce); - it('when invalid witness is provided for a legitimately credentialed user', () => { + // Check path does not match + const truePath = + shieldedAccessControl.privateState.getCommitmentPathWithFindForLeaf( + ADMIN.roleCommitment, + ); shieldedAccessControl.overrideWitness( 'wit_getRoleCommitmentPath', RETURN_BAD_PATH, ); - expect( - shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), - ).toBe(false); + const witnessCalculatedPath = + shieldedAccessControl.privateState.getCommitmentPathWithWitnessImpl( + ADMIN.roleCommitment, + ); + expect(witnessCalculatedPath).not.toEqual(truePath); + + expect(() => + shieldedAccessControl.assertOnlyRole(ADMIN.role), + ).toThrow('ShieldedAccessControl: unauthorized account'); }); - // an invalid witness should not violate the security invariant: revoked roles - // are permanent - it('when an invalid witness is provided for a revoked role', () => { - shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); - shieldedAccessControl.overrideWitness( - 'wit_getRoleCommitmentPath', - RETURN_BAD_PATH, + it('when authorized caller has incorrect nonce', () => { + // Check caller is admin, has admin role + expect( + shieldedAccessControl.getCallerContext().currentZswapLocalState + .coinPublicKey, + ).toEqual(ADMIN.zPublicKey); + expect(shieldedAccessControl.getRoleAdmin(ADMIN.role)).toEqual( + new Uint8Array(ADMIN.role), ); expect( shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), - ).toBe(false); - }); - }); - - describe('should return true', () => { - it('when role is granted', () => { - expect( - shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), - ).toBe(true); - }); - - it('when accountId has multiple roles', () => { - shieldedAccessControl._grantRole(OPERATOR_1.role, ADMIN.accountId); - shieldedAccessControl._grantRole(OPERATOR_2.role, ADMIN.accountId); - shieldedAccessControl._grantRole(OPERATOR_3.role, ADMIN.accountId); - - expect( - shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), - ).toBe(true); - expect( - shieldedAccessControl._validateRole( - OPERATOR_1.role, - ADMIN.accountId, - ), - ).toBe(true); - expect( - shieldedAccessControl._validateRole( - OPERATOR_2.role, - ADMIN.accountId, - ), - ).toBe(true); - expect( - shieldedAccessControl._validateRole( - OPERATOR_3.role, - ADMIN.accountId, - ), - ).toBe(true); - }); - - it('when role is revoked and re-issued with a different accountId', () => { - shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); - - shieldedAccessControl.privateState.injectSecretNonce( - ADMIN.role, - Buffer.alloc(32, 'NEW_ADMIN_NONCE'), - ); - const newAdminAccountId = buildAccountIdHash( - ADMIN.zPublicKey, - shieldedAccessControl.privateState.getCurrentSecretNonce( - ADMIN.role, - ), - ); - expect(newAdminAccountId).not.toEqual(ADMIN.accountId); - - shieldedAccessControl._grantRole(ADMIN.role, newAdminAccountId); - expect( - shieldedAccessControl._validateRole( - ADMIN.role, - newAdminAccountId, - ), - ).toBe(true); - }); - - it('when multiple users have the same role', () => { - // All users will use OPERATOR_1.secretNonce as their nonce value - // when generating their accountId for simplicity - shieldedAccessControl.privateState.injectSecretNonce( - OPERATOR_1.role, - OPERATOR_1.secretNonce, - ); - // A unique accountId must be constructed for each new role using its associated secretNonce - const operator1AdminAccountId = buildAccountIdHash( - ADMIN.zPublicKey, - OPERATOR_1.secretNonce, - ); - shieldedAccessControl._grantRole( - OPERATOR_1.role, - operator1AdminAccountId, - ); - shieldedAccessControl.as(ADMIN.publicKey); // assert ADMIN has OP_1 role - expect( - shieldedAccessControl._validateRole( - OPERATOR_1.role, - operator1AdminAccountId, - ), - ).toBe(true); - - const operator1Op2AccountId = buildAccountIdHash( - OPERATOR_2.zPublicKey, - OPERATOR_1.secretNonce, - ); - shieldedAccessControl._grantRole( - OPERATOR_1.role, - operator1Op2AccountId, - ); - shieldedAccessControl.as(OPERATOR_2.publicKey); // assert OP_2 has OP_1 role - expect( - shieldedAccessControl._validateRole( - OPERATOR_1.role, - operator1Op2AccountId, - ), - ).toBe(true); - - const operator1Op3AccountId = buildAccountIdHash( - OPERATOR_3.zPublicKey, - OPERATOR_1.secretNonce, - ); - shieldedAccessControl._grantRole( - OPERATOR_1.role, - operator1Op3AccountId, - ); - shieldedAccessControl.as(OPERATOR_3.publicKey); // assert OP_3 has OP_1 role - expect( - shieldedAccessControl._validateRole( - OPERATOR_1.role, - operator1Op3AccountId, - ), - ).toBe(true); - }); - }); - }); - - describe('assertOnlyRole', () => { - beforeEach(() => { - shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); - shieldedAccessControl.setPersistentCaller(ADMIN.publicKey); - }); - - describe('should fail', () => { - it('when wit_getRoleCommitmentPath returns a valid path for a different role, accountId pairing', () => { - shieldedAccessControl._grantRole( - OPERATOR_1.role, - OPERATOR_1.accountId, - ); - // Override witness to return valid path for OPERATOR_1 role commitment - shieldedAccessControl.overrideWitness( - 'wit_getRoleCommitmentPath', - () => { - const privateState = shieldedAccessControl.getPrivateState(); - const operator1MtPath = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.findPathForLeaf( - OPERATOR_1.roleCommitment, - ); - if (operator1MtPath) return [privateState, operator1MtPath]; - throw new Error('Merkle tree path should be defined'); - }, - ); - expect(() => { - shieldedAccessControl.assertOnlyRole(ADMIN.role); - }).toThrow( - 'ShieldedAccessControl: Path must contain leaf matching computed role commitment for the provided role, accountId pairing', - ); - }); - - it('when caller was never granted the role', () => { - shieldedAccessControl.as(UNAUTHORIZED.publicKey); - expect(() => - shieldedAccessControl.assertOnlyRole(ADMIN.role), - ).toThrow('ShieldedAccessControl: unauthorized account'); - }); - - it('when authorized caller has incorrect path', () => { - // Check caller is admin, has admin role - expect( - shieldedAccessControl.getCallerContext().currentZswapLocalState - .coinPublicKey, - ).toEqual(ADMIN.zPublicKey); - expect(shieldedAccessControl.getRoleAdmin(ADMIN.role)).toEqual( - new Uint8Array(ADMIN.role), - ); - expect( - shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), - ).toBe(true); - - // Check nonce is correct - expect( - shieldedAccessControl.privateState.getCurrentSecretNonce( - ADMIN.role, - ), - ).toBe(ADMIN.secretNonce); - - // Check path does not match - const truePath = - shieldedAccessControl.privateState.getCommitmentPathWithFindForLeaf( - ADMIN.roleCommitment, - ); - shieldedAccessControl.overrideWitness( - 'wit_getRoleCommitmentPath', - RETURN_BAD_PATH, - ); - const witnessCalculatedPath = - shieldedAccessControl.privateState.getCommitmentPathWithWitnessImpl( - ADMIN.roleCommitment, - ); - expect(witnessCalculatedPath).not.toEqual(truePath); - - expect(() => - shieldedAccessControl.assertOnlyRole(ADMIN.role), - ).toThrow('ShieldedAccessControl: unauthorized account'); - }); - - it('when authorized caller has incorrect nonce', () => { - // Check caller is admin, has admin role - expect( - shieldedAccessControl.getCallerContext().currentZswapLocalState - .coinPublicKey, - ).toEqual(ADMIN.zPublicKey); - expect(shieldedAccessControl.getRoleAdmin(ADMIN.role)).toEqual( - new Uint8Array(ADMIN.role), - ); - expect( - shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), - ).toBe(true); + ).toBe(true); shieldedAccessControl.privateState.injectSecretNonce( ADMIN.role, @@ -742,18 +423,9 @@ describe('ShieldedAccessControl', () => { OPERATOR_3.secretNonce, ); - shieldedAccessControl._grantRole( - OPERATOR_1.role, - operator1AccountId, - ); - shieldedAccessControl._grantRole( - OPERATOR_2.role, - operator2AccountId, - ); - shieldedAccessControl._grantRole( - OPERATOR_3.role, - operator3AccountId, - ); + shieldedAccessControl._grantRole(OPERATOR_1.role, operator1AccountId); + shieldedAccessControl._grantRole(OPERATOR_2.role, operator2AccountId); + shieldedAccessControl._grantRole(OPERATOR_3.role, operator3AccountId); expect(() => { shieldedAccessControl.assertOnlyRole(ADMIN.role); shieldedAccessControl.assertOnlyRole(OPERATOR_1.role); @@ -803,9 +475,7 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl._grantRole(ADMIN.role, newAdminAccountId); expect(() => - shieldedAccessControl.assertOnlyRole( - ADMIN.role, - ) + shieldedAccessControl.assertOnlyRole(ADMIN.role), ).not.toThrow(); }); @@ -853,216 +523,723 @@ describe('ShieldedAccessControl', () => { }); }); - describe('grantRole', () => { + describe('canProveRole', () => { beforeEach(() => { shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); shieldedAccessControl.setPersistentCaller(ADMIN.publicKey); }); - describe('should fail', () => { - it('when caller does not have the admin role', () => { - shieldedAccessControl.as(UNAUTHORIZED.publicKey); - expect(() => - shieldedAccessControl.grantRole( - OPERATOR_1.role, - OPERATOR_1.accountId, - ), - ).toThrow('ShieldedAccessControl: unauthorized account'); - }); + it('should fail when caller provides valid path for a different role, accountId pairing', () => { + shieldedAccessControl._grantRole(OPERATOR_1.role, OPERATOR_1.accountId); + // Override witness to return valid path for OPERATOR_1 role commitment + shieldedAccessControl.overrideWitness( + 'wit_getRoleCommitmentPath', + () => { + const privateState = shieldedAccessControl.getPrivateState(); + const operator1MtPath = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf( + OPERATOR_1.roleCommitment, + ); + if (operator1MtPath) return [privateState, operator1MtPath]; + throw new Error('Merkle tree path should be defined'); + }, + ); + expect(() => { + shieldedAccessControl.canProveRole(ADMIN.role); + }).toThrow( + 'ShieldedAccessControl: Path must contain leaf matching computed role commitment for the provided role, accountId pairing', + ); + }); - it('when wit_getRoleCommitmentPath returns a valid path for a different role, accountId pairing', () => { - shieldedAccessControl._grantRole( - OPERATOR_1.role, - OPERATOR_1.accountId, + describe('should return true', () => { + it('when caller has role', () => { + // Check caller is admin, has admin role + expect( + shieldedAccessControl.getCallerContext().currentZswapLocalState + .coinPublicKey, + ).toEqual(ADMIN.zPublicKey); + expect( + shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), + ).toBe(true); + + expect(shieldedAccessControl.canProveRole(ADMIN.role)).toBe(true); + }); + + it('when caller has multiple roles', () => { + // setup test + shieldedAccessControl.privateState.injectSecretNonce( + OPERATOR_1.role, + OPERATOR_1.secretNonce, ); - // Override witness to return valid path for OPERATOR_1 role commitment - shieldedAccessControl.overrideWitness( - 'wit_getRoleCommitmentPath', - () => { - const privateState = shieldedAccessControl.getPrivateState(); - const operator1MtPath = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.findPathForLeaf( - OPERATOR_1.roleCommitment, - ); - if (operator1MtPath) return [privateState, operator1MtPath]; - throw new Error('Merkle tree path should be defined'); - }, + shieldedAccessControl.privateState.injectSecretNonce( + OPERATOR_2.role, + OPERATOR_2.secretNonce, ); - expect(() => { - shieldedAccessControl.grantRole(ADMIN.role, ADMIN.accountId); - }).toThrow( - 'ShieldedAccessControl: Path must contain leaf matching computed role commitment for the provided role, accountId pairing', + shieldedAccessControl.privateState.injectSecretNonce( + OPERATOR_3.role, + OPERATOR_3.secretNonce, ); - }); - - it('when admin with duplicate roles is revoked', () => { - // create duplicate roles - shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); - shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); + const account1 = buildAccountIdHash( + ADMIN.zPublicKey, + OPERATOR_1.secretNonce, + ); + const account2 = buildAccountIdHash( + ADMIN.zPublicKey, + OPERATOR_2.secretNonce, + ); + const account3 = buildAccountIdHash( + ADMIN.zPublicKey, + OPERATOR_3.secretNonce, + ); + shieldedAccessControl._grantRole(OPERATOR_1.role, account1); + shieldedAccessControl._grantRole(OPERATOR_2.role, account2); + shieldedAccessControl._grantRole(OPERATOR_3.role, account3); - shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); - expect(() => - shieldedAccessControl.grantRole(ADMIN.role, ADMIN.accountId), - ).toThrow('ShieldedAccessControl: unauthorized account'); + expect(shieldedAccessControl.canProveRole(ADMIN.role)).toBe(true); + expect(shieldedAccessControl.canProveRole(OPERATOR_1.role)).toBe( + true, + ); + expect(shieldedAccessControl.canProveRole(OPERATOR_2.role)).toBe( + true, + ); + expect(shieldedAccessControl.canProveRole(OPERATOR_3.role)).toBe( + true, + ); }); - it('when admin role is revoked', () => { + it('when role is revoked and re-issued with a different accountId', () => { shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); - expect(() => - shieldedAccessControl.grantRole(ADMIN.role, ADMIN.accountId), - ).toThrow('ShieldedAccessControl: unauthorized account'); - }); - it('when admin provides incorrect nonce', () => { shieldedAccessControl.privateState.injectSecretNonce( ADMIN.role, - BAD_INPUT.secretNonce, + Buffer.alloc(32, 'NEW_ADMIN_NONCE'), ); - expect( + const newAdminAccountId = buildAccountIdHash( + ADMIN.zPublicKey, shieldedAccessControl.privateState.getCurrentSecretNonce( ADMIN.role, ), - ).not.toEqual(ADMIN.secretNonce); - expect(() => - shieldedAccessControl.grantRole(ADMIN.role, ADMIN.accountId), - ).toThrow('ShieldedAccessControl: unauthorized account'); - }); - - it('when admin provides bad witness path', () => { - shieldedAccessControl.overrideWitness( - 'wit_getRoleCommitmentPath', - RETURN_BAD_PATH, ); - expect(() => - shieldedAccessControl.grantRole(ADMIN.role, ADMIN.accountId), - ).toThrow('ShieldedAccessControl: unauthorized account'); + expect(newAdminAccountId).not.toEqual(ADMIN.accountId); + + shieldedAccessControl._grantRole(ADMIN.role, newAdminAccountId); + expect(shieldedAccessControl.canProveRole(ADMIN.role)).toBe(true); }); - it('when non-admin caller has role', () => { - shieldedAccessControl._grantRole( + it('when multiple users have the same role', () => { + // All users will use OPERATOR_1.secretNonce as their nonce value + // when generating their accountId for simplicity + shieldedAccessControl.privateState.injectSecretNonce( OPERATOR_1.role, - OPERATOR_1.accountId, + OPERATOR_1.secretNonce, + ); + // A unique accountId must be constructed for each new role using its associated secretNonce + const operator1AdminAccountId = buildAccountIdHash( + ADMIN.zPublicKey, + OPERATOR_1.secretNonce, ); - - shieldedAccessControl.as(OPERATOR_1.publicKey); - // OP_1 has role but is not authorized to grant roles to other users - expect(() => - shieldedAccessControl.grantRole( - OPERATOR_1.role, - OPERATOR_2.accountId, - ), - ).toThrow('ShieldedAccessControl: unauthorized account'); - }); - }); - - describe('should not update _operatorRoles Merkle tree', () => { - it('when role is revoked', () => { - // setup test shieldedAccessControl._grantRole( OPERATOR_1.role, - OPERATOR_1.accountId, + operator1AdminAccountId, ); - shieldedAccessControl._revokeRole( - OPERATOR_1.role, - OPERATOR_1.accountId, + shieldedAccessControl.as(ADMIN.publicKey); // prove ADMIN has OP_1 role + expect(shieldedAccessControl.canProveRole(OPERATOR_1.role)).toBe( + true, ); - const initialRoot = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.root(); - shieldedAccessControl.grantRole( + const operator1Op2AccountId = buildAccountIdHash( + OPERATOR_2.zPublicKey, + OPERATOR_1.secretNonce, + ); + shieldedAccessControl._grantRole( OPERATOR_1.role, - OPERATOR_1.accountId, + operator1Op2AccountId, + ); + shieldedAccessControl.as(OPERATOR_2.publicKey); // prove OP_2 has OP_1 role + expect(shieldedAccessControl.canProveRole(OPERATOR_1.role)).toBe( + true, ); - const updatedRoot = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.root(); - expect(initialRoot).toEqual(updatedRoot); - }); - }); - - describe('should grant role', () => { - it('when caller has the admin role', () => { - expect(() => - shieldedAccessControl.grantRole( - OPERATOR_1.role, - OPERATOR_1.accountId, - ), - ).not.toThrow(); - expect( - shieldedAccessControl._validateRole( - OPERATOR_1.role, - OPERATOR_1.accountId, - ), - ).toBe(true); - }); - - it('when caller has custom admin role', () => { - // Make OPERATOR_1.role the admin of OPERATOR_2.role. - shieldedAccessControl._setRoleAdmin( - OPERATOR_2.role, - OPERATOR_1.role, + const operator1Op3AccountId = buildAccountIdHash( + OPERATOR_3.zPublicKey, + OPERATOR_1.secretNonce, ); - // Grant OPERATOR_1.role to OPERATOR_1.accountId - shieldedAccessControl.grantRole( + shieldedAccessControl._grantRole( OPERATOR_1.role, - OPERATOR_1.accountId, + operator1Op3AccountId, + ); + shieldedAccessControl.as(OPERATOR_3.publicKey); // prove OP_3 has OP_1 role + expect(shieldedAccessControl.canProveRole(OPERATOR_1.role)).toBe( + true, ); + }); + }); - // Switch to OPERATOR_1 as caller and inject their nonce for their role. + describe('should return false', () => { + it('when caller does not have role', () => { + // setup test shieldedAccessControl.privateState.injectSecretNonce( OPERATOR_1.role, OPERATOR_1.secretNonce, ); - shieldedAccessControl.setPersistentCaller(OPERATOR_1.publicKey); + const accountId = buildAccountIdHash( + ADMIN.zPublicKey, + OPERATOR_1.secretNonce, + ); - // OPERATOR_1.accountId (who holds OPERATOR_1.role) can now grant OPERATOR_2.role. - expect(() => - shieldedAccessControl.grantRole( - OPERATOR_2.role, - OPERATOR_2.accountId, - ), - ).not.toThrow(); + // Check does not have OPERATOR role expect( - shieldedAccessControl._validateRole( - OPERATOR_2.role, - OPERATOR_2.accountId, - ), - ).toBe(true); + shieldedAccessControl._validateRole(OPERATOR_1.role, accountId), + ).toBe(false); + + expect(shieldedAccessControl.canProveRole(OPERATOR_1.role)).toBe( + false, + ); }); - it('when admin role is revoked and re-issued with a different accountId', () => { - // setup test + it('when caller has revoked role', () => { shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); - const newNonce = Buffer.alloc(32, 'NEW_ADMIN_NONCE'); - shieldedAccessControl.privateState.injectSecretNonce( - ADMIN.role, - newNonce, - ); - const newAccountId = buildAccountIdHash(ADMIN.zPublicKey, newNonce); - shieldedAccessControl._grantRole(ADMIN.role, newAccountId); - expect(() => { - shieldedAccessControl.grantRole( - OPERATOR_1.role, - OPERATOR_1.accountId, - ); - }).not.toThrow(); - expect( - shieldedAccessControl._validateRole( - OPERATOR_1.role, - OPERATOR_1.accountId, - ), - ).toBe(true); + // check role revoked expect( - shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.findPathForLeaf( - OPERATOR_1.roleCommitment, - ), - ).toBeDefined(); + shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), + ).toBe(false); + + expect(shieldedAccessControl.canProveRole(ADMIN.role)).toBe(false); + }); + + it('when revoked role is re-granted', () => { + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); + // check role revoked + expect( + shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), + ).toBe(false); + + shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); + expect(shieldedAccessControl.canProveRole(ADMIN.role)).toBe(false); + }); + + it('when an unauthorized caller has valid nonce', () => { + // UNAUTHORIZED uses the same private state (ADMIN.secretNonce for ADMIN.role), + // so their derived accountId won't match the committed one. + shieldedAccessControl.as(UNAUTHORIZED.publicKey); + expect(shieldedAccessControl.canProveRole(ADMIN.role)).toBe(false); + }); + + it('when an authorized caller provides invalid nonce', () => { + // Check caller is admin, has admin role + expect( + shieldedAccessControl.getCallerContext().currentZswapLocalState + .coinPublicKey, + ).toEqual(ADMIN.zPublicKey); + expect( + shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), + ).toBe(true); + + shieldedAccessControl.privateState.injectSecretNonce( + ADMIN.role, + BAD_INPUT.secretNonce, + ); + // nonce should not match + expect(ADMIN.secretNonce).not.toEqual( + shieldedAccessControl.privateState.getCurrentSecretNonce( + ADMIN.role, + ), + ); + + expect(shieldedAccessControl.canProveRole(ADMIN.role)).toBe(false); + }); + + it('when an authorized caller provides invalid witness path', () => { + // Check caller is admin, has admin role + expect( + shieldedAccessControl.getCallerContext().currentZswapLocalState + .coinPublicKey, + ).toEqual(ADMIN.zPublicKey); + expect( + shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), + ).toBe(true); + + shieldedAccessControl.overrideWitness( + 'wit_getRoleCommitmentPath', + RETURN_BAD_PATH, + ); + expect(shieldedAccessControl.canProveRole(ADMIN.role)).toBe(false); + }); + }); + }); + + describe('_uncheckedCanProveRole', () => { + beforeEach(() => { + shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); + shieldedAccessControl.setPersistentCaller(ADMIN.publicKey); + }); + + it('should fail when caller provides valid path for a different role, accountId pairing', () => { + shieldedAccessControl._grantRole(OPERATOR_1.role, OPERATOR_1.accountId); + // Override witness to return valid path for OPERATOR_1 role commitment + shieldedAccessControl.overrideWitness( + 'wit_getRoleCommitmentPath', + () => { + const privateState = shieldedAccessControl.getPrivateState(); + const operator1MtPath = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf( + OPERATOR_1.roleCommitment, + ); + if (operator1MtPath) return [privateState, operator1MtPath]; + throw new Error('Merkle tree path should be defined'); + }, + ); + expect(() => { + shieldedAccessControl._uncheckedCanProveRole(ADMIN.role); + }).toThrow( + 'ShieldedAccessControl: Path must contain leaf matching computed role commitment for the provided role, accountId pairing', + ); + }); + + describe('should return true', () => { + it('when caller has role', () => { + // Check caller is admin, has admin role + expect( + shieldedAccessControl.getCallerContext().currentZswapLocalState + .coinPublicKey, + ).toEqual(ADMIN.zPublicKey); + expect( + shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), + ).toBe(true); + + expect(shieldedAccessControl._uncheckedCanProveRole(ADMIN.role)).toBe( + true, + ); + }); + + it('when caller has multiple roles', () => { + // setup test + shieldedAccessControl.privateState.injectSecretNonce( + OPERATOR_1.role, + OPERATOR_1.secretNonce, + ); + shieldedAccessControl.privateState.injectSecretNonce( + OPERATOR_2.role, + OPERATOR_2.secretNonce, + ); + shieldedAccessControl.privateState.injectSecretNonce( + OPERATOR_3.role, + OPERATOR_3.secretNonce, + ); + const account1 = buildAccountIdHash( + ADMIN.zPublicKey, + OPERATOR_1.secretNonce, + ); + const account2 = buildAccountIdHash( + ADMIN.zPublicKey, + OPERATOR_2.secretNonce, + ); + const account3 = buildAccountIdHash( + ADMIN.zPublicKey, + OPERATOR_3.secretNonce, + ); + shieldedAccessControl._grantRole(OPERATOR_1.role, account1); + shieldedAccessControl._grantRole(OPERATOR_2.role, account2); + shieldedAccessControl._grantRole(OPERATOR_3.role, account3); + + expect(shieldedAccessControl._uncheckedCanProveRole(ADMIN.role)).toBe( + true, + ); + expect( + shieldedAccessControl._uncheckedCanProveRole(OPERATOR_1.role), + ).toBe(true); + expect( + shieldedAccessControl._uncheckedCanProveRole(OPERATOR_2.role), + ).toBe(true); + expect( + shieldedAccessControl._uncheckedCanProveRole(OPERATOR_3.role), + ).toBe(true); + }); + + it('when role is revoked and re-issued with a different accountId', () => { + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); + + shieldedAccessControl.privateState.injectSecretNonce( + ADMIN.role, + Buffer.alloc(32, 'NEW_ADMIN_NONCE'), + ); + const newAdminAccountId = buildAccountIdHash( + ADMIN.zPublicKey, + shieldedAccessControl.privateState.getCurrentSecretNonce( + ADMIN.role, + ), + ); + expect(newAdminAccountId).not.toEqual(ADMIN.accountId); + + shieldedAccessControl._grantRole(ADMIN.role, newAdminAccountId); + expect(shieldedAccessControl._uncheckedCanProveRole(ADMIN.role)).toBe( + true, + ); + }); + + it('when multiple users have the same role', () => { + // All users will use OPERATOR_1.secretNonce as their nonce value + // when generating their accountId for simplicity + shieldedAccessControl.privateState.injectSecretNonce( + OPERATOR_1.role, + OPERATOR_1.secretNonce, + ); + // A unique accountId must be constructed for each new role using its associated secretNonce + const operator1AdminAccountId = buildAccountIdHash( + ADMIN.zPublicKey, + OPERATOR_1.secretNonce, + ); + shieldedAccessControl._grantRole( + OPERATOR_1.role, + operator1AdminAccountId, + ); + shieldedAccessControl.as(ADMIN.publicKey); // prove ADMIN has OP_1 role + expect( + shieldedAccessControl._uncheckedCanProveRole(OPERATOR_1.role), + ).toBe(true); + + const operator1Op2AccountId = buildAccountIdHash( + OPERATOR_2.zPublicKey, + OPERATOR_1.secretNonce, + ); + shieldedAccessControl._grantRole( + OPERATOR_1.role, + operator1Op2AccountId, + ); + shieldedAccessControl.as(OPERATOR_2.publicKey); // prove OP_2 has OP_1 role + expect( + shieldedAccessControl._uncheckedCanProveRole(OPERATOR_1.role), + ).toBe(true); + + const operator1Op3AccountId = buildAccountIdHash( + OPERATOR_3.zPublicKey, + OPERATOR_1.secretNonce, + ); + shieldedAccessControl._grantRole( + OPERATOR_1.role, + operator1Op3AccountId, + ); + shieldedAccessControl.as(OPERATOR_3.publicKey); // prove OP_3 has OP_1 role + expect( + shieldedAccessControl._uncheckedCanProveRole(OPERATOR_1.role), + ).toBe(true); + }); + }); + + describe('should return false', () => { + it('when caller does not have role', () => { + // setup test + shieldedAccessControl.privateState.injectSecretNonce( + OPERATOR_1.role, + OPERATOR_1.secretNonce, + ); + const accountId = buildAccountIdHash( + ADMIN.zPublicKey, + OPERATOR_1.secretNonce, + ); + + // Check does not have OPERATOR role + expect( + shieldedAccessControl._validateRole(OPERATOR_1.role, accountId), + ).toBe(false); + + expect( + shieldedAccessControl._uncheckedCanProveRole(OPERATOR_1.role), + ).toBe(false); + }); + + it('when caller has revoked role', () => { + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); + + // check role revoked + expect( + shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), + ).toBe(false); + + expect(shieldedAccessControl._uncheckedCanProveRole(ADMIN.role)).toBe( + false, + ); + }); + + it('when revoked role is re-granted', () => { + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); + // check role revoked + expect( + shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), + ).toBe(false); + + shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); + expect(shieldedAccessControl._uncheckedCanProveRole(ADMIN.role)).toBe( + false, + ); + }); + + it('when an unauthorized caller has valid nonce', () => { + // UNAUTHORIZED uses the same private state (ADMIN.secretNonce for ADMIN.role), + // so their derived accountId won't match the committed one. + shieldedAccessControl.as(UNAUTHORIZED.publicKey); + expect(shieldedAccessControl._uncheckedCanProveRole(ADMIN.role)).toBe( + false, + ); + }); + + it('when an authorized caller provides invalid nonce', () => { + // Check caller is admin, has admin role + expect( + shieldedAccessControl.getCallerContext().currentZswapLocalState + .coinPublicKey, + ).toEqual(ADMIN.zPublicKey); + expect( + shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), + ).toBe(true); + + shieldedAccessControl.privateState.injectSecretNonce( + ADMIN.role, + BAD_INPUT.secretNonce, + ); + // nonce should not match + expect(ADMIN.secretNonce).not.toEqual( + shieldedAccessControl.privateState.getCurrentSecretNonce( + ADMIN.role, + ), + ); + + expect(shieldedAccessControl._uncheckedCanProveRole(ADMIN.role)).toBe( + false, + ); + }); + + it('when an authorized caller provides invalid witness path', () => { + // Check caller is admin, has admin role + expect( + shieldedAccessControl.getCallerContext().currentZswapLocalState + .coinPublicKey, + ).toEqual(ADMIN.zPublicKey); + expect( + shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), + ).toBe(true); + + shieldedAccessControl.overrideWitness( + 'wit_getRoleCommitmentPath', + RETURN_BAD_PATH, + ); + expect(shieldedAccessControl._uncheckedCanProveRole(ADMIN.role)).toBe( + false, + ); + }); + }); + }); + + describe('grantRole', () => { + beforeEach(() => { + shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); + shieldedAccessControl.setPersistentCaller(ADMIN.publicKey); + }); + + describe('should fail', () => { + it('when caller does not have the admin role', () => { + shieldedAccessControl.as(UNAUTHORIZED.publicKey); + expect(() => + shieldedAccessControl.grantRole( + OPERATOR_1.role, + OPERATOR_1.accountId, + ), + ).toThrow('ShieldedAccessControl: unauthorized account'); + }); + + it('when wit_getRoleCommitmentPath returns a valid path for a different role, accountId pairing', () => { + shieldedAccessControl._grantRole( + OPERATOR_1.role, + OPERATOR_1.accountId, + ); + // Override witness to return valid path for OPERATOR_1 role commitment + shieldedAccessControl.overrideWitness( + 'wit_getRoleCommitmentPath', + () => { + const privateState = shieldedAccessControl.getPrivateState(); + const operator1MtPath = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf( + OPERATOR_1.roleCommitment, + ); + if (operator1MtPath) return [privateState, operator1MtPath]; + throw new Error('Merkle tree path should be defined'); + }, + ); + expect(() => { + shieldedAccessControl.grantRole(ADMIN.role, ADMIN.accountId); + }).toThrow( + 'ShieldedAccessControl: Path must contain leaf matching computed role commitment for the provided role, accountId pairing', + ); + }); + + it('when admin with duplicate roles is revoked', () => { + // create duplicate roles + shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); + shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); + + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); + expect(() => + shieldedAccessControl.grantRole(ADMIN.role, ADMIN.accountId), + ).toThrow('ShieldedAccessControl: unauthorized account'); + }); + + it('when admin role is revoked', () => { + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); + expect(() => + shieldedAccessControl.grantRole(ADMIN.role, ADMIN.accountId), + ).toThrow('ShieldedAccessControl: unauthorized account'); + }); + + it('when admin provides incorrect nonce', () => { + shieldedAccessControl.privateState.injectSecretNonce( + ADMIN.role, + BAD_INPUT.secretNonce, + ); + expect( + shieldedAccessControl.privateState.getCurrentSecretNonce( + ADMIN.role, + ), + ).not.toEqual(ADMIN.secretNonce); + expect(() => + shieldedAccessControl.grantRole(ADMIN.role, ADMIN.accountId), + ).toThrow('ShieldedAccessControl: unauthorized account'); + }); + + it('when admin provides bad witness path', () => { + shieldedAccessControl.overrideWitness( + 'wit_getRoleCommitmentPath', + RETURN_BAD_PATH, + ); + expect(() => + shieldedAccessControl.grantRole(ADMIN.role, ADMIN.accountId), + ).toThrow('ShieldedAccessControl: unauthorized account'); + }); + + it('when non-admin caller has role', () => { + shieldedAccessControl._grantRole( + OPERATOR_1.role, + OPERATOR_1.accountId, + ); + + shieldedAccessControl.as(OPERATOR_1.publicKey); + // OP_1 has role but is not authorized to grant roles to other users + expect(() => + shieldedAccessControl.grantRole( + OPERATOR_1.role, + OPERATOR_2.accountId, + ), + ).toThrow('ShieldedAccessControl: unauthorized account'); + }); + }); + + describe('should not update _operatorRoles Merkle tree', () => { + it('when role is revoked', () => { + // setup test + shieldedAccessControl._grantRole( + OPERATOR_1.role, + OPERATOR_1.accountId, + ); + shieldedAccessControl._revokeRole( + OPERATOR_1.role, + OPERATOR_1.accountId, + ); + + const initialRoot = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.root(); + shieldedAccessControl.grantRole( + OPERATOR_1.role, + OPERATOR_1.accountId, + ); + + const updatedRoot = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.root(); + expect(initialRoot).toEqual(updatedRoot); + }); + }); + + describe('should grant role', () => { + it('when caller has the admin role', () => { + expect(() => + shieldedAccessControl.grantRole( + OPERATOR_1.role, + OPERATOR_1.accountId, + ), + ).not.toThrow(); + expect( + shieldedAccessControl._validateRole( + OPERATOR_1.role, + OPERATOR_1.accountId, + ), + ).toBe(true); + }); + + it('when caller has custom admin role', () => { + // Make OPERATOR_1.role the admin of OPERATOR_2.role. + shieldedAccessControl._setRoleAdmin(OPERATOR_2.role, OPERATOR_1.role); + // Grant OPERATOR_1.role to OPERATOR_1.accountId + shieldedAccessControl.grantRole( + OPERATOR_1.role, + OPERATOR_1.accountId, + ); + + // Switch to OPERATOR_1 as caller and inject their nonce for their role. + shieldedAccessControl.privateState.injectSecretNonce( + OPERATOR_1.role, + OPERATOR_1.secretNonce, + ); + shieldedAccessControl.setPersistentCaller(OPERATOR_1.publicKey); + + // OPERATOR_1.accountId (who holds OPERATOR_1.role) can now grant OPERATOR_2.role. + expect(() => + shieldedAccessControl.grantRole( + OPERATOR_2.role, + OPERATOR_2.accountId, + ), + ).not.toThrow(); + expect( + shieldedAccessControl._validateRole( + OPERATOR_2.role, + OPERATOR_2.accountId, + ), + ).toBe(true); + }); + + it('when admin role is revoked and re-issued with a different accountId', () => { + // setup test + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); + const newNonce = Buffer.alloc(32, 'NEW_ADMIN_NONCE'); + shieldedAccessControl.privateState.injectSecretNonce( + ADMIN.role, + newNonce, + ); + const newAccountId = buildAccountIdHash(ADMIN.zPublicKey, newNonce); + shieldedAccessControl._grantRole(ADMIN.role, newAccountId); + + expect(() => { + shieldedAccessControl.grantRole( + OPERATOR_1.role, + OPERATOR_1.accountId, + ); + }).not.toThrow(); + expect( + shieldedAccessControl._validateRole( + OPERATOR_1.role, + OPERATOR_1.accountId, + ), + ).toBe(true); + expect( + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__operatorRoles.findPathForLeaf( + OPERATOR_1.roleCommitment, + ), + ).toBeDefined(); }); it('when multiple admins of the same role exist', () => { @@ -1371,13 +1548,121 @@ describe('ShieldedAccessControl', () => { }); }); - describe('revokeRole', () => { + describe('renounceRole', () => { beforeEach(() => { shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); - shieldedAccessControl._grantRole( + shieldedAccessControl.setPersistentCaller(ADMIN.publicKey); + }); + + it('should allow caller to renounce their own role', () => { + expect(() => + shieldedAccessControl.renounceRole(ADMIN.role, ADMIN.accountId), + ).not.toThrow(); + expect( + shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), + ).toBe(false); + }); + + it('should allow caller to renounce role that does not exist', () => { + // Set ADMIN.secretNonce for UNINITIALIZED role so circuit computes ADMIN.accountId + shieldedAccessControl.privateState.injectSecretNonce( + UNINITIALIZED.role, + ADMIN.secretNonce, + ); + expect(() => + shieldedAccessControl.renounceRole( + UNINITIALIZED.role, + ADMIN.accountId, + ), + ).not.toThrow(); + expect( + shieldedAccessControl._validateRole( + UNINITIALIZED.role, + ADMIN.accountId, + ), + ).toBe(false); + }); + + it('should allow caller to renounce a role they do not have', () => { + // Set ADMIN.secretNonce for OPERATOR_1 role so circuit computes ADMIN.accountId + shieldedAccessControl.privateState.injectSecretNonce( OPERATOR_1.role, - OPERATOR_1.accountId, + ADMIN.secretNonce, + ); + expect(() => + shieldedAccessControl.renounceRole(OPERATOR_1.role, ADMIN.accountId), + ).not.toThrow(); + expect( + shieldedAccessControl._validateRole(OPERATOR_1.role, ADMIN.accountId), + ).toBe(false); + }); + + it('should fail when caller provides bad nonce', () => { + shieldedAccessControl.privateState.injectSecretNonce( + ADMIN.role, + BAD_INPUT.secretNonce, ); + + expect(() => + shieldedAccessControl.renounceRole(ADMIN.role, ADMIN.accountId), + ).toThrow('ShieldedAccessControl: bad confirmation'); + }); + + it('should fail when caller provides bad accountId', () => { + expect(() => + shieldedAccessControl.renounceRole(ADMIN.role, BAD_INPUT.accountId), + ).toThrow('ShieldedAccessControl: bad confirmation'); + }); + + it('should fail when unauthorized caller provides valid nonce, and accountId', () => { + // check we have valid secret nonce in private state + expect( + shieldedAccessControl.privateState.getCurrentSecretNonce(ADMIN.role), + ).toEqual(ADMIN.secretNonce); + + shieldedAccessControl.as(UNAUTHORIZED.publicKey); + expect(() => + shieldedAccessControl.renounceRole(ADMIN.role, ADMIN.accountId), + ).toThrow('ShieldedAccessControl: bad confirmation'); + }); + + it('should be a no-op when role is already revoked', () => { + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); + // renounceRole calls _revokeRole internally which silently returns false + // when the role is already revoked — no assertion, so no throw. + expect(() => + shieldedAccessControl.renounceRole(ADMIN.role, ADMIN.accountId), + ).not.toThrow(); + expect( + shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), + ).toBe(false); + }); + + it('should update nullifier set on successful renounce', () => { + const nullifierSetSize = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.size(); + expect(nullifierSetSize).toBe(0n); + + shieldedAccessControl.renounceRole(ADMIN.role, ADMIN.accountId); + const updatedSetSize = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.size(); + expect(updatedSetSize).toEqual(1n); + expect( + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.member( + ADMIN.roleNullifier, + ), + ).toBe(true); + }); + }); + + describe('revokeRole', () => { + beforeEach(() => { + shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); + shieldedAccessControl._grantRole(OPERATOR_1.role, OPERATOR_1.accountId); shieldedAccessControl.setPersistentCaller(ADMIN.publicKey); }); @@ -1534,10 +1819,7 @@ describe('ShieldedAccessControl', () => { OPERATOR_1.secretNonce, ); // OP_1 is admin of OP_2 role - shieldedAccessControl._setRoleAdmin( - OPERATOR_2.role, - OPERATOR_1.role, - ); + shieldedAccessControl._setRoleAdmin(OPERATOR_2.role, OPERATOR_1.role); shieldedAccessControl.as(OPERATOR_1.publicKey); expect(() => @@ -1784,10 +2066,7 @@ describe('ShieldedAccessControl', () => { it('when revoking role with bad accountId', () => { expect( - shieldedAccessControl._revokeRole( - ADMIN.role, - BAD_INPUT.accountId, - ), + shieldedAccessControl._revokeRole(ADMIN.role, BAD_INPUT.accountId), ).toBe(true); }); }); @@ -2067,17 +2346,25 @@ describe('ShieldedAccessControl', () => { shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); }); - describe(`UpdateType.Grant`, () => { + describe('UpdateType.Grant', () => { describe('should return true', () => { it('when granting a new role', () => { expect( - shieldedAccessControl._updateRole(OPERATOR_1.role, OPERATOR_1.accountId, UpdateType.Grant), + shieldedAccessControl._updateRole( + OPERATOR_1.role, + OPERATOR_1.accountId, + UpdateType.Grant, + ), ).toBe(true); }); it('when re-granting an active role', () => { expect( - shieldedAccessControl._updateRole(ADMIN.role, ADMIN.accountId, UpdateType.Grant), + shieldedAccessControl._updateRole( + ADMIN.role, + ADMIN.accountId, + UpdateType.Grant, + ), ).toBe(true); }); }); @@ -2088,7 +2375,11 @@ describe('ShieldedAccessControl', () => { .getPublicState() .ShieldedAccessControl__operatorRoles.root(); - shieldedAccessControl._updateRole(OPERATOR_1.role, OPERATOR_1.accountId, UpdateType.Grant); + shieldedAccessControl._updateRole( + OPERATOR_1.role, + OPERATOR_1.accountId, + UpdateType.Grant, + ); const updatedRoot = shieldedAccessControl .getPublicState() @@ -2097,7 +2388,9 @@ describe('ShieldedAccessControl', () => { const path = shieldedAccessControl .getPublicState() - .ShieldedAccessControl__operatorRoles.findPathForLeaf(OPERATOR_1.roleCommitment); + .ShieldedAccessControl__operatorRoles.findPathForLeaf( + OPERATOR_1.roleCommitment, + ); expect(path).toBeDefined(); expect(path?.leaf).toStrictEqual(OPERATOR_1.roleCommitment); }); @@ -2105,21 +2398,37 @@ describe('ShieldedAccessControl', () => { describe('should return false', () => { it('when granting a revoked role', () => { - shieldedAccessControl._updateRole(ADMIN.role, ADMIN.accountId, UpdateType.Revoke); + shieldedAccessControl._updateRole( + ADMIN.role, + ADMIN.accountId, + UpdateType.Revoke, + ); expect( - shieldedAccessControl._updateRole(ADMIN.role, ADMIN.accountId, UpdateType.Grant), + shieldedAccessControl._updateRole( + ADMIN.role, + ADMIN.accountId, + UpdateType.Grant, + ), ).toBe(false); }); }); describe('should not update _operatorRoles merkle tree', () => { it('when granting a revoked role', () => { - shieldedAccessControl._updateRole(ADMIN.role, ADMIN.accountId, UpdateType.Revoke); + shieldedAccessControl._updateRole( + ADMIN.role, + ADMIN.accountId, + UpdateType.Revoke, + ); const merkleRoot = shieldedAccessControl .getPublicState() .ShieldedAccessControl__operatorRoles.root(); - shieldedAccessControl._updateRole(ADMIN.role, ADMIN.accountId, UpdateType.Grant); + shieldedAccessControl._updateRole( + ADMIN.role, + ADMIN.accountId, + UpdateType.Grant, + ); const newMerkleRoot = shieldedAccessControl .getPublicState() @@ -2133,13 +2442,21 @@ describe('ShieldedAccessControl', () => { describe('should return true', () => { it('when revoking an active role', () => { expect( - shieldedAccessControl._updateRole(ADMIN.role, ADMIN.accountId, UpdateType.Revoke), + shieldedAccessControl._updateRole( + ADMIN.role, + ADMIN.accountId, + UpdateType.Revoke, + ), ).toBe(true); }); it('when revoking a role that does not exist', () => { expect( - shieldedAccessControl._updateRole(UNINITIALIZED.role, UNINITIALIZED.accountId, UpdateType.Revoke), + shieldedAccessControl._updateRole( + UNINITIALIZED.role, + UNINITIALIZED.accountId, + UpdateType.Revoke, + ), ).toBe(true); }); }); @@ -2151,7 +2468,11 @@ describe('ShieldedAccessControl', () => { .ShieldedAccessControl__roleCommitmentNullifiers.size(); expect(initialSetSize).toBe(0n); - shieldedAccessControl._updateRole(ADMIN.role, ADMIN.accountId, UpdateType.Revoke); + shieldedAccessControl._updateRole( + ADMIN.role, + ADMIN.accountId, + UpdateType.Revoke, + ); const updatedSetSize = shieldedAccessControl .getPublicState() @@ -2160,50 +2481,163 @@ describe('ShieldedAccessControl', () => { expect( shieldedAccessControl .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.member(ADMIN.roleNullifier), + .ShieldedAccessControl__roleCommitmentNullifiers.member( + ADMIN.roleNullifier, + ), ).toBe(true); }); }); describe('should return false', () => { it('when re-revoking an already revoked role', () => { - shieldedAccessControl._updateRole(ADMIN.role, ADMIN.accountId, UpdateType.Revoke); + shieldedAccessControl._updateRole( + ADMIN.role, + ADMIN.accountId, + UpdateType.Revoke, + ); expect( - shieldedAccessControl._updateRole(ADMIN.role, ADMIN.accountId, UpdateType.Revoke), + shieldedAccessControl._updateRole( + ADMIN.role, + ADMIN.accountId, + UpdateType.Revoke, + ), ).toBe(false); }); }); describe('should not update nullifier set', () => { it('when re-revoking an already revoked role', () => { - shieldedAccessControl._updateRole(ADMIN.role, ADMIN.accountId, UpdateType.Revoke); + shieldedAccessControl._updateRole( + ADMIN.role, + ADMIN.accountId, + UpdateType.Revoke, + ); const initialSetSize = shieldedAccessControl .getPublicState() .ShieldedAccessControl__roleCommitmentNullifiers.size(); expect(initialSetSize).toBe(1n); - shieldedAccessControl._updateRole(ADMIN.role, ADMIN.accountId, UpdateType.Revoke); + shieldedAccessControl._updateRole( + ADMIN.role, + ADMIN.accountId, + UpdateType.Revoke, + ); + + const updatedSetSize = shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__roleCommitmentNullifiers.size(); + expect(updatedSetSize).toEqual(initialSetSize); + }); + }); + }); + }); + + describe('getRoleAdmin', () => { + it('should return zero bytes (DEFAULT_ADMIN_ROLE) for a role with no admin set', () => { + expect( + shieldedAccessControl.getRoleAdmin(OPERATOR_1.role), + ).toStrictEqual(new Uint8Array(32)); + expect( + shieldedAccessControl.getRoleAdmin(OPERATOR_1.role), + ).toStrictEqual(shieldedAccessControl.DEFAULT_ADMIN_ROLE()); + }); + + it('should return the admin role after _setRoleAdmin', () => { + shieldedAccessControl._setRoleAdmin(OPERATOR_1.role, ADMIN.role); + expect(shieldedAccessControl.getRoleAdmin(OPERATOR_1.role)).toEqual( + new Uint8Array(ADMIN.role), + ); + }); + }); + + describe('_setRoleAdmin', () => { + it('should set admin role', () => { + shieldedAccessControl._setRoleAdmin(OPERATOR_1.role, ADMIN.role); + expect(shieldedAccessControl.getRoleAdmin(OPERATOR_1.role)).toEqual( + new Uint8Array(ADMIN.role), + ); + }); + + it('should update _adminRoles map', () => { + expect( + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__adminRoles.isEmpty(), + ).toBe(true); + + // setup test + shieldedAccessControl._setRoleAdmin(OPERATOR_1.role, ADMIN.role); + shieldedAccessControl._setRoleAdmin(OPERATOR_2.role, ADMIN.role); + shieldedAccessControl._setRoleAdmin(OPERATOR_3.role, ADMIN.role); + + // check updated state + expect( + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__adminRoles.isEmpty(), + ).toBe(false); + expect( + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__adminRoles.size(), + ).toBe(3n); + + // check new values exist + expect( + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__adminRoles.member(OPERATOR_1.role), + ).toBe(true); + expect( + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__adminRoles.lookup(OPERATOR_1.role), + ).toEqual(new Uint8Array(ADMIN.role)); + + expect( + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__adminRoles.member(OPERATOR_2.role), + ).toBe(true); + expect( + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__adminRoles.lookup(OPERATOR_2.role), + ).toEqual(new Uint8Array(ADMIN.role)); + + expect( + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__adminRoles.member(OPERATOR_3.role), + ).toBe(true); + expect( + shieldedAccessControl + .getPublicState() + .ShieldedAccessControl__adminRoles.lookup(OPERATOR_3.role), + ).toEqual(new Uint8Array(ADMIN.role)); + }); - const updatedSetSize = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.size(); - expect(updatedSetSize).toEqual(initialSetSize); - }); - }); + it('should override an existing admin role', () => { + shieldedAccessControl._setRoleAdmin(OPERATOR_1.role, ADMIN.role); + expect(shieldedAccessControl.getRoleAdmin(OPERATOR_1.role)).toEqual( + new Uint8Array(ADMIN.role), + ); + + shieldedAccessControl._setRoleAdmin(OPERATOR_1.role, OPERATOR_2.role); + expect(shieldedAccessControl.getRoleAdmin(OPERATOR_1.role)).toEqual( + new Uint8Array(OPERATOR_2.role), + ); }); }); - describe('canProveRole', () => { + describe('_validateRole', () => { beforeEach(() => { shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); shieldedAccessControl.setPersistentCaller(ADMIN.publicKey); }); - it('should fail when caller provides valid path for a different role, accountId pairing', () => { - shieldedAccessControl._grantRole( - OPERATOR_1.role, - OPERATOR_1.accountId, - ); + it('should fail when wit_getRoleCommitmentPath returns a valid path for a different role, accountId pairing', () => { + shieldedAccessControl._grantRole(OPERATOR_1.role, OPERATOR_1.accountId); // Override witness to return valid path for OPERATOR_1 role commitment shieldedAccessControl.overrideWitness( 'wit_getRoleCommitmentPath', @@ -2219,336 +2653,120 @@ describe('ShieldedAccessControl', () => { }, ); expect(() => { - shieldedAccessControl.canProveRole(ADMIN.role); + shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId); }).toThrow( 'ShieldedAccessControl: Path must contain leaf matching computed role commitment for the provided role, accountId pairing', ); }); - describe('should return true', () => { - it('when caller has role', () => { - // Check caller is admin, has admin role - expect( - shieldedAccessControl.getCallerContext().currentZswapLocalState - .coinPublicKey, - ).toEqual(ADMIN.zPublicKey); - expect( - shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), - ).toBe(true); - - expect(shieldedAccessControl.canProveRole(ADMIN.role)).toBe( - true, - ); - }); - - it('when caller has multiple roles', () => { - // setup test - shieldedAccessControl.privateState.injectSecretNonce( - OPERATOR_1.role, - OPERATOR_1.secretNonce, - ); - shieldedAccessControl.privateState.injectSecretNonce( - OPERATOR_2.role, - OPERATOR_2.secretNonce, - ); - shieldedAccessControl.privateState.injectSecretNonce( - OPERATOR_3.role, - OPERATOR_3.secretNonce, - ); - const account1 = buildAccountIdHash( - ADMIN.zPublicKey, - OPERATOR_1.secretNonce, - ); - const account2 = buildAccountIdHash( - ADMIN.zPublicKey, - OPERATOR_2.secretNonce, - ); - const account3 = buildAccountIdHash( - ADMIN.zPublicKey, - OPERATOR_3.secretNonce, - ); - shieldedAccessControl._grantRole(OPERATOR_1.role, account1); - shieldedAccessControl._grantRole(OPERATOR_2.role, account2); - shieldedAccessControl._grantRole(OPERATOR_3.role, account3); - - expect(shieldedAccessControl.canProveRole(ADMIN.role)).toBe( - true, - ); - expect(shieldedAccessControl.canProveRole(OPERATOR_1.role)).toBe( - true, - ); - expect(shieldedAccessControl.canProveRole(OPERATOR_2.role)).toBe( - true, - ); - expect(shieldedAccessControl.canProveRole(OPERATOR_3.role)).toBe( - true, - ); - }); - - it('when role is revoked and re-issued with a different accountId', () => { - shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); - - shieldedAccessControl.privateState.injectSecretNonce( - ADMIN.role, - Buffer.alloc(32, 'NEW_ADMIN_NONCE'), - ); - const newAdminAccountId = buildAccountIdHash( - ADMIN.zPublicKey, - shieldedAccessControl.privateState.getCurrentSecretNonce( - ADMIN.role, - ), - ); - expect(newAdminAccountId).not.toEqual(ADMIN.accountId); - - shieldedAccessControl._grantRole(ADMIN.role, newAdminAccountId); - expect(shieldedAccessControl.canProveRole(ADMIN.role)).toBe( - true, - ); - }); - - it('when multiple users have the same role', () => { - // All users will use OPERATOR_1.secretNonce as their nonce value - // when generating their accountId for simplicity - shieldedAccessControl.privateState.injectSecretNonce( - OPERATOR_1.role, - OPERATOR_1.secretNonce, - ); - // A unique accountId must be constructed for each new role using its associated secretNonce - const operator1AdminAccountId = buildAccountIdHash( - ADMIN.zPublicKey, - OPERATOR_1.secretNonce, - ); - shieldedAccessControl._grantRole( - OPERATOR_1.role, - operator1AdminAccountId, - ); - shieldedAccessControl.as(ADMIN.publicKey); // prove ADMIN has OP_1 role - expect(shieldedAccessControl.canProveRole(OPERATOR_1.role)).toBe( - true, - ); - - const operator1Op2AccountId = buildAccountIdHash( - OPERATOR_2.zPublicKey, - OPERATOR_1.secretNonce, - ); - shieldedAccessControl._grantRole( - OPERATOR_1.role, - operator1Op2AccountId, - ); - shieldedAccessControl.as(OPERATOR_2.publicKey); // prove OP_2 has OP_1 role - expect(shieldedAccessControl.canProveRole(OPERATOR_1.role)).toBe( - true, - ); - - const operator1Op3AccountId = buildAccountIdHash( - OPERATOR_3.zPublicKey, - OPERATOR_1.secretNonce, - ); - shieldedAccessControl._grantRole( - OPERATOR_1.role, - operator1Op3AccountId, - ); - shieldedAccessControl.as(OPERATOR_3.publicKey); // prove OP_3 has OP_1 role - expect(shieldedAccessControl.canProveRole(OPERATOR_1.role)).toBe( - true, - ); - }); - }); - describe('should return false', () => { - it('when caller does not have role', () => { - // setup test - shieldedAccessControl.privateState.injectSecretNonce( - OPERATOR_1.role, - OPERATOR_1.secretNonce, - ); - const accountId = buildAccountIdHash( - ADMIN.zPublicKey, - OPERATOR_1.secretNonce, - ); + type CheckRoleCases = [ + badRoleId: boolean, + badAccountId: boolean, + args: unknown[], + ]; + const checkedCircuits: CheckRoleCases[] = [ + [false, true, [ADMIN.role, BAD_INPUT.accountId]], + [true, false, [BAD_INPUT.role, ADMIN.accountId]], + [false, false, [BAD_INPUT.role, BAD_INPUT.accountId]], + ]; - // Check does not have OPERATOR role + it.each( + checkedCircuits, + )('when badRoleId=%s badAccountId=%s', (_badRoleId, _badAccountId, args) => { + // Test protected circuit expect( - shieldedAccessControl._validateRole(OPERATOR_1.role, accountId), + ( + shieldedAccessControl._validateRole as ( + ...args: unknown[] + ) => boolean + )(...args), ).toBe(false); - - expect(shieldedAccessControl.canProveRole(OPERATOR_1.role)).toBe( - false, - ); }); - it('when caller has revoked role', () => { - shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); - - // check role revoked + it('when role does not exist', () => { expect( - shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), + shieldedAccessControl._validateRole( + UNINITIALIZED.role, + ADMIN.accountId, + ), ).toBe(false); - - expect(shieldedAccessControl.canProveRole(ADMIN.role)).toBe( - false, - ); }); - it('when revoked role is re-granted', () => { + it('when revoked role is re-issued to the same accountId', () => { shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); - // check role revoked + shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); expect( shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), ).toBe(false); - - shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); - expect(shieldedAccessControl.canProveRole(ADMIN.role)).toBe( - false, - ); - }); - - it('when an unauthorized caller has valid nonce', () => { - // UNAUTHORIZED uses the same private state (ADMIN.secretNonce for ADMIN.role), - // so their derived accountId won't match the committed one. - shieldedAccessControl.as(UNAUTHORIZED.publicKey); - expect(shieldedAccessControl.canProveRole(ADMIN.role)).toBe( - false, - ); }); - it('when an authorized caller provides invalid nonce', () => { - // Check caller is admin, has admin role - expect( - shieldedAccessControl.getCallerContext().currentZswapLocalState - .coinPublicKey, - ).toEqual(ADMIN.zPublicKey); - expect( - shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), - ).toBe(true); - - shieldedAccessControl.privateState.injectSecretNonce( + it('when role is revoked, ', () => { + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); + const roleCheck = shieldedAccessControl._validateRole( ADMIN.role, - BAD_INPUT.secretNonce, - ); - // nonce should not match - expect(ADMIN.secretNonce).not.toEqual( - shieldedAccessControl.privateState.getCurrentSecretNonce( - ADMIN.role, - ), + ADMIN.accountId, ); + expect(roleCheck).toBe(false); + }); - expect(shieldedAccessControl.canProveRole(ADMIN.role)).toBe( - false, + it('when invalid witness is provided for a legitimately credentialed user', () => { + shieldedAccessControl.overrideWitness( + 'wit_getRoleCommitmentPath', + RETURN_BAD_PATH, ); - }); - - it('when an authorized caller provides invalid witness path', () => { - // Check caller is admin, has admin role - expect( - shieldedAccessControl.getCallerContext().currentZswapLocalState - .coinPublicKey, - ).toEqual(ADMIN.zPublicKey); expect( shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), - ).toBe(true); + ).toBe(false); + }); + // an invalid witness should not violate the security invariant: revoked roles + // are permanent + it('when an invalid witness is provided for a revoked role', () => { + shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); shieldedAccessControl.overrideWitness( 'wit_getRoleCommitmentPath', RETURN_BAD_PATH, ); - expect(shieldedAccessControl.canProveRole(ADMIN.role)).toBe( - false, - ); + expect( + shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), + ).toBe(false); }); }); - }); - - describe('_uncheckedCanProveRole', () => { - beforeEach(() => { - shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); - shieldedAccessControl.setPersistentCaller(ADMIN.publicKey); - }); - - it('should fail when caller provides valid path for a different role, accountId pairing', () => { - shieldedAccessControl._grantRole( - OPERATOR_1.role, - OPERATOR_1.accountId, - ); - // Override witness to return valid path for OPERATOR_1 role commitment - shieldedAccessControl.overrideWitness( - 'wit_getRoleCommitmentPath', - () => { - const privateState = shieldedAccessControl.getPrivateState(); - const operator1MtPath = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__operatorRoles.findPathForLeaf( - OPERATOR_1.roleCommitment, - ); - if (operator1MtPath) return [privateState, operator1MtPath]; - throw new Error('Merkle tree path should be defined'); - }, - ); - expect(() => { - shieldedAccessControl._uncheckedCanProveRole(ADMIN.role); - }).toThrow( - 'ShieldedAccessControl: Path must contain leaf matching computed role commitment for the provided role, accountId pairing', - ); - }); describe('should return true', () => { - it('when caller has role', () => { - // Check caller is admin, has admin role - expect( - shieldedAccessControl.getCallerContext().currentZswapLocalState - .coinPublicKey, - ).toEqual(ADMIN.zPublicKey); + it('when role is granted', () => { expect( shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), ).toBe(true); - - expect(shieldedAccessControl._uncheckedCanProveRole(ADMIN.role)).toBe( - true, - ); }); - it('when caller has multiple roles', () => { - // setup test - shieldedAccessControl.privateState.injectSecretNonce( - OPERATOR_1.role, - OPERATOR_1.secretNonce, - ); - shieldedAccessControl.privateState.injectSecretNonce( - OPERATOR_2.role, - OPERATOR_2.secretNonce, - ); - shieldedAccessControl.privateState.injectSecretNonce( - OPERATOR_3.role, - OPERATOR_3.secretNonce, - ); - const account1 = buildAccountIdHash( - ADMIN.zPublicKey, - OPERATOR_1.secretNonce, - ); - const account2 = buildAccountIdHash( - ADMIN.zPublicKey, - OPERATOR_2.secretNonce, - ); - const account3 = buildAccountIdHash( - ADMIN.zPublicKey, - OPERATOR_3.secretNonce, - ); - shieldedAccessControl._grantRole(OPERATOR_1.role, account1); - shieldedAccessControl._grantRole(OPERATOR_2.role, account2); - shieldedAccessControl._grantRole(OPERATOR_3.role, account3); + it('when accountId has multiple roles', () => { + shieldedAccessControl._grantRole(OPERATOR_1.role, ADMIN.accountId); + shieldedAccessControl._grantRole(OPERATOR_2.role, ADMIN.accountId); + shieldedAccessControl._grantRole(OPERATOR_3.role, ADMIN.accountId); - expect(shieldedAccessControl._uncheckedCanProveRole(ADMIN.role)).toBe( - true, - ); - expect(shieldedAccessControl._uncheckedCanProveRole(OPERATOR_1.role)).toBe( - true, - ); - expect(shieldedAccessControl._uncheckedCanProveRole(OPERATOR_2.role)).toBe( - true, - ); - expect(shieldedAccessControl._uncheckedCanProveRole(OPERATOR_3.role)).toBe( - true, - ); + expect( + shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), + ).toBe(true); + expect( + shieldedAccessControl._validateRole( + OPERATOR_1.role, + ADMIN.accountId, + ), + ).toBe(true); + expect( + shieldedAccessControl._validateRole( + OPERATOR_2.role, + ADMIN.accountId, + ), + ).toBe(true); + expect( + shieldedAccessControl._validateRole( + OPERATOR_3.role, + ADMIN.accountId, + ), + ).toBe(true); }); it('when role is revoked and re-issued with a different accountId', () => { @@ -2567,9 +2785,9 @@ describe('ShieldedAccessControl', () => { expect(newAdminAccountId).not.toEqual(ADMIN.accountId); shieldedAccessControl._grantRole(ADMIN.role, newAdminAccountId); - expect(shieldedAccessControl._uncheckedCanProveRole(ADMIN.role)).toBe( - true, - ); + expect( + shieldedAccessControl._validateRole(ADMIN.role, newAdminAccountId), + ).toBe(true); }); it('when multiple users have the same role', () => { @@ -2588,10 +2806,13 @@ describe('ShieldedAccessControl', () => { OPERATOR_1.role, operator1AdminAccountId, ); - shieldedAccessControl.as(ADMIN.publicKey); // prove ADMIN has OP_1 role - expect(shieldedAccessControl._uncheckedCanProveRole(OPERATOR_1.role)).toBe( - true, - ); + shieldedAccessControl.as(ADMIN.publicKey); // assert ADMIN has OP_1 role + expect( + shieldedAccessControl._validateRole( + OPERATOR_1.role, + operator1AdminAccountId, + ), + ).toBe(true); const operator1Op2AccountId = buildAccountIdHash( OPERATOR_2.zPublicKey, @@ -2601,10 +2822,13 @@ describe('ShieldedAccessControl', () => { OPERATOR_1.role, operator1Op2AccountId, ); - shieldedAccessControl.as(OPERATOR_2.publicKey); // prove OP_2 has OP_1 role - expect(shieldedAccessControl._uncheckedCanProveRole(OPERATOR_1.role)).toBe( - true, - ); + shieldedAccessControl.as(OPERATOR_2.publicKey); // assert OP_2 has OP_1 role + expect( + shieldedAccessControl._validateRole( + OPERATOR_1.role, + operator1Op2AccountId, + ), + ).toBe(true); const operator1Op3AccountId = buildAccountIdHash( OPERATOR_3.zPublicKey, @@ -2614,334 +2838,137 @@ describe('ShieldedAccessControl', () => { OPERATOR_1.role, operator1Op3AccountId, ); - shieldedAccessControl.as(OPERATOR_3.publicKey); // prove OP_3 has OP_1 role - expect(shieldedAccessControl._uncheckedCanProveRole(OPERATOR_1.role)).toBe( - true, - ); - }); - }); - - describe('should return false', () => { - it('when caller does not have role', () => { - // setup test - shieldedAccessControl.privateState.injectSecretNonce( - OPERATOR_1.role, - OPERATOR_1.secretNonce, - ); - const accountId = buildAccountIdHash( - ADMIN.zPublicKey, - OPERATOR_1.secretNonce, - ); - - // Check does not have OPERATOR role - expect( - shieldedAccessControl._validateRole(OPERATOR_1.role, accountId), - ).toBe(false); - - expect(shieldedAccessControl._uncheckedCanProveRole(OPERATOR_1.role)).toBe( - false, - ); - }); - - it('when caller has revoked role', () => { - shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); - - // check role revoked - expect( - shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), - ).toBe(false); - - expect(shieldedAccessControl._uncheckedCanProveRole(ADMIN.role)).toBe( - false, - ); - }); - - it('when revoked role is re-granted', () => { - shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); - // check role revoked - expect( - shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), - ).toBe(false); - - shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); - expect(shieldedAccessControl._uncheckedCanProveRole(ADMIN.role)).toBe( - false, - ); - }); - - it('when an unauthorized caller has valid nonce', () => { - // UNAUTHORIZED uses the same private state (ADMIN.secretNonce for ADMIN.role), - // so their derived accountId won't match the committed one. - shieldedAccessControl.as(UNAUTHORIZED.publicKey); - expect(shieldedAccessControl._uncheckedCanProveRole(ADMIN.role)).toBe( - false, - ); - }); - - it('when an authorized caller provides invalid nonce', () => { - // Check caller is admin, has admin role - expect( - shieldedAccessControl.getCallerContext().currentZswapLocalState - .coinPublicKey, - ).toEqual(ADMIN.zPublicKey); - expect( - shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), - ).toBe(true); - - shieldedAccessControl.privateState.injectSecretNonce( - ADMIN.role, - BAD_INPUT.secretNonce, - ); - // nonce should not match - expect(ADMIN.secretNonce).not.toEqual( - shieldedAccessControl.privateState.getCurrentSecretNonce( - ADMIN.role, - ), - ); - - expect(shieldedAccessControl._uncheckedCanProveRole(ADMIN.role)).toBe( - false, - ); - }); - - it('when an authorized caller provides invalid witness path', () => { - // Check caller is admin, has admin role - expect( - shieldedAccessControl.getCallerContext().currentZswapLocalState - .coinPublicKey, - ).toEqual(ADMIN.zPublicKey); + shieldedAccessControl.as(OPERATOR_3.publicKey); // assert OP_3 has OP_1 role expect( - shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), - ).toBe(true); - - shieldedAccessControl.overrideWitness( - 'wit_getRoleCommitmentPath', - RETURN_BAD_PATH, - ); - expect(shieldedAccessControl._uncheckedCanProveRole(ADMIN.role)).toBe( - false, - ); + shieldedAccessControl._validateRole( + OPERATOR_1.role, + operator1Op3AccountId, + ), + ).toBe(true); }); }); }); - describe('getRoleAdmin', () => { - it('should return zero bytes (DEFAULT_ADMIN_ROLE) for a role with no admin set', () => { - expect(shieldedAccessControl.getRoleAdmin(OPERATOR_1.role)).toStrictEqual( - new Uint8Array(32), - ); - expect(shieldedAccessControl.getRoleAdmin(OPERATOR_1.role)).toStrictEqual( - shieldedAccessControl.DEFAULT_ADMIN_ROLE(), - ); - }); - - it('should return the admin role after _setRoleAdmin', () => { - shieldedAccessControl._setRoleAdmin(OPERATOR_1.role, ADMIN.role); - expect(shieldedAccessControl.getRoleAdmin(OPERATOR_1.role)).toEqual( - new Uint8Array(ADMIN.role), - ); - }); - }); - - describe('_setRoleAdmin', () => { - it('should set admin role', () => { - shieldedAccessControl._setRoleAdmin(OPERATOR_1.role, ADMIN.role); - expect(shieldedAccessControl.getRoleAdmin(OPERATOR_1.role)).toEqual( - new Uint8Array(ADMIN.role), - ); - }); - - it('should update _adminRoles map', () => { + describe('computeRoleCommitment', () => { + it('should match computed commitment', () => { expect( - shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__adminRoles.isEmpty(), - ).toBe(true); - - // setup test - shieldedAccessControl._setRoleAdmin(OPERATOR_1.role, ADMIN.role); - shieldedAccessControl._setRoleAdmin(OPERATOR_2.role, ADMIN.role); - shieldedAccessControl._setRoleAdmin(OPERATOR_3.role, ADMIN.role); + shieldedAccessControl.computeRoleCommitment( + ADMIN.role, + ADMIN.accountId, + ), + ).toEqual(ADMIN.roleCommitment); + }); - // check updated state - expect( - shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__adminRoles.isEmpty(), - ).toBe(false); - expect( - shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__adminRoles.size(), - ).toBe(3n); + type ComputeCommitmentCases = [ + isValidRoleId: boolean, + isValidAccountId: boolean, + args: unknown[], + ]; - // check new values exist - expect( - shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__adminRoles.member(OPERATOR_1.role), - ).toBe(true); - expect( - shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__adminRoles.lookup(OPERATOR_1.role), - ).toEqual(new Uint8Array(ADMIN.role)); + const checkedCircuits: ComputeCommitmentCases[] = [ + [false, true, [BAD_INPUT.role, ADMIN.accountId]], + [true, false, [ADMIN.role, BAD_INPUT.accountId]], + [false, false, [BAD_INPUT.role, BAD_INPUT.accountId]], + ]; + it.each( + checkedCircuits, + )('should not compute commitment with isValidRoleId=%s, isValidAccountId=%s', (_isValidRoleId, _isValidAccountId, args) => { + // Test protected circuit expect( - shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__adminRoles.member(OPERATOR_2.role), - ).toBe(true); - expect( - shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__adminRoles.lookup(OPERATOR_2.role), - ).toEqual(new Uint8Array(ADMIN.role)); + ( + shieldedAccessControl.computeRoleCommitment as ( + ...args: unknown[] + ) => Uint8Array + )(...args), + ).not.toEqual(ADMIN.roleCommitment); + }); + }); + describe('computeNullifier', () => { + it('should match nullifier', () => { expect( - shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__adminRoles.member(OPERATOR_3.role), - ).toBe(true); - expect( - shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__adminRoles.lookup(OPERATOR_3.role), - ).toEqual(new Uint8Array(ADMIN.role)); + shieldedAccessControl.computeNullifier(ADMIN.roleCommitment), + ).toEqual(ADMIN.roleNullifier); }); - it('should override an existing admin role', () => { - shieldedAccessControl._setRoleAdmin(OPERATOR_1.role, ADMIN.role); - expect(shieldedAccessControl.getRoleAdmin(OPERATOR_1.role)).toEqual( - new Uint8Array(ADMIN.role), - ); - - shieldedAccessControl._setRoleAdmin( - OPERATOR_1.role, - OPERATOR_2.role, - ); - expect(shieldedAccessControl.getRoleAdmin(OPERATOR_1.role)).toEqual( - new Uint8Array(OPERATOR_2.role), - ); + it('should not match bad commitment inputs', () => { + expect( + shieldedAccessControl.computeNullifier(BAD_INPUT.roleCommitment), + ).not.toEqual(ADMIN.roleNullifier); }); }); - describe('renounceRole', () => { + describe('_computeAccountId', () => { beforeEach(() => { - shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); shieldedAccessControl.setPersistentCaller(ADMIN.publicKey); }); - it('should allow caller to renounce their own role', () => { - expect(() => - shieldedAccessControl.renounceRole(ADMIN.role, ADMIN.accountId), - ).not.toThrow(); - expect( - shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), - ).toBe(false); - }); - - it('should allow caller to renounce role that does not exist', () => { - // Set ADMIN.secretNonce for UNINITIALIZED role so circuit computes ADMIN.accountId - shieldedAccessControl.privateState.injectSecretNonce( - UNINITIALIZED.role, - ADMIN.secretNonce, - ); - expect(() => - shieldedAccessControl.renounceRole( - UNINITIALIZED.role, - ADMIN.accountId, - ), - ).not.toThrow(); - expect( - shieldedAccessControl._validateRole( - UNINITIALIZED.role, - ADMIN.accountId, - ), - ).toBe(false); - }); - - it('should allow caller to renounce a role they do not have', () => { - // Set ADMIN.secretNonce for OPERATOR_1 role so circuit computes ADMIN.accountId - shieldedAccessControl.privateState.injectSecretNonce( - OPERATOR_1.role, - ADMIN.secretNonce, + it('should match when authorized caller with correct nonce', () => { + expect(shieldedAccessControl._computeAccountId(ADMIN.role)).toEqual( + ADMIN.accountId, ); - expect(() => - shieldedAccessControl.renounceRole( - OPERATOR_1.role, - ADMIN.accountId, - ), - ).not.toThrow(); - expect( - shieldedAccessControl._validateRole( - OPERATOR_1.role, - ADMIN.accountId, - ), - ).toBe(false); }); - it('should fail when caller provides bad nonce', () => { + it('should not match when authorized caller with bad nonce', () => { shieldedAccessControl.privateState.injectSecretNonce( ADMIN.role, BAD_INPUT.secretNonce, ); - - expect(() => - shieldedAccessControl.renounceRole(ADMIN.role, ADMIN.accountId), - ).toThrow('ShieldedAccessControl: bad confirmation'); + const computedAccountId = shieldedAccessControl._computeAccountId( + ADMIN.role, + ); + expect(computedAccountId).not.toEqual(ADMIN.accountId); + expect(computedAccountId).toEqual( + buildAccountIdHash(ADMIN.zPublicKey, BAD_INPUT.secretNonce), + ); }); - it('should fail when caller provides bad accountId', () => { - expect(() => - shieldedAccessControl.renounceRole(ADMIN.role, BAD_INPUT.accountId), - ).toThrow('ShieldedAccessControl: bad confirmation'); + it('should not match when unauthorized caller with correct nonce', () => { + shieldedAccessControl.as(UNAUTHORIZED.publicKey); + const computedAccountId = shieldedAccessControl._computeAccountId( + ADMIN.role, + ); + expect(computedAccountId).not.toEqual(ADMIN.accountId); + expect(computedAccountId).toEqual( + buildAccountIdHash(UNAUTHORIZED.zPublicKey, ADMIN.secretNonce), + ); }); + }); - it('should fail when unauthorized caller provides valid nonce, and accountId', () => { - // check we have valid secret nonce in private state + describe('computeAccountId', () => { + it('should match when given correct account and nonce', () => { expect( - shieldedAccessControl.privateState.getCurrentSecretNonce( - ADMIN.role, + shieldedAccessControl.computeAccountId( + ADMIN.zPublicKey, + ADMIN.secretNonce, + INSTANCE_SALT, ), - ).toEqual(ADMIN.secretNonce); - - shieldedAccessControl.as(UNAUTHORIZED.publicKey); - expect(() => - shieldedAccessControl.renounceRole(ADMIN.role, ADMIN.accountId), - ).toThrow('ShieldedAccessControl: bad confirmation'); + ).toEqual(ADMIN.accountId); }); - it('should be a no-op when role is already revoked', () => { - shieldedAccessControl._revokeRole(ADMIN.role, ADMIN.accountId); - // renounceRole calls _revokeRole internally which silently returns false - // when the role is already revoked — no assertion, so no throw. - expect(() => - shieldedAccessControl.renounceRole(ADMIN.role, ADMIN.accountId), - ).not.toThrow(); - expect( - shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), - ).toBe(false); + it('should not match when given correct account with bad nonce', () => { + const computedAccountId = shieldedAccessControl.computeAccountId( + ADMIN.zPublicKey, + BAD_INPUT.secretNonce, + INSTANCE_SALT, + ); + expect(computedAccountId).not.toEqual(ADMIN.accountId); + expect(computedAccountId).toEqual( + buildAccountIdHash(ADMIN.zPublicKey, BAD_INPUT.secretNonce), + ); }); - it('should update nullifier set on successful renounce', () => { - const nullifierSetSize = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.size(); - expect(nullifierSetSize).toBe(0n); - - shieldedAccessControl.renounceRole(ADMIN.role, ADMIN.accountId); - const updatedSetSize = shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.size(); - expect(updatedSetSize).toEqual(1n); - expect( - shieldedAccessControl - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.member( - ADMIN.roleNullifier, - ), - ).toBe(true); + it('should not match when given unauthorized account with correct nonce', () => { + const computedAccountId = shieldedAccessControl.computeAccountId( + UNAUTHORIZED.zPublicKey, + ADMIN.secretNonce, + INSTANCE_SALT, + ); + expect(computedAccountId).not.toEqual(ADMIN.accountId); + expect(computedAccountId).toEqual( + buildAccountIdHash(UNAUTHORIZED.zPublicKey, ADMIN.secretNonce), + ); }); }); }); diff --git a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact index 116c3b14..2f808ba4 100644 --- a/contracts/src/access/test/mocks/MockShieldedAccessControl.compact +++ b/contracts/src/access/test/mocks/MockShieldedAccessControl.compact @@ -44,36 +44,12 @@ constructor(instanceSalt: Bytes<32>, isInit: Boolean) { } } -export circuit computeRoleCommitment( - role: ShieldedAccessControl_RoleIdentifier, - accountId: ShieldedAccessControl_AccountIdentifier - ): ShieldedAccessControl_RoleCommitment { - - return ShieldedAccessControl_computeRoleCommitment(role, accountId); -} - export pure circuit DEFAULT_ADMIN_ROLE(): ShieldedAccessControl_RoleIdentifier { return ShieldedAccessControl_DEFAULT_ADMIN_ROLE(); } -// circuit is reimplemented in the Mock contract for testing -export circuit _computeAccountId(role: ShieldedAccessControl_RoleIdentifier): ShieldedAccessControl_AccountIdentifier { - return disclose(ShieldedAccessControl_computeAccountId(ownPublicKey(), wit_secretNonce(role), ShieldedAccessControl__instanceSalt)); -} - -export pure circuit computeAccountId( - account: ZswapCoinPublicKey, - secretNonce: Bytes<32>, - instanceSalt: Bytes<32> - ): ShieldedAccessControl_AccountIdentifier { - - return ShieldedAccessControl_computeAccountId(account, secretNonce, instanceSalt); -} - -export pure circuit computeNullifier( - roleCommitment: ShieldedAccessControl_RoleCommitment - ): ShieldedAccessControl_RoleNullifier { - return ShieldedAccessControl_computeNullifier(roleCommitment); +export circuit assertOnlyRole(role: ShieldedAccessControl_RoleIdentifier): [] { + ShieldedAccessControl_assertOnlyRole(role); } export circuit canProveRole(role: ShieldedAccessControl_RoleIdentifier): Boolean { @@ -86,43 +62,6 @@ export circuit _uncheckedCanProveRole(role: ShieldedAccessControl_RoleIdentifier return _validateRole(role, accountId); } -export circuit assertOnlyRole(role: ShieldedAccessControl_RoleIdentifier): [] { - ShieldedAccessControl_assertOnlyRole(role); -} - -// _validateRole is re-implemented in the Mock contract for testing -export circuit _validateRole( - role: ShieldedAccessControl_RoleIdentifier, - accountId: ShieldedAccessControl_AccountIdentifier - ): Boolean { - const roleCommitment = computeRoleCommitment(role, accountId); - const roleCommitmentPath = wit_getRoleCommitmentPath(roleCommitment); - const isValidPath = - ShieldedAccessControl__operatorRoles.checkRoot( - merkleTreePathRoot<20, ShieldedAccessControl_RoleCommitment>( - disclose(roleCommitmentPath) - ) - ); - - // If the path is valid we assert it’s a path for the queried leaf (not some other leaf). - if (isValidPath) { - assert(roleCommitmentPath.leaf == roleCommitment, - "ShieldedAccessControl: Path must contain leaf matching computed role commitment for the provided role, accountId pairing" - ); - } - - const roleNullifier = computeNullifier(roleCommitment); - const isRevoked = ShieldedAccessControl__roleCommitmentNullifiers.member(disclose(roleNullifier)); - - return isValidPath && !isRevoked; -} - -export circuit getRoleAdmin( - role: ShieldedAccessControl_RoleIdentifier - ): ShieldedAccessControl_RoleIdentifier { - return ShieldedAccessControl_getRoleAdmin(role); -} - export circuit grantRole( role: ShieldedAccessControl_RoleIdentifier, accountId: ShieldedAccessControl_AccountIdentifier @@ -130,11 +69,11 @@ export circuit grantRole( ShieldedAccessControl_grantRole(role, accountId); } -export circuit revokeRole( +export circuit _grantRole( role: ShieldedAccessControl_RoleIdentifier, accountId: ShieldedAccessControl_AccountIdentifier - ): [] { - ShieldedAccessControl_revokeRole(role, accountId); + ): Boolean { + return ShieldedAccessControl__grantRole(role, accountId); } export circuit renounceRole( @@ -144,18 +83,11 @@ export circuit renounceRole( ShieldedAccessControl_renounceRole(role, accountIdConfirmation); } -export circuit _setRoleAdmin( - role: ShieldedAccessControl_RoleIdentifier, - adminRole: ShieldedAccessControl_RoleIdentifier - ): [] { - ShieldedAccessControl__setRoleAdmin(role, adminRole); -} - -export circuit _grantRole( +export circuit revokeRole( role: ShieldedAccessControl_RoleIdentifier, accountId: ShieldedAccessControl_AccountIdentifier - ): Boolean { - return ShieldedAccessControl__grantRole(role, accountId); + ): [] { + ShieldedAccessControl_revokeRole(role, accountId); } export circuit _revokeRole( @@ -179,7 +111,7 @@ export circuit _updateRole( return false; } - // disclosure only necessary here because we're exporting the circuit for testing + // disclosure only necessary here because we’re exporting the circuit for testing if (disclose(updateType) == ShieldedAccessControl_UpdateType.Grant) { ShieldedAccessControl__operatorRoles.insert(disclose(roleCommitment)); } else { @@ -188,3 +120,71 @@ export circuit _updateRole( return true; } + +export circuit getRoleAdmin( + role: ShieldedAccessControl_RoleIdentifier + ): ShieldedAccessControl_RoleIdentifier { + return ShieldedAccessControl_getRoleAdmin(role); +} + +export circuit _setRoleAdmin( + role: ShieldedAccessControl_RoleIdentifier, + adminRole: ShieldedAccessControl_RoleIdentifier + ): [] { + ShieldedAccessControl__setRoleAdmin(role, adminRole); +} + +// _validateRole is re-implemented in the Mock contract for testing +export circuit _validateRole( + role: ShieldedAccessControl_RoleIdentifier, + accountId: ShieldedAccessControl_AccountIdentifier + ): Boolean { + const roleCommitment = computeRoleCommitment(role, accountId); + const roleCommitmentPath = wit_getRoleCommitmentPath(roleCommitment); + const isValidPath = + ShieldedAccessControl__operatorRoles.checkRoot( + merkleTreePathRoot<20, ShieldedAccessControl_RoleCommitment>( + disclose(roleCommitmentPath) + ) + ); + + // If the path is valid we assert it’s a path for the queried leaf (not some other leaf). + if (isValidPath) { + assert(roleCommitmentPath.leaf == roleCommitment, + "ShieldedAccessControl: Path must contain leaf matching computed role commitment for the provided role, accountId pairing" + ); + } + + const roleNullifier = computeNullifier(roleCommitment); + const isRevoked = ShieldedAccessControl__roleCommitmentNullifiers.member(disclose(roleNullifier)); + + return isValidPath && !isRevoked; +} + +export circuit computeRoleCommitment( + role: ShieldedAccessControl_RoleIdentifier, + accountId: ShieldedAccessControl_AccountIdentifier + ): ShieldedAccessControl_RoleCommitment { + + return ShieldedAccessControl_computeRoleCommitment(role, accountId); +} + +export pure circuit computeNullifier( + roleCommitment: ShieldedAccessControl_RoleCommitment + ): ShieldedAccessControl_RoleNullifier { + return ShieldedAccessControl_computeNullifier(roleCommitment); +} + +// circuit is reimplemented in the Mock contract for testing +export circuit _computeAccountId(role: ShieldedAccessControl_RoleIdentifier): ShieldedAccessControl_AccountIdentifier { + return disclose(ShieldedAccessControl_computeAccountId(ownPublicKey(), wit_secretNonce(role), ShieldedAccessControl__instanceSalt)); +} + +export pure circuit computeAccountId( + account: ZswapCoinPublicKey, + secretNonce: Bytes<32>, + instanceSalt: Bytes<32> + ): ShieldedAccessControl_AccountIdentifier { + + return ShieldedAccessControl_computeAccountId(account, secretNonce, instanceSalt); +} diff --git a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts index b8a1664b..6aa533aa 100644 --- a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts +++ b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts @@ -6,8 +6,8 @@ import { import { ledger, Contract as MockShieldedAccessControl, - ZswapCoinPublicKey, type ShieldedAccessControl_UpdateType as UpdateType, + type ZswapCoinPublicKey, } from '../../../../artifacts/MockShieldedAccessControl/contract/index.js'; import { ShieldedAccessControlPrivateState, @@ -38,7 +38,8 @@ const ShieldedAccessControlSimulatorBase = createSimulator< return [instanceSalt, isInit]; }, ledgerExtractor: (state) => ledger(state), - witnessesFactory: () => ShieldedAccessControlWitnesses(), + witnessesFactory: () => + ShieldedAccessControlWitnesses(), }); /** @@ -56,35 +57,14 @@ export class ShieldedAccessControlSimulator extends ShieldedAccessControlSimulat super([instanceSalt, isInit], options); } - public computeRoleCommitment( - role: Uint8Array, - accountId: Uint8Array, - ): Uint8Array { - return this.circuits.impure.computeRoleCommitment(role, accountId); - } - - public _computeAccountId( - role: Uint8Array, - ): Uint8Array { - return this.circuits.impure._computeAccountId(role); - } - - public computeAccountId( - account: ZswapCoinPublicKey, - secretNonce: Uint8Array, - instanceSalt: Uint8Array - ): Uint8Array { - return this.circuits.pure.computeAccountId(account, secretNonce, instanceSalt); - } - - public computeNullifier(roleCommitment: Uint8Array): Uint8Array { - return this.circuits.pure.computeNullifier(roleCommitment); - } - public DEFAULT_ADMIN_ROLE(): Uint8Array { return this.circuits.pure.DEFAULT_ADMIN_ROLE(); } + public assertOnlyRole(role: Uint8Array) { + this.circuits.impure.assertOnlyRole(role); + } + public canProveRole(role: Uint8Array): boolean { return this.circuits.impure.canProveRole(role); } @@ -93,44 +73,71 @@ export class ShieldedAccessControlSimulator extends ShieldedAccessControlSimulat return this.circuits.impure._uncheckedCanProveRole(role); } - public assertOnlyRole(role: Uint8Array) { - this.circuits.impure.assertOnlyRole(role); - } - - public _validateRole(role: Uint8Array, accountId: Uint8Array): boolean { - return this.circuits.impure._validateRole(role, accountId); + public grantRole(role: Uint8Array, accountId: Uint8Array) { + this.circuits.impure.grantRole(role, accountId); } - public getRoleAdmin(role: Uint8Array): Uint8Array { - return this.circuits.impure.getRoleAdmin(role); + public _grantRole(role: Uint8Array, accountId: Uint8Array): boolean { + return this.circuits.impure._grantRole(role, accountId); } - public grantRole(role: Uint8Array, accountId: Uint8Array) { - this.circuits.impure.grantRole(role, accountId); + public renounceRole(role: Uint8Array, callerConfirmation: Uint8Array) { + this.circuits.impure.renounceRole(role, callerConfirmation); } public revokeRole(role: Uint8Array, accountId: Uint8Array) { this.circuits.impure.revokeRole(role, accountId); } - public _updateRole(role: Uint8Array, accountId: Uint8Array, updateType: UpdateType) { + public _revokeRole(role: Uint8Array, accountId: Uint8Array): boolean { + return this.circuits.impure._revokeRole(role, accountId); + } + + public _updateRole( + role: Uint8Array, + accountId: Uint8Array, + updateType: UpdateType, + ) { return this.circuits.impure._updateRole(role, accountId, updateType); } - public renounceRole(role: Uint8Array, callerConfirmation: Uint8Array) { - this.circuits.impure.renounceRole(role, callerConfirmation); + public getRoleAdmin(role: Uint8Array): Uint8Array { + return this.circuits.impure.getRoleAdmin(role); } public _setRoleAdmin(role: Uint8Array, adminRole: Uint8Array) { this.circuits.impure._setRoleAdmin(role, adminRole); } - public _grantRole(role: Uint8Array, accountId: Uint8Array): boolean { - return this.circuits.impure._grantRole(role, accountId); + public _validateRole(role: Uint8Array, accountId: Uint8Array): boolean { + return this.circuits.impure._validateRole(role, accountId); } - public _revokeRole(role: Uint8Array, accountId: Uint8Array): boolean { - return this.circuits.impure._revokeRole(role, accountId); + public computeRoleCommitment( + role: Uint8Array, + accountId: Uint8Array, + ): Uint8Array { + return this.circuits.impure.computeRoleCommitment(role, accountId); + } + + public computeNullifier(roleCommitment: Uint8Array): Uint8Array { + return this.circuits.pure.computeNullifier(roleCommitment); + } + + public _computeAccountId(role: Uint8Array): Uint8Array { + return this.circuits.impure._computeAccountId(role); + } + + public computeAccountId( + account: ZswapCoinPublicKey, + secretNonce: Uint8Array, + instanceSalt: Uint8Array, + ): Uint8Array { + return this.circuits.pure.computeAccountId( + account, + secretNonce, + instanceSalt, + ); } public readonly privateState = { @@ -160,8 +167,8 @@ export class ShieldedAccessControlSimulator extends ShieldedAccessControlSimulat getCurrentSecretNonce: (role: Uint8Array): Uint8Array => { const roleString = Buffer.from(role).toString('hex'); const roleNonce = this.getPrivateState().roles[roleString]; - if (typeof roleNonce === "undefined") { - throw new Error(`Missing secret nonce for role ${roleString}`) + if (typeof roleNonce === 'undefined') { + throw new Error(`Missing secret nonce for role ${roleString}`); } return roleNonce; }, diff --git a/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts b/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts index 8ffde118..ab4be5c9 100644 --- a/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts +++ b/contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts @@ -1,6 +1,8 @@ import { getRandomValues } from 'node:crypto'; -import type { WitnessContext, MerkleTreePath } from '@midnight-ntwrk/compact-runtime'; - +import type { + MerkleTreePath, + WitnessContext, +} from '@midnight-ntwrk/compact-runtime'; /** * @description Interface defining the witness methods for ShieldedAccessControl operations @@ -81,14 +83,14 @@ export const ShieldedAccessControlPrivateState = { const roles: Record = {}; for (const [k, v] of Object.entries(privateState.roles)) { - if (typeof v === "undefined") { + if (typeof v === 'undefined') { throw new Error(`Missing secret nonce for role ${k}`); } roles[k] = new Uint8Array(v); } roles[roleString] = new Uint8Array(nonce); - return { roles } + return { roles }; }, getRoleCommitmentPath: ( @@ -115,29 +117,30 @@ export const ShieldedAccessControlPrivateState = { * @description Factory function creating witness implementations for Shielded AccessControl operations. * @returns An object implementing the Witnesses interface for ShieldedAccessControlPrivateState. */ -export const ShieldedAccessControlWitnesses = - (): IShieldedAccessControlWitnesses => ({ - wit_secretNonce( - context: WitnessContext, - role: Uint8Array, - ): [ShieldedAccessControlPrivateState, Uint8Array] { - const roleString = Buffer.from(role).toString('hex'); - const roleNonce = context.privateState.roles[roleString]; - if (typeof roleNonce === "undefined") { - throw new Error(`Missing secret nonce for role ${roleString}`); - } - return [context.privateState, roleNonce]; - }, - wit_getRoleCommitmentPath( - context: WitnessContext, - roleCommitment: Uint8Array, - ): [ShieldedAccessControlPrivateState, MerkleTreePath] { - return [ - context.privateState, - ShieldedAccessControlPrivateState.getRoleCommitmentPath( - context.ledger, - roleCommitment, - ), - ]; - }, - }); +export const ShieldedAccessControlWitnesses = < + L, +>(): IShieldedAccessControlWitnesses => ({ + wit_secretNonce( + context: WitnessContext, + role: Uint8Array, + ): [ShieldedAccessControlPrivateState, Uint8Array] { + const roleString = Buffer.from(role).toString('hex'); + const roleNonce = context.privateState.roles[roleString]; + if (typeof roleNonce === 'undefined') { + throw new Error(`Missing secret nonce for role ${roleString}`); + } + return [context.privateState, roleNonce]; + }, + wit_getRoleCommitmentPath( + context: WitnessContext, + roleCommitment: Uint8Array, + ): [ShieldedAccessControlPrivateState, MerkleTreePath] { + return [ + context.privateState, + ShieldedAccessControlPrivateState.getRoleCommitmentPath( + context.ledger, + roleCommitment, + ), + ]; + }, +}); diff --git a/packages/simulator/test/fixtures/utils/address.ts b/packages/simulator/test/fixtures/utils/address.ts index d99fde3f..125be771 100644 --- a/packages/simulator/test/fixtures/utils/address.ts +++ b/packages/simulator/test/fixtures/utils/address.ts @@ -71,12 +71,12 @@ const baseGeneratePubKeyPair = ( str: string, asEither: boolean, ): [ - string, - ( - | Compact.ZswapCoinPublicKey - | Compact.Either - ), - ] => { + string, + ( + | Compact.ZswapCoinPublicKey + | Compact.Either + ), +] => { const pk = toHexPadded(str); const zpk = asEither ? createEitherTestUser(str) : encodeToPK(str); return [pk, zpk]; @@ -104,4 +104,4 @@ export const ZERO_ADDRESS = { is_left: false, left: encodeToPK(''), right: { bytes: zeroUint8Array() }, -}; \ No newline at end of file +}; From 3c41f952a451417f47a4c085e0a4679755fd5234 Mon Sep 17 00:00:00 2001 From: 0xisk Date: Fri, 27 Mar 2026 17:06:49 +0100 Subject: [PATCH 322/322] refactor(access): adding more test cases for shielded access control (#411) --- .../access/test/ShieldedAccessControl.test.ts | 162 ++++++++++++++++++ 1 file changed, 162 insertions(+) diff --git a/contracts/src/access/test/ShieldedAccessControl.test.ts b/contracts/src/access/test/ShieldedAccessControl.test.ts index 0f0da7c7..29728922 100644 --- a/contracts/src/access/test/ShieldedAccessControl.test.ts +++ b/contracts/src/access/test/ShieldedAccessControl.test.ts @@ -1134,6 +1134,25 @@ describe('ShieldedAccessControl', () => { ), ).toThrow('ShieldedAccessControl: unauthorized account'); }); + + it('when admin role has been reassigned via _setRoleAdmin', () => { + // Make OPERATOR_1.role the admin of OPERATOR_2.role + shieldedAccessControl._setRoleAdmin(OPERATOR_2.role, OPERATOR_1.role); + + shieldedAccessControl.privateState.injectSecretNonce( + OPERATOR_1.role, + ADMIN.secretNonce, + ); + + // ADMIN holds DEFAULT_ADMIN_ROLE but not OPERATOR_1.role, + // so granting OPERATOR_2.role should fail + expect(() => + shieldedAccessControl.grantRole( + OPERATOR_2.role, + OPERATOR_2.accountId, + ), + ).toThrow('ShieldedAccessControl: unauthorized account'); + }); }); describe('should not update _operatorRoles Merkle tree', () => { @@ -1657,6 +1676,32 @@ describe('ShieldedAccessControl', () => { ), ).toBe(true); }); + + it('should permanently block re-grant to the same accountId after renounce', () => { + shieldedAccessControl.renounceRole(ADMIN.role, ADMIN.accountId); + + // re-grant with same accountId — nullifier blocks it + shieldedAccessControl._grantRole(ADMIN.role, ADMIN.accountId); + expect( + shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), + ).toBe(false); + }); + + it('should allow re-grant with a new accountId after renounce', () => { + shieldedAccessControl.renounceRole(ADMIN.role, ADMIN.accountId); + + const newNonce = Buffer.alloc(32, 'NEW_ADMIN_NONCE'); + shieldedAccessControl.privateState.injectSecretNonce( + ADMIN.role, + newNonce, + ); + const newAccountId = buildAccountIdHash(ADMIN.zPublicKey, newNonce); + shieldedAccessControl._grantRole(ADMIN.role, newAccountId); + + expect( + shieldedAccessControl._validateRole(ADMIN.role, newAccountId), + ).toBe(true); + }); }); describe('revokeRole', () => { @@ -1979,6 +2024,27 @@ describe('ShieldedAccessControl', () => { ), ).toBe(false); }); + + it('when admin self-revokes then cannot further grant or revoke', () => { + shieldedAccessControl.revokeRole(ADMIN.role, ADMIN.accountId); + expect( + shieldedAccessControl._validateRole(ADMIN.role, ADMIN.accountId), + ).toBe(false); + + expect(() => + shieldedAccessControl.grantRole( + OPERATOR_1.role, + OPERATOR_1.accountId, + ), + ).toThrow('ShieldedAccessControl: unauthorized account'); + + expect(() => + shieldedAccessControl.revokeRole( + OPERATOR_1.role, + OPERATOR_1.accountId, + ), + ).toThrow('ShieldedAccessControl: unauthorized account'); + }); }); }); @@ -2367,6 +2433,27 @@ describe('ShieldedAccessControl', () => { ), ).toBe(true); }); + + it('when granting a different accountId for a role whose previous accountId was revoked', () => { + shieldedAccessControl._updateRole( + ADMIN.role, + ADMIN.accountId, + UpdateType.Revoke, + ); + expect( + shieldedAccessControl._updateRole( + ADMIN.role, + OPERATOR_1.accountId, + UpdateType.Grant, + ), + ).toBe(true); + expect( + shieldedAccessControl._validateRole( + ADMIN.role, + OPERATOR_1.accountId, + ), + ).toBe(true); + }); }); describe('should update _operatorRoles merkle tree', () => { @@ -2628,6 +2715,31 @@ describe('ShieldedAccessControl', () => { new Uint8Array(OPERATOR_2.role), ); }); + + it('should return DEFAULT_ADMIN_ROLE when admin is explicitly set to zero bytes', () => { + // Set a custom admin first, then reset to zero bytes (DEFAULT_ADMIN_ROLE) + shieldedAccessControl._setRoleAdmin(OPERATOR_1.role, ADMIN.role); + shieldedAccessControl._setRoleAdmin( + OPERATOR_1.role, + new Uint8Array(32), + ); + + // getRoleAdmin takes the map-lookup path (member is true) but returns zero bytes, + // which should equal DEFAULT_ADMIN_ROLE + expect( + shieldedAccessControl.getRoleAdmin(OPERATOR_1.role), + ).toStrictEqual(new Uint8Array(32)); + expect( + shieldedAccessControl.getRoleAdmin(OPERATOR_1.role), + ).toStrictEqual(shieldedAccessControl.DEFAULT_ADMIN_ROLE()); + }); + + it('should allow a role to be set as its own admin', () => { + shieldedAccessControl._setRoleAdmin(OPERATOR_1.role, OPERATOR_1.role); + expect(shieldedAccessControl.getRoleAdmin(OPERATOR_1.role)).toEqual( + new Uint8Array(OPERATOR_1.role), + ); + }); }); describe('_validateRole', () => { @@ -2883,6 +2995,30 @@ describe('ShieldedAccessControl', () => { )(...args), ).not.toEqual(ADMIN.roleCommitment); }); + + it('should produce a different commitment for the same (role, accountId) when instanceSalt differs', () => { + const differentSalt = new Uint8Array(32).fill(1); + const otherInstance = new ShieldedAccessControlSimulator( + differentSalt, + true, + { + privateState: ShieldedAccessControlPrivateState.withRoleAndNonce( + ADMIN.role, + ADMIN.secretNonce, + ), + }, + ); + + const commitment1 = shieldedAccessControl.computeRoleCommitment( + ADMIN.role, + ADMIN.accountId, + ); + const commitment2 = otherInstance.computeRoleCommitment( + ADMIN.role, + ADMIN.accountId, + ); + expect(commitment1).not.toEqual(commitment2); + }); }); describe('computeNullifier', () => { @@ -2970,6 +3106,32 @@ describe('ShieldedAccessControl', () => { buildAccountIdHash(UNAUTHORIZED.zPublicKey, ADMIN.secretNonce), ); }); + + it('should produce a different accountId for the same (account, nonce) when instanceSalt differs', () => { + const differentSalt = new Uint8Array(32).fill(1); + const accountId1 = shieldedAccessControl.computeAccountId( + ADMIN.zPublicKey, + ADMIN.secretNonce, + INSTANCE_SALT, + ); + const accountId2 = shieldedAccessControl.computeAccountId( + ADMIN.zPublicKey, + ADMIN.secretNonce, + differentSalt, + ); + expect(accountId1).not.toEqual(accountId2); + }); + + it('should succeed when secretNonce is zero bytes', () => { + const zeroNonce = new Uint8Array(32); + expect( + shieldedAccessControl.computeAccountId( + ADMIN.zPublicKey, + zeroNonce, + INSTANCE_SALT, + ), + ).toEqual(buildAccountIdHash(ADMIN.zPublicKey, zeroNonce)); + }); }); }); });