3
0
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:
langerhans 2014-12-23 18:09:55 +01:00
commit 905629a78f
158 changed files with 9728 additions and 3792 deletions

View File

@ -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:

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -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;
}
/**

View File

@ -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();
}

View File

@ -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);

View File

@ -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

View File

@ -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

View File

@ -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) {

View 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;
}
}

View File

@ -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;
}
}

View File

@ -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();
}

View File

@ -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();

View File

@ -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());

View File

@ -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.
*/

View File

@ -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;
}
}

View File

@ -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

View File

@ -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);
}
/**

View File

@ -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;

View File

@ -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

View File

@ -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();

View File

@ -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; }
}

View File

@ -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;
}
/**

View File

@ -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.

View File

@ -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
}
}

View File

@ -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;
}
}

View File

@ -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);

View File

@ -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;
}
/**

View File

@ -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;
}
/**

View File

@ -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);
}
}

View File

@ -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();
}
}
}

View 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);
}
}

View File

@ -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();
}

View File

@ -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);
}
}

View File

@ -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

View File

@ -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);
}

View File

@ -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();
}

View File

@ -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 {

View File

@ -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];

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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();

View File

@ -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;
}
}

View File

@ -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
}

View File

@ -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();

View File

@ -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();
}
}

View File

@ -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() {

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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);

View File

@ -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;

View File

@ -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;

View File

@ -44,6 +44,8 @@ public class UnitTestParams extends NetworkParameters {
spendableCoinbaseDepth = 5;
subsidyDecreaseBlockCount = 100;
dnsSeeds = null;
bip32HeaderPub = 0x043587CF;
bip32HeaderPriv = 0x04358394;
}
private static UnitTestParams instance;

View File

@ -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;

View File

@ -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;
}
};

View File

@ -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

View File

@ -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);

View File

@ -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);
}
/**

View File

@ -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() {

View File

@ -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()) {

View File

@ -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 &lt;pubkey hash&gt; 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 &lt;script hash&gt; 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;

View File

@ -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();
}
}

View File

@ -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);

View File

@ -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

View File

@ -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.
*/

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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");
}
}
}
}

View File

@ -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:

View File

@ -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);

View File

@ -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);
}

View File

@ -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));
}
}

View File

@ -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 {

View File

@ -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);
}
}

View File

@ -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.

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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) {

View File

@ -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

View File

@ -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) {

View File

@ -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.

View File

@ -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) {

View File

@ -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();

View File

@ -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;
}
}

View File

@ -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())

View File

@ -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() {

File diff suppressed because it is too large Load Diff

26
core/src/peerseeds.proto Normal file
View 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;
}

View File

@ -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;

View File

@ -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) {}
}
}

View File

@ -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));
}

View File

@ -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