cormorant: add block stats rpc call, and prefer for block summaries

This commit is contained in:
Craig Raw 2025-05-14 10:52:21 +02:00
parent 94b27ba7e8
commit b1ab157ee3
10 changed files with 119 additions and 8 deletions

View file

@ -4,7 +4,6 @@ import com.github.arteam.simplejsonrpc.client.JsonRpcClient;
import com.github.arteam.simplejsonrpc.client.Transport;
import com.github.arteam.simplejsonrpc.client.exception.JsonRpcBatchException;
import com.github.arteam.simplejsonrpc.client.exception.JsonRpcException;
import com.github.arteam.simplejsonrpc.core.domain.ErrorMessage;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager;
@ -191,6 +190,24 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
}
}
@Override
@SuppressWarnings("unchecked")
public Map<Integer, BlockStats> getBlockStats(Transport transport, Set<Integer> blockHeights) {
PagedBatchRequestBuilder<Integer, BlockStats> batchRequest = PagedBatchRequestBuilder.create(transport, idCounter).keysType(Integer.class).returnType(BlockStats.class);
for(Integer height : blockHeights) {
batchRequest.add(height, "blockchain.block.stats", height);
}
try {
return batchRequest.execute();
} catch(JsonRpcBatchException e) {
return (Map<Integer, BlockStats>)e.getSuccesses();
} catch(Exception e) {
throw new ElectrumServerRpcException("Failed to retrieve block stats for block heights: " + blockHeights, e);
}
}
@Override
@SuppressWarnings("unchecked")
public Map<String, String> getTransactions(Transport transport, Wallet wallet, Set<String> txids) {

View file

@ -0,0 +1,14 @@
package com.sparrowwallet.sparrow.net;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.sparrowwallet.sparrow.BlockSummary;
import java.util.Date;
@JsonIgnoreProperties(ignoreUnknown = true)
public record BlockStats(int height, String blockhash, double[] feerate_percentiles, int total_weight, int txs, long time) {
public BlockSummary toBlockSummary() {
Double medianFee = feerate_percentiles != null && feerate_percentiles.length > 0 ? feerate_percentiles[feerate_percentiles.length / 2] : null;
return new BlockSummary(height, new Date(time * 1000), medianFee, txs, total_weight);
}
}

View file

@ -82,6 +82,8 @@ public class ElectrumServer {
private static Server coreElectrumServer;
private static ServerCapability serverCapability;
private static final Pattern RPC_WALLET_LOADING_PATTERN = Pattern.compile(".*\"(Wallet loading failed[:.][^\"]*)\".*");
private static synchronized CloseableTransport getTransport() throws ServerException {
@ -981,6 +983,21 @@ public class ElectrumServer {
}
public Map<Integer, BlockSummary> getBlockSummaryMap(Integer height, BlockHeader blockHeader) throws ServerException {
if(serverCapability.supportsBlockStats()) {
if(height == null) {
Integer current = AppServices.getCurrentBlockHeight();
if(current == null) {
return Collections.emptyMap();
}
Set<Integer> heights = IntStream.range(current - 1, current + 1).boxed().collect(Collectors.toSet());
Map<Integer, BlockStats> blockStats = electrumServerRpc.getBlockStats(getTransport(), heights);
return blockStats.keySet().stream().collect(Collectors.toMap(java.util.function.Function.identity(), v -> blockStats.get(v).toBlockSummary()));
} else {
Map<Integer, BlockStats> blockStats = electrumServerRpc.getBlockStats(getTransport(), Set.of(height));
return blockStats.keySet().stream().collect(Collectors.toMap(java.util.function.Function.identity(), v -> blockStats.get(v).toBlockSummary()));
}
}
FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource();
feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource);
@ -1010,7 +1027,7 @@ public class ElectrumServer {
if(current == null) {
return Collections.emptyMap();
}
Set<BlockTransactionHash> references = IntStream.range(current - 4, current + 1)
Set<BlockTransactionHash> references = IntStream.range(current - 1, current + 1)
.mapToObj(i -> new BlockTransaction(null, i, null, null, null)).collect(Collectors.toSet());
Map<Integer, BlockHeader> blockHeaders = getBlockHeaders(null, references);
return blockHeaders.keySet().stream()
@ -1219,7 +1236,7 @@ public class ElectrumServer {
}
if(server.startsWith("cormorant")) {
return new ServerCapability(true);
return new ServerCapability(true, false, true);
}
if(server.startsWith("electrs/")) {
@ -1405,7 +1422,7 @@ public class ElectrumServer {
firstCall = false;
//If electrumx is detected, we can upgrade to batched RPC. Electrs/EPS do not support batching.
ServerCapability serverCapability = getServerCapability(serverVersion);
serverCapability = getServerCapability(serverVersion);
if(serverCapability.supportsBatching()) {
log.debug("Upgrading to batched JSON-RPC");
electrumServerRpc = new BatchedElectrumServerRpc(electrumServerRpc.getIdCounterValue(), serverCapability.getMaxTargetBlocks());

View file

@ -26,6 +26,8 @@ public interface ElectrumServerRpc {
Map<Integer, String> getBlockHeaders(Transport transport, Wallet wallet, Set<Integer> blockHeights);
Map<Integer, BlockStats> getBlockStats(Transport transport, Set<Integer> blockHeights);
Map<String, String> getTransactions(Transport transport, Wallet wallet, Set<String> txids);
Map<String, VerboseTransaction> getVerboseTransactions(Transport transport, Set<String> txids, String scriptHash);

View file

@ -4,7 +4,7 @@ import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.wallet.BlockTransaction;
import com.sparrowwallet.drongo.wallet.BlockTransactionHash;
class ScriptHashTx {
public class ScriptHashTx {
public static final ScriptHashTx ERROR_TX = new ScriptHashTx() {
@Override
public BlockTransactionHash getBlockchainTransactionHash() {

View file

@ -5,15 +5,29 @@ import com.sparrowwallet.sparrow.AppServices;
public class ServerCapability {
private final boolean supportsBatching;
private final int maxTargetBlocks;
private final boolean supportsRecentMempool;
private final boolean supportsBlockStats;
public ServerCapability(boolean supportsBatching) {
this.supportsBatching = supportsBatching;
this.maxTargetBlocks = AppServices.TARGET_BLOCKS_RANGE.getLast();
this(supportsBatching, AppServices.TARGET_BLOCKS_RANGE.getLast());
}
public ServerCapability(boolean supportsBatching, int maxTargetBlocks) {
this.supportsBatching = supportsBatching;
this.maxTargetBlocks = maxTargetBlocks;
this.supportsRecentMempool = false;
this.supportsBlockStats = false;
}
public ServerCapability(boolean supportsBatching, boolean supportsRecentMempool, boolean supportsBlockStats) {
this(supportsBatching, AppServices.TARGET_BLOCKS_RANGE.getLast(), supportsRecentMempool, supportsBlockStats);
}
public ServerCapability(boolean supportsBatching, int maxTargetBlocks, boolean supportsRecentMempool, boolean supportsBlockStats) {
this.supportsBatching = supportsBatching;
this.maxTargetBlocks = maxTargetBlocks;
this.supportsRecentMempool = supportsRecentMempool;
this.supportsBlockStats = supportsBlockStats;
}
public boolean supportsBatching() {
@ -23,4 +37,12 @@ public class ServerCapability {
public int getMaxTargetBlocks() {
return maxTargetBlocks;
}
public boolean supportsRecentMempool() {
return supportsRecentMempool;
}
public boolean supportsBlockStats() {
return supportsBlockStats;
}
}

View file

@ -177,6 +177,29 @@ public class SimpleElectrumServerRpc implements ElectrumServerRpc {
return result;
}
@Override
public Map<Integer, BlockStats> getBlockStats(Transport transport, Set<Integer> blockHeights) {
JsonRpcClient client = new JsonRpcClient(transport);
Map<Integer, BlockStats> result = new LinkedHashMap<>();
for(Integer blockHeight : blockHeights) {
try {
BlockStats blockStats = new RetryLogic<BlockStats>(MAX_RETRIES, RETRY_DELAY, List.of(IllegalStateException.class, IllegalArgumentException.class)).getResult(() ->
client.createRequest().returnAs(BlockStats.class).method("blockchain.block.stats").id(idCounter.incrementAndGet()).params(blockHeight).execute());
result.put(blockHeight, blockStats);
} catch(ServerException e) {
//If there is an error with the server connection, don't keep trying - this may take too long given many blocks
throw new ElectrumServerRpcException("Failed to retrieve block stats for block height: " + blockHeight, e);
} catch(JsonRpcException e) {
log.warn("Failed to retrieve block stats for block height: " + blockHeight + (e.getErrorMessage() != null ? " (" + e.getErrorMessage().getMessage() + ")" : ""));
} catch(Exception e) {
log.warn("Failed to retrieve block stats for block height: " + blockHeight + " (" + e.getMessage() + ")");
}
}
return result;
}
@Override
public Map<String, String> getTransactions(Transport transport, Wallet wallet, Set<String> txids) {
JsonRpcClient client = new JsonRpcClient(transport);

View file

@ -10,7 +10,7 @@ import com.sparrowwallet.sparrow.AppServices;
import java.util.Date;
@JsonIgnoreProperties(ignoreUnknown = true)
class VerboseTransaction {
public class VerboseTransaction {
public String blockhash;
public long blocktime;
public int confirmations;

View file

@ -7,6 +7,7 @@ import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcOptional;
import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcParam;
import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcService;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.sparrow.net.BlockStats;
import java.util.List;
import java.util.Map;
@ -48,6 +49,9 @@ public interface BitcoindClientService {
@JsonRpcMethod("getblockheader")
VerboseBlockHeader getBlockHeader(@JsonRpcParam("blockhash") String blockhash);
@JsonRpcMethod("getblockstats")
BlockStats getBlockStats(@JsonRpcParam("blockhash") int hash_or_height);
@JsonRpcMethod("getrawtransaction")
Object getRawTransaction(@JsonRpcParam("txid") String txid, @JsonRpcParam("verbose") boolean verbose);

View file

@ -10,6 +10,7 @@ import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.SparrowWallet;
import com.sparrowwallet.sparrow.event.MempoolEntriesInitializedEvent;
import com.sparrowwallet.drongo.Version;
import com.sparrowwallet.sparrow.net.BlockStats;
import com.sparrowwallet.sparrow.net.cormorant.Cormorant;
import com.sparrowwallet.sparrow.net.cormorant.bitcoind.*;
import com.sparrowwallet.sparrow.net.cormorant.index.TxEntry;
@ -157,6 +158,17 @@ public class ElectrumServerService {
}
}
@JsonRpcMethod("blockchain.block.stats")
public BlockStats getBlockStats(@JsonRpcParam("height") int height) throws BitcoindIOException, BlockNotFoundException {
try {
return bitcoindClient.getBitcoindService().getBlockStats(height);
} catch(JsonRpcException e) {
throw new BlockNotFoundException(e.getErrorMessage());
} catch(IllegalStateException e) {
throw new BitcoindIOException(e);
}
}
@JsonRpcMethod("blockchain.transaction.get")
@SuppressWarnings("unchecked")
public Object getTransaction(@JsonRpcParam("tx_hash") String tx_hash, @JsonRpcParam("verbose") @JsonRpcOptional boolean verbose) throws BitcoindIOException, TransactionNotFoundException {