Initial commit

This commit is contained in:
Craig Raw 2019-03-15 20:15:28 +02:00
commit 911a54347d
47 changed files with 4283 additions and 0 deletions

7
.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
.idea
.gradle
*iml
build
/*.properties
out
*.log

43
build.gradle Normal file
View file

@ -0,0 +1,43 @@
plugins {
id 'java'
id 'com.github.johnrengelman.shadow' version '4.0.2'
}
group 'com.craigraw'
version '0.1'
sourceCompatibility = 1.8
targetCompatibility = 1.8
repositories {
mavenCentral()
}
dependencies {
compile 'org.zeromq:jeromq:0.5.0'
compile 'com.googlecode.json-simple:json-simple:1.1.1'
compile 'org.bouncycastle:bcprov-jdk15on:1.60'
implementation 'org.slf4j:slf4j-api:1.7.25'
runtime 'org.slf4j:slf4j-log4j12:1.7.25'
testCompile group: 'junit', name: 'junit', version: '4.11'
}
task(runDrongo, dependsOn: 'classes', type: JavaExec) {
main = 'com.craigraw.drongo.Main'
classpath = sourceSets.main.runtimeClasspath
args 'drongo.properties'
}
jar {
manifest {
attributes "Main-Class": "com.craigraw.drongo.Main"
}
baseName = 'drongo'
version = '0.1'
}
shadowJar {
version = '0.1'
classifier = 'all'
}

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View file

@ -0,0 +1,6 @@
#Tue Feb 26 12:05:19 SAST 2019
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip

172
gradlew vendored Executable file
View file

@ -0,0 +1,172 @@
#!/usr/bin/env sh
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"

84
gradlew.bat vendored Normal file
View file

@ -0,0 +1,84 @@
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

2
settings.gradle Normal file
View file

@ -0,0 +1,2 @@
rootProject.name = 'drongo'

View file

@ -0,0 +1,69 @@
package com.craigraw.drongo;
import com.craigraw.drongo.rpc.BitcoinJSONRPCClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.zeromq.SocketType;
import org.zeromq.ZContext;
import org.zeromq.ZMQ;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Drongo {
private static final Logger log = LoggerFactory.getLogger(Drongo.class);
private String nodeZmqAddress;
private BitcoinJSONRPCClient bitcoinJSONRPCClient;
private List<WatchWallet> watchWallets;
private String[] notifyRecipients;
public Drongo(String nodeZmqAddress, Map<String, String> nodeRpc, List<WatchWallet> watchWallets, String[] notifyRecipients) {
this.nodeZmqAddress = nodeZmqAddress;
this.bitcoinJSONRPCClient = new BitcoinJSONRPCClient(nodeRpc.get("host"), nodeRpc.get("port"), nodeRpc.get("user"), nodeRpc.get("password"));
this.watchWallets = watchWallets;
this.notifyRecipients = notifyRecipients;
}
public void start() {
ExecutorService executorService = null;
try {
executorService = Executors.newFixedThreadPool(2);
try (ZContext context = new ZContext()) {
ZMQ.Socket subscriber = context.createSocket(SocketType.SUB);
subscriber.setRcvHWM(0);
subscriber.connect(nodeZmqAddress);
String subscription = "rawtx";
subscriber.subscribe(subscription.getBytes(ZMQ.CHARSET));
while (true) {
String topic = subscriber.recvStr();
if (topic == null)
break;
byte[] data = subscriber.recv();
assert (topic.equals(subscription));
if(subscriber.hasReceiveMore()) {
byte[] endData = subscriber.recv();
}
TransactionTask transactionTask = new TransactionTask(this, data);
executorService.submit(transactionTask);
}
}
} finally {
if(executorService != null) {
executorService.shutdown();
}
}
}
public BitcoinJSONRPCClient getBitcoinJSONRPCClient() {
return bitcoinJSONRPCClient;
}
}

View file

@ -0,0 +1,77 @@
package com.craigraw.drongo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.*;
public class Main {
private static final Logger log = LoggerFactory.getLogger(Main.class);
public static void main(String [] args) {
String propertiesFile = "./drongo.properties";
if(args.length > 0) {
propertiesFile = args[0];
}
Properties properties = new Properties();
properties.setProperty("nodeAddress", "localhost");
try {
File file = new File(propertiesFile);
properties.load(new FileInputStream(propertiesFile));
log.info("Loaded properties from " + file.getCanonicalPath());
} catch (IOException e) {
log.error("Could not load properties from provided path " + propertiesFile);
}
String nodeZmqAddress = properties.getProperty("node.zmqpubrawtx");
if(nodeZmqAddress == null) {
log.error("Property node.zmqpubrawtx not set, provide the zmqpubrawtx setting of the local node");
System.exit(1);
}
Map<String, String> rpcConnection = new LinkedHashMap<String, String>() {
{
put("host", properties.getProperty("node.rpcconnect", "127.0.0.1"));
put("port", properties.getProperty("node.rpcport", "8332"));
put("user", properties.getProperty("node.rpcuser"));
put("password", properties.getProperty("node.rpcpassword"));
}
};
List<WatchWallet> watchWallets = new ArrayList<>();
int walletNumber = 1;
WatchWallet wallet = getWalletFromProperties(properties, walletNumber);
if(wallet == null) {
log.error("Property wallet.name.1 and/or wallet.pubkey.1 not set, provide wallet name and Base58 encoded key starting with xpub or ypub");
System.exit(1);
}
while(wallet != null) {
watchWallets.add(wallet);
wallet = getWalletFromProperties(properties, ++walletNumber);
}
String notifyRecipients = properties.getProperty("notify.recipients");
if(notifyRecipients == null) {
log.error("Property notify.recipients not set, provide comma separated email addresses to receive wallet change notifications");
System.exit(1);
}
Drongo drongo = new Drongo(nodeZmqAddress, rpcConnection, watchWallets, notifyRecipients.split(","));
drongo.start();
}
private static WatchWallet getWalletFromProperties(Properties properties, int walletNumber) {
String walletName = properties.getProperty("wallet.name." + walletNumber);
String walletPubKey = properties.getProperty("wallet.pubkey." + walletNumber);
if(walletName != null && walletPubKey != null) {
return new WatchWallet(walletName, walletPubKey);
}
return null;
}
}

View file

@ -0,0 +1,259 @@
package com.craigraw.drongo;
import com.craigraw.drongo.address.Address;
import com.craigraw.drongo.address.P2PKHAddress;
import com.craigraw.drongo.address.P2SHAddress;
import com.craigraw.drongo.address.P2WPKHAddress;
import com.craigraw.drongo.crypto.ChildNumber;
import com.craigraw.drongo.crypto.DeterministicKey;
import com.craigraw.drongo.crypto.ECKey;
import com.craigraw.drongo.crypto.LazyECPoint;
import com.craigraw.drongo.protocol.Base58;
import com.craigraw.drongo.protocol.Script;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class OutputDescriptor {
private static final Logger log = LoggerFactory.getLogger(OutputDescriptor.class);
private static final int bip32HeaderP2PKHXPub = 0x0488B21E; //The 4 byte header that serializes in base58 to "xpub".
private static final int bip32HeaderP2PKHYPub = 0x049D7CB2; //The 4 byte header that serializes in base58 to "ypub".
private static final int bip32HeaderP2WPKHZPub = 0x04B24746; // The 4 byte header that serializes in base58 to "zpub"
private static final Pattern DESCRIPTOR_PATTERN = Pattern.compile("(.+)\\((\\[[^\\]]+\\])?(xpub[^/\\)]+)(/[/\\d*']+)?\\)\\)?");
private String script;
private int parentFingerprint;
private String keyDerivationPath;
private DeterministicKey pubKey;
private String childDerivationPath;
private ChildNumber pubKeyChildNumber;
public OutputDescriptor(String script, int parentFingerprint, String keyDerivationPath, DeterministicKey pubKey, String childDerivationPath, ChildNumber pubKeyChildNumber) {
this.script = script;
this.parentFingerprint = parentFingerprint;
this.keyDerivationPath = keyDerivationPath;
this.pubKey = pubKey;
this.childDerivationPath = childDerivationPath;
this.pubKeyChildNumber = pubKeyChildNumber;
}
public String getScript() {
return script;
}
public int getParentFingerprint() {
return parentFingerprint;
}
public List<ChildNumber> getKeyDerivation() {
return parsePath(keyDerivationPath);
}
public DeterministicKey getPubKey() {
return pubKey;
}
public List<ChildNumber> getChildDerivation() {
return getChildDerivation(0);
}
public List<ChildNumber> getChildDerivation(int wildCardReplacement) {
return getChildDerivation(new ChildNumber(0, getPubKey().getChildNumber().isHardened()), childDerivationPath, wildCardReplacement);
}
public boolean describesMultipleAddresses() {
return childDerivationPath.endsWith("/*");
}
public List<ChildNumber> getReceivingDerivation(int wildCardReplacement) {
if(describesMultipleAddresses()) {
if(childDerivationPath.endsWith("0/*")) {
return getChildDerivation(new ChildNumber(0, getPubKey().getChildNumber().isHardened()), childDerivationPath, wildCardReplacement);
}
if(pubKeyChildNumber.num() == 0 && childDerivationPath.endsWith("/*")) {
return getChildDerivation(new ChildNumber(0, getPubKey().getChildNumber().isHardened()), childDerivationPath, wildCardReplacement);
}
}
throw new IllegalStateException("Cannot derive receiving address from output descriptor " + this.toString());
}
public List<ChildNumber> getChangeDerivation(int wildCardReplacement) {
if(describesMultipleAddresses()) {
if(childDerivationPath.endsWith("0/*")) {
return getChildDerivation(new ChildNumber(0, getPubKey().getChildNumber().isHardened()), childDerivationPath.replace("0/*", "1/*"), wildCardReplacement);
}
if(pubKeyChildNumber.num() == 1 && childDerivationPath.endsWith("/*")) {
return getChildDerivation(new ChildNumber(1, getPubKey().getChildNumber().isHardened()), childDerivationPath, wildCardReplacement);
}
}
throw new IllegalStateException("Cannot derive change address from output descriptor " + this.toString());
}
private List<ChildNumber> getChildDerivation(ChildNumber firstChild, String derivationPath, int wildCardReplacement) {
List<ChildNumber> path = new ArrayList<>();
path.add(firstChild);
path.addAll(parsePath(derivationPath, wildCardReplacement));
return path;
}
public Address getAddress(DeterministicKey childKey) {
Address address = null;
if(script.equals("pkh")) {
address = new P2PKHAddress(childKey.getPubKeyHash());
} else if(script.equals("sh(wpkh")) {
Address p2wpkhAddress = new P2WPKHAddress(childKey.getPubKeyHash());
Script receivingP2wpkhScript = p2wpkhAddress.getOutputScript();
address = P2SHAddress.fromProgram(receivingP2wpkhScript.getProgram());
} else if(script.equals("wpkh")) {
address = new P2WPKHAddress(childKey.getPubKeyHash());
} else {
throw new IllegalStateException("Cannot determine address for script " + script);
}
return address;
}
// See https://github.com/bitcoin/bitcoin/blob/master/doc/descriptors.md
public static OutputDescriptor getOutputDescriptor(String descriptor) {
String script;
String keyDerivationPath ="";
String extPubKey = null;
String childDerivationPath = "/0/*";
Matcher matcher = DESCRIPTOR_PATTERN.matcher(descriptor);
if(matcher.matches()) {
script = matcher.group(1);
if(matcher.group(2) != null) {
keyDerivationPath = matcher.group(2);
}
extPubKey = matcher.group(3);
if(matcher.group(4) != null) {
childDerivationPath = matcher.group(4);
}
} else if (descriptor.startsWith("xpub")) {
extPubKey = descriptor;
script = "pkh";
} else if(descriptor.startsWith("ypub")) {
extPubKey = descriptor;
script = "sh(wpkh";
} else if(descriptor.startsWith("zpub")) {
extPubKey = descriptor;
script = "wpkh";
} else {
throw new IllegalArgumentException("Could not parse output descriptor:" + descriptor);
}
byte[] serializedKey = Base58.decodeChecked(extPubKey);
ByteBuffer buffer = ByteBuffer.wrap(serializedKey);
int header = buffer.getInt();
if(!(header == bip32HeaderP2PKHXPub || header == bip32HeaderP2PKHYPub || header == bip32HeaderP2WPKHZPub)) {
throw new IllegalArgumentException("Unknown header bytes: " + DeterministicKey.toBase58(serializedKey).substring(0, 4));
}
int depth = buffer.get() & 0xFF; // convert signed byte to positive int since depth cannot be negative
final int parentFingerprint = buffer.getInt();
final int i = buffer.getInt();
ChildNumber childNumber;
List<ChildNumber> path;
if(depth == 0) {
//Poorly formatted extended public key, add first child path element
childNumber = new ChildNumber(0, false);
} else if ((i & ChildNumber.HARDENED_BIT) != 0) {
childNumber = new ChildNumber(i ^ ChildNumber.HARDENED_BIT, true); //already hardened
} else {
childNumber = new ChildNumber(i, false);
}
path = Collections.unmodifiableList(new ArrayList<>(Arrays.asList(childNumber)));
//Remove account level for depth 4 keys
if(depth == 4 && (descriptor.startsWith("xpub") || descriptor.startsWith("ypub") || descriptor.startsWith("zpub"))) {
log.warn("Output descriptor describes a public key derived at depth 4; change addresses not available");
childDerivationPath = "/*";
}
byte[] chainCode = new byte[32];
buffer.get(chainCode);
byte[] data = new byte[33];
buffer.get(data);
if(buffer.hasRemaining()) {
throw new IllegalArgumentException("Found unexpected data in key");
}
DeterministicKey pubKey = new DeterministicKey(path, chainCode, new LazyECPoint(ECKey.CURVE.getCurve(), data), depth, parentFingerprint);
return new OutputDescriptor(script, parentFingerprint, keyDerivationPath, pubKey, childDerivationPath, childNumber);
}
public static List<ChildNumber> parsePath(String path) {
return parsePath(path, 0);
}
public static List<ChildNumber> parsePath(String path, int wildcardReplacement) {
String[] parsedNodes = path.replace("M", "").split("/");
List<ChildNumber> nodes = new ArrayList<>();
for (String n : parsedNodes) {
n = n.replaceAll(" ", "");
if (n.length() == 0) continue;
boolean isHard = n.endsWith("H") || n.endsWith("h") || n.endsWith("'");
if (isHard) n = n.substring(0, n.length() - 1);
if (n.equals("*")) n = Integer.toString(wildcardReplacement);
int nodeNumber = Integer.parseInt(n);
nodes.add(new ChildNumber(nodeNumber, isHard));
}
return nodes;
}
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append(script);
builder.append("(");
builder.append(getExtendedPublicKey());
builder.append(childDerivationPath);
builder.append(")");
if(script.contains("(")){
builder.append(")");
}
return builder.toString();
}
public String getExtendedPublicKey() {
return Base58.encodeChecked(getExtendedPublicKeyBytes());
}
public byte[] getExtendedPublicKeyBytes() {
ByteBuffer buffer = ByteBuffer.allocate(78);
buffer.putInt(bip32HeaderP2PKHXPub);
List<ChildNumber> childPath = parsePath(childDerivationPath);
int depth = 5 - childPath.size();
buffer.put((byte)depth);
buffer.putInt(parentFingerprint);
buffer.putInt(pubKeyChildNumber.i());
buffer.put(pubKey.getChainCode());
buffer.put(pubKey.getPubKey());
return buffer.array();
}
}

View file

@ -0,0 +1,79 @@
package com.craigraw.drongo;
import com.craigraw.drongo.address.Address;
import com.craigraw.drongo.protocol.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
public class TransactionTask implements Runnable {
private static final Logger log = LoggerFactory.getLogger(Drongo.class);
private Drongo drongo;
private byte[] transactionData;
public TransactionTask(Drongo drongo, byte[] transactionData) {
this.drongo = drongo;
this.transactionData = transactionData;
}
@Override
public void run() {
Transaction transaction = new Transaction(transactionData);
Map<String, Transaction> referencedTransactions = new HashMap<>();
Sha256Hash txid = transaction.getTxId();
StringBuilder builder = new StringBuilder("Txid: " + txid.toString() + " ");
StringJoiner inputJoiner = new StringJoiner(", ", "[", "]");
int vin = 0;
for(TransactionInput input : transaction.getInputs()) {
if(input.isCoinBase()) {
inputJoiner.add("Coinbase:" + vin);
} else {
String referencedTxID = input.getOutpoint().getHash().toString();
long referencedVout = input.getOutpoint().getIndex();
Transaction referencedTransaction = referencedTransactions.get(referencedTxID);
if(referencedTransaction == null) {
String referencedTransactionHex = drongo.getBitcoinJSONRPCClient().getRawTransaction(referencedTxID);
referencedTransaction = new Transaction(Utils.hexToBytes(referencedTransactionHex));
referencedTransactions.put(referencedTxID, referencedTransaction);
}
TransactionOutput referencedOutput = referencedTransaction.getOutputs().get((int)referencedVout);
if(referencedOutput.getScript().containsToAddress()) {
Address[] inputAddresses = referencedOutput.getScript().getToAddresses();
input.getOutpoint().setAddresses(inputAddresses);
inputJoiner.add((inputAddresses.length == 1 ? inputAddresses[0] : Arrays.asList(inputAddresses)) + ":" + vin);
} else {
log.warn("Could not determine nature of referenced input tx: " + referencedTxID + ":" + referencedVout);
}
}
vin++;
}
builder.append(inputJoiner.toString() + " => ");
StringJoiner outputJoiner = new StringJoiner(", ", "[", "]");
int vout = 0;
for(TransactionOutput output : transaction.getOutputs()) {
try {
if(output.getScript().containsToAddress()) {
Address[] outputAddresses = output.getScript().getToAddresses();
output.setAddresses(outputAddresses);
outputJoiner.add((outputAddresses.length == 1 ? outputAddresses[0] : Arrays.asList(outputAddresses)) + ":" + vout + " (" + output.getValue() + ")");
}
} catch(ProtocolException e) {
log.debug("Invalid script for output " + vout + " detected (" + e.getMessage() + "). Skipping...");
}
vout++;
}
builder.append(outputJoiner.toString());
log.info(builder.toString());
}
}

View file

@ -0,0 +1,223 @@
package com.craigraw.drongo;
import com.craigraw.drongo.crypto.ChildNumber;
import com.craigraw.drongo.protocol.ProtocolException;
import com.craigraw.drongo.protocol.Ripemd160;
import com.craigraw.drongo.protocol.Sha256Hash;
import org.bouncycastle.crypto.digests.SHA512Digest;
import org.bouncycastle.crypto.macs.HMac;
import org.bouncycastle.crypto.params.KeyParameter;
import javax.xml.bind.DatatypeConverter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.StringJoiner;
public class Utils {
public static final int MAX_INITIAL_ARRAY_LENGTH = 20;
private final static char[] hexArray = "0123456789abcdef".toCharArray();
public static String bytesToHex(byte[] bytes) {
char[] hexChars = new char[bytes.length * 2];
for ( int j = 0; j < bytes.length; j++ ) {
int v = bytes[j] & 0xFF;
hexChars[j * 2] = hexArray[v >>> 4];
hexChars[j * 2 + 1] = hexArray[v & 0x0F];
}
return new String(hexChars);
}
public static byte[] hexToBytes(final String data) {
return decodeHex(data.toCharArray());
}
public static byte[] decodeHex(final char[] data) {
final int len = data.length;
if ((len & 0x01) != 0) {
throw new ProtocolException("Odd number of characters.");
}
final byte[] out = new byte[len >> 1];
// two characters form the hex value.
for (int i = 0, j = 0; j < len; i++) {
int f = toDigit(data[j], j) << 4;
j++;
f = f | toDigit(data[j], j);
j++;
out[i] = (byte) (f & 0xFF);
}
return out;
}
protected static int toDigit(final char ch, final int index) {
final int digit = Character.digit(ch, 16);
if (digit == -1) {
throw new ProtocolException("Illegal hexadecimal character " + ch + " at index " + index);
}
return digit;
}
/** Parse 4 bytes from the byte array (starting at the offset) as unsigned 32-bit integer in little endian format. */
public static long readUint32(byte[] bytes, int offset) {
return (bytes[offset] & 0xffl) |
((bytes[offset + 1] & 0xffl) << 8) |
((bytes[offset + 2] & 0xffl) << 16) |
((bytes[offset + 3] & 0xffl) << 24);
}
/** Parse 8 bytes from the byte array (starting at the offset) as signed 64-bit integer in little endian format. */
public static long readInt64(byte[] bytes, int offset) {
return (bytes[offset] & 0xffl) |
((bytes[offset + 1] & 0xffl) << 8) |
((bytes[offset + 2] & 0xffl) << 16) |
((bytes[offset + 3] & 0xffl) << 24) |
((bytes[offset + 4] & 0xffl) << 32) |
((bytes[offset + 5] & 0xffl) << 40) |
((bytes[offset + 6] & 0xffl) << 48) |
((bytes[offset + 7] & 0xffl) << 56);
}
/** Parse 2 bytes from the byte array (starting at the offset) as unsigned 16-bit integer in little endian format. */
public static int readUint16(byte[] bytes, int offset) {
return (bytes[offset] & 0xff) |
((bytes[offset + 1] & 0xff) << 8);
}
/** Parse 2 bytes from the stream as unsigned 16-bit integer in little endian format. */
public static int readUint16FromStream(InputStream is) {
try {
return (is.read() & 0xff) |
((is.read() & 0xff) << 8);
} catch (IOException x) {
throw new RuntimeException(x);
}
}
/** Parse 4 bytes from the stream as unsigned 32-bit integer in little endian format. */
public static long readUint32FromStream(InputStream is) {
try {
return (is.read() & 0xffl) |
((is.read() & 0xffl) << 8) |
((is.read() & 0xffl) << 16) |
((is.read() & 0xffl) << 24);
} catch (IOException x) {
throw new RuntimeException(x);
}
}
/** Write 2 bytes to the byte array (starting at the offset) as unsigned 16-bit integer in little endian format. */
public static void uint16ToByteArrayLE(int val, byte[] out, int offset) {
out[offset] = (byte) (0xFF & val);
out[offset + 1] = (byte) (0xFF & (val >> 8));
}
/** Write 4 bytes to the byte array (starting at the offset) as unsigned 32-bit integer in little endian format. */
public static void uint32ToByteArrayLE(long val, byte[] out, int offset) {
out[offset] = (byte) (0xFF & val);
out[offset + 1] = (byte) (0xFF & (val >> 8));
out[offset + 2] = (byte) (0xFF & (val >> 16));
out[offset + 3] = (byte) (0xFF & (val >> 24));
}
/** Write 8 bytes to the byte array (starting at the offset) as signed 64-bit integer in little endian format. */
public static void int64ToByteArrayLE(long val, byte[] out, int offset) {
out[offset] = (byte) (0xFF & val);
out[offset + 1] = (byte) (0xFF & (val >> 8));
out[offset + 2] = (byte) (0xFF & (val >> 16));
out[offset + 3] = (byte) (0xFF & (val >> 24));
out[offset + 4] = (byte) (0xFF & (val >> 32));
out[offset + 5] = (byte) (0xFF & (val >> 40));
out[offset + 6] = (byte) (0xFF & (val >> 48));
out[offset + 7] = (byte) (0xFF & (val >> 56));
}
/** Write 2 bytes to the output stream as unsigned 16-bit integer in little endian format. */
public static void uint16ToByteStreamLE(int val, OutputStream stream) throws IOException {
stream.write((int) (0xFF & val));
stream.write((int) (0xFF & (val >> 8)));
}
/** Write 4 bytes to the output stream as unsigned 32-bit integer in little endian format. */
public static void uint32ToByteStreamLE(long val, OutputStream stream) throws IOException {
stream.write((int) (0xFF & val));
stream.write((int) (0xFF & (val >> 8)));
stream.write((int) (0xFF & (val >> 16)));
stream.write((int) (0xFF & (val >> 24)));
}
/** Write 8 bytes to the output stream as signed 64-bit integer in little endian format. */
public static void int64ToByteStreamLE(long val, OutputStream stream) throws IOException {
stream.write((int) (0xFF & val));
stream.write((int) (0xFF & (val >> 8)));
stream.write((int) (0xFF & (val >> 16)));
stream.write((int) (0xFF & (val >> 24)));
stream.write((int) (0xFF & (val >> 32)));
stream.write((int) (0xFF & (val >> 40)));
stream.write((int) (0xFF & (val >> 48)));
stream.write((int) (0xFF & (val >> 56)));
}
/**
* Returns a copy of the given byte array in reverse order.
*/
public static byte[] reverseBytes(byte[] bytes) {
// We could use the XOR trick here but it's easier to understand if we don't. If we find this is really a
// performance issue the matter can be revisited.
byte[] buf = new byte[bytes.length];
for (int i = 0; i < bytes.length; i++)
buf[i] = bytes[bytes.length - 1 - i];
return buf;
}
/**
* Calculates RIPEMD160(SHA256(input)). This is used in Address calculations.
*/
public static byte[] sha256hash160(byte[] input) {
byte[] sha256 = Sha256Hash.hash(input);
return Ripemd160.getHash(sha256);
}
/** Convert to a string path, starting with "M/" */
public static String formatHDPath(List<ChildNumber> path) {
StringJoiner joiner = new StringJoiner("/");
joiner.add("M");
for(ChildNumber number : path) {
joiner.add(number.toString());
}
return joiner.toString();
}
public static List<ChildNumber> appendChild(List<ChildNumber> path, ChildNumber childNumber) {
List<ChildNumber> childPath = new ArrayList<>(path);
childPath.add(childNumber);
return Collections.unmodifiableList(childPath);
}
static HMac createHmacSha512Digest(byte[] key) {
SHA512Digest digest = new SHA512Digest();
HMac hMac = new HMac(digest);
hMac.init(new KeyParameter(key));
return hMac;
}
public static byte[] hmacSha512(HMac hmacSha512, byte[] input) {
hmacSha512.reset();
hmacSha512.update(input, 0, input.length);
byte[] out = new byte[64];
hmacSha512.doFinal(out, 0);
return out;
}
public static byte[] hmacSha512(byte[] key, byte[] data) {
return hmacSha512(createHmacSha512Digest(key), data);
}
}

View file

@ -0,0 +1,59 @@
package com.craigraw.drongo;
import com.craigraw.drongo.address.Address;
import com.craigraw.drongo.crypto.*;
import java.util.HashMap;
import java.util.List;
public class WatchWallet {
private static final int LOOK_AHEAD_LIMIT = 500;
private String name;
private String extPubKey;
private OutputDescriptor outputDescriptor;
private DeterministicHierarchy hierarchy;
private HashMap<String,String> addresses = new HashMap<>(LOOK_AHEAD_LIMIT*2);
public WatchWallet(String name, String descriptor) {
this.name = name;
this.outputDescriptor = OutputDescriptor.getOutputDescriptor(descriptor);
this.hierarchy = new DeterministicHierarchy(outputDescriptor.getPubKey());
}
public void initialiseAddresses() {
if(outputDescriptor.describesMultipleAddresses()) {
for(int index = 0; index <= LOOK_AHEAD_LIMIT; index++) {
List<ChildNumber> receivingDerivation = outputDescriptor.getReceivingDerivation(index);
Address address = getAddress(receivingDerivation);
addresses.put(address.toString(), Utils.formatHDPath(receivingDerivation));
}
for(int index = 0; index <= LOOK_AHEAD_LIMIT; index++) {
List<ChildNumber> changeDerivation = outputDescriptor.getChangeDerivation(index);
Address address = getAddress(changeDerivation);
addresses.put(address.toString(), Utils.formatHDPath(changeDerivation));
}
} else {
List<ChildNumber> derivation = outputDescriptor.getChildDerivation();
Address address = getAddress(derivation);
addresses.put(address.toString(), Utils.formatHDPath(derivation));
}
}
public Address getReceivingAddress(int index) {
return getAddress(outputDescriptor.getReceivingDerivation(index));
}
public Address getChangeAddress(int index) {
return getAddress(outputDescriptor.getChangeDerivation(index));
}
private Address getAddress(List<ChildNumber> path) {
DeterministicKey childKey = hierarchy.get(path);
return outputDescriptor.getAddress(childKey);
}
}

View file

@ -0,0 +1,28 @@
package com.craigraw.drongo.address;
import com.craigraw.drongo.protocol.Base58;
import com.craigraw.drongo.protocol.Script;
public abstract class Address {
protected final byte[] pubKeyHash;
public Address(byte[] pubKeyHash) {
this.pubKeyHash = pubKeyHash;
}
public byte[] getPubKeyHash() {
return pubKeyHash;
}
public String getAddress() {
return Base58.encodeChecked(getVersion(), pubKeyHash);
}
public String toString() {
return getAddress();
}
public abstract int getVersion();
public abstract Script getOutputScript();
}

View file

@ -0,0 +1,30 @@
package com.craigraw.drongo.address;
import com.craigraw.drongo.Utils;
import com.craigraw.drongo.protocol.Script;
import com.craigraw.drongo.protocol.ScriptChunk;
import com.craigraw.drongo.protocol.ScriptOpCodes;
import java.util.ArrayList;
import java.util.List;
public class P2PKAddress extends Address {
private byte[] pubKey;
public P2PKAddress(byte[] pubKey) {
super(Utils.sha256hash160(pubKey));
this.pubKey = pubKey;
}
public int getVersion() {
return 0;
}
public Script getOutputScript() {
List<ScriptChunk> chunks = new ArrayList<>();
chunks.add(new ScriptChunk(pubKey.length, pubKey));
chunks.add(new ScriptChunk(ScriptOpCodes.OP_CHECKSIG, null));
return new Script(chunks);
}
}

View file

@ -0,0 +1,29 @@
package com.craigraw.drongo.address;
import com.craigraw.drongo.protocol.Script;
import com.craigraw.drongo.protocol.ScriptChunk;
import com.craigraw.drongo.protocol.ScriptOpCodes;
import java.util.ArrayList;
import java.util.List;
public class P2PKHAddress extends Address {
public P2PKHAddress(byte[] pubKeyHash) {
super(pubKeyHash);
}
public int getVersion() {
return 0;
}
public Script getOutputScript() {
List<ScriptChunk> chunks = new ArrayList<>();
chunks.add(new ScriptChunk(ScriptOpCodes.OP_DUP, null));
chunks.add(new ScriptChunk(ScriptOpCodes.OP_HASH160, null));
chunks.add(new ScriptChunk(pubKeyHash.length, pubKeyHash));
chunks.add(new ScriptChunk(ScriptOpCodes.OP_EQUALVERIFY, null));
chunks.add(new ScriptChunk(ScriptOpCodes.OP_CHECKSIG, null));
return new Script(chunks);
}
}

View file

@ -0,0 +1,32 @@
package com.craigraw.drongo.address;
import com.craigraw.drongo.Utils;
import com.craigraw.drongo.protocol.Script;
import com.craigraw.drongo.protocol.ScriptChunk;
import com.craigraw.drongo.protocol.ScriptOpCodes;
import java.util.ArrayList;
import java.util.List;
public class P2SHAddress extends Address {
public P2SHAddress(byte[] pubKeyHash) {
super(pubKeyHash);
}
public int getVersion() {
return 5;
}
public Script getOutputScript() {
List<ScriptChunk> chunks = new ArrayList<>();
chunks.add(new ScriptChunk(ScriptOpCodes.OP_HASH160, null));
chunks.add(new ScriptChunk(pubKeyHash.length, pubKeyHash));
chunks.add(new ScriptChunk(ScriptOpCodes.OP_EQUAL, null));
return new Script(chunks);
}
public static P2SHAddress fromProgram(byte[] program) {
return new P2SHAddress(Utils.sha256hash160(program));
}
}

View file

@ -0,0 +1,32 @@
package com.craigraw.drongo.address;
import com.craigraw.drongo.protocol.Bech32;
import com.craigraw.drongo.protocol.Script;
import com.craigraw.drongo.protocol.ScriptChunk;
import java.util.ArrayList;
import java.util.List;
public class P2WPKHAddress extends Address {
public static final String HRP = "bc";
public P2WPKHAddress(byte[] pubKeyHash) {
super(pubKeyHash);
}
public int getVersion() {
return 0;
}
public String getAddress() {
return Bech32.encode(HRP, getVersion(), pubKeyHash);
}
public Script getOutputScript() {
List<ScriptChunk> chunks = new ArrayList<>();
chunks.add(new ScriptChunk(Script.encodeToOpN(getVersion()), null));
chunks.add(new ScriptChunk(pubKeyHash.length, pubKeyHash));
return new Script(chunks);
}
}

View file

@ -0,0 +1,61 @@
package com.craigraw.drongo.crypto;
import java.util.Locale;
public class ChildNumber {
/**
* The bit that's set in the child number to indicate whether this key is "hardened". Given a hardened key, it is
* not possible to derive a child public key if you know only the hardened public key. With a non-hardened key this
* is possible, so you can derive trees of public keys given only a public parent, but the downside is that it's
* possible to leak private keys if you disclose a parent public key and a child private key (elliptic curve maths
* allows you to work upwards).
*/
public static final int HARDENED_BIT = 0x80000000;
public static final ChildNumber ZERO = new ChildNumber(0);
public static final ChildNumber ZERO_HARDENED = new ChildNumber(0, true);
public static final ChildNumber ONE = new ChildNumber(1);
public static final ChildNumber ONE_HARDENED = new ChildNumber(1, true);
/** Integer i as per BIP 32 spec, including the MSB denoting derivation type (0 = public, 1 = private) **/
private final int i;
public ChildNumber(int childNumber, boolean isHardened) {
if (hasHardenedBit(childNumber))
throw new IllegalArgumentException("Most significant bit is reserved and shouldn't be set: " + childNumber);
i = isHardened ? (childNumber | HARDENED_BIT) : childNumber;
}
public ChildNumber(int i) {
this.i = i;
}
private static boolean hasHardenedBit(int a) {
return (a & HARDENED_BIT) != 0;
}
public boolean isHardened() {
return hasHardenedBit(i);
}
public int num() {
return i & (~HARDENED_BIT);
}
/** Returns the uint32 encoded form of the path element, including the most significant bit. */
public int i() { return i; }
public String toString() {
return String.format(Locale.US, "%d%s", num(), isHardened() ? "H" : "");
}
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
return i == ((ChildNumber)o).i;
}
public int hashCode() {
return i;
}
}

View file

@ -0,0 +1,51 @@
package com.craigraw.drongo.crypto;
import com.craigraw.drongo.Utils;
import com.craigraw.drongo.protocol.Base58;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
public class DeterministicHierarchy {
private final Map<List<ChildNumber>, DeterministicKey> keys = new HashMap<>();
private final List<ChildNumber> rootPath;
// Keep track of how many child keys each node has. This is kind of weak.
private final Map<List<ChildNumber>, ChildNumber> lastChildNumbers = new HashMap<>();
public DeterministicHierarchy(DeterministicKey rootKey) {
putKey(rootKey);
rootPath = rootKey.getPath();
}
public final void putKey(DeterministicKey key) {
List<ChildNumber> path = key.getPath();
// Update our tracking of what the next child in each branch of the tree should be. Just assume that keys are
// inserted in order here.
final DeterministicKey parent = key.getParent();
if (parent != null)
lastChildNumbers.put(parent.getPath(), key.getChildNumber());
keys.put(path, key);
}
/**
* Returns a key for the given path, optionally creating it.
*
* @param path the path to the key
* @return next newly created key using the child derivation function
* @throws IllegalArgumentException if create is false and the path was not found.
*/
public DeterministicKey get(List<ChildNumber> path) {
if(!keys.containsKey(path)) {
if(path.size() == 0) {
throw new IllegalArgumentException("Can't derive the master key: nothing to derive from.");
}
DeterministicKey parent = get(path.subList(0, path.size() - 1));
putKey(HDKeyDerivation.deriveChildKey(parent, path.get(path.size() - 1)));
}
return keys.get(path);
}
}

View file

@ -0,0 +1,113 @@
package com.craigraw.drongo.crypto;
import com.craigraw.drongo.Utils;
import com.craigraw.drongo.protocol.Base58;
import com.craigraw.drongo.protocol.Sha256Hash;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.List;
public class DeterministicKey extends ECKey {
private final DeterministicKey parent;
private final List<ChildNumber> childNumberPath;
private final int depth;
private int parentFingerprint; // 0 if this key is root node of key hierarchy
/** 32 bytes */
private final byte[] chainCode;
/**
* Constructs a key from its components, including its public key data and possibly-redundant
* information about its parent key. Invoked when deserializing, but otherwise not something that
* you normally should use.
*/
public DeterministicKey(List<ChildNumber> childNumberPath,
byte[] chainCode,
LazyECPoint publicAsPoint,
int depth,
int parentFingerprint) {
super(compressPoint(publicAsPoint));
if(chainCode.length != 32) {
throw new IllegalArgumentException("Chaincode not 32 bytes in length");
}
this.parent = null;
this.childNumberPath = childNumberPath;
this.chainCode = Arrays.copyOf(chainCode, chainCode.length);
this.depth = depth;
this.parentFingerprint = parentFingerprint;
}
public DeterministicKey(List<ChildNumber> childNumberPath,
byte[] chainCode,
LazyECPoint publicAsPoint,
DeterministicKey parent) {
super(compressPoint(publicAsPoint));
if(chainCode.length != 32) {
throw new IllegalArgumentException("Chaincode not 32 bytes in length");
}
this.parent = parent;
this.childNumberPath = childNumberPath;
this.chainCode = Arrays.copyOf(chainCode, chainCode.length);
this.depth = parent == null ? 0 : parent.depth + 1;
this.parentFingerprint = (parent != null) ? parent.getFingerprint() : 0;
}
/**
* Return this key's depth in the hierarchy, where the root node is at depth zero.
* This may be different than the number of segments in the path if this key was
* deserialized without access to its parent.
*/
public int getDepth() {
return depth;
}
/** Returns the first 32 bits of the result of {@link #getIdentifier()}. */
public int getFingerprint() {
// TODO: why is this different than armory's fingerprint? BIP 32: "The first 32 bits of the identifier are called the fingerprint."
return ByteBuffer.wrap(Arrays.copyOfRange(getIdentifier(), 0, 4)).getInt();
}
/**
* Returns RIPE-MD160(SHA256(pub key bytes)).
*/
public byte[] getIdentifier() {
return Utils.sha256hash160(getPubKey());
}
/**
* Returns the path through some DeterministicHierarchy which reaches this keys position in the tree.
* A path can be written as 0/1/0 which means the first child of the root, the second child of that node, then
* the first child of that node.
*/
public List<ChildNumber> getPath() {
return childNumberPath;
}
public DeterministicKey getParent() {
return parent;
}
/** Returns the last element of the path returned by {@link DeterministicKey#getPath()} */
public ChildNumber getChildNumber() {
return childNumberPath.size() == 0 ? ChildNumber.ZERO : childNumberPath.get(childNumberPath.size() - 1);
}
public byte[] getChainCode() {
return chainCode;
}
public static String toBase58(byte[] ser) {
return Base58.encode(addChecksum(ser));
}
static byte[] addChecksum(byte[] input) {
int inputLength = input.length;
byte[] checksummed = new byte[inputLength + 4];
System.arraycopy(input, 0, checksummed, 0, inputLength);
byte[] checksum = Sha256Hash.hashTwice(input);
System.arraycopy(checksum, 0, checksummed, inputLength, 4);
return checksummed;
}
}

View file

@ -0,0 +1,101 @@
package com.craigraw.drongo.crypto;
import com.craigraw.drongo.Utils;
import org.bouncycastle.asn1.x9.X9ECParameters;
import org.bouncycastle.crypto.ec.CustomNamedCurves;
import org.bouncycastle.crypto.params.ECDomainParameters;
import org.bouncycastle.math.ec.ECPoint;
import org.bouncycastle.math.ec.FixedPointCombMultiplier;
import org.bouncycastle.math.ec.FixedPointUtil;
import java.math.BigInteger;
import java.security.SecureRandom;
public class ECKey {
// The parameters of the secp256k1 curve that Bitcoin uses.
private static final X9ECParameters CURVE_PARAMS = CustomNamedCurves.getByName("secp256k1");
/** The parameters of the secp256k1 curve that Bitcoin uses. */
public static final ECDomainParameters CURVE;
/**
* Equal to CURVE.getN().shiftRight(1), used for canonicalising the S value of a signature. If you aren't
* sure what this is about, you can ignore it.
*/
public static final BigInteger HALF_CURVE_ORDER;
private static final SecureRandom secureRandom;
static {
// Tell Bouncy Castle to precompute data that's needed during secp256k1 calculations.
FixedPointUtil.precompute(CURVE_PARAMS.getG());
CURVE = new ECDomainParameters(CURVE_PARAMS.getCurve(), CURVE_PARAMS.getG(), CURVE_PARAMS.getN(),
CURVE_PARAMS.getH());
HALF_CURVE_ORDER = CURVE_PARAMS.getN().shiftRight(1);
secureRandom = new SecureRandom();
}
protected final LazyECPoint pub;
private byte[] pubKeyHash;
protected ECKey(LazyECPoint pub) {
this.pub = pub;
}
/**
* Utility for compressing an elliptic curve point. Returns the same point if it's already compressed.
* See the ECKey class docs for a discussion of point compression.
*/
public static ECPoint compressPoint(ECPoint point) {
return getPointWithCompression(point, true);
}
public static LazyECPoint compressPoint(LazyECPoint point) {
return point.isCompressed() ? point : new LazyECPoint(compressPoint(point.get()));
}
private static ECPoint getPointWithCompression(ECPoint point, boolean compressed) {
if (point.isCompressed() == compressed)
return point;
point = point.normalize();
BigInteger x = point.getAffineXCoord().toBigInteger();
BigInteger y = point.getAffineYCoord().toBigInteger();
return CURVE.getCurve().createPoint(x, y, compressed);
}
/**
* Gets the raw public key value. This appears in transaction scriptSigs. Note that this is <b>not</b> the same
* as the pubKeyHash/address.
*/
public byte[] getPubKey() {
return pub.getEncoded();
}
/** Gets the public key in the form of an elliptic curve point object from Bouncy Castle. */
public ECPoint getPubKeyPoint() {
return pub.get();
}
/** Gets the hash160 form of the public key (as seen in addresses). */
public byte[] getPubKeyHash() {
if (pubKeyHash == null)
pubKeyHash = Utils.sha256hash160(this.pub.getEncoded());
return pubKeyHash;
}
/**
* Returns public key point from the given private key. To convert a byte array into a BigInteger,
* use {@code new BigInteger(1, bytes);}
*/
public static ECPoint publicPointFromPrivate(BigInteger privKey) {
/*
* TODO: FixedPointCombMultiplier currently doesn't support scalars longer than the group order,
* but that could change in future versions.
*/
if (privKey.bitLength() > CURVE.getN().bitLength()) {
privKey = privKey.mod(CURVE.getN());
}
return new FixedPointCombMultiplier().multiply(CURVE.getG(), privKey);
}
}

View file

@ -0,0 +1,52 @@
package com.craigraw.drongo.crypto;
import com.craigraw.drongo.Utils;
import org.bouncycastle.math.ec.ECPoint;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.util.Arrays;
public class HDKeyDerivation {
public static DeterministicKey deriveChildKey(DeterministicKey parent, ChildNumber childNumber) {
RawKeyBytes rawKey = deriveChildKeyBytesFromPublic(parent, childNumber);
return new DeterministicKey(Utils.appendChild(parent.getPath(), childNumber), rawKey.chainCode, new LazyECPoint(ECKey.CURVE.getCurve(), rawKey.keyBytes), parent);
}
public static RawKeyBytes deriveChildKeyBytesFromPublic(DeterministicKey parent, ChildNumber childNumber) {
if(childNumber.isHardened()) {
throw new IllegalArgumentException("Can't use private derivation with public keys only.");
}
byte[] parentPublicKey = parent.getPubKeyPoint().getEncoded(true);
if(parentPublicKey.length != 33) {
throw new IllegalArgumentException("Parent pubkey must be 33 bytes, but is " + parentPublicKey.length);
}
ByteBuffer data = ByteBuffer.allocate(37);
data.put(parentPublicKey);
data.putInt(childNumber.i());
byte[] i = Utils.hmacSha512(parent.getChainCode(), data.array());
if(i.length != 64) {
throw new IllegalStateException("HmacSHA512 output must be 64 bytes, is" + i.length);
}
byte[] il = Arrays.copyOfRange(i, 0, 32);
byte[] chainCode = Arrays.copyOfRange(i, 32, 64);
BigInteger ilInt = new BigInteger(1, il);
final BigInteger N = ECKey.CURVE.getN();
ECPoint Ki = ECKey.publicPointFromPrivate(ilInt).add(parent.getPubKeyPoint());
return new RawKeyBytes(Ki.getEncoded(true), chainCode);
}
public static class RawKeyBytes {
public final byte[] keyBytes, chainCode;
public RawKeyBytes(byte[] keyBytes, byte[] chainCode) {
this.keyBytes = keyBytes;
this.chainCode = chainCode;
}
}
}

View file

@ -0,0 +1,52 @@
package com.craigraw.drongo.crypto;
import org.bouncycastle.math.ec.ECCurve;
import org.bouncycastle.math.ec.ECPoint;
import java.util.Arrays;
public class LazyECPoint {
// If curve is set, bits is also set. If curve is unset, point is set and bits is unset. Point can be set along
// with curve and bits when the cached form has been accessed and thus must have been converted.
private final ECCurve curve;
private final byte[] bits;
// This field is effectively final - once set it won't change again. However it can be set after
// construction.
private ECPoint point;
public LazyECPoint(ECCurve curve, byte[] bits) {
this.curve = curve;
this.bits = bits;
}
public LazyECPoint(ECPoint point) {
this.point = point;
this.curve = null;
this.bits = null;
}
public ECPoint get() {
if (point == null)
point = curve.decodePoint(bits);
return point;
}
// Delegated methods.
public ECPoint getDetachedPoint() {
return get().getDetachedPoint();
}
public boolean isCompressed() {
return get().isCompressed();
}
public byte[] getEncoded() {
if (bits != null)
return Arrays.copyOf(bits, bits.length);
else
return get().getEncoded();
}
}

View file

@ -0,0 +1,216 @@
/*
* Copyright 2011 Google Inc.
* Copyright 2018 Andreas Schildbach
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.craigraw.drongo.protocol;
import java.math.BigInteger;
import java.util.Arrays;
/**
* Base58 is a way to encode Bitcoin addresses (or arbitrary data) as alphanumeric strings.
* <p>
* Note that this is not the same base58 as used by Flickr, which you may find referenced around the Internet.
* <p>
* Satoshi explains: why base-58 instead of standard base-64 encoding?
* <ul>
* <li>Don't want 0OIl characters that look the same in some fonts and
* could be used to create visually identical looking account numbers.</li>
* <li>A string with non-alphanumeric characters is not as easily accepted as an account number.</li>
* <li>E-mail usually won't line-break if there's no punctuation to break at.</li>
* <li>Doubleclicking selects the whole number as one word if it's all alphanumeric.</li>
* </ul>
* <p>
* However, note that the encoding/decoding runs in O(n&sup2;) time, so it is not useful for large data.
* <p>
* The basic idea of the encoding is to treat the data bytes as a large number represented using
* base-256 digits, convert the number to be represented using base-58 digits, preserve the exact
* number of leading zeros (which are otherwise lost during the mathematical operations on the
* numbers), and finally represent the resulting base-58 digits as alphanumeric ASCII characters.
*/
public class Base58 {
public static final char[] ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz".toCharArray();
private static final char ENCODED_ZERO = ALPHABET[0];
private static final int[] INDEXES = new int[128];
static {
Arrays.fill(INDEXES, -1);
for (int i = 0; i < ALPHABET.length; i++) {
INDEXES[ALPHABET[i]] = i;
}
}
/**
* Encodes the given bytes as a base58 string (no checksum is appended).
*
* @param input the bytes to encode
* @return the base58-encoded string
*/
public static String encode(byte[] input) {
if (input.length == 0) {
return "";
}
// Count leading zeros.
int zeros = 0;
while (zeros < input.length && input[zeros] == 0) {
++zeros;
}
// Convert base-256 digits to base-58 digits (plus conversion to ASCII characters)
input = Arrays.copyOf(input, input.length); // since we modify it in-place
char[] encoded = new char[input.length * 2]; // upper bound
int outputStart = encoded.length;
for (int inputStart = zeros; inputStart < input.length; ) {
encoded[--outputStart] = ALPHABET[divmod(input, inputStart, 256, 58)];
if (input[inputStart] == 0) {
++inputStart; // optimization - skip leading zeros
}
}
// Preserve exactly as many leading encoded zeros in output as there were leading zeros in input.
while (outputStart < encoded.length && encoded[outputStart] == ENCODED_ZERO) {
++outputStart;
}
while (--zeros >= 0) {
encoded[--outputStart] = ENCODED_ZERO;
}
// Return encoded string (including encoded leading zeros).
return new String(encoded, outputStart, encoded.length - outputStart);
}
/**
* Encodes the bytes as a base58 string. A checksum is appended.
*
* @param payload the bytes to encode, e.g. pubkey hash
* @return the base58-encoded string
*/
public static String encodeChecked(byte[] payload) {
// A stringified buffer is:
// data bytes + 4 bytes check code (a truncated hash)
byte[] addressBytes = new byte[payload.length + 4];
System.arraycopy(payload, 0, addressBytes, 0, payload.length);
byte[] checksum = Sha256Hash.hashTwice(addressBytes, 0, payload.length);
System.arraycopy(checksum, 0, addressBytes, payload.length, 4);
return Base58.encode(addressBytes);
}
/**
* Encodes the given version and bytes as a base58 string. A checksum is appended.
*
* @param version the version to encode
* @param payload the bytes to encode, e.g. pubkey hash
* @return the base58-encoded string
*/
public static String encodeChecked(int version, byte[] payload) {
if (version < 0 || version > 255)
throw new IllegalArgumentException("Version not in range.");
// A stringified buffer is:
// 1 byte version + data bytes + 4 bytes check code (a truncated hash)
byte[] addressBytes = new byte[1 + payload.length + 4];
addressBytes[0] = (byte) version;
System.arraycopy(payload, 0, addressBytes, 1, payload.length);
byte[] checksum = Sha256Hash.hashTwice(addressBytes, 0, payload.length + 1);
System.arraycopy(checksum, 0, addressBytes, payload.length + 1, 4);
return Base58.encode(addressBytes);
}
/**
* Decodes the given base58 string into the original data bytes.
*
* @param input the base58-encoded string to decode
* @return the decoded data bytes
*/
public static byte[] decode(String input) {
if (input.length() == 0) {
return new byte[0];
}
// Convert the base58-encoded ASCII chars to a base58 byte sequence (base58 digits).
byte[] input58 = new byte[input.length()];
for (int i = 0; i < input.length(); ++i) {
char c = input.charAt(i);
int digit = c < 128 ? INDEXES[c] : -1;
if (digit < 0) {
throw new ProtocolException("Invalid character " + c + " at position " + i);
}
input58[i] = (byte) digit;
}
// Count leading zeros.
int zeros = 0;
while (zeros < input58.length && input58[zeros] == 0) {
++zeros;
}
// Convert base-58 digits to base-256 digits.
byte[] decoded = new byte[input.length()];
int outputStart = decoded.length;
for (int inputStart = zeros; inputStart < input58.length; ) {
decoded[--outputStart] = divmod(input58, inputStart, 58, 256);
if (input58[inputStart] == 0) {
++inputStart; // optimization - skip leading zeros
}
}
// Ignore extra leading zeroes that were added during the calculation.
while (outputStart < decoded.length && decoded[outputStart] == 0) {
++outputStart;
}
// Return decoded data (including original number of leading zeros).
return Arrays.copyOfRange(decoded, outputStart - zeros, decoded.length);
}
public static BigInteger decodeToBigInteger(String input) {
return new BigInteger(1, decode(input));
}
/**
* Decodes the given base58 string into the original data bytes, using the checksum in the
* last 4 bytes of the decoded data to verify that the rest are correct. The checksum is
* removed from the returned data.
*
* @param input the base58-encoded string to decode (which should include the checksum)
*/
public static byte[] decodeChecked(String input) {
byte[] decoded = decode(input);
if (decoded.length < 4)
throw new ProtocolException("Input too short: " + decoded.length);
byte[] data = Arrays.copyOfRange(decoded, 0, decoded.length - 4);
byte[] checksum = Arrays.copyOfRange(decoded, decoded.length - 4, decoded.length);
byte[] actualChecksum = Arrays.copyOfRange(Sha256Hash.hashTwice(data), 0, 4);
if (!Arrays.equals(checksum, actualChecksum))
throw new ProtocolException("Invalid checksum");
return data;
}
/**
* Divides a number, represented as an array of bytes each containing a single digit
* in the specified base, by the given divisor. The given number is modified in-place
* to contain the quotient, and the return value is the remainder.
*
* @param number the number to divide
* @param firstDigit the index within the array of the first non-zero digit
* (this is used for optimization by skipping the leading zeros)
* @param base the base in which the number's digits are represented (up to 256)
* @param divisor the number to divide by (up to 256)
* @return the remainder of the division operation
*/
private static byte divmod(byte[] number, int firstDigit, int base, int divisor) {
// this is just long division which accounts for the base of the input digits
int remainder = 0;
for (int i = firstDigit; i < number.length; i++) {
int digit = (int) number[i] & 0xFF;
int temp = remainder * base + digit;
number[i] = (byte) (temp / divisor);
remainder = temp % divisor;
}
return (byte) remainder;
}
}

View file

@ -0,0 +1,209 @@
package com.craigraw.drongo.protocol;
/*
* Copyright 2018 Coinomi Ltd
*
* 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.
*/
import java.io.ByteArrayOutputStream;
import java.util.Arrays;
import java.util.Locale;
public class Bech32 {
/** The Bech32 character set for encoding. */
private static final String CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
/** The Bech32 character set for decoding. */
private static final byte[] CHARSET_REV = {
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
15, -1, 10, 17, 21, 20, 26, 30, 7, 5, -1, -1, -1, -1, -1, -1,
-1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1,
1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1,
-1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1,
1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1
};
public static class Bech32Data {
public final String hrp;
public final byte[] data;
private Bech32Data(final String hrp, final byte[] data) {
this.hrp = hrp;
this.data = data;
}
}
/** Find the polynomial with value coefficients mod the generator as 30-bit. */
private static int polymod(final byte[] values) {
int c = 1;
for (byte v_i: values) {
int c0 = (c >>> 25) & 0xff;
c = ((c & 0x1ffffff) << 5) ^ (v_i & 0xff);
if ((c0 & 1) != 0) c ^= 0x3b6a57b2;
if ((c0 & 2) != 0) c ^= 0x26508e6d;
if ((c0 & 4) != 0) c ^= 0x1ea119fa;
if ((c0 & 8) != 0) c ^= 0x3d4233dd;
if ((c0 & 16) != 0) c ^= 0x2a1462b3;
}
return c;
}
/** Expand a HRP for use in checksum computation. */
private static byte[] expandHrp(final String hrp) {
int hrpLength = hrp.length();
byte ret[] = new byte[hrpLength * 2 + 1];
for (int i = 0; i < hrpLength; ++i) {
int c = hrp.charAt(i) & 0x7f; // Limit to standard 7-bit ASCII
ret[i] = (byte) ((c >>> 5) & 0x07);
ret[i + hrpLength + 1] = (byte) (c & 0x1f);
}
ret[hrpLength] = 0;
return ret;
}
/** Verify a checksum. */
private static boolean verifyChecksum(final String hrp, final byte[] values) {
byte[] hrpExpanded = expandHrp(hrp);
byte[] combined = new byte[hrpExpanded.length + values.length];
System.arraycopy(hrpExpanded, 0, combined, 0, hrpExpanded.length);
System.arraycopy(values, 0, combined, hrpExpanded.length, values.length);
return polymod(combined) == 1;
}
/** Create a checksum. */
private static byte[] createChecksum(final String hrp, final byte[] values) {
byte[] hrpExpanded = expandHrp(hrp);
byte[] enc = new byte[hrpExpanded.length + values.length + 6];
System.arraycopy(hrpExpanded, 0, enc, 0, hrpExpanded.length);
System.arraycopy(values, 0, enc, hrpExpanded.length, values.length);
int mod = polymod(enc) ^ 1;
byte[] ret = new byte[6];
for (int i = 0; i < 6; ++i) {
ret[i] = (byte) ((mod >>> (5 * (5 - i))) & 31);
}
return ret;
}
/** Encode a Bech32 string. */
public static String encode(final Bech32Data bech32) {
return encode(bech32.hrp, bech32.data);
}
/** Encode a Bech32 string. */
public static String encode(String hrp, int version, final byte[] values) {
return encode(hrp, encode(0, values));
}
/** Encode a Bech32 string. */
public static String encode(String hrp, final byte[] values) {
if(hrp.length() < 1) {
throw new ProtocolException("Human-readable part is too short");
}
if(hrp.length() > 83) {
throw new ProtocolException("Human-readable part is too long");
}
hrp = hrp.toLowerCase(Locale.ROOT);
byte[] checksum = createChecksum(hrp, values);
byte[] combined = new byte[values.length + checksum.length];
System.arraycopy(values, 0, combined, 0, values.length);
System.arraycopy(checksum, 0, combined, values.length, checksum.length);
StringBuilder sb = new StringBuilder(hrp.length() + 1 + combined.length);
sb.append(hrp);
sb.append('1');
for (byte b : combined) {
sb.append(CHARSET.charAt(b));
}
return sb.toString();
}
/** Decode a Bech32 string. */
public static Bech32Data decode(final String str) {
boolean lower = false, upper = false;
if (str.length() < 8)
throw new ProtocolException("Input too short: " + str.length());
if (str.length() > 90)
throw new ProtocolException("Input too long: " + str.length());
for (int i = 0; i < str.length(); ++i) {
char c = str.charAt(i);
if (c < 33 || c > 126) throw new ProtocolException("Invalid character " + c + " at position " + i);
if (c >= 'a' && c <= 'z') {
if (upper)
throw new ProtocolException("Invalid character " + c + " at position " + i);
lower = true;
}
if (c >= 'A' && c <= 'Z') {
if (lower)
throw new ProtocolException("Invalid character " + c + " at position " + i);
upper = true;
}
}
final int pos = str.lastIndexOf('1');
if (pos < 1) throw new ProtocolException("Missing human-readable part");
final int dataPartLength = str.length() - 1 - pos;
if (dataPartLength < 6) throw new ProtocolException("Data part too short: " + dataPartLength);
byte[] values = new byte[dataPartLength];
for (int i = 0; i < dataPartLength; ++i) {
char c = str.charAt(i + pos + 1);
if (CHARSET_REV[c] == -1) throw new ProtocolException("Invalid character " + c + " at position " + i);
values[i] = CHARSET_REV[c];
}
String hrp = str.substring(0, pos).toLowerCase(Locale.ROOT);
if (!verifyChecksum(hrp, values)) throw new ProtocolException("Invalid checksum");
return new Bech32Data(hrp, Arrays.copyOfRange(values, 0, values.length - 6));
}
private static byte[] encode(int witnessVersion, byte[] witnessProgram) {
byte[] convertedProgram = convertBits(witnessProgram, 0, witnessProgram.length, 8, 5, true);
byte[] bytes = new byte[1 + convertedProgram.length];
bytes[0] = (byte) (Script.encodeToOpN(witnessVersion) & 0xff);
System.arraycopy(convertedProgram, 0, bytes, 1, convertedProgram.length);
return bytes;
}
/**
* Helper for re-arranging bits into groups.
*/
private static byte[] convertBits(final byte[] in, final int inStart, final int inLen, final int fromBits,
final int toBits, final boolean pad) {
int acc = 0;
int bits = 0;
ByteArrayOutputStream out = new ByteArrayOutputStream(64);
final int maxv = (1 << toBits) - 1;
final int max_acc = (1 << (fromBits + toBits - 1)) - 1;
for (int i = 0; i < inLen; i++) {
int value = in[i + inStart] & 0xff;
if ((value >>> fromBits) != 0) {
throw new ProtocolException(
String.format("Input value '%X' exceeds '%d' bit size", value, fromBits));
}
acc = ((acc << fromBits) | value) & max_acc;
bits += fromBits;
while (bits >= toBits) {
bits -= toBits;
out.write((acc >>> bits) & maxv);
}
}
if (pad) {
if (bits > 0)
out.write((acc << (toBits - bits)) & maxv);
} else if (bits >= fromBits || ((acc << (toBits - bits)) & maxv) != 0) {
throw new ProtocolException("Could not convert bits, invalid padding");
}
return out.toByteArray();
}
}

View file

@ -0,0 +1,22 @@
package com.craigraw.drongo.protocol;
public class ProtocolException extends RuntimeException {
public ProtocolException() {
}
public ProtocolException(String message) {
super(message);
}
public ProtocolException(String message, Throwable cause) {
super(message, cause);
}
public ProtocolException(Throwable cause) {
super(cause);
}
public ProtocolException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}

View file

@ -0,0 +1,157 @@
package com.craigraw.drongo.protocol;
/*
* Bitcoin cryptography library
* Copyright (c) Project Nayuki
*
* https://www.nayuki.io/page/bitcoin-cryptography-library
* https://github.com/nayuki/Bitcoin-Cryptography-Library
*/
import static java.lang.Integer.rotateLeft;
import java.util.Arrays;
import java.util.Objects;
/**
* Computes the RIPEMD-160 hash of an array of bytes. Not instantiable.
*/
public final class Ripemd160 {
private static final int BLOCK_LEN = 64; // In bytes
/*---- Static functions ----*/
/**
* Computes and returns a 20-byte (160-bit) hash of the specified binary message.
* Each call will return a new byte array object instance.
* @param msg the message to compute the hash of
* @return a 20-byte array representing the message's RIPEMD-160 hash
* @throws NullPointerException if the message is {@code null}
*/
public static byte[] getHash(byte[] msg) {
// Compress whole message blocks
Objects.requireNonNull(msg);
int[] state = {0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0xC3D2E1F0};
int off = msg.length / BLOCK_LEN * BLOCK_LEN;
compress(state, msg, off);
// Final blocks, padding, and length
byte[] block = new byte[BLOCK_LEN];
System.arraycopy(msg, off, block, 0, msg.length - off);
off = msg.length % block.length;
block[off] = (byte)0x80;
off++;
if (off + 8 > block.length) {
compress(state, block, block.length);
Arrays.fill(block, (byte)0);
}
long len = (long)msg.length << 3;
for (int i = 0; i < 8; i++)
block[block.length - 8 + i] = (byte)(len >>> (i * 8));
compress(state, block, block.length);
// Int32 array to bytes in little endian
byte[] result = new byte[state.length * 4];
for (int i = 0; i < result.length; i++)
result[i] = (byte)(state[i / 4] >>> (i % 4 * 8));
return result;
}
/*---- Private functions ----*/
private static void compress(int[] state, byte[] blocks, int len) {
if (len % BLOCK_LEN != 0)
throw new IllegalArgumentException();
for (int i = 0; i < len; i += BLOCK_LEN) {
// Message schedule
int[] schedule = new int[16];
for (int j = 0; j < BLOCK_LEN; j++)
schedule[j / 4] |= (blocks[i + j] & 0xFF) << (j % 4 * 8);
// The 80 rounds
int al = state[0], ar = state[0];
int bl = state[1], br = state[1];
int cl = state[2], cr = state[2];
int dl = state[3], dr = state[3];
int el = state[4], er = state[4];
for (int j = 0; j < 80; j++) {
int temp;
temp = rotateLeft(al + f(j, bl, cl, dl) + schedule[RL[j]] + KL[j / 16], SL[j]) + el;
al = el;
el = dl;
dl = rotateLeft(cl, 10);
cl = bl;
bl = temp;
temp = rotateLeft(ar + f(79 - j, br, cr, dr) + schedule[RR[j]] + KR[j / 16], SR[j]) + er;
ar = er;
er = dr;
dr = rotateLeft(cr, 10);
cr = br;
br = temp;
}
int temp = state[1] + cl + dr;
state[1] = state[2] + dl + er;
state[2] = state[3] + el + ar;
state[3] = state[4] + al + br;
state[4] = state[0] + bl + cr;
state[0] = temp;
}
}
private static int f(int i, int x, int y, int z) {
assert 0 <= i && i < 80;
if (i < 16) return x ^ y ^ z;
if (i < 32) return (x & y) | (~x & z);
if (i < 48) return (x | ~y) ^ z;
if (i < 64) return (x & z) | (y & ~z);
return x ^ (y | ~z);
}
/*---- Class constants ----*/
private static final int[] KL = {0x00000000, 0x5A827999, 0x6ED9EBA1, 0x8F1BBCDC, 0xA953FD4E}; // Round constants for left line
private static final int[] KR = {0x50A28BE6, 0x5C4DD124, 0x6D703EF3, 0x7A6D76E9, 0x00000000}; // Round constants for right line
private static final int[] RL = { // Message schedule for left line
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
7, 4, 13, 1, 10, 6, 15, 3, 12, 0, 9, 5, 2, 14, 11, 8,
3, 10, 14, 4, 9, 15, 8, 1, 2, 7, 0, 6, 13, 11, 5, 12,
1, 9, 11, 10, 0, 8, 12, 4, 13, 3, 7, 15, 14, 5, 6, 2,
4, 0, 5, 9, 7, 12, 2, 10, 14, 1, 3, 8, 11, 6, 15, 13};
private static final int[] RR = { // Message schedule for right line
5, 14, 7, 0, 9, 2, 11, 4, 13, 6, 15, 8, 1, 10, 3, 12,
6, 11, 3, 7, 0, 13, 5, 10, 14, 15, 8, 12, 4, 9, 1, 2,
15, 5, 1, 3, 7, 14, 6, 9, 11, 8, 12, 2, 10, 0, 4, 13,
8, 6, 4, 1, 3, 11, 15, 0, 5, 12, 2, 13, 9, 7, 10, 14,
12, 15, 10, 4, 1, 5, 8, 7, 6, 2, 13, 14, 0, 3, 9, 11};
private static final int[] SL = { // Left-rotation for left line
11, 14, 15, 12, 5, 8, 7, 9, 11, 13, 14, 15, 6, 7, 9, 8,
7, 6, 8, 13, 11, 9, 7, 15, 7, 12, 15, 9, 11, 7, 13, 12,
11, 13, 6, 7, 14, 9, 13, 15, 14, 8, 13, 6, 5, 12, 7, 5,
11, 12, 14, 15, 14, 15, 9, 8, 9, 14, 5, 6, 8, 6, 5, 12,
9, 15, 5, 11, 6, 8, 13, 12, 5, 12, 13, 14, 11, 8, 5, 6};
private static final int[] SR = { // Left-rotation for right line
8, 9, 9, 11, 13, 15, 15, 5, 7, 7, 8, 11, 14, 14, 12, 6,
9, 13, 15, 7, 12, 8, 9, 11, 7, 7, 12, 7, 6, 15, 13, 11,
9, 7, 15, 11, 8, 6, 6, 14, 12, 13, 5, 14, 13, 13, 7, 5,
15, 5, 8, 11, 14, 14, 6, 14, 6, 9, 12, 9, 12, 5, 15, 8,
8, 5, 12, 9, 12, 5, 14, 6, 8, 13, 6, 5, 15, 13, 11, 11};
/*---- Miscellaneous ----*/
private Ripemd160() {} // Not instantiable
}

View file

@ -0,0 +1,169 @@
package com.craigraw.drongo.protocol;
import com.craigraw.drongo.Utils;
import com.craigraw.drongo.address.*;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import static com.craigraw.drongo.protocol.ScriptOpCodes.*;
public class Script {
public static final long MAX_SCRIPT_ELEMENT_SIZE = 520;
// The program is a set of chunks where each element is either [opcode] or [data, data, data ...]
protected List<ScriptChunk> chunks;
protected byte[] program;
public Script(byte[] programBytes) {
program = programBytes;
parse(programBytes);
}
public Script(List<ScriptChunk> chunks) {
this.chunks = Collections.unmodifiableList(new ArrayList<>(chunks));
}
private static final ScriptChunk[] STANDARD_TRANSACTION_SCRIPT_CHUNKS = {
new ScriptChunk(ScriptOpCodes.OP_DUP, null, 0),
new ScriptChunk(ScriptOpCodes.OP_HASH160, null, 1),
new ScriptChunk(ScriptOpCodes.OP_EQUALVERIFY, null, 23),
new ScriptChunk(ScriptOpCodes.OP_CHECKSIG, null, 24),
};
private void parse(byte[] program) {
chunks = new ArrayList<>(5); // Common size.
ByteArrayInputStream bis = new ByteArrayInputStream(program);
int initialSize = bis.available();
while (bis.available() > 0) {
int startLocationInProgram = initialSize - bis.available();
int opcode = bis.read();
long dataToRead = -1;
if (opcode >= 0 && opcode < OP_PUSHDATA1) {
// Read some bytes of data, where how many is the opcode value itself.
dataToRead = opcode;
} else if (opcode == OP_PUSHDATA1) {
if (bis.available() < 1) throw new ProtocolException("Unexpected end of script");
dataToRead = bis.read();
} else if (opcode == OP_PUSHDATA2) {
// Read a short, then read that many bytes of data.
if (bis.available() < 2) throw new ProtocolException("Unexpected end of script");
dataToRead = Utils.readUint16FromStream(bis);
} else if (opcode == OP_PUSHDATA4) {
// Read a uint32, then read that many bytes of data.
// Though this is allowed, because its value cannot be > 520, it should never actually be used
if (bis.available() < 4) throw new ProtocolException("Unexpected end of script");
dataToRead = Utils.readUint32FromStream(bis);
}
ScriptChunk chunk;
if (dataToRead == -1) {
chunk = new ScriptChunk(opcode, null, startLocationInProgram);
} else {
if (dataToRead > bis.available())
throw new ProtocolException("Push of data element that is larger than remaining data");
byte[] data = new byte[(int)dataToRead];
if(dataToRead != 0 && bis.read(data, 0, (int)dataToRead) != dataToRead) {
throw new ProtocolException();
}
chunk = new ScriptChunk(opcode, data, startLocationInProgram);
}
// Save some memory by eliminating redundant copies of the same chunk objects.
for (ScriptChunk c : STANDARD_TRANSACTION_SCRIPT_CHUNKS) {
if (c.equals(chunk)) chunk = c;
}
chunks.add(chunk);
}
}
/** Returns the serialized program as a newly created byte array. */
public byte[] getProgram() {
try {
// Don't round-trip as Bitcoin Core doesn't and it would introduce a mismatch.
if (program != null)
return Arrays.copyOf(program, program.length);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
for (ScriptChunk chunk : chunks) {
chunk.write(bos);
}
program = bos.toByteArray();
return program;
} catch (IOException e) {
throw new RuntimeException(e); // Cannot happen.
}
}
/**
* Returns true if this script has the required form to contain a destination address
*/
public boolean containsToAddress() {
return ScriptPattern.isP2PK(this) || ScriptPattern.isP2PKH(this) || ScriptPattern.isP2SH(this) || ScriptPattern.isP2WH(this) || ScriptPattern.isSentToMultisig(this);
}
/**
* <p>If the program somehow pays to a hash, returns the hash.</p>
*
* <p>Otherwise this method throws a ScriptException.</p>
*/
public byte[] getPubKeyHash() throws ProtocolException {
if (ScriptPattern.isP2PKH(this))
return ScriptPattern.extractHashFromP2PKH(this);
else if (ScriptPattern.isP2SH(this))
return ScriptPattern.extractHashFromP2SH(this);
else if (ScriptPattern.isP2WH(this))
return ScriptPattern.extractHashFromP2WH(this);
else
throw new ProtocolException("Script not in the standard scriptPubKey form");
}
/**
* Gets the destination address from this script, if it's in the required form.
*/
public Address[] getToAddresses() {
if (ScriptPattern.isP2PK(this))
return new Address[] { new P2PKAddress( ScriptPattern.extractPKFromP2PK(this)) };
else if (ScriptPattern.isP2PKH(this))
return new Address[] { new P2PKHAddress( ScriptPattern.extractHashFromP2PKH(this)) };
else if (ScriptPattern.isP2SH(this))
return new Address[] { new P2SHAddress(ScriptPattern.extractHashFromP2SH(this)) };
else if (ScriptPattern.isP2WH(this))
return new Address[] { new P2WPKHAddress(ScriptPattern.extractHashFromP2WH(this)) };
else if (ScriptPattern.isSentToMultisig(this))
return ScriptPattern.extractMultisigAddresses(this);
else
throw new ProtocolException("Cannot cast this script to an address");
}
public static int decodeFromOpN(int opcode) {
if((opcode != OP_0 && opcode != OP_1NEGATE) && (opcode < OP_1 || opcode > OP_16)) {
throw new ProtocolException("decodeFromOpN called on non OP_N opcode: " + opcode);
}
if (opcode == OP_0)
return 0;
else if (opcode == OP_1NEGATE)
return -1;
else
return opcode + 1 - OP_1;
}
public static int encodeToOpN(int value) {
if(value < -1 || value > 16) {
throw new ProtocolException("encodeToOpN called for " + value + " which we cannot encode in an opcode.");
}
if (value == 0)
return OP_0;
else if (value == -1)
return OP_1NEGATE;
else
return value - 1 + OP_1;
}
}

View file

@ -0,0 +1,71 @@
package com.craigraw.drongo.protocol;
import com.craigraw.drongo.Utils;
import java.io.IOException;
import java.io.OutputStream;
import static com.craigraw.drongo.protocol.ScriptOpCodes.*;
public class ScriptChunk {
/** Operation to be executed. Opcodes are defined in {@link ScriptOpCodes}. */
public final int opcode;
/**
* For push operations, this is the vector to be pushed on the stack. For {@link ScriptOpCodes#OP_0}, the vector is
* empty. Null for non-push operations.
*/
public final byte[] data;
private int startLocationInProgram;
public ScriptChunk(int opcode, byte[] data) {
this(opcode, data, -1);
}
public ScriptChunk(int opcode, byte[] data, int startLocationInProgram) {
this.opcode = opcode;
this.data = data;
this.startLocationInProgram = startLocationInProgram;
}
public boolean equalsOpCode(int opcode) {
return opcode == this.opcode;
}
/**
* If this chunk is a single byte of non-pushdata content (could be OP_RESERVED or some invalid Opcode)
*/
public boolean isOpCode() {
return opcode > OP_PUSHDATA4;
}
public void write(OutputStream stream) throws IOException {
if (isOpCode()) {
if(data != null) throw new IllegalStateException("Data must be null for opcode chunk");
stream.write(opcode);
} else if (data != null) {
if (opcode < OP_PUSHDATA1) {
if(data.length != opcode) throw new IllegalStateException("Data length must equal opcode value");
stream.write(opcode);
} else if (opcode == OP_PUSHDATA1) {
if(data.length > 0xFF) throw new IllegalStateException("Data length must be less than or equal to 256");
stream.write(OP_PUSHDATA1);
stream.write(data.length);
} else if (opcode == OP_PUSHDATA2) {
if(data.length > 0xFFFF) throw new IllegalStateException("Data length must be less than or equal to 65536");
stream.write(OP_PUSHDATA2);
Utils.uint16ToByteStreamLE(data.length, stream);
} else if (opcode == OP_PUSHDATA4) {
if(data.length > Script.MAX_SCRIPT_ELEMENT_SIZE) throw new IllegalStateException("Data length must be less than or equal to " + Script.MAX_SCRIPT_ELEMENT_SIZE);
stream.write(OP_PUSHDATA4);
Utils.uint32ToByteStreamLE(data.length, stream);
} else {
throw new RuntimeException("Unimplemented");
}
stream.write(data);
} else {
stream.write(opcode); // smallNum
}
}
}

View file

@ -0,0 +1,164 @@
/*
* Copyright 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.craigraw.drongo.protocol;
import java.util.Map;
/**
* Various constants that define the assembly-like scripting language that forms part of the Bitcoin protocol.
* See {@link Script} for details. Also provides a method to convert them to a string.
*/
public class ScriptOpCodes {
// push value
public static final int OP_0 = 0x00; // push empty vector
public static final int OP_FALSE = OP_0;
public static final int OP_PUSHDATA1 = 0x4c;
public static final int OP_PUSHDATA2 = 0x4d;
public static final int OP_PUSHDATA4 = 0x4e;
public static final int OP_1NEGATE = 0x4f;
public static final int OP_RESERVED = 0x50;
public static final int OP_1 = 0x51;
public static final int OP_TRUE = OP_1;
public static final int OP_2 = 0x52;
public static final int OP_3 = 0x53;
public static final int OP_4 = 0x54;
public static final int OP_5 = 0x55;
public static final int OP_6 = 0x56;
public static final int OP_7 = 0x57;
public static final int OP_8 = 0x58;
public static final int OP_9 = 0x59;
public static final int OP_10 = 0x5a;
public static final int OP_11 = 0x5b;
public static final int OP_12 = 0x5c;
public static final int OP_13 = 0x5d;
public static final int OP_14 = 0x5e;
public static final int OP_15 = 0x5f;
public static final int OP_16 = 0x60;
// control
public static final int OP_NOP = 0x61;
public static final int OP_VER = 0x62;
public static final int OP_IF = 0x63;
public static final int OP_NOTIF = 0x64;
public static final int OP_VERIF = 0x65;
public static final int OP_VERNOTIF = 0x66;
public static final int OP_ELSE = 0x67;
public static final int OP_ENDIF = 0x68;
public static final int OP_VERIFY = 0x69;
public static final int OP_RETURN = 0x6a;
// stack ops
public static final int OP_TOALTSTACK = 0x6b;
public static final int OP_FROMALTSTACK = 0x6c;
public static final int OP_2DROP = 0x6d;
public static final int OP_2DUP = 0x6e;
public static final int OP_3DUP = 0x6f;
public static final int OP_2OVER = 0x70;
public static final int OP_2ROT = 0x71;
public static final int OP_2SWAP = 0x72;
public static final int OP_IFDUP = 0x73;
public static final int OP_DEPTH = 0x74;
public static final int OP_DROP = 0x75;
public static final int OP_DUP = 0x76;
public static final int OP_NIP = 0x77;
public static final int OP_OVER = 0x78;
public static final int OP_PICK = 0x79;
public static final int OP_ROLL = 0x7a;
public static final int OP_ROT = 0x7b;
public static final int OP_SWAP = 0x7c;
public static final int OP_TUCK = 0x7d;
// splice ops
public static final int OP_CAT = 0x7e;
public static final int OP_SUBSTR = 0x7f;
public static final int OP_LEFT = 0x80;
public static final int OP_RIGHT = 0x81;
public static final int OP_SIZE = 0x82;
// bit logic
public static final int OP_INVERT = 0x83;
public static final int OP_AND = 0x84;
public static final int OP_OR = 0x85;
public static final int OP_XOR = 0x86;
public static final int OP_EQUAL = 0x87;
public static final int OP_EQUALVERIFY = 0x88;
public static final int OP_RESERVED1 = 0x89;
public static final int OP_RESERVED2 = 0x8a;
// numeric
public static final int OP_1ADD = 0x8b;
public static final int OP_1SUB = 0x8c;
public static final int OP_2MUL = 0x8d;
public static final int OP_2DIV = 0x8e;
public static final int OP_NEGATE = 0x8f;
public static final int OP_ABS = 0x90;
public static final int OP_NOT = 0x91;
public static final int OP_0NOTEQUAL = 0x92;
public static final int OP_ADD = 0x93;
public static final int OP_SUB = 0x94;
public static final int OP_MUL = 0x95;
public static final int OP_DIV = 0x96;
public static final int OP_MOD = 0x97;
public static final int OP_LSHIFT = 0x98;
public static final int OP_RSHIFT = 0x99;
public static final int OP_BOOLAND = 0x9a;
public static final int OP_BOOLOR = 0x9b;
public static final int OP_NUMEQUAL = 0x9c;
public static final int OP_NUMEQUALVERIFY = 0x9d;
public static final int OP_NUMNOTEQUAL = 0x9e;
public static final int OP_LESSTHAN = 0x9f;
public static final int OP_GREATERTHAN = 0xa0;
public static final int OP_LESSTHANOREQUAL = 0xa1;
public static final int OP_GREATERTHANOREQUAL = 0xa2;
public static final int OP_MIN = 0xa3;
public static final int OP_MAX = 0xa4;
public static final int OP_WITHIN = 0xa5;
// crypto
public static final int OP_RIPEMD160 = 0xa6;
public static final int OP_SHA1 = 0xa7;
public static final int OP_SHA256 = 0xa8;
public static final int OP_HASH160 = 0xa9;
public static final int OP_HASH256 = 0xaa;
public static final int OP_CODESEPARATOR = 0xab;
public static final int OP_CHECKSIG = 0xac;
public static final int OP_CHECKSIGVERIFY = 0xad;
public static final int OP_CHECKMULTISIG = 0xae;
public static final int OP_CHECKMULTISIGVERIFY = 0xaf;
// block state
/** Check lock time of the block. Introduced in BIP 65, replacing OP_NOP2 */
public static final int OP_CHECKLOCKTIMEVERIFY = 0xb1;
public static final int OP_CHECKSEQUENCEVERIFY = 0xb2;
// expansion
public static final int OP_NOP1 = 0xb0;
/** Deprecated by BIP 65 */
@Deprecated
public static final int OP_NOP2 = OP_CHECKLOCKTIMEVERIFY;
/** Deprecated by BIP 112 */
@Deprecated
public static final int OP_NOP3 = OP_CHECKSEQUENCEVERIFY;
public static final int OP_NOP4 = 0xb3;
public static final int OP_NOP5 = 0xb4;
public static final int OP_NOP6 = 0xb5;
public static final int OP_NOP7 = 0xb6;
public static final int OP_NOP8 = 0xb7;
public static final int OP_NOP9 = 0xb8;
public static final int OP_NOP10 = 0xb9;
public static final int OP_INVALIDOPCODE = 0xff;
}

View file

@ -0,0 +1,184 @@
package com.craigraw.drongo.protocol;
import com.craigraw.drongo.Utils;
import com.craigraw.drongo.address.Address;
import com.craigraw.drongo.address.P2PKAddress;
import java.util.ArrayList;
import java.util.List;
import static com.craigraw.drongo.protocol.ScriptOpCodes.*;
import static com.craigraw.drongo.protocol.Script.decodeFromOpN;
public class ScriptPattern {
/**
* Returns true if this script is of the form {@code DUP HASH160 <pubkey hash> EQUALVERIFY CHECKSIG}, ie, payment to an
* public key like {@code 2102f3b08938a7f8d2609d567aebc4989eeded6e2e880c058fdf092c5da82c3bc5eeac}.
*/
public static boolean isP2PK(Script script) {
List<ScriptChunk> chunks = script.chunks;
if (chunks.size() != 2)
return false;
if (!chunks.get(0).equalsOpCode(0x21))
return false;
byte[] chunk2data = chunks.get(0).data;
if (chunk2data == null)
return false;
if (chunk2data.length != 33)
return false;
if (!chunks.get(1).equalsOpCode(OP_CHECKSIG))
return false;
return true;
}
/**
* Extract the pubkey from a P2PK scriptPubKey. It's important that the script is in the correct form, so you
* will want to guard calls to this method with {@link #isP2PK(Script)}.
*/
public static byte[] extractPKFromP2PK(Script script) {
return script.chunks.get(0).data;
}
/**
* Returns true if this script is of the form {@code DUP HASH160 <pubkey hash> EQUALVERIFY CHECKSIG}, ie, payment to an
* address like {@code 1VayNert3x1KzbpzMGt2qdqrAThiRovi8}. This form was originally intended for the case where you wish
* to send somebody money with a written code because their node is offline, but over time has become the standard
* way to make payments due to the short and recognizable base58 form addresses come in.
*/
public static boolean isP2PKH(Script script) {
List<ScriptChunk> chunks = script.chunks;
if (chunks.size() != 5)
return false;
if (!chunks.get(0).equalsOpCode(OP_DUP))
return false;
if (!chunks.get(1).equalsOpCode(OP_HASH160))
return false;
byte[] chunk2data = chunks.get(2).data;
if (chunk2data == null)
return false;
if (chunk2data.length != 20)
return false;
if (!chunks.get(3).equalsOpCode(OP_EQUALVERIFY))
return false;
if (!chunks.get(4).equalsOpCode(OP_CHECKSIG))
return false;
return true;
}
/**
* Extract the pubkey hash from a P2PKH scriptPubKey. It's important that the script is in the correct form, so you
* will want to guard calls to this method with {@link #isP2PKH(Script)}.
*/
public static byte[] extractHashFromP2PKH(Script script) {
return script.chunks.get(2).data;
}
/**
* <p>
* Whether or not this is a scriptPubKey representing a P2SH output. In such outputs, the logic that
* controls reclamation is not actually in the output at all. Instead there's just a hash, and it's up to the
* spending input to provide a program matching that hash.
* </p>
* <p>
* P2SH is described by <a href="https://github.com/bitcoin/bips/blob/master/bip-0016.mediawiki">BIP16</a>.
* </p>
*/
public static boolean isP2SH(Script script) {
List<ScriptChunk> chunks = script.chunks;
// We check for the effective serialized form because BIP16 defines a P2SH output using an exact byte
// template, not the logical program structure. Thus you can have two programs that look identical when
// printed out but one is a P2SH script and the other isn't! :(
// We explicitly test that the op code used to load the 20 bytes is 0x14 and not something logically
// equivalent like {@code OP_HASH160 OP_PUSHDATA1 0x14 <20 bytes of script hash> OP_EQUAL}
if (chunks.size() != 3)
return false;
if (!chunks.get(0).equalsOpCode(OP_HASH160))
return false;
ScriptChunk chunk1 = chunks.get(1);
if (chunk1.opcode != 0x14)
return false;
byte[] chunk1data = chunk1.data;
if (chunk1data == null)
return false;
if (chunk1data.length != 20)
return false;
if (!chunks.get(2).equalsOpCode(OP_EQUAL))
return false;
return true;
}
/**
* Returns whether this script matches the format used for multisig outputs:
* {@code [n] [keys...] [m] CHECKMULTISIG}
*/
public static boolean isSentToMultisig(Script script) {
List<ScriptChunk> chunks = script.chunks;
if (chunks.size() < 4) return false;
ScriptChunk chunk = chunks.get(chunks.size() - 1);
// Must end in OP_CHECKMULTISIG[VERIFY].
if (!chunk.isOpCode()) return false;
if (!(chunk.equalsOpCode(OP_CHECKMULTISIG) || chunk.equalsOpCode(OP_CHECKMULTISIGVERIFY))) return false;
try {
// Second to last chunk must be an OP_N opcode and there should be that many data chunks (keys).
ScriptChunk m = chunks.get(chunks.size() - 2);
if (!m.isOpCode()) return false;
int numKeys = decodeFromOpN(m.opcode);
if (numKeys < 1 || chunks.size() != 3 + numKeys) return false;
for (int i = 1; i < chunks.size() - 2; i++) {
if (chunks.get(i).isOpCode()) return false;
}
// First chunk must be an OP_N opcode too.
if (decodeFromOpN(chunks.get(0).opcode) < 1) return false;
} catch (IllegalStateException e) {
return false; // Not an OP_N opcode.
}
return true;
}
public static Address[] extractMultisigAddresses(Script script) {
List<Address> addresses = new ArrayList<>();
List<ScriptChunk> chunks = script.chunks;
for (int i = 1; i < chunks.size() - 2; i++) {
byte[] pubKey = chunks.get(i).data;
addresses.add(new P2PKAddress(pubKey));
}
return addresses.toArray(new Address[addresses.size()]);
}
/**
* Extract the script hash from a P2SH scriptPubKey. It's important that the script is in the correct form, so you
* will want to guard calls to this method with {@link #isP2SH(Script)}.
*/
public static byte[] extractHashFromP2SH(Script script) {
return script.chunks.get(1).data;
}
/**
* Returns true if this script is of the form {@code OP_0 <hash>}. This can either be a P2WPKH or P2WSH scriptPubKey. These
* two script types were introduced with segwit.
*/
public static boolean isP2WH(Script script) {
List<ScriptChunk> chunks = script.chunks;
if (chunks.size() != 2)
return false;
if (!chunks.get(0).equalsOpCode(OP_0))
return false;
byte[] chunk1data = chunks.get(1).data;
if (chunk1data == null)
return false;
if (chunk1data.length != 20 && chunk1data.length != 32)
return false;
return true;
}
/**
* Extract the pubkey hash from a P2WPKH or the script hash from a P2WSH scriptPubKey. It's important that the
* script is in the correct form, so you will want to guard calls to this method with
* {@link #isP2WH(Script)}.
*/
public static byte[] extractHashFromP2WH(Script script) {
return script.chunks.get(1).data;
}
}

View file

@ -0,0 +1,261 @@
/*
* Copyright 2011 Google Inc.
* Copyright 2014 Andreas Schildbach
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.craigraw.drongo.protocol;
import com.craigraw.drongo.Utils;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
public class Sha256Hash implements Comparable<Sha256Hash> {
public static final int LENGTH = 32; // bytes
public static final Sha256Hash ZERO_HASH = wrap(new byte[LENGTH]);
private final byte[] bytes;
private Sha256Hash(byte[] rawHashBytes) {
if(rawHashBytes.length != LENGTH) {
throw new ProtocolException();
}
this.bytes = rawHashBytes;
}
/**
* Creates a new instance that wraps the given hash value.
*
* @param rawHashBytes the raw hash bytes to wrap
* @return a new instance
* @throws IllegalArgumentException if the given array length is not exactly 32
*/
public static Sha256Hash wrap(byte[] rawHashBytes) {
return new Sha256Hash(rawHashBytes);
}
/**
* Creates a new instance that wraps the given hash value (represented as a hex string).
*
* @param hexString a hash value represented as a hex string
* @return a new instance
* @throws IllegalArgumentException if the given string is not a valid
* hex string, or if it does not represent exactly 32 bytes
*/
public static Sha256Hash wrap(String hexString) {
return wrap(Utils.hexToBytes(hexString));
}
/**
* Creates a new instance that wraps the given hash value, but with byte order reversed.
*
* @param rawHashBytes the raw hash bytes to wrap
* @return a new instance
* @throws IllegalArgumentException if the given array length is not exactly 32
*/
public static Sha256Hash wrapReversed(byte[] rawHashBytes) {
return wrap(Utils.reverseBytes(rawHashBytes));
}
/**
* Creates a new instance containing the calculated (one-time) hash of the given bytes.
*
* @param contents the bytes on which the hash value is calculated
* @return a new instance containing the calculated (one-time) hash
*/
public static Sha256Hash of(byte[] contents) {
return wrap(hash(contents));
}
/**
* Creates a new instance containing the hash of the calculated hash of the given bytes.
*
* @param contents the bytes on which the hash value is calculated
* @return a new instance containing the calculated (two-time) hash
*/
public static Sha256Hash twiceOf(byte[] contents) {
return wrap(hashTwice(contents));
}
/**
* Creates a new instance containing the hash of the calculated hash of the given bytes.
*
* @param content1 first bytes on which the hash value is calculated
* @param content2 second bytes on which the hash value is calculated
* @return a new instance containing the calculated (two-time) hash
*/
public static Sha256Hash twiceOf(byte[] content1, byte[] content2) {
return wrap(hashTwice(content1, content2));
}
/**
* Returns a new SHA-256 MessageDigest instance.
*
* This is a convenience method which wraps the checked
* exception that can never occur with a RuntimeException.
*
* @return a new SHA-256 MessageDigest instance
*/
public static MessageDigest newDigest() {
try {
return MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e); // Can't happen.
}
}
/**
* Calculates the SHA-256 hash of the given bytes.
*
* @param input the bytes to hash
* @return the hash (in big-endian order)
*/
public static byte[] hash(byte[] input) {
return hash(input, 0, input.length);
}
/**
* Calculates the SHA-256 hash of the given byte range.
*
* @param input the array containing the bytes to hash
* @param offset the offset within the array of the bytes to hash
* @param length the number of bytes to hash
* @return the hash (in big-endian order)
*/
public static byte[] hash(byte[] input, int offset, int length) {
MessageDigest digest = newDigest();
digest.update(input, offset, length);
return digest.digest();
}
/**
* Calculates the SHA-256 hash of the given bytes,
* and then hashes the resulting hash again.
*
* @param input the bytes to hash
* @return the double-hash (in big-endian order)
*/
public static byte[] hashTwice(byte[] input) {
return hashTwice(input, 0, input.length);
}
/**
* Calculates the hash of hash on the given chunks of bytes. This is equivalent to concatenating the two
* chunks and then passing the result to {@link #hashTwice(byte[])}.
*/
public static byte[] hashTwice(byte[] input1, byte[] input2) {
MessageDigest digest = newDigest();
digest.update(input1);
digest.update(input2);
return digest.digest(digest.digest());
}
/**
* Calculates the SHA-256 hash of the given byte range,
* and then hashes the resulting hash again.
*
* @param input the array containing the bytes to hash
* @param offset the offset within the array of the bytes to hash
* @param length the number of bytes to hash
* @return the double-hash (in big-endian order)
*/
public static byte[] hashTwice(byte[] input, int offset, int length) {
MessageDigest digest = newDigest();
digest.update(input, offset, length);
return digest.digest(digest.digest());
}
/**
* Calculates the hash of hash on the given byte ranges. This is equivalent to
* concatenating the two ranges and then passing the result to {@link #hashTwice(byte[])}.
*/
public static byte[] hashTwice(byte[] input1, int offset1, int length1,
byte[] input2, int offset2, int length2) {
MessageDigest digest = newDigest();
digest.update(input1, offset1, length1);
digest.update(input2, offset2, length2);
return digest.digest(digest.digest());
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
return Arrays.equals(bytes, ((Sha256Hash)o).bytes);
}
/**
* Returns the last four bytes of the wrapped hash. This should be unique enough to be a suitable hash code even for
* blocks, where the goal is to try and get the first bytes to be zeros (i.e. the value as a big integer lower
* than the target value).
*/
@Override
public int hashCode() {
// Use the last 4 bytes, not the first 4 which are often zeros in Bitcoin.
return fromBytes(bytes[LENGTH - 4], bytes[LENGTH - 3], bytes[LENGTH - 2], bytes[LENGTH - 1]);
}
/**
* Returns the {@code int} value whose byte representation is the given 4 bytes, in big-endian
* order; equivalent to {@code Ints.fromByteArray(new byte[] {b1, b2, b3, b4})}.
*
* @since 7.0
*/
public static int fromBytes(byte b1, byte b2, byte b3, byte b4) {
return b1 << 24 | (b2 & 0xFF) << 16 | (b3 & 0xFF) << 8 | (b4 & 0xFF);
}
@Override
public String toString() {
return Utils.bytesToHex(bytes);
}
/**
* Returns the bytes interpreted as a positive integer.
*/
public BigInteger toBigInteger() {
return new BigInteger(1, bytes);
}
/**
* Returns the internal byte array, without defensively copying. Therefore do NOT modify the returned array.
*/
public byte[] getBytes() {
return bytes;
}
/**
* Returns a reversed copy of the internal byte array.
*/
public byte[] getReversedBytes() {
return Utils.reverseBytes(bytes);
}
@Override
public int compareTo(final Sha256Hash other) {
for (int i = LENGTH - 1; i >= 0; i--) {
final int thisByte = this.bytes[i] & 0xff;
final int otherByte = other.bytes[i] & 0xff;
if (thisByte > otherByte)
return 1;
if (thisByte < otherByte)
return -1;
}
return 0;
}
}

View file

@ -0,0 +1,188 @@
package com.craigraw.drongo.protocol;
import com.craigraw.drongo.Utils;
import com.craigraw.drongo.address.Address;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static com.craigraw.drongo.Utils.uint32ToByteStreamLE;
public class Transaction extends TransactionPart {
private long version;
private long lockTime;
private Sha256Hash cachedTxId;
private Sha256Hash cachedWTxId;
private ArrayList<TransactionInput> inputs;
private ArrayList<TransactionOutput> outputs;
public Transaction(byte[] rawtx) {
super(rawtx, 0);
}
public Sha256Hash getTxId() {
if (cachedTxId == null) {
if (!hasWitnesses() && cachedWTxId != null) {
cachedTxId = cachedWTxId;
} else {
ByteArrayOutputStream stream = new UnsafeByteArrayOutputStream(length < 32 ? 32 : length + 32);
try {
bitcoinSerializeToStream(stream, false);
} catch (IOException e) {
throw new RuntimeException(e); // cannot happen
}
cachedTxId = Sha256Hash.wrapReversed(Sha256Hash.hashTwice(stream.toByteArray()));
}
}
return cachedTxId;
}
public Sha256Hash getWTxId() {
if (cachedWTxId == null) {
if (!hasWitnesses() && cachedTxId != null) {
cachedWTxId = cachedTxId;
} else {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
bitcoinSerializeToStream(baos, hasWitnesses());
} catch (IOException e) {
throw new RuntimeException(e); // cannot happen
}
cachedWTxId = Sha256Hash.wrapReversed(Sha256Hash.hashTwice(baos.toByteArray()));
}
}
return cachedWTxId;
}
public boolean hasWitnesses() {
for (TransactionInput in : inputs)
if (in.hasWitness())
return true;
return false;
}
protected void bitcoinSerializeToStream(OutputStream stream) throws IOException {
boolean useSegwit = hasWitnesses();
bitcoinSerializeToStream(stream, useSegwit);
}
/**
* Serialize according to <a href="https://github.com/bitcoin/bips/blob/master/bip-0144.mediawiki">BIP144</a> or the
* <a href="https://en.bitcoin.it/wiki/Protocol_documentation#tx">classic format</a>, depending on if segwit is
* desired.
*/
protected void bitcoinSerializeToStream(OutputStream stream, boolean useSegwit) throws IOException {
// version
uint32ToByteStreamLE(version, stream);
// marker, flag
if (useSegwit) {
stream.write(0);
stream.write(1);
}
// txin_count, txins
stream.write(new VarInt(inputs.size()).encode());
for (TransactionInput in : inputs)
in.bitcoinSerialize(stream);
// txout_count, txouts
stream.write(new VarInt(outputs.size()).encode());
for (TransactionOutput out : outputs)
out.bitcoinSerialize(stream);
// script_witnisses
if (useSegwit) {
for (TransactionInput in : inputs) {
in.getWitness().bitcoinSerializeToStream(stream);
}
}
// lock_time
uint32ToByteStreamLE(lockTime, stream);
}
/**
* Deserialize according to <a href="https://github.com/bitcoin/bips/blob/master/bip-0144.mediawiki">BIP144</a> or
* the <a href="https://en.bitcoin.it/wiki/Protocol_documentation#tx">classic format</a>, depending on if the
* transaction is segwit or not.
*/
public void parse() {
// version
version = readUint32();
// peek at marker
byte marker = rawtx[cursor];
boolean useSegwit = marker == 0;
// marker, flag
if (useSegwit) {
readBytes(2);
}
// txin_count, txins
parseInputs();
// txout_count, txouts
parseOutputs();
// script_witnesses
if (useSegwit)
parseWitnesses();
// lock_time
lockTime = readUint32();
length = cursor - offset;
}
private void parseInputs() {
long numInputs = readVarInt();
inputs = new ArrayList<>(Math.min((int) numInputs, Utils.MAX_INITIAL_ARRAY_LENGTH));
for (long i = 0; i < numInputs; i++) {
TransactionInput input = new TransactionInput(this, rawtx, cursor);
inputs.add(input);
long scriptLen = readVarInt(TransactionOutPoint.MESSAGE_LENGTH);
cursor += scriptLen + 4;
}
}
private void parseOutputs() {
long numOutputs = readVarInt();
outputs = new ArrayList<>(Math.min((int) numOutputs, Utils.MAX_INITIAL_ARRAY_LENGTH));
for (long i = 0; i < numOutputs; i++) {
TransactionOutput output = new TransactionOutput(this, rawtx, cursor);
outputs.add(output);
long scriptLen = readVarInt(8);
cursor += scriptLen;
}
}
private void parseWitnesses() {
int numWitnesses = inputs.size();
for (int i = 0; i < numWitnesses; i++) {
long pushCount = readVarInt();
TransactionWitness witness = new TransactionWitness((int) pushCount);
inputs.get(i).setWitness(witness);
for (int y = 0; y < pushCount; y++) {
long pushSize = readVarInt();
byte[] push = readBytes((int) pushSize);
witness.setPush(y, push);
}
}
}
/** Returns an unmodifiable view of all inputs. */
public List<TransactionInput> getInputs() {
return Collections.unmodifiableList(inputs);
}
/** Returns an unmodifiable view of all outputs. */
public List<TransactionOutput> getOutputs() {
return Collections.unmodifiableList(outputs);
}
public static final void main(String[] args) {
String hex = "0100000001e0ea4cd2f1307820d5f33e61aa6b636d8ff94fa7e3b1913f058fb1c8a765fde0340000006a47304402201aa0955638da2902ba972100816d21bde55d0415b98064b7fa511ffefa41397702203f9c93e27557b5b04187784e79f2c1eb74a3202a73085ddfb4509069b90cbbed0121023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ffffffff3510270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac2a91a401000000001976a9141f924ac57c8e44cfbf860fbe0a3ea072b5fb8d0f88ac00000000";
byte[] transactionBytes = Utils.hexToBytes(hex);
Transaction transaction = new Transaction(transactionBytes);
Address[] addresses = transaction.getOutputs().get(3).getScript().getToAddresses();
System.out.println(addresses[0]);
}
}

View file

@ -0,0 +1,85 @@
package com.craigraw.drongo.protocol;
import com.craigraw.drongo.Utils;
import java.io.IOException;
import java.io.OutputStream;
public class TransactionInput extends TransactionPart {
// Allows for altering transactions after they were broadcast. Values below NO_SEQUENCE-1 mean it can be altered.
private long sequence;
// Data needed to connect to the output of the transaction we're gathering coins from.
private TransactionOutPoint outpoint;
private byte[] scriptBytes;
private Script script;
private TransactionWitness witness;
public TransactionInput(Transaction transaction, byte[] rawtx, int offset) {
super(rawtx, offset);
setParent(transaction);
}
protected void parse() throws ProtocolException {
outpoint = new TransactionOutPoint(rawtx, cursor, this);
cursor += outpoint.getMessageSize();
int scriptLen = (int) readVarInt();
length = cursor - offset + scriptLen + 4;
scriptBytes = readBytes(scriptLen);
sequence = readUint32();
}
public byte[] getScriptBytes() {
return scriptBytes;
}
public Script getScript() {
if(script == null) {
script = new Script(scriptBytes);
}
return script;
}
public TransactionWitness getWitness() {
return witness != null ? witness : TransactionWitness.EMPTY;
}
public void setWitness(TransactionWitness witness) {
this.witness = witness;
}
public boolean hasWitness() {
return witness != null && witness.getPushCount() != 0;
}
public TransactionOutPoint getOutpoint() {
return outpoint;
}
public long getSequenceNumber() {
return sequence;
}
public void setSequenceNumber(long sequence) {
this.sequence = sequence;
}
/**
* Coinbase transactions have special inputs with hashes of zero. If this is such an input, returns true.
*/
public boolean isCoinBase() {
return outpoint.getHash().equals(Sha256Hash.ZERO_HASH) &&
(outpoint.getIndex() & 0xFFFFFFFFL) == 0xFFFFFFFFL; // -1 but all is serialized to the wire as unsigned int.
}
protected void bitcoinSerializeToStream(OutputStream stream) throws IOException {
outpoint.bitcoinSerialize(stream);
stream.write(new VarInt(scriptBytes.length).encode());
stream.write(scriptBytes);
Utils.uint32ToByteStreamLE(sequence, stream);
}
}

View file

@ -0,0 +1,42 @@
package com.craigraw.drongo.protocol;
import com.craigraw.drongo.address.Address;
public class TransactionOutPoint extends TransactionPart {
static final int MESSAGE_LENGTH = 36;
/** Hash of the transaction to which we refer. */
private Sha256Hash hash;
/** Which output of that transaction we are talking about. */
private long index;
private Address[] addresses;
public TransactionOutPoint(byte[] rawtx, int offset, TransactionPart parent) {
super(rawtx, offset);
setParent(parent);
}
protected void parse() throws ProtocolException {
length = MESSAGE_LENGTH;
hash = readHash();
index = readUint32();
}
public Sha256Hash getHash() {
return hash;
}
public long getIndex() {
return index;
}
public Address[] getAddresses() {
return addresses;
}
public void setAddresses(Address[] addresses) {
this.addresses = addresses;
}
}

View file

@ -0,0 +1,65 @@
package com.craigraw.drongo.protocol;
import com.craigraw.drongo.Utils;
import com.craigraw.drongo.address.Address;
import java.io.IOException;
import java.io.OutputStream;
public class TransactionOutput extends TransactionPart {
// The output's value is kept as a native type in order to save class instances.
private long value;
// A transaction output has a script used for authenticating that the redeemer is allowed to spend
// this output.
private byte[] scriptBytes;
private Script script;
private int scriptLen;
private Address[] addresses;
public TransactionOutput(Transaction transaction, byte[] rawtx, int offset) {
super(rawtx, offset);
setParent(transaction);
}
protected void parse() throws ProtocolException {
value = readInt64();
scriptLen = (int) readVarInt();
length = cursor - offset + scriptLen;
scriptBytes = readBytes(scriptLen);
}
protected void bitcoinSerializeToStream(OutputStream stream) throws IOException {
Utils.int64ToByteStreamLE(value, stream);
// TODO: Move script serialization into the Script class, where it belongs.
stream.write(new VarInt(scriptBytes.length).encode());
stream.write(scriptBytes);
}
public byte[] getScriptBytes() {
return scriptBytes;
}
public Script getScript() {
if(script == null) {
script = new Script(scriptBytes);
}
return script;
}
public long getValue() {
return value;
}
public Address[] getAddresses() {
return addresses;
}
public void setAddresses(Address[] addresses) {
this.addresses = addresses;
}
}

View file

@ -0,0 +1,119 @@
package com.craigraw.drongo.protocol;
import com.craigraw.drongo.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.OutputStream;
public abstract class TransactionPart {
private static final Logger log = LoggerFactory.getLogger(TransactionPart.class);
public static final int MAX_SIZE = 0x02000000; // 32MB
public static final int UNKNOWN_LENGTH = Integer.MIN_VALUE;
protected byte[] rawtx;
// The offset is how many bytes into the provided byte array this message payload starts at.
protected int offset;
// The cursor keeps track of where we are in the byte array as we parse it.
// Note that it's relative to the start of the array NOT the start of the message payload.
protected int cursor;
protected TransactionPart parent;
protected int length = UNKNOWN_LENGTH;
public TransactionPart(byte[] rawtx, int offset) {
this.rawtx = rawtx;
this.cursor = this.offset = offset;
parse();
}
protected abstract void parse() throws ProtocolException;
public final void setParent(TransactionPart parent) {
this.parent = parent;
}
/**
* This returns a correct value by parsing the message.
*/
public final int getMessageSize() {
if (length == UNKNOWN_LENGTH) {
throw new ProtocolException();
}
return length;
}
protected long readUint32() throws ProtocolException {
try {
long u = Utils.readUint32(rawtx, cursor);
cursor += 4;
return u;
} catch (ArrayIndexOutOfBoundsException e) {
throw new ProtocolException(e);
}
}
protected long readInt64() throws ProtocolException {
try {
long u = Utils.readInt64(rawtx, cursor);
cursor += 8;
return u;
} catch (ArrayIndexOutOfBoundsException e) {
throw new ProtocolException(e);
}
}
protected byte[] readBytes(int length) throws ProtocolException {
if ((length > MAX_SIZE) || (cursor + length > rawtx.length)) {
throw new ProtocolException("Claimed value length too large: " + length);
}
try {
byte[] b = new byte[length];
System.arraycopy(rawtx, cursor, b, 0, length);
cursor += length;
return b;
} catch (IndexOutOfBoundsException e) {
throw new ProtocolException(e);
}
}
protected long readVarInt() throws ProtocolException {
return readVarInt(0);
}
protected long readVarInt(int offset) throws ProtocolException {
try {
VarInt varint = new VarInt(rawtx, cursor + offset);
cursor += offset + varint.getOriginalSizeInBytes();
return varint.value;
} catch (ArrayIndexOutOfBoundsException e) {
throw new ProtocolException(e);
}
}
protected Sha256Hash readHash() throws ProtocolException {
// We have to flip it around, as it's been read off the wire in little endian.
// Not the most efficient way to do this but the clearest.
return Sha256Hash.wrapReversed(readBytes(32));
}
public final void bitcoinSerialize(OutputStream stream) throws IOException {
// 1st check for cached bytes.
if (rawtx != null && length != UNKNOWN_LENGTH) {
stream.write(rawtx, offset, length);
return;
}
bitcoinSerializeToStream(stream);
}
protected void bitcoinSerializeToStream(OutputStream stream) throws IOException {
log.error("Error: {} class has not implemented bitcoinSerializeToStream method. Generating message with no payload", getClass());
}
}

View file

@ -0,0 +1,38 @@
package com.craigraw.drongo.protocol;
import com.craigraw.drongo.Utils;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
public class TransactionWitness {
public static final TransactionWitness EMPTY = new TransactionWitness(0);
private final List<byte[]> pushes;
public TransactionWitness(int pushCount) {
pushes = new ArrayList<>(Math.min(pushCount, Utils.MAX_INITIAL_ARRAY_LENGTH));
}
public void setPush(int i, byte[] value) {
while (i >= pushes.size()) {
pushes.add(new byte[]{});
}
pushes.set(i, value);
}
public int getPushCount() {
return pushes.size();
}
protected void bitcoinSerializeToStream(OutputStream stream) throws IOException {
stream.write(new VarInt(pushes.size()).encode());
for (int i = 0; i < pushes.size(); i++) {
byte[] push = pushes.get(i);
stream.write(new VarInt(push.length).encode());
stream.write(push);
}
}
}

View file

@ -0,0 +1,138 @@
package com.craigraw.drongo.protocol;
/*
* Copyright 2011 Steve Coughlan.
*
* 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.
*/
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
/**
* <p>An unsynchronized implementation of ByteArrayOutputStream that will return the backing byte array if its length == size().
* This avoids unneeded array copy where the BOS is simply being used to extract a byte array of known length from a
* 'serialized to stream' method.</p>
*
* <p>Unless the final length can be accurately predicted the only performance this will yield is due to unsynchronized
* methods.</p>
*
* @author git
*/
public class UnsafeByteArrayOutputStream extends ByteArrayOutputStream {
public UnsafeByteArrayOutputStream() {
super(32);
}
public UnsafeByteArrayOutputStream(int size) {
super(size);
}
/**
* Writes the specified byte to this byte array output stream.
*
* @param b the byte to be written.
*/
@Override
public void write(int b) {
int newcount = count + 1;
if (newcount > buf.length) {
buf = copyOf(buf, Math.max(buf.length << 1, newcount));
}
buf[count] = (byte) b;
count = newcount;
}
/**
* Writes {@code len} bytes from the specified byte array
* starting at offset {@code off} to this byte array output stream.
*
* @param b the data.
* @param off the start offset in the data.
* @param len the number of bytes to write.
*/
@Override
public void write(byte[] b, int off, int len) {
if ((off < 0) || (off > b.length) || (len < 0) ||
((off + len) > b.length) || ((off + len) < 0)) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return;
}
int newcount = count + len;
if (newcount > buf.length) {
buf = copyOf(buf, Math.max(buf.length << 1, newcount));
}
System.arraycopy(b, off, buf, count, len);
count = newcount;
}
/**
* Writes the complete contents of this byte array output stream to
* the specified output stream argument, as if by calling the output
* stream's write method using {@code out.write(buf, 0, count)}.
*
* @param out the output stream to which to write the data.
* @throws IOException if an I/O error occurs.
*/
@Override
public void writeTo(OutputStream out) throws IOException {
out.write(buf, 0, count);
}
/**
* Resets the {@code count} field of this byte array output
* stream to zero, so that all currently accumulated output in the
* output stream is discarded. The output stream can be used again,
* reusing the already allocated buffer space.
*
* @see java.io.ByteArrayInputStream#count
*/
@Override
public void reset() {
count = 0;
}
/**
* Creates a newly allocated byte array. Its size is the current
* size of this output stream and the valid contents of the buffer
* have been copied into it.
*
* @return the current contents of this output stream, as a byte array.
* @see java.io.ByteArrayOutputStream#size()
*/
@Override
public byte toByteArray()[] {
return count == buf.length ? buf : copyOf(buf, count);
}
/**
* Returns the current size of the buffer.
*
* @return the value of the {@code count} field, which is the number
* of valid bytes in this output stream.
* @see java.io.ByteArrayOutputStream#count
*/
@Override
public int size() {
return count;
}
private static byte[] copyOf(byte[] in, int length) {
byte[] out = new byte[length];
System.arraycopy(in, 0, out, 0, Math.min(length, in.length));
return out;
}
}

View file

@ -0,0 +1,117 @@
/*
* Copyright 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.craigraw.drongo.protocol;
import com.craigraw.drongo.Utils;
/**
* A variable-length encoded unsigned integer using Satoshi's encoding (a.k.a. "CompactSize").
*/
public class VarInt {
public final long value;
private final int originallyEncodedSize;
/**
* Constructs a new VarInt with the given unsigned long value.
*
* @param value the unsigned long value (beware widening conversion of negatives!)
*/
public VarInt(long value) {
this.value = value;
originallyEncodedSize = getSizeInBytes();
}
/**
* Constructs a new VarInt with the value parsed from the specified offset of the given buffer.
*
* @param buf the buffer containing the value
* @param offset the offset of the value
*/
public VarInt(byte[] buf, int offset) {
int first = 0xFF & buf[offset];
if (first < 253) {
value = first;
originallyEncodedSize = 1; // 1 data byte (8 bits)
} else if (first == 253) {
value = Utils.readUint16(buf, offset + 1);
originallyEncodedSize = 3; // 1 marker + 2 data bytes (16 bits)
} else if (first == 254) {
value = Utils.readUint32(buf, offset + 1);
originallyEncodedSize = 5; // 1 marker + 4 data bytes (32 bits)
} else {
value = Utils.readInt64(buf, offset + 1);
originallyEncodedSize = 9; // 1 marker + 8 data bytes (64 bits)
}
}
/**
* Returns the original number of bytes used to encode the value if it was
* deserialized from a byte array, or the minimum encoded size if it was not.
*/
public int getOriginalSizeInBytes() {
return originallyEncodedSize;
}
/**
* Returns the minimum encoded size of the value.
*/
public final int getSizeInBytes() {
return sizeOf(value);
}
/**
* Returns the minimum encoded size of the given unsigned long value.
*
* @param value the unsigned long value (beware widening conversion of negatives!)
*/
public static int sizeOf(long value) {
// if negative, it's actually a very large unsigned long value
if (value < 0) return 9; // 1 marker + 8 data bytes
if (value < 253) return 1; // 1 data byte
if (value <= 0xFFFFL) return 3; // 1 marker + 2 data bytes
if (value <= 0xFFFFFFFFL) return 5; // 1 marker + 4 data bytes
return 9; // 1 marker + 8 data bytes
}
/**
* Encodes the value into its minimal representation.
*
* @return the minimal encoded bytes of the value
*/
public byte[] encode() {
byte[] bytes;
switch (sizeOf(value)) {
case 1:
return new byte[]{(byte) value};
case 3:
bytes = new byte[3];
bytes[0] = (byte) 253;
Utils.uint16ToByteArrayLE((int) value, bytes, 1);
return bytes;
case 5:
bytes = new byte[5];
bytes[0] = (byte) 254;
Utils.uint32ToByteArrayLE(value, bytes, 1);
return bytes;
default:
bytes = new byte[9];
bytes[0] = (byte) 255;
Utils.int64ToByteArrayLE(value, bytes, 1);
return bytes;
}
}
}

View file

@ -0,0 +1,128 @@
package com.craigraw.drongo.rpc;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.*;
import java.nio.charset.Charset;
import java.util.*;
public class BitcoinJSONRPCClient {
private static final Logger log = LoggerFactory.getLogger(BitcoinJSONRPCClient.class);
public static final Charset QUERY_CHARSET = Charset.forName("ISO8859-1");
public static final String RESPONSE_ID = "drongo";
public final URL rpcURL;
private final URL noAuthURL;
private final String authStr;
public BitcoinJSONRPCClient(String host, String port, String user, String password) {
this.rpcURL = getConnectUrl(host, port, user, password);
try {
this.noAuthURL = new URI(rpcURL.getProtocol(), null, rpcURL.getHost(), rpcURL.getPort(), rpcURL.getPath(), rpcURL.getQuery(), null).toURL();
} catch (MalformedURLException | URISyntaxException ex) {
throw new IllegalArgumentException(rpcURL.toString(), ex);
}
this.authStr = rpcURL.getUserInfo() == null ? null : new String(Base64.getEncoder().encode(rpcURL.getUserInfo().getBytes(QUERY_CHARSET)), QUERY_CHARSET);
}
private URL getConnectUrl(String host, String port, String user, String password) {
try {
return new URL("http://" + user + ':' + password + "@" + host + ":" + (port == null ? "8332" : port) + "/");
} catch (MalformedURLException e) {
throw new IllegalArgumentException("Invalid RPC connection details", e);
}
}
public Object query(String method, Object... o) throws BitcoinRPCException {
HttpURLConnection conn;
try {
conn = (HttpURLConnection) noAuthURL.openConnection();
conn.setDoOutput(true);
conn.setDoInput(true);
conn.setRequestProperty("Authorization", "Basic " + authStr);
byte[] r = prepareRequest(method, o);
log.debug("Bitcoin JSON-RPC request: " + new String(r, QUERY_CHARSET));
conn.getOutputStream().write(r);
conn.getOutputStream().close();
int responseCode = conn.getResponseCode();
if (responseCode != 200) {
InputStream errorStream = conn.getErrorStream();
throw new BitcoinRPCException(method,
Arrays.deepToString(o),
responseCode,
conn.getResponseMessage(),
errorStream == null ? null : new String(loadStream(errorStream, true)));
}
return loadResponse(conn.getInputStream(), RESPONSE_ID, true);
} catch (IOException ex) {
throw new BitcoinRPCException(method, Arrays.deepToString(o), ex);
}
}
protected byte[] prepareRequest(final String method, final Object... params) {
return JSONObject.toJSONString(new LinkedHashMap<String, Object>() {
{
put("method", method);
put("params", Arrays.asList(params));
put("id", RESPONSE_ID);
put("jsonrpc", "1.0");
}
}).getBytes(QUERY_CHARSET);
}
private static byte[] loadStream(InputStream in, boolean close) throws IOException {
ByteArrayOutputStream o = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
for (;;) {
int nr = in.read(buffer);
if (nr == -1)
break;
if (nr == 0)
throw new IOException("Read timed out");
o.write(buffer, 0, nr);
}
return o.toByteArray();
}
@SuppressWarnings("rawtypes")
public Object loadResponse(InputStream in, Object expectedID, boolean close) throws IOException, BitcoinRPCException {
try {
String r = new String(loadStream(in, close), QUERY_CHARSET);
log.debug("Bitcoin JSON-RPC response: " + r);
try {
JSONParser jsonParser = new JSONParser();
Map response = (Map) jsonParser.parse(r);
if (!expectedID.equals(response.get("id")))
throw new BitcoinRPCException("Wrong response ID (expected: " + String.valueOf(expectedID) + ", response: " + response.get("id") + ")");
if (response.get("error") != null)
throw new BitcoinRPCException(new BitcoinRPCError((Map)response.get("error")));
return response.get("result");
} catch (ClassCastException | ParseException ex) {
throw new BitcoinRPCException("Invalid server response format (data: \"" + r + "\")");
}
} finally {
if (close)
in.close();
}
}
public String getRawTransaction(String txId) throws BitcoinRPCException {
return (String) query("getrawtransaction", txId);
}
}

View file

@ -0,0 +1,23 @@
package com.craigraw.drongo.rpc;
import java.util.Map;
public class BitcoinRPCError {
private int code;
private String message;
@SuppressWarnings({ "rawtypes" })
public BitcoinRPCError(Map errorMap) {
Number n = (Number) errorMap.get("code");
this.code = n != null ? n.intValue() : 0;
this.message = (String) errorMap.get("message");
}
public int getCode() {
return code;
}
public String getMessage() {
return message;
}
}

View file

@ -0,0 +1,115 @@
package com.craigraw.drongo.rpc;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Map;
public class BitcoinRPCException extends RuntimeException {
private static final Logger log = LoggerFactory.getLogger(BitcoinJSONRPCClient.class);
private String rpcMethod;
private String rpcParams;
private int responseCode;
private String responseMessage;
private String response;
private BitcoinRPCError rpcError;
/**
* Creates a new instance of <code>BitcoinRPCException</code> with response
* detail.
*
* @param method the rpc method called
* @param params the parameters sent
* @param responseCode the HTTP code received
* @param responseMessage the HTTP response message
* @param response the error stream received
*/
@SuppressWarnings("rawtypes")
public BitcoinRPCException(String method,
String params,
int responseCode,
String responseMessage,
String response) {
super("RPC Query Failed (method: " + method + ", params: " + params + ", response code: " + responseCode + " responseMessage " + responseMessage + ", response: " + response);
this.rpcMethod = method;
this.rpcParams = params;
this.responseCode = responseCode;
this.responseMessage = responseMessage;
this.response = response;
if ( responseCode == 500 ) {
// Bitcoind application error when handle the request
// extract code/message for callers to handle
try {
JSONParser jsonParser = new JSONParser();
Map error = (Map) ((Map)jsonParser.parse(response)).get("error");
if ( error != null ) {
rpcError = new BitcoinRPCError(error);
}
} catch(ParseException e) {
log.error("Could not parse bitcoind error", e);
}
}
}
public BitcoinRPCException(String method, String params, Throwable cause) {
super("RPC Query Failed (method: " + method + ", params: " + params + ")", cause);
this.rpcMethod = method;
this.rpcParams = params;
}
/**
* Constructs an instance of <code>BitcoinRPCException</code> with the
* specified detail message.
*
* @param msg the detail message.
*/
public BitcoinRPCException(String msg) {
super(msg);
}
public BitcoinRPCException(BitcoinRPCError error) {
super(error.getMessage());
this.rpcError = error;
}
public BitcoinRPCException(String message, Throwable cause) {
super(message, cause);
}
public int getResponseCode() {
return responseCode;
}
public String getRpcMethod() {
return rpcMethod;
}
public String getRpcParams() {
return rpcParams;
}
/**
* @return the HTTP response message
*/
public String getResponseMessage() {
return responseMessage;
}
/**
* @return response message from bitcored
*/
public String getResponse() {
return this.response;
}
/**
* @return response message from bitcored
*/
public BitcoinRPCError getRPCError() {
return this.rpcError;
}
}

View file

@ -0,0 +1,10 @@
log4j.rootLogger=INFO, stdout, file
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=[%-5p] %d %c - %m%n
log4j.appender.file=org.apache.log4j.FileAppender
log4j.appender.file.File=sentinel.log
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=[%-5p] %d %c - %m%n

View file

@ -0,0 +1,43 @@
package com.craigraw.drongo;
import org.junit.Assert;
import org.junit.Test;
public class OutputDescriptorTest {
@Test
public void electrumP2PKH() {
OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("xpub661MyMwAqRbcFT5HwyRoP5hebbeRDvy2RGDTH2uxFyDPaf5FLtu4njuishddViQxTABZKzoWKuwpy6MsgfPvTw9pKnRGDL5eBxDej9kF54Z");
Assert.assertEquals("pkh(xpub6BemYiVEULcbpkxh3wp6KUzfzGPFL7JNcxbfQcXxGnJ6sPugTkR69neX8RT9iXdMHFV1FCge72a21WpoHjgoeBTcZju3JKyFf9DztGT2FhE/0/*)", descriptor.toString());
}
@Test
public void iancolemanP2PKH() {
OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("xpub6EEznxrqoN5HUXfD3QC3B8Vjw8Lj9UnRj17uTzNaBnEYN5xgwe6Un46Z443sSTBP2bzLZuDzygkdD1FtVWSexFmg4yAuCTxE2HxXFtz541z");
Assert.assertEquals("pkh(xpub6EEznxrqoN5HUXfD3QC3B8Vjw8Lj9UnRj17uTzNaBnEYN5xgwe6Un46Z443sSTBP2bzLZuDzygkdD1FtVWSexFmg4yAuCTxE2HxXFtz541z/*)", descriptor.toString());
}
@Test
public void electrumP2WPKH() {
OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("zpub6njbcfTHEfK4U96Z8dBaTULdb1LGWMtj73yYZ76kfmE9nuf3KhNSsXfzDefz5KV6TreWjnQbgvnSmSttudzTugesV2HFunYu7gWYJUD4eoR");
Assert.assertEquals("wpkh(xpub6CqLiu9VMua6V5yFXtXrfZgJqWsG2a8dQdBuk34KFdCCYXvCtx41CmWugPJVZNzBXyHCWy8uHgVUMpePCxh2S3VXueYG8dWLDh49dQ9MJGu/0/*)", descriptor.toString());
}
@Test
public void iancolemanP2SHP2WPKH() {
OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("ypub6Zken22QbjfomRUXki5v4ndP6T1DEtaBhGGZBvR4ocoooM44dFmnF8DyFmvcK76TKnuvdFfaPnicVvTAPdqEcbuEfKEqfnRoUjSkTB4u1os");
Assert.assertEquals("sh(wpkh(xpub6EvPUMMVT48Kv8HQvMJHrhXsvUrmJGagn9kLQXXBRcRvkFEqNbcDd4ZqEZy2KCSXv9o7sn51w8N4cdqbfwRDpNDdnyYR5scKD1P74ZAKbGm/*))", descriptor.toString());
}
@Test
public void bip84P2WPKH() {
OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs");
Assert.assertEquals("wpkh(xpub6CatWdiZiodmUeTDp8LT5or8nmbKNcuyvz7WyksVFkKB4RHwCD3XyuvPEbvqAQY3rAPshWcMLoP2fMFMKHPJ4ZeZXYVUhLv1VMrjPC7PW6V/0/*)", descriptor.toString());
}
@Test
public void redditP2SHP2WPKH() {
OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("ypub6XiW9nhToS1gjVsFKzgmtWZuqo6V1YY7xaCns37aR3oYhFyAsTehAqV1iW2UCNtgWFQFkz3aNSZZbkfe5d1tD8MzjZuFJQn2XnczsxtjoXr");
Assert.assertEquals("sh(wpkh(xpub6CtEr82YekUCtCg8Vdu9gRUQfpx34vYd3Tga5eDh33RfeA9wcoV8YmpshJ4tCUEm6cHT1WT1unD1iU45MvbsQtgPsECpiVxYG4ZMVKEKqGP/0/*))", descriptor.toString());
}
}

View file

@ -0,0 +1,58 @@
package com.craigraw.drongo;
import org.junit.Assert;
import org.junit.Test;
public class WatchWalletTest {
@Test
public void electrumP2PKH() {
WatchWallet wallet = new WatchWallet("", "xpub661MyMwAqRbcFT5HwyRoP5hebbeRDvy2RGDTH2uxFyDPaf5FLtu4njuishddViQxTABZKzoWKuwpy6MsgfPvTw9pKnRGDL5eBxDej9kF54Z");
Assert.assertEquals("1QEjP9f7KRtJobfwmRuykpLjaR5QchGo8q", wallet.getReceivingAddress(0).toString());
Assert.assertEquals("17kCok3XAUHyL6kjzBF44e1YuzMmRXPuu5", wallet.getReceivingAddress(1).toString());
Assert.assertEquals("1Dh3Lofy2cFdEQ2rk4Eq6fbPeQQ63pDdRN", wallet.getChangeAddress(0).toString());
}
@Test
public void iancolemanP2PKH() {
WatchWallet wallet = new WatchWallet("", "xpub6EEznxrqoN5HUXfD3QC3B8Vjw8Lj9UnRj17uTzNaBnEYN5xgwe6Un46Z443sSTBP2bzLZuDzygkdD1FtVWSexFmg4yAuCTxE2HxXFtz541z");
Assert.assertEquals("179cMrkiyx6zD2E1sqBAQLg1SQPAS5vjQW", wallet.getReceivingAddress(0).toString());
Assert.assertEquals("1GdWCzdt5oDYh5n1qeZQCxg5rQKVTuTMJg", wallet.getReceivingAddress(1).toString());
}
@Test
public void electrumP2WPKH() {
WatchWallet wallet = new WatchWallet("", "zpub6njbcfTHEfK4U96Z8dBaTULdb1LGWMtj73yYZ76kfmE9nuf3KhNSsXfzDefz5KV6TreWjnQbgvnSmSttudzTugesV2HFunYu7gWYJUD4eoR");
Assert.assertEquals("bc1q4s5v0u9qmmcp25mnr3mfzhyftjzw8mccqawmwf", wallet.getReceivingAddress(0).toString());
Assert.assertEquals("bc1qffy90ge6wljh53t07q4al2pgsmuqgy48wrk8wq", wallet.getReceivingAddress(1).toString());
Assert.assertEquals("bc1q87fg9yjxratt4hemjn0m4re97n2p39ssq5xhv4", wallet.getChangeAddress(0).toString());
}
@Test
public void iancolemanP2SHP2WPKH() {
WatchWallet wallet = new WatchWallet("", "ypub6Zken22QbjfomRUXki5v4ndP6T1DEtaBhGGZBvR4ocoooM44dFmnF8DyFmvcK76TKnuvdFfaPnicVvTAPdqEcbuEfKEqfnRoUjSkTB4u1os");
Assert.assertEquals("34SgiHwNwJt3nYCVUQcgJWhefVRBZ4aSHf", wallet.getReceivingAddress(0).toString());
Assert.assertEquals("3MgPnbF6UYM3FBhZWXoL2ebLPEa3zCCXLh", wallet.getReceivingAddress(1).toString());
}
@Test
public void bip84P2WPKH() {
WatchWallet wallet = new WatchWallet("", "zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs");
Assert.assertEquals("bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu", wallet.getReceivingAddress(0).toString());
Assert.assertEquals("bc1qnjg0jd8228aq7egyzacy8cys3knf9xvrerkf9g", wallet.getReceivingAddress(1).toString());
Assert.assertEquals("bc1q8c6fshw2dlwun7ekn9qwf37cu2rn755upcp6el", wallet.getChangeAddress(0).toString());
}
@Test
public void redditP2SHP2WPKH() {
WatchWallet wallet = new WatchWallet("", "ypub6XiW9nhToS1gjVsFKzgmtWZuqo6V1YY7xaCns37aR3oYhFyAsTehAqV1iW2UCNtgWFQFkz3aNSZZbkfe5d1tD8MzjZuFJQn2XnczsxtjoXr");
Assert.assertEquals("34TBBnwqv338BT6BVnTKqziFq8HWY6BNbw", wallet.getReceivingAddress(0).toString());
Assert.assertEquals("35Jhf9LGCpb1ihJjWH7uLZ8othr1diuspS", wallet.getChangeAddress(0).toString());
}
}