Skip to content

GDPR Smart Contract

Visual specification of the Plutus validator that governs a data controller's compliance trie. Three complementary diagram types: UTxO state transitions (what the chain sees), the validator guard table (what the contract checks), and lifecycle state machines (how compliance records evolve).

The UTxO landscape

The validator governs one UTxO — the controller's compliance trie root. It reads two reference inputs: the identity trie (attested actor keys) and the regulation trie (controller standing, certification body qualifications).

graph LR
    subgraph Reference Inputs
        ID_UTxO["Identity UTxO<br/><b>identity trie root</b><br/><i>attested actor keys</i>"]
        REG_UTxO["Regulation UTxO<br/><b>regulation trie root</b><br/><i>controller standing<br/>cert body qualifications</i>"]
    end

    subgraph Spent + Produced
        TRIE_IN["Compliance UTxO<br/><b>root hash₁</b><br/><i>datum: trie state</i>"]
        TX{{"Transaction<br/><i>redeemer action</i>"}}
        TRIE_OUT["Compliance UTxO'<br/><b>root hash₂</b><br/><i>datum: trie state'</i>"]
    end

    TRIE_IN -->|spent| TX
    TX -->|produced| TRIE_OUT
    ID_UTxO -.->|reference| TX
    REG_UTxO -.->|reference| TX

    subgraph Signatures Required
        SIG_PROC["Process signature<br/><i>signing function</i>"]
        SIG_ACTOR["Actor signature<br/><i>baton holder</i>"]
    end

    SIG_PROC -.-> TX
    SIG_ACTOR -.-> TX

Every transaction follows this shape. What changes between redeemer actions is which guards the validator checks, what the datum transformation must look like, and which signatures are required.

Redeemer actions

The contract recognises ten redeemer actions, grouped by GDPR obligation.

graph TB
    subgraph "Breach Notification (Art 33–34)"
        R1["CommitBreach"]
        R2["SubmitBreachNotification"]
    end

    subgraph "Data Subject Rights (Art 12–22)"
        R3["CommitRightsRequest"]
        R4["SubmitRightsResponse"]
        R5["ExtendRightsDeadline"]
    end

    subgraph "Consent (Art 7)"
        R6["RecordConsent"]
        R7["WithdrawConsent"]
    end

    subgraph "Processing Records (Art 30)"
        R8["UpdateProcessingRecord"]
    end

    subgraph "Certification (Art 42)"
        R9["ReferenceCertification"]
    end

    subgraph "Impact Assessment (Art 35)"
        R10["RecordDPIA"]
    end

UTxO state transitions per action

CommitBreach

The controller detects a breach and anchors the detection time on-chain. This is the first half of the 72-hour protocol.

graph LR
    subgraph Input
        IN["Compliance UTxO<br/><b>datum:</b><br/>breach_commitment: None"]
    end

    subgraph Transaction
        TX{{"CommitBreach<br/><br/>redeemer:<br/>leaf_key, breach_category"}}
    end

    subgraph Output
        OUT["Compliance UTxO'<br/><b>datum:</b><br/>breach_commitment:<br/>  commit_slot: current_slot<br/>  deadline: current_slot + 72h<br/>  category: breach_category"]
    end

    IN --> TX --> OUT

    subgraph Guards
        G1["controller key ∈ identity trie"]
        G2["controller standing ≠ suspended<br/>(regulation trie)"]
        G3["leaf has no existing<br/>breach commitment"]
        G4["controller signature present"]
    end

    G1 -.-> TX
    G2 -.-> TX
    G3 -.-> TX
    G4 -.-> TX

SubmitBreachNotification

The controller submits the notification hash, clearing the commitment. The chain records how many slots elapsed between commitment and submission.

graph LR
    subgraph Input
        IN["Compliance UTxO<br/><b>datum:</b><br/>breach_commitment:<br/>  commit_slot: S₁<br/>  deadline: S₁ + 72h<br/>  category: C"]
    end

    subgraph Transaction
        TX{{"SubmitBreachNotification<br/><br/>redeemer:<br/>leaf_key, notification_hash"}}
    end

    subgraph Output
        OUT["Compliance UTxO'<br/><b>datum:</b><br/>breach_commitment: None<br/>breach_record:<br/>  commit_slot: S₁<br/>  submit_slot: current_slot<br/>  notification_hash: H<br/>  timely: current_slot ≤ S₁ + 72h"]
    end

    IN --> TX --> OUT

    subgraph Guards
        G1["breach commitment exists"]
        G2["notification_hash ≠ empty"]
        G3["controller signature present"]
        G4["current_slot within tx validity range"]
    end

    G1 -.-> TX
    G2 -.-> TX
    G3 -.-> TX
    G4 -.-> TX

Timeliness is recorded, not enforced

The validator does not reject late notifications. It records timely: false when current_slot > deadline. The SA reads this flag — late submission is evidence for enforcement proceedings, not a transaction failure. A controller must be able to submit late rather than not submit at all.

CommitRightsRequest

A data subject submits a rights request. The controller anchors the receipt time.

graph LR
    subgraph Input
        IN["Compliance UTxO<br/><b>datum:</b><br/>rights_commitment: None"]
    end

    subgraph Transaction
        TX{{"CommitRightsRequest<br/><br/>redeemer:<br/>leaf_key, request_type,<br/>request_hash"}}
    end

    subgraph Output
        OUT["Compliance UTxO'<br/><b>datum:</b><br/>rights_commitment:<br/>  commit_slot: current_slot<br/>  deadline: current_slot + 1month<br/>  request_type: access|rectify|erase|...<br/>  request_hash: H"]
    end

    IN --> TX --> OUT

    subgraph Guards
        G1["controller key ∈ identity trie"]
        G2["leaf has no existing<br/>rights commitment"]
        G3["request_hash includes<br/>process signature<br/>(data subject signed)"]
        G4["controller signature present"]
    end

    G1 -.-> TX
    G2 -.-> TX
    G3 -.-> TX
    G4 -.-> TX

SubmitRightsResponse

The controller responds within the deadline window.

graph LR
    subgraph Input
        IN["Compliance UTxO<br/><b>datum:</b><br/>rights_commitment:<br/>  commit_slot: S₁<br/>  deadline: S₁ + 1month<br/>  request_type: T"]
    end

    subgraph Transaction
        TX{{"SubmitRightsResponse<br/><br/>redeemer:<br/>leaf_key, response_hash"}}
    end

    subgraph Output
        OUT["Compliance UTxO'<br/><b>datum:</b><br/>rights_commitment: None<br/>rights_record:<br/>  commit_slot: S₁<br/>  response_slot: current_slot<br/>  request_type: T<br/>  response_hash: H<br/>  timely: current_slot ≤ deadline"]
    end

    IN --> TX --> OUT

    subgraph Guards
        G1["rights commitment exists"]
        G2["response_hash ≠ empty"]
        G3["controller signature present"]
    end

    G1 -.-> TX
    G2 -.-> TX
    G3 -.-> TX

ExtendRightsDeadline

For complex requests, Art 12(3) allows extension to 3 months. The controller must notify the subject within the first month.

graph LR
    subgraph Input
        IN["Compliance UTxO<br/><b>datum:</b><br/>rights_commitment:<br/>  deadline: S₁ + 1month"]
    end

    subgraph Transaction
        TX{{"ExtendRightsDeadline<br/><br/>redeemer:<br/>leaf_key,<br/>extension_reason_hash"}}
    end

    subgraph Output
        OUT["Compliance UTxO'<br/><b>datum:</b><br/>rights_commitment:<br/>  deadline: S₁ + 3months<br/>  extended: true<br/>  extension_slot: current_slot<br/>  reason_hash: H"]
    end

    IN --> TX --> OUT

    subgraph Guards
        G1["rights commitment exists"]
        G2["not already extended"]
        G3["current_slot ≤ original deadline<br/>(must extend before expiry)"]
        G4["extension_reason_hash ≠ empty"]
        G5["controller signature present"]
    end

    G1 -.-> TX
    G2 -.-> TX
    G3 -.-> TX
    G4 -.-> TX
    G5 -.-> TX

RecordConsent

Consent given for a specific processing purpose.

graph LR
    subgraph Input
        IN["Compliance UTxO<br/><b>datum:</b><br/>consent leaf: absent<br/>or consent_state: withdrawn"]
    end

    subgraph Transaction
        TX{{"RecordConsent<br/><br/>redeemer:<br/>leaf_key, purpose_hash,<br/>consent_record_hash"}}
    end

    subgraph Output
        OUT["Compliance UTxO'<br/><b>datum:</b><br/>consent leaf:<br/>  consent_state: given<br/>  purpose_hash: P<br/>  record_hash: H<br/>  given_slot: current_slot"]
    end

    IN --> TX --> OUT

    subgraph Guards
        G1["controller key ∈ identity trie"]
        G2["consent_record_hash includes<br/>process signature<br/>(data subject signed)"]
        G3["purpose_hash ≠ empty"]
        G4["if leaf exists: consent_state<br/>must be withdrawn or absent"]
        G5["controller signature present"]
    end

    G1 -.-> TX
    G2 -.-> TX
    G3 -.-> TX
    G4 -.-> TX
    G5 -.-> TX

WithdrawConsent

Consent withdrawn. Must be as easy as giving it (Art 7(3)).

graph LR
    subgraph Input
        IN["Compliance UTxO<br/><b>datum:</b><br/>consent leaf:<br/>  consent_state: given<br/>  purpose_hash: P<br/>  given_slot: S₁"]
    end

    subgraph Transaction
        TX{{"WithdrawConsent<br/><br/>redeemer:<br/>leaf_key,<br/>withdrawal_record_hash"}}
    end

    subgraph Output
        OUT["Compliance UTxO'<br/><b>datum:</b><br/>consent leaf:<br/>  consent_state: withdrawn<br/>  purpose_hash: P<br/>  given_slot: S₁<br/>  withdrawn_slot: current_slot<br/>  withdrawal_hash: H"]
    end

    IN --> TX --> OUT

    subgraph Guards
        G1["consent leaf exists"]
        G2["consent_state = given"]
        G3["withdrawal_record_hash includes<br/>process signature<br/>(data subject signed)"]
        G4["controller signature present"]
    end

    G1 -.-> TX
    G2 -.-> TX
    G3 -.-> TX
    G4 -.-> TX

UpdateProcessingRecord

Art 30 processing activity records — purposes, categories, recipients, transfers, retention periods.

graph LR
    subgraph Input
        IN["Compliance UTxO<br/><b>datum:</b><br/>processing_record leaf:<br/>  record_hash: H₁<br/>  updated_slot: S₁"]
    end

    subgraph Transaction
        TX{{"UpdateProcessingRecord<br/><br/>redeemer:<br/>leaf_key,<br/>new_record_hash"}}
    end

    subgraph Output
        OUT["Compliance UTxO'<br/><b>datum:</b><br/>processing_record leaf:<br/>  record_hash: H₂<br/>  updated_slot: current_slot<br/>  previous_hash: H₁"]
    end

    IN --> TX --> OUT

    subgraph Guards
        G1["controller key ∈ identity trie"]
        G2["controller standing ≠ suspended"]
        G3["new_record_hash ≠ old record_hash"]
        G4["controller signature present"]
    end

    G1 -.-> TX
    G2 -.-> TX
    G3 -.-> TX
    G4 -.-> TX

ReferenceCertification

Controller references a valid Art 42 certification token.

graph LR
    subgraph Input
        IN["Compliance UTxO<br/><b>datum:</b><br/>certification_ref: None<br/>or expired"]
    end

    subgraph Transaction
        TX{{"ReferenceCertification<br/><br/>redeemer:<br/>cert_token_ref"}}
    end

    subgraph "Additional Reference Input"
        CERT["Certification Token UTxO<br/><b>datum:</b><br/>  issuer: cert_body_key<br/>  expiry_slot: S_exp<br/>  scope_hash: H"]
    end

    subgraph Output
        OUT["Compliance UTxO'<br/><b>datum:</b><br/>certification_ref:<br/>  token_ref: cert_token_ref<br/>  issuer: cert_body_key<br/>  expiry_slot: S_exp<br/>  referenced_slot: current_slot"]
    end

    IN --> TX --> OUT
    CERT -.->|reference| TX

    subgraph Guards
        G1["cert token exists at<br/>referenced UTxO"]
        G2["issuer key ∈ regulation trie<br/>(qualified cert body)"]
        G3["expiry_slot > current_slot<br/>(not expired)"]
        G4["controller signature present"]
    end

    G1 -.-> TX
    G2 -.-> TX
    G3 -.-> TX
    G4 -.-> TX

RecordDPIA

DPIA attestation anchored with timestamp.

graph LR
    subgraph Input
        IN["Compliance UTxO<br/><b>datum:</b><br/>dpia leaf: absent<br/>or previous assessment"]
    end

    subgraph Transaction
        TX{{"RecordDPIA<br/><br/>redeemer:<br/>leaf_key,<br/>dpia_hash"}}
    end

    subgraph Output
        OUT["Compliance UTxO'<br/><b>datum:</b><br/>dpia leaf:<br/>  dpia_hash: H<br/>  recorded_slot: current_slot<br/>  previous_hash: H₁ or None"]
    end

    IN --> TX --> OUT

    subgraph Guards
        G1["controller key ∈ identity trie"]
        G2["dpia_hash ≠ empty"]
        G3["controller signature present"]
    end

    G1 -.-> TX
    G2 -.-> TX
    G3 -.-> TX

Validator guard table

All guards across all redeemer actions in a single matrix. This is the complete specification of what the validator checks.

Guard CommitBreach SubmitBreach CommitRights SubmitRights ExtendRights RecordConsent WithdrawConsent UpdateRecord RefCert RecordDPIA
Controller key ∈ identity trie x x x x x
Controller standing ≠ suspended x x
No existing commitment on leaf x x
Commitment exists on leaf x x x
Hash ≠ empty x x x x x x
Process sig (data subject signed) x x x
Controller signature x x x x x x x x x x
Consent state = given x
Consent state ≠ given (or absent) x
Not already extended x
Current slot ≤ original deadline x
Cert token exists + not expired x
Cert issuer ∈ regulation trie x
Root hash consistent (MPT) x x x x x x x x x x

Universal guards

Two guards apply to every action: controller signature and MPT root hash consistency. The controller must sign every transaction (they are the operator). The MPT root hash must be recomputed correctly after the leaf update (structural integrity of the trie).

Lifecycle state machines

Breach notification lifecycle

stateDiagram-v2
    [*] --> NoBreach: normal operations

    NoBreach --> BreachDetected: CommitBreach<br/>(slot recorded)

    BreachDetected --> NotifiedTimely: SubmitBreachNotification<br/>(slot ≤ deadline)
    BreachDetected --> NotifiedLate: SubmitBreachNotification<br/>(slot > deadline)

    NotifiedTimely --> [*]: record closed<br/>(timely: true)
    NotifiedLate --> [*]: record closed<br/>(timely: false)

    note right of BreachDetected
        72h countdown starts
        SA can observe the
        commitment on-chain
    end note

    note right of NotifiedLate
        Late submission is evidence
        for enforcement — not a
        transaction rejection
    end note

Rights request lifecycle

stateDiagram-v2
    [*] --> NoRequest: normal operations

    NoRequest --> RequestReceived: CommitRightsRequest<br/>(slot recorded, type tagged)

    RequestReceived --> Extended: ExtendRightsDeadline<br/>(complex request, max 3 months)
    RequestReceived --> RespondedTimely: SubmitRightsResponse<br/>(slot ≤ 1 month deadline)
    RequestReceived --> RespondedLate: SubmitRightsResponse<br/>(slot > deadline)

    Extended --> RespondedTimely: SubmitRightsResponse<br/>(slot ≤ extended deadline)
    Extended --> RespondedLate: SubmitRightsResponse<br/>(slot > extended deadline)

    RespondedTimely --> [*]: record closed<br/>(timely: true)
    RespondedLate --> [*]: record closed<br/>(timely: false)

    note right of RequestReceived
        1-month countdown starts
        request_type: access, rectify,
        erase, restrict, port, object
    end note

    note right of Extended
        Extension must happen
        within original deadline
        and only once
    end note
stateDiagram-v2
    [*] --> NoConsent: no consent for this purpose

    NoConsent --> Given: RecordConsent<br/>(purpose hash + subject signature)
    Given --> Withdrawn: WithdrawConsent<br/>(subject signature)
    Withdrawn --> Given: RecordConsent<br/>(new consent event)

    note right of Given
        On-chain: purpose_hash,
        record_hash, given_slot
        Off-chain: actual consent
        form, subject identity
    end note

    note right of Withdrawn
        Triggers off-chain obligations:
        cease processing, consider
        erasure of data processed
        under this consent
    end note

Certification lifecycle

stateDiagram-v2
    [*] --> NoCert: no certification

    NoCert --> Active: ReferenceCertification<br/>(cert token valid)
    Active --> Expired: expiry_slot reached<br/>(no transaction needed)
    Active --> NoCert: cert body revokes token<br/>(token burned externally)
    Expired --> Active: ReferenceCertification<br/>(renewed token)
    Expired --> NoCert: certification lapses

    note right of Active
        Token referenced in
        compliance trie. Must be
        re-checked at each use:
        expiry_slot > current_slot
    end note

Processing record lifecycle

stateDiagram-v2
    [*] --> Created: UpdateProcessingRecord<br/>(initial Art 30 record)

    Created --> Updated: UpdateProcessingRecord<br/>(new hash, previous linked)
    Updated --> Updated: UpdateProcessingRecord<br/>(chain of hashes)

    note right of Updated
        Each update links to
        previous_hash — the trie
        preserves the full history
        of Art 30 record changes
    end note

Composite view: a controller's compliance trie

The trie is a Merkle Patricia Trie where each leaf is a compliance record. Different leaf types coexist under the same root.

graph TD
    ROOT["Compliance Trie Root<br/><b>root_hash</b>"]

    ROOT --- CONSENT["consent/<br/>purpose-marketing"]
    ROOT --- CONSENT2["consent/<br/>purpose-analytics"]
    ROOT --- BREACH["breach/<br/>2026-03-15"]
    ROOT --- RIGHTS["rights/<br/>req-00142"]
    ROOT --- ART30["processing/<br/>customer-crm"]
    ROOT --- ART30B["processing/<br/>payroll"]
    ROOT --- DPIA["dpia/<br/>crm-assessment"]
    ROOT --- CERT["certification/<br/>iso-27701"]
    ROOT --- PROC["processor/<br/>cloud-provider-x"]

    CONSENT --- CL1["consent_state: given<br/>purpose_hash: 0xab...<br/>given_slot: 48201000"]
    CONSENT2 --- CL2["consent_state: withdrawn<br/>purpose_hash: 0xcd...<br/>withdrawn_slot: 49100200"]
    BREACH --- BL["commit_slot: 50002100<br/>submit_slot: 50004800<br/>timely: true<br/>notification_hash: 0xef..."]
    RIGHTS --- RL["commit_slot: 49800000<br/>response_slot: 49843200<br/>request_type: access<br/>timely: true"]
    ART30 --- AL["record_hash: 0x12...<br/>updated_slot: 49500000<br/>previous_hash: 0x34..."]
    DPIA --- DL["dpia_hash: 0x56...<br/>recorded_slot: 48000000"]
    CERT --- CTL["token_ref: cert-utxo-id<br/>expiry_slot: 51000000<br/>issuer: cert-body-key"]
    PROC --- PL["agreement_hash: 0x78...<br/>recorded_slot: 48100000"]

The key path structure (consent/, breach/, rights/, processing/, dpia/, certification/, processor/) is a convention — the validator identifies leaf type by the redeemer action, not by the key path. The path exists for off-chain tooling and auditor readability.

The full transaction: breach notification example

End-to-end UTxO flow for the flagship 72-hour protocol, showing both transactions.

sequenceDiagram
    participant C as Controller
    participant V as Validator
    participant IT as Identity Trie
    participant RT as Regulation Trie

    rect rgb(60, 60, 80)
    Note over C,RT: Transaction 1: CommitBreach

    C->>V: Spend compliance UTxO<br/>redeemer: CommitBreach(leaf_key, category)
    V->>IT: Read reference: controller key present?
    IT-->>V: ✓ attested
    V->>RT: Read reference: controller standing?
    RT-->>V: ✓ not suspended
    V->>V: Check: no existing breach commitment on leaf
    V->>V: Check: controller signature present
    V->>V: Check: new root hash consistent
    V-->>C: ✓ Produce compliance UTxO'<br/>datum: breach_commitment =<br/>  commit_slot: 50002100<br/>  deadline: 50005700 (72h)
    end

    Note over C: Off-chain: investigate breach,<br/>prepare notification to SA<br/>(nature, categories, numbers,<br/>consequences, measures taken)

    rect rgb(60, 80, 60)
    Note over C,RT: Transaction 2: SubmitBreachNotification

    C->>V: Spend compliance UTxO'<br/>redeemer: SubmitBreachNotification(leaf_key, hash)
    V->>V: Check: breach commitment exists
    V->>V: Check: notification_hash ≠ empty
    V->>V: Check: controller signature present
    V->>V: Compute: timely = (current_slot ≤ 50005700)
    V->>V: Check: new root hash consistent
    V-->>C: ✓ Produce compliance UTxO''<br/>datum: breach_commitment = None<br/>  breach_record =<br/>    commit_slot: 50002100<br/>    submit_slot: 50004800<br/>    notification_hash: 0xef...<br/>    timely: true
    end

    Note over C: SA reads the trie:<br/>breach detected at slot 50002100<br/>notified at slot 50004800<br/>elapsed: 2700 slots (~67.5h)<br/>timely: ✓

Sources

The validator design follows patterns established in the framework:

For GDPR article references, see the GDPR case study sources.

For eUTxO diagram conventions, see: