mirror of
https://github.com/sparrowwallet/drongo.git
synced 2025-11-05 11:56:38 +00:00
Merge branch 'master' of https://github.com/sparrowwallet/drongo into update-dec-2022
This commit is contained in:
commit
131478dcd0
113 changed files with 7193 additions and 1362 deletions
72
build.gradle
72
build.gradle
|
|
@ -4,35 +4,30 @@ buildscript {
|
|||
url "https://plugins.gradle.org/m2/"
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
classpath "org.javamodularity:moduleplugin:1.6.0"
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'java-library'
|
||||
id 'extra-java-module-info'
|
||||
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) {
|
||||
preserveFileTimestamps = false
|
||||
reproducibleFileOrder = true
|
||||
}
|
||||
|
||||
group 'com.sparrowwallet'
|
||||
version '0.9'
|
||||
version '1.0'
|
||||
|
||||
sourceCompatibility = 1.9
|
||||
targetCompatibility = 1.9
|
||||
def os = org.gradle.internal.os.OperatingSystem.current()
|
||||
def osName = os.getFamilyName()
|
||||
if(os.macOsX) {
|
||||
osName = "osx"
|
||||
}
|
||||
|
||||
sourceCompatibility = 16
|
||||
targetCompatibility = 16
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
|
|
@ -49,13 +44,17 @@ dependencies {
|
|||
implementation ('org.bouncycastle:bcprov-jdk15on:1.64') {
|
||||
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: '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.slf4j'
|
||||
}
|
||||
implementation ('org.slf4j:slf4j-api:1.7.30')
|
||||
testImplementation ('junit:junit:4.12') {
|
||||
exclude group: 'org.hamcrest', module: 'hamcrest-core'
|
||||
}
|
||||
|
|
@ -63,8 +62,16 @@ dependencies {
|
|||
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) {
|
||||
main = 'com.sparrowwallet.drongo.Main'
|
||||
mainClass = 'com.sparrowwallet.drongo.Main'
|
||||
classpath = sourceSets.main.runtimeClasspath
|
||||
args 'drongo.properties'
|
||||
}
|
||||
|
|
@ -84,3 +91,30 @@ shadowJar {
|
|||
archiveVersion = '0.9'
|
||||
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
21
buildSrc/build.gradle
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
|
|
@ -1,5 +1,5 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
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
|
||||
zipStorePath=wrapper/dists
|
||||
|
|
|
|||
275
gradlew
vendored
275
gradlew
vendored
|
|
@ -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");
|
||||
# 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
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
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.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
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."
|
||||
fi
|
||||
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.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
|
|
@ -105,79 +140,101 @@ location of your Java installation."
|
|||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin 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" ;;
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=`save "$@"`
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --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
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
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" "$@"
|
||||
|
|
|
|||
34
gradlew.bat
vendored
34
gradlew.bat
vendored
|
|
@ -14,7 +14,7 @@
|
|||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
|
|
@ -25,7 +25,7 @@
|
|||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
|
|
@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
|
|||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto init
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
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_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto init
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
|
|
@ -64,38 +64,26 @@ echo location of your Java installation.
|
|||
|
||||
goto fail
|
||||
|
||||
:init
|
||||
@rem Get command-line arguments, handling Windows variants
|
||||
|
||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||
|
||||
:win9xME_args
|
||||
@rem Slurp the command line arguments.
|
||||
set CMD_LINE_ARGS=
|
||||
set _SKIP=2
|
||||
|
||||
:win9xME_args_slurp
|
||||
if "x%~1" == "x" goto execute
|
||||
|
||||
set CMD_LINE_ARGS=%*
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -17,7 +17,7 @@ public enum BitcoinUnit {
|
|||
BTC("GRS") {
|
||||
@Override
|
||||
public long getSatsValue(double unitValue) {
|
||||
return (long)(unitValue * Transaction.SATOSHIS_PER_BITCOIN);
|
||||
return Math.round(unitValue * Transaction.SATOSHIS_PER_BITCOIN);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
package com.sparrowwallet.drongo;
|
||||
|
||||
import com.sparrowwallet.drongo.rpc.BitcoinJSONRPCClient;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.slf4j.event.Level;
|
||||
import org.zeromq.SocketType;
|
||||
import org.zeromq.ZContext;
|
||||
import org.zeromq.ZMQ;
|
||||
|
||||
import java.security.Provider;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
|
@ -74,4 +77,18 @@ public class Drongo {
|
|||
public List<WatchWallet> getWallets() {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -206,7 +206,7 @@ public class ExtendedKey {
|
|||
}
|
||||
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -5,14 +5,19 @@ import com.sparrowwallet.drongo.crypto.ChildNumber;
|
|||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public class KeyDerivation {
|
||||
private final String masterFingerprint;
|
||||
private final String derivationPath;
|
||||
private transient List<ChildNumber> derivation;
|
||||
|
||||
public KeyDerivation(String masterFingerprint, List<ChildNumber> derivation) {
|
||||
this(masterFingerprint, writePath(derivation));
|
||||
}
|
||||
|
||||
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.derivation = parsePath(derivationPath);
|
||||
}
|
||||
|
|
@ -68,13 +73,17 @@ public class KeyDerivation {
|
|||
}
|
||||
|
||||
public static String writePath(List<ChildNumber> pathList) {
|
||||
String path = "m";
|
||||
for (ChildNumber child: pathList) {
|
||||
path += "/";
|
||||
path += child.toString();
|
||||
return writePath(pathList, true);
|
||||
}
|
||||
|
||||
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) {
|
||||
|
|
@ -87,6 +96,10 @@ public class KeyDerivation {
|
|||
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() {
|
||||
return new KeyDerivation(masterFingerprint, derivationPath);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,18 @@ package com.sparrowwallet.drongo;
|
|||
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public enum KeyPurpose {
|
||||
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;
|
||||
|
||||
KeyPurpose(ChildNumber pathIndex) {
|
||||
|
|
|
|||
7
src/main/java/com/sparrowwallet/drongo/LogHandler.java
Normal file
7
src/main/java/com/sparrowwallet/drongo/LogHandler.java
Normal 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);
|
||||
}
|
||||
118
src/main/java/com/sparrowwallet/drongo/NativeUtils.java
Normal file
118
src/main/java/com/sparrowwallet/drongo/NativeUtils.java
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,16 @@
|
|||
package com.sparrowwallet.drongo;
|
||||
|
||||
public enum Network {
|
||||
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);
|
||||
import java.util.Locale;
|
||||
|
||||
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.p2pkhAddressHeader = p2pkhAddressHeader;
|
||||
this.p2pkhAddressPrefix = p2pkhAddressPrefix;
|
||||
|
|
@ -14,6 +19,7 @@ public enum Network {
|
|||
this.bech32AddressHrp = bech32AddressHrp;
|
||||
this.xprvHeader = xprvHeader;
|
||||
this.xpubHeader = xpubHeader;
|
||||
this.dumpedPrivateKeyHeader = dumpedPrivateKeyHeader;
|
||||
this.defaultPort = defaultPort;
|
||||
}
|
||||
|
||||
|
|
@ -25,6 +31,7 @@ public enum Network {
|
|||
private final String bech32AddressHrp;
|
||||
private final ExtendedKey.Header xprvHeader;
|
||||
private final ExtendedKey.Header xpubHeader;
|
||||
private final int dumpedPrivateKeyHeader;
|
||||
private final int defaultPort;
|
||||
|
||||
private static Network currentNetwork;
|
||||
|
|
@ -33,6 +40,10 @@ public enum Network {
|
|||
return name;
|
||||
}
|
||||
|
||||
public String toDisplayString() {
|
||||
return name.substring(0, 1).toUpperCase(Locale.ROOT) + name.substring(1);
|
||||
}
|
||||
|
||||
public int getP2PKHAddressHeader() {
|
||||
return p2pkhAddressHeader;
|
||||
}
|
||||
|
|
@ -53,6 +64,10 @@ public enum Network {
|
|||
return xpubHeader;
|
||||
}
|
||||
|
||||
public int getDumpedPrivateKeyHeader() {
|
||||
return dumpedPrivateKeyHeader;
|
||||
}
|
||||
|
||||
public int getDefaultPort() {
|
||||
return defaultPort;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,10 +9,7 @@ import com.sparrowwallet.drongo.policy.PolicyType;
|
|||
import com.sparrowwallet.drongo.protocol.ProtocolException;
|
||||
import com.sparrowwallet.drongo.protocol.Script;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.KeystoreSource;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.*;
|
||||
|
|
@ -25,10 +22,11 @@ public class OutputDescriptor {
|
|||
private static final String INPUT_CHARSET = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ ";
|
||||
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 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 MULTIPATH_PATTERN = Pattern.compile("<([\\d*'hH;]+)>");
|
||||
private static final Pattern CHECKSUM_PATTERN = Pattern.compile("#([" + CHECKSUM_CHARSET + "]{8})$");
|
||||
|
||||
private final ScriptType scriptType;
|
||||
|
|
@ -134,6 +132,10 @@ public class OutputDescriptor {
|
|||
return extendedPublicKeys.size() > 1;
|
||||
}
|
||||
|
||||
public boolean isCosigner() {
|
||||
return !isMultisig() && scriptType.isAllowed(PolicyType.MULTI);
|
||||
}
|
||||
|
||||
public ExtendedKey getSingletonExtendedPublicKey() {
|
||||
if(isMultisig()) {
|
||||
throw new IllegalStateException("Output descriptor contains multiple public keys but singleton requested");
|
||||
|
|
@ -235,7 +237,7 @@ public class OutputDescriptor {
|
|||
|
||||
public Wallet toWallet() {
|
||||
Wallet wallet = new Wallet();
|
||||
wallet.setPolicyType(isMultisig() ? PolicyType.MULTI : PolicyType.SINGLE);
|
||||
wallet.setPolicyType(isMultisig() || isCosigner() ? PolicyType.MULTI : PolicyType.SINGLE);
|
||||
wallet.setScriptType(scriptType);
|
||||
|
||||
for(Map.Entry<ExtendedKey,KeyDerivation> extKeyEntry : extendedPublicKeys.entrySet()) {
|
||||
|
|
@ -252,17 +254,59 @@ public class OutputDescriptor {
|
|||
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) {
|
||||
return getOutputDescriptor(wallet, null);
|
||||
}
|
||||
|
||||
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, String> extendedKeyChildDerivationMap = new LinkedHashMap<>();
|
||||
for(Keystore keystore : wallet.getKeystores()) {
|
||||
extendedKeyDerivationMap.put(keystore.getExtendedPublicKey(), keystore.getKeyDerivation());
|
||||
if(keyPurpose != null) {
|
||||
extendedKeyChildDerivationMap.put(keystore.getExtendedPublicKey(), keyPurpose.getPathIndex().num() + "/*");
|
||||
if(keyPurposes != null) {
|
||||
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);
|
||||
}
|
||||
|
||||
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) {
|
||||
BigInteger c = BigInteger.valueOf(1);
|
||||
int cls = 0;
|
||||
|
|
@ -361,7 +416,7 @@ public class OutputDescriptor {
|
|||
int pos = INPUT_CHARSET.indexOf(ch);
|
||||
|
||||
if(pos < 0) {
|
||||
return "";
|
||||
continue;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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) {
|
||||
c = c.xor(new BigInteger("f5dee51989", 16));
|
||||
|
|
@ -431,7 +486,7 @@ public class OutputDescriptor {
|
|||
builder.append(ScriptType.MULTISIG.getDescriptor());
|
||||
StringJoiner joiner = new StringJoiner(",");
|
||||
joiner.add(Integer.toString(multisigThreshold));
|
||||
for(ExtendedKey pubKey : extendedPublicKeys.keySet()) {
|
||||
for(ExtendedKey pubKey : sortExtendedPubKeys(extendedPublicKeys.keySet())) {
|
||||
String extKeyString = toString(pubKey, addKeyOrigin);
|
||||
joiner.add(extKeyString);
|
||||
}
|
||||
|
|
@ -452,6 +507,47 @@ public class OutputDescriptor {
|
|||
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) {
|
||||
StringBuilder keyBuilder = new StringBuilder();
|
||||
KeyDerivation keyDerivation = extendedPublicKeys.get(pubKey);
|
||||
|
|
@ -461,7 +557,7 @@ public class OutputDescriptor {
|
|||
keyBuilder.append(keyDerivation.getMasterFingerprint());
|
||||
keyBuilder.append("/");
|
||||
}
|
||||
keyBuilder.append(keyDerivation.getDerivationPath().replaceFirst("^m?/", ""));
|
||||
keyBuilder.append(keyDerivation.getDerivationPath().replaceFirst("^m?/", "").replace('\'', 'h'));
|
||||
keyBuilder.append("]");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ package com.sparrowwallet.drongo;
|
|||
|
||||
import ch.qos.logback.core.PropertyDefinerBase;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public class PropertyDefiner extends PropertyDefinerBase {
|
||||
private String application;
|
||||
|
||||
|
|
@ -11,15 +13,15 @@ public class PropertyDefiner extends PropertyDefinerBase {
|
|||
|
||||
@Override
|
||||
public String getPropertyValue() {
|
||||
if(System.getProperty(application.toLowerCase() + ".home") != null) {
|
||||
return System.getProperty(application.toLowerCase() + ".home");
|
||||
if(System.getProperty(application.toLowerCase(Locale.ROOT) + ".home") != null) {
|
||||
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() {
|
||||
String osName = System.getProperty("os.name");
|
||||
return (osName != null && osName.toLowerCase().startsWith("windows"));
|
||||
return (osName != null && osName.toLowerCase(Locale.ROOT).startsWith("windows"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,18 +73,6 @@ public class SecureString implements CharSequence {
|
|||
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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -12,9 +12,7 @@ import java.io.IOException;
|
|||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.Buffer;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.CharBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
|
||||
|
|
@ -115,6 +113,21 @@ public class Utils {
|
|||
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. */
|
||||
public static long readUint32(byte[] bytes, int offset) {
|
||||
return (bytes[offset] & 0xffl) |
|
||||
|
|
@ -285,6 +298,16 @@ public class Utils {
|
|||
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[]> {
|
||||
@Override
|
||||
public int compare(byte[] left, byte[] right) {
|
||||
|
|
|
|||
|
|
@ -7,16 +7,17 @@ import com.sparrowwallet.drongo.protocol.Script;
|
|||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
|
||||
public abstract class Address {
|
||||
protected final byte[] hash;
|
||||
protected final byte[] data;
|
||||
|
||||
public Address(byte[] hash) {
|
||||
this.hash = hash;
|
||||
public Address(byte[] data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public byte[] getHash() {
|
||||
return hash;
|
||||
public byte[] getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public String getAddress() {
|
||||
|
|
@ -24,7 +25,7 @@ public abstract class Address {
|
|||
}
|
||||
|
||||
public String getAddress(Network network) {
|
||||
return Base58.encodeChecked(getVersion(network), hash);
|
||||
return Base58.encodeChecked(getVersion(network), data);
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
|
|
@ -43,23 +44,26 @@ public abstract class Address {
|
|||
|
||||
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 boolean equals(Object obj) {
|
||||
if(!(obj instanceof Address)) {
|
||||
if(!(obj instanceof Address address)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Address address = (Address)obj;
|
||||
return address.getAddress().equals(this.getAddress());
|
||||
return Arrays.equals(data, address.data) && getVersion(Network.get()) == address.getVersion(Network.get());
|
||||
}
|
||||
|
||||
public int hashCode() {
|
||||
return getAddress().hashCode();
|
||||
return Arrays.hashCode(data) + getVersion(Network.get());
|
||||
}
|
||||
|
||||
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 {
|
||||
Bech32.Bech32Data data = Bech32.decode(address);
|
||||
if(data.hrp.equals(network.getBech32AddressHRP())) {
|
||||
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[] witnessProgram = Bech32.convertBits(convertedProgram, 0, convertedProgram.length, 5, 8, false);
|
||||
if (witnessProgram.length == 20) {
|
||||
if(witnessProgram.length == 20) {
|
||||
return new P2WPKHAddress(witnessProgram);
|
||||
}
|
||||
if (witnessProgram.length == 32) {
|
||||
if(witnessProgram.length == 32) {
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -6,11 +6,8 @@ import com.sparrowwallet.drongo.protocol.Script;
|
|||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
|
||||
public class P2PKAddress extends Address {
|
||||
private byte[] pubKey;
|
||||
|
||||
public P2PKAddress(byte[] pubKey) {
|
||||
super(Utils.sha256hash160(pubKey));
|
||||
this.pubKey = pubKey;
|
||||
super(pubKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -18,19 +15,15 @@ public class P2PKAddress extends Address {
|
|||
return network.getP2PKHAddressHeader();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAddress(Network network) {
|
||||
return Utils.bytesToHex(data);
|
||||
}
|
||||
|
||||
public ScriptType getScriptType() {
|
||||
return ScriptType.P2PK;
|
||||
}
|
||||
|
||||
public Script getOutputScript() {
|
||||
return getScriptType().getOutputScript(pubKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getOutputScriptData() {
|
||||
return pubKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getOutputScriptDataType() {
|
||||
return "Public Key";
|
||||
|
|
|
|||
|
|
@ -19,16 +19,6 @@ public class P2PKHAddress extends Address {
|
|||
return ScriptType.P2PKH;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Script getOutputScript() {
|
||||
return getScriptType().getOutputScript(hash);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getOutputScriptData() {
|
||||
return hash;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getOutputScriptDataType() {
|
||||
return "Public Key Hash";
|
||||
|
|
|
|||
|
|
@ -20,16 +20,6 @@ public class P2SHAddress extends Address {
|
|||
return ScriptType.P2SH;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Script getOutputScript() {
|
||||
return getScriptType().getOutputScript(hash);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getOutputScriptData() {
|
||||
return hash;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getOutputScriptDataType() {
|
||||
return "Script Hash";
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
|
@ -17,7 +17,7 @@ public class P2WPKHAddress extends Address {
|
|||
|
||||
@Override
|
||||
public String getAddress(Network network) {
|
||||
return Bech32.encode(network.getBech32AddressHRP(), getVersion(), hash);
|
||||
return Bech32.encode(network.getBech32AddressHRP(), getVersion(), data);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -25,16 +25,6 @@ public class P2WPKHAddress extends Address {
|
|||
return ScriptType.P2WPKH;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Script getOutputScript() {
|
||||
return getScriptType().getOutputScript(hash);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getOutputScriptData() {
|
||||
return hash;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getOutputScriptDataType() {
|
||||
return "Witness Public Key Hash";
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ public class P2WSHAddress extends Address {
|
|||
|
||||
@Override
|
||||
public String getAddress(Network network) {
|
||||
return Bech32.encode(network.getBech32AddressHRP(), getVersion(), hash);
|
||||
return Bech32.encode(network.getBech32AddressHRP(), getVersion(), data);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -23,16 +23,6 @@ public class P2WSHAddress extends Address {
|
|||
return ScriptType.P2WSH;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Script getOutputScript() {
|
||||
return getScriptType().getOutputScript(hash);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getOutputScriptData() {
|
||||
return hash;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getOutputScriptDataType() {
|
||||
return "Witness Script Hash";
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
104
src/main/java/com/sparrowwallet/drongo/bip47/PaymentAddress.java
Normal file
104
src/main/java/com/sparrowwallet/drongo/bip47/PaymentAddress.java
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
393
src/main/java/com/sparrowwallet/drongo/bip47/PaymentCode.java
Normal file
393
src/main/java/com/sparrowwallet/drongo/bip47/PaymentCode.java
Normal 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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
165
src/main/java/com/sparrowwallet/drongo/crypto/BIP38.java
Normal file
165
src/main/java/com/sparrowwallet/drongo/crypto/BIP38.java
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -46,7 +46,11 @@ public class ChildNumber {
|
|||
public int i() { return i; }
|
||||
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
package com.sparrowwallet.drongo.crypto;
|
||||
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.protocol.SignatureDecodeException;
|
||||
import com.sparrowwallet.drongo.protocol.VarInt;
|
||||
import com.sparrowwallet.drongo.protocol.*;
|
||||
import org.bitcoin.NativeSecp256k1;
|
||||
import org.bitcoin.NativeSecp256k1Util;
|
||||
import org.bitcoin.Secp256k1Context;
|
||||
import org.bouncycastle.asn1.*;
|
||||
import org.bouncycastle.asn1.x9.X9ECParameters;
|
||||
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.ECPublicKeyParameters;
|
||||
import org.bouncycastle.crypto.signers.ECDSASigner;
|
||||
import org.bouncycastle.math.ec.ECAlgorithms;
|
||||
import org.bouncycastle.math.ec.ECPoint;
|
||||
import org.bouncycastle.math.ec.FixedPointCombMultiplier;
|
||||
import org.bouncycastle.math.ec.FixedPointUtil;
|
||||
import org.bouncycastle.math.ec.*;
|
||||
import org.bouncycastle.math.ec.custom.sec.SecP256K1Curve;
|
||||
import org.bouncycastle.util.Properties;
|
||||
import org.bouncycastle.util.encoders.Hex;
|
||||
import org.slf4j.Logger;
|
||||
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
|
||||
* 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);
|
||||
|
||||
/** 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.
|
||||
private static final X9ECParameters CURVE_PARAMS = CustomNamedCurves.getByName("secp256k1");
|
||||
|
||||
|
|
@ -108,13 +92,6 @@ public class ECKey implements EncryptableItem {
|
|||
protected final BigInteger priv;
|
||||
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;
|
||||
|
||||
/**
|
||||
|
|
@ -159,18 +136,6 @@ public class ECKey implements EncryptableItem {
|
|||
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.
|
||||
* See the ECKey class docs for a discussion of point compression.
|
||||
|
|
@ -268,11 +233,6 @@ public class ECKey implements EncryptableItem {
|
|||
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
|
||||
* in its wallet storage format.
|
||||
|
|
@ -315,13 +275,20 @@ public class ECKey implements EncryptableItem {
|
|||
* use {@code new BigInteger(1, bytes);}
|
||||
*/
|
||||
public static ECPoint publicPointFromPrivate(BigInteger privKey) {
|
||||
/*
|
||||
* TODO: FixedPointCombMultiplier currently doesn't support scalars longer than the group order,
|
||||
* but that could change in future versions.
|
||||
*/
|
||||
if (privKey.bitLength() > CURVE.getN().bitLength()) {
|
||||
privKey = privKey.mod(CURVE.getN());
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -340,6 +307,13 @@ public class ECKey implements EncryptableItem {
|
|||
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. */
|
||||
public ECPoint getPubKeyPoint() {
|
||||
return pub.get();
|
||||
|
|
@ -359,6 +333,17 @@ public class ECKey implements EncryptableItem {
|
|||
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.
|
||||
*/
|
||||
|
|
@ -366,257 +351,129 @@ public class ECKey implements EncryptableItem {
|
|||
return pub.isCompressed();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 static class ECDSASignature {
|
||||
/** The two components of the signature. */
|
||||
public final BigInteger r, s;
|
||||
public TransactionSignature sign(Sha256Hash input, SigHash sigHash, TransactionSignature.Type type) {
|
||||
TransactionSignature transactionSignature;
|
||||
|
||||
/**
|
||||
* 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(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);
|
||||
if(type == TransactionSignature.Type.SCHNORR) {
|
||||
SchnorrSignature schnorrSignature = signSchnorr(input);
|
||||
transactionSignature = new TransactionSignature(schnorrSignature, sigHash);
|
||||
} else {
|
||||
// No decryption of private key required.
|
||||
if (priv == null) {
|
||||
throw new MissingPrivateKeyException();
|
||||
}
|
||||
ECDSASignature ecdsaSignature = signEcdsa(input);
|
||||
transactionSignature = new TransactionSignature(ecdsaSignature, sigHash);
|
||||
}
|
||||
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");
|
||||
}
|
||||
|
||||
ECDSASignature signature;
|
||||
int counter = 0;
|
||||
Integer counter = null;
|
||||
do {
|
||||
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);
|
||||
BigInteger[] components = signer.generateSignature(input.getBytes());
|
||||
signature = new ECDSASignature(components[0], components[1]).toCanonicalised();
|
||||
counter++;
|
||||
counter = (counter == null ? 1 : counter+1);
|
||||
} while(!signature.hasLowR());
|
||||
|
||||
return signature;
|
||||
}
|
||||
|
||||
/**
|
||||
* <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 signature ASN.1 encoded signature.
|
||||
* @param pub The public key bytes to use.
|
||||
* Signs the given hash and returns the R and S components as a SchnorrSignature.
|
||||
*/
|
||||
public static boolean verify(byte[] data, ECDSASignature signature, byte[] pub) {
|
||||
ECDSASigner signer = new ECDSASigner();
|
||||
ECPublicKeyParameters params = new ECPublicKeyParameters(CURVE.getCurve().decodePoint(pub), CURVE);
|
||||
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;
|
||||
public SchnorrSignature signSchnorr(Sha256Hash input) {
|
||||
if(priv == null) {
|
||||
throw new IllegalArgumentException("Private key cannot be null");
|
||||
}
|
||||
|
||||
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.
|
||||
*
|
||||
* @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.
|
||||
* Verifies the given TransactionSignature against the provided byte array using the public key.
|
||||
*/
|
||||
public static boolean verify(byte[] data, byte[] signature, byte[] pub) throws SignatureDecodeException {
|
||||
return verify(data, ECDSASignature.decodeFromDER(signature), pub);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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());
|
||||
public boolean verify(byte[] data, TransactionSignature signature) {
|
||||
return signature.verify(data, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the given R/S pair (signature) against a hash using the public key.
|
||||
*/
|
||||
public boolean verify(Sha256Hash sigHash, ECDSASignature signature) {
|
||||
return ECKey.verify(sigHash.getBytes(), signature, getPubKey());
|
||||
public boolean verify(Sha256Hash sigHash, TransactionSignature signature) {
|
||||
return verify(sigHash.getBytes(), signature);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the given ASN.1 encoded ECDSA signature against a hash using the public key, and throws an exception
|
||||
* if the signature doesn't match
|
||||
* @throws SignatureDecodeException if the signature is unparseable in some way.
|
||||
* @throws java.security.SignatureException if the signature does not match.
|
||||
*/
|
||||
public void verifyOrThrow(byte[] hash, byte[] signature) throws SignatureDecodeException, SignatureException {
|
||||
if (!verify(hash, signature)) {
|
||||
throw new SignatureException();
|
||||
public ECKey getTweakedOutputKey() {
|
||||
TaprootPubKey taprootPubKey = liftX(getPubKeyXCoord());
|
||||
ECPoint internalKey = taprootPubKey.ecPoint;
|
||||
byte[] taggedHash = Utils.taggedHash("TapTweak", internalKey.getXCoord().getEncoded());
|
||||
ECKey tweakValue = ECKey.fromPrivate(taggedHash);
|
||||
ECPoint outputKey = internalKey.add(tweakValue.getPubKeyPoint());
|
||||
|
||||
if(hasPrivKey()) {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the given R/S pair (signature) against a hash using the public key, and throws an exception
|
||||
* if the signature doesn't match
|
||||
* @throws java.security.SignatureException if the signature does not match.
|
||||
*/
|
||||
public void verifyOrThrow(Sha256Hash sigHash, ECDSASignature signature) throws SignatureException {
|
||||
if (!ECKey.verify(sigHash.getBytes(), signature, getPubKey())) {
|
||||
throw new SignatureException();
|
||||
private static TaprootPubKey liftX(byte[] bytes) {
|
||||
SecP256K1Curve secP256K1Curve = (SecP256K1Curve)CURVE_PARAMS.getCurve();
|
||||
BigInteger x = new BigInteger(1, bytes);
|
||||
BigInteger p = secP256K1Curve.getQ();
|
||||
if(x.compareTo(p) > -1) {
|
||||
throw new IllegalArgumentException("Provided bytes must be less than secp256k1 field size");
|
||||
}
|
||||
|
||||
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.
|
||||
*/
|
||||
public static boolean isPubKeyCanonical(byte[] pubkey) {
|
||||
if (pubkey.length < 33)
|
||||
if (pubkey.length < 32)
|
||||
return false;
|
||||
if (pubkey.length == 32)
|
||||
return true;
|
||||
if (pubkey[0] == 0x04) {
|
||||
// Uncompressed pubkey
|
||||
if (pubkey.length != 65)
|
||||
|
|
@ -643,7 +502,7 @@ public class ECKey implements EncryptableItem {
|
|||
* Returns true if the given pubkey is in its compressed form.
|
||||
*/
|
||||
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;
|
||||
else if (encoded.length == 65 && encoded[0] == 0x04)
|
||||
return false;
|
||||
|
|
@ -715,12 +574,11 @@ public class ECKey implements EncryptableItem {
|
|||
* encoded string.
|
||||
*
|
||||
* @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);
|
||||
Sha256Hash hash = Sha256Hash.of(data);
|
||||
ECDSASignature sig = sign(hash, aesKey);
|
||||
ECDSASignature sig = signEcdsa(hash);
|
||||
byte recId = findRecoveryId(hash, sig);
|
||||
int headerByte = recId + getSigningTypeConstant(scriptType);
|
||||
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 KeyIsEncryptedException extends MissingPrivateKeyException {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
|
|
|
|||
|
|
@ -38,6 +38,10 @@ public class HDKeyDerivation {
|
|||
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 {
|
||||
if(parent.isPubKeyOnly()) {
|
||||
RawKeyBytes rawKey = deriveChildKeyBytesFromPublic(parent, childNumber);
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ public class HMacDSANonceKCalculator implements DSAKCalculator {
|
|||
private final HMac hMac;
|
||||
private final byte[] K;
|
||||
private final byte[] V;
|
||||
private final long counter;
|
||||
private final Long counter;
|
||||
|
||||
private BigInteger n;
|
||||
|
||||
|
|
@ -31,11 +31,11 @@ public class HMacDSANonceKCalculator implements DSAKCalculator {
|
|||
* @param digest digest to build the HMAC on.
|
||||
* @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.V = 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()
|
||||
|
|
@ -74,8 +74,12 @@ public class HMacDSANonceKCalculator implements DSAKCalculator {
|
|||
|
||||
System.arraycopy(mVal, 0, m, m.length - mVal.length, mVal.length);
|
||||
|
||||
BigInteger additional = BigInteger.valueOf(counter);
|
||||
byte[] aData = Utils.bigIntegerToBytes(additional, size);
|
||||
byte[] c = null;
|
||||
if(counter != null) {
|
||||
BigInteger additional = BigInteger.valueOf(counter);
|
||||
c = Utils.bigIntegerToBytes(additional, size);
|
||||
Utils.reverse(c);
|
||||
}
|
||||
|
||||
hMac.init(new KeyParameter(K));
|
||||
|
||||
|
|
@ -83,7 +87,9 @@ public class HMacDSANonceKCalculator implements DSAKCalculator {
|
|||
hMac.update((byte)0x00);
|
||||
hMac.update(x, 0, x.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);
|
||||
|
||||
|
|
@ -97,6 +103,9 @@ public class HMacDSANonceKCalculator implements DSAKCalculator {
|
|||
hMac.update((byte)0x01);
|
||||
hMac.update(x, 0, x.length);
|
||||
hMac.update(m, 0, m.length);
|
||||
if(counter != null) {
|
||||
hMac.update(c, 0, c.length);
|
||||
}
|
||||
|
||||
hMac.doFinal(K, 0);
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ public class LazyECPoint {
|
|||
|
||||
public LazyECPoint(ECCurve curve, byte[] bits) {
|
||||
this.curve = curve;
|
||||
this.bits = bits;
|
||||
this.bits = (bits != null && bits.length == 32 ? addYCoord(bits) : bits);
|
||||
this.compressed = ECKey.isPubKeyCompressed(bits);
|
||||
}
|
||||
|
||||
|
|
@ -61,6 +61,13 @@ public class LazyECPoint {
|
|||
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() {
|
||||
return Hex.toHexString(getEncoded());
|
||||
}
|
||||
|
|
@ -80,4 +87,11 @@ public class LazyECPoint {
|
|||
private byte[] getCanonicalEncoding() {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import java.util.regex.Pattern;
|
|||
|
||||
public class Miniscript {
|
||||
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 String script;
|
||||
|
|
@ -27,6 +28,11 @@ public class Miniscript {
|
|||
return 1;
|
||||
}
|
||||
|
||||
Matcher taprootMatcher = TAPROOT_PATTERN.matcher(script);
|
||||
if(taprootMatcher.find()) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
Matcher multiMatcher = MULTI_PATTERN.matcher(script);
|
||||
if(multiMatcher.find()) {
|
||||
String threshold = multiMatcher.group(1);
|
||||
|
|
|
|||
|
|
@ -2,13 +2,14 @@ package com.sparrowwallet.drongo.policy;
|
|||
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.Persistable;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static com.sparrowwallet.drongo.protocol.ScriptType.*;
|
||||
import static com.sparrowwallet.drongo.policy.PolicyType.*;
|
||||
|
||||
public class Policy {
|
||||
public class Policy extends Persistable {
|
||||
private static final String DEFAULT_NAME = "Default";
|
||||
|
||||
private String name;
|
||||
|
|
@ -23,6 +24,10 @@ public class Policy {
|
|||
this.miniscript = miniscript;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public Miniscript getMiniscript() {
|
||||
return miniscript;
|
||||
}
|
||||
|
|
@ -57,6 +62,8 @@ public class Policy {
|
|||
}
|
||||
|
||||
public Policy copy() {
|
||||
return new Policy(name, miniscript.copy());
|
||||
Policy policy = new Policy(name, miniscript.copy());
|
||||
policy.setId(getId());
|
||||
return policy;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,10 +39,18 @@ public class Bech32 {
|
|||
public static class Bech32Data {
|
||||
public final String hrp;
|
||||
public final byte[] data;
|
||||
public final Encoding encoding;
|
||||
|
||||
private Bech32Data(final String hrp, final byte[] data) {
|
||||
this.hrp = hrp;
|
||||
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. */
|
||||
private static byte[] expandHrp(final String hrp) {
|
||||
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) {
|
||||
int c = hrp.charAt(i) & 0x7f; // Limit to standard 7-bit ASCII
|
||||
ret[i] = (byte) ((c >>> 5) & 0x07);
|
||||
|
|
@ -75,21 +83,29 @@ public class Bech32 {
|
|||
}
|
||||
|
||||
/** 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[] combined = new byte[hrpExpanded.length + values.length];
|
||||
System.arraycopy(hrpExpanded, 0, combined, 0, hrpExpanded.length);
|
||||
System.arraycopy(values, 0, combined, hrpExpanded.length, values.length);
|
||||
return polymod(combined) == 1;
|
||||
|
||||
int check = polymod(combined);
|
||||
for(Encoding encoding : Encoding.values()) {
|
||||
if(check == encoding.checksumConstant) {
|
||||
return encoding;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 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[] enc = new byte[hrpExpanded.length + values.length + 6];
|
||||
System.arraycopy(hrpExpanded, 0, enc, 0, hrpExpanded.length);
|
||||
System.arraycopy(values, 0, enc, hrpExpanded.length, values.length);
|
||||
int mod = polymod(enc) ^ 1;
|
||||
int mod = polymod(enc) ^ encoding.checksumConstant;
|
||||
byte[] ret = new byte[6];
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
ret[i] = (byte) ((mod >>> (5 * (5 - i))) & 31);
|
||||
|
|
@ -99,16 +115,17 @@ public class Bech32 {
|
|||
|
||||
/** Encode a Bech32 string. */
|
||||
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. */
|
||||
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. */
|
||||
public static String encode(String hrp, final byte[] values) {
|
||||
public static String encode(String hrp, Encoding encoding, final byte[] values) {
|
||||
if(hrp.length() < 1) {
|
||||
throw new ProtocolException("Human-readable part is too short");
|
||||
}
|
||||
|
|
@ -118,7 +135,7 @@ public class Bech32 {
|
|||
}
|
||||
|
||||
hrp = hrp.toLowerCase(Locale.ROOT);
|
||||
byte[] checksum = createChecksum(hrp, values);
|
||||
byte[] checksum = createChecksum(hrp, encoding, values);
|
||||
byte[] combined = new byte[values.length + checksum.length];
|
||||
System.arraycopy(values, 0, combined, 0, values.length);
|
||||
System.arraycopy(checksum, 0, combined, values.length, checksum.length);
|
||||
|
|
@ -133,10 +150,14 @@ public class Bech32 {
|
|||
|
||||
/** Decode a Bech32 string. */
|
||||
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;
|
||||
if (str.length() < 8)
|
||||
throw new ProtocolException("Input too short: " + str.length());
|
||||
if (str.length() > 90)
|
||||
if (str.length() > limit)
|
||||
throw new ProtocolException("Input too long: " + str.length());
|
||||
for (int i = 0; i < str.length(); ++i) {
|
||||
char c = str.charAt(i);
|
||||
|
|
@ -163,14 +184,18 @@ public class Bech32 {
|
|||
values[i] = CHARSET_REV[c];
|
||||
}
|
||||
String hrp = str.substring(0, pos).toLowerCase(Locale.ROOT);
|
||||
if (!verifyChecksum(hrp, values)) throw new ProtocolException("Invalid checksum");
|
||||
return new Bech32Data(hrp, Arrays.copyOfRange(values, 0, values.length - 6));
|
||||
Encoding encoding = verifyChecksum(hrp, values);
|
||||
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) {
|
||||
byte[] convertedProgram = convertBits(witnessProgram, 0, witnessProgram.length, 8, 5, true);
|
||||
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);
|
||||
return bytes;
|
||||
}
|
||||
|
|
@ -206,4 +231,14 @@ public class Bech32 {
|
|||
}
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
public enum Encoding {
|
||||
BECH32(1), BECH32M(0x2bc830a3);
|
||||
|
||||
private final int checksumConstant;
|
||||
|
||||
Encoding(int checksumConstant) {
|
||||
this.checksumConstant = checksumConstant;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
package com.sparrowwallet.drongo.protocol;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Date;
|
||||
|
||||
import static com.sparrowwallet.drongo.Utils.uint32ToByteStreamLE;
|
||||
|
||||
public class BlockHeader extends Message {
|
||||
private long version;
|
||||
private Sha256Hash prevBlockHash;
|
||||
|
|
@ -14,6 +19,16 @@ public class BlockHeader extends Message {
|
|||
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
|
||||
protected void parse() throws ProtocolException {
|
||||
version = readUint32();
|
||||
|
|
@ -57,4 +72,25 @@ public class BlockHeader extends Message {
|
|||
public long getNonce() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -135,6 +135,21 @@ public class Script {
|
|||
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>
|
||||
*
|
||||
|
|
@ -150,6 +165,14 @@ public class Script {
|
|||
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.
|
||||
*/
|
||||
|
|
@ -160,8 +183,15 @@ public class Script {
|
|||
}
|
||||
}
|
||||
|
||||
if(P2PK.isScriptType(this)) {
|
||||
return new Address[] { P2PK.getAddress(P2PK.getPublicKeyFromScript(this).getPubKey()) };
|
||||
//Special handling for taproot tweaked keys - we don't want to tweak them again
|
||||
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)) {
|
||||
|
|
@ -178,7 +208,8 @@ public class Script {
|
|||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -110,8 +110,8 @@ public class ScriptChunk {
|
|||
}
|
||||
|
||||
try {
|
||||
ECKey.ECDSASignature.decodeFromDER(data);
|
||||
} catch(SignatureDecodeException e) {
|
||||
TransactionSignature.decodeFromBitcoin(data, false);
|
||||
} catch(Exception e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -120,7 +120,7 @@ public class ScriptChunk {
|
|||
|
||||
public TransactionSignature getSignature() {
|
||||
try {
|
||||
return TransactionSignature.decodeFromBitcoin(data, false, false);
|
||||
return TransactionSignature.decodeFromBitcoin(data, false);
|
||||
} catch(SignatureDecodeException e) {
|
||||
throw new ProtocolException("Could not decode signature", e);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import com.sparrowwallet.drongo.crypto.ChildNumber;
|
|||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.Month;
|
||||
import java.util.*;
|
||||
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;
|
||||
|
||||
public enum ScriptType {
|
||||
P2PK("P2PK", "m/44'/17'/0'") {
|
||||
P2PK("P2PK", "Legacy (P2PK)", "m/44'/17'/0'") {
|
||||
@Override
|
||||
public Address getAddress(byte[] pubKey) {
|
||||
return new P2PKAddress(pubKey);
|
||||
|
|
@ -126,12 +128,17 @@ public enum ScriptType {
|
|||
throw new ProtocolException(getName() + " is not a multisig script type");
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransactionSignature.Type getSignatureType() {
|
||||
return TransactionSignature.Type.ECDSA;
|
||||
};
|
||||
|
||||
@Override
|
||||
public List<PolicyType> getAllowedPolicyTypes() {
|
||||
return List.of(SINGLE);
|
||||
}
|
||||
},
|
||||
P2PKH("P2PKH", "m/44'/17'/0'") {
|
||||
P2PKH("P2PKH", "Legacy (P2PKH)", "m/44'/17'/0'") {
|
||||
@Override
|
||||
public Address getAddress(byte[] pubKeyHash) {
|
||||
return new P2PKHAddress(pubKeyHash);
|
||||
|
|
@ -239,12 +246,17 @@ public enum ScriptType {
|
|||
throw new ProtocolException(getName() + " is not a multisig script type");
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransactionSignature.Type getSignatureType() {
|
||||
return TransactionSignature.Type.ECDSA;
|
||||
};
|
||||
|
||||
@Override
|
||||
public List<PolicyType> getAllowedPolicyTypes() {
|
||||
return List.of(SINGLE);
|
||||
}
|
||||
},
|
||||
MULTISIG("Bare Multisig", "m/44'/17'/0'") {
|
||||
MULTISIG("Bare Multisig", "Bare Multisig", "m/44'/17'/0'") {
|
||||
@Override
|
||||
public Address getAddress(byte[] bytes) {
|
||||
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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransactionSignature.Type getSignatureType() {
|
||||
return TransactionSignature.Type.ECDSA;
|
||||
};
|
||||
|
||||
@Override
|
||||
public List<PolicyType> getAllowedPolicyTypes() {
|
||||
return List.of(MULTI);
|
||||
}
|
||||
},
|
||||
P2SH("P2SH", "m/45'") {
|
||||
P2SH("P2SH", "Legacy (P2SH)", "m/45'") {
|
||||
@Override
|
||||
public Address getAddress(byte[] scriptHash) {
|
||||
return new P2SHAddress(scriptHash);
|
||||
|
|
@ -550,12 +567,17 @@ public enum ScriptType {
|
|||
return transaction.addInput(prevOutput.getHash(), prevOutput.getIndex(), scriptSig);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransactionSignature.Type getSignatureType() {
|
||||
return TransactionSignature.Type.ECDSA;
|
||||
};
|
||||
|
||||
@Override
|
||||
public List<PolicyType> getAllowedPolicyTypes() {
|
||||
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
|
||||
public Address getAddress(byte[] scriptHash) {
|
||||
return P2SH.getAddress(scriptHash);
|
||||
|
|
@ -653,12 +675,17 @@ public enum ScriptType {
|
|||
throw new ProtocolException(getName() + " is not a multisig script type");
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransactionSignature.Type getSignatureType() {
|
||||
return TransactionSignature.Type.ECDSA;
|
||||
};
|
||||
|
||||
@Override
|
||||
public List<PolicyType> getAllowedPolicyTypes() {
|
||||
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
|
||||
public Address getAddress(byte[] scriptHash) {
|
||||
return P2SH.getAddress(scriptHash);
|
||||
|
|
@ -754,12 +781,17 @@ public enum ScriptType {
|
|||
return transaction.addInput(prevOutput.getHash(), prevOutput.getIndex(), scriptSig, witness);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransactionSignature.Type getSignatureType() {
|
||||
return TransactionSignature.Type.ECDSA;
|
||||
};
|
||||
|
||||
@Override
|
||||
public List<PolicyType> getAllowedPolicyTypes() {
|
||||
return List.of(MULTI, CUSTOM);
|
||||
}
|
||||
},
|
||||
P2WPKH("P2WPKH", "m/84'/17'/0'") {
|
||||
P2WPKH("P2WPKH", "Native Segwit (P2WPKH)", "m/84'/17'/0'") {
|
||||
@Override
|
||||
public Address getAddress(byte[] pubKeyHash) {
|
||||
return new P2WPKHAddress(pubKeyHash);
|
||||
|
|
@ -859,12 +891,17 @@ public enum ScriptType {
|
|||
throw new ProtocolException(getName() + " is not a multisig script type");
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransactionSignature.Type getSignatureType() {
|
||||
return TransactionSignature.Type.ECDSA;
|
||||
};
|
||||
|
||||
@Override
|
||||
public List<PolicyType> getAllowedPolicyTypes() {
|
||||
return List.of(SINGLE);
|
||||
}
|
||||
},
|
||||
P2WSH("P2WSH", "m/48'/17'/0'/2'") {
|
||||
P2WSH("P2WSH", "Native Segwit (P2WSH)", "m/48'/17'/0'/2'") {
|
||||
@Override
|
||||
public Address getAddress(byte[] scriptHash) {
|
||||
return new P2WSHAddress(scriptHash);
|
||||
|
|
@ -970,17 +1007,144 @@ public enum ScriptType {
|
|||
return transaction.addInput(prevOutput.getHash(), prevOutput.getIndex(), scriptSig, witness);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransactionSignature.Type getSignatureType() {
|
||||
return TransactionSignature.Type.ECDSA;
|
||||
};
|
||||
|
||||
@Override
|
||||
public List<PolicyType> getAllowedPolicyTypes() {
|
||||
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 description;
|
||||
private final String defaultDerivationPath;
|
||||
|
||||
ScriptType(String name, String defaultDerivationPath) {
|
||||
ScriptType(String name, String description, String defaultDerivationPath) {
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
this.defaultDerivationPath = defaultDerivationPath;
|
||||
}
|
||||
|
||||
|
|
@ -988,6 +1152,10 @@ public enum ScriptType {
|
|||
return name;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public String getDefaultDerivationPath() {
|
||||
return Network.get() != Network.MAINNET ? defaultDerivationPath.replace("/17'/0'", "/1'/0'") : defaultDerivationPath;
|
||||
}
|
||||
|
|
@ -1027,6 +1195,10 @@ public enum ScriptType {
|
|||
return getAllowedPolicyTypes().contains(policyType);
|
||||
}
|
||||
|
||||
public ECKey getOutputKey(ECKey derivedKey) {
|
||||
return derivedKey;
|
||||
}
|
||||
|
||||
public abstract Address getAddress(byte[] bytes);
|
||||
|
||||
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 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[] 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[] 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) {
|
||||
return Arrays.stream(values()).filter(scriptType -> scriptType.isAllowed(policyType)).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
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) {
|
||||
|
|
@ -1110,7 +1288,7 @@ public enum ScriptType {
|
|||
scriptTypes.sort((o1, o2) -> o2.getDescriptor().length() - o1.getDescriptor().length());
|
||||
|
||||
for(ScriptType scriptType : scriptTypes) {
|
||||
if(descriptor.toLowerCase().startsWith(scriptType.getDescriptor())) {
|
||||
if(descriptor.toLowerCase(Locale.ROOT).startsWith(scriptType.getDescriptor())) {
|
||||
return scriptType;
|
||||
}
|
||||
}
|
||||
|
|
@ -1160,6 +1338,9 @@ public enum ScriptType {
|
|||
return (32 + 4 + 1 + 13 + (107 / WITNESS_SCALE_FACTOR) + 4);
|
||||
} else if(P2SH_P2WSH.equals(this)) {
|
||||
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)) {
|
||||
//Return length of spending input with 75% discount to script size
|
||||
return (32 + 4 + 1 + (107 / WITNESS_SCALE_FACTOR) + 4);
|
||||
|
|
|
|||
|
|
@ -1,22 +1,27 @@
|
|||
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
|
||||
* transaction can be redeemed, specifically, they control how the hash of the transaction is calculated.
|
||||
*/
|
||||
public enum SigHash {
|
||||
ALL("All (Recommended)", (byte)1),
|
||||
ALL("All", (byte)1),
|
||||
NONE("None", (byte)2),
|
||||
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_ALL("All + Anyone Can Pay", (byte)0x81),
|
||||
ANYONECANPAY_NONE("None + Anyone Can Pay", (byte)0x82),
|
||||
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;
|
||||
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) {
|
||||
this.name = name;
|
||||
this.value = value;
|
||||
|
|
|
|||
|
|
@ -22,7 +22,8 @@ public class Transaction extends ChildMessage {
|
|||
public static final long SATOSHIS_PER_BITCOIN = 100 * 1000 * 1000L;
|
||||
public static final long MAX_BLOCK_LOCKTIME = 500000000L;
|
||||
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
|
||||
//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
|
||||
public static final double DEFAULT_MIN_RELAY_FEE = 1d;
|
||||
|
||||
public static final byte LEAF_VERSION_TAPSCRIPT = (byte)0xc0;
|
||||
|
||||
private long version;
|
||||
private long locktime;
|
||||
private boolean segwit;
|
||||
private int segwitVersion;
|
||||
private int segwitFlag;
|
||||
|
||||
private Sha256Hash cachedTxId;
|
||||
private Sha256Hash cachedWTxId;
|
||||
|
|
@ -89,8 +92,6 @@ public class Transaction extends ChildMessage {
|
|||
}
|
||||
|
||||
public boolean isReplaceByFee() {
|
||||
if(locktime == 0) return false;
|
||||
|
||||
for(TransactionInput input : inputs) {
|
||||
if(input.isReplaceByFeeEnabled()) {
|
||||
return true;
|
||||
|
|
@ -136,17 +137,17 @@ public class Transaction extends ChildMessage {
|
|||
return segwit;
|
||||
}
|
||||
|
||||
public int getSegwitVersion() {
|
||||
return segwitVersion;
|
||||
public int getSegwitFlag() {
|
||||
return segwitFlag;
|
||||
}
|
||||
|
||||
public void setSegwitVersion(int segwitVersion) {
|
||||
public void setSegwitFlag(int segwitFlag) {
|
||||
if(!segwit) {
|
||||
adjustLength(2);
|
||||
this.segwit = true;
|
||||
}
|
||||
|
||||
this.segwitVersion = segwitVersion;
|
||||
this.segwitFlag = segwitFlag;
|
||||
}
|
||||
|
||||
public void clearSegwit() {
|
||||
|
|
@ -210,7 +211,7 @@ public class Transaction extends ChildMessage {
|
|||
// marker, flag
|
||||
if(useWitnessFormat) {
|
||||
stream.write(0);
|
||||
stream.write(segwitVersion);
|
||||
stream.write(segwitFlag);
|
||||
}
|
||||
|
||||
// txin_count, txins
|
||||
|
|
@ -255,7 +256,7 @@ public class Transaction extends ChildMessage {
|
|||
// marker, flag
|
||||
if (segwit) {
|
||||
byte[] segwitHeader = readBytes(2);
|
||||
segwitVersion = segwitHeader[1];
|
||||
segwitFlag = segwitHeader[1];
|
||||
}
|
||||
// txin_count, txins
|
||||
parseInputs();
|
||||
|
|
@ -305,8 +306,8 @@ public class Transaction extends ChildMessage {
|
|||
return length;
|
||||
}
|
||||
|
||||
public int getVirtualSize() {
|
||||
return (int)Math.ceil((double)getWeightUnits() / (double)WITNESS_SCALE_FACTOR);
|
||||
public double getVirtualSize() {
|
||||
return (double)getWeightUnits() / (double)WITNESS_SCALE_FACTOR;
|
||||
}
|
||||
|
||||
public int getWeightUnits() {
|
||||
|
|
@ -354,7 +355,7 @@ public class Transaction extends ChildMessage {
|
|||
|
||||
public TransactionInput addInput(Sha256Hash spendTxHash, long outputIndex, Script script, TransactionWitness witness) {
|
||||
if(!isSegwit()) {
|
||||
setSegwitVersion(DEFAULT_SEGWIT_VERSION);
|
||||
setSegwitFlag(DEFAULT_SEGWIT_FLAG);
|
||||
}
|
||||
|
||||
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) {
|
||||
//Incomplete quick test
|
||||
if(bytes.length == 0) {
|
||||
return false;
|
||||
}
|
||||
long version = Utils.readUint32(bytes, 0);
|
||||
return version > 0 && version < 5;
|
||||
}
|
||||
|
|
@ -608,4 +612,123 @@ public class Transaction extends ChildMessage {
|
|||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.sparrowwallet.drongo.protocol;
|
|||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Objects;
|
||||
|
|
@ -51,6 +52,18 @@ public class TransactionOutPoint extends ChildMessage {
|
|||
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
|
||||
protected void bitcoinSerializeToStream(OutputStream stream) throws IOException {
|
||||
stream.write(hash.getReversedBytes());
|
||||
|
|
|
|||
|
|
@ -1,13 +1,19 @@
|
|||
package com.sparrowwallet.drongo.protocol;
|
||||
|
||||
import com.sparrowwallet.drongo.crypto.ECDSASignature;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.crypto.SchnorrSignature;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.ByteBuffer;
|
||||
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
|
||||
* 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;
|
||||
|
||||
/** Constructs a signature with the given components and SIGHASH_ALL. */
|
||||
public TransactionSignature(BigInteger r, BigInteger s) {
|
||||
this(r, s, SigHash.ALL.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 signature with the given components of the given type and SIGHASH_ALL. */
|
||||
public TransactionSignature(BigInteger r, BigInteger s, Type type) {
|
||||
this(r, s, type, type == Type.ECDSA ? SigHash.ALL.value : SigHash.DEFAULT.value);
|
||||
}
|
||||
|
||||
/** Constructs a transaction signature based on the ECDSA signature. */
|
||||
public TransactionSignature(ECKey.ECDSASignature signature, SigHash sigHash) {
|
||||
super(signature.r, signature.s);
|
||||
sighashFlags = sigHash.value;
|
||||
public TransactionSignature(ECDSASignature signature, SigHash sigHash) {
|
||||
this(signature.r, signature.s, Type.ECDSA, 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
|
||||
* real signature later.
|
||||
*/
|
||||
public static TransactionSignature dummy() {
|
||||
public static TransactionSignature dummy(Type type) {
|
||||
BigInteger val = ECKey.HALF_CURVE_ORDER;
|
||||
return new TransactionSignature(val, val);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
return new TransactionSignature(val, val, type);
|
||||
}
|
||||
|
||||
public boolean anyoneCanPay() {
|
||||
return (sighashFlags & SigHash.ANYONECANPAY.value) != 0;
|
||||
}
|
||||
|
||||
public SigHash getSigHash() {
|
||||
private SigHash getSigHash() {
|
||||
if(sighashFlags == SigHash.DEFAULT.byteValue()) {
|
||||
return SigHash.DEFAULT;
|
||||
}
|
||||
|
||||
boolean anyoneCanPay = anyoneCanPay();
|
||||
final int mode = sighashFlags & 0x1f;
|
||||
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.
|
||||
*/
|
||||
public byte[] encodeToBitcoin() {
|
||||
try {
|
||||
ByteArrayOutputStream bos = derByteStream();
|
||||
bos.write(sighashFlags);
|
||||
return bos.toByteArray();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e); // Cannot happen.
|
||||
if(ecdsaSignature != null) {
|
||||
try {
|
||||
ByteArrayOutputStream bos = ecdsaSignature.derByteStream();
|
||||
bos.write(sighashFlags);
|
||||
return bos.toByteArray();
|
||||
} 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 ECKey.ECDSASignature toCanonicalised() {
|
||||
return new TransactionSignature(super.toCanonicalised(), getSigHash());
|
||||
public static TransactionSignature decodeFromBitcoin(byte[] bytes, boolean requireCanonicalEncoding) throws SignatureDecodeException {
|
||||
if(bytes.length == 64) {
|
||||
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
|
||||
|
|
@ -140,39 +136,16 @@ public class TransactionSignature extends ECKey.ECDSASignature {
|
|||
if(o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
if(!super.equals(o)) {
|
||||
return false;
|
||||
}
|
||||
TransactionSignature signature = (TransactionSignature) o;
|
||||
return sighashFlags == signature.sighashFlags;
|
||||
TransactionSignature that = (TransactionSignature) o;
|
||||
return sighashFlags == that.sighashFlags && Objects.equals(ecdsaSignature, that.ecdsaSignature) && Objects.equals(schnorrSignature, that.schnorrSignature);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(super.hashCode(), sighashFlags);
|
||||
return Objects.hash(ecdsaSignature, schnorrSignature, sighashFlags);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
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]);
|
||||
public enum Type {
|
||||
ECDSA, SCHNORR
|
||||
}
|
||||
}
|
||||
|
|
@ -14,6 +14,12 @@ import java.util.List;
|
|||
public class TransactionWitness extends ChildMessage {
|
||||
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) {
|
||||
setParent(transaction);
|
||||
this.pushes = new ArrayList<>();
|
||||
|
|
|
|||
|
|
@ -16,6 +16,9 @@ import java.nio.ByteBuffer;
|
|||
import java.util.*;
|
||||
|
||||
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 static final byte PSBT_GLOBAL_UNSIGNED_TX = 0x00;
|
||||
|
|
@ -50,7 +53,7 @@ public class PSBT {
|
|||
this.transaction = transaction;
|
||||
|
||||
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++) {
|
||||
|
|
@ -87,12 +90,16 @@ public class PSBT {
|
|||
this.version = version;
|
||||
}
|
||||
|
||||
boolean alwaysIncludeWitnessUtxo = wallet.getKeystores().stream().anyMatch(keystore -> keystore.getWalletModel().alwaysIncludeNonWitnessUtxo());
|
||||
|
||||
int inputIndex = 0;
|
||||
for(Iterator<Map.Entry<BlockTransactionHashIndex, WalletNode>> iter = walletTransaction.getSelectedUtxos().entrySet().iterator(); iter.hasNext(); inputIndex++) {
|
||||
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();
|
||||
TransactionOutput utxoOutput = utxo.getOutputs().get(utxoIndex);
|
||||
|
||||
|
|
@ -109,12 +116,17 @@ public class PSBT {
|
|||
}
|
||||
|
||||
Map<ECKey, KeyDerivation> derivedPublicKeys = new LinkedHashMap<>();
|
||||
for(Keystore keystore : wallet.getKeystores()) {
|
||||
WalletNode walletNode = utxoEntry.getValue();
|
||||
derivedPublicKeys.put(keystore.getPubKey(walletNode), keystore.getKeyDerivation().extend(walletNode.getDerivation()));
|
||||
ECKey tapInternalKey = null;
|
||||
for(Keystore keystore : signingWallet.getKeystores()) {
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -122,28 +134,29 @@ public class PSBT {
|
|||
for(TransactionOutput txOutput : transaction.getOutputs()) {
|
||||
try {
|
||||
Address address = txOutput.getScript().getToAddresses()[0];
|
||||
if(walletTransaction.getPayments().stream().anyMatch(payment -> payment.getAddress().equals(address))) {
|
||||
outputNodes.add(wallet.getWalletAddresses().getOrDefault(address, null));
|
||||
} else if(address.equals(wallet.getAddress(walletTransaction.getChangeNode()))) {
|
||||
outputNodes.add(walletTransaction.getChangeNode());
|
||||
if(walletTransaction.getAddressNodeMap().containsKey(address)) {
|
||||
outputNodes.add(walletTransaction.getAddressNodeMap().get(address));
|
||||
} else if(walletTransaction.getChangeMap().keySet().stream().anyMatch(changeNode -> changeNode.getAddress().equals(address))) {
|
||||
outputNodes.add(walletTransaction.getChangeMap().keySet().stream().filter(changeNode -> changeNode.getAddress().equals(address)).findFirst().orElse(null));
|
||||
}
|
||||
} catch(NonStandardScriptException e) {
|
||||
//Should never happen
|
||||
throw new IllegalArgumentException(e);
|
||||
//Ignore, likely OP_RETURN output
|
||||
outputNodes.add(null);
|
||||
}
|
||||
}
|
||||
|
||||
for(int outputIndex = 0; outputIndex < outputNodes.size(); outputIndex++) {
|
||||
WalletNode outputNode = outputNodes.get(outputIndex);
|
||||
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);
|
||||
} else {
|
||||
TransactionOutput txOutput = transaction.getOutputs().get(outputIndex);
|
||||
Wallet recipientWallet = outputNode.getWallet();
|
||||
|
||||
//Construct dummy transaction to spend the UTXO created by this wallet's txOutput
|
||||
Transaction transaction = new Transaction();
|
||||
TransactionInput spendingInput = wallet.addDummySpendingInput(transaction, outputNode, txOutput);
|
||||
TransactionInput spendingInput = addDummySpendingInput(transaction, outputNode, txOutput);
|
||||
|
||||
Script redeemScript = null;
|
||||
if(ScriptType.P2SH.isScriptType(txOutput.getScript())) {
|
||||
|
|
@ -156,22 +169,32 @@ public class PSBT {
|
|||
}
|
||||
|
||||
Map<ECKey, KeyDerivation> derivedPublicKeys = new LinkedHashMap<>();
|
||||
for(Keystore keystore : wallet.getKeystores()) {
|
||||
derivedPublicKeys.put(keystore.getPubKey(outputNode), keystore.getKeyDerivation().extend(outputNode.getDerivation()));
|
||||
ECKey tapInternalKey = null;
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public PSBT(byte[] psbt) throws PSBTParseException {
|
||||
this.psbtBytes = psbt;
|
||||
parse();
|
||||
this(psbt, true);
|
||||
}
|
||||
|
||||
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 seenOutputs = 0;
|
||||
|
||||
|
|
@ -212,7 +235,7 @@ public class PSBT {
|
|||
seenInputs++;
|
||||
if (seenInputs == inputs) {
|
||||
currentState = STATE_OUTPUTS;
|
||||
parseInputEntries(inputEntryLists);
|
||||
parseInputEntries(inputEntryLists, verifySignatures);
|
||||
}
|
||||
break;
|
||||
case STATE_OUTPUTS:
|
||||
|
|
@ -245,14 +268,6 @@ public class PSBT {
|
|||
if(transaction == null) {
|
||||
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()) {
|
||||
|
|
@ -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) {
|
||||
PSBTEntry duplicate = findDuplicateKey(inputEntries);
|
||||
if(duplicate != null) {
|
||||
|
|
@ -321,15 +336,13 @@ public class PSBT {
|
|||
}
|
||||
|
||||
int inputIndex = this.psbtInputs.size();
|
||||
PSBTInput input = new PSBTInput(inputEntries, transaction, inputIndex);
|
||||
|
||||
boolean verified = input.verifySignatures();
|
||||
if(!verified && input.getPartialSignatures().size() > 0) {
|
||||
throw new PSBTParseException("Unverifiable partial signatures provided");
|
||||
}
|
||||
|
||||
PSBTInput input = new PSBTInput(this, inputEntries, transaction, inputIndex);
|
||||
this.psbtInputs.add(input);
|
||||
}
|
||||
|
||||
if(verifySignatures) {
|
||||
verifySignatures(psbtInputs);
|
||||
}
|
||||
}
|
||||
|
||||
private void parseOutputEntries(List<List<PSBTEntry>> outputEntryLists) throws PSBTParseException {
|
||||
|
|
@ -364,7 +377,7 @@ public class PSBT {
|
|||
if(utxo != null) {
|
||||
fee += utxo.getValue();
|
||||
} else {
|
||||
log.error("Cannot determine fee - not enough information provided on inputs");
|
||||
log.warn("Cannot determine fee - inputs are missing UTXO data");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -377,9 +390,25 @@ public class PSBT {
|
|||
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() {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -432,6 +461,10 @@ public class PSBT {
|
|||
}
|
||||
|
||||
public byte[] serialize() {
|
||||
return serialize(true);
|
||||
}
|
||||
|
||||
public byte[] serialize(boolean includeXpubs) {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
|
||||
baos.writeBytes(Utils.hexToBytes(PSBT_MAGIC_HEX));
|
||||
|
|
@ -439,14 +472,19 @@ public class PSBT {
|
|||
|
||||
List<PSBTEntry> globalEntries = getGlobalEntries();
|
||||
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});
|
||||
|
||||
for(PSBTInput psbtInput : getPsbtInputs()) {
|
||||
List<PSBTEntry> inputEntries = psbtInput.getInputEntries();
|
||||
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});
|
||||
}
|
||||
|
|
@ -454,7 +492,11 @@ public class PSBT {
|
|||
for(PSBTOutput psbtOutput : getPsbtOutputs()) {
|
||||
List<PSBTEntry> outputEntries = psbtOutput.getOutputEntries();
|
||||
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});
|
||||
}
|
||||
|
|
@ -511,7 +553,7 @@ public class PSBT {
|
|||
Transaction finalTransaction = new Transaction(transaction.bitcoinSerialize());
|
||||
|
||||
if(hasWitness && !finalTransaction.isSegwit()) {
|
||||
finalTransaction.setSegwitVersion(1);
|
||||
finalTransaction.setSegwitFlag(Transaction.DEFAULT_SEGWIT_FLAG);
|
||||
}
|
||||
|
||||
for(int i = 0; i < finalTransaction.getInputs().size(); i++) {
|
||||
|
|
@ -584,7 +626,11 @@ public class PSBT {
|
|||
}
|
||||
|
||||
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) {
|
||||
|
|
@ -600,14 +646,24 @@ public class PSBT {
|
|||
}
|
||||
|
||||
public static boolean isPSBT(String s) {
|
||||
if (Utils.isHex(s) && s.startsWith(PSBT_MAGIC_HEX)) {
|
||||
return true;
|
||||
} else {
|
||||
return Utils.isBase64(s) && Utils.bytesToHex(Base64.decode(s)).startsWith(PSBT_MAGIC_HEX);
|
||||
try {
|
||||
if(Utils.isHex(s) && s.startsWith(PSBT_MAGIC_HEX)) {
|
||||
return true;
|
||||
} 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 {
|
||||
return fromString(strPSBT, true);
|
||||
}
|
||||
|
||||
public static PSBT fromString(String strPSBT, boolean verifySignatures) throws PSBTParseException {
|
||||
if (!isPSBT(strPSBT)) {
|
||||
throw new PSBTParseException("Provided string is not a PSBT");
|
||||
}
|
||||
|
|
@ -617,6 +673,6 @@ public class PSBT {
|
|||
}
|
||||
|
||||
byte[] psbtBytes = Utils.hexToBytes(strPSBT);
|
||||
return new PSBT(psbtBytes);
|
||||
return new PSBT(psbtBytes, verifySignatures);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ package com.sparrowwallet.drongo.psbt;
|
|||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.protocol.VarInt;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
|
|
@ -10,6 +12,7 @@ import java.nio.ByteOrder;
|
|||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class PSBTEntry {
|
||||
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 {
|
||||
if(data.length < 4) {
|
||||
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);
|
||||
}
|
||||
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));
|
||||
String bip32path = KeyDerivation.writePath(bip32pathList);
|
||||
|
|
@ -83,7 +105,7 @@ public class PSBTEntry {
|
|||
|
||||
do {
|
||||
bb.get(buf);
|
||||
reverse(buf);
|
||||
Utils.reverse(buf);
|
||||
ByteBuffer pbuf = ByteBuffer.wrap(buf);
|
||||
path.add(new ChildNumber(pbuf.getInt()));
|
||||
} while(bb.hasRemaining());
|
||||
|
|
@ -91,6 +113,19 @@ public class PSBTEntry {
|
|||
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) {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
byte[] fingerprintBytes = Utils.hexToBytes(keyDerivation.getMasterFingerprint());
|
||||
|
|
@ -202,14 +237,6 @@ public class PSBTEntry {
|
|||
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 {
|
||||
if(this.getKey().length != 1) {
|
||||
throw new PSBTParseException("PSBT key type must be one byte");
|
||||
|
|
@ -218,13 +245,19 @@ public class PSBTEntry {
|
|||
|
||||
public void checkOneBytePlusXpubKey() throws PSBTParseException {
|
||||
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 {
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,8 +9,10 @@ import org.slf4j.LoggerFactory;
|
|||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.sparrowwallet.drongo.protocol.ScriptType.*;
|
||||
import static com.sparrowwallet.drongo.protocol.TransactionSignature.Type.*;
|
||||
import static com.sparrowwallet.drongo.psbt.PSBTEntry.*;
|
||||
|
||||
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_POR_COMMITMENT = 0x09;
|
||||
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 TransactionOutput witnessUtxo;
|
||||
private final Map<ECKey, TransactionSignature> partialSignatures = new LinkedHashMap<>();
|
||||
|
|
@ -37,20 +43,23 @@ public class PSBTInput {
|
|||
private TransactionWitness finalScriptWitness;
|
||||
private String porCommitment;
|
||||
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 int index;
|
||||
|
||||
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.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) {
|
||||
this(transaction, index);
|
||||
sigHash = SigHash.ALL;
|
||||
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(psbt, transaction, index);
|
||||
|
||||
if(Arrays.asList(ScriptType.WITNESS_TYPES).contains(scriptType)) {
|
||||
this.witnessUtxo = utxo.getOutputs().get(utxoIndex);
|
||||
|
|
@ -66,11 +75,24 @@ public class PSBTInput {
|
|||
this.redeemScript = redeemScript;
|
||||
this.witnessScript = witnessScript;
|
||||
|
||||
this.derivedPublicKeys.putAll(derivedPublicKeys);
|
||||
if(scriptType != P2TR) {
|
||||
this.derivedPublicKeys.putAll(derivedPublicKeys);
|
||||
}
|
||||
|
||||
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) {
|
||||
switch(entry.getKeyType()) {
|
||||
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());
|
||||
}
|
||||
for(TransactionOutput output: nonWitnessTx.getOutputs()) {
|
||||
try {
|
||||
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);
|
||||
}
|
||||
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());
|
||||
}
|
||||
break;
|
||||
case PSBT_IN_WITNESS_UTXO:
|
||||
entry.checkOneByteKey();
|
||||
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");
|
||||
}
|
||||
this.witnessUtxo = witnessTxOutput;
|
||||
|
|
@ -112,8 +130,12 @@ public class PSBTInput {
|
|||
case PSBT_IN_PARTIAL_SIG:
|
||||
entry.checkOneBytePlusPubKey();
|
||||
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
|
||||
TransactionSignature signature = TransactionSignature.decodeFromBitcoin(entry.getData(), true, false);
|
||||
TransactionSignature signature = TransactionSignature.decodeFromBitcoin(ECDSA, entry.getData(), true);
|
||||
this.partialSignatures.put(sigPublicKey, signature);
|
||||
log.debug("Found input partial signature with public key " + sigPublicKey + " signature " + Utils.bytesToHex(entry.getData()));
|
||||
break;
|
||||
|
|
@ -136,11 +158,15 @@ public class PSBTInput {
|
|||
throw new PSBTParseException("Witness UTXO provided but redeem script is not P2WPKH or P2WSH");
|
||||
}
|
||||
}
|
||||
if(scriptPubKey == null || !P2SH.isScriptType(scriptPubKey)) {
|
||||
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()));
|
||||
if(scriptPubKey == null) {
|
||||
log.warn("PSBT provided a redeem script for a transaction output that was not provided");
|
||||
} else {
|
||||
if(!P2SH.isScriptType(scriptPubKey)) {
|
||||
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;
|
||||
|
|
@ -156,7 +182,7 @@ public class PSBTInput {
|
|||
pubKeyHash = this.witnessUtxo.getScript().getPubKeyHash();
|
||||
}
|
||||
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)) {
|
||||
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()));
|
||||
log.debug("Found proprietary input " + Utils.bytesToHex(entry.getKeyData()) + ": " + Utils.bytesToHex(entry.getData()));
|
||||
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:
|
||||
log.warn("PSBT input not recognized key type: " + entry.getKeyType());
|
||||
}
|
||||
|
|
@ -205,7 +254,8 @@ public class PSBTInput {
|
|||
List<PSBTEntry> entries = new ArrayList<>();
|
||||
|
||||
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) {
|
||||
|
|
@ -250,6 +300,20 @@ public class PSBTInput {
|
|||
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;
|
||||
}
|
||||
|
||||
|
|
@ -283,6 +347,16 @@ public class PSBTInput {
|
|||
}
|
||||
|
||||
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() {
|
||||
|
|
@ -379,8 +453,38 @@ public class PSBTInput {
|
|||
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() {
|
||||
if(!getPartialSignatures().isEmpty()) {
|
||||
if(getTapKeyPathSignature() != null) {
|
||||
return true;
|
||||
} else if(!getPartialSignatures().isEmpty()) {
|
||||
try {
|
||||
//All partial sigs are already verified
|
||||
int reqSigs = getSigningScript().getNumRequiredSignatures();
|
||||
|
|
@ -399,27 +503,40 @@ public class PSBTInput {
|
|||
return getFinalScriptWitness().getSignatures();
|
||||
} else if(getFinalScriptSig() != null) {
|
||||
return getFinalScriptSig().getSignatures();
|
||||
} else if(getTapKeyPathSignature() != null) {
|
||||
return List.of(getTapKeyPathSignature());
|
||||
} else {
|
||||
return getPartialSignatures().values();
|
||||
}
|
||||
}
|
||||
|
||||
private SigHash getDefaultSigHash() {
|
||||
if(isTaproot()) {
|
||||
return SigHash.DEFAULT;
|
||||
}
|
||||
|
||||
return SigHash.ALL;
|
||||
}
|
||||
|
||||
public boolean sign(ECKey privKey) {
|
||||
SigHash localSigHash = getSigHash();
|
||||
if(localSigHash == null) {
|
||||
//Assume SigHash.ALL
|
||||
localSigHash = SigHash.ALL;
|
||||
localSigHash = getDefaultSigHash();
|
||||
}
|
||||
|
||||
if(getNonWitnessUtxo() != null || getWitnessUtxo() != null) {
|
||||
Script signingScript = getSigningScript();
|
||||
if(signingScript != null) {
|
||||
Sha256Hash hash = getHashForSignature(signingScript, localSigHash);
|
||||
ECKey.ECDSASignature ecdsaSignature = privKey.sign(hash);
|
||||
TransactionSignature transactionSignature = new TransactionSignature(ecdsaSignature, localSigHash);
|
||||
TransactionSignature.Type type = isTaproot() ? SCHNORR : ECDSA;
|
||||
TransactionSignature transactionSignature = privKey.sign(hash, localSigHash, type);
|
||||
|
||||
ECKey pubKey = ECKey.fromPublicOnly(privKey);
|
||||
getPartialSignatures().put(pubKey, transactionSignature);
|
||||
if(type == SCHNORR) {
|
||||
tapKeyPathSignature = transactionSignature;
|
||||
} else {
|
||||
ECKey pubKey = ECKey.fromPublicOnly(privKey);
|
||||
getPartialSignatures().put(pubKey, transactionSignature);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
@ -428,11 +545,10 @@ public class PSBTInput {
|
|||
return false;
|
||||
}
|
||||
|
||||
boolean verifySignatures() throws PSBTParseException {
|
||||
boolean verifySignatures() throws PSBTSignatureException {
|
||||
SigHash localSigHash = getSigHash();
|
||||
if(localSigHash == null) {
|
||||
//Assume SigHash.ALL
|
||||
localSigHash = SigHash.ALL;
|
||||
localSigHash = getDefaultSigHash();
|
||||
}
|
||||
|
||||
if(getNonWitnessUtxo() != null || getWitnessUtxo() != null) {
|
||||
|
|
@ -440,10 +556,17 @@ public class PSBTInput {
|
|||
if(signingScript != null) {
|
||||
Sha256Hash hash = getHashForSignature(signingScript, localSigHash);
|
||||
|
||||
for(ECKey sigPublicKey : getPartialSignatures().keySet()) {
|
||||
TransactionSignature signature = getPartialSignature(sigPublicKey);
|
||||
if(!sigPublicKey.verify(hash, signature)) {
|
||||
throw new PSBTParseException("Partial signature does not verify against provided public key");
|
||||
if(isTaproot() && tapKeyPathSignature != null) {
|
||||
ECKey outputKey = P2TR.getPublicKeyFromScript(getUtxo().getScript());
|
||||
if(!outputKey.verify(hash, tapKeyPathSignature)) {
|
||||
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<>();
|
||||
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(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;
|
||||
}
|
||||
|
||||
|
|
@ -549,18 +677,19 @@ public class PSBTInput {
|
|||
witnessScript = null;
|
||||
porCommitment = null;
|
||||
proprietary.clear();
|
||||
tapDerivedPublicKeys.clear();
|
||||
tapKeyPathSignature = null;
|
||||
}
|
||||
|
||||
private Sha256Hash getHashForSignature(Script connectedScript, SigHash localSigHash) {
|
||||
Sha256Hash hash;
|
||||
|
||||
ScriptType scriptType = getScriptType();
|
||||
if(getWitnessUtxo() == null && Arrays.asList(WITNESS_TYPES).contains(scriptType)) {
|
||||
throw new IllegalStateException("Trying to get signature hash for " + scriptType + " script without a PSBT witness UTXO");
|
||||
}
|
||||
|
||||
if(getWitnessUtxo() != null) {
|
||||
long prevValue = getWitnessUtxo().getValue();
|
||||
if(scriptType == ScriptType.P2TR) {
|
||||
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)) {
|
||||
long prevValue = getUtxo().getValue();
|
||||
hash = transaction.hashForWitnessSignature(index, connectedScript, prevValue, localSigHash);
|
||||
} else {
|
||||
hash = transaction.hashForLegacySignature(index, connectedScript, localSigHash);
|
||||
|
|
|
|||
|
|
@ -4,26 +4,30 @@ import com.sparrowwallet.drongo.KeyDerivation;
|
|||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
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.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
|
||||
import static com.sparrowwallet.drongo.protocol.ScriptType.*;
|
||||
import static com.sparrowwallet.drongo.psbt.PSBTEntry.*;
|
||||
|
||||
public class PSBTOutput {
|
||||
public static final byte PSBT_OUT_REDEEM_SCRIPT = 0x00;
|
||||
public static final byte PSBT_OUT_WITNESS_SCRIPT = 0x01;
|
||||
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;
|
||||
|
||||
private Script redeemScript;
|
||||
private Script witnessScript;
|
||||
private final Map<ECKey, KeyDerivation> derivedPublicKeys = 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);
|
||||
|
||||
|
|
@ -31,11 +35,22 @@ public class PSBTOutput {
|
|||
//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.witnessScript = witnessScript;
|
||||
this.derivedPublicKeys.putAll(derivedPublicKeys);
|
||||
|
||||
if(scriptType != P2TR) {
|
||||
this.derivedPublicKeys.putAll(derivedPublicKeys);
|
||||
}
|
||||
|
||||
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 {
|
||||
|
|
@ -64,6 +79,24 @@ public class PSBTOutput {
|
|||
proprietary.put(Utils.bytesToHex(entry.getKeyData()), Utils.bytesToHex(entry.getData()));
|
||||
log.debug("Found proprietary output " + Utils.bytesToHex(entry.getKeyData()) + ": " + Utils.bytesToHex(entry.getData()));
|
||||
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:
|
||||
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())));
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -103,6 +146,12 @@ public class PSBTOutput {
|
|||
|
||||
derivedPublicKeys.putAll(psbtOutput.derivedPublicKeys);
|
||||
proprietary.putAll(psbtOutput.proprietary);
|
||||
|
||||
tapDerivedPublicKeys.putAll(psbtOutput.tapDerivedPublicKeys);
|
||||
|
||||
if(psbtOutput.tapInternalKey != null) {
|
||||
tapInternalKey = psbtOutput.tapInternalKey;
|
||||
}
|
||||
}
|
||||
|
||||
public Script getRedeemScript() {
|
||||
|
|
@ -132,4 +181,24 @@ public class PSBTOutput {
|
|||
public Map<String, String> getProprietary() {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.sparrowwallet.drongo.uri;
|
|||
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.address.InvalidAddressException;
|
||||
import com.sparrowwallet.drongo.wallet.Payment;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
|
@ -86,9 +87,7 @@ public class BitcoinURI {
|
|||
* @throws BitcoinURIParseException if the URI is not syntactically or semantically valid.
|
||||
*/
|
||||
public BitcoinURI(String input) throws BitcoinURIParseException {
|
||||
String scheme = BITCOIN_SCHEME;
|
||||
|
||||
// Attempt to form the URI (fail fast syntax checking to official standards).
|
||||
// Attempt to parse the URI
|
||||
URI uri;
|
||||
try {
|
||||
uri = new URI(input);
|
||||
|
|
@ -99,23 +98,14 @@ public class BitcoinURI {
|
|||
// URI is formed as bitcoin:<address>?<query parameters>
|
||||
// blockchain.info generates URIs of non-BIP compliant form bitcoin://address?....
|
||||
|
||||
// Remove the bitcoin scheme.
|
||||
// (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 {
|
||||
if (!BITCOIN_SCHEME.equalsIgnoreCase(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.
|
||||
String[] addressSplitTokens = schemeSpecificPart.split("\\?", 2);
|
||||
if(addressSplitTokens.length == 0) {
|
||||
|
|
@ -164,7 +154,7 @@ public class BitcoinURI {
|
|||
if(sepIndex == 0) {
|
||||
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);
|
||||
|
||||
// Parse the amount.
|
||||
|
|
@ -325,6 +315,11 @@ public class BitcoinURI {
|
|||
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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -1,26 +1,40 @@
|
|||
package com.sparrowwallet.drongo.wallet;
|
||||
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.protocol.*;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public class BlockTransaction extends BlockTransactionHash implements Comparable<BlockTransaction> {
|
||||
private final Transaction transaction;
|
||||
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) {
|
||||
this(hash, height, date, fee, transaction, null);
|
||||
}
|
||||
|
||||
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.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() {
|
||||
|
|
@ -31,64 +45,46 @@ public class BlockTransaction extends BlockTransactionHash implements Comparable
|
|||
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
|
||||
public int compareTo(BlockTransaction blkTx) {
|
||||
if(getHeight() != blkTx.getHeight()) {
|
||||
return getComparisonHeight() - blkTx.getComparisonHeight();
|
||||
}
|
||||
|
||||
if(getReferencedOutpoints(this).removeAll(getOutputs(blkTx))) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if(getReferencedOutpoints(blkTx).removeAll(getOutputs(this))) {
|
||||
return -1;
|
||||
int blockOrder = compareBlockOrder(blkTx);
|
||||
if(blockOrder != 0) {
|
||||
return blockOrder;
|
||||
}
|
||||
|
||||
return super.compareTo(blkTx);
|
||||
}
|
||||
|
||||
private static List<HashIndex> getReferencedOutpoints(BlockTransaction blockchainTransaction) {
|
||||
if(blockchainTransaction.getTransaction() == null) {
|
||||
return Collections.emptyList();
|
||||
public int compareBlockOrder(BlockTransaction blkTx) {
|
||||
if(getHeight() != blkTx.getHeight()) {
|
||||
return getComparisonHeight() - blkTx.getComparisonHeight();
|
||||
}
|
||||
|
||||
return blockchainTransaction.getTransaction().getInputs().stream()
|
||||
.map(txInput -> new HashIndex(txInput.getOutpoint().getHash(), (int)txInput.getOutpoint().getIndex()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private static List<HashIndex> getOutputs(BlockTransaction blockchainTransaction) {
|
||||
if(blockchainTransaction.getTransaction() == null) {
|
||||
return Collections.emptyList();
|
||||
if(!Collections.disjoint(spending, blkTx.funding)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return blockchainTransaction.getTransaction().getOutputs().stream()
|
||||
.map(txOutput -> new HashIndex(blockchainTransaction.getHash(), txOutput.getIndex()))
|
||||
.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;
|
||||
if(!Collections.disjoint(blkTx.spending, funding)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
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);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
|||
import java.util.Date;
|
||||
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_FULLY_CONFIRM = 100;
|
||||
|
||||
|
|
@ -16,11 +16,12 @@ public abstract class BlockTransactionHash {
|
|||
|
||||
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.height = height;
|
||||
this.date = date;
|
||||
this.fee = fee;
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
public Sha256Hash getHash() {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
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.value = value;
|
||||
this.spentBy = spentBy;
|
||||
|
|
@ -92,6 +96,8 @@ public class BlockTransactionHashIndex extends BlockTransactionHash implements C
|
|||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import org.slf4j.LoggerFactory;
|
|||
|
||||
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 int TOTAL_TRIES = 100000;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@ import com.sparrowwallet.drongo.crypto.*;
|
|||
import java.security.SecureRandom;
|
||||
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 MAX_SEED_ENTROPY_BITS = 512;
|
||||
|
||||
|
|
@ -184,6 +184,7 @@ public class DeterministicSeed implements EncryptableItem {
|
|||
Arrays.fill(mnemonicBytes != null ? mnemonicBytes : new byte[0], (byte)0);
|
||||
|
||||
DeterministicSeed seed = new DeterministicSeed(encryptedMnemonic, needsPassphrase, creationTimeSeconds, type);
|
||||
seed.setId(getId());
|
||||
seed.setPassphrase(passphrase);
|
||||
|
||||
return seed;
|
||||
|
|
@ -209,6 +210,7 @@ public class DeterministicSeed implements EncryptableItem {
|
|||
KeyDeriver keyDeriver = getEncryptionType().getDeriver().getKeyDeriver(encryptedMnemonicCode.getKeySalt());
|
||||
Key key = keyDeriver.deriveKey(password);
|
||||
DeterministicSeed seed = decrypt(key);
|
||||
seed.setId(getId());
|
||||
key.clear();
|
||||
|
||||
return seed;
|
||||
|
|
@ -225,6 +227,7 @@ public class DeterministicSeed implements EncryptableItem {
|
|||
Arrays.fill(decrypted, (byte)0);
|
||||
|
||||
DeterministicSeed seed = new DeterministicSeed(mnemonic, needsPassphrase, creationTimeSeconds, type);
|
||||
seed.setId(getId());
|
||||
seed.setPassphrase(passphrase);
|
||||
|
||||
return seed;
|
||||
|
|
@ -341,6 +344,7 @@ public class DeterministicSeed implements EncryptableItem {
|
|||
seed = new DeterministicSeed(new ArrayList<>(mnemonicCode), needsPassphrase, creationTimeSeconds, type);
|
||||
}
|
||||
|
||||
seed.setId(getId());
|
||||
seed.setPassphrase(passphrase);
|
||||
return seed;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,10 +5,7 @@ import com.sparrowwallet.drongo.crypto.ECKey;
|
|||
import com.sparrowwallet.drongo.policy.Miniscript;
|
||||
import com.sparrowwallet.drongo.policy.Policy;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.NonStandardScriptException;
|
||||
import com.sparrowwallet.drongo.protocol.Script;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.protocol.TransactionSignature;
|
||||
import com.sparrowwallet.drongo.protocol.*;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -125,4 +125,16 @@ public class FinalizingPSBTWallet extends Wallet {
|
|||
public boolean canSign(PSBT psbt) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -4,11 +4,20 @@ import com.sparrowwallet.drongo.ExtendedKey;
|
|||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
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.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
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 int MAX_LABEL_LENGTH = 16;
|
||||
|
||||
|
|
@ -17,8 +26,16 @@ public class Keystore {
|
|||
private WalletModel walletModel = WalletModel.SPARROW;
|
||||
private KeyDerivation keyDerivation;
|
||||
private ExtendedKey extendedPublicKey;
|
||||
private PaymentCode externalPaymentCode;
|
||||
private MasterPrivateExtendedKey masterPrivateExtendedKey;
|
||||
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() {
|
||||
this(DEFAULT_LABEL);
|
||||
}
|
||||
|
|
@ -32,7 +49,7 @@ public class Keystore {
|
|||
}
|
||||
|
||||
public String getScriptName() {
|
||||
return label.replace(" ", "").toLowerCase();
|
||||
return label.replace(" ", "");
|
||||
}
|
||||
|
||||
public void setLabel(String label) {
|
||||
|
|
@ -69,6 +86,31 @@ public class Keystore {
|
|||
|
||||
public void setExtendedPublicKey(ExtendedKey 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() {
|
||||
|
|
@ -79,16 +121,60 @@ public class Keystore {
|
|||
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 {
|
||||
if(seed == null) {
|
||||
throw new IllegalArgumentException("Keystore does not contain a seed");
|
||||
if(seed == null && masterPrivateExtendedKey == null) {
|
||||
throw new IllegalArgumentException("Keystore does not contain a master private key, or seed to derive one from");
|
||||
}
|
||||
|
||||
if(seed.isEncrypted()) {
|
||||
throw new IllegalArgumentException("Seed is encrypted");
|
||||
if(seed != null) {
|
||||
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 {
|
||||
|
|
@ -107,22 +193,44 @@ public class Keystore {
|
|||
return ExtendedKey.fromDescriptor(xprv.toString());
|
||||
}
|
||||
|
||||
public DeterministicKey getKey(WalletNode walletNode) throws MnemonicException {
|
||||
return getKey(walletNode.getKeyPurpose(), walletNode.getIndex());
|
||||
}
|
||||
public ECKey getKey(WalletNode walletNode) throws MnemonicException {
|
||||
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();
|
||||
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);
|
||||
}
|
||||
|
||||
public DeterministicKey getPubKey(WalletNode walletNode) {
|
||||
return getPubKey(walletNode.getKeyPurpose(), walletNode.getIndex());
|
||||
}
|
||||
public ECKey getPubKey(WalletNode walletNode) {
|
||||
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 = List.of(extendedPublicKey.getKeyChildNumber(), keyPurpose.getPathIndex(), new ChildNumber(keyIndex));
|
||||
List<ChildNumber> derivation = new ArrayList<>();
|
||||
derivation.add(extendedPublicKey.getKeyChildNumber());
|
||||
derivation.addAll(walletNode.getDerivation());
|
||||
return extendedPublicKey.getKey(derivation);
|
||||
}
|
||||
|
||||
|
|
@ -178,11 +286,11 @@ public class Keystore {
|
|||
}
|
||||
|
||||
if(source == KeystoreSource.SW_SEED) {
|
||||
if(seed == null) {
|
||||
throw new InvalidKeystoreException("Source of " + source + " but no seed is present");
|
||||
if(seed == null && masterPrivateExtendedKey == null) {
|
||||
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 {
|
||||
List<ChildNumber> derivation = getKeyDerivation().getDerivation();
|
||||
DeterministicKey derivedKey = getExtendedMasterPrivateKey().getKey(derivation);
|
||||
|
|
@ -191,15 +299,27 @@ public class Keystore {
|
|||
if(!xpub.equals(getExtendedPublicKey())) {
|
||||
throw new InvalidKeystoreException("Specified extended public key does not match public key derived from seed");
|
||||
}
|
||||
extendedPublicKeyChecked = true;
|
||||
} catch(MnemonicException 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() {
|
||||
Keystore copy = new Keystore(label);
|
||||
copy.setId(getId());
|
||||
copy.setSource(source);
|
||||
copy.setWalletModel(walletModel);
|
||||
if(keyDerivation != null) {
|
||||
|
|
@ -208,59 +328,94 @@ public class Keystore {
|
|||
if(extendedPublicKey != null) {
|
||||
copy.setExtendedPublicKey(extendedPublicKey.copy());
|
||||
}
|
||||
if(masterPrivateExtendedKey != null) {
|
||||
copy.setMasterPrivateExtendedKey(masterPrivateExtendedKey.copy());
|
||||
}
|
||||
if(seed != null) {
|
||||
copy.setSeed(seed.copy());
|
||||
}
|
||||
if(externalPaymentCode != null) {
|
||||
copy.setExternalPaymentCode(externalPaymentCode.copy());
|
||||
}
|
||||
if(bip47ExtendedPrivateKey != null) {
|
||||
copy.setBip47ExtendedPrivateKey(bip47ExtendedPrivateKey.copy());
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
public static Keystore fromSeed(DeterministicSeed seed, List<ChildNumber> derivation) throws MnemonicException {
|
||||
Keystore keystore = new Keystore();
|
||||
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();
|
||||
String masterFingerprint = Utils.bytesToHex(xprv.getKey().getFingerprint());
|
||||
DeterministicKey derivedKey = xprv.getKey(derivation);
|
||||
DeterministicKey derivedKeyPublicOnly = derivedKey.dropPrivateBytes().dropParent();
|
||||
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.setWalletModel(WalletModel.SPARROW);
|
||||
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, KeyDerivation.writePath(derivation)));
|
||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(xpub.toString()));
|
||||
|
||||
return keystore;
|
||||
}
|
||||
|
||||
public boolean hasSeed() {
|
||||
return seed != null;
|
||||
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);
|
||||
DeterministicKey bip47Key = xprv.getKey(bip47Derivation);
|
||||
ExtendedKey bip47ExtendedPrivateKey = new ExtendedKey(bip47Key, bip47Key.getParentFingerprint(), bip47Derivation.get(bip47Derivation.size() - 1));
|
||||
keystore.setBip47ExtendedPrivateKey(ExtendedKey.fromDescriptor(bip47ExtendedPrivateKey.toString()));
|
||||
}
|
||||
|
||||
public boolean isEncrypted() {
|
||||
return seed != null && seed.isEncrypted();
|
||||
return (seed != null && seed.isEncrypted()) || (masterPrivateExtendedKey != null && masterPrivateExtendedKey.isEncrypted());
|
||||
}
|
||||
|
||||
public void encrypt(Key key) {
|
||||
if(hasSeed() && !seed.isEncrypted()) {
|
||||
seed = seed.encrypt(key);
|
||||
}
|
||||
if(hasMasterPrivateExtendedKey() && !masterPrivateExtendedKey.isEncrypted()) {
|
||||
masterPrivateExtendedKey = masterPrivateExtendedKey.encrypt(key);
|
||||
}
|
||||
}
|
||||
|
||||
public void decrypt(CharSequence password) {
|
||||
if(hasSeed() && seed.isEncrypted()) {
|
||||
seed = seed.decrypt(password);
|
||||
}
|
||||
if(hasMasterPrivateExtendedKey() && masterPrivateExtendedKey.isEncrypted()) {
|
||||
masterPrivateExtendedKey = masterPrivateExtendedKey.decrypt(password);
|
||||
}
|
||||
}
|
||||
|
||||
public void decrypt(Key key) {
|
||||
if(hasSeed() && seed.isEncrypted()) {
|
||||
seed = seed.decrypt(key);
|
||||
}
|
||||
if(hasMasterPrivateExtendedKey() && masterPrivateExtendedKey.isEncrypted()) {
|
||||
masterPrivateExtendedKey = masterPrivateExtendedKey.decrypt(key);
|
||||
}
|
||||
}
|
||||
|
||||
public void clearPrivate() {
|
||||
if(hasSeed()) {
|
||||
seed.clear();
|
||||
}
|
||||
if(hasMasterPrivateExtendedKey()) {
|
||||
masterPrivateExtendedKey.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
package com.sparrowwallet.drongo.wallet;
|
||||
|
||||
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) {
|
||||
this.displayName = displayName;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import com.sparrowwallet.drongo.protocol.Transaction;
|
|||
import java.util.*;
|
||||
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 final long noInputsFee;
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -3,9 +3,9 @@ package com.sparrowwallet.drongo.wallet;
|
|||
import java.util.Collection;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class MaxUtxoSelector implements UtxoSelector {
|
||||
public class MaxUtxoSelector extends SingleSetUtxoSelector {
|
||||
@Override
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
96
src/main/java/com/sparrowwallet/drongo/wallet/MixConfig.java
Normal file
96
src/main/java/com/sparrowwallet/drongo/wallet/MixConfig.java
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -39,4 +39,16 @@ public class MnemonicException extends Exception {
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package com.sparrowwallet.drongo.wallet;
|
||||
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
|
@ -7,6 +9,7 @@ import static com.sparrowwallet.drongo.protocol.Transaction.WITNESS_SCALE_FACTOR
|
|||
|
||||
public class OutputGroup {
|
||||
private final List<BlockTransactionHashIndex> utxos = new ArrayList<>();
|
||||
private final ScriptType scriptType;
|
||||
private final int walletBlockHeight;
|
||||
private final long inputWeightUnits;
|
||||
private final double feeRate;
|
||||
|
|
@ -17,15 +20,17 @@ public class OutputGroup {
|
|||
private long longTermFee = 0;
|
||||
private int depth = Integer.MAX_VALUE;
|
||||
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.inputWeightUnits = inputWeightUnits;
|
||||
this.feeRate = feeRate;
|
||||
this.longTermFeeRate = longTermFeeRate;
|
||||
}
|
||||
|
||||
public void add(BlockTransactionHashIndex utxo, boolean allInputsFromWallet) {
|
||||
public void add(BlockTransactionHashIndex utxo, boolean allInputsFromWallet, boolean spendLast) {
|
||||
utxos.add(utxo);
|
||||
value += utxo.getValue();
|
||||
effectiveValue += utxo.getValue() - (long)(inputWeightUnits * feeRate / WITNESS_SCALE_FACTOR);
|
||||
|
|
@ -33,6 +38,7 @@ public class OutputGroup {
|
|||
longTermFee += (long)(inputWeightUnits * longTermFeeRate / WITNESS_SCALE_FACTOR);
|
||||
depth = utxo.getHeight() <= 0 ? 0 : Math.min(depth, walletBlockHeight - utxo.getHeight() + 1);
|
||||
this.allInputsFromWallet &= allInputsFromWallet;
|
||||
this.spendLast |= spendLast;
|
||||
}
|
||||
|
||||
public void remove(BlockTransactionHashIndex utxo) {
|
||||
|
|
@ -48,6 +54,10 @@ public class OutputGroup {
|
|||
return utxos;
|
||||
}
|
||||
|
||||
public ScriptType getScriptType() {
|
||||
return scriptType;
|
||||
}
|
||||
|
||||
public long getValue() {
|
||||
return value;
|
||||
}
|
||||
|
|
@ -72,21 +82,27 @@ public class OutputGroup {
|
|||
return allInputsFromWallet;
|
||||
}
|
||||
|
||||
public boolean isSpendLast() {
|
||||
return spendLast;
|
||||
}
|
||||
|
||||
public static class Filter {
|
||||
private final int minWalletConfirmations;
|
||||
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.minExternalConfirmations = minExternalConfirmations;
|
||||
this.includeSpendLast = includeSpendLast;
|
||||
}
|
||||
|
||||
public boolean isEligible(OutputGroup outputGroup) {
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,12 +7,18 @@ public class Payment {
|
|||
private String label;
|
||||
private long amount;
|
||||
private boolean sendMax;
|
||||
private Type type;
|
||||
|
||||
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.label = label;
|
||||
this.amount = amount;
|
||||
this.sendMax = sendMax;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public Address getAddress() {
|
||||
|
|
@ -46,4 +52,16 @@ public class Payment {
|
|||
public void setSendMax(boolean 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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -5,11 +5,18 @@ import java.util.Collection;
|
|||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class PresetUtxoSelector implements UtxoSelector {
|
||||
public class PresetUtxoSelector extends SingleSetUtxoSelector {
|
||||
private final Collection<BlockTransactionHashIndex> presetUtxos;
|
||||
private final boolean maintainOrder;
|
||||
|
||||
public PresetUtxoSelector(Collection<BlockTransactionHashIndex> presetUtxos) {
|
||||
this.presetUtxos = presetUtxos;
|
||||
this.maintainOrder = false;
|
||||
}
|
||||
|
||||
public PresetUtxoSelector(Collection<BlockTransactionHashIndex> presetUtxos, boolean maintainOrder) {
|
||||
this.presetUtxos = presetUtxos;
|
||||
this.maintainOrder = maintainOrder;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -26,10 +33,19 @@ public class PresetUtxoSelector implements UtxoSelector {
|
|||
}
|
||||
}
|
||||
|
||||
if(maintainOrder && utxos.containsAll(presetUtxos)) {
|
||||
return presetUtxos;
|
||||
}
|
||||
|
||||
return utxos;
|
||||
}
|
||||
|
||||
public Collection<BlockTransactionHashIndex> getPresetUtxos() {
|
||||
return presetUtxos;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shuffleInputs() {
|
||||
return !maintainOrder;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import java.math.BigInteger;
|
|||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class PriorityUtxoSelector implements UtxoSelector {
|
||||
public class PriorityUtxoSelector extends SingleSetUtxoSelector {
|
||||
private final int currentBlockHeight;
|
||||
|
||||
public PriorityUtxoSelector(int currentBlockHeight) {
|
||||
|
|
|
|||
58
src/main/java/com/sparrowwallet/drongo/wallet/SeedQR.java
Normal file
58
src/main/java/com/sparrowwallet/drongo/wallet/SeedQR.java
Normal 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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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 + "}";
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,11 @@
|
|||
package com.sparrowwallet.drongo.wallet;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
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
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,12 @@
|
|||
package com.sparrowwallet.drongo.wallet;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
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) {
|
||||
return valueOf(model.toUpperCase());
|
||||
return valueOf(model.toUpperCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
|
|
@ -12,7 +14,7 @@ public enum WalletModel {
|
|||
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";
|
||||
}
|
||||
|
||||
|
|
@ -20,6 +22,10 @@ public enum WalletModel {
|
|||
return "digitalbitbox";
|
||||
}
|
||||
|
||||
if(this == BITCOIN_CORE) {
|
||||
return "bitcoincore";
|
||||
}
|
||||
|
||||
if(this == BITBOX_02) {
|
||||
return "bitbox02";
|
||||
}
|
||||
|
|
@ -32,11 +38,19 @@ public enum WalletModel {
|
|||
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() {
|
||||
if(this == COLDCARD || this == COBO_VAULT || this == PASSPORT) {
|
||||
if(this == COLDCARD || this == COBO_VAULT || this == PASSPORT || this == KEYSTONE || this == GORDIAN_SEED_TOOL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -62,7 +76,7 @@ public enum WalletModel {
|
|||
}
|
||||
|
||||
public String toDisplayString() {
|
||||
String line = this.toString().toLowerCase();
|
||||
String line = this.toString().toLowerCase(Locale.ROOT);
|
||||
String[] words = line.split("_");
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for(String word : words) {
|
||||
|
|
|
|||
|
|
@ -2,40 +2,68 @@ package com.sparrowwallet.drongo.wallet;
|
|||
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.protocol.Script;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class WalletNode implements Comparable<WalletNode> {
|
||||
public class WalletNode extends Persistable implements Comparable<WalletNode> {
|
||||
private final String derivationPath;
|
||||
private String label;
|
||||
private Address address;
|
||||
private TreeSet<WalletNode> children = new TreeSet<>();
|
||||
private TreeSet<BlockTransactionHashIndex> transactionOutputs = new TreeSet<>();
|
||||
|
||||
private transient Wallet wallet;
|
||||
private transient KeyPurpose keyPurpose;
|
||||
private transient int index = -1;
|
||||
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) {
|
||||
this.derivationPath = derivationPath;
|
||||
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.derivationPath = KeyDerivation.writePath(derivation);
|
||||
this.keyPurpose = keyPurpose;
|
||||
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.derivationPath = KeyDerivation.writePath(derivation);
|
||||
this.keyPurpose = keyPurpose;
|
||||
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() {
|
||||
return derivationPath;
|
||||
}
|
||||
|
|
@ -87,25 +115,47 @@ public class WalletNode implements Comparable<WalletNode> {
|
|||
}
|
||||
|
||||
public Set<WalletNode> getChildren() {
|
||||
return children == null ? null : Collections.unmodifiableSet(children);
|
||||
return children;
|
||||
}
|
||||
|
||||
public void setChildren(TreeSet<WalletNode> children) {
|
||||
this.children = children;
|
||||
}
|
||||
|
||||
public boolean isUsed() {
|
||||
return !transactionOutputs.isEmpty();
|
||||
}
|
||||
|
||||
public Set<BlockTransactionHashIndex> getTransactionOutputs() {
|
||||
return transactionOutputs == null ? null : Collections.unmodifiableSet(transactionOutputs);
|
||||
return transactionOutputs;
|
||||
}
|
||||
|
||||
public void setTransactionOutputs(TreeSet<BlockTransactionHashIndex> transactionOutputs) {
|
||||
this.transactionOutputs = transactionOutputs;
|
||||
}
|
||||
|
||||
public synchronized void updateTransactionOutputs(Set<BlockTransactionHashIndex> updatedOutputs) {
|
||||
public synchronized void updateTransactionOutputs(Wallet wallet, Set<BlockTransactionHashIndex> 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();
|
||||
optionalLabel.ifPresent(txo::setLabel);
|
||||
if(!transactionOutputs.isEmpty()) {
|
||||
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();
|
||||
|
|
@ -116,9 +166,14 @@ public class WalletNode implements Comparable<WalletNode> {
|
|||
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);
|
||||
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() {
|
||||
|
|
@ -130,11 +185,55 @@ public class WalletNode implements Comparable<WalletNode> {
|
|||
return value;
|
||||
}
|
||||
|
||||
public synchronized void fillToIndex(int index) {
|
||||
for(int i = 0; i <= index; i++) {
|
||||
WalletNode node = new WalletNode(getKeyPurpose(), i);
|
||||
children.add(node);
|
||||
public Set<WalletNode> fillToIndex(Wallet wallet, int index) {
|
||||
Set<WalletNode> newNodes = fillToIndex(index);
|
||||
if(wallet.isValid()) {
|
||||
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;
|
||||
}
|
||||
|
||||
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
|
||||
public String toString() {
|
||||
return derivationPath;
|
||||
return derivationPath.replace("m", "..");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -161,12 +307,12 @@ public class WalletNode implements Comparable<WalletNode> {
|
|||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
WalletNode node = (WalletNode) o;
|
||||
return derivationPath.equals(node.derivationPath);
|
||||
return Objects.equals(wallet, node.wallet) && derivationPath.equals(node.derivationPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(derivationPath);
|
||||
return Objects.hash(wallet, derivationPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -193,12 +339,14 @@ public class WalletNode implements Comparable<WalletNode> {
|
|||
}
|
||||
}
|
||||
|
||||
public WalletNode copy() {
|
||||
WalletNode copy = new WalletNode(derivationPath);
|
||||
public WalletNode copy(Wallet walletCopy) {
|
||||
WalletNode copy = new WalletNode(walletCopy, derivationPath);
|
||||
copy.setId(getId());
|
||||
copy.setLabel(label);
|
||||
copy.setAddress(address);
|
||||
|
||||
for(WalletNode child : getChildren()) {
|
||||
copy.children.add(child.copy());
|
||||
copy.children.add(child.copy(walletCopy));
|
||||
}
|
||||
|
||||
for(BlockTransactionHashIndex txo : getTransactionOutputs()) {
|
||||
|
|
@ -207,4 +355,69 @@ public class WalletNode implements Comparable<WalletNode> {
|
|||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
package com.sparrowwallet.drongo.wallet;
|
||||
|
||||
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.psbt.PSBT;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 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 Transaction transaction;
|
||||
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 WalletNode changeNode;
|
||||
private final long changeAmount;
|
||||
private final Map<WalletNode, Long> changeMap;
|
||||
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) {
|
||||
this(wallet, transaction, utxoSelectors, selectedUtxos, payments, null, 0L, fee);
|
||||
private Map<Wallet, Map<Address, WalletNode>> addressNodeMap = new HashMap<>();
|
||||
|
||||
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.transaction = transaction;
|
||||
this.utxoSelectors = utxoSelectors;
|
||||
this.selectedUtxos = selectedUtxos;
|
||||
this.selectedUtxoSets = selectedUtxoSets;
|
||||
this.payments = payments;
|
||||
this.changeNode = changeNode;
|
||||
this.changeAmount = changeAmount;
|
||||
this.changeMap = changeMap;
|
||||
this.fee = fee;
|
||||
this.inputTransactions = inputTransactions;
|
||||
}
|
||||
|
||||
public PSBT createPSBT() {
|
||||
|
|
@ -54,23 +60,29 @@ public class WalletTransaction {
|
|||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public List<Map<BlockTransactionHashIndex, WalletNode>> getSelectedUtxoSets() {
|
||||
return selectedUtxoSets;
|
||||
}
|
||||
|
||||
public List<Payment> getPayments() {
|
||||
return payments;
|
||||
}
|
||||
|
||||
public WalletNode getChangeNode() {
|
||||
return changeNode;
|
||||
public Map<WalletNode, Long> getChangeMap() {
|
||||
return changeMap;
|
||||
}
|
||||
|
||||
public Address getChangeAddress() {
|
||||
return getWallet().getAddress(getChangeNode());
|
||||
}
|
||||
|
||||
public long getChangeAmount() {
|
||||
return changeAmount;
|
||||
public Address getChangeAddress(WalletNode changeNode) {
|
||||
return changeNode.getAddress();
|
||||
}
|
||||
|
||||
public long getFee() {
|
||||
|
|
@ -82,7 +94,15 @@ public class WalletTransaction {
|
|||
}
|
||||
|
||||
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
|
||||
*/
|
||||
public double getFeePercentage() {
|
||||
return (double)getFee() / (getTotal() - getFee());
|
||||
return getFee() <= 0 || getTotal() <= 0 ? 0 : (double)getFee() / (getTotal() - getFee());
|
||||
}
|
||||
|
||||
public boolean isCoinControlUsed() {
|
||||
return !utxoSelectors.isEmpty() && utxoSelectors.get(0) instanceof PresetUtxoSelector;
|
||||
}
|
||||
|
||||
public boolean isTwoPersonCoinjoin() {
|
||||
return !utxoSelectors.isEmpty() && utxoSelectors.get(0) instanceof StonewallUtxoSelector;
|
||||
}
|
||||
|
||||
public boolean isConsolidationSend(Payment payment) {
|
||||
if(payment.getAddress() != null && getWallet() != null) {
|
||||
return getWallet().isWalletOutputScript(payment.getAddress().getOutputScript());
|
||||
return isWalletSend(getWallet(), payment);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public List<WalletNode> getConsolidationSendNodes() {
|
||||
List<WalletNode> walletNodes = new ArrayList<>();
|
||||
public boolean isWalletSend(Wallet wallet, Payment payment) {
|
||||
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) {
|
||||
if(payment.getAddress() != null && getWallet() != null) {
|
||||
WalletNode walletNode = getWallet().getWalletOutputScripts().get(payment.getAddress().getOutputScript());
|
||||
if(walletNode != null) {
|
||||
walletNodes.add(walletNode);
|
||||
if(walletAddressNodeMap.containsKey(payment.getAddress())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
open module com.sparrowwallet.drongo {
|
||||
requires org.bouncycastle.provider;
|
||||
requires de.mkammerer.argon2;
|
||||
requires slf4j.api;
|
||||
requires de.mkammerer.argon2.nolibs;
|
||||
requires org.slf4j;
|
||||
requires logback.core;
|
||||
requires logback.classic;
|
||||
requires json.simple;
|
||||
requires jeromq;
|
||||
exports com.sparrowwallet.drongo;
|
||||
|
|
@ -13,4 +14,6 @@ open module com.sparrowwallet.drongo {
|
|||
exports com.sparrowwallet.drongo.wallet;
|
||||
exports com.sparrowwallet.drongo.policy;
|
||||
exports com.sparrowwallet.drongo.uri;
|
||||
exports com.sparrowwallet.drongo.bip47;
|
||||
exports org.bitcoin;
|
||||
}
|
||||
910
src/main/java/org/bitcoin/NativeSecp256k1.java
Normal file
910
src/main/java/org/bitcoin/NativeSecp256k1.java
Normal 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);
|
||||
}
|
||||
51
src/main/java/org/bitcoin/NativeSecp256k1Util.java
Normal file
51
src/main/java/org/bitcoin/NativeSecp256k1Util.java
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
60
src/main/java/org/bitcoin/Secp256k1Context.java
Normal file
60
src/main/java/org/bitcoin/Secp256k1Context.java
Normal 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();
|
||||
}
|
||||
BIN
src/main/resources/native/linux/aarch64/libsecp256k1.so
Normal file
BIN
src/main/resources/native/linux/aarch64/libsecp256k1.so
Normal file
Binary file not shown.
BIN
src/main/resources/native/linux/x64/libsecp256k1.so
Executable file
BIN
src/main/resources/native/linux/x64/libsecp256k1.so
Executable file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue