Fluxe: A Universal Privacy Protocol for Cross-Chain Stablecoin Payments with Programmable Compliance
Abstract
We present Fluxe, a universal privacy protocol enabling private stablecoin transfers across multiple blockchain networks with regulatory compliance. The protocol combines hybrid proof architectures (client-side Groth16, server-side SP1), confidential UTXO models with indexed merkle trees, and zk-promises for asynchronous compliance callbacks. The system supports cross-chain private payments with automatic liquidity balancing and programmable payment rails.
1. Introduction
Current privacy protocols face the trilemma of privacy, compliance, and cross-chain interoperability. Fluxe solves this through zk-promises integration enabling asynchronous compliance determination without compromising transaction privacy.
2. Technical Architecture
2.1 Confidential UTXO Model
Each note NN is defined as:
N = \{asset\_type, value, owner, \psi, chain\_hint, compliance\_data, lineage\_hash, pool\_id, callbacks\}N={asset_type,value,owner,ψ,chain_hint,compliance_data,lineage_hash,pool_id,callbacks}
The note commitment is computed as:
cm = H(asset\_type \parallel value \parallel owner \parallel \psi \parallel chain\_hint \parallel compliance\_data \parallel lineage\_hash \parallel pool\_id \parallel H(callbacks))cm=H(asset_type∥value∥owner∥ψ∥chain_hint∥compliance_data∥lineage_hash∥pool_id∥H(callbacks))
The nullifier derivation prevents double-spending:
nf = H(auth\_secret \parallel \psi \parallel cm)nf=H(auth_secret∥ψ∥cm)
where auth\_secretauth_secret is derived from the owner’s private key and \psiψ is per-note entropy.
2.2 Indexed Merkle Trees
Traditional sparse merkle trees require d = 256d=256 hash operations for membership proofs. Fluxe uses indexed merkle trees with sorted linked-list structure reducing this to d = 64d=64 operations.
Each leaf node stores:
leaf = \{value, next\_index, next\_value\}leaf={value,next_index,next_value}
Non-membership Proof Implementation:
Using indexed Merkle trees, non-membership is proven by finding the low nullifier (the sanctioned address with the largest value less than the target address):
\text{NonMembershipProof}(addr, root, proof) =NonMembershipProof(addr,root,proof)=
\begin{cases}\text{MerkleProof}(low\_addr, root, path) \land \\low\_addr.value < addr \land \\(addr < low\_addr.next\_value \lor low\_addr.next\_value == 0)\end{cases}⎧⎨⎩MerkleProof(low_addr,root,path)∧low_addr.value<addr∧(addr<low_addr.next_value∨low_addr.next_value==0)
where low\_addr = \{value, next\_index, next\_value\}low_addr={value,next_index,next_value} is the sanctioned address entry that the target address would fall after in the sorted list.
The proof consists of:
- Merkle path for node v_ivi (64 hashes)
- Verification that v_i.next\_value = v_{i+1}vi.next_value=vi+1
- Range check: v_i < v < v_{i+1}vi<v<vi+1
2.3 Hybrid Proof System
Client-side circuits (Groth16):
Deposit Circuit:
Public: deposit_amount, asset_type, tx_hash, cm_outPrivate: owner_secret, ψConstraints:- Verify deposit transaction validity- cm_out = H(asset_type || deposit_amount || owner || ψ || ...)Transfer Circuit:
Public: nf_in, cm_out, merkle_rootPrivate: note_in, auth_secret, note_outConstraints: - nf_in = H(auth_secret || note_in.ψ || note_in.cm) - MerkleProof(note_in.cm, merkle_root) - value_in ≥ value_out + fee - cm_out = H(note_out)Withdrawal Circuit:
Public: nf, amount, dest_chain, recipientPrivate: note, auth_secretConstraints:- nf = H(auth_secret || note.ψ || note.cm)- amount ≤ note.value- dest_chain ∈ supported_chainsServer-side verification (SP1):
SP1 verifies client Groth16 proofs using the groth16_bn254 precompile and performs batch state updates:
fn verify_and_update_state(old_root: [u8; 32],groth16_proofs: Vec<Groth16Proof>,public_inputs: Vec<PublicInputs>) -> [u8; 32] {for (proof, inputs) in groth16_proofs.zip(public_inputs) {groth16::verify(proof, inputs);}let new_nullifiers = extract_nullifiers(public_inputs);indexed_merkle_tree::batch_insert(old_root, new_nullifiers)}2.4 Basic Client-Side Sanctions Screening
For basic compliance, the most fundamental requirement is proving that transaction addresses are not sanctioned. This is implemented through client-side zero-knowledge proofs against a commitment to flagged addresses.
Sanctions List Commitment:
The protocol maintains a Merkle tree commitment to sanctioned addresses:
sanctions\_root = \text{MerkleRoot}(\{addr_1, addr_2, ..., addr_n\})sanctions_root=MerkleRoot({addr1,addr2,...,addrn})
where each addr_iaddri is a sanctioned address hash.
Basic Sanctions Screening Circuit:
Public: sanctions_root, tx_validPrivate: sender_addr, recipient_addr, sanctions_proof_sender, sanctions_proof_recipientConstraints:- NonMembershipProof(sender_addr, sanctions_root, sanctions_proof_sender)- NonMembershipProof(recipient_addr, sanctions_root, sanctions_proof_recipient)- tx_valid = (sender_proof_valid ∧ recipient_proof_valid)NOTE: While basic sanctions screening happens client-side, the system’s true power lies in its ability to enforce compliance retroactively through callbacks. See Section 3.10 for how assets can be frozen post-transaction if compliance issues are discovered.
2.5 Cross-Chain Bridge Protocol
Bridge contracts maintain the invariant:
\sum_{i} deposits_i = \sum_{j} withdrawals_j + \sum_{k} liquidity\_k∑idepositsi=∑jwithdrawalsj+∑kliquidity_k
For cross-chain operations, the protocol coordinates:
- Source chain withdrawal: Burn note, emit event with proof
- Message passing: CCTP (for USDC) or LayerZero (for others)
- Destination chain deposit: Mint equivalent note after verification
The liquidity routing algorithm selects optimal paths based on:
cost(path) = gas\_cost + bridge\_fee + time\_penalty \cdot delaycost(path)=gas_cost+bridge_fee+time_penalty⋅delay
This is done through external keepers that help pay gas and perform this actions on observed events, although the balancing functions are permissionless and can be called by anybody.
2.6 Integration with Twine Multi-Settlement Network
Fluxe operates as an application layer on top of Twine’s multi-settlement infrastructure, leveraging its light client architecture for cross-chain functionality:
- Light Client Verification: Twine maintains light clients for each supported chain to verify state transitions
- Cross-Chain Proofs: State updates are verified through light client proofs before acceptance on remote chains
- Asynchronous Settlement: Each chain processes Fluxe state updates upon light client verification
- Trust-Minimized Bridges: Asset transfers verified through light client state proofs
This architecture allows Fluxe to maintain privacy while achieving cross-chain interoperability through cryptographic verification rather than consensus.
┌────────────────────────────────────────────────────────┐│ TWINE ││ ││ ┌──────────────────────────────────────────────────┐ ││ │ FLUXE │ ││ │ │ ││ │ • Nullifier Tree (IMT) │ ││ │ • Commitment Tree │ ││ │ • Transfer Proofs (Groth16) │ ││ │ • Compliance Callbacks │ ││ │ • SP1 Verification │ ││ └──────────────────────────────────────────────────┘ ││ ↑ ││ Light Clients │└─────────────────────────┼──────────────────────────────┘↓┌──────────┐ ┌──────────┐│ Ethereum │ │ Solana ││ Bridge │ │ Bridge │└──────────┘ └──────────┘(Deposit/ (Deposit/Withdraw) Withdraw)3. zk-Promises Integration for Asynchronous Compliance
3.1 zk-Objects Foundation
Fluxe extends the zk-objects model where each user maintains a compliance object containing state and a unique nullifier for updates. The object structure:
obj = \{state, serial, cbList\}obj={state,serial,cbList}
where statestate contains compliance information, serialserial is a unique identifier preventing replay attacks, and cbListcbList maintains pending callbacks.
The object commitment:
cm_{obj} = \text{Commit}(state \parallel serial \parallel H(cbList), r_{obj})cmobj=Commit(state∥serial∥H(cbList),robj)
Object updates follow copy-on-write semantics: creating new commitments with fresh nullifiers while proving valid state transitions.
3.2 Dual Bulletin Board Architecture
The system maintains two global bulletin boards:
Object Bulletin Board (bb_{obj}bbobj): Stores object commitments using Merkle trees for membership proofs. The tree root rt_{obj}rtobj represents all valid object states.
Callback Bulletin Board (bb_{cb}bbcb): Stores callback invocations requiring both membership and non-membership proofs. Uses complement set approach partitioning unused ticket space into ranges.
Non-membership proofs implemented through signed range sets:
\text{NonMember}(ticket) \iff \exists (a,b) \in \text{RangeSet}: a < ticket < bNonMember(ticket)⟺∃(a,b)∈RangeSet:a<ticket<b
3.3 Callback Creation Protocol
Users create compliance callbacks during transaction execution through the \text{ExecMethodAndCreateCallback}ExecMethodAndCreateCallback algorithm:
Step 1: Generate rerandomized ticket from compliance provider’s public key:
ticket = pk_{compliance} \cdot r^{sk_{user}} \mod pticket=pkcompliance⋅rskusermodp
where rr is cryptographically random and sk_{user}skuser is the user’s signing key.
Step 2: Create callback entry with expiration and encryption:
cb_{entry} = \{ticket, exp\_time, enc\_key, method\_id\}cbentry={ticket,exp_time,enc_key,method_id}
where enc\_key = \text{KDF}(user\_secret, ticket)enc_key=KDF(user_secret,ticket) for per-callback encryption.
Step 3: Update object with new callback list:
obj'.cbList = obj.cbList \parallel cb_{entry}obj′.cbList=obj.cbList∥cbentry
obj'.serial = \text{Fresh}()obj′.serial=Fresh()
Step 4: Generate ZK proof of valid transition:
Public: cm_old, cm_new, method_idPrivate: obj_old, obj_new, r_old, r_new, cb_entryConstraints:- cm_old = Commit(obj_old, r_old)- cm_new = Commit(obj_new, r_new)- obj_new.cbList = obj_old.cbList || cb_entry- obj_new.state = ValidTransition(obj_old.state, method_id)3.4 Callback Invocation and Processing
Compliance providers invoke callbacks by posting to bb_{cb}bbcb:
invocation = \{ticket, \text{Enc}_{enc\_key}(args), timestamp, \sigma\}invocation={ticket,Encenc_key(args),timestamp,σ}
where \sigmaσ is a signature over (ticket, args, timestamp)(ticket,args,timestamp) using the ticket as public key.
Users process callbacks through \text{ScanOne}ScanOne algorithm:
Step 1: Iterate through pending callbacks in cbListcbList
Step 2: Check membership in bb_{cb}bbcb:
\exists invocation \in bb_{cb}: invocation.ticket = cb_{entry}.ticket∃invocation∈bbcb:invocation.ticket=cbentry.ticket
Step 3: If found and valid:
- Decrypt arguments: args = \text{Dec}_{enc\_key}(invocation.args)args=Decenc_key(invocation.args)
- Execute method: state' = method(state, args)state′=method(state,args)
- Remove from callback list: cbList' = cbList \setminus \{cb_{entry}\}cbList′=cbList∖{cbentry}
Step 4: Generate proof of correct processing:
Public: cm_old, cm_new, timestampPrivate: obj_old, obj_new, args, cb_entryConstraints:- ValidDecryption(cb_entry.enc_key, invocation.args, args)- obj_new.state = ExecuteMethod(obj_old.state, args)- obj_new.cbList = obj_old.cbList \ {cb_entry}- timestamp < cb_entry.exp_time3.5 Security Properties and Unlinkability
Unlinking Creation and Invocation: The rerandomization scheme ensures:
\Pr[\text{Link}(ticket_{create}, ticket_{invoke}) = 1] \leq \text{negl}(\lambda)Pr[Link(ticketcreate,ticketinvoke)=1]≤negl(λ)
without knowledge of sk_{user}skuser or rr.
Authenticity via Signature-based Tickets: Tickets function as verification keys in signature scheme:
\text{Verify}(ticket, (args, timestamp), \sigma) = 1 \iff \text{valid invocation}Verify(ticket,(args,timestamp),σ)=1⟺valid invocation
Confidentiality: Per-callback encryption prevents unauthorized access:
\text{Enc}_{enc\_key}(args) \text{ computationally indistinguishable from random}Encenc_key(args) computationally indistinguishable from random
3.6 Compliance State Machine Implementation
Enhanced compliance state for payment protocols:
S = \{S={
\quad compliance\_level \in \{0,1,2,3\},compliance_level∈{0,1,2,3},
\quad last\_review\_time \in \mathbb{N},last_review_time∈N,
\quad pending\_callbacks: \text{List}[CallbackEntry],pending_callbacks:List[CallbackEntry],
\quad risk\_score \in [0, 2^{32}),risk_score∈[0,232),
\quad jurisdiction\_flags: \text{BitSet},jurisdiction_flags:BitSet,
\quad transaction\_limits: \{daily, monthly, yearly\},transaction_limits:{daily,monthly,yearly},
\quad reputation\_vector: \mathbb{R}^nreputation_vector:Rn
\}}
State Transition Functions:
\text{CreateTxCallback}(S, tx\_data) \rightarrow S'CreateTxCallback(S,tx_data)→S′:
- Analyze transaction risk: risk = \text{RiskScore}(tx\_data, S.reputation\_vector)risk=RiskScore(tx_data,S.reputation_vector)
- Generate callbacks based on risk thresholds
- Update pending list: S'.pending = S.pending \cup \{\text{new callbacks}\}S′.pending=S.pending∪{new callbacks}
\text{ProcessCallback}(S, result) \rightarrow S'ProcessCallback(S,result)→S′:
- Update compliance level: S'.level = f(S.level, result.determination)S′.level=f(S.level,result.determination)
- Adjust risk score: S'.risk\_score = \text{UpdateRisk}(S.risk\_score, result)S′.risk_score=UpdateRisk(S.risk_score,result)
- Modify limits if necessary: S'.limits = \text{AdjustLimits}(S.limits, result)S′.limits=AdjustLimits(S.limits





