Skip to main content
May 11, 2026Reading time: 8 min

Inside Filosign's Signing Flow: Infrastructure Built for Legal Certainty

Inside Filosign's Signing Flow: Infrastructure Built for Legal Certainty

When we rebuilt Filosign's signing flow, we had one non-negotiable requirement: every signature must be independently verifiable by anyone, anywhere, without trusting Filosign's servers.

Most e-signature platforms store proof as database rows and PDF certificates with a vendor logo. If that database disappears, your audit trail may disappear with it.

Our placement-based signing infrastructure combines cryptographic placement manifests, Merkle inclusion proofs, and FSEnvelopeRegistry attestations on Base to produce evidence that is:

  • Legally grounded: built around identity, intent, association, and timestamps frameworks like ESIGN and eIDAS expect
  • Independently verifiable: compliance bundle + public chain data, no API key required
  • Tamper-evident: any change to fields, assignments, or completions breaks the hash chain
  • Privacy-preserving: document plaintext is never written on-chain; the bundle does not expose encrypted bytes

The problem with traditional e-signatures

Traditional platforms optimize for convenience, not portability of proof:

  1. Platform risk: If the vendor shuts down, verification may become impossible
  2. Database tampering: An insider could alter records without external detection
  3. No field-level granularity: You can prove who signed, but not which fields they completed
  4. Vendor lock-in: Exports often lose the verification context

We needed a foundation where math and chain state, not our Postgres row, carry the proof.

Core architecture: placement manifests + Merkle trees

At the heart of the system is the Placement Manifest, a cryptographic commitment to exactly where and what each signer must sign.

What is a placement manifest?

When a sender prepares a document, they place signature fields on specific pages at specific coordinates. The manifest captures:

  • Field ID (unique identifier)
  • Page index (0-based)
  • Normalized rectangle (x, y, width, height as fractions of page dimensions)
  • Assigned signer (Ethereum address)
  • Required/optional flag
  • Field type (signature, initial, date, etc.)
interface PlacementManifest {
version: 1;
fields: Array<{
id: string;
pageIndex: number;
rect: {
x: number;
y: number;
width: number;
height: number;
};
assignedSigner: `0x${string}`;
required: boolean;
type: "signature" | "initial" | "date" | "name" | "email" | "text" | "checkbox";
}>;
}

The manifest is serialized to canonical JSON (sorted keys, no whitespace) and hashed to create the placement commitment, a bytes32 value stored on-chain. Any change to field positions, assignments, or requirements changes the hash.

The signer's Merkle root

When a signer completes their fields, we compute a Merkle root of their completed field IDs:

  1. Collect all field IDs the signer marked complete (sorted, deduplicated)
  2. Compute each leaf as keccak256(abi.encode(fieldId, placementCommitment, cidIdentifier, signerAddress))
  3. Build a standard Merkle tree (sorted pairwise hashing)
  4. Store the root on-chain with their signature attestation

This creates field-level accountability. A verifier can later ask: "Did signer 0x123… specifically complete field sig-abc on this document?" The Merkle proof provides cryptographic evidence without revealing other signers' completions.

leaf = keccak256(abi.encode(
fieldId,
placementCommitment,
cidIdentifier, // keccak256(pieceCid), not the raw CID on-chain
signerAddress
))

The compliance bundle: a complete audit record

For legal proceedings or compliance audits, we generate a Compliance Bundle, a server-built JSON document containing everything needed to verify the signing ceremony:

interface ComplianceBundle {
version: 5;
pieceCid: string;
chainId: number;
exportedAtIso: string;
executionStatus: "fully_executed" | "partially_executed";
placementCommitment: `0x${string}`;
placementManifest: PlacementManifest;
registration: {
sender: `0x${string}`;
registrationTxHash: `0x${string}`;
createdAtIso: string;
};
parties: Array<{
role: "sender" | "signer" | "viewer";
wallet: `0x${string}`;
email: string;
displayName: string | null;
emailCommitment: `0x${string}`;
authSubjectCommitment: `0x${string}` | null;
}>;
onchainRegistration: {
cidIdentifier: `0x${string}`;
sender: `0x${string}`;
signersCommitment: `0x${string}`;
viewersCommitment: `0x${string}`;
placementCommitment: `0x${string}`;
senderEmailCommitment: `0x${string}`;
senderAuthSubjectCommitment: `0x${string}`;
signersCount: number;
signaturesCount: number;
timestamp: string;
} | null;
transactions: Array<{
kind: "file_registered" | "file_signed" | "payout_executed";
txHash: `0x${string}`;
chainId: number;
contractAddress: `0x${string}`;
summary: string;
relatedAddresses: `0x${string}`[];
blockNumber: number | null;
timestamp: number | null;
fetchedAtIso: string | null;
}>;
signers: Array<{
wallet: `0x${string}`;
displayName: string | null;
email: string | null;
signed: boolean;
assignedFieldIds: string[];
requiredFieldIds: string[];
optionalFieldIds: string[];
onchainTxHash: `0x${string}` | null;
signedAtIso: string | null;
messageTimestampIso: string | null;
blockTimestampFromTx: number | null;
completedFieldIds: string[];
completionsRoot: `0x${string}` | null;
leafSchemaVersion: number | null;
merkleProofs: Array<{
fieldId: string;
leafHash: `0x${string}`;
leafIndex: number;
siblings: `0x${string}`[];
}>;
draftCompletedFieldIds: string[];
}>;
settlements: Array<{
onChainRuleId: string;
recipientWallet: `0x${string}`;
tokenAddress: `0x${string}`;
amount: string;
releaseType: string;
status: string;
registerRuleTxHash: `0x${string}`;
approveTxHash: `0x${string}`;
payoutTxHash: `0x${string}` | null;
executedAtIso: string | null;
lastError: string | null;
}>;
offChainEvidence: {
acknowledgements: Array<{
wallet: `0x${string}`;
createdAtIso: string;
emailCommitment: `0x${string}`;
authSubjectCommitment: `0x${string}` | null;
ackSha256: `0x${string}` | null;
}>;
};
}

Bundle integrity

The bundle is canonicalized (sorted keys recursively) and hashed with SHA-256. The server stores this hash in compliance_export_logs with export metadata. That gives you a timestamped record of what was exported, when, and by whom, separate from the on-chain attestations themselves.

On-chain registration flow

When a signer finalizes, the following happens:

  1. Client-side preparation:

    • Compute Merkle root of completed field IDs
    • Generate Dilithium (post-quantum) signature commitment for the signing message
    • Produce EIP-712 typed data signature for the relay transaction
  2. Server validation:

    • Verify signatures cryptographically
    • Check required fields are present in the Merkle tree
    • Ensure field IDs match the signer's assigned fields in the manifest
    • Simulate the on-chain transaction (catches revert conditions)
  3. On-chain registration (relayer pool relay, onlyRelayer):

    function registerFileSignature(
    string calldata pieceCid_,
    address sender_,
    address signerWallet_,
    bytes32 signerEmailCommitment_,
    bytes32 authSubjectCommitment_,
    bytes20 dl3SignatureCommitment_,
    uint256 timestamp_,
    bytes calldata signature_,
    bytes32 completionsRoot_,
    uint8 leafSchemaVersion_
    ) external onlyRelayer
  4. Database persistence:

    • Store completedFieldIds, completionsRoot, onchainTxHash in file_signatures
    • Clear draft state after finalization

Independent verification (no trust required)

Given the Compliance Bundle and public blockchain data, a verifier can:

  1. Verify the manifest: Recompute placementCommitment from manifest JSON; compare to on-chain value
  2. Verify field completion: For any field ID, use the Merkle proof to verify inclusion in completionsRoot
  3. Verify the attestation: Confirm completionsRoot was registered for the claimed signer in FSEnvelopeRegistry
  4. Verify timestamp: Cross-reference the on-chain transaction block time

All of this requires:

  • The Compliance Bundle JSON
  • An EVM RPC endpoint (any public node)
  • Standard cryptographic libraries

No Filosign API keys. No platform login. The server builds the bundle for convenience; verification is math + chain.

Privacy architecture

  • CID hash on-chain: We store keccak256(pieceCid) in public logs, not the raw Content ID. Only parties with the original CID can fetch encrypted bytes from storage.
  • Encrypted metadata: Document names, signer emails, and other PII are not written on-chain.
  • Selective disclosure: Merkle proofs attest to specific field completions without exposing unrelated signers' selections in the proof path.
RequirementHow we satisfy it
Identity authenticationWallet signatures prove private key ownership; optional social login links via commitments
Intent to signEIP-712 typed data shows what is being signed; Dilithium adds post-quantum intent binding
Association with documentplacementCommitment and cidIdentifier bind signatures to a specific document
TimestampingBlock timestamp on FileSigned is immutable and publicly auditable
Tamper detectionManifest or completion changes break Merkle proofs or on-chain hash checks
Audit trailCompliance bundles provide exportable, hash-stamped records
Long-term validityPost-quantum signatures reduce dependence on ECDSA alone for document intent

Conclusion

We rebuilt the signing flow with a simple principle: cryptography first, platform second. Filosign's servers assemble bundles and relay transactions, but the legal weight sits in hashes, Merkle proofs, and FSEnvelopeRegistry state that anyone can re-check.

Your documents stay encrypted. Your field-level completions stay provably yours. And anyone with the bundle and an RPC endpoint can verify that.


Try it yourself: The next time you sign on Filosign, export the Compliance Bundle and walk through a Merkle proof against Base. The math does not depend on us.