3
0
mirror of https://github.com/Qortal/altcoinj.git synced 2025-02-01 07:42:17 +00:00

Add classes for name lookups with SPV verification.

This commit is contained in:
JeremyRand 2016-06-30 03:01:08 +00:00
parent 80faef7303
commit ca28f2a0ba
10 changed files with 752 additions and 0 deletions

1
.gitignore vendored
View File

@ -1,4 +1,5 @@
core/target
namecoin/target
.project
.classpath
.settings

112
namecoin/pom.xml Normal file
View File

@ -0,0 +1,112 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.libdohj</groupId>
<artifactId>libdohj-namecoin</artifactId>
<version>0.14-SNAPSHOT</version>
<packaging>jar</packaging>
<licenses>
<license>
<name>The Apache Software License, Version 2.0</name>
<url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
<distribution>repo</distribution>
</license>
</licenses>
<!-- Dummy block to make Maven Central happy: authors list is in AUTHORS -->
<developers>
<developer>
<name>The libdohj team.</name>
<email>info@dogecoin.com</email>
</developer>
</developers>
<profiles>
<profile>
<id>update-protobuf</id>
<activation>
<property>
<name>updateProtobuf</name>
<value>true</value>
</property>
</activation>
<build>
<plugins>
<plugin>
<artifactId>maven-antrun-plugin</artifactId>
<executions>
<execution>
<id>compile-protoc</id>
<phase>generate-sources</phase>
<configuration>
<tasks>
<path id="proto.path">
<fileset dir="src">
<include name="**/*.proto"/>
</fileset>
</path>
<pathconvert pathsep=" " property="proto.files" refid="proto.path"/>
<exec executable="protoc" failonerror="true">
<arg value="--java_out=${project.basedir}/src/main/java"/>
<arg value="-I${project.basedir}/src"/>
<arg line="${proto.files}"/>
</exec>
</tasks>
</configuration>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.7</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
<type>jar</type>
</dependency>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>2.5.0</version>
</dependency>
<dependency>
<groupId>com.lambdaworks</groupId>
<artifactId>scrypt</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.bitcoinj</groupId>
<artifactId>bitcoinj-core</artifactId>
<version>0.14.2</version>
</dependency>
<dependency>
<groupId>org.libdohj</groupId>
<artifactId>libdohj-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.7.5</version>
</dependency>
</dependencies>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.6</maven.compiler.source>
<maven.compiler.target>1.6</maven.compiler.target>
</properties>
<name>libdohj</name>
</project>

View File

@ -0,0 +1,29 @@
/*
* Copyright 2016 Jeremy Rand.
*
* 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 org.libdohj.names;
import org.bitcoinj.core.Sha256Hash;
import org.bitcoinj.core.Transaction;
// TODO: Document this.
// identity is used for things like Tor stream isolation
public interface NameLookupByBlockHash {
public Transaction getNameTransaction(String name, Sha256Hash blockHash, String identity) throws Exception;
}

View File

@ -0,0 +1,61 @@
/*
* Copyright 2016 Jeremy Rand.
*
* 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 org.libdohj.names;
import org.bitcoinj.core.Block;
import org.bitcoinj.core.PeerGroup;
import org.bitcoinj.core.Sha256Hash;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionOutput;
import java.util.EnumSet;
// TODO: document this
public class NameLookupByBlockHashOneFullBlock implements NameLookupByBlockHash {
protected PeerGroup peerGroup;
public NameLookupByBlockHashOneFullBlock (PeerGroup peerGroup) {
this.peerGroup = peerGroup;
}
@Override
public Transaction getNameTransaction(String name, Sha256Hash blockHash, String identity) throws Exception {
Block nameFullBlock = peerGroup.getDownloadPeer().getBlock(blockHash).get();
// The full block hasn't been verified in any way!
// So let's do that now.
final EnumSet<Block.VerifyFlag> flags = EnumSet.noneOf(Block.VerifyFlag.class);
nameFullBlock.verify(-1, flags);
// Now we know that the block is internally valid (including the merkle root).
// We haven't verified signature validity, but our threat model is SPV.
for (Transaction tx : nameFullBlock.getTransactions()) {
if (NameTransactionUtils.getNameAnyUpdateOutput(tx, name) != null) {
return tx;
}
}
// The name wasn't found.
return null;
}
}

View File

@ -0,0 +1,27 @@
/*
* Copyright 2016 Jeremy Rand.
*
* 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 org.libdohj.names;
import org.bitcoinj.core.Transaction;
// TODO: document this
public interface NameLookupByBlockHeight {
public Transaction getNameTransaction(String name, int height, String identity) throws Exception;
}

View File

@ -0,0 +1,108 @@
/*
* Copyright 2016 Jeremy Rand.
*
* 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 org.libdohj.names;
import org.bitcoinj.core.BlockChain;
import org.bitcoinj.core.Sha256Hash;
import org.bitcoinj.core.StoredBlock;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.store.BlockStore;
import org.bitcoinj.store.BlockStoreException;
import java.util.concurrent.ConcurrentHashMap;
// TODO: breakout the 36000 expiration time into NetworkParameters.
// TODO: breakout the hash cache into its own class
// TODO: update blockHashCache with new blocks as they come into the chain
// TODO: document this
public class NameLookupByBlockHeightHashCache implements NameLookupByBlockHeight {
protected BlockChain chain;
protected BlockStore store;
protected NameLookupByBlockHash hashLookup;
protected ConcurrentHashMap<Integer, Sha256Hash> blockHashCache;
public NameLookupByBlockHeightHashCache (BlockChain chain, NameLookupByBlockHash hashLookup) throws Exception {
this.chain = chain;
this.store = chain.getBlockStore();
this.hashLookup = hashLookup;
initBlockHashCache();
}
protected void initBlockHashCache() throws BlockStoreException {
blockHashCache = new ConcurrentHashMap<Integer, Sha256Hash>(72000);
StoredBlock blockPointer = chain.getChainHead();
int headHeight = blockPointer.getHeight();
int reorgSafety = 120;
int newestHeight = headHeight - reorgSafety;
int oldestHeight = headHeight - 36000 - reorgSafety; // 36000 = name expiration
while (blockPointer.getHeight() >= oldestHeight) {
if (blockPointer.getHeight() <= newestHeight) {
blockHashCache.put(new Integer(blockPointer.getHeight()), blockPointer.getHeader().getHash());
}
blockPointer = blockPointer.getPrev(store);
}
}
@Override
public Transaction getNameTransaction(String name, int height, String identity) throws Exception {
Sha256Hash blockHash = getBlockHash(height);
Transaction tx = hashLookup.getNameTransaction(name, blockHash, identity);
tx.getConfidence().setAppearedAtChainHeight(height); // TODO: test this line
tx.getConfidence().setDepthInBlocks(chain.getChainHead().getHeight() - height + 1);
return tx;
}
public Sha256Hash getBlockHash(int height) throws BlockStoreException {
Sha256Hash maybeResult = blockHashCache.get(new Integer(height));
if (maybeResult != null) {
return maybeResult;
}
// If we got this far, the block height is uncached.
// This could be because the block is immature,
// or it could be because the cache is only initialized on initial startup.
StoredBlock blockPointer = chain.getChainHead();
while (blockPointer.getHeight() != height) {
blockPointer = blockPointer.getPrev(store);
}
return blockPointer.getHeader().getHash();
}
}

View File

@ -0,0 +1,27 @@
/*
* Copyright 2016 Jeremy Rand.
*
* 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 org.libdohj.names;
import org.bitcoinj.core.Transaction;
// TODO: document this
public interface NameLookupLatest {
public Transaction getNameTransaction(String name, String identity) throws Exception;
}

View File

@ -0,0 +1,141 @@
/*
* Copyright 2016 Jeremy Rand.
*
* 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 org.libdohj.names;
import org.bitcoinj.core.BlockChain;
import org.bitcoinj.core.Transaction;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
// TODO: document this
public class NameLookupLatestRestHeightApi implements NameLookupLatest {
protected BlockChain chain;
protected NameLookupByBlockHeight heightLookup;
protected String restUrlPrefix;
protected String restUrlSuffix;
public NameLookupLatestRestHeightApi (String restUrlPrefix, String restUrlSuffix, BlockChain chain, NameLookupByBlockHeight heightLookup) {
this.restUrlPrefix = restUrlPrefix;
this.restUrlSuffix = restUrlSuffix;
this.chain = chain;
this.heightLookup = heightLookup;
}
// TODO: make a new Exception class
@Override
public Transaction getNameTransaction(String name, String identity) throws Exception {
int height = getHeight(name);
return heightLookup.getNameTransaction(name, height, identity);
}
// TODO: break out the getHeight into its own class + interface
// TODO: add identity isolation
// TODO: use an older height if the newest height has insufficient confirmations, instead of throwing an Exception
// NOTE: this might fail if special characters are in the name, since it's not URL-escaping them.
public int getHeight(String name) throws Exception {
ArrayList<NameData> untrustedNameHistory = getUntrustedNameHistory(name);
int height;
int index;
for (index = untrustedNameHistory.size() - 1; index >= 0; index--) {
height = untrustedNameHistory.get(index).height;
try {
verifyHeightTrustworthy(height);
return height;
}
catch (Exception e) {
continue;
}
}
throw new Exception("Height not trustworthy or name does not exist.");
}
// TODO: add identity isolation
protected ArrayList<NameData> getUntrustedNameHistory(String name) throws Exception {
URL nameUrl = new URL(restUrlPrefix + name + restUrlSuffix);
ObjectMapper mapper = new ObjectMapper();
ArrayList<NameData> untrustedNameHistory = new ArrayList<NameData>(Arrays.asList(mapper.readValue(nameUrl, NameData[].class)));
return untrustedNameHistory;
}
protected void verifyHeightTrustworthy(int height) throws Exception {
if (height < 1) {
throw new Exception("Nonpositive block height; not trustworthy!");
}
int headHeight = chain.getChainHead().getHeight();
int confirmations = headHeight - height + 1;
// TODO: optionally use transaction chains (with signature checks) to verify transactions without 12 confirmations
// TODO: the above needs to be optional, because some applications (e.g. cert transparency) require confirmations
if (confirmations < 12) {
throw new Exception("Block does not yet have 12 confirmations; not trustworthy!");
}
// TODO: check for off-by-one errors on this line
if (confirmations >= 36000) {
throw new Exception("Block has expired; not trustworthy!");
}
}
static protected class NameData {
public String name;
public String value;
public String txid;
public String address;
public int expires_in;
public int height;
@JsonCreator
public NameData(@JsonProperty("name") String name,
@JsonProperty("value") String value,
@JsonProperty("txid") String txid,
@JsonProperty("address") String address,
@JsonProperty("expires_in") int expires_in,
@JsonProperty("height") int height) {
this.name = name;
this.value = value;
this.txid = txid;
this.address = address;
this.expires_in = expires_in;
this.height = height;
}
}
}

View File

@ -0,0 +1,191 @@
/*
* Copyright 2016 Jeremy Rand.
*
* 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 org.libdohj.names;
import org.bitcoinj.core.Block;
import org.bitcoinj.core.BlockChain;
import org.bitcoinj.core.MerkleBranch;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.Sha256Hash;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.Utils;
import org.bitcoinj.store.BlockStore;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
// TODO: document this
public class NameLookupLatestRestMerkleApi implements NameLookupLatest {
protected NetworkParameters params;
protected BlockChain chain;
protected BlockStore store;
protected NameLookupByBlockHeightHashCache heightLookup; // only needed for the hash cache
protected String restUrlPrefix;
protected String restUrlSuffix;
// TODO: break out the hash cache into its own class so that we don't need the NameLookup features.
public NameLookupLatestRestMerkleApi (NetworkParameters params, String restUrlPrefix, String restUrlSuffix, BlockChain chain, BlockStore store, NameLookupByBlockHeightHashCache heightLookup) {
this.params = params;
this.restUrlPrefix = restUrlPrefix;
this.restUrlSuffix = restUrlSuffix;
this.chain = chain;
this.store = store;
this.heightLookup = heightLookup;
}
// TODO: make a new Exception class
@Override
public Transaction getNameTransaction(String name, String identity) throws Exception {
NameData data = getLatestUntrustedNameData(name);
Sha256Hash blockHash = heightLookup.getBlockHash(data.height);
Block blockHeader = store.get(blockHash).getHeader();
// Convert merkle hashes from String to Sha256Hash
ArrayList<Sha256Hash> merkleHashes = new ArrayList<Sha256Hash>(data.mrkl_branch.size());
for (String merkleHashString : data.mrkl_branch) {
merkleHashes.add(Sha256Hash.wrap(merkleHashString));
}
long merkleBranchSideMask = data.tx_idx;
MerkleBranch branch = new MerkleBranch(params, null, merkleHashes, merkleBranchSideMask);
Transaction tx = new Transaction(params, Utils.HEX.decode(data.rawtx));
Sha256Hash txId = tx.getHash();
if(! blockHeader.getMerkleRoot().equals(branch.calculateMerkleRoot(txId))) {
throw new Exception("Merkle proof failed to verify!");
}
tx.getConfidence().setAppearedAtChainHeight(data.height); // TODO: test this line
tx.getConfidence().setDepthInBlocks(chain.getChainHead().getHeight() - data.height + 1);
if (NameTransactionUtils.getNameAnyUpdateOutput(tx, name) == null) {
throw new Exception("Not a name_anyupdate transaction or wrong name!");
}
return tx;
}
// TODO: break out the getHeight into its own class + interface
// TODO: add identity isolation
// TODO: use an older height if the newest height has insufficient confirmations, instead of throwing an Exception
// NOTE: this might fail if special characters are in the name, since it's not URL-escaping them.
public NameData getLatestUntrustedNameData(String name) throws Exception {
ArrayList<NameData> untrustedNameHistory = getUntrustedNameHistory(name);
int height;
int index;
for (index = untrustedNameHistory.size() - 1; index >= 0; index--) {
NameData candidate = untrustedNameHistory.get(index);
try {
verifyHeightTrustworthy(candidate.height);
return candidate;
}
catch (Exception e) {
continue;
}
}
throw new Exception("Height not trustworthy or name does not exist.");
}
// TODO: add identity isolation
protected ArrayList<NameData> getUntrustedNameHistory(String name) throws Exception {
URL nameUrl = new URL(restUrlPrefix + name + restUrlSuffix);
ObjectMapper mapper = new ObjectMapper();
ArrayList<NameData> untrustedNameHistory = new ArrayList<NameData>(Arrays.asList(mapper.readValue(nameUrl, NameData[].class)));
return untrustedNameHistory;
}
protected void verifyHeightTrustworthy(int height) throws Exception {
if (height < 1) {
throw new Exception("Nonpositive block height; not trustworthy!");
}
int headHeight = chain.getChainHead().getHeight();
int confirmations = headHeight - height + 1;
// TODO: optionally use transaction chains (with signature checks) to verify transactions without 12 confirmations
// TODO: the above needs to be optional, because some applications (e.g. cert transparency) require confirmations
if (confirmations < 12) {
throw new Exception("Block does not yet have 12 confirmations; not trustworthy!");
}
// TODO: check for off-by-one errors on this line
if (confirmations >= 36000) {
throw new Exception("Block has expired; not trustworthy!");
}
}
// TODO: break this out into its own class; add the extra fields to bitcoinj-addons too
static protected class NameData {
public String name;
public String value;
public String txid;
public String address;
public int expires_in;
public int height;
public long tx_idx;
public ArrayList<String> mrkl_branch;
public String rawtx;
@JsonCreator
public NameData(@JsonProperty("name") String name,
@JsonProperty("value") String value,
@JsonProperty("txid") String txid,
@JsonProperty("address") String address,
@JsonProperty("expires_in") int expires_in,
@JsonProperty("height") int height,
@JsonProperty("tx_idx") long tx_idx,
@JsonProperty("mrkl_branch") ArrayList<String> mrkl_branch,
@JsonProperty("rawtx") String rawtx) {
this.name = name;
this.value = value;
this.txid = txid;
this.address = address;
this.expires_in = expires_in;
this.height = height;
this.tx_idx = tx_idx;
this.mrkl_branch = mrkl_branch;
this.rawtx = rawtx;
}
}
}

View File

@ -0,0 +1,55 @@
/*
* Copyright 2016 Jeremy Rand.
*
* 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 org.libdohj.names;
import org.bitcoinj.core.BlockChain;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.store.BlockStore;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
// This lookup client only downloads a single transaction from the API rather than a history.
// This means that it's usually faster, but the API has to be careful to choose the correct transaction.
// As of writing (2016 Jun 26), webbtc does *not* always make the correct choice.
// That means that using this lookup client will result in an incorrect "nonexistent" result
// if the latest name_update for the targeted name has a depth between 1 and 11 (inclusive).
// I'm engaging with Marius from webbtc and hope to have a solution soon.
// -- Jeremy
public class NameLookupLatestRestMerkleApiSingleTx extends NameLookupLatestRestMerkleApi {
public NameLookupLatestRestMerkleApiSingleTx (NetworkParameters params, String restUrlPrefix, String restUrlSuffix, BlockChain chain, BlockStore store, NameLookupByBlockHeightHashCache heightLookup) {
super(params, restUrlPrefix, restUrlSuffix, chain, store, heightLookup);
}
@Override
protected ArrayList<NameData> getUntrustedNameHistory(String name) throws Exception {
URL nameUrl = new URL(restUrlPrefix + name + restUrlSuffix);
ObjectMapper mapper = new ObjectMapper();
NameData[] untrustedNameSingleEntry = {mapper.readValue(nameUrl, NameData.class)};
ArrayList<NameData> untrustedNameHistory = new ArrayList<NameData>(Arrays.asList(untrustedNameSingleEntry));
return untrustedNameHistory;
}
}