From 9eefb65047cc34c5e571f90a154f1ee62818037d Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 25 Feb 2024 15:43:01 -0500 Subject: [PATCH] Generalizes the interface for block explorers in OTS --- .../vitorpamplona/quartz/events/OtsEvent.kt | 7 ++- .../quartz/ots/BitcoinExplorer.java | 21 +++++++ ...{Esplora.java => BlockstreamExplorer.java} | 24 ++++---- .../quartz/ots/OpenTimestamps.java | 58 ++++++++++--------- 4 files changed, 68 insertions(+), 42 deletions(-) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/ots/BitcoinExplorer.java rename quartz/src/main/java/com/vitorpamplona/quartz/ots/{Esplora.java => BlockstreamExplorer.java} (74%) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/OtsEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/OtsEvent.kt index a88bc406d..988ec5836 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/OtsEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/OtsEvent.kt @@ -24,6 +24,7 @@ import android.util.Log import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.hexToByteArray +import com.vitorpamplona.quartz.ots.BlockstreamExplorer import com.vitorpamplona.quartz.ots.DetachedTimestampFile import com.vitorpamplona.quartz.ots.Hash import com.vitorpamplona.quartz.ots.OpenTimestamps @@ -57,7 +58,7 @@ class OtsEvent( val detachedOts = DetachedTimestampFile.deserialize(otsByteArray()) - val result = OpenTimestamps.verify(detachedOts, digest) + val result = OpenTimestamps(BlockstreamExplorer()).verify(detachedOts, digest) if (result == null || result.isEmpty()) { return null } else { @@ -72,7 +73,7 @@ class OtsEvent( fun info(): String { val detachedOts = DetachedTimestampFile.deserialize(otsByteArray()) - return OpenTimestamps.info(detachedOts) + return OpenTimestamps(BlockstreamExplorer()).info(detachedOts) } companion object { @@ -82,7 +83,7 @@ class OtsEvent( fun stamp(eventId: HexKey): ByteArray { val hash = Hash(eventId.hexToByteArray(), OpSHA256._TAG) val file = DetachedTimestampFile.from(hash) - val timestamp = OpenTimestamps.stamp(file) + val timestamp = OpenTimestamps(BlockstreamExplorer()).stamp(file) val detachedToSerialize = DetachedTimestampFile(hash.getOp(), timestamp) return detachedToSerialize.serialize() } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/BitcoinExplorer.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/BitcoinExplorer.java new file mode 100644 index 000000000..5a5bbd811 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/BitcoinExplorer.java @@ -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; +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/Esplora.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/BlockstreamExplorer.java similarity index 74% rename from quartz/src/main/java/com/vitorpamplona/quartz/ots/Esplora.java rename to quartz/src/main/java/com/vitorpamplona/quartz/ots/BlockstreamExplorer.java index f2ca8e6e6..01c84254f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/ots/Esplora.java +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/BlockstreamExplorer.java @@ -1,17 +1,15 @@ 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.Response; -import org.json.JSONObject; - import java.net.URL; import java.util.concurrent.*; -import java.util.logging.Logger; -public class Esplora { - - private static final Logger log = Utils.getLogger(Esplora.class.getName()); +public class BlockstreamExplorer implements BitcoinExplorer { private static final String esploraUrl = "https://blockstream.info/api"; /** @@ -21,7 +19,7 @@ public class Esplora { * @return the blockheader of the hash * @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 Request task = new Request(url); final ExecutorService executor = Executors.newSingleThreadExecutor(); @@ -31,14 +29,14 @@ public class Esplora { if (!take.isOk()) throw new Exception(); - final JSONObject jsonObject = take.getJson(); - final String merkleroot = jsonObject.getString("merkle_root"); - final String time = String.valueOf(jsonObject.getInt("timestamp")); + final JsonNode jsonObject = take.getJson(); + final String merkleroot = jsonObject.get("merkle_root").asText(); + final String time = String.valueOf(jsonObject.get("timestamp").asInt()); final BlockHeader blockHeader = new BlockHeader(); blockHeader.setMerkleroot(merkleroot); blockHeader.setTime(time); blockHeader.setBlockHash(hash); - log.info(take.getFromUrl() + " " + blockHeader); + Log.i("BlockstreamExplorer", take.getFromUrl() + " " + blockHeader); return blockHeader; //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 * @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 Request task = new Request(url); final ExecutorService executor = Executors.newSingleThreadExecutor(); @@ -60,7 +58,7 @@ public class Esplora { if (!take.isOk()) throw new Exception(); final String blockHash = take.getString(); - log.info(take.getFromUrl() + " " + blockHash); + Log.i("BlockstreamExplorer", take.getFromUrl() + " " + blockHash); return blockHash; } } \ No newline at end of file diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/OpenTimestamps.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/OpenTimestamps.java index f2ba09282..648885639 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/ots/OpenTimestamps.java +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/OpenTimestamps.java @@ -32,13 +32,19 @@ import java.util.concurrent.Executors; */ public class OpenTimestamps { + BitcoinExplorer explorer; + + public OpenTimestamps(BitcoinExplorer explorer) { + this.explorer = explorer; + } + /** * Show information on a detached timestamp. * * @param detachedTimestampFile The DetachedTimestampFile ots. * @return the string representation of the timestamp. */ - public static String info(DetachedTimestampFile detachedTimestampFile) { + public String info(DetachedTimestampFile detachedTimestampFile) { return info(detachedTimestampFile, false); } @@ -49,7 +55,7 @@ public class OpenTimestamps { * @param verbose Show verbose output. * @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) { return "No ots file"; } @@ -68,7 +74,7 @@ public class OpenTimestamps { * @param timestamp The timestamp buffered. * @return the string representation of the timestamp. */ - public static String info(Timestamp timestamp) { + public String info(Timestamp timestamp) { if (timestamp == null) { return "No timestamp"; } @@ -86,8 +92,8 @@ public class OpenTimestamps { * @return The plain array buffer of stamped. * @throws IOException if fileTimestamp is not valid, or the stamp procedure fails. */ - public static Timestamp stamp(DetachedTimestampFile fileTimestamp) throws IOException { - return OpenTimestamps.stamp(fileTimestamp, null, 0); + public Timestamp stamp(DetachedTimestampFile fileTimestamp) throws IOException { + return stamp(fileTimestamp, null, 0); } /** @@ -99,11 +105,11 @@ public class OpenTimestamps { * @return The plain array buffer of stamped. * @throws IOException if fileTimestamp is not valid, or the stamp procedure fails. */ - public static Timestamp stamp(DetachedTimestampFile fileTimestamp, List calendarsUrl, Integer m) throws IOException { + public Timestamp stamp(DetachedTimestampFile fileTimestamp, List calendarsUrl, Integer m) throws IOException { List fileTimestamps = new ArrayList(); 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. * @throws IOException if fileTimestamp is not valid, or the stamp procedure fails. */ - public static Timestamp stamp(List fileTimestamps, List calendarsUrl, Integer m) throws IOException { + public Timestamp stamp(List fileTimestamps, List calendarsUrl, Integer m) throws IOException { // Parse parameters if (fileTimestamps == null || fileTimestamps.size() == 0) { throw new IOException(); @@ -144,14 +150,14 @@ public class OpenTimestamps { } // Build markle tree - Timestamp merkleTip = OpenTimestamps.makeMerkleTree(fileTimestamps); + Timestamp merkleTip = makeMerkleTree(fileTimestamps); if (merkleTip == null) { throw new IOException(); } // Stamping - Timestamp resultTimestamp = OpenTimestamps.create(merkleTip, calendarsUrl, m); + Timestamp resultTimestamp = create(merkleTip, calendarsUrl, m); if (resultTimestamp == null) { throw new IOException(); @@ -173,7 +179,7 @@ public class OpenTimestamps { * @param m Number of calendars to use. * @return The created timestamp. */ - private static Timestamp create(Timestamp timestamp, List calendarUrls, Integer m) { + private Timestamp create(Timestamp timestamp, List calendarUrls, Integer m) { int capacity = calendarUrls.size(); ExecutorService executor = Executors.newFixedThreadPool(4); ArrayBlockingQueue> queue = new ArrayBlockingQueue<>(capacity); @@ -227,7 +233,7 @@ public class OpenTimestamps { * @param fileTimestamps The list of DetachedTimestampFile. * @return merkle tip timestamp. */ - public static Timestamp makeMerkleTree(List fileTimestamps) { + public Timestamp makeMerkleTree(List fileTimestamps) { List merkleRoots = new ArrayList<>(); for (DetachedTimestampFile fileTimestamp : fileTimestamps) { @@ -259,14 +265,14 @@ public class OpenTimestamps { * @throws Exception if the verification procedure fails. */ - public static HashMap verify(DetachedTimestampFile ots, byte[] diggest) throws Exception { + public HashMap verify(DetachedTimestampFile ots, byte[] diggest) throws Exception { if (!Arrays.equals(ots.fileDigest(), diggest)) { Log.e("OpenTimestamp", "Expected digest " + Hex.encode(ots.fileDigest()).toLowerCase()); Log.e("OpenTimestamp", "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. * @throws Exception if the verification procedure fails. */ - public static HashMap verify(Timestamp timestamp) throws Exception { + public HashMap verify(Timestamp timestamp) throws Exception { HashMap verifyResults = new HashMap<>(); for (Map.Entry 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 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(); BlockHeader blockInfo; try { - String blockHash = Esplora.blockHash(height); - blockInfo = Esplora.block(blockHash); + String blockHash = explorer.blockHash(height); + blockInfo = explorer.block(blockHash); Log.i("OpenTimestamps", "Lite-client verification, assuming block " + blockHash + " is valid"); } catch (Exception e2) { e2.printStackTrace(); @@ -364,13 +370,13 @@ public class OpenTimestamps { * @throws VerificationException if it doesn't check the merkle root of the block. * @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(); BlockHeader blockInfo; try { - String blockHash = Esplora.blockHash(height); - blockInfo = Esplora.block(blockHash); + String blockHash = explorer.blockHash(height); + blockInfo = explorer.block(blockHash); Log.i("OpenTimestamps", "Lite-client verification, assuming block " + blockHash + " is valid"); } catch (Exception e2) { e2.printStackTrace(); @@ -387,9 +393,9 @@ public class OpenTimestamps { * @return a boolean representing if the timestamp has changed. * @throws Exception if the upgrading procedure fails. */ - public static boolean upgrade(DetachedTimestampFile detachedTimestamp) throws Exception { + public boolean upgrade(DetachedTimestampFile detachedTimestamp) throws Exception { // Upgrade timestamp - boolean changed = OpenTimestamps.upgrade(detachedTimestamp.timestamp); + boolean changed = upgrade(detachedTimestamp.timestamp); return changed; } @@ -401,7 +407,7 @@ public class OpenTimestamps { * @return a boolean representing if the timestamp has changed. * @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. // This time we only check PendingAttestations - we can't be as agressive. @@ -417,7 +423,7 @@ public class OpenTimestamps { try { Calendar calendar = new Calendar(calendarUrl); - Timestamp upgradedStamp = OpenTimestamps.upgrade(subStamp, calendar, commitment, existingAttestations); + Timestamp upgradedStamp = upgrade(subStamp, calendar, commitment, existingAttestations); try { subStamp.merge(upgradedStamp); @@ -436,7 +442,7 @@ public class OpenTimestamps { return upgraded; } - private static Timestamp upgrade(Timestamp subStamp, Calendar calendar, byte[] commitment, Set existingAttestations) throws Exception { + private Timestamp upgrade(Timestamp subStamp, Calendar calendar, byte[] commitment, Set existingAttestations) throws Exception { Timestamp upgradedStamp; try {