Skip to main content

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:
1

Collect Transactions

Gather all pending L2PS transactions from mempool (up to 10 per batch).
2

Aggregate GCR Edits

Combine all balance changes into a single set of GCR edits.
3

Generate ZK Proof

Create PLONK proof verifying batch validity without revealing content.
4

Submit L1 Batch

Single L1 transaction submitted containing proof and batch hash.

Configuration

SettingDefaultDescription
L2PS_MAX_BATCH_SIZE10Maximum transactions per batch
L2PS_MIN_BATCH_SIZE1Minimum transactions to create batch
L2PS_AGGREGATION_INTERVAL_MS10000Aggregation 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)
StatusMeaningReversible?
ExecutedValidated locally, GCR edits generatedYes (if batch fails)
BatchedIncluded in L1 batch transactionYes (if block fails)
ConfirmedL1 block finalizedNo

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