ValiBlocks

Integrator documentation

Overview

ValiBlocks provides an on-chain proof-of-existence registry for clinical trial and life sciences data. Your application hashes patient or study data locally — no personal data leaves your system — and submits only the digest to the smart contract. The contract stores an immutable, timestamped record that any authorised party can use to verify the data is unchanged at any point in the future.

How it works

  1. Your application canonicalises the data (consistent key ordering and number formatting).
  2. A digest (hash) is computed from the canonical form using a registered algorithm such as SHA-256.
  3. The digest — never the raw data — is submitted to the ValGuard contract on-chain.
  4. The contract records the digest, the algorithm used, the submitting address, and a block timestamp.
  5. To verify later: re-canonicalise and re-hash the data, then compare the result against the on-chain record.

Key concepts

  • Trial — a logical namespace for a set of study records. Each trial has an admin who controls which addresses may submit records.
  • Record — a data point within a trial, identified by a recordId you supply. Each record holds an ordered list of versions.
  • Version — a single submitted hash, tagged with the algorithm, your amendment version number, the submitter's address, and a timestamp. Versions are append-only and strictly ordered.
  • Algorithm ID — a numeric identifier registered on-chain against a description (e.g. 1 = "SHA-256"). Each version records which algorithm produced its digest, so older records remain verifiable even if your algorithm changes.

Prerequisites

  • PMAToken balance — required to create a trial. Contact info@valiblocks.com to obtain tokens for your organisation.
  • ethers.js v5 (or equivalent Web3 library) in your application.
  • Contract addresses — provided by ValiBlocks for your target network. You will need the ValGuard address and ABI.
  • A funded wallet — each storeHash call requires a small ETH fee (the current storageFee is readable from the contract).

Step 1: Create a trial

A trial is the top-level namespace for your study records. The wallet creating the trial becomes its admin and is automatically authorised to submit records.

const valguard = new ethers.Contract(VALGUARD_ADDRESS, VALGUARD_ABI, signer);

const tx = await valguard.createTrial();
const receipt = await tx.wait();

const event = receipt.events.find(e => e.event === 'TrialCreated');
const trialId = event.args.trialId; // bytes32
Important: store trialId securely in your system. It cannot be recovered without the original transaction. It is intentionally non-guessable to prevent enumeration by third parties.

Step 2: Authorise submitters

Only authorised addresses may submit or amend records in a trial. The trial admin and the ValiBlocks contract owner can both grant and revoke access.

// Grant access to a site investigator wallet
await valguard.authorizeAddress(trialId, siteWalletAddress);

// Revoke access when no longer needed
await valguard.revokeAddress(trialId, siteWalletAddress);

The trial admin's own address cannot be revoked. Multiple addresses from different organisations may all be authorised to the same trial.

Step 3: Canonicalise and hash your data

For verification to work, the same data must always produce the same digest. JSON objects do not guarantee key ordering, and numbers may be represented differently across systems. You must canonicalise before hashing.

The canonical form is: keys sorted alphabetically at every level of nesting, whitespace removed, numbers in a consistent normalised form (per RFC 8259).

// Canonicalise then hash — this is the correct approach
function hashDataPoint(jsonObject) {
  const canonical = canonicalise(JSON.stringify(jsonObject));
  return ethers.utils.keccak256(ethers.utils.toUtf8Bytes(canonical));
}
Do not use ethers.utils.hashMessage() — it prepends an Ethereum signing prefix to the input before hashing. The on-chain digest will not match a plain hash of the same data, making independent verification fail.

A reference canonicalisation implementation in JavaScript:

function canonicalise(json) {
  const strs = [];
  return reorderProperties(
    strs,
    json
      .replace(/"(?:[^\\"]|\\.)*"/g, x => { strs.push(x); return `"@${strs.length-1}"` })
      .replace(/[\x00-\x20]+/g, '')
      .replace(/([+\-]?)(?=[\d.])(\d*)(?:[.](\d*))?(?:[eE]([+\-]?\d+))?(?=[,\]\}]|$)/g, canonNumber)
  ).replace(/"@(\d+)"/g, (_, i) => JSON.stringify(JSON.parse(strs[i])));
}

function canonNumber(_, sign, integer, fraction, mantissa) {
  if (sign !== '-') sign = '';
  integer = integer || '0';
  fraction = fraction || '';
  mantissa = (+mantissa || 0) + integer.length - 1;
  let digits = (integer + fraction).replace(/^(?:0(?!$))+/, s => (mantissa -= s.length, '')) || '0';
  if (digits === '0') mantissa = 0;
  if (-26 >= mantissa || mantissa >= 26) {
    const afterdot = digits.substring(1).replace(/0+$/, '');
    return sign + digits[0] + (afterdot ? `.${afterdot}` : '') + 'e' + mantissa;
  }
  return mantissa < 0
    ? sign + '0.' + '0'.repeat(-mantissa - 1) + digits
    : mantissa + 1 < digits.length
    ? sign + digits.substring(0, mantissa + 1) + '.' + digits.substring(mantissa + 1)
    : sign + digits + '0'.repeat(mantissa - digits.length + 1);
}

function reorderProperties(strs, s) {
  const tokens = [];
  const pat = /[{}\[\],]/g;
  for (let m; (m = pat.exec(s));) tokens.push(m);
  if (!tokens.length) return s;
  const memo = [];
  function key(i) { return memo[i] ?? (memo[i] = JSON.parse(strs[i])); }
  function reorder(l, r) {
    const { 0: tok, index } = tokens[l];
    let start = l, valStart = null, depth, cur, ranges = [];
    for (depth = 1, cur = l + 1; cur < r; ++cur) {
      switch (tokens[cur][0]) {
        case '{': case '[': if (depth === 1) valStart = cur; ++depth; break;
        case ']': case '}': --depth; break;
        case ',': if (depth === 1) { ranges.push([start, valStart, cur]); start = cur; valStart = null; }
      }
    }
    ranges.push([start, valStart, r]);
    ranges = ranges.map(([s, v, e]) => v === null
      ? str.substring(tokens[s].index + 1, tokens[e].index)
      : str.substring(tokens[s].index + 1, tokens[v].index) + reorder(v, e - 1));
    if (tok === '{') ranges.sort((a, b) => { const ak = key(/^"@(\d+)":/.exec(a)[1]), bk = key(/^"@(\d+)":/.exec(b)[1]); return ak < bk ? -1 : ak === bk ? 0 : 1; });
    return tok + ranges.join(',') + tokens[r][0];
  }
  var str = s;
  return reorder(0, tokens.length - 1);
}
If your application is not JavaScript, you must implement equivalent canonicalisation in your language. The important invariants are: keys sorted lexicographically at every nesting level, no whitespace, and consistent number representation. Test against known inputs before deploying.

Step 4: Store the hash

Submit the digest to the contract using storeHash. You supply:

  • trialId — the bytes32 ID from Step 1.
  • recordId — your own identifier for this data point within the trial (e.g. a patient visit ID). Not validated by the contract; use whatever makes sense for your system.
  • hash — the bytes32 digest from Step 3.
  • algorithmId — the numeric ID of the algorithm used (e.g. 1 for SHA-256). Must be registered in the contract's algorithm registry.
  • amendmentVersion — your version number for this record. Must be a positive integer strictly greater than the previous version for this recordId. Gaps are allowed — you may use sequential integers, dates (20250401), or any other monotonically increasing scheme.
const storageFee = await valguard.storageFee();

await valguard.storeHash(
  trialId,          // bytes32
  recordId,         // uint256 — your local identifier
  digest,           // bytes32 — from Step 3
  1,                // algorithmId (1 = SHA-256)
  amendmentVersion, // uint256 — strictly increasing per recordId
  { value: storageFee }
);
If you send more ETH than the current storageFee, the excess is automatically refunded in the same transaction. Query storageFee() before each call or cache it — the owner may update the fee over time.

Step 5: Verify a data point

To prove a data point is unchanged, re-canonicalise and re-hash the current data and compare it against the on-chain record. No special permissions are required — anyone with the trialId and recordId can query the contract.

// Retrieve the latest version of a record
const version = await valguard.getLatestVersion(trialId, recordId);

// Or retrieve a specific amendment version
const version = await valguard.getVersion(trialId, recordId, amendmentVersion);

// Re-hash the data you want to verify
const currentDigest = hashDataPoint(dataToVerify);

if (version.hash === currentDigest) {
  const date = new Date(version.timestamp.toNumber() * 1000);
  console.log('Verified. Recorded at:', date.toISOString());
  console.log('Submitted by:', version.submitter);
  console.log('Algorithm ID:', version.algorithmId.toNumber());
} else {
  console.log('Verification failed — digest does not match on-chain record.');
}

The version object contains:

FieldTypeDescription
hashbytes32The stored digest
algorithmIduint256Algorithm used to produce the digest
amendmentVersionuint256Your supplied version number
submitteraddressWallet that submitted this version
timestampuint256Block timestamp at submission (Unix seconds)

Step 6: Amendment history

Every version ever submitted for a record is preserved in order. You can retrieve the complete chain of custody for any data point.

// Full ordered history — oldest first
const history = await valguard.getFullHistory(trialId, recordId);

history.forEach(v => {
  const date = new Date(v.timestamp.toNumber() * 1000);
  console.log(
    `v${v.amendmentVersion} | ${date.toISOString()} | by ${v.submitter} | algo ${v.algorithmId}`
  );
});

// Count of versions (useful before paginating a large history)
const count = await valguard.getVersionCount(trialId, recordId);

Because each version records the submitter's wallet address, the history provides a complete, tamper-evident chain of custody showing who amended a record and when — without requiring any centralised audit system.

Algorithm registry

The contract maintains an on-chain registry mapping numeric IDs to algorithm descriptions. This lets verifiers determine exactly which algorithm produced a stored digest without relying on off-chain documentation.

// Look up the description for an algorithm ID
const description = await valguard.algorithms(1); // "SHA-256"

Registered algorithms (current defaults):

IDAlgorithm
1SHA-256
2SHA-3-256

New algorithm IDs may be added over time as standards evolve. Existing IDs are immutable — once registered, a description never changes. This means any digest stored against ID 1 will always be verifiable as SHA-256, even decades later.

Only the ValiBlocks contract owner may register new algorithm IDs. Contact support@valiblocks.com if your use case requires an additional algorithm.

Contract reference

ValGuard — write functions

FunctionWho can callDescription
createTrial() PMAToken holders Creates a new trial. Returns a non-guessable bytes32 trial ID.
authorizeAddress(trialId, addr) Trial admin, contract owner Grants addr permission to submit records in the trial.
revokeAddress(trialId, addr) Trial admin, contract owner Removes submission permission. Cannot revoke the trial admin.
storeHash(trialId, recordId, hash, algorithmId, amendmentVersion) Authorised addresses Stores a digest. Payable — must include storageFee in ETH. Excess is refunded.

ValGuard — read functions

FunctionReturnsDescription
getLatestVersion(trialId, recordId) RecordVersion Most recently submitted version of a record.
getVersion(trialId, recordId, amendmentVersion) RecordVersion A specific amendment version of a record.
getVersionCount(trialId, recordId) uint256 Total number of versions stored for a record.
getFullHistory(trialId, recordId) RecordVersion[] Complete ordered amendment history, oldest first.
algorithms(uint256 id) string Description of a registered algorithm ID.
storageFee() uint256 Current fee in wei required per storeHash call.

Get in touch

For contract addresses, PMAToken access, or integration support:

  • Email

    info@valiblocks.com
  • Support

    support@valiblocks.com