Blockchain Two-Party Supply Chain Traceability — Step by Step
1. The problem: single-party traceability isn't traceability
TechnoPKG already had a blockchain traceability page — a real SHA-256 hash chain with proof-of-work, persisted server-side and tamper-evident. But it had a conceptual hole: one logged-in account mined all eight supply-chain stages itself.
That means the supplier never actually attested “yes, I shipped and QC’d these components,” and the customer never attested “yes, I received and accept them.” One party narrated the entire story. For real traceability, each step needs to be authorized by the party responsible for it, and nobody should be able to complete a passport alone.
2. The design
Two parties, each with their own key. The supplier signs the upstream stages (Supplier Receipt, Component Lot, Quality Check); the customer signs the downstream stages and the final acceptance.
The mechanism. Each party gets an Ed25519 keypair — public key stored on the user record, private key encrypted at rest. Every block’s SHA-256 hash (which also covers its role and signer identity) is signed with that party’s key. Validation re-checks the hash chain, that the correct role signed each stage, and that every signature verifies against the stored public key.
A passport is only SEALED and valid when both parties have signed. That single rule closes the hole.
3. Deploying safely to a live server
The portal is live, so every change went through a four-step, reversible flow: backup → deploy → verify → healthcheck, with a one-line rollback at every stage.
./1_backup.sh # full project + data + mongodump
./2_deploy.sh # install files, ensure cryptography in venv
./3_verify.sh # files present, app boots, routes registered
systemctl restart technopkg.service
./4_healthcheck.sh # service up, page wired, self-test passesA gotcha worth sharing: the first healthcheck failed — not in the feature, but in the test fixture. It created throwaway users without an email, and the live Mongo has a unique index on email, so the second collided on a null email. Lesson: when testing against a real database, fixtures need unique keys and must clean up after themselves.
4. Setting up the two parties
I registered a supplier and a customer account, then granted each a role and minted its keypair with a one-line admin script:
python scripts/grant_sc_role.py supply@precisionparts.com supplier
python scripts/grant_sc_role.py orders@dpsound.com customer
python scripts/grant_sc_role.py --list5. The live walkthrough
I opened two sessions — the customer in a normal window, the supplier in a private window. The cleanest tell of which account you’re on is the UI itself: the Initiate form only appears for the customer.
Move 1 — Customer initiates
As the customer: Finished Good ITM-001, blank serial, supplier username → Initiate.
Move 2 — Supplier signs
Switching to the supplier window, the same passport appears with a Sign as Supplier button. One click signs the three upstream blocks with the supplier’s key.
Move 3 — Customer accepts & seals
Back as the customer, the passport shows Accept & Seal. One click signs the five downstream blocks and seals it.
6. Proving it's real (from the backend)
Screens are nice; cryptography is the proof. I pulled the sealed passport straight from MongoDB and re-validated it:
status : SEALED
signers: ['supply@precisionparts.com', 'orders@dpsound.com']
blocks : 9
valid : True - Passport valid — all stages correctly signed by their parties.
roles : [(0,'system'),(1,'supplier'),(2,'supplier'),(3,'supplier'),
(4,'customer'),(5,'customer'),(6,'customer'),(7,'customer'),(8,'customer')]Tamper & forgery tests
ORIGINAL : (True, -1, 'Passport valid — all stages correctly signed by their parties.')
TAMPERED : (False, 2, 'Block #2: hash mismatch (tampered).')
FORGED : (False, 5, 'Block #5: signature invalid or forged.')Two distinct guarantees, both working: change any block’s contents and the recomputed SHA-256 no longer matches; sign a block with any key other than the real signer’s and verification fails.
7. Where MCP fits in
Alongside the web UI, the portal exposes its ledger over an MCP server (Model Context Protocol) — a small JSON-RPC service that lets an external agent or auditor talk to the chain through a standard tool interface, with no UI and no Flask. For the two-party feature I added three tools: list_two_party_passports, get_two_party_passport, and verify_two_party_passport.
Why it helped: it gave me a third, independent way to verify the same chain. During testing I could confirm a passport in the browser, read it straight from MongoDB, and verify it again over MCP — three layers agreeing on one cryptographic result. And in a real supply chain it’s the integration point: a customer’s system, an auditor, or an AI assistant can call verify_two_party_passport and get a pass/fail verdict programmatically, without logging into the portal.
verify_two_party_passport(serial="SN001-2026-4261")
-> { "valid": true, "status": "SEALED",
"signers": ["supply@precisionparts.com","orders@dpsound.com"],
"message": "Passport valid — all stages correctly signed by their parties." }8. What I learned
- “Real” lives in the validation, not the UI — the chain is only as trustworthy as what
validate()re-checks. - Testing against a live DB teaches you things; a unique index turned a sloppy fixture into a good failure.
- Enforce roles in the UI and the API — the supplier never sees an Initiate button, and the server returns 403 anyway.
- Custodial keys are a deliberate simplification: the server holds each key (encrypted) and signs on the authenticated user’s behalf. The signatures are real Ed25519; production would push keys client-side or to an HSM/KMS.
Built on TechnoPKG, a personal learning portal. All data shown is fictional demo data (DP Sound Systems & PrecisionParts Inc). This is a single-node educational chain — not a public distributed network — but the cryptography, per-party signing, and consent lifecycle are genuine.
Comments (0)