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:
- Platform risk: If the vendor shuts down, verification may become impossible
- Database tampering: An insider could alter records without external detection
- No field-level granularity: You can prove who signed, but not which fields they completed
- 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,heightas 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:
- Collect all field IDs the signer marked complete (sorted, deduplicated)
- Compute each leaf as
keccak256(abi.encode(fieldId, placementCommitment, cidIdentifier, signerAddress)) - Build a standard Merkle tree (sorted pairwise hashing)
- 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:
-
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
-
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)
-
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 -
Database persistence:
- Store
completedFieldIds,completionsRoot,onchainTxHashinfile_signatures - Clear draft state after finalization
- Store
Independent verification (no trust required)
Given the Compliance Bundle and public blockchain data, a verifier can:
- Verify the manifest: Recompute
placementCommitmentfrom manifest JSON; compare to on-chain value - Verify field completion: For any field ID, use the Merkle proof to verify inclusion in
completionsRoot - Verify the attestation: Confirm
completionsRootwas registered for the claimed signer inFSEnvelopeRegistry - 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.
Compliance and legal framework
| Requirement | How we satisfy it |
|---|---|
| Identity authentication | Wallet signatures prove private key ownership; optional social login links via commitments |
| Intent to sign | EIP-712 typed data shows what is being signed; Dilithium adds post-quantum intent binding |
| Association with document | placementCommitment and cidIdentifier bind signatures to a specific document |
| Timestamping | Block timestamp on FileSigned is immutable and publicly auditable |
| Tamper detection | Manifest or completion changes break Merkle proofs or on-chain hash checks |
| Audit trail | Compliance bundles provide exportable, hash-stamped records |
| Long-term validity | Post-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.