Skip to content

Not legal advice

dpdpkit is software that helps you implement obligations under India's Digital Personal Data Protection Act, 2023 and the DPDP Rules, 2025. It does not provide legal advice and does not guarantee compliance.

Ledger

ledger

Append-only, hash-chained consent ledger and audit trail.

Each tenant has two chains: consent (grant/deny/withdraw events) and audit (every other state change). A row's hash is SHA-256 over its canonical JSON (all fields except hash), which includes prev_hash and seq. Editing a row breaks its own hash; deleting one breaks seq continuity; truncating the tail is caught by comparing with an exported RootHash.

Root hashes are only useful if they are kept somewhere the operator cannot rewrite. Call Ledger.export_root on a schedule with a RootSink, and/or subscribe a webhook to the ledger.root_exported event so a third party holds a copy.

GENESIS module-attribute

GENESIS = '0' * 64

Ledger

Ledger(kit: Kit)
append_consent(*, principal: str, purpose: str, action: ConsentAction, notice_version: int | None, notice_hash: str | None, locale: str | None, source: str, metadata: dict[str, Any] | None = None) -> ConsentEvent

audit

audit(action: str, *, subject_type: str, subject_id: str, actor: str = 'system', data: dict[str, Any] | None = None) -> AuditEntry

consent_events

consent_events(principal: str | None = None) -> Iterator[ConsentEvent]

audit_entries

audit_entries(subject_id: str | None = None) -> Iterator[AuditEntry]

verify

verify() -> LedgerVerification

Check both chains. Raises LedgerTampered naming the first bad row.

verify_report

verify_report() -> LedgerVerification

Like verify but returns a failed report instead of raising.

root_hash

root_hash() -> RootHash

Current heads of both chains, for periodic export to somewhere the operator cannot rewrite.

export_root

export_root(*sinks: RootSink) -> RootHash

Compute the current root hash, write it to each sink and publish ledger.root_exported.

A sink that fails raises, so a scheduler surfaces the error instead of silently skipping an anchor.

check_root

check_root(anchor: RootHash) -> None

Confirm the chains still contain the rows an earlier root export covered.

Because every row hashes its predecessor, checking the newest anchor also covers every older one.

RootSink

Bases: Protocol

Somewhere exported root hashes are kept: a write-once bucket, a transparency log, a partner's API.

write

write(root: RootHash) -> None

FileRootSink

FileRootSink(path: str | Path)

Appends one JSON line per export. Point it at storage you cannot rewrite (an S3 bucket with Object Lock, a WORM volume, a mounted share owned by someone else); a local file only protects against accidents.

path instance-attribute

path = Path(path)

write

write(root: RootHash) -> None

read

read() -> list[RootHash]

latest

latest(tenant_id: str) -> RootHash | None

row_hash

row_hash(row: ConsentEvent | AuditEntry) -> str