mirror of
https://github.com/sparrowwallet/drongo.git
synced 2024-11-02 18:26:43 +00:00
Initial commit
This commit is contained in:
commit
911a54347d
47 changed files with 4283 additions and 0 deletions
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
.idea
|
||||||
|
.gradle
|
||||||
|
*iml
|
||||||
|
build
|
||||||
|
/*.properties
|
||||||
|
out
|
||||||
|
*.log
|
43
build.gradle
Normal file
43
build.gradle
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
plugins {
|
||||||
|
id 'java'
|
||||||
|
id 'com.github.johnrengelman.shadow' version '4.0.2'
|
||||||
|
}
|
||||||
|
|
||||||
|
group 'com.craigraw'
|
||||||
|
version '0.1'
|
||||||
|
|
||||||
|
sourceCompatibility = 1.8
|
||||||
|
targetCompatibility = 1.8
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compile 'org.zeromq:jeromq:0.5.0'
|
||||||
|
compile 'com.googlecode.json-simple:json-simple:1.1.1'
|
||||||
|
compile 'org.bouncycastle:bcprov-jdk15on:1.60'
|
||||||
|
implementation 'org.slf4j:slf4j-api:1.7.25'
|
||||||
|
runtime 'org.slf4j:slf4j-log4j12:1.7.25'
|
||||||
|
testCompile group: 'junit', name: 'junit', version: '4.11'
|
||||||
|
}
|
||||||
|
|
||||||
|
task(runDrongo, dependsOn: 'classes', type: JavaExec) {
|
||||||
|
main = 'com.craigraw.drongo.Main'
|
||||||
|
classpath = sourceSets.main.runtimeClasspath
|
||||||
|
args 'drongo.properties'
|
||||||
|
}
|
||||||
|
|
||||||
|
jar {
|
||||||
|
manifest {
|
||||||
|
attributes "Main-Class": "com.craigraw.drongo.Main"
|
||||||
|
}
|
||||||
|
|
||||||
|
baseName = 'drongo'
|
||||||
|
version = '0.1'
|
||||||
|
}
|
||||||
|
|
||||||
|
shadowJar {
|
||||||
|
version = '0.1'
|
||||||
|
classifier = 'all'
|
||||||
|
}
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
#Tue Feb 26 12:05:19 SAST 2019
|
||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip
|
172
gradlew
vendored
Executable file
172
gradlew
vendored
Executable file
|
@ -0,0 +1,172 @@
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
##
|
||||||
|
## Gradle start up script for UN*X
|
||||||
|
##
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
PRG="$0"
|
||||||
|
# Need this for relative symlinks.
|
||||||
|
while [ -h "$PRG" ] ; do
|
||||||
|
ls=`ls -ld "$PRG"`
|
||||||
|
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||||
|
if expr "$link" : '/.*' > /dev/null; then
|
||||||
|
PRG="$link"
|
||||||
|
else
|
||||||
|
PRG=`dirname "$PRG"`"/$link"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
SAVED="`pwd`"
|
||||||
|
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||||
|
APP_HOME="`pwd -P`"
|
||||||
|
cd "$SAVED" >/dev/null
|
||||||
|
|
||||||
|
APP_NAME="Gradle"
|
||||||
|
APP_BASE_NAME=`basename "$0"`
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS=""
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD="maximum"
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "`uname`" in
|
||||||
|
CYGWIN* )
|
||||||
|
cygwin=true
|
||||||
|
;;
|
||||||
|
Darwin* )
|
||||||
|
darwin=true
|
||||||
|
;;
|
||||||
|
MINGW* )
|
||||||
|
msys=true
|
||||||
|
;;
|
||||||
|
NONSTOP* )
|
||||||
|
nonstop=true
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||||
|
else
|
||||||
|
JAVACMD="$JAVA_HOME/bin/java"
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD="java"
|
||||||
|
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||||
|
MAX_FD_LIMIT=`ulimit -H -n`
|
||||||
|
if [ $? -eq 0 ] ; then
|
||||||
|
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||||
|
MAX_FD="$MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
ulimit -n $MAX_FD
|
||||||
|
if [ $? -ne 0 ] ; then
|
||||||
|
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Darwin, add options to specify how the application appears in the dock
|
||||||
|
if $darwin; then
|
||||||
|
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Cygwin, switch paths to Windows format before running java
|
||||||
|
if $cygwin ; then
|
||||||
|
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||||
|
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||||
|
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||||
|
|
||||||
|
# We build the pattern for arguments to be converted via cygpath
|
||||||
|
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||||
|
SEP=""
|
||||||
|
for dir in $ROOTDIRSRAW ; do
|
||||||
|
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||||
|
SEP="|"
|
||||||
|
done
|
||||||
|
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||||
|
# Add a user-defined pattern to the cygpath arguments
|
||||||
|
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||||
|
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||||
|
fi
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
i=0
|
||||||
|
for arg in "$@" ; do
|
||||||
|
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||||
|
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||||
|
|
||||||
|
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||||
|
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||||
|
else
|
||||||
|
eval `echo args$i`="\"$arg\""
|
||||||
|
fi
|
||||||
|
i=$((i+1))
|
||||||
|
done
|
||||||
|
case $i in
|
||||||
|
(0) set -- ;;
|
||||||
|
(1) set -- "$args0" ;;
|
||||||
|
(2) set -- "$args0" "$args1" ;;
|
||||||
|
(3) set -- "$args0" "$args1" "$args2" ;;
|
||||||
|
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||||
|
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||||
|
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||||
|
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||||
|
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||||
|
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Escape application args
|
||||||
|
save () {
|
||||||
|
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||||
|
echo " "
|
||||||
|
}
|
||||||
|
APP_ARGS=$(save "$@")
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||||
|
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||||
|
|
||||||
|
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
|
||||||
|
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
84
gradlew.bat
vendored
Normal file
84
gradlew.bat
vendored
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
@if "%DEBUG%" == "" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%" == "" set DIRNAME=.
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS=
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if "%ERRORLEVEL%" == "0" goto init
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto init
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:init
|
||||||
|
@rem Get command-line arguments, handling Windows variants
|
||||||
|
|
||||||
|
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||||
|
|
||||||
|
:win9xME_args
|
||||||
|
@rem Slurp the command line arguments.
|
||||||
|
set CMD_LINE_ARGS=
|
||||||
|
set _SKIP=2
|
||||||
|
|
||||||
|
:win9xME_args_slurp
|
||||||
|
if "x%~1" == "x" goto execute
|
||||||
|
|
||||||
|
set CMD_LINE_ARGS=%*
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||||
|
exit /b 1
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
2
settings.gradle
Normal file
2
settings.gradle
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
rootProject.name = 'drongo'
|
||||||
|
|
69
src/main/java/com/craigraw/drongo/Drongo.java
Normal file
69
src/main/java/com/craigraw/drongo/Drongo.java
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
package com.craigraw.drongo;
|
||||||
|
|
||||||
|
import com.craigraw.drongo.rpc.BitcoinJSONRPCClient;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.zeromq.SocketType;
|
||||||
|
import org.zeromq.ZContext;
|
||||||
|
import org.zeromq.ZMQ;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
|
public class Drongo {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(Drongo.class);
|
||||||
|
|
||||||
|
private String nodeZmqAddress;
|
||||||
|
private BitcoinJSONRPCClient bitcoinJSONRPCClient;
|
||||||
|
private List<WatchWallet> watchWallets;
|
||||||
|
private String[] notifyRecipients;
|
||||||
|
|
||||||
|
public Drongo(String nodeZmqAddress, Map<String, String> nodeRpc, List<WatchWallet> watchWallets, String[] notifyRecipients) {
|
||||||
|
this.nodeZmqAddress = nodeZmqAddress;
|
||||||
|
this.bitcoinJSONRPCClient = new BitcoinJSONRPCClient(nodeRpc.get("host"), nodeRpc.get("port"), nodeRpc.get("user"), nodeRpc.get("password"));
|
||||||
|
this.watchWallets = watchWallets;
|
||||||
|
this.notifyRecipients = notifyRecipients;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void start() {
|
||||||
|
ExecutorService executorService = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
executorService = Executors.newFixedThreadPool(2);
|
||||||
|
|
||||||
|
try (ZContext context = new ZContext()) {
|
||||||
|
ZMQ.Socket subscriber = context.createSocket(SocketType.SUB);
|
||||||
|
subscriber.setRcvHWM(0);
|
||||||
|
subscriber.connect(nodeZmqAddress);
|
||||||
|
|
||||||
|
String subscription = "rawtx";
|
||||||
|
subscriber.subscribe(subscription.getBytes(ZMQ.CHARSET));
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
String topic = subscriber.recvStr();
|
||||||
|
if (topic == null)
|
||||||
|
break;
|
||||||
|
byte[] data = subscriber.recv();
|
||||||
|
assert (topic.equals(subscription));
|
||||||
|
|
||||||
|
if(subscriber.hasReceiveMore()) {
|
||||||
|
byte[] endData = subscriber.recv();
|
||||||
|
}
|
||||||
|
|
||||||
|
TransactionTask transactionTask = new TransactionTask(this, data);
|
||||||
|
executorService.submit(transactionTask);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if(executorService != null) {
|
||||||
|
executorService.shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public BitcoinJSONRPCClient getBitcoinJSONRPCClient() {
|
||||||
|
return bitcoinJSONRPCClient;
|
||||||
|
}
|
||||||
|
}
|
77
src/main/java/com/craigraw/drongo/Main.java
Normal file
77
src/main/java/com/craigraw/drongo/Main.java
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
package com.craigraw.drongo;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public class Main {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(Main.class);
|
||||||
|
|
||||||
|
public static void main(String [] args) {
|
||||||
|
String propertiesFile = "./drongo.properties";
|
||||||
|
if(args.length > 0) {
|
||||||
|
propertiesFile = args[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
Properties properties = new Properties();
|
||||||
|
properties.setProperty("nodeAddress", "localhost");
|
||||||
|
|
||||||
|
try {
|
||||||
|
File file = new File(propertiesFile);
|
||||||
|
properties.load(new FileInputStream(propertiesFile));
|
||||||
|
log.info("Loaded properties from " + file.getCanonicalPath());
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Could not load properties from provided path " + propertiesFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
String nodeZmqAddress = properties.getProperty("node.zmqpubrawtx");
|
||||||
|
if(nodeZmqAddress == null) {
|
||||||
|
log.error("Property node.zmqpubrawtx not set, provide the zmqpubrawtx setting of the local node");
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, String> rpcConnection = new LinkedHashMap<String, String>() {
|
||||||
|
{
|
||||||
|
put("host", properties.getProperty("node.rpcconnect", "127.0.0.1"));
|
||||||
|
put("port", properties.getProperty("node.rpcport", "8332"));
|
||||||
|
put("user", properties.getProperty("node.rpcuser"));
|
||||||
|
put("password", properties.getProperty("node.rpcpassword"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
List<WatchWallet> watchWallets = new ArrayList<>();
|
||||||
|
int walletNumber = 1;
|
||||||
|
WatchWallet wallet = getWalletFromProperties(properties, walletNumber);
|
||||||
|
if(wallet == null) {
|
||||||
|
log.error("Property wallet.name.1 and/or wallet.pubkey.1 not set, provide wallet name and Base58 encoded key starting with xpub or ypub");
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
while(wallet != null) {
|
||||||
|
watchWallets.add(wallet);
|
||||||
|
wallet = getWalletFromProperties(properties, ++walletNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
String notifyRecipients = properties.getProperty("notify.recipients");
|
||||||
|
if(notifyRecipients == null) {
|
||||||
|
log.error("Property notify.recipients not set, provide comma separated email addresses to receive wallet change notifications");
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
Drongo drongo = new Drongo(nodeZmqAddress, rpcConnection, watchWallets, notifyRecipients.split(","));
|
||||||
|
drongo.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static WatchWallet getWalletFromProperties(Properties properties, int walletNumber) {
|
||||||
|
String walletName = properties.getProperty("wallet.name." + walletNumber);
|
||||||
|
String walletPubKey = properties.getProperty("wallet.pubkey." + walletNumber);
|
||||||
|
if(walletName != null && walletPubKey != null) {
|
||||||
|
return new WatchWallet(walletName, walletPubKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
259
src/main/java/com/craigraw/drongo/OutputDescriptor.java
Normal file
259
src/main/java/com/craigraw/drongo/OutputDescriptor.java
Normal file
|
@ -0,0 +1,259 @@
|
||||||
|
package com.craigraw.drongo;
|
||||||
|
|
||||||
|
import com.craigraw.drongo.address.Address;
|
||||||
|
import com.craigraw.drongo.address.P2PKHAddress;
|
||||||
|
import com.craigraw.drongo.address.P2SHAddress;
|
||||||
|
import com.craigraw.drongo.address.P2WPKHAddress;
|
||||||
|
import com.craigraw.drongo.crypto.ChildNumber;
|
||||||
|
import com.craigraw.drongo.crypto.DeterministicKey;
|
||||||
|
import com.craigraw.drongo.crypto.ECKey;
|
||||||
|
import com.craigraw.drongo.crypto.LazyECPoint;
|
||||||
|
import com.craigraw.drongo.protocol.Base58;
|
||||||
|
import com.craigraw.drongo.protocol.Script;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
public class OutputDescriptor {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(OutputDescriptor.class);
|
||||||
|
|
||||||
|
private static final int bip32HeaderP2PKHXPub = 0x0488B21E; //The 4 byte header that serializes in base58 to "xpub".
|
||||||
|
private static final int bip32HeaderP2PKHYPub = 0x049D7CB2; //The 4 byte header that serializes in base58 to "ypub".
|
||||||
|
private static final int bip32HeaderP2WPKHZPub = 0x04B24746; // The 4 byte header that serializes in base58 to "zpub"
|
||||||
|
|
||||||
|
private static final Pattern DESCRIPTOR_PATTERN = Pattern.compile("(.+)\\((\\[[^\\]]+\\])?(xpub[^/\\)]+)(/[/\\d*']+)?\\)\\)?");
|
||||||
|
|
||||||
|
private String script;
|
||||||
|
private int parentFingerprint;
|
||||||
|
private String keyDerivationPath;
|
||||||
|
private DeterministicKey pubKey;
|
||||||
|
private String childDerivationPath;
|
||||||
|
private ChildNumber pubKeyChildNumber;
|
||||||
|
|
||||||
|
public OutputDescriptor(String script, int parentFingerprint, String keyDerivationPath, DeterministicKey pubKey, String childDerivationPath, ChildNumber pubKeyChildNumber) {
|
||||||
|
this.script = script;
|
||||||
|
this.parentFingerprint = parentFingerprint;
|
||||||
|
this.keyDerivationPath = keyDerivationPath;
|
||||||
|
this.pubKey = pubKey;
|
||||||
|
this.childDerivationPath = childDerivationPath;
|
||||||
|
this.pubKeyChildNumber = pubKeyChildNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getScript() {
|
||||||
|
return script;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getParentFingerprint() {
|
||||||
|
return parentFingerprint;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ChildNumber> getKeyDerivation() {
|
||||||
|
return parsePath(keyDerivationPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DeterministicKey getPubKey() {
|
||||||
|
return pubKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ChildNumber> getChildDerivation() {
|
||||||
|
return getChildDerivation(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ChildNumber> getChildDerivation(int wildCardReplacement) {
|
||||||
|
return getChildDerivation(new ChildNumber(0, getPubKey().getChildNumber().isHardened()), childDerivationPath, wildCardReplacement);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean describesMultipleAddresses() {
|
||||||
|
return childDerivationPath.endsWith("/*");
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ChildNumber> getReceivingDerivation(int wildCardReplacement) {
|
||||||
|
if(describesMultipleAddresses()) {
|
||||||
|
if(childDerivationPath.endsWith("0/*")) {
|
||||||
|
return getChildDerivation(new ChildNumber(0, getPubKey().getChildNumber().isHardened()), childDerivationPath, wildCardReplacement);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(pubKeyChildNumber.num() == 0 && childDerivationPath.endsWith("/*")) {
|
||||||
|
return getChildDerivation(new ChildNumber(0, getPubKey().getChildNumber().isHardened()), childDerivationPath, wildCardReplacement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IllegalStateException("Cannot derive receiving address from output descriptor " + this.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ChildNumber> getChangeDerivation(int wildCardReplacement) {
|
||||||
|
if(describesMultipleAddresses()) {
|
||||||
|
if(childDerivationPath.endsWith("0/*")) {
|
||||||
|
return getChildDerivation(new ChildNumber(0, getPubKey().getChildNumber().isHardened()), childDerivationPath.replace("0/*", "1/*"), wildCardReplacement);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(pubKeyChildNumber.num() == 1 && childDerivationPath.endsWith("/*")) {
|
||||||
|
return getChildDerivation(new ChildNumber(1, getPubKey().getChildNumber().isHardened()), childDerivationPath, wildCardReplacement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IllegalStateException("Cannot derive change address from output descriptor " + this.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ChildNumber> getChildDerivation(ChildNumber firstChild, String derivationPath, int wildCardReplacement) {
|
||||||
|
List<ChildNumber> path = new ArrayList<>();
|
||||||
|
path.add(firstChild);
|
||||||
|
path.addAll(parsePath(derivationPath, wildCardReplacement));
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Address getAddress(DeterministicKey childKey) {
|
||||||
|
Address address = null;
|
||||||
|
if(script.equals("pkh")) {
|
||||||
|
address = new P2PKHAddress(childKey.getPubKeyHash());
|
||||||
|
} else if(script.equals("sh(wpkh")) {
|
||||||
|
Address p2wpkhAddress = new P2WPKHAddress(childKey.getPubKeyHash());
|
||||||
|
Script receivingP2wpkhScript = p2wpkhAddress.getOutputScript();
|
||||||
|
address = P2SHAddress.fromProgram(receivingP2wpkhScript.getProgram());
|
||||||
|
} else if(script.equals("wpkh")) {
|
||||||
|
address = new P2WPKHAddress(childKey.getPubKeyHash());
|
||||||
|
} else {
|
||||||
|
throw new IllegalStateException("Cannot determine address for script " + script);
|
||||||
|
}
|
||||||
|
|
||||||
|
return address;
|
||||||
|
}
|
||||||
|
|
||||||
|
// See https://github.com/bitcoin/bitcoin/blob/master/doc/descriptors.md
|
||||||
|
public static OutputDescriptor getOutputDescriptor(String descriptor) {
|
||||||
|
String script;
|
||||||
|
String keyDerivationPath ="";
|
||||||
|
String extPubKey = null;
|
||||||
|
String childDerivationPath = "/0/*";
|
||||||
|
|
||||||
|
Matcher matcher = DESCRIPTOR_PATTERN.matcher(descriptor);
|
||||||
|
if(matcher.matches()) {
|
||||||
|
script = matcher.group(1);
|
||||||
|
if(matcher.group(2) != null) {
|
||||||
|
keyDerivationPath = matcher.group(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
extPubKey = matcher.group(3);
|
||||||
|
if(matcher.group(4) != null) {
|
||||||
|
childDerivationPath = matcher.group(4);
|
||||||
|
}
|
||||||
|
} else if (descriptor.startsWith("xpub")) {
|
||||||
|
extPubKey = descriptor;
|
||||||
|
script = "pkh";
|
||||||
|
} else if(descriptor.startsWith("ypub")) {
|
||||||
|
extPubKey = descriptor;
|
||||||
|
script = "sh(wpkh";
|
||||||
|
} else if(descriptor.startsWith("zpub")) {
|
||||||
|
extPubKey = descriptor;
|
||||||
|
script = "wpkh";
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("Could not parse output descriptor:" + descriptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] serializedKey = Base58.decodeChecked(extPubKey);
|
||||||
|
ByteBuffer buffer = ByteBuffer.wrap(serializedKey);
|
||||||
|
int header = buffer.getInt();
|
||||||
|
if(!(header == bip32HeaderP2PKHXPub || header == bip32HeaderP2PKHYPub || header == bip32HeaderP2WPKHZPub)) {
|
||||||
|
throw new IllegalArgumentException("Unknown header bytes: " + DeterministicKey.toBase58(serializedKey).substring(0, 4));
|
||||||
|
}
|
||||||
|
|
||||||
|
int depth = buffer.get() & 0xFF; // convert signed byte to positive int since depth cannot be negative
|
||||||
|
final int parentFingerprint = buffer.getInt();
|
||||||
|
final int i = buffer.getInt();
|
||||||
|
ChildNumber childNumber;
|
||||||
|
List<ChildNumber> path;
|
||||||
|
|
||||||
|
if(depth == 0) {
|
||||||
|
//Poorly formatted extended public key, add first child path element
|
||||||
|
childNumber = new ChildNumber(0, false);
|
||||||
|
} else if ((i & ChildNumber.HARDENED_BIT) != 0) {
|
||||||
|
childNumber = new ChildNumber(i ^ ChildNumber.HARDENED_BIT, true); //already hardened
|
||||||
|
} else {
|
||||||
|
childNumber = new ChildNumber(i, false);
|
||||||
|
}
|
||||||
|
path = Collections.unmodifiableList(new ArrayList<>(Arrays.asList(childNumber)));
|
||||||
|
|
||||||
|
//Remove account level for depth 4 keys
|
||||||
|
if(depth == 4 && (descriptor.startsWith("xpub") || descriptor.startsWith("ypub") || descriptor.startsWith("zpub"))) {
|
||||||
|
log.warn("Output descriptor describes a public key derived at depth 4; change addresses not available");
|
||||||
|
childDerivationPath = "/*";
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] chainCode = new byte[32];
|
||||||
|
buffer.get(chainCode);
|
||||||
|
byte[] data = new byte[33];
|
||||||
|
buffer.get(data);
|
||||||
|
if(buffer.hasRemaining()) {
|
||||||
|
throw new IllegalArgumentException("Found unexpected data in key");
|
||||||
|
}
|
||||||
|
|
||||||
|
DeterministicKey pubKey = new DeterministicKey(path, chainCode, new LazyECPoint(ECKey.CURVE.getCurve(), data), depth, parentFingerprint);
|
||||||
|
return new OutputDescriptor(script, parentFingerprint, keyDerivationPath, pubKey, childDerivationPath, childNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<ChildNumber> parsePath(String path) {
|
||||||
|
return parsePath(path, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<ChildNumber> parsePath(String path, int wildcardReplacement) {
|
||||||
|
String[] parsedNodes = path.replace("M", "").split("/");
|
||||||
|
List<ChildNumber> nodes = new ArrayList<>();
|
||||||
|
|
||||||
|
for (String n : parsedNodes) {
|
||||||
|
n = n.replaceAll(" ", "");
|
||||||
|
if (n.length() == 0) continue;
|
||||||
|
boolean isHard = n.endsWith("H") || n.endsWith("h") || n.endsWith("'");
|
||||||
|
if (isHard) n = n.substring(0, n.length() - 1);
|
||||||
|
if (n.equals("*")) n = Integer.toString(wildcardReplacement);
|
||||||
|
int nodeNumber = Integer.parseInt(n);
|
||||||
|
nodes.add(new ChildNumber(nodeNumber, isHard));
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String toString() {
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
builder.append(script);
|
||||||
|
builder.append("(");
|
||||||
|
builder.append(getExtendedPublicKey());
|
||||||
|
builder.append(childDerivationPath);
|
||||||
|
builder.append(")");
|
||||||
|
|
||||||
|
if(script.contains("(")){
|
||||||
|
builder.append(")");
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getExtendedPublicKey() {
|
||||||
|
return Base58.encodeChecked(getExtendedPublicKeyBytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getExtendedPublicKeyBytes() {
|
||||||
|
ByteBuffer buffer = ByteBuffer.allocate(78);
|
||||||
|
buffer.putInt(bip32HeaderP2PKHXPub);
|
||||||
|
|
||||||
|
List<ChildNumber> childPath = parsePath(childDerivationPath);
|
||||||
|
int depth = 5 - childPath.size();
|
||||||
|
buffer.put((byte)depth);
|
||||||
|
|
||||||
|
buffer.putInt(parentFingerprint);
|
||||||
|
|
||||||
|
buffer.putInt(pubKeyChildNumber.i());
|
||||||
|
|
||||||
|
buffer.put(pubKey.getChainCode());
|
||||||
|
buffer.put(pubKey.getPubKey());
|
||||||
|
|
||||||
|
return buffer.array();
|
||||||
|
}
|
||||||
|
}
|
79
src/main/java/com/craigraw/drongo/TransactionTask.java
Normal file
79
src/main/java/com/craigraw/drongo/TransactionTask.java
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
package com.craigraw.drongo;
|
||||||
|
|
||||||
|
import com.craigraw.drongo.address.Address;
|
||||||
|
import com.craigraw.drongo.protocol.*;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public class TransactionTask implements Runnable {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(Drongo.class);
|
||||||
|
|
||||||
|
private Drongo drongo;
|
||||||
|
private byte[] transactionData;
|
||||||
|
|
||||||
|
public TransactionTask(Drongo drongo, byte[] transactionData) {
|
||||||
|
this.drongo = drongo;
|
||||||
|
this.transactionData = transactionData;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Transaction transaction = new Transaction(transactionData);
|
||||||
|
Map<String, Transaction> referencedTransactions = new HashMap<>();
|
||||||
|
|
||||||
|
Sha256Hash txid = transaction.getTxId();
|
||||||
|
StringBuilder builder = new StringBuilder("Txid: " + txid.toString() + " ");
|
||||||
|
StringJoiner inputJoiner = new StringJoiner(", ", "[", "]");
|
||||||
|
|
||||||
|
int vin = 0;
|
||||||
|
for(TransactionInput input : transaction.getInputs()) {
|
||||||
|
if(input.isCoinBase()) {
|
||||||
|
inputJoiner.add("Coinbase:" + vin);
|
||||||
|
} else {
|
||||||
|
String referencedTxID = input.getOutpoint().getHash().toString();
|
||||||
|
long referencedVout = input.getOutpoint().getIndex();
|
||||||
|
|
||||||
|
Transaction referencedTransaction = referencedTransactions.get(referencedTxID);
|
||||||
|
if(referencedTransaction == null) {
|
||||||
|
String referencedTransactionHex = drongo.getBitcoinJSONRPCClient().getRawTransaction(referencedTxID);
|
||||||
|
referencedTransaction = new Transaction(Utils.hexToBytes(referencedTransactionHex));
|
||||||
|
referencedTransactions.put(referencedTxID, referencedTransaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
TransactionOutput referencedOutput = referencedTransaction.getOutputs().get((int)referencedVout);
|
||||||
|
if(referencedOutput.getScript().containsToAddress()) {
|
||||||
|
Address[] inputAddresses = referencedOutput.getScript().getToAddresses();
|
||||||
|
input.getOutpoint().setAddresses(inputAddresses);
|
||||||
|
inputJoiner.add((inputAddresses.length == 1 ? inputAddresses[0] : Arrays.asList(inputAddresses)) + ":" + vin);
|
||||||
|
} else {
|
||||||
|
log.warn("Could not determine nature of referenced input tx: " + referencedTxID + ":" + referencedVout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vin++;
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.append(inputJoiner.toString() + " => ");
|
||||||
|
StringJoiner outputJoiner = new StringJoiner(", ", "[", "]");
|
||||||
|
|
||||||
|
int vout = 0;
|
||||||
|
for(TransactionOutput output : transaction.getOutputs()) {
|
||||||
|
try {
|
||||||
|
if(output.getScript().containsToAddress()) {
|
||||||
|
Address[] outputAddresses = output.getScript().getToAddresses();
|
||||||
|
output.setAddresses(outputAddresses);
|
||||||
|
outputJoiner.add((outputAddresses.length == 1 ? outputAddresses[0] : Arrays.asList(outputAddresses)) + ":" + vout + " (" + output.getValue() + ")");
|
||||||
|
}
|
||||||
|
} catch(ProtocolException e) {
|
||||||
|
log.debug("Invalid script for output " + vout + " detected (" + e.getMessage() + "). Skipping...");
|
||||||
|
}
|
||||||
|
|
||||||
|
vout++;
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.append(outputJoiner.toString());
|
||||||
|
log.info(builder.toString());
|
||||||
|
}
|
||||||
|
}
|
223
src/main/java/com/craigraw/drongo/Utils.java
Normal file
223
src/main/java/com/craigraw/drongo/Utils.java
Normal file
|
@ -0,0 +1,223 @@
|
||||||
|
package com.craigraw.drongo;
|
||||||
|
|
||||||
|
import com.craigraw.drongo.crypto.ChildNumber;
|
||||||
|
import com.craigraw.drongo.protocol.ProtocolException;
|
||||||
|
import com.craigraw.drongo.protocol.Ripemd160;
|
||||||
|
import com.craigraw.drongo.protocol.Sha256Hash;
|
||||||
|
import org.bouncycastle.crypto.digests.SHA512Digest;
|
||||||
|
import org.bouncycastle.crypto.macs.HMac;
|
||||||
|
import org.bouncycastle.crypto.params.KeyParameter;
|
||||||
|
|
||||||
|
import javax.xml.bind.DatatypeConverter;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.StringJoiner;
|
||||||
|
|
||||||
|
public class Utils {
|
||||||
|
public static final int MAX_INITIAL_ARRAY_LENGTH = 20;
|
||||||
|
private final static char[] hexArray = "0123456789abcdef".toCharArray();
|
||||||
|
|
||||||
|
public static String bytesToHex(byte[] bytes) {
|
||||||
|
char[] hexChars = new char[bytes.length * 2];
|
||||||
|
for ( int j = 0; j < bytes.length; j++ ) {
|
||||||
|
int v = bytes[j] & 0xFF;
|
||||||
|
hexChars[j * 2] = hexArray[v >>> 4];
|
||||||
|
hexChars[j * 2 + 1] = hexArray[v & 0x0F];
|
||||||
|
}
|
||||||
|
return new String(hexChars);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] hexToBytes(final String data) {
|
||||||
|
return decodeHex(data.toCharArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] decodeHex(final char[] data) {
|
||||||
|
|
||||||
|
final int len = data.length;
|
||||||
|
|
||||||
|
if ((len & 0x01) != 0) {
|
||||||
|
throw new ProtocolException("Odd number of characters.");
|
||||||
|
}
|
||||||
|
|
||||||
|
final byte[] out = new byte[len >> 1];
|
||||||
|
|
||||||
|
// two characters form the hex value.
|
||||||
|
for (int i = 0, j = 0; j < len; i++) {
|
||||||
|
int f = toDigit(data[j], j) << 4;
|
||||||
|
j++;
|
||||||
|
f = f | toDigit(data[j], j);
|
||||||
|
j++;
|
||||||
|
out[i] = (byte) (f & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static int toDigit(final char ch, final int index) {
|
||||||
|
final int digit = Character.digit(ch, 16);
|
||||||
|
if (digit == -1) {
|
||||||
|
throw new ProtocolException("Illegal hexadecimal character " + ch + " at index " + index);
|
||||||
|
}
|
||||||
|
return digit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parse 4 bytes from the byte array (starting at the offset) as unsigned 32-bit integer in little endian format. */
|
||||||
|
public static long readUint32(byte[] bytes, int offset) {
|
||||||
|
return (bytes[offset] & 0xffl) |
|
||||||
|
((bytes[offset + 1] & 0xffl) << 8) |
|
||||||
|
((bytes[offset + 2] & 0xffl) << 16) |
|
||||||
|
((bytes[offset + 3] & 0xffl) << 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parse 8 bytes from the byte array (starting at the offset) as signed 64-bit integer in little endian format. */
|
||||||
|
public static long readInt64(byte[] bytes, int offset) {
|
||||||
|
return (bytes[offset] & 0xffl) |
|
||||||
|
((bytes[offset + 1] & 0xffl) << 8) |
|
||||||
|
((bytes[offset + 2] & 0xffl) << 16) |
|
||||||
|
((bytes[offset + 3] & 0xffl) << 24) |
|
||||||
|
((bytes[offset + 4] & 0xffl) << 32) |
|
||||||
|
((bytes[offset + 5] & 0xffl) << 40) |
|
||||||
|
((bytes[offset + 6] & 0xffl) << 48) |
|
||||||
|
((bytes[offset + 7] & 0xffl) << 56);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parse 2 bytes from the byte array (starting at the offset) as unsigned 16-bit integer in little endian format. */
|
||||||
|
public static int readUint16(byte[] bytes, int offset) {
|
||||||
|
return (bytes[offset] & 0xff) |
|
||||||
|
((bytes[offset + 1] & 0xff) << 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parse 2 bytes from the stream as unsigned 16-bit integer in little endian format. */
|
||||||
|
public static int readUint16FromStream(InputStream is) {
|
||||||
|
try {
|
||||||
|
return (is.read() & 0xff) |
|
||||||
|
((is.read() & 0xff) << 8);
|
||||||
|
} catch (IOException x) {
|
||||||
|
throw new RuntimeException(x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parse 4 bytes from the stream as unsigned 32-bit integer in little endian format. */
|
||||||
|
public static long readUint32FromStream(InputStream is) {
|
||||||
|
try {
|
||||||
|
return (is.read() & 0xffl) |
|
||||||
|
((is.read() & 0xffl) << 8) |
|
||||||
|
((is.read() & 0xffl) << 16) |
|
||||||
|
((is.read() & 0xffl) << 24);
|
||||||
|
} catch (IOException x) {
|
||||||
|
throw new RuntimeException(x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Write 2 bytes to the byte array (starting at the offset) as unsigned 16-bit integer in little endian format. */
|
||||||
|
public static void uint16ToByteArrayLE(int val, byte[] out, int offset) {
|
||||||
|
out[offset] = (byte) (0xFF & val);
|
||||||
|
out[offset + 1] = (byte) (0xFF & (val >> 8));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Write 4 bytes to the byte array (starting at the offset) as unsigned 32-bit integer in little endian format. */
|
||||||
|
public static void uint32ToByteArrayLE(long val, byte[] out, int offset) {
|
||||||
|
out[offset] = (byte) (0xFF & val);
|
||||||
|
out[offset + 1] = (byte) (0xFF & (val >> 8));
|
||||||
|
out[offset + 2] = (byte) (0xFF & (val >> 16));
|
||||||
|
out[offset + 3] = (byte) (0xFF & (val >> 24));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Write 8 bytes to the byte array (starting at the offset) as signed 64-bit integer in little endian format. */
|
||||||
|
public static void int64ToByteArrayLE(long val, byte[] out, int offset) {
|
||||||
|
out[offset] = (byte) (0xFF & val);
|
||||||
|
out[offset + 1] = (byte) (0xFF & (val >> 8));
|
||||||
|
out[offset + 2] = (byte) (0xFF & (val >> 16));
|
||||||
|
out[offset + 3] = (byte) (0xFF & (val >> 24));
|
||||||
|
out[offset + 4] = (byte) (0xFF & (val >> 32));
|
||||||
|
out[offset + 5] = (byte) (0xFF & (val >> 40));
|
||||||
|
out[offset + 6] = (byte) (0xFF & (val >> 48));
|
||||||
|
out[offset + 7] = (byte) (0xFF & (val >> 56));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Write 2 bytes to the output stream as unsigned 16-bit integer in little endian format. */
|
||||||
|
public static void uint16ToByteStreamLE(int val, OutputStream stream) throws IOException {
|
||||||
|
stream.write((int) (0xFF & val));
|
||||||
|
stream.write((int) (0xFF & (val >> 8)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Write 4 bytes to the output stream as unsigned 32-bit integer in little endian format. */
|
||||||
|
public static void uint32ToByteStreamLE(long val, OutputStream stream) throws IOException {
|
||||||
|
stream.write((int) (0xFF & val));
|
||||||
|
stream.write((int) (0xFF & (val >> 8)));
|
||||||
|
stream.write((int) (0xFF & (val >> 16)));
|
||||||
|
stream.write((int) (0xFF & (val >> 24)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Write 8 bytes to the output stream as signed 64-bit integer in little endian format. */
|
||||||
|
public static void int64ToByteStreamLE(long val, OutputStream stream) throws IOException {
|
||||||
|
stream.write((int) (0xFF & val));
|
||||||
|
stream.write((int) (0xFF & (val >> 8)));
|
||||||
|
stream.write((int) (0xFF & (val >> 16)));
|
||||||
|
stream.write((int) (0xFF & (val >> 24)));
|
||||||
|
stream.write((int) (0xFF & (val >> 32)));
|
||||||
|
stream.write((int) (0xFF & (val >> 40)));
|
||||||
|
stream.write((int) (0xFF & (val >> 48)));
|
||||||
|
stream.write((int) (0xFF & (val >> 56)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a copy of the given byte array in reverse order.
|
||||||
|
*/
|
||||||
|
public static byte[] reverseBytes(byte[] bytes) {
|
||||||
|
// We could use the XOR trick here but it's easier to understand if we don't. If we find this is really a
|
||||||
|
// performance issue the matter can be revisited.
|
||||||
|
byte[] buf = new byte[bytes.length];
|
||||||
|
for (int i = 0; i < bytes.length; i++)
|
||||||
|
buf[i] = bytes[bytes.length - 1 - i];
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates RIPEMD160(SHA256(input)). This is used in Address calculations.
|
||||||
|
*/
|
||||||
|
public static byte[] sha256hash160(byte[] input) {
|
||||||
|
byte[] sha256 = Sha256Hash.hash(input);
|
||||||
|
return Ripemd160.getHash(sha256);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Convert to a string path, starting with "M/" */
|
||||||
|
public static String formatHDPath(List<ChildNumber> path) {
|
||||||
|
StringJoiner joiner = new StringJoiner("/");
|
||||||
|
joiner.add("M");
|
||||||
|
for(ChildNumber number : path) {
|
||||||
|
joiner.add(number.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return joiner.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<ChildNumber> appendChild(List<ChildNumber> path, ChildNumber childNumber) {
|
||||||
|
List<ChildNumber> childPath = new ArrayList<>(path);
|
||||||
|
childPath.add(childNumber);
|
||||||
|
return Collections.unmodifiableList(childPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
static HMac createHmacSha512Digest(byte[] key) {
|
||||||
|
SHA512Digest digest = new SHA512Digest();
|
||||||
|
HMac hMac = new HMac(digest);
|
||||||
|
hMac.init(new KeyParameter(key));
|
||||||
|
return hMac;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] hmacSha512(HMac hmacSha512, byte[] input) {
|
||||||
|
hmacSha512.reset();
|
||||||
|
hmacSha512.update(input, 0, input.length);
|
||||||
|
byte[] out = new byte[64];
|
||||||
|
hmacSha512.doFinal(out, 0);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] hmacSha512(byte[] key, byte[] data) {
|
||||||
|
return hmacSha512(createHmacSha512Digest(key), data);
|
||||||
|
}
|
||||||
|
}
|
59
src/main/java/com/craigraw/drongo/WatchWallet.java
Normal file
59
src/main/java/com/craigraw/drongo/WatchWallet.java
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
package com.craigraw.drongo;
|
||||||
|
|
||||||
|
import com.craigraw.drongo.address.Address;
|
||||||
|
import com.craigraw.drongo.crypto.*;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class WatchWallet {
|
||||||
|
private static final int LOOK_AHEAD_LIMIT = 500;
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
private String extPubKey;
|
||||||
|
|
||||||
|
private OutputDescriptor outputDescriptor;
|
||||||
|
private DeterministicHierarchy hierarchy;
|
||||||
|
|
||||||
|
private HashMap<String,String> addresses = new HashMap<>(LOOK_AHEAD_LIMIT*2);
|
||||||
|
|
||||||
|
public WatchWallet(String name, String descriptor) {
|
||||||
|
this.name = name;
|
||||||
|
this.outputDescriptor = OutputDescriptor.getOutputDescriptor(descriptor);
|
||||||
|
this.hierarchy = new DeterministicHierarchy(outputDescriptor.getPubKey());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void initialiseAddresses() {
|
||||||
|
if(outputDescriptor.describesMultipleAddresses()) {
|
||||||
|
for(int index = 0; index <= LOOK_AHEAD_LIMIT; index++) {
|
||||||
|
List<ChildNumber> receivingDerivation = outputDescriptor.getReceivingDerivation(index);
|
||||||
|
Address address = getAddress(receivingDerivation);
|
||||||
|
addresses.put(address.toString(), Utils.formatHDPath(receivingDerivation));
|
||||||
|
}
|
||||||
|
|
||||||
|
for(int index = 0; index <= LOOK_AHEAD_LIMIT; index++) {
|
||||||
|
List<ChildNumber> changeDerivation = outputDescriptor.getChangeDerivation(index);
|
||||||
|
Address address = getAddress(changeDerivation);
|
||||||
|
addresses.put(address.toString(), Utils.formatHDPath(changeDerivation));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
List<ChildNumber> derivation = outputDescriptor.getChildDerivation();
|
||||||
|
Address address = getAddress(derivation);
|
||||||
|
addresses.put(address.toString(), Utils.formatHDPath(derivation));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Address getReceivingAddress(int index) {
|
||||||
|
return getAddress(outputDescriptor.getReceivingDerivation(index));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Address getChangeAddress(int index) {
|
||||||
|
return getAddress(outputDescriptor.getChangeDerivation(index));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Address getAddress(List<ChildNumber> path) {
|
||||||
|
DeterministicKey childKey = hierarchy.get(path);
|
||||||
|
return outputDescriptor.getAddress(childKey);
|
||||||
|
}
|
||||||
|
}
|
28
src/main/java/com/craigraw/drongo/address/Address.java
Normal file
28
src/main/java/com/craigraw/drongo/address/Address.java
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
package com.craigraw.drongo.address;
|
||||||
|
|
||||||
|
import com.craigraw.drongo.protocol.Base58;
|
||||||
|
import com.craigraw.drongo.protocol.Script;
|
||||||
|
|
||||||
|
public abstract class Address {
|
||||||
|
protected final byte[] pubKeyHash;
|
||||||
|
|
||||||
|
public Address(byte[] pubKeyHash) {
|
||||||
|
this.pubKeyHash = pubKeyHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getPubKeyHash() {
|
||||||
|
return pubKeyHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAddress() {
|
||||||
|
return Base58.encodeChecked(getVersion(), pubKeyHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String toString() {
|
||||||
|
return getAddress();
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract int getVersion();
|
||||||
|
|
||||||
|
public abstract Script getOutputScript();
|
||||||
|
}
|
30
src/main/java/com/craigraw/drongo/address/P2PKAddress.java
Normal file
30
src/main/java/com/craigraw/drongo/address/P2PKAddress.java
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
package com.craigraw.drongo.address;
|
||||||
|
|
||||||
|
import com.craigraw.drongo.Utils;
|
||||||
|
import com.craigraw.drongo.protocol.Script;
|
||||||
|
import com.craigraw.drongo.protocol.ScriptChunk;
|
||||||
|
import com.craigraw.drongo.protocol.ScriptOpCodes;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class P2PKAddress extends Address {
|
||||||
|
private byte[] pubKey;
|
||||||
|
|
||||||
|
public P2PKAddress(byte[] pubKey) {
|
||||||
|
super(Utils.sha256hash160(pubKey));
|
||||||
|
this.pubKey = pubKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getVersion() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Script getOutputScript() {
|
||||||
|
List<ScriptChunk> chunks = new ArrayList<>();
|
||||||
|
chunks.add(new ScriptChunk(pubKey.length, pubKey));
|
||||||
|
chunks.add(new ScriptChunk(ScriptOpCodes.OP_CHECKSIG, null));
|
||||||
|
|
||||||
|
return new Script(chunks);
|
||||||
|
}
|
||||||
|
}
|
29
src/main/java/com/craigraw/drongo/address/P2PKHAddress.java
Normal file
29
src/main/java/com/craigraw/drongo/address/P2PKHAddress.java
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
package com.craigraw.drongo.address;
|
||||||
|
|
||||||
|
import com.craigraw.drongo.protocol.Script;
|
||||||
|
import com.craigraw.drongo.protocol.ScriptChunk;
|
||||||
|
import com.craigraw.drongo.protocol.ScriptOpCodes;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class P2PKHAddress extends Address {
|
||||||
|
public P2PKHAddress(byte[] pubKeyHash) {
|
||||||
|
super(pubKeyHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getVersion() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Script getOutputScript() {
|
||||||
|
List<ScriptChunk> chunks = new ArrayList<>();
|
||||||
|
chunks.add(new ScriptChunk(ScriptOpCodes.OP_DUP, null));
|
||||||
|
chunks.add(new ScriptChunk(ScriptOpCodes.OP_HASH160, null));
|
||||||
|
chunks.add(new ScriptChunk(pubKeyHash.length, pubKeyHash));
|
||||||
|
chunks.add(new ScriptChunk(ScriptOpCodes.OP_EQUALVERIFY, null));
|
||||||
|
chunks.add(new ScriptChunk(ScriptOpCodes.OP_CHECKSIG, null));
|
||||||
|
|
||||||
|
return new Script(chunks);
|
||||||
|
}
|
||||||
|
}
|
32
src/main/java/com/craigraw/drongo/address/P2SHAddress.java
Normal file
32
src/main/java/com/craigraw/drongo/address/P2SHAddress.java
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
package com.craigraw.drongo.address;
|
||||||
|
|
||||||
|
import com.craigraw.drongo.Utils;
|
||||||
|
import com.craigraw.drongo.protocol.Script;
|
||||||
|
import com.craigraw.drongo.protocol.ScriptChunk;
|
||||||
|
import com.craigraw.drongo.protocol.ScriptOpCodes;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class P2SHAddress extends Address {
|
||||||
|
public P2SHAddress(byte[] pubKeyHash) {
|
||||||
|
super(pubKeyHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getVersion() {
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Script getOutputScript() {
|
||||||
|
List<ScriptChunk> chunks = new ArrayList<>();
|
||||||
|
chunks.add(new ScriptChunk(ScriptOpCodes.OP_HASH160, null));
|
||||||
|
chunks.add(new ScriptChunk(pubKeyHash.length, pubKeyHash));
|
||||||
|
chunks.add(new ScriptChunk(ScriptOpCodes.OP_EQUAL, null));
|
||||||
|
|
||||||
|
return new Script(chunks);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static P2SHAddress fromProgram(byte[] program) {
|
||||||
|
return new P2SHAddress(Utils.sha256hash160(program));
|
||||||
|
}
|
||||||
|
}
|
32
src/main/java/com/craigraw/drongo/address/P2WPKHAddress.java
Normal file
32
src/main/java/com/craigraw/drongo/address/P2WPKHAddress.java
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
package com.craigraw.drongo.address;
|
||||||
|
|
||||||
|
import com.craigraw.drongo.protocol.Bech32;
|
||||||
|
import com.craigraw.drongo.protocol.Script;
|
||||||
|
import com.craigraw.drongo.protocol.ScriptChunk;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class P2WPKHAddress extends Address {
|
||||||
|
public static final String HRP = "bc";
|
||||||
|
|
||||||
|
public P2WPKHAddress(byte[] pubKeyHash) {
|
||||||
|
super(pubKeyHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getVersion() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAddress() {
|
||||||
|
return Bech32.encode(HRP, getVersion(), pubKeyHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Script getOutputScript() {
|
||||||
|
List<ScriptChunk> chunks = new ArrayList<>();
|
||||||
|
chunks.add(new ScriptChunk(Script.encodeToOpN(getVersion()), null));
|
||||||
|
chunks.add(new ScriptChunk(pubKeyHash.length, pubKeyHash));
|
||||||
|
|
||||||
|
return new Script(chunks);
|
||||||
|
}
|
||||||
|
}
|
61
src/main/java/com/craigraw/drongo/crypto/ChildNumber.java
Normal file
61
src/main/java/com/craigraw/drongo/crypto/ChildNumber.java
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
package com.craigraw.drongo.crypto;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
public class ChildNumber {
|
||||||
|
/**
|
||||||
|
* The bit that's set in the child number to indicate whether this key is "hardened". Given a hardened key, it is
|
||||||
|
* not possible to derive a child public key if you know only the hardened public key. With a non-hardened key this
|
||||||
|
* is possible, so you can derive trees of public keys given only a public parent, but the downside is that it's
|
||||||
|
* possible to leak private keys if you disclose a parent public key and a child private key (elliptic curve maths
|
||||||
|
* allows you to work upwards).
|
||||||
|
*/
|
||||||
|
public static final int HARDENED_BIT = 0x80000000;
|
||||||
|
|
||||||
|
public static final ChildNumber ZERO = new ChildNumber(0);
|
||||||
|
public static final ChildNumber ZERO_HARDENED = new ChildNumber(0, true);
|
||||||
|
public static final ChildNumber ONE = new ChildNumber(1);
|
||||||
|
public static final ChildNumber ONE_HARDENED = new ChildNumber(1, true);
|
||||||
|
|
||||||
|
/** Integer i as per BIP 32 spec, including the MSB denoting derivation type (0 = public, 1 = private) **/
|
||||||
|
private final int i;
|
||||||
|
|
||||||
|
public ChildNumber(int childNumber, boolean isHardened) {
|
||||||
|
if (hasHardenedBit(childNumber))
|
||||||
|
throw new IllegalArgumentException("Most significant bit is reserved and shouldn't be set: " + childNumber);
|
||||||
|
i = isHardened ? (childNumber | HARDENED_BIT) : childNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChildNumber(int i) {
|
||||||
|
this.i = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean hasHardenedBit(int a) {
|
||||||
|
return (a & HARDENED_BIT) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isHardened() {
|
||||||
|
return hasHardenedBit(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int num() {
|
||||||
|
return i & (~HARDENED_BIT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the uint32 encoded form of the path element, including the most significant bit. */
|
||||||
|
public int i() { return i; }
|
||||||
|
|
||||||
|
public String toString() {
|
||||||
|
return String.format(Locale.US, "%d%s", num(), isHardened() ? "H" : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
return i == ((ChildNumber)o).i;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int hashCode() {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
package com.craigraw.drongo.crypto;
|
||||||
|
|
||||||
|
import com.craigraw.drongo.Utils;
|
||||||
|
import com.craigraw.drongo.protocol.Base58;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class DeterministicHierarchy {
|
||||||
|
private final Map<List<ChildNumber>, DeterministicKey> keys = new HashMap<>();
|
||||||
|
private final List<ChildNumber> rootPath;
|
||||||
|
// Keep track of how many child keys each node has. This is kind of weak.
|
||||||
|
private final Map<List<ChildNumber>, ChildNumber> lastChildNumbers = new HashMap<>();
|
||||||
|
|
||||||
|
public DeterministicHierarchy(DeterministicKey rootKey) {
|
||||||
|
putKey(rootKey);
|
||||||
|
rootPath = rootKey.getPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
public final void putKey(DeterministicKey key) {
|
||||||
|
List<ChildNumber> path = key.getPath();
|
||||||
|
// Update our tracking of what the next child in each branch of the tree should be. Just assume that keys are
|
||||||
|
// inserted in order here.
|
||||||
|
final DeterministicKey parent = key.getParent();
|
||||||
|
if (parent != null)
|
||||||
|
lastChildNumbers.put(parent.getPath(), key.getChildNumber());
|
||||||
|
keys.put(path, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a key for the given path, optionally creating it.
|
||||||
|
*
|
||||||
|
* @param path the path to the key
|
||||||
|
* @return next newly created key using the child derivation function
|
||||||
|
* @throws IllegalArgumentException if create is false and the path was not found.
|
||||||
|
*/
|
||||||
|
public DeterministicKey get(List<ChildNumber> path) {
|
||||||
|
if(!keys.containsKey(path)) {
|
||||||
|
if(path.size() == 0) {
|
||||||
|
throw new IllegalArgumentException("Can't derive the master key: nothing to derive from.");
|
||||||
|
}
|
||||||
|
|
||||||
|
DeterministicKey parent = get(path.subList(0, path.size() - 1));
|
||||||
|
putKey(HDKeyDerivation.deriveChildKey(parent, path.get(path.size() - 1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return keys.get(path);
|
||||||
|
}
|
||||||
|
}
|
113
src/main/java/com/craigraw/drongo/crypto/DeterministicKey.java
Normal file
113
src/main/java/com/craigraw/drongo/crypto/DeterministicKey.java
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
package com.craigraw.drongo.crypto;
|
||||||
|
|
||||||
|
|
||||||
|
import com.craigraw.drongo.Utils;
|
||||||
|
import com.craigraw.drongo.protocol.Base58;
|
||||||
|
import com.craigraw.drongo.protocol.Sha256Hash;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class DeterministicKey extends ECKey {
|
||||||
|
private final DeterministicKey parent;
|
||||||
|
private final List<ChildNumber> childNumberPath;
|
||||||
|
private final int depth;
|
||||||
|
private int parentFingerprint; // 0 if this key is root node of key hierarchy
|
||||||
|
|
||||||
|
/** 32 bytes */
|
||||||
|
private final byte[] chainCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a key from its components, including its public key data and possibly-redundant
|
||||||
|
* information about its parent key. Invoked when deserializing, but otherwise not something that
|
||||||
|
* you normally should use.
|
||||||
|
*/
|
||||||
|
public DeterministicKey(List<ChildNumber> childNumberPath,
|
||||||
|
byte[] chainCode,
|
||||||
|
LazyECPoint publicAsPoint,
|
||||||
|
int depth,
|
||||||
|
int parentFingerprint) {
|
||||||
|
super(compressPoint(publicAsPoint));
|
||||||
|
if(chainCode.length != 32) {
|
||||||
|
throw new IllegalArgumentException("Chaincode not 32 bytes in length");
|
||||||
|
}
|
||||||
|
this.parent = null;
|
||||||
|
this.childNumberPath = childNumberPath;
|
||||||
|
this.chainCode = Arrays.copyOf(chainCode, chainCode.length);
|
||||||
|
this.depth = depth;
|
||||||
|
this.parentFingerprint = parentFingerprint;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DeterministicKey(List<ChildNumber> childNumberPath,
|
||||||
|
byte[] chainCode,
|
||||||
|
LazyECPoint publicAsPoint,
|
||||||
|
DeterministicKey parent) {
|
||||||
|
super(compressPoint(publicAsPoint));
|
||||||
|
if(chainCode.length != 32) {
|
||||||
|
throw new IllegalArgumentException("Chaincode not 32 bytes in length");
|
||||||
|
}
|
||||||
|
this.parent = parent;
|
||||||
|
this.childNumberPath = childNumberPath;
|
||||||
|
this.chainCode = Arrays.copyOf(chainCode, chainCode.length);
|
||||||
|
this.depth = parent == null ? 0 : parent.depth + 1;
|
||||||
|
this.parentFingerprint = (parent != null) ? parent.getFingerprint() : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return this key's depth in the hierarchy, where the root node is at depth zero.
|
||||||
|
* This may be different than the number of segments in the path if this key was
|
||||||
|
* deserialized without access to its parent.
|
||||||
|
*/
|
||||||
|
public int getDepth() {
|
||||||
|
return depth;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the first 32 bits of the result of {@link #getIdentifier()}. */
|
||||||
|
public int getFingerprint() {
|
||||||
|
// TODO: why is this different than armory's fingerprint? BIP 32: "The first 32 bits of the identifier are called the fingerprint."
|
||||||
|
return ByteBuffer.wrap(Arrays.copyOfRange(getIdentifier(), 0, 4)).getInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns RIPE-MD160(SHA256(pub key bytes)).
|
||||||
|
*/
|
||||||
|
public byte[] getIdentifier() {
|
||||||
|
return Utils.sha256hash160(getPubKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the path through some DeterministicHierarchy which reaches this keys position in the tree.
|
||||||
|
* A path can be written as 0/1/0 which means the first child of the root, the second child of that node, then
|
||||||
|
* the first child of that node.
|
||||||
|
*/
|
||||||
|
public List<ChildNumber> getPath() {
|
||||||
|
return childNumberPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DeterministicKey getParent() {
|
||||||
|
return parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the last element of the path returned by {@link DeterministicKey#getPath()} */
|
||||||
|
public ChildNumber getChildNumber() {
|
||||||
|
return childNumberPath.size() == 0 ? ChildNumber.ZERO : childNumberPath.get(childNumberPath.size() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getChainCode() {
|
||||||
|
return chainCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String toBase58(byte[] ser) {
|
||||||
|
return Base58.encode(addChecksum(ser));
|
||||||
|
}
|
||||||
|
|
||||||
|
static byte[] addChecksum(byte[] input) {
|
||||||
|
int inputLength = input.length;
|
||||||
|
byte[] checksummed = new byte[inputLength + 4];
|
||||||
|
System.arraycopy(input, 0, checksummed, 0, inputLength);
|
||||||
|
byte[] checksum = Sha256Hash.hashTwice(input);
|
||||||
|
System.arraycopy(checksum, 0, checksummed, inputLength, 4);
|
||||||
|
return checksummed;
|
||||||
|
}
|
||||||
|
}
|
101
src/main/java/com/craigraw/drongo/crypto/ECKey.java
Normal file
101
src/main/java/com/craigraw/drongo/crypto/ECKey.java
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
package com.craigraw.drongo.crypto;
|
||||||
|
|
||||||
|
import com.craigraw.drongo.Utils;
|
||||||
|
import org.bouncycastle.asn1.x9.X9ECParameters;
|
||||||
|
import org.bouncycastle.crypto.ec.CustomNamedCurves;
|
||||||
|
import org.bouncycastle.crypto.params.ECDomainParameters;
|
||||||
|
import org.bouncycastle.math.ec.ECPoint;
|
||||||
|
import org.bouncycastle.math.ec.FixedPointCombMultiplier;
|
||||||
|
import org.bouncycastle.math.ec.FixedPointUtil;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
|
||||||
|
public class ECKey {
|
||||||
|
// The parameters of the secp256k1 curve that Bitcoin uses.
|
||||||
|
private static final X9ECParameters CURVE_PARAMS = CustomNamedCurves.getByName("secp256k1");
|
||||||
|
|
||||||
|
/** The parameters of the secp256k1 curve that Bitcoin uses. */
|
||||||
|
public static final ECDomainParameters CURVE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Equal to CURVE.getN().shiftRight(1), used for canonicalising the S value of a signature. If you aren't
|
||||||
|
* sure what this is about, you can ignore it.
|
||||||
|
*/
|
||||||
|
public static final BigInteger HALF_CURVE_ORDER;
|
||||||
|
|
||||||
|
private static final SecureRandom secureRandom;
|
||||||
|
|
||||||
|
static {
|
||||||
|
// Tell Bouncy Castle to precompute data that's needed during secp256k1 calculations.
|
||||||
|
FixedPointUtil.precompute(CURVE_PARAMS.getG());
|
||||||
|
CURVE = new ECDomainParameters(CURVE_PARAMS.getCurve(), CURVE_PARAMS.getG(), CURVE_PARAMS.getN(),
|
||||||
|
CURVE_PARAMS.getH());
|
||||||
|
HALF_CURVE_ORDER = CURVE_PARAMS.getN().shiftRight(1);
|
||||||
|
secureRandom = new SecureRandom();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final LazyECPoint pub;
|
||||||
|
|
||||||
|
private byte[] pubKeyHash;
|
||||||
|
|
||||||
|
protected ECKey(LazyECPoint pub) {
|
||||||
|
this.pub = pub;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility for compressing an elliptic curve point. Returns the same point if it's already compressed.
|
||||||
|
* See the ECKey class docs for a discussion of point compression.
|
||||||
|
*/
|
||||||
|
public static ECPoint compressPoint(ECPoint point) {
|
||||||
|
return getPointWithCompression(point, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LazyECPoint compressPoint(LazyECPoint point) {
|
||||||
|
return point.isCompressed() ? point : new LazyECPoint(compressPoint(point.get()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ECPoint getPointWithCompression(ECPoint point, boolean compressed) {
|
||||||
|
if (point.isCompressed() == compressed)
|
||||||
|
return point;
|
||||||
|
point = point.normalize();
|
||||||
|
BigInteger x = point.getAffineXCoord().toBigInteger();
|
||||||
|
BigInteger y = point.getAffineYCoord().toBigInteger();
|
||||||
|
return CURVE.getCurve().createPoint(x, y, compressed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the raw public key value. This appears in transaction scriptSigs. Note that this is <b>not</b> the same
|
||||||
|
* as the pubKeyHash/address.
|
||||||
|
*/
|
||||||
|
public byte[] getPubKey() {
|
||||||
|
return pub.getEncoded();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gets the public key in the form of an elliptic curve point object from Bouncy Castle. */
|
||||||
|
public ECPoint getPubKeyPoint() {
|
||||||
|
return pub.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gets the hash160 form of the public key (as seen in addresses). */
|
||||||
|
public byte[] getPubKeyHash() {
|
||||||
|
if (pubKeyHash == null)
|
||||||
|
pubKeyHash = Utils.sha256hash160(this.pub.getEncoded());
|
||||||
|
return pubKeyHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns public key point from the given private key. To convert a byte array into a BigInteger,
|
||||||
|
* use {@code new BigInteger(1, bytes);}
|
||||||
|
*/
|
||||||
|
public static ECPoint publicPointFromPrivate(BigInteger privKey) {
|
||||||
|
/*
|
||||||
|
* TODO: FixedPointCombMultiplier currently doesn't support scalars longer than the group order,
|
||||||
|
* but that could change in future versions.
|
||||||
|
*/
|
||||||
|
if (privKey.bitLength() > CURVE.getN().bitLength()) {
|
||||||
|
privKey = privKey.mod(CURVE.getN());
|
||||||
|
}
|
||||||
|
return new FixedPointCombMultiplier().multiply(CURVE.getG(), privKey);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
52
src/main/java/com/craigraw/drongo/crypto/LazyECPoint.java
Normal file
52
src/main/java/com/craigraw/drongo/crypto/LazyECPoint.java
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
package com.craigraw.drongo.crypto;
|
||||||
|
|
||||||
|
import org.bouncycastle.math.ec.ECCurve;
|
||||||
|
import org.bouncycastle.math.ec.ECPoint;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
public class LazyECPoint {
|
||||||
|
// If curve is set, bits is also set. If curve is unset, point is set and bits is unset. Point can be set along
|
||||||
|
// with curve and bits when the cached form has been accessed and thus must have been converted.
|
||||||
|
|
||||||
|
private final ECCurve curve;
|
||||||
|
private final byte[] bits;
|
||||||
|
|
||||||
|
// This field is effectively final - once set it won't change again. However it can be set after
|
||||||
|
// construction.
|
||||||
|
private ECPoint point;
|
||||||
|
|
||||||
|
public LazyECPoint(ECCurve curve, byte[] bits) {
|
||||||
|
this.curve = curve;
|
||||||
|
this.bits = bits;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LazyECPoint(ECPoint point) {
|
||||||
|
this.point = point;
|
||||||
|
this.curve = null;
|
||||||
|
this.bits = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ECPoint get() {
|
||||||
|
if (point == null)
|
||||||
|
point = curve.decodePoint(bits);
|
||||||
|
return point;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delegated methods.
|
||||||
|
|
||||||
|
public ECPoint getDetachedPoint() {
|
||||||
|
return get().getDetachedPoint();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isCompressed() {
|
||||||
|
return get().isCompressed();
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getEncoded() {
|
||||||
|
if (bits != null)
|
||||||
|
return Arrays.copyOf(bits, bits.length);
|
||||||
|
else
|
||||||
|
return get().getEncoded();
|
||||||
|
}
|
||||||
|
}
|
216
src/main/java/com/craigraw/drongo/protocol/Base58.java
Normal file
216
src/main/java/com/craigraw/drongo/protocol/Base58.java
Normal file
|
@ -0,0 +1,216 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2011 Google Inc.
|
||||||
|
* Copyright 2018 Andreas Schildbach
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.craigraw.drongo.protocol;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base58 is a way to encode Bitcoin addresses (or arbitrary data) as alphanumeric strings.
|
||||||
|
* <p>
|
||||||
|
* Note that this is not the same base58 as used by Flickr, which you may find referenced around the Internet.
|
||||||
|
* <p>
|
||||||
|
* Satoshi explains: why base-58 instead of standard base-64 encoding?
|
||||||
|
* <ul>
|
||||||
|
* <li>Don't want 0OIl characters that look the same in some fonts and
|
||||||
|
* could be used to create visually identical looking account numbers.</li>
|
||||||
|
* <li>A string with non-alphanumeric characters is not as easily accepted as an account number.</li>
|
||||||
|
* <li>E-mail usually won't line-break if there's no punctuation to break at.</li>
|
||||||
|
* <li>Doubleclicking selects the whole number as one word if it's all alphanumeric.</li>
|
||||||
|
* </ul>
|
||||||
|
* <p>
|
||||||
|
* However, note that the encoding/decoding runs in O(n²) time, so it is not useful for large data.
|
||||||
|
* <p>
|
||||||
|
* The basic idea of the encoding is to treat the data bytes as a large number represented using
|
||||||
|
* base-256 digits, convert the number to be represented using base-58 digits, preserve the exact
|
||||||
|
* number of leading zeros (which are otherwise lost during the mathematical operations on the
|
||||||
|
* numbers), and finally represent the resulting base-58 digits as alphanumeric ASCII characters.
|
||||||
|
*/
|
||||||
|
public class Base58 {
|
||||||
|
public static final char[] ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz".toCharArray();
|
||||||
|
private static final char ENCODED_ZERO = ALPHABET[0];
|
||||||
|
private static final int[] INDEXES = new int[128];
|
||||||
|
static {
|
||||||
|
Arrays.fill(INDEXES, -1);
|
||||||
|
for (int i = 0; i < ALPHABET.length; i++) {
|
||||||
|
INDEXES[ALPHABET[i]] = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes the given bytes as a base58 string (no checksum is appended).
|
||||||
|
*
|
||||||
|
* @param input the bytes to encode
|
||||||
|
* @return the base58-encoded string
|
||||||
|
*/
|
||||||
|
public static String encode(byte[] input) {
|
||||||
|
if (input.length == 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
// Count leading zeros.
|
||||||
|
int zeros = 0;
|
||||||
|
while (zeros < input.length && input[zeros] == 0) {
|
||||||
|
++zeros;
|
||||||
|
}
|
||||||
|
// Convert base-256 digits to base-58 digits (plus conversion to ASCII characters)
|
||||||
|
input = Arrays.copyOf(input, input.length); // since we modify it in-place
|
||||||
|
char[] encoded = new char[input.length * 2]; // upper bound
|
||||||
|
int outputStart = encoded.length;
|
||||||
|
for (int inputStart = zeros; inputStart < input.length; ) {
|
||||||
|
encoded[--outputStart] = ALPHABET[divmod(input, inputStart, 256, 58)];
|
||||||
|
if (input[inputStart] == 0) {
|
||||||
|
++inputStart; // optimization - skip leading zeros
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Preserve exactly as many leading encoded zeros in output as there were leading zeros in input.
|
||||||
|
while (outputStart < encoded.length && encoded[outputStart] == ENCODED_ZERO) {
|
||||||
|
++outputStart;
|
||||||
|
}
|
||||||
|
while (--zeros >= 0) {
|
||||||
|
encoded[--outputStart] = ENCODED_ZERO;
|
||||||
|
}
|
||||||
|
// Return encoded string (including encoded leading zeros).
|
||||||
|
return new String(encoded, outputStart, encoded.length - outputStart);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes the bytes as a base58 string. A checksum is appended.
|
||||||
|
*
|
||||||
|
* @param payload the bytes to encode, e.g. pubkey hash
|
||||||
|
* @return the base58-encoded string
|
||||||
|
*/
|
||||||
|
public static String encodeChecked(byte[] payload) {
|
||||||
|
// A stringified buffer is:
|
||||||
|
// data bytes + 4 bytes check code (a truncated hash)
|
||||||
|
byte[] addressBytes = new byte[payload.length + 4];
|
||||||
|
System.arraycopy(payload, 0, addressBytes, 0, payload.length);
|
||||||
|
byte[] checksum = Sha256Hash.hashTwice(addressBytes, 0, payload.length);
|
||||||
|
System.arraycopy(checksum, 0, addressBytes, payload.length, 4);
|
||||||
|
return Base58.encode(addressBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes the given version and bytes as a base58 string. A checksum is appended.
|
||||||
|
*
|
||||||
|
* @param version the version to encode
|
||||||
|
* @param payload the bytes to encode, e.g. pubkey hash
|
||||||
|
* @return the base58-encoded string
|
||||||
|
*/
|
||||||
|
public static String encodeChecked(int version, byte[] payload) {
|
||||||
|
if (version < 0 || version > 255)
|
||||||
|
throw new IllegalArgumentException("Version not in range.");
|
||||||
|
|
||||||
|
// A stringified buffer is:
|
||||||
|
// 1 byte version + data bytes + 4 bytes check code (a truncated hash)
|
||||||
|
byte[] addressBytes = new byte[1 + payload.length + 4];
|
||||||
|
addressBytes[0] = (byte) version;
|
||||||
|
System.arraycopy(payload, 0, addressBytes, 1, payload.length);
|
||||||
|
byte[] checksum = Sha256Hash.hashTwice(addressBytes, 0, payload.length + 1);
|
||||||
|
System.arraycopy(checksum, 0, addressBytes, payload.length + 1, 4);
|
||||||
|
return Base58.encode(addressBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes the given base58 string into the original data bytes.
|
||||||
|
*
|
||||||
|
* @param input the base58-encoded string to decode
|
||||||
|
* @return the decoded data bytes
|
||||||
|
*/
|
||||||
|
public static byte[] decode(String input) {
|
||||||
|
if (input.length() == 0) {
|
||||||
|
return new byte[0];
|
||||||
|
}
|
||||||
|
// Convert the base58-encoded ASCII chars to a base58 byte sequence (base58 digits).
|
||||||
|
byte[] input58 = new byte[input.length()];
|
||||||
|
for (int i = 0; i < input.length(); ++i) {
|
||||||
|
char c = input.charAt(i);
|
||||||
|
int digit = c < 128 ? INDEXES[c] : -1;
|
||||||
|
if (digit < 0) {
|
||||||
|
throw new ProtocolException("Invalid character " + c + " at position " + i);
|
||||||
|
}
|
||||||
|
input58[i] = (byte) digit;
|
||||||
|
}
|
||||||
|
// Count leading zeros.
|
||||||
|
int zeros = 0;
|
||||||
|
while (zeros < input58.length && input58[zeros] == 0) {
|
||||||
|
++zeros;
|
||||||
|
}
|
||||||
|
// Convert base-58 digits to base-256 digits.
|
||||||
|
byte[] decoded = new byte[input.length()];
|
||||||
|
int outputStart = decoded.length;
|
||||||
|
for (int inputStart = zeros; inputStart < input58.length; ) {
|
||||||
|
decoded[--outputStart] = divmod(input58, inputStart, 58, 256);
|
||||||
|
if (input58[inputStart] == 0) {
|
||||||
|
++inputStart; // optimization - skip leading zeros
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Ignore extra leading zeroes that were added during the calculation.
|
||||||
|
while (outputStart < decoded.length && decoded[outputStart] == 0) {
|
||||||
|
++outputStart;
|
||||||
|
}
|
||||||
|
// Return decoded data (including original number of leading zeros).
|
||||||
|
return Arrays.copyOfRange(decoded, outputStart - zeros, decoded.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BigInteger decodeToBigInteger(String input) {
|
||||||
|
return new BigInteger(1, decode(input));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes the given base58 string into the original data bytes, using the checksum in the
|
||||||
|
* last 4 bytes of the decoded data to verify that the rest are correct. The checksum is
|
||||||
|
* removed from the returned data.
|
||||||
|
*
|
||||||
|
* @param input the base58-encoded string to decode (which should include the checksum)
|
||||||
|
*/
|
||||||
|
public static byte[] decodeChecked(String input) {
|
||||||
|
byte[] decoded = decode(input);
|
||||||
|
if (decoded.length < 4)
|
||||||
|
throw new ProtocolException("Input too short: " + decoded.length);
|
||||||
|
byte[] data = Arrays.copyOfRange(decoded, 0, decoded.length - 4);
|
||||||
|
byte[] checksum = Arrays.copyOfRange(decoded, decoded.length - 4, decoded.length);
|
||||||
|
byte[] actualChecksum = Arrays.copyOfRange(Sha256Hash.hashTwice(data), 0, 4);
|
||||||
|
if (!Arrays.equals(checksum, actualChecksum))
|
||||||
|
throw new ProtocolException("Invalid checksum");
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Divides a number, represented as an array of bytes each containing a single digit
|
||||||
|
* in the specified base, by the given divisor. The given number is modified in-place
|
||||||
|
* to contain the quotient, and the return value is the remainder.
|
||||||
|
*
|
||||||
|
* @param number the number to divide
|
||||||
|
* @param firstDigit the index within the array of the first non-zero digit
|
||||||
|
* (this is used for optimization by skipping the leading zeros)
|
||||||
|
* @param base the base in which the number's digits are represented (up to 256)
|
||||||
|
* @param divisor the number to divide by (up to 256)
|
||||||
|
* @return the remainder of the division operation
|
||||||
|
*/
|
||||||
|
private static byte divmod(byte[] number, int firstDigit, int base, int divisor) {
|
||||||
|
// this is just long division which accounts for the base of the input digits
|
||||||
|
int remainder = 0;
|
||||||
|
for (int i = firstDigit; i < number.length; i++) {
|
||||||
|
int digit = (int) number[i] & 0xFF;
|
||||||
|
int temp = remainder * base + digit;
|
||||||
|
number[i] = (byte) (temp / divisor);
|
||||||
|
remainder = temp % divisor;
|
||||||
|
}
|
||||||
|
return (byte) remainder;
|
||||||
|
}
|
||||||
|
}
|
209
src/main/java/com/craigraw/drongo/protocol/Bech32.java
Normal file
209
src/main/java/com/craigraw/drongo/protocol/Bech32.java
Normal file
|
@ -0,0 +1,209 @@
|
||||||
|
package com.craigraw.drongo.protocol;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright 2018 Coinomi Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
public class Bech32 {
|
||||||
|
/** The Bech32 character set for encoding. */
|
||||||
|
private static final String CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
||||||
|
|
||||||
|
/** The Bech32 character set for decoding. */
|
||||||
|
private static final byte[] CHARSET_REV = {
|
||||||
|
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||||
|
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||||
|
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||||
|
15, -1, 10, 17, 21, 20, 26, 30, 7, 5, -1, -1, -1, -1, -1, -1,
|
||||||
|
-1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1,
|
||||||
|
1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1,
|
||||||
|
-1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1,
|
||||||
|
1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1
|
||||||
|
};
|
||||||
|
|
||||||
|
public static class Bech32Data {
|
||||||
|
public final String hrp;
|
||||||
|
public final byte[] data;
|
||||||
|
|
||||||
|
private Bech32Data(final String hrp, final byte[] data) {
|
||||||
|
this.hrp = hrp;
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Find the polynomial with value coefficients mod the generator as 30-bit. */
|
||||||
|
private static int polymod(final byte[] values) {
|
||||||
|
int c = 1;
|
||||||
|
for (byte v_i: values) {
|
||||||
|
int c0 = (c >>> 25) & 0xff;
|
||||||
|
c = ((c & 0x1ffffff) << 5) ^ (v_i & 0xff);
|
||||||
|
if ((c0 & 1) != 0) c ^= 0x3b6a57b2;
|
||||||
|
if ((c0 & 2) != 0) c ^= 0x26508e6d;
|
||||||
|
if ((c0 & 4) != 0) c ^= 0x1ea119fa;
|
||||||
|
if ((c0 & 8) != 0) c ^= 0x3d4233dd;
|
||||||
|
if ((c0 & 16) != 0) c ^= 0x2a1462b3;
|
||||||
|
}
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Expand a HRP for use in checksum computation. */
|
||||||
|
private static byte[] expandHrp(final String hrp) {
|
||||||
|
int hrpLength = hrp.length();
|
||||||
|
byte ret[] = new byte[hrpLength * 2 + 1];
|
||||||
|
for (int i = 0; i < hrpLength; ++i) {
|
||||||
|
int c = hrp.charAt(i) & 0x7f; // Limit to standard 7-bit ASCII
|
||||||
|
ret[i] = (byte) ((c >>> 5) & 0x07);
|
||||||
|
ret[i + hrpLength + 1] = (byte) (c & 0x1f);
|
||||||
|
}
|
||||||
|
ret[hrpLength] = 0;
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Verify a checksum. */
|
||||||
|
private static boolean verifyChecksum(final String hrp, final byte[] values) {
|
||||||
|
byte[] hrpExpanded = expandHrp(hrp);
|
||||||
|
byte[] combined = new byte[hrpExpanded.length + values.length];
|
||||||
|
System.arraycopy(hrpExpanded, 0, combined, 0, hrpExpanded.length);
|
||||||
|
System.arraycopy(values, 0, combined, hrpExpanded.length, values.length);
|
||||||
|
return polymod(combined) == 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create a checksum. */
|
||||||
|
private static byte[] createChecksum(final String hrp, final byte[] values) {
|
||||||
|
byte[] hrpExpanded = expandHrp(hrp);
|
||||||
|
byte[] enc = new byte[hrpExpanded.length + values.length + 6];
|
||||||
|
System.arraycopy(hrpExpanded, 0, enc, 0, hrpExpanded.length);
|
||||||
|
System.arraycopy(values, 0, enc, hrpExpanded.length, values.length);
|
||||||
|
int mod = polymod(enc) ^ 1;
|
||||||
|
byte[] ret = new byte[6];
|
||||||
|
for (int i = 0; i < 6; ++i) {
|
||||||
|
ret[i] = (byte) ((mod >>> (5 * (5 - i))) & 31);
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Encode a Bech32 string. */
|
||||||
|
public static String encode(final Bech32Data bech32) {
|
||||||
|
return encode(bech32.hrp, bech32.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Encode a Bech32 string. */
|
||||||
|
public static String encode(String hrp, int version, final byte[] values) {
|
||||||
|
return encode(hrp, encode(0, values));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Encode a Bech32 string. */
|
||||||
|
public static String encode(String hrp, final byte[] values) {
|
||||||
|
if(hrp.length() < 1) {
|
||||||
|
throw new ProtocolException("Human-readable part is too short");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(hrp.length() > 83) {
|
||||||
|
throw new ProtocolException("Human-readable part is too long");
|
||||||
|
}
|
||||||
|
|
||||||
|
hrp = hrp.toLowerCase(Locale.ROOT);
|
||||||
|
byte[] checksum = createChecksum(hrp, values);
|
||||||
|
byte[] combined = new byte[values.length + checksum.length];
|
||||||
|
System.arraycopy(values, 0, combined, 0, values.length);
|
||||||
|
System.arraycopy(checksum, 0, combined, values.length, checksum.length);
|
||||||
|
StringBuilder sb = new StringBuilder(hrp.length() + 1 + combined.length);
|
||||||
|
sb.append(hrp);
|
||||||
|
sb.append('1');
|
||||||
|
for (byte b : combined) {
|
||||||
|
sb.append(CHARSET.charAt(b));
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Decode a Bech32 string. */
|
||||||
|
public static Bech32Data decode(final String str) {
|
||||||
|
boolean lower = false, upper = false;
|
||||||
|
if (str.length() < 8)
|
||||||
|
throw new ProtocolException("Input too short: " + str.length());
|
||||||
|
if (str.length() > 90)
|
||||||
|
throw new ProtocolException("Input too long: " + str.length());
|
||||||
|
for (int i = 0; i < str.length(); ++i) {
|
||||||
|
char c = str.charAt(i);
|
||||||
|
if (c < 33 || c > 126) throw new ProtocolException("Invalid character " + c + " at position " + i);
|
||||||
|
if (c >= 'a' && c <= 'z') {
|
||||||
|
if (upper)
|
||||||
|
throw new ProtocolException("Invalid character " + c + " at position " + i);
|
||||||
|
lower = true;
|
||||||
|
}
|
||||||
|
if (c >= 'A' && c <= 'Z') {
|
||||||
|
if (lower)
|
||||||
|
throw new ProtocolException("Invalid character " + c + " at position " + i);
|
||||||
|
upper = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final int pos = str.lastIndexOf('1');
|
||||||
|
if (pos < 1) throw new ProtocolException("Missing human-readable part");
|
||||||
|
final int dataPartLength = str.length() - 1 - pos;
|
||||||
|
if (dataPartLength < 6) throw new ProtocolException("Data part too short: " + dataPartLength);
|
||||||
|
byte[] values = new byte[dataPartLength];
|
||||||
|
for (int i = 0; i < dataPartLength; ++i) {
|
||||||
|
char c = str.charAt(i + pos + 1);
|
||||||
|
if (CHARSET_REV[c] == -1) throw new ProtocolException("Invalid character " + c + " at position " + i);
|
||||||
|
values[i] = CHARSET_REV[c];
|
||||||
|
}
|
||||||
|
String hrp = str.substring(0, pos).toLowerCase(Locale.ROOT);
|
||||||
|
if (!verifyChecksum(hrp, values)) throw new ProtocolException("Invalid checksum");
|
||||||
|
return new Bech32Data(hrp, Arrays.copyOfRange(values, 0, values.length - 6));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] encode(int witnessVersion, byte[] witnessProgram) {
|
||||||
|
byte[] convertedProgram = convertBits(witnessProgram, 0, witnessProgram.length, 8, 5, true);
|
||||||
|
byte[] bytes = new byte[1 + convertedProgram.length];
|
||||||
|
bytes[0] = (byte) (Script.encodeToOpN(witnessVersion) & 0xff);
|
||||||
|
System.arraycopy(convertedProgram, 0, bytes, 1, convertedProgram.length);
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper for re-arranging bits into groups.
|
||||||
|
*/
|
||||||
|
private static byte[] convertBits(final byte[] in, final int inStart, final int inLen, final int fromBits,
|
||||||
|
final int toBits, final boolean pad) {
|
||||||
|
int acc = 0;
|
||||||
|
int bits = 0;
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream(64);
|
||||||
|
final int maxv = (1 << toBits) - 1;
|
||||||
|
final int max_acc = (1 << (fromBits + toBits - 1)) - 1;
|
||||||
|
for (int i = 0; i < inLen; i++) {
|
||||||
|
int value = in[i + inStart] & 0xff;
|
||||||
|
if ((value >>> fromBits) != 0) {
|
||||||
|
throw new ProtocolException(
|
||||||
|
String.format("Input value '%X' exceeds '%d' bit size", value, fromBits));
|
||||||
|
}
|
||||||
|
acc = ((acc << fromBits) | value) & max_acc;
|
||||||
|
bits += fromBits;
|
||||||
|
while (bits >= toBits) {
|
||||||
|
bits -= toBits;
|
||||||
|
out.write((acc >>> bits) & maxv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pad) {
|
||||||
|
if (bits > 0)
|
||||||
|
out.write((acc << (toBits - bits)) & maxv);
|
||||||
|
} else if (bits >= fromBits || ((acc << (toBits - bits)) & maxv) != 0) {
|
||||||
|
throw new ProtocolException("Could not convert bits, invalid padding");
|
||||||
|
}
|
||||||
|
return out.toByteArray();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
157
src/main/java/com/craigraw/drongo/protocol/Ripemd160.java
Normal file
157
src/main/java/com/craigraw/drongo/protocol/Ripemd160.java
Normal file
|
@ -0,0 +1,157 @@
|
||||||
|
package com.craigraw.drongo.protocol;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Bitcoin cryptography library
|
||||||
|
* Copyright (c) Project Nayuki
|
||||||
|
*
|
||||||
|
* https://www.nayuki.io/page/bitcoin-cryptography-library
|
||||||
|
* https://github.com/nayuki/Bitcoin-Cryptography-Library
|
||||||
|
*/
|
||||||
|
|
||||||
|
import static java.lang.Integer.rotateLeft;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the RIPEMD-160 hash of an array of bytes. Not instantiable.
|
||||||
|
*/
|
||||||
|
public final class Ripemd160 {
|
||||||
|
|
||||||
|
private static final int BLOCK_LEN = 64; // In bytes
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*---- Static functions ----*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes and returns a 20-byte (160-bit) hash of the specified binary message.
|
||||||
|
* Each call will return a new byte array object instance.
|
||||||
|
* @param msg the message to compute the hash of
|
||||||
|
* @return a 20-byte array representing the message's RIPEMD-160 hash
|
||||||
|
* @throws NullPointerException if the message is {@code null}
|
||||||
|
*/
|
||||||
|
public static byte[] getHash(byte[] msg) {
|
||||||
|
// Compress whole message blocks
|
||||||
|
Objects.requireNonNull(msg);
|
||||||
|
int[] state = {0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0xC3D2E1F0};
|
||||||
|
int off = msg.length / BLOCK_LEN * BLOCK_LEN;
|
||||||
|
compress(state, msg, off);
|
||||||
|
|
||||||
|
// Final blocks, padding, and length
|
||||||
|
byte[] block = new byte[BLOCK_LEN];
|
||||||
|
System.arraycopy(msg, off, block, 0, msg.length - off);
|
||||||
|
off = msg.length % block.length;
|
||||||
|
block[off] = (byte)0x80;
|
||||||
|
off++;
|
||||||
|
if (off + 8 > block.length) {
|
||||||
|
compress(state, block, block.length);
|
||||||
|
Arrays.fill(block, (byte)0);
|
||||||
|
}
|
||||||
|
long len = (long)msg.length << 3;
|
||||||
|
for (int i = 0; i < 8; i++)
|
||||||
|
block[block.length - 8 + i] = (byte)(len >>> (i * 8));
|
||||||
|
compress(state, block, block.length);
|
||||||
|
|
||||||
|
// Int32 array to bytes in little endian
|
||||||
|
byte[] result = new byte[state.length * 4];
|
||||||
|
for (int i = 0; i < result.length; i++)
|
||||||
|
result[i] = (byte)(state[i / 4] >>> (i % 4 * 8));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*---- Private functions ----*/
|
||||||
|
|
||||||
|
private static void compress(int[] state, byte[] blocks, int len) {
|
||||||
|
if (len % BLOCK_LEN != 0)
|
||||||
|
throw new IllegalArgumentException();
|
||||||
|
for (int i = 0; i < len; i += BLOCK_LEN) {
|
||||||
|
|
||||||
|
// Message schedule
|
||||||
|
int[] schedule = new int[16];
|
||||||
|
for (int j = 0; j < BLOCK_LEN; j++)
|
||||||
|
schedule[j / 4] |= (blocks[i + j] & 0xFF) << (j % 4 * 8);
|
||||||
|
|
||||||
|
// The 80 rounds
|
||||||
|
int al = state[0], ar = state[0];
|
||||||
|
int bl = state[1], br = state[1];
|
||||||
|
int cl = state[2], cr = state[2];
|
||||||
|
int dl = state[3], dr = state[3];
|
||||||
|
int el = state[4], er = state[4];
|
||||||
|
for (int j = 0; j < 80; j++) {
|
||||||
|
int temp;
|
||||||
|
temp = rotateLeft(al + f(j, bl, cl, dl) + schedule[RL[j]] + KL[j / 16], SL[j]) + el;
|
||||||
|
al = el;
|
||||||
|
el = dl;
|
||||||
|
dl = rotateLeft(cl, 10);
|
||||||
|
cl = bl;
|
||||||
|
bl = temp;
|
||||||
|
temp = rotateLeft(ar + f(79 - j, br, cr, dr) + schedule[RR[j]] + KR[j / 16], SR[j]) + er;
|
||||||
|
ar = er;
|
||||||
|
er = dr;
|
||||||
|
dr = rotateLeft(cr, 10);
|
||||||
|
cr = br;
|
||||||
|
br = temp;
|
||||||
|
}
|
||||||
|
int temp = state[1] + cl + dr;
|
||||||
|
state[1] = state[2] + dl + er;
|
||||||
|
state[2] = state[3] + el + ar;
|
||||||
|
state[3] = state[4] + al + br;
|
||||||
|
state[4] = state[0] + bl + cr;
|
||||||
|
state[0] = temp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static int f(int i, int x, int y, int z) {
|
||||||
|
assert 0 <= i && i < 80;
|
||||||
|
if (i < 16) return x ^ y ^ z;
|
||||||
|
if (i < 32) return (x & y) | (~x & z);
|
||||||
|
if (i < 48) return (x | ~y) ^ z;
|
||||||
|
if (i < 64) return (x & z) | (y & ~z);
|
||||||
|
return x ^ (y | ~z);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*---- Class constants ----*/
|
||||||
|
|
||||||
|
private static final int[] KL = {0x00000000, 0x5A827999, 0x6ED9EBA1, 0x8F1BBCDC, 0xA953FD4E}; // Round constants for left line
|
||||||
|
private static final int[] KR = {0x50A28BE6, 0x5C4DD124, 0x6D703EF3, 0x7A6D76E9, 0x00000000}; // Round constants for right line
|
||||||
|
|
||||||
|
private static final int[] RL = { // Message schedule for left line
|
||||||
|
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
|
||||||
|
7, 4, 13, 1, 10, 6, 15, 3, 12, 0, 9, 5, 2, 14, 11, 8,
|
||||||
|
3, 10, 14, 4, 9, 15, 8, 1, 2, 7, 0, 6, 13, 11, 5, 12,
|
||||||
|
1, 9, 11, 10, 0, 8, 12, 4, 13, 3, 7, 15, 14, 5, 6, 2,
|
||||||
|
4, 0, 5, 9, 7, 12, 2, 10, 14, 1, 3, 8, 11, 6, 15, 13};
|
||||||
|
|
||||||
|
private static final int[] RR = { // Message schedule for right line
|
||||||
|
5, 14, 7, 0, 9, 2, 11, 4, 13, 6, 15, 8, 1, 10, 3, 12,
|
||||||
|
6, 11, 3, 7, 0, 13, 5, 10, 14, 15, 8, 12, 4, 9, 1, 2,
|
||||||
|
15, 5, 1, 3, 7, 14, 6, 9, 11, 8, 12, 2, 10, 0, 4, 13,
|
||||||
|
8, 6, 4, 1, 3, 11, 15, 0, 5, 12, 2, 13, 9, 7, 10, 14,
|
||||||
|
12, 15, 10, 4, 1, 5, 8, 7, 6, 2, 13, 14, 0, 3, 9, 11};
|
||||||
|
|
||||||
|
private static final int[] SL = { // Left-rotation for left line
|
||||||
|
11, 14, 15, 12, 5, 8, 7, 9, 11, 13, 14, 15, 6, 7, 9, 8,
|
||||||
|
7, 6, 8, 13, 11, 9, 7, 15, 7, 12, 15, 9, 11, 7, 13, 12,
|
||||||
|
11, 13, 6, 7, 14, 9, 13, 15, 14, 8, 13, 6, 5, 12, 7, 5,
|
||||||
|
11, 12, 14, 15, 14, 15, 9, 8, 9, 14, 5, 6, 8, 6, 5, 12,
|
||||||
|
9, 15, 5, 11, 6, 8, 13, 12, 5, 12, 13, 14, 11, 8, 5, 6};
|
||||||
|
|
||||||
|
private static final int[] SR = { // Left-rotation for right line
|
||||||
|
8, 9, 9, 11, 13, 15, 15, 5, 7, 7, 8, 11, 14, 14, 12, 6,
|
||||||
|
9, 13, 15, 7, 12, 8, 9, 11, 7, 7, 12, 7, 6, 15, 13, 11,
|
||||||
|
9, 7, 15, 11, 8, 6, 6, 14, 12, 13, 5, 14, 13, 13, 7, 5,
|
||||||
|
15, 5, 8, 11, 14, 14, 6, 14, 6, 9, 12, 9, 12, 5, 15, 8,
|
||||||
|
8, 5, 12, 9, 12, 5, 14, 6, 8, 13, 6, 5, 15, 13, 11, 11};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*---- Miscellaneous ----*/
|
||||||
|
|
||||||
|
private Ripemd160() {} // Not instantiable
|
||||||
|
|
||||||
|
}
|
169
src/main/java/com/craigraw/drongo/protocol/Script.java
Normal file
169
src/main/java/com/craigraw/drongo/protocol/Script.java
Normal file
|
@ -0,0 +1,169 @@
|
||||||
|
package com.craigraw.drongo.protocol;
|
||||||
|
|
||||||
|
import com.craigraw.drongo.Utils;
|
||||||
|
import com.craigraw.drongo.address.*;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static com.craigraw.drongo.protocol.ScriptOpCodes.*;
|
||||||
|
|
||||||
|
public class Script {
|
||||||
|
public static final long MAX_SCRIPT_ELEMENT_SIZE = 520;
|
||||||
|
|
||||||
|
// The program is a set of chunks where each element is either [opcode] or [data, data, data ...]
|
||||||
|
protected List<ScriptChunk> chunks;
|
||||||
|
|
||||||
|
protected byte[] program;
|
||||||
|
|
||||||
|
public Script(byte[] programBytes) {
|
||||||
|
program = programBytes;
|
||||||
|
parse(programBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Script(List<ScriptChunk> chunks) {
|
||||||
|
this.chunks = Collections.unmodifiableList(new ArrayList<>(chunks));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final ScriptChunk[] STANDARD_TRANSACTION_SCRIPT_CHUNKS = {
|
||||||
|
new ScriptChunk(ScriptOpCodes.OP_DUP, null, 0),
|
||||||
|
new ScriptChunk(ScriptOpCodes.OP_HASH160, null, 1),
|
||||||
|
new ScriptChunk(ScriptOpCodes.OP_EQUALVERIFY, null, 23),
|
||||||
|
new ScriptChunk(ScriptOpCodes.OP_CHECKSIG, null, 24),
|
||||||
|
};
|
||||||
|
|
||||||
|
private void parse(byte[] program) {
|
||||||
|
chunks = new ArrayList<>(5); // Common size.
|
||||||
|
ByteArrayInputStream bis = new ByteArrayInputStream(program);
|
||||||
|
int initialSize = bis.available();
|
||||||
|
while (bis.available() > 0) {
|
||||||
|
int startLocationInProgram = initialSize - bis.available();
|
||||||
|
int opcode = bis.read();
|
||||||
|
|
||||||
|
long dataToRead = -1;
|
||||||
|
if (opcode >= 0 && opcode < OP_PUSHDATA1) {
|
||||||
|
// Read some bytes of data, where how many is the opcode value itself.
|
||||||
|
dataToRead = opcode;
|
||||||
|
} else if (opcode == OP_PUSHDATA1) {
|
||||||
|
if (bis.available() < 1) throw new ProtocolException("Unexpected end of script");
|
||||||
|
dataToRead = bis.read();
|
||||||
|
} else if (opcode == OP_PUSHDATA2) {
|
||||||
|
// Read a short, then read that many bytes of data.
|
||||||
|
if (bis.available() < 2) throw new ProtocolException("Unexpected end of script");
|
||||||
|
dataToRead = Utils.readUint16FromStream(bis);
|
||||||
|
} else if (opcode == OP_PUSHDATA4) {
|
||||||
|
// Read a uint32, then read that many bytes of data.
|
||||||
|
// Though this is allowed, because its value cannot be > 520, it should never actually be used
|
||||||
|
if (bis.available() < 4) throw new ProtocolException("Unexpected end of script");
|
||||||
|
dataToRead = Utils.readUint32FromStream(bis);
|
||||||
|
}
|
||||||
|
|
||||||
|
ScriptChunk chunk;
|
||||||
|
if (dataToRead == -1) {
|
||||||
|
chunk = new ScriptChunk(opcode, null, startLocationInProgram);
|
||||||
|
} else {
|
||||||
|
if (dataToRead > bis.available())
|
||||||
|
throw new ProtocolException("Push of data element that is larger than remaining data");
|
||||||
|
byte[] data = new byte[(int)dataToRead];
|
||||||
|
if(dataToRead != 0 && bis.read(data, 0, (int)dataToRead) != dataToRead) {
|
||||||
|
throw new ProtocolException();
|
||||||
|
}
|
||||||
|
|
||||||
|
chunk = new ScriptChunk(opcode, data, startLocationInProgram);
|
||||||
|
}
|
||||||
|
// Save some memory by eliminating redundant copies of the same chunk objects.
|
||||||
|
for (ScriptChunk c : STANDARD_TRANSACTION_SCRIPT_CHUNKS) {
|
||||||
|
if (c.equals(chunk)) chunk = c;
|
||||||
|
}
|
||||||
|
chunks.add(chunk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the serialized program as a newly created byte array. */
|
||||||
|
public byte[] getProgram() {
|
||||||
|
try {
|
||||||
|
// Don't round-trip as Bitcoin Core doesn't and it would introduce a mismatch.
|
||||||
|
if (program != null)
|
||||||
|
return Arrays.copyOf(program, program.length);
|
||||||
|
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||||
|
for (ScriptChunk chunk : chunks) {
|
||||||
|
chunk.write(bos);
|
||||||
|
}
|
||||||
|
program = bos.toByteArray();
|
||||||
|
return program;
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e); // Cannot happen.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if this script has the required form to contain a destination address
|
||||||
|
*/
|
||||||
|
public boolean containsToAddress() {
|
||||||
|
return ScriptPattern.isP2PK(this) || ScriptPattern.isP2PKH(this) || ScriptPattern.isP2SH(this) || ScriptPattern.isP2WH(this) || ScriptPattern.isSentToMultisig(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>If the program somehow pays to a hash, returns the hash.</p>
|
||||||
|
*
|
||||||
|
* <p>Otherwise this method throws a ScriptException.</p>
|
||||||
|
*/
|
||||||
|
public byte[] getPubKeyHash() throws ProtocolException {
|
||||||
|
if (ScriptPattern.isP2PKH(this))
|
||||||
|
return ScriptPattern.extractHashFromP2PKH(this);
|
||||||
|
else if (ScriptPattern.isP2SH(this))
|
||||||
|
return ScriptPattern.extractHashFromP2SH(this);
|
||||||
|
else if (ScriptPattern.isP2WH(this))
|
||||||
|
return ScriptPattern.extractHashFromP2WH(this);
|
||||||
|
else
|
||||||
|
throw new ProtocolException("Script not in the standard scriptPubKey form");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the destination address from this script, if it's in the required form.
|
||||||
|
*/
|
||||||
|
public Address[] getToAddresses() {
|
||||||
|
if (ScriptPattern.isP2PK(this))
|
||||||
|
return new Address[] { new P2PKAddress( ScriptPattern.extractPKFromP2PK(this)) };
|
||||||
|
else if (ScriptPattern.isP2PKH(this))
|
||||||
|
return new Address[] { new P2PKHAddress( ScriptPattern.extractHashFromP2PKH(this)) };
|
||||||
|
else if (ScriptPattern.isP2SH(this))
|
||||||
|
return new Address[] { new P2SHAddress(ScriptPattern.extractHashFromP2SH(this)) };
|
||||||
|
else if (ScriptPattern.isP2WH(this))
|
||||||
|
return new Address[] { new P2WPKHAddress(ScriptPattern.extractHashFromP2WH(this)) };
|
||||||
|
else if (ScriptPattern.isSentToMultisig(this))
|
||||||
|
return ScriptPattern.extractMultisigAddresses(this);
|
||||||
|
else
|
||||||
|
throw new ProtocolException("Cannot cast this script to an address");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int decodeFromOpN(int opcode) {
|
||||||
|
if((opcode != OP_0 && opcode != OP_1NEGATE) && (opcode < OP_1 || opcode > OP_16)) {
|
||||||
|
throw new ProtocolException("decodeFromOpN called on non OP_N opcode: " + opcode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opcode == OP_0)
|
||||||
|
return 0;
|
||||||
|
else if (opcode == OP_1NEGATE)
|
||||||
|
return -1;
|
||||||
|
else
|
||||||
|
return opcode + 1 - OP_1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int encodeToOpN(int value) {
|
||||||
|
if(value < -1 || value > 16) {
|
||||||
|
throw new ProtocolException("encodeToOpN called for " + value + " which we cannot encode in an opcode.");
|
||||||
|
}
|
||||||
|
if (value == 0)
|
||||||
|
return OP_0;
|
||||||
|
else if (value == -1)
|
||||||
|
return OP_1NEGATE;
|
||||||
|
else
|
||||||
|
return value - 1 + OP_1;
|
||||||
|
}
|
||||||
|
}
|
71
src/main/java/com/craigraw/drongo/protocol/ScriptChunk.java
Normal file
71
src/main/java/com/craigraw/drongo/protocol/ScriptChunk.java
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
package com.craigraw.drongo.protocol;
|
||||||
|
|
||||||
|
import com.craigraw.drongo.Utils;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
|
import static com.craigraw.drongo.protocol.ScriptOpCodes.*;
|
||||||
|
|
||||||
|
public class ScriptChunk {
|
||||||
|
/** Operation to be executed. Opcodes are defined in {@link ScriptOpCodes}. */
|
||||||
|
public final int opcode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For push operations, this is the vector to be pushed on the stack. For {@link ScriptOpCodes#OP_0}, the vector is
|
||||||
|
* empty. Null for non-push operations.
|
||||||
|
*/
|
||||||
|
public final byte[] data;
|
||||||
|
|
||||||
|
private int startLocationInProgram;
|
||||||
|
|
||||||
|
public ScriptChunk(int opcode, byte[] data) {
|
||||||
|
this(opcode, data, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ScriptChunk(int opcode, byte[] data, int startLocationInProgram) {
|
||||||
|
this.opcode = opcode;
|
||||||
|
this.data = data;
|
||||||
|
this.startLocationInProgram = startLocationInProgram;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean equalsOpCode(int opcode) {
|
||||||
|
return opcode == this.opcode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If this chunk is a single byte of non-pushdata content (could be OP_RESERVED or some invalid Opcode)
|
||||||
|
*/
|
||||||
|
public boolean isOpCode() {
|
||||||
|
return opcode > OP_PUSHDATA4;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void write(OutputStream stream) throws IOException {
|
||||||
|
if (isOpCode()) {
|
||||||
|
if(data != null) throw new IllegalStateException("Data must be null for opcode chunk");
|
||||||
|
stream.write(opcode);
|
||||||
|
} else if (data != null) {
|
||||||
|
if (opcode < OP_PUSHDATA1) {
|
||||||
|
if(data.length != opcode) throw new IllegalStateException("Data length must equal opcode value");
|
||||||
|
stream.write(opcode);
|
||||||
|
} else if (opcode == OP_PUSHDATA1) {
|
||||||
|
if(data.length > 0xFF) throw new IllegalStateException("Data length must be less than or equal to 256");
|
||||||
|
stream.write(OP_PUSHDATA1);
|
||||||
|
stream.write(data.length);
|
||||||
|
} else if (opcode == OP_PUSHDATA2) {
|
||||||
|
if(data.length > 0xFFFF) throw new IllegalStateException("Data length must be less than or equal to 65536");
|
||||||
|
stream.write(OP_PUSHDATA2);
|
||||||
|
Utils.uint16ToByteStreamLE(data.length, stream);
|
||||||
|
} else if (opcode == OP_PUSHDATA4) {
|
||||||
|
if(data.length > Script.MAX_SCRIPT_ELEMENT_SIZE) throw new IllegalStateException("Data length must be less than or equal to " + Script.MAX_SCRIPT_ELEMENT_SIZE);
|
||||||
|
stream.write(OP_PUSHDATA4);
|
||||||
|
Utils.uint32ToByteStreamLE(data.length, stream);
|
||||||
|
} else {
|
||||||
|
throw new RuntimeException("Unimplemented");
|
||||||
|
}
|
||||||
|
stream.write(data);
|
||||||
|
} else {
|
||||||
|
stream.write(opcode); // smallNum
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
164
src/main/java/com/craigraw/drongo/protocol/ScriptOpCodes.java
Normal file
164
src/main/java/com/craigraw/drongo/protocol/ScriptOpCodes.java
Normal file
|
@ -0,0 +1,164 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2013 Google Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.craigraw.drongo.protocol;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Various constants that define the assembly-like scripting language that forms part of the Bitcoin protocol.
|
||||||
|
* See {@link Script} for details. Also provides a method to convert them to a string.
|
||||||
|
*/
|
||||||
|
public class ScriptOpCodes {
|
||||||
|
// push value
|
||||||
|
public static final int OP_0 = 0x00; // push empty vector
|
||||||
|
public static final int OP_FALSE = OP_0;
|
||||||
|
public static final int OP_PUSHDATA1 = 0x4c;
|
||||||
|
public static final int OP_PUSHDATA2 = 0x4d;
|
||||||
|
public static final int OP_PUSHDATA4 = 0x4e;
|
||||||
|
public static final int OP_1NEGATE = 0x4f;
|
||||||
|
public static final int OP_RESERVED = 0x50;
|
||||||
|
public static final int OP_1 = 0x51;
|
||||||
|
public static final int OP_TRUE = OP_1;
|
||||||
|
public static final int OP_2 = 0x52;
|
||||||
|
public static final int OP_3 = 0x53;
|
||||||
|
public static final int OP_4 = 0x54;
|
||||||
|
public static final int OP_5 = 0x55;
|
||||||
|
public static final int OP_6 = 0x56;
|
||||||
|
public static final int OP_7 = 0x57;
|
||||||
|
public static final int OP_8 = 0x58;
|
||||||
|
public static final int OP_9 = 0x59;
|
||||||
|
public static final int OP_10 = 0x5a;
|
||||||
|
public static final int OP_11 = 0x5b;
|
||||||
|
public static final int OP_12 = 0x5c;
|
||||||
|
public static final int OP_13 = 0x5d;
|
||||||
|
public static final int OP_14 = 0x5e;
|
||||||
|
public static final int OP_15 = 0x5f;
|
||||||
|
public static final int OP_16 = 0x60;
|
||||||
|
|
||||||
|
// control
|
||||||
|
public static final int OP_NOP = 0x61;
|
||||||
|
public static final int OP_VER = 0x62;
|
||||||
|
public static final int OP_IF = 0x63;
|
||||||
|
public static final int OP_NOTIF = 0x64;
|
||||||
|
public static final int OP_VERIF = 0x65;
|
||||||
|
public static final int OP_VERNOTIF = 0x66;
|
||||||
|
public static final int OP_ELSE = 0x67;
|
||||||
|
public static final int OP_ENDIF = 0x68;
|
||||||
|
public static final int OP_VERIFY = 0x69;
|
||||||
|
public static final int OP_RETURN = 0x6a;
|
||||||
|
|
||||||
|
// stack ops
|
||||||
|
public static final int OP_TOALTSTACK = 0x6b;
|
||||||
|
public static final int OP_FROMALTSTACK = 0x6c;
|
||||||
|
public static final int OP_2DROP = 0x6d;
|
||||||
|
public static final int OP_2DUP = 0x6e;
|
||||||
|
public static final int OP_3DUP = 0x6f;
|
||||||
|
public static final int OP_2OVER = 0x70;
|
||||||
|
public static final int OP_2ROT = 0x71;
|
||||||
|
public static final int OP_2SWAP = 0x72;
|
||||||
|
public static final int OP_IFDUP = 0x73;
|
||||||
|
public static final int OP_DEPTH = 0x74;
|
||||||
|
public static final int OP_DROP = 0x75;
|
||||||
|
public static final int OP_DUP = 0x76;
|
||||||
|
public static final int OP_NIP = 0x77;
|
||||||
|
public static final int OP_OVER = 0x78;
|
||||||
|
public static final int OP_PICK = 0x79;
|
||||||
|
public static final int OP_ROLL = 0x7a;
|
||||||
|
public static final int OP_ROT = 0x7b;
|
||||||
|
public static final int OP_SWAP = 0x7c;
|
||||||
|
public static final int OP_TUCK = 0x7d;
|
||||||
|
|
||||||
|
// splice ops
|
||||||
|
public static final int OP_CAT = 0x7e;
|
||||||
|
public static final int OP_SUBSTR = 0x7f;
|
||||||
|
public static final int OP_LEFT = 0x80;
|
||||||
|
public static final int OP_RIGHT = 0x81;
|
||||||
|
public static final int OP_SIZE = 0x82;
|
||||||
|
|
||||||
|
// bit logic
|
||||||
|
public static final int OP_INVERT = 0x83;
|
||||||
|
public static final int OP_AND = 0x84;
|
||||||
|
public static final int OP_OR = 0x85;
|
||||||
|
public static final int OP_XOR = 0x86;
|
||||||
|
public static final int OP_EQUAL = 0x87;
|
||||||
|
public static final int OP_EQUALVERIFY = 0x88;
|
||||||
|
public static final int OP_RESERVED1 = 0x89;
|
||||||
|
public static final int OP_RESERVED2 = 0x8a;
|
||||||
|
|
||||||
|
// numeric
|
||||||
|
public static final int OP_1ADD = 0x8b;
|
||||||
|
public static final int OP_1SUB = 0x8c;
|
||||||
|
public static final int OP_2MUL = 0x8d;
|
||||||
|
public static final int OP_2DIV = 0x8e;
|
||||||
|
public static final int OP_NEGATE = 0x8f;
|
||||||
|
public static final int OP_ABS = 0x90;
|
||||||
|
public static final int OP_NOT = 0x91;
|
||||||
|
public static final int OP_0NOTEQUAL = 0x92;
|
||||||
|
public static final int OP_ADD = 0x93;
|
||||||
|
public static final int OP_SUB = 0x94;
|
||||||
|
public static final int OP_MUL = 0x95;
|
||||||
|
public static final int OP_DIV = 0x96;
|
||||||
|
public static final int OP_MOD = 0x97;
|
||||||
|
public static final int OP_LSHIFT = 0x98;
|
||||||
|
public static final int OP_RSHIFT = 0x99;
|
||||||
|
public static final int OP_BOOLAND = 0x9a;
|
||||||
|
public static final int OP_BOOLOR = 0x9b;
|
||||||
|
public static final int OP_NUMEQUAL = 0x9c;
|
||||||
|
public static final int OP_NUMEQUALVERIFY = 0x9d;
|
||||||
|
public static final int OP_NUMNOTEQUAL = 0x9e;
|
||||||
|
public static final int OP_LESSTHAN = 0x9f;
|
||||||
|
public static final int OP_GREATERTHAN = 0xa0;
|
||||||
|
public static final int OP_LESSTHANOREQUAL = 0xa1;
|
||||||
|
public static final int OP_GREATERTHANOREQUAL = 0xa2;
|
||||||
|
public static final int OP_MIN = 0xa3;
|
||||||
|
public static final int OP_MAX = 0xa4;
|
||||||
|
public static final int OP_WITHIN = 0xa5;
|
||||||
|
|
||||||
|
// crypto
|
||||||
|
public static final int OP_RIPEMD160 = 0xa6;
|
||||||
|
public static final int OP_SHA1 = 0xa7;
|
||||||
|
public static final int OP_SHA256 = 0xa8;
|
||||||
|
public static final int OP_HASH160 = 0xa9;
|
||||||
|
public static final int OP_HASH256 = 0xaa;
|
||||||
|
public static final int OP_CODESEPARATOR = 0xab;
|
||||||
|
public static final int OP_CHECKSIG = 0xac;
|
||||||
|
public static final int OP_CHECKSIGVERIFY = 0xad;
|
||||||
|
public static final int OP_CHECKMULTISIG = 0xae;
|
||||||
|
public static final int OP_CHECKMULTISIGVERIFY = 0xaf;
|
||||||
|
|
||||||
|
// block state
|
||||||
|
/** Check lock time of the block. Introduced in BIP 65, replacing OP_NOP2 */
|
||||||
|
public static final int OP_CHECKLOCKTIMEVERIFY = 0xb1;
|
||||||
|
public static final int OP_CHECKSEQUENCEVERIFY = 0xb2;
|
||||||
|
|
||||||
|
// expansion
|
||||||
|
public static final int OP_NOP1 = 0xb0;
|
||||||
|
/** Deprecated by BIP 65 */
|
||||||
|
@Deprecated
|
||||||
|
public static final int OP_NOP2 = OP_CHECKLOCKTIMEVERIFY;
|
||||||
|
/** Deprecated by BIP 112 */
|
||||||
|
@Deprecated
|
||||||
|
public static final int OP_NOP3 = OP_CHECKSEQUENCEVERIFY;
|
||||||
|
public static final int OP_NOP4 = 0xb3;
|
||||||
|
public static final int OP_NOP5 = 0xb4;
|
||||||
|
public static final int OP_NOP6 = 0xb5;
|
||||||
|
public static final int OP_NOP7 = 0xb6;
|
||||||
|
public static final int OP_NOP8 = 0xb7;
|
||||||
|
public static final int OP_NOP9 = 0xb8;
|
||||||
|
public static final int OP_NOP10 = 0xb9;
|
||||||
|
public static final int OP_INVALIDOPCODE = 0xff;
|
||||||
|
}
|
184
src/main/java/com/craigraw/drongo/protocol/ScriptPattern.java
Normal file
184
src/main/java/com/craigraw/drongo/protocol/ScriptPattern.java
Normal file
|
@ -0,0 +1,184 @@
|
||||||
|
package com.craigraw.drongo.protocol;
|
||||||
|
|
||||||
|
import com.craigraw.drongo.Utils;
|
||||||
|
import com.craigraw.drongo.address.Address;
|
||||||
|
import com.craigraw.drongo.address.P2PKAddress;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static com.craigraw.drongo.protocol.ScriptOpCodes.*;
|
||||||
|
import static com.craigraw.drongo.protocol.Script.decodeFromOpN;
|
||||||
|
|
||||||
|
public class ScriptPattern {
|
||||||
|
/**
|
||||||
|
* Returns true if this script is of the form {@code DUP HASH160 <pubkey hash> EQUALVERIFY CHECKSIG}, ie, payment to an
|
||||||
|
* public key like {@code 2102f3b08938a7f8d2609d567aebc4989eeded6e2e880c058fdf092c5da82c3bc5eeac}.
|
||||||
|
*/
|
||||||
|
public static boolean isP2PK(Script script) {
|
||||||
|
List<ScriptChunk> chunks = script.chunks;
|
||||||
|
if (chunks.size() != 2)
|
||||||
|
return false;
|
||||||
|
if (!chunks.get(0).equalsOpCode(0x21))
|
||||||
|
return false;
|
||||||
|
byte[] chunk2data = chunks.get(0).data;
|
||||||
|
if (chunk2data == null)
|
||||||
|
return false;
|
||||||
|
if (chunk2data.length != 33)
|
||||||
|
return false;
|
||||||
|
if (!chunks.get(1).equalsOpCode(OP_CHECKSIG))
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract the pubkey from a P2PK scriptPubKey. It's important that the script is in the correct form, so you
|
||||||
|
* will want to guard calls to this method with {@link #isP2PK(Script)}.
|
||||||
|
*/
|
||||||
|
public static byte[] extractPKFromP2PK(Script script) {
|
||||||
|
return script.chunks.get(0).data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if this script is of the form {@code DUP HASH160 <pubkey hash> EQUALVERIFY CHECKSIG}, ie, payment to an
|
||||||
|
* address like {@code 1VayNert3x1KzbpzMGt2qdqrAThiRovi8}. This form was originally intended for the case where you wish
|
||||||
|
* to send somebody money with a written code because their node is offline, but over time has become the standard
|
||||||
|
* way to make payments due to the short and recognizable base58 form addresses come in.
|
||||||
|
*/
|
||||||
|
public static boolean isP2PKH(Script script) {
|
||||||
|
List<ScriptChunk> chunks = script.chunks;
|
||||||
|
if (chunks.size() != 5)
|
||||||
|
return false;
|
||||||
|
if (!chunks.get(0).equalsOpCode(OP_DUP))
|
||||||
|
return false;
|
||||||
|
if (!chunks.get(1).equalsOpCode(OP_HASH160))
|
||||||
|
return false;
|
||||||
|
byte[] chunk2data = chunks.get(2).data;
|
||||||
|
if (chunk2data == null)
|
||||||
|
return false;
|
||||||
|
if (chunk2data.length != 20)
|
||||||
|
return false;
|
||||||
|
if (!chunks.get(3).equalsOpCode(OP_EQUALVERIFY))
|
||||||
|
return false;
|
||||||
|
if (!chunks.get(4).equalsOpCode(OP_CHECKSIG))
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract the pubkey hash from a P2PKH scriptPubKey. It's important that the script is in the correct form, so you
|
||||||
|
* will want to guard calls to this method with {@link #isP2PKH(Script)}.
|
||||||
|
*/
|
||||||
|
public static byte[] extractHashFromP2PKH(Script script) {
|
||||||
|
return script.chunks.get(2).data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>
|
||||||
|
* Whether or not this is a scriptPubKey representing a P2SH output. In such outputs, the logic that
|
||||||
|
* controls reclamation is not actually in the output at all. Instead there's just a hash, and it's up to the
|
||||||
|
* spending input to provide a program matching that hash.
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* P2SH is described by <a href="https://github.com/bitcoin/bips/blob/master/bip-0016.mediawiki">BIP16</a>.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public static boolean isP2SH(Script script) {
|
||||||
|
List<ScriptChunk> chunks = script.chunks;
|
||||||
|
// We check for the effective serialized form because BIP16 defines a P2SH output using an exact byte
|
||||||
|
// template, not the logical program structure. Thus you can have two programs that look identical when
|
||||||
|
// printed out but one is a P2SH script and the other isn't! :(
|
||||||
|
// We explicitly test that the op code used to load the 20 bytes is 0x14 and not something logically
|
||||||
|
// equivalent like {@code OP_HASH160 OP_PUSHDATA1 0x14 <20 bytes of script hash> OP_EQUAL}
|
||||||
|
if (chunks.size() != 3)
|
||||||
|
return false;
|
||||||
|
if (!chunks.get(0).equalsOpCode(OP_HASH160))
|
||||||
|
return false;
|
||||||
|
ScriptChunk chunk1 = chunks.get(1);
|
||||||
|
if (chunk1.opcode != 0x14)
|
||||||
|
return false;
|
||||||
|
byte[] chunk1data = chunk1.data;
|
||||||
|
if (chunk1data == null)
|
||||||
|
return false;
|
||||||
|
if (chunk1data.length != 20)
|
||||||
|
return false;
|
||||||
|
if (!chunks.get(2).equalsOpCode(OP_EQUAL))
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether this script matches the format used for multisig outputs:
|
||||||
|
* {@code [n] [keys...] [m] CHECKMULTISIG}
|
||||||
|
*/
|
||||||
|
public static boolean isSentToMultisig(Script script) {
|
||||||
|
List<ScriptChunk> chunks = script.chunks;
|
||||||
|
if (chunks.size() < 4) return false;
|
||||||
|
ScriptChunk chunk = chunks.get(chunks.size() - 1);
|
||||||
|
// Must end in OP_CHECKMULTISIG[VERIFY].
|
||||||
|
if (!chunk.isOpCode()) return false;
|
||||||
|
if (!(chunk.equalsOpCode(OP_CHECKMULTISIG) || chunk.equalsOpCode(OP_CHECKMULTISIGVERIFY))) return false;
|
||||||
|
try {
|
||||||
|
// Second to last chunk must be an OP_N opcode and there should be that many data chunks (keys).
|
||||||
|
ScriptChunk m = chunks.get(chunks.size() - 2);
|
||||||
|
if (!m.isOpCode()) return false;
|
||||||
|
int numKeys = decodeFromOpN(m.opcode);
|
||||||
|
if (numKeys < 1 || chunks.size() != 3 + numKeys) return false;
|
||||||
|
for (int i = 1; i < chunks.size() - 2; i++) {
|
||||||
|
if (chunks.get(i).isOpCode()) return false;
|
||||||
|
}
|
||||||
|
// First chunk must be an OP_N opcode too.
|
||||||
|
if (decodeFromOpN(chunks.get(0).opcode) < 1) return false;
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
return false; // Not an OP_N opcode.
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Address[] extractMultisigAddresses(Script script) {
|
||||||
|
List<Address> addresses = new ArrayList<>();
|
||||||
|
|
||||||
|
List<ScriptChunk> chunks = script.chunks;
|
||||||
|
for (int i = 1; i < chunks.size() - 2; i++) {
|
||||||
|
byte[] pubKey = chunks.get(i).data;
|
||||||
|
addresses.add(new P2PKAddress(pubKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
return addresses.toArray(new Address[addresses.size()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract the script hash from a P2SH scriptPubKey. It's important that the script is in the correct form, so you
|
||||||
|
* will want to guard calls to this method with {@link #isP2SH(Script)}.
|
||||||
|
*/
|
||||||
|
public static byte[] extractHashFromP2SH(Script script) {
|
||||||
|
return script.chunks.get(1).data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if this script is of the form {@code OP_0 <hash>}. This can either be a P2WPKH or P2WSH scriptPubKey. These
|
||||||
|
* two script types were introduced with segwit.
|
||||||
|
*/
|
||||||
|
public static boolean isP2WH(Script script) {
|
||||||
|
List<ScriptChunk> chunks = script.chunks;
|
||||||
|
if (chunks.size() != 2)
|
||||||
|
return false;
|
||||||
|
if (!chunks.get(0).equalsOpCode(OP_0))
|
||||||
|
return false;
|
||||||
|
byte[] chunk1data = chunks.get(1).data;
|
||||||
|
if (chunk1data == null)
|
||||||
|
return false;
|
||||||
|
if (chunk1data.length != 20 && chunk1data.length != 32)
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract the pubkey hash from a P2WPKH or the script hash from a P2WSH scriptPubKey. It's important that the
|
||||||
|
* script is in the correct form, so you will want to guard calls to this method with
|
||||||
|
* {@link #isP2WH(Script)}.
|
||||||
|
*/
|
||||||
|
public static byte[] extractHashFromP2WH(Script script) {
|
||||||
|
return script.chunks.get(1).data;
|
||||||
|
}
|
||||||
|
}
|
261
src/main/java/com/craigraw/drongo/protocol/Sha256Hash.java
Normal file
261
src/main/java/com/craigraw/drongo/protocol/Sha256Hash.java
Normal file
|
@ -0,0 +1,261 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2011 Google Inc.
|
||||||
|
* Copyright 2014 Andreas Schildbach
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.craigraw.drongo.protocol;
|
||||||
|
|
||||||
|
import com.craigraw.drongo.Utils;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
public class Sha256Hash implements Comparable<Sha256Hash> {
|
||||||
|
public static final int LENGTH = 32; // bytes
|
||||||
|
public static final Sha256Hash ZERO_HASH = wrap(new byte[LENGTH]);
|
||||||
|
|
||||||
|
private final byte[] bytes;
|
||||||
|
|
||||||
|
private Sha256Hash(byte[] rawHashBytes) {
|
||||||
|
if(rawHashBytes.length != LENGTH) {
|
||||||
|
throw new ProtocolException();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.bytes = rawHashBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance that wraps the given hash value.
|
||||||
|
*
|
||||||
|
* @param rawHashBytes the raw hash bytes to wrap
|
||||||
|
* @return a new instance
|
||||||
|
* @throws IllegalArgumentException if the given array length is not exactly 32
|
||||||
|
*/
|
||||||
|
public static Sha256Hash wrap(byte[] rawHashBytes) {
|
||||||
|
return new Sha256Hash(rawHashBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance that wraps the given hash value (represented as a hex string).
|
||||||
|
*
|
||||||
|
* @param hexString a hash value represented as a hex string
|
||||||
|
* @return a new instance
|
||||||
|
* @throws IllegalArgumentException if the given string is not a valid
|
||||||
|
* hex string, or if it does not represent exactly 32 bytes
|
||||||
|
*/
|
||||||
|
public static Sha256Hash wrap(String hexString) {
|
||||||
|
return wrap(Utils.hexToBytes(hexString));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance that wraps the given hash value, but with byte order reversed.
|
||||||
|
*
|
||||||
|
* @param rawHashBytes the raw hash bytes to wrap
|
||||||
|
* @return a new instance
|
||||||
|
* @throws IllegalArgumentException if the given array length is not exactly 32
|
||||||
|
*/
|
||||||
|
public static Sha256Hash wrapReversed(byte[] rawHashBytes) {
|
||||||
|
return wrap(Utils.reverseBytes(rawHashBytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance containing the calculated (one-time) hash of the given bytes.
|
||||||
|
*
|
||||||
|
* @param contents the bytes on which the hash value is calculated
|
||||||
|
* @return a new instance containing the calculated (one-time) hash
|
||||||
|
*/
|
||||||
|
public static Sha256Hash of(byte[] contents) {
|
||||||
|
return wrap(hash(contents));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance containing the hash of the calculated hash of the given bytes.
|
||||||
|
*
|
||||||
|
* @param contents the bytes on which the hash value is calculated
|
||||||
|
* @return a new instance containing the calculated (two-time) hash
|
||||||
|
*/
|
||||||
|
public static Sha256Hash twiceOf(byte[] contents) {
|
||||||
|
return wrap(hashTwice(contents));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance containing the hash of the calculated hash of the given bytes.
|
||||||
|
*
|
||||||
|
* @param content1 first bytes on which the hash value is calculated
|
||||||
|
* @param content2 second bytes on which the hash value is calculated
|
||||||
|
* @return a new instance containing the calculated (two-time) hash
|
||||||
|
*/
|
||||||
|
public static Sha256Hash twiceOf(byte[] content1, byte[] content2) {
|
||||||
|
return wrap(hashTwice(content1, content2));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a new SHA-256 MessageDigest instance.
|
||||||
|
*
|
||||||
|
* This is a convenience method which wraps the checked
|
||||||
|
* exception that can never occur with a RuntimeException.
|
||||||
|
*
|
||||||
|
* @return a new SHA-256 MessageDigest instance
|
||||||
|
*/
|
||||||
|
public static MessageDigest newDigest() {
|
||||||
|
try {
|
||||||
|
return MessageDigest.getInstance("SHA-256");
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new RuntimeException(e); // Can't happen.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the SHA-256 hash of the given bytes.
|
||||||
|
*
|
||||||
|
* @param input the bytes to hash
|
||||||
|
* @return the hash (in big-endian order)
|
||||||
|
*/
|
||||||
|
public static byte[] hash(byte[] input) {
|
||||||
|
return hash(input, 0, input.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the SHA-256 hash of the given byte range.
|
||||||
|
*
|
||||||
|
* @param input the array containing the bytes to hash
|
||||||
|
* @param offset the offset within the array of the bytes to hash
|
||||||
|
* @param length the number of bytes to hash
|
||||||
|
* @return the hash (in big-endian order)
|
||||||
|
*/
|
||||||
|
public static byte[] hash(byte[] input, int offset, int length) {
|
||||||
|
MessageDigest digest = newDigest();
|
||||||
|
digest.update(input, offset, length);
|
||||||
|
return digest.digest();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the SHA-256 hash of the given bytes,
|
||||||
|
* and then hashes the resulting hash again.
|
||||||
|
*
|
||||||
|
* @param input the bytes to hash
|
||||||
|
* @return the double-hash (in big-endian order)
|
||||||
|
*/
|
||||||
|
public static byte[] hashTwice(byte[] input) {
|
||||||
|
return hashTwice(input, 0, input.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the hash of hash on the given chunks of bytes. This is equivalent to concatenating the two
|
||||||
|
* chunks and then passing the result to {@link #hashTwice(byte[])}.
|
||||||
|
*/
|
||||||
|
public static byte[] hashTwice(byte[] input1, byte[] input2) {
|
||||||
|
MessageDigest digest = newDigest();
|
||||||
|
digest.update(input1);
|
||||||
|
digest.update(input2);
|
||||||
|
return digest.digest(digest.digest());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the SHA-256 hash of the given byte range,
|
||||||
|
* and then hashes the resulting hash again.
|
||||||
|
*
|
||||||
|
* @param input the array containing the bytes to hash
|
||||||
|
* @param offset the offset within the array of the bytes to hash
|
||||||
|
* @param length the number of bytes to hash
|
||||||
|
* @return the double-hash (in big-endian order)
|
||||||
|
*/
|
||||||
|
public static byte[] hashTwice(byte[] input, int offset, int length) {
|
||||||
|
MessageDigest digest = newDigest();
|
||||||
|
digest.update(input, offset, length);
|
||||||
|
return digest.digest(digest.digest());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the hash of hash on the given byte ranges. This is equivalent to
|
||||||
|
* concatenating the two ranges and then passing the result to {@link #hashTwice(byte[])}.
|
||||||
|
*/
|
||||||
|
public static byte[] hashTwice(byte[] input1, int offset1, int length1,
|
||||||
|
byte[] input2, int offset2, int length2) {
|
||||||
|
MessageDigest digest = newDigest();
|
||||||
|
digest.update(input1, offset1, length1);
|
||||||
|
digest.update(input2, offset2, length2);
|
||||||
|
return digest.digest(digest.digest());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
return Arrays.equals(bytes, ((Sha256Hash)o).bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the last four bytes of the wrapped hash. This should be unique enough to be a suitable hash code even for
|
||||||
|
* blocks, where the goal is to try and get the first bytes to be zeros (i.e. the value as a big integer lower
|
||||||
|
* than the target value).
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
// Use the last 4 bytes, not the first 4 which are often zeros in Bitcoin.
|
||||||
|
return fromBytes(bytes[LENGTH - 4], bytes[LENGTH - 3], bytes[LENGTH - 2], bytes[LENGTH - 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the {@code int} value whose byte representation is the given 4 bytes, in big-endian
|
||||||
|
* order; equivalent to {@code Ints.fromByteArray(new byte[] {b1, b2, b3, b4})}.
|
||||||
|
*
|
||||||
|
* @since 7.0
|
||||||
|
*/
|
||||||
|
public static int fromBytes(byte b1, byte b2, byte b3, byte b4) {
|
||||||
|
return b1 << 24 | (b2 & 0xFF) << 16 | (b3 & 0xFF) << 8 | (b4 & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return Utils.bytesToHex(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the bytes interpreted as a positive integer.
|
||||||
|
*/
|
||||||
|
public BigInteger toBigInteger() {
|
||||||
|
return new BigInteger(1, bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the internal byte array, without defensively copying. Therefore do NOT modify the returned array.
|
||||||
|
*/
|
||||||
|
public byte[] getBytes() {
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a reversed copy of the internal byte array.
|
||||||
|
*/
|
||||||
|
public byte[] getReversedBytes() {
|
||||||
|
return Utils.reverseBytes(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compareTo(final Sha256Hash other) {
|
||||||
|
for (int i = LENGTH - 1; i >= 0; i--) {
|
||||||
|
final int thisByte = this.bytes[i] & 0xff;
|
||||||
|
final int otherByte = other.bytes[i] & 0xff;
|
||||||
|
if (thisByte > otherByte)
|
||||||
|
return 1;
|
||||||
|
if (thisByte < otherByte)
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
188
src/main/java/com/craigraw/drongo/protocol/Transaction.java
Normal file
188
src/main/java/com/craigraw/drongo/protocol/Transaction.java
Normal file
|
@ -0,0 +1,188 @@
|
||||||
|
package com.craigraw.drongo.protocol;
|
||||||
|
|
||||||
|
import com.craigraw.drongo.Utils;
|
||||||
|
import com.craigraw.drongo.address.Address;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static com.craigraw.drongo.Utils.uint32ToByteStreamLE;
|
||||||
|
|
||||||
|
public class Transaction extends TransactionPart {
|
||||||
|
private long version;
|
||||||
|
private long lockTime;
|
||||||
|
|
||||||
|
private Sha256Hash cachedTxId;
|
||||||
|
private Sha256Hash cachedWTxId;
|
||||||
|
|
||||||
|
private ArrayList<TransactionInput> inputs;
|
||||||
|
private ArrayList<TransactionOutput> outputs;
|
||||||
|
|
||||||
|
public Transaction(byte[] rawtx) {
|
||||||
|
super(rawtx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Sha256Hash getTxId() {
|
||||||
|
if (cachedTxId == null) {
|
||||||
|
if (!hasWitnesses() && cachedWTxId != null) {
|
||||||
|
cachedTxId = cachedWTxId;
|
||||||
|
} else {
|
||||||
|
ByteArrayOutputStream stream = new UnsafeByteArrayOutputStream(length < 32 ? 32 : length + 32);
|
||||||
|
try {
|
||||||
|
bitcoinSerializeToStream(stream, false);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e); // cannot happen
|
||||||
|
}
|
||||||
|
cachedTxId = Sha256Hash.wrapReversed(Sha256Hash.hashTwice(stream.toByteArray()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cachedTxId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Sha256Hash getWTxId() {
|
||||||
|
if (cachedWTxId == null) {
|
||||||
|
if (!hasWitnesses() && cachedTxId != null) {
|
||||||
|
cachedWTxId = cachedTxId;
|
||||||
|
} else {
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
try {
|
||||||
|
bitcoinSerializeToStream(baos, hasWitnesses());
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e); // cannot happen
|
||||||
|
}
|
||||||
|
cachedWTxId = Sha256Hash.wrapReversed(Sha256Hash.hashTwice(baos.toByteArray()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cachedWTxId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasWitnesses() {
|
||||||
|
for (TransactionInput in : inputs)
|
||||||
|
if (in.hasWitness())
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void bitcoinSerializeToStream(OutputStream stream) throws IOException {
|
||||||
|
boolean useSegwit = hasWitnesses();
|
||||||
|
bitcoinSerializeToStream(stream, useSegwit);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialize according to <a href="https://github.com/bitcoin/bips/blob/master/bip-0144.mediawiki">BIP144</a> or the
|
||||||
|
* <a href="https://en.bitcoin.it/wiki/Protocol_documentation#tx">classic format</a>, depending on if segwit is
|
||||||
|
* desired.
|
||||||
|
*/
|
||||||
|
protected void bitcoinSerializeToStream(OutputStream stream, boolean useSegwit) throws IOException {
|
||||||
|
// version
|
||||||
|
uint32ToByteStreamLE(version, stream);
|
||||||
|
// marker, flag
|
||||||
|
if (useSegwit) {
|
||||||
|
stream.write(0);
|
||||||
|
stream.write(1);
|
||||||
|
}
|
||||||
|
// txin_count, txins
|
||||||
|
stream.write(new VarInt(inputs.size()).encode());
|
||||||
|
for (TransactionInput in : inputs)
|
||||||
|
in.bitcoinSerialize(stream);
|
||||||
|
// txout_count, txouts
|
||||||
|
stream.write(new VarInt(outputs.size()).encode());
|
||||||
|
for (TransactionOutput out : outputs)
|
||||||
|
out.bitcoinSerialize(stream);
|
||||||
|
// script_witnisses
|
||||||
|
if (useSegwit) {
|
||||||
|
for (TransactionInput in : inputs) {
|
||||||
|
in.getWitness().bitcoinSerializeToStream(stream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// lock_time
|
||||||
|
uint32ToByteStreamLE(lockTime, stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deserialize according to <a href="https://github.com/bitcoin/bips/blob/master/bip-0144.mediawiki">BIP144</a> or
|
||||||
|
* the <a href="https://en.bitcoin.it/wiki/Protocol_documentation#tx">classic format</a>, depending on if the
|
||||||
|
* transaction is segwit or not.
|
||||||
|
*/
|
||||||
|
public void parse() {
|
||||||
|
// version
|
||||||
|
version = readUint32();
|
||||||
|
// peek at marker
|
||||||
|
byte marker = rawtx[cursor];
|
||||||
|
boolean useSegwit = marker == 0;
|
||||||
|
// marker, flag
|
||||||
|
if (useSegwit) {
|
||||||
|
readBytes(2);
|
||||||
|
}
|
||||||
|
// txin_count, txins
|
||||||
|
parseInputs();
|
||||||
|
// txout_count, txouts
|
||||||
|
parseOutputs();
|
||||||
|
// script_witnesses
|
||||||
|
if (useSegwit)
|
||||||
|
parseWitnesses();
|
||||||
|
// lock_time
|
||||||
|
lockTime = readUint32();
|
||||||
|
|
||||||
|
length = cursor - offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void parseInputs() {
|
||||||
|
long numInputs = readVarInt();
|
||||||
|
inputs = new ArrayList<>(Math.min((int) numInputs, Utils.MAX_INITIAL_ARRAY_LENGTH));
|
||||||
|
for (long i = 0; i < numInputs; i++) {
|
||||||
|
TransactionInput input = new TransactionInput(this, rawtx, cursor);
|
||||||
|
inputs.add(input);
|
||||||
|
long scriptLen = readVarInt(TransactionOutPoint.MESSAGE_LENGTH);
|
||||||
|
cursor += scriptLen + 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void parseOutputs() {
|
||||||
|
long numOutputs = readVarInt();
|
||||||
|
outputs = new ArrayList<>(Math.min((int) numOutputs, Utils.MAX_INITIAL_ARRAY_LENGTH));
|
||||||
|
for (long i = 0; i < numOutputs; i++) {
|
||||||
|
TransactionOutput output = new TransactionOutput(this, rawtx, cursor);
|
||||||
|
outputs.add(output);
|
||||||
|
long scriptLen = readVarInt(8);
|
||||||
|
cursor += scriptLen;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void parseWitnesses() {
|
||||||
|
int numWitnesses = inputs.size();
|
||||||
|
for (int i = 0; i < numWitnesses; i++) {
|
||||||
|
long pushCount = readVarInt();
|
||||||
|
TransactionWitness witness = new TransactionWitness((int) pushCount);
|
||||||
|
inputs.get(i).setWitness(witness);
|
||||||
|
for (int y = 0; y < pushCount; y++) {
|
||||||
|
long pushSize = readVarInt();
|
||||||
|
byte[] push = readBytes((int) pushSize);
|
||||||
|
witness.setPush(y, push);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns an unmodifiable view of all inputs. */
|
||||||
|
public List<TransactionInput> getInputs() {
|
||||||
|
return Collections.unmodifiableList(inputs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns an unmodifiable view of all outputs. */
|
||||||
|
public List<TransactionOutput> getOutputs() {
|
||||||
|
return Collections.unmodifiableList(outputs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final void main(String[] args) {
|
||||||
|
String hex = "0100000001e0ea4cd2f1307820d5f33e61aa6b636d8ff94fa7e3b1913f058fb1c8a765fde0340000006a47304402201aa0955638da2902ba972100816d21bde55d0415b98064b7fa511ffefa41397702203f9c93e27557b5b04187784e79f2c1eb74a3202a73085ddfb4509069b90cbbed0121023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ffffffff3510270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac2a91a401000000001976a9141f924ac57c8e44cfbf860fbe0a3ea072b5fb8d0f88ac00000000";
|
||||||
|
byte[] transactionBytes = Utils.hexToBytes(hex);
|
||||||
|
Transaction transaction = new Transaction(transactionBytes);
|
||||||
|
|
||||||
|
Address[] addresses = transaction.getOutputs().get(3).getScript().getToAddresses();
|
||||||
|
System.out.println(addresses[0]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
119
src/main/java/com/craigraw/drongo/protocol/TransactionPart.java
Normal file
119
src/main/java/com/craigraw/drongo/protocol/TransactionPart.java
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
package com.craigraw.drongo.protocol;
|
||||||
|
|
||||||
|
import com.craigraw.drongo.Utils;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
|
public abstract class TransactionPart {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(TransactionPart.class);
|
||||||
|
|
||||||
|
public static final int MAX_SIZE = 0x02000000; // 32MB
|
||||||
|
public static final int UNKNOWN_LENGTH = Integer.MIN_VALUE;
|
||||||
|
|
||||||
|
protected byte[] rawtx;
|
||||||
|
|
||||||
|
// The offset is how many bytes into the provided byte array this message payload starts at.
|
||||||
|
protected int offset;
|
||||||
|
// The cursor keeps track of where we are in the byte array as we parse it.
|
||||||
|
// Note that it's relative to the start of the array NOT the start of the message payload.
|
||||||
|
protected int cursor;
|
||||||
|
|
||||||
|
protected TransactionPart parent;
|
||||||
|
|
||||||
|
protected int length = UNKNOWN_LENGTH;
|
||||||
|
|
||||||
|
public TransactionPart(byte[] rawtx, int offset) {
|
||||||
|
this.rawtx = rawtx;
|
||||||
|
this.cursor = this.offset = offset;
|
||||||
|
|
||||||
|
parse();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract void parse() throws ProtocolException;
|
||||||
|
|
||||||
|
public final void setParent(TransactionPart parent) {
|
||||||
|
this.parent = parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This returns a correct value by parsing the message.
|
||||||
|
*/
|
||||||
|
public final int getMessageSize() {
|
||||||
|
if (length == UNKNOWN_LENGTH) {
|
||||||
|
throw new ProtocolException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected long readUint32() throws ProtocolException {
|
||||||
|
try {
|
||||||
|
long u = Utils.readUint32(rawtx, cursor);
|
||||||
|
cursor += 4;
|
||||||
|
return u;
|
||||||
|
} catch (ArrayIndexOutOfBoundsException e) {
|
||||||
|
throw new ProtocolException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected long readInt64() throws ProtocolException {
|
||||||
|
try {
|
||||||
|
long u = Utils.readInt64(rawtx, cursor);
|
||||||
|
cursor += 8;
|
||||||
|
return u;
|
||||||
|
} catch (ArrayIndexOutOfBoundsException e) {
|
||||||
|
throw new ProtocolException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected byte[] readBytes(int length) throws ProtocolException {
|
||||||
|
if ((length > MAX_SIZE) || (cursor + length > rawtx.length)) {
|
||||||
|
throw new ProtocolException("Claimed value length too large: " + length);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
byte[] b = new byte[length];
|
||||||
|
System.arraycopy(rawtx, cursor, b, 0, length);
|
||||||
|
cursor += length;
|
||||||
|
return b;
|
||||||
|
} catch (IndexOutOfBoundsException e) {
|
||||||
|
throw new ProtocolException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected long readVarInt() throws ProtocolException {
|
||||||
|
return readVarInt(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected long readVarInt(int offset) throws ProtocolException {
|
||||||
|
try {
|
||||||
|
VarInt varint = new VarInt(rawtx, cursor + offset);
|
||||||
|
cursor += offset + varint.getOriginalSizeInBytes();
|
||||||
|
return varint.value;
|
||||||
|
} catch (ArrayIndexOutOfBoundsException e) {
|
||||||
|
throw new ProtocolException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Sha256Hash readHash() throws ProtocolException {
|
||||||
|
// We have to flip it around, as it's been read off the wire in little endian.
|
||||||
|
// Not the most efficient way to do this but the clearest.
|
||||||
|
return Sha256Hash.wrapReversed(readBytes(32));
|
||||||
|
}
|
||||||
|
|
||||||
|
public final void bitcoinSerialize(OutputStream stream) throws IOException {
|
||||||
|
// 1st check for cached bytes.
|
||||||
|
if (rawtx != null && length != UNKNOWN_LENGTH) {
|
||||||
|
stream.write(rawtx, offset, length);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bitcoinSerializeToStream(stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void bitcoinSerializeToStream(OutputStream stream) throws IOException {
|
||||||
|
log.error("Error: {} class has not implemented bitcoinSerializeToStream method. Generating message with no payload", getClass());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
package com.craigraw.drongo.protocol;
|
||||||
|
|
||||||
|
import com.craigraw.drongo.Utils;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class TransactionWitness {
|
||||||
|
public static final TransactionWitness EMPTY = new TransactionWitness(0);
|
||||||
|
|
||||||
|
private final List<byte[]> pushes;
|
||||||
|
|
||||||
|
public TransactionWitness(int pushCount) {
|
||||||
|
pushes = new ArrayList<>(Math.min(pushCount, Utils.MAX_INITIAL_ARRAY_LENGTH));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPush(int i, byte[] value) {
|
||||||
|
while (i >= pushes.size()) {
|
||||||
|
pushes.add(new byte[]{});
|
||||||
|
}
|
||||||
|
pushes.set(i, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getPushCount() {
|
||||||
|
return pushes.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void bitcoinSerializeToStream(OutputStream stream) throws IOException {
|
||||||
|
stream.write(new VarInt(pushes.size()).encode());
|
||||||
|
for (int i = 0; i < pushes.size(); i++) {
|
||||||
|
byte[] push = pushes.get(i);
|
||||||
|
stream.write(new VarInt(push.length).encode());
|
||||||
|
stream.write(push);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,138 @@
|
||||||
|
package com.craigraw.drongo.protocol;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright 2011 Steve Coughlan.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>An unsynchronized implementation of ByteArrayOutputStream that will return the backing byte array if its length == size().
|
||||||
|
* This avoids unneeded array copy where the BOS is simply being used to extract a byte array of known length from a
|
||||||
|
* 'serialized to stream' method.</p>
|
||||||
|
*
|
||||||
|
* <p>Unless the final length can be accurately predicted the only performance this will yield is due to unsynchronized
|
||||||
|
* methods.</p>
|
||||||
|
*
|
||||||
|
* @author git
|
||||||
|
*/
|
||||||
|
public class UnsafeByteArrayOutputStream extends ByteArrayOutputStream {
|
||||||
|
|
||||||
|
public UnsafeByteArrayOutputStream() {
|
||||||
|
super(32);
|
||||||
|
}
|
||||||
|
|
||||||
|
public UnsafeByteArrayOutputStream(int size) {
|
||||||
|
super(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes the specified byte to this byte array output stream.
|
||||||
|
*
|
||||||
|
* @param b the byte to be written.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void write(int b) {
|
||||||
|
int newcount = count + 1;
|
||||||
|
if (newcount > buf.length) {
|
||||||
|
buf = copyOf(buf, Math.max(buf.length << 1, newcount));
|
||||||
|
}
|
||||||
|
buf[count] = (byte) b;
|
||||||
|
count = newcount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes {@code len} bytes from the specified byte array
|
||||||
|
* starting at offset {@code off} to this byte array output stream.
|
||||||
|
*
|
||||||
|
* @param b the data.
|
||||||
|
* @param off the start offset in the data.
|
||||||
|
* @param len the number of bytes to write.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void write(byte[] b, int off, int len) {
|
||||||
|
if ((off < 0) || (off > b.length) || (len < 0) ||
|
||||||
|
((off + len) > b.length) || ((off + len) < 0)) {
|
||||||
|
throw new IndexOutOfBoundsException();
|
||||||
|
} else if (len == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int newcount = count + len;
|
||||||
|
if (newcount > buf.length) {
|
||||||
|
buf = copyOf(buf, Math.max(buf.length << 1, newcount));
|
||||||
|
}
|
||||||
|
System.arraycopy(b, off, buf, count, len);
|
||||||
|
count = newcount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes the complete contents of this byte array output stream to
|
||||||
|
* the specified output stream argument, as if by calling the output
|
||||||
|
* stream's write method using {@code out.write(buf, 0, count)}.
|
||||||
|
*
|
||||||
|
* @param out the output stream to which to write the data.
|
||||||
|
* @throws IOException if an I/O error occurs.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void writeTo(OutputStream out) throws IOException {
|
||||||
|
out.write(buf, 0, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the {@code count} field of this byte array output
|
||||||
|
* stream to zero, so that all currently accumulated output in the
|
||||||
|
* output stream is discarded. The output stream can be used again,
|
||||||
|
* reusing the already allocated buffer space.
|
||||||
|
*
|
||||||
|
* @see java.io.ByteArrayInputStream#count
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void reset() {
|
||||||
|
count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a newly allocated byte array. Its size is the current
|
||||||
|
* size of this output stream and the valid contents of the buffer
|
||||||
|
* have been copied into it.
|
||||||
|
*
|
||||||
|
* @return the current contents of this output stream, as a byte array.
|
||||||
|
* @see java.io.ByteArrayOutputStream#size()
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public byte toByteArray()[] {
|
||||||
|
return count == buf.length ? buf : copyOf(buf, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current size of the buffer.
|
||||||
|
*
|
||||||
|
* @return the value of the {@code count} field, which is the number
|
||||||
|
* of valid bytes in this output stream.
|
||||||
|
* @see java.io.ByteArrayOutputStream#count
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int size() {
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] copyOf(byte[] in, int length) {
|
||||||
|
byte[] out = new byte[length];
|
||||||
|
System.arraycopy(in, 0, out, 0, Math.min(length, in.length));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
}
|
117
src/main/java/com/craigraw/drongo/protocol/VarInt.java
Normal file
117
src/main/java/com/craigraw/drongo/protocol/VarInt.java
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2011 Google Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.craigraw.drongo.protocol;
|
||||||
|
|
||||||
|
import com.craigraw.drongo.Utils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A variable-length encoded unsigned integer using Satoshi's encoding (a.k.a. "CompactSize").
|
||||||
|
*/
|
||||||
|
public class VarInt {
|
||||||
|
public final long value;
|
||||||
|
private final int originallyEncodedSize;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new VarInt with the given unsigned long value.
|
||||||
|
*
|
||||||
|
* @param value the unsigned long value (beware widening conversion of negatives!)
|
||||||
|
*/
|
||||||
|
public VarInt(long value) {
|
||||||
|
this.value = value;
|
||||||
|
originallyEncodedSize = getSizeInBytes();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new VarInt with the value parsed from the specified offset of the given buffer.
|
||||||
|
*
|
||||||
|
* @param buf the buffer containing the value
|
||||||
|
* @param offset the offset of the value
|
||||||
|
*/
|
||||||
|
public VarInt(byte[] buf, int offset) {
|
||||||
|
int first = 0xFF & buf[offset];
|
||||||
|
if (first < 253) {
|
||||||
|
value = first;
|
||||||
|
originallyEncodedSize = 1; // 1 data byte (8 bits)
|
||||||
|
} else if (first == 253) {
|
||||||
|
value = Utils.readUint16(buf, offset + 1);
|
||||||
|
originallyEncodedSize = 3; // 1 marker + 2 data bytes (16 bits)
|
||||||
|
} else if (first == 254) {
|
||||||
|
value = Utils.readUint32(buf, offset + 1);
|
||||||
|
originallyEncodedSize = 5; // 1 marker + 4 data bytes (32 bits)
|
||||||
|
} else {
|
||||||
|
value = Utils.readInt64(buf, offset + 1);
|
||||||
|
originallyEncodedSize = 9; // 1 marker + 8 data bytes (64 bits)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the original number of bytes used to encode the value if it was
|
||||||
|
* deserialized from a byte array, or the minimum encoded size if it was not.
|
||||||
|
*/
|
||||||
|
public int getOriginalSizeInBytes() {
|
||||||
|
return originallyEncodedSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the minimum encoded size of the value.
|
||||||
|
*/
|
||||||
|
public final int getSizeInBytes() {
|
||||||
|
return sizeOf(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the minimum encoded size of the given unsigned long value.
|
||||||
|
*
|
||||||
|
* @param value the unsigned long value (beware widening conversion of negatives!)
|
||||||
|
*/
|
||||||
|
public static int sizeOf(long value) {
|
||||||
|
// if negative, it's actually a very large unsigned long value
|
||||||
|
if (value < 0) return 9; // 1 marker + 8 data bytes
|
||||||
|
if (value < 253) return 1; // 1 data byte
|
||||||
|
if (value <= 0xFFFFL) return 3; // 1 marker + 2 data bytes
|
||||||
|
if (value <= 0xFFFFFFFFL) return 5; // 1 marker + 4 data bytes
|
||||||
|
return 9; // 1 marker + 8 data bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes the value into its minimal representation.
|
||||||
|
*
|
||||||
|
* @return the minimal encoded bytes of the value
|
||||||
|
*/
|
||||||
|
public byte[] encode() {
|
||||||
|
byte[] bytes;
|
||||||
|
switch (sizeOf(value)) {
|
||||||
|
case 1:
|
||||||
|
return new byte[]{(byte) value};
|
||||||
|
case 3:
|
||||||
|
bytes = new byte[3];
|
||||||
|
bytes[0] = (byte) 253;
|
||||||
|
Utils.uint16ToByteArrayLE((int) value, bytes, 1);
|
||||||
|
return bytes;
|
||||||
|
case 5:
|
||||||
|
bytes = new byte[5];
|
||||||
|
bytes[0] = (byte) 254;
|
||||||
|
Utils.uint32ToByteArrayLE(value, bytes, 1);
|
||||||
|
return bytes;
|
||||||
|
default:
|
||||||
|
bytes = new byte[9];
|
||||||
|
bytes[0] = (byte) 255;
|
||||||
|
Utils.int64ToByteArrayLE(value, bytes, 1);
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
128
src/main/java/com/craigraw/drongo/rpc/BitcoinJSONRPCClient.java
Normal file
128
src/main/java/com/craigraw/drongo/rpc/BitcoinJSONRPCClient.java
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
package com.craigraw.drongo.rpc;
|
||||||
|
|
||||||
|
import org.json.simple.JSONObject;
|
||||||
|
import org.json.simple.parser.JSONParser;
|
||||||
|
import org.json.simple.parser.ParseException;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.*;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public class BitcoinJSONRPCClient {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(BitcoinJSONRPCClient.class);
|
||||||
|
public static final Charset QUERY_CHARSET = Charset.forName("ISO8859-1");
|
||||||
|
public static final String RESPONSE_ID = "drongo";
|
||||||
|
|
||||||
|
public final URL rpcURL;
|
||||||
|
private final URL noAuthURL;
|
||||||
|
private final String authStr;
|
||||||
|
|
||||||
|
public BitcoinJSONRPCClient(String host, String port, String user, String password) {
|
||||||
|
this.rpcURL = getConnectUrl(host, port, user, password);
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.noAuthURL = new URI(rpcURL.getProtocol(), null, rpcURL.getHost(), rpcURL.getPort(), rpcURL.getPath(), rpcURL.getQuery(), null).toURL();
|
||||||
|
} catch (MalformedURLException | URISyntaxException ex) {
|
||||||
|
throw new IllegalArgumentException(rpcURL.toString(), ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.authStr = rpcURL.getUserInfo() == null ? null : new String(Base64.getEncoder().encode(rpcURL.getUserInfo().getBytes(QUERY_CHARSET)), QUERY_CHARSET);
|
||||||
|
}
|
||||||
|
|
||||||
|
private URL getConnectUrl(String host, String port, String user, String password) {
|
||||||
|
try {
|
||||||
|
return new URL("http://" + user + ':' + password + "@" + host + ":" + (port == null ? "8332" : port) + "/");
|
||||||
|
} catch (MalformedURLException e) {
|
||||||
|
throw new IllegalArgumentException("Invalid RPC connection details", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Object query(String method, Object... o) throws BitcoinRPCException {
|
||||||
|
HttpURLConnection conn;
|
||||||
|
try {
|
||||||
|
conn = (HttpURLConnection) noAuthURL.openConnection();
|
||||||
|
|
||||||
|
conn.setDoOutput(true);
|
||||||
|
conn.setDoInput(true);
|
||||||
|
|
||||||
|
conn.setRequestProperty("Authorization", "Basic " + authStr);
|
||||||
|
byte[] r = prepareRequest(method, o);
|
||||||
|
log.debug("Bitcoin JSON-RPC request: " + new String(r, QUERY_CHARSET));
|
||||||
|
conn.getOutputStream().write(r);
|
||||||
|
conn.getOutputStream().close();
|
||||||
|
int responseCode = conn.getResponseCode();
|
||||||
|
if (responseCode != 200) {
|
||||||
|
InputStream errorStream = conn.getErrorStream();
|
||||||
|
throw new BitcoinRPCException(method,
|
||||||
|
Arrays.deepToString(o),
|
||||||
|
responseCode,
|
||||||
|
conn.getResponseMessage(),
|
||||||
|
errorStream == null ? null : new String(loadStream(errorStream, true)));
|
||||||
|
}
|
||||||
|
return loadResponse(conn.getInputStream(), RESPONSE_ID, true);
|
||||||
|
} catch (IOException ex) {
|
||||||
|
throw new BitcoinRPCException(method, Arrays.deepToString(o), ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected byte[] prepareRequest(final String method, final Object... params) {
|
||||||
|
return JSONObject.toJSONString(new LinkedHashMap<String, Object>() {
|
||||||
|
{
|
||||||
|
put("method", method);
|
||||||
|
put("params", Arrays.asList(params));
|
||||||
|
put("id", RESPONSE_ID);
|
||||||
|
put("jsonrpc", "1.0");
|
||||||
|
}
|
||||||
|
}).getBytes(QUERY_CHARSET);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] loadStream(InputStream in, boolean close) throws IOException {
|
||||||
|
ByteArrayOutputStream o = new ByteArrayOutputStream();
|
||||||
|
byte[] buffer = new byte[1024];
|
||||||
|
for (;;) {
|
||||||
|
int nr = in.read(buffer);
|
||||||
|
|
||||||
|
if (nr == -1)
|
||||||
|
break;
|
||||||
|
if (nr == 0)
|
||||||
|
throw new IOException("Read timed out");
|
||||||
|
|
||||||
|
o.write(buffer, 0, nr);
|
||||||
|
}
|
||||||
|
return o.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("rawtypes")
|
||||||
|
public Object loadResponse(InputStream in, Object expectedID, boolean close) throws IOException, BitcoinRPCException {
|
||||||
|
try {
|
||||||
|
String r = new String(loadStream(in, close), QUERY_CHARSET);
|
||||||
|
log.debug("Bitcoin JSON-RPC response: " + r);
|
||||||
|
try {
|
||||||
|
JSONParser jsonParser = new JSONParser();
|
||||||
|
Map response = (Map) jsonParser.parse(r);
|
||||||
|
|
||||||
|
if (!expectedID.equals(response.get("id")))
|
||||||
|
throw new BitcoinRPCException("Wrong response ID (expected: " + String.valueOf(expectedID) + ", response: " + response.get("id") + ")");
|
||||||
|
|
||||||
|
if (response.get("error") != null)
|
||||||
|
throw new BitcoinRPCException(new BitcoinRPCError((Map)response.get("error")));
|
||||||
|
|
||||||
|
return response.get("result");
|
||||||
|
} catch (ClassCastException | ParseException ex) {
|
||||||
|
throw new BitcoinRPCException("Invalid server response format (data: \"" + r + "\")");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (close)
|
||||||
|
in.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRawTransaction(String txId) throws BitcoinRPCException {
|
||||||
|
return (String) query("getrawtransaction", txId);
|
||||||
|
}
|
||||||
|
}
|
23
src/main/java/com/craigraw/drongo/rpc/BitcoinRPCError.java
Normal file
23
src/main/java/com/craigraw/drongo/rpc/BitcoinRPCError.java
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
package com.craigraw.drongo.rpc;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class BitcoinRPCError {
|
||||||
|
private int code;
|
||||||
|
private String message;
|
||||||
|
|
||||||
|
@SuppressWarnings({ "rawtypes" })
|
||||||
|
public BitcoinRPCError(Map errorMap) {
|
||||||
|
Number n = (Number) errorMap.get("code");
|
||||||
|
this.code = n != null ? n.intValue() : 0;
|
||||||
|
this.message = (String) errorMap.get("message");
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCode() {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMessage() {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
}
|
115
src/main/java/com/craigraw/drongo/rpc/BitcoinRPCException.java
Normal file
115
src/main/java/com/craigraw/drongo/rpc/BitcoinRPCException.java
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
package com.craigraw.drongo.rpc;
|
||||||
|
|
||||||
|
import org.json.simple.JSONObject;
|
||||||
|
import org.json.simple.parser.JSONParser;
|
||||||
|
import org.json.simple.parser.ParseException;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class BitcoinRPCException extends RuntimeException {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(BitcoinJSONRPCClient.class);
|
||||||
|
|
||||||
|
private String rpcMethod;
|
||||||
|
private String rpcParams;
|
||||||
|
private int responseCode;
|
||||||
|
private String responseMessage;
|
||||||
|
private String response;
|
||||||
|
private BitcoinRPCError rpcError;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance of <code>BitcoinRPCException</code> with response
|
||||||
|
* detail.
|
||||||
|
*
|
||||||
|
* @param method the rpc method called
|
||||||
|
* @param params the parameters sent
|
||||||
|
* @param responseCode the HTTP code received
|
||||||
|
* @param responseMessage the HTTP response message
|
||||||
|
* @param response the error stream received
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("rawtypes")
|
||||||
|
public BitcoinRPCException(String method,
|
||||||
|
String params,
|
||||||
|
int responseCode,
|
||||||
|
String responseMessage,
|
||||||
|
String response) {
|
||||||
|
super("RPC Query Failed (method: " + method + ", params: " + params + ", response code: " + responseCode + " responseMessage " + responseMessage + ", response: " + response);
|
||||||
|
this.rpcMethod = method;
|
||||||
|
this.rpcParams = params;
|
||||||
|
this.responseCode = responseCode;
|
||||||
|
this.responseMessage = responseMessage;
|
||||||
|
this.response = response;
|
||||||
|
if ( responseCode == 500 ) {
|
||||||
|
// Bitcoind application error when handle the request
|
||||||
|
// extract code/message for callers to handle
|
||||||
|
try {
|
||||||
|
JSONParser jsonParser = new JSONParser();
|
||||||
|
Map error = (Map) ((Map)jsonParser.parse(response)).get("error");
|
||||||
|
if ( error != null ) {
|
||||||
|
rpcError = new BitcoinRPCError(error);
|
||||||
|
}
|
||||||
|
} catch(ParseException e) {
|
||||||
|
log.error("Could not parse bitcoind error", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public BitcoinRPCException(String method, String params, Throwable cause) {
|
||||||
|
super("RPC Query Failed (method: " + method + ", params: " + params + ")", cause);
|
||||||
|
this.rpcMethod = method;
|
||||||
|
this.rpcParams = params;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an instance of <code>BitcoinRPCException</code> with the
|
||||||
|
* specified detail message.
|
||||||
|
*
|
||||||
|
* @param msg the detail message.
|
||||||
|
*/
|
||||||
|
public BitcoinRPCException(String msg) {
|
||||||
|
super(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BitcoinRPCException(BitcoinRPCError error) {
|
||||||
|
super(error.getMessage());
|
||||||
|
this.rpcError = error;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BitcoinRPCException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getResponseCode() {
|
||||||
|
return responseCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRpcMethod() {
|
||||||
|
return rpcMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRpcParams() {
|
||||||
|
return rpcParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the HTTP response message
|
||||||
|
*/
|
||||||
|
public String getResponseMessage() {
|
||||||
|
return responseMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return response message from bitcored
|
||||||
|
*/
|
||||||
|
public String getResponse() {
|
||||||
|
return this.response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return response message from bitcored
|
||||||
|
*/
|
||||||
|
public BitcoinRPCError getRPCError() {
|
||||||
|
return this.rpcError;
|
||||||
|
}
|
||||||
|
}
|
10
src/main/resources/log4j.properties
Normal file
10
src/main/resources/log4j.properties
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
log4j.rootLogger=INFO, stdout, file
|
||||||
|
|
||||||
|
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
|
||||||
|
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
|
||||||
|
log4j.appender.stdout.layout.ConversionPattern=[%-5p] %d %c - %m%n
|
||||||
|
|
||||||
|
log4j.appender.file=org.apache.log4j.FileAppender
|
||||||
|
log4j.appender.file.File=sentinel.log
|
||||||
|
log4j.appender.file.layout=org.apache.log4j.PatternLayout
|
||||||
|
log4j.appender.file.layout.ConversionPattern=[%-5p] %d %c - %m%n
|
43
src/test/java/com/craigraw/drongo/OutputDescriptorTest.java
Normal file
43
src/test/java/com/craigraw/drongo/OutputDescriptorTest.java
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
package com.craigraw.drongo;
|
||||||
|
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
public class OutputDescriptorTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void electrumP2PKH() {
|
||||||
|
OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("xpub661MyMwAqRbcFT5HwyRoP5hebbeRDvy2RGDTH2uxFyDPaf5FLtu4njuishddViQxTABZKzoWKuwpy6MsgfPvTw9pKnRGDL5eBxDej9kF54Z");
|
||||||
|
Assert.assertEquals("pkh(xpub6BemYiVEULcbpkxh3wp6KUzfzGPFL7JNcxbfQcXxGnJ6sPugTkR69neX8RT9iXdMHFV1FCge72a21WpoHjgoeBTcZju3JKyFf9DztGT2FhE/0/*)", descriptor.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void iancolemanP2PKH() {
|
||||||
|
OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("xpub6EEznxrqoN5HUXfD3QC3B8Vjw8Lj9UnRj17uTzNaBnEYN5xgwe6Un46Z443sSTBP2bzLZuDzygkdD1FtVWSexFmg4yAuCTxE2HxXFtz541z");
|
||||||
|
Assert.assertEquals("pkh(xpub6EEznxrqoN5HUXfD3QC3B8Vjw8Lj9UnRj17uTzNaBnEYN5xgwe6Un46Z443sSTBP2bzLZuDzygkdD1FtVWSexFmg4yAuCTxE2HxXFtz541z/*)", descriptor.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void electrumP2WPKH() {
|
||||||
|
OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("zpub6njbcfTHEfK4U96Z8dBaTULdb1LGWMtj73yYZ76kfmE9nuf3KhNSsXfzDefz5KV6TreWjnQbgvnSmSttudzTugesV2HFunYu7gWYJUD4eoR");
|
||||||
|
Assert.assertEquals("wpkh(xpub6CqLiu9VMua6V5yFXtXrfZgJqWsG2a8dQdBuk34KFdCCYXvCtx41CmWugPJVZNzBXyHCWy8uHgVUMpePCxh2S3VXueYG8dWLDh49dQ9MJGu/0/*)", descriptor.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void iancolemanP2SHP2WPKH() {
|
||||||
|
OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("ypub6Zken22QbjfomRUXki5v4ndP6T1DEtaBhGGZBvR4ocoooM44dFmnF8DyFmvcK76TKnuvdFfaPnicVvTAPdqEcbuEfKEqfnRoUjSkTB4u1os");
|
||||||
|
Assert.assertEquals("sh(wpkh(xpub6EvPUMMVT48Kv8HQvMJHrhXsvUrmJGagn9kLQXXBRcRvkFEqNbcDd4ZqEZy2KCSXv9o7sn51w8N4cdqbfwRDpNDdnyYR5scKD1P74ZAKbGm/*))", descriptor.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void bip84P2WPKH() {
|
||||||
|
OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs");
|
||||||
|
Assert.assertEquals("wpkh(xpub6CatWdiZiodmUeTDp8LT5or8nmbKNcuyvz7WyksVFkKB4RHwCD3XyuvPEbvqAQY3rAPshWcMLoP2fMFMKHPJ4ZeZXYVUhLv1VMrjPC7PW6V/0/*)", descriptor.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void redditP2SHP2WPKH() {
|
||||||
|
OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("ypub6XiW9nhToS1gjVsFKzgmtWZuqo6V1YY7xaCns37aR3oYhFyAsTehAqV1iW2UCNtgWFQFkz3aNSZZbkfe5d1tD8MzjZuFJQn2XnczsxtjoXr");
|
||||||
|
Assert.assertEquals("sh(wpkh(xpub6CtEr82YekUCtCg8Vdu9gRUQfpx34vYd3Tga5eDh33RfeA9wcoV8YmpshJ4tCUEm6cHT1WT1unD1iU45MvbsQtgPsECpiVxYG4ZMVKEKqGP/0/*))", descriptor.toString());
|
||||||
|
}
|
||||||
|
}
|
58
src/test/java/com/craigraw/drongo/WatchWalletTest.java
Normal file
58
src/test/java/com/craigraw/drongo/WatchWalletTest.java
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
package com.craigraw.drongo;
|
||||||
|
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
public class WatchWalletTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void electrumP2PKH() {
|
||||||
|
WatchWallet wallet = new WatchWallet("", "xpub661MyMwAqRbcFT5HwyRoP5hebbeRDvy2RGDTH2uxFyDPaf5FLtu4njuishddViQxTABZKzoWKuwpy6MsgfPvTw9pKnRGDL5eBxDej9kF54Z");
|
||||||
|
|
||||||
|
Assert.assertEquals("1QEjP9f7KRtJobfwmRuykpLjaR5QchGo8q", wallet.getReceivingAddress(0).toString());
|
||||||
|
Assert.assertEquals("17kCok3XAUHyL6kjzBF44e1YuzMmRXPuu5", wallet.getReceivingAddress(1).toString());
|
||||||
|
Assert.assertEquals("1Dh3Lofy2cFdEQ2rk4Eq6fbPeQQ63pDdRN", wallet.getChangeAddress(0).toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void iancolemanP2PKH() {
|
||||||
|
WatchWallet wallet = new WatchWallet("", "xpub6EEznxrqoN5HUXfD3QC3B8Vjw8Lj9UnRj17uTzNaBnEYN5xgwe6Un46Z443sSTBP2bzLZuDzygkdD1FtVWSexFmg4yAuCTxE2HxXFtz541z");
|
||||||
|
|
||||||
|
Assert.assertEquals("179cMrkiyx6zD2E1sqBAQLg1SQPAS5vjQW", wallet.getReceivingAddress(0).toString());
|
||||||
|
Assert.assertEquals("1GdWCzdt5oDYh5n1qeZQCxg5rQKVTuTMJg", wallet.getReceivingAddress(1).toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void electrumP2WPKH() {
|
||||||
|
WatchWallet wallet = new WatchWallet("", "zpub6njbcfTHEfK4U96Z8dBaTULdb1LGWMtj73yYZ76kfmE9nuf3KhNSsXfzDefz5KV6TreWjnQbgvnSmSttudzTugesV2HFunYu7gWYJUD4eoR");
|
||||||
|
|
||||||
|
Assert.assertEquals("bc1q4s5v0u9qmmcp25mnr3mfzhyftjzw8mccqawmwf", wallet.getReceivingAddress(0).toString());
|
||||||
|
Assert.assertEquals("bc1qffy90ge6wljh53t07q4al2pgsmuqgy48wrk8wq", wallet.getReceivingAddress(1).toString());
|
||||||
|
Assert.assertEquals("bc1q87fg9yjxratt4hemjn0m4re97n2p39ssq5xhv4", wallet.getChangeAddress(0).toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void iancolemanP2SHP2WPKH() {
|
||||||
|
WatchWallet wallet = new WatchWallet("", "ypub6Zken22QbjfomRUXki5v4ndP6T1DEtaBhGGZBvR4ocoooM44dFmnF8DyFmvcK76TKnuvdFfaPnicVvTAPdqEcbuEfKEqfnRoUjSkTB4u1os");
|
||||||
|
|
||||||
|
Assert.assertEquals("34SgiHwNwJt3nYCVUQcgJWhefVRBZ4aSHf", wallet.getReceivingAddress(0).toString());
|
||||||
|
Assert.assertEquals("3MgPnbF6UYM3FBhZWXoL2ebLPEa3zCCXLh", wallet.getReceivingAddress(1).toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void bip84P2WPKH() {
|
||||||
|
WatchWallet wallet = new WatchWallet("", "zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs");
|
||||||
|
|
||||||
|
Assert.assertEquals("bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu", wallet.getReceivingAddress(0).toString());
|
||||||
|
Assert.assertEquals("bc1qnjg0jd8228aq7egyzacy8cys3knf9xvrerkf9g", wallet.getReceivingAddress(1).toString());
|
||||||
|
Assert.assertEquals("bc1q8c6fshw2dlwun7ekn9qwf37cu2rn755upcp6el", wallet.getChangeAddress(0).toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void redditP2SHP2WPKH() {
|
||||||
|
WatchWallet wallet = new WatchWallet("", "ypub6XiW9nhToS1gjVsFKzgmtWZuqo6V1YY7xaCns37aR3oYhFyAsTehAqV1iW2UCNtgWFQFkz3aNSZZbkfe5d1tD8MzjZuFJQn2XnczsxtjoXr");
|
||||||
|
|
||||||
|
Assert.assertEquals("34TBBnwqv338BT6BVnTKqziFq8HWY6BNbw", wallet.getReceivingAddress(0).toString());
|
||||||
|
Assert.assertEquals("35Jhf9LGCpb1ihJjWH7uLZ8othr1diuspS", wallet.getChangeAddress(0).toString());
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue