mirror of
https://github.com/Qortal/altcoinj.git
synced 2025-02-01 07:42:17 +00:00
Merge branch 'master' of https://github.com/bitcoinj/bitcoinj into rebase
Conflicts: README.md core/pom.xml core/src/main/java/com/dogecoin/dogecoinj/core/DownloadListener.java core/src/main/java/com/dogecoin/dogecoinj/core/MemoryPool.java core/src/main/java/com/dogecoin/dogecoinj/core/NetworkParameters.java core/src/main/java/com/dogecoin/dogecoinj/core/PeerFilterProvider.java core/src/main/java/com/dogecoin/dogecoinj/core/PeerGroup.java core/src/main/java/com/dogecoin/dogecoinj/core/StoredTransactionOutput.java core/src/main/java/com/dogecoin/dogecoinj/core/Wallet.java core/src/main/java/com/dogecoin/dogecoinj/crypto/EncryptedPrivateKey.java core/src/main/java/com/dogecoin/dogecoinj/crypto/KeyCrypterScrypt.java core/src/main/java/com/dogecoin/dogecoinj/jni/NativeTransactionConfidenceListener.java core/src/main/java/com/dogecoin/dogecoinj/kits/WalletAppKit.java core/src/main/java/com/dogecoin/dogecoinj/net/FilterMerger.java core/src/main/java/com/dogecoin/dogecoinj/protocols/channels/IPaymentChannelClient.java core/src/main/java/com/dogecoin/dogecoinj/protocols/channels/PaymentChannelClient.java core/src/main/java/com/dogecoin/dogecoinj/protocols/channels/PaymentChannelClientConnection.java core/src/main/java/com/dogecoin/dogecoinj/protocols/channels/PaymentChannelClientState.java core/src/main/java/com/dogecoin/dogecoinj/store/BlockStore.java core/src/main/java/com/dogecoin/dogecoinj/store/FullPrunedBlockStore.java core/src/main/java/com/dogecoin/dogecoinj/store/H2FullPrunedBlockStore.java core/src/main/java/com/dogecoin/dogecoinj/store/PostgresFullPrunedBlockStore.java core/src/main/java/com/dogecoin/dogecoinj/testing/FakeTxBuilder.java core/src/main/java/com/dogecoin/dogecoinj/testing/TestWithPeerGroup.java core/src/main/java/com/dogecoin/dogecoinj/wallet/KeyChainGroup.java core/src/main/java/org/bitcoinj/core/DownloadListener.java core/src/main/java/org/bitcoinj/core/DownloadProgressTracker.java core/src/main/java/org/bitcoinj/core/MemoryPool.java core/src/main/java/org/bitcoinj/core/StoredTransactionOutput.java core/src/main/java/org/bitcoinj/core/TxConfidenceTable.java core/src/main/java/org/bitcoinj/core/UTXO.java core/src/main/java/org/bitcoinj/params/MainNetParams.java core/src/test/java/com/dogecoin/dogecoinj/core/AbstractFullPrunedBlockChainTest.java core/src/test/java/com/dogecoin/dogecoinj/core/BitcoindComparisonTool.java core/src/test/java/com/dogecoin/dogecoinj/core/FilteredBlockAndPartialMerkleTreeTests.java core/src/test/java/com/dogecoin/dogecoinj/core/FullBlockTestGenerator.java core/src/test/java/com/dogecoin/dogecoinj/core/PostgresFullPrunedBlockChainTest.java core/src/test/java/com/dogecoin/dogecoinj/core/WalletTest.java core/src/test/java/com/dogecoin/dogecoinj/crypto/ChildKeyDerivationTest.java core/src/test/java/com/dogecoin/dogecoinj/store/WalletProtobufSerializerTest.java core/src/test/java/com/dogecoin/dogecoinj/wallet/DefaultRiskAnalysisTest.java core/src/test/java/com/dogecoin/dogecoinj/wallet/DeterministicKeyChainTest.java tools/src/main/java/com/dogecoin/dogecoinj/tools/WalletTool.java wallettemplate/pom.xml wallettemplate/src/main/java/wallettemplate/Main.java wallettemplate/src/main/java/wallettemplate/MainController.java wallettemplate/src/main/java/wallettemplate/SendMoneyController.java
This commit is contained in:
commit
905629a78f
@ -1,13 +1,19 @@
|
||||
# configuration for https://travis-ci.org/bitcoinj/bitcoinj
|
||||
language: java
|
||||
jdk: openjdk6
|
||||
jdk: oraclejdk8
|
||||
before_install: lsb_release -a
|
||||
install: true # remove default
|
||||
script:
|
||||
- mvn -q clean install
|
||||
- cd wallettemplate
|
||||
- jdk_switcher use oraclejdk8
|
||||
- mvn -q clean install
|
||||
- jdk_switcher use openjdk6
|
||||
- cd orchid
|
||||
- mvn -q clean package
|
||||
- cd ../core
|
||||
- mvn -q clean package
|
||||
|
||||
after_success:
|
||||
- cd ../core
|
||||
- mvn jacoco:report coveralls:report
|
||||
|
||||
notifications:
|
||||
irc:
|
||||
|
3
AUTHORS
3
AUTHORS
@ -38,3 +38,6 @@ Oscar Guindzberg <oscar.guindzberg@gmail.com>
|
||||
Richard Green <richardagreen@gmail.com>
|
||||
Martin Zachrison <zac@cyberzac.se>
|
||||
Michael Bumann <michael@railslove.com>
|
||||
Bennett Hoffman <bennett@buttercoin.com>
|
||||
Olle Kullberg <olle.kullberg@strawpay.com>
|
||||
Carlos Lopez <c.lopez@kmels.net>
|
@ -8,7 +8,7 @@ This is a port of bitcoinj. All credits for the hard work goes to the bitcoinj t
|
||||
|
||||
### Technologies
|
||||
|
||||
* Java 6+
|
||||
* Java 6 for the core modules, Java 8 for everything else
|
||||
* [Maven 3+](http://maven.apache.org) - for building the project
|
||||
* [Orchid](https://github.com/subgraph/Orchid) - for secure communications over [TOR](https://www.torproject.org)
|
||||
* [Google Protocol Buffers](https://code.google.com/p/protobuf/) - for use with serialization and hardware communications
|
||||
|
87
core/pom.xml
87
core/pom.xml
@ -95,6 +95,7 @@
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<!-- Ensure compilation is done under Java 6 for backwards compatibility -->
|
||||
<plugin>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
@ -121,7 +122,6 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<version>2.9.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-javadocs</id>
|
||||
@ -164,7 +164,7 @@
|
||||
<!-- Format is URN of groupId:artifactId:version:type:classifier:scope:hash -->
|
||||
<!-- classifier is "null" if not present -->
|
||||
<urns>
|
||||
<urn>com.dogecoin:orchid:1.0:jar:null:compile:a201b0e6a0e18c6b3435753103f8d3e707c718ad</urn>
|
||||
<urn>com.dogecoin:orchid:1.1:jar:null:compile:393d53cad40f32f1c00a717326deeb4bde8ee57b</urn>
|
||||
<urn>cglib:cglib-nodep:2.2:jar:null:test:59afed7ab65e7ec6585d5bc60556c3cbd203532b</urn>
|
||||
<urn>com.google.code.findbugs:jsr305:2.0.1:jar:null:compile:516c03b21d50a644d538de0f0369c620989cd8f0</urn>
|
||||
<urn>com.google.guava:guava:16.0.1:jar:null:compile:5fa98cd1a63c99a44dd8d3b77e4762b066a5d0c5</urn>
|
||||
@ -181,6 +181,7 @@
|
||||
<urn>org.apache.maven.plugins:maven-enforcer-plugin:1.2:maven-plugin:null:runtime:6b755a9a0d618f8f57c0b5c4a0737a012e710a46</urn>
|
||||
<urn>org.apache.maven.plugins:maven-install-plugin:2.5.1:maven-plugin:null:runtime:b6f5a4b621b9c26699c8deadb20fdc35ce568e35</urn>
|
||||
<urn>org.apache.maven.plugins:maven-jar-plugin:2.5:maven-plugin:null:runtime:344d667f5ec8b90d03d698d096a1147672fc522f</urn>
|
||||
<urn>org.apache.maven.plugins:maven-javadoc-plugin:2.9.1:maven-plugin:null:runtime:95ea7abf00e37e08bd927bf7e448c1e7fe4c6cb9</urn>
|
||||
<urn>org.apache.maven.plugins:maven-resources-plugin:2.6:maven-plugin:null:runtime:dd093ff6a4b680eae7ae83b5ab04310249fc6590</urn>
|
||||
<urn>org.apache.maven.plugins:maven-shade-plugin:2.3:maven-plugin:null:runtime:d136adc7abccc9c12adcad6ae7a9bc51b2b7184b</urn>
|
||||
<urn>org.apache.maven.plugins:maven-site-plugin:3.3:maven-plugin:null:runtime:77ba1752b1ac4c4339d6f11554800960a56a4ae1</urn>
|
||||
@ -195,6 +196,8 @@
|
||||
<url>com.fasterxml.jackson.core:jackson-databind:2.4.2:jar:null:test:8e31266a272ad25ac4c089734d93e8d811652c1f</url>
|
||||
<url>com.fasterxml.jackson.core:jackson-core:2.4.2:jar:null:test:ceb72830d95c512b4b300a38f29febc85bdf6e4b</url>
|
||||
<url>com.fasterxml.jackson.core:jackson-annotations:2.4.2:jar:null:test:6bb52af09372d5064206d47d7887d41671f00f7d</url>
|
||||
<urn>org.jacoco:jacoco-maven-plugin:0.7.2.201409121644:maven-plugin:null:runtime:b2cb310459d082db505fdfa66dbadd4d8bac8e34</urn>
|
||||
<urn>org.eluder.coveralls:coveralls-maven-plugin:3.0.1:maven-plugin:null:runtime:3907ee5cf1e5c85af7bb90e486ce4c7b1408a552</urn>
|
||||
<!-- A check for the rules themselves -->
|
||||
<urn>uk.co.froot.maven.enforcer:digest-enforcer-rules:0.0.1:jar:null:runtime:16a9e04f3fe4bb143c42782d07d5faf65b32106f</urn>
|
||||
</urns>
|
||||
@ -245,6 +248,69 @@
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<!-- Code coverage plugin, generates coverage report to target/site/jacoco/
|
||||
To skip coverage generation add -Djacoco.skip=true
|
||||
-->
|
||||
<plugin>
|
||||
<groupId>org.jacoco</groupId>
|
||||
<artifactId>jacoco-maven-plugin</artifactId>
|
||||
<version>0.7.2.201409121644</version>
|
||||
<configuration>
|
||||
<excludes>
|
||||
<exclude>**/Protos*.class</exclude> <!-- Exclude generated protobuf classes -->
|
||||
<exclude>org/bitcoinj/jni/*</exclude> <!-- Exclude JNI classes -->
|
||||
<exclude>org/bitcoin/*</exclude> <!-- Exclude native classes -->
|
||||
</excludes>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>pre-unit-test</id>
|
||||
<goals>
|
||||
<goal>prepare-agent</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<destFile>${project.build.directory}/coverage-reports/jacoco.exec</destFile>
|
||||
<propertyName>surefireArgLine</propertyName>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>post-unit-test</id>
|
||||
<phase>test</phase>
|
||||
<goals>
|
||||
<goal>report</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<dataFile>${project.build.directory}/coverage-reports/jacoco.exec</dataFile>
|
||||
<outputDirectory>${project.reporting.outputDirectory}/jacoco</outputDirectory>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>default-report</id>
|
||||
<phase>prepare-package</phase>
|
||||
<goals>
|
||||
<goal>report</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<!-- Unit tests plugin, to skip runing test add -Dmaven.test.skip -->
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>2.12.4</version>
|
||||
<configuration>
|
||||
<argLine>${surefireArgLine}</argLine> <!-- This is required for code coverage to work. -->
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<!-- Coveralls is a online code coverage tool, you can track code coverage here: https://coveralls.io/r/bitcoinj/bitcoinj -->
|
||||
<plugin>
|
||||
<groupId>org.eluder.coveralls</groupId>
|
||||
<artifactId>coveralls-maven-plugin</artifactId>
|
||||
<version>3.0.1</version>
|
||||
</plugin>
|
||||
|
||||
<!-- Create a bundled executable test jar that runs the regtester/pulltester.
|
||||
The comparison tool is kind of messy and badly needs a seriously refactoring.
|
||||
It depends on classes which are only in the test tree so we must do some
|
||||
@ -383,19 +449,26 @@
|
||||
<artifactId>scrypt</artifactId>
|
||||
<version>1.4.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Add in to test/use Postgres blockstore -->
|
||||
<!--
|
||||
<!-- Note this is an optional dependency: Postgres blockstore -->
|
||||
<!-- To Test remove optional -->
|
||||
<dependency>
|
||||
<groupId>postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<version>9.1-901.jdbc4</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<!-- Note this is an optional dependency: MySQL blockstore -->
|
||||
<!-- To Test remove optional -->
|
||||
<dependency>
|
||||
<groupId>mysql</groupId>
|
||||
<artifactId>mysql-connector-java</artifactId>
|
||||
<version>5.1.33</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
-->
|
||||
<dependency>
|
||||
<groupId>com.dogecoin</groupId>
|
||||
<artifactId>orchid</artifactId>
|
||||
<version>1.0</version>
|
||||
<version>1.1</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
|
@ -86,6 +86,7 @@ public abstract class AbstractBlockChain {
|
||||
|
||||
/** Keeps a map of block hashes to StoredBlocks. */
|
||||
private final BlockStore blockStore;
|
||||
private final Context context;
|
||||
|
||||
/**
|
||||
* Tracks the top of the best known chain.<p>
|
||||
@ -149,6 +150,11 @@ public abstract class AbstractBlockChain {
|
||||
this.params = params;
|
||||
this.listeners = new CopyOnWriteArrayList<ListenerRegistration<BlockChainListener>>();
|
||||
for (BlockChainListener l : listeners) addListener(l, Threading.SAME_THREAD);
|
||||
context = new Context();
|
||||
}
|
||||
|
||||
public Context getContext() {
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -51,7 +51,7 @@ public abstract class AbstractWalletEventListener extends AbstractKeyChainEventL
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScriptsAdded(Wallet wallet, List<Script> scripts) {
|
||||
public void onScriptsChanged(Wallet wallet, List<Script> scripts, boolean isAddingScripts) {
|
||||
onChange();
|
||||
}
|
||||
|
||||
|
@ -47,7 +47,7 @@ public class Address extends VersionedChecksummedBytes {
|
||||
/**
|
||||
* Construct an address from parameters, the address version, and the hash160 form. Example:<p>
|
||||
*
|
||||
* <pre>new Address(NetworkParameters.prodNet(), NetworkParameters.getAddressHeader(), Hex.decode("4a22c3c4cbb31e4d03b15550636762bda0baf85a"));</pre>
|
||||
* <pre>new Address(MainNetParams.get(), NetworkParameters.getAddressHeader(), Hex.decode("4a22c3c4cbb31e4d03b15550636762bda0baf85a"));</pre>
|
||||
*/
|
||||
public Address(NetworkParameters params, int version, byte[] hash160) throws WrongNetworkException {
|
||||
super(version, hash160);
|
||||
@ -76,7 +76,7 @@ public class Address extends VersionedChecksummedBytes {
|
||||
/**
|
||||
* Construct an address from parameters and the hash160 form. Example:<p>
|
||||
*
|
||||
* <pre>new Address(NetworkParameters.prodNet(), Hex.decode("4a22c3c4cbb31e4d03b15550636762bda0baf85a"));</pre>
|
||||
* <pre>new Address(MainNetParams.get(), Hex.decode("4a22c3c4cbb31e4d03b15550636762bda0baf85a"));</pre>
|
||||
*/
|
||||
public Address(NetworkParameters params, byte[] hash160) {
|
||||
super(params.getAddressHeader(), hash160);
|
||||
@ -87,12 +87,12 @@ public class Address extends VersionedChecksummedBytes {
|
||||
/**
|
||||
* Construct an address from parameters and the standard "human readable" form. Example:<p>
|
||||
*
|
||||
* <pre>new Address(NetworkParameters.prodNet(), "17kzeh4N8g49GFvdDzSf8PjaPfyoD1MndL");</pre><p>
|
||||
* <pre>new Address(MainNetParams.get(), "17kzeh4N8g49GFvdDzSf8PjaPfyoD1MndL");</pre><p>
|
||||
*
|
||||
* @param params The expected NetworkParameters or null if you don't want validation.
|
||||
* @param address The textual form of the address, such as "17kzeh4N8g49GFvdDzSf8PjaPfyoD1MndL"
|
||||
* @throws AddressFormatException if the given address doesn't parse or the checksum is invalid
|
||||
* @throws WrongNetworkException if the given address is valid but for a different chain (eg testnet vs prodnet)
|
||||
* @throws WrongNetworkException if the given address is valid but for a different chain (eg testnet vs mainnet)
|
||||
*/
|
||||
public Address(@Nullable NetworkParameters params, String address) throws AddressFormatException {
|
||||
super(address);
|
||||
|
@ -1,5 +1,6 @@
|
||||
/**
|
||||
* Copyright 2011 Google Inc.
|
||||
* Copyright 2014 Andreas Schildbach
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -559,7 +560,7 @@ public class Block extends Message {
|
||||
|
||||
/**
|
||||
* Returns the hash of the block (which for a valid, solved block should be below the target) in the form seen on
|
||||
* the block explorer. If you call this on block 1 in the production chain
|
||||
* the block explorer. If you call this on block 1 in the mainnet chain
|
||||
* you will get "00000000839a8e6886ab5951d76f411475428afc90947ee320161bbf18eb6048".
|
||||
*/
|
||||
public String getHashAsString() {
|
||||
@ -642,7 +643,7 @@ public class Block extends Message {
|
||||
s.append(" time: [");
|
||||
s.append(time);
|
||||
s.append("] ");
|
||||
s.append(new Date(time * 1000));
|
||||
s.append(Utils.dateTimeFormat(time * 1000));
|
||||
s.append("\n");
|
||||
s.append(" difficulty target (nBits): ");
|
||||
s.append(difficultyTarget);
|
||||
@ -1020,6 +1021,7 @@ public class Block extends Message {
|
||||
static private int txCounter;
|
||||
|
||||
/** Adds a coinbase transaction to the block. This exists for unit tests. */
|
||||
@VisibleForTesting
|
||||
void addCoinbaseTransaction(byte[] pubKeyTo, Coin value) {
|
||||
unCacheTransactions();
|
||||
transactions = new ArrayList<Transaction>();
|
||||
@ -1029,7 +1031,8 @@ public class Block extends Message {
|
||||
//
|
||||
// Here we will do things a bit differently so a new address isn't needed every time. We'll put a simple
|
||||
// counter in the scriptSig so every transaction has a different hash.
|
||||
coinbase.addInput(new TransactionInput(params, coinbase, new byte[]{(byte) txCounter, (byte) (txCounter++ >> 8)}));
|
||||
coinbase.addInput(new TransactionInput(params, coinbase,
|
||||
new ScriptBuilder().data(new byte[]{(byte) txCounter, (byte) (txCounter++ >> 8)}).build().getProgram()));
|
||||
coinbase.addOutput(new TransactionOutput(params, coinbase, value,
|
||||
ScriptBuilder.createOutputScript(ECKey.fromPublicOnly(pubKeyTo)).getProgram()));
|
||||
transactions.add(coinbase);
|
||||
@ -1099,12 +1102,12 @@ public class Block extends Message {
|
||||
|
||||
@VisibleForTesting
|
||||
public Block createNextBlock(@Nullable Address to, TransactionOutPoint prevOut) {
|
||||
return createNextBlock(to, prevOut, Utils.currentTimeSeconds(), pubkeyForTesting, FIFTY_COINS);
|
||||
return createNextBlock(to, prevOut, getTimeSeconds() + 5, pubkeyForTesting, FIFTY_COINS);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public Block createNextBlock(@Nullable Address to, Coin value) {
|
||||
return createNextBlock(to, null, Utils.currentTimeSeconds(), pubkeyForTesting, value);
|
||||
return createNextBlock(to, null, getTimeSeconds() + 5, pubkeyForTesting, value);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
|
@ -93,7 +93,7 @@ public class BloomFilter extends Message {
|
||||
*
|
||||
* <p>In order for filtered block download to function efficiently, the number of matched transactions in any given
|
||||
* block should be less than (with some headroom) the maximum size of the MemoryPool used by the Peer
|
||||
* doing the downloading (default is {@link MemoryPool#MAX_SIZE}). See the comment in processBlock(FilteredBlock)
|
||||
* doing the downloading (default is {@link TxConfidenceTable#MAX_SIZE}). See the comment in processBlock(FilteredBlock)
|
||||
* for more information on this restriction.</p>
|
||||
*
|
||||
* <p>randomNonce is a tweak for the hash function used to prevent some theoretical DoS attacks.
|
||||
@ -339,7 +339,17 @@ public class BloomFilter extends Message {
|
||||
}
|
||||
}
|
||||
}
|
||||
return found;
|
||||
if (found) return true;
|
||||
for (TransactionInput input : tx.getInputs()) {
|
||||
if (contains(input.getOutpoint().bitcoinSerialize())) {
|
||||
return true;
|
||||
}
|
||||
for (ScriptChunk chunk : input.getScriptSig().getChunks()) {
|
||||
if (chunk.isPushData() && contains(chunk.data))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -128,7 +128,12 @@ public final class Coin implements Monetary, Comparable<Coin>, Serializable {
|
||||
* @throws IllegalArgumentException if you try to specify fractional satoshis, or a value out of range.
|
||||
*/
|
||||
public static Coin parseCoin(final String str) {
|
||||
return Coin.valueOf(new BigDecimal(str).movePointRight(SMALLEST_UNIT_EXPONENT).toBigIntegerExact().longValue());
|
||||
try {
|
||||
long satoshis = new BigDecimal(str).movePointRight(SMALLEST_UNIT_EXPONENT).toBigIntegerExact().longValue();
|
||||
return Coin.valueOf(satoshis);
|
||||
} catch (ArithmeticException e) {
|
||||
throw new IllegalArgumentException(e); // Repackage exception to honor method contract
|
||||
}
|
||||
}
|
||||
|
||||
public Coin add(final Coin value) {
|
||||
|
25
core/src/main/java/com/dogecoin/dogecoinj/core/Context.java
Normal file
25
core/src/main/java/com/dogecoin/dogecoinj/core/Context.java
Normal file
@ -0,0 +1,25 @@
|
||||
package com.dogecoin.dogecoinj.core;
|
||||
|
||||
/**
|
||||
* The Context object holds various objects that are scoped to a specific instantiation of bitcoinj for a specific
|
||||
* network. You can get an instance of this class through {@link AbstractBlockChain#getContext()}. At the momemnt it
|
||||
* only contains a {@link com.dogecoin.dogecoinj.core.TxConfidenceTable} but in future it will likely contain file paths and
|
||||
* other global configuration of use.
|
||||
*/
|
||||
public class Context {
|
||||
protected TxConfidenceTable confidenceTable;
|
||||
|
||||
protected Context() {
|
||||
confidenceTable = new TxConfidenceTable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link TxConfidenceTable} created by this context. The pool tracks advertised
|
||||
* and downloaded transactions so their confidence can be measured as a proportion of how many peers announced it.
|
||||
* With an un-tampered with internet connection, the more peers announce a transaction the more confidence you can
|
||||
* have that it's really valid.
|
||||
*/
|
||||
public TxConfidenceTable getConfidenceTable() {
|
||||
return confidenceTable;
|
||||
}
|
||||
}
|
@ -0,0 +1,122 @@
|
||||
/**
|
||||
* Copyright 2011 Google Inc.
|
||||
* Copyright 2014 Andreas Schildbach
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.dogecoin.dogecoinj.core;
|
||||
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.common.util.concurrent.SettableFuture;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
/**
|
||||
* <p>An implementation of {@link AbstractPeerEventListener} that listens to chain download events and tracks progress
|
||||
* as a percentage. The default implementation prints progress to stdout, but you can subclass it and override the
|
||||
* progress method to update a GUI instead.</p>
|
||||
*/
|
||||
public class DownloadProgressTracker extends AbstractPeerEventListener {
|
||||
private static final Logger log = LoggerFactory.getLogger(DownloadProgressTracker.class);
|
||||
private int originalBlocksLeft = -1;
|
||||
private int lastPercent = 0;
|
||||
private SettableFuture<Long> future = SettableFuture.create();
|
||||
private boolean caughtUp = false;
|
||||
|
||||
@Override
|
||||
public void onChainDownloadStarted(Peer peer, int blocksLeft) {
|
||||
if (blocksLeft > 0 && originalBlocksLeft == -1)
|
||||
startDownload(blocksLeft);
|
||||
// Only mark this the first time, because this method can be called more than once during a chain download
|
||||
// if we switch peers during it.
|
||||
if (originalBlocksLeft == -1)
|
||||
originalBlocksLeft = blocksLeft;
|
||||
else
|
||||
log.info("Chain download switched to {}", peer);
|
||||
if (blocksLeft == 0) {
|
||||
doneDownload();
|
||||
future.set(peer.getBestHeight());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBlocksDownloaded(Peer peer, Block block, int blocksLeft) {
|
||||
if (caughtUp)
|
||||
return;
|
||||
|
||||
if (blocksLeft == 0) {
|
||||
caughtUp = true;
|
||||
doneDownload();
|
||||
future.set(peer.getBestHeight());
|
||||
}
|
||||
|
||||
if (blocksLeft < 0 || originalBlocksLeft <= 0)
|
||||
return;
|
||||
|
||||
double pct = 100.0 - (100.0 * (blocksLeft / (double) originalBlocksLeft));
|
||||
if ((int) pct != lastPercent) {
|
||||
progress(pct, blocksLeft, new Date(block.getTimeSeconds() * 1000));
|
||||
lastPercent = (int) pct;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when download progress is made.
|
||||
*
|
||||
* @param pct the percentage of chain downloaded, estimated
|
||||
* @param date the date of the last block downloaded
|
||||
*/
|
||||
protected void progress(double pct, int blocksSoFar, Date date) {
|
||||
log.info(String.format("Chain download %d%% done with %d blocks to go, block date %s", (int) pct, blocksSoFar,
|
||||
Utils.dateTimeFormat(date)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when download is initiated.
|
||||
*
|
||||
* @param blocks the number of blocks to download, estimated
|
||||
*/
|
||||
protected void startDownload(int blocks) {
|
||||
log.info("Downloading block chain of size " + blocks + ". " +
|
||||
(blocks > 1000 ? "This may take a while." : ""));
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when we are done downloading the block chain.
|
||||
*/
|
||||
protected void doneDownload() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for the chain to be downloaded.
|
||||
*/
|
||||
public void await() throws InterruptedException {
|
||||
try {
|
||||
future.get();
|
||||
} catch (ExecutionException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a listenable future that completes with the height of the best chain (as reported by the peer) once chain
|
||||
* download seems to be finished.
|
||||
*/
|
||||
public ListenableFuture<Long> getFuture() {
|
||||
return future;
|
||||
}
|
||||
}
|
@ -128,6 +128,10 @@ public class ECKey implements EncryptableItem, Serializable {
|
||||
private static final long serialVersionUID = -728224901792295832L;
|
||||
|
||||
static {
|
||||
// Init proper random number generator, as some old Android installations have bugs that make it unsecure.
|
||||
if (Utils.isAndroidRuntime())
|
||||
new LinuxSecureRandom();
|
||||
|
||||
// Tell Bouncy Castle to precompute data that's needed during secp256k1 calculations. Increasing the width
|
||||
// number makes calculations faster, but at a cost of extra memory usage and with decreasing returns. 12 was
|
||||
// picked after consulting with the BC team.
|
||||
@ -141,7 +145,7 @@ public class ECKey implements EncryptableItem, Serializable {
|
||||
// The two parts of the key. If "priv" is set, "pub" can always be calculated. If "pub" is set but not "priv", we
|
||||
// can only verify signatures not make them.
|
||||
protected final BigInteger priv; // A field element.
|
||||
protected final ECPoint pub;
|
||||
protected final LazyECPoint pub;
|
||||
|
||||
// Creation time of the key in seconds since the epoch, or zero if the key was deserialized from a version that did
|
||||
// not have this field.
|
||||
@ -173,11 +177,16 @@ public class ECKey implements EncryptableItem, Serializable {
|
||||
ECPrivateKeyParameters privParams = (ECPrivateKeyParameters) keypair.getPrivate();
|
||||
ECPublicKeyParameters pubParams = (ECPublicKeyParameters) keypair.getPublic();
|
||||
priv = privParams.getD();
|
||||
pub = CURVE.getCurve().decodePoint(pubParams.getQ().getEncoded(true));
|
||||
pub = new LazyECPoint(CURVE.getCurve(), pubParams.getQ().getEncoded(true));
|
||||
creationTimeSeconds = Utils.currentTimeSeconds();
|
||||
}
|
||||
|
||||
protected ECKey(@Nullable BigInteger priv, ECPoint pub) {
|
||||
this.priv = priv;
|
||||
this.pub = new LazyECPoint(checkNotNull(pub));
|
||||
}
|
||||
|
||||
protected ECKey(@Nullable BigInteger priv, LazyECPoint pub) {
|
||||
this.priv = priv;
|
||||
this.pub = checkNotNull(pub);
|
||||
}
|
||||
@ -186,16 +195,24 @@ public class ECKey implements EncryptableItem, Serializable {
|
||||
* Utility for compressing an elliptic curve point. Returns the same point if it's already compressed.
|
||||
* See the ECKey class docs for a discussion of point compression.
|
||||
*/
|
||||
public static ECPoint compressPoint(ECPoint uncompressed) {
|
||||
return CURVE.getCurve().decodePoint(uncompressed.getEncoded(true));
|
||||
public static ECPoint compressPoint(ECPoint point) {
|
||||
return point.isCompressed() ? point : CURVE.getCurve().decodePoint(point.getEncoded(true));
|
||||
}
|
||||
|
||||
public static LazyECPoint compressPoint(LazyECPoint point) {
|
||||
return point.isCompressed() ? point : new LazyECPoint(compressPoint(point.get()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility for decompressing an elliptic curve point. Returns the same point if it's already compressed.
|
||||
* See the ECKey class docs for a discussion of point compression.
|
||||
*/
|
||||
public static ECPoint decompressPoint(ECPoint compressed) {
|
||||
return CURVE.getCurve().decodePoint(compressed.getEncoded(false));
|
||||
public static ECPoint decompressPoint(ECPoint point) {
|
||||
return !point.isCompressed() ? point : CURVE.getCurve().decodePoint(point.getEncoded(false));
|
||||
}
|
||||
|
||||
public static LazyECPoint decompressPoint(LazyECPoint point) {
|
||||
return !point.isCompressed() ? point : new LazyECPoint(decompressPoint(point.get()));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -283,7 +300,7 @@ public class ECKey implements EncryptableItem, Serializable {
|
||||
if (!pub.isCompressed())
|
||||
return this;
|
||||
else
|
||||
return new ECKey(priv, decompressPoint(pub));
|
||||
return new ECKey(priv, decompressPoint(pub.get()));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -340,12 +357,12 @@ public class ECKey implements EncryptableItem, Serializable {
|
||||
ECPoint point = CURVE.getG().multiply(privKey);
|
||||
if (compressed)
|
||||
point = compressPoint(point);
|
||||
this.pub = point;
|
||||
this.pub = new LazyECPoint(point);
|
||||
} else {
|
||||
// We expect the pubkey to be in regular encoded form, just as a BigInteger. Therefore the first byte is
|
||||
// a special marker byte.
|
||||
// TODO: This is probably not a useful API and may be confusing.
|
||||
this.pub = CURVE.getCurve().decodePoint(pubKey);
|
||||
this.pub = new LazyECPoint(CURVE.getCurve().decodePoint(pubKey));
|
||||
}
|
||||
}
|
||||
|
||||
@ -431,7 +448,7 @@ public class ECKey implements EncryptableItem, Serializable {
|
||||
|
||||
/** Gets the public key in the form of an elliptic curve point object from Bouncy Castle. */
|
||||
public ECPoint getPubKeyPoint() {
|
||||
return pub;
|
||||
return pub.get();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -664,11 +681,11 @@ public class ECKey implements EncryptableItem, Serializable {
|
||||
/**
|
||||
* Verifies the given ASN.1 encoded ECDSA signature against a hash using the public key.
|
||||
*
|
||||
* @param data Hash of the data to verify.
|
||||
* @param hash Hash of the data to verify.
|
||||
* @param signature ASN.1 encoded signature.
|
||||
*/
|
||||
public boolean verify(byte[] data, byte[] signature) {
|
||||
return ECKey.verify(data, signature, getPubKey());
|
||||
public boolean verify(byte[] hash, byte[] signature) {
|
||||
return ECKey.verify(hash, signature, getPubKey());
|
||||
}
|
||||
|
||||
/**
|
||||
@ -678,6 +695,26 @@ public class ECKey implements EncryptableItem, Serializable {
|
||||
return ECKey.verify(sigHash.getBytes(), signature, getPubKey());
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the given ASN.1 encoded ECDSA signature against a hash using the public key, and throws an exception
|
||||
* if the signature doesn't match
|
||||
* @throws java.security.SignatureException if the signature does not match.
|
||||
*/
|
||||
public void verifyOrThrow(byte[] hash, byte[] signature) throws SignatureException {
|
||||
if (!verify(hash, signature))
|
||||
throw new SignatureException();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the given R/S pair (signature) against a hash using the public key, and throws an exception
|
||||
* if the signature doesn't match
|
||||
* @throws java.security.SignatureException if the signature does not match.
|
||||
*/
|
||||
public void verifyOrThrow(Sha256Hash sigHash, ECDSASignature signature) throws SignatureException {
|
||||
if (!ECKey.verify(sigHash.getBytes(), signature, getPubKey()))
|
||||
throw new SignatureException();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given pubkey is canonical, i.e. the correct length taking into account compression.
|
||||
*/
|
||||
@ -1157,6 +1194,7 @@ public class ECKey implements EncryptableItem, Serializable {
|
||||
if (includePrivate)
|
||||
helper.add("encryptedPrivateKey", encryptedPrivateKey);
|
||||
helper.add("isEncrypted", isEncrypted());
|
||||
helper.add("isPubKeyOnly", isPubKeyOnly());
|
||||
return helper.toString();
|
||||
}
|
||||
|
||||
|
@ -150,6 +150,44 @@ public class FullPrunedBlockChain extends AbstractBlockChain {
|
||||
}
|
||||
}
|
||||
|
||||
/** Get the {@link Script} from the script bytes or null if it doesn't parse. */
|
||||
@Nullable
|
||||
private Script getScript(byte[] scriptBytes) {
|
||||
try {
|
||||
return new Script(scriptBytes);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the address from the {@link Script} if it exists otherwise return empty string "".
|
||||
* @param script The script.
|
||||
* @return The address.
|
||||
*/
|
||||
private String getScriptAddress(@Nullable Script script) {
|
||||
String address = "";
|
||||
try {
|
||||
if (script != null) {
|
||||
address = script.getToAddress(params, true).toString();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
}
|
||||
return address;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link Script.ScriptType} of this script.
|
||||
* @param script The script.
|
||||
* @return The script type.
|
||||
*/
|
||||
private Script.ScriptType getScriptType(@Nullable Script script) {
|
||||
if (script != null) {
|
||||
return script.getScriptType();
|
||||
}
|
||||
return Script.ScriptType.NO_TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TransactionOutputChanges connectTransactions(int height, Block block)
|
||||
throws VerificationException, BlockStoreException {
|
||||
@ -161,8 +199,8 @@ public class FullPrunedBlockChain extends AbstractBlockChain {
|
||||
|
||||
blockStore.beginDatabaseBatchWrite();
|
||||
|
||||
LinkedList<StoredTransactionOutput> txOutsSpent = new LinkedList<StoredTransactionOutput>();
|
||||
LinkedList<StoredTransactionOutput> txOutsCreated = new LinkedList<StoredTransactionOutput>();
|
||||
LinkedList<UTXO> txOutsSpent = new LinkedList<UTXO>();
|
||||
LinkedList<UTXO> txOutsCreated = new LinkedList<UTXO>();
|
||||
long sigOps = 0;
|
||||
final Set<VerifyFlag> verifyFlags = EnumSet.noneOf(VerifyFlag.class);
|
||||
if (block.getTimeSeconds() >= NetworkParameters.BIP16_ENFORCE_TIME)
|
||||
@ -199,15 +237,18 @@ public class FullPrunedBlockChain extends AbstractBlockChain {
|
||||
// outputs.
|
||||
for (int index = 0; index < tx.getInputs().size(); index++) {
|
||||
TransactionInput in = tx.getInputs().get(index);
|
||||
StoredTransactionOutput prevOut = blockStore.getTransactionOutput(in.getOutpoint().getHash(),
|
||||
UTXO prevOut = blockStore.getTransactionOutput(in.getOutpoint().getHash(),
|
||||
in.getOutpoint().getIndex());
|
||||
if (prevOut == null)
|
||||
throw new VerificationException("Attempted to spend a non-existent or already spent output!");
|
||||
// Coinbases can't be spent until they mature, to avoid re-orgs destroying entire transaction
|
||||
// chains. The assumption is there will ~never be re-orgs deeper than the spendable coinbase
|
||||
// chain depth.
|
||||
if (height - prevOut.getHeight() < params.getSpendableCoinbaseDepth())
|
||||
throw new VerificationException("Tried to spend coinbase at depth " + (height - prevOut.getHeight()));
|
||||
if (prevOut.isCoinbase()) {
|
||||
if (height - prevOut.getHeight() < params.getSpendableCoinbaseDepth()) {
|
||||
throw new VerificationException("Tried to spend coinbase at depth " + (height - prevOut.getHeight()));
|
||||
}
|
||||
}
|
||||
// TODO: Check we're not spending the genesis transaction here. Satoshis code won't allow it.
|
||||
valueIn = valueIn.add(prevOut.getValue());
|
||||
if (verifyFlags.contains(VerifyFlag.P2SH)) {
|
||||
@ -229,8 +270,14 @@ public class FullPrunedBlockChain extends AbstractBlockChain {
|
||||
for (TransactionOutput out : tx.getOutputs()) {
|
||||
valueOut = valueOut.add(out.getValue());
|
||||
// For each output, add it to the set of unspent outputs so it can be consumed in future.
|
||||
StoredTransactionOutput newOut = new StoredTransactionOutput(hash, out.getIndex(), out.getValue(),
|
||||
height, isCoinBase, out.getScriptBytes());
|
||||
Script script = getScript(out.getScriptBytes());
|
||||
UTXO newOut = new UTXO(hash,
|
||||
out.getIndex(),
|
||||
out.getValue(),
|
||||
height, isCoinBase,
|
||||
out.getScriptBytes(),
|
||||
getScriptAddress(script),
|
||||
getScriptType(script).ordinal());
|
||||
blockStore.addUnspentTransactionOutput(newOut);
|
||||
txOutsCreated.add(newOut);
|
||||
}
|
||||
@ -301,8 +348,8 @@ public class FullPrunedBlockChain extends AbstractBlockChain {
|
||||
try {
|
||||
List<Transaction> transactions = block.getTransactions();
|
||||
if (transactions != null) {
|
||||
LinkedList<StoredTransactionOutput> txOutsSpent = new LinkedList<StoredTransactionOutput>();
|
||||
LinkedList<StoredTransactionOutput> txOutsCreated = new LinkedList<StoredTransactionOutput>();
|
||||
LinkedList<UTXO> txOutsSpent = new LinkedList<UTXO>();
|
||||
LinkedList<UTXO> txOutsCreated = new LinkedList<UTXO>();
|
||||
long sigOps = 0;
|
||||
final Set<VerifyFlag> verifyFlags = EnumSet.noneOf(VerifyFlag.class);
|
||||
if (newBlock.getHeader().getTimeSeconds() >= NetworkParameters.BIP16_ENFORCE_TIME)
|
||||
@ -328,11 +375,11 @@ public class FullPrunedBlockChain extends AbstractBlockChain {
|
||||
if (!isCoinBase) {
|
||||
for (int index = 0; index < tx.getInputs().size(); index++) {
|
||||
final TransactionInput in = tx.getInputs().get(index);
|
||||
final StoredTransactionOutput prevOut = blockStore.getTransactionOutput(in.getOutpoint().getHash(),
|
||||
final UTXO prevOut = blockStore.getTransactionOutput(in.getOutpoint().getHash(),
|
||||
in.getOutpoint().getIndex());
|
||||
if (prevOut == null)
|
||||
throw new VerificationException("Attempted spend of a non-existent or already spent output!");
|
||||
if (newBlock.getHeight() - prevOut.getHeight() < params.getSpendableCoinbaseDepth())
|
||||
if (prevOut.isCoinbase() && newBlock.getHeight() - prevOut.getHeight() < params.getSpendableCoinbaseDepth())
|
||||
throw new VerificationException("Tried to spend coinbase at depth " + (newBlock.getHeight() - prevOut.getHeight()));
|
||||
valueIn = valueIn.add(prevOut.getValue());
|
||||
if (verifyFlags.contains(VerifyFlag.P2SH)) {
|
||||
@ -352,9 +399,15 @@ public class FullPrunedBlockChain extends AbstractBlockChain {
|
||||
Sha256Hash hash = tx.getHash();
|
||||
for (TransactionOutput out : tx.getOutputs()) {
|
||||
valueOut = valueOut.add(out.getValue());
|
||||
StoredTransactionOutput newOut = new StoredTransactionOutput(hash, out.getIndex(), out.getValue(),
|
||||
newBlock.getHeight(), isCoinBase,
|
||||
out.getScriptBytes());
|
||||
Script script = getScript(out.getScriptBytes());
|
||||
UTXO newOut = new UTXO(hash,
|
||||
out.getIndex(),
|
||||
out.getValue(),
|
||||
newBlock.getHeight(),
|
||||
isCoinBase,
|
||||
out.getScriptBytes(),
|
||||
getScriptAddress(script),
|
||||
getScriptType(script).ordinal());
|
||||
blockStore.addUnspentTransactionOutput(newOut);
|
||||
txOutsCreated.add(newOut);
|
||||
}
|
||||
@ -397,14 +450,14 @@ public class FullPrunedBlockChain extends AbstractBlockChain {
|
||||
} else {
|
||||
txOutChanges = block.getTxOutChanges();
|
||||
if (!params.isCheckpoint(newBlock.getHeight()))
|
||||
for(StoredTransactionOutput out : txOutChanges.txOutsCreated) {
|
||||
for(UTXO out : txOutChanges.txOutsCreated) {
|
||||
Sha256Hash hash = out.getHash();
|
||||
if (blockStore.getTransactionOutput(hash, out.getIndex()) != null)
|
||||
throw new VerificationException("Block failed BIP30 test!");
|
||||
}
|
||||
for (StoredTransactionOutput out : txOutChanges.txOutsCreated)
|
||||
for (UTXO out : txOutChanges.txOutsCreated)
|
||||
blockStore.addUnspentTransactionOutput(out);
|
||||
for (StoredTransactionOutput out : txOutChanges.txOutsSpent)
|
||||
for (UTXO out : txOutChanges.txOutsSpent)
|
||||
blockStore.removeUnspentTransactionOutput(out);
|
||||
}
|
||||
} catch (VerificationException e) {
|
||||
@ -431,9 +484,9 @@ public class FullPrunedBlockChain extends AbstractBlockChain {
|
||||
StoredUndoableBlock undoBlock = blockStore.getUndoBlock(oldBlock.getHeader().getHash());
|
||||
if (undoBlock == null) throw new PrunedException(oldBlock.getHeader().getHash());
|
||||
TransactionOutputChanges txOutChanges = undoBlock.getTxOutChanges();
|
||||
for(StoredTransactionOutput out : txOutChanges.txOutsSpent)
|
||||
for(UTXO out : txOutChanges.txOutsSpent)
|
||||
blockStore.addUnspentTransactionOutput(out);
|
||||
for(StoredTransactionOutput out : txOutChanges.txOutsCreated)
|
||||
for(UTXO out : txOutChanges.txOutsCreated)
|
||||
blockStore.removeUnspentTransactionOutput(out);
|
||||
} catch (PrunedException e) {
|
||||
blockStore.abortDatabaseBatchWrite();
|
||||
|
@ -47,6 +47,11 @@ public class HeadersMessage extends Message {
|
||||
blockHeaders = Arrays.asList(headers);
|
||||
}
|
||||
|
||||
public HeadersMessage(NetworkParameters params, List<Block> headers) throws ProtocolException {
|
||||
super(params);
|
||||
blockHeaders = headers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bitcoinSerializeToStream(OutputStream stream) throws IOException {
|
||||
stream.write(new VarInt(blockHeaders.size()).encode());
|
||||
|
@ -22,7 +22,7 @@ import java.io.OutputStream;
|
||||
/**
|
||||
* The "mempool" message asks a remote peer to announce all transactions in its memory pool, possibly restricted by
|
||||
* any Bloom filter set on the connection. The list of transaction hashes comes back in an inv message. Note that
|
||||
* this is different to the {@link MemoryPool} object which doesn't try to keep track of all pending transactions,
|
||||
* this is different to the {@link TxConfidenceTable} object which doesn't try to keep track of all pending transactions,
|
||||
* it's just a holding area for transactions that a part of the app may find interesting. The mempool message has
|
||||
* no fields.
|
||||
*/
|
||||
|
@ -80,6 +80,8 @@ public abstract class NetworkParameters implements Serializable {
|
||||
protected int newTargetTimespan;
|
||||
protected int diffChangeTarget;
|
||||
protected byte[] alertSigningKey;
|
||||
protected int bip32HeaderPub;
|
||||
protected int bip32HeaderPriv;
|
||||
|
||||
/**
|
||||
* See getId(). This may be null for old deserialized wallets. In that case we derive it heuristically
|
||||
@ -272,7 +274,7 @@ public abstract class NetworkParameters implements Serializable {
|
||||
* block to be valid, it must be eventually possible to work backwards to the genesis block by following the
|
||||
* prevBlockHash pointers in the block headers.</p>
|
||||
*
|
||||
* <p>The genesis blocks for both test and prod networks contain the timestamp of when they were created,
|
||||
* <p>The genesis blocks for both test and main networks contain the timestamp of when they were created,
|
||||
* and a message in the coinbase transaction. It says, <i>"The Times 03/Jan/2009 Chancellor on brink of second
|
||||
* bailout for banks"</i>.</p>
|
||||
*/
|
||||
@ -372,4 +374,14 @@ public abstract class NetworkParameters implements Serializable {
|
||||
public byte[] getAlertSigningKey() {
|
||||
return alertSigningKey;
|
||||
}
|
||||
|
||||
/** Returns the 4 byte header for BIP32 (HD) wallet - public key part. */
|
||||
public int getBip32HeaderPub() {
|
||||
return bip32HeaderPub;
|
||||
}
|
||||
|
||||
/** Returns the 4 byte header for BIP32 (HD) wallet - private key part. */
|
||||
public int getBip32HeaderPriv() {
|
||||
return bip32HeaderPriv;
|
||||
}
|
||||
}
|
||||
|
@ -185,14 +185,14 @@ public class PartialMerkleTree extends Message {
|
||||
private Sha256Hash recursiveExtractHashes(int height, int pos, ValuesUsed used, List<Sha256Hash> matchedHashes) throws VerificationException {
|
||||
if (used.bitsUsed >= matchedChildBits.length*8) {
|
||||
// overflowed the bits array - failure
|
||||
throw new VerificationException("CPartialMerkleTree overflowed its bits array");
|
||||
throw new VerificationException("PartialMerkleTree overflowed its bits array");
|
||||
}
|
||||
boolean parentOfMatch = checkBitLE(matchedChildBits, used.bitsUsed++);
|
||||
if (height == 0 || !parentOfMatch) {
|
||||
// if at height 0, or nothing interesting below, use stored hash and do not descend
|
||||
if (used.hashesUsed >= hashes.size()) {
|
||||
// overflowed the hash array - failure
|
||||
throw new VerificationException("CPartialMerkleTree overflowed its hash array");
|
||||
throw new VerificationException("PartialMerkleTree overflowed its hash array");
|
||||
}
|
||||
Sha256Hash hash = hashes.get(used.hashesUsed++);
|
||||
if (height == 0 && parentOfMatch) // in case of height 0, we have a matched txid
|
||||
@ -201,10 +201,13 @@ public class PartialMerkleTree extends Message {
|
||||
} else {
|
||||
// otherwise, descend into the subtrees to extract matched txids and hashes
|
||||
byte[] left = recursiveExtractHashes(height - 1, pos * 2, used, matchedHashes).getBytes(), right;
|
||||
if (pos * 2 + 1 < getTreeWidth(transactionCount, height-1))
|
||||
if (pos * 2 + 1 < getTreeWidth(transactionCount, height-1)) {
|
||||
right = recursiveExtractHashes(height - 1, pos * 2 + 1, used, matchedHashes).getBytes();
|
||||
else
|
||||
if (Arrays.equals(right, left))
|
||||
throw new VerificationException("Invalid merkle tree with duplicated left/right branches");
|
||||
} else {
|
||||
right = left;
|
||||
}
|
||||
// and combine them before returning
|
||||
return combineLeftRight(left, right);
|
||||
}
|
||||
@ -223,13 +226,12 @@ public class PartialMerkleTree extends Message {
|
||||
* The returned root should be checked against the
|
||||
* merkle root contained in the block header for security.
|
||||
*
|
||||
* @param matchedHashes A list which will contain the matched txn (will be cleared)
|
||||
* Required to be a LinkedHashSet in order to retain order or transactions in the block
|
||||
* @param matchedHashesOut A list which will contain the matched txn (will be cleared).
|
||||
* @return the merkle root of this merkle tree
|
||||
* @throws ProtocolException if this partial merkle tree is invalid
|
||||
*/
|
||||
public Sha256Hash getTxnHashAndMerkleRoot(List<Sha256Hash> matchedHashes) throws VerificationException {
|
||||
matchedHashes.clear();
|
||||
public Sha256Hash getTxnHashAndMerkleRoot(List<Sha256Hash> matchedHashesOut) throws VerificationException {
|
||||
matchedHashesOut.clear();
|
||||
|
||||
// An empty set will not work
|
||||
if (transactionCount == 0)
|
||||
@ -249,7 +251,7 @@ public class PartialMerkleTree extends Message {
|
||||
height++;
|
||||
// traverse the partial tree
|
||||
ValuesUsed used = new ValuesUsed();
|
||||
Sha256Hash merkleRoot = recursiveExtractHashes(height, 0, used, matchedHashes);
|
||||
Sha256Hash merkleRoot = recursiveExtractHashes(height, 0, used, matchedHashesOut);
|
||||
// verify that all bits were consumed (except for the padding caused by serializing it as a byte sequence)
|
||||
if ((used.bitsUsed+7)/8 != matchedChildBits.length ||
|
||||
// verify that all hashes were consumed
|
||||
|
@ -89,7 +89,7 @@ public class Peer extends PeerSocketHandler {
|
||||
private final AtomicInteger blocksAnnounced = new AtomicInteger();
|
||||
// A class that tracks recent transactions that have been broadcast across the network, counts how many
|
||||
// peers announced them and updates the transaction confidence data. It is passed to each Peer.
|
||||
private final MemoryPool memoryPool;
|
||||
@Nullable private final TxConfidenceTable confidenceTable;
|
||||
// Each wallet added to the peer will be notified of downloaded transaction data.
|
||||
private final CopyOnWriteArrayList<Wallet> wallets;
|
||||
// A time before which we only download block headers, after that point we download block bodies.
|
||||
@ -132,12 +132,9 @@ public class Peer extends PeerSocketHandler {
|
||||
private static class GetDataRequest {
|
||||
Sha256Hash hash;
|
||||
SettableFuture future;
|
||||
// If the peer does not support the notfound message, we'll use ping/pong messages to simulate it. This is
|
||||
// a nasty hack that relies on the fact that bitcoin-qt is single threaded and processes messages in order.
|
||||
// The nonce field records which pong should clear this request as "not found".
|
||||
long nonce;
|
||||
}
|
||||
private final CopyOnWriteArrayList<GetDataRequest> getDataFutures;
|
||||
@GuardedBy("getAddrFutures") private final LinkedList<SettableFuture<AddressMessage>> getAddrFutures;
|
||||
|
||||
// Outstanding pings against this peer and how long the last one took to complete.
|
||||
private final ReentrantLock lastPingTimesLock = new ReentrantLock();
|
||||
@ -171,12 +168,12 @@ public class Peer extends PeerSocketHandler {
|
||||
* used to keep track of which peers relayed transactions and offer more descriptive logging.</p>
|
||||
*/
|
||||
public Peer(NetworkParameters params, VersionMessage ver, @Nullable AbstractBlockChain chain, PeerAddress remoteAddress) {
|
||||
this(params, ver, remoteAddress, chain, null);
|
||||
this(params, ver, remoteAddress, chain);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Construct a peer that reads/writes from the given block chain and memory pool. Transactions stored in a memory
|
||||
* pool will have their confidence levels updated when a peer announces it, to reflect the greater likelyhood that
|
||||
* <p>Construct a peer that reads/writes from the given block chain. Transactions stored in a {@link com.dogecoin.dogecoinj.core.TxConfidenceTable}
|
||||
* will have their confidence levels updated when a peer announces it, to reflect the greater likelyhood that
|
||||
* the transaction is valid.</p>
|
||||
*
|
||||
* <p>Note that this does <b>NOT</b> make a connection to the given remoteAddress, it only creates a handler for a
|
||||
@ -189,13 +186,13 @@ public class Peer extends PeerSocketHandler {
|
||||
* used to keep track of which peers relayed transactions and offer more descriptive logging.</p>
|
||||
*/
|
||||
public Peer(NetworkParameters params, VersionMessage ver, PeerAddress remoteAddress,
|
||||
@Nullable AbstractBlockChain chain, @Nullable MemoryPool mempool) {
|
||||
this(params, ver, remoteAddress, chain, mempool, true);
|
||||
@Nullable AbstractBlockChain chain) {
|
||||
this(params, ver, remoteAddress, chain, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Construct a peer that reads/writes from the given block chain and memory pool. Transactions stored in a memory
|
||||
* pool will have their confidence levels updated when a peer announces it, to reflect the greater likelyhood that
|
||||
* <p>Construct a peer that reads/writes from the given block chain. Transactions stored in a {@link com.dogecoin.dogecoinj.core.TxConfidenceTable}
|
||||
* will have their confidence levels updated when a peer announces it, to reflect the greater likelyhood that
|
||||
* the transaction is valid.</p>
|
||||
*
|
||||
* <p>Note that this does <b>NOT</b> make a connection to the given remoteAddress, it only creates a handler for a
|
||||
@ -208,20 +205,21 @@ public class Peer extends PeerSocketHandler {
|
||||
* used to keep track of which peers relayed transactions and offer more descriptive logging.</p>
|
||||
*/
|
||||
public Peer(NetworkParameters params, VersionMessage ver, PeerAddress remoteAddress,
|
||||
@Nullable AbstractBlockChain chain, @Nullable MemoryPool mempool, boolean downloadTxDependencies) {
|
||||
@Nullable AbstractBlockChain chain, boolean downloadTxDependencies) {
|
||||
super(params, remoteAddress);
|
||||
this.params = Preconditions.checkNotNull(params);
|
||||
this.versionMessage = Preconditions.checkNotNull(ver);
|
||||
this.vDownloadTxDependencies = downloadTxDependencies;
|
||||
this.vDownloadTxDependencies = chain != null && downloadTxDependencies;
|
||||
this.blockChain = chain; // Allowed to be null.
|
||||
this.vDownloadData = chain != null;
|
||||
this.getDataFutures = new CopyOnWriteArrayList<GetDataRequest>();
|
||||
this.eventListeners = new CopyOnWriteArrayList<PeerListenerRegistration>();
|
||||
this.getAddrFutures = new LinkedList<SettableFuture<AddressMessage>>();
|
||||
this.fastCatchupTimeSecs = params.getGenesisBlock().getTimeSeconds();
|
||||
this.isAcked = false;
|
||||
this.pendingPings = new CopyOnWriteArrayList<PendingPing>();
|
||||
this.wallets = new CopyOnWriteArrayList<Wallet>();
|
||||
this.memoryPool = mempool;
|
||||
this.confidenceTable = chain != null ? chain.getContext().getConfidenceTable() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -365,6 +363,7 @@ public class Peer extends PeerSocketHandler {
|
||||
// We don't care about addresses of the network right now. But in future,
|
||||
// we should save them in the wallet so we don't put too much load on the seed nodes and can
|
||||
// properly explore the network.
|
||||
processAddressMessage((AddressMessage) m);
|
||||
} else if (m instanceof HeadersMessage) {
|
||||
processHeaders((HeadersMessage) m);
|
||||
} else if (m instanceof AlertMessage) {
|
||||
@ -414,11 +413,23 @@ public class Peer extends PeerSocketHandler {
|
||||
utxosFuture = null;
|
||||
future.set((UTXOsMessage)m);
|
||||
}
|
||||
} else if (m instanceof RejectMessage) {
|
||||
log.error("Received Message {}", m);
|
||||
} else {
|
||||
log.warn("Received unhandled message: {}", m);
|
||||
}
|
||||
}
|
||||
|
||||
private void processAddressMessage(AddressMessage m) {
|
||||
SettableFuture<AddressMessage> future;
|
||||
synchronized (getAddrFutures) {
|
||||
future = getAddrFutures.poll();
|
||||
if (future == null) // Not an addr message we are waiting for.
|
||||
return;
|
||||
}
|
||||
future.set(m);
|
||||
}
|
||||
|
||||
private void processVersionMessage(VersionMessage m) throws ProtocolException {
|
||||
if (vPeerVersionMessage != null)
|
||||
throw new ProtocolException("Got two version messages from peer");
|
||||
@ -603,9 +614,9 @@ public class Peer extends PeerSocketHandler {
|
||||
lock.lock();
|
||||
try {
|
||||
log.debug("{}: Received tx {}", getAddress(), tx.getHashAsString());
|
||||
if (memoryPool != null) {
|
||||
if (confidenceTable != null) {
|
||||
// We may get back a different transaction object.
|
||||
tx = memoryPool.seen(tx, getAddress());
|
||||
tx = confidenceTable.seen(tx, getAddress());
|
||||
}
|
||||
fTx = tx;
|
||||
// Label the transaction as coming in from the P2P network (as opposed to being created by us, direct import,
|
||||
@ -706,7 +717,7 @@ public class Peer extends PeerSocketHandler {
|
||||
* <p>Note that dependencies downloaded this way will not trigger the onTransaction method of event listeners.</p>
|
||||
*/
|
||||
public ListenableFuture<List<Transaction>> downloadDependencies(Transaction tx) {
|
||||
checkNotNull(memoryPool, "Must have a configured MemoryPool object to download dependencies.");
|
||||
checkNotNull(confidenceTable, "Must have a configured TxConfidenceTable object to download dependencies.");
|
||||
TransactionConfidence.ConfidenceType txConfidence = tx.getConfidence().getConfidenceType();
|
||||
Preconditions.checkArgument(txConfidence != TransactionConfidence.ConfidenceType.BUILDING);
|
||||
log.info("{}: Downloading dependencies of {}", getAddress(), tx.getHashAsString());
|
||||
@ -732,7 +743,7 @@ public class Peer extends PeerSocketHandler {
|
||||
private ListenableFuture<Object> downloadDependenciesInternal(final Transaction tx,
|
||||
final Object marker,
|
||||
final List<Transaction> results) {
|
||||
checkNotNull(memoryPool, "Must have a configured MemoryPool object to download dependencies.");
|
||||
checkNotNull(confidenceTable, "Must have a configured TxConfidenceTable object to download dependencies.");
|
||||
final SettableFuture<Object> resultFuture = SettableFuture.create();
|
||||
final Sha256Hash rootTxHash = tx.getHash();
|
||||
// We want to recursively grab its dependencies. This is so listeners can learn important information like
|
||||
@ -747,7 +758,7 @@ public class Peer extends PeerSocketHandler {
|
||||
for (TransactionInput input : tx.getInputs()) {
|
||||
// There may be multiple inputs that connect to the same transaction.
|
||||
Sha256Hash hash = input.getOutpoint().getHash();
|
||||
Transaction dep = memoryPool.get(hash);
|
||||
Transaction dep = confidenceTable.get(hash);
|
||||
if (dep == null) {
|
||||
needToRequest.add(hash);
|
||||
} else {
|
||||
@ -760,7 +771,6 @@ public class Peer extends PeerSocketHandler {
|
||||
// Build the request for the missing dependencies.
|
||||
List<ListenableFuture<Transaction>> futures = Lists.newArrayList();
|
||||
GetDataMessage getdata = new GetDataMessage(params);
|
||||
final long nonce = (long)(Math.random()*Long.MAX_VALUE);
|
||||
if (needToRequest.size() > 1)
|
||||
log.info("{}: Requesting {} transactions for dep resolution", getAddress(), needToRequest.size());
|
||||
for (Sha256Hash hash : needToRequest) {
|
||||
@ -768,9 +778,6 @@ public class Peer extends PeerSocketHandler {
|
||||
GetDataRequest req = new GetDataRequest();
|
||||
req.hash = hash;
|
||||
req.future = SettableFuture.create();
|
||||
if (!isNotFoundMessageSupported()) {
|
||||
req.nonce = nonce;
|
||||
}
|
||||
futures.add(req.future);
|
||||
getDataFutures.add(req);
|
||||
}
|
||||
@ -819,25 +826,6 @@ public class Peer extends PeerSocketHandler {
|
||||
});
|
||||
// Start the operation.
|
||||
sendMessage(getdata);
|
||||
if (!isNotFoundMessageSupported()) {
|
||||
// If the peer isn't new enough to support the notfound message, we use a nasty hack instead and
|
||||
// assume if we send a ping message after the getdata message, it'll be processed after all answers
|
||||
// from getdata are done, so we can watch for the pong message as a substitute.
|
||||
log.info("{}: Dep resolution waiting for a pong with nonce {}", this, nonce);
|
||||
ping(nonce).addListener(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// The pong came back so clear out any transactions we requested but didn't get.
|
||||
for (GetDataRequest req : getDataFutures) {
|
||||
if (req.nonce == nonce) {
|
||||
log.info("{}: Bottomed out dep tree at {}", this, req.hash);
|
||||
req.future.cancel(true);
|
||||
getDataFutures.remove(req);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, Threading.SAME_THREAD);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("{}: Couldn't send getdata in downloadDependencies({})", this, tx.getHash());
|
||||
resultFuture.setException(e);
|
||||
@ -1086,7 +1074,7 @@ public class Peer extends PeerSocketHandler {
|
||||
Iterator<InventoryItem> it = transactions.iterator();
|
||||
while (it.hasNext()) {
|
||||
InventoryItem item = it.next();
|
||||
if (memoryPool == null) {
|
||||
if (confidenceTable == null) {
|
||||
if (downloadData) {
|
||||
// If there's no memory pool only download transactions if we're configured to.
|
||||
getdata.addItem(item);
|
||||
@ -1098,7 +1086,7 @@ public class Peer extends PeerSocketHandler {
|
||||
// peers run at different speeds. However to conserve bandwidth on mobile devices we try to only download a
|
||||
// transaction once. This means we can miss broadcasts if the peer disconnects between sending us an inv and
|
||||
// sending us the transaction: currently we'll never try to re-fetch after a timeout.
|
||||
if (memoryPool.maybeWasSeen(item.hash)) {
|
||||
if (confidenceTable.maybeWasSeen(item.hash)) {
|
||||
// Some other peer already announced this so don't download.
|
||||
it.remove();
|
||||
} else {
|
||||
@ -1106,7 +1094,7 @@ public class Peer extends PeerSocketHandler {
|
||||
getdata.addItem(item);
|
||||
}
|
||||
// This can trigger transaction confidence listeners.
|
||||
memoryPool.seen(item.hash, this.getAddress());
|
||||
confidenceTable.seen(item.hash, this.getAddress());
|
||||
}
|
||||
}
|
||||
|
||||
@ -1216,6 +1204,16 @@ public class Peer extends PeerSocketHandler {
|
||||
return req.future;
|
||||
}
|
||||
|
||||
/** Sends a getaddr request to the peer and returns a future that completes with the answer once the peer has replied. */
|
||||
public ListenableFuture<AddressMessage> getAddr() {
|
||||
SettableFuture<AddressMessage> future = SettableFuture.create();
|
||||
synchronized (getAddrFutures) {
|
||||
getAddrFutures.add(future);
|
||||
}
|
||||
sendMessage(new GetAddrMessage(params));
|
||||
return future;
|
||||
}
|
||||
|
||||
/**
|
||||
* When downloading the block chain, the bodies will be skipped for blocks created before the given date. Any
|
||||
* transactions relevant to the wallet will therefore not be found, but if you know your wallet has no such
|
||||
@ -1571,7 +1569,7 @@ public class Peer extends PeerSocketHandler {
|
||||
* unset a filter, though the underlying p2p protocol does support it.</p>
|
||||
*/
|
||||
public void setBloomFilter(BloomFilter filter) {
|
||||
setBloomFilter(filter, memoryPool != null || vDownloadData);
|
||||
setBloomFilter(filter, confidenceTable != null || vDownloadData);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -90,7 +90,7 @@ public class PeerAddress extends ChildMessage {
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a peer address from the given IP address. Port and protocol version are default for the prodnet.
|
||||
* Constructs a peer address from the given IP address. Port and protocol version are default for the mainnet.
|
||||
*/
|
||||
public PeerAddress(InetAddress addr) {
|
||||
this(addr, MainNetParams.get().getPort());
|
||||
@ -155,9 +155,6 @@ public class PeerAddress extends ChildMessage {
|
||||
port = ((0xFF & payload[cursor++]) << 8) | (0xFF & payload[cursor++]);
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see Message#getMessageSize()
|
||||
*/
|
||||
@Override
|
||||
public int getMessageSize() {
|
||||
// The 4 byte difference is the uint32 timestamp that was introduced in version 31402
|
||||
@ -165,72 +162,51 @@ public class PeerAddress extends ChildMessage {
|
||||
return length;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the addr
|
||||
*/
|
||||
public InetAddress getAddr() {
|
||||
maybeParse();
|
||||
return addr;
|
||||
}
|
||||
|
||||
public InetSocketAddress getSocketAddress() {
|
||||
return new InetSocketAddress(getAddr(), getPort());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param addr the addr to set
|
||||
*/
|
||||
public void setAddr(InetAddress addr) {
|
||||
unCache();
|
||||
this.addr = addr;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return the port
|
||||
*/
|
||||
public int getPort() {
|
||||
maybeParse();
|
||||
return port;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param port the port to set
|
||||
*/
|
||||
public void setPort(int port) {
|
||||
unCache();
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return the services
|
||||
*/
|
||||
public BigInteger getServices() {
|
||||
maybeParse();
|
||||
return services;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param services the services to set
|
||||
*/
|
||||
public void setServices(BigInteger services) {
|
||||
unCache();
|
||||
this.services = services;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return the time
|
||||
*/
|
||||
public long getTime() {
|
||||
maybeParse();
|
||||
return time;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param time the time to set
|
||||
*/
|
||||
public void setTime(long time) {
|
||||
unCache();
|
||||
this.time = time;
|
||||
|
@ -31,6 +31,16 @@ public interface PeerFilterProvider {
|
||||
*/
|
||||
public long getEarliestKeyCreationTime();
|
||||
|
||||
/**
|
||||
* Called on all registered filter providers before getBloomFilterElementCount and getBloomFilter are called.
|
||||
* Once called, the provider should ensure that the items it will want to insert into the filter don't change.
|
||||
* The reason is that all providers will have their element counts queried, and then a filter big enough for
|
||||
* all of them will be specified. So the provider must use consistent state. There is guaranteed to be a matching
|
||||
* call to endBloomFilterCalculation that can be used to e.g. unlock a lock.
|
||||
*/
|
||||
public void beginBloomFilterCalculation();
|
||||
|
||||
|
||||
/**
|
||||
* Gets the number of elements that will be added to a bloom filter returned by
|
||||
* {@link PeerFilterProvider#getBloomFilter(int, double, long)}
|
||||
@ -46,14 +56,5 @@ public interface PeerFilterProvider {
|
||||
/** Whether this filter provider depends on the server updating the filter on all matches */
|
||||
public boolean isRequiringUpdateAllBloomFilter();
|
||||
|
||||
/**
|
||||
* Returns an object that will be locked before any other methods are called and unlocked afterwards. You must
|
||||
* provide one of these because the results from calling the above methods must be consistent. Otherwise it's
|
||||
* possible for the {@link com.dogecoin.dogecoinj.net.FilterMerger} to request the counts of a bunch of providers
|
||||
* with {@link #getBloomFilterElementCount()}, create a filter of the right size, call {@link #getBloomFilter(int, double, long)}
|
||||
* and then the filter provider discovers it's been mutated in the mean time and now has a different number of
|
||||
* elements. For instance, a Wallet that has keys added to it whilst a filter recalc is in progress could cause
|
||||
* experience this race.
|
||||
*/
|
||||
public Lock getLock();
|
||||
public void endBloomFilterCalculation();
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -137,6 +137,13 @@ public class RejectMessage extends Message {
|
||||
return reason;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A String representation of the relevant details of this reject message.
|
||||
* Be aware that the value returned by this method includes the value returned by
|
||||
* {@link #getReasonString() getReasonString}, which is taken from the reject message unchecked.
|
||||
* Through malice or otherwise, it might contain control characters or other harmful content.
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
Sha256Hash hash = getRejectedObjectHash();
|
||||
|
@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2014 Adam Mackler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.dogecoin.dogecoinj.core;
|
||||
|
||||
/**
|
||||
* This exception is used by the TransactionBroadcast class to indicate that a broadcast
|
||||
* Transaction has been rejected by the network, for example because it violates a
|
||||
* protocol rule. Note that not all invalid transactions generate a reject message, and
|
||||
* some peers may never do so.
|
||||
*/
|
||||
public class RejectedTransactionException extends Exception {
|
||||
private Transaction tx;
|
||||
private RejectMessage rejectMessage;
|
||||
|
||||
public RejectedTransactionException(Transaction tx, RejectMessage rejectMessage) {
|
||||
super(rejectMessage.toString());
|
||||
this.tx = tx;
|
||||
this.rejectMessage = rejectMessage;
|
||||
}
|
||||
|
||||
/** Return the original Transaction object whose broadcast was rejected. */
|
||||
public Transaction getTransaction() { return tx; }
|
||||
|
||||
/** Return the RejectMessage object representing the broadcast rejection. */
|
||||
public RejectMessage getRejectMessage() { return rejectMessage; }
|
||||
}
|
@ -39,12 +39,11 @@ public class Sha256Hash implements Serializable, Comparable<Sha256Hash> {
|
||||
public static final Sha256Hash ZERO_HASH = new Sha256Hash(new byte[32]);
|
||||
|
||||
/**
|
||||
* Creates a Sha256Hash by wrapping the given byte array. It must be 32 bytes long.
|
||||
* Creates a Sha256Hash by wrapping the given byte array. It must be 32 bytes long. Takes ownership!
|
||||
*/
|
||||
public Sha256Hash(byte[] rawHashBytes) {
|
||||
checkArgument(rawHashBytes.length == 32);
|
||||
this.bytes = rawHashBytes;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -32,8 +32,6 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.*;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
|
||||
import static com.dogecoin.dogecoinj.core.Utils.*;
|
||||
@ -379,7 +377,7 @@ public class Transaction extends ChildMessage implements Serializable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the difference of {@link Transaction#getValueSentFromMe(TransactionBag)} and {@link Transaction#getValueSentToMe(TransactionBag)}.
|
||||
* Returns the difference of {@link Transaction#getValueSentToMe(TransactionBag)} and {@link Transaction#getValueSentFromMe(TransactionBag)}.
|
||||
*/
|
||||
public Coin getValue(TransactionBag wallet) throws ScriptException {
|
||||
return getValueSentToMe(wallet).subtract(getValueSentFromMe(wallet));
|
||||
@ -676,8 +674,11 @@ public class Transaction extends ChildMessage implements Serializable {
|
||||
s.append(outpoint.toString());
|
||||
final TransactionOutput connectedOutput = outpoint.getConnectedOutput();
|
||||
if (connectedOutput != null) {
|
||||
s.append(" hash160:");
|
||||
s.append(Utils.HEX.encode(connectedOutput.getScriptPubKey().getPubKeyHash()));
|
||||
Script scriptPubKey = connectedOutput.getScriptPubKey();
|
||||
if (scriptPubKey.isSentToAddress() || scriptPubKey.isPayToScriptHash()) {
|
||||
s.append(" hash160:");
|
||||
s.append(Utils.HEX.encode(scriptPubKey.getPubKeyHash()));
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
s.append("[exception: ").append(e.getMessage()).append("]");
|
||||
@ -704,6 +705,9 @@ public class Transaction extends ChildMessage implements Serializable {
|
||||
}
|
||||
s.append(String.format("%n"));
|
||||
}
|
||||
Coin fee = getFee();
|
||||
if (fee != null)
|
||||
s.append(" fee ").append(fee.toFriendlyString()).append(String.format("%n"));
|
||||
return s.toString();
|
||||
}
|
||||
|
||||
@ -1113,20 +1117,22 @@ public class Transaction extends ChildMessage implements Serializable {
|
||||
Collections.shuffle(outputs);
|
||||
}
|
||||
|
||||
/** @return the given transaction: same as getInputs().get(index). */
|
||||
public TransactionInput getInput(int index) {
|
||||
/** Same as getInputs().get(index). */
|
||||
public TransactionInput getInput(long index) {
|
||||
maybeParse();
|
||||
return inputs.get(index);
|
||||
return inputs.get((int)index);
|
||||
}
|
||||
|
||||
public TransactionOutput getOutput(int index) {
|
||||
/** Same as getOutputs().get(index) */
|
||||
public TransactionOutput getOutput(long index) {
|
||||
maybeParse();
|
||||
return outputs.get(index);
|
||||
return outputs.get((int)index);
|
||||
}
|
||||
|
||||
/** Returns the confidence object that is owned by this transaction object. */
|
||||
public synchronized TransactionConfidence getConfidence() {
|
||||
if (confidence == null) {
|
||||
confidence = new TransactionConfidence(this);
|
||||
confidence = new TransactionConfidence(getHash());
|
||||
}
|
||||
return confidence;
|
||||
}
|
||||
@ -1260,19 +1266,6 @@ public class Transaction extends ChildMessage implements Serializable {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the string either as a whole number of blocks, or if it contains slashes as a YYYY/MM/DD format date
|
||||
* and returns the lock time in wire format.
|
||||
*/
|
||||
public static long parseLockTimeStr(String lockTimeStr) throws ParseException {
|
||||
if (lockTimeStr.indexOf("/") != -1) {
|
||||
SimpleDateFormat format = new SimpleDateFormat("yyyy/MM/dd", Locale.US);
|
||||
Date date = format.parse(lockTimeStr);
|
||||
return date.getTime() / 1000;
|
||||
}
|
||||
return Long.parseLong(lockTimeStr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns either the lock time as a date, if it was specified in seconds, or an estimate based on the time in
|
||||
* the current head block if it was specified as a block time.
|
||||
|
@ -24,6 +24,7 @@ import com.google.common.util.concurrent.SettableFuture;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
@ -32,9 +33,8 @@ import java.util.Random;
|
||||
* Represents a single transaction broadcast that we are performing. A broadcast occurs after a new transaction is created
|
||||
* (typically by a {@link Wallet} and needs to be sent to the network. A broadcast can succeed or fail. A success is
|
||||
* defined as seeing the transaction be announced by peers via inv messages, thus indicating their acceptance. A failure
|
||||
* is defined as not reaching acceptance within a timeout period, or getting an explicit error message from peers
|
||||
* indicating that the transaction was not acceptable (this isn't currently implemented in v0.8 of the network protocol
|
||||
* but should be coming in 0.9).
|
||||
* is defined as not reaching acceptance within a timeout period, or getting an explicit reject message from a peer
|
||||
* indicating that the transaction was not acceptable.
|
||||
*/
|
||||
public class TransactionBroadcast {
|
||||
private static final Logger log = LoggerFactory.getLogger(TransactionBroadcast.class);
|
||||
@ -42,16 +42,19 @@ public class TransactionBroadcast {
|
||||
private final SettableFuture<Transaction> future = SettableFuture.create();
|
||||
private final PeerGroup peerGroup;
|
||||
private final Transaction tx;
|
||||
@Nullable private final Context context;
|
||||
private int minConnections;
|
||||
private int numWaitingFor, numToBroadcastTo;
|
||||
private int numWaitingFor;
|
||||
|
||||
/** Used for shuffling the peers before broadcast: unit tests can replace this to make themselves deterministic. */
|
||||
@VisibleForTesting
|
||||
public static Random random = new Random();
|
||||
private Transaction pinnedTx;
|
||||
|
||||
public TransactionBroadcast(PeerGroup peerGroup, Transaction tx) {
|
||||
// TODO: Context being owned by BlockChain isn't right w.r.t future intentions so it shouldn't really be optional here.
|
||||
TransactionBroadcast(PeerGroup peerGroup, @Nullable Context context, Transaction tx) {
|
||||
this.peerGroup = peerGroup;
|
||||
this.context = context;
|
||||
this.tx = tx;
|
||||
this.minConnections = Math.max(1, peerGroup.getMinBroadcastConnections());
|
||||
}
|
||||
@ -64,7 +67,22 @@ public class TransactionBroadcast {
|
||||
this.minConnections = minConnections;
|
||||
}
|
||||
|
||||
private PeerEventListener rejectionListener = new AbstractPeerEventListener() {
|
||||
@Override
|
||||
public Message onPreMessageReceived(Peer peer, Message m) {
|
||||
if (m instanceof RejectMessage) {
|
||||
RejectMessage rejectMessage = (RejectMessage)m;
|
||||
if (tx.getHash().equals(rejectMessage.getRejectedObjectHash())) {
|
||||
future.setException(new RejectedTransactionException(tx, rejectMessage));
|
||||
peerGroup.removeEventListener(this);
|
||||
}
|
||||
}
|
||||
return m;
|
||||
}
|
||||
};
|
||||
|
||||
public ListenableFuture<Transaction> broadcast() {
|
||||
peerGroup.addEventListener(rejectionListener, Threading.SAME_THREAD);
|
||||
log.info("Waiting for {} peers required for broadcast ...", minConnections);
|
||||
peerGroup.waitForPeers(minConnections).addListener(new EnoughAvailablePeers(), Threading.SAME_THREAD);
|
||||
return future;
|
||||
@ -84,7 +102,8 @@ public class TransactionBroadcast {
|
||||
// a big effect.
|
||||
List<Peer> peers = peerGroup.getConnectedPeers(); // snapshots
|
||||
// We intern the tx here so we are using a canonical version of the object (as it's unfortunately mutable).
|
||||
pinnedTx = peerGroup.getMemoryPool().intern(tx);
|
||||
// TODO: Once confidence state is moved out of Transaction we can kill off this step.
|
||||
pinnedTx = context != null ? context.getConfidenceTable().intern(tx) : pinnedTx;
|
||||
// Prepare to send the transaction by adding a listener that'll be called when confidence changes.
|
||||
// Only bother with this if we might actually hear back:
|
||||
if (minConnections > 1)
|
||||
@ -98,7 +117,7 @@ public class TransactionBroadcast {
|
||||
// our version message, as SPV nodes cannot relay it doesn't give away any additional information
|
||||
// to skip the inv here - we wouldn't send invs anyway.
|
||||
int numConnected = peers.size();
|
||||
numToBroadcastTo = (int) Math.max(1, Math.round(Math.ceil(peers.size() / 2.0)));
|
||||
int numToBroadcastTo = (int) Math.max(1, Math.round(Math.ceil(peers.size() / 2.0)));
|
||||
numWaitingFor = (int) Math.ceil((peers.size() - numToBroadcastTo) / 2.0);
|
||||
Collections.shuffle(peers, random);
|
||||
peers = peers.subList(0, numToBroadcastTo);
|
||||
@ -118,6 +137,7 @@ public class TransactionBroadcast {
|
||||
// So we just have to assume we're done, at that point. This happens when we're not given
|
||||
// any peer discovery source and the user just calls connectTo() once.
|
||||
if (minConnections == 1) {
|
||||
peerGroup.removeEventListener(rejectionListener);
|
||||
future.set(pinnedTx);
|
||||
}
|
||||
}
|
||||
@ -125,9 +145,8 @@ public class TransactionBroadcast {
|
||||
|
||||
private class ConfidenceChange implements TransactionConfidence.Listener {
|
||||
@Override
|
||||
public void onConfidenceChanged(Transaction tx, ChangeReason reason) {
|
||||
public void onConfidenceChanged(TransactionConfidence conf, ChangeReason reason) {
|
||||
// The number of peers that announced this tx has gone up.
|
||||
final TransactionConfidence conf = tx.getConfidence();
|
||||
int numSeenPeers = conf.numBroadcastPeers();
|
||||
boolean mined = tx.getAppearsInHashes() != null;
|
||||
log.info("broadcastTransaction: {}: TX {} seen by {} peers{}", reason, pinnedTx.getHashAsString(),
|
||||
@ -147,7 +166,8 @@ public class TransactionBroadcast {
|
||||
// We're done! It's important that the PeerGroup lock is not held (by this thread) at this
|
||||
// point to avoid triggering inversions when the Future completes.
|
||||
log.info("broadcastTransaction: {} complete", pinnedTx.getHashAsString());
|
||||
tx.getConfidence().removeEventListener(this);
|
||||
peerGroup.removeEventListener(rejectionListener);
|
||||
conf.removeEventListener(this);
|
||||
future.set(pinnedTx); // RE-ENTRANCY POINT
|
||||
}
|
||||
}
|
||||
|
@ -70,7 +70,7 @@ public class TransactionConfidence implements Serializable {
|
||||
*/
|
||||
private CopyOnWriteArrayList<PeerAddress> broadcastBy;
|
||||
/** The Transaction that this confidence object is associated with. */
|
||||
private final Transaction transaction;
|
||||
private final Sha256Hash hash;
|
||||
// Lazily created listeners array.
|
||||
private transient CopyOnWriteArrayList<ListenerRegistration<Listener>> listeners;
|
||||
|
||||
@ -135,11 +135,11 @@ public class TransactionConfidence implements Serializable {
|
||||
}
|
||||
private Source source = Source.UNKNOWN;
|
||||
|
||||
public TransactionConfidence(Transaction tx) {
|
||||
public TransactionConfidence(Sha256Hash hash) {
|
||||
// Assume a default number of peers for our set.
|
||||
broadcastBy = new CopyOnWriteArrayList<PeerAddress>();
|
||||
listeners = new CopyOnWriteArrayList<ListenerRegistration<Listener>>();
|
||||
transaction = tx;
|
||||
this.hash = hash;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -175,7 +175,7 @@ public class TransactionConfidence implements Serializable {
|
||||
*/
|
||||
SEEN_PEERS,
|
||||
}
|
||||
public void onConfidenceChanged(Transaction tx, ChangeReason reason);
|
||||
public void onConfidenceChanged(TransactionConfidence confidence, ChangeReason reason);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -382,7 +382,7 @@ public class TransactionConfidence implements Serializable {
|
||||
|
||||
/** Returns a copy of this object. Event listeners are not duplicated. */
|
||||
public synchronized TransactionConfidence duplicate() {
|
||||
TransactionConfidence c = new TransactionConfidence(transaction);
|
||||
TransactionConfidence c = new TransactionConfidence(hash);
|
||||
// There is no point in this sync block, it's just to help FindBugs.
|
||||
synchronized (c) {
|
||||
c.broadcastBy.addAll(broadcastBy);
|
||||
@ -404,7 +404,7 @@ public class TransactionConfidence implements Serializable {
|
||||
registration.executor.execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
registration.listener.onConfidenceChanged(transaction, reason);
|
||||
registration.listener.onConfidenceChanged(TransactionConfidence.this, reason);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -435,23 +435,27 @@ public class TransactionConfidence implements Serializable {
|
||||
* depth to one will wait until it appears in a block on the best chain, and zero will wait until it has been seen
|
||||
* on the network.
|
||||
*/
|
||||
public synchronized ListenableFuture<Transaction> getDepthFuture(final int depth, Executor executor) {
|
||||
final SettableFuture<Transaction> result = SettableFuture.create();
|
||||
public synchronized ListenableFuture<TransactionConfidence> getDepthFuture(final int depth, Executor executor) {
|
||||
final SettableFuture<TransactionConfidence> result = SettableFuture.create();
|
||||
if (getDepthInBlocks() >= depth) {
|
||||
result.set(transaction);
|
||||
result.set(this);
|
||||
}
|
||||
addEventListener(new Listener() {
|
||||
@Override public void onConfidenceChanged(Transaction tx, ChangeReason reason) {
|
||||
@Override public void onConfidenceChanged(TransactionConfidence confidence, ChangeReason reason) {
|
||||
if (getDepthInBlocks() >= depth) {
|
||||
removeEventListener(this);
|
||||
result.set(transaction);
|
||||
result.set(confidence);
|
||||
}
|
||||
}
|
||||
}, executor);
|
||||
return result;
|
||||
}
|
||||
|
||||
public synchronized ListenableFuture<Transaction> getDepthFuture(final int depth) {
|
||||
public synchronized ListenableFuture<TransactionConfidence> getDepthFuture(final int depth) {
|
||||
return getDepthFuture(depth, Threading.USER_THREAD);
|
||||
}
|
||||
|
||||
public Sha256Hash getTransactionHash() {
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
|
@ -91,7 +91,11 @@ public class TransactionInput extends ChildMessage implements Serializable {
|
||||
TransactionInput(NetworkParameters params, Transaction parentTransaction, TransactionOutput output) {
|
||||
super(params);
|
||||
long outputIndex = output.getIndex();
|
||||
outpoint = new TransactionOutPoint(params, outputIndex, output.getParentTransaction());
|
||||
if(output.getParentTransaction() != null ) {
|
||||
outpoint = new TransactionOutPoint(params, outputIndex, output.getParentTransaction());
|
||||
} else {
|
||||
outpoint = new TransactionOutPoint(params, output);
|
||||
}
|
||||
scriptBytes = EMPTY_ARRAY;
|
||||
sequence = NO_SEQUENCE;
|
||||
setParent(parentTransaction);
|
||||
|
@ -46,6 +46,9 @@ public class TransactionOutPoint extends ChildMessage implements Serializable {
|
||||
// It points to the connected transaction.
|
||||
Transaction fromTx;
|
||||
|
||||
// The connected output.
|
||||
private TransactionOutput connectedOutput;
|
||||
|
||||
public TransactionOutPoint(NetworkParameters params, long index, @Nullable Transaction fromTx) {
|
||||
super(params);
|
||||
this.index = index;
|
||||
@ -66,6 +69,11 @@ public class TransactionOutPoint extends ChildMessage implements Serializable {
|
||||
length = MESSAGE_LENGTH;
|
||||
}
|
||||
|
||||
public TransactionOutPoint(NetworkParameters params, TransactionOutput connectedOutput) {
|
||||
this(params, connectedOutput.getIndex(), connectedOutput.getParentTransactionHash());
|
||||
this.connectedOutput = connectedOutput;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Deserializes the message. This is usually part of a transaction message.
|
||||
@ -120,8 +128,12 @@ public class TransactionOutPoint extends ChildMessage implements Serializable {
|
||||
*/
|
||||
@Nullable
|
||||
public TransactionOutput getConnectedOutput() {
|
||||
if (fromTx == null) return null;
|
||||
return fromTx.getOutputs().get((int) index);
|
||||
if (fromTx != null) {
|
||||
return fromTx.getOutputs().get((int) index);
|
||||
} else if (connectedOutput != null) {
|
||||
return connectedOutput;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -91,7 +91,7 @@ public class TransactionOutput extends ChildMessage implements Serializable {
|
||||
|
||||
/**
|
||||
* Creates an output that sends 'value' to the given address (public key hash). The amount should be created with
|
||||
* something like {@link Utils#valueOf(int, int)}. Typically you would use
|
||||
* something like {@link Coin#valueOf(int, int)}. Typically you would use
|
||||
* {@link Transaction#addOutput(Coin, Address)} instead of creating a TransactionOutput directly.
|
||||
*/
|
||||
public TransactionOutput(NetworkParameters params, @Nullable Transaction parent, Coin value, Address to) {
|
||||
@ -100,7 +100,7 @@ public class TransactionOutput extends ChildMessage implements Serializable {
|
||||
|
||||
/**
|
||||
* Creates an output that sends 'value' to the given public key using a simple CHECKSIG script (no addresses). The
|
||||
* amount should be created with something like {@link Utils#valueOf(int, int)}. Typically you would use
|
||||
* amount should be created with something like {@link Coin#valueOf(int, int)}. Typically you would use
|
||||
* {@link Transaction#addOutput(Coin, ECKey)} instead of creating an output directly.
|
||||
*/
|
||||
public TransactionOutput(NetworkParameters params, @Nullable Transaction parent, Coin value, ECKey to) {
|
||||
@ -257,7 +257,7 @@ public class TransactionOutput extends ChildMessage implements Serializable {
|
||||
|
||||
/**
|
||||
* Returns the minimum value for this output to be considered "not dust", i.e. the transaction will be relayable
|
||||
* and mined by default miners. For normal pay to address outputs, this is 5460 satoshis, the same as
|
||||
* and mined by default miners. For normal pay to address outputs, this is 546 satoshis, the same as
|
||||
* {@link Transaction#MIN_NONDUST_OUTPUT}.
|
||||
*/
|
||||
public Coin getMinNonDustValue() {
|
||||
@ -388,10 +388,42 @@ public class TransactionOutput extends ChildMessage implements Serializable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the transaction that owns this output, or throws NullPointerException if unowned.
|
||||
* Returns the transaction that owns this output.
|
||||
*/
|
||||
@Nullable
|
||||
public Transaction getParentTransaction() {
|
||||
return checkNotNull((Transaction) parent, "Free-standing TransactionOutput");
|
||||
if(parent != null) {
|
||||
return (Transaction) parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the transaction hash that owns this output.
|
||||
*/
|
||||
@Nullable
|
||||
public Sha256Hash getParentTransactionHash() {
|
||||
if (getParentTransaction() != null) {
|
||||
return getParentTransaction().getHash();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the depth in blocks of the parent tx.
|
||||
*
|
||||
* <p>If the transaction appears in the top block, the depth is one. If it's anything else (pending, dead, unknown)
|
||||
* then -1.</p>
|
||||
* @return The tx depth or -1.
|
||||
*/
|
||||
public int getParentTransactionDepthInBlocks() {
|
||||
if (getParentTransaction() != null) {
|
||||
TransactionConfidence confidence = getParentTransaction().getConfidence();
|
||||
if (confidence.getConfidenceType() == TransactionConfidence.ConfidenceType.BUILDING) {
|
||||
return confidence.getDepthInBlocks();
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -29,10 +29,10 @@ import java.util.List;
|
||||
* BIP30 (no duplicate txid creation if the previous one was not fully spent prior to this block) verification.</p>
|
||||
*/
|
||||
public class TransactionOutputChanges {
|
||||
public final List<StoredTransactionOutput> txOutsCreated;
|
||||
public final List<StoredTransactionOutput> txOutsSpent;
|
||||
public final List<UTXO> txOutsCreated;
|
||||
public final List<UTXO> txOutsSpent;
|
||||
|
||||
public TransactionOutputChanges(List<StoredTransactionOutput> txOutsCreated, List<StoredTransactionOutput> txOutsSpent) {
|
||||
public TransactionOutputChanges(List<UTXO> txOutsCreated, List<UTXO> txOutsSpent) {
|
||||
this.txOutsCreated = txOutsCreated;
|
||||
this.txOutsSpent = txOutsSpent;
|
||||
}
|
||||
@ -42,17 +42,17 @@ public class TransactionOutputChanges {
|
||||
((in.read() & 0xFF) << 8) |
|
||||
((in.read() & 0xFF) << 16) |
|
||||
((in.read() & 0xFF) << 24);
|
||||
txOutsCreated = new LinkedList<StoredTransactionOutput>();
|
||||
txOutsCreated = new LinkedList<UTXO>();
|
||||
for (int i = 0; i < numOutsCreated; i++)
|
||||
txOutsCreated.add(new StoredTransactionOutput(in));
|
||||
txOutsCreated.add(new UTXO(in));
|
||||
|
||||
int numOutsSpent = ((in.read() & 0xFF) << 0) |
|
||||
((in.read() & 0xFF) << 8) |
|
||||
((in.read() & 0xFF) << 16) |
|
||||
((in.read() & 0xFF) << 24);
|
||||
txOutsSpent = new LinkedList<StoredTransactionOutput>();
|
||||
txOutsSpent = new LinkedList<UTXO>();
|
||||
for (int i = 0; i < numOutsSpent; i++)
|
||||
txOutsSpent.add(new StoredTransactionOutput(in));
|
||||
txOutsSpent.add(new UTXO(in));
|
||||
}
|
||||
|
||||
public void serializeToStream(OutputStream bos) throws IOException {
|
||||
@ -61,7 +61,7 @@ public class TransactionOutputChanges {
|
||||
bos.write(0xFF & (numOutsCreated >> 8));
|
||||
bos.write(0xFF & (numOutsCreated >> 16));
|
||||
bos.write(0xFF & (numOutsCreated >> 24));
|
||||
for (StoredTransactionOutput output : txOutsCreated) {
|
||||
for (UTXO output : txOutsCreated) {
|
||||
output.serializeToStream(bos);
|
||||
}
|
||||
|
||||
@ -70,7 +70,7 @@ public class TransactionOutputChanges {
|
||||
bos.write(0xFF & (numOutsSpent >> 8));
|
||||
bos.write(0xFF & (numOutsSpent >> 16));
|
||||
bos.write(0xFF & (numOutsSpent >> 24));
|
||||
for (StoredTransactionOutput output : txOutsSpent) {
|
||||
for (UTXO output : txOutsSpent) {
|
||||
output.serializeToStream(bos);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,318 @@
|
||||
/*
|
||||
* Copyright 2012 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.dogecoin.dogecoinj.core;
|
||||
|
||||
import com.dogecoin.dogecoinj.utils.Threading;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.lang.ref.Reference;
|
||||
import java.lang.ref.ReferenceQueue;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
|
||||
/**
|
||||
* <p>Tracks transactions that are being announced across the network. Typically one is created for you by a
|
||||
* {@link PeerGroup} and then given to each Peer to update. The current purpose is to let Peers update the confidence
|
||||
* (number of peers broadcasting). It helps address an attack scenario in which a malicious remote peer (or several)
|
||||
* feeds you invalid transactions, eg, ones that spend coins which don't exist. If you don't see most of the peers
|
||||
* announce the transaction within a reasonable time, it may be that the TX is not valid. Alternatively, an attacker
|
||||
* may control your entire internet connection: in this scenario counting broadcasting peers does not help you.</p>
|
||||
*
|
||||
* <p>It is <b>not</b> at this time directly equivalent to the Satoshi clients memory pool, which tracks
|
||||
* all transactions not currently included in the best chain - it's simply a cache.</p>
|
||||
*/
|
||||
public class TxConfidenceTable {
|
||||
private static final Logger log = LoggerFactory.getLogger(TxConfidenceTable.class);
|
||||
protected ReentrantLock lock = Threading.lock("txconfidencetable");
|
||||
|
||||
// For each transaction we may have seen:
|
||||
// - only its hash in an inv packet
|
||||
// - the full transaction itself, if we asked for it to be sent to us (or a peer sent it regardless), or if we
|
||||
// sent it.
|
||||
//
|
||||
// Before we see the full transaction, we need to track how many peers advertised it, so we can estimate its
|
||||
// confidence pre-chain inclusion assuming an un-tampered with network connection. After we see the full transaction
|
||||
// we need to switch from tracking that data in the Entry to tracking it in the TransactionConfidence object itself.
|
||||
private static class WeakTransactionReference extends WeakReference<Transaction> {
|
||||
public Sha256Hash hash;
|
||||
public WeakTransactionReference(Transaction tx, ReferenceQueue<Transaction> queue) {
|
||||
super(tx, queue);
|
||||
hash = tx.getHash();
|
||||
}
|
||||
}
|
||||
private static class Entry {
|
||||
// Invariants: one of the two fields must be null, to indicate which is used.
|
||||
Set<PeerAddress> addresses;
|
||||
// We keep a weak reference to the transaction. This means that if no other bit of code finds the transaction
|
||||
// worth keeping around it will drop out of memory and we will, at some point, forget about it, which means
|
||||
// both addresses and tx.get() will be null. When this happens the WeakTransactionReference appears in the queue
|
||||
// allowing us to delete the associated entry (the tx itself has already gone away).
|
||||
WeakTransactionReference tx;
|
||||
}
|
||||
private LinkedHashMap<Sha256Hash, Entry> table;
|
||||
|
||||
// This ReferenceQueue gets entries added to it when they are only weakly reachable, ie, the TxConfidenceTable is the
|
||||
// only thing that is tracking the transaction anymore. We check it from time to time and delete table entries
|
||||
// corresponding to expired transactions. In this way memory usage of the system is in line with however many
|
||||
// transactions you actually care to track the confidence of. We can still end up with lots of hashes being stored
|
||||
// if our peers flood us with invs but the MAX_SIZE param caps this.
|
||||
private ReferenceQueue<Transaction> referenceQueue;
|
||||
|
||||
/** The max size of a table created with the no-args constructor. */
|
||||
public static final int MAX_SIZE = 1000;
|
||||
|
||||
/**
|
||||
* Creates a table that will track at most the given number of transactions (allowing you to bound memory
|
||||
* usage).
|
||||
* @param size Max number of transactions to track. The table will fill up to this size then stop growing.
|
||||
*/
|
||||
public TxConfidenceTable(final int size) {
|
||||
table = new LinkedHashMap<Sha256Hash, Entry>() {
|
||||
@Override
|
||||
protected boolean removeEldestEntry(Map.Entry<Sha256Hash, TxConfidenceTable.Entry> entry) {
|
||||
// An arbitrary choice to stop the memory used by tracked transactions getting too huge in the event
|
||||
// of some kind of DoS attack.
|
||||
return size() > size;
|
||||
}
|
||||
};
|
||||
referenceQueue = new ReferenceQueue<Transaction>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a table that will track at most {@link TxConfidenceTable#MAX_SIZE} entries. You should normally use
|
||||
* this constructor.
|
||||
*/
|
||||
public TxConfidenceTable() {
|
||||
this(MAX_SIZE);
|
||||
}
|
||||
|
||||
/**
|
||||
* If any transactions have expired due to being only weakly reachable through us, go ahead and delete their
|
||||
* table entries - it means we downloaded the transaction and sent it to various event listeners, none of
|
||||
* which bothered to keep a reference. Typically, this is because the transaction does not involve any keys that
|
||||
* are relevant to any of our wallets.
|
||||
*/
|
||||
private void cleanTable() {
|
||||
lock.lock();
|
||||
try {
|
||||
Reference<? extends Transaction> ref;
|
||||
while ((ref = referenceQueue.poll()) != null) {
|
||||
// Find which transaction got deleted by the GC.
|
||||
WeakTransactionReference txRef = (WeakTransactionReference) ref;
|
||||
// And remove the associated map entry so the other bits of memory can also be reclaimed.
|
||||
table.remove(txRef.hash);
|
||||
}
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of peers that have seen the given hash recently.
|
||||
*/
|
||||
public int numBroadcastPeers(Sha256Hash txHash) {
|
||||
lock.lock();
|
||||
try {
|
||||
cleanTable();
|
||||
Entry entry = table.get(txHash);
|
||||
if (entry == null) {
|
||||
// No such TX known.
|
||||
return 0;
|
||||
} else if (entry.tx == null) {
|
||||
// We've seen at least one peer announce with an inv.
|
||||
checkNotNull(entry.addresses);
|
||||
return entry.addresses.size();
|
||||
} else {
|
||||
final Transaction tx = entry.tx.get();
|
||||
if (tx == null) {
|
||||
// We previously downloaded this transaction, but nothing cared about it so the garbage collector threw
|
||||
// it away. We also deleted the set that tracked which peers had seen it. Treat this case as a zero and
|
||||
// just delete it from the map.
|
||||
table.remove(txHash);
|
||||
return 0;
|
||||
} else {
|
||||
checkState(entry.addresses == null);
|
||||
return tx.getConfidence().numBroadcastPeers();
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Puts the tx into the table and returns either it, or a different Transaction object that has the same hash.
|
||||
* Unlike seen and the other methods, this one does not imply that a tx has been announced by a peer and does
|
||||
* not mark it as such.
|
||||
*/
|
||||
public Transaction intern(Transaction tx) {
|
||||
lock.lock();
|
||||
try {
|
||||
cleanTable();
|
||||
Entry entry = table.get(tx.getHash());
|
||||
if (entry != null) {
|
||||
// This TX or its hash have been previously interned.
|
||||
if (entry.tx != null) {
|
||||
// We already interned it (but may have thrown it away).
|
||||
checkState(entry.addresses == null);
|
||||
// We only want one canonical object instance for a transaction no matter how many times it is
|
||||
// deserialized.
|
||||
Transaction transaction = entry.tx.get();
|
||||
if (transaction != null) {
|
||||
// We saw it before and kept it around. Hand back the canonical copy.
|
||||
tx = transaction;
|
||||
}
|
||||
return tx;
|
||||
} else {
|
||||
// We received a transaction that we have previously seen announced but not downloaded until now.
|
||||
checkNotNull(entry.addresses);
|
||||
entry.tx = new WeakTransactionReference(tx, referenceQueue);
|
||||
Set<PeerAddress> addrs = entry.addresses;
|
||||
entry.addresses = null;
|
||||
TransactionConfidence confidence = tx.getConfidence();
|
||||
log.debug("Adding tx [{}] {} to the confidence table",
|
||||
confidence.numBroadcastPeers(), tx.getHashAsString());
|
||||
for (PeerAddress a : addrs) {
|
||||
markBroadcast(a, tx);
|
||||
}
|
||||
return tx;
|
||||
}
|
||||
} else {
|
||||
// This often happens when we are downloading a Bloom filtered chain, or recursively downloading
|
||||
// dependencies of a relevant transaction (see Peer.downloadDependencies).
|
||||
log.debug("Provided with a downloaded transaction we didn't see announced yet: {}", tx.getHashAsString());
|
||||
entry = new Entry();
|
||||
entry.tx = new WeakTransactionReference(tx, referenceQueue);
|
||||
table.put(tx.getHash(), entry);
|
||||
return tx;
|
||||
}
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by peers when they receive a "tx" message containing a valid serialized transaction.
|
||||
* @param tx The TX deserialized from the wire.
|
||||
* @param byPeer The Peer that received it.
|
||||
* @return An object that is semantically the same TX but may be a different object instance.
|
||||
*/
|
||||
public Transaction seen(Transaction tx, PeerAddress byPeer) {
|
||||
lock.lock();
|
||||
try {
|
||||
final Transaction interned = intern(tx);
|
||||
markBroadcast(byPeer, interned);
|
||||
return interned;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by peers when they see a transaction advertised in an "inv" message. It either will increase the
|
||||
* confidence of the pre-existing transaction or will just keep a record of the address for future usage.
|
||||
*/
|
||||
public void seen(Sha256Hash hash, PeerAddress byPeer) {
|
||||
lock.lock();
|
||||
try {
|
||||
cleanTable();
|
||||
Entry entry = table.get(hash);
|
||||
if (entry != null) {
|
||||
// This TX or its hash have been previously announced.
|
||||
if (entry.tx != null) {
|
||||
checkState(entry.addresses == null);
|
||||
Transaction tx = entry.tx.get();
|
||||
if (tx != null) {
|
||||
markBroadcast(byPeer, tx);
|
||||
log.debug("{}: Peer announced transaction we have seen before [{}] {}",
|
||||
byPeer, tx.getConfidence().numBroadcastPeers(), tx.getHashAsString());
|
||||
} else {
|
||||
// The inv is telling us about a transaction that we previously downloaded, and threw away
|
||||
// because nothing found it interesting enough to keep around. So do nothing.
|
||||
}
|
||||
} else {
|
||||
checkNotNull(entry.addresses);
|
||||
entry.addresses.add(byPeer);
|
||||
log.debug("{}: Peer announced transaction we have seen announced before [{}] {}",
|
||||
byPeer, entry.addresses.size(), hash);
|
||||
}
|
||||
} else {
|
||||
// This TX has never been seen before.
|
||||
entry = new Entry();
|
||||
// TODO: Using hashsets here is inefficient compared to just having an array.
|
||||
entry.addresses = new HashSet<PeerAddress>();
|
||||
entry.addresses.add(byPeer);
|
||||
table.put(hash, entry);
|
||||
log.info("{}: Peer announced new transaction [1] {}", byPeer, hash);
|
||||
}
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void markBroadcast(PeerAddress byPeer, Transaction tx) {
|
||||
checkState(lock.isHeldByCurrentThread());
|
||||
final TransactionConfidence confidence = tx.getConfidence();
|
||||
if (confidence.markBroadcastBy(byPeer))
|
||||
confidence.queueListeners(TransactionConfidence.Listener.ChangeReason.SEEN_PEERS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link Transaction} for the given hash if we have downloaded it, or null if that hash is unknown or
|
||||
* we only saw advertisements for it yet or it has been downloaded but garbage collected due to nowhere else
|
||||
* holding a reference to it.
|
||||
*/
|
||||
@Nullable
|
||||
public Transaction get(Sha256Hash hash) {
|
||||
lock.lock();
|
||||
try {
|
||||
Entry entry = table.get(hash);
|
||||
if (entry == null) return null; // Unknown.
|
||||
if (entry.tx == null) return null; // Seen but only in advertisements.
|
||||
if (entry.tx.get() == null) return null; // Was downloaded but garbage collected.
|
||||
Transaction tx = entry.tx.get();
|
||||
checkNotNull(tx);
|
||||
return tx;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the TX identified by hash has been seen before (ie, in an inv). Note that a transaction that
|
||||
* was broadcast, downloaded and nothing kept a reference to it will eventually be cleared out by the garbage
|
||||
* collector and wasSeen() will return false - it does not keep a permanent record of every hash ever broadcast.
|
||||
*/
|
||||
public boolean maybeWasSeen(Sha256Hash hash) {
|
||||
lock.lock();
|
||||
try {
|
||||
Entry entry = table.get(hash);
|
||||
return entry != null;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
249
core/src/main/java/com/dogecoin/dogecoinj/core/UTXO.java
Normal file
249
core/src/main/java/com/dogecoin/dogecoinj/core/UTXO.java
Normal file
@ -0,0 +1,249 @@
|
||||
/**
|
||||
* Copyright 2012 Matt Corallo.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.dogecoin.dogecoinj.core;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.*;
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* A UTXO message contains the information necessary to check a spending transaction.
|
||||
* It avoids having to store the entire parentTransaction just to get the hash and index.
|
||||
* Useful when working with free standing outputs.
|
||||
*/
|
||||
public class UTXO implements Serializable {
|
||||
private static final Logger log = LoggerFactory.getLogger(UTXO.class);
|
||||
private static final long serialVersionUID = -8744924157056340509L;
|
||||
|
||||
/**
|
||||
* A transaction output has some value and a script used for authenticating that the redeemer is allowed to spend
|
||||
* this output.
|
||||
*/
|
||||
private Coin value;
|
||||
private byte[] scriptBytes;
|
||||
|
||||
/** Hash of the transaction to which we refer. */
|
||||
private Sha256Hash hash;
|
||||
/** Which output of that transaction we are talking about. */
|
||||
private long index;
|
||||
/** The height of the tx of this output */
|
||||
private int height;
|
||||
/** If this output is from a coinbase tx */
|
||||
private boolean coinbase;
|
||||
/** The address of this output */
|
||||
private String address;
|
||||
/** The type of this address */
|
||||
private int addressType;
|
||||
|
||||
/**
|
||||
* Creates a stored transaction output.
|
||||
* @param hash The hash of the containing transaction.
|
||||
* @param index The outpoint.
|
||||
* @param value The value available.
|
||||
* @param height The height this output was created in.
|
||||
* @param coinbase The coinbase flag.
|
||||
* @param scriptBytes The script bytes.
|
||||
*/
|
||||
public UTXO(Sha256Hash hash,
|
||||
long index,
|
||||
Coin value,
|
||||
int height,
|
||||
boolean coinbase,
|
||||
byte[] scriptBytes) {
|
||||
this.hash = hash;
|
||||
this.index = index;
|
||||
this.value = value;
|
||||
this.height = height;
|
||||
this.scriptBytes = scriptBytes;
|
||||
this.coinbase = coinbase;
|
||||
this.address = "";
|
||||
this.addressType = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a stored transaction output.
|
||||
* @param hash The hash of the containing transaction.
|
||||
* @param index The outpoint.
|
||||
* @param value The value available.
|
||||
* @param height The height this output was created in.
|
||||
* @param coinbase The coinbase flag.
|
||||
* @param scriptBytes The script bytes.
|
||||
* @param address The address.
|
||||
* @param addressType The address type.
|
||||
*/
|
||||
public UTXO(Sha256Hash hash,
|
||||
long index,
|
||||
Coin value,
|
||||
int height,
|
||||
boolean coinbase,
|
||||
byte[] scriptBytes,
|
||||
String address,
|
||||
int addressType) {
|
||||
this(hash, index, value, height, coinbase, scriptBytes);
|
||||
this.address = address;
|
||||
this.addressType = addressType;
|
||||
}
|
||||
|
||||
public UTXO(InputStream in) throws IOException {
|
||||
byte[] valueBytes = new byte[8];
|
||||
if (in.read(valueBytes, 0, 8) != 8)
|
||||
throw new EOFException();
|
||||
value = Coin.valueOf(Utils.readInt64(valueBytes, 0));
|
||||
|
||||
int scriptBytesLength = ((in.read() & 0xFF) << 0) |
|
||||
((in.read() & 0xFF) << 8) |
|
||||
((in.read() & 0xFF) << 16) |
|
||||
((in.read() & 0xFF) << 24);
|
||||
scriptBytes = new byte[scriptBytesLength];
|
||||
if (in.read(scriptBytes) != scriptBytesLength)
|
||||
throw new EOFException();
|
||||
|
||||
byte[] hashBytes = new byte[32];
|
||||
if (in.read(hashBytes) != 32)
|
||||
throw new EOFException();
|
||||
hash = new Sha256Hash(hashBytes);
|
||||
|
||||
byte[] indexBytes = new byte[4];
|
||||
if (in.read(indexBytes) != 4)
|
||||
throw new EOFException();
|
||||
index = Utils.readUint32(indexBytes, 0);
|
||||
|
||||
height = ((in.read() & 0xFF) << 0) |
|
||||
((in.read() & 0xFF) << 8) |
|
||||
((in.read() & 0xFF) << 16) |
|
||||
((in.read() & 0xFF) << 24);
|
||||
|
||||
byte[] coinbaseByte = new byte[1];
|
||||
in.read(coinbaseByte);
|
||||
if (coinbaseByte[0] == 1) {
|
||||
coinbase = true;
|
||||
} else {
|
||||
coinbase = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The value which this Transaction output holds.
|
||||
* @return the value.
|
||||
*/
|
||||
public Coin getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* The backing script bytes which can be turned into a Script object.
|
||||
* @return the scriptBytes.
|
||||
*/
|
||||
public byte[] getScriptBytes() {
|
||||
return scriptBytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* The hash of the transaction which holds this output.
|
||||
* @return the hash.
|
||||
*/
|
||||
public Sha256Hash getHash() {
|
||||
return hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* The index of this output in the transaction which holds it.
|
||||
* @return the index.
|
||||
*/
|
||||
public long getIndex() {
|
||||
return index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the height of the block that created this output.
|
||||
* @return The height.
|
||||
*/
|
||||
public int getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the flag of whether this was created by a coinbase tx.
|
||||
* @return The coinbase flag.
|
||||
*/
|
||||
public boolean isCoinbase() {
|
||||
return coinbase;
|
||||
}
|
||||
|
||||
/**
|
||||
* The address of this output.
|
||||
* @return The address.
|
||||
*/
|
||||
public String getAddress() {
|
||||
return address;
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of the address.
|
||||
* @return The address type.
|
||||
*/
|
||||
public int getAddressType() {
|
||||
return addressType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("Stored TxOut of %s (%s:%d)", value.toFriendlyString(), hash.toString(), index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return hash.hashCode() + (int)index;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
UTXO other = (UTXO) o;
|
||||
return getHash().equals(other.getHash()) &&
|
||||
getIndex() == other.getIndex();
|
||||
}
|
||||
|
||||
public void serializeToStream(OutputStream bos) throws IOException {
|
||||
Utils.uint64ToByteStreamLE(BigInteger.valueOf(value.value), bos);
|
||||
|
||||
bos.write(0xFF & scriptBytes.length >> 0);
|
||||
bos.write(0xFF & scriptBytes.length >> 8);
|
||||
bos.write(0xFF & (scriptBytes.length >> 16));
|
||||
bos.write(0xFF & (scriptBytes.length >> 24));
|
||||
bos.write(scriptBytes);
|
||||
|
||||
bos.write(hash.getBytes());
|
||||
Utils.uint32ToByteStreamLE(index, bos);
|
||||
|
||||
bos.write(0xFF & (height >> 0));
|
||||
bos.write(0xFF & (height >> 8));
|
||||
bos.write(0xFF & (height >> 16));
|
||||
bos.write(0xFF & (height >> 24));
|
||||
|
||||
byte[] coinbaseByte = new byte[1];
|
||||
if(coinbase) {
|
||||
coinbaseByte[0] = 1;
|
||||
} else {
|
||||
coinbaseByte[0] = 0;
|
||||
}
|
||||
bos.write(coinbaseByte);
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Copyright 2014 Kalpesh Parmar.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.dogecoin.dogecoinj.core;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A UTXOProvider encapsulates functionality for returning unspent transaction outputs,
|
||||
* for use by the wallet or other code that crafts spends.
|
||||
*
|
||||
* <p>A {@link com.dogecoin.dogecoinj.store.FullPrunedBlockStore} is an internal implementation within bitcoinj.</p>
|
||||
*/
|
||||
public interface UTXOProvider {
|
||||
|
||||
/**
|
||||
* // TODO currently the access to outputs is by address. Change to ECKey
|
||||
* Get the list of {@link UTXO}'s for a given address.
|
||||
* @param addresses List of address.
|
||||
* @return The list of transaction outputs.
|
||||
* @throws UTXOProvider If there is an error.
|
||||
*/
|
||||
List<UTXO> getOpenTransactionOutputs(List<Address> addresses) throws UTXOProviderException;
|
||||
|
||||
/**
|
||||
* Get the height of the chain head.
|
||||
* @return The chain head height.
|
||||
* @throws UTXOProvider If there is an error.
|
||||
*/
|
||||
int getChainHeadHeight() throws UTXOProviderException;
|
||||
|
||||
/**
|
||||
* The {@link NetworkParameters} of this provider.
|
||||
* @return The network parameters.
|
||||
*/
|
||||
NetworkParameters getParams();
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Copyright 2014 Kalpesh Parmar.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.dogecoin.dogecoinj.core;
|
||||
|
||||
public class UTXOProviderException extends Exception {
|
||||
public UTXOProviderException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public UTXOProviderException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public UTXOProviderException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public UTXOProviderException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
@ -36,6 +36,8 @@ import java.math.BigInteger;
|
||||
import java.net.URL;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
@ -453,6 +455,28 @@ public class Utils {
|
||||
return currentTimeMillis() / 1000;
|
||||
}
|
||||
|
||||
private static final TimeZone UTC = TimeZone.getTimeZone("UTC");
|
||||
|
||||
/**
|
||||
* Formats a given date+time value to an ISO 8601 string.
|
||||
* @param dateTime value to format, as a Date
|
||||
*/
|
||||
public static String dateTimeFormat(Date dateTime) {
|
||||
DateFormat iso8601 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US);
|
||||
iso8601.setTimeZone(UTC);
|
||||
return iso8601.format(dateTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a given date+time value to an ISO 8601 string.
|
||||
* @param dateTime value to format, unix time (ms)
|
||||
*/
|
||||
public static String dateTimeFormat(long dateTime) {
|
||||
DateFormat iso8601 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US);
|
||||
iso8601.setTimeZone(UTC);
|
||||
return iso8601.format(dateTime);
|
||||
}
|
||||
|
||||
public static byte[] copyOf(byte[] in, int length) {
|
||||
byte[] out = new byte[length];
|
||||
System.arraycopy(in, 0, out, 0, Math.min(length, in.length));
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -116,6 +116,10 @@ public interface WalletEventListener extends KeyChainEventListener {
|
||||
*/
|
||||
void onWalletChanged(Wallet wallet);
|
||||
|
||||
/** Called whenever a new watched script is added to the wallet. */
|
||||
void onScriptsAdded(Wallet wallet, List<Script> scripts);
|
||||
/**
|
||||
* Called whenever a new watched script is added to the wallet.
|
||||
*
|
||||
* @param isAddingScripts will be true if added scripts, false if removed scripts.
|
||||
*/
|
||||
void onScriptsChanged(Wallet wallet, List<Script> scripts, boolean isAddingScripts);
|
||||
}
|
||||
|
@ -46,15 +46,10 @@ public class DeterministicKey extends ECKey {
|
||||
/** 32 bytes */
|
||||
private final byte[] chainCode;
|
||||
|
||||
/** The 4 byte header that serializes in base58 to "xpub" */
|
||||
public static final int HEADER_PUB = 0x0488B21E;
|
||||
/** The 4 byte header that serializes in base58 to "xprv" */
|
||||
public static final int HEADER_PRIV = 0x0488ADE4;
|
||||
|
||||
/** Constructs a key from its components. This is not normally something you should use. */
|
||||
public DeterministicKey(ImmutableList<ChildNumber> childNumberPath,
|
||||
byte[] chainCode,
|
||||
ECPoint publicAsPoint,
|
||||
LazyECPoint publicAsPoint,
|
||||
@Nullable BigInteger priv,
|
||||
@Nullable DeterministicKey parent) {
|
||||
super(priv, compressPoint(checkNotNull(publicAsPoint)));
|
||||
@ -64,6 +59,14 @@ public class DeterministicKey extends ECKey {
|
||||
this.chainCode = Arrays.copyOf(chainCode, chainCode.length);
|
||||
}
|
||||
|
||||
public DeterministicKey(ImmutableList<ChildNumber> childNumberPath,
|
||||
byte[] chainCode,
|
||||
ECPoint publicAsPoint,
|
||||
@Nullable BigInteger priv,
|
||||
@Nullable DeterministicKey parent) {
|
||||
this(childNumberPath, chainCode, new LazyECPoint(publicAsPoint), priv, parent);
|
||||
}
|
||||
|
||||
/** Constructs a key from its components. This is not normally something you should use. */
|
||||
public DeterministicKey(ImmutableList<ChildNumber> childNumberPath,
|
||||
byte[] chainCode,
|
||||
@ -79,7 +82,10 @@ public class DeterministicKey extends ECKey {
|
||||
/** Constructs a key from its components. This is not normally something you should use. */
|
||||
public DeterministicKey(ImmutableList<ChildNumber> childNumberPath,
|
||||
byte[] chainCode,
|
||||
KeyCrypter crypter, ECPoint pub, EncryptedData priv, @Nullable DeterministicKey parent) {
|
||||
KeyCrypter crypter,
|
||||
LazyECPoint pub,
|
||||
EncryptedData priv,
|
||||
@Nullable DeterministicKey parent) {
|
||||
this(childNumberPath, chainCode, pub, null, parent);
|
||||
this.encryptedPrivateKey = checkNotNull(priv);
|
||||
this.keyCrypter = checkNotNull(crypter);
|
||||
@ -87,7 +93,7 @@ public class DeterministicKey extends ECKey {
|
||||
|
||||
/** Clones the key */
|
||||
public DeterministicKey(DeterministicKey keyToClone, DeterministicKey newParent) {
|
||||
super(keyToClone.priv, keyToClone.pub);
|
||||
super(keyToClone.priv, keyToClone.pub.get());
|
||||
this.parent = newParent;
|
||||
this.childNumberPath = keyToClone.childNumberPath;
|
||||
this.chainCode = keyToClone.chainCode;
|
||||
@ -160,8 +166,7 @@ public class DeterministicKey extends ECKey {
|
||||
*/
|
||||
public DeterministicKey getPubOnly() {
|
||||
if (isPubKeyOnly()) return this;
|
||||
//final DeterministicKey parentPub = getParent() == null ? null : getParent().getPubOnly();
|
||||
return new DeterministicKey(getPath(), getChainCode(), getPubKeyPoint(), null, parent);
|
||||
return new DeterministicKey(getPath(), getChainCode(), pub, null, parent);
|
||||
}
|
||||
|
||||
|
||||
@ -298,7 +303,9 @@ public class DeterministicKey extends ECKey {
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives a child at the given index (note: not the "i" value).
|
||||
* Derives a child at the given index using hardened derivation. Note: <code>index</code> is
|
||||
* not the "i" value. If you want the softened derivation, then use instead
|
||||
* <code>HDKeyDerivation.deriveChildKey(this, new ChildNumber(child, false))</code>.
|
||||
*/
|
||||
public DeterministicKey derive(int child) {
|
||||
return HDKeyDerivation.deriveChildKey(this, new ChildNumber(child, true));
|
||||
@ -316,17 +323,17 @@ public class DeterministicKey extends ECKey {
|
||||
return key;
|
||||
}
|
||||
|
||||
public byte[] serializePublic() {
|
||||
return serialize(true);
|
||||
public byte[] serializePublic(NetworkParameters params) {
|
||||
return serialize(params, true);
|
||||
}
|
||||
|
||||
public byte[] serializePrivate() {
|
||||
return serialize(false);
|
||||
public byte[] serializePrivate(NetworkParameters params) {
|
||||
return serialize(params, false);
|
||||
}
|
||||
|
||||
private byte[] serialize(boolean pub) {
|
||||
private byte[] serialize(NetworkParameters params, boolean pub) {
|
||||
ByteBuffer ser = ByteBuffer.allocate(78);
|
||||
ser.putInt(pub ? HEADER_PUB : HEADER_PRIV);
|
||||
ser.putInt(pub ? params.getBip32HeaderPub() : params.getBip32HeaderPriv());
|
||||
ser.put((byte) getDepth());
|
||||
if (parent == null) {
|
||||
ser.putInt(0);
|
||||
@ -340,12 +347,12 @@ public class DeterministicKey extends ECKey {
|
||||
return ser.array();
|
||||
}
|
||||
|
||||
public String serializePubB58() {
|
||||
return toBase58(serialize(true));
|
||||
public String serializePubB58(NetworkParameters params) {
|
||||
return toBase58(serialize(params, true));
|
||||
}
|
||||
|
||||
public String serializePrivB58() {
|
||||
return toBase58(serialize(false));
|
||||
public String serializePrivB58(NetworkParameters params) {
|
||||
return toBase58(serialize(params, false));
|
||||
}
|
||||
|
||||
static String toBase58(byte[] ser) {
|
||||
@ -353,17 +360,17 @@ public class DeterministicKey extends ECKey {
|
||||
}
|
||||
|
||||
/** Deserialize a base-58-encoded HD Key with no parent */
|
||||
public static DeterministicKey deserializeB58(String base58) {
|
||||
return deserializeB58(null, base58);
|
||||
public static DeterministicKey deserializeB58(String base58, NetworkParameters params) {
|
||||
return deserializeB58(null, base58, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize a base-58-encoded HD Key.
|
||||
* @param parent The parent node in the given key's deterministic hierarchy.
|
||||
*/
|
||||
public static DeterministicKey deserializeB58(@Nullable DeterministicKey parent, String base58) {
|
||||
public static DeterministicKey deserializeB58(@Nullable DeterministicKey parent, String base58, NetworkParameters params) {
|
||||
try {
|
||||
return deserialize(parent, Base58.decodeChecked(base58));
|
||||
return deserialize(params, Base58.decodeChecked(base58), parent);
|
||||
} catch (AddressFormatException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
@ -372,20 +379,20 @@ public class DeterministicKey extends ECKey {
|
||||
/**
|
||||
* Deserialize an HD Key with no parent
|
||||
*/
|
||||
public static DeterministicKey deserialize(byte[] serializedKey) {
|
||||
return deserialize(null, serializedKey);
|
||||
public static DeterministicKey deserialize(NetworkParameters params, byte[] serializedKey) {
|
||||
return deserialize(params, serializedKey, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize an HD Key.
|
||||
* @param parent The parent node in the given key's deterministic hierarchy.
|
||||
*/
|
||||
public static DeterministicKey deserialize(@Nullable DeterministicKey parent, byte[] serializedKey) {
|
||||
* @param parent The parent node in the given key's deterministic hierarchy.
|
||||
*/
|
||||
public static DeterministicKey deserialize(NetworkParameters params, byte[] serializedKey, @Nullable DeterministicKey parent) {
|
||||
ByteBuffer buffer = ByteBuffer.wrap(serializedKey);
|
||||
int header = buffer.getInt();
|
||||
if (header != HEADER_PRIV && header != HEADER_PUB)
|
||||
if (header != params.getBip32HeaderPriv() && header != params.getBip32HeaderPub())
|
||||
throw new IllegalArgumentException("Unknown header bytes: " + toBase58(serializedKey).substring(0, 4));
|
||||
boolean pub = header == HEADER_PUB;
|
||||
boolean pub = header == params.getBip32HeaderPub();
|
||||
byte depth = buffer.get();
|
||||
byte[] parentFingerprint = new byte[4];
|
||||
buffer.get(parentFingerprint);
|
||||
@ -416,7 +423,7 @@ public class DeterministicKey extends ECKey {
|
||||
checkArgument(!buffer.hasRemaining(), "Found unexpected data in key");
|
||||
if (pub) {
|
||||
ECPoint point = ECKey.CURVE.getCurve().decodePoint(data);
|
||||
return new DeterministicKey(path, chainCode, point, null, parent);
|
||||
return new DeterministicKey(path, chainCode, new LazyECPoint(point), null, parent);
|
||||
} else {
|
||||
return new DeterministicKey(path, chainCode, new BigInteger(1, data), parent);
|
||||
}
|
||||
@ -434,6 +441,18 @@ public class DeterministicKey extends ECKey {
|
||||
return super.getCreationTimeSeconds();
|
||||
}
|
||||
|
||||
/**
|
||||
* The creation time of a deterministic key is equal to that of its parent, unless this key is the root of a tree.
|
||||
* Thus, setting the creation time on a leaf is forbidden.
|
||||
*/
|
||||
@Override
|
||||
public void setCreationTimeSeconds(long newCreationTimeSeconds) {
|
||||
if (parent != null)
|
||||
throw new IllegalStateException("Creation time can only be set on root keys.");
|
||||
else
|
||||
super.setCreationTimeSeconds(newCreationTimeSeconds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies equality of all fields but NOT the parent pointer (thus the same key derived in two separate heirarchy
|
||||
* objects will equal each other.
|
||||
@ -466,6 +485,8 @@ public class DeterministicKey extends ECKey {
|
||||
helper.add("path", getPathAsString());
|
||||
if (creationTimeSeconds > 0)
|
||||
helper.add("creationTimeSeconds", creationTimeSeconds);
|
||||
helper.add("isEncrypted", isEncrypted());
|
||||
helper.add("isPubKeyOnly", isPubKeyOnly());
|
||||
return helper.toString();
|
||||
}
|
||||
|
||||
|
@ -35,8 +35,16 @@ import static com.google.common.base.Preconditions.checkState;
|
||||
* deterministic wallet child key generation algorithm.
|
||||
*/
|
||||
public final class HDKeyDerivation {
|
||||
static {
|
||||
// Init proper random number generator, as some old Android installations have bugs that make it unsecure.
|
||||
if (Utils.isAndroidRuntime())
|
||||
new LinuxSecureRandom();
|
||||
|
||||
RAND_INT = new BigInteger(256, new SecureRandom());
|
||||
}
|
||||
|
||||
// Some arbitrary random number. Doesn't matter what it is.
|
||||
private static final BigInteger RAND_INT = new BigInteger(256, new SecureRandom());
|
||||
private static final BigInteger RAND_INT;
|
||||
|
||||
private HDKeyDerivation() { }
|
||||
|
||||
@ -86,12 +94,16 @@ public final class HDKeyDerivation {
|
||||
}
|
||||
|
||||
public static DeterministicKey createMasterPubKeyFromBytes(byte[] pubKeyBytes, byte[] chainCode) {
|
||||
return new DeterministicKey(ImmutableList.<ChildNumber>of(), chainCode, ECKey.CURVE.getCurve().decodePoint(pubKeyBytes), null, null);
|
||||
return new DeterministicKey(ImmutableList.<ChildNumber>of(), chainCode, new LazyECPoint(ECKey.CURVE.getCurve(), pubKeyBytes), null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives a key given the "extended" child number, ie. with the 0x80000000 bit specifying whether to use hardened
|
||||
* derivation or not.
|
||||
* Derives a key given the "extended" child number, ie. the 0x80000000 bit of the value that you
|
||||
* pass for <code>childNumber</code> will determine whether to use hardened derivation or not.
|
||||
* Consider whether your code would benefit from the clarity of the equivalent, but explicit, form
|
||||
* of this method that takes a <code>ChildNumber</code> rather than an <code>int</code>, for example:
|
||||
* <code>deriveChildKey(parent, new ChildNumber(childNumber, true))</code>
|
||||
* where the value of the hardened bit of <code>childNumber</code> is zero.
|
||||
*/
|
||||
public static DeterministicKey deriveChildKey(DeterministicKey parent, int childNumber) {
|
||||
return deriveChildKey(parent, new ChildNumber(childNumber));
|
||||
@ -126,7 +138,7 @@ public final class HDKeyDerivation {
|
||||
return new DeterministicKey(
|
||||
HDUtils.append(parent.getPath(), childNumber),
|
||||
rawKey.chainCode,
|
||||
ECKey.CURVE.getCurve().decodePoint(rawKey.keyBytes), // c'tor will compress
|
||||
new LazyECPoint(ECKey.CURVE.getCurve(), rawKey.keyBytes),
|
||||
null,
|
||||
parent);
|
||||
} else {
|
||||
|
@ -19,6 +19,7 @@ package com.dogecoin.dogecoinj.crypto;
|
||||
import com.google.common.base.Objects;
|
||||
import com.google.protobuf.ByteString;
|
||||
import com.lambdaworks.crypto.SCrypt;
|
||||
import com.dogecoin.dogecoinj.core.Utils;
|
||||
import com.dogecoin.dogecoinj.wallet.Protos;
|
||||
import com.dogecoin.dogecoinj.wallet.Protos.ScryptParameters;
|
||||
import com.dogecoin.dogecoinj.wallet.Protos.Wallet.EncryptionType;
|
||||
@ -52,6 +53,7 @@ import static com.google.common.base.Preconditions.checkNotNull;
|
||||
* the AES symmetric cipher. Eight bytes of salt is used to prevent dictionary attacks.</p>
|
||||
*/
|
||||
public class KeyCrypterScrypt implements KeyCrypter, Serializable {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(KeyCrypterScrypt.class);
|
||||
private static final long serialVersionUID = 949662512049152670L;
|
||||
|
||||
@ -71,7 +73,15 @@ public class KeyCrypterScrypt implements KeyCrypter, Serializable {
|
||||
*/
|
||||
public static final int SALT_LENGTH = 8;
|
||||
|
||||
private static final transient SecureRandom secureRandom = new SecureRandom();
|
||||
static {
|
||||
// Init proper random number generator, as some old Android installations have bugs that make it unsecure.
|
||||
if (Utils.isAndroidRuntime())
|
||||
new LinuxSecureRandom();
|
||||
|
||||
secureRandom = new SecureRandom();
|
||||
}
|
||||
|
||||
private static final transient SecureRandom secureRandom;
|
||||
|
||||
private static byte[] randomSalt() {
|
||||
byte[] salt = new byte[SALT_LENGTH];
|
||||
|
@ -0,0 +1,187 @@
|
||||
package com.dogecoin.dogecoinj.crypto;
|
||||
|
||||
import org.spongycastle.math.ec.ECCurve;
|
||||
import org.spongycastle.math.ec.ECFieldElement;
|
||||
import org.spongycastle.math.ec.ECPoint;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Arrays;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
/**
|
||||
* A wrapper around ECPoint that delays decoding of the point for as long as possible. This is useful because point
|
||||
* encode/decode in Bouncy Castle is quite slow especially on Dalvik, as it often involves decompression/recompression.
|
||||
*/
|
||||
public class LazyECPoint {
|
||||
// If curve is set, bits is also set. If curve is unset, point is set and bits is unset. Point can be set along
|
||||
// with curve and bits when the cached form has been accessed and thus must have been converted.
|
||||
|
||||
private final ECCurve curve;
|
||||
private final byte[] bits;
|
||||
|
||||
// This field is effectively final - once set it won't change again. However it can be set after
|
||||
// construction.
|
||||
@Nullable
|
||||
private ECPoint point;
|
||||
|
||||
public LazyECPoint(ECCurve curve, byte[] bits) {
|
||||
this.curve = curve;
|
||||
this.bits = bits;
|
||||
}
|
||||
|
||||
public LazyECPoint(ECPoint point) {
|
||||
this.point = checkNotNull(point);
|
||||
this.curve = null;
|
||||
this.bits = null;
|
||||
}
|
||||
|
||||
public ECPoint get() {
|
||||
if (point == null)
|
||||
point = curve.decodePoint(bits);
|
||||
return point;
|
||||
}
|
||||
|
||||
// Delegated methods.
|
||||
|
||||
public ECPoint getDetachedPoint() {
|
||||
return get().getDetachedPoint();
|
||||
}
|
||||
|
||||
public byte[] getEncoded() {
|
||||
if (bits != null)
|
||||
return Arrays.copyOf(bits, bits.length);
|
||||
else
|
||||
return get().getEncoded();
|
||||
}
|
||||
|
||||
public boolean isInfinity() {
|
||||
return get().isInfinity();
|
||||
}
|
||||
|
||||
public ECPoint timesPow2(int e) {
|
||||
return get().timesPow2(e);
|
||||
}
|
||||
|
||||
public ECFieldElement getYCoord() {
|
||||
return get().getYCoord();
|
||||
}
|
||||
|
||||
public ECFieldElement[] getZCoords() {
|
||||
return get().getZCoords();
|
||||
}
|
||||
|
||||
public boolean isNormalized() {
|
||||
return get().isNormalized();
|
||||
}
|
||||
|
||||
public boolean isCompressed() {
|
||||
if (bits != null)
|
||||
return bits[0] == 2 || bits[0] == 3;
|
||||
else
|
||||
return get().isCompressed();
|
||||
}
|
||||
|
||||
public ECPoint multiply(BigInteger k) {
|
||||
return get().multiply(k);
|
||||
}
|
||||
|
||||
public ECPoint subtract(ECPoint b) {
|
||||
return get().subtract(b);
|
||||
}
|
||||
|
||||
public boolean isValid() {
|
||||
return get().isValid();
|
||||
}
|
||||
|
||||
public ECPoint scaleY(ECFieldElement scale) {
|
||||
return get().scaleY(scale);
|
||||
}
|
||||
|
||||
public ECFieldElement getXCoord() {
|
||||
return get().getXCoord();
|
||||
}
|
||||
|
||||
public ECPoint scaleX(ECFieldElement scale) {
|
||||
return get().scaleX(scale);
|
||||
}
|
||||
|
||||
public boolean equals(ECPoint other) {
|
||||
return get().equals(other);
|
||||
}
|
||||
|
||||
public ECPoint negate() {
|
||||
return get().negate();
|
||||
}
|
||||
|
||||
public ECPoint threeTimes() {
|
||||
return get().threeTimes();
|
||||
}
|
||||
|
||||
public ECFieldElement getZCoord(int index) {
|
||||
return get().getZCoord(index);
|
||||
}
|
||||
|
||||
public byte[] getEncoded(boolean compressed) {
|
||||
if (compressed == isCompressed() && bits != null)
|
||||
return Arrays.copyOf(bits, bits.length);
|
||||
else
|
||||
return get().getEncoded(compressed);
|
||||
}
|
||||
|
||||
public ECPoint add(ECPoint b) {
|
||||
return get().add(b);
|
||||
}
|
||||
|
||||
public ECPoint twicePlus(ECPoint b) {
|
||||
return get().twicePlus(b);
|
||||
}
|
||||
|
||||
public ECCurve getCurve() {
|
||||
return get().getCurve();
|
||||
}
|
||||
|
||||
public ECPoint normalize() {
|
||||
return get().normalize();
|
||||
}
|
||||
|
||||
public ECFieldElement getY() {
|
||||
return get().getY();
|
||||
}
|
||||
|
||||
public ECPoint twice() {
|
||||
return get().twice();
|
||||
}
|
||||
|
||||
public ECFieldElement getAffineYCoord() {
|
||||
return get().getAffineYCoord();
|
||||
}
|
||||
|
||||
public ECFieldElement getAffineXCoord() {
|
||||
return get().getAffineXCoord();
|
||||
}
|
||||
|
||||
public ECFieldElement getX() {
|
||||
return get().getX();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
LazyECPoint point1 = (LazyECPoint) o;
|
||||
if (bits != null && point1.bits != null)
|
||||
return Arrays.equals(bits, point1.bits);
|
||||
else
|
||||
return get().equals(point1.get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
if (bits != null)
|
||||
return Arrays.hashCode(bits);
|
||||
else
|
||||
return get().hashCode();
|
||||
}
|
||||
}
|
@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Copyright 2013 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.dogecoin.dogecoinj.crypto;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.security.Provider;
|
||||
import java.security.SecureRandomSpi;
|
||||
import java.security.Security;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* A SecureRandom implementation that is able to override the standard JVM provided implementation, and which simply
|
||||
* serves random numbers by reading /dev/urandom. That is, it delegates to the kernel on UNIX systems and is unusable on
|
||||
* other platforms. Attempts to manually set the seed are ignored. There is no difference between seed bytes and
|
||||
* non-seed bytes, they are all from the same source.
|
||||
*/
|
||||
public class LinuxSecureRandom extends SecureRandomSpi {
|
||||
private static final FileInputStream urandom;
|
||||
|
||||
private static class LinuxSecureRandomProvider extends Provider {
|
||||
public LinuxSecureRandomProvider() {
|
||||
super("LinuxSecureRandom", 1.0, "A Linux specific random number provider that uses /dev/urandom");
|
||||
put("SecureRandom.LinuxSecureRandom", LinuxSecureRandom.class.getName());
|
||||
}
|
||||
}
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(LinuxSecureRandom.class);
|
||||
|
||||
static {
|
||||
try {
|
||||
File file = new File("/dev/urandom");
|
||||
if (file.exists()) {
|
||||
// This stream is deliberately leaked.
|
||||
urandom = new FileInputStream(file);
|
||||
// Now override the default SecureRandom implementation with this one.
|
||||
int position = Security.insertProviderAt(new LinuxSecureRandomProvider(), 1);
|
||||
|
||||
if (position != -1)
|
||||
log.info("Secure randomness will be read from {} only.", file);
|
||||
else
|
||||
log.info("Randomness is already secure.");
|
||||
} else {
|
||||
urandom = null;
|
||||
|
||||
log.info("Does not exist: {}", file);
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
// Should never happen.
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private final DataInputStream dis;
|
||||
|
||||
public LinuxSecureRandom() {
|
||||
// DataInputStream is not thread safe, so each random object has its own.
|
||||
dis = new DataInputStream(urandom);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void engineSetSeed(byte[] bytes) {
|
||||
// Ignore.
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void engineNextBytes(byte[] bytes) {
|
||||
try {
|
||||
dis.readFully(bytes); // This will block until all the bytes can be read.
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e); // Fatal error. Do not attempt to recover from this.
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] engineGenerateSeed(int i) {
|
||||
byte[] bits = new byte[i];
|
||||
engineNextBytes(bits);
|
||||
return bits;
|
||||
}
|
||||
}
|
@ -16,6 +16,7 @@
|
||||
|
||||
package com.dogecoin.dogecoinj.jni;
|
||||
|
||||
import com.dogecoin.dogecoinj.core.Sha256Hash;
|
||||
import com.dogecoin.dogecoinj.core.Transaction;
|
||||
import com.dogecoin.dogecoinj.core.TransactionConfidence;
|
||||
|
||||
@ -28,5 +29,5 @@ public class NativeTransactionConfidenceListener implements TransactionConfidenc
|
||||
public long ptr;
|
||||
|
||||
@Override
|
||||
public native void onConfidenceChanged(Transaction tx, ChangeReason reason);
|
||||
public native void onConfidenceChanged(TransactionConfidence confidence, ChangeReason reason);
|
||||
}
|
||||
|
@ -52,5 +52,5 @@ public class NativeWalletEventListener implements WalletEventListener {
|
||||
public native void onKeysAdded(List<ECKey> keys);
|
||||
|
||||
@Override
|
||||
public native void onScriptsAdded(Wallet wallet, List<Script> scripts);
|
||||
public native void onScriptsChanged(Wallet wallet, List<Script> scripts, boolean isAddingScripts);
|
||||
}
|
||||
|
@ -18,12 +18,11 @@
|
||||
package com.dogecoin.dogecoinj.kits;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.util.concurrent.AbstractIdleService;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
import com.google.common.util.concurrent.Service;
|
||||
import com.google.common.util.concurrent.*;
|
||||
import com.subgraph.orchid.TorClient;
|
||||
import com.dogecoin.dogecoinj.core.*;
|
||||
import com.dogecoin.dogecoinj.net.discovery.DnsDiscovery;
|
||||
import com.dogecoin.dogecoinj.net.discovery.PeerDiscovery;
|
||||
import com.dogecoin.dogecoinj.protocols.channels.StoredPaymentChannelClientStates;
|
||||
import com.dogecoin.dogecoinj.protocols.channels.StoredPaymentChannelServerStates;
|
||||
import com.dogecoin.dogecoinj.store.BlockStoreException;
|
||||
@ -92,6 +91,7 @@ public class WalletAppKit extends AbstractIdleService {
|
||||
protected String userAgent, version;
|
||||
protected WalletProtobufSerializer.WalletFactory walletFactory;
|
||||
@Nullable protected DeterministicSeed restoreFromSeed;
|
||||
@Nullable protected PeerDiscovery discovery;
|
||||
|
||||
public WalletAppKit(NetworkParameters params, File directory, String filePrefix) {
|
||||
this.params = checkNotNull(params);
|
||||
@ -131,7 +131,8 @@ public class WalletAppKit extends AbstractIdleService {
|
||||
|
||||
/**
|
||||
* If you want to learn about the sync process, you can provide a listener here. For instance, a
|
||||
* {@link DownloadListener} is a good choice.
|
||||
* {@link com.dogecoin.dogecoinj.core.DownloadProgressTracker} is a good choice. This has no effect unless setBlockingStartup(false) has been called
|
||||
* too, due to some missing implementation code.
|
||||
*/
|
||||
public WalletAppKit setDownloadListener(PeerEventListener listener) {
|
||||
this.downloadListener = listener;
|
||||
@ -199,6 +200,14 @@ public class WalletAppKit extends AbstractIdleService {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the peer discovery class to use. If none is provided then DNS is used, which is a reasonable default.
|
||||
*/
|
||||
public WalletAppKit setDiscovery(@Nullable PeerDiscovery discovery) {
|
||||
this.discovery = discovery;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Override this to return wallet extensions if any are necessary.</p>
|
||||
*
|
||||
@ -256,22 +265,30 @@ public class WalletAppKit extends AbstractIdleService {
|
||||
|
||||
// Initiate Bitcoin network objects (block store, blockchain and peer group)
|
||||
vStore = new SPVBlockStore(params, chainFile);
|
||||
if ((!chainFileExists || restoreFromSeed != null) && checkpoints != null) {
|
||||
// Initialize the chain file with a checkpoint to speed up first-run sync.
|
||||
long time;
|
||||
if (restoreFromSeed != null) {
|
||||
time = restoreFromSeed.getCreationTimeSeconds();
|
||||
if (chainFileExists) {
|
||||
log.info("Deleting the chain file in preparation from restore.");
|
||||
vStore.close();
|
||||
if (!chainFile.delete())
|
||||
throw new Exception("Failed to delete chain file in preparation for restore.");
|
||||
vStore = new SPVBlockStore(params, chainFile);
|
||||
if (!chainFileExists || restoreFromSeed != null) {
|
||||
if (checkpoints != null) {
|
||||
// Initialize the chain file with a checkpoint to speed up first-run sync.
|
||||
long time;
|
||||
if (restoreFromSeed != null) {
|
||||
time = restoreFromSeed.getCreationTimeSeconds();
|
||||
if (chainFileExists) {
|
||||
log.info("Deleting the chain file in preparation from restore.");
|
||||
vStore.close();
|
||||
if (!chainFile.delete())
|
||||
throw new Exception("Failed to delete chain file in preparation for restore.");
|
||||
vStore = new SPVBlockStore(params, chainFile);
|
||||
}
|
||||
} else {
|
||||
time = vWallet.getEarliestKeyCreationTime();
|
||||
}
|
||||
} else {
|
||||
time = vWallet.getEarliestKeyCreationTime();
|
||||
CheckpointManager.checkpoint(params, checkpoints, vStore, time);
|
||||
} else if (chainFileExists) {
|
||||
log.info("Deleting the chain file in preparation from restore.");
|
||||
vStore.close();
|
||||
if (!chainFile.delete())
|
||||
throw new Exception("Failed to delete chain file in preparation for restore.");
|
||||
vStore = new SPVBlockStore(params, chainFile);
|
||||
}
|
||||
CheckpointManager.checkpoint(params, checkpoints, vStore, time);
|
||||
}
|
||||
vChain = new BlockChain(params, vStore);
|
||||
vPeerGroup = createPeerGroup();
|
||||
@ -285,38 +302,37 @@ public class WalletAppKit extends AbstractIdleService {
|
||||
vPeerGroup.setMaxConnections(peerAddresses.length);
|
||||
peerAddresses = null;
|
||||
} else {
|
||||
vPeerGroup.addPeerDiscovery(new DnsDiscovery(params));
|
||||
vPeerGroup.addPeerDiscovery(discovery != null ? discovery : new DnsDiscovery(params));
|
||||
}
|
||||
vChain.addWallet(vWallet);
|
||||
vPeerGroup.addWallet(vWallet);
|
||||
onSetupCompleted();
|
||||
|
||||
if (blockingStartup) {
|
||||
vPeerGroup.startAsync();
|
||||
vPeerGroup.awaitRunning();
|
||||
vPeerGroup.start();
|
||||
// Make sure we shut down cleanly.
|
||||
installShutdownHook();
|
||||
completeExtensionInitiations(vPeerGroup);
|
||||
|
||||
// TODO: Be able to use the provided download listener when doing a blocking startup.
|
||||
final DownloadListener listener = new DownloadListener();
|
||||
final DownloadProgressTracker listener = new DownloadProgressTracker();
|
||||
vPeerGroup.startBlockChainDownload(listener);
|
||||
listener.await();
|
||||
} else {
|
||||
vPeerGroup.startAsync();
|
||||
vPeerGroup.addListener(new Service.Listener() {
|
||||
Futures.addCallback(vPeerGroup.startAsync(), new FutureCallback() {
|
||||
@Override
|
||||
public void running() {
|
||||
public void onSuccess(@Nullable Object result) {
|
||||
completeExtensionInitiations(vPeerGroup);
|
||||
final PeerEventListener l = downloadListener == null ? new DownloadListener() : downloadListener;
|
||||
final PeerEventListener l = downloadListener == null ? new DownloadProgressTracker() : downloadListener;
|
||||
vPeerGroup.startBlockChainDownload(l);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void failed(State from, Throwable failure) {
|
||||
throw new RuntimeException(failure);
|
||||
public void onFailure(Throwable t) {
|
||||
throw new RuntimeException(t);
|
||||
|
||||
}
|
||||
}, MoreExecutors.sameThreadExecutor());
|
||||
});
|
||||
}
|
||||
} catch (BlockStoreException e) {
|
||||
throw new IOException(e);
|
||||
@ -440,8 +456,7 @@ public class WalletAppKit extends AbstractIdleService {
|
||||
protected void shutDown() throws Exception {
|
||||
// Runs in a separate thread.
|
||||
try {
|
||||
vPeerGroup.stopAsync();
|
||||
vPeerGroup.awaitTerminated();
|
||||
vPeerGroup.stop();
|
||||
vWallet.saveToFile(vWalletFile);
|
||||
vStore.close();
|
||||
|
||||
|
@ -16,6 +16,8 @@
|
||||
|
||||
package com.dogecoin.dogecoinj.net;
|
||||
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.common.util.concurrent.SettableFuture;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
@ -47,6 +49,7 @@ public class BlockingClient implements MessageWriteTarget {
|
||||
private final ByteBuffer dbuf;
|
||||
private Socket socket;
|
||||
private volatile boolean vCloseRequested = false;
|
||||
private SettableFuture<SocketAddress> connectFuture;
|
||||
|
||||
/**
|
||||
* <p>Creates a new client to the given server address using the given {@link StreamParser} to decode the data.
|
||||
@ -62,6 +65,7 @@ public class BlockingClient implements MessageWriteTarget {
|
||||
*/
|
||||
public BlockingClient(final SocketAddress serverAddress, final StreamParser parser,
|
||||
final int connectTimeoutMillis, final SocketFactory socketFactory, @Nullable final Set<BlockingClient> clientSet) throws IOException {
|
||||
connectFuture = SettableFuture.create();
|
||||
// Try to fit at least one message in the network buffer, but place an upper and lower limit on its size to make
|
||||
// sure it doesnt get too large or have to call read too often.
|
||||
dbuf = ByteBuffer.allocateDirect(Math.min(Math.max(parser.getMaxMessageSize(), BUFFER_SIZE_LOWER_BOUND), BUFFER_SIZE_UPPER_BOUND));
|
||||
@ -73,9 +77,9 @@ public class BlockingClient implements MessageWriteTarget {
|
||||
if (clientSet != null)
|
||||
clientSet.add(BlockingClient.this);
|
||||
try {
|
||||
InetSocketAddress iServerAddress = (InetSocketAddress)serverAddress;
|
||||
socket.connect(serverAddress, connectTimeoutMillis);
|
||||
parser.connectionOpened();
|
||||
connectFuture.set(serverAddress);
|
||||
InputStream stream = socket.getInputStream();
|
||||
byte[] readBuff = new byte[dbuf.capacity()];
|
||||
|
||||
@ -97,8 +101,10 @@ public class BlockingClient implements MessageWriteTarget {
|
||||
dbuf.compact();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
if (!vCloseRequested)
|
||||
if (!vCloseRequested) {
|
||||
log.error("Error trying to open/read from connection: " + serverAddress, e);
|
||||
connectFuture.setException(e);
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
socket.close();
|
||||
@ -143,4 +149,9 @@ public class BlockingClient implements MessageWriteTarget {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns a future that completes once connection has occurred at the socket level or with an exception if failed to connect. */
|
||||
public ListenableFuture<SocketAddress> getConnectFuture() {
|
||||
return connectFuture;
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,7 @@
|
||||
package com.dogecoin.dogecoinj.net;
|
||||
|
||||
import com.google.common.util.concurrent.AbstractIdleService;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
import javax.net.SocketFactory;
|
||||
import java.io.IOException;
|
||||
@ -54,11 +55,11 @@ public class BlockingClientManager extends AbstractIdleService implements Client
|
||||
}
|
||||
|
||||
@Override
|
||||
public void openConnection(SocketAddress serverAddress, StreamParser parser) {
|
||||
if (!isRunning())
|
||||
throw new IllegalStateException();
|
||||
public ListenableFuture<SocketAddress> openConnection(SocketAddress serverAddress, StreamParser parser) {
|
||||
try {
|
||||
new BlockingClient(serverAddress, parser, connectTimeoutMillis, socketFactory, clients);
|
||||
if (!isRunning())
|
||||
throw new IllegalStateException();
|
||||
return new BlockingClient(serverAddress, parser, connectTimeoutMillis, socketFactory, clients).getConnectFuture();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e); // This should only happen if we are, eg, out of system resources
|
||||
}
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
package com.dogecoin.dogecoinj.net;
|
||||
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.common.util.concurrent.Service;
|
||||
|
||||
import java.net.SocketAddress;
|
||||
@ -31,7 +32,7 @@ public interface ClientConnectionManager extends Service {
|
||||
/**
|
||||
* Creates a new connection to the given address, with the given parser used to handle incoming data.
|
||||
*/
|
||||
void openConnection(SocketAddress serverAddress, StreamParser parser);
|
||||
ListenableFuture<SocketAddress> openConnection(SocketAddress serverAddress, StreamParser parser);
|
||||
|
||||
/** Gets the number of connected peers */
|
||||
int getConnectedClientCount();
|
||||
|
@ -151,12 +151,12 @@ class ConnectionHandler implements MessageWriteTarget {
|
||||
setWriteOps();
|
||||
} catch (IOException e) {
|
||||
lock.unlock();
|
||||
log.error("Error writing message to connection, closing connection", e);
|
||||
log.warn("Error writing message to connection, closing connection", e);
|
||||
closeConnection();
|
||||
throw e;
|
||||
} catch (CancelledKeyException e) {
|
||||
lock.unlock();
|
||||
log.error("Error writing message to connection, closing connection", e);
|
||||
log.warn("Error writing message to connection, closing connection", e);
|
||||
closeConnection();
|
||||
throw new IOException(e);
|
||||
}
|
||||
@ -225,7 +225,8 @@ class ConnectionHandler implements MessageWriteTarget {
|
||||
} catch (Exception e) {
|
||||
// This can happen eg if the channel closes while the thread is about to get killed
|
||||
// (ClosedByInterruptException), or if handler.parser.receiveBytes throws something
|
||||
log.error("Error handling SelectionKey: {}", Throwables.getRootCause(e).getMessage());
|
||||
Throwable t = Throwables.getRootCause(e);
|
||||
log.warn("Error handling SelectionKey: {}", t.getMessage() != null ? t.getMessage() : t.getClass().getName());
|
||||
handler.closeConnection();
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
package com.dogecoin.dogecoinj.net;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.dogecoin.dogecoinj.core.BloomFilter;
|
||||
import com.dogecoin.dogecoinj.core.PeerFilterProvider;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
|
||||
// This code is unit tested by the PeerGroup tests.
|
||||
|
||||
@ -14,20 +14,23 @@ import java.util.concurrent.locks.Lock;
|
||||
* {@link com.dogecoin.dogecoinj.core.BloomFilter} and earliest key time for all of them.
|
||||
* Used by the {@link com.dogecoin.dogecoinj.core.PeerGroup} class internally.</p>
|
||||
*
|
||||
* <p>Thread safety: this class tracks the element count of the last filter it calculated and so must be synchronised
|
||||
* externally or used from only one thread. It will acquire a lock on each filter in turn before performing the
|
||||
* calculation because the providers may be mutated in other threads in parallel, but global consistency is required
|
||||
* to produce a merged filter.</p>
|
||||
* <p>Thread safety: threading here can be complicated. Each filter provider is given a begin event, which may acquire
|
||||
* a lock (and is guaranteed to receive an end event). This class is mostly thread unsafe and is meant to be used from a
|
||||
* single thread only, PeerGroup ensures this by only accessing it from the dedicated PeerGroup thread. PeerGroup does
|
||||
* not hold any locks whilst this object is used, relying on the single thread to prevent multiple filters being
|
||||
* calculated in parallel, thus a filter provider can do things like make blocking calls into PeerGroup from a separate
|
||||
* thread. However the bloomFilterFPRate property IS thread safe, for convenience.</p>
|
||||
*/
|
||||
public class FilterMerger {
|
||||
// We use a constant tweak to avoid giving up privacy when we regenerate our filter with new keys
|
||||
private final long bloomFilterTweak = (long) (Math.random() * Long.MAX_VALUE);
|
||||
private double bloomFilterFPRate;
|
||||
|
||||
private volatile double vBloomFilterFPRate;
|
||||
private int lastBloomFilterElementCount;
|
||||
private BloomFilter lastFilter;
|
||||
|
||||
public FilterMerger(double bloomFilterFPRate) {
|
||||
this.bloomFilterFPRate = bloomFilterFPRate;
|
||||
this.vBloomFilterFPRate = bloomFilterFPRate;
|
||||
}
|
||||
|
||||
public static class Result {
|
||||
@ -37,16 +40,15 @@ public class FilterMerger {
|
||||
}
|
||||
|
||||
public Result calculate(ImmutableList<PeerFilterProvider> providers) {
|
||||
LinkedList<Lock> takenLocks = new LinkedList<Lock>();
|
||||
LinkedList<PeerFilterProvider> begunProviders = Lists.newLinkedList();
|
||||
try {
|
||||
// Lock all the providers so they cannot be mutated out from underneath us whilst we're in the process
|
||||
// of calculating the Bloom filter. All providers must be in a consistent, unchanging state because the
|
||||
// filter is a merged one that's large enough for all providers elements: if a provider were to get more
|
||||
// elements in the middle of the calculation, we might assert or calculate the filter wrongly.
|
||||
// All providers must be in a consistent, unchanging state because the filter is a merged one that's
|
||||
// large enough for all providers elements: if a provider were to get more elements in the middle of the
|
||||
// calculation, we might assert or calculate the filter wrongly. Most providers use a lock here but
|
||||
// snapshotting required state is also a legitimate strategy.
|
||||
for (PeerFilterProvider provider : providers) {
|
||||
Lock lock = provider.getLock();
|
||||
lock.lock();
|
||||
takenLocks.add(lock);
|
||||
provider.beginBloomFilterCalculation();
|
||||
begunProviders.add(provider);
|
||||
}
|
||||
Result result = new Result();
|
||||
result.earliestKeyTimeSecs = Long.MAX_VALUE;
|
||||
@ -66,9 +68,10 @@ public class FilterMerger {
|
||||
lastBloomFilterElementCount = elements > lastBloomFilterElementCount ? elements + 100 : lastBloomFilterElementCount;
|
||||
BloomFilter.BloomUpdate bloomFlags =
|
||||
requiresUpdateAll ? BloomFilter.BloomUpdate.UPDATE_ALL : BloomFilter.BloomUpdate.UPDATE_P2PUBKEY_ONLY;
|
||||
BloomFilter filter = new BloomFilter(lastBloomFilterElementCount, bloomFilterFPRate, bloomFilterTweak, bloomFlags);
|
||||
double fpRate = vBloomFilterFPRate;
|
||||
BloomFilter filter = new BloomFilter(lastBloomFilterElementCount, fpRate, bloomFilterTweak, bloomFlags);
|
||||
for (PeerFilterProvider p : providers)
|
||||
filter.merge(p.getBloomFilter(lastBloomFilterElementCount, bloomFilterFPRate, bloomFilterTweak));
|
||||
filter.merge(p.getBloomFilter(lastBloomFilterElementCount, fpRate, bloomFilterTweak));
|
||||
|
||||
result.changed = !filter.equals(lastFilter);
|
||||
result.filter = lastFilter = filter;
|
||||
@ -79,18 +82,18 @@ public class FilterMerger {
|
||||
result.earliestKeyTimeSecs -= 86400 * 7;
|
||||
return result;
|
||||
} finally {
|
||||
for (Lock takenLock : takenLocks) {
|
||||
takenLock.unlock();
|
||||
for (PeerFilterProvider provider : begunProviders) {
|
||||
provider.endBloomFilterCalculation();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setBloomFilterFPRate(double bloomFilterFPRate) {
|
||||
this.bloomFilterFPRate = bloomFilterFPRate;
|
||||
this.vBloomFilterFPRate = bloomFilterFPRate;
|
||||
}
|
||||
|
||||
public double getBloomFilterFPRate() {
|
||||
return bloomFilterFPRate;
|
||||
return vBloomFilterFPRate;
|
||||
}
|
||||
|
||||
public BloomFilter getLastFilter() {
|
||||
|
@ -18,9 +18,13 @@ package com.dogecoin.dogecoinj.net;
|
||||
|
||||
import com.google.common.base.Throwables;
|
||||
import com.google.common.util.concurrent.AbstractExecutionThreadService;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.common.util.concurrent.SettableFuture;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.ConnectException;
|
||||
import java.net.SocketAddress;
|
||||
import java.nio.channels.*;
|
||||
import java.nio.channels.spi.SelectorProvider;
|
||||
@ -36,12 +40,15 @@ public class NioClientManager extends AbstractExecutionThreadService implements
|
||||
|
||||
private final Selector selector;
|
||||
|
||||
// SocketChannels and StreamParsers of newly-created connections which should be registered with OP_CONNECT
|
||||
class SocketChannelAndParser {
|
||||
SocketChannel sc; StreamParser parser;
|
||||
SocketChannelAndParser(SocketChannel sc, StreamParser parser) { this.sc = sc; this.parser = parser; }
|
||||
class PendingConnect {
|
||||
SocketChannel sc;
|
||||
StreamParser parser;
|
||||
SocketAddress address;
|
||||
SettableFuture<SocketAddress> future = SettableFuture.create();
|
||||
|
||||
PendingConnect(SocketChannel sc, StreamParser parser, SocketAddress address) { this.sc = sc; this.parser = parser; this.address = address; }
|
||||
}
|
||||
final Queue<SocketChannelAndParser> newConnectionChannels = new LinkedBlockingQueue<SocketChannelAndParser>();
|
||||
final Queue<PendingConnect> newConnectionChannels = new LinkedBlockingQueue<PendingConnect>();
|
||||
|
||||
// Added to/removed from by the individual ConnectionHandler's, thus must by synchronized on its own.
|
||||
private final Set<ConnectionHandler> connectedHandlers = Collections.synchronizedSet(new HashSet<ConnectionHandler>());
|
||||
@ -51,25 +58,31 @@ public class NioClientManager extends AbstractExecutionThreadService implements
|
||||
// We could have a !isValid() key here if the connection is already closed at this point
|
||||
if (key.isValid() && key.isConnectable()) { // ie a client connection which has finished the initial connect process
|
||||
// Create a ConnectionHandler and hook everything together
|
||||
StreamParser parser = (StreamParser) key.attachment();
|
||||
PendingConnect data = (PendingConnect) key.attachment();
|
||||
StreamParser parser = data.parser;
|
||||
SocketChannel sc = (SocketChannel) key.channel();
|
||||
ConnectionHandler handler = new ConnectionHandler(parser, key, connectedHandlers);
|
||||
try {
|
||||
if (sc.finishConnect()) {
|
||||
log.info("Successfully connected to {}", sc.socket().getRemoteSocketAddress());
|
||||
key.interestOps((key.interestOps() | SelectionKey.OP_READ) & ~SelectionKey.OP_CONNECT).attach(handler);
|
||||
handler.parser.connectionOpened();
|
||||
parser.connectionOpened();
|
||||
data.future.set(data.address);
|
||||
} else {
|
||||
log.error("Failed to connect to {}", sc.socket().getRemoteSocketAddress());
|
||||
log.warn("Failed to connect to {}", sc.socket().getRemoteSocketAddress());
|
||||
handler.closeConnection(); // Failed to connect for some reason
|
||||
data.future.setException(new ConnectException("Unknown reason"));
|
||||
data.future = null;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// If e is a CancelledKeyException, there is a race to get to interestOps after finishConnect() which
|
||||
// may cause this. Otherwise it may be any arbitrary kind of connection failure.
|
||||
// Calling sc.socket().getRemoteSocketAddress() here throws an exception, so we can only log the error itself
|
||||
Throwable cause = Throwables.getRootCause(e);
|
||||
log.error("Failed to connect with exception: {}: {}", cause.getClass().getName(), cause.getMessage());
|
||||
log.warn("Failed to connect with exception: {}: {}", cause.getClass().getName(), cause.getMessage());
|
||||
handler.closeConnection();
|
||||
data.future.setException(cause);
|
||||
data.future = null;
|
||||
}
|
||||
} else // Process bytes read
|
||||
ConnectionHandler.handleKey(key);
|
||||
@ -92,13 +105,13 @@ public class NioClientManager extends AbstractExecutionThreadService implements
|
||||
try {
|
||||
Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
|
||||
while (isRunning()) {
|
||||
SocketChannelAndParser conn;
|
||||
PendingConnect conn;
|
||||
while ((conn = newConnectionChannels.poll()) != null) {
|
||||
try {
|
||||
SelectionKey key = conn.sc.register(selector, SelectionKey.OP_CONNECT);
|
||||
key.attach(conn.parser);
|
||||
key.attach(conn);
|
||||
} catch (ClosedChannelException e) {
|
||||
log.info("SocketChannel was closed before it could be registered");
|
||||
log.warn("SocketChannel was closed before it could be registered");
|
||||
}
|
||||
}
|
||||
|
||||
@ -112,14 +125,14 @@ public class NioClientManager extends AbstractExecutionThreadService implements
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error trying to open/read from connection: ", e);
|
||||
log.warn("Error trying to open/read from connection: ", e);
|
||||
} finally {
|
||||
// Go through and close everything, without letting IOExceptions get in our way
|
||||
for (SelectionKey key : selector.keys()) {
|
||||
try {
|
||||
key.channel().close();
|
||||
} catch (IOException e) {
|
||||
log.error("Error closing channel", e);
|
||||
log.warn("Error closing channel", e);
|
||||
}
|
||||
key.cancel();
|
||||
if (key.attachment() instanceof ConnectionHandler)
|
||||
@ -128,13 +141,13 @@ public class NioClientManager extends AbstractExecutionThreadService implements
|
||||
try {
|
||||
selector.close();
|
||||
} catch (IOException e) {
|
||||
log.error("Error closing client manager selector", e);
|
||||
log.warn("Error closing client manager selector", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void openConnection(SocketAddress serverAddress, StreamParser parser) {
|
||||
public ListenableFuture<SocketAddress> openConnection(SocketAddress serverAddress, StreamParser parser) {
|
||||
if (!isRunning())
|
||||
throw new IllegalStateException();
|
||||
// Create a new connection, give it a parser as an attachment
|
||||
@ -142,14 +155,12 @@ public class NioClientManager extends AbstractExecutionThreadService implements
|
||||
SocketChannel sc = SocketChannel.open();
|
||||
sc.configureBlocking(false);
|
||||
sc.connect(serverAddress);
|
||||
newConnectionChannels.offer(new SocketChannelAndParser(sc, parser));
|
||||
PendingConnect data = new PendingConnect(sc, parser, serverAddress);
|
||||
newConnectionChannels.offer(data);
|
||||
selector.wakeup();
|
||||
} catch (IOException e) {
|
||||
log.error("Could not connect to " + serverAddress);
|
||||
throw new RuntimeException(e); // This should only happen if we are, eg, out of system resources
|
||||
} catch (AssertionError e) {
|
||||
log.error("Could not connect to " + serverAddress);
|
||||
throw new RuntimeException(e); // Happens on Android when libcore.io.Posix.getsockname() throws libcore.io.ErrnoException.
|
||||
return data.future;
|
||||
} catch (Throwable e) {
|
||||
return Futures.immediateFailedFuture(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -30,7 +30,7 @@ import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* IrcDiscovery provides a way to find network peers by joining a pre-agreed rendevouz point on the LFnet IRC network.
|
||||
* <b>This class is deprecated because LFnet has ceased to operate and DNS seeds now exist for both prod and test
|
||||
* <b>This class is deprecated because LFnet has ceased to operate and DNS seeds now exist for both main and test
|
||||
* networks.</b> It may conceivably still be useful for running small ad-hoc networks by yourself.
|
||||
*/
|
||||
@Deprecated
|
||||
@ -49,7 +49,7 @@ public class IrcDiscovery implements PeerDiscovery {
|
||||
* Finds a list of peers by connecting to an IRC network, joining a channel, decoding the nicks and then
|
||||
* disconnecting.
|
||||
*
|
||||
* @param channel The IRC channel to join, either "#bitcoin" or "#bitcoinTEST3" for the production and test networks
|
||||
* @param channel The IRC channel to join, either "#bitcoin" or "#bitcoinTEST3" for the main and test networks
|
||||
* respectively.
|
||||
*/
|
||||
public IrcDiscovery(String channel) {
|
||||
@ -61,7 +61,7 @@ public class IrcDiscovery implements PeerDiscovery {
|
||||
* disconnecting.
|
||||
*
|
||||
* @param server Name or textual IP address of the IRC server to join.
|
||||
* @param channel The IRC channel to join, either "#bitcoin" or "#bitcoinTEST3" for the production and test networks
|
||||
* @param channel The IRC channel to join, either "#bitcoin" or "#bitcoinTEST3" for the main and test networks
|
||||
*/
|
||||
public IrcDiscovery(String channel, String server, int port) {
|
||||
this.channel = channel;
|
||||
|
@ -39,6 +39,8 @@ public class MainNetParams extends NetworkParameters {
|
||||
acceptableAddressCodes = new int[] { addressHeader, p2shHeader };
|
||||
port = 22556;
|
||||
packetMagic = 0xc0c0c0c0;
|
||||
bip32HeaderPub = 0x0488B21E; //The 4 byte header that serializes in base58 to "xpub".
|
||||
bip32HeaderPriv = 0x0488ADE4; //The 4 byte header that serializes in base58 to "xprv"
|
||||
genesisBlock.setDifficultyTarget(0x1e0ffff0L);
|
||||
genesisBlock.setTime(1386325540L);
|
||||
genesisBlock.setNonce(99943L);
|
||||
|
@ -46,6 +46,8 @@ public class TestNet2Params extends NetworkParameters {
|
||||
String genesisHash = genesisBlock.getHashAsString();
|
||||
checkState(genesisHash.equals("00000007199508e34a9ff81e6ec0c477a4cccff2a4767a8eee39c11db367b008"));
|
||||
dnsSeeds = null;
|
||||
bip32HeaderPub = 0x043587CF;
|
||||
bip32HeaderPriv = 0x04358394;
|
||||
}
|
||||
|
||||
private static TestNet2Params instance;
|
||||
|
@ -59,6 +59,8 @@ public class TestNet3Params extends NetworkParameters {
|
||||
"testnet-seed.bitcoin.schildbach.de", // Andreas Schildbach
|
||||
"testnet-seed.bitcoin.petertodd.org" // Peter Todd
|
||||
};
|
||||
bip32HeaderPub = 0x043587CF;
|
||||
bip32HeaderPriv = 0x04358394;
|
||||
}
|
||||
|
||||
private static TestNet3Params instance;
|
||||
|
@ -44,6 +44,8 @@ public class UnitTestParams extends NetworkParameters {
|
||||
spendableCoinbaseDepth = 5;
|
||||
subsidyDecreaseBlockCount = 100;
|
||||
dnsSeeds = null;
|
||||
bip32HeaderPub = 0x043587CF;
|
||||
bip32HeaderPriv = 0x04358394;
|
||||
}
|
||||
|
||||
private static UnitTestParams instance;
|
||||
|
@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Network parameters encapsulate some of the differences between different Bitcoin networks such as the main/production
|
||||
* Network parameters encapsulate some of the differences between different Bitcoin networks such as the main
|
||||
* network, the testnet, regtest mode, unit testing params and so on.
|
||||
*/
|
||||
package com.dogecoin.dogecoinj.params;
|
@ -735,13 +735,31 @@ public final class ClientState {
|
||||
*/
|
||||
com.google.protobuf.ByteString getRefundTransaction();
|
||||
|
||||
// required bytes myPublicKey = 8;
|
||||
/**
|
||||
* <code>required bytes myPublicKey = 8;</code>
|
||||
*/
|
||||
boolean hasMyPublicKey();
|
||||
/**
|
||||
* <code>required bytes myPublicKey = 8;</code>
|
||||
*/
|
||||
com.google.protobuf.ByteString getMyPublicKey();
|
||||
|
||||
// required bytes myKey = 4;
|
||||
/**
|
||||
* <code>required bytes myKey = 4;</code>
|
||||
*
|
||||
* <pre>
|
||||
* Deprecated, key is already stored in the wallet, and found using myPublicKey;
|
||||
* </pre>
|
||||
*/
|
||||
boolean hasMyKey();
|
||||
/**
|
||||
* <code>required bytes myKey = 4;</code>
|
||||
*
|
||||
* <pre>
|
||||
* Deprecated, key is already stored in the wallet, and found using myPublicKey;
|
||||
* </pre>
|
||||
*/
|
||||
com.google.protobuf.ByteString getMyKey();
|
||||
|
||||
@ -859,25 +877,30 @@ public final class ClientState {
|
||||
break;
|
||||
}
|
||||
case 34: {
|
||||
bitField0_ |= 0x00000008;
|
||||
bitField0_ |= 0x00000010;
|
||||
myKey_ = input.readBytes();
|
||||
break;
|
||||
}
|
||||
case 40: {
|
||||
bitField0_ |= 0x00000010;
|
||||
bitField0_ |= 0x00000020;
|
||||
valueToMe_ = input.readUInt64();
|
||||
break;
|
||||
}
|
||||
case 48: {
|
||||
bitField0_ |= 0x00000020;
|
||||
bitField0_ |= 0x00000040;
|
||||
refundFees_ = input.readUInt64();
|
||||
break;
|
||||
}
|
||||
case 58: {
|
||||
bitField0_ |= 0x00000040;
|
||||
bitField0_ |= 0x00000080;
|
||||
closeTransactionHash_ = input.readBytes();
|
||||
break;
|
||||
}
|
||||
case 66: {
|
||||
bitField0_ |= 0x00000008;
|
||||
myPublicKey_ = input.readBytes();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (com.google.protobuf.InvalidProtocolBufferException e) {
|
||||
@ -966,17 +989,41 @@ public final class ClientState {
|
||||
return refundTransaction_;
|
||||
}
|
||||
|
||||
// required bytes myPublicKey = 8;
|
||||
public static final int MYPUBLICKEY_FIELD_NUMBER = 8;
|
||||
private com.google.protobuf.ByteString myPublicKey_;
|
||||
/**
|
||||
* <code>required bytes myPublicKey = 8;</code>
|
||||
*/
|
||||
public boolean hasMyPublicKey() {
|
||||
return ((bitField0_ & 0x00000008) == 0x00000008);
|
||||
}
|
||||
/**
|
||||
* <code>required bytes myPublicKey = 8;</code>
|
||||
*/
|
||||
public com.google.protobuf.ByteString getMyPublicKey() {
|
||||
return myPublicKey_;
|
||||
}
|
||||
|
||||
// required bytes myKey = 4;
|
||||
public static final int MYKEY_FIELD_NUMBER = 4;
|
||||
private com.google.protobuf.ByteString myKey_;
|
||||
/**
|
||||
* <code>required bytes myKey = 4;</code>
|
||||
*
|
||||
* <pre>
|
||||
* Deprecated, key is already stored in the wallet, and found using myPublicKey;
|
||||
* </pre>
|
||||
*/
|
||||
public boolean hasMyKey() {
|
||||
return ((bitField0_ & 0x00000008) == 0x00000008);
|
||||
return ((bitField0_ & 0x00000010) == 0x00000010);
|
||||
}
|
||||
/**
|
||||
* <code>required bytes myKey = 4;</code>
|
||||
*
|
||||
* <pre>
|
||||
* Deprecated, key is already stored in the wallet, and found using myPublicKey;
|
||||
* </pre>
|
||||
*/
|
||||
public com.google.protobuf.ByteString getMyKey() {
|
||||
return myKey_;
|
||||
@ -989,7 +1036,7 @@ public final class ClientState {
|
||||
* <code>required uint64 valueToMe = 5;</code>
|
||||
*/
|
||||
public boolean hasValueToMe() {
|
||||
return ((bitField0_ & 0x00000010) == 0x00000010);
|
||||
return ((bitField0_ & 0x00000020) == 0x00000020);
|
||||
}
|
||||
/**
|
||||
* <code>required uint64 valueToMe = 5;</code>
|
||||
@ -1005,7 +1052,7 @@ public final class ClientState {
|
||||
* <code>required uint64 refundFees = 6;</code>
|
||||
*/
|
||||
public boolean hasRefundFees() {
|
||||
return ((bitField0_ & 0x00000020) == 0x00000020);
|
||||
return ((bitField0_ & 0x00000040) == 0x00000040);
|
||||
}
|
||||
/**
|
||||
* <code>required uint64 refundFees = 6;</code>
|
||||
@ -1027,7 +1074,7 @@ public final class ClientState {
|
||||
* </pre>
|
||||
*/
|
||||
public boolean hasCloseTransactionHash() {
|
||||
return ((bitField0_ & 0x00000040) == 0x00000040);
|
||||
return ((bitField0_ & 0x00000080) == 0x00000080);
|
||||
}
|
||||
/**
|
||||
* <code>optional bytes closeTransactionHash = 7;</code>
|
||||
@ -1046,6 +1093,7 @@ public final class ClientState {
|
||||
id_ = com.google.protobuf.ByteString.EMPTY;
|
||||
contractTransaction_ = com.google.protobuf.ByteString.EMPTY;
|
||||
refundTransaction_ = com.google.protobuf.ByteString.EMPTY;
|
||||
myPublicKey_ = com.google.protobuf.ByteString.EMPTY;
|
||||
myKey_ = com.google.protobuf.ByteString.EMPTY;
|
||||
valueToMe_ = 0L;
|
||||
refundFees_ = 0L;
|
||||
@ -1068,6 +1116,10 @@ public final class ClientState {
|
||||
memoizedIsInitialized = 0;
|
||||
return false;
|
||||
}
|
||||
if (!hasMyPublicKey()) {
|
||||
memoizedIsInitialized = 0;
|
||||
return false;
|
||||
}
|
||||
if (!hasMyKey()) {
|
||||
memoizedIsInitialized = 0;
|
||||
return false;
|
||||
@ -1096,18 +1148,21 @@ public final class ClientState {
|
||||
if (((bitField0_ & 0x00000004) == 0x00000004)) {
|
||||
output.writeBytes(3, refundTransaction_);
|
||||
}
|
||||
if (((bitField0_ & 0x00000008) == 0x00000008)) {
|
||||
if (((bitField0_ & 0x00000010) == 0x00000010)) {
|
||||
output.writeBytes(4, myKey_);
|
||||
}
|
||||
if (((bitField0_ & 0x00000010) == 0x00000010)) {
|
||||
if (((bitField0_ & 0x00000020) == 0x00000020)) {
|
||||
output.writeUInt64(5, valueToMe_);
|
||||
}
|
||||
if (((bitField0_ & 0x00000020) == 0x00000020)) {
|
||||
if (((bitField0_ & 0x00000040) == 0x00000040)) {
|
||||
output.writeUInt64(6, refundFees_);
|
||||
}
|
||||
if (((bitField0_ & 0x00000040) == 0x00000040)) {
|
||||
if (((bitField0_ & 0x00000080) == 0x00000080)) {
|
||||
output.writeBytes(7, closeTransactionHash_);
|
||||
}
|
||||
if (((bitField0_ & 0x00000008) == 0x00000008)) {
|
||||
output.writeBytes(8, myPublicKey_);
|
||||
}
|
||||
getUnknownFields().writeTo(output);
|
||||
}
|
||||
|
||||
@ -1129,22 +1184,26 @@ public final class ClientState {
|
||||
size += com.google.protobuf.CodedOutputStream
|
||||
.computeBytesSize(3, refundTransaction_);
|
||||
}
|
||||
if (((bitField0_ & 0x00000008) == 0x00000008)) {
|
||||
if (((bitField0_ & 0x00000010) == 0x00000010)) {
|
||||
size += com.google.protobuf.CodedOutputStream
|
||||
.computeBytesSize(4, myKey_);
|
||||
}
|
||||
if (((bitField0_ & 0x00000010) == 0x00000010)) {
|
||||
if (((bitField0_ & 0x00000020) == 0x00000020)) {
|
||||
size += com.google.protobuf.CodedOutputStream
|
||||
.computeUInt64Size(5, valueToMe_);
|
||||
}
|
||||
if (((bitField0_ & 0x00000020) == 0x00000020)) {
|
||||
if (((bitField0_ & 0x00000040) == 0x00000040)) {
|
||||
size += com.google.protobuf.CodedOutputStream
|
||||
.computeUInt64Size(6, refundFees_);
|
||||
}
|
||||
if (((bitField0_ & 0x00000040) == 0x00000040)) {
|
||||
if (((bitField0_ & 0x00000080) == 0x00000080)) {
|
||||
size += com.google.protobuf.CodedOutputStream
|
||||
.computeBytesSize(7, closeTransactionHash_);
|
||||
}
|
||||
if (((bitField0_ & 0x00000008) == 0x00000008)) {
|
||||
size += com.google.protobuf.CodedOutputStream
|
||||
.computeBytesSize(8, myPublicKey_);
|
||||
}
|
||||
size += getUnknownFields().getSerializedSize();
|
||||
memoizedSerializedSize = size;
|
||||
return size;
|
||||
@ -1272,14 +1331,16 @@ public final class ClientState {
|
||||
bitField0_ = (bitField0_ & ~0x00000002);
|
||||
refundTransaction_ = com.google.protobuf.ByteString.EMPTY;
|
||||
bitField0_ = (bitField0_ & ~0x00000004);
|
||||
myKey_ = com.google.protobuf.ByteString.EMPTY;
|
||||
myPublicKey_ = com.google.protobuf.ByteString.EMPTY;
|
||||
bitField0_ = (bitField0_ & ~0x00000008);
|
||||
valueToMe_ = 0L;
|
||||
myKey_ = com.google.protobuf.ByteString.EMPTY;
|
||||
bitField0_ = (bitField0_ & ~0x00000010);
|
||||
refundFees_ = 0L;
|
||||
valueToMe_ = 0L;
|
||||
bitField0_ = (bitField0_ & ~0x00000020);
|
||||
closeTransactionHash_ = com.google.protobuf.ByteString.EMPTY;
|
||||
refundFees_ = 0L;
|
||||
bitField0_ = (bitField0_ & ~0x00000040);
|
||||
closeTransactionHash_ = com.google.protobuf.ByteString.EMPTY;
|
||||
bitField0_ = (bitField0_ & ~0x00000080);
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -1323,18 +1384,22 @@ public final class ClientState {
|
||||
if (((from_bitField0_ & 0x00000008) == 0x00000008)) {
|
||||
to_bitField0_ |= 0x00000008;
|
||||
}
|
||||
result.myKey_ = myKey_;
|
||||
result.myPublicKey_ = myPublicKey_;
|
||||
if (((from_bitField0_ & 0x00000010) == 0x00000010)) {
|
||||
to_bitField0_ |= 0x00000010;
|
||||
}
|
||||
result.valueToMe_ = valueToMe_;
|
||||
result.myKey_ = myKey_;
|
||||
if (((from_bitField0_ & 0x00000020) == 0x00000020)) {
|
||||
to_bitField0_ |= 0x00000020;
|
||||
}
|
||||
result.refundFees_ = refundFees_;
|
||||
result.valueToMe_ = valueToMe_;
|
||||
if (((from_bitField0_ & 0x00000040) == 0x00000040)) {
|
||||
to_bitField0_ |= 0x00000040;
|
||||
}
|
||||
result.refundFees_ = refundFees_;
|
||||
if (((from_bitField0_ & 0x00000080) == 0x00000080)) {
|
||||
to_bitField0_ |= 0x00000080;
|
||||
}
|
||||
result.closeTransactionHash_ = closeTransactionHash_;
|
||||
result.bitField0_ = to_bitField0_;
|
||||
onBuilt();
|
||||
@ -1361,6 +1426,9 @@ public final class ClientState {
|
||||
if (other.hasRefundTransaction()) {
|
||||
setRefundTransaction(other.getRefundTransaction());
|
||||
}
|
||||
if (other.hasMyPublicKey()) {
|
||||
setMyPublicKey(other.getMyPublicKey());
|
||||
}
|
||||
if (other.hasMyKey()) {
|
||||
setMyKey(other.getMyKey());
|
||||
}
|
||||
@ -1390,6 +1458,10 @@ public final class ClientState {
|
||||
|
||||
return false;
|
||||
}
|
||||
if (!hasMyPublicKey()) {
|
||||
|
||||
return false;
|
||||
}
|
||||
if (!hasMyKey()) {
|
||||
|
||||
return false;
|
||||
@ -1532,37 +1604,89 @@ public final class ClientState {
|
||||
return this;
|
||||
}
|
||||
|
||||
// required bytes myPublicKey = 8;
|
||||
private com.google.protobuf.ByteString myPublicKey_ = com.google.protobuf.ByteString.EMPTY;
|
||||
/**
|
||||
* <code>required bytes myPublicKey = 8;</code>
|
||||
*/
|
||||
public boolean hasMyPublicKey() {
|
||||
return ((bitField0_ & 0x00000008) == 0x00000008);
|
||||
}
|
||||
/**
|
||||
* <code>required bytes myPublicKey = 8;</code>
|
||||
*/
|
||||
public com.google.protobuf.ByteString getMyPublicKey() {
|
||||
return myPublicKey_;
|
||||
}
|
||||
/**
|
||||
* <code>required bytes myPublicKey = 8;</code>
|
||||
*/
|
||||
public Builder setMyPublicKey(com.google.protobuf.ByteString value) {
|
||||
if (value == null) {
|
||||
throw new NullPointerException();
|
||||
}
|
||||
bitField0_ |= 0x00000008;
|
||||
myPublicKey_ = value;
|
||||
onChanged();
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* <code>required bytes myPublicKey = 8;</code>
|
||||
*/
|
||||
public Builder clearMyPublicKey() {
|
||||
bitField0_ = (bitField0_ & ~0x00000008);
|
||||
myPublicKey_ = getDefaultInstance().getMyPublicKey();
|
||||
onChanged();
|
||||
return this;
|
||||
}
|
||||
|
||||
// required bytes myKey = 4;
|
||||
private com.google.protobuf.ByteString myKey_ = com.google.protobuf.ByteString.EMPTY;
|
||||
/**
|
||||
* <code>required bytes myKey = 4;</code>
|
||||
*
|
||||
* <pre>
|
||||
* Deprecated, key is already stored in the wallet, and found using myPublicKey;
|
||||
* </pre>
|
||||
*/
|
||||
public boolean hasMyKey() {
|
||||
return ((bitField0_ & 0x00000008) == 0x00000008);
|
||||
return ((bitField0_ & 0x00000010) == 0x00000010);
|
||||
}
|
||||
/**
|
||||
* <code>required bytes myKey = 4;</code>
|
||||
*
|
||||
* <pre>
|
||||
* Deprecated, key is already stored in the wallet, and found using myPublicKey;
|
||||
* </pre>
|
||||
*/
|
||||
public com.google.protobuf.ByteString getMyKey() {
|
||||
return myKey_;
|
||||
}
|
||||
/**
|
||||
* <code>required bytes myKey = 4;</code>
|
||||
*
|
||||
* <pre>
|
||||
* Deprecated, key is already stored in the wallet, and found using myPublicKey;
|
||||
* </pre>
|
||||
*/
|
||||
public Builder setMyKey(com.google.protobuf.ByteString value) {
|
||||
if (value == null) {
|
||||
throw new NullPointerException();
|
||||
}
|
||||
bitField0_ |= 0x00000008;
|
||||
bitField0_ |= 0x00000010;
|
||||
myKey_ = value;
|
||||
onChanged();
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* <code>required bytes myKey = 4;</code>
|
||||
*
|
||||
* <pre>
|
||||
* Deprecated, key is already stored in the wallet, and found using myPublicKey;
|
||||
* </pre>
|
||||
*/
|
||||
public Builder clearMyKey() {
|
||||
bitField0_ = (bitField0_ & ~0x00000008);
|
||||
bitField0_ = (bitField0_ & ~0x00000010);
|
||||
myKey_ = getDefaultInstance().getMyKey();
|
||||
onChanged();
|
||||
return this;
|
||||
@ -1574,7 +1698,7 @@ public final class ClientState {
|
||||
* <code>required uint64 valueToMe = 5;</code>
|
||||
*/
|
||||
public boolean hasValueToMe() {
|
||||
return ((bitField0_ & 0x00000010) == 0x00000010);
|
||||
return ((bitField0_ & 0x00000020) == 0x00000020);
|
||||
}
|
||||
/**
|
||||
* <code>required uint64 valueToMe = 5;</code>
|
||||
@ -1586,7 +1710,7 @@ public final class ClientState {
|
||||
* <code>required uint64 valueToMe = 5;</code>
|
||||
*/
|
||||
public Builder setValueToMe(long value) {
|
||||
bitField0_ |= 0x00000010;
|
||||
bitField0_ |= 0x00000020;
|
||||
valueToMe_ = value;
|
||||
onChanged();
|
||||
return this;
|
||||
@ -1595,7 +1719,7 @@ public final class ClientState {
|
||||
* <code>required uint64 valueToMe = 5;</code>
|
||||
*/
|
||||
public Builder clearValueToMe() {
|
||||
bitField0_ = (bitField0_ & ~0x00000010);
|
||||
bitField0_ = (bitField0_ & ~0x00000020);
|
||||
valueToMe_ = 0L;
|
||||
onChanged();
|
||||
return this;
|
||||
@ -1607,7 +1731,7 @@ public final class ClientState {
|
||||
* <code>required uint64 refundFees = 6;</code>
|
||||
*/
|
||||
public boolean hasRefundFees() {
|
||||
return ((bitField0_ & 0x00000020) == 0x00000020);
|
||||
return ((bitField0_ & 0x00000040) == 0x00000040);
|
||||
}
|
||||
/**
|
||||
* <code>required uint64 refundFees = 6;</code>
|
||||
@ -1619,7 +1743,7 @@ public final class ClientState {
|
||||
* <code>required uint64 refundFees = 6;</code>
|
||||
*/
|
||||
public Builder setRefundFees(long value) {
|
||||
bitField0_ |= 0x00000020;
|
||||
bitField0_ |= 0x00000040;
|
||||
refundFees_ = value;
|
||||
onChanged();
|
||||
return this;
|
||||
@ -1628,7 +1752,7 @@ public final class ClientState {
|
||||
* <code>required uint64 refundFees = 6;</code>
|
||||
*/
|
||||
public Builder clearRefundFees() {
|
||||
bitField0_ = (bitField0_ & ~0x00000020);
|
||||
bitField0_ = (bitField0_ & ~0x00000040);
|
||||
refundFees_ = 0L;
|
||||
onChanged();
|
||||
return this;
|
||||
@ -1646,7 +1770,7 @@ public final class ClientState {
|
||||
* </pre>
|
||||
*/
|
||||
public boolean hasCloseTransactionHash() {
|
||||
return ((bitField0_ & 0x00000040) == 0x00000040);
|
||||
return ((bitField0_ & 0x00000080) == 0x00000080);
|
||||
}
|
||||
/**
|
||||
* <code>optional bytes closeTransactionHash = 7;</code>
|
||||
@ -1673,7 +1797,7 @@ public final class ClientState {
|
||||
if (value == null) {
|
||||
throw new NullPointerException();
|
||||
}
|
||||
bitField0_ |= 0x00000040;
|
||||
bitField0_ |= 0x00000080;
|
||||
closeTransactionHash_ = value;
|
||||
onChanged();
|
||||
return this;
|
||||
@ -1688,7 +1812,7 @@ public final class ClientState {
|
||||
* </pre>
|
||||
*/
|
||||
public Builder clearCloseTransactionHash() {
|
||||
bitField0_ = (bitField0_ & ~0x00000040);
|
||||
bitField0_ = (bitField0_ & ~0x00000080);
|
||||
closeTransactionHash_ = getDefaultInstance().getCloseTransactionHash();
|
||||
onChanged();
|
||||
return this;
|
||||
@ -1727,13 +1851,13 @@ public final class ClientState {
|
||||
"\n storedclientpaymentchannel.proto\022\017paym" +
|
||||
"entchannels\"\\\n\033StoredClientPaymentChanne" +
|
||||
"ls\022=\n\010channels\030\001 \003(\0132+.paymentchannels.S" +
|
||||
"toredClientPaymentChannel\"\264\001\n\032StoredClie" +
|
||||
"toredClientPaymentChannel\"\311\001\n\032StoredClie" +
|
||||
"ntPaymentChannel\022\n\n\002id\030\001 \002(\014\022\033\n\023contract" +
|
||||
"Transaction\030\002 \002(\014\022\031\n\021refundTransaction\030\003" +
|
||||
" \002(\014\022\r\n\005myKey\030\004 \002(\014\022\021\n\tvalueToMe\030\005 \002(\004\022\022" +
|
||||
"\n\nrefundFees\030\006 \002(\004\022\034\n\024closeTransactionHa" +
|
||||
"sh\030\007 \001(\014B.\n\037org.bitcoinj.protocols.chann" +
|
||||
"elsB\013ClientState"
|
||||
" \002(\014\022\023\n\013myPublicKey\030\010 \002(\014\022\r\n\005myKey\030\004 \002(\014" +
|
||||
"\022\021\n\tvalueToMe\030\005 \002(\004\022\022\n\nrefundFees\030\006 \002(\004\022" +
|
||||
"\034\n\024closeTransactionHash\030\007 \001(\014B.\n\037org.bit" +
|
||||
"coinj.protocols.channelsB\013ClientState"
|
||||
};
|
||||
com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner =
|
||||
new com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner() {
|
||||
@ -1751,7 +1875,7 @@ public final class ClientState {
|
||||
internal_static_paymentchannels_StoredClientPaymentChannel_fieldAccessorTable = new
|
||||
com.google.protobuf.GeneratedMessage.FieldAccessorTable(
|
||||
internal_static_paymentchannels_StoredClientPaymentChannel_descriptor,
|
||||
new java.lang.String[] { "Id", "ContractTransaction", "RefundTransaction", "MyKey", "ValueToMe", "RefundFees", "CloseTransactionHash", });
|
||||
new java.lang.String[] { "Id", "ContractTransaction", "RefundTransaction", "MyPublicKey", "MyKey", "ValueToMe", "RefundFees", "CloseTransactionHash", });
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
@ -17,11 +17,13 @@
|
||||
package com.dogecoin.dogecoinj.protocols.channels;
|
||||
|
||||
import com.dogecoin.dogecoinj.core.Coin;
|
||||
import com.dogecoin.dogecoinj.core.ECKey;
|
||||
import com.dogecoin.dogecoinj.core.InsufficientMoneyException;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
import org.bitcoin.paymentchannel.Protos;
|
||||
import org.spongycastle.crypto.params.KeyParameter;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
@ -82,9 +84,12 @@ public interface IPaymentChannelClient {
|
||||
* ({@link PaymentChannelClientConnection#state()}.getTotalValue())
|
||||
* @throws IllegalStateException If the channel has been closed or is not yet open
|
||||
* (see {@link PaymentChannelClientConnection#getChannelOpenFuture()} for the second)
|
||||
* @throws ECKey.KeyIsEncryptedException If the keys are encrypted and no AES key has been provided,
|
||||
* @return a future that completes when the server acknowledges receipt and acceptance of the payment.
|
||||
*/
|
||||
ListenableFuture<PaymentIncrementAck> incrementPayment(Coin size, @Nullable ByteString info) throws ValueOutOfRangeException, IllegalStateException;
|
||||
ListenableFuture<PaymentIncrementAck> incrementPayment(Coin size, @Nullable ByteString info,
|
||||
@Nullable KeyParameter userKey)
|
||||
throws ValueOutOfRangeException, IllegalStateException, ECKey.KeyIsEncryptedException;
|
||||
|
||||
/**
|
||||
* Implements the connection between this client and the server, providing an interface which allows messages to be
|
||||
|
@ -1,5 +1,6 @@
|
||||
/*
|
||||
* Copyright 2013 Google Inc.
|
||||
* Copyright 2014 Andreas Schildbach
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -27,10 +28,9 @@ import com.google.protobuf.ByteString;
|
||||
import net.jcip.annotations.GuardedBy;
|
||||
import org.bitcoin.paymentchannel.Protos;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.spongycastle.crypto.params.KeyParameter;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
@ -66,8 +66,6 @@ public class PaymentChannelClient implements IPaymentChannelClient {
|
||||
// The state object used to step through initialization and pay the server
|
||||
@GuardedBy("lock") private PaymentChannelClientState state;
|
||||
|
||||
public static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
|
||||
|
||||
// The step we are at in initialization, this is partially duplicated in the state object
|
||||
private enum InitStep {
|
||||
WAITING_FOR_CONNECTION_OPEN,
|
||||
@ -95,6 +93,9 @@ public class PaymentChannelClient implements IPaymentChannelClient {
|
||||
|
||||
private Coin missing;
|
||||
|
||||
// key to decrypt myKey, if it is encrypted, during setup.
|
||||
private KeyParameter userKeySetup;
|
||||
|
||||
private final long timeWindow;
|
||||
|
||||
@GuardedBy("lock") private long minPayment;
|
||||
@ -129,7 +130,7 @@ public class PaymentChannelClient implements IPaymentChannelClient {
|
||||
* the server)
|
||||
*/
|
||||
public PaymentChannelClient(Wallet wallet, ECKey myKey, Coin maxValue, Sha256Hash serverId, ClientConnection conn) {
|
||||
this(wallet,myKey,maxValue,serverId, DEFAULT_TIME_WINDOW, conn);
|
||||
this(wallet,myKey,maxValue,serverId, DEFAULT_TIME_WINDOW, null, conn);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -148,10 +149,12 @@ public class PaymentChannelClient implements IPaymentChannelClient {
|
||||
* @param timeWindow The time in seconds, relative to now, on how long this channel should be kept open. Note that is is
|
||||
* a proposal to the server. The server may in turn propose something different.
|
||||
* See {@link com.dogecoin.dogecoinj.protocols.channels.IPaymentChannelClient.ClientConnection#acceptExpireTime(long)}
|
||||
* @param userKeySetup Key derived from a user password, used to decrypt myKey, if it is encrypted, during setup.
|
||||
* @param conn A callback listener which represents the connection to the server (forwards messages we generate to
|
||||
* the server)
|
||||
*/
|
||||
public PaymentChannelClient(Wallet wallet, ECKey myKey, Coin maxValue, Sha256Hash serverId, long timeWindow, ClientConnection conn) {
|
||||
public PaymentChannelClient(Wallet wallet, ECKey myKey, Coin maxValue, Sha256Hash serverId, long timeWindow,
|
||||
@Nullable KeyParameter userKeySetup, ClientConnection conn) {
|
||||
this.wallet = checkNotNull(wallet);
|
||||
this.myKey = checkNotNull(myKey);
|
||||
this.maxValue = checkNotNull(maxValue);
|
||||
@ -159,6 +162,7 @@ public class PaymentChannelClient implements IPaymentChannelClient {
|
||||
checkState(timeWindow >= 0);
|
||||
this.timeWindow = timeWindow;
|
||||
this.conn = checkNotNull(conn);
|
||||
this.userKeySetup = userKeySetup;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -173,14 +177,18 @@ public class PaymentChannelClient implements IPaymentChannelClient {
|
||||
|
||||
@Nullable
|
||||
@GuardedBy("lock")
|
||||
private CloseReason receiveInitiate(Protos.Initiate initiate, Coin contractValue, Protos.Error.Builder errorBuilder) throws VerificationException, InsufficientMoneyException {
|
||||
private CloseReason receiveInitiate(Protos.Initiate initiate, Coin contractValue, Protos.Error.Builder errorBuilder)
|
||||
throws VerificationException, InsufficientMoneyException, ECKey.KeyIsEncryptedException {
|
||||
log.info("Got INITIATE message:\n{}", initiate.toString());
|
||||
|
||||
if (wallet.isEncrypted() && this.userKeySetup == null)
|
||||
throw new ECKey.KeyIsEncryptedException();
|
||||
|
||||
final long expireTime = initiate.getExpireTimeSecs();
|
||||
checkState( expireTime >= 0 && initiate.getMinAcceptedChannelSize() >= 0);
|
||||
|
||||
if (! conn.acceptExpireTime(expireTime)) {
|
||||
log.error("Server suggested expire time was out of our allowed bounds: {} ({} s)", dateFormat.format(new Date(expireTime * 1000)), expireTime);
|
||||
log.error("Server suggested expire time was out of our allowed bounds: {} ({} s)", Utils.dateTimeFormat(expireTime * 1000), expireTime);
|
||||
errorBuilder.setCode(Protos.Error.ErrorCode.TIME_WINDOW_UNACCEPTABLE);
|
||||
return CloseReason.TIME_WINDOW_UNACCEPTABLE;
|
||||
}
|
||||
@ -209,7 +217,7 @@ public class PaymentChannelClient implements IPaymentChannelClient {
|
||||
throw new VerificationException("Server gave us a non-canonical public key, protocol error.");
|
||||
state = new PaymentChannelClientState(wallet, myKey, ECKey.fromPublicOnly(pubKeyBytes), contractValue, expireTime);
|
||||
try {
|
||||
state.initiate();
|
||||
state.initiate(userKeySetup);
|
||||
} catch (ValueOutOfRangeException e) {
|
||||
log.error("Value out of range when trying to initiate", e);
|
||||
errorBuilder.setCode(Protos.Error.ErrorCode.CHANNEL_VALUE_TOO_LARGE);
|
||||
@ -230,11 +238,11 @@ public class PaymentChannelClient implements IPaymentChannelClient {
|
||||
}
|
||||
|
||||
@GuardedBy("lock")
|
||||
private void receiveRefund(Protos.TwoWayChannelMessage refundMsg) throws VerificationException {
|
||||
private void receiveRefund(Protos.TwoWayChannelMessage refundMsg, @Nullable KeyParameter userKey) throws VerificationException {
|
||||
checkState(step == InitStep.WAITING_FOR_REFUND_RETURN && refundMsg.hasReturnRefund());
|
||||
log.info("Got RETURN_REFUND message, providing signed contract");
|
||||
Protos.ReturnRefund returnedRefund = refundMsg.getReturnRefund();
|
||||
state.provideRefundSignature(returnedRefund.getSignature().toByteArray());
|
||||
state.provideRefundSignature(returnedRefund.getSignature().toByteArray(), userKey);
|
||||
step = InitStep.WAITING_FOR_CHANNEL_OPEN;
|
||||
|
||||
// Before we can send the server the contract (ie send it to the network), we must ensure that our refund
|
||||
@ -246,7 +254,7 @@ public class PaymentChannelClient implements IPaymentChannelClient {
|
||||
try {
|
||||
// Make an initial payment of the dust limit, and put it into the message as well. The size of the
|
||||
// server-requested dust limit was already sanity checked by this point.
|
||||
PaymentChannelClientState.IncrementedPayment payment = state().incrementPaymentBy(Coin.valueOf(minPayment));
|
||||
PaymentChannelClientState.IncrementedPayment payment = state().incrementPaymentBy(Coin.valueOf(minPayment), userKey);
|
||||
Protos.UpdatePayment.Builder initialMsg = contractMsg.getInitialPaymentBuilder();
|
||||
initialMsg.setSignature(ByteString.copyFrom(payment.signature.encodeToBitcoin()));
|
||||
initialMsg.setClientChangeValue(state.getValueRefunded().value);
|
||||
@ -313,7 +321,9 @@ public class PaymentChannelClient implements IPaymentChannelClient {
|
||||
log.error("Initiate failed with error: {}", errorBuilder.build().toString());
|
||||
break;
|
||||
case RETURN_REFUND:
|
||||
receiveRefund(msg);
|
||||
receiveRefund(msg, userKeySetup);
|
||||
// Key not used anymore
|
||||
userKeySetup = null;
|
||||
return;
|
||||
case CHANNEL_OPEN:
|
||||
receiveChannelOpen();
|
||||
@ -500,7 +510,7 @@ public class PaymentChannelClient implements IPaymentChannelClient {
|
||||
* (see {@link PaymentChannelClientConnection#getChannelOpenFuture()} for the second)
|
||||
*/
|
||||
public ListenableFuture<PaymentIncrementAck> incrementPayment(Coin size) throws ValueOutOfRangeException, IllegalStateException {
|
||||
return incrementPayment(size, null);
|
||||
return incrementPayment(size, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -512,22 +522,28 @@ public class PaymentChannelClient implements IPaymentChannelClient {
|
||||
*
|
||||
* @param size How many satoshis to increment the payment by (note: not the new total).
|
||||
* @param info Information about this update, used to extend this protocol.
|
||||
* @param userKey Key derived from a user password, needed for any signing when the wallet is encrypted.
|
||||
* The wallet KeyCrypter is assumed.
|
||||
* @return a future that completes when the server acknowledges receipt and acceptance of the payment.
|
||||
* @throws ValueOutOfRangeException If the size is negative or would pay more than this channel's total value
|
||||
* ({@link PaymentChannelClientConnection#state()}.getTotalValue())
|
||||
* @throws IllegalStateException If the channel has been closed or is not yet open
|
||||
* (see {@link PaymentChannelClientConnection#getChannelOpenFuture()} for the second)
|
||||
* @throws ECKey.KeyIsEncryptedException If the keys are encrypted and no AES key has been provided,
|
||||
*/
|
||||
@Override
|
||||
public ListenableFuture<PaymentIncrementAck> incrementPayment(Coin size, @Nullable ByteString info) throws ValueOutOfRangeException, IllegalStateException {
|
||||
public ListenableFuture<PaymentIncrementAck> incrementPayment(Coin size, @Nullable ByteString info, @Nullable KeyParameter userKey)
|
||||
throws ValueOutOfRangeException, IllegalStateException, ECKey.KeyIsEncryptedException {
|
||||
lock.lock();
|
||||
try {
|
||||
if (state() == null || !connectionOpen || step != InitStep.CHANNEL_OPEN)
|
||||
throw new IllegalStateException("Channel is not fully initialized/has already been closed");
|
||||
if (increasePaymentFuture != null)
|
||||
throw new IllegalStateException("Already incrementing paying, wait for previous payment to complete.");
|
||||
if (wallet.isEncrypted() && userKey == null)
|
||||
throw new ECKey.KeyIsEncryptedException();
|
||||
|
||||
PaymentChannelClientState.IncrementedPayment payment = state().incrementPaymentBy(size);
|
||||
PaymentChannelClientState.IncrementedPayment payment = state().incrementPaymentBy(size, userKey);
|
||||
Protos.UpdatePayment.Builder updatePaymentBuilder = Protos.UpdatePayment.newBuilder()
|
||||
.setSignature(ByteString.copyFrom(payment.signature.encodeToBitcoin()))
|
||||
.setClientChangeValue(state.getValueRefunded().value);
|
||||
|
@ -29,7 +29,9 @@ import com.google.common.util.concurrent.SettableFuture;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
import org.bitcoin.paymentchannel.Protos;
|
||||
import org.spongycastle.crypto.params.KeyParameter;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
@ -45,14 +47,15 @@ public class PaymentChannelClientConnection {
|
||||
|
||||
/**
|
||||
* Attempts to open a new connection to and open a payment channel with the given host and port, blocking until the
|
||||
* connection is open. The server is requested to keep the channel open for {@link com.dogecoin.dogecoinj.protocols.channels.PaymentChannelClient#DEFAULT_TIME_WINDOW}
|
||||
* connection is open. The server is requested to keep the channel open for
|
||||
* {@link com.dogecoin.dogecoinj.protocols.channels.PaymentChannelClient#DEFAULT_TIME_WINDOW}
|
||||
* seconds. If the server proposes a longer time the channel will be closed.
|
||||
*
|
||||
* @param server The host/port pair where the server is listening.
|
||||
* @param timeoutSeconds The connection timeout and read timeout during initialization. This should be large enough
|
||||
* to accommodate ECDSA signature operations and network latency.
|
||||
* @param wallet The wallet which will be paid from, and where completed transactions will be committed.
|
||||
* Must already have a {@link StoredPaymentChannelClientStates} object in its extensions set.
|
||||
* Must be unencrypted. Must already have a {@link StoredPaymentChannelClientStates} object in its extensions set.
|
||||
* @param myKey A freshly generated keypair used for the multisig contract and refund output.
|
||||
* @param maxValue The maximum value this channel is allowed to request
|
||||
* @param serverId A unique ID which is used to attempt reopening of an existing channel.
|
||||
@ -65,7 +68,8 @@ public class PaymentChannelClientConnection {
|
||||
*/
|
||||
public PaymentChannelClientConnection(InetSocketAddress server, int timeoutSeconds, Wallet wallet, ECKey myKey,
|
||||
Coin maxValue, String serverId) throws IOException, ValueOutOfRangeException {
|
||||
this(server, timeoutSeconds, wallet, myKey, maxValue, serverId, PaymentChannelClient.DEFAULT_TIME_WINDOW);
|
||||
this(server, timeoutSeconds, wallet, myKey, maxValue, serverId,
|
||||
PaymentChannelClient.DEFAULT_TIME_WINDOW, null);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -77,7 +81,8 @@ public class PaymentChannelClientConnection {
|
||||
* @param timeoutSeconds The connection timeout and read timeout during initialization. This should be large enough
|
||||
* to accommodate ECDSA signature operations and network latency.
|
||||
* @param wallet The wallet which will be paid from, and where completed transactions will be committed.
|
||||
* Must already have a {@link StoredPaymentChannelClientStates} object in its extensions set.
|
||||
* Can be encrypted if user key is supplied when needed. Must already have a
|
||||
* {@link StoredPaymentChannelClientStates} object in its extensions set.
|
||||
* @param myKey A freshly generated keypair used for the multisig contract and refund output.
|
||||
* @param maxValue The maximum value this channel is allowed to request
|
||||
* @param serverId A unique ID which is used to attempt reopening of an existing channel.
|
||||
@ -85,16 +90,19 @@ public class PaymentChannelClientConnection {
|
||||
* API, this should also probably encompass some caller UID to avoid applications opening channels
|
||||
* which were created by others.
|
||||
* @param timeWindow The time in seconds, relative to now, on how long this channel should be kept open.
|
||||
* @param userKeySetup Key derived from a user password, used to decrypt myKey, if it is encrypted, during setup.
|
||||
*
|
||||
* @throws IOException if there's an issue using the network.
|
||||
* @throws ValueOutOfRangeException if the balance of wallet is lower than maxValue.
|
||||
*/
|
||||
public PaymentChannelClientConnection(InetSocketAddress server, int timeoutSeconds, Wallet wallet, ECKey myKey,
|
||||
Coin maxValue, String serverId, final long timeWindow) throws IOException, ValueOutOfRangeException {
|
||||
Coin maxValue, String serverId, final long timeWindow,
|
||||
@Nullable KeyParameter userKeySetup)
|
||||
throws IOException, ValueOutOfRangeException {
|
||||
// Glue the object which vends/ingests protobuf messages in order to manage state to the network object which
|
||||
// reads/writes them to the wire in length prefixed form.
|
||||
channelClient = new PaymentChannelClient(wallet, myKey, maxValue, Sha256Hash.create(serverId.getBytes()), timeWindow,
|
||||
new PaymentChannelClient.ClientConnection() {
|
||||
userKeySetup, new PaymentChannelClient.ClientConnection() {
|
||||
@Override
|
||||
public void sendToServer(Protos.TwoWayChannelMessage msg) {
|
||||
wireParser.write(msg);
|
||||
@ -170,7 +178,7 @@ public class PaymentChannelClientConnection {
|
||||
* (see {@link PaymentChannelClientConnection#getChannelOpenFuture()} for the second)
|
||||
*/
|
||||
public ListenableFuture<PaymentIncrementAck> incrementPayment(Coin size) throws ValueOutOfRangeException, IllegalStateException {
|
||||
return channelClient.incrementPayment(size, null);
|
||||
return channelClient.incrementPayment(size, null, null);
|
||||
}
|
||||
/**
|
||||
* Increments the total value which we pay the server.
|
||||
@ -182,8 +190,28 @@ public class PaymentChannelClientConnection {
|
||||
* @throws IllegalStateException If the channel has been closed or is not yet open
|
||||
* (see {@link PaymentChannelClientConnection#getChannelOpenFuture()} for the second)
|
||||
*/
|
||||
/*
|
||||
public ListenableFuture<PaymentIncrementAck> incrementPayment(Coin size, ByteString info) throws ValueOutOfRangeException, IllegalStateException {
|
||||
return channelClient.incrementPayment(size, info);
|
||||
return channelClient.incrementPayment(size, info, null);
|
||||
}
|
||||
*/
|
||||
/**
|
||||
* Increments the total value which we pay the server.
|
||||
*
|
||||
* @param size How many satoshis to increment the payment by (note: not the new total).
|
||||
* @param info Information about this payment increment, used to extend this protocol.
|
||||
* @param userKey Key derived from a user password, needed for any signing when the wallet is encrypted.
|
||||
* The wallet KeyCrypter is assumed.
|
||||
* @throws ValueOutOfRangeException If the size is negative or would pay more than this channel's total value
|
||||
* ({@link PaymentChannelClientConnection#state()}.getTotalValue())
|
||||
* @throws IllegalStateException If the channel has been closed or is not yet open
|
||||
* (see {@link PaymentChannelClientConnection#getChannelOpenFuture()} for the second)
|
||||
*/
|
||||
public ListenableFuture<PaymentIncrementAck> incrementPayment(Coin size,
|
||||
@Nullable ByteString info,
|
||||
@Nullable KeyParameter userKey)
|
||||
throws ValueOutOfRangeException, IllegalStateException {
|
||||
return channelClient.incrementPayment(size, info, userKey);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -22,6 +22,7 @@ import com.dogecoin.dogecoinj.script.Script;
|
||||
import com.dogecoin.dogecoinj.script.ScriptBuilder;
|
||||
import com.dogecoin.dogecoinj.utils.Threading;
|
||||
import com.dogecoin.dogecoinj.wallet.AllowUnconfirmedCoinSelector;
|
||||
import org.spongycastle.crypto.params.KeyParameter;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.base.Throwables;
|
||||
import com.google.common.collect.Lists;
|
||||
@ -31,6 +32,7 @@ import com.google.common.util.concurrent.ListenableFuture;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.List;
|
||||
|
||||
import static com.google.common.base.Preconditions.*;
|
||||
@ -59,7 +61,7 @@ import static com.google.common.base.Preconditions.*;
|
||||
* INITIATED and creates the initial multi-sig contract and refund transaction. If the wallet has insufficient funds an
|
||||
* exception will be thrown at this point. Once this is done, call
|
||||
* {@link PaymentChannelClientState#getIncompleteRefundTransaction()} and pass the resultant transaction through to the
|
||||
* server. Once you have retrieved the signature, use {@link PaymentChannelClientState#provideRefundSignature(byte[])}.
|
||||
* server. Once you have retrieved the signature, use {@link PaymentChannelClientState#provideRefundSignature(byte[], KeyParameter)}.
|
||||
* You must then call {@link PaymentChannelClientState#storeChannelInWallet(Sha256Hash)} to store the refund transaction
|
||||
* in the wallet, protecting you against a malicious server attempting to destroy all your coins. At this point, you can
|
||||
* provide the server with the multi-sig contract (via {@link PaymentChannelClientState#getMultisigContract()}) safely.
|
||||
@ -149,7 +151,7 @@ public class PaymentChannelClientState {
|
||||
* @param myKey a freshly generated private key for this channel.
|
||||
* @param serverMultisigKey a public key retrieved from the server used for the initial multisig contract
|
||||
* @param value how many satoshis to put into this contract. If the channel reaches this limit, it must be closed.
|
||||
* It is suggested you use at least {@link Utils#CENT} to avoid paying fees if you need to spend the refund transaction
|
||||
* It is suggested you use at least {@link Coin#CENT} to avoid paying fees if you need to spend the refund transaction
|
||||
* @param expiryTimeInSeconds At what point (UNIX timestamp +/- a few hours) the channel will expire
|
||||
*
|
||||
* @throws VerificationException If either myKey's pubkey or serverMultisigKey's pubkey are non-canonical (ie invalid)
|
||||
@ -195,10 +197,10 @@ public class PaymentChannelClientState {
|
||||
// of this channel along with the refund tx from the wallet, because we're not going to need
|
||||
// any of that any more.
|
||||
final TransactionConfidence confidence = storedChannel.close.getConfidence();
|
||||
ListenableFuture<Transaction> future = confidence.getDepthFuture(CONFIRMATIONS_FOR_DELETE, Threading.SAME_THREAD);
|
||||
Futures.addCallback(future, new FutureCallback<Transaction>() {
|
||||
ListenableFuture<TransactionConfidence> future = confidence.getDepthFuture(CONFIRMATIONS_FOR_DELETE, Threading.SAME_THREAD);
|
||||
Futures.addCallback(future, new FutureCallback<TransactionConfidence>() {
|
||||
@Override
|
||||
public void onSuccess(Transaction result) {
|
||||
public void onSuccess(TransactionConfidence result) {
|
||||
deleteChannelFromWallet();
|
||||
}
|
||||
|
||||
@ -235,7 +237,23 @@ public class PaymentChannelClientState {
|
||||
* @throws ValueOutOfRangeException if the value being used is too small to be accepted by the network
|
||||
* @throws InsufficientMoneyException if the wallet doesn't contain enough balance to initiate
|
||||
*/
|
||||
public synchronized void initiate() throws ValueOutOfRangeException, InsufficientMoneyException {
|
||||
public void initiate() throws ValueOutOfRangeException, InsufficientMoneyException {
|
||||
initiate(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the initial multisig contract and incomplete refund transaction which can be requested at the appropriate
|
||||
* time using {@link PaymentChannelClientState#getIncompleteRefundTransaction} and
|
||||
* {@link PaymentChannelClientState#getMultisigContract()}. The way the contract is crafted can be adjusted by
|
||||
* overriding {@link PaymentChannelClientState#editContractSendRequest(com.dogecoin.dogecoinj.core.Wallet.SendRequest)}.
|
||||
* By default unconfirmed coins are allowed to be used, as for micropayments the risk should be relatively low.
|
||||
* @param userKey Key derived from a user password, needed for any signing when the wallet is encrypted.
|
||||
* The wallet KeyCrypter is assumed.
|
||||
*
|
||||
* @throws ValueOutOfRangeException if the value being used is too small to be accepted by the network
|
||||
* @throws InsufficientMoneyException if the wallet doesn't contain enough balance to initiate
|
||||
*/
|
||||
public synchronized void initiate(@Nullable KeyParameter userKey) throws ValueOutOfRangeException, InsufficientMoneyException {
|
||||
final NetworkParameters params = wallet.getParams();
|
||||
Transaction template = new Transaction(params);
|
||||
// We always place the client key before the server key because, if either side wants some privacy, they can
|
||||
@ -251,6 +269,7 @@ public class PaymentChannelClientState {
|
||||
req.coinSelector = AllowUnconfirmedCoinSelector.get();
|
||||
editContractSendRequest(req);
|
||||
req.shuffleOutputs = false; // TODO: Fix things so shuffling is usable.
|
||||
req.aesKey = userKey;
|
||||
wallet.completeTx(req);
|
||||
Coin multisigFee = req.tx.getFee();
|
||||
multisigContract = req.tx;
|
||||
@ -291,7 +310,7 @@ public class PaymentChannelClientState {
|
||||
|
||||
/**
|
||||
* Returns the transaction that locks the money to the agreement of both parties. Do not mutate the result.
|
||||
* Once this step is done, you can use {@link PaymentChannelClientState#incrementPaymentBy(Coin)} to
|
||||
* Once this step is done, you can use {@link PaymentChannelClientState#incrementPaymentBy(Coin, KeyParameter)} to
|
||||
* start paying the server.
|
||||
*/
|
||||
public synchronized Transaction getMultisigContract() {
|
||||
@ -304,7 +323,7 @@ public class PaymentChannelClientState {
|
||||
/**
|
||||
* Returns a partially signed (invalid) refund transaction that should be passed to the server. Once the server
|
||||
* has checked it out and provided its own signature, call
|
||||
* {@link PaymentChannelClientState#provideRefundSignature(byte[])} with the result.
|
||||
* {@link PaymentChannelClientState#provideRefundSignature(byte[], KeyParameter)} with the result.
|
||||
*/
|
||||
public synchronized Transaction getIncompleteRefundTransaction() {
|
||||
checkState(refundTx != null);
|
||||
@ -322,7 +341,8 @@ public class PaymentChannelClientState {
|
||||
* transaction are automatically committed to wallet so that it can handle broadcasting the refund transaction at
|
||||
* the appropriate time if necessary.</p>
|
||||
*/
|
||||
public synchronized void provideRefundSignature(byte[] theirSignature) throws VerificationException {
|
||||
public synchronized void provideRefundSignature(byte[] theirSignature, @Nullable KeyParameter userKey)
|
||||
throws VerificationException {
|
||||
checkNotNull(theirSignature);
|
||||
checkState(state == State.WAITING_FOR_SIGNED_REFUND);
|
||||
TransactionSignature theirSig = TransactionSignature.decodeFromBitcoin(theirSignature, true);
|
||||
@ -336,7 +356,8 @@ public class PaymentChannelClientState {
|
||||
throw new RuntimeException(e); // Cannot happen: we built this ourselves.
|
||||
}
|
||||
TransactionSignature ourSignature =
|
||||
refundTx.calculateSignature(0, myKey, multisigScript, Transaction.SigHash.ALL, false);
|
||||
refundTx.calculateSignature(0, myKey.maybeDecrypt(userKey),
|
||||
multisigScript, Transaction.SigHash.ALL, false);
|
||||
// Insert the signatures.
|
||||
Script scriptSig = ScriptBuilder.createMultiSigInputScript(ourSignature, theirSig);
|
||||
log.info("Refund scriptSig: {}", scriptSig);
|
||||
@ -390,7 +411,8 @@ public class PaymentChannelClientState {
|
||||
* @throws ValueOutOfRangeException If size is negative or the channel does not have sufficient money in it to
|
||||
* complete this payment.
|
||||
*/
|
||||
public synchronized IncrementedPayment incrementPaymentBy(Coin size) throws ValueOutOfRangeException {
|
||||
public synchronized IncrementedPayment incrementPaymentBy(Coin size, @Nullable KeyParameter userKey)
|
||||
throws ValueOutOfRangeException {
|
||||
checkState(state == State.READY);
|
||||
checkNotExpired();
|
||||
checkNotNull(size); // Validity of size will be checked by makeUnsignedChannelContract.
|
||||
@ -413,7 +435,7 @@ public class PaymentChannelClientState {
|
||||
mode = Transaction.SigHash.NONE;
|
||||
else
|
||||
mode = Transaction.SigHash.SINGLE;
|
||||
TransactionSignature sig = tx.calculateSignature(0, myKey, multisigScript, mode, true);
|
||||
TransactionSignature sig = tx.calculateSignature(0, myKey.maybeDecrypt(userKey), multisigScript, mode, true);
|
||||
valueToMe = newValueToMe;
|
||||
updateChannelInWallet();
|
||||
IncrementedPayment payment = new IncrementedPayment();
|
||||
@ -506,7 +528,7 @@ public class PaymentChannelClientState {
|
||||
|
||||
/**
|
||||
* Once the servers signature over the refund transaction has been received and provided using
|
||||
* {@link PaymentChannelClientState#provideRefundSignature(byte[])} then this
|
||||
* {@link PaymentChannelClientState#provideRefundSignature(byte[], KeyParameter)} then this
|
||||
* method can be called to receive the now valid and broadcastable refund transaction.
|
||||
*/
|
||||
public synchronized Transaction getCompletedRefundTransaction() {
|
||||
|
@ -264,13 +264,14 @@ public class StoredPaymentChannelClientStates implements WalletExtension {
|
||||
// First a few asserts to make sure things won't break
|
||||
checkState(channel.valueToMe.signum() >= 0 && channel.valueToMe.compareTo(NetworkParameters.MAX_MONEY) < 0);
|
||||
checkState(channel.refundFees.signum() >= 0 && channel.refundFees.compareTo(NetworkParameters.MAX_MONEY) < 0);
|
||||
checkNotNull(channel.myKey.getPrivKeyBytes());
|
||||
checkNotNull(channel.myKey.getPubKey());
|
||||
checkState(channel.refund.getConfidence().getSource() == TransactionConfidence.Source.SELF);
|
||||
final ClientState.StoredClientPaymentChannel.Builder value = ClientState.StoredClientPaymentChannel.newBuilder()
|
||||
.setId(ByteString.copyFrom(channel.id.getBytes()))
|
||||
.setContractTransaction(ByteString.copyFrom(channel.contract.bitcoinSerialize()))
|
||||
.setRefundTransaction(ByteString.copyFrom(channel.refund.bitcoinSerialize()))
|
||||
.setMyKey(ByteString.copyFrom(channel.myKey.getPrivKeyBytes()))
|
||||
.setMyKey(ByteString.copyFrom(new byte[0])) // Not used, but protobuf message requires
|
||||
.setMyPublicKey(ByteString.copyFrom(channel.myKey.getPubKey()))
|
||||
.setValueToMe(channel.valueToMe.value)
|
||||
.setRefundFees(channel.refundFees.value);
|
||||
if (channel.close != null)
|
||||
@ -294,10 +295,13 @@ public class StoredPaymentChannelClientStates implements WalletExtension {
|
||||
for (ClientState.StoredClientPaymentChannel storedState : states.getChannelsList()) {
|
||||
Transaction refundTransaction = new Transaction(params, storedState.getRefundTransaction().toByteArray());
|
||||
refundTransaction.getConfidence().setSource(TransactionConfidence.Source.SELF);
|
||||
ECKey myKey = (storedState.getMyKey().isEmpty()) ?
|
||||
containingWallet.findKeyFromPubKey(storedState.getMyPublicKey().toByteArray()) :
|
||||
ECKey.fromPrivate(storedState.getMyKey().toByteArray());
|
||||
StoredClientChannel channel = new StoredClientChannel(new Sha256Hash(storedState.getId().toByteArray()),
|
||||
new Transaction(params, storedState.getContractTransaction().toByteArray()),
|
||||
refundTransaction,
|
||||
ECKey.fromPrivate(storedState.getMyKey().toByteArray()),
|
||||
myKey,
|
||||
Coin.valueOf(storedState.getValueToMe()),
|
||||
Coin.valueOf(storedState.getRefundFees()), false);
|
||||
if (storedState.hasCloseTransactionHash()) {
|
||||
|
@ -51,6 +51,15 @@ import static com.google.common.base.Preconditions.*;
|
||||
*/
|
||||
public class Script {
|
||||
|
||||
/** Enumeration to encapsulate the type of this script. */
|
||||
public enum ScriptType {
|
||||
// Do NOT change the ordering of the following definitions because their ordinals are stored in databases.
|
||||
NO_TYPE,
|
||||
P2PKH,
|
||||
PUB_KEY,
|
||||
P2SH
|
||||
};
|
||||
|
||||
/** Flags to pass to {@link Script#correctlySpends(Transaction, long, Script, Set)}. */
|
||||
public enum VerifyFlag {
|
||||
P2SH, // Enable BIP16-style subscript evaluation.
|
||||
@ -243,10 +252,16 @@ public class Script {
|
||||
}
|
||||
|
||||
/**
|
||||
* If a program matches the standard template DUP HASH160 <pubkey hash> EQUALVERIFY CHECKSIG
|
||||
* then this function retrieves the third element, otherwise it throws a ScriptException.<p>
|
||||
* <p>If a program matches the standard template DUP HASH160 <pubkey hash> EQUALVERIFY CHECKSIG
|
||||
* then this function retrieves the third element.
|
||||
* In this case, this is useful for fetching the destination address of a transaction.</p>
|
||||
*
|
||||
* <p>If a program matches the standard template HASH160 <script hash> EQUAL
|
||||
* then this function retrieves the second element.
|
||||
* In this case, this is useful for fetching the hash of the redeem script of a transaction.</p>
|
||||
*
|
||||
* <p>Otherwise it throws a ScriptException.</p>
|
||||
*
|
||||
* This is useful for fetching the destination address of a transaction.
|
||||
*/
|
||||
public byte[] getPubKeyHash() throws ScriptException {
|
||||
if (isSentToAddress())
|
||||
@ -725,6 +740,10 @@ public class Script {
|
||||
return Utils.decodeMPI(Utils.reverseBytes(chunk), false);
|
||||
}
|
||||
|
||||
public boolean isOpReturn() {
|
||||
return chunks.size() == 2 && chunks.get(0).equalsOpCode(OP_RETURN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exposes the script interpreter. Normally you should not use this directly, instead use
|
||||
* {@link com.dogecoin.dogecoinj.core.TransactionInput#verify(com.dogecoin.dogecoinj.core.TransactionOutput)} or
|
||||
@ -1448,6 +1467,22 @@ public class Script {
|
||||
return getProgram();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link com.dogecoin.dogecoinj.script.Script.ScriptType}.
|
||||
* @return The script type.
|
||||
*/
|
||||
public ScriptType getScriptType() {
|
||||
ScriptType type = ScriptType.NO_TYPE;
|
||||
if (isSentToAddress()) {
|
||||
type = ScriptType.P2PKH;
|
||||
} else if (isSentToRawPubKey()) {
|
||||
type = ScriptType.PUB_KEY;
|
||||
} else if (isPayToScriptHash()) {
|
||||
type = ScriptType.P2SH;
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
|
@ -331,4 +331,14 @@ public class ScriptBuilder {
|
||||
Collections.sort(pubkeys, ECKey.PUBKEY_COMPARATOR);
|
||||
return ScriptBuilder.createMultiSigOutputScript(threshold, pubkeys);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a script of the form OP_RETURN [data]. This feature allows you to attach a small piece of data (like
|
||||
* a hash of something stored elsewhere) to a zero valued output which can never be spent and thus does not pollute
|
||||
* the ledger.
|
||||
*/
|
||||
public static Script createOpReturnScript(byte[] data) {
|
||||
checkArgument(data.length <= 40);
|
||||
return new ScriptBuilder().op(OP_RETURN).data(data).build();
|
||||
}
|
||||
}
|
||||
|
@ -81,13 +81,15 @@ public class ScriptChunk {
|
||||
*/
|
||||
public boolean isShortestPossiblePushData() {
|
||||
checkState(isPushData());
|
||||
if (data == null)
|
||||
return true; // OP_N
|
||||
if (data.length == 0)
|
||||
return opcode == OP_0;
|
||||
if (data.length == 1) {
|
||||
byte b = data[0];
|
||||
if (b >= 0x01 && b <= 0x10)
|
||||
return opcode == OP_1 + b - 1;
|
||||
if (b == 0x81)
|
||||
if ((b & 0xFF) == 0x81)
|
||||
return opcode == OP_1NEGATE;
|
||||
}
|
||||
if (data.length < OP_PUSHDATA1)
|
||||
@ -106,7 +108,6 @@ public class ScriptChunk {
|
||||
checkState(data == null);
|
||||
stream.write(opcode);
|
||||
} else if (data != null) {
|
||||
checkNotNull(data);
|
||||
if (opcode < OP_PUSHDATA1) {
|
||||
checkState(data.length == opcode);
|
||||
stream.write(opcode);
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
package com.dogecoin.dogecoinj.store;
|
||||
|
||||
import com.dogecoin.dogecoinj.core.NetworkParameters;
|
||||
import com.dogecoin.dogecoinj.core.Sha256Hash;
|
||||
import com.dogecoin.dogecoinj.core.StoredBlock;
|
||||
|
||||
@ -58,4 +59,10 @@ public interface BlockStore {
|
||||
|
||||
/** Closes the store. */
|
||||
void close() throws BlockStoreException;
|
||||
|
||||
/**
|
||||
* Get the {@link com.dogecoin.dogecoinj.core.NetworkParameters} of this store.
|
||||
* @return The network params.
|
||||
*/
|
||||
NetworkParameters getParams();
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -16,10 +16,7 @@
|
||||
|
||||
package com.dogecoin.dogecoinj.store;
|
||||
|
||||
import com.dogecoin.dogecoinj.core.Sha256Hash;
|
||||
import com.dogecoin.dogecoinj.core.StoredBlock;
|
||||
import com.dogecoin.dogecoinj.core.StoredTransactionOutput;
|
||||
import com.dogecoin.dogecoinj.core.StoredUndoableBlock;
|
||||
import com.dogecoin.dogecoinj.core.*;
|
||||
|
||||
/**
|
||||
* <p>An implementor of FullPrunedBlockStore saves StoredBlock objects to some storage mechanism.</p>
|
||||
@ -43,12 +40,12 @@ import com.dogecoin.dogecoinj.core.StoredUndoableBlock;
|
||||
* <p>A FullPrunedBlockStore contains a map of hashes to [Full]StoredBlock. The hash is the double digest of the
|
||||
* Bitcoin serialization of the block header, <b>not</b> the header with the extra data as well.</p>
|
||||
*
|
||||
* <p>A FullPrunedBlockStore also contains a map of hash+index to StoredTransactionOutput. Again, the hash is
|
||||
* <p>A FullPrunedBlockStore also contains a map of hash+index to UTXO. Again, the hash is
|
||||
* a standard Bitcoin double-SHA256 hash of the transaction.</p>
|
||||
*
|
||||
* <p>FullPrunedBlockStores are thread safe.</p>
|
||||
*/
|
||||
public interface FullPrunedBlockStore extends BlockStore {
|
||||
public interface FullPrunedBlockStore extends BlockStore, UTXOProvider {
|
||||
/**
|
||||
* <p>Saves the given {@link StoredUndoableBlock} and {@link StoredBlock}. Calculates keys from the {@link StoredBlock}</p>
|
||||
*
|
||||
@ -74,21 +71,21 @@ public interface FullPrunedBlockStore extends BlockStore {
|
||||
StoredUndoableBlock getUndoBlock(Sha256Hash hash) throws BlockStoreException;
|
||||
|
||||
/**
|
||||
* Gets a {@link StoredTransactionOutput} with the given hash and index, or null if none is found
|
||||
* Gets a {@link com.dogecoin.dogecoinj.core.UTXO} with the given hash and index, or null if none is found
|
||||
*/
|
||||
StoredTransactionOutput getTransactionOutput(Sha256Hash hash, long index) throws BlockStoreException;
|
||||
UTXO getTransactionOutput(Sha256Hash hash, long index) throws BlockStoreException;
|
||||
|
||||
/**
|
||||
* Adds a {@link StoredTransactionOutput} to the list of unspent TransactionOutputs
|
||||
* Adds a {@link com.dogecoin.dogecoinj.core.UTXO} to the list of unspent TransactionOutputs
|
||||
*/
|
||||
void addUnspentTransactionOutput(StoredTransactionOutput out) throws BlockStoreException;
|
||||
void addUnspentTransactionOutput(UTXO out) throws BlockStoreException;
|
||||
|
||||
/**
|
||||
* Removes a {@link StoredTransactionOutput} from the list of unspent TransactionOutputs
|
||||
* Removes a {@link com.dogecoin.dogecoinj.core.UTXO} from the list of unspent TransactionOutputs
|
||||
* Note that the coinbase of the genesis block should NEVER be spendable and thus never in the list.
|
||||
* @throws BlockStoreException if there is an underlying storage issue, or out was not in the list.
|
||||
*/
|
||||
void removeUnspentTransactionOutput(StoredTransactionOutput out) throws BlockStoreException;
|
||||
void removeUnspentTransactionOutput(UTXO out) throws BlockStoreException;
|
||||
|
||||
/**
|
||||
* True if this store has any unspent outputs from a transaction with a hash equal to the first parameter
|
||||
@ -109,7 +106,7 @@ public interface FullPrunedBlockStore extends BlockStore {
|
||||
* before a call to commitDatabaseBatchWrite.
|
||||
*
|
||||
* If chainHead has a greater height than the non-verified chain head (ie that set with
|
||||
* {@link BlockStore.setChainHead}) the non-verified chain head should be set to the one set here.
|
||||
* {@link BlockStore#setChainHead}) the non-verified chain head should be set to the one set here.
|
||||
* In this way a class using a FullPrunedBlockStore only in full-verification mode can ignore the regular
|
||||
* {@link BlockStore} functions implemented as a part of a FullPrunedBlockStore.
|
||||
*/
|
||||
|
@ -1,5 +1,6 @@
|
||||
/*
|
||||
* Copyright 2012 Matt Corallo.
|
||||
* Copyright 2014 Kalpesh Parmar.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -17,17 +18,13 @@
|
||||
package com.dogecoin.dogecoinj.store;
|
||||
|
||||
import com.dogecoin.dogecoinj.core.*;
|
||||
import com.google.common.collect.Lists;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.sql.*;
|
||||
import java.util.LinkedList;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
// Originally written for Apache Derby, but its DELETE (and general) performance was awful
|
||||
@ -39,52 +36,52 @@ import java.util.List;
|
||||
* H2 automatically frees some space at shutdown, so close()ing the database
|
||||
* decreases the space usage somewhat (to only around 1.3G).
|
||||
*/
|
||||
public class H2FullPrunedBlockStore implements FullPrunedBlockStore {
|
||||
public class H2FullPrunedBlockStore extends DatabaseFullPrunedBlockStore {
|
||||
private static final Logger log = LoggerFactory.getLogger(H2FullPrunedBlockStore.class);
|
||||
|
||||
private Sha256Hash chainHeadHash;
|
||||
private StoredBlock chainHeadBlock;
|
||||
private Sha256Hash verifiedChainHeadHash;
|
||||
private StoredBlock verifiedChainHeadBlock;
|
||||
private NetworkParameters params;
|
||||
private ThreadLocal<Connection> conn;
|
||||
private List<Connection> allConnections;
|
||||
private String connectionURL;
|
||||
private int fullStoreDepth;
|
||||
private static final String H2_DUPLICATE_KEY_ERROR_CODE = "23505";
|
||||
private static final String DATABASE_DRIVER_CLASS = "org.h2.Driver";
|
||||
private static final String DATABASE_CONNECTION_URL_PREFIX = "jdbc:h2:";
|
||||
|
||||
static final String driver = "org.h2.Driver";
|
||||
static final String CREATE_SETTINGS_TABLE = "CREATE TABLE settings ( "
|
||||
+ "name VARCHAR(32) NOT NULL CONSTRAINT settings_pk PRIMARY KEY,"
|
||||
+ "value BLOB"
|
||||
+ ")";
|
||||
static final String CHAIN_HEAD_SETTING = "chainhead";
|
||||
static final String VERIFIED_CHAIN_HEAD_SETTING = "verifiedchainhead";
|
||||
static final String VERSION_SETTING = "version";
|
||||
// create table SQL
|
||||
private static final String CREATE_SETTINGS_TABLE = "CREATE TABLE settings ( "
|
||||
+ "name VARCHAR(32) NOT NULL CONSTRAINT settings_pk PRIMARY KEY,"
|
||||
+ "value BLOB"
|
||||
+ ")";
|
||||
|
||||
static final String CREATE_HEADERS_TABLE = "CREATE TABLE headers ( "
|
||||
+ "hash BINARY(28) NOT NULL CONSTRAINT headers_pk PRIMARY KEY,"
|
||||
+ "chainWork BLOB NOT NULL,"
|
||||
+ "height INT NOT NULL,"
|
||||
+ "header BLOB NOT NULL,"
|
||||
+ "wasUndoable BOOL NOT NULL"
|
||||
+ ")";
|
||||
private static final String CREATE_HEADERS_TABLE = "CREATE TABLE headers ( "
|
||||
+ "hash BINARY(28) NOT NULL CONSTRAINT headers_pk PRIMARY KEY,"
|
||||
+ "chainWork BLOB NOT NULL,"
|
||||
+ "height INT NOT NULL,"
|
||||
+ "header BLOB NOT NULL,"
|
||||
+ "wasUndoable BOOL NOT NULL"
|
||||
+ ")";
|
||||
|
||||
static final String CREATE_UNDOABLE_TABLE = "CREATE TABLE undoableBlocks ( "
|
||||
+ "hash BINARY(28) NOT NULL CONSTRAINT undoableBlocks_pk PRIMARY KEY,"
|
||||
+ "height INT NOT NULL,"
|
||||
+ "txOutChanges BLOB,"
|
||||
+ "transactions BLOB"
|
||||
+ ")";
|
||||
static final String CREATE_UNDOABLE_TABLE_INDEX = "CREATE INDEX heightIndex ON undoableBlocks (height)";
|
||||
private static final String CREATE_UNDOABLE_TABLE = "CREATE TABLE undoableBlocks ( "
|
||||
+ "hash BINARY(28) NOT NULL CONSTRAINT undoableBlocks_pk PRIMARY KEY,"
|
||||
+ "height INT NOT NULL,"
|
||||
+ "txOutChanges BLOB,"
|
||||
+ "transactions BLOB"
|
||||
+ ")";
|
||||
|
||||
static final String CREATE_OPEN_OUTPUT_TABLE = "CREATE TABLE openOutputs ("
|
||||
+ "hash BINARY(32) NOT NULL,"
|
||||
+ "index INT NOT NULL,"
|
||||
+ "height INT NOT NULL,"
|
||||
+ "value BLOB NOT NULL,"
|
||||
+ "scriptBytes BLOB NOT NULL,"
|
||||
+ "PRIMARY KEY (hash, index),"
|
||||
+ ")";
|
||||
private static final String CREATE_OPEN_OUTPUT_TABLE = "CREATE TABLE openOutputs ("
|
||||
+ "hash BINARY(32) NOT NULL,"
|
||||
+ "index INT NOT NULL,"
|
||||
+ "height INT NOT NULL,"
|
||||
+ "value BIGINT NOT NULL,"
|
||||
+ "scriptBytes BLOB NOT NULL,"
|
||||
+ "toaddress VARCHAR(35),"
|
||||
+ "addresstargetable TINYINT,"
|
||||
+ "coinbase BOOLEAN,"
|
||||
+ "PRIMARY KEY (hash, index),"
|
||||
+ ")";
|
||||
|
||||
// Some indexes to speed up inserts
|
||||
private static final String CREATE_OUTPUTS_ADDRESS_MULTI_INDEX = "CREATE INDEX openoutputs_hash_index_height_toaddress_idx ON openoutputs (hash, index, height, toaddress)";
|
||||
private static final String CREATE_OUTPUTS_TOADDRESS_INDEX = "CREATE INDEX openoutputs_toaddress_idx ON openoutputs (toaddress)";
|
||||
private static final String CREATE_OUTPUTS_ADDRESSTARGETABLE_INDEX = "CREATE INDEX openoutputs_addresstargetable_idx ON openoutputs (addresstargetable)";
|
||||
private static final String CREATE_OUTPUTS_HASH_INDEX = "CREATE INDEX openoutputs_hash_idx ON openoutputs (hash)";
|
||||
private static final String CREATE_UNDOABLE_TABLE_INDEX = "CREATE INDEX undoableblocks_height_idx ON undoableBlocks (height)";
|
||||
|
||||
/**
|
||||
* Creates a new H2FullPrunedBlockStore
|
||||
@ -94,32 +91,7 @@ public class H2FullPrunedBlockStore implements FullPrunedBlockStore {
|
||||
* @throws BlockStoreException if the database fails to open for any reason
|
||||
*/
|
||||
public H2FullPrunedBlockStore(NetworkParameters params, String dbName, int fullStoreDepth) throws BlockStoreException {
|
||||
this.params = params;
|
||||
this.fullStoreDepth = fullStoreDepth;
|
||||
// We choose a very lax timeout to avoid the database throwing exceptions on complex operations, as time is not
|
||||
// a particularly precious resource when just keeping up with the chain.
|
||||
connectionURL = "jdbc:h2:" + dbName + ";create=true;LOCK_TIMEOUT=60000";
|
||||
|
||||
conn = new ThreadLocal<Connection>();
|
||||
allConnections = new LinkedList<Connection>();
|
||||
|
||||
try {
|
||||
Class.forName(driver);
|
||||
log.info(driver + " loaded. ");
|
||||
} catch (java.lang.ClassNotFoundException e) {
|
||||
log.error("check CLASSPATH for H2 jar ", e);
|
||||
}
|
||||
|
||||
maybeConnect();
|
||||
|
||||
try {
|
||||
// Create tables if needed
|
||||
if (!tableExists("settings"))
|
||||
createTables();
|
||||
initFromDatabase();
|
||||
} catch (SQLException e) {
|
||||
throw new BlockStoreException(e);
|
||||
}
|
||||
super(params, DATABASE_CONNECTION_URL_PREFIX + dbName + ";create=true;LOCK_TIMEOUT=60000;DB_CLOSE_ON_EXIT=FALSE", fullStoreDepth, null, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -134,7 +106,6 @@ public class H2FullPrunedBlockStore implements FullPrunedBlockStore {
|
||||
*/
|
||||
public H2FullPrunedBlockStore(NetworkParameters params, String dbName, int fullStoreDepth, int cacheSize) throws BlockStoreException {
|
||||
this(params, dbName, fullStoreDepth);
|
||||
|
||||
try {
|
||||
Statement s = conn.get().createStatement();
|
||||
s.executeUpdate("SET CACHE_SIZE " + cacheSize);
|
||||
@ -144,635 +115,40 @@ public class H2FullPrunedBlockStore implements FullPrunedBlockStore {
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void maybeConnect() throws BlockStoreException {
|
||||
try {
|
||||
if (conn.get() != null)
|
||||
return;
|
||||
|
||||
conn.set(DriverManager.getConnection(connectionURL));
|
||||
allConnections.add(conn.get());
|
||||
log.info("Made a new connection to database " + connectionURL);
|
||||
} catch (SQLException ex) {
|
||||
throw new BlockStoreException(ex);
|
||||
}
|
||||
@Override
|
||||
protected String getDuplicateKeyErrorCode() {
|
||||
return H2_DUPLICATE_KEY_ERROR_CODE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void close() {
|
||||
for (Connection conn : allConnections) {
|
||||
try {
|
||||
conn.rollback();
|
||||
conn.close();
|
||||
} catch (SQLException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
allConnections.clear();
|
||||
}
|
||||
|
||||
public void resetStore() throws BlockStoreException {
|
||||
maybeConnect();
|
||||
try {
|
||||
Statement s = conn.get().createStatement();
|
||||
s.executeUpdate("DROP TABLE settings");
|
||||
s.executeUpdate("DROP TABLE headers");
|
||||
s.executeUpdate("DROP TABLE undoableBlocks");
|
||||
s.executeUpdate("DROP TABLE openOutputs");
|
||||
s.close();
|
||||
createTables();
|
||||
initFromDatabase();
|
||||
} catch (SQLException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void createTables() throws SQLException, BlockStoreException {
|
||||
Statement s = conn.get().createStatement();
|
||||
log.debug("H2FullPrunedBlockStore : CREATE headers table");
|
||||
s.executeUpdate(CREATE_HEADERS_TABLE);
|
||||
|
||||
log.debug("H2FullPrunedBlockStore : CREATE settings table");
|
||||
s.executeUpdate(CREATE_SETTINGS_TABLE);
|
||||
|
||||
log.debug("H2FullPrunedBlockStore : CREATE undoable block table");
|
||||
s.executeUpdate(CREATE_UNDOABLE_TABLE);
|
||||
|
||||
log.debug("H2FullPrunedBlockStore : CREATE undoable block index");
|
||||
s.executeUpdate(CREATE_UNDOABLE_TABLE_INDEX);
|
||||
|
||||
log.debug("H2FullPrunedBlockStore : CREATE open output table");
|
||||
s.executeUpdate(CREATE_OPEN_OUTPUT_TABLE);
|
||||
|
||||
s.executeUpdate("INSERT INTO settings(name, value) VALUES('" + CHAIN_HEAD_SETTING + "', NULL)");
|
||||
s.executeUpdate("INSERT INTO settings(name, value) VALUES('" + VERIFIED_CHAIN_HEAD_SETTING + "', NULL)");
|
||||
s.executeUpdate("INSERT INTO settings(name, value) VALUES('" + VERSION_SETTING + "', '03')");
|
||||
s.close();
|
||||
createNewStore(params);
|
||||
}
|
||||
|
||||
private void initFromDatabase() throws SQLException, BlockStoreException {
|
||||
Statement s = conn.get().createStatement();
|
||||
ResultSet rs = s.executeQuery("SHOW TABLES");
|
||||
while (rs.next())
|
||||
if (rs.getString(1).equalsIgnoreCase("openOutputsIndex"))
|
||||
throw new BlockStoreException("Attempted to open a H2 database with an old schema, please reset database.");
|
||||
|
||||
rs = s.executeQuery("SELECT value FROM settings WHERE name = '" + CHAIN_HEAD_SETTING + "'");
|
||||
if (!rs.next()) {
|
||||
throw new BlockStoreException("corrupt H2 block store - no chain head pointer");
|
||||
}
|
||||
Sha256Hash hash = new Sha256Hash(rs.getBytes(1));
|
||||
rs.close();
|
||||
this.chainHeadBlock = get(hash);
|
||||
this.chainHeadHash = hash;
|
||||
if (this.chainHeadBlock == null)
|
||||
{
|
||||
throw new BlockStoreException("corrupt H2 block store - head block not found");
|
||||
}
|
||||
|
||||
rs = s.executeQuery("SELECT value FROM settings WHERE name = '" + VERIFIED_CHAIN_HEAD_SETTING + "'");
|
||||
if (!rs.next()) {
|
||||
throw new BlockStoreException("corrupt H2 block store - no verified chain head pointer");
|
||||
}
|
||||
hash = new Sha256Hash(rs.getBytes(1));
|
||||
rs.close();
|
||||
s.close();
|
||||
this.verifiedChainHeadBlock = get(hash);
|
||||
this.verifiedChainHeadHash = hash;
|
||||
if (this.verifiedChainHeadBlock == null)
|
||||
{
|
||||
throw new BlockStoreException("corrupt H2 block store - verified head block not found");
|
||||
}
|
||||
}
|
||||
|
||||
private void createNewStore(NetworkParameters params) throws BlockStoreException {
|
||||
try {
|
||||
// Set up the genesis block. When we start out fresh, it is by
|
||||
// definition the top of the chain.
|
||||
StoredBlock storedGenesisHeader = new StoredBlock(params.getGenesisBlock().cloneAsHeader(), params.getGenesisBlock().getWork(), 0);
|
||||
// The coinbase in the genesis block is not spendable. This is because of how the reference client inits
|
||||
// its database - the genesis transaction isn't actually in the db so its spent flags can never be updated.
|
||||
List<Transaction> genesisTransactions = Lists.newLinkedList();
|
||||
StoredUndoableBlock storedGenesis = new StoredUndoableBlock(params.getGenesisBlock().getHash(), genesisTransactions);
|
||||
put(storedGenesisHeader, storedGenesis);
|
||||
setChainHead(storedGenesisHeader);
|
||||
setVerifiedChainHead(storedGenesisHeader);
|
||||
} catch (VerificationException e) {
|
||||
throw new RuntimeException(e); // Cannot happen.
|
||||
}
|
||||
}
|
||||
|
||||
private boolean tableExists(String table) throws SQLException {
|
||||
Statement s = conn.get().createStatement();
|
||||
try {
|
||||
ResultSet results = s.executeQuery("SELECT * FROM " + table + " WHERE 1 = 2");
|
||||
results.close();
|
||||
return true;
|
||||
} catch (SQLException ex) {
|
||||
return false;
|
||||
} finally {
|
||||
s.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dumps information about the size of actual data in the database to standard output
|
||||
* The only truly useless data counted is printed in the form "N in id indexes"
|
||||
* This does not take database indexes into account
|
||||
*/
|
||||
public void dumpSizes() throws SQLException, BlockStoreException {
|
||||
maybeConnect();
|
||||
Statement s = conn.get().createStatement();
|
||||
long size = 0;
|
||||
long totalSize = 0;
|
||||
int count = 0;
|
||||
ResultSet rs = s.executeQuery("SELECT name, value FROM settings");
|
||||
while (rs.next()) {
|
||||
size += rs.getString(1).length();
|
||||
size += rs.getBytes(2).length;
|
||||
count++;
|
||||
}
|
||||
rs.close();
|
||||
System.out.printf("Settings size: %d, count: %d, average size: %f%n", size, count, (double)size/count);
|
||||
|
||||
totalSize += size; size = 0; count = 0;
|
||||
rs = s.executeQuery("SELECT chainWork, header FROM headers");
|
||||
while (rs.next()) {
|
||||
size += 28; // hash
|
||||
size += rs.getBytes(1).length;
|
||||
size += 4; // height
|
||||
size += rs.getBytes(2).length;
|
||||
count++;
|
||||
}
|
||||
rs.close();
|
||||
System.out.printf("Headers size: %d, count: %d, average size: %f%n", size, count, (double)size/count);
|
||||
|
||||
totalSize += size; size = 0; count = 0;
|
||||
rs = s.executeQuery("SELECT txOutChanges, transactions FROM undoableBlocks");
|
||||
while (rs.next()) {
|
||||
size += 28; // hash
|
||||
size += 4; // height
|
||||
byte[] txOutChanges = rs.getBytes(1);
|
||||
byte[] transactions = rs.getBytes(2);
|
||||
if (txOutChanges == null)
|
||||
size += transactions.length;
|
||||
else
|
||||
size += txOutChanges.length;
|
||||
// size += the space to represent NULL
|
||||
count++;
|
||||
}
|
||||
rs.close();
|
||||
System.out.printf("Undoable Blocks size: %d, count: %d, average size: %f%n", size, count, (double)size/count);
|
||||
|
||||
totalSize += size; size = 0; count = 0;
|
||||
long scriptSize = 0;
|
||||
rs = s.executeQuery("SELECT value, scriptBytes FROM openOutputs");
|
||||
while (rs.next()) {
|
||||
size += 32; // hash
|
||||
size += 4; // index
|
||||
size += 4; // height
|
||||
size += rs.getBytes(1).length;
|
||||
size += rs.getBytes(2).length;
|
||||
scriptSize += rs.getBytes(2).length;
|
||||
count++;
|
||||
}
|
||||
rs.close();
|
||||
System.out.printf("Open Outputs size: %d, count: %d, average size: %f, average script size: %f (%d in id indexes)%n",
|
||||
size, count, (double)size/count, (double)scriptSize/count, count * 8);
|
||||
|
||||
totalSize += size;
|
||||
System.out.println("Total Size: " + totalSize);
|
||||
|
||||
s.close();
|
||||
}
|
||||
|
||||
|
||||
private void putUpdateStoredBlock(StoredBlock storedBlock, boolean wasUndoable) throws SQLException {
|
||||
try {
|
||||
PreparedStatement s =
|
||||
conn.get().prepareStatement("INSERT INTO headers(hash, chainWork, height, header, wasUndoable)"
|
||||
+ " VALUES(?, ?, ?, ?, ?)");
|
||||
// We skip the first 4 bytes because (on prodnet) the minimum target has 4 0-bytes
|
||||
byte[] hashBytes = new byte[28];
|
||||
System.arraycopy(storedBlock.getHeader().getHash().getBytes(), 3, hashBytes, 0, 28);
|
||||
s.setBytes(1, hashBytes);
|
||||
s.setBytes(2, storedBlock.getChainWork().toByteArray());
|
||||
s.setInt(3, storedBlock.getHeight());
|
||||
s.setBytes(4, storedBlock.getHeader().unsafeBitcoinSerialize());
|
||||
s.setBoolean(5, wasUndoable);
|
||||
s.executeUpdate();
|
||||
s.close();
|
||||
} catch (SQLException e) {
|
||||
// It is possible we try to add a duplicate StoredBlock if we upgraded
|
||||
// In that case, we just update the entry to mark it wasUndoable
|
||||
if (e.getErrorCode() != 23505 || !wasUndoable)
|
||||
throw e;
|
||||
|
||||
PreparedStatement s = conn.get().prepareStatement("UPDATE headers SET wasUndoable=? WHERE hash=?");
|
||||
s.setBoolean(1, true);
|
||||
// We skip the first 4 bytes because (on prodnet) the minimum target has 4 0-bytes
|
||||
byte[] hashBytes = new byte[28];
|
||||
System.arraycopy(storedBlock.getHeader().getHash().getBytes(), 3, hashBytes, 0, 28);
|
||||
s.setBytes(2, hashBytes);
|
||||
s.executeUpdate();
|
||||
s.close();
|
||||
}
|
||||
protected List<String> getCreateTablesSQL() {
|
||||
List<String> sqlStatements = new ArrayList<String>();
|
||||
sqlStatements.add(CREATE_SETTINGS_TABLE);
|
||||
sqlStatements.add(CREATE_HEADERS_TABLE);
|
||||
sqlStatements.add(CREATE_UNDOABLE_TABLE);
|
||||
sqlStatements.add(CREATE_OPEN_OUTPUT_TABLE);
|
||||
return sqlStatements;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void put(StoredBlock storedBlock) throws BlockStoreException {
|
||||
maybeConnect();
|
||||
try {
|
||||
putUpdateStoredBlock(storedBlock, false);
|
||||
} catch (SQLException e) {
|
||||
throw new BlockStoreException(e);
|
||||
}
|
||||
protected List<String> getCreateIndexesSQL() {
|
||||
List<String> sqlStatements = new ArrayList<String>();
|
||||
sqlStatements.add(CREATE_UNDOABLE_TABLE_INDEX);
|
||||
sqlStatements.add(CREATE_OUTPUTS_ADDRESS_MULTI_INDEX);
|
||||
sqlStatements.add(CREATE_OUTPUTS_ADDRESSTARGETABLE_INDEX);
|
||||
sqlStatements.add(CREATE_OUTPUTS_HASH_INDEX);
|
||||
sqlStatements.add(CREATE_OUTPUTS_TOADDRESS_INDEX);
|
||||
return sqlStatements;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void put(StoredBlock storedBlock, StoredUndoableBlock undoableBlock) throws BlockStoreException {
|
||||
maybeConnect();
|
||||
// We skip the first 4 bytes because (on prodnet) the minimum target has 4 0-bytes
|
||||
byte[] hashBytes = new byte[28];
|
||||
System.arraycopy(storedBlock.getHeader().getHash().getBytes(), 3, hashBytes, 0, 28);
|
||||
int height = storedBlock.getHeight();
|
||||
byte[] transactions = null;
|
||||
byte[] txOutChanges = null;
|
||||
try {
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
if (undoableBlock.getTxOutChanges() != null) {
|
||||
undoableBlock.getTxOutChanges().serializeToStream(bos);
|
||||
txOutChanges = bos.toByteArray();
|
||||
} else {
|
||||
int numTxn = undoableBlock.getTransactions().size();
|
||||
bos.write((int) (0xFF & (numTxn >> 0)));
|
||||
bos.write((int) (0xFF & (numTxn >> 8)));
|
||||
bos.write((int) (0xFF & (numTxn >> 16)));
|
||||
bos.write((int) (0xFF & (numTxn >> 24)));
|
||||
for (Transaction tx : undoableBlock.getTransactions())
|
||||
tx.bitcoinSerialize(bos);
|
||||
transactions = bos.toByteArray();
|
||||
}
|
||||
bos.close();
|
||||
} catch (IOException e) {
|
||||
throw new BlockStoreException(e);
|
||||
}
|
||||
|
||||
try {
|
||||
try {
|
||||
PreparedStatement s =
|
||||
conn.get().prepareStatement("INSERT INTO undoableBlocks(hash, height, txOutChanges, transactions)"
|
||||
+ " VALUES(?, ?, ?, ?)");
|
||||
s.setBytes(1, hashBytes);
|
||||
s.setInt(2, height);
|
||||
if (transactions == null) {
|
||||
s.setBytes(3, txOutChanges);
|
||||
s.setNull(4, Types.BLOB);
|
||||
} else {
|
||||
s.setNull(3, Types.BLOB);
|
||||
s.setBytes(4, transactions);
|
||||
}
|
||||
s.executeUpdate();
|
||||
s.close();
|
||||
try {
|
||||
putUpdateStoredBlock(storedBlock, true);
|
||||
} catch (SQLException e) {
|
||||
throw new BlockStoreException(e);
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
if (e.getErrorCode() != 23505)
|
||||
throw new BlockStoreException(e);
|
||||
|
||||
// There is probably an update-or-insert statement, but it wasn't obvious from the docs
|
||||
PreparedStatement s =
|
||||
conn.get().prepareStatement("UPDATE undoableBlocks SET txOutChanges=?, transactions=?"
|
||||
+ " WHERE hash = ?");
|
||||
s.setBytes(3, hashBytes);
|
||||
if (transactions == null) {
|
||||
s.setBytes(1, txOutChanges);
|
||||
s.setNull(2, Types.BLOB);
|
||||
} else {
|
||||
s.setNull(1, Types.BLOB);
|
||||
s.setBytes(2, transactions);
|
||||
}
|
||||
s.executeUpdate();
|
||||
s.close();
|
||||
}
|
||||
} catch (SQLException ex) {
|
||||
throw new BlockStoreException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public StoredBlock get(Sha256Hash hash, boolean wasUndoableOnly) throws BlockStoreException {
|
||||
// Optimize for chain head
|
||||
if (chainHeadHash != null && chainHeadHash.equals(hash))
|
||||
return chainHeadBlock;
|
||||
if (verifiedChainHeadHash != null && verifiedChainHeadHash.equals(hash))
|
||||
return verifiedChainHeadBlock;
|
||||
maybeConnect();
|
||||
PreparedStatement s = null;
|
||||
try {
|
||||
s = conn.get().prepareStatement("SELECT chainWork, height, header, wasUndoable FROM headers WHERE hash = ?");
|
||||
// We skip the first 4 bytes because (on prodnet) the minimum target has 4 0-bytes
|
||||
byte[] hashBytes = new byte[28];
|
||||
System.arraycopy(hash.getBytes(), 3, hashBytes, 0, 28);
|
||||
s.setBytes(1, hashBytes);
|
||||
ResultSet results = s.executeQuery();
|
||||
if (!results.next()) {
|
||||
return null;
|
||||
}
|
||||
// Parse it.
|
||||
if (wasUndoableOnly && !results.getBoolean(4))
|
||||
return null;
|
||||
BigInteger chainWork = new BigInteger(results.getBytes(1));
|
||||
int height = results.getInt(2);
|
||||
Block b = new Block(params, results.getBytes(3));
|
||||
b.verifyHeader();
|
||||
return new StoredBlock(b, chainWork, height);
|
||||
} catch (SQLException ex) {
|
||||
throw new BlockStoreException(ex);
|
||||
} catch (ProtocolException e) {
|
||||
// Corrupted database.
|
||||
throw new BlockStoreException(e);
|
||||
} catch (VerificationException e) {
|
||||
// Should not be able to happen unless the database contains bad
|
||||
// blocks.
|
||||
throw new BlockStoreException(e);
|
||||
} finally {
|
||||
if (s != null) {
|
||||
try {
|
||||
s.close();
|
||||
} catch (SQLException e) {
|
||||
throw new BlockStoreException("Failed to close PreparedStatement");
|
||||
}
|
||||
}
|
||||
}
|
||||
protected List<String> getCreateSchemeSQL() {
|
||||
// do nothing
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public StoredBlock get(Sha256Hash hash) throws BlockStoreException {
|
||||
return get(hash, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public StoredBlock getOnceUndoableStoredBlock(Sha256Hash hash) throws BlockStoreException {
|
||||
return get(hash, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public StoredUndoableBlock getUndoBlock(Sha256Hash hash) throws BlockStoreException {
|
||||
maybeConnect();
|
||||
PreparedStatement s = null;
|
||||
try {
|
||||
s = conn.get()
|
||||
.prepareStatement("SELECT txOutChanges, transactions FROM undoableBlocks WHERE hash = ?");
|
||||
// We skip the first 4 bytes because (on prodnet) the minimum target has 4 0-bytes
|
||||
byte[] hashBytes = new byte[28];
|
||||
System.arraycopy(hash.getBytes(), 3, hashBytes, 0, 28);
|
||||
s.setBytes(1, hashBytes);
|
||||
ResultSet results = s.executeQuery();
|
||||
if (!results.next()) {
|
||||
return null;
|
||||
}
|
||||
// Parse it.
|
||||
byte[] txOutChanges = results.getBytes(1);
|
||||
byte[] transactions = results.getBytes(2);
|
||||
StoredUndoableBlock block;
|
||||
if (txOutChanges == null) {
|
||||
int offset = 0;
|
||||
int numTxn = ((transactions[offset++] & 0xFF) << 0) |
|
||||
((transactions[offset++] & 0xFF) << 8) |
|
||||
((transactions[offset++] & 0xFF) << 16) |
|
||||
((transactions[offset++] & 0xFF) << 24);
|
||||
List<Transaction> transactionList = new LinkedList<Transaction>();
|
||||
for (int i = 0; i < numTxn; i++) {
|
||||
Transaction tx = new Transaction(params, transactions, offset);
|
||||
transactionList.add(tx);
|
||||
offset += tx.getMessageSize();
|
||||
}
|
||||
block = new StoredUndoableBlock(hash, transactionList);
|
||||
} else {
|
||||
TransactionOutputChanges outChangesObject =
|
||||
new TransactionOutputChanges(new ByteArrayInputStream(txOutChanges));
|
||||
block = new StoredUndoableBlock(hash, outChangesObject);
|
||||
}
|
||||
return block;
|
||||
} catch (SQLException ex) {
|
||||
throw new BlockStoreException(ex);
|
||||
} catch (NullPointerException e) {
|
||||
// Corrupted database.
|
||||
throw new BlockStoreException(e);
|
||||
} catch (ClassCastException e) {
|
||||
// Corrupted database.
|
||||
throw new BlockStoreException(e);
|
||||
} catch (ProtocolException e) {
|
||||
// Corrupted database.
|
||||
throw new BlockStoreException(e);
|
||||
} catch (IOException e) {
|
||||
// Corrupted database.
|
||||
throw new BlockStoreException(e);
|
||||
} finally {
|
||||
if (s != null)
|
||||
try {
|
||||
s.close();
|
||||
} catch (SQLException e) { throw new BlockStoreException("Failed to close PreparedStatement"); }
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public StoredBlock getChainHead() throws BlockStoreException {
|
||||
return chainHeadBlock;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setChainHead(StoredBlock chainHead) throws BlockStoreException {
|
||||
Sha256Hash hash = chainHead.getHeader().getHash();
|
||||
this.chainHeadHash = hash;
|
||||
this.chainHeadBlock = chainHead;
|
||||
maybeConnect();
|
||||
try {
|
||||
PreparedStatement s = conn.get()
|
||||
.prepareStatement("UPDATE settings SET value = ? WHERE name = ?");
|
||||
s.setString(2, CHAIN_HEAD_SETTING);
|
||||
s.setBytes(1, hash.getBytes());
|
||||
s.executeUpdate();
|
||||
s.close();
|
||||
} catch (SQLException ex) {
|
||||
throw new BlockStoreException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public StoredBlock getVerifiedChainHead() throws BlockStoreException {
|
||||
return verifiedChainHeadBlock;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVerifiedChainHead(StoredBlock chainHead) throws BlockStoreException {
|
||||
Sha256Hash hash = chainHead.getHeader().getHash();
|
||||
this.verifiedChainHeadHash = hash;
|
||||
this.verifiedChainHeadBlock = chainHead;
|
||||
maybeConnect();
|
||||
try {
|
||||
PreparedStatement s = conn.get()
|
||||
.prepareStatement("UPDATE settings SET value = ? WHERE name = ?");
|
||||
s.setString(2, VERIFIED_CHAIN_HEAD_SETTING);
|
||||
s.setBytes(1, hash.getBytes());
|
||||
s.executeUpdate();
|
||||
s.close();
|
||||
} catch (SQLException ex) {
|
||||
throw new BlockStoreException(ex);
|
||||
}
|
||||
if (this.chainHeadBlock.getHeight() < chainHead.getHeight())
|
||||
setChainHead(chainHead);
|
||||
removeUndoableBlocksWhereHeightIsLessThan(chainHead.getHeight() - fullStoreDepth);
|
||||
}
|
||||
|
||||
private void removeUndoableBlocksWhereHeightIsLessThan(int height) throws BlockStoreException {
|
||||
try {
|
||||
PreparedStatement s = conn.get()
|
||||
.prepareStatement("DELETE FROM undoableBlocks WHERE height <= ?");
|
||||
s.setInt(1, height);
|
||||
s.executeUpdate();
|
||||
s.close();
|
||||
} catch (SQLException ex) {
|
||||
throw new BlockStoreException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public StoredTransactionOutput getTransactionOutput(Sha256Hash hash, long index) throws BlockStoreException {
|
||||
maybeConnect();
|
||||
PreparedStatement s = null;
|
||||
try {
|
||||
s = conn.get()
|
||||
.prepareStatement("SELECT height, value, scriptBytes FROM openOutputs " +
|
||||
"WHERE hash = ? AND index = ?");
|
||||
s.setBytes(1, hash.getBytes());
|
||||
// index is actually an unsigned int
|
||||
s.setInt(2, (int)index);
|
||||
ResultSet results = s.executeQuery();
|
||||
if (!results.next()) {
|
||||
return null;
|
||||
}
|
||||
// Parse it.
|
||||
int height = results.getInt(1);
|
||||
Coin value = Coin.valueOf(new BigInteger(results.getBytes(2)).longValue());
|
||||
// Tell the StoredTransactionOutput that we are a coinbase, as that is encoded in height
|
||||
return new StoredTransactionOutput(hash, index, value, height, true, results.getBytes(3));
|
||||
} catch (SQLException ex) {
|
||||
throw new BlockStoreException(ex);
|
||||
} finally {
|
||||
if (s != null)
|
||||
try {
|
||||
s.close();
|
||||
} catch (SQLException e) { throw new BlockStoreException("Failed to close PreparedStatement"); }
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addUnspentTransactionOutput(StoredTransactionOutput out) throws BlockStoreException {
|
||||
maybeConnect();
|
||||
PreparedStatement s = null;
|
||||
try {
|
||||
s = conn.get().prepareStatement("INSERT INTO openOutputs (hash, index, height, value, scriptBytes) " +
|
||||
"VALUES (?, ?, ?, ?, ?)");
|
||||
s.setBytes(1, out.getHash().getBytes());
|
||||
// index is actually an unsigned int
|
||||
s.setInt(2, (int)out.getIndex());
|
||||
s.setInt(3, out.getHeight());
|
||||
s.setBytes(4, BigInteger.valueOf(out.getValue().value).toByteArray());
|
||||
s.setBytes(5, out.getScriptBytes());
|
||||
s.executeUpdate();
|
||||
s.close();
|
||||
} catch (SQLException e) {
|
||||
if (e.getErrorCode() != 23505)
|
||||
throw new BlockStoreException(e);
|
||||
} finally {
|
||||
if (s != null)
|
||||
try {
|
||||
s.close();
|
||||
} catch (SQLException e) { throw new BlockStoreException(e); }
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeUnspentTransactionOutput(StoredTransactionOutput out) throws BlockStoreException {
|
||||
maybeConnect();
|
||||
try {
|
||||
PreparedStatement s = conn.get()
|
||||
.prepareStatement("DELETE FROM openOutputs WHERE hash = ? AND index = ?");
|
||||
s.setBytes(1, out.getHash().getBytes());
|
||||
// index is actually an unsigned int
|
||||
s.setInt(2, (int)out.getIndex());
|
||||
s.executeUpdate();
|
||||
int updateCount = s.getUpdateCount();
|
||||
s.close();
|
||||
if (updateCount == 0)
|
||||
throw new BlockStoreException("Tried to remove a StoredTransactionOutput from H2FullPrunedBlockStore that it didn't have!");
|
||||
} catch (SQLException e) {
|
||||
throw new BlockStoreException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beginDatabaseBatchWrite() throws BlockStoreException {
|
||||
maybeConnect();
|
||||
try {
|
||||
conn.get().setAutoCommit(false);
|
||||
} catch (SQLException e) {
|
||||
throw new BlockStoreException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void commitDatabaseBatchWrite() throws BlockStoreException {
|
||||
maybeConnect();
|
||||
try {
|
||||
conn.get().commit();
|
||||
conn.get().setAutoCommit(true);
|
||||
} catch (SQLException e) {
|
||||
throw new BlockStoreException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void abortDatabaseBatchWrite() throws BlockStoreException {
|
||||
maybeConnect();
|
||||
try {
|
||||
conn.get().rollback();
|
||||
conn.get().setAutoCommit(true);
|
||||
} catch (SQLException e) {
|
||||
throw new BlockStoreException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasUnspentOutputs(Sha256Hash hash, int numOutputs) throws BlockStoreException {
|
||||
maybeConnect();
|
||||
PreparedStatement s = null;
|
||||
try {
|
||||
s = conn.get()
|
||||
.prepareStatement("SELECT COUNT(*) FROM openOutputs WHERE hash = ?");
|
||||
s.setBytes(1, hash.getBytes());
|
||||
ResultSet results = s.executeQuery();
|
||||
if (!results.next()) {
|
||||
throw new BlockStoreException("Got no results from a COUNT(*) query");
|
||||
}
|
||||
int count = results.getInt(1);
|
||||
return count != 0;
|
||||
} catch (SQLException ex) {
|
||||
throw new BlockStoreException(ex);
|
||||
} finally {
|
||||
if (s != null)
|
||||
try {
|
||||
s.close();
|
||||
} catch (SQLException e) { throw new BlockStoreException("Failed to close PreparedStatement"); }
|
||||
}
|
||||
protected String getDatabaseDriverClass() {
|
||||
return DATABASE_DRIVER_CLASS;
|
||||
}
|
||||
}
|
||||
|
@ -32,6 +32,7 @@ public class MemoryBlockStore implements BlockStore {
|
||||
}
|
||||
};
|
||||
private StoredBlock chainHead;
|
||||
private NetworkParameters params;
|
||||
|
||||
public MemoryBlockStore(NetworkParameters params) {
|
||||
// Insert the genesis block.
|
||||
@ -40,6 +41,7 @@ public class MemoryBlockStore implements BlockStore {
|
||||
StoredBlock storedGenesis = new StoredBlock(genesisHeader, genesisHeader.getWork(), 0);
|
||||
put(storedGenesis);
|
||||
setChainHead(storedGenesis);
|
||||
this.params = params;
|
||||
} catch (BlockStoreException e) {
|
||||
throw new RuntimeException(e); // Cannot happen.
|
||||
} catch (VerificationException e) {
|
||||
@ -76,4 +78,9 @@ public class MemoryBlockStore implements BlockStore {
|
||||
public void close() {
|
||||
blockMap = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public NetworkParameters getParams() {
|
||||
return params;
|
||||
}
|
||||
}
|
||||
|
@ -42,7 +42,7 @@ class StoredTransactionOutPoint implements Serializable {
|
||||
this.index = index;
|
||||
}
|
||||
|
||||
StoredTransactionOutPoint(StoredTransactionOutput out) {
|
||||
StoredTransactionOutPoint(UTXO out) {
|
||||
this.hash = out.getHash();
|
||||
this.index = out.getIndex();
|
||||
}
|
||||
@ -133,6 +133,14 @@ class TransactionalHashMap<KeyType, ValueType> {
|
||||
return map.get(key);
|
||||
}
|
||||
|
||||
public List<ValueType> values() {
|
||||
List<ValueType> valueTypes = new ArrayList<ValueType>();
|
||||
for (KeyType keyType : map.keySet()) {
|
||||
valueTypes.add(get(keyType));
|
||||
}
|
||||
return valueTypes;
|
||||
}
|
||||
|
||||
public void put(KeyType key, ValueType value) {
|
||||
if (Boolean.TRUE.equals(inTransaction.get())) {
|
||||
if (tempSetRemoved.get() != null)
|
||||
@ -224,10 +232,10 @@ class TransactionalMultiKeyHashMap<UniqueKeyType, MultiKeyType, ValueType> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps {@link StoredBlock}s, {@link StoredUndoableBlock}s and {@link StoredTransactionOutput}s in memory.
|
||||
* Keeps {@link StoredBlock}s, {@link StoredUndoableBlock}s and {@link com.dogecoin.dogecoinj.core.UTXO}s in memory.
|
||||
* Used primarily for unit testing.
|
||||
*/
|
||||
public class MemoryFullPrunedBlockStore implements FullPrunedBlockStore {
|
||||
public class MemoryFullPrunedBlockStore implements FullPrunedBlockStore, UTXOProvider {
|
||||
protected static class StoredBlockAndWasUndoableFlag {
|
||||
public StoredBlock block;
|
||||
public boolean wasUndoable;
|
||||
@ -236,10 +244,11 @@ public class MemoryFullPrunedBlockStore implements FullPrunedBlockStore {
|
||||
private TransactionalHashMap<Sha256Hash, StoredBlockAndWasUndoableFlag> blockMap;
|
||||
private TransactionalMultiKeyHashMap<Sha256Hash, Integer, StoredUndoableBlock> fullBlockMap;
|
||||
//TODO: Use something more suited to remove-heavy use?
|
||||
private TransactionalHashMap<StoredTransactionOutPoint, StoredTransactionOutput> transactionOutputMap;
|
||||
private TransactionalHashMap<StoredTransactionOutPoint, UTXO> transactionOutputMap;
|
||||
private StoredBlock chainHead;
|
||||
private StoredBlock verifiedChainHead;
|
||||
private int fullStoreDepth;
|
||||
private NetworkParameters params;
|
||||
|
||||
/**
|
||||
* Set up the MemoryFullPrunedBlockStore
|
||||
@ -249,7 +258,7 @@ public class MemoryFullPrunedBlockStore implements FullPrunedBlockStore {
|
||||
public MemoryFullPrunedBlockStore(NetworkParameters params, int fullStoreDepth) {
|
||||
blockMap = new TransactionalHashMap<Sha256Hash, StoredBlockAndWasUndoableFlag>();
|
||||
fullBlockMap = new TransactionalMultiKeyHashMap<Sha256Hash, Integer, StoredUndoableBlock>();
|
||||
transactionOutputMap = new TransactionalHashMap<StoredTransactionOutPoint, StoredTransactionOutput>();
|
||||
transactionOutputMap = new TransactionalHashMap<StoredTransactionOutPoint, UTXO>();
|
||||
this.fullStoreDepth = fullStoreDepth > 0 ? fullStoreDepth : 1;
|
||||
// Insert the genesis block.
|
||||
try {
|
||||
@ -260,6 +269,7 @@ public class MemoryFullPrunedBlockStore implements FullPrunedBlockStore {
|
||||
put(storedGenesisHeader, storedGenesis);
|
||||
setChainHead(storedGenesisHeader);
|
||||
setVerifiedChainHead(storedGenesisHeader);
|
||||
this.params = params;
|
||||
} catch (BlockStoreException e) {
|
||||
throw new RuntimeException(e); // Cannot happen.
|
||||
} catch (VerificationException e) {
|
||||
@ -343,22 +353,22 @@ public class MemoryFullPrunedBlockStore implements FullPrunedBlockStore {
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public synchronized StoredTransactionOutput getTransactionOutput(Sha256Hash hash, long index) throws BlockStoreException {
|
||||
public synchronized UTXO getTransactionOutput(Sha256Hash hash, long index) throws BlockStoreException {
|
||||
Preconditions.checkNotNull(transactionOutputMap, "MemoryFullPrunedBlockStore is closed");
|
||||
return transactionOutputMap.get(new StoredTransactionOutPoint(hash, index));
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void addUnspentTransactionOutput(StoredTransactionOutput out) throws BlockStoreException {
|
||||
public synchronized void addUnspentTransactionOutput(UTXO out) throws BlockStoreException {
|
||||
Preconditions.checkNotNull(transactionOutputMap, "MemoryFullPrunedBlockStore is closed");
|
||||
transactionOutputMap.put(new StoredTransactionOutPoint(out), out);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void removeUnspentTransactionOutput(StoredTransactionOutput out) throws BlockStoreException {
|
||||
public synchronized void removeUnspentTransactionOutput(UTXO out) throws BlockStoreException {
|
||||
Preconditions.checkNotNull(transactionOutputMap, "MemoryFullPrunedBlockStore is closed");
|
||||
if (transactionOutputMap.remove(new StoredTransactionOutPoint(out)) == null)
|
||||
throw new BlockStoreException("Tried to remove a StoredTransactionOutput from MemoryFullPrunedBlockStore that it didn't have!");
|
||||
throw new BlockStoreException("Tried to remove a UTXO from MemoryFullPrunedBlockStore that it didn't have!");
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -389,4 +399,34 @@ public class MemoryFullPrunedBlockStore implements FullPrunedBlockStore {
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public NetworkParameters getParams() {
|
||||
return params;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getChainHeadHeight() throws UTXOProviderException {
|
||||
try {
|
||||
return getVerifiedChainHead().getHeight();
|
||||
} catch (BlockStoreException e) {
|
||||
throw new UTXOProviderException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<UTXO> getOpenTransactionOutputs(List<Address> addresses) throws UTXOProviderException {
|
||||
// This is *NOT* optimal: We go through all the outputs and select the ones we are looking for.
|
||||
// If someone uses this store for production then they have a lot more to worry about than an inefficient impl :)
|
||||
List<UTXO> foundOutputs = new ArrayList<UTXO>();
|
||||
List<UTXO> outputsList = transactionOutputMap.values();
|
||||
for (UTXO output : outputsList) {
|
||||
for (Address address : addresses) {
|
||||
if (output.getAddress().equals(address.toString())) {
|
||||
foundOutputs.add(output);
|
||||
}
|
||||
}
|
||||
}
|
||||
return foundOutputs;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,160 @@
|
||||
/*
|
||||
* Copyright 2014 Kalpesh Parmar
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.dogecoin.dogecoinj.store;
|
||||
|
||||
import com.dogecoin.dogecoinj.core.NetworkParameters;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* <p>A full pruned block store using the MySQL database engine. As an added bonus an address index is calculated,
|
||||
* so you can use {@link #calculateBalanceForAddress(com.dogecoin.dogecoinj.core.Address)} to quickly look up
|
||||
* the quantity of bitcoins controlled by that address.</p>
|
||||
*/
|
||||
public class MySQLFullPrunedBlockStore extends DatabaseFullPrunedBlockStore {
|
||||
private static final Logger log = LoggerFactory.getLogger(MySQLFullPrunedBlockStore.class);
|
||||
|
||||
private static final String MYSQL_DUPLICATE_KEY_ERROR_CODE = "23000";
|
||||
private static final String DATABASE_DRIVER_CLASS = "com.mysql.jdbc.Driver";
|
||||
private static final String DATABASE_CONNECTION_URL_PREFIX = "jdbc:mysql://";
|
||||
|
||||
// create table SQL
|
||||
private static final String CREATE_SETTINGS_TABLE = "CREATE TABLE settings (\n" +
|
||||
" name varchar(32) NOT NULL,\n" +
|
||||
" value blob,\n" +
|
||||
" CONSTRAINT `setting_pk` PRIMARY KEY (name)\n" +
|
||||
")\n";
|
||||
|
||||
private static final String CREATE_HEADERS_TABLE = "CREATE TABLE headers (\n" +
|
||||
" hash blob NOT NULL,\n" +
|
||||
" chainwork mediumblob NOT NULL,\n" +
|
||||
" height integer NOT NULL,\n" +
|
||||
" header blob NOT NULL,\n" +
|
||||
" wasundoable tinyint(1) NOT NULL,\n" +
|
||||
" CONSTRAINT `headers_pk` PRIMARY KEY (hash(28)) USING btree\n" +
|
||||
")";
|
||||
|
||||
private static final String CREATE_UNDOABLE_TABLE = "CREATE TABLE undoableblocks (\n" +
|
||||
" hash blob NOT NULL,\n" +
|
||||
" height integer NOT NULL,\n" +
|
||||
" txoutchanges mediumblob,\n" +
|
||||
" transactions mediumblob,\n" +
|
||||
" CONSTRAINT `undoableblocks_pk` PRIMARY KEY (hash(28)) USING btree\n" +
|
||||
")\n";
|
||||
|
||||
private static final String CREATE_OPEN_OUTPUT_TABLE = "CREATE TABLE openoutputs (\n" +
|
||||
" hash blob NOT NULL,\n" +
|
||||
" `index` integer NOT NULL,\n" +
|
||||
" height integer NOT NULL,\n" +
|
||||
" value bigint NOT NULL,\n" +
|
||||
" scriptbytes mediumblob NOT NULL,\n" +
|
||||
" toaddress varchar(35),\n" +
|
||||
" addresstargetable tinyint(1),\n" +
|
||||
" coinbase boolean,\n" +
|
||||
" CONSTRAINT `openoutputs_pk` PRIMARY KEY (hash(32),`index`) USING btree\n" +
|
||||
")\n";
|
||||
|
||||
// Some indexes to speed up inserts
|
||||
private static final String CREATE_OUTPUTS_ADDRESS_MULTI_INDEX = "CREATE INDEX openoutputs_hash_index_height_toaddress_idx ON openoutputs (hash(32), `index`, height, toaddress) USING btree";
|
||||
private static final String CREATE_OUTPUTS_TOADDRESS_INDEX = "CREATE INDEX openoutputs_toaddress_idx ON openoutputs (toaddress) USING btree";
|
||||
private static final String CREATE_OUTPUTS_ADDRESSTARGETABLE_INDEX = "CREATE INDEX openoutputs_addresstargetable_idx ON openoutputs (addresstargetable) USING btree";
|
||||
private static final String CREATE_OUTPUTS_HASH_INDEX = "CREATE INDEX openoutputs_hash_idx ON openoutputs (hash(32)) USING btree";
|
||||
private static final String CREATE_UNDOABLE_TABLE_INDEX = "CREATE INDEX undoableblocks_height_idx ON undoableBlocks (height) USING btree";
|
||||
|
||||
// SQL involving index column (table openOutputs) overridden as it is a reserved word and must be back ticked in MySQL.
|
||||
private static final String SELECT_OPENOUTPUTS_SQL = "SELECT height, value, scriptBytes, coinbase, toaddress, addresstargetable FROM openOutputs WHERE hash = ? AND `index` = ?";
|
||||
private static final String INSERT_OPENOUTPUTS_SQL = "INSERT INTO openOutputs (hash, `index`, height, value, scriptBytes, toAddress, addressTargetable, coinbase) VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
|
||||
private static final String DELETE_OPENOUTPUTS_SQL = "DELETE FROM openOutputs WHERE hash = ? AND `index` = ?";
|
||||
|
||||
private static final String SELECT_TRANSACTION_OUTPUTS_SQL = "SELECT hash, value, scriptBytes, height, `index`, coinbase, toaddress, addresstargetable FROM openOutputs where toaddress = ?";
|
||||
|
||||
/**
|
||||
* Creates a new MySQLFullPrunedBlockStore.
|
||||
*
|
||||
* @param params A copy of the NetworkParameters used
|
||||
* @param fullStoreDepth The number of blocks of history stored in full (something like 1000 is pretty safe)
|
||||
* @param hostname The hostname of the database to connect to
|
||||
* @param dbName The database to connect to
|
||||
* @param username The database username
|
||||
* @param password The password to the database
|
||||
* @throws BlockStoreException if the database fails to open for any reason
|
||||
*/
|
||||
public MySQLFullPrunedBlockStore(NetworkParameters params, int fullStoreDepth, String hostname, String dbName,
|
||||
String username, String password) throws BlockStoreException {
|
||||
super(params, DATABASE_CONNECTION_URL_PREFIX + hostname + "/" + dbName, fullStoreDepth, username, password, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getDuplicateKeyErrorCode() {
|
||||
return MYSQL_DUPLICATE_KEY_ERROR_CODE;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getSelectOpenoutputsSQL() {
|
||||
return SELECT_OPENOUTPUTS_SQL;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getInsertOpenoutputsSQL() {
|
||||
return INSERT_OPENOUTPUTS_SQL;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getDeleteOpenoutputsSQL() {
|
||||
return DELETE_OPENOUTPUTS_SQL;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getTrasactionOutputSelectSQL() {
|
||||
return SELECT_TRANSACTION_OUTPUTS_SQL;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<String> getCreateTablesSQL() {
|
||||
List<String> sqlStatements = new ArrayList<String>();
|
||||
sqlStatements.add(CREATE_SETTINGS_TABLE);
|
||||
sqlStatements.add(CREATE_HEADERS_TABLE);
|
||||
sqlStatements.add(CREATE_UNDOABLE_TABLE);
|
||||
sqlStatements.add(CREATE_OPEN_OUTPUT_TABLE);
|
||||
return sqlStatements;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<String> getCreateIndexesSQL() {
|
||||
List<String> sqlStatements = new ArrayList<String>();
|
||||
sqlStatements.add(CREATE_UNDOABLE_TABLE_INDEX);
|
||||
sqlStatements.add(CREATE_OUTPUTS_ADDRESS_MULTI_INDEX);
|
||||
sqlStatements.add(CREATE_OUTPUTS_ADDRESSTARGETABLE_INDEX);
|
||||
sqlStatements.add(CREATE_OUTPUTS_HASH_INDEX);
|
||||
sqlStatements.add(CREATE_OUTPUTS_TOADDRESS_INDEX);
|
||||
return sqlStatements;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<String> getCreateSchemeSQL() {
|
||||
// do nothing
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getDatabaseDriverClass() {
|
||||
return DATABASE_DRIVER_CLASS;
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
/*
|
||||
* Copyright 2014 BitPOS Pty Ltd.
|
||||
* Copyright 2014 Andreas Schildbach
|
||||
* Copyright 2014 Kalpesh Parmar
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -14,90 +15,79 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.dogecoin.dogecoinj.store;
|
||||
|
||||
import com.dogecoin.dogecoinj.core.*;
|
||||
import com.dogecoin.dogecoinj.script.Script;
|
||||
import com.google.common.collect.Lists;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.sql.*;
|
||||
import java.util.LinkedList;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Types;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
|
||||
/**
|
||||
* <p>A full pruned block store using the Postgres database engine. As an added bonus an address index is calculated,
|
||||
* so you can use {@link #calculateBalanceForAddress(com.dogecoin.dogecoinj.core.Address)} to quickly look up
|
||||
* the quantity of bitcoins controlled by that address.</p>
|
||||
*/
|
||||
public class PostgresFullPrunedBlockStore implements FullPrunedBlockStore {
|
||||
public class PostgresFullPrunedBlockStore extends DatabaseFullPrunedBlockStore {
|
||||
private static final Logger log = LoggerFactory.getLogger(PostgresFullPrunedBlockStore.class);
|
||||
|
||||
private static final String POSTGRES_DUPLICATE_KEY_ERROR_CODE = "23505";
|
||||
private static final String DATABASE_DRIVER_CLASS = "org.postgresql.Driver";
|
||||
private static final String DATABASE_CONNECTION_URL_PREFIX = "jdbc:postgresql://";
|
||||
|
||||
private Sha256Hash chainHeadHash;
|
||||
private StoredBlock chainHeadBlock;
|
||||
private Sha256Hash verifiedChainHeadHash;
|
||||
private StoredBlock verifiedChainHeadBlock;
|
||||
private NetworkParameters params;
|
||||
private ThreadLocal<Connection> conn;
|
||||
private List<Connection> allConnections;
|
||||
private String connectionURL;
|
||||
private int fullStoreDepth;
|
||||
private String username;
|
||||
private String password;
|
||||
private String schemaName;
|
||||
|
||||
private static final String driver = "org.postgresql.Driver";
|
||||
// create table SQL
|
||||
private static final String CREATE_SETTINGS_TABLE = "CREATE TABLE settings (\n" +
|
||||
" name character varying(32) NOT NULL,\n" +
|
||||
" value bytea\n" +
|
||||
");";
|
||||
private static final String CHAIN_HEAD_SETTING = "chainhead";
|
||||
private static final String VERIFIED_CHAIN_HEAD_SETTING = "verifiedchainhead";
|
||||
private static final String VERSION_SETTING = "version";
|
||||
" value bytea,\n" +
|
||||
" CONSTRAINT setting_pk PRIMARY KEY (name)\n" +
|
||||
")\n";
|
||||
|
||||
private static final String CREATE_HEADERS_TABLE = "CREATE TABLE headers (" +
|
||||
" hash bytea NOT NULL," +
|
||||
" chainwork bytea NOT NULL," +
|
||||
" height integer NOT NULL," +
|
||||
" header bytea NOT NULL," +
|
||||
" wasundoable boolean NOT NULL" +
|
||||
");";
|
||||
private static final String CREATE_HEADERS_TABLE = "CREATE TABLE headers (\n" +
|
||||
" hash bytea NOT NULL,\n" +
|
||||
" chainwork bytea NOT NULL,\n" +
|
||||
" height integer NOT NULL,\n" +
|
||||
" header bytea NOT NULL,\n" +
|
||||
" wasundoable boolean NOT NULL,\n" +
|
||||
" CONSTRAINT headers_pk PRIMARY KEY (hash)\n" +
|
||||
")\n";
|
||||
|
||||
private static final String CREATE_UNDOABLE_TABLE = "CREATE TABLE undoableblocks (" +
|
||||
" hash bytea NOT NULL," +
|
||||
" height integer NOT NULL," +
|
||||
" txoutchanges bytea," +
|
||||
" transactions bytea" +
|
||||
");";
|
||||
private static final String CREATE_OPEN_OUTPUT_TABLE = "CREATE TABLE openoutputs (" +
|
||||
" hash bytea NOT NULL," +
|
||||
" index integer NOT NULL," +
|
||||
" height integer NOT NULL," +
|
||||
" value bytea NOT NULL," +
|
||||
" scriptbytes bytea NOT NULL," +
|
||||
" toaddress character varying(35)," +
|
||||
" addresstargetable integer" +
|
||||
");";
|
||||
private static final String CREATE_UNDOABLE_TABLE = "CREATE TABLE undoableblocks (\n" +
|
||||
" hash bytea NOT NULL,\n" +
|
||||
" height integer NOT NULL,\n" +
|
||||
" txoutchanges bytea,\n" +
|
||||
" transactions bytea,\n" +
|
||||
" CONSTRAINT undoableblocks_pk PRIMARY KEY (hash)\n" +
|
||||
")\n";
|
||||
|
||||
private static final String CREATE_UNDOABLE_TABLE_INDEX = "CREATE INDEX heightIndex ON undoableBlocks (height)";
|
||||
private static final String CREATE_OPEN_OUTPUT_TABLE = "CREATE TABLE openoutputs (\n" +
|
||||
" hash bytea NOT NULL,\n" +
|
||||
" index integer NOT NULL,\n" +
|
||||
" height integer NOT NULL,\n" +
|
||||
" value bigint NOT NULL,\n" +
|
||||
" scriptbytes bytea NOT NULL,\n" +
|
||||
" toaddress character varying(35),\n" +
|
||||
" addresstargetable smallint,\n" +
|
||||
" coinbase boolean,\n" +
|
||||
" CONSTRAINT openoutputs_pk PRIMARY KEY (hash,index)\n" +
|
||||
")\n";
|
||||
|
||||
// Some indexes to speed up inserts
|
||||
private static final String CREATE_HEADERS_HASH_INDEX = "CREATE INDEX headershashindex ON headers USING btree (hash);";
|
||||
private static final String CREATE_OUTPUTS_ADDRESS_INDEX = "CREATE INDEX idx_address ON openoutputs USING btree (hash, index, height, toaddress);";
|
||||
private static final String CREATE_OUTPUT_ADDRESS_TYPE_INDEX = "CREATE INDEX idx_addresstargetable ON openoutputs USING btree (addresstargetable);";
|
||||
private static final String CREATE_OUTPUTS_HASH_INDEX = "CREATE INDEX openoutputshash ON openoutputs USING btree (hash);";
|
||||
private static final String CREATE_OUTPUTS_HASH_INDEX_INDEX = "CREATE INDEX openoutputshashindex ON openoutputs USING btree (hash, index);";
|
||||
private static final String CREATE_UNDOABLE_HASH_INDEX = "CREATE INDEX undoableblockshashindex ON undoableblocks USING btree (hash);";
|
||||
private static final String CREATE_OUTPUTS_ADDRESS_MULTI_INDEX = "CREATE INDEX openoutputs_hash_index_num_height_toaddress_idx ON openoutputs USING btree (hash, index, height, toaddress)";
|
||||
private static final String CREATE_OUTPUTS_TOADDRESS_INDEX = "CREATE INDEX openoutputs_toaddress_idx ON openoutputs USING btree (toaddress)";
|
||||
private static final String CREATE_OUTPUTS_ADDRESSTARGETABLE_INDEX = "CREATE INDEX openoutputs_addresstargetable_idx ON openoutputs USING btree (addresstargetable)";
|
||||
private static final String CREATE_OUTPUTS_HASH_INDEX = "CREATE INDEX openoutputs_hash_idx ON openoutputs USING btree (hash)";
|
||||
private static final String CREATE_UNDOABLE_TABLE_INDEX = "CREATE INDEX undoableblocks_height_idx ON undoableBlocks USING btree (height)";
|
||||
|
||||
private static final String SELECT_UNDOABLEBLOCKS_EXISTS_SQL = "select 1 from undoableBlocks where hash = ?";
|
||||
|
||||
/**
|
||||
* Creates a new PostgresFullPrunedBlockStore.
|
||||
@ -112,7 +102,7 @@ public class PostgresFullPrunedBlockStore implements FullPrunedBlockStore {
|
||||
*/
|
||||
public PostgresFullPrunedBlockStore(NetworkParameters params, int fullStoreDepth, String hostname, String dbName,
|
||||
String username, String password) throws BlockStoreException {
|
||||
this(params, "jdbc:postgresql://" + hostname + "/" + dbName, fullStoreDepth, username, password, null);
|
||||
super(params, DATABASE_CONNECTION_URL_PREFIX + hostname + "/" + dbName, fullStoreDepth, username, password, null);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -133,342 +123,54 @@ public class PostgresFullPrunedBlockStore implements FullPrunedBlockStore {
|
||||
*/
|
||||
public PostgresFullPrunedBlockStore(NetworkParameters params, int fullStoreDepth, String hostname, String dbName,
|
||||
String username, String password, @Nullable String schemaName) throws BlockStoreException {
|
||||
this(params, "jdbc:postgresql://" + hostname + "/" + dbName, fullStoreDepth, username, password, schemaName);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Create a new PostgresFullPrunedBlockStore, using the full connection URL instead of a hostname and password,
|
||||
* and optionally allowing a schema to be specified.</p>
|
||||
*
|
||||
* <p>The connection URL will be passed to the database driver, and should look like
|
||||
* "jdbc:postrgresql://host[:port]/databasename". You can use this to change the port, or specify additional
|
||||
* parameters. See <a href="http://jdbc.postgresql.org/documentation/head/connect.html#connection-parameters">
|
||||
* the PostgreSQL JDBC documentation</a> for more on the connection URL.</p>
|
||||
*
|
||||
* <p>This constructor also accepts a schema name to use, which can be used to avoid name collisions, or to keep the
|
||||
* database organized. If no schema is provided the default schema for the username will be used. See
|
||||
* <a href="http://www.postgres.org/docs/9.3/static/ddl-schemas.html">the postgres schema docs</a> for more on
|
||||
* schemas.</p>
|
||||
*
|
||||
*
|
||||
* @param params A copy of the NetworkParameters used.
|
||||
* @param connectionURL The jdbc url to connect to the database.
|
||||
* @param fullStoreDepth The number of blocks of history stored in full (something like 1000 is pretty safe).
|
||||
* @param username The database username.
|
||||
* @param password The password to the database.
|
||||
* @param schemaName The name of the schema to put the tables in. May be null if no schema is being used.
|
||||
* @throws BlockStoreException If the database fails to open for any reason.
|
||||
*/
|
||||
public PostgresFullPrunedBlockStore(NetworkParameters params, String connectionURL, int fullStoreDepth,
|
||||
String username, String password, @Nullable String schemaName) throws BlockStoreException {
|
||||
this.params = params;
|
||||
this.fullStoreDepth = fullStoreDepth;
|
||||
this.connectionURL = connectionURL;
|
||||
this.schemaName = schemaName;
|
||||
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
|
||||
conn = new ThreadLocal<Connection>();
|
||||
allConnections = new LinkedList<Connection>();
|
||||
|
||||
try {
|
||||
Class.forName(driver);
|
||||
log.info(driver + " loaded. ");
|
||||
} catch (java.lang.ClassNotFoundException e) {
|
||||
log.error("check CLASSPATH for Postgres jar ", e);
|
||||
}
|
||||
|
||||
maybeConnect();
|
||||
|
||||
try {
|
||||
// Create tables if needed
|
||||
if (!tableExists("settings"))
|
||||
createTables();
|
||||
initFromDatabase();
|
||||
} catch (SQLException e) {
|
||||
throw new BlockStoreException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private synchronized void maybeConnect() throws BlockStoreException {
|
||||
try {
|
||||
if (conn.get() != null && !conn.get().isClosed())
|
||||
return;
|
||||
|
||||
Properties props = new Properties();
|
||||
props.setProperty("user", this.username);
|
||||
props.setProperty("password", this.password);
|
||||
|
||||
conn.set(DriverManager.getConnection(connectionURL, props));
|
||||
|
||||
Connection connection = conn.get();
|
||||
// set the schema if one is needed
|
||||
if(schemaName != null) {
|
||||
Statement s = connection.createStatement();
|
||||
s.execute("CREATE SCHEMA IF NOT EXISTS " + schemaName + ";");
|
||||
s.execute("set search_path to '" + schemaName +"';");
|
||||
}
|
||||
allConnections.add(conn.get());
|
||||
log.info("Made a new connection to database " + connectionURL);
|
||||
} catch (SQLException ex) {
|
||||
throw new BlockStoreException(ex);
|
||||
}
|
||||
super(params, DATABASE_CONNECTION_URL_PREFIX + hostname + "/" + dbName, fullStoreDepth, username, password, schemaName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void close() {
|
||||
for (Connection conn : allConnections) {
|
||||
try {
|
||||
if(!conn.getAutoCommit()) {
|
||||
conn.rollback();
|
||||
}
|
||||
conn.close();
|
||||
if(conn == this.conn.get()) {
|
||||
this.conn.set(null);
|
||||
}
|
||||
} catch (SQLException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
allConnections.clear();
|
||||
}
|
||||
|
||||
public void resetStore() throws BlockStoreException {
|
||||
maybeConnect();
|
||||
try {
|
||||
Statement s = conn.get().createStatement();
|
||||
s.execute("DROP TABLE settings");
|
||||
s.execute("DROP TABLE headers");
|
||||
s.execute("DROP TABLE undoableBlocks");
|
||||
s.execute("DROP TABLE openOutputs");
|
||||
s.close();
|
||||
createTables();
|
||||
initFromDatabase();
|
||||
} catch (SQLException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void createTables() throws SQLException, BlockStoreException {
|
||||
Statement s = conn.get().createStatement();
|
||||
if (log.isDebugEnabled())
|
||||
log.debug("PostgresFullPrunedBlockStore : CREATE headers table");
|
||||
s.executeUpdate(CREATE_HEADERS_TABLE);
|
||||
|
||||
if (log.isDebugEnabled())
|
||||
log.debug("PostgresFullPrunedBlockStore : CREATE settings table");
|
||||
s.executeUpdate(CREATE_SETTINGS_TABLE);
|
||||
|
||||
if (log.isDebugEnabled())
|
||||
log.debug("PostgresFullPrunedBlockStore : CREATE undoable block table");
|
||||
s.executeUpdate(CREATE_UNDOABLE_TABLE);
|
||||
|
||||
if (log.isDebugEnabled())
|
||||
log.debug("PostgresFullPrunedBlockStore : CREATE undoable block index");
|
||||
s.executeUpdate(CREATE_UNDOABLE_TABLE_INDEX);
|
||||
if (log.isDebugEnabled())
|
||||
log.debug("PostgresFullPrunedBlockStore : CREATE open output table");
|
||||
s.executeUpdate(CREATE_OPEN_OUTPUT_TABLE);
|
||||
|
||||
// Create indexes..
|
||||
s.executeUpdate(CREATE_HEADERS_HASH_INDEX);
|
||||
s.executeUpdate(CREATE_OUTPUT_ADDRESS_TYPE_INDEX);
|
||||
s.executeUpdate(CREATE_OUTPUTS_ADDRESS_INDEX);
|
||||
s.executeUpdate(CREATE_OUTPUTS_HASH_INDEX);
|
||||
s.executeUpdate(CREATE_OUTPUTS_HASH_INDEX_INDEX);
|
||||
s.executeUpdate(CREATE_UNDOABLE_HASH_INDEX);
|
||||
|
||||
|
||||
s.executeUpdate("INSERT INTO settings(name, value) VALUES('" + CHAIN_HEAD_SETTING + "', NULL)");
|
||||
s.executeUpdate("INSERT INTO settings(name, value) VALUES('" + VERIFIED_CHAIN_HEAD_SETTING + "', NULL)");
|
||||
s.executeUpdate("INSERT INTO settings(name, value) VALUES('" + VERSION_SETTING + "', '03')");
|
||||
s.close();
|
||||
createNewStore(params);
|
||||
}
|
||||
|
||||
private void initFromDatabase() throws SQLException, BlockStoreException {
|
||||
Statement s = conn.get().createStatement();
|
||||
ResultSet rs;
|
||||
|
||||
rs = s.executeQuery("SELECT value FROM settings WHERE name = '" + CHAIN_HEAD_SETTING + "'");
|
||||
if (!rs.next()) {
|
||||
throw new BlockStoreException("corrupt Postgres block store - no chain head pointer");
|
||||
}
|
||||
Sha256Hash hash = new Sha256Hash(rs.getBytes(1));
|
||||
rs.close();
|
||||
this.chainHeadBlock = get(hash);
|
||||
this.chainHeadHash = hash;
|
||||
if (this.chainHeadBlock == null) {
|
||||
throw new BlockStoreException("corrupt Postgres block store - head block not found");
|
||||
}
|
||||
|
||||
rs = s.executeQuery("SELECT value FROM settings WHERE name = '" + VERIFIED_CHAIN_HEAD_SETTING + "'");
|
||||
if (!rs.next()) {
|
||||
throw new BlockStoreException("corrupt Postgres block store - no verified chain head pointer");
|
||||
}
|
||||
hash = new Sha256Hash(rs.getBytes(1));
|
||||
rs.close();
|
||||
s.close();
|
||||
this.verifiedChainHeadBlock = get(hash);
|
||||
this.verifiedChainHeadHash = hash;
|
||||
if (this.verifiedChainHeadBlock == null) {
|
||||
throw new BlockStoreException("corrupt Postgres block store - verified head block not found");
|
||||
}
|
||||
}
|
||||
|
||||
private void createNewStore(NetworkParameters params) throws BlockStoreException {
|
||||
try {
|
||||
// Set up the genesis block. When we start out fresh, it is by
|
||||
// definition the top of the chain.
|
||||
StoredBlock storedGenesisHeader = new StoredBlock(params.getGenesisBlock().cloneAsHeader(), params.getGenesisBlock().getWork(), 0);
|
||||
// The coinbase in the genesis block is not spendable. This is because of how the reference client inits
|
||||
// its database - the genesis transaction isn't actually in the db so its spent flags can never be updated.
|
||||
List<Transaction> genesisTransactions = Lists.newLinkedList();
|
||||
StoredUndoableBlock storedGenesis = new StoredUndoableBlock(params.getGenesisBlock().getHash(), genesisTransactions);
|
||||
put(storedGenesisHeader, storedGenesis);
|
||||
setChainHead(storedGenesisHeader);
|
||||
setVerifiedChainHead(storedGenesisHeader);
|
||||
} catch (VerificationException e) {
|
||||
throw new RuntimeException(e); // Cannot happen.
|
||||
}
|
||||
}
|
||||
|
||||
private boolean tableExists(String table) throws SQLException {
|
||||
Statement s = conn.get().createStatement();
|
||||
try {
|
||||
ResultSet results = s.executeQuery("SELECT * FROM " + table + " WHERE 1 = 2");
|
||||
results.close();
|
||||
return true;
|
||||
} catch (SQLException ex) {
|
||||
return false;
|
||||
} finally {
|
||||
s.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dumps information about the size of actual data in the database to standard output
|
||||
* The only truly useless data counted is printed in the form "N in id indexes"
|
||||
* This does not take database indexes into account
|
||||
*/
|
||||
public void dumpSizes() throws SQLException, BlockStoreException {
|
||||
maybeConnect();
|
||||
Statement s = conn.get().createStatement();
|
||||
long size = 0;
|
||||
long totalSize = 0;
|
||||
int count = 0;
|
||||
ResultSet rs = s.executeQuery("SELECT name, value FROM settings");
|
||||
while (rs.next()) {
|
||||
size += rs.getString(1).length();
|
||||
size += rs.getBytes(2).length;
|
||||
count++;
|
||||
}
|
||||
rs.close();
|
||||
System.out.printf("Settings size: %d, count: %d, average size: %f%n", size, count, (double)size/count);
|
||||
|
||||
totalSize += size; size = 0; count = 0;
|
||||
rs = s.executeQuery("SELECT chainWork, header FROM headers");
|
||||
while (rs.next()) {
|
||||
size += 28; // hash
|
||||
size += rs.getBytes(1).length;
|
||||
size += 4; // height
|
||||
size += rs.getBytes(2).length;
|
||||
count++;
|
||||
}
|
||||
rs.close();
|
||||
System.out.printf("Headers size: %d, count: %d, average size: %f%n", size, count, (double)size/count);
|
||||
|
||||
totalSize += size; size = 0; count = 0;
|
||||
rs = s.executeQuery("SELECT txOutChanges, transactions FROM undoableBlocks");
|
||||
while (rs.next()) {
|
||||
size += 28; // hash
|
||||
size += 4; // height
|
||||
byte[] txOutChanges = rs.getBytes(1);
|
||||
byte[] transactions = rs.getBytes(2);
|
||||
if (txOutChanges == null)
|
||||
size += transactions.length;
|
||||
else
|
||||
size += txOutChanges.length;
|
||||
// size += the space to represent NULL
|
||||
count++;
|
||||
}
|
||||
rs.close();
|
||||
System.out.printf("Undoable Blocks size: %d, count: %d, average size: %f%n", size, count, (double)size/count);
|
||||
|
||||
totalSize += size; size = 0; count = 0;
|
||||
long scriptSize = 0;
|
||||
rs = s.executeQuery("SELECT value, scriptBytes FROM openOutputs");
|
||||
while (rs.next()) {
|
||||
size += 32; // hash
|
||||
size += 4; // index
|
||||
size += 4; // height
|
||||
size += rs.getBytes(1).length;
|
||||
size += rs.getBytes(2).length;
|
||||
scriptSize += rs.getBytes(2).length;
|
||||
count++;
|
||||
}
|
||||
rs.close();
|
||||
System.out.printf("Open Outputs size: %d, count: %d, average size: %f, average script size: %f (%d in id indexes)%n",
|
||||
size, count, (double)size/count, (double)scriptSize/count, count * 8);
|
||||
|
||||
totalSize += size;
|
||||
System.out.println("Total Size: " + totalSize);
|
||||
|
||||
s.close();
|
||||
}
|
||||
|
||||
|
||||
private void putUpdateStoredBlock(StoredBlock storedBlock, boolean wasUndoable) throws SQLException {
|
||||
try {
|
||||
PreparedStatement s =
|
||||
conn.get().prepareStatement("INSERT INTO headers(hash, chainWork, height, header, wasUndoable)"
|
||||
+ " VALUES(?, ?, ?, ?, ?)");
|
||||
// We skip the first 4 bytes because (on prodnet) the minimum target has 4 0-bytes
|
||||
byte[] hashBytes = new byte[28];
|
||||
System.arraycopy(storedBlock.getHeader().getHash().getBytes(), 3, hashBytes, 0, 28);
|
||||
s.setBytes(1, hashBytes);
|
||||
s.setBytes(2, storedBlock.getChainWork().toByteArray());
|
||||
s.setInt(3, storedBlock.getHeight());
|
||||
s.setBytes(4, storedBlock.getHeader().unsafeBitcoinSerialize());
|
||||
s.setBoolean(5, wasUndoable);
|
||||
s.executeUpdate();
|
||||
s.close();
|
||||
} catch (SQLException e) {
|
||||
// It is possible we try to add a duplicate StoredBlock if we upgraded
|
||||
// In that case, we just update the entry to mark it wasUndoable
|
||||
if (!(e.getSQLState().equals(POSTGRES_DUPLICATE_KEY_ERROR_CODE)) || !wasUndoable)
|
||||
throw e;
|
||||
|
||||
PreparedStatement s = conn.get().prepareStatement("UPDATE headers SET wasUndoable=? WHERE hash=?");
|
||||
s.setBoolean(1, true);
|
||||
// We skip the first 4 bytes because (on prodnet) the minimum target has 4 0-bytes
|
||||
byte[] hashBytes = new byte[28];
|
||||
System.arraycopy(storedBlock.getHeader().getHash().getBytes(), 3, hashBytes, 0, 28);
|
||||
s.setBytes(2, hashBytes);
|
||||
s.executeUpdate();
|
||||
s.close();
|
||||
}
|
||||
protected String getDuplicateKeyErrorCode() {
|
||||
return POSTGRES_DUPLICATE_KEY_ERROR_CODE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void put(StoredBlock storedBlock) throws BlockStoreException {
|
||||
maybeConnect();
|
||||
try {
|
||||
putUpdateStoredBlock(storedBlock, false);
|
||||
} catch (SQLException e) {
|
||||
throw new BlockStoreException(e);
|
||||
}
|
||||
protected List<String> getCreateTablesSQL() {
|
||||
List<String> sqlStatements = new ArrayList<String>();
|
||||
sqlStatements.add(CREATE_SETTINGS_TABLE);
|
||||
sqlStatements.add(CREATE_HEADERS_TABLE);
|
||||
sqlStatements.add(CREATE_UNDOABLE_TABLE);
|
||||
sqlStatements.add(CREATE_OPEN_OUTPUT_TABLE);
|
||||
return sqlStatements;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<String> getCreateIndexesSQL() {
|
||||
List<String> sqlStatements = new ArrayList<String>();
|
||||
sqlStatements.add(CREATE_UNDOABLE_TABLE_INDEX);
|
||||
sqlStatements.add(CREATE_OUTPUTS_ADDRESS_MULTI_INDEX);
|
||||
sqlStatements.add(CREATE_OUTPUTS_ADDRESSTARGETABLE_INDEX);
|
||||
sqlStatements.add(CREATE_OUTPUTS_HASH_INDEX);
|
||||
sqlStatements.add(CREATE_OUTPUTS_TOADDRESS_INDEX);
|
||||
return sqlStatements;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<String> getCreateSchemeSQL() {
|
||||
List<String> sqlStatements = new ArrayList<String>();
|
||||
sqlStatements.add("CREATE SCHEMA IF NOT EXISTS " + schemaName);
|
||||
sqlStatements.add("set search_path to '" + schemaName +"'");
|
||||
return sqlStatements;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getDatabaseDriverClass() {
|
||||
return DATABASE_DRIVER_CLASS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void put(StoredBlock storedBlock, StoredUndoableBlock undoableBlock) throws BlockStoreException {
|
||||
maybeConnect();
|
||||
// We skip the first 4 bytes because (on prodnet) the minimum target has 4 0-bytes
|
||||
// We skip the first 4 bytes because (on mainnet) the minimum target has 4 0-bytes
|
||||
byte[] hashBytes = new byte[28];
|
||||
System.arraycopy(storedBlock.getHeader().getHash().getBytes(), 3, hashBytes, 0, 28);
|
||||
System.arraycopy(storedBlock.getHeader().getHash().getBytes(), 4, hashBytes, 0, 28);
|
||||
int height = storedBlock.getHeight();
|
||||
byte[] transactions = null;
|
||||
byte[] txOutChanges = null;
|
||||
@ -497,7 +199,7 @@ public class PostgresFullPrunedBlockStore implements FullPrunedBlockStore {
|
||||
if (log.isDebugEnabled())
|
||||
log.debug("Looking for undoable block with hash: " + Utils.HEX.encode(hashBytes));
|
||||
|
||||
PreparedStatement findS = conn.get().prepareStatement("select 1 from undoableBlocks where hash = ?");
|
||||
PreparedStatement findS = conn.get().prepareStatement(SELECT_UNDOABLEBLOCKS_EXISTS_SQL);
|
||||
findS.setBytes(1, hashBytes);
|
||||
|
||||
ResultSet rs = findS.executeQuery();
|
||||
@ -509,8 +211,7 @@ public class PostgresFullPrunedBlockStore implements FullPrunedBlockStore {
|
||||
// Postgres insert-or-updates are very complex (and finnicky). This level of transaction isolation
|
||||
// seems to work for bitcoinj
|
||||
PreparedStatement s =
|
||||
conn.get().prepareStatement("UPDATE undoableBlocks SET txOutChanges=?, transactions=?"
|
||||
+ " WHERE hash = ?");
|
||||
conn.get().prepareStatement(getUpdateUndoableBlocksSQL());
|
||||
s.setBytes(3, hashBytes);
|
||||
|
||||
if (log.isDebugEnabled())
|
||||
@ -530,8 +231,7 @@ public class PostgresFullPrunedBlockStore implements FullPrunedBlockStore {
|
||||
}
|
||||
|
||||
PreparedStatement s =
|
||||
conn.get().prepareStatement("INSERT INTO undoableBlocks(hash, height, txOutChanges, transactions)"
|
||||
+ " VALUES(?, ?, ?, ?)");
|
||||
conn.get().prepareStatement(getInsertUndoableBlocksSQL());
|
||||
s.setBytes(1, hashBytes);
|
||||
s.setInt(2, height);
|
||||
|
||||
@ -558,416 +258,4 @@ public class PostgresFullPrunedBlockStore implements FullPrunedBlockStore {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public StoredBlock get(Sha256Hash hash, boolean wasUndoableOnly) throws BlockStoreException {
|
||||
// Optimize for chain head
|
||||
if (chainHeadHash != null && chainHeadHash.equals(hash))
|
||||
return chainHeadBlock;
|
||||
if (verifiedChainHeadHash != null && verifiedChainHeadHash.equals(hash))
|
||||
return verifiedChainHeadBlock;
|
||||
maybeConnect();
|
||||
PreparedStatement s = null;
|
||||
try {
|
||||
s = conn.get()
|
||||
.prepareStatement("SELECT chainWork, height, header, wasUndoable FROM headers WHERE hash = ?");
|
||||
// We skip the first 4 bytes because (on prodnet) the minimum target has 4 0-bytes
|
||||
byte[] hashBytes = new byte[28];
|
||||
System.arraycopy(hash.getBytes(), 3, hashBytes, 0, 28);
|
||||
s.setBytes(1, hashBytes);
|
||||
ResultSet results = s.executeQuery();
|
||||
if (!results.next()) {
|
||||
return null;
|
||||
}
|
||||
// Parse it.
|
||||
|
||||
if (wasUndoableOnly && !results.getBoolean(4))
|
||||
return null;
|
||||
|
||||
BigInteger chainWork = new BigInteger(results.getBytes(1));
|
||||
int height = results.getInt(2);
|
||||
Block b = new Block(params, results.getBytes(3));
|
||||
b.verifyHeader();
|
||||
StoredBlock stored = new StoredBlock(b, chainWork, height);
|
||||
return stored;
|
||||
} catch (SQLException ex) {
|
||||
throw new BlockStoreException(ex);
|
||||
} catch (ProtocolException e) {
|
||||
// Corrupted database.
|
||||
throw new BlockStoreException(e);
|
||||
} catch (VerificationException e) {
|
||||
// Should not be able to happen unless the database contains bad
|
||||
// blocks.
|
||||
throw new BlockStoreException(e);
|
||||
} finally {
|
||||
if (s != null)
|
||||
try {
|
||||
s.close();
|
||||
} catch (SQLException e) { throw new BlockStoreException("Failed to close PreparedStatement"); }
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public StoredBlock get(Sha256Hash hash) throws BlockStoreException {
|
||||
return get(hash, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public StoredBlock getOnceUndoableStoredBlock(Sha256Hash hash) throws BlockStoreException {
|
||||
return get(hash, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public StoredUndoableBlock getUndoBlock(Sha256Hash hash) throws BlockStoreException {
|
||||
maybeConnect();
|
||||
PreparedStatement s = null;
|
||||
try {
|
||||
s = conn.get()
|
||||
.prepareStatement("SELECT txOutChanges, transactions FROM undoableBlocks WHERE hash = ?");
|
||||
// We skip the first 4 bytes because (on prodnet) the minimum target has 4 0-bytes
|
||||
|
||||
byte[] hashBytes = new byte[28];
|
||||
System.arraycopy(hash.getBytes(), 3, hashBytes, 0, 28);
|
||||
s.setBytes(1, hashBytes);
|
||||
ResultSet results = s.executeQuery();
|
||||
if (!results.next()) {
|
||||
return null;
|
||||
}
|
||||
// Parse it.
|
||||
byte[] txOutChanges = results.getBytes(1);
|
||||
byte[] transactions = results.getBytes(2);
|
||||
StoredUndoableBlock block;
|
||||
if (txOutChanges == null) {
|
||||
int offset = 0;
|
||||
int numTxn = ((transactions[offset++] & 0xFF) << 0) |
|
||||
((transactions[offset++] & 0xFF) << 8) |
|
||||
((transactions[offset++] & 0xFF) << 16) |
|
||||
((transactions[offset++] & 0xFF) << 24);
|
||||
List<Transaction> transactionList = new LinkedList<Transaction>();
|
||||
for (int i = 0; i < numTxn; i++) {
|
||||
Transaction tx = new Transaction(params, transactions, offset);
|
||||
transactionList.add(tx);
|
||||
offset += tx.getMessageSize();
|
||||
}
|
||||
block = new StoredUndoableBlock(hash, transactionList);
|
||||
} else {
|
||||
TransactionOutputChanges outChangesObject =
|
||||
new TransactionOutputChanges(new ByteArrayInputStream(txOutChanges));
|
||||
block = new StoredUndoableBlock(hash, outChangesObject);
|
||||
}
|
||||
return block;
|
||||
} catch (SQLException ex) {
|
||||
throw new BlockStoreException(ex);
|
||||
} catch (NullPointerException e) {
|
||||
// Corrupted database.
|
||||
throw new BlockStoreException(e);
|
||||
} catch (ClassCastException e) {
|
||||
// Corrupted database.
|
||||
throw new BlockStoreException(e);
|
||||
} catch (ProtocolException e) {
|
||||
// Corrupted database.
|
||||
throw new BlockStoreException(e);
|
||||
} catch (IOException e) {
|
||||
// Corrupted database.
|
||||
throw new BlockStoreException(e);
|
||||
} finally {
|
||||
if (s != null)
|
||||
try {
|
||||
s.close();
|
||||
} catch (SQLException e) { throw new BlockStoreException("Failed to close PreparedStatement"); }
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public StoredBlock getChainHead() throws BlockStoreException {
|
||||
return chainHeadBlock;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setChainHead(StoredBlock chainHead) throws BlockStoreException {
|
||||
Sha256Hash hash = chainHead.getHeader().getHash();
|
||||
this.chainHeadHash = hash;
|
||||
this.chainHeadBlock = chainHead;
|
||||
maybeConnect();
|
||||
try {
|
||||
PreparedStatement s = conn.get()
|
||||
.prepareStatement("UPDATE settings SET value = ? WHERE name = ?");
|
||||
s.setString(2, CHAIN_HEAD_SETTING);
|
||||
s.setBytes(1, hash.getBytes());
|
||||
s.executeUpdate();
|
||||
s.close();
|
||||
} catch (SQLException ex) {
|
||||
throw new BlockStoreException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public StoredBlock getVerifiedChainHead() throws BlockStoreException {
|
||||
return verifiedChainHeadBlock;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVerifiedChainHead(StoredBlock chainHead) throws BlockStoreException {
|
||||
Sha256Hash hash = chainHead.getHeader().getHash();
|
||||
this.verifiedChainHeadHash = hash;
|
||||
this.verifiedChainHeadBlock = chainHead;
|
||||
maybeConnect();
|
||||
try {
|
||||
PreparedStatement s = conn.get()
|
||||
.prepareStatement("UPDATE settings SET value = ? WHERE name = ?");
|
||||
s.setString(2, VERIFIED_CHAIN_HEAD_SETTING);
|
||||
s.setBytes(1, hash.getBytes());
|
||||
s.executeUpdate();
|
||||
s.close();
|
||||
} catch (SQLException ex) {
|
||||
throw new BlockStoreException(ex);
|
||||
}
|
||||
if (this.chainHeadBlock.getHeight() < chainHead.getHeight())
|
||||
setChainHead(chainHead);
|
||||
removeUndoableBlocksWhereHeightIsLessThan(chainHead.getHeight() - fullStoreDepth);
|
||||
}
|
||||
|
||||
private void removeUndoableBlocksWhereHeightIsLessThan(int height) throws BlockStoreException {
|
||||
try {
|
||||
PreparedStatement s = conn.get()
|
||||
.prepareStatement("DELETE FROM undoableBlocks WHERE height <= ?");
|
||||
s.setInt(1, height);
|
||||
|
||||
if (log.isDebugEnabled())
|
||||
log.debug("Deleting undoable undoable block with height <= " + height);
|
||||
|
||||
|
||||
s.executeUpdate();
|
||||
s.close();
|
||||
} catch (SQLException ex) {
|
||||
throw new BlockStoreException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public StoredTransactionOutput getTransactionOutput(Sha256Hash hash, long index) throws BlockStoreException {
|
||||
maybeConnect();
|
||||
PreparedStatement s = null;
|
||||
try {
|
||||
s = conn.get()
|
||||
.prepareStatement("SELECT height, value, scriptBytes FROM openOutputs " +
|
||||
"WHERE hash = ? AND index = ?");
|
||||
s.setBytes(1, hash.getBytes());
|
||||
// index is actually an unsigned int
|
||||
s.setInt(2, (int)index);
|
||||
ResultSet results = s.executeQuery();
|
||||
if (!results.next()) {
|
||||
return null;
|
||||
}
|
||||
// Parse it.
|
||||
int height = results.getInt(1);
|
||||
Coin value = Coin.valueOf(new BigInteger(results.getBytes(2)).longValue());
|
||||
// Tell the StoredTransactionOutput that we are a coinbase, as that is encoded in height
|
||||
StoredTransactionOutput txout = new StoredTransactionOutput(hash, index, value, height, true, results.getBytes(3));
|
||||
return txout;
|
||||
} catch (SQLException ex) {
|
||||
throw new BlockStoreException(ex);
|
||||
} finally {
|
||||
if (s != null)
|
||||
try {
|
||||
s.close();
|
||||
} catch (SQLException e) { throw new BlockStoreException("Failed to close PreparedStatement"); }
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addUnspentTransactionOutput(StoredTransactionOutput out) throws BlockStoreException {
|
||||
maybeConnect();
|
||||
PreparedStatement s = null;
|
||||
|
||||
// Calculate the toAddress (if any)
|
||||
String dbAddress = "";
|
||||
int type = 0;
|
||||
Script outputScript = null;
|
||||
try
|
||||
{
|
||||
outputScript = new Script(out.getScriptBytes());
|
||||
}
|
||||
catch (ScriptException e)
|
||||
{
|
||||
// Unparseable, but this isn't an error - it's an output not containing an address
|
||||
log.info("Could not parse script for output: " + out.getHash().toString());
|
||||
}
|
||||
if (outputScript != null && (outputScript.isSentToAddress()
|
||||
|| outputScript.isSentToRawPubKey()
|
||||
|| outputScript.isPayToScriptHash()))
|
||||
{
|
||||
if (outputScript.isSentToAddress())
|
||||
{
|
||||
Address targetAddr = new Address(params, outputScript.getPubKeyHash());
|
||||
dbAddress = targetAddr.toString();
|
||||
type = 1;
|
||||
}
|
||||
else if (outputScript.isSentToRawPubKey())
|
||||
{
|
||||
/*
|
||||
* Note we use the deprecated getFromAddress here. Coinbase outputs seem to have the target address
|
||||
* in the pubkey of the script - perhaps we can rename this function?
|
||||
*/
|
||||
|
||||
dbAddress = outputScript.getFromAddress(params).toString();
|
||||
type = 2;
|
||||
} else {
|
||||
dbAddress = Address.fromP2SHHash(params, outputScript.getPubKeyHash()).toString();
|
||||
type = 3;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
s = conn.get().prepareStatement("INSERT INTO openOutputs (hash, index, height, value, scriptBytes, toAddress, addressTargetable) " +
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?)");
|
||||
s.setBytes(1, out.getHash().getBytes());
|
||||
// index is actually an unsigned int
|
||||
s.setInt(2, (int)out.getIndex());
|
||||
s.setInt(3, out.getHeight());
|
||||
s.setBytes(4, BigInteger.valueOf(out.getValue().value).toByteArray());
|
||||
s.setBytes(5, out.getScriptBytes());
|
||||
s.setString(6, dbAddress);
|
||||
s.setInt(7, type);
|
||||
s.executeUpdate();
|
||||
s.close();
|
||||
} catch (SQLException e) {
|
||||
if (!(e.getSQLState().equals(POSTGRES_DUPLICATE_KEY_ERROR_CODE)))
|
||||
throw new BlockStoreException(e);
|
||||
} finally {
|
||||
if (s != null)
|
||||
try {
|
||||
s.close();
|
||||
} catch (SQLException e) { throw new BlockStoreException(e); }
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeUnspentTransactionOutput(StoredTransactionOutput out) throws BlockStoreException {
|
||||
maybeConnect();
|
||||
// TODO: This should only need one query (maybe a stored procedure)
|
||||
if (getTransactionOutput(out.getHash(), out.getIndex()) == null)
|
||||
throw new BlockStoreException("Tried to remove a StoredTransactionOutput from PostgresFullPrunedBlockStore that it didn't have!");
|
||||
try {
|
||||
PreparedStatement s = conn.get()
|
||||
.prepareStatement("DELETE FROM openOutputs WHERE hash = ? AND index = ?");
|
||||
s.setBytes(1, out.getHash().getBytes());
|
||||
// index is actually an unsigned int
|
||||
s.setInt(2, (int)out.getIndex());
|
||||
s.executeUpdate();
|
||||
s.close();
|
||||
} catch (SQLException e) {
|
||||
throw new BlockStoreException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beginDatabaseBatchWrite() throws BlockStoreException {
|
||||
|
||||
maybeConnect();
|
||||
if (log.isDebugEnabled())
|
||||
log.debug("Starting database batch write with connection: " + conn.get().toString());
|
||||
|
||||
|
||||
try {
|
||||
conn.get().setAutoCommit(false);
|
||||
} catch (SQLException e) {
|
||||
throw new BlockStoreException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void commitDatabaseBatchWrite() throws BlockStoreException {
|
||||
maybeConnect();
|
||||
|
||||
if (log.isDebugEnabled())
|
||||
log.debug("Committing database batch write with connection: " + conn.get().toString());
|
||||
|
||||
|
||||
try {
|
||||
conn.get().commit();
|
||||
conn.get().setAutoCommit(true);
|
||||
} catch (SQLException e) {
|
||||
throw new BlockStoreException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void abortDatabaseBatchWrite() throws BlockStoreException {
|
||||
|
||||
maybeConnect();
|
||||
if (log.isDebugEnabled())
|
||||
log.debug("Rollback database batch write with connection: " + conn.get().toString());
|
||||
|
||||
try {
|
||||
if (!conn.get().getAutoCommit()) {
|
||||
conn.get().rollback();
|
||||
conn.get().setAutoCommit(true);
|
||||
} else {
|
||||
log.warn("Warning: Rollback attempt without transaction");
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
throw new BlockStoreException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasUnspentOutputs(Sha256Hash hash, int numOutputs) throws BlockStoreException {
|
||||
maybeConnect();
|
||||
PreparedStatement s = null;
|
||||
try {
|
||||
s = conn.get()
|
||||
.prepareStatement("SELECT COUNT(*) FROM openOutputs WHERE hash = ?");
|
||||
s.setBytes(1, hash.getBytes());
|
||||
ResultSet results = s.executeQuery();
|
||||
if (!results.next()) {
|
||||
throw new BlockStoreException("Got no results from a COUNT(*) query");
|
||||
}
|
||||
int count = results.getInt(1);
|
||||
return count != 0;
|
||||
} catch (SQLException ex) {
|
||||
throw new BlockStoreException(ex);
|
||||
} finally {
|
||||
if (s != null)
|
||||
try {
|
||||
s.close();
|
||||
} catch (SQLException e) { throw new BlockStoreException("Failed to close PreparedStatement"); }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the balance for a coinbase, to-address, or p2sh address.
|
||||
* @param address The address to calculate the balance of
|
||||
* @return The balance of the address supplied. If the address has not been seen, or there are no outputs open for this
|
||||
* address, the return value is 0
|
||||
* @throws BlockStoreException
|
||||
*/
|
||||
public BigInteger calculateBalanceForAddress(Address address) throws BlockStoreException {
|
||||
maybeConnect();
|
||||
PreparedStatement s = null;
|
||||
|
||||
|
||||
try {
|
||||
s = conn.get().prepareStatement("select sum(('x'||lpad(substr(value::text, 3, 50),16,'0'))::bit(64)::bigint) "
|
||||
+ "from openoutputs where toaddress = ?");
|
||||
s.setString(1, address.toString());
|
||||
ResultSet rs = s.executeQuery();
|
||||
if (rs.next()) {
|
||||
return BigInteger.valueOf(rs.getLong(1));
|
||||
} else {
|
||||
throw new BlockStoreException("Failed to execute balance lookup");
|
||||
}
|
||||
|
||||
} catch (SQLException ex) {
|
||||
throw new BlockStoreException(ex);
|
||||
} finally {
|
||||
if (s != null)
|
||||
try {
|
||||
s.close();
|
||||
} catch (SQLException e) {
|
||||
throw new BlockStoreException("Could not close statement");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -276,6 +276,11 @@ public class SPVBlockStore implements BlockStore {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public NetworkParameters getParams() {
|
||||
return params;
|
||||
}
|
||||
|
||||
protected static final int RECORD_SIZE = 32 /* hash */ + StoredBlock.COMPACT_SERIALIZED_SIZE;
|
||||
|
||||
// File format:
|
||||
|
@ -72,7 +72,8 @@ import static com.google.common.base.Preconditions.checkNotNull;
|
||||
*/
|
||||
public class WalletProtobufSerializer {
|
||||
private static final Logger log = LoggerFactory.getLogger(WalletProtobufSerializer.class);
|
||||
|
||||
/** Current version used for serializing wallets. A version higher than this is considered from the future. */
|
||||
public static final int CURRENT_WALLET_VERSION = Protos.Wallet.getDefaultInstance().getVersion();
|
||||
// Used for de-serialization
|
||||
protected Map<ByteString, Transaction> txMap;
|
||||
|
||||
@ -366,9 +367,9 @@ public class WalletProtobufSerializer {
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Parses a wallet from the given stream, using the provided Wallet instance to load data into. This is primarily
|
||||
* used when you want to register extensions. Data in the proto will be added into the wallet where applicable and
|
||||
* overwrite where not.</p>
|
||||
* <p>Loads wallet data from the given protocol buffer and inserts it into the given Wallet object. This is primarily
|
||||
* useful when you wish to pre-register extension objects. Note that if loading fails the provided Wallet object
|
||||
* may be in an indeterminate state and should be thrown away.</p>
|
||||
*
|
||||
* <p>A wallet can be unreadable for various reasons, such as inability to open the file, corrupt data, internally
|
||||
* inconsistent data, a wallet extension marked as mandatory that cannot be handled and so on. You should always
|
||||
@ -376,16 +377,18 @@ public class WalletProtobufSerializer {
|
||||
*
|
||||
* @throws UnreadableWalletException thrown in various error conditions (see description).
|
||||
*/
|
||||
public Wallet readWallet(InputStream input) throws UnreadableWalletException {
|
||||
public Wallet readWallet(InputStream input, @Nullable WalletExtension... walletExtensions) throws UnreadableWalletException {
|
||||
try {
|
||||
Protos.Wallet walletProto = parseToProto(input);
|
||||
final String paramsID = walletProto.getNetworkIdentifier();
|
||||
NetworkParameters params = NetworkParameters.fromID(paramsID);
|
||||
if (params == null)
|
||||
throw new UnreadableWalletException("Unknown network parameters ID " + paramsID);
|
||||
return readWallet(params, null, walletProto);
|
||||
return readWallet(params, walletExtensions, walletProto);
|
||||
} catch (IOException e) {
|
||||
throw new UnreadableWalletException("Could not parse input stream to protobuf", e);
|
||||
} catch (IllegalStateException e) {
|
||||
throw new UnreadableWalletException("Could not parse input stream to protobuf", e);
|
||||
}
|
||||
}
|
||||
|
||||
@ -402,7 +405,7 @@ public class WalletProtobufSerializer {
|
||||
*/
|
||||
public Wallet readWallet(NetworkParameters params, @Nullable WalletExtension[] extensions,
|
||||
Protos.Wallet walletProto) throws UnreadableWalletException {
|
||||
if (walletProto.getVersion() > 1)
|
||||
if (walletProto.getVersion() > CURRENT_WALLET_VERSION)
|
||||
throw new UnreadableWalletException.FutureVersion();
|
||||
if (!walletProto.getNetworkIdentifier().equals(params.getId()))
|
||||
throw new UnreadableWalletException.WrongNetwork();
|
||||
@ -513,7 +516,7 @@ public class WalletProtobufSerializer {
|
||||
} else {
|
||||
log.info("Loading wallet extension {}", id);
|
||||
try {
|
||||
wallet.deserializeAndAddExtension(extension, extProto.getData().toByteArray());
|
||||
wallet.deserializeExtension(extension, extProto.getData().toByteArray());
|
||||
} catch (Exception e) {
|
||||
if (extProto.getMandatory() && requireMandatoryExtensions) {
|
||||
log.error("Error whilst reading extension {}, failing to read wallet", id, e);
|
||||
|
@ -17,6 +17,8 @@
|
||||
package com.dogecoin.dogecoinj.testing;
|
||||
|
||||
import com.dogecoin.dogecoinj.core.*;
|
||||
import com.dogecoin.dogecoinj.crypto.TransactionSignature;
|
||||
import com.dogecoin.dogecoinj.script.ScriptBuilder;
|
||||
import com.dogecoin.dogecoinj.store.BlockStore;
|
||||
import com.dogecoin.dogecoinj.store.BlockStoreException;
|
||||
|
||||
@ -43,7 +45,8 @@ public class FakeTxBuilder {
|
||||
TransactionOutput prevOut = new TransactionOutput(params, prevTx, value, to);
|
||||
prevTx.addOutput(prevOut);
|
||||
// Connect it.
|
||||
t.addInput(prevOut);
|
||||
t.addInput(prevOut).setScriptSig(ScriptBuilder.createInputScript(TransactionSignature.dummy()));
|
||||
// Fake signature.
|
||||
// Serialize/deserialize to ensure internal state is stripped, as if it had been read from the wire.
|
||||
return roundTripTransaction(params, t);
|
||||
}
|
||||
|
@ -0,0 +1,40 @@
|
||||
package com.dogecoin.dogecoinj.testing;
|
||||
|
||||
import com.dogecoin.dogecoinj.core.Wallet;
|
||||
import com.dogecoin.dogecoinj.core.WalletExtension;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
|
||||
public class FooWalletExtension implements WalletExtension {
|
||||
private final byte[] data = new byte[]{1, 2, 3};
|
||||
|
||||
private final boolean isMandatory;
|
||||
private final String id;
|
||||
|
||||
public FooWalletExtension(String id, boolean isMandatory) {
|
||||
this.isMandatory = isMandatory;
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getWalletExtensionID() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWalletExtensionMandatory() {
|
||||
return isMandatory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] serializeWalletExtension() {
|
||||
return data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deserializeWalletExtension(Wallet wallet, byte[] data) {
|
||||
checkArgument(Arrays.equals(this.data, data));
|
||||
}
|
||||
}
|
@ -16,21 +16,27 @@
|
||||
|
||||
package com.dogecoin.dogecoinj.testing;
|
||||
|
||||
import com.google.common.util.concurrent.*;
|
||||
import com.dogecoin.dogecoinj.core.*;
|
||||
import com.dogecoin.dogecoinj.net.BlockingClientManager;
|
||||
import com.dogecoin.dogecoinj.net.ClientConnectionManager;
|
||||
import com.dogecoin.dogecoinj.net.NioClientManager;
|
||||
import com.dogecoin.dogecoinj.params.UnitTestParams;
|
||||
import com.dogecoin.dogecoinj.store.BlockStore;
|
||||
import com.dogecoin.dogecoinj.store.MemoryBlockStore;
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.dogecoin.dogecoinj.utils.DaemonThreadFactory;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
|
||||
/**
|
||||
* Utility class that makes it easy to work with mock NetworkConnections in PeerGroups.
|
||||
* You can derive from this class and call peerGroup.start() in your tests to get a functional PeerGroup that can be
|
||||
* used with loopback peers created using connectPeer. This involves real TCP connections so is a pretty accurate
|
||||
* mock, but means unit tests cannot be run simultaneously.
|
||||
*/
|
||||
public class TestWithPeerGroup extends TestWithNetworkConnections {
|
||||
protected static final NetworkParameters params = UnitTestParams.get();
|
||||
@ -58,6 +64,7 @@ public class TestWithPeerGroup extends TestWithNetworkConnections {
|
||||
remoteVersionMessage = new VersionMessage(unitTestParams, 1);
|
||||
remoteVersionMessage.localServices = VersionMessage.NODE_NETWORK;
|
||||
remoteVersionMessage.clientVersion = NotFoundMessage.MIN_PROTOCOL_VERSION;
|
||||
blockJobs = false;
|
||||
initPeerGroup();
|
||||
}
|
||||
|
||||
@ -65,9 +72,10 @@ public class TestWithPeerGroup extends TestWithNetworkConnections {
|
||||
public void tearDown() {
|
||||
try {
|
||||
super.tearDown();
|
||||
blockJobs = false;
|
||||
Utils.finishMockSleep();
|
||||
peerGroup.stopAsync();
|
||||
peerGroup.awaitTerminated();
|
||||
if (peerGroup.isRunning())
|
||||
peerGroup.stopAsync();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
@ -75,11 +83,38 @@ public class TestWithPeerGroup extends TestWithNetworkConnections {
|
||||
|
||||
protected void initPeerGroup() {
|
||||
if (clientType == ClientType.NIO_CLIENT_MANAGER)
|
||||
peerGroup = new PeerGroup(unitTestParams, blockChain, new NioClientManager());
|
||||
peerGroup = createPeerGroup(new NioClientManager());
|
||||
else
|
||||
peerGroup = new PeerGroup(unitTestParams, blockChain, new BlockingClientManager());
|
||||
peerGroup = createPeerGroup(new BlockingClientManager());
|
||||
peerGroup.setPingIntervalMsec(0); // Disable the pings as they just get in the way of most tests.
|
||||
peerGroup.addWallet(wallet);
|
||||
peerGroup.setUseLocalhostPeerWhenPossible(false); // Prevents from connecting to bitcoin nodes on localhost.
|
||||
}
|
||||
|
||||
protected boolean blockJobs = false;
|
||||
protected final Semaphore jobBlocks = new Semaphore(0);
|
||||
|
||||
private PeerGroup createPeerGroup(final ClientConnectionManager manager) {
|
||||
return new PeerGroup(unitTestParams, blockChain, manager) {
|
||||
@Override
|
||||
protected ListeningScheduledExecutorService createPrivateExecutor() {
|
||||
return MoreExecutors.listeningDecorator(new ScheduledThreadPoolExecutor(1, new DaemonThreadFactory("PeerGroup test thread")) {
|
||||
@Override
|
||||
public ScheduledFuture<?> schedule(final Runnable command, final long delay, final TimeUnit unit) {
|
||||
if (!blockJobs)
|
||||
return super.schedule(command, delay, unit);
|
||||
return super.schedule(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Utils.rollMockClockMillis(unit.toMillis(delay));
|
||||
command.run();
|
||||
jobBlocks.acquireUninterruptibly();
|
||||
}
|
||||
}, 0 /* immediate */, unit);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected InboundMessageQueuer connectPeerWithoutVersionExchange(int id) throws Exception {
|
||||
|
@ -23,7 +23,6 @@ import com.dogecoin.dogecoinj.store.MemoryBlockStore;
|
||||
import com.dogecoin.dogecoinj.utils.BriefLogFormatter;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.IOException;
|
||||
|
||||
import static com.dogecoin.dogecoinj.testing.FakeTxBuilder.createFakeBlock;
|
||||
import static com.dogecoin.dogecoinj.testing.FakeTxBuilder.createFakeTx;
|
||||
@ -62,7 +61,7 @@ public class TestWithWallet {
|
||||
|
||||
@Nullable
|
||||
protected Transaction sendMoneyToWallet(Wallet wallet, Transaction tx, AbstractBlockChain.NewBlockType type)
|
||||
throws IOException, VerificationException {
|
||||
throws VerificationException {
|
||||
if (type == null) {
|
||||
// Pending/broadcast tx.
|
||||
if (wallet.isPendingTransactionRelevant(tx))
|
||||
@ -77,22 +76,22 @@ public class TestWithWallet {
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected Transaction sendMoneyToWallet(Transaction tx, AbstractBlockChain.NewBlockType type) throws IOException, VerificationException {
|
||||
protected Transaction sendMoneyToWallet(Transaction tx, AbstractBlockChain.NewBlockType type) throws VerificationException {
|
||||
return sendMoneyToWallet(this.wallet, tx, type);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected Transaction sendMoneyToWallet(Wallet wallet, Coin value, Address toAddress, AbstractBlockChain.NewBlockType type) throws IOException, VerificationException {
|
||||
protected Transaction sendMoneyToWallet(Wallet wallet, Coin value, Address toAddress, AbstractBlockChain.NewBlockType type) throws VerificationException {
|
||||
return sendMoneyToWallet(wallet, createFakeTx(params, value, toAddress), type);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected Transaction sendMoneyToWallet(Wallet wallet, Coin value, ECKey toPubKey, AbstractBlockChain.NewBlockType type) throws IOException, VerificationException {
|
||||
protected Transaction sendMoneyToWallet(Wallet wallet, Coin value, ECKey toPubKey, AbstractBlockChain.NewBlockType type) throws VerificationException {
|
||||
return sendMoneyToWallet(wallet, createFakeTx(params, value, toPubKey), type);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected Transaction sendMoneyToWallet(Coin value, AbstractBlockChain.NewBlockType type) throws IOException, VerificationException {
|
||||
protected Transaction sendMoneyToWallet(Coin value, AbstractBlockChain.NewBlockType type) throws VerificationException {
|
||||
return sendMoneyToWallet(this.wallet, createFakeTx(params, value, myAddress), type);
|
||||
}
|
||||
}
|
||||
|
@ -1,19 +1,17 @@
|
||||
/*
|
||||
* Copyright 2012, 2014 the original author or authors.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.dogecoin.dogecoinj.uri;
|
||||
@ -153,7 +151,7 @@ public class BitcoinURI {
|
||||
}
|
||||
|
||||
// Split off the address from the rest of the query parameters.
|
||||
String[] addressSplitTokens = schemeSpecificPart.split("\\?");
|
||||
String[] addressSplitTokens = schemeSpecificPart.split("\\?", 2);
|
||||
if (addressSplitTokens.length == 0)
|
||||
throw new BitcoinURIParseException("No data found after the bitcoin: prefix");
|
||||
String addressToken = addressSplitTokens[0]; // may be empty!
|
||||
@ -163,12 +161,8 @@ public class BitcoinURI {
|
||||
// Only an address is specified - use an empty '<name>=<value>' token array.
|
||||
nameValuePairTokens = new String[] {};
|
||||
} else {
|
||||
if (addressSplitTokens.length == 2) {
|
||||
// Split into '<name>=<value>' tokens.
|
||||
nameValuePairTokens = addressSplitTokens[1].split("&");
|
||||
} else {
|
||||
throw new BitcoinURIParseException("Too many question marks in URI '" + uri + "'");
|
||||
}
|
||||
// Split into '<name>=<value>' tokens.
|
||||
nameValuePairTokens = addressSplitTokens[1].split("&");
|
||||
}
|
||||
|
||||
// Attempt to parse the rest of the URI parameters.
|
||||
|
@ -1,19 +1,17 @@
|
||||
/*
|
||||
* Copyright 2012 the original author or authors.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.dogecoin.dogecoinj.uri;
|
||||
|
@ -1,16 +1,28 @@
|
||||
package com.dogecoin.dogecoinj.utils;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
|
||||
/** Thread factory whose threads are marked as daemon and won't prevent process exit. */
|
||||
public class DaemonThreadFactory implements ThreadFactory {
|
||||
@Nullable private final String name;
|
||||
|
||||
public DaemonThreadFactory(@Nullable String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public DaemonThreadFactory() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Thread newThread(@Nonnull Runnable runnable) {
|
||||
Thread thread = Executors.defaultThreadFactory().newThread(runnable);
|
||||
thread.setDaemon(true);
|
||||
if (name != null)
|
||||
thread.setName(name);
|
||||
return thread;
|
||||
}
|
||||
}
|
||||
|
@ -81,8 +81,13 @@ public final class Fiat implements Monetary, Comparable<Fiat>, Serializable {
|
||||
* if you try to specify fractional satoshis, or a value out of range.
|
||||
*/
|
||||
public static Fiat parseFiat(final String currencyCode, final String str) {
|
||||
return Fiat.valueOf(currencyCode, new BigDecimal(str).movePointRight(SMALLEST_UNIT_EXPONENT)
|
||||
.toBigIntegerExact().longValue());
|
||||
try {
|
||||
long val = new BigDecimal(str).movePointRight(SMALLEST_UNIT_EXPONENT)
|
||||
.toBigIntegerExact().longValue();
|
||||
return Fiat.valueOf(currencyCode, val);
|
||||
} catch (ArithmeticException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public Fiat add(final Fiat value) {
|
||||
|
@ -1,4 +1,4 @@
|
||||
package org.bitcoinj.wallet;
|
||||
package com.dogecoin.dogecoinj.wallet;
|
||||
|
||||
/**
|
||||
* Indicates that an attempt was made to upgrade a random wallet to deterministic, but there were no non-rotating
|
@ -167,8 +167,9 @@ public class BasicKeyChain implements EncryptableKeyChain {
|
||||
}
|
||||
|
||||
private void importKeyLocked(ECKey key) {
|
||||
pubkeyToKeys.put(ByteString.copyFrom(key.getPubKey()), key);
|
||||
ECKey previousKey = pubkeyToKeys.put(ByteString.copyFrom(key.getPubKey()), key);
|
||||
hashToKeys.put(ByteString.copyFrom(key.getPubKeyHash()), key);
|
||||
checkState(previousKey == null);
|
||||
}
|
||||
|
||||
private void importKeysLocked(List<ECKey> keys) {
|
||||
|
@ -7,7 +7,7 @@ import java.util.Collection;
|
||||
|
||||
/**
|
||||
* Represents the results of a
|
||||
* {@link CoinSelector#select(Coin, java.util.LinkedList)} operation. A
|
||||
* {@link CoinSelector#select(Coin, java.util.List)} operation. A
|
||||
* coin selection represents a list of spendable transaction outputs that sum together to give valueGathered.
|
||||
* Different coin selections could be produced by different coin selectors from the same input set, according
|
||||
* to their varying policies.
|
||||
|
@ -20,7 +20,7 @@ public class DefaultCoinSelector implements CoinSelector {
|
||||
@Override
|
||||
public CoinSelection select(Coin biTarget, List<TransactionOutput> candidates) {
|
||||
long target = biTarget.value;
|
||||
HashSet<TransactionOutput> selected = new HashSet<TransactionOutput>();
|
||||
ArrayList<TransactionOutput> selected = new ArrayList<TransactionOutput>();
|
||||
// Sort the inputs by age*value so we get the highest "coindays" spent.
|
||||
// TODO: Consider changing the wallets internal format to track just outputs and keep them ordered.
|
||||
ArrayList<TransactionOutput> sortedOutputs = new ArrayList<TransactionOutput>(candidates);
|
||||
@ -48,14 +48,8 @@ public class DefaultCoinSelector implements CoinSelector {
|
||||
Collections.sort(outputs, new Comparator<TransactionOutput>() {
|
||||
@Override
|
||||
public int compare(TransactionOutput a, TransactionOutput b) {
|
||||
int depth1 = 0;
|
||||
int depth2 = 0;
|
||||
TransactionConfidence conf1 = a.getParentTransaction().getConfidence();
|
||||
TransactionConfidence conf2 = b.getParentTransaction().getConfidence();
|
||||
if (conf1.getConfidenceType() == TransactionConfidence.ConfidenceType.BUILDING)
|
||||
depth1 = conf1.getDepthInBlocks();
|
||||
if (conf2.getConfidenceType() == TransactionConfidence.ConfidenceType.BUILDING)
|
||||
depth2 = conf2.getDepthInBlocks();
|
||||
int depth1 = a.getParentTransactionDepthInBlocks();
|
||||
int depth2 = b.getParentTransactionDepthInBlocks();
|
||||
Coin aValue = a.getValue();
|
||||
Coin bValue = b.getValue();
|
||||
BigInteger aCoinDepth = BigInteger.valueOf(aValue.value).multiply(BigInteger.valueOf(depth1));
|
||||
@ -66,8 +60,8 @@ public class DefaultCoinSelector implements CoinSelector {
|
||||
int c2 = bValue.compareTo(aValue);
|
||||
if (c2 != 0) return c2;
|
||||
// They are entirely equivalent (possibly pending) so sort by hash to ensure a total ordering.
|
||||
BigInteger aHash = a.getParentTransaction().getHash().toBigInteger();
|
||||
BigInteger bHash = b.getParentTransaction().getHash().toBigInteger();
|
||||
BigInteger aHash = a.getParentTransactionHash().toBigInteger();
|
||||
BigInteger bHash = b.getParentTransactionHash().toBigInteger();
|
||||
return aHash.compareTo(bHash);
|
||||
}
|
||||
});
|
||||
@ -75,7 +69,10 @@ public class DefaultCoinSelector implements CoinSelector {
|
||||
|
||||
/** Sub-classes can override this to just customize whether transactions are usable, but keep age sorting. */
|
||||
protected boolean shouldSelect(Transaction tx) {
|
||||
return isSelectable(tx);
|
||||
if (tx != null) {
|
||||
return isSelectable(tx);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean isSelectable(Transaction tx) {
|
||||
|
@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright 2013 The bitcoinj developers.
|
||||
* Copyright 2014 The bitcoinj developers.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -797,7 +797,7 @@ public class DeterministicKeyChain implements EncryptableKeyChain {
|
||||
for (int i : key.getDeterministicKey().getPathList())
|
||||
path.add(new ChildNumber(i));
|
||||
// Deserialize the public key and path.
|
||||
ECPoint pubkey = ECKey.CURVE.getCurve().decodePoint(key.getPublicKey().toByteArray());
|
||||
LazyECPoint pubkey = new LazyECPoint(ECKey.CURVE.getCurve(), key.getPublicKey().toByteArray());
|
||||
final ImmutableList<ChildNumber> immutablePath = ImmutableList.copyOf(path);
|
||||
// Possibly create the chain, if we didn't already do so yet.
|
||||
boolean isWatchingAccountKey = false;
|
||||
@ -1200,6 +1200,12 @@ public class DeterministicKeyChain implements EncryptableKeyChain {
|
||||
return keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns only the keys that have been issued by this chain, lookahead not included.
|
||||
*/
|
||||
public List<ECKey> getIssuedReceiveKeys() {
|
||||
return getKeys(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns leaf keys issued by this chain (including lookahead zone)
|
||||
@ -1277,7 +1283,8 @@ public class DeterministicKeyChain implements EncryptableKeyChain {
|
||||
seed.toHexString())
|
||||
);
|
||||
}
|
||||
builder2.append(String.format("Seed birthday: %d [%s]%n", seed.getCreationTimeSeconds(), new Date(seed.getCreationTimeSeconds() * 1000)));
|
||||
builder2.append(String.format("Seed birthday: %d [%s]%n", seed.getCreationTimeSeconds(),
|
||||
Utils.dateTimeFormat(seed.getCreationTimeSeconds() * 1000)));
|
||||
}
|
||||
final DeterministicKey watchingKey = getWatchingKey();
|
||||
// Don't show if it's been imported from a watching wallet already, because it'd result in a weird/
|
||||
@ -1285,7 +1292,7 @@ public class DeterministicKeyChain implements EncryptableKeyChain {
|
||||
// due to the parent fingerprint being missing/not stored. In future we could store the parent fingerprint
|
||||
// optionally as well to fix this, but it seems unimportant for now.
|
||||
if (watchingKey.getParent() != null) {
|
||||
builder2.append(String.format("Key to watch: %s%n", watchingKey.serializePubB58()));
|
||||
builder2.append(String.format("Key to watch: %s%n", watchingKey.serializePubB58(params)));
|
||||
}
|
||||
formatAddresses(includePrivateKeys, params, builder2);
|
||||
return builder2.toString();
|
||||
|
@ -21,6 +21,7 @@ import com.dogecoin.dogecoinj.core.*;
|
||||
import com.dogecoin.dogecoinj.crypto.ChildNumber;
|
||||
import com.dogecoin.dogecoinj.crypto.DeterministicKey;
|
||||
import com.dogecoin.dogecoinj.crypto.KeyCrypter;
|
||||
import com.dogecoin.dogecoinj.crypto.LinuxSecureRandom;
|
||||
import com.dogecoin.dogecoinj.script.Script;
|
||||
import com.dogecoin.dogecoinj.script.ScriptBuilder;
|
||||
import com.dogecoin.dogecoinj.store.UnreadableWalletException;
|
||||
@ -33,7 +34,6 @@ import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Multimap;
|
||||
import com.google.protobuf.ByteString;
|
||||
import org.bitcoinj.wallet.AllRandomKeysRotating;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.spongycastle.crypto.params.KeyParameter;
|
||||
@ -65,14 +65,22 @@ import static com.google.common.base.Preconditions.*;
|
||||
* class docs for {@link DeterministicKeyChain} for more information on this topic.</p>
|
||||
*/
|
||||
public class KeyChainGroup implements KeyBag {
|
||||
|
||||
static {
|
||||
// Init proper random number generator, as some old Android installations have bugs that make it unsecure.
|
||||
if (Utils.isAndroidRuntime())
|
||||
new LinuxSecureRandom();
|
||||
}
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(KeyChainGroup.class);
|
||||
|
||||
private BasicKeyChain basic;
|
||||
private NetworkParameters params;
|
||||
protected final LinkedList<DeterministicKeyChain> chains;
|
||||
// currentKeys is used for normal, non-multisig/married wallets. currentAddresses is used when we're handing out
|
||||
// P2SH addresses. They're mutually exclusive.
|
||||
private final EnumMap<KeyChain.KeyPurpose, DeterministicKey> currentKeys;
|
||||
|
||||
private EnumMap<KeyChain.KeyPurpose, Address> currentAddresses;
|
||||
private final EnumMap<KeyChain.KeyPurpose, Address> currentAddresses;
|
||||
@Nullable private KeyCrypter keyCrypter;
|
||||
private int lookaheadSize = -1;
|
||||
private int lookaheadThreshold = -1;
|
||||
@ -359,6 +367,22 @@ public class KeyChainGroup implements KeyBag {
|
||||
return null;
|
||||
}
|
||||
|
||||
public void markP2SHAddressAsUsed(Address address) {
|
||||
checkState(isMarried());
|
||||
checkArgument(address.isP2SHAddress());
|
||||
RedeemData data = findRedeemDataFromScriptHash(address.getHash160());
|
||||
if (data == null)
|
||||
return; // Not our P2SH address.
|
||||
for (ECKey key : data.keys) {
|
||||
for (DeterministicKeyChain chain : chains) {
|
||||
DeterministicKey k = chain.findKeyFromPubKey(key.getPubKey());
|
||||
if (k == null) continue;
|
||||
chain.markKeyAsUsed(k);
|
||||
maybeMarkCurrentAddressAsUsed(address);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public ECKey findKeyFromPubHash(byte[] pubkeyHash) {
|
||||
@ -386,12 +410,27 @@ public class KeyChainGroup implements KeyBag {
|
||||
}
|
||||
}
|
||||
|
||||
/** If the given P2SH address is "current", advance it to a new one. */
|
||||
private void maybeMarkCurrentAddressAsUsed(Address address) {
|
||||
checkState(isMarried());
|
||||
checkArgument(address.isP2SHAddress());
|
||||
for (Map.Entry<KeyChain.KeyPurpose, Address> entry : currentAddresses.entrySet()) {
|
||||
if (entry.getValue() != null && entry.getValue().equals(address)) {
|
||||
log.info("Marking P2SH address as used: {}", address);
|
||||
currentAddresses.put(entry.getKey(), freshAddress(entry.getKey()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** If the given key is "current", advance the current key to a new one. */
|
||||
private void maybeMarkCurrentKeyAsUsed(DeterministicKey key) {
|
||||
// It's OK for currentKeys to be empty here: it means we're a married wallet and the key may be a part of a
|
||||
// rotating chain.
|
||||
for (Map.Entry<KeyChain.KeyPurpose, DeterministicKey> entry : currentKeys.entrySet()) {
|
||||
if (entry.getValue() != null && entry.getValue().equals(key)) {
|
||||
log.info("Marking key as used: {}", key);
|
||||
currentKeys.put(entry.getKey(), null);
|
||||
currentKeys.put(entry.getKey(), freshKey(entry.getKey()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -232,7 +232,7 @@ public class MarriedKeyChain extends DeterministicKeyChain {
|
||||
@Override
|
||||
protected void formatAddresses(boolean includePrivateKeys, NetworkParameters params, StringBuilder builder2) {
|
||||
for (DeterministicKeyChain followingChain : followingKeyChains) {
|
||||
builder2.append(String.format("Following chain: %s%n", followingChain.getWatchingKey().serializePubB58()));
|
||||
builder2.append(String.format("Following chain: %s%n", followingChain.getWatchingKey().serializePubB58(params)));
|
||||
}
|
||||
builder2.append(String.format("%n"));
|
||||
for (RedeemData redeemData : marriedKeysRedeemData.values())
|
||||
|
@ -14255,7 +14255,8 @@ public final class Protos {
|
||||
*
|
||||
* <pre>
|
||||
* The version number of the wallet - used to detect wallets that were produced in the future
|
||||
* (i.e the wallet may contain some future format this protobuf/ code does not know about)
|
||||
* (i.e. the wallet may contain some future format this protobuf or parser code does not know about).
|
||||
* A version that's higher than the default is considered from the future.
|
||||
* </pre>
|
||||
*/
|
||||
boolean hasVersion();
|
||||
@ -14264,7 +14265,8 @@ public final class Protos {
|
||||
*
|
||||
* <pre>
|
||||
* The version number of the wallet - used to detect wallets that were produced in the future
|
||||
* (i.e the wallet may contain some future format this protobuf/ code does not know about)
|
||||
* (i.e. the wallet may contain some future format this protobuf or parser code does not know about).
|
||||
* A version that's higher than the default is considered from the future.
|
||||
* </pre>
|
||||
*/
|
||||
int getVersion();
|
||||
@ -15013,7 +15015,8 @@ public final class Protos {
|
||||
*
|
||||
* <pre>
|
||||
* The version number of the wallet - used to detect wallets that were produced in the future
|
||||
* (i.e the wallet may contain some future format this protobuf/ code does not know about)
|
||||
* (i.e. the wallet may contain some future format this protobuf or parser code does not know about).
|
||||
* A version that's higher than the default is considered from the future.
|
||||
* </pre>
|
||||
*/
|
||||
public boolean hasVersion() {
|
||||
@ -15024,7 +15027,8 @@ public final class Protos {
|
||||
*
|
||||
* <pre>
|
||||
* The version number of the wallet - used to detect wallets that were produced in the future
|
||||
* (i.e the wallet may contain some future format this protobuf/ code does not know about)
|
||||
* (i.e. the wallet may contain some future format this protobuf or parser code does not know about).
|
||||
* A version that's higher than the default is considered from the future.
|
||||
* </pre>
|
||||
*/
|
||||
public int getVersion() {
|
||||
@ -17126,7 +17130,8 @@ public final class Protos {
|
||||
*
|
||||
* <pre>
|
||||
* The version number of the wallet - used to detect wallets that were produced in the future
|
||||
* (i.e the wallet may contain some future format this protobuf/ code does not know about)
|
||||
* (i.e. the wallet may contain some future format this protobuf or parser code does not know about).
|
||||
* A version that's higher than the default is considered from the future.
|
||||
* </pre>
|
||||
*/
|
||||
public boolean hasVersion() {
|
||||
@ -17137,7 +17142,8 @@ public final class Protos {
|
||||
*
|
||||
* <pre>
|
||||
* The version number of the wallet - used to detect wallets that were produced in the future
|
||||
* (i.e the wallet may contain some future format this protobuf/ code does not know about)
|
||||
* (i.e. the wallet may contain some future format this protobuf or parser code does not know about).
|
||||
* A version that's higher than the default is considered from the future.
|
||||
* </pre>
|
||||
*/
|
||||
public int getVersion() {
|
||||
@ -17148,7 +17154,8 @@ public final class Protos {
|
||||
*
|
||||
* <pre>
|
||||
* The version number of the wallet - used to detect wallets that were produced in the future
|
||||
* (i.e the wallet may contain some future format this protobuf/ code does not know about)
|
||||
* (i.e. the wallet may contain some future format this protobuf or parser code does not know about).
|
||||
* A version that's higher than the default is considered from the future.
|
||||
* </pre>
|
||||
*/
|
||||
public Builder setVersion(int value) {
|
||||
@ -17162,7 +17169,8 @@ public final class Protos {
|
||||
*
|
||||
* <pre>
|
||||
* The version number of the wallet - used to detect wallets that were produced in the future
|
||||
* (i.e the wallet may contain some future format this protobuf/ code does not know about)
|
||||
* (i.e. the wallet may contain some future format this protobuf or parser code does not know about).
|
||||
* A version that's higher than the default is considered from the future.
|
||||
* </pre>
|
||||
*/
|
||||
public Builder clearVersion() {
|
||||
|
2299
core/src/main/java/org/bitcoin/crawler/PeerSeedProtos.java
Normal file
2299
core/src/main/java/org/bitcoin/crawler/PeerSeedProtos.java
Normal file
File diff suppressed because it is too large
Load Diff
26
core/src/peerseeds.proto
Normal file
26
core/src/peerseeds.proto
Normal file
@ -0,0 +1,26 @@
|
||||
package org.bitcoin.crawler;
|
||||
|
||||
//
|
||||
// A simple protocol for describing signed sets of IP addresses. Intended to be distributed via HTTP[S] or in files.
|
||||
//
|
||||
|
||||
option java_package = "org.bitcoin.crawler";
|
||||
option java_outer_classname = "PeerSeedProtos";
|
||||
|
||||
message PeerSeedData {
|
||||
required string ip_address = 1;
|
||||
required uint32 port = 2;
|
||||
required uint32 services = 3;
|
||||
}
|
||||
|
||||
message PeerSeeds {
|
||||
repeated PeerSeedData seed = 1;
|
||||
required uint64 timestamp = 2; // seconds since UNIX epoch
|
||||
required string net = 3;
|
||||
}
|
||||
|
||||
message SignedPeerSeeds {
|
||||
required bytes peer_seeds = 1;
|
||||
required bytes signature = 2;
|
||||
required bytes pubkey = 3;
|
||||
}
|
@ -39,6 +39,8 @@ message StoredClientPaymentChannel {
|
||||
required bytes id = 1;
|
||||
required bytes contractTransaction = 2;
|
||||
required bytes refundTransaction = 3;
|
||||
required bytes myPublicKey = 8;
|
||||
// Deprecated, key is already stored in the wallet, and found using myPublicKey;
|
||||
required bytes myKey = 4;
|
||||
required uint64 valueToMe = 5;
|
||||
required uint64 refundFees = 6;
|
||||
|
@ -17,6 +17,7 @@
|
||||
|
||||
package com.dogecoin.dogecoinj.core;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.dogecoin.dogecoinj.params.MainNetParams;
|
||||
import com.dogecoin.dogecoinj.params.UnitTestParams;
|
||||
import com.dogecoin.dogecoinj.script.Script;
|
||||
@ -24,6 +25,7 @@ import com.dogecoin.dogecoinj.store.BlockStoreException;
|
||||
import com.dogecoin.dogecoinj.store.FullPrunedBlockStore;
|
||||
import com.dogecoin.dogecoinj.utils.BlockFileLoader;
|
||||
import com.dogecoin.dogecoinj.utils.BriefLogFormatter;
|
||||
import com.dogecoin.dogecoinj.wallet.WalletTransaction;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.slf4j.Logger;
|
||||
@ -32,6 +34,7 @@ import org.slf4j.LoggerFactory;
|
||||
import java.io.File;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static com.dogecoin.dogecoinj.core.Coin.FIFTY_COINS;
|
||||
import static org.junit.Assert.*;
|
||||
@ -44,9 +47,9 @@ public abstract class AbstractFullPrunedBlockChainTest
|
||||
{
|
||||
private static final Logger log = LoggerFactory.getLogger(AbstractFullPrunedBlockChainTest.class);
|
||||
|
||||
private NetworkParameters params;
|
||||
private FullPrunedBlockChain chain;
|
||||
private FullPrunedBlockStore store;
|
||||
protected NetworkParameters params;
|
||||
protected FullPrunedBlockChain chain;
|
||||
protected FullPrunedBlockStore store;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
@ -70,13 +73,12 @@ public abstract class AbstractFullPrunedBlockChainTest
|
||||
RuleList blockList = generator.getBlocksToTest(false, false, null);
|
||||
|
||||
store = createStore(params, blockList.maximumReorgBlockCount);
|
||||
resetStore(store);
|
||||
chain = new FullPrunedBlockChain(params, store);
|
||||
|
||||
for (Rule rule : blockList.list) {
|
||||
if (!(rule instanceof BlockAndValidity))
|
||||
if (!(rule instanceof FullBlockTestGenerator.BlockAndValidity))
|
||||
continue;
|
||||
BlockAndValidity block = (BlockAndValidity) rule;
|
||||
FullBlockTestGenerator.BlockAndValidity block = (FullBlockTestGenerator.BlockAndValidity) rule;
|
||||
log.info("Testing rule " + block.ruleName + " with block hash " + block.block.getHash());
|
||||
boolean threw = false;
|
||||
try {
|
||||
@ -108,12 +110,14 @@ public abstract class AbstractFullPrunedBlockChainTest
|
||||
fail();
|
||||
}
|
||||
}
|
||||
try {
|
||||
store.close();
|
||||
} catch (Exception e) {}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void skipScripts() throws Exception {
|
||||
store = createStore(params, 10);
|
||||
resetStore(store);
|
||||
chain = new FullPrunedBlockChain(params, store);
|
||||
|
||||
// Check that we aren't accidentally leaving any references
|
||||
@ -144,13 +148,15 @@ public abstract class AbstractFullPrunedBlockChainTest
|
||||
} catch (VerificationException e) {
|
||||
fail();
|
||||
}
|
||||
try {
|
||||
store.close();
|
||||
} catch (Exception e) {}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFinalizedBlocks() throws Exception {
|
||||
final int UNDOABLE_BLOCKS_STORED = 10;
|
||||
store = createStore(params, UNDOABLE_BLOCKS_STORED);
|
||||
resetStore(store);
|
||||
chain = new FullPrunedBlockChain(params, store);
|
||||
|
||||
// Check that we aren't accidentally leaving any references
|
||||
@ -168,13 +174,13 @@ public abstract class AbstractFullPrunedBlockChainTest
|
||||
chain.add(rollingBlock);
|
||||
}
|
||||
|
||||
WeakReference<StoredTransactionOutput> out = new WeakReference<StoredTransactionOutput>
|
||||
WeakReference<UTXO> out = new WeakReference<UTXO>
|
||||
(store.getTransactionOutput(spendableOutput.getHash(), spendableOutput.getIndex()));
|
||||
rollingBlock = rollingBlock.createNextBlock(null);
|
||||
|
||||
Transaction t = new Transaction(params);
|
||||
// Entirely invalid scriptPubKey
|
||||
t.addOutput(new TransactionOutput(params, t, FIFTY_COINS, new byte[] {}));
|
||||
t.addOutput(new TransactionOutput(params, t, FIFTY_COINS, new byte[]{}));
|
||||
t.addSignedInput(spendableOutput, new Script(spendableOutputScriptPubKey), outKey);
|
||||
rollingBlock.addTransaction(t);
|
||||
rollingBlock.solve();
|
||||
@ -199,6 +205,9 @@ public abstract class AbstractFullPrunedBlockChainTest
|
||||
assertNull(undoBlock.get());
|
||||
assertNull(changes.get());
|
||||
assertNull(out.get());
|
||||
try {
|
||||
store.close();
|
||||
} catch (Exception e) {}
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -212,5 +221,121 @@ public abstract class AbstractFullPrunedBlockChainTest
|
||||
chain = new FullPrunedBlockChain(params, store);
|
||||
for (Block block : loader)
|
||||
chain.add(block);
|
||||
try {
|
||||
store.close();
|
||||
} catch (Exception e) {}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetOpenTransactionOutputs() throws Exception {
|
||||
final int UNDOABLE_BLOCKS_STORED = 10;
|
||||
store = createStore(params, UNDOABLE_BLOCKS_STORED);
|
||||
chain = new FullPrunedBlockChain(params, store);
|
||||
|
||||
// Check that we aren't accidentally leaving any references
|
||||
// to the full StoredUndoableBlock's lying around (ie memory leaks)
|
||||
ECKey outKey = new ECKey();
|
||||
|
||||
// Build some blocks on genesis block to create a spendable output
|
||||
Block rollingBlock = params.getGenesisBlock().createNextBlockWithCoinbase(outKey.getPubKey());
|
||||
chain.add(rollingBlock);
|
||||
Transaction transaction = rollingBlock.getTransactions().get(0);
|
||||
TransactionOutPoint spendableOutput = new TransactionOutPoint(params, 0, transaction.getHash());
|
||||
byte[] spendableOutputScriptPubKey = transaction.getOutputs().get(0).getScriptBytes();
|
||||
for (int i = 1; i < params.getSpendableCoinbaseDepth(); i++) {
|
||||
rollingBlock = rollingBlock.createNextBlockWithCoinbase(outKey.getPubKey());
|
||||
chain.add(rollingBlock);
|
||||
}
|
||||
rollingBlock = rollingBlock.createNextBlock(null);
|
||||
|
||||
// Create bitcoin spend of 1 BTC.
|
||||
ECKey toKey = new ECKey();
|
||||
Coin amount = Coin.valueOf(100000000);
|
||||
Address address = new Address(params, toKey.getPubKeyHash());
|
||||
Coin totalAmount = Coin.ZERO;
|
||||
|
||||
Transaction t = new Transaction(params);
|
||||
t.addOutput(new TransactionOutput(params, t, amount, toKey));
|
||||
t.addSignedInput(spendableOutput, new Script(spendableOutputScriptPubKey), outKey);
|
||||
rollingBlock.addTransaction(t);
|
||||
rollingBlock.solve();
|
||||
chain.add(rollingBlock);
|
||||
totalAmount = totalAmount.add(amount);
|
||||
|
||||
List<UTXO> outputs = store.getOpenTransactionOutputs(Lists.newArrayList(address));
|
||||
assertNotNull(outputs);
|
||||
assertEquals("Wrong Number of Outputs", 1, outputs.size());
|
||||
UTXO output = outputs.get(0);
|
||||
assertEquals("The address is not equal", address.toString(), output.getAddress());
|
||||
assertEquals("The amount is not equal", totalAmount, output.getValue());
|
||||
|
||||
outputs = null;
|
||||
output = null;
|
||||
try {
|
||||
store.close();
|
||||
} catch (Exception e) {}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUTXOProviderWithWallet() throws Exception {
|
||||
final int UNDOABLE_BLOCKS_STORED = 10;
|
||||
store = createStore(params, UNDOABLE_BLOCKS_STORED);
|
||||
chain = new FullPrunedBlockChain(params, store);
|
||||
|
||||
// Check that we aren't accidentally leaving any references
|
||||
// to the full StoredUndoableBlock's lying around (ie memory leaks)
|
||||
ECKey outKey = new ECKey();
|
||||
|
||||
// Build some blocks on genesis block to create a spendable output.
|
||||
Block rollingBlock = params.getGenesisBlock().createNextBlockWithCoinbase(outKey.getPubKey());
|
||||
chain.add(rollingBlock);
|
||||
Transaction transaction = rollingBlock.getTransactions().get(0);
|
||||
TransactionOutPoint spendableOutput = new TransactionOutPoint(params, 0, transaction.getHash());
|
||||
byte[] spendableOutputScriptPubKey = transaction.getOutputs().get(0).getScriptBytes();
|
||||
for (int i = 1; i < params.getSpendableCoinbaseDepth(); i++) {
|
||||
rollingBlock = rollingBlock.createNextBlockWithCoinbase(outKey.getPubKey());
|
||||
chain.add(rollingBlock);
|
||||
}
|
||||
rollingBlock = rollingBlock.createNextBlock(null);
|
||||
|
||||
// Create 1 BTC spend to a key in this wallet (to ourselves).
|
||||
Wallet wallet = new Wallet(params);
|
||||
assertEquals("Available balance is incorrect", Coin.ZERO, wallet.getBalance(Wallet.BalanceType.AVAILABLE));
|
||||
assertEquals("Estimated balance is incorrect", Coin.ZERO, wallet.getBalance(Wallet.BalanceType.ESTIMATED));
|
||||
|
||||
wallet.setUTXOProvider(store);
|
||||
ECKey toKey = wallet.freshReceiveKey();
|
||||
Coin amount = Coin.valueOf(100000000);
|
||||
|
||||
Transaction t = new Transaction(params);
|
||||
t.addOutput(new TransactionOutput(params, t, amount, toKey));
|
||||
t.addSignedInput(spendableOutput, new Script(spendableOutputScriptPubKey), outKey);
|
||||
rollingBlock.addTransaction(t);
|
||||
rollingBlock.solve();
|
||||
chain.add(rollingBlock);
|
||||
|
||||
// Create another spend of 1/2 the value of BTC we have available using the wallet (store coin selector).
|
||||
ECKey toKey2 = new ECKey();
|
||||
Coin amount2 = amount.divide(2);
|
||||
Address address2 = new Address(params, toKey2.getPubKeyHash());
|
||||
Wallet.SendRequest req = Wallet.SendRequest.to(address2, amount2);
|
||||
wallet.completeTx(req);
|
||||
wallet.commitTx(req.tx);
|
||||
Coin fee = req.fee;
|
||||
|
||||
// There should be one pending tx (our spend).
|
||||
assertEquals("Wrong number of PENDING.4", 1, wallet.getPoolSize(WalletTransaction.Pool.PENDING));
|
||||
Coin totalPendingTxAmount = Coin.ZERO;
|
||||
for (Transaction tx : wallet.getPendingTransactions()) {
|
||||
totalPendingTxAmount = totalPendingTxAmount.add(tx.getValueSentToMe(wallet));
|
||||
}
|
||||
|
||||
// The availbale balance should be the 0 (as we spent the 1 BTC that's pending) and estimated should be 1/2 - fee BTC
|
||||
assertEquals("Available balance is incorrect", Coin.ZERO, wallet.getBalance(Wallet.BalanceType.AVAILABLE));
|
||||
assertEquals("Estimated balance is incorrect", amount2.subtract(fee), wallet.getBalance(Wallet.BalanceType.ESTIMATED));
|
||||
assertEquals("Pending tx amount is incorrect", amount2.subtract(fee), totalPendingTxAmount);
|
||||
try {
|
||||
store.close();
|
||||
} catch (Exception e) {}
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ import com.dogecoin.dogecoinj.params.MainNetParams;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.net.InetAddress;
|
||||
import java.nio.BufferUnderflowException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
@ -64,6 +65,12 @@ public class BitcoinSerializerTest {
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream(addrMessage.length);
|
||||
bs.serialize(a, bos);
|
||||
|
||||
assertEquals(31, a.getMessageSize());
|
||||
a.addAddress(new PeerAddress(InetAddress.getLocalHost()));
|
||||
assertEquals(61, a.getMessageSize());
|
||||
a.removeAddress(0);
|
||||
assertEquals(31, a.getMessageSize());
|
||||
|
||||
//this wont be true due to dynamic timestamps.
|
||||
//assertTrue(LazyParseByteCacheTest.arrayContains(bos.toByteArray(), addrMessage));
|
||||
}
|
||||
|
@ -17,10 +17,16 @@
|
||||
|
||||
package com.dogecoin.dogecoinj.core;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.util.concurrent.SettableFuture;
|
||||
import com.google.common.util.concurrent.Uninterruptibles;
|
||||
import com.dogecoin.dogecoinj.net.NioClient;
|
||||
import com.dogecoin.dogecoinj.params.RegTestParams;
|
||||
import com.dogecoin.dogecoinj.store.BlockStoreException;
|
||||
import com.dogecoin.dogecoinj.store.FullPrunedBlockStore;
|
||||
import com.dogecoin.dogecoinj.store.H2FullPrunedBlockStore;
|
||||
import com.dogecoin.dogecoinj.store.MemoryBlockStore;
|
||||
import com.dogecoin.dogecoinj.utils.BlockFileLoader;
|
||||
import com.dogecoin.dogecoinj.utils.BriefLogFormatter;
|
||||
import com.dogecoin.dogecoinj.utils.Threading;
|
||||
@ -29,9 +35,10 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
|
||||
/**
|
||||
* A tool for comparing the blocks which are accepted/rejected by bitcoind/dogecoinj
|
||||
@ -44,15 +51,17 @@ public class BitcoindComparisonTool {
|
||||
private static NetworkParameters params;
|
||||
private static FullPrunedBlockStore store;
|
||||
private static FullPrunedBlockChain chain;
|
||||
private static PeerGroup peers;
|
||||
private static Sha256Hash bitcoindChainHead;
|
||||
private static volatile Peer bitcoind;
|
||||
private static volatile InventoryMessage mostRecentInv = null;
|
||||
|
||||
static class BlockWrapper {
|
||||
public Block block;
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
BriefLogFormatter.init();
|
||||
System.out.println("USAGE: bitcoinjBlockStoreLocation runLargeReorgs(1/0) [port=18444]");
|
||||
boolean runLargeReorgs = args.length > 1 && Integer.parseInt(args[1]) == 1;
|
||||
System.out.println("USAGE: bitcoinjBlockStoreLocation runExpensiveTests(1/0) [port=18444]");
|
||||
boolean runExpensiveTests = args.length > 1 && Integer.parseInt(args[1]) == 1;
|
||||
|
||||
params = RegTestParams.get();
|
||||
|
||||
@ -60,8 +69,9 @@ public class BitcoindComparisonTool {
|
||||
blockFile.deleteOnExit();
|
||||
|
||||
FullBlockTestGenerator generator = new FullBlockTestGenerator(params);
|
||||
RuleList blockList = generator.getBlocksToTest(false, runLargeReorgs, blockFile);
|
||||
Iterator<Block> blocks = new BlockFileLoader(params, Arrays.asList(blockFile));
|
||||
final RuleList blockList = generator.getBlocksToTest(false, runExpensiveTests, blockFile);
|
||||
final Map<Sha256Hash, Block> preloadedBlocks = new HashMap<Sha256Hash, Block>();
|
||||
final Iterator<Block> blocks = new BlockFileLoader(params, Arrays.asList(blockFile));
|
||||
|
||||
try {
|
||||
store = new H2FullPrunedBlockStore(params, args.length > 0 ? args[0] : "BitcoindComparisonTool", blockList.maximumReorgBlockCount);
|
||||
@ -73,25 +83,43 @@ public class BitcoindComparisonTool {
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
peers = new PeerGroup(params, chain);
|
||||
peers.setUserAgent("BlockAcceptanceComparisonTool", "1.0");
|
||||
VersionMessage ver = new VersionMessage(params, 42);
|
||||
ver.appendToSubVer("BlockAcceptanceComparisonTool", "1.1", null);
|
||||
ver.localServices = VersionMessage.NODE_NETWORK;
|
||||
final Peer bitcoind = new Peer(params, ver, new BlockChain(params, new MemoryBlockStore(params)), new PeerAddress(InetAddress.getLocalHost()));
|
||||
Preconditions.checkState(bitcoind.getVersionMessage().hasBlockChain());
|
||||
|
||||
// bitcoind MUST be on localhost or we will get banned as a DoSer
|
||||
peers.addAddress(new PeerAddress(InetAddress.getByName("localhost"), args.length > 2 ? Integer.parseInt(args[2]) : params.getPort()));
|
||||
final BlockWrapper currentBlock = new BlockWrapper();
|
||||
|
||||
final Set<Sha256Hash> blocksRequested = Collections.synchronizedSet(new HashSet<Sha256Hash>());
|
||||
final Set<Sha256Hash> blocksPendingSend = Collections.synchronizedSet(new HashSet<Sha256Hash>());
|
||||
final AtomicInteger unexpectedInvs = new AtomicInteger(0);
|
||||
peers.addEventListener(new AbstractPeerEventListener() {
|
||||
final SettableFuture<Void> connectedFuture = SettableFuture.create();
|
||||
bitcoind.addEventListener(new AbstractPeerEventListener() {
|
||||
@Override
|
||||
public void onPeerConnected(Peer peer, int peerCount) {
|
||||
super.onPeerConnected(peer, peerCount);
|
||||
if (!peer.getPeerVersionMessage().subVer.contains("Satoshi")) {
|
||||
System.out.println();
|
||||
System.out.println("************************************************************************************************************************\n" +
|
||||
"WARNING: You appear to be using this to test an alternative implementation with full validation rules. You should go\n" +
|
||||
"think hard about what you're doing. Seriously, no one has gotten even close to correctly reimplementing Bitcoin\n" +
|
||||
"consensus rules, despite serious investment in trying. It is a huge task and the slightest difference is a huge bug.\n" +
|
||||
"Instead, go work on making Bitcoin Core consensus rules a shared library and use that. Seriously, you wont get it right,\n" +
|
||||
"and starting with this tester as a way to try to do so will simply end in pain and lost coins.\n" +
|
||||
"************************************************************************************************************************");
|
||||
System.out.println();
|
||||
System.out.println("Giving you 30 seconds to think about the above warning...");
|
||||
Uninterruptibles.sleepUninterruptibly(30, TimeUnit.SECONDS);
|
||||
}
|
||||
log.info("bitcoind connected");
|
||||
bitcoind = peer;
|
||||
// Make sure bitcoind has no blocks
|
||||
bitcoind.setDownloadParameters(0, false);
|
||||
bitcoind.startBlockChainDownload();
|
||||
connectedFuture.set(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPeerDisconnected(Peer peer, int peerCount) {
|
||||
super.onPeerDisconnected(peer, peerCount);
|
||||
log.error("bitcoind node disconnected!");
|
||||
System.exit(1);
|
||||
}
|
||||
@ -99,16 +127,73 @@ public class BitcoindComparisonTool {
|
||||
@Override
|
||||
public Message onPreMessageReceived(Peer peer, Message m) {
|
||||
if (m instanceof HeadersMessage) {
|
||||
for (Block block : ((HeadersMessage) m).getBlockHeaders())
|
||||
bitcoindChainHead = block.getHash();
|
||||
if (!((HeadersMessage) m).getBlockHeaders().isEmpty()) {
|
||||
Block b = Iterables.getLast(((HeadersMessage) m).getBlockHeaders());
|
||||
log.info("Got header from bitcoind " + b.getHashAsString());
|
||||
bitcoindChainHead = b.getHash();
|
||||
} else
|
||||
log.info("Got empty header message from bitcoind");
|
||||
return null;
|
||||
} else if (m instanceof Block) {
|
||||
log.error("bitcoind sent us a block it already had, make sure bitcoind has no blocks!");
|
||||
System.exit(1);
|
||||
} else if (m instanceof GetDataMessage) {
|
||||
for (InventoryItem item : ((GetDataMessage)m).items)
|
||||
if (item.type == InventoryItem.Type.Block)
|
||||
for (InventoryItem item : ((GetDataMessage) m).items)
|
||||
if (item.type == InventoryItem.Type.Block) {
|
||||
log.info("Requested " + item.hash);
|
||||
if (currentBlock.block.getHash().equals(item.hash))
|
||||
bitcoind.sendMessage(currentBlock.block);
|
||||
else {
|
||||
Block nextBlock = preloadedBlocks.get(item.hash);
|
||||
if (nextBlock != null)
|
||||
bitcoind.sendMessage(nextBlock);
|
||||
else {
|
||||
blocksPendingSend.add(item.hash);
|
||||
log.info("...which we will not provide yet");
|
||||
}
|
||||
}
|
||||
blocksRequested.add(item.hash);
|
||||
}
|
||||
return null;
|
||||
} else if (m instanceof GetHeadersMessage) {
|
||||
try {
|
||||
if (currentBlock.block == null) {
|
||||
log.info("Got a request for a header before we had even begun processing blocks!");
|
||||
return null;
|
||||
}
|
||||
LinkedList<Block> headers = new LinkedList<Block>();
|
||||
Block it = blockList.hashHeaderMap.get(currentBlock.block.getHash());
|
||||
while (it != null) {
|
||||
headers.addFirst(it);
|
||||
it = blockList.hashHeaderMap.get(it.getPrevBlockHash());
|
||||
}
|
||||
LinkedList<Block> sendHeaders = new LinkedList<Block>();
|
||||
boolean found = false;
|
||||
for (Sha256Hash hash : ((GetHeadersMessage) m).getLocator()) {
|
||||
for (Block b : headers) {
|
||||
if (found) {
|
||||
sendHeaders.addLast(b);
|
||||
log.info("Sending header (" + b.getPrevBlockHash() + ") -> " + b.getHash());
|
||||
if (b.getHash().equals(((GetHeadersMessage) m).getStopHash()))
|
||||
break;
|
||||
} else if (b.getHash().equals(hash)) {
|
||||
log.info("Found header " + b.getHashAsString());
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
if (found)
|
||||
break;
|
||||
}
|
||||
if (!found)
|
||||
sendHeaders = headers;
|
||||
bitcoind.sendMessage(new HeadersMessage(params, sendHeaders));
|
||||
InventoryMessage i = new InventoryMessage(params);
|
||||
for (Block b : sendHeaders)
|
||||
i.addBlock(b);
|
||||
bitcoind.sendMessage(i);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return null;
|
||||
} else if (m instanceof InventoryMessage) {
|
||||
if (mostRecentInv != null) {
|
||||
@ -120,114 +205,125 @@ public class BitcoindComparisonTool {
|
||||
return m;
|
||||
}
|
||||
}, Threading.SAME_THREAD);
|
||||
peers.addPeerFilterProvider(new PeerFilterProvider() {
|
||||
private final Lock lock = Threading.lock("pfp");
|
||||
|
||||
@Override public long getEarliestKeyCreationTime() {
|
||||
return Long.MAX_VALUE;
|
||||
}
|
||||
|
||||
@Override public int getBloomFilterElementCount() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRequiringUpdateAllBloomFilter() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Lock getLock() {
|
||||
return lock;
|
||||
}
|
||||
|
||||
@Override public BloomFilter getBloomFilter(int size, double falsePositiveRate, long nTweak) {
|
||||
BloomFilter filter = new BloomFilter(1, 0.99, 0);
|
||||
filter.setMatchAll();
|
||||
return filter;
|
||||
}
|
||||
});
|
||||
|
||||
bitcoindChainHead = params.getGenesisBlock().getHash();
|
||||
|
||||
// Connect to bitcoind and make sure it has no blocks
|
||||
peers.startAsync();
|
||||
peers.setMaxConnections(1);
|
||||
peers.downloadBlockChain();
|
||||
// bitcoind MUST be on localhost or we will get banned as a DoSer
|
||||
new NioClient(new InetSocketAddress(InetAddress.getByName("127.0.0.1"), args.length > 2 ? Integer.parseInt(args[2]) : params.getPort()), bitcoind, 1000);
|
||||
|
||||
while (bitcoind == null)
|
||||
Thread.sleep(50);
|
||||
connectedFuture.get();
|
||||
|
||||
ArrayList<Sha256Hash> locator = new ArrayList<Sha256Hash>(1);
|
||||
locator.add(params.getGenesisBlock().getHash());
|
||||
Sha256Hash hashTo = new Sha256Hash("0000000000000000000000000000000000000000000000000000000000000000");
|
||||
|
||||
int differingBlocks = 0;
|
||||
int invalidBlocks = 0;
|
||||
int mempoolRulesFailed = 0;
|
||||
int utxoRulesFailed = 0;
|
||||
int rulesSinceFirstFail = 0;
|
||||
for (Rule rule : blockList.list) {
|
||||
if (rule instanceof BlockAndValidity) {
|
||||
BlockAndValidity block = (BlockAndValidity) rule;
|
||||
if (rule instanceof FullBlockTestGenerator.BlockAndValidity) {
|
||||
FullBlockTestGenerator.BlockAndValidity block = (FullBlockTestGenerator.BlockAndValidity) rule;
|
||||
boolean threw = false;
|
||||
Block nextBlock = blocks.next();
|
||||
Block nextBlock = preloadedBlocks.get(((FullBlockTestGenerator.BlockAndValidity) rule).blockHash);
|
||||
// Often load at least one block because sometimes we have duplicates with the same hash (b56/57)
|
||||
for (int i = 0; i < 1
|
||||
|| nextBlock == null || !nextBlock.getHash().equals(block.blockHash);
|
||||
i++) {
|
||||
try {
|
||||
Block b = blocks.next();
|
||||
Block oldBlockWithSameHash = preloadedBlocks.put(b.getHash(), b);
|
||||
if (oldBlockWithSameHash != null && oldBlockWithSameHash.getTransactions().size() != b.getTransactions().size())
|
||||
blocksRequested.remove(b.getHash());
|
||||
nextBlock = preloadedBlocks.get(block.blockHash);
|
||||
} catch (NoSuchElementException e) {
|
||||
if (nextBlock == null || !nextBlock.getHash().equals(block.blockHash))
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
currentBlock.block = nextBlock;
|
||||
log.info("Testing block {} {}", block.ruleName, currentBlock.block.getHash());
|
||||
try {
|
||||
if (chain.add(nextBlock) != block.connects) {
|
||||
log.error("Block didn't match connects flag on block \"" + block.ruleName + "\"");
|
||||
invalidBlocks++;
|
||||
log.error("ERROR: Block didn't match connects flag on block \"" + block.ruleName + "\"");
|
||||
rulesSinceFirstFail++;
|
||||
}
|
||||
} catch (VerificationException e) {
|
||||
threw = true;
|
||||
if (!block.throwsException) {
|
||||
log.error("Block didn't match throws flag on block \"" + block.ruleName + "\"");
|
||||
log.error("ERROR: Block didn't match throws flag on block \"" + block.ruleName + "\"");
|
||||
e.printStackTrace();
|
||||
invalidBlocks++;
|
||||
rulesSinceFirstFail++;
|
||||
} else if (block.connects) {
|
||||
log.error("Block didn't match connects flag on block \"" + block.ruleName + "\"");
|
||||
log.error("ERROR: Block didn't match connects flag on block \"" + block.ruleName + "\"");
|
||||
e.printStackTrace();
|
||||
invalidBlocks++;
|
||||
rulesSinceFirstFail++;
|
||||
}
|
||||
}
|
||||
if (!threw && block.throwsException) {
|
||||
log.error("Block didn't match throws flag on block \"" + block.ruleName + "\"");
|
||||
invalidBlocks++;
|
||||
log.error("ERROR: Block didn't match throws flag on block \"" + block.ruleName + "\"");
|
||||
rulesSinceFirstFail++;
|
||||
} else if (!chain.getChainHead().getHeader().getHash().equals(block.hashChainTipAfterBlock)) {
|
||||
log.error("New block head didn't match the correct value after block \"" + block.ruleName + "\"");
|
||||
invalidBlocks++;
|
||||
log.error("ERROR: New block head didn't match the correct value after block \"" + block.ruleName + "\"");
|
||||
rulesSinceFirstFail++;
|
||||
} else if (chain.getChainHead().getHeight() != block.heightAfterBlock) {
|
||||
log.error("New block head didn't match the correct height after block " + block.ruleName);
|
||||
invalidBlocks++;
|
||||
log.error("ERROR: New block head didn't match the correct height after block " + block.ruleName);
|
||||
rulesSinceFirstFail++;
|
||||
}
|
||||
|
||||
// Shouldnt double-request
|
||||
boolean shouldntRequest = blocksRequested.contains(nextBlock.getHash());
|
||||
if (shouldntRequest)
|
||||
blocksRequested.remove(nextBlock.getHash());
|
||||
InventoryMessage message = new InventoryMessage(params);
|
||||
message.addBlock(nextBlock);
|
||||
bitcoind.sendMessage(message);
|
||||
// bitcoind doesn't request blocks inline so we can't rely on a ping for synchronization
|
||||
for (int i = 0; !blocksRequested.contains(nextBlock.getHash()); i++) {
|
||||
if (i % 20 == 19)
|
||||
log.error("bitcoind still hasn't requested block " + block.ruleName + " with hash " + nextBlock.getHash());
|
||||
Thread.sleep(50);
|
||||
log.info("Sent inv with block " + nextBlock.getHashAsString());
|
||||
if (blocksPendingSend.contains(nextBlock.getHash())) {
|
||||
bitcoind.sendMessage(nextBlock);
|
||||
log.info("Sent full block " + nextBlock.getHashAsString());
|
||||
}
|
||||
bitcoind.sendMessage(nextBlock);
|
||||
// bitcoind doesn't request blocks inline so we can't rely on a ping for synchronization
|
||||
for (int i = 0; !shouldntRequest && !blocksRequested.contains(nextBlock.getHash()); i++) {
|
||||
int SLEEP_TIME = 1;
|
||||
if (i % 1000/SLEEP_TIME == 1000/SLEEP_TIME - 1)
|
||||
log.error("bitcoind still hasn't requested block " + block.ruleName + " with hash " + nextBlock.getHash());
|
||||
Thread.sleep(SLEEP_TIME);
|
||||
if (i > 60000/SLEEP_TIME) {
|
||||
log.error("bitcoind failed to request block " + block.ruleName);
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
if (shouldntRequest) {
|
||||
Thread.sleep(100);
|
||||
if (blocksRequested.contains(nextBlock.getHash())) {
|
||||
log.error("ERROR: bitcoind re-requested block " + block.ruleName + " with hash " + nextBlock.getHash());
|
||||
rulesSinceFirstFail++;
|
||||
}
|
||||
}
|
||||
// If the block throws, we may want to get bitcoind to request the same block again
|
||||
if (block.throwsException)
|
||||
blocksRequested.remove(nextBlock.getHash());
|
||||
//bitcoind.sendMessage(nextBlock);
|
||||
locator.clear();
|
||||
locator.add(bitcoindChainHead);
|
||||
bitcoind.sendMessage(new GetHeadersMessage(params, locator, hashTo));
|
||||
bitcoind.ping().get();
|
||||
if (!chain.getChainHead().getHeader().getHash().equals(bitcoindChainHead)) {
|
||||
differingBlocks++;
|
||||
log.error("bitcoind and dogecoinj acceptance differs on block \"" + block.ruleName + "\"");
|
||||
rulesSinceFirstFail++;
|
||||
log.error("ERROR: bitcoind and bitcoinj acceptance differs on block \"" + block.ruleName + "\"");
|
||||
}
|
||||
if (block.sendOnce)
|
||||
preloadedBlocks.remove(nextBlock.getHash());
|
||||
log.info("Block \"" + block.ruleName + "\" completed processing");
|
||||
} else if (rule instanceof MemoryPoolState) {
|
||||
MemoryPoolMessage message = new MemoryPoolMessage();
|
||||
bitcoind.sendMessage(message);
|
||||
bitcoind.ping().get();
|
||||
if (mostRecentInv == null && !((MemoryPoolState) rule).mempool.isEmpty()) {
|
||||
log.error("bitcoind had an empty mempool, but we expected some transactions on rule " + rule.ruleName);
|
||||
mempoolRulesFailed++;
|
||||
log.error("ERROR: bitcoind had an empty mempool, but we expected some transactions on rule " + rule.ruleName);
|
||||
rulesSinceFirstFail++;
|
||||
} else if (mostRecentInv != null && ((MemoryPoolState) rule).mempool.isEmpty()) {
|
||||
log.error("bitcoind had a non-empty mempool, but we expected an empty one on rule " + rule.ruleName);
|
||||
mempoolRulesFailed++;
|
||||
log.error("ERROR: bitcoind had a non-empty mempool, but we expected an empty one on rule " + rule.ruleName);
|
||||
rulesSinceFirstFail++;
|
||||
} else if (mostRecentInv != null) {
|
||||
Set<InventoryItem> originalRuleSet = new HashSet<InventoryItem>(((MemoryPoolState)rule).mempool);
|
||||
boolean matches = mostRecentInv.items.size() == ((MemoryPoolState)rule).mempool.size();
|
||||
@ -243,31 +339,34 @@ public class BitcoindComparisonTool {
|
||||
log.info(" The expected mempool was: ");
|
||||
for (InventoryItem item : originalRuleSet)
|
||||
log.info(" " + item.hash);
|
||||
mempoolRulesFailed++;
|
||||
rulesSinceFirstFail++;
|
||||
}
|
||||
mostRecentInv = null;
|
||||
} else if (rule instanceof UTXORule) {
|
||||
UTXORule r = (UTXORule) rule;
|
||||
UTXOsMessage result = bitcoind.getUTXOs(r.query).get();
|
||||
if (!result.equals(r.result)) {
|
||||
log.error("utxo result was not what we expected.");
|
||||
log.error("Wanted {}", r.result);
|
||||
log.error("but got {}", result);
|
||||
utxoRulesFailed++;
|
||||
} else {
|
||||
log.info("Successful utxo query {}: {}", r.ruleName, result);
|
||||
if (bitcoind.getPeerVersionMessage().isGetUTXOsSupported()) {
|
||||
UTXORule r = (UTXORule) rule;
|
||||
UTXOsMessage result = bitcoind.getUTXOs(r.query).get();
|
||||
if (!result.equals(r.result)) {
|
||||
log.error("utxo result was not what we expected.");
|
||||
log.error("Wanted {}", r.result);
|
||||
log.error("but got {}", result);
|
||||
rulesSinceFirstFail++;
|
||||
} else {
|
||||
log.info("Successful utxo query {}: {}", r.ruleName, result);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new RuntimeException("Unknown rule");
|
||||
}
|
||||
if (rulesSinceFirstFail > 0)
|
||||
rulesSinceFirstFail++;
|
||||
if (rulesSinceFirstFail > 6)
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
log.info("Done testing.\n" +
|
||||
"Blocks which were not handled the same between bitcoind/dogecoinj: " + differingBlocks + "\n" +
|
||||
"Blocks which should/should not have been accepted but weren't/were: " + invalidBlocks + "\n" +
|
||||
"Transactions which were/weren't in memory pool but shouldn't/should have been: " + mempoolRulesFailed + "\n" +
|
||||
"UTXO query mismatches: " + utxoRulesFailed + "\n" +
|
||||
"Unexpected inv messages: " + unexpectedInvs.get());
|
||||
System.exit(differingBlocks > 0 || invalidBlocks > 0 || mempoolRulesFailed > 0 || utxoRulesFailed > 0 || unexpectedInvs.get() > 0 ? 1 : 0);
|
||||
if (unexpectedInvs.get() > 0)
|
||||
log.error("ERROR: Got " + unexpectedInvs.get() + " unexpected invs from bitcoind");
|
||||
log.info("Done testing.");
|
||||
System.exit(rulesSinceFirstFail > 0 || unexpectedInvs.get() > 0 ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user