diff --git a/nx.json b/nx.json index 816559d48..3729decc9 100644 --- a/nx.json +++ b/nx.json @@ -17,6 +17,7 @@ "prisma:generate": { "inputs": [ "{projectRoot}/package.json", + "{projectRoot}/prisma/*", "{projectRoot}/prisma/**/*" ] }, diff --git a/package-lock.json b/package-lock.json index d311f8a4e..64fcd3f4e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1087,6 +1087,8 @@ }, "node_modules/@ioredis/commands": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", + "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==", "license": "MIT", "peer": true }, @@ -2467,6 +2469,8 @@ }, "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", + "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", "cpu": [ "arm64" ], @@ -2477,6 +2481,76 @@ ], "peer": true }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", + "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", + "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", + "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", + "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", + "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "license": "MIT", @@ -3258,10 +3332,11 @@ } }, "node_modules/@prisma/client": { - "version": "5.21.1", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.21.1.tgz", - "integrity": "sha512-3n+GgbAZYjaS/k0M03yQsQfR1APbr411r74foknnsGpmhNKBG49VuUkxIU6jORgvJPChoD4WC4PqoHImN1FP0w==", + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz", + "integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==", "hasInstallScript": true, + "license": "Apache-2.0", "engines": { "node": ">=16.13" }, @@ -3275,84 +3350,93 @@ } }, "node_modules/@prisma/debug": { - "version": "5.21.1", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.21.1.tgz", - "integrity": "sha512-uY8SAhcnORhvgtOrNdvWS98Aq/nkQ9QDUxrWAgW8XrCZaI3j2X7zb7Xe6GQSh6xSesKffFbFlkw0c2luHQviZA==" + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz", + "integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==", + "license": "Apache-2.0" }, "node_modules/@prisma/engines": { - "version": "5.21.1", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.21.1.tgz", - "integrity": "sha512-hGVTldUkIkTwoV8//hmnAAiAchi4oMEKD3aW5H2RrnI50tTdwza7VQbTTAyN3OIHWlK5DVg6xV7X8N/9dtOydA==", + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz", + "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==", "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "5.21.1", - "@prisma/engines-version": "5.21.1-1.bf0e5e8a04cada8225617067eaa03d041e2bba36", - "@prisma/fetch-engine": "5.21.1", - "@prisma/get-platform": "5.21.1" + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/fetch-engine": "5.22.0", + "@prisma/get-platform": "5.22.0" } }, "node_modules/@prisma/engines-version": { - "version": "5.21.1-1.bf0e5e8a04cada8225617067eaa03d041e2bba36", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.21.1-1.bf0e5e8a04cada8225617067eaa03d041e2bba36.tgz", - "integrity": "sha512-qvnEflL0//lh44S/T9NcvTMxfyowNeUxTunPcDfKPjyJNrCNf2F1zQLcUv5UHAruECpX+zz21CzsC7V2xAeM7Q==" + "version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz", + "integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==", + "license": "Apache-2.0" }, "node_modules/@prisma/fetch-engine": { - "version": "5.21.1", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.21.1.tgz", - "integrity": "sha512-70S31vgpCGcp9J+mh/wHtLCkVezLUqe/fGWk3J3JWZIN7prdYSlr1C0niaWUyNK2VflLXYi8kMjAmSxUVq6WGQ==", + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz", + "integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==", + "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "5.21.1", - "@prisma/engines-version": "5.21.1-1.bf0e5e8a04cada8225617067eaa03d041e2bba36", - "@prisma/get-platform": "5.21.1" + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/get-platform": "5.22.0" } }, "node_modules/@prisma/generator-helper": { - "version": "5.21.1", - "resolved": "https://registry.npmjs.org/@prisma/generator-helper/-/generator-helper-5.21.1.tgz", - "integrity": "sha512-56+FLaNGO7uKIEjN5asV7L0cAWqTc+IoyFbtafYnzfvBS3HURgT+l9UGHrHfPO5EWFiot3my3UOJ/hGZfhNPbA==", + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/generator-helper/-/generator-helper-5.22.0.tgz", + "integrity": "sha512-LwqcBQ5/QsuAaLNQZAIVIAJDJBMjHwMwn16e06IYx/3Okj/xEEfw9IvrqB2cJCl3b2mCBlh3eVH0w9WGmi4aHg==", + "license": "Apache-2.0", "peer": true, "dependencies": { - "@prisma/debug": "5.21.1" + "@prisma/debug": "5.22.0" } }, "node_modules/@prisma/get-platform": { - "version": "5.21.1", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.21.1.tgz", - "integrity": "sha512-sRxjL3Igst3ct+e8ya/x//cDXmpLbZQ5vfps2N4tWl4VGKQAmym77C/IG/psSMsQKszc8uFC/q1dgmKFLUgXZQ==", + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz", + "integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==", + "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "5.21.1" + "@prisma/debug": "5.22.0" } }, "node_modules/@prisma/internals": { - "version": "5.21.1", - "resolved": "https://registry.npmjs.org/@prisma/internals/-/internals-5.21.1.tgz", - "integrity": "sha512-XCjJUUDBwT0ioLrTfUKA/YsVs5gtWtMisxRssGS5tgqVaVzc8XEY01avtYIzyhz8RDLn7QlecTQGHW+5J9+w6Q==", + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/internals/-/internals-5.22.0.tgz", + "integrity": "sha512-Rsjw2ARB9VQzDczzEimUriSBdXmYG/Z5tNRer2IEwof/O8Q6A9cqV3oNVUpJ52TgWfQqMAq5K/KEf8LvvYLLOw==", + "license": "Apache-2.0", "peer": true, "dependencies": { - "@prisma/debug": "5.21.1", - "@prisma/engines": "5.21.1", - "@prisma/fetch-engine": "5.21.1", - "@prisma/generator-helper": "5.21.1", - "@prisma/get-platform": "5.21.1", - "@prisma/prisma-schema-wasm": "5.21.1-1.bf0e5e8a04cada8225617067eaa03d041e2bba36", - "@prisma/schema-files-loader": "5.21.1", + "@prisma/debug": "5.22.0", + "@prisma/engines": "5.22.0", + "@prisma/fetch-engine": "5.22.0", + "@prisma/generator-helper": "5.22.0", + "@prisma/get-platform": "5.22.0", + "@prisma/prisma-schema-wasm": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/schema-files-loader": "5.22.0", "arg": "5.0.2", "prompts": "2.4.2" } }, "node_modules/@prisma/prisma-schema-wasm": { - "version": "5.21.1-1.bf0e5e8a04cada8225617067eaa03d041e2bba36", - "resolved": "https://registry.npmjs.org/@prisma/prisma-schema-wasm/-/prisma-schema-wasm-5.21.1-1.bf0e5e8a04cada8225617067eaa03d041e2bba36.tgz", - "integrity": "sha512-JJnZ7A0CSW4SNFSWe2/GexJ/Z5s69+3UqbsBuGxJe7EQyVgw/hmskmhFmM4gccihh9PA2hlDip5kmraAfO7vSA==", + "version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "resolved": "https://registry.npmjs.org/@prisma/prisma-schema-wasm/-/prisma-schema-wasm-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz", + "integrity": "sha512-WPNB7SgTxF/rSHMa5o5/9AIINy4oVnRhvUkRzqR4Nfp8Hu9Q2IyUptxuiDuzRVJdjJBRi/U82sHTxyiD3oBBhQ==", + "license": "Apache-2.0", "peer": true }, "node_modules/@prisma/schema-files-loader": { - "version": "5.21.1", - "resolved": "https://registry.npmjs.org/@prisma/schema-files-loader/-/schema-files-loader-5.21.1.tgz", - "integrity": "sha512-rcZgI4rSq5tlieWQ5e+AfyjquwQ+d2vCecvbf9qvfcvs/Nw5FbjAPTLEx9l4g/iW4qgc0CTPLr19azD3npwHQw==", + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/schema-files-loader/-/schema-files-loader-5.22.0.tgz", + "integrity": "sha512-/TNAJXvMSk6mCgZa+gIBM6sp5OUQBnb7rbjiSQm88gvcSibxEuKkVV/2pT3RmQpEAn1yiabvS4+dOvIotYe3ww==", + "license": "Apache-2.0", "peer": true, "dependencies": { - "@prisma/prisma-schema-wasm": "5.21.1-1.bf0e5e8a04cada8225617067eaa03d041e2bba36", + "@prisma/prisma-schema-wasm": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", "fs-extra": "11.1.1" } }, @@ -3360,6 +3444,7 @@ "version": "11.1.1", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", + "license": "MIT", "peer": true, "dependencies": { "graceful-fs": "^4.2.0", @@ -3707,6 +3792,7 @@ "version": "0.24.0", "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.24.0.tgz", "integrity": "sha512-c1xMmNHWpNselmpIqursHeOHHBTIsJLbB+NuovbTTRCNiTLEr/U9dbJ8qy0jd/O2x5pc3seWuOUN5R2IoOTp8A==", + "license": "MIT", "peer": true, "dependencies": { "fast-glob": "^3.3.2", @@ -3719,6 +3805,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "license": "MIT", "peer": true, "bin": { "mkdirp": "dist/cjs/src/bin.js" @@ -3877,6 +3964,7 @@ "version": "1.3.9", "resolved": "https://registry.npmjs.org/@types/graphql-fields/-/graphql-fields-1.3.9.tgz", "integrity": "sha512-HynTnp1HrE58uYcFcAK5UOfdrHSOIHDLCjvMU4yCmQLMj21uo7ZiZqnDGrD27pgCgHH5a1e8GYNK98Ndmma7ig==", + "license": "MIT", "peer": true, "dependencies": { "graphql": "*" @@ -4728,6 +4816,7 @@ "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "license": "MIT", "peer": true }, "node_modules/argparse": { @@ -5189,6 +5278,8 @@ }, "node_modules/blakejs": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/blakejs/-/blakejs-1.2.1.tgz", + "integrity": "sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ==", "license": "MIT", "peer": true }, @@ -5293,7 +5384,9 @@ } }, "node_modules/bullmq": { - "version": "3.15.8", + "version": "3.16.2", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-3.16.2.tgz", + "integrity": "sha512-wZIsCdI2H6lza6GdePquCWbrslhYHS7GnDPpP0hzoHkvKiBOt/5jHcsHcHhJi/fob+dfo8dazWKTSLlGLFFqUw==", "license": "MIT", "peer": true, "dependencies": { @@ -5309,6 +5402,9 @@ }, "node_modules/bullmq/node_modules/glob": { "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", "license": "ISC", "peer": true, "dependencies": { @@ -5327,6 +5423,8 @@ }, "node_modules/bullmq/node_modules/minimatch": { "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "license": "ISC", "peer": true, "dependencies": { @@ -5338,6 +5436,8 @@ }, "node_modules/bullmq/node_modules/uuid": { "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" @@ -5835,6 +5935,7 @@ "version": "13.0.3", "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz", "integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==", + "license": "MIT", "peer": true }, "node_modules/code-excerpt": { @@ -6371,6 +6472,8 @@ }, "node_modules/cron-parser": { "version": "4.9.0", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz", + "integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==", "license": "MIT", "peer": true, "dependencies": { @@ -6683,6 +6786,8 @@ }, "node_modules/denque": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", "license": "Apache-2.0", "peer": true, "engines": { @@ -6737,6 +6842,8 @@ }, "node_modules/detect-libc": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", "license": "Apache-2.0", "optional": true, "peer": true, @@ -8615,6 +8722,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/graphql-fields/-/graphql-fields-2.0.3.tgz", "integrity": "sha512-x3VE5lUcR4XCOxPIqaO4CE+bTK8u6gVouOdpQX9+EKHr+scqtK5Pp/l8nIGqIpN1TUlkKE6jDCCycm/WtLRAwA==", + "license": "MIT", "peer": true }, "node_modules/graphql-query-complexity": { @@ -9919,7 +10027,9 @@ } }, "node_modules/ioredis": { - "version": "5.4.1", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.5.0.tgz", + "integrity": "sha512-7CutT89g23FfSa8MDoIFs2GYYa0PaNiW/OrT+nRyjRXHDZd17HmIgy+reOQ/yhh72NznNjGuS8kbCAcA4Ro4mw==", "license": "MIT", "peer": true, "dependencies": { @@ -10398,6 +10508,8 @@ }, "node_modules/isomorphic-fetch": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", + "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", "license": "MIT", "peer": true, "dependencies": { @@ -12206,6 +12318,8 @@ }, "node_modules/js-sha256": { "version": "0.9.0", + "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz", + "integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA==", "license": "MIT", "peer": true }, @@ -13268,6 +13382,8 @@ }, "node_modules/lodash.defaults": { "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", "license": "MIT", "peer": true }, @@ -13284,6 +13400,8 @@ }, "node_modules/lodash.isarguments": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", "license": "MIT", "peer": true }, @@ -13538,7 +13656,9 @@ "license": "MIT" }, "node_modules/luxon": { - "version": "3.4.4", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", + "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==", "license": "MIT", "peer": true, "engines": { @@ -14413,7 +14533,9 @@ "license": "MIT" }, "node_modules/msgpackr": { - "version": "1.10.2", + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.2.tgz", + "integrity": "sha512-F9UngXRlPyWCDEASDpTf6c9uNhGPTqnTeLVt7bN+bU1eajoR/8V9ys2BRaV5C/e5ihE6sJ9uPIKaYt6bFuO32g==", "license": "MIT", "peer": true, "optionalDependencies": { @@ -14422,6 +14544,8 @@ }, "node_modules/msgpackr-extract": { "version": "3.0.3", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", + "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", "hasInstallScript": true, "license": "MIT", "optional": true, @@ -14567,6 +14691,8 @@ }, "node_modules/node-gyp-build-optional-packages": { "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", "license": "MIT", "optional": true, "peer": true, @@ -15684,6 +15810,8 @@ }, "node_modules/o1js/node_modules/cachedir": { "version": "2.4.0", + "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.4.0.tgz", + "integrity": "sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ==", "license": "MIT", "peer": true, "engines": { @@ -16374,6 +16502,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "license": "MIT", "peer": true }, "node_modules/path-exists": { @@ -16533,6 +16662,7 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "license": "MIT", "peer": true, "engines": { "node": ">=4" @@ -16614,12 +16744,13 @@ } }, "node_modules/prisma": { - "version": "5.21.1", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.21.1.tgz", - "integrity": "sha512-PB+Iqzld/uQBPaaw2UVIk84kb0ITsLajzsxzsadxxl54eaU5Gyl2/L02ysivHxK89t7YrfQJm+Ggk37uvM70oQ==", + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz", + "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { - "@prisma/engines": "5.21.1" + "@prisma/engines": "5.22.0" }, "bin": { "prisma": "build/index.js" @@ -17203,6 +17334,8 @@ }, "node_modules/redis-errors": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", "license": "MIT", "peer": true, "engines": { @@ -17211,6 +17344,8 @@ }, "node_modules/redis-parser": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", "license": "MIT", "peer": true, "dependencies": { @@ -17992,6 +18127,8 @@ }, "node_modules/standard-as-callback": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", "license": "MIT", "peer": true }, @@ -18560,6 +18697,7 @@ "version": "23.0.0", "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-23.0.0.tgz", "integrity": "sha512-FcvFx7a9E8TUe6T3ShihXJLiJOiqyafzFKUO4aqIHDUCIvADdGNShcbc2W5PMr3LerXRv7mafvFZ9lRENxJmug==", + "license": "MIT", "peer": true, "dependencies": { "@ts-morph/common": "~0.24.0", @@ -18649,6 +18787,8 @@ }, "node_modules/tsyringe": { "version": "4.8.0", + "resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.8.0.tgz", + "integrity": "sha512-YB1FG+axdxADa3ncEtRnQCFq/M0lALGLxSZeVNbTU8NqhOVc51nnv2CISTcvc1kyv6EGPtXVr0v6lWeDxiijOA==", "license": "MIT", "peer": true, "dependencies": { @@ -18660,6 +18800,8 @@ }, "node_modules/tsyringe/node_modules/tslib": { "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "license": "0BSD", "peer": true }, @@ -18983,6 +19125,7 @@ "version": "0.28.0", "resolved": "https://registry.npmjs.org/typegraphql-prisma/-/typegraphql-prisma-0.28.0.tgz", "integrity": "sha512-LMTsYEAteeXhnJRmH2sBWarR8Jc9Um13mtSpi8Vh6G8QUTlp5tRfqMjd+Fw06HS6SBshU6NilZJlsPaz26SQCw==", + "license": "MIT", "peer": true, "dependencies": { "@prisma/generator-helper": "^5.18.0", @@ -19378,6 +19521,8 @@ }, "node_modules/whatwg-fetch": { "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", "license": "MIT", "peer": true }, @@ -19934,7 +20079,7 @@ "dependencies": { "@envelop/extended-validation": "^4.1.0", "@inkjs/ui": "^1.0.0", - "@prisma/client": "^5.18.0", + "@prisma/client": "^5.22.0", "@types/yargs": "^17.0.29", "figlet": "^1.7.0", "ink": "^4.4.1", @@ -20024,9 +20169,9 @@ "version": "0.1.1-develop.833+397881ed", "license": "MIT", "dependencies": { - "@prisma/client": "^5.18.0", + "@prisma/client": "^5.22.0", "lodash": "^4.17.21", - "prisma": "^5.18.0", + "prisma": "^5.22.0", "redis": "^4.6.12", "reflect-metadata": "^0.1.13" }, @@ -20049,7 +20194,7 @@ "license": "MIT", "dependencies": { "@inkjs/ui": "^1.0.0", - "@prisma/client": "^5.19.1", + "@prisma/client": "^5.22.0", "@types/yargs": "^17.0.29", "figlet": "^1.7.0", "ink": "^4.4.1", diff --git a/packages/api/src/graphql/modules/MerkleWitnessResolver.ts b/packages/api/src/graphql/modules/MerkleWitnessResolver.ts index 8c59b8cc8..c48a734ab 100644 --- a/packages/api/src/graphql/modules/MerkleWitnessResolver.ts +++ b/packages/api/src/graphql/modules/MerkleWitnessResolver.ts @@ -3,8 +3,10 @@ import { Length } from "class-validator"; import { inject } from "tsyringe"; import { RollupMerkleTree, RollupMerkleTreeWitness } from "@proto-kit/common"; import { - AsyncMerkleTreeStore, + BlockStorage, CachedMerkleTreeStore, + MaskName, + TreeStoreCreator, } from "@proto-kit/sequencer"; import { GraphqlModule, graphqlModule } from "../GraphqlModule"; @@ -34,7 +36,9 @@ export class MerkleWitnessDTO { @graphqlModule() export class MerkleWitnessResolver extends GraphqlModule { public constructor( - @inject("AsyncMerkleStore") private readonly treeStore: AsyncMerkleTreeStore + @inject("TreeStoreCreator") + private readonly treeStoreCreator: TreeStoreCreator, + @inject("BlockStorage") private readonly blockStorage: BlockStorage ) { super(); } @@ -44,7 +48,14 @@ export class MerkleWitnessResolver extends GraphqlModule { "Allows retrieval of merkle witnesses corresponding to a specific path in the appchain's state tree. These proves are generally retrieved from the current 'proven' state", }) public async witness(@Arg("path") path: string) { - const syncStore = new CachedMerkleTreeStore(this.treeStore); + const latestBlock = await this.blockStorage.getLatestBlock(); + const maskName = + latestBlock !== undefined + ? MaskName.block(latestBlock.block.height) + : MaskName.base(); + const treeStore = this.treeStoreCreator.getMask(maskName); + + const syncStore = new CachedMerkleTreeStore(treeStore); await syncStore.preloadKey(BigInt(path)); const tree = new RollupMerkleTree(syncStore); diff --git a/packages/indexer/package.json b/packages/indexer/package.json index 76d942113..fe42eeef9 100644 --- a/packages/indexer/package.json +++ b/packages/indexer/package.json @@ -22,7 +22,7 @@ "dependencies": { "@envelop/extended-validation": "^4.1.0", "@inkjs/ui": "^1.0.0", - "@prisma/client": "^5.18.0", + "@prisma/client": "^5.22.0", "@types/yargs": "^17.0.29", "figlet": "^1.7.0", "ink": "^4.4.1", diff --git a/packages/persistance/package.json b/packages/persistance/package.json index 1d2e7d640..235f08cb5 100644 --- a/packages/persistance/package.json +++ b/packages/persistance/package.json @@ -20,9 +20,9 @@ "access": "public" }, "dependencies": { - "@prisma/client": "^5.18.0", + "@prisma/client": "^5.22.0", "lodash": "^4.17.21", - "prisma": "^5.18.0", + "prisma": "^5.22.0", "redis": "^4.6.12", "reflect-metadata": "^0.1.13" }, diff --git a/packages/persistance/prisma/migrations/20250129151707_state_masking/migration.sql b/packages/persistance/prisma/migrations/20250129151707_state_masking/migration.sql new file mode 100644 index 000000000..bb4f4821c --- /dev/null +++ b/packages/persistance/prisma/migrations/20250129151707_state_masking/migration.sql @@ -0,0 +1,29 @@ +/* + Warnings: + + - The primary key for the `State` table will be changed. If it partially fails, the table could be left without primary key constraint. + - You are about to drop the column `mask` on the `State` table. All the data in the column will be lost. + - Added the required column `maskId` to the `State` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +TRUNCATE TABLE "State"; +ALTER TABLE "State" DROP CONSTRAINT "State_pkey", +DROP COLUMN "mask", +ADD COLUMN "maskId" INTEGER NOT NULL, +ADD CONSTRAINT "State_pkey" PRIMARY KEY ("path", "maskId"); + +-- CreateTable +CREATE TABLE "Mask" ( + "id" SERIAL NOT NULL, + "name" VARCHAR(256) NOT NULL, + "parent" INTEGER, + + CONSTRAINT "Mask_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "State" ADD CONSTRAINT "State_maskId_fkey" FOREIGN KEY ("maskId") REFERENCES "Mask"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Mask" ADD CONSTRAINT "Mask_parent_fkey" FOREIGN KEY ("parent") REFERENCES "Mask"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/packages/persistance/prisma/schema.prisma b/packages/persistance/prisma/schema.prisma index 58c214477..24ce7059a 100644 --- a/packages/persistance/prisma/schema.prisma +++ b/packages/persistance/prisma/schema.prisma @@ -3,9 +3,6 @@ generator client { provider = "prisma-client-js" - // output = "./../node_modules/.prisma/client" - // Enable after upgrade to 5.9.0 - // previewFeatures = ["relationJoins"] } datasource db { @@ -16,9 +13,21 @@ datasource db { model State { path Decimal @db.Decimal(78, 0) values Decimal[] @db.Decimal(78, 0) - mask String @db.VarChar(256) - @@id([path, mask]) + maskId Int + mask Mask @relation(fields: [maskId], references: [id]) + + @@id([path, maskId]) +} + +model Mask { + id Int @id @default(autoincrement()) + name String @db.VarChar(256) + + parent Int? + parentMask Mask? @relation("ParentMasks", fields: [parent], references: [id]) + children Mask[] @relation("ParentMasks") + State State[] } model Transaction { diff --git a/packages/persistance/src/PrismaDatabaseConnection.ts b/packages/persistance/src/PrismaDatabaseConnection.ts index 54f5cfb30..9a04dbae0 100644 --- a/packages/persistance/src/PrismaDatabaseConnection.ts +++ b/packages/persistance/src/PrismaDatabaseConnection.ts @@ -6,12 +6,12 @@ import { } from "@proto-kit/sequencer"; import { DependencyFactory, OmitKeys } from "@proto-kit/common"; -import { PrismaStateService } from "./services/prisma/PrismaStateService"; import { PrismaBatchStore } from "./services/prisma/PrismaBatchStore"; import { PrismaBlockStorage } from "./services/prisma/PrismaBlockStorage"; import { PrismaSettlementStorage } from "./services/prisma/PrismaSettlementStorage"; import { PrismaMessageStorage } from "./services/prisma/PrismaMessageStorage"; import { PrismaTransactionStorage } from "./services/prisma/PrismaTransactionStorage"; +import { PrismaStateServiceCreator } from "./creators/PrismaStateServiceCreator"; export interface PrismaDatabaseConfig { // Either object-based config or connection string @@ -49,12 +49,9 @@ export class PrismaDatabaseConnection public dependencies(): OmitKeys< StorageDependencyMinimumDependencies, - "asyncMerkleStore" | "blockTreeStore" | "unprovenMerkleStore" + "blockTreeStore" | "treeStoreCreator" > { return { - asyncStateService: { - useFactory: () => new PrismaStateService(this, "batch"), - }, batchStorage: { useClass: PrismaBatchStore, }, @@ -64,9 +61,6 @@ export class PrismaDatabaseConnection blockStorage: { useClass: PrismaBlockStorage, }, - unprovenStateService: { - useFactory: () => new PrismaStateService(this, "block"), - }, settlementStorage: { useClass: PrismaSettlementStorage, }, @@ -76,6 +70,9 @@ export class PrismaDatabaseConnection transactionStorage: { useClass: PrismaTransactionStorage, }, + stateServiceCreator: { + useClass: PrismaStateServiceCreator, + }, }; } @@ -90,6 +87,7 @@ export class PrismaDatabaseConnection "Settlement", "IncomingMessageBatch", "IncomingMessageBatchTransaction", + "Mask", ]; await this.prismaClient.$transaction( diff --git a/packages/persistance/src/PrismaRedisDatabase.ts b/packages/persistance/src/PrismaRedisDatabase.ts index 735703035..34f8fa639 100644 --- a/packages/persistance/src/PrismaRedisDatabase.ts +++ b/packages/persistance/src/PrismaRedisDatabase.ts @@ -4,6 +4,7 @@ import { StorageDependencyMinimumDependencies, Database, closeable, + Sequencer, } from "@proto-kit/sequencer"; import { ChildContainerProvider } from "@proto-kit/common"; import { PrismaClient } from "@prisma/client"; @@ -20,6 +21,7 @@ import { RedisConnectionModule, RedisTransaction, } from "./RedisConnection"; +import { inject } from "tsyringe"; export interface PrismaRedisCombinedConfig { prisma: PrismaDatabaseConfig; @@ -36,10 +38,10 @@ export class PrismaRedisDatabase public redis: RedisConnectionModule; - public constructor() { + public constructor(@inject("Sequencer") sequencer: Sequencer) { super(); this.prisma = new PrismaDatabaseConnection(); - this.redis = new RedisConnectionModule(); + this.redis = new RedisConnectionModule(sequencer); } public get prismaClient(): PrismaClient { diff --git a/packages/persistance/src/RedisConnection.ts b/packages/persistance/src/RedisConnection.ts index da10ea78e..553baf42d 100644 --- a/packages/persistance/src/RedisConnection.ts +++ b/packages/persistance/src/RedisConnection.ts @@ -1,12 +1,15 @@ import { createClient, RedisClientType } from "redis"; import { + Sequencer, SequencerModule, StorageDependencyMinimumDependencies, } from "@proto-kit/sequencer"; -import { DependencyFactory } from "@proto-kit/common"; +import { DependencyFactory, log } from "@proto-kit/common"; import isArray from "lodash/isArray"; import { RedisMerkleTreeStore } from "./services/redis/RedisMerkleTreeStore"; +import { RedisTreeStoreCreator } from "./creators/RedisTreeStoreCreator"; +import { TreeMaskRecovery } from "./TreeMaskRecovery"; export interface RedisConnectionConfig { host: string; @@ -26,6 +29,10 @@ export class RedisConnectionModule extends SequencerModule implements DependencyFactory, RedisConnection { + public constructor(private readonly sequencer: Sequencer) { + super(); + } + private client?: RedisClientType; public get redisClient(): RedisClientType { @@ -39,18 +46,15 @@ export class RedisConnectionModule public dependencies(): Pick< StorageDependencyMinimumDependencies, - "asyncMerkleStore" | "blockTreeStore" | "unprovenMerkleStore" + "blockTreeStore" | "treeStoreCreator" > { return { - asyncMerkleStore: { - useFactory: () => new RedisMerkleTreeStore(this), - }, - unprovenMerkleStore: { - useFactory: () => new RedisMerkleTreeStore(this, "unproven"), - }, blockTreeStore: { useFactory: () => new RedisMerkleTreeStore(this, "blockHash"), }, + treeStoreCreator: { + useClass: RedisTreeStoreCreator, + }, }; } @@ -81,8 +85,19 @@ export class RedisConnectionModule } } + private async recoverMemoryMasks() { + log.info("Starting recovery of tree masks"); + + const recovery = + this.sequencer.dependencyContainer.resolve(TreeMaskRecovery); + + await recovery.recreateMasks(); + } + public async start(): Promise { await this.init(); + + await this.recoverMemoryMasks(); } public async close() { diff --git a/packages/persistance/src/TreeMaskRecovery.ts b/packages/persistance/src/TreeMaskRecovery.ts new file mode 100644 index 000000000..aab7528aa --- /dev/null +++ b/packages/persistance/src/TreeMaskRecovery.ts @@ -0,0 +1,87 @@ +import { inject, injectable } from "tsyringe"; +import { + applyStateDiff, + assertBlockHasResult, + AsyncMerkleTreeStore, + BlockQueue, + BlockWithResult, + CachedMerkleTreeStore, + collectStateDiff, + MaskName, + TreeStoreCreator, +} from "@proto-kit/sequencer"; +import { reduceSequential } from "@proto-kit/common"; + +@injectable() +export class TreeMaskRecovery { + public constructor( + @inject("BlockQueue") + private readonly blockQueue: BlockQueue, + @inject("TreeStoreCreator") + private readonly treeStoreCreator: TreeStoreCreator + ) {} + + /** + * Strategy: + * 1. Collect all STs + * 2. Collect a distinct diff of all to-values (state diff) + * 3. Iteratively apply that to the tree + */ + private async applyBlock(mask: AsyncMerkleTreeStore, block: BlockWithResult) { + const transitions = [block.block.beforeBlockStateTransitions]; + const txTransitions = block.block.transactions.flatMap((tx) => + tx.stateTransitions + .filter((batch) => batch.applied) + .map((batch) => batch.stateTransitions) + ); + transitions.push(...txTransitions); + transitions.push(block.result.afterBlockStateTransitions); + + const stateDiff = collectStateDiff(transitions.flat()); + const cache = new CachedMerkleTreeStore(mask); + + await applyStateDiff(cache, stateDiff); + + await cache.mergeIntoParent(); + } + + /** + * This method fetches all pending (i.e. un-batched) blocks and recreates + * their in-memory tree masks. + * + * The crash might have happened between block production and result generation. + * In this case, we need to first recreate all earlier masks, then generate the result. + * The last part happens automatically in BlockProducerModule.start(). + * However, since normal block production doesn't do any tree ops, we can skip + * that potential incomplete block altogether. + */ + public async recreateMasks() { + const blocks = await this.blockQueue.getPendingBlocks(); + + await reduceSequential( + blocks, + async (previousMask, block) => { + if (previousMask === undefined) { + throw new Error( + "More than one result missing for the recent blocks, something is wrong" + ); + } + if (block.result !== undefined) { + const maskName = MaskName.block(block.block.height); + const mask = await this.treeStoreCreator.createMask( + maskName, + previousMask + ); + + assertBlockHasResult(block); + + await this.applyBlock(mask, block); + + return maskName; + } + return undefined; + }, + MaskName.base() as string | undefined + ); + } +} diff --git a/packages/persistance/src/creators/PrismaStateServiceCreator.ts b/packages/persistance/src/creators/PrismaStateServiceCreator.ts new file mode 100644 index 000000000..3b1de25b3 --- /dev/null +++ b/packages/persistance/src/creators/PrismaStateServiceCreator.ts @@ -0,0 +1,33 @@ +import { AsyncStateService, StateServiceCreator } from "@proto-kit/sequencer"; +import { inject, injectable } from "tsyringe"; + +import { PrismaStateService } from "../services/prisma/PrismaStateService"; +import type { PrismaConnection } from "../PrismaDatabaseConnection"; + +@injectable() +export class PrismaStateServiceCreator implements StateServiceCreator { + public constructor( + @inject("Database") private readonly connection: PrismaConnection + ) {} + + public async createMask( + name: string, + parent: string + ): Promise { + return new PrismaStateService(this.connection, name, parent); + } + + public getMask(name: string): AsyncStateService { + return new PrismaStateService(this.connection, name); + } + + public async mergeIntoParent(name: string): Promise { + const service = new PrismaStateService(this.connection, name); + await service.mergeIntoParent(); + } + + public async drop(name: string): Promise { + const service = new PrismaStateService(this.connection, name); + await service.drop(); + } +} diff --git a/packages/persistance/src/creators/RedisTreeStoreCreator.ts b/packages/persistance/src/creators/RedisTreeStoreCreator.ts new file mode 100644 index 000000000..79d21497a --- /dev/null +++ b/packages/persistance/src/creators/RedisTreeStoreCreator.ts @@ -0,0 +1,24 @@ +import { + AsyncMerkleTreeStore, + InMemoryMerkleTreeStoreMask, + MaskGraph, + TreeStoreCreator, +} from "@proto-kit/sequencer"; +import { inject, injectable } from "tsyringe"; + +import { RedisMerkleTreeStore } from "../services/redis/RedisMerkleTreeStore"; +import type { RedisConnection } from "../RedisConnection"; + +@injectable() +export class RedisTreeStoreCreator + extends MaskGraph< + AsyncMerkleTreeStore, + RedisMerkleTreeStore, + InMemoryMerkleTreeStoreMask + > + implements TreeStoreCreator +{ + public constructor(@inject("Database") connection: RedisConnection) { + super(new RedisMerkleTreeStore(connection, "base")); + } +} diff --git a/packages/persistance/src/services/prisma/PrismaBlockStorage.ts b/packages/persistance/src/services/prisma/PrismaBlockStorage.ts index 51ebaed62..4ed8b1615 100644 --- a/packages/persistance/src/services/prisma/PrismaBlockStorage.ts +++ b/packages/persistance/src/services/prisma/PrismaBlockStorage.ts @@ -197,7 +197,7 @@ export class PrismaBlockStorage return result; } - public async getNewBlocks(): Promise { + public async getPendingBlocks(): Promise { const blocks = await this.connection.prismaClient.block.findMany({ where: { batch: null, @@ -226,15 +226,10 @@ export class PrismaBlockStorage const { result } = block; - if (result === null) { - throw new Error( - `No BlockResult has been set for block ${block.hash} yet` - ); - } - return { block: decodedBlock, - result: this.blockResultMapper.mapIn(result), + result: + result !== null ? this.blockResultMapper.mapIn(result) : undefined, }; }); } diff --git a/packages/persistance/src/services/prisma/PrismaStateService.ts b/packages/persistance/src/services/prisma/PrismaStateService.ts index 8e34fd22c..6535fdfd2 100644 --- a/packages/persistance/src/services/prisma/PrismaStateService.ts +++ b/packages/persistance/src/services/prisma/PrismaStateService.ts @@ -1,4 +1,4 @@ -import { AsyncStateService, StateEntry } from "@proto-kit/sequencer"; +import { AsyncStateService, MaskName, StateEntry } from "@proto-kit/sequencer"; import { Field } from "o1js"; import { Prisma } from "@prisma/client"; import { noop } from "@proto-kit/common"; @@ -6,6 +6,10 @@ import { injectable } from "tsyringe"; import type { PrismaConnection } from "../../PrismaDatabaseConnection"; +import { readState } from "./sql/readState"; +import { deleteCollisionsFromParentMask } from "./sql/deleteCollisionsFromParentMask"; +import { mergeIntoParent } from "./sql/mergeIntoParent"; + // We need to create a correctly configured Decimal constructor // with our parameters const Decimal = Prisma.Decimal.clone({ @@ -16,24 +20,92 @@ const Decimal = Prisma.Decimal.clone({ export class PrismaStateService implements AsyncStateService { private cache: StateEntry[] = []; + private maskId?: { id: number; parentId?: number }; + /** * @param connection - * @param mask A indicator to which masking level the values belong + * @param mask A indicator to which masking level the values belong. + * This name has to be unique + * @param parentName */ public constructor( private readonly connection: PrismaConnection, - private readonly mask: string + private readonly mask: string, + private readonly parentName?: string ) {} + private async getMaskId(): Promise<{ id: number; parentId?: number }> { + if (this.maskId === undefined) { + this.maskId = await this.initializeMask(this.mask, this.parentName); + } + return this.maskId; + } + + private async initializeMask( + mask: string, + parentName?: string + ): Promise<{ id: number; parentId?: number }> { + const { prismaClient } = this.connection; + + const found = await prismaClient.mask.findFirst({ + where: { + name: mask, + }, + include: { + parentMask: true, + }, + }); + + if (found === null) { + // Find parent id + let parentId: number | undefined = undefined; + if (parentName !== undefined) { + const parent = await prismaClient.mask.findFirst({ + where: { + name: parentName, + }, + }); + if (parent === null) { + throw new Error(`Parent mask with name ${parentName} not found`); + } + + parentId = parent.id; + } else if (mask !== MaskName.base()) { + throw new Error( + "Can't initialize mask that's not the base using a null-parent " + ); + } + + // Create mask + const createdMask = await prismaClient.mask.create({ + data: { + parent: parentId, + name: mask, + }, + }); + return { + id: createdMask.id, + parentId: parentId, + }; + } + + return { + id: found.id, + parentId: found.parent ?? undefined, + }; + } + public async commit(): Promise { const { prismaClient } = this.connection; + const { id: maskId } = await this.getMaskId(); + const data = this.cache .filter((entry) => entry.value !== undefined) .map((entry) => ({ path: new Decimal(entry.key.toString()), values: entry.value!.map((field) => new Decimal(field.toString())), - mask: this.mask, + maskId, })); await prismaClient.state.deleteMany({ @@ -41,7 +113,7 @@ export class PrismaStateService implements AsyncStateService { path: { in: this.cache.map((x) => new Decimal(x.key.toString())), }, - mask: this.mask, + maskId, }, }); await prismaClient.state.createMany({ @@ -52,23 +124,20 @@ export class PrismaStateService implements AsyncStateService { } public async getMany(keys: Field[]): Promise { - const records = await this.connection.prismaClient.state.findMany({ - where: { - AND: [ - { - path: { - in: keys.map((key) => new Decimal(key.toString())), - }, - }, - { - mask: this.mask, - }, - ], - }, - }); + const { id: maskId } = await this.getMaskId(); + const paths = keys.map((key) => new Decimal(key.toString())); + + const records: { + path: Prisma.Decimal; + // TODO This could potentially be non-null, but should be tested + values: Prisma.Decimal[] | null; + }[] = await this.connection.prismaClient.$queryRaw( + readState(maskId, paths) + ); + return records.map((record) => ({ key: Field(record.path.toFixed()), - value: record.values.map((x) => Field(x.toFixed())), + value: record.values?.map((x) => Field(x.toFixed())) ?? [], })); } @@ -84,4 +153,64 @@ export class PrismaStateService implements AsyncStateService { public writeStates(entries: StateEntry[]): void { this.cache.push(...entries); } + + public async createMask(name: string): Promise { + // We only call this to make sure this mask actually exists, therefore that the + // relation can be satisfied + await this.getMaskId(); + return new PrismaStateService(this.connection, name, this.mask); + } + + public async mergeIntoParent(): Promise { + const { id: maskId, parentId } = await this.getMaskId(); + + const client = this.connection.prismaClient; + + if (parentId !== undefined) { + // Rough strategy here: + // 1. Delete all entries that are bound to be overwritten from the parent mask + // 2. Update this mask's entries to parent mask id + // 3. Re-link all children of this mask to this mask's parent + // 4. Delete mask + + await client.$transaction([ + client.$queryRaw(deleteCollisionsFromParentMask(maskId)), + // MergeIntoParent could be a prisma query, but it isn't, because eventually, + // this Service should be stateless, therefore the parentId wouldn't be on hand + // anymore, so we need to inline it's retrieval into the query + client.$queryRaw(mergeIntoParent(maskId)), + client.mask.updateMany({ + where: { + parent: maskId, + }, + data: { + parent: parentId, + }, + }), + client.mask.delete({ + where: { + id: maskId, + }, + }), + ]); + } else { + throw new Error("Can't merge into parent without a parent"); + } + } + + public async drop(): Promise { + const { id: maskId } = await this.getMaskId(); + + await this.connection.prismaClient.state.deleteMany({ + where: { + maskId, + }, + }); + + await this.connection.prismaClient.mask.delete({ + where: { + id: maskId, + }, + }); + } } diff --git a/packages/persistance/src/services/prisma/sql/deleteCollisionsFromParentMask.ts b/packages/persistance/src/services/prisma/sql/deleteCollisionsFromParentMask.ts new file mode 100644 index 000000000..c711180f5 --- /dev/null +++ b/packages/persistance/src/services/prisma/sql/deleteCollisionsFromParentMask.ts @@ -0,0 +1,24 @@ +import { Prisma } from "@prisma/client"; + +/* + DELETE PATHS FROM PARENT MASK + + Strategy on this query: + 1. Find the mask we are interested in, as a record with the id and it's parent id + 2. Select all paths that exist on that mask + 3. Delete all records whose mask is the parent of (1), and whose path is contained in (2) + */ + +export function deleteCollisionsFromParentMask(maskId: number) { + return Prisma.sql` + WITH mask AS ( + SELECT id, parent FROM "Mask" m WHERE m.id = ${maskId}), + paths AS ( + SELECT path FROM "State" s + JOIN mask m on s."maskId" = m.id + ) + DELETE FROM "State" s + WHERE s."maskId" IN (SELECT parent FROM mask) + AND s.path IN (SELECT path FROM paths) + `; +} diff --git a/packages/persistance/src/services/prisma/sql/mergeIntoParent.ts b/packages/persistance/src/services/prisma/sql/mergeIntoParent.ts new file mode 100644 index 000000000..17e752d7e --- /dev/null +++ b/packages/persistance/src/services/prisma/sql/mergeIntoParent.ts @@ -0,0 +1,16 @@ +import { Prisma } from "@prisma/client"; + +/* +Strategy: + 1. Find the mask we are interested in, as a record with the id and it's parent id + 2. Update the maskId to the masks parent for all the entries of that particular mask +*/ + +export function mergeIntoParent(maskId: number) { + return Prisma.sql` + WITH mask AS ( + SELECT id, parent FROM "Mask" m WHERE m.id = ${maskId}) + UPDATE "State" SET "maskId" = (SELECT parent FROM mask) + WHERE "maskId" = (SELECT id FROM mask) + `; +} diff --git a/packages/persistance/src/services/prisma/sql/readState.ts b/packages/persistance/src/services/prisma/sql/readState.ts new file mode 100644 index 000000000..f39306d85 --- /dev/null +++ b/packages/persistance/src/services/prisma/sql/readState.ts @@ -0,0 +1,16 @@ +import { Prisma } from "@prisma/client"; + +export function readState(maskId: number, paths: Prisma.Decimal[]) { + return Prisma.sql` + WITH RECURSIVE find_masks(id, parent) AS ( + SELECT m.id, m.parent FROM "Mask" m WHERE m.id = ${maskId} + UNION ALL + SELECT s.id, s.parent FROM "Mask" s, find_masks f + WHERE f.parent = s.id + ) + SELECT distinct on (state.path) state.path, state.values FROM find_masks + JOIN "State" state ON state."maskId" = find_masks.id + WHERE state.path = ANY(${paths}) + ORDER BY state.path, state."maskId" DESC; + `; +} diff --git a/packages/persistance/src/services/redis/RedisMerkleTreeStore.ts b/packages/persistance/src/services/redis/RedisMerkleTreeStore.ts index 3de87a29e..af4f70e53 100644 --- a/packages/persistance/src/services/redis/RedisMerkleTreeStore.ts +++ b/packages/persistance/src/services/redis/RedisMerkleTreeStore.ts @@ -1,5 +1,6 @@ import { AsyncMerkleTreeStore, + InMemoryMerkleTreeStoreMask, MerkleTreeNode, MerkleTreeNodeQuery, } from "@proto-kit/sequencer"; @@ -10,9 +11,11 @@ import type { RedisConnection } from "../../RedisConnection"; export class RedisMerkleTreeStore implements AsyncMerkleTreeStore { private cache: MerkleTreeNode[] = []; + public readonly name = "base"; + public constructor( private readonly connection: RedisConnection, - private readonly mask: string = "base" + private readonly mask: string ) {} private getKey(node: MerkleTreeNodeQuery): string { @@ -80,4 +83,8 @@ export class RedisMerkleTreeStore implements AsyncMerkleTreeStore { // }); // console.log(`Reduced ${concat.length} to ${this.cache.length} items to write`) } + + public async createMask(name: string) { + return new InMemoryMerkleTreeStoreMask(this, name); + } } diff --git a/packages/persistance/test-integration/PrismaBlockProduction.test.ts b/packages/persistance/test-integration/PrismaBlockProduction.test.ts index 61eb02d3c..015076930 100644 --- a/packages/persistance/test-integration/PrismaBlockProduction.test.ts +++ b/packages/persistance/test-integration/PrismaBlockProduction.test.ts @@ -17,7 +17,7 @@ import { import { createPrismaAppchain, IntegrationTestDBConfig, - prepareBlock, + injectTransaction, } from "./utils"; describe("prisma integration", () => { @@ -56,7 +56,7 @@ describe("prisma integration", () => { beforeAll(async () => { await setup(); - await prepareBlock(appChain, sender.toPublicKey(), senderNonce); + await injectTransaction(appChain, sender.toPublicKey(), senderNonce); senderNonce++; [block, batch] = await appChain.sequencer @@ -179,7 +179,7 @@ describe("prisma integration", () => { signer: sender2, }, }); - await prepareBlock(appChain, sender2.toPublicKey(), 0); + await injectTransaction(appChain, sender2.toPublicKey(), 0); }); it("should produce the block", async () => { @@ -210,7 +210,7 @@ describe("prisma integration", () => { beforeAll(async () => { await setup(); - transaction = await prepareBlock( + transaction = await injectTransaction( appChain, sender.toPublicKey(), senderNonce diff --git a/packages/persistance/test-integration/SequencerRestart.test.ts b/packages/persistance/test-integration/SequencerRestart.test.ts index 6619ed5ab..ee001cd95 100644 --- a/packages/persistance/test-integration/SequencerRestart.test.ts +++ b/packages/persistance/test-integration/SequencerRestart.test.ts @@ -1,13 +1,13 @@ import "reflect-metadata"; import { afterAll, beforeAll, expect } from "@jest/globals"; -import { expectDefined } from "@proto-kit/common"; +import { expectDefined, log, sleep } from "@proto-kit/common"; import { PrivateKey } from "o1js"; import { container } from "tsyringe"; import { createPrismaAppchain, IntegrationTestDBConfig, - prepareBlock, + injectTransaction, } from "./utils"; describe("sequencer restart", () => { @@ -16,13 +16,11 @@ describe("sequencer restart", () => { const sender = PrivateKey.random(); let senderNonce = 0; - const clearDB = async () => { - const db = appChain.sequencer.resolve("Database"); - await db.prisma.pruneDatabase(); - await db.redis.pruneDatabase(); - }; + const setup = async (pruneOnStartup: boolean) => { + if (pruneOnStartup) { + senderNonce = 0; + } - const setup = async () => { const { prismaConfig, redisConfig } = IntegrationTestDBConfig; appChain = createPrismaAppchain(prismaConfig, redisConfig); @@ -30,6 +28,11 @@ describe("sequencer restart", () => { Signer: { signer: sender, }, + Sequencer: { + DatabasePruneModule: { + pruneOnStartup: pruneOnStartup, + }, + }, }); await appChain.start(false, container.createChildContainer()); @@ -39,55 +42,110 @@ describe("sequencer restart", () => { await appChain.sequencer.resolve("Database").close(); }; - beforeAll(async () => { - await setup(); - await clearDB(); - + const produce = async (num: number, type: "block" | "batch") => { const blockTrigger = appChain.sequencer.resolve("BlockTrigger"); - console.log(sender.toPublicKey().toBase58()); - - for (let block = 0; block < 2; block++) { - await prepareBlock(appChain, sender.toPublicKey(), senderNonce); + for (let block = 0; block < num; block++) { + await injectTransaction(appChain, sender.toPublicKey(), senderNonce); senderNonce++; - const [producedBlock] = await blockTrigger.produceBlockAndBatch(); + const producedBlock = await blockTrigger.produceBlock(); if ((producedBlock?.transactions.length ?? 0) === 0) { - throw new Error(`Block not produced correctly: ${block}`); + throw new Error(`Block at height ${block} not produced correctly`); + } + if (type === "batch") { + await blockTrigger.produceBatch(); } } + }; - await teardown(); + describe("resume at latest block", () => { + beforeAll(async () => { + await setup(true); - await setup(); - }, 40000); + await produce(2, "batch"); - afterAll(async () => { - await teardown(); - }); + await teardown(); + + await setup(false); + }, 40000); + + afterAll(async () => { + await teardown(); + }); + + it("should fetch correct nonce", async () => { + const accountState = + await appChain.query.protocol.AccountState.accountState.get( + sender.toPublicKey() + ); + + expectDefined(accountState); + + expect(accountState.nonce.toString()).toBe("2"); + }); + + it("should be able to produce a block on top", async () => { + const blockTrigger = appChain.sequencer.resolve("BlockTrigger"); + await injectTransaction(appChain, sender.toPublicKey(), senderNonce); + senderNonce++; - it("should fetch correct nonce", async () => { - const accountState = - await appChain.query.protocol.AccountState.accountState.get( - sender.toPublicKey() - ); + const [block, batch] = await blockTrigger.produceBlockAndBatch(); - expectDefined(accountState); + expectDefined(block); + expectDefined(batch); - expect(accountState.nonce.toString()).toBe("2"); + expect(block.transactions).toHaveLength(1); + expect(block.transactions[0].tx.nonce.toString()).toBe("2"); + }, 15000); }); - it("should be able to produce a block on top", async () => { - const blockTrigger = appChain.sequencer.resolve("BlockTrigger"); - await prepareBlock(appChain, sender.toPublicKey(), senderNonce); - senderNonce++; + describe("reconstruct untraced block masks", () => { + beforeEach(async () => { + log.setLevel("TRACE"); + + await setup(true); + + await sleep(1000); + + await produce(2, "block"); + + await teardown(); - const [block, batch] = await blockTrigger.produceBlockAndBatch(); + await setup(false); - expectDefined(block); - expectDefined(batch); + console.log("beforeEach"); + }, 40000); + + afterEach(async () => { + await teardown(); + }); + + it("should be able to produce a block on top", async () => { + const blockTrigger = appChain.sequencer.resolve("BlockTrigger"); + await injectTransaction(appChain, sender.toPublicKey(), senderNonce); + senderNonce++; - expect(block.transactions).toHaveLength(1); - expect(block.transactions[0].tx.nonce.toString()).toBe("2"); - }, 15000); + const block = await blockTrigger.produceBlock(); + + expectDefined(block); + + expect(block.transactions).toHaveLength(1); + expect(block.transactions[0].tx.nonce.toString()).toBe("2"); + + // Should be able to trace after additional block + const batch = await blockTrigger.produceBatch(); + + expectDefined(batch); + expect(batch.blockHashes).toHaveLength(3); + }, 15000); + + it("should be able to trace", async () => { + const blockTrigger = appChain.sequencer.resolve("BlockTrigger"); + const batch = await blockTrigger.produceBatch(); + + expectDefined(batch); + expect(batch.blockHashes).toHaveLength(2); + }); + }); }); diff --git a/packages/persistance/test-integration/utils.ts b/packages/persistance/test-integration/utils.ts index e42b336e8..267ab42e8 100644 --- a/packages/persistance/test-integration/utils.ts +++ b/packages/persistance/test-integration/utils.ts @@ -32,6 +32,7 @@ import { BlockProducerModule, VanillaTaskWorkerModules, SequencerStartupModule, + DatabasePruneModule, } from "@proto-kit/sequencer"; import { Bool, PrivateKey, PublicKey, Struct } from "o1js"; @@ -104,6 +105,7 @@ export function createPrismaAppchain( Sequencer: Sequencer.from({ modules: { Database: PrismaRedisDatabase, + DatabasePruneModule, Mempool: PrivateMempool, LocalTaskWorkerModule: LocalTaskWorkerModule.from( @@ -143,6 +145,9 @@ export function createPrismaAppchain( }, redis: redisConnection, }, + DatabasePruneModule: { + pruneOnStartup: false, + }, BlockTrigger: {}, Mempool: {}, BatchProducerModule: {}, @@ -165,7 +170,7 @@ export function createPrismaAppchain( return appChain; } -export async function prepareBlock( +export async function injectTransaction( appChain: ReturnType, sender: PublicKey, nonce: number diff --git a/packages/persistance/test/connection.test.ts b/packages/persistance/test/connection.test.ts index ef83f454b..561d83c0f 100644 --- a/packages/persistance/test/connection.test.ts +++ b/packages/persistance/test/connection.test.ts @@ -22,7 +22,7 @@ describe.skip("prisma", () => { password: "password", }; await db.start(); - const store = new RedisMerkleTreeStore(db); + const store = new RedisMerkleTreeStore(db, "base"); const cached = new CachedMerkleTreeStore(store); const tree = new RollupMerkleTree(cached); @@ -38,7 +38,7 @@ describe.skip("prisma", () => { console.log(`Root ${tree.getRoot().toBigInt()}`); - const store2 = new RedisMerkleTreeStore(db); + const store2 = new RedisMerkleTreeStore(db, "base"); const cached2 = new CachedMerkleTreeStore(store2); const tree2 = new RollupMerkleTree(cached2); diff --git a/packages/processor/package.json b/packages/processor/package.json index 63a5a9848..2be7e77bf 100644 --- a/packages/processor/package.json +++ b/packages/processor/package.json @@ -24,7 +24,7 @@ "main": "dist/index.js", "dependencies": { "@inkjs/ui": "^1.0.0", - "@prisma/client": "^5.19.1", + "@prisma/client": "^5.22.0", "@types/yargs": "^17.0.29", "figlet": "^1.7.0", "ink": "^4.4.1", diff --git a/packages/protocol/src/index.ts b/packages/protocol/src/index.ts index d161590a2..5c34b007e 100644 --- a/packages/protocol/src/index.ts +++ b/packages/protocol/src/index.ts @@ -39,7 +39,7 @@ export * from "./state/context/RuntimeMethodExecutionContext"; export * from "./state/protocol/ProtocolState"; export * from "./state/State"; export * from "./state/StateMap"; -export * from "./state/StateService"; +export * from "./state/SimpleAsyncStateService"; export * from "./state/StateServiceProvider"; export * from "./state/assert/assert"; export * from "./settlement/contracts/authorizations/ContractAuthorization"; diff --git a/packages/protocol/src/protocol/Protocol.ts b/packages/protocol/src/protocol/Protocol.ts index 8d7c9e021..e3ec89f32 100644 --- a/packages/protocol/src/protocol/Protocol.ts +++ b/packages/protocol/src/protocol/Protocol.ts @@ -13,7 +13,7 @@ import { DependencyContainer, Lifecycle } from "tsyringe"; import { BlockProvable } from "../prover/block/BlockProvable"; import { StateTransitionProvable } from "../prover/statetransition/StateTransitionProvable"; import { StateServiceProvider } from "../state/StateServiceProvider"; -import { SimpleAsyncStateService } from "../state/StateService"; +import { SimpleAsyncStateService } from "../state/SimpleAsyncStateService"; import { NoopBlockHook } from "../hooks/NoopBlockHook"; import { BlockHeightHook } from "../hooks/BlockHeightHook"; import { LastStateRootBlockHook } from "../hooks/LastStateRootBlockHook"; diff --git a/packages/protocol/src/protocol/ProtocolEnvironment.ts b/packages/protocol/src/protocol/ProtocolEnvironment.ts index f6a291de7..5203694b1 100644 --- a/packages/protocol/src/protocol/ProtocolEnvironment.ts +++ b/packages/protocol/src/protocol/ProtocolEnvironment.ts @@ -1,6 +1,6 @@ import { AreProofsEnabled } from "@proto-kit/common"; -import { SimpleAsyncStateService } from "../state/StateService"; +import { SimpleAsyncStateService } from "../state/SimpleAsyncStateService"; import { StateServiceProvider } from "../state/StateServiceProvider"; export interface ProtocolEnvironment { diff --git a/packages/protocol/src/prover/block/BlockProver.ts b/packages/protocol/src/prover/block/BlockProver.ts index 6362e7055..03cc63fdc 100644 --- a/packages/protocol/src/prover/block/BlockProver.ts +++ b/packages/protocol/src/prover/block/BlockProver.ts @@ -531,7 +531,7 @@ export class BlockProverProgrammable extends ZkProgrammable< witnessedRootsHash, stateTransitionProof.publicOutput.witnessedRootsHash, apply, - "Root accumulator Commitment is not the same that have been executed by the ST proof" + "Root accumulator Commitment is not the same as has been executed by the ST proof" ); // update root only if we didn't defer diff --git a/packages/protocol/src/state/StateService.ts b/packages/protocol/src/state/SimpleAsyncStateService.ts similarity index 100% rename from packages/protocol/src/state/StateService.ts rename to packages/protocol/src/state/SimpleAsyncStateService.ts diff --git a/packages/protocol/src/state/StateServiceProvider.ts b/packages/protocol/src/state/StateServiceProvider.ts index d39fea612..40d54717b 100644 --- a/packages/protocol/src/state/StateServiceProvider.ts +++ b/packages/protocol/src/state/StateServiceProvider.ts @@ -1,7 +1,7 @@ import { injectable } from "tsyringe"; import { log } from "@proto-kit/common"; -import { SimpleAsyncStateService } from "./StateService"; +import { SimpleAsyncStateService } from "./SimpleAsyncStateService"; const errors = { stateServiceNotSet: () => diff --git a/packages/sdk/src/query/StateServiceQueryModule.ts b/packages/sdk/src/query/StateServiceQueryModule.ts index ec78f44aa..d1a1e7770 100644 --- a/packages/sdk/src/query/StateServiceQueryModule.ts +++ b/packages/sdk/src/query/StateServiceQueryModule.ts @@ -5,6 +5,10 @@ import { Sequencer, SequencerModulesRecord, AsyncMerkleTreeStore, + BlockStorage, + TreeStoreCreator, + StateServiceCreator, + MaskName, } from "@proto-kit/sequencer"; import { Field } from "o1js"; import { inject, injectable } from "tsyringe"; @@ -23,24 +27,46 @@ export class StateServiceQueryModule super(); } - public get asyncStateService(): AsyncStateService { - return this.sequencer.dependencyContainer.resolve( - "UnprovenStateService" + private async getCurrentTreeMask() { + const block = await this.blockStorage().getLatestBlock(); + if (block !== undefined) { + return MaskName.block(block.block.height); + } + return MaskName.base(); + } + + public blockStorage() { + return this.sequencer.dependencyContainer.resolve( + "BlockStorage" ); } - public get treeStore(): AsyncMerkleTreeStore { - return this.sequencer.dependencyContainer.resolve("AsyncMerkleStore"); + public async asyncStateService(): Promise { + const stateServiceCreator = + this.sequencer.dependencyContainer.resolve( + "StateServiceCreator" + ); + return stateServiceCreator.getMask(MaskName.base()); + } + + public async treeStore(): Promise { + const treeStoreCreator = + this.sequencer.dependencyContainer.resolve( + "TreeStoreCreator" + ); + return treeStoreCreator.getMask(await this.getCurrentTreeMask()); } - public get(key: Field) { - return this.asyncStateService.get(key); + public async get(key: Field) { + const stateService = await this.asyncStateService(); + return await stateService.get(key); } public async merkleWitness( path: Field ): Promise { - const syncStore = new CachedMerkleTreeStore(this.treeStore); + const treeStore = await this.treeStore(); + const syncStore = new CachedMerkleTreeStore(treeStore); await syncStore.preloadKey(path.toBigInt()); const tree = new RollupMerkleTree(syncStore); diff --git a/packages/sequencer/src/index.ts b/packages/sequencer/src/index.ts index 462da091d..67cbcc71a 100644 --- a/packages/sequencer/src/index.ts +++ b/packages/sequencer/src/index.ts @@ -75,6 +75,10 @@ export * from "./storage/inmemory/InMemoryBatchStorage"; export * from "./storage/inmemory/InMemorySettlementStorage"; export * from "./storage/inmemory/InMemoryMessageStorage"; export * from "./storage/inmemory/InMemoryTransactionStorage"; +export * from "./storage/inmemory/masking/InMemoryStateServiceCreator"; +export * from "./storage/inmemory/masking/InMemoryTreeStoreCreator"; +export * from "./storage/inmemory/masking/InMemoryStateServiceMask"; +export * from "./storage/inmemory/masking/InMemoryMerkleTreeStoreMask"; export * from "./storage/StorageDependencyFactory"; export * from "./storage/Database"; export * from "./storage/DatabasePruneModule"; @@ -89,6 +93,10 @@ export * from "./state/merkle/CachedMerkleTreeStore"; export * from "./state/merkle/SyncCachedMerkleTreeStore"; export * from "./state/state/DummyStateService"; export * from "./state/state/CachedStateService"; +export * from "./state/masking/MaskGraph"; +export * from "./state/masking/MaskName"; +export * from "./state/masking/StateServiceCreator"; +export * from "./state/masking/TreeStoreCreator"; export * from "./settlement/SettlementModule"; export * from "./settlement/messages/WithdrawalQueue"; export * from "./settlement/messages/IncomingMessageAdapter"; diff --git a/packages/sequencer/src/mempool/private/PrivateMempool.ts b/packages/sequencer/src/mempool/private/PrivateMempool.ts index f1c56f5af..1c1a7f241 100644 --- a/packages/sequencer/src/mempool/private/PrivateMempool.ts +++ b/packages/sequencer/src/mempool/private/PrivateMempool.ts @@ -27,8 +27,9 @@ import { SequencerModulesRecord, } from "../../sequencer/executor/Sequencer"; import { CachedStateService } from "../../state/state/CachedStateService"; -import { AsyncStateService } from "../../state/async/AsyncStateService"; import { distinctByPredicate } from "../../helpers/utils"; +import { StateServiceCreator } from "../../state/masking/StateServiceCreator"; +import { MaskName } from "../../state/masking/MaskName"; type MempoolTransactionPaths = { transaction: PendingTransaction; @@ -54,8 +55,8 @@ export class PrivateMempool private readonly protocol: Protocol, @inject("Sequencer") private readonly sequencer: Sequencer, - @inject("UnprovenStateService") - private readonly stateService: AsyncStateService + @inject("StateServiceCreator") + private readonly stateServiceCreator: StateServiceCreator ) { super(); this.accountStateHook = @@ -104,7 +105,8 @@ export class PrivateMempool public async getTxs(limit?: number): Promise { const txs = await this.transactionStorage.getPendingUserTransactions(); - const baseCachedStateService = new CachedStateService(this.stateService); + const stateService = this.stateServiceCreator.getMask(MaskName.base()); + const baseCachedStateService = new CachedStateService(stateService); const networkState = (await this.getStagedNetworkState()) ?? NetworkState.empty(); diff --git a/packages/sequencer/src/protocol/production/BatchProducerModule.ts b/packages/sequencer/src/protocol/production/BatchProducerModule.ts index 6bdb77202..bcc31f950 100644 --- a/packages/sequencer/src/protocol/production/BatchProducerModule.ts +++ b/packages/sequencer/src/protocol/production/BatchProducerModule.ts @@ -5,7 +5,7 @@ import { NetworkState, } from "@proto-kit/protocol"; import { Field, Proof } from "o1js"; -import { log, noop } from "@proto-kit/common"; +import { log, mapSequential, noop, RollupMerkleTree } from "@proto-kit/common"; import { sequencerModule, @@ -14,9 +14,9 @@ import { import { BatchStorage } from "../../storage/repositories/BatchStorage"; import { SettleableBatch } from "../../storage/model/Batch"; import { CachedMerkleTreeStore } from "../../state/merkle/CachedMerkleTreeStore"; -import { AsyncMerkleTreeStore } from "../../state/async/AsyncMerkleTreeStore"; import { BlockWithResult } from "../../storage/model/Block"; import type { Database } from "../../storage/Database"; +import { TreeStoreCreator } from "../../state/masking/TreeStoreCreator"; import { BlockProofSerializer } from "./tasks/serializers/BlockProofSerializer"; import { BatchTracingService } from "./tracing/BatchTracingService"; @@ -47,8 +47,8 @@ export class BatchProducerModule extends SequencerModule { private productionInProgress = false; public constructor( - @inject("AsyncMerkleStore") - private readonly merkleStore: AsyncMerkleTreeStore, + @inject("TreeStoreCreator") + private readonly treeStoreCreator: TreeStoreCreator, @inject("BatchStorage") private readonly batchStorage: BatchStorage, @inject("Database") private readonly database: Database, @@ -101,6 +101,16 @@ export class BatchProducerModule extends SequencerModule { return undefined; } + private async mergeBatchIntoStorage(blocks: BlockWithResult[]) { + // Reverse, so that we merge from latest block to oldest. This way, + // nodes that have been written in multiple masks are only written to + // the base once, therefore implicitly deduped + await mapSequential(blocks.slice().reverse(), async (block) => { + const mask = `block-${block.block.height.toBigInt()}`; + await this.treeStoreCreator.mergeIntoParent(mask); + }); + } + private async tryProduceBatch( blocks: BlockWithResult[] ): Promise { @@ -122,7 +132,8 @@ export class BatchProducerModule extends SequencerModule { // Apply state changes to current MerkleTreeStore await this.database.executeInTransaction(async () => { await this.batchStorage.pushBatch(batchWithStateDiff.batch); - await batchWithStateDiff.changes.mergeIntoParent(); + + await this.mergeBatchIntoStorage(blocks); }); // TODO Add transition from unproven to proven state for stateservice @@ -160,6 +171,23 @@ export class BatchProducerModule extends SequencerModule { }; } + private async checkTreeConsistency( + merkleTreeStore: CachedMerkleTreeStore, + publicOutput: BlockProverPublicOutput + ) { + // Preload root + const [fetchedRoot] = await merkleTreeStore.getNodesAsync([ + { key: 0n, level: 255 }, + ]); + const root = fetchedRoot ?? RollupMerkleTree.EMPTY_ROOT; + + if (root !== publicOutput.stateRoot.toBigInt()) { + throw new Error( + `Mismatch in output state roots: ${root} != ${publicOutput.stateRoot.toBigInt()}` + ); + } + } + /** * Computes a batch based on an array of sequenced blocks. * This process is also known as tracing, as we "trace" every computational step @@ -186,7 +214,8 @@ export class BatchProducerModule extends SequencerModule { throw errors.blockWithoutTxs(); } - const merkleTreeStore = new CachedMerkleTreeStore(this.merkleStore); + const mask = this.treeStoreCreator.getMask("base"); + const merkleTreeStore = new CachedMerkleTreeStore(mask); const trace = await this.batchTraceService.traceBatch( blocks.map((block) => block), @@ -195,6 +224,8 @@ export class BatchProducerModule extends SequencerModule { const proof = await this.batchFlow.executeBatch(trace, blockId); + await this.checkTreeConsistency(merkleTreeStore, proof.publicOutput); + const fromNetworkState = blocks[0].block.networkState.before; const toNetworkState = blocks.at(-1)!.result.afterNetworkState; diff --git a/packages/sequencer/src/protocol/production/sequencing/BlockProducerModule.ts b/packages/sequencer/src/protocol/production/sequencing/BlockProducerModule.ts index 78e358955..b264ec6ba 100644 --- a/packages/sequencer/src/protocol/production/sequencing/BlockProducerModule.ts +++ b/packages/sequencer/src/protocol/production/sequencing/BlockProducerModule.ts @@ -16,7 +16,6 @@ import { import { BlockQueue } from "../../../storage/repositories/BlockStorage"; import { PendingTransaction } from "../../../mempool/PendingTransaction"; import { AsyncMerkleTreeStore } from "../../../state/async/AsyncMerkleTreeStore"; -import { AsyncStateService } from "../../../state/async/AsyncStateService"; import { Block, BlockResult, @@ -24,6 +23,9 @@ import { } from "../../../storage/model/Block"; import { Database } from "../../../storage/Database"; import { IncomingMessagesService } from "../../../settlement/messages/IncomingMessagesService"; +import { TreeStoreCreator } from "../../../state/masking/TreeStoreCreator"; +import { StateServiceCreator } from "../../../state/masking/StateServiceCreator"; +import { MaskName } from "../../../state/masking/MaskName"; import { BlockProductionService } from "./BlockProductionService"; import { BlockResultService } from "./BlockResultService"; @@ -40,10 +42,12 @@ export class BlockProducerModule extends SequencerModule { public constructor( @inject("Mempool") private readonly mempool: Mempool, private readonly messageService: IncomingMessagesService, - @inject("UnprovenStateService") - private readonly unprovenStateService: AsyncStateService, - @inject("UnprovenMerkleStore") - private readonly unprovenMerkleStore: AsyncMerkleTreeStore, + @inject("StateServiceCreator") + private readonly stateServiceCreator: StateServiceCreator, + @inject("TreeStoreCreator") + private readonly treeStoreCreator: TreeStoreCreator, + // @inject("UnprovenMerkleStore") + // private readonly unprovenMerkleStore: AsyncMerkleTreeStore, @inject("BlockQueue") private readonly blockQueue: BlockQueue, @inject("BlockTreeStore") @@ -106,12 +110,22 @@ export class BlockProducerModule extends SequencerModule { } public async generateMetadata(block: Block): Promise { + const height = block.height.toBigInt(); + const maskName = MaskName.block(height); + const asyncStateService = this.stateServiceCreator.getMask(maskName); + + const asyncTreeStore = await this.treeStoreCreator.createMask( + maskName, + MaskName.block(height - 1n), + MaskName.base() + ); + const { result, blockHashTreeStore, treeStore, stateService } = await this.resultService.generateMetadataForNextBlock( block, - this.unprovenMerkleStore, + asyncTreeStore, this.blockTreeStore, - this.unprovenStateService + asyncStateService ); await this.database.executeInTransaction(async () => { @@ -120,6 +134,8 @@ export class BlockProducerModule extends SequencerModule { await stateService.mergeIntoParent(); await this.blockQueue.pushResult(result); + + await this.stateServiceCreator.mergeIntoParent(maskName); }); return result; @@ -197,6 +213,16 @@ export class BlockProducerModule extends SequencerModule { }; } + private async createMask(previousBlock: Block) { + const isFirstBlock = previousBlock.hash.equals(0n).toBoolean(); + const height = isFirstBlock ? 0n : previousBlock.height.toBigInt() + 1n; + + return await this.stateServiceCreator.createMask( + MaskName.block(height), + MaskName.base() + ); + } + private async produceBlock(): Promise { this.productionInProgress = true; @@ -207,8 +233,10 @@ export class BlockProducerModule extends SequencerModule { return undefined; } + const stateService = await this.createMask(metadata.block); + const blockResult = await this.productionService.createBlock( - this.unprovenStateService, + stateService, txs, metadata, this.allowEmptyBlock() diff --git a/packages/sequencer/src/protocol/production/sequencing/BlockResultService.ts b/packages/sequencer/src/protocol/production/sequencing/BlockResultService.ts index dcebfef0a..426630490 100644 --- a/packages/sequencer/src/protocol/production/sequencing/BlockResultService.ts +++ b/packages/sequencer/src/protocol/production/sequencing/BlockResultService.ts @@ -28,7 +28,7 @@ import type { StateRecord } from "../BatchProducerModule"; import { executeWithExecutionContext } from "./TransactionExecutionService"; -function collectStateDiff( +export function collectStateDiff( stateTransitions: UntypedStateTransition[] ): StateRecord { return stateTransitions.reduce>( @@ -43,8 +43,8 @@ function collectStateDiff( } function createCombinedStateDiff( - transactions: TransactionExecutionResult[], - blockHookSTs: UntypedStateTransition[] + blockHookSTs: UntypedStateTransition[], + transactions: TransactionExecutionResult[] ) { // Flatten diff list into a single diff by applying them over each other return transactions @@ -53,7 +53,7 @@ function createCombinedStateDiff( .filter(({ applied }) => applied) .flatMap(({ stateTransitions }) => stateTransitions); - transitions.push(...blockHookSTs); + transitions.splice(0, 0, ...blockHookSTs); return collectStateDiff(transitions); }) @@ -63,6 +63,28 @@ function createCombinedStateDiff( }, {}); } +export async function applyStateDiff( + store: CachedMerkleTreeStore, + stateDiff: StateRecord +): Promise { + await store.preloadKeys(Object.keys(stateDiff).map(BigInt)); + + // In case the diff is empty, we preload key 0 in order to + // retrieve the root, which we need later + if (Object.keys(stateDiff).length === 0) { + await store.preloadKey(0n); + } + + const tree = new RollupMerkleTree(store); + + Object.entries(stateDiff).forEach(([key, state]) => { + const treeValue = state !== undefined ? Poseidon.hash(state) : Field(0); + tree.setLeaf(BigInt(key), treeValue); + }); + + return tree; +} + @injectable() @scoped(Lifecycle.ContainerScoped) export class BlockResultService { @@ -147,28 +169,6 @@ export class BlockResultService { }; } - public async applyStateDiff( - store: CachedMerkleTreeStore, - stateDiff: StateRecord - ): Promise { - await store.preloadKeys(Object.keys(stateDiff).map(BigInt)); - - // In case the diff is empty, we preload key 0 in order to - // retrieve the root, which we need later - if (Object.keys(stateDiff).length === 0) { - await store.preloadKey(0n); - } - - const tree = new RollupMerkleTree(store); - - Object.entries(stateDiff).forEach(([key, state]) => { - const treeValue = state !== undefined ? Poseidon.hash(state) : Field(0); - tree.setLeaf(BigInt(key), treeValue); - }); - - return tree; - } - public async generateMetadataForNextBlock( block: Block, merkleTreeStore: AsyncMerkleTreeStore, @@ -181,13 +181,13 @@ export class BlockResultService { stateService: CachedStateService; }> { const combinedDiff = createCombinedStateDiff( - block.transactions, - block.beforeBlockStateTransitions + block.beforeBlockStateTransitions, + block.transactions ); const inMemoryStore = new CachedMerkleTreeStore(merkleTreeStore); - const tree = await this.applyStateDiff(inMemoryStore, combinedDiff); + const tree = await applyStateDiff(inMemoryStore, combinedDiff); const witnessedStateRoot = tree.getRoot(); @@ -210,7 +210,7 @@ export class BlockResultService { ); // Apply afterBlock STs to the tree - const tree2 = await this.applyStateDiff( + const tree2 = await applyStateDiff( inMemoryStore, collectStateDiff( stateTransitions.map((stateTransition) => diff --git a/packages/sequencer/src/protocol/production/trigger/BlockTrigger.ts b/packages/sequencer/src/protocol/production/trigger/BlockTrigger.ts index 09d47c997..abb812594 100644 --- a/packages/sequencer/src/protocol/production/trigger/BlockTrigger.ts +++ b/packages/sequencer/src/protocol/production/trigger/BlockTrigger.ts @@ -12,7 +12,11 @@ import { BlockProducerModule } from "../sequencing/BlockProducerModule"; import { BlockQueue } from "../../../storage/repositories/BlockStorage"; import { SequencerModule } from "../../../sequencer/builder/SequencerModule"; import { SettlementModule } from "../../../settlement/SettlementModule"; -import { Block, BlockWithResult } from "../../../storage/model/Block"; +import { + assertBlockHasResult, + Block, + BlockWithResult, +} from "../../../storage/model/Block"; /** * A BlockTrigger is the primary method to start the production of a block and @@ -49,9 +53,17 @@ export class BlockTriggerBase< } protected async produceBatch(): Promise { - const blocks = await this.blockQueue.getNewBlocks(); + const blocks = await this.blockQueue.getPendingBlocks(); + + // Make sure all blocks have results + const completedBlocks = blocks.map((block) => { + assertBlockHasResult(block); + return block; + }); + if (blocks.length > 0) { - const batch = await this.batchProducerModule?.createBatch(blocks); + const batch = + await this.batchProducerModule?.createBatch(completedBlocks); if (batch !== undefined) { this.events.emit("batch-produced", batch); } diff --git a/packages/sequencer/src/settlement/BridgingModule.ts b/packages/sequencer/src/settlement/BridgingModule.ts index c4e407246..9fe8bf14c 100644 --- a/packages/sequencer/src/settlement/BridgingModule.ts +++ b/packages/sequencer/src/settlement/BridgingModule.ts @@ -41,9 +41,10 @@ import { sequencerModule, } from "../sequencer/builder/SequencerModule"; import { CachedMerkleTreeStore } from "../state/merkle/CachedMerkleTreeStore"; -import { AsyncMerkleTreeStore } from "../state/async/AsyncMerkleTreeStore"; import { FeeStrategy } from "../protocol/baselayer/fees/FeeStrategy"; import type { MinaBaseLayer } from "../protocol/baselayer/MinaBaseLayer"; +import { TreeStoreCreator } from "../state/masking/TreeStoreCreator"; +import { MaskName } from "../state/masking/MaskName"; import type { OutgoingMessageAdapter } from "./messages/WithdrawalQueue"; import type { SettlementModule } from "./SettlementModule"; @@ -75,8 +76,8 @@ export class BridgingModule extends SequencerModule { private readonly settlementModule: SettlementModule, @inject("OutgoingMessageQueue") private readonly outgoingMessageQueue: OutgoingMessageAdapter, - @inject("AsyncMerkleStore") - private readonly merkleTreeStore: AsyncMerkleTreeStore, + @inject("TreeStoreCreator") + private readonly treeStoreCreator: TreeStoreCreator, @inject("FeeStrategy") private readonly feeStrategy: FeeStrategy, @inject("AreProofsEnabled") areProofsEnabled: AreProofsEnabled, @@ -357,7 +358,8 @@ export class BridgingModule extends SequencerModule { const bridgeContract = this.createBridgeContract(bridgeAddress, tokenId); - const cachedStore = new CachedMerkleTreeStore(this.merkleTreeStore); + const merkleTreeStore = this.treeStoreCreator.getMask(MaskName.base()); + const cachedStore = new CachedMerkleTreeStore(merkleTreeStore); const tree = new RollupMerkleTree(cachedStore); const [withdrawalModule, withdrawalStateName] = diff --git a/packages/sequencer/src/state/async/AsyncStateService.ts b/packages/sequencer/src/state/async/AsyncStateService.ts index b5420abc3..1d365eb4c 100644 --- a/packages/sequencer/src/state/async/AsyncStateService.ts +++ b/packages/sequencer/src/state/async/AsyncStateService.ts @@ -11,13 +11,17 @@ export interface StateEntry { * CachedStateService to preload keys for In-Circuit usage. */ export interface AsyncStateService { - openTransaction: () => Promise; + openTransaction(): Promise; - commit: () => Promise; + commit(): Promise; - writeStates: (entries: StateEntry[]) => void; + writeStates(entries: StateEntry[]): void; - getMany: (keys: Field[]) => Promise; + getMany(keys: Field[]): Promise; - get: (key: Field) => Promise; + get(key: Field): Promise; + + mergeIntoParent(): Promise; + + drop(): Promise; } diff --git a/packages/sequencer/src/state/masking/MaskGraph.ts b/packages/sequencer/src/state/masking/MaskGraph.ts new file mode 100644 index 000000000..9f87a56fd --- /dev/null +++ b/packages/sequencer/src/state/masking/MaskGraph.ts @@ -0,0 +1,127 @@ +function assertMaskFound( + service: R | undefined, + name: string +): asserts service is R { + if (service === undefined) { + throw new Error(`Mask with name ${name} not found`); + } +} + +type Node = { + mask: T; + children: Child[]; + parent: Parent; +}; + +type BaseMaskType = Interface & { + createMask(name: string): Promise; + name: string; +}; + +type MaskType = BaseMaskType & { + mergeIntoParent(): Promise; + updateParent(parent: Interface): void; +}; + +export class MaskGraph< + Interface, + Base extends BaseMaskType, + Mask extends MaskType, +> { + public constructor(base: Base) { + this.root = { + mask: base, + children: [], + parent: undefined, + }; + } + + root: Node; + + masks: Record> = {}; + + private findMaskNode(name: string) { + return this.masks[name]; + } + + private findNode( + name: string + ): Node | undefined { + if (name === "base") { + return this.root; + } + return this.findMaskNode(name); + } + + private findService(name: string): Mask | Base | undefined { + return this.findNode(name)?.mask; + } + + public getMask(name: string) { + const candidate = this.findService(name); + + assertMaskFound(candidate, name); + + return candidate; + } + + public async createMask( + name: string, + parentName: string, + fallback?: string + ): Promise { + const candidate = this.findService(name); + if (candidate !== undefined) { + return candidate; + } + + let parent = this.findService(parentName); + + if (parent === undefined && fallback !== undefined) { + parent = this.findService(fallback); + } + + assertMaskFound(parent, `${parentName} | ${fallback}`); + + const mask = await parent.createMask(name); + + this.masks[name] = { + mask, + parent, + children: [], + }; + this.findNode(parentName)?.children.push(mask); + + return mask; + } + + private removeFromGraph(name: string) { + const node = this.findMaskNode(name); + + assertMaskFound(node, name); + + const { children, parent } = node; + + children.forEach((child) => { + child.updateParent(parent!); + this.findNode(child.name)!.parent = parent; + }); + const parentNode = this.findNode(parent!.name)!; + parentNode.children = parentNode.children.filter((c) => c !== node.mask); + parentNode.children.push(...children); + + delete this.masks[name]; + + return node.mask; + } + + public async mergeIntoParent(name: string): Promise { + const mask = this.removeFromGraph(name); + + await mask.mergeIntoParent(); + } + + public async drop(name: string) { + this.removeFromGraph(name); + } +} diff --git a/packages/sequencer/src/state/masking/MaskName.ts b/packages/sequencer/src/state/masking/MaskName.ts new file mode 100644 index 000000000..ee994c98b --- /dev/null +++ b/packages/sequencer/src/state/masking/MaskName.ts @@ -0,0 +1,10 @@ +import { Field } from "o1js"; + +export const MaskName = { + block(height: bigint | Field) { + const heightBigint = + typeof height === "bigint" ? height : height.toBigInt(); + return `block-${heightBigint}`; + }, + base: () => "base", +}; diff --git a/packages/sequencer/src/state/masking/StateServiceCreator.ts b/packages/sequencer/src/state/masking/StateServiceCreator.ts new file mode 100644 index 000000000..da62b8785 --- /dev/null +++ b/packages/sequencer/src/state/masking/StateServiceCreator.ts @@ -0,0 +1,10 @@ +import { AsyncStateService } from "../async/AsyncStateService"; + +export interface StateServiceCreator { + createMask(name: string, parent: string): Promise; + getMask(name: string): AsyncStateService; + mergeIntoParent(name: string): Promise; + drop(name: string): Promise; +} + +// TODO Add Prefix diff --git a/packages/sequencer/src/state/masking/TreeStoreCreator.ts b/packages/sequencer/src/state/masking/TreeStoreCreator.ts new file mode 100644 index 000000000..e9f186c79 --- /dev/null +++ b/packages/sequencer/src/state/masking/TreeStoreCreator.ts @@ -0,0 +1,12 @@ +import { AsyncMerkleTreeStore } from "../async/AsyncMerkleTreeStore"; + +export interface TreeStoreCreator { + createMask( + name: string, + parent: string, + fallback?: string + ): Promise; + getMask(name: string): AsyncMerkleTreeStore; + mergeIntoParent(name: string): Promise; + drop(name: string): Promise; +} diff --git a/packages/sequencer/src/state/merkle/CachedMerkleTreeStore.ts b/packages/sequencer/src/state/merkle/CachedMerkleTreeStore.ts index 677668abd..eb4018e7f 100644 --- a/packages/sequencer/src/state/merkle/CachedMerkleTreeStore.ts +++ b/packages/sequencer/src/state/merkle/CachedMerkleTreeStore.ts @@ -29,10 +29,14 @@ export class CachedMerkleTreeStore noop(); } - public constructor(private readonly parent: AsyncMerkleTreeStore) { + public constructor(public parent: AsyncMerkleTreeStore) { super(); } + public updateParent(parent: AsyncMerkleTreeStore) { + this.parent = parent; + } + public getNode(key: bigint, level: number): bigint | undefined { return super.getNode(key, level); } diff --git a/packages/sequencer/src/state/state/CachedStateService.ts b/packages/sequencer/src/state/state/CachedStateService.ts index 2824ba418..645df5e7d 100644 --- a/packages/sequencer/src/state/state/CachedStateService.ts +++ b/packages/sequencer/src/state/state/CachedStateService.ts @@ -15,10 +15,19 @@ export class CachedStateService { private writes: StateEntry[] = []; - public constructor(private readonly parent: AsyncStateService | undefined) { + public constructor(private parent: AsyncStateService | undefined) { super(); } + public updateParent(parent: AsyncStateService) { + this.parent = parent; + } + + public async drop(): Promise { + this.writes = []; + this.values = {}; + } + /** * Works like get(), but if a value is in this store, * but is known to be empty, this will return null @@ -43,6 +52,7 @@ export class CachedStateService await mapSequential(this.writes, async ({ key, value }) => { await this.set(key, value); }); + this.writes = []; } public async openTransaction(): Promise { diff --git a/packages/sequencer/src/storage/StorageDependencyFactory.ts b/packages/sequencer/src/storage/StorageDependencyFactory.ts index ed660d8fa..135bb08ca 100644 --- a/packages/sequencer/src/storage/StorageDependencyFactory.ts +++ b/packages/sequencer/src/storage/StorageDependencyFactory.ts @@ -4,8 +4,9 @@ import { DependencyRecord, } from "@proto-kit/common"; -import { AsyncStateService } from "../state/async/AsyncStateService"; import { AsyncMerkleTreeStore } from "../state/async/AsyncMerkleTreeStore"; +import { StateServiceCreator } from "../state/masking/StateServiceCreator"; +import { TreeStoreCreator } from "../state/masking/TreeStoreCreator"; import { BatchStorage } from "./repositories/BatchStorage"; import { BlockQueue, BlockStorage } from "./repositories/BlockStorage"; @@ -14,17 +15,15 @@ import { SettlementStorage } from "./repositories/SettlementStorage"; import { TransactionStorage } from "./repositories/TransactionStorage"; export interface StorageDependencyMinimumDependencies extends DependencyRecord { - asyncStateService: DependencyDeclaration; - asyncMerkleStore: DependencyDeclaration; batchStorage: DependencyDeclaration; blockQueue: DependencyDeclaration; blockStorage: DependencyDeclaration; - unprovenStateService: DependencyDeclaration; - unprovenMerkleStore: DependencyDeclaration; blockTreeStore: DependencyDeclaration; messageStorage: DependencyDeclaration; settlementStorage: DependencyDeclaration; transactionStorage: DependencyDeclaration; + stateServiceCreator: DependencyDeclaration; + treeStoreCreator: DependencyDeclaration; } export interface StorageDependencyFactory extends DependencyFactory { diff --git a/packages/sequencer/src/storage/inmemory/InMemoryBlockStorage.ts b/packages/sequencer/src/storage/inmemory/InMemoryBlockStorage.ts index 9a14f7d8c..879b8cb44 100644 --- a/packages/sequencer/src/storage/inmemory/InMemoryBlockStorage.ts +++ b/packages/sequencer/src/storage/inmemory/InMemoryBlockStorage.ts @@ -64,7 +64,7 @@ export class InMemoryBlockStorage return result; } - public async getNewBlocks(): Promise { + public async getPendingBlocks(): Promise { const latestBatch = await this.batchStorage.getLatestBatch(); let cursor = 0; @@ -89,7 +89,7 @@ export class InMemoryBlockStorage return slice.map((block, index) => ({ block, - result: results[index + 1]!, + result: results.at(index + 1), })); } diff --git a/packages/sequencer/src/storage/inmemory/InMemoryDatabase.ts b/packages/sequencer/src/storage/inmemory/InMemoryDatabase.ts index a8eb330ab..a27b06d9c 100644 --- a/packages/sequencer/src/storage/inmemory/InMemoryDatabase.ts +++ b/packages/sequencer/src/storage/inmemory/InMemoryDatabase.ts @@ -1,6 +1,5 @@ import { noop } from "@proto-kit/common"; -import { CachedStateService } from "../../state/state/CachedStateService"; import { sequencerModule, SequencerModule, @@ -10,23 +9,19 @@ import { Database } from "../Database"; import { closeable } from "../../sequencer/builder/Closeable"; import { InMemoryBlockStorage } from "./InMemoryBlockStorage"; -import { InMemoryAsyncMerkleTreeStore } from "./InMemoryAsyncMerkleTreeStore"; import { InMemoryBatchStorage } from "./InMemoryBatchStorage"; import { InMemoryMessageStorage } from "./InMemoryMessageStorage"; import { InMemorySettlementStorage } from "./InMemorySettlementStorage"; import { InMemoryTransactionStorage } from "./InMemoryTransactionStorage"; +import { InMemoryStateServiceCreator } from "./masking/InMemoryStateServiceCreator"; +import { InMemoryTreeStoreCreator } from "./masking/InMemoryTreeStoreCreator"; +import { InMemoryBaseMerkleTreeStore } from "./masking/InMemoryMerkleTreeStoreMask"; @sequencerModule() @closeable() export class InMemoryDatabase extends SequencerModule implements Database { public dependencies(): StorageDependencyMinimumDependencies { return { - asyncMerkleStore: { - useClass: InMemoryAsyncMerkleTreeStore, - }, - asyncStateService: { - useFactory: () => new CachedStateService(undefined), - }, batchStorage: { useClass: InMemoryBatchStorage, }, @@ -36,14 +31,14 @@ export class InMemoryDatabase extends SequencerModule implements Database { blockStorage: { useToken: "BlockQueue", }, - unprovenStateService: { - useFactory: () => new CachedStateService(undefined), + stateServiceCreator: { + useClass: InMemoryStateServiceCreator, }, - unprovenMerkleStore: { - useClass: InMemoryAsyncMerkleTreeStore, + treeStoreCreator: { + useClass: InMemoryTreeStoreCreator, }, blockTreeStore: { - useClass: InMemoryAsyncMerkleTreeStore, + useClass: InMemoryBaseMerkleTreeStore, }, messageStorage: { useClass: InMemoryMessageStorage, diff --git a/packages/sequencer/src/storage/inmemory/masking/InMemoryMerkleTreeStoreMask.ts b/packages/sequencer/src/storage/inmemory/masking/InMemoryMerkleTreeStoreMask.ts new file mode 100644 index 000000000..68f59177b --- /dev/null +++ b/packages/sequencer/src/storage/inmemory/masking/InMemoryMerkleTreeStoreMask.ts @@ -0,0 +1,24 @@ +import { AsyncMerkleTreeStore } from "../../../state/async/AsyncMerkleTreeStore"; +import { CachedMerkleTreeStore } from "../../../state/merkle/CachedMerkleTreeStore"; +import { InMemoryAsyncMerkleTreeStore } from "../InMemoryAsyncMerkleTreeStore"; + +export class InMemoryMerkleTreeStoreMask extends CachedMerkleTreeStore { + public constructor( + parent: AsyncMerkleTreeStore, + public readonly name: string + ) { + super(parent); + } + + public async createMask(name: string): Promise { + return new InMemoryMerkleTreeStoreMask(this, name); + } +} + +export class InMemoryBaseMerkleTreeStore extends InMemoryAsyncMerkleTreeStore { + public name = "base"; + + public async createMask(name: string): Promise { + return new InMemoryMerkleTreeStoreMask(this, name); + } +} diff --git a/packages/sequencer/src/storage/inmemory/masking/InMemoryStateServiceCreator.ts b/packages/sequencer/src/storage/inmemory/masking/InMemoryStateServiceCreator.ts new file mode 100644 index 000000000..9b21d7c61 --- /dev/null +++ b/packages/sequencer/src/storage/inmemory/masking/InMemoryStateServiceCreator.ts @@ -0,0 +1,18 @@ +import { StateServiceCreator } from "../../../state/masking/StateServiceCreator"; +import { MaskGraph } from "../../../state/masking/MaskGraph"; +import { AsyncStateService } from "../../../state/async/AsyncStateService"; + +import { InMemoryStateServiceMask } from "./InMemoryStateServiceMask"; + +export class InMemoryStateServiceCreator + extends MaskGraph< + AsyncStateService, + InMemoryStateServiceMask, + InMemoryStateServiceMask + > + implements StateServiceCreator +{ + public constructor() { + super(new InMemoryStateServiceMask(undefined, "base")); + } +} diff --git a/packages/sequencer/src/storage/inmemory/masking/InMemoryStateServiceMask.ts b/packages/sequencer/src/storage/inmemory/masking/InMemoryStateServiceMask.ts new file mode 100644 index 000000000..529cb24c1 --- /dev/null +++ b/packages/sequencer/src/storage/inmemory/masking/InMemoryStateServiceMask.ts @@ -0,0 +1,15 @@ +import { AsyncStateService } from "../../../state/async/AsyncStateService"; +import { CachedStateService } from "../../../state/state/CachedStateService"; + +export class InMemoryStateServiceMask extends CachedStateService { + public constructor( + parent: AsyncStateService | undefined, + public readonly name: string + ) { + super(parent); + } + + public async createMask(name: string): Promise { + return new InMemoryStateServiceMask(this, name); + } +} diff --git a/packages/sequencer/src/storage/inmemory/masking/InMemoryTreeStoreCreator.ts b/packages/sequencer/src/storage/inmemory/masking/InMemoryTreeStoreCreator.ts new file mode 100644 index 000000000..5a13b4585 --- /dev/null +++ b/packages/sequencer/src/storage/inmemory/masking/InMemoryTreeStoreCreator.ts @@ -0,0 +1,21 @@ +import { MaskGraph } from "../../../state/masking/MaskGraph"; +import { AsyncMerkleTreeStore } from "../../../state/async/AsyncMerkleTreeStore"; +import { TreeStoreCreator } from "../../../state/masking/TreeStoreCreator"; + +import { + InMemoryBaseMerkleTreeStore, + InMemoryMerkleTreeStoreMask, +} from "./InMemoryMerkleTreeStoreMask"; + +export class InMemoryTreeStoreCreator + extends MaskGraph< + AsyncMerkleTreeStore, + InMemoryBaseMerkleTreeStore, + InMemoryMerkleTreeStoreMask + > + implements TreeStoreCreator +{ + public constructor() { + super(new InMemoryBaseMerkleTreeStore()); + } +} diff --git a/packages/sequencer/src/storage/model/Block.ts b/packages/sequencer/src/storage/model/Block.ts index 823b3c8c6..d099ac4d3 100644 --- a/packages/sequencer/src/storage/model/Block.ts +++ b/packages/sequencer/src/storage/model/Block.ts @@ -82,6 +82,14 @@ export interface BlockWithMaybeResult { result?: BlockResult; } +export function assertBlockHasResult( + block: BlockWithMaybeResult +): asserts block is BlockWithResult { + if (block.result === undefined) { + throw new Error("Given block has no result attached"); + } +} + // eslint-disable-next-line @typescript-eslint/no-redeclare export const BlockWithResult = { // toBlockProverState: ({ block, result }: BlockWithResult) => ({ diff --git a/packages/sequencer/src/storage/repositories/BlockStorage.ts b/packages/sequencer/src/storage/repositories/BlockStorage.ts index 4803ba5a5..9c51c0e68 100644 --- a/packages/sequencer/src/storage/repositories/BlockStorage.ts +++ b/packages/sequencer/src/storage/repositories/BlockStorage.ts @@ -8,7 +8,7 @@ import type { export interface BlockQueue { pushBlock: (block: Block) => Promise; pushResult: (result: BlockResult) => Promise; - getNewBlocks: () => Promise; + getPendingBlocks: () => Promise; getLatestBlockAndResult: () => Promise; } diff --git a/packages/sequencer/test/integration/BlockProduction.test.ts b/packages/sequencer/test/integration/BlockProduction.test.ts index b24a6a732..8a18a4632 100644 --- a/packages/sequencer/test/integration/BlockProduction.test.ts +++ b/packages/sequencer/test/integration/BlockProduction.test.ts @@ -232,7 +232,7 @@ describe("block production", () => { ); // TODO // const newState = await test.getState(balancesPath, "batch"); - const newUnprovenState = await test.getState(balancesPath, "block"); + const newUnprovenState = await test.getState(balancesPath); // expect(newState).toBeDefined(); expect(newUnprovenState).toBeDefined(); @@ -248,7 +248,7 @@ describe("block production", () => { accountModule.accountState.keyType, publicKey ); - const newAccountState = await test.getState(accountStatePath, "block"); + const newAccountState = await test.getState(accountStatePath); expect(newAccountState).toBeDefined(); expect(AccountState.fromFields(newAccountState!).nonce.toBigInt()).toBe(1n); @@ -273,14 +273,14 @@ describe("block production", () => { expect(batch!.blockHashes).toHaveLength(1); expect(batch!.proof.proof).toBe(MOCK_PROOF); - const state2 = await test.getState(balancesPath, "block"); + const state2 = await test.getState(balancesPath); expect(state2).toBeDefined(); expect(UInt64.fromFields(state2!)).toStrictEqual(UInt64.from(200)); }, 60_000); it("should reject tx and not apply the state", async () => { - expect.assertions(5); + expect.assertions(4); const privateKey = PrivateKey.random(); @@ -302,12 +302,10 @@ describe("block production", () => { balanceModule.balances.keyType, PublicKey.empty() ); - const unprovenState = await test.getState(balancesPath, "block"); - const newState = await test.getState(balancesPath, "batch"); + const unprovenState = await test.getState(balancesPath); // Assert that state is not set expect(unprovenState).toBeUndefined(); - expect(newState).toBeUndefined(); }, 30_000); it("should produce txs in non-consecutive blocks", async () => { @@ -441,7 +439,7 @@ describe("block production", () => { balanceModule.balances.keyType, publicKey ); - const newState = await test.getState(balancesPath, "block"); + const newState = await test.getState(balancesPath); expect(newState).toBeDefined(); expect(UInt64.fromFields(newState!)).toStrictEqual( @@ -485,7 +483,7 @@ describe("block production", () => { balanceModule.balances.keyType, pk1.toPublicKey() ); - const newState1 = await test.getState(balancesPath1, "block"); + const newState1 = await test.getState(balancesPath1); expect(newState1).toBeUndefined(); @@ -494,7 +492,7 @@ describe("block production", () => { balanceModule.balances.keyType, pk2.toPublicKey() ); - const newState2 = await test.getState(balancesPath2, "block"); + const newState2 = await test.getState(balancesPath2); expect(newState2).toBeDefined(); expect(UInt64.fromFields(newState2!)).toStrictEqual(UInt64.from(100)); @@ -601,7 +599,7 @@ describe("block production", () => { "totalSupply", PROTOKIT_PREFIXES.STATE_RUNTIME ); - const newState = await test.getState(supplyPath, "block"); + const newState = await test.getState(supplyPath); expect(newState).toBeDefined(); expect(UInt64.fromFields(newState!)).toStrictEqual( @@ -617,7 +615,7 @@ describe("block production", () => { pk2 ); - const newBalance = await test.getState(balancesPath, "block"); + const newBalance = await test.getState(balancesPath); expect(newBalance).toBeDefined(); expect(UInt64.fromFields(newBalance!)).toStrictEqual(UInt64.from(200)); diff --git a/packages/sequencer/test/integration/StorageIntegration.test.ts b/packages/sequencer/test/integration/StorageIntegration.test.ts index 9fa4543b6..fc68163fc 100644 --- a/packages/sequencer/test/integration/StorageIntegration.test.ts +++ b/packages/sequencer/test/integration/StorageIntegration.test.ts @@ -8,7 +8,6 @@ import { Bool, Field, PrivateKey, UInt64 } from "o1js"; import { TypedClass, expectDefined } from "@proto-kit/common"; import { - AsyncStateService, BatchStorage, HistoricalBatchStorage, HistoricalBlockStorage, @@ -20,6 +19,8 @@ import { StorageDependencyFactory, BlockStorage, VanillaTaskWorkerModules, + StateServiceCreator, + MaskName, } from "../../src"; import { DefaultTestingSequencerModules, @@ -63,8 +64,7 @@ describe.each([["InMemory", InMemoryDatabase]])( >; let runtime: Runtime<{ Balance: typeof Balance }>; - let unprovenState: AsyncStateService; - let provenState: AsyncStateService; + let stateMasks: StateServiceCreator; // let unprovenTreeStore: AsyncMerkleTreeStore; // let provenTreeStore: AsyncMerkleTreeStore; @@ -125,8 +125,7 @@ describe.each([["InMemory", InMemoryDatabase]])( runtime = appChain.runtime; sequencer = appChain.sequencer; - unprovenState = sequencer.resolve("UnprovenStateService"); - provenState = sequencer.resolve("AsyncStateService"); + stateMasks = sequencer.resolve("StateServiceCreator"); }); it("test unproven block prod", async () => { @@ -146,7 +145,7 @@ describe.each([["InMemory", InMemoryDatabase]])( expectDefined(generatedBlock); - const blocks = await sequencer.resolve("BlockQueue").getNewBlocks(); + const blocks = await sequencer.resolve("BlockQueue").getPendingBlocks(); expect(blocks).toHaveLength(1); @@ -174,14 +173,11 @@ describe.each([["InMemory", InMemoryDatabase]])( ) ); - const state = await unprovenState.getMany( - Object.keys(stateDiff).map(Field) - ); + const mask = await stateMasks.getMask(MaskName.base()); + const state = await mask.getMany(Object.keys(stateDiff).map(Field)); expect(checkStateDiffEquality(stateDiff, state)).toBe(true); expect(state.length).toBeGreaterThanOrEqual(1); - - await expect(provenState.get(state[0].key)).resolves.toBeUndefined(); }); it("test proven block prod", async () => { @@ -191,7 +187,7 @@ describe.each([["InMemory", InMemoryDatabase]])( expectDefined(generatedBatch); - const blocks = await sequencer.resolve("BlockQueue").getNewBlocks(); + const blocks = await sequencer.resolve("BlockQueue").getPendingBlocks(); expect(blocks).toHaveLength(0); const batchStorage = sequencer.resolve( diff --git a/packages/sequencer/test/integration/services/BlockTestService.ts b/packages/sequencer/test/integration/services/BlockTestService.ts index f51c64393..cc44dfe01 100644 --- a/packages/sequencer/test/integration/services/BlockTestService.ts +++ b/packages/sequencer/test/integration/services/BlockTestService.ts @@ -3,12 +3,9 @@ import { Field, PrivateKey } from "o1js"; import { Runtime, RuntimeModulesRecord } from "@proto-kit/module"; import { ArgumentTypes } from "@proto-kit/common"; -import { - AsyncStateService, - ManualBlockTrigger, - PrivateMempool, -} from "../../../src"; +import { ManualBlockTrigger, MaskName, PrivateMempool } from "../../../src"; import { createTransaction } from "../utils"; +import { StateServiceCreator } from "../../../src/state/masking/StateServiceCreator"; @injectable() @scoped(Lifecycle.ContainerScoped) @@ -17,8 +14,8 @@ export class BlockTestService { @inject("BlockTrigger") private trigger: ManualBlockTrigger, @inject("Mempool") private mempool: PrivateMempool, @inject("Runtime") private runtime: Runtime, - @inject("AsyncStateService") private batchStateService: AsyncStateService, - @inject("UnprovenStateService") private blockStateService: AsyncStateService + @inject("StateServiceCreator") + private stateServiceCreator: StateServiceCreator ) {} private nonces: Record = {}; @@ -47,9 +44,8 @@ export class BlockTestService { this.nonces[privateKey.toPublicKey().toBase58()] = nonce + 1; } - public async getState(path: Field, type: "block" | "batch" = "block") { - const service = - type === "batch" ? this.batchStateService : this.blockStateService; + public async getState(path: Field) { + const service = await this.stateServiceCreator.getMask(MaskName.base()); return await service.get(path); } diff --git a/packages/sequencer/test/merkle/CachedMerkleStore.test.ts b/packages/sequencer/test/merkle/CachedMerkleStore.test.ts index 4d882d7db..aa1d2004a 100644 --- a/packages/sequencer/test/merkle/CachedMerkleStore.test.ts +++ b/packages/sequencer/test/merkle/CachedMerkleStore.test.ts @@ -45,10 +45,15 @@ describe("cached merkle store", () => { it("should preload through multiple levels", async () => { const cache2 = new CachedMerkleTreeStore(cache1); + const cache3 = new CachedMerkleTreeStore(cache2); - await cache2.preloadKey(5n); + await expect( + cache3.getNodesAsync([{ key: 5n, level: 0 }]) + ).resolves.toStrictEqual([10n]); + + await cache3.preloadKey(5n); - expect(cache2.getNode(5n, 0)).toStrictEqual(10n); + expect(cache3.getNode(5n, 0)).toStrictEqual(10n); }); it("should cache correctly", async () => {