Skip to content

Commit dfc1018

Browse files
committed
ots: add Stamp & Verify drop-zone on /ots/calendars
Pure browser flow, no backend round-trip: - Drop a regular file: SHA-256 it via WebCrypto, POST the 32-byte digest to https://a.pool.opentimestamps.org/digest, wrap the calendar's reply into a complete .ots binary and offer it as a download. The file itself never leaves the browser; only the hash is sent. - Drop a .ots receipt: parse it with ordpool-parser (parseOtsFile + collectBitcoinAttestations), look up each Bitcoin attestation's block on api.ordpool.space (/api/block-height/N -> /api/block/<hash>), byte-reverse the receipt's expected merkleroot to display order, and compare. Result: "valid by block N" / "mismatch" / "pending only". - File detection by header magic. If the first 31 bytes match the OTS HEADER_MAGIC -> verify; else -> stamp. Verifies entirely against open data: ordpool's own electrs index for block lookups, the in-house parser for the receipt math. No reliance on opentimestamps.org's verifier or any other third party at verify time.
1 parent 9a44380 commit dfc1018

5 files changed

Lines changed: 499 additions & 0 deletions

File tree

frontend/src/app/components/_ordpool/ots-calendars/ots-calendars.component.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ <h1>OpenTimestamps Calendars</h1>
2828
</em>
2929
</p>
3030

31+
<app-ots-stamp-verify class="d-block mt-4"></app-ots-stamp-verify>
32+
3133
<h2 class="mt-4">Calendars</h2>
3234
@if (!calendarsLoaded) {
3335
<div class="skeleton-loader"></div>
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
<div class="card ots-stamp-verify mb-4">
2+
<div class="card-body">
3+
<h2 class="card-title mb-3">Stamp &amp; Verify</h2>
4+
<p class="smaller-text text-muted mb-3">
5+
Drop a file here to stamp it with a public OpenTimestamps calendar, or drop
6+
a <code>.ots</code> receipt to verify it. Everything happens in your
7+
browser. Stamping sends only the file's SHA-256 hash, never the file
8+
itself.
9+
</p>
10+
11+
<label class="dropzone"
12+
[class.dragging]="isDragging"
13+
(dragover)="onDragOver($event)"
14+
(dragleave)="onDragLeave($event)"
15+
(drop)="onDrop($event)">
16+
<input type="file" class="dropzone-input" (change)="onPick($event)">
17+
<span class="dropzone-text">
18+
<strong>Drop a file</strong> to stamp,<br>
19+
or a <strong><code>.ots</code> proof file</strong> to verify
20+
</span>
21+
</label>
22+
23+
@switch (status.kind) {
24+
@case ('busy') {
25+
<div class="alert alert-info mt-3 mb-0">
26+
<span class="spinner-border spinner-border-sm me-2"></span>
27+
{{ status.message }}
28+
</div>
29+
}
30+
31+
@case ('error') {
32+
<div class="alert alert-danger mt-3 mb-0">
33+
<strong>Could not process file:</strong> {{ status.message }}
34+
<button type="button" class="btn btn-sm btn-outline-light ms-2" (click)="reset()">Try again</button>
35+
</div>
36+
}
37+
38+
@case ('stamped') {
39+
<div class="alert alert-success mt-3 mb-0">
40+
<p class="mb-2">
41+
<strong>Stamped.</strong> The calendar has accepted your file's hash and
42+
will commit it in the next Bitcoin block. Save this <code>.ots</code>
43+
receipt now: keep it next to your file, you'll need it to verify the
44+
timestamp later.
45+
</p>
46+
<p class="mb-2">
47+
<code class="smaller-text">SHA-256: {{ status.hashHex }}</code>
48+
</p>
49+
@if (stampedDownload) {
50+
<a class="btn btn-primary btn-sm me-2"
51+
[href]="stampedDownload.url"
52+
[download]="stampedDownload.filename">
53+
<fa-icon [icon]="['fas', 'download']" class="me-1"></fa-icon>
54+
Download {{ stampedDownload.filename }}
55+
</a>
56+
}
57+
<button type="button" class="btn btn-outline-light btn-sm" (click)="reset()">Stamp another</button>
58+
<p class="smaller-text mt-2 mb-0 text-muted">
59+
<em>This receipt is "pending" until Bitcoin confirms (~10 minutes).
60+
After confirmation, drop the receipt back in to verify it against the
61+
actual on-chain block.</em>
62+
</p>
63+
</div>
64+
}
65+
66+
@case ('verified') {
67+
<div class="verify-result mt-3">
68+
<h3 class="h5">Verification result</h3>
69+
<p class="mb-2 smaller-text">
70+
File hash ({{ status.result.fileHashAlgo }}):
71+
<code class="smaller-text">{{ status.result.fileHashHex }}</code>
72+
</p>
73+
74+
@if (status.result.bitcoinAttestations.length === 0) {
75+
<div class="alert alert-warning mb-0">
76+
<strong>Not yet on-chain.</strong>
77+
@if (status.result.pendingCalendars.length > 0) {
78+
The receipt only contains pending calendar attestations:
79+
<ul class="mb-0 mt-1">
80+
@for (uri of status.result.pendingCalendars; track uri) {
81+
<li><code>{{ uri }}</code></li>
82+
}
83+
</ul>
84+
Once any of these calendars commits to a Bitcoin block (~10 min
85+
after stamping), upgrade this <code>.ots</code> via the official
86+
OTS client (or this page after upgrade support ships) and drop it
87+
back in.
88+
} @else {
89+
No Bitcoin attestation found and no pending calendars listed.
90+
The file may have been stamped by a chain we don't verify
91+
(Litecoin, Ethereum) or with custom attestations.
92+
}
93+
</div>
94+
} @else {
95+
@for (att of status.result.bitcoinAttestations; track att.blockheight) {
96+
<div class="alert mb-2"
97+
[class.alert-success]="att.match === true"
98+
[class.alert-warning]="att.match === null"
99+
[class.alert-danger]="att.match === false">
100+
@if (att.match === true) {
101+
<p class="mb-1">
102+
<strong>Valid.</strong> This file existed by Bitcoin block
103+
<a [routerLink]="['/block/' | relativeUrl, att.blockheight]" class="alert-link">{{ att.blockheight | number }}</a>
104+
@if (att.blockTime) {
105+
(mined {{ att.blockTime * 1000 | date:'medium' }})
106+
}.
107+
Merkle root matches.
108+
</p>
109+
<p class="smaller-text mb-0 text-muted">
110+
<code>{{ att.expectedMerkleRoot }}</code>
111+
</p>
112+
} @else if (att.match === false) {
113+
<p class="mb-1">
114+
<strong>Mismatch.</strong> The receipt claims block
115+
{{ att.blockheight | number }}, but the Merkle root in the
116+
receipt does not match the actual block's Merkle root. The
117+
receipt is invalid or for a different chain.
118+
</p>
119+
<p class="smaller-text mb-0 text-muted">
120+
Receipt says <code>{{ att.expectedMerkleRoot }}</code><br>
121+
On-chain says <code>{{ att.actualMerkleRoot }}</code>
122+
</p>
123+
} @else {
124+
<p class="mb-0">
125+
Could not look up Bitcoin block {{ att.blockheight | number }}
126+
on api.ordpool.space. The Merkle root in the receipt is
127+
<code>{{ att.expectedMerkleRoot }}</code>; you can compare it
128+
manually against any block explorer.
129+
</p>
130+
}
131+
</div>
132+
}
133+
}
134+
135+
@if (status.result.unknownAttestations > 0) {
136+
<p class="smaller-text text-muted mb-2">
137+
The receipt also contains {{ status.result.unknownAttestations }}
138+
attestation(s) we don't recognise (probably non-Bitcoin chains).
139+
</p>
140+
}
141+
142+
<button type="button" class="btn btn-outline-light btn-sm" (click)="reset()">Verify another</button>
143+
</div>
144+
}
145+
}
146+
</div>
147+
</div>
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
:host {
2+
display: block;
3+
}
4+
5+
.ots-stamp-verify {
6+
background-color: #181b2d;
7+
border: 1px solid #2d3250;
8+
9+
.dropzone {
10+
display: flex;
11+
align-items: center;
12+
justify-content: center;
13+
min-height: 140px;
14+
border: 2px dashed #4a5085;
15+
border-radius: 8px;
16+
cursor: pointer;
17+
background-color: #11132a;
18+
transition: border-color 0.15s ease, background-color 0.15s ease;
19+
text-align: center;
20+
padding: 20px;
21+
margin: 0;
22+
23+
&:hover,
24+
&.dragging {
25+
border-color: var(--bs-primary, #f7931a);
26+
background-color: #1a1d36;
27+
}
28+
29+
.dropzone-input {
30+
display: none;
31+
}
32+
33+
.dropzone-text {
34+
color: #9aa0c0;
35+
line-height: 1.7;
36+
37+
strong {
38+
color: #fff;
39+
}
40+
}
41+
}
42+
43+
.verify-result {
44+
code {
45+
word-break: break-all;
46+
}
47+
}
48+
}

0 commit comments

Comments
 (0)