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.
- Anyone can call
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. -
reportExistsmodifier:- Ensures functions like revoke and verify can only run on an existing report.
- Uses
issuedAt != 0as 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
reportHashcannot be issued twice (prevents accidental duplicates).
-
Storage:
- Saves a new
Reportin thereportsmapping.
- Saves a new
-
Metadata:
-
issuedByis set tomsg.sender(the caller’s address). -
issuedAtisblock.timestamp, giving an on‑chain issuance time.
-
-
Event:
- Emits
ReportIssued, which can be indexed and tracked off‑chain for analytics, UI updates, or integrations.
- Emits
In a real system, the lab’s backend would:
- Generate the medical report (PDF or JSON).
- Hash it off‑chain (for example, using
keccak256or an equivalent hashing function). - Hash the patient identifier.
- 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
reportExistsmodifier 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
revokedtotrueand emits aReportRevokedevent.
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:
- Take the report file received from the patient.
- Hash it off‑chain:
reportHash = keccak256(reportBytes). - Call
verifyReport(reportHash)on‑chain (directly or via a backend / frontend dApp). - Read the response:
patientIdHashissuedByissuedAtrevoked
- Optionally:
- Compare
issuedBywith the expected lab address. - Check
issuedAtfalls within a valid time window. - Confirm
revoked == false.
- Compare
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 (
bytes32values). - 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:
-
patientIdHashis 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)
- Generate the medical report (PDF or JSON).
- Compute:
reportHash = keccak256(reportBytes)patientIdHash = keccak256(abi.encodePacked(patientIdentifier))
- From the lab’s wallet:
- Call
issueReport(reportHash, patientIdHash).
- Call
- 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)
- User uploads the report file.
- App hashes it:
reportHash = keccak256(reportBytes). - App calls
verifyReport(reportHash)on‑chain. - 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
issuedByis not in a trusted lab list → show “Report was not issued by a trusted provider.”
- 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.