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
| Contract | Storage Model | Rationale |
|---|---|---|
| IntegraSignalV1 | Full struct storage | Payment requests have a lifecycle (PENDING -> PAID/CANCELLED), timeouts, and extensions. State must persist and be queryable. |
| IntegraMessageV1 | Event-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
timeoutDaysparameter (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 daysApproximate Gas Costs
| Operation | Gas |
|---|---|
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 processHashMessage Format
Each message includes:
integraHash-- Record identifiertokenId-- 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 timestampregistrant-- 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
| Threat | Prevention |
|---|---|
| Message spam | ZK proof requirement (IntegraMessageV1) |
| Unauthorized payment requests | Token holder verification (IntegraSignalV1) |
| Payment detail exposure | Client-side hybrid encryption |
| Payload tampering | EAS attestation of payload hash |
| Invalid state transitions | State machine validation |
| Indefinite pending payments | Configurable timeout + extension limits |
| Cross-record replay | processHash binding |
| Front-running | EAS 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
| Scenario | Contract | Why |
|---|---|---|
| Invoice payment request | IntegraSignalV1 | Needs encrypted details, lifecycle tracking, timeout |
| Workflow status update | IntegraMessageV1 | Append-only event, no state to manage |
| Payment confirmation | IntegraSignalV1 | markPaid() transitions state with proof |
| Milestone notification | IntegraMessageV1 | Informational event for off-chain tracking |
| Dispute initiation | IntegraSignalV1 | State machine handles dispute flow |
| Audit trail entry | IntegraMessageV1 | Immutable, gas-efficient event record |
Both contracts support ERC-2771 meta-transactions for gasless operation.