diff --git a/.gitignore b/.gitignore index f6135466..b707fe81 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ core/target +namecoin/target .project .classpath .settings diff --git a/namecoin/pom.xml b/namecoin/pom.xml new file mode 100644 index 00000000..c6932134 --- /dev/null +++ b/namecoin/pom.xml @@ -0,0 +1,112 @@ + + + 4.0.0 + org.libdohj + libdohj-namecoin + 0.14-SNAPSHOT + jar + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + + + The libdohj team. + info@dogecoin.com + + + + + update-protobuf + + + updateProtobuf + true + + + + + + maven-antrun-plugin + + + compile-protoc + generate-sources + + + + + + + + + + + + + + + + + run + + + + + + + + + + + + org.slf4j + slf4j-api + 1.7.7 + + + junit + junit + 4.12 + test + jar + + + com.google.protobuf + protobuf-java + 2.5.0 + + + com.lambdaworks + scrypt + 1.4.0 + + + org.bitcoinj + bitcoinj-core + 0.14.2 + + + org.libdohj + libdohj-core + ${project.version} + + + com.fasterxml.jackson.core + jackson-databind + 2.7.5 + + + + UTF-8 + 1.6 + 1.6 + + libdohj + diff --git a/namecoin/src/main/java/org/libdohj/names/NameLookupByBlockHash.java b/namecoin/src/main/java/org/libdohj/names/NameLookupByBlockHash.java new file mode 100644 index 00000000..48803e97 --- /dev/null +++ b/namecoin/src/main/java/org/libdohj/names/NameLookupByBlockHash.java @@ -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; + +} diff --git a/namecoin/src/main/java/org/libdohj/names/NameLookupByBlockHashOneFullBlock.java b/namecoin/src/main/java/org/libdohj/names/NameLookupByBlockHashOneFullBlock.java new file mode 100644 index 00000000..a0a87c3a --- /dev/null +++ b/namecoin/src/main/java/org/libdohj/names/NameLookupByBlockHashOneFullBlock.java @@ -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 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; + } + +} diff --git a/namecoin/src/main/java/org/libdohj/names/NameLookupByBlockHeight.java b/namecoin/src/main/java/org/libdohj/names/NameLookupByBlockHeight.java new file mode 100644 index 00000000..544f704e --- /dev/null +++ b/namecoin/src/main/java/org/libdohj/names/NameLookupByBlockHeight.java @@ -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; + +} diff --git a/namecoin/src/main/java/org/libdohj/names/NameLookupByBlockHeightHashCache.java b/namecoin/src/main/java/org/libdohj/names/NameLookupByBlockHeightHashCache.java new file mode 100644 index 00000000..45cb9211 --- /dev/null +++ b/namecoin/src/main/java/org/libdohj/names/NameLookupByBlockHeightHashCache.java @@ -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 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(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(); + } + +} diff --git a/namecoin/src/main/java/org/libdohj/names/NameLookupLatest.java b/namecoin/src/main/java/org/libdohj/names/NameLookupLatest.java new file mode 100644 index 00000000..03159d84 --- /dev/null +++ b/namecoin/src/main/java/org/libdohj/names/NameLookupLatest.java @@ -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; + +} diff --git a/namecoin/src/main/java/org/libdohj/names/NameLookupLatestRestHeightApi.java b/namecoin/src/main/java/org/libdohj/names/NameLookupLatestRestHeightApi.java new file mode 100644 index 00000000..d3055d98 --- /dev/null +++ b/namecoin/src/main/java/org/libdohj/names/NameLookupLatestRestHeightApi.java @@ -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 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 getUntrustedNameHistory(String name) throws Exception { + URL nameUrl = new URL(restUrlPrefix + name + restUrlSuffix); + + ObjectMapper mapper = new ObjectMapper(); + + ArrayList untrustedNameHistory = new ArrayList(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; + } + } + +} diff --git a/namecoin/src/main/java/org/libdohj/names/NameLookupLatestRestMerkleApi.java b/namecoin/src/main/java/org/libdohj/names/NameLookupLatestRestMerkleApi.java new file mode 100644 index 00000000..a8f8f40d --- /dev/null +++ b/namecoin/src/main/java/org/libdohj/names/NameLookupLatestRestMerkleApi.java @@ -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 merkleHashes = new ArrayList(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 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 getUntrustedNameHistory(String name) throws Exception { + URL nameUrl = new URL(restUrlPrefix + name + restUrlSuffix); + + ObjectMapper mapper = new ObjectMapper(); + + ArrayList untrustedNameHistory = new ArrayList(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 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 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; + } + } + +} diff --git a/namecoin/src/main/java/org/libdohj/names/NameLookupLatestRestMerkleApiSingleTx.java b/namecoin/src/main/java/org/libdohj/names/NameLookupLatestRestMerkleApiSingleTx.java new file mode 100644 index 00000000..6b4ade96 --- /dev/null +++ b/namecoin/src/main/java/org/libdohj/names/NameLookupLatestRestMerkleApiSingleTx.java @@ -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 getUntrustedNameHistory(String name) throws Exception { + URL nameUrl = new URL(restUrlPrefix + name + restUrlSuffix); + + ObjectMapper mapper = new ObjectMapper(); + + NameData[] untrustedNameSingleEntry = {mapper.readValue(nameUrl, NameData.class)}; + ArrayList untrustedNameHistory = new ArrayList(Arrays.asList(untrustedNameSingleEntry)); + + return untrustedNameHistory; + } + +}