Script: migrate creationTime field to java.time API

This commit is contained in:
Andreas Schildbach 2023-03-09 14:43:41 +01:00
parent 9ba34fc2ed
commit a61da4fff6
6 changed files with 111 additions and 35 deletions

View file

@ -54,6 +54,7 @@ import java.io.OutputStream;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@ -61,6 +62,7 @@ import java.util.EnumSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import static com.google.common.base.Preconditions.checkArgument;
@ -222,8 +224,8 @@ public class Script {
// must preserve the exact bytes that we read off the wire, along with the parsed form.
protected byte[] program;
// Creation time of the associated keys in seconds since the epoch.
private long creationTimeSeconds;
// Creation time of the associated keys, or null if unknown.
@Nullable private Instant creationTime;
/** Creates an empty script that serializes to nothing. */
private Script() {
@ -233,32 +235,71 @@ public class Script {
// Used from ScriptBuilder.
Script(List<ScriptChunk> chunks) {
this.chunks = Collections.unmodifiableList(new ArrayList<>(chunks));
creationTimeSeconds = TimeUtils.currentTimeSeconds();
creationTime = TimeUtils.currentTime();
}
/**
* Construct a Script that copies and wraps the programBytes array. The array is parsed and checked for syntactic
* validity. Use this if the creation time is not known.
* @param programBytes Array of program bytes from a transaction.
*/
public Script(byte[] programBytes) throws ScriptException {
this.program = programBytes;
parse(programBytes);
this.creationTime = null;
}
/**
* Construct a Script that copies and wraps the programBytes array. The array is parsed and checked for syntactic
* validity.
* @param programBytes Array of program bytes from a transaction.
* @param creationTime creation time of the script
*/
public Script(byte[] programBytes) throws ScriptException {
program = programBytes;
public Script(byte[] programBytes, Instant creationTime) throws ScriptException {
this.program = programBytes;
parse(programBytes);
creationTimeSeconds = 0;
this.creationTime = checkNotNull(creationTime);
}
public Script(byte[] programBytes, long creationTimeSeconds) throws ScriptException {
program = programBytes;
parse(programBytes);
this.creationTimeSeconds = creationTimeSeconds;
/**
* Gets the creation time of this script, or empty if unknown.
* @return creation time of this script, or empty if unknown
*/
public Optional<Instant> getCreationTime() {
return Optional.ofNullable(creationTime);
}
/** @deprecated use {@link #getCreationTime()} */
@Deprecated
public long getCreationTimeSeconds() {
return creationTimeSeconds;
return getCreationTime().orElse(Instant.EPOCH).getEpochSecond();
}
public void setCreationTimeSeconds(long creationTimeSeconds) {
this.creationTimeSeconds = creationTimeSeconds;
/**
* Sets the creation time of this script.
* @param creationTime creation time of this script
*/
public void setCreationTime(Instant creationTime) {
this.creationTime = checkNotNull(creationTime);
}
/**
* Clears the creation time of this script. This is mainly used deserialization and cloning. Normally you should not
* need to usethis, as keys should have proper creation times whenever possible.
*/
public void clearCreationTime() {
this.creationTime = null;
}
/** @deprecated use {@link #setCreationTime(Instant)} */
@Deprecated
public void setCreationTimeSeconds(long creationTimeSecs) {
if (creationTimeSecs > 0)
setCreationTime(Instant.ofEpochSecond(creationTimeSecs));
else if (creationTimeSecs == 0)
clearCreationTime();
else
throw new IllegalArgumentException("Cannot set creation time to negative value: " + creationTimeSecs);
}
/**

View file

@ -252,8 +252,7 @@ public class MarriedKeyChain extends DeterministicKeyChain {
builder.append(script.getToAddress(params));
builder.append(" hash160:");
builder.append(ByteUtils.formatHex(script.getPubKeyHash()));
if (script.getCreationTimeSeconds() > 0)
builder.append(" creationTimeSeconds:").append(script.getCreationTimeSeconds());
script.getCreationTime().ifPresent(creationTime -> builder.append(" creationTimeSeconds:").append(creationTime));
builder.append('\n');
}

View file

@ -1038,38 +1038,69 @@ public class Wallet extends BaseTaggableObject
* Same as {@link #addWatchedAddress(Address, long)} with the current time as the creation time.
*/
public boolean addWatchedAddress(final Address address) {
long now = TimeUtils.currentTimeSeconds();
Instant now = TimeUtils.currentTime();
return addWatchedAddresses(Lists.newArrayList(address), now) == 1;
}
/**
* Adds the given address to the wallet to be watched. Outputs can be retrieved by {@link #getWatchedOutputs(boolean)}.
*
* @param creationTime creation time in seconds since the epoch, for scanning the blockchain
* @param creationTime creation time, for scanning the blockchain
* @return whether the address was added successfully (not already present)
*/
public boolean addWatchedAddress(final Address address, long creationTime) {
public boolean addWatchedAddress(final Address address, Instant creationTime) {
return addWatchedAddresses(Lists.newArrayList(address), creationTime) == 1;
}
/** @deprecated use {@link #addWatchedAddress(Address, Instant)} or {@link #addWatchedAddress(Address)} */
@Deprecated
public boolean addWatchedAddress(final Address address, long creationTime) {
return creationTime > 0 ?
addWatchedAddress(address, Instant.ofEpochSecond(creationTime)) :
addWatchedAddress(address);
}
/**
* Adds the given address to the wallet to be watched. Outputs can be retrieved
* Adds the given addresses to the wallet to be watched. Outputs can be retrieved
* by {@link #getWatchedOutputs(boolean)}.
*
* @param addresses addresses to be watched
* @param creationTime creation time of the addresses
* @return how many addresses were added successfully
*/
public int addWatchedAddresses(final List<Address> addresses, long creationTime) {
public int addWatchedAddresses(final List<Address> addresses, Instant creationTime) {
List<Script> scripts = new ArrayList<>();
for (Address address : addresses) {
Script script = ScriptBuilder.createOutputScript(address);
script.setCreationTimeSeconds(creationTime);
script.setCreationTime(creationTime);
scripts.add(script);
}
return addWatchedScripts(scripts);
}
/**
* Adds the given addresses to the wallet to be watched. Outputs can be retrieved
* by {@link #getWatchedOutputs(boolean)}. Use this if the creation time of the addresses is unknown.
* @param addresses addresses to be watched
* @return how many addresses were added successfully
*/
public int addWatchedAddresses(final List<Address> addresses) {
List<Script> scripts = new ArrayList<>();
for (Address address : addresses) {
Script script = ScriptBuilder.createOutputScript(address);
script.clearCreationTime();
scripts.add(script);
}
return addWatchedScripts(scripts);
}
/** @deprecated use {@link #addWatchedAddresses(List, Instant)} or {@link #addWatchedAddresses(List)} */
@Deprecated
public int addWatchedAddresses(final List<Address> addresses, long creationTime) {
return creationTime > 0 ?
addWatchedAddresses(addresses, creationTime) :
addWatchedAddresses(addresses);
}
/**
* Adds the given output scripts to the wallet to be watched. Outputs can be retrieved by {@link #getWatchedOutputs(boolean)}.
* If a script is already being watched, the object is replaced with the one in the given list. As {@link Script}
@ -1088,7 +1119,7 @@ public class Wallet extends BaseTaggableObject
// a script in the wallet with an incorrect creation time.
if (watchedScripts.contains(script))
watchedScripts.remove(script);
if (script.getCreationTimeSeconds() == 0)
if (!script.getCreationTime().isPresent())
log.warn("Adding a script to the wallet with a creation time of zero, this will disable the checkpointing optimization! {}", script);
watchedScripts.add(script);
added++;
@ -3596,7 +3627,7 @@ public class Wallet extends BaseTaggableObject
try {
Instant earliestTime = keyChainGroup.getEarliestKeyCreationTimeInstant();
for (Script script : watchedScripts)
earliestTime = TimeUtils.earlier(Instant.ofEpochSecond(script.getCreationTimeSeconds()), earliestTime);
earliestTime = TimeUtils.earlier(script.getCreationTime().orElse(Instant.EPOCH), earliestTime);
return earliestTime;
} finally {
keyChainGroupLock.unlock();

View file

@ -189,7 +189,7 @@ public class WalletProtobufSerializer {
Protos.Script protoScript =
Protos.Script.newBuilder()
.setProgram(ByteString.copyFrom(script.getProgram()))
.setCreationTimestamp(script.getCreationTimeSeconds() * 1000)
.setCreationTimestamp(script.getCreationTime().orElse(Instant.EPOCH).toEpochMilli())
.build();
walletBuilder.addWatchedScript(protoScript);
@ -502,9 +502,13 @@ public class WalletProtobufSerializer {
List<Script> scripts = new ArrayList<>();
for (Protos.Script protoScript : walletProto.getWatchedScriptList()) {
try {
Script script =
new Script(protoScript.getProgram().toByteArray(),
protoScript.getCreationTimestamp() / 1000);
long creationTimestamp = protoScript.getCreationTimestamp();
byte[] programBytes = protoScript.getProgram().toByteArray();
Script script;
if (creationTimestamp > 0)
script = new Script(programBytes, Instant.ofEpochMilli(creationTimestamp));
else
script = new Script(programBytes);
scripts.add(script);
} catch (ScriptException e) {
throw new UnreadableWalletException("Unparseable script in wallet");

View file

@ -67,6 +67,7 @@ import java.math.BigInteger;
import java.net.InetAddress;
import java.security.SecureRandom;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
@ -93,7 +94,7 @@ public class WalletProtobufSerializerTest {
private Wallet myWallet;
public static String WALLET_DESCRIPTION = "The quick brown fox lives in \u4f26\u6566"; // Beijing in Chinese
private long mScriptCreationTime;
private Instant mScriptCreationTime;
@BeforeClass
public static void setUpClass() {
@ -110,7 +111,7 @@ public class WalletProtobufSerializerTest {
myAddress = myKey.toAddress(ScriptType.P2PKH, BitcoinNetwork.TESTNET);
myWallet = new Wallet(TESTNET, KeyChainGroup.builder(TESTNET).fromRandom(ScriptType.P2PKH).build());
myWallet.importKey(myKey);
mScriptCreationTime = new Date().getTime() / 1000 - 1234;
mScriptCreationTime = TimeUtils.currentTime().minusSeconds(1234);
myWallet.addWatchedAddress(myWatchedKey.toAddress(ScriptType.P2PKH, BitcoinNetwork.TESTNET), mScriptCreationTime);
myWallet.setDescription(WALLET_DESCRIPTION);
}
@ -125,8 +126,8 @@ public class WalletProtobufSerializerTest {
assertArrayEquals(myKey.getPubKey(), foundKey.getPubKey());
assertArrayEquals(myKey.getPrivKeyBytes(), foundKey.getPrivKeyBytes());
assertEquals(myKey.getCreationTimeSeconds(), foundKey.getCreationTimeSeconds());
assertEquals(mScriptCreationTime,
wallet1.getWatchedScripts().get(0).getCreationTimeSeconds());
assertEquals(mScriptCreationTime.truncatedTo(ChronoUnit.MILLIS),
wallet1.getWatchedScripts().get(0).getCreationTime().get());
assertEquals(1, wallet1.getWatchedScripts().size());
assertEquals(ScriptBuilder.createOutputScript(myWatchedKey.toAddress(ScriptType.P2PKH, BitcoinNetwork.TESTNET)),
wallet1.getWatchedScripts().get(0));

View file

@ -641,7 +641,7 @@ public class WalletTool implements Callable<Integer> {
try {
Address address = LegacyAddress.fromBase58(params.network(), addrStr);
// If no creation time is specified, assume genesis (zero).
wallet.addWatchedAddress(address, getCreationTimeSeconds());
wallet.addWatchedAddress(address, Instant.ofEpochSecond(getCreationTimeSeconds()));
} catch (AddressFormatException e) {
System.err.println("Could not parse given address, or wrong network: " + addrStr);
}