Block: migrate time field to java.time API

The precision of Bitcoin time is only a second, so we need to
truncate any surplus precision to avoid rounding errors in the
PoW verification.
This commit is contained in:
Andreas Schildbach 2023-03-02 00:49:21 +01:00
parent 50caafc8d5
commit d1aa4de677
11 changed files with 96 additions and 50 deletions

View File

@ -39,6 +39,9 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.math.BigInteger;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
@ -78,7 +81,7 @@ public class Block extends Message {
/** How many bytes are required to represent a block header WITHOUT the trailing 00 length byte. */
public static final int HEADER_SIZE = 80;
static final long ALLOWED_TIME_DRIFT = 2 * 60 * 60; // Same value as Bitcoin Core.
static final Duration ALLOWED_TIME_DRIFT = Duration.ofHours(2); // Same value as Bitcoin Core.
/**
* A constant shared by the entire network: how large in bytes a block is allowed to be. One day we may have to
@ -116,7 +119,7 @@ public class Block extends Message {
private long version;
private Sha256Hash prevBlockHash;
private Sha256Hash merkleRoot, witnessRoot;
private long time;
private Instant time;
private long difficultyTarget; // "nBits"
private long nonce;
@ -141,7 +144,7 @@ public class Block extends Message {
// Set up a few basic things. We are not complete after this though.
version = setVersion;
difficultyTarget = 0x1d07fff8L;
time = TimeUtils.currentTimeSeconds();
time = TimeUtils.currentTime().truncatedTo(ChronoUnit.SECONDS); // convert to Bitcoin time
prevBlockHash = Sha256Hash.ZERO_HASH;
length = HEADER_SIZE;
@ -201,12 +204,12 @@ public class Block extends Message {
* @param version This should usually be set to 1 or 2, depending on if the height is in the coinbase input.
* @param prevBlockHash Reference to previous block in the chain or {@link Sha256Hash#ZERO_HASH} if genesis.
* @param merkleRoot The root of the merkle tree formed by the transactions.
* @param time UNIX time when the block was mined.
* @param time time when the block was mined.
* @param difficultyTarget Number which this block hashes lower than.
* @param nonce Arbitrary number to make the block hash lower than the target.
* @param transactions List of transactions including the coinbase.
*/
public Block(NetworkParameters params, long version, Sha256Hash prevBlockHash, Sha256Hash merkleRoot, long time,
public Block(NetworkParameters params, long version, Sha256Hash prevBlockHash, Sha256Hash merkleRoot, Instant time,
long difficultyTarget, long nonce, List<Transaction> transactions) {
super(params);
this.version = version;
@ -219,6 +222,25 @@ public class Block extends Message {
this.transactions.addAll(transactions);
}
/**
* Construct a block initialized with all the given fields.
* @param params Which network the block is for.
* @param version This should usually be set to 1 or 2, depending on if the height is in the coinbase input.
* @param prevBlockHash Reference to previous block in the chain or {@link Sha256Hash#ZERO_HASH} if genesis.
* @param merkleRoot The root of the merkle tree formed by the transactions.
* @param time UNIX time seconds when the block was mined.
* @param difficultyTarget Number which this block hashes lower than.
* @param nonce Arbitrary number to make the block hash lower than the target.
* @param transactions List of transactions including the coinbase.
* @deprecated use {@link #Block(NetworkParameters, long, Sha256Hash, Sha256Hash, Instant, long, long, List)}
*/
@Deprecated
public Block(NetworkParameters params, long version, Sha256Hash prevBlockHash, Sha256Hash merkleRoot, long time,
long difficultyTarget, long nonce, List<Transaction> transactions) {
this(params, version, prevBlockHash, merkleRoot, Instant.ofEpochSecond(time), difficultyTarget, nonce,
transactions);
}
/** @deprecated Use {@link BitcoinNetworkParams#getBlockInflation(int)} */
@Deprecated
public Coin getBlockInflation(int height) {
@ -263,7 +285,7 @@ public class Block extends Message {
version = readUint32();
prevBlockHash = readHash();
merkleRoot = readHash();
time = readUint32();
time = Instant.ofEpochSecond(readUint32());
difficultyTarget = readUint32();
nonce = readUint32();
hash = Sha256Hash.wrapReversed(Sha256Hash.hashTwice(payload, offset, cursor - offset));
@ -325,7 +347,7 @@ public class Block extends Message {
ByteUtils.uint32ToByteStreamLE(version, stream);
stream.write(prevBlockHash.getReversedBytes());
stream.write(getMerkleRoot().getReversedBytes());
ByteUtils.uint32ToByteStreamLE(time, stream);
ByteUtils.uint32ToByteStreamLE(time.getEpochSecond(), stream);
ByteUtils.uint32ToByteStreamLE(difficultyTarget, stream);
ByteUtils.uint32ToByteStreamLE(nonce, stream);
}
@ -517,7 +539,7 @@ public class Block extends Message {
s.append(" (").append(bips).append(')');
s.append('\n');
s.append(" previous block: ").append(getPrevBlockHash()).append("\n");
s.append(" time: ").append(time).append(" (").append(TimeUtils.dateTimeFormat(time * 1000)).append(")\n");
s.append(" time: ").append(time).append(" (").append(TimeUtils.dateTimeFormat(time.toEpochMilli())).append(")\n");
s.append(" difficulty target (nBits): ").append(difficultyTarget).append("\n");
s.append(" nonce: ").append(nonce).append("\n");
if (transactions != null && transactions.size() > 0) {
@ -594,11 +616,12 @@ public class Block extends Message {
}
private void checkTimestamp() throws VerificationException {
final long allowedTime = TimeUtils.currentTimeSeconds() + ALLOWED_TIME_DRIFT;
if (time > allowedTime)
final Instant allowedTime = TimeUtils.currentTime().truncatedTo(ChronoUnit.SECONDS).plus(ALLOWED_TIME_DRIFT);
if (time.isAfter(allowedTime))
throw new VerificationException(String.format(Locale.US,
"Block too far in future: %s (%d) vs allowed %s (%d)", TimeUtils.dateTimeFormat(time * 1000), time,
TimeUtils.dateTimeFormat(allowedTime * 1000), allowedTime));
"Block too far in future: %s (%d) vs allowed %s (%d)",
TimeUtils.dateTimeFormat(time.toEpochMilli()), time.toEpochMilli(),
TimeUtils.dateTimeFormat(allowedTime.toEpochMilli()), allowedTime.toEpochMilli()));
}
private void checkSigOps() throws VerificationException {
@ -877,24 +900,35 @@ public class Block extends Message {
}
/**
* Returns the time at which the block was solved and broadcast, according to the clock of the solving node. This
* is measured in seconds since the UNIX epoch (midnight Jan 1st 1970).
* Returns the time at which the block was solved and broadcast, according to the clock of the solving node.
*/
public long getTimeSeconds() {
public Instant getTimeInstant() {
return time;
}
/**
* Returns the time at which the block was solved and broadcast, according to the clock of the solving node.
* Returns the time at which the block was solved and broadcast, according to the clock of the solving node. This
* is measured in seconds since the UNIX epoch (midnight Jan 1st 1970).
* @deprecated use {@link #getTimeInstant()}
*/
@Deprecated
public long getTimeSeconds() {
return time.getEpochSecond();
}
/**
* Returns the time at which the block was solved and broadcast, according to the clock of the solving node.
* @deprecated use {@link #getTimeInstant()}
*/
@Deprecated
public Date getTime() {
return new Date(getTimeSeconds()*1000);
return new Date(getTimeInstant().toEpochMilli());
}
@VisibleForTesting
public void setTime(long time) {
public void setTime(Instant time) {
unCacheHeader();
this.time = time;
this.time = time.truncatedTo(ChronoUnit.SECONDS); // convert to Bitcoin time
this.hash = null;
}
@ -987,7 +1021,7 @@ public class Block extends Message {
* Returns a solved block that builds on top of this one. This exists for unit tests.
*/
@VisibleForTesting
public Block createNextBlock(Address to, long version, long time, int blockHeight) {
public Block createNextBlock(Address to, long version, Instant time, int blockHeight) {
return createNextBlock(to, version, null, time, pubkeyForTesting, FIFTY_COINS, blockHeight);
}
@ -999,7 +1033,7 @@ public class Block extends Message {
*/
@VisibleForTesting
Block createNextBlock(@Nullable final Address to, final long version,
@Nullable TransactionOutPoint prevOut, final long time,
@Nullable TransactionOutPoint prevOut, final Instant time,
final byte[] pubKey, final Coin coinbaseValue,
final int height) {
Block b = new Block(params, version);
@ -1022,10 +1056,11 @@ public class Block extends Message {
b.setPrevBlockHash(getHash());
// Don't let timestamp go backwards
if (getTimeSeconds() >= time)
b.setTime(getTimeSeconds() + 1);
Instant bitcoinTime = time.truncatedTo(ChronoUnit.SECONDS);
if (getTimeInstant().compareTo(bitcoinTime) >= 0)
b.setTime(getTimeInstant().plusSeconds(1));
else
b.setTime(time);
b.setTime(bitcoinTime);
b.solve();
try {
b.verifyHeader();
@ -1049,12 +1084,12 @@ public class Block extends Message {
@VisibleForTesting
public Block createNextBlock(@Nullable Address to, TransactionOutPoint prevOut) {
return createNextBlock(to, BLOCK_VERSION_GENESIS, prevOut, getTimeSeconds() + 5, pubkeyForTesting, FIFTY_COINS, BLOCK_HEIGHT_UNKNOWN);
return createNextBlock(to, BLOCK_VERSION_GENESIS, prevOut, getTimeInstant().plusSeconds(5), pubkeyForTesting, FIFTY_COINS, BLOCK_HEIGHT_UNKNOWN);
}
@VisibleForTesting
public Block createNextBlock(@Nullable Address to, Coin value) {
return createNextBlock(to, BLOCK_VERSION_GENESIS, null, getTimeSeconds() + 5, pubkeyForTesting, value, BLOCK_HEIGHT_UNKNOWN);
return createNextBlock(to, BLOCK_VERSION_GENESIS, null, getTimeInstant().plusSeconds(5), pubkeyForTesting, value, BLOCK_HEIGHT_UNKNOWN);
}
@VisibleForTesting
@ -1065,7 +1100,7 @@ public class Block extends Message {
@VisibleForTesting
public Block createNextBlockWithCoinbase(long version, byte[] pubKey, Coin coinbaseValue, final int height) {
return createNextBlock(null, version, (TransactionOutPoint) null,
TimeUtils.currentTimeSeconds(), pubKey, coinbaseValue, height);
TimeUtils.currentTime(), pubKey, coinbaseValue, height);
}
/**
@ -1075,7 +1110,7 @@ public class Block extends Message {
@VisibleForTesting
Block createNextBlockWithCoinbase(long version, byte[] pubKey, final int height) {
return createNextBlock(null, version, (TransactionOutPoint) null,
TimeUtils.currentTimeSeconds(), pubKey, FIFTY_COINS, height);
TimeUtils.currentTime(), pubKey, FIFTY_COINS, height);
}
@VisibleForTesting

View File

@ -22,6 +22,8 @@ import org.bitcoinj.base.utils.ByteUtils;
import org.bitcoinj.core.Block;
import org.bitcoinj.base.Sha256Hash;
import java.time.Instant;
import static com.google.common.base.Preconditions.checkState;
/**
@ -134,7 +136,7 @@ public class MainNetParams extends BitcoinNetworkParams {
if (genesisBlock == null) {
genesisBlock = Block.createGenesis(this);
genesisBlock.setDifficultyTarget(Block.STANDARD_MAX_DIFFICULTY_TARGET);
genesisBlock.setTime(GENESIS_TIME);
genesisBlock.setTime(Instant.ofEpochSecond(GENESIS_TIME));
genesisBlock.setNonce(GENESIS_NONCE);
checkState(genesisBlock.getHash().equals(GENESIS_HASH), "Invalid genesis hash");
}

View File

@ -22,6 +22,8 @@ import org.bitcoinj.base.utils.ByteUtils;
import org.bitcoinj.core.Block;
import org.bitcoinj.base.Sha256Hash;
import java.time.Instant;
import static com.google.common.base.Preconditions.checkState;
/**
@ -82,7 +84,7 @@ public class RegTestParams extends BitcoinNetworkParams {
if (genesisBlock == null) {
genesisBlock = Block.createGenesis(this);
genesisBlock.setDifficultyTarget(Block.EASIEST_DIFFICULTY_TARGET);
genesisBlock.setTime(GENESIS_TIME);
genesisBlock.setTime(Instant.ofEpochSecond(GENESIS_TIME));
genesisBlock.setNonce(GENESIS_NONCE);
checkState(genesisBlock.getHash().equals(GENESIS_HASH), "Invalid genesis hash");
}

View File

@ -21,6 +21,8 @@ import org.bitcoinj.base.utils.ByteUtils;
import org.bitcoinj.core.Block;
import org.bitcoinj.base.Sha256Hash;
import java.time.Instant;
import static com.google.common.base.Preconditions.checkState;
/**
@ -79,7 +81,7 @@ public class SigNetParams extends BitcoinNetworkParams {
if (genesisBlock == null) {
genesisBlock = Block.createGenesis(this);
genesisBlock.setDifficultyTarget(GENESIS_DIFFICULTY);
genesisBlock.setTime(GENESIS_TIME);
genesisBlock.setTime(Instant.ofEpochSecond(GENESIS_TIME));
genesisBlock.setNonce(GENESIS_NONCE);
checkState(genesisBlock.getHash().equals(GENESIS_HASH), "Invalid genesis hash");
}

View File

@ -28,6 +28,7 @@ import org.bitcoinj.store.BlockStore;
import org.bitcoinj.store.BlockStoreException;
import java.math.BigInteger;
import java.time.Instant;
import java.util.Date;
import static com.google.common.base.Preconditions.checkState;
@ -90,7 +91,7 @@ public class TestNet3Params extends BitcoinNetworkParams {
if (genesisBlock == null) {
genesisBlock = Block.createGenesis(this);
genesisBlock.setDifficultyTarget(Block.STANDARD_MAX_DIFFICULTY_TARGET);
genesisBlock.setTime(GENESIS_TIME);
genesisBlock.setTime(Instant.ofEpochSecond(GENESIS_TIME));
genesisBlock.setNonce(GENESIS_NONCE);
checkState(genesisBlock.getHash().equals(GENESIS_HASH), "Invalid genesis hash");
}

View File

@ -75,7 +75,7 @@ public class UnitTestParams extends BitcoinNetworkParams {
if (genesisBlock == null) {
genesisBlock = Block.createGenesis(this);
genesisBlock.setDifficultyTarget(Block.EASIEST_DIFFICULTY_TARGET);
genesisBlock.setTime(TimeUtils.currentTimeSeconds());
genesisBlock.setTime(TimeUtils.currentTime());
genesisBlock.solve();
}
}

View File

@ -43,6 +43,7 @@ import org.bitcoinj.store.BlockStoreException;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.time.Instant;
import java.util.Random;
import static com.google.common.base.Preconditions.checkState;
@ -270,7 +271,7 @@ public class FakeTxBuilder {
try {
Block previousBlock = previousStoredBlock.getHeader();
Address to = randomAddress(previousBlock.getParams());
Block b = previousBlock.createNextBlock(to, version, timeSeconds, height);
Block b = previousBlock.createNextBlock(to, version, Instant.ofEpochSecond(timeSeconds), height);
// Coinbase tx was already added.
for (Transaction tx : transactions) {
tx.getConfidence().setSource(TransactionConfidence.Source.NETWORK);

View File

@ -40,6 +40,7 @@ import org.junit.rules.ExpectedException;
import java.math.BigInteger;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.util.Date;
import java.util.Locale;
import java.util.concurrent.CompletableFuture;
@ -173,7 +174,7 @@ public class BlockChainTest {
Block prev = UNITTEST.getGenesisBlock();
TimeUtils.setMockClock();
for (int height = 0; height < UNITTEST.getInterval() - 1; height++) {
Block newBlock = prev.createNextBlock(coinbaseTo, 1, TimeUtils.currentTimeSeconds(), height);
Block newBlock = prev.createNextBlock(coinbaseTo, 1, TimeUtils.currentTime(), height);
assertTrue(chain.add(newBlock));
prev = newBlock;
// The fake chain should seem to be "fast" for the purposes of difficulty calculations.
@ -181,13 +182,13 @@ public class BlockChainTest {
}
// Now add another block that has no difficulty adjustment, it should be rejected.
try {
chain.add(prev.createNextBlock(coinbaseTo, 1, TimeUtils.currentTimeSeconds(), UNITTEST.getInterval()));
chain.add(prev.createNextBlock(coinbaseTo, 1, TimeUtils.currentTime(), UNITTEST.getInterval()));
fail();
} catch (VerificationException e) {
}
// Create a new block with the right difficulty target given our blistering speed relative to the huge amount
// of time it's supposed to take (set in the unit test network parameters).
Block b = prev.createNextBlock(coinbaseTo, 1, TimeUtils.currentTimeSeconds(), UNITTEST.getInterval() + 1);
Block b = prev.createNextBlock(coinbaseTo, 1, TimeUtils.currentTime(), UNITTEST.getInterval() + 1);
b.setDifficultyTarget(0x201fFFFFL);
b.solve();
assertTrue(chain.add(b));
@ -204,7 +205,7 @@ public class BlockChainTest {
bad.setMerkleRoot(Sha256Hash.wrap("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"));
// Nonce was just some number that made the hash < difficulty limit set below, it can be anything.
bad.setNonce(140548933);
bad.setTime(1279242649);
bad.setTime(Instant.ofEpochSecond(1279242649));
bad.setPrevBlockHash(b2.getHash());
// We're going to make this block so easy 50% of solutions will pass, and check it gets rejected for having a
// bad difficulty target. Unfortunately the encoding mechanism means we cannot make one that accepts all
@ -413,7 +414,7 @@ public class BlockChainTest {
Block b2 = new Block(TESTNET, Block.BLOCK_VERSION_GENESIS);
b2.setMerkleRoot(Sha256Hash.wrap("20222eb90f5895556926c112bb5aa0df4ab5abc3107e21a6950aec3b2e3541e2"));
b2.setNonce(875942400L);
b2.setTime(1296688946L);
b2.setTime(Instant.ofEpochSecond(1296688946L));
b2.setDifficultyTarget(0x1d00ffff);
b2.setPrevBlockHash(Sha256Hash.wrap("00000000b873e79784647a6c82962c70d228557d24a747ea4d1b8bbe878e1206"));
assertEquals("000000006c02c8ea6e4ff69651f7fcde348fb9d557a06e6957b65552002a7820", b2.getHashAsString());
@ -425,7 +426,7 @@ public class BlockChainTest {
Block b1 = new Block(TESTNET, Block.BLOCK_VERSION_GENESIS);
b1.setMerkleRoot(Sha256Hash.wrap("f0315ffc38709d70ad5647e22048358dd3745f3ce3874223c80a7c92fab0c8ba"));
b1.setNonce(1924588547);
b1.setTime(1296688928);
b1.setTime(Instant.ofEpochSecond(1296688928));
b1.setDifficultyTarget(0x1d00ffff);
b1.setPrevBlockHash(Sha256Hash.wrap("000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943"));
assertEquals("00000000b873e79784647a6c82962c70d228557d24a747ea4d1b8bbe878e1206", b1.getHashAsString());

View File

@ -40,6 +40,7 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.math.BigInteger;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumSet;
@ -378,7 +379,7 @@ public class BlockTest {
public void testGenesisBlock() {
Block genesisBlock = Block.createGenesis(MainNetParams.get());
genesisBlock.setDifficultyTarget(0x1d00ffffL);
genesisBlock.setTime(1231006505L);
genesisBlock.setTime(Instant.ofEpochSecond(1231006505L));
genesisBlock.setNonce(2083236893);
assertEquals(Sha256Hash.wrap("000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"), genesisBlock.getHash());
}

View File

@ -37,6 +37,7 @@ import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.time.Period;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
@ -915,7 +916,7 @@ public class FullBlockTestGenerator {
b44.addTransaction(t);
b44.setPrevBlockHash(b43.getHash());
b44.setTime(b43.block.getTimeSeconds() + 1);
b44.setTime(b43.block.getTimeInstant().plusSeconds(1));
}
b44.solve();
blocks.add(new BlockAndValidity(b44, true, false, b44.getHash(), chainHeadHeight + 15, "b44"));
@ -943,7 +944,7 @@ public class FullBlockTestGenerator {
b45.addTransaction(t, false);
b45.setPrevBlockHash(b44.getHash());
b45.setTime(b44.getTimeSeconds() + 1);
b45.setTime(b44.getTimeInstant().plusSeconds(1));
}
b45.solve();
blocks.add(new BlockAndValidity(b45, false, true, b44.getHash(), chainHeadHeight + 15, "b45"));
@ -956,7 +957,7 @@ public class FullBlockTestGenerator {
b46.setMerkleRoot(Sha256Hash.ZERO_HASH);
b46.setPrevBlockHash(b44.getHash());
b46.setTime(b44.getTimeSeconds() + 1);
b46.setTime(b44.getTimeInstant().plusSeconds(1));
}
b46.solve();
blocks.add(new BlockAndValidity(b46, false, true, b44.getHash(), chainHeadHeight + 15, "b46"));
@ -982,7 +983,7 @@ public class FullBlockTestGenerator {
// Block with timestamp > 2h in the future
NewBlock b48 = createNextBlock(b44, chainHeadHeight + 16, out15, null);
b48.block.setTime(TimeUtils.currentTimeSeconds() + 60 * 60 * 3);
b48.block.setTime(TimeUtils.currentTime().plusSeconds(60 * 60 * 3));
b48.solve();
blocks.add(new BlockAndValidity(b48, false, true, b44.getHash(), chainHeadHeight + 15, "b48"));
@ -1038,13 +1039,13 @@ public class FullBlockTestGenerator {
// Block with invalid timestamp
NewBlock b54 = createNextBlock(b53, chainHeadHeight + 16, out15, null);
b54.block.setTime(b35.block.getTimeSeconds() - 1);
b54.block.setTime(b35.block.getTimeInstant().minusSeconds(1));
b54.solve();
blocks.add(new BlockAndValidity(b54, false, true, b44.getHash(), chainHeadHeight + 15, "b54"));
// Block with valid timestamp
NewBlock b55 = createNextBlock(b53, chainHeadHeight + 16, out15, null);
b55.block.setTime(b35.block.getTimeSeconds());
b55.block.setTime(b35.block.getTimeInstant());
b55.solve();
blocks.add(new BlockAndValidity(b55, true, false, b55.getHash(), chainHeadHeight + 16, "b55"));
spendableOutputs.offer(b55.getCoinbaseOutput());

View File

@ -454,15 +454,15 @@ public class PeerTest extends TestWithNetworkConnections {
blockChain.add(b1);
TimeUtils.rollMockClock(60 * 10); // 10 minutes later.
Block b2 = makeSolvedTestBlock(b1);
b2.setTime(TimeUtils.currentTimeSeconds());
b2.setTime(TimeUtils.currentTime());
b2.solve();
TimeUtils.rollMockClock(60 * 10); // 10 minutes later.
Block b3 = makeSolvedTestBlock(b2);
b3.setTime(TimeUtils.currentTimeSeconds());
b3.setTime(TimeUtils.currentTime());
b3.solve();
TimeUtils.rollMockClock(60 * 10);
Block b4 = makeSolvedTestBlock(b3);
b4.setTime(TimeUtils.currentTimeSeconds());
b4.setTime(TimeUtils.currentTime());
b4.solve();
// Request headers until the last 2 blocks.