On‑Chain Medical Report Verification with `MedicalStringReportRegistry

Why medical reports need better verification

Most hospitals and diagnostic labs share medical reports as PDFs over email, WhatsApp, or web portals. Patients then forward these files to doctors, insurers, employers, or agencies. But PDFs and screenshots can be edited or faked, and email headers are easy to spoof.

Core problem: How can a verifier trust that a report is genuine, unaltered, and really issued by a specific lab at a specific time?

The MedicalStringReportRegistry smart contract solves this by putting a verifiable fingerprint of each report on‑chain, without exposing any sensitive medical data.


GitHub Repo; https://github.com/Vikash-8090-Yadav/cfxmedical
Demo Video; https://youtu.be/OAQTwbJ59A0?si=QzIZPxGdt9l0rFL8

What MedicalStringReportRegistry does (high level)

  • Stores fingerprints, not files:
    • reportHash: hash of the medical report (for example, a PDF or JSON document).
    • patientIdHash: hash of a patient identifier (MRN, national ID, or a pseudonymous ID).
  • Tracks issuer and time:
    • issuedBy: address of the lab or hospital system that issued the report.
    • issuedAt: on‑chain timestamp when the report was issued.
  • Supports revocation:
    • revoked: flag that marks a report as no longer valid.
  • Allows public verification:
    • Anyone can call verifyReport(reportHash) to confirm:
      • The report exists.
      • Who issued it.
      • When it was issued.
      • Whether it has been revoked.

In short, the contract acts as an on‑chain registry of medical report proofs that verifiers can trust.


The Solidity contract

Here is the complete medcical.sol contract:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract MedicalStringReportRegistry {

    struct Report {
        bytes32 reportHash;
        bytes32 patientIdHash;
        address issuedBy;
        uint256 issuedAt;
        bool revoked;
    }

    mapping(bytes32 => Report) private reports;

    event ReportIssued(
        bytes32 indexed reportHash,
        bytes32 indexed patientIdHash,
        address indexed issuedBy,
        uint256 issuedAt
    );

    event ReportRevoked(bytes32 indexed reportHash);

    modifier reportExists(bytes32 _reportHash) {
        require(reports[_reportHash].issuedAt != 0, "Report not found");
        _;
    }

    function issueReport(
        bytes32 _reportHash,
        bytes32 _patientIdHash
    ) external {

        require(reports[_reportHash].issuedAt == 0, "Report already exists");

        reports[_reportHash] = Report({
            reportHash: _reportHash,
            patientIdHash: _patientIdHash,
            issuedBy: msg.sender,
            issuedAt: block.timestamp,
            revoked: false
        });

        emit ReportIssued(
            _reportHash,
            _patientIdHash,
            msg.sender,
            block.timestamp
        );
    }

    function revokeReport(bytes32 _reportHash)
        external
        reportExists(_reportHash)
    {
        require(
            reports[_reportHash].issuedBy == msg.sender,
            "Only issuing lab can revoke"
        );

        reports[_reportHash].revoked = true;

        emit ReportRevoked(_reportHash);
    }

    function verifyReport(bytes32 _reportHash)
        external
        view
        reportExists(_reportHash)
        returns (
            bytes32 patientIdHash,
            address issuedBy,
            uint256 issuedAt,
            bool revoked
        )
    {
        Report memory report = reports[_reportHash];

        return (
            report.patientIdHash,
            report.issuedBy,
            report.issuedAt,
            report.revoked
        );
    }
}

Core data model: Report struct and storage

The contract revolves around a simple Report struct and a mapping:

struct Report {
    bytes32 reportHash;
    bytes32 patientIdHash;
    address issuedBy;
    uint256 issuedAt;
    bool revoked;
}

mapping(bytes32 => Report) private reports;
  • reportHash: a unique fingerprint of the full report content.
  • patientIdHash: a hash of the patient identifier (protects privacy).
  • issuedBy: the wallet address of the issuing lab or hospital.
  • issuedAt: timestamp (Unix time) recorded at issuance.
  • revoked: flag that marks whether the report is still valid.

The mapping lets you do constant‑time lookups: given a reportHash, you can retrieve its full on‑chain metadata.


Events and existence checks

event ReportIssued(
    bytes32 indexed reportHash,
    bytes32 indexed patientIdHash,
    address indexed issuedBy,
    uint256 issuedAt
);

event ReportRevoked(bytes32 indexed reportHash);

modifier reportExists(bytes32 _reportHash) {
    require(reports[_reportHash].issuedAt != 0, "Report not found");
    _;
}
  • ReportIssued: emitted whenever a new report is registered on‑chain.
  • ReportRevoked: emitted when an existing report is revoked.
  • reportExists modifier:
    • Ensures functions like revoke and verify can only run on an existing report.
    • Uses issuedAt != 0 as a simple existence check.

These events allow off‑chain services (indexers, UIs, analytics) to track all report issuances and revocations over time.


The most important code: issuing a report

This is the heart of the system—the function that turns an off‑chain report into an on‑chain, verifiable record.

function issueReport(
    bytes32 _reportHash,
    bytes32 _patientIdHash
) external {

    require(reports[_reportHash].issuedAt == 0, "Report already exists");

    reports[_reportHash] = Report({
        reportHash: _reportHash,
        patientIdHash: _patientIdHash,
        issuedBy: msg.sender,
        issuedAt: block.timestamp,
        revoked: false
    });

    emit ReportIssued(
        _reportHash,
        _patientIdHash,
        msg.sender,
        block.timestamp
    );
}

How it works:

  • Inputs:
    • _reportHash: hash of the full report.
    • _patientIdHash: hash of the patient identifier.
  • Uniqueness check:
    • require(reports[_reportHash].issuedAt == 0, "Report already exists");
    • Ensures the same reportHash cannot be issued twice (prevents accidental duplicates).
  • Storage:
    • Saves a new Report in the reports mapping.
  • Metadata:
    • issuedBy is set to msg.sender (the caller’s address).
    • issuedAt is block.timestamp, giving an on‑chain issuance time.
  • Event:
    • Emits ReportIssued, which can be indexed and tracked off‑chain for analytics, UI updates, or integrations.

In a real system, the lab’s backend would:

  1. Generate the medical report (PDF or JSON).
  2. Hash it off‑chain (for example, using keccak256 or an equivalent hashing function).
  3. Hash the patient identifier.
  4. Call issueReport(reportHash, patientIdHash) from the lab’s wallet.

Revoking a report

Sometimes a report must be invalidated (e.g. correction, mis‑entry, wrong patient). That’s handled by revokeReport:

function revokeReport(bytes32 _reportHash)
    external
    reportExists(_reportHash)
{
    require(
        reports[_reportHash].issuedBy == msg.sender,
        "Only issuing lab can revoke"
    );

    reports[_reportHash].revoked = true;

    emit ReportRevoked(_reportHash);
}

Key points:

  • Uses the reportExists modifier to ensure a valid report:
    • require(reports[_reportHash].issuedAt != 0, "Report not found");
  • Only the original issuer (issuedBy) can revoke, which prevents arbitrary revocations.
  • Sets revoked to true and emits a ReportRevoked event.

For any consumer of the system, if a report is revoked, verifyReport will still work but the revoked flag will be true. UIs can visually mark such reports as “Invalid / Revoked”.


Verifying a report

Verification is read‑only and open to everyone:

function verifyReport(bytes32 _reportHash)
    external
    view
    reportExists(_reportHash)
    returns (
        bytes32 patientIdHash,
        address issuedBy,
        uint256 issuedAt,
        bool revoked
    )
{
    Report memory report = reports[_reportHash];

    return (
        report.patientIdHash,
        report.issuedBy,
        report.issuedAt,
        report.revoked
    );
}

Usage pattern:

  1. Take the report file received from the patient.
  2. Hash it off‑chain: reportHash = keccak256(reportBytes).
  3. Call verifyReport(reportHash) on‑chain (directly or via a backend / frontend dApp).
  4. Read the response:
    • patientIdHash
    • issuedBy
    • issuedAt
    • revoked
  5. Optionally:
    • Compare issuedBy with the expected lab address.
    • Check issuedAt falls within a valid time window.
    • Confirm revoked == false.

If the hash is not found, the modifier reportExists will revert with "Report not found", which your app can surface to the user.


Privacy by design

A crucial design choice is that the contract never stores raw medical data:

  • Only hashes are stored (bytes32 values).
  • This avoids:
    • Putting personal health information (PHI) directly on a public blockchain.
    • Complicated compliance issues around deletion and the “right to be forgotten” (hashes are opaque without the original data).
  • Patients remain pseudonymous on‑chain:
    • patientIdHash is a one‑way mapping: observers can’t see who the patient is unless they already know the identifier and can recompute the hash.

This approach makes the contract suitable as a trust anchor while keeping the sensitive content off‑chain.


Example integration flows

Issuing side (Lab / Hospital backend)

  1. Generate the medical report (PDF or JSON).
  2. Compute:
    • reportHash = keccak256(reportBytes)
    • patientIdHash = keccak256(abi.encodePacked(patientIdentifier))
  3. From the lab’s wallet:
    • Call issueReport(reportHash, patientIdHash).
  4. Store the report as usual in the lab’s internal system, but now you also have an on‑chain proof.

Verification side (Doctor / Insurer app)

  1. User uploads the report file.
  2. App hashes it: reportHash = keccak256(reportBytes).
  3. App calls verifyReport(reportHash) on‑chain.
  4. The app checks:
    • If the call reverts → show “No matching on‑chain record found.”
    • If revoked == true → show “This report has been revoked by the issuer.”
    • If issuedBy is not in a trusted lab list → show “Report was not issued by a trusted provider.”
  5. If all checks pass, the app marks the report as verified authentic, with timestamp and lab identity.

Why this design is powerful

  • Simplicity: The contract is small and easy to audit, with just three main functions (issueReport, revokeReport, verifyReport).
  • Composable: It can be integrated into:
    • Hospital and laboratory information systems.
    • Insurance claim workflows.
    • Visa and immigration medical portals.
    • Employer health verification platforms.
  • Interoperable: Any frontend or backend that can compute the hash can interact with the contract.
  • Future‑proof: You can later deploy upgraded versions (e.g. MedicalStringReportRegistryV2) that:
    • Add role‑based access control (only whitelisted addresses can issue).
    • Attach additional metadata (test type, facility code, region).
    • Run on layer‑2 networks for lower transaction costs.

Overall, MedicalStringReportRegistry provides a clean, on‑chain foundation for tamper‑evident, verifiable medical reports that respect patient privacy.