commit 911a54347d3ed72834e468c5b4ac32eb6b8502bd Author: Craig Raw Date: Fri Mar 15 20:15:28 2019 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0f93c9f --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.idea +.gradle +*iml +build +/*.properties +out +*.log \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..fc041b6 --- /dev/null +++ b/build.gradle @@ -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' +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..01b8bf6 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..7f68226 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..cccdd3d --- /dev/null +++ b/gradlew @@ -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" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..e95643d --- /dev/null +++ b/gradlew.bat @@ -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 diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..aa346c0 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'drongo' + diff --git a/src/main/java/com/craigraw/drongo/Drongo.java b/src/main/java/com/craigraw/drongo/Drongo.java new file mode 100644 index 0000000..e48f434 --- /dev/null +++ b/src/main/java/com/craigraw/drongo/Drongo.java @@ -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 watchWallets; + private String[] notifyRecipients; + + public Drongo(String nodeZmqAddress, Map nodeRpc, List 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; + } +} diff --git a/src/main/java/com/craigraw/drongo/Main.java b/src/main/java/com/craigraw/drongo/Main.java new file mode 100644 index 0000000..aab68e6 --- /dev/null +++ b/src/main/java/com/craigraw/drongo/Main.java @@ -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 rpcConnection = new LinkedHashMap() { + { + 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 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; + } +} diff --git a/src/main/java/com/craigraw/drongo/OutputDescriptor.java b/src/main/java/com/craigraw/drongo/OutputDescriptor.java new file mode 100644 index 0000000..11c756d --- /dev/null +++ b/src/main/java/com/craigraw/drongo/OutputDescriptor.java @@ -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 getKeyDerivation() { + return parsePath(keyDerivationPath); + } + + public DeterministicKey getPubKey() { + return pubKey; + } + + public List getChildDerivation() { + return getChildDerivation(0); + } + + public List getChildDerivation(int wildCardReplacement) { + return getChildDerivation(new ChildNumber(0, getPubKey().getChildNumber().isHardened()), childDerivationPath, wildCardReplacement); + } + + public boolean describesMultipleAddresses() { + return childDerivationPath.endsWith("/*"); + } + + public List 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 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 getChildDerivation(ChildNumber firstChild, String derivationPath, int wildCardReplacement) { + List 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 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 parsePath(String path) { + return parsePath(path, 0); + } + + public static List parsePath(String path, int wildcardReplacement) { + String[] parsedNodes = path.replace("M", "").split("/"); + List 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 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(); + } +} diff --git a/src/main/java/com/craigraw/drongo/TransactionTask.java b/src/main/java/com/craigraw/drongo/TransactionTask.java new file mode 100644 index 0000000..e05dd6c --- /dev/null +++ b/src/main/java/com/craigraw/drongo/TransactionTask.java @@ -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 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()); + } +} diff --git a/src/main/java/com/craigraw/drongo/Utils.java b/src/main/java/com/craigraw/drongo/Utils.java new file mode 100644 index 0000000..367ceb4 --- /dev/null +++ b/src/main/java/com/craigraw/drongo/Utils.java @@ -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 path) { + StringJoiner joiner = new StringJoiner("/"); + joiner.add("M"); + for(ChildNumber number : path) { + joiner.add(number.toString()); + } + + return joiner.toString(); + } + + public static List appendChild(List path, ChildNumber childNumber) { + List 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); + } +} diff --git a/src/main/java/com/craigraw/drongo/WatchWallet.java b/src/main/java/com/craigraw/drongo/WatchWallet.java new file mode 100644 index 0000000..c9ad3fe --- /dev/null +++ b/src/main/java/com/craigraw/drongo/WatchWallet.java @@ -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 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 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 changeDerivation = outputDescriptor.getChangeDerivation(index); + Address address = getAddress(changeDerivation); + addresses.put(address.toString(), Utils.formatHDPath(changeDerivation)); + } + } else { + List 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 path) { + DeterministicKey childKey = hierarchy.get(path); + return outputDescriptor.getAddress(childKey); + } +} diff --git a/src/main/java/com/craigraw/drongo/address/Address.java b/src/main/java/com/craigraw/drongo/address/Address.java new file mode 100644 index 0000000..d82441d --- /dev/null +++ b/src/main/java/com/craigraw/drongo/address/Address.java @@ -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(); +} diff --git a/src/main/java/com/craigraw/drongo/address/P2PKAddress.java b/src/main/java/com/craigraw/drongo/address/P2PKAddress.java new file mode 100644 index 0000000..b741fc0 --- /dev/null +++ b/src/main/java/com/craigraw/drongo/address/P2PKAddress.java @@ -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 chunks = new ArrayList<>(); + chunks.add(new ScriptChunk(pubKey.length, pubKey)); + chunks.add(new ScriptChunk(ScriptOpCodes.OP_CHECKSIG, null)); + + return new Script(chunks); + } +} diff --git a/src/main/java/com/craigraw/drongo/address/P2PKHAddress.java b/src/main/java/com/craigraw/drongo/address/P2PKHAddress.java new file mode 100644 index 0000000..d93f120 --- /dev/null +++ b/src/main/java/com/craigraw/drongo/address/P2PKHAddress.java @@ -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 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); + } +} diff --git a/src/main/java/com/craigraw/drongo/address/P2SHAddress.java b/src/main/java/com/craigraw/drongo/address/P2SHAddress.java new file mode 100644 index 0000000..5df38da --- /dev/null +++ b/src/main/java/com/craigraw/drongo/address/P2SHAddress.java @@ -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 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)); + } +} diff --git a/src/main/java/com/craigraw/drongo/address/P2WPKHAddress.java b/src/main/java/com/craigraw/drongo/address/P2WPKHAddress.java new file mode 100644 index 0000000..e1b66b9 --- /dev/null +++ b/src/main/java/com/craigraw/drongo/address/P2WPKHAddress.java @@ -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 chunks = new ArrayList<>(); + chunks.add(new ScriptChunk(Script.encodeToOpN(getVersion()), null)); + chunks.add(new ScriptChunk(pubKeyHash.length, pubKeyHash)); + + return new Script(chunks); + } +} diff --git a/src/main/java/com/craigraw/drongo/crypto/ChildNumber.java b/src/main/java/com/craigraw/drongo/crypto/ChildNumber.java new file mode 100644 index 0000000..da13318 --- /dev/null +++ b/src/main/java/com/craigraw/drongo/crypto/ChildNumber.java @@ -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; + } +} diff --git a/src/main/java/com/craigraw/drongo/crypto/DeterministicHierarchy.java b/src/main/java/com/craigraw/drongo/crypto/DeterministicHierarchy.java new file mode 100644 index 0000000..1f53bee --- /dev/null +++ b/src/main/java/com/craigraw/drongo/crypto/DeterministicHierarchy.java @@ -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, DeterministicKey> keys = new HashMap<>(); + private final List rootPath; + // Keep track of how many child keys each node has. This is kind of weak. + private final Map, ChildNumber> lastChildNumbers = new HashMap<>(); + + public DeterministicHierarchy(DeterministicKey rootKey) { + putKey(rootKey); + rootPath = rootKey.getPath(); + } + + public final void putKey(DeterministicKey key) { + List 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 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); + } +} diff --git a/src/main/java/com/craigraw/drongo/crypto/DeterministicKey.java b/src/main/java/com/craigraw/drongo/crypto/DeterministicKey.java new file mode 100644 index 0000000..88e8645 --- /dev/null +++ b/src/main/java/com/craigraw/drongo/crypto/DeterministicKey.java @@ -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 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 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 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 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; + } +} diff --git a/src/main/java/com/craigraw/drongo/crypto/ECKey.java b/src/main/java/com/craigraw/drongo/crypto/ECKey.java new file mode 100644 index 0000000..3fd2e9a --- /dev/null +++ b/src/main/java/com/craigraw/drongo/crypto/ECKey.java @@ -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 not 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); + } +} diff --git a/src/main/java/com/craigraw/drongo/crypto/HDKeyDerivation.java b/src/main/java/com/craigraw/drongo/crypto/HDKeyDerivation.java new file mode 100644 index 0000000..849493e --- /dev/null +++ b/src/main/java/com/craigraw/drongo/crypto/HDKeyDerivation.java @@ -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; + } + } +} diff --git a/src/main/java/com/craigraw/drongo/crypto/LazyECPoint.java b/src/main/java/com/craigraw/drongo/crypto/LazyECPoint.java new file mode 100644 index 0000000..34d278b --- /dev/null +++ b/src/main/java/com/craigraw/drongo/crypto/LazyECPoint.java @@ -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(); + } +} diff --git a/src/main/java/com/craigraw/drongo/protocol/Base58.java b/src/main/java/com/craigraw/drongo/protocol/Base58.java new file mode 100644 index 0000000..f42dd63 --- /dev/null +++ b/src/main/java/com/craigraw/drongo/protocol/Base58.java @@ -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. + *

+ * Note that this is not the same base58 as used by Flickr, which you may find referenced around the Internet. + *

+ * Satoshi explains: why base-58 instead of standard base-64 encoding? + *

    + *
  • Don't want 0OIl characters that look the same in some fonts and + * could be used to create visually identical looking account numbers.
  • + *
  • A string with non-alphanumeric characters is not as easily accepted as an account number.
  • + *
  • E-mail usually won't line-break if there's no punctuation to break at.
  • + *
  • Doubleclicking selects the whole number as one word if it's all alphanumeric.
  • + *
+ *

+ * However, note that the encoding/decoding runs in O(n²) time, so it is not useful for large data. + *

+ * 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; + } +} diff --git a/src/main/java/com/craigraw/drongo/protocol/Bech32.java b/src/main/java/com/craigraw/drongo/protocol/Bech32.java new file mode 100644 index 0000000..bb67301 --- /dev/null +++ b/src/main/java/com/craigraw/drongo/protocol/Bech32.java @@ -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(); + } +} diff --git a/src/main/java/com/craigraw/drongo/protocol/ProtocolException.java b/src/main/java/com/craigraw/drongo/protocol/ProtocolException.java new file mode 100644 index 0000000..5280452 --- /dev/null +++ b/src/main/java/com/craigraw/drongo/protocol/ProtocolException.java @@ -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); + } +} diff --git a/src/main/java/com/craigraw/drongo/protocol/Ripemd160.java b/src/main/java/com/craigraw/drongo/protocol/Ripemd160.java new file mode 100644 index 0000000..65ea60b --- /dev/null +++ b/src/main/java/com/craigraw/drongo/protocol/Ripemd160.java @@ -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 + +} diff --git a/src/main/java/com/craigraw/drongo/protocol/Script.java b/src/main/java/com/craigraw/drongo/protocol/Script.java new file mode 100644 index 0000000..6282bf9 --- /dev/null +++ b/src/main/java/com/craigraw/drongo/protocol/Script.java @@ -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 chunks; + + protected byte[] program; + + public Script(byte[] programBytes) { + program = programBytes; + parse(programBytes); + } + + public Script(List 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); + } + + /** + *

If the program somehow pays to a hash, returns the hash.

+ * + *

Otherwise this method throws a ScriptException.

+ */ + 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; + } +} diff --git a/src/main/java/com/craigraw/drongo/protocol/ScriptChunk.java b/src/main/java/com/craigraw/drongo/protocol/ScriptChunk.java new file mode 100644 index 0000000..89d67aa --- /dev/null +++ b/src/main/java/com/craigraw/drongo/protocol/ScriptChunk.java @@ -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 + } + } +} diff --git a/src/main/java/com/craigraw/drongo/protocol/ScriptOpCodes.java b/src/main/java/com/craigraw/drongo/protocol/ScriptOpCodes.java new file mode 100644 index 0000000..c1a4691 --- /dev/null +++ b/src/main/java/com/craigraw/drongo/protocol/ScriptOpCodes.java @@ -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; +} \ No newline at end of file diff --git a/src/main/java/com/craigraw/drongo/protocol/ScriptPattern.java b/src/main/java/com/craigraw/drongo/protocol/ScriptPattern.java new file mode 100644 index 0000000..e9ad2d3 --- /dev/null +++ b/src/main/java/com/craigraw/drongo/protocol/ScriptPattern.java @@ -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 EQUALVERIFY CHECKSIG}, ie, payment to an + * public key like {@code 2102f3b08938a7f8d2609d567aebc4989eeded6e2e880c058fdf092c5da82c3bc5eeac}. + */ + public static boolean isP2PK(Script script) { + List 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 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 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; + } + + /** + *

+ * 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. + *

+ *

+ * P2SH is described by BIP16. + *

+ */ + public static boolean isP2SH(Script script) { + List 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 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
addresses = new ArrayList<>(); + + List 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 }. This can either be a P2WPKH or P2WSH scriptPubKey. These + * two script types were introduced with segwit. + */ + public static boolean isP2WH(Script script) { + List 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; + } +} diff --git a/src/main/java/com/craigraw/drongo/protocol/Sha256Hash.java b/src/main/java/com/craigraw/drongo/protocol/Sha256Hash.java new file mode 100644 index 0000000..e17fb96 --- /dev/null +++ b/src/main/java/com/craigraw/drongo/protocol/Sha256Hash.java @@ -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 { + 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; + } +} diff --git a/src/main/java/com/craigraw/drongo/protocol/Transaction.java b/src/main/java/com/craigraw/drongo/protocol/Transaction.java new file mode 100644 index 0000000..1076640 --- /dev/null +++ b/src/main/java/com/craigraw/drongo/protocol/Transaction.java @@ -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 inputs; + private ArrayList 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 BIP144 or the + * classic format, 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 BIP144 or + * the classic format, 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 getInputs() { + return Collections.unmodifiableList(inputs); + } + + /** Returns an unmodifiable view of all outputs. */ + public List 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]); + } +} diff --git a/src/main/java/com/craigraw/drongo/protocol/TransactionInput.java b/src/main/java/com/craigraw/drongo/protocol/TransactionInput.java new file mode 100644 index 0000000..fcb41f5 --- /dev/null +++ b/src/main/java/com/craigraw/drongo/protocol/TransactionInput.java @@ -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); + } +} diff --git a/src/main/java/com/craigraw/drongo/protocol/TransactionOutPoint.java b/src/main/java/com/craigraw/drongo/protocol/TransactionOutPoint.java new file mode 100644 index 0000000..f8bf861 --- /dev/null +++ b/src/main/java/com/craigraw/drongo/protocol/TransactionOutPoint.java @@ -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; + } +} diff --git a/src/main/java/com/craigraw/drongo/protocol/TransactionOutput.java b/src/main/java/com/craigraw/drongo/protocol/TransactionOutput.java new file mode 100644 index 0000000..0cc993c --- /dev/null +++ b/src/main/java/com/craigraw/drongo/protocol/TransactionOutput.java @@ -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; + } +} diff --git a/src/main/java/com/craigraw/drongo/protocol/TransactionPart.java b/src/main/java/com/craigraw/drongo/protocol/TransactionPart.java new file mode 100644 index 0000000..151d10e --- /dev/null +++ b/src/main/java/com/craigraw/drongo/protocol/TransactionPart.java @@ -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()); + } +} diff --git a/src/main/java/com/craigraw/drongo/protocol/TransactionWitness.java b/src/main/java/com/craigraw/drongo/protocol/TransactionWitness.java new file mode 100644 index 0000000..947336a --- /dev/null +++ b/src/main/java/com/craigraw/drongo/protocol/TransactionWitness.java @@ -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 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); + } + } +} diff --git a/src/main/java/com/craigraw/drongo/protocol/UnsafeByteArrayOutputStream.java b/src/main/java/com/craigraw/drongo/protocol/UnsafeByteArrayOutputStream.java new file mode 100644 index 0000000..9ed2f79 --- /dev/null +++ b/src/main/java/com/craigraw/drongo/protocol/UnsafeByteArrayOutputStream.java @@ -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; + +/** + *

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.

+ * + *

Unless the final length can be accurately predicted the only performance this will yield is due to unsynchronized + * methods.

+ * + * @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; + } +} diff --git a/src/main/java/com/craigraw/drongo/protocol/VarInt.java b/src/main/java/com/craigraw/drongo/protocol/VarInt.java new file mode 100644 index 0000000..f753e0c --- /dev/null +++ b/src/main/java/com/craigraw/drongo/protocol/VarInt.java @@ -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; + } + } +} diff --git a/src/main/java/com/craigraw/drongo/rpc/BitcoinJSONRPCClient.java b/src/main/java/com/craigraw/drongo/rpc/BitcoinJSONRPCClient.java new file mode 100644 index 0000000..deea868 --- /dev/null +++ b/src/main/java/com/craigraw/drongo/rpc/BitcoinJSONRPCClient.java @@ -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() { + { + 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); + } +} diff --git a/src/main/java/com/craigraw/drongo/rpc/BitcoinRPCError.java b/src/main/java/com/craigraw/drongo/rpc/BitcoinRPCError.java new file mode 100644 index 0000000..a9ba4b1 --- /dev/null +++ b/src/main/java/com/craigraw/drongo/rpc/BitcoinRPCError.java @@ -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; + } +} diff --git a/src/main/java/com/craigraw/drongo/rpc/BitcoinRPCException.java b/src/main/java/com/craigraw/drongo/rpc/BitcoinRPCException.java new file mode 100644 index 0000000..31f319e --- /dev/null +++ b/src/main/java/com/craigraw/drongo/rpc/BitcoinRPCException.java @@ -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 BitcoinRPCException 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 BitcoinRPCException 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; + } +} diff --git a/src/main/resources/log4j.properties b/src/main/resources/log4j.properties new file mode 100644 index 0000000..0f03697 --- /dev/null +++ b/src/main/resources/log4j.properties @@ -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 \ No newline at end of file diff --git a/src/test/java/com/craigraw/drongo/OutputDescriptorTest.java b/src/test/java/com/craigraw/drongo/OutputDescriptorTest.java new file mode 100644 index 0000000..d27da9a --- /dev/null +++ b/src/test/java/com/craigraw/drongo/OutputDescriptorTest.java @@ -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()); + } +} diff --git a/src/test/java/com/craigraw/drongo/WatchWalletTest.java b/src/test/java/com/craigraw/drongo/WatchWalletTest.java new file mode 100644 index 0000000..b7ae1fc --- /dev/null +++ b/src/test/java/com/craigraw/drongo/WatchWalletTest.java @@ -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()); + } +}