-
Notifications
You must be signed in to change notification settings - Fork 109
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Can't use PrivateKey
on server in a NextJS 14 app
#1575
Comments
Ok, I just found a NextJS config that might be something I am missing. /** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: false,
webpack(config) {
config.resolve.alias = {
...config.resolve.alias,
o1js: require('path').resolve('node_modules/o1js')
};
config.experiments = { ...config.experiments, topLevelAwait: true };
return config;
},
// To enable o1js for the web, we must set the COOP and COEP headers.
// See here for more information: https://docs.minaprotocol.com/zkapps/how-to-write-a-zkapp-ui#enabling-coop-and-coep-headers
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'Cross-Origin-Opener-Policy',
value: 'same-origin',
},
{
key: 'Cross-Origin-Embedder-Policy',
value: 'require-corp',
},
],
},
];
}
};
module.exports = nextConfig Why does |
Why is Looks like there are some major bugs in the way |
I've now tried creating a very simple React Server Component: "use server";
import { Resource } from "sst";
export default async function Root() {
const { PrivateKey } = await import("o1js");
const privateKey = PrivateKey.fromBase58(Resource.RootKey.value);
return (
<main>
<h1>Public Key</h1>
<span>{privateKey?.toPublicKey().toBase58()}</span>
</main>
);
} I am disappointed. I really wanted to experiment with Mina but |
Hey @sam-goodwin! I appreciate you trying it out and writing up the problems you encountered!
o1js is a normal ES module. It is distributed in two different builds, one for Node.js and one for the web. Both "just work" when imported in their target environments: Node.js with o1js is also pretty fancy technology so it relies on some web APIs that your typical Todo app doesn't need. One of them is SharedArrayBuffer, for multithreading (without which snark proving would just be too slow). To allow use of SharedArrayBuffer, browsers require the COEP and COOP headers which you saw in that nextjs config. I think an extra config step to enable that is fine. The main problem here, as far as I can tell, is that NextJS doesn't "just work" when importing modern ES modules. It has trouble with top level await, which we use in our web export and which is supported in all major browsers since 3 years. Instead of serving these modules to the web in their existing, working form, NextJS runs them through the outdated webpack pipeline and messes them up. I'm not sure but I assume that our hacky custom resolving config is working around exactly that. Maybe @ymekuria can confirm. And in turn, the custom resolving might cause NextJS to pick the wrong o1js export when running the React server component - obviously it should use the Nodejs export, but the "navigator not defined" error suggests that it tries to use the web export instead. (I'm just extrapolating from your error messages, we still need to debug this ourselves) |
So, there's an existing plan that would allow us to get rid of top level await #1205 I'm not sure if that already solves everything though. It might still be necessary to tell webpack not to transpile our web export when bundling, since it might still not handle everything there. And we still need to find out if that's really what causes the Nodejs export to be used in the browser and vice versa, which are the two NextJS bugs reported here |
const { Mina, PrivateKey } = await import('o1js');
const { Add } = await import('../../../contracts/build/src/'); Btw @sam-goodwin this "bizarre code" is just about loading the library lazily, to reduce initial loading time. Actually I think it's a common pattern to use dynamic import for that 😅 |
Thanks for the responses, @mitschabaude.
Bundlers will optimize the bundled code and if there's expensive initialization code, we can move that out of the module import path and put it in a function. Give control to the user through explicit functions instead of through the Sticking to ordinary practices is going to have far less bugs and also scare less people off. I don't think I've ever seen an Is this the only place where top-level await is required? For the bindings? https://github.com/o1-labs/o1js/blob/main/src/snarky.js Could we instead defer this evaluation by placing it in an const Mina = await initMina(); Avoiding global state and expensive async processing when importing a module is generally good practice.
I think this is fine. Seems unavoidable. This bit scares me: reactStrictMode: false,
webpack(config) {
config.resolve.alias = {
...config.resolve.alias,
o1js: require('path').resolve('node_modules/o1js')
};
config.experiments = { ...config.experiments, topLevelAwait: true };
return config;
}, Anything we can do to remove that would be a win.
I appreciate that top-level await has been supported by browsers, but for Mina to succeed, I think prioritizing smooth integration with the popular web frameworks is more important than using a less supported, modern feature. RE: #1205 - glad to see there is a plan to remove top-level await. Anything I can do to help? I'd very much like to be able to use |
In the web version, this is the only top level await: Line 4 in c427142
|
And the plan to get rid of it is to:
|
What about the node version? I'd like to run this on the server side not just the client. Is that more work? |
Where does this get initialized: let snarky = globalThis.__snarky; |
Which places? Is that what is described here: #1205
|
Is this what you mean? async runAndCheck(f: (() => Promise<void>) | (() => void)) {
await initO1(); // call it here?
await generateWitness(f, { checkConstraints: true });
}, |
It's not obvious how to call |
Managed to get the node version of "main": "./dist/web/index.js", // <-remove this
"exports": {
"types": "./dist/node/index.d.ts",
"browser": "./dist/web/index.js",
"node": {
"import": "./dist/node/index.js",
"require": "./dist/node/index.cjs"
},
"default": "./dist/web/index.js"
}, Still hanging but at least running the right version now I think (not getting |
Nice catch!! 😮 |
Node version uses the one in |
The mechanism is that there are sometimes So in this case |
Yes, but that only listed the ones that weren't already async at the time of writing. (And therefore their external API needed to change in a breaking way). Others are:
|
|
Opened a PR to update the I believe this is required as a first step. Is there a reason why that code warrants a separate repo vs just being in this repo for simplicity? How can I make changes to both repos in 1 PR? Or is that not possible? |
it's inconvenient for sure, has to do with how the code is licensed
o1js-bindings is a git submodule of o1js. so you
|
Oh that's interesting. I thought it was possible to license different folders within a single repository. Major bummer if that's not possible here. |
I got most of the way towards removing TLA here: #1583 |
Hi @sam-goodwin! Thanks for all your feedback and clear descriptions of the problems you are facing. I agree that there are opportunities to remove the friction to create an app. The current NextJS scaffold in the reactStrictMode: false,
webpack(config) {
config.resolve.alias = {
...config.resolve.alias,
o1js: require('path').resolve('node_modules/o1js')
};
config.experiments = { ...config.experiments, topLevelAwait: true };
return config;
}, |
I am having trouble using
o1js
in a simple NextJS 14 app.You can find my repo here: https://github.com/sam-goodwin/private-wallet
Check out the repo and run:
It will just hang:
Comment out the
PrivateKey.random()
and the error goes away.When looking at the tutorials I spotted this bizarre code:
This raises some red flags. Is
o1js
not designed to work as a normal module that can be imported?The text was updated successfully, but these errors were encountered: