Tradeoffs Discussion: Access pattern hiding / Spicy Printfs

Hi Secret Community,
One more tradeoffs discussion post for today.

We have coordinated a vulnerability disclosure affecting SNIP-20 tokens. (Secret’s blog.)
Our research paper is here Some mitigations against replay attacks are already present.

Even after these mitigations, the SNIP-20 transactions do not provide sender-receiver privacy. This is the “spicy prints” problem, as illustrated by the following:

Right now, the storage access pattern leaked to the untrusted operating system is very fine grained, leaking the exact record being accessed. This means that SNIP-20 tokens today do not provide any receiver privacy - transfers are completely traceable from account to account.

One approach to hiding the access pattern is to insert an (optional) Oblivious RAM (ORAM) algorithm in between the trusted and untrusted codebases, which can be accessed by the contract developer through additional opcodes. This could increase the compute cost and disk I/O bandwidth (which is a bottleneck resource) for large arrays like SNIP-20 account accesses. The overhead may be too big to justify, and it may take significant development effort to get there given how Secret and Cosmos currently work. As an alternative to platform-level access pattern hiding, lightweight “obfuscation” (similar to Monero) could be implemented by SNIP-20 developers in the existing contract system. This wouldn’t require platform level upgrades and may have less performance overhead, but would also leak statistical information.


A few things to mention - this is a really cool (and important) problem to solve. It’s unrelated to the use of TEEs, and in general it’s a big and interesting problem to resolve.

The immediate reference implementation that we’ll post in a week or so will take the second mitigation approach (Monero-like), which uses decoys to obfuscate. I agree (and we all do) that an ORAM-like solution is indeed cooler and more useful, but would indeed take more time and effort. There are partial solutions as well (e.g., bucketing addresses together), but all of these have challenges.

All in all this is an active research area. We’ll also give worthy grants to anyone who wishes to work on this. We can provide some guidance.


Before mainnet we planned to use MRENCLAVE for everything, but then we realized that upgrades cannot happen in a trivial manner that way, and that implementing a seed handover mechanics can brick the network if not perfectly implemented.

I’d say that using MRENCLAVE is the ultimate goal here, but I’m not sure how urgent this is at this stage.

Would it be fair to say that the longer you leave the transition to MRENCLAVE and as Secret Network gets more complex with other improvements/enhancements, the change to MRENCLAVE at a later date would become even more difficult?

No, and I’d say it’s becoming easier with the light client feature that we’ve added thanks to Andrew’s research.


Several points warrant consideration regarding the described privacy leaks:

  1. It is important to note that these leaks are not limited to SNIP contracts; they also affect applications built on the network more broadly.
  2. Audit results for dApps on the Secret Network have not addressed these issues, suggesting that many crypto-focused audit firms may not have the expertise to delve into such matters.
  3. Enhancing guidance for application developers is crucial to ensure they adequately consider the implications of their design choices and take appropriate measures to mitigate privacy risks.

To be fair, dapp auditors are not looking at the network code, only the contract code, so catching this leak could be considered out of scope.

Also, afaik neither the SNIP20 contract code nor the network code has been professionally audited.


@darwinzer0 what do you think about point 1 and 3?

Absolutely I agree, those are definitely important points to consider!

1 Like

So is it fair to say that this should be max priority to address and resolve as soon as possible?

Otherwise the whole value prop of Secret Network is kind of misleading and technically has never been true.

1 Like

For those interested in discussing the decoy mechanism being implemented in SNIP-25, here’s the current PR:

I have concerns about this fix:
copying from my msg to andrew miller:

Leor Fishman, [3/21/2023 10:22 AM]
because it looks like how it works is each addr is assigned 2 decoys

Leor Fishman, [3/21/2023 10:22 AM]
but that has 2 issues

Leor Fishman, [3/21/2023 10:23 AM]
either the decoys change or they don’t change

Leor Fishman, [3/21/2023 10:23 AM]
if they change, then any time someone is sent 2 txns in the same or nearby blocks, you can trivially reidentify via set intersection

Leor Fishman, [3/21/2023 10:23 AM]
if they don’t change, then you can learn the 3-1 key mapping instead of the 1-1 and spicy printf still works

EDIT: The above was based on my misunderstanding --decoys are user-generated not protocol generated, which means it’s up to the user to securely/reliably generate decoy addrs for snip20 sends (if the wallet generates them randomly, then the set intersect problem appears – probably the wallet needs to have some mechanism for pseudorandom generation of only plausible/popular addresses?)

1 Like

Upon further looking into it it looks like the decoys are user-selected?

1 Like

which means that the security here is dependent on how the user decoys are generated

1 Like

Here’s a concept for an alternative approach to SNIP-20 tokens that works by using off-chain compute based on queries. The main idea comes from Gabe, who’s been suggesting to break up potentially very large computations into off chain computations based on queries. Even though queries cannot directly modify the on-chain state, queries can produce output that is authenticated using a MAC or signature. A compute worker (any validator node) can submit the output back to the contract, which checks the MAC before applying the output on-chain. In this case, the computation is a linear scan (naive oram) over the entire array of account balances, and the query output consists of two things: 1) the transaction response (like withdrawals/side effects), which must be submitted back on chain, and 2) the re-encrypted array of account balances, which can be fed into the next query but does not have to be re-stored on chain. Illustration below then more notes and pseudocode.

  • The main drawback is that because this is carried out by off-chain computation, a single transfer takes two transactions, which will typically be separated by 1 or more blocks.

  • The off-chain Queries are not automatically carried out by ordinary SN validator nodes. However, any validator node is capable of carrying it out if they choose to. Given the on-chain contract state, i.e. a checkpoint and a sequence of requests, the sequence of queries and resulting encrypted/MAC’d intermediate states are deterministic.

  • This clearly has a lot in common with L2 rollups/sidechains etc., but it’s not clear the best analogy. Since the off-chain computation is deterministic, this is most similar to off-chain compute systems like Truebit. The use of enclave-based MAC substitutes for a “SNARK”.

  • This is NOT Aepic-safe! The use of a MAC this way could turn a privacy failure into an integrity failure (knowledge of sk would enable fake withdraws). This could be mitigated by adding a Merkle tree proof that the contract checks for each transaction…. Still a gas cost savings relative to running an ORAM client in the contract.

  • This is just a simple example of a token, since it doesn’t aim to provide transaction histories or other features, but the same concept would work for those.

Contract state:
	- checkpoint [ (addr,amt),... ]	// Just balances
	- checkpoint_seqno
	- requests: [ (“xfer”,from,to,amt),.... (“deposit”,addr,amt), (“withdraw”,addr,amt)]
	- uint current_seqno; // This keeps track of the most recent request whose *side effect* (if any) has already been applied.
	- bytes32 sk // Secret key used to encrypt and authenticate balances & to mac the public side effects

	// The “side effect” of deposit occurs when the request is scheduled, unlike withdraw where it is applied when it is finalized
handle_withdraw(addr,amt): requests.append((“widthraw”,addr,amt))
handle_xfer(from,amt,to):wes requests.append();

handle_finalizetx(resp, resp_mac):
        // Verifies the response for the next transaction
	verify_mac(sk, resp_mac, current_seqno+1 || resp) 

       // Only withdrawal responses have a side effect
       If (“withdraw”,addr,amt)) = resp:

        // Update the most recently finalized tx
        current_seqno += 1

	// Produces an auth’encd copy of the current checkpoint
        return auth_enc(sk, checkpoint_seqno || checkpoint)
query_process(encst_inp, &encst_out, &resp, &resp_mac):
	// encst_out will include a re-encrypted version of the balances table.
	//    This could be written back to the checkpoint, but it is not needed
	// resp/resp_mac can be posted to the blockchain to finalize the tx and carry out side effects, in this case just withdrawals

	// Decrypt and authenticate
	(seqno, balances) := auth_decrypt(sk, encst_inp)

	// Get the next command to process
	cmd := requests[seqno]
	if cmd = (“xfer”,from,to,amt):
		// Linear scan to read.
		for (addr,bal) in balances: if addr==from: snd_bal := bal
		// If the transaction fails, we still need to update the state
                If (snd_bal < amt) {
  	             Balances’ := balances
                 } else {
		        // Write back
	         	Balances’ := []
 		        for (addr,bal) in balances:
			   If addr == from: balances’.append(addr,bal-amt)
			   If addr == to: balances’.append(addr,bal+amt)
			    Else: balances’.append(addr,bal)
         	encst_out := auth_enc(sk, seqno+1 || balances’)
		effect_out := ””  // no public side effect for tranfers
		effect_mac := mac(sk, seqno+1 || effect_out)

	If cmd = (“withdraw”,addr,amt):
		// Linear scan to read 
		bal = none
		for (addr’,bal’) in balances: if addr==addr’: bal := bal’

		If (bal < amt) {
			Balances’ := balances
			effect_out := mac(seqno+1,””)
		} else {
	        	// Write back
		        Balances’ := []
		        For (addr’,bal) in balances:
		         	If addr’ == addr: balances’.append(addr,bal-amt)
                                Else: balances’.append(addr’,bal)  
		        Encst_out := auth_enc(sk, seqno+1 || balances’)
		        Effect_out := (“withdraw”,addr,amt)
		        Effect_mac := mac(sk, seqno+1|| effect_out)

	// Deposit omitted but simpler than withdraw

query_accountbalance(addr, query_permit, seqno, encst):
	// Linear scan to let a user fetch their account balance.
	// Should check that the encrypted state matches the sequence number the user specifies

edit: here’s a gpt4 discussion that expands on the linear scan concept