If you’re building on the Mina blockchain, this blog post is a must-read.
After completing an extensive security audit of the o1js v1 library — spanning 39 person-weeks with 4 security analysts — we’ve gained a comprehensive understanding of the framework. We’re eager to share our insights with the community.
To help you develop securely with the o1js TypeScript library, we’ve distilled our findings into four topics. In this post, we highlight common pitfalls, share real-world examples of vulnerabilities, and provide actionable guidance to ensure your projects harness o1js effectively and securely.
o1js brings cutting-edge technology to developers, enabling them to write ZK circuits in TypeScript and seamlessly deploy them to the Mina blockchain. While o1js abstracts much of the complexity of zero-knowledge proofs, it also introduces unique challenges and anti-patterns that developers must carefully navigate.
In this post, we dive into four illustrative examples:
Let’s dive in!
A UInt64 type is a supposed to represent a value in the range [0, 2⁶⁴), i.e. 0, 1, 2, 3, …, up to 18,446,744,073,709,551,615
UInt64.assertLessThan() allows you to assert that a certain value x is less than another value
Provable.runAndCheck(() => {\
// Introduce a UInt64 variable in the program\
let x = Provable.witness(UInt64, () => {return new UInt64(100n);});\
// Prove the variable is at most 2**48\
x.assertLessThan(new UInt64(2n**48n));\
})
In the above program, all we prove is that x is some number less than 2⁴⁸. We set x to 100 in our generator, but anyone can change the witness generator. More specifically, anything inside the *Provable.witness* function is not proven! *x* can have any value, so long as it satisfies the constraints!
But where are constraints defined?
Provable.witness(UInt64, ...) adds constraints defined in UInt64.check() to ensure that verification won’t succeed unless x is in the range [0, 2⁶⁴)
x.assertLessThan(new UInt64(2n**48n)) then asserts that x is in the range [0, 2⁴⁸)
So far so good….
Now a clever user looks at this and might think: “Why check x is in [0, 2⁶⁴) if we are going to check x is in [0, 2⁴⁸) anyway? We can just do the second check!”
// BUG IN CODE: DO NOT USE\
Provable.runAndCheck(() => {\
// Introduce an unconstrained variable in the program\
let xAsField = Provable.exists(1, () => {return new UInt64(100n);});\
// Unsafe cast it to a UInt64. This adds no constraints\
let x = UInt64.Unsafe.from(xAsField);\
// Prove the variable is at most 2**48\
x.assertLessThan(new UInt64(2n**48n));\
})
While this looks innocuous, it is actually under-constrained! We can use values for x which are much larger than the input field.
Why?
UInt64.assertLessThan() assumes that we already know *x < 2**64*. Under the hood, it then asserts that 2⁴⁸ - x is in the range [0, 2⁶⁴). Remember that x is really a field element, so all arithmetic occurs modulo p for some large [p](https://electriccoin.co/blog/the-pasta-curves-for-halo-2-and-beyond/), and we can think of the range [0, p) as all possible values of x!The implementation has the following cases:
This is why we can’t cheat: we need the constraint x ∈ 2⁶⁴ from Provable.witness(UInt64, ...) to eliminate this 3rd “bad case”.
One common mistake we’ve seen across ecosystems comes from a limitation of ZK itself: the circuit’s control flow is entirely static.
No if/else
How can we do computations then? Conditional data flow.
Instead of
if case1 {
x = f(y)
}
else {
x = g(y)
}
We have to compute f(y), compute g(y), and then set
x = case1 ? f(y) : g(y)
Of course, in o1js this would look like
x = Provable.if(
case1,
MyProvableType, // tell o1js the type to use
x,
y
)
How can this cause issues? We often want to do computations which mightsucceed. A classical example is division: division-by-zero is undefined.
Suppose we are implementing a simple vault. The vault holds funds. You can deposit funds equaling 1% of the currently managed funds, and the vault will mint you a number of tokens equal to 1% of the currently existing tokens.
For example, imagine the supply of vault Tokens is 1000 Token, and the vault is holding 100 USDC. If I deposit 1 USDC, the vault will mint me 1 USDC * 1000 Token / 100 USDC = 10 Token. If I deposit 10 USDC, the vault will mint 10 USDC * 1000 Token / 100 USDC = 100 Token.
Ignoring decimals, this might be written simply as
amountToMint = totalSupply.mul(depositedAmount).div(managedFunds)
But what about the starting case? Suppose there are no funds and we are the first depositor. Commonly, we will set some fixed ratio: e.g. mint 1 token per initial USDC deposited. A first attempt at implementing this might be the below:
// BUG IN CODE: DO NOT USE\
amountToMint = Provable.if(\
managedFunds.equals(new UInt64(0n)),\
UInt64, // Output type\
depositedAmount,\
totalSupply.mul(depositedAmount).div(managedFunds)\
)
This looks like it will work: if no funds are currently managed, amountToMintis depositedAmount. Otherwise, we compute the ratio of tokens to managed funds.
The problem is simple: we always computetotalSupply.mul(depositedAmount).div(managedFunds), even when managedFunds is equal to zero. To guarantee its correctness, UInt64.div() will cause an assertion failure when the denominator is zero.
This may seem not so bad: we’ll catch it during testing. The problem is, it isn’t always so obvious. For example, what if the above contract starts with a non-zero amount of funds/total supply set up before the vault was opened to the public? Then this issue will only manifest if the managedFunds reaches 0, at which point it can never be deposited into again.
A more serious (but analogous) example could prevent the last user from withdrawing from the vault.
While o1js looks a lot like other popular smart contract languages, there are some important differences.
Each @method creates an AccountUpdate: A single object representing a change of on-chain state. These AccountUpdates have preconditions on the state to ensure valid access. For example, if I am decreasing a smart contract Mina balance by 10, the node must validate the account has at least 10 Mina—even if I have a proof that I executed the smart contract correctly.
Why? Remember that ZK is “state-less:” it has no hard drive, disk, or memory. All you prove is that, for some inputs which only you know, you did the correct computation.
When dealing with on-chain values, we need to prove that we used the actual on-chain values, and not just some random numbers! How? We output the “private-input” as “public preconditions” of the AccountUpdate. The node can then check the public preconditions
With state, we do this via the getAndRequireEquals() function. o1js will cause an error if you call get() without getAndRequireEquals(). It will now also call an error if you getAndRequireEquals() with two different values, due to an issue reported by Veridise (see #1712 Assert Preconditions are not already set when trying to set their values)
Let’s take a simple example.
// BUG IN CODE: DO NOT USE
export class Pool extends SmartContract {
@state(Boolean) paused = State
@method function mint(amount: UInt64): UInt64 {
this.paused.get().assertFalse(“Pool paused!”);
// Minting logic
}
}
The above snippet is intended to prevent minting when the protocol is paused. In actuality, it just proves that the prover set paused to False in their local environment when generating the proof. To ensure that the network validates this assumption, the code should instead use getAndRequireEquals(). This way, the assumption paused = False is included in the AccountUpdate as part of the proof, forcing the network node to validate that the on-chain protocol is not paused.
export class Pool extends SmartContract {
@state(Boolean) paused = State
@method function mint(amount: UInt64): UInt64 {
this.paused.getAndRequireEquals().assertFalse(“Pool paused!”);
// Minting logic
}
}
Preconditions can cause problems when concurrent accesses are occurring. Say you and I are both incrementing a counter. Our AccountUpdate will have two important parts:
xx+1If you and I both call this function when x = 3, we both have a precondition *x=3*! That means whichever one of us is executed second will have our AccountUpdate fail, since after the first person goes, x = 4 != 3
How can we fix this? We queue up actions.
Mina has a feature called actions & reducers. You can submit an “Action”, which gets put in a queue. Later, users can call a “reducer” which calls a function on those actions
Let’s look at an example taken from reducer-composite.ts:
class MaybeIncrement extends Struct({
isIncrement: Bool,
otherData: Field,
}) {}
const INCREMENT = { isIncrement: Bool(true), otherData: Field(0) };
class Counter extends SmartContract {
// the “reducer” field describes a type of action that we can dispatch, and reduce later
reducer = Reducer({ actionType: MaybeIncrement });
// on-chain version of our state. it will typically lag behind the
// version that’s implicitly represented by the list of actions
@state(Field) counter = State
@method async incrementCounter() {
this.reducer.dispatch(INCREMENT);
}
@method async dispatchData(data: Field) {
this.reducer.dispatch({ isIncrement: Bool(false), otherData: data });
}
@method async rollupIncrements() {
// get previous counter & actions hash, assert that they’re the same as on-chain values
let counter = this.counter.getAndRequireEquals();
let actionState = this.actionState.getAndRequireEquals();
// compute the new counter and hash from pending actions\
let pendingActions = this.reducer.getActions({\
fromActionState: actionState,\
});
let newCounter = this.reducer.reduce(\
pendingActions,\
// state type\
Field,\
// function that says how to apply an action\
(state: Field, action: MaybeIncrement) => {\
return Provable.if(action.isIncrement, state.add(1), state);\
},\
counter,\
{ maxUpdatesWithActions: 10 }\
);
// update on-chain state\
this.counter.set(newCounter);\
this.actionState.set(pendingActions.hash);\ }\ }
In this code, incrementCounter() dispatches an action to the queue requesting an increment. dispatchData() adds a queue with some other unrelated data.
Anyone can process the entire queue by calling rollupIncrements(). This will go through the whole queue, incrementing once for each submitted (but unprocessed) request to increment.
Note:
actionState field, which tracks “where we are in the queue.” In particular, this.actionState tracks what parts of the queue have been processed, while the Mina node automatically tracks what actions have been submitted via dispatchSuppose that, instead of just incrementing by one, the user provided a number to add (e.g. an account balance change).
class MaybeIncrement extends Struct({
isIncrement: Bool,
otherData: Field,
amount: UInt64
}) {}
// function that says how to apply an action
(state: Field, action: MaybeIncrement) => {
return Provable.if(action.isIncrement, state.add(action.amount), state);
},
A malicious user could submit several large amounts, e.g.
{
isIncrement: new Bool(0),
otherData: new Field(0),
amount: new UInt64(UInt64.MAX),
}
Action submission will work smoothly, but this single action can permanently prevent all other actions from being processed!
Using this simplified example, the only way to process actions is in a single, large batch. Using more complex constructions like the batch reducer, or a custom rollup proof can get around this issue, but at the time of audit, only simple examples were made available for us to review.
If you decide to use the actions and reducer pattern, the reducer must be guaranteed to succeed once an action is submitted. This means that any arithmetic must be inspected carefully, actions must be strictly validated at submission, and must be canonicalized (see this PR #1759 Canonical representation of provable types, which was developed as a solution to an issue raised during the Veridise audit).
To see what other solutions the O(1) community is working on, check out this article from zkNoid.
o1js empowers developers to build cutting-edge zero-knowledge applications on the Mina blockchain, leveraging the simplicity of TypeScript to create and deploy ZK circuits.
However, this power comes with responsibilities. Developers must be vigilant about potential vulnerabilities, from respecting type constraints and avoiding under-constrained circuits to managing state updates effectively and mitigating concurrency risks.
We hope the examples and strategies shared in this blog provide a solid foundation for developing securely on Mina. The challenges and pitfalls of working with zero-knowledge circuits can be intricate, but with careful attention to detail and adherence to best practices, they can be navigated successfully.
Ben Sepanski, Chief Security Officer at Veridise
| Github | Request Audit |
[

](https://medium.com/veridise?source=post_page—post_publication_info–fff3a3f4f6d1—————————————)
[
](https://medium.com/veridise?source=post_page—post_publication_info–fff3a3f4f6d1—————————————)
Our mission in to harden blockchain security with formal methods. We write about blockchain security, zero-knowledge proofs, and our bug discoveries.
Follow
[

](https://medium.com/@veridise?source=post_page—post_author_info–fff3a3f4f6d1—————————————)
[
](https://medium.com/@veridise?source=post_page—post_author_info–fff3a3f4f6d1—————————————)
Hardening blockchain security with formal methods. We write about blockchain & zero-knowledge proof security. Contact us for industry-leading security audits.
Follow
[
What are your thoughts?
](https://medium.com/m/signin?operation=register&redirect=https%3A%2F%2Fmedium.com%2Fveridise%2Fmastering-o1js-on-mina-four-key-strategies-for-secure-development-fff3a3f4f6d1&source=—post_responses–fff3a3f4f6d1———————respond_sidebar——————)
Cancel
Respond
Respond
Also publish to my profile

[

](https://medium.com/veridise?source=post_page—author_recirc–fff3a3f4f6d1—-0———————d84e01f0_b292_493f_bbea_aac71074a8f8————–)
In
[
Veridise
](https://medium.com/veridise?source=post_page—author_recirc–fff3a3f4f6d1—-0———————d84e01f0_b292_493f_bbea_aac71074a8f8————–)
by
[
Veridise
](https://medium.com/@veridise?source=post_page—author_recirc–fff3a3f4f6d1—-0———————d84e01f0_b292_493f_bbea_aac71074a8f8————–)
[
](https://medium.com/veridise/zero-knowledge-for-dummies-introduction-to-zk-proofs-29e3fe9604f1?source=post_page—author_recirc–fff3a3f4f6d1—-0———————d84e01f0_b292_493f_bbea_aac71074a8f8————–)
Aug 24, 2023
[
150
1
](https://medium.com/veridise/zero-knowledge-for-dummies-introduction-to-zk-proofs-29e3fe9604f1?source=post_page—author_recirc–fff3a3f4f6d1—-0———————d84e01f0_b292_493f_bbea_aac71074a8f8————–)

[

](https://medium.com/veridise?source=post_page—author_recirc–fff3a3f4f6d1—-1———————d84e01f0_b292_493f_bbea_aac71074a8f8————–)
In
[
Veridise
](https://medium.com/veridise?source=post_page—author_recirc–fff3a3f4f6d1—-1———————d84e01f0_b292_493f_bbea_aac71074a8f8————–)
by
[
Veridise
](https://medium.com/@veridise?source=post_page—author_recirc–fff3a3f4f6d1—-1———————d84e01f0_b292_493f_bbea_aac71074a8f8————–)
[
](https://medium.com/veridise/zero-knowledge-for-dummies-demystifying-zk-circuits-c140a64c6ed3?source=post_page—author_recirc–fff3a3f4f6d1—-1———————d84e01f0_b292_493f_bbea_aac71074a8f8————–)
Jan 19, 2024
[
27
](https://medium.com/veridise/zero-knowledge-for-dummies-demystifying-zk-circuits-c140a64c6ed3?source=post_page—author_recirc–fff3a3f4f6d1—-1———————d84e01f0_b292_493f_bbea_aac71074a8f8————–)

[

](https://medium.com/veridise?source=post_page—author_recirc–fff3a3f4f6d1—-2———————d84e01f0_b292_493f_bbea_aac71074a8f8————–)
In
[
Veridise
](https://medium.com/veridise?source=post_page—author_recirc–fff3a3f4f6d1—-2———————d84e01f0_b292_493f_bbea_aac71074a8f8————–)
by
[
Veridise
](https://medium.com/@veridise?source=post_page—author_recirc–fff3a3f4f6d1—-2———————d84e01f0_b292_493f_bbea_aac71074a8f8————–)
[
](https://medium.com/veridise/highlights-from-the-veridise-o1js-v1-audit-three-zero-knowledge-security-bugs-explained-2f5708f13681?source=post_page—author_recirc–fff3a3f4f6d1—-2———————d84e01f0_b292_493f_bbea_aac71074a8f8————–)
Feb 3

[

](https://medium.com/veridise?source=post_page—author_recirc–fff3a3f4f6d1—-3———————d84e01f0_b292_493f_bbea_aac71074a8f8————–)
In
[
Veridise
](https://medium.com/veridise?source=post_page—author_recirc–fff3a3f4f6d1—-3———————d84e01f0_b292_493f_bbea_aac71074a8f8————–)
by
[
Veridise
](https://medium.com/@veridise?source=post_page—author_recirc–fff3a3f4f6d1—-3———————d84e01f0_b292_493f_bbea_aac71074a8f8————–)
[
](https://medium.com/veridise/introduction-to-nova-and-zk-folding-schemes-4ef717574484?source=post_page—author_recirc–fff3a3f4f6d1—-3———————d84e01f0_b292_493f_bbea_aac71074a8f8————–)
Jul 27, 2023
[
70
](https://medium.com/veridise/introduction-to-nova-and-zk-folding-schemes-4ef717574484?source=post_page—author_recirc–fff3a3f4f6d1—-3———————d84e01f0_b292_493f_bbea_aac71074a8f8————–)
[
See all from Veridise
](https://medium.com/@veridise?source=post_page—author_recirc–fff3a3f4f6d1—————————————)
[
See all from Veridise
](https://medium.com/veridise?source=post_page—author_recirc–fff3a3f4f6d1—————————————)
[

](https://medium.com/@mohantaankit2002?source=post_page—read_next_recirc–fff3a3f4f6d1—-0———————ebb31a75_5f5f_46b7_8d3d_a06f39dfabc3————–)
[
Ankit
](https://medium.com/@mohantaankit2002?source=post_page—read_next_recirc–fff3a3f4f6d1—-0———————ebb31a75_5f5f_46b7_8d3d_a06f39dfabc3————–)
[
](https://medium.com/@mohantaankit2002/making-react-native-animations-buttery-smooth-on-budget-phones-f6ff3d4215bd?source=post_page—read_next_recirc–fff3a3f4f6d1—-0———————ebb31a75_5f5f_46b7_8d3d_a06f39dfabc3————–)
Feb 15

[

](https://medium.com/stackademic?source=post_page—read_next_recirc–fff3a3f4f6d1—-1———————ebb31a75_5f5f_46b7_8d3d_a06f39dfabc3————–)
In
[
Stackademic
](https://medium.com/stackademic?source=post_page—read_next_recirc–fff3a3f4f6d1—-1———————ebb31a75_5f5f_46b7_8d3d_a06f39dfabc3————–)
by
[
Abvhishek kumar
](https://medium.com/@abvhishekkumaar?source=post_page—read_next_recirc–fff3a3f4f6d1—-1———————ebb31a75_5f5f_46b7_8d3d_a06f39dfabc3————–)
[
](https://medium.com/stackademic/mastering-react-native-in-2025-real-talk-from-a-developers-perspective-96aa64910a20?source=post_page—read_next_recirc–fff3a3f4f6d1—-1———————ebb31a75_5f5f_46b7_8d3d_a06f39dfabc3————–)
6d ago
[
2
1
](https://medium.com/stackademic/mastering-react-native-in-2025-real-talk-from-a-developers-perspective-96aa64910a20?source=post_page—read_next_recirc–fff3a3f4f6d1—-1———————ebb31a75_5f5f_46b7_8d3d_a06f39dfabc3————–)
[


817 stories-1632 saves
](https://medium.com/@MediumStaff/list/staff-picks-c7bc6e1ee00f?source=post_page—read_next_recirc–fff3a3f4f6d1—————————————)
[



19 stories-942 saves
](https://medium.com/@MediumStaff/list/stories-to-help-you-levelup-at-work-faca18b0622f?source=post_page—read_next_recirc–fff3a3f4f6d1—————————————)
[



20 stories-3313 saves
](https://medium.com/@MediumForTeams/list/selfimprovement-101-3c62b6cb0526?source=post_page—read_next_recirc–fff3a3f4f6d1—————————————)
[



20 stories-2787 saves
](https://medium.com/@MediumForTeams/list/productivity-101-f09f1aaf38cd?source=post_page—read_next_recirc–fff3a3f4f6d1—————————————)
[

](https://medium.com/@impure?source=post_page—read_next_recirc–fff3a3f4f6d1—-0———————ebb31a75_5f5f_46b7_8d3d_a06f39dfabc3————–)
[
Andrew Zuo
](https://medium.com/@impure?source=post_page—read_next_recirc–fff3a3f4f6d1—-0———————ebb31a75_5f5f_46b7_8d3d_a06f39dfabc3————–)
[
](https://medium.com/@impure/go-is-a-poorly-designed-language-actually-a8ec508fc2ed?source=post_page—read_next_recirc–fff3a3f4f6d1—-0———————ebb31a75_5f5f_46b7_8d3d_a06f39dfabc3————–)
4d ago
[
263
24
](https://medium.com/@impure/go-is-a-poorly-designed-language-actually-a8ec508fc2ed?source=post_page—read_next_recirc–fff3a3f4f6d1—-0———————ebb31a75_5f5f_46b7_8d3d_a06f39dfabc3————–)

[

](https://medium.com/gitconnected?source=post_page—read_next_recirc–fff3a3f4f6d1—-1———————ebb31a75_5f5f_46b7_8d3d_a06f39dfabc3————–)
In
[
Level Up Coding
](https://medium.com/gitconnected?source=post_page—read_next_recirc–fff3a3f4f6d1—-1———————ebb31a75_5f5f_46b7_8d3d_a06f39dfabc3————–)
by
[
Promise Chukwuenyem
](https://medium.com/@promisepreston?source=post_page—read_next_recirc–fff3a3f4f6d1—-1———————ebb31a75_5f5f_46b7_8d3d_a06f39dfabc3————–)
[
](https://medium.com/gitconnected/from-linux-to-mac-building-the-perfect-development-machine-in-2025-14db582f239f?source=post_page—read_next_recirc–fff3a3f4f6d1—-1———————ebb31a75_5f5f_46b7_8d3d_a06f39dfabc3————–)
Jan 13
[
351
3
](https://medium.com/gitconnected/from-linux-to-mac-building-the-perfect-development-machine-in-2025-14db582f239f?source=post_page—read_next_recirc–fff3a3f4f6d1—-1———————ebb31a75_5f5f_46b7_8d3d_a06f39dfabc3————–)

[

](https://medium.com/veridise?source=post_page—read_next_recirc–fff3a3f4f6d1—-2———————ebb31a75_5f5f_46b7_8d3d_a06f39dfabc3————–)
In
[
Veridise
](https://medium.com/veridise?source=post_page—read_next_recirc–fff3a3f4f6d1—-2———————ebb31a75_5f5f_46b7_8d3d_a06f39dfabc3————–)
by
[
Veridise
](https://medium.com/@veridise?source=post_page—read_next_recirc–fff3a3f4f6d1—-2———————ebb31a75_5f5f_46b7_8d3d_a06f39dfabc3————–)
[
](https://medium.com/veridise/highlights-from-the-veridise-o1js-v1-audit-three-zero-knowledge-security-bugs-explained-2f5708f13681?source=post_page—read_next_recirc–fff3a3f4f6d1—-2———————ebb31a75_5f5f_46b7_8d3d_a06f39dfabc3————–)
Feb 3

[

](https://medium.com/@cryptowithlorenzo?source=post_page—read_next_recirc–fff3a3f4f6d1—-3———————ebb31a75_5f5f_46b7_8d3d_a06f39dfabc3————–)
[
Crypto with Lorenzo
](https://medium.com/@cryptowithlorenzo?source=post_page—read_next_recirc–fff3a3f4f6d1—-3———————ebb31a75_5f5f_46b7_8d3d_a06f39dfabc3————–)
[
](https://medium.com/@cryptowithlorenzo/bitcoin-is-going-to-zero-5562122f5481?source=post_page—read_next_recirc–fff3a3f4f6d1—-3———————ebb31a75_5f5f_46b7_8d3d_a06f39dfabc3————–)
Feb 5
[
390
36
](https://medium.com/@cryptowithlorenzo/bitcoin-is-going-to-zero-5562122f5481?source=post_page—read_next_recirc–fff3a3f4f6d1—-3———————ebb31a75_5f5f_46b7_8d3d_a06f39dfabc3————–)
[
See more recommendations
](https://medium.com/?source=post_page—read_next_recirc–fff3a3f4f6d1—————————————)
[
Help
](https://help.medium.com/hc/en-us?source=post_page—–fff3a3f4f6d1—————————————)
[
Status
](https://medium.statuspage.io/?source=post_page—–fff3a3f4f6d1—————————————)
[
About
](https://medium.com/about?autoplay=1&source=post_page—–fff3a3f4f6d1—————————————)
[
Careers
](https://medium.com/jobs-at-medium/work-at-medium-959d1a85284e?source=post_page—–fff3a3f4f6d1—————————————)
[
Press
](mailto:[email protected])
[
Blog
](https://blog.medium.com/?source=post_page—–fff3a3f4f6d1—————————————)
[
Privacy
](https://policy.medium.com/medium-privacy-policy-f03bf92035c9?source=post_page—–fff3a3f4f6d1—————————————)
[
Terms
](https://policy.medium.com/medium-terms-of-service-9db0094a1e0f?source=post_page—–fff3a3f4f6d1—————————————)
[
Text to speech
](https://speechify.com/medium?source=post_page—–fff3a3f4f6d1—————————————)
[
Teams
](https://medium.com/business?source=post_page—–fff3a3f4f6d1—————————————)ridise%2Fhighlights-from-the-veridise-o1js-v1-audit-three-zero-knowledge-security-bugs-explained-2f5708f13681&source=post_page—top_nav_layout_nav———————–global_nav——————)

[

](https://medium.com/@veridise?source=post_page—byline–2f5708f13681—————————————)
[

](https://medium.com/veridise?source=post_page—byline–2f5708f13681—————————————)
Published in
[
Veridise
](https://medium.com/veridise?source=post_page—byline–2f5708f13681—————————————)
10 min read
Feb 3, 2025

In 2024, the Veridise team conducted a comprehensive security audit of o1js, a crucial TypeScript library that powers zero-knowledge application development on the Mina blockchain.
The security assessment spanned 39 person-weeks, with four security analysts working over a period of 13 weeks. The audit strategy combined tool-assisted analysis of the source code by Veridise engineers with extensive manual, line-by-line code reviews.
In this blog post, we’ll dive into three of the most intriguing vulnerabilities uncovered during our audit. These issues are particularly noteworthy because they span different layers of the cryptographic stack, ranging from low-level field arithmetic to high-level protocol design. What unites them all is their relation to range checks.
To make the findings easier to follow and understand, we’ve simplified the bugs into illustrative examples. Full reporting on the actual vulnerabilities can be found in the full audit report.
Our collaboration with o1Labs was both productive and engaging. We had weekly meetings and maintained constant communication via Slack.
Most of our interactions were with Gregor and Florian, who were highly active and deeply involved. They worked closely with us to enhance our understanding of the system and even identified some of the bugs independently, working in parallel with our team.
They frequently shared detailed insights through long Slack threads, and were responsive to any queries from our auditors. Their deep knowledge of the codebase allowed them to efficiently guide us to the areas our auditors needed to focus on.
A highlight of the collaboration was Gregor’s incredibly thorough writeups on optimizations. These were invaluable in helping us navigate and comprehend complex circuits, such as emulated field arithmetic and multi-scalar multiplication. His detailed explanations were helpful in our ability to follow and address these intricate components.
Among the high-severity vulnerabilities we discovered in o1js was a subtle but dangerous flaw in how circuits which validate ECDSA signatures or manipulate foreign curve points are verified. This bug (V-O1J-VUL-006) highlights how missing range checks can undermine the security of cryptographic protocols.
As an overview, in o1js, data is often decomposed into smaller components for processing. A common example is bit decomposition, where a number is broken down into its binary representation:
For instance:
3 can be written as 1 + 1 * 2, which is encoded as [1, 1].7 can be written as 1 + 1 * 2 + 1 * 4, encoded as [1, 1, 1].This same decomposition concept can be applied to larger bases. For example, in o1js, instead of base 2, you might use 2^88:
[1, 1, 1] in this context represents:1 + 1 * 2^88 + 1 * (2^88)^2 = 1 + 1 * 2^88 + 1 * 2^176.The decomposition is only well-defined (or unique) if each component in the representation remains within a specified range.
In bit decomposition, each entry must be either 0 or 1. If this condition is violated, ambiguities arise.
For instance, 7 could be decomposed in multiple ways:[1, 3, 0] or [1, 1, 1].
This happens because you can “borrow” from higher components. Example:
1 + 1 * 2 + 1 * 4 can also be expressed as:1 + (1 + 2) * 2 + (2 - 2) * 4 = 1 + 3 * 2.2^88)For larger bases like 2^88, each entry must satisfy:0 ≤ entry < 2^88.
Without this constraint, similar ambiguity occurs: You can “add” or “subtract” between components to create alternate decompositions.
In this case, a custom type was represented using three limbs, each of size 2^88.
However, there was no check to ensure that the limbs were actually within the range [0, 2^88 - 1].
An attacker can manipulate the values of these limbs and carefully choose values that overflow during subsequent computations.
This creates opportunities for cryptographic exploits and undermines the integrity of the protocol.
The root cause of this vulnerability — and similar ones — is missing range checks. Ensuring proper type and range validation is critical to maintaining the security and correctness of zero-knowledge circuits.
The basic idea of bug V-O1J-VUL-002 is that a mapping is being stored in a Merkle tree with more leaves than keys in the map.
keys are limited to 254 bits, so they lie within the range [0, 2**254). However, the Merkle tree has more index es than there are unique key s!
This means some index es in the tree must map to the same **key**.
In fact, it is straightforward to determine which indexes share the same key—there are trillions of possibilities.
Suppose a key is an address and a value indicates whether the address is blacklisted (true) or not (false).
A single key might correspond to two distinct indexes in the Merkle tree: At one index, the value stored is true. At another index, the value stored is false (an edge case overlooked by developers).
The attacker can choose which index to use, enabling them to exploit the system. Naturally, the attacker will select the index with the value advantageous to them.
To summarize, the core issue is that instead of a one-to-one relationship between keys and indexes, some keys correspond to multiple indexes. This allows attackers to exploit the ambiguity and choose the mapping that benefits them.
As shown in the above proof of concept, a user may prove that some entries of a MerkleMap are empty, even after they have been set. This can have critical consequences, such as user could prove their address is not in a blacklist, or that a certain nullifier is not in a Merkle tree.
We recommended a range-check on to prevent this overflow.
While this high-level overview omits some details, it captures the essence of the vulnerability. Full description of the bug can be found in the audit report, PDF page 15.
The core concept in the first bug (V-O1J-VUL-001, PDF page 15) revolves around multiscalar multiplication and a bug in a compress-select-decompressprocess. This bug would have enabled attackers to have control over the output of the multi-scalar computation.
In this blog post, we’re giving a simplified example of the bug for easier comprehension and readability. The actual bug specific to o1js can be studied in the audit report.
At a high level, multiscalar multiplication essentially means performing multiple multiplications and adding the results together. However, instead of multiplying numbers, in this context one is usually dealing with cryptographic constructs called elliptic curve points.
Multiscalar multiplication (MSM) is usually implemented as a specialized operation designed to make common calculations faster and more efficient.
Rather than describe the full context, this blog will focus on a particular step in the multi-scalar multiplication algorithm. At this step, o1js needs to choose between two possible values for an array. For details on how this fits into the larger MSM algorithm, see here. For the purposes of this blog, just know that if an attacker can control which of the two possible arrays is chosen, they can potentially alter the output of standard cryptographic algorithms like ECDSA.
ZK circuits lack traditional control flow structures like if-else statements. Instead, both options (e.g., left and right) must be computed, and the desired option is selected by multiplying it by 1, while the undesired option is multiplied by 0.
This approach can be inefficient, especially when the options involve large datasets. For example, if Option 1 sets 10 values (e.g., 1, 2, 3, ... 10) and Option 2 sets another 10 values (e.g., 11, 12, 13, ... 20), the circuit essentially computes both options in full and pays the computational cost for each, even though only one is used. This inefficiency is the problem o1js aims to optimize.
By compressing the data first, the circuit avoids the overhead of making the decision 10 times. Instead, it only makes the decision once, based on the compressed hashes.
The key vulnerability lies in the compress-select-decompress process. In ZK circuits, decompression must reliably produce the exact original values from the compressed hash. Any mismatch here could lead to attackers gaining control of the execution.
Compression functions typically handle data as 1s and 0s. These binary representations might correspond to scalars, field elements, or booleans, or other types.
If the same binary data (1s and 0s) can be decoded into multiple possible values, the decompression process might yield incorrect or unexpected results. This ambiguity creates a potential exploit.
In simple terms, the issue in the o1js code arises from an optimization routine that is not injective. Injective means that the decoding process can produce multiple valid solutions, leading to potential vulnerabilities.
Here’s how the original optimization process works:
[a0, a1] are compressed into a single hash: hash(encode([a0, a1])).[b0, b1] is compressed: hash(encode([b0, b1])).2. “Switch” step:
choice.3. Decompression:
assert(hash(encode([r0, r1])) == choice).4. Result Extraction:
[r0, r1] are obtained as the resulting values.The encoding process lacks the necessary property to guarantee that decoding and re-encoding will always yield the same result. In other words:
decode(encode([r0, r1])) == choice
does not consistently hold true. This means that the same encoded value can be decoded into multiple different outputs, introducing ambiguity.
Such behavior creates a vulnerability, as the system may fail to reliably match the original values after decompression. This non-injective encoding undermines the security and correctness of the algorithm.
Consider an array of length 2, where each value is a boolean (0 or 1). Possible values for the array are:[0, 0], [0, 1], [1, 0], or [1, 1]
We define the following:
[a0, a1][b0, b1]s, which can be 0 or 1[r0, r1]For each element in the result:
Compute r0 using:r0 := (1-s) * a0 + s * b0
Why does this work?
s = 0:r0 := (1 - 0) * a0 + 0 * b0 = a0s = 1:r0 := 0 * a0 + 1 * b0 = b0Similarly, compute r1 using:
r1 := (1-s) * a1 + s * b1This computation is sometimes called “switching” between a and b.
This approach works but can be slow, as it requires repeating the computation for each element. With an array of length two, this is not so bad. But as the arrays get longer and longer, the repeated computation can take a toll.
Instead of directly switching, the optimization uses an encoding function:
encode([a0, a1]) = a0 + 2 * a1encode([b0, b1]) = b0 + 2 * b1This encoding maps the arrays uniquely:
[0, 0] → 0 + 2 * 0 = 0[0, 1] → 0 + 2 * 1 = 2[1, 0] → 1 + 2 * 0 = 1[1, 1] → 1 + 2 * 1 = 3This seems fine so far, as the mappings are unique.
cA := a0 + 2 * a1
cB := b0 + 2 * b1
s to choose between the compressed values:cR := (1-s) * cA + s * cB
r0, r1 from the witness generator and add a constraint to ensure the decompressed values match:r0 + 2 * r1 = cR
The issue lies in the decompression step. The witness generator is not constrained to produce valid boolean values (0 or 1) for r0 and r1.
This means that instead of [r0, r1] being restricted to [0, 1], they can take arbitrary values.
An attacker could choose r0 = 1,000,000 and r1 = -500,000. Let’s compute the compressed value:
cR = r0 + 2 * r1 = 1,000,000 + 2 * (-500,000) = 1,000,000 - 1,000,000 = 0
This satisfies the constraint r0 + 2 * r1 = cR, but clearly, the values [r0, r1] do not represent valid boolean values.
During compression, the original arrays [a0, a1] and [b0, b1] were constrained to boolean values (0 or 1).
However, during decompression, the original code failed to enforce these constraints, allowing the witness generator to produce invalid values.
This lack of constraints makes the encoding non-injective, meaning the decompressed values can correspond to multiple possible outputs, enabling attackers to exploit the system.
The vulnerability arises because the decompression step lacks proper constraints, breaking the injective property of the encoding-decoding process. To fix this, the system must enforce that r0 and r1 remain within their valid range (0 or 1) during decompression.
o1js has a very active set of contributors and is very interested in security. They have worked hard to bring ZK technology to TypeScript developers, and overcome a set of unique challenges in bringing smart contract logic to the Mina blockchain.
We found out it remarkably straightforward to prototype proof-of-concept applications using the TypeScript library. This ease of use extends to identifying under-constrained bugs and testing circuits in ways that developers might not have anticipated.
If you’re planning to develop dapps on the Mina blockchain using TypeScript, stay tuned — our upcoming blog post will provide insights and best practices for secure development, drawing from our auditing experience.
Download the full o1js security audit report here.
Author:
Ben Sepanski, Chief Security Officer at Veridise
Editor: Mikko Ikola, VP of Marketing
]]>In 2024, the Veridise team conducted a comprehensive security audit of o1js, a crucial TypeScript library that powers zero-knowledge application development on the Mina blockchain.
The security assessment spanned 39 person-weeks, with four security analysts working over a period of 13 weeks. The audit strategy combined tool-assisted analysis of the source code by Veridise engineers with extensive manual, line-by-line code reviews.
In this blog post, we’ll dive into three of the most intriguing vulnerabilities uncovered during our audit. These issues are particularly noteworthy because they span different layers of the cryptographic stack, ranging from low-level field arithmetic to high-level protocol design. What unites them all is their relation to range checks.
To make the findings easier to follow and understand, we’ve simplified the bugs into illustrative examples. Full reporting on the actual vulnerabilities can be found in the full audit report.
Our collaboration with o1Labs was both productive and engaging. We had weekly meetings and maintained constant communication via Slack.
Most of our interactions were with Gregor and Florian, who were highly active and deeply involved. They worked closely with us to enhance our understanding of the system and even identified some of the bugs independently, working in parallel with our team.
They frequently shared detailed insights through long Slack threads, and were responsive to any queries from our auditors. Their deep knowledge of the codebase allowed them to efficiently guide us to the areas our auditors needed to focus on.
A highlight of the collaboration was Gregor’s incredibly thorough writeups on optimizations. These were invaluable in helping us navigate and comprehend complex circuits, such as emulated field arithmetic and multi-scalar multiplication. His detailed explanations were helpful in our ability to follow and address these intricate components.
Among the high-severity vulnerabilities we discovered in o1js was a subtle but dangerous flaw in how circuits which validate ECDSA signatures or manipulate foreign curve points are verified. This bug (V-O1J-VUL-006) highlights how missing range checks can undermine the security of cryptographic protocols.
As an overview, in o1js, data is often decomposed into smaller components for processing. A common example is bit decomposition, where a number is broken down into its binary representation:
For instance:
3 can be written as 1 + 1 * 2, which is encoded as [1, 1].7 can be written as 1 + 1 * 2 + 1 * 4, encoded as [1, 1, 1].This same decomposition concept can be applied to larger bases. For example, in o1js, instead of base 2, you might use 2^88:
[1, 1, 1] in this context represents:1 + 1 * 2^88 + 1 * (2^88)^2 = 1 + 1 * 2^88 + 1 * 2^176.The decomposition is only well-defined (or unique) if each component in the representation remains within a specified range.
In bit decomposition, each entry must be either 0 or 1. If this condition is violated, ambiguities arise.
For instance, 7 could be decomposed in multiple ways:[1, 3, 0] or [1, 1, 1].
This happens because you can “borrow” from higher components. Example:
1 + 1 * 2 + 1 * 4 can also be expressed as:1 + (1 + 2) * 2 + (2 - 2) * 4 = 1 + 3 * 2.2^88)For larger bases like 2^88, each entry must satisfy:0 ≤ entry < 2^88.
Without this constraint, similar ambiguity occurs: You can “add” or “subtract” between components to create alternate decompositions.
In this case, a custom type was represented using three limbs, each of size 2^88.
However, there was no check to ensure that the limbs were actually within the range [0, 2^88 - 1].
An attacker can manipulate the values of these limbs and carefully choose values that overflow during subsequent computations.
This creates opportunities for cryptographic exploits and undermines the integrity of the protocol.
The root cause of this vulnerability — and similar ones — is missing range checks. Ensuring proper type and range validation is critical to maintaining the security and correctness of zero-knowledge circuits.
The basic idea of bug V-O1J-VUL-002 is that a mapping is being stored in a Merkle tree with more leaves than keys in the map.
keys are limited to 254 bits, so they lie within the range [0, 2**254). However, the Merkle tree has more index es than there are unique key s!
This means some index es in the tree must map to the same **key**.
In fact, it is straightforward to determine which indexes share the same key—there are trillions of possibilities.
Suppose a key is an address and a value indicates whether the address is blacklisted (true) or not (false).
A single key might correspond to two distinct indexes in the Merkle tree: At one index, the value stored is true. At another index, the value stored is false (an edge case overlooked by developers).
The attacker can choose which index to use, enabling them to exploit the system. Naturally, the attacker will select the index with the value advantageous to them.
To summarize, the core issue is that instead of a one-to-one relationship between keys and indexes, some keys correspond to multiple indexes. This allows attackers to exploit the ambiguity and choose the mapping that benefits them.
As shown in the above proof of concept, a user may prove that some entries of a MerkleMap are empty, even after they have been set. This can have critical consequences, such as user could prove their address is not in a blacklist, or that a certain nullifier is not in a Merkle tree.
We recommended a range-check on to prevent this overflow.
While this high-level overview omits some details, it captures the essence of the vulnerability. Full description of the bug can be found in the audit report, PDF page 15.
The core concept in the first bug (V-O1J-VUL-001, PDF page 15) revolves around multiscalar multiplication and a bug in a compress-select-decompressprocess. This bug would have enabled attackers to have control over the output of the multi-scalar computation.
In this blog post, we’re giving a simplified example of the bug for easier comprehension and readability. The actual bug specific to o1js can be studied in the audit report.
At a high level, multiscalar multiplication essentially means performing multiple multiplications and adding the results together. However, instead of multiplying numbers, in this context one is usually dealing with cryptographic constructs called elliptic curve points.
Multiscalar multiplication (MSM) is usually implemented as a specialized operation designed to make common calculations faster and more efficient.
Rather than describe the full context, this blog will focus on a particular step in the multi-scalar multiplication algorithm. At this step, o1js needs to choose between two possible values for an array. For details on how this fits into the larger MSM algorithm, see here. For the purposes of this blog, just know that if an attacker can control which of the two possible arrays is chosen, they can potentially alter the output of standard cryptographic algorithms like ECDSA.
ZK circuits lack traditional control flow structures like if-else statements. Instead, both options (e.g., left and right) must be computed, and the desired option is selected by multiplying it by 1, while the undesired option is multiplied by 0.
This approach can be inefficient, especially when the options involve large datasets. For example, if Option 1 sets 10 values (e.g., 1, 2, 3, ... 10) and Option 2 sets another 10 values (e.g., 11, 12, 13, ... 20), the circuit essentially computes both options in full and pays the computational cost for each, even though only one is used. This inefficiency is the problem o1js aims to optimize.
By compressing the data first, the circuit avoids the overhead of making the decision 10 times. Instead, it only makes the decision once, based on the compressed hashes.
The key vulnerability lies in the compress-select-decompress process. In ZK circuits, decompression must reliably produce the exact original values from the compressed hash. Any mismatch here could lead to attackers gaining control of the execution.
Compression functions typically handle data as 1s and 0s. These binary representations might correspond to scalars, field elements, or booleans, or other types.
If the same binary data (1s and 0s) can be decoded into multiple possible values, the decompression process might yield incorrect or unexpected results. This ambiguity creates a potential exploit.
In simple terms, the issue in the o1js code arises from an optimization routine that is not injective. Injective means that the decoding process can produce multiple valid solutions, leading to potential vulnerabilities.
Here’s how the original optimization process works:
[a0, a1] are compressed into a single hash: hash(encode([a0, a1])).[b0, b1] is compressed: hash(encode([b0, b1])).2. “Switch” step:
choice.3. Decompression:
assert(hash(encode([r0, r1])) == choice).4. Result Extraction:
[r0, r1] are obtained as the resulting values.The encoding process lacks the necessary property to guarantee that decoding and re-encoding will always yield the same result. In other words:
decode(encode([r0, r1])) == choice
does not consistently hold true. This means that the same encoded value can be decoded into multiple different outputs, introducing ambiguity.
Such behavior creates a vulnerability, as the system may fail to reliably match the original values after decompression. This non-injective encoding undermines the security and correctness of the algorithm.
Consider an array of length 2, where each value is a boolean (0 or 1). Possible values for the array are:[0, 0], [0, 1], [1, 0], or [1, 1]
We define the following:
[a0, a1][b0, b1]s, which can be 0 or 1[r0, r1]For each element in the result:
Compute r0 using:r0 := (1-s) * a0 + s * b0
Why does this work?
s = 0:r0 := (1 - 0) * a0 + 0 * b0 = a0s = 1:r0 := 0 * a0 + 1 * b0 = b0Similarly, compute r1 using:
r1 := (1-s) * a1 + s * b1This computation is sometimes called “switching” between a and b.
This approach works but can be slow, as it requires repeating the computation for each element. With an array of length two, this is not so bad. But as the arrays get longer and longer, the repeated computation can take a toll.
Instead of directly switching, the optimization uses an encoding function:
encode([a0, a1]) = a0 + 2 * a1encode([b0, b1]) = b0 + 2 * b1This encoding maps the arrays uniquely:
[0, 0] → 0 + 2 * 0 = 0[0, 1] → 0 + 2 * 1 = 2[1, 0] → 1 + 2 * 0 = 1[1, 1] → 1 + 2 * 1 = 3This seems fine so far, as the mappings are unique.
cA := a0 + 2 * a1
cB := b0 + 2 * b1
s to choose between the compressed values:cR := (1-s) * cA + s * cB
r0, r1 from the witness generator and add a constraint to ensure the decompressed values match:r0 + 2 * r1 = cR
The issue lies in the decompression step. The witness generator is not constrained to produce valid boolean values (0 or 1) for r0 and r1.
This means that instead of [r0, r1] being restricted to [0, 1], they can take arbitrary values.
An attacker could choose r0 = 1,000,000 and r1 = -500,000. Let’s compute the compressed value:
cR = r0 + 2 * r1 = 1,000,000 + 2 * (-500,000) = 1,000,000 - 1,000,000 = 0
This satisfies the constraint r0 + 2 * r1 = cR, but clearly, the values [r0, r1] do not represent valid boolean values.
During compression, the original arrays [a0, a1] and [b0, b1] were constrained to boolean values (0 or 1).
However, during decompression, the original code failed to enforce these constraints, allowing the witness generator to produce invalid values.
This lack of constraints makes the encoding non-injective, meaning the decompressed values can correspond to multiple possible outputs, enabling attackers to exploit the system.
The vulnerability arises because the decompression step lacks proper constraints, breaking the injective property of the encoding-decoding process. To fix this, the system must enforce that r0 and r1 remain within their valid range (0 or 1) during decompression.
o1js has a very active set of contributors and is very interested in security. They have worked hard to bring ZK technology to TypeScript developers, and overcome a set of unique challenges in bringing smart contract logic to the Mina blockchain.
We found out it remarkably straightforward to prototype proof-of-concept applications using the TypeScript library. This ease of use extends to identifying under-constrained bugs and testing circuits in ways that developers might not have anticipated.
If you’re planning to develop dapps on the Mina blockchain using TypeScript, stay tuned — our upcoming blog post will provide insights and best practices for secure development, drawing from our auditing experience.
Download the full o1js security audit report here.
Author:
Ben Sepanski, Chief Security Officer at Veridise
Editor: Mikko Ikola, VP of Marketing
]]>EDIT Jan 3, 2022: Note that this process is only available for the Intellij IDEA Ultimate edition, not the Community edition.
Intellij has its own guides on these topics, check them out here:
Intellij IDEA is an incredibly powerful IDE. If you’re anything like me, it’s become an essential component of any Java program you write. However, compiling and running large applications on my laptop gets frustratingly slow. Since I have access to bigger and better machines, I want to compile and run on those remote servers. However, I need several things before this actually improves my workflow:
(1) and (3) can be achieving using deployment: setting up a remote clone of a project that Intellij syncs in the background. (2) can be achieved using remote debug. In the rest of the blog, I’ll show you how to set this up using an example project.
I’m running Intellij 2021.2.1. Any recent version of Intellij should work. You’ll need to first set up SSH for your remote server, which I’ll call remote. For instance, you should be able to successfully run
ssh remoteUserName@remote
I’ll assume that your SSH key is located in ~/.ssh/id_rsa. Password-based authentication is a similar process.
For this example, we’ll start by making a new Java project using the Maven build system.


Now we’re ready to set up for deployment!
First, open the deployment configuration by going to Tools > Deployment > Configuration

Click +, and add an SFTP server. Choose whatever name you want, it doesn’t matter.

If you already have an SSH configuration setup on Intellij for your desired server, go ahead and select it. Otherwise, let’s set one up! Click the three dots next to SSH configuration to get started:

Enter the host, your remote username, and select your authentication type. I’m going to assume you’re using a password-protected private key in ~/.ssh/id_rsa. Only change the port from 22 (or set the local port) if you know what you’re doing!
Once you’re done, press “Test Connection” to make sure it works.

You can set the “root” directory if you wish. This sets what Intellij perceives as the root directory of the remote server (not the root directory of your remote project, we’ll set that later). If you do set the root, just remember that the file mappings are relative to the root you set.
Once you’re done, press OK and make sure your remote is in bold on the left menu. If it is not, select it and press the check mark to make it the default configuration.

Finally, we need to set up the file mappings.
On your remote, pick some path where you want your remote to be stored. I’m going to use ~/intellijRemotes/<projectName>.
Create that directory.
myRemoteUserName@remote> mkdir ~/intellijRemotes/IntellijRemoteExample
Click on “Mappings”, and copy the path to the deployed project on your remote.

Press OK, and now you’re good to go! What exactly does that mean?
Look over some options by going to Tool > Deployment > Options

You can also exclude items by names/patterns at this menu. Another place you can exclude specific paths for specific remotes is by clicking Tools > Deployment > Configuration and selecting “Excluded Paths”.
Note that you can create multiple remotes by repeating this process! Intellij only automatically uploads changes to the default. All other uploads, downloads, and syncs have to be manual.
Intellij’s debugger is one of its most powerful features. There’s no reason you should lose that just because you want to run your program remotely.
First, we’re going to build a configuration that will help us connect to our remote application. Start by clicking “Add configuration.”

Click the + on the top left, and select “Remote JVM Debug”.

Name the configuration whatever you want. Enter the host name and whatever port you want to use to connect. If you have several maven projects/sub-projects, make sure to select the correct module classpath!
I usually use port 8000, but all that matters is that TCP connections can be made from your local IP address to your remote at that port. If you have issues, you can use this guide to figure out which ports are open.

Next, you’ll want to copy the “Command line arguments for remote JVM.” You’re going to need these arguments later.

Once you’re done, press “OK”.
Now, SSH into your remote, and run your application using the arguments you copied.
myUserName@localhost> ssh myRemoteUserName@remote
myRemoteUserName@Remote> java \
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8000 \
-jar myApp.jar
You should see this as output:
Listening for transport dt_socket at address: 8000
Now go back to the local Intellij instance and run the configuration you just created! It should connect and start debugging like normal.
Note that all terminal output from your application will appear on your remote terminal, not on the local Intellij terminal. However, the Debugger tab will work as usual.
I hope this tutorial was helpful for you! Before I let you go, I’d like to warn you of a couple pitfalls that I commonly ran into when I first started using this setup.
htop on the remote to check for and kill these processes.Tools > Deployment > Upload to ... (resp. Download, Sync) it will only Upload (resp. Download, Sync) the file which is currently open. To Upload (resp. Download, Sync) the entire project, you need to right-click on the directory from the Project tab.None of the technical ideas discussed in this blog are my own, they are summaries/explanations based on the referenced works.
Tdoday’s paper is Denali: A Goal-directed Superoptimizer. At the time of its publication (2002), it was one of the first superoptimizers: a code generator which seeks to find truly optimal code. This is a dramatically different approach from traditional compiler optimizations, and is usually specific to efficiency-critical straight-line kernels written at the assembly level.
Plenty of compilers are optimizing compilers. However, in the strictest sense of the word, they don’t really find an optimal translation. They just find one that, according to some heuristics, ought to improve upon a naive translation. Why? Finding optimal translations is, in general, undecidable. Even for simplified, decidable versions of the problem, it is prohibitively time consuming to insert into any mortal programmer’s build-run-debug development cycle.
However, sometimes it is worth the effort to find a truly optimal solution. To disambiguate between these two “optimization” procedures, we use the term superoptimization when we are seeking a “truly optimal” solution. Superoptimization is an offline procedure and typically targets straight-line sequences of machine code inside critical loops.
With a few simplifying assumptions, the shortest straight-line code is the fastest. Consequently, we seek the shortest program.
Alexia Massalin coined the term “superoptimization” in her 1987 paper Superoptimizer – A look at the Smallest Program. Massalin used a (pruned) exhaustive search to find the shortest implementation of various straight line computations in the 68000 instruction set. For instance, she found the shortest programs to compute the signum function, absolute value, max/min, and others. Her goal was to identify unintuitive idioms in these shortest programs so that performance engineers could use them in practice.
While Massalin’s technique was powerful, it did not scale well (the shortest programs were at most 13 instructions long in Massalin’s paper). Moreover, the output programs were not automatically verified to be equivalent to the input programs. They are instead highly likely to be equivalent, and must be verified by hand.
Granlund \& Kenner followed up on Massalin’s work in 1992 with the GNU Superoptimizer. They integrated a variation of Massalin’s superoptimizer into GCC to eliminate branching.
Until 2002, research in superoptimizers seemed to stall. Judging by citations during that period, most researchers considered Massalin’s work to fit inside the field of optimizing compilers. These researchers viewed superoptimization as a useful engineering tool, but of little theoretical interest or scalability. Rather, superoptimization was seen as an interesting application of brute-force search. Massalin and the GNU Superoptimizer seemed to become a token citation in the optimizing compiler literature.
Massalin’s superoptimizer relies on brute-force search: enumerate candidate programs until you find the desired program. Given the massive size of any modern instruction set, this does not scale well. However, since we want the shortest program, we have to rely on some kind of brute-force search. Denali’s insight is that Massalin’s search algorithm was enumerating all candidate programs, instead of only enumerating relevant candidate programs.
Denali users specify their desired program as a set of (memory location, expression to evaluate) pairs. For instance, (%rdi, 2 * %rdi) is the program which doubles the value of %rdi.
Denali’s algorithm only “enumerates” candidate programs which it can prove are equivalent to the desired program. For efficiency, it stores this enumeration in a compact graph structure called an E-Graph, then searches the E-Graph using a SAT solver.
An E-Graph is used to represent expressions. For instance, a literal 4 or a register value %rdi is represented as a node with no children.
The expression %rdi * 4 is represented as a node ‘*’ whose first child represents %rdi and whose second child represents 4.
Bigger expressions are represented just like you would think. For instance, the expression %rdi * 4 + 1 would be represented as
So far, this just looks like an Abstract Syntax Tree. E-Graphs are distinguished from ASTs by the ability to represent multiple equivalent expressions. Suppose we wish to add the equivalence 4=2**2 to our E-graph. We do this by adding a special equivalence edge
Since there is no machine exponentiation instruction, this does not look useful at first. However, now we can add a further equivalence edge based on the fact that %rdi « 2 = %rdi * 2**2 = %rdi * * 4.
Since E-Graphs represent A=B by keeping both A and B around, they can become quite massive.
We can use proof rules to repeatedly grow the E-Graph and/or add equivalence edges. If we keep applying our proof rules until our graph stops changing, then we’ve deduced all the ways we can provably compute our expression (relative to our proof rules). For instance, in the previous example we had only three proof rules:
If we add more proof rules, we may be able to deduce faster ways to compute our expression.
An early variant of E-Graphs is described in Greg Nelson’s (one of the Denali authors) Ph.D. Thesis. These were used by Nelson in the automated theorem prover Simplify for equational reasoning. Since then, search over E-graphs via the congruence closure algorithm is used by many modern SMT solveres for reasoning about equality of uninterpreted functions (it is even taught in Dr. Isil Dillig’s CS 389L course here at UT!). For example, the Z3 SMT solver implements an E-graph, and the CVC4 Solver implements an incremental congruence closure.
Nodes in an E-Graph that are connected by an equivalence edge represent expressions that are equivalent according to the proof rules. Therefore, we only need to evaluate one of the nodes. Denali can use a SAT solver to figure out the optimal choice of nodes. Their encoding is not too complicated.
The basic idea of the encoding is as follows:
For each machine instruction node T,
L(i, T) = { 1 T starts executing on cycle i
{ 0 otherwise
Then, all we have to do is add constraints so that
Now we can find the shortest program encoded in our E-Graph by constraining the SAT solver to look for a program of length 1, then length 2, then length 3, …. until we find a solution.
The Denali paper presents several preliminary results. For the Alpha instruction set architecture, they are able to generate some programs of length up to 31 instructions. For comparison, the GNU superoptimizer is unable to generate (near) optimal instructions sequences of length greater than 5.
However, in addition to Denali’s built-in architectural axioms, the programmers specify program-specific axioms in their examples. This trades off automation for the ability to generate longer (near) optimal instruction sequences.
Denali demonstrated that, for small programs, it is possible to generate provably equivalent, (nearly) optimal code. Since then, there has been a lot of interest in superoptimization. Here are some projects/papers that have popped up since Denali.
However, while there is active industry and research interest in the problem that Denali presented (finding a provably equivalent, near-optimal translation), most modern approaches (e.g. souper) rely on SMT-based synthesis techniques. Denali’s methods of superoptimization seem to have largely fallen by the wayside. Part of this is because Denali’s provably (almost) optimal program relies on a set of user-specified axioms, and is only optimal with respect to those axioms. Part of the appeal of an SMT solver is standardized theories for certain objects and operations.
Both enumerative search (e.g. STOKE) and goal-directed search (e.g. souper) are used today. In addition, Denali’s general notion of specification (a set of symbolic input-output pairs) is still used, with various project-specific modifications. Projects still rely on (often hand-written) heuristics to measure the cost/cycle-count of candidate programs.
All graphs were built using graphviz. The example E-Graph in section What is an E-Graph? is based on the example in today’s paper.
]]>