Submitting transactions anonymously
audience: integrators
Submission is entirely the zipnet organism’s job. The lattice
consumes zipnet unchanged; the zipnet developer book
is the authoritative reference. This page covers only the bits
that are specific to submitting into a lattice rather than a
standalone zipnet deployment.
Which Config you pass
Integrators pass the lattice’s zipnet config (ETH_MAINNET.zipnet),
not a standalone zipnet config:
use zipnet::Zipnet;
let submitter = Zipnet::<Tx2718>::submit(&network, Ð_MAINNET.zipnet).await?;
The lattice’s zipnet config is content + intent addressed under
the lattice’s instance name (e.g. "ethereum.mainnet"). Two
lattices sharing a zipnet committee is not a supported pattern;
each lattice derives its own zipnet GroupId from its own root.
Picking a datum type
Every EVM lattice’s zipnet organism shuffles a chain-native
sealed envelope type. The reference datum is Tx2718 for EIP-
2718 encoded transactions, sealed to the lattice’s unseal
committee public key:
use zipnet::{DecodeError, ShuffleDatum, UniqueId, unique_id};
pub struct Tx2718(pub [u8; 1024]);
impl ShuffleDatum for Tx2718 {
const TYPE_TAG: UniqueId = unique_id!("builder.tx2718-v1");
const WIRE_SIZE: usize = 1024;
fn encode(&self) -> Vec<u8> { self.0.to_vec() }
fn decode(bytes: &[u8]) -> Result<Self, DecodeError> {
<[u8; 1024]>::try_from(bytes).map(Self).map_err(|e| DecodeError(e.to_string()))
}
}
TYPE_TAG folds into the zipnet instance’s fingerprint, so a
lattice that ships a Tx2718 v2 with a different size is a
different zipnet GroupId and is not cross-compatible with the
v1 lattice. This is by design; see the zipnet wire
invariants.
Sealing the payload to unseal
Zipnet’s DC-net construction provides sender anonymity; it does
not by itself provide payload confidentiality against the
zipnet committee. The lattice achieves payload confidentiality
by sealing the payload to the unseal committee’s threshold
public key before writing it into the Tx2718 buffer.
Pseudocode:
let ct = unseal::seal(Ð_MAINNET.unseal, &tx_rlp_bytes)?;
let padded = pad_to(ct, 1024);
submitter.send(Tx2718(padded)).await?;
unseal::seal is a pure function parameterised on the lattice’s
unseal config; it encrypts the payload to the committee’s
published threshold public key, producing a ciphertext of
deterministic size (plaintext + AEAD overhead). pad_to
right-pads to Tx2718::WIRE_SIZE. Zipnet rejects non-exact sizes.
Bytes you never put into the Tx2718 buffer:
- Your wallet address in cleartext.
- Any metadata linking your submission to prior on-chain activity.
- Unpadded payloads; a variable-size envelope leaks sender identity.
Cover traffic
A Submitter<Tx2718> with no messages in flight sends a cover
envelope per zipnet round. That is the default and you should
not turn it off:
let submitter = Zipnet::<Tx2718>::submit(&network, Ð_MAINNET.zipnet)
.await?;
// submitter sends cover traffic every round while idle.
// drop(submitter) to stop.
The zipnet publishing page covers the tuning knobs — polling cadence, cover payload — which are unchanged in the lattice context.
Retry policy
Zipnet’s send returns SubmissionId once the envelope is
queued. The envelope may or may not land in the round’s broadcast
vector (deterministic slot assignment; collisions are possible
until footprint scheduling ships). A searcher or wallet that
needs guaranteed inclusion polls its Reader<Tx2718> for
byte-equality on future rounds until the envelope lands:
let mut reader = Zipnet::<Tx2718>::read(&network, Ð_MAINNET.zipnet).await?;
let expected = tx.clone();
let mut deadline = tokio::time::Instant::now() + Duration::from_secs(30);
loop {
tokio::select! {
Some(got) = reader.next() => {
if got == expected { break; }
}
_ = tokio::time::sleep_until(deadline) => {
submitter.send(expected.clone()).await?;
deadline = tokio::time::Instant::now() + Duration::from_secs(30);
}
}
}
In production, wrap this in your agent’s retry backoff logic and surface failures to your operator.
What happens after submission
Your envelope lands in a zipnet::Broadcasts[S] entry for some
slot S. The unseal committee produces the cleartext for that
slot into unseal::UnsealedPool[S]. atelier includes
transactions from the unsealed pool in its candidate block for
slot S. If you care whether your transaction made it on-chain
under this lattice, watch tally::Refunds[S] or the chain’s own
state; see Receiving refunds and attributions.
Next reading
- Placing bundle bids — the searcher-side surface zipnet feeds into.
- Reading built blocks — checking whether your submission landed in a candidate block.
- zipnet publishing messages — the full submission reference.