3
0
mirror of https://github.com/Qortal/altcoinj.git synced 2025-02-07 06:44:16 +00:00

Merge pull request #18 from JeremyRand/namecoin-initial-pull-request

Initial Namecoin support
This commit is contained in:
Ross Nicoll 2016-07-05 20:15:00 +01:00 committed by GitHub
commit faadcca7ad
24 changed files with 1902 additions and 0 deletions

1
.gitignore vendored
View File

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

View File

@ -0,0 +1,76 @@
/*
* 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.libdohj.script.NameScript;
import org.bitcoinj.core.ScriptException;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionOutput;
import org.bitcoinj.script.Script;
import java.io.UnsupportedEncodingException;
// TODO: document this
// TODO: look into allowing name and value to be ArrayList<byte> as an alternative to String.
public class NameTransactionUtils {
// Providing the name is, in theory, superfluous, since only 1 name output can exist per transaction.
// However, this might be changed in the future, to allow atomic updates of multiple names.
// This could enable things like CoinJoin for names.
public static TransactionOutput getNameAnyUpdateOutput(Transaction tx, String name) {
for (TransactionOutput output : tx.getOutputs()) {
try {
Script scriptPubKey = output.getScriptPubKey();
NameScript ns = new NameScript(scriptPubKey);
if(ns.isNameOp() && ns.isAnyUpdate() && new String(ns.getOpName().data, "ISO-8859-1").equals(name)) {
return output;
}
} catch (ScriptException e) {
continue;
} catch (UnsupportedEncodingException e) {
continue;
}
}
// No such output was found.
return null;
}
public static NameScript getNameAnyUpdateScript(Transaction tx, String name) {
TransactionOutput output = getNameAnyUpdateOutput(tx, name);
if (output == null) {
return null;
}
Script scriptPubKey = output.getScriptPubKey();
return new NameScript(scriptPubKey);
}
public static String getNameValueAsString(Transaction tx, String name) throws UnsupportedEncodingException {
NameScript ns = getNameAnyUpdateScript(tx, name);
if (ns == null) {
return null;
}
return new String(ns.getOpValue().data, "ISO-8859-1");
}
}

View File

@ -0,0 +1,291 @@
/*
* 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.params;
import java.io.ByteArrayOutputStream;
import java.math.BigInteger;
import java.util.concurrent.TimeUnit;
import com.google.common.base.Stopwatch;
import org.bitcoinj.core.AltcoinBlock;
import org.bitcoinj.core.Block;
import org.bitcoinj.core.Coin;
import static org.bitcoinj.core.Coin.COIN;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.VerificationException;
import org.bitcoinj.script.Script;
import org.bitcoinj.script.ScriptOpCodes;
import org.bitcoinj.store.BlockStore;
import org.bitcoinj.store.BlockStoreException;
import org.bitcoinj.utils.MonetaryFormat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.bitcoinj.core.Sha256Hash;
import org.bitcoinj.core.StoredBlock;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionInput;
import org.bitcoinj.core.TransactionOutput;
import org.bitcoinj.core.Utils;
import org.libdohj.core.AltcoinSerializer;
import org.libdohj.core.AuxPoWNetworkParameters;
// TODO: review this
/**
* Common parameters for Namecoin networks.
*/
public abstract class AbstractNamecoinParams extends NetworkParameters implements AuxPoWNetworkParameters {
/** Standard format for the NMC denomination. */
public static final MonetaryFormat NMC;
/** Standard format for the mNMC denomination. */
public static final MonetaryFormat MNMC;
/** Standard format for the uBTC denomination. */
public static final MonetaryFormat UNMC;
public static final int AUXPOW_CHAIN_ID = 0x0001; // 1
/** Currency code for base 1 Namecoin. */
public static final String CODE_NMC = "NMC";
/** Currency code for base 1/1,000 Namecoin. */
public static final String CODE_MNMC = "mNMC";
/** Currency code for base 1/1,000,000 Namecoin. */
public static final String CODE_UNMC = "µNMC";
protected int auxpowStartHeight;
private static final int BLOCK_VERSION_FLAG_AUXPOW = 0x00000100;
static {
NMC = MonetaryFormat.BTC.noCode()
.code(0, CODE_NMC)
.code(3, CODE_MNMC)
.code(6, CODE_UNMC);
MNMC = NMC.shift(3).minDecimals(2).optionalDecimals(2);
UNMC = NMC.shift(6).minDecimals(0).optionalDecimals(0);
}
/** The string returned by getId() for the main, production network where people trade things. */
public static final String ID_NMC_MAINNET = "org.namecoin.production";
/** The string returned by getId() for the testnet. */
public static final String ID_NMC_TESTNET = "org.namecoin.test";
protected Logger log = LoggerFactory.getLogger(AbstractNamecoinParams.class);
public static final int NAMECOIN_PROTOCOL_VERSION_GETHEADERS = 38000;
public AbstractNamecoinParams() {
super();
genesisBlock = createGenesis(this);
interval = INTERVAL;
targetTimespan = TARGET_TIMESPAN;
maxTarget = Utils.decodeCompactBits(0x1e0fffffL); // TODO: figure out the Namecoin value of this
// BIP 43 recommends using these values regardless of which blockchain is in use.
bip32HeaderPub = 0x0488B21E; //The 4 byte header that serializes in base58 to "xpub".
bip32HeaderPriv = 0x0488ADE4; //The 4 byte header that serializes in base58 to "xprv"
}
private static AltcoinBlock createGenesis(NetworkParameters params) {
AltcoinBlock genesisBlock = new AltcoinBlock(params, Block.BLOCK_VERSION_GENESIS);
Transaction t = new Transaction(params);
try {
// "... choose what comes next. Lives of your own, or a return to chains. -- V"
byte[] bytes = Utils.HEX.decode
("04ff7f001c020a024b2e2e2e2063686f6f7365207768617420636f6d6573206e6578742e20204c69766573206f6620796f7572206f776e2c206f7220612072657475726e20746f20636861696e732e202d2d2056");
t.addInput(new TransactionInput(params, t, bytes));
ByteArrayOutputStream scriptPubKeyBytes = new ByteArrayOutputStream();
Script.writeBytes(scriptPubKeyBytes, Utils.HEX.decode
("04b620369050cd899ffbbc4e8ee51e8c4534a855bb463439d63d235d4779685d8b6f4870a238cf365ac94fa13ef9a2a22cd99d0d5ee86dcabcafce36c7acf43ce5"));
scriptPubKeyBytes.write(ScriptOpCodes.OP_CHECKSIG);
t.addOutput(new TransactionOutput(params, t, COIN.multiply(50), scriptPubKeyBytes.toByteArray()));
} catch (Exception e) {
// Cannot happen.
throw new RuntimeException(e);
}
genesisBlock.addTransaction(t);
return genesisBlock;
}
@Override
public Coin getBlockSubsidy(final int height) {
return COIN.multiply(50).shiftRight(height / getSubsidyDecreaseBlockCount());
}
@Override
public MonetaryFormat getMonetaryFormat() {
return NMC;
}
@Override
public Coin getMaxMoney() {
return MAX_MONEY;
}
// TODO: this is Bitcoin, need to figure out if it's the same for Namecoin
@Override
public Coin getMinNonDustOutput() {
return Transaction.MIN_NONDUST_OUTPUT;
}
@Override
public String getUriScheme() {
return "namecoin";
}
@Override
public boolean hasMaxMoney() {
return true;
}
// This is copied from Bitcoin
/**
* Checks if we are at a difficulty transition point.
* @param storedPrev The previous stored block
* @return If this is a difficulty transition point
*/
protected boolean isDifficultyTransitionPoint(StoredBlock storedPrev) {
return ((storedPrev.getHeight() + 1) % this.getInterval()) == 0;
}
@Override
public void checkDifficultyTransitions(StoredBlock storedPrev, Block nextBlock, BlockStore blockStore)
throws VerificationException, BlockStoreException {
// This is copied verbatim from Bitcoin except for the Namecoin changes marked accordingly
Block prev = storedPrev.getHeader();
// Is this supposed to be a difficulty transition point?
if (!isDifficultyTransitionPoint(storedPrev)) {
// No ... so check the difficulty didn't actually change.
if (nextBlock.getDifficultyTarget() != prev.getDifficultyTarget())
throw new VerificationException("Unexpected change in difficulty at height " + storedPrev.getHeight() +
": " + Long.toHexString(nextBlock.getDifficultyTarget()) + " vs " +
Long.toHexString(prev.getDifficultyTarget()));
return;
}
// We need to find a block far back in the chain. It's OK that this is expensive because it only occurs every
// two weeks after the initial block chain download.
final Stopwatch watch = Stopwatch.createStarted();
StoredBlock cursor = blockStore.get(prev.getHash());
// Namecoin addition
int blocksBack = this.getInterval() - 1;
if (storedPrev.getHeight() >= this.getAuxpowStartHeight() && (storedPrev.getHeight() + 1 > this.getInterval())) {
blocksBack = this.getInterval();
}
// Namecoin modification
//for (int i = 0; i < this.getInterval() - 1; i++) {
for (int i = 0; i < blocksBack; i++) {
if (cursor == null) {
// This should never happen. If it does, it means we are following an incorrect or busted chain.
throw new VerificationException(
"Difficulty transition point but we did not find a way back to the genesis block.");
}
cursor = blockStore.get(cursor.getHeader().getPrevBlockHash());
}
watch.stop();
if (watch.elapsed(TimeUnit.MILLISECONDS) > 50)
log.info("Difficulty transition traversal took {}", watch);
Block blockIntervalAgo = cursor.getHeader();
int timespan = (int) (prev.getTimeSeconds() - blockIntervalAgo.getTimeSeconds());
// Limit the adjustment step.
final int targetTimespan = this.getTargetTimespan();
if (timespan < targetTimespan / 4)
timespan = targetTimespan / 4;
if (timespan > targetTimespan * 4)
timespan = targetTimespan * 4;
BigInteger newTarget = Utils.decodeCompactBits(prev.getDifficultyTarget());
newTarget = newTarget.multiply(BigInteger.valueOf(timespan));
newTarget = newTarget.divide(BigInteger.valueOf(targetTimespan));
if (newTarget.compareTo(this.getMaxTarget()) > 0) {
log.info("Difficulty hit proof of work limit: {}", newTarget.toString(16));
newTarget = this.getMaxTarget();
}
int accuracyBytes = (int) (nextBlock.getDifficultyTarget() >>> 24) - 3;
long receivedTargetCompact = nextBlock.getDifficultyTarget();
// The calculated difficulty is to a higher precision than received, so reduce here.
BigInteger mask = BigInteger.valueOf(0xFFFFFFL).shiftLeft(accuracyBytes * 8);
newTarget = newTarget.and(mask);
long newTargetCompact = Utils.encodeCompactBits(newTarget);
if (newTargetCompact != receivedTargetCompact)
throw new VerificationException("Network provided difficulty bits do not match what was calculated: " +
Long.toHexString(newTargetCompact) + " vs " + Long.toHexString(receivedTargetCompact));
}
@Override
public int getChainID() {
return AUXPOW_CHAIN_ID;
}
// TODO: re-add this when we introduce Testnet2
/**
* Whether this network has special rules to enable minimum difficulty blocks
* after a long interval between two blocks (i.e. testnet).
*/
//public abstract boolean allowMinDifficultyBlocks();
/**
* Get the hash to use for a block.
*/
@Override
public Sha256Hash getBlockDifficultyHash(Block block) {
return block.getHash();
}
@Override
public AltcoinSerializer getSerializer(boolean parseRetain) {
return new AltcoinSerializer(this, parseRetain);
}
// TODO: look into allowing PeerGroups that don't support GetHeaders (since for full block retrieval it's not needed)
@Override
public int getProtocolVersionNum(final ProtocolVersion version) {
switch (version) {
case MINIMUM:
return NAMECOIN_PROTOCOL_VERSION_GETHEADERS;
default:
return version.getBitcoinProtocolVersion();
}
}
@Override
public boolean isAuxPoWBlockVersion(long version) {
return (version & BLOCK_VERSION_FLAG_AUXPOW) > 0;
}
public int getAuxpowStartHeight() {
return auxpowStartHeight;
}
private static class CheckpointEncounteredException extends Exception {
private CheckpointEncounteredException() {
}
}
}

View File

@ -0,0 +1,120 @@
/*
* Copyright 2013 Google Inc, 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.params;
import org.bitcoinj.core.Sha256Hash;
import org.spongycastle.util.encoders.Hex;
import static com.google.common.base.Preconditions.checkState;
// TODO: review this
/**
* Parameters for the main Namecoin production network on which people trade
* goods and services.
*/
public class NamecoinMainNetParams extends AbstractNamecoinParams {
public static final int MAINNET_MAJORITY_WINDOW = 1000;
public static final int MAINNET_MAJORITY_REJECT_BLOCK_OUTDATED = 950;
public static final int MAINNET_MAJORITY_ENFORCE_BLOCK_UPGRADE = 750;
public NamecoinMainNetParams() {
super();
dumpedPrivateKeyHeader = 180; //This is always addressHeader + 128
addressHeader = 52;
p2shHeader = 13;
//acceptableAddressCodes = new int[] { addressHeader, p2shHeader };
acceptableAddressCodes = new int[] { addressHeader }; // Namecoin doesn't yet enforce P2SH, so we disable it for now.
port = 8334;
packetMagic = 0xf9beb4fe;
genesisBlock.setDifficultyTarget(0x1C007FFFL);
genesisBlock.setTime(1303000001L);
genesisBlock.setNonce(2719916434L);
id = ID_NMC_MAINNET;
subsidyDecreaseBlockCount = 210000;
spendableCoinbaseDepth = 100;
auxpowStartHeight = 19200;
String genesisHash = genesisBlock.getHashAsString();
checkState(genesisHash.equals("000000000062b72c5e2ceb45fbc8587e807c155b0da735e6483dfba2f0a9c770"),
genesisHash);
// TODO: remove alert key since it's removed from Bitcoin Core / Namecoin Core
alertSigningKey = Hex.decode("04ba207043c1575208f08ea6ac27ed2aedd4f84e70b874db129acb08e6109a3bbb7c479ae22565973ebf0ac0391514511a22cb9345bdb772be20cfbd38be578b0c");
majorityEnforceBlockUpgrade = MAINNET_MAJORITY_ENFORCE_BLOCK_UPGRADE;
majorityRejectBlockOutdated = MAINNET_MAJORITY_REJECT_BLOCK_OUTDATED;
majorityWindow = MAINNET_MAJORITY_WINDOW;
// TODO: check whether there are any non BIP30 blocks in Namecoin; add them here if they exist
// This contains (at a minimum) the blocks which are not BIP30 compliant. BIP30 changed how duplicate
// transactions are handled. Duplicated transactions could occur in the case where a coinbase had the same
// extraNonce and the same outputs but appeared at different heights, and greatly complicated re-org handling.
// Having these here simplifies block connection logic considerably.
checkpoints.put( 2016, Sha256Hash.wrap("0000000000660bad0d9fbde55ba7ee14ddf766ed5f527e3fbca523ac11460b92"));
checkpoints.put( 4032, Sha256Hash.wrap("0000000000493b5696ad482deb79da835fe2385304b841beef1938655ddbc411"));
checkpoints.put( 6048, Sha256Hash.wrap("000000000027939a2e1d8bb63f36c47da858e56d570f143e67e85068943470c9"));
checkpoints.put( 8064, Sha256Hash.wrap("000000000003a01f708da7396e54d081701ea406ed163e519589717d8b7c95a5"));
checkpoints.put( 10080, Sha256Hash.wrap("00000000000fed3899f818b2228b4f01b9a0a7eeee907abd172852df71c64b06"));
checkpoints.put( 12096, Sha256Hash.wrap("0000000000006c06988ff361f124314f9f4bb45b6997d90a7ee4cedf434c670f"));
checkpoints.put( 14112, Sha256Hash.wrap("00000000000045d95e0588c47c17d593c7b5cb4fb1e56213d1b3843c1773df2b"));
checkpoints.put( 16128, Sha256Hash.wrap("000000000001d9964f9483f9096cf9d6c6c2886ed1e5dec95ad2aeec3ce72fa9"));
checkpoints.put( 18940, Sha256Hash.wrap("00000000000087f7fc0c8085217503ba86f796fa4984f7e5a08b6c4c12906c05"));
checkpoints.put( 30240, Sha256Hash.wrap("e1c8c862ff342358384d4c22fa6ea5f669f3e1cdcf34111f8017371c3c0be1da"));
checkpoints.put( 57000, Sha256Hash.wrap("aa3ec60168a0200799e362e2b572ee01f3c3852030d07d036e0aa884ec61f203"));
checkpoints.put(112896, Sha256Hash.wrap("73f880e78a04dd6a31efc8abf7ca5db4e262c4ae130d559730d6ccb8808095bf"));
checkpoints.put(182000, Sha256Hash.wrap("d47b4a8fd282f635d66ce34ebbeb26ffd64c35b41f286646598abfd813cba6d9"));
checkpoints.put(193000, Sha256Hash.wrap("3b85e70ba7f5433049cfbcf0ae35ed869496dbedcd1c0fafadb0284ec81d7b58"));
dnsSeeds = new String[] {
"namecoindnsseed.digi-masters.com", // George Lloyd
"namecoindnsseed.digi-masters.uk", // George Lloyd
"seed.namecoin.domob.eu", // Daniel Kraft
"nmc.seed.quisquis.de", // Peter Conrad
"dnsseed.namecoin.webbtc.com", // Marius Hanne
};
// TODO: look into HTTP seeds or Addr seeds as is done for Bitcoin
}
private static NamecoinMainNetParams instance;
public static synchronized NamecoinMainNetParams get() {
if (instance == null) {
instance = new NamecoinMainNetParams();
}
return instance;
}
// TODO: re-add this when we introduce Testnet2
/*
@Override
public boolean allowMinDifficultyBlocks() {
return false;
}
*/
@Override
public String getPaymentProtocolId() {
// TODO: CHANGE THIS (comment from Dogecoin)
return ID_NMC_MAINNET;
}
@Override
public boolean isTestNet() {
return false;
}
}

View File

@ -0,0 +1,181 @@
/*
* 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.script;
import org.bitcoinj.core.ScriptException;
import org.bitcoinj.script.Script;
import org.bitcoinj.script.ScriptBuilder;
import org.bitcoinj.script.ScriptChunk;
import static org.bitcoinj.script.ScriptOpCodes.*;
import java.util.ArrayList;
import java.util.List;
// TODO: review this
public class NameScript {
public static final int OP_NAME_NEW = OP_1;
public static final int OP_NAME_FIRSTUPDATE = OP_2;
public static final int OP_NAME_UPDATE = OP_3;
protected int op;
protected ArrayList<ScriptChunk> args;
protected Script address;
public NameScript(Script baseScript) {
op = OP_NOP;
args = new ArrayList<ScriptChunk>();
address = baseScript;
ScriptChunk nameOp;
int pc = 0;
List<ScriptChunk> chunks = baseScript.getChunks();
try {
nameOp = chunks.get(pc);
} catch (IndexOutOfBoundsException e) {
return;
}
pc++;
while(true) {
ScriptChunk arg;
try {
arg = chunks.get(pc);
} catch (IndexOutOfBoundsException e) {
return;
}
pc++;
if(arg.opcode == OP_DROP || arg.opcode == OP_2DROP || arg.opcode == OP_NOP) {
break;
}
if( ! (arg.opcode >= 0 && arg.opcode <= OP_PUSHDATA4) ) {
return;
}
args.add(arg);
}
// Move the pc to after any DROP or NOP.
try {
while(chunks.get(pc).opcode == OP_DROP || chunks.get(pc).opcode == OP_2DROP || chunks.get(pc).opcode == OP_NOP) {
pc++;
}
} catch (IndexOutOfBoundsException e) {
}
/* Now, we have the args and the operation. Check if we have indeed
a valid name operation and valid argument counts. Only now set the
op and address members, if everything is valid. */
switch (nameOp.opcode) {
case OP_NAME_NEW:
if(args.size() != 1) {
return;
}
break;
case OP_NAME_FIRSTUPDATE:
if(args.size() != 3) {
return;
}
break;
case OP_NAME_UPDATE:
if(args.size() != 2) {
return;
}
break;
default:
return;
}
op = nameOp.opcode;
ScriptBuilder addressBuilder = new ScriptBuilder();
while(pc < chunks.size()) {
addressBuilder.addChunk(chunks.get(pc));
pc++;
}
address = addressBuilder.build();
}
public boolean isNameOp() {
switch(op) {
case OP_NAME_NEW:
case OP_NAME_FIRSTUPDATE:
case OP_NAME_UPDATE:
return true;
case OP_NOP:
return false;
default:
throw new ScriptException("Invalid name op");
}
}
public Script getAddress() {
return address;
}
// TODO: getNameOp
public boolean isAnyUpdate() {
switch(op) {
case OP_NAME_NEW:
return false;
case OP_NAME_FIRSTUPDATE:
case OP_NAME_UPDATE:
return true;
default:
throw new ScriptException("Not a name op");
}
}
public ScriptChunk getOpName() {
switch(op) {
case OP_NAME_FIRSTUPDATE:
case OP_NAME_UPDATE:
return args.get(0);
default:
throw new ScriptException("Not an AnyUpdate op");
}
}
public ScriptChunk getOpValue() {
switch(op) {
case OP_NAME_FIRSTUPDATE:
return args.get(2);
case OP_NAME_UPDATE:
return args.get(1);
default:
throw new ScriptException("Not an AnyUpdate op");
}
}
// TODO: getOpRand, getOpHash, isNameScript, buildNameNew, buildNameFirstupdate, buildNameUpdate
}

View File

@ -0,0 +1,127 @@
/*
* 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.libdohj.params.AbstractNamecoinParams;
import org.libdohj.params.NamecoinMainNetParams;
import org.bitcoinj.core.Context;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.Util;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
/**
*
* @author Jeremy Rand
*/
public class NameTransactionUtilsTest {
private static final AbstractNamecoinParams params = NamecoinMainNetParams.get();
@Before
public void setUp() throws Exception {
Context context = new Context(params);
}
@Test
public void nameNewGetValueAsString() throws IOException {
final Transaction tx = getNameNewTransaction();
assertNull(NameTransactionUtils.getNameValueAsString(tx, "d/bitcoin"));
assertNull(NameTransactionUtils.getNameValueAsString(tx, "wrongname"));
}
@Test
public void nameFirstUpdateGetValueAsString() throws IOException {
final Transaction tx = getNameFirstUpdateTransaction();
assertEquals("webpagedeveloper.me/namecoin", NameTransactionUtils.getNameValueAsString(tx, "d/bitcoin"));
assertNull(NameTransactionUtils.getNameValueAsString(tx, "wrongname"));
}
@Test
public void nameUpdateGetValueAsString() throws IOException {
final Transaction tx = getNameUpdateTransaction();
assertEquals("{\"info\":{\"registrar\":\"http://register.dot-bit.org\"},\"email\": \"register@dot-bit.org\",\"ns\":[\"ns0.web-sweet-web.net\",\"ns1.web-sweet-web.net\"],\"map\":{\"\":{\"ns\":[\"ns0.web-sweet-web.net\",\"ns1.web-sweet-web.net\"]}}}", NameTransactionUtils.getNameValueAsString(tx, "d/bitcoin"));
assertNull(NameTransactionUtils.getNameValueAsString(tx, "wrongname"));
}
@Test
public void currencyGetValueAsString() throws IOException {
final Transaction tx = getCurrencyTransaction();
assertNull(NameTransactionUtils.getNameValueAsString(tx, "d/bitcoin"));
assertNull(NameTransactionUtils.getNameValueAsString(tx, "wrongname"));
}
Transaction getNameNewTransaction() throws IOException {
byte[] payload;
final Transaction tx;
// https://namecoin.webbtc.com/tx/6047ce28a076118403aa960909c9c4d0056f97ee0da4d37d109515f8367e2ccb
payload = Util.getBytes(getClass().getResourceAsStream("namecoin_name_new_d_bitcoin.bin"));
tx = new Transaction(params, payload);
return tx;
}
Transaction getNameFirstUpdateTransaction() throws IOException {
byte[] payload;
final Transaction tx;
// https://namecoin.webbtc.com/tx/ab1207bd605af57ed0b5325ac94d19578cff3bce668ebe8dda2f42a00b001f5d
payload = Util.getBytes(getClass().getResourceAsStream("namecoin_name_firstupdate_d_bitcoin.bin"));
tx = new Transaction(params, payload);
return tx;
}
Transaction getNameUpdateTransaction() throws IOException {
byte[] payload;
final Transaction tx;
// https://namecoin.webbtc.com/tx/3376c5e0e5b69d0a104863de8432d7c13f891065e7628a72487b770c6418d397
payload = Util.getBytes(getClass().getResourceAsStream("namecoin_name_update_d_bitcoin.bin"));
tx = new Transaction(params, payload);
return tx;
}
Transaction getCurrencyTransaction() throws IOException {
byte[] payload;
final Transaction tx;
// https://namecoin.webbtc.com/tx/4ea5d679d63ef46449a44ca056584a986412676641bdaf13d44a7c7c2e32cca1
payload = Util.getBytes(getClass().getResourceAsStream("namecoin_p2pkh.bin"));
tx = new Transaction(params, payload);
return tx;
}
}

View File

@ -0,0 +1,355 @@
/*
* 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.script;
import org.libdohj.params.AbstractNamecoinParams;
import org.libdohj.params.NamecoinMainNetParams;
import org.bitcoinj.core.Context;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionOutput;
import org.bitcoinj.core.Util;
import org.bitcoinj.script.Script;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import org.junit.rules.ExpectedException;
import java.io.IOException;
/**
*
* @author Jeremy Rand
*/
public class NameScriptTest {
private static final AbstractNamecoinParams params = NamecoinMainNetParams.get();
@Before
public void setUp() throws Exception {
Context context = new Context(params);
}
@Rule
public ExpectedException expectedEx = ExpectedException.none();
@Test
public void nameNewIsNameOp() throws IOException {
final NameScript ns = getNameNewNameScript();
assertTrue(ns.isNameOp());
}
@Test
public void nameNewGetAddress() throws IOException {
final NameScript ns = getNameNewNameScript();
assertEquals("MyVbKbD4MYNUMEpdNAm3Jd3nbr5t8djALC", ns.getAddress().getToAddress(params).toString());
}
// TODO: getNameOp when it's implemented
@Test
public void nameNewIsAnyUpdate() throws IOException {
final NameScript ns = getNameNewNameScript();
assertFalse(ns.isAnyUpdate());
}
@Test
public void nameNewGetOpName() throws IOException {
final NameScript ns = getNameNewNameScript();
expectedEx.expect(org.bitcoinj.core.ScriptException.class);
expectedEx.expectMessage("Not an AnyUpdate op");
ns.getOpName();
}
@Test
public void nameNewGetOpValue() throws IOException {
final NameScript ns = getNameNewNameScript();
expectedEx.expect(org.bitcoinj.core.ScriptException.class);
expectedEx.expectMessage("Not an AnyUpdate op");
ns.getOpValue();
}
// TODO: getOpRand, getOpHash, isNameScript when they're implemented
@Test
public void nameFirstUpdateIsNameOp() throws IOException {
final NameScript ns = getNameFirstUpdateNameScript();
assertTrue(ns.isNameOp());
}
@Test
public void nameFirstUpdateGetAddress() throws IOException {
final NameScript ns = getNameFirstUpdateNameScript();
assertEquals("NGcTVLgw6cgdavaE7C9QvWaY7gKiWbLrjP", ns.getAddress().getToAddress(params).toString());
}
// TODO: getNameOp when it's implemented
@Test
public void nameFirstUpdateIsAnyUpdate() throws IOException {
final NameScript ns = getNameFirstUpdateNameScript();
assertTrue(ns.isAnyUpdate());
}
@Test
public void nameFirstUpdateGetOpName() throws IOException {
final NameScript ns = getNameFirstUpdateNameScript();
assertEquals("d/bitcoin", new String(ns.getOpName().data, "ISO-8859-1"));
}
@Test
public void nameFirstUpdateGetOpValue() throws IOException {
final NameScript ns = getNameFirstUpdateNameScript();
assertEquals("webpagedeveloper.me/namecoin", new String(ns.getOpValue().data, "ISO-8859-1"));
}
// TODO: getOpRand, getOpHash, isNameScript when they're implemented
@Test
public void nameUpdateIsNameOp() throws IOException {
final NameScript ns = getNameUpdateNameScript();
assertTrue(ns.isNameOp());
}
@Test
public void nameUpdateGetAddress() throws IOException {
final NameScript ns = getNameUpdateNameScript();
assertEquals("N9dLs1zHRfZr5cJNjSrvhWrrUcmNSthdmz", ns.getAddress().getToAddress(params).toString());
}
// TODO: getNameOp when it's implemented
@Test
public void nameUpdateIsAnyUpdate() throws IOException {
final NameScript ns = getNameUpdateNameScript();
assertTrue(ns.isAnyUpdate());
}
@Test
public void nameUpdateGetOpName() throws IOException {
final NameScript ns = getNameUpdateNameScript();
assertEquals("d/bitcoin", new String(ns.getOpName().data, "ISO-8859-1"));
}
@Test
public void nameUpdateGetOpValue() throws IOException {
final NameScript ns = getNameUpdateNameScript();
assertEquals("{\"info\":{\"registrar\":\"http://register.dot-bit.org\"},\"email\": \"register@dot-bit.org\",\"ns\":[\"ns0.web-sweet-web.net\",\"ns1.web-sweet-web.net\"],\"map\":{\"\":{\"ns\":[\"ns0.web-sweet-web.net\",\"ns1.web-sweet-web.net\"]}}}", new String(ns.getOpValue().data, "ISO-8859-1"));
}
// TODO: getOpRand, getOpHash, isNameScript when they're implemented
@Test
public void currencyIsNameOp() throws IOException {
final NameScript ns = getCurrencyNameScript();
assertFalse(ns.isNameOp());
}
@Test
public void currencyGetAddress() throws IOException {
final NameScript ns = getCurrencyNameScript();
assertEquals("NCMmrGC7uaJ3uv8feLgBTtwGLQSWfmxMCk", ns.getAddress().getToAddress(params).toString());
}
// TODO: getNameOp when it's implemented
@Test
public void currencyIsAnyUpdate() throws IOException {
final NameScript ns = getCurrencyNameScript();
expectedEx.expect(org.bitcoinj.core.ScriptException.class);
expectedEx.expectMessage("Not a name op");
ns.isAnyUpdate();
}
@Test
public void currencyGetOpName() throws IOException {
final NameScript ns = getCurrencyNameScript();
expectedEx.expect(org.bitcoinj.core.ScriptException.class);
expectedEx.expectMessage("Not an AnyUpdate op");
ns.getOpName();
}
@Test
public void currencyGetOpValue() throws IOException {
final NameScript ns = getCurrencyNameScript();
expectedEx.expect(org.bitcoinj.core.ScriptException.class);
expectedEx.expectMessage("Not an AnyUpdate op");
ns.getOpValue();
}
// TODO: getOpRand, getOpHash, isNameScript when they're implemented
@Test
public void returnIsNameOp() throws IOException {
final NameScript ns = getReturnNameScript();
assertFalse(ns.isNameOp());
}
@Test
public void returnGetAddress() throws IOException {
final NameScript ns = getReturnNameScript();
expectedEx.expect(org.bitcoinj.core.ScriptException.class);
expectedEx.expectMessage("Cannot cast this script to a pay-to-address type");
ns.getAddress().getToAddress(params).toString();
}
// TODO: getNameOp when it's implemented
@Test
public void returnIsAnyUpdate() throws IOException {
final NameScript ns = getReturnNameScript();
expectedEx.expect(org.bitcoinj.core.ScriptException.class);
expectedEx.expectMessage("Not a name op");
ns.isAnyUpdate();
}
@Test
public void returnGetOpName() throws IOException {
final NameScript ns = getReturnNameScript();
expectedEx.expect(org.bitcoinj.core.ScriptException.class);
expectedEx.expectMessage("Not an AnyUpdate op");
ns.getOpName();
}
@Test
public void returnGetOpValue() throws IOException {
final NameScript ns = getReturnNameScript();
expectedEx.expect(org.bitcoinj.core.ScriptException.class);
expectedEx.expectMessage("Not an AnyUpdate op");
ns.getOpValue();
}
// TODO: getOpRand, getOpHash, isNameScript when they're implemented
NameScript getNameNewNameScript() throws IOException {
byte[] payload;
final Transaction tx;
final TransactionOutput out;
final Script outScript;
final NameScript ns;
// https://namecoin.webbtc.com/tx/6047ce28a076118403aa960909c9c4d0056f97ee0da4d37d109515f8367e2ccb
payload = Util.getBytes(getClass().getResourceAsStream("namecoin_name_new_d_bitcoin.bin"));
tx = new Transaction(params, payload);
out = tx.getOutputs().get(1);
outScript = out.getScriptPubKey();
ns = new NameScript(outScript);
return ns;
}
NameScript getNameFirstUpdateNameScript() throws IOException {
byte[] payload;
final Transaction tx;
final TransactionOutput out;
final Script outScript;
final NameScript ns;
// https://namecoin.webbtc.com/tx/ab1207bd605af57ed0b5325ac94d19578cff3bce668ebe8dda2f42a00b001f5d
payload = Util.getBytes(getClass().getResourceAsStream("namecoin_name_firstupdate_d_bitcoin.bin"));
tx = new Transaction(params, payload);
out = tx.getOutputs().get(1);
outScript = out.getScriptPubKey();
ns = new NameScript(outScript);
return ns;
}
NameScript getNameUpdateNameScript() throws IOException {
byte[] payload;
final Transaction tx;
final TransactionOutput out;
final Script outScript;
final NameScript ns;
// https://namecoin.webbtc.com/tx/3376c5e0e5b69d0a104863de8432d7c13f891065e7628a72487b770c6418d397
payload = Util.getBytes(getClass().getResourceAsStream("namecoin_name_update_d_bitcoin.bin"));
tx = new Transaction(params, payload);
out = tx.getOutputs().get(1);
outScript = out.getScriptPubKey();
ns = new NameScript(outScript);
return ns;
}
NameScript getCurrencyNameScript() throws IOException {
byte[] payload;
final Transaction tx;
final TransactionOutput out;
final Script outScript;
final NameScript ns;
// https://namecoin.webbtc.com/tx/4ea5d679d63ef46449a44ca056584a986412676641bdaf13d44a7c7c2e32cca1
payload = Util.getBytes(getClass().getResourceAsStream("namecoin_p2pkh.bin"));
tx = new Transaction(params, payload);
out = tx.getOutputs().get(0);
outScript = out.getScriptPubKey();
ns = new NameScript(outScript);
return ns;
}
NameScript getReturnNameScript() throws IOException {
byte[] payload;
final Transaction tx;
final TransactionOutput out;
final Script outScript;
final NameScript ns;
// https://namecoin.webbtc.com/tx/ab1207bd605af57ed0b5325ac94d19578cff3bce668ebe8dda2f42a00b001f5d
payload = Util.getBytes(getClass().getResourceAsStream("namecoin_name_firstupdate_d_bitcoin.bin"));
tx = new Transaction(params, payload);
out = tx.getOutputs().get(2);
outScript = out.getScriptPubKey();
ns = new NameScript(outScript);
return ns;
}
}

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