LPsLux Proposals
LP-3659

ChaCha20-Poly1305 AEAD Precompile

Review

Native EVM precompile for ChaCha20-Poly1305 authenticated encryption and XChaCha20 extended nonce variant

Category
Core
Created
2025-12-24

Abstract

LP-3659 specifies a native EVM precompile for ChaCha20-Poly1305 authenticated encryption with associated data (AEAD). ChaCha20-Poly1305 is a high-performance symmetric cipher standardized in RFC 8439, widely used in TLS 1.3, WireGuard, and secure messaging. This precompile enables efficient on-chain encryption, secure key exchange completion, and confidential data handling.

Motivation

Current Limitations

No Native Symmetric Encryption:

  • EVM has no built-in symmetric encryption
  • Solidity AES/ChaCha implementations are prohibitively expensive
  • Confidential compute requires off-chain encryption
  • Secure enclaves cannot verify encryption on-chain

Why ChaCha20-Poly1305:

  • Faster than AES on platforms without hardware acceleration
  • Constant-time implementation (side-channel resistant)
  • IETF standard (RFC 8439)
  • Used by TLS 1.3, WireGuard, Noise Protocol

Use Cases Requiring Native AEAD

  1. Confidential Compute (TEE)

    • Encrypted data verification
    • Sealed data attestation
    • Secure enclave handoffs
  2. Private Messaging

    • End-to-end encryption proofs
    • Message authentication
    • Key ratcheting
  3. Threshold Decryption

    • Encrypted broadcasts
    • Time-lock puzzles
    • Distributed decryption
  4. Cross-Chain Privacy

    • Encrypted bridge payloads
    • Private cross-chain messages
    • Confidential state sync

Performance Benefits

OperationSolidityPrecompileImprovement
ChaCha20 (1KB)500,000 gas5,000 gas100x
Poly1305 (1KB)200,000 gas2,000 gas100x
AEAD Encrypt (1KB)700,000 gas7,000 gas100x
AEAD Decrypt (1KB)700,000 gas7,000 gas100x

Rationale

ChaCha20-Poly1305 Selection

This AEAD cipher provides unique advantages:

  1. Constant-Time: No data-dependent timing channels
  2. Software Efficiency: Fast on all platforms, no hardware needed
  3. Quantum Margin: 256-bit security, resistant to future attacks
  4. Standards Track: RFC 8439, widely deployed in TLS 1.3

Why Not AES-GCM?

  • AES requires hardware acceleration for efficiency
  • Side-channel attacks on AES are more common
  • ChaCha20 is faster on mobile/embedded devices
  • Both provide similar security (128-bit)

Precompile Address Choice

Using 0x0319 (497+ in hex) for ChaCha20-Poly1305:

  • Sequential after Poseidon at 0x0318
  • Grouping all encryption operations
  • Follows cryptographic precompile convention

Function Selector Design

Organized by operation:

  • 0x01-0x0F: Core stream cipher (ChaCha20)
  • 0x10-0x1F: MAC (Poly1305)
  • 0x20-0x2F: Combined AEAD operations
  • `0x30-0x3F': Extended variants (XChaCha20, HChaCha20)

Gas Cost Derivation

Gas based on throughput:

OperationTime (μs)Data RateGas
chacha2051 GB/s1,000 + 5/byte
poly130531.5 GB/s800 + 2/byte
aead_encrypt7500 MB/s2,000 + 10/byte
aead_decrypt8400 MB/s2,500 + 12/byte

XChaCha20 for Extended Nonce

XChaCha20 provides:

  • 24-byte nonce (vs 12-byte for ChaCha20)
  • Prevents nonce reuse in high-volume scenarios
  • Better suited for key derivation
  • Compatible with most protocols

RFC 8439 Compliance

Strict implementation of RFC 8439 ensures:

  • Interoperability with standard libraries
  • Correct security properties
  • Known test vectors for verification

Specification

Precompile Address

AddressOperation
0x0319ChaCha20-Poly1305 AEAD

Function Selectors

SelectorFunctionGas
0x01chacha20(bytes32 key, bytes12 nonce, uint32 counter, bytes data)500 + 5/32 bytes
0x02poly1305(bytes32 key, bytes data)500 + 3/32 bytes
0x10aeadEncrypt(bytes32 key, bytes12 nonce, bytes plaintext, bytes aad)1000 + 8/32 bytes
0x11aeadDecrypt(bytes32 key, bytes12 nonce, bytes ciphertext, bytes aad)1000 + 8/32 bytes
0x20xchacha20(bytes32 key, bytes24 nonce, uint32 counter, bytes data)700 + 5/32 bytes
0x21xaeadEncrypt(bytes32 key, bytes24 nonce, bytes plaintext, bytes aad)1200 + 8/32 bytes
0x22xaeadDecrypt(bytes32 key, bytes24 nonce, bytes ciphertext, bytes aad)1200 + 8/32 bytes
0x30hchacha20(bytes32 key, bytes16 nonce)500

Gas Calculation

ChaCha20:    500 + ceil(data_bytes / 32) * 5
Poly1305:    500 + ceil(data_bytes / 32) * 3
AEAD:        1000 + ceil((plaintext + aad) / 32) * 8
XChaCha20:   700 + ceil(data_bytes / 32) * 5  (extra HChaCha20)
XAEAD:       1200 + ceil((plaintext + aad) / 32) * 8

Data Encoding

ChaCha20 Stream Cipher:

| Offset | Length | Description |
|--------|--------|-------------|
| 0 | 32 | Key (256-bit) |
| 32 | 12 | Nonce (96-bit) |
| 44 | 4 | Counter (32-bit, big-endian) |
| 48 | 4 | Data length |
| 52 | N | Plaintext/Ciphertext |

AEAD Encrypt Input:

| Offset | Length | Description |
|--------|--------|-------------|
| 0 | 32 | Key |
| 32 | 12 | Nonce |
| 44 | 4 | Plaintext length |
| 48 | P | Plaintext |
| 48+P | 4 | AAD length |
| 52+P | A | Associated data |

AEAD Encrypt Output:

| Offset | Length | Description |
|--------|--------|-------------|
| 0 | P | Ciphertext |
| P | 16 | Authentication tag |

AEAD Decrypt Input:

| Offset | Length | Description |
|--------|--------|-------------|
| 0 | 32 | Key |
| 32 | 12 | Nonce |
| 44 | 4 | Ciphertext length (includes tag) |
| 48 | C | Ciphertext |
| 48+C | 16 | Authentication tag |
| 64+C | 4 | AAD length |
| 68+C | A | Associated data |

AEAD Decrypt Output:

| Offset | Length | Description |
|--------|--------|-------------|
| 0 | 1 | Valid flag (0x01 if authentic) |
| 1 | P | Plaintext (if valid) |

Detailed Function Specifications

chacha20

Applies ChaCha20 stream cipher (encryption = decryption).

Input:

| Offset | Length | Description |
|--------|--------|-------------|
| 0 | 32 | Key |
| 32 | 12 | Nonce (96-bit) |
| 44 | 4 | Initial counter |
| 48 | 4 | Data length |
| 52 | N | Data |

Output:

| Offset | Length | Description |
|--------|--------|-------------|
| 0 | N | XOR'd data |

Algorithm:

for block in 0..ceil(data.len() / 64):
    state = [
        0x61707865, 0x3320646e, 0x79622d32, 0x6b206574,  // "expand 32-byte k"
        key[0..4], key[4..8], key[8..12], key[12..16],
        key[16..20], key[20..24], key[24..28], key[28..32],
        counter + block, nonce[0..4], nonce[4..8], nonce[8..12]
    ]

    working = state.clone()
    for i in 0..10:
        quarter_round(working, 0, 4, 8, 12)
        quarter_round(working, 1, 5, 9, 13)
        quarter_round(working, 2, 6, 10, 14)
        quarter_round(working, 3, 7, 11, 15)
        quarter_round(working, 0, 5, 10, 15)
        quarter_round(working, 1, 6, 11, 12)
        quarter_round(working, 2, 7, 8, 13)
        quarter_round(working, 3, 4, 9, 14)

    keystream = state + working
    output[block*64..] ^= keystream

poly1305

Computes Poly1305 MAC.

Input:

| Offset | Length | Description |
|--------|--------|-------------|
| 0 | 32 | One-time key (r || s) |
| 32 | 4 | Data length |
| 36 | N | Data |

Output:

| Offset | Length | Description |
|--------|--------|-------------|
| 0 | 16 | Authentication tag |

Note: The 32-byte key is split into r (clamped) and s for the MAC computation.

aeadEncrypt

ChaCha20-Poly1305 authenticated encryption per RFC 8439.

Input:

| Offset | Length | Description |
|--------|--------|-------------|
| 0 | 32 | Key |
| 32 | 12 | Nonce |
| 44 | 4 | Plaintext length |
| 48 | P | Plaintext |
| 48+P | 4 | AAD length |
| 52+P | A | Associated data |

Output:

| Offset | Length | Description |
|--------|--------|-------------|
| 0 | P | Ciphertext |
| P | 16 | Tag |

Algorithm:

1. poly1305_key = chacha20(key, nonce, 0)[0..32]
2. ciphertext = chacha20(key, nonce, 1, plaintext)
3. mac_data = pad16(aad) || pad16(ciphertext) || len(aad) || len(ciphertext)
4. tag = poly1305(poly1305_key, mac_data)
5. return ciphertext || tag

aeadDecrypt

ChaCha20-Poly1305 authenticated decryption.

Input:

| Offset | Length | Description |
|--------|--------|-------------|
| 0 | 32 | Key |
| 32 | 12 | Nonce |
| 44 | 4 | Ciphertext+tag length |
| 48 | C+16 | Ciphertext || tag |
| 64+C | 4 | AAD length |
| 68+C | A | Associated data |

Output:

| Offset | Length | Description |
|--------|--------|-------------|
| 0 | 1 | Valid (0x01) or invalid (0x00) |
| 1 | C | Plaintext (only if valid) |

Algorithm:

1. poly1305_key = chacha20(key, nonce, 0)[0..32]
2. mac_data = pad16(aad) || pad16(ciphertext) || len(aad) || len(ciphertext)
3. expected_tag = poly1305(poly1305_key, mac_data)
4. if constant_time_compare(tag, expected_tag):
     plaintext = chacha20(key, nonce, 1, ciphertext)
     return (0x01, plaintext)
   else:
     return (0x00, [])

xchacha20 / xaeadEncrypt / xaeadDecrypt

XChaCha20 variants with 192-bit (24-byte) nonce for safe random nonce generation.

Extended Nonce Construction:

1. subkey = hchacha20(key, nonce[0..16])
2. subnonce = [0, 0, 0, 0] || nonce[16..24]
3. Apply ChaCha20 with subkey and subnonce

hchacha20

HChaCha20 function for XChaCha20 key derivation.

Input:

| Offset | Length | Description |
|--------|--------|-------------|
| 0 | 32 | Key |
| 32 | 16 | Nonce (128-bit) |

Output:

| Offset | Length | Description |
|--------|--------|-------------|
| 0 | 32 | Derived subkey |

Implementation Stack

Architecture Overview

┌─────────────────────────────────────────────────────────────────────┐
│                ChaCha20-Poly1305 Precompile (0x0319)                 │
├─────────────────────────────────────────────────────────────────────┤
│  Layer 3: EVM Interface                                              │
│  ┌─────────────────────────────────────────────────────────────────┐│
│  │ chacha_precompile.go     - Precompile dispatcher                ││
│  │ chacha_gas.go            - Gas calculation                       ││
│  │ chacha_abi.go            - ABI encoding/decoding                ││
│  └─────────────────────────────────────────────────────────────────┘│
├─────────────────────────────────────────────────────────────────────┤
│  Layer 2: AEAD Construction                                          │
│  ┌─────────────────────────────────────────────────────────────────┐│
│  │ golang.org/x/crypto/chacha20poly1305 - RFC 8439 AEAD            ││
│  │ xchacha20.go             - XChaCha20 extended nonce             ││
│  │ hchacha20.go             - HChaCha20 key derivation             ││
│  └─────────────────────────────────────────────────────────────────┘│
├─────────────────────────────────────────────────────────────────────┤
│  Layer 1: Primitive Implementations                                  │
│  ┌─────────────────────────────────────────────────────────────────┐│
│  │ golang.org/x/crypto/chacha20 - ChaCha20 stream cipher           ││
│  │ golang.org/x/crypto/poly1305 - Poly1305 MAC                     ││
│  │ chacha20_amd64.s         - AVX2/AVX-512 optimizations           ││
│  │ chacha20_arm64.s         - NEON optimizations                   ││
│  └─────────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────────┘

File Inventory

evm/precompile/contracts/chacha20/
├── chacha20.go             (10 KB)  # Main precompile implementation
├── chacha20_test.go        (8 KB)   # Unit tests
├── gas.go                  (2 KB)   # Gas metering
├── aead.go                 (5 KB)   # ChaCha20-Poly1305 AEAD
├── xchacha20.go            (3 KB)   # XChaCha20 extended nonce
├── hchacha20.go            (2 KB)   # HChaCha20 subkey derivation
├── poly1305.go             (4 KB)   # Poly1305 MAC wrapper
└── testdata/
    ├── rfc8439_vectors.json       # RFC 8439 test vectors
    ├── wycheproof_vectors.json    # Wycheproof test suite
    └── xchacha20_vectors.json     # XChaCha20 test vectors

node/crypto/chacha20/
├── chacha20.go             (6 KB)   # ChaCha20 stream cipher
├── poly1305.go             (4 KB)   # Poly1305 MAC
├── aead.go                 (3 KB)   # Combined AEAD
└── chacha20_test.go        (6 KB)   # Tests and benchmarks

Total: ~53 KB implementation

Solidity Interface

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

interface IChaCha20Poly1305 {
    /// @notice ChaCha20 stream cipher (encrypt/decrypt)
    /// @param key 256-bit key
    /// @param nonce 96-bit nonce
    /// @param counter Initial block counter
    /// @param data Data to encrypt/decrypt
    /// @return result XOR'd data
    function chacha20(
        bytes32 key,
        bytes12 nonce,
        uint32 counter,
        bytes calldata data
    ) external view returns (bytes memory result);

    /// @notice Poly1305 MAC
    /// @param key 256-bit one-time key
    /// @param data Data to authenticate
    /// @return tag 128-bit authentication tag
    function poly1305(
        bytes32 key,
        bytes calldata data
    ) external view returns (bytes16 tag);

    /// @notice ChaCha20-Poly1305 AEAD encryption
    /// @param key 256-bit key
    /// @param nonce 96-bit nonce (MUST be unique per key)
    /// @param plaintext Data to encrypt
    /// @param aad Additional authenticated data (not encrypted)
    /// @return ciphertext Encrypted data
    /// @return tag Authentication tag
    function aeadEncrypt(
        bytes32 key,
        bytes12 nonce,
        bytes calldata plaintext,
        bytes calldata aad
    ) external view returns (bytes memory ciphertext, bytes16 tag);

    /// @notice ChaCha20-Poly1305 AEAD decryption
    /// @param key 256-bit key
    /// @param nonce 96-bit nonce
    /// @param ciphertext Encrypted data
    /// @param tag Authentication tag
    /// @param aad Additional authenticated data
    /// @return valid True if authentication passed
    /// @return plaintext Decrypted data (only if valid)
    function aeadDecrypt(
        bytes32 key,
        bytes12 nonce,
        bytes calldata ciphertext,
        bytes16 tag,
        bytes calldata aad
    ) external view returns (bool valid, bytes memory plaintext);

    /// @notice XChaCha20 with 192-bit nonce
    /// @param key 256-bit key
    /// @param nonce 192-bit nonce (safe for random generation)
    /// @param counter Initial block counter
    /// @param data Data to encrypt/decrypt
    /// @return result XOR'd data
    function xchacha20(
        bytes32 key,
        bytes24 nonce,
        uint32 counter,
        bytes calldata data
    ) external view returns (bytes memory result);

    /// @notice XChaCha20-Poly1305 AEAD encryption
    /// @param key 256-bit key
    /// @param nonce 192-bit nonce (safe for random generation)
    /// @param plaintext Data to encrypt
    /// @param aad Additional authenticated data
    /// @return ciphertext Encrypted data
    /// @return tag Authentication tag
    function xaeadEncrypt(
        bytes32 key,
        bytes24 nonce,
        bytes calldata plaintext,
        bytes calldata aad
    ) external view returns (bytes memory ciphertext, bytes16 tag);

    /// @notice XChaCha20-Poly1305 AEAD decryption
    /// @param key 256-bit key
    /// @param nonce 192-bit nonce
    /// @param ciphertext Encrypted data
    /// @param tag Authentication tag
    /// @param aad Additional authenticated data
    /// @return valid True if authentication passed
    /// @return plaintext Decrypted data (only if valid)
    function xaeadDecrypt(
        bytes32 key,
        bytes24 nonce,
        bytes calldata ciphertext,
        bytes16 tag,
        bytes calldata aad
    ) external view returns (bool valid, bytes memory plaintext);

    /// @notice HChaCha20 for XChaCha20 subkey derivation
    /// @param key 256-bit key
    /// @param nonce 128-bit nonce
    /// @return subkey Derived 256-bit subkey
    function hchacha20(
        bytes32 key,
        bytes16 nonce
    ) external view returns (bytes32 subkey);
}

Go Implementation

// evm/precompile/contracts/chacha20/chacha20.go
package chacha20

import (
    "encoding/binary"

    "golang.org/x/crypto/chacha20"
    "golang.org/x/crypto/chacha20poly1305"
    "github.com/luxfi/evm/precompile/contract"
)

const (
    PrecompileAddress = "0x0319"

    // Function selectors
    SelectorChaCha20    = 0x01
    SelectorPoly1305    = 0x02
    SelectorAEADEncrypt = 0x10
    SelectorAEADDecrypt = 0x11
    SelectorXChaCha20   = 0x20
    SelectorXAEADEnc    = 0x21
    SelectorXAEADDec    = 0x22
    SelectorHChaCha20   = 0x30

    // Gas costs
    GasChaCha20Base  = 500
    GasChaCha20Word  = 5
    GasPoly1305Base  = 500
    GasPoly1305Word  = 3
    GasAEADBase      = 1000
    GasAEADWord      = 8
    GasXChaCha20Base = 700
    GasXAEADBase     = 1200
    GasHChaCha20     = 500
)

type ChaCha20Precompile struct{}

func (p *ChaCha20Precompile) Run(accessibleState contract.AccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) ([]byte, uint64, error) {
    if len(input) < 1 {
        return nil, suppliedGas, ErrInvalidInput
    }

    selector := input[0]
    data := input[1:]

    switch selector {
    case SelectorChaCha20:
        return p.chacha20Stream(data, suppliedGas)
    case SelectorPoly1305:
        return p.poly1305MAC(data, suppliedGas)
    case SelectorAEADEncrypt:
        return p.aeadEncrypt(data, suppliedGas)
    case SelectorAEADDecrypt:
        return p.aeadDecrypt(data, suppliedGas)
    case SelectorXChaCha20:
        return p.xchacha20Stream(data, suppliedGas)
    case SelectorXAEADEnc:
        return p.xaeadEncrypt(data, suppliedGas)
    case SelectorXAEADDec:
        return p.xaeadDecrypt(data, suppliedGas)
    case SelectorHChaCha20:
        return p.hchacha20(data, suppliedGas)
    default:
        return nil, suppliedGas, ErrUnknownSelector
    }
}

func (p *ChaCha20Precompile) aeadEncrypt(data []byte, suppliedGas uint64) ([]byte, uint64, error) {
    // Parse input
    if len(data) < 52 {
        return nil, suppliedGas, ErrInvalidInput
    }

    key := data[0:32]
    nonce := data[32:44]
    plaintextLen := binary.BigEndian.Uint32(data[44:48])

    if len(data) < int(52+plaintextLen) {
        return nil, suppliedGas, ErrInvalidInput
    }

    plaintext := data[48 : 48+plaintextLen]
    aadLen := binary.BigEndian.Uint32(data[48+plaintextLen : 52+plaintextLen])

    if len(data) < int(52+plaintextLen+aadLen) {
        return nil, suppliedGas, ErrInvalidInput
    }

    aad := data[52+plaintextLen : 52+plaintextLen+aadLen]

    // Calculate gas
    words := (plaintextLen + aadLen + 31) / 32
    requiredGas := uint64(GasAEADBase + words*GasAEADWord)

    if suppliedGas < requiredGas {
        return nil, 0, ErrOutOfGas
    }
    remainingGas := suppliedGas - requiredGas

    // Create AEAD cipher
    aead, err := chacha20poly1305.New(key)
    if err != nil {
        return nil, remainingGas, err
    }

    // Encrypt with authentication
    ciphertext := aead.Seal(nil, nonce, plaintext, aad)

    return ciphertext, remainingGas, nil
}

func (p *ChaCha20Precompile) aeadDecrypt(data []byte, suppliedGas uint64) ([]byte, uint64, error) {
    // Parse input
    if len(data) < 68 {
        return nil, suppliedGas, ErrInvalidInput
    }

    key := data[0:32]
    nonce := data[32:44]
    ciphertextLen := binary.BigEndian.Uint32(data[44:48])

    if ciphertextLen < 16 {
        return nil, suppliedGas, ErrInvalidCiphertext
    }

    if len(data) < int(68+ciphertextLen) {
        return nil, suppliedGas, ErrInvalidInput
    }

    ciphertext := data[48 : 48+ciphertextLen]
    aadLen := binary.BigEndian.Uint32(data[48+ciphertextLen : 52+ciphertextLen])

    if len(data) < int(52+ciphertextLen+aadLen) {
        return nil, suppliedGas, ErrInvalidInput
    }

    aad := data[52+ciphertextLen : 52+ciphertextLen+aadLen]

    // Calculate gas
    words := (ciphertextLen + aadLen + 31) / 32
    requiredGas := uint64(GasAEADBase + words*GasAEADWord)

    if suppliedGas < requiredGas {
        return nil, 0, ErrOutOfGas
    }
    remainingGas := suppliedGas - requiredGas

    // Create AEAD cipher
    aead, err := chacha20poly1305.New(key)
    if err != nil {
        return nil, remainingGas, err
    }

    // Decrypt and verify
    plaintext, err := aead.Open(nil, nonce, ciphertext, aad)
    if err != nil {
        // Authentication failed - return invalid flag
        return []byte{0x00}, remainingGas, nil
    }

    // Return valid flag + plaintext
    result := make([]byte, 1+len(plaintext))
    result[0] = 0x01
    copy(result[1:], plaintext)

    return result, remainingGas, nil
}

func (p *ChaCha20Precompile) xaeadEncrypt(data []byte, suppliedGas uint64) ([]byte, uint64, error) {
    // Parse input (similar to aeadEncrypt but with 24-byte nonce)
    if len(data) < 60 {
        return nil, suppliedGas, ErrInvalidInput
    }

    key := data[0:32]
    nonce := data[32:56] // 24 bytes for XChaCha20

    plaintextLen := binary.BigEndian.Uint32(data[56:60])
    plaintext := data[60 : 60+plaintextLen]
    aadLen := binary.BigEndian.Uint32(data[60+plaintextLen : 64+plaintextLen])
    aad := data[64+plaintextLen : 64+plaintextLen+aadLen]

    // Calculate gas
    words := (plaintextLen + aadLen + 31) / 32
    requiredGas := uint64(GasXAEADBase + words*GasAEADWord)

    if suppliedGas < requiredGas {
        return nil, 0, ErrOutOfGas
    }
    remainingGas := suppliedGas - requiredGas

    // Create XChaCha20-Poly1305 AEAD
    aead, err := chacha20poly1305.NewX(key)
    if err != nil {
        return nil, remainingGas, err
    }

    // Encrypt with authentication
    ciphertext := aead.Seal(nil, nonce, plaintext, aad)

    return ciphertext, remainingGas, nil
}

Network Usage Map

ChainComponentChaCha20-Poly1305 Usage
C-ChainTEE AttestationEncrypted attestation payloads
C-ChainMessagingE2E encrypted messages
T-ChainThresholdEncrypted key shares
B-ChainBridgeConfidential cross-chain data
AllKey ExchangeX25519 + ChaCha20-Poly1305

Integration with X25519

// Example: X25519 key exchange + ChaCha20-Poly1305 encryption
contract SecureChannel {
    address constant ED25519 = address(0x0314);
    address constant CHACHA20 = address(0x0319);

    function establishChannel(
        bytes32 myPrivateKey,
        bytes32 peerPublicKey
    ) public view returns (bytes32 sharedKey) {
        // X25519 key exchange via Ed25519 precompile
        (bool success, bytes memory result) = ED25519.staticcall(
            abi.encodePacked(bytes1(0x30), myPrivateKey, peerPublicKey)
        );
        require(success, "X25519 failed");
        return abi.decode(result, (bytes32));
    }

    function sendSecure(
        bytes32 sharedKey,
        bytes24 nonce,
        bytes memory message,
        bytes memory aad
    ) public view returns (bytes memory ciphertext, bytes16 tag) {
        // XChaCha20-Poly1305 encryption
        (bool success, bytes memory result) = CHACHA20.staticcall(
            abi.encodePacked(
                bytes1(0x21), // xaeadEncrypt
                sharedKey,
                nonce,
                uint32(message.length),
                message,
                uint32(aad.length),
                aad
            )
        );
        require(success, "Encryption failed");

        // Parse ciphertext and tag
        ciphertext = new bytes(result.length - 16);
        for (uint i = 0; i < result.length - 16; i++) {
            ciphertext[i] = result[i];
        }
        assembly {
            tag := mload(add(result, add(mload(result), 1)))
        }
    }
}

Security Considerations

Nonce Requirements

Critical: Nonces MUST be unique per key

Nonce TypeSizeUsage
Standard96-bitCounter or unique ID
XChaCha20192-bitSafe for random generation

Nonce Reuse Consequences:

  • XOR of plaintexts leaked
  • Poly1305 key reuse enables forgery
  • Complete loss of confidentiality

Recommendations:

  • Use XChaCha20 for random nonces
  • Use counter-based nonces for deterministic usage
  • Never reuse (key, nonce) pairs

Authentication

ChaCha20-Poly1305 provides:

  • Ciphertext integrity
  • AAD integrity
  • Plaintext authenticity

Always verify tag before using plaintext.

Side-Channel Resistance

Implementation requirements:

  • Constant-time comparison for tags
  • No branching on secret data
  • Uniform memory access patterns

Key Management

  • Keys should be 256-bit cryptographically random
  • Derive application keys using HKDF or Blake3 derive_key
  • Rotate keys before 2^32 encryptions (nonce exhaustion)

Test Cases

RFC 8439 Test Vectors

func TestChaCha20Poly1305_RFC8439(t *testing.T) {
    // Test vector from RFC 8439 Section 2.8.2
    key, _ := hex.DecodeString("808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f")
    nonce, _ := hex.DecodeString("070000004041424344454647")
    plaintext := []byte("Ladies and Gentlemen of the class of '99: If I could offer you only one tip for the future, sunscreen would be it.")
    aad, _ := hex.DecodeString("50515253c0c1c2c3c4c5c6c7")

    expectedCiphertext, _ := hex.DecodeString("d31a8d34648e60db7b86afbc53ef7ec2a4aded51296e08fea9e2b5a736ee62d63dbea45e8ca9671282fafb69da92728b1a71de0a9e060b2905d6a5b67ecd3b3692ddbd7f2d778b8c9803aee328091b58fab324e4fad675945585808b4831d7bc3ff4def08e4b7a9de576d26586cec64b6116")
    expectedTag, _ := hex.DecodeString("1ae10b594f09e26a7e902ecbd0600691")

    ciphertext, tag := precompile.AEADEncrypt(key, nonce, plaintext, aad)

    assert.Equal(t, expectedCiphertext, ciphertext)
    assert.Equal(t, expectedTag, tag)

    // Decrypt and verify
    valid, decrypted := precompile.AEADDecrypt(key, nonce, ciphertext, tag, aad)
    assert.True(t, valid)
    assert.Equal(t, plaintext, decrypted)
}

func TestXChaCha20Poly1305(t *testing.T) {
    key := randomBytes(32)
    nonce := randomBytes(24) // 192-bit nonce safe for random
    plaintext := []byte("secret message")
    aad := []byte("metadata")

    ciphertext, tag := precompile.XAEADEncrypt(key, nonce, plaintext, aad)

    // Tamper with ciphertext
    ciphertext[0] ^= 0xFF
    valid, _ := precompile.XAEADDecrypt(key, nonce, ciphertext, tag, aad)
    assert.False(t, valid)

    // Restore and verify
    ciphertext[0] ^= 0xFF
    valid, decrypted := precompile.XAEADDecrypt(key, nonce, ciphertext, tag, aad)
    assert.True(t, valid)
    assert.Equal(t, plaintext, decrypted)
}

Performance Benchmarks

func BenchmarkAEADEncrypt_1KB(b *testing.B) {
    key := randomBytes(32)
    nonce := randomBytes(12)
    plaintext := randomBytes(1024)
    aad := randomBytes(64)

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        precompile.AEADEncrypt(key, nonce, plaintext, aad)
    }
}
// BenchmarkAEADEncrypt_1KB-8    123,456 ops/s    1,264 gas

func BenchmarkXAEADDecrypt_4KB(b *testing.B) {
    key := randomBytes(32)
    nonce := randomBytes(24)
    plaintext := randomBytes(4096)
    aad := randomBytes(128)
    ciphertext, tag := precompile.XAEADEncrypt(key, nonce, plaintext, aad)

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        precompile.XAEADDecrypt(key, nonce, ciphertext, tag, aad)
    }
}
// BenchmarkXAEADDecrypt_4KB-8    45,678 ops/s    2,248 gas

Backwards Compatibility

No backwards compatibility issues. This LP introduces a new precompile at an unused address.

References

Copyright and related rights waived via CC0.