Skip to content

Commit

Permalink
Add Bitcoin Core version & health check to RpcService
Browse files Browse the repository at this point in the history
Make a 'getnetworkinfo' RPC call to bitcoind immediately upon startup,
to check that the node is up (and throw a ConnectException to ensure
that the user is presented with an appropriate warning popup otherwise).
Log a warning if the node version is outside the 0.18.0 - 0.20.1 range.

Additionally, call 'getbestblockhash' to check that the chain tip is not
stale (> 6 hours old). As part of this, make sure the 'getblock' RPC
call works correctly with verbosity < 2, by fixing JSON deserialisation
of the response when the block or txs are in summary (hex string) form.

(These version & health checks are almost identical to the ones done by
the original btcd-cli4j library during RPC client startup.)
  • Loading branch information
stejbac committed Jan 12, 2021
1 parent 0fd7432 commit d92867b
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 4 deletions.
42 changes: 41 additions & 1 deletion core/src/main/java/bisq/core/dao/node/full/RpcService.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
import bisq.core.dao.state.model.blockchain.TxInput;
import bisq.core.user.Preferences;

import bisq.network.http.HttpException;

import bisq.common.UserThread;
import bisq.common.config.Config;
import bisq.common.handlers.ResultHandler;
Expand All @@ -38,17 +40,24 @@
import javax.inject.Named;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Range;
import com.google.common.primitives.Chars;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;

import java.io.IOException;

import java.math.BigDecimal;

import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.stream.Collectors;

Expand All @@ -69,6 +78,7 @@ public class RpcService {
private static final Set<String> BSQ_TXS_DISALLOWING_SEGWIT_PUB_KEYS = Set.of(
"d1f45e55be6101b1b75e6bf9fc5e5341c6ab420647be7555863bbbddd84e92f3" // in mainnet block 660384, 2020-12-07
);
private static final Range<Integer> SUPPORTED_NODE_VERSION_RANGE = Range.closedOpen(180000, 210000);

private final String rpcUser;
private final String rpcPassword;
Expand Down Expand Up @@ -143,12 +153,13 @@ void setup(ResultHandler resultHandler, Consumer<Throwable> errorHandler) {
.rpcUser(rpcUser)
.rpcPassword(rpcPassword)
.build();
checkNodeVersionAndHealth();

daemon = new BitcoindDaemon(rpcBlockHost, rpcBlockPort, throwable -> {
log.error(throwable.toString());
throwable.printStackTrace();
UserThread.execute(() -> errorHandler.accept(new RpcException(throwable)));
});
// TODO: Client should ping or request network info from bitcoind to make sure it is up.

log.info("Setup took {} ms", System.currentTimeMillis() - startTs);
} catch (Throwable e) {
Expand All @@ -170,6 +181,35 @@ public void onFailure(@NotNull Throwable throwable) {
}, MoreExecutors.directExecutor());
}

private String decodeNodeVersion(Integer encodedVersion) {
var paddedEncodedVersion = Strings.padStart(encodedVersion.toString(), 8, '0');

return Lists.partition(Chars.asList(paddedEncodedVersion.toCharArray()), 2).stream()
.map(chars -> new String(Chars.toArray(chars)).replaceAll("^0", ""))
.collect(Collectors.joining("."))
.replaceAll("\\.0$", "");
}

private void checkNodeVersionAndHealth() throws IOException, HttpException {
var networkInfo = client.getNetworkInfo();
var nodeVersion = decodeNodeVersion(networkInfo.getVersion());

if (SUPPORTED_NODE_VERSION_RANGE.contains(networkInfo.getVersion())) {
log.info("Got Bitcoin Core version: {}", nodeVersion);
} else {
log.warn("Server version mismatch - client optimized for '[{} .. {})', node responded with '{}'",
decodeNodeVersion(SUPPORTED_NODE_VERSION_RANGE.lowerEndpoint()),
decodeNodeVersion(SUPPORTED_NODE_VERSION_RANGE.upperEndpoint()), nodeVersion);
}

var bestRawBlock = client.getBlock(client.getBestBlockHash(), 1);
long currentTime = System.currentTimeMillis() / 1000;
if ((currentTime - bestRawBlock.getTime()) > TimeUnit.HOURS.toSeconds(6)) {
log.warn("Last available block was mined >{} hours ago; please check your network connection",
((currentTime - bestRawBlock.getTime()) / 3600));
}
}

void addNewDtoBlockHandler(Consumer<RawBlock> dtoBlockHandler,
Consumer<Throwable> errorHandler) {
daemon.setBlockListener(blockHash -> {
Expand Down
18 changes: 18 additions & 0 deletions core/src/main/java/bisq/core/dao/node/full/rpc/dto/RawBlock.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,18 @@

package bisq.core.dao.node.full.rpc.dto;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import com.fasterxml.jackson.annotation.JsonValue;

import java.util.List;

import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Data
Expand Down Expand Up @@ -63,4 +67,18 @@ public class RawBlock {
private String previousBlockHash;
@JsonProperty("nextblockhash")
private String nextBlockHash;

@JsonCreator
public static Summarized summarized(String hex) {
var result = new Summarized();
result.setHex(hex);
return result;
}

@Data
@EqualsAndHashCode(callSuper = true)
public static class Summarized extends RawBlock {
@Getter(onMethod_ = @JsonValue)
private String hex;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@

package bisq.core.dao.node.full.rpc.dto;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import com.fasterxml.jackson.annotation.JsonValue;

import java.util.List;

Expand Down Expand Up @@ -56,4 +58,19 @@ public class RawTransaction {
private Long blockTime;
private Long time;
private String hex;

@JsonCreator
public static Summarized summarized(String hex) {
var result = new Summarized();
result.setHex(hex);
return result;
}

public static class Summarized extends RawTransaction {
@Override
@JsonValue
public String getHex() {
return super.getHex();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@

package bisq.core.dao.node.full.rpc;

import bisq.core.dao.node.full.rpc.dto.RawBlock;
import bisq.core.dao.node.full.rpc.dto.RawTransaction;

import bisq.network.http.HttpException;

import com.fasterxml.jackson.databind.ObjectMapper;
Expand All @@ -36,7 +39,6 @@
import java.io.IOException;

import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;

import static java.nio.charset.StandardCharsets.UTF_8;
Expand Down Expand Up @@ -149,13 +151,11 @@ public void testGetBlockHash_heightOutOfRange() throws Exception {
}

@Test
@Ignore // TODO: Allow serialization/deserialization between RawBlock (with special field) and Json string
public void testGetBlock_verbosity_0() throws Exception {
doTestGetBlock(0, "\"" + TEST_BLOCK_VERBOSITY_0 + "\"");
}

@Test
@Ignore // TODO: Allow serialization/deserialization between RawTransaction (with special field) and Json string
public void testGetBlock_verbosity_1() throws Exception {
doTestGetBlock(1, TEST_BLOCK_VERBOSITY_1);
}
Expand All @@ -173,6 +173,10 @@ private void doTestGetBlock(int verbosity, String blockJson) throws Exception {
var block = client.getBlock(TEST_BLOCK_HASH, verbosity);
var blockJsonRoundTripped = new ObjectMapper().writeValueAsString(block);

assertEquals(verbosity == 0, block instanceof RawBlock.Summarized);
assertEquals(verbosity == 1, block.getTx() != null &&
block.getTx().stream().allMatch(tx -> tx instanceof RawTransaction.Summarized));

assertEquals(blockJson, blockJsonRoundTripped);
assertEquals(expectedRequest, mockOutputStream.toString(UTF_8));
}
Expand Down

0 comments on commit d92867b

Please sign in to comment.