How L2PS Transactions Are Handled
This page explains the complete lifecycle of an L2PS transaction from client submission to L1 confirmation.
Client-Side Encryption
Before any transaction leaves the user’s device, it is encrypted using AES-256-CBC:
// Client-side encryption (in browser)
import { L2PSEncryption } from '@kynesyslabs/demosdk/l2ps'
// Create inner transaction (what you want to send)
const innerTx = {
type: "native",
from: "0xYourAddress...",
to: "0xRecipient...",
amount: 5
}
// Encrypt with shared L2PS keys
const encryptedBlob = L2PSEncryption.encrypt(innerTx, aesKey, iv)
// Wrap in outer L2PS transaction
const l2psTx = {
type: "l2ps",
to: "testnet_l2ps_001", // L2PS UID
data: encryptedBlob
}
The AES key and IV must match exactly between client and L2PS nodes. If they don’t match, the node will fail to decrypt and reject the transaction.
Node Processing
When an L2PS node receives the encrypted transaction:
Step 1: Decryption & Validation
// Node-side processing (handleL2PS.ts)
async function handleL2PS(encryptedTx) {
// 1. Load L2PS network keys
const network = await ParallelNetworks.getNetwork(l2psUid)
// 2. Decrypt the transaction
const decryptedTx = network.decrypt(encryptedTx.data)
// 3. Verify sender signature
const isValid = Cryptography.verify(decryptedTx)
// 4. Validate against L1 state (balance, nonce)
const validationResult = await L2PSTransactionExecutor.execute(decryptedTx)
return validationResult
}
Step 2: GCR Edit Generation
L2PS uses the Global Change Registry (GCR) architecture. Instead of modifying state directly, transactions generate GCR edits:
// GCR edits generated for a transfer
[
{
type: "balance_edit",
account: "0xSender...",
delta: -6 // amount + 1 DEM fee
},
{
type: "balance_edit",
account: "0xRecipient...",
delta: +5
}
]
The 1 DEM fee not credited to any account.
Step 3: Mempool Storage
Validated transactions are stored in the L2PS-specific mempool:
-- L2PS Mempool Table
CREATE TABLE l2ps_mempool (
hash VARCHAR PRIMARY KEY,
l2ps_uid VARCHAR NOT NULL,
encrypted_tx JSONB NOT NULL,
gcr_edits JSONB NOT NULL,
status VARCHAR DEFAULT 'executed',
timestamp BIGINT NOT NULL
);
Batch Aggregation
The L2PS Batch Aggregator runs per L1 block and bundles pending transactions:
Collect Transactions
Gather all pending L2PS transactions from mempool (up to 10 per batch).
Aggregate GCR Edits
Combine all balance changes into a single set of GCR edits.
Generate ZK Proof
Create PLONK proof verifying batch validity without revealing content.
Submit L1 Batch
Single L1 transaction submitted containing proof and batch hash.
Configuration
| Setting | Default | Description |
|---|
L2PS_MAX_BATCH_SIZE | 10 | Maximum transactions per batch |
L2PS_MIN_BATCH_SIZE | 1 | Minimum transactions to create batch |
L2PS_AGGREGATION_INTERVAL_MS | 10000 | Aggregation check interval |
ZK Proof Generation
For each batch, a PLONK zero-knowledge proof is generated:
// ZK Proof for batch validity
const proof = await zkProver.generateProof({
transactionCount: batch.length,
inputHash: sha256(inputs),
outputHash: sha256(gcrEdits),
merkleRoot: calculateMerkleRoot(batch)
})
The proof verifies:
- ✅ All transactions have valid signatures
- ✅ All GCR edits are mathematically correct
- ✅ No double-spending within the batch
- ✅ Batch integrity (no tampering)
ZK proofs allow validators to verify batch validity without seeing the actual transaction content.
Consensus Integration
When a new L1 block is created, L2PS Consensus applies the batch:
// L2PSConsensus.ts
async function applyPendingProofs(blockNumber) {
// 1. Get pending proofs for this block
const proofs = await L2PSProofManager.getProofsForBlock(blockNumber)
// 2. Verify each proof
for (const proof of proofs) {
const isValid = await L2PSProofManager.verifyProof(proof)
// 3. Apply GCR edits to L1 state
for (const edit of proof.gcr_edits) {
await HandleGCR.apply(edit)
}
}
// 4. Update transaction statuses to 'confirmed'
await updateTransactionStatuses(proofs, 'confirmed')
}
Transaction Status Flow
⚡ Executed
Validated locally, GCR edits generated (instant)
📦 Batched
Included in L1 batch transaction (per block)
✓ Confirmed
L1 block finalized (final)
| Status | Meaning | Reversible? |
|---|
| Executed | Validated locally, GCR edits generated | Yes (if batch fails) |
| Batched | Included in L1 batch transaction | Yes (if block fails) |
| Confirmed | L1 block finalized | No |
Authenticated History Access
Unlike L1 transactions (public), L2PS history requires cryptographic proof of ownership:
// Client requesting their L2PS history
const timestamp = Date.now()
const message = `getL2PSHistory:${address}:${timestamp}`
const signature = await wallet.signMessage(message)
const response = await rpcCall({
method: "getL2PSAccountTransactions",
data: {
l2psUid: "testnet_l2ps_001",
address: address,
timestamp: timestamp,
signature: signature
}
})
If the signature is invalid or doesn’t match the requested address, the node returns 403 Access Denied.
Data Separation
L2PS maintains strict data separation for privacy:
L2PS Participants
- Store: Full encrypted transactions
- Process: Decrypt locally
- See: Transaction content
Validators
- Store: Only batch hashes
- Process: Verify ZK proofs
- See: Zero TX visibility
Next Steps