diff --git a/.github/workflows/release-develop.yml b/.github/workflows/release-develop.yml index 793b57dc7..f2f88e3aa 100644 --- a/.github/workflows/release-develop.yml +++ b/.github/workflows/release-develop.yml @@ -31,8 +31,8 @@ jobs: - name: "Build" run: npm run build - - name: Run Lerna Release - run: npm run publish:canary + - name: "Publish nightly" + run: npx pkg-pr-new publish './packages/*' - name: Trigger website build & deployment env: diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..ebc5f6f87 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,17 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) + +## Unreleased + +### Added + +- Added Mempool sorting [#395](https://github.com/proto-kit/framework/pull/395) +- Introduced dynamic block building and JIT transaction fetching [#394](https://github.com/proto-kit/framework/pull/394) +- Introduced block explorer [#381](https://github.com/proto-kit/framework/pull/381) +- Added CircuitAnalysisModule for easy analysis of protocol circuits [#379](https://github.com/proto-kit/framework/pull/379) +- Separated settlement and bridging functionally, so now settlement can be used without bridging [#376](https://github.com/proto-kit/framework/pull/376) +- Added nightly releases via pkg.pr.new [#384](https://github.com/proto-kit/framework/pull/384) +- Introduced Changelog [#378](https://github.com/proto-kit/framework/pull/378) diff --git a/README.md b/README.md index 962e4fd0b..37e93f0c1 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,11 @@
-[![npm version](https://img.shields.io/npm/v/@proto-kit/sdk.svg?style=flat&logo=npm)](https://www.npmjs.com/package/o1js) [![Documentation](https://img.shields.io/badge/Documentation-website-blue.svg)](https://protokit.dev) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-green.svg)]() +[![npm version](https://img.shields.io/npm/v/@proto-kit/sdk.svg?style=flat&logo=npm)](https://www.npmjs.com/package/@proto-kit/sdk) +[![pkg.pr.new](https://pkg.pr.new/badge/proto-kit/framework)](https://pkg.pr.new/~/proto-kit/framework) # Protokit diff --git a/package-lock.json b/package-lock.json index 62c5fb92a..61f19dbb1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,6 +39,7 @@ "lint-staged": "^13.1.0", "nx": "15.6.2", "nyc": "^17.1.0", + "pkg-pr-new": "^0.0.62", "prettier": "^3.2.5", "ts-jest": "^29.0.5", "typedoc": "^0.26.11", @@ -60,6 +61,45 @@ } } }, + "node_modules/@actions/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz", + "integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@actions/exec": "^1.1.1", + "@actions/http-client": "^2.0.1" + } + }, + "node_modules/@actions/exec": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz", + "integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@actions/io": "^1.0.1" + } + }, + "node_modules/@actions/http-client": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.3.tgz", + "integrity": "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tunnel": "^0.0.6", + "undici": "^5.25.4" + } + }, + "node_modules/@actions/io": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz", + "integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@alcalzone/ansi-tokenize": { "version": "0.1.3", "license": "MIT", @@ -85,6 +125,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "license": "MIT", "engines": { "node": ">=10" }, @@ -662,10 +703,11 @@ } }, "node_modules/@emnapi/core": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz", - "integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", + "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==", "dev": true, + "license": "MIT", "optional": true, "dependencies": { "@emnapi/wasi-threads": "1.1.0", @@ -673,10 +715,11 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", - "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", "dev": true, + "license": "MIT", "optional": true, "dependencies": { "tslib": "^2.4.0" @@ -687,6 +730,7 @@ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", "dev": true, + "license": "MIT", "optional": true, "dependencies": { "tslib": "^2.4.0" @@ -820,10 +864,21 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/@floating-ui/core": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "license": "MIT", "dependencies": { "@floating-ui/utils": "^0.2.10" } @@ -832,6 +887,7 @@ "version": "1.7.4", "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "license": "MIT", "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" @@ -841,6 +897,7 @@ "version": "2.1.6", "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", + "license": "MIT", "dependencies": { "@floating-ui/dom": "^1.7.4" }, @@ -852,7 +909,8 @@ "node_modules/@floating-ui/utils": { "version": "0.2.10", "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", - "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==" + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" }, "node_modules/@gar/promisify": { "version": "1.1.3", @@ -1100,6 +1158,7 @@ "version": "3.10.0", "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.10.0.tgz", "integrity": "sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag==", + "license": "MIT", "peerDependencies": { "react-hook-form": "^7.0.0" } @@ -1997,6 +2056,22 @@ "url": "https://opencollective.com/js-sdsl" } }, + "node_modules/@jsdevtools/ez-spawn": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@jsdevtools/ez-spawn/-/ez-spawn-3.0.4.tgz", + "integrity": "sha512-f5DRIOZf7wxogefH03RjMPMdBF7ADTWUMoOs9kaJo06EfwF+aFhMZMDZxHg/Xe12hptN9xoZjGso2fdjapBRIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-me-maybe": "^1.0.1", + "cross-spawn": "^7.0.3", + "string-argv": "^0.3.1", + "type-detect": "^4.0.8" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@kamilkisiela/fast-url-parser": { "version": "1.1.4", "license": "MIT" @@ -2618,6 +2693,7 @@ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", "dev": true, + "license": "MIT", "optional": true, "dependencies": { "@emnapi/core": "^1.4.3", @@ -2628,13 +2704,15 @@ "node_modules/@next/env": { "version": "14.2.4", "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.4.tgz", - "integrity": "sha512-3EtkY5VDkuV2+lNmKlbkibIJxcO4oIHEhBWne6PaAp+76J9KoSsGvNikp6ivzAT8dhhBMYrm6op2pS1ApG0Hzg==" + "integrity": "sha512-3EtkY5VDkuV2+lNmKlbkibIJxcO4oIHEhBWne6PaAp+76J9KoSsGvNikp6ivzAT8dhhBMYrm6op2pS1ApG0Hzg==", + "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { "version": "14.2.4", "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-14.2.4.tgz", "integrity": "sha512-svSFxW9f3xDaZA3idQmlFw7SusOuWTpDTAeBlO3AEPDltrraV+lqs7mAc6A27YdnpQVVIA3sODqUAAHdWhVWsA==", "dev": true, + "license": "MIT", "dependencies": { "glob": "10.3.10" } @@ -2644,6 +2722,7 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", "dev": true, + "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^2.3.5", @@ -2666,6 +2745,7 @@ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" }, @@ -2686,6 +2766,7 @@ "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "darwin" @@ -2701,6 +2782,7 @@ "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "darwin" @@ -2716,6 +2798,7 @@ "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -2731,6 +2814,7 @@ "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -2746,6 +2830,7 @@ "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -2761,6 +2846,7 @@ "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -2776,6 +2862,7 @@ "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "win32" @@ -2791,6 +2878,7 @@ "cpu": [ "ia32" ], + "license": "MIT", "optional": true, "os": [ "win32" @@ -2806,6 +2894,7 @@ "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "win32" @@ -2848,6 +2937,7 @@ "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", "dev": true, + "license": "MIT", "engines": { "node": ">=12.4.0" } @@ -3380,6 +3470,298 @@ "tao": "index.js" } }, + "node_modules/@octokit/action": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@octokit/action/-/action-6.1.0.tgz", + "integrity": "sha512-lo+nHx8kAV86bxvOVOI3vFjX3gXPd/L7guAUbvs3pUvnR2KC+R7yjBkA1uACt4gYhs4LcWP3AXSGQzsbeN2XXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/auth-action": "^4.0.0", + "@octokit/core": "^5.0.0", + "@octokit/plugin-paginate-rest": "^9.0.0", + "@octokit/plugin-rest-endpoint-methods": "^10.0.0", + "@octokit/types": "^12.0.0", + "undici": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/action/node_modules/@octokit/auth-token": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", + "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/action/node_modules/@octokit/core": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.2.tgz", + "integrity": "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/auth-token": "^4.0.0", + "@octokit/graphql": "^7.1.0", + "@octokit/request": "^8.4.1", + "@octokit/request-error": "^5.1.1", + "@octokit/types": "^13.0.0", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/action/node_modules/@octokit/core/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/action/node_modules/@octokit/core/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@octokit/action/node_modules/@octokit/endpoint": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.6.tgz", + "integrity": "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/action/node_modules/@octokit/endpoint/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/action/node_modules/@octokit/endpoint/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@octokit/action/node_modules/@octokit/graphql": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.1.tgz", + "integrity": "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/request": "^8.4.1", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/action/node_modules/@octokit/graphql/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/action/node_modules/@octokit/graphql/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@octokit/action/node_modules/@octokit/openapi-types": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", + "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/action/node_modules/@octokit/plugin-paginate-rest": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.2.2.tgz", + "integrity": "sha512-u3KYkGF7GcZnSD/3UP0S7K5XUFT2FkOQdcfXZGZQPGv3lm4F2Xbf71lvjldr8c1H3nNbF+33cLEkWYbokGWqiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^12.6.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@octokit/action/node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-10.4.1.tgz", + "integrity": "sha512-xV1b+ceKV9KytQe3zCVqjg+8GTGfDYwaT1ATU5isiUyVtlVAO3HNdzpS4sr4GBx4hxQ46s7ITtZrAsxG22+rVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^12.6.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@octokit/action/node_modules/@octokit/request": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.1.tgz", + "integrity": "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^9.0.6", + "@octokit/request-error": "^5.1.1", + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/action/node_modules/@octokit/request-error": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.1.tgz", + "integrity": "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.1.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/action/node_modules/@octokit/request-error/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/action/node_modules/@octokit/request-error/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@octokit/action/node_modules/@octokit/request/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/action/node_modules/@octokit/request/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@octokit/action/node_modules/@octokit/types": { + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", + "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^20.0.0" + } + }, + "node_modules/@octokit/action/node_modules/undici": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz", + "integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/@octokit/auth-action": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-action/-/auth-action-4.1.0.tgz", + "integrity": "sha512-m+3t7K46IYyMk7Bl6/lF4Rv09GqDZjYmNg8IWycJ2Fa3YE3DE7vQcV6G2hUPmR9NDqenefNJwVtlisMjzymPiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/auth-token": "^4.0.0", + "@octokit/types": "^13.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-action/node_modules/@octokit/auth-token": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", + "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-action/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/auth-action/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, "node_modules/@octokit/auth-token": { "version": "3.0.4", "dev": true, @@ -5252,17 +5634,20 @@ "node_modules/@radix-ui/number": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", - "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==" + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" }, "node_modules/@radix-ui/primitive": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", - "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==" + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" }, "node_modules/@radix-ui/react-arrow": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, @@ -5285,6 +5670,7 @@ "version": "1.3.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", + "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", @@ -5314,6 +5700,7 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", @@ -5339,6 +5726,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, @@ -5356,6 +5744,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -5370,6 +5759,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -5384,6 +5774,7 @@ "version": "1.1.15", "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", @@ -5419,6 +5810,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, @@ -5436,6 +5828,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -5450,6 +5843,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", @@ -5476,6 +5870,7 @@ "version": "2.1.16", "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", + "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", @@ -5504,6 +5899,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -5518,6 +5914,7 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", @@ -5542,6 +5939,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, @@ -5559,6 +5957,7 @@ "version": "2.1.8", "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz", "integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==", + "license": "MIT", "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, @@ -5581,6 +5980,7 @@ "version": "2.1.4", "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", "dependencies": { "@radix-ui/react-slot": "1.2.4" }, @@ -5603,6 +6003,7 @@ "version": "2.1.16", "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", @@ -5642,6 +6043,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, @@ -5659,6 +6061,7 @@ "version": "1.2.14", "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz", "integrity": "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==", + "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", @@ -5694,6 +6097,7 @@ "version": "1.1.15", "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", + "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", @@ -5730,6 +6134,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, @@ -5747,6 +6152,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", @@ -5778,6 +6184,7 @@ "version": "1.1.9", "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" @@ -5801,6 +6208,7 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" @@ -5824,6 +6232,7 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", "dependencies": { "@radix-ui/react-slot": "1.2.3" }, @@ -5846,6 +6255,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, @@ -5863,6 +6273,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", @@ -5893,6 +6304,7 @@ "version": "2.2.6", "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", + "license": "MIT", "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", @@ -5935,6 +6347,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, @@ -5952,6 +6365,7 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, @@ -5969,6 +6383,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", + "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", @@ -6002,6 +6417,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, @@ -6019,6 +6435,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -6033,6 +6450,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" @@ -6051,6 +6469,7 @@ "version": "0.0.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, @@ -6068,6 +6487,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, @@ -6085,6 +6505,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -6099,6 +6520,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -6113,6 +6535,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", "dependencies": { "@radix-ui/rect": "1.1.1" }, @@ -6130,6 +6553,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, @@ -6147,6 +6571,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, @@ -6168,7 +6593,8 @@ "node_modules/@radix-ui/rect": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", - "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==" + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" }, "node_modules/@redis/bloom": { "version": "1.2.0", @@ -6229,7 +6655,8 @@ "version": "1.15.0", "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.15.0.tgz", "integrity": "sha512-ojSshQPKwVvSMR8yT2L/QtUkV5SXi/IfDiJ4/8d6UbTPjiHVmxZzUAzGD8Tzks1b9+qQkZa0isUOvYObedITaw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@shikijs/core": { "version": "1.29.2", @@ -6442,12 +6869,14 @@ "node_modules/@swc/counter": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", - "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==" + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "license": "Apache-2.0" }, "node_modules/@swc/helpers": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz", "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==", + "license": "Apache-2.0", "dependencies": { "@swc/counter": "^0.1.3", "tslib": "^2.4.0" @@ -6527,6 +6956,7 @@ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", "dev": true, + "license": "MIT", "optional": true, "dependencies": { "tslib": "^2.4.0" @@ -6890,6 +7320,7 @@ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "devOptional": true, + "license": "MIT", "peerDependencies": { "@types/react": "^18.0.0" } @@ -7294,6 +7725,7 @@ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -7307,6 +7739,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -7320,6 +7753,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -7333,6 +7767,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -7346,6 +7781,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -7359,6 +7795,7 @@ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -7372,6 +7809,7 @@ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -7385,6 +7823,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -7398,6 +7837,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -7411,6 +7851,7 @@ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -7424,6 +7865,7 @@ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -7437,6 +7879,7 @@ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -7450,6 +7893,7 @@ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -7463,6 +7907,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -7476,6 +7921,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -7489,6 +7935,7 @@ "wasm32" ], "dev": true, + "license": "MIT", "optional": true, "dependencies": { "@napi-rs/wasm-runtime": "^0.2.11" @@ -7505,6 +7952,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -7518,6 +7966,7 @@ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -7531,6 +7980,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -7682,7 +8132,9 @@ } }, "node_modules/acorn": { - "version": "8.12.0", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -7822,7 +8274,8 @@ "node_modules/any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==" + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "license": "MIT" }, "node_modules/anymatch": { "version": "3.1.3", @@ -7880,6 +8333,7 @@ "version": "1.2.6", "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", "dependencies": { "tslib": "^2.0.0" }, @@ -7892,6 +8346,7 @@ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">= 0.4" } @@ -7901,6 +8356,7 @@ "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" @@ -7976,6 +8432,7 @@ "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -8032,6 +8489,7 @@ "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", @@ -8050,6 +8508,7 @@ "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -8066,6 +8525,7 @@ "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", "dev": true, + "license": "MIT", "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", @@ -8109,7 +8569,8 @@ "version": "0.0.8", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/astral-regex": { "version": "2.0.0", @@ -8127,6 +8588,7 @@ "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -8158,6 +8620,7 @@ "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", "dev": true, + "license": "MIT", "dependencies": { "possible-typed-array-names": "^1.0.0" }, @@ -8169,10 +8632,11 @@ } }, "node_modules/axe-core": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.0.tgz", - "integrity": "sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ==", + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.1.tgz", + "integrity": "sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==", "dev": true, + "license": "MPL-2.0", "engines": { "node": ">=4" } @@ -8192,6 +8656,7 @@ "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">= 0.4" } @@ -8472,6 +8937,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", "engines": { "node": ">=8" }, @@ -8898,6 +9364,7 @@ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", "dev": true, + "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", @@ -8927,6 +9394,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" @@ -8938,6 +9406,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-me-maybe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", + "dev": true, + "license": "MIT" + }, "node_modules/callsites": { "version": "3.1.0", "license": "MIT", @@ -8956,6 +9431,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "license": "MIT", "engines": { "node": ">= 6" } @@ -9155,6 +9631,7 @@ "version": "0.7.1", "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", "dependencies": { "clsx": "^2.1.1" }, @@ -9278,7 +9755,8 @@ "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", - "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" }, "node_modules/cliui": { "version": "7.0.4", @@ -9325,6 +9803,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", "engines": { "node": ">=6" } @@ -9351,6 +9830,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz", "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==", + "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "^1.1.1", "@radix-ui/react-dialog": "^1.1.6", @@ -9560,6 +10040,13 @@ "typedarray": "^0.0.6" } }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, "node_modules/config-chain": { "version": "1.1.12", "dev": true, @@ -10107,7 +10594,8 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", - "dev": true + "dev": true, + "license": "BSD-2-Clause" }, "node_modules/dargs": { "version": "7.0.0", @@ -10122,6 +10610,7 @@ "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", @@ -10139,6 +10628,7 @@ "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", @@ -10156,6 +10646,7 @@ "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -10183,6 +10674,7 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", "dependencies": { "ms": "^2.1.3" }, @@ -10226,6 +10718,16 @@ "node": ">=0.10.0" } }, + "node_modules/decode-uri-component": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.4.1.tgz", + "integrity": "sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + } + }, "node_modules/dedent": { "version": "0.7.0", "dev": true, @@ -10415,7 +10917,8 @@ "node_modules/detect-node-es": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", - "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" }, "node_modules/devlop": { "version": "1.1.0", @@ -10432,7 +10935,8 @@ "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==" + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "license": "Apache-2.0" }, "node_modules/diff": { "version": "4.0.2", @@ -10461,7 +10965,8 @@ "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "license": "MIT" }, "node_modules/doctrine": { "version": "3.0.0", @@ -10751,10 +11256,11 @@ } }, "node_modules/es-abstract": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", - "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", "dev": true, + "license": "MIT", "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", @@ -10834,26 +11340,27 @@ } }, "node_modules/es-iterator-helpers": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", - "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.2.tgz", + "integrity": "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", - "call-bound": "^1.0.3", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.6", + "es-abstract": "^1.24.1", "es-errors": "^1.3.0", - "es-set-tostringtag": "^2.0.3", + "es-set-tostringtag": "^2.1.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.6", + "get-intrinsic": "^1.3.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", - "iterator.prototype": "^1.1.4", + "iterator.prototype": "^1.1.5", "safe-array-concat": "^1.1.3" }, "engines": { @@ -10898,6 +11405,7 @@ "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", "dev": true, + "license": "MIT", "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", @@ -11031,6 +11539,7 @@ "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-14.2.4.tgz", "integrity": "sha512-Qr0wMgG9m6m4uYy2jrYJmyuNlYZzPRQq5Kvb9IDlYwn+7yq6W6sfMNFgb+9guM1KYwuIo6TIaiFhZJ6SnQ/Efw==", "dev": true, + "license": "MIT", "dependencies": { "@next/eslint-plugin-next": "14.2.4", "@rushstack/eslint-patch": "^1.3.3", @@ -11057,6 +11566,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.2.0.tgz", "integrity": "sha512-5FKsVcHTk6TafQKQbuIVkXq58Fnbkd2wDL4LB7AURN7RUOu1utVP+G8+6u3ZhEroW3DF6hyo3ZEXxgKgp4KeCg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "@typescript-eslint/scope-manager": "7.2.0", "@typescript-eslint/types": "7.2.0", @@ -11085,6 +11595,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.2.0.tgz", "integrity": "sha512-Qh976RbQM/fYtjx9hs4XkayYujB/aPwglw2choHmf3zBjB4qOywWSdt9+KLRdHubGcoSwBnXUH2sR3hkyaERRg==", "dev": true, + "license": "MIT", "dependencies": { "@typescript-eslint/types": "7.2.0", "@typescript-eslint/visitor-keys": "7.2.0" @@ -11102,6 +11613,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.2.0.tgz", "integrity": "sha512-XFtUHPI/abFhm4cbCDc5Ykc8npOKBSJePY3a3s+lwumt7XWJuzP5cZcfZ610MIPHjQjNsOLlYK8ASPaNG8UiyA==", "dev": true, + "license": "MIT", "engines": { "node": "^16.0.0 || >=18.0.0" }, @@ -11115,6 +11627,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.2.0.tgz", "integrity": "sha512-cyxS5WQQCoBwSakpMrvMXuMDEbhOo9bNHHrNcEWis6XHx6KF518tkF1wBvKIn/tpq5ZpUYK7Bdklu8qY0MsFIA==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "@typescript-eslint/types": "7.2.0", "@typescript-eslint/visitor-keys": "7.2.0", @@ -11143,6 +11656,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.2.0.tgz", "integrity": "sha512-c6EIQRHhcpl6+tO8EMR+kjkkV+ugUNXOmeASA1rlzkd8EPIriavpWoiEz1HR/VLhbVIdhqnV6E7JZm00cBDx2A==", "dev": true, + "license": "MIT", "dependencies": { "@typescript-eslint/types": "7.2.0", "eslint-visitor-keys": "^3.4.1" @@ -11160,6 +11674,7 @@ "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", "dev": true, + "license": "ISC", "dependencies": { "@nolyfill/is-core-module": "1.0.39", "debug": "^4.4.0", @@ -11194,6 +11709,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -11388,6 +11904,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", "dev": true, + "license": "MIT", "dependencies": { "aria-query": "^5.3.2", "array-includes": "^3.1.8", @@ -11417,6 +11934,7 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -11426,13 +11944,15 @@ "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/eslint-plugin-jsx-a11y/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -11474,6 +11994,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", "dev": true, + "license": "MIT", "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", @@ -11506,6 +12027,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.0.0-canary-7118f5dd7-20230705.tgz", "integrity": "sha512-AZYbMo/NW9chdL7vk6HQzQhT+PvTAEVqWk9ziruUoW2kAOcN5qNyelv70e0F1VNQAbvutOC9oc+xfWycI9FxDw==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -11518,6 +12040,7 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -11528,6 +12051,7 @@ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", "dev": true, + "license": "Apache-2.0", "dependencies": { "esutils": "^2.0.2" }, @@ -11540,6 +12064,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -11552,6 +12077,7 @@ "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", "dev": true, + "license": "MIT", "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -11569,6 +12095,7 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" } @@ -12117,6 +12644,19 @@ "node": ">=8" } }, + "node_modules/filter-obj": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-5.1.0.tgz", + "integrity": "sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/finalhandler": { "version": "1.3.1", "license": "MIT", @@ -12322,6 +12862,7 @@ "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", "dev": true, + "license": "MIT", "dependencies": { "is-callable": "^1.2.7" }, @@ -12464,6 +13005,7 @@ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -12484,6 +13026,7 @@ "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", @@ -12613,6 +13156,7 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/geist/-/geist-1.5.1.tgz", "integrity": "sha512-mAHZxIsL2o3ZITFaBVFBnwyDOw+zNLYum6A6nIjpzCGIO8QtC3V76XF2RnZTyLx1wlDTmMDy8jg3Ib52MIjGvQ==", + "license": "SIL OPEN FONT LICENSE", "peerDependencies": { "next": ">=13.2.0" } @@ -12665,6 +13209,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", "engines": { "node": ">=6" } @@ -12816,6 +13361,7 @@ "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", @@ -12833,6 +13379,7 @@ "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", "dev": true, + "license": "MIT", "dependencies": { "resolve-pkg-maps": "^1.0.0" }, @@ -13972,6 +14519,7 @@ "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -14002,6 +14550,7 @@ "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", "dev": true, + "license": "MIT", "dependencies": { "dunder-proto": "^1.0.0" }, @@ -15273,6 +15822,7 @@ "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", "dev": true, + "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", @@ -15346,6 +15896,7 @@ "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", @@ -15367,6 +15918,7 @@ "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", "dev": true, + "license": "MIT", "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", @@ -15386,6 +15938,7 @@ "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", "dev": true, + "license": "MIT", "dependencies": { "has-bigints": "^1.0.2" }, @@ -15400,6 +15953,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" }, @@ -15412,6 +15966,7 @@ "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" @@ -15428,6 +15983,7 @@ "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", "dev": true, + "license": "MIT", "dependencies": { "semver": "^7.7.1" } @@ -15475,6 +16031,7 @@ "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", @@ -15492,6 +16049,7 @@ "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" @@ -15528,6 +16086,7 @@ "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.3" }, @@ -15602,6 +16161,7 @@ "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -15640,6 +16200,7 @@ "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" @@ -15696,6 +16257,7 @@ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", @@ -15725,6 +16287,7 @@ "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -15737,6 +16300,7 @@ "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.3" }, @@ -15770,6 +16334,7 @@ "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" @@ -15786,6 +16351,7 @@ "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "has-symbols": "^1.1.0", @@ -15814,6 +16380,7 @@ "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "dev": true, + "license": "MIT", "dependencies": { "which-typed-array": "^1.1.16" }, @@ -15867,6 +16434,7 @@ "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -15879,6 +16447,7 @@ "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.3" }, @@ -15894,6 +16463,7 @@ "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" @@ -15926,7 +16496,21 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/isbinaryfile": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", + "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } }, "node_modules/isexe": { "version": "2.0.0", @@ -16237,6 +16821,7 @@ "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", "dev": true, + "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", "es-object-atoms": "^1.0.0", @@ -17712,6 +18297,7 @@ "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "license": "MIT", "bin": { "jiti": "bin/jiti.js" } @@ -17909,6 +18495,7 @@ "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", "dev": true, + "license": "MIT", "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", @@ -18012,13 +18599,15 @@ "version": "0.3.23", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", - "dev": true + "dev": true, + "license": "CC0-1.0" }, "node_modules/language-tags": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", "dev": true, + "license": "MIT", "dependencies": { "language-subtag-registry": "^0.3.20" }, @@ -19198,6 +19787,7 @@ "version": "0.395.0", "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.395.0.tgz", "integrity": "sha512-6hzdNH5723A4FLaYZWpK50iyZH8iS2Jq5zuPRRotOFkhu6kxxJiebVdJ72tCR5XkiIeYFOU5NUawFZOac+VeYw==", + "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0" } @@ -19894,6 +20484,7 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -20211,6 +20802,19 @@ "node": ">=10" } }, + "node_modules/mlly": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", + "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.15.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.1" + } + }, "node_modules/modify-values": { "version": "1.0.1", "dev": true, @@ -20265,7 +20869,8 @@ "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" }, "node_modules/msgpackr": { "version": "1.10.2", @@ -20330,6 +20935,7 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "license": "MIT", "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", @@ -20358,6 +20964,7 @@ "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", "dev": true, + "license": "MIT", "bin": { "napi-postinstall": "lib/cli.js" }, @@ -20387,6 +20994,8 @@ "version": "14.2.4", "resolved": "https://registry.npmjs.org/next/-/next-14.2.4.tgz", "integrity": "sha512-R8/V7vugY+822rsQGQCjoLhMuC9oFj9SOi4Cl4b2wjDrseD0LRZ10W7R6Czo4w9ZznVSshKjuIomsRjvm9EKJQ==", + "deprecated": "This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/security-update-2025-12-11 for more details.", + "license": "MIT", "dependencies": { "@next/env": "14.2.4", "@swc/helpers": "0.5.5", @@ -20450,6 +21059,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", @@ -21615,6 +22225,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", "engines": { "node": ">= 6" } @@ -21623,6 +22234,7 @@ "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -21643,6 +22255,7 @@ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", @@ -21677,6 +22290,7 @@ "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", @@ -21745,6 +22359,7 @@ "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", @@ -21949,6 +22564,7 @@ "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", "dev": true, + "license": "MIT", "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", @@ -22495,6 +23111,13 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/pg-int8": { "version": "1.0.1", "license": "ISC", @@ -22618,6 +23241,38 @@ "node": ">=8" } }, + "node_modules/pkg-pr-new": { + "version": "0.0.62", + "resolved": "https://registry.npmjs.org/pkg-pr-new/-/pkg-pr-new-0.0.62.tgz", + "integrity": "sha512-K2jtf1PLCJJFDimQIpasPXDdnRTZSYPy36Ldz+QhMpLz2YN1Wi0ZQkomVt5Wi3NdBZcYuYGXekyFWfJ6fAHYjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@actions/core": "^1.11.1", + "@jsdevtools/ez-spawn": "^3.0.4", + "@octokit/action": "^6.1.0", + "ignore": "^5.3.1", + "isbinaryfile": "^5.0.2", + "pkg-types": "^1.1.1", + "query-registry": "^3.0.1", + "tinyglobby": "^0.2.9" + }, + "bin": { + "pkg-pr-new": "bin/cli.js" + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, "node_modules/pluralize": { "version": "8.0.0", "license": "MIT", @@ -22652,6 +23307,7 @@ "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -22687,6 +23343,7 @@ "version": "15.1.0", "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "license": "MIT", "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", @@ -22713,6 +23370,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "camelcase-css": "^2.0.1" }, @@ -22737,6 +23395,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "lilconfig": "^3.1.1" }, @@ -22768,6 +23427,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "license": "MIT", "engines": { "node": ">=14" }, @@ -22789,6 +23449,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "postcss-selector-parser": "^6.1.1" }, @@ -22803,6 +23464,7 @@ "version": "6.1.2", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "license": "MIT", "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -22814,7 +23476,8 @@ "node_modules/postcss-value-parser": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" }, "node_modules/postgres-array": { "version": "2.0.0", @@ -23159,6 +23822,65 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/query-registry": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/query-registry/-/query-registry-3.0.1.tgz", + "integrity": "sha512-M9RxRITi2mHMVPU5zysNjctUT8bAPx6ltEXo/ir9+qmiM47Y7f0Ir3+OxUO5OjYAWdicBQRew7RtHtqUXydqlg==", + "dev": true, + "license": "MIT", + "dependencies": { + "query-string": "^9.0.0", + "quick-lru": "^7.0.0", + "url-join": "^5.0.0", + "validate-npm-package-name": "^5.0.1", + "zod": "^3.23.8", + "zod-package-json": "^1.0.3" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/query-registry/node_modules/quick-lru": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-7.3.0.tgz", + "integrity": "sha512-k9lSsjl36EJdK7I06v7APZCbyGT2vMTsYSRX1Q2nbYmnkBqgUhRkAuzH08Ciotteu/PLJmIF2+tti7o3C/ts2g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/query-registry/node_modules/validate-npm-package-name": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz", + "integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/query-string": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-9.3.1.tgz", + "integrity": "sha512-5fBfMOcDi5SA9qj5jZhWAcTtDfKF5WFdd2uD9nVNlbxVv1baq65aALy6qofpNEGELHvisjjasxQp7BlM9gvMzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "decode-uri-component": "^0.4.1", + "filter-obj": "^5.1.0", + "split-on-first": "^3.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "funding": [ @@ -23240,6 +23962,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -23249,9 +23972,10 @@ } }, "node_modules/react-hook-form": { - "version": "7.67.0", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.67.0.tgz", - "integrity": "sha512-E55EOwKJHHIT/I6J9DmQbCWToAYSw9nN5R57MZw9rMtjh+YQreMDxRLfdjfxQbiJ3/qbg3Z02wGzBX4M+5fMtQ==", + "version": "7.71.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.0.tgz", + "integrity": "sha512-oFDt/iIFMV9ZfV52waONXzg4xuSlbwKUPvXVH2jumL1me5qFhBMc4knZxuXiZ2+j6h546sYe3ZKJcg/900/iHw==", + "license": "MIT", "engines": { "node": ">=18.0.0" }, @@ -23295,6 +24019,7 @@ "version": "2.7.2", "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", @@ -23319,6 +24044,7 @@ "version": "2.3.8", "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" @@ -23340,6 +24066,7 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" @@ -23361,6 +24088,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/react-truncate-inside/-/react-truncate-inside-1.0.3.tgz", "integrity": "sha512-XwQ+9ayvygHI4WjJwaTDYFiYLrLMbHiBc5VdhWuDrCAm8MWFT6YR/+BCgq5JUwyuISYleJyZ0yCAGbXMhGr9hw==", + "license": "MIT", "peerDependencies": { "react": ">= 16.x.x" } @@ -23380,6 +24108,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "license": "MIT", "dependencies": { "pify": "^2.3.0" } @@ -23388,6 +24117,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -23751,6 +24481,7 @@ "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", @@ -23795,6 +24526,7 @@ "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", @@ -23920,6 +24652,7 @@ "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } @@ -24017,6 +24750,7 @@ "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", @@ -24059,6 +24793,7 @@ "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", "dev": true, + "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" @@ -24075,6 +24810,7 @@ "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -24162,6 +24898,7 @@ "version": "7.7.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -24345,6 +25082,7 @@ "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", "dev": true, + "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", @@ -24409,6 +25147,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", @@ -24427,6 +25166,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" @@ -24442,6 +25182,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -24459,6 +25200,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -24862,6 +25604,19 @@ "node": "*" } }, + "node_modules/split-on-first": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-3.0.0.tgz", + "integrity": "sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/split2": { "version": "3.2.2", "dev": true, @@ -24905,7 +25660,8 @@ "version": "0.0.5", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/stack-generator": { "version": "2.0.10", @@ -24986,6 +25742,7 @@ "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", "dev": true, + "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" @@ -25075,6 +25832,7 @@ "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -25089,6 +25847,7 @@ "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", @@ -25116,6 +25875,7 @@ "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", "dev": true, + "license": "MIT", "dependencies": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5" @@ -25126,6 +25886,7 @@ "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", @@ -25147,6 +25908,7 @@ "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", @@ -25265,6 +26027,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==", + "license": "MIT", "dependencies": { "client-only": "0.0.1" }, @@ -25287,6 +26050,7 @@ "version": "3.35.1", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", @@ -25308,6 +26072,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "license": "MIT", "engines": { "node": ">= 6" } @@ -25315,7 +26080,8 @@ "node_modules/sucrase/node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" }, "node_modules/supports-color": { "version": "5.5.0", @@ -25385,15 +26151,17 @@ "version": "2.6.0", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz", "integrity": "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/dcastil" } }, "node_modules/tailwindcss": { - "version": "3.4.18", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.18.tgz", - "integrity": "sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==", + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -25430,6 +26198,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", + "license": "MIT", "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders" } @@ -25438,6 +26207,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -25461,6 +26231,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", "dependencies": { "is-glob": "^4.0.1" }, @@ -25472,6 +26243,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "license": "MIT", "engines": { "node": ">=14" }, @@ -25483,6 +26255,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", "dependencies": { "picomatch": "^2.2.1" }, @@ -25733,6 +26506,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "license": "MIT", "dependencies": { "any-promise": "^1.0.0" } @@ -25741,6 +26515,7 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "license": "MIT", "dependencies": { "thenify": ">= 3.1.0 < 4" }, @@ -25786,6 +26561,7 @@ "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "license": "MIT", "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" @@ -25801,6 +26577,7 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", "engines": { "node": ">=12.0.0" }, @@ -25817,6 +26594,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", "engines": { "node": ">=12" }, @@ -25917,7 +26695,8 @@ "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "license": "Apache-2.0" }, "node_modules/ts-jest": { "version": "29.1.5", @@ -26225,6 +27004,16 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, "node_modules/type-check": { "version": "0.4.0", "dev": true, @@ -26305,6 +27094,7 @@ "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", @@ -26319,6 +27109,7 @@ "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", @@ -26338,6 +27129,7 @@ "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", "dev": true, + "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", @@ -26359,6 +27151,7 @@ "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", @@ -26486,6 +27279,13 @@ "dev": true, "license": "MIT" }, + "node_modules/ufo": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.2.tgz", + "integrity": "sha512-heMioaxBcG9+Znsda5Q8sQbWnLJSl98AFDXTO80wELWEzX3hordXsTdxrIfMQoO9IY1MEnoGoPjpoKpMj+Yx0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/uglify-js": { "version": "3.18.0", "license": "BSD-2-Clause", @@ -26501,6 +27301,7 @@ "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", @@ -26534,6 +27335,19 @@ "node": "*" } }, + "node_modules/undici": { + "version": "5.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", + "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, "node_modules/undici-types": { "version": "6.21.0", "license": "MIT" @@ -26681,6 +27495,7 @@ "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", "dev": true, "hasInstallScript": true, + "license": "MIT", "dependencies": { "napi-postinstall": "^0.3.0" }, @@ -26762,6 +27577,16 @@ "node": ">= 0.10" } }, + "node_modules/url-join": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-5.0.0.tgz", + "integrity": "sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, "node_modules/urlpattern-polyfill": { "version": "10.0.0", "license": "MIT" @@ -26770,6 +27595,7 @@ "version": "1.3.3", "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", "dependencies": { "tslib": "^2.0.0" }, @@ -26790,6 +27616,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" @@ -27018,6 +27845,7 @@ "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", "dev": true, + "license": "MIT", "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", @@ -27037,6 +27865,7 @@ "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "function.prototype.name": "^1.1.6", @@ -27064,6 +27893,7 @@ "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", "dev": true, + "license": "MIT", "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", @@ -27087,6 +27917,7 @@ "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", "dev": true, + "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", @@ -27498,10 +28329,24 @@ "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } }, + "node_modules/zod-package-json": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/zod-package-json/-/zod-package-json-1.2.0.tgz", + "integrity": "sha512-tamtgPM3MkP+obfO2dLr/G+nYoYkpJKmuHdYEy6IXRKfLybruoJ5NUj0lM0LxwOpC9PpoGLbll1ecoeyj43Wsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "zod": "^3.25.64" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/zwitch": { "version": "2.0.4", "dev": true, diff --git a/package.json b/package.json index 7ffba608c..a27f72dc1 100644 --- a/package.json +++ b/package.json @@ -45,20 +45,21 @@ "eslint-plugin-prettier": "^5.1.3", "eslint-plugin-sonarjs": "^0.25.1", "husky": "^8.0.3", + "istanbul-merge": "^2.0.0", "istanbul-reporter-html-monorepo": "^1.1.3", "jest": "^29.4.0", "lerna": "^6.4.1", "lint-staged": "^13.1.0", "nx": "15.6.2", "nyc": "^17.1.0", + "pkg-pr-new": "^0.0.62", "prettier": "^3.2.5", "ts-jest": "^29.0.5", "typedoc": "^0.26.11", "typedoc-plugin-frontmatter": "1.0.0", "typedoc-plugin-inline-sources": "1.2.0", "typedoc-plugin-markdown": "4.2.10", - "typescript": "5.1", - "istanbul-merge": "^2.0.0" + "typescript": "5.1" }, "workspaces": [ "packages/*" diff --git a/packages/api/src/graphql/modules/BlockResolver.ts b/packages/api/src/graphql/modules/BlockResolver.ts index 036184873..0acb876ca 100644 --- a/packages/api/src/graphql/modules/BlockResolver.ts +++ b/packages/api/src/graphql/modules/BlockResolver.ts @@ -4,7 +4,7 @@ import { Arg, Field, ObjectType, Query } from "type-graphql"; import { GraphqlModule, graphqlModule } from "../GraphqlModule"; -import { BatchTransactionModel } from "./model/BatchTransactionModel"; +import { TransactionExecutionResultModel } from "./model/TransactionExecutionResultModel"; @ObjectType() export class BlockModel { @@ -12,9 +12,9 @@ export class BlockModel { return new BlockModel( Number(block.networkState.during.block.height.toBigInt()), block.transactions.map((tx) => - BatchTransactionModel.fromServiceLayerModel({ + TransactionExecutionResultModel.fromServiceLayerModel({ tx: tx.tx, - status: tx.status.toBoolean(), + status: tx.status, statusMessage: tx.statusMessage, }) ), @@ -33,15 +33,15 @@ export class BlockModel { @Field() height: number; - @Field(() => [BatchTransactionModel]) - txs: BatchTransactionModel[]; + @Field(() => [TransactionExecutionResultModel]) + txs: TransactionExecutionResultModel[]; @Field() transactionsHash: string; private constructor( height: number, - txs: BatchTransactionModel[], + txs: TransactionExecutionResultModel[], transactionsHash: string, hash: string, previousBlockHash: string | undefined diff --git a/packages/api/src/graphql/modules/MempoolResolver.ts b/packages/api/src/graphql/modules/MempoolResolver.ts index 034eee651..0ea9c1a0d 100644 --- a/packages/api/src/graphql/modules/MempoolResolver.ts +++ b/packages/api/src/graphql/modules/MempoolResolver.ts @@ -161,7 +161,10 @@ export class MempoolResolver extends GraphqlModule { "Returns the hashes of all transactions that are currently inside the mempool", }) public async transactions() { - const txs = await this.transactionStorage.getPendingUserTransactions(); + const txs = await this.transactionStorage.getPendingUserTransactions( + 0, + 1000 + ); return txs.map((x) => x.hash().toString()); } } diff --git a/packages/api/src/graphql/modules/model/BatchTransactionModel.ts b/packages/api/src/graphql/modules/model/TransactionExecutionResultModel.ts similarity index 69% rename from packages/api/src/graphql/modules/model/BatchTransactionModel.ts rename to packages/api/src/graphql/modules/model/TransactionExecutionResultModel.ts index 316218778..0998e59c2 100644 --- a/packages/api/src/graphql/modules/model/BatchTransactionModel.ts +++ b/packages/api/src/graphql/modules/model/TransactionExecutionResultModel.ts @@ -1,16 +1,18 @@ import { ObjectType, Field } from "type-graphql"; -import { BatchTransaction } from "@proto-kit/sequencer"; import { IsBoolean } from "class-validator"; +import { TransactionExecutionResult } from "@proto-kit/sequencer"; import { TransactionObject } from "../MempoolResolver"; @ObjectType() -export class BatchTransactionModel { - public static fromServiceLayerModel(cbt: BatchTransaction) { +export class TransactionExecutionResultModel { + public static fromServiceLayerModel( + cbt: Pick + ) { const { tx, status, statusMessage } = cbt; - return new BatchTransactionModel( + return new TransactionExecutionResultModel( TransactionObject.fromServiceLayerModel(tx), - status, + status.toBoolean(), statusMessage ); } diff --git a/packages/cli/src/scripts/bridge/deposit.ts b/packages/cli/src/scripts/bridge/deposit.ts index 0f222b80a..cfb4ad492 100644 --- a/packages/cli/src/scripts/bridge/deposit.ts +++ b/packages/cli/src/scripts/bridge/deposit.ts @@ -9,7 +9,7 @@ import { AppChain, } from "@proto-kit/sequencer"; import { Runtime } from "@proto-kit/module"; -import { Protocol } from "@proto-kit/protocol"; +import { DispatchSmartContract, Protocol } from "@proto-kit/protocol"; import { DefaultConfigs, DefaultModules } from "@proto-kit/stack"; import { AccountUpdate, @@ -118,7 +118,10 @@ export default async function ( BridgingModule ); - const { settlement, dispatch } = settlementModule.getContracts(); + const settlement = settlementModule.getSettlementContract(); + const dispatch = + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + bridgingModule.getDispatchContract() as DispatchSmartContract; await fetchAccount({ publicKey: fromPrivateKey.toPublicKey() }); await fetchAccount({ publicKey: settlement.address }); @@ -161,12 +164,11 @@ export default async function ( ); console.log(tx.toPretty()); - settlementModule.signTransaction( - tx, - [fromPrivateKey], - [tokenOwnerPrivateKey], - [dispatch.address] - ); + settlementModule.utils.signTransaction(tx, { + signingPublicKeys: [fromPrivateKey.toPublicKey()], + preventNoncePreconditionFor: [dispatch.address], + signingWithSignatureCheck: [tokenOwnerPrivateKey.toPublicKey()], + }); console.log("Sending..."); console.log(tx.toPretty()); diff --git a/packages/cli/src/scripts/bridge/redeem.ts b/packages/cli/src/scripts/bridge/redeem.ts index 35bc126d5..7a3b2c039 100644 --- a/packages/cli/src/scripts/bridge/redeem.ts +++ b/packages/cli/src/scripts/bridge/redeem.ts @@ -134,7 +134,10 @@ export default async function ( SettlementModule ); - settlementModule.signTransaction(tx, [toPrivateKey], [tokenOwnerPrivateKey]); + settlementModule.utils.signTransaction(tx, { + signingPublicKeys: [toPrivateKey.toPublicKey()], + signingWithSignatureCheck: [tokenOwnerPrivateKey.toPublicKey()], + }); console.log("Sending..."); diff --git a/packages/cli/src/scripts/settlement/deploy-token.ts b/packages/cli/src/scripts/settlement/deploy-token.ts index 28c707158..96d06a92d 100644 --- a/packages/cli/src/scripts/settlement/deploy-token.ts +++ b/packages/cli/src/scripts/settlement/deploy-token.ts @@ -1,7 +1,7 @@ /* eslint-disable no-console */ /* eslint-disable func-names */ import { Runtime } from "@proto-kit/module"; -import { Protocol } from "@proto-kit/protocol"; +import { DispatchSmartContract, Protocol } from "@proto-kit/protocol"; import { ArchiveNode, MinaTransactionSender, @@ -10,6 +10,7 @@ import { SettlementModule, SignedSettlementPermissions, AppChain, + BridgingModule, } from "@proto-kit/sequencer"; import { AccountUpdate, @@ -98,6 +99,11 @@ export default async function ( SettlementModule ); + const bridgingModule = appChain.sequencer.resolveOrFail( + "BridgingModule", + BridgingModule + ); + const isSignedSettlement = settlementModule.utils.isSignedSettlement(); const tokenOwnerKey = PrivateKey.fromBase58( @@ -155,11 +161,13 @@ export default async function ( console.log("Sending deploy transaction..."); console.log(tx.toPretty()); - settlementModule.signTransaction( - tx, - [feepayerPrivateKey, tokenOwnerKey, tokenAdminKey], - [tokenOwnerKey, tokenAdminKey] - ); + settlementModule.utils.signTransaction(tx, { + signingWithSignatureCheck: [ + tokenOwnerKey.toPublicKey(), + tokenAdminKey.toPublicKey(), + ], + signingPublicKeys: [feepayerPrivateKey.toPublicKey()], + }); await appChain.sequencer .resolveOrFail("TransactionSender", MinaTransactionSender) @@ -193,11 +201,14 @@ export default async function ( await tokenOwner!.mint(receiverPublicKey, UInt64.from(mintAmount)); } ); - settlementModule.utils.signTransaction( - tx, - [feepayerPrivateKey], - [tokenOwnerKey, tokenAdminKey] - ); + + settlementModule.utils.signTransaction(tx, { + signingPublicKeys: [feepayerPrivateKey.toPublicKey()], + signingWithSignatureCheck: [ + tokenOwnerKey.toPublicKey(), + tokenAdminKey.toPublicKey(), + ], + }); await appChain.sequencer .resolveOrFail("TransactionSender", MinaTransactionSender) @@ -205,19 +216,23 @@ export default async function ( } async function deployBridge() { - const { settlement, dispatch } = settlementModule.getAddresses(); + const settlement = settlementModule.getSettlementContract(); + + const dispatch = + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + bridgingModule.getDispatchContract() as DispatchSmartContract; + await fetchAccount({ - publicKey: settlementModule.config.feepayer.toPublicKey(), + publicKey: settlementModule.utils.getSigner(), }); - await fetchAccount({ publicKey: settlement }); - await fetchAccount({ publicKey: dispatch }); + await fetchAccount({ publicKey: settlement.address }); + await fetchAccount({ publicKey: dispatch.address }); const tokenOwner = new FungibleToken(tokenOwnerKey.toPublicKey()); // SetAdminEvent. - await settlementModule.deployTokenBridge( + await bridgingModule.deployTokenBridge( tokenOwner, - tokenOwnerKey, - tokenBridgeKey, + tokenBridgeKey.toPublicKey(), {} ); console.log( diff --git a/packages/cli/src/scripts/settlement/deploy.ts b/packages/cli/src/scripts/settlement/deploy.ts index 9e0530fb0..c36f836fe 100644 --- a/packages/cli/src/scripts/settlement/deploy.ts +++ b/packages/cli/src/scripts/settlement/deploy.ts @@ -9,7 +9,7 @@ import { SettlementModule, AppChain, } from "@proto-kit/sequencer"; -import { PrivateKey, Provable } from "o1js"; +import { Provable, PublicKey } from "o1js"; import "reflect-metadata"; import { container } from "tsyringe"; import { DefaultConfigs, DefaultModules } from "@proto-kit/stack"; @@ -59,25 +59,22 @@ export default async function (options?: LoadEnvOptions) { console.log("Deploying settlement contracts..."); - await settlementModule.deploy( - PrivateKey.fromBase58( - getRequiredEnv("PROTOKIT_SETTLEMENT_CONTRACT_PRIVATE_KEY") + await settlementModule.deploy({ + settlementContract: PublicKey.fromBase58( + getRequiredEnv("PROTOKIT_SETTLEMENT_CONTRACT_PUBLIC_KEY") ), - PrivateKey.fromBase58( - getRequiredEnv("PROTOKIT_DISPATCHER_CONTRACT_PRIVATE_KEY") + dispatchContract: PublicKey.fromBase58( + getRequiredEnv("PROTOKIT_DISPATCHER_CONTRACT_PUBLIC_KEY") ), - PrivateKey.fromBase58( - getRequiredEnv("PROTOKIT_MINA_BRIDGE_CONTRACT_PRIVATE_KEY") - ) - ); + }); Provable.log("Deployed and initialized settlement contracts", { - settlement: PrivateKey.fromBase58( - getRequiredEnv("PROTOKIT_SETTLEMENT_CONTRACT_PRIVATE_KEY") - ).toPublicKey(), - dispatcher: PrivateKey.fromBase58( - getRequiredEnv("PROTOKIT_DISPATCHER_CONTRACT_PRIVATE_KEY") - ).toPublicKey(), + settlement: PublicKey.fromBase58( + getRequiredEnv("PROTOKIT_SETTLEMENT_CONTRACT_PUBLIC_KEY") + ), + dispatcher: PublicKey.fromBase58( + getRequiredEnv("PROTOKIT_DISPATCHER_CONTRACT_PUBLIC_KEY") + ), }); await appChain.close(); diff --git a/packages/common/src/compiling/CompileRegistry.ts b/packages/common/src/compiling/CompileRegistry.ts index e34dbc4b5..a85766ffe 100644 --- a/packages/common/src/compiling/CompileRegistry.ts +++ b/packages/common/src/compiling/CompileRegistry.ts @@ -47,22 +47,17 @@ export class CompileRegistry { return result; } - public async compile(target: CompileTarget, proverNeeded: boolean = true) { - if (this.artifacts[target.name] === undefined || this.inForceProverBlock) { + public async compile(target: CompileTarget, nameOverride?: string) { + const name = nameOverride ?? target.name; + if (this.artifacts[name] === undefined || this.inForceProverBlock) { const artifact = await this.compiler.compileContract(target); - this.artifacts[target.name] = artifact; + this.artifacts[name] = artifact; return artifact; } - return this.artifacts[target.name]; + return this.artifacts[name]; } public getArtifact(name: string): CompileArtifact | undefined { - if (this.artifacts[name] === undefined) { - throw new Error( - `Artifact for ${name} not available, did you compile it via the CompileRegistry?` - ); - } - return this.artifacts[name]; } diff --git a/packages/common/src/log.ts b/packages/common/src/log.ts index ed3c39ef1..a8659d239 100644 --- a/packages/common/src/log.ts +++ b/packages/common/src/log.ts @@ -28,6 +28,8 @@ function logProvable( // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions if (process.env?.IN_CI ?? false) { loglevel.setLevel("ERROR"); +} else { + loglevel.setLevel("INFO"); } const timeMap: Record = {}; diff --git a/packages/common/src/trees/sparse/RollupMerkleTree.ts b/packages/common/src/trees/sparse/RollupMerkleTree.ts index 1974e7078..e7503df46 100644 --- a/packages/common/src/trees/sparse/RollupMerkleTree.ts +++ b/packages/common/src/trees/sparse/RollupMerkleTree.ts @@ -7,6 +7,17 @@ import { TypedClass } from "../../types"; import { MerkleTreeStore } from "./MerkleTreeStore"; import { InMemoryMerkleTreeStorage } from "./InMemoryMerkleTreeStorage"; +/** + * More efficient version of `maybeSwapBad` which + * reuses an intermediate variable + */ +export function maybeSwap(b: Bool, x: Field, y: Field): [Field, Field] { + const m = b.toField().mul(x.sub(y)); // b*(x - y) + const x1 = y.add(m); // y + b*(x - y) + const y2 = x.sub(m); // x - b*(x - y) = x + b*(y - x) + return [x1, y2]; +} + export class StructTemplate extends Struct({ path: Provable.Array(Field, 0), isLeft: Provable.Array(Bool, 0), @@ -22,6 +33,11 @@ export interface AbstractMerkleWitness extends StructTemplate { */ calculateRoot(hash: Field): Field; + calculateRootIncrement( + index: Field, + leaf: Field + ): [Field, AbstractMerkleWitness]; + /** * Calculates the index of the leaf node that belongs to this Witness. * @returns Index of the leaf. @@ -119,6 +135,24 @@ export interface AbstractMerkleTreeClass { * It also holds the Witness class under tree.WITNESS */ export function createMerkleTree(height: number): AbstractMerkleTreeClass { + function generateZeroes() { + const zeroes = [0n]; + for (let index = 1; index < height; index += 1) { + const previousLevel = Field(zeroes[index - 1]); + zeroes.push(Poseidon.hash([previousLevel, previousLevel]).toBigInt()); + } + return zeroes; + } + + let zeroCache: bigint[] | undefined = undefined; + + function getZeroes() { + if (zeroCache === undefined) { + zeroCache = generateZeroes(); + } + return zeroCache; + } + /** * The {@link RollupMerkleWitness} class defines a circuit-compatible base class * for [Merkle Witness'](https://computersciencewiki.org/index.php/Merkle_proof). @@ -147,7 +181,7 @@ export function createMerkleTree(height: number): AbstractMerkleTreeClass { for (let index = 1; index < n; ++index) { const isLeft = this.isLeft[index - 1]; - // eslint-disable-next-line @typescript-eslint/no-use-before-define + const [left, right] = maybeSwap(isLeft, hash, this.path[index - 1]); hash = Poseidon.hash([left, right]); } @@ -155,6 +189,73 @@ export function createMerkleTree(height: number): AbstractMerkleTreeClass { return hash; } + public calculateRootIncrement( + leafIndex: Field, + leaf: Field + ): [Field, RollupMerkleWitness] { + const root = this.calculateRoot(leaf); + + const newWitness = Provable.witness(RollupMerkleWitness, () => { + const zero = getZeroes(); + + if (zero.length === 0) { + throw new Error("Zeroes not initialized"); + } + const zeroes = zero.map((x) => Field(x)); + + let hash = leaf; + const n = this.height(); + + let notDiverged = true; + const newPath = leafIndex.add(1).toBits(); + newPath.push(Bool(false)); + + const newSiblings: Field[] = []; + const newIsLefts: Bool[] = []; + + for (let index = 0; index < n - 1; ++index) { + const isLeft = this.isLeft[index]; + const sibling = this.path[index]; + + const newIsLeft = newPath[index].not(); + + // Bool(true) default for root level + let convergesNextLevel = true; + if (index < n - 2) { + convergesNextLevel = newPath[index + 1] + .equals(this.isLeft[index + 1]) + .not() + .toBoolean(); + } + + const nextSibling = + // eslint-disable-next-line no-nested-ternary + convergesNextLevel && notDiverged + ? hash + : notDiverged + ? zeroes[index] + : sibling; + + notDiverged = notDiverged && !convergesNextLevel; + + newSiblings.push(nextSibling); + newIsLefts.push(newIsLeft); + + const [left, right] = maybeSwap(isLeft, hash, sibling); + hash = Poseidon.hash([left, right]); + } + + return new RollupMerkleWitness({ + isLeft: newIsLefts, + path: newSiblings, + }); + }); + + newWitness.calculateIndex().assertEquals(leafIndex.add(1)); + + return [root, newWitness]; + } + /** * Calculates the index of the leaf node that belongs to this Witness. * @returns Index of the leaf. @@ -215,6 +316,7 @@ export function createMerkleTree(height: number): AbstractMerkleTreeClass { }); } } + return class AbstractRollupMerkleTree implements AbstractMerkleTree { public static HEIGHT = height; @@ -238,13 +340,7 @@ export function createMerkleTree(height: number): AbstractMerkleTreeClass { public constructor(store: MerkleTreeStore) { this.store = store; - this.zeroes = [0n]; - for (let index = 1; index < AbstractRollupMerkleTree.HEIGHT; index += 1) { - const previousLevel = Field(this.zeroes[index - 1]); - this.zeroes.push( - Poseidon.hash([previousLevel, previousLevel]).toBigInt() - ); - } + this.zeroes = generateZeroes(); } public assertIndexRange(index: bigint) { @@ -414,14 +510,3 @@ export function createMerkleTree(height: number): AbstractMerkleTreeClass { export class RollupMerkleTree extends createMerkleTree(256) {} export class RollupMerkleTreeWitness extends RollupMerkleTree.WITNESS {} - -/** - * More efficient version of `maybeSwapBad` which - * reuses an intermediate variable - */ -export function maybeSwap(b: Bool, x: Field, y: Field): [Field, Field] { - const m = b.toField().mul(x.sub(y)); // b*(x - y) - const x1 = y.add(m); // y + b*(x - y) - const y2 = x.sub(m); // x - b*(x - y) = x + b*(y - x) - return [x1, y2]; -} diff --git a/packages/common/src/types.ts b/packages/common/src/types.ts index 4d25b1192..f2a0e452e 100644 --- a/packages/common/src/types.ts +++ b/packages/common/src/types.ts @@ -1,5 +1,13 @@ // allows to reference interfaces as 'classes' rather than instances -import { Bool, DynamicProof, Field, Proof, ProofBase, PublicKey } from "o1js"; +import { + Bool, + DynamicProof, + Field, + Proof, + ProofBase, + PublicKey, + Option, +} from "o1js"; export type TypedClass = new (...args: any[]) => Class; @@ -56,3 +64,5 @@ export type InferProofBase< : ProofType extends DynamicProof ? ProofBase : undefined; + +export class O1PublicKeyOption extends Option(PublicKey) {} diff --git a/packages/common/src/utils.ts b/packages/common/src/utils.ts index 0576aacb5..27a0a036c 100644 --- a/packages/common/src/utils.ts +++ b/packages/common/src/utils.ts @@ -87,7 +87,11 @@ export function yieldSequential( array, async ([state, collectedTargets], curr, index, arr) => { const [newState, addition] = await callbackfn(state, curr, index, arr); - return [newState, collectedTargets.concat(addition)]; + // The reason we wrap this in an array here is for a special case where Target is a tuple + // or array itself. In this case, js interprets by flattening the Value in the array + // (which it does when a function (like concat) uses a spread operator and the + // input is an array) + return [newState, collectedTargets.concat([addition])]; }, [initialValue, []] ); @@ -105,6 +109,18 @@ export function mapSequential( }, Promise.resolve([])); } +export function unzip(array: [A, B][]): [A[], B[]] { + const as = array.map(([a]) => a); + const bs = array.map(([, b]) => b); + return [as, bs]; +} + +export function assertSizeOneOrTwo(arr: T[]): asserts arr is [T] | [T, T] { + if (!(arr.length === 1 || arr.length === 2)) { + throw new Error("Given array not size 1 or 2"); + } +} + /** * Computes a dummy value for the given value type. * diff --git a/packages/common/test/trees/MerkleTree.test.ts b/packages/common/test/trees/MerkleTree.test.ts index 221c8d70e..b2c673eb8 100644 --- a/packages/common/test/trees/MerkleTree.test.ts +++ b/packages/common/test/trees/MerkleTree.test.ts @@ -217,4 +217,35 @@ describe.each([4, 16, 256])("cachedMerkleTree - %s", (height) => { tree.getNode(0, index); }).toThrow("Index greater than maximum leaf number"); }); + + it("witness incrementing", () => { + tree.setLeaf(0n, Field(3256)); + tree.setLeaf(1n, Field(3256)); + tree.setLeaf(2n, Field(3256)); + + const witness = tree.getWitness(3n); + + const [root, newWitness] = witness.calculateRootIncrement( + Field(3), + Field(1234) + ); + tree.setLeaf(3n, Field(1234)); + + expect(tree.getRoot().toString()).toStrictEqual(root.toString()); + expect(newWitness.calculateIndex().toString()).toStrictEqual("4"); + + const [root2, newWitness2] = newWitness.calculateRootIncrement( + Field(4), + Field(4321) + ); + tree.setLeaf(4n, Field(4321)); + + expect(tree.getRoot().toString()).toStrictEqual(root2.toString()); + expect(newWitness2.calculateIndex().toString()).toStrictEqual("5"); + + const root3 = newWitness2.calculateRoot(Field(555)); + tree.setLeaf(5n, Field(555)); + + expect(tree.getRoot().toString()).toStrictEqual(root3.toString()); + }); }); diff --git a/packages/indexer/src/IndexerNotifier.ts b/packages/indexer/src/IndexerNotifier.ts index 5f6816f8b..93a33921c 100644 --- a/packages/indexer/src/IndexerNotifier.ts +++ b/packages/indexer/src/IndexerNotifier.ts @@ -81,7 +81,7 @@ export class IndexerNotifier extends SequencerModule> { await txQueue.addTask(task); } catch (err) { - console.error("Failed to add pending-tx task", err); + log.error("Failed to add pending-tx task", err); } }); this.sequencer.events.on("batch-produced", async (batch) => { diff --git a/packages/indexer/src/tasks/IndexPendingTxTask.ts b/packages/indexer/src/tasks/IndexPendingTxTask.ts index db468660d..9d448c6a7 100644 --- a/packages/indexer/src/tasks/IndexPendingTxTask.ts +++ b/packages/indexer/src/tasks/IndexPendingTxTask.ts @@ -30,7 +30,7 @@ export class IndexPendingTxTask public async compute(input: PendingTransaction): Promise { try { - await this.transactionStorage.pushUserTransaction(input); + await this.transactionStorage.pushUserTransaction(input, 0); return ""; } catch (err) { log.error("Failed to process pending tx task", err); diff --git a/packages/library/src/hooks/RuntimeFeeAnalyzerService.ts b/packages/library/src/hooks/RuntimeFeeAnalyzerService.ts index 707d80a4c..1e8d2581c 100644 --- a/packages/library/src/hooks/RuntimeFeeAnalyzerService.ts +++ b/packages/library/src/hooks/RuntimeFeeAnalyzerService.ts @@ -79,8 +79,8 @@ export class RuntimeFeeAnalyzerService extends ConfigurableModule { + public async beforeTransaction({ + transaction: { transaction }, + }: BeforeTransactionHookArguments): Promise { const feeConfig = Provable.witness(MethodFeeConfigData, () => - this.feeAnalyzer.getFeeConfig( - executionData.transaction.methodId.toBigInt() - ) + this.feeAnalyzer.getFeeConfig(transaction.methodId.toBigInt()) ); const witness = Provable.witness( RuntimeFeeAnalyzerService.getWitnessType(), - () => - this.feeAnalyzer.getWitness( - executionData.transaction.methodId.toBigInt() - ) + () => this.feeAnalyzer.getWitness(transaction.methodId.toBigInt()) ); const root = Field(this.feeAnalyzer.getRoot()); @@ -147,14 +142,14 @@ export class TransactionFeeHook extends ProvableTransactionHook { + public async removeTransactionWhen({ + transaction, + }: BeforeTransactionHookArguments): Promise { const feeConfig = this.feeAnalyzer.getFeeConfig( - args.transaction.methodId.toBigInt() + transaction.transaction.methodId.toBigInt() ); const fee = this.getFee(feeConfig); const tokenId = new TokenId(this.config.tokenId); - const feeRecipient = PublicKey.fromBase58(this.config.feeRecipient); const balanceAvailable = await this.balances.balances.get({ tokenId, - address: feeRecipient, + address: transaction.transaction.sender.value, }); - return balanceAvailable.orElse(Balance.from(0)).lessThan(fee).toBoolean(); + return balanceAvailable + .orElse(Balance.from(0)) + .lessThan(fee) + .or(transaction.isMessage) + .toBoolean(); } } diff --git a/packages/library/src/protocol/VanillaProtocolModules.ts b/packages/library/src/protocol/VanillaProtocolModules.ts index 89a87d08b..20b936b05 100644 --- a/packages/library/src/protocol/VanillaProtocolModules.ts +++ b/packages/library/src/protocol/VanillaProtocolModules.ts @@ -1,11 +1,7 @@ import { - AccountStateHook, - BlockHeightHook, - BlockProver, MandatoryProtocolModulesRecord, ProtocolModulesRecord, - StateTransitionProver, - LastStateRootBlockHook, + Protocol, } from "@proto-kit/protocol"; import { PrivateKey } from "o1js"; @@ -20,11 +16,7 @@ export class VanillaProtocolModules { additionalModules: ProtocolModules ): MandatoryProtocolModulesRecord & ProtocolModules { return { - StateTransitionProver, - BlockProver, - AccountState: AccountStateHook, - BlockHeight: BlockHeightHook, - LastStateRoot: LastStateRootBlockHook, + ...Protocol.defaultModules(), ...additionalModules, }; } @@ -40,11 +32,7 @@ export class VanillaProtocolModules { public static mandatoryConfig() { return { - BlockProver: {}, - StateTransitionProver: {}, - AccountState: {}, - BlockHeight: {}, - LastStateRoot: {}, + ...Protocol.defaultConfig(), }; } diff --git a/packages/module/src/method/runtimeMethod.ts b/packages/module/src/method/runtimeMethod.ts index d53a9439c..ffa8145af 100644 --- a/packages/module/src/method/runtimeMethod.ts +++ b/packages/module/src/method/runtimeMethod.ts @@ -25,11 +25,6 @@ const errors = { runtimeNotProvided: (name: string) => new Error(`Runtime was not provided for module: ${name}`), - methodInputsNotProvided: () => - new Error( - "Method execution inputs not provided, provide them via context.inputs" - ), - runtimeNameNotSet: () => new Error("Runtime name was not set"), fieldNotConstant: (name: string) => diff --git a/packages/module/test/method/MethodParameterEncoder.test.ts b/packages/module/test/method/MethodParameterEncoder.test.ts index 9f4bb5c05..38436ead8 100644 --- a/packages/module/test/method/MethodParameterEncoder.test.ts +++ b/packages/module/test/method/MethodParameterEncoder.test.ts @@ -1,3 +1,4 @@ +import "reflect-metadata"; import { Struct, Field, diff --git a/packages/module/test/method/runtimeMethod-fail.test.ts b/packages/module/test/method/runtimeMethod-fail.test.ts index 79eb331b7..56ba1e1d6 100644 --- a/packages/module/test/method/runtimeMethod-fail.test.ts +++ b/packages/module/test/method/runtimeMethod-fail.test.ts @@ -1,3 +1,4 @@ +import "reflect-metadata"; import { Bool, Field, PublicKey, Struct, ZkProgram } from "o1js"; import { noop } from "@proto-kit/common"; diff --git a/packages/persistance/prisma/migrations/20260117134313_transaction_priority/migration.sql b/packages/persistance/prisma/migrations/20260117134313_transaction_priority/migration.sql new file mode 100644 index 000000000..e04752925 --- /dev/null +++ b/packages/persistance/prisma/migrations/20260117134313_transaction_priority/migration.sql @@ -0,0 +1,10 @@ +-- CreateTable +CREATE TABLE "TransactionPriority" ( + "transactionHash" TEXT NOT NULL, + "priority" BIGINT NOT NULL, + + CONSTRAINT "TransactionPriority_pkey" PRIMARY KEY ("transactionHash") +); + +-- AddForeignKey +ALTER TABLE "TransactionPriority" ADD CONSTRAINT "TransactionPriority_transactionHash_fkey" FOREIGN KEY ("transactionHash") REFERENCES "Transaction"("hash") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/packages/persistance/prisma/migrations/20260122210623_transaction_input_paths/migration.sql b/packages/persistance/prisma/migrations/20260122210623_transaction_input_paths/migration.sql new file mode 100644 index 000000000..8f9d6cd06 --- /dev/null +++ b/packages/persistance/prisma/migrations/20260122210623_transaction_input_paths/migration.sql @@ -0,0 +1,10 @@ +-- CreateTable +CREATE TABLE "SkippedTransactionInputPaths" ( + "transactionHash" TEXT NOT NULL, + "paths" DECIMAL(78,0)[], + + CONSTRAINT "SkippedTransactionInputPaths_pkey" PRIMARY KEY ("transactionHash") +); + +-- AddForeignKey +ALTER TABLE "SkippedTransactionInputPaths" ADD CONSTRAINT "SkippedTransactionInputPaths_transactionHash_fkey" FOREIGN KEY ("transactionHash") REFERENCES "Transaction"("hash") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/packages/persistance/prisma/schema.prisma b/packages/persistance/prisma/schema.prisma index d0ed1472e..3613a00a6 100644 --- a/packages/persistance/prisma/schema.prisma +++ b/packages/persistance/prisma/schema.prisma @@ -54,6 +54,28 @@ model Transaction { executionResult TransactionExecutionResult? IncomingMessageBatchTransaction IncomingMessageBatchTransaction[] + + priority TransactionPriority? + + inputPaths SkippedTransactionInputPaths? +} + +model TransactionPriority { + transactionHash String + + priority BigInt + + Transaction Transaction @relation(fields: [transactionHash], references: [hash]) + + @@id([transactionHash]) +} + +model SkippedTransactionInputPaths { + transactionHash String @id + + paths Decimal[] @db.Decimal(78, 0) + + transaction Transaction @relation(fields: [transactionHash], references: [hash]) } model TransactionExecutionResult { diff --git a/packages/persistance/src/services/prisma/PrismaTransactionStorage.ts b/packages/persistance/src/services/prisma/PrismaTransactionStorage.ts index d4544a7c4..5722421ed 100644 --- a/packages/persistance/src/services/prisma/PrismaTransactionStorage.ts +++ b/packages/persistance/src/services/prisma/PrismaTransactionStorage.ts @@ -9,6 +9,7 @@ import { import type { PrismaConnection } from "../../PrismaDatabaseConnection"; import { TransactionMapper } from "./mappers/TransactionMapper"; +import { Decimal } from "./PrismaStateService"; @injectable() export class PrismaTransactionStorage implements TransactionStorage { @@ -19,7 +20,10 @@ export class PrismaTransactionStorage implements TransactionStorage { ) {} @trace("db.txs.get") - public async getPendingUserTransactions(): Promise { + public async getPendingUserTransactions( + offset: number, + limit?: number + ): Promise { const { prismaClient } = this.connection; const txs = await prismaClient.transaction.findMany({ @@ -30,7 +34,17 @@ export class PrismaTransactionStorage implements TransactionStorage { isMessage: { equals: false, }, + inputPaths: { + is: null, + }, + }, + orderBy: { + priority: { + priority: "desc", + }, }, + skip: offset, + take: limit, }); return txs.map((tx) => this.transactionMapper.mapIn(tx)); } @@ -51,13 +65,27 @@ export class PrismaTransactionStorage implements TransactionStorage { } } - public async pushUserTransaction(tx: PendingTransaction): Promise { + public async pushUserTransaction( + tx: PendingTransaction, + priority: number + ): Promise { const { prismaClient } = this.connection; - const result = await prismaClient.transaction.createMany({ - data: [this.transactionMapper.mapOut(tx)], - skipDuplicates: true, - }); + const transactionData = this.transactionMapper.mapOut(tx); + + const [result] = await prismaClient.$transaction([ + prismaClient.transaction.createMany({ + data: [transactionData], + skipDuplicates: true, + }), + + prismaClient.transactionPriority.create({ + data: { + priority, + transactionHash: transactionData.hash, + }, + }), + ]); return result.count === 1; } @@ -103,4 +131,29 @@ export class PrismaTransactionStorage implements TransactionStorage { batch, }; } + + public async reportSkippedTransactions( + paths: Record + ): Promise { + const { prismaClient } = this.connection; + + await prismaClient.skippedTransactionInputPaths.createMany({ + data: Object.entries(paths).map(([transactionHash, pathArray]) => ({ + transactionHash, + paths: pathArray.map((path) => new Decimal(path.toString())), + })), + }); + } + + public async reportChangedPaths(paths: bigint[]): Promise { + const { prismaClient } = this.connection; + + await prismaClient.skippedTransactionInputPaths.deleteMany({ + where: { + paths: { + hasSome: paths.map((path) => new Decimal(path.toString())), + }, + }, + }); + } } diff --git a/packages/persistance/test-integration/PrismaBlockProduction.test.ts b/packages/persistance/test-integration/PrismaBlockProduction.test.ts index f6299f6b1..e9d335156 100644 --- a/packages/persistance/test-integration/PrismaBlockProduction.test.ts +++ b/packages/persistance/test-integration/PrismaBlockProduction.test.ts @@ -251,7 +251,7 @@ describe("prisma integration", () => { PrismaTransactionStorage ); - const txs = await txResolver.getPendingUserTransactions(); + const txs = await txResolver.getPendingUserTransactions(0); expectDefined(transaction.transaction); diff --git a/packages/persistance/test-integration/SequencerRestart.test.ts b/packages/persistance/test-integration/SequencerRestart.test.ts index 940bc40ca..2864424b9 100644 --- a/packages/persistance/test-integration/SequencerRestart.test.ts +++ b/packages/persistance/test-integration/SequencerRestart.test.ts @@ -40,7 +40,7 @@ describe("sequencer restart", () => { }; const teardown = async () => { - await appChain.sequencer.resolve("Database").close(); + await appChain.close(); }; beforeAll(async () => { diff --git a/packages/persistance/test-integration/utils.ts b/packages/persistance/test-integration/utils.ts index 7c50982c1..e62050794 100644 --- a/packages/persistance/test-integration/utils.ts +++ b/packages/persistance/test-integration/utils.ts @@ -126,6 +126,7 @@ export function createPrismaAppchain( AccountState: {}, BlockProver: {}, StateTransitionProver: {}, + TransactionProver: {}, BlockHeight: {}, LastStateRoot: {}, }, diff --git a/packages/protocol/src/hooks/AccountStateHook.ts b/packages/protocol/src/hooks/AccountStateHook.ts index 6e05f4762..021b67999 100644 --- a/packages/protocol/src/hooks/AccountStateHook.ts +++ b/packages/protocol/src/hooks/AccountStateHook.ts @@ -14,15 +14,19 @@ export class AccountState extends Struct({ nonce: UInt64, }) {} +export type AccountStateHookConfig = { + maximumNonceLookahead?: number; +}; + @injectable() -export class AccountStateHook extends ProvableTransactionHook { +export class AccountStateHook extends ProvableTransactionHook { @state() public accountState = StateMap.from( PublicKey, AccountState ); public async beforeTransaction({ - transaction, + transaction: { transaction }, }: BeforeTransactionHookArguments) { const sender = transaction.sender.value; @@ -57,7 +61,7 @@ export class AccountStateHook extends ProvableTransactionHook { // Under these conditions we want the tx removed from the mempool. public async removeTransactionWhen({ - transaction, + transaction: { transaction }, }: BeforeTransactionHookArguments): Promise { const sender = transaction.sender.value; @@ -67,6 +71,10 @@ export class AccountStateHook extends ProvableTransactionHook { const currentNonce = accountState.nonce; - return transaction.nonce.value.lessThan(currentNonce).toBoolean(); + const exceedsMaximumLookahead = transaction.nonce.value.greaterThan( + currentNonce.add(this.config.maximumNonceLookahead ?? 10) + ); + const nonceIsInPast = transaction.nonce.value.lessThan(currentNonce); + return nonceIsInPast.or(exceedsMaximumLookahead).toBoolean(); } } diff --git a/packages/protocol/src/hooks/NoopSettlementHook.ts b/packages/protocol/src/hooks/NoopSettlementHook.ts index 166ff678f..99ecbde77 100644 --- a/packages/protocol/src/hooks/NoopSettlementHook.ts +++ b/packages/protocol/src/hooks/NoopSettlementHook.ts @@ -5,14 +5,14 @@ import { ProvableSettlementHook, SettlementHookInputs, } from "../settlement/modularity/ProvableSettlementHook"; -import { SettlementSmartContractBase } from "../settlement/contracts/SettlementSmartContract"; +import { SettlementContractType } from "../settlement/contracts/settlement/SettlementBase"; @injectable() export class NoopSettlementHook extends ProvableSettlementHook< Record > { public async beforeSettlement( - contract: SettlementSmartContractBase, + contract: SettlementContractType, state: SettlementHookInputs ) { noop(); diff --git a/packages/protocol/src/index.ts b/packages/protocol/src/index.ts index eeb2ba0d5..99cf03fdf 100644 --- a/packages/protocol/src/index.ts +++ b/packages/protocol/src/index.ts @@ -6,7 +6,7 @@ export * from "./model/StateTransitionProvableBatch"; export * from "./model/Option"; export * from "./model/Path"; export * from "./model/network/NetworkState"; -export * from "./model/transaction/SignedTransaction"; +export * from "./model/transaction/AuthorizedTransaction"; export * from "./model/transaction/RuntimeTransaction"; export * from "./model/transaction/ValueOption"; export * from "./model/MethodPublicOutput"; @@ -22,6 +22,7 @@ export * from "./prover/accumulators/StateTransitionReductionList"; export * from "./prover/accumulators/AppliedBatchHashList"; export * from "./prover/accumulators/WitnessedRootHashList"; export * from "./prover/accumulators/TransactionHashList"; +export * from "./prover/accumulators/BlockHashList"; export * from "./prover/block/BlockProver"; export * from "./prover/block/BlockProvable"; export * from "./prover/block/accummulators/RuntimeVerificationKeyTree"; @@ -29,6 +30,9 @@ export * from "./prover/block/accummulators/BlockHashMerkleTree"; export * from "./prover/block/services/RuntimeVerificationKeyRootService"; export * from "./prover/statetransition/StateTransitionProver"; export * from "./prover/statetransition/StateTransitionProvable"; +export * from "./prover/transaction/TransactionProver"; +export * from "./prover/transaction/TransactionProvable"; +export * from "./prover/utils"; export * from "./protocol/Protocol"; export * from "./protocol/ProtocolModule"; export * from "./protocol/ProtocolEnvironment"; @@ -45,8 +49,11 @@ export * from "./state/assert/assert"; export * from "./settlement/contracts/authorizations/ContractAuthorization"; export * from "./settlement/contracts/authorizations/UpdateMessagesHashAuth"; export * from "./settlement/contracts/authorizations/TokenBridgeDeploymentAuth"; -export * from "./settlement/contracts/SettlementSmartContract"; -export * from "./settlement/contracts/SettlementContractProtocolModule"; +export * from "./settlement/contracts/settlement/SettlementBase"; +export * from "./settlement/contracts/settlement/SettlementContract"; +export * from "./settlement/contracts/settlement/BridgingSettlementContract"; +export * from "./settlement/contracts/BridgingSettlementContractModule"; +export * from "./settlement/contracts/SettlementSmartContractModule"; export * from "./settlement/contracts/DispatchSmartContract"; export * from "./settlement/contracts/DispatchContractProtocolModule"; export * from "./settlement/contracts/BridgeContract"; @@ -60,6 +67,7 @@ export * from "./settlement/messages/OutgoingMessageArgument"; export * from "./settlement/messages/OutgoingMessage"; export * from "./settlement/modules/NetworkStateSettlementModule"; export * from "./settlement/messages/Deposit"; +export * from "./settlement/ContractArgsRegistry"; export { constants as ProtocolConstants } from "./Constants"; export * from "./hashing/protokit-prefixes"; export * from "./hashing/mina-prefixes"; diff --git a/packages/protocol/src/model/transaction/SignedTransaction.ts b/packages/protocol/src/model/transaction/AuthorizedTransaction.ts similarity index 64% rename from packages/protocol/src/model/transaction/SignedTransaction.ts rename to packages/protocol/src/model/transaction/AuthorizedTransaction.ts index 8f29c7752..a870624bb 100644 --- a/packages/protocol/src/model/transaction/SignedTransaction.ts +++ b/packages/protocol/src/model/transaction/AuthorizedTransaction.ts @@ -2,26 +2,30 @@ import { Bool, Field, Scalar, Signature, Struct, UInt64 } from "o1js"; import { RuntimeTransaction } from "./RuntimeTransaction"; -export class SignedTransaction extends Struct({ +export class AuthorizedTransaction extends Struct({ transaction: RuntimeTransaction, signature: Signature, + isMessage: Bool, }) { public static getSignatureData(args: { methodId: Field; nonce: UInt64; argsHash: Field; }): Field[] { + // No isMessage here - we don't sign that return [args.methodId, ...args.nonce.value.toFields(), args.argsHash]; } - public static dummy(): SignedTransaction { - return new SignedTransaction({ + public static dummy(): AuthorizedTransaction { + return new AuthorizedTransaction({ transaction: RuntimeTransaction.dummyTransaction(), signature: Signature.fromObject({ s: Scalar.from(0), r: Field(0), }), + + isMessage: Bool(false), }); } @@ -31,17 +35,16 @@ export class SignedTransaction extends Struct({ public getSignatureData(): Field[] { const { methodId, argsHash, nonce } = this.transaction; - return SignedTransaction.getSignatureData({ + return AuthorizedTransaction.getSignatureData({ nonce: nonce.value, methodId, argsHash, }); } - public validateSignature(): Bool { - return this.signature.verify( - this.transaction.sender.value, - this.getSignatureData() - ); + public validateAuthorization(): Bool { + return this.signature + .verify(this.transaction.sender.value, this.getSignatureData()) + .or(this.isMessage); } } diff --git a/packages/protocol/src/protocol/Protocol.ts b/packages/protocol/src/protocol/Protocol.ts index 0d1496d86..4b0e4d240 100644 --- a/packages/protocol/src/protocol/Protocol.ts +++ b/packages/protocol/src/protocol/Protocol.ts @@ -3,6 +3,7 @@ import { ChildContainerProvider, log, ModuleContainer, + ModulesConfig, ModulesRecord, Startable, StringKeyOf, @@ -21,6 +22,10 @@ import { ProvableSettlementHook } from "../settlement/modularity/ProvableSettlem import { NoopSettlementHook } from "../hooks/NoopSettlementHook"; import { AccountStateHook } from "../hooks/AccountStateHook"; import { NoopTransactionHook } from "../hooks/NoopTransactionHook"; +import { TransactionProvable } from "../prover/transaction/TransactionProvable"; +import { StateTransitionProver } from "../prover/statetransition/StateTransitionProver"; +import { TransactionProver } from "../prover/transaction/TransactionProver"; +import { BlockProver } from "../prover/block/BlockProver"; import { ProtocolModule } from "./ProtocolModule"; import { ProvableTransactionHook } from "./ProvableTransactionHook"; @@ -44,6 +49,10 @@ export type ProtocolModulesRecord = ModulesRecord< TypedClass> >; +export interface TransactionProverType + extends ProtocolModule, + TransactionProvable {} + export interface BlockProverType extends ProtocolModule, BlockProvable {} export interface StateTransitionProverType @@ -51,6 +60,7 @@ export interface StateTransitionProverType StateTransitionProvable {} export type MandatoryProtocolModulesRecord = { + TransactionProver: TypedClass; BlockProver: TypedClass; StateTransitionProver: TypedClass; AccountState: TypedClass; @@ -115,6 +125,14 @@ export class Protocol< return this.definition[moduleName] !== undefined; } + public get transactionProver(): TransactionProvable { + // Why do I resolve directly here? + // I don't know exactly but generics don't let me use .resolve() + return this.container.resolve>( + "TransactionProver" + ); + } + public get blockProver(): BlockProvable { // Why do I resolve directly here? // I don't know exactly but generics don't let me use .resolve() @@ -129,6 +147,28 @@ export class Protocol< >("StateTransitionProver"); } + public static defaultModules() { + return { + StateTransitionProver, + TransactionProver, + BlockProver, + AccountState: AccountStateHook, + BlockHeight: BlockHeightHook, + LastStateRoot: LastStateRootBlockHook, + }; + } + + public static defaultConfig() { + return { + StateTransitionProver: {}, + TransactionProver: {}, + BlockProver: {}, + AccountState: {}, + BlockHeight: {}, + LastStateRoot: {}, + } satisfies ModulesConfig>; + } + public getAreProofsEnabled(): AreProofsEnabled { return this.container.resolve("AreProofsEnabled"); } diff --git a/packages/protocol/src/protocol/ProvableBlockHook.ts b/packages/protocol/src/protocol/ProvableBlockHook.ts index b5381e193..4de723a6f 100644 --- a/packages/protocol/src/protocol/ProvableBlockHook.ts +++ b/packages/protocol/src/protocol/ProvableBlockHook.ts @@ -2,56 +2,55 @@ import { Field } from "o1js"; import { NoConfig } from "@proto-kit/common"; import { NetworkState } from "../model/network/NetworkState"; -import { MethodPublicOutput } from "../model/MethodPublicOutput"; -import { BlockProverTransactionArguments } from "../prover/block/BlockProvable"; - -import { TransitioningProtocolModule } from "./TransitioningProtocolModule"; import { - AfterTransactionHookArguments, - BeforeTransactionHookArguments, - ProvableHookBlockState, - toProvableHookBlockState, -} from "./ProvableTransactionHook"; + BlockProverPublicInput, + BlockArguments, + BlockProverState, +} from "../prover/block/BlockProvable"; -export interface BeforeBlockHookArguments extends ProvableHookBlockState {} +import { TransitioningProtocolModule } from "./TransitioningProtocolModule"; -export interface AfterBlockHookArguments extends BeforeBlockHookArguments { - stateRoot: Field; -} - -export function toBeforeTransactionHookArgument( - executionData: Omit< - BlockProverTransactionArguments, - "verificationKeyAttestation" - >, - networkState: NetworkState, - state: Parameters[0] -): BeforeTransactionHookArguments { - const { transaction, signature } = executionData; +export type ProvableHookBlockState = Pick< + BlockProverPublicInput & BlockArguments, + "eternalTransactionsHash" | "incomingMessagesHash" | "blockHashRoot" +>; +export function toBeforeBlockHookArgument( + state: Pick< + BlockProverState, + "eternalTransactionsList" | "incomingMessages" | "blockHashRoot" + > +) { + const { eternalTransactionsList, incomingMessages, blockHashRoot } = state; return { - networkState, - transaction, - signature, - prover: toProvableHookBlockState(state), + eternalTransactionsHash: eternalTransactionsList.commitment, + incomingMessagesHash: incomingMessages.commitment, + blockHashRoot, }; } -export function toAfterTransactionHookArgument( - executionData: Omit< - BlockProverTransactionArguments, - "verificationKeyAttestation" +export function toAfterBlockHookArgument( + state: Pick< + BlockProverState, + "eternalTransactionsList" | "incomingMessages" | "blockHashRoot" >, - networkState: NetworkState, - state: Parameters[0], - runtimeResult: MethodPublicOutput -): AfterTransactionHookArguments { + stateRoot: Field, + transactionsHash: Field +) { return { - ...toBeforeTransactionHookArgument(executionData, networkState, state), - runtimeResult, + ...toBeforeBlockHookArgument(state), + stateRoot, + transactionsHash, }; } +export interface BeforeBlockHookArguments extends ProvableHookBlockState {} + +export interface AfterBlockHookArguments extends BeforeBlockHookArguments { + stateRoot: Field; + transactionsHash: Field; +} + // Purpose is to build transition from -> to network state export abstract class ProvableBlockHook< Config = NoConfig, diff --git a/packages/protocol/src/protocol/ProvableTransactionHook.ts b/packages/protocol/src/protocol/ProvableTransactionHook.ts index 32aec366f..605dc8814 100644 --- a/packages/protocol/src/protocol/ProvableTransactionHook.ts +++ b/packages/protocol/src/protocol/ProvableTransactionHook.ts @@ -1,44 +1,58 @@ import { NoConfig } from "@proto-kit/common"; -import { Signature } from "o1js"; +import { Field } from "o1js"; -import { RuntimeTransaction } from "../model/transaction/RuntimeTransaction"; import { NetworkState } from "../model/network/NetworkState"; import { MethodPublicOutput } from "../model/MethodPublicOutput"; -import type { - BlockProverState, - BlockProverStateCommitments, -} from "../prover/block/BlockProvable"; +import { TransactionProverState } from "../prover/transaction/TransactionProvable"; +import { AuthorizedTransaction } from "../model/transaction/AuthorizedTransaction"; import { TransitioningProtocolModule } from "./TransitioningProtocolModule"; -export type ProvableHookBlockState = Pick< - BlockProverStateCommitments, - | "transactionsHash" - | "eternalTransactionsHash" - | "incomingMessagesHash" - | "blockHashRoot" ->; +export type ProvableHookTransactionState = { + transactionsHash: Field; + eternalTransactionsHash: Field; + incomingMessagesHash: Field; +}; -export function toProvableHookBlockState( +export function toProvableHookTransactionState( state: Pick< - BlockProverState, - | "transactionList" - | "eternalTransactionsList" - | "incomingMessages" - | "blockHashRoot" + TransactionProverState, + "transactionList" | "eternalTransactionsList" | "incomingMessages" > -) { - const { - transactionList, - eternalTransactionsList, - incomingMessages, - blockHashRoot, - } = state; +): ProvableHookTransactionState { + const { transactionList, eternalTransactionsList, incomingMessages } = state; return { transactionsHash: transactionList.commitment, eternalTransactionsHash: eternalTransactionsList.commitment, incomingMessagesHash: incomingMessages.commitment, - blockHashRoot, + }; +} + +export function toBeforeTransactionHookArgument( + authorizedTransaction: AuthorizedTransaction, + networkState: NetworkState, + state: Parameters[0] +): BeforeTransactionHookArguments { + return { + networkState, + transaction: authorizedTransaction, + prover: toProvableHookTransactionState(state), + }; +} + +export function toAfterTransactionHookArgument( + authorizedTransaction: AuthorizedTransaction, + networkState: NetworkState, + state: Parameters[0], + runtimeResult: MethodPublicOutput +): AfterTransactionHookArguments { + return { + ...toBeforeTransactionHookArgument( + authorizedTransaction, + networkState, + state + ), + runtimeResult, }; } @@ -53,10 +67,9 @@ export type TransactionResult = Omit< >; export interface BeforeTransactionHookArguments { - transaction: RuntimeTransaction; - signature: Signature; + transaction: AuthorizedTransaction; networkState: NetworkState; - prover: ProvableHookBlockState; + prover: ProvableHookTransactionState; } export interface AfterTransactionHookArguments diff --git a/packages/protocol/src/prover/accumulators/BlockHashList.ts b/packages/protocol/src/prover/accumulators/BlockHashList.ts new file mode 100644 index 000000000..b0e732e65 --- /dev/null +++ b/packages/protocol/src/prover/accumulators/BlockHashList.ts @@ -0,0 +1,117 @@ +import { Field, Struct } from "o1js"; + +import { DefaultProvableHashList } from "../../utils/ProvableHashList"; +import type { TransactionProverState } from "../transaction/TransactionProvable"; +import { NetworkState } from "../../model/network/NetworkState"; + +export class BundlePreimage extends Struct({ + preimage: Field, + fromStateTransitionsHash: Field, + fromWitnessedRootsHash: Field, +}) {} + +export class FieldTransition extends Struct({ + from: Field, + to: Field, +}) {} + +/** + * A bundle represents an ordered list of transactions and their evaluated effects. + * Specifically, this includes beforeTransaction, runtime and afterTransaction evaluation, + * but not block hooks. + */ +export class Bundle extends Struct({ + // Those are per-block trackers + networkStateHash: Field, + transactionsHash: Field, + + // Those are non-linear trackers that we assert later in the blockprover + pendingSTBatchesHash: FieldTransition, + witnessedRootsHash: FieldTransition, +}) {} + +/** + * This hash list collects an ordered list of Bundle instances. + * "Pushing" onto this list can mean either appending a new bundle or updating the + * bundle at the tip of this list, according to the following rules: + * The validated preimage (via checkLastBundleElement) is: + * - == commitment: A new bundle will be appended + * - something else: The preimage is the actual preimage, therefore as a operation, + * the old one will be popped (silently) and the updates bundle will be pushed, + * resulting in an semantic update of the tip. + */ +export class BundleHashList extends DefaultProvableHashList { + public constructor( + commitment: Field = Field(0), + // TODO Refactor this into preimage and "auxiliary batch information" - this is confusing + public preimage?: BundlePreimage + ) { + super(Bundle, commitment); + } + + /** Verifies this list's preimage against the prover's state + * The main impact this function has is that it makes the preimage trusted + * i.e. we can safely use it to add to the bundle/open a new bundle + */ + public checkLastBundleElement( + state: TransactionProverState, + networkState: NetworkState + ) { + const { preimage, fromWitnessedRootsHash, fromStateTransitionsHash } = + this.preimage!; + + // Check and append to bundlelist + const lastElement = new Bundle({ + networkStateHash: networkState.hash(), + transactionsHash: state.transactionList.commitment, + pendingSTBatchesHash: { + from: fromStateTransitionsHash, + to: state.pendingSTBatches.commitment, + }, + witnessedRootsHash: { + from: fromWitnessedRootsHash, + to: state.witnessedRoots.commitment, + }, + }); + + const newBundle = this.commitment.equals(preimage); + this.witnessTip(preimage, lastElement) + .or(newBundle) + .assertTrue("Last element not valid"); + + newBundle + .implies(state.transactionList.isEmpty()) + .assertTrue("Transaction list not empty for new bundle"); + } + + /** + * This function pushes a new bundle onto this list or updates the bundle at + * the tip of this list, according to the rules of the preimage algorithms (see class docs) + */ + public addToBundle( + state: TransactionProverState, + networkState: NetworkState + ) { + const { preimage, fromWitnessedRootsHash, fromStateTransitionsHash } = + this.preimage!; + + const newElement = new Bundle({ + networkStateHash: networkState.hash(), + transactionsHash: state.transactionList.commitment, + pendingSTBatchesHash: { + from: fromStateTransitionsHash, + to: state.pendingSTBatches.commitment, + }, + witnessedRootsHash: { + from: fromWitnessedRootsHash, + to: state.witnessedRoots.commitment, + }, + }); + + // We always overwrite here, the invariant is that the preimage is + // either the actual preimage in case of addition to the existing bundle + // or the current commitment in case of a new bundle + this.commitment = preimage; + this.push(newElement); + } +} diff --git a/packages/protocol/src/prover/accumulators/WitnessedRootHashList.ts b/packages/protocol/src/prover/accumulators/WitnessedRootHashList.ts index 9a8250168..9671c6582 100644 --- a/packages/protocol/src/prover/accumulators/WitnessedRootHashList.ts +++ b/packages/protocol/src/prover/accumulators/WitnessedRootHashList.ts @@ -1,4 +1,4 @@ -import { Bool, Field, Struct } from "o1js"; +import { Bool, Field, Provable, Struct } from "o1js"; import { DefaultProvableHashList } from "../../utils/ProvableHashList"; @@ -13,7 +13,6 @@ export class WitnessedRoot extends Struct({ export class WitnessedRootWitness extends Struct({ witnessedRoot: Field, - preimage: Field, }) {} /** @@ -21,7 +20,10 @@ export class WitnessedRootWitness extends Struct({ */ export class WitnessedRootHashList extends DefaultProvableHashList { - public constructor(commitment: Field = Field(0)) { + public constructor( + commitment: Field = Field(0), + public preimage: Field = Field(0) + ) { super(WitnessedRoot, commitment); } @@ -34,28 +36,27 @@ export class WitnessedRootHashList extends DefaultProvableHashList Already covered in BlockProver + // (1) don't append if witnessedRoot == finalizedRoot -> Already covered in BlockProver // (2) don't append if preimage.push({ finalizedRoot, pendingSTBatchesHash }) == this.commitment const skipPush = preimageCheckList.commitment.equals(this.commitment); - return this.pushIf(witnessedRoot, condition.and(skipPush.not())); + const fromCommitment = this.commitment; + + const pushCondition = condition.and(skipPush.not()); + this.pushIf(witnessedRoot, pushCondition); + + this.preimage = Provable.if(pushCondition, fromCommitment, this.preimage); } } diff --git a/packages/protocol/src/prover/block/BlockProvable.ts b/packages/protocol/src/prover/block/BlockProvable.ts index 84d6493af..fc2a15052 100644 --- a/packages/protocol/src/prover/block/BlockProvable.ts +++ b/packages/protocol/src/prover/block/BlockProvable.ts @@ -1,18 +1,7 @@ -// eslint-disable-next-line max-classes-per-file -import { - Bool, - DynamicProof, - Field, - Proof, - Signature, - Struct, - Void, -} from "o1js"; -import { WithZkProgrammable, CompilableModule } from "@proto-kit/common"; +import { Bool, Field, Poseidon, Proof, Provable, Struct } from "o1js"; +import { CompilableModule, WithZkProgrammable } from "@proto-kit/common"; import { StateTransitionProof } from "../statetransition/StateTransitionProvable"; -import { MethodPublicOutput } from "../../model/MethodPublicOutput"; -import { RuntimeTransaction } from "../../model/transaction/RuntimeTransaction"; import { NetworkState } from "../../model/network/NetworkState"; import { TransactionHashList } from "../accumulators/TransactionHashList"; import { MinaActionsHashList } from "../../utils/MinaPrefixedProvableHashList"; @@ -21,34 +10,139 @@ import { WitnessedRootHashList, WitnessedRootWitness, } from "../accumulators/WitnessedRootHashList"; +import { TransactionProof } from "../transaction/TransactionProvable"; +import { BundleHashList, FieldTransition } from "../accumulators/BlockHashList"; +import { NonMethods } from "../../utils/utils"; import { BlockHashMerkleTreeWitness } from "./accummulators/BlockHashMerkleTree"; -import { RuntimeVerificationKeyAttestation } from "./accummulators/RuntimeVerificationKeyTree"; -// Should be equal to BlockProver.PublicInput -export interface BlockProverState { - /** - * The current state root of the block prover - */ - stateRoot: Field; +export const BLOCK_ARGUMENT_BATCH_SIZE = 4; - /** - * The current commitment of the transaction-list which - * will at the end equal the bundle hash - */ - transactionList: TransactionHashList; +export class BlockArguments extends Struct({ + afterBlockRootWitness: WitnessedRootWitness, + transactionsHash: Field, + pendingSTBatchesHash: FieldTransition, + witnessedRootsHash: FieldTransition, + isDummy: Bool, +}) { + public static noop( + state: NonMethods>, + stateRoot: Field + ) { + return new BlockArguments({ + afterBlockRootWitness: { + witnessedRoot: stateRoot, + }, + transactionsHash: Field(0), + pendingSTBatchesHash: { + from: state.pendingSTBatches.commitment, + to: state.pendingSTBatches.commitment, + }, + witnessedRootsHash: { + from: state.witnessedRoots.commitment, + to: state.witnessedRoots.commitment, + }, + isDummy: Bool(true), + }); + } +} + +export class BlockArgumentsBatch extends Struct({ + batch: Provable.Array(BlockArguments, BLOCK_ARGUMENT_BATCH_SIZE), +}) {} + +const BlockProverStateBaseFields = { + eternalTransactionsHash: Field, + incomingMessagesHash: Field, + stateRoot: Field, + blockHashRoot: Field, + blockNumber: Field, + networkStateHash: Field, +}; + +export class BlockProverPublicInput extends Struct({ + // Tracker of the current block prover state + proverStateRemainder: Field, + ...BlockProverStateBaseFields, +}) { + public equals(input: BlockProverPublicInput): Bool { + const output2 = BlockProverPublicInput.toFields(input); + const output1 = BlockProverPublicInput.toFields(this); + return output1 + .map((value1, index) => value1.equals(output2[index])) + .reduce((a, b) => a.and(b)); + } + + public clone() { + return new BlockProverPublicInput( + BlockProverPublicInput.fromFields(BlockProverPublicInput.toFields(this)) + ); + } +} + +export const BlockProverStateCommitments = { + remainders: { + // Commitment to the list of unprocessed (pending) batches of STs that need to be proven + pendingSTBatchesHash: Field, + witnessedRootsHash: Field, + bundlesHash: Field, + witnessedRootsPreimage: Field, + }, + ...BlockProverStateBaseFields, +}; +export class BlockProverStateInput extends Struct(BlockProverStateCommitments) { + public hash() { + return Poseidon.hash(BlockProverStateInput.toFields(this)); + } + + public static fromPublicInput(input: BlockProverPublicInput) { + return new BlockProverStateInput({ + remainders: { + bundlesHash: Field(0), + pendingSTBatchesHash: Field(0), + witnessedRootsHash: Field(0), + witnessedRootsPreimage: Field(0), + }, + eternalTransactionsHash: input.eternalTransactionsHash, + incomingMessagesHash: input.incomingMessagesHash, + stateRoot: input.stateRoot, + blockHashRoot: input.blockHashRoot, + blockNumber: input.blockNumber, + networkStateHash: input.networkStateHash, + }); + } + + public finalize(condition: Bool) { + condition + .implies( + this.remainders.bundlesHash + .equals(0) + .and(this.remainders.pendingSTBatchesHash.equals(0)) + .and(this.remainders.witnessedRootsHash.equals(0)) + ) + .assertTrue("Remainers not fully removed"); + + return new BlockProverPublicInput({ + proverStateRemainder: Field(0), + eternalTransactionsHash: this.eternalTransactionsHash, + incomingMessagesHash: this.incomingMessagesHash, + stateRoot: this.stateRoot, + blockHashRoot: this.blockHashRoot, + blockNumber: this.blockNumber, + networkStateHash: this.networkStateHash, + }); + } +} + +export class BlockProverPublicOutput extends BlockProverPublicInput {} + +export class BlockProverState { /** * The network state which gives access to values such as blockHeight * This value is the same for the whole batch (L2 block) */ - networkState: NetworkState; - - /** - * The root of the merkle tree encoding all block hashes, - * see `BlockHashMerkleTree` - */ - blockHashRoot: Field; + bundleList: BundleHashList; /** * A variant of the transactionsHash that is never reset. @@ -63,156 +157,221 @@ export interface BlockProverState { witnessedRoots: WitnessedRootHashList; + /** + * The current state root of the block prover + */ + stateRoot: Field; + + /** + * The root of the merkle tree encoding all block hashes, + * see `BlockHashMerkleTree` + */ + blockHashRoot: Field; + blockNumber: Field; -} -// TODO Sort and organize public inputs and outputs -export class BlockProverStateCommitments extends Struct({ - transactionsHash: Field, - stateRoot: Field, - // Commitment to the list of unprocessed (pending) batches of STs that need to be proven - pendingSTBatchesHash: Field, - witnessedRootsHash: Field, - networkStateHash: Field, - blockHashRoot: Field, - eternalTransactionsHash: Field, - incomingMessagesHash: Field, - blockNumber: Field, -}) { - public static fromBlockProverState( - state: BlockProverState - ): BlockProverStateCommitments { - return { - networkStateHash: state.networkState.hash(), - stateRoot: state.stateRoot, - blockNumber: state.blockNumber, - blockHashRoot: state.blockHashRoot, - pendingSTBatchesHash: state.pendingSTBatches.commitment, - transactionsHash: state.transactionList.commitment, - eternalTransactionsHash: state.eternalTransactionsList.commitment, - incomingMessagesHash: state.incomingMessages.commitment, - witnessedRootsHash: state.witnessedRoots.commitment, - }; + blockWitness: BlockHashMerkleTreeWitness; + + networkState: NetworkState; + + constructor(args: { + networkState: NetworkState; + eternalTransactionsList: TransactionHashList; + pendingSTBatches: AppliedBatchHashList; + stateRoot: Field; + blockHashRoot: Field; + blockNumber: Field; + bundleList: BundleHashList; + blockWitness: BlockHashMerkleTreeWitness; + witnessedRoots: WitnessedRootHashList; + incomingMessages: MinaActionsHashList; + }) { + this.bundleList = args.bundleList; + this.eternalTransactionsList = args.eternalTransactionsList; + this.pendingSTBatches = args.pendingSTBatches; + this.stateRoot = args.stateRoot; + this.blockHashRoot = args.blockHashRoot; + this.blockNumber = args.blockNumber; + this.networkState = args.networkState; + this.blockWitness = args.blockWitness; + this.witnessedRoots = args.witnessedRoots; + this.incomingMessages = args.incomingMessages; } - public static toBlockProverState( - publicInput: BlockProverStateCommitments, - networkState: NetworkState - ): BlockProverState { - publicInput.networkStateHash.assertEquals( - networkState.hash(), - "ExecutionData Networkstate doesn't equal public input hash" - ); + public toCommitments(): BlockProverStateInput { + return new BlockProverStateInput({ + remainders: { + bundlesHash: this.bundleList.commitment, + pendingSTBatchesHash: this.pendingSTBatches.commitment, + witnessedRootsHash: this.witnessedRoots.commitment, + witnessedRootsPreimage: this.witnessedRoots.preimage, + }, + eternalTransactionsHash: this.eternalTransactionsList.commitment, + incomingMessagesHash: this.incomingMessages.commitment, + stateRoot: this.stateRoot, + blockHashRoot: this.blockHashRoot, + blockNumber: this.blockNumber, + networkStateHash: this.networkState.hash(), + }); + } - return { - networkState, - stateRoot: publicInput.stateRoot, - blockHashRoot: publicInput.blockHashRoot, - transactionList: new TransactionHashList(publicInput.transactionsHash), + public static blockProverFromCommitments( + stateInput: NonMethods, + networkState: NetworkState, + blockWitness: BlockHashMerkleTreeWitness + ): BlockProverState { + return new BlockProverState({ + bundleList: new BundleHashList(stateInput.remainders.bundlesHash), eternalTransactionsList: new TransactionHashList( - publicInput.eternalTransactionsHash + stateInput.eternalTransactionsHash ), incomingMessages: new MinaActionsHashList( - publicInput.incomingMessagesHash + stateInput.incomingMessagesHash ), pendingSTBatches: new AppliedBatchHashList( - publicInput.pendingSTBatchesHash + stateInput.remainders.pendingSTBatchesHash ), - witnessedRoots: new WitnessedRootHashList(publicInput.witnessedRootsHash), - blockNumber: publicInput.blockNumber, - }; - } -} - -export class BlockProverPublicInput extends BlockProverStateCommitments {} - -export class BlockProverPublicOutput extends Struct({ - transactionsHash: Field, - stateRoot: Field, - pendingSTBatchesHash: Field, - witnessedRootsHash: Field, - networkStateHash: Field, - blockHashRoot: Field, - eternalTransactionsHash: Field, - incomingMessagesHash: Field, - closed: Bool, - blockNumber: Field, -}) { - public equals(input: BlockProverPublicInput, closed: Bool): Bool { - const output2 = BlockProverPublicOutput.toFields({ - ...input, - closed, + witnessedRoots: new WitnessedRootHashList( + stateInput.remainders.witnessedRootsHash, + stateInput.remainders.witnessedRootsPreimage + ), + stateRoot: stateInput.stateRoot, + blockHashRoot: stateInput.blockHashRoot, + blockNumber: stateInput.blockNumber, + networkState, + blockWitness, }); - const output1 = BlockProverPublicOutput.toFields(this); - return output1 - .map((value1, index) => value1.equals(output2[index])) - .reduce((a, b) => a.and(b)); } -} -export type BlockProverProof = Proof< - BlockProverPublicInput, - BlockProverPublicOutput ->; - -export class BlockProverTransactionArguments extends Struct({ - transaction: RuntimeTransaction, - signature: Signature, - verificationKeyAttestation: RuntimeVerificationKeyAttestation, -}) {} + public copy() { + return BlockProverState.fromFields(this.toFields()); + } -export class DynamicRuntimeProof extends DynamicProof< - Void, - MethodPublicOutput -> { - static publicInputType = Void; + public toFields() { + return [ + this.bundleList.commitment, + this.eternalTransactionsList.commitment, + this.pendingSTBatches.commitment, + this.incomingMessages.commitment, + this.witnessedRoots.commitment, + this.witnessedRoots.preimage, + this.stateRoot, + this.blockHashRoot, + this.blockNumber, + ...NetworkState.toFields(this.networkState), + ...BlockHashMerkleTreeWitness.toFields(this.blockWitness), + ]; + } - static publicOutputType = MethodPublicOutput; + // TODO Unit test + public static fromFields(fields: Field[]) { + return new BlockProverState({ + bundleList: new BundleHashList(fields[0]), + eternalTransactionsList: new TransactionHashList(fields[1]), + pendingSTBatches: new AppliedBatchHashList(fields[2]), + incomingMessages: new MinaActionsHashList(fields[3]), + witnessedRoots: new WitnessedRootHashList(fields[4], fields[5]), + stateRoot: fields[6], + blockHashRoot: fields[7], + blockNumber: fields[8], + networkState: new NetworkState(NetworkState.fromFields(fields.slice(9))), + blockWitness: new BlockHashMerkleTreeWitness( + BlockHashMerkleTreeWitness.fromFields( + fields.slice(9 + NetworkState.sizeInFields()) + ) + ), + }); + } - // TODO this won't be 0 for proofs-as-args - static maxProofsVerified = 0 as const; + public static choose( + condition: Bool, + a: BlockProverState, + b: BlockProverState + ) { + return new BlockProverState({ + bundleList: new BundleHashList( + Provable.if(condition, a.bundleList.commitment, b.bundleList.commitment) + ), + eternalTransactionsList: new TransactionHashList( + Provable.if( + condition, + a.eternalTransactionsList.commitment, + b.eternalTransactionsList.commitment + ) + ), + pendingSTBatches: new AppliedBatchHashList( + Provable.if( + condition, + a.pendingSTBatches.commitment, + b.pendingSTBatches.commitment + ) + ), + incomingMessages: new MinaActionsHashList( + Provable.if( + condition, + a.incomingMessages.commitment, + b.incomingMessages.commitment + ) + ), + witnessedRoots: new WitnessedRootHashList( + Provable.if( + condition, + a.witnessedRoots.commitment, + b.witnessedRoots.commitment + ), + Provable.if( + condition, + a.witnessedRoots.preimage, + b.witnessedRoots.preimage + ) + ), + stateRoot: Provable.if(condition, a.stateRoot, b.stateRoot), + blockHashRoot: Provable.if(condition, a.blockHashRoot, b.blockHashRoot), + blockWitness: new BlockHashMerkleTreeWitness( + Provable.if( + condition, + BlockHashMerkleTreeWitness, + a.blockWitness, + b.blockWitness + ) + ), + blockNumber: Provable.if(condition, a.blockNumber, b.blockNumber), + networkState: new NetworkState( + Provable.if(condition, NetworkState, a.networkState, b.networkState) + ), + }); + } } -export class BlockProverSingleTransactionExecutionData extends Struct({ - transaction: BlockProverTransactionArguments, - networkState: NetworkState, -}) {} - -export class BlockProverMultiTransactionExecutionData extends Struct({ - transaction1: BlockProverTransactionArguments, - transaction2: BlockProverTransactionArguments, - networkState: NetworkState, -}) {} +export type BlockProof = Proof; export interface BlockProvable extends WithZkProgrammable, CompilableModule { - proveTransaction: ( + proveBlockBatchNoProofs: ( publicInput: BlockProverPublicInput, - runtimeProof: DynamicRuntimeProof, - executionData: BlockProverSingleTransactionExecutionData - ) => Promise; - - proveTransactions: ( - publicInput: BlockProverPublicInput, - runtimeProof1: DynamicRuntimeProof, - runtimeProof2: DynamicRuntimeProof, - executionData: BlockProverMultiTransactionExecutionData + stateWitness: BlockProverStateInput, + networkState: NetworkState, + blockWitness: BlockHashMerkleTreeWitness, + batch: BlockArgumentsBatch, + finalize: Bool ) => Promise; - proveBlock: ( + proveBlockBatchWithProofs: ( publicInput: BlockProverPublicInput, + stateWitness: BlockProverStateInput, networkState: NetworkState, blockWitness: BlockHashMerkleTreeWitness, + batch: BlockArgumentsBatch, + deferSTProof: Bool, + deferTransactionProof: Bool, stateTransitionProof: StateTransitionProof, - deferSTs: Bool, - afterBlockRootWitness: WitnessedRootWitness, - transactionProof: BlockProverProof + transactionProof: TransactionProof ) => Promise; merge: ( publicInput: BlockProverPublicInput, - proof1: BlockProverProof, - proof2: BlockProverProof + proof1: BlockProof, + proof2: BlockProof ) => Promise; } diff --git a/packages/protocol/src/prover/block/BlockProver.ts b/packages/protocol/src/prover/block/BlockProver.ts index f97edd403..f03430c2e 100644 --- a/packages/protocol/src/prover/block/BlockProver.ts +++ b/packages/protocol/src/prover/block/BlockProver.ts @@ -1,12 +1,4 @@ -import { - Bool, - Field, - Proof, - Provable, - SelfProof, - VerificationKey, - ZkProgram, -} from "o1js"; +import { Bool, Field, Provable, SelfProof, ZkProgram } from "o1js"; import { container, inject, injectable, injectAll } from "tsyringe"; import { AreProofsEnabled, @@ -14,14 +6,14 @@ import { CompileArtifact, CompileRegistry, log, - MAX_FIELD, + NonMethods, PlainZkProgram, provableMethod, + reduceSequential, WithZkProgrammable, ZkProgrammable, } from "@proto-kit/common"; -import { MethodPublicOutput } from "../../model/MethodPublicOutput"; import { ProtocolModule } from "../../protocol/ProtocolModule"; import { StateTransitionProof, @@ -30,84 +22,51 @@ import { StateTransitionProverPublicOutput, } from "../statetransition/StateTransitionProvable"; import { RuntimeTransaction } from "../../model/transaction/RuntimeTransaction"; -import { - ProvableStateTransition, - StateTransition, -} from "../../model/StateTransition"; -import { - AfterTransactionHookArguments, - BeforeTransactionHookArguments, - ProvableTransactionHook, - toProvableHookBlockState, -} from "../../protocol/ProvableTransactionHook"; -import { - RuntimeMethodExecutionContext, - RuntimeMethodExecutionData, -} from "../../state/context/RuntimeMethodExecutionContext"; +import { RuntimeMethodExecutionContext } from "../../state/context/RuntimeMethodExecutionContext"; import { AfterBlockHookArguments, BeforeBlockHookArguments, ProvableBlockHook, - toAfterTransactionHookArgument, - toBeforeTransactionHookArgument, + toAfterBlockHookArgument, + toBeforeBlockHookArgument, } from "../../protocol/ProvableBlockHook"; import { NetworkState } from "../../model/network/NetworkState"; -import { SignedTransaction } from "../../model/transaction/SignedTransaction"; -import { MinaActions } from "../../utils/MinaPrefixedProvableHashList"; -import { StateTransitionReductionList } from "../accumulators/StateTransitionReductionList"; import { assertEqualsIf } from "../../utils/utils"; -import { WitnessedRootWitness } from "../accumulators/WitnessedRootHashList"; import { StateServiceProvider } from "../../state/StateServiceProvider"; -import { AppliedStateTransitionBatch } from "../../model/AppliedStateTransitionBatch"; +import { executeHooks } from "../utils"; +import { + TransactionProof, + TransactionProvable, + TransactionProverPublicInput, + TransactionProverPublicOutput, +} from "../transaction/TransactionProvable"; +import { Bundle } from "../accumulators/BlockHashList"; import { + BlockArguments, + BlockArgumentsBatch, + BlockProof, BlockProvable, - BlockProverProof, BlockProverPublicInput, BlockProverPublicOutput, - DynamicRuntimeProof, - BlockProverMultiTransactionExecutionData, - BlockProverTransactionArguments, - BlockProverSingleTransactionExecutionData, BlockProverState, - BlockProverStateCommitments, + BlockProverStateInput, } from "./BlockProvable"; import { BlockHashMerkleTreeWitness, BlockHashTreeEntry, } from "./accummulators/BlockHashMerkleTree"; -import { - MethodVKConfigData, - MinimalVKTreeService, - RuntimeVerificationKeyAttestation, -} from "./accummulators/RuntimeVerificationKeyTree"; -import { RuntimeVerificationKeyRootService } from "./services/RuntimeVerificationKeyRootService"; const errors = { propertyNotMatchingStep: (propertyName: string, step: string) => `${propertyName} not matching: ${step}`, propertyNotMatching: (propertyName: string) => `${propertyName} not matching`, - - stateRootNotMatching: (step: string) => - errors.propertyNotMatchingStep("StateRoots", step), - - transactionsHashNotMatching: (step: string) => - errors.propertyNotMatchingStep("Transactions hash", step), - - networkStateHashNotMatching: (step: string) => - errors.propertyNotMatchingStep("Network state hash", step), - - invalidZkProgramTreeRoot: () => - "Root hash of the provided zkProgram config witness is invalid", }; -type ApplyTransactionArguments = Omit< - BlockProverTransactionArguments, - "verificationKeyAttestation" ->; - -export type BlockProof = Proof; +type BlockHookArgument = T extends "before" + ? BeforeBlockHookArguments + : AfterBlockHookArguments; export class BlockProverProgrammable extends ZkProgrammable< BlockProverPublicInput, @@ -119,10 +78,12 @@ export class BlockProverProgrammable extends ZkProgrammable< StateTransitionProverPublicInput, StateTransitionProverPublicOutput >, - private readonly transactionHooks: ProvableTransactionHook[], + public readonly transactionProver: ZkProgrammable< + TransactionProverPublicInput, + TransactionProverPublicOutput + >, private readonly blockHooks: ProvableBlockHook[], - private readonly stateServiceProvider: StateServiceProvider, - private readonly verificationKeyService: MinimalVKTreeService + private readonly stateServiceProvider: StateServiceProvider ) { super(); } @@ -133,213 +94,16 @@ export class BlockProverProgrammable extends ZkProgrammable< return this.prover.areProofsEnabled; } - /** - * Applies and checks the two proofs and applies the corresponding state - * changes to the given state. - * - * The rough high level workflow of this function: - * 1. Execute beforeTransaction hooks, pushing the ST batch - * 2. Add Transaction to bundle, meaning appending it to all the respective commitments - * 3. Push the runtime ST batch - * 4. Execute afterTransaction hooks, pushing the ST batch - * 5. Some consistency checks and signature verification - * - * @param fromState The from-state of the BlockProver - * @param runtimeOutput - * @param executionData - * @param networkState - * @returns The new BlockProver-state to be used as public output - */ - public async applyTransaction( - fromState: BlockProverState, - runtimeOutput: MethodPublicOutput, - executionData: ApplyTransactionArguments, - networkState: NetworkState - ): Promise { - const { transaction, signature } = executionData; - - let state = { ...fromState }; - - const { isMessage } = runtimeOutput; - - const beforeTxHookArguments = toBeforeTransactionHookArgument( - executionData, - networkState, - state - ); - - // Apply beforeTransaction hook state transitions - const beforeBatch = await this.executeTransactionHooks( - async (module, args) => await module.beforeTransaction(args), - beforeTxHookArguments, - isMessage - ); - - state = this.addTransactionToBundle( - state, - runtimeOutput.isMessage, - transaction - ); - - state.pendingSTBatches.push(beforeBatch); - - state.pendingSTBatches.push({ - batchHash: runtimeOutput.stateTransitionsHash, - applied: runtimeOutput.status, - }); - - // Apply afterTransaction hook state transitions - const afterTxHookArguments = toAfterTransactionHookArgument( - executionData, - networkState, - state, - runtimeOutput - ); - - // Switch to different state set for afterTx hooks - this.stateServiceProvider.popCurrentStateService(); - - const afterBatch = await this.executeTransactionHooks( - async (module, args) => await module.afterTransaction(args), - afterTxHookArguments, - isMessage - ); - state.pendingSTBatches.push(afterBatch); - - // Check transaction integrity against appProof - const blockTransactionHash = transaction.hash(); - - blockTransactionHash.assertEquals( - runtimeOutput.transactionHash, - "Transactions provided in AppProof and BlockProof do not match" - ); - - // Check transaction signature - new SignedTransaction({ - transaction, - signature, - }) - .validateSignature() - .or(isMessage) - .assertTrue("Transaction signature not valid"); - - // Validate layout of transaction witness - transaction.assertTransactionType(isMessage); - - // Check network state integrity against appProof - state.networkState - .hash() - .assertEquals( - runtimeOutput.networkStateHash, - "Network state does not match state used in AppProof" - ); - - return state; - } - - // eslint-disable-next-line max-len - // TODO How does this interact with the RuntimeMethodExecutionContext when executing runtimemethods? - - /** - * Constructs a AppliedBatch based on a list of STs and the flag whether to - * be applied or not. The AppliedBatch is a condensed commitment to a batch - * of STs. - */ - private constructBatch( - stateTransitions: StateTransition[], - applied: Bool - ) { - const transitions = stateTransitions.map((transition) => - transition.toProvable() - ); - - const hashList = new StateTransitionReductionList(ProvableStateTransition); - transitions.forEach((transition) => { - hashList.push(transition); - }); - - return new AppliedStateTransitionBatch({ - batchHash: hashList.commitment, - applied, - }); - } - - private async executeTransactionHooks< - T extends BeforeTransactionHookArguments | AfterTransactionHookArguments, - >( - hook: (module: ProvableTransactionHook, args: T) => Promise, - hookArguments: T, - isMessage: Bool - ) { - const { batch, rawStatus } = await this.executeHooks( - hookArguments, - async () => { - for (const module of this.transactionHooks) { - // eslint-disable-next-line no-await-in-loop - await hook(module, hookArguments); - } - }, - isMessage - ); - - // This is going to set applied to false in case the hook fails - // (that's only possible for messages though as others are hard-asserted) - batch.applied = rawStatus; - - return batch; - } - - private async executeHooks( - contextArguments: RuntimeMethodExecutionData, - method: () => Promise, - isMessage: Bool | undefined = undefined - ) { - const executionContext = container.resolve(RuntimeMethodExecutionContext); - executionContext.clear(); - - // Setup context for potential calls to runtime methods. - // This way they can use this.transaction etc. while still having provable - // integrity between data - executionContext.setup(contextArguments); - executionContext.beforeMethod("", "", []); - - const result = await method(); - - executionContext.afterMethod(); - - const { stateTransitions, status, statusMessage } = - executionContext.current().result; - - // See https://github.com/proto-kit/framework/issues/321 for why we do this here - if (isMessage !== undefined) { - // isMessage is defined for all tx hooks - status - .or(isMessage) - .assertTrue( - `Transaction hook call failed for non-message tx: ${statusMessage ?? "-"}` - ); - } else { - // isMessage is undefined for all block hooks - status.assertTrue(`Block hook call failed: ${statusMessage ?? "-"}`); - } - - return { - batch: this.constructBatch(stateTransitions, Bool(true)), - result, - rawStatus: status, - }; - } - - public async executeBlockHooks< - T extends BeforeBlockHookArguments | AfterBlockHookArguments, - >( + public async executeBlockHooks( + type: T, hook: ( module: ProvableBlockHook, networkState: NetworkState, - args: T + args: BlockHookArgument ) => Promise, - hookArguments: T, - inputNetworkState: NetworkState + hookArguments: BlockHookArgument, + inputNetworkState: NetworkState, + isDummy: Bool ) { const transaction = RuntimeTransaction.dummyTransaction(); const startingInputs = { @@ -347,155 +111,33 @@ export class BlockProverProgrammable extends ZkProgrammable< networkState: inputNetworkState, }; - return await this.executeHooks(startingInputs, async () => { - const executionContext = container.resolve(RuntimeMethodExecutionContext); - - return await this.blockHooks.reduce>( - async (networkStatePromise, blockHook) => { - const networkState = await networkStatePromise; - - // Setup context for potential calls to runtime methods. - // With the special case that we set the new networkstate for every hook - // We also have to put in a dummy transaction for network.transaction - executionContext.setup({ - transaction: RuntimeTransaction.dummyTransaction(), - networkState, - }); - - return await hook(blockHook, networkState, hookArguments); - }, - Promise.resolve(inputNetworkState) - ); - }); - } - - public addTransactionToBundle< - T extends Pick< - BlockProverState, - "transactionList" | "eternalTransactionsList" | "incomingMessages" - >, - >(state: T, isMessage: Bool, transaction: RuntimeTransaction): T { - const transactionHash = transaction.hash(); - - // Append tx to transaction list - state.transactionList.pushIf(transactionHash, isMessage.not()); - - // Append tx to eternal transaction list - // TODO Change that to the a sequence-state compatible transaction struct - state.eternalTransactionsList.push(transactionHash); - - // Append tx to incomingMessagesHash - const actionHash = MinaActions.actionHash(transaction.hashData()); - - state.incomingMessages.pushIf(actionHash, isMessage); - - return state; - } - - private verifyVerificationKeyAttestation( - attestation: RuntimeVerificationKeyAttestation, - methodId: Field - ): VerificationKey { - // Verify the [methodId, vk] tuple against the baked-in vk tree root - const { verificationKey, witness: verificationKeyTreeWitness } = - attestation; - - const root = Field(this.verificationKeyService.getRoot()); - const calculatedRoot = verificationKeyTreeWitness.calculateRoot( - new MethodVKConfigData({ - methodId: methodId, - vkHash: verificationKey.hash, - }).hash() - ); - root.assertEquals(calculatedRoot, errors.invalidZkProgramTreeRoot()); - - return verificationKey; - } - - public async proveTransactionInternal( - fromState: BlockProverState, - runtimeProof: DynamicRuntimeProof, - { transaction, networkState }: BlockProverSingleTransactionExecutionData - ): Promise { - const verificationKey = this.verifyVerificationKeyAttestation( - transaction.verificationKeyAttestation, - transaction.transaction.methodId - ); - - runtimeProof.verify(verificationKey); - - return await this.applyTransaction( - fromState, - runtimeProof.publicOutput, - transaction, - networkState - ); - } - - private staticChecks(publicInput: BlockProverPublicInput) { - publicInput.blockNumber.assertEquals( - MAX_FIELD, - "blockNumber has to be MAX for transaction proofs" - ); - } - - @provableMethod() - public async proveTransaction( - publicInput: BlockProverPublicInput, - runtimeProof: DynamicRuntimeProof, - executionData: BlockProverSingleTransactionExecutionData - ): Promise { - const state = BlockProverStateCommitments.toBlockProverState( - publicInput, - executionData.networkState - ); - - this.staticChecks(publicInput); + return await executeHooks( + startingInputs, + `${type}Block`, + async () => { + const executionContext = container.resolve( + RuntimeMethodExecutionContext + ); - const stateTo = await this.proveTransactionInternal( - state, - runtimeProof, - executionData - ); + return await this.blockHooks.reduce>( + async (networkStatePromise, blockHook) => { + const networkState = await networkStatePromise; - return new BlockProverPublicOutput({ - ...BlockProverStateCommitments.fromBlockProverState(stateTo), - closed: Bool(false), - }); - } + // Setup context for potential calls to runtime methods. + // With the special case that we set the new networkstate for every hook + // We also have to put in a dummy transaction for network.transaction + executionContext.setup({ + transaction: RuntimeTransaction.dummyTransaction(), + networkState, + }); - @provableMethod() - public async proveTransactions( - publicInput: BlockProverPublicInput, - runtimeProof1: DynamicRuntimeProof, - runtimeProof2: DynamicRuntimeProof, - executionData: BlockProverMultiTransactionExecutionData - ): Promise { - const state = BlockProverStateCommitments.toBlockProverState( - publicInput, - executionData.networkState + return await hook(blockHook, networkState, hookArguments); + }, + Promise.resolve(inputNetworkState) + ); + }, + isDummy ); - - this.staticChecks(publicInput); - - const state1 = await this.proveTransactionInternal(state, runtimeProof1, { - transaction: executionData.transaction1, - networkState: executionData.networkState, - }); - - // Switch to next state record for 2nd tx beforeTx hook - // TODO Can be prevented by merging 1st afterTx + 2nd beforeTx - this.stateServiceProvider.popCurrentStateService(); - - const stateTo = await this.proveTransactionInternal(state1, runtimeProof2, { - transaction: executionData.transaction2, - networkState: executionData.networkState, - }); - - return new BlockProverPublicOutput({ - ...BlockProverStateCommitments.fromBlockProverState(stateTo), - closed: Bool(false), - }); } public includeSTProof( @@ -580,348 +222,394 @@ export class BlockProverProgrammable extends ZkProgrammable< }; } - @provableMethod() - public async proveBlock( - publicInput: BlockProverPublicInput, - networkState: NetworkState, - blockWitness: BlockHashMerkleTreeWitness, + private verifySTProof( + state: BlockProverState, stateTransitionProof: StateTransitionProof, - deferSTProof: Bool, - afterBlockRootWitness: WitnessedRootWitness, - transactionProof: BlockProverProof - ): Promise { - // 1. Make assertions about the inputs - publicInput.transactionsHash.assertEquals( - Field(0), - "Transactionshash has to start at 0" - ); + deferSTProof: Bool + ) { + // Verify ST Proof only if STs have been emitted, + // and we don't defer the verification of the STs + // otherwise we can input a dummy proof + const batchesEmpty = state.pendingSTBatches.commitment.equals(Field(0)); + const verifyStProof = deferSTProof.not().and(batchesEmpty.not()); + log.provable.debug("Verify STProof", verifyStProof); + stateTransitionProof.verifyIf(verifyStProof); - // TransactionProof format checks - transactionProof.publicInput.blockHashRoot.assertEquals( - Field(0), - "TransactionProof cannot carry the blockHashRoot - publicInput" - ); - transactionProof.publicOutput.blockHashRoot.assertEquals( - Field(0), - "TransactionProof cannot carry the blockHashRoot - publicOutput" - ); - transactionProof.publicInput.networkStateHash.assertEquals( - transactionProof.publicOutput.networkStateHash, - "TransactionProof cannot alter the network state" + // Apply STProof if not deferred + const stateProofResult = this.includeSTProof( + stateTransitionProof, + verifyStProof, + state.stateRoot, + state.pendingSTBatches.commitment, + state.witnessedRoots.commitment ); + state.stateRoot = stateProofResult.stateRoot; + state.pendingSTBatches.commitment = stateProofResult.pendingSTBatchesHash; + state.witnessedRoots.commitment = stateProofResult.witnessedRootsHash; + } - const state = BlockProverStateCommitments.toBlockProverState( - publicInput, - networkState + private verifyTransactionProof( + state: BlockProverState, + transactionProof: TransactionProof, + deferTransactionProof: Bool + ) { + // Verify Transaction proof if it has at least 1 tx and it isn't deferred + const finalizeBlockProof = deferTransactionProof.not(); + const verifyTransactionProof = finalizeBlockProof.and( + state.bundleList.isEmpty().not() ); - // Verify Transaction proof if it has at least 1 tx - i.e. the - // input and output doesn't match fully - // We have to compare the whole input and output because we can make no - // assumptions about the values, since it can be an arbitrary dummy-proof - const txProofOutput = transactionProof.publicOutput; - const isEmptyTransition = txProofOutput.equals( - transactionProof.publicInput, - txProofOutput.closed - ); - const skipTransactionProofVerification = isEmptyTransition; - const verifyTransactionProof = isEmptyTransition.not(); - log.provable.debug("VerifyIf TxProof", verifyTransactionProof); transactionProof.verifyIf(verifyTransactionProof); - // 2. Execute beforeBlock hooks - const beforeBlockArgs = toProvableHookBlockState(state); - const beforeBlockResult = await this.executeBlockHooks( - async (module, networkStateArg, args) => - await module.beforeBlock(networkStateArg, args), - beforeBlockArgs, - networkState + // Fast-forward transaction trackers by the results of the aggregated transaction proof + // Implicitly, the 'from' values here are asserted against the publicInput, since the hashlists + // are created out of the public input + state.eternalTransactionsList.fastForwardIf( + { + from: transactionProof.publicInput.eternalTransactionsHash, + to: transactionProof.publicOutput.eternalTransactionsHash, + }, + verifyTransactionProof, + "eternalTransactionsList" ); - state.pendingSTBatches.push(beforeBlockResult.batch); + state.incomingMessages.fastForwardIf( + { + from: transactionProof.publicInput.incomingMessagesHash, + to: transactionProof.publicOutput.incomingMessagesHash, + }, + verifyTransactionProof, + "incomingMessages" + ); - // 4. Apply TX-type BlockProof - transactionProof.publicInput.networkStateHash - .equals(beforeBlockResult.result.hash()) - .or(skipTransactionProofVerification) - .assertTrue( - "TransactionProof networkstate hash not matching beforeBlock hook result" - ); - transactionProof.publicInput.stateRoot.assertEquals( - transactionProof.publicOutput.stateRoot, - "TransactionProofs can't change the state root" + // Cancel out remainders for transaction proof + assertEqualsIf( + transactionProof.publicInput.bundlesHash, + Field(0), + verifyTransactionProof, + "TransactionProof has to start bundles at 0" ); - // Check that the transaction proof's STs start after the beforeBlock hook - transactionProof.publicInput.pendingSTBatchesHash.assertEquals( - state.pendingSTBatches.commitment, - "Transaction proof doesn't start their STs after the beforeBlockHook" + // Fast Backwards actually, but logic holds + state.bundleList.fastForwardIf( + { + from: transactionProof.publicOutput.bundlesHash, + to: state.bundleList.empty(), + }, + verifyTransactionProof, + "bundles hash" ); - // Fast-forward the stBatchHashList to after all transactions appended - state.pendingSTBatches.commitment = - transactionProof.publicOutput.pendingSTBatchesHash; + } - // Fast-forward block content commitments by the results of the aggregated transaction proof - // Implicitly, the 'from' values here are asserted against the publicInput, since the hashlists - // are created out of the public input - state.transactionList.fastForward({ - from: transactionProof.publicInput.transactionsHash, - to: transactionProof.publicOutput.transactionsHash, - }); - state.eternalTransactionsList.fastForward({ - from: transactionProof.publicInput.eternalTransactionsHash, - to: transactionProof.publicOutput.eternalTransactionsHash, - }); - state.incomingMessages.fastForward({ - from: transactionProof.publicInput.incomingMessagesHash, - to: transactionProof.publicOutput.incomingMessagesHash, - }); + private parseState( + publicInput: BlockProverPublicInput, + stateWitness: BlockProverStateInput, + networkState: NetworkState, + blockWitness: BlockHashMerkleTreeWitness + ) { + const hasNoStateRemained = publicInput.proverStateRemainder.equals(0); - // Witness root - const isEmpty = state.pendingSTBatches.commitment.equals(0); - isEmpty - .implies(state.stateRoot.equals(afterBlockRootWitness.witnessedRoot)) - .assertTrue(); + // If the state is supplied as a witness, we check that it is equals the PI's stateHash + stateWitness + .hash() + .equals(publicInput.proverStateRemainder) + .or(hasNoStateRemained) + .assertTrue("Input state witness is invalid"); - state.witnessedRoots.witnessRoot( - { - appliedBatchListState: state.pendingSTBatches.commitment, - root: afterBlockRootWitness.witnessedRoot, - }, - afterBlockRootWitness.preimage, - isEmpty.not() + const stateInputs = Provable.if( + hasNoStateRemained, + BlockProverStateInput, + BlockProverStateInput.fromPublicInput(publicInput), + stateWitness ); - // 5. Calculate the new block tree hash + stateInputs.networkStateHash.assertEquals( + networkState.hash(), + "Network state not valid" + ); + + const state = BlockProverState.blockProverFromCommitments( + stateInputs, + networkState, + blockWitness + ); + + // Verify block witness validity const blockIndex = blockWitness.calculateIndex(); - blockIndex.assertEquals(publicInput.blockNumber); + blockIndex.assertEquals(stateInputs.blockNumber); blockWitness .calculateRoot(Field(0)) .assertEquals( - publicInput.blockHashRoot, + stateInputs.blockHashRoot, "Supplied block hash witness not matching state root" ); - state.blockHashRoot = blockWitness.calculateRoot( - new BlockHashTreeEntry({ - block: { - index: blockIndex, - transactionListHash: state.transactionList.commitment, - }, - closed: Bool(true), - }).hash() - ); - - // 6. Execute afterBlock hooks + return state; + } - // Switch state service to afterBlock one - this.stateServiceProvider.popCurrentStateService(); + private computeOutput( + publicInput: BlockProverPublicInput, + state: BlockProverState, + finalizeBlockProof: Bool + ) { + const finalizedOutput = state.toCommitments(); - const afterBlockHookArgs = toProvableHookBlockState(state); - const afterBlockResult = await this.executeBlockHooks( - async (module, networkStateArg, args) => - await module.afterBlock(networkStateArg, args), - { - ...afterBlockHookArgs, - stateRoot: afterBlockRootWitness.witnessedRoot, - }, - beforeBlockResult.result + const deferredOutput = { + ...publicInput, + }; + deferredOutput.proverStateRemainder = finalizedOutput.hash(); + + return new BlockProverPublicOutput( + Provable.if( + finalizeBlockProof, + BlockProverPublicOutput, + finalizedOutput.finalize(finalizeBlockProof), + deferredOutput + ) ); + } - state.pendingSTBatches.push(afterBlockResult.batch); - - state.networkState = afterBlockResult.result; - - // 7. Close block + @provableMethod() + public async proveBlockBatchNoProofs( + publicInput: BlockProverPublicInput, + stateWitness: BlockProverStateInput, + networkState: NetworkState, + blockWitness: BlockHashMerkleTreeWitness, + batch: BlockArgumentsBatch, + finalize: Bool + ) { + return await this.proveBlockBatch( + false, + publicInput, + stateWitness, + networkState, + blockWitness, + batch, + Bool(true), + Bool(true), + finalize + ); + } - // Verify ST Proof only if STs have been emitted, - // and we don't defer the verification of the STs - // otherwise we can input a dummy proof - const batchesEmpty = state.pendingSTBatches.commitment.equals(Field(0)); - const verifyStProof = deferSTProof.not().and(batchesEmpty.not()); - log.provable.debug("Verify STProof", verifyStProof); - stateTransitionProof.verifyIf(verifyStProof); + @provableMethod() + public async proveBlockBatchWithProofs( + publicInput: BlockProverPublicInput, + stateWitness: BlockProverStateInput, + networkState: NetworkState, + blockWitness: BlockHashMerkleTreeWitness, + batch: BlockArgumentsBatch, + deferSTProof: Bool, + deferTransactionProof: Bool, + stateTransitionProof: StateTransitionProof, + transactionProof: TransactionProof + ) { + const finalize = deferTransactionProof.or(deferSTProof).not(); - // Apply STProof if not deferred - const stateProofResult = this.includeSTProof( + return await this.proveBlockBatch( + true, + publicInput, + stateWitness, + networkState, + blockWitness, + batch, + deferSTProof, + deferTransactionProof, + finalize, stateTransitionProof, - verifyStProof, - state.stateRoot, - state.pendingSTBatches.commitment, - state.witnessedRoots.commitment + transactionProof ); - state.stateRoot = stateProofResult.stateRoot; - state.pendingSTBatches.commitment = stateProofResult.pendingSTBatchesHash; - state.witnessedRoots.commitment = stateProofResult.witnessedRootsHash; - - state.blockNumber = blockIndex.add(1); - - return new BlockProverPublicOutput({ - ...BlockProverStateCommitments.fromBlockProverState(state), - closed: Bool(true), - }); } - @provableMethod() - public async merge( + public async proveBlockBatch( + doProofVerification: boolean, publicInput: BlockProverPublicInput, - proof1: BlockProverProof, - proof2: BlockProverProof + stateWitness: BlockProverStateInput, + networkState: NetworkState, + blockWitness: BlockHashMerkleTreeWitness, + batch: BlockArgumentsBatch, + deferSTProof: Bool, + deferTransactionProof: Bool, + finalize: Bool, + stateTransitionProof?: StateTransitionProof, + transactionProof?: TransactionProof ): Promise { - proof1.verify(); - proof2.verify(); - - // Check state - publicInput.stateRoot.assertEquals( - proof1.publicInput.stateRoot, - errors.stateRootNotMatching("publicInput.from -> proof1.from") + let state = this.parseState( + publicInput, + stateWitness, + networkState, + blockWitness ); - proof1.publicOutput.stateRoot.assertEquals( - proof2.publicInput.stateRoot, - errors.stateRootNotMatching("proof1.to -> proof2.from") + + // Prove blocks iteratively + state = await reduceSequential( + batch.batch, + async (current, block) => { + const result = await this.proveBlock(current.copy(), block); + + this.stateServiceProvider.popCurrentStateService(); + + return BlockProverState.choose(block.isDummy, current, result); + }, + state ); - // Check transaction list hash. - // Only assert them if these are tx proofs, skip for closed proofs - publicInput.transactionsHash - .equals(proof1.publicInput.transactionsHash) - .or(proof1.publicOutput.closed) - .assertTrue( - errors.transactionsHashNotMatching("publicInput.from -> proof1.from") - ); - proof1.publicOutput.transactionsHash - .equals(proof2.publicInput.transactionsHash) - .or(proof1.publicOutput.closed) - .assertTrue( - errors.transactionsHashNotMatching("proof1.to -> proof2.from") + if (doProofVerification) { + this.verifyTransactionProof( + state, + transactionProof!, + deferTransactionProof ); + this.verifySTProof(state, stateTransitionProof!, deferSTProof); + } - // Check networkhash - publicInput.networkStateHash.assertEquals( - proof1.publicInput.networkStateHash, - errors.networkStateHashNotMatching("publicInput.from -> proof1.from") - ); - proof1.publicOutput.networkStateHash.assertEquals( - proof2.publicInput.networkStateHash, - errors.networkStateHashNotMatching("proof1.to -> proof2.from") - ); + return this.computeOutput(publicInput, state, finalize); + } + + private async proveBlock( + state: BlockProverState, + args: BlockArguments + ): Promise { + const { networkState, blockWitness } = state; + const { afterBlockRootWitness, transactionsHash, isDummy } = args; + + const startingPendingStBatches = state.pendingSTBatches.commitment; - // Check blockHashRoot - publicInput.blockHashRoot.assertEquals( - proof1.publicInput.blockHashRoot, - errors.transactionsHashNotMatching("publicInput.from -> proof1.from") + // 1. Execute beforeBlock hooks + const beforeBlockArgs = toBeforeBlockHookArgument(state); + const beforeBlockResult = await this.executeBlockHooks( + "before", + async (module, networkStateArg, hookArgs) => + await module.beforeBlock(networkStateArg, hookArgs), + beforeBlockArgs, + networkState, + isDummy ); - proof1.publicOutput.blockHashRoot.assertEquals( - proof2.publicInput.blockHashRoot, - errors.transactionsHashNotMatching("proof1.to -> proof2.from") + + state.pendingSTBatches.push(beforeBlockResult.batch); + + // 2. "Apply" TX-type BlockProof + args.pendingSTBatchesHash.from.assertEquals( + state.pendingSTBatches.commitment ); + args.witnessedRootsHash.from.assertEquals(state.witnessedRoots.commitment); + const isEmptyBlock = transactionsHash.equals(Field(0)); + const isNotEmptyBlock = isEmptyBlock.not(); - // Check eternalTransactionsHash - publicInput.eternalTransactionsHash.assertEquals( - proof1.publicInput.eternalTransactionsHash, - errors.transactionsHashNotMatching("publicInput.from -> proof1.from") + // Check & fast-forward the stBatchHashList to after all transactions appended + state.pendingSTBatches.fastForward( + args.pendingSTBatchesHash, + "Transaction proof doesn't start their STs after the beforeBlockHook" ); - proof1.publicOutput.eternalTransactionsHash.assertEquals( - proof2.publicInput.eternalTransactionsHash, - errors.transactionsHashNotMatching("proof1.to -> proof2.from") + // Same for witnessedRootsHash + state.witnessedRoots.fastForward( + args.witnessedRootsHash, + "Transaction proof doesn't start with correct witnessed roots hash" ); - // Check incomingMessagesHash - publicInput.incomingMessagesHash.assertEquals( - proof1.publicInput.incomingMessagesHash, - errors.propertyNotMatchingStep( - "IncomingMessagesHash", - "publicInput.from -> proof1.from" - ) - ); - proof1.publicOutput.incomingMessagesHash.assertEquals( - proof2.publicInput.incomingMessagesHash, - errors.propertyNotMatchingStep( - "IncomingMessagesHash", - "proof1.to -> proof2.from" - ) + // Add block to bundles list + const bundle = new Bundle({ + transactionsHash: transactionsHash, + networkStateHash: beforeBlockResult.result.hash(), + pendingSTBatchesHash: args.pendingSTBatchesHash, + witnessedRootsHash: args.witnessedRootsHash, + }); + state.bundleList.pushIf(bundle, isNotEmptyBlock); + + // 3. + // Calculate new block tree root and increment witness + // Blocknumber as the index here is already authenticated previously + const [root, newWitness] = blockWitness.calculateRootIncrement( + state.blockNumber, + new BlockHashTreeEntry({ + block: { + index: state.blockNumber, + transactionListHash: transactionsHash, + }, + closed: Bool(true), + }).hash() ); - // Check pendingSTBatchesHash - publicInput.pendingSTBatchesHash.assertEquals( - proof1.publicInput.pendingSTBatchesHash, - errors.transactionsHashNotMatching("publicInput.from -> proof1.from") + state.blockHashRoot = root; + state.blockWitness = newWitness; + + state.blockNumber = state.blockNumber.add(1); + + // 4. Execute afterBlock hooks + // Witness root + const hasNoSTBatches = state.pendingSTBatches.commitment.equals( + startingPendingStBatches ); - proof1.publicOutput.pendingSTBatchesHash.assertEquals( - proof2.publicInput.pendingSTBatchesHash, - errors.transactionsHashNotMatching("proof1.to -> proof2.from") + + // TODO Cover case when we witness root but pendingSTBatches is completely empty + + state.witnessedRoots.witnessRoot( + { + appliedBatchListState: state.pendingSTBatches.commitment, + root: afterBlockRootWitness.witnessedRoot, + }, + hasNoSTBatches.not() ); - // Check witnessedRootsHash - publicInput.witnessedRootsHash.assertEquals( - proof1.publicInput.witnessedRootsHash, - errors.transactionsHashNotMatching("publicInput.from -> proof1.from") + // Switch state service to afterBlock one + this.stateServiceProvider.popCurrentStateService(); + + // Execute hooks + const afterBlockHookArgs = toAfterBlockHookArgument( + state, + afterBlockRootWitness.witnessedRoot, + transactionsHash ); - proof1.publicOutput.witnessedRootsHash.assertEquals( - proof2.publicInput.witnessedRootsHash, - errors.transactionsHashNotMatching("proof1.to -> proof2.from") + const afterBlockResult = await this.executeBlockHooks( + "after", + async (module, networkStateArg, hookArgs) => + await module.afterBlock(networkStateArg, hookArgs), + { + ...afterBlockHookArgs, + }, + beforeBlockResult.result, + isDummy ); - // Assert closed indicator matches - // (i.e. we can only merge TX-Type and Block-Type with each other) - proof1.publicOutput.closed.assertEquals( - proof2.publicOutput.closed, - "Closed indicators not matching" - ); + // Apply state and network state changes + state.pendingSTBatches.push(afterBlockResult.batch); + state.networkState = afterBlockResult.result; + + return state; + } - // Either - // blockNumbers are unset and proofs are unclosed or - // both blocks are closed, then they have to increment or - // one block is closed, then height has to be the same - - // Imperative algo would look like - // if(proof1.height == MAX && proof2.height == MAX){ - // assert !proof1.closed && !proof2.closed; - // }else if(proof1.closed && proof2.closed){ - // assert proof1.height + 1 == proof2.height - // // next one is omitted for now - // }else if(proof1.closed || proof2.closed{ - // assert proof1.height == proof2.height - // } - - const proof1Closed = proof1.publicOutput.closed; - const proof2Closed = proof2.publicOutput.closed; - - const blockNumberProgressionValid = publicInput.blockNumber - .equals(proof1.publicInput.blockNumber) - .and( - proof1.publicOutput.blockNumber.equals(proof2.publicInput.blockNumber) + @provableMethod() + public async merge( + publicInput: BlockProverPublicInput, + proof1: BlockProof, + proof2: BlockProof + ): Promise { + proof1.verify(); + proof2.verify(); + + function checkProperty< + Key extends keyof NonMethods, + >(key: Key) { + // Check state + publicInput[key].assertEquals( + proof1.publicInput[key], + errors.propertyNotMatchingStep(key, "publicInput.from -> proof1.from") ); + proof1.publicOutput[key].assertEquals( + proof2.publicInput[key], + errors.propertyNotMatchingStep(key, "proof1.to -> proof2.from") + ); + } - // For tx proofs, we check that the progression starts and end with MAX - // in addition to that both proofs are non-closed - const isValidTransactionMerge = publicInput.blockNumber - .equals(MAX_FIELD) - .and(blockNumberProgressionValid) - .and(proof1Closed.or(proof2Closed).not()); - - const isValidClosedMerge = proof1Closed - .and(proof2Closed) - .and(blockNumberProgressionValid); - - isValidTransactionMerge - .or(isValidClosedMerge) - .assertTrue("Invalid BlockProof merge"); - - return new BlockProverPublicOutput({ - stateRoot: proof2.publicOutput.stateRoot, - transactionsHash: proof2.publicOutput.transactionsHash, - networkStateHash: proof2.publicOutput.networkStateHash, - blockHashRoot: proof2.publicOutput.blockHashRoot, - eternalTransactionsHash: proof2.publicOutput.eternalTransactionsHash, - incomingMessagesHash: proof2.publicOutput.incomingMessagesHash, - closed: isValidClosedMerge, - blockNumber: proof2.publicOutput.blockNumber, - pendingSTBatchesHash: proof2.publicOutput.pendingSTBatchesHash, - witnessedRootsHash: proof2.publicOutput.witnessedRootsHash, - }); + checkProperty("stateRoot"); + checkProperty("networkStateHash"); + checkProperty("blockHashRoot"); + checkProperty("eternalTransactionsHash"); + checkProperty("incomingMessagesHash"); + checkProperty("proverStateRemainder"); + + return proof2.publicOutput; } /** @@ -933,11 +621,12 @@ export class BlockProverProgrammable extends ZkProgrammable< BlockProverPublicInput, BlockProverPublicOutput >[] { - const { prover, stateTransitionProver } = this; + const { prover, stateTransitionProver, transactionProver } = this; const StateTransitionProofClass = stateTransitionProver.zkProgram[0].Proof; - const proveTransaction = prover.proveTransaction.bind(prover); - const proveTransactions = prover.proveTransactions.bind(prover); - const proveBlock = prover.proveBlock.bind(prover); + const TransactionProofClass = transactionProver.zkProgram[0].Proof; + const proveBlockBatchWithProofs = + prover.proveBlockBatchWithProofs.bind(prover); + const proveBlockBatchNoProofs = prover.proveBlockBatchNoProofs.bind(prover); const merge = prover.merge.bind(prover); const program = ZkProgram({ @@ -946,78 +635,68 @@ export class BlockProverProgrammable extends ZkProgrammable< publicOutput: BlockProverPublicOutput, methods: { - proveTransaction: { + proveBlockBatchWithProofs: { privateInputs: [ - DynamicRuntimeProof, - BlockProverSingleTransactionExecutionData, - ], - - async method( - publicInput: BlockProverPublicInput, - runtimeProof: DynamicRuntimeProof, - executionData: BlockProverSingleTransactionExecutionData - ) { - return { - publicOutput: await proveTransaction( - publicInput, - runtimeProof, - executionData - ), - }; - }, - }, - - proveTransactions: { - privateInputs: [ - DynamicRuntimeProof, - DynamicRuntimeProof, - BlockProverMultiTransactionExecutionData, + BlockProverStateInput, + NetworkState, + BlockHashMerkleTreeWitness, + BlockArgumentsBatch, + Bool, + Bool, + StateTransitionProofClass, + TransactionProofClass, ], - async method( publicInput: BlockProverPublicInput, - runtimeProof1: DynamicRuntimeProof, - runtimeProof2: DynamicRuntimeProof, - executionData: BlockProverMultiTransactionExecutionData + stateWitness: BlockProverStateInput, + networkState: NetworkState, + blockWitness: BlockHashMerkleTreeWitness, + batch: BlockArgumentsBatch, + deferSTProof: Bool, + deferTransactionProof: Bool, + stateTransitionProof: StateTransitionProof, + transactionProof: TransactionProof ) { return { - publicOutput: await proveTransactions( + publicOutput: await proveBlockBatchWithProofs( publicInput, - runtimeProof1, - runtimeProof2, - executionData + stateWitness, + networkState, + blockWitness, + batch, + deferSTProof, + deferTransactionProof, + stateTransitionProof, + transactionProof ), }; }, }, - proveBlock: { + proveBlockBatchNoProofs: { privateInputs: [ + BlockProverStateInput, NetworkState, BlockHashMerkleTreeWitness, - StateTransitionProofClass, + BlockArgumentsBatch, Bool, - WitnessedRootWitness, - SelfProof, ], async method( publicInput: BlockProverPublicInput, + stateWitness: BlockProverStateInput, networkState: NetworkState, blockWitness: BlockHashMerkleTreeWitness, - stateTransitionProof: StateTransitionProof, - deferSTs: Bool, - afterBlockRootWitness: WitnessedRootWitness, - transactionProof: BlockProverProof + batch: BlockArgumentsBatch, + finalize: Bool ) { return { - publicOutput: await proveBlock( + publicOutput: await proveBlockBatchNoProofs( publicInput, + stateWitness, networkState, blockWitness, - stateTransitionProof, - deferSTs, - afterBlockRootWitness, - transactionProof + batch, + finalize ), }; }, @@ -1031,8 +710,8 @@ export class BlockProverProgrammable extends ZkProgrammable< async method( publicInput: BlockProverPublicInput, - proof1: BlockProverProof, - proof2: BlockProverProof + proof1: BlockProof, + proof2: BlockProof ) { return { publicOutput: await merge(publicInput, proof1, proof2) }; }, @@ -1041,9 +720,8 @@ export class BlockProverProgrammable extends ZkProgrammable< }); const methods = { - proveTransaction: program.proveTransaction, - proveTransactions: program.proveTransactions, - proveBlock: program.proveBlock, + proveBlockBatchWithProofs: program.proveBlockBatchWithProofs, + proveBlockBatchNoProofs: program.proveBlockBatchNoProofs, merge: program.merge, }; @@ -1081,25 +759,24 @@ export class BlockProver StateTransitionProverPublicOutput > & StateTransitionProvable, - @inject("Runtime") - public readonly runtime: WithZkProgrammable & - CompilableModule, - @injectAll("ProvableTransactionHook") - transactionHooks: ProvableTransactionHook[], + @inject("TransactionProver") + public readonly transactionProver: WithZkProgrammable< + TransactionProverPublicInput, + TransactionProverPublicOutput + > & + TransactionProvable, @injectAll("ProvableBlockHook") blockHooks: ProvableBlockHook[], @inject("StateServiceProvider") - stateServiceProvider: StateServiceProvider, - verificationKeyService: RuntimeVerificationKeyRootService + stateServiceProvider: StateServiceProvider ) { super(); this.zkProgrammable = new BlockProverProgrammable( this, stateTransitionProver.zkProgrammable, - transactionHooks, + transactionProver.zkProgrammable, blockHooks, - stateServiceProvider, - verificationKeyService + stateServiceProvider ); } @@ -1108,61 +785,57 @@ export class BlockProver ): Promise | undefined> { return await registry.forceProverExists(async () => { await this.stateTransitionProver.compile(registry); - await this.runtime.compile(registry); + await this.transactionProver.compile(registry); return await this.zkProgrammable.compile(registry); }); } - public proveTransaction( - publicInput: BlockProverPublicInput, - runtimeProof: DynamicRuntimeProof, - executionData: BlockProverSingleTransactionExecutionData - ): Promise { - return this.zkProgrammable.proveTransaction( - publicInput, - runtimeProof, - executionData - ); - } - - public proveTransactions( + public proveBlockBatchNoProofs( publicInput: BlockProverPublicInput, - runtimeProof1: DynamicRuntimeProof, - runtimeProof2: DynamicRuntimeProof, - executionData: BlockProverMultiTransactionExecutionData + stateWitness: BlockProverStateInput, + networkState: NetworkState, + blockWitness: BlockHashMerkleTreeWitness, + batch: BlockArgumentsBatch, + finalize: Bool ): Promise { - return this.zkProgrammable.proveTransactions( + return this.zkProgrammable.proveBlockBatchNoProofs( publicInput, - runtimeProof1, - runtimeProof2, - executionData + stateWitness, + networkState, + blockWitness, + batch, + finalize ); } - public proveBlock( + public proveBlockBatchWithProofs( publicInput: BlockProverPublicInput, + stateWitness: BlockProverStateInput, networkState: NetworkState, blockWitness: BlockHashMerkleTreeWitness, + batch: BlockArgumentsBatch, + deferSTProof: Bool, + deferTransactionProof: Bool, stateTransitionProof: StateTransitionProof, - deferSTs: Bool, - afterBlockRootWitness: WitnessedRootWitness, - transactionProof: BlockProverProof + transactionProof: TransactionProof ): Promise { - return this.zkProgrammable.proveBlock( + return this.zkProgrammable.proveBlockBatchWithProofs( publicInput, + stateWitness, networkState, blockWitness, + batch, + deferSTProof, + deferTransactionProof, stateTransitionProof, - deferSTs, - afterBlockRootWitness, transactionProof ); } public merge( publicInput: BlockProverPublicInput, - proof1: BlockProverProof, - proof2: BlockProverProof + proof1: BlockProof, + proof2: BlockProof ): Promise { return this.zkProgrammable.merge(publicInput, proof1, proof2); } diff --git a/packages/protocol/src/prover/statetransition/StateTransitionProver.ts b/packages/protocol/src/prover/statetransition/StateTransitionProver.ts index e8716c529..dcfeeaaa7 100644 --- a/packages/protocol/src/prover/statetransition/StateTransitionProver.ts +++ b/packages/protocol/src/prover/statetransition/StateTransitionProver.ts @@ -20,7 +20,6 @@ import { StateTransitionProvableBatch, StateTransitionType, } from "../../model/StateTransitionProvableBatch"; -import { StateTransitionProverType } from "../../protocol/Protocol"; import { ProtocolModule } from "../../protocol/ProtocolModule"; import { DefaultProvableHashList } from "../../utils/ProvableHashList"; import { WitnessedRootHashList } from "../accumulators/WitnessedRootHashList"; @@ -433,10 +432,7 @@ export class StateTransitionProverProgrammable extends ZkProgrammable< @injectable() export class StateTransitionProver extends ProtocolModule - implements - StateTransitionProvable, - StateTransitionProverType, - CompilableModule + implements StateTransitionProvable, CompilableModule { public zkProgrammable: StateTransitionProverProgrammable; diff --git a/packages/protocol/src/prover/transaction/TransactionProvable.ts b/packages/protocol/src/prover/transaction/TransactionProvable.ts new file mode 100644 index 000000000..0803946a5 --- /dev/null +++ b/packages/protocol/src/prover/transaction/TransactionProvable.ts @@ -0,0 +1,183 @@ +// eslint-disable-next-line max-classes-per-file +import { CompilableModule, WithZkProgrammable } from "@proto-kit/common"; +import { DynamicProof, Field, Proof, Signature, Struct, Void } from "o1js"; + +import { RuntimeTransaction } from "../../model/transaction/RuntimeTransaction"; +import { RuntimeVerificationKeyAttestation } from "../block/accummulators/RuntimeVerificationKeyTree"; +import { MethodPublicOutput } from "../../model/MethodPublicOutput"; +import { NetworkState } from "../../model/network/NetworkState"; +import { TransactionHashList } from "../accumulators/TransactionHashList"; +import { AppliedBatchHashList } from "../accumulators/AppliedBatchHashList"; +import { MinaActionsHashList } from "../../utils/MinaPrefixedProvableHashList"; +import { WitnessedRootHashList } from "../accumulators/WitnessedRootHashList"; +import { BundleHashList, BundlePreimage } from "../accumulators/BlockHashList"; + +export class TransactionProverState { + /** + * The current commitment of the transaction-list which + * will at the end equal the bundle hash + */ + transactionList: TransactionHashList; + + /** + * The network state which gives access to values such as blockHeight + * This value is the same for the whole batch (L2 block) + */ + bundleList: BundleHashList; + + /** + * A variant of the transactionsHash that is never reset. + * Thought for usage in the sequence state mempool. + * In comparison, transactionsHash restarts at 0 for every new block + */ + eternalTransactionsList: TransactionHashList; + + pendingSTBatches: AppliedBatchHashList; + + incomingMessages: MinaActionsHashList; + + witnessedRoots: WitnessedRootHashList; + + constructor(args: { + transactionList: TransactionHashList; + bundleList: BundleHashList; + eternalTransactionsList: TransactionHashList; + pendingSTBatches: AppliedBatchHashList; + incomingMessages: MinaActionsHashList; + witnessedRoots: WitnessedRootHashList; + }) { + this.transactionList = args.transactionList; + this.bundleList = args.bundleList; + this.eternalTransactionsList = args.eternalTransactionsList; + this.pendingSTBatches = args.pendingSTBatches; + this.incomingMessages = args.incomingMessages; + this.witnessedRoots = args.witnessedRoots; + } + + public toCommitments(): TransactionProverPublicInput { + return { + bundlesHash: this.bundleList.commitment, + // pendingSTBatchesHash: this.pendingSTBatches.commitment, + // transactionsHash: this.transactionList.commitment, + eternalTransactionsHash: this.eternalTransactionsList.commitment, + incomingMessagesHash: this.incomingMessages.commitment, + // witnessedRootsHash: this.witnessedRoots.commitment, + }; + } + + public static fromCommitments( + publicInput: TransactionProverPublicInput, + args: TransactionProverArguments + ): TransactionProverState { + return new TransactionProverState({ + // Stuff that has to be authenticated via public input, since it's not inside the bundle hash + bundleList: new BundleHashList( + publicInput.bundlesHash, + args.bundleListPreimage + ), + eternalTransactionsList: new TransactionHashList( + publicInput.eternalTransactionsHash + ), + incomingMessages: new MinaActionsHashList( + publicInput.incomingMessagesHash + ), + // Remainders (i.e. stuff that goes into the bundle) + transactionList: new TransactionHashList(args.transactionHash), + pendingSTBatches: new AppliedBatchHashList(args.pendingSTBatchesHash), + witnessedRoots: new WitnessedRootHashList(args.witnessedRootsHash), + }); + } +} + +// These are all linear trackers, i.e. continuously progressing without +// interruptions from the block prover +export const TransactionProverStateCommitments = { + bundlesHash: Field, + eternalTransactionsHash: Field, + incomingMessagesHash: Field, +}; + +export class TransactionProverArguments extends Struct({ + // Commitment to the list of unprocessed (pending) batches of STs that need to be proven + pendingSTBatchesHash: Field, + witnessedRootsHash: Field, + transactionHash: Field, + bundleListPreimage: BundlePreimage, + networkState: NetworkState, +}) {} + +export class TransactionProverPublicInput extends Struct( + TransactionProverStateCommitments +) { + public static equals( + input1: TransactionProverPublicInput, + input2: TransactionProverPublicInput + ) { + const output2 = TransactionProverPublicInput.toFields(input2); + const output1 = TransactionProverPublicInput.toFields(input1); + return output1 + .map((value1, index) => value1.equals(output2[index])) + .reduce((a, b) => a.and(b)); + } +} + +export class TransactionProverPublicOutput extends TransactionProverPublicInput {} + +export class TransactionProverTransactionArguments extends Struct({ + transaction: RuntimeTransaction, + signature: Signature, + verificationKeyAttestation: RuntimeVerificationKeyAttestation, +}) {} + +export class DynamicRuntimeProof extends DynamicProof< + Void, + MethodPublicOutput +> { + static publicInputType = Void; + + static publicOutputType = MethodPublicOutput; + + // TODO this won't be 0 for proofs-as-args + static maxProofsVerified = 0 as const; +} + +export class TransactionProverExecutionData extends Struct({ + transaction: TransactionProverTransactionArguments, + args: TransactionProverArguments, +}) {} + +export type TransactionProof = Proof< + TransactionProverPublicInput, + TransactionProverPublicOutput +>; + +export interface TransactionProvable + extends WithZkProgrammable< + TransactionProverPublicInput, + TransactionProverPublicOutput + >, + CompilableModule { + proveTransaction: ( + publicInput: TransactionProverPublicInput, + runtimeProof: DynamicRuntimeProof, + executionData: TransactionProverExecutionData + ) => Promise; + + proveTransactions: ( + publicInput: TransactionProverPublicInput, + runtimeProof1: DynamicRuntimeProof, + runtimeProof2: DynamicRuntimeProof, + executionData1: TransactionProverExecutionData, + executionData2: TransactionProverExecutionData + ) => Promise; + + dummy: ( + publicInput: TransactionProverPublicInput + ) => Promise; + + merge: ( + publicInput: TransactionProverPublicInput, + proof1: TransactionProof, + proof2: TransactionProof + ) => Promise; +} diff --git a/packages/protocol/src/prover/transaction/TransactionProver.ts b/packages/protocol/src/prover/transaction/TransactionProver.ts new file mode 100644 index 000000000..7d8291cf4 --- /dev/null +++ b/packages/protocol/src/prover/transaction/TransactionProver.ts @@ -0,0 +1,561 @@ +import { + AreProofsEnabled, + CompilableModule, + CompileArtifact, + CompileRegistry, + PlainZkProgram, + provableMethod, + WithZkProgrammable, + ZkProgrammable, +} from "@proto-kit/common"; +import { Bool, Field, SelfProof, VerificationKey, ZkProgram } from "o1js"; +import { inject, injectable, injectAll } from "tsyringe"; + +import { NetworkState } from "../../model/network/NetworkState"; +import { ProtocolModule } from "../../protocol/ProtocolModule"; +import { MethodPublicOutput } from "../../model/MethodPublicOutput"; +import { + AfterTransactionHookArguments, + BeforeTransactionHookArguments, + ProvableTransactionHook, + toAfterTransactionHookArgument, + toBeforeTransactionHookArgument, +} from "../../protocol/ProvableTransactionHook"; +import { StateServiceProvider } from "../../state/StateServiceProvider"; +import { RuntimeVerificationKeyRootService } from "../block/services/RuntimeVerificationKeyRootService"; +import { addTransactionToBundle, executeHooks } from "../utils"; +import { AuthorizedTransaction } from "../../model/transaction/AuthorizedTransaction"; +import { + MethodVKConfigData, + MinimalVKTreeService, + RuntimeVerificationKeyAttestation, +} from "../block/accummulators/RuntimeVerificationKeyTree"; + +import { + TransactionProverExecutionData, + DynamicRuntimeProof, + TransactionProof, + TransactionProvable, + TransactionProverArguments, + TransactionProverPublicInput, + TransactionProverPublicOutput, + TransactionProverState, + TransactionProverTransactionArguments, +} from "./TransactionProvable"; + +const errors = { + invalidZkProgramTreeRoot: () => + "Root hash of the provided zkProgram config witness is invalid", + + propertyNotMatchingStep: (propertyName: string, step: string) => + `${propertyName} not matching: ${step}`, + + transactionsHashNotMatching: (step: string) => + errors.propertyNotMatchingStep("Transactions hash", step), + + bundlesHashNotMatching: (step: string) => + errors.propertyNotMatchingStep("Bundles hash", step), +}; + +type ApplyTransactionArguments = Omit< + TransactionProverTransactionArguments, + "verificationKeyAttestation" +>; + +type TransactionHookArgument = T extends "before" + ? BeforeTransactionHookArguments + : AfterTransactionHookArguments; + +export class TransactionProverZkProgrammable extends ZkProgrammable< + TransactionProverPublicInput, + TransactionProverPublicOutput +> { + public constructor( + private readonly prover: TransactionProver, + private readonly transactionHooks: ProvableTransactionHook[], + private readonly stateServiceProvider: StateServiceProvider, + private readonly verificationKeyService: MinimalVKTreeService + ) { + super(); + } + + name = "TransactionProver"; + + public get areProofsEnabled(): AreProofsEnabled | undefined { + return this.prover.areProofsEnabled; + } + + /** + * Applies and checks the two proofs and applies the corresponding state + * changes to the given state. + * + * The rough high level workflow of this function: + * 1. Execute beforeTransaction hooks, pushing the ST batch + * 2. Add Transaction to bundle, meaning appending it to all the respective commitments + * 3. Push the runtime ST batch + * 4. Execute afterTransaction hooks, pushing the ST batch + * 5. Some consistency checks and signature verification + * + * @param fromState The from-state of the BlockProver + * @param runtimeOutput + * @param executionData + * @param networkState + * @param bundleListPreimage + * @returns The new BlockProver-state to be used as public output + */ + public async applyTransaction( + fromState: TransactionProverState, + runtimeOutput: MethodPublicOutput, + executionData: ApplyTransactionArguments, + networkState: NetworkState + ): Promise { + const { transaction, signature } = executionData; + + let state = { ...fromState }; + + const { isMessage } = runtimeOutput; + + const authorizedTransaction = new AuthorizedTransaction({ + transaction, + signature, + isMessage, + }); + + const beforeTxHookArguments = toBeforeTransactionHookArgument( + authorizedTransaction, + networkState, + state + ); + + // Apply beforeTransaction hook state transitions + const beforeBatch = await this.executeTransactionHooks( + "before", + async (module, args) => await module.beforeTransaction(args), + beforeTxHookArguments, + isMessage + ); + + state.pendingSTBatches.push(beforeBatch); + + state.pendingSTBatches.push({ + batchHash: runtimeOutput.stateTransitionsHash, + applied: runtimeOutput.status, + }); + + state = addTransactionToBundle(state, runtimeOutput.isMessage, transaction); + + // Apply afterTransaction hook state transitions + const afterTxHookArguments = toAfterTransactionHookArgument( + authorizedTransaction, + networkState, + state, + runtimeOutput + ); + + // Switch to different state set for afterTx hooks + this.stateServiceProvider.popCurrentStateService(); + + const afterBatch = await this.executeTransactionHooks( + "after", + async (module, args) => await module.afterTransaction(args), + afterTxHookArguments, + isMessage + ); + state.pendingSTBatches.push(afterBatch); + + // Check transaction integrity against appProof + const blockTransactionHash = transaction.hash(); + + blockTransactionHash.assertEquals( + runtimeOutput.transactionHash, + "Transactions provided in AppProof and BlockProof do not match" + ); + + // Check transaction signature or isMessage + authorizedTransaction + .validateAuthorization() + .assertTrue("Transaction authorization not valid"); + + // Validate layout of transaction witness + transaction.assertTransactionType(isMessage); + + return new TransactionProverState(state); + } + + private verifyVerificationKeyAttestation( + attestation: RuntimeVerificationKeyAttestation, + methodId: Field + ): VerificationKey { + // Verify the [methodId, vk] tuple against the baked-in vk tree root + const { verificationKey, witness: verificationKeyTreeWitness } = + attestation; + + const root = Field(this.verificationKeyService.getRoot()); + const calculatedRoot = verificationKeyTreeWitness.calculateRoot( + new MethodVKConfigData({ + methodId: methodId, + vkHash: verificationKey.hash, + }).hash() + ); + root.assertEquals(calculatedRoot, errors.invalidZkProgramTreeRoot()); + + return verificationKey; + } + + private async executeTransactionHooks( + type: T, + hook: ( + module: ProvableTransactionHook, + args: TransactionHookArgument + ) => Promise, + hookArguments: TransactionHookArgument, + isMessage: Bool + ) { + const { batch, rawStatus } = await executeHooks( + { + transaction: hookArguments.transaction.transaction, + networkState: hookArguments.networkState, + }, + `${type}Transaction`, + async () => { + for (const module of this.transactionHooks) { + // eslint-disable-next-line no-await-in-loop + await hook(module, hookArguments); + } + }, + isMessage + ); + + // This is going to set applied to false in case the hook fails + // (that's only possible for messages though as others are hard-asserted) + batch.applied = rawStatus; + + return batch; + } + + public async proveTransactionInternal( + publicInput: TransactionProverPublicInput, + runtimeProof: DynamicRuntimeProof, + transaction: TransactionProverTransactionArguments, + args: TransactionProverArguments + ): Promise { + const state = TransactionProverState.fromCommitments(publicInput, args); + + state.bundleList.checkLastBundleElement(state, args.networkState); + + const verificationKey = this.verifyVerificationKeyAttestation( + transaction.verificationKeyAttestation, + transaction.transaction.methodId + ); + + runtimeProof.verify(verificationKey); + + const result = await this.applyTransaction( + state, + runtimeProof.publicOutput, + transaction, + args.networkState + ); + + result.bundleList.addToBundle(result, args.networkState); + + return result.toCommitments(); + } + + @provableMethod() + public async proveTransaction( + publicInput: TransactionProverPublicInput, + runtimeProof: DynamicRuntimeProof, + executionData: TransactionProverExecutionData + ): Promise { + return await this.proveTransactionInternal( + publicInput, + runtimeProof, + executionData.transaction, + executionData.args + ); + } + + @provableMethod() + public async proveTransactions( + publicInput: TransactionProverPublicInput, + runtimeProof1: DynamicRuntimeProof, + runtimeProof2: DynamicRuntimeProof, + executionData1: TransactionProverExecutionData, + executionData2: TransactionProverExecutionData + ): Promise { + const state1 = await this.proveTransactionInternal( + publicInput, + runtimeProof1, + executionData1.transaction, + executionData1.args + ); + + // Switch to next state record for 2nd tx beforeTx hook + this.stateServiceProvider.popCurrentStateService(); + + return await this.proveTransactionInternal( + state1, + runtimeProof2, + executionData2.transaction, + executionData2.args + ); + } + + @provableMethod() + public async dummy( + publicInput: TransactionProverPublicInput + ): Promise { + return publicInput; + } + + @provableMethod() + public async merge( + publicInput: TransactionProverPublicInput, + proof1: TransactionProof, + proof2: TransactionProof + ): Promise { + proof1.verify(); + proof2.verify(); + + // Check bundlesHash + publicInput.bundlesHash.assertEquals( + proof1.publicInput.bundlesHash, + errors.bundlesHashNotMatching("publicInput.from -> proof1.from") + ); + proof1.publicOutput.bundlesHash.assertEquals( + proof2.publicInput.bundlesHash, + errors.bundlesHashNotMatching("proof1.to -> proof2.from") + ); + + // Check eternalTransactionsHash + publicInput.eternalTransactionsHash.assertEquals( + proof1.publicInput.eternalTransactionsHash, + errors.transactionsHashNotMatching("publicInput.from -> proof1.from") + ); + proof1.publicOutput.eternalTransactionsHash.assertEquals( + proof2.publicInput.eternalTransactionsHash, + errors.transactionsHashNotMatching("proof1.to -> proof2.from") + ); + + // Check incomingMessagesHash + publicInput.incomingMessagesHash.assertEquals( + proof1.publicInput.incomingMessagesHash, + errors.propertyNotMatchingStep( + "IncomingMessagesHash", + "publicInput.from -> proof1.from" + ) + ); + proof1.publicOutput.incomingMessagesHash.assertEquals( + proof2.publicInput.incomingMessagesHash, + errors.propertyNotMatchingStep( + "IncomingMessagesHash", + "proof1.to -> proof2.from" + ) + ); + + return new TransactionProverPublicOutput({ + bundlesHash: proof2.publicOutput.bundlesHash, + eternalTransactionsHash: proof2.publicOutput.eternalTransactionsHash, + incomingMessagesHash: proof2.publicOutput.incomingMessagesHash, + }); + } + + /** + * Creates the BlockProver ZkProgram. + * Recursive linking of proofs is done via the previously + * injected StateTransitionProver and the required AppChainProof class + */ + public zkProgramFactory(): PlainZkProgram< + TransactionProverPublicInput, + TransactionProverPublicOutput + >[] { + const { prover } = this; + const proveTransaction = prover.proveTransaction.bind(prover); + const proveTransactions = prover.proveTransactions.bind(prover); + const merge = prover.merge.bind(prover); + const dummy = prover.dummy.bind(prover); + + const program = ZkProgram({ + name: "TransactionProver", + publicInput: TransactionProverPublicInput, + publicOutput: TransactionProverPublicOutput, + + methods: { + proveTransaction: { + privateInputs: [DynamicRuntimeProof, TransactionProverExecutionData], + + async method( + publicInput: TransactionProverPublicInput, + runtimeProof: DynamicRuntimeProof, + executionData: TransactionProverExecutionData + ) { + return { + publicOutput: await proveTransaction( + publicInput, + runtimeProof, + executionData + ), + }; + }, + }, + + proveTransactions: { + privateInputs: [ + DynamicRuntimeProof, + DynamicRuntimeProof, + TransactionProverExecutionData, + TransactionProverExecutionData, + ], + + async method( + publicInput: TransactionProverPublicInput, + runtimeProof1: DynamicRuntimeProof, + runtimeProof2: DynamicRuntimeProof, + executionData1: TransactionProverExecutionData, + executionData2: TransactionProverExecutionData + ) { + return { + publicOutput: await proveTransactions( + publicInput, + runtimeProof1, + runtimeProof2, + executionData1, + executionData2 + ), + }; + }, + }, + + dummy: { + privateInputs: [], + async method(publicInput: TransactionProverPublicInput) { + return { publicOutput: await dummy(publicInput) }; + }, + }, + + merge: { + privateInputs: [ + SelfProof< + TransactionProverPublicInput, + TransactionProverPublicOutput + >, + SelfProof< + TransactionProverPublicInput, + TransactionProverPublicOutput + >, + ], + + async method( + publicInput: TransactionProverPublicInput, + proof1: TransactionProof, + proof2: TransactionProof + ) { + return { publicOutput: await merge(publicInput, proof1, proof2) }; + }, + }, + }, + }); + + const methods = { + proveTransaction: program.proveTransaction, + proveTransactions: program.proveTransactions, + dummy: program.dummy, + merge: program.merge, + }; + + const SelfProofClass = ZkProgram.Proof(program); + + return [ + { + name: program.name, + compile: program.compile.bind(program), + verify: program.verify.bind(program), + analyzeMethods: program.analyzeMethods.bind(program), + Proof: SelfProofClass, + methods, + }, + ]; + } +} + +/** + * BlockProver class, which aggregates a AppChainProof and + * a StateTransitionProof into a single BlockProof, that can + * then be merged to be committed to the base-layer contract + */ +@injectable() +export class TransactionProver + extends ProtocolModule + implements TransactionProvable, CompilableModule +{ + public zkProgrammable: TransactionProverZkProgrammable; + + public constructor( + @inject("Runtime") + public readonly runtime: WithZkProgrammable & + CompilableModule, + @injectAll("ProvableTransactionHook") + transactionHooks: ProvableTransactionHook[], + @inject("StateServiceProvider") + stateServiceProvider: StateServiceProvider, + verificationKeyService: RuntimeVerificationKeyRootService + ) { + super(); + this.zkProgrammable = new TransactionProverZkProgrammable( + this, + transactionHooks, + stateServiceProvider, + verificationKeyService + ); + } + + public async compile( + registry: CompileRegistry + ): Promise | undefined> { + return await registry.forceProverExists(async () => { + await this.runtime.compile(registry); + return await this.zkProgrammable.compile(registry); + }); + } + + public proveTransaction( + publicInput: TransactionProverPublicInput, + runtimeProof: DynamicRuntimeProof, + executionData: TransactionProverExecutionData + ): Promise { + return this.zkProgrammable.proveTransaction( + publicInput, + runtimeProof, + executionData + ); + } + + public proveTransactions( + publicInput: TransactionProverPublicInput, + runtimeProof1: DynamicRuntimeProof, + runtimeProof2: DynamicRuntimeProof, + executionData1: TransactionProverExecutionData, + executionData2: TransactionProverExecutionData + ): Promise { + return this.zkProgrammable.proveTransactions( + publicInput, + runtimeProof1, + runtimeProof2, + executionData1, + executionData2 + ); + } + + public dummy(publicInput: TransactionProverPublicInput) { + return this.zkProgrammable.dummy(publicInput); + } + + public merge( + publicInput: TransactionProverPublicInput, + proof1: TransactionProof, + proof2: TransactionProof + ): Promise { + return this.zkProgrammable.merge(publicInput, proof1, proof2); + } +} diff --git a/packages/protocol/src/prover/utils.ts b/packages/protocol/src/prover/utils.ts new file mode 100644 index 000000000..9adc0e68b --- /dev/null +++ b/packages/protocol/src/prover/utils.ts @@ -0,0 +1,108 @@ +import { Bool } from "o1js"; +import { container } from "tsyringe"; + +import { + ProvableStateTransition, + StateTransition, +} from "../model/StateTransition"; +import { AppliedStateTransitionBatch } from "../model/AppliedStateTransitionBatch"; +import { + RuntimeMethodExecutionContext, + RuntimeMethodExecutionData, +} from "../state/context/RuntimeMethodExecutionContext"; +import { RuntimeTransaction } from "../model/transaction/RuntimeTransaction"; +import { MinaActions } from "../utils/MinaPrefixedProvableHashList"; + +import { StateTransitionReductionList } from "./accumulators/StateTransitionReductionList"; +import { TransactionProverState } from "./transaction/TransactionProvable"; + +/** + * Constructs a AppliedBatch based on a list of STs and the flag whether to + * be applied or not. The AppliedBatch is a condensed commitment to a batch + * of STs. + */ +export function constructBatch( + stateTransitions: StateTransition[], + applied: Bool +) { + const transitions = stateTransitions.map((transition) => + transition.toProvable() + ); + + const hashList = new StateTransitionReductionList(ProvableStateTransition); + transitions.forEach((transition) => { + hashList.push(transition); + }); + + return new AppliedStateTransitionBatch({ + batchHash: hashList.commitment, + applied, + }); +} + +// TODO How does this interact with the RuntimeMethodExecutionContext when executing runtimemethods? +export async function executeHooks( + contextArguments: RuntimeMethodExecutionData, + hookName: string, + method: () => Promise, + // This can be either that the tx is a message, or we are inside a dummy block hook + skipEnforceStatus: Bool | undefined = undefined +) { + const executionContext = container.resolve(RuntimeMethodExecutionContext); + executionContext.clear(); + + // Setup context for potential calls to runtime methods. + // This way they can use this.transaction etc. while still having provable + // integrity between data + executionContext.setup(contextArguments); + executionContext.beforeMethod("", "", []); + + const result = await method(); + + executionContext.afterMethod(); + + const { stateTransitions, status, statusMessage } = + executionContext.current().result; + + // See https://github.com/proto-kit/framework/issues/321 for why we do this here + if (skipEnforceStatus !== undefined) { + // isMessage is defined for all tx hooks + status + .or(skipEnforceStatus) + .assertTrue( + `${hookName} hook call failed for non-message tx: ${statusMessage ?? "-"}` + ); + } else { + // isMessage is undefined for all block hooks + status.assertTrue(`${hookName} hook call failed: ${statusMessage ?? "-"}`); + } + + return { + batch: constructBatch(stateTransitions, Bool(true)), + result, + rawStatus: status, + }; +} + +export function addTransactionToBundle< + T extends Pick< + TransactionProverState, + "transactionList" | "eternalTransactionsList" | "incomingMessages" + >, +>(state: T, isMessage: Bool, transaction: RuntimeTransaction): T { + const transactionHash = transaction.hash(); + + // Append tx to transaction list + state.transactionList.push(transactionHash); + + // Append tx to eternal transaction list + // TODO Change that to the a sequence-state compatible transaction struct + state.eternalTransactionsList.push(transactionHash); + + // Append tx to incomingMessagesHash + const actionHash = MinaActions.actionHash(transaction.hashData()); + + state.incomingMessages.pushIf(actionHash, isMessage); + + return state; +} diff --git a/packages/protocol/src/settlement/ContractArgsRegistry.ts b/packages/protocol/src/settlement/ContractArgsRegistry.ts new file mode 100644 index 000000000..f9c2f0020 --- /dev/null +++ b/packages/protocol/src/settlement/ContractArgsRegistry.ts @@ -0,0 +1,60 @@ +import { injectable, singleton } from "tsyringe"; +import merge from "lodash/merge"; + +export interface StaticInitializationContract { + getInitializationArgs(): Args; +} + +export type NaiveObjectSchema = { + [Key in keyof Obj]: undefined extends Obj[Key] ? "Optional" : "Required"; +}; + +/* +interface Test { + one: string; + two?: string; +} + +const x: NaiveObjectSchema = { + one: "Required", + two: "Optional", +}; +*/ + +@injectable() +@singleton() +export class ContractArgsRegistry { + args: Record = {}; + + public addArgs(name: string, addition: Partial) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const args: Partial = this.args[name] ?? {}; + this.args[name] = merge(args, addition); + } + + public resetArgs(name: string) { + delete this.args[name]; + } + + public getArgs(name: string, schema: NaiveObjectSchema): Type { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const args = this.args[name] ?? {}; + + const missing = Object.entries<"Optional" | "Required">(schema).filter( + ([key, type]) => { + // We filter only if the key is required and isn't present + return type === "Required" && args[key] === undefined; + } + ); + + if (missing.length > 0) { + const missingKeys = missing.map(([key]) => key); + throw new Error( + `Contract args for ${name} not all present, ${missingKeys} are missing` + ); + } else { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + return args as Type; + } + } +} diff --git a/packages/protocol/src/settlement/SettlementContractModule.ts b/packages/protocol/src/settlement/SettlementContractModule.ts index 631842d22..3bfaa778b 100644 --- a/packages/protocol/src/settlement/SettlementContractModule.ts +++ b/packages/protocol/src/settlement/SettlementContractModule.ts @@ -16,17 +16,19 @@ import { ProtocolModule } from "../protocol/ProtocolModule"; import { ContractModule } from "./ContractModule"; import { DispatchContractProtocolModule } from "./contracts/DispatchContractProtocolModule"; import { DispatchContractType } from "./contracts/DispatchSmartContract"; -import { - SettlementContractConfig, - SettlementContractProtocolModule, -} from "./contracts/SettlementContractProtocolModule"; -import { SettlementContractType } from "./contracts/SettlementSmartContract"; +import { BridgingSettlementContractModule } from "./contracts/BridgingSettlementContractModule"; import { BridgeContractType } from "./contracts/BridgeContract"; import { BridgeContractConfig, BridgeContractProtocolModule, } from "./contracts/BridgeContractProtocolModule"; -import { GetContracts } from "./modularity/types"; +import { GetContracts, InferContractType } from "./modularity/types"; +import { BridgingSettlementContractType } from "./contracts/settlement/BridgingSettlementContract"; +import { SettlementContractType } from "./contracts/settlement/SettlementBase"; +import { + SettlementContractConfig, + SettlementSmartContractModule, +} from "./contracts/SettlementSmartContractModule"; export type SettlementModulesRecord = ModulesRecord< TypedClass> @@ -36,6 +38,12 @@ export type MandatorySettlementModulesRecord = { SettlementContract: TypedClass< ContractModule >; +}; + +export type BridgingSettlementModulesRecord = { + SettlementContract: TypedClass< + ContractModule + >; DispatchContract: TypedClass>; BridgeContract: TypedClass< ContractModule @@ -44,8 +52,7 @@ export type MandatorySettlementModulesRecord = { @injectable() export class SettlementContractModule< - SettlementModules extends SettlementModulesRecord & - MandatorySettlementModulesRecord, + SettlementModules extends SettlementModulesRecord, > extends ModuleContainer implements ProtocolModule @@ -54,10 +61,7 @@ export class SettlementContractModule< super(definition); } - public static from< - SettlementModules extends SettlementModulesRecord & - MandatorySettlementModulesRecord, - >( + public static from( modules: SettlementModules ): TypedClass> { return class ScopedSettlementContractModule extends SettlementContractModule { @@ -67,27 +71,18 @@ export class SettlementContractModule< }; } - public static mandatoryModules() { + public static settlementOnly() { return { - SettlementContract: SettlementContractProtocolModule, - DispatchContract: DispatchContractProtocolModule, - BridgeContract: BridgeContractProtocolModule, + SettlementContract: SettlementSmartContractModule, } as const; } - public static fromDefaults() { - return SettlementContractModule.from( - SettlementContractModule.mandatoryModules() - ); - } - - public static with( - additionalModules: AdditionalModules - ) { - return SettlementContractModule.from({ - ...SettlementContractModule.mandatoryModules(), - ...additionalModules, - } as const); + public static settlementAndBridging() { + return { + SettlementContract: BridgingSettlementContractModule, + DispatchContract: DispatchContractProtocolModule, + BridgeContract: BridgeContractProtocolModule, + } as const; } // ** For protocol module @@ -116,30 +111,40 @@ export class SettlementContractModule< return Object.fromEntries(contracts); } - public createContracts(addresses: { - settlement: PublicKey; - dispatch: PublicKey; - }): { - settlement: SettlementContractType & SmartContract; - dispatch: DispatchContractType & SmartContract; - } { - const { DispatchContract, SettlementContract } = this.getContractClasses(); - - const dispatchInstance = new DispatchContract(addresses.dispatch); - const settlementInstance = new SettlementContract(addresses.settlement); - - return { - dispatch: dispatchInstance, - settlement: settlementInstance, - }; - } - - public createBridgeContract( + public createContract>( + contractName: ContractName, address: PublicKey, tokenId?: Field - ): BridgeContractType & SmartContract { - const { BridgeContract } = this.getContractClasses(); + ): InferContractType { + const module = this.resolve(contractName); + const ContractClass = module.contractFactory(); + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + return new ContractClass(address, tokenId) as InferContractType< + SettlementModules[ContractName] + >; + } - return new BridgeContract(address, tokenId); + public createContracts< + ContractName extends keyof SettlementModules, + >(addresses: { + [Key in ContractName]: PublicKey; + }): { + [Key in ContractName]: SmartContract & + InferContractType; + } { + const classes = this.getContractClasses(); + + const obj: Record = {}; + // eslint-disable-next-line guard-for-in + for (const key in addresses) { + const ContractClass = classes[key]; + obj[key] = new ContractClass(addresses[key]); + } + + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + return obj as { + [Key in keyof SettlementModules]: SmartContract & + InferContractType; + }; } } diff --git a/packages/protocol/src/settlement/contracts/BridgeContract.ts b/packages/protocol/src/settlement/contracts/BridgeContract.ts index 34917d865..f56f8ec95 100644 --- a/packages/protocol/src/settlement/contracts/BridgeContract.ts +++ b/packages/protocol/src/settlement/contracts/BridgeContract.ts @@ -28,8 +28,13 @@ import { import { Path } from "../../model/Path"; import { OutgoingMessageProcessor } from "../modularity/OutgoingMessageProcessor"; import { PROTOKIT_FIELD_PREFIXES } from "../../hashing/protokit-prefixes"; +import { + ContractArgsRegistry, + NaiveObjectSchema, + StaticInitializationContract, +} from "../ContractArgsRegistry"; -import type { SettlementContractType } from "./SettlementSmartContract"; +import type { BridgingSettlementContractType } from "./settlement/BridgingSettlementContract"; export type BridgeContractType = { stateRoot: State; @@ -64,19 +69,33 @@ export class BridgeContractContext { } = { messageInputs: [] }; } -export abstract class BridgeContractBase extends TokenContract { - public static args: { - SettlementContract: - | (TypedClass & typeof SmartContract) - | undefined; - messageProcessors: OutgoingMessageProcessor[]; - batchSize?: number; - }; +export interface BridgeContractArgs { + SettlementContract: TypedClass & + typeof SmartContract; + messageProcessors: OutgoingMessageProcessor[]; + batchSize?: number; +} + +export const BridgeContractArgsSchema: NaiveObjectSchema = { + batchSize: "Optional", + SettlementContract: "Required", + messageProcessors: "Required", +}; +export abstract class BridgeContractBase + extends TokenContract + implements StaticInitializationContract +{ public constructor(address: PublicKey, tokenId?: Field) { super(address, tokenId); } + getInitializationArgs(): BridgeContractArgs { + return container + .resolve(ContractArgsRegistry) + .getArgs("BridgeContract", BridgeContractArgsSchema); + } + abstract settlementContractAddress: State; abstract stateRoot: State; @@ -134,14 +153,11 @@ export abstract class BridgeContractBase extends TokenContract { // witness values, not update/insert this.stateRoot.set(root); + const args = this.getInitializationArgs(); + const settlementContractAddress = this.settlementContractAddress.getAndRequireEquals(); - const SettlementContractClass = BridgeContractBase.args.SettlementContract; - if (SettlementContractClass === undefined) { - throw new Error( - "Settlement Contract class hasn't been set yet, something is wrong with your module composition" - ); - } + const SettlementContractClass = args.SettlementContract; const settlementContract = new SettlementContractClass( settlementContractAddress ); @@ -150,11 +166,14 @@ export abstract class BridgeContractBase extends TokenContract { } private batchSize() { - return BridgeContractBase.args.batchSize ?? OUTGOING_MESSAGE_BATCH_SIZE; + return ( + this.getInitializationArgs().batchSize ?? OUTGOING_MESSAGE_BATCH_SIZE + ); } private executeProcessors(batchIndex: number, args: OutgoingMessageArgument) { - return BridgeContractBase.args.messageProcessors.map((processor, j) => { + const { messageProcessors } = this.getInitializationArgs(); + return messageProcessors.map((processor, j) => { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const value = Experimental.memoizeWitness(processor.type, () => { return container.resolve(BridgeContractContext).data.messageInputs[ diff --git a/packages/protocol/src/settlement/contracts/BridgeContractProtocolModule.ts b/packages/protocol/src/settlement/contracts/BridgeContractProtocolModule.ts index 1ab75c588..e4537d242 100644 --- a/packages/protocol/src/settlement/contracts/BridgeContractProtocolModule.ts +++ b/packages/protocol/src/settlement/contracts/BridgeContractProtocolModule.ts @@ -3,10 +3,11 @@ import { CompileRegistry } from "@proto-kit/common"; import { ContractModule } from "../ContractModule"; import { OutgoingMessageProcessor } from "../modularity/OutgoingMessageProcessor"; +import { ContractArgsRegistry } from "../ContractArgsRegistry"; import { BridgeContract, - BridgeContractBase, + BridgeContractArgs, BridgeContractType, } from "./BridgeContract"; @@ -21,7 +22,8 @@ export class BridgeContractProtocolModule extends ContractModule< > { public constructor( @injectAll("OutgoingMessageProcessor", { isOptional: true }) - private readonly messageProcessors: OutgoingMessageProcessor[] + private readonly messageProcessors: OutgoingMessageProcessor[], + private readonly contractArgsRegistry: ContractArgsRegistry ) { super(); } @@ -29,11 +31,10 @@ export class BridgeContractProtocolModule extends ContractModule< public contractFactory() { const { config } = this; - BridgeContractBase.args = { - SettlementContract: BridgeContractBase.args?.SettlementContract, + this.contractArgsRegistry.addArgs("BridgeContract", { messageProcessors: this.messageProcessors, batchSize: config.outgoingBatchSize, - }; + }); return BridgeContract; } diff --git a/packages/protocol/src/settlement/contracts/SettlementContractProtocolModule.ts b/packages/protocol/src/settlement/contracts/BridgingSettlementContractModule.ts similarity index 50% rename from packages/protocol/src/settlement/contracts/SettlementContractProtocolModule.ts rename to packages/protocol/src/settlement/contracts/BridgingSettlementContractModule.ts index 6cdf51952..9d1fef1ed 100644 --- a/packages/protocol/src/settlement/contracts/SettlementContractProtocolModule.ts +++ b/packages/protocol/src/settlement/contracts/BridgingSettlementContractModule.ts @@ -12,27 +12,26 @@ import { SmartContractClassFromInterface, } from "../ContractModule"; import { ProvableSettlementHook } from "../modularity/ProvableSettlementHook"; +import { ContractArgsRegistry } from "../ContractArgsRegistry"; -import { DispatchSmartContractBase } from "./DispatchSmartContract"; +import { DispatchContractArgs } from "./DispatchSmartContract"; import { - SettlementContractType, - SettlementSmartContract, - SettlementSmartContractBase, -} from "./SettlementSmartContract"; -import { BridgeContractBase } from "./BridgeContract"; + BridgingSettlementContractType, + BridgingSettlementContract, + BridgingSettlementContractArgs, +} from "./settlement/BridgingSettlementContract"; +import { BridgeContractArgs } from "./BridgeContract"; import { DispatchContractProtocolModule } from "./DispatchContractProtocolModule"; import { BridgeContractProtocolModule } from "./BridgeContractProtocolModule"; - -export type SettlementContractConfig = { - escapeHatchSlotsInterval?: number; -}; - -// 24 hours -const DEFAULT_ESCAPE_HATCH = (60 / 3) * 24; +import { + DEFAULT_ESCAPE_HATCH, + SettlementContractConfig, +} from "./SettlementSmartContractModule"; +import { SettlementContract } from "./settlement/SettlementContract"; @injectable() -export class SettlementContractProtocolModule extends ContractModule< - SettlementContractType, +export class BridgingSettlementContractModule extends ContractModule< + BridgingSettlementContractType, SettlementContractConfig > { public constructor( @@ -44,12 +43,13 @@ export class SettlementContractProtocolModule extends ContractModule< private readonly dispatchContractModule: DispatchContractProtocolModule, @inject("BridgeContract") private readonly bridgeContractModule: BridgeContractProtocolModule, - private readonly childVerificationKeyService: ChildVerificationKeyService + private readonly childVerificationKeyService: ChildVerificationKeyService, + private readonly argsRegistry: ContractArgsRegistry ) { super(); } - public contractFactory(): SmartContractClassFromInterface { + public contractFactory(): SmartContractClassFromInterface { const { hooks, config } = this; const dispatchContract = this.dispatchContractModule.contractFactory(); const bridgeContract = this.bridgeContractModule.contractFactory(); @@ -57,27 +57,27 @@ export class SettlementContractProtocolModule extends ContractModule< const escapeHatchSlotsInterval = config.escapeHatchSlotsInterval ?? DEFAULT_ESCAPE_HATCH; - const { args } = SettlementSmartContractBase; - SettlementSmartContractBase.args = { - ...args, - DispatchContract: dispatchContract, - hooks, - escapeHatchSlotsInterval, - BridgeContract: bridgeContract, - BridgeContractVerificationKey: args?.BridgeContractVerificationKey, - BridgeContractPermissions: args?.BridgeContractPermissions, - signedSettlements: args?.signedSettlements, - ChildVerificationKeyService: this.childVerificationKeyService, - }; - - // Ideally we don't want to have this cyclic dependency, but we have it in the protocol, - // So its logical that we can't avoid that here - BridgeContractBase.args.SettlementContract = SettlementSmartContract; + this.argsRegistry.addArgs( + "SettlementContract", + { + DispatchContract: dispatchContract, + hooks, + escapeHatchSlotsInterval, + BridgeContract: bridgeContract, + ChildVerificationKeyService: this.childVerificationKeyService, + } + ); - DispatchSmartContractBase.args.settlementContractClass = - SettlementSmartContract; + // Ideally, we don't want to have this cyclic dependency, but we have it in the protocol, + // So it's logical that we can't avoid that here + this.argsRegistry.addArgs("BridgeContract", { + SettlementContract: BridgingSettlementContract, + }); + this.argsRegistry.addArgs("DispatchContract", { + settlementContractClass: BridgingSettlementContract, + }); - return SettlementSmartContract; + return BridgingSettlementContract; } public async compile( @@ -91,19 +91,22 @@ export class SettlementContractProtocolModule extends ContractModule< this.contractFactory(); // Init params - SettlementSmartContractBase.args.BridgeContractVerificationKey = - bridgeArtifact.BridgeContract.verificationKey; - - if (SettlementSmartContractBase.args.signedSettlements === undefined) { - throw new Error( - "Args not fully initialized - make sure to also include the SettlementModule in the sequencer" - ); - } + this.argsRegistry.addArgs( + "SettlementContract", + { + BridgeContractVerificationKey: + bridgeArtifact.BridgeContract.verificationKey, + } + ); log.debug("Compiling Settlement Contract"); const artifact = await registry.forceProverExists( - async (reg) => await registry.compile(SettlementSmartContract) + async (reg) => + await registry.compile( + BridgingSettlementContract, + SettlementContract.name + ) ); return { diff --git a/packages/protocol/src/settlement/contracts/DispatchContractProtocolModule.ts b/packages/protocol/src/settlement/contracts/DispatchContractProtocolModule.ts index 5b3663a82..f421d9f1e 100644 --- a/packages/protocol/src/settlement/contracts/DispatchContractProtocolModule.ts +++ b/packages/protocol/src/settlement/contracts/DispatchContractProtocolModule.ts @@ -7,11 +7,12 @@ import { ContractModule, SmartContractClassFromInterface, } from "../ContractModule"; +import { ContractArgsRegistry } from "../ContractArgsRegistry"; import { DispatchSmartContract, DispatchContractType, - DispatchSmartContractBase, + DispatchContractArgs, } from "./DispatchSmartContract"; export type DispatchContractConfig = { @@ -23,7 +24,10 @@ export class DispatchContractProtocolModule extends ContractModule< DispatchContractType, DispatchContractConfig > { - public constructor(@inject("Runtime") private readonly runtime: RuntimeLike) { + public constructor( + @inject("Runtime") private readonly runtime: RuntimeLike, + private readonly contractArgsRegistry: ContractArgsRegistry + ) { super(); } @@ -52,20 +56,18 @@ export class DispatchContractProtocolModule extends ContractModule< this.checkConfigIntegrity(incomingMessagesMethods, methodIdMappings); - DispatchSmartContractBase.args = { - incomingMessagesPaths: incomingMessagesMethods, - methodIdMappings, - settlementContractClass: - DispatchSmartContractBase.args?.settlementContractClass, - }; + this.contractArgsRegistry.addArgs( + "DispatchContract", + { + incomingMessagesPaths: incomingMessagesMethods, + methodIdMappings, + } + ); return DispatchSmartContract; } public async compile(registry: CompileRegistry) { - if (DispatchSmartContractBase.args.settlementContractClass === undefined) { - throw new Error("Reference to Settlement Contract not set"); - } return { DispatchSmartContract: await registry.forceProverExists( async () => await registry.compile(DispatchSmartContract) diff --git a/packages/protocol/src/settlement/contracts/DispatchSmartContract.ts b/packages/protocol/src/settlement/contracts/DispatchSmartContract.ts index 74d765e46..1767b4ebc 100644 --- a/packages/protocol/src/settlement/contracts/DispatchSmartContract.ts +++ b/packages/protocol/src/settlement/contracts/DispatchSmartContract.ts @@ -1,6 +1,7 @@ import { AccountUpdate, Bool, + DeployArgs, Field, method, Poseidon, @@ -13,8 +14,10 @@ import { state, TokenId, UInt64, + Permissions, } from "o1js"; import { InMemoryMerkleTreeStorage, TypedClass } from "@proto-kit/common"; +import { container } from "tsyringe"; import { RuntimeMethodIdMapping } from "../../model/RuntimeLike"; import { RuntimeTransaction } from "../../model/transaction/RuntimeTransaction"; @@ -23,8 +26,13 @@ import { MinaEvents, } from "../../utils/MinaPrefixedProvableHashList"; import { Deposit } from "../messages/Deposit"; +import { + ContractArgsRegistry, + NaiveObjectSchema, + StaticInitializationContract, +} from "../ContractArgsRegistry"; -import type { SettlementContractType } from "./SettlementSmartContract"; +import type { BridgingSettlementContractType } from "./settlement/BridgingSettlementContract"; import { TokenBridgeDeploymentAuth } from "./authorizations/TokenBridgeDeploymentAuth"; import { UpdateMessagesHashAuth } from "./authorizations/UpdateMessagesHashAuth"; import { @@ -38,11 +46,14 @@ import { export const ACTIONS_EMPTY_HASH = Reducer.initialActionState; export interface DispatchContractType { + events: { + "token-bridge-added": typeof TokenBridgeTreeAddition; + }; + updateMessagesHash: ( executedMessagesHash: Field, newPromisedMessagesHash: Field ) => Promise; - initialize: (settlementContract: PublicKey) => Promise; enableTokenDeposits: ( tokenId: Field, bridgeContractAddress: PublicKey, @@ -50,20 +61,36 @@ export interface DispatchContractType { ) => Promise; promisedMessagesHash: State; + + deployAndInitialize: ( + args: DeployArgs | undefined, + permissions: Permissions, + settlementContract: PublicKey + ) => Promise; } const tokenBridgeRoot = new TokenBridgeTree( new InMemoryMerkleTreeStorage() ).getRoot(); -export abstract class DispatchSmartContractBase extends SmartContract { - public static args: { - methodIdMappings: RuntimeMethodIdMapping; - incomingMessagesPaths: Record; - settlementContractClass?: TypedClass & - typeof SmartContract; +export interface DispatchContractArgs { + methodIdMappings: RuntimeMethodIdMapping; + incomingMessagesPaths: Record; + settlementContractClass: TypedClass & + typeof SmartContract; +} + +export const DispatchContractArgsSchema: NaiveObjectSchema = + { + incomingMessagesPaths: "Required", + methodIdMappings: "Required", + settlementContractClass: "Required", }; +export abstract class DispatchSmartContractBase + extends SmartContract + implements StaticInitializationContract +{ events = { "token-bridge-added": TokenBridgeTreeAddition, // We need a placeholder event here, so that o1js internally adds a identifier to the @@ -82,6 +109,12 @@ export abstract class DispatchSmartContractBase extends SmartContract { abstract tokenBridgeCount: State; + getInitializationArgs(): DispatchContractArgs { + return container + .resolve(ContractArgsRegistry) + .getArgs("DispatchContract", DispatchContractArgsSchema); + } + protected updateMessagesHashBase( executedMessagesHash: Field, newPromisedMessagesHash: Field @@ -98,12 +131,12 @@ export abstract class DispatchSmartContractBase extends SmartContract { this.self.account.actionState.requireEquals(newPromisedMessagesHash); this.promisedMessagesHash.set(newPromisedMessagesHash); + const args = this.getInitializationArgs(); const settlementContractAddress = this.settlementContract.getAndRequireEquals(); - const settlementContract = - new DispatchSmartContractBase.args.settlementContractClass!( - settlementContractAddress - ); + const settlementContract = new args.settlementContractClass!( + settlementContractAddress + ); settlementContract.authorizationField.requireEquals( new UpdateMessagesHashAuth({ @@ -116,12 +149,6 @@ export abstract class DispatchSmartContractBase extends SmartContract { } protected initializeBase(settlementContract: PublicKey) { - this.promisedMessagesHash.getAndRequireEquals().assertEquals(Field(0)); - this.honoredMessagesHash.getAndRequireEquals().assertEquals(Field(0)); - this.settlementContract - .getAndRequireEquals() - .assertEquals(PublicKey.empty()); - this.promisedMessagesHash.set(ACTIONS_EMPTY_HASH); this.honoredMessagesHash.set(ACTIONS_EMPTY_HASH); this.settlementContract.set(settlementContract); @@ -174,10 +201,10 @@ export abstract class DispatchSmartContractBase extends SmartContract { // treeWitness: TokenBridgeTreeWitness ) { this.settlementContract.requireEquals(settlementContractAddress); - const settlementContract = - new DispatchSmartContractBase.args.settlementContractClass!( - settlementContractAddress - ); + const args = this.getInitializationArgs(); + const settlementContract = new args.settlementContractClass!( + settlementContractAddress + ); // Append bridge address to the tree // TODO This not concurrent and will fail if multiple users deploy bridges at the same time @@ -239,6 +266,18 @@ export class DispatchSmartContract @state(Field) public tokenBridgeCount = State(); + public async deployAndInitialize( + args: DeployArgs | undefined, + permissions: Permissions, + settlementContract: PublicKey + ): Promise { + await super.deploy(args); + + this.self.account.permissions.set(permissions); + + this.initializeBase(settlementContract); + } + @method public async enableTokenDeposits( tokenId: Field, @@ -263,11 +302,6 @@ export class DispatchSmartContract ); } - @method - public async initialize(settlementContract: PublicKey) { - return this.initializeBase(settlementContract); - } - @method public async deposit( amount: UInt64, @@ -310,7 +344,7 @@ export class DispatchSmartContract }); const { methodIdMappings, incomingMessagesPaths } = - DispatchSmartContractBase.args; + this.getInitializationArgs(); const methodId = Field( methodIdMappings[incomingMessagesPaths.deposit].methodId diff --git a/packages/protocol/src/settlement/contracts/SettlementSmartContract.ts b/packages/protocol/src/settlement/contracts/SettlementSmartContract.ts deleted file mode 100644 index 7dc375d2f..000000000 --- a/packages/protocol/src/settlement/contracts/SettlementSmartContract.ts +++ /dev/null @@ -1,502 +0,0 @@ -import { - prefixToField, - TypedClass, - mapSequential, - ChildVerificationKeyService, - LinkedMerkleTree, -} from "@proto-kit/common"; -import { - AccountUpdate, - Bool, - Field, - method, - PublicKey, - Signature, - SmartContract, - State, - state, - UInt32, - AccountUpdateForest, - TokenContract, - PrivateKey, - VerificationKey, - Permissions, - Struct, - Provable, - TokenId, - DynamicProof, -} from "o1js"; - -import { NetworkState } from "../../model/network/NetworkState"; -import { BlockHashMerkleTree } from "../../prover/block/accummulators/BlockHashMerkleTree"; -import { - BlockProverPublicInput, - BlockProverPublicOutput, -} from "../../prover/block/BlockProvable"; -import { - ProvableSettlementHook, - SettlementHookInputs, - SettlementStateRecord, -} from "../modularity/ProvableSettlementHook"; - -import { DispatchContractType } from "./DispatchSmartContract"; -import { BridgeContractType } from "./BridgeContract"; -import { TokenBridgeDeploymentAuth } from "./authorizations/TokenBridgeDeploymentAuth"; -import { UpdateMessagesHashAuth } from "./authorizations/UpdateMessagesHashAuth"; - -/* eslint-disable @typescript-eslint/lines-between-class-members */ - -export class DynamicBlockProof extends DynamicProof< - BlockProverPublicInput, - BlockProverPublicOutput -> { - public static publicInputType = BlockProverPublicInput; - - public static publicOutputType = BlockProverPublicOutput; - - public static maxProofsVerified = 2 as const; -} - -export class TokenMapping extends Struct({ - tokenId: Field, - publicKey: PublicKey, -}) {} - -export interface SettlementContractType { - authorizationField: State; - - initialize: ( - sequencer: PublicKey, - dispatchContract: PublicKey, - bridgeContract: PublicKey, - contractKey: PrivateKey - ) => Promise; - assertStateRoot: (root: Field) => AccountUpdate; - settle: ( - blockProof: DynamicBlockProof, - signature: Signature, - dispatchContractAddress: PublicKey, - publicKey: PublicKey, - inputNetworkState: NetworkState, - outputNetworkState: NetworkState, - newPromisedMessagesHash: Field - ) => Promise; - addTokenBridge: ( - tokenId: Field, - address: PublicKey, - dispatchContract: PublicKey - ) => Promise; -} - -// Some random prefix for the sequencer signature -export const BATCH_SIGNATURE_PREFIX = prefixToField("pk-batchSignature"); - -// @singleton() -// export class SettlementSmartContractStaticArgs { -// public args?: { -// DispatchContract: TypedClass; -// hooks: ProvableSettlementHook[]; -// escapeHatchSlotsInterval: number; -// BridgeContract: TypedClass & typeof SmartContract; -// // Lazily initialized -// BridgeContractVerificationKey: VerificationKey | undefined; -// BridgeContractPermissions: Permissions | undefined; -// signedSettlements: boolean | undefined; -// }; -// } - -export abstract class SettlementSmartContractBase extends TokenContract { - // This pattern of injecting args into a smartcontract is currently the only - // viable solution that works given the inheritance issues of o1js - // public static args = container.resolve(SettlementSmartContractStaticArgs); - public static args: { - DispatchContract: TypedClass; - hooks: ProvableSettlementHook[]; - escapeHatchSlotsInterval: number; - BridgeContract: TypedClass & typeof SmartContract; - // Lazily initialized - BridgeContractVerificationKey: VerificationKey | undefined; - BridgeContractPermissions: Permissions | undefined; - signedSettlements: boolean | undefined; - ChildVerificationKeyService: ChildVerificationKeyService; - }; - - events = { - "token-bridge-deployed": TokenMapping, - }; - - abstract sequencerKey: State; - abstract lastSettlementL1BlockHeight: State; - abstract stateRoot: State; - abstract networkStateHash: State; - abstract blockHashRoot: State; - abstract dispatchContractAddressX: State; - - abstract authorizationField: State; - - // Not @state - // abstract offchainStateCommitmentsHash: State; - - public assertStateRoot(root: Field): AccountUpdate { - this.stateRoot.requireEquals(root); - return this.self; - } - - // TODO Like these properties, I am too lazy to properly infer the types here - private assertLazyConfigsInitialized() { - const uninitializedProperties: string[] = []; - const { args } = SettlementSmartContractBase; - if (args.BridgeContractPermissions === undefined) { - uninitializedProperties.push("BridgeContractPermissions"); - } - if (args.signedSettlements === undefined) { - uninitializedProperties.push("signedSettlements"); - } - if (uninitializedProperties.length > 0) { - throw new Error( - `Lazy configs of SettlementSmartContract haven't been initialized ${uninitializedProperties.reduce( - (a, b) => `${a},${b}` - )}` - ); - } - } - - protected async deployTokenBridge( - tokenId: Field, - address: PublicKey, - dispatchContractAddress: PublicKey, - dispatchContractPreconditionEnforced = false - ) { - Provable.asProver(() => { - this.assertLazyConfigsInitialized(); - }); - - const { args } = SettlementSmartContractBase; - const BridgeContractClass = args.BridgeContract; - const bridgeContract = new BridgeContractClass(address, tokenId); - - const { - BridgeContractVerificationKey, - signedSettlements, - BridgeContractPermissions, - } = args; - - if ( - signedSettlements === undefined || - BridgeContractPermissions === undefined - ) { - throw new Error( - "Static arguments for SettlementSmartContract not initialized" - ); - } - - if ( - BridgeContractVerificationKey !== undefined && - !BridgeContractVerificationKey.hash.isConstant() - ) { - throw new Error("Bridge contract verification key has to be constants"); - } - - // This function is not a zkapps method, therefore it will be part of this methods execution - // The returning account update (owner.self) is therefore part of this circuit and is assertable - const deploymentAccountUpdate = await bridgeContract.deployProvable( - args.BridgeContractVerificationKey, - args.signedSettlements!, - args.BridgeContractPermissions!, - this.address - ); - - this.approve(deploymentAccountUpdate); - - this.self.body.mayUseToken = { - // Only set this if we deploy a custom token - parentsOwnToken: tokenId.equals(TokenId.default).not(), - inheritFromParent: Bool(false), - }; - - this.emitEvent( - "token-bridge-deployed", - new TokenMapping({ - tokenId: tokenId, - publicKey: address, - }) - ); - - // We can't set a precondition twice, for the $mina bridge deployment that - // would be the case, so we disable it in this case - if (!dispatchContractPreconditionEnforced) { - this.dispatchContractAddressX.requireEquals(dispatchContractAddress.x); - } - - // Set authorization for the auth callback, that we need - this.authorizationField.set( - new TokenBridgeDeploymentAuth({ - target: dispatchContractAddress, - tokenId, - address, - }).hash() - ); - const dispatchContract = - new SettlementSmartContractBase.args.DispatchContract( - dispatchContractAddress - ); - await dispatchContract.enableTokenDeposits(tokenId, address, this.address); - } - - protected async initializeBase( - sequencer: PublicKey, - dispatchContract: PublicKey, - bridgeContract: PublicKey, - contractKey: PrivateKey - ) { - this.sequencerKey.getAndRequireEquals().assertEquals(Field(0)); - this.stateRoot.getAndRequireEquals().assertEquals(Field(0)); - this.blockHashRoot.getAndRequireEquals().assertEquals(Field(0)); - this.networkStateHash.getAndRequireEquals().assertEquals(Field(0)); - this.dispatchContractAddressX.getAndRequireEquals().assertEquals(Field(0)); - - this.sequencerKey.set(sequencer.x); - this.stateRoot.set(LinkedMerkleTree.EMPTY_ROOT); - this.blockHashRoot.set(Field(BlockHashMerkleTree.EMPTY_ROOT)); - this.networkStateHash.set(NetworkState.empty().hash()); - this.dispatchContractAddressX.set(dispatchContract.x); - - const { DispatchContract } = SettlementSmartContractBase.args; - const contractInstance = new DispatchContract(dispatchContract); - await contractInstance.initialize(this.address); - - // Deploy bridge contract for $Mina - await this.deployTokenBridge( - this.tokenId, - bridgeContract, - dispatchContract, - true - ); - - contractKey.toPublicKey().assertEquals(this.address); - } - - protected async settleBase( - blockProof: DynamicBlockProof, - signature: Signature, - dispatchContractAddress: PublicKey, - publicKey: PublicKey, - inputNetworkState: NetworkState, - outputNetworkState: NetworkState, - newPromisedMessagesHash: Field - ) { - // Brought in as a constant - const blockProofVk = - SettlementSmartContractBase.args.ChildVerificationKeyService.getVerificationKey( - "BlockProver" - ); - if (!blockProofVk.hash.isConstant()) { - throw new Error("Sanity check - vk hash has to be constant"); - } - - // Verify the blockproof - blockProof.verify(blockProofVk); - - // Get and assert on-chain values - const stateRoot = this.stateRoot.getAndRequireEquals(); - const networkStateHash = this.networkStateHash.getAndRequireEquals(); - const blockHashRoot = this.blockHashRoot.getAndRequireEquals(); - const sequencerKey = this.sequencerKey.getAndRequireEquals(); - const lastSettlementL1BlockHeight = - this.lastSettlementL1BlockHeight.getAndRequireEquals(); - const onChainDispatchContractAddressX = - this.dispatchContractAddressX.getAndRequireEquals(); - - onChainDispatchContractAddressX.assertEquals( - dispatchContractAddress.x, - "DispatchContract address not provided correctly" - ); - - const { DispatchContract, escapeHatchSlotsInterval, hooks } = - SettlementSmartContractBase.args; - - // Get dispatch contract values - // These values are witnesses but will be checked later on the AU - // call to the dispatch contract via .updateMessagesHash() - const dispatchContract = new DispatchContract(dispatchContractAddress); - const promisedMessagesHash = dispatchContract.promisedMessagesHash.get(); - - // Get block height and use the lower bound for all ops - const minBlockHeightIncluded = this.network.blockchainLength.get(); - this.network.blockchainLength.requireBetween( - minBlockHeightIncluded, - // 5 because that is the length the newPromisedMessagesHash will be valid - minBlockHeightIncluded.add(4) - ); - - // Check signature/escape catch - publicKey.x.assertEquals( - sequencerKey, - "Sequencer public key witness not matching" - ); - const signatureValid = signature.verify(publicKey, [ - BATCH_SIGNATURE_PREFIX, - lastSettlementL1BlockHeight.value, - ]); - const escapeHatchActivated = lastSettlementL1BlockHeight - .add(UInt32.from(escapeHatchSlotsInterval)) - .lessThan(minBlockHeightIncluded); - signatureValid - .or(escapeHatchActivated) - .assertTrue( - "Sequencer signature not valid and escape hatch not activated" - ); - - // Assert correctness of networkState witness - inputNetworkState - .hash() - .assertEquals(networkStateHash, "InputNetworkState witness not valid"); - outputNetworkState - .hash() - .assertEquals( - blockProof.publicOutput.networkStateHash, - "OutputNetworkState witness not valid" - ); - - blockProof.publicOutput.closed.assertEquals( - Bool(true), - "Supplied proof is not a closed BlockProof" - ); - blockProof.publicOutput.pendingSTBatchesHash.assertEquals( - Field(0), - "Supplied proof is has outstanding STs to be proven" - ); - - // Execute onSettlementHooks for additional checks - const stateRecord: SettlementStateRecord = { - blockHashRoot, - stateRoot, - networkStateHash, - lastSettlementL1BlockHeight, - sequencerKey: publicKey, - }; - const inputs: SettlementHookInputs = { - blockProof, - contractState: stateRecord, - newPromisedMessagesHash, - fromNetworkState: inputNetworkState, - toNetworkState: outputNetworkState, - currentL1BlockHeight: minBlockHeightIncluded, - }; - await mapSequential(hooks, async (hook) => { - await hook.beforeSettlement(this, inputs); - }); - - // Apply blockProof - stateRoot.assertEquals( - blockProof.publicInput.stateRoot, - "Input state root not matching" - ); - - networkStateHash.assertEquals( - blockProof.publicInput.networkStateHash, - "Input networkStateHash not matching" - ); - blockHashRoot.assertEquals( - blockProof.publicInput.blockHashRoot, - "Input blockHashRoot not matching" - ); - this.stateRoot.set(blockProof.publicOutput.stateRoot); - this.networkStateHash.set(blockProof.publicOutput.networkStateHash); - this.blockHashRoot.set(blockProof.publicOutput.blockHashRoot); - - // Assert and apply deposit commitments - promisedMessagesHash.assertEquals( - blockProof.publicOutput.incomingMessagesHash, - "Promised messages not honored" - ); - - // Set authorization for the dispatchContract to verify the messages hash update - this.authorizationField.set( - new UpdateMessagesHashAuth({ - target: dispatchContract.address, - executedMessagesHash: promisedMessagesHash, - newPromisedMessagesHash, - }).hash() - ); - - // Call DispatchContract - // This call checks that the promisedMessagesHash, which is already proven - // to be the blockProofs publicoutput, is actually the current on-chain - // promisedMessageHash. It also checks the newPromisedMessagesHash to be - // a current sequencestate value - await dispatchContract.updateMessagesHash( - promisedMessagesHash, - newPromisedMessagesHash - ); - - this.lastSettlementL1BlockHeight.set(minBlockHeightIncluded); - } -} - -export class SettlementSmartContract - extends SettlementSmartContractBase - implements SettlementContractType -{ - @state(Field) public sequencerKey = State(); - @state(UInt32) public lastSettlementL1BlockHeight = State(); - - @state(Field) public stateRoot = State(); - @state(Field) public networkStateHash = State(); - @state(Field) public blockHashRoot = State(); - - @state(Field) public dispatchContractAddressX = State(); - - @state(Field) public authorizationField = State(); - - @method async approveBase(forest: AccountUpdateForest) { - this.checkZeroBalanceChange(forest); - } - - @method - public async initialize( - sequencer: PublicKey, - dispatchContract: PublicKey, - bridgeContract: PublicKey, - contractKey: PrivateKey - ) { - await this.initializeBase( - sequencer, - dispatchContract, - bridgeContract, - contractKey - ); - } - - @method - public async addTokenBridge( - tokenId: Field, - address: PublicKey, - dispatchContract: PublicKey - ) { - await this.deployTokenBridge(tokenId, address, dispatchContract); - } - - @method - public async settle( - blockProof: DynamicBlockProof, - signature: Signature, - dispatchContractAddress: PublicKey, - publicKey: PublicKey, - inputNetworkState: NetworkState, - outputNetworkState: NetworkState, - newPromisedMessagesHash: Field - ) { - return await this.settleBase( - blockProof, - signature, - dispatchContractAddress, - publicKey, - inputNetworkState, - outputNetworkState, - newPromisedMessagesHash - ); - } -} - -/* eslint-enable @typescript-eslint/lines-between-class-members */ diff --git a/packages/protocol/src/settlement/contracts/SettlementSmartContractModule.ts b/packages/protocol/src/settlement/contracts/SettlementSmartContractModule.ts new file mode 100644 index 000000000..c776e5ff5 --- /dev/null +++ b/packages/protocol/src/settlement/contracts/SettlementSmartContractModule.ts @@ -0,0 +1,79 @@ +import { inject, injectable, injectAll } from "tsyringe"; +import { + ArtifactRecord, + ChildVerificationKeyService, + CompileRegistry, + log, +} from "@proto-kit/common"; + +import { BlockProvable } from "../../prover/block/BlockProvable"; +import { + ContractModule, + SmartContractClassFromInterface, +} from "../ContractModule"; +import { ProvableSettlementHook } from "../modularity/ProvableSettlementHook"; +import { ContractArgsRegistry } from "../ContractArgsRegistry"; + +import { + SettlementContractArgs, + SettlementContractType, +} from "./settlement/SettlementBase"; +import { SettlementContract } from "./settlement/SettlementContract"; + +export type SettlementContractConfig = { + escapeHatchSlotsInterval?: number; +}; + +// 24 hours +export const DEFAULT_ESCAPE_HATCH = (60 / 3) * 24; + +@injectable() +export class SettlementSmartContractModule extends ContractModule< + SettlementContractType, + SettlementContractConfig +> { + public constructor( + @injectAll("ProvableSettlementHook") + private readonly hooks: ProvableSettlementHook[], + @inject("BlockProver") + private readonly blockProver: BlockProvable, + private readonly childVerificationKeyService: ChildVerificationKeyService, + private readonly argsRegistry: ContractArgsRegistry + ) { + super(); + } + + public contractFactory(): SmartContractClassFromInterface { + const { hooks, config } = this; + + const escapeHatchSlotsInterval = + config.escapeHatchSlotsInterval ?? DEFAULT_ESCAPE_HATCH; + + this.argsRegistry.addArgs("SettlementContract", { + hooks, + escapeHatchSlotsInterval, + ChildVerificationKeyService: this.childVerificationKeyService, + }); + + return SettlementContract; + } + + public async compile( + registry: CompileRegistry + ): Promise { + // Dependencies + await this.blockProver.compile(registry); + + this.contractFactory(); + + log.debug("Compiling Settlement Contract"); + + const artifact = await registry.forceProverExists( + async (reg) => await registry.compile(SettlementContract) + ); + + return { + SettlementSmartContract: artifact, + }; + } +} diff --git a/packages/protocol/src/settlement/contracts/settlement/BridgingSettlementContract.ts b/packages/protocol/src/settlement/contracts/settlement/BridgingSettlementContract.ts new file mode 100644 index 000000000..4865e4ec5 --- /dev/null +++ b/packages/protocol/src/settlement/contracts/settlement/BridgingSettlementContract.ts @@ -0,0 +1,283 @@ +import { TypedClass, O1PublicKeyOption } from "@proto-kit/common"; +import { + AccountUpdate, + Bool, + Field, + method, + PublicKey, + Signature, + SmartContract, + State, + state, + UInt32, + AccountUpdateForest, + VerificationKey, + Permissions, + Struct, + TokenId, + DeployArgs, +} from "o1js"; +import { container } from "tsyringe"; + +import { NetworkState } from "../../../model/network/NetworkState"; +import { DispatchContractType } from "../DispatchSmartContract"; +import { BridgeContractType } from "../BridgeContract"; +import { TokenBridgeDeploymentAuth } from "../authorizations/TokenBridgeDeploymentAuth"; +import { UpdateMessagesHashAuth } from "../authorizations/UpdateMessagesHashAuth"; +import { + ContractArgsRegistry, + NaiveObjectSchema, + StaticInitializationContract, +} from "../../ContractArgsRegistry"; + +import { + DynamicBlockProof, + SettlementBase, + SettlementContractArgs, + SettlementContractArgsSchema, + SettlementContractType, +} from "./SettlementBase"; + +/* eslint-disable @typescript-eslint/lines-between-class-members */ + +export class TokenMapping extends Struct({ + tokenId: Field, + publicKey: PublicKey, +}) {} + +export interface BridgingSettlementContractType extends SettlementContractType { + authorizationField: State; + + assertStateRoot: (root: Field) => AccountUpdate; + addTokenBridge: (tokenId: Field, address: PublicKey) => Promise; +} + +export interface BridgingSettlementContractArgs extends SettlementContractArgs { + DispatchContract: TypedClass; + BridgeContract: TypedClass & typeof SmartContract; + // Lazily initialized + BridgeContractVerificationKey: VerificationKey | undefined; + BridgeContractPermissions: Permissions; +} + +export const BridgingSettlementContractArgsSchema: NaiveObjectSchema = + { + ...SettlementContractArgsSchema, + DispatchContract: "Required", + BridgeContract: "Required", + BridgeContractVerificationKey: "Optional", + BridgeContractPermissions: "Required", + }; + +export abstract class BridgingSettlementContractBase + extends SettlementBase + implements StaticInitializationContract +{ + public getInitializationArgs(): BridgingSettlementContractArgs { + return container + .resolve(ContractArgsRegistry) + .getArgs("SettlementContract", BridgingSettlementContractArgsSchema); + } + + events = { + "token-bridge-deployed": TokenMapping, + }; + + abstract dispatchContractAddress: State; + + abstract authorizationField: State; + + // Not @state + // abstract offchainStateCommitmentsHash: State; + + public assertStateRoot(root: Field): AccountUpdate { + this.stateRoot.requireEquals(root); + return this.self; + } + + protected async initializeBaseBridging( + sequencer: PublicKey, + dispatchContract: PublicKey + ) { + await super.initializeBase(sequencer); + + this.dispatchContractAddress.set(dispatchContract); + } + + // TODO We should move this to the dispatchcontract eventually - or after mesa + // to the combined settlement & dispatch contract + protected async deployTokenBridge(tokenId: Field, address: PublicKey) { + const { + BridgeContractVerificationKey, + signedSettlements, + BridgeContractPermissions, + BridgeContract: BridgeContractClass, + DispatchContract, + } = this.getInitializationArgs(); + + const bridgeContract = new BridgeContractClass(address, tokenId); + + if ( + BridgeContractVerificationKey !== undefined && + !BridgeContractVerificationKey.hash.isConstant() + ) { + throw new Error("Bridge contract verification key has to be constants"); + } + + // This function is not a zkapps method, therefore it will be part of this methods execution + // The returning account update (owner.self) is therefore part of this circuit and is assertable + const deploymentAccountUpdate = await bridgeContract.deployProvable( + BridgeContractVerificationKey, + signedSettlements!, + BridgeContractPermissions!, + this.address + ); + + this.approve(deploymentAccountUpdate); + + this.self.body.mayUseToken = { + // Only set this if we deploy a custom token + parentsOwnToken: tokenId.equals(TokenId.default).not(), + inheritFromParent: Bool(false), + }; + + this.emitEvent( + "token-bridge-deployed", + new TokenMapping({ + tokenId: tokenId, + publicKey: address, + }) + ); + + const dispatchContractAddress = + this.dispatchContractAddress.getAndRequireEquals(); + + // Set authorization for the auth callback, that we need + this.authorizationField.set( + new TokenBridgeDeploymentAuth({ + target: dispatchContractAddress, + tokenId, + address, + }).hash() + ); + const dispatchContract = new DispatchContract(dispatchContractAddress); + await dispatchContract.enableTokenDeposits(tokenId, address, this.address); + } + + protected async settleBaseBridging( + blockProof: DynamicBlockProof, + signature: Signature, + publicKey: PublicKey, + inputNetworkState: NetworkState, + outputNetworkState: NetworkState, + newPromisedMessagesHash: Field + ) { + await super.settleBase( + blockProof, + signature, + publicKey, + inputNetworkState, + outputNetworkState, + newPromisedMessagesHash + ); + + const dispatchContractAddress = + this.dispatchContractAddress.getAndRequireEquals(); + + const { DispatchContract } = this.getInitializationArgs(); + + // Get dispatch contract values + // These values are witnesses but will be checked later on the AU + // call to the dispatch contract via .updateMessagesHash() + const dispatchContract = new DispatchContract(dispatchContractAddress); + const promisedMessagesHash = dispatchContract.promisedMessagesHash.get(); + + // Assert and apply deposit commitments + promisedMessagesHash.assertEquals( + blockProof.publicOutput.incomingMessagesHash, + "Promised messages not honored" + ); + + // Set authorization for the dispatchContract to verify the messages hash update + this.authorizationField.set( + new UpdateMessagesHashAuth({ + target: dispatchContract.address, + executedMessagesHash: promisedMessagesHash, + newPromisedMessagesHash, + }).hash() + ); + + // Call DispatchContract + // This call checks that the promisedMessagesHash, which is already proven + // to be the blockProofs publicoutput, is actually the current on-chain + // promisedMessageHash. It also checks the newPromisedMessagesHash to be + // a current sequencestate value + await dispatchContract.updateMessagesHash( + promisedMessagesHash, + newPromisedMessagesHash + ); + } +} + +export class BridgingSettlementContract + extends BridgingSettlementContractBase + implements BridgingSettlementContractType +{ + @state(Field) public sequencerKey = State(); + @state(UInt32) public lastSettlementL1BlockHeight = State(); + + @state(Field) public stateRoot = State(); + @state(Field) public networkStateHash = State(); + @state(Field) public blockHashRoot = State(); + + @state(PublicKey) public dispatchContractAddress = State(); + + @state(Field) public authorizationField = State(); + + public async deployAndInitialize( + args: DeployArgs | undefined, + permissions: Permissions, + sequencer: PublicKey, + dispatchContract: O1PublicKeyOption + ): Promise { + dispatchContract.assertSome( + "Bridging-enabled settlement contract requires a dispatch contract address" + ); + + await super.deploy(args); + + this.self.account.permissions.set(permissions); + + await this.initializeBaseBridging(sequencer, dispatchContract.value); + } + + @method async approveBase(forest: AccountUpdateForest) { + this.checkZeroBalanceChange(forest); + } + + @method + public async addTokenBridge(tokenId: Field, address: PublicKey) { + await this.deployTokenBridge(tokenId, address); + } + + @method + public async settle( + blockProof: DynamicBlockProof, + signature: Signature, + publicKey: PublicKey, + inputNetworkState: NetworkState, + outputNetworkState: NetworkState, + newPromisedMessagesHash: Field + ) { + return await this.settleBaseBridging( + blockProof, + signature, + publicKey, + inputNetworkState, + outputNetworkState, + newPromisedMessagesHash + ); + } +} + +/* eslint-enable @typescript-eslint/lines-between-class-members */ diff --git a/packages/protocol/src/settlement/contracts/settlement/SettlementBase.ts b/packages/protocol/src/settlement/contracts/settlement/SettlementBase.ts new file mode 100644 index 000000000..050543b5f --- /dev/null +++ b/packages/protocol/src/settlement/contracts/settlement/SettlementBase.ts @@ -0,0 +1,249 @@ +import { + DeployArgs, + DynamicProof, + Field, + Option, + PublicKey, + Signature, + State, + TokenContract, + UInt32, + Permissions, +} from "o1js"; +import { + ChildVerificationKeyService, + LinkedMerkleTree, + mapSequential, + prefixToField, +} from "@proto-kit/common"; +import { container } from "tsyringe"; + +import { BlockHashMerkleTree } from "../../../prover/block/accummulators/BlockHashMerkleTree"; +import { NetworkState } from "../../../model/network/NetworkState"; +import { + ProvableSettlementHook, + SettlementHookInputs, + SettlementStateRecord, +} from "../../modularity/ProvableSettlementHook"; +import { + BlockProverPublicInput, + BlockProverPublicOutput, +} from "../../../prover/block/BlockProvable"; +import { + ContractArgsRegistry, + NaiveObjectSchema, + StaticInitializationContract, +} from "../../ContractArgsRegistry"; + +/* eslint-disable @typescript-eslint/lines-between-class-members */ + +// Some random prefix for the sequencer signature +export const BATCH_SIGNATURE_PREFIX = prefixToField("pk-batchSignature"); + +export class DynamicBlockProof extends DynamicProof< + BlockProverPublicInput, + BlockProverPublicOutput +> { + public static publicInputType = BlockProverPublicInput; + + public static publicOutputType = BlockProverPublicOutput; + + public static maxProofsVerified = 2 as const; +} + +export interface SettlementContractType { + sequencerKey: State; + lastSettlementL1BlockHeight: State; + stateRoot: State; + networkStateHash: State; + blockHashRoot: State; + + deployAndInitialize: ( + args: DeployArgs | undefined, + permissions: Permissions, + sequencer: PublicKey, + dispatchContract: Option + ) => Promise; + + settle: ( + blockProof: DynamicBlockProof, + signature: Signature, + publicKey: PublicKey, + inputNetworkState: NetworkState, + outputNetworkState: NetworkState, + newPromisedMessagesHash: Field + ) => Promise; +} + +export interface SettlementContractArgs { + hooks: ProvableSettlementHook[]; + escapeHatchSlotsInterval: number; + signedSettlements: boolean; + ChildVerificationKeyService: ChildVerificationKeyService; +} + +export const SettlementContractArgsSchema: NaiveObjectSchema = + { + ChildVerificationKeyService: "Required", + hooks: "Required", + escapeHatchSlotsInterval: "Required", + signedSettlements: "Required", + }; + +export abstract class SettlementBase + extends TokenContract + implements StaticInitializationContract +{ + getInitializationArgs(): SettlementContractArgs { + return container + .resolve(ContractArgsRegistry) + .getArgs("SettlementContract", SettlementContractArgsSchema); + } + + abstract sequencerKey: State; + abstract lastSettlementL1BlockHeight: State; + abstract stateRoot: State; + abstract networkStateHash: State; + abstract blockHashRoot: State; + + protected async initializeBase(sequencer: PublicKey) { + this.sequencerKey.set(sequencer.x); + this.stateRoot.set(LinkedMerkleTree.EMPTY_ROOT); + this.blockHashRoot.set(Field(BlockHashMerkleTree.EMPTY_ROOT)); + this.networkStateHash.set(NetworkState.empty().hash()); + } + + abstract settle( + blockProof: DynamicBlockProof, + signature: Signature, + publicKey: PublicKey, + inputNetworkState: NetworkState, + outputNetworkState: NetworkState, + newPromisedMessagesHash: Field + ): Promise; + + abstract deployAndInitialize( + args: DeployArgs | undefined, + permissions: Permissions, + sequencer: PublicKey, + dispatchContract: Option + ): Promise; + + protected async settleBase( + blockProof: DynamicBlockProof, + signature: Signature, + publicKey: PublicKey, + inputNetworkState: NetworkState, + outputNetworkState: NetworkState, + newPromisedMessagesHash: Field + ) { + const { + escapeHatchSlotsInterval, + hooks, + ChildVerificationKeyService: childVerificationKeyService, + } = this.getInitializationArgs(); + + // Brought in as a constant + const blockProofVk = + childVerificationKeyService.getVerificationKey("BlockProver"); + if (!blockProofVk.hash.isConstant()) { + throw new Error("Sanity check - vk hash has to be constant"); + } + // Verify the blockproof + + blockProof.verify(blockProofVk); + // Get and assert on-chain values + const stateRoot = this.stateRoot.getAndRequireEquals(); + const networkStateHash = this.networkStateHash.getAndRequireEquals(); + const blockHashRoot = this.blockHashRoot.getAndRequireEquals(); + const sequencerKey = this.sequencerKey.getAndRequireEquals(); + + const lastSettlementL1BlockHeight = + this.lastSettlementL1BlockHeight.getAndRequireEquals(); + + // Get block height and use the lower bound for all ops + const minBlockHeightIncluded = this.network.blockchainLength.get(); + this.network.blockchainLength.requireBetween( + minBlockHeightIncluded, + // 5 because that is the length the newPromisedMessagesHash will be valid + minBlockHeightIncluded.add(4) + ); + + // Check signature/escape catch + publicKey.x.assertEquals( + sequencerKey, + "Sequencer public key witness not matching" + ); + const signatureValid = signature.verify(publicKey, [ + BATCH_SIGNATURE_PREFIX, + lastSettlementL1BlockHeight.value, + ]); + const escapeHatchActivated = lastSettlementL1BlockHeight + .add(UInt32.from(escapeHatchSlotsInterval)) + .lessThan(minBlockHeightIncluded); + signatureValid + .or(escapeHatchActivated) + .assertTrue( + "Sequencer signature not valid and escape hatch not activated" + ); + + // Assert correctness of networkState witness + inputNetworkState + .hash() + .assertEquals(networkStateHash, "InputNetworkState witness not valid"); + outputNetworkState + .hash() + .assertEquals( + blockProof.publicOutput.networkStateHash, + "OutputNetworkState witness not valid" + ); + + // Check remainders are zero + blockProof.publicOutput.proverStateRemainder.assertEquals( + Field(0), + "Supplied proof is has outstanding block prover state to be proven" + ); + + // Execute onSettlementHooks for additional checks + const stateRecord: SettlementStateRecord = { + blockHashRoot, + stateRoot, + networkStateHash, + lastSettlementL1BlockHeight, + sequencerKey: publicKey, + }; + const inputs: SettlementHookInputs = { + blockProof, + contractState: stateRecord, + newPromisedMessagesHash, + fromNetworkState: inputNetworkState, + toNetworkState: outputNetworkState, + currentL1BlockHeight: minBlockHeightIncluded, + }; + await mapSequential(hooks, async (hook) => { + await hook.beforeSettlement(this, inputs); + }); + + // Apply blockProof + stateRoot.assertEquals( + blockProof.publicInput.stateRoot, + "Input state root not matching" + ); + + networkStateHash.assertEquals( + blockProof.publicInput.networkStateHash, + "Input networkStateHash not matching" + ); + blockHashRoot.assertEquals( + blockProof.publicInput.blockHashRoot, + "Input blockHashRoot not matching" + ); + this.stateRoot.set(blockProof.publicOutput.stateRoot); + this.networkStateHash.set(blockProof.publicOutput.networkStateHash); + this.blockHashRoot.set(blockProof.publicOutput.blockHashRoot); + + this.lastSettlementL1BlockHeight.set(minBlockHeightIncluded); + } +} + +/* eslint-enable @typescript-eslint/lines-between-class-members */ diff --git a/packages/protocol/src/settlement/contracts/settlement/SettlementContract.ts b/packages/protocol/src/settlement/contracts/settlement/SettlementContract.ts new file mode 100644 index 000000000..95321684f --- /dev/null +++ b/packages/protocol/src/settlement/contracts/settlement/SettlementContract.ts @@ -0,0 +1,75 @@ +import { + State, + UInt32, + AccountUpdateForest, + state, + method, + PublicKey, + Field, + Signature, + DeployArgs, + Permissions, +} from "o1js"; +import { O1PublicKeyOption } from "@proto-kit/common"; + +import { NetworkState } from "../../../model/network/NetworkState"; + +import { + DynamicBlockProof, + SettlementBase, + SettlementContractType, +} from "./SettlementBase"; + +export class SettlementContract + extends SettlementBase + implements SettlementContractType +{ + @state(Field) sequencerKey = State(); + + @state(UInt32) lastSettlementL1BlockHeight = State(); + + @state(Field) stateRoot = State(); + + @state(Field) networkStateHash = State(); + + @state(Field) blockHashRoot = State(); + + public async deployAndInitialize( + args: DeployArgs | undefined, + permissions: Permissions, + sequencer: PublicKey, + dispatchContract: O1PublicKeyOption + ): Promise { + dispatchContract.assertNone( + "Non-bridging settlement contract doesn't require a dispatch contract" + ); + + await super.deploy(args); + + this.self.account.permissions.set(permissions); + + await this.initializeBase(sequencer); + } + + @method async approveBase(forest: AccountUpdateForest) { + this.checkZeroBalanceChange(forest); + } + + @method async settle( + blockProof: DynamicBlockProof, + signature: Signature, + publicKey: PublicKey, + inputNetworkState: NetworkState, + outputNetworkState: NetworkState, + newPromisedMessagesHash: Field + ): Promise { + await super.settleBase( + blockProof, + signature, + publicKey, + inputNetworkState, + outputNetworkState, + newPromisedMessagesHash + ); + } +} diff --git a/packages/protocol/src/settlement/modularity/ProvableSettlementHook.ts b/packages/protocol/src/settlement/modularity/ProvableSettlementHook.ts index ecb117129..67148bfa1 100644 --- a/packages/protocol/src/settlement/modularity/ProvableSettlementHook.ts +++ b/packages/protocol/src/settlement/modularity/ProvableSettlementHook.ts @@ -3,8 +3,8 @@ import { InferProofBase } from "@proto-kit/common"; import { ProtocolModule } from "../../protocol/ProtocolModule"; import { NetworkState } from "../../model/network/NetworkState"; -import type { BlockProof } from "../../prover/block/BlockProver"; -import type { SettlementSmartContractBase } from "../contracts/SettlementSmartContract"; +import type { BlockProof } from "../../prover/block/BlockProvable"; +import type { SettlementContractType } from "../contracts/settlement/SettlementBase"; export type InputBlockProof = InferProofBase; @@ -30,7 +30,7 @@ export abstract class ProvableSettlementHook< Config, > extends ProtocolModule { public abstract beforeSettlement( - smartContract: SettlementSmartContractBase, + smartContract: SettlementContractType, inputs: SettlementHookInputs ): Promise; } diff --git a/packages/protocol/src/settlement/modularity/types.ts b/packages/protocol/src/settlement/modularity/types.ts index 8f7c706e6..b05abdf6f 100644 --- a/packages/protocol/src/settlement/modularity/types.ts +++ b/packages/protocol/src/settlement/modularity/types.ts @@ -1,4 +1,5 @@ import { TypedClass } from "@proto-kit/common"; +import { SmartContract } from "o1js"; import { ContractModule, @@ -11,7 +12,7 @@ export type InferContractType< > = Module extends TypedClass ? ConcreteModule extends ContractModule - ? Contract + ? Contract & SmartContract : never : never; diff --git a/packages/protocol/src/settlement/modules/NetworkStateSettlementModule.ts b/packages/protocol/src/settlement/modules/NetworkStateSettlementModule.ts index 739ccc27a..169492175 100644 --- a/packages/protocol/src/settlement/modules/NetworkStateSettlementModule.ts +++ b/packages/protocol/src/settlement/modules/NetworkStateSettlementModule.ts @@ -4,7 +4,7 @@ import { ProvableSettlementHook, SettlementHookInputs, } from "../modularity/ProvableSettlementHook"; -import { SettlementSmartContract } from "../contracts/SettlementSmartContract"; +import { SettlementContractType } from "../contracts/settlement/SettlementBase"; type NetworkStateSettlementModuleConfig = { blocksPerL1Block: UInt64; @@ -13,7 +13,7 @@ type NetworkStateSettlementModuleConfig = { /* eslint-disable @typescript-eslint/no-unused-vars */ export class NetworkStateSettlementModule extends ProvableSettlementHook { public async beforeSettlement( - smartContract: SettlementSmartContract, + smartContract: SettlementContractType, { blockProof, fromNetworkState, diff --git a/packages/protocol/src/utils/MinaPrefixedProvableHashList.ts b/packages/protocol/src/utils/MinaPrefixedProvableHashList.ts index eaa2ff8f8..bfa6cb98e 100644 --- a/packages/protocol/src/utils/MinaPrefixedProvableHashList.ts +++ b/packages/protocol/src/utils/MinaPrefixedProvableHashList.ts @@ -47,11 +47,15 @@ export class MinaPrefixedProvableHashList< public constructor( valueType: ProvablePure, public readonly prefix: string, - internalCommitment: Field = Field(0) + internalCommitment?: Field ) { super(valueType, internalCommitment); } + public empty(): Field { + return Field(0); + } + protected hash(elements: Field[]): Field { const init = salt(this.prefix); const digest = Poseidon.update(init, elements); @@ -60,7 +64,7 @@ export class MinaPrefixedProvableHashList< } export class MinaActionsHashList extends MinaPrefixedProvableHashList { - public constructor(internalCommitment: Field = Field(0)) { + public constructor(internalCommitment?: Field) { super(Field, MINA_PREFIXES.sequenceEvents, internalCommitment); } } diff --git a/packages/protocol/src/utils/PrefixedProvableHashList.ts b/packages/protocol/src/utils/PrefixedProvableHashList.ts index a31b78eea..4e9d4a755 100644 --- a/packages/protocol/src/utils/PrefixedProvableHashList.ts +++ b/packages/protocol/src/utils/PrefixedProvableHashList.ts @@ -15,6 +15,10 @@ export class PrefixedProvableHashList extends ProvableHashList { this.prefix = stringToField(prefix); } + public empty(): Field { + return Field(0); + } + protected hash(elements: Field[]): Field { return Poseidon.hash([this.prefix, ...elements]); } diff --git a/packages/protocol/src/utils/ProvableHashList.ts b/packages/protocol/src/utils/ProvableHashList.ts index 9b824d60a..da96f7a2a 100644 --- a/packages/protocol/src/utils/ProvableHashList.ts +++ b/packages/protocol/src/utils/ProvableHashList.ts @@ -23,23 +23,34 @@ export type VerifiedTransition = { * Utilities for creating a hash list from a given value type. */ export abstract class ProvableHashList { + public commitment: Field; + public constructor( protected readonly valueType: ProvablePure, - public commitment: Field = Field(0), + commitment?: Field | undefined, private unconstrainedList: Unconstrained< ProvableHashListData[] > = Unconstrained.from([]) - ) {} + ) { + this.commitment = commitment ?? this.empty(); + } + + protected abstract empty(): Field; protected abstract hash(elements: Field[]): Field; - private pushUnconstrained(preimage: Field, value: Value) { - const valueConstant = this.valueType.fromFields( - this.valueType.toFields(value).map((field) => field.toConstant()) - ); - this.unconstrainedList.get().push({ - preimage: preimage.toConstant(), - value: valueConstant, + private pushUnconstrained(preimage: Field, value: Value, condition?: Bool) { + this.unconstrainedList.updateAsProver((array) => { + if (condition !== undefined && !condition.toBoolean()) { + return array; + } + return [ + ...array, + { + preimage: preimage.toConstant(), + value: Provable.toConstant(this.valueType, value), + }, + ]; }); } @@ -61,11 +72,28 @@ export abstract class ProvableHashList { this.commitment = to; } + public fastForwardIf( + transition: VerifiedTransition, + condition: Bool, + message: string = "some hashlist" + ) { + const { from, to } = transition; + + // Equal to condition -> (from == this.commitment) + from + .mul(condition.toField()) + .assertEquals( + this.commitment.mul(condition.toField()), + `From-commitment for ${message} not matching` + ); + + this.commitment = Provable.if(condition, to, this.commitment); + } + public witnessTip(preimage: Field, value: Value): Bool { - return this.hash([ - this.commitment, - ...this.valueType.toFields(value), - ]).equals(this.commitment); + return this.hash([preimage, ...this.valueType.toFields(value)]).equals( + this.commitment + ); } /** @@ -76,9 +104,7 @@ export abstract class ProvableHashList { * @returns Current hash list. */ public push(value: Value) { - Provable.asProver(() => { - this.pushUnconstrained(this.commitment, value); - }); + this.pushUnconstrained(this.commitment, value); this.commitment = this.hash([ this.commitment, @@ -89,11 +115,7 @@ export abstract class ProvableHashList { } public pushIf(value: Value, condition: Bool) { - Provable.asProver(() => { - if (condition.toBoolean()) { - this.pushUnconstrained(this.commitment, value); - } - }); + this.pushUnconstrained(this.commitment, value, condition); const newCommitment = this.hash([ this.commitment, @@ -111,6 +133,10 @@ export abstract class ProvableHashList { return this.commitment; } + public isEmpty(): Bool { + return this.commitment.equals(this.empty()); + } + public getUnconstrainedValues(): Unconstrained< ProvableHashListData[] > { @@ -122,4 +148,8 @@ export class DefaultProvableHashList extends ProvableHashList { public hash(elements: Field[]): Field { return Poseidon.hash(elements); } + + public empty(): Field { + return Field(0); + } } diff --git a/packages/protocol/test/BlockProver.test.ts b/packages/protocol/test/BlockProver.test.ts index 44b49dc6b..f06ac25a1 100644 --- a/packages/protocol/test/BlockProver.test.ts +++ b/packages/protocol/test/BlockProver.test.ts @@ -10,7 +10,7 @@ import "reflect-metadata"; import { MethodPublicOutput, NetworkState, - SignedTransaction, + AuthorizedTransaction, StateTransitionProverPublicInput, StateTransitionProverPublicOutput, } from "../src"; diff --git a/packages/protocol/test/TestingProtocol.ts b/packages/protocol/test/TestingProtocol.ts index 6370201e8..9e8d85fa2 100644 --- a/packages/protocol/test/TestingProtocol.ts +++ b/packages/protocol/test/TestingProtocol.ts @@ -3,33 +3,13 @@ import { Runtime } from "@proto-kit/module"; import { Balance } from "@proto-kit/sequencer/test/integration/mocks/Balance"; import { NoopRuntime } from "@proto-kit/sequencer/test/integration/mocks/NoopRuntime"; -import { - AccountStateHook, - BlockHeightHook, - BlockProver, - LastStateRootBlockHook, - Protocol, - StateServiceProvider, - StateTransitionProver, -} from "../src"; +import { Protocol, StateServiceProvider } from "../src"; export function createAndInitTestingProtocol() { - const ProtocolClass = Protocol.from({ - StateTransitionProver: StateTransitionProver, - BlockProver: BlockProver, - AccountState: AccountStateHook, - BlockHeight: BlockHeightHook, - LastStateRoot: LastStateRootBlockHook, - }); + const ProtocolClass = Protocol.from(Protocol.defaultModules()); const protocol = new ProtocolClass(); - protocol.configure({ - BlockProver: {}, - AccountState: {}, - BlockHeight: {}, - StateTransitionProver: {}, - LastStateRoot: {}, - }); + protocol.configure(Protocol.defaultConfig()); const appChain = container.createChildContainer(); diff --git a/packages/sdk/src/client/ClientAppChain.ts b/packages/sdk/src/client/ClientAppChain.ts index bc1b1c63d..e46414d1b 100644 --- a/packages/sdk/src/client/ClientAppChain.ts +++ b/packages/sdk/src/client/ClientAppChain.ts @@ -19,18 +19,12 @@ import { } from "@proto-kit/protocol"; import { DummyStateService, - NetworkStateQuery, - NetworkStateTransportModule, Query, - QueryBuilderFactory, - QueryTransportModule, Sequencer, UnsignedTransaction, AppChain, AppChainModule, MinimalAppChainDefinition, - BlockExplorerQuery, - BlockExplorerTransportModule, } from "@proto-kit/sequencer"; import { container } from "tsyringe"; import { Field, PublicKey, UInt64 } from "o1js"; @@ -42,6 +36,7 @@ import { GraphqlTransactionSender } from "../graphql/GraphqlTransactionSender"; import { Signer } from "../transaction/InMemorySigner"; import { AppChainTransaction } from "../transaction/AppChainTransaction"; import { TransactionSender } from "../transaction/InMemoryTransactionSender"; +import { QueryService } from "../query/QueryService"; export type InferModules>> = Container extends TypedClass @@ -192,49 +187,15 @@ export class ClientAppChain< return transaction; } - public get query(): { - runtime: Query< - RuntimeModule, - InferModules - >; - protocol: Query< - ProtocolModule, - InferModules - >; - network: NetworkStateQuery; - explorer: BlockExplorerQuery; - } { - const queryTransportModule = this.container.resolve( - "QueryTransportModule" - ); - - const networkStateTransportModule = - this.container.resolve( - "NetworkStateTransportModule" - ); - - const blockExplorerTransportModule = - this.container.resolve( - "BlockExplorerTransportModule" - ); - - const network = new NetworkStateQuery(networkStateTransportModule); - const explorer = new BlockExplorerQuery(blockExplorerTransportModule); - - return { - runtime: QueryBuilderFactory.fromRuntime( - this.runtime, - queryTransportModule - ), - - protocol: QueryBuilderFactory.fromProtocol( - this.protocol, - queryTransportModule - ), - - network, - - explorer, - }; + public get query(): QueryService< + InferModules, + InferModules + > { + return this.container.resolve< + QueryService< + InferModules, + InferModules + > + >(QueryService); } } diff --git a/packages/sdk/src/query/QueryService.ts b/packages/sdk/src/query/QueryService.ts new file mode 100644 index 000000000..e49f750fc --- /dev/null +++ b/packages/sdk/src/query/QueryService.ts @@ -0,0 +1,105 @@ +import { inject, injectable, Lifecycle, scoped } from "tsyringe"; +import { + Runtime, + RuntimeModule, + RuntimeModulesRecord, +} from "@proto-kit/module"; +import { + MandatoryProtocolModulesRecord, + Protocol, + ProtocolModule, + ProtocolModulesRecord, +} from "@proto-kit/protocol"; +import { + BlockExplorerQuery, + BlockExplorerTransportModule, + NetworkStateQuery, + NetworkStateTransportModule, + Query, + QueryBuilderFactory, + QueryTransportModule, +} from "@proto-kit/sequencer"; + +@scoped(Lifecycle.ContainerScoped) +@injectable() +export class QueryService< + RuntimeModules extends RuntimeModulesRecord, + ProtocolModules extends ProtocolModulesRecord & + MandatoryProtocolModulesRecord, +> { + // Lazily initialized query instances + private RuntimeQuery?: Query, RuntimeModules>; + + private ProtocolQuery?: Query, ProtocolModules>; + + private NetworkQuery?: NetworkStateQuery; + + private ExplorerQuery?: BlockExplorerQuery; + + public constructor( + @inject("Runtime") + private readonly runtimeInstance: Runtime, + @inject("Protocol") + private readonly protocolInstance: Protocol, + @inject("QueryTransportModule", { isOptional: true }) + private readonly queryTransport: QueryTransportModule, + @inject("NetworkStateTransportModule", { isOptional: true }) + private readonly networkStateTransport: NetworkStateTransportModule, + @inject("BlockExplorerTransportModule", { isOptional: true }) + private readonly blockExplorerTransport: BlockExplorerTransportModule + ) {} + + /** + * Getter of query module for runtime modules. + * If not initialized before, it is initialized. + * @returns A {@link Query} module for runtime module. + */ + public get runtime(): Query, RuntimeModules> { + if (this.RuntimeQuery === undefined) { + this.RuntimeQuery = QueryBuilderFactory.fromRuntime( + this.runtimeInstance, + this.queryTransport + ); + } + return this.RuntimeQuery; + } + + /** + * Getter of query module for protocol. + * If not initialized before, it is initialized. + * @returns A {@link Query} module for protocol module. + */ + public get protocol(): Query, ProtocolModules> { + if (this.ProtocolQuery === undefined) { + this.ProtocolQuery = QueryBuilderFactory.fromProtocol( + this.protocolInstance, + this.queryTransport + ); + } + return this.ProtocolQuery; + } + + /** + * Getter of network state query module. + * If not initialized before, it is initialized. + * @returns A {@link NetworkStateQuery} module. + */ + public get network(): NetworkStateQuery { + if (this.NetworkQuery === undefined) { + this.NetworkQuery = new NetworkStateQuery(this.networkStateTransport); + } + return this.NetworkQuery; + } + + /** + * Getter of block explorer query module. + * If not initialized before, it is initialized. + * @returns A {@link BlockExplorerQuery} module. + */ + public get explorer(): BlockExplorerQuery { + if (this.ExplorerQuery === undefined) { + this.ExplorerQuery = new BlockExplorerQuery(this.blockExplorerTransport); + } + return this.ExplorerQuery; + } +} diff --git a/packages/sdk/src/testing/TestingAppChain.ts b/packages/sdk/src/testing/TestingAppChain.ts index 63e703715..3487a4175 100644 --- a/packages/sdk/src/testing/TestingAppChain.ts +++ b/packages/sdk/src/testing/TestingAppChain.ts @@ -53,11 +53,7 @@ export class TestingAppChain< appChain.configurePartial({ Protocol: { - AccountState: {}, - BlockProver: {}, - StateTransitionProver: {}, - BlockHeight: {}, - LastStateRoot: {}, + ...Protocol.defaultConfig(), TransactionFee: { tokenId: 0n, feeRecipient: randomFeeRecipient, diff --git a/packages/sdk/src/transaction/AuroSigner.ts b/packages/sdk/src/transaction/AuroSigner.ts index 14c999aec..07b4a405a 100644 --- a/packages/sdk/src/transaction/AuroSigner.ts +++ b/packages/sdk/src/transaction/AuroSigner.ts @@ -4,6 +4,7 @@ import { AppChainModule } from "@proto-kit/sequencer"; import { Signer } from "./InMemorySigner"; +// Will be implemented for MinaSigner here. @injectable() export class AuroSigner extends AppChainModule implements Signer { public async sign(message: Field[]): Promise { diff --git a/packages/sdk/test/blockProof/blockProof.test.ts b/packages/sdk/test/blockProof/blockProof.test.ts index f2c81ad29..1ac9e0376 100644 --- a/packages/sdk/test/blockProof/blockProof.test.ts +++ b/packages/sdk/test/blockProof/blockProof.test.ts @@ -4,8 +4,9 @@ import { RollupMerkleTree, mapSequential, } from "@proto-kit/common"; -import { Field, PrivateKey, UInt64 as O1UInt64, Signature } from "o1js"; +import { Field, PrivateKey, UInt64 as O1UInt64, Signature, Bool } from "o1js"; import { + AuthorizedTransaction, BlockProverPublicOutput, NetworkState, ProvableTransactionHook, @@ -89,19 +90,21 @@ describe.skip("blockProof", () => { await mapSequential(txHooks, async (hook) => { await hook.beforeTransaction({ - transaction: RuntimeTransaction.fromTransaction({ - sender: alice, - nonce: O1UInt64.from(0), - methodId: Field(balancesMethodId), - argsHash: Field(0), + transaction: new AuthorizedTransaction({ + transaction: RuntimeTransaction.fromTransaction({ + sender: alice, + nonce: O1UInt64.from(0), + methodId: Field(balancesMethodId), + argsHash: Field(0), + }), + signature: Signature.create(PrivateKey.random(), [Field(0)]), + isMessage: Bool(false), }), networkState: NetworkState.empty(), - signature: Signature.create(PrivateKey.random(), [Field(0)]), prover: { incomingMessagesHash: Field(0), transactionsHash: Field(0), eternalTransactionsHash: Field(0), - blockHashRoot: Field(0), }, }); }); diff --git a/packages/sdk/test/fees-multi-zkprograms.test.ts b/packages/sdk/test/fees-multi-zkprograms.test.ts index 53d4389f9..e6921b708 100644 --- a/packages/sdk/test/fees-multi-zkprograms.test.ts +++ b/packages/sdk/test/fees-multi-zkprograms.test.ts @@ -185,9 +185,7 @@ describe("check fee analyzer", () => { }, }, Sequencer: { - Mempool: { - validationEnabled: true, - }, + Mempool: {}, }, }); diff --git a/packages/sdk/test/modularization.test.ts b/packages/sdk/test/modularization.test.ts index 307082a71..f0ee7a8bb 100644 --- a/packages/sdk/test/modularization.test.ts +++ b/packages/sdk/test/modularization.test.ts @@ -67,11 +67,7 @@ describe("modularization", () => { TestRuntimeModule: {}, }, Protocol: { - AccountState: {}, - BlockProver: {}, - StateTransitionProver: {}, - BlockHeight: {}, - LastStateRoot: {}, + ...Protocol.defaultConfig(), TransactionFee: { tokenId: 0n, feeRecipient: PrivateKey.random().toPublicKey().toBase58(), diff --git a/packages/sequencer/src/appChain/AppChain.ts b/packages/sequencer/src/appChain/AppChain.ts index cb2369cf9..0036b4bf2 100644 --- a/packages/sequencer/src/appChain/AppChain.ts +++ b/packages/sequencer/src/appChain/AppChain.ts @@ -61,7 +61,7 @@ export class AppChain< */ public async start( proofsEnabled: boolean = false, - dependencyContainer: DependencyContainer = container + dependencyContainer: DependencyContainer = container.createChildContainer() ) { this.create(() => dependencyContainer); diff --git a/packages/sequencer/src/helpers/BusyGuard.ts b/packages/sequencer/src/helpers/BusyGuard.ts new file mode 100644 index 000000000..f38dc77a1 --- /dev/null +++ b/packages/sequencer/src/helpers/BusyGuard.ts @@ -0,0 +1,30 @@ +import { log } from "@proto-kit/common"; +/** + * Decorator that ensures a function/method is not currently in use. + * Mostly useful for production of blocks, batches and tasks. + */ +export function ensureNotBusy() { + let inProgress = false; + + return function innerFunction( + _target: T, + methodName: string, + descriptor: TypedPropertyDescriptor<(...args: any[]) => Promise> + ): void { + const originalMethod = descriptor.value!; + + descriptor.value = async function wrapped(this: T, ...args: unknown[]) { + if (inProgress) { + log.trace(`${methodName} is in use at the moment.`); + return undefined; + } + + inProgress = true; + try { + return await originalMethod.apply(this, args); + } finally { + inProgress = false; + } + }; + }; +} diff --git a/packages/sequencer/src/helpers/CircuitAnalysisModule.ts b/packages/sequencer/src/helpers/CircuitAnalysisModule.ts new file mode 100644 index 000000000..c9b33572f --- /dev/null +++ b/packages/sequencer/src/helpers/CircuitAnalysisModule.ts @@ -0,0 +1,75 @@ +import { inject, injectable } from "tsyringe"; +import { RuntimeEnvironment } from "@proto-kit/module"; +import { log, mapSequential, PlainZkProgram } from "@proto-kit/common"; +import { + MandatoryProtocolModulesRecord, + Protocol, + ProtocolModulesRecord, + SettlementContractModule, + SettlementModulesRecord, +} from "@proto-kit/protocol"; + +@injectable() +export class CircuitAnalysisModule { + public constructor( + @inject("Protocol") + private readonly protocol: Protocol< + ProtocolModulesRecord & MandatoryProtocolModulesRecord + >, + @inject("Runtime") + private readonly runtime: RuntimeEnvironment + ) {} + + public async printSummary() { + const summary = await this.analyseMethods(); + log.info(summary); + } + + public async analyseMethods() { + const zkProgrammables = [ + this.runtime, + this.protocol.stateTransitionProver, + this.protocol.transactionProver, + this.protocol.blockProver, + ]; + + const zkProgrammablePromises = await mapSequential( + zkProgrammables, + (withZkProgrammable) => + mapSequential( + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + withZkProgrammable.zkProgrammable.zkProgramFactory() as PlainZkProgram< + unknown, + unknown + >[], + async (program) => { + const result = await program.analyzeMethods(); + return [program.name, result] as const; + } + ) + ); + + const settlementModule = this.protocol.dependencyContainer.resolve< + SettlementContractModule + >("SettlementContractModule"); + + const contractPromises = await mapSequential( + Object.entries(settlementModule.getContractClasses()), + async ([key, clas]) => { + const result = await clas.analyzeMethods(); + return [key, result] as const; + } + ); + + const allResults = [...zkProgrammablePromises.flat(), ...contractPromises]; + + const summary = allResults.map(([program, result]) => { + const methods = Object.entries(result).map( + ([methodName, constraints]) => [methodName, constraints.rows] as const + ); + return [program, Object.fromEntries(methods)] as const; + }); + + return Object.fromEntries(summary); + } +} diff --git a/packages/sequencer/src/index.ts b/packages/sequencer/src/index.ts index 01314dee8..0257b41d3 100644 --- a/packages/sequencer/src/index.ts +++ b/packages/sequencer/src/index.ts @@ -1,8 +1,11 @@ export * from "./helpers/utils"; +export * from "./helpers/BusyGuard"; export * from "./mempool/Mempool"; export * from "./mempool/PendingTransaction"; export * from "./mempool/CompressedSignature"; export * from "./mempool/private/PrivateMempool"; +export * from "./mempool/sorting/MempoolSorting"; +export * from "./mempool/sorting/DefaultMempoolSorting"; export * from "./sequencer/executor/Sequencer"; export * from "./sequencer/executor/Sequenceable"; export * from "./sequencer/SequencerIdProvider"; @@ -87,6 +90,7 @@ export * from "./helpers/query/NetworkStateQuery"; export * from "./helpers/query/NetworkStateTransportModule"; export * from "./helpers/query/BlockExplorerQuery"; export * from "./helpers/query/BlockExplorerTransportModule"; +export * from "./helpers/CircuitAnalysisModule"; export * from "./state/prefilled/PreFilledStateService"; export * from "./state/async/AsyncMerkleTreeStore"; export * from "./state/async/AsyncStateService"; @@ -97,6 +101,7 @@ export * from "./state/state/DummyStateService"; export * from "./state/state/CachedStateService"; export * from "./state/lmt/AsyncLinkedMerkleTreeDatabase"; export * from "./state/lmt/CachedLinkedLeafStore"; +export * from "./settlement/MinaSigner"; export * from "./settlement/SettlementModule"; export * from "./settlement/BridgingModule"; export * from "./settlement/messages/outgoing/DefaultOutgoingMessageAdapter"; diff --git a/packages/sequencer/src/mempool/Mempool.ts b/packages/sequencer/src/mempool/Mempool.ts index 97401a973..9aca0be96 100644 --- a/packages/sequencer/src/mempool/Mempool.ts +++ b/packages/sequencer/src/mempool/Mempool.ts @@ -17,7 +17,9 @@ export interface Mempool /** * Retrieve all transactions that are currently in the mempool */ - getTxs: (limit?: number) => Promise; + getTxs: (offset: number, limit?: number) => Promise; + + getMandatoryTxs: () => Promise; removeTxs: (included: string[], dropped: string[]) => Promise; } diff --git a/packages/sequencer/src/mempool/PendingTransaction.ts b/packages/sequencer/src/mempool/PendingTransaction.ts index fd1fedfec..3b81066e4 100644 --- a/packages/sequencer/src/mempool/PendingTransaction.ts +++ b/packages/sequencer/src/mempool/PendingTransaction.ts @@ -10,7 +10,7 @@ import { import { PublicKeyOption, RuntimeTransaction, - SignedTransaction, + AuthorizedTransaction, UInt64Option, } from "@proto-kit/protocol"; @@ -82,7 +82,7 @@ export class UnsignedTransaction implements UnsignedTransactionBody { } public getSignatureData(): Field[] { - return SignedTransaction.getSignatureData({ + return AuthorizedTransaction.getSignatureData({ nonce: this.nonce, methodId: this.methodId, argsHash: this.argsHash(), @@ -186,10 +186,11 @@ export class PendingTransaction extends UnsignedTransaction { }; } - public toProtocolTransaction(): SignedTransaction { - return new SignedTransaction({ + public toProtocolTransaction(): AuthorizedTransaction { + return new AuthorizedTransaction({ transaction: this.toRuntimeTransaction(), signature: this.signature, + isMessage: Bool(this.isMessage), }); } } diff --git a/packages/sequencer/src/mempool/private/PrivateMempool.ts b/packages/sequencer/src/mempool/private/PrivateMempool.ts index e750060ea..c2e3c4e14 100644 --- a/packages/sequencer/src/mempool/private/PrivateMempool.ts +++ b/packages/sequencer/src/mempool/private/PrivateMempool.ts @@ -1,22 +1,5 @@ -import { - EventEmitter, - log, - noop, - ModuleContainerLike, -} from "@proto-kit/common"; -import { container, inject } from "tsyringe"; -import { - AccountStateHook, - BlockHashMerkleTree, - MandatoryProtocolModulesRecord, - NetworkState, - Protocol, - ProvableHookBlockState, - RuntimeMethodExecutionContext, - RuntimeMethodExecutionData, - StateServiceProvider, -} from "@proto-kit/protocol"; -import { Field } from "o1js"; +import { EventEmitter, log, noop } from "@proto-kit/common"; +import { inject } from "tsyringe"; import type { Mempool, MempoolEvents } from "../Mempool"; import type { PendingTransaction } from "../PendingTransaction"; @@ -26,22 +9,16 @@ import { } from "../../sequencer/builder/SequencerModule"; import { TransactionStorage } from "../../storage/repositories/TransactionStorage"; import { TransactionValidator } from "../verification/TransactionValidator"; -import { BlockStorage } from "../../storage/repositories/BlockStorage"; -import { CachedStateService } from "../../state/state/CachedStateService"; -import { AsyncStateService } from "../../state/async/AsyncStateService"; -import { distinctByPredicate } from "../../helpers/utils"; import { Tracer } from "../../logging/Tracer"; import { trace } from "../../logging/trace"; +import { IncomingMessagesService } from "../../settlement/messages/IncomingMessagesService"; +import { MempoolSorting } from "../sorting/MempoolSorting"; +import { DefaultMempoolSorting } from "../sorting/DefaultMempoolSorting"; -type MempoolTransactionPaths = { - transaction: PendingTransaction; - paths: Field[]; +type PrivateMempoolConfig = { + type?: "hybrid" | "private" | "based"; }; -interface PrivateMempoolConfig { - validationEnabled?: boolean; -} - @sequencerModule() export class PrivateMempool extends SequencerModule @@ -49,34 +26,40 @@ export class PrivateMempool { public readonly events = new EventEmitter(); - private readonly accountStateHook: AccountStateHook; + private readonly mempoolSorting: MempoolSorting; public constructor( private readonly transactionValidator: TransactionValidator, @inject("TransactionStorage") private readonly transactionStorage: TransactionStorage, - @inject("Protocol") - private readonly protocol: Protocol, - @inject("Sequencer") - private readonly sequencer: ModuleContainerLike, - @inject("UnprovenStateService") - private readonly stateService: AsyncStateService, - @inject("Tracer") public readonly tracer: Tracer + @inject("IncomingMessagesService", { isOptional: true }) + private readonly messageService: IncomingMessagesService | undefined, + @inject("Tracer") public readonly tracer: Tracer, + @inject("MempoolSorting", { isOptional: true }) + mempoolSorting: MempoolSorting | undefined ) { super(); - this.accountStateHook = - this.protocol.dependencyContainer.resolve("AccountState"); + this.mempoolSorting = mempoolSorting ?? new DefaultMempoolSorting(); + } + + private type() { + return this.config.type ?? "hybrid"; } public async length(): Promise { - const txs = await this.transactionStorage.getPendingUserTransactions(); + const txs = await this.transactionStorage.getPendingUserTransactions(0); return txs.length; } public async add(tx: PendingTransaction): Promise { const [txValid, error] = this.transactionValidator.validateTx(tx); if (txValid) { - const success = await this.transactionStorage.pushUserTransaction(tx); + const sortingValue = this.mempoolSorting!.presortingPriority(tx); + + const success = await this.transactionStorage.pushUserTransaction( + tx, + sortingValue + ); if (success) { this.events.emit("mempool-transaction-added", tx); log.trace(`Transaction added to mempool: ${tx.hash().toString()}`); @@ -99,169 +82,38 @@ export class PrivateMempool ); } - private get unprovenQueue(): BlockStorage { - return this.sequencer.dependencyContainer.resolve( - "BlockStorage" - ); - } - - public async getStagedNetworkState(): Promise { - const result = await this.unprovenQueue.getLatestBlock(); - return result?.result.afterNetworkState; - } - public async removeTxs(included: string[], dropped: string[]) { await this.transactionStorage.removeTx(included, "included"); await this.transactionStorage.removeTx(dropped, "dropped"); } @trace("mempool.get_txs") - public async getTxs(limit?: number): Promise { - // TODO Add limit to the storage (or do something smarter entirely) - const txs = await this.transactionStorage.getPendingUserTransactions(); - - const baseCachedStateService = new CachedStateService(this.stateService); - - const networkState = - (await this.getStagedNetworkState()) ?? NetworkState.empty(); - - const validationEnabled = this.config.validationEnabled ?? false; - const sortedTxs = validationEnabled - ? await this.checkTxValid( - txs, - baseCachedStateService, - this.protocol.stateServiceProvider, - networkState, - limit - ) - : txs.slice(0, limit); - - this.protocol.stateServiceProvider.popCurrentStateService(); - return sortedTxs; - } - - // We iterate through the transactions. For each tx we run the account state hook. - // If the txs succeeds then it can be returned. If it fails then we keep track of it - // in the skipped txs list and when later txs succeed we check to see if any state transition - // paths are shared between the just succeeded tx and any of the skipped txs. This is - // because a failed tx may succeed now if the failure was to do with a nonce issue, say. - // TODO Refactor - @trace("mempool.validate_txs") - // eslint-disable-next-line sonarjs/cognitive-complexity - private async checkTxValid( - transactions: PendingTransaction[], - baseService: CachedStateService, - stateServiceProvider: StateServiceProvider, - networkState: NetworkState, + public async getTxs( + offset?: number, limit?: number - ) { - const executionContext = container.resolve( - RuntimeMethodExecutionContext - ); - executionContext.clear(); - - // Initialize starting state - const sortedTransactions: PendingTransaction[] = []; - const skippedTransactions: Record = {}; - - let queue: PendingTransaction[] = [...transactions]; - - const previousBlock = await this.unprovenQueue.getLatestBlock(); - - // TODO This is not sound currently as the prover state changes all the time - // in the actual blockprover. We need to properly simulate that - const proverState: ProvableHookBlockState = { - blockHashRoot: Field( - previousBlock?.result.blockHashRoot ?? BlockHashMerkleTree.EMPTY_ROOT - ), - eternalTransactionsHash: - previousBlock?.block.toEternalTransactionsHash ?? Field(0), - transactionsHash: previousBlock?.block.transactionsHash ?? Field(0), - incomingMessagesHash: previousBlock?.block.toMessagesHash ?? Field(0), - }; - - while ( - queue.length > 0 && - sortedTransactions.length < (limit ?? Number.MAX_VALUE) - ) { - const [tx] = queue.splice(0, 1); - const txStateService = new CachedStateService(baseService); - stateServiceProvider.setCurrentStateService(txStateService); - const contextInputs: RuntimeMethodExecutionData = { - networkState: networkState, - transaction: tx.toProtocolTransaction().transaction, - }; - executionContext.setup(contextInputs); - - const signedTransaction = tx.toProtocolTransaction(); + ): Promise { + if (this.type() === "based") { + return []; + } - // eslint-disable-next-line no-await-in-loop - await this.accountStateHook.beforeTransaction({ - networkState: networkState, - transaction: signedTransaction.transaction, - signature: signedTransaction.signature, - prover: proverState, - }); - const { status, statusMessage, stateTransitions } = - executionContext.current().result; + let txs = await this.transactionStorage.getPendingUserTransactions( + offset ?? 0, + limit + ); - if (status.toBoolean()) { - log.trace(`Accepted tx ${tx.hash().toString()}`); - sortedTransactions.push(tx); - // eslint-disable-next-line no-await-in-loop - await txStateService.applyStateTransitions(stateTransitions); - // eslint-disable-next-line no-await-in-loop - await txStateService.mergeIntoParent(); - delete skippedTransactions[tx.hash().toString()]; - if (Object.entries(skippedTransactions).length > 0) { - // eslint-disable-next-line @typescript-eslint/no-loop-func - stateTransitions.forEach((st) => { - Object.values(skippedTransactions).forEach((value) => { - if (value.paths.some((x) => x.equals(st.path))) { - queue.push(value.transaction); - } - }); - }); - queue = queue.filter(distinctByPredicate((a, b) => a === b)); - } - } else { - // eslint-disable-next-line no-await-in-loop - const removeTxWhen = await this.accountStateHook.removeTransactionWhen({ - networkState: networkState, - transaction: signedTransaction.transaction, - signature: signedTransaction.signature, - prover: proverState, - }); - if (removeTxWhen) { - // eslint-disable-next-line no-await-in-loop - await this.transactionStorage.removeTx( - [tx.hash().toString()], - "dropped" - ); - log.trace( - `Deleting tx ${tx.hash().toString()} from mempool because removeTransactionWhen condition is satisfied` - ); - // eslint-disable-next-line no-continue - continue; - } + if (this.mempoolSorting.enablePostSorting()) { + txs = this.mempoolSorting.postSorting(txs); + } - log.trace( - `Skipped tx ${tx.hash().toString()} because ${statusMessage}` - ); - if (!(tx.hash().toString() in skippedTransactions)) { - skippedTransactions[tx.hash().toString()] = { - transaction: tx, - paths: stateTransitions - .map((x) => x.path) - .filter((id, idx, arr) => arr.indexOf(id) === idx), - }; - } - stateServiceProvider.popCurrentStateService(); - } + return txs; + } - executionContext.clear(); + @trace("mempool.get_mandatory_txs") + public async getMandatoryTxs(): Promise { + if (this.type() === "private") { + return []; } - return sortedTransactions; + return (await this.messageService?.getPendingMessages()) ?? []; } public async start(): Promise { diff --git a/packages/sequencer/src/mempool/sorting/DefaultMempoolSorting.ts b/packages/sequencer/src/mempool/sorting/DefaultMempoolSorting.ts new file mode 100644 index 000000000..d8666ad7e --- /dev/null +++ b/packages/sequencer/src/mempool/sorting/DefaultMempoolSorting.ts @@ -0,0 +1,32 @@ +import { noop } from "@proto-kit/common"; + +import { PendingTransaction } from "../PendingTransaction"; +import { + SequencerModule, + sequencerModule, +} from "../../sequencer/builder/SequencerModule"; + +import { MempoolSorting } from "./MempoolSorting"; + +@sequencerModule() +export class DefaultMempoolSorting + extends SequencerModule + implements MempoolSorting +{ + public async start() { + noop(); + } + + public enablePostSorting(): boolean { + return false; + } + + public postSorting(transactions: PendingTransaction[]): PendingTransaction[] { + return transactions; + } + + public presortingPriority(tx: PendingTransaction): number { + // This means we order by first in, first out in the db + return Date.UTC(2500, 0) - Date.now(); + } +} diff --git a/packages/sequencer/src/mempool/sorting/MempoolSorting.ts b/packages/sequencer/src/mempool/sorting/MempoolSorting.ts new file mode 100644 index 000000000..fde5370e9 --- /dev/null +++ b/packages/sequencer/src/mempool/sorting/MempoolSorting.ts @@ -0,0 +1,26 @@ +import { PendingTransaction } from "../PendingTransaction"; + +export interface MempoolSorting { + /** + * Presorting happens on the backend (i.e. the DB), before the data travels to the sequencer. + * It's very fast, but limited to only integer sorting. + * The value returned here has to be static per transaction, since it will be sorted and + * compared on the DB-side. + * + * @param tx + * @returns Priority of the transaction - larger is better (therefore will be + * put in the block first) + */ + presortingPriority(tx: PendingTransaction): number; + + /** + * Indicate whether to do pre-sorting (as it's expensive depending on your block size) + */ + enablePostSorting(): boolean; + + /** + * Postsorting happens on the sequencer-side. It's less fast but can take in any two + * transactions and directly compare them based on arbitrary logic + */ + postSorting(transactions: PendingTransaction[]): PendingTransaction[]; +} diff --git a/packages/sequencer/src/protocol/baselayer/BaseLayer.ts b/packages/sequencer/src/protocol/baselayer/BaseLayer.ts index d16cbc656..71788a8c8 100644 --- a/packages/sequencer/src/protocol/baselayer/BaseLayer.ts +++ b/packages/sequencer/src/protocol/baselayer/BaseLayer.ts @@ -7,13 +7,23 @@ import { import { IncomingMessageAdapter } from "../../settlement/messages/IncomingMessageAdapter"; import type { OutgoingMessageAdapter } from "../../settlement/messages/outgoing/OutgoingMessageCollector"; -export interface BaseLayerDependencyRecord extends DependencyRecord { +import { MinaNetworkUtils } from "./network-utils/MinaNetworkUtils"; + +export interface StaticBaseLayerDependencyRecord extends DependencyRecord { IncomingMessageAdapter: DependencyDeclaration; OutgoingMessageAdapter: DependencyDeclaration< OutgoingMessageAdapter >; } +export interface StaticBaseLayer extends DependencyFactory { + dependencies: () => StaticBaseLayerDependencyRecord; +} + +export interface BaseLayerDependencyRecord extends DependencyRecord { + NetworkUtils: DependencyDeclaration; +} + export interface BaseLayer extends DependencyFactory { dependencies: () => BaseLayerDependencyRecord; } diff --git a/packages/sequencer/src/protocol/baselayer/MinaBaseLayer.ts b/packages/sequencer/src/protocol/baselayer/MinaBaseLayer.ts index 1d8e56fbd..f22279d34 100644 --- a/packages/sequencer/src/protocol/baselayer/MinaBaseLayer.ts +++ b/packages/sequencer/src/protocol/baselayer/MinaBaseLayer.ts @@ -2,7 +2,6 @@ import { AreProofsEnabled, DependencyFactory, ModuleContainerLike, - DependencyRecord, } from "@proto-kit/common"; import { Mina } from "o1js"; import { match } from "ts-pattern"; @@ -15,9 +14,8 @@ import { } from "../../sequencer/builder/SequencerModule"; import { MinaTransactionSender } from "../../settlement/transactions/MinaTransactionSender"; import { DefaultOutgoingMessageAdapter } from "../../settlement/messages/outgoing/DefaultOutgoingMessageAdapter"; -import { IncomingMessagesService } from "../../settlement/messages/IncomingMessagesService"; -import { BaseLayer } from "./BaseLayer"; +import { BaseLayer, StaticBaseLayer } from "./BaseLayer"; import { LocalBlockchainUtils } from "./network-utils/LocalBlockchainUtils"; import { LightnetUtils } from "./network-utils/LightnetUtils"; import { RemoteNetworkUtils } from "./network-utils/RemoteNetworkUtils"; @@ -65,20 +63,6 @@ export class MinaBaseLayer } public static dependencies() { - return { - IncomingMessagesService: { - useClass: IncomingMessagesService, - }, - } satisfies DependencyRecord; - } - - public dependencies() { - const NetworkUtilsClass = match(this.config.network.type) - .with("local", () => LocalBlockchainUtils) - .with("lightnet", () => LightnetUtils) - .with("remote", () => RemoteNetworkUtils) - .exhaustive(); - return { IncomingMessageAdapter: { useClass: MinaIncomingMessageAdapter, @@ -91,7 +75,17 @@ export class MinaBaseLayer OutgoingMessageAdapter: { useClass: DefaultOutgoingMessageAdapter, }, + }; + } + public dependencies() { + const NetworkUtilsClass = match(this.config.network.type) + .with("local", () => LocalBlockchainUtils) + .with("lightnet", () => LightnetUtils) + .with("remote", () => RemoteNetworkUtils) + .exhaustive(); + + return { NetworkUtils: { useClass: NetworkUtilsClass, }, @@ -109,6 +103,14 @@ export class MinaBaseLayer return this.config.network.type === "local"; } + /** + * Signed settlement happens when proofs are disabled and the network is remote + * This is because on local network we can use mock proofs, while on remotes ones we can't + */ + public isSignedSettlement(): boolean { + return !this.areProofsEnabled.areProofsEnabled && !this.isLocalBlockChain(); + } + public async start(): Promise { const { network } = this.config; @@ -144,4 +146,4 @@ export class MinaBaseLayer } } -MinaBaseLayer satisfies DependencyFactory; +MinaBaseLayer satisfies StaticBaseLayer; diff --git a/packages/sequencer/src/protocol/baselayer/NoopBaseLayer.ts b/packages/sequencer/src/protocol/baselayer/NoopBaseLayer.ts index 9e64f6996..430fa3272 100644 --- a/packages/sequencer/src/protocol/baselayer/NoopBaseLayer.ts +++ b/packages/sequencer/src/protocol/baselayer/NoopBaseLayer.ts @@ -11,7 +11,7 @@ import { PendingTransaction } from "../../mempool/PendingTransaction"; import { OutgoingMessageAdapter } from "../../settlement/messages/outgoing/OutgoingMessageCollector"; import { Block } from "../../storage/model/Block"; -import { BaseLayer, BaseLayerDependencyRecord } from "./BaseLayer"; +import { StaticBaseLayer, StaticBaseLayerDependencyRecord } from "./BaseLayer"; class NoopIncomingMessageAdapter implements IncomingMessageAdapter { async fetchPendingMessages( @@ -40,7 +40,7 @@ class NoopMessageAdapter implements OutgoingMessageAdapter { } @sequencerModule() -export class NoopBaseLayer extends SequencerModule implements BaseLayer { +export class NoopBaseLayer extends SequencerModule { public async blockProduced(): Promise { noop(); } @@ -49,7 +49,7 @@ export class NoopBaseLayer extends SequencerModule implements BaseLayer { noop(); } - public dependencies(): BaseLayerDependencyRecord { + public static dependencies(): StaticBaseLayerDependencyRecord { return { OutgoingMessageAdapter: { useClass: NoopMessageAdapter, @@ -60,3 +60,5 @@ export class NoopBaseLayer extends SequencerModule implements BaseLayer { }; } } + +NoopBaseLayer satisfies StaticBaseLayer; diff --git a/packages/sequencer/src/protocol/production/BatchProducerModule.ts b/packages/sequencer/src/protocol/production/BatchProducerModule.ts index 545190624..81ee673be 100644 --- a/packages/sequencer/src/protocol/production/BatchProducerModule.ts +++ b/packages/sequencer/src/protocol/production/BatchProducerModule.ts @@ -17,6 +17,7 @@ import { BlockWithResult } from "../../storage/model/Block"; import type { Database } from "../../storage/Database"; import { AsyncLinkedLeafStore } from "../../state/async/AsyncLinkedLeafStore"; import { CachedLinkedLeafStore } from "../../state/lmt/CachedLinkedLeafStore"; +import { ensureNotBusy } from "../../helpers/BusyGuard"; import { BlockProofSerializer } from "./tasks/serializers/BlockProofSerializer"; import { BatchTracingService } from "./tracing/BatchTracingService"; @@ -44,8 +45,6 @@ const errors = { */ @sequencerModule() export class BatchProducerModule extends SequencerModule { - private productionInProgress = false; - public constructor( @inject("AsyncLinkedLeafStore") private readonly merkleStore: AsyncLinkedLeafStore, @@ -62,47 +61,11 @@ export class BatchProducerModule extends SequencerModule { /** * Main function to call when wanting to create a new block based on the * transactions that are present in the mempool. This function should also - * be the one called by BlockTriggerss + * be the one called by BlockTriggers. */ + @ensureNotBusy() public async createBatch( blocks: BlockWithResult[] - ): Promise { - if (!this.productionInProgress) { - try { - this.productionInProgress = true; - - const batch = await this.tryProduceBatch(blocks); - - this.productionInProgress = false; - - return batch; - } catch (error: unknown) { - this.productionInProgress = false; - // TODO Check if that still makes sense - if (error instanceof Error) { - if ( - !error.message.includes( - "Can't create a block with zero transactions" - ) - ) { - log.error(error); - } - - throw error; - } else { - log.error(error); - } - } - } else { - log.debug( - "Skipping new block production because production is still in progress" - ); - } - return undefined; - } - - private async tryProduceBatch( - blocks: BlockWithResult[] ): Promise { log.info("Producing batch..."); diff --git a/packages/sequencer/src/protocol/production/flow/BatchFlow.ts b/packages/sequencer/src/protocol/production/flow/BatchFlow.ts index 6fc80b17e..e2267ec60 100644 --- a/packages/sequencer/src/protocol/production/flow/BatchFlow.ts +++ b/packages/sequencer/src/protocol/production/flow/BatchFlow.ts @@ -5,14 +5,9 @@ import { Protocol, StateTransitionProverPublicInput, StateTransitionProverPublicOutput, + TransactionProverPublicInput, } from "@proto-kit/protocol"; -import { - isFull, - mapSequential, - MAX_FIELD, - Nullable, - range, -} from "@proto-kit/common"; +import { isFull, mapSequential, Nullable } from "@proto-kit/common"; import { FlowCreator } from "../../../worker/flow/Flow"; import { NewBlockProvingParameters, NewBlockTask } from "../tasks/NewBlockTask"; @@ -33,7 +28,7 @@ export class BatchFlow { private readonly blockProvingTask: NewBlockTask, private readonly blockReductionTask: BlockReductionTask, private readonly stateTransitionFlow: StateTransitionFlow, - private readonly blockFlow: BlockFlow, + private readonly transactionFlow: BlockFlow, @inject("Protocol") private readonly protocol: Protocol, @inject("Tracer") @@ -42,7 +37,7 @@ export class BatchFlow { private isBlockProofsMergable(a: BlockProof, b: BlockProof): boolean { // TODO Proper replication of merge logic - const part1 = a.publicOutput.stateRoot + return a.publicOutput.stateRoot .equals(b.publicInput.stateRoot) .and(a.publicOutput.blockHashRoot.equals(b.publicInput.blockHashRoot)) .and( @@ -53,26 +48,12 @@ export class BatchFlow { b.publicInput.eternalTransactionsHash ) ) - .and(a.publicOutput.closed.equals(b.publicOutput.closed)) + .and( + a.publicOutput.proverStateRemainder.equals( + b.publicInput.proverStateRemainder + ) + ) .toBoolean(); - - const proof1Closed = a.publicOutput.closed; - const proof2Closed = b.publicOutput.closed; - - const blockNumberProgressionValid = a.publicOutput.blockNumber.equals( - b.publicInput.blockNumber - ); - - const isValidTransactionMerge = a.publicInput.blockNumber - .equals(MAX_FIELD) - .and(blockNumberProgressionValid) - .and(proof1Closed.or(proof2Closed).not()); - - const isValidClosedMerge = proof1Closed - .and(proof2Closed) - .and(blockNumberProgressionValid); - - return part1 && isValidClosedMerge.or(isValidTransactionMerge).toBoolean(); } private async pushBlockInput( @@ -92,6 +73,14 @@ export class BatchFlow { ); } + private dummyTransactionProof() { + return this.protocol.transactionProver.zkProgrammable.zkProgram[0].Proof.dummy( + TransactionProverPublicInput.empty(), + TransactionProverPublicInput.empty(), + 2 + ); + } + @trace("batch.prove", ([, batchId]) => ({ batchId })) public async executeBatch(batch: BatchTrace, batchId: number) { const batchFlow = new ReductionTaskFlow( @@ -105,41 +94,54 @@ export class BatchFlow { this.flowCreator ); - const map: Record< - number, - Nullable - > = Object.fromEntries( - batch.blocks.map((blockTrace, i) => [ - i, - { - params: blockTrace.blockParams, - input1: undefined, - input2: undefined, - }, - ]) - ); + const lastBlockProofCollector: Nullable = { + params: batch.blocks.at(-1)!.block, + input1: undefined, + input2: undefined, + }; const dummySTProof = await this.dummySTProof(); - range(0, batch.blocks.length - 1).forEach((index) => { - map[index].input1 = dummySTProof; - }); + const dummyTransactionProof = await this.dummyTransactionProof(); + // TODO Make sure we use deferErrorsTo to everywhere (preferably with a nice pattern) + // Currently, a lot of errors just get eaten and the chain just halts with no + // error being thrown await this.stateTransitionFlow.executeBatches( batch.stateTransitionTrace, batchId, async (proof) => { - const index = batch.blocks.length - 1; - map[index].input1 = proof; - await this.pushBlockInput(map[index], batchFlow); + lastBlockProofCollector.input1 = proof; + await this.pushBlockInput(lastBlockProofCollector, batchFlow); } ); - await mapSequential(batch.blocks, async (blockTrace, blockIndex) => { - await this.blockFlow.executeBlock(blockTrace, async (proof) => { - map[blockIndex].input2 = proof; - await this.pushBlockInput(map[blockIndex], batchFlow); - }); - }); + // TODO Proper height + await this.transactionFlow.createTransactionProof( + batch.blocks[0].heights[0], + batch.transactions, + async (proof) => { + lastBlockProofCollector.input2 = proof; + await this.pushBlockInput(lastBlockProofCollector, batchFlow); + } + ); + + // TODO Cover case where either 0 STs or 0 Transactions are in a batch + + // Push all blocks except the last one with dummy proofs + // except the last one, which will wait on the two proofs to complete + await mapSequential( + batch.blocks.slice(0, batch.blocks.length - 1), + async (blockTrace) => { + await this.pushBlockInput( + { + input1: dummySTProof, + input2: dummyTransactionProof, + params: blockTrace.block, + }, + batchFlow + ); + } + ); return await new Promise((res, rej) => { batchFlow.onCompletion(async (result) => res(result)); diff --git a/packages/sequencer/src/protocol/production/flow/BlockFlow.ts b/packages/sequencer/src/protocol/production/flow/BlockFlow.ts index 8dea8b59c..874a4f03d 100644 --- a/packages/sequencer/src/protocol/production/flow/BlockFlow.ts +++ b/packages/sequencer/src/protocol/production/flow/BlockFlow.ts @@ -1,23 +1,22 @@ import { inject, injectable, Lifecycle, scoped } from "tsyringe"; import { - BlockProof, - BlockProverPublicInput, - BlockProverPublicOutput, MandatoryProtocolModulesRecord, Protocol, + TransactionProof, } from "@proto-kit/protocol"; -import { Bool, Field } from "o1js"; -import { MAX_FIELD } from "@proto-kit/common"; +import { mapSequential } from "@proto-kit/common"; +// eslint-disable-next-line import/no-extraneous-dependencies +import chunk from "lodash/chunk"; import { TransactionProvingTask } from "../tasks/TransactionProvingTask"; -import { BlockReductionTask } from "../tasks/BlockReductionTask"; -import { TransactionProvingTaskParameters } from "../tasks/serializers/types/TransactionProvingTypes"; import { FlowCreator } from "../../../worker/flow/Flow"; -import { BlockTrace } from "../tracing/BlockTracingService"; +import { TransactionReductionTask } from "../tasks/TransactionReductionTask"; +import { TransactionTrace } from "../tracing/TransactionTracingService"; import { ReductionTaskFlow } from "./ReductionTaskFlow"; import { TransactionFlow } from "./TransactionFlow"; +// TODO Rename to TransactionFlow @injectable() @scoped(Lifecycle.ContainerScoped) export class BlockFlow { @@ -25,54 +24,41 @@ export class BlockFlow { private readonly flowCreator: FlowCreator, @inject("Protocol") private readonly protocol: Protocol, - private readonly transactionProvingTask: TransactionProvingTask, - private readonly blockReductionTask: BlockReductionTask, - private readonly transactionFlow: TransactionFlow + private readonly runtimeFlow: TransactionFlow, + private readonly transactionTask: TransactionProvingTask, + private readonly transactionMergeTask: TransactionReductionTask ) {} - private async dummyTransactionProof(trace: BlockTrace) { - const publicInput = { - ...trace.blockParams.publicInput, - networkStateHash: Field(0), - transactionsHash: Field(0), - blockHashRoot: Field(0), - blockNumber: MAX_FIELD, - } satisfies BlockProverPublicInput; + private dummyProof: TransactionProof | undefined = undefined; - // TODO Set publicInput.stateRoot to result after block hooks! - const publicOutput = new BlockProverPublicOutput({ - ...publicInput, - closed: Bool(true), - }); + private async dummyTransactionProof() { + if (this.dummyProof !== undefined) { + return this.dummyProof; + } - return await this.protocol.blockProver.zkProgrammable.zkProgram[0].Proof.dummy( - publicInput, - publicOutput, - 2 - ); + const flow = this.flowCreator.createFlow("transaction-dummy", undefined); + const dummy = await flow.withFlow(async (resolve) => { + await flow.pushTask(this.transactionTask, "dummy", async (result) => { + resolve(result); + }); + }); + this.dummyProof = dummy; + return dummy; } - private async executeTransactions( - trace: BlockTrace - ): Promise> { - const transactionFlow = new ReductionTaskFlow( + private async proveTransactions(height: string, traces: TransactionTrace[]) { + const flow = new ReductionTaskFlow( { - name: `transactions-${trace.height}`, - inputLength: trace.transactions.length, - mappingTask: this.transactionProvingTask, - reductionTask: this.blockReductionTask, - + name: `transaction-${height}`, + inputLength: Math.ceil(traces.length / 2), + mappingTask: this.transactionTask, + reductionTask: this.transactionMergeTask, mergableFunction: (a, b) => - a.publicOutput.stateRoot - .equals(b.publicInput.stateRoot) + a.publicOutput.eternalTransactionsHash + .equals(b.publicInput.eternalTransactionsHash) .and( - a.publicOutput.transactionsHash.equals( - b.publicInput.transactionsHash - ) - ) - .and( - a.publicInput.networkStateHash.equals( - b.publicInput.networkStateHash + a.publicOutput.incomingMessagesHash.equals( + b.publicInput.incomingMessagesHash ) ) .toBoolean(), @@ -80,32 +66,30 @@ export class BlockFlow { this.flowCreator ); - await transactionFlow.flow.forEach( - trace.transactions, - async (transactionTrace, txIndex) => { - await this.transactionFlow.proveRuntimes( - transactionTrace, - trace.height, - txIndex, - async (parameters) => { - await transactionFlow.pushInput(parameters); - } - ); - } - ); + await mapSequential(chunk(traces, 2), async (traceChunk, index) => { + await this.runtimeFlow.proveRuntimes( + traceChunk, + height, + index, + async (result) => { + await flow.pushInput(result); + } + ); + }); - return transactionFlow; + return flow; } - public async executeBlock( - trace: BlockTrace, - callback: (proof: BlockProof) => Promise + public async createTransactionProof( + height: string, + trace: TransactionTrace[], + callback: (proof: TransactionProof) => Promise ) { - if (trace.transactions.length === 0) { - const proof = await this.dummyTransactionProof(trace); + if (trace.length === 0) { + const proof = await this.dummyTransactionProof(); await callback(proof); } else { - const flow = await this.executeTransactions(trace); + const flow = await this.proveTransactions(height, trace); flow.onCompletion(async (result) => { await callback(result); }); diff --git a/packages/sequencer/src/protocol/production/flow/ReductionTaskFlow.ts b/packages/sequencer/src/protocol/production/flow/ReductionTaskFlow.ts index 1ae3ec92b..e4d950ad5 100644 --- a/packages/sequencer/src/protocol/production/flow/ReductionTaskFlow.ts +++ b/packages/sequencer/src/protocol/production/flow/ReductionTaskFlow.ts @@ -176,7 +176,7 @@ export class ReductionTaskFlow { /** * To be used in conjunction with onCompletion - * It allows errors from this flow to be "defered" to another parent + * It allows errors from this flow to be "deferred" to another parent * flow which might be properly awaited and therefore will throw the * error up to the user * @param flow diff --git a/packages/sequencer/src/protocol/production/flow/TransactionFlow.ts b/packages/sequencer/src/protocol/production/flow/TransactionFlow.ts index d124a352c..5d968758a 100644 --- a/packages/sequencer/src/protocol/production/flow/TransactionFlow.ts +++ b/packages/sequencer/src/protocol/production/flow/TransactionFlow.ts @@ -1,10 +1,10 @@ import { injectable } from "tsyringe"; +import { assertSizeOneOrTwo } from "@proto-kit/common"; import { Flow, FlowCreator } from "../../../worker/flow/Flow"; import { RuntimeProof, TransactionProvingTaskParameters, - TransactionProvingType, } from "../tasks/serializers/types/TransactionProvingTypes"; import { RuntimeProvingTask } from "../tasks/RuntimeProvingTask"; import { TransactionTrace } from "../tracing/TransactionTracingService"; @@ -20,31 +20,31 @@ export class TransactionFlow { flow: Flow<{ runtimeProofs: { proof: RuntimeProof; index: number }[]; }>, - trace: TransactionTrace, + trace: [TransactionTrace] | [TransactionTrace, TransactionTrace], callback: (params: TransactionProvingTaskParameters) => Promise ) { - const requiredLength = trace.type === TransactionProvingType.MULTI ? 2 : 1; + const requiredLength = trace.length; if (flow.state.runtimeProofs.length === requiredLength) { let parameters: TransactionProvingTaskParameters; - if (trace.type === TransactionProvingType.MULTI) { + if (requiredLength === 2) { // Sort ascending const sorted = flow.state.runtimeProofs.sort( ({ index: a }, { index: b }) => a - b ); - parameters = { - type: trace.type, - parameters: trace.transaction, - proof1: sorted[0].proof, - proof2: sorted[1].proof, - }; + + parameters = [ + { parameters: trace[0].transaction, proof: sorted[0].proof }, + { parameters: trace[1].transaction, proof: sorted[1].proof }, + ]; } else { - parameters = { - type: trace.type, - parameters: trace.transaction, - proof1: flow.state.runtimeProofs[0].proof, - }; + parameters = [ + { + parameters: trace[0].transaction, + proof: flow.state.runtimeProofs[0].proof, + }, + ]; } await callback(parameters); @@ -52,13 +52,15 @@ export class TransactionFlow { } public async proveRuntimes( - trace: TransactionTrace, + trace: TransactionTrace[], blockHeight: string, txIndex: number, callback: (params: TransactionProvingTaskParameters) => Promise ) { + assertSizeOneOrTwo(trace); + const name = `transaction-${blockHeight}-${txIndex}${ - trace.type === TransactionProvingType.MULTI ? "-double" : "" + trace.length === 2 ? "-double" : "" }`; const flow = this.flowCreator.createFlow<{ runtimeProofs: { proof: RuntimeProof; index: number }[]; @@ -66,24 +68,17 @@ export class TransactionFlow { runtimeProofs: [], }); - await flow.pushTask( - this.runtimeProvingTask, - trace.runtime[0], - async (proof) => { - flow.state.runtimeProofs.push({ proof, index: 0 }); - await this.resolveTransactionFlow(flow, trace, callback); - } + await Promise.all( + trace.map(async (transaction, index) => { + await flow.pushTask( + this.runtimeProvingTask, + transaction.runtime, + async (proof) => { + flow.state.runtimeProofs.push({ proof, index }); + await this.resolveTransactionFlow(flow, trace, callback); + } + ); + }) ); - - if (trace.type === TransactionProvingType.MULTI) { - await flow.pushTask( - this.runtimeProvingTask, - trace.runtime[1], - async (proof) => { - flow.state.runtimeProofs.push({ proof, index: 1 }); - await this.resolveTransactionFlow(flow, trace, callback); - } - ); - } } } diff --git a/packages/sequencer/src/protocol/production/sequencing/BlockBuilder.ts b/packages/sequencer/src/protocol/production/sequencing/BlockBuilder.ts new file mode 100644 index 000000000..6ebe0b43c --- /dev/null +++ b/packages/sequencer/src/protocol/production/sequencing/BlockBuilder.ts @@ -0,0 +1,152 @@ +import { inject, injectable } from "tsyringe"; +import { + BeforeTransactionHookArguments, + MandatoryProtocolModulesRecord, + NetworkState, + Protocol, + ProtocolModulesRecord, + ProvableTransactionHook, + StateServiceProvider, + toBeforeTransactionHookArgument, +} from "@proto-kit/protocol"; +import { log, mapSequential } from "@proto-kit/common"; + +import { Mempool } from "../../../mempool/Mempool"; +import { CachedStateService } from "../../../state/state/CachedStateService"; +import { PendingTransaction } from "../../../mempool/PendingTransaction"; +import { Tracer } from "../../../logging/Tracer"; +import { trace } from "../../../logging/trace"; + +import { + BlockTrackers, + TransactionExecutionResultStatus, + TransactionExecutionService, +} from "./TransactionExecutionService"; +import { Ordering, OrderingMetadata } from "./Ordering"; + +// TODO Allow user overriding of the blockbuilder +@injectable() +export class BlockBuilder { + private readonly transactionHooks: ProvableTransactionHook[]; + + public constructor( + private readonly executionService: TransactionExecutionService, + @inject("Mempool") private readonly mempool: Mempool, + @inject("StateServiceProvider") + private readonly stateServiceProvider: StateServiceProvider, + @inject("Protocol") + protocol: Protocol, + @inject("Tracer") public readonly tracer: Tracer + ) { + this.transactionHooks = protocol.dependencyContainer.resolveAll( + "ProvableTransactionHook" + ); + } + + private async shouldRemove( + state: CachedStateService, + args: BeforeTransactionHookArguments + ) { + this.stateServiceProvider.setCurrentStateService(state); + + const returnValues = await mapSequential(this.transactionHooks, (hook) => + hook.removeTransactionWhen(args) + ); + + this.stateServiceProvider.popCurrentStateService(); + return returnValues.some((x) => x); + } + + @trace("block.build") + // eslint-disable-next-line sonarjs/cognitive-complexity + public async buildBlock( + asyncStateService: CachedStateService, + networkState: NetworkState, + state: BlockTrackers, + maximumBlockSize: number + ): Promise<{ + blockState: BlockTrackers; + executionResults: TransactionExecutionResultStatus[]; + orderingMetadata: OrderingMetadata; + }> { + let blockState = state; + const exceptionExecutionResults: TransactionExecutionResultStatus[] = []; + + const networkStateHash = networkState.hash(); + + const ordering = new Ordering(this.mempool, maximumBlockSize); + + let tx: PendingTransaction | undefined; + // eslint-disable-next-line no-await-in-loop,no-cond-assign + while ((tx = await ordering.requestNextTransaction()) !== undefined) { + try { + const newState = this.executionService.addTransactionToBlockProverState( + BlockTrackers.clone(blockState), + tx + ); + + // TODO Use RecordingStateService -> async asProver needed + const recordingStateService = new CachedStateService(asyncStateService); + + // Create execution trace + const executionTrace = + // eslint-disable-next-line no-await-in-loop + await this.executionService.createExecutionTrace( + recordingStateService, + tx, + { networkState, hash: networkStateHash }, + blockState, + newState + ); + + const transactionIncluded = + executionTrace.hooksStatus.toBoolean() || executionTrace.tx.isMessage; + + let shouldRemove = false; + if (transactionIncluded) { + blockState = newState; + + // Only for successful hooks, messages will be included but progress thrown away + if (executionTrace.hooksStatus.toBoolean()) { + // eslint-disable-next-line no-await-in-loop + await recordingStateService.mergeIntoParent(); + } + } else { + // Execute removeWhen to determine whether it should be dropped + // eslint-disable-next-line no-await-in-loop + shouldRemove = await this.shouldRemove( + new CachedStateService(asyncStateService), + toBeforeTransactionHookArgument( + tx.toProtocolTransaction(), + networkState, + blockState + ) + ); + + const actionMessage = shouldRemove + ? "removing as to removeWhen hooks" + : "skipping"; + log.error( + `Error in inclusion of tx, ${actionMessage}: Protocol hooks not executable: ${executionTrace.statusMessage ?? "unknown reason"}` + ); + } + + ordering.reportResult({ result: executionTrace, shouldRemove }); + } catch (error) { + if (error instanceof Error) { + log.error("Error in inclusion of tx, dropping", error); + exceptionExecutionResults.push({ tx, status: "shouldRemove" }); + } + } + } + + const { results: orderingResults, orderingMetadata } = + ordering.getResults(); + + return { + blockState, + executionResults: orderingResults.concat(...exceptionExecutionResults), + orderingMetadata, + }; + } +} diff --git a/packages/sequencer/src/protocol/production/sequencing/BlockProducerModule.ts b/packages/sequencer/src/protocol/production/sequencing/BlockProducerModule.ts index 8f035c338..278de217a 100644 --- a/packages/sequencer/src/protocol/production/sequencing/BlockProducerModule.ts +++ b/packages/sequencer/src/protocol/production/sequencing/BlockProducerModule.ts @@ -14,7 +14,6 @@ import { SequencerModule, } from "../../../sequencer/builder/SequencerModule"; import { BlockQueue } from "../../../storage/repositories/BlockStorage"; -import { PendingTransaction } from "../../../mempool/PendingTransaction"; import { AsyncMerkleTreeStore } from "../../../state/async/AsyncMerkleTreeStore"; import { AsyncStateService } from "../../../state/async/AsyncStateService"; import { @@ -23,10 +22,11 @@ import { BlockWithResult, } from "../../../storage/model/Block"; import { Database } from "../../../storage/Database"; -import { IncomingMessagesService } from "../../../settlement/messages/IncomingMessagesService"; import { Tracer } from "../../../logging/Tracer"; import { trace } from "../../../logging/trace"; import { AsyncLinkedLeafStore } from "../../../state/async/AsyncLinkedLeafStore"; +import { TransactionStorage } from "../../../storage/repositories/TransactionStorage"; +import { ensureNotBusy } from "../../../helpers/BusyGuard"; import { BlockProductionService } from "./BlockProductionService"; import { BlockResultService } from "./BlockResultService"; @@ -38,18 +38,16 @@ export interface BlockConfig { @sequencerModule() export class BlockProducerModule extends SequencerModule { - private productionInProgress = false; - public constructor( @inject("Mempool") private readonly mempool: Mempool, - @inject("IncomingMessagesService", { isOptional: true }) - private readonly messageService: IncomingMessagesService | undefined, @inject("UnprovenStateService") private readonly unprovenStateService: AsyncStateService, @inject("UnprovenLinkedLeafStore") private readonly unprovenLinkedLeafStore: AsyncLinkedLeafStore, @inject("BlockQueue") private readonly blockQueue: BlockQueue, + @inject("TransactionStorage") + private readonly transactionStorage: TransactionStorage, @inject("BlockTreeStore") private readonly blockTreeStore: AsyncMerkleTreeStore, private readonly productionService: BlockProductionService, @@ -140,48 +138,29 @@ export class BlockProducerModule extends SequencerModule { return result; } + @ensureNotBusy() public async tryProduceBlock(): Promise { - if (!this.productionInProgress) { - try { - const block = await this.produceBlock(); - - if (block === undefined) { - if (!this.allowEmptyBlock()) { - log.info("No transactions in mempool, skipping production"); - } else { - log.error("Something wrong happened, skipping block"); - } - return undefined; - } + const block = await this.produceBlock(); - log.info( - `Produced block #${block.height.toBigInt()} (${block.transactions.length} txs)` - ); - this.prettyPrintBlockContents(block); - - return block; - } catch (error: unknown) { - if (error instanceof Error) { - throw error; - } else { - log.error(error); - } - } finally { - this.productionInProgress = false; + if (block === undefined) { + if (!this.allowEmptyBlock()) { + log.info("No transactions in mempool, skipping production"); + } else { + log.error("Something wrong happened, skipping block"); } + return undefined; } - return undefined; + + log.info( + `Produced block #${block.height.toBigInt()} (${block.transactions.length} txs)` + ); + this.prettyPrintBlockContents(block); + + return block; } - // TODO Move to different service, to remove dependency on mempool and messagequeue - // Idea: Create a service that aggregates a bunch of different sources @trace("block.collect_inputs") - private async collectProductionData(): Promise<{ - txs: PendingTransaction[]; - metadata: BlockWithResult; - }> { - const txs = await this.mempool.getTxs(this.maximumBlockSize()); - + private async collectProductionData(): Promise { const parentBlock = await this.blockQueue.getLatestBlockAndResult(); let metadata: BlockWithResult; @@ -203,68 +182,60 @@ export class BlockProducerModule extends SequencerModule { }; } - let messages: PendingTransaction[] = []; - if (this.messageService !== undefined) { - messages = await this.messageService.getPendingMessages(); - } - - log.debug( - `Block collected, ${txs.length} txs, ${messages.length} messages` - ); - - return { - txs: messages.concat(txs), - metadata, - }; + return metadata; } @trace("block") private async produceBlock(): Promise { - this.productionInProgress = true; - - const { txs, metadata } = await this.collectProductionData(); - - // Skip production if no transactions are available for now - if (txs.length === 0 && !this.allowEmptyBlock()) { - return undefined; - } + const metadata = await this.collectProductionData(); const blockResult = await this.productionService.createBlock( this.unprovenStateService, - txs, metadata, - this.allowEmptyBlock() + this.allowEmptyBlock(), + this.maximumBlockSize() ); if (blockResult !== undefined) { - const { block, stateChanges } = blockResult; + const { block, stateChanges, orderingMetadata } = blockResult; + + // Skip production if no transactions are available for now + if (block.transactions.length === 0 && !this.allowEmptyBlock()) { + return undefined; + } await this.tracer.trace( "block.commit", - async () => + async () => { // Push changes to the database atomically await this.database.executeInTransaction(async () => { await stateChanges.mergeIntoParent(); await this.blockQueue.pushBlock(block); - }), + + // Remove included or dropped txs, leave skipped ones alone + await this.mempool.removeTxs( + blockResult.includedTxs + .filter((x) => x.type === "included") + .map((x) => x.hash), + blockResult.includedTxs + .filter((x) => x.type === "shouldRemove") + .map((x) => x.hash) + ); + + await this.transactionStorage.reportChangedPaths( + orderingMetadata.allChangedPaths + ); + await this.transactionStorage.reportSkippedTransactions( + orderingMetadata.skippedPaths + ); + }); + }, { height: block.height.toString(), } ); - - // Remove included or dropped txs, leave skipped ones alone - await this.mempool.removeTxs( - blockResult.includedTxs - .filter((x) => x.type === "included") - .map((x) => x.hash), - blockResult.includedTxs - .filter((x) => x.type === "shouldRemove") - .map((x) => x.hash) - ); } - this.productionInProgress = false; - return blockResult?.block; } diff --git a/packages/sequencer/src/protocol/production/sequencing/BlockProductionService.ts b/packages/sequencer/src/protocol/production/sequencing/BlockProductionService.ts index 18010fa28..95ad398a4 100644 --- a/packages/sequencer/src/protocol/production/sequencing/BlockProductionService.ts +++ b/packages/sequencer/src/protocol/production/sequencing/BlockProductionService.ts @@ -10,7 +10,7 @@ import { reduceStateTransitions, RuntimeTransaction, StateServiceProvider, - toProvableHookBlockState, + toBeforeBlockHookArgument, TransactionHashList, } from "@proto-kit/protocol"; import { Field } from "o1js"; @@ -33,12 +33,15 @@ import { BlockTrackers, executeWithExecutionContext, TransactionExecutionResultStatus, - TransactionExecutionService, } from "./TransactionExecutionService"; - -function isIncludedTxs( - x: TransactionExecutionResultStatus -): x is { status: "included"; result: TransactionExecutionResult } { +import { BlockBuilder } from "./BlockBuilder"; +import { OrderingMetadata } from "./Ordering"; + +function isIncludedTxs(x: TransactionExecutionResultStatus): x is { + status: "included"; + tx: PendingTransaction; + result: TransactionExecutionResult; +} { return x.status === "included"; } @@ -52,7 +55,7 @@ export class BlockProductionService { protocol: Protocol, @inject("Tracer") public readonly tracer: Tracer, - private readonly transactionExecutionService: TransactionExecutionService, + private readonly blockBuilder: BlockBuilder, @inject("StateServiceProvider") private readonly stateServiceProvider: StateServiceProvider ) { @@ -98,9 +101,9 @@ export class BlockProductionService { */ public async createBlock( asyncStateService: AsyncStateService, - transactions: PendingTransaction[], lastBlockWithResult: BlockWithResult, - allowEmptyBlocks: boolean + allowEmptyBlocks: boolean, + maximumBlockSize: number ): Promise< | { block: Block; @@ -109,6 +112,7 @@ export class BlockProductionService { hash: string; type: "included" | "skipped" | "shouldRemove"; }[]; + orderingMetadata: OrderingMetadata; } | undefined > { @@ -128,7 +132,7 @@ export class BlockProductionService { // Get used networkState by executing beforeBlock() hooks const beforeHookResult = await this.executeBeforeBlockHook( - toProvableHookBlockState(blockState), + toBeforeBlockHookArgument(blockState), lastResult.afterNetworkState, stateService ); @@ -140,13 +144,16 @@ export class BlockProductionService { UntypedStateTransition.fromStateTransition(transition) ); - const { blockState: newBlockState, executionResults } = - await this.transactionExecutionService.createExecutionTraces( - stateService, - transactions, - networkState, - blockState - ); + const { + blockState: newBlockState, + executionResults, + orderingMetadata, + } = await this.blockBuilder.buildBlock( + stateService, + networkState, + blockState, + maximumBlockSize + ); const previousBlockHash = lastResult.blockHash === 0n ? undefined : Field(lastResult.blockHash); @@ -204,6 +211,7 @@ export class BlockProductionService { }, stateChanges: stateService, includedTxs, + orderingMetadata, }; } } diff --git a/packages/sequencer/src/protocol/production/sequencing/Ordering.ts b/packages/sequencer/src/protocol/production/sequencing/Ordering.ts new file mode 100644 index 000000000..9cc8132ae --- /dev/null +++ b/packages/sequencer/src/protocol/production/sequencing/Ordering.ts @@ -0,0 +1,195 @@ +import { Field } from "o1js"; +import { filterNonUndefined } from "@proto-kit/common"; + +import { distinct, distinctByPredicate } from "../../../helpers/utils"; +import { Mempool } from "../../../mempool/Mempool"; +import { PendingTransaction } from "../../../mempool/PendingTransaction"; +import { TransactionExecutionResult } from "../../../storage/model/Block"; + +import { TransactionExecutionResultStatus } from "./TransactionExecutionService"; + +function allKeys(stateTransitions: { path: Field }[]): bigint[] { + // We have to do the distinct with strings because + // array.indexOf() doesn't work with fields + return stateTransitions.map((st) => st.path.toBigInt()).filter(distinct); +} + +export type OrderingReport = { + result: TransactionExecutionResult; + shouldRemove: boolean; +}; + +export type OrderingMetadata = { + skippedPaths: { [p: string]: bigint[] }; + allChangedPaths: bigint[]; +}; + +export class PathResolution { + failedTxIds = new Map(); + + paths = new Map(); + + public resolvePaths(paths: bigint[]) { + const allSymbols = paths.flatMap((key) => { + const symbols = this.paths.get(key); + if (symbols !== undefined) { + this.paths.delete(key); + } + return symbols ?? []; + }); + + return allSymbols + .map((symbol) => { + const tx = this.failedTxIds.get(symbol); + this.failedTxIds.delete(symbol); + return tx; + }) + .filter(filterNonUndefined); + } + + public pushPaths(object: Object, paths: bigint[]) { + const symbol = Symbol("tx"); + this.failedTxIds.set(symbol, object); + + paths.forEach((path) => { + const symbols = this.paths.get(path) ?? []; + symbols.push(symbol); + this.paths.set(path, symbols); + }); + } + + // TODO I hate how inefficient this function is - the tradeoff here is + // lookup performance during block production vs. after + public retrieveUnresolved(key: (o: Object) => string) { + const paths = new Map(); + for (const [path, txs] of this.paths.entries()) { + txs.forEach((tx) => { + const hash = key(this.failedTxIds.get(tx)!); + const thisPaths = paths.get(hash) ?? []; + thisPaths.push(path); + paths.set(hash, thisPaths); + }); + } + return Object.fromEntries(paths.entries()); + } +} + +export class Ordering { + public constructor( + private readonly mempool: Mempool, + private sizeLimit: number + ) {} + + mandatoryTransactionsCompleted = false; + + transactionQueue: PendingTransaction[] = []; + + results: TransactionExecutionResultStatus[] = []; + + ordered = 0; + + userTxOffset = 0; + + // For dependency resolution + pathResolution = new PathResolution(); + + allChangedPaths = new Set(); + + public resolvePaths(result: TransactionExecutionResult) { + const paths = allKeys( + result.stateTransitions.flatMap((x) => x.stateTransitions) + ); + + const txs = this.pathResolution.resolvePaths(paths); + + this.transactionQueue.push(...txs); + + paths.forEach((path) => this.allChangedPaths.add(path)); + } + + private pushFailed(result: TransactionExecutionResult) { + const keys = allKeys(result.stateTransitions[0].stateTransitions); + + this.pathResolution.pushPaths(result.tx, keys); + } + + public reportResult({ result, shouldRemove }: OrderingReport) { + if (result.hooksStatus.toBoolean() || result.tx.isMessage) { + // Included + this.ordered += 1; + this.results.push({ + status: "included", + result, + tx: result.tx, + }); + this.resolvePaths(result); + } else if (shouldRemove) { + // Dropped + this.results.push({ + status: "shouldRemove", + tx: result.tx, + }); + } else { + // Might become valid + this.results.push({ + status: "skipped", + tx: result.tx, + }); + this.pushFailed(result); + } + } + + private space() { + return this.sizeLimit - this.ordered; + } + + private mandoQueue: PendingTransaction[] = []; + + public async requestNextTransaction() { + // Fetch messages + if (!this.mandatoryTransactionsCompleted) { + const mandos = await this.mempool.getMandatoryTxs(); + this.mandoQueue.push(...mandos); + this.mandatoryTransactionsCompleted = true; + } + + if (this.mandoQueue.length > 0) { + return this.mandoQueue.shift(); + } + + const space = this.space(); + if (space > 0) { + if (this.transactionQueue.length === 0) { + // Fetch as many txs as space is availabe + const newTxs = await this.mempool.getTxs(this.userTxOffset, space); + this.userTxOffset += space; + this.transactionQueue.push(...newTxs); + } + + return this.transactionQueue.shift(); + } else { + return undefined; + } + } + + public getResults() { + const results = this.results + .reverse() + .filter( + distinctByPredicate( + (x, y) => x.tx.hash().toBigInt() === y.tx.hash().toBigInt() + ) + ) + .reverse(); + + return { + results, + orderingMetadata: { + skippedPaths: this.pathResolution.retrieveUnresolved((tx) => + tx.hash().toString() + ), + allChangedPaths: Array.from(this.allChangedPaths), + }, + }; + } +} diff --git a/packages/sequencer/src/protocol/production/sequencing/TransactionExecutionService.ts b/packages/sequencer/src/protocol/production/sequencing/TransactionExecutionService.ts index 753cfbb8f..4fffe2859 100644 --- a/packages/sequencer/src/protocol/production/sequencing/TransactionExecutionService.ts +++ b/packages/sequencer/src/protocol/production/sequencing/TransactionExecutionService.ts @@ -13,8 +13,6 @@ import { MandatoryProtocolModulesRecord, reduceStateTransitions, StateTransition, - BlockProver, - BlockProverProgrammable, BeforeTransactionHookArguments, AfterTransactionHookArguments, BlockProverState, @@ -23,9 +21,13 @@ import { toAfterTransactionHookArgument, ProvableStateTransition, DefaultProvableHashList, + addTransactionToBundle, + TransactionProverState, + TransactionHashList, + MinaActionsHashList, } from "@proto-kit/protocol"; import { Bool, Field } from "o1js"; -import { AreProofsEnabled, log, mapSequential } from "@proto-kit/common"; +import { log, mapSequential } from "@proto-kit/common"; import { MethodParameterEncoder, Runtime, @@ -59,25 +61,28 @@ export type RuntimeContextReducedExecutionResult = Pick< >; export type BlockTrackers = Pick< - BlockProverState, - | "transactionList" - | "eternalTransactionsList" - | "incomingMessages" - | "blockHashRoot" ->; - -function getAreProofsEnabledFromModule( - module: RuntimeModule -): AreProofsEnabled { - if (module.parent === undefined) { - throw new Error("Runtime on RuntimeModule not set"); - } - if (module.parent.areProofsEnabled === undefined) { - throw new Error("AppChain on Runtime not set"); - } - const { areProofsEnabled } = module.parent; - return areProofsEnabled; -} + TransactionProverState, + "eternalTransactionsList" | "incomingMessages" | "transactionList" +> & + Pick; + +// eslint-disable-next-line @typescript-eslint/no-redeclare +export const BlockTrackers = { + clone: (trackers: BlockTrackers) => { + return { + eternalTransactionsList: new TransactionHashList( + trackers.eternalTransactionsList.commitment + ), + transactionList: new TransactionHashList( + trackers.transactionList.commitment + ), + incomingMessages: new MinaActionsHashList( + trackers.incomingMessages.commitment + ), + blockHashRoot: trackers.blockHashRoot, + } satisfies BlockTrackers; + }, +}; async function decodeTransaction( tx: PendingTransaction, @@ -114,14 +119,14 @@ async function decodeTransaction( } function extractEvents( - runtimeResult: RuntimeContextReducedExecutionResult, + events: RuntimeContextReducedExecutionResult["events"], source: "afterTxHook" | "beforeTxHook" | "runtime" ): { eventName: string; data: Field[]; source: "afterTxHook" | "beforeTxHook" | "runtime"; }[] { - return runtimeResult.events.reduce( + return events.reduce( (acc, event) => { if (event.condition.toBoolean()) { const obj = { @@ -200,6 +205,8 @@ function traceLogSTs(msg: string, stateTransitions: StateTransition[]) { export type TransactionExecutionResultStatus = | { result: TransactionExecutionResult; + // Just for convenience + tx: PendingTransaction; status: "included"; } | { tx: PendingTransaction; status: "skipped" } @@ -210,10 +217,6 @@ export type TransactionExecutionResultStatus = export class TransactionExecutionService { private readonly transactionHooks: ProvableTransactionHook[]; - private readonly blockProver: BlockProverProgrammable; - - private readonly txHooks: ProvableTransactionHook[]; - public constructor( @inject("Runtime") private readonly runtime: Runtime, @inject("Protocol") @@ -227,13 +230,6 @@ export class TransactionExecutionService { this.transactionHooks = protocol.dependencyContainer.resolveAll( "ProvableTransactionHook" ); - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - this.blockProver = (protocol.blockProver as BlockProver).zkProgrammable; - - this.txHooks = - protocol.dependencyContainer.resolveAll( - "ProvableTransactionHook" - ); } private async executeRuntimeMethod( @@ -278,7 +274,7 @@ export class TransactionExecutionService { ); }), { - transaction: hookArguments.transaction, + transaction: hookArguments.transaction.transaction, networkState: hookArguments.networkState, }, runSimulated @@ -320,90 +316,13 @@ export class TransactionExecutionService { ): BlockTrackers { const signedTransaction = tx.toProtocolTransaction(); // Add tx to commitments - return this.blockProver.addTransactionToBundle( + return addTransactionToBundle( state, Bool(tx.isMessage), signedTransaction.transaction ); } - // eslint-disable-next-line sonarjs/cognitive-complexity - public async createExecutionTraces( - asyncStateService: CachedStateService, - transactions: PendingTransaction[], - networkState: NetworkState, - state: BlockTrackers - ): Promise<{ - blockState: BlockTrackers; - executionResults: TransactionExecutionResultStatus[]; - }> { - let blockState = state; - const executionResults: TransactionExecutionResultStatus[] = []; - - const networkStateHash = networkState.hash(); - - for (const tx of transactions) { - try { - const newState = this.addTransactionToBlockProverState(blockState, tx); - - // Create execution trace - const { result: executionTrace, shouldRemove } = - // eslint-disable-next-line no-await-in-loop - await this.createExecutionTrace( - asyncStateService, - tx, - { networkState, hash: networkStateHash }, - blockState, - newState - ); - - // If the hooks fail AND the tx is not a message (in which case we - // have to still execute it), we skip this tx and don't add it to the block - if ( - !executionTrace.hooksStatus.toBoolean() && - !executionTrace.tx.isMessage - ) { - const actionMessage = shouldRemove - ? "removing as to removeWhen hooks" - : "skipping"; - log.error( - `Error in inclusion of tx, ${actionMessage}: Protocol hooks not executable: ${executionTrace.statusMessage ?? "unknown reason"}` - ); - executionResults.push({ - tx, - status: shouldRemove ? "shouldRemove" : "skipped", - }); - } else { - blockState = newState; - - // Push result to results and transaction onto bundle-hash - executionResults.push({ result: executionTrace, status: "included" }); - } - } catch (error) { - if (error instanceof Error) { - log.error("Error in inclusion of tx, dropping", error); - executionResults.push({ tx, status: "shouldRemove" }); - } - } - } - - return { blockState, executionResults }; - } - - private async shouldRemove( - state: CachedStateService, - args: BeforeTransactionHookArguments - ) { - this.stateServiceProvider.setCurrentStateService(state); - - const returnValues = await mapSequential(this.transactionHooks, (hook) => - hook.removeTransactionWhen(args) - ); - - this.stateServiceProvider.popCurrentStateService(); - return returnValues.some((x) => x); - } - @trace("block.transaction", ([, tx, { networkState }]) => ({ height: networkState.block.height.toString(), methodId: tx.methodId.toString(), @@ -418,28 +337,13 @@ export class TransactionExecutionService { }: { networkState: NetworkState; hash: Field }, state: BlockTrackers, newState: BlockTrackers - ): Promise<{ result: TransactionExecutionResult; shouldRemove: boolean }> { - // TODO Use RecordingStateService -> async asProver needed - const recordingStateService = new CachedStateService(asyncStateService); - - const { method, args, module } = await decodeTransaction(tx, this.runtime); - - // Disable proof generation for sequencing the runtime - // TODO Is that even needed? - const appChain = getAreProofsEnabledFromModule(module); - const previousProofsEnabled = appChain.areProofsEnabled; - appChain.setProofsEnabled(false); - + ): Promise { const signedTransaction = tx.toProtocolTransaction(); - const runtimeContextInputs = { - transaction: signedTransaction.transaction, - networkState, - }; // The following steps generate and apply the correct STs with the right values - this.stateServiceProvider.setCurrentStateService(recordingStateService); + this.stateServiceProvider.setCurrentStateService(asyncStateService); - // Execute beforeTransaction hooks + // 1. beforeTransaction hooks const beforeTxArguments = toBeforeTransactionHookArgument( signedTransaction, networkState, @@ -456,12 +360,22 @@ export class TransactionExecutionService { "beforeTx" ) ); - const beforeHookEvents = extractEvents(beforeTxHookResult, "beforeTxHook"); + const beforeHookEvents = extractEvents( + beforeTxHookResult.events, + "beforeTxHook" + ); - await recordingStateService.applyStateTransitions( + await asyncStateService.applyStateTransitions( beforeTxHookResult.stateTransitions ); + // 2. Runtime + const { method, args } = await decodeTransaction(tx, this.runtime); + const runtimeContextInputs = { + transaction: signedTransaction.transaction, + networkState, + }; + const runtimeResult = await this.tracer.trace( "block.transaction.execute", () => this.executeRuntimeMethod(method, args, runtimeContextInputs) @@ -471,7 +385,7 @@ export class TransactionExecutionService { // Apply runtime STs (only if the tx succeeded) if (runtimeResult.status.toBoolean()) { // Apply protocol STs - await recordingStateService.applyStateTransitions( + await asyncStateService.applyStateTransitions( runtimeResult.stateTransitions ); } @@ -481,7 +395,7 @@ export class TransactionExecutionService { runtimeResult.stateTransitions ); - // Execute afterTransaction hook + // 3. afterTransaction hook const afterTxArguments = toAfterTransactionHookArgument( signedTransaction, networkState, @@ -505,33 +419,23 @@ export class TransactionExecutionService { "afterTx" ) ); - const afterHookEvents = extractEvents(afterTxHookResult, "afterTxHook"); - await recordingStateService.applyStateTransitions( + const afterHookEvents = extractEvents( + afterTxHookResult.events, + "afterTxHook" + ); + await asyncStateService.applyStateTransitions( afterTxHookResult.stateTransitions ); const txHooksValid = beforeTxHookResult.status.toBoolean() && afterTxHookResult.status.toBoolean(); - let shouldRemove = false; - if (txHooksValid) { - await recordingStateService.mergeIntoParent(); - } else { - // Execute removeWhen to determine whether it should be dropped - shouldRemove = await this.shouldRemove( - asyncStateService, - beforeTxArguments - ); - } // Reset global stateservice this.stateServiceProvider.popCurrentStateService(); - // Reset proofs enabled - appChain.setProofsEnabled(previousProofsEnabled); - // Extract sequencing results - const runtimeResultEvents = extractEvents(runtimeResult, "runtime"); + const runtimeResultEvents = extractEvents(runtimeResult.events, "runtime"); const stateTransitions = this.buildSTBatches( [ beforeTxHookResult.stateTransitions, @@ -542,19 +446,16 @@ export class TransactionExecutionService { ); return { - result: { - tx, - hooksStatus: Bool(txHooksValid), - status: runtimeResult.status, - statusMessage: - beforeTxHookResult.statusMessage ?? - afterTxHookResult.statusMessage ?? - runtimeResult.statusMessage, - - stateTransitions, - events: beforeHookEvents.concat(runtimeResultEvents, afterHookEvents), - }, - shouldRemove, + tx, + hooksStatus: Bool(txHooksValid), + status: runtimeResult.status, + statusMessage: + beforeTxHookResult.statusMessage ?? + afterTxHookResult.statusMessage ?? + runtimeResult.statusMessage, + + stateTransitions, + events: beforeHookEvents.concat(runtimeResultEvents, afterHookEvents), }; } } diff --git a/packages/sequencer/src/protocol/production/tasks/BlockReductionTask.ts b/packages/sequencer/src/protocol/production/tasks/BlockReductionTask.ts index 068bd3488..24b5423a0 100644 --- a/packages/sequencer/src/protocol/production/tasks/BlockReductionTask.ts +++ b/packages/sequencer/src/protocol/production/tasks/BlockReductionTask.ts @@ -18,7 +18,6 @@ import { PairTuple, ProofTaskSerializer, } from "../../../helpers/utils"; -import { VerificationKeyService } from "../../runtime/RuntimeVerificationKeyService"; @injectable() @scoped(Lifecycle.ContainerScoped) @@ -36,8 +35,7 @@ export class BlockReductionTask MandatoryProtocolModulesRecord & ProtocolModulesRecord >, private readonly executionContext: ProvableMethodExecutionContext, - private readonly compileRegistry: CompileRegistry, - private readonly verificationKeyService: VerificationKeyService + private readonly compileRegistry: CompileRegistry ) { super(); this.blockProver = this.protocol.blockProver; diff --git a/packages/sequencer/src/protocol/production/tasks/CircuitCompilerTask.ts b/packages/sequencer/src/protocol/production/tasks/CircuitCompilerTask.ts index f3a06bd72..2e14928d8 100644 --- a/packages/sequencer/src/protocol/production/tasks/CircuitCompilerTask.ts +++ b/packages/sequencer/src/protocol/production/tasks/CircuitCompilerTask.ts @@ -15,8 +15,10 @@ import { Protocol, SettlementContractModule, RuntimeVerificationKeyRootService, - SettlementSmartContractBase, MandatoryProtocolModulesRecord, + type SettlementModulesRecord, + BridgingSettlementContractArgs, + ContractArgsRegistry, } from "@proto-kit/protocol"; import { TaskSerializer } from "../../../worker/flow/Task"; @@ -48,7 +50,8 @@ export class CircuitCompilerTask extends UnpreparingTask< @inject("Runtime") protected readonly runtime: Runtime, @inject("Protocol") protected readonly protocol: Protocol, - private readonly compileRegistry: CompileRegistry + private readonly compileRegistry: CompileRegistry, + private readonly contractArgsRegistry: ContractArgsRegistry ) { super(); } @@ -97,7 +100,7 @@ export class CircuitCompilerTask extends UnpreparingTask< const container = this.protocol.dependencyContainer; if (container.isRegistered("SettlementContractModule")) { const settlementModule = container.resolve< - SettlementContractModule + SettlementContractModule >("SettlementContractModule"); // Needed so that all contractFactory functions are called, because @@ -115,9 +118,10 @@ export class CircuitCompilerTask extends UnpreparingTask< const sumModule = { compile: async (registry: CompileRegistry) => { - await reduceSequential( - modules.map(([, module]) => module), - async (record, module) => { + await reduceSequential<[string, CompilableModule], ArtifactRecord>( + modules, + async (record, [moduleName, module]) => { + log.info(`Compiling ${moduleName}`); const artifacts = await module.compile(registry); return { ...record, @@ -129,9 +133,9 @@ export class CircuitCompilerTask extends UnpreparingTask< }, }; - modules.push(["Settlement", sumModule]); + const combinedModules = [...modules, ["Settlement", sumModule]]; - return Object.fromEntries(modules); + return Object.fromEntries(combinedModules); } return {}; } @@ -150,16 +154,17 @@ export class CircuitCompilerTask extends UnpreparingTask< } if (input.isSignedSettlement !== undefined) { - const contractArgs = SettlementSmartContractBase.args; - SettlementSmartContractBase.args = { - ...contractArgs, - signedSettlements: input.isSignedSettlement, - // TODO Add distinction between mina and custom tokens - BridgeContractPermissions: (input.isSignedSettlement - ? new SignedSettlementPermissions() - : new ProvenSettlementPermissions() - ).bridgeContractMina(), - }; + this.contractArgsRegistry.addArgs( + "SettlementContract", + { + signedSettlements: input.isSignedSettlement, + // TODO Add distinction between mina and custom tokens + BridgeContractPermissions: (input.isSignedSettlement + ? new SignedSettlementPermissions() + : new ProvenSettlementPermissions() + ).bridgeContractMina(), + } + ); } // TODO make adaptive diff --git a/packages/sequencer/src/protocol/production/tasks/NewBlockTask.ts b/packages/sequencer/src/protocol/production/tasks/NewBlockTask.ts index 460d0c337..dfa151eaf 100644 --- a/packages/sequencer/src/protocol/production/tasks/NewBlockTask.ts +++ b/packages/sequencer/src/protocol/production/tasks/NewBlockTask.ts @@ -2,16 +2,21 @@ import { inject, injectable, Lifecycle, scoped } from "tsyringe"; import { BlockProvable, BlockProverPublicInput, - BlockProverPublicOutput, NetworkState, Protocol, StateTransitionProof, StateTransitionProvable, BlockHashMerkleTreeWitness, MandatoryProtocolModulesRecord, - WitnessedRootWitness, + TransactionProof, + BlockProof, + TransactionProvable, + BlockArguments, + BlockArgumentsBatch, + BlockProverStateInput, + BLOCK_ARGUMENT_BATCH_SIZE, } from "@proto-kit/protocol"; -import { Bool, Proof } from "o1js"; +import { Bool, Provable } from "o1js"; import { ProvableMethodExecutionContext, CompileRegistry, @@ -26,21 +31,25 @@ import type { TaskStateRecord } from "../tracing/BlockTracingService"; import { NewBlockProvingParametersSerializer } from "./serializers/NewBlockProvingParametersSerializer"; import { executeWithPrefilledStateService } from "./TransactionProvingTask"; -type BlockProof = Proof; +export type NewBlockArguments = { + args: BlockArguments; + startingStateBeforeHook: TaskStateRecord; + startingStateAfterHook: TaskStateRecord; +}; export interface NewBlockProverParameters { publicInput: BlockProverPublicInput; + stateWitness: BlockProverStateInput; networkState: NetworkState; blockWitness: BlockHashMerkleTreeWitness; deferSTProof: Bool; - afterBlockRootWitness: WitnessedRootWitness; - startingStateBeforeHook: TaskStateRecord; - startingStateAfterHook: TaskStateRecord; + deferTransactionProof: Bool; + blocks: NewBlockArguments[]; } export type NewBlockProvingParameters = PairingDerivedInput< StateTransitionProof, - BlockProof, + TransactionProof, NewBlockProverParameters >; @@ -52,6 +61,8 @@ export class NewBlockTask { private readonly stateTransitionProver: StateTransitionProvable; + private readonly transactionProver: TransactionProvable; + private readonly blockProver: BlockProvable; public readonly name = "newBlock"; @@ -64,7 +75,8 @@ export class NewBlockTask ) { super(); this.stateTransitionProver = protocol.stateTransitionProver; - this.blockProver = this.protocol.blockProver; + this.transactionProver = protocol.transactionProver; + this.blockProver = protocol.blockProver; } public inputSerializer(): TaskSerializer { @@ -72,13 +84,13 @@ export class NewBlockTask this.stateTransitionProver.zkProgrammable.zkProgram[0].Proof ); - const blockProofSerializer = new ProofTaskSerializer( - this.blockProver.zkProgrammable.zkProgram[0].Proof + const transactionProofSerializer = new ProofTaskSerializer( + this.transactionProver.zkProgrammable.zkProgram[0].Proof ); return new NewBlockProvingParametersSerializer( stProofSerializer, - blockProofSerializer + transactionProofSerializer ); } @@ -93,39 +105,71 @@ export class NewBlockTask const { networkState, blockWitness, - startingStateBeforeHook, - startingStateAfterHook, publicInput, + stateWitness, deferSTProof, - afterBlockRootWitness, + deferTransactionProof, + blocks, } = parameters; - await this.blockProver.proveBlock( - publicInput, - networkState, - blockWitness, - input1, - deferSTProof, - afterBlockRootWitness, - input2 - ); + if (blocks.length !== BLOCK_ARGUMENT_BATCH_SIZE) { + throw new Error("Given block argument length not exactly batch size"); + } + + const blockArgumentBatch = new BlockArgumentsBatch({ + batch: blocks.map((block) => block.args), + }); + + const stateRecords = blocks.flatMap((block) => [ + block.startingStateBeforeHook, + block.startingStateAfterHook, + ]); await executeWithPrefilledStateService( this.protocol.stateServiceProvider, - [startingStateBeforeHook, startingStateAfterHook], - async () => {} + stateRecords, + async () => { + if (deferSTProof.toBoolean() && deferTransactionProof.toBoolean()) { + await this.blockProver.proveBlockBatchNoProofs( + publicInput, + stateWitness, + networkState, + blockWitness, + blockArgumentBatch, + Bool(false) + // deferSTProof.or(deferTransactionProof) + ); + } else { + await this.blockProver.proveBlockBatchWithProofs( + publicInput, + stateWitness, + networkState, + blockWitness, + blockArgumentBatch, + deferSTProof, + deferTransactionProof, + input1, + input2 + ); + } + } ); - return await executeWithPrefilledStateService( + const proof = await executeWithPrefilledStateService( this.protocol.stateServiceProvider, - [startingStateBeforeHook, startingStateAfterHook], + stateRecords, async () => await this.executionContext.current().result.prove() ); + + Provable.log("Input", proof.publicInput); + Provable.log("Output", proof.publicOutput); + + return proof; } public async prepare(): Promise { // Compile - await this.blockProver.compile(this.compileRegistry); + await this.transactionProver.compile(this.compileRegistry); } } diff --git a/packages/sequencer/src/protocol/production/tasks/StateTransitionTask.ts b/packages/sequencer/src/protocol/production/tasks/StateTransitionTask.ts index b7281c649..532a0d5d4 100644 --- a/packages/sequencer/src/protocol/production/tasks/StateTransitionTask.ts +++ b/packages/sequencer/src/protocol/production/tasks/StateTransitionTask.ts @@ -9,10 +9,8 @@ import { StateTransitionProvable, StateTransitionProvableBatch, StateTransitionProverPublicInput, - StateTransitionProverPublicOutput, } from "@proto-kit/protocol"; import { - log, ProvableMethodExecutionContext, CompileRegistry, LinkedMerkleTreeWitness, @@ -66,16 +64,12 @@ export class StateTransitionTask public async compute( input: StateTransitionProofParameters ): Promise { - const output = await this.stateTransitionProver.proveBatch( + await this.stateTransitionProver.proveBatch( input.publicInput, input.batch, new MerkleWitnessBatch({ witnesses: input.merkleWitnesses.slice() }), input.batchState ); - log.debug("STTask public io:", { - input: StateTransitionProverPublicInput.toJSON(input.publicInput), - output: StateTransitionProverPublicOutput.toJSON(output), - }); return await this.executionContext .current() diff --git a/packages/sequencer/src/protocol/production/tasks/TransactionProvingTask.ts b/packages/sequencer/src/protocol/production/tasks/TransactionProvingTask.ts index 38473a22c..5f6896b99 100644 --- a/packages/sequencer/src/protocol/production/tasks/TransactionProvingTask.ts +++ b/packages/sequencer/src/protocol/production/tasks/TransactionProvingTask.ts @@ -1,11 +1,12 @@ import { - BlockProof, - BlockProvable, MandatoryProtocolModulesRecord, Protocol, ProtocolModulesRecord, StateServiceProvider, DynamicRuntimeProof, + TransactionProvable, + TransactionProof, + TransactionProverPublicInput, } from "@proto-kit/protocol"; import { Runtime } from "@proto-kit/module"; import { inject, injectable, Lifecycle, scoped } from "tsyringe"; @@ -21,10 +22,7 @@ import { TaskWorkerModule } from "../../../worker/worker/TaskWorkerModule"; import type { TaskStateRecord } from "../tracing/BlockTracingService"; import { TransactionProvingTaskParameterSerializer } from "./serializers/TransactionProvingTaskParameterSerializer"; -import { - TransactionProvingTaskParameters, - TransactionProvingType, -} from "./serializers/types/TransactionProvingTypes"; +import { TransactionProvingTaskParameters } from "./serializers/types/TransactionProvingTypes"; export async function executeWithPrefilledStateService( stateServiceProvider: StateServiceProvider, @@ -53,14 +51,14 @@ export async function executeWithPrefilledStateService( @scoped(Lifecycle.ContainerScoped) export class TransactionProvingTask extends TaskWorkerModule - implements Task + implements Task { - private readonly blockProver: BlockProvable; + private readonly transactionProver: TransactionProvable; private readonly runtimeProofType = this.runtime.zkProgrammable.zkProgram[0].Proof; - public name = "block"; + public name = "transaction"; public constructor( @inject("Protocol") @@ -72,7 +70,7 @@ export class TransactionProvingTask private readonly compileRegistry: CompileRegistry ) { super(); - this.blockProver = protocol.blockProver; + this.transactionProver = protocol.transactionProver; } public inputSerializer(): TaskSerializer { @@ -84,35 +82,63 @@ export class TransactionProvingTask ); } - public resultSerializer(): TaskSerializer { + public resultSerializer(): TaskSerializer { return new ProofTaskSerializer( - this.blockProver.zkProgrammable.zkProgram[0].Proof + this.transactionProver.zkProgrammable.zkProgram[0].Proof + ); + } + + private async computeDummy(): Promise { + await executeWithPrefilledStateService( + this.protocol.stateServiceProvider, + [{}, {}], + async () => { + await this.transactionProver.dummy( + TransactionProverPublicInput.empty() + ); + } + ); + + return await executeWithPrefilledStateService( + this.protocol.stateServiceProvider, + [{}, {}], + async () => + await this.executionContext.current().result.prove() ); } public async compute( input: TransactionProvingTaskParameters - ): Promise { + ): Promise { + if (input === "dummy") { + return await this.computeDummy(); + } + + const startingState = input.flatMap((i) => i.parameters.startingState); + await executeWithPrefilledStateService( this.protocol.stateServiceProvider, - input.parameters.startingState, + startingState, async () => { - const { type, parameters } = input; + const { parameters, proof } = input[0]; - const proof1 = DynamicRuntimeProof.fromProof(input.proof1); + const proof1 = DynamicRuntimeProof.fromProof(proof); - if (type === TransactionProvingType.SINGLE) { - await this.blockProver.proveTransaction( + if (input.length === 1) { + await this.transactionProver.proveTransaction( parameters.publicInput, proof1, parameters.executionData ); } else { - await this.blockProver.proveTransactions( + const { parameters: parameters2, proof: proof2 } = input[1]; + + await this.transactionProver.proveTransactions( parameters.publicInput, proof1, - DynamicRuntimeProof.fromProof(input.proof2), - parameters.executionData + DynamicRuntimeProof.fromProof(proof2), + parameters.executionData, + parameters2.executionData ); } } @@ -120,14 +146,14 @@ export class TransactionProvingTask return await executeWithPrefilledStateService( this.protocol.stateServiceProvider, - input.parameters.startingState, + startingState, async () => - await this.executionContext.current().result.prove() + await this.executionContext.current().result.prove() ); } public async prepare(): Promise { // Compile - await this.blockProver.compile(this.compileRegistry); + await this.transactionProver.compile(this.compileRegistry); } } diff --git a/packages/sequencer/src/protocol/production/tasks/TransactionReductionTask.ts b/packages/sequencer/src/protocol/production/tasks/TransactionReductionTask.ts new file mode 100644 index 000000000..46e5ddac9 --- /dev/null +++ b/packages/sequencer/src/protocol/production/tasks/TransactionReductionTask.ts @@ -0,0 +1,69 @@ +import { inject, injectable, Lifecycle, scoped } from "tsyringe"; +import { + MandatoryProtocolModulesRecord, + Protocol, + ProtocolModulesRecord, + TransactionProof, + TransactionProvable, +} from "@proto-kit/protocol"; +import { + CompileRegistry, + ProvableMethodExecutionContext, +} from "@proto-kit/common"; + +import { TaskWorkerModule } from "../../../worker/worker/TaskWorkerModule"; +import { Task, TaskSerializer } from "../../../worker/flow/Task"; +import { + PairProofTaskSerializer, + PairTuple, + ProofTaskSerializer, +} from "../../../helpers/utils"; + +@injectable() +@scoped(Lifecycle.ContainerScoped) +export class TransactionReductionTask + extends TaskWorkerModule + implements Task, TransactionProof> +{ + private readonly transactionProver: TransactionProvable; + + public name = "transactionReduction"; + + public constructor( + @inject("Protocol") + private readonly protocol: Protocol< + MandatoryProtocolModulesRecord & ProtocolModulesRecord + >, + private readonly executionContext: ProvableMethodExecutionContext, + private readonly compileRegistry: CompileRegistry + ) { + super(); + this.transactionProver = this.protocol.transactionProver; + } + + public inputSerializer(): TaskSerializer> { + return new PairProofTaskSerializer( + this.transactionProver.zkProgrammable.zkProgram[0].Proof + ); + } + + public resultSerializer(): TaskSerializer { + return new ProofTaskSerializer( + this.transactionProver.zkProgrammable.zkProgram[0].Proof + ); + } + + public async compute( + input: PairTuple + ): Promise { + const [r1, r2] = input; + await this.transactionProver.merge(r1.publicInput, r1, r2); + return await this.executionContext + .current() + .result.prove(); + } + + public async prepare(): Promise { + await this.transactionProver.compile(this.compileRegistry); + } +} diff --git a/packages/sequencer/src/protocol/production/tasks/serializers/NewBlockProvingParametersSerializer.ts b/packages/sequencer/src/protocol/production/tasks/serializers/NewBlockProvingParametersSerializer.ts index c8a1f2640..3c59f730d 100644 --- a/packages/sequencer/src/protocol/production/tasks/serializers/NewBlockProvingParametersSerializer.ts +++ b/packages/sequencer/src/protocol/production/tasks/serializers/NewBlockProvingParametersSerializer.ts @@ -1,14 +1,16 @@ import { + BlockArguments, BlockHashMerkleTreeWitness, - BlockProof, BlockProverPublicInput, - BlockProverPublicOutput, + BlockProverStateInput, NetworkState, ReturnType, StateTransitionProof, StateTransitionProverPublicInput, StateTransitionProverPublicOutput, - WitnessedRootWitness, + TransactionProof, + TransactionProverPublicInput, + TransactionProverPublicOutput, } from "@proto-kit/protocol"; import { Bool } from "o1js"; @@ -27,18 +29,22 @@ interface JsonType { input2: string; params: { publicInput: ReturnType; + stateWitness: ReturnType; networkState: ReturnType; blockWitness: ReturnType; - startingStateBeforeHook: JSONEncodableState; - startingStateAfterHook: JSONEncodableState; deferSTProof: boolean; - afterBlockRootWitness: ReturnType; + deferTransactionProof: boolean; + blocks: { + startingStateBeforeHook: JSONEncodableState; + startingStateAfterHook: JSONEncodableState; + args: ReturnType; + }[]; }; } type NewBlockPayload = PairingDerivedInput< StateTransitionProof, - BlockProof, + TransactionProof, NewBlockProverParameters >; @@ -50,39 +56,44 @@ export class NewBlockProvingParametersSerializer StateTransitionProverPublicInput, StateTransitionProverPublicOutput >, - private readonly blockProofSerializer: ProofTaskSerializer< - BlockProverPublicInput, - BlockProverPublicOutput + private readonly transactionProofSerializer: ProofTaskSerializer< + TransactionProverPublicInput, + TransactionProverPublicOutput > ) {} public toJSON(input: NewBlockPayload) { return JSON.stringify({ input1: this.stProofSerializer.toJSON(input.input1), - input2: this.blockProofSerializer.toJSON(input.input2), + input2: this.transactionProofSerializer.toJSON(input.input2), params: { publicInput: BlockProverPublicInput.toJSON(input.params.publicInput), + stateWitness: BlockProverStateInput.toJSON(input.params.stateWitness), + networkState: NetworkState.toJSON(input.params.networkState), blockWitness: BlockHashMerkleTreeWitness.toJSON( input.params.blockWitness ), - startingStateBeforeHook: DecodedStateSerializer.toJSON( - input.params.startingStateBeforeHook - ), + blocks: input.params.blocks.map((block) => { + return { + startingStateBeforeHook: DecodedStateSerializer.toJSON( + block.startingStateBeforeHook + ), - startingStateAfterHook: DecodedStateSerializer.toJSON( - input.params.startingStateAfterHook - ), + startingStateAfterHook: DecodedStateSerializer.toJSON( + block.startingStateAfterHook + ), - deferSTProof: input.params.deferSTProof.toBoolean(), + args: BlockArguments.toJSON(block.args), + }; + }), - afterBlockRootWitness: WitnessedRootWitness.toJSON( - input.params.afterBlockRootWitness - ), + deferSTProof: input.params.deferSTProof.toBoolean(), + deferTransactionProof: input.params.deferTransactionProof.toBoolean(), }, } satisfies JsonType); } @@ -92,11 +103,15 @@ export class NewBlockProvingParametersSerializer const jsonObject: JsonType = JSON.parse(json); return { input1: await this.stProofSerializer.fromJSON(jsonObject.input1), - input2: await this.blockProofSerializer.fromJSON(jsonObject.input2), + input2: await this.transactionProofSerializer.fromJSON(jsonObject.input2), params: { - publicInput: BlockProverPublicInput.fromJSON( - jsonObject.params.publicInput + publicInput: new BlockProverPublicInput( + BlockProverPublicInput.fromJSON(jsonObject.params.publicInput) + ), + + stateWitness: new BlockProverStateInput( + BlockProverStateInput.fromJSON(jsonObject.params.stateWitness) ), networkState: new NetworkState( @@ -107,19 +122,22 @@ export class NewBlockProvingParametersSerializer BlockHashMerkleTreeWitness.fromJSON(jsonObject.params.blockWitness) ), - startingStateBeforeHook: DecodedStateSerializer.fromJSON( - jsonObject.params.startingStateBeforeHook - ), + blocks: jsonObject.params.blocks.map((block) => { + return { + startingStateBeforeHook: DecodedStateSerializer.fromJSON( + block.startingStateBeforeHook + ), - startingStateAfterHook: DecodedStateSerializer.fromJSON( - jsonObject.params.startingStateBeforeHook - ), + startingStateAfterHook: DecodedStateSerializer.fromJSON( + block.startingStateBeforeHook + ), - deferSTProof: Bool(jsonObject.params.deferSTProof), + args: BlockArguments.fromJSON(block.args), + }; + }), - afterBlockRootWitness: WitnessedRootWitness.fromJSON( - jsonObject.params.afterBlockRootWitness - ), + deferSTProof: Bool(jsonObject.params.deferSTProof), + deferTransactionProof: Bool(jsonObject.params.deferTransactionProof), }, }; } diff --git a/packages/sequencer/src/protocol/production/tasks/serializers/TransactionProvingTaskParameterSerializer.ts b/packages/sequencer/src/protocol/production/tasks/serializers/TransactionProvingTaskParameterSerializer.ts index da655d863..0f34205ad 100644 --- a/packages/sequencer/src/protocol/production/tasks/serializers/TransactionProvingTaskParameterSerializer.ts +++ b/packages/sequencer/src/protocol/production/tasks/serializers/TransactionProvingTaskParameterSerializer.ts @@ -1,19 +1,20 @@ import { - BlockProverPublicInput, - BlockProverTransactionArguments, MethodPublicOutput, - NetworkState, ReturnType, RuntimeTransaction, + TransactionProverPublicInput, + TransactionProverTransactionArguments, + TransactionProverArguments, } from "@proto-kit/protocol"; import { JsonProof, Signature } from "o1js"; +import { assertSizeOneOrTwo, mapSequential } from "@proto-kit/common"; import { TaskSerializer } from "../../../../worker/flow/Task"; import { ProofTaskSerializer } from "../../../../helpers/utils"; import { + TransactionProverTaskParameters, TransactionProvingTaskParameters, - TransactionProvingType, } from "./types/TransactionProvingTypes"; import { DecodedStateSerializer, @@ -21,7 +22,21 @@ import { } from "./DecodedStateSerializer"; import { RuntimeVerificationKeyAttestationSerializer } from "./RuntimeVerificationKeyAttestationSerializer"; -export type BlockProverTransactionArgumentsJSON = { +export type TransactionProvingTaskParametersJSON = { + parameters: TransactionProverTaskParametersJSON; + proof: JsonProof; +}[]; + +export type TransactionProverTaskParametersJSON = { + startingState: JSONEncodableState[]; + publicInput: ReturnType; + executionData: { + transaction: TransactionProverTransactionArgumentsJSON; + args: ReturnType; + }; +}; + +export type TransactionProverTransactionArgumentsJSON = { transaction: ReturnType; signature: ReturnType; verificationKeyAttestation: ReturnType< @@ -29,38 +44,6 @@ export type BlockProverTransactionArgumentsJSON = { >; }; -export type SingleExecutionDataJSON = { - transaction: BlockProverTransactionArgumentsJSON; - networkState: ReturnType; -}; - -export type MultiExecutionDataJSON = { - transaction1: BlockProverTransactionArgumentsJSON; - transaction2: BlockProverTransactionArgumentsJSON; - networkState: ReturnType; -}; - -export type TransactionProverTaskParametersJSON< - ExecutionData extends SingleExecutionDataJSON | MultiExecutionDataJSON, -> = { - startingState: JSONEncodableState[]; - publicInput: ReturnType; - executionData: ExecutionData; -}; - -export type TransactionProvingTaskParametersJSON = - | { - type: TransactionProvingType.SINGLE; - proof1: JsonProof; - parameters: TransactionProverTaskParametersJSON; - } - | { - type: TransactionProvingType.MULTI; - proof1: JsonProof; - proof2: JsonProof; - parameters: TransactionProverTaskParametersJSON; - }; - export class TransactionProvingTaskParameterSerializer implements TaskSerializer { @@ -71,9 +54,9 @@ export class TransactionProvingTaskParameterSerializer > ) {} - private blockProverArgumentsToJson( - args: BlockProverTransactionArguments - ): BlockProverTransactionArgumentsJSON { + private transactionProverArgumentsToJson( + args: TransactionProverTransactionArguments + ): TransactionProverTransactionArgumentsJSON { return { transaction: RuntimeTransaction.toJSON(args.transaction), // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment @@ -85,9 +68,9 @@ export class TransactionProvingTaskParameterSerializer }; } - private blockProverArgumentsFromJson( - args: BlockProverTransactionArgumentsJSON - ): BlockProverTransactionArguments { + private transactionProverArgumentsFromJson( + args: TransactionProverTransactionArgumentsJSON + ): TransactionProverTransactionArguments { return { transaction: new RuntimeTransaction( RuntimeTransaction.fromJSON(args.transaction) @@ -100,59 +83,39 @@ export class TransactionProvingTaskParameterSerializer }; } - public toJSON(input: TransactionProvingTaskParameters): string { - let taskParamsJson: TransactionProvingTaskParametersJSON; + public toJSON(inputs: TransactionProvingTaskParameters): string { + if (inputs === "dummy") { + return "dummy"; + } - const { type, parameters } = input; + const taskParamsJson: TransactionProvingTaskParametersJSON = inputs.map( + (input) => { + const { parameters, proof } = input; + const { executionData } = parameters; - const partialParameters = { - publicInput: BlockProverPublicInput.toJSON(parameters.publicInput), + const proofJSON = this.runtimeProofSerializer.toJSONProof(proof); - startingState: parameters.startingState.map((stateRecord) => - DecodedStateSerializer.toJSON(stateRecord) - ), - }; + const parametersJSON: TransactionProverTaskParametersJSON = { + publicInput: TransactionProverPublicInput.toJSON( + parameters.publicInput + ), - // The reason we can't just use the structs toJSON is that the VerificationKey - // toJSON and fromJSON isn't consistent -> i.e. the serialization doesn't work - // the same both ways. We fix that in our custom serializer - if (type === TransactionProvingType.SINGLE) { - const { executionData } = parameters; - const executionDataJson: SingleExecutionDataJSON = { - networkState: NetworkState.toJSON(executionData.networkState), - transaction: this.blockProverArgumentsToJson(executionData.transaction), - }; + startingState: parameters.startingState.map((stateRecord) => + DecodedStateSerializer.toJSON(stateRecord) + ), - taskParamsJson = { - type, - proof1: this.runtimeProofSerializer.toJSONProof(input.proof1), - parameters: { - ...partialParameters, - executionData: executionDataJson, - }, - }; - } else { - const { executionData } = parameters; - const executionDataJson: MultiExecutionDataJSON = { - networkState: NetworkState.toJSON(executionData.networkState), - transaction1: this.blockProverArgumentsToJson( - executionData.transaction1 - ), - transaction2: this.blockProverArgumentsToJson( - executionData.transaction2 - ), - }; + executionData: { + args: TransactionProverArguments.toJSON(executionData.args), - taskParamsJson = { - type, - proof1: this.runtimeProofSerializer.toJSONProof(input.proof1), - proof2: this.runtimeProofSerializer.toJSONProof(input.proof2), - parameters: { - ...partialParameters, - executionData: executionDataJson, - }, - }; - } + transaction: this.transactionProverArgumentsToJson( + executionData.transaction + ), + }, + }; + + return { parameters: parametersJSON, proof: proofJSON }; + } + ); return JSON.stringify(taskParamsJson); } @@ -160,62 +123,44 @@ export class TransactionProvingTaskParameterSerializer public async fromJSON( json: string ): Promise { + if (json === "dummy") { + return "dummy"; + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const jsonReadyObject: TransactionProvingTaskParametersJSON = JSON.parse(json); - const { type, parameters } = jsonReadyObject; - - const partialParameters = { - publicInput: BlockProverPublicInput.fromJSON(parameters.publicInput), + const result = await mapSequential(jsonReadyObject, async (input) => { + const { parameters, proof } = input; - startingState: parameters.startingState.map((stateRecord) => - DecodedStateSerializer.fromJSON(stateRecord) - ), - }; + const decodedProof = + await this.runtimeProofSerializer.fromJSONProof(proof); - if (type === TransactionProvingType.SINGLE) { - return { - type, - proof1: await this.runtimeProofSerializer.fromJSONProof( - jsonReadyObject.proof1 + const decodedParameters: TransactionProverTaskParameters = { + publicInput: TransactionProverPublicInput.fromJSON( + parameters.publicInput + ), + startingState: parameters.startingState.map((stateRecord) => + DecodedStateSerializer.fromJSON(stateRecord) ), - parameters: { - ...partialParameters, - executionData: { - transaction: this.blockProverArgumentsFromJson( - parameters.executionData.transaction - ), - networkState: new NetworkState( - NetworkState.fromJSON(parameters.executionData.networkState) - ), - }, - }, - }; - } - - return { - type, - proof1: await this.runtimeProofSerializer.fromJSONProof( - jsonReadyObject.proof1 - ), - proof2: await this.runtimeProofSerializer.fromJSONProof( - jsonReadyObject.proof2 - ), - parameters: { - ...partialParameters, executionData: { - transaction1: this.blockProverArgumentsFromJson( - parameters.executionData.transaction1 + transaction: this.transactionProverArgumentsFromJson( + parameters.executionData.transaction ), - transaction2: this.blockProverArgumentsFromJson( - parameters.executionData.transaction2 - ), - networkState: new NetworkState( - NetworkState.fromJSON(parameters.executionData.networkState) + args: TransactionProverArguments.fromJSON( + parameters.executionData.args ), }, - }, - }; + }; + return { + parameters: decodedParameters, + proof: decodedProof, + }; + }); + + assertSizeOneOrTwo(result); + + return result; } } diff --git a/packages/sequencer/src/protocol/production/tasks/serializers/types/TransactionProvingTypes.ts b/packages/sequencer/src/protocol/production/tasks/serializers/types/TransactionProvingTypes.ts index b091abdc2..ae4a2ac27 100644 --- a/packages/sequencer/src/protocol/production/tasks/serializers/types/TransactionProvingTypes.ts +++ b/packages/sequencer/src/protocol/production/tasks/serializers/types/TransactionProvingTypes.ts @@ -1,8 +1,7 @@ import { - BlockProverMultiTransactionExecutionData, - BlockProverPublicInput, - BlockProverSingleTransactionExecutionData, MethodPublicOutput, + TransactionProverExecutionData, + TransactionProverPublicInput, } from "@proto-kit/protocol"; import { Proof } from "o1js"; @@ -10,30 +9,17 @@ import type { TaskStateRecord } from "../../../tracing/BlockTracingService"; export type RuntimeProof = Proof; -export enum TransactionProvingType { - SINGLE, - MULTI, -} - -export interface TransactionProverTaskParameters< - ExecutionData extends - | BlockProverSingleTransactionExecutionData - | BlockProverMultiTransactionExecutionData, -> { - publicInput: BlockProverPublicInput; - executionData: ExecutionData; +export interface TransactionProverTaskParameters { + publicInput: TransactionProverPublicInput; + executionData: TransactionProverExecutionData; startingState: TaskStateRecord[]; } +export type OneOrTwo = [Type] | [Type, Type]; + export type TransactionProvingTaskParameters = - | { - type: TransactionProvingType.SINGLE; - parameters: TransactionProverTaskParameters; - proof1: RuntimeProof; - } - | { - type: TransactionProvingType.MULTI; - parameters: TransactionProverTaskParameters; - proof1: RuntimeProof; - proof2: RuntimeProof; - }; + | "dummy" + | OneOrTwo<{ + parameters: TransactionProverTaskParameters; + proof: RuntimeProof; + }>; diff --git a/packages/sequencer/src/protocol/production/tracing/BatchTracingService.ts b/packages/sequencer/src/protocol/production/tracing/BatchTracingService.ts index f54d52e6f..3df227201 100644 --- a/packages/sequencer/src/protocol/production/tracing/BatchTracingService.ts +++ b/packages/sequencer/src/protocol/production/tracing/BatchTracingService.ts @@ -1,29 +1,43 @@ -import { log, yieldSequential } from "@proto-kit/common"; +import { log, range, unzip, yieldSequential } from "@proto-kit/common"; import { AppliedBatchHashList, + BLOCK_ARGUMENT_BATCH_SIZE, MinaActionsHashList, TransactionHashList, WitnessedRootHashList, + BundleHashList, + BlockArguments, } from "@proto-kit/protocol"; import { inject, injectable } from "tsyringe"; +// eslint-disable-next-line import/no-extraneous-dependencies +import chunk from "lodash/chunk"; +import { Bool, Field } from "o1js"; import { StateTransitionProofParameters } from "../tasks/StateTransitionTask"; import { BlockWithResult } from "../../../storage/model/Block"; import { trace } from "../../../logging/trace"; import { Tracer } from "../../../logging/Tracer"; import { CachedLinkedLeafStore } from "../../../state/lmt/CachedLinkedLeafStore"; - import { - BlockTrace, - BlockTracingService, - BlockTracingState, -} from "./BlockTracingService"; + NewBlockArguments, + NewBlockProverParameters, +} from "../tasks/NewBlockTask"; + +import { BlockTracingService, BlockTracingState } from "./BlockTracingService"; import { StateTransitionTracingService } from "./StateTransitionTracingService"; +import { TransactionTrace } from "./TransactionTracingService"; -type BatchTracingState = Omit; +type BatchTracingState = BlockTracingState; + +export type BlockTrace = { + block: NewBlockProverParameters; + // Only for debugging and logging + heights: [string, string]; +}; export type BatchTrace = { blocks: BlockTrace[]; + transactions: TransactionTrace[]; stateTransitionTrace: StateTransitionProofParameters[]; }; @@ -40,12 +54,15 @@ export class BatchTracingService { return { pendingSTBatches: new AppliedBatchHashList(), witnessedRoots: new WitnessedRootHashList(), + bundleList: new BundleHashList(), stateRoot: block.block.fromStateRoot, eternalTransactionsList: new TransactionHashList( block.block.fromEternalTransactionsHash ), incomingMessages: new MinaActionsHashList(block.block.fromMessagesHash), networkState: block.block.networkState.before, + blockNumber: block.block.height, + blockHashRoot: block.block.fromBlockHashRoot, }; } @@ -55,27 +72,91 @@ export class BatchTracingService { const batchState = this.createBatchState(blocks[0]); + const publicInput = this.blockTracingService.openBatch( + batchState, + blocks[0] + ); + // Trace blocks const numBlocks = blocks.length; + const numBatches = Math.ceil(numBlocks / BLOCK_ARGUMENT_BATCH_SIZE); + const [, blockTraces] = await yieldSequential( - blocks, - async (state, block, index) => { - const blockProverState: BlockTracingState = { - ...state, - transactionList: new TransactionHashList(), + chunk(blocks, BLOCK_ARGUMENT_BATCH_SIZE), + async (state, batch, index) => { + // Trace batch of blocks fitting in single proof + const partialBlockTrace = this.blockTracingService.openBlock( + state, + batch[0], + publicInput + ); + const start = state.blockNumber.toString(); + + // PI is taken for first chunk of batch, all others use the stateWitness + // Copy here because we need a fresh instance + const currentPublicInput = publicInput.clone(); + if (index > 0) { + currentPublicInput.proverStateRemainder = + partialBlockTrace.stateWitness.hash(); + } + + const [newState, combinedTraces] = await yieldSequential( + batch, + async (state2, block, jndex) => { + const [newState2, blockTrace, transactions] = + await this.blockTracingService.traceBlock(state2, block); + return [ + newState2, + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + [blockTrace, transactions] as [ + NewBlockArguments, + TransactionTrace[], + ], + ]; + }, + state + ); + + const [blockArgumentBatch, transactionTraces] = unzip(combinedTraces); + + // Fill up with dummies + const dummyBlockArgs = BlockArguments.noop( + newState, + Field(blocks.at(-1)!.result.stateRoot) + ); + const dummies = range( + blockArgumentBatch.length, + BLOCK_ARGUMENT_BATCH_SIZE + ).map(() => ({ + args: dummyBlockArgs, + startingStateAfterHook: {}, + startingStateBeforeHook: {}, + })); + + const blockTrace: BlockTrace = { + block: { + ...partialBlockTrace, + publicInput: currentPublicInput, + blocks: blockArgumentBatch.concat(dummies), + deferTransactionProof: Bool(numBatches - 1 !== index), + deferSTProof: Bool(numBatches - 1 !== index), + }, + heights: [start, newState.blockNumber.toString()], }; - const [newState, blockTrace] = - await this.blockTracingService.traceBlock( - blockProverState, - block, - index === numBlocks - 1 - ); - return [newState, blockTrace]; + + return [ + newState, + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + [blockTrace, transactionTraces] as [BlockTrace, TransactionTrace[][]], + ]; }, batchState ); - return blockTraces; + return { + blockTraces: blockTraces.map(([x]) => x), + transactionTraces: blockTraces.map(([, x]) => x), + }; } @trace("batch.trace.transitions") @@ -102,20 +183,22 @@ export class BatchTracingService { batchId: number ): Promise { if (blocks.length === 0) { - return { blocks: [], stateTransitionTrace: [] }; + return { blocks: [], stateTransitionTrace: [], transactions: [] }; } // Traces the STs and the blocks in parallel, however not in separate processes // Therefore, we only optimize the idle time for async operations like DB reads - const [blockTraces, stateTransitionTrace] = await Promise.all([ - // Trace blocks - this.traceBlocks(blocks), - // Trace STs - this.traceStateTransitions(blocks, merkleTreeStore), - ]); + const [{ blockTraces, transactionTraces }, stateTransitionTrace] = + await Promise.all([ + // Trace blocks + this.traceBlocks(blocks), + // Trace STs + this.traceStateTransitions(blocks, merkleTreeStore), + ]); return { blocks: blockTraces, + transactions: transactionTraces.flat(2), stateTransitionTrace, }; } diff --git a/packages/sequencer/src/protocol/production/tracing/BlockTracingService.ts b/packages/sequencer/src/protocol/production/tracing/BlockTracingService.ts index 8b6a98bbf..98c02358f 100644 --- a/packages/sequencer/src/protocol/production/tracing/BlockTracingService.ts +++ b/packages/sequencer/src/protocol/production/tracing/BlockTracingService.ts @@ -1,17 +1,25 @@ import { + BlockArguments, BlockProverPublicInput, BlockProverState, + Bundle, + TransactionHashList, + TransactionProverState, WitnessedRootWitness, + BundleHashList, + BundlePreimage, + BlockProverStateInput, } from "@proto-kit/protocol"; import { Bool, Field } from "o1js"; import { toStateTransitionsHash } from "@proto-kit/module"; -import { yieldSequential } from "@proto-kit/common"; -// eslint-disable-next-line import/no-extraneous-dependencies -import chunk from "lodash/chunk"; +import { NonMethods, yieldSequential } from "@proto-kit/common"; import { inject, injectable } from "tsyringe"; import { BlockWithResult } from "../../../storage/model/Block"; -import type { NewBlockProverParameters } from "../tasks/NewBlockTask"; +import type { + NewBlockArguments, + NewBlockProverParameters, +} from "../tasks/NewBlockTask"; import { Tracer } from "../../../logging/Tracer"; import { trace } from "../../../logging/trace"; @@ -23,24 +31,10 @@ import { export type TaskStateRecord = Record; -export type BlockTracingState = Pick< - BlockProverState, - | "witnessedRoots" - | "stateRoot" - | "pendingSTBatches" - | "networkState" - | "transactionList" - | "eternalTransactionsList" - | "incomingMessages" +export type BlockTracingState = NonMethods< + Omit >; -export type BlockTrace = { - blockParams: NewBlockProverParameters; - transactions: TransactionTrace[]; - // Only for debugging and logging - height: string; -}; - @injectable() export class BlockTracingService { public constructor( @@ -49,38 +43,64 @@ export class BlockTracingService { public readonly tracer: Tracer ) {} + public openBatch( + state: BlockTracingState, + { block: firstBlock, result: firstResult }: BlockWithResult + ) { + return new BlockProverPublicInput({ + stateRoot: state.stateRoot, + blockNumber: firstBlock.height, + blockHashRoot: firstBlock.fromBlockHashRoot, + eternalTransactionsHash: firstBlock.fromEternalTransactionsHash, + incomingMessagesHash: firstBlock.fromMessagesHash, + networkStateHash: firstBlock.networkState.before.hash(), + proverStateRemainder: Field(0), + }); + } + + public openBlock( + state: BlockTracingState, + { block: firstBlock, result: firstResult }: BlockWithResult, + batchInput: BlockProverPublicInput + ): Pick< + NewBlockProverParameters, + "stateWitness" | "networkState" | "blockWitness" + > { + const stateWitness = new BlockProverStateInput({ + stateRoot: state.stateRoot, + blockNumber: firstBlock.height, + blockHashRoot: firstBlock.fromBlockHashRoot, + networkStateHash: firstBlock.networkState.before.hash(), + // The next two are properties that we fast-forward only after tx proofs are verified + // Therefore those don't change over multiple block batches + eternalTransactionsHash: batchInput.eternalTransactionsHash, + incomingMessagesHash: batchInput.incomingMessagesHash, + remainders: { + pendingSTBatchesHash: state.pendingSTBatches.commitment, + bundlesHash: state.bundleList.commitment, + witnessedRootsHash: state.witnessedRoots.commitment, + witnessedRootsPreimage: state.witnessedRoots.preimage, + }, + }); + + return { + stateWitness, + networkState: firstBlock.networkState.before, + blockWitness: firstResult.blockHashWitness, + }; + } + @trace("batch.trace.block", ([, block]) => ({ height: block.block.height.toString(), })) public async traceBlock( state: BlockTracingState, - block: BlockWithResult, - includeSTProof: boolean - ): Promise<[BlockTracingState, BlockTrace]> { - const publicInput: BlockProverPublicInput = new BlockProverPublicInput({ - stateRoot: state.stateRoot, - blockNumber: block.block.height, - blockHashRoot: block.block.fromBlockHashRoot, - eternalTransactionsHash: block.block.fromEternalTransactionsHash, - incomingMessagesHash: block.block.fromMessagesHash, - transactionsHash: Field(0), - networkStateHash: block.block.networkState.before.hash(), - witnessedRootsHash: state.witnessedRoots.commitment, - pendingSTBatchesHash: state.pendingSTBatches.commitment, - }); - + block: BlockWithResult + ): Promise<[BlockTracingState, NewBlockArguments, TransactionTrace[]]> { const startingStateBeforeHook = collectStartingState( block.block.beforeBlockStateTransitions ); - const blockTrace = { - publicInput, - networkState: block.block.networkState.before, - deferSTProof: Bool(!includeSTProof), - blockWitness: block.result.blockHashWitness, - startingStateBeforeHook, - } satisfies Partial; - state.pendingSTBatches.push({ batchHash: toStateTransitionsHash( block.block.beforeBlockStateTransitions @@ -89,43 +109,98 @@ export class BlockTracingService { }); state.networkState = block.block.networkState.during; + const blockArgsPartial = { + fromPendingSTBatchesHash: state.pendingSTBatches.commitment, + fromWitnessedRootsHash: state.witnessedRoots.commitment, + }; + + const transactionProverState = new TransactionProverState({ + transactionList: new TransactionHashList(), + witnessedRoots: state.witnessedRoots, + pendingSTBatches: state.pendingSTBatches, + incomingMessages: state.incomingMessages, + eternalTransactionsList: state.eternalTransactionsList, + bundleList: new BundleHashList( + state.bundleList.commitment, + // The preimage here is just the current state (the start of the block) + // Internally, both provers will detect commitment == preimage and start + // a new bundle + new BundlePreimage({ + preimage: state.bundleList.commitment, + fromStateTransitionsHash: state.pendingSTBatches.commitment, + fromWitnessedRootsHash: state.witnessedRoots.commitment, + }) + ), + }); + const [afterState, transactionTraces] = await yieldSequential( - chunk(block.block.transactions, 2), - async (input, [transaction1, transaction2]) => { + block.block.transactions, + async (input, transaction) => { const [output, transactionTrace] = - transaction2 !== undefined - ? await this.transactionTracing.createMultiTransactionTrace( - input, - transaction1, - transaction2 - ) - : await this.transactionTracing.createSingleTransactionTrace( - input, - transaction1 - ); + await this.transactionTracing.createTransactionTrace( + input, + state.networkState, + transaction + ); return [output, transactionTrace]; }, - state + transactionProverState + ); + + // TODO Maybe replace this with replicating the in-circuit version inside createTransactionTrace + // Add to bundleList (before all the afterBlock stuff since bundles only care about + // all the stuff that happens in the TransactionProver) + // Also, this list is a different instance than the one used in transaction tracing + const finishedBundle = new Bundle({ + networkStateHash: state.networkState.hash(), + transactionsHash: block.block.transactionsHash, + pendingSTBatchesHash: { + from: blockArgsPartial.fromPendingSTBatchesHash, + to: afterState.pendingSTBatches.commitment, + }, + witnessedRootsHash: { + from: blockArgsPartial.fromWitnessedRootsHash, + to: afterState.witnessedRoots.commitment, + }, + }); + state.bundleList.pushIf( + finishedBundle, + afterState.transactionList.isEmpty().not() ); - const preimage = afterState.witnessedRoots - .getUnconstrainedValues() - .get() - .at(-2)?.preimage; + state.pendingSTBatches = afterState.pendingSTBatches; + state.witnessedRoots = afterState.witnessedRoots; + state.incomingMessages = afterState.incomingMessages; + state.eternalTransactionsList = afterState.eternalTransactionsList; const afterBlockRootWitness: WitnessedRootWitness = { witnessedRoot: Field(block.result.witnessedRoots[0]), - preimage: preimage ?? Field(0), }; + // We create the batch here, because we need the afterBlockRootWitness, + // but the afterBlock's witnessed root can't be in the arguments, because + // it is temporally **after** the bundle, not inside it + const args = new BlockArguments({ + transactionsHash: afterState.transactionList.commitment, + afterBlockRootWitness, + witnessedRootsHash: { + from: blockArgsPartial.fromWitnessedRootsHash, + to: state.witnessedRoots.commitment, + }, + pendingSTBatchesHash: { + from: blockArgsPartial.fromPendingSTBatchesHash, + to: state.pendingSTBatches.commitment, + }, + isDummy: Bool(false), + }); + if (afterState.pendingSTBatches.commitment.equals(0).not().toBoolean()) { state.witnessedRoots.witnessRoot( { appliedBatchListState: afterState.pendingSTBatches.commitment, root: afterBlockRootWitness.witnessedRoot, }, - afterBlockRootWitness.preimage, state.pendingSTBatches.commitment.equals(0).not() ); } @@ -133,19 +208,24 @@ export class BlockTracingService { const startingStateAfterHook = collectStartingState( block.result.afterBlockStateTransitions ); + state.pendingSTBatches.push({ + batchHash: toStateTransitionsHash( + block.result.afterBlockStateTransitions + ), + applied: Bool(true), + }); state.networkState = block.result.afterNetworkState; + state.blockNumber = state.blockNumber.add(1); + return [ - afterState, + state, { - blockParams: { - ...blockTrace, - startingStateAfterHook, - afterBlockRootWitness, - }, - transactions: transactionTraces, - height: block.block.height.toString(), + args, + startingStateBeforeHook, + startingStateAfterHook, }, + transactionTraces, ]; } } diff --git a/packages/sequencer/src/protocol/production/tracing/TransactionTracingService.ts b/packages/sequencer/src/protocol/production/tracing/TransactionTracingService.ts index 4b30745b5..89e391e50 100644 --- a/packages/sequencer/src/protocol/production/tracing/TransactionTracingService.ts +++ b/packages/sequencer/src/protocol/production/tracing/TransactionTracingService.ts @@ -1,42 +1,30 @@ import { - BlockProver, - BlockProverMultiTransactionExecutionData, - BlockProverProgrammable, - BlockProverPublicInput, - BlockProverSingleTransactionExecutionData, - BlockProverTransactionArguments, - MandatoryProtocolModulesRecord, + addTransactionToBundle, NetworkState, - Protocol, + TransactionProverArguments, + TransactionProverPublicInput, + TransactionProverState, + TransactionProverTransactionArguments, } from "@proto-kit/protocol"; -import { Bool, Field } from "o1js"; -import { MAX_FIELD } from "@proto-kit/common"; +import { Bool } from "o1js"; import { toStateTransitionsHash } from "@proto-kit/module"; -import { inject, injectable } from "tsyringe"; +import { injectable } from "tsyringe"; import { TransactionExecutionResult } from "../../../storage/model/Block"; import { PendingTransaction } from "../../../mempool/PendingTransaction"; import type { RuntimeProofParameters } from "../tasks/RuntimeProvingTask"; -import { - TransactionProverTaskParameters, - TransactionProvingType, -} from "../tasks/serializers/types/TransactionProvingTypes"; +import { TransactionProverTaskParameters } from "../tasks/serializers/types/TransactionProvingTypes"; import { UntypedStateTransition } from "../helpers/UntypedStateTransition"; import { VerificationKeyService } from "../../runtime/RuntimeVerificationKeyService"; -import type { BlockTracingState, TaskStateRecord } from "./BlockTracingService"; - -export type TransactionTrace = - | { - type: TransactionProvingType.SINGLE; - transaction: TransactionProverTaskParameters; - runtime: [RuntimeProofParameters]; - } - | { - type: TransactionProvingType.MULTI; - transaction: TransactionProverTaskParameters; - runtime: [RuntimeProofParameters, RuntimeProofParameters]; - }; +import type { TaskStateRecord } from "./BlockTracingService"; + +export type TransactionTrace = { + transaction: TransactionProverTaskParameters; + runtime: RuntimeProofParameters; +}; + +export type TransactionTracingState = TransactionProverState; export function collectStartingState( stateTransitions: UntypedStateTransition[] @@ -59,19 +47,13 @@ export function collectStartingState( @injectable() export class TransactionTracingService { - private readonly blockProver: BlockProverProgrammable; - public constructor( - private readonly verificationKeyService: VerificationKeyService, - @inject("Protocol") protocol: Protocol - ) { - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - this.blockProver = (protocol.blockProver as BlockProver).zkProgrammable; - } + private readonly verificationKeyService: VerificationKeyService + ) {} public async getTransactionData( transaction: PendingTransaction - ): Promise { + ): Promise { const verificationKeyAttestation = this.verificationKeyService.getAttestation( transaction.methodId.toBigInt() @@ -85,27 +67,21 @@ export class TransactionTracingService { } private getTransactionProofPublicInput( - previousState: BlockTracingState - ): BlockProverPublicInput { + previousState: TransactionTracingState + ): TransactionProverPublicInput { return { - stateRoot: previousState.stateRoot, - transactionsHash: previousState.transactionList.commitment, + bundlesHash: previousState.bundleList.commitment, eternalTransactionsHash: previousState.eternalTransactionsList.commitment, incomingMessagesHash: previousState.incomingMessages.commitment, - networkStateHash: previousState.networkState.hash(), - witnessedRootsHash: previousState.witnessedRoots.commitment, - pendingSTBatchesHash: previousState.pendingSTBatches.commitment, - blockHashRoot: Field(0), - blockNumber: MAX_FIELD, }; } private appendTransactionToState( - previousState: BlockTracingState, + previousState: TransactionTracingState, transaction: TransactionExecutionResult ) { // TODO Remove this call and instead reuse results from sequencing - const newState = this.blockProver.addTransactionToBundle( + const newState = addTransactionToBundle( previousState, Bool(transaction.tx.isMessage), transaction.tx.toRuntimeTransaction() @@ -137,7 +113,8 @@ export class TransactionTracingService { } private async traceTransaction( - previousState: BlockTracingState, + previousState: TransactionTracingState, + networkState: NetworkState, transaction: TransactionExecutionResult ) { const beforeHookStartingState = collectStartingState( @@ -146,90 +123,61 @@ export class TransactionTracingService { const runtimeTrace1 = this.createRuntimeProofParams( transaction, - previousState.networkState + networkState ); const afterHookStartingState = collectStartingState( transaction.stateTransitions[2].stateTransitions.flat() ); + const args: TransactionProverArguments = { + networkState: networkState, + transactionHash: previousState.transactionList.commitment, + pendingSTBatchesHash: previousState.pendingSTBatches.commitment, + witnessedRootsHash: previousState.witnessedRoots.commitment, + bundleListPreimage: previousState.bundleList.preimage!, + }; + const newState = this.appendTransactionToState(previousState, transaction); + newState.bundleList.addToBundle(newState, networkState); + return { state: newState, runtime: runtimeTrace1, startingState: [beforeHookStartingState, afterHookStartingState], + args, }; } - public async createSingleTransactionTrace( - previousState: BlockTracingState, + public async createTransactionTrace( + previousState: TransactionTracingState, + networkState: NetworkState, transaction: TransactionExecutionResult - ): Promise<[BlockTracingState, TransactionTrace]> { + ): Promise<[TransactionTracingState, TransactionTrace]> { const publicInput = this.getTransactionProofPublicInput(previousState); const { state: newState, startingState, runtime, - } = await this.traceTransaction(previousState, transaction); - - const transactionTrace: TransactionProverTaskParameters = - { - executionData: { - transaction: await this.getTransactionData(transaction.tx), - networkState: previousState.networkState, - }, - startingState, - publicInput, - }; + args, + } = await this.traceTransaction(previousState, networkState, transaction); - return [ - newState, - { - type: TransactionProvingType.SINGLE, - transaction: transactionTrace, - runtime: [runtime], + const transactionTrace: TransactionProverTaskParameters = { + executionData: { + transaction: await this.getTransactionData(transaction.tx), + args, }, - ]; - } - - public async createMultiTransactionTrace( - previousState: BlockTracingState, - transaction1: TransactionExecutionResult, - transaction2: TransactionExecutionResult - ): Promise<[BlockTracingState, TransactionTrace]> { - const publicInput = this.getTransactionProofPublicInput(previousState); - - const { - state: tmpState, - startingState: startingState1, - runtime: runtime1, - } = await this.traceTransaction(previousState, transaction1); - - const { - state: resultState, - startingState: startingState2, - runtime: runtime2, - } = await this.traceTransaction(tmpState, transaction2); - - const transactionTrace: TransactionProverTaskParameters = - { - executionData: { - transaction1: await this.getTransactionData(transaction1.tx), - transaction2: await this.getTransactionData(transaction2.tx), - networkState: previousState.networkState, - }, - startingState: [...startingState1, ...startingState2], - publicInput, - }; + startingState, + publicInput, + }; return [ - resultState, + newState, { - type: TransactionProvingType.MULTI, transaction: transactionTrace, - runtime: [runtime1, runtime2], + runtime, }, ]; } diff --git a/packages/sequencer/src/protocol/production/trigger/TimedBlockTrigger.ts b/packages/sequencer/src/protocol/production/trigger/TimedBlockTrigger.ts index 73b630be1..5e784342d 100644 --- a/packages/sequencer/src/protocol/production/trigger/TimedBlockTrigger.ts +++ b/packages/sequencer/src/protocol/production/trigger/TimedBlockTrigger.ts @@ -12,6 +12,7 @@ import { BridgingModule, SettlementTokenConfig, } from "../../../settlement/BridgingModule"; +import { ensureNotBusy } from "../../../helpers/BusyGuard"; import { BlockEvents, BlockTriggerBase } from "./BlockTrigger"; @@ -45,9 +46,6 @@ export class TimedBlockTrigger private interval?: any; - // TODO Move that logic to somewhere proper - private settlementInProgress = false; - public constructor( @inject("BatchProducerModule", { isOptional: true }) batchProducerModule: BatchProducerModule | undefined, @@ -123,15 +121,9 @@ export class TimedBlockTrigger // otherwise treat as unproven-only if ( settlementInterval !== undefined && - totalTime % settlementInterval === 0 && - !this.settlementInProgress + totalTime % settlementInterval === 0 ) { - this.settlementInProgress = true; - const batch = await this.produceBatch(); - if (batch !== undefined) { - await this.settle(batch, this.config.settlementTokenConfig); - } - this.settlementInProgress = false; + await this.tryProduceSettlement(); } } catch (error) { log.error(error); @@ -143,7 +135,7 @@ export class TimedBlockTrigger private async produceUnprovenBlock() { // TODO Optimize towards mempool.length() - const mempoolTxs = await this.mempool.getTxs(); + const mempoolTxs = await this.mempool.getTxs(0); // Produce a block if either produceEmptyBlocks is true or we have more // than 1 tx in mempool if (mempoolTxs.length > 0 || (this.config.produceEmptyBlocks ?? true)) { @@ -151,6 +143,14 @@ export class TimedBlockTrigger } } + @ensureNotBusy() + private async tryProduceSettlement(): Promise { + const batch = await this.produceBatch(); + if (batch !== undefined) { + await this.settle(batch, this.config.settlementTokenConfig); + } + } + public async close(): Promise { // eslint-disable-next-line @typescript-eslint/no-unsafe-argument clearInterval(this.interval); diff --git a/packages/sequencer/src/sequencer/SequencerStartupModule.ts b/packages/sequencer/src/sequencer/SequencerStartupModule.ts index 998b3e520..603835f7a 100644 --- a/packages/sequencer/src/sequencer/SequencerStartupModule.ts +++ b/packages/sequencer/src/sequencer/SequencerStartupModule.ts @@ -1,9 +1,10 @@ import { inject } from "tsyringe"; import { + BridgingSettlementContractArgs, + ContractArgsRegistry, MandatoryProtocolModulesRecord, Protocol, RuntimeVerificationKeyRootService, - SettlementSmartContractBase, } from "@proto-kit/protocol"; import { log, @@ -21,7 +22,6 @@ import { } from "../protocol/production/tasks/CircuitCompilerTask"; import { VerificationKeyService } from "../protocol/runtime/RuntimeVerificationKeyService"; import type { MinaBaseLayer } from "../protocol/baselayer/MinaBaseLayer"; -import { SettlementUtils } from "../settlement/utils/SettlementUtils"; import { NoopBaseLayer } from "../protocol/baselayer/NoopBaseLayer"; import { SequencerModule, sequencerModule } from "./builder/SequencerModule"; @@ -44,7 +44,8 @@ export class SequencerStartupModule @inject("BaseLayer", { isOptional: true }) private readonly baseLayer: MinaBaseLayer | undefined, @inject("AreProofsEnabled") - private readonly areProofsEnabled: AreProofsEnabled + private readonly areProofsEnabled: AreProofsEnabled, + private readonly contractArgsRegistry: ContractArgsRegistry ) { super(); } @@ -140,13 +141,9 @@ export class SequencerStartupModule .resolve(ChildVerificationKeyService) .setCompileRegistry(this.compileRegistry); - // TODO Find a way to generalize this or at least make it nicer - too much logic here const isSignedSettlement = - this.baseLayer !== undefined && !(this.baseLayer instanceof NoopBaseLayer) - ? new SettlementUtils( - this.areProofsEnabled, - this.baseLayer - ).isSignedSettlement() + this.baseLayer && !(this.baseLayer instanceof NoopBaseLayer) + ? this.baseLayer.isSignedSettlement() : undefined; log.info("Compiling Protocol circuits, this can take a few minutes"); @@ -173,8 +170,13 @@ export class SequencerStartupModule // Init BridgeContract vk for settlement contract const bridgeVk = protocolBridgeArtifacts.BridgeContract; if (bridgeVk !== undefined) { - SettlementSmartContractBase.args.BridgeContractVerificationKey = - bridgeVk.verificationKey; + // TODO Inject CompileRegistry directly + this.contractArgsRegistry.addArgs( + "SettlementContract", + { + BridgeContractVerificationKey: bridgeVk.verificationKey, + } + ); } await this.registrationFlow.start({ diff --git a/packages/sequencer/src/sequencer/SettlementStartupModule.ts b/packages/sequencer/src/sequencer/SettlementStartupModule.ts index 8a76e7284..1936fb3c1 100644 --- a/packages/sequencer/src/sequencer/SettlementStartupModule.ts +++ b/packages/sequencer/src/sequencer/SettlementStartupModule.ts @@ -11,6 +11,7 @@ import { CircuitCompilerTask } from "../protocol/production/tasks/CircuitCompile @injectable() export class SettlementStartupModule { + // TODO Why is this a separate module? public constructor( private readonly compileRegistry: CompileRegistry, private readonly flowCreator: FlowCreator, @@ -38,40 +39,38 @@ export class SettlementStartupModule { return artifacts; } - private async getArtifacts(retry: boolean): Promise<{ - SettlementSmartContract: CompileArtifact; - DispatchSmartContract: CompileArtifact; - }> { - const settlementVerificationKey = this.compileRegistry.getArtifact( - "SettlementSmartContract" - ); - const dispatchVerificationKey = this.compileRegistry.getArtifact( - "DispatchSmartContract" + private async getArtifacts>( + contracts: Contracts, + retry: boolean + ): Promise> { + const artifacts = Object.entries(contracts).map( + ([contract]) => + [contract, this.compileRegistry.getArtifact(contract)] as const ); - if ( - settlementVerificationKey === undefined || - dispatchVerificationKey === undefined - ) { + if (artifacts.some((x) => x[1] === undefined)) { if (retry) { log.info( "Settlement Contracts not yet compiled, initializing compilation" ); await this.compile(); - return await this.getArtifacts(false); + return await this.getArtifacts(contracts, false); } throw new Error( "Settlement contract verification keys not available for deployment" ); } - return { - SettlementSmartContract: settlementVerificationKey, - DispatchSmartContract: dispatchVerificationKey, - }; + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + return Object.fromEntries(artifacts) as Record< + keyof Contracts, + CompileArtifact + >; } - public async retrieveVerificationKeys() { - return await this.getArtifacts(true); + public async retrieveVerificationKeys>( + contracts: Contracts + ) { + return await this.getArtifacts(contracts, true); } } diff --git a/packages/sequencer/src/settlement/BridgingModule.ts b/packages/sequencer/src/settlement/BridgingModule.ts index 09f8fa407..a575c5e3a 100644 --- a/packages/sequencer/src/settlement/BridgingModule.ts +++ b/packages/sequencer/src/settlement/BridgingModule.ts @@ -3,7 +3,6 @@ import { BridgeContractConfig, BridgeContractType, MandatoryProtocolModulesRecord, - MandatorySettlementModulesRecord, OUTGOING_MESSAGE_BATCH_SIZE, OutgoingMessageArgument, OutgoingMessageArgumentBatch, @@ -18,21 +17,26 @@ import { PROTOKIT_FIELD_PREFIXES, OutgoingMessageEvent, BridgeContractContext, + BridgingSettlementModulesRecord, + DispatchContractType, + BridgingSettlementContractType, + ContractArgsRegistry, + BridgingSettlementContractArgs, } from "@proto-kit/protocol"; import { AccountUpdate, Field, Mina, - PrivateKey, Provable, PublicKey, + SmartContract, TokenContract, TokenId, Transaction, UInt32, } from "o1js"; import { - AreProofsEnabled, + DependencyRecord, filterNonUndefined, LinkedMerkleTree, log, @@ -43,31 +47,45 @@ import { match, Pattern } from "ts-pattern"; import { FungibleToken } from "mina-fungible-token"; // eslint-disable-next-line import/no-extraneous-dependencies import groupBy from "lodash/groupBy"; +// eslint-disable-next-line import/no-extraneous-dependencies +import truncate from "lodash/truncate"; import { FeeStrategy } from "../protocol/baselayer/fees/FeeStrategy"; import type { MinaBaseLayer } from "../protocol/baselayer/MinaBaseLayer"; import { AsyncLinkedLeafStore } from "../state/async/AsyncLinkedLeafStore"; import { CachedLinkedLeafStore } from "../state/lmt/CachedLinkedLeafStore"; import { SettleableBatch } from "../storage/model/Batch"; +import { SequencerModule } from "../sequencer/builder/SequencerModule"; import type { SettlementModule } from "./SettlementModule"; import { SettlementUtils } from "./utils/SettlementUtils"; import { MinaTransactionSender } from "./transactions/MinaTransactionSender"; import { OutgoingMessageCollector } from "./messages/outgoing/OutgoingMessageCollector"; import { ArchiveNode } from "./utils/ArchiveNode"; +import { MinaSigner } from "./MinaSigner"; +import { SignedSettlementPermissions } from "./permissions/SignedSettlementPermissions"; +import { ProvenSettlementPermissions } from "./permissions/ProvenSettlementPermissions"; +import { AddressRegistry } from "./interactions/AddressRegistry"; +import { IncomingMessagesService } from "./messages/IncomingMessagesService"; export type SettlementTokenConfig = Record< string, | { - bridgingContractPrivateKey?: PrivateKey; + bridgingContractPublicKey?: PublicKey; } | { tokenOwner: FungibleToken; - bridgingContractPrivateKey?: PrivateKey; - tokenOwnerPrivateKey?: PrivateKey; + bridgingContractPublicKey?: PublicKey; + tokenOwnerPublicKey?: PublicKey; } >; +export type BridgingModuleConfig = { + addresses?: { + DispatchContract: PublicKey; + }; +}; + /** * Module that facilitates all transaction creation and monitoring for * bridging related operations. @@ -75,18 +93,18 @@ export type SettlementTokenConfig = Record< * for those as needed */ @injectable() -export class BridgingModule { +export class BridgingModule extends SequencerModule { + // TODO Eventually, we don't want to store this here either, but build a smarter AddressRegistry private seenBridgeDeployments: { latestDeployment: number; - // tokenId => Bridge address - deployments: Record; } = { latestDeployment: -1, - deployments: {}, }; private utils: SettlementUtils; + protected dispatchContract?: DispatchContractType & SmartContract; + public constructor( @inject("Protocol") private readonly protocol: Protocol, @@ -97,12 +115,46 @@ export class BridgingModule { private readonly linkedLeafStore: AsyncLinkedLeafStore, @inject("FeeStrategy") private readonly feeStrategy: FeeStrategy, - @inject("AreProofsEnabled") areProofsEnabled: AreProofsEnabled, @inject("BaseLayer") private readonly baseLayer: MinaBaseLayer, + @inject("SettlementSigner") private readonly signer: MinaSigner, @inject("TransactionSender") - private readonly transactionSender: MinaTransactionSender + private readonly transactionSender: MinaTransactionSender, + @inject("AddressRegistry") + private readonly addressRegistry: AddressRegistry, + private readonly argsRegistry: ContractArgsRegistry ) { - this.utils = new SettlementUtils(areProofsEnabled, baseLayer); + super(); + + this.utils = new SettlementUtils(baseLayer, signer); + } + + public static dependencies() { + return { + IncomingMessagesService: { + useClass: IncomingMessagesService, + }, + } satisfies DependencyRecord; + } + + public getDispatchContract() { + if (this.dispatchContract === undefined) { + const address = this.getDispatchContractAddress(); + this.dispatchContract = this.settlementContractModule().createContract( + "DispatchContract", + address + ); + } + return this.dispatchContract; + } + + public getDispatchContractAddress(): PublicKey { + const keys = + this.addressRegistry.getContractAddress("DispatchContract") ?? + this.config.addresses?.DispatchContract; + if (keys === undefined) { + throw new Error("Contracts not initialized yet"); + } + return keys; } private getMessageProcessors() { @@ -111,7 +163,7 @@ export class BridgingModule { >("OutgoingMessageProcessor"); } - protected settlementContractModule(): SettlementContractModule { + protected settlementContractModule(): SettlementContractModule { return this.protocol.dependencyContainer.resolve( "SettlementContractModule" ); @@ -128,10 +180,11 @@ export class BridgingModule { return config; } + // TODO Use AddressRegistry for bridge addresses public async updateBridgeAddresses() { const events = await this.settlementModule - .getContracts() - .settlement.fetchEvents( + .getContract() + .fetchEvents( UInt32.from(this.seenBridgeDeployments.latestDeployment + 1) ); const tuples = events @@ -139,41 +192,112 @@ export class BridgingModule { .map((event) => { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const mapping = event.event.data as unknown as TokenMapping; - return [mapping.tokenId.toString(), mapping.publicKey]; + return [mapping.tokenId.toBigInt(), mapping.publicKey] as const; }); - const mergedDeployments = { - ...this.seenBridgeDeployments.deployments, - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - ...(Object.fromEntries(tuples) as Record), - }; + + tuples.forEach(([tokenId, publicKey]) => { + this.addressRegistry.addContractAddress( + this.addressRegistry.getIdentifier("BridgeContract", tokenId), + publicKey + ); + }); + const latestDeployment = events .map((event) => Number(event.blockHeight.toString())) .reduce((a, b) => (a > b ? a : b), 0); this.seenBridgeDeployments = { - deployments: mergedDeployments, latestDeployment, }; } + public async deployMinaBridge( + contractKey: PublicKey, + options: { + nonce?: number; + } + ) { + return await this.deployTokenBridge(undefined, contractKey, options); + } + + /** + * Deploys a token bridge (BridgeContract) and authorizes it on the DispatchContract + * + * Invariant: The owner has to be specified, unless the bridge is for the mina token + * + * @param owner reference to the token owner contract (used to approve the deployment AUs) + * @param contractKey PublicKey to which the new bridge contract should be deployed to + * @param options + */ + public async deployTokenBridge( + owner: TokenContract | undefined, + contractKey: PublicKey, + options: { + nonce?: number; + } + ) { + const feepayer = this.signer.getFeepayerKey(); + const nonce = options?.nonce ?? undefined; + + const tokenId = owner?.deriveTokenId() ?? TokenId.default; + const settlementContract = + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + this.settlementModule.getContract() as BridgingSettlementContractType & + SmartContract; + + const tx = await Mina.transaction( + { + sender: feepayer, + nonce: nonce, + memo: `Deploy token bridge for ${truncate(tokenId.toString(), { length: 6 })}`, + fee: this.feeStrategy.getFee(), + }, + async () => { + AccountUpdate.fundNewAccount(feepayer, 1); + + await settlementContract.addTokenBridge(tokenId, contractKey); + + if (owner !== undefined) { + await owner.approveAccountUpdate(settlementContract.self); + } + } + ); + + // Only ContractKeys and OwnerKey for check. + // Used all in signing process. + const txSigned = this.utils.signTransaction(tx, { + signingWithSignatureCheck: [ + ...this.signer.getContractAddresses(), + ...(owner ? [owner.address] : []), + ], + signingPublicKeys: [contractKey], + }); + + await this.transactionSender.proveAndSendTransaction(txSigned, "included"); + } + public async getBridgeAddress( tokenId: Field ): Promise { - const { deployments } = this.seenBridgeDeployments; + const identifier = this.addressRegistry.getIdentifier( + "BridgeContract", + tokenId.toBigInt() + ); + const deployment = this.addressRegistry.getContractAddress(identifier); - if (Object.keys(deployments).includes(tokenId.toString())) { - return deployments[tokenId.toString()]; + if (deployment !== undefined) { + return deployment; } await this.updateBridgeAddresses(); - return this.seenBridgeDeployments.deployments[tokenId.toString()]; + return this.addressRegistry.getContractAddress(identifier); } public async getDepositContractAttestation(tokenId: Field) { await ArchiveNode.waitOnSync(this.baseLayer.config); - const { dispatch } = this.settlementModule.getContracts(); + const DispatchContract = this.getDispatchContract(); - const tree = await TokenBridgeTree.buildTreeFromEvents(dispatch); + const tree = await TokenBridgeTree.buildTreeFromEvents(DispatchContract); const index = tree.getIndex(tokenId); return new TokenBridgeAttestation({ index: Field(index), @@ -182,8 +306,8 @@ export class BridgingModule { } private async fetchFeepayerNonce() { - const { feepayer } = this.settlementModule.config; - return await this.transactionSender.getNextNonce(feepayer.toPublicKey()); + const feepayer = this.signer.getFeepayerKey(); + return await this.transactionSender.getNextNonce(feepayer); } public async sendRollupTransactions( @@ -244,13 +368,13 @@ export class BridgingModule { options: | { nonce: number; - bridgingContractPrivateKey?: PrivateKey; + bridgingContractPublicKey?: PublicKey; } | { nonce: number; tokenOwner: FungibleToken; - bridgingContractPrivateKey?: PrivateKey; - tokenOwnerPrivateKey?: PrivateKey; + bridgingContractPublicKey?: PublicKey; + tokenOwnerPublicKey?: PublicKey; } ) { return await match(options) @@ -258,18 +382,16 @@ export class BridgingModule { { nonce: Pattern.number, tokenOwner: Pattern.instanceOf(FungibleToken), - bridgingContractPrivateKey: Pattern.optional( - Pattern.instanceOf(PrivateKey) - ), - tokenOwnerPrivateKey: Pattern.optional( - Pattern.instanceOf(PrivateKey) + bridgingContractPublicKey: Pattern.optional( + Pattern.instanceOf(PublicKey) ), + tokenOwnerPublicKey: Pattern.optional(Pattern.instanceOf(PublicKey)), }, ({ nonce, tokenOwner, - bridgingContractPrivateKey, - tokenOwnerPrivateKey, + bridgingContractPublicKey, + tokenOwnerPublicKey, }) => { return this.sendRollupTransactionsBase( async (au: AccountUpdate) => { @@ -280,8 +402,8 @@ export class BridgingModule { { nonce, contractKeys: [ - bridgingContractPrivateKey, - tokenOwnerPrivateKey, + bridgingContractPublicKey, + tokenOwnerPublicKey, ].filter(filterNonUndefined), } ); @@ -290,11 +412,11 @@ export class BridgingModule { .with( { nonce: Pattern.number, - bridgingContractPrivateKey: Pattern.optional( - Pattern.instanceOf(PrivateKey) + bridgingContractPublicKey: Pattern.optional( + Pattern.instanceOf(PublicKey) ), }, - ({ nonce, bridgingContractPrivateKey }) => { + ({ nonce, bridgingContractPublicKey }) => { return this.sendRollupTransactionsBase( async () => {}, TokenId.default, @@ -302,8 +424,8 @@ export class BridgingModule { { nonce, contractKeys: - bridgingContractPrivateKey !== undefined - ? [bridgingContractPrivateKey] + bridgingContractPublicKey !== undefined + ? [bridgingContractPublicKey] : [], } ); @@ -314,7 +436,8 @@ export class BridgingModule { public createBridgeContract(contractAddress: PublicKey, tokenId: Field) { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - return this.settlementContractModule().createBridgeContract( + return this.settlementContractModule().createContract( + "BridgeContract", contractAddress, tokenId ) as BridgeContractType & TokenContract; @@ -354,12 +477,12 @@ export class BridgingModule { public async pullStateRoot( tokenWrapper: (au: AccountUpdate) => Promise, tokenId: Field, - options: { nonce: number; contractKeys: PrivateKey[] } + options: { nonce: number; contractKeys: PublicKey[] } ): Promise< | { nonceUsed: false } | { nonceUsed: true; tx: Mina.Transaction } > { - const settlementContract = this.settlementModule.getContracts().settlement; + const settlementContract = this.settlementModule.getSettlementContract(); const bridge = await this.getBridgeContract(tokenId); log.debug( @@ -381,12 +504,12 @@ export class BridgingModule { if (settledRoot.toBigInt() !== (tokenBridgeRoot?.toBigInt() ?? -1n)) { // Create transaction - const { feepayer } = this.settlementModule.config; + const feepayer = this.signer.getFeepayerKey(); let { nonce } = options; const tx = await Mina.transaction( { - sender: feepayer.toPublicKey(), + sender: feepayer, // eslint-disable-next-line no-plusplus nonce: nonce++, fee: this.feeStrategy.getFee(), @@ -398,11 +521,9 @@ export class BridgingModule { } ); - const signedTx = this.utils.signTransaction( - tx, - [feepayer], - options.contractKeys - ); + const signedTx = this.utils.signTransaction(tx, { + signingWithSignatureCheck: options.contractKeys, + }); await this.transactionSender.proveAndSendTransaction( signedTx, @@ -423,13 +544,13 @@ export class BridgingModule { tokenWrapper: (au: AccountUpdate) => Promise, tokenId: Field, events: OutgoingMessageEvent[], - options: { nonce: number; contractKeys: PrivateKey[] } + options: { nonce: number; contractKeys: PublicKey[] } ): Promise< { tx: Transaction; }[] > { - const { feepayer } = this.settlementModule.config; + const feepayer = this.signer.getFeepayerKey(); let { nonce } = options; const txs: { @@ -444,7 +565,10 @@ export class BridgingModule { ); } - if (this.utils.isSignedSettlement() && options.contractKeys.length === 0) { + if ( + this.baseLayer.isSignedSettlement() && + options.contractKeys.length === 0 + ) { throw new Error( "Bridging contract private key for signed settlement has to be provided" ); @@ -503,7 +627,7 @@ export class BridgingModule { const tx = await Mina.transaction( { - sender: feepayer.toPublicKey(), + sender: feepayer, // eslint-disable-next-line no-plusplus nonce: nonce++, fee: this.feeStrategy.getFee(), @@ -525,21 +649,16 @@ export class BridgingModule { }); // Pay account creation fees for internal token accounts - AccountUpdate.fundNewAccount( - feepayer.toPublicKey(), - numNewAccountsNumber - ); + AccountUpdate.fundNewAccount(feepayer, numNewAccountsNumber); } ); log.debug("Sending rollup transaction:"); log.debug(tx.toPretty()); - const signedTx = this.utils.signTransaction( - tx, - [feepayer], - options.contractKeys - ); + const signedTx = this.utils.signTransaction(tx, { + signingWithSignatureCheck: [...options.contractKeys], + }); await this.transactionSender.proveAndSendTransaction( signedTx, @@ -553,5 +672,28 @@ export class BridgingModule { return txs; } + + public async start(): Promise { + this.argsRegistry.addArgs( + "SettlementContract", + { + // TODO Add distinction between mina and custom tokens + BridgeContractPermissions: (this.baseLayer.isSignedSettlement() + ? new SignedSettlementPermissions() + : new ProvenSettlementPermissions() + ).bridgeContractMina(), + } + ); + + const dispatchAddress = this.config.addresses?.DispatchContract; + if (dispatchAddress !== undefined) { + this.addressRegistry.addContractAddress( + "DispatchContract", + dispatchAddress + ); + } + } /* eslint-enable no-await-in-loop */ } + +// BridgingModule satisfies DependencyFactory; diff --git a/packages/sequencer/src/settlement/MinaSigner.ts b/packages/sequencer/src/settlement/MinaSigner.ts new file mode 100644 index 000000000..47ded0928 --- /dev/null +++ b/packages/sequencer/src/settlement/MinaSigner.ts @@ -0,0 +1,335 @@ +import { Field, PrivateKey, PublicKey, Signature, Transaction } from "o1js"; +import { noop } from "@proto-kit/common"; +import { injectable } from "tsyringe"; + +import { SequencerModule } from "../sequencer/builder/SequencerModule"; + +/** + * Options for signing transactions. + */ +export interface SignTxOptions { + /** + * Optional array of public keys whose corresponding private keys + * should be used to sign the transaction in addition to the feepayer key. + */ + pubKeys?: PublicKey[]; +} + +/** + * Interface for signing Mina blockchain transactions and managing cryptographic keys. + * Provides methods for signing transactions, managing contract addresses, and handling + * cryptographic signatures for the Mina protocol. + */ +export interface MinaSigner { + /** + * Retrieves the public key of the feepayer account. + * The feepayer is the account that pays + * transaction fees on the Mina blockchain. + * + * @returns The {@link PublicKey} of the feepayer account. + */ + getFeepayerKey(): PublicKey; + + /** + * Retrieves the public keys of all managed smart contract addresses. + * These typically include settlement, dispatch, and bridge contract addresses. + * + * @returns Array of {@link PublicKey}s for the managed contracts. + */ + getContractAddresses(): PublicKey[]; + + /** + * Signs arbitrary data using the feepayer's private key. + * + * @param signatureData - Array of {@link Field} elements representing the data to be signed. + * @returns A cryptographic {@link Signature} over the provided data. + */ + sign(signatureData: Field[]): Signature; + + /** + * Signs arbitrary data using a specific registered private key. + * + * @param publicKey - The {@link PublicKey} whose corresponding private key + * will be used for signing. + * @param signatureData - Array of {@link Field} elements representing the data to be signed. + * @returns A cryptographic {@link Signature} over the provided data. + * @throws An {@link Error} If the corresponding private key is not found in the key registry. + */ + signWithKey(publicKey: PublicKey, signatureData: Field[]): Signature; + + /** + * Signs a Mina transaction with the feepayer key and optionally additional keys. + * + * @param tx - The unsigned {@link Transaction} to be signed. + * @param options - Optional {@link SignTxOptions} specifying additional keys to use for signing. + * @returns The signed {@link Transaction} ready for submission to the network. + * @throws An {@link Error} If any required private key is not found. + */ + signTx( + tx: Transaction, + options?: SignTxOptions + ): Transaction; + + /** + * Registers a new private key in the signer's key management system. + * + * @param privateKey - The {@link PrivateKey} to register. + * @returns {PublicKey} The corresponding public key of the registered private key. + */ + registerKey(privateKey: PrivateKey): PublicKey; + + /** + * Retrieves the public keys of all managed token bridge addresses. + * Token bridges facilitate cross-chain token transfers. + * + * @returns {PublicKey[]} Array of public keys for token bridge contracts, + * or empty array if none configured. + */ + getTokenAddresses(): PublicKey[]; +} + +/** + * Configuration for the in-memory Mina signer. + * Defines all private keys that the signer will manage. + */ +export interface InMemorySignerConfig { + /** + * Private key of the feepayer account that will pay for transaction fees. + */ + feepayer: PrivateKey; + + /** + * Private keys for smart contracts managed by this signer. + * Expected order: [settlement, dispatch, minaBridge] + */ + contractKeys: PrivateKey[]; + + /** + * Optional private keys for token bridge contracts. + * Used when the application manages cross-chain token transfers. + */ + tokenBridgeKeys?: PrivateKey[]; +} + +/** + * In-memory implementation of the MinaSigner interface. + * Stores private keys in memory and provides cryptographic signing operations + * for the Mina blockchain protocol. This implementation is suitable for + * server-side sequencer operations where keys can be securely stored in memory. + */ +@injectable() +export class InMemoryMinaSigner + extends SequencerModule + implements MinaSigner +{ + /** + * Internal map storing the relationship between public keys (in base58 format) + * and their corresponding private keys for efficient lookup during signing operations. + */ + private keyMap!: Map; + + public constructor() { + super(); + } + + /** + * Initializes the internal key mapping from configured private keys. + * Populates the keyMap with both token bridge keys and contract keys, + * using base58-encoded public keys as lookup identifiers. + * + * @private + */ + private initializeKeyMap(): void { + this.keyMap = new Map(); + + // Register token bridge keys if configured + this.config.tokenBridgeKeys?.forEach((key) => { + this.keyMap.set(key.toPublicKey().toBase58(), key); + }); + + // Register contract keys + this.config.contractKeys.forEach((key) => { + this.keyMap.set(key.toPublicKey().toBase58(), key); + }); + } + + /** + * Retrieves the public key of the feepayer account. + * The feepayer is responsible for paying transaction fees on the Mina blockchain. + * + * @returns {PublicKey} The public key derived from the configured feepayer private key. + */ + public getFeepayerKey(): PublicKey { + return this.config.feepayer.toPublicKey(); + } + + /** + * Retrieves all configured token bridge public keys. + * + * {@link PublicKey[]} Array of public keys for all configured token bridge contracts. + * Returns empty array if no token bridge keys are configured. + */ + public getTokenAddresses(): PublicKey[] { + if (this.config.tokenBridgeKeys) { + return this.config.tokenBridgeKeys.map((x) => x.toPublicKey()); + } + return []; + } + + /** + * Retrieves public keys for all managed smart contracts. + * + * **Expected key order**: + * - Index 0: Settlement contract public key + * - Index 1: Dispatch contract public key + * - Index 2: Mina bridge contract public key + * + * The order corresponds to the order of private keys in the configuration. + * + * @returnsArray of {@link PublicKey}s derived from configured contract private keys. + */ + public getContractAddresses(): PublicKey[] { + const contractPrivateKeys = this.config.contractKeys; + return contractPrivateKeys.map((key) => key.toPublicKey()); + } + + /** + * Signs arbitrary data using the feepayer's private key. + * This is useful for creating signatures or signing + * custom protocol messages. + * + * @param signatureData - Array of {@link Field} elements to be signed. + * Field is the base type for Mina's cryptographic operations. + * @returns Cryptographic {@link Signature} created using the feepayer's private key. + * + * @example + * ```typescript + * const message = [Field(123), Field(456)]; + * const signature = signer.sign(message); + * ``` + */ + public sign(signatureData: Field[]): Signature { + return Signature.create(this.config.feepayer, signatureData); + } + + /** + * Signs arbitrary data using a specific registered private key. + * Enables signing with contract or bridge keys rather than the feepayer key. + * + * @param publicKey - The {@link PublicKey} identifying which private key to use for signing. + * This public key must have been previously registered in the keyMap. + * @param signatureData - Array of {@link Field} elements representing the data to be signed. + * @returns Cryptographic {@link Signature} created with the specified private key. + * @throws An {@link Error} if no private key is found for the provided public key. + * + * @example + * ```typescript + * const contractPubKey = signer.getContractAddresses()[0]; + * const message = [Field(789)]; + * const signature = signer.signWithKey(contractPubKey, message); + * ``` + */ + public signWithKey(publicKey: PublicKey, signatureData: Field[]): Signature { + const key = this.keyMap.get(publicKey.toBase58()); + if (!key) { + throw new Error(`Relevant key not found for ${publicKey.toBase58()}`); + } + return Signature.create(key, signatureData); + } + + /** + * Registers a new private key in the signer's key management system. + * If the key already exists, returns the existing public key without modification. + * This method is idempotent - calling it multiple times with the same key is safe. + * + * @param privateKey - The {@link PrivateKey} to register for future signing operations. + * @returns {PublicKey} The public key corresponding to the registered private key. + * + * @example + * ```typescript + * const newKey = PrivateKey.random(); + * const publicKey = signer.registerKey(newKey); + * // Now this key can be used for signing operations. + * ``` + */ + public registerKey(privateKey: PrivateKey): PublicKey { + const publicKey = privateKey.toPublicKey(); + const publicKeyString = publicKey.toBase58(); + const keyExist = this.keyMap.get(publicKeyString); + + if (keyExist) { + return publicKey; + } else { + this.keyMap.set(publicKeyString, privateKey); + return publicKey; + } + } + + /** + * Signs a Mina transaction with multiple private keys. + * Always signs with the feepayer key, optionally with additional + * keys specified in the options. This is required for transactions that + * need authorization from multiple parties (e.g., contract deployments in + * our case) + * + * @param tx - The unsigned {@link Transaction< false, false>} object to be signed. + * Must be of type Transaction (unsigned). + * @param options - Optional {@link SignTxOptions} configuration containing: + * - pubKeys: Array of public keys whose + * private keys should sign the transaction. + * Each public key must be registered in the keyMap. + * @returns The signed {@link Transaction} ready for network submission. + * @throws An {@link Error} If any required private key is not found in the key registry. + * + * @example + * ```typescript + * const unsignedTx = await Mina.transaction(sender, () => { + * // transaction logic + * }); + * + * // Sign with feepayer only + * const signedTx1 = signer.signTx(unsignedTx); + * + * // Sign with feepayer and additional keys + * const contractKeys = signer.getContractAddresses(); + * const signedTx2 = signer.signTx(unsignedTx, { + * pubKeys: [contractKeys[0], contractKeys[1]] + * }); + * ``` + */ + public signTx( + tx: Transaction, + options: SignTxOptions = {} + ): Transaction { + const { pubKeys = [] } = options; + const privateKeys: PrivateKey[] = []; + + // Gather all required private keys from the keyMap + for (const pubKey of pubKeys) { + const privKey = this.keyMap.get(pubKey.toBase58()); + if (!privKey) { + throw new Error( + `Error in signTx function: relevant private key to ${pubKey.toBase58()} not found!` + ); + } + privateKeys.push(privKey); + } + + // Sign with feepayer first, then additional keys + const keys = [this.config.feepayer, ...privateKeys]; + return tx.sign(keys); + } + + /** + * Lifecycle method called when the signer module starts. + * Initializes the internal key mapping from the configured private keys. + * This method is part of the SequencerModule lifecycle and is automatically + * called by the framework during sequencer startup. + * + * @async + */ + public async start() { + this.initializeKeyMap(); + noop(); + } +} diff --git a/packages/sequencer/src/settlement/SettlementModule.ts b/packages/sequencer/src/settlement/SettlementModule.ts index 800e2578e..3d9486e05 100644 --- a/packages/sequencer/src/settlement/SettlementModule.ts +++ b/packages/sequencer/src/settlement/SettlementModule.ts @@ -1,33 +1,22 @@ import { Protocol, SettlementContractModule, - BATCH_SIGNATURE_PREFIX, - DispatchSmartContract, - SettlementSmartContract, MandatorySettlementModulesRecord, MandatoryProtocolModulesRecord, - SettlementSmartContractBase, - DynamicBlockProof, + type SettlementContractType, + ContractArgsRegistry, + SettlementContractArgs, } from "@proto-kit/protocol"; -import { - AccountUpdate, - Mina, - PrivateKey, - PublicKey, - Signature, - TokenContract, - Transaction, -} from "o1js"; +import { fetchAccount, Field, Mina, PublicKey, SmartContract } from "o1js"; import { inject } from "tsyringe"; import { EventEmitter, EventEmittingComponent, - log, - AreProofsEnabled, DependencyFactory, + ModuleContainerLike, + DependencyRecord, + log, } from "@proto-kit/common"; -// eslint-disable-next-line import/no-extraneous-dependencies -import truncate from "lodash/truncate"; import { SequencerModule, @@ -35,26 +24,24 @@ import { } from "../sequencer/builder/SequencerModule"; import type { MinaBaseLayer } from "../protocol/baselayer/MinaBaseLayer"; import { SettleableBatch } from "../storage/model/Batch"; -import { BlockProofSerializer } from "../protocol/production/tasks/serializers/BlockProofSerializer"; import { Settlement } from "../storage/model/Settlement"; -import { FeeStrategy } from "../protocol/baselayer/fees/FeeStrategy"; -import { SettlementStartupModule } from "../sequencer/SettlementStartupModule"; import { SettlementStorage } from "../storage/repositories/SettlementStorage"; -import { MinaTransactionSender } from "./transactions/MinaTransactionSender"; -import { ProvenSettlementPermissions } from "./permissions/ProvenSettlementPermissions"; -import { SignedSettlementPermissions } from "./permissions/SignedSettlementPermissions"; import { SettlementUtils } from "./utils/SettlementUtils"; -import { BridgingModule } from "./BridgingModule"; +import type { BridgingModule } from "./BridgingModule"; +import { MinaSigner } from "./MinaSigner"; +import { BridgingDeployInteraction } from "./interactions/bridging/BridgingDeployInteraction"; +import { VanillaDeployInteraction } from "./interactions/vanilla/VanillaDeployInteraction"; +import { BridgingSettlementInteraction } from "./interactions/bridging/BridgingSettlementInteraction"; +import { VanillaSettlementInteraction } from "./interactions/vanilla/VanillaSettlementInteraction"; +import { + AddressRegistry, + InMemoryAddressRegistry, +} from "./interactions/AddressRegistry"; export type SettlementModuleConfig = { - feepayer: PrivateKey; -} & { - // TODO Add possibility to only configure public keys (for proven operation) - keys?: { - settlement: PrivateKey; - dispatch: PrivateKey; - minaBridge: PrivateKey; + addresses?: { + SettlementContract: PublicKey; }; }; @@ -65,118 +52,91 @@ export type SettlementModuleEvents = { @sequencerModule() export class SettlementModule extends SequencerModule - implements EventEmittingComponent, DependencyFactory + implements EventEmittingComponent { - protected contracts?: { - settlement: SettlementSmartContract; - dispatch: DispatchSmartContract; - }; - - private keys?: { - settlement: PrivateKey; - dispatch: PrivateKey; - minaBridge: PrivateKey; - }; + protected contract?: SettlementContractType & SmartContract; public utils: SettlementUtils; public events = new EventEmitter(); public constructor( - @inject("BaseLayer") baseLayer: MinaBaseLayer, + @inject("BaseLayer") private readonly baseLayer: MinaBaseLayer, @inject("Protocol") private readonly protocol: Protocol, @inject("SettlementStorage") private readonly settlementStorage: SettlementStorage, - private readonly blockProofSerializer: BlockProofSerializer, - @inject("TransactionSender") - private readonly transactionSender: MinaTransactionSender, - @inject("AreProofsEnabled") areProofsEnabled: AreProofsEnabled, - @inject("FeeStrategy") - private readonly feeStrategy: FeeStrategy, - private readonly settlementStartupModule: SettlementStartupModule + @inject("SettlementSigner") private readonly signer: MinaSigner, + @inject("Sequencer") + private readonly parentContainer: ModuleContainerLike, + @inject("AddressRegistry") + private readonly addressRegistry: AddressRegistry, + private readonly argsRegistry: ContractArgsRegistry ) { super(); - this.utils = new SettlementUtils(areProofsEnabled, baseLayer); + this.utils = new SettlementUtils(this.baseLayer, this.signer); } - public dependencies() { + public static dependencies(): DependencyRecord { return { - BridgingModule: { - useClass: BridgingModule, + AddressRegistry: { + useClass: InMemoryAddressRegistry, }, }; } + private bridgingModule(): BridgingModule | undefined { + const container = this.parentContainer.dependencyContainer; + if (container.isRegistered("BridgingModule")) { + return container.resolve("BridgingModule"); + } + return undefined; + } + protected settlementContractModule(): SettlementContractModule { return this.protocol.dependencyContainer.resolve( "SettlementContractModule" ); } - public getContractKeys(): { - settlement: PrivateKey; - dispatch: PrivateKey; - minaBridge: PrivateKey; - } { - const keys = this.keys ?? this.config.keys; + public getSettlementContractAddress(): PublicKey { + const keys = + this.addressRegistry.getContractAddress("SettlementContract") ?? + this.config.addresses?.SettlementContract; + if (keys === undefined) { throw new Error("Contracts not initialized yet"); } return keys; } - public getContractSigningKeys() { - return Object.values(this.getContractKeys()); - } + public getSettlementContract() { + if (this.contract === undefined) { + const address = this.getSettlementContractAddress(); + this.contract = this.settlementContractModule().createContract( + "SettlementContract", + address + ); + } - public getAddresses() { - const keys = this.getContractKeys(); - return { - settlement: keys.settlement.toPublicKey(), - dispatch: keys.dispatch.toPublicKey(), - }; + return this.contract; } - public getContracts() { - if (this.contracts === undefined) { - const addresses = this.getAddresses(); + public getContract() { + if (this.contract === undefined) { + const address = this.getSettlementContractAddress(); const { protocol } = this; const settlementContractModule = protocol.dependencyContainer.resolve< SettlementContractModule >("SettlementContractModule"); - // TODO Add generic inference of concrete Contract types - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - this.contracts = settlementContractModule.createContracts(addresses) as { - settlement: SettlementSmartContract; - dispatch: DispatchSmartContract; - }; + this.contract = settlementContractModule.createContract( + "SettlementContract", + address + ); } - return this.contracts; - } - - public signTransaction( - tx: Transaction, - pks: PrivateKey[], - tokenContractKeys: PrivateKey[] = [], - preventNoncePreconditionFor: PublicKey[] = [] - ): Transaction { - return this.utils.signTransaction( - tx, - pks, - this.getContractSigningKeys().concat(tokenContractKeys), - preventNoncePreconditionFor - ); - } - - private async fetchContractAccounts() { - const contracts = this.getContracts(); - await this.utils.fetchContractAccounts( - contracts.settlement, - contracts.dispatch - ); + return this.contract; } public async settleBatch( @@ -185,59 +145,18 @@ export class SettlementModule nonce?: number; } = {} ): Promise { - await this.fetchContractAccounts(); - const { settlement: settlementContract, dispatch } = this.getContracts(); - const { feepayer } = this.config; - log.debug("Preparing settlement"); - const lastSettlementL1BlockHeight = - settlementContract.lastSettlementL1BlockHeight.get().value; - const signature = Signature.create(feepayer, [ - BATCH_SIGNATURE_PREFIX, - lastSettlementL1BlockHeight, - ]); - - const latestSequenceStateHash = dispatch.account.actionState.get(); - - const blockProof = await this.blockProofSerializer - .getBlockProofSerializer() - .fromJSONProof(batch.proof); - - const dynamicBlockProof = DynamicBlockProof.fromProof(blockProof); - - const tx = await Mina.transaction( - { - sender: feepayer.toPublicKey(), - nonce: options?.nonce, - fee: this.feeStrategy.getFee(), - memo: "Protokit settle", - }, - async () => { - await settlementContract.settle( - dynamicBlockProof, - signature, - dispatch.address, - feepayer.toPublicKey(), - batch.fromNetworkState, - batch.toNetworkState, - latestSequenceStateHash - ); - } - ); - - this.utils.signTransaction(tx, [feepayer], this.getContractSigningKeys()); - - const { hash: transactionHash } = - await this.transactionSender.proveAndSendTransaction(tx, "included"); - - log.info("Settlement transaction sent and included"); - - const settlement = { - batches: [batch.height], - promisedMessagesHash: latestSequenceStateHash.toString(), - transactionHash, - }; + const bridgingModule = this.bridgingModule(); + const interaction = + bridgingModule !== undefined + ? this.parentContainer.dependencyContainer.resolve( + BridgingSettlementInteraction + ) + : this.parentContainer.dependencyContainer.resolve( + VanillaSettlementInteraction + ); + const settlement = await interaction.settle(batch, options); await this.settlementStorage.pushSettlement(settlement); @@ -246,157 +165,101 @@ export class SettlementModule return settlement; } + // Can't do anything for now - initialize() method use settlementKey. + // TODO Rethink that interface - deploy with addresses as args would be pretty nice public async deploy( - settlementKey: PrivateKey, - dispatchKey: PrivateKey, - minaBridgeKey: PrivateKey, + addresses: { + settlementContract: PublicKey; + dispatchContract?: PublicKey; + }, options: { nonce?: number; } = {} ) { - const feepayerKey = this.config.feepayer; - const feepayer = feepayerKey.toPublicKey(); - - const nonce = options?.nonce ?? 0; + const bridgingModule = this.bridgingModule(); + // TODO Add overwriting in dependency factories and then resolve based on that here + const interaction = + bridgingModule !== undefined + ? this.parentContainer.dependencyContainer.resolve( + BridgingDeployInteraction + ) + : this.parentContainer.dependencyContainer.resolve( + VanillaDeployInteraction + ); + + await interaction.deploy(addresses, options); + } - const sm = this.protocol.dependencyContainer.resolve< - SettlementContractModule - >("SettlementContractModule"); - const { settlement, dispatch } = sm.createContracts({ - settlement: settlementKey.toPublicKey(), - dispatch: dispatchKey.toPublicKey(), + public async start(): Promise { + this.argsRegistry.addArgs("SettlementContract", { + signedSettlements: this.baseLayer.isSignedSettlement(), }); - const verificationsKeys = - await this.settlementStartupModule.retrieveVerificationKeys(); + const settlementContractAddress = this.config.addresses?.SettlementContract; + if (settlementContractAddress !== undefined) { + this.addressRegistry.addContractAddress( + "SettlementContract", + settlementContractAddress + ); - const permissions = this.utils.isSignedSettlement() - ? new SignedSettlementPermissions() - : new ProvenSettlementPermissions(); - - const tx = await Mina.transaction( - { - sender: feepayer, - nonce, - fee: this.feeStrategy.getFee(), - memo: "Protokit settlement deploy", - }, - async () => { - AccountUpdate.fundNewAccount(feepayer, 2); - await settlement.deploy({ - verificationKey: - verificationsKeys.SettlementSmartContract.verificationKey, - }); - settlement.account.permissions.set(permissions.settlementContract()); - - await dispatch.deploy({ - verificationKey: - verificationsKeys.DispatchSmartContract.verificationKey, - }); - dispatch.account.permissions.set(permissions.dispatchContract()); - } - ).sign([feepayerKey, settlementKey, dispatchKey]); - // Note: We can't use this.signTransaction on the above tx - - // This should already apply the tx result to the - // cached accounts / local blockchain - await this.transactionSender.proveAndSendTransaction(tx, "included"); - - this.keys = { - settlement: settlementKey, - dispatch: dispatchKey, - minaBridge: minaBridgeKey, - }; - - await this.utils.fetchContractAccounts(settlement, dispatch); - - const initTx = await Mina.transaction( - { - sender: feepayer, - nonce: nonce + 1, - fee: this.feeStrategy.getFee(), - memo: "Protokit settlement init", - }, - async () => { - AccountUpdate.fundNewAccount(feepayer, 1); - await settlement.initialize( - feepayerKey.toPublicKey(), - dispatchKey.toPublicKey(), - minaBridgeKey.toPublicKey(), - settlementKey - ); - } - ); - - const initTxSigned = this.utils.signTransaction( - initTx, - // Specify the mina bridge key here explicitly, since initialize() will issue - // a account update to that address and by default new accounts have a signature permission - [feepayerKey, minaBridgeKey], - [...this.getContractSigningKeys(), minaBridgeKey] - ); - - await this.transactionSender.proveAndSendTransaction( - initTxSigned, - "included" - ); + await this.checkDeployment(); + } } - public async deployTokenBridge( - owner: TokenContract, - ownerKey: PrivateKey, - contractKey: PrivateKey, - options: { - nonce?: number; + public async checkDeployment( + tokenBridges?: Array<{ address: PublicKey; tokenId: Field }> + ): Promise { + const addresses = [ + this.addressRegistry.getContractAddress("SettlementContract")!, + ]; + + const bridgeContractAddress = + this.bridgingModule()?.config.addresses?.DispatchContract; + if (bridgeContractAddress !== undefined) { + addresses.push(bridgeContractAddress); } - ) { - const feepayerKey = this.config.feepayer; - const feepayer = feepayerKey.toPublicKey(); - const nonce = options?.nonce ?? undefined; - - const tokenId = owner.deriveTokenId(); - const { settlement, dispatch } = this.getContracts(); - - const tx = await Mina.transaction( - { - sender: feepayer, - nonce: nonce, - memo: `Deploy token bridge for ${truncate(tokenId.toString(), { length: 6 })}`, - fee: this.feeStrategy.getFee(), - }, - async () => { - AccountUpdate.fundNewAccount(feepayer, 1); - await settlement.addTokenBridge( - tokenId, - contractKey.toPublicKey(), - dispatch.address - ); - await owner.approveAccountUpdate(settlement.self); - } - ); - const txSigned = this.utils.signTransaction( - tx, - // Specify the mina bridge key here explicitly, since deploy() will issue - // a account update to that address and by default new accounts have a signature permission - [feepayerKey, contractKey], - [...this.getContractSigningKeys(), ownerKey] + const contracts: Array<{ address: PublicKey; tokenId?: Field }> = [ + ...addresses.map((addr) => ({ address: addr })), + ...(tokenBridges ?? []), + ]; + + const isLocal = this.baseLayer.isLocalBlockChain(); + const missing: Array<{ address: string; error: string }> = []; + + await Promise.all( + contracts.map(async ({ address, tokenId }) => { + if (isLocal) { + if (!Mina.hasAccount(address, tokenId)) { + missing.push({ + address: address.toBase58(), + error: "Not found on local chain", + }); + } + } else { + const { account, error } = await fetchAccount({ + publicKey: address, + tokenId, + }); + if (account === null || account === undefined) { + missing.push({ + address: address.toBase58(), + error: error?.statusText ?? "Not found on chain", + }); + } + } + }) ); - await this.transactionSender.proveAndSendTransaction(txSigned, "included"); - } - - public async start(): Promise { - const contractArgs = SettlementSmartContractBase.args; - - SettlementSmartContractBase.args = { - ...contractArgs, - signedSettlements: this.utils.isSignedSettlement(), - // TODO Add distinction between mina and custom tokens - BridgeContractPermissions: (this.utils.isSignedSettlement() - ? new SignedSettlementPermissions() - : new ProvenSettlementPermissions() - ).bridgeContractMina(), - }; + if (missing.length > 0) { + const errorList = missing + .map((m) => ` ${m.address}: ${m.error}`) + .join("\n"); + throw new Error(` + Missing contracts:\n${errorList} + `); + } } } + +SettlementModule satisfies DependencyFactory; diff --git a/packages/sequencer/src/settlement/interactions/AddressRegistry.ts b/packages/sequencer/src/settlement/interactions/AddressRegistry.ts new file mode 100644 index 000000000..449f345cd --- /dev/null +++ b/packages/sequencer/src/settlement/interactions/AddressRegistry.ts @@ -0,0 +1,31 @@ +import { PublicKey } from "o1js"; + +export interface AddressRegistry { + getIdentifier(name: string, tokenId?: bigint): string; + + getContractAddress(identifier: string): PublicKey | undefined; + + addContractAddress(identifier: string, address: PublicKey): void; + + hasContractAddress(identifier: string): boolean; +} + +export class InMemoryAddressRegistry implements AddressRegistry { + addresses: Record = {}; + + getIdentifier(name: string, tokenId?: bigint) { + return `${name}-${tokenId}`; + } + + addContractAddress(identifier: string, address: PublicKey): void { + this.addresses[identifier] = address; + } + + getContractAddress(identifier: string): PublicKey { + return this.addresses[identifier]; + } + + hasContractAddress(identifier: string): boolean { + return this.addresses[identifier] !== undefined; + } +} diff --git a/packages/sequencer/src/settlement/interactions/DeployInteraction.ts b/packages/sequencer/src/settlement/interactions/DeployInteraction.ts new file mode 100644 index 000000000..e2501d866 --- /dev/null +++ b/packages/sequencer/src/settlement/interactions/DeployInteraction.ts @@ -0,0 +1,11 @@ +import { PublicKey } from "o1js"; + +export interface DeployInteraction { + deploy( + addresses: { + settlementContract: PublicKey; + dispatchContract: PublicKey; + }, + options?: { nonce?: number } + ): void; +} diff --git a/packages/sequencer/src/settlement/interactions/SettleInteraction.ts b/packages/sequencer/src/settlement/interactions/SettleInteraction.ts new file mode 100644 index 000000000..3291df213 --- /dev/null +++ b/packages/sequencer/src/settlement/interactions/SettleInteraction.ts @@ -0,0 +1,11 @@ +import { SettleableBatch } from "../../storage/model/Batch"; +import { Settlement } from "../../storage/model/Settlement"; + +export interface SettleInteraction { + settle( + batch: SettleableBatch, + options: { + nonce?: number; + } + ): Promise; +} diff --git a/packages/sequencer/src/settlement/interactions/bridging/BridgingDeployInteraction.ts b/packages/sequencer/src/settlement/interactions/bridging/BridgingDeployInteraction.ts new file mode 100644 index 000000000..c1c24d4a4 --- /dev/null +++ b/packages/sequencer/src/settlement/interactions/bridging/BridgingDeployInteraction.ts @@ -0,0 +1,129 @@ +import { inject, injectable } from "tsyringe"; +import { AccountUpdate, Mina, PublicKey } from "o1js"; +import { + BridgingSettlementModulesRecord, + MandatoryProtocolModulesRecord, + Protocol, + SettlementContractModule, +} from "@proto-kit/protocol"; +import { O1PublicKeyOption } from "@proto-kit/common"; + +import { DeployInteraction } from "../DeployInteraction"; +import { AddressRegistry } from "../AddressRegistry"; +import { MinaSigner } from "../../MinaSigner"; +import { MinaBaseLayer } from "../../../protocol/baselayer/MinaBaseLayer"; +import { SettlementStartupModule } from "../../../sequencer/SettlementStartupModule"; +import { SignedSettlementPermissions } from "../../permissions/SignedSettlementPermissions"; +import { ProvenSettlementPermissions } from "../../permissions/ProvenSettlementPermissions"; +import { FeeStrategy } from "../../../protocol/baselayer/fees/FeeStrategy"; +import { MinaTransactionSender } from "../../transactions/MinaTransactionSender"; +import { SettlementUtils } from "../../utils/SettlementUtils"; + +@injectable() +export class BridgingDeployInteraction implements DeployInteraction { + public constructor( + @inject("AddressRegistry") + private readonly addressRegistry: AddressRegistry, + @inject("SettlementSigner") private readonly signer: MinaSigner, + @inject("BaseLayer") private readonly baseLayer: MinaBaseLayer, + @inject("Protocol") + private readonly protocol: Protocol, + private readonly settlementStartupModule: SettlementStartupModule, + @inject("FeeStrategy") + private readonly feeStrategy: FeeStrategy, + @inject("TransactionSender") + private readonly transactionSender: MinaTransactionSender + ) {} + + protected settlementContractModule(): SettlementContractModule { + return this.protocol.dependencyContainer.resolve( + "SettlementContractModule" + ); + } + + public async deploy( + { + dispatchContract: dispatchKey, + settlementContract: settlementKey, + }: { + settlementContract: PublicKey; + dispatchContract?: PublicKey; + }, + options: { + nonce?: number; + } = {} + ) { + if (dispatchKey === undefined) { + throw new Error("DispatchContract address not provided"); + } + + const feepayer = this.signer.getFeepayerKey(); + + const nonce = options?.nonce ?? 0; + + const sm = this.settlementContractModule(); + const { + SettlementContract: settlementContract, + DispatchContract: dispatchContract, + } = sm.createContracts({ + SettlementContract: settlementKey, + DispatchContract: dispatchKey, + }); + + const verificationsKeys = + await this.settlementStartupModule.retrieveVerificationKeys({ + SettlementContract: true, + DispatchSmartContract: true, + }); + + const utils = new SettlementUtils(this.baseLayer, this.signer); + await utils.fetchContractAccounts(settlementContract, dispatchContract); + + const permissions = this.baseLayer.isSignedSettlement() + ? new SignedSettlementPermissions() + : new ProvenSettlementPermissions(); + + const tx = await Mina.transaction( + { + sender: feepayer, + nonce, + fee: this.feeStrategy.getFee(), + memo: "Protokit settlement deploy", + }, + async () => { + AccountUpdate.fundNewAccount(feepayer, 2); + + await dispatchContract.deployAndInitialize( + { + verificationKey: + verificationsKeys.DispatchSmartContract.verificationKey, + }, + permissions.dispatchContract(), + settlementContract.address + ); + + await settlementContract.deployAndInitialize( + { + verificationKey: + verificationsKeys.SettlementContract.verificationKey, + }, + permissions.settlementContract(), + feepayer, + O1PublicKeyOption.from(dispatchKey) + ); + } + ); + + utils.signTransaction(tx, { + signingWithSignatureCheck: [...this.signer.getContractAddresses()], + }); + + await this.transactionSender.proveAndSendTransaction(tx, "included"); + + this.addressRegistry.addContractAddress( + "SettlementContract", + settlementKey + ); + this.addressRegistry.addContractAddress("DispatchContract", dispatchKey); + } +} diff --git a/packages/sequencer/src/settlement/interactions/bridging/BridgingSettlementInteraction.ts b/packages/sequencer/src/settlement/interactions/bridging/BridgingSettlementInteraction.ts new file mode 100644 index 000000000..ee8b74983 --- /dev/null +++ b/packages/sequencer/src/settlement/interactions/bridging/BridgingSettlementInteraction.ts @@ -0,0 +1,126 @@ +import { inject, injectable } from "tsyringe"; +import { Mina } from "o1js"; +import { + BATCH_SIGNATURE_PREFIX, + BridgingSettlementModulesRecord, + DynamicBlockProof, + MandatoryProtocolModulesRecord, + Protocol, + SettlementContractModule, +} from "@proto-kit/protocol"; +import { log } from "@proto-kit/common"; + +import { AddressRegistry } from "../AddressRegistry"; +import { MinaSigner } from "../../MinaSigner"; +import { MinaBaseLayer } from "../../../protocol/baselayer/MinaBaseLayer"; +import { FeeStrategy } from "../../../protocol/baselayer/fees/FeeStrategy"; +import { MinaTransactionSender } from "../../transactions/MinaTransactionSender"; +import { SettlementUtils } from "../../utils/SettlementUtils"; +import { BlockProofSerializer } from "../../../protocol/production/tasks/serializers/BlockProofSerializer"; +import { SettleableBatch } from "../../../storage/model/Batch"; +import { Settlement } from "../../../storage/model/Settlement"; +import { SettleInteraction } from "../SettleInteraction"; + +@injectable() +export class BridgingSettlementInteraction implements SettleInteraction { + public constructor( + @inject("AddressRegistry") + private readonly addressRegistry: AddressRegistry, + @inject("SettlementSigner") private readonly signer: MinaSigner, + @inject("BaseLayer") private readonly baseLayer: MinaBaseLayer, + @inject("Protocol") + private readonly protocol: Protocol, + @inject("FeeStrategy") + private readonly feeStrategy: FeeStrategy, + @inject("TransactionSender") + private readonly transactionSender: MinaTransactionSender, + private readonly blockProofSerializer: BlockProofSerializer + ) {} + + protected settlementContractModule(): SettlementContractModule { + return this.protocol.dependencyContainer.resolve( + "SettlementContractModule" + ); + } + + public async settle( + batch: SettleableBatch, + options: { + nonce?: number; + } = {} + ): Promise { + const feepayer = this.signer.getFeepayerKey(); + + const settlementKey = + this.addressRegistry.getContractAddress("SettlementContract"); + const dispatchKey = + this.addressRegistry.getContractAddress("DispatchContract"); + + if (settlementKey === undefined || dispatchKey === undefined) { + throw new Error( + "Settlement and/or DispatchContract addresses haven't been initialized" + ); + } + + const sm = this.settlementContractModule(); + const { + SettlementContract: settlementContract, + DispatchContract: dispatchContract, + } = sm.createContracts({ + SettlementContract: settlementKey, + DispatchContract: dispatchKey, + }); + + const utils = new SettlementUtils(this.baseLayer, this.signer); + await utils.fetchContractAccounts(settlementContract, dispatchContract); + + const lastSettlementL1BlockHeight = + settlementContract.lastSettlementL1BlockHeight.get().value; + const signature = this.signer.sign([ + BATCH_SIGNATURE_PREFIX, + lastSettlementL1BlockHeight, + ]); + + const latestSequenceStateHash = dispatchContract.account.actionState.get(); + + const blockProof = await this.blockProofSerializer + .getBlockProofSerializer() + .fromJSONProof(batch.proof); + + const dynamicBlockProof = DynamicBlockProof.fromProof(blockProof); + + const tx = await Mina.transaction( + { + sender: feepayer, + nonce: options?.nonce, + fee: this.feeStrategy.getFee(), + memo: "Protokit settle", + }, + async () => { + await settlementContract.settle( + dynamicBlockProof, + signature, + feepayer, + batch.fromNetworkState, + batch.toNetworkState, + latestSequenceStateHash + ); + } + ); + + utils.signTransaction(tx, { + signingWithSignatureCheck: this.signer.getContractAddresses(), + }); + + const { hash: transactionHash } = + await this.transactionSender.proveAndSendTransaction(tx, "included"); + + log.info("Settlement transaction sent and included"); + + return { + batches: [batch.height], + promisedMessagesHash: latestSequenceStateHash.toString(), + transactionHash, + }; + } +} diff --git a/packages/sequencer/src/settlement/interactions/vanilla/VanillaDeployInteraction.ts b/packages/sequencer/src/settlement/interactions/vanilla/VanillaDeployInteraction.ts new file mode 100644 index 000000000..c5fb126fe --- /dev/null +++ b/packages/sequencer/src/settlement/interactions/vanilla/VanillaDeployInteraction.ts @@ -0,0 +1,117 @@ +import { inject, injectable } from "tsyringe"; +import { AccountUpdate, Mina, PublicKey } from "o1js"; +import { + MandatoryProtocolModulesRecord, + MandatorySettlementModulesRecord, + Protocol, + SettlementContractModule, +} from "@proto-kit/protocol"; +import { log, O1PublicKeyOption } from "@proto-kit/common"; + +import { DeployInteraction } from "../DeployInteraction"; +import { AddressRegistry } from "../AddressRegistry"; +import { MinaSigner } from "../../MinaSigner"; +import { MinaBaseLayer } from "../../../protocol/baselayer/MinaBaseLayer"; +import { SettlementStartupModule } from "../../../sequencer/SettlementStartupModule"; +import { SignedSettlementPermissions } from "../../permissions/SignedSettlementPermissions"; +import { ProvenSettlementPermissions } from "../../permissions/ProvenSettlementPermissions"; +import { FeeStrategy } from "../../../protocol/baselayer/fees/FeeStrategy"; +import { MinaTransactionSender } from "../../transactions/MinaTransactionSender"; +import { SettlementUtils } from "../../utils/SettlementUtils"; + +@injectable() +export class VanillaDeployInteraction implements DeployInteraction { + public constructor( + @inject("AddressRegistry") + private readonly addressRegistry: AddressRegistry, + @inject("SettlementSigner") private readonly signer: MinaSigner, + @inject("BaseLayer") private readonly baseLayer: MinaBaseLayer, + @inject("Protocol") + private readonly protocol: Protocol, + private readonly settlementStartupModule: SettlementStartupModule, + @inject("FeeStrategy") + private readonly feeStrategy: FeeStrategy, + @inject("TransactionSender") + private readonly transactionSender: MinaTransactionSender + ) {} + + protected settlementContractModule(): SettlementContractModule { + return this.protocol.dependencyContainer.resolve( + "SettlementContractModule" + ); + } + + public async deploy( + { + settlementContract: settlementKey, + dispatchContract: dispatchKey, + }: { + settlementContract: PublicKey; + dispatchContract?: PublicKey; + }, + options: { + nonce?: number; + } = {} + ) { + if (dispatchKey !== undefined) { + log.error( + "DispatchContract address provided for deploy(), however the module configuration hints at a " + + "settlement-only deployment, therefore the DispatchContract will not be deployed" + ); + } + + const feepayer = this.signer.getFeepayerKey(); + + const nonce = options?.nonce ?? 0; + + const sm = this.settlementContractModule(); + const { SettlementContract: settlementContract } = sm.createContracts({ + SettlementContract: settlementKey, + }); + + const utils = new SettlementUtils(this.baseLayer, this.signer); + await utils.fetchContractAccounts(settlementContract); + + const verificationsKeys = + await this.settlementStartupModule.retrieveVerificationKeys({ + SettlementContract: true, + }); + + const permissions = this.baseLayer.isSignedSettlement() + ? new SignedSettlementPermissions() + : new ProvenSettlementPermissions(); + + const tx = await Mina.transaction( + { + sender: feepayer, + nonce, + fee: this.feeStrategy.getFee(), + memo: "Protokit settlement deploy", + }, + async () => { + AccountUpdate.fundNewAccount(feepayer, 1); + + await settlementContract.deployAndInitialize( + { + verificationKey: + verificationsKeys.SettlementContract.verificationKey, + }, + permissions.settlementContract(), + feepayer, + O1PublicKeyOption.none() + ); + } + ); + + utils.signTransaction(tx, { + signingWithSignatureCheck: [...this.signer.getContractAddresses()], + }); + + await this.transactionSender.proveAndSendTransaction(tx, "included"); + + this.addressRegistry.addContractAddress( + "SettlementContract", + settlementKey + ); + } +} diff --git a/packages/sequencer/src/settlement/interactions/vanilla/VanillaSettlementInteraction.ts b/packages/sequencer/src/settlement/interactions/vanilla/VanillaSettlementInteraction.ts new file mode 100644 index 000000000..096438ab5 --- /dev/null +++ b/packages/sequencer/src/settlement/interactions/vanilla/VanillaSettlementInteraction.ts @@ -0,0 +1,118 @@ +import { inject, injectable } from "tsyringe"; +import { Field, Mina } from "o1js"; +import { + BATCH_SIGNATURE_PREFIX, + BridgingSettlementModulesRecord, + DynamicBlockProof, + MandatoryProtocolModulesRecord, + Protocol, + SettlementContractModule, +} from "@proto-kit/protocol"; +import { log } from "@proto-kit/common"; + +import { AddressRegistry } from "../AddressRegistry"; +import { MinaSigner } from "../../MinaSigner"; +import { MinaBaseLayer } from "../../../protocol/baselayer/MinaBaseLayer"; +import { FeeStrategy } from "../../../protocol/baselayer/fees/FeeStrategy"; +import { MinaTransactionSender } from "../../transactions/MinaTransactionSender"; +import { SettlementUtils } from "../../utils/SettlementUtils"; +import { BlockProofSerializer } from "../../../protocol/production/tasks/serializers/BlockProofSerializer"; +import { SettleableBatch } from "../../../storage/model/Batch"; +import { Settlement } from "../../../storage/model/Settlement"; +import { SettleInteraction } from "../SettleInteraction"; + +@injectable() +export class VanillaSettlementInteraction implements SettleInteraction { + public constructor( + @inject("AddressRegistry") + private readonly addressRegistry: AddressRegistry, + @inject("SettlementSigner") private readonly signer: MinaSigner, + @inject("BaseLayer") private readonly baseLayer: MinaBaseLayer, + @inject("Protocol") + private readonly protocol: Protocol, + @inject("FeeStrategy") + private readonly feeStrategy: FeeStrategy, + @inject("TransactionSender") + private readonly transactionSender: MinaTransactionSender, + private readonly blockProofSerializer: BlockProofSerializer + ) {} + + protected settlementContractModule(): SettlementContractModule { + return this.protocol.dependencyContainer.resolve( + "SettlementContractModule" + ); + } + + public async settle( + batch: SettleableBatch, + options: { + nonce?: number; + } = {} + ): Promise { + const feepayer = this.signer.getFeepayerKey(); + + const settlementKey = + this.addressRegistry.getContractAddress("SettlementContract"); + + if (settlementKey === undefined) { + throw new Error("Settlement addresses haven't been initialized"); + } + + const sm = this.settlementContractModule(); + const { SettlementContract: settlementContract } = sm.createContracts({ + SettlementContract: settlementKey, + }); + + const utils = new SettlementUtils(this.baseLayer, this.signer); + await utils.fetchContractAccounts(settlementContract); + + const lastSettlementL1BlockHeight = + settlementContract.lastSettlementL1BlockHeight.get().value; + const signature = this.signer.sign([ + BATCH_SIGNATURE_PREFIX, + lastSettlementL1BlockHeight, + ]); + + const latestSequenceStateHash = Field(0); + + const blockProof = await this.blockProofSerializer + .getBlockProofSerializer() + .fromJSONProof(batch.proof); + + const dynamicBlockProof = DynamicBlockProof.fromProof(blockProof); + + const tx = await Mina.transaction( + { + sender: feepayer, + nonce: options?.nonce, + fee: this.feeStrategy.getFee(), + memo: "Protokit settle", + }, + async () => { + await settlementContract.settle( + dynamicBlockProof, + signature, + feepayer, + batch.fromNetworkState, + batch.toNetworkState, + latestSequenceStateHash + ); + } + ); + + utils.signTransaction(tx, { + signingWithSignatureCheck: this.signer.getContractAddresses(), + }); + + const { hash: transactionHash } = + await this.transactionSender.proveAndSendTransaction(tx, "included"); + + log.info("Settlement transaction sent and included"); + + return { + batches: [batch.height], + promisedMessagesHash: latestSequenceStateHash.toString(), + transactionHash, + }; + } +} diff --git a/packages/sequencer/src/settlement/messages/IncomingMessagesService.ts b/packages/sequencer/src/settlement/messages/IncomingMessagesService.ts index 509500df3..6311cb30a 100644 --- a/packages/sequencer/src/settlement/messages/IncomingMessagesService.ts +++ b/packages/sequencer/src/settlement/messages/IncomingMessagesService.ts @@ -5,7 +5,7 @@ import { SettlementStorage } from "../../storage/repositories/SettlementStorage" import { MessageStorage } from "../../storage/repositories/MessageStorage"; import { BlockStorage } from "../../storage/repositories/BlockStorage"; import { PendingTransaction } from "../../mempool/PendingTransaction"; -import type { SettlementModule } from "../SettlementModule"; +import type { BridgingModule } from "../BridgingModule"; import { IncomingMessageAdapter } from "./IncomingMessageAdapter"; @@ -20,8 +20,8 @@ export class IncomingMessagesService { private readonly messagesAdapter: IncomingMessageAdapter, @inject("BlockStorage") private readonly blockStorage: BlockStorage, - @inject("SettlementModule") - private readonly settlementModule: SettlementModule + @inject("BridgingModule") + private readonly bridgingModule: BridgingModule ) {} private async fetchRemaining( @@ -29,7 +29,7 @@ export class IncomingMessagesService { toMessagesHash: string ) { const dispatchContractAddress = - this.settlementModule.getAddresses().dispatch; + this.bridgingModule.getDispatchContractAddress(); const fetched = await this.messagesAdapter.fetchPendingMessages( dispatchContractAddress, diff --git a/packages/sequencer/src/settlement/utils/SettlementUtils.ts b/packages/sequencer/src/settlement/utils/SettlementUtils.ts index 152c48775..9c9f2bba8 100644 --- a/packages/sequencer/src/settlement/utils/SettlementUtils.ts +++ b/packages/sequencer/src/settlement/utils/SettlementUtils.ts @@ -7,53 +7,51 @@ import { Transaction, UInt32, } from "o1js"; -import { AreProofsEnabled, mapSequential } from "@proto-kit/common"; +import { mapSequential } from "@proto-kit/common"; import type { MinaBaseLayer } from "../../protocol/baselayer/MinaBaseLayer"; +import { MinaSigner } from "../MinaSigner"; + +interface SignTransactionOptions { + signingWithSignatureCheck?: PublicKey[]; + signingPublicKeys?: PublicKey[]; + preventNoncePreconditionFor?: PublicKey[]; +} /** * Utils class that provides methods for sending transactions that are signed-settlement-enabled */ export class SettlementUtils { public constructor( - private readonly areProofsEnabled: AreProofsEnabled, - private readonly baseLayer: MinaBaseLayer + private readonly baseLayer: MinaBaseLayer, + private readonly signer: MinaSigner ) {} - /** - * Signed settlement happens when proofs are disabled and the network is remote - * This is because on local network we can use mock proofs, while on remotes ones we can't - */ - public isSignedSettlement(): boolean { - return ( - !this.areProofsEnabled.areProofsEnabled && - !this.baseLayer.isLocalBlockChain() - ); - } - - /** - * Sign transaction with two variants: - * - * - If it normal settlement (proofs enabled or mock proofs): - * Sign the transaction normally - * - * - If it is signed settlement: - * Signed the transactions and make all contract AUs where a private key is known - * via the contractKeys param require a signature and sign them using that key - */ public signTransaction( tx: Transaction, - pks: PrivateKey[], - contractKeys: PrivateKey[], - preventNoncePreconditionFor: PublicKey[] = [] - ): Transaction { - const contractKeyArray = this.isSignedSettlement() ? contractKeys : []; + options: SignTransactionOptions = {} + ) { + const { + signingWithSignatureCheck = [], + signingPublicKeys = [], + preventNoncePreconditionFor = [], + } = options; + + const contractKeyArray = this.baseLayer.isSignedSettlement() + ? signingWithSignatureCheck + : []; + this.requireSignatureIfNecessary( tx, - contractKeyArray.map((key) => key.toPublicKey()), + contractKeyArray, preventNoncePreconditionFor ); - return tx.sign([...pks, ...contractKeyArray]); + + const pubKeys = signingWithSignatureCheck.concat(signingPublicKeys); + + return this.signer.signTx(tx, { + pubKeys, + }); } private requireSignatureIfNecessary( @@ -61,7 +59,7 @@ export class SettlementUtils { addresses: PublicKey[], preventNoncePreconditionFor: PublicKey[] ) { - if (this.isSignedSettlement() && addresses !== undefined) { + if (this.baseLayer.isSignedSettlement() && addresses !== undefined) { const nonces: Record = {}; tx.transaction.accountUpdates.forEach((au) => { @@ -107,6 +105,18 @@ export class SettlementUtils { } } + public registerKey(privateKey: PrivateKey): PublicKey { + return this.signer.registerKey(privateKey); + } + + public getSigner(): PublicKey { + return this.signer.getFeepayerKey(); + } + + public isSignedSettlement(): boolean { + return this.baseLayer.isSignedSettlement(); + } + /** * Fetch a set of accounts (and there update internally) with respect to what network is set */ diff --git a/packages/sequencer/src/storage/adapters/PostgresStateModule.ts b/packages/sequencer/src/storage/adapters/PostgresStateModule.ts deleted file mode 100644 index 7e9c22e24..000000000 --- a/packages/sequencer/src/storage/adapters/PostgresStateModule.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { - sequencerModule, - SequencerModule, -} from "../../sequencer/builder/SequencerModule"; - -@sequencerModule() -export class PostgresStateModule extends SequencerModule { - public async start(): Promise { - return undefined; - } -} diff --git a/packages/sequencer/src/storage/inmemory/InMemoryTransactionStorage.ts b/packages/sequencer/src/storage/inmemory/InMemoryTransactionStorage.ts index df0fe7fd7..0d672d6bc 100644 --- a/packages/sequencer/src/storage/inmemory/InMemoryTransactionStorage.ts +++ b/packages/sequencer/src/storage/inmemory/InMemoryTransactionStorage.ts @@ -1,15 +1,17 @@ import { inject, injectable } from "tsyringe"; import { Field } from "o1js"; +import { splitArray } from "@proto-kit/common"; import { TransactionStorage } from "../repositories/TransactionStorage"; import { PendingTransaction } from "../../mempool/PendingTransaction"; import { BlockStorage } from "../repositories/BlockStorage"; +import { PathResolution } from "../../protocol/production/sequencing/Ordering"; import { InMemoryBatchStorage } from "./InMemoryBatchStorage"; @injectable() export class InMemoryTransactionStorage implements TransactionStorage { - private queue: PendingTransaction[] = []; + private queue: { tx: PendingTransaction; sortingValue: number }[] = []; private latestScannedBlock = -1; @@ -21,13 +23,21 @@ export class InMemoryTransactionStorage implements TransactionStorage { public async removeTx(hashes: string[]) { const hashSet = new Set(hashes); - this.queue = this.queue.filter((tx) => { + this.queue = this.queue.filter(({ tx }) => { const hash = tx.hash().toString(); return !hashSet.has(hash); }); } - public async getPendingUserTransactions(): Promise { + private sortQueue() { + // Sort in-place and descending + this.queue.sort(({ sortingValue: a }, { sortingValue: b }) => b - a); + } + + public async getPendingUserTransactions( + offset: number, + limit?: number + ): Promise { const nextHeight = await this.blockStorage.getCurrentBlockHeight(); for ( let height = this.latestScannedBlock + 1; @@ -39,22 +49,33 @@ export class InMemoryTransactionStorage implements TransactionStorage { if (block !== undefined) { const hashes = block.transactions.map((tx) => tx.tx.hash().toString()); this.queue = this.queue.filter( - (tx) => !hashes.includes(tx.hash().toString()) + ({ tx }) => !hashes.includes(tx.hash().toString()) ); } } this.latestScannedBlock = nextHeight - 1; - return this.queue.slice(); + this.sortQueue(); + + const from = offset ?? 0; + const to = + limit !== undefined + ? Math.min(from + limit, this.queue.length) + : undefined; + + return this.queue.slice(from, to).map(({ tx }) => tx); } - public async pushUserTransaction(tx: PendingTransaction): Promise { + public async pushUserTransaction( + tx: PendingTransaction, + priority: number + ): Promise { const notInQueue = this.queue.find( - (tx2) => tx2.hash().toString() === tx.hash().toString() + ({ tx: tx2 }) => tx2.hash().toString() === tx.hash().toString() ) === undefined; if (notInQueue) { - this.queue.push(tx); + this.queue.push({ tx, sortingValue: priority }); } return notInQueue; } @@ -83,7 +104,7 @@ export class InMemoryTransactionStorage implements TransactionStorage { } | undefined > { - const pending = await this.getPendingUserTransactions(); + const pending = await this.getPendingUserTransactions(0); const pendingResult = pending.find((tx) => tx.hash().toString() === hash); if (pendingResult !== undefined) { return { @@ -115,4 +136,38 @@ export class InMemoryTransactionStorage implements TransactionStorage { } return undefined; } + + private pathResolution = new PathResolution(); + + private unresolvedSet: { tx: PendingTransaction; sortingValue: number }[] = + []; + + public async reportSkippedTransactions( + paths: Record + ): Promise { + Object.entries(paths).forEach(([txHash, transactionPaths]) => { + this.pathResolution.pushPaths(txHash, transactionPaths); + }); + + // Remove all unresolved txs from queue and append them to the unresolvedSet + const unresolvedHashes = Object.keys(paths); + const split = splitArray(this.queue, (x) => + unresolvedHashes.includes(x.tx.hash().toString()) ? "unresolved" : "queue" + ); + this.queue = split.queue ?? []; + this.unresolvedSet.push(...(split.unresolved ?? [])); + } + + public async reportChangedPaths(paths: bigint[]): Promise { + const resolved = this.pathResolution.resolvePaths(paths); + + // Move resolved from unresolvedSet to queue, then sort queue + const resolvedSplit = splitArray(this.unresolvedSet, (x) => + resolved.includes(x.tx.hash().toString()) ? "resolved" : "unresolved" + ); + this.queue.push(...(resolvedSplit.resolved ?? [])); + this.unresolvedSet = resolvedSplit.unresolved ?? []; + + this.sortQueue(); + } } diff --git a/packages/sequencer/src/storage/model/Batch.ts b/packages/sequencer/src/storage/model/Batch.ts index 731b14718..65e325a5b 100644 --- a/packages/sequencer/src/storage/model/Batch.ts +++ b/packages/sequencer/src/storage/model/Batch.ts @@ -1,14 +1,6 @@ import { JsonProof } from "o1js"; import { NetworkState } from "@proto-kit/protocol"; -import { PendingTransaction } from "../../mempool/PendingTransaction"; - -export interface BatchTransaction { - tx: PendingTransaction; - status: boolean; - statusMessage?: string; -} - export interface Batch { proof: JsonProof; blockHashes: string[]; diff --git a/packages/sequencer/src/storage/model/Block.ts b/packages/sequencer/src/storage/model/Block.ts index da2e44802..2a9ea57d5 100644 --- a/packages/sequencer/src/storage/model/Block.ts +++ b/packages/sequencer/src/storage/model/Block.ts @@ -85,11 +85,6 @@ export interface BlockWithMaybeResult { // eslint-disable-next-line @typescript-eslint/no-redeclare export const BlockWithResult = { - // toBlockProverState: ({ block, result }: BlockWithResult) => ({ - // stateRoot: result.stateRoot, - // - // } satisfies BlockProverStateCommitments), - createEmpty: () => ({ block: { diff --git a/packages/sequencer/src/storage/repositories/TransactionStorage.ts b/packages/sequencer/src/storage/repositories/TransactionStorage.ts index ef353ad08..9106b72d2 100644 --- a/packages/sequencer/src/storage/repositories/TransactionStorage.ts +++ b/packages/sequencer/src/storage/repositories/TransactionStorage.ts @@ -1,9 +1,15 @@ import { PendingTransaction } from "../../mempool/PendingTransaction"; export interface TransactionStorage { - pushUserTransaction: (tx: PendingTransaction) => Promise; + pushUserTransaction: ( + tx: PendingTransaction, + priority: number + ) => Promise; - getPendingUserTransactions: () => Promise; + getPendingUserTransactions: ( + offset: number, + limit?: number + ) => Promise; removeTx: (txHashes: string[], type: "included" | "dropped") => Promise; @@ -22,4 +28,13 @@ export interface TransactionStorage { } | undefined >; + + /** + * Mapping hash => path[] + */ + reportSkippedTransactions: (paths: Record) => Promise; + + reportChangedPaths: (paths: bigint[]) => Promise; + + // TODO Add a method to retrieve all conflict transactions and expose it through the APIs } diff --git a/packages/sequencer/src/worker/flow/Flow.ts b/packages/sequencer/src/worker/flow/Flow.ts index bd9dd7fd0..6dd7d5e02 100644 --- a/packages/sequencer/src/worker/flow/Flow.ts +++ b/packages/sequencer/src/worker/flow/Flow.ts @@ -114,40 +114,46 @@ export class Flow implements Closeable { taskName?: string; } ): Promise { - const queueName = task.name; - const taskName = overrides?.taskName ?? task.name; - const queue = await this.queueImpl.getQueue(queueName); - - const payload = await task.inputSerializer().toJSON(input); - - this.taskCounter += 1; - const taskId = String(this.taskCounter); - - log.trace(`Pushing task ${task.name}`); - - await queue.addTask({ - name: taskName, - taskId, - flowId: this.flowId, - payload, - sequencerId: this.sequencerId, - }); + // We wrap this in a try-catch here, because the flow architecture + // sometimes lets errors vanish + try { + const queueName = task.name; + const taskName = overrides?.taskName ?? task.name; + const queue = await this.queueImpl.getQueue(queueName); + + const payload = await task.inputSerializer().toJSON(input); + this.taskCounter += 1; + const taskId = String(this.taskCounter); + + log.trace(`Pushing task ${task.name}`); + + await queue.addTask({ + name: taskName, + taskId, + flowId: this.flowId, + payload, + sequencerId: this.sequencerId, + }); - this.tasksInProgress += 1; - - const callback = async (returnPayload: TaskPayload) => { - log.trace( - `Completed ${returnPayload.name}, task: ${returnPayload.flowId}:${ - returnPayload?.taskId ?? "-" - }` - ); - const decoded = await task - .resultSerializer() - .fromJSON(returnPayload.payload); - this.tasksInProgress -= 1; - return await completed?.(decoded, input); - }; - await this.waitForResult(queue, taskId, callback); + this.tasksInProgress += 1; + + const callback = async (returnPayload: TaskPayload) => { + log.trace( + `Completed ${returnPayload.name}, task: ${returnPayload.flowId}:${ + returnPayload?.taskId ?? "-" + }` + ); + const decoded = await task + .resultSerializer() + .fromJSON(returnPayload.payload); + this.tasksInProgress -= 1; + return await completed?.(decoded, input); + }; + await this.waitForResult(queue, taskId, callback); + } catch (e) { + log.error(e); + throw e; + } } public async forEach( diff --git a/packages/sequencer/src/worker/queue/LocalTaskQueue.ts b/packages/sequencer/src/worker/queue/LocalTaskQueue.ts index c6046477e..e325508ca 100644 --- a/packages/sequencer/src/worker/queue/LocalTaskQueue.ts +++ b/packages/sequencer/src/worker/queue/LocalTaskQueue.ts @@ -1,4 +1,4 @@ -import { log, mapSequential, noop } from "@proto-kit/common"; +import { log, mapSequential, noop, sleep } from "@proto-kit/common"; import { sequencerModule } from "../../sequencer/builder/SequencerModule"; import { TaskPayload } from "../flow/Task"; @@ -8,12 +8,6 @@ import { InstantiatedQueue, TaskQueue } from "./TaskQueue"; import { ListenerList } from "./ListenerList"; import { AbstractTaskQueue } from "./AbstractTaskQueue"; -async function sleep(ms: number) { - await new Promise((resolve) => { - setTimeout(resolve, ms); - }); -} - // Had to extract it to here bc eslint would ruin the code interface QueueListener { (payload: TaskPayload): Promise; diff --git a/packages/sequencer/src/worker/worker/FlowTaskWorker.ts b/packages/sequencer/src/worker/worker/FlowTaskWorker.ts index d2cac1f89..d07142898 100644 --- a/packages/sequencer/src/worker/worker/FlowTaskWorker.ts +++ b/packages/sequencer/src/worker/worker/FlowTaskWorker.ts @@ -92,7 +92,7 @@ export class FlowTaskWorker[]> // Call them in order of registration, because the prepare methods // might depend on each other or a result that is saved in a DI singleton for (const task of tasks) { - log.debug(`Preparing task ${task.constructor.name}`); + log.info(`Preparing task ${task.constructor.name}`); // eslint-disable-next-line no-await-in-loop await task.prepare(); log.debug(`${task.constructor.name} prepared`); diff --git a/packages/sequencer/src/worker/worker/LocalTaskWorkerModule.ts b/packages/sequencer/src/worker/worker/LocalTaskWorkerModule.ts index 97aec991a..85fa05663 100644 --- a/packages/sequencer/src/worker/worker/LocalTaskWorkerModule.ts +++ b/packages/sequencer/src/worker/worker/LocalTaskWorkerModule.ts @@ -28,6 +28,7 @@ import { closeable } from "../../sequencer/builder/Closeable"; import { StateTransitionReductionTask } from "../../protocol/production/tasks/StateTransitionReductionTask"; import { TransactionProvingTask } from "../../protocol/production/tasks/TransactionProvingTask"; import { BlockReductionTask } from "../../protocol/production/tasks/BlockReductionTask"; +import { TransactionReductionTask } from "../../protocol/production/tasks/TransactionReductionTask"; import { FlowTaskWorker } from "./FlowTaskWorker"; import { TaskWorkerModule } from "./TaskWorkerModule"; @@ -135,6 +136,7 @@ export class VanillaTaskWorkerModules { StateTransitionReductionTask, RuntimeProvingTask, TransactionProvingTask, + TransactionReductionTask, BlockReductionTask, NewBlockTask, CircuitCompilerTask, @@ -154,6 +156,7 @@ export class VanillaTaskWorkerModules { StateTransitionTask: {}, RuntimeProvingTask: {}, TransactionProvingTask: {}, + TransactionReductionTask: {}, BlockReductionTask: {}, NewBlockTask: {}, StateTransitionReductionTask: {}, diff --git a/packages/sequencer/src/worker/worker/startup/WorkerRegistrationTask.ts b/packages/sequencer/src/worker/worker/startup/WorkerRegistrationTask.ts index 3c53dfd44..30339d0fc 100644 --- a/packages/sequencer/src/worker/worker/startup/WorkerRegistrationTask.ts +++ b/packages/sequencer/src/worker/worker/startup/WorkerRegistrationTask.ts @@ -9,9 +9,10 @@ import { } from "@proto-kit/common"; import { inject, injectable } from "tsyringe"; import { + BridgingSettlementContractArgs, + ContractArgsRegistry, RuntimeVerificationKeyRootService, SettlementContractModule, - SettlementSmartContractBase, } from "@proto-kit/protocol"; import { VerificationKey } from "o1js"; @@ -48,7 +49,8 @@ export class WorkerRegistrationTask public constructor( @inject("Protocol") private readonly protocol: ModuleContainerLike, - private readonly compileRegistry: CompileRegistry + private readonly compileRegistry: CompileRegistry, + private readonly contractArgsRegistry: ContractArgsRegistry ) { super(); } @@ -78,7 +80,7 @@ export class WorkerRegistrationTask this.protocol.dependencyContainer .resolve< SettlementContractModule< - ReturnType + ReturnType > >("SettlementContractModule") .resolve("SettlementContract") @@ -86,21 +88,24 @@ export class WorkerRegistrationTask } if (input.bridgeContractVerificationKey !== undefined) { - SettlementSmartContractBase.args.BridgeContractVerificationKey = - input.bridgeContractVerificationKey; + this.contractArgsRegistry.addArgs( + "SettlementContract", + { BridgeContractVerificationKey: input.bridgeContractVerificationKey } + ); } if (input.isSignedSettlement !== undefined) { - const contractArgs = SettlementSmartContractBase.args; - SettlementSmartContractBase.args = { - ...contractArgs, - signedSettlements: input.isSignedSettlement, - // TODO Add distinction between mina and custom tokens - BridgeContractPermissions: (input.isSignedSettlement - ? new SignedSettlementPermissions() - : new ProvenSettlementPermissions() - ).bridgeContractMina(), - }; + this.contractArgsRegistry.addArgs( + "SettlementContract", + { + signedSettlements: input.isSignedSettlement, + // TODO Add distinction between mina and custom tokens + BridgeContractPermissions: (input.isSignedSettlement + ? new SignedSettlementPermissions() + : new ProvenSettlementPermissions() + ).bridgeContractMina(), + } + ); } this.compileRegistry.addArtifactsRaw(input.compiledArtifacts); diff --git a/packages/sequencer/test-integration/benchmarks/tps.test.ts b/packages/sequencer/test-integration/benchmarks/tps.test.ts index b3eca0093..68e2dd5a0 100644 --- a/packages/sequencer/test-integration/benchmarks/tps.test.ts +++ b/packages/sequencer/test-integration/benchmarks/tps.test.ts @@ -104,9 +104,7 @@ export async function createAppChain() { maximumBlockSize: 100, }, BlockTrigger: {}, - Mempool: { - validationEnabled: false, - }, + Mempool: {}, }, Signer: { signer: PrivateKey.random(), diff --git a/packages/sequencer/test/integration/Mempool.test.ts b/packages/sequencer/test/integration/Block-order.test.ts similarity index 71% rename from packages/sequencer/test/integration/Mempool.test.ts rename to packages/sequencer/test/integration/Block-order.test.ts index 2609ef78f..6d7a1819f 100644 --- a/packages/sequencer/test/integration/Mempool.test.ts +++ b/packages/sequencer/test/integration/Block-order.test.ts @@ -1,11 +1,10 @@ -import { log, TypedClass } from "@proto-kit/common"; -import { VanillaProtocolModules } from "@proto-kit/library"; +import { expectDefined, log, TypedClass } from "@proto-kit/common"; import { Runtime } from "@proto-kit/module"; import { Protocol } from "@proto-kit/protocol"; import { Bool, PrivateKey, UInt64 } from "o1js"; import "reflect-metadata"; import { container } from "tsyringe"; -import { afterEach } from "@jest/globals"; +import { afterEach, expect, jest } from "@jest/globals"; import { InMemoryDatabase, @@ -15,6 +14,7 @@ import { StorageDependencyFactory, VanillaTaskWorkerModules, AppChain, + ManualBlockTrigger, } from "../../src"; import { DefaultTestingSequencerModules, @@ -25,17 +25,20 @@ import { Balance } from "./mocks/Balance"; import { createTransaction } from "./utils"; describe.each([["InMemory", InMemoryDatabase]])( - "Mempool test", + "Block Ordering test: %s", ( testName, Database: TypedClass ) => { let appChain: ReturnType; let sequencer: Sequencer< - DefaultTestingSequencerModules & { Database: typeof Database } + DefaultTestingSequencerModules & { + Database: typeof Database; + } >; let runtime: Runtime<{ Balance: typeof Balance }>; let mempool: PrivateMempool; + let trigger: ManualBlockTrigger; async function mempoolAddTransactions( userPrivateKey: PrivateKey, @@ -74,9 +77,7 @@ describe.each([["InMemory", InMemoryDatabase]])( const sequencerClass = Sequencer.from(testingSequencerModules({})); - const protocolClass = Protocol.from( - VanillaProtocolModules.mandatoryModules({}) - ); + const protocolClass = Protocol.from(Protocol.defaultModules()); return AppChain.from({ Sequencer: sequencerClass, @@ -97,9 +98,7 @@ describe.each([["InMemory", InMemoryDatabase]])( Sequencer: { Database: {}, BlockTrigger: {}, - Mempool: { - validationEnabled: true, - }, + Mempool: {}, FeeStrategy: {}, BatchProducerModule: {}, BlockProducerModule: {}, @@ -108,13 +107,7 @@ describe.each([["InMemory", InMemoryDatabase]])( TaskQueue: {}, SequencerStartupModule: {}, }, - Protocol: { - AccountState: {}, - BlockProver: {}, - StateTransitionProver: {}, - BlockHeight: {}, - LastStateRoot: {}, - }, + Protocol: Protocol.defaultConfig(), }); // Start AppChain @@ -124,14 +117,17 @@ describe.each([["InMemory", InMemoryDatabase]])( sequencer = appChain.sequencer; mempool = sequencer.resolve("Mempool"); + trigger = sequencer.resolve("BlockTrigger"); }); afterEach(async () => { await appChain.close(); + + jest.restoreAllMocks(); }); it("transactions are returned in right order - simple", async () => { - expect.assertions(13); + expect.assertions(14); await mempoolAddTransactions(user1PrivateKey, 0); await mempoolAddTransactions(user2PrivateKey, 0); @@ -140,7 +136,9 @@ describe.each([["InMemory", InMemoryDatabase]])( await mempoolAddTransactions(user2PrivateKey, 1); await mempoolAddTransactions(user3PrivateKey, 1); - const txs = await mempool.getTxs(); + const block = await trigger.produceBlock(); + expectDefined(block); + const txs = block.transactions.map((x) => x.tx); expect(txs).toHaveLength(6); expect(txs[0].nonce.toBigInt()).toStrictEqual(0n); @@ -158,7 +156,7 @@ describe.each([["InMemory", InMemoryDatabase]])( }); it("transactions are returned in right order - medium", async () => { - expect.assertions(13); + expect.assertions(14); log.setLevel("TRACE"); @@ -169,7 +167,9 @@ describe.each([["InMemory", InMemoryDatabase]])( await mempoolAddTransactions(user2PrivateKey, 1); await mempoolAddTransactions(user3PrivateKey, 0); - const txs = await mempool.getTxs(); + const block = await trigger.produceBlock(); + expectDefined(block); + const txs = block.transactions.map((x) => x.tx); expect(txs).toHaveLength(6); expect(txs[0].nonce.toBigInt()).toStrictEqual(0n); @@ -187,7 +187,7 @@ describe.each([["InMemory", InMemoryDatabase]])( }); it("transactions are returned in right order - harder", async () => { - expect.assertions(13); + expect.assertions(14); await mempoolAddTransactions(user1PrivateKey, 0); await mempoolAddTransactions(user2PrivateKey, 1); @@ -196,7 +196,9 @@ describe.each([["InMemory", InMemoryDatabase]])( await mempoolAddTransactions(user3PrivateKey, 0); await mempoolAddTransactions(user1PrivateKey, 1); - const txs = await mempool.getTxs(); + const block = await trigger.produceBlock(); + expectDefined(block); + const txs = block.transactions.map((x) => x.tx); expect(txs).toHaveLength(6); expect(txs[0].nonce.toBigInt()).toStrictEqual(0n); @@ -214,7 +216,7 @@ describe.each([["InMemory", InMemoryDatabase]])( }); it("transactions are returned in right order - hardest", async () => { - expect.assertions(13); + expect.assertions(14); await mempoolAddTransactions(user1PrivateKey, 0); await mempoolAddTransactions(user1PrivateKey, 4); @@ -225,7 +227,9 @@ describe.each([["InMemory", InMemoryDatabase]])( await mempoolAddTransactions(user3PrivateKey, 0); await mempoolAddTransactions(user1PrivateKey, 1); - const txs = await mempool.getTxs(); + const block = await trigger.produceBlock(); + expectDefined(block); + const txs = block.transactions.map((x) => x.tx); expect(txs).toHaveLength(6); expect(txs[0].nonce.toBigInt()).toStrictEqual(0n); @@ -241,5 +245,52 @@ describe.each([["InMemory", InMemoryDatabase]])( expect(txs[5].nonce.toBigInt()).toStrictEqual(1n); expect(txs[5].sender).toStrictEqual(user3PublicKey); }); + + it("transactions are returned in right order in multiple distinct blocks - hardest", async () => { + expect.assertions(18); + + sequencer.resolve("BlockProducerModule").config.maximumBlockSize = 3; + const txStorage = sequencer.resolve("TransactionStorage"); + const getTxsSpy = jest.spyOn(txStorage, "getPendingUserTransactions"); + + await mempoolAddTransactions(user1PrivateKey, 0); + await mempoolAddTransactions(user1PrivateKey, 4); + await mempoolAddTransactions(user1PrivateKey, 5); + await mempoolAddTransactions(user2PrivateKey, 1); + await mempoolAddTransactions(user3PrivateKey, 1); + await mempoolAddTransactions(user2PrivateKey, 0); + await mempoolAddTransactions(user3PrivateKey, 0); + await mempoolAddTransactions(user1PrivateKey, 1); + + let block = await trigger.produceBlock(); + expectDefined(block); + + let txs = block.transactions.map((x) => x.tx); + expect(txs).toHaveLength(3); + + expect(txs[0].nonce.toBigInt()).toStrictEqual(0n); + expect(txs[0].sender).toStrictEqual(user1PublicKey); + expect(txs[1].nonce.toBigInt()).toStrictEqual(0n); + expect(txs[1].sender).toStrictEqual(user2PublicKey); + expect(txs[2].nonce.toBigInt()).toStrictEqual(0n); + expect(txs[2].sender).toStrictEqual(user3PublicKey); + + expect(getTxsSpy).toHaveBeenCalledTimes(3); + + block = await trigger.produceBlock(); + expectDefined(block); + + txs = block.transactions.map((x) => x.tx); + expect(txs).toHaveLength(3); + + expect(txs[0].nonce.toBigInt()).toStrictEqual(1n); + expect(txs[0].sender).toStrictEqual(user2PublicKey); + expect(txs[1].nonce.toBigInt()).toStrictEqual(1n); + expect(txs[1].sender).toStrictEqual(user3PublicKey); + expect(txs[2].nonce.toBigInt()).toStrictEqual(1n); + expect(txs[2].sender).toStrictEqual(user1PublicKey); + + expect(getTxsSpy).toHaveBeenCalledTimes(4); + }); } ); diff --git a/packages/sequencer/test/integration/BlockProduction-test.ts b/packages/sequencer/test/integration/BlockProduction-test.ts index 3189f4798..58f1b0c94 100644 --- a/packages/sequencer/test/integration/BlockProduction-test.ts +++ b/packages/sequencer/test/integration/BlockProduction-test.ts @@ -34,6 +34,7 @@ import { DatabasePruneModule, AsyncLinkedLeafStore, AppChain, + BlockProducerModule, } from "../../src"; import { DefaultTestingSequencerModules, @@ -157,11 +158,7 @@ export function testBlockProduction< EventMaker: {}, }, Protocol: { - AccountState: {}, - BlockProver: {}, - StateTransitionProver: {}, - BlockHeight: {}, - LastStateRoot: {}, + ...Protocol.defaultConfig(), ProtocolStateTestHook: {}, }, }); @@ -422,7 +419,7 @@ export function testBlockProduction< const numberTxs = 3; - it("should produce block with multiple transaction", async () => { + it("should produce block with multiple transactions", async () => { log.setLevel("TRACE"); expect.assertions(6 + 4 * numberTxs); @@ -541,22 +538,24 @@ export function testBlockProduction< it.each([ [2, 1, 1], - [1, 2, 1], - [1, 1, 2], + [1, 5, 1], [2, 2, 2], [1, 14, 0], + [1, 6, 5], ])( "should produce multiple blocks with multiple batches with multiple transactions", async (batches, blocksPerBatch, txsPerBlock) => { expect.assertions( 2 * batches + - 1 * batches * blocksPerBatch + - 2 * batches * blocksPerBatch * txsPerBlock + 2 * batches * blocksPerBatch + + 1 * batches * blocksPerBatch * txsPerBlock ); log.setLevel("DEBUG"); - const sender = PrivateKey.random(); + const sender = PrivateKey.fromBase58( + "EKEiL7J4ouZGAz8uHo3oUebfA8zTWYYwLsojTyK9cAafi9sBBRpN" + ); const keys = range(0, batches * blocksPerBatch * txsPerBlock).map(() => PrivateKey.random() @@ -586,9 +585,9 @@ export function testBlockProduction< expect(block).toBeDefined(); + expect(block!.transactions).toHaveLength(txsPerBlock); for (let k = 0; k < txsPerBlock; k++) { - expect(block!.transactions).toHaveLength(txsPerBlock); - expect(block!.transactions[0].status.toBoolean()).toBe(true); + expect(block!.transactions[k].status.toBoolean()).toBe(true); } } @@ -686,6 +685,49 @@ export function testBlockProduction< expect(batch!.proof.proof).toBe(MOCK_PROOF); }, 30000); + it.each([4, 6, 9])( + "should produce some filled blocks and some empty blocks", + async (numBlocks) => { + log.setLevel("INFO"); + + (sequencer.resolve("BlockProducerModule") as BlockProducerModule).config = + { + maximumBlockSize: 5, + }; + + const privateKey = PrivateKey.random(); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + for (const i of range(0, 7)) { + await test.addTransaction({ + method: ["Balance", "addBalance"], + privateKey, + args: [PrivateKey.random().toPublicKey(), UInt64.from(100)], + }); + } + + // Produce 6 blocks, 5 txs each into 1 batch + const block = await test.produceBlock(); + + expectDefined(block); + expect(block.transactions).toHaveLength(5); + expect(block.transactions[0].status.toBoolean()).toBe(true); + + await mapSequential( + range(0, numBlocks - 1), + async () => await test.produceBlock() + ); + const batch = await test.produceBatch(); + + expectDefined(batch); + + console.log(batch.proof); + + expect(batch.blockHashes).toHaveLength(numBlocks); + }, + 30000 + ); + it("events - should produce block with the right events", async () => { log.setLevel("TRACE"); diff --git a/packages/sequencer/test/integration/BlockProductionSize.test.ts b/packages/sequencer/test/integration/BlockProductionSize.test.ts index ad263f018..67d6d7ca6 100644 --- a/packages/sequencer/test/integration/BlockProductionSize.test.ts +++ b/packages/sequencer/test/integration/BlockProductionSize.test.ts @@ -70,9 +70,7 @@ describe("block limit", () => { Sequencer: { Database: {}, BlockTrigger: {}, - Mempool: { - validationEnabled: true, - }, + Mempool: {}, BatchProducerModule: {}, BlockProducerModule: { maximumBlockSize: maxBlockSize, @@ -88,11 +86,7 @@ describe("block limit", () => { NoopRuntime: {}, }, Protocol: { - AccountState: {}, - BlockProver: {}, - StateTransitionProver: {}, - BlockHeight: {}, - LastStateRoot: {}, + ...Protocol.defaultConfig(), ProtocolStateTestHook: {}, }, }); diff --git a/packages/sequencer/test/integration/MempoolTxRemoved.test.ts b/packages/sequencer/test/integration/MempoolTxRemoved.test.ts index de598cde9..02fae71f5 100644 --- a/packages/sequencer/test/integration/MempoolTxRemoved.test.ts +++ b/packages/sequencer/test/integration/MempoolTxRemoved.test.ts @@ -1,12 +1,19 @@ +import "reflect-metadata"; import { Balances } from "@proto-kit/library"; import { Runtime } from "@proto-kit/module"; -import { TestingAppChain } from "@proto-kit/sdk"; import { Bool, PrivateKey, UInt64 } from "o1js"; -import "reflect-metadata"; import { expectDefined, log } from "@proto-kit/common"; import { afterEach, beforeEach, describe, expect } from "@jest/globals"; +import { Protocol } from "@proto-kit/protocol"; -import { PrivateMempool, Sequencer } from "../../src"; +import { + AppChain, + ManualBlockTrigger, + PrivateMempool, + Sequencer, + VanillaTaskWorkerModules, +} from "../../src"; +import { testingSequencerModules } from "../TestingSequencer"; import { createTransaction } from "./utils"; import { Balance } from "./mocks/Balance"; @@ -17,44 +24,61 @@ describe("mempool removal mechanism", () => { let mempool: PrivateMempool; let runtime: Runtime<{ Balances: typeof Balances; Balance: typeof Balance }>; let sequencer: Sequencer; + let trigger: ManualBlockTrigger; + + const createAppChain = async () => { + const app = AppChain.from({ + Sequencer: Sequencer.from(testingSequencerModules({})), + Protocol: Protocol.from(Protocol.defaultModules()), + Runtime: Runtime.from({ + Balances, + Balance, + }), + }); - const createAppChain = async (validationEnabled: boolean) => { - // eslint-disable-next-line @typescript-eslint/no-shadow - const appChain = TestingAppChain.fromRuntime({ Balance }); - - appChain.configurePartial({ + app.configurePartial({ Runtime: { Balance: {}, Balances: {}, }, Protocol: { - ...appChain.config.Protocol!, + ...Protocol.defaultConfig(), }, Sequencer: { - ...appChain.config.Sequencer, - Mempool: { validationEnabled }, + Database: {}, + BlockTrigger: {}, + Mempool: {}, + BatchProducerModule: {}, + BlockProducerModule: {}, + LocalTaskWorkerModule: VanillaTaskWorkerModules.defaultConfig(), + BaseLayer: {}, + TaskQueue: {}, + FeeStrategy: {}, + SequencerStartupModule: {}, }, }); - await appChain.start(); - runtime = appChain.runtime; - sequencer = appChain.sequencer; + await app.start(); + runtime = app.runtime; + sequencer = app.sequencer; // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment mempool = sequencer.resolve("Mempool"); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + trigger = sequencer.resolve("BlockTrigger"); - return appChain; + return app; }; + beforeEach(async () => { + appChain = await createAppChain(); + }, 60_000); + afterEach(async () => { await appChain.close(); }); describe("block pipeline reaction", () => { - beforeEach(async () => { - appChain = await createAppChain(false); - }, 60_000); - it("check only one is included, other is skipped", async () => { log.setLevel("trace"); @@ -81,12 +105,12 @@ describe("mempool removal mechanism", () => { const txs2 = await mempool.getTxs(); expect(txs2.length).toBe(2); - const block = await appChain.produceBlock(); + const block = await trigger.produceBlock(); expectDefined(block); expect(block.transactions).toHaveLength(1); - await expect(mempool.getTxs()).resolves.toHaveLength(1); + await expect(mempool.getTxs()).resolves.toHaveLength(0); }); it("check only one is included, other is removed", async () => { @@ -113,7 +137,7 @@ describe("mempool removal mechanism", () => { const txs2 = await mempool.getTxs(); expect(txs2.length).toBe(2); - const block = await appChain.produceBlock(); + const block = await trigger.produceBlock(); expectDefined(block); expect(block.transactions).toHaveLength(1); @@ -122,11 +146,7 @@ describe("mempool removal mechanism", () => { }); }); - describe("mempool simulation", () => { - beforeEach(async () => { - appChain = await createAppChain(true); - }, 60_000); - + describe("block production reordering", () => { it("check tx is removed", async () => { await mempool.add( createTransaction({ @@ -151,7 +171,7 @@ describe("mempool removal mechanism", () => { const txs = await mempool.getTxs(); expect(txs.length).toBe(2); - await appChain!.produceBlock(); + await trigger!.produceBlock(); await mempool.add( createTransaction({ diff --git a/packages/sequencer/test/integration/Proven.test.ts b/packages/sequencer/test/integration/Proven.test.ts index 6de1a5517..bca7f53af 100644 --- a/packages/sequencer/test/integration/Proven.test.ts +++ b/packages/sequencer/test/integration/Proven.test.ts @@ -9,11 +9,12 @@ import { import { Runtime } from "@proto-kit/module"; import { BridgeContract, + BridgingSettlementContract, + BridgingSettlementContractArgs, + ContractArgsRegistry, DispatchSmartContract, Protocol, SettlementContractModule, - SettlementSmartContract, - SettlementSmartContractBase, } from "@proto-kit/protocol"; import { VanillaProtocolModules } from "@proto-kit/library"; import { container } from "tsyringe"; @@ -66,7 +67,8 @@ describe.skip("Proven", () => { ProtocolStateTestHook, // ProtocolStateTestHook2, }), - SettlementContractModule: SettlementContractModule.with({ + SettlementContractModule: SettlementContractModule.from({ + ...SettlementContractModule.settlementAndBridging(), // FungibleToken: FungibleTokenContractModule, // FungibleTokenAdmin: FungibleTokenAdminContractModule, }), @@ -103,20 +105,13 @@ describe.skip("Proven", () => { type: "local", }, }, - SettlementModule: { - // TODO - feepayer: PrivateKey.random(), - }, + SettlementModule: {}, }, Runtime: { Balances: {}, }, Protocol: { - AccountState: {}, - BlockProver: {}, - StateTransitionProver: {}, - BlockHeight: {}, - LastStateRoot: {}, + ...Protocol.defaultConfig(), ProtocolStateTestHook: {}, SettlementContractModule: { SettlementContract: {}, @@ -152,12 +147,15 @@ describe.skip("Proven", () => { SettlementStartupModule ); - const vks = await module.retrieveVerificationKeys(); + const vks = await module.retrieveVerificationKeys({ + SettlementContract: true, + DispatchSmartContract: true, + }); console.log(vks); expect(vks.DispatchSmartContract).toBeDefined(); - expect(vks.SettlementSmartContract).toBeDefined(); + expect(vks.SettlementContract).toBeDefined(); }); it.skip("Hello", async () => { @@ -172,18 +170,21 @@ describe.skip("Proven", () => { }, }); vkService.setCompileRegistry(registry); - SettlementSmartContractBase.args = { - DispatchContract: DispatchSmartContract, - ChildVerificationKeyService: vkService, - BridgeContractVerificationKey: MOCK_VERIFICATION_KEY, - signedSettlements: false, - BridgeContract: BridgeContract, - hooks: [], - BridgeContractPermissions: - new ProvenSettlementPermissions().bridgeContractMina(), - escapeHatchSlotsInterval: 1000, - }; - const vk = await SettlementSmartContract.compile(); + + container + .resolve(ContractArgsRegistry) + .addArgs("SettlementContract", { + DispatchContract: DispatchSmartContract, + ChildVerificationKeyService: vkService, + BridgeContractVerificationKey: MOCK_VERIFICATION_KEY, + signedSettlements: false, + BridgeContract: BridgeContract, + hooks: [], + BridgeContractPermissions: + new ProvenSettlementPermissions().bridgeContractMina(), + escapeHatchSlotsInterval: 1000, + }); + const vk = await BridgingSettlementContract.compile(); console.log(vk.verificationKey); } catch (e) { console.error(e); diff --git a/packages/sequencer/test/integration/StorageIntegration.test.ts b/packages/sequencer/test/integration/StorageIntegration.test.ts index b26a5008b..55fb07b8f 100644 --- a/packages/sequencer/test/integration/StorageIntegration.test.ts +++ b/packages/sequencer/test/integration/StorageIntegration.test.ts @@ -1,6 +1,5 @@ import "reflect-metadata"; -import { expect } from "@jest/globals"; -import { VanillaProtocolModules } from "@proto-kit/library"; +import { afterAll, expect } from "@jest/globals"; import { Protocol } from "@proto-kit/protocol"; import { Runtime } from "@proto-kit/module"; import { Bool, Field, PrivateKey, UInt64 } from "o1js"; @@ -59,9 +58,6 @@ describe.each([["InMemory", InMemoryDatabase]])( let unprovenState: AsyncStateService; let provenState: AsyncStateService; - // let unprovenTreeStore: AsyncMerkleTreeStore; - // let provenTreeStore: AsyncMerkleTreeStore; - const sk = PrivateKey.random(); const pk = sk.toPublicKey(); let pkNonce = 0; @@ -77,9 +73,7 @@ describe.each([["InMemory", InMemoryDatabase]])( Balance, }); - const protocolClass = Protocol.from( - VanillaProtocolModules.mandatoryModules({}) - ); + const protocolClass = Protocol.from(Protocol.defaultModules()); return AppChain.from({ Sequencer: sequencerClass, @@ -107,13 +101,7 @@ describe.each([["InMemory", InMemoryDatabase]])( FeeStrategy: {}, SequencerStartupModule: {}, }, - Protocol: { - AccountState: {}, - BlockProver: {}, - StateTransitionProver: {}, - BlockHeight: {}, - LastStateRoot: {}, - }, + Protocol: Protocol.defaultConfig(), }); await appChain.start(false); @@ -125,6 +113,10 @@ describe.each([["InMemory", InMemoryDatabase]])( provenState = sequencer.resolve("AsyncStateService"); }); + afterAll(async () => { + await appChain.close(); + }); + it("test unproven block prod", async () => { await appChain.sequencer.resolve("Mempool").add( createTransaction({ @@ -212,7 +204,7 @@ describe.each([["InMemory", InMemoryDatabase]])( }); await mempool.add(tx); - const txs = await txStorage.getPendingUserTransactions(); + const txs = await txStorage.getPendingUserTransactions(0); expect(txs).toHaveLength(1); expect(txs[0].hash().toString()).toStrictEqual(tx.hash().toString()); @@ -220,7 +212,7 @@ describe.each([["InMemory", InMemoryDatabase]])( await sequencer.resolve("BlockTrigger").produceBlock(); await expect( - txStorage.getPendingUserTransactions() + txStorage.getPendingUserTransactions(0) ).resolves.toHaveLength(0); }, 60_000); } diff --git a/packages/sequencer/test/integration/mocks/ProtocolStateTestHook.ts b/packages/sequencer/test/integration/mocks/ProtocolStateTestHook.ts index 6ae5db6bf..00528849a 100644 --- a/packages/sequencer/test/integration/mocks/ProtocolStateTestHook.ts +++ b/packages/sequencer/test/integration/mocks/ProtocolStateTestHook.ts @@ -17,7 +17,7 @@ export class ProtocolStateTestHook extends ProvableTransactionHook { public async beforeTransaction( executionData: BeforeTransactionHookArguments ): Promise { - const { methodId } = executionData.transaction; + const { methodId } = executionData.transaction.transaction; const invocations = await this.methodIdInvocations.get(methodId); await this.methodIdInvocations.set( methodId, diff --git a/packages/sequencer/test/protocol/production/sequencing/atomic-block-production.test.ts b/packages/sequencer/test/protocol/production/sequencing/atomic-block-production.test.ts index a9b156023..79dccbcd4 100644 --- a/packages/sequencer/test/protocol/production/sequencing/atomic-block-production.test.ts +++ b/packages/sequencer/test/protocol/production/sequencing/atomic-block-production.test.ts @@ -3,7 +3,7 @@ import { Runtime } from "@proto-kit/module"; import { Protocol } from "@proto-kit/protocol"; import { VanillaProtocolModules } from "@proto-kit/library"; import { container } from "tsyringe"; -import { jest } from "@jest/globals"; +import { afterEach, jest } from "@jest/globals"; import { expectDefined } from "@proto-kit/common"; import { @@ -63,11 +63,7 @@ describe("atomic block production", () => { Balance: {}, }, Protocol: { - AccountState: {}, - BlockProver: {}, - StateTransitionProver: {}, - BlockHeight: {}, - LastStateRoot: {}, + ...Protocol.defaultConfig(), ProtocolStateTestHook: {}, }, }); @@ -80,6 +76,10 @@ describe("atomic block production", () => { trigger = app.sequencer.resolve("BlockTrigger"); }); + afterEach(async () => { + await appchain.close(); + }); + /** * This test does two passes on block generation. * In the first, the metadata generation function is mocked to throw an error diff --git a/packages/sequencer/test/settlement/Settlement-only.ts b/packages/sequencer/test/settlement/Settlement-only.ts new file mode 100644 index 000000000..c894bfbcf --- /dev/null +++ b/packages/sequencer/test/settlement/Settlement-only.ts @@ -0,0 +1,302 @@ +import "reflect-metadata"; +import { Field, PrivateKey } from "o1js"; +import { Runtime } from "@proto-kit/module"; +import { + BlockStorageNetworkStateModule, + ClientAppChain, + InMemoryBlockExplorer, + InMemorySigner, + InMemoryTransactionSender, + StateServiceQueryModule, +} from "@proto-kit/sdk"; +import { + BlockProverPublicInput, + ContractArgsRegistry, + Protocol, + SettlementContractModule, +} from "@proto-kit/protocol"; +import { UInt64, VanillaProtocolModules } from "@proto-kit/library"; +import { + expectDefined, + LinkedMerkleTree, + mapSequential, +} from "@proto-kit/common"; +import { container } from "tsyringe"; +import { afterAll } from "@jest/globals"; + +import { createTransaction } from "../integration/utils"; +import { testingSequencerModules } from "../TestingSequencer"; +import { + BlockQueue, + InMemoryMinaSigner, + ManualBlockTrigger, + MinaBaseLayer, + MinaBaseLayerConfig, + MinaNetworkUtils, + PendingTransaction, + PrivateMempool, + Sequencer, + SettlementModule, + SettlementProvingTask, + VanillaTaskWorkerModules, +} from "../../src"; + +import { Withdrawals } from "./mocks/Withdrawals"; +import { Balances } from "./mocks/Balances"; + +// Most of this code is copied from test/Settlement.ts and thinned down to +// settlement-only - eventually we should consolidate and/or make the API nicer +// to require less code +export const settlementOnlyTestFn = ( + settlementType: "signed" | "mock-proofs" | "proven", + baseLayerConfig: MinaBaseLayerConfig, + timeout: number = 120_000 +) => { + let testAccounts: PrivateKey[] = []; + + const sequencerKey = PrivateKey.random(); + const settlementKey = PrivateKey.random(); + + let trigger: ManualBlockTrigger; + let settlementModule: SettlementModule; + let blockQueue: BlockQueue; + + function setupAppChain() { + const runtime = Runtime.from({ + Balances, + Withdrawals, + }); + + // eslint-disable-next-line @typescript-eslint/dot-notation + MinaBaseLayer.prototype["isSignedSettlement"] = () => + settlementType === "signed"; + + const sequencer = Sequencer.from( + testingSequencerModules( + { + BaseLayer: MinaBaseLayer, + SettlementModule: SettlementModule, + SettlementSigner: InMemoryMinaSigner, + }, + { + SettlementProvingTask, + } + ) + ); + + const appchain = ClientAppChain.from({ + Runtime: runtime, + Sequencer: sequencer, + + Protocol: Protocol.from({ + ...VanillaProtocolModules.mandatoryModules({}), + SettlementContractModule: SettlementContractModule.from({ + ...SettlementContractModule.settlementOnly(), + }), + }), + + Signer: InMemorySigner, + TransactionSender: InMemoryTransactionSender, + QueryTransportModule: StateServiceQueryModule, + NetworkStateTransportModule: BlockStorageNetworkStateModule, + BlockExplorerTransportModule: InMemoryBlockExplorer, + }); + + appchain.configure({ + Runtime: { + Balances: { + totalSupply: UInt64.from(1000), + }, + Withdrawals: {}, + }, + + Sequencer: { + Database: {}, + BlockTrigger: {}, + Mempool: {}, + BatchProducerModule: {}, + LocalTaskWorkerModule: { + ...VanillaTaskWorkerModules.defaultConfig(), + }, + BaseLayer: baseLayerConfig, + SettlementSigner: { + feepayer: sequencerKey, + contractKeys: [settlementKey], + }, + BlockProducerModule: {}, + FeeStrategy: {}, + SettlementModule: {}, + SequencerStartupModule: {}, + + TaskQueue: { + simulatedDuration: 0, + }, + }, + Protocol: { + ...Protocol.defaultConfig(), + SettlementContractModule: { + SettlementContract: {}, + }, + }, + TransactionSender: {}, + QueryTransportModule: {}, + Signer: { + signer: sequencerKey, + }, + NetworkStateTransportModule: {}, + BlockExplorerTransportModule: {}, + }); + + return appchain; + } + + let appChain: ReturnType; + + async function createBatch( + withTransactions: boolean, + customNonce: number = 0, + txs: PendingTransaction[] = [] + ) { + const mempool = appChain.sequencer.resolve("Mempool") as PrivateMempool; + if (withTransactions) { + const key = testAccounts[0]; + const tx = createTransaction({ + runtime: appChain.runtime, + method: ["Balances", "mint"], + privateKey: key, + args: [Field(1), key.toPublicKey(), UInt64.from(1e9 * 100)], + nonce: customNonce, + }); + + await mempool.add(tx); + } + await mapSequential(txs, async (tx) => { + await mempool.add(tx); + }); + + const result = await trigger.produceBlockAndBatch(); + const [block] = result; + + console.log( + `block ${block?.height.toString()} ${block?.fromMessagesHash.toString()} -> ${block?.toMessagesHash.toString()}` + ); + + return result; + } + + beforeAll(async () => { + appChain = setupAppChain(); + + await appChain.start( + settlementType === "proven", + container.createChildContainer() + ); + + settlementModule = appChain.sequencer.resolve( + "SettlementModule" + ) as SettlementModule; + trigger = + appChain.sequencer.dependencyContainer.resolve( + "BlockTrigger" + ); + blockQueue = appChain.sequencer.resolve("BlockQueue") as BlockQueue; + + const networkUtils = + appChain.sequencer.dependencyContainer.resolve( + "NetworkUtils" + ); + const accs = await networkUtils.getFundedAccounts(3); + testAccounts = accs.slice(1); + + await networkUtils.waitForNetwork(); + + console.log( + `Funding ${sequencerKey.toPublicKey().toBase58()} from ${accs[0].toPublicKey().toBase58()}` + ); + + await networkUtils.faucet(sequencerKey.toPublicKey(), 20 * 1e9); + }, timeout * 3); + + afterAll(async () => { + container.resolve(ContractArgsRegistry).resetArgs("SettlementContract"); + + await appChain.close(); + }); + + let nonceCounter = 0; + + it("should throw error", async () => { + await expect(settlementModule.checkDeployment()).rejects.toThrow(); + }); + + it( + "should deploy settlement contracts", + async () => { + // Deploy contract + await settlementModule.deploy( + { + settlementContract: settlementKey.toPublicKey(), + }, + { + nonce: nonceCounter, + } + ); + + nonceCounter += 1; + + console.log("Deployed"); + }, + timeout + ); + + it( + "should settle", + async () => { + try { + const [, batch] = await createBatch(true); + + const input = BlockProverPublicInput.fromFields( + batch!.proof.publicInput.map((x) => Field(x)) + ); + expect(input.stateRoot.toString()).toStrictEqual( + LinkedMerkleTree.EMPTY_ROOT.toString() + ); + + const lastBlock = await blockQueue.getLatestBlockAndResult(); + + await trigger.settle(batch!, {}); + nonceCounter++; + + // TODO Check Smartcontract tx layout (call to dispatch with good preconditions, etc) + + console.log("Block settled"); + + await settlementModule.utils.fetchContractAccounts({ + address: settlementModule.getSettlementContractAddress(), + }); + const settlement = settlementModule.getSettlementContract(); + expectDefined(lastBlock); + expectDefined(lastBlock.result); + expect(settlement.networkStateHash.get().toString()).toStrictEqual( + lastBlock!.result.afterNetworkState.hash().toString() + ); + expect(settlement.stateRoot.get().toString()).toStrictEqual( + lastBlock!.result.stateRoot.toString() + ); + expect(settlement.blockHashRoot.get().toString()).toStrictEqual( + lastBlock!.result.blockHashRoot.toString() + ); + } catch (e) { + console.error(e); + throw e; + } + }, + timeout + ); + + it("should not throw error after settlement", async () => { + expect.assertions(1); + + await expect(settlementModule.checkDeployment()).resolves.toBeUndefined(); + }); +}; diff --git a/packages/sequencer/test/settlement/Settlement.test.ts b/packages/sequencer/test/settlement/Settlement.test.ts index fd7ee60c5..b1aa7044a 100644 --- a/packages/sequencer/test/settlement/Settlement.test.ts +++ b/packages/sequencer/test/settlement/Settlement.test.ts @@ -3,6 +3,7 @@ import { FungibleToken } from "mina-fungible-token"; import { MinaBaseLayerConfig } from "../../src"; import { settlementTestFn } from "./Settlement"; +import { settlementOnlyTestFn } from "./Settlement-only"; describe.each(["mock-proofs", "signed"] as const)( "Settlement contracts: local blockchain - %s", @@ -22,5 +23,9 @@ describe.each(["mock-proofs", "signed"] as const)( tokenOwner: FungibleToken, }); }); + + describe("Settlement only", () => { + settlementOnlyTestFn(type, network); + }); } ); diff --git a/packages/sequencer/test/settlement/Settlement.ts b/packages/sequencer/test/settlement/Settlement.ts index 49704ba7f..0b89e8871 100644 --- a/packages/sequencer/test/settlement/Settlement.ts +++ b/packages/sequencer/test/settlement/Settlement.ts @@ -9,19 +9,20 @@ import { Runtime } from "@proto-kit/module"; import { BlockProverPublicInput, BridgeContract, + ContractArgsRegistry, + DispatchSmartContract, NetworkState, Protocol, ReturnType, SettlementContractModule, - SettlementSmartContractBase, } from "@proto-kit/protocol"; import { ClientAppChain, BlockStorageNetworkStateModule, - InMemorySigner, InMemoryTransactionSender, StateServiceQueryModule, InMemoryBlockExplorer, + InMemorySigner, } from "@proto-kit/sdk"; import { AccountUpdate, @@ -34,6 +35,7 @@ import { SmartContract, UInt8, Bool, + PublicKey, } from "o1js"; import "reflect-metadata"; import { container } from "tsyringe"; @@ -53,13 +55,14 @@ import { ProvenSettlementPermissions, VanillaTaskWorkerModules, Sequencer, + InMemoryMinaSigner, + CircuitAnalysisModule, } from "../../src"; import { BlockProofSerializer } from "../../src/protocol/production/tasks/serializers/BlockProofSerializer"; import { testingSequencerModules } from "../TestingSequencer"; import { createTransaction } from "../integration/utils"; import { FeeStrategy } from "../../src/protocol/baselayer/fees/FeeStrategy"; import { BridgingModule } from "../../src/settlement/BridgingModule"; -import { SettlementUtils } from "../../src/settlement/utils/SettlementUtils"; import { FungibleTokenContractModule } from "../../src/settlement/utils/FungibleTokenContractModule"; import { FungibleTokenAdminContractModule } from "../../src/settlement/utils/FungibleTokenAdminContractModule"; import { MinaNetworkUtils } from "../../src/protocol/baselayer/network-utils/MinaNetworkUtils"; @@ -88,6 +91,12 @@ export const settlementTestFn = ( tokenOwner: PrivateKey.random(), admin: PrivateKey.random(), }; + + const tokenOwnerPubKeys = { + tokenOwner: tokenOwnerKey.tokenOwner.toPublicKey(), + admin: tokenOwnerKey.admin.toPublicKey(), + }; + const tokenOwner = tokenConfig !== undefined ? // eslint-disable-next-line new-cap @@ -98,6 +107,7 @@ export const settlementTestFn = ( let settlementModule: SettlementModule; let bridgingModule: BridgingModule; let blockQueue: BlockQueue; + let userPublicKey: PublicKey; let feeStrategy: FeeStrategy; @@ -113,7 +123,7 @@ export const settlementTestFn = ( }); // eslint-disable-next-line @typescript-eslint/dot-notation - SettlementUtils.prototype["isSignedSettlement"] = () => + MinaBaseLayer.prototype["isSignedSettlement"] = () => settlementType === "signed"; const sequencer = Sequencer.from( @@ -121,6 +131,8 @@ export const settlementTestFn = ( { BaseLayer: MinaBaseLayer, SettlementModule: SettlementModule, + BridgingModule: BridgingModule, + SettlementSigner: InMemoryMinaSigner, }, { SettlementProvingTask, @@ -134,7 +146,8 @@ export const settlementTestFn = ( Protocol: Protocol.from({ ...VanillaProtocolModules.mandatoryModules({}), - SettlementContractModule: SettlementContractModule.with({ + SettlementContractModule: SettlementContractModule.from({ + ...SettlementContractModule.settlementAndBridging(), FungibleToken: FungibleTokenContractModule, FungibleTokenAdmin: FungibleTokenAdminContractModule, }), @@ -161,13 +174,23 @@ export const settlementTestFn = ( BlockTrigger: {}, Mempool: {}, BatchProducerModule: {}, - LocalTaskWorkerModule: VanillaTaskWorkerModules.defaultConfig(), + LocalTaskWorkerModule: { + ...VanillaTaskWorkerModules.defaultConfig(), + }, BaseLayer: baseLayerConfig, - BlockProducerModule: {}, - FeeStrategy: {}, - SettlementModule: { + SettlementSigner: { feepayer: sequencerKey, + contractKeys: [settlementKey, dispatchKey, minaBridgeKey], + tokenBridgeKeys: [ + tokenBridgeKey, + tokenOwnerKey.tokenOwner, + tokenOwnerKey.admin, + ], }, + BlockProducerModule: {}, + FeeStrategy: {}, + SettlementModule: {}, + BridgingModule: {}, SequencerStartupModule: {}, TaskQueue: { @@ -175,11 +198,7 @@ export const settlementTestFn = ( }, }, Protocol: { - StateTransitionProver: {}, - BlockHeight: {}, - AccountState: {}, - BlockProver: {}, - LastStateRoot: {}, + ...Protocol.defaultConfig(), SettlementContractModule: { SettlementContract: {}, BridgeContract: {}, @@ -259,7 +278,6 @@ export const settlementTestFn = ( bridgingModule = appChain.sequencer.resolve( "BridgingModule" ) as BridgingModule; - trigger = appChain.sequencer.dependencyContainer.resolve( "BlockTrigger" @@ -287,8 +305,7 @@ export const settlementTestFn = ( }, timeout * 3); afterAll(async () => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - SettlementSmartContractBase.args = undefined as any; + container.resolve(ContractArgsRegistry).resetArgs("SettlementContract"); await appChain.close(); }); @@ -297,19 +314,62 @@ export const settlementTestFn = ( let user0Nonce = 0; let acc0L2Nonce = 0; + it.skip("Print constraint summary", async () => { + await appChain.protocol.dependencyContainer + .resolve(CircuitAnalysisModule) + .printSummary(); + }); + + it("should throw error", async () => { + const additionalAddresses = + tokenConfig === undefined + ? undefined + : [ + { + address: tokenBridgeKey.toPublicKey(), + tokenId: tokenOwner!.deriveTokenId(), + }, + ]; + + await expect( + settlementModule.checkDeployment(additionalAddresses) + ).rejects.toThrow(); + }); + + it( + "should deploy settlement contracts", + async () => { + // Deploy contract + await settlementModule.deploy( + { + dispatchContract: dispatchKey.toPublicKey(), + settlementContract: settlementKey.toPublicKey(), + }, + { + nonce: nonceCounter, + } + ); + + nonceCounter += 1; + + console.log("Deployed"); + }, + timeout + ); + it( - "should deploy", + "should deploy mina bridge", async () => { // Deploy contract - await settlementModule.deploy(settlementKey, dispatchKey, minaBridgeKey, { + await bridgingModule.deployMinaBridge(minaBridgeKey.toPublicKey(), { nonce: nonceCounter, }); - nonceCounter += 2; + nonceCounter += 1; - console.log("Deployed"); + console.log("Deployed mina bridge"); }, - timeout * 2 + timeout ); if (tokenConfig !== undefined) { @@ -360,11 +420,14 @@ export const settlementTestFn = ( ); console.log(tx.toPretty()); - settlementModule.signTransaction( - tx, - [sequencerKey, tokenOwnerKey.tokenOwner, tokenOwnerKey.admin], - [tokenOwnerKey.tokenOwner, tokenOwnerKey.admin] - ); + settlementModule.utils.signTransaction(tx, { + signingWithSignatureCheck: [ + tokenOwnerPubKeys.tokenOwner, + tokenOwnerPubKeys.admin, + settlementModule.getSettlementContractAddress(), + bridgingModule.getDispatchContractAddress(), + ], + }); await appChain.sequencer .resolveOrFail("TransactionSender", MinaTransactionSender) @@ -404,11 +467,12 @@ export const settlementTestFn = ( // tokenOwner!.self.body.incrementNonce = Bool(false); } ); - settlementModule.utils.signTransaction( - tx, - [sequencerKey], - [tokenOwnerKey.tokenOwner, tokenOwnerKey.admin] - ); + settlementModule.utils.signTransaction(tx, { + signingWithSignatureCheck: [ + tokenOwnerPubKeys.tokenOwner, + tokenOwnerPubKeys.admin, + ], + }); await appChain.sequencer .resolveOrFail("TransactionSender", MinaTransactionSender) @@ -420,10 +484,9 @@ export const settlementTestFn = ( it( "should deploy custom token bridge", async () => { - await settlementModule.deployTokenBridge( + await bridgingModule.deployTokenBridge( tokenOwner!, - tokenOwnerKey.tokenOwner, - tokenBridgeKey, + tokenBridgeKey.toPublicKey(), { nonce: nonceCounter++, } @@ -431,7 +494,6 @@ export const settlementTestFn = ( console.log( `Token bridge address: ${tokenBridgeKey.toPublicKey().toBase58()} @ ${tokenOwner!.deriveTokenId().toString()}` ); - expect(tokenOwner!.deriveTokenId().toString()).toStrictEqual( bridgedTokenId.toString() ); @@ -464,9 +526,9 @@ export const settlementTestFn = ( console.log("Block settled"); await settlementModule.utils.fetchContractAccounts({ - address: settlementModule.getAddresses().settlement, + address: settlementModule.getSettlementContractAddress(), }); - const { settlement } = settlementModule.getContracts(); + const settlement = settlementModule.getSettlementContract(); expectDefined(lastBlock); expectDefined(lastBlock.result); expect(settlement.networkStateHash.get().toString()).toStrictEqual( @@ -490,7 +552,9 @@ export const settlementTestFn = ( "should include deposit", async () => { try { - const { settlement, dispatch } = settlementModule.getContracts(); + const settlement = settlementModule.getSettlementContract(); + const dispatch = + bridgingModule.getDispatchContract() as DispatchSmartContract; const bridge = new BridgeContract( tokenBridgeKey.toPublicKey(), bridgedTokenId @@ -537,12 +601,17 @@ export const settlementTestFn = ( } ); - settlementModule.signTransaction( - tx, - [userKey], - [tokenOwnerKey.tokenOwner], - [dispatch.address] - ); + // Register userKey, to use later. + userPublicKey = settlementModule.utils.registerKey(userKey); + + settlementModule.utils.signTransaction(tx, { + signingWithSignatureCheck: [ + tokenOwnerPubKeys.tokenOwner, + settlementModule.getSettlementContractAddress(), + ], + signingPublicKeys: [userPublicKey], + preventNoncePreconditionFor: [dispatch.address], + }); console.log(tx.toPretty()); @@ -551,6 +620,12 @@ export const settlementTestFn = ( .proveAndSendTransaction(tx, "included"); const actions = await Mina.fetchActions(dispatch.address); + if (baseLayerConfig.network.type !== "local") { + await fetchAccount({ + publicKey: tokenBridgeKey.toPublicKey(), + tokenId: bridgedTokenId, + }); + } const balanceDiff = bridge.account.balance .get() .sub(contractBalanceBefore); @@ -644,8 +719,8 @@ export const settlementTestFn = ( const settlementResult = await trigger.settle(batch, { [bridgedTokenId.toString()]: { - bridgingContractPrivateKey: tokenBridgeKey, - tokenOwnerPrivateKey: tokenOwnerKey.tokenOwner, + bridgingContractPublicKey: tokenBridgeKey.toPublicKey(), + tokenOwnerPublicKey: tokenOwnerKey.tokenOwner.toPublicKey(), tokenOwner: tokenOwner, }, }); @@ -657,12 +732,11 @@ export const settlementTestFn = ( expect(settlementResult.bridgeTransactions).toHaveLength(2); - if (baseLayerConfig.network.type !== "local") { - await fetchAccount({ - publicKey: userKey.toPublicKey(), - tokenId: bridgingContract.deriveTokenId(), - }); - } + await settlementModule.utils.fetchContractAccounts({ + address: userKey.toPublicKey(), + tokenId: bridgingContract.deriveTokenId(), + }); + const account = Mina.getAccount( userKey.toPublicKey(), bridgingContract.deriveTokenId() @@ -720,11 +794,14 @@ export const settlementTestFn = ( } ); - const signed = settlementModule.signTransaction( - tx, - [userKey], - [tokenBridgeKey, tokenOwnerKey.tokenOwner] - ); + const signed = settlementModule.utils.signTransaction(tx, { + signingWithSignatureCheck: [ + tokenBridgeKey.toPublicKey(), + tokenOwnerPubKeys.tokenOwner, + settlementModule.getSettlementContractAddress(), + ], + signingPublicKeys: [userPublicKey], + }); await appChain.sequencer .resolveOrFail("TransactionSender", MinaTransactionSender) @@ -750,4 +827,23 @@ export const settlementTestFn = ( }, timeout ); + + it("should not throw error after settlement", async () => { + expect.assertions(1); + + // Obtain promise of deployment check + const additionalAddresses = + tokenConfig === undefined + ? undefined + : [ + { + address: tokenBridgeKey.toPublicKey(), + tokenId: tokenOwner!.deriveTokenId(), + }, + ]; + + await expect( + settlementModule.checkDeployment(additionalAddresses) + ).resolves.toBeUndefined(); + }); }; diff --git a/packages/stack/src/presets/modules/index.ts b/packages/stack/src/presets/modules/index.ts index bdb606004..bf1d0a4d6 100644 --- a/packages/stack/src/presets/modules/index.ts +++ b/packages/stack/src/presets/modules/index.ts @@ -20,6 +20,7 @@ import { InMemoryDatabase, LocalTaskQueue, AppChainModulesRecord, + InMemoryMinaSigner, } from "@proto-kit/sequencer"; import { IndexerNotifier, @@ -100,6 +101,7 @@ export class DefaultModules { FeeStrategy: ConstantFeeStrategy, BatchProducerModule, SettlementModule, + SettlementSigner: InMemoryMinaSigner, LocalTaskWorkerModule: LocalTaskWorkerModule.from( VanillaTaskWorkerModules.allTasks() ), @@ -379,17 +381,20 @@ export class DefaultConfigs { }, }, SettlementModule: { - feepayer: PrivateKey.fromBase58(config.sequencerPrivateKey), - keys: { - settlement: PrivateKey.fromBase58( + addresses: { + SettlementContract: PrivateKey.fromBase58( config.settlementContractPrivateKey - ), - dispatch: PrivateKey.fromBase58(config.dispatcherContractPrivateKey), - minaBridge: PrivateKey.fromBase58( - config.minaBridgeContractPrivateKey - ), + ).toPublicKey(), }, }, + SettlementSigner: { + feepayer: PrivateKey.fromBase58(config.sequencerPrivateKey), + contractKeys: [ + PrivateKey.fromBase58(config.settlementContractPrivateKey), + PrivateKey.fromBase58(config.dispatcherContractPrivateKey), + PrivateKey.fromBase58(config.minaBridgeContractPrivateKey), + ], + }, FeeStrategy: {}, BatchProducerModule: {}, LocalTaskWorkerModule: VanillaTaskWorkerModules.defaultConfig(), diff --git a/packages/stack/src/scripts/graphql/server.ts b/packages/stack/src/scripts/graphql/server.ts index a927694bf..2cdaf2604 100644 --- a/packages/stack/src/scripts/graphql/server.ts +++ b/packages/stack/src/scripts/graphql/server.ts @@ -137,6 +137,7 @@ export async function startServer() { Protocol: { BlockProver: {}, StateTransitionProver: {}, + TransactionProver: {}, AccountState: {}, BlockHeight: {}, TransactionFee: { diff --git a/packages/stack/src/scripts/worker/app.ts b/packages/stack/src/scripts/worker/app.ts index 2ae296279..f1964c30d 100644 --- a/packages/stack/src/scripts/worker/app.ts +++ b/packages/stack/src/scripts/worker/app.ts @@ -34,6 +34,7 @@ export const app = { protocol: { AccountState: {}, BlockProver: {}, + TransactionProver: {}, BlockHeight: {}, StateTransitionProver: {}, LastStateRoot: {}, diff --git a/packages/stack/test/graphql/graphql-server.ts b/packages/stack/test/graphql/graphql-server.ts index 01c26e565..584fb6fb2 100644 --- a/packages/stack/test/graphql/graphql-server.ts +++ b/packages/stack/test/graphql/graphql-server.ts @@ -135,10 +135,7 @@ export async function startGraphqlServer() { }, Protocol: { - BlockProver: {}, - StateTransitionProver: {}, - AccountState: {}, - BlockHeight: {}, + ...Protocol.defaultConfig(), TransactionFee: { tokenId: 0n, feeRecipient: PrivateKey.random().toPublicKey().toBase58(), @@ -146,7 +143,6 @@ export async function startGraphqlServer() { methods: {}, perWeightUnitFee: 0n, }, - LastStateRoot: {}, }, Sequencer: { diff --git a/packages/stack/test/graphql/graphql.test.ts b/packages/stack/test/graphql/graphql.test.ts index 2cbb2526f..45b29206b 100644 --- a/packages/stack/test/graphql/graphql.test.ts +++ b/packages/stack/test/graphql/graphql.test.ts @@ -57,10 +57,7 @@ function prepareClient() { }, Protocol: { - AccountState: {}, - BlockProver: {}, - StateTransitionProver: {}, - BlockHeight: {}, + ...Protocol.defaultConfig(), TransactionFee: { tokenId: 0n, feeRecipient: PrivateKey.random().toPublicKey().toBase58(), @@ -68,7 +65,6 @@ function prepareClient() { methods: {}, perWeightUnitFee: 0n, }, - LastStateRoot: {}, }, Sequencer: {