Merge branch 'master' of https://github.com/sparrowwallet/drongo into update-dec-2022

This commit is contained in:
HashEngineering 2022-12-27 21:38:23 -08:00
commit 131478dcd0
No known key found for this signature in database
GPG key ID: A615EB0C5CEBDEDE
113 changed files with 7193 additions and 1362 deletions

View file

@ -4,35 +4,30 @@ buildscript {
url "https://plugins.gradle.org/m2/" url "https://plugins.gradle.org/m2/"
} }
} }
dependencies {
classpath "org.javamodularity:moduleplugin:1.6.0"
}
} }
plugins { plugins {
id 'java' id 'java-library'
id 'extra-java-module-info'
id 'com.github.johnrengelman.shadow' version '5.2.0' id 'com.github.johnrengelman.shadow' version '5.2.0'
} }
def javamodularityPluginId = 'org.javamodularity.moduleplugin'
final hasPlugin = project.getPlugins().hasPlugin(javamodularityPluginId);
if(hasPlugin) {
final Plugin plugin = project.getPlugins().getPlugin(javamodularityPluginId)
println 'Plugin already applied - version ' + plugin.properties['javamodularityPluginId']
} else {
apply plugin: "org.javamodularity.moduleplugin"
}
tasks.withType(AbstractArchiveTask) { tasks.withType(AbstractArchiveTask) {
preserveFileTimestamps = false preserveFileTimestamps = false
reproducibleFileOrder = true reproducibleFileOrder = true
} }
group 'com.sparrowwallet' group 'com.sparrowwallet'
version '0.9' version '1.0'
sourceCompatibility = 1.9 def os = org.gradle.internal.os.OperatingSystem.current()
targetCompatibility = 1.9 def osName = os.getFamilyName()
if(os.macOsX) {
osName = "osx"
}
sourceCompatibility = 16
targetCompatibility = 16
repositories { repositories {
mavenCentral() mavenCentral()
@ -49,13 +44,17 @@ dependencies {
implementation ('org.bouncycastle:bcprov-jdk15on:1.64') { implementation ('org.bouncycastle:bcprov-jdk15on:1.64') {
exclude group: 'org.hamcrest', module: 'hamcrest-core' exclude group: 'org.hamcrest', module: 'hamcrest-core'
} }
implementation ('de.mkammerer:argon2-jvm:2.7') { implementation ('de.mkammerer:argon2-jvm:2.11') {
exclude group: 'org.hamcrest', module: 'hamcrest-core' exclude group: 'org.hamcrest', module: 'hamcrest-core'
exclude group: 'junit', module: 'junit' exclude group: 'junit', module: 'junit'
exclude group: 'net.java.dev.jna', module: 'jna'
} }
implementation ('ch.qos.logback:logback-classic:1.2.3') { implementation ('net.java.dev.jna:jna:5.8.0')
implementation ('ch.qos.logback:logback-classic:1.2.8') {
exclude group: 'org.hamcrest', module: 'hamcrest-core' exclude group: 'org.hamcrest', module: 'hamcrest-core'
exclude group: 'org.slf4j'
} }
implementation ('org.slf4j:slf4j-api:1.7.30')
testImplementation ('junit:junit:4.12') { testImplementation ('junit:junit:4.12') {
exclude group: 'org.hamcrest', module: 'hamcrest-core' exclude group: 'org.hamcrest', module: 'hamcrest-core'
} }
@ -63,8 +62,16 @@ dependencies {
implementation 'de.sfuhrm:saphir-hash-core:3.0.5' implementation 'de.sfuhrm:saphir-hash-core:3.0.5'
} }
processResources {
doLast {
delete fileTree("$buildDir/resources/main/native").matching {
exclude "${osName}/**"
}
}
}
task(runDrongo, dependsOn: 'classes', type: JavaExec) { task(runDrongo, dependsOn: 'classes', type: JavaExec) {
main = 'com.sparrowwallet.drongo.Main' mainClass = 'com.sparrowwallet.drongo.Main'
classpath = sourceSets.main.runtimeClasspath classpath = sourceSets.main.runtimeClasspath
args 'drongo.properties' args 'drongo.properties'
} }
@ -84,3 +91,30 @@ shadowJar {
archiveVersion = '0.9' archiveVersion = '0.9'
classifier = 'all' classifier = 'all'
} }
extraJavaModuleInfo {
module('logback-core-1.2.8.jar', 'logback.core', '1.2.8') {
exports('ch.qos.logback.core')
exports('ch.qos.logback.core.spi')
requires('java.xml')
}
module('logback-classic-1.2.8.jar', 'logback.classic', '1.2.8') {
exports('ch.qos.logback.classic')
exports('ch.qos.logback.classic.spi')
requires('org.slf4j')
requires('logback.core')
requires('java.xml')
requires('java.logging')
}
module('jeromq-0.5.0.jar', 'jeromq', '0.5.0') {
exports('org.zeromq')
}
module('json-simple-1.1.1.jar', 'json.simple', '1.1.1') {
exports('org.json.simple')
exports('org.json.simple.parser')
}
module('jnacl-1.0.0.jar', 'eu.neilalexander.jnacl', '1.0.0')
module('junit-4.12.jar', 'junit', '4.12') {
exports('org.junit')
}
}

21
buildSrc/build.gradle Normal file
View file

@ -0,0 +1,21 @@
plugins {
id 'java-gradle-plugin' // so we can assign and ID to our plugin
}
dependencies {
implementation 'org.ow2.asm:asm:8.0.1'
}
repositories {
mavenCentral()
}
gradlePlugin {
plugins {
// here we register our plugin with an ID
register("extra-java-module-info") {
id = "extra-java-module-info"
implementationClass = "org.gradle.sample.transform.javamodules.ExtraModuleInfoPlugin"
}
}
}

View file

@ -0,0 +1,54 @@
package org.gradle.sample.transform.javamodules;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.attributes.Attribute;
import org.gradle.api.plugins.JavaPlugin;
/**
* Entry point of our plugin that should be applied in the root project.
*/
public class ExtraModuleInfoPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
// register the plugin extension as 'extraJavaModuleInfo {}' configuration block
ExtraModuleInfoPluginExtension extension = project.getObjects().newInstance(ExtraModuleInfoPluginExtension.class);
project.getExtensions().add(ExtraModuleInfoPluginExtension.class, "extraJavaModuleInfo", extension);
// setup the transform for all projects in the build
project.getPlugins().withType(JavaPlugin.class).configureEach(javaPlugin -> configureTransform(project, extension));
}
private void configureTransform(Project project, ExtraModuleInfoPluginExtension extension) {
Attribute<String> artifactType = Attribute.of("artifactType", String.class);
Attribute<Boolean> javaModule = Attribute.of("javaModule", Boolean.class);
// compile and runtime classpath express that they only accept modules by requesting the javaModule=true attribute
project.getConfigurations().matching(this::isResolvingJavaPluginConfiguration).all(
c -> c.getAttributes().attribute(javaModule, true));
// all Jars have a javaModule=false attribute by default; the transform also recognizes modules and returns them without modification
project.getDependencies().getArtifactTypes().getByName("jar").getAttributes().attribute(javaModule, false);
// register the transform for Jars and "javaModule=false -> javaModule=true"; the plugin extension object fills the input parameter
project.getDependencies().registerTransform(ExtraModuleInfoTransform.class, t -> {
t.parameters(p -> {
p.setModuleInfo(extension.getModuleInfo());
p.setAutomaticModules(extension.getAutomaticModules());
});
t.getFrom().attribute(artifactType, "jar").attribute(javaModule, false);
t.getTo().attribute(artifactType, "jar").attribute(javaModule, true);
});
}
private boolean isResolvingJavaPluginConfiguration(Configuration configuration) {
if (!configuration.isCanBeResolved()) {
return false;
}
return configuration.getName().endsWith(JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME.substring(1))
|| configuration.getName().endsWith(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME.substring(1))
|| configuration.getName().endsWith(JavaPlugin.ANNOTATION_PROCESSOR_CONFIGURATION_NAME.substring(1));
}
}

View file

@ -0,0 +1,52 @@
package org.gradle.sample.transform.javamodules;
import org.gradle.api.Action;
import javax.annotation.Nullable;
import java.util.HashMap;
import java.util.Map;
/**
* A data class to collect all the module information we want to add.
* Here the class is used as extension that can be configured in the build script
* and as input to the ExtraModuleInfoTransform that add the information to Jars.
*/
public class ExtraModuleInfoPluginExtension {
private final Map<String, ModuleInfo> moduleInfo = new HashMap<>();
private final Map<String, String> automaticModules = new HashMap<>();
/**
* Add full module information for a given Jar file.
*/
public void module(String jarName, String moduleName, String moduleVersion) {
module(jarName, moduleName, moduleVersion, null);
}
/**
* Add full module information, including exported packages and dependencies, for a given Jar file.
*/
public void module(String jarName, String moduleName, String moduleVersion, @Nullable Action<? super ModuleInfo> conf) {
ModuleInfo moduleInfo = new ModuleInfo(moduleName, moduleVersion);
if (conf != null) {
conf.execute(moduleInfo);
}
this.moduleInfo.put(jarName, moduleInfo);
}
/**
* Add only an automatic module name to a given jar file.
*/
public void automaticModule(String jarName, String moduleName) {
automaticModules.put(jarName, moduleName);
}
protected Map<String, ModuleInfo> getModuleInfo() {
return moduleInfo;
}
protected Map<String, String> getAutomaticModules() {
return automaticModules;
}
}

View file

@ -0,0 +1,164 @@
package org.gradle.sample.transform.javamodules;
import org.gradle.api.artifacts.transform.InputArtifact;
import org.gradle.api.artifacts.transform.TransformAction;
import org.gradle.api.artifacts.transform.TransformOutputs;
import org.gradle.api.artifacts.transform.TransformParameters;
import org.gradle.api.file.FileSystemLocation;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.Input;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.ModuleVisitor;
import org.objectweb.asm.Opcodes;
import java.io.*;
import java.util.Collections;
import java.util.Map;
import java.util.jar.*;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
/**
* An artifact transform that applies additional information to Jars without module information.
* The transformation fails the build if a Jar does not contain information and no extra information
* was defined for it. This way we make sure that all Jars are turned into modules.
*/
abstract public class ExtraModuleInfoTransform implements TransformAction<ExtraModuleInfoTransform.Parameter> {
public static class Parameter implements TransformParameters, Serializable {
private Map<String, ModuleInfo> moduleInfo = Collections.emptyMap();
private Map<String, String> automaticModules = Collections.emptyMap();
@Input
public Map<String, ModuleInfo> getModuleInfo() {
return moduleInfo;
}
@Input
public Map<String, String> getAutomaticModules() {
return automaticModules;
}
public void setModuleInfo(Map<String, ModuleInfo> moduleInfo) {
this.moduleInfo = moduleInfo;
}
public void setAutomaticModules(Map<String, String> automaticModules) {
this.automaticModules = automaticModules;
}
}
@InputArtifact
protected abstract Provider<FileSystemLocation> getInputArtifact();
@Override
public void transform(TransformOutputs outputs) {
Map<String, ModuleInfo> moduleInfo = getParameters().moduleInfo;
Map<String, String> automaticModules = getParameters().automaticModules;
File originalJar = getInputArtifact().get().getAsFile();
String originalJarName = originalJar.getName();
if (isModule(originalJar)) {
outputs.file(originalJar);
} else if (moduleInfo.containsKey(originalJarName)) {
addModuleDescriptor(originalJar, getModuleJar(outputs, originalJar), moduleInfo.get(originalJarName));
} else if (isAutoModule(originalJar)) {
outputs.file(originalJar);
} else if (automaticModules.containsKey(originalJarName)) {
addAutomaticModuleName(originalJar, getModuleJar(outputs, originalJar), automaticModules.get(originalJarName));
} else {
throw new RuntimeException("Not a module and no mapping defined: " + originalJarName);
}
}
private boolean isModule(File jar) {
Pattern moduleInfoClassMrjarPath = Pattern.compile("META-INF/versions/\\d+/module-info.class");
try (JarInputStream inputStream = new JarInputStream(new FileInputStream(jar))) {
boolean isMultiReleaseJar = containsMultiReleaseJarEntry(inputStream);
ZipEntry next = inputStream.getNextEntry();
while (next != null) {
if ("module-info.class".equals(next.getName())) {
return true;
}
if (isMultiReleaseJar && moduleInfoClassMrjarPath.matcher(next.getName()).matches()) {
return true;
}
next = inputStream.getNextEntry();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
return false;
}
private boolean containsMultiReleaseJarEntry(JarInputStream jarStream) {
Manifest manifest = jarStream.getManifest();
return manifest != null && Boolean.parseBoolean(manifest.getMainAttributes().getValue("Multi-Release"));
}
private boolean isAutoModule(File jar) {
try (JarInputStream inputStream = new JarInputStream(new FileInputStream(jar))) {
return inputStream.getManifest().getMainAttributes().getValue("Automatic-Module-Name") != null;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private File getModuleJar(TransformOutputs outputs, File originalJar) {
return outputs.file(originalJar.getName().substring(0, originalJar.getName().lastIndexOf('.')) + "-module.jar");
}
private static void addAutomaticModuleName(File originalJar, File moduleJar, String moduleName) {
try (JarInputStream inputStream = new JarInputStream(new FileInputStream(originalJar))) {
Manifest manifest = inputStream.getManifest();
manifest.getMainAttributes().put(new Attributes.Name("Automatic-Module-Name"), moduleName);
try (JarOutputStream outputStream = new JarOutputStream(new FileOutputStream(moduleJar), inputStream.getManifest())) {
copyEntries(inputStream, outputStream);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private static void addModuleDescriptor(File originalJar, File moduleJar, ModuleInfo moduleInfo) {
try (JarInputStream inputStream = new JarInputStream(new FileInputStream(originalJar))) {
try (JarOutputStream outputStream = new JarOutputStream(new FileOutputStream(moduleJar), inputStream.getManifest())) {
copyEntries(inputStream, outputStream);
outputStream.putNextEntry(new JarEntry("module-info.class"));
outputStream.write(addModuleInfo(moduleInfo));
outputStream.closeEntry();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private static void copyEntries(JarInputStream inputStream, JarOutputStream outputStream) throws IOException {
JarEntry jarEntry = inputStream.getNextJarEntry();
while (jarEntry != null) {
outputStream.putNextEntry(jarEntry);
outputStream.write(inputStream.readAllBytes());
outputStream.closeEntry();
jarEntry = inputStream.getNextJarEntry();
}
}
private static byte[] addModuleInfo(ModuleInfo moduleInfo) {
ClassWriter classWriter = new ClassWriter(0);
classWriter.visit(Opcodes.V9, Opcodes.ACC_MODULE, "module-info", null, null, null);
ModuleVisitor moduleVisitor = classWriter.visitModule(moduleInfo.getModuleName(), Opcodes.ACC_OPEN, moduleInfo.getModuleVersion());
for (String packageName : moduleInfo.getExports()) {
moduleVisitor.visitExport(packageName.replace('.', '/'), 0);
}
moduleVisitor.visitRequire("java.base", 0, null);
for (String requireName : moduleInfo.getRequires()) {
moduleVisitor.visitRequire(requireName, 0, null);
}
for (String requireName : moduleInfo.getRequiresTransitive()) {
moduleVisitor.visitRequire(requireName, Opcodes.ACC_TRANSITIVE, null);
}
moduleVisitor.visitEnd();
classWriter.visitEnd();
return classWriter.toByteArray();
}
}

View file

@ -0,0 +1,53 @@
package org.gradle.sample.transform.javamodules;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* Data class to hold the information that should be added as module-info.class to an existing Jar file.
*/
public class ModuleInfo implements Serializable {
private String moduleName;
private String moduleVersion;
private List<String> exports = new ArrayList<>();
private List<String> requires = new ArrayList<>();
private List<String> requiresTransitive = new ArrayList<>();
ModuleInfo(String moduleName, String moduleVersion) {
this.moduleName = moduleName;
this.moduleVersion = moduleVersion;
}
public void exports(String exports) {
this.exports.add(exports);
}
public void requires(String requires) {
this.requires.add(requires);
}
public void requiresTransitive(String requiresTransitive) {
this.requiresTransitive.add(requiresTransitive);
}
public String getModuleName() {
return moduleName;
}
protected String getModuleVersion() {
return moduleVersion;
}
protected List<String> getExports() {
return exports;
}
protected List<String> getRequires() {
return requires;
}
protected List<String> getRequiresTransitive() {
return requiresTransitive;
}
}

Binary file not shown.

View file

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

275
gradlew vendored
View file

@ -1,7 +1,7 @@
#!/usr/bin/env sh #!/bin/sh
# #
# Copyright 2015 the original author or authors. # Copyright © 2015-2021 the original authors.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@ -17,78 +17,113 @@
# #
############################################################################## ##############################################################################
## #
## Gradle start up script for UN*X # Gradle start up script for POSIX generated by Gradle.
## #
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
############################################################################## ##############################################################################
# Attempt to set APP_HOME # Attempt to set APP_HOME
# Resolve links: $0 may be a link # Resolve links: $0 may be a link
PRG="$0" app_path=$0
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do # Need this for daisy-chained symlinks.
ls=`ls -ld "$PRG"` while
link=`expr "$ls" : '.*-> \(.*\)$'` APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
if expr "$link" : '/.*' > /dev/null; then [ -h "$app_path" ]
PRG="$link" do
else ls=$( ls -ld "$app_path" )
PRG=`dirname "$PRG"`"/$link" link=${ls#*' -> '}
fi case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle" APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"` APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum" MAX_FD=maximum
warn () { warn () {
echo "$*" echo "$*"
} } >&2
die () { die () {
echo echo
echo "$*" echo "$*"
echo echo
exit 1 exit 1
} } >&2
# OS specific support (must be 'true' or 'false'). # OS specific support (must be 'true' or 'false').
cygwin=false cygwin=false
msys=false msys=false
darwin=false darwin=false
nonstop=false nonstop=false
case "`uname`" in case "$( uname )" in #(
CYGWIN* ) CYGWIN* ) cygwin=true ;; #(
cygwin=true Darwin* ) darwin=true ;; #(
;; MSYS* | MINGW* ) msys=true ;; #(
Darwin* ) NONSTOP* ) nonstop=true ;;
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM. # Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables # IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java" JAVACMD=$JAVA_HOME/jre/sh/java
else else
JAVACMD="$JAVA_HOME/bin/java" JAVACMD=$JAVA_HOME/bin/java
fi fi
if [ ! -x "$JAVACMD" ] ; then if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@ -97,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the
location of your Java installation." location of your Java installation."
fi fi
else else
JAVACMD="java" 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. 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 Please set the JAVA_HOME variable in your environment to match the
@ -105,79 +140,101 @@ location of your Java installation."
fi fi
# Increase the maximum file descriptors if we can. # Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
MAX_FD_LIMIT=`ulimit -H -n` case $MAX_FD in #(
if [ $? -eq 0 ] ; then max*)
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD=$( ulimit -H -n ) ||
MAX_FD="$MAX_FD_LIMIT" warn "Could not query maximum file descriptor limit"
fi esac
ulimit -n $MAX_FD case $MAX_FD in #(
if [ $? -ne 0 ] ; then '' | soft) :;; #(
warn "Could not set maximum file descriptor limit: $MAX_FD" *)
fi ulimit -n "$MAX_FD" ||
else warn "Could not set maximum file descriptor limit to $MAX_FD"
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 or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; 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=`expr $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 esac
fi fi
# Escape application args # Collect all arguments for the java command, stacking in reverse order:
save () { # * args from the command line
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done # * the main class name
echo " " # * -classpath
} # * -D...appname settings
APP_ARGS=`save "$@"` # * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# Collect all arguments for the java command, following the shell quoting and substitution rules # For Cygwin or MSYS, switch paths to Windows format before running java
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@" exec "$JAVACMD" "$@"

34
gradlew.bat vendored
View file

@ -14,7 +14,7 @@
@rem limitations under the License. @rem limitations under the License.
@rem @rem
@if "%DEBUG%" == "" @echo off @if "%DEBUG%"=="" @echo off
@rem ########################################################################## @rem ##########################################################################
@rem @rem
@rem Gradle startup script for Windows @rem Gradle startup script for Windows
@ -25,7 +25,7 @@
if "%OS%"=="Windows_NT" setlocal if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0 set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=. if "%DIRNAME%"=="" set DIRNAME=.
set APP_BASE_NAME=%~n0 set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME% set APP_HOME=%DIRNAME%
@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init if %ERRORLEVEL% equ 0 goto execute
echo. echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@ -54,7 +54,7 @@ goto fail
set JAVA_HOME=%JAVA_HOME:"=% set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init if exist "%JAVA_EXE%" goto execute
echo. echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
@ -64,38 +64,26 @@ echo location of your Java installation.
goto fail 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 :execute
@rem Setup the command line @rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle @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% "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end :end
@rem End local scope for the variables with windows NT shell @rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd if %ERRORLEVEL% equ 0 goto mainEnd
:fail :fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code! rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 set EXIT_CODE=%ERRORLEVEL%
exit /b 1 if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd :mainEnd
if "%OS%"=="Windows_NT" endlocal if "%OS%"=="Windows_NT" endlocal

View file

@ -0,0 +1,20 @@
package com.sparrowwallet.drongo;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.AppenderBase;
import org.slf4j.event.Level;
import java.lang.reflect.InvocationTargetException;
public class ApplicationAppender extends AppenderBase<ILoggingEvent> {
private LogHandler callback;
@Override
protected void append(ILoggingEvent e) {
callback.handleLog(e.getThreadName(), Level.valueOf(e.getLevel().toString()), e.getMessage(), e.getLoggerName(), e.getTimeStamp(), e.getCallerData());
}
public void setCallback(String callback) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
this.callback = (LogHandler)Class.forName(callback).getConstructor().newInstance();
}
}

View file

@ -17,7 +17,7 @@ public enum BitcoinUnit {
BTC("GRS") { BTC("GRS") {
@Override @Override
public long getSatsValue(double unitValue) { public long getSatsValue(double unitValue) {
return (long)(unitValue * Transaction.SATOSHIS_PER_BITCOIN); return Math.round(unitValue * Transaction.SATOSHIS_PER_BITCOIN);
} }
@Override @Override

View file

@ -1,12 +1,15 @@
package com.sparrowwallet.drongo; package com.sparrowwallet.drongo;
import com.sparrowwallet.drongo.rpc.BitcoinJSONRPCClient; import com.sparrowwallet.drongo.rpc.BitcoinJSONRPCClient;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.slf4j.event.Level;
import org.zeromq.SocketType; import org.zeromq.SocketType;
import org.zeromq.ZContext; import org.zeromq.ZContext;
import org.zeromq.ZMQ; import org.zeromq.ZMQ;
import java.security.Provider;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
@ -74,4 +77,18 @@ public class Drongo {
public List<WatchWallet> getWallets() { public List<WatchWallet> getWallets() {
return watchWallets; return watchWallets;
} }
public static void setRootLogLevel(Level level) {
ch.qos.logback.classic.Logger root = (ch.qos.logback.classic.Logger)LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);
root.setLevel(ch.qos.logback.classic.Level.toLevel(level.toString()));
}
public static void removeRootLogAppender(String appenderName) {
ch.qos.logback.classic.Logger root = (ch.qos.logback.classic.Logger)LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);
root.detachAppender(appenderName);
}
public static Provider getProvider() {
return new BouncyCastleProvider();
}
} }

View file

@ -206,7 +206,7 @@ public class ExtendedKey {
} }
public static List<Header> getHeaders(Network network) { public static List<Header> getHeaders(Network network) {
return Arrays.stream(Header.values()).filter(header -> header.getNetwork() == network || (header.getNetwork() == Network.TESTNET && network == Network.REGTEST)).collect(Collectors.toList()); return Arrays.stream(Header.values()).filter(header -> header.getNetwork() == network || (header.getNetwork() == Network.TESTNET && network == Network.REGTEST) || (header.getNetwork() == Network.TESTNET && network == Network.SIGNET)).collect(Collectors.toList());
} }
public static Header fromExtendedKey(String xkey) { public static Header fromExtendedKey(String xkey) {

View file

@ -5,14 +5,19 @@ import com.sparrowwallet.drongo.crypto.ChildNumber;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Locale;
public class KeyDerivation { public class KeyDerivation {
private final String masterFingerprint; private final String masterFingerprint;
private final String derivationPath; private final String derivationPath;
private transient List<ChildNumber> derivation; private transient List<ChildNumber> derivation;
public KeyDerivation(String masterFingerprint, List<ChildNumber> derivation) {
this(masterFingerprint, writePath(derivation));
}
public KeyDerivation(String masterFingerprint, String derivationPath) { public KeyDerivation(String masterFingerprint, String derivationPath) {
this.masterFingerprint = masterFingerprint == null ? null : masterFingerprint.toLowerCase(); this.masterFingerprint = masterFingerprint == null ? null : masterFingerprint.toLowerCase(Locale.ROOT);
this.derivationPath = derivationPath; this.derivationPath = derivationPath;
this.derivation = parsePath(derivationPath); this.derivation = parsePath(derivationPath);
} }
@ -68,13 +73,17 @@ public class KeyDerivation {
} }
public static String writePath(List<ChildNumber> pathList) { public static String writePath(List<ChildNumber> pathList) {
String path = "m"; return writePath(pathList, true);
for (ChildNumber child: pathList) { }
path += "/";
path += child.toString(); public static String writePath(List<ChildNumber> pathList, boolean useApostrophes) {
StringBuilder path = new StringBuilder("m");
for(ChildNumber child: pathList) {
path.append("/");
path.append(child.toString(useApostrophes));
} }
return path; return path.toString();
} }
public static boolean isValid(String derivationPath) { public static boolean isValid(String derivationPath) {
@ -87,6 +96,10 @@ public class KeyDerivation {
return true; return true;
} }
public static List<ChildNumber> getBip47Derivation(int account) {
return List.of(new ChildNumber(47, true), new ChildNumber(Network.get() == Network.MAINNET ? 0 : 1, true), new ChildNumber(Math.max(0, account), true));
}
public KeyDerivation copy() { public KeyDerivation copy() {
return new KeyDerivation(masterFingerprint, derivationPath); return new KeyDerivation(masterFingerprint, derivationPath);
} }

View file

@ -2,9 +2,18 @@ package com.sparrowwallet.drongo;
import com.sparrowwallet.drongo.crypto.ChildNumber; import com.sparrowwallet.drongo.crypto.ChildNumber;
import java.util.List;
public enum KeyPurpose { public enum KeyPurpose {
RECEIVE(ChildNumber.ZERO), CHANGE(ChildNumber.ONE); RECEIVE(ChildNumber.ZERO), CHANGE(ChildNumber.ONE);
public static final List<KeyPurpose> DEFAULT_PURPOSES = List.of(RECEIVE, CHANGE);
//The receive derivation is also used for BIP47 notifications
public static final KeyPurpose NOTIFICATION = RECEIVE;
//The change derivation is reused for the send chain in BIP47 wallets
public static final KeyPurpose SEND = CHANGE;
private final ChildNumber pathIndex; private final ChildNumber pathIndex;
KeyPurpose(ChildNumber pathIndex) { KeyPurpose(ChildNumber pathIndex) {

View file

@ -0,0 +1,7 @@
package com.sparrowwallet.drongo;
import org.slf4j.event.Level;
public interface LogHandler {
void handleLog(String threadName, Level level, String message, String loggerName, long timestamp, StackTraceElement[] callerData);
}

View file

@ -0,0 +1,118 @@
package com.sparrowwallet.drongo;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.*;
/**
* A simple library class which helps with loading dynamic libraries stored in the
* JAR archive. These libraries usually contain implementation of some methods in
* native code (using JNI - Java Native Interface).
*
* @see <a href="http://adamheinrich.com/blog/2012/how-to-load-native-jni-library-from-jar">http://adamheinrich.com/blog/2012/how-to-load-native-jni-library-from-jar</a>
* @see <a href="https://github.com/adamheinrich/native-utils">https://github.com/adamheinrich/native-utils</a>
*
*/
public class NativeUtils {
/**
* The minimum length a prefix for a file has to have according to {@link File#createTempFile(String, String)}}.
*/
private static final int MIN_PREFIX_LENGTH = 3;
public static final String NATIVE_FOLDER_PATH_PREFIX = "nativeutils";
/**
* Temporary directory which will contain the DLLs.
*/
private static File temporaryDir;
/**
* Private constructor - this class will never be instanced
*/
private NativeUtils() {
}
/**
* Loads library from current JAR archive
*
* The file from JAR is copied into system temporary directory and then loaded. The temporary file is deleted after
* exiting.
* Method uses String as filename because the pathname is "abstract", not system-dependent.
*
* @param path The path of file inside JAR as absolute path (beginning with '/'), e.g. /package/File.ext
* @throws IOException If temporary file creation or read/write operation fails
* @throws IllegalArgumentException If source file (param path) does not exist
* @throws IllegalArgumentException If the path is not absolute or if the filename is shorter than three characters
* (restriction of {@link File#createTempFile(String, String)}).
* @throws FileNotFoundException If the file could not be found inside the JAR.
*/
public static void loadLibraryFromJar(String path) throws IOException {
if (null == path || !path.startsWith("/")) {
throw new IllegalArgumentException("The path has to be absolute (start with '/').");
}
// Obtain filename from path
String[] parts = path.split("/");
String filename = (parts.length > 1) ? parts[parts.length - 1] : null;
// Check if the filename is okay
if (filename == null || filename.length() < MIN_PREFIX_LENGTH) {
throw new IllegalArgumentException("The filename has to be at least 3 characters long.");
}
// Prepare temporary file
if (temporaryDir == null) {
temporaryDir = createTempDirectory(NATIVE_FOLDER_PATH_PREFIX);
temporaryDir.deleteOnExit();
}
File temp = new File(temporaryDir, filename);
try (InputStream is = NativeUtils.class.getResourceAsStream(path)) {
Files.copy(is, temp.toPath(), StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
temp.delete();
throw e;
} catch (NullPointerException e) {
temp.delete();
throw new FileNotFoundException("File " + path + " was not found inside JAR.");
}
try {
System.load(temp.getAbsolutePath());
} finally {
if (isPosixCompliant()) {
// Assume POSIX compliant file system, can be deleted after loading
temp.delete();
} else {
// Assume non-POSIX, and don't delete until last file descriptor closed
temp.deleteOnExit();
}
}
}
private static boolean isPosixCompliant() {
try {
return FileSystems.getDefault()
.supportedFileAttributeViews()
.contains("posix");
} catch (FileSystemNotFoundException
| ProviderNotFoundException
| SecurityException e) {
return false;
}
}
private static File createTempDirectory(String prefix) throws IOException {
String tempDir = System.getProperty("java.io.tmpdir");
File generatedDir = new File(tempDir, prefix + System.nanoTime());
if (!generatedDir.mkdir())
throw new IOException("Failed to create temp directory " + generatedDir.getName());
return generatedDir;
}
}

View file

@ -1,11 +1,16 @@
package com.sparrowwallet.drongo; package com.sparrowwallet.drongo;
public enum Network { import java.util.Locale;
MAINNET("mainnet", 36, "F", 5, "3", "grs", ExtendedKey.Header.xprv, ExtendedKey.Header.xpub, 1331),
TESTNET("testnet", 111, "mn", 196, "2", "tgrs", ExtendedKey.Header.tprv, ExtendedKey.Header.tpub, 17777),
REGTEST("regtest", 111, "mn", 239, "2", "grsrt", ExtendedKey.Header.tprv, ExtendedKey.Header.tpub, 18888);
Network(String name, int p2pkhAddressHeader, String p2pkhAddressPrefix, int p2shAddressHeader, String p2shAddressPrefix, String bech32AddressHrp, ExtendedKey.Header xprvHeader, ExtendedKey.Header xpubHeader, int defaultPort) { public enum Network {
MAINNET("mainnet", 36, "F", 5, "3", "grs", ExtendedKey.Header.xprv, ExtendedKey.Header.xpub, 128, 1331),
TESTNET("testnet", 111, "mn", 196, "2", "tgrs", ExtendedKey.Header.tprv, ExtendedKey.Header.tpub, 239, 17777),
REGTEST("regtest", 111, "mn", 239, "2", "grsrt", ExtendedKey.Header.tprv, ExtendedKey.Header.tpub, 239, 18888);
SIGNET("signet", 111, "mn", 196, "2", "tgrs", ExtendedKey.Header.tprv, ExtendedKey.Header.tpub, 239, 31331);
public static final String BLOCK_HEIGHT_PROPERTY = "com.sparrowwallet.blockHeight";
Network(String name, int p2pkhAddressHeader, String p2pkhAddressPrefix, int p2shAddressHeader, String p2shAddressPrefix, String bech32AddressHrp, ExtendedKey.Header xprvHeader, ExtendedKey.Header xpubHeader, int dumpedPrivateKeyHeader, int defaultPort) {
this.name = name; this.name = name;
this.p2pkhAddressHeader = p2pkhAddressHeader; this.p2pkhAddressHeader = p2pkhAddressHeader;
this.p2pkhAddressPrefix = p2pkhAddressPrefix; this.p2pkhAddressPrefix = p2pkhAddressPrefix;
@ -14,6 +19,7 @@ public enum Network {
this.bech32AddressHrp = bech32AddressHrp; this.bech32AddressHrp = bech32AddressHrp;
this.xprvHeader = xprvHeader; this.xprvHeader = xprvHeader;
this.xpubHeader = xpubHeader; this.xpubHeader = xpubHeader;
this.dumpedPrivateKeyHeader = dumpedPrivateKeyHeader;
this.defaultPort = defaultPort; this.defaultPort = defaultPort;
} }
@ -25,6 +31,7 @@ public enum Network {
private final String bech32AddressHrp; private final String bech32AddressHrp;
private final ExtendedKey.Header xprvHeader; private final ExtendedKey.Header xprvHeader;
private final ExtendedKey.Header xpubHeader; private final ExtendedKey.Header xpubHeader;
private final int dumpedPrivateKeyHeader;
private final int defaultPort; private final int defaultPort;
private static Network currentNetwork; private static Network currentNetwork;
@ -33,6 +40,10 @@ public enum Network {
return name; return name;
} }
public String toDisplayString() {
return name.substring(0, 1).toUpperCase(Locale.ROOT) + name.substring(1);
}
public int getP2PKHAddressHeader() { public int getP2PKHAddressHeader() {
return p2pkhAddressHeader; return p2pkhAddressHeader;
} }
@ -53,6 +64,10 @@ public enum Network {
return xpubHeader; return xpubHeader;
} }
public int getDumpedPrivateKeyHeader() {
return dumpedPrivateKeyHeader;
}
public int getDefaultPort() { public int getDefaultPort() {
return defaultPort; return defaultPort;
} }

View file

@ -9,10 +9,7 @@ import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ProtocolException; import com.sparrowwallet.drongo.protocol.ProtocolException;
import com.sparrowwallet.drongo.protocol.Script; import com.sparrowwallet.drongo.protocol.Script;
import com.sparrowwallet.drongo.protocol.ScriptType; import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Keystore; import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.drongo.wallet.KeystoreSource;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletModel;
import java.math.BigInteger; import java.math.BigInteger;
import java.util.*; import java.util.*;
@ -25,10 +22,11 @@ public class OutputDescriptor {
private static final String INPUT_CHARSET = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ "; private static final String INPUT_CHARSET = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ ";
private static final String CHECKSUM_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"; private static final String CHECKSUM_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
private static final Pattern XPUB_PATTERN = Pattern.compile("(\\[[^\\]]+\\])?(.pub[^/\\,)]{100,112})(/[/\\d*'hH]+)?"); private static final Pattern XPUB_PATTERN = Pattern.compile("(\\[[^\\]]+\\])?(.pub[^/\\,)]{100,112})(/[/\\d*'hH<>;]+)?");
private static final Pattern PUBKEY_PATTERN = Pattern.compile("(\\[[^\\]]+\\])?(0[23][0-9a-fA-F]{32})"); private static final Pattern PUBKEY_PATTERN = Pattern.compile("(\\[[^\\]]+\\])?(0[23][0-9a-fA-F]{32})");
private static final Pattern MULTI_PATTERN = Pattern.compile("multi\\(([\\d+])"); private static final Pattern MULTI_PATTERN = Pattern.compile("multi\\(([\\d+])");
private static final Pattern KEY_ORIGIN_PATTERN = Pattern.compile("\\[([A-Fa-f0-9]{8})([/\\d'hH]+)\\]"); private static final Pattern KEY_ORIGIN_PATTERN = Pattern.compile("\\[([A-Fa-f0-9]{8})([/\\d'hH]+)\\]");
private static final Pattern MULTIPATH_PATTERN = Pattern.compile("<([\\d*'hH;]+)>");
private static final Pattern CHECKSUM_PATTERN = Pattern.compile("#([" + CHECKSUM_CHARSET + "]{8})$"); private static final Pattern CHECKSUM_PATTERN = Pattern.compile("#([" + CHECKSUM_CHARSET + "]{8})$");
private final ScriptType scriptType; private final ScriptType scriptType;
@ -134,6 +132,10 @@ public class OutputDescriptor {
return extendedPublicKeys.size() > 1; return extendedPublicKeys.size() > 1;
} }
public boolean isCosigner() {
return !isMultisig() && scriptType.isAllowed(PolicyType.MULTI);
}
public ExtendedKey getSingletonExtendedPublicKey() { public ExtendedKey getSingletonExtendedPublicKey() {
if(isMultisig()) { if(isMultisig()) {
throw new IllegalStateException("Output descriptor contains multiple public keys but singleton requested"); throw new IllegalStateException("Output descriptor contains multiple public keys but singleton requested");
@ -235,7 +237,7 @@ public class OutputDescriptor {
public Wallet toWallet() { public Wallet toWallet() {
Wallet wallet = new Wallet(); Wallet wallet = new Wallet();
wallet.setPolicyType(isMultisig() ? PolicyType.MULTI : PolicyType.SINGLE); wallet.setPolicyType(isMultisig() || isCosigner() ? PolicyType.MULTI : PolicyType.SINGLE);
wallet.setScriptType(scriptType); wallet.setScriptType(scriptType);
for(Map.Entry<ExtendedKey,KeyDerivation> extKeyEntry : extendedPublicKeys.entrySet()) { for(Map.Entry<ExtendedKey,KeyDerivation> extKeyEntry : extendedPublicKeys.entrySet()) {
@ -252,17 +254,59 @@ public class OutputDescriptor {
return wallet; return wallet;
} }
public Wallet toKeystoreWallet(String masterFingerprint) {
Wallet wallet = new Wallet();
if(isMultisig()) {
throw new IllegalStateException("Multisig output descriptors are unsupported.");
}
ExtendedKey extendedKey = getSingletonExtendedPublicKey();
if(masterFingerprint == null) {
masterFingerprint = getKeyDerivation(extendedKey).getMasterFingerprint();
}
wallet.setScriptType(getScriptType());
Keystore keystore = new Keystore();
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, KeyDerivation.writePath(getKeyDerivation(extendedKey).getDerivation())));
keystore.setExtendedPublicKey(extendedKey);
wallet.getKeystores().add(keystore);
wallet.setDefaultPolicy(Policy.getPolicy(isCosigner() ? PolicyType.MULTI : PolicyType.SINGLE, wallet.getScriptType(), wallet.getKeystores(), 1));
return wallet;
}
public static String toDescriptorString(Address address) {
return "addr(" + address + ")";
}
public static OutputDescriptor getOutputDescriptor(Wallet wallet) { public static OutputDescriptor getOutputDescriptor(Wallet wallet) {
return getOutputDescriptor(wallet, null); return getOutputDescriptor(wallet, null);
} }
public static OutputDescriptor getOutputDescriptor(Wallet wallet, KeyPurpose keyPurpose) { public static OutputDescriptor getOutputDescriptor(Wallet wallet, KeyPurpose keyPurpose) {
return getOutputDescriptor(wallet, keyPurpose, null);
}
public static OutputDescriptor getOutputDescriptor(Wallet wallet, KeyPurpose keyPurpose, Integer index) {
return getOutputDescriptor(wallet, keyPurpose == null ? null : List.of(keyPurpose), index);
}
public static OutputDescriptor getOutputDescriptor(Wallet wallet, List<KeyPurpose> keyPurposes, Integer index) {
Map<ExtendedKey, KeyDerivation> extendedKeyDerivationMap = new LinkedHashMap<>(); Map<ExtendedKey, KeyDerivation> extendedKeyDerivationMap = new LinkedHashMap<>();
Map<ExtendedKey, String> extendedKeyChildDerivationMap = new LinkedHashMap<>(); Map<ExtendedKey, String> extendedKeyChildDerivationMap = new LinkedHashMap<>();
for(Keystore keystore : wallet.getKeystores()) { for(Keystore keystore : wallet.getKeystores()) {
extendedKeyDerivationMap.put(keystore.getExtendedPublicKey(), keystore.getKeyDerivation()); extendedKeyDerivationMap.put(keystore.getExtendedPublicKey(), keystore.getKeyDerivation());
if(keyPurpose != null) { if(keyPurposes != null) {
extendedKeyChildDerivationMap.put(keystore.getExtendedPublicKey(), keyPurpose.getPathIndex().num() + "/*"); String chain;
if(keyPurposes.size() == 1) {
chain = Integer.toString(keyPurposes.get(0).getPathIndex().num());
} else {
StringJoiner joiner = new StringJoiner(";");
keyPurposes.forEach(keyPurpose -> joiner.add(Integer.toString(keyPurpose.getPathIndex().num())));
chain = "<" + joiner + ">";
}
extendedKeyChildDerivationMap.put(keystore.getExtendedPublicKey(), chain + "/" + (index == null ? "*" : index));
} }
} }
@ -352,6 +396,17 @@ public class OutputDescriptor {
return new OutputDescriptor(scriptType, multisigThreshold, keyDerivationMap, keyChildDerivationMap); return new OutputDescriptor(scriptType, multisigThreshold, keyDerivationMap, keyChildDerivationMap);
} }
public static String normalize(String descriptor) {
String normalized = descriptor.replaceAll("'", "h");
int checksumHash = normalized.lastIndexOf('#');
if(checksumHash > -1) {
normalized = normalized.substring(0, checksumHash);
}
return normalized + "#" + getChecksum(normalized);
}
private static String getChecksum(String descriptor) { private static String getChecksum(String descriptor) {
BigInteger c = BigInteger.valueOf(1); BigInteger c = BigInteger.valueOf(1);
int cls = 0; int cls = 0;
@ -361,7 +416,7 @@ public class OutputDescriptor {
int pos = INPUT_CHARSET.indexOf(ch); int pos = INPUT_CHARSET.indexOf(ch);
if(pos < 0) { if(pos < 0) {
return ""; continue;
} }
c = polyMod(c, pos & 31); // Emit a symbol for the position inside the group, for every character. c = polyMod(c, pos & 31); // Emit a symbol for the position inside the group, for every character.
@ -394,7 +449,7 @@ public class OutputDescriptor {
private static BigInteger polyMod(BigInteger c, int val) private static BigInteger polyMod(BigInteger c, int val)
{ {
byte c0 = c.shiftRight(35).byteValue(); byte c0 = c.shiftRight(35).byteValue();
c = c.and(new BigInteger("7ffffffff", 16)).shiftLeft(5).or(BigInteger.valueOf(val)); c = c.and(new BigInteger("7ffffffff", 16)).shiftLeft(5).xor(BigInteger.valueOf(val));
if((c0 & 1) > 0) { if((c0 & 1) > 0) {
c = c.xor(new BigInteger("f5dee51989", 16)); c = c.xor(new BigInteger("f5dee51989", 16));
@ -431,7 +486,7 @@ public class OutputDescriptor {
builder.append(ScriptType.MULTISIG.getDescriptor()); builder.append(ScriptType.MULTISIG.getDescriptor());
StringJoiner joiner = new StringJoiner(","); StringJoiner joiner = new StringJoiner(",");
joiner.add(Integer.toString(multisigThreshold)); joiner.add(Integer.toString(multisigThreshold));
for(ExtendedKey pubKey : extendedPublicKeys.keySet()) { for(ExtendedKey pubKey : sortExtendedPubKeys(extendedPublicKeys.keySet())) {
String extKeyString = toString(pubKey, addKeyOrigin); String extKeyString = toString(pubKey, addKeyOrigin);
joiner.add(extKeyString); joiner.add(extKeyString);
} }
@ -452,6 +507,47 @@ public class OutputDescriptor {
return builder.toString(); return builder.toString();
} }
private List<ExtendedKey> sortExtendedPubKeys(Collection<ExtendedKey> keys) {
List<ExtendedKey> sortedKeys = new ArrayList<>(keys);
if(mapChildrenDerivations == null || mapChildrenDerivations.isEmpty() || mapChildrenDerivations.containsKey(null)) {
return sortedKeys;
}
Utils.LexicographicByteArrayComparator lexicographicByteArrayComparator = new Utils.LexicographicByteArrayComparator();
sortedKeys.sort((o1, o2) -> {
ECKey key1 = getChildKeyForExtendedPubKey(o1);
ECKey key2 = getChildKeyForExtendedPubKey(o2);
return lexicographicByteArrayComparator.compare(key1.getPubKey(), key2.getPubKey());
});
return sortedKeys;
}
private ECKey getChildKeyForExtendedPubKey(ExtendedKey extendedKey) {
if(mapChildrenDerivations.get(extendedKey) == null) {
return extendedKey.getKey();
}
List<ChildNumber> derivation = getDerivations(mapChildrenDerivations.get(extendedKey)).get(0);
derivation.add(0, extendedKey.getKeyChildNumber());
return extendedKey.getKey(derivation);
}
private List<List<ChildNumber>> getDerivations(String childDerivation) {
Matcher matcher = MULTIPATH_PATTERN.matcher(childDerivation);
if(matcher.find()) {
String multipath = matcher.group(1);
String[] paths = multipath.split(";");
List<List<ChildNumber>> derivations = new ArrayList<>();
for(String path : paths) {
derivations.add(KeyDerivation.parsePath(childDerivation.replace(matcher.group(), path)));
}
return derivations;
} else {
return List.of(KeyDerivation.parsePath(childDerivation));
}
}
private String toString(ExtendedKey pubKey, boolean addKeyOrigin) { private String toString(ExtendedKey pubKey, boolean addKeyOrigin) {
StringBuilder keyBuilder = new StringBuilder(); StringBuilder keyBuilder = new StringBuilder();
KeyDerivation keyDerivation = extendedPublicKeys.get(pubKey); KeyDerivation keyDerivation = extendedPublicKeys.get(pubKey);
@ -461,7 +557,7 @@ public class OutputDescriptor {
keyBuilder.append(keyDerivation.getMasterFingerprint()); keyBuilder.append(keyDerivation.getMasterFingerprint());
keyBuilder.append("/"); keyBuilder.append("/");
} }
keyBuilder.append(keyDerivation.getDerivationPath().replaceFirst("^m?/", "")); keyBuilder.append(keyDerivation.getDerivationPath().replaceFirst("^m?/", "").replace('\'', 'h'));
keyBuilder.append("]"); keyBuilder.append("]");
} }

View file

@ -2,6 +2,8 @@ package com.sparrowwallet.drongo;
import ch.qos.logback.core.PropertyDefinerBase; import ch.qos.logback.core.PropertyDefinerBase;
import java.util.Locale;
public class PropertyDefiner extends PropertyDefinerBase { public class PropertyDefiner extends PropertyDefinerBase {
private String application; private String application;
@ -11,15 +13,15 @@ public class PropertyDefiner extends PropertyDefinerBase {
@Override @Override
public String getPropertyValue() { public String getPropertyValue() {
if(System.getProperty(application.toLowerCase() + ".home") != null) { if(System.getProperty(application.toLowerCase(Locale.ROOT) + ".home") != null) {
return System.getProperty(application.toLowerCase() + ".home"); return System.getProperty(application.toLowerCase(Locale.ROOT) + ".home");
} }
return isWindows() ? System.getenv("APPDATA") + "/" + application.substring(0, 1).toUpperCase() + application.substring(1).toLowerCase() : System.getProperty("user.home") + "/." + application.toLowerCase(); return isWindows() ? System.getenv("APPDATA") + "/" + application.substring(0, 1).toUpperCase(Locale.ROOT) + application.substring(1).toLowerCase(Locale.ROOT) : System.getProperty("user.home") + "/." + application.toLowerCase(Locale.ROOT);
} }
private boolean isWindows() { private boolean isWindows() {
String osName = System.getProperty("os.name"); String osName = System.getProperty("os.name");
return (osName != null && osName.toLowerCase().startsWith("windows")); return (osName != null && osName.toLowerCase(Locale.ROOT).startsWith("windows"));
} }
} }

View file

@ -73,18 +73,6 @@ public class SecureString implements CharSequence {
return "Secure:XXXXX"; return "Secure:XXXXX";
} }
/**
* Called by garbage collector.
* <p>
* {@inheritDoc}
*/
@SuppressWarnings("deprecation")
@Override
public void finalize() throws Throwable {
clear();
super.finalize();
}
/** /**
* Randomly pad the characters to not store the real character in memory. * Randomly pad the characters to not store the real character in memory.
* *

View file

@ -12,9 +12,7 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.math.BigInteger; import java.math.BigInteger;
import java.nio.Buffer;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.*; import java.util.*;
@ -115,6 +113,21 @@ public class Utils {
return dest; return dest;
} }
public static void reverse(byte[] array) {
for (int i = 0; i < array.length / 2; i++) {
byte temp = array[i];
array[i] = array[array.length - i - 1];
array[array.length - i - 1] = temp;
}
}
public static byte[] concat(byte[] a, byte[] b) {
byte[] c = new byte[a.length + b.length];
System.arraycopy(a, 0, c, 0, a.length);
System.arraycopy(b, 0, c, a.length, b.length);
return c;
}
/** Parse 4 bytes from the byte array (starting at the offset) as unsigned 32-bit integer in little endian format. */ /** 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) { public static long readUint32(byte[] bytes, int offset) {
return (bytes[offset] & 0xffl) | return (bytes[offset] & 0xffl) |
@ -285,6 +298,16 @@ public class Utils {
return out; return out;
} }
public static byte[] taggedHash(String tag, byte[] msg) {
byte[] hash = Sha256Hash.hash(tag.getBytes(StandardCharsets.UTF_8));
ByteBuffer buffer = ByteBuffer.allocate(hash.length + hash.length + msg.length);
buffer.put(hash);
buffer.put(hash);
buffer.put(msg);
return Sha256Hash.hash(buffer.array());
}
public static class LexicographicByteArrayComparator implements Comparator<byte[]> { public static class LexicographicByteArrayComparator implements Comparator<byte[]> {
@Override @Override
public int compare(byte[] left, byte[] right) { public int compare(byte[] left, byte[] right) {

View file

@ -7,16 +7,17 @@ import com.sparrowwallet.drongo.protocol.Script;
import com.sparrowwallet.drongo.protocol.ScriptType; import com.sparrowwallet.drongo.protocol.ScriptType;
import java.util.Arrays; import java.util.Arrays;
import java.util.Locale;
public abstract class Address { public abstract class Address {
protected final byte[] hash; protected final byte[] data;
public Address(byte[] hash) { public Address(byte[] data) {
this.hash = hash; this.data = data;
} }
public byte[] getHash() { public byte[] getData() {
return hash; return data;
} }
public String getAddress() { public String getAddress() {
@ -24,7 +25,7 @@ public abstract class Address {
} }
public String getAddress(Network network) { public String getAddress(Network network) {
return Base58.encodeChecked(getVersion(network), hash); return Base58.encodeChecked(getVersion(network), data);
} }
public String toString() { public String toString() {
@ -43,23 +44,26 @@ public abstract class Address {
public abstract ScriptType getScriptType(); public abstract ScriptType getScriptType();
public abstract Script getOutputScript(); public Script getOutputScript() {
return getScriptType().getOutputScript(data);
}
public abstract byte[] getOutputScriptData(); public byte[] getOutputScriptData() {
return data;
}
public abstract String getOutputScriptDataType(); public abstract String getOutputScriptDataType();
public boolean equals(Object obj) { public boolean equals(Object obj) {
if(!(obj instanceof Address)) { if(!(obj instanceof Address address)) {
return false; return false;
} }
Address address = (Address)obj; return Arrays.equals(data, address.data) && getVersion(Network.get()) == address.getVersion(Network.get());
return address.getAddress().equals(this.getAddress());
} }
public int hashCode() { public int hashCode() {
return getAddress().hashCode(); return Arrays.hashCode(data) + getVersion(Network.get());
} }
public static Address fromString(String address) throws InvalidAddressException { public static Address fromString(String address) throws InvalidAddressException {
@ -103,20 +107,34 @@ public abstract class Address {
} }
} }
if(address.toLowerCase().startsWith(network.getBech32AddressHRP())) { if(address.toLowerCase(Locale.ROOT).startsWith(network.getBech32AddressHRP())) {
try { try {
Bech32.Bech32Data data = Bech32.decode(address); Bech32.Bech32Data data = Bech32.decode(address);
if(data.hrp.equals(network.getBech32AddressHRP())) { if(data.hrp.equals(network.getBech32AddressHRP())) {
int witnessVersion = data.data[0]; int witnessVersion = data.data[0];
if (witnessVersion == 0) { if(witnessVersion == 0) {
if(data.encoding != Bech32.Encoding.BECH32) {
throw new InvalidAddressException("Invalid address - witness version is 0 but encoding is " + data.encoding);
}
byte[] convertedProgram = Arrays.copyOfRange(data.data, 1, data.data.length); byte[] convertedProgram = Arrays.copyOfRange(data.data, 1, data.data.length);
byte[] witnessProgram = Bech32.convertBits(convertedProgram, 0, convertedProgram.length, 5, 8, false); byte[] witnessProgram = Bech32.convertBits(convertedProgram, 0, convertedProgram.length, 5, 8, false);
if (witnessProgram.length == 20) { if(witnessProgram.length == 20) {
return new P2WPKHAddress(witnessProgram); return new P2WPKHAddress(witnessProgram);
} }
if (witnessProgram.length == 32) { if(witnessProgram.length == 32) {
return new P2WSHAddress(witnessProgram); return new P2WSHAddress(witnessProgram);
} }
} else if(witnessVersion == 1) {
if(data.encoding != Bech32.Encoding.BECH32M) {
throw new InvalidAddressException("Invalid address - witness version is 1 but encoding is " + data.encoding);
}
byte[] convertedProgram = Arrays.copyOfRange(data.data, 1, data.data.length);
byte[] witnessProgram = Bech32.convertBits(convertedProgram, 0, convertedProgram.length, 5, 8, false);
if(witnessProgram.length == 32) {
return new P2TRAddress(witnessProgram);
}
} }
} }
} catch (Exception e) { } catch (Exception e) {

View file

@ -6,11 +6,8 @@ import com.sparrowwallet.drongo.protocol.Script;
import com.sparrowwallet.drongo.protocol.ScriptType; import com.sparrowwallet.drongo.protocol.ScriptType;
public class P2PKAddress extends Address { public class P2PKAddress extends Address {
private byte[] pubKey;
public P2PKAddress(byte[] pubKey) { public P2PKAddress(byte[] pubKey) {
super(Utils.sha256hash160(pubKey)); super(pubKey);
this.pubKey = pubKey;
} }
@Override @Override
@ -18,19 +15,15 @@ public class P2PKAddress extends Address {
return network.getP2PKHAddressHeader(); return network.getP2PKHAddressHeader();
} }
@Override
public String getAddress(Network network) {
return Utils.bytesToHex(data);
}
public ScriptType getScriptType() { public ScriptType getScriptType() {
return ScriptType.P2PK; return ScriptType.P2PK;
} }
public Script getOutputScript() {
return getScriptType().getOutputScript(pubKey);
}
@Override
public byte[] getOutputScriptData() {
return pubKey;
}
@Override @Override
public String getOutputScriptDataType() { public String getOutputScriptDataType() {
return "Public Key"; return "Public Key";

View file

@ -19,16 +19,6 @@ public class P2PKHAddress extends Address {
return ScriptType.P2PKH; return ScriptType.P2PKH;
} }
@Override
public Script getOutputScript() {
return getScriptType().getOutputScript(hash);
}
@Override
public byte[] getOutputScriptData() {
return hash;
}
@Override @Override
public String getOutputScriptDataType() { public String getOutputScriptDataType() {
return "Public Key Hash"; return "Public Key Hash";

View file

@ -20,16 +20,6 @@ public class P2SHAddress extends Address {
return ScriptType.P2SH; return ScriptType.P2SH;
} }
@Override
public Script getOutputScript() {
return getScriptType().getOutputScript(hash);
}
@Override
public byte[] getOutputScriptData() {
return hash;
}
@Override @Override
public String getOutputScriptDataType() { public String getOutputScriptDataType() {
return "Script Hash"; return "Script Hash";

View file

@ -0,0 +1,32 @@
package com.sparrowwallet.drongo.address;
import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.drongo.protocol.Bech32;
import com.sparrowwallet.drongo.protocol.Script;
import com.sparrowwallet.drongo.protocol.ScriptType;
public class P2TRAddress extends Address {
public P2TRAddress(byte[] pubKey) {
super(pubKey);
}
@Override
public int getVersion(Network network) {
return 1;
}
@Override
public String getAddress(Network network) {
return Bech32.encode(network.getBech32AddressHRP(), getVersion(), data);
}
@Override
public ScriptType getScriptType() {
return ScriptType.P2TR;
}
@Override
public String getOutputScriptDataType() {
return "Taproot";
}
}

View file

@ -17,7 +17,7 @@ public class P2WPKHAddress extends Address {
@Override @Override
public String getAddress(Network network) { public String getAddress(Network network) {
return Bech32.encode(network.getBech32AddressHRP(), getVersion(), hash); return Bech32.encode(network.getBech32AddressHRP(), getVersion(), data);
} }
@Override @Override
@ -25,16 +25,6 @@ public class P2WPKHAddress extends Address {
return ScriptType.P2WPKH; return ScriptType.P2WPKH;
} }
@Override
public Script getOutputScript() {
return getScriptType().getOutputScript(hash);
}
@Override
public byte[] getOutputScriptData() {
return hash;
}
@Override @Override
public String getOutputScriptDataType() { public String getOutputScriptDataType() {
return "Witness Public Key Hash"; return "Witness Public Key Hash";

View file

@ -15,7 +15,7 @@ public class P2WSHAddress extends Address {
@Override @Override
public String getAddress(Network network) { public String getAddress(Network network) {
return Bech32.encode(network.getBech32AddressHRP(), getVersion(), hash); return Bech32.encode(network.getBech32AddressHRP(), getVersion(), data);
} }
@Override @Override
@ -23,16 +23,6 @@ public class P2WSHAddress extends Address {
return ScriptType.P2WSH; return ScriptType.P2WSH;
} }
@Override
public Script getOutputScript() {
return getScriptType().getOutputScript(hash);
}
@Override
public byte[] getOutputScriptData() {
return hash;
}
@Override @Override
public String getOutputScriptDataType() { public String getOutputScriptDataType() {
return "Witness Script Hash"; return "Witness Script Hash";

View file

@ -0,0 +1,19 @@
package com.sparrowwallet.drongo.bip47;
public class InvalidPaymentCodeException extends Exception {
public InvalidPaymentCodeException() {
super();
}
public InvalidPaymentCodeException(String msg) {
super(msg);
}
public InvalidPaymentCodeException(Throwable cause) {
super(cause);
}
public InvalidPaymentCodeException(String message, Throwable cause) {
super(message, cause);
}
}

View file

@ -0,0 +1,19 @@
package com.sparrowwallet.drongo.bip47;
public class NotSecp256k1Exception extends Exception {
public NotSecp256k1Exception() {
super();
}
public NotSecp256k1Exception(String msg) {
super(msg);
}
public NotSecp256k1Exception(Throwable cause) {
super(cause);
}
public NotSecp256k1Exception(String message, Throwable cause) {
super(message, cause);
}
}

View file

@ -0,0 +1,104 @@
package com.sparrowwallet.drongo.bip47;
import com.sparrowwallet.drongo.crypto.ECKey;
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 java.math.BigInteger;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.spec.InvalidKeySpecException;
public class PaymentAddress {
private static final X9ECParameters CURVE_PARAMS = CustomNamedCurves.getByName("secp256k1");
private static final ECDomainParameters CURVE = new ECDomainParameters(CURVE_PARAMS.getCurve(), CURVE_PARAMS.getG(), CURVE_PARAMS.getN(), CURVE_PARAMS.getH());
private final PaymentCode paymentCode;
private final int index;
private final byte[] privKey;
public PaymentAddress(PaymentCode paymentCode, int index, byte[] privKey) {
this.paymentCode = paymentCode;
this.index = index;
this.privKey = privKey;
}
public ECKey getSendECKey() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchProviderException, IllegalStateException, InvalidKeySpecException, NotSecp256k1Exception {
return getSendECKey(getSecretPoint());
}
public ECKey getReceiveECKey() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchProviderException, IllegalStateException, InvalidKeySpecException, NotSecp256k1Exception {
return getReceiveECKey(getSecretPoint());
}
public SecretPoint getSharedSecret() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchProviderException, IllegalStateException, InvalidKeySpecException {
return sharedSecret();
}
public BigInteger getSecretPoint() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchProviderException, IllegalStateException, InvalidKeySpecException, NotSecp256k1Exception {
return secretPoint();
}
public ECPoint getECPoint() {
ECKey ecKey = ECKey.fromPublicOnly(paymentCode.getKey(index).getPubKey());
return ecKey.getPubKeyPoint();
}
public byte[] hashSharedSecret() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchProviderException, IllegalStateException, InvalidKeySpecException {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
return digest.digest(getSharedSecret().ECDHSecretAsBytes());
}
private ECPoint get_sG(BigInteger s) {
return CURVE_PARAMS.getG().multiply(s);
}
private ECKey getSendECKey(BigInteger s) throws IllegalStateException {
ECPoint ecPoint = getECPoint();
ECPoint sG = get_sG(s);
return ECKey.fromPublicOnly(ecPoint.add(sG).getEncoded(true));
}
private ECKey getReceiveECKey(BigInteger s) {
BigInteger privKeyValue = ECKey.fromPrivate(privKey).getPrivKey();
return ECKey.fromPrivate(addSecp256k1(privKeyValue, s));
}
private BigInteger addSecp256k1(BigInteger b1, BigInteger b2) {
BigInteger ret = b1.add(b2);
if(ret.bitLength() > CURVE.getN().bitLength()) {
return ret.mod(CURVE.getN());
}
return ret;
}
private SecretPoint sharedSecret() throws InvalidKeySpecException, InvalidKeyException, IllegalStateException, NoSuchAlgorithmException, NoSuchProviderException {
return new SecretPoint(privKey, paymentCode.getKey(index).getPubKey());
}
private boolean isSecp256k1(BigInteger b) {
return b.compareTo(BigInteger.ONE) > 0 && b.bitLength() <= CURVE.getN().bitLength();
}
private BigInteger secretPoint() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException, NotSecp256k1Exception {
//
// convert hash to value 's'
//
BigInteger s = new BigInteger(1, hashSharedSecret());
//
// check that 's' is on the secp256k1 curve
//
if(!isSecp256k1(s)) {
throw new NotSecp256k1Exception("Secret point not on Secp256k1 curve");
}
return s;
}
}

View file

@ -0,0 +1,393 @@
package com.sparrowwallet.drongo.bip47;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.crypto.DeterministicKey;
import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.crypto.HDKeyDerivation;
import com.sparrowwallet.drongo.protocol.*;
import com.sparrowwallet.drongo.wallet.Keystore;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
public class PaymentCode {
private static final Logger log = LoggerFactory.getLogger(PaymentCode.class);
private static final int PUBLIC_KEY_Y_OFFSET = 2;
private static final int PUBLIC_KEY_X_OFFSET = 3;
private static final int CHAIN_OFFSET = 35;
private static final int PUBLIC_KEY_X_LEN = 32;
private static final int PUBLIC_KEY_Y_LEN = 1;
private static final int CHAIN_LEN = 32;
private static final int PAYLOAD_LEN = 80;
private static final int SAMOURAI_FEATURE_BYTE = 79;
private static final int SAMOURAI_SEGWIT_BIT = 0;
private final String strPaymentCode;
private final byte[] pubkey;
private final byte[] chain;
public static final List<ScriptType> SEGWIT_SCRIPT_TYPES = List.of(ScriptType.P2PKH, ScriptType.P2SH_P2WPKH, ScriptType.P2WPKH);
public static final List<ScriptType> V1_SCRIPT_TYPES = List.of(ScriptType.P2PKH);
private PaymentCode(String strPaymentCode, byte[] pubkey, byte[] chain) {
this.strPaymentCode = strPaymentCode;
this.pubkey = pubkey;
this.chain = chain;
}
public PaymentCode(String payment_code) throws InvalidPaymentCodeException {
strPaymentCode = payment_code;
Map.Entry<byte[], byte[]> pubKeyChain = parse().entrySet().iterator().next();
this.pubkey = pubKeyChain.getKey();
this.chain = pubKeyChain.getValue();
}
public PaymentCode(byte[] payload) {
if(payload.length != 80) {
throw new IllegalArgumentException("Payment code must be 80 bytes");
}
pubkey = new byte[PUBLIC_KEY_Y_LEN + PUBLIC_KEY_X_LEN];
chain = new byte[CHAIN_LEN];
System.arraycopy(payload, PUBLIC_KEY_Y_OFFSET, pubkey, 0, PUBLIC_KEY_Y_LEN + PUBLIC_KEY_X_LEN);
System.arraycopy(payload, CHAIN_OFFSET, chain, 0, CHAIN_LEN);
strPaymentCode = makeV1();
}
public PaymentCode(byte[] pubkey, byte[] chain) {
this.pubkey = pubkey;
this.chain = chain;
strPaymentCode = makeV1();
}
public ECKey getNotificationKey() {
DeterministicKey masterPubKey = createMasterPubKeyFromBytes();
return HDKeyDerivation.deriveChildKey(masterPubKey, ChildNumber.ZERO);
}
public Address getNotificationAddress() {
return ScriptType.P2PKH.getAddress(getNotificationKey());
}
public ECKey getKey(int index) {
DeterministicKey masterPubKey = createMasterPubKeyFromBytes();
return HDKeyDerivation.deriveChildKey(masterPubKey, new ChildNumber(index));
}
public byte[] getPayload() {
byte[] pcBytes = Base58.decodeChecked(strPaymentCode);
byte[] payload = new byte[PAYLOAD_LEN];
System.arraycopy(pcBytes, 1, payload, 0, payload.length);
return payload;
}
public int getType() throws InvalidPaymentCodeException {
byte[] payload = getPayload();
ByteBuffer bb = ByteBuffer.wrap(payload);
return bb.get();
}
public boolean isSegwitEnabled() {
return isBitSet(getPayload()[SAMOURAI_FEATURE_BYTE], SAMOURAI_SEGWIT_BIT);
}
public String toString() {
return strPaymentCode;
}
public static PaymentCode getPaymentCode(Transaction transaction, Keystore keystore) throws InvalidPaymentCodeException {
try {
TransactionInput txInput = getDesignatedInput(transaction);
ECKey pubKey = getDesignatedPubKey(txInput);
List<ChildNumber> derivation = keystore.getKeyDerivation().getDerivation();
ChildNumber derivationStart = derivation.isEmpty() ? ChildNumber.ZERO_HARDENED : derivation.get(derivation.size() - 1);
ECKey notificationPrivKey = keystore.getBip47ExtendedPrivateKey().getKey(List.of(derivationStart, new ChildNumber(0)));
SecretPoint secretPoint = new SecretPoint(notificationPrivKey.getPrivKeyBytes(), pubKey.getPubKey());
byte[] blindingMask = getMask(secretPoint.ECDHSecretAsBytes(), txInput.getOutpoint().bitcoinSerialize());
byte[] blindedPaymentCode = getOpReturnData(transaction);
return new PaymentCode(PaymentCode.blind(blindedPaymentCode, blindingMask));
} catch(Exception e) {
throw new InvalidPaymentCodeException("Could not determine payment code from transaction", e);
}
}
public static TransactionInput getDesignatedInput(Transaction transaction) {
for(TransactionInput txInput : transaction.getInputs()) {
if(getDesignatedPubKey(txInput) != null) {
return txInput;
}
}
throw new IllegalArgumentException("Cannot find designated input in notification transaction");
}
private static ECKey getDesignatedPubKey(TransactionInput txInput) {
for(ScriptChunk scriptChunk : txInput.getScriptSig().getChunks()) {
if(scriptChunk.isPubKey()) {
return scriptChunk.getPubKey();
}
}
for(ScriptChunk scriptChunk : txInput.getWitness().asScriptChunks()) {
if(scriptChunk.isPubKey()) {
return scriptChunk.getPubKey();
}
}
return null;
}
public static byte[] getOpReturnData(Transaction transaction) {
for(TransactionOutput txOutput : transaction.getOutputs()) {
List<ScriptChunk> scriptChunks = getOpReturnChunks(txOutput);
if(scriptChunks == null) {
continue;
}
return scriptChunks.get(1).getData();
}
throw new IllegalArgumentException("Cannot find OP_RETURN output in notification transaction");
}
private static List<ScriptChunk> getOpReturnChunks(TransactionOutput txOutput) {
List<ScriptChunk> scriptChunks = txOutput.getScript().getChunks();
if(scriptChunks.size() != 2) {
return null;
}
if(scriptChunks.get(0).getOpcode() != ScriptOpCodes.OP_RETURN) {
return null;
}
if(scriptChunks.get(1).getData() != null && scriptChunks.get(1).getData().length != 80) {
return null;
}
byte[] data = scriptChunks.get(1).getData();
if(data[0] != 0x01 || (data[2] != 0x02 && data[2] != 0x03)) {
return null;
}
return scriptChunks;
}
public static byte[] getMask(byte[] sPoint, byte[] oPoint) {
Mac sha512_HMAC;
byte[] mac_data = null;
try {
sha512_HMAC = Mac.getInstance("HmacSHA512");
SecretKeySpec secretkey = new SecretKeySpec(oPoint, "HmacSHA512");
sha512_HMAC.init(secretkey);
mac_data = sha512_HMAC.doFinal(sPoint);
} catch(InvalidKeyException | NoSuchAlgorithmException ignored) {
//ignore
}
return mac_data;
}
public static byte[] blind(byte[] payload, byte[] mask) throws InvalidPaymentCodeException {
byte[] ret = new byte[PAYLOAD_LEN];
byte[] pubkey = new byte[PUBLIC_KEY_X_LEN];
byte[] chain = new byte[CHAIN_LEN];
byte[] buf0 = new byte[PUBLIC_KEY_X_LEN];
byte[] buf1 = new byte[CHAIN_LEN];
System.arraycopy(payload, 0, ret, 0, PAYLOAD_LEN);
System.arraycopy(payload, PUBLIC_KEY_X_OFFSET, pubkey, 0, PUBLIC_KEY_X_LEN);
System.arraycopy(payload, CHAIN_OFFSET, chain, 0, CHAIN_LEN);
System.arraycopy(mask, 0, buf0, 0, PUBLIC_KEY_X_LEN);
System.arraycopy(mask, PUBLIC_KEY_X_LEN, buf1, 0, CHAIN_LEN);
System.arraycopy(xor(pubkey, buf0), 0, ret, PUBLIC_KEY_X_OFFSET, PUBLIC_KEY_X_LEN);
System.arraycopy(xor(chain, buf1), 0, ret, CHAIN_OFFSET, CHAIN_LEN);
return ret;
}
private Map<byte[], byte[]> parse() throws InvalidPaymentCodeException {
byte[] pcBytes = Base58.decodeChecked(strPaymentCode);
ByteBuffer bb = ByteBuffer.wrap(pcBytes);
if(bb.get() != 0x47) {
throw new InvalidPaymentCodeException("Invalid payment code version");
}
byte[] chain = new byte[CHAIN_LEN];
byte[] pub = new byte[PUBLIC_KEY_X_LEN + PUBLIC_KEY_Y_LEN];
// type:
bb.get();
// features:
bb.get();
bb.get(pub);
if(pub[0] != 0x02 && pub[0] != 0x03) {
throw new InvalidPaymentCodeException("Invalid public key");
}
bb.get(chain);
return Map.of(pub, chain);
}
private String makeV1() {
return make(0x01);
}
private String make(int type) {
byte[] payload = new byte[PAYLOAD_LEN];
byte[] payment_code = new byte[PAYLOAD_LEN + 1];
for(int i = 0; i < payload.length; i++) {
payload[i] = (byte) 0x00;
}
// byte 0: type.
payload[0] = (byte) type;
// byte 1: features bit field. All bits must be zero except where specified elsewhere in this specification
// bit 0: Bitmessage notification
// bits 1-7: reserved
payload[1] = (byte) 0x00;
// replace sign & x code (33 bytes)
System.arraycopy(pubkey, 0, payload, PUBLIC_KEY_Y_OFFSET, pubkey.length);
// replace chain code (32 bytes)
System.arraycopy(chain, 0, payload, CHAIN_OFFSET, chain.length);
// add version byte
payment_code[0] = (byte) 0x47;
System.arraycopy(payload, 0, payment_code, 1, payload.length);
// append checksum
return base58EncodeChecked(payment_code);
}
public String makeSamouraiPaymentCode() throws InvalidPaymentCodeException {
byte[] payload = getPayload();
// set bit0 = 1 in 'Samourai byte' for segwit. Can send/receive P2PKH, P2SH-P2WPKH, P2WPKH (bech32)
payload[SAMOURAI_FEATURE_BYTE] = setBit(payload[SAMOURAI_FEATURE_BYTE], SAMOURAI_SEGWIT_BIT);
byte[] payment_code = new byte[PAYLOAD_LEN + 1];
// add version byte
payment_code[0] = (byte) 0x47;
System.arraycopy(payload, 0, payment_code, 1, payload.length);
// append checksum
return base58EncodeChecked(payment_code);
}
private String base58EncodeChecked(byte[] buf) {
byte[] checksum = Arrays.copyOfRange(Sha256Hash.hashTwice(buf), 0, 4);
byte[] bufChecked = new byte[buf.length + checksum.length];
System.arraycopy(buf, 0, bufChecked, 0, buf.length);
System.arraycopy(checksum, 0, bufChecked, bufChecked.length - 4, checksum.length);
return Base58.encode(bufChecked);
}
private boolean isBitSet(byte b, int pos) {
byte test = 0;
return (setBit(test, pos) & b) > 0;
}
private byte setBit(byte b, int pos) {
return (byte) (b | (1 << pos));
}
private DeterministicKey createMasterPubKeyFromBytes() {
return HDKeyDerivation.createMasterPubKeyFromBytes(pubkey, chain);
}
private static byte[] xor(byte[] a, byte[] b) {
if(a.length != b.length) {
log.error("Invalid length for xor: " + a.length + " vs " + b.length);
return null;
}
byte[] ret = new byte[a.length];
for(int i = 0; i < a.length; i++) {
ret[i] = (byte) ((int) b[i] ^ (int) a[i]);
}
return ret;
}
public boolean isValid() {
try {
byte[] pcodeBytes = Base58.decodeChecked(strPaymentCode);
ByteBuffer byteBuffer = ByteBuffer.wrap(pcodeBytes);
if(byteBuffer.get() != 0x47) {
throw new InvalidPaymentCodeException("Invalid version: " + strPaymentCode);
} else {
byte[] chain = new byte[32];
byte[] pub = new byte[33];
// type:
byteBuffer.get();
// feature:
byteBuffer.get();
byteBuffer.get(pub);
byteBuffer.get(chain);
ByteBuffer pubBytes = ByteBuffer.wrap(pub);
int firstByte = pubBytes.get();
return firstByte == 0x02 || firstByte == 0x03;
}
} catch(BufferUnderflowException | InvalidPaymentCodeException bue) {
return false;
}
}
public static PaymentCode fromString(String strPaymentCode) {
try {
return new PaymentCode(strPaymentCode);
} catch(InvalidPaymentCodeException e) {
log.error("Invalid payment code", e);
}
return null;
}
public PaymentCode copy() {
return new PaymentCode(strPaymentCode, pubkey, chain);
}
public String toAbbreviatedString() {
return strPaymentCode.substring(0, 8) + "..." + strPaymentCode.substring(strPaymentCode.length() - 3);
}
@Override
public boolean equals(Object o) {
if(this == o) {
return true;
}
if(o == null || getClass() != o.getClass()) {
return false;
}
PaymentCode that = (PaymentCode) o;
return strPaymentCode.equals(that.strPaymentCode);
}
@Override
public int hashCode() {
return strPaymentCode.hashCode();
}
}

View file

@ -0,0 +1,53 @@
package com.sparrowwallet.drongo.bip47;
import org.bouncycastle.jce.ECNamedCurveTable;
import org.bouncycastle.jce.spec.ECParameterSpec;
import org.bouncycastle.jce.spec.ECPrivateKeySpec;
import org.bouncycastle.jce.spec.ECPublicKeySpec;
import javax.crypto.KeyAgreement;
import javax.crypto.SecretKey;
import java.math.BigInteger;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
public class SecretPoint {
private static final ECParameterSpec params = ECNamedCurveTable.getParameterSpec("secp256k1");
private static final String KEY_PROVIDER = "BC";
private final PrivateKey privKey;
private final PublicKey pubKey;
private final KeyFactory kf;
static {
Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
}
public SecretPoint(byte[] dataPrv, byte[] dataPub) throws InvalidKeySpecException, InvalidKeyException, IllegalStateException, NoSuchAlgorithmException, NoSuchProviderException {
kf = KeyFactory.getInstance("ECDH", KEY_PROVIDER);
privKey = loadPrivateKey(dataPrv);
pubKey = loadPublicKey(dataPub);
}
public byte[] ECDHSecretAsBytes() throws InvalidKeyException, IllegalStateException, NoSuchAlgorithmException, NoSuchProviderException {
return ECDHSecret().getEncoded();
}
private SecretKey ECDHSecret() throws InvalidKeyException, IllegalStateException, NoSuchAlgorithmException, NoSuchProviderException {
KeyAgreement ka = KeyAgreement.getInstance("ECDH", KEY_PROVIDER);
ka.init(privKey);
ka.doPhase(pubKey, true);
return ka.generateSecret("AES");
}
private PublicKey loadPublicKey(byte[] data) throws InvalidKeySpecException {
ECPublicKeySpec pubKey = new ECPublicKeySpec(params.getCurve().decodePoint(data), params);
return kf.generatePublic(pubKey);
}
private PrivateKey loadPrivateKey(byte[] data) throws InvalidKeySpecException {
ECPrivateKeySpec prvkey = new ECPrivateKeySpec(new BigInteger(1, data), params);
return kf.generatePrivate(prvkey);
}
}

View file

@ -0,0 +1,165 @@
/**
* Implementation of BIP38 encryption / decryption / key-address generation
* Based on https://github.com/bitcoin/bips/blob/master/bip-0038.mediawiki
*
* Tips much appreciated: 1EmwBbfgH7BPMoCpcFzyzgAN9Ya7jm8L1Z :)
*
* Copyright 2014 Diego Basch
*
* 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.sparrowwallet.drongo.crypto;
import com.sparrowwallet.drongo.Drongo;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.protocol.Base58;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import org.bouncycastle.crypto.generators.SCrypt;
import org.bouncycastle.math.ec.ECPoint;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.util.Arrays;
import static com.sparrowwallet.drongo.crypto.ECKey.CURVE;
public class BIP38 {
/**
* Decrypts an encrypted key.
* @param passphrase
* @param encryptedKey
* @throws UnsupportedEncodingException
* @throws GeneralSecurityException
*/
public static DumpedPrivateKey decrypt(String passphrase, String encryptedKey) throws UnsupportedEncodingException, GeneralSecurityException {
byte[] encryptedKeyBytes = Base58.decodeChecked(encryptedKey);
DumpedPrivateKey result;
byte ec = encryptedKeyBytes[1];
switch (ec) {
case 0x43: result = decryptEC(passphrase, encryptedKeyBytes);
break;
case 0x42: result = decryptNoEC(passphrase, encryptedKeyBytes);
break;
default: throw new RuntimeException("Invalid key - second byte is: " + ec);
}
return result;
}
/**
* Decrypts a key encrypted with EC multiplication
* @param passphrase
* @param encryptedKey
* @throws UnsupportedEncodingException
* @throws GeneralSecurityException
*/
public static DumpedPrivateKey decryptEC(String passphrase, byte[] encryptedKey) throws UnsupportedEncodingException, GeneralSecurityException {
byte flagByte = encryptedKey[2];
byte[] passFactor;
boolean hasLot = (flagByte & 4) == 4;
byte[] ownerSalt = Arrays.copyOfRange(encryptedKey, 7, 15 - (flagByte & 4));
if (!hasLot) {
passFactor = SCrypt.generate(passphrase.getBytes("UTF8"), ownerSalt, 16384, 8, 8, 32);
}
else {
byte[] preFactor = SCrypt.generate(passphrase.getBytes("UTF8"), ownerSalt, 16384, 8, 8, 32);
byte[] ownerEntropy = Arrays.copyOfRange(encryptedKey, 7, 15);
byte[] tmp = Utils.concat(preFactor, ownerEntropy);
passFactor = Sha256Hash.hashTwice(tmp, 0, 40);
}
byte[] addressHash = Arrays.copyOfRange(encryptedKey, 3, 7);
ECPoint g = CURVE.getG();
ECPoint p = g.multiply(new BigInteger(1, passFactor));
byte[] passPoint = p.getEncoded(true);
byte[] salt = new byte[12];
byte[] encryptedPart2 = Arrays.copyOfRange(encryptedKey, 23, 39);
System.arraycopy(addressHash, 0, salt, 0, 4);
System.arraycopy(encryptedKey, 7, salt, 4, 8);
byte[] secondKey = SCrypt.generate(passPoint, salt, 1024, 1, 1, 64);
byte[] derivedHalf1 = Arrays.copyOfRange(secondKey, 0, 32);
byte[] derivedHalf2 = Arrays.copyOfRange(secondKey, 32, 64);
byte[] m2 = decryptAES(encryptedPart2, derivedHalf2);
byte[] encryptedPart1 = new byte[16];
System.arraycopy(encryptedKey, 15, encryptedPart1, 0, 8);
byte[] seedB = new byte[24];
for (int i = 0; i < 16; i++) {
m2[i] = (byte) (m2[i] ^ derivedHalf1[16 + i]);
}
System.arraycopy(m2, 0, encryptedPart1, 8, 8);
byte[] m1 = decryptAES(encryptedPart1, derivedHalf2);
for (int i = 0; i < 16; i++) {
seedB[i] = (byte) (m1[i] ^ derivedHalf1[i]);
}
System.arraycopy(m2, 8, seedB, 16, 8);
byte[] factorB = Sha256Hash.hashTwice(seedB, 0, 24);
BigInteger n = CURVE.getN();
BigInteger pk = new BigInteger(1, passFactor).multiply(new BigInteger(1, factorB)).remainder(n);
ECKey privKey = ECKey.fromPrivate(pk, false);
return privKey.getPrivateKeyEncoded();
}
/**
* Decrypts a key that was encrypted without EC multiplication.
* @param passphrase
* @param encryptedKey
* @throws UnsupportedEncodingException
* @throws GeneralSecurityException
*/
public static DumpedPrivateKey decryptNoEC(String passphrase, byte[] encryptedKey) throws UnsupportedEncodingException, GeneralSecurityException {
byte[] addressHash = Arrays.copyOfRange(encryptedKey, 3, 7);
byte[] scryptKey = SCrypt.generate(passphrase.getBytes("UTF8"), addressHash, 16384, 8, 8, 64);
byte[] derivedHalf1 = Arrays.copyOfRange(scryptKey, 0, 32);
byte[] derivedHalf2 = Arrays.copyOfRange(scryptKey, 32, 64);
byte[] encryptedHalf1 = Arrays.copyOfRange(encryptedKey, 7, 23);
byte[] encryptedHalf2 = Arrays.copyOfRange(encryptedKey, 23, 39);
byte[] k1 = decryptAES(encryptedHalf1, derivedHalf2);
byte[] k2 = decryptAES(encryptedHalf2, derivedHalf2);
byte[] keyBytes = new byte[32];
for (int i = 0; i < 16; i++) {
keyBytes[i] = (byte) (k1[i] ^ derivedHalf1[i]);
keyBytes[i + 16] = (byte) (k2[i] ^ derivedHalf1[i + 16]);
}
boolean compressed = (encryptedKey[2] & (byte) 0x20) == 0x20;
ECKey k = new ECKey(new BigInteger(1, keyBytes), null, compressed);
return k.getPrivateKeyEncoded();
}
/**
* Decrypts ciphertext with AES
* @param ciphertext
* @param key
* @throws GeneralSecurityException
*/
public static byte[] decryptAES(byte[] ciphertext, byte[] key) throws GeneralSecurityException {
Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding", Drongo.getProvider());
SecretKeySpec aesKey = new SecretKeySpec(key, "AES");
cipher.init(Cipher.DECRYPT_MODE, aesKey);
return cipher.doFinal(ciphertext);
}
}

View file

@ -46,7 +46,11 @@ public class ChildNumber {
public int i() { return i; } public int i() { return i; }
public String toString() { public String toString() {
return String.format(Locale.US, "%d%s", num(), isHardened() ? "'" : ""); return toString(true);
}
public String toString(boolean useApostrophes) {
return num() + (isHardened() ? (useApostrophes ? "'" : "h") : "");
} }
public boolean equals(Object o) { public boolean equals(Object o) {

View file

@ -0,0 +1,88 @@
package com.sparrowwallet.drongo.crypto;
import com.sparrowwallet.drongo.Network;
import java.util.Arrays;
import java.util.Objects;
/**
* Parses and generates private keys in the form used by the Bitcoin "dumpprivkey" command. This is the private key
* bytes with a header byte and 4 checksum bytes at the end. If there are 33 private key bytes instead of 32, then
* the last byte is a discriminator value for the compressed pubkey.
*/
public class DumpedPrivateKey extends VersionedChecksummedBytes {
private final boolean compressed;
/**
* Construct a private key from its Base58 representation.
* @param base58
* The textual form of the private key.
*/
public static DumpedPrivateKey fromBase58(String base58) {
return new DumpedPrivateKey(base58);
}
// Used by ECKey.getPrivateKeyEncoded()
DumpedPrivateKey(byte[] keyBytes, boolean compressed) {
super(Network.get().getDumpedPrivateKeyHeader(), encode(keyBytes, compressed));
this.compressed = compressed;
}
private static byte[] encode(byte[] keyBytes, boolean compressed) {
if(keyBytes.length != 32) {
throw new IllegalArgumentException("Private keys must be 32 bytes");
}
if (!compressed) {
return keyBytes;
} else {
// Keys that have compressed public components have an extra 1 byte on the end in dumped form.
byte[] bytes = new byte[33];
System.arraycopy(keyBytes, 0, bytes, 0, 32);
bytes[32] = 1;
return bytes;
}
}
private DumpedPrivateKey(String encoded) {
super(encoded);
if(version != Network.get().getDumpedPrivateKeyHeader())
throw new IllegalArgumentException("Invalid version " + version + " for network " + Network.get());
if(bytes.length == 33 && bytes[32] == 1) {
compressed = true;
bytes = Arrays.copyOf(bytes, 32); // Chop off the additional marker byte.
} else if(bytes.length == 32) {
compressed = false;
} else {
throw new IllegalArgumentException("Wrong number of bytes for a private key, not 32 or 33");
}
}
/**
* Returns an ECKey created from this encoded private key.
*/
public ECKey getKey() {
return ECKey.fromPrivate(bytes, compressed);
}
@Override
public boolean equals(Object o) {
if(this == o) {
return true;
}
if(o == null || getClass() != o.getClass()) {
return false;
}
if(!super.equals(o)) {
return false;
}
DumpedPrivateKey that = (DumpedPrivateKey) o;
return compressed == that.compressed;
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), compressed);
}
}

View file

@ -0,0 +1,254 @@
package com.sparrowwallet.drongo.crypto;
import com.sparrowwallet.drongo.protocol.SigHash;
import com.sparrowwallet.drongo.protocol.SignatureDecodeException;
import com.sparrowwallet.drongo.protocol.TransactionSignature;
import com.sparrowwallet.drongo.protocol.VerificationException;
import org.bouncycastle.asn1.*;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.crypto.signers.ECDSASigner;
import org.bouncycastle.util.Properties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.util.Objects;
import static com.sparrowwallet.drongo.crypto.ECKey.CURVE;
/**
* Groups the two components that make up a signature, and provides a way to encode to DER form, which is
* how ECDSA signatures are represented when embedded in other data structures in the Bitcoin protocol. The raw
* components can be useful for doing further EC maths on them.
*/
public class ECDSASignature {
private static final Logger log = LoggerFactory.getLogger(ECDSASignature.class);
/**
* The two components of the signature.
*/
public final BigInteger r, s;
/**
* Constructs a signature with the given components. Does NOT automatically canonicalise the signature.
*/
public ECDSASignature(BigInteger r, BigInteger s) {
this.r = r;
this.s = s;
}
/**
* Returns true if the S component is "low", that means it is below {@link ECKey#HALF_CURVE_ORDER}. See <a
* href="https://github.com/bitcoin/bips/blob/master/bip-0062.mediawiki#Low_S_values_in_signatures">BIP62</a>.
*/
public boolean isCanonical() {
return s.compareTo(ECKey.HALF_CURVE_ORDER) <= 0;
}
/**
* Will automatically adjust the S component to be less than or equal to half the curve order, if necessary.
* This is required because for every signature (r,s) the signature (r, -s (mod N)) is a valid signature of
* the same message. However, we dislike the ability to modify the bits of a Bitcoin transaction after it's
* been signed, as that violates various assumed invariants. Thus in future only one of those forms will be
* considered legal and the other will be banned.
*/
public ECDSASignature toCanonicalised() {
if(!isCanonical()) {
// The order of the curve is the number of valid points that exist on that curve. If S is in the upper
// half of the number of valid points, then bring it back to the lower half. Otherwise, imagine that
// N = 10
// s = 8, so (-8 % 10 == 2) thus both (r, 8) and (r, 2) are valid solutions.
// 10 - 8 == 2, giving us always the latter solution, which is canonical.
return new ECDSASignature(r, CURVE.getN().subtract(s));
} else {
return this;
}
}
/**
* DER is an international standard for serializing data structures which is widely used in cryptography.
* It's somewhat like protocol buffers but less convenient. This method returns a standard DER encoding
* of the signature, as recognized by OpenSSL and other libraries.
*/
public byte[] encodeToDER() {
try {
return derByteStream().toByteArray();
} catch(IOException e) {
throw new RuntimeException(e); // Cannot happen.
}
}
/**
* @throws SignatureDecodeException if the signature is unparseable in some way.
*/
public static ECDSASignature decodeFromDER(byte[] bytes) throws SignatureDecodeException {
ASN1InputStream decoder = null;
try {
// BouncyCastle by default is strict about parsing ASN.1 integers. We relax this check, because some
// Bitcoin signatures would not parse.
Properties.setThreadOverride("org.bouncycastle.asn1.allow_unsafe_integer", true);
decoder = new ASN1InputStream(bytes);
final ASN1Primitive seqObj = decoder.readObject();
if(seqObj == null) {
throw new SignatureDecodeException("Reached past end of ASN.1 stream.");
}
if(!(seqObj instanceof DLSequence)) {
throw new SignatureDecodeException("Read unexpected class: " + seqObj.getClass().getName());
}
final DLSequence seq = (DLSequence) seqObj;
ASN1Integer r, s;
try {
r = (ASN1Integer) seq.getObjectAt(0);
s = (ASN1Integer) seq.getObjectAt(1);
} catch(ClassCastException e) {
throw new SignatureDecodeException(e);
}
// OpenSSL deviates from the DER spec by interpreting these values as unsigned, though they should not be
// Thus, we always use the positive versions. See: http://r6.ca/blog/20111119T211504Z.html
return new ECDSASignature(r.getPositiveValue(), s.getPositiveValue());
} catch(IOException e) {
throw new SignatureDecodeException(e);
} finally {
if(decoder != null) {
try {
decoder.close();
} catch(IOException x) {
}
}
Properties.removeThreadOverride("org.bouncycastle.asn1.allow_unsafe_integer");
}
}
/**
* <p>Verifies the given ECDSA signature against the message bytes using the public key bytes.</p>
*
* <p>When using native ECDSA verification, data must be 32 bytes, and no element may be
* larger than 520 bytes.</p>
*
* @param data Hash of the data to verify.
* @param pub The public key bytes to use.
*/
public boolean verify(byte[] data, byte[] pub) {
ECDSASigner signer = new ECDSASigner();
ECPublicKeyParameters params = new ECPublicKeyParameters(CURVE.getCurve().decodePoint(pub), CURVE);
signer.init(false, params);
try {
return signer.verifySignature(data, r, s);
} catch (NullPointerException e) {
// Bouncy Castle contains a bug that can cause NPEs given specially crafted signatures. Those signatures
// are inherently invalid/attack sigs so we just fail them here rather than crash the thread.
log.error("Caught NPE inside bouncy castle", e);
return false;
}
}
public ByteArrayOutputStream derByteStream() throws IOException {
// Usually 70-72 bytes.
ByteArrayOutputStream bos = new ByteArrayOutputStream(72);
DERSequenceGenerator seq = new DERSequenceGenerator(bos);
seq.addObject(new ASN1Integer(r));
seq.addObject(new ASN1Integer(s));
seq.close();
return bos;
}
protected boolean hasLowR() {
//A low R signature will have less than 71 bytes when encoded to DER
return toCanonicalised().encodeToDER().length < 71;
}
@Override
public boolean equals(Object o) {
if(this == o) {
return true;
}
if(o == null || getClass() != o.getClass()) {
return false;
}
ECDSASignature other = (ECDSASignature) o;
return r.equals(other.r) && s.equals(other.s);
}
@Override
public int hashCode() {
return Objects.hash(r, s);
}
/**
* Returns a decoded signature.
*
* @param requireCanonicalEncoding if the encoding of the signature must
* be canonical.
* @param requireCanonicalSValue if the S-value must be canonical (below half
* the order of the curve).
* @throws SignatureDecodeException if the signature is unparseable in some way.
* @throws VerificationException if the signature is invalid.
*/
public static TransactionSignature decodeFromBitcoin(byte[] bytes, boolean requireCanonicalEncoding,
boolean requireCanonicalSValue) throws SignatureDecodeException, VerificationException {
// Bitcoin encoding is DER signature + sighash byte.
if (requireCanonicalEncoding && !isEncodingCanonical(bytes))
throw new VerificationException.NoncanonicalSignature();
ECDSASignature sig = ECDSASignature.decodeFromDER(bytes);
if (requireCanonicalSValue && !sig.isCanonical())
throw new VerificationException("S-value is not canonical.");
// In Bitcoin, any value of the final byte is valid, but not necessarily canonical. See javadocs for
// isEncodingCanonical to learn more about this. So we must store the exact byte found.
return new TransactionSignature(sig.r, sig.s, TransactionSignature.Type.ECDSA, bytes[bytes.length - 1]);
}
/**
* Returns true if the given signature is has canonical encoding, and will thus be accepted as standard by
* Bitcoin Core. DER and the SIGHASH encoding allow for quite some flexibility in how the same structures
* are encoded, and this can open up novel attacks in which a man in the middle takes a transaction and then
* changes its signature such that the transaction hash is different but it's still valid. This can confuse wallets
* and generally violates people's mental model of how Bitcoin should work, thus, non-canonical signatures are now
* not relayed by default.
*/
public static boolean isEncodingCanonical(byte[] signature) {
// See Bitcoin Core's IsCanonicalSignature, https://bitcointalk.org/index.php?topic=8392.msg127623#msg127623
// A canonical signature exists of: <30> <total len> <02> <len R> <R> <02> <len S> <S> <hashtype>
// Where R and S are not negative (their first byte has its highest bit not set), and not
// excessively padded (do not start with a 0 byte, unless an otherwise negative number follows,
// in which case a single 0 byte is necessary and even required).
// Empty signatures, while not strictly DER encoded, are allowed.
if (signature.length == 0)
return true;
if (signature.length < 9 || signature.length > 73)
return false;
int hashType = (signature[signature.length-1] & 0xff) & ~SigHash.ANYONECANPAY.value; // mask the byte to prevent sign-extension hurting us
if (hashType < SigHash.ALL.value || hashType > SigHash.SINGLE.value)
return false;
// "wrong type" "wrong length marker"
if ((signature[0] & 0xff) != 0x30 || (signature[1] & 0xff) != signature.length-3)
return false;
int lenR = signature[3] & 0xff;
if (5 + lenR >= signature.length || lenR == 0)
return false;
int lenS = signature[5+lenR] & 0xff;
if (lenR + lenS + 7 != signature.length || lenS == 0)
return false;
// R value type mismatch R value negative
if (signature[4-2] != 0x02 || (signature[4] & 0x80) == 0x80)
return false;
if (lenR > 1 && signature[4] == 0x00 && (signature[4+1] & 0x80) != 0x80)
return false; // R value excessively padded
// S value type mismatch S value negative
if (signature[6 + lenR - 2] != 0x02 || (signature[6 + lenR] & 0x80) == 0x80)
return false;
if (lenS > 1 && signature[6 + lenR] == 0x00 && (signature[6 + lenR + 1] & 0x80) != 0x80)
return false; // S value excessively padded
return true;
}
}

View file

@ -1,10 +1,10 @@
package com.sparrowwallet.drongo.crypto; package com.sparrowwallet.drongo.crypto;
import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.protocol.ScriptType; import com.sparrowwallet.drongo.protocol.*;
import com.sparrowwallet.drongo.protocol.Sha256Hash; import org.bitcoin.NativeSecp256k1;
import com.sparrowwallet.drongo.protocol.SignatureDecodeException; import org.bitcoin.NativeSecp256k1Util;
import com.sparrowwallet.drongo.protocol.VarInt; import org.bitcoin.Secp256k1Context;
import org.bouncycastle.asn1.*; import org.bouncycastle.asn1.*;
import org.bouncycastle.asn1.x9.X9ECParameters; import org.bouncycastle.asn1.x9.X9ECParameters;
import org.bouncycastle.asn1.x9.X9IntegerConverter; import org.bouncycastle.asn1.x9.X9IntegerConverter;
@ -17,12 +17,8 @@ import org.bouncycastle.crypto.params.ECKeyGenerationParameters;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters; import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters; import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.crypto.signers.ECDSASigner; import org.bouncycastle.crypto.signers.ECDSASigner;
import org.bouncycastle.math.ec.ECAlgorithms; import org.bouncycastle.math.ec.*;
import org.bouncycastle.math.ec.ECPoint;
import org.bouncycastle.math.ec.FixedPointCombMultiplier;
import org.bouncycastle.math.ec.FixedPointUtil;
import org.bouncycastle.math.ec.custom.sec.SecP256K1Curve; import org.bouncycastle.math.ec.custom.sec.SecP256K1Curve;
import org.bouncycastle.util.Properties;
import org.bouncycastle.util.encoders.Hex; import org.bouncycastle.util.encoders.Hex;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -66,21 +62,9 @@ import java.util.Objects;
* this class so round-tripping preserves state. Unless you're working with old software or doing unusual things, you * this class so round-tripping preserves state. Unless you're working with old software or doing unusual things, you
* can usually ignore the compressed/uncompressed distinction.</p> * can usually ignore the compressed/uncompressed distinction.</p>
*/ */
public class ECKey implements EncryptableItem { public class ECKey {
private static final Logger log = LoggerFactory.getLogger(ECKey.class); private static final Logger log = LoggerFactory.getLogger(ECKey.class);
/** Sorts oldest keys first, newest last. */
public static final Comparator<ECKey> AGE_COMPARATOR = new Comparator<ECKey>() {
@Override
public int compare(ECKey k1, ECKey k2) {
if (k1.creationTimeSeconds == k2.creationTimeSeconds)
return 0;
else
return k1.creationTimeSeconds > k2.creationTimeSeconds ? 1 : -1;
}
};
// The parameters of the secp256k1 curve that Bitcoin uses. // The parameters of the secp256k1 curve that Bitcoin uses.
private static final X9ECParameters CURVE_PARAMS = CustomNamedCurves.getByName("secp256k1"); private static final X9ECParameters CURVE_PARAMS = CustomNamedCurves.getByName("secp256k1");
@ -108,13 +92,6 @@ public class ECKey implements EncryptableItem {
protected final BigInteger priv; protected final BigInteger priv;
protected final LazyECPoint pub; protected final LazyECPoint pub;
// Creation time of the key in seconds since the epoch, or zero if the key was deserialized from a version that did
// not have this field.
protected long creationTimeSeconds;
protected KeyCrypter keyCrypter;
protected EncryptedData encryptedPrivateKey;
private byte[] pubKeyHash; private byte[] pubKeyHash;
/** /**
@ -159,18 +136,6 @@ public class ECKey implements EncryptableItem {
this.pub = pub; this.pub = pub;
} }
/**
* Constructs a key that has an encrypted private component. The given object wraps encrypted bytes and an
* initialization vector. Note that the key will not be decrypted during this call: the returned ECKey is
* unusable for signing unless a decryption key is supplied.
*/
public static ECKey fromEncrypted(EncryptedData encryptedPrivateKey, KeyCrypter crypter, byte[] pubKey) {
ECKey key = fromPublicOnly(pubKey);
key.encryptedPrivateKey = encryptedPrivateKey;
key.keyCrypter = crypter;
return key;
}
/** /**
* Utility for compressing an elliptic curve point. Returns the same point if it's already compressed. * 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. * See the ECKey class docs for a discussion of point compression.
@ -268,11 +233,6 @@ public class ECKey implements EncryptableItem {
return priv != null; return priv != null;
} }
/** Returns true if this key is watch only, meaning it has a public key but no private key. */
public boolean isWatching() {
return isPubKeyOnly() && !isEncrypted();
}
/** /**
* Output this ECKey as an ASN.1 encoded private key, as understood by OpenSSL or used by Bitcoin Core * Output this ECKey as an ASN.1 encoded private key, as understood by OpenSSL or used by Bitcoin Core
* in its wallet storage format. * in its wallet storage format.
@ -315,13 +275,20 @@ public class ECKey implements EncryptableItem {
* use {@code new BigInteger(1, bytes);} * use {@code new BigInteger(1, bytes);}
*/ */
public static ECPoint publicPointFromPrivate(BigInteger privKey) { 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()) { if (privKey.bitLength() > CURVE.getN().bitLength()) {
privKey = privKey.mod(CURVE.getN()); privKey = privKey.mod(CURVE.getN());
} }
if(Secp256k1Context.isEnabled()) {
try {
byte[] pubKeyBytes = NativeSecp256k1.computePubkey(Utils.bigIntegerToBytes(privKey, 32), false);
LazyECPoint lazyECPoint = new LazyECPoint(CURVE.getCurve(), pubKeyBytes);
return lazyECPoint.get();
} catch(NativeSecp256k1Util.AssertFailException e) {
log.error("Error computing public key from private", e);
}
}
return new FixedPointCombMultiplier().multiply(CURVE.getG(), privKey); return new FixedPointCombMultiplier().multiply(CURVE.getG(), privKey);
} }
@ -340,6 +307,13 @@ public class ECKey implements EncryptableItem {
return pub.getEncoded(); return pub.getEncoded();
} }
/**
* Gets the x coordinate of the raw public key value. This appears in transaction scriptPubKeys for Taproot outputs.
*/
public byte[] getPubKeyXCoord() {
return pub.getEncodedXCoord();
}
/** Gets the public key in the form of an elliptic curve point object from Bouncy Castle. */ /** Gets the public key in the form of an elliptic curve point object from Bouncy Castle. */
public ECPoint getPubKeyPoint() { public ECPoint getPubKeyPoint() {
return pub.get(); return pub.get();
@ -359,6 +333,17 @@ public class ECKey implements EncryptableItem {
return priv; return priv;
} }
/**
* Exports the private key in the form used by Bitcoin Core's "dumpprivkey" and "importprivkey" commands. Use
* the {@link DumpedPrivateKey#toString()} method to get the string.
*
* @return Private key bytes as a {@link DumpedPrivateKey}.
* @throws IllegalStateException if the private key is not available.
*/
public DumpedPrivateKey getPrivateKeyEncoded() {
return new DumpedPrivateKey(getPrivKeyBytes(), isCompressed());
}
/** /**
* Returns whether this key is using the compressed form or not. Compressed pubkeys are only 33 bytes, not 64. * Returns whether this key is using the compressed form or not. Compressed pubkeys are only 33 bytes, not 64.
*/ */
@ -366,257 +351,129 @@ public class ECKey implements EncryptableItem {
return pub.isCompressed(); return pub.isCompressed();
} }
/** public TransactionSignature sign(Sha256Hash input, SigHash sigHash, TransactionSignature.Type type) {
* Groups the two components that make up a signature, and provides a way to encode to DER form, which is TransactionSignature transactionSignature;
* how ECDSA signatures are represented when embedded in other data structures in the Bitcoin protocol. The raw
* components can be useful for doing further EC maths on them.
*/
public static class ECDSASignature {
/** The two components of the signature. */
public final BigInteger r, s;
/** if(type == TransactionSignature.Type.SCHNORR) {
* Constructs a signature with the given components. Does NOT automatically canonicalise the signature. SchnorrSignature schnorrSignature = signSchnorr(input);
*/ transactionSignature = new TransactionSignature(schnorrSignature, sigHash);
public ECDSASignature(BigInteger r, BigInteger s) {
this.r = r;
this.s = s;
}
/**
* Returns true if the S component is "low", that means it is below {@link ECKey#HALF_CURVE_ORDER}. See <a
* href="https://github.com/bitcoin/bips/blob/master/bip-0062.mediawiki#Low_S_values_in_signatures">BIP62</a>.
*/
public boolean isCanonical() {
return s.compareTo(HALF_CURVE_ORDER) <= 0;
}
/**
* Will automatically adjust the S component to be less than or equal to half the curve order, if necessary.
* This is required because for every signature (r,s) the signature (r, -s (mod N)) is a valid signature of
* the same message. However, we dislike the ability to modify the bits of a Bitcoin transaction after it's
* been signed, as that violates various assumed invariants. Thus in future only one of those forms will be
* considered legal and the other will be banned.
*/
public ECDSASignature toCanonicalised() {
if (!isCanonical()) {
// The order of the curve is the number of valid points that exist on that curve. If S is in the upper
// half of the number of valid points, then bring it back to the lower half. Otherwise, imagine that
// N = 10
// s = 8, so (-8 % 10 == 2) thus both (r, 8) and (r, 2) are valid solutions.
// 10 - 8 == 2, giving us always the latter solution, which is canonical.
return new ECDSASignature(r, CURVE.getN().subtract(s));
} else {
return this;
}
}
/**
* DER is an international standard for serializing data structures which is widely used in cryptography.
* It's somewhat like protocol buffers but less convenient. This method returns a standard DER encoding
* of the signature, as recognized by OpenSSL and other libraries.
*/
public byte[] encodeToDER() {
try {
return derByteStream().toByteArray();
} catch (IOException e) {
throw new RuntimeException(e); // Cannot happen.
}
}
/**
* @throws SignatureDecodeException if the signature is unparseable in some way.
*/
public static ECDSASignature decodeFromDER(byte[] bytes) throws SignatureDecodeException {
ASN1InputStream decoder = null;
try {
// BouncyCastle by default is strict about parsing ASN.1 integers. We relax this check, because some
// Bitcoin signatures would not parse.
Properties.setThreadOverride("org.bouncycastle.asn1.allow_unsafe_integer", true);
decoder = new ASN1InputStream(bytes);
final ASN1Primitive seqObj = decoder.readObject();
if (seqObj == null)
throw new SignatureDecodeException("Reached past end of ASN.1 stream.");
if (!(seqObj instanceof DLSequence))
throw new SignatureDecodeException("Read unexpected class: " + seqObj.getClass().getName());
final DLSequence seq = (DLSequence) seqObj;
ASN1Integer r, s;
try {
r = (ASN1Integer) seq.getObjectAt(0);
s = (ASN1Integer) seq.getObjectAt(1);
} catch (ClassCastException e) {
throw new SignatureDecodeException(e);
}
// OpenSSL deviates from the DER spec by interpreting these values as unsigned, though they should not be
// Thus, we always use the positive versions. See: http://r6.ca/blog/20111119T211504Z.html
return new ECDSASignature(r.getPositiveValue(), s.getPositiveValue());
} catch (IOException e) {
throw new SignatureDecodeException(e);
} finally {
if (decoder != null)
try { decoder.close(); } catch (IOException x) {}
Properties.removeThreadOverride("org.bouncycastle.asn1.allow_unsafe_integer");
}
}
protected ByteArrayOutputStream derByteStream() throws IOException {
// Usually 70-72 bytes.
ByteArrayOutputStream bos = new ByteArrayOutputStream(72);
DERSequenceGenerator seq = new DERSequenceGenerator(bos);
seq.addObject(new ASN1Integer(r));
seq.addObject(new ASN1Integer(s));
seq.close();
return bos;
}
protected boolean hasLowR() {
return r.toByteArray().length <= 32;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ECDSASignature other = (ECDSASignature) o;
return r.equals(other.r) && s.equals(other.s);
}
@Override
public int hashCode() {
return Objects.hash(r, s);
}
}
/**
* Signs the given hash and returns the R and S components as BigIntegers. In the Bitcoin protocol, they are
* usually encoded using ASN.1 format, so you want {@link ECKey.ECDSASignature#toASN1()}
* instead. However sometimes the independent components can be useful, for instance, if you're going to do
* further EC maths on them.
* @throws KeyCrypterException if this ECKey doesn't have a private part.
*/
public ECDSASignature sign(Sha256Hash input) throws KeyCrypterException {
return sign(input, null);
}
/**
* Signs the given hash and returns the R and S components as BigIntegers. In the Bitcoin protocol, they are
* usually encoded using DER format, so you want {@link ECKey.ECDSASignature#encodeToDER()}
* instead. However sometimes the independent components can be useful, for instance, if you're doing to do further
* EC maths on them.
*
* @param aesKey The AES key to use for decryption of the private key. If null then no decryption is required.
* @throws KeyCrypterException if there's something wrong with aesKey.
* @throws ECKey.MissingPrivateKeyException if this key cannot sign because it's pubkey only.
*/
public ECDSASignature sign(Sha256Hash input, Key aesKey) throws KeyCrypterException {
KeyCrypter crypter = getKeyCrypter();
if (crypter != null) {
if (aesKey == null) {
throw new KeyIsEncryptedException();
}
return decrypt(aesKey).sign(input);
} else { } else {
// No decryption of private key required. ECDSASignature ecdsaSignature = signEcdsa(input);
if (priv == null) { transactionSignature = new TransactionSignature(ecdsaSignature, sigHash);
throw new MissingPrivateKeyException();
}
} }
return doSign(input, priv);
//Verify transaction signature immediately after signing as recommended in BIP340
if(!transactionSignature.verify(input.getBytes(), this)) {
throw new IllegalStateException("Generated signature failed verification");
}
return transactionSignature;
} }
protected ECDSASignature doSign(Sha256Hash input, BigInteger privateKeyForSigning) { /**
if(privateKeyForSigning == null) { * Signs the given hash and returns the R and S components as an ECDSASignature.
*/
public ECDSASignature signEcdsa(Sha256Hash input) {
if(priv == null) {
throw new IllegalArgumentException("Private key cannot be null"); throw new IllegalArgumentException("Private key cannot be null");
} }
ECDSASignature signature; ECDSASignature signature;
int counter = 0; Integer counter = null;
do { do {
ECDSASigner signer = new ECDSASigner(new HMacDSANonceKCalculator(new SHA256Digest(), counter)); ECDSASigner signer = new ECDSASigner(new HMacDSANonceKCalculator(new SHA256Digest(), counter));
ECPrivateKeyParameters privKey = new ECPrivateKeyParameters(privateKeyForSigning, CURVE); ECPrivateKeyParameters privKey = new ECPrivateKeyParameters(priv, CURVE);
signer.init(true, privKey); signer.init(true, privKey);
BigInteger[] components = signer.generateSignature(input.getBytes()); BigInteger[] components = signer.generateSignature(input.getBytes());
signature = new ECDSASignature(components[0], components[1]).toCanonicalised(); signature = new ECDSASignature(components[0], components[1]).toCanonicalised();
counter++; counter = (counter == null ? 1 : counter+1);
} while(!signature.hasLowR()); } while(!signature.hasLowR());
return signature; return signature;
} }
/** /**
* <p>Verifies the given ECDSA signature against the message bytes using the public key bytes.</p> * Signs the given hash and returns the R and S components as a SchnorrSignature.
*
* <p>When using native ECDSA verification, data must be 32 bytes, and no element may be
* larger than 520 bytes.</p>
*
* @param data Hash of the data to verify.
* @param signature ASN.1 encoded signature.
* @param pub The public key bytes to use.
*/ */
public static boolean verify(byte[] data, ECDSASignature signature, byte[] pub) { public SchnorrSignature signSchnorr(Sha256Hash input) {
ECDSASigner signer = new ECDSASigner(); if(priv == null) {
ECPublicKeyParameters params = new ECPublicKeyParameters(CURVE.getCurve().decodePoint(pub), CURVE); throw new IllegalArgumentException("Private key cannot be null");
signer.init(false, params);
try {
return signer.verifySignature(data, signature.r, signature.s);
} catch (NullPointerException e) {
// Bouncy Castle contains a bug that can cause NPEs given specially crafted signatures. Those signatures
// are inherently invalid/attack sigs so we just fail them here rather than crash the thread.
log.error("Caught NPE inside bouncy castle", e);
return false;
} }
if(!Secp256k1Context.isEnabled()) {
throw new IllegalStateException("libsecp256k1 is not enabled");
}
try {
byte[] sigBytes = NativeSecp256k1.schnorrSign(input.getBytes(), Utils.bigIntegerToBytes(priv, 32), new byte[32]);
return SchnorrSignature.decode(sigBytes);
} catch(NativeSecp256k1Util.AssertFailException e) {
log.error("Error signing schnorr", e);
}
return null;
} }
/** /**
* Verifies the given ASN.1 encoded ECDSA signature against a hash using the public key. * Verifies the given TransactionSignature against the provided byte array using the public key.
*
* @param data Hash of the data to verify.
* @param signature ASN.1 encoded signature.
* @param pub The public key bytes to use.
* @throws SignatureDecodeException if the signature is unparseable in some way.
*/ */
public static boolean verify(byte[] data, byte[] signature, byte[] pub) throws SignatureDecodeException { public boolean verify(byte[] data, TransactionSignature signature) {
return verify(data, ECDSASignature.decodeFromDER(signature), pub); return signature.verify(data, this);
}
/**
* Verifies the given ASN.1 encoded ECDSA signature against a hash using the public key.
*
* @param hash Hash of the data to verify.
* @param signature ASN.1 encoded signature.
* @throws SignatureDecodeException if the signature is unparseable in some way.
*/
public boolean verify(byte[] hash, byte[] signature) throws SignatureDecodeException {
return ECKey.verify(hash, signature, getPubKey());
} }
/** /**
* Verifies the given R/S pair (signature) against a hash using the public key. * Verifies the given R/S pair (signature) against a hash using the public key.
*/ */
public boolean verify(Sha256Hash sigHash, ECDSASignature signature) { public boolean verify(Sha256Hash sigHash, TransactionSignature signature) {
return ECKey.verify(sigHash.getBytes(), signature, getPubKey()); return verify(sigHash.getBytes(), signature);
} }
/** public ECKey getTweakedOutputKey() {
* Verifies the given ASN.1 encoded ECDSA signature against a hash using the public key, and throws an exception TaprootPubKey taprootPubKey = liftX(getPubKeyXCoord());
* if the signature doesn't match ECPoint internalKey = taprootPubKey.ecPoint;
* @throws SignatureDecodeException if the signature is unparseable in some way. byte[] taggedHash = Utils.taggedHash("TapTweak", internalKey.getXCoord().getEncoded());
* @throws java.security.SignatureException if the signature does not match. ECKey tweakValue = ECKey.fromPrivate(taggedHash);
*/ ECPoint outputKey = internalKey.add(tweakValue.getPubKeyPoint());
public void verifyOrThrow(byte[] hash, byte[] signature) throws SignatureDecodeException, SignatureException {
if (!verify(hash, signature)) { if(hasPrivKey()) {
throw new SignatureException(); BigInteger taprootPriv = priv;
BigInteger tweakedPrivKey = taprootPriv.add(tweakValue.getPrivKey()).mod(CURVE_PARAMS.getCurve().getOrder());
//TODO: Improve on this hack. How do we know whether to negate the private key before tweaking it?
if(!ECKey.fromPrivate(tweakedPrivKey).getPubKeyPoint().equals(outputKey)) {
taprootPriv = CURVE_PARAMS.getCurve().getOrder().subtract(priv);
tweakedPrivKey = taprootPriv.add(tweakValue.getPrivKey()).mod(CURVE_PARAMS.getCurve().getOrder());
}
return new ECKey(tweakedPrivKey, outputKey, true);
} }
return ECKey.fromPublicOnly(outputKey, true);
} }
/** private static TaprootPubKey liftX(byte[] bytes) {
* Verifies the given R/S pair (signature) against a hash using the public key, and throws an exception SecP256K1Curve secP256K1Curve = (SecP256K1Curve)CURVE_PARAMS.getCurve();
* if the signature doesn't match BigInteger x = new BigInteger(1, bytes);
* @throws java.security.SignatureException if the signature does not match. BigInteger p = secP256K1Curve.getQ();
*/ if(x.compareTo(p) > -1) {
public void verifyOrThrow(Sha256Hash sigHash, ECDSASignature signature) throws SignatureException { throw new IllegalArgumentException("Provided bytes must be less than secp256k1 field size");
if (!ECKey.verify(sigHash.getBytes(), signature, getPubKey())) { }
throw new SignatureException();
BigInteger y_sq = x.modPow(BigInteger.valueOf(3), p).add(BigInteger.valueOf(7)).mod(p);
BigInteger y = y_sq.modPow(p.add(BigInteger.valueOf(1)).divide(BigInteger.valueOf(4)), p);
if(!y.modPow(BigInteger.valueOf(2), p).equals(y_sq)) {
throw new IllegalStateException("Calculated invalid y_sq when solving for y co-ordinate");
}
return y.and(BigInteger.ONE).equals(BigInteger.ZERO) ? new TaprootPubKey(secP256K1Curve.createPoint(x, y), false) : new TaprootPubKey(secP256K1Curve.createPoint(x, p.subtract(y)), true);
}
private static class TaprootPubKey {
public final ECPoint ecPoint;
public final boolean negated;
public TaprootPubKey(ECPoint ecPoint, boolean negated) {
this.ecPoint = ecPoint;
this.negated = negated;
} }
} }
@ -624,8 +481,10 @@ public class ECKey implements EncryptableItem {
* Returns true if the given pubkey is canonical, i.e. the correct length taking into account compression. * Returns true if the given pubkey is canonical, i.e. the correct length taking into account compression.
*/ */
public static boolean isPubKeyCanonical(byte[] pubkey) { public static boolean isPubKeyCanonical(byte[] pubkey) {
if (pubkey.length < 33) if (pubkey.length < 32)
return false; return false;
if (pubkey.length == 32)
return true;
if (pubkey[0] == 0x04) { if (pubkey[0] == 0x04) {
// Uncompressed pubkey // Uncompressed pubkey
if (pubkey.length != 65) if (pubkey.length != 65)
@ -643,7 +502,7 @@ public class ECKey implements EncryptableItem {
* Returns true if the given pubkey is in its compressed form. * Returns true if the given pubkey is in its compressed form.
*/ */
public static boolean isPubKeyCompressed(byte[] encoded) { public static boolean isPubKeyCompressed(byte[] encoded) {
if (encoded.length == 33 && (encoded[0] == 0x02 || encoded[0] == 0x03)) if (encoded.length == 32 || (encoded.length == 33 && (encoded[0] == 0x02 || encoded[0] == 0x03)))
return true; return true;
else if (encoded.length == 65 && encoded[0] == 0x04) else if (encoded.length == 65 && encoded[0] == 0x04)
return false; return false;
@ -715,12 +574,11 @@ public class ECKey implements EncryptableItem {
* encoded string. * encoded string.
* *
* @throws IllegalStateException if this ECKey does not have the private part. * @throws IllegalStateException if this ECKey does not have the private part.
* @throws KeyCrypterException if this ECKey is encrypted and no AESKey is provided or it does not decrypt the ECKey.
*/ */
public String signMessage(String message, ScriptType scriptType, Key aesKey) throws KeyCrypterException { public String signMessage(String message, ScriptType scriptType) {
byte[] data = formatMessageForSigning(message); byte[] data = formatMessageForSigning(message);
Sha256Hash hash = Sha256Hash.of(data); Sha256Hash hash = Sha256Hash.of(data);
ECDSASignature sig = sign(hash, aesKey); ECDSASignature sig = signEcdsa(hash);
byte recId = findRecoveryId(hash, sig); byte recId = findRecoveryId(hash, sig);
int headerByte = recId + getSigningTypeConstant(scriptType); int headerByte = recId + getSigningTypeConstant(scriptType);
byte[] sigData = new byte[65]; // 1 header + 32 bytes for R + 32 bytes for S byte[] sigData = new byte[65]; // 1 header + 32 bytes for R + 32 bytes for S
@ -939,181 +797,9 @@ public class ECKey implements EncryptableItem {
} }
} }
/**
* Returns the creation time of this key or zero if the key was deserialized from a version that did not store
* that data.
*/
@Override
public long getCreationTimeSeconds() {
return creationTimeSeconds;
}
/**
* Sets the creation time of this key. Zero is a convention to mean "unavailable". This method can be useful when
* you have a raw key you are importing from somewhere else.
*/
public void setCreationTimeSeconds(long newCreationTimeSeconds) {
if (newCreationTimeSeconds < 0) {
throw new IllegalArgumentException("Cannot set creation time to negative value: " + newCreationTimeSeconds);
}
creationTimeSeconds = newCreationTimeSeconds;
}
/**
* Create an encrypted private key with the keyCrypter and the AES key supplied.
* This method returns a new encrypted key and leaves the original unchanged.
*
* @param keyCrypter The keyCrypter that specifies exactly how the encrypted bytes are created.
* @param aesKey The Key with the AES encryption key (usually constructed with keyCrypter#deriveKey and cached as it is slow to create).
* @return encryptedKey
*/
public ECKey encrypt(KeyCrypter keyCrypter, Key aesKey) throws KeyCrypterException {
if(keyCrypter == null) {
throw new KeyCrypterException("Keycrypter cannot be null");
}
final byte[] privKeyBytes = getPrivKeyBytes();
EncryptedData encryptedPrivateKey = keyCrypter.encrypt(privKeyBytes, null, aesKey);
ECKey result = ECKey.fromEncrypted(encryptedPrivateKey, keyCrypter, getPubKey());
result.setCreationTimeSeconds(creationTimeSeconds);
return result;
}
/**
* Create a decrypted private key with the keyCrypter and AES key supplied. Note that if the aesKey is wrong, this
* has some chance of throwing KeyCrypterException due to the corrupted padding that will result, but it can also
* just yield a garbage key.
*
* @param keyCrypter The keyCrypter that specifies exactly how the decrypted bytes are created.
* @param aesKey The Key with the AES encryption key (usually constructed with keyCrypter#deriveKey and cached).
*/
public ECKey decrypt(KeyCrypter keyCrypter, Key aesKey) throws KeyCrypterException {
if(keyCrypter == null) {
throw new KeyCrypterException("Keycrypter cannot be null");
}
// Check that the keyCrypter matches the one used to encrypt the keys, if set.
if (this.keyCrypter != null && !this.keyCrypter.equals(keyCrypter)) {
throw new KeyCrypterException("The keyCrypter being used to decrypt the key is different to the one that was used to encrypt it");
}
if(encryptedPrivateKey == null) {
throw new IllegalArgumentException("This key is not encrypted");
}
byte[] unencryptedPrivateKey = keyCrypter.decrypt(encryptedPrivateKey, aesKey);
ECKey key = ECKey.fromPrivate(unencryptedPrivateKey);
if (!Arrays.equals(key.getPubKey(), getPubKey())) {
throw new KeyCrypterException("Provided AES key is wrong");
}
key.setCreationTimeSeconds(creationTimeSeconds);
return key;
}
/**
* Create a decrypted private key with AES key. Note that if the AES key is wrong, this
* has some chance of throwing KeyCrypterException due to the corrupted padding that will result, but it can also
* just yield a garbage key.
*
* @param aesKey The Key with the AES encryption key (usually constructed with keyCrypter#deriveKey and cached).
*/
public ECKey decrypt(Key aesKey) throws KeyCrypterException {
final KeyCrypter crypter = getKeyCrypter();
if (crypter == null) {
throw new KeyCrypterException("No key crypter available");
}
return decrypt(crypter, aesKey);
}
/**
* Creates decrypted private key if needed.
*/
public ECKey maybeDecrypt(Key aesKey) throws KeyCrypterException {
return isEncrypted() && aesKey != null ? decrypt(aesKey) : this;
}
/**
* <p>Check that it is possible to decrypt the key with the keyCrypter and that the original key is returned.</p>
*
* <p>Because it is a critical failure if the private keys cannot be decrypted successfully (resulting of loss of all
* bitcoins controlled by the private key) you can use this method to check when you *encrypt* a wallet that
* it can definitely be decrypted successfully.</p>
*
* @return true if the encrypted key can be decrypted back to the original key successfully.
*/
public static boolean encryptionIsReversible(ECKey originalKey, ECKey encryptedKey, KeyCrypter keyCrypter, Key aesKey) {
try {
ECKey rebornUnencryptedKey = encryptedKey.decrypt(keyCrypter, aesKey);
byte[] originalPrivateKeyBytes = originalKey.getPrivKeyBytes();
byte[] rebornKeyBytes = rebornUnencryptedKey.getPrivKeyBytes();
if (!Arrays.equals(originalPrivateKeyBytes, rebornKeyBytes)) {
log.error("The check that encryption could be reversed failed for {}", originalKey);
return false;
}
return true;
} catch (KeyCrypterException kce) {
log.error(kce.getMessage());
return false;
}
}
/**
* Indicates whether the private key is encrypted (true) or not (false).
* A private key is deemed to be encrypted when there is both a KeyCrypter and the encryptedPrivateKey is non-zero.
*/
@Override
public boolean isEncrypted() {
return keyCrypter != null && encryptedPrivateKey != null && encryptedPrivateKey.getEncryptedBytes().length > 0;
}
@Override
public EncryptionType getEncryptionType() {
return new EncryptionType(EncryptionType.Deriver.SCRYPT, keyCrypter != null ? keyCrypter.getCrypterType() : EncryptionType.Crypter.NONE);
}
/**
* A wrapper for {@link #getPrivKeyBytes()} that returns null if the private key bytes are missing or would have
* to be derived (for the HD key case).
*/
@Override
public byte[] getSecretBytes() {
if (hasPrivKey()) {
return getPrivKeyBytes();
} else {
return null;
}
}
/** An alias for {@link #getEncryptedPrivateKey()} */
@Override
public EncryptedData getEncryptedData() {
return getEncryptedPrivateKey();
}
/**
* Returns the the encrypted private key bytes and initialisation vector for this ECKey, or null if the ECKey
* is not encrypted.
*/
public EncryptedData getEncryptedPrivateKey() {
return encryptedPrivateKey;
}
/**
* Returns the KeyCrypter that was used to encrypt to encrypt this ECKey. You need this to decrypt the ECKey.
*/
public KeyCrypter getKeyCrypter() {
return keyCrypter;
}
public static class MissingPrivateKeyException extends RuntimeException { public static class MissingPrivateKeyException extends RuntimeException {
} }
public static class KeyIsEncryptedException extends MissingPrivateKeyException {
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;

View file

@ -38,6 +38,10 @@ public class HDKeyDerivation {
return new DeterministicKey(childNumberPath, chainCode, priv, null); return new DeterministicKey(childNumberPath, chainCode, priv, null);
} }
public static DeterministicKey createMasterPubKeyFromBytes(byte[] pubKeyBytes, byte[] chainCode) {
return new DeterministicKey(List.of(), chainCode, new LazyECPoint(ECKey.CURVE.getCurve(), pubKeyBytes), null, null);
}
public static DeterministicKey deriveChildKey(DeterministicKey parent, ChildNumber childNumber) throws HDDerivationException { public static DeterministicKey deriveChildKey(DeterministicKey parent, ChildNumber childNumber) throws HDDerivationException {
if(parent.isPubKeyOnly()) { if(parent.isPubKeyOnly()) {
RawKeyBytes rawKey = deriveChildKeyBytesFromPublic(parent, childNumber); RawKeyBytes rawKey = deriveChildKeyBytesFromPublic(parent, childNumber);

View file

@ -21,7 +21,7 @@ public class HMacDSANonceKCalculator implements DSAKCalculator {
private final HMac hMac; private final HMac hMac;
private final byte[] K; private final byte[] K;
private final byte[] V; private final byte[] V;
private final long counter; private final Long counter;
private BigInteger n; private BigInteger n;
@ -31,11 +31,11 @@ public class HMacDSANonceKCalculator implements DSAKCalculator {
* @param digest digest to build the HMAC on. * @param digest digest to build the HMAC on.
* @param counter additional data as per RFC 6979 3.6 * @param counter additional data as per RFC 6979 3.6
*/ */
public HMacDSANonceKCalculator(Digest digest, int counter) { public HMacDSANonceKCalculator(Digest digest, Integer counter) {
this.hMac = new HMac(digest); this.hMac = new HMac(digest);
this.V = new byte[hMac.getMacSize()]; this.V = new byte[hMac.getMacSize()];
this.K = new byte[hMac.getMacSize()]; this.K = new byte[hMac.getMacSize()];
this.counter = Integer.toUnsignedLong(counter); this.counter = (counter == null ? null : Integer.toUnsignedLong(counter));
} }
public boolean isDeterministic() public boolean isDeterministic()
@ -74,8 +74,12 @@ public class HMacDSANonceKCalculator implements DSAKCalculator {
System.arraycopy(mVal, 0, m, m.length - mVal.length, mVal.length); System.arraycopy(mVal, 0, m, m.length - mVal.length, mVal.length);
BigInteger additional = BigInteger.valueOf(counter); byte[] c = null;
byte[] aData = Utils.bigIntegerToBytes(additional, size); if(counter != null) {
BigInteger additional = BigInteger.valueOf(counter);
c = Utils.bigIntegerToBytes(additional, size);
Utils.reverse(c);
}
hMac.init(new KeyParameter(K)); hMac.init(new KeyParameter(K));
@ -83,7 +87,9 @@ public class HMacDSANonceKCalculator implements DSAKCalculator {
hMac.update((byte)0x00); hMac.update((byte)0x00);
hMac.update(x, 0, x.length); hMac.update(x, 0, x.length);
hMac.update(m, 0, m.length); hMac.update(m, 0, m.length);
hMac.update(aData, 0, aData.length); if(c != null) {
hMac.update(c, 0, c.length);
}
hMac.doFinal(K, 0); hMac.doFinal(K, 0);
@ -97,6 +103,9 @@ public class HMacDSANonceKCalculator implements DSAKCalculator {
hMac.update((byte)0x01); hMac.update((byte)0x01);
hMac.update(x, 0, x.length); hMac.update(x, 0, x.length);
hMac.update(m, 0, m.length); hMac.update(m, 0, m.length);
if(counter != null) {
hMac.update(c, 0, c.length);
}
hMac.doFinal(K, 0); hMac.doFinal(K, 0);

View file

@ -20,7 +20,7 @@ public class LazyECPoint {
public LazyECPoint(ECCurve curve, byte[] bits) { public LazyECPoint(ECCurve curve, byte[] bits) {
this.curve = curve; this.curve = curve;
this.bits = bits; this.bits = (bits != null && bits.length == 32 ? addYCoord(bits) : bits);
this.compressed = ECKey.isPubKeyCompressed(bits); this.compressed = ECKey.isPubKeyCompressed(bits);
} }
@ -61,6 +61,13 @@ public class LazyECPoint {
return get().getEncoded(compressed); return get().getEncoded(compressed);
} }
public byte[] getEncodedXCoord() {
byte[] compressed = getEncoded(true);
byte[] xcoord = new byte[32];
System.arraycopy(compressed, 1, xcoord, 0, 32);
return xcoord;
}
public String toString() { public String toString() {
return Hex.toHexString(getEncoded()); return Hex.toHexString(getEncoded());
} }
@ -80,4 +87,11 @@ public class LazyECPoint {
private byte[] getCanonicalEncoding() { private byte[] getCanonicalEncoding() {
return getEncoded(true); return getEncoded(true);
} }
private static byte[] addYCoord(byte[] xcoord) {
byte[] compressed = new byte[33];
compressed[0] = 0x02;
System.arraycopy(xcoord, 0, compressed, 1, 32);
return compressed;
}
} }

View file

@ -0,0 +1,98 @@
package com.sparrowwallet.drongo.crypto;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.protocol.TransactionSignature;
import org.bitcoin.NativeSecp256k1;
import org.bitcoin.NativeSecp256k1Util;
import org.bitcoin.Secp256k1Context;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Objects;
/**
* Groups the two components that make up a Schnorr signature
*/
public class SchnorrSignature {
private static final Logger log = LoggerFactory.getLogger(SchnorrSignature.class);
/**
* The two components of the signature.
*/
public final BigInteger r, s;
/**
* Constructs a signature with the given components. Does NOT automatically canonicalise the signature.
*/
public SchnorrSignature(BigInteger r, BigInteger s) {
this.r = r;
this.s = s;
}
public byte[] encode() {
ByteBuffer buffer = ByteBuffer.allocate(64);
buffer.put(Utils.bigIntegerToBytes(r, 32));
buffer.put(Utils.bigIntegerToBytes(s, 32));
return buffer.array();
}
public static SchnorrSignature decode(byte[] bytes) {
if(bytes.length != 64) {
throw new IllegalArgumentException("Invalid Schnorr signature length of " + bytes.length + " bytes");
}
BigInteger r = new BigInteger(1, Arrays.copyOfRange(bytes, 0, 32));
BigInteger s = new BigInteger(1, Arrays.copyOfRange(bytes, 32, 64));
return new SchnorrSignature(r, s);
}
public static TransactionSignature decodeFromBitcoin(byte[] bytes) {
if(bytes.length < 64 || bytes.length > 65) {
throw new IllegalArgumentException("Invalid Schnorr signature length of " + bytes.length + " bytes");
}
BigInteger r = new BigInteger(1, Arrays.copyOfRange(bytes, 0, 32));
BigInteger s = new BigInteger(1, Arrays.copyOfRange(bytes, 32, 64));
if(bytes.length == 65) {
return new TransactionSignature(r, s, TransactionSignature.Type.SCHNORR, bytes[64]);
}
return new TransactionSignature(r, s, TransactionSignature.Type.SCHNORR, (byte)0);
}
public boolean verify(byte[] data, byte[] pub) {
if(!Secp256k1Context.isEnabled()) {
throw new IllegalStateException("libsecp256k1 is not enabled");
}
try {
return NativeSecp256k1.schnorrVerify(encode(), data, pub);
} catch(NativeSecp256k1Util.AssertFailException e) {
log.error("Error verifying schnorr signature", e);
}
return false;
}
@Override
public boolean equals(Object o) {
if(this == o) {
return true;
}
if(o == null || getClass() != o.getClass()) {
return false;
}
SchnorrSignature that = (SchnorrSignature) o;
return r.equals(that.r) && s.equals(that.s);
}
@Override
public int hashCode() {
return Objects.hash(r, s);
}
}

View file

@ -0,0 +1,109 @@
package com.sparrowwallet.drongo.crypto;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.protocol.Base58;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Objects;
/**
* <p>In Bitcoin the following format is often used to represent some type of key:</p>
* <p/>
* <pre>[one version byte] [data bytes] [4 checksum bytes]</pre>
* <p/>
* <p>and the result is then Base58 encoded. This format is used for addresses, and private keys exported using the
* dumpprivkey command.</p>
*/
public class VersionedChecksummedBytes implements Serializable, Cloneable, Comparable<VersionedChecksummedBytes> {
protected final int version;
protected byte[] bytes;
protected VersionedChecksummedBytes(String encoded) {
byte[] versionAndDataBytes = Base58.decodeChecked(encoded);
byte versionByte = versionAndDataBytes[0];
version = versionByte & 0xFF;
bytes = new byte[versionAndDataBytes.length - 1];
System.arraycopy(versionAndDataBytes, 1, bytes, 0, versionAndDataBytes.length - 1);
}
protected VersionedChecksummedBytes(int version, byte[] bytes) {
this.version = version;
this.bytes = bytes;
}
/**
* Returns the base-58 encoded String representation of this
* object, including version and checksum bytes.
*/
public final String toBase58() {
// A stringified buffer is:
// 1 byte version + data bytes + 4 bytes check code (a truncated hash)
byte[] addressBytes = new byte[1 + bytes.length + 4];
addressBytes[0] = (byte) version;
System.arraycopy(bytes, 0, addressBytes, 1, bytes.length);
byte[] checksum = Sha256Hash.hashTwice(addressBytes, 0, bytes.length + 1);
System.arraycopy(checksum, 0, addressBytes, bytes.length + 1, 4);
return Base58.encode(addressBytes);
}
@Override
public String toString() {
return toBase58();
}
@Override
public boolean equals(Object o) {
if(this == o) {
return true;
}
if(o == null || getClass() != o.getClass()) {
return false;
}
VersionedChecksummedBytes that = (VersionedChecksummedBytes) o;
return version == that.version && Arrays.equals(bytes, that.bytes);
}
@Override
public int hashCode() {
int result = Objects.hash(version);
result = 31 * result + Arrays.hashCode(bytes);
return result;
}
/**
* {@inheritDoc}
*
* This implementation narrows the return type to <code>VersionedChecksummedBytes</code>
* and allows subclasses to throw <code>CloneNotSupportedException</code> even though it
* is never thrown by this implementation.
*/
@Override
public VersionedChecksummedBytes clone() throws CloneNotSupportedException {
return (VersionedChecksummedBytes) super.clone();
}
/**
* {@inheritDoc}
*
* This implementation uses an optimized Google Guava method to compare <code>bytes</code>.
*/
@Override
public int compareTo(VersionedChecksummedBytes o) {
int result = Integer.compare(this.version, o.version);
Utils.LexicographicByteArrayComparator lexicographicByteArrayComparator = new Utils.LexicographicByteArrayComparator();
return result != 0 ? result : lexicographicByteArrayComparator.compare(this.bytes, o.bytes);
}
/**
* Returns the "version" or "header" byte: the first byte of the data. This is used to disambiguate what the
* contents apply to, for example, which network the key or address is valid on.
*
* @return A positive number between 0 and 255.
*/
public int getVersion() {
return version;
}
}

View file

@ -5,6 +5,7 @@ import java.util.regex.Pattern;
public class Miniscript { public class Miniscript {
private static final Pattern SINGLE_PATTERN = Pattern.compile("pkh?\\("); private static final Pattern SINGLE_PATTERN = Pattern.compile("pkh?\\(");
private static final Pattern TAPROOT_PATTERN = Pattern.compile("tr\\(");
private static final Pattern MULTI_PATTERN = Pattern.compile("multi\\(([\\d+])"); private static final Pattern MULTI_PATTERN = Pattern.compile("multi\\(([\\d+])");
private String script; private String script;
@ -27,6 +28,11 @@ public class Miniscript {
return 1; return 1;
} }
Matcher taprootMatcher = TAPROOT_PATTERN.matcher(script);
if(taprootMatcher.find()) {
return 1;
}
Matcher multiMatcher = MULTI_PATTERN.matcher(script); Matcher multiMatcher = MULTI_PATTERN.matcher(script);
if(multiMatcher.find()) { if(multiMatcher.find()) {
String threshold = multiMatcher.group(1); String threshold = multiMatcher.group(1);

View file

@ -2,13 +2,14 @@ package com.sparrowwallet.drongo.policy;
import com.sparrowwallet.drongo.protocol.ScriptType; import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Keystore; import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.Persistable;
import java.util.List; import java.util.List;
import static com.sparrowwallet.drongo.protocol.ScriptType.*; import static com.sparrowwallet.drongo.protocol.ScriptType.*;
import static com.sparrowwallet.drongo.policy.PolicyType.*; import static com.sparrowwallet.drongo.policy.PolicyType.*;
public class Policy { public class Policy extends Persistable {
private static final String DEFAULT_NAME = "Default"; private static final String DEFAULT_NAME = "Default";
private String name; private String name;
@ -23,6 +24,10 @@ public class Policy {
this.miniscript = miniscript; this.miniscript = miniscript;
} }
public String getName() {
return name;
}
public Miniscript getMiniscript() { public Miniscript getMiniscript() {
return miniscript; return miniscript;
} }
@ -57,6 +62,8 @@ public class Policy {
} }
public Policy copy() { public Policy copy() {
return new Policy(name, miniscript.copy()); Policy policy = new Policy(name, miniscript.copy());
policy.setId(getId());
return policy;
} }
} }

View file

@ -39,10 +39,18 @@ public class Bech32 {
public static class Bech32Data { public static class Bech32Data {
public final String hrp; public final String hrp;
public final byte[] data; public final byte[] data;
public final Encoding encoding;
private Bech32Data(final String hrp, final byte[] data) { private Bech32Data(final String hrp, final byte[] data) {
this.hrp = hrp; this.hrp = hrp;
this.data = data; this.data = data;
this.encoding = (data[0] == 0x00 ? Encoding.BECH32 : Encoding.BECH32M);
}
public Bech32Data(String hrp, byte[] data, Encoding encoding) {
this.hrp = hrp;
this.data = data;
this.encoding = encoding;
} }
} }
@ -64,7 +72,7 @@ public class Bech32 {
/** Expand a HRP for use in checksum computation. */ /** Expand a HRP for use in checksum computation. */
private static byte[] expandHrp(final String hrp) { private static byte[] expandHrp(final String hrp) {
int hrpLength = hrp.length(); int hrpLength = hrp.length();
byte ret[] = new byte[hrpLength * 2 + 1]; byte[] ret = new byte[hrpLength * 2 + 1];
for (int i = 0; i < hrpLength; ++i) { for (int i = 0; i < hrpLength; ++i) {
int c = hrp.charAt(i) & 0x7f; // Limit to standard 7-bit ASCII int c = hrp.charAt(i) & 0x7f; // Limit to standard 7-bit ASCII
ret[i] = (byte) ((c >>> 5) & 0x07); ret[i] = (byte) ((c >>> 5) & 0x07);
@ -75,21 +83,29 @@ public class Bech32 {
} }
/** Verify a checksum. */ /** Verify a checksum. */
private static boolean verifyChecksum(final String hrp, final byte[] values) { private static Encoding verifyChecksum(final String hrp, final byte[] values) {
byte[] hrpExpanded = expandHrp(hrp); byte[] hrpExpanded = expandHrp(hrp);
byte[] combined = new byte[hrpExpanded.length + values.length]; byte[] combined = new byte[hrpExpanded.length + values.length];
System.arraycopy(hrpExpanded, 0, combined, 0, hrpExpanded.length); System.arraycopy(hrpExpanded, 0, combined, 0, hrpExpanded.length);
System.arraycopy(values, 0, combined, hrpExpanded.length, values.length); System.arraycopy(values, 0, combined, hrpExpanded.length, values.length);
return polymod(combined) == 1;
int check = polymod(combined);
for(Encoding encoding : Encoding.values()) {
if(check == encoding.checksumConstant) {
return encoding;
}
}
return null;
} }
/** Create a checksum. */ /** Create a checksum. */
private static byte[] createChecksum(final String hrp, final byte[] values) { private static byte[] createChecksum(final String hrp, Encoding encoding, final byte[] values) {
byte[] hrpExpanded = expandHrp(hrp); byte[] hrpExpanded = expandHrp(hrp);
byte[] enc = new byte[hrpExpanded.length + values.length + 6]; byte[] enc = new byte[hrpExpanded.length + values.length + 6];
System.arraycopy(hrpExpanded, 0, enc, 0, hrpExpanded.length); System.arraycopy(hrpExpanded, 0, enc, 0, hrpExpanded.length);
System.arraycopy(values, 0, enc, hrpExpanded.length, values.length); System.arraycopy(values, 0, enc, hrpExpanded.length, values.length);
int mod = polymod(enc) ^ 1; int mod = polymod(enc) ^ encoding.checksumConstant;
byte[] ret = new byte[6]; byte[] ret = new byte[6];
for (int i = 0; i < 6; ++i) { for (int i = 0; i < 6; ++i) {
ret[i] = (byte) ((mod >>> (5 * (5 - i))) & 31); ret[i] = (byte) ((mod >>> (5 * (5 - i))) & 31);
@ -99,16 +115,17 @@ public class Bech32 {
/** Encode a Bech32 string. */ /** Encode a Bech32 string. */
public static String encode(final Bech32Data bech32) { public static String encode(final Bech32Data bech32) {
return encode(bech32.hrp, bech32.data); return encode(bech32.hrp, bech32.encoding, bech32.data);
} }
/** Encode a Bech32 string. */ /** Encode a Bech32 string. */
public static String encode(String hrp, int version, final byte[] values) { public static String encode(String hrp, int version, final byte[] values) {
return encode(hrp, encode(0, values)); Encoding encoding = (version == 0 ? Encoding.BECH32 : Encoding.BECH32M);
return encode(hrp, encoding, encode(version, values));
} }
/** Encode a Bech32 string. */ /** Encode a Bech32 string. */
public static String encode(String hrp, final byte[] values) { public static String encode(String hrp, Encoding encoding, final byte[] values) {
if(hrp.length() < 1) { if(hrp.length() < 1) {
throw new ProtocolException("Human-readable part is too short"); throw new ProtocolException("Human-readable part is too short");
} }
@ -118,7 +135,7 @@ public class Bech32 {
} }
hrp = hrp.toLowerCase(Locale.ROOT); hrp = hrp.toLowerCase(Locale.ROOT);
byte[] checksum = createChecksum(hrp, values); byte[] checksum = createChecksum(hrp, encoding, values);
byte[] combined = new byte[values.length + checksum.length]; byte[] combined = new byte[values.length + checksum.length];
System.arraycopy(values, 0, combined, 0, values.length); System.arraycopy(values, 0, combined, 0, values.length);
System.arraycopy(checksum, 0, combined, values.length, checksum.length); System.arraycopy(checksum, 0, combined, values.length, checksum.length);
@ -133,10 +150,14 @@ public class Bech32 {
/** Decode a Bech32 string. */ /** Decode a Bech32 string. */
public static Bech32Data decode(final String str) { public static Bech32Data decode(final String str) {
return decode(str, 90);
}
public static Bech32Data decode(final String str, int limit) {
boolean lower = false, upper = false; boolean lower = false, upper = false;
if (str.length() < 8) if (str.length() < 8)
throw new ProtocolException("Input too short: " + str.length()); throw new ProtocolException("Input too short: " + str.length());
if (str.length() > 90) if (str.length() > limit)
throw new ProtocolException("Input too long: " + str.length()); throw new ProtocolException("Input too long: " + str.length());
for (int i = 0; i < str.length(); ++i) { for (int i = 0; i < str.length(); ++i) {
char c = str.charAt(i); char c = str.charAt(i);
@ -163,14 +184,18 @@ public class Bech32 {
values[i] = CHARSET_REV[c]; values[i] = CHARSET_REV[c];
} }
String hrp = str.substring(0, pos).toLowerCase(Locale.ROOT); String hrp = str.substring(0, pos).toLowerCase(Locale.ROOT);
if (!verifyChecksum(hrp, values)) throw new ProtocolException("Invalid checksum"); Encoding encoding = verifyChecksum(hrp, values);
return new Bech32Data(hrp, Arrays.copyOfRange(values, 0, values.length - 6)); if(encoding == null) {
throw new ProtocolException("Invalid checksum");
}
return new Bech32Data(hrp, Arrays.copyOfRange(values, 0, values.length - 6), encoding);
} }
private static byte[] encode(int witnessVersion, byte[] witnessProgram) { private static byte[] encode(int witnessVersion, byte[] witnessProgram) {
byte[] convertedProgram = convertBits(witnessProgram, 0, witnessProgram.length, 8, 5, true); byte[] convertedProgram = convertBits(witnessProgram, 0, witnessProgram.length, 8, 5, true);
byte[] bytes = new byte[1 + convertedProgram.length]; byte[] bytes = new byte[1 + convertedProgram.length];
bytes[0] = (byte) (Script.encodeToOpN(witnessVersion) & 0xff); bytes[0] = (byte)(witnessVersion & 0xff);
System.arraycopy(convertedProgram, 0, bytes, 1, convertedProgram.length); System.arraycopy(convertedProgram, 0, bytes, 1, convertedProgram.length);
return bytes; return bytes;
} }
@ -206,4 +231,14 @@ public class Bech32 {
} }
return out.toByteArray(); return out.toByteArray();
} }
public enum Encoding {
BECH32(1), BECH32M(0x2bc830a3);
private final int checksumConstant;
Encoding(int checksumConstant) {
this.checksumConstant = checksumConstant;
}
}
} }

View file

@ -1,7 +1,12 @@
package com.sparrowwallet.drongo.protocol; package com.sparrowwallet.drongo.protocol;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Date; import java.util.Date;
import static com.sparrowwallet.drongo.Utils.uint32ToByteStreamLE;
public class BlockHeader extends Message { public class BlockHeader extends Message {
private long version; private long version;
private Sha256Hash prevBlockHash; private Sha256Hash prevBlockHash;
@ -14,6 +19,16 @@ public class BlockHeader extends Message {
super(rawheader, 0); super(rawheader, 0);
} }
public BlockHeader(long version, Sha256Hash prevBlockHash, Sha256Hash merkleRoot, Sha256Hash witnessRoot, long time, long difficultyTarget, long nonce) {
this.version = version;
this.prevBlockHash = prevBlockHash;
this.merkleRoot = merkleRoot;
this.witnessRoot = witnessRoot;
this.time = time;
this.difficultyTarget = difficultyTarget;
this.nonce = nonce;
}
@Override @Override
protected void parse() throws ProtocolException { protected void parse() throws ProtocolException {
version = readUint32(); version = readUint32();
@ -57,4 +72,25 @@ public class BlockHeader extends Message {
public long getNonce() { public long getNonce() {
return nonce; return nonce;
} }
public byte[] bitcoinSerialize() {
try {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
bitcoinSerializeToStream(outputStream);
return outputStream.toByteArray();
} catch (IOException e) {
//can't happen
}
return null;
}
protected void bitcoinSerializeToStream(OutputStream stream) throws IOException {
uint32ToByteStreamLE(version, stream);
stream.write(prevBlockHash.getReversedBytes());
stream.write(merkleRoot.getReversedBytes());
uint32ToByteStreamLE(time, stream);
uint32ToByteStreamLE(difficultyTarget, stream);
uint32ToByteStreamLE(nonce, stream);
}
} }

View file

@ -0,0 +1,48 @@
package com.sparrowwallet.drongo.protocol;
public class HashIndex {
private final Sha256Hash hash;
private final long index;
public HashIndex(Sha256Hash hash, long index) {
this.hash = hash;
this.index = index;
}
public Sha256Hash getHash() {
return hash;
}
public long getIndex() {
return index;
}
@Override
public String toString() {
return hash.toString() + ":" + index;
}
@Override
public boolean equals(Object o) {
if(this == o) {
return true;
}
if(o == null || getClass() != o.getClass()) {
return false;
}
HashIndex hashIndex = (HashIndex) o;
if(index != hashIndex.index) {
return false;
}
return hash.equals(hashIndex.hash);
}
@Override
public int hashCode() {
int result = hash.hashCode();
result = 31 * result + (int) (index ^ (index >>> 32));
return result;
}
}

View file

@ -135,6 +135,21 @@ public class Script {
return false; return false;
} }
/**
* <p>If the program somehow pays to a pubkey, returns the pubkey.</p>
*
* <p>Otherwise this method throws a ScriptException.</p>
*/
public ECKey getPubKey() throws ProtocolException {
for(ScriptType scriptType : SINGLE_KEY_TYPES) {
if(scriptType.isScriptType(this)) {
return scriptType.getPublicKeyFromScript(this);
}
}
throw new ProtocolException("Script not a standard form that contains a single key");
}
/** /**
* <p>If the program somehow pays to a hash, returns the hash.</p> * <p>If the program somehow pays to a hash, returns the hash.</p>
* *
@ -150,6 +165,14 @@ public class Script {
throw new ProtocolException("Script not a standard form that contains a single hash"); throw new ProtocolException("Script not a standard form that contains a single hash");
} }
public Address getToAddress() {
try {
return getToAddresses()[0];
} catch(Exception e) {
return null;
}
}
/** /**
* Gets the destination address from this script, if it's in the required form. * Gets the destination address from this script, if it's in the required form.
*/ */
@ -160,8 +183,15 @@ public class Script {
} }
} }
if(P2PK.isScriptType(this)) { //Special handling for taproot tweaked keys - we don't want to tweak them again
return new Address[] { P2PK.getAddress(P2PK.getPublicKeyFromScript(this).getPubKey()) }; if(P2TR.isScriptType(this)) {
return new Address[] { new P2TRAddress(P2TR.getPublicKeyFromScript(this).getPubKeyXCoord()) };
}
for(ScriptType scriptType : SINGLE_KEY_TYPES) {
if(scriptType.isScriptType(this)) {
return new Address[] { scriptType.getAddress(scriptType.getPublicKeyFromScript(this)) };
}
} }
if(MULTISIG.isScriptType(this)) { if(MULTISIG.isScriptType(this)) {
@ -178,7 +208,8 @@ public class Script {
} }
public int getNumRequiredSignatures() throws NonStandardScriptException { public int getNumRequiredSignatures() throws NonStandardScriptException {
if(P2PK.isScriptType(this) || P2PKH.isScriptType(this) || P2WPKH.isScriptType(this)) { //TODO: Handle P2TR script path spends
if(P2PK.isScriptType(this) || P2PKH.isScriptType(this) || P2WPKH.isScriptType(this) || P2TR.isScriptType(this)) {
return 1; return 1;
} }

View file

@ -110,8 +110,8 @@ public class ScriptChunk {
} }
try { try {
ECKey.ECDSASignature.decodeFromDER(data); TransactionSignature.decodeFromBitcoin(data, false);
} catch(SignatureDecodeException e) { } catch(Exception e) {
return false; return false;
} }
@ -120,7 +120,7 @@ public class ScriptChunk {
public TransactionSignature getSignature() { public TransactionSignature getSignature() {
try { try {
return TransactionSignature.decodeFromBitcoin(data, false, false); return TransactionSignature.decodeFromBitcoin(data, false);
} catch(SignatureDecodeException e) { } catch(SignatureDecodeException e) {
throw new ProtocolException("Could not decode signature", e); throw new ProtocolException("Could not decode signature", e);
} }

View file

@ -8,6 +8,8 @@ import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.crypto.ECKey; import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.policy.PolicyType; import com.sparrowwallet.drongo.policy.PolicyType;
import java.time.LocalDate;
import java.time.Month;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -17,7 +19,7 @@ import static com.sparrowwallet.drongo.protocol.ScriptOpCodes.*;
import static com.sparrowwallet.drongo.protocol.Transaction.WITNESS_SCALE_FACTOR; import static com.sparrowwallet.drongo.protocol.Transaction.WITNESS_SCALE_FACTOR;
public enum ScriptType { public enum ScriptType {
P2PK("P2PK", "m/44'/17'/0'") { P2PK("P2PK", "Legacy (P2PK)", "m/44'/17'/0'") {
@Override @Override
public Address getAddress(byte[] pubKey) { public Address getAddress(byte[] pubKey) {
return new P2PKAddress(pubKey); return new P2PKAddress(pubKey);
@ -126,12 +128,17 @@ public enum ScriptType {
throw new ProtocolException(getName() + " is not a multisig script type"); throw new ProtocolException(getName() + " is not a multisig script type");
} }
@Override
public TransactionSignature.Type getSignatureType() {
return TransactionSignature.Type.ECDSA;
};
@Override @Override
public List<PolicyType> getAllowedPolicyTypes() { public List<PolicyType> getAllowedPolicyTypes() {
return List.of(SINGLE); return List.of(SINGLE);
} }
}, },
P2PKH("P2PKH", "m/44'/17'/0'") { P2PKH("P2PKH", "Legacy (P2PKH)", "m/44'/17'/0'") {
@Override @Override
public Address getAddress(byte[] pubKeyHash) { public Address getAddress(byte[] pubKeyHash) {
return new P2PKHAddress(pubKeyHash); return new P2PKHAddress(pubKeyHash);
@ -239,12 +246,17 @@ public enum ScriptType {
throw new ProtocolException(getName() + " is not a multisig script type"); throw new ProtocolException(getName() + " is not a multisig script type");
} }
@Override
public TransactionSignature.Type getSignatureType() {
return TransactionSignature.Type.ECDSA;
};
@Override @Override
public List<PolicyType> getAllowedPolicyTypes() { public List<PolicyType> getAllowedPolicyTypes() {
return List.of(SINGLE); return List.of(SINGLE);
} }
}, },
MULTISIG("Bare Multisig", "m/44'/17'/0'") { MULTISIG("Bare Multisig", "Bare Multisig", "m/44'/17'/0'") {
@Override @Override
public Address getAddress(byte[] bytes) { public Address getAddress(byte[] bytes) {
throw new ProtocolException("No single address for multisig script type"); throw new ProtocolException("No single address for multisig script type");
@ -425,12 +437,17 @@ public enum ScriptType {
return transaction.addInput(prevOutput.getHash(), prevOutput.getIndex(), scriptSig); return transaction.addInput(prevOutput.getHash(), prevOutput.getIndex(), scriptSig);
} }
@Override
public TransactionSignature.Type getSignatureType() {
return TransactionSignature.Type.ECDSA;
};
@Override @Override
public List<PolicyType> getAllowedPolicyTypes() { public List<PolicyType> getAllowedPolicyTypes() {
return List.of(MULTI); return List.of(MULTI);
} }
}, },
P2SH("P2SH", "m/45'") { P2SH("P2SH", "Legacy (P2SH)", "m/45'") {
@Override @Override
public Address getAddress(byte[] scriptHash) { public Address getAddress(byte[] scriptHash) {
return new P2SHAddress(scriptHash); return new P2SHAddress(scriptHash);
@ -550,12 +567,17 @@ public enum ScriptType {
return transaction.addInput(prevOutput.getHash(), prevOutput.getIndex(), scriptSig); return transaction.addInput(prevOutput.getHash(), prevOutput.getIndex(), scriptSig);
} }
@Override
public TransactionSignature.Type getSignatureType() {
return TransactionSignature.Type.ECDSA;
};
@Override @Override
public List<PolicyType> getAllowedPolicyTypes() { public List<PolicyType> getAllowedPolicyTypes() {
return List.of(MULTI); return List.of(MULTI);
} }
}, },
P2SH_P2WPKH("P2SH-P2WPKH", "m/49'/17'/0'") { P2SH_P2WPKH("P2SH-P2WPKH", "Nested Segwit (P2SH-P2WPKH)", "m/49'/17'/0'") {
@Override @Override
public Address getAddress(byte[] scriptHash) { public Address getAddress(byte[] scriptHash) {
return P2SH.getAddress(scriptHash); return P2SH.getAddress(scriptHash);
@ -653,12 +675,17 @@ public enum ScriptType {
throw new ProtocolException(getName() + " is not a multisig script type"); throw new ProtocolException(getName() + " is not a multisig script type");
} }
@Override
public TransactionSignature.Type getSignatureType() {
return TransactionSignature.Type.ECDSA;
};
@Override @Override
public List<PolicyType> getAllowedPolicyTypes() { public List<PolicyType> getAllowedPolicyTypes() {
return List.of(SINGLE); return List.of(SINGLE);
} }
}, },
P2SH_P2WSH("P2SH-P2WSH", "m/48'/17'/0'/1'") { P2SH_P2WSH("P2SH-P2WSH", "Nested Segwit (P2SH-P2WSH)", "m/48'/17'/0'/1'") {
@Override @Override
public Address getAddress(byte[] scriptHash) { public Address getAddress(byte[] scriptHash) {
return P2SH.getAddress(scriptHash); return P2SH.getAddress(scriptHash);
@ -754,12 +781,17 @@ public enum ScriptType {
return transaction.addInput(prevOutput.getHash(), prevOutput.getIndex(), scriptSig, witness); return transaction.addInput(prevOutput.getHash(), prevOutput.getIndex(), scriptSig, witness);
} }
@Override
public TransactionSignature.Type getSignatureType() {
return TransactionSignature.Type.ECDSA;
};
@Override @Override
public List<PolicyType> getAllowedPolicyTypes() { public List<PolicyType> getAllowedPolicyTypes() {
return List.of(MULTI, CUSTOM); return List.of(MULTI, CUSTOM);
} }
}, },
P2WPKH("P2WPKH", "m/84'/17'/0'") { P2WPKH("P2WPKH", "Native Segwit (P2WPKH)", "m/84'/17'/0'") {
@Override @Override
public Address getAddress(byte[] pubKeyHash) { public Address getAddress(byte[] pubKeyHash) {
return new P2WPKHAddress(pubKeyHash); return new P2WPKHAddress(pubKeyHash);
@ -859,12 +891,17 @@ public enum ScriptType {
throw new ProtocolException(getName() + " is not a multisig script type"); throw new ProtocolException(getName() + " is not a multisig script type");
} }
@Override
public TransactionSignature.Type getSignatureType() {
return TransactionSignature.Type.ECDSA;
};
@Override @Override
public List<PolicyType> getAllowedPolicyTypes() { public List<PolicyType> getAllowedPolicyTypes() {
return List.of(SINGLE); return List.of(SINGLE);
} }
}, },
P2WSH("P2WSH", "m/48'/17'/0'/2'") { P2WSH("P2WSH", "Native Segwit (P2WSH)", "m/48'/17'/0'/2'") {
@Override @Override
public Address getAddress(byte[] scriptHash) { public Address getAddress(byte[] scriptHash) {
return new P2WSHAddress(scriptHash); return new P2WSHAddress(scriptHash);
@ -970,17 +1007,144 @@ public enum ScriptType {
return transaction.addInput(prevOutput.getHash(), prevOutput.getIndex(), scriptSig, witness); return transaction.addInput(prevOutput.getHash(), prevOutput.getIndex(), scriptSig, witness);
} }
@Override
public TransactionSignature.Type getSignatureType() {
return TransactionSignature.Type.ECDSA;
};
@Override @Override
public List<PolicyType> getAllowedPolicyTypes() { public List<PolicyType> getAllowedPolicyTypes() {
return List.of(MULTI, CUSTOM); return List.of(MULTI, CUSTOM);
} }
},
P2TR("P2TR", "Taproot (P2TR)", "m/86'/0'/0'") {
@Override
public ECKey getOutputKey(ECKey derivedKey) {
return derivedKey.getTweakedOutputKey();
}
@Override
public Address getAddress(byte[] pubKey) {
return new P2TRAddress(pubKey);
}
@Override
public Address getAddress(ECKey derivedKey) {
return getAddress(getOutputKey(derivedKey).getPubKeyXCoord());
}
@Override
public Address getAddress(Script script) {
throw new ProtocolException("Cannot create a taproot address without a keypath");
}
@Override
public Script getOutputScript(byte[] pubKey) {
List<ScriptChunk> chunks = new ArrayList<>();
chunks.add(new ScriptChunk(OP_1, null));
chunks.add(new ScriptChunk(pubKey.length, pubKey));
return new Script(chunks);
}
@Override
public Script getOutputScript(ECKey derivedKey) {
return getOutputScript(getOutputKey(derivedKey).getPubKeyXCoord());
}
@Override
public Script getOutputScript(Script script) {
throw new ProtocolException("Cannot create a taproot output script without a keypath");
}
@Override
public String getOutputDescriptor(ECKey derivedKey) {
return getDescriptor() + Utils.bytesToHex(derivedKey.getPubKeyXCoord()) + getCloseDescriptor();
}
@Override
public String getOutputDescriptor(Script script) {
throw new ProtocolException("Cannot create a taproot output descriptor without a keypath");
}
@Override
public String getDescriptor() {
return "tr(";
}
@Override
public boolean isScriptType(Script script) {
List<ScriptChunk> chunks = script.chunks;
if (chunks.size() != 2)
return false;
if (!chunks.get(0).equalsOpCode(OP_1))
return false;
byte[] chunk1data = chunks.get(1).data;
if (chunk1data == null)
return false;
if (chunk1data.length != 32)
return false;
return true;
}
@Override
public byte[] getHashFromScript(Script script) {
throw new ProtocolException("P2TR script does not contain a hash, use getPublicKeyFromScript(script) to retrieve public key");
}
@Override
public ECKey getPublicKeyFromScript(Script script) {
return ECKey.fromPublicOnly(script.chunks.get(1).data);
}
@Override
public Script getScriptSig(Script scriptPubKey, ECKey pubKey, TransactionSignature signature) {
if(!isScriptType(scriptPubKey)) {
throw new ProtocolException("Provided scriptPubKey is not a " + getName() + " script");
}
if(!scriptPubKey.equals(getOutputScript(pubKey))) {
throw new ProtocolException("Provided P2TR scriptPubKey does not match constructed pubkey script");
}
return new Script(new byte[0]);
}
@Override
public TransactionInput addSpendingInput(Transaction transaction, TransactionOutput prevOutput, ECKey pubKey, TransactionSignature signature) {
Script scriptSig = getScriptSig(prevOutput.getScript(), pubKey, signature);
TransactionWitness witness = new TransactionWitness(transaction, signature);
return transaction.addInput(prevOutput.getHash(), prevOutput.getIndex(), scriptSig, witness);
}
@Override
public Script getMultisigScriptSig(Script scriptPubKey, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
throw new UnsupportedOperationException("Constructing Taproot inputs is not yet supported");
}
@Override
public TransactionInput addMultisigSpendingInput(Transaction transaction, TransactionOutput prevOutput, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
throw new UnsupportedOperationException("Constructing Taproot inputs is not yet supported");
}
@Override
public TransactionSignature.Type getSignatureType() {
return TransactionSignature.Type.SCHNORR;
};
@Override
public List<PolicyType> getAllowedPolicyTypes() {
return List.of(SINGLE);
}
}; };
private final String name; private final String name;
private final String description;
private final String defaultDerivationPath; private final String defaultDerivationPath;
ScriptType(String name, String defaultDerivationPath) { ScriptType(String name, String description, String defaultDerivationPath) {
this.name = name; this.name = name;
this.description = description;
this.defaultDerivationPath = defaultDerivationPath; this.defaultDerivationPath = defaultDerivationPath;
} }
@ -988,6 +1152,10 @@ public enum ScriptType {
return name; return name;
} }
public String getDescription() {
return description;
}
public String getDefaultDerivationPath() { public String getDefaultDerivationPath() {
return Network.get() != Network.MAINNET ? defaultDerivationPath.replace("/17'/0'", "/1'/0'") : defaultDerivationPath; return Network.get() != Network.MAINNET ? defaultDerivationPath.replace("/17'/0'", "/1'/0'") : defaultDerivationPath;
} }
@ -1027,6 +1195,10 @@ public enum ScriptType {
return getAllowedPolicyTypes().contains(policyType); return getAllowedPolicyTypes().contains(policyType);
} }
public ECKey getOutputKey(ECKey derivedKey) {
return derivedKey;
}
public abstract Address getAddress(byte[] bytes); public abstract Address getAddress(byte[] bytes);
public abstract Address getAddress(ECKey key); public abstract Address getAddress(ECKey key);
@ -1081,18 +1253,24 @@ public enum ScriptType {
public abstract TransactionInput addMultisigSpendingInput(Transaction transaction, TransactionOutput prevOutput, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures); public abstract TransactionInput addMultisigSpendingInput(Transaction transaction, TransactionOutput prevOutput, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures);
public abstract TransactionSignature.Type getSignatureType();
public static final ScriptType[] SINGLE_KEY_TYPES = {P2PK, P2TR};
public static final ScriptType[] SINGLE_HASH_TYPES = {P2PKH, P2SH, P2SH_P2WPKH, P2SH_P2WSH, P2WPKH, P2WSH}; public static final ScriptType[] SINGLE_HASH_TYPES = {P2PKH, P2SH, P2SH_P2WPKH, P2SH_P2WSH, P2WPKH, P2WSH};
public static final ScriptType[] ADDRESSABLE_TYPES = {P2PKH, P2SH, P2SH_P2WPKH, P2SH_P2WSH, P2WPKH, P2WSH, P2TR};
public static final ScriptType[] NON_WITNESS_TYPES = {P2PK, P2PKH, P2SH}; public static final ScriptType[] NON_WITNESS_TYPES = {P2PK, P2PKH, P2SH};
public static final ScriptType[] WITNESS_TYPES = {P2SH_P2WPKH, P2SH_P2WSH, P2WPKH, P2WSH}; public static final ScriptType[] WITNESS_TYPES = {P2SH_P2WPKH, P2SH_P2WSH, P2WPKH, P2WSH, P2TR};
public static List<ScriptType> getScriptTypesForPolicyType(PolicyType policyType) { public static List<ScriptType> getScriptTypesForPolicyType(PolicyType policyType) {
return Arrays.stream(values()).filter(scriptType -> scriptType.isAllowed(policyType)).collect(Collectors.toList()); return Arrays.stream(values()).filter(scriptType -> scriptType.isAllowed(policyType)).collect(Collectors.toList());
} }
public static List<ScriptType> getAddressableScriptTypes(PolicyType policyType) { public static List<ScriptType> getAddressableScriptTypes(PolicyType policyType) {
return Arrays.stream(values()).filter(scriptType -> scriptType.isAllowed(policyType) && Arrays.asList(SINGLE_HASH_TYPES).contains(scriptType)).collect(Collectors.toList()); return Arrays.stream(ADDRESSABLE_TYPES).filter(scriptType -> scriptType.isAllowed(policyType)).collect(Collectors.toList());
} }
public static ScriptType getType(Script script) { public static ScriptType getType(Script script) {
@ -1110,7 +1288,7 @@ public enum ScriptType {
scriptTypes.sort((o1, o2) -> o2.getDescriptor().length() - o1.getDescriptor().length()); scriptTypes.sort((o1, o2) -> o2.getDescriptor().length() - o1.getDescriptor().length());
for(ScriptType scriptType : scriptTypes) { for(ScriptType scriptType : scriptTypes) {
if(descriptor.toLowerCase().startsWith(scriptType.getDescriptor())) { if(descriptor.toLowerCase(Locale.ROOT).startsWith(scriptType.getDescriptor())) {
return scriptType; return scriptType;
} }
} }
@ -1160,6 +1338,9 @@ public enum ScriptType {
return (32 + 4 + 1 + 13 + (107 / WITNESS_SCALE_FACTOR) + 4); return (32 + 4 + 1 + 13 + (107 / WITNESS_SCALE_FACTOR) + 4);
} else if(P2SH_P2WSH.equals(this)) { } else if(P2SH_P2WSH.equals(this)) {
return (32 + 4 + 1 + 35 + (107 / WITNESS_SCALE_FACTOR) + 4); return (32 + 4 + 1 + 35 + (107 / WITNESS_SCALE_FACTOR) + 4);
} else if(P2TR.equals(this)) {
//Assume a default keypath spend
return (32 + 4 + 1 + (66 / WITNESS_SCALE_FACTOR) + 4);
} else if(Arrays.asList(WITNESS_TYPES).contains(this)) { } else if(Arrays.asList(WITNESS_TYPES).contains(this)) {
//Return length of spending input with 75% discount to script size //Return length of spending input with 75% discount to script size
return (32 + 4 + 1 + (107 / WITNESS_SCALE_FACTOR) + 4); return (32 + 4 + 1 + (107 / WITNESS_SCALE_FACTOR) + 4);

View file

@ -1,22 +1,27 @@
package com.sparrowwallet.drongo.protocol; package com.sparrowwallet.drongo.protocol;
import java.util.List;
/** /**
* These constants are a part of a scriptSig signature on the inputs. They define the details of how a * These constants are a part of a scriptSig signature on the inputs. They define the details of how a
* transaction can be redeemed, specifically, they control how the hash of the transaction is calculated. * transaction can be redeemed, specifically, they control how the hash of the transaction is calculated.
*/ */
public enum SigHash { public enum SigHash {
ALL("All (Recommended)", (byte)1), ALL("All", (byte)1),
NONE("None", (byte)2), NONE("None", (byte)2),
SINGLE("Single", (byte)3), SINGLE("Single", (byte)3),
ANYONECANPAY("Anyone Can Pay", (byte)0x80), // Caution: Using this type in isolation is non-standard. Treated similar to ANYONECANPAY_ALL. ANYONECANPAY("Anyone Can Pay", (byte)0x80), // Caution: Using this type in isolation is non-standard. Treated similar to ANYONECANPAY_ALL.
ANYONECANPAY_ALL("All + Anyone Can Pay", (byte)0x81), ANYONECANPAY_ALL("All + Anyone Can Pay", (byte)0x81),
ANYONECANPAY_NONE("None + Anyone Can Pay", (byte)0x82), ANYONECANPAY_NONE("None + Anyone Can Pay", (byte)0x82),
ANYONECANPAY_SINGLE("Single + Anyone Can Pay", (byte)0x83), ANYONECANPAY_SINGLE("Single + Anyone Can Pay", (byte)0x83),
UNSET("Unset", (byte)0); // Caution: Using this type in isolation is non-standard. Treated similar to ALL. DEFAULT("Default", (byte)0);
private final String name; private final String name;
public final byte value; public final byte value;
public static final List<SigHash> LEGACY_SIGNING_TYPES = List.of(ALL, NONE, SINGLE, ANYONECANPAY_ALL, ANYONECANPAY_NONE, ANYONECANPAY_SINGLE);
public static final List<SigHash> TAPROOT_SIGNING_TYPES = List.of(DEFAULT, ALL, NONE, SINGLE, ANYONECANPAY_ALL, ANYONECANPAY_NONE, ANYONECANPAY_SINGLE);
private SigHash(final String name, final byte value) { private SigHash(final String name, final byte value) {
this.name = name; this.name = name;
this.value = value; this.value = value;

View file

@ -22,7 +22,8 @@ public class Transaction extends ChildMessage {
public static final long SATOSHIS_PER_BITCOIN = 100 * 1000 * 1000L; public static final long SATOSHIS_PER_BITCOIN = 100 * 1000 * 1000L;
public static final long MAX_BLOCK_LOCKTIME = 500000000L; public static final long MAX_BLOCK_LOCKTIME = 500000000L;
public static final int WITNESS_SCALE_FACTOR = 4; public static final int WITNESS_SCALE_FACTOR = 4;
public static final int DEFAULT_SEGWIT_VERSION = 1; public static final int DEFAULT_SEGWIT_FLAG = 1;
public static final int COINBASE_MATURITY_THRESHOLD = 100;
//Min feerate for defining dust, defined in sats/vByte //Min feerate for defining dust, defined in sats/vByte
//From: https://github.com/bitcoin/bitcoin/blob/0.19/src/policy/policy.h#L50 //From: https://github.com/bitcoin/bitcoin/blob/0.19/src/policy/policy.h#L50
@ -31,10 +32,12 @@ public class Transaction extends ChildMessage {
//Default min feerate, defined in sats/vByte //Default min feerate, defined in sats/vByte
public static final double DEFAULT_MIN_RELAY_FEE = 1d; public static final double DEFAULT_MIN_RELAY_FEE = 1d;
public static final byte LEAF_VERSION_TAPSCRIPT = (byte)0xc0;
private long version; private long version;
private long locktime; private long locktime;
private boolean segwit; private boolean segwit;
private int segwitVersion; private int segwitFlag;
private Sha256Hash cachedTxId; private Sha256Hash cachedTxId;
private Sha256Hash cachedWTxId; private Sha256Hash cachedWTxId;
@ -89,8 +92,6 @@ public class Transaction extends ChildMessage {
} }
public boolean isReplaceByFee() { public boolean isReplaceByFee() {
if(locktime == 0) return false;
for(TransactionInput input : inputs) { for(TransactionInput input : inputs) {
if(input.isReplaceByFeeEnabled()) { if(input.isReplaceByFeeEnabled()) {
return true; return true;
@ -136,17 +137,17 @@ public class Transaction extends ChildMessage {
return segwit; return segwit;
} }
public int getSegwitVersion() { public int getSegwitFlag() {
return segwitVersion; return segwitFlag;
} }
public void setSegwitVersion(int segwitVersion) { public void setSegwitFlag(int segwitFlag) {
if(!segwit) { if(!segwit) {
adjustLength(2); adjustLength(2);
this.segwit = true; this.segwit = true;
} }
this.segwitVersion = segwitVersion; this.segwitFlag = segwitFlag;
} }
public void clearSegwit() { public void clearSegwit() {
@ -210,7 +211,7 @@ public class Transaction extends ChildMessage {
// marker, flag // marker, flag
if(useWitnessFormat) { if(useWitnessFormat) {
stream.write(0); stream.write(0);
stream.write(segwitVersion); stream.write(segwitFlag);
} }
// txin_count, txins // txin_count, txins
@ -255,7 +256,7 @@ public class Transaction extends ChildMessage {
// marker, flag // marker, flag
if (segwit) { if (segwit) {
byte[] segwitHeader = readBytes(2); byte[] segwitHeader = readBytes(2);
segwitVersion = segwitHeader[1]; segwitFlag = segwitHeader[1];
} }
// txin_count, txins // txin_count, txins
parseInputs(); parseInputs();
@ -305,8 +306,8 @@ public class Transaction extends ChildMessage {
return length; return length;
} }
public int getVirtualSize() { public double getVirtualSize() {
return (int)Math.ceil((double)getWeightUnits() / (double)WITNESS_SCALE_FACTOR); return (double)getWeightUnits() / (double)WITNESS_SCALE_FACTOR;
} }
public int getWeightUnits() { public int getWeightUnits() {
@ -354,7 +355,7 @@ public class Transaction extends ChildMessage {
public TransactionInput addInput(Sha256Hash spendTxHash, long outputIndex, Script script, TransactionWitness witness) { public TransactionInput addInput(Sha256Hash spendTxHash, long outputIndex, Script script, TransactionWitness witness) {
if(!isSegwit()) { if(!isSegwit()) {
setSegwitVersion(DEFAULT_SEGWIT_VERSION); setSegwitFlag(DEFAULT_SEGWIT_FLAG);
} }
return addInput(new TransactionInput(this, new TransactionOutPoint(spendTxHash, outputIndex), script.getProgram(), witness)); return addInput(new TransactionInput(this, new TransactionOutPoint(spendTxHash, outputIndex), script.getProgram(), witness));
@ -439,6 +440,9 @@ public class Transaction extends ChildMessage {
public static boolean isTransaction(byte[] bytes) { public static boolean isTransaction(byte[] bytes) {
//Incomplete quick test //Incomplete quick test
if(bytes.length == 0) {
return false;
}
long version = Utils.readUint32(bytes, 0); long version = Utils.readUint32(bytes, 0);
return version > 0 && version < 5; return version > 0 && version < 5;
} }
@ -608,4 +612,123 @@ public class Transaction extends ChildMessage {
return Sha256Hash.of(bos.toByteArray()); return Sha256Hash.of(bos.toByteArray());
} }
/**
* <p>Calculates a signature hash, that is, a hash of a simplified form of the transaction. How exactly the transaction
* is simplified is specified by the type and anyoneCanPay parameters.</p>
*
* (See BIP341: https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki)</p>
*
* @param spentUtxos the ordered list of spent UTXOs corresponding to the inputs of this transaction
* @param inputIndex input the signature is being calculated for. Tx signatures are always relative to an input.
* @param scriptPath whether we are signing for the keypath or the scriptpath
* @param script if signing for the scriptpath, the script to sign
* @param sigHash should usually be SigHash.ALL
* @param annex annex data
*/
public synchronized Sha256Hash hashForTaprootSignature(List<TransactionOutput> spentUtxos, int inputIndex, boolean scriptPath, Script script, SigHash sigHash, byte[] annex) {
return hashForTaprootSignature(spentUtxos, inputIndex, scriptPath, script, sigHash.value, annex);
}
public synchronized Sha256Hash hashForTaprootSignature(List<TransactionOutput> spentUtxos, int inputIndex, boolean scriptPath, Script script, byte sigHashType, byte[] annex) {
if(spentUtxos.size() != getInputs().size()) {
throw new IllegalArgumentException("Provided spent UTXOs length does not equal the number of transaction inputs");
}
if(inputIndex >= getInputs().size()) {
throw new IllegalArgumentException("Input index is greater than the number of transaction inputs");
}
ByteArrayOutputStream bos = new UnsafeByteArrayOutputStream(length == UNKNOWN_LENGTH ? 256 : length + 4);
try {
byte outType = sigHashType == 0x00 ? SigHash.ALL.value : (byte)(sigHashType & 0x03);
boolean anyoneCanPay = (sigHashType & SigHash.ANYONECANPAY.value) == SigHash.ANYONECANPAY.value;
bos.write(0x00);
bos.write(sigHashType);
uint32ToByteStreamLE(this.version, bos);
uint32ToByteStreamLE(this.locktime, bos);
if(!anyoneCanPay) {
ByteArrayOutputStream outpoints = new ByteArrayOutputStream();
ByteArrayOutputStream outputValues = new ByteArrayOutputStream();
ByteArrayOutputStream outputScriptPubKeys = new ByteArrayOutputStream();
ByteArrayOutputStream inputSequences = new ByteArrayOutputStream();
for(int i = 0; i < getInputs().size(); i++) {
TransactionInput input = getInputs().get(i);
input.getOutpoint().bitcoinSerializeToStream(outpoints);
Utils.uint64ToByteStreamLE(BigInteger.valueOf(spentUtxos.get(i).getValue()), outputValues);
byteArraySerialize(spentUtxos.get(i).getScriptBytes(), outputScriptPubKeys);
Utils.uint32ToByteStreamLE(input.getSequenceNumber(), inputSequences);
}
bos.write(Sha256Hash.hash(outpoints.toByteArray()));
bos.write(Sha256Hash.hash(outputValues.toByteArray()));
bos.write(Sha256Hash.hash(outputScriptPubKeys.toByteArray()));
bos.write(Sha256Hash.hash(inputSequences.toByteArray()));
}
if(outType == SigHash.ALL.value) {
ByteArrayOutputStream outputs = new ByteArrayOutputStream();
for(TransactionOutput output : getOutputs()) {
output.bitcoinSerializeToStream(outputs);
}
bos.write(Sha256Hash.hash(outputs.toByteArray()));
}
byte spendType = 0x00;
if(annex != null) {
spendType |= 0x01;
}
if(scriptPath) {
spendType |= 0x02;
}
bos.write(spendType);
if(anyoneCanPay) {
getInputs().get(inputIndex).getOutpoint().bitcoinSerializeToStream(bos);
Utils.uint32ToByteStreamLE(spentUtxos.get(inputIndex).getValue(), bos);
byteArraySerialize(spentUtxos.get(inputIndex).getScriptBytes(), bos);
Utils.uint32ToByteStreamLE(getInputs().get(inputIndex).getSequenceNumber(), bos);
} else {
Utils.uint32ToByteStreamLE(inputIndex, bos);
}
if((spendType & 0x01) != 0) {
ByteArrayOutputStream annexStream = new ByteArrayOutputStream();
byteArraySerialize(annex, annexStream);
bos.write(Sha256Hash.hash(annexStream.toByteArray()));
}
if(outType == SigHash.SINGLE.value) {
if(inputIndex < getOutputs().size()) {
bos.write(Sha256Hash.hash(getOutputs().get(inputIndex).bitcoinSerialize()));
} else {
bos.write(Sha256Hash.ZERO_HASH.getBytes());
}
}
if(scriptPath) {
ByteArrayOutputStream leafStream = new ByteArrayOutputStream();
leafStream.write(LEAF_VERSION_TAPSCRIPT);
byteArraySerialize(script.getProgram(), leafStream);
bos.write(Utils.taggedHash("TapLeaf", leafStream.toByteArray()));
bos.write(0x00);
Utils.uint32ToByteStreamLE(-1, bos);
}
byte[] msgBytes = bos.toByteArray();
long requiredLength = 175 - (anyoneCanPay ? 49 : 0) - (outType != SigHash.ALL.value && outType != SigHash.SINGLE.value ? 32 : 0) + (annex != null ? 32 : 0) + (scriptPath ? 37 : 0);
if(msgBytes.length != requiredLength) {
throw new IllegalStateException("Invalid message length, was " + msgBytes.length + " not " + requiredLength);
}
return Sha256Hash.wrap(Utils.taggedHash("TapSighash", msgBytes));
} catch (IOException e) {
throw new RuntimeException(e); // Cannot happen.
}
}
private void byteArraySerialize(byte[] bytes, OutputStream outputStream) throws IOException {
outputStream.write(new VarInt(bytes.length).encode());
outputStream.write(bytes);
}
} }

View file

@ -3,6 +3,7 @@ package com.sparrowwallet.drongo.protocol;
import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.address.Address; import com.sparrowwallet.drongo.address.Address;
import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.Objects; import java.util.Objects;
@ -51,6 +52,18 @@ public class TransactionOutPoint extends ChildMessage {
this.addresses = addresses; this.addresses = addresses;
} }
public byte[] bitcoinSerialize() {
try {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
bitcoinSerializeToStream(outputStream);
return outputStream.toByteArray();
} catch (IOException e) {
//can't happen
}
return null;
}
@Override @Override
protected void bitcoinSerializeToStream(OutputStream stream) throws IOException { protected void bitcoinSerializeToStream(OutputStream stream) throws IOException {
stream.write(hash.getReversedBytes()); stream.write(hash.getReversedBytes());

View file

@ -1,13 +1,19 @@
package com.sparrowwallet.drongo.protocol; package com.sparrowwallet.drongo.protocol;
import com.sparrowwallet.drongo.crypto.ECDSASignature;
import com.sparrowwallet.drongo.crypto.ECKey; import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.crypto.SchnorrSignature;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.math.BigInteger; import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.util.Objects; import java.util.Objects;
public class TransactionSignature extends ECKey.ECDSASignature { public class TransactionSignature {
private final ECDSASignature ecdsaSignature;
private final SchnorrSignature schnorrSignature;
/** /**
* A byte that controls which parts of a transaction are signed. This is exposed because signatures * A byte that controls which parts of a transaction are signed. This is exposed because signatures
* parsed off the wire may have sighash flags that aren't "normal" serializations of the enum values. * parsed off the wire may have sighash flags that aren't "normal" serializations of the enum values.
@ -16,21 +22,26 @@ public class TransactionSignature extends ECKey.ECDSASignature {
*/ */
public final byte sighashFlags; public final byte sighashFlags;
/** Constructs a signature with the given components and SIGHASH_ALL. */ /** Constructs a signature with the given components of the given type and SIGHASH_ALL. */
public TransactionSignature(BigInteger r, BigInteger s) { public TransactionSignature(BigInteger r, BigInteger s, Type type) {
this(r, s, SigHash.ALL.value); this(r, s, type, type == Type.ECDSA ? SigHash.ALL.value : SigHash.DEFAULT.value);
}
/** Constructs a signature with the given components and raw sighash flag bytes (needed for rule compatibility). */
public TransactionSignature(BigInteger r, BigInteger s, byte sighashFlags) {
super(r, s);
this.sighashFlags = sighashFlags;
} }
/** Constructs a transaction signature based on the ECDSA signature. */ /** Constructs a transaction signature based on the ECDSA signature. */
public TransactionSignature(ECKey.ECDSASignature signature, SigHash sigHash) { public TransactionSignature(ECDSASignature signature, SigHash sigHash) {
super(signature.r, signature.s); this(signature.r, signature.s, Type.ECDSA, sigHash.value);
sighashFlags = sigHash.value; }
/** Constructs a transaction signature based on the Schnorr signature. */
public TransactionSignature(SchnorrSignature signature, SigHash sigHash) {
this(signature.r, signature.s, Type.SCHNORR, sigHash.value);
}
/** Constructs a signature with the given components, type and raw sighash flag bytes (needed for rule compatibility). */
public TransactionSignature(BigInteger r, BigInteger s, Type type, byte sighashFlags) {
ecdsaSignature = type == Type.ECDSA ? new ECDSASignature(r, s) : null;
schnorrSignature = type == Type.SCHNORR ? new SchnorrSignature(r, s) : null;
this.sighashFlags = sighashFlags;
} }
/** /**
@ -39,68 +50,20 @@ public class TransactionSignature extends ECKey.ECDSASignature {
* right size (e.g. for fee calculations) but don't have the requisite signing key yet and will fill out the * right size (e.g. for fee calculations) but don't have the requisite signing key yet and will fill out the
* real signature later. * real signature later.
*/ */
public static TransactionSignature dummy() { public static TransactionSignature dummy(Type type) {
BigInteger val = ECKey.HALF_CURVE_ORDER; BigInteger val = ECKey.HALF_CURVE_ORDER;
return new TransactionSignature(val, val); return new TransactionSignature(val, val, type);
}
/**
* Returns true if the given signature is has canonical encoding, and will thus be accepted as standard by
* Bitcoin Core. DER and the SIGHASH encoding allow for quite some flexibility in how the same structures
* are encoded, and this can open up novel attacks in which a man in the middle takes a transaction and then
* changes its signature such that the transaction hash is different but it's still valid. This can confuse wallets
* and generally violates people's mental model of how Bitcoin should work, thus, non-canonical signatures are now
* not relayed by default.
*/
public static boolean isEncodingCanonical(byte[] signature) {
// See Bitcoin Core's IsCanonicalSignature, https://bitcointalk.org/index.php?topic=8392.msg127623#msg127623
// A canonical signature exists of: <30> <total len> <02> <len R> <R> <02> <len S> <S> <hashtype>
// Where R and S are not negative (their first byte has its highest bit not set), and not
// excessively padded (do not start with a 0 byte, unless an otherwise negative number follows,
// in which case a single 0 byte is necessary and even required).
// Empty signatures, while not strictly DER encoded, are allowed.
if (signature.length == 0)
return true;
if (signature.length < 9 || signature.length > 73)
return false;
int hashType = (signature[signature.length-1] & 0xff) & ~SigHash.ANYONECANPAY.value; // mask the byte to prevent sign-extension hurting us
if (hashType < SigHash.ALL.value || hashType > SigHash.SINGLE.value)
return false;
// "wrong type" "wrong length marker"
if ((signature[0] & 0xff) != 0x30 || (signature[1] & 0xff) != signature.length-3)
return false;
int lenR = signature[3] & 0xff;
if (5 + lenR >= signature.length || lenR == 0)
return false;
int lenS = signature[5+lenR] & 0xff;
if (lenR + lenS + 7 != signature.length || lenS == 0)
return false;
// R value type mismatch R value negative
if (signature[4-2] != 0x02 || (signature[4] & 0x80) == 0x80)
return false;
if (lenR > 1 && signature[4] == 0x00 && (signature[4+1] & 0x80) != 0x80)
return false; // R value excessively padded
// S value type mismatch S value negative
if (signature[6 + lenR - 2] != 0x02 || (signature[6 + lenR] & 0x80) == 0x80)
return false;
if (lenS > 1 && signature[6 + lenR] == 0x00 && (signature[6 + lenR + 1] & 0x80) != 0x80)
return false; // S value excessively padded
return true;
} }
public boolean anyoneCanPay() { public boolean anyoneCanPay() {
return (sighashFlags & SigHash.ANYONECANPAY.value) != 0; return (sighashFlags & SigHash.ANYONECANPAY.value) != 0;
} }
public SigHash getSigHash() { private SigHash getSigHash() {
if(sighashFlags == SigHash.DEFAULT.byteValue()) {
return SigHash.DEFAULT;
}
boolean anyoneCanPay = anyoneCanPay(); boolean anyoneCanPay = anyoneCanPay();
final int mode = sighashFlags & 0x1f; final int mode = sighashFlags & 0x1f;
if (mode == SigHash.NONE.value) { if (mode == SigHash.NONE.value) {
@ -118,18 +81,51 @@ public class TransactionSignature extends ECKey.ECDSASignature {
* components into a structure, and then we append a byte to the end for the sighash flags. * components into a structure, and then we append a byte to the end for the sighash flags.
*/ */
public byte[] encodeToBitcoin() { public byte[] encodeToBitcoin() {
try { if(ecdsaSignature != null) {
ByteArrayOutputStream bos = derByteStream(); try {
bos.write(sighashFlags); ByteArrayOutputStream bos = ecdsaSignature.derByteStream();
return bos.toByteArray(); bos.write(sighashFlags);
} catch (IOException e) { return bos.toByteArray();
throw new RuntimeException(e); // Cannot happen. } catch (IOException e) {
throw new RuntimeException(e); // Cannot happen.
}
} else if(schnorrSignature != null) {
SigHash sigHash = getSigHash();
ByteBuffer buffer = ByteBuffer.allocate(sigHash == SigHash.DEFAULT ? 64 : 65);
buffer.put(schnorrSignature.encode());
if(sigHash != SigHash.DEFAULT) {
buffer.put(sighashFlags);
}
return buffer.array();
} }
throw new IllegalStateException("TransactionSignature has no values");
} }
@Override public static TransactionSignature decodeFromBitcoin(byte[] bytes, boolean requireCanonicalEncoding) throws SignatureDecodeException {
public ECKey.ECDSASignature toCanonicalised() { if(bytes.length == 64) {
return new TransactionSignature(super.toCanonicalised(), getSigHash()); return decodeFromBitcoin(Type.SCHNORR, bytes, requireCanonicalEncoding);
}
return decodeFromBitcoin(Type.ECDSA, bytes, requireCanonicalEncoding);
}
public static TransactionSignature decodeFromBitcoin(Type type, byte[] bytes, boolean requireCanonicalEncoding) throws SignatureDecodeException {
if(type == Type.ECDSA) {
return ECDSASignature.decodeFromBitcoin(bytes, requireCanonicalEncoding, false);
} else if(type == Type.SCHNORR) {
return SchnorrSignature.decodeFromBitcoin(bytes);
}
throw new IllegalStateException("Unknown TransactionSignature type " + type);
}
public boolean verify(byte[] data, ECKey pubKey) {
if(ecdsaSignature != null) {
return ecdsaSignature.verify(data, pubKey.getPubKey());
} else {
return schnorrSignature.verify(data, pubKey.getPubKeyXCoord());
}
} }
@Override @Override
@ -140,39 +136,16 @@ public class TransactionSignature extends ECKey.ECDSASignature {
if(o == null || getClass() != o.getClass()) { if(o == null || getClass() != o.getClass()) {
return false; return false;
} }
if(!super.equals(o)) { TransactionSignature that = (TransactionSignature) o;
return false; return sighashFlags == that.sighashFlags && Objects.equals(ecdsaSignature, that.ecdsaSignature) && Objects.equals(schnorrSignature, that.schnorrSignature);
}
TransactionSignature signature = (TransactionSignature) o;
return sighashFlags == signature.sighashFlags;
} }
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hash(super.hashCode(), sighashFlags); return Objects.hash(ecdsaSignature, schnorrSignature, sighashFlags);
} }
/** public enum Type {
* Returns a decoded signature. ECDSA, SCHNORR
*
* @param requireCanonicalEncoding if the encoding of the signature must
* be canonical.
* @param requireCanonicalSValue if the S-value must be canonical (below half
* the order of the curve).
* @throws SignatureDecodeException if the signature is unparseable in some way.
* @throws VerificationException if the signature is invalid.
*/
public static TransactionSignature decodeFromBitcoin(byte[] bytes, boolean requireCanonicalEncoding,
boolean requireCanonicalSValue) throws SignatureDecodeException, VerificationException {
// Bitcoin encoding is DER signature + sighash byte.
if (requireCanonicalEncoding && !isEncodingCanonical(bytes))
throw new VerificationException.NoncanonicalSignature();
ECKey.ECDSASignature sig = ECKey.ECDSASignature.decodeFromDER(bytes);
if (requireCanonicalSValue && !sig.isCanonical())
throw new VerificationException("S-value is not canonical.");
// In Bitcoin, any value of the final byte is valid, but not necessarily canonical. See javadocs for
// isEncodingCanonical to learn more about this. So we must store the exact byte found.
return new TransactionSignature(sig.r, sig.s, bytes[bytes.length - 1]);
} }
} }

View file

@ -14,6 +14,12 @@ import java.util.List;
public class TransactionWitness extends ChildMessage { public class TransactionWitness extends ChildMessage {
private List<byte[]> pushes; private List<byte[]> pushes;
public TransactionWitness(Transaction transaction, TransactionSignature signature) {
setParent(transaction);
this.pushes = new ArrayList<>();
pushes.add(signature.encodeToBitcoin());
}
public TransactionWitness(Transaction transaction, ECKey pubKey, TransactionSignature signature) { public TransactionWitness(Transaction transaction, ECKey pubKey, TransactionSignature signature) {
setParent(transaction); setParent(transaction);
this.pushes = new ArrayList<>(); this.pushes = new ArrayList<>();

View file

@ -16,6 +16,9 @@ import java.nio.ByteBuffer;
import java.util.*; import java.util.*;
import static com.sparrowwallet.drongo.psbt.PSBTEntry.*; import static com.sparrowwallet.drongo.psbt.PSBTEntry.*;
import static com.sparrowwallet.drongo.psbt.PSBTInput.*;
import static com.sparrowwallet.drongo.psbt.PSBTOutput.*;
import static com.sparrowwallet.drongo.wallet.Wallet.addDummySpendingInput;
public class PSBT { public class PSBT {
public static final byte PSBT_GLOBAL_UNSIGNED_TX = 0x00; public static final byte PSBT_GLOBAL_UNSIGNED_TX = 0x00;
@ -50,7 +53,7 @@ public class PSBT {
this.transaction = transaction; this.transaction = transaction;
for(int i = 0; i < transaction.getInputs().size(); i++) { for(int i = 0; i < transaction.getInputs().size(); i++) {
psbtInputs.add(new PSBTInput(transaction, i)); psbtInputs.add(new PSBTInput(this, transaction, i));
} }
for(int i = 0; i < transaction.getOutputs().size(); i++) { for(int i = 0; i < transaction.getOutputs().size(); i++) {
@ -87,12 +90,16 @@ public class PSBT {
this.version = version; this.version = version;
} }
boolean alwaysIncludeWitnessUtxo = wallet.getKeystores().stream().anyMatch(keystore -> keystore.getWalletModel().alwaysIncludeNonWitnessUtxo());
int inputIndex = 0; int inputIndex = 0;
for(Iterator<Map.Entry<BlockTransactionHashIndex, WalletNode>> iter = walletTransaction.getSelectedUtxos().entrySet().iterator(); iter.hasNext(); inputIndex++) { for(Iterator<Map.Entry<BlockTransactionHashIndex, WalletNode>> iter = walletTransaction.getSelectedUtxos().entrySet().iterator(); iter.hasNext(); inputIndex++) {
Map.Entry<BlockTransactionHashIndex, WalletNode> utxoEntry = iter.next(); Map.Entry<BlockTransactionHashIndex, WalletNode> utxoEntry = iter.next();
Transaction utxo = wallet.getTransactions().get(utxoEntry.getKey().getHash()).getTransaction();
WalletNode walletNode = utxoEntry.getValue();
Wallet signingWallet = walletNode.getWallet();
boolean alwaysIncludeWitnessUtxo = signingWallet.getKeystores().stream().anyMatch(keystore -> keystore.getWalletModel().alwaysIncludeNonWitnessUtxo());
Transaction utxo = signingWallet.getTransactions().get(utxoEntry.getKey().getHash()).getTransaction();
int utxoIndex = (int)utxoEntry.getKey().getIndex(); int utxoIndex = (int)utxoEntry.getKey().getIndex();
TransactionOutput utxoOutput = utxo.getOutputs().get(utxoIndex); TransactionOutput utxoOutput = utxo.getOutputs().get(utxoIndex);
@ -109,12 +116,17 @@ public class PSBT {
} }
Map<ECKey, KeyDerivation> derivedPublicKeys = new LinkedHashMap<>(); Map<ECKey, KeyDerivation> derivedPublicKeys = new LinkedHashMap<>();
for(Keystore keystore : wallet.getKeystores()) { ECKey tapInternalKey = null;
WalletNode walletNode = utxoEntry.getValue(); for(Keystore keystore : signingWallet.getKeystores()) {
derivedPublicKeys.put(keystore.getPubKey(walletNode), keystore.getKeyDerivation().extend(walletNode.getDerivation())); derivedPublicKeys.put(signingWallet.getScriptType().getOutputKey(keystore.getPubKey(walletNode)), keystore.getKeyDerivation().extend(walletNode.getDerivation()));
//TODO: Implement Musig for multisig wallets
if(signingWallet.getScriptType() == ScriptType.P2TR) {
tapInternalKey = keystore.getPubKey(walletNode);
}
} }
PSBTInput psbtInput = new PSBTInput(wallet.getScriptType(), transaction, inputIndex, utxo, utxoIndex, redeemScript, witnessScript, derivedPublicKeys, Collections.emptyMap(), alwaysIncludeWitnessUtxo); PSBTInput psbtInput = new PSBTInput(this, signingWallet.getScriptType(), transaction, inputIndex, utxo, utxoIndex, redeemScript, witnessScript, derivedPublicKeys, Collections.emptyMap(), tapInternalKey, alwaysIncludeWitnessUtxo);
psbtInputs.add(psbtInput); psbtInputs.add(psbtInput);
} }
@ -122,28 +134,29 @@ public class PSBT {
for(TransactionOutput txOutput : transaction.getOutputs()) { for(TransactionOutput txOutput : transaction.getOutputs()) {
try { try {
Address address = txOutput.getScript().getToAddresses()[0]; Address address = txOutput.getScript().getToAddresses()[0];
if(walletTransaction.getPayments().stream().anyMatch(payment -> payment.getAddress().equals(address))) { if(walletTransaction.getAddressNodeMap().containsKey(address)) {
outputNodes.add(wallet.getWalletAddresses().getOrDefault(address, null)); outputNodes.add(walletTransaction.getAddressNodeMap().get(address));
} else if(address.equals(wallet.getAddress(walletTransaction.getChangeNode()))) { } else if(walletTransaction.getChangeMap().keySet().stream().anyMatch(changeNode -> changeNode.getAddress().equals(address))) {
outputNodes.add(walletTransaction.getChangeNode()); outputNodes.add(walletTransaction.getChangeMap().keySet().stream().filter(changeNode -> changeNode.getAddress().equals(address)).findFirst().orElse(null));
} }
} catch(NonStandardScriptException e) { } catch(NonStandardScriptException e) {
//Should never happen //Ignore, likely OP_RETURN output
throw new IllegalArgumentException(e); outputNodes.add(null);
} }
} }
for(int outputIndex = 0; outputIndex < outputNodes.size(); outputIndex++) { for(int outputIndex = 0; outputIndex < outputNodes.size(); outputIndex++) {
WalletNode outputNode = outputNodes.get(outputIndex); WalletNode outputNode = outputNodes.get(outputIndex);
if(outputNode == null) { if(outputNode == null) {
PSBTOutput externalRecipientOutput = new PSBTOutput(null, null, Collections.emptyMap(), Collections.emptyMap()); PSBTOutput externalRecipientOutput = new PSBTOutput(null, null, null, Collections.emptyMap(), Collections.emptyMap(), null);
psbtOutputs.add(externalRecipientOutput); psbtOutputs.add(externalRecipientOutput);
} else { } else {
TransactionOutput txOutput = transaction.getOutputs().get(outputIndex); TransactionOutput txOutput = transaction.getOutputs().get(outputIndex);
Wallet recipientWallet = outputNode.getWallet();
//Construct dummy transaction to spend the UTXO created by this wallet's txOutput //Construct dummy transaction to spend the UTXO created by this wallet's txOutput
Transaction transaction = new Transaction(); Transaction transaction = new Transaction();
TransactionInput spendingInput = wallet.addDummySpendingInput(transaction, outputNode, txOutput); TransactionInput spendingInput = addDummySpendingInput(transaction, outputNode, txOutput);
Script redeemScript = null; Script redeemScript = null;
if(ScriptType.P2SH.isScriptType(txOutput.getScript())) { if(ScriptType.P2SH.isScriptType(txOutput.getScript())) {
@ -156,22 +169,32 @@ public class PSBT {
} }
Map<ECKey, KeyDerivation> derivedPublicKeys = new LinkedHashMap<>(); Map<ECKey, KeyDerivation> derivedPublicKeys = new LinkedHashMap<>();
for(Keystore keystore : wallet.getKeystores()) { ECKey tapInternalKey = null;
derivedPublicKeys.put(keystore.getPubKey(outputNode), keystore.getKeyDerivation().extend(outputNode.getDerivation())); for(Keystore keystore : recipientWallet.getKeystores()) {
derivedPublicKeys.put(recipientWallet.getScriptType().getOutputKey(keystore.getPubKey(outputNode)), keystore.getKeyDerivation().extend(outputNode.getDerivation()));
//TODO: Implement Musig for multisig wallets
if(recipientWallet.getScriptType() == ScriptType.P2TR) {
tapInternalKey = keystore.getPubKey(outputNode);
}
} }
PSBTOutput walletOutput = new PSBTOutput(redeemScript, witnessScript, derivedPublicKeys, Collections.emptyMap()); PSBTOutput walletOutput = new PSBTOutput(recipientWallet.getScriptType(), redeemScript, witnessScript, derivedPublicKeys, Collections.emptyMap(), tapInternalKey);
psbtOutputs.add(walletOutput); psbtOutputs.add(walletOutput);
} }
} }
} }
public PSBT(byte[] psbt) throws PSBTParseException { public PSBT(byte[] psbt) throws PSBTParseException {
this.psbtBytes = psbt; this(psbt, true);
parse();
} }
private void parse() throws PSBTParseException { public PSBT(byte[] psbt, boolean verifySignatures) throws PSBTParseException {
this.psbtBytes = psbt;
parse(verifySignatures);
}
private void parse(boolean verifySignatures) throws PSBTParseException {
int seenInputs = 0; int seenInputs = 0;
int seenOutputs = 0; int seenOutputs = 0;
@ -212,7 +235,7 @@ public class PSBT {
seenInputs++; seenInputs++;
if (seenInputs == inputs) { if (seenInputs == inputs) {
currentState = STATE_OUTPUTS; currentState = STATE_OUTPUTS;
parseInputEntries(inputEntryLists); parseInputEntries(inputEntryLists, verifySignatures);
} }
break; break;
case STATE_OUTPUTS: case STATE_OUTPUTS:
@ -245,14 +268,6 @@ public class PSBT {
if(transaction == null) { if(transaction == null) {
throw new PSBTParseException("Missing transaction"); throw new PSBTParseException("Missing transaction");
} }
if(currentState == STATE_INPUTS) {
throw new PSBTParseException("Missing inputs");
}
if(currentState == STATE_OUTPUTS) {
throw new PSBTParseException("Missing outputs");
}
} }
if(log.isDebugEnabled()) { if(log.isDebugEnabled()) {
@ -313,7 +328,7 @@ public class PSBT {
} }
} }
private void parseInputEntries(List<List<PSBTEntry>> inputEntryLists) throws PSBTParseException { private void parseInputEntries(List<List<PSBTEntry>> inputEntryLists, boolean verifySignatures) throws PSBTParseException {
for(List<PSBTEntry> inputEntries : inputEntryLists) { for(List<PSBTEntry> inputEntries : inputEntryLists) {
PSBTEntry duplicate = findDuplicateKey(inputEntries); PSBTEntry duplicate = findDuplicateKey(inputEntries);
if(duplicate != null) { if(duplicate != null) {
@ -321,15 +336,13 @@ public class PSBT {
} }
int inputIndex = this.psbtInputs.size(); int inputIndex = this.psbtInputs.size();
PSBTInput input = new PSBTInput(inputEntries, transaction, inputIndex); PSBTInput input = new PSBTInput(this, inputEntries, transaction, inputIndex);
boolean verified = input.verifySignatures();
if(!verified && input.getPartialSignatures().size() > 0) {
throw new PSBTParseException("Unverifiable partial signatures provided");
}
this.psbtInputs.add(input); this.psbtInputs.add(input);
} }
if(verifySignatures) {
verifySignatures(psbtInputs);
}
} }
private void parseOutputEntries(List<List<PSBTEntry>> outputEntryLists) throws PSBTParseException { private void parseOutputEntries(List<List<PSBTEntry>> outputEntryLists) throws PSBTParseException {
@ -364,7 +377,7 @@ public class PSBT {
if(utxo != null) { if(utxo != null) {
fee += utxo.getValue(); fee += utxo.getValue();
} else { } else {
log.error("Cannot determine fee - not enough information provided on inputs"); log.warn("Cannot determine fee - inputs are missing UTXO data");
return null; return null;
} }
} }
@ -377,9 +390,25 @@ public class PSBT {
return fee; return fee;
} }
public void verifySignatures() throws PSBTSignatureException {
verifySignatures(getPsbtInputs());
}
private void verifySignatures(List<PSBTInput> psbtInputs) throws PSBTSignatureException {
for(PSBTInput input : psbtInputs) {
boolean verified = input.verifySignatures();
if(!verified && input.getPartialSignatures().size() > 0) {
throw new PSBTSignatureException("Unverifiable partial signatures provided");
}
if(!verified && input.isTaproot() && input.getTapKeyPathSignature() != null) {
throw new PSBTSignatureException("Unverifiable taproot keypath signature provided");
}
}
}
public boolean hasSignatures() { public boolean hasSignatures() {
for(PSBTInput psbtInput : getPsbtInputs()) { for(PSBTInput psbtInput : getPsbtInputs()) {
if(!psbtInput.getPartialSignatures().isEmpty() || psbtInput.getFinalScriptSig() != null || psbtInput.getFinalScriptWitness() != null) { if(!psbtInput.getPartialSignatures().isEmpty() || psbtInput.getTapKeyPathSignature() != null || psbtInput.getFinalScriptSig() != null || psbtInput.getFinalScriptWitness() != null) {
return true; return true;
} }
} }
@ -432,6 +461,10 @@ public class PSBT {
} }
public byte[] serialize() { public byte[] serialize() {
return serialize(true);
}
public byte[] serialize(boolean includeXpubs) {
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream();
baos.writeBytes(Utils.hexToBytes(PSBT_MAGIC_HEX)); baos.writeBytes(Utils.hexToBytes(PSBT_MAGIC_HEX));
@ -439,14 +472,19 @@ public class PSBT {
List<PSBTEntry> globalEntries = getGlobalEntries(); List<PSBTEntry> globalEntries = getGlobalEntries();
for(PSBTEntry entry : globalEntries) { for(PSBTEntry entry : globalEntries) {
entry.serializeToStream(baos); if(includeXpubs || (entry.getKeyType() != PSBT_GLOBAL_BIP32_PUBKEY && entry.getKeyType() != PSBT_GLOBAL_PROPRIETARY)) {
entry.serializeToStream(baos);
}
} }
baos.writeBytes(new byte[] {(byte)0x00}); baos.writeBytes(new byte[] {(byte)0x00});
for(PSBTInput psbtInput : getPsbtInputs()) { for(PSBTInput psbtInput : getPsbtInputs()) {
List<PSBTEntry> inputEntries = psbtInput.getInputEntries(); List<PSBTEntry> inputEntries = psbtInput.getInputEntries();
for(PSBTEntry entry : inputEntries) { for(PSBTEntry entry : inputEntries) {
entry.serializeToStream(baos); if(includeXpubs || (entry.getKeyType() != PSBT_IN_BIP32_DERIVATION && entry.getKeyType() != PSBT_IN_PROPRIETARY
&& entry.getKeyType() != PSBT_IN_TAP_INTERNAL_KEY && entry.getKeyType() != PSBT_IN_TAP_BIP32_DERIVATION)) {
entry.serializeToStream(baos);
}
} }
baos.writeBytes(new byte[] {(byte)0x00}); baos.writeBytes(new byte[] {(byte)0x00});
} }
@ -454,7 +492,11 @@ public class PSBT {
for(PSBTOutput psbtOutput : getPsbtOutputs()) { for(PSBTOutput psbtOutput : getPsbtOutputs()) {
List<PSBTEntry> outputEntries = psbtOutput.getOutputEntries(); List<PSBTEntry> outputEntries = psbtOutput.getOutputEntries();
for(PSBTEntry entry : outputEntries) { for(PSBTEntry entry : outputEntries) {
entry.serializeToStream(baos); if(includeXpubs || (entry.getKeyType() != PSBT_OUT_REDEEM_SCRIPT && entry.getKeyType() != PSBT_OUT_WITNESS_SCRIPT
&& entry.getKeyType() != PSBT_OUT_BIP32_DERIVATION && entry.getKeyType() != PSBT_OUT_PROPRIETARY
&& entry.getKeyType() != PSBT_OUT_TAP_INTERNAL_KEY && entry.getKeyType() != PSBT_OUT_TAP_BIP32_DERIVATION)) {
entry.serializeToStream(baos);
}
} }
baos.writeBytes(new byte[] {(byte)0x00}); baos.writeBytes(new byte[] {(byte)0x00});
} }
@ -511,7 +553,7 @@ public class PSBT {
Transaction finalTransaction = new Transaction(transaction.bitcoinSerialize()); Transaction finalTransaction = new Transaction(transaction.bitcoinSerialize());
if(hasWitness && !finalTransaction.isSegwit()) { if(hasWitness && !finalTransaction.isSegwit()) {
finalTransaction.setSegwitVersion(1); finalTransaction.setSegwitFlag(Transaction.DEFAULT_SEGWIT_FLAG);
} }
for(int i = 0; i < finalTransaction.getInputs().size(); i++) { for(int i = 0; i < finalTransaction.getInputs().size(); i++) {
@ -584,7 +626,11 @@ public class PSBT {
} }
public String toBase64String() { public String toBase64String() {
return Base64.toBase64String(serialize()); return toBase64String(true);
}
public String toBase64String(boolean includeXpubs) {
return Base64.toBase64String(serialize(includeXpubs));
} }
public static boolean isPSBT(byte[] b) { public static boolean isPSBT(byte[] b) {
@ -600,14 +646,24 @@ public class PSBT {
} }
public static boolean isPSBT(String s) { public static boolean isPSBT(String s) {
if (Utils.isHex(s) && s.startsWith(PSBT_MAGIC_HEX)) { try {
return true; if(Utils.isHex(s) && s.startsWith(PSBT_MAGIC_HEX)) {
} else { return true;
return Utils.isBase64(s) && Utils.bytesToHex(Base64.decode(s)).startsWith(PSBT_MAGIC_HEX); } else {
return Utils.isBase64(s) && Utils.bytesToHex(Base64.decode(s)).startsWith(PSBT_MAGIC_HEX);
}
} catch(Exception e) {
//ignore
} }
return false;
} }
public static PSBT fromString(String strPSBT) throws PSBTParseException { public static PSBT fromString(String strPSBT) throws PSBTParseException {
return fromString(strPSBT, true);
}
public static PSBT fromString(String strPSBT, boolean verifySignatures) throws PSBTParseException {
if (!isPSBT(strPSBT)) { if (!isPSBT(strPSBT)) {
throw new PSBTParseException("Provided string is not a PSBT"); throw new PSBTParseException("Provided string is not a PSBT");
} }
@ -617,6 +673,6 @@ public class PSBT {
} }
byte[] psbtBytes = Utils.hexToBytes(strPSBT); byte[] psbtBytes = Utils.hexToBytes(strPSBT);
return new PSBT(psbtBytes); return new PSBT(psbtBytes, verifySignatures);
} }
} }

View file

@ -3,6 +3,8 @@ package com.sparrowwallet.drongo.psbt;
import com.sparrowwallet.drongo.KeyDerivation; import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.crypto.ChildNumber; import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.protocol.VarInt;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
@ -10,6 +12,7 @@ import java.nio.ByteOrder;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map;
public class PSBTEntry { public class PSBTEntry {
private final byte[] key; private final byte[] key;
@ -55,6 +58,25 @@ public class PSBTEntry {
} }
} }
public static Map<KeyDerivation, List<Sha256Hash>> parseTaprootKeyDerivation(byte[] data) throws PSBTParseException {
if(data.length < 1) {
throw new PSBTParseException("Invalid taproot key derivation: no bytes");
}
VarInt varInt = new VarInt(data, 0);
int offset = varInt.getOriginalSizeInBytes();
if(data.length < offset + (varInt.value * 32)) {
throw new PSBTParseException("Invalid taproot key derivation: not enough bytes for leaf hashes");
}
List<Sha256Hash> leafHashes = new ArrayList<>();
for(int i = 0; i < varInt.value; i++) {
leafHashes.add(Sha256Hash.wrap(Arrays.copyOfRange(data, offset + (i * 32), offset + (i * 32) + 32)));
}
KeyDerivation keyDerivation = parseKeyDerivation(Arrays.copyOfRange(data, offset + (leafHashes.size() * 32), data.length));
return Map.of(keyDerivation, leafHashes);
}
public static KeyDerivation parseKeyDerivation(byte[] data) throws PSBTParseException { public static KeyDerivation parseKeyDerivation(byte[] data) throws PSBTParseException {
if(data.length < 4) { if(data.length < 4) {
throw new PSBTParseException("Invalid master fingerprint specified: not enough bytes"); throw new PSBTParseException("Invalid master fingerprint specified: not enough bytes");
@ -64,7 +86,7 @@ public class PSBTEntry {
throw new PSBTParseException("Invalid master fingerprint specified: " + masterFingerprint); throw new PSBTParseException("Invalid master fingerprint specified: " + masterFingerprint);
} }
if(data.length < 8) { if(data.length < 8) {
throw new PSBTParseException("Invalid key derivation specified: not enough bytes"); return new KeyDerivation(masterFingerprint, "m");
} }
List<ChildNumber> bip32pathList = readBIP32Derivation(Arrays.copyOfRange(data, 4, data.length)); List<ChildNumber> bip32pathList = readBIP32Derivation(Arrays.copyOfRange(data, 4, data.length));
String bip32path = KeyDerivation.writePath(bip32pathList); String bip32path = KeyDerivation.writePath(bip32pathList);
@ -83,7 +105,7 @@ public class PSBTEntry {
do { do {
bb.get(buf); bb.get(buf);
reverse(buf); Utils.reverse(buf);
ByteBuffer pbuf = ByteBuffer.wrap(buf); ByteBuffer pbuf = ByteBuffer.wrap(buf);
path.add(new ChildNumber(pbuf.getInt())); path.add(new ChildNumber(pbuf.getInt()));
} while(bb.hasRemaining()); } while(bb.hasRemaining());
@ -91,6 +113,19 @@ public class PSBTEntry {
return path; return path;
} }
public static byte[] serializeTaprootKeyDerivation(List<Sha256Hash> leafHashes, KeyDerivation keyDerivation) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
VarInt hashesLen = new VarInt(leafHashes.size());
baos.writeBytes(hashesLen.encode());
for(Sha256Hash leafHash : leafHashes) {
baos.writeBytes(leafHash.getBytes());
}
baos.writeBytes(serializeKeyDerivation(keyDerivation));
return baos.toByteArray();
}
public static byte[] serializeKeyDerivation(KeyDerivation keyDerivation) { public static byte[] serializeKeyDerivation(KeyDerivation keyDerivation) {
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] fingerprintBytes = Utils.hexToBytes(keyDerivation.getMasterFingerprint()); byte[] fingerprintBytes = Utils.hexToBytes(keyDerivation.getMasterFingerprint());
@ -202,14 +237,6 @@ public class PSBTEntry {
return bb.array(); return bb.array();
} }
private static void reverse(byte[] array) {
for (int i = 0; i < array.length / 2; i++) {
byte temp = array[i];
array[i] = array[array.length - i - 1];
array[array.length - i - 1] = temp;
}
}
public void checkOneByteKey() throws PSBTParseException { public void checkOneByteKey() throws PSBTParseException {
if(this.getKey().length != 1) { if(this.getKey().length != 1) {
throw new PSBTParseException("PSBT key type must be one byte"); throw new PSBTParseException("PSBT key type must be one byte");
@ -218,13 +245,19 @@ public class PSBTEntry {
public void checkOneBytePlusXpubKey() throws PSBTParseException { public void checkOneBytePlusXpubKey() throws PSBTParseException {
if(this.getKey().length != 79) { if(this.getKey().length != 79) {
throw new PSBTParseException("PSBT key type must be one byte"); throw new PSBTParseException("PSBT key type must be one byte plus xpub");
} }
} }
public void checkOneBytePlusPubKey() throws PSBTParseException { public void checkOneBytePlusPubKey() throws PSBTParseException {
if(this.getKey().length != 34) { if(this.getKey().length != 34) {
throw new PSBTParseException("PSBT key type must be one byte"); throw new PSBTParseException("PSBT key type must be one byte plus pub key");
}
}
public void checkOneBytePlusXOnlyPubKey() throws PSBTParseException {
if(this.getKey().length != 33) {
throw new PSBTParseException("PSBT key type must be one byte plus x only pub key");
} }
} }
} }

View file

@ -9,8 +9,10 @@ import org.slf4j.LoggerFactory;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.*; import java.util.*;
import java.util.stream.Collectors;
import static com.sparrowwallet.drongo.protocol.ScriptType.*; import static com.sparrowwallet.drongo.protocol.ScriptType.*;
import static com.sparrowwallet.drongo.protocol.TransactionSignature.Type.*;
import static com.sparrowwallet.drongo.psbt.PSBTEntry.*; import static com.sparrowwallet.drongo.psbt.PSBTEntry.*;
public class PSBTInput { public class PSBTInput {
@ -25,7 +27,11 @@ public class PSBTInput {
public static final byte PSBT_IN_FINAL_SCRIPTWITNESS = 0x08; public static final byte PSBT_IN_FINAL_SCRIPTWITNESS = 0x08;
public static final byte PSBT_IN_POR_COMMITMENT = 0x09; public static final byte PSBT_IN_POR_COMMITMENT = 0x09;
public static final byte PSBT_IN_PROPRIETARY = (byte)0xfc; public static final byte PSBT_IN_PROPRIETARY = (byte)0xfc;
public static final byte PSBT_IN_TAP_KEY_SIG = 0x13;
public static final byte PSBT_IN_TAP_BIP32_DERIVATION = 0x16;
public static final byte PSBT_IN_TAP_INTERNAL_KEY = 0x17;
private final PSBT psbt;
private Transaction nonWitnessUtxo; private Transaction nonWitnessUtxo;
private TransactionOutput witnessUtxo; private TransactionOutput witnessUtxo;
private final Map<ECKey, TransactionSignature> partialSignatures = new LinkedHashMap<>(); private final Map<ECKey, TransactionSignature> partialSignatures = new LinkedHashMap<>();
@ -37,20 +43,23 @@ public class PSBTInput {
private TransactionWitness finalScriptWitness; private TransactionWitness finalScriptWitness;
private String porCommitment; private String porCommitment;
private final Map<String, String> proprietary = new LinkedHashMap<>(); private final Map<String, String> proprietary = new LinkedHashMap<>();
private TransactionSignature tapKeyPathSignature;
private Map<ECKey, Map<KeyDerivation, List<Sha256Hash>>> tapDerivedPublicKeys = new LinkedHashMap<>();
private ECKey tapInternalKey;
private final Transaction transaction; private final Transaction transaction;
private final int index; private final int index;
private static final Logger log = LoggerFactory.getLogger(PSBTInput.class); private static final Logger log = LoggerFactory.getLogger(PSBTInput.class);
PSBTInput(Transaction transaction, int index) { PSBTInput(PSBT psbt, Transaction transaction, int index) {
this.psbt = psbt;
this.transaction = transaction; this.transaction = transaction;
this.index = index; this.index = index;
} }
PSBTInput(ScriptType scriptType, Transaction transaction, int index, Transaction utxo, int utxoIndex, Script redeemScript, Script witnessScript, Map<ECKey, KeyDerivation> derivedPublicKeys, Map<String, String> proprietary, boolean alwaysAddNonWitnessTx) { PSBTInput(PSBT psbt, ScriptType scriptType, Transaction transaction, int index, Transaction utxo, int utxoIndex, Script redeemScript, Script witnessScript, Map<ECKey, KeyDerivation> derivedPublicKeys, Map<String, String> proprietary, ECKey tapInternalKey, boolean alwaysAddNonWitnessTx) {
this(transaction, index); this(psbt, transaction, index);
sigHash = SigHash.ALL;
if(Arrays.asList(ScriptType.WITNESS_TYPES).contains(scriptType)) { if(Arrays.asList(ScriptType.WITNESS_TYPES).contains(scriptType)) {
this.witnessUtxo = utxo.getOutputs().get(utxoIndex); this.witnessUtxo = utxo.getOutputs().get(utxoIndex);
@ -66,11 +75,24 @@ public class PSBTInput {
this.redeemScript = redeemScript; this.redeemScript = redeemScript;
this.witnessScript = witnessScript; this.witnessScript = witnessScript;
this.derivedPublicKeys.putAll(derivedPublicKeys); if(scriptType != P2TR) {
this.derivedPublicKeys.putAll(derivedPublicKeys);
}
this.proprietary.putAll(proprietary); this.proprietary.putAll(proprietary);
this.tapInternalKey = tapInternalKey == null ? null : ECKey.fromPublicOnly(tapInternalKey.getPubKeyXCoord());
if(tapInternalKey != null && !derivedPublicKeys.values().isEmpty()) {
KeyDerivation tapKeyDerivation = derivedPublicKeys.values().iterator().next();
tapDerivedPublicKeys.put(this.tapInternalKey, Map.of(tapKeyDerivation, Collections.emptyList()));
}
this.sigHash = getDefaultSigHash();
} }
PSBTInput(List<PSBTEntry> inputEntries, Transaction transaction, int index) throws PSBTParseException { PSBTInput(PSBT psbt, List<PSBTEntry> inputEntries, Transaction transaction, int index) throws PSBTParseException {
this.psbt = psbt;
for(PSBTEntry entry : inputEntries) { for(PSBTEntry entry : inputEntries) {
switch(entry.getKeyType()) { switch(entry.getKeyType()) {
case PSBT_IN_NON_WITNESS_UTXO: case PSBT_IN_NON_WITNESS_UTXO:
@ -89,17 +111,13 @@ public class PSBTInput {
log.debug(" Transaction input references txid: " + input.getOutpoint().getHash() + " vout " + input.getOutpoint().getIndex() + " with script " + input.getScriptSig()); log.debug(" Transaction input references txid: " + input.getOutpoint().getHash() + " vout " + input.getOutpoint().getIndex() + " with script " + input.getScriptSig());
} }
for(TransactionOutput output: nonWitnessTx.getOutputs()) { for(TransactionOutput output: nonWitnessTx.getOutputs()) {
try { log.debug(" Transaction output value: " + output.getValue() + (output.getScript().getToAddress() != null ? " to address " + output.getScript().getToAddress() : "") + " with script hex " + Utils.bytesToHex(output.getScript().getProgram()) + " to script " + output.getScript());
log.debug(" Transaction output value: " + output.getValue() + " to addresses " + Arrays.asList(output.getScript().getToAddresses()) + " with script hex " + Utils.bytesToHex(output.getScript().getProgram()) + " to script " + output.getScript());
} catch(NonStandardScriptException e) {
log.error("Unknown script type", e);
}
} }
break; break;
case PSBT_IN_WITNESS_UTXO: case PSBT_IN_WITNESS_UTXO:
entry.checkOneByteKey(); entry.checkOneByteKey();
TransactionOutput witnessTxOutput = new TransactionOutput(null, entry.getData(), 0); TransactionOutput witnessTxOutput = new TransactionOutput(null, entry.getData(), 0);
if(!P2SH.isScriptType(witnessTxOutput.getScript()) && !P2WPKH.isScriptType(witnessTxOutput.getScript()) && !P2WSH.isScriptType(witnessTxOutput.getScript())) { if(!P2SH.isScriptType(witnessTxOutput.getScript()) && !P2WPKH.isScriptType(witnessTxOutput.getScript()) && !P2WSH.isScriptType(witnessTxOutput.getScript()) && !P2TR.isScriptType(witnessTxOutput.getScript())) {
throw new PSBTParseException("Witness UTXO provided for non-witness or unknown input"); throw new PSBTParseException("Witness UTXO provided for non-witness or unknown input");
} }
this.witnessUtxo = witnessTxOutput; this.witnessUtxo = witnessTxOutput;
@ -112,8 +130,12 @@ public class PSBTInput {
case PSBT_IN_PARTIAL_SIG: case PSBT_IN_PARTIAL_SIG:
entry.checkOneBytePlusPubKey(); entry.checkOneBytePlusPubKey();
ECKey sigPublicKey = ECKey.fromPublicOnly(entry.getKeyData()); ECKey sigPublicKey = ECKey.fromPublicOnly(entry.getKeyData());
if(entry.getData().length == 64 || entry.getData().length == 65) {
log.error("Schnorr signature provided as ECDSA partial signature, ignoring");
break;
}
//TODO: Verify signature //TODO: Verify signature
TransactionSignature signature = TransactionSignature.decodeFromBitcoin(entry.getData(), true, false); TransactionSignature signature = TransactionSignature.decodeFromBitcoin(ECDSA, entry.getData(), true);
this.partialSignatures.put(sigPublicKey, signature); this.partialSignatures.put(sigPublicKey, signature);
log.debug("Found input partial signature with public key " + sigPublicKey + " signature " + Utils.bytesToHex(entry.getData())); log.debug("Found input partial signature with public key " + sigPublicKey + " signature " + Utils.bytesToHex(entry.getData()));
break; break;
@ -136,11 +158,15 @@ public class PSBTInput {
throw new PSBTParseException("Witness UTXO provided but redeem script is not P2WPKH or P2WSH"); throw new PSBTParseException("Witness UTXO provided but redeem script is not P2WPKH or P2WSH");
} }
} }
if(scriptPubKey == null || !P2SH.isScriptType(scriptPubKey)) { if(scriptPubKey == null) {
throw new PSBTParseException("PSBT provided a redeem script for a transaction output that does not need one"); log.warn("PSBT provided a redeem script for a transaction output that was not provided");
} } else {
if(!Arrays.equals(Utils.sha256hash160(redeemScript.getProgram()), scriptPubKey.getPubKeyHash())) { if(!P2SH.isScriptType(scriptPubKey)) {
throw new PSBTParseException("Redeem script hash does not match transaction output script pubkey hash " + Utils.bytesToHex(scriptPubKey.getPubKeyHash())); throw new PSBTParseException("PSBT provided a redeem script for a transaction output that does not need one");
}
if(!Arrays.equals(Utils.sha256hash160(redeemScript.getProgram()), scriptPubKey.getPubKeyHash())) {
throw new PSBTParseException("Redeem script hash does not match transaction output script pubkey hash " + Utils.bytesToHex(scriptPubKey.getPubKeyHash()));
}
} }
this.redeemScript = redeemScript; this.redeemScript = redeemScript;
@ -156,7 +182,7 @@ public class PSBTInput {
pubKeyHash = this.witnessUtxo.getScript().getPubKeyHash(); pubKeyHash = this.witnessUtxo.getScript().getPubKeyHash();
} }
if(pubKeyHash == null) { if(pubKeyHash == null) {
throw new PSBTParseException("Witness script provided without P2WSH witness utxo or P2SH redeem script"); log.warn("Witness script provided without P2WSH witness utxo or P2SH redeem script");
} else if(!Arrays.equals(Sha256Hash.hash(witnessScript.getProgram()), pubKeyHash)) { } else if(!Arrays.equals(Sha256Hash.hash(witnessScript.getProgram()), pubKeyHash)) {
throw new PSBTParseException("Witness script hash does not match provided pay to script hash " + Utils.bytesToHex(pubKeyHash)); throw new PSBTParseException("Witness script hash does not match provided pay to script hash " + Utils.bytesToHex(pubKeyHash));
} }
@ -192,6 +218,29 @@ public class PSBTInput {
this.proprietary.put(Utils.bytesToHex(entry.getKeyData()), Utils.bytesToHex(entry.getData())); this.proprietary.put(Utils.bytesToHex(entry.getKeyData()), Utils.bytesToHex(entry.getData()));
log.debug("Found proprietary input " + Utils.bytesToHex(entry.getKeyData()) + ": " + Utils.bytesToHex(entry.getData())); log.debug("Found proprietary input " + Utils.bytesToHex(entry.getKeyData()) + ": " + Utils.bytesToHex(entry.getData()));
break; break;
case PSBT_IN_TAP_KEY_SIG:
entry.checkOneByteKey();
this.tapKeyPathSignature = TransactionSignature.decodeFromBitcoin(SCHNORR, entry.getData(), true);
log.debug("Found input taproot key path signature " + Utils.bytesToHex(entry.getData()));
break;
case PSBT_IN_TAP_BIP32_DERIVATION:
entry.checkOneBytePlusXOnlyPubKey();
ECKey tapPublicKey = ECKey.fromPublicOnly(entry.getKeyData());
Map<KeyDerivation, List<Sha256Hash>> tapKeyDerivations = parseTaprootKeyDerivation(entry.getData());
if(tapKeyDerivations.isEmpty()) {
log.warn("PSBT provided an invalid input taproot key derivation");
} else {
this.tapDerivedPublicKeys.put(tapPublicKey, tapKeyDerivations);
for(KeyDerivation tapKeyDerivation : tapKeyDerivations.keySet()) {
log.debug("Found input taproot key derivation for key " + Utils.bytesToHex(entry.getKeyData()) + " with master fingerprint " + tapKeyDerivation.getMasterFingerprint() + " at path " + tapKeyDerivation.getDerivationPath());
}
}
break;
case PSBT_IN_TAP_INTERNAL_KEY:
entry.checkOneByteKey();
this.tapInternalKey = ECKey.fromPublicOnly(entry.getData());
log.debug("Found input taproot internal key " + Utils.bytesToHex(entry.getData()));
break;
default: default:
log.warn("PSBT input not recognized key type: " + entry.getKeyType()); log.warn("PSBT input not recognized key type: " + entry.getKeyType());
} }
@ -205,7 +254,8 @@ public class PSBTInput {
List<PSBTEntry> entries = new ArrayList<>(); List<PSBTEntry> entries = new ArrayList<>();
if(nonWitnessUtxo != null) { if(nonWitnessUtxo != null) {
entries.add(populateEntry(PSBT_IN_NON_WITNESS_UTXO, null, nonWitnessUtxo.bitcoinSerialize())); //Serialize all nonWitnessUtxo fields without witness data (pre-Segwit serialization) to reduce PSBT size
entries.add(populateEntry(PSBT_IN_NON_WITNESS_UTXO, null, nonWitnessUtxo.bitcoinSerialize(false)));
} }
if(witnessUtxo != null) { if(witnessUtxo != null) {
@ -250,6 +300,20 @@ public class PSBTInput {
entries.add(populateEntry(PSBT_IN_PROPRIETARY, Utils.hexToBytes(entry.getKey()), Utils.hexToBytes(entry.getValue()))); entries.add(populateEntry(PSBT_IN_PROPRIETARY, Utils.hexToBytes(entry.getKey()), Utils.hexToBytes(entry.getValue())));
} }
if(tapKeyPathSignature != null) {
entries.add(populateEntry(PSBT_IN_TAP_KEY_SIG, null, tapKeyPathSignature.encodeToBitcoin()));
}
for(Map.Entry<ECKey, Map<KeyDerivation, List<Sha256Hash>>> entry : tapDerivedPublicKeys.entrySet()) {
if(!entry.getValue().isEmpty()) {
entries.add(populateEntry(PSBT_IN_TAP_BIP32_DERIVATION, entry.getKey().getPubKeyXCoord(), serializeTaprootKeyDerivation(Collections.emptyList(), entry.getValue().keySet().iterator().next())));
}
}
if(tapInternalKey != null) {
entries.add(populateEntry(PSBT_IN_TAP_INTERNAL_KEY, null, tapInternalKey.getPubKeyXCoord()));
}
return entries; return entries;
} }
@ -283,6 +347,16 @@ public class PSBTInput {
} }
proprietary.putAll(psbtInput.proprietary); proprietary.putAll(psbtInput.proprietary);
if(psbtInput.tapKeyPathSignature != null) {
tapKeyPathSignature = psbtInput.tapKeyPathSignature;
}
tapDerivedPublicKeys.putAll(psbtInput.tapDerivedPublicKeys);
if(psbtInput.tapInternalKey != null) {
tapInternalKey = psbtInput.tapInternalKey;
}
} }
public Transaction getNonWitnessUtxo() { public Transaction getNonWitnessUtxo() {
@ -379,8 +453,38 @@ public class PSBTInput {
return proprietary; return proprietary;
} }
public TransactionSignature getTapKeyPathSignature() {
return tapKeyPathSignature;
}
public void setTapKeyPathSignature(TransactionSignature tapKeyPathSignature) {
this.tapKeyPathSignature = tapKeyPathSignature;
}
public Map<ECKey, Map<KeyDerivation, List<Sha256Hash>>> getTapDerivedPublicKeys() {
return tapDerivedPublicKeys;
}
public void setTapDerivedPublicKeys(Map<ECKey, Map<KeyDerivation, List<Sha256Hash>>> tapDerivedPublicKeys) {
this.tapDerivedPublicKeys = tapDerivedPublicKeys;
}
public ECKey getTapInternalKey() {
return tapInternalKey;
}
public void setTapInternalKey(ECKey tapInternalKey) {
this.tapInternalKey = tapInternalKey;
}
public boolean isTaproot() {
return getUtxo() != null && getScriptType() == P2TR;
}
public boolean isSigned() { public boolean isSigned() {
if(!getPartialSignatures().isEmpty()) { if(getTapKeyPathSignature() != null) {
return true;
} else if(!getPartialSignatures().isEmpty()) {
try { try {
//All partial sigs are already verified //All partial sigs are already verified
int reqSigs = getSigningScript().getNumRequiredSignatures(); int reqSigs = getSigningScript().getNumRequiredSignatures();
@ -399,27 +503,40 @@ public class PSBTInput {
return getFinalScriptWitness().getSignatures(); return getFinalScriptWitness().getSignatures();
} else if(getFinalScriptSig() != null) { } else if(getFinalScriptSig() != null) {
return getFinalScriptSig().getSignatures(); return getFinalScriptSig().getSignatures();
} else if(getTapKeyPathSignature() != null) {
return List.of(getTapKeyPathSignature());
} else { } else {
return getPartialSignatures().values(); return getPartialSignatures().values();
} }
} }
private SigHash getDefaultSigHash() {
if(isTaproot()) {
return SigHash.DEFAULT;
}
return SigHash.ALL;
}
public boolean sign(ECKey privKey) { public boolean sign(ECKey privKey) {
SigHash localSigHash = getSigHash(); SigHash localSigHash = getSigHash();
if(localSigHash == null) { if(localSigHash == null) {
//Assume SigHash.ALL localSigHash = getDefaultSigHash();
localSigHash = SigHash.ALL;
} }
if(getNonWitnessUtxo() != null || getWitnessUtxo() != null) { if(getNonWitnessUtxo() != null || getWitnessUtxo() != null) {
Script signingScript = getSigningScript(); Script signingScript = getSigningScript();
if(signingScript != null) { if(signingScript != null) {
Sha256Hash hash = getHashForSignature(signingScript, localSigHash); Sha256Hash hash = getHashForSignature(signingScript, localSigHash);
ECKey.ECDSASignature ecdsaSignature = privKey.sign(hash); TransactionSignature.Type type = isTaproot() ? SCHNORR : ECDSA;
TransactionSignature transactionSignature = new TransactionSignature(ecdsaSignature, localSigHash); TransactionSignature transactionSignature = privKey.sign(hash, localSigHash, type);
ECKey pubKey = ECKey.fromPublicOnly(privKey); if(type == SCHNORR) {
getPartialSignatures().put(pubKey, transactionSignature); tapKeyPathSignature = transactionSignature;
} else {
ECKey pubKey = ECKey.fromPublicOnly(privKey);
getPartialSignatures().put(pubKey, transactionSignature);
}
return true; return true;
} }
@ -428,11 +545,10 @@ public class PSBTInput {
return false; return false;
} }
boolean verifySignatures() throws PSBTParseException { boolean verifySignatures() throws PSBTSignatureException {
SigHash localSigHash = getSigHash(); SigHash localSigHash = getSigHash();
if(localSigHash == null) { if(localSigHash == null) {
//Assume SigHash.ALL localSigHash = getDefaultSigHash();
localSigHash = SigHash.ALL;
} }
if(getNonWitnessUtxo() != null || getWitnessUtxo() != null) { if(getNonWitnessUtxo() != null || getWitnessUtxo() != null) {
@ -440,10 +556,17 @@ public class PSBTInput {
if(signingScript != null) { if(signingScript != null) {
Sha256Hash hash = getHashForSignature(signingScript, localSigHash); Sha256Hash hash = getHashForSignature(signingScript, localSigHash);
for(ECKey sigPublicKey : getPartialSignatures().keySet()) { if(isTaproot() && tapKeyPathSignature != null) {
TransactionSignature signature = getPartialSignature(sigPublicKey); ECKey outputKey = P2TR.getPublicKeyFromScript(getUtxo().getScript());
if(!sigPublicKey.verify(hash, signature)) { if(!outputKey.verify(hash, tapKeyPathSignature)) {
throw new PSBTParseException("Partial signature does not verify against provided public key"); throw new PSBTSignatureException("Tweaked internal key does not verify against provided taproot keypath signature");
}
} else {
for(ECKey sigPublicKey : getPartialSignatures().keySet()) {
TransactionSignature signature = getPartialSignature(sigPublicKey);
if(!sigPublicKey.verify(hash, signature)) {
throw new PSBTSignatureException("Partial signature does not verify against provided public key");
}
} }
} }
@ -462,7 +585,7 @@ public class PSBTInput {
Map<ECKey, TransactionSignature> signingKeys = new LinkedHashMap<>(); Map<ECKey, TransactionSignature> signingKeys = new LinkedHashMap<>();
if(signingScript != null) { if(signingScript != null) {
Sha256Hash hash = getHashForSignature(signingScript, getSigHash() == null ? SigHash.ALL : getSigHash()); Sha256Hash hash = getHashForSignature(signingScript, getSigHash() == null ? getDefaultSigHash() : getSigHash());
for(ECKey sigPublicKey : availableKeys) { for(ECKey sigPublicKey : availableKeys) {
for(TransactionSignature signature : signatures) { for(TransactionSignature signature : signatures) {
@ -526,6 +649,11 @@ public class PSBTInput {
} }
} }
if(P2TR.isScriptType(signingScript)) {
//For now, only support keypath spends and just return the ScriptPubKey
//In future return the script from PSBT_IN_TAP_LEAF_SCRIPT
}
return signingScript; return signingScript;
} }
@ -549,18 +677,19 @@ public class PSBTInput {
witnessScript = null; witnessScript = null;
porCommitment = null; porCommitment = null;
proprietary.clear(); proprietary.clear();
tapDerivedPublicKeys.clear();
tapKeyPathSignature = null;
} }
private Sha256Hash getHashForSignature(Script connectedScript, SigHash localSigHash) { private Sha256Hash getHashForSignature(Script connectedScript, SigHash localSigHash) {
Sha256Hash hash; Sha256Hash hash;
ScriptType scriptType = getScriptType(); ScriptType scriptType = getScriptType();
if(getWitnessUtxo() == null && Arrays.asList(WITNESS_TYPES).contains(scriptType)) { if(scriptType == ScriptType.P2TR) {
throw new IllegalStateException("Trying to get signature hash for " + scriptType + " script without a PSBT witness UTXO"); List<TransactionOutput> spentUtxos = psbt.getPsbtInputs().stream().map(PSBTInput::getUtxo).collect(Collectors.toList());
} hash = transaction.hashForTaprootSignature(spentUtxos, index, !P2TR.isScriptType(connectedScript), connectedScript, localSigHash, null);
} else if(Arrays.asList(WITNESS_TYPES).contains(scriptType)) {
if(getWitnessUtxo() != null) { long prevValue = getUtxo().getValue();
long prevValue = getWitnessUtxo().getValue();
hash = transaction.hashForWitnessSignature(index, connectedScript, prevValue, localSigHash); hash = transaction.hashForWitnessSignature(index, connectedScript, prevValue, localSigHash);
} else { } else {
hash = transaction.hashForLegacySignature(index, connectedScript, localSigHash); hash = transaction.hashForLegacySignature(index, connectedScript, localSigHash);

View file

@ -4,26 +4,30 @@ import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.crypto.ECKey; import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.protocol.Script; import com.sparrowwallet.drongo.protocol.Script;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.util.ArrayList; import java.util.*;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import static com.sparrowwallet.drongo.protocol.ScriptType.*;
import static com.sparrowwallet.drongo.psbt.PSBTEntry.*; import static com.sparrowwallet.drongo.psbt.PSBTEntry.*;
public class PSBTOutput { public class PSBTOutput {
public static final byte PSBT_OUT_REDEEM_SCRIPT = 0x00; public static final byte PSBT_OUT_REDEEM_SCRIPT = 0x00;
public static final byte PSBT_OUT_WITNESS_SCRIPT = 0x01; public static final byte PSBT_OUT_WITNESS_SCRIPT = 0x01;
public static final byte PSBT_OUT_BIP32_DERIVATION = 0x02; public static final byte PSBT_OUT_BIP32_DERIVATION = 0x02;
public static final byte PSBT_OUT_TAP_INTERNAL_KEY = 0x05;
public static final byte PSBT_OUT_TAP_BIP32_DERIVATION = 0x07;
public static final byte PSBT_OUT_PROPRIETARY = (byte)0xfc; public static final byte PSBT_OUT_PROPRIETARY = (byte)0xfc;
private Script redeemScript; private Script redeemScript;
private Script witnessScript; private Script witnessScript;
private final Map<ECKey, KeyDerivation> derivedPublicKeys = new LinkedHashMap<>(); private final Map<ECKey, KeyDerivation> derivedPublicKeys = new LinkedHashMap<>();
private final Map<String, String> proprietary = new LinkedHashMap<>(); private final Map<String, String> proprietary = new LinkedHashMap<>();
private Map<ECKey, Map<KeyDerivation, List<Sha256Hash>>> tapDerivedPublicKeys = new LinkedHashMap<>();
private ECKey tapInternalKey;
private static final Logger log = LoggerFactory.getLogger(PSBTOutput.class); private static final Logger log = LoggerFactory.getLogger(PSBTOutput.class);
@ -31,11 +35,22 @@ public class PSBTOutput {
//empty constructor //empty constructor
} }
PSBTOutput(Script redeemScript, Script witnessScript, Map<ECKey, KeyDerivation> derivedPublicKeys, Map<String, String> proprietary) { PSBTOutput(ScriptType scriptType, Script redeemScript, Script witnessScript, Map<ECKey, KeyDerivation> derivedPublicKeys, Map<String, String> proprietary, ECKey tapInternalKey) {
this.redeemScript = redeemScript; this.redeemScript = redeemScript;
this.witnessScript = witnessScript; this.witnessScript = witnessScript;
this.derivedPublicKeys.putAll(derivedPublicKeys);
if(scriptType != P2TR) {
this.derivedPublicKeys.putAll(derivedPublicKeys);
}
this.proprietary.putAll(proprietary); this.proprietary.putAll(proprietary);
this.tapInternalKey = tapInternalKey == null ? null : ECKey.fromPublicOnly(tapInternalKey.getPubKeyXCoord());
if(tapInternalKey != null && !derivedPublicKeys.values().isEmpty()) {
KeyDerivation tapKeyDerivation = derivedPublicKeys.values().iterator().next();
tapDerivedPublicKeys.put(this.tapInternalKey, Map.of(tapKeyDerivation, Collections.emptyList()));
}
} }
PSBTOutput(List<PSBTEntry> outputEntries) throws PSBTParseException { PSBTOutput(List<PSBTEntry> outputEntries) throws PSBTParseException {
@ -64,6 +79,24 @@ public class PSBTOutput {
proprietary.put(Utils.bytesToHex(entry.getKeyData()), Utils.bytesToHex(entry.getData())); proprietary.put(Utils.bytesToHex(entry.getKeyData()), Utils.bytesToHex(entry.getData()));
log.debug("Found proprietary output " + Utils.bytesToHex(entry.getKeyData()) + ": " + Utils.bytesToHex(entry.getData())); log.debug("Found proprietary output " + Utils.bytesToHex(entry.getKeyData()) + ": " + Utils.bytesToHex(entry.getData()));
break; break;
case PSBT_OUT_TAP_INTERNAL_KEY:
entry.checkOneByteKey();
this.tapInternalKey = ECKey.fromPublicOnly(entry.getData());
log.debug("Found output taproot internal key " + Utils.bytesToHex(entry.getData()));
break;
case PSBT_OUT_TAP_BIP32_DERIVATION:
entry.checkOneBytePlusXOnlyPubKey();
ECKey tapPublicKey = ECKey.fromPublicOnly(entry.getKeyData());
Map<KeyDerivation, List<Sha256Hash>> tapKeyDerivations = parseTaprootKeyDerivation(entry.getData());
if(tapKeyDerivations.isEmpty()) {
log.warn("PSBT provided an invalid output taproot key derivation");
} else {
this.tapDerivedPublicKeys.put(tapPublicKey, tapKeyDerivations);
for(KeyDerivation tapKeyDerivation : tapKeyDerivations.keySet()) {
log.debug("Found output taproot key derivation for key " + Utils.bytesToHex(entry.getKeyData()) + " with master fingerprint " + tapKeyDerivation.getMasterFingerprint() + " at path " + tapKeyDerivation.getDerivationPath());
}
}
break;
default: default:
log.warn("PSBT output not recognized key type: " + entry.getKeyType()); log.warn("PSBT output not recognized key type: " + entry.getKeyType());
} }
@ -89,6 +122,16 @@ public class PSBTOutput {
entries.add(populateEntry(PSBT_OUT_PROPRIETARY, Utils.hexToBytes(entry.getKey()), Utils.hexToBytes(entry.getValue()))); entries.add(populateEntry(PSBT_OUT_PROPRIETARY, Utils.hexToBytes(entry.getKey()), Utils.hexToBytes(entry.getValue())));
} }
for(Map.Entry<ECKey, Map<KeyDerivation, List<Sha256Hash>>> entry : tapDerivedPublicKeys.entrySet()) {
if(!entry.getValue().isEmpty()) {
entries.add(populateEntry(PSBT_OUT_TAP_BIP32_DERIVATION, entry.getKey().getPubKeyXCoord(), serializeTaprootKeyDerivation(Collections.emptyList(), entry.getValue().keySet().iterator().next())));
}
}
if(tapInternalKey != null) {
entries.add(populateEntry(PSBT_OUT_TAP_INTERNAL_KEY, null, tapInternalKey.getPubKeyXCoord()));
}
return entries; return entries;
} }
@ -103,6 +146,12 @@ public class PSBTOutput {
derivedPublicKeys.putAll(psbtOutput.derivedPublicKeys); derivedPublicKeys.putAll(psbtOutput.derivedPublicKeys);
proprietary.putAll(psbtOutput.proprietary); proprietary.putAll(psbtOutput.proprietary);
tapDerivedPublicKeys.putAll(psbtOutput.tapDerivedPublicKeys);
if(psbtOutput.tapInternalKey != null) {
tapInternalKey = psbtOutput.tapInternalKey;
}
} }
public Script getRedeemScript() { public Script getRedeemScript() {
@ -132,4 +181,24 @@ public class PSBTOutput {
public Map<String, String> getProprietary() { public Map<String, String> getProprietary() {
return proprietary; return proprietary;
} }
public Map<ECKey, Map<KeyDerivation, List<Sha256Hash>>> getTapDerivedPublicKeys() {
return tapDerivedPublicKeys;
}
public void setTapDerivedPublicKeys(Map<ECKey, Map<KeyDerivation, List<Sha256Hash>>> tapDerivedPublicKeys) {
this.tapDerivedPublicKeys = tapDerivedPublicKeys;
}
public ECKey getTapInternalKey() {
return tapInternalKey;
}
public void setTapInternalKey(ECKey tapInternalKey) {
this.tapInternalKey = tapInternalKey;
}
public void clearNonFinalFields() {
tapDerivedPublicKeys.clear();
}
} }

View file

@ -0,0 +1,19 @@
package com.sparrowwallet.drongo.psbt;
public class PSBTSignatureException extends PSBTParseException {
public PSBTSignatureException() {
super();
}
public PSBTSignatureException(String message) {
super(message);
}
public PSBTSignatureException(Throwable cause) {
super(cause);
}
public PSBTSignatureException(String message, Throwable cause) {
super(message, cause);
}
}

View file

@ -2,6 +2,7 @@ package com.sparrowwallet.drongo.uri;
import com.sparrowwallet.drongo.address.Address; import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.address.InvalidAddressException; import com.sparrowwallet.drongo.address.InvalidAddressException;
import com.sparrowwallet.drongo.wallet.Payment;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -86,9 +87,7 @@ public class BitcoinURI {
* @throws BitcoinURIParseException if the URI is not syntactically or semantically valid. * @throws BitcoinURIParseException if the URI is not syntactically or semantically valid.
*/ */
public BitcoinURI(String input) throws BitcoinURIParseException { public BitcoinURI(String input) throws BitcoinURIParseException {
String scheme = BITCOIN_SCHEME; // Attempt to parse the URI
// Attempt to form the URI (fail fast syntax checking to official standards).
URI uri; URI uri;
try { try {
uri = new URI(input); uri = new URI(input);
@ -99,23 +98,14 @@ public class BitcoinURI {
// URI is formed as bitcoin:<address>?<query parameters> // URI is formed as bitcoin:<address>?<query parameters>
// blockchain.info generates URIs of non-BIP compliant form bitcoin://address?.... // blockchain.info generates URIs of non-BIP compliant form bitcoin://address?....
// Remove the bitcoin scheme. if (!BITCOIN_SCHEME.equalsIgnoreCase(uri.getScheme())) {
// (Note: getSchemeSpecificPart() is not used as it unescapes the label and parse then fails.
// For instance with : bitcoin:129mVqKUmJ9uwPxKJBnNdABbuaaNfho4Ha?amount=0.06&label=Tom%20%26%20Jerry
// the & (%26) in Tom and Jerry gets interpreted as a separator and the label then gets parsed
// as 'Tom ' instead of 'Tom & Jerry')
String blockchainInfoScheme = scheme + "://";
String correctScheme = scheme + ":";
String schemeSpecificPart;
final String inputLc = input.toLowerCase(Locale.US);
if(inputLc.startsWith(blockchainInfoScheme)) {
schemeSpecificPart = input.substring(blockchainInfoScheme.length());
} else if(inputLc.startsWith(correctScheme)) {
schemeSpecificPart = input.substring(correctScheme.length());
} else {
throw new BitcoinURIParseException("Unsupported URI scheme: " + uri.getScheme()); throw new BitcoinURIParseException("Unsupported URI scheme: " + uri.getScheme());
} }
String schemeSpecificPart = uri.getRawSchemeSpecificPart().startsWith("//")
? uri.getRawSchemeSpecificPart().substring(2)
: uri.getRawSchemeSpecificPart();
// Split off the address from the rest of the query parameters. // Split off the address from the rest of the query parameters.
String[] addressSplitTokens = schemeSpecificPart.split("\\?", 2); String[] addressSplitTokens = schemeSpecificPart.split("\\?", 2);
if(addressSplitTokens.length == 0) { if(addressSplitTokens.length == 0) {
@ -164,7 +154,7 @@ public class BitcoinURI {
if(sepIndex == 0) { if(sepIndex == 0) {
throw new BitcoinURIParseException("Malformed Groestlcoin URI - empty name '" + nameValuePairToken + "'"); throw new BitcoinURIParseException("Malformed Groestlcoin URI - empty name '" + nameValuePairToken + "'");
} }
final String nameToken = nameValuePairToken.substring(0, sepIndex).toLowerCase(Locale.ENGLISH); final String nameToken = nameValuePairToken.substring(0, sepIndex).toLowerCase(Locale.ROOT);
final String valueToken = nameValuePairToken.substring(sepIndex + 1); final String valueToken = nameValuePairToken.substring(sepIndex + 1);
// Parse the amount. // Parse the amount.
@ -325,6 +315,11 @@ public class BitcoinURI {
return builder.toString(); return builder.toString();
} }
public Payment toPayment() {
long amount = getAmount() == null ? -1 : getAmount();
return new Payment(getAddress(), getLabel(), amount, false);
}
/** /**
* Constructs a new BitcoinURI from the given address. * Constructs a new BitcoinURI from the given address.
* *

View file

@ -1,26 +1,40 @@
package com.sparrowwallet.drongo.wallet; package com.sparrowwallet.drongo.wallet;
import com.sparrowwallet.drongo.protocol.Sha256Hash; import com.sparrowwallet.drongo.protocol.*;
import com.sparrowwallet.drongo.protocol.Transaction;
import java.util.Collections; import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.HashSet;
import java.util.Objects; import java.util.Set;
import java.util.stream.Collectors;
public class BlockTransaction extends BlockTransactionHash implements Comparable<BlockTransaction> { public class BlockTransaction extends BlockTransactionHash implements Comparable<BlockTransaction> {
private final Transaction transaction; private final Transaction transaction;
private final Sha256Hash blockHash; private final Sha256Hash blockHash;
private final Set<HashIndex> spending = new HashSet<>();
private final Set<HashIndex> funding = new HashSet<>();
public BlockTransaction(Sha256Hash hash, int height, Date date, Long fee, Transaction transaction) { public BlockTransaction(Sha256Hash hash, int height, Date date, Long fee, Transaction transaction) {
this(hash, height, date, fee, transaction, null); this(hash, height, date, fee, transaction, null);
} }
public BlockTransaction(Sha256Hash hash, int height, Date date, Long fee, Transaction transaction, Sha256Hash blockHash) { public BlockTransaction(Sha256Hash hash, int height, Date date, Long fee, Transaction transaction, Sha256Hash blockHash) {
super(hash, height, date, fee); this(hash, height, date, fee, transaction, blockHash, null);
}
public BlockTransaction(Sha256Hash hash, int height, Date date, Long fee, Transaction transaction, Sha256Hash blockHash, String label) {
super(hash, height, date, fee, label);
this.transaction = transaction; this.transaction = transaction;
this.blockHash = blockHash; this.blockHash = blockHash;
if(transaction != null) {
for(TransactionInput txInput : transaction.getInputs()) {
spending.add(new HashIndex(txInput.getOutpoint().getHash(), txInput.getOutpoint().getIndex()));
}
for(TransactionOutput txOutput : transaction.getOutputs()) {
funding.add(new HashIndex(hash, txOutput.getIndex()));
}
}
} }
public Transaction getTransaction() { public Transaction getTransaction() {
@ -31,64 +45,46 @@ public class BlockTransaction extends BlockTransactionHash implements Comparable
return blockHash; return blockHash;
} }
public Set<HashIndex> getSpending() {
return Collections.unmodifiableSet(spending);
}
public Set<HashIndex> getFunding() {
return Collections.unmodifiableSet(funding);
}
public Double getFeeRate() {
if(getFee() != null && transaction != null) {
double vSize = transaction.getVirtualSize();
return getFee() / vSize;
}
return null;
}
@Override @Override
public int compareTo(BlockTransaction blkTx) { public int compareTo(BlockTransaction blkTx) {
if(getHeight() != blkTx.getHeight()) { int blockOrder = compareBlockOrder(blkTx);
return getComparisonHeight() - blkTx.getComparisonHeight(); if(blockOrder != 0) {
} return blockOrder;
if(getReferencedOutpoints(this).removeAll(getOutputs(blkTx))) {
return 1;
}
if(getReferencedOutpoints(blkTx).removeAll(getOutputs(this))) {
return -1;
} }
return super.compareTo(blkTx); return super.compareTo(blkTx);
} }
private static List<HashIndex> getReferencedOutpoints(BlockTransaction blockchainTransaction) { public int compareBlockOrder(BlockTransaction blkTx) {
if(blockchainTransaction.getTransaction() == null) { if(getHeight() != blkTx.getHeight()) {
return Collections.emptyList(); return getComparisonHeight() - blkTx.getComparisonHeight();
} }
return blockchainTransaction.getTransaction().getInputs().stream() if(!Collections.disjoint(spending, blkTx.funding)) {
.map(txInput -> new HashIndex(txInput.getOutpoint().getHash(), (int)txInput.getOutpoint().getIndex())) return 1;
.collect(Collectors.toList());
}
private static List<HashIndex> getOutputs(BlockTransaction blockchainTransaction) {
if(blockchainTransaction.getTransaction() == null) {
return Collections.emptyList();
} }
return blockchainTransaction.getTransaction().getOutputs().stream() if(!Collections.disjoint(blkTx.spending, funding)) {
.map(txOutput -> new HashIndex(blockchainTransaction.getHash(), txOutput.getIndex())) return -1;
.collect(Collectors.toList());
}
private static class HashIndex {
public Sha256Hash hash;
public int index;
public HashIndex(Sha256Hash hash, int index) {
this.hash = hash;
this.index = index;
} }
@Override return 0;
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
HashIndex hashIndex = (HashIndex) o;
return index == hashIndex.index &&
hash.equals(hashIndex.hash);
}
@Override
public int hashCode() {
return Objects.hash(hash, index);
}
} }
} }

View file

@ -5,7 +5,7 @@ import com.sparrowwallet.drongo.protocol.Sha256Hash;
import java.util.Date; import java.util.Date;
import java.util.Objects; import java.util.Objects;
public abstract class BlockTransactionHash { public abstract class BlockTransactionHash extends Persistable {
public static final int BLOCKS_TO_CONFIRM = 6; public static final int BLOCKS_TO_CONFIRM = 6;
public static final int BLOCKS_TO_FULLY_CONFIRM = 100; public static final int BLOCKS_TO_FULLY_CONFIRM = 100;
@ -16,11 +16,12 @@ public abstract class BlockTransactionHash {
private String label; private String label;
public BlockTransactionHash(Sha256Hash hash, int height, Date date, Long fee) { public BlockTransactionHash(Sha256Hash hash, int height, Date date, Long fee, String label) {
this.hash = hash; this.hash = hash;
this.height = height; this.height = height;
this.date = date; this.date = date;
this.fee = fee; this.fee = fee;
this.label = label;
} }
public Sha256Hash getHash() { public Sha256Hash getHash() {

View file

@ -16,7 +16,11 @@ public class BlockTransactionHashIndex extends BlockTransactionHash implements C
} }
public BlockTransactionHashIndex(Sha256Hash hash, int height, Date date, Long fee, long index, long value, BlockTransactionHashIndex spentBy) { public BlockTransactionHashIndex(Sha256Hash hash, int height, Date date, Long fee, long index, long value, BlockTransactionHashIndex spentBy) {
super(hash, height, date, fee); this(hash, height, date, fee, index, value, spentBy, null);
}
public BlockTransactionHashIndex(Sha256Hash hash, int height, Date date, Long fee, long index, long value, BlockTransactionHashIndex spentBy, String label) {
super(hash, height, date, fee, label);
this.index = index; this.index = index;
this.value = value; this.value = value;
this.spentBy = spentBy; this.spentBy = spentBy;
@ -92,6 +96,8 @@ public class BlockTransactionHashIndex extends BlockTransactionHash implements C
} }
public BlockTransactionHashIndex copy() { public BlockTransactionHashIndex copy() {
return new BlockTransactionHashIndex(super.getHash(), super.getHeight(), super.getDate(), super.getFee(), index, value, spentBy == null ? null : spentBy.copy()); BlockTransactionHashIndex copy = new BlockTransactionHashIndex(super.getHash(), super.getHeight(), super.getDate(), super.getFee(), index, value, spentBy == null ? null : spentBy.copy(), super.getLabel());
copy.setId(getId());
return copy;
} }
} }

View file

@ -6,7 +6,7 @@ import org.slf4j.LoggerFactory;
import java.util.*; import java.util.*;
public class BnBUtxoSelector implements UtxoSelector { public class BnBUtxoSelector extends SingleSetUtxoSelector {
private static final Logger log = LoggerFactory.getLogger(BnBUtxoSelector.class); private static final Logger log = LoggerFactory.getLogger(BnBUtxoSelector.class);
private static final int TOTAL_TRIES = 100000; private static final int TOTAL_TRIES = 100000;

View file

@ -0,0 +1,23 @@
package com.sparrowwallet.drongo.wallet;
import com.sparrowwallet.drongo.protocol.Transaction;
public class CoinbaseUtxoFilter implements UtxoFilter {
private final Wallet wallet;
public CoinbaseUtxoFilter(Wallet wallet) {
this.wallet = wallet;
}
@Override
public boolean isEligible(BlockTransactionHashIndex candidate) {
//Disallow immature coinbase outputs
BlockTransaction blockTransaction = wallet.getWalletTransaction(candidate.getHash());
if(blockTransaction != null && blockTransaction.getTransaction() != null && blockTransaction.getTransaction().isCoinBase()
&& wallet.getStoredBlockHeight() != null && candidate.getConfirmations(wallet.getStoredBlockHeight()) < Transaction.COINBASE_MATURITY_THRESHOLD) {
return false;
}
return true;
}
}

View file

@ -7,7 +7,7 @@ import com.sparrowwallet.drongo.crypto.*;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.*; import java.util.*;
public class DeterministicSeed implements EncryptableItem { public class DeterministicSeed extends Persistable implements EncryptableItem {
public static final int DEFAULT_SEED_ENTROPY_BITS = 128; public static final int DEFAULT_SEED_ENTROPY_BITS = 128;
public static final int MAX_SEED_ENTROPY_BITS = 512; public static final int MAX_SEED_ENTROPY_BITS = 512;
@ -184,6 +184,7 @@ public class DeterministicSeed implements EncryptableItem {
Arrays.fill(mnemonicBytes != null ? mnemonicBytes : new byte[0], (byte)0); Arrays.fill(mnemonicBytes != null ? mnemonicBytes : new byte[0], (byte)0);
DeterministicSeed seed = new DeterministicSeed(encryptedMnemonic, needsPassphrase, creationTimeSeconds, type); DeterministicSeed seed = new DeterministicSeed(encryptedMnemonic, needsPassphrase, creationTimeSeconds, type);
seed.setId(getId());
seed.setPassphrase(passphrase); seed.setPassphrase(passphrase);
return seed; return seed;
@ -209,6 +210,7 @@ public class DeterministicSeed implements EncryptableItem {
KeyDeriver keyDeriver = getEncryptionType().getDeriver().getKeyDeriver(encryptedMnemonicCode.getKeySalt()); KeyDeriver keyDeriver = getEncryptionType().getDeriver().getKeyDeriver(encryptedMnemonicCode.getKeySalt());
Key key = keyDeriver.deriveKey(password); Key key = keyDeriver.deriveKey(password);
DeterministicSeed seed = decrypt(key); DeterministicSeed seed = decrypt(key);
seed.setId(getId());
key.clear(); key.clear();
return seed; return seed;
@ -225,6 +227,7 @@ public class DeterministicSeed implements EncryptableItem {
Arrays.fill(decrypted, (byte)0); Arrays.fill(decrypted, (byte)0);
DeterministicSeed seed = new DeterministicSeed(mnemonic, needsPassphrase, creationTimeSeconds, type); DeterministicSeed seed = new DeterministicSeed(mnemonic, needsPassphrase, creationTimeSeconds, type);
seed.setId(getId());
seed.setPassphrase(passphrase); seed.setPassphrase(passphrase);
return seed; return seed;
@ -341,6 +344,7 @@ public class DeterministicSeed implements EncryptableItem {
seed = new DeterministicSeed(new ArrayList<>(mnemonicCode), needsPassphrase, creationTimeSeconds, type); seed = new DeterministicSeed(new ArrayList<>(mnemonicCode), needsPassphrase, creationTimeSeconds, type);
} }
seed.setId(getId());
seed.setPassphrase(passphrase); seed.setPassphrase(passphrase);
return seed; return seed;
} }

View file

@ -5,10 +5,7 @@ import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.policy.Miniscript; import com.sparrowwallet.drongo.policy.Miniscript;
import com.sparrowwallet.drongo.policy.Policy; import com.sparrowwallet.drongo.policy.Policy;
import com.sparrowwallet.drongo.policy.PolicyType; import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.NonStandardScriptException; import com.sparrowwallet.drongo.protocol.*;
import com.sparrowwallet.drongo.protocol.Script;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.protocol.TransactionSignature;
import com.sparrowwallet.drongo.psbt.PSBT; import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.psbt.PSBTInput; import com.sparrowwallet.drongo.psbt.PSBTInput;
@ -67,6 +64,9 @@ public class FinalizingPSBTWallet extends Wallet {
} }
} }
setGapLimit(0);
purposeNode.setChildren(new TreeSet<>());
setPolicyType(numSignatures == 1 ? PolicyType.SINGLE : PolicyType.MULTI); setPolicyType(numSignatures == 1 ? PolicyType.SINGLE : PolicyType.MULTI);
} }
@ -125,4 +125,16 @@ public class FinalizingPSBTWallet extends Wallet {
public boolean canSign(PSBT psbt) { public boolean canSign(PSBT psbt) {
return !getSigningNodes(psbt).isEmpty(); return !getSigningNodes(psbt).isEmpty();
} }
@Override
public boolean isWalletTxo(TransactionInput txInput) {
for(PSBTInput psbtInput : signedInputNodes.keySet()) {
TransactionInput psbtTxInput = psbtInput.getInput();
if(psbtTxInput.getOutpoint().getHash().equals(txInput.getOutpoint().getHash()) && psbtTxInput.getOutpoint().getIndex() == txInput.getOutpoint().getIndex()) {
return true;
}
}
return false;
}
} }

View file

@ -4,11 +4,20 @@ import com.sparrowwallet.drongo.ExtendedKey;
import com.sparrowwallet.drongo.KeyDerivation; import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.KeyPurpose; import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.bip47.PaymentAddress;
import com.sparrowwallet.drongo.bip47.PaymentCode;
import com.sparrowwallet.drongo.crypto.*; import com.sparrowwallet.drongo.crypto.*;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List; import java.util.List;
public class Keystore { public class Keystore extends Persistable {
private static final Logger log = LoggerFactory.getLogger(Keystore.class);
public static final String DEFAULT_LABEL = "Keystore 1"; public static final String DEFAULT_LABEL = "Keystore 1";
public static final int MAX_LABEL_LENGTH = 16; public static final int MAX_LABEL_LENGTH = 16;
@ -17,8 +26,16 @@ public class Keystore {
private WalletModel walletModel = WalletModel.SPARROW; private WalletModel walletModel = WalletModel.SPARROW;
private KeyDerivation keyDerivation; private KeyDerivation keyDerivation;
private ExtendedKey extendedPublicKey; private ExtendedKey extendedPublicKey;
private PaymentCode externalPaymentCode;
private MasterPrivateExtendedKey masterPrivateExtendedKey;
private DeterministicSeed seed; private DeterministicSeed seed;
//For BIP47 keystores - not persisted but must be unencrypted to generate keys
private transient ExtendedKey bip47ExtendedPrivateKey;
//Avoid performing repeated expensive seed derivation checks
private transient boolean extendedPublicKeyChecked;
public Keystore() { public Keystore() {
this(DEFAULT_LABEL); this(DEFAULT_LABEL);
} }
@ -32,7 +49,7 @@ public class Keystore {
} }
public String getScriptName() { public String getScriptName() {
return label.replace(" ", "").toLowerCase(); return label.replace(" ", "");
} }
public void setLabel(String label) { public void setLabel(String label) {
@ -69,6 +86,31 @@ public class Keystore {
public void setExtendedPublicKey(ExtendedKey extendedPublicKey) { public void setExtendedPublicKey(ExtendedKey extendedPublicKey) {
this.extendedPublicKey = extendedPublicKey; this.extendedPublicKey = extendedPublicKey;
this.extendedPublicKeyChecked = false;
}
public PaymentCode getExternalPaymentCode() {
return externalPaymentCode;
}
public void setExternalPaymentCode(PaymentCode paymentCode) {
this.externalPaymentCode = paymentCode;
}
public boolean hasMasterPrivateExtendedKey() {
return masterPrivateExtendedKey != null;
}
public MasterPrivateExtendedKey getMasterPrivateExtendedKey() {
return masterPrivateExtendedKey;
}
public void setMasterPrivateExtendedKey(MasterPrivateExtendedKey masterPrivateExtendedKey) {
this.masterPrivateExtendedKey = masterPrivateExtendedKey;
}
public boolean hasSeed() {
return seed != null;
} }
public DeterministicSeed getSeed() { public DeterministicSeed getSeed() {
@ -79,16 +121,60 @@ public class Keystore {
this.seed = seed; this.seed = seed;
} }
public boolean hasMasterPrivateKey() {
return hasSeed() || hasMasterPrivateExtendedKey();
}
public boolean hasPrivateKey() {
return hasMasterPrivateKey() || (source == KeystoreSource.SW_PAYMENT_CODE && bip47ExtendedPrivateKey != null);
}
public boolean needsPassphrase() {
if(seed != null) {
return seed.needsPassphrase();
}
return false;
}
public PaymentCode getPaymentCode() {
DeterministicKey bip47Key = bip47ExtendedPrivateKey.getKey();
return new PaymentCode(bip47Key.getPubKey(), bip47Key.getChainCode());
}
public ExtendedKey getBip47ExtendedPrivateKey() {
return bip47ExtendedPrivateKey;
}
public void setBip47ExtendedPrivateKey(ExtendedKey bip47ExtendedPrivateKey) {
this.bip47ExtendedPrivateKey = bip47ExtendedPrivateKey;
}
public PaymentAddress getPaymentAddress(KeyPurpose keyPurpose, int index) {
List<ChildNumber> derivation = keyDerivation.getDerivation();
ChildNumber derivationStart = keyDerivation.getDerivation().isEmpty() ? ChildNumber.ZERO_HARDENED : keyDerivation.getDerivation().get(derivation.size() - 1);
DeterministicKey privateKey = bip47ExtendedPrivateKey.getKey(List.of(derivationStart, new ChildNumber(keyPurpose == KeyPurpose.SEND ? 0 : index)));
return new PaymentAddress(externalPaymentCode, keyPurpose == KeyPurpose.SEND ? index : 0, privateKey.getPrivKeyBytes());
}
public DeterministicKey getMasterPrivateKey() throws MnemonicException { public DeterministicKey getMasterPrivateKey() throws MnemonicException {
if(seed == null) { if(seed == null && masterPrivateExtendedKey == null) {
throw new IllegalArgumentException("Keystore does not contain a seed"); throw new IllegalArgumentException("Keystore does not contain a master private key, or seed to derive one from");
} }
if(seed.isEncrypted()) { if(seed != null) {
throw new IllegalArgumentException("Seed is encrypted"); if(seed.isEncrypted()) {
throw new IllegalArgumentException("Seed is encrypted");
}
return HDKeyDerivation.createMasterPrivateKey(seed.getSeedBytes());
} }
return HDKeyDerivation.createMasterPrivateKey(seed.getSeedBytes()); if(masterPrivateExtendedKey.isEncrypted()) {
throw new IllegalArgumentException("Master private key is encrypted");
}
return masterPrivateExtendedKey.getPrivateKey();
} }
public ExtendedKey getExtendedMasterPrivateKey() throws MnemonicException { public ExtendedKey getExtendedMasterPrivateKey() throws MnemonicException {
@ -107,22 +193,44 @@ public class Keystore {
return ExtendedKey.fromDescriptor(xprv.toString()); return ExtendedKey.fromDescriptor(xprv.toString());
} }
public DeterministicKey getKey(WalletNode walletNode) throws MnemonicException { public ECKey getKey(WalletNode walletNode) throws MnemonicException {
return getKey(walletNode.getKeyPurpose(), walletNode.getIndex()); if(source == KeystoreSource.SW_PAYMENT_CODE) {
} try {
if(walletNode.getKeyPurpose() != KeyPurpose.RECEIVE) {
throw new IllegalArgumentException("Cannot get private key for non-receive chain");
}
PaymentAddress paymentAddress = getPaymentAddress(walletNode.getKeyPurpose(), walletNode.getIndex());
return paymentAddress.getReceiveECKey();
} catch(IllegalArgumentException e) {
throw new IllegalArgumentException("Invalid payment code " + externalPaymentCode, e);
} catch(Exception e) {
log.error("Cannot get receive private key at index " + walletNode.getIndex() + " for payment code " + externalPaymentCode, e);
}
}
public DeterministicKey getKey(KeyPurpose keyPurpose, int keyIndex) throws MnemonicException {
ExtendedKey extendedPrivateKey = getExtendedPrivateKey(); ExtendedKey extendedPrivateKey = getExtendedPrivateKey();
List<ChildNumber> derivation = List.of(extendedPrivateKey.getKeyChildNumber(), keyPurpose.getPathIndex(), new ChildNumber(keyIndex)); List<ChildNumber> derivation = new ArrayList<>();
derivation.add(extendedPrivateKey.getKeyChildNumber());
derivation.addAll(walletNode.getDerivation());
return extendedPrivateKey.getKey(derivation); return extendedPrivateKey.getKey(derivation);
} }
public DeterministicKey getPubKey(WalletNode walletNode) { public ECKey getPubKey(WalletNode walletNode) {
return getPubKey(walletNode.getKeyPurpose(), walletNode.getIndex()); if(source == KeystoreSource.SW_PAYMENT_CODE) {
} try {
PaymentAddress paymentAddress = getPaymentAddress(walletNode.getKeyPurpose(), walletNode.getIndex());
return walletNode.getKeyPurpose() == KeyPurpose.RECEIVE ? ECKey.fromPublicOnly(paymentAddress.getReceiveECKey()) : paymentAddress.getSendECKey();
} catch(IllegalArgumentException e) {
throw new IllegalArgumentException("Invalid payment code " + externalPaymentCode, e);
} catch(Exception e) {
log.error("Cannot get receive private key at index " + walletNode.getIndex() + " for payment code " + externalPaymentCode, e);
}
}
public DeterministicKey getPubKey(KeyPurpose keyPurpose, int keyIndex) { List<ChildNumber> derivation = new ArrayList<>();
List<ChildNumber> derivation = List.of(extendedPublicKey.getKeyChildNumber(), keyPurpose.getPathIndex(), new ChildNumber(keyIndex)); derivation.add(extendedPublicKey.getKeyChildNumber());
derivation.addAll(walletNode.getDerivation());
return extendedPublicKey.getKey(derivation); return extendedPublicKey.getKey(derivation);
} }
@ -178,11 +286,11 @@ public class Keystore {
} }
if(source == KeystoreSource.SW_SEED) { if(source == KeystoreSource.SW_SEED) {
if(seed == null) { if(seed == null && masterPrivateExtendedKey == null) {
throw new InvalidKeystoreException("Source of " + source + " but no seed is present"); throw new InvalidKeystoreException("Source of " + source + " but no seed or master private key is present");
} }
if(!seed.isEncrypted()) { if(!extendedPublicKeyChecked && ((seed != null && !seed.isEncrypted()) || (masterPrivateExtendedKey != null && !masterPrivateExtendedKey.isEncrypted()))) {
try { try {
List<ChildNumber> derivation = getKeyDerivation().getDerivation(); List<ChildNumber> derivation = getKeyDerivation().getDerivation();
DeterministicKey derivedKey = getExtendedMasterPrivateKey().getKey(derivation); DeterministicKey derivedKey = getExtendedMasterPrivateKey().getKey(derivation);
@ -191,15 +299,27 @@ public class Keystore {
if(!xpub.equals(getExtendedPublicKey())) { if(!xpub.equals(getExtendedPublicKey())) {
throw new InvalidKeystoreException("Specified extended public key does not match public key derived from seed"); throw new InvalidKeystoreException("Specified extended public key does not match public key derived from seed");
} }
extendedPublicKeyChecked = true;
} catch(MnemonicException e) { } catch(MnemonicException e) {
throw new InvalidKeystoreException("Invalid mnemonic specified for seed", e); throw new InvalidKeystoreException("Invalid mnemonic specified for seed", e);
} }
} }
} }
if(source == KeystoreSource.SW_PAYMENT_CODE) {
if(externalPaymentCode == null) {
throw new InvalidKeystoreException("Source of " + source + " but no payment code is present");
}
if(bip47ExtendedPrivateKey == null) {
throw new InvalidKeystoreException("Source of " + source + " but no extended private key is present");
}
}
} }
public Keystore copy() { public Keystore copy() {
Keystore copy = new Keystore(label); Keystore copy = new Keystore(label);
copy.setId(getId());
copy.setSource(source); copy.setSource(source);
copy.setWalletModel(walletModel); copy.setWalletModel(walletModel);
if(keyDerivation != null) { if(keyDerivation != null) {
@ -208,59 +328,94 @@ public class Keystore {
if(extendedPublicKey != null) { if(extendedPublicKey != null) {
copy.setExtendedPublicKey(extendedPublicKey.copy()); copy.setExtendedPublicKey(extendedPublicKey.copy());
} }
if(masterPrivateExtendedKey != null) {
copy.setMasterPrivateExtendedKey(masterPrivateExtendedKey.copy());
}
if(seed != null) { if(seed != null) {
copy.setSeed(seed.copy()); copy.setSeed(seed.copy());
} }
if(externalPaymentCode != null) {
copy.setExternalPaymentCode(externalPaymentCode.copy());
}
if(bip47ExtendedPrivateKey != null) {
copy.setBip47ExtendedPrivateKey(bip47ExtendedPrivateKey.copy());
}
return copy; return copy;
} }
public static Keystore fromSeed(DeterministicSeed seed, List<ChildNumber> derivation) throws MnemonicException { public static Keystore fromSeed(DeterministicSeed seed, List<ChildNumber> derivation) throws MnemonicException {
Keystore keystore = new Keystore(); Keystore keystore = new Keystore();
keystore.setSeed(seed); keystore.setSeed(seed);
keystore.setLabel(seed.getType().name());
rederiveKeystoreFromMaster(keystore, derivation);
return keystore;
}
public static Keystore fromMasterPrivateExtendedKey(MasterPrivateExtendedKey masterPrivateExtendedKey, List<ChildNumber> derivation) throws MnemonicException {
Keystore keystore = new Keystore();
keystore.setMasterPrivateExtendedKey(masterPrivateExtendedKey);
keystore.setLabel("Master Key");
rederiveKeystoreFromMaster(keystore, derivation);
return keystore;
}
private static void rederiveKeystoreFromMaster(Keystore keystore, List<ChildNumber> derivation) throws MnemonicException {
ExtendedKey xprv = keystore.getExtendedMasterPrivateKey(); ExtendedKey xprv = keystore.getExtendedMasterPrivateKey();
String masterFingerprint = Utils.bytesToHex(xprv.getKey().getFingerprint()); String masterFingerprint = Utils.bytesToHex(xprv.getKey().getFingerprint());
DeterministicKey derivedKey = xprv.getKey(derivation); DeterministicKey derivedKey = xprv.getKey(derivation);
DeterministicKey derivedKeyPublicOnly = derivedKey.dropPrivateBytes().dropParent(); DeterministicKey derivedKeyPublicOnly = derivedKey.dropPrivateBytes().dropParent();
ExtendedKey xpub = new ExtendedKey(derivedKeyPublicOnly, derivedKey.getParentFingerprint(), derivation.isEmpty() ? ChildNumber.ZERO : derivation.get(derivation.size() - 1)); ExtendedKey xpub = new ExtendedKey(derivedKeyPublicOnly, derivedKey.getParentFingerprint(), derivation.isEmpty() ? ChildNumber.ZERO : derivation.get(derivation.size() - 1));
keystore.setLabel(seed.getType().name());
keystore.setSource(KeystoreSource.SW_SEED); keystore.setSource(KeystoreSource.SW_SEED);
keystore.setWalletModel(WalletModel.SPARROW); keystore.setWalletModel(WalletModel.SPARROW);
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, KeyDerivation.writePath(derivation))); keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, KeyDerivation.writePath(derivation)));
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(xpub.toString())); keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(xpub.toString()));
return keystore; int account = ScriptType.getScriptTypesForPolicyType(PolicyType.SINGLE).stream()
} .mapToInt(scriptType -> scriptType.getAccount(keystore.getKeyDerivation().getDerivationPath())).filter(idx -> idx > -1).findFirst().orElse(0);
List<ChildNumber> bip47Derivation = KeyDerivation.getBip47Derivation(account);
public boolean hasSeed() { DeterministicKey bip47Key = xprv.getKey(bip47Derivation);
return seed != null; ExtendedKey bip47ExtendedPrivateKey = new ExtendedKey(bip47Key, bip47Key.getParentFingerprint(), bip47Derivation.get(bip47Derivation.size() - 1));
keystore.setBip47ExtendedPrivateKey(ExtendedKey.fromDescriptor(bip47ExtendedPrivateKey.toString()));
} }
public boolean isEncrypted() { public boolean isEncrypted() {
return seed != null && seed.isEncrypted(); return (seed != null && seed.isEncrypted()) || (masterPrivateExtendedKey != null && masterPrivateExtendedKey.isEncrypted());
} }
public void encrypt(Key key) { public void encrypt(Key key) {
if(hasSeed() && !seed.isEncrypted()) { if(hasSeed() && !seed.isEncrypted()) {
seed = seed.encrypt(key); seed = seed.encrypt(key);
} }
if(hasMasterPrivateExtendedKey() && !masterPrivateExtendedKey.isEncrypted()) {
masterPrivateExtendedKey = masterPrivateExtendedKey.encrypt(key);
}
} }
public void decrypt(CharSequence password) { public void decrypt(CharSequence password) {
if(hasSeed() && seed.isEncrypted()) { if(hasSeed() && seed.isEncrypted()) {
seed = seed.decrypt(password); seed = seed.decrypt(password);
} }
if(hasMasterPrivateExtendedKey() && masterPrivateExtendedKey.isEncrypted()) {
masterPrivateExtendedKey = masterPrivateExtendedKey.decrypt(password);
}
} }
public void decrypt(Key key) { public void decrypt(Key key) {
if(hasSeed() && seed.isEncrypted()) { if(hasSeed() && seed.isEncrypted()) {
seed = seed.decrypt(key); seed = seed.decrypt(key);
} }
if(hasMasterPrivateExtendedKey() && masterPrivateExtendedKey.isEncrypted()) {
masterPrivateExtendedKey = masterPrivateExtendedKey.decrypt(key);
}
} }
public void clearPrivate() { public void clearPrivate() {
if(hasSeed()) { if(hasSeed()) {
seed.clear(); seed.clear();
} }
if(hasMasterPrivateExtendedKey()) {
masterPrivateExtendedKey.clear();
}
} }
} }

View file

@ -1,9 +1,13 @@
package com.sparrowwallet.drongo.wallet; package com.sparrowwallet.drongo.wallet;
public enum KeystoreSource { public enum KeystoreSource {
HW_USB("Connected Hardware Wallet"), HW_AIRGAPPED("Airgapped Hardware Wallet"), SW_SEED("Software Wallet"), SW_WATCH("Watch Only Wallet"); HW_USB("Connected Hardware Wallet"),
HW_AIRGAPPED("Airgapped Hardware Wallet"),
SW_SEED("Software Wallet"),
SW_WATCH("Watch Only Wallet"),
SW_PAYMENT_CODE("Payment Code Wallet");
private String displayName; private final String displayName;
KeystoreSource(String displayName) { KeystoreSource(String displayName) {
this.displayName = displayName; this.displayName = displayName;

View file

@ -5,7 +5,7 @@ import com.sparrowwallet.drongo.protocol.Transaction;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class KnapsackUtxoSelector implements UtxoSelector { public class KnapsackUtxoSelector extends SingleSetUtxoSelector {
private static final long MIN_CHANGE = Transaction.SATOSHIS_PER_BITCOIN / 1000; private static final long MIN_CHANGE = Transaction.SATOSHIS_PER_BITCOIN / 1000;
private final long noInputsFee; private final long noInputsFee;

View file

@ -0,0 +1,144 @@
package com.sparrowwallet.drongo.wallet;
import com.sparrowwallet.drongo.ExtendedKey;
import com.sparrowwallet.drongo.crypto.*;
import java.nio.ByteBuffer;
import java.util.Arrays;
public class MasterPrivateExtendedKey extends Persistable implements EncryptableItem {
private final byte[] privateKey;
private final byte[] chainCode;
private final EncryptedData encryptedKey;
public MasterPrivateExtendedKey(byte[] privateKey, byte[] chainCode) {
this.privateKey = privateKey;
this.chainCode = chainCode;
this.encryptedKey = null;
}
public MasterPrivateExtendedKey(EncryptedData encryptedKey) {
this.privateKey = null;
this.chainCode = null;
this.encryptedKey = encryptedKey;
}
public DeterministicKey getPrivateKey() {
return HDKeyDerivation.createMasterPrivKeyFromBytes(privateKey, chainCode);
}
public ExtendedKey getExtendedPrivateKey() {
return new ExtendedKey(getPrivateKey(), new byte[4], ChildNumber.ZERO);
}
@Override
public boolean isEncrypted() {
if((privateKey != null || chainCode != null) && encryptedKey != null) {
throw new IllegalStateException("Cannot be in a encrypted and unencrypted state");
}
return encryptedKey != null;
}
@Override
public byte[] getSecretBytes() {
if(privateKey == null || chainCode == null) {
throw new IllegalStateException("Cannot get secret bytes for null or encrypted key");
}
ByteBuffer byteBuffer = ByteBuffer.allocate(64);
byteBuffer.put(privateKey);
byteBuffer.put(chainCode);
return byteBuffer.array();
}
@Override
public EncryptedData getEncryptedData() {
return encryptedKey;
}
@Override
public EncryptionType getEncryptionType() {
return new EncryptionType(EncryptionType.Deriver.ARGON2, EncryptionType.Crypter.AES_CBC_PKCS7);
}
@Override
public long getCreationTimeSeconds() {
return 0;
}
public MasterPrivateExtendedKey encrypt(Key key) {
if(encryptedKey != null) {
throw new IllegalArgumentException("Trying to encrypt twice");
}
if(privateKey == null || chainCode == null) {
throw new IllegalArgumentException("Private key data missing so cannot encrypt");
}
KeyCrypter keyCrypter = getEncryptionType().getCrypter().getKeyCrypter();
byte[] secretBytes = getSecretBytes();
EncryptedData encryptedKeyData = keyCrypter.encrypt(secretBytes, null, key);
Arrays.fill(secretBytes != null ? secretBytes : new byte[0], (byte)0);
MasterPrivateExtendedKey mpek = new MasterPrivateExtendedKey(encryptedKeyData);
mpek.setId(getId());
return mpek;
}
public MasterPrivateExtendedKey decrypt(CharSequence password) {
if(!isEncrypted()) {
throw new IllegalStateException("Cannot decrypt unencrypted master private key");
}
KeyDeriver keyDeriver = getEncryptionType().getDeriver().getKeyDeriver(encryptedKey.getKeySalt());
Key key = keyDeriver.deriveKey(password);
MasterPrivateExtendedKey mpek = decrypt(key);
mpek.setId(getId());
key.clear();
return mpek;
}
public MasterPrivateExtendedKey decrypt(Key key) {
if(!isEncrypted()) {
throw new IllegalStateException("Cannot decrypt unencrypted master private key");
}
KeyCrypter keyCrypter = getEncryptionType().getCrypter().getKeyCrypter();
byte[] decrypted = keyCrypter.decrypt(encryptedKey, key);
try {
MasterPrivateExtendedKey mpek = new MasterPrivateExtendedKey(Arrays.copyOfRange(decrypted, 0, 32), Arrays.copyOfRange(decrypted, 32, 64));
mpek.setId(getId());
return mpek;
} finally {
Arrays.fill(decrypted, (byte)0);
}
}
public MasterPrivateExtendedKey copy() {
MasterPrivateExtendedKey copy;
if(isEncrypted()) {
copy = new MasterPrivateExtendedKey(encryptedKey.copy());
} else {
copy = new MasterPrivateExtendedKey(Arrays.copyOf(privateKey, 32), Arrays.copyOf(chainCode, 32));
}
copy.setId(getId());
return copy;
}
public void clear() {
if(privateKey != null) {
Arrays.fill(privateKey, (byte)0);
}
if(chainCode != null) {
Arrays.fill(chainCode, (byte)0);
}
}
public MasterPrivateExtendedKey fromXprv(ExtendedKey xprv) {
return new MasterPrivateExtendedKey(xprv.getKey().getPrivKeyBytes(), xprv.getKey().getChainCode());
}
}

View file

@ -3,9 +3,9 @@ package com.sparrowwallet.drongo.wallet;
import java.util.Collection; import java.util.Collection;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class MaxUtxoSelector implements UtxoSelector { public class MaxUtxoSelector extends SingleSetUtxoSelector {
@Override @Override
public Collection<BlockTransactionHashIndex> select(long targetValue, Collection<OutputGroup> candidates) { public Collection<BlockTransactionHashIndex> select(long targetValue, Collection<OutputGroup> candidates) {
return candidates.stream().flatMap(outputGroup -> outputGroup.getUtxos().stream()).collect(Collectors.toUnmodifiableList()); return candidates.stream().filter(outputGroup -> outputGroup.getEffectiveValue() >= 0).flatMap(outputGroup -> outputGroup.getUtxos().stream()).collect(Collectors.toUnmodifiableList());
} }
} }

View file

@ -0,0 +1,96 @@
package com.sparrowwallet.drongo.wallet;
import java.io.File;
public class MixConfig extends Persistable {
private String scode;
private Boolean mixOnStartup;
private String indexRange;
private File mixToWalletFile;
private String mixToWalletName;
private Integer minMixes;
private int receiveIndex;
private int changeIndex;
public MixConfig() {
}
public MixConfig(String scode, Boolean mixOnStartup, String indexRange, File mixToWalletFile, String mixToWalletName, Integer minMixes, int receiveIndex, int changeIndex) {
this.scode = scode;
this.mixOnStartup = mixOnStartup;
this.indexRange = indexRange;
this.mixToWalletFile = mixToWalletFile;
this.mixToWalletName = mixToWalletName;
this.minMixes = minMixes;
this.receiveIndex = receiveIndex;
this.changeIndex = changeIndex;
}
public String getScode() {
return scode;
}
public void setScode(String scode) {
this.scode = scode;
}
public Boolean getMixOnStartup() {
return mixOnStartup;
}
public void setMixOnStartup(Boolean mixOnStartup) {
this.mixOnStartup = mixOnStartup;
}
public String getIndexRange() {
return indexRange;
}
public void setIndexRange(String indexRange) {
this.indexRange = indexRange;
}
public File getMixToWalletFile() {
return mixToWalletFile;
}
public void setMixToWalletFile(File mixToWalletFile) {
this.mixToWalletFile = mixToWalletFile;
}
public String getMixToWalletName() {
return mixToWalletName;
}
public void setMixToWalletName(String mixToWalletName) {
this.mixToWalletName = mixToWalletName;
}
public Integer getMinMixes() {
return minMixes;
}
public void setMinMixes(Integer minMixes) {
this.minMixes = minMixes;
}
public int getReceiveIndex() {
return receiveIndex;
}
public void setReceiveIndex(int receiveIndex) {
this.receiveIndex = receiveIndex;
}
public int getChangeIndex() {
return changeIndex;
}
public void setChangeIndex(int changeIndex) {
this.changeIndex = changeIndex;
}
public MixConfig copy() {
return new MixConfig(scode, mixOnStartup, indexRange, mixToWalletFile, mixToWalletName, minMixes, receiveIndex, changeIndex);
}
}

View file

@ -39,4 +39,16 @@ public class MnemonicException extends Exception {
this.badWord = badWord; this.badWord = badWord;
} }
} }
/**
* Thrown when the mnemonic is valid, but for for the expected standard
*/
public static class MnemonicTypeException extends MnemonicException {
public final DeterministicSeed.Type invalidType;
public MnemonicTypeException(DeterministicSeed.Type invalidType) {
super();
this.invalidType = invalidType;
}
}
} }

View file

@ -1,5 +1,7 @@
package com.sparrowwallet.drongo.wallet; package com.sparrowwallet.drongo.wallet;
import com.sparrowwallet.drongo.protocol.ScriptType;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -7,6 +9,7 @@ import static com.sparrowwallet.drongo.protocol.Transaction.WITNESS_SCALE_FACTOR
public class OutputGroup { public class OutputGroup {
private final List<BlockTransactionHashIndex> utxos = new ArrayList<>(); private final List<BlockTransactionHashIndex> utxos = new ArrayList<>();
private final ScriptType scriptType;
private final int walletBlockHeight; private final int walletBlockHeight;
private final long inputWeightUnits; private final long inputWeightUnits;
private final double feeRate; private final double feeRate;
@ -17,15 +20,17 @@ public class OutputGroup {
private long longTermFee = 0; private long longTermFee = 0;
private int depth = Integer.MAX_VALUE; private int depth = Integer.MAX_VALUE;
private boolean allInputsFromWallet = true; private boolean allInputsFromWallet = true;
private boolean spendLast;
public OutputGroup(int walletBlockHeight, long inputWeightUnits, double feeRate, double longTermFeeRate) { public OutputGroup(ScriptType scriptType, int walletBlockHeight, long inputWeightUnits, double feeRate, double longTermFeeRate) {
this.scriptType = scriptType;
this.walletBlockHeight = walletBlockHeight; this.walletBlockHeight = walletBlockHeight;
this.inputWeightUnits = inputWeightUnits; this.inputWeightUnits = inputWeightUnits;
this.feeRate = feeRate; this.feeRate = feeRate;
this.longTermFeeRate = longTermFeeRate; this.longTermFeeRate = longTermFeeRate;
} }
public void add(BlockTransactionHashIndex utxo, boolean allInputsFromWallet) { public void add(BlockTransactionHashIndex utxo, boolean allInputsFromWallet, boolean spendLast) {
utxos.add(utxo); utxos.add(utxo);
value += utxo.getValue(); value += utxo.getValue();
effectiveValue += utxo.getValue() - (long)(inputWeightUnits * feeRate / WITNESS_SCALE_FACTOR); effectiveValue += utxo.getValue() - (long)(inputWeightUnits * feeRate / WITNESS_SCALE_FACTOR);
@ -33,6 +38,7 @@ public class OutputGroup {
longTermFee += (long)(inputWeightUnits * longTermFeeRate / WITNESS_SCALE_FACTOR); longTermFee += (long)(inputWeightUnits * longTermFeeRate / WITNESS_SCALE_FACTOR);
depth = utxo.getHeight() <= 0 ? 0 : Math.min(depth, walletBlockHeight - utxo.getHeight() + 1); depth = utxo.getHeight() <= 0 ? 0 : Math.min(depth, walletBlockHeight - utxo.getHeight() + 1);
this.allInputsFromWallet &= allInputsFromWallet; this.allInputsFromWallet &= allInputsFromWallet;
this.spendLast |= spendLast;
} }
public void remove(BlockTransactionHashIndex utxo) { public void remove(BlockTransactionHashIndex utxo) {
@ -48,6 +54,10 @@ public class OutputGroup {
return utxos; return utxos;
} }
public ScriptType getScriptType() {
return scriptType;
}
public long getValue() { public long getValue() {
return value; return value;
} }
@ -72,21 +82,27 @@ public class OutputGroup {
return allInputsFromWallet; return allInputsFromWallet;
} }
public boolean isSpendLast() {
return spendLast;
}
public static class Filter { public static class Filter {
private final int minWalletConfirmations; private final int minWalletConfirmations;
private final int minExternalConfirmations; private final int minExternalConfirmations;
private final boolean includeSpendLast;
public Filter(int minWalletConfirmations, int minExternalConfirmations) { public Filter(int minWalletConfirmations, int minExternalConfirmations, boolean includeSpendLast) {
this.minWalletConfirmations = minWalletConfirmations; this.minWalletConfirmations = minWalletConfirmations;
this.minExternalConfirmations = minExternalConfirmations; this.minExternalConfirmations = minExternalConfirmations;
this.includeSpendLast = includeSpendLast;
} }
public boolean isEligible(OutputGroup outputGroup) { public boolean isEligible(OutputGroup outputGroup) {
if(outputGroup.isAllInputsFromWallet()) { if(outputGroup.isAllInputsFromWallet()) {
return outputGroup.getDepth() >= minWalletConfirmations; return outputGroup.getDepth() >= minWalletConfirmations && (includeSpendLast || !outputGroup.isSpendLast());
} }
return outputGroup.getDepth() >= minExternalConfirmations; return outputGroup.getDepth() >= minExternalConfirmations && (includeSpendLast || !outputGroup.isSpendLast());
} }
} }
} }

View file

@ -7,12 +7,18 @@ public class Payment {
private String label; private String label;
private long amount; private long amount;
private boolean sendMax; private boolean sendMax;
private Type type;
public Payment(Address address, String label, long amount, boolean sendMax) { public Payment(Address address, String label, long amount, boolean sendMax) {
this(address, label, amount, sendMax, Type.DEFAULT);
}
public Payment(Address address, String label, long amount, boolean sendMax, Type type) {
this.address = address; this.address = address;
this.label = label; this.label = label;
this.amount = amount; this.amount = amount;
this.sendMax = sendMax; this.sendMax = sendMax;
this.type = type;
} }
public Address getAddress() { public Address getAddress() {
@ -46,4 +52,16 @@ public class Payment {
public void setSendMax(boolean sendMax) { public void setSendMax(boolean sendMax) {
this.sendMax = sendMax; this.sendMax = sendMax;
} }
public Type getType() {
return type;
}
public void setType(Type type) {
this.type = type;
}
public enum Type {
DEFAULT, WHIRLPOOL_FEE, FAKE_MIX, MIX;
}
} }

View file

@ -0,0 +1,13 @@
package com.sparrowwallet.drongo.wallet;
public class Persistable {
private Long id;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
}

View file

@ -5,11 +5,18 @@ import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class PresetUtxoSelector implements UtxoSelector { public class PresetUtxoSelector extends SingleSetUtxoSelector {
private final Collection<BlockTransactionHashIndex> presetUtxos; private final Collection<BlockTransactionHashIndex> presetUtxos;
private final boolean maintainOrder;
public PresetUtxoSelector(Collection<BlockTransactionHashIndex> presetUtxos) { public PresetUtxoSelector(Collection<BlockTransactionHashIndex> presetUtxos) {
this.presetUtxos = presetUtxos; this.presetUtxos = presetUtxos;
this.maintainOrder = false;
}
public PresetUtxoSelector(Collection<BlockTransactionHashIndex> presetUtxos, boolean maintainOrder) {
this.presetUtxos = presetUtxos;
this.maintainOrder = maintainOrder;
} }
@Override @Override
@ -26,10 +33,19 @@ public class PresetUtxoSelector implements UtxoSelector {
} }
} }
if(maintainOrder && utxos.containsAll(presetUtxos)) {
return presetUtxos;
}
return utxos; return utxos;
} }
public Collection<BlockTransactionHashIndex> getPresetUtxos() { public Collection<BlockTransactionHashIndex> getPresetUtxos() {
return presetUtxos; return presetUtxos;
} }
@Override
public boolean shuffleInputs() {
return !maintainOrder;
}
} }

View file

@ -4,7 +4,7 @@ import java.math.BigInteger;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class PriorityUtxoSelector implements UtxoSelector { public class PriorityUtxoSelector extends SingleSetUtxoSelector {
private final int currentBlockHeight; private final int currentBlockHeight;
public PriorityUtxoSelector(int currentBlockHeight) { public PriorityUtxoSelector(int currentBlockHeight) {

View file

@ -0,0 +1,58 @@
package com.sparrowwallet.drongo.wallet;
import com.sparrowwallet.drongo.Utils;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class SeedQR {
public static DeterministicSeed getSeed(String seedQr) {
if(seedQr.length() < 48 || seedQr.length() > 96 || seedQr.length() % 4 > 0) {
throw new IllegalArgumentException("Invalid SeedQR length: " + seedQr.length());
}
if(!seedQr.chars().allMatch(c -> c >= '0' && c <= '9')) {
throw new IllegalArgumentException("SeedQR contains non-digit characters: " + seedQr);
}
List<Integer> indexes = IntStream.iterate(0, i -> i + 4).limit((int)Math.ceil(seedQr.length() / 4.0))
.mapToObj(i -> seedQr.substring(i, Math.min(i + 4, seedQr.length())))
.map(Integer::parseInt)
.collect(Collectors.toList());
List<String> words = new ArrayList<>(indexes.size());
for(Integer index : indexes) {
words.add(Bip39MnemonicCode.INSTANCE.getWordList().get(index));
}
return new DeterministicSeed(words, null, System.currentTimeMillis(), DeterministicSeed.Type.BIP39);
}
public static DeterministicSeed getSeed(byte[] compactSeedQr) {
if(compactSeedQr[0] != 0x41 && compactSeedQr[0] != 0x42) {
throw new IllegalArgumentException("Invalid CompactSeedQR header");
}
if(compactSeedQr.length < 19) {
throw new IllegalArgumentException("Invalid CompactSeedQR length");
}
String qrHex = Utils.bytesToHex(compactSeedQr);
String seedHex;
if(qrHex.endsWith("0ec")) {
seedHex = qrHex.substring(3, qrHex.length() - 3);
} else {
seedHex = qrHex.substring(3, qrHex.length() - 1);
}
byte[] seed = Utils.hexToBytes(seedHex);
if(seed.length < 16 || seed.length > 32 || seed.length % 4 > 0) {
throw new IllegalArgumentException("Invalid CompactSeedQR length: " + compactSeedQr.length);
}
return new DeterministicSeed(seed, null, System.currentTimeMillis());
}
}

View file

@ -0,0 +1,13 @@
package com.sparrowwallet.drongo.wallet;
import java.util.Collection;
import java.util.List;
public abstract class SingleSetUtxoSelector implements UtxoSelector {
@Override
public List<Collection<BlockTransactionHashIndex>> selectSets(long targetValue, Collection<OutputGroup> candidates) {
return List.of(select(targetValue, candidates));
}
public abstract Collection<BlockTransactionHashIndex> select(long targetValue, Collection<OutputGroup> candidates);
}

View file

@ -0,0 +1,70 @@
package com.sparrowwallet.drongo.wallet;
import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.protocol.ScriptType;
import java.util.List;
public enum StandardAccount {
ACCOUNT_0("Account #0", new ChildNumber(0, true)),
ACCOUNT_1("Account #1", new ChildNumber(1, true)),
ACCOUNT_2("Account #2", new ChildNumber(2, true)),
ACCOUNT_3("Account #3", new ChildNumber(3, true)),
ACCOUNT_4("Account #4", new ChildNumber(4, true)),
ACCOUNT_5("Account #5", new ChildNumber(5, true)),
ACCOUNT_6("Account #6", new ChildNumber(6, true)),
ACCOUNT_7("Account #7", new ChildNumber(7, true)),
ACCOUNT_8("Account #8", new ChildNumber(8, true)),
ACCOUNT_9("Account #9", new ChildNumber(9, true)),
WHIRLPOOL_PREMIX("Premix", new ChildNumber(2147483645, true), ScriptType.P2WPKH, null),
WHIRLPOOL_POSTMIX("Postmix", new ChildNumber(2147483646, true), ScriptType.P2WPKH, Wallet.DEFAULT_LOOKAHEAD * 2),
WHIRLPOOL_BADBANK("Badbank", new ChildNumber(2147483644, true), ScriptType.P2WPKH, null);
public static final List<StandardAccount> MIXABLE_ACCOUNTS = List.of(ACCOUNT_0, WHIRLPOOL_BADBANK);
public static final List<StandardAccount> WHIRLPOOL_ACCOUNTS = List.of(WHIRLPOOL_PREMIX, WHIRLPOOL_POSTMIX, WHIRLPOOL_BADBANK);
public static final List<StandardAccount> WHIRLPOOL_MIX_ACCOUNTS = List.of(WHIRLPOOL_PREMIX, WHIRLPOOL_POSTMIX);
StandardAccount(String name, ChildNumber childNumber) {
this.name = name;
this.childNumber = childNumber;
this.requiredScriptType = null;
this.minimumGapLimit = null;
}
StandardAccount(String name, ChildNumber childNumber, ScriptType requiredScriptType, Integer minimumGapLimit) {
this.name = name;
this.childNumber = childNumber;
this.requiredScriptType = requiredScriptType;
this.minimumGapLimit = minimumGapLimit;
}
private final String name;
private final ChildNumber childNumber;
private final ScriptType requiredScriptType;
private final Integer minimumGapLimit;
public String getName() {
return name;
}
public ChildNumber getChildNumber() {
return childNumber;
}
public int getAccountNumber() {
return childNumber.num();
}
public ScriptType getRequiredScriptType() {
return requiredScriptType;
}
public Integer getMinimumGapLimit() {
return minimumGapLimit;
}
@Override
public String toString() {
return name;
}
}

View file

@ -0,0 +1,93 @@
package com.sparrowwallet.drongo.wallet;
import com.sparrowwallet.drongo.protocol.ScriptType;
import java.util.*;
import java.util.stream.Collectors;
public class StonewallUtxoSelector implements UtxoSelector {
private final ScriptType preferredScriptType;
private final long noInputsFee;
//Use the same seed so the UTXO selection is deterministic
private final Random random = new Random(42);
public StonewallUtxoSelector(ScriptType preferredScriptType, long noInputsFee) {
this.preferredScriptType = preferredScriptType;
this.noInputsFee = noInputsFee;
}
@Override
public List<Collection<BlockTransactionHashIndex>> selectSets(long targetValue, Collection<OutputGroup> candidates) {
long actualTargetValue = targetValue + noInputsFee;
List<OutputGroup> uniqueCandidates = new ArrayList<>();
for(OutputGroup candidate : candidates) {
OutputGroup existingTxGroup = getTransactionAlreadySelected(uniqueCandidates, candidate);
if(existingTxGroup != null) {
if(candidate.getValue() > existingTxGroup.getValue()) {
uniqueCandidates.remove(existingTxGroup);
uniqueCandidates.add(candidate);
}
} else {
uniqueCandidates.add(candidate);
}
}
List<OutputGroup> preferredCandidates = uniqueCandidates.stream().filter(outputGroup -> outputGroup.getScriptType().equals(preferredScriptType)).collect(Collectors.toList());
List<Collection<BlockTransactionHashIndex>> preferredSets = selectSets(targetValue, preferredCandidates, actualTargetValue);
if(!preferredSets.isEmpty()) {
return preferredSets;
}
return selectSets(targetValue, uniqueCandidates, actualTargetValue);
}
private List<Collection<BlockTransactionHashIndex>> selectSets(long targetValue, List<OutputGroup> uniqueCandidates, long actualTargetValue) {
for(int i = 0; i < 15; i++) {
List<OutputGroup> randomized = new ArrayList<>(uniqueCandidates);
Collections.shuffle(randomized, random);
List<OutputGroup> set1 = new ArrayList<>();
long selectedValue1 = getUtxoSet(actualTargetValue, set1, randomized);
List<OutputGroup> set2 = new ArrayList<>();
long selectedValue2 = getUtxoSet(actualTargetValue, set2, randomized);
if(selectedValue1 >= targetValue && selectedValue2 >= targetValue) {
return List.of(getUtxos(set1), getUtxos(set2));
}
}
return Collections.emptyList();
}
private long getUtxoSet(long targetValue, List<OutputGroup> selectedSet, List<OutputGroup> randomized) {
long selectedValue = 0;
while(selectedValue <= targetValue && !randomized.isEmpty()) {
OutputGroup candidate = randomized.remove(0);
selectedSet.add(candidate);
selectedValue = selectedSet.stream().mapToLong(OutputGroup::getEffectiveValue).sum();
}
return selectedValue;
}
private OutputGroup getTransactionAlreadySelected(List<OutputGroup> selected, OutputGroup candidateGroup) {
for(OutputGroup selectedGroup : selected) {
for(BlockTransactionHashIndex selectedUtxo : selectedGroup.getUtxos()) {
for(BlockTransactionHashIndex candidateUtxo : candidateGroup.getUtxos()) {
if(selectedUtxo.getHash().equals(candidateUtxo.getHash())) {
return selectedGroup;
}
}
}
}
return null;
}
private Collection<BlockTransactionHashIndex> getUtxos(List<OutputGroup> set) {
return set.stream().flatMap(outputGroup -> outputGroup.getUtxos().stream()).collect(Collectors.toList());
}
}

View file

@ -0,0 +1,24 @@
package com.sparrowwallet.drongo.wallet;
public class UtxoMixData extends Persistable {
private final int mixesDone;
private final Long expired;
public UtxoMixData(int mixesDone, Long expired) {
this.mixesDone = mixesDone;
this.expired = expired;
}
public int getMixesDone() {
return mixesDone;
}
public Long getExpired() {
return expired;
}
@Override
public String toString() {
return "{mixesDone:" + mixesDone + ", expired: " + expired + "}";
}
}

View file

@ -1,7 +1,11 @@
package com.sparrowwallet.drongo.wallet; package com.sparrowwallet.drongo.wallet;
import java.util.Collection; import java.util.Collection;
import java.util.List;
public interface UtxoSelector { public interface UtxoSelector {
Collection<BlockTransactionHashIndex> select(long targetValue, Collection<OutputGroup> candidates); List<Collection<BlockTransactionHashIndex>> selectSets(long targetValue, Collection<OutputGroup> candidates);
default boolean shuffleInputs() {
return true;
}
} }

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,41 @@
package com.sparrowwallet.drongo.wallet;
public class WalletConfig extends Persistable {
private byte[] iconData;
private boolean userIcon;
private boolean usePayNym;
public WalletConfig() {
}
public WalletConfig(byte[] iconData, boolean userIcon, boolean usePayNym) {
this.iconData = iconData;
this.userIcon = userIcon;
this.usePayNym = usePayNym;
}
public byte[] getIconData() {
return iconData;
}
public boolean isUserIcon() {
return userIcon;
}
public void setIconData(byte[] iconData, boolean userIcon) {
this.iconData = iconData;
this.userIcon = userIcon;
}
public boolean isUsePayNym() {
return usePayNym;
}
public void setUsePayNym(boolean usePayNym) {
this.usePayNym = usePayNym;
}
public WalletConfig copy() {
return new WalletConfig(iconData, userIcon, usePayNym);
}
}

View file

@ -1,10 +1,12 @@
package com.sparrowwallet.drongo.wallet; package com.sparrowwallet.drongo.wallet;
import java.util.Locale;
public enum WalletModel { public enum WalletModel {
SEED, SPARROW, BITCOIN_CORE, ELECTRUM, TREZOR_1, TREZOR_T, COLDCARD, LEDGER_NANO_S, LEDGER_NANO_X, DIGITALBITBOX_01, KEEPKEY, SPECTER_DESKTOP, COBO_VAULT, BITBOX_02, SPECTER_DIY, PASSPORT; SEED, SPARROW, BITCOIN_CORE, ELECTRUM, TREZOR_1, TREZOR_T, COLDCARD, LEDGER_NANO_S, LEDGER_NANO_X, DIGITALBITBOX_01, KEEPKEY, SPECTER_DESKTOP, COBO_VAULT, BITBOX_02, SPECTER_DIY, PASSPORT, BLUE_WALLET, KEYSTONE, SEEDSIGNER, CARAVAN, GORDIAN_SEED_TOOL, JADE, LEDGER_NANO_S_PLUS, EPS;
public static WalletModel getModel(String model) { public static WalletModel getModel(String model) {
return valueOf(model.toUpperCase()); return valueOf(model.toUpperCase(Locale.ROOT));
} }
public String getType() { public String getType() {
@ -12,7 +14,7 @@ public enum WalletModel {
return "trezor"; return "trezor";
} }
if(this == LEDGER_NANO_S || this == LEDGER_NANO_X) { if(this == LEDGER_NANO_S || this == LEDGER_NANO_X || this == LEDGER_NANO_S_PLUS) {
return "ledger"; return "ledger";
} }
@ -20,6 +22,10 @@ public enum WalletModel {
return "digitalbitbox"; return "digitalbitbox";
} }
if(this == BITCOIN_CORE) {
return "bitcoincore";
}
if(this == BITBOX_02) { if(this == BITBOX_02) {
return "bitbox02"; return "bitbox02";
} }
@ -32,11 +38,19 @@ public enum WalletModel {
return "specter"; return "specter";
} }
return this.toString().toLowerCase(); if(this == BLUE_WALLET) {
return "bluewallet";
}
if(this == GORDIAN_SEED_TOOL) {
return "seedtool";
}
return this.toString().toLowerCase(Locale.ROOT);
} }
public boolean alwaysIncludeNonWitnessUtxo() { public boolean alwaysIncludeNonWitnessUtxo() {
if(this == COLDCARD || this == COBO_VAULT || this == PASSPORT) { if(this == COLDCARD || this == COBO_VAULT || this == PASSPORT || this == KEYSTONE || this == GORDIAN_SEED_TOOL) {
return false; return false;
} }
@ -62,7 +76,7 @@ public enum WalletModel {
} }
public String toDisplayString() { public String toDisplayString() {
String line = this.toString().toLowerCase(); String line = this.toString().toLowerCase(Locale.ROOT);
String[] words = line.split("_"); String[] words = line.split("_");
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
for(String word : words) { for(String word : words) {

View file

@ -2,40 +2,68 @@ package com.sparrowwallet.drongo.wallet;
import com.sparrowwallet.drongo.KeyDerivation; import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.KeyPurpose; import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.crypto.ChildNumber; import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.protocol.Script;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class WalletNode implements Comparable<WalletNode> { public class WalletNode extends Persistable implements Comparable<WalletNode> {
private final String derivationPath; private final String derivationPath;
private String label; private String label;
private Address address;
private TreeSet<WalletNode> children = new TreeSet<>(); private TreeSet<WalletNode> children = new TreeSet<>();
private TreeSet<BlockTransactionHashIndex> transactionOutputs = new TreeSet<>(); private TreeSet<BlockTransactionHashIndex> transactionOutputs = new TreeSet<>();
private transient Wallet wallet;
private transient KeyPurpose keyPurpose; private transient KeyPurpose keyPurpose;
private transient int index = -1; private transient int index = -1;
private transient List<ChildNumber> derivation; private transient List<ChildNumber> derivation;
//Cache pubkeys for BIP47 wallets to avoid time-consuming ECDH calculations
private transient ECKey cachedPubKey;
//Note use of this constructor must be followed by setting the wallet field
public WalletNode(String derivationPath) { public WalletNode(String derivationPath) {
this.derivationPath = derivationPath; this.derivationPath = derivationPath;
parseDerivation(); parseDerivation();
} }
public WalletNode(KeyPurpose keyPurpose) { public WalletNode(Wallet wallet, String derivationPath) {
this.wallet = wallet;
this.derivationPath = derivationPath;
parseDerivation();
}
public WalletNode(Wallet wallet, KeyPurpose keyPurpose) {
this.wallet = wallet;
this.derivation = List.of(keyPurpose.getPathIndex()); this.derivation = List.of(keyPurpose.getPathIndex());
this.derivationPath = KeyDerivation.writePath(derivation); this.derivationPath = KeyDerivation.writePath(derivation);
this.keyPurpose = keyPurpose; this.keyPurpose = keyPurpose;
this.index = keyPurpose.getPathIndex().num(); this.index = keyPurpose.getPathIndex().num();
} }
public WalletNode(KeyPurpose keyPurpose, int index) { public WalletNode(Wallet wallet, KeyPurpose keyPurpose, int index) {
this.wallet = wallet;
this.derivation = List.of(keyPurpose.getPathIndex(), new ChildNumber(index)); this.derivation = List.of(keyPurpose.getPathIndex(), new ChildNumber(index));
this.derivationPath = KeyDerivation.writePath(derivation); this.derivationPath = KeyDerivation.writePath(derivation);
this.keyPurpose = keyPurpose; this.keyPurpose = keyPurpose;
this.index = index; this.index = index;
} }
public Wallet getWallet() {
return wallet;
}
public void setWallet(Wallet wallet) {
this.wallet = wallet;
for(WalletNode childNode : getChildren()) {
childNode.setWallet(wallet);
}
}
public String getDerivationPath() { public String getDerivationPath() {
return derivationPath; return derivationPath;
} }
@ -87,25 +115,47 @@ public class WalletNode implements Comparable<WalletNode> {
} }
public Set<WalletNode> getChildren() { public Set<WalletNode> getChildren() {
return children == null ? null : Collections.unmodifiableSet(children); return children;
} }
public void setChildren(TreeSet<WalletNode> children) { public void setChildren(TreeSet<WalletNode> children) {
this.children = children; this.children = children;
} }
public boolean isUsed() {
return !transactionOutputs.isEmpty();
}
public Set<BlockTransactionHashIndex> getTransactionOutputs() { public Set<BlockTransactionHashIndex> getTransactionOutputs() {
return transactionOutputs == null ? null : Collections.unmodifiableSet(transactionOutputs); return transactionOutputs;
} }
public void setTransactionOutputs(TreeSet<BlockTransactionHashIndex> transactionOutputs) { public void setTransactionOutputs(TreeSet<BlockTransactionHashIndex> transactionOutputs) {
this.transactionOutputs = transactionOutputs; this.transactionOutputs = transactionOutputs;
} }
public synchronized void updateTransactionOutputs(Set<BlockTransactionHashIndex> updatedOutputs) { public synchronized void updateTransactionOutputs(Wallet wallet, Set<BlockTransactionHashIndex> updatedOutputs) {
for(BlockTransactionHashIndex txo : updatedOutputs) { for(BlockTransactionHashIndex txo : updatedOutputs) {
Optional<String> optionalLabel = transactionOutputs.stream().filter(oldTxo -> oldTxo.getHash().equals(txo.getHash()) && oldTxo.getIndex() == txo.getIndex()).map(BlockTransactionHash::getLabel).filter(Objects::nonNull).findFirst(); if(!transactionOutputs.isEmpty()) {
optionalLabel.ifPresent(txo::setLabel); Optional<String> optionalLabel = transactionOutputs.stream().filter(oldTxo -> oldTxo.getHash().equals(txo.getHash()) && oldTxo.getIndex() == txo.getIndex()).map(BlockTransactionHash::getLabel).filter(Objects::nonNull).findFirst();
optionalLabel.ifPresent(txo::setLabel);
Optional<Status> optionalStatus = transactionOutputs.stream().filter(oldTxo -> oldTxo.getHash().equals(txo.getHash()) && oldTxo.getIndex() == txo.getIndex()).map(BlockTransactionHashIndex::getStatus).filter(Objects::nonNull).findFirst();
optionalStatus.ifPresent(txo::setStatus);
}
if(!wallet.getDetachedLabels().isEmpty()) {
String label = wallet.getDetachedLabels().remove(txo.getHash().toString() + "<" + txo.getIndex());
if(label != null && (txo.getLabel() == null || txo.getLabel().isEmpty())) {
txo.setLabel(label);
}
if(txo.isSpent()) {
String spentByLabel = wallet.getDetachedLabels().remove(txo.getSpentBy().getHash() + ">" + txo.getSpentBy().getIndex());
if(spentByLabel != null && (txo.getSpentBy().getLabel() == null || txo.getSpentBy().getLabel().isEmpty())) {
txo.getSpentBy().setLabel(spentByLabel);
}
}
}
} }
transactionOutputs.clear(); transactionOutputs.clear();
@ -116,9 +166,14 @@ public class WalletNode implements Comparable<WalletNode> {
return getUnspentTransactionOutputs(false); return getUnspentTransactionOutputs(false);
} }
public Set<BlockTransactionHashIndex> getUnspentTransactionOutputs(boolean includeMempoolInputs) { public Set<BlockTransactionHashIndex> getUnspentTransactionOutputs(boolean includeSpentMempoolOutputs) {
if(transactionOutputs.isEmpty()) {
return Collections.emptySet();
}
Set<BlockTransactionHashIndex> unspentTXOs = new TreeSet<>(transactionOutputs); Set<BlockTransactionHashIndex> unspentTXOs = new TreeSet<>(transactionOutputs);
return unspentTXOs.stream().filter(txo -> !txo.isSpent() || (includeMempoolInputs && txo.getSpentBy().getHeight() <= 0)).collect(Collectors.toCollection(HashSet::new)); unspentTXOs.removeIf(txo -> txo.isSpent() && (!includeSpentMempoolOutputs || txo.getSpentBy().getHeight() > 0));
return unspentTXOs;
} }
public long getUnspentValue() { public long getUnspentValue() {
@ -130,11 +185,55 @@ public class WalletNode implements Comparable<WalletNode> {
return value; return value;
} }
public synchronized void fillToIndex(int index) { public Set<WalletNode> fillToIndex(Wallet wallet, int index) {
for(int i = 0; i <= index; i++) { Set<WalletNode> newNodes = fillToIndex(index);
WalletNode node = new WalletNode(getKeyPurpose(), i); if(wallet.isValid()) {
children.add(node); if(!wallet.getDetachedLabels().isEmpty()) {
for(WalletNode newNode : newNodes) {
String label = wallet.getDetachedLabels().remove(newNode.getAddress().toString());
if(label != null && (newNode.getLabel() == null || newNode.getLabel().isEmpty())) {
newNode.setLabel(label);
}
}
}
if(wallet.isBip47() && keyPurpose == KeyPurpose.RECEIVE && wallet.getLabel() != null && !newNodes.isEmpty()) {
String suffix = " " + wallet.getScriptType().getName();
for(WalletNode newNode : newNodes) {
if((newNode.getLabel() == null || newNode.getLabel().isEmpty()) && wallet.getLabel().endsWith(suffix)) {
newNode.setLabel("From " + wallet.getLabel().substring(0, wallet.getLabel().length() - suffix.length()));
}
}
}
} }
return newNodes;
}
public synchronized Set<WalletNode> fillToIndex(int index) {
//Optimization to check if child nodes already monotonically increment to the desired index
int indexCheck = 0;
for(WalletNode childNode : getChildren()) {
if(childNode.index == indexCheck) {
indexCheck++;
} else {
break;
}
if(childNode.index == index) {
return Collections.emptySet();
}
}
Set<WalletNode> newNodes = new TreeSet<>();
for(int i = 0; i <= index; i++) {
WalletNode node = new WalletNode(wallet, getKeyPurpose(), i);
if(children.add(node)) {
newNodes.add(node);
}
}
return newNodes;
} }
/** /**
@ -151,9 +250,56 @@ public class WalletNode implements Comparable<WalletNode> {
return highestNode == null ? null : highestNode.index; return highestNode == null ? null : highestNode.index;
} }
public ECKey getPubKey() {
if(cachedPubKey != null) {
return cachedPubKey;
}
if(wallet.isBip47()) {
cachedPubKey = wallet.getPubKey(this);
return cachedPubKey;
}
return wallet.getPubKey(this);
}
public List<ECKey> getPubKeys() {
return wallet.getPubKeys(this);
}
public Address getAddress() {
if(address != null) {
return address;
}
Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
if(masterWallet.getKeystores().stream().noneMatch(Keystore::needsPassphrase)) {
address = wallet.getAddress(this);
return address;
}
return wallet.getAddress(this);
}
public byte[] getAddressData() {
return address == null ? null : address.getData();
}
public void setAddress(Address address) {
this.address = address;
}
public Script getOutputScript() {
return getAddress().getOutputScript();
}
public String getOutputDescriptor() {
return wallet.getOutputDescriptor(this);
}
@Override @Override
public String toString() { public String toString() {
return derivationPath; return derivationPath.replace("m", "..");
} }
@Override @Override
@ -161,12 +307,12 @@ public class WalletNode implements Comparable<WalletNode> {
if (this == o) return true; if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false; if (o == null || getClass() != o.getClass()) return false;
WalletNode node = (WalletNode) o; WalletNode node = (WalletNode) o;
return derivationPath.equals(node.derivationPath); return Objects.equals(wallet, node.wallet) && derivationPath.equals(node.derivationPath);
} }
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hash(derivationPath); return Objects.hash(wallet, derivationPath);
} }
@Override @Override
@ -193,12 +339,14 @@ public class WalletNode implements Comparable<WalletNode> {
} }
} }
public WalletNode copy() { public WalletNode copy(Wallet walletCopy) {
WalletNode copy = new WalletNode(derivationPath); WalletNode copy = new WalletNode(walletCopy, derivationPath);
copy.setId(getId());
copy.setLabel(label); copy.setLabel(label);
copy.setAddress(address);
for(WalletNode child : getChildren()) { for(WalletNode child : getChildren()) {
copy.children.add(child.copy()); copy.children.add(child.copy(walletCopy));
} }
for(BlockTransactionHashIndex txo : getTransactionOutputs()) { for(BlockTransactionHashIndex txo : getTransactionOutputs()) {
@ -207,4 +355,69 @@ public class WalletNode implements Comparable<WalletNode> {
return copy; return copy;
} }
public static String nodeRangesToString(Set<WalletNode> nodes) {
return nodeRangesToString(nodes.stream().map(WalletNode::getDerivationPath).collect(Collectors.toList()));
}
public static String nodeRangesToString(Collection<String> nodeDerivations) {
List<String> sortedDerivations = new ArrayList<>(nodeDerivations);
if(nodeDerivations.isEmpty()) {
return "[]";
}
List<List<String>> contiguous = splitToContiguous(sortedDerivations);
String abbrev = "[";
for(Iterator<List<String>> iter = contiguous.iterator(); iter.hasNext(); ) {
List<String> range = iter.next();
abbrev += range.get(0);
if(range.size() > 1) {
abbrev += "-" + range.get(range.size() - 1);
}
if(iter.hasNext()) {
abbrev += ", ";
}
}
abbrev += "]";
return abbrev;
}
private static List<List<String>> splitToContiguous(List<String> input) {
List<List<String>> result = new ArrayList<>();
int prev = 0;
int keyPurpose = getKeyPurpose(input.get(0));
int index = getIndex(input.get(0));
for (int cur = 0; cur < input.size(); cur++) {
if(getKeyPurpose(input.get(cur)) != keyPurpose || getIndex(input.get(cur)) != index) {
result.add(input.subList(prev, cur));
prev = cur;
}
index = getIndex(input.get(cur)) + 1;
keyPurpose = getKeyPurpose(input.get(cur));
}
result.add(input.subList(prev, input.size()));
return result;
}
private static int getKeyPurpose(String path) {
List<ChildNumber> childNumbers = KeyDerivation.parsePath(path);
if(childNumbers.isEmpty()) {
return -1;
}
return childNumbers.get(0).num();
}
private static int getIndex(String path) {
List<ChildNumber> childNumbers = KeyDerivation.parsePath(path);
if(childNumbers.isEmpty()) {
return -1;
}
return childNumbers.get(childNumbers.size() - 1).num();
}
} }

View file

@ -1,12 +1,12 @@
package com.sparrowwallet.drongo.wallet; package com.sparrowwallet.drongo.wallet;
import com.sparrowwallet.drongo.address.Address; import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.protocol.Script;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.protocol.Transaction; import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.psbt.PSBT; import com.sparrowwallet.drongo.psbt.PSBT;
import java.util.ArrayList; import java.util.*;
import java.util.List;
import java.util.Map;
/** /**
* WalletTransaction contains a draft transaction along with associated metadata. The draft transaction has empty signatures but is otherwise complete. * WalletTransaction contains a draft transaction along with associated metadata. The draft transaction has empty signatures but is otherwise complete.
@ -16,25 +16,31 @@ public class WalletTransaction {
private final Wallet wallet; private final Wallet wallet;
private final Transaction transaction; private final Transaction transaction;
private final List<UtxoSelector> utxoSelectors; private final List<UtxoSelector> utxoSelectors;
private final Map<BlockTransactionHashIndex, WalletNode> selectedUtxos; private final List<Map<BlockTransactionHashIndex, WalletNode>> selectedUtxoSets;
private final List<Payment> payments; private final List<Payment> payments;
private final WalletNode changeNode; private final Map<WalletNode, Long> changeMap;
private final long changeAmount;
private final long fee; private final long fee;
private final Map<Sha256Hash, BlockTransaction> inputTransactions;
public WalletTransaction(Wallet wallet, Transaction transaction, List<UtxoSelector> utxoSelectors, Map<BlockTransactionHashIndex, WalletNode> selectedUtxos, List<Payment> payments, long fee) { private Map<Wallet, Map<Address, WalletNode>> addressNodeMap = new HashMap<>();
this(wallet, transaction, utxoSelectors, selectedUtxos, payments, null, 0L, fee);
public WalletTransaction(Wallet wallet, Transaction transaction, List<UtxoSelector> utxoSelectors, List<Map<BlockTransactionHashIndex, WalletNode>> selectedUtxoSets, List<Payment> payments, long fee) {
this(wallet, transaction, utxoSelectors, selectedUtxoSets, payments, Collections.emptyMap(), fee);
} }
public WalletTransaction(Wallet wallet, Transaction transaction, List<UtxoSelector> utxoSelectors, Map<BlockTransactionHashIndex, WalletNode> selectedUtxos, List<Payment> payments, WalletNode changeNode, long changeAmount, long fee) { public WalletTransaction(Wallet wallet, Transaction transaction, List<UtxoSelector> utxoSelectors, List<Map<BlockTransactionHashIndex, WalletNode>> selectedUtxoSets, List<Payment> payments, Map<WalletNode, Long> changeMap, long fee) {
this(wallet, transaction, utxoSelectors, selectedUtxoSets, payments, changeMap, fee, Collections.emptyMap());
}
public WalletTransaction(Wallet wallet, Transaction transaction, List<UtxoSelector> utxoSelectors, List<Map<BlockTransactionHashIndex, WalletNode>> selectedUtxoSets, List<Payment> payments, Map<WalletNode, Long> changeMap, long fee, Map<Sha256Hash, BlockTransaction> inputTransactions) {
this.wallet = wallet; this.wallet = wallet;
this.transaction = transaction; this.transaction = transaction;
this.utxoSelectors = utxoSelectors; this.utxoSelectors = utxoSelectors;
this.selectedUtxos = selectedUtxos; this.selectedUtxoSets = selectedUtxoSets;
this.payments = payments; this.payments = payments;
this.changeNode = changeNode; this.changeMap = changeMap;
this.changeAmount = changeAmount;
this.fee = fee; this.fee = fee;
this.inputTransactions = inputTransactions;
} }
public PSBT createPSBT() { public PSBT createPSBT() {
@ -54,23 +60,29 @@ public class WalletTransaction {
} }
public Map<BlockTransactionHashIndex, WalletNode> getSelectedUtxos() { public Map<BlockTransactionHashIndex, WalletNode> getSelectedUtxos() {
if(selectedUtxoSets.size() == 1) {
return selectedUtxoSets.get(0);
}
Map<BlockTransactionHashIndex, WalletNode> selectedUtxos = new LinkedHashMap<>();
selectedUtxoSets.forEach(selectedUtxos::putAll);
return selectedUtxos; return selectedUtxos;
} }
public List<Map<BlockTransactionHashIndex, WalletNode>> getSelectedUtxoSets() {
return selectedUtxoSets;
}
public List<Payment> getPayments() { public List<Payment> getPayments() {
return payments; return payments;
} }
public WalletNode getChangeNode() { public Map<WalletNode, Long> getChangeMap() {
return changeNode; return changeMap;
} }
public Address getChangeAddress() { public Address getChangeAddress(WalletNode changeNode) {
return getWallet().getAddress(getChangeNode()); return changeNode.getAddress();
}
public long getChangeAmount() {
return changeAmount;
} }
public long getFee() { public long getFee() {
@ -82,7 +94,15 @@ public class WalletTransaction {
} }
public long getTotal() { public long getTotal() {
return selectedUtxos.keySet().stream().mapToLong(BlockTransactionHashIndex::getValue).sum(); return inputAmountsValid() ? getSelectedUtxos().keySet().stream().mapToLong(BlockTransactionHashIndex::getValue).sum() : 0;
}
private boolean inputAmountsValid() {
return getSelectedUtxos().keySet().stream().allMatch(ref -> ref.getValue() > 0);
}
public Map<Sha256Hash, BlockTransaction> getInputTransactions() {
return inputTransactions;
} }
/** /**
@ -90,32 +110,73 @@ public class WalletTransaction {
* @return the fee percentage * @return the fee percentage
*/ */
public double getFeePercentage() { public double getFeePercentage() {
return (double)getFee() / (getTotal() - getFee()); return getFee() <= 0 || getTotal() <= 0 ? 0 : (double)getFee() / (getTotal() - getFee());
} }
public boolean isCoinControlUsed() { public boolean isCoinControlUsed() {
return !utxoSelectors.isEmpty() && utxoSelectors.get(0) instanceof PresetUtxoSelector; return !utxoSelectors.isEmpty() && utxoSelectors.get(0) instanceof PresetUtxoSelector;
} }
public boolean isTwoPersonCoinjoin() {
return !utxoSelectors.isEmpty() && utxoSelectors.get(0) instanceof StonewallUtxoSelector;
}
public boolean isConsolidationSend(Payment payment) { public boolean isConsolidationSend(Payment payment) {
if(payment.getAddress() != null && getWallet() != null) { return isWalletSend(getWallet(), payment);
return getWallet().isWalletOutputScript(payment.getAddress().getOutputScript()); }
public boolean isPremixSend(Payment payment) {
return isWalletSend(StandardAccount.WHIRLPOOL_PREMIX, payment);
}
public boolean isBadbankSend(Payment payment) {
return isWalletSend(StandardAccount.WHIRLPOOL_BADBANK, payment);
}
private boolean isWalletSend(StandardAccount childAccount, Payment payment) {
if(getWallet() != null) {
return isWalletSend(getWallet().getChildWallet(childAccount), payment);
} }
return false; return false;
} }
public List<WalletNode> getConsolidationSendNodes() { public boolean isWalletSend(Wallet wallet, Payment payment) {
List<WalletNode> walletNodes = new ArrayList<>(); if(wallet == null) {
return false;
}
return getAddressNodeMap(wallet).get(payment.getAddress()) != null;
}
public void updateAddressNodeMap(Map<Wallet, Map<Address, WalletNode>> addressNodeMap, Wallet wallet) {
this.addressNodeMap = addressNodeMap;
getAddressNodeMap(wallet);
}
public Map<Address, WalletNode> getAddressNodeMap() {
return getAddressNodeMap(getWallet());
}
public Map<Address, WalletNode> getAddressNodeMap(Wallet wallet) {
Map<Address, WalletNode> walletAddresses = null;
Map<Address, WalletNode> walletAddressNodeMap = addressNodeMap.computeIfAbsent(wallet, w -> new LinkedHashMap<>());
for(Payment payment : payments) { for(Payment payment : payments) {
if(payment.getAddress() != null && getWallet() != null) { if(walletAddressNodeMap.containsKey(payment.getAddress())) {
WalletNode walletNode = getWallet().getWalletOutputScripts().get(payment.getAddress().getOutputScript()); continue;
if(walletNode != null) { }
walletNodes.add(walletNode);
if(payment.getAddress() != null && wallet != null) {
if(walletAddresses == null) {
walletAddresses = wallet.getWalletAddresses();
} }
WalletNode walletNode = walletAddresses.get(payment.getAddress());
walletAddressNodeMap.put(payment.getAddress(), walletNode);
} }
} }
return walletNodes; return walletAddressNodeMap;
} }
} }

View file

@ -1,8 +1,9 @@
open module com.sparrowwallet.drongo { open module com.sparrowwallet.drongo {
requires org.bouncycastle.provider; requires org.bouncycastle.provider;
requires de.mkammerer.argon2; requires de.mkammerer.argon2.nolibs;
requires slf4j.api; requires org.slf4j;
requires logback.core; requires logback.core;
requires logback.classic;
requires json.simple; requires json.simple;
requires jeromq; requires jeromq;
exports com.sparrowwallet.drongo; exports com.sparrowwallet.drongo;
@ -13,4 +14,6 @@ open module com.sparrowwallet.drongo {
exports com.sparrowwallet.drongo.wallet; exports com.sparrowwallet.drongo.wallet;
exports com.sparrowwallet.drongo.policy; exports com.sparrowwallet.drongo.policy;
exports com.sparrowwallet.drongo.uri; exports com.sparrowwallet.drongo.uri;
exports com.sparrowwallet.drongo.bip47;
exports org.bitcoin;
} }

View file

@ -0,0 +1,910 @@
/*
* Copyright 2013 Google Inc.
* Copyright 2014-2016 the libsecp256k1 contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.bitcoin;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.math.BigInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import static org.bitcoin.NativeSecp256k1Util.*;
/**
* <p>This class holds native methods to handle ECDSA verification.</p>
*
* <p>You can find an example library that can be used for this at https://github.com/bitcoin-core/secp256k1</p>
*
* <p>To build secp256k1 for use with bitcoinj, run
* `./configure --enable-jni --enable-experimental --enable-module-ecdh`
* and `make` then copy `.libs/libsecp256k1.so` to your system library path
* or point the JVM to the folder containing it with -Djava.library.path
* </p>
*/
public class NativeSecp256k1 {
private static final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private static final Lock r = rwl.readLock();
private static final Lock w = rwl.writeLock();
private static ThreadLocal<ByteBuffer> nativeECDSABuffer = new ThreadLocal<ByteBuffer>();
/**
* Verifies the given secp256k1 signature in native code.
* Calling when enabled == false is undefined (probably library not loaded)
*
* @param data The data which was signed, must be exactly 32 bytes
* @param signature The signature
* @param pub The public key which did the signing
*/
public static boolean verify(byte[] data, byte[] signature, byte[] pub) {
checkArgument(data.length == 32 && signature.length <= 520 && pub.length <= 520);
ByteBuffer byteBuff = nativeECDSABuffer.get();
if (byteBuff == null || byteBuff.capacity() < 520) {
byteBuff = ByteBuffer.allocateDirect(520);
byteBuff.order(ByteOrder.nativeOrder());
nativeECDSABuffer.set(byteBuff);
}
safeRewind(byteBuff);
byteBuff.put(data);
byteBuff.put(signature);
byteBuff.put(pub);
r.lock();
try {
return secp256k1_ecdsa_verify(byteBuff, Secp256k1Context.getContext(), signature.length, pub.length) == 1;
} finally {
r.unlock();
}
}
/**
* libsecp256k1 Create an ECDSA signature.
*
* @param data Message hash, 32 bytes
* @param seckey ECDSA Secret key, 32 bytes
* @return sig byte array of signature
*/
public static byte[] sign(byte[] data, byte[] seckey) throws AssertFailException{
checkArgument(data.length == 32 && seckey.length <= 32);
ByteBuffer byteBuff = nativeECDSABuffer.get();
if (byteBuff == null || byteBuff.capacity() < 32 + 32) {
byteBuff = ByteBuffer.allocateDirect(32 + 32);
byteBuff.order(ByteOrder.nativeOrder());
nativeECDSABuffer.set(byteBuff);
}
safeRewind(byteBuff);
byteBuff.put(data);
byteBuff.put(seckey);
byte[][] retByteArray;
r.lock();
try {
retByteArray = secp256k1_ecdsa_sign(byteBuff, Secp256k1Context.getContext());
} finally {
r.unlock();
}
byte[] sigArr = retByteArray[0];
int sigLen = new BigInteger(new byte[] { retByteArray[1][0] }).intValue();
int retVal = new BigInteger(new byte[] { retByteArray[1][1] }).intValue();
assertEquals(sigArr.length, sigLen, "Got bad signature length.");
return retVal == 0 ? new byte[0] : sigArr;
}
/**
* libsecp256k1 Create an ECDSA signature adding specified entropy.
*
* This can be used to include your own entropy to nonce generation
* in addition to the message and private key, while still doing so deterministically.
*
* In particular, this is used when generating low R signatures.
* See https://github.com/bitcoin/bitcoin/pull/13666/
*
* @param data Message hash, 32 bytes
* @param seckey ECDSA Secret key, 32 bytes
* @param entropy 32 bytes of entropy
* @return sig byte array of signature
*/
public static byte[] signWithEntropy(byte[] data, byte[] seckey, byte[] entropy) throws AssertFailException{
checkArgument(data.length == 32 && seckey.length == 32 && entropy.length == 32);
ByteBuffer byteBuff = nativeECDSABuffer.get();
if (byteBuff == null || byteBuff.capacity() < 32 + 32 + 32) {
byteBuff = ByteBuffer.allocateDirect(32 + 32 + 32);
byteBuff.order(ByteOrder.nativeOrder());
nativeECDSABuffer.set(byteBuff);
}
safeRewind(byteBuff);
byteBuff.put(data);
byteBuff.put(seckey);
byteBuff.put(entropy);
byte[][] retByteArray;
r.lock();
try {
retByteArray = secp256k1_ecdsa_sign_with_entropy(byteBuff, Secp256k1Context.getContext());
} finally {
r.unlock();
}
byte[] sigArr = retByteArray[0];
int sigLen = new BigInteger(new byte[] { retByteArray[1][0] }).intValue();
int retVal = new BigInteger(new byte[] { retByteArray[1][1] }).intValue();
assertEquals(sigArr.length, sigLen, "Got bad signature length.");
return retVal == 0 ? new byte[0] : sigArr;
}
/**
* libsecp256k1 Seckey Verify - Verifies an ECDSA secret key
*
* @param seckey ECDSA Secret key, 32 bytes
* @return true if valid, false if invalid
*/
public static boolean secKeyVerify(byte[] seckey) {
checkArgument(seckey.length == 32);
ByteBuffer byteBuff = nativeECDSABuffer.get();
if (byteBuff == null || byteBuff.capacity() < seckey.length) {
byteBuff = ByteBuffer.allocateDirect(seckey.length);
byteBuff.order(ByteOrder.nativeOrder());
nativeECDSABuffer.set(byteBuff);
}
safeRewind(byteBuff);
byteBuff.put(seckey);
r.lock();
try {
return secp256k1_ec_seckey_verify(byteBuff,Secp256k1Context.getContext()) == 1;
} finally {
r.unlock();
}
}
/**
* libsecp256k1 Compute Pubkey - computes public key from secret key
*
* @param seckey ECDSA Secret key, 32 bytes
* @param compressed Should the generated public key be compressed
* @return pubkey ECDSA Public key, 33 or 65 bytes
*/
public static byte[] computePubkey(byte[] seckey, boolean compressed) throws AssertFailException{
checkArgument(seckey.length == 32);
ByteBuffer byteBuff = nativeECDSABuffer.get();
if (byteBuff == null || byteBuff.capacity() < seckey.length) {
byteBuff = ByteBuffer.allocateDirect(seckey.length);
byteBuff.order(ByteOrder.nativeOrder());
nativeECDSABuffer.set(byteBuff);
}
safeRewind(byteBuff);
byteBuff.put(seckey);
byte[][] retByteArray;
r.lock();
try {
retByteArray = secp256k1_ec_pubkey_create(byteBuff, Secp256k1Context.getContext(), compressed);
} finally {
r.unlock();
}
byte[] pubArr = retByteArray[0];
int pubLen = new BigInteger(new byte[] { retByteArray[1][0] }).intValue();
int retVal = new BigInteger(new byte[] { retByteArray[1][1] }).intValue();
assertEquals(pubArr.length, pubLen, "Got bad pubkey length.");
return retVal == 0 ? new byte[0]: pubArr;
}
/**
* libsecp256k1 Cleanup - This destroys the secp256k1 context object
* This should be called at the end of the program for proper cleanup of the context.
*/
public static synchronized void cleanup() {
w.lock();
try {
secp256k1_destroy_context(Secp256k1Context.getContext());
} finally {
w.unlock();
}
}
public static long cloneContext() {
r.lock();
try {
return secp256k1_ctx_clone(Secp256k1Context.getContext());
} finally { r.unlock(); }
}
/**
* libsecp256k1 PrivKey Tweak-Mul - Tweak seckey by multiplying to it
*
* @param seckey ECDSA Secret key, 32 bytes
* @param tweak some bytes to tweak with
*/
public static byte[] privKeyTweakMul(byte[] seckey, byte[] tweak) throws AssertFailException{
checkArgument(seckey.length == 32);
ByteBuffer byteBuff = nativeECDSABuffer.get();
if (byteBuff == null || byteBuff.capacity() < seckey.length + tweak.length) {
byteBuff = ByteBuffer.allocateDirect(seckey.length + tweak.length);
byteBuff.order(ByteOrder.nativeOrder());
nativeECDSABuffer.set(byteBuff);
}
safeRewind(byteBuff);
byteBuff.put(seckey);
byteBuff.put(tweak);
byte[][] retByteArray;
r.lock();
try {
retByteArray = secp256k1_privkey_tweak_mul(byteBuff,Secp256k1Context.getContext());
} finally {
r.unlock();
}
byte[] privArr = retByteArray[0];
int privLen = (byte) new BigInteger(new byte[] { retByteArray[1][0] }).intValue() & 0xFF;
int retVal = new BigInteger(new byte[] { retByteArray[1][1] }).intValue();
assertEquals(privArr.length, privLen, "Got bad pubkey length.");
assertEquals(retVal, 1, "Failed return value check.");
return privArr;
}
/**
* libsecp256k1 PrivKey Tweak-Add - Tweak seckey by adding to it
*
* @param seckey ECDSA Secret key, 32 bytes
* @param tweak some bytes to tweak with
*/
public static byte[] privKeyTweakAdd(byte[] seckey, byte[] tweak) throws AssertFailException{
checkArgument(seckey.length == 32);
ByteBuffer byteBuff = nativeECDSABuffer.get();
if (byteBuff == null || byteBuff.capacity() < seckey.length + tweak.length) {
byteBuff = ByteBuffer.allocateDirect(seckey.length + tweak.length);
byteBuff.order(ByteOrder.nativeOrder());
nativeECDSABuffer.set(byteBuff);
}
safeRewind(byteBuff);
byteBuff.put(seckey);
byteBuff.put(tweak);
byte[][] retByteArray;
r.lock();
try {
retByteArray = secp256k1_privkey_tweak_add(byteBuff,Secp256k1Context.getContext());
} finally {
r.unlock();
}
byte[] privArr = retByteArray[0];
int privLen = (byte) new BigInteger(new byte[] { retByteArray[1][0] }).intValue() & 0xFF;
int retVal = new BigInteger(new byte[] { retByteArray[1][1] }).intValue();
assertEquals(privArr.length, privLen, "Got bad pubkey length.");
assertEquals(retVal, 1, "Failed return value check.");
return privArr;
}
/**
* libsecp256k1 PubKey Tweak-Add - Tweak pubkey by adding to it
*
* @param pubkey ECDSA Public key, 33 or 65 bytes
* @param tweak some bytes to tweak with
* @param compressed should the output public key be compressed
*/
public static byte[] pubKeyTweakAdd(byte[] pubkey, byte[] tweak, boolean compressed) throws AssertFailException{
checkArgument(pubkey.length == 33 || pubkey.length == 65);
ByteBuffer byteBuff = nativeECDSABuffer.get();
if (byteBuff == null || byteBuff.capacity() < pubkey.length + tweak.length) {
byteBuff = ByteBuffer.allocateDirect(pubkey.length + tweak.length);
byteBuff.order(ByteOrder.nativeOrder());
nativeECDSABuffer.set(byteBuff);
}
safeRewind(byteBuff);
byteBuff.put(pubkey);
byteBuff.put(tweak);
byte[][] retByteArray;
r.lock();
try {
retByteArray = secp256k1_pubkey_tweak_add(byteBuff, Secp256k1Context.getContext(), pubkey.length, compressed);
} finally {
r.unlock();
}
byte[] pubArr = retByteArray[0];
int pubLen = (byte) new BigInteger(new byte[] { retByteArray[1][0] }).intValue() & 0xFF;
int retVal = new BigInteger(new byte[] { retByteArray[1][1] }).intValue();
assertEquals(pubArr.length, pubLen, "Got bad pubkey length.");
assertEquals(retVal, 1, "Failed return value check.");
return pubArr;
}
/**
* libsecp256k1 PubKey Tweak-Mul - Tweak pubkey by multiplying to it
*
* @param pubkey ECDSA Public key, 33 or 65 bytes
* @param tweak some bytes to tweak with
* @param compressed should the output public key be compressed
*/
public static byte[] pubKeyTweakMul(byte[] pubkey, byte[] tweak, boolean compressed) throws AssertFailException{
checkArgument(pubkey.length == 33 || pubkey.length == 65);
ByteBuffer byteBuff = nativeECDSABuffer.get();
if (byteBuff == null || byteBuff.capacity() < pubkey.length + tweak.length) {
byteBuff = ByteBuffer.allocateDirect(pubkey.length + tweak.length);
byteBuff.order(ByteOrder.nativeOrder());
nativeECDSABuffer.set(byteBuff);
}
safeRewind(byteBuff);
byteBuff.put(pubkey);
byteBuff.put(tweak);
byte[][] retByteArray;
r.lock();
try {
retByteArray = secp256k1_pubkey_tweak_mul(byteBuff,Secp256k1Context.getContext(), pubkey.length, compressed);
} finally {
r.unlock();
}
byte[] pubArr = retByteArray[0];
int pubLen = (byte) new BigInteger(new byte[] { retByteArray[1][0] }).intValue() & 0xFF;
int retVal = new BigInteger(new byte[] { retByteArray[1][1] }).intValue();
assertEquals(pubArr.length, pubLen, "Got bad pubkey length.");
assertEquals(retVal, 1, "Failed return value check.");
return pubArr;
}
/**
* libsecp256k1 PubKey Combine - Add pubkeys together
*
* @param pubkeys array of ECDSA Public key, 33 or 65 bytes each
* @param compressed should the output public key be compressed
*/
public static byte[] pubKeyCombine(byte[][] pubkeys, boolean compressed) throws AssertFailException{
int numKeys = pubkeys.length;
checkArgument(numKeys > 0);
int pubkeyLength = pubkeys[0].length;
checkArgument(pubkeyLength == 33 || pubkeyLength == 65);
for (byte[] pubkey : pubkeys) {
checkArgument(pubkey.length == pubkeyLength);
}
ByteBuffer byteBuff = nativeECDSABuffer.get();
if (byteBuff == null || byteBuff.capacity() < numKeys * pubkeyLength) {
byteBuff = ByteBuffer.allocateDirect(numKeys * pubkeyLength);
byteBuff.order(ByteOrder.nativeOrder());
nativeECDSABuffer.set(byteBuff);
}
safeRewind(byteBuff);
for (byte[] pubkey : pubkeys) {
byteBuff.put(pubkey);
}
byte[][] retByteArray;
r.lock();
try {
retByteArray = secp256k1_ec_pubkey_combine(byteBuff,Secp256k1Context.getContext(), pubkeyLength, numKeys, compressed);
} finally {
r.unlock();
}
byte[] pubArr = retByteArray[0];
int pubLen = (byte) new BigInteger(new byte[] { retByteArray[1][0] }).intValue() & 0xFF;
int retVal = new BigInteger(new byte[] { retByteArray[1][1] }).intValue();
assertEquals(pubArr.length, pubLen, "Got bad pubkey length.");
assertEquals(retVal, 1, "Failed return value check.");
return pubArr;
}
/**
* libsecp256k1 Decompress - Parse and decompress a variable-length pub key
*
* @param pubkey ECDSA Public key, 33 or 65 bytes
*/
public static byte[] decompress(byte[] pubkey) throws AssertFailException{
checkArgument(pubkey.length == 33 || pubkey.length == 65);
ByteBuffer byteBuff = nativeECDSABuffer.get();
if (byteBuff == null || byteBuff.capacity() < pubkey.length) {
byteBuff = ByteBuffer.allocateDirect(pubkey.length);
byteBuff.order(ByteOrder.nativeOrder());
nativeECDSABuffer.set(byteBuff);
}
safeRewind(byteBuff);
byteBuff.put(pubkey);
byte[][] retByteArray;
r.lock();
try {
retByteArray = secp256k1_ec_pubkey_decompress(byteBuff, Secp256k1Context.getContext(), pubkey.length);
} finally {
r.unlock();
}
byte[] pubArr = retByteArray[0];
int pubLen = (byte) new BigInteger(new byte[] { retByteArray[1][0] }).intValue() & 0xFF;
int retVal = new BigInteger(new byte[] { retByteArray[1][1] }).intValue();
assertEquals(pubArr.length, pubLen, "Got bad pubkey length.");
assertEquals(retVal, 1, "Failed return value check.");
return pubArr;
}
/**
* libsecp256k1 IsValidPubKey - Checks if a pubkey is valid
*
* @param pubkey ECDSA Public key, 33 or 65 bytes
*/
public static boolean isValidPubKey(byte[] pubkey) {
if (!(pubkey.length == 33 || pubkey.length == 65)) {
return false;
}
ByteBuffer byteBuff = nativeECDSABuffer.get();
if (byteBuff == null || byteBuff.capacity() < pubkey.length) {
byteBuff = ByteBuffer.allocateDirect(pubkey.length);
byteBuff.order(ByteOrder.nativeOrder());
nativeECDSABuffer.set(byteBuff);
}
safeRewind(byteBuff);
byteBuff.put(pubkey);
byte[][] retByteArray;
r.lock();
try {
retByteArray = secp256k1_ec_pubkey_decompress(byteBuff, Secp256k1Context.getContext(), pubkey.length);
} finally {
r.unlock();
}
int retVal = new BigInteger(new byte[] { retByteArray[1][1] }).intValue();
return retVal == 1;
}
/**
* libsecp256k1 create ECDH secret - constant time ECDH calculation
*
* @param seckey byte array of secret key used in exponentiaion
* @param pubkey byte array of public key used in exponentiaion
*/
public static byte[] createECDHSecret(byte[] seckey, byte[] pubkey) throws AssertFailException{
checkArgument(seckey.length <= 32 && pubkey.length <= 65);
ByteBuffer byteBuff = nativeECDSABuffer.get();
if (byteBuff == null || byteBuff.capacity() < 32 + pubkey.length) {
byteBuff = ByteBuffer.allocateDirect(32 + pubkey.length);
byteBuff.order(ByteOrder.nativeOrder());
nativeECDSABuffer.set(byteBuff);
}
safeRewind(byteBuff);
byteBuff.put(seckey);
byteBuff.put(pubkey);
byte[][] retByteArray;
r.lock();
try {
retByteArray = secp256k1_ecdh(byteBuff, Secp256k1Context.getContext(), pubkey.length);
} finally {
r.unlock();
}
byte[] resArr = retByteArray[0];
int retVal = new BigInteger(new byte[] { retByteArray[1][0] }).intValue();
assertEquals(resArr.length, 32, "Got bad result length.");
assertEquals(retVal, 1, "Failed return value check.");
return resArr;
}
/**
* libsecp256k1 schnorr sign - generates a BIP 340 Schnorr signature
*
* @param data message to sign
* @param secKey key to sign with
*/
public static byte[] schnorrSign(byte[] data, byte[] secKey, byte[] auxRand) throws AssertFailException {
checkArgument(data.length == 32 && secKey.length == 32 && auxRand.length == 32);
ByteBuffer byteBuff = nativeECDSABuffer.get();
if (byteBuff == null || byteBuff.capacity() < 32 + 32 + 32) {
byteBuff = ByteBuffer.allocateDirect(32 + 32 + 32);
byteBuff.order(ByteOrder.nativeOrder());
nativeECDSABuffer.set(byteBuff);
}
byteBuff.rewind();
byteBuff.put(data);
byteBuff.put(secKey);
byteBuff.put(auxRand);
byte[][] retByteArray;
r.lock();
try {
retByteArray = secp256k1_schnorrsig_sign(byteBuff, Secp256k1Context.getContext());
} finally {
r.unlock();
}
byte[] sigArray = retByteArray[0];
int retVal = new BigInteger(new byte[]{retByteArray[1][0]}).intValue();
assertEquals(retVal, 1, "Failed return value check.");
return sigArray;
}
/**
* libsecp256k1 schnorr sign - generates a BIP 340 Schnorr signature
*
* @param data message to sign
* @param secKey key to sign with
* @param nonce the nonce (k value) used in signing
*/
public static byte[] schnorrSignWithNonce(byte[] data, byte[] secKey, byte[] nonce) throws AssertFailException {
checkArgument(data.length == 32 && secKey.length == 32 && nonce.length == 32);
ByteBuffer byteBuff = nativeECDSABuffer.get();
if (byteBuff == null || byteBuff.capacity() < 32 + 32 + 32) {
byteBuff = ByteBuffer.allocateDirect(32 + 32 + 32);
byteBuff.order(ByteOrder.nativeOrder());
nativeECDSABuffer.set(byteBuff);
}
byteBuff.rewind();
byteBuff.put(data);
byteBuff.put(secKey);
byteBuff.put(nonce);
byte[][] retByteArray;
r.lock();
try {
retByteArray = secp256k1_schnorrsig_sign_with_nonce(byteBuff, Secp256k1Context.getContext());
} finally {
r.unlock();
}
byte[] sigArray = retByteArray[0];
int retVal = new BigInteger(new byte[]{retByteArray[1][0]}).intValue();
assertEquals(retVal, 1, "Failed return value check.");
return sigArray;
}
/*
public static byte[] schnorrComputeSigPoint(byte[] data, byte[] nonce, byte[] pubkey, boolean compressed) throws AssertFailException {
checkArgument(data.length == 32 && nonce.length == 32 && pubkey.length == 32);
ByteBuffer byteBuff = nativeECDSABuffer.get();
if (byteBuff == null || byteBuff.capacity() < 32 + 32 + 32) {
byteBuff = ByteBuffer.allocateDirect(32 + 32 + 32);
byteBuff.order(ByteOrder.nativeOrder());
nativeECDSABuffer.set(byteBuff);
}
byteBuff.rewind();
byteBuff.put(data);
byteBuff.put(nonce);
byteBuff.put(pubkey);
byte[][] retByteArray;
r.lock();
try {
retByteArray = secp256k1_schnorrsig_compute_sigpoint(byteBuff, Secp256k1Context.getContext(), compressed);
} finally {
r.unlock();
}
byte[] pointArray = retByteArray[0];
int outputLen = new BigInteger(new byte[] { retByteArray[1][0] }).intValue() & 0xFF;
int retVal = new BigInteger(new byte[] { retByteArray[1][1] }).intValue();
assertEquals(pointArray.length, outputLen, "Got bad point length.");
assertEquals(retVal, 1, "Failed return value check.");
return pointArray;
}
*/
/**
* libsecp256k1 schnorr verify - verifies BIP 340 Schnorr signatures
*
* @param sig signature to verify
* @param data message the signature has signed
* @param pubx the key that did the signing
*/
public static boolean schnorrVerify(byte[] sig, byte[] data, byte[] pubx) throws AssertFailException {
checkArgument(sig.length == 64 && data.length == 32 && pubx.length == 32);
ByteBuffer byteBuffer = nativeECDSABuffer.get();
if (byteBuffer == null || byteBuffer.capacity() < 64 + 32 + 32) {
byteBuffer = ByteBuffer.allocateDirect(64 + 32 + 32);
byteBuffer.order(ByteOrder.nativeOrder());
nativeECDSABuffer.set(byteBuffer);
}
byteBuffer.rewind();
byteBuffer.put(sig);
byteBuffer.put(data);
byteBuffer.put(pubx);
r.lock();
try {
return secp256k1_schnorrsig_verify(byteBuffer, Secp256k1Context.getContext()) == 1;
} finally {
r.unlock();
}
}
public static byte[] adaptorSign(byte[] seckey, byte[] adaptorPoint, byte[] data, byte[] auxRand) throws AssertFailException{
checkArgument(seckey.length == 32 &&
data.length == 32 &&
(adaptorPoint.length == 33 || adaptorPoint.length == 65) &&
auxRand.length == 32);
ByteBuffer byteBuff = nativeECDSABuffer.get();
if (byteBuff == null || byteBuff.capacity() < 32 + 32 + adaptorPoint.length + 32) {
byteBuff = ByteBuffer.allocateDirect(32 + 32 + adaptorPoint.length + 32);
byteBuff.order(ByteOrder.nativeOrder());
nativeECDSABuffer.set(byteBuff);
}
byteBuff.rewind();
byteBuff.put(seckey);
byteBuff.put(adaptorPoint);
byteBuff.put(data);
byteBuff.put(auxRand);
byte[][] retByteArray;
r.lock();
try {
retByteArray = secp256k1_ecdsa_adaptor_sign(byteBuff, Secp256k1Context.getContext(), adaptorPoint.length);
} finally {
r.unlock();
}
byte[] sigArr = retByteArray[0];
int retVal = new BigInteger(new byte[] { retByteArray[1][0] }).intValue();
if (retVal == 0) {
return new byte[]{};
} else {
return sigArr;
}
}
public static boolean adaptorVerify(byte[] adaptorSig, byte[] pubKey, byte[] data, byte[] adaptorPoint) throws AssertFailException{
checkArgument(data.length == 32 &&
adaptorSig.length == 162 &&
(pubKey.length == 33 || pubKey.length == 65) &&
adaptorPoint.length == pubKey.length);
ByteBuffer byteBuff = nativeECDSABuffer.get();
int buffLen = 32 + 162 + pubKey.length + adaptorPoint.length;
if (byteBuff == null || byteBuff.capacity() < buffLen) {
byteBuff = ByteBuffer.allocateDirect(buffLen);
byteBuff.order(ByteOrder.nativeOrder());
nativeECDSABuffer.set(byteBuff);
}
byteBuff.rewind();
byteBuff.put(adaptorSig);
byteBuff.put(pubKey);
byteBuff.put(data);
byteBuff.put(adaptorPoint);
r.lock();
try {
return secp256k1_ecdsa_adaptor_sig_verify(byteBuff, Secp256k1Context.getContext(), pubKey.length) == 1;
} finally {
r.unlock();
}
}
public static byte[] adaptorAdapt(byte[] adaptorSec, byte[] adaptorSig) throws AssertFailException{
checkArgument(adaptorSec.length == 32 && adaptorSig.length == 162);
ByteBuffer byteBuff = nativeECDSABuffer.get();
if (byteBuff == null || byteBuff.capacity() < 32 + 162) {
byteBuff = ByteBuffer.allocateDirect(32 + 162);
byteBuff.order(ByteOrder.nativeOrder());
nativeECDSABuffer.set(byteBuff);
}
byteBuff.rewind();
byteBuff.put(adaptorSec);
byteBuff.put(adaptorSig);
byte[][] retByteArray;
r.lock();
try {
retByteArray = secp256k1_ecdsa_adaptor_adapt(byteBuff, Secp256k1Context.getContext());
} finally {
r.unlock();
}
byte[] sigArr = retByteArray[0];
int sigLen = new BigInteger(new byte[] { retByteArray[1][0] }).intValue();
int retVal = new BigInteger(new byte[] { retByteArray[1][1] }).intValue();
assertEquals(sigArr.length, sigLen, "Got bad signature length.");
return retVal == 0 ? new byte[0] : sigArr;
}
public static byte[] adaptorExtractSecret(byte[] sig, byte[] adaptorSig, byte[] adaptor) throws AssertFailException{
checkArgument(sig.length <= 520 && (adaptor.length == 33 || adaptor.length == 65) && adaptorSig.length == 162);
ByteBuffer byteBuff = nativeECDSABuffer.get();
if (byteBuff == null || byteBuff.capacity() < sig.length + adaptor.length + 162) {
byteBuff = ByteBuffer.allocateDirect(sig.length + adaptor.length + 162);
byteBuff.order(ByteOrder.nativeOrder());
nativeECDSABuffer.set(byteBuff);
}
byteBuff.rewind();
byteBuff.put(sig);
byteBuff.put(adaptorSig);
byteBuff.put(adaptor);
byte[][] retByteArray;
r.lock();
try {
retByteArray = secp256k1_ecdsa_adaptor_extract_secret(byteBuff, Secp256k1Context.getContext(), sig.length, adaptor.length);
} finally {
r.unlock();
}
byte[] sigArr = retByteArray[0];
int retVal = new BigInteger(new byte[] { retByteArray[1][0] }).intValue();
return retVal == 0 ? new byte[0] : sigArr;
}
/**
* libsecp256k1 randomize - updates the context randomization
*
* @param seed 32-byte random seed
*/
public static synchronized boolean randomize(byte[] seed) throws AssertFailException{
checkArgument(seed.length == 32);
ByteBuffer byteBuff = nativeECDSABuffer.get();
if (byteBuff == null || byteBuff.capacity() < seed.length) {
byteBuff = ByteBuffer.allocateDirect(seed.length);
byteBuff.order(ByteOrder.nativeOrder());
nativeECDSABuffer.set(byteBuff);
}
safeRewind(byteBuff);
byteBuff.put(seed);
w.lock();
try {
return secp256k1_context_randomize(byteBuff, Secp256k1Context.getContext()) == 1;
} finally {
w.unlock();
}
}
/**
* This helper method is needed to resolve issue 1524 on bitcoin-s
* This is because the API changed for ByteBuffer between jdks < 9 and jdk >= 9
* In the earlier versions of the jdk, a [[java.nio.Buffer]] is returned, but greather than jdk 8
* returns a [[ByteBuffer]]. This causes issues when compiling with jdk 11 but running with jdk 8
* as the APIs are incompatible.
* @see https://github.com/bitcoin-s/bitcoin-s/issues/1524
* @param byteBuff
*/
private static void safeRewind(ByteBuffer byteBuff) {
((Buffer) byteBuff).rewind();
}
private static native long secp256k1_ctx_clone(long context);
private static native int secp256k1_context_randomize(ByteBuffer byteBuff, long context);
private static native byte[][] secp256k1_privkey_tweak_add(ByteBuffer byteBuff, long context);
private static native byte[][] secp256k1_privkey_tweak_mul(ByteBuffer byteBuff, long context);
private static native byte[][] secp256k1_pubkey_tweak_add(ByteBuffer byteBuff, long context, int pubLen, boolean compressed);
private static native byte[][] secp256k1_pubkey_tweak_mul(ByteBuffer byteBuff, long context, int pubLen, boolean compressed);
private static native void secp256k1_destroy_context(long context);
private static native int secp256k1_ecdsa_verify(ByteBuffer byteBuff, long context, int sigLen, int pubLen);
private static native byte[][] secp256k1_ecdsa_sign(ByteBuffer byteBuff, long context);
private static native byte[][] secp256k1_ecdsa_sign_with_entropy(ByteBuffer byteBuff, long context);
private static native int secp256k1_ec_seckey_verify(ByteBuffer byteBuff, long context);
private static native byte[][] secp256k1_ec_pubkey_create(ByteBuffer byteBuff, long context, boolean compressed);
private static native byte[][] secp256k1_ec_pubkey_combine(ByteBuffer byteBuff, long context, int pubLen, int numKeys, boolean compressed);
private static native byte[][] secp256k1_ec_pubkey_decompress(ByteBuffer byteBuff, long context, int inputLen);
private static native byte[][] secp256k1_ecdh(ByteBuffer byteBuff, long context, int inputLen);
private static native byte[][] secp256k1_schnorrsig_sign(ByteBuffer byteBuff, long context);
private static native byte[][] secp256k1_schnorrsig_sign_with_nonce(ByteBuffer byteBuff, long context);
//private static native byte[][] secp256k1_schnorrsig_compute_sigpoint(ByteBuffer byteBuff, long context, boolean compressed);
private static native int secp256k1_schnorrsig_verify(ByteBuffer byteBuffer, long context);
private static native byte[][] secp256k1_ecdsa_adaptor_sign(ByteBuffer byteBuff, long context, int adaptorLen);
private static native int secp256k1_ecdsa_adaptor_sig_verify(ByteBuffer byteBuff, long context, int pubKeyLen);
private static native byte[][] secp256k1_ecdsa_adaptor_adapt(ByteBuffer byteBuff, long context);
private static native byte[][] secp256k1_ecdsa_adaptor_extract_secret(ByteBuffer byteBuff, long context, int sigLen, int adaptorLen);
}

View file

@ -0,0 +1,51 @@
/*
* Copyright 2014-2016 the libsecp256k1 contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.bitcoin;
public class NativeSecp256k1Util{
public static void assertEquals( int val, int val2, String message ) throws AssertFailException{
if( val != val2 )
throw new AssertFailException("FAIL: " + message);
}
public static void assertEquals( boolean val, boolean val2, String message ) throws AssertFailException{
if( val != val2 )
throw new AssertFailException("FAIL: " + message);
else
System.out.println("PASS: " + message);
}
public static void assertEquals( String val, String val2, String message ) throws AssertFailException{
if( !val.equals(val2) )
throw new AssertFailException("FAIL: " + message);
else
System.out.println("PASS: " + message);
}
public static class AssertFailException extends Exception {
public AssertFailException(String message) {
super( message );
}
}
public static void checkArgument(boolean expression) {
if (!expression) {
throw new IllegalArgumentException();
}
}
}

View file

@ -0,0 +1,60 @@
package org.bitcoin;
import com.sparrowwallet.drongo.NativeUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
public class Secp256k1Context {
private static final boolean enabled; // true if the library is loaded
private static final long context; // ref to pointer to context obj
private static final Logger log = LoggerFactory.getLogger(Secp256k1Context.class);
static { // static initializer
enabled = loadLibrary();
if(enabled) {
context = secp256k1_init_context();
} else {
context = -1;
}
}
public static boolean isEnabled() {
return enabled;
}
public static long getContext() {
if (!enabled)
return -1; // sanity check
return context;
}
private static boolean loadLibrary() {
try {
String osName = System.getProperty("os.name");
String osArch = System.getProperty("os.arch");
if(osName.startsWith("Mac") && osArch.equals("aarch64")) {
NativeUtils.loadLibraryFromJar("/native/osx/aarch64/libsecp256k1.dylib");
} else if(osName.startsWith("Mac")) {
NativeUtils.loadLibraryFromJar("/native/osx/x64/libsecp256k1.dylib");
} else if(osName.startsWith("Windows")) {
NativeUtils.loadLibraryFromJar("/native/windows/x64/libsecp256k1-0.dll");
} else if(osArch.equals("aarch64")) {
NativeUtils.loadLibraryFromJar("/native/linux/aarch64/libsecp256k1.so");
} else {
NativeUtils.loadLibraryFromJar("/native/linux/x64/libsecp256k1.so");
}
return true;
} catch(IOException e) {
log.error("Error loading libsecp256k1 library", e);
}
return false;
}
private static native long secp256k1_init_context();
}

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show more