Skip to main content

Messaging

Privacy-preserving communication between token holders using IntegraSignalV1 for encrypted payment requests and IntegraMessageV1 for ZK-gated workflow events.

Overview

The Integra messaging layer enables direct communication between protocol participants without revealing message content on-chain. It provides two contracts that address distinct communication needs:

  • IntegraSignalV1 -- Encrypted payment request lifecycle with hybrid encryption and EAS attestation integration
  • IntegraMessageV1 -- Zero-knowledge-gated workflow event messaging with event-sourced storage

Both contracts operate over the tokenizer layer. Participants are identified by token holdings rather than raw wallet addresses, creating a trust substrate: you can only send a payment request to someone who holds a token on the same record, and you can only register a workflow message if you can produce a valid ZK proof for the process.


Design Principles

Privacy Through Client-Side Encryption

Neither contract reads or interprets message content. Encrypted payloads are opaque bytes stored (or emitted) on-chain. Decryption happens entirely in client applications.

For IntegraSignalV1, the encryption model is hybrid: a random AES-256-GCM session key encrypts the payment payload, and that session key is then encrypted separately for each party using their respective public keys.

For IntegraMessageV1, messages are emitted as plaintext strings in event logs. If application-layer encryption is needed, the caller encrypts before calling registerMessage and decrypts when reading events.

Event-Sourced vs. Storage-Backed

ContractStorage ModelRationale
IntegraSignalV1Full struct storagePayment requests have a lifecycle (PENDING -> PAID/CANCELLED), timeouts, and extensions. State must persist and be queryable.
IntegraMessageV1Event-only (no storage)Workflow messages are append-only audit entries. No state to mutate after emission. Indexers reconstruct history from events.

Process Hash Correlation

Both contracts require a processHash parameter on every state-changing call. This bytes32 value ties on-chain activity to off-chain workflow processes. Indexers and workflow engines use processHash to correlate payment requests, messages, and other events into coherent process timelines.


IntegraSignalV1: Payment Requests

IntegraSignalV1 enables secure, privacy-preserving payment requests between token holders on the same record.

Payment Request Lifecycle

                 sendPaymentRequest()
                        |
                        v
                   +-----------+
                   |  PENDING  |
                   +-----------+
                    /         \
      markPaid()  /           \  cancelPayment()
                 /             \
                v               v
         +-----------+    +-----------+
         |   PAID    |    | CANCELLED |
         +-----------+    +-----------+

Payment requests begin in PENDING status. From there, they can transition to PAID (by requestor, payer, or operator) or CANCELLED (by requestor or payer, or by anyone after timeout expiry). Both transitions are terminal.

Timeout and Extension Mechanics

Each payment request has a configurable timeout:

  • Default timeout: 60 days (configurable by governor, bounded by 7-365 days)
  • Custom timeout: Specified per-request via timeoutDays parameter (0 = use default)
  • Grace period: 3 hours added to every expiry calculation
  • Extensions: Up to 90 days per extension, cumulative maximum of 180 days

Expiry formula: timestamp + baseTimeout + totalExtensions + TIMESTAMP_GRACE_PERIOD

Hybrid Encryption Flow

The contract does not perform encryption or decryption. This happens entirely client-side:

Encryption (before sendPaymentRequest):

// 1. Serialize payment details
const paymentDetails = {
    amount: 1000.00,
    currency: "USD",
    bankName: "Chase Bank",
    instructions: "Wire transfer for Invoice INV-2024-001"
};

// 2. Generate random session key
const sessionKey = crypto.randomBytes(32);

// 3. Encrypt payload with session key (AES-256-GCM)
const encryptedPayload = await encryptAES256GCM(
    JSON.stringify(paymentDetails), sessionKey
);

// 4. Encrypt session key for each party
const encryptedKeyRequestor = await rsaEncrypt(sessionKey, requestorPublicKey);
const encryptedKeyPayer = await rsaEncrypt(sessionKey, payerPublicKey);

// 5. Create EAS attestation of payload hash
const payloadHash = ethers.keccak256(encryptedPayload);
const attestation = await eas.attest({
    schema: paymentPayloadSchemaUID,
    data: {
        recipient: requestorAddress,
        revocable: false,
        data: ethers.AbiCoder.defaultAbiCoder().encode(["bytes32"], [payloadHash])
    }
});

// 6. Submit payment request
const requestId = await integraSignalV1.sendPaymentRequest(
    integraHash,
    requestorTokenId,
    payerTokenId,
    payerAddress,
    encryptedPayload,
    encryptedKeyRequestor,
    encryptedKeyPayer,
    attestation.uid,
    "INV-2024-001",
    100000,  // display amount: $1000.00
    "USD",
    processHash,
    30       // 30-day timeout
);

Decryption (payer reads payment details):

// 1. Retrieve payment request
const request = await integraSignalV1.getPaymentRequest(requestId);

// 2. Decrypt session key with payer's private key
const sessionKey = await rsaDecrypt(
    request.encryptedSessionKeyPayer, payerPrivateKey
);

// 3. Decrypt payload with session key
const paymentDetails = JSON.parse(
    await decryptAES256GCM(request.encryptedPayload, sessionKey)
);

Token Holder Verification

IntegraSignalV1 verifies that both the requestor and payer hold tokens on the record. Token ownership is checked at request creation time through the tokenizer associated with the record (looked up via IntegraRecordV1.getTokenizer()). This prevents strangers from sending payment requests to arbitrary addresses.

Key Operations

Send a payment request:

bytes32 requestId = integraSignalV1.sendPaymentRequest(
    integraHash,        // Record identifier
    requestorTokenId,   // Requestor's token ID
    payerTokenId,       // Payer's token ID
    payerAddress,       // Payer's address
    encryptedPayload,   // Encrypted payment details (max 5000 bytes)
    encKeyRequestor,    // Session key encrypted for requestor
    encKeyPayer,        // Session key encrypted for payer
    attestationUID,     // EAS attestation of payload hash
    "INV-2024-001",     // Invoice reference (max 200 chars)
    100000,             // Display amount
    "USD",              // Display currency (max 10 chars)
    processHash,        // Workflow correlation
    30                  // Timeout in days (0 = default)
);

Mark as paid:

integraSignalV1.markPaid(requestId, paymentProofHash, processHash);

Cancel:

integraSignalV1.cancelPayment(requestId, processHash);

Extend timeout:

integraSignalV1.extendPaymentRequest(requestId, 30, processHash); // +30 days

Approximate Gas Costs

OperationGas
sendPaymentRequest()~200,000
markPaid()~50,000
cancelPayment()~45,000
extendPaymentRequest()~40,000

IntegraMessageV1: Workflow Events

IntegraMessageV1 is an event-sourced messaging system for workflow coordination. Participants register timestamped messages correlated to records and workflows using ZK proof-based anti-spam protection.

Why ZK Proofs?

Instead of token-holder verification, IntegraMessageV1 requires callers to submit a Groth16 ZK proof demonstrating knowledge of the processHash. This proves the sender is a legitimate workflow participant without revealing which specific process they belong to. It also prevents spam: without knowing the processHash pre-image, generating a valid proof is impossible.

How It Works

1. Participant knows processHash (derived from workflow secret)
2. Generates Groth16 ZK proof off-chain (proves knowledge of pre-image)
3. Calls registerMessage() with proof
4. Contract verifies proof against registered verifier
5. Event emitted (MessageRegistered) -- no storage writes
6. Off-chain indexer correlates messages by processHash

Message Format

Each message includes:

  • integraHash -- Record identifier
  • tokenId -- Sender's token ID (for correlation)
  • processHash -- Workflow correlation identifier (ZK-proven)
  • eventRef -- Event type string (1-100 chars, e.g., "PAYMENT_INITIATED")
  • message -- Event description (1-1000 chars)
  • timestamp -- Block timestamp
  • registrant -- Address that registered the message

Code Example

// Generate ZK proof off-chain
const { proofA, proofB, proofC } = await generateProcessHashProof(
    processHash, workflowSecret
);

// Register workflow event
await integraMessageV1.registerMessage(
    integraHash,
    tokenId,
    processHash,
    proofA, proofB, proofC,
    "PAYMENT_INITIATED",
    "Payment request INV-2024-001 sent to buyer"
);

Workflow Event Sequence Example

// Purchase order workflow
await registerMessage({ eventRef: "ORDER_CREATED",    message: "PO #001 created by buyer" });
await registerMessage({ eventRef: "ORDER_CONFIRMED",  message: "PO confirmed by seller" });
await registerMessage({ eventRef: "PAYMENT_REQUESTED", message: "Payment request for $10,000" });
await registerMessage({ eventRef: "PAYMENT_RECEIVED",  message: "Wire transfer received" });
await registerMessage({ eventRef: "GOODS_SHIPPED",     message: "Shipped via FedEx #123456789" });
await registerMessage({ eventRef: "GOODS_RECEIVED",    message: "Received and inspected" });

Off-Chain Indexing

Since IntegraMessageV1 stores nothing on-chain, indexers reconstruct message history from events:

const integraMessage = new ethers.Contract(address, abi, provider);

integraMessage.on("MessageRegistered", async (
    integraHash, processHash, tokenId,
    eventRef, message, timestamp, registrant, event
) => {
    await db.messages.insert({
        processHash, integraHash, tokenId: tokenId.toString(),
        eventRef, message, timestamp: timestamp.toNumber(),
        registrant, blockNumber: event.blockNumber,
        transactionHash: event.transactionHash
    });
});

// Query messages by workflow
async function getWorkflowMessages(processHash) {
    return await db.messages
        .where({ processHash })
        .orderBy('timestamp', 'asc')
        .toArray();
}

Gas Cost

registerMessage() costs approximately 60,000-80,000 gas (including ZK proof verification). Because there are no storage writes, gas usage does not grow with message volume.


Security Model

Defense in Depth

ThreatPrevention
Message spamZK proof requirement (IntegraMessageV1)
Unauthorized payment requestsToken holder verification (IntegraSignalV1)
Payment detail exposureClient-side hybrid encryption
Payload tamperingEAS attestation of payload hash
Invalid state transitionsState machine validation
Indefinite pending paymentsConfigurable timeout + extension limits
Cross-record replayprocessHash binding
Front-runningEAS recipient binding

Privacy Guarantees

IntegraSignalV1:

  • Payment amounts encrypted in payload (only display amount visible for UI)
  • Only requestor and payer can decrypt details
  • Session key unique per request
  • No financial details exposed on-chain

IntegraMessageV1:

  • processHash reveals nothing about the workflow (Poseidon hash)
  • Correlation requires processHash knowledge
  • Messages are public in event logs -- encrypt sensitive data before submission

When to Use Which Contract

ScenarioContractWhy
Invoice payment requestIntegraSignalV1Needs encrypted details, lifecycle tracking, timeout
Workflow status updateIntegraMessageV1Append-only event, no state to manage
Payment confirmationIntegraSignalV1markPaid() transitions state with proof
Milestone notificationIntegraMessageV1Informational event for off-chain tracking
Dispute initiationIntegraSignalV1State machine handles dispute flow
Audit trail entryIntegraMessageV1Immutable, gas-efficient event record

Both contracts support ERC-2771 meta-transactions for gasless operation.