I registered bitwrap.io in 2014 because of OP_RETURN.
Bitcoin had just expanded OP_RETURN from 40 to 80 bytes — enough to embed a hash, a schema pointer, a fingerprint of off-chain state. The idea that caught me wasn't the 80 bytes themselves but what they implied: you could anchor structured computation to a chain without bloating it. Put the proof on-chain, keep the witness off-chain.
Two years later SegWit made the same insight explicit at the protocol level. Segregated Witness literally separated transaction data from witness data — the signature proving you're authorized from the transaction describing what you want to do. The block sees the commitment; the witness lives elsewhere.
That separation — commitment on-chain, witness off-chain — is exactly what a ZK proof does. The verifier checks a succinct proof. The prover holds the private witness. The chain never sees the state.
The question I couldn't answer in 2014 was: what structure should the witness have? A hash of what, exactly? The data model was missing. You could embed 80 bytes in OP_RETURN but you had no formal language for what those bytes meant.
Petri nets turned out to be the answer.
A Petri net state — a marking — is a vector of integers: how many tokens sit on each place. A transition fires by consuming tokens from input places and producing tokens on output places. The rules are structural. The arcs are the specification.
This maps directly to the ZK witness pattern:
The circuit doesn't know it's verifying a token transfer, or a game move, or a workflow step. It knows places, transitions, and arcs. Change the topology constants and you get ZK proofs for a completely different application.
We covered the circuit mechanics in Zero-Knowledge Proofs for Petri Nets. Bitwrap is where that theory becomes a tool.
Bitwrap treats a Petri net model as a single source of truth that compiles to three artifacts:
1. ZK Proof (Groth16). Guards become arithmetic constraints. State roots use MiMC-BN254 Merkle trees. The gnark prover generates Groth16 proofs on the BN254 curve — Ethereum-compatible, verifiable on-chain. Six circuits cover ERC-20 operations: transfer, transferFrom, approve, mint, burn, and vesting claims.
2. Solidity Contract. States become storage variables. Guards become require() statements. Arcs become storage updates. Events become Solidity events. A complete Foundry test harness and genesis script are generated alongside the contract. The output is deployable, not a sketch.
3. .btw DSL. A compact textual syntax for the same models:
schema ERC20 {
version "1.0.0"
register ASSETS.AVAILABLE map[address]uint256 observable
fn(transfer) {
var from address
var to address
var amount amount
require(ASSETS.AVAILABLE[from] >= amount)
ASSETS.AVAILABLE[from] -|amount|> transfer
transfer -|amount|> ASSETS.AVAILABLE[to]
}
}
The -|amount|> arc syntax reads like a Petri net: consume amount tokens from ASSETS.AVAILABLE[from], produce amount tokens at ASSETS.AVAILABLE[to]. The DSL compiles to the same JSON-LD schema that the visual editor produces.
There is no translation layer between these three outputs. They share the same metamodel — the same states, actions, arcs, and guards. A bug in the Solidity contract means a bug in the model, which means the ZK circuit would also reject it. They can't diverge because they're the same structure rendered three ways.
The ERC templates are where the abstraction proves itself. Every major token standard maps to a Petri net:
ERC-20 (fungible tokens): three states (totalSupply, balances, allowances), five actions (transfer, approve, transferFrom, mint, burn). The guard balances[from] >= amount is both a ZK constraint and a require() statement.
ERC-721 (NFTs): token ownership as place markings, approval as a separate state, safe transfer checks as guards.
ERC-1155 (multi-token): batch operations as parallel transitions, per-token-id balances as parameterized places.
ERC-4626 (tokenized vaults): deposit/withdraw/redeem with share conversion — the exchange rate is a function of the marking.
ERC-5725 (vesting NFTs): temporal claims gated by block timestamps, revocation as an inhibitor arc.
These aren't approximations. The bitwrap ERC-20 template produces a contract that handles transfer, approve, transferFrom, mint, and burn with correct event emission, access control, and overflow protection — directly from the Petri net structure.
Every model saved through bitwrap gets a CID — a content identifier computed from JSON-LD canonicalization (URDNA2015), SHA2-256 multihash, CIDv1 encoding. Same model always produces the same CID. Models are immutable.
This closes the loop from OP_RETURN. In 2014 we wanted to anchor structured state to a chain via a hash. Now we have:
The 80-byte OP_RETURN hash was pointing at something we hadn't built yet. The CID of a Petri net model is what it was pointing at.
The witness — your actual balances, your position in the Merkle tree — never leaves your browser. Bitwrap publishes JavaScript modules (MiMC hash, Merkle tree, witness builder) that run client-side. The prover backend receives the witness and returns a proof, but a future version could run the entire prover in WASM (the 22MB prover.wasm binary already exists).
This is the SegWit principle applied to application state: the witness is yours. The chain sees only the proof.
Bitwrap sits at the top of the pflow ecosystem:
cell, func, arrow, guard)The capstone isn't the ZK prover or the Solidity generator in isolation. It's that one visual model flows through the entire stack without impedance mismatch. Draw a net, prove a transition, deploy a contract. Same arcs, same guards, same semantics.
Bitwrap is live at bitwrap.io. The editor, prover, and Solidity generator are all available. The Remix plugin integrates directly with the Solidity IDE for contract deployment.
The source is at github.com/stackdump/bitwrap-io.
What started as an 80-byte hash in a Bitcoin transaction became a complete pipeline: model, prove, deploy. The detour through Petri nets wasn't a detour — it was finding the right witness structure. The net topology is the specification. The proof is the 80 bytes we always wanted to put on-chain.