📜 Notary
This guide explains how you could implement a Notary contract on pod.
The Notary smart contract provides a timestamping service for documents. By submitting a document hash and an associated timestamp, users can create an immutable record that attests to the document’s existence at or before a certain time. Importantly, this mechanism proves the age of a document but does not make any assertions about its ownership or authorship.
Notary Contract
Features
Immutable Timestamping: Once a document is timestamped, the earliest submitted valid timestamp is retained.
Multiple Submissions: Re-submissions of the same document hash are permitted and can only reduce (never increase) the timestamp.
Conflict Resolution via Minimum Timestamp: Competing transactions for the same document hash are resolved by storing the smallest valid timestamp.
Smart contract interface
State
- Maps a document’s keccak256 hash to the smallest timestamp submitted.
- Public visibility allows external verification of timestamps.
Events
- Emitted whenever a document hash is timestamped or updated with an earlier timestamp.
Functions
Parameters
documentHash: Keccak256 hash of the document content.ts: Proposed timestamp, which must be in the future at validation time.
Behavior
- Requires
tsto be in the future (requireTimeBefore(ts, ...)). - If the document has not been timestamped before, sets
timestamps[documentHash] = ts. - If the document is already timestamped, updates it to the minimum of the existing and the new timestamp.
- Emits
DocumentTimestampedevent.
🧠 pod caveats
Execution Model in pod network
The pod execution environment does not guarantee a total order of transactions signed by different signers. Instead:
- Transactions signed by the same signer are strictly ordered via their nonce.
- Transactions signed by different signers may be executed in different orders on different validators, leading to temporary state divergence across the network.
This property can lead to the following behavior in the context of the Notary contract:
- If multiple users simultaneously attempt to timestamp the same document with different timestamps, validators might execute these transactions in different orders.
- Consequently, different validators may temporarily hold different
timestamps[documentHash]values depending on execution order.
Consistency Mechanism
Despite this temporary divergence, pod ensures eventual consistency:
- As all validators eventually process the same set of transactions, the state converges to a single consistent result.
- The contract is designed to accommodate this by always applying the minimum timestamp, ensuring that final state across validators aligns.
Attestation vs. Execution
The requireTimeBefore function executes only during the attestation stage, not during execution:
- Attestation Stage: Ensures the timestamp is in the future at the time of submission. The transaction is decided to be valid in the eyes of each validator separately and signed, making it eligible for execution in the second stage.
- Execution Stage: The contract updates state without re-checking
requireTimeBefore. This is intentional so that all validators execute the transaction if the quorum attested its validity no matter when the transaction is actually executed.
Security Considerations
- This contract does not validate ownership of the document.
- Users could timestamp arbitrary data, so clients must verify document authenticity through external means.
Interacting with the contract
A Rust-based client can interact with the Notary contract via the pod-sdk, which includes utilities for constructing
transactions, querying state, and subscribing to events.
Generating bindings
Bindings can be auto-generated using the
alloy-sol-types::sol! macro. It needs to be
passed path to the smart contract file.
Timestamping a document
Here’s how a document can be timestamped.
First, we need to create a PodProvider and an instance of the Notary contract client.
Then, the document is hashed using keccak256 and sent in a transaction calling the timestamp() function.
Finally, we wait for the past perfect time after the timestamp to make sure its value settled and will never change (ensure finality).
Retrieving a document timestamp
In order to get a timestamp we need to create a PodProvider again, but this time it doesn’t need a private key. The
reason is because we will not be executing a transaction, but only calling a contract method, which can happen on a node only (it never reaches a validator).
A timestamp equal to zero means that the document is not timestamped
Watching DocumentTimestamped event
The watch() function demonstrates how to subscribe to DocumentTimestamped events.
First, it creates a filter for the DocumentTimestamped event.
It then optionally filters by a specific submitter address.
It subscribes to events with subscribe_verifiable_logs() to receive logs with validator attestations.
The received logs are decoded and verified each event against the committee retrieved from the node.
It finally prints the resulting timestamp, document hash, and submitter address.