Generalizes the interface for block explorers in OTS

This commit is contained in:
Vitor Pamplona
2024-02-25 15:43:01 -05:00
parent 31c9041461
commit 9eefb65047
4 changed files with 68 additions and 42 deletions
@@ -24,6 +24,7 @@ import android.util.Log
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.hexToByteArray import com.vitorpamplona.quartz.encoders.hexToByteArray
import com.vitorpamplona.quartz.ots.BlockstreamExplorer
import com.vitorpamplona.quartz.ots.DetachedTimestampFile import com.vitorpamplona.quartz.ots.DetachedTimestampFile
import com.vitorpamplona.quartz.ots.Hash import com.vitorpamplona.quartz.ots.Hash
import com.vitorpamplona.quartz.ots.OpenTimestamps import com.vitorpamplona.quartz.ots.OpenTimestamps
@@ -57,7 +58,7 @@ class OtsEvent(
val detachedOts = DetachedTimestampFile.deserialize(otsByteArray()) val detachedOts = DetachedTimestampFile.deserialize(otsByteArray())
val result = OpenTimestamps.verify(detachedOts, digest) val result = OpenTimestamps(BlockstreamExplorer()).verify(detachedOts, digest)
if (result == null || result.isEmpty()) { if (result == null || result.isEmpty()) {
return null return null
} else { } else {
@@ -72,7 +73,7 @@ class OtsEvent(
fun info(): String { fun info(): String {
val detachedOts = DetachedTimestampFile.deserialize(otsByteArray()) val detachedOts = DetachedTimestampFile.deserialize(otsByteArray())
return OpenTimestamps.info(detachedOts) return OpenTimestamps(BlockstreamExplorer()).info(detachedOts)
} }
companion object { companion object {
@@ -82,7 +83,7 @@ class OtsEvent(
fun stamp(eventId: HexKey): ByteArray { fun stamp(eventId: HexKey): ByteArray {
val hash = Hash(eventId.hexToByteArray(), OpSHA256._TAG) val hash = Hash(eventId.hexToByteArray(), OpSHA256._TAG)
val file = DetachedTimestampFile.from(hash) val file = DetachedTimestampFile.from(hash)
val timestamp = OpenTimestamps.stamp(file) val timestamp = OpenTimestamps(BlockstreamExplorer()).stamp(file)
val detachedToSerialize = DetachedTimestampFile(hash.getOp(), timestamp) val detachedToSerialize = DetachedTimestampFile(hash.getOp(), timestamp)
return detachedToSerialize.serialize() return detachedToSerialize.serialize()
} }
@@ -0,0 +1,21 @@
package com.vitorpamplona.quartz.ots;
public interface BitcoinExplorer {
/**
* Retrieve the block information from the block hash.
*
* @param hash Hash of the block.
* @return the blockheader of the hash
* @throws Exception desc
*/
public BlockHeader block(final String hash) throws Exception;
/**
* Retrieve the block hash from the block height.
*
* @param height Height of the block.
* @return the hash of the block at height height
* @throws Exception desc
*/
public String blockHash(final Integer height) throws Exception;
}
@@ -1,17 +1,15 @@
package com.vitorpamplona.quartz.ots; package com.vitorpamplona.quartz.ots;
import android.util.Log;
import com.fasterxml.jackson.databind.JsonNode;
import com.vitorpamplona.quartz.ots.http.Request; import com.vitorpamplona.quartz.ots.http.Request;
import com.vitorpamplona.quartz.ots.http.Response; import com.vitorpamplona.quartz.ots.http.Response;
import org.json.JSONObject;
import java.net.URL; import java.net.URL;
import java.util.concurrent.*; import java.util.concurrent.*;
import java.util.logging.Logger;
public class Esplora { public class BlockstreamExplorer implements BitcoinExplorer {
private static final Logger log = Utils.getLogger(Esplora.class.getName());
private static final String esploraUrl = "https://blockstream.info/api"; private static final String esploraUrl = "https://blockstream.info/api";
/** /**
@@ -21,7 +19,7 @@ public class Esplora {
* @return the blockheader of the hash * @return the blockheader of the hash
* @throws Exception desc * @throws Exception desc
*/ */
public static BlockHeader block(final String hash) throws Exception { public BlockHeader block(final String hash) throws Exception {
final URL url = new URL(esploraUrl + "/block/" + hash); final URL url = new URL(esploraUrl + "/block/" + hash);
final Request task = new Request(url); final Request task = new Request(url);
final ExecutorService executor = Executors.newSingleThreadExecutor(); final ExecutorService executor = Executors.newSingleThreadExecutor();
@@ -31,14 +29,14 @@ public class Esplora {
if (!take.isOk()) if (!take.isOk())
throw new Exception(); throw new Exception();
final JSONObject jsonObject = take.getJson(); final JsonNode jsonObject = take.getJson();
final String merkleroot = jsonObject.getString("merkle_root"); final String merkleroot = jsonObject.get("merkle_root").asText();
final String time = String.valueOf(jsonObject.getInt("timestamp")); final String time = String.valueOf(jsonObject.get("timestamp").asInt());
final BlockHeader blockHeader = new BlockHeader(); final BlockHeader blockHeader = new BlockHeader();
blockHeader.setMerkleroot(merkleroot); blockHeader.setMerkleroot(merkleroot);
blockHeader.setTime(time); blockHeader.setTime(time);
blockHeader.setBlockHash(hash); blockHeader.setBlockHash(hash);
log.info(take.getFromUrl() + " " + blockHeader); Log.i("BlockstreamExplorer", take.getFromUrl() + " " + blockHeader);
return blockHeader; return blockHeader;
//log.warning("Cannot parse merkleroot from body: " + jsonObject + ": " + e.getMessage()); //log.warning("Cannot parse merkleroot from body: " + jsonObject + ": " + e.getMessage());
} }
@@ -50,7 +48,7 @@ public class Esplora {
* @return the hash of the block at height height * @return the hash of the block at height height
* @throws Exception desc * @throws Exception desc
*/ */
public static String blockHash(final Integer height) throws Exception { public String blockHash(final Integer height) throws Exception {
final URL url = new URL(esploraUrl + "/block-height/" + height); final URL url = new URL(esploraUrl + "/block-height/" + height);
final Request task = new Request(url); final Request task = new Request(url);
final ExecutorService executor = Executors.newSingleThreadExecutor(); final ExecutorService executor = Executors.newSingleThreadExecutor();
@@ -60,7 +58,7 @@ public class Esplora {
if (!take.isOk()) if (!take.isOk())
throw new Exception(); throw new Exception();
final String blockHash = take.getString(); final String blockHash = take.getString();
log.info(take.getFromUrl() + " " + blockHash); Log.i("BlockstreamExplorer", take.getFromUrl() + " " + blockHash);
return blockHash; return blockHash;
} }
} }
@@ -32,13 +32,19 @@ import java.util.concurrent.Executors;
*/ */
public class OpenTimestamps { public class OpenTimestamps {
BitcoinExplorer explorer;
public OpenTimestamps(BitcoinExplorer explorer) {
this.explorer = explorer;
}
/** /**
* Show information on a detached timestamp. * Show information on a detached timestamp.
* *
* @param detachedTimestampFile The DetachedTimestampFile ots. * @param detachedTimestampFile The DetachedTimestampFile ots.
* @return the string representation of the timestamp. * @return the string representation of the timestamp.
*/ */
public static String info(DetachedTimestampFile detachedTimestampFile) { public String info(DetachedTimestampFile detachedTimestampFile) {
return info(detachedTimestampFile, false); return info(detachedTimestampFile, false);
} }
@@ -49,7 +55,7 @@ public class OpenTimestamps {
* @param verbose Show verbose output. * @param verbose Show verbose output.
* @return the string representation of the timestamp. * @return the string representation of the timestamp.
*/ */
public static String info(DetachedTimestampFile detachedTimestampFile, boolean verbose) { public String info(DetachedTimestampFile detachedTimestampFile, boolean verbose) {
if (detachedTimestampFile == null) { if (detachedTimestampFile == null) {
return "No ots file"; return "No ots file";
} }
@@ -68,7 +74,7 @@ public class OpenTimestamps {
* @param timestamp The timestamp buffered. * @param timestamp The timestamp buffered.
* @return the string representation of the timestamp. * @return the string representation of the timestamp.
*/ */
public static String info(Timestamp timestamp) { public String info(Timestamp timestamp) {
if (timestamp == null) { if (timestamp == null) {
return "No timestamp"; return "No timestamp";
} }
@@ -86,8 +92,8 @@ public class OpenTimestamps {
* @return The plain array buffer of stamped. * @return The plain array buffer of stamped.
* @throws IOException if fileTimestamp is not valid, or the stamp procedure fails. * @throws IOException if fileTimestamp is not valid, or the stamp procedure fails.
*/ */
public static Timestamp stamp(DetachedTimestampFile fileTimestamp) throws IOException { public Timestamp stamp(DetachedTimestampFile fileTimestamp) throws IOException {
return OpenTimestamps.stamp(fileTimestamp, null, 0); return stamp(fileTimestamp, null, 0);
} }
/** /**
@@ -99,11 +105,11 @@ public class OpenTimestamps {
* @return The plain array buffer of stamped. * @return The plain array buffer of stamped.
* @throws IOException if fileTimestamp is not valid, or the stamp procedure fails. * @throws IOException if fileTimestamp is not valid, or the stamp procedure fails.
*/ */
public static Timestamp stamp(DetachedTimestampFile fileTimestamp, List<String> calendarsUrl, Integer m) throws IOException { public Timestamp stamp(DetachedTimestampFile fileTimestamp, List<String> calendarsUrl, Integer m) throws IOException {
List<DetachedTimestampFile> fileTimestamps = new ArrayList<DetachedTimestampFile>(); List<DetachedTimestampFile> fileTimestamps = new ArrayList<DetachedTimestampFile>();
fileTimestamps.add(fileTimestamp); fileTimestamps.add(fileTimestamp);
return OpenTimestamps.stamp(fileTimestamps, calendarsUrl, m); return stamp(fileTimestamps, calendarsUrl, m);
} }
/** /**
@@ -115,7 +121,7 @@ public class OpenTimestamps {
* @return The plain array buffer of stamped. * @return The plain array buffer of stamped.
* @throws IOException if fileTimestamp is not valid, or the stamp procedure fails. * @throws IOException if fileTimestamp is not valid, or the stamp procedure fails.
*/ */
public static Timestamp stamp(List<DetachedTimestampFile> fileTimestamps, List<String> calendarsUrl, Integer m) throws IOException { public Timestamp stamp(List<DetachedTimestampFile> fileTimestamps, List<String> calendarsUrl, Integer m) throws IOException {
// Parse parameters // Parse parameters
if (fileTimestamps == null || fileTimestamps.size() == 0) { if (fileTimestamps == null || fileTimestamps.size() == 0) {
throw new IOException(); throw new IOException();
@@ -144,14 +150,14 @@ public class OpenTimestamps {
} }
// Build markle tree // Build markle tree
Timestamp merkleTip = OpenTimestamps.makeMerkleTree(fileTimestamps); Timestamp merkleTip = makeMerkleTree(fileTimestamps);
if (merkleTip == null) { if (merkleTip == null) {
throw new IOException(); throw new IOException();
} }
// Stamping // Stamping
Timestamp resultTimestamp = OpenTimestamps.create(merkleTip, calendarsUrl, m); Timestamp resultTimestamp = create(merkleTip, calendarsUrl, m);
if (resultTimestamp == null) { if (resultTimestamp == null) {
throw new IOException(); throw new IOException();
@@ -173,7 +179,7 @@ public class OpenTimestamps {
* @param m Number of calendars to use. * @param m Number of calendars to use.
* @return The created timestamp. * @return The created timestamp.
*/ */
private static Timestamp create(Timestamp timestamp, List<String> calendarUrls, Integer m) { private Timestamp create(Timestamp timestamp, List<String> calendarUrls, Integer m) {
int capacity = calendarUrls.size(); int capacity = calendarUrls.size();
ExecutorService executor = Executors.newFixedThreadPool(4); ExecutorService executor = Executors.newFixedThreadPool(4);
ArrayBlockingQueue<Optional<Timestamp>> queue = new ArrayBlockingQueue<>(capacity); ArrayBlockingQueue<Optional<Timestamp>> queue = new ArrayBlockingQueue<>(capacity);
@@ -227,7 +233,7 @@ public class OpenTimestamps {
* @param fileTimestamps The list of DetachedTimestampFile. * @param fileTimestamps The list of DetachedTimestampFile.
* @return merkle tip timestamp. * @return merkle tip timestamp.
*/ */
public static Timestamp makeMerkleTree(List<DetachedTimestampFile> fileTimestamps) { public Timestamp makeMerkleTree(List<DetachedTimestampFile> fileTimestamps) {
List<Timestamp> merkleRoots = new ArrayList<>(); List<Timestamp> merkleRoots = new ArrayList<>();
for (DetachedTimestampFile fileTimestamp : fileTimestamps) { for (DetachedTimestampFile fileTimestamp : fileTimestamps) {
@@ -259,14 +265,14 @@ public class OpenTimestamps {
* @throws Exception if the verification procedure fails. * @throws Exception if the verification procedure fails.
*/ */
public static HashMap<VerifyResult.Chains, VerifyResult> verify(DetachedTimestampFile ots, byte[] diggest) throws Exception { public HashMap<VerifyResult.Chains, VerifyResult> verify(DetachedTimestampFile ots, byte[] diggest) throws Exception {
if (!Arrays.equals(ots.fileDigest(), diggest)) { if (!Arrays.equals(ots.fileDigest(), diggest)) {
Log.e("OpenTimestamp", "Expected digest " + Hex.encode(ots.fileDigest()).toLowerCase()); Log.e("OpenTimestamp", "Expected digest " + Hex.encode(ots.fileDigest()).toLowerCase());
Log.e("OpenTimestamp", "File does not match original!"); Log.e("OpenTimestamp", "File does not match original!");
throw new Exception("File does not match original!"); throw new Exception("File does not match original!");
} }
return OpenTimestamps.verify(ots.timestamp); return verify(ots.timestamp);
} }
/** /**
@@ -276,7 +282,7 @@ public class OpenTimestamps {
* @return HashMap of block heights and timestamps indexed by chain: timestamp in seconds from 1 January 1970. * @return HashMap of block heights and timestamps indexed by chain: timestamp in seconds from 1 January 1970.
* @throws Exception if the verification procedure fails. * @throws Exception if the verification procedure fails.
*/ */
public static HashMap<VerifyResult.Chains, VerifyResult> verify(Timestamp timestamp) throws Exception { public HashMap<VerifyResult.Chains, VerifyResult> verify(Timestamp timestamp) throws Exception {
HashMap<VerifyResult.Chains, VerifyResult> verifyResults = new HashMap<>(); HashMap<VerifyResult.Chains, VerifyResult> verifyResults = new HashMap<>();
for (Map.Entry<byte[], TimeAttestation> item : timestamp.allAttestations().entrySet()) { for (Map.Entry<byte[], TimeAttestation> item : timestamp.allAttestations().entrySet()) {
@@ -339,13 +345,13 @@ public class OpenTimestamps {
* @throws VerificationException if it doesn't check the merkle root of the block. * @throws VerificationException if it doesn't check the merkle root of the block.
* @throws Exception if the verification procedure fails. * @throws Exception if the verification procedure fails.
*/ */
public static Long verify(BitcoinBlockHeaderAttestation attestation, byte[] msg) throws VerificationException, Exception { public Long verify(BitcoinBlockHeaderAttestation attestation, byte[] msg) throws VerificationException, Exception {
Integer height = attestation.getHeight(); Integer height = attestation.getHeight();
BlockHeader blockInfo; BlockHeader blockInfo;
try { try {
String blockHash = Esplora.blockHash(height); String blockHash = explorer.blockHash(height);
blockInfo = Esplora.block(blockHash); blockInfo = explorer.block(blockHash);
Log.i("OpenTimestamps", "Lite-client verification, assuming block " + blockHash + " is valid"); Log.i("OpenTimestamps", "Lite-client verification, assuming block " + blockHash + " is valid");
} catch (Exception e2) { } catch (Exception e2) {
e2.printStackTrace(); e2.printStackTrace();
@@ -364,13 +370,13 @@ public class OpenTimestamps {
* @throws VerificationException if it doesn't check the merkle root of the block. * @throws VerificationException if it doesn't check the merkle root of the block.
* @throws Exception if the verification procedure fails. * @throws Exception if the verification procedure fails.
*/ */
public static Long verify(LitecoinBlockHeaderAttestation attestation, byte[] msg) throws VerificationException, Exception { public Long verify(LitecoinBlockHeaderAttestation attestation, byte[] msg) throws VerificationException, Exception {
Integer height = attestation.getHeight(); Integer height = attestation.getHeight();
BlockHeader blockInfo; BlockHeader blockInfo;
try { try {
String blockHash = Esplora.blockHash(height); String blockHash = explorer.blockHash(height);
blockInfo = Esplora.block(blockHash); blockInfo = explorer.block(blockHash);
Log.i("OpenTimestamps", "Lite-client verification, assuming block " + blockHash + " is valid"); Log.i("OpenTimestamps", "Lite-client verification, assuming block " + blockHash + " is valid");
} catch (Exception e2) { } catch (Exception e2) {
e2.printStackTrace(); e2.printStackTrace();
@@ -387,9 +393,9 @@ public class OpenTimestamps {
* @return a boolean representing if the timestamp has changed. * @return a boolean representing if the timestamp has changed.
* @throws Exception if the upgrading procedure fails. * @throws Exception if the upgrading procedure fails.
*/ */
public static boolean upgrade(DetachedTimestampFile detachedTimestamp) throws Exception { public boolean upgrade(DetachedTimestampFile detachedTimestamp) throws Exception {
// Upgrade timestamp // Upgrade timestamp
boolean changed = OpenTimestamps.upgrade(detachedTimestamp.timestamp); boolean changed = upgrade(detachedTimestamp.timestamp);
return changed; return changed;
} }
@@ -401,7 +407,7 @@ public class OpenTimestamps {
* @return a boolean representing if the timestamp has changed. * @return a boolean representing if the timestamp has changed.
* @throws Exception if the upgrading procedure fails. * @throws Exception if the upgrading procedure fails.
*/ */
public static boolean upgrade(Timestamp timestamp) throws Exception { public boolean upgrade(Timestamp timestamp) throws Exception {
// Check remote calendars for upgrades. // Check remote calendars for upgrades.
// This time we only check PendingAttestations - we can't be as agressive. // This time we only check PendingAttestations - we can't be as agressive.
@@ -417,7 +423,7 @@ public class OpenTimestamps {
try { try {
Calendar calendar = new Calendar(calendarUrl); Calendar calendar = new Calendar(calendarUrl);
Timestamp upgradedStamp = OpenTimestamps.upgrade(subStamp, calendar, commitment, existingAttestations); Timestamp upgradedStamp = upgrade(subStamp, calendar, commitment, existingAttestations);
try { try {
subStamp.merge(upgradedStamp); subStamp.merge(upgradedStamp);
@@ -436,7 +442,7 @@ public class OpenTimestamps {
return upgraded; return upgraded;
} }
private static Timestamp upgrade(Timestamp subStamp, Calendar calendar, byte[] commitment, Set<TimeAttestation> existingAttestations) throws Exception { private Timestamp upgrade(Timestamp subStamp, Calendar calendar, byte[] commitment, Set<TimeAttestation> existingAttestations) throws Exception {
Timestamp upgradedStamp; Timestamp upgradedStamp;
try { try {