Compare commits
1 Commits
master
...
secp256k1-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14a1a4ec8f |
@ -6,7 +6,7 @@ Drongo is a Java Bitcoin library built mainly to support [Sparrow Wallet](https:
|
||||
|
||||
Drongo can be built with
|
||||
|
||||
`./gradlew jar`
|
||||
`./gradlew shadowJar`
|
||||
|
||||
## License
|
||||
|
||||
|
||||
52
build.gradle
52
build.gradle
@ -1,5 +1,14 @@
|
||||
buildscript {
|
||||
repositories {
|
||||
maven {
|
||||
url "https://plugins.gradle.org/m2/"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id 'java-library'
|
||||
id 'extra-java-module-info'
|
||||
}
|
||||
|
||||
tasks.withType(AbstractArchiveTask) {
|
||||
@ -15,31 +24,50 @@ if(os.macOsX) {
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
flatDir {
|
||||
dirs 'libs'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation ('org.bouncycastle:bcprov-jdk18on:1.82')
|
||||
implementation('org.pgpainless:pgpainless-core:1.7.7') {
|
||||
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-common'
|
||||
implementation files('libs/secp-api-0.0.1.jar')
|
||||
implementation files('libs/secp-ffm-0.0.1.jar')
|
||||
implementation ('org.jspecify:jspecify:1.0.0')
|
||||
implementation ('com.googlecode.json-simple:json-simple:1.1.1') {
|
||||
exclude group: 'junit', module: 'junit'
|
||||
}
|
||||
implementation ('de.mkammerer:argon2-jvm:2.12') {
|
||||
implementation ('org.bouncycastle:bcprov-jdk18on:1.77')
|
||||
implementation('org.pgpainless:pgpainless-core:1.6.7')
|
||||
implementation ('de.mkammerer:argon2-jvm:2.11') {
|
||||
exclude group: 'net.java.dev.jna', module: 'jna'
|
||||
}
|
||||
implementation('dnsjava:dnsjava:3.6.4')
|
||||
implementation('com.github.ben-manes.caffeine:caffeine:3.2.3')
|
||||
implementation ('net.java.dev.jna:jna:5.18.1')
|
||||
implementation ('ch.qos.logback:logback-classic:1.5.32') {
|
||||
implementation ('net.java.dev.jna:jna:5.8.0')
|
||||
implementation ('ch.qos.logback:logback-classic:1.4.14') {
|
||||
exclude group: 'org.slf4j'
|
||||
}
|
||||
implementation ('org.slf4j:slf4j-api:2.0.17')
|
||||
testImplementation('org.junit.jupiter:junit-jupiter-api:5.14.1')
|
||||
testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine:5.14.1')
|
||||
implementation ('org.slf4j:slf4j-api:2.0.12')
|
||||
testImplementation('org.junit.jupiter:junit-jupiter-api:5.10.0')
|
||||
testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine:5.10.0')
|
||||
testRuntimeOnly('org.junit.platform:junit-platform-launcher')
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
jvmArgs = ["--enable-native-access=ALL-UNNAMED"]
|
||||
}
|
||||
|
||||
processResources {
|
||||
doLast {
|
||||
delete fileTree("$buildDir/resources/main/native").matching {
|
||||
exclude "${osName}/**"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extraJavaModuleInfo {
|
||||
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('jsr305-3.0.2.jar', 'com.google.code.findbugs.jsr305', '3.0.2')
|
||||
}
|
||||
|
||||
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,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
15
gradlew
vendored
15
gradlew
vendored
@ -1,7 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015 the original 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.
|
||||
@ -15,8 +15,6 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
@ -57,7 +55,7 @@
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# https://github.com/gradle/gradle/blob/HEAD/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/.
|
||||
@ -86,7 +84,7 @@ done
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
@ -114,6 +112,7 @@ case "$( uname )" in #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
@ -171,6 +170,7 @@ fi
|
||||
# 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" )
|
||||
|
||||
@ -203,14 +203,15 @@ fi
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
|
||||
25
gradlew.bat
vendored
25
gradlew.bat
vendored
@ -13,8 +13,6 @@
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@ -45,11 +43,11 @@ set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
@ -59,21 +57,22 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
: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%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
"%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
|
||||
|
||||
BIN
libs/secp-api-0.0.1.jar
Normal file
BIN
libs/secp-api-0.0.1.jar
Normal file
Binary file not shown.
BIN
libs/secp-ffm-0.0.1.jar
Normal file
BIN
libs/secp-ffm-0.0.1.jar
Normal file
Binary file not shown.
@ -75,14 +75,10 @@ public class ExtendedKey {
|
||||
}
|
||||
|
||||
public static ExtendedKey fromDescriptor(String descriptor) {
|
||||
return fromDescriptor(descriptor, false);
|
||||
}
|
||||
|
||||
public static ExtendedKey fromDescriptor(String descriptor, boolean ignoreNetwork) {
|
||||
byte[] serializedKey = Base58.decodeChecked(descriptor);
|
||||
ByteBuffer buffer = ByteBuffer.wrap(serializedKey);
|
||||
int headerInt = buffer.getInt();
|
||||
Header header = Header.getHeader(headerInt, ignoreNetwork);
|
||||
Header header = Header.getHeader(headerInt);
|
||||
if(header == null) {
|
||||
throw new IllegalArgumentException("Unknown header bytes for extended key on " + Network.getCanonical().getName() + ": " + DeterministicKey.toBase58(serializedKey).substring(0, 4));
|
||||
}
|
||||
@ -243,7 +239,7 @@ public class ExtendedKey {
|
||||
return Network.get().getXpubHeader();
|
||||
}
|
||||
|
||||
private static Header getHeader(int header, boolean ignoreNetwork) {
|
||||
private static Header getHeader(int header) {
|
||||
for(Header extendedKeyHeader : getHeaders(Network.get())) {
|
||||
if(header == extendedKeyHeader.header) {
|
||||
return extendedKeyHeader;
|
||||
@ -253,9 +249,6 @@ public class ExtendedKey {
|
||||
for(Network otherNetwork : getOtherNetworks(Network.get())) {
|
||||
for(Header otherNetworkKeyHeader : getHeaders(otherNetwork)) {
|
||||
if(header == otherNetworkKeyHeader.header) {
|
||||
if(ignoreNetwork) {
|
||||
return otherNetworkKeyHeader;
|
||||
}
|
||||
throw new IllegalArgumentException("Provided " + otherNetworkKeyHeader.name + " extended key invalid on configured " + Network.getCanonical().getName() + " network. Use a " + otherNetwork.getName() + " configuration to use this extended key.");
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +0,0 @@
|
||||
package com.sparrowwallet.drongo;
|
||||
|
||||
public enum FileType {
|
||||
TEXT, JSON, BINARY, UNKNOWN;
|
||||
}
|
||||
@ -1,157 +0,0 @@
|
||||
package com.sparrowwallet.drongo;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.*;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarFile;
|
||||
|
||||
public class IOUtils {
|
||||
private static final Logger log = LoggerFactory.getLogger(IOUtils.class);
|
||||
|
||||
public static FileType getFileType(File file) {
|
||||
try {
|
||||
String type = Files.probeContentType(file.toPath());
|
||||
if(type == null) {
|
||||
if(file.getName().toLowerCase(Locale.ROOT).endsWith("txn") || file.getName().toLowerCase(Locale.ROOT).endsWith("psbt")) {
|
||||
return FileType.TEXT;
|
||||
}
|
||||
|
||||
if(file.exists()) {
|
||||
try(BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8))) {
|
||||
String line = br.readLine();
|
||||
if(line != null) {
|
||||
if(line.startsWith("01000000") || line.startsWith("cHNid")) {
|
||||
return FileType.TEXT;
|
||||
} else if(line.startsWith("{")) {
|
||||
return FileType.JSON;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return FileType.BINARY;
|
||||
} else if (type.equals("application/json")) {
|
||||
return FileType.JSON;
|
||||
} else if (type.startsWith("text")) {
|
||||
return FileType.TEXT;
|
||||
}
|
||||
} catch(IOException e) {
|
||||
//ignore
|
||||
}
|
||||
|
||||
return FileType.UNKNOWN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists the contents of a resource directory. Non-recursive.
|
||||
* Works for regular files, JARs, and Java modules.
|
||||
*
|
||||
* @param clazz A class from the same module or package as the resources.
|
||||
* @param path The resource directory path (e.g., "myfolder/"). Must end with "/", must not start with "/".
|
||||
* @return An array of filenames (not full paths) in the specified directory.
|
||||
* @throws IOException If an I/O error occurs while accessing the resources.
|
||||
* @throws URISyntaxException If the path is invalid or unsupported.
|
||||
*/
|
||||
public static String[] getResourceListing(Class<?> clazz, String path) throws URISyntaxException, IOException {
|
||||
URL dirURL = clazz.getClassLoader().getResource(path);
|
||||
if(dirURL != null && dirURL.getProtocol().equals("file")) {
|
||||
return new File(dirURL.toURI()).list();
|
||||
}
|
||||
|
||||
if(dirURL == null) {
|
||||
String me = clazz.getName().replace(".", "/") + ".class";
|
||||
dirURL = clazz.getClassLoader().getResource(me);
|
||||
if(dirURL == null) {
|
||||
throw new IOException("Resource directory '" + path + "' not found for class " + clazz.getName());
|
||||
}
|
||||
}
|
||||
|
||||
if(dirURL.getProtocol().equals("jar")) {
|
||||
String jarPath = dirURL.getPath().substring(5, dirURL.getPath().indexOf("!"));
|
||||
Set<String> result = new HashSet<>();
|
||||
|
||||
try(JarFile jar = new JarFile(URLDecoder.decode(jarPath, StandardCharsets.UTF_8))) {
|
||||
Enumeration<JarEntry> entries = jar.entries();
|
||||
|
||||
while(entries.hasMoreElements()) {
|
||||
String name = entries.nextElement().getName();
|
||||
if(name.startsWith(path)) {
|
||||
String entry = name.substring(path.length());
|
||||
int checkSubdir = entry.indexOf("/");
|
||||
if(checkSubdir >= 0) {
|
||||
entry = entry.substring(0, checkSubdir);
|
||||
}
|
||||
if(!entry.isEmpty()) {
|
||||
result.add(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result.toArray(new String[0]);
|
||||
}
|
||||
|
||||
if(dirURL.getProtocol().equals("jrt")) {
|
||||
Module module = clazz.getModule();
|
||||
if(module == null || module.getName() == null) {
|
||||
throw new IOException("Class " + clazz.getName() + " is not in a named module");
|
||||
}
|
||||
try(java.nio.file.FileSystem jrtFs = FileSystems.newFileSystem(URI.create("jrt:/"), Collections.emptyMap())) {
|
||||
Path resourcePath = jrtFs.getPath("modules", module.getName(), path);
|
||||
try(var stream = Files.list(resourcePath)) {
|
||||
return stream.filter(Files::isRegularFile).map(p -> p.getFileName().toString()).toArray(String[]::new);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new UnsupportedOperationException("Cannot list files for URL " + dirURL);
|
||||
}
|
||||
|
||||
public static boolean deleteDirectory(File directory) {
|
||||
try(var stream = Files.walk(directory.toPath())) {
|
||||
stream.sorted(Comparator.reverseOrder())
|
||||
.map(Path::toFile)
|
||||
.forEach(File::delete);
|
||||
} catch(IOException e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean secureDelete(File file) {
|
||||
if(file.exists()) {
|
||||
long length = file.length();
|
||||
SecureRandom random = new SecureRandom();
|
||||
byte[] data = new byte[1024*1024];
|
||||
random.nextBytes(data);
|
||||
try(RandomAccessFile raf = new RandomAccessFile(file, "rws")) {
|
||||
raf.seek(0);
|
||||
raf.getFilePointer();
|
||||
int pos = 0;
|
||||
while(pos < length) {
|
||||
raf.write(data);
|
||||
pos += data.length;
|
||||
}
|
||||
} catch(IOException e) {
|
||||
log.warn("Error overwriting file for deletion: " + file.getName(), e);
|
||||
}
|
||||
|
||||
return file.delete();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -8,8 +8,6 @@ import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public class KeyDerivation {
|
||||
public static final String DEFAULT_WATCH_ONLY_FINGERPRINT = "00000000";
|
||||
|
||||
private final String masterFingerprint;
|
||||
private final String derivationPath;
|
||||
private transient List<ChildNumber> derivation;
|
||||
@ -19,13 +17,9 @@ public class KeyDerivation {
|
||||
}
|
||||
|
||||
public KeyDerivation(String masterFingerprint, String derivationPath) {
|
||||
this(masterFingerprint, derivationPath, false);
|
||||
}
|
||||
|
||||
public KeyDerivation(String masterFingerprint, String derivationPath, boolean rewritePath) {
|
||||
this.masterFingerprint = masterFingerprint == null ? null : masterFingerprint.toLowerCase(Locale.ROOT);
|
||||
this.derivationPath = derivationPath;
|
||||
this.derivation = parsePath(derivationPath);
|
||||
this.derivationPath = rewritePath ? writePath(derivation) : derivationPath;
|
||||
}
|
||||
|
||||
public String getMasterFingerprint() {
|
||||
@ -106,24 +100,6 @@ public class KeyDerivation {
|
||||
return List.of(new ChildNumber(47, true), new ChildNumber(Network.get() == Network.MAINNET ? 0 : 1, true), new ChildNumber(Math.max(0, account), true));
|
||||
}
|
||||
|
||||
public static List<ChildNumber> getBip352Derivation(int account) {
|
||||
return List.of(new ChildNumber(352, true), new ChildNumber(Network.get() == Network.MAINNET ? 0 : 1, true), new ChildNumber(Math.max(0, account), true));
|
||||
}
|
||||
|
||||
public static List<ChildNumber> getBip352ScanDerivation(List<ChildNumber> derivation) {
|
||||
List<ChildNumber> scanDerivation = new ArrayList<>(derivation);
|
||||
scanDerivation.add(new ChildNumber(1, true));
|
||||
scanDerivation.add(new ChildNumber(0, false));
|
||||
return Collections.unmodifiableList(scanDerivation);
|
||||
}
|
||||
|
||||
public static List<ChildNumber> getBip352SpendDerivation(List<ChildNumber> derivation) {
|
||||
List<ChildNumber> spendDerivation = new ArrayList<>(derivation);
|
||||
spendDerivation.add(new ChildNumber(0, true));
|
||||
spendDerivation.add(new ChildNumber(0, false));
|
||||
return Collections.unmodifiableList(spendDerivation);
|
||||
}
|
||||
|
||||
public KeyDerivation copy() {
|
||||
return new KeyDerivation(masterFingerprint, derivationPath);
|
||||
}
|
||||
|
||||
@ -5,10 +5,6 @@ import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.*;
|
||||
import java.nio.file.attribute.PosixFilePermission;
|
||||
import java.nio.file.attribute.PosixFilePermissions;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* A simple library class which helps with loading dynamic libraries stored in the
|
||||
@ -114,33 +110,9 @@ public class NativeUtils {
|
||||
String tempDir = System.getProperty("java.io.tmpdir");
|
||||
File generatedDir = new File(tempDir, prefix + System.nanoTime());
|
||||
|
||||
if(!createOwnerOnlyDirectory(generatedDir)) {
|
||||
if (!generatedDir.mkdir())
|
||||
throw new IOException("Failed to create temp directory " + generatedDir.getName());
|
||||
}
|
||||
|
||||
return generatedDir;
|
||||
}
|
||||
|
||||
public static boolean createOwnerOnlyDirectory(File directory) throws IOException {
|
||||
try {
|
||||
if(OsType.getCurrent() == OsType.WINDOWS) {
|
||||
Files.createDirectories(directory.toPath());
|
||||
return true;
|
||||
}
|
||||
|
||||
Files.createDirectories(directory.toPath(), PosixFilePermissions.asFileAttribute(getDirectoryOwnerOnlyPosixFilePermissions()));
|
||||
return true;
|
||||
} catch(UnsupportedOperationException e) {
|
||||
return directory.mkdirs();
|
||||
}
|
||||
}
|
||||
|
||||
private static Set<PosixFilePermission> getDirectoryOwnerOnlyPosixFilePermissions() {
|
||||
Set<PosixFilePermission> ownerOnly = EnumSet.noneOf(PosixFilePermission.class);
|
||||
ownerOnly.add(PosixFilePermission.OWNER_READ);
|
||||
ownerOnly.add(PosixFilePermission.OWNER_WRITE);
|
||||
ownerOnly.add(PosixFilePermission.OWNER_EXECUTE);
|
||||
|
||||
return ownerOnly;
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,16 +3,16 @@ package com.sparrowwallet.drongo;
|
||||
import java.util.Locale;
|
||||
|
||||
public enum Network {
|
||||
MAINNET("mainnet", "Mainnet", "mainnet", 0, "1", 5, "3", "bc", "sp", "spscan", "spspend", ExtendedKey.Header.xprv, ExtendedKey.Header.xpub, 128, 8332),
|
||||
TESTNET("testnet", "Testnet3", "testnet3", 111, "mn", 196, "2", "tb", "tsp", "tspscan", "tspspend", ExtendedKey.Header.tprv, ExtendedKey.Header.tpub, 239, 18332),
|
||||
REGTEST("regtest", "Regtest", "regtest", 111, "mn", 196, "2", "bcrt", "sprt", "tspscan", "tspspend", ExtendedKey.Header.tprv, ExtendedKey.Header.tpub, 239, 18443),
|
||||
SIGNET("signet", "Signet", "signet", 111, "mn", 196, "2", "tb", "tsp", "tspscan", "tspspend", ExtendedKey.Header.tprv, ExtendedKey.Header.tpub, 239, 38332),
|
||||
TESTNET4("testnet4", "Testnet4", "testnet4", 111, "mn", 196, "2", "tb", "tsp", "tspscan", "tspspend", ExtendedKey.Header.tprv, ExtendedKey.Header.tpub, 239, 48332);
|
||||
MAINNET("mainnet", "Mainnet", "mainnet", 0, "1", 5, "3", "bc", ExtendedKey.Header.xprv, ExtendedKey.Header.xpub, 128, 8332),
|
||||
TESTNET("testnet", "Testnet3", "testnet3", 111, "mn", 196, "2", "tb", ExtendedKey.Header.tprv, ExtendedKey.Header.tpub, 239, 18332),
|
||||
REGTEST("regtest", "Regtest", "regtest", 111, "mn", 196, "2", "bcrt", ExtendedKey.Header.tprv, ExtendedKey.Header.tpub, 239, 18443),
|
||||
SIGNET("signet", "Signet", "signet", 111, "mn", 196, "2", "tb", ExtendedKey.Header.tprv, ExtendedKey.Header.tpub, 239, 38332),
|
||||
TESTNET4("testnet4", "Testnet4", "testnet4", 111, "mn", 196, "2", "tb", ExtendedKey.Header.tprv, ExtendedKey.Header.tpub, 239, 48332);
|
||||
|
||||
public static final String BLOCK_HEIGHT_PROPERTY = "com.sparrowwallet.blockHeight";
|
||||
private static final Network[] CANONICAL_VALUES = new Network[]{MAINNET, TESTNET, REGTEST, SIGNET};
|
||||
|
||||
Network(String name, String displayName, String home, int p2pkhAddressHeader, String p2pkhAddressPrefix, int p2shAddressHeader, String p2shAddressPrefix, String bech32AddressHrp, String spAddressHrp, String spScanKeyHrp, String spSpendKeyHrp, ExtendedKey.Header xprvHeader, ExtendedKey.Header xpubHeader, int dumpedPrivateKeyHeader, int defaultPort) {
|
||||
Network(String name, String displayName, String home, int p2pkhAddressHeader, String p2pkhAddressPrefix, int p2shAddressHeader, String p2shAddressPrefix, String bech32AddressHrp, ExtendedKey.Header xprvHeader, ExtendedKey.Header xpubHeader, int dumpedPrivateKeyHeader, int defaultPort) {
|
||||
this.name = name;
|
||||
this.displayName = displayName;
|
||||
this.home = home;
|
||||
@ -21,9 +21,6 @@ public enum Network {
|
||||
this.p2shAddressHeader = p2shAddressHeader;
|
||||
this.p2shAddressPrefix = p2shAddressPrefix;
|
||||
this.bech32AddressHrp = bech32AddressHrp;
|
||||
this.spAddressHrp = spAddressHrp;
|
||||
this.spScanKeyHrp = spScanKeyHrp;
|
||||
this.spSpendKeyHrp = spSpendKeyHrp;
|
||||
this.xprvHeader = xprvHeader;
|
||||
this.xpubHeader = xpubHeader;
|
||||
this.dumpedPrivateKeyHeader = dumpedPrivateKeyHeader;
|
||||
@ -38,9 +35,6 @@ public enum Network {
|
||||
private final int p2shAddressHeader;
|
||||
private final String p2shAddressPrefix;
|
||||
private final String bech32AddressHrp;
|
||||
private final String spAddressHrp;
|
||||
private final String spScanKeyHrp;
|
||||
private final String spSpendKeyHrp;
|
||||
private final ExtendedKey.Header xprvHeader;
|
||||
private final ExtendedKey.Header xpubHeader;
|
||||
private final int dumpedPrivateKeyHeader;
|
||||
@ -76,18 +70,6 @@ public enum Network {
|
||||
return bech32AddressHrp;
|
||||
}
|
||||
|
||||
public String getSilentPaymentsAddressHrp() {
|
||||
return spAddressHrp;
|
||||
}
|
||||
|
||||
public String getSilentPaymentsScanKeyHrp() {
|
||||
return spScanKeyHrp;
|
||||
}
|
||||
|
||||
public String getSilentPaymentsSpendKeyHrp() {
|
||||
return spSpendKeyHrp;
|
||||
}
|
||||
|
||||
public ExtendedKey.Header getXprvHeader() {
|
||||
return xprvHeader;
|
||||
}
|
||||
|
||||
@ -1,48 +0,0 @@
|
||||
package com.sparrowwallet.drongo;
|
||||
|
||||
public enum OsType {
|
||||
WINDOWS("Windows"),
|
||||
MACOS("macOS"),
|
||||
UNIX("Unix"),
|
||||
UNKNOWN("");
|
||||
|
||||
private static final OsType current = getCurrentPlatform();
|
||||
private final String platformId;
|
||||
|
||||
OsType(String platformId) {
|
||||
this.platformId = platformId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns platform id. Usually used to specify platform dependent styles
|
||||
*
|
||||
* @return platform id
|
||||
*/
|
||||
public String getPlatformId() {
|
||||
return platformId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the current OS.
|
||||
*/
|
||||
public static OsType getCurrent() {
|
||||
return current;
|
||||
}
|
||||
|
||||
private static OsType getCurrentPlatform() {
|
||||
String osName = System.getProperty("os.name");
|
||||
if(osName.startsWith("Windows")) {
|
||||
return WINDOWS;
|
||||
}
|
||||
if(osName.startsWith("Mac")) {
|
||||
return MACOS;
|
||||
}
|
||||
if(osName.startsWith("SunOS")) {
|
||||
return UNIX;
|
||||
}
|
||||
if(osName.startsWith("Linux")) {
|
||||
return UNIX;
|
||||
}
|
||||
return UNKNOWN;
|
||||
}
|
||||
}
|
||||
@ -6,18 +6,15 @@ import com.sparrowwallet.drongo.crypto.DeterministicKey;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.policy.Policy;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.crypto.DumpedPrivateKey;
|
||||
import com.sparrowwallet.drongo.protocol.ProtocolException;
|
||||
import com.sparrowwallet.drongo.protocol.Script;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.silentpayments.SilentPaymentScanAddress;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.sparrowwallet.drongo.KeyDerivation.parsePath;
|
||||
|
||||
@ -25,17 +22,12 @@ public class OutputDescriptor {
|
||||
private static final String INPUT_CHARSET = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ ";
|
||||
private static final String CHECKSUM_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
||||
|
||||
public static final Pattern XPUB_PATTERN = Pattern.compile("(\\[[^\\]]+\\])?(.(?:pub|prv)[^/\\,)]{100,112})(/[/\\d*'hH<>;]+)?");
|
||||
private static final Pattern PUBKEY_PATTERN = Pattern.compile("(\\[[^\\]]+\\])?(0[23][0-9a-fA-F]{64})");
|
||||
private static final Pattern XPUB_PATTERN = Pattern.compile("(\\[[^\\]]+\\])?(.(?:pub|prv)[^/\\,)]{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+)");
|
||||
public static final Pattern KEY_ORIGIN_PATTERN = Pattern.compile("\\[([A-Fa-f0-9]{8})([/\\d'hH]+)?\\]");
|
||||
private static final Pattern KEY_ORIGIN_PATTERN = Pattern.compile("\\[([A-Fa-f0-9]{8})([/\\d'hH]+)?\\]");
|
||||
private static final Pattern MULTIPATH_PATTERN = Pattern.compile("<([\\d*'hH;]+)>");
|
||||
private static final Pattern CHECKSUM_PATTERN = Pattern.compile("#([" + CHECKSUM_CHARSET + "]{8})$");
|
||||
private static final Pattern ANNOTATION_PATTERN = Pattern.compile("([a-zA-Z]+)=([0-9]+)");
|
||||
|
||||
public static final String ANNOTATION_BLOCK_HEIGHT = "bh";
|
||||
public static final String ANNOTATION_GAP_LIMIT = "gl";
|
||||
public static final String ANNOTATION_MAX_LABEL = "ml";
|
||||
|
||||
private final ScriptType scriptType;
|
||||
private final int multisigThreshold;
|
||||
@ -43,9 +35,6 @@ public class OutputDescriptor {
|
||||
private final Map<ExtendedKey, String> mapChildrenDerivations;
|
||||
private final Map<ExtendedKey, String> mapExtendedPublicKeyLabels;
|
||||
private final Map<ExtendedKey, ExtendedKey> extendedMasterPrivateKeys;
|
||||
private final Map<SilentPaymentScanAddress, KeyDerivation> silentPaymentScanAddresses;
|
||||
private final Map<SilentPaymentScanAddress, String> mapSilentPaymentLabels;
|
||||
private final Map<String, Integer> annotations;
|
||||
|
||||
public OutputDescriptor(ScriptType scriptType, ExtendedKey extendedPublicKey, KeyDerivation keyDerivation) {
|
||||
this(scriptType, Collections.singletonMap(extendedPublicKey, keyDerivation));
|
||||
@ -72,45 +61,18 @@ public class OutputDescriptor {
|
||||
}
|
||||
|
||||
public OutputDescriptor(ScriptType scriptType, int multisigThreshold, Map<ExtendedKey, KeyDerivation> extendedPublicKeys, Map<ExtendedKey, String> mapChildrenDerivations, Map<ExtendedKey, String> mapExtendedPublicKeyLabels, Map<ExtendedKey, ExtendedKey> extendedMasterPrivateKeys) {
|
||||
this(scriptType, multisigThreshold, extendedPublicKeys, mapChildrenDerivations, mapExtendedPublicKeyLabels, extendedMasterPrivateKeys, new LinkedHashMap<>());
|
||||
}
|
||||
|
||||
public OutputDescriptor(ScriptType scriptType, int multisigThreshold, Map<ExtendedKey, KeyDerivation> extendedPublicKeys, Map<ExtendedKey, String> mapChildrenDerivations, Map<ExtendedKey, String> mapExtendedPublicKeyLabels, Map<ExtendedKey, ExtendedKey> extendedMasterPrivateKeys, Map<String, Integer> annotations) {
|
||||
this.scriptType = scriptType;
|
||||
this.multisigThreshold = multisigThreshold;
|
||||
this.extendedPublicKeys = extendedPublicKeys;
|
||||
this.mapChildrenDerivations = mapChildrenDerivations;
|
||||
this.mapExtendedPublicKeyLabels = mapExtendedPublicKeyLabels;
|
||||
this.extendedMasterPrivateKeys = extendedMasterPrivateKeys;
|
||||
this.silentPaymentScanAddresses = new LinkedHashMap<>();
|
||||
this.mapSilentPaymentLabels = new LinkedHashMap<>();
|
||||
this.annotations = annotations;
|
||||
}
|
||||
|
||||
public OutputDescriptor(Map<SilentPaymentScanAddress, KeyDerivation> silentPaymentScanAddresses, Map<SilentPaymentScanAddress, String> mapSilentPaymentLabels) {
|
||||
this(silentPaymentScanAddresses, mapSilentPaymentLabels, new LinkedHashMap<>());
|
||||
}
|
||||
|
||||
public OutputDescriptor(Map<SilentPaymentScanAddress, KeyDerivation> silentPaymentScanAddresses, Map<SilentPaymentScanAddress, String> mapSilentPaymentLabels, Map<String, Integer> annotations) {
|
||||
this.scriptType = ScriptType.P2TR;
|
||||
this.multisigThreshold = 1;
|
||||
this.extendedPublicKeys = new LinkedHashMap<>();
|
||||
this.mapChildrenDerivations = new LinkedHashMap<>();
|
||||
this.mapExtendedPublicKeyLabels = new LinkedHashMap<>();
|
||||
this.extendedMasterPrivateKeys = new LinkedHashMap<>();
|
||||
this.silentPaymentScanAddresses = silentPaymentScanAddresses;
|
||||
this.mapSilentPaymentLabels = mapSilentPaymentLabels;
|
||||
this.annotations = annotations;
|
||||
}
|
||||
|
||||
public Set<ExtendedKey> getExtendedPublicKeys() {
|
||||
return Collections.unmodifiableSet(extendedPublicKeys.keySet());
|
||||
}
|
||||
|
||||
public Map<ExtendedKey, KeyDerivation> getExtendedPublicKeysMap() {
|
||||
return Collections.unmodifiableMap(extendedPublicKeys);
|
||||
}
|
||||
|
||||
public KeyDerivation getKeyDerivation(ExtendedKey extendedPublicKey) {
|
||||
return extendedPublicKeys.get(extendedPublicKey);
|
||||
}
|
||||
@ -119,10 +81,6 @@ public class OutputDescriptor {
|
||||
return multisigThreshold;
|
||||
}
|
||||
|
||||
public Map<ExtendedKey, String> getChildDerivationsMap() {
|
||||
return Collections.unmodifiableMap(mapChildrenDerivations);
|
||||
}
|
||||
|
||||
public String getChildDerivationPath(ExtendedKey extendedPublicKey) {
|
||||
return mapChildrenDerivations.get(extendedPublicKey);
|
||||
}
|
||||
@ -195,7 +153,7 @@ public class OutputDescriptor {
|
||||
}
|
||||
|
||||
public boolean isCosigner() {
|
||||
return !isMultisig() && scriptType.isAllowed(PolicyType.MULTI_HD);
|
||||
return !isMultisig() && scriptType.isAllowed(PolicyType.MULTI);
|
||||
}
|
||||
|
||||
public ExtendedKey getSingletonExtendedPublicKey() {
|
||||
@ -210,22 +168,6 @@ public class OutputDescriptor {
|
||||
return scriptType;
|
||||
}
|
||||
|
||||
public Map<SilentPaymentScanAddress, KeyDerivation> getSilentPaymentScanAddresses() {
|
||||
return Collections.unmodifiableMap(silentPaymentScanAddresses);
|
||||
}
|
||||
|
||||
public boolean isSilentPayments() {
|
||||
return !silentPaymentScanAddresses.isEmpty();
|
||||
}
|
||||
|
||||
public Map<String, Integer> getAnnotations() {
|
||||
return Collections.unmodifiableMap(annotations);
|
||||
}
|
||||
|
||||
public void clearAnnotations() {
|
||||
annotations.clear();
|
||||
}
|
||||
|
||||
public boolean describesMultipleAddresses() {
|
||||
for(ExtendedKey pubKey : extendedPublicKeys.keySet()) {
|
||||
if(describesMultipleAddresses(pubKey)) {
|
||||
@ -282,7 +224,7 @@ public class OutputDescriptor {
|
||||
}
|
||||
|
||||
public Address getAddress(DeterministicKey childKey) {
|
||||
return scriptType.getAddress(PolicyType.SINGLE_HD, childKey);
|
||||
return scriptType.getAddress(childKey);
|
||||
}
|
||||
|
||||
private Address getAddress(Script multisigScript) {
|
||||
@ -314,12 +256,8 @@ public class OutputDescriptor {
|
||||
}
|
||||
|
||||
public Wallet toWallet() {
|
||||
if(isSilentPayments()) {
|
||||
return toSilentPaymentWallet();
|
||||
}
|
||||
|
||||
Wallet wallet = new Wallet();
|
||||
wallet.setPolicyType(isMultisig() || isCosigner() ? PolicyType.MULTI_HD : PolicyType.SINGLE_HD);
|
||||
wallet.setPolicyType(isMultisig() || isCosigner() ? PolicyType.MULTI : PolicyType.SINGLE);
|
||||
wallet.setScriptType(scriptType);
|
||||
|
||||
for(Map.Entry<ExtendedKey,KeyDerivation> extKeyEntry : extendedPublicKeys.entrySet()) {
|
||||
@ -329,13 +267,11 @@ public class OutputDescriptor {
|
||||
ExtendedKey xprv = extendedMasterPrivateKeys.get(xpub);
|
||||
MasterPrivateExtendedKey masterPrivateExtendedKey = new MasterPrivateExtendedKey(xprv.getKey().getPrivKeyBytes(), xprv.getKey().getChainCode());
|
||||
String childDerivation = mapChildrenDerivations.get(xpub) == null ? scriptType.getDefaultDerivationPath() : mapChildrenDerivations.get(xpub);
|
||||
if(childDerivation.endsWith("/<0;1>/*")) {
|
||||
childDerivation = childDerivation.substring(0, childDerivation.length() - 8);
|
||||
} else if(childDerivation.endsWith("/0/*") || childDerivation.endsWith("/1/*")) {
|
||||
if(childDerivation.endsWith("/0/*") || childDerivation.endsWith("/1/*")) {
|
||||
childDerivation = childDerivation.substring(0, childDerivation.length() - 4);
|
||||
}
|
||||
try {
|
||||
keystore = Keystore.fromMasterPrivateExtendedKey(masterPrivateExtendedKey, wallet.getPolicyType(), KeyDerivation.parsePath(childDerivation));
|
||||
keystore = Keystore.fromMasterPrivateExtendedKey(masterPrivateExtendedKey, KeyDerivation.parsePath(childDerivation));
|
||||
} catch(MnemonicException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
@ -351,54 +287,10 @@ public class OutputDescriptor {
|
||||
}
|
||||
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(wallet.getPolicyType(), wallet.getScriptType(), wallet.getKeystores(), getMultisigThreshold()));
|
||||
applyAnnotations(wallet);
|
||||
|
||||
return wallet;
|
||||
}
|
||||
|
||||
private Wallet toSilentPaymentWallet() {
|
||||
Wallet wallet = new Wallet();
|
||||
wallet.setPolicyType(PolicyType.SINGLE_SP);
|
||||
wallet.setScriptType(ScriptType.P2TR);
|
||||
|
||||
Map.Entry<SilentPaymentScanAddress, KeyDerivation> entry = silentPaymentScanAddresses.entrySet().iterator().next();
|
||||
SilentPaymentScanAddress spScanAddress = entry.getKey();
|
||||
KeyDerivation keyDerivation = entry.getValue();
|
||||
|
||||
Keystore keystore = new Keystore();
|
||||
keystore.setSource(KeystoreSource.SW_WATCH);
|
||||
keystore.setWalletModel(WalletModel.SPARROW);
|
||||
keystore.setKeyDerivation(keyDerivation);
|
||||
keystore.setSilentPaymentScanAddress(spScanAddress);
|
||||
setKeystoreLabel(keystore);
|
||||
|
||||
wallet.getKeystores().add(keystore);
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE_SP, ScriptType.P2TR, wallet.getKeystores(), 1));
|
||||
applyAnnotations(wallet);
|
||||
|
||||
return wallet;
|
||||
}
|
||||
|
||||
private void applyAnnotations(Wallet wallet) {
|
||||
if(annotations.containsKey(ANNOTATION_BLOCK_HEIGHT)) {
|
||||
wallet.setBirthHeight(annotations.get(ANNOTATION_BLOCK_HEIGHT));
|
||||
}
|
||||
if(annotations.containsKey(ANNOTATION_GAP_LIMIT) && wallet.getPolicyType() != PolicyType.SINGLE_SP) {
|
||||
wallet.setGapLimit(annotations.get(ANNOTATION_GAP_LIMIT));
|
||||
}
|
||||
}
|
||||
|
||||
public Wallet toKeystoreWallet(String masterFingerprint) {
|
||||
if(isSilentPayments()) {
|
||||
Wallet wallet = toSilentPaymentWallet();
|
||||
if(masterFingerprint != null) {
|
||||
KeyDerivation existing = wallet.getKeystores().getFirst().getKeyDerivation();
|
||||
wallet.getKeystores().getFirst().setKeyDerivation(new KeyDerivation(masterFingerprint, existing.getDerivationPath()));
|
||||
}
|
||||
|
||||
return wallet;
|
||||
}
|
||||
|
||||
Wallet wallet = new Wallet();
|
||||
if(isMultisig()) {
|
||||
throw new IllegalStateException("Multisig output descriptors are unsupported.");
|
||||
@ -415,21 +307,14 @@ public class OutputDescriptor {
|
||||
keystore.setExtendedPublicKey(extendedKey);
|
||||
setKeystoreLabel(keystore);
|
||||
wallet.getKeystores().add(keystore);
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(isCosigner() ? PolicyType.MULTI_HD : PolicyType.SINGLE_HD, wallet.getScriptType(), wallet.getKeystores(), 1));
|
||||
applyAnnotations(wallet);
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(isCosigner() ? PolicyType.MULTI : PolicyType.SINGLE, wallet.getScriptType(), wallet.getKeystores(), 1));
|
||||
|
||||
return wallet;
|
||||
}
|
||||
|
||||
public void setKeystoreLabel(Keystore keystore) {
|
||||
String label = null;
|
||||
if(keystore.getExtendedPublicKey() != null && mapExtendedPublicKeyLabels.get(keystore.getExtendedPublicKey()) != null) {
|
||||
label = mapExtendedPublicKeyLabels.get(keystore.getExtendedPublicKey());
|
||||
} else if(keystore.getSilentPaymentScanAddress() != null && mapSilentPaymentLabels.get(keystore.getSilentPaymentScanAddress()) != null) {
|
||||
label = mapSilentPaymentLabels.get(keystore.getSilentPaymentScanAddress());
|
||||
}
|
||||
if(label != null) {
|
||||
label = label.trim();
|
||||
String label = mapExtendedPublicKeyLabels.get(keystore.getExtendedPublicKey()).trim();
|
||||
if(label.length() > Keystore.MAX_LABEL_LENGTH) {
|
||||
label = label.substring(0, Keystore.MAX_LABEL_LENGTH);
|
||||
}
|
||||
@ -454,16 +339,6 @@ public class OutputDescriptor {
|
||||
}
|
||||
|
||||
public static OutputDescriptor getOutputDescriptor(Wallet wallet, List<KeyPurpose> keyPurposes, Integer index) {
|
||||
Map<String, Integer> annotations = getWalletAnnotations(wallet);
|
||||
|
||||
if(wallet.getPolicyType() == PolicyType.SINGLE_SP) {
|
||||
Keystore keystore = wallet.getKeystores().getFirst();
|
||||
Map<SilentPaymentScanAddress, KeyDerivation> spMap = new LinkedHashMap<>();
|
||||
spMap.put(keystore.getSilentPaymentScanAddress(), keystore.getKeyDerivation());
|
||||
|
||||
return new OutputDescriptor(spMap, new LinkedHashMap<>(), annotations);
|
||||
}
|
||||
|
||||
Map<ExtendedKey, KeyDerivation> extendedKeyDerivationMap = new LinkedHashMap<>();
|
||||
Map<ExtendedKey, String> extendedKeyChildDerivationMap = new LinkedHashMap<>();
|
||||
for(Keystore keystore : wallet.getKeystores()) {
|
||||
@ -485,45 +360,12 @@ public class OutputDescriptor {
|
||||
return new OutputDescriptor(wallet.getScriptType(), wallet.getDefaultPolicy().getNumSignaturesRequired(), extendedKeyDerivationMap, extendedKeyChildDerivationMap);
|
||||
}
|
||||
|
||||
private static Map<String, Integer> getWalletAnnotations(Wallet wallet) {
|
||||
Map<String, Integer> annotations = new LinkedHashMap<>();
|
||||
if(wallet.getBirthHeight() != null) {
|
||||
annotations.put(ANNOTATION_BLOCK_HEIGHT, wallet.getBirthHeight());
|
||||
}
|
||||
if(wallet.gapLimit() != null && wallet.getPolicyType() != PolicyType.SINGLE_SP) {
|
||||
annotations.put(ANNOTATION_GAP_LIMIT, wallet.getGapLimit());
|
||||
}
|
||||
|
||||
return annotations;
|
||||
}
|
||||
|
||||
// See https://github.com/bitcoin/bitcoin/blob/master/doc/descriptors.md
|
||||
public static OutputDescriptor getOutputDescriptor(String descriptor) {
|
||||
return getOutputDescriptor(descriptor, new LinkedHashMap<>());
|
||||
}
|
||||
|
||||
public static OutputDescriptor getOutputDescriptor(String descriptor, Map<ExtendedKey, String> mapExtendedPublicKeyLabels) {
|
||||
Matcher checksumMatcher = CHECKSUM_PATTERN.matcher(descriptor);
|
||||
if(checksumMatcher.find()) {
|
||||
String checksum = checksumMatcher.group(1);
|
||||
String calculatedChecksum = getChecksum(descriptor.substring(0, checksumMatcher.start()));
|
||||
if(!checksum.equals(calculatedChecksum)) {
|
||||
throw new IllegalArgumentException("Descriptor checksum invalid - checksum of " + checksum + " did not match calculated checksum of " + calculatedChecksum);
|
||||
}
|
||||
descriptor = descriptor.substring(0, checksumMatcher.start());
|
||||
}
|
||||
|
||||
Map<String, Integer> annotations = new LinkedHashMap<>();
|
||||
int annotationStart = descriptor.indexOf('?');
|
||||
if(annotationStart >= 0) {
|
||||
annotations = parseAnnotations(descriptor.substring(annotationStart + 1));
|
||||
descriptor = descriptor.substring(0, annotationStart);
|
||||
}
|
||||
|
||||
if(descriptor.toLowerCase(Locale.ROOT).startsWith("sp(")) {
|
||||
return parseSilentPaymentDescriptor(descriptor, annotations);
|
||||
}
|
||||
|
||||
ScriptType scriptType = ScriptType.fromDescriptor(descriptor);
|
||||
if(scriptType == null) {
|
||||
ExtendedKey.Header header = ExtendedKey.Header.fromExtendedKey(descriptor);
|
||||
@ -535,7 +377,7 @@ public class OutputDescriptor {
|
||||
}
|
||||
|
||||
int threshold = getMultisigThreshold(descriptor);
|
||||
return getOutputDescriptorImpl(scriptType, threshold, descriptor, mapExtendedPublicKeyLabels, annotations);
|
||||
return getOutputDescriptorImpl(scriptType, threshold, descriptor, mapExtendedPublicKeyLabels);
|
||||
}
|
||||
|
||||
private static int getMultisigThreshold(String descriptor) {
|
||||
@ -548,47 +390,56 @@ public class OutputDescriptor {
|
||||
}
|
||||
}
|
||||
|
||||
private static OutputDescriptor getOutputDescriptorImpl(ScriptType scriptType, int multisigThreshold, String descriptor, Map<ExtendedKey, String> mapExtendedPublicKeyLabels, Map<String, Integer> annotations) {
|
||||
private static OutputDescriptor getOutputDescriptorImpl(ScriptType scriptType, int multisigThreshold, String descriptor, Map<ExtendedKey, String> mapExtendedPublicKeyLabels) {
|
||||
Matcher checksumMatcher = CHECKSUM_PATTERN.matcher(descriptor);
|
||||
if(checksumMatcher.find()) {
|
||||
String checksum = checksumMatcher.group(1);
|
||||
String calculatedChecksum = getChecksum(descriptor.substring(0, checksumMatcher.start()));
|
||||
if(!checksum.equals(calculatedChecksum)) {
|
||||
throw new IllegalArgumentException("Descriptor checksum invalid - checksum of " + checksum + " did not match calculated checksum of " + calculatedChecksum);
|
||||
}
|
||||
}
|
||||
|
||||
Map<ExtendedKey, KeyDerivation> keyDerivationMap = new LinkedHashMap<>();
|
||||
Map<ExtendedKey, String> keyChildDerivationMap = new LinkedHashMap<>();
|
||||
Map<ExtendedKey, ExtendedKey> masterPrivateKeyMap = new LinkedHashMap<>();
|
||||
Matcher matcher = XPUB_PATTERN.matcher(descriptor);
|
||||
while(matcher.find()) {
|
||||
String keyOriginAndExtKey = (matcher.group(1) != null ? matcher.group(1) : "") + matcher.group(2);
|
||||
KeyDerivationAndKey originResult = parseKeyOrigin(keyOriginAndExtKey);
|
||||
KeyDerivation keyDerivation = originResult.keyDerivation();
|
||||
String extKey = originResult.key();
|
||||
String childDerivationPath = matcher.group(3);
|
||||
String masterFingerprint = null;
|
||||
String keyDerivationPath = null;
|
||||
String extKey;
|
||||
String childDerivationPath = null;
|
||||
|
||||
if(matcher.group(1) != null) {
|
||||
String keyOrigin = matcher.group(1);
|
||||
Matcher keyOriginMatcher = KEY_ORIGIN_PATTERN.matcher(keyOrigin);
|
||||
if(keyOriginMatcher.matches()) {
|
||||
byte[] masterFingerprintBytes = keyOriginMatcher.group(1) != null ? Utils.hexToBytes(keyOriginMatcher.group(1)) : new byte[4];
|
||||
if(masterFingerprintBytes.length != 4) {
|
||||
throw new IllegalArgumentException("Master fingerprint must be 4 bytes: " + Utils.bytesToHex(masterFingerprintBytes));
|
||||
}
|
||||
masterFingerprint = Utils.bytesToHex(masterFingerprintBytes);
|
||||
keyDerivationPath = KeyDerivation.writePath(KeyDerivation.parsePath(keyOriginMatcher.group(2)));
|
||||
}
|
||||
}
|
||||
|
||||
extKey = matcher.group(2);
|
||||
if(matcher.group(3) != null) {
|
||||
childDerivationPath = matcher.group(3);
|
||||
}
|
||||
|
||||
KeyDerivation keyDerivation = new KeyDerivation(masterFingerprint, keyDerivationPath);
|
||||
try {
|
||||
ExtendedKey extendedKey = ExtendedKey.fromDescriptor(extKey);
|
||||
if(childDerivationPath != null) {
|
||||
try {
|
||||
List<ChildNumber> childPath = KeyDerivation.parsePath(childDerivationPath.replace("<0;1>", "0"));
|
||||
if(childPath.size() > 2 && (extendedKey.getKey().hasPrivKey() || childPath.stream().noneMatch(ChildNumber::isHardened))) {
|
||||
childDerivationPath = childDerivationPath.substring(childDerivationPath.lastIndexOf("/", childDerivationPath.lastIndexOf("/") - 1));
|
||||
childPath = childPath.subList(0, childPath.size() - 2);
|
||||
if(keyDerivation.getMasterFingerprint() == null) {
|
||||
keyDerivation = new KeyDerivation(Utils.bytesToHex(extendedKey.getKey().getFingerprint()), keyDerivation.getDerivationPath());
|
||||
}
|
||||
keyDerivation = keyDerivation.extend(childPath);
|
||||
childPath.addFirst(extendedKey.getKeyChildNumber());
|
||||
DeterministicKey derivedKey = extendedKey.getKey(childPath);
|
||||
DeterministicKey pubKey = new DeterministicKey(List.of(derivedKey.getPath().getLast()), derivedKey.getChainCode(), derivedKey.getPubKey(), derivedKey.getDepth(), derivedKey.getParentFingerprint());
|
||||
extendedKey = new ExtendedKey(pubKey, pubKey.getParentFingerprint(), childPath.getLast());
|
||||
}
|
||||
} catch(Exception e) {
|
||||
//ignore and continue
|
||||
}
|
||||
}
|
||||
if(extendedKey.getKey().hasPrivKey()) {
|
||||
ExtendedKey privateExtendedKey = extendedKey;
|
||||
List<ChildNumber> derivation = keyDerivation.getDerivation();
|
||||
int depth = derivation.isEmpty() ? scriptType.getDefaultDerivation().size() : derivation.size();
|
||||
int depth = derivation.size() == 0 ? scriptType.getDefaultDerivation().size() : derivation.size();
|
||||
DeterministicKey prvKey = extendedKey.getKey();
|
||||
DeterministicKey pubKey = new DeterministicKey(List.of(prvKey.getPath().getLast()), prvKey.getChainCode(), prvKey.getPubKey(), depth, extendedKey.getParentFingerprint());
|
||||
DeterministicKey pubKey = new DeterministicKey(prvKey.getPath(), prvKey.getChainCode(), prvKey.getPubKey(), depth, extendedKey.getParentFingerprint());
|
||||
extendedKey = new ExtendedKey(pubKey, pubKey.getParentFingerprint(), extendedKey.getKeyChildNumber());
|
||||
|
||||
if(derivation.isEmpty()) {
|
||||
if(derivation.size() == 0) {
|
||||
masterPrivateKeyMap.put(extendedKey, privateExtendedKey);
|
||||
}
|
||||
}
|
||||
@ -606,170 +457,7 @@ public class OutputDescriptor {
|
||||
}
|
||||
}
|
||||
|
||||
return new OutputDescriptor(scriptType, multisigThreshold, keyDerivationMap, keyChildDerivationMap, mapExtendedPublicKeyLabels, masterPrivateKeyMap, annotations);
|
||||
}
|
||||
|
||||
private static OutputDescriptor parseSilentPaymentDescriptor(String descriptor, Map<String, Integer> annotations) {
|
||||
if(!descriptor.startsWith("sp(") || !descriptor.endsWith(")")) {
|
||||
throw new IllegalArgumentException("Invalid sp() descriptor format");
|
||||
}
|
||||
String inner = descriptor.substring(3, descriptor.length() - 1);
|
||||
|
||||
int commaIndex = inner.indexOf(',');
|
||||
if(commaIndex < 0) {
|
||||
return parseSingleArgSp(inner.trim(), annotations);
|
||||
} else {
|
||||
return parseTwoArgSp(inner.substring(0, commaIndex).trim(), inner.substring(commaIndex + 1).trim(), annotations);
|
||||
}
|
||||
}
|
||||
|
||||
private record KeyDerivationAndKey(KeyDerivation keyDerivation, String key) {}
|
||||
|
||||
private static KeyDerivationAndKey parseKeyOrigin(String arg) {
|
||||
KeyDerivation keyDerivation = new KeyDerivation(null, (String)null);
|
||||
if(arg.startsWith("[")) {
|
||||
int closeBracket = arg.indexOf(']');
|
||||
if(closeBracket < 0) {
|
||||
throw new IllegalArgumentException("Unclosed key origin bracket in descriptor");
|
||||
}
|
||||
String keyOrigin = arg.substring(0, closeBracket + 1);
|
||||
Matcher keyOriginMatcher = KEY_ORIGIN_PATTERN.matcher(keyOrigin);
|
||||
if(keyOriginMatcher.matches()) {
|
||||
byte[] masterFingerprintBytes = keyOriginMatcher.group(1) != null ? Utils.hexToBytes(keyOriginMatcher.group(1)) : new byte[4];
|
||||
if(masterFingerprintBytes.length != 4) {
|
||||
throw new IllegalArgumentException("Master fingerprint must be 4 bytes: " + Utils.bytesToHex(masterFingerprintBytes));
|
||||
}
|
||||
String masterFingerprint = Utils.bytesToHex(masterFingerprintBytes);
|
||||
String keyDerivationPath = KeyDerivation.writePath(KeyDerivation.parsePath(keyOriginMatcher.group(2)));
|
||||
keyDerivation = new KeyDerivation(masterFingerprint, keyDerivationPath);
|
||||
}
|
||||
arg = arg.substring(closeBracket + 1);
|
||||
}
|
||||
|
||||
return new KeyDerivationAndKey(keyDerivation, arg);
|
||||
}
|
||||
|
||||
private static OutputDescriptor parseSingleArgSp(String arg, Map<String, Integer> annotations) {
|
||||
KeyDerivationAndKey originResult = parseKeyOrigin(arg);
|
||||
KeyDerivation keyDerivation = originResult.keyDerivation();
|
||||
arg = originResult.key();
|
||||
|
||||
String lowerArg = arg.toLowerCase(Locale.ROOT);
|
||||
String scanHrp = Network.get().getSilentPaymentsScanKeyHrp();
|
||||
String spendHrp = Network.get().getSilentPaymentsSpendKeyHrp();
|
||||
if(!lowerArg.startsWith(scanHrp + "1") && !lowerArg.startsWith(spendHrp + "1")) {
|
||||
throw new IllegalArgumentException("Single argument sp() descriptor requires spscan or spspend encoded key, got: " + arg);
|
||||
}
|
||||
|
||||
SilentPaymentScanAddress spScanAddress = SilentPaymentScanAddress.fromKeyString(arg);
|
||||
Map<SilentPaymentScanAddress, KeyDerivation> spMap = new LinkedHashMap<>();
|
||||
spMap.put(spScanAddress, keyDerivation);
|
||||
|
||||
return new OutputDescriptor(spMap, new LinkedHashMap<>(), annotations);
|
||||
}
|
||||
|
||||
private static OutputDescriptor parseTwoArgSp(String scanArg, String spendArg, Map<String, Integer> annotations) {
|
||||
KeyDerivationAndKey originResult = parseKeyOrigin(scanArg);
|
||||
KeyDerivation keyDerivation = originResult.keyDerivation();
|
||||
if(keyDerivation.getDerivation().size() > 2) {
|
||||
List<ChildNumber> accountDerivation = keyDerivation.getDerivation().subList(0, keyDerivation.getDerivation().size() - 2);
|
||||
if(KeyDerivation.getBip352ScanDerivation(accountDerivation).equals(keyDerivation.getDerivation())) {
|
||||
keyDerivation = new KeyDerivation(keyDerivation.getMasterFingerprint(), accountDerivation);
|
||||
}
|
||||
}
|
||||
|
||||
ECKey scanPrivateKey = parseSilentPaymentScanKey(originResult.key());
|
||||
ECKey spendKey = parseSilentPaymentSpendKey(spendArg);
|
||||
|
||||
ECKey spendPubKey = spendKey.isPubKeyOnly() ? spendKey : ECKey.fromPublicOnly(spendKey.getPubKey());
|
||||
SilentPaymentScanAddress spScanAddress = new SilentPaymentScanAddress(scanPrivateKey, spendPubKey);
|
||||
Map<SilentPaymentScanAddress, KeyDerivation> spMap = new LinkedHashMap<>();
|
||||
spMap.put(spScanAddress, keyDerivation);
|
||||
|
||||
return new OutputDescriptor(spMap, new LinkedHashMap<>(), annotations);
|
||||
}
|
||||
|
||||
private static ECKey parseSilentPaymentScanKey(String arg) {
|
||||
Matcher xprvMatcher = XPUB_PATTERN.matcher(arg);
|
||||
if(xprvMatcher.matches()) {
|
||||
String extKeyStr = xprvMatcher.group(2);
|
||||
String childPath = xprvMatcher.group(3);
|
||||
ExtendedKey extKey = ExtendedKey.fromDescriptor(extKeyStr);
|
||||
if(!extKey.getKey().hasPrivKey()) {
|
||||
throw new IllegalArgumentException("The scan key must be private, and not an xpub: " + extKeyStr);
|
||||
}
|
||||
if(childPath != null) {
|
||||
List<ChildNumber> path = KeyDerivation.parsePath(childPath);
|
||||
path.addFirst(extKey.getKeyChildNumber());
|
||||
DeterministicKey derived = extKey.getKey(path);
|
||||
|
||||
return ECKey.fromPrivate(derived.getPrivKeyBytes(), true);
|
||||
}
|
||||
|
||||
return ECKey.fromPrivate(extKey.getKey().getPrivKeyBytes(), true);
|
||||
}
|
||||
|
||||
DumpedPrivateKey dpk;
|
||||
try {
|
||||
dpk = DumpedPrivateKey.fromBase58(arg);
|
||||
} catch(Exception e) {
|
||||
throw new IllegalArgumentException("Cannot parse sp() scan key as xprv or WIF: " + arg, e);
|
||||
}
|
||||
|
||||
ECKey key = dpk.getKey();
|
||||
if(!key.isCompressed()) {
|
||||
throw new IllegalArgumentException("Uncompressed keys are not allowed in sp() descriptors");
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
private static ECKey parseSilentPaymentSpendKey(String arg) {
|
||||
if(arg.startsWith("[")) {
|
||||
int closeBracket = arg.indexOf(']');
|
||||
if(closeBracket >= 0) {
|
||||
arg = arg.substring(closeBracket + 1);
|
||||
}
|
||||
}
|
||||
|
||||
Matcher xpubMatcher = XPUB_PATTERN.matcher(arg);
|
||||
if(xpubMatcher.matches()) {
|
||||
String extKeyStr = xpubMatcher.group(2);
|
||||
String childPath = xpubMatcher.group(3);
|
||||
ExtendedKey extKey = ExtendedKey.fromDescriptor(extKeyStr);
|
||||
if(childPath != null) {
|
||||
List<ChildNumber> path = KeyDerivation.parsePath(childPath);
|
||||
path.addFirst(extKey.getKeyChildNumber());
|
||||
DeterministicKey derived = extKey.getKey(path);
|
||||
|
||||
return extKey.getKey().hasPrivKey() ? ECKey.fromPrivate(derived.getPrivKeyBytes(), true) : ECKey.fromPublicOnly(derived.getPubKey());
|
||||
}
|
||||
|
||||
return extKey.getKey().hasPrivKey() ? ECKey.fromPrivate(extKey.getKey().getPrivKeyBytes(), true) : ECKey.fromPublicOnly(extKey.getKey().getPubKey());
|
||||
}
|
||||
|
||||
Matcher pubKeyMatcher = PUBKEY_PATTERN.matcher(arg);
|
||||
if(pubKeyMatcher.matches()) {
|
||||
return ECKey.fromPublicOnly(Utils.hexToBytes(pubKeyMatcher.group(2)));
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Cannot parse sp() spend key as xpub, xprv, or compressed pubkey: " + arg);
|
||||
}
|
||||
|
||||
private static Map<String, Integer> parseAnnotations(String annotationString) {
|
||||
Map<String, Integer> annotations = new LinkedHashMap<>();
|
||||
String[] pairs = annotationString.split("&");
|
||||
for(String pair : pairs) {
|
||||
Matcher matcher = ANNOTATION_PATTERN.matcher(pair);
|
||||
if(matcher.matches()) {
|
||||
String key = matcher.group(1).toLowerCase(Locale.ROOT);
|
||||
if(!annotations.containsKey(key)) {
|
||||
annotations.put(key, Integer.parseInt(matcher.group(2)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return annotations;
|
||||
return new OutputDescriptor(scriptType, multisigThreshold, keyDerivationMap, keyChildDerivationMap, mapExtendedPublicKeyLabels, masterPrivateKeyMap);
|
||||
}
|
||||
|
||||
public static String normalize(String descriptor) {
|
||||
@ -859,53 +547,24 @@ public class OutputDescriptor {
|
||||
}
|
||||
|
||||
public String toString(boolean addKeyOrigin, boolean addKey, boolean addChecksum) {
|
||||
return toString(addKeyOrigin, addKey, addChecksum, false);
|
||||
}
|
||||
|
||||
public String toString(boolean addKeyOrigin, boolean addKey, boolean addChecksum, boolean alternateForm) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append(scriptType.getDescriptor());
|
||||
|
||||
if(isSilentPayments()) {
|
||||
Map.Entry<SilentPaymentScanAddress, KeyDerivation> entry = silentPaymentScanAddresses.entrySet().iterator().next();
|
||||
builder.append("sp(");
|
||||
if(alternateForm) {
|
||||
KeyDerivation scanDerivation = new KeyDerivation(entry.getValue().getMasterFingerprint(), KeyDerivation.getBip352ScanDerivation(entry.getValue().getDerivation()));
|
||||
builder.append(writeKey(entry.getKey().getScanKey(), scanDerivation, addKeyOrigin, addKey));
|
||||
builder.append(",");
|
||||
KeyDerivation spendDerivation = new KeyDerivation(entry.getValue().getMasterFingerprint(), KeyDerivation.getBip352SpendDerivation(entry.getValue().getDerivation()));
|
||||
builder.append(writeKey(entry.getKey().getSpendKey(), spendDerivation, addKeyOrigin, addKey));
|
||||
} else {
|
||||
builder.append(writeKey(entry.getKey(), entry.getValue(), addKeyOrigin, addKey));
|
||||
if(isMultisig()) {
|
||||
builder.append(ScriptType.MULTISIG.getDescriptor());
|
||||
StringJoiner joiner = new StringJoiner(",");
|
||||
joiner.add(Integer.toString(multisigThreshold));
|
||||
for(ExtendedKey pubKey : sortExtendedPubKeys(extendedPublicKeys.keySet())) {
|
||||
String extKeyString = toString(pubKey, addKeyOrigin, addKey);
|
||||
joiner.add(extKeyString);
|
||||
}
|
||||
builder.append(")");
|
||||
builder.append(joiner.toString());
|
||||
builder.append(ScriptType.MULTISIG.getCloseDescriptor());
|
||||
} else {
|
||||
builder.append(scriptType.getDescriptor());
|
||||
|
||||
if(isMultisig()) {
|
||||
builder.append(ScriptType.MULTISIG.getDescriptor());
|
||||
StringJoiner joiner = new StringJoiner(",");
|
||||
joiner.add(Integer.toString(multisigThreshold));
|
||||
for(ExtendedKey pubKey : sortExtendedPubKeys(extendedPublicKeys.keySet())) {
|
||||
String extKeyString = toString(pubKey, addKeyOrigin, addKey);
|
||||
joiner.add(extKeyString);
|
||||
}
|
||||
builder.append(joiner.toString());
|
||||
builder.append(ScriptType.MULTISIG.getCloseDescriptor());
|
||||
} else {
|
||||
ExtendedKey extendedPublicKey = getSingletonExtendedPublicKey();
|
||||
builder.append(toString(extendedPublicKey, addKeyOrigin, addKey));
|
||||
}
|
||||
builder.append(scriptType.getCloseDescriptor());
|
||||
}
|
||||
|
||||
if(!annotations.isEmpty()) {
|
||||
builder.append("?");
|
||||
StringJoiner annotationJoiner = new StringJoiner("&");
|
||||
for(Map.Entry<String, Integer> entry : annotations.entrySet()) {
|
||||
annotationJoiner.add(entry.getKey() + "=" + entry.getValue());
|
||||
}
|
||||
builder.append(annotationJoiner);
|
||||
ExtendedKey extendedPublicKey = getSingletonExtendedPublicKey();
|
||||
builder.append(toString(extendedPublicKey, addKeyOrigin, addKey));
|
||||
}
|
||||
builder.append(scriptType.getCloseDescriptor());
|
||||
|
||||
if(addChecksum) {
|
||||
String descriptor = builder.toString();
|
||||
@ -916,7 +575,7 @@ public class OutputDescriptor {
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
public List<ExtendedKey> sortExtendedPubKeys(Collection<ExtendedKey> keys) {
|
||||
private List<ExtendedKey> sortExtendedPubKeys(Collection<ExtendedKey> keys) {
|
||||
List<ExtendedKey> sortedKeys = new ArrayList<>(keys);
|
||||
if(mapChildrenDerivations == null || mapChildrenDerivations.isEmpty() || mapChildrenDerivations.containsKey(null)) {
|
||||
return sortedKeys;
|
||||
@ -963,60 +622,13 @@ public class OutputDescriptor {
|
||||
return writeKey(pubKey, keyDerivation, childDerivation, addKeyOrigin, addKey);
|
||||
}
|
||||
|
||||
public static String writeKey(SilentPaymentScanAddress spScanAddress, KeyDerivation keyDerivation, boolean addKeyOrigin, boolean addKey) {
|
||||
return writeKey(spScanAddress, keyDerivation, addKeyOrigin, addKey, false);
|
||||
}
|
||||
|
||||
public static String writeKey(SilentPaymentScanAddress spScanAddress, KeyDerivation keyDerivation, boolean addKeyOrigin, boolean addKey, boolean useApostrophes) {
|
||||
StringBuilder keyBuilder = new StringBuilder();
|
||||
if(addKeyOrigin && keyDerivation != null && keyDerivation.getMasterFingerprint() != null && keyDerivation.getMasterFingerprint().length() == 8 && Utils.isHex(keyDerivation.getMasterFingerprint())) {
|
||||
keyBuilder.append("[");
|
||||
keyBuilder.append(keyDerivation.getMasterFingerprint());
|
||||
if(!keyDerivation.getDerivation().isEmpty()) {
|
||||
keyBuilder.append(KeyDerivation.writePath(keyDerivation.getDerivation(), useApostrophes).substring(1));
|
||||
}
|
||||
keyBuilder.append("]");
|
||||
}
|
||||
if(addKey) {
|
||||
keyBuilder.append(spScanAddress.toKeyString());
|
||||
}
|
||||
|
||||
return keyBuilder.toString();
|
||||
}
|
||||
|
||||
public static String writeKey(ECKey ecKey, KeyDerivation keyDerivation, boolean addKeyOrigin, boolean addKey) {
|
||||
return writeKey(ecKey, keyDerivation, addKeyOrigin, addKey, false);
|
||||
}
|
||||
|
||||
public static String writeKey(ECKey ecKey, KeyDerivation keyDerivation, boolean addKeyOrigin, boolean addKey, boolean useApostrophes) {
|
||||
StringBuilder keyBuilder = new StringBuilder();
|
||||
if(addKeyOrigin && keyDerivation != null && keyDerivation.getMasterFingerprint() != null && keyDerivation.getMasterFingerprint().length() == 8 && Utils.isHex(keyDerivation.getMasterFingerprint())) {
|
||||
keyBuilder.append("[");
|
||||
keyBuilder.append(keyDerivation.getMasterFingerprint());
|
||||
if(!keyDerivation.getDerivation().isEmpty()) {
|
||||
keyBuilder.append(KeyDerivation.writePath(keyDerivation.getDerivation(), useApostrophes).substring(1));
|
||||
}
|
||||
keyBuilder.append("]");
|
||||
}
|
||||
if(addKey) {
|
||||
keyBuilder.append(ecKey.hasPrivKey() ? ecKey.getPrivateKeyEncoded().toString() : Utils.bytesToHex(ecKey.getPubKey()));
|
||||
}
|
||||
|
||||
return keyBuilder.toString();
|
||||
}
|
||||
|
||||
public static String writeKey(ExtendedKey pubKey, KeyDerivation keyDerivation, String childDerivation, boolean addKeyOrigin, boolean addKey) {
|
||||
return writeKey(pubKey, keyDerivation, childDerivation, addKeyOrigin, addKey, false);
|
||||
}
|
||||
|
||||
public static String writeKey(ExtendedKey pubKey, KeyDerivation keyDerivation, String childDerivation, boolean addKeyOrigin, boolean addKey, boolean useApostrophes) {
|
||||
StringBuilder keyBuilder = new StringBuilder();
|
||||
if(keyDerivation != null && keyDerivation.getMasterFingerprint() != null && keyDerivation.getMasterFingerprint().length() == 8 && Utils.isHex(keyDerivation.getMasterFingerprint()) && addKeyOrigin) {
|
||||
keyBuilder.append("[");
|
||||
keyBuilder.append(keyDerivation.getMasterFingerprint());
|
||||
if(!keyDerivation.getDerivation().isEmpty()) {
|
||||
String path = KeyDerivation.writePath(KeyDerivation.parsePath(keyDerivation.getDerivationPath()), useApostrophes).substring(1);
|
||||
keyBuilder.append(path);
|
||||
keyBuilder.append(keyDerivation.getDerivationPath().replaceFirst("^m?/", "/").replace('\'', 'h'));
|
||||
}
|
||||
keyBuilder.append("]");
|
||||
}
|
||||
@ -1037,44 +649,4 @@ public class OutputDescriptor {
|
||||
|
||||
return keyBuilder.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean equals(Object o) {
|
||||
if(this == o) {
|
||||
return true;
|
||||
}
|
||||
if(!(o instanceof OutputDescriptor that)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return toString().equals(that.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return toString().hashCode();
|
||||
}
|
||||
|
||||
public OutputDescriptor copy(boolean includeChildDerivations) {
|
||||
if(isSilentPayments()) {
|
||||
return new OutputDescriptor(new LinkedHashMap<>(silentPaymentScanAddresses), new LinkedHashMap<>(mapSilentPaymentLabels), new LinkedHashMap<>(annotations));
|
||||
}
|
||||
|
||||
Map<ExtendedKey, KeyDerivation> copyExtendedPublicKeys = new LinkedHashMap<>(extendedPublicKeys);
|
||||
Map<ExtendedKey, String> copyChildDerivations = new LinkedHashMap<>(mapChildrenDerivations);
|
||||
Map<ExtendedKey, String> copyExtendedPublicKeyLabels = new LinkedHashMap<>(mapExtendedPublicKeyLabels);
|
||||
Map<ExtendedKey, ExtendedKey> copyExtendedMasterPrivateKeys = new LinkedHashMap<>(extendedMasterPrivateKeys);
|
||||
Map<String, Integer> copyAnnotations = new LinkedHashMap<>(annotations);
|
||||
|
||||
if(!includeChildDerivations) {
|
||||
//Ensure consistent xpub order by sorting on the first receive address
|
||||
Map<ExtendedKey, String> childDerivations = copyExtendedPublicKeys.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, _ -> "/0/0"));
|
||||
OutputDescriptor copyFirstReceive = new OutputDescriptor(scriptType, multisigThreshold, copyExtendedPublicKeys, childDerivations);
|
||||
OutputDescriptor copySortedXpubs = OutputDescriptor.getOutputDescriptor(copyFirstReceive.toString());
|
||||
|
||||
return new OutputDescriptor(scriptType, multisigThreshold, copySortedXpubs.extendedPublicKeys, Collections.emptyMap(), copyExtendedPublicKeyLabels, copyExtendedMasterPrivateKeys, copyAnnotations);
|
||||
}
|
||||
|
||||
return new OutputDescriptor(scriptType, multisigThreshold, copyExtendedPublicKeys, copyChildDerivations, copyExtendedPublicKeyLabels, copyExtendedMasterPrivateKeys, copyAnnotations);
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,8 +4,6 @@ import com.sparrowwallet.drongo.crypto.*;
|
||||
import com.sparrowwallet.drongo.protocol.ProtocolException;
|
||||
import com.sparrowwallet.drongo.protocol.Ripemd160;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
|
||||
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
|
||||
import org.bouncycastle.crypto.digests.SHA512Digest;
|
||||
import org.bouncycastle.crypto.macs.HMac;
|
||||
import org.bouncycastle.crypto.params.KeyParameter;
|
||||
@ -14,13 +12,9 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.math.BigInteger;
|
||||
import java.net.URI;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.CharsetDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.util.*;
|
||||
|
||||
public class Utils {
|
||||
@ -53,56 +47,6 @@ public class Utils {
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isHex(byte[] bytes) {
|
||||
if(bytes == null || bytes.length == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for(byte b : trim(bytes)) {
|
||||
if(!((b >= '0' && b <= '9') || (b >= 'A' && b <= 'F') || (b >= 'a' && b <= 'f'))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean isBase64(byte[] bytes) {
|
||||
if(bytes == null || bytes.length == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for(byte b : trim(bytes)) {
|
||||
if(!((b >= '0' && b <= '9') || (b >= 'A' && b <= 'Z') || (b >= 'a' && b <= 'z') || (b == '+') || (b == '/') || (b == '='))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static byte[] trim(byte[] bytes) {
|
||||
int len = bytes.length;
|
||||
int st = 0;
|
||||
while ((st < len) && Character.isWhitespace((bytes[st] & 0xff))) {
|
||||
st++;
|
||||
}
|
||||
while ((st < len) && Character.isWhitespace((bytes[len - 1] & 0xff))) {
|
||||
len--;
|
||||
}
|
||||
|
||||
return ((st > 0) || (len < bytes.length)) ? Arrays.copyOfRange(bytes, st, len) : bytes;
|
||||
}
|
||||
|
||||
public static boolean isSecureUrl(URI uri) {
|
||||
if(uri == null || uri.getScheme() == null || uri.getHost() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String scheme = uri.getScheme().toLowerCase(Locale.ROOT);
|
||||
return "https".equals(scheme) || ("http".equals(scheme) && uri.getHost().toLowerCase(Locale.ROOT).endsWith(".onion"));
|
||||
}
|
||||
|
||||
public static String bytesToHex(byte[] bytes) {
|
||||
char[] hexChars = new char[bytes.length * 2];
|
||||
for ( int j = 0; j < bytes.length; j++ ) {
|
||||
@ -394,21 +338,6 @@ public class Utils {
|
||||
return Sha256Hash.hash(buffer.array());
|
||||
}
|
||||
|
||||
public static byte[] getRawKeyBytesFromPKCS8(PrivateKey pkcs8Key) {
|
||||
try {
|
||||
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(pkcs8Key.getEncoded());
|
||||
PrivateKeyInfo privateKeyInfo = PrivateKeyInfo.getInstance(keySpec.getEncoded());
|
||||
return privateKeyInfo.parsePrivateKey().toASN1Primitive().getEncoded();
|
||||
} catch(IOException e) {
|
||||
throw new IllegalArgumentException("Error parsing private key", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] getRawKeyBytesFromX509(PublicKey x509Key) {
|
||||
SubjectPublicKeyInfo spki = SubjectPublicKeyInfo.getInstance(x509Key.getEncoded());
|
||||
return spki.getPublicKeyData().getBytes();
|
||||
}
|
||||
|
||||
public static class LexicographicByteArrayComparator implements Comparator<byte[]> {
|
||||
@Override
|
||||
public int compare(byte[] left, byte[] right) {
|
||||
|
||||
@ -1,55 +0,0 @@
|
||||
package com.sparrowwallet.drongo;
|
||||
|
||||
public class Version implements Comparable<Version> {
|
||||
private final String version;
|
||||
|
||||
public final String get() {
|
||||
return this.version;
|
||||
}
|
||||
|
||||
public Version(String version) {
|
||||
if(version == null) {
|
||||
throw new IllegalArgumentException("Version can not be null");
|
||||
}
|
||||
version = version.replaceAll("[^0-9.].*", "");
|
||||
if(!version.matches("[0-9]+(\\.[0-9]+)*")) {
|
||||
throw new IllegalArgumentException("Invalid version format");
|
||||
}
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(Version that) {
|
||||
if(that == null) {
|
||||
return 1;
|
||||
}
|
||||
String[] thisParts = this.get().split("\\.");
|
||||
String[] thatParts = that.get().split("\\.");
|
||||
int length = Math.max(thisParts.length, thatParts.length);
|
||||
for(int i = 0; i < length; i++) {
|
||||
int thisPart = i < thisParts.length ? Integer.parseInt(thisParts[i]) : 0;
|
||||
int thatPart = i < thatParts.length ? Integer.parseInt(thatParts[i]) : 0;
|
||||
if(thisPart < thatPart) {
|
||||
return -1;
|
||||
}
|
||||
if(thisPart > thatPart) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object that) {
|
||||
if(this == that) {
|
||||
return true;
|
||||
}
|
||||
if(that == null) {
|
||||
return false;
|
||||
}
|
||||
if(this.getClass() != that.getClass()) {
|
||||
return false;
|
||||
}
|
||||
return this.compareTo((Version) that) == 0;
|
||||
}
|
||||
}
|
||||
@ -59,11 +59,11 @@ public abstract class Address {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Arrays.equals(data, address.data) && getVersion(Network.get()) == address.getVersion(Network.get()) && getScriptType() == address.getScriptType();
|
||||
return Arrays.equals(data, address.data) && getVersion(Network.get()) == address.getVersion(Network.get());
|
||||
}
|
||||
|
||||
public int hashCode() {
|
||||
return Arrays.hashCode(data) + getVersion(Network.get()) + getScriptType().hashCode();
|
||||
return Arrays.hashCode(data) + getVersion(Network.get());
|
||||
}
|
||||
|
||||
public static Address fromString(String address) throws InvalidAddressException {
|
||||
@ -134,8 +134,6 @@ public abstract class Address {
|
||||
byte[] witnessProgram = Bech32.convertBits(convertedProgram, 0, convertedProgram.length, 5, 8, false);
|
||||
if(witnessProgram.length == 32) {
|
||||
return new P2TRAddress(witnessProgram);
|
||||
} else if(Arrays.equals(witnessProgram, ScriptType.ANCHOR_WITNESS_PROGRAM)) {
|
||||
return new P2AAddress(witnessProgram);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,31 +0,0 @@
|
||||
package com.sparrowwallet.drongo.address;
|
||||
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.protocol.Bech32;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
|
||||
public class P2AAddress extends Address {
|
||||
public P2AAddress(byte[] data) {
|
||||
super(data);
|
||||
}
|
||||
|
||||
@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.P2A;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getOutputScriptDataType() {
|
||||
return "Anchor";
|
||||
}
|
||||
}
|
||||
@ -5,7 +5,6 @@ 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.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.*;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import org.slf4j.Logger;
|
||||
@ -83,7 +82,7 @@ public class PaymentCode {
|
||||
}
|
||||
|
||||
public Address getNotificationAddress() {
|
||||
return ScriptType.P2PKH.getAddress(PolicyType.SINGLE_HD, getNotificationKey());
|
||||
return ScriptType.P2PKH.getAddress(getNotificationKey());
|
||||
}
|
||||
|
||||
public ECKey getKey(int index) {
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
package com.sparrowwallet.drongo.crypto;
|
||||
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.*;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTInput;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTInputSigner;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTSignatureException;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@ -16,117 +16,41 @@ import java.util.*;
|
||||
import static com.sparrowwallet.drongo.protocol.ScriptType.P2TR;
|
||||
|
||||
public class Bip322 {
|
||||
public static final String SIMPLE_PREFIX = "smp";
|
||||
public static final String FULL_PREFIX = "ful";
|
||||
public static final String FULL_POF_PREFIX = "pof";
|
||||
|
||||
public static String signMessageBip322(ScriptType scriptType, String message, ECKey privKey) {
|
||||
checkScriptType(scriptType);
|
||||
|
||||
ECKey pubKey = ECKey.fromPublicOnly(privKey);
|
||||
Address address = scriptType.getAddress(PolicyType.SINGLE_HD, pubKey);
|
||||
|
||||
PSBT psbt = getBip322Psbt(scriptType, address, message);
|
||||
PSBTInput psbtInput = psbt.getPsbtInputs().getFirst();
|
||||
psbtInput.sign(scriptType.getOutputKey(PolicyType.SINGLE_HD, privKey));
|
||||
|
||||
return getBip322SignatureFromPsbt(scriptType, psbt, pubKey);
|
||||
}
|
||||
|
||||
public static PSBT getBip322Psbt(ScriptType scriptType, Address address, String message) {
|
||||
checkScriptType(scriptType);
|
||||
Address address = scriptType.getAddress(pubKey);
|
||||
|
||||
Transaction toSpend = getBip322ToSpend(address, message);
|
||||
Transaction toSign = getBip322ToSign(toSpend);
|
||||
TransactionOutput utxoOutput = toSpend.getOutputs().getFirst();
|
||||
|
||||
TransactionOutput utxoOutput = toSpend.getOutputs().get(0);
|
||||
|
||||
PSBT psbt = new PSBT(toSign);
|
||||
psbt.setGenericSignedMessage(message);
|
||||
PSBTInput psbtInput = psbt.getPsbtInputs().getFirst();
|
||||
PSBTInput psbtInput = psbt.getPsbtInputs().get(0);
|
||||
psbtInput.setWitnessUtxo(utxoOutput);
|
||||
psbtInput.setSigHash(SigHash.ALL);
|
||||
psbtInput.sign(scriptType.getOutputKey(privKey));
|
||||
|
||||
return psbt;
|
||||
}
|
||||
|
||||
public static PSBT getBip322PsbtSp(Address address, String message, byte[] silentPaymentsTweak, Map<ECKey, KeyDerivation> spendDerivations) {
|
||||
if(silentPaymentsTweak == null) {
|
||||
throw new IllegalArgumentException("Silent payments tweak is required");
|
||||
}
|
||||
|
||||
PSBT psbt = getBip322Psbt(P2TR, address, message);
|
||||
PSBTInput psbtInput = psbt.getPsbtInputs().getFirst();
|
||||
psbtInput.setSilentPaymentsTweak(silentPaymentsTweak);
|
||||
if(spendDerivations != null && !spendDerivations.isEmpty()) {
|
||||
psbtInput.getSilentPaymentsSpendDerivations().putAll(spendDerivations);
|
||||
}
|
||||
|
||||
return psbt;
|
||||
}
|
||||
|
||||
public static String signMessageBip322Sp(Address address, String message, ECKey spendPrivKey, byte[] silentPaymentsTweak) {
|
||||
PSBT psbt = getBip322PsbtSp(address, message, silentPaymentsTweak, Collections.emptyMap());
|
||||
PSBTInput psbtInput = psbt.getPsbtInputs().getFirst();
|
||||
|
||||
if(!psbtInput.signSilentPayments(spendPrivKey)) {
|
||||
throw new IllegalStateException("Failed to sign BIP322 PSBT with silent payments tweak");
|
||||
}
|
||||
|
||||
return getBip322SignatureFromPsbtSp(psbt);
|
||||
}
|
||||
|
||||
public static String getBip322SignatureFromPsbtSp(PSBT signedPsbt) {
|
||||
PSBTInput psbtInput = signedPsbt.getPsbtInputs().getFirst();
|
||||
TransactionSignature signature = psbtInput.getTapKeyPathSignature();
|
||||
if(signature == null) {
|
||||
throw new IllegalArgumentException("PSBT does not contain a taproot keypath signature");
|
||||
}
|
||||
|
||||
Transaction finalizeTransaction = new Transaction();
|
||||
TransactionWitness witness = new TransactionWitness(finalizeTransaction, signature);
|
||||
finalizeTransaction.addInput(Sha256Hash.ZERO_HASH, 0, new Script(new byte[0]), witness);
|
||||
|
||||
return SIMPLE_PREFIX + Base64.getEncoder().encodeToString(witness.toByteArray());
|
||||
}
|
||||
|
||||
public static String getBip322SignatureFromPsbt(ScriptType scriptType, PSBT signedPsbt, ECKey pubKey) {
|
||||
checkScriptType(scriptType);
|
||||
|
||||
PSBTInput psbtInput = signedPsbt.getPsbtInputs().getFirst();
|
||||
TransactionSignature signature = psbtInput.isTaproot() ? psbtInput.getTapKeyPathSignature() : psbtInput.getPartialSignature(pubKey);
|
||||
|
||||
if(signature == null) {
|
||||
throw new IllegalArgumentException("PSBT does not contain a signature");
|
||||
}
|
||||
|
||||
TransactionOutput utxoOutput = psbtInput.getWitnessUtxo();
|
||||
Transaction finalizeTransaction = new Transaction();
|
||||
Script scriptSig = scriptType.getScriptSig(PolicyType.SINGLE_HD, utxoOutput.getScript(), pubKey, signature);
|
||||
TransactionWitness witness = psbtInput.isTaproot() ? new TransactionWitness(finalizeTransaction, signature) : new TransactionWitness(finalizeTransaction, pubKey, signature);
|
||||
TransactionInput finalizedTxInput = finalizeTransaction.addInput(Sha256Hash.ZERO_HASH, 0, scriptSig, witness);
|
||||
TransactionInput finalizedTxInput = scriptType.addSpendingInput(finalizeTransaction, utxoOutput, pubKey, signature);
|
||||
|
||||
return SIMPLE_PREFIX + Base64.getEncoder().encodeToString(finalizedTxInput.getWitness().toByteArray());
|
||||
return Base64.getEncoder().encodeToString(finalizedTxInput.getWitness().toByteArray());
|
||||
}
|
||||
|
||||
public static boolean verifyMessageBip322(ScriptType scriptType, Address address, String message, String signatureBase64) throws SignatureException {
|
||||
checkScriptType(scriptType);
|
||||
|
||||
String trimmed = signatureBase64.trim();
|
||||
if(trimmed.isEmpty()) {
|
||||
if(signatureBase64.trim().isEmpty()) {
|
||||
throw new SignatureException("Provided signature is empty.");
|
||||
}
|
||||
|
||||
if(trimmed.startsWith(FULL_PREFIX) || trimmed.startsWith(FULL_POF_PREFIX)) {
|
||||
throw new SignatureException("Only the simple BIP322 signature variant is supported.");
|
||||
}
|
||||
|
||||
if(trimmed.startsWith(SIMPLE_PREFIX)) {
|
||||
trimmed = trimmed.substring(SIMPLE_PREFIX.length());
|
||||
}
|
||||
|
||||
byte[] signatureEncoded;
|
||||
try {
|
||||
signatureEncoded = Base64.getDecoder().decode(trimmed);
|
||||
signatureEncoded = Base64.getDecoder().decode(signatureBase64);
|
||||
} catch(IllegalArgumentException e) {
|
||||
throw new SignatureException("Could not decode base64 signature", e);
|
||||
}
|
||||
@ -150,17 +74,17 @@ public class Bip322 {
|
||||
}
|
||||
|
||||
if(scriptType == ScriptType.P2WPKH) {
|
||||
signature = witness.getSignatures().getFirst();
|
||||
signature = witness.getSignatures().get(0);
|
||||
if(witness.getPushes().size() <= 1) {
|
||||
throw new SignatureException("BIP322 simple signature for P2WPKH script type does not contain a pubkey.");
|
||||
}
|
||||
pubKey = ECKey.fromPublicOnly(witness.getPushes().get(1));
|
||||
|
||||
if(!address.equals(scriptType.getAddress(PolicyType.SINGLE_HD, pubKey))) {
|
||||
if(!address.equals(scriptType.getAddress(pubKey))) {
|
||||
throw new SignatureException("Provided address does not match pubkey in signature");
|
||||
}
|
||||
} else if(scriptType == ScriptType.P2TR) {
|
||||
signature = witness.getSignatures().getFirst();
|
||||
signature = witness.getSignatures().get(0);
|
||||
pubKey = P2TR.getPublicKeyFromScript(address.getOutputScript());
|
||||
} else {
|
||||
throw new SignatureException(scriptType + " addresses are not supported");
|
||||
@ -170,8 +94,8 @@ public class Bip322 {
|
||||
Transaction toSign = getBip322ToSign(toSpend);
|
||||
|
||||
PSBT psbt = new PSBT(toSign);
|
||||
PSBTInput psbtInput = psbt.getPsbtInputs().getFirst();
|
||||
psbtInput.setWitnessUtxo(toSpend.getOutputs().getFirst());
|
||||
PSBTInput psbtInput = psbt.getPsbtInputs().get(0);
|
||||
psbtInput.setWitnessUtxo(toSpend.getOutputs().get(0));
|
||||
psbtInput.setSigHash(SigHash.ALL);
|
||||
|
||||
if(scriptType == ScriptType.P2TR) {
|
||||
@ -190,7 +114,7 @@ public class Bip322 {
|
||||
}
|
||||
|
||||
private static void checkScriptType(ScriptType scriptType) {
|
||||
if(!scriptType.isAllowed(PolicyType.SINGLE_HD)) {
|
||||
if(!scriptType.isAllowed(PolicyType.SINGLE)) {
|
||||
throw new UnsupportedOperationException("Only singlesig addresses are currently supported");
|
||||
}
|
||||
|
||||
@ -217,7 +141,7 @@ public class Bip322 {
|
||||
scriptSigChunks.add(ScriptChunk.fromData(getBip322MessageHash(message)));
|
||||
Script scriptSig = new Script(scriptSigChunks);
|
||||
toSpend.addInput(Sha256Hash.ZERO_HASH, 0xFFFFFFFFL, scriptSig, new TransactionWitness(toSpend, Collections.emptyList()));
|
||||
toSpend.getInputs().getFirst().setSequenceNumber(0L);
|
||||
toSpend.getInputs().get(0).setSequenceNumber(0L);
|
||||
toSpend.addOutput(0L, address.getOutputScript());
|
||||
|
||||
return toSpend;
|
||||
@ -230,7 +154,7 @@ public class Bip322 {
|
||||
|
||||
TransactionWitness witness = new TransactionWitness(toSign);
|
||||
toSign.addInput(toSpend.getTxId(), 0L, new Script(new byte[0]), witness);
|
||||
toSign.getInputs().getFirst().setSequenceNumber(0L);
|
||||
toSign.getInputs().get(0).setSequenceNumber(0L);
|
||||
toSign.addOutput(0, new Script(List.of(ScriptChunk.fromOpcode(ScriptOpCodes.OP_RETURN))));
|
||||
|
||||
return toSign;
|
||||
|
||||
@ -30,7 +30,7 @@ public class ChildNumber {
|
||||
this.i = i;
|
||||
}
|
||||
|
||||
public static boolean hasHardenedBit(int a) {
|
||||
private static boolean hasHardenedBit(int a) {
|
||||
return (a & HARDENED_BIT) != 0;
|
||||
}
|
||||
|
||||
|
||||
@ -1,218 +0,0 @@
|
||||
package com.sparrowwallet.drongo.crypto;
|
||||
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import org.bouncycastle.math.ec.ECPoint;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* Implementation of BIP-374 Discrete Log Equality Proofs.
|
||||
*
|
||||
* This class provides methods to generate and verify zero-knowledge DLEQ proofs
|
||||
* that prove knowledge of a scalar a such that A = a⋅G and C = a⋅B without
|
||||
* revealing the value of a.
|
||||
*/
|
||||
public class DLEQProof {
|
||||
private static final String DLEQ_TAG_AUX = "BIP0374/aux";
|
||||
private static final String DLEQ_TAG_NONCE = "BIP0374/nonce";
|
||||
private static final String DLEQ_TAG_CHALLENGE = "BIP0374/challenge";
|
||||
|
||||
/**
|
||||
* Generate a DLEQ proof according to BIP-374.
|
||||
*
|
||||
* @param a The secret key (256-bit unsigned integer)
|
||||
* @param B The public key point on the curve
|
||||
* @param r Auxiliary random data (32 bytes)
|
||||
* @param G The generator point (if null, uses secp256k1 generator)
|
||||
* @param m Optional message (32 bytes or null)
|
||||
* @return The proof (64 bytes) or null if generation fails
|
||||
* @throws IllegalArgumentException if r is not 32 bytes or m is not 32 bytes (when provided)
|
||||
*/
|
||||
public static byte[] generateProof(BigInteger a, ECKey B, byte[] r, ECKey G, byte[] m) {
|
||||
if(r.length != 32) {
|
||||
throw new IllegalArgumentException("Auxiliary random data must be 32 bytes");
|
||||
}
|
||||
|
||||
// Fail if a = 0 or a >= n
|
||||
if(a.equals(BigInteger.ZERO) || a.compareTo(ECKey.CURVE.getN()) >= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Fail if is_infinite(B)
|
||||
if(B.getPubKeyPoint().isInfinity()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if(m != null && m.length != 32) {
|
||||
throw new IllegalArgumentException("Message must be 32 bytes");
|
||||
}
|
||||
|
||||
// Use secp256k1 generator if G is null
|
||||
if(G == null) {
|
||||
G = ECKey.fromPublicOnly(ECKey.CURVE.getG(), true);
|
||||
}
|
||||
|
||||
// Let A = a⋅G
|
||||
ECKey A = G.multiply(a, true);
|
||||
|
||||
// Let C = a⋅B
|
||||
ECKey C = B.multiply(a, true);
|
||||
|
||||
// Let t be the byte-wise xor of bytes(32, a) and hash_BIP0374/aux(r)
|
||||
byte[] aBytes = Utils.bigIntegerToBytes(a, 32);
|
||||
byte[] auxHash = Utils.taggedHash(DLEQ_TAG_AUX, r);
|
||||
byte[] t = Utils.xor(aBytes, auxHash);
|
||||
|
||||
// Let m' = m if m is provided, otherwise an empty byte array
|
||||
byte[] mPrime = (m == null) ? new byte[0] : m;
|
||||
|
||||
// Let rand = hash_BIP0374/nonce(t || cbytes(A) || cbytes(C) || m')
|
||||
ByteBuffer nonceBuffer = ByteBuffer.allocate(t.length + 33 + 33 + mPrime.length);
|
||||
nonceBuffer.put(t);
|
||||
nonceBuffer.put(A.getPubKey());
|
||||
nonceBuffer.put(C.getPubKey());
|
||||
nonceBuffer.put(mPrime);
|
||||
byte[] rand = Utils.taggedHash(DLEQ_TAG_NONCE, nonceBuffer.array());
|
||||
|
||||
// Let k = int(rand) mod n
|
||||
BigInteger k = new BigInteger(1, rand).mod(ECKey.CURVE.getN());
|
||||
|
||||
// Fail if k = 0
|
||||
if(k.equals(BigInteger.ZERO)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Let R1 = k⋅G
|
||||
ECKey R1 = G.multiply(k, true);
|
||||
|
||||
// Let R2 = k⋅B
|
||||
ECKey R2 = B.multiply(k, true);
|
||||
|
||||
// Let e = int(hash_BIP0374/challenge(...))
|
||||
BigInteger e = dleqChallenge(A, B, C, R1, R2, m, G);
|
||||
|
||||
// Let s = (k + e⋅a) mod n
|
||||
BigInteger s = k.add(e.multiply(a)).mod(ECKey.CURVE.getN());
|
||||
|
||||
// Let proof = bytes(32, e) || bytes(32, s)
|
||||
byte[] proof = new byte[64];
|
||||
byte[] eBytes = Utils.bigIntegerToBytes(e, 32);
|
||||
byte[] sBytes = Utils.bigIntegerToBytes(s, 32);
|
||||
System.arraycopy(eBytes, 0, proof, 0, 32);
|
||||
System.arraycopy(sBytes, 0, proof, 32, 32);
|
||||
|
||||
// If VerifyProof fails, abort
|
||||
if(!verifyProof(A, B, C, proof, G, m)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return proof;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a DLEQ proof according to BIP-374.
|
||||
*
|
||||
* @param A The public key of the secret key used in proof generation
|
||||
* @param B The public key used in proof generation
|
||||
* @param C The result of multiplying the secret and public keys (a⋅B)
|
||||
* @param proof The proof (64 bytes)
|
||||
* @param G The generator point (if null, uses secp256k1 generator)
|
||||
* @param m Optional message (32 bytes or null)
|
||||
* @return true if the proof is valid, false otherwise
|
||||
* @throws IllegalArgumentException if m is not 32 bytes (when provided)
|
||||
*/
|
||||
public static boolean verifyProof(ECKey A, ECKey B, ECKey C, byte[] proof, ECKey G, byte[] m) {
|
||||
// Fail if any of is_infinite(A), is_infinite(B), is_infinite(C), is_infinite(G)
|
||||
if(A.getPubKeyPoint().isInfinity() || B.getPubKeyPoint().isInfinity() ||
|
||||
C.getPubKeyPoint().isInfinity()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(proof.length != 64) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(m != null && m.length != 32) {
|
||||
throw new IllegalArgumentException("Message must be 32 bytes");
|
||||
}
|
||||
|
||||
// Use secp256k1 generator if G is null
|
||||
if(G == null) {
|
||||
G = ECKey.fromPublicOnly(ECKey.CURVE.getG(), true);
|
||||
}
|
||||
|
||||
if(G.getPubKeyPoint().isInfinity()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Let e = int(proof[0:32])
|
||||
byte[] eBytes = new byte[32];
|
||||
System.arraycopy(proof, 0, eBytes, 0, 32);
|
||||
BigInteger e = new BigInteger(1, eBytes);
|
||||
|
||||
// Let s = int(proof[32:64]); fail if s >= n
|
||||
byte[] sBytes = new byte[32];
|
||||
System.arraycopy(proof, 32, sBytes, 0, 32);
|
||||
BigInteger s = new BigInteger(1, sBytes);
|
||||
if(s.compareTo(ECKey.CURVE.getN()) >= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Let R1 = s⋅G - e⋅A
|
||||
ECPoint R1Point = G.getPubKeyPoint().multiply(s).add(A.getPubKeyPoint().multiply(e).negate()).normalize();
|
||||
|
||||
// Fail if is_infinite(R1)
|
||||
if(R1Point.isInfinity()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ECKey R1 = ECKey.fromPublicOnly(R1Point, true);
|
||||
|
||||
// Let R2 = s⋅B - e⋅C
|
||||
ECPoint R2Point = B.getPubKeyPoint().multiply(s).add(C.getPubKeyPoint().multiply(e).negate()).normalize();
|
||||
|
||||
// Fail if is_infinite(R2)
|
||||
if(R2Point.isInfinity()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ECKey R2 = ECKey.fromPublicOnly(R2Point, true);
|
||||
|
||||
// Fail if e ≠ int(hash_BIP0374/challenge(...))
|
||||
BigInteger eExpected = dleqChallenge(A, B, C, R1, R2, m, G);
|
||||
if(!e.equals(eExpected)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the DLEQ challenge hash according to BIP-374.
|
||||
*
|
||||
* @param A The public key A = a⋅G
|
||||
* @param B The public key B
|
||||
* @param C The shared secret C = a⋅B
|
||||
* @param R1 The first commitment R1 = k⋅G
|
||||
* @param R2 The second commitment R2 = k⋅B
|
||||
* @param m Optional message (32 bytes or null)
|
||||
* @param G The generator point
|
||||
* @return The challenge value e
|
||||
*/
|
||||
private static BigInteger dleqChallenge(ECKey A, ECKey B, ECKey C, ECKey R1, ECKey R2, byte[] m, ECKey G) {
|
||||
byte[] mPrime = (m == null) ? new byte[0] : m;
|
||||
|
||||
ByteBuffer challengeBuffer = ByteBuffer.allocate(33 + 33 + 33 + 33 + 33 + 33 + mPrime.length);
|
||||
challengeBuffer.put(A.getPubKey());
|
||||
challengeBuffer.put(B.getPubKey());
|
||||
challengeBuffer.put(C.getPubKey());
|
||||
challengeBuffer.put(G.getPubKey());
|
||||
challengeBuffer.put(R1.getPubKey());
|
||||
challengeBuffer.put(R2.getPubKey());
|
||||
challengeBuffer.put(mPrime);
|
||||
|
||||
byte[] hash = Utils.taggedHash(DLEQ_TAG_CHALLENGE, challengeBuffer.array());
|
||||
return new BigInteger(1, hash);
|
||||
}
|
||||
}
|
||||
@ -1,12 +1,13 @@
|
||||
package com.sparrowwallet.drongo.crypto;
|
||||
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
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.bitcoin.Secp256k1Context;
|
||||
import org.bitcoinj.secp.api.*;
|
||||
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;
|
||||
@ -131,15 +132,16 @@ public class ECDSASignature {
|
||||
* @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);
|
||||
if(!Secp256k1Context.isEnabled()) {
|
||||
throw new IllegalStateException("libsecp256k1 is not enabled");
|
||||
}
|
||||
|
||||
try(Secp256k1 secp = Secp256k1.get()) {
|
||||
byte[] sigBytes = Utils.concat(Utils.bigIntegerToBytes(r, 32), Utils.bigIntegerToBytes(s, 32));
|
||||
SignatureData sig = secp.ecdsaSignatureParseCompact(() -> sigBytes).get();
|
||||
return secp.ecdsaVerify(sig, data, secp.ecPubKeyParse(() -> pub).get()).get();
|
||||
} catch(Exception e) {
|
||||
log.error("Error verifying ecdsa", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,9 +2,8 @@ package com.sparrowwallet.drongo.crypto;
|
||||
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.protocol.*;
|
||||
import org.bitcoin.NativeSecp256k1;
|
||||
import org.bitcoin.NativeSecp256k1Util;
|
||||
import org.bitcoin.Secp256k1Context;
|
||||
import org.bitcoinj.secp.api.*;
|
||||
import org.bouncycastle.asn1.*;
|
||||
import org.bouncycastle.asn1.x9.X9ECParameters;
|
||||
import org.bouncycastle.asn1.x9.X9IntegerConverter;
|
||||
@ -280,11 +279,11 @@ public class ECKey {
|
||||
}
|
||||
|
||||
if(Secp256k1Context.isEnabled()) {
|
||||
try {
|
||||
byte[] pubKeyBytes = NativeSecp256k1.computePubkey(Utils.bigIntegerToBytes(privKey, 32), false);
|
||||
LazyECPoint lazyECPoint = new LazyECPoint(CURVE.getCurve(), pubKeyBytes);
|
||||
try(Secp256k1 secp = Secp256k1.get()) {
|
||||
P256k1PubKey pubkey = secp.ecPubKeyCreate(new BigIntegerP256k1PrivKey(privKey));
|
||||
LazyECPoint lazyECPoint = new LazyECPoint(CURVE.getCurve(), pubkey.getCompressed());
|
||||
return lazyECPoint.get();
|
||||
} catch(NativeSecp256k1Util.AssertFailException e) {
|
||||
} catch(Exception e) {
|
||||
log.error("Error computing public key from private", e);
|
||||
}
|
||||
}
|
||||
@ -326,68 +325,6 @@ public class ECKey {
|
||||
return pub.get();
|
||||
}
|
||||
|
||||
/** Returns true if the Y coordinate of the public key is odd. */
|
||||
public boolean hasOddYCoord() {
|
||||
return pub.hasOddYCoord();
|
||||
}
|
||||
|
||||
/** Multiply the public point by the provided private key */
|
||||
public ECKey multiply(BigInteger privKey) {
|
||||
return multiply(privKey, false);
|
||||
}
|
||||
|
||||
/** Multiply the public point by the provided private key */
|
||||
public ECKey multiply(BigInteger privKey, boolean compressed) {
|
||||
ECPoint point = pub.get().multiply(privKey);
|
||||
return ECKey.fromPublicOnly(point, compressed);
|
||||
}
|
||||
|
||||
/** Add to the public point by the provided public key */
|
||||
public ECKey add(ECKey pubKey) {
|
||||
return add(pubKey, false);
|
||||
}
|
||||
|
||||
/** Add to the public point by the provided public key */
|
||||
public ECKey add(ECKey pubKey, boolean compressed) {
|
||||
ECPoint point = pub.get().add(pubKey.getPubKeyPoint());
|
||||
return ECKey.fromPublicOnly(point, compressed);
|
||||
}
|
||||
|
||||
/** Negate the provided public key */
|
||||
public ECKey negate() {
|
||||
return ECKey.fromPublicOnly(getPubKeyPoint().negate().normalize(), isCompressed());
|
||||
}
|
||||
|
||||
/** Add to the private key by the provided private key using modular arithmetic */
|
||||
public ECKey addPrivate(ECKey privKey) {
|
||||
return addPrivate(privKey, true);
|
||||
}
|
||||
|
||||
/** Add to the private key by the provided private key using modular arithmetic */
|
||||
public ECKey addPrivate(ECKey privKey, boolean compressed) {
|
||||
if(this.priv == null || privKey.priv == null) {
|
||||
throw new IllegalStateException("Key did not contain a private key");
|
||||
}
|
||||
|
||||
return ECKey.fromPrivate(this.priv.add(privKey.priv).mod(CURVE.getN()), compressed);
|
||||
}
|
||||
|
||||
/** Negate the provided private key */
|
||||
public ECKey negatePrivate() {
|
||||
if(priv == null) {
|
||||
throw new IllegalStateException("Key did not contain a private key");
|
||||
}
|
||||
|
||||
BigInteger negatedPrivKey = CURVE.getN().subtract(priv);
|
||||
return ECKey.fromPrivate(negatedPrivKey, isCompressed());
|
||||
}
|
||||
|
||||
/** Calculate the value of the public key point modulo the secp256k1 curve order */
|
||||
public BigInteger moduloCurveOrder() {
|
||||
BigInteger xCoordinate = pub.get().normalize().getAffineXCoord().toBigInteger();
|
||||
return xCoordinate.mod(CURVE_PARAMS.getCurve().getOrder());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the private key in the form of an integer field element. The public key is derived by performing EC
|
||||
* point addition this number of times (i.e. point multiplying).
|
||||
@ -447,16 +384,34 @@ public class ECKey {
|
||||
throw new IllegalArgumentException("Private key cannot be null");
|
||||
}
|
||||
|
||||
ECDSASignature signature;
|
||||
if(!Secp256k1Context.isEnabled()) {
|
||||
throw new IllegalStateException("libsecp256k1 is not enabled");
|
||||
}
|
||||
|
||||
ECDSASignature signature = null;
|
||||
Integer counter = null;
|
||||
do {
|
||||
ECDSASigner signer = new ECDSASigner(new HMacDSANonceKCalculator(new SHA256Digest(), counter));
|
||||
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 == null ? 1 : counter+1);
|
||||
} while(!signature.hasLowR());
|
||||
try(Secp256k1 secp = Secp256k1.get()) {
|
||||
do {
|
||||
ECDSASigner signer = new ECDSASigner(new HMacDSANonceKCalculator(new SHA256Digest(), counter));
|
||||
ECPrivateKeyParameters privKey = new ECPrivateKeyParameters(priv, CURVE);
|
||||
signer.init(true, privKey);
|
||||
BigInteger[] components = signer.generateSignature(input.getBytes());
|
||||
signature = new ECDSASignature(components[0], components[1]).toCanonicalised();
|
||||
|
||||
//No way to specify counter as a parameter while grinding for low R
|
||||
SignatureData sig = secp.ecdsaSign(input.getBytes(), new BigIntegerP256k1PrivKey(priv)).get();
|
||||
CompressedSignatureData serialized_signature = secp.ecdsaSignatureSerializeCompact(sig).get();
|
||||
BigInteger r = new BigInteger(1, Arrays.copyOfRange(serialized_signature.bytes(), 0, 32));
|
||||
BigInteger s = new BigInteger(1, Arrays.copyOfRange(serialized_signature.bytes(),32, 64));
|
||||
ECDSASignature signature2 = new ECDSASignature(r, s);
|
||||
if(!signature.equals(signature2)) {
|
||||
System.out.println("Signatures not equal " + signature.hasLowR() + " " + signature2.hasLowR() + " " + counter);
|
||||
}
|
||||
counter = (counter == null ? 1 : counter + 1);
|
||||
} while(!signature.hasLowR());
|
||||
} catch(Exception e) {
|
||||
log.error("Error signing ecdsa", e);
|
||||
}
|
||||
|
||||
return signature;
|
||||
}
|
||||
@ -473,10 +428,10 @@ public class ECKey {
|
||||
throw new IllegalStateException("libsecp256k1 is not enabled");
|
||||
}
|
||||
|
||||
try {
|
||||
byte[] sigBytes = NativeSecp256k1.schnorrSign(input.getBytes(), Utils.bigIntegerToBytes(priv, 32), new byte[32]);
|
||||
try(Secp256k1 secp = Secp256k1.get()) {
|
||||
byte[] sigBytes = secp.schnorrSigSign32(input.getBytes(), secp.ecKeyPairCreate(new BigIntegerP256k1PrivKey(priv)));
|
||||
return SchnorrSignature.decode(sigBytes);
|
||||
} catch(NativeSecp256k1Util.AssertFailException e) {
|
||||
} catch(Exception e) {
|
||||
log.error("Error signing schnorr", e);
|
||||
}
|
||||
|
||||
@ -497,20 +452,53 @@ public class ECKey {
|
||||
return verify(sigHash.getBytes(), signature);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tweak the key for use as a Taproot output key (without script path) as defined in BIP341
|
||||
*/
|
||||
public ECKey getTweakedOutputKey() {
|
||||
ECKey key = this;
|
||||
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(pub.hasOddYCoord()) {
|
||||
key = hasPrivKey() ? key.negatePrivate() : key.negate();
|
||||
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);
|
||||
}
|
||||
|
||||
byte[] tweakHash = Utils.taggedHash("TapTweak", key.getPubKeyXCoord());
|
||||
ECKey tweakKey = ECKey.fromPrivate(tweakHash);
|
||||
return ECKey.fromPublicOnly(outputKey, true);
|
||||
}
|
||||
|
||||
return hasPrivKey() ? key.addPrivate(tweakKey, true) : key.add(tweakKey, true);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -912,4 +900,22 @@ public class ECKey {
|
||||
public interface ECDSAHashSigner {
|
||||
ECDSASignature sign(Sha256Hash hash);
|
||||
}
|
||||
|
||||
private static class BigIntegerP256k1PrivKey implements P256k1PrivKey {
|
||||
private final BigInteger privKey;
|
||||
|
||||
public BigIntegerP256k1PrivKey(BigInteger privKey) {
|
||||
this.privKey = privKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getEncoded() {
|
||||
return Utils.bigIntegerToBytes(privKey, 32);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -68,10 +68,6 @@ public class LazyECPoint {
|
||||
return xcoord;
|
||||
}
|
||||
|
||||
public boolean hasOddYCoord() {
|
||||
return get().normalize().getYCoord().toBigInteger().testBit(0);
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return Hex.toHexString(getEncoded());
|
||||
}
|
||||
|
||||
@ -2,9 +2,10 @@ 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.bitcoinj.secp.api.P256K1XOnlyPubKey;
|
||||
import org.bitcoinj.secp.api.Result;
|
||||
import org.bitcoinj.secp.api.Secp256k1;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@ -70,9 +71,9 @@ public class SchnorrSignature {
|
||||
throw new IllegalStateException("libsecp256k1 is not enabled");
|
||||
}
|
||||
|
||||
try {
|
||||
return NativeSecp256k1.schnorrVerify(encode(), data, pub);
|
||||
} catch(NativeSecp256k1Util.AssertFailException e) {
|
||||
try(Secp256k1 secp = Secp256k1.get()) {
|
||||
return secp.schnorrSigVerify(encode(), data, new ByteArrayP256K1XOnlyPubKey(pub)).get();
|
||||
} catch(Exception e) {
|
||||
log.error("Error verifying schnorr signature", e);
|
||||
}
|
||||
|
||||
@ -95,4 +96,22 @@ public class SchnorrSignature {
|
||||
public int hashCode() {
|
||||
return Objects.hash(r, s);
|
||||
}
|
||||
|
||||
private static class ByteArrayP256K1XOnlyPubKey implements P256K1XOnlyPubKey {
|
||||
private final byte[] pub;
|
||||
|
||||
public ByteArrayP256K1XOnlyPubKey(byte[] pub) {
|
||||
this.pub = pub;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BigInteger getX() {
|
||||
return new BigInteger(1, pub);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getSerialized() {
|
||||
return pub;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,39 +0,0 @@
|
||||
package com.sparrowwallet.drongo.crypto;
|
||||
|
||||
import org.bouncycastle.asn1.x9.ECNamedCurveTable;
|
||||
import org.bouncycastle.asn1.x9.X9ECParameters;
|
||||
import org.bouncycastle.crypto.params.ECDomainParameters;
|
||||
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
|
||||
import org.bouncycastle.crypto.signers.ECDSASigner;
|
||||
import org.bouncycastle.math.ec.ECPoint;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
public class Secp256r1Key {
|
||||
private static final X9ECParameters CURVE_PARAMS = ECNamedCurveTable.getByName("P-256");
|
||||
|
||||
public static final ECDomainParameters CURVE;
|
||||
|
||||
private final ECPoint point;
|
||||
|
||||
static {
|
||||
CURVE = new ECDomainParameters(CURVE_PARAMS.getCurve(), CURVE_PARAMS.getG(), CURVE_PARAMS.getN(), CURVE_PARAMS.getH());
|
||||
}
|
||||
|
||||
public Secp256r1Key(byte[] publicKeyBytes) {
|
||||
this.point = CURVE.getCurve().decodePoint(publicKeyBytes);
|
||||
}
|
||||
|
||||
public boolean verify(byte[] challenge, byte[] challengeSignature) {
|
||||
ECDSASigner signer = new ECDSASigner();
|
||||
signer.init(false, new ECPublicKeyParameters(point, CURVE));
|
||||
|
||||
int halfLength = challengeSignature.length / 2;
|
||||
byte[] r = new byte[halfLength];
|
||||
byte[] s = new byte[halfLength];
|
||||
System.arraycopy(challengeSignature, 0, r, 0, halfLength);
|
||||
System.arraycopy(challengeSignature, halfLength, s, 0, halfLength);
|
||||
|
||||
return signer.verifySignature(challenge, new BigInteger(1, r), new BigInteger(1, s));
|
||||
}
|
||||
}
|
||||
@ -1,128 +0,0 @@
|
||||
package com.sparrowwallet.drongo.crypto;
|
||||
|
||||
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
|
||||
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
|
||||
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
|
||||
import org.bouncycastle.crypto.params.X25519PrivateKeyParameters;
|
||||
import org.bouncycastle.crypto.params.X25519PublicKeyParameters;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.security.*;
|
||||
import java.security.interfaces.XECPrivateKey;
|
||||
import java.security.interfaces.XECPublicKey;
|
||||
import java.security.spec.AlgorithmParameterSpec;
|
||||
import java.util.Optional;
|
||||
|
||||
public class X25519Key {
|
||||
private final KeyPair keyPair;
|
||||
private final AlgorithmParameterSpec ecSpec;
|
||||
|
||||
public X25519Key() {
|
||||
this(generatePrivateKey());
|
||||
}
|
||||
|
||||
public X25519Key(byte[] priv) {
|
||||
try {
|
||||
final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("X25519");
|
||||
this.ecSpec = keyPairGenerator.generateKeyPair().getPrivate().getParams();
|
||||
} catch(NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
X25519PrivateKeyParameters privateKeyParams = new X25519PrivateKeyParameters(priv, 0);
|
||||
X25519PublicKeyParameters publicKeyParams = privateKeyParams.generatePublicKey();
|
||||
|
||||
PrivateKey privateKey = new BouncyCastlePrivateKey(privateKeyParams);
|
||||
PublicKey publicKey = new BouncyCastlePublicKey(publicKeyParams);
|
||||
this.keyPair = new KeyPair(publicKey, privateKey);
|
||||
}
|
||||
|
||||
public KeyPair getKeyPair() {
|
||||
return keyPair;
|
||||
}
|
||||
|
||||
public byte[] getRawPrivateKeyBytes() {
|
||||
return keyPair.getPrivate().getEncoded();
|
||||
}
|
||||
|
||||
private static byte[] generatePrivateKey() {
|
||||
SecureRandom secureRandom = new SecureRandom();
|
||||
byte[] privateKey = new byte[32];
|
||||
secureRandom.nextBytes(privateKey);
|
||||
return privateKey;
|
||||
}
|
||||
|
||||
public class BouncyCastlePrivateKey implements XECPrivateKey {
|
||||
private final X25519PrivateKeyParameters privateKeyParams;
|
||||
|
||||
BouncyCastlePrivateKey(X25519PrivateKeyParameters privateKeyParams) {
|
||||
this.privateKeyParams = privateKeyParams;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAlgorithm() {
|
||||
return "X25519";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFormat() {
|
||||
return "RAW";
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getEncoded() {
|
||||
return privateKeyParams.getEncoded();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<byte[]> getScalar() {
|
||||
return Optional.of(getEncoded());
|
||||
}
|
||||
|
||||
@Override
|
||||
public AlgorithmParameterSpec getParams() {
|
||||
return ecSpec;
|
||||
}
|
||||
}
|
||||
|
||||
public class BouncyCastlePublicKey implements XECPublicKey {
|
||||
private final X25519PublicKeyParameters publicKeyParams;
|
||||
|
||||
BouncyCastlePublicKey(X25519PublicKeyParameters publicKeyParams) {
|
||||
this.publicKeyParams = publicKeyParams;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAlgorithm() {
|
||||
return "X25519";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFormat() {
|
||||
return "X.509";
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getEncoded() {
|
||||
try {
|
||||
ASN1ObjectIdentifier algOid = new ASN1ObjectIdentifier("1.3.101.110");
|
||||
AlgorithmIdentifier algId = new AlgorithmIdentifier(algOid);
|
||||
SubjectPublicKeyInfo spki = new SubjectPublicKeyInfo(algId, publicKeyParams.getEncoded());
|
||||
return spki.getEncoded();
|
||||
} catch(IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public BigInteger getU() {
|
||||
return new BigInteger(1, publicKeyParams.getEncoded());
|
||||
}
|
||||
|
||||
@Override
|
||||
public AlgorithmParameterSpec getParams() {
|
||||
return ecSpec;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,59 +0,0 @@
|
||||
package com.sparrowwallet.drongo.dns;
|
||||
|
||||
import org.xbill.DNS.*;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
public class AuthenticatingResolver implements Resolver {
|
||||
private final Resolver delegate;
|
||||
private boolean authenticated;
|
||||
|
||||
public AuthenticatingResolver(Resolver delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPort(int port) {
|
||||
delegate.setPort(port);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTCP(boolean flag) {
|
||||
delegate.setTCP(flag);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setIgnoreTruncation(boolean flag) {
|
||||
delegate.setIgnoreTruncation(flag);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEDNS(int version, int payloadSize, int flags, List<EDNSOption> options) {
|
||||
delegate.setEDNS(version, payloadSize, flags, options);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTSIGKey(TSIG key) {
|
||||
delegate.setTSIGKey(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTimeout(Duration timeout) {
|
||||
delegate.setTimeout(timeout);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletionStage<Message> sendAsync(Message query, Executor executor) {
|
||||
return delegate.sendAsync(query, executor).thenApply(response -> {
|
||||
this.authenticated = response.getHeader().getFlag(Flags.AD);
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
public boolean isAuthenticated() {
|
||||
return authenticated;
|
||||
}
|
||||
}
|
||||
@ -1,40 +0,0 @@
|
||||
package com.sparrowwallet.drongo.dns;
|
||||
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class DnsAddress {
|
||||
private final Address address;
|
||||
private final SilentPaymentAddress silentPaymentAddress;
|
||||
|
||||
public DnsAddress(Address address) {
|
||||
this.address = address;
|
||||
this.silentPaymentAddress = null;
|
||||
}
|
||||
|
||||
public DnsAddress(SilentPaymentAddress silentPaymentAddress) {
|
||||
this.address = null;
|
||||
this.silentPaymentAddress = silentPaymentAddress;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean equals(Object o) {
|
||||
if(this == o) {
|
||||
return true;
|
||||
}
|
||||
if(!(o instanceof DnsAddress that)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Objects.equals(address, that.address) && Objects.equals(silentPaymentAddress, that.silentPaymentAddress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = Objects.hashCode(address);
|
||||
result = 31 * result + Objects.hashCode(silentPaymentAddress);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@ -1,58 +0,0 @@
|
||||
package com.sparrowwallet.drongo.dns;
|
||||
|
||||
import com.sparrowwallet.drongo.uri.BitcoinURI;
|
||||
import org.xbill.DNS.*;
|
||||
import org.xbill.DNS.Record;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Optional;
|
||||
|
||||
import static com.sparrowwallet.drongo.dns.RecordUtils.fromWire;
|
||||
|
||||
public record DnsPayment(String hrn, BitcoinURI bitcoinURI, byte[] proofChain) {
|
||||
public String toString() {
|
||||
return "₿" + hrn;
|
||||
}
|
||||
|
||||
public long getTTL() {
|
||||
long ttl = DnsPaymentCache.MAX_TTL_SECONDS;
|
||||
DNSInput in = new DNSInput(proofChain);
|
||||
while(in.remaining() > 0) {
|
||||
try {
|
||||
Record record = fromWire(in, Section.ANSWER, false);
|
||||
ttl = Math.min(ttl, record.getTTL());
|
||||
} catch(WireParseException e) {
|
||||
//ignore
|
||||
}
|
||||
}
|
||||
|
||||
return ttl;
|
||||
}
|
||||
|
||||
public boolean hasAddress() {
|
||||
return bitcoinURI.getAddress() != null;
|
||||
}
|
||||
|
||||
public boolean hasSilentPaymentAddress() {
|
||||
return bitcoinURI.getSilentPaymentAddress() != null;
|
||||
}
|
||||
|
||||
public static Optional<String> getHrn(String value) {
|
||||
String hrn = value;
|
||||
if(value.endsWith(".")) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
if(hrn.startsWith("₿")) {
|
||||
hrn = hrn.substring(1);
|
||||
}
|
||||
|
||||
String[] addressParts = hrn.split("@");
|
||||
if(addressParts.length == 2 && addressParts[1].indexOf('.') > -1 && addressParts[1].substring(addressParts[1].indexOf('.') + 1).length() > 1 &&
|
||||
StandardCharsets.US_ASCII.newEncoder().canEncode(hrn)) {
|
||||
return Optional.of(hrn);
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
@ -1,69 +0,0 @@
|
||||
package com.sparrowwallet.drongo.dns;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import com.github.benmanes.caffeine.cache.Expiry;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.silentpayments.SilentPayment;
|
||||
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
|
||||
import com.sparrowwallet.drongo.wallet.Payment;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class DnsPaymentCache {
|
||||
public static final long MAX_TTL_SECONDS = 604800L;
|
||||
public static final long MIN_TTL_SECONDS = 1800L;
|
||||
|
||||
private static final Cache<DnsAddress, DnsPayment> dnsPayments = Caffeine.newBuilder().expireAfter(new Expiry<DnsAddress, DnsPayment>() {
|
||||
@Override
|
||||
public long expireAfterCreate(DnsAddress address, DnsPayment dnsPayment, long currentTime) {
|
||||
return TimeUnit.SECONDS.toNanos(Math.max(dnsPayment.getTTL(), MIN_TTL_SECONDS));
|
||||
}
|
||||
|
||||
@Override
|
||||
public long expireAfterUpdate(DnsAddress address, DnsPayment dnsPayment, long currentTime, long currentDuration) {
|
||||
return expireAfterCreate(address, dnsPayment, currentTime);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long expireAfterRead(DnsAddress address, DnsPayment dnsPayment, long currentTime, long currentDuration) {
|
||||
return currentDuration;
|
||||
}
|
||||
}).build();
|
||||
|
||||
private DnsPaymentCache() {}
|
||||
|
||||
public static DnsPayment getDnsPayment(Address address) {
|
||||
return dnsPayments.getIfPresent(new DnsAddress(address));
|
||||
}
|
||||
|
||||
public static DnsPayment getDnsPayment(SilentPaymentAddress silentPaymentAddress) {
|
||||
return dnsPayments.getIfPresent(new DnsAddress(silentPaymentAddress));
|
||||
}
|
||||
|
||||
public static DnsPayment getDnsPayment(Payment payment) {
|
||||
if(payment instanceof SilentPayment silentPayment) {
|
||||
return dnsPayments.getIfPresent(new DnsAddress(silentPayment.getSilentPaymentAddress()));
|
||||
} else {
|
||||
return dnsPayments.getIfPresent(new DnsAddress(payment.getAddress()));
|
||||
}
|
||||
}
|
||||
|
||||
public static DnsPayment getDnsPayment(String hrn) {
|
||||
for(DnsPayment dnsPayment : dnsPayments.asMap().values()) {
|
||||
if(dnsPayment.hrn().equals(hrn)) {
|
||||
return dnsPayment;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void putDnsPayment(Address address, DnsPayment dnsPayment) {
|
||||
dnsPayments.put(new DnsAddress(address), dnsPayment);
|
||||
}
|
||||
|
||||
public static void putDnsPayment(SilentPaymentAddress silentPaymentAddress, DnsPayment dnsPayment) {
|
||||
dnsPayments.put(new DnsAddress(silentPaymentAddress), dnsPayment);
|
||||
}
|
||||
}
|
||||
@ -1,193 +0,0 @@
|
||||
package com.sparrowwallet.drongo.dns;
|
||||
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.uri.BitcoinURI;
|
||||
import com.sparrowwallet.drongo.uri.BitcoinURIParseException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.xbill.DNS.*;
|
||||
import org.xbill.DNS.Record;
|
||||
import org.xbill.DNS.dnssec.ValidatingResolver;
|
||||
import org.xbill.DNS.lookup.LookupResult;
|
||||
import org.xbill.DNS.lookup.LookupSession;
|
||||
import org.xbill.DNS.lookup.NoSuchDomainException;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import static com.sparrowwallet.drongo.uri.BitcoinURI.BITCOIN_SCHEME;
|
||||
|
||||
public class DnsPaymentResolver {
|
||||
private static final Logger log = LoggerFactory.getLogger(DnsPaymentResolver.class);
|
||||
|
||||
private static final String BITCOIN_URI_PREFIX = BITCOIN_SCHEME + ":";
|
||||
private static final String DEFAULT_RESOLVER_IP_ADDRESS = "8.8.8.8";
|
||||
|
||||
static String ROOT = ". IN DS 20326 8 2 E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D\n" +
|
||||
". IN DS 38696 8 2 683D2D0ACB8C9B712A1948B27F741219298D0A450D612C483AF444A4C0FB2B16";
|
||||
|
||||
private final String hrn;
|
||||
private final String domain;
|
||||
private final Clock clock;
|
||||
|
||||
public DnsPaymentResolver(String hrn) {
|
||||
this(hrn, Clock.systemUTC());
|
||||
}
|
||||
|
||||
public DnsPaymentResolver(String hrn, Clock clock) {
|
||||
if(!StandardCharsets.US_ASCII.newEncoder().canEncode(hrn)) {
|
||||
throw new IllegalArgumentException("Invalid HRN containing non-ASCII characters: " + hrn);
|
||||
}
|
||||
this.hrn = hrn;
|
||||
String[] parts = hrn.split("@");
|
||||
if(parts.length != 2) {
|
||||
throw new IllegalArgumentException("Invalid HRN: " + hrn);
|
||||
}
|
||||
this.domain = parts[0] + ".user._bitcoin-payment." + parts[1];
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
public Optional<DnsPayment> resolve() throws IOException, DnsPaymentValidationException, BitcoinURIParseException, ExecutionException, InterruptedException {
|
||||
return resolve(DEFAULT_RESOLVER_IP_ADDRESS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs online resolution of the BIP 353 HRN via the configured resolver
|
||||
*
|
||||
* @param resolverIpAddress the IP address of the resolver to use for the DNS lookup
|
||||
* @return The DNS payment instruction, if present
|
||||
* @throws IOException Thrown for a general I/O error
|
||||
* @throws DnsPaymentValidationException Thrown for a DNSSEC or BIP 353 validation failure
|
||||
* @throws BitcoinURIParseException Thrown for an invalid BIP 21 URI
|
||||
*/
|
||||
public Optional<DnsPayment> resolve(String resolverIpAddress) throws IOException, DnsPaymentValidationException, BitcoinURIParseException, ExecutionException, InterruptedException {
|
||||
log.debug("Resolving payment record for: " + domain);
|
||||
|
||||
PersistingResolver persistingResolver = new PersistingResolver(resolverIpAddress);
|
||||
ValidatingResolver validatingResolver = new ValidatingResolver(persistingResolver, clock);
|
||||
validatingResolver.loadTrustAnchors(new ByteArrayInputStream(ROOT.getBytes(StandardCharsets.US_ASCII)));
|
||||
validatingResolver.setEDNS(0, 0, ExtendedFlags.DO);
|
||||
AuthenticatingResolver authenticatingResolver = new AuthenticatingResolver(validatingResolver);
|
||||
|
||||
try {
|
||||
LookupSession lookupSession = LookupSession.builder().resolver(authenticatingResolver).build();
|
||||
LookupResult result = lookupSession.lookupAsync(getName(), Type.TXT, DClass.IN).toCompletableFuture().get();
|
||||
if(result.getRecords().isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
String strBitcoinUri = getBitcoinURI(result.getRecords());
|
||||
if(strBitcoinUri.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
BitcoinURI bitcoinURI = new BitcoinURI(strBitcoinUri);
|
||||
validateResponse(authenticatingResolver, new ArrayList<>(persistingResolver.getChain()));
|
||||
|
||||
byte[] proofChain = persistingResolver.chainToWire();
|
||||
log.debug("Resolved " + hrn + " with proof " + Utils.bytesToHex(proofChain));
|
||||
|
||||
return Optional.of(new DnsPayment(hrn, bitcoinURI, proofChain));
|
||||
} catch(ExecutionException e) {
|
||||
if(e.getCause() instanceof NoSuchDomainException) {
|
||||
return Optional.empty();
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs offline resolution of the BIP 353 HRN via the provided authentication chain
|
||||
*
|
||||
* @param proofChain authentication chain of unsorted DNS records in wire format
|
||||
* @return The DNS payment instruction, if present
|
||||
* @throws IOException Thrown for a general I/O error
|
||||
* @throws DnsPaymentValidationException Thrown for a DNSSEC or BIP 353 validation failure
|
||||
* @throws BitcoinURIParseException Thrown for an invalid BIP 21 URI
|
||||
*/
|
||||
public Optional<DnsPayment> resolve(byte[] proofChain) throws IOException, DnsPaymentValidationException, BitcoinURIParseException, ExecutionException, InterruptedException {
|
||||
OfflineResolver offlineResolver = new OfflineResolver(proofChain);
|
||||
ValidatingResolver offlineValidatingResolver = new ValidatingResolver(offlineResolver, clock);
|
||||
offlineValidatingResolver.loadTrustAnchors(new ByteArrayInputStream(ROOT.getBytes(StandardCharsets.US_ASCII)));
|
||||
AuthenticatingResolver authenticatingResolver = new AuthenticatingResolver(offlineValidatingResolver);
|
||||
|
||||
Instant now = clock.instant();
|
||||
Instant oneHourAgo = now.minusSeconds(3600);
|
||||
for(Record record : offlineResolver.getCachedSigs()) {
|
||||
if(record instanceof RRSIGRecord rrsig) {
|
||||
if(rrsig.getTimeSigned().isAfter(now)) {
|
||||
throw new DnsPaymentValidationException("Invalid RRSIG record signed in the future");
|
||||
} else if(rrsig.getExpire().isBefore(oneHourAgo)) {
|
||||
throw new DnsPaymentValidationException("Invalid RRSIG record expired earlier than 1 hour ago");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
LookupSession lookupSession = LookupSession.builder().resolver(authenticatingResolver).build();
|
||||
LookupResult result = lookupSession.lookupAsync(getName(), Type.TXT, DClass.IN).toCompletableFuture().get();
|
||||
if(result.getRecords().isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
String strBitcoinUri = getBitcoinURI(result.getRecords());
|
||||
if(strBitcoinUri.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
BitcoinURI bitcoinURI = new BitcoinURI(strBitcoinUri);
|
||||
validateResponse(authenticatingResolver, offlineResolver.getRecords());
|
||||
|
||||
return Optional.of(new DnsPayment(hrn, bitcoinURI, proofChain));
|
||||
} catch(ExecutionException e) {
|
||||
if(e.getCause() instanceof NoSuchDomainException) {
|
||||
return Optional.empty();
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Name getName() throws TextParseException {
|
||||
return Name.fromString(domain + ".");
|
||||
}
|
||||
|
||||
private void validateResponse(AuthenticatingResolver resolver, List<Record> records) throws DnsPaymentValidationException {
|
||||
boolean isValidated = resolver.isAuthenticated();
|
||||
if(!isValidated) {
|
||||
throw new DnsPaymentValidationException("DNSSEC validation failed, could not authenticate the payment instruction");
|
||||
}
|
||||
|
||||
Map<Record, String> securityWarnings = RecordUtils.checkSecurityConstraints(records);
|
||||
if(!securityWarnings.isEmpty()) {
|
||||
Optional<String> optWarning = securityWarnings.entrySet().stream().map(e -> e.getKey().getName() + ": " + e.getValue()).reduce((a, b) -> a + "\n" + b);
|
||||
throw new DnsPaymentValidationException("DNSSEC validation failed with the following errors:\n" + optWarning.get());
|
||||
}
|
||||
}
|
||||
|
||||
private String getBitcoinURI(List<Record> answers) throws DnsPaymentValidationException {
|
||||
StringBuilder uriBuilder = new StringBuilder();
|
||||
for(Record record : answers) {
|
||||
if(record.getType() == Type.TXT) {
|
||||
TXTRecord txt = (TXTRecord)record;
|
||||
List<String> strings = txt.getStrings();
|
||||
log.debug("Found TXT records for " + domain + ": " + strings);
|
||||
if(strings.isEmpty() || !strings.getFirst().startsWith(BITCOIN_URI_PREFIX)) {
|
||||
continue;
|
||||
}
|
||||
if(strings.getFirst().startsWith(BITCOIN_URI_PREFIX) && !uriBuilder.isEmpty()) {
|
||||
throw new DnsPaymentValidationException("Multiple TXT records found starting with " + BITCOIN_URI_PREFIX);
|
||||
}
|
||||
for(String s : strings) {
|
||||
uriBuilder.append(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return uriBuilder.toString();
|
||||
}
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
package com.sparrowwallet.drongo.dns;
|
||||
|
||||
public class DnsPaymentValidationException extends Exception {
|
||||
public DnsPaymentValidationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@ -1,185 +0,0 @@
|
||||
package com.sparrowwallet.drongo.dns;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import org.xbill.DNS.*;
|
||||
import org.xbill.DNS.Record;
|
||||
|
||||
import static com.sparrowwallet.drongo.dns.RecordUtils.fromWire;
|
||||
|
||||
public class OfflineResolver implements Resolver {
|
||||
private final List<Record> cachedRrs = new ArrayList<>();
|
||||
private final List<RRSIGRecord> cachedSigs = new ArrayList<>();
|
||||
|
||||
public OfflineResolver(byte[] chain) throws WireParseException {
|
||||
DNSInput in = new DNSInput(chain);
|
||||
while(in.remaining() > 0) {
|
||||
Record record = fromWire(in, Section.ANSWER, false);
|
||||
if(record instanceof RRSIGRecord rrsig) {
|
||||
cachedSigs.add(rrsig);
|
||||
} else {
|
||||
cachedRrs.add(record);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPort(int port) {
|
||||
throw new UnsupportedOperationException("Unsupported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTCP(boolean flag) {
|
||||
throw new UnsupportedOperationException("Unsupported");
|
||||
}
|
||||
|
||||
// No-op
|
||||
@Override
|
||||
public void setIgnoreTruncation(boolean flag) {}
|
||||
|
||||
// No-op
|
||||
@Override
|
||||
public void setEDNS(int level, int payloadSize, int flags, List<EDNSOption> options) {}
|
||||
|
||||
@Override
|
||||
public void setTSIGKey(TSIG key) {
|
||||
throw new UnsupportedOperationException("Unsupported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTimeout(Duration timeout) {
|
||||
throw new UnsupportedOperationException("Unsupported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletionStage<Message> sendAsync(Message query, Executor executor) {
|
||||
Message response = makeNoErrorResponse(query);
|
||||
addRecords(query.getQuestion(), response);
|
||||
|
||||
if(response.getSection(Section.ANSWER).isEmpty() && response.getSection(Section.AUTHORITY).isEmpty()) {
|
||||
response = makeNoDomainResponse(query);
|
||||
}
|
||||
|
||||
return CompletableFuture.completedFuture(response);
|
||||
}
|
||||
|
||||
private void addRecords(Record question, Message response) {
|
||||
Name name = question.getName();
|
||||
|
||||
RRset cnameSet = getRRSet(name, Type.CNAME, question.getDClass());
|
||||
addRRSetToMessage(response, cnameSet);
|
||||
|
||||
if(!cnameSet.rrs().isEmpty() && cnameSet.rrs().getFirst() instanceof CNAMERecord cnameRecord) {
|
||||
name = cnameRecord.getTarget();
|
||||
}
|
||||
|
||||
RRset answerSet = getRRSet(name, question.getType(), question.getDClass());
|
||||
addRRSetToMessage(response, answerSet);
|
||||
}
|
||||
|
||||
private void addRRSetToMessage(Message response, RRset rrset) {
|
||||
rrset.rrs().stream().forEach(it -> response.addRecord(it, Section.ANSWER));
|
||||
rrset.sigs().stream().forEach(it -> response.addRecord(it, Section.ANSWER));
|
||||
|
||||
if(!rrset.sigs().isEmpty()) {
|
||||
Name wildcard = RecordUtils.rrsetWildcard(rrset);
|
||||
if(wildcard != null) {
|
||||
RRset nsecRRset = getNSecRRSetForWildcard(wildcard);
|
||||
nsecRRset.rrs().stream().forEach(it -> response.addRecord(it, Section.AUTHORITY));
|
||||
nsecRRset.sigs().stream().forEach(it -> response.addRecord(it, Section.AUTHORITY));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private RRset getRRSet(Name name, int type, int dclass) {
|
||||
RRset rrset = new RRset();
|
||||
for(Record it : cachedRrs) {
|
||||
if(it.getName().equals(name) && it.getType() == type && it.getDClass() == dclass) {
|
||||
rrset.addRR(it);
|
||||
}
|
||||
}
|
||||
|
||||
for(RRSIGRecord it : cachedSigs) {
|
||||
if(it.getName().equals(name) && it.getTypeCovered() == type && it.getDClass() == dclass) {
|
||||
rrset.addRR(it);
|
||||
}
|
||||
}
|
||||
|
||||
return rrset;
|
||||
}
|
||||
|
||||
private RRset getNSecRRSetForWildcard(Name wildcard) {
|
||||
RRset rrset = new RRset();
|
||||
|
||||
for(Record it : cachedRrs) {
|
||||
if((it.getType() == Type.NSEC || it.getType() == Type.NSEC3) && RecordUtils.longestCommonName(it.getName(), wildcard) != Name.root) {
|
||||
rrset.addRR(it);
|
||||
}
|
||||
}
|
||||
|
||||
for(RRSIGRecord it : cachedSigs) {
|
||||
if((it.getTypeCovered() == Type.NSEC || it.getTypeCovered() == Type.NSEC3) && RecordUtils.longestCommonName(it.getName(), wildcard) != Name.root) {
|
||||
rrset.addRR(it);
|
||||
}
|
||||
}
|
||||
|
||||
return rrset;
|
||||
}
|
||||
|
||||
private Message makeNoDomainResponse(Message query) {
|
||||
Header messageHeader = new Header();
|
||||
messageHeader.setID(query.getHeader().getID());
|
||||
messageHeader.setRcode(Rcode.NXDOMAIN);
|
||||
messageHeader.setFlag(Flags.QR);
|
||||
messageHeader.setFlag(Flags.CD);
|
||||
messageHeader.setFlag(Flags.RD);
|
||||
messageHeader.setFlag(Flags.RA);
|
||||
|
||||
Message answerMessage = new Message();
|
||||
answerMessage.setHeader(messageHeader);
|
||||
|
||||
for(Record record : query.getSection(Section.QUESTION)) {
|
||||
answerMessage.addRecord(record, Section.QUESTION);
|
||||
}
|
||||
|
||||
return answerMessage;
|
||||
}
|
||||
|
||||
private Message makeNoErrorResponse(Message query) {
|
||||
Header messageHeader = new Header();
|
||||
messageHeader.setID(query.getHeader().getID());
|
||||
messageHeader.setRcode(Rcode.NOERROR);
|
||||
messageHeader.setFlag(Flags.QR);
|
||||
messageHeader.setFlag(Flags.CD);
|
||||
messageHeader.setFlag(Flags.RD);
|
||||
messageHeader.setFlag(Flags.RA);
|
||||
|
||||
Message answerMessage = new Message();
|
||||
answerMessage.setHeader(messageHeader);
|
||||
|
||||
for(Record record : query.getSection(Section.QUESTION)) {
|
||||
answerMessage.addRecord(record, Section.QUESTION);
|
||||
}
|
||||
|
||||
return answerMessage;
|
||||
}
|
||||
|
||||
public List<Record> getCachedRrs() {
|
||||
return cachedRrs;
|
||||
}
|
||||
|
||||
public List<RRSIGRecord> getCachedSigs() {
|
||||
return cachedSigs;
|
||||
}
|
||||
|
||||
public List<Record> getRecords() {
|
||||
List<Record> records = new ArrayList<>(cachedRrs);
|
||||
records.addAll(cachedSigs);
|
||||
return records;
|
||||
}
|
||||
}
|
||||
@ -1,79 +0,0 @@
|
||||
package com.sparrowwallet.drongo.dns;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.xbill.DNS.*;
|
||||
import org.xbill.DNS.Record;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
public class PersistingResolver extends SimpleResolver {
|
||||
private static final Logger log = LoggerFactory.getLogger(PersistingResolver.class);
|
||||
|
||||
private final Set<Record> chain = new LinkedHashSet<>();
|
||||
|
||||
public PersistingResolver(String hostname) throws UnknownHostException {
|
||||
super(hostname);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletionStage<Message> sendAsync(Message query, Executor executor) {
|
||||
CompletionStage<Message> result = super.sendAsync(query, executor);
|
||||
return result.thenApply(response -> {
|
||||
if(log.isDebugEnabled()) {
|
||||
log.debug(responseToString(query, response));
|
||||
}
|
||||
|
||||
addAnswerSectionToChain(response.getSection(Section.ANSWER));
|
||||
addAuthoritySectionToChain(response.getSection(Section.AUTHORITY));
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
private void addAnswerSectionToChain(List<org.xbill.DNS.Record> section) {
|
||||
if(section != null) {
|
||||
chain.addAll(section);
|
||||
}
|
||||
}
|
||||
|
||||
private void addAuthoritySectionToChain(List<Record> section) {
|
||||
if(section != null) {
|
||||
for(Record r : section) {
|
||||
if((r.getType() == Type.RRSIG && (r.getRRsetType() == Type.NSEC || r.getRRsetType() == Type.NSEC3)) || r.getType() == Type.NSEC || r.getType() == Type.NSEC3) {
|
||||
chain.add(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Set<Record> getChain() {
|
||||
return chain;
|
||||
}
|
||||
|
||||
public byte[] chainToWire() {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
|
||||
List<Record> sorted = new ArrayList<>(chain);
|
||||
Collections.sort(sorted);
|
||||
for(Record record : sorted) {
|
||||
baos.writeBytes(record.toWireCanonical());
|
||||
}
|
||||
|
||||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
private static String responseToString(Message query, Message response) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("Query for ").append(query.getQuestion().getName()).append(" returned:\n");
|
||||
sb.append("Answer section:\n");
|
||||
response.getSection(Section.ANSWER).stream().forEach(rr -> sb.append(rr).append("\n"));
|
||||
sb.append("Authority section:\n");
|
||||
response.getSection(Section.AUTHORITY).stream().forEach(rr -> sb.append(rr).append("\n"));
|
||||
sb.append("\n");
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@ -1,123 +0,0 @@
|
||||
package com.sparrowwallet.drongo.dns;
|
||||
|
||||
import org.xbill.DNS.*;
|
||||
import org.xbill.DNS.Record;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class RecordUtils {
|
||||
public static org.xbill.DNS.Record fromWire(DNSInput in, int section, boolean isUpdate) throws WireParseException {
|
||||
int type;
|
||||
int dclass;
|
||||
long ttl;
|
||||
int length;
|
||||
Name name;
|
||||
|
||||
name = new Name(in);
|
||||
type = in.readU16();
|
||||
dclass = in.readU16();
|
||||
|
||||
if(section == Section.QUESTION) {
|
||||
return org.xbill.DNS.Record.newRecord(name, type, dclass);
|
||||
}
|
||||
|
||||
ttl = in.readU32();
|
||||
length = in.readU16();
|
||||
if(length == 0 && isUpdate && (section == Section.PREREQ || section == Section.UPDATE)) {
|
||||
return org.xbill.DNS.Record.newRecord(name, type, dclass, ttl);
|
||||
}
|
||||
|
||||
return Record.newRecord(name, type, dclass, ttl, length, in.readByteArray(length));
|
||||
}
|
||||
|
||||
public static Map<Record, String> checkSecurityConstraints(List<Record> section) {
|
||||
Map<Record, String> warnings = new HashMap<>();
|
||||
if(section != null) {
|
||||
for(Record record : section) {
|
||||
if(record.getType() == Type.RRSIG) {
|
||||
RRSIGRecord rrsig = (RRSIGRecord)record;
|
||||
if(rrsig.getAlgorithm() == DNSSEC.Algorithm.RSASHA1 || rrsig.getAlgorithm() == DNSSEC.Algorithm.RSA_NSEC3_SHA1) {
|
||||
warnings.put(record, "Record contains weak SHA-1 based signature");
|
||||
}
|
||||
} else if(record.getType() == Type.DNSKEY) {
|
||||
DNSKEYRecord dnskey = (DNSKEYRecord)record;
|
||||
if(dnskey.getAlgorithm() == DNSSEC.Algorithm.RSASHA1 || dnskey.getAlgorithm() == DNSSEC.Algorithm.RSA_NSEC3_SHA1 ||
|
||||
dnskey.getAlgorithm() == DNSSEC.Algorithm.RSASHA256 || dnskey.getAlgorithm() == DNSSEC.Algorithm.RSASHA512) {
|
||||
try {
|
||||
java.security.PublicKey publicKey = dnskey.getPublicKey();
|
||||
if(publicKey instanceof java.security.interfaces.RSAPublicKey rsaKey) {
|
||||
int keyLength = rsaKey.getModulus().bitLength();
|
||||
if(keyLength < 1024) {
|
||||
warnings.put(record, "Record contains weak RSA public key with key length of " + keyLength + " bits");
|
||||
}
|
||||
}
|
||||
} catch(DNSSEC.DNSSECException e) {
|
||||
warnings.put(record, "Record contains invalid public key");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return warnings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine by looking at a signed RRset whether the RRset name was the result of a wildcard
|
||||
* expansion. If so, return the name of the generating wildcard.
|
||||
*
|
||||
* @param rrset The rrset to chedck.
|
||||
* @return the wildcard name, if the rrset was synthesized from a wildcard. null if not.
|
||||
*/
|
||||
public static Name rrsetWildcard(RRset rrset) {
|
||||
List<RRSIGRecord> sigs = rrset.sigs();
|
||||
RRSIGRecord firstSig = sigs.getFirst();
|
||||
|
||||
// check rest of signatures have identical label count
|
||||
for(int i = 1; i < sigs.size(); i++) {
|
||||
if(sigs.get(i).getLabels() != firstSig.getLabels()) {
|
||||
throw new IllegalArgumentException("Label count mismatch on RRSIGs");
|
||||
}
|
||||
}
|
||||
|
||||
// if the RRSIG label count is shorter than the number of actual labels,
|
||||
// then this rrset was synthesized from a wildcard.
|
||||
// Note that the RRSIG label count doesn't count the root label.
|
||||
Name wn = rrset.getName();
|
||||
|
||||
// skip a leading wildcard label in the dname (RFC4035 2.2)
|
||||
if(rrset.getName().isWild()) {
|
||||
wn = new Name(wn, 1);
|
||||
}
|
||||
|
||||
int labelDiff = (wn.labels() - 1) - firstSig.getLabels();
|
||||
if(labelDiff > 0) {
|
||||
return wn.wild(labelDiff);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the longest domain name in common with the given name.
|
||||
*
|
||||
* @param domain1 The first domain to process.
|
||||
* @param domain2 The second domain to process.
|
||||
* @return The longest label in common of domain1 and domain2. The least common name is the root.
|
||||
*/
|
||||
public static Name longestCommonName(Name domain1, Name domain2) {
|
||||
int l = Math.min(domain1.labels(), domain2.labels());
|
||||
domain1 = new Name(domain1, domain1.labels() - l);
|
||||
domain2 = new Name(domain2, domain2.labels() - l);
|
||||
for(int i = 0; i < l - 1; i++) {
|
||||
Name ns1 = new Name(domain1, i);
|
||||
if(ns1.equals(new Name(domain2, i))) {
|
||||
return ns1;
|
||||
}
|
||||
}
|
||||
|
||||
return Name.root;
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,5 @@
|
||||
package com.sparrowwallet.drongo.pgp;
|
||||
|
||||
import com.sparrowwallet.drongo.IOUtils;
|
||||
import org.bouncycastle.bcpg.ArmoredInputStream;
|
||||
import org.bouncycastle.gpg.keybox.KeyBlob;
|
||||
import org.bouncycastle.gpg.keybox.PublicKeyRingBlob;
|
||||
@ -23,11 +22,14 @@ import java.io.InputStream;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public class PGPUtils {
|
||||
private static final Logger log = LoggerFactory.getLogger(PGPUtils.class);
|
||||
public static final String APPLICATION_KEYRING_DIR = "gpg/";
|
||||
public static final String APPLICATION_KEYRING = "/gpg/pubkeys.asc";
|
||||
public static final String PUBRING_GPG = "pubring.gpg";
|
||||
public static final String PUBRING_KBX = "pubring.kbx";
|
||||
|
||||
@ -51,7 +53,9 @@ public class PGPUtils {
|
||||
if(userPgpPublicKeyRingCollection != null) {
|
||||
options.addVerificationCerts(userPgpPublicKeyRingCollection);
|
||||
}
|
||||
options.addVerificationCerts(appPgpPublicKeyRingCollection);
|
||||
if(appPgpPublicKeyRingCollection != null) {
|
||||
options.addVerificationCerts(appPgpPublicKeyRingCollection);
|
||||
}
|
||||
if(detachedSignatureStream != null) {
|
||||
options.addVerificationOfDetachedSignatures(detachedSignatureStream);
|
||||
}
|
||||
@ -76,7 +80,8 @@ public class PGPUtils {
|
||||
signedByKey = publicKeyRing.getPublicKey(primaryKeyId);
|
||||
keySource = PGPKeySource.USER;
|
||||
log.debug("Signed using provided public key");
|
||||
} else if(appPgpPublicKeyRingCollection.getPublicKey(primaryKeyId) != null && !isExpired(appPgpPublicKeyRingCollection.getPublicKey(primaryKeyId))) {
|
||||
} else if(appPgpPublicKeyRingCollection != null && appPgpPublicKeyRingCollection.getPublicKey(primaryKeyId) != null
|
||||
&& !isExpired(appPgpPublicKeyRingCollection.getPublicKey(primaryKeyId))) {
|
||||
signedByKey = appPgpPublicKeyRingCollection.getPublicKey(primaryKeyId);
|
||||
keySource = PGPKeySource.APPLICATION;
|
||||
log.debug("Signed using application public key");
|
||||
@ -84,7 +89,7 @@ public class PGPUtils {
|
||||
signedByKey = userPgpPublicKeyRingCollection.getPublicKey(primaryKeyId);
|
||||
keySource = PGPKeySource.GPG;
|
||||
log.debug("Signed using user public key");
|
||||
} else if(appPgpPublicKeyRingCollection.getPublicKey(primaryKeyId) != null) {
|
||||
} else if(appPgpPublicKeyRingCollection != null && appPgpPublicKeyRingCollection.getPublicKey(primaryKeyId) != null) {
|
||||
signedByKey = appPgpPublicKeyRingCollection.getPublicKey(primaryKeyId);
|
||||
keySource = PGPKeySource.APPLICATION;
|
||||
log.debug("Signed using expired application public key");
|
||||
@ -110,9 +115,9 @@ public class PGPUtils {
|
||||
}
|
||||
|
||||
if(!result.getRejectedDetachedSignatures().isEmpty()) {
|
||||
throw new PGPVerificationException(result.getRejectedDetachedSignatures().getFirst().getValidationException().getMessage());
|
||||
throw new PGPVerificationException(result.getRejectedDetachedSignatures().get(0).getValidationException().getMessage());
|
||||
} else if(!result.getRejectedInlineSignatures().isEmpty()) {
|
||||
throw new PGPVerificationException(result.getRejectedInlineSignatures().getFirst().getValidationException().getMessage());
|
||||
throw new PGPVerificationException(result.getRejectedInlineSignatures().get(0).getValidationException().getMessage());
|
||||
}
|
||||
|
||||
throw new PGPVerificationException("No signatures found");
|
||||
@ -122,22 +127,16 @@ public class PGPUtils {
|
||||
}
|
||||
}
|
||||
|
||||
private static PGPPublicKeyRingCollection getApplicationKeyRingCollection() {
|
||||
List<PGPPublicKeyRing> rings = new ArrayList<>();
|
||||
try {
|
||||
String[] keyFiles = IOUtils.getResourceListing(PGPUtils.class, APPLICATION_KEYRING_DIR);
|
||||
for(String keyFile : keyFiles) {
|
||||
try(InputStream pubkeyStream = PGPUtils.class.getResourceAsStream("/" + APPLICATION_KEYRING_DIR + keyFile)) {
|
||||
if(pubkeyStream != null) {
|
||||
rings.add(PGPainless.readKeyRing().publicKeyRing(pubkeyStream));
|
||||
}
|
||||
}
|
||||
private static PGPPublicKeyRingCollection getApplicationKeyRingCollection() throws IOException {
|
||||
try(InputStream pubKeyStream = PGPUtils.class.getResourceAsStream(APPLICATION_KEYRING)) {
|
||||
if(pubKeyStream != null) {
|
||||
return PGPainless.readKeyRing().publicKeyRingCollection(pubKeyStream);
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.warn("Error loading application key rings", e);
|
||||
}
|
||||
|
||||
return new PGPPublicKeyRingCollection(rings);
|
||||
return null;
|
||||
}
|
||||
|
||||
private static PGPPublicKeyRingCollection getUserKeyRingCollection() {
|
||||
|
||||
@ -4,9 +4,8 @@ import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class Miniscript {
|
||||
private static final Pattern KEYHASH_PATTERN = Pattern.compile("pkh?\\(");
|
||||
private static final Pattern SINGLE_PATTERN = Pattern.compile("pkh?\\(");
|
||||
private static final Pattern TAPROOT_PATTERN = Pattern.compile("tr\\(");
|
||||
private static final Pattern SILENT_PAYMENTS_PATTERN = Pattern.compile("sp\\(");
|
||||
private static final Pattern MULTI_PATTERN = Pattern.compile("multi\\((\\d+)");
|
||||
|
||||
private String script;
|
||||
@ -24,7 +23,7 @@ public class Miniscript {
|
||||
}
|
||||
|
||||
public int getNumSignaturesRequired() {
|
||||
Matcher singleMatcher = KEYHASH_PATTERN.matcher(script);
|
||||
Matcher singleMatcher = SINGLE_PATTERN.matcher(script);
|
||||
if(singleMatcher.find()) {
|
||||
return 1;
|
||||
}
|
||||
@ -34,11 +33,6 @@ public class Miniscript {
|
||||
return 1;
|
||||
}
|
||||
|
||||
Matcher silentPaymentsMatcher = SILENT_PAYMENTS_PATTERN.matcher(script);
|
||||
if(silentPaymentsMatcher.find()) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
Matcher multiMatcher = MULTI_PATTERN.matcher(script);
|
||||
if(multiMatcher.find()) {
|
||||
String threshold = multiMatcher.group(1);
|
||||
|
||||
@ -41,11 +41,11 @@ public class Policy extends Persistable {
|
||||
}
|
||||
|
||||
public static Policy getPolicy(PolicyType policyType, ScriptType scriptType, List<Keystore> keystores, Integer threshold) {
|
||||
if(SINGLE_HD.equals(policyType)) {
|
||||
if(SINGLE.equals(policyType)) {
|
||||
return new Policy(new Miniscript(scriptType.getDescriptor() + keystores.get(0).getScriptName() + scriptType.getCloseDescriptor()));
|
||||
}
|
||||
|
||||
if(MULTI_HD.equals(policyType)) {
|
||||
if(MULTI.equals(policyType)) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append(scriptType.getDescriptor());
|
||||
builder.append(MULTISIG.getDescriptor());
|
||||
@ -58,10 +58,6 @@ public class Policy extends Persistable {
|
||||
return new Policy(new Miniscript(builder.toString()));
|
||||
}
|
||||
|
||||
if(SINGLE_SP.equals(policyType)) {
|
||||
return new Policy(new Miniscript("sp(" + keystores.get(0).getScriptName() + ")"));
|
||||
}
|
||||
|
||||
throw new PolicyException("No standard policy for " + policyType + " policy with script type " + scriptType);
|
||||
}
|
||||
|
||||
|
||||
@ -5,15 +5,13 @@ import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import static com.sparrowwallet.drongo.protocol.ScriptType.*;
|
||||
|
||||
public enum PolicyType {
|
||||
SINGLE_HD("Single Signature HD", "Single Signature HD", P2WPKH), MULTI_HD("Multi Signature HD", "Multi Signature HD", P2WSH), SINGLE_SP("Single Signature SP", "Single Signature SP (Silent Payments)", P2TR);
|
||||
SINGLE("Single Signature", P2WPKH), MULTI("Multi Signature", P2WSH), CUSTOM("Custom", P2WSH);
|
||||
|
||||
private final String name;
|
||||
private final String description;
|
||||
private final ScriptType defaultScriptType;
|
||||
private String name;
|
||||
private ScriptType defaultScriptType;
|
||||
|
||||
PolicyType(String name, String description, ScriptType defaultScriptType) {
|
||||
PolicyType(String name, ScriptType defaultScriptType) {
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
this.defaultScriptType = defaultScriptType;
|
||||
}
|
||||
|
||||
@ -21,10 +19,6 @@ public enum PolicyType {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public ScriptType getDefaultScriptType() {
|
||||
return defaultScriptType;
|
||||
}
|
||||
|
||||
@ -22,10 +22,10 @@ import java.util.Locale;
|
||||
|
||||
public class Bech32 {
|
||||
/** The Bech32 character set for encoding. */
|
||||
public static final String CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
||||
private static final String CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
||||
|
||||
/** The Bech32 character set for decoding. */
|
||||
public static final byte[] CHARSET_REV = {
|
||||
private static final byte[] CHARSET_REV = {
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
@ -36,9 +36,6 @@ public class Bech32 {
|
||||
1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1
|
||||
};
|
||||
|
||||
private static final int BECH32_CHECKSUM_LEN = 6;
|
||||
public static final char BECH32_SEPARATOR = '1';
|
||||
|
||||
public static class Bech32Data {
|
||||
public final String hrp;
|
||||
public final byte[] data;
|
||||
@ -105,12 +102,12 @@ public class Bech32 {
|
||||
/** Create a checksum. */
|
||||
private static byte[] createChecksum(final String hrp, Encoding encoding, final byte[] values) {
|
||||
byte[] hrpExpanded = expandHrp(hrp);
|
||||
byte[] enc = new byte[hrpExpanded.length + values.length + BECH32_CHECKSUM_LEN];
|
||||
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) ^ encoding.checksumConstant;
|
||||
byte[] ret = new byte[BECH32_CHECKSUM_LEN];
|
||||
for (int i = 0; i < BECH32_CHECKSUM_LEN; ++i) {
|
||||
byte[] ret = new byte[6];
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
ret[i] = (byte) ((mod >>> (5 * (5 - i))) & 31);
|
||||
}
|
||||
return ret;
|
||||
@ -124,11 +121,6 @@ public class Bech32 {
|
||||
/** Encode a Bech32 string. */
|
||||
public static String encode(String hrp, int version, final byte[] values) {
|
||||
Encoding encoding = (version == 0 ? Encoding.BECH32 : Encoding.BECH32M);
|
||||
return encode(hrp, version, encoding, values);
|
||||
}
|
||||
|
||||
/** Encode a Bech32 string. */
|
||||
public static String encode(String hrp, int version, Encoding encoding, final byte[] values) {
|
||||
return encode(hrp, encoding, encode(version, values));
|
||||
}
|
||||
|
||||
@ -149,7 +141,7 @@ public class Bech32 {
|
||||
System.arraycopy(checksum, 0, combined, values.length, checksum.length);
|
||||
StringBuilder sb = new StringBuilder(hrp.length() + 1 + combined.length);
|
||||
sb.append(hrp);
|
||||
sb.append(BECH32_SEPARATOR);
|
||||
sb.append('1');
|
||||
for (byte b : combined) {
|
||||
sb.append(CHARSET.charAt(b));
|
||||
}
|
||||
@ -162,21 +154,6 @@ public class Bech32 {
|
||||
}
|
||||
|
||||
public static Bech32Data decode(final String str, int limit) {
|
||||
final int separatorPos = str.lastIndexOf(BECH32_SEPARATOR);
|
||||
validate(str, limit, separatorPos, BECH32_CHECKSUM_LEN);
|
||||
byte[] values = rawDecode(str, separatorPos);
|
||||
String hrp = str.substring(0, separatorPos).toLowerCase(Locale.ROOT);
|
||||
Encoding encoding = verifyChecksum(hrp, values);
|
||||
if(encoding == null) {
|
||||
throw new ProtocolException("Invalid checksum");
|
||||
}
|
||||
|
||||
return new Bech32Data(hrp, Arrays.copyOfRange(values, 0, values.length - BECH32_CHECKSUM_LEN), encoding);
|
||||
}
|
||||
|
||||
/** Helper for validating the basic string correctness */
|
||||
public static void validate(final String str, int limit, int separatorPos, int checksumLen) {
|
||||
if (separatorPos < 1) throw new ProtocolException("Missing human-readable part");
|
||||
boolean lower = false, upper = false;
|
||||
if (str.length() < 8)
|
||||
throw new ProtocolException("Input too short: " + str.length());
|
||||
@ -196,23 +173,23 @@ public class Bech32 {
|
||||
upper = true;
|
||||
}
|
||||
}
|
||||
final int dataPartLength = str.length() - 1 - separatorPos;
|
||||
if (dataPartLength < checksumLen) throw new ProtocolException("Data part too short: " + dataPartLength);
|
||||
|
||||
for (int i = 0; i < dataPartLength; ++i) {
|
||||
char c = str.charAt(i + separatorPos + 1);
|
||||
if (CHARSET_REV[c] == -1) throw new ProtocolException("Invalid character " + c + " at position " + i);
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] rawDecode(final String str, int separator_pos) {
|
||||
final int dataPartLength = str.length() - 1 - separator_pos;
|
||||
final int pos = str.lastIndexOf('1');
|
||||
if (pos < 1) throw new ProtocolException("Missing human-readable part");
|
||||
final int dataPartLength = str.length() - 1 - pos;
|
||||
if (dataPartLength < 6) throw new ProtocolException("Data part too short: " + dataPartLength);
|
||||
byte[] values = new byte[dataPartLength];
|
||||
for (int i = 0; i < dataPartLength; ++i) {
|
||||
char c = str.charAt(i + separator_pos + 1);
|
||||
char c = str.charAt(i + pos + 1);
|
||||
if (CHARSET_REV[c] == -1) throw new ProtocolException("Invalid character " + c + " at position " + i);
|
||||
values[i] = CHARSET_REV[c];
|
||||
}
|
||||
return values;
|
||||
String hrp = str.substring(0, pos).toLowerCase(Locale.ROOT);
|
||||
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) {
|
||||
@ -227,12 +204,7 @@ public class Bech32 {
|
||||
* Helper for re-arranging bits into groups.
|
||||
*/
|
||||
public static byte[] convertBits(final byte[] in, final int inStart, final int inLen, final int fromBits,
|
||||
final int toBits, final boolean pad) {
|
||||
return convertBits(in, inStart, inLen, fromBits,toBits, pad, true);
|
||||
}
|
||||
|
||||
public static byte[] convertBits(final byte[] in, final int inStart, final int inLen, final int fromBits,
|
||||
final int toBits, final boolean pad, final boolean enforcePaddingZero) {
|
||||
final int toBits, final boolean pad) {
|
||||
int acc = 0;
|
||||
int bits = 0;
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(64);
|
||||
@ -254,11 +226,7 @@ public class Bech32 {
|
||||
if (pad) {
|
||||
if (bits > 0)
|
||||
out.write((acc << (toBits - bits)) & maxv);
|
||||
} else if (bits >= fromBits) {
|
||||
// Incomplete group at end must be less than fromBits
|
||||
throw new ProtocolException("Could not convert bits, invalid padding");
|
||||
} else if (enforcePaddingZero && (((acc << (toBits - bits)) & maxv) != 0)) {
|
||||
// Incomplete group at end must be all zeros
|
||||
} else if (bits >= fromBits || ((acc << (toBits - bits)) & maxv) != 0) {
|
||||
throw new ProtocolException("Could not convert bits, invalid padding");
|
||||
}
|
||||
return out.toByteArray();
|
||||
|
||||
@ -1,45 +0,0 @@
|
||||
package com.sparrowwallet.drongo.protocol;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class Block extends Message {
|
||||
private BlockHeader blockHeader;
|
||||
private Sha256Hash hash;
|
||||
private List<Transaction> transactions;
|
||||
|
||||
public Block(byte[] payload) {
|
||||
super(payload, 0);
|
||||
}
|
||||
|
||||
public void parse() {
|
||||
blockHeader = new BlockHeader(payload, cursor);
|
||||
cursor += blockHeader.getMessageSize();
|
||||
|
||||
hash = Sha256Hash.wrapReversed(Sha256Hash.hashTwice(payload, offset, cursor - offset));
|
||||
if(cursor != payload.length) {
|
||||
int numTransactions = (int)readVarInt();
|
||||
transactions = new ArrayList<>(numTransactions);
|
||||
for(int i = 0; i < numTransactions; i++) {
|
||||
Transaction tx = new Transaction(payload, cursor);
|
||||
transactions.add(tx);
|
||||
cursor += tx.getMessageSize();
|
||||
}
|
||||
} else {
|
||||
transactions = Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
public BlockHeader getBlockHeader() {
|
||||
return blockHeader;
|
||||
}
|
||||
|
||||
public Sha256Hash getHash() {
|
||||
return hash;
|
||||
}
|
||||
|
||||
public List<Transaction> getTransactions() {
|
||||
return transactions;
|
||||
}
|
||||
}
|
||||
@ -19,10 +19,6 @@ public class BlockHeader extends Message {
|
||||
super(rawheader, 0);
|
||||
}
|
||||
|
||||
public BlockHeader(byte[] blockdata, int offset) {
|
||||
super(blockdata, offset);
|
||||
}
|
||||
|
||||
public BlockHeader(long version, Sha256Hash prevBlockHash, Sha256Hash merkleRoot, Sha256Hash witnessRoot, long time, long difficultyTarget, long nonce) {
|
||||
this.version = version;
|
||||
this.prevBlockHash = prevBlockHash;
|
||||
|
||||
@ -3,7 +3,6 @@ package com.sparrowwallet.drongo.protocol;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.address.*;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import org.bouncycastle.util.encoders.Hex;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@ -199,7 +198,7 @@ public class Script {
|
||||
|
||||
for(ScriptType scriptType : SINGLE_KEY_TYPES) {
|
||||
if(scriptType.isScriptType(this)) {
|
||||
return new Address[] { scriptType.getAddress(PolicyType.SINGLE_HD, scriptType.getPublicKeyFromScript(this)) };
|
||||
return new Address[] { scriptType.getAddress(scriptType.getPublicKeyFromScript(this)) };
|
||||
}
|
||||
}
|
||||
|
||||
@ -213,10 +212,6 @@ public class Script {
|
||||
return addresses.toArray(new Address[addresses.size()]);
|
||||
}
|
||||
|
||||
if(P2A.isScriptType(this)) {
|
||||
return new Address[] { P2A.getAddress(P2A.getDataFromScript(this)) };
|
||||
}
|
||||
|
||||
throw new NonStandardScriptException("Cannot find addresses in non standard script: " + toString());
|
||||
}
|
||||
|
||||
|
||||
@ -24,7 +24,7 @@ public enum ScriptType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Address getAddress(PolicyType policyType, ECKey key) {
|
||||
public Address getAddress(ECKey key) {
|
||||
return getAddress(key.getPubKey());
|
||||
}
|
||||
|
||||
@ -48,7 +48,7 @@ public enum ScriptType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Script getOutputScript(PolicyType policyType, ECKey key) {
|
||||
public Script getOutputScript(ECKey key) {
|
||||
return getOutputScript(key.getPubKey());
|
||||
}
|
||||
|
||||
@ -100,7 +100,7 @@ public enum ScriptType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Script getScriptSig(PolicyType policyType, Script scriptPubKey, ECKey pubKey, TransactionSignature signature) {
|
||||
public Script getScriptSig(Script scriptPubKey, ECKey pubKey, TransactionSignature signature) {
|
||||
if(!isScriptType(scriptPubKey)) {
|
||||
throw new ProtocolException("Provided scriptPubKey is not a " + getName() + " script");
|
||||
}
|
||||
@ -111,18 +111,18 @@ public enum ScriptType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransactionInput addSpendingInput(PolicyType policyType, Transaction transaction, TransactionOutput prevOutput, ECKey pubKey, TransactionSignature signature) {
|
||||
Script scriptSig = getScriptSig(policyType, prevOutput.getScript(), pubKey, signature);
|
||||
public TransactionInput addSpendingInput(Transaction transaction, TransactionOutput prevOutput, ECKey pubKey, TransactionSignature signature) {
|
||||
Script scriptSig = getScriptSig(prevOutput.getScript(), pubKey, signature);
|
||||
return transaction.addInput(prevOutput.getHash(), prevOutput.getIndex(), scriptSig);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Script getMultisigScriptSig(PolicyType policyType, Script scriptPubKey, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
public Script getMultisigScriptSig(Script scriptPubKey, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
throw new ProtocolException(getName() + " is not a multisig script type");
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransactionInput addMultisigSpendingInput(PolicyType policyType, Transaction transaction, TransactionOutput prevOutput, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
public TransactionInput addMultisigSpendingInput(Transaction transaction, TransactionOutput prevOutput, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
throw new ProtocolException(getName() + " is not a multisig script type");
|
||||
}
|
||||
|
||||
@ -133,7 +133,7 @@ public enum ScriptType {
|
||||
|
||||
@Override
|
||||
public List<PolicyType> getAllowedPolicyTypes() {
|
||||
return List.of(SINGLE_HD);
|
||||
return List.of(SINGLE);
|
||||
}
|
||||
},
|
||||
P2PKH("P2PKH", "Legacy (P2PKH)", "m/44'/0'/0'") {
|
||||
@ -143,7 +143,7 @@ public enum ScriptType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Address getAddress(PolicyType policyType, ECKey key) {
|
||||
public Address getAddress(ECKey key) {
|
||||
return getAddress(key.getPubKeyHash());
|
||||
}
|
||||
|
||||
@ -165,7 +165,7 @@ public enum ScriptType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Script getOutputScript(PolicyType policyType, ECKey key) {
|
||||
public Script getOutputScript(ECKey key) {
|
||||
return getOutputScript(key.getPubKeyHash());
|
||||
}
|
||||
|
||||
@ -216,7 +216,7 @@ public enum ScriptType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Script getScriptSig(PolicyType policyType, Script scriptPubKey, ECKey pubKey, TransactionSignature signature) {
|
||||
public Script getScriptSig(Script scriptPubKey, ECKey pubKey, TransactionSignature signature) {
|
||||
if(!isScriptType(scriptPubKey)) {
|
||||
throw new ProtocolException("Provided scriptPubKey is not a " + getName() + " script");
|
||||
}
|
||||
@ -229,18 +229,18 @@ public enum ScriptType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransactionInput addSpendingInput(PolicyType policyType, Transaction transaction, TransactionOutput prevOutput, ECKey pubKey, TransactionSignature signature) {
|
||||
Script scriptSig = getScriptSig(policyType, prevOutput.getScript(), pubKey, signature);
|
||||
public TransactionInput addSpendingInput(Transaction transaction, TransactionOutput prevOutput, ECKey pubKey, TransactionSignature signature) {
|
||||
Script scriptSig = getScriptSig(prevOutput.getScript(), pubKey, signature);
|
||||
return transaction.addInput(prevOutput.getHash(), prevOutput.getIndex(), scriptSig);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Script getMultisigScriptSig(PolicyType policyType, Script scriptPubKey, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
public Script getMultisigScriptSig(Script scriptPubKey, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
throw new ProtocolException(getName() + " is not a multisig script type");
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransactionInput addMultisigSpendingInput(PolicyType policyType, Transaction transaction, TransactionOutput prevOutput, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
public TransactionInput addMultisigSpendingInput(Transaction transaction, TransactionOutput prevOutput, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
throw new ProtocolException(getName() + " is not a multisig script type");
|
||||
}
|
||||
|
||||
@ -251,7 +251,7 @@ public enum ScriptType {
|
||||
|
||||
@Override
|
||||
public List<PolicyType> getAllowedPolicyTypes() {
|
||||
return List.of(SINGLE_HD);
|
||||
return List.of(SINGLE);
|
||||
}
|
||||
},
|
||||
MULTISIG("Bare Multisig", "Bare Multisig", "m/44'/0'/0'") {
|
||||
@ -266,7 +266,7 @@ public enum ScriptType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Address getAddress(PolicyType policyType, ECKey key) {
|
||||
public Address getAddress(ECKey key) {
|
||||
throw new ProtocolException("No single key address for multisig script type");
|
||||
}
|
||||
|
||||
@ -281,7 +281,7 @@ public enum ScriptType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Script getOutputScript(PolicyType policyType, ECKey key) {
|
||||
public Script getOutputScript(ECKey key) {
|
||||
throw new ProtocolException("Output script for multisig script type must be constructed with method getOutputScript(int threshold, List<ECKey> pubKeys)");
|
||||
}
|
||||
|
||||
@ -398,17 +398,17 @@ public enum ScriptType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Script getScriptSig(PolicyType policyType, Script scriptPubKey, ECKey pubKey, TransactionSignature signature) {
|
||||
public Script getScriptSig(Script scriptPubKey, ECKey pubKey, TransactionSignature signature) {
|
||||
throw new ProtocolException(getName() + " is a multisig script type");
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransactionInput addSpendingInput(PolicyType policyType, Transaction transaction, TransactionOutput prevOutput, ECKey pubKey, TransactionSignature signature) {
|
||||
public TransactionInput addSpendingInput(Transaction transaction, TransactionOutput prevOutput, ECKey pubKey, TransactionSignature signature) {
|
||||
throw new ProtocolException(getName() + " is a multisig script type");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Script getMultisigScriptSig(PolicyType policyType, Script scriptPubKey, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
public Script getMultisigScriptSig(Script scriptPubKey, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
if(!isScriptType(scriptPubKey)) {
|
||||
throw new ProtocolException("Provided scriptPubKey is not a " + getName() + " script");
|
||||
}
|
||||
@ -430,8 +430,8 @@ public enum ScriptType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransactionInput addMultisigSpendingInput(PolicyType policyType, Transaction transaction, TransactionOutput prevOutput, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
Script scriptSig = getMultisigScriptSig(policyType, prevOutput.getScript(), threshold, pubKeySignatures);
|
||||
public TransactionInput addMultisigSpendingInput(Transaction transaction, TransactionOutput prevOutput, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
Script scriptSig = getMultisigScriptSig(prevOutput.getScript(), threshold, pubKeySignatures);
|
||||
return transaction.addInput(prevOutput.getHash(), prevOutput.getIndex(), scriptSig);
|
||||
}
|
||||
|
||||
@ -442,7 +442,7 @@ public enum ScriptType {
|
||||
|
||||
@Override
|
||||
public List<PolicyType> getAllowedPolicyTypes() {
|
||||
return List.of(MULTI_HD);
|
||||
return List.of(MULTI);
|
||||
}
|
||||
},
|
||||
P2SH("P2SH", "Legacy (P2SH)", "m/45'") {
|
||||
@ -452,7 +452,7 @@ public enum ScriptType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Address getAddress(PolicyType policyType, ECKey key) {
|
||||
public Address getAddress(ECKey key) {
|
||||
throw new ProtocolException("No single key address for script hash type");
|
||||
}
|
||||
|
||||
@ -472,7 +472,7 @@ public enum ScriptType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Script getOutputScript(PolicyType policyType, ECKey key) {
|
||||
public Script getOutputScript(ECKey key) {
|
||||
throw new ProtocolException("No single key output script for script hash type");
|
||||
}
|
||||
|
||||
@ -531,17 +531,17 @@ public enum ScriptType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Script getScriptSig(PolicyType policyType, Script scriptPubKey, ECKey pubKey, TransactionSignature signature) {
|
||||
public Script getScriptSig(Script scriptPubKey, ECKey pubKey, TransactionSignature signature) {
|
||||
throw new ProtocolException("Only multisig scriptSigs supported for " + getName() + " scriptPubKeys");
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransactionInput addSpendingInput(PolicyType policyType, Transaction transaction, TransactionOutput prevOutput, ECKey pubKey, TransactionSignature signature) {
|
||||
public TransactionInput addSpendingInput(Transaction transaction, TransactionOutput prevOutput, ECKey pubKey, TransactionSignature signature) {
|
||||
throw new ProtocolException("Only multisig scriptSigs supported for " + getName() + " scriptPubKeys");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Script getMultisigScriptSig(PolicyType policyType, Script scriptPubKey, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
public Script getMultisigScriptSig(Script scriptPubKey, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
if(!isScriptType(scriptPubKey)) {
|
||||
throw new ProtocolException("Provided scriptPubKey is not a " + getName() + " script");
|
||||
}
|
||||
@ -551,7 +551,7 @@ public enum ScriptType {
|
||||
throw new ProtocolException("P2SH scriptPubKey hash does not match constructed redeem script hash");
|
||||
}
|
||||
|
||||
Script multisigScript = MULTISIG.getMultisigScriptSig(policyType, redeemScript, threshold, pubKeySignatures);
|
||||
Script multisigScript = MULTISIG.getMultisigScriptSig(redeemScript, threshold, pubKeySignatures);
|
||||
List<ScriptChunk> chunks = new ArrayList<>(multisigScript.getChunks());
|
||||
ScriptChunk redeemScriptChunk = ScriptChunk.fromData(redeemScript.getProgram());
|
||||
chunks.add(redeemScriptChunk);
|
||||
@ -560,8 +560,8 @@ public enum ScriptType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransactionInput addMultisigSpendingInput(PolicyType policyType, Transaction transaction, TransactionOutput prevOutput, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
Script scriptSig = getMultisigScriptSig(policyType, prevOutput.getScript(), threshold, pubKeySignatures);
|
||||
public TransactionInput addMultisigSpendingInput(Transaction transaction, TransactionOutput prevOutput, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
Script scriptSig = getMultisigScriptSig(prevOutput.getScript(), threshold, pubKeySignatures);
|
||||
return transaction.addInput(prevOutput.getHash(), prevOutput.getIndex(), scriptSig);
|
||||
}
|
||||
|
||||
@ -572,7 +572,7 @@ public enum ScriptType {
|
||||
|
||||
@Override
|
||||
public List<PolicyType> getAllowedPolicyTypes() {
|
||||
return List.of(MULTI_HD);
|
||||
return List.of(MULTI);
|
||||
}
|
||||
},
|
||||
P2SH_P2WPKH("P2SH-P2WPKH", "Nested Segwit (P2SH-P2WPKH)", "m/49'/0'/0'") {
|
||||
@ -582,7 +582,7 @@ public enum ScriptType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Address getAddress(PolicyType policyType, ECKey key) {
|
||||
public Address getAddress(ECKey key) {
|
||||
Script p2wpkhScript = P2WPKH.getOutputScript(key.getPubKeyHash());
|
||||
return P2SH.getAddress(p2wpkhScript);
|
||||
}
|
||||
@ -602,7 +602,7 @@ public enum ScriptType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Script getOutputScript(PolicyType policyType, ECKey key) {
|
||||
public Script getOutputScript(ECKey key) {
|
||||
Script p2wpkhScript = P2WPKH.getOutputScript(key.getPubKeyHash());
|
||||
return P2SH.getOutputScript(p2wpkhScript);
|
||||
}
|
||||
@ -642,12 +642,12 @@ public enum ScriptType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Script getScriptSig(PolicyType policyType, Script scriptPubKey, ECKey pubKey, TransactionSignature signature) {
|
||||
public Script getScriptSig(Script scriptPubKey, ECKey pubKey, TransactionSignature signature) {
|
||||
if(!isScriptType(scriptPubKey)) {
|
||||
throw new ProtocolException("Provided scriptPubKey is not a " + getName() + " script");
|
||||
}
|
||||
|
||||
Script redeemScript = P2WPKH.getOutputScript(policyType, pubKey);
|
||||
Script redeemScript = P2WPKH.getOutputScript(pubKey);
|
||||
if(!scriptPubKey.equals(P2SH.getOutputScript(redeemScript))) {
|
||||
throw new ProtocolException(getName() + " scriptPubKey hash does not match constructed redeem script hash");
|
||||
}
|
||||
@ -657,19 +657,19 @@ public enum ScriptType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransactionInput addSpendingInput(PolicyType policyType, Transaction transaction, TransactionOutput prevOutput, ECKey pubKey, TransactionSignature signature) {
|
||||
Script scriptSig = getScriptSig(policyType, prevOutput.getScript(), pubKey, signature);
|
||||
public TransactionInput addSpendingInput(Transaction transaction, TransactionOutput prevOutput, ECKey pubKey, TransactionSignature signature) {
|
||||
Script scriptSig = getScriptSig(prevOutput.getScript(), pubKey, signature);
|
||||
TransactionWitness witness = new TransactionWitness(transaction, pubKey, signature);
|
||||
return transaction.addInput(prevOutput.getHash(), prevOutput.getIndex(), scriptSig, witness);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Script getMultisigScriptSig(PolicyType policyType, Script scriptPubKey, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
public Script getMultisigScriptSig(Script scriptPubKey, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
throw new ProtocolException(getName() + " is not a multisig script type");
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransactionInput addMultisigSpendingInput(PolicyType policyType, Transaction transaction, TransactionOutput prevOutput, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
public TransactionInput addMultisigSpendingInput(Transaction transaction, TransactionOutput prevOutput, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
throw new ProtocolException(getName() + " is not a multisig script type");
|
||||
}
|
||||
|
||||
@ -680,7 +680,7 @@ public enum ScriptType {
|
||||
|
||||
@Override
|
||||
public List<PolicyType> getAllowedPolicyTypes() {
|
||||
return List.of(SINGLE_HD);
|
||||
return List.of(SINGLE);
|
||||
}
|
||||
},
|
||||
P2SH_P2WSH("P2SH-P2WSH", "Nested Segwit (P2SH-P2WSH)", "m/48'/0'/0'/1'") {
|
||||
@ -690,7 +690,7 @@ public enum ScriptType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Address getAddress(PolicyType policyType, ECKey key) {
|
||||
public Address getAddress(ECKey key) {
|
||||
throw new ProtocolException("No single key address for wrapped witness script hash type");
|
||||
}
|
||||
|
||||
@ -706,7 +706,7 @@ public enum ScriptType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Script getOutputScript(PolicyType policyType, ECKey key) {
|
||||
public Script getOutputScript(ECKey key) {
|
||||
throw new ProtocolException("No single key output script for wrapped witness script hash type");
|
||||
}
|
||||
|
||||
@ -746,17 +746,17 @@ public enum ScriptType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Script getScriptSig(PolicyType policyType, Script scriptPubKey, ECKey pubKey, TransactionSignature signature) {
|
||||
public Script getScriptSig(Script scriptPubKey, ECKey pubKey, TransactionSignature signature) {
|
||||
throw new ProtocolException("Only multisig scriptSigs supported for " + getName() + " scriptPubKeys");
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransactionInput addSpendingInput(PolicyType policyType, Transaction transaction, TransactionOutput prevOutput, ECKey pubKey, TransactionSignature signature) {
|
||||
public TransactionInput addSpendingInput(Transaction transaction, TransactionOutput prevOutput, ECKey pubKey, TransactionSignature signature) {
|
||||
throw new ProtocolException("Only multisig scriptSigs supported for " + getName() + " scriptPubKeys");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Script getMultisigScriptSig(PolicyType policyType, Script scriptPubKey, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
public Script getMultisigScriptSig(Script scriptPubKey, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
if(!isScriptType(scriptPubKey)) {
|
||||
throw new ProtocolException("Provided scriptPubKey is not a " + getName() + " script");
|
||||
}
|
||||
@ -772,8 +772,8 @@ public enum ScriptType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransactionInput addMultisigSpendingInput(PolicyType policyType, Transaction transaction, TransactionOutput prevOutput, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
Script scriptSig = getMultisigScriptSig(policyType, prevOutput.getScript(), threshold, pubKeySignatures);
|
||||
public TransactionInput addMultisigSpendingInput(Transaction transaction, TransactionOutput prevOutput, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
Script scriptSig = getMultisigScriptSig(prevOutput.getScript(), threshold, pubKeySignatures);
|
||||
Script witnessScript = MULTISIG.getOutputScript(threshold, pubKeySignatures.keySet());
|
||||
TransactionWitness witness = new TransactionWitness(transaction, pubKeySignatures.values().stream().filter(Objects::nonNull).limit(threshold).collect(Collectors.toList()), witnessScript);
|
||||
return transaction.addInput(prevOutput.getHash(), prevOutput.getIndex(), scriptSig, witness);
|
||||
@ -786,7 +786,7 @@ public enum ScriptType {
|
||||
|
||||
@Override
|
||||
public List<PolicyType> getAllowedPolicyTypes() {
|
||||
return List.of(MULTI_HD);
|
||||
return List.of(MULTI, CUSTOM);
|
||||
}
|
||||
},
|
||||
P2WPKH("P2WPKH", "Native Segwit (P2WPKH)", "m/84'/0'/0'") {
|
||||
@ -796,7 +796,7 @@ public enum ScriptType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Address getAddress(PolicyType policyType, ECKey key) {
|
||||
public Address getAddress(ECKey key) {
|
||||
return getAddress(key.getPubKeyHash());
|
||||
}
|
||||
|
||||
@ -815,7 +815,7 @@ public enum ScriptType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Script getOutputScript(PolicyType policyType, ECKey key) {
|
||||
public Script getOutputScript(ECKey key) {
|
||||
return getOutputScript(key.getPubKeyHash());
|
||||
}
|
||||
|
||||
@ -860,12 +860,12 @@ public enum ScriptType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Script getScriptSig(PolicyType policyType, Script scriptPubKey, ECKey pubKey, TransactionSignature signature) {
|
||||
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(policyType, pubKey))) {
|
||||
if(!scriptPubKey.equals(getOutputScript(pubKey))) {
|
||||
throw new ProtocolException("P2WPKH scriptPubKey hash does not match constructed pubkey script hash");
|
||||
}
|
||||
|
||||
@ -873,19 +873,19 @@ public enum ScriptType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransactionInput addSpendingInput(PolicyType policyType, Transaction transaction, TransactionOutput prevOutput, ECKey pubKey, TransactionSignature signature) {
|
||||
Script scriptSig = getScriptSig(policyType, prevOutput.getScript(), pubKey, signature);
|
||||
public TransactionInput addSpendingInput(Transaction transaction, TransactionOutput prevOutput, ECKey pubKey, TransactionSignature signature) {
|
||||
Script scriptSig = getScriptSig(prevOutput.getScript(), pubKey, signature);
|
||||
TransactionWitness witness = new TransactionWitness(transaction, pubKey, signature);
|
||||
return transaction.addInput(prevOutput.getHash(), prevOutput.getIndex(), scriptSig, witness);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Script getMultisigScriptSig(PolicyType policyType, Script scriptPubKey, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
public Script getMultisigScriptSig(Script scriptPubKey, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
throw new ProtocolException(getName() + " is not a multisig script type");
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransactionInput addMultisigSpendingInput(PolicyType policyType, Transaction transaction, TransactionOutput prevOutput, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
public TransactionInput addMultisigSpendingInput(Transaction transaction, TransactionOutput prevOutput, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
throw new ProtocolException(getName() + " is not a multisig script type");
|
||||
}
|
||||
|
||||
@ -896,7 +896,7 @@ public enum ScriptType {
|
||||
|
||||
@Override
|
||||
public List<PolicyType> getAllowedPolicyTypes() {
|
||||
return List.of(SINGLE_HD);
|
||||
return List.of(SINGLE);
|
||||
}
|
||||
},
|
||||
P2WSH("P2WSH", "Native Segwit (P2WSH)", "m/48'/0'/0'/2'") {
|
||||
@ -906,7 +906,7 @@ public enum ScriptType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Address getAddress(PolicyType policyType, ECKey key) {
|
||||
public Address getAddress(ECKey key) {
|
||||
throw new ProtocolException("No single key address for witness script hash type");
|
||||
}
|
||||
|
||||
@ -925,7 +925,7 @@ public enum ScriptType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Script getOutputScript(PolicyType policyType, ECKey key) {
|
||||
public Script getOutputScript(ECKey key) {
|
||||
throw new ProtocolException("No single key output script for witness script hash type");
|
||||
}
|
||||
|
||||
@ -974,17 +974,17 @@ public enum ScriptType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Script getScriptSig(PolicyType policyType, Script scriptPubKey, ECKey pubKey, TransactionSignature signature) {
|
||||
public Script getScriptSig(Script scriptPubKey, ECKey pubKey, TransactionSignature signature) {
|
||||
throw new ProtocolException("Only multisig scriptSigs supported for " + getName() + " scriptPubKeys");
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransactionInput addSpendingInput(PolicyType policyType, Transaction transaction, TransactionOutput prevOutput, ECKey pubKey, TransactionSignature signature) {
|
||||
public TransactionInput addSpendingInput(Transaction transaction, TransactionOutput prevOutput, ECKey pubKey, TransactionSignature signature) {
|
||||
throw new ProtocolException("Only multisig scriptSigs supported for " + getName() + " scriptPubKeys");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Script getMultisigScriptSig(PolicyType policyType, Script scriptPubKey, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
public Script getMultisigScriptSig(Script scriptPubKey, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
if(!isScriptType(scriptPubKey)) {
|
||||
throw new ProtocolException("Provided scriptPubKey is not a " + getName() + " script");
|
||||
}
|
||||
@ -998,8 +998,8 @@ public enum ScriptType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransactionInput addMultisigSpendingInput(PolicyType policyType, Transaction transaction, TransactionOutput prevOutput, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
Script scriptSig = getMultisigScriptSig(policyType, prevOutput.getScript(), threshold, pubKeySignatures);
|
||||
public TransactionInput addMultisigSpendingInput(Transaction transaction, TransactionOutput prevOutput, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
Script scriptSig = getMultisigScriptSig(prevOutput.getScript(), threshold, pubKeySignatures);
|
||||
Script witnessScript = MULTISIG.getOutputScript(threshold, pubKeySignatures.keySet());
|
||||
TransactionWitness witness = new TransactionWitness(transaction, pubKeySignatures.values().stream().filter(Objects::nonNull).limit(threshold).collect(Collectors.toList()), witnessScript);
|
||||
return transaction.addInput(prevOutput.getHash(), prevOutput.getIndex(), scriptSig, witness);
|
||||
@ -1012,13 +1012,13 @@ public enum ScriptType {
|
||||
|
||||
@Override
|
||||
public List<PolicyType> getAllowedPolicyTypes() {
|
||||
return List.of(MULTI_HD);
|
||||
return List.of(MULTI, CUSTOM);
|
||||
}
|
||||
},
|
||||
P2TR("P2TR", "Taproot (P2TR)", "m/86'/0'/0'") {
|
||||
@Override
|
||||
public ECKey getOutputKey(PolicyType policyType, ECKey derivedKey) {
|
||||
return policyType == SINGLE_SP ? derivedKey : derivedKey.getTweakedOutputKey();
|
||||
public ECKey getOutputKey(ECKey derivedKey) {
|
||||
return derivedKey.getTweakedOutputKey();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -1027,8 +1027,8 @@ public enum ScriptType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Address getAddress(PolicyType policyType, ECKey derivedKey) {
|
||||
return getAddress(getOutputKey(policyType, derivedKey).getPubKeyXCoord());
|
||||
public Address getAddress(ECKey derivedKey) {
|
||||
return getAddress(getOutputKey(derivedKey).getPubKeyXCoord());
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -1046,8 +1046,8 @@ public enum ScriptType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Script getOutputScript(PolicyType policyType, ECKey derivedKey) {
|
||||
return getOutputScript(getOutputKey(policyType, derivedKey).getPubKeyXCoord());
|
||||
public Script getOutputScript(ECKey derivedKey) {
|
||||
return getOutputScript(getOutputKey(derivedKey).getPubKeyXCoord());
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -1096,12 +1096,12 @@ public enum ScriptType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Script getScriptSig(PolicyType policyType, Script scriptPubKey, ECKey pubKey, TransactionSignature signature) {
|
||||
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(policyType, pubKey))) {
|
||||
if(!scriptPubKey.equals(getOutputScript(pubKey))) {
|
||||
throw new ProtocolException("Provided P2TR scriptPubKey does not match constructed pubkey script");
|
||||
}
|
||||
|
||||
@ -1109,19 +1109,19 @@ public enum ScriptType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransactionInput addSpendingInput(PolicyType policyType, Transaction transaction, TransactionOutput prevOutput, ECKey pubKey, TransactionSignature signature) {
|
||||
Script scriptSig = getScriptSig(policyType, prevOutput.getScript(), pubKey, signature);
|
||||
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(PolicyType policyType, Script scriptPubKey, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
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(PolicyType policyType, Transaction transaction, TransactionOutput prevOutput, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
public TransactionInput addMultisigSpendingInput(Transaction transaction, TransactionOutput prevOutput, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
throw new UnsupportedOperationException("Constructing Taproot inputs is not yet supported");
|
||||
}
|
||||
|
||||
@ -1132,123 +1132,7 @@ public enum ScriptType {
|
||||
|
||||
@Override
|
||||
public List<PolicyType> getAllowedPolicyTypes() {
|
||||
return List.of(SINGLE_HD, SINGLE_SP);
|
||||
}
|
||||
},
|
||||
P2A("P2A", "Anchor (P2A)", "m/86'/0'/0'") {
|
||||
@Override
|
||||
public Address getAddress(byte[] data) {
|
||||
return new P2AAddress(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Address getAddress(PolicyType policyType, ECKey derivedKey) {
|
||||
throw new ProtocolException("Cannot create a anchor address with a key");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Address getAddress(Script script) {
|
||||
throw new ProtocolException("Cannot create a anchor address with a script");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Script getOutputScript(byte[] data) {
|
||||
List<ScriptChunk> chunks = new ArrayList<>();
|
||||
chunks.add(new ScriptChunk(OP_1, null));
|
||||
chunks.add(new ScriptChunk(data.length, data));
|
||||
|
||||
return new Script(chunks);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Script getOutputScript(PolicyType policyType, ECKey derivedKey) {
|
||||
throw new ProtocolException("Cannot create an anchor output script with a key");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Script getOutputScript(Script script) {
|
||||
throw new ProtocolException("Cannot create an anchor output script with a script");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getOutputDescriptor(ECKey derivedKey) {
|
||||
throw new ProtocolException("Cannot create an anchor output descriptor with a key");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getOutputDescriptor(Script script) {
|
||||
throw new ProtocolException("Cannot create an anchor output descriptor with a script");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescriptor() {
|
||||
return "addr(";
|
||||
}
|
||||
|
||||
@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 (!Arrays.equals(chunk1data, ANCHOR_WITNESS_PROGRAM)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getDataFromScript(Script script) {
|
||||
return script.chunks.get(1).data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getHashFromScript(Script script) {
|
||||
throw new ProtocolException("P2A does not contain a hash");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ECKey getPublicKeyFromScript(Script script) {
|
||||
throw new ProtocolException("P2A does not contain a key");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Script getScriptSig(PolicyType policyType, Script scriptPubKey, ECKey pubKey, TransactionSignature signature) {
|
||||
if(!isScriptType(scriptPubKey)) {
|
||||
throw new ProtocolException("Provided scriptPubKey is not a " + getName() + " script");
|
||||
}
|
||||
|
||||
return new Script(new byte[0]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransactionInput addSpendingInput(PolicyType policyType, Transaction transaction, TransactionOutput prevOutput, ECKey pubKey, TransactionSignature signature) {
|
||||
Script scriptSig = getScriptSig(policyType, prevOutput.getScript(), pubKey, signature);
|
||||
return transaction.addInput(prevOutput.getHash(), prevOutput.getIndex(), scriptSig);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Script getMultisigScriptSig(PolicyType policyType, Script scriptPubKey, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
throw new UnsupportedOperationException("Constructing Taproot inputs is not yet supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransactionInput addMultisigSpendingInput(PolicyType policyType, 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 Collections.emptyList();
|
||||
return List.of(SINGLE);
|
||||
}
|
||||
};
|
||||
|
||||
@ -1270,10 +1154,6 @@ public enum ScriptType {
|
||||
return description;
|
||||
}
|
||||
|
||||
public String getDescription(boolean includePolicyType) {
|
||||
return includePolicyType && !getAllowedPolicyTypes().isEmpty() ? getAllowedPolicyTypes().getFirst().getName().toLowerCase(Locale.ROOT) + " " + description : description;
|
||||
}
|
||||
|
||||
public String getDefaultDerivationPath() {
|
||||
return Network.get() != Network.MAINNET ? defaultDerivationPath.replace("/0'/0'", "/1'/0'") : defaultDerivationPath;
|
||||
}
|
||||
@ -1334,19 +1214,19 @@ public enum ScriptType {
|
||||
return getAllowedPolicyTypes().contains(policyType);
|
||||
}
|
||||
|
||||
public ECKey getOutputKey(PolicyType policyType, ECKey derivedKey) {
|
||||
public ECKey getOutputKey(ECKey derivedKey) {
|
||||
return derivedKey;
|
||||
}
|
||||
|
||||
public abstract Address getAddress(byte[] bytes);
|
||||
|
||||
public abstract Address getAddress(PolicyType policyType, ECKey key);
|
||||
public abstract Address getAddress(ECKey key);
|
||||
|
||||
public abstract Address getAddress(Script script);
|
||||
|
||||
public abstract Script getOutputScript(byte[] bytes);
|
||||
|
||||
public abstract Script getOutputScript(PolicyType policyType, ECKey key);
|
||||
public abstract Script getOutputScript(ECKey key);
|
||||
|
||||
public abstract Script getOutputScript(Script script);
|
||||
|
||||
@ -1366,10 +1246,6 @@ public enum ScriptType {
|
||||
|
||||
public abstract boolean isScriptType(Script script);
|
||||
|
||||
public byte[] getDataFromScript(Script script) {
|
||||
throw new ProtocolException("Script type " + this + " does not contain data");
|
||||
}
|
||||
|
||||
public abstract byte[] getHashFromScript(Script script);
|
||||
|
||||
public Address[] getAddresses(Script script) {
|
||||
@ -1388,13 +1264,13 @@ public enum ScriptType {
|
||||
throw new ProtocolException("Script type " + this + " is not a multisig script");
|
||||
}
|
||||
|
||||
public abstract Script getScriptSig(PolicyType policyType, Script scriptPubKey, ECKey pubKey, TransactionSignature signature);
|
||||
public abstract Script getScriptSig(Script scriptPubKey, ECKey pubKey, TransactionSignature signature);
|
||||
|
||||
public abstract TransactionInput addSpendingInput(PolicyType policyType, Transaction transaction, TransactionOutput prevOutput, ECKey pubKey, TransactionSignature signature);
|
||||
public abstract TransactionInput addSpendingInput(Transaction transaction, TransactionOutput prevOutput, ECKey pubKey, TransactionSignature signature);
|
||||
|
||||
public abstract Script getMultisigScriptSig(PolicyType policyType, Script scriptPubKey, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures);
|
||||
public abstract Script getMultisigScriptSig(Script scriptPubKey, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures);
|
||||
|
||||
public abstract TransactionInput addMultisigSpendingInput(PolicyType policyType, Transaction transaction, TransactionOutput prevOutput, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures);
|
||||
public abstract TransactionInput addMultisigSpendingInput(Transaction transaction, TransactionOutput prevOutput, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures);
|
||||
|
||||
public abstract TransactionSignature.Type getSignatureType();
|
||||
|
||||
@ -1402,13 +1278,11 @@ public enum ScriptType {
|
||||
|
||||
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, P2A};
|
||||
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, P2TR, P2A};
|
||||
|
||||
public static final byte[] ANCHOR_WITNESS_PROGRAM = new byte[] {78, 115};
|
||||
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());
|
||||
@ -1486,8 +1360,6 @@ public enum ScriptType {
|
||||
} else if(P2TR.equals(this)) {
|
||||
//Assume a default keypath spend
|
||||
return (32 + 4 + 1 + ((double)66 / WITNESS_SCALE_FACTOR) + 4);
|
||||
} else if(P2A.equals(this)) {
|
||||
return 32 + 4 + 1 + 4;
|
||||
} else if(Arrays.asList(WITNESS_TYPES).contains(this)) {
|
||||
//Return length of spending input with 75% discount to script size
|
||||
return (32 + 4 + 1 + ((double)107 / WITNESS_SCALE_FACTOR) + 4);
|
||||
|
||||
@ -3,7 +3,6 @@ package com.sparrowwallet.drongo.protocol;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
@ -15,7 +14,7 @@ import static com.sparrowwallet.drongo.Utils.uint32ToByteStreamLE;
|
||||
import static com.sparrowwallet.drongo.Utils.uint64ToByteStreamLE;
|
||||
|
||||
public class Transaction extends ChildMessage {
|
||||
public static final int MAX_BLOCK_SIZE_VBYTES = 1000 * 1000;
|
||||
public static final int MAX_BLOCK_SIZE = 1000 * 1000;
|
||||
public static final long MAX_BITCOIN = 21 * 1000 * 1000L;
|
||||
public static final long SATOSHIS_PER_BITCOIN = 100 * 1000 * 1000L;
|
||||
public static final long MAX_BLOCK_LOCKTIME = 500000000L;
|
||||
@ -54,10 +53,6 @@ public class Transaction extends ChildMessage {
|
||||
super(rawtx, 0);
|
||||
}
|
||||
|
||||
public Transaction(byte[] blockdata, int offset) {
|
||||
super(blockdata, offset);
|
||||
}
|
||||
|
||||
public long getVersion() {
|
||||
return version;
|
||||
}
|
||||
@ -104,8 +99,8 @@ public class Transaction extends ChildMessage {
|
||||
}
|
||||
|
||||
public Sha256Hash getTxId() {
|
||||
if(cachedTxId == null) {
|
||||
if(!isSegwit() && cachedWTxId != null) {
|
||||
if (cachedTxId == null) {
|
||||
if (!hasWitnesses() && cachedWTxId != null) {
|
||||
cachedTxId = cachedWTxId;
|
||||
} else {
|
||||
cachedTxId = calculateTxId(false);
|
||||
@ -115,11 +110,11 @@ public class Transaction extends ChildMessage {
|
||||
}
|
||||
|
||||
public Sha256Hash getWTxId() {
|
||||
if(cachedWTxId == null) {
|
||||
if(!isSegwit() && cachedTxId != null) {
|
||||
if (cachedWTxId == null) {
|
||||
if (!hasWitnesses() && cachedTxId != null) {
|
||||
cachedWTxId = cachedTxId;
|
||||
} else {
|
||||
cachedWTxId = calculateTxId(isSegwit());
|
||||
cachedWTxId = calculateTxId(true);
|
||||
}
|
||||
}
|
||||
return cachedWTxId;
|
||||
@ -374,8 +369,8 @@ public class Transaction extends ChildMessage {
|
||||
return Collections.unmodifiableList(outputs);
|
||||
}
|
||||
|
||||
public void swapOutputs(int i, int j) {
|
||||
Collections.swap(outputs, i, j);
|
||||
public void shuffleOutputs() {
|
||||
Collections.shuffle(outputs);
|
||||
}
|
||||
|
||||
public TransactionOutput addOutput(long value, Script script) {
|
||||
@ -387,7 +382,7 @@ public class Transaction extends ChildMessage {
|
||||
}
|
||||
|
||||
public TransactionOutput addOutput(long value, ECKey pubkey) {
|
||||
return addOutput(new TransactionOutput(this, value, ScriptType.P2PK.getOutputScript(PolicyType.SINGLE_HD, pubkey)));
|
||||
return addOutput(new TransactionOutput(this, value, ScriptType.P2PK.getOutputScript(pubkey)));
|
||||
}
|
||||
|
||||
public TransactionOutput addOutput(TransactionOutput output) {
|
||||
@ -400,7 +395,7 @@ public class Transaction extends ChildMessage {
|
||||
public void verify() throws VerificationException {
|
||||
if (inputs.size() == 0 || outputs.size() == 0)
|
||||
throw new VerificationException.EmptyInputsOrOutputs();
|
||||
if (this.getMessageSize() > (MAX_BLOCK_SIZE_VBYTES * WITNESS_SCALE_FACTOR))
|
||||
if (this.getMessageSize() > MAX_BLOCK_SIZE)
|
||||
throw new VerificationException.LargerThanMaxBlockSize();
|
||||
|
||||
HashSet<TransactionOutPoint> outpoints = new HashSet<>();
|
||||
@ -442,18 +437,11 @@ public class Transaction extends ChildMessage {
|
||||
|
||||
public static boolean isTransaction(byte[] bytes) {
|
||||
//Incomplete quick test
|
||||
if(bytes.length <= 5 || bytes.length > (MAX_BLOCK_SIZE_VBYTES * WITNESS_SCALE_FACTOR)) {
|
||||
if(bytes.length == 0) {
|
||||
return false;
|
||||
}
|
||||
long version = Utils.readUint32(bytes, 0);
|
||||
if(version <= 0) {
|
||||
return false;
|
||||
}
|
||||
boolean segwit = (bytes[4] == 0);
|
||||
if(segwit && bytes[5] == 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
return version > 0 && version < 5;
|
||||
}
|
||||
|
||||
public Sha256Hash hashForLegacySignature(int inputIndex, Script redeemScript, SigHash sigHash) {
|
||||
|
||||
@ -7,7 +7,6 @@ import java.io.OutputStream;
|
||||
|
||||
public class TransactionInput extends ChildMessage {
|
||||
public static final long SEQUENCE_LOCKTIME_DISABLED = 4294967295L;
|
||||
public static final long SEQUENCE_RBF_DISABLED = 4294967294L;
|
||||
public static final long SEQUENCE_RBF_ENABLED = 4294967293L;
|
||||
public static final long MAX_RELATIVE_TIMELOCK = 2147483647L;
|
||||
public static final long RELATIVE_TIMELOCK_VALUE_MASK = 0xFFFF;
|
||||
|
||||
@ -60,20 +60,6 @@ public class TransactionOutput extends ChildMessage {
|
||||
return scriptBytes;
|
||||
}
|
||||
|
||||
public void setScriptBytes(byte[] scriptBytes) {
|
||||
super.payload = null;
|
||||
this.script = null;
|
||||
int oldLength = length;
|
||||
this.scriptBytes = scriptBytes;
|
||||
// 8 = value
|
||||
int newLength = 8 + (scriptBytes == null ? 1 : VarInt.sizeOf(scriptBytes.length) + scriptBytes.length);
|
||||
adjustLength(newLength - oldLength);
|
||||
}
|
||||
|
||||
public void clearScriptBytes() {
|
||||
setScriptBytes(new byte[0]);
|
||||
}
|
||||
|
||||
public Script getScript() {
|
||||
if(script == null) {
|
||||
script = new Script(scriptBytes);
|
||||
|
||||
@ -89,8 +89,12 @@ public class TransactionWitness extends ChildMessage {
|
||||
int length = new VarInt(pushes.size()).getSizeInBytes();
|
||||
for (int i = 0; i < pushes.size(); i++) {
|
||||
byte[] push = pushes.get(i);
|
||||
length += new VarInt(push.length).getSizeInBytes();
|
||||
length += push.length;
|
||||
if(push.length == 1 && push[0] == 0) {
|
||||
length++;
|
||||
} else {
|
||||
length += new VarInt(push.length).getSizeInBytes();
|
||||
length += push.length;
|
||||
}
|
||||
}
|
||||
|
||||
return length;
|
||||
@ -100,8 +104,12 @@ public class TransactionWitness extends ChildMessage {
|
||||
stream.write(new VarInt(pushes.size()).encode());
|
||||
for(int i = 0; i < pushes.size(); i++) {
|
||||
byte[] push = pushes.get(i);
|
||||
stream.write(new VarInt(push.length).encode());
|
||||
stream.write(push);
|
||||
if(push.length == 1 && push[0] == 0) {
|
||||
stream.write(push);
|
||||
} else {
|
||||
stream.write(new VarInt(push.length).encode());
|
||||
stream.write(push);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -9,8 +9,10 @@ import com.sparrowwallet.drongo.protocol.VarInt;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class PSBTEntry {
|
||||
private final byte[] key;
|
||||
@ -25,7 +27,7 @@ public class PSBTEntry {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public PSBTEntry(ByteBuffer psbtByteBuffer) throws PSBTParseException {
|
||||
PSBTEntry(ByteBuffer psbtByteBuffer) throws PSBTParseException {
|
||||
int keyLen = readCompactInt(psbtByteBuffer);
|
||||
|
||||
if (keyLen == 0x00) {
|
||||
@ -77,9 +79,6 @@ public class PSBTEntry {
|
||||
}
|
||||
|
||||
public static KeyDerivation parseKeyDerivation(byte[] data) throws PSBTParseException {
|
||||
if(data.length == 0) {
|
||||
return new KeyDerivation(KeyDerivation.DEFAULT_WATCH_ONLY_FINGERPRINT, Collections.emptyList());
|
||||
}
|
||||
if(data.length < 4) {
|
||||
throw new PSBTParseException("Invalid master fingerprint specified: not enough bytes");
|
||||
}
|
||||
@ -145,39 +144,6 @@ public class PSBTEntry {
|
||||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
public static Map<String, byte[]> parseDnssecProof(byte[] data) throws PSBTParseException {
|
||||
if(data.length == 0) {
|
||||
throw new PSBTParseException("No data provided for DNSSEC proof");
|
||||
}
|
||||
|
||||
ByteBuffer bb = ByteBuffer.wrap(data);
|
||||
int strLen = bb.get();
|
||||
if(data.length < strLen + 1) {
|
||||
throw new PSBTParseException("Invalid string length of " + strLen + " provided for DNSSEC proof");
|
||||
}
|
||||
|
||||
byte[] strBytes = new byte[strLen];
|
||||
bb.get(strBytes);
|
||||
String hrn = new String(strBytes, StandardCharsets.US_ASCII);
|
||||
byte[] proof = new byte[bb.remaining()];
|
||||
bb.get(proof);
|
||||
return Map.of(hrn, proof);
|
||||
}
|
||||
|
||||
public static byte[] serializeDnssecProof(Map<String, byte[]> dnssecProof) {
|
||||
if(dnssecProof.isEmpty()) {
|
||||
throw new IllegalArgumentException("No DNSSEC proof provided");
|
||||
}
|
||||
|
||||
String hrn = dnssecProof.keySet().iterator().next();
|
||||
byte[] proof = dnssecProof.get(hrn);
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
baos.write(hrn.length());
|
||||
baos.writeBytes(hrn.getBytes(StandardCharsets.US_ASCII));
|
||||
baos.writeBytes(proof);
|
||||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
static PSBTEntry populateEntry(int type, byte[] keyData, byte[] data) {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream(1 + (keyData == null ? 0 : keyData.length));
|
||||
baos.writeBytes(writeCompactInt(type));
|
||||
@ -293,16 +259,4 @@ public class PSBTEntry {
|
||||
throw new PSBTParseException("PSBT key type must be one byte plus x only pub key");
|
||||
}
|
||||
}
|
||||
|
||||
public void checkOneBytePlusRipe160Key() throws PSBTParseException {
|
||||
if(this.getKey().length != 21) {
|
||||
throw new PSBTParseException("PSBT key type must be one byte plus Ripe160MD hash");
|
||||
}
|
||||
}
|
||||
|
||||
public void checkOneBytePlusSha256Key() throws PSBTParseException {
|
||||
if(this.getKey().length != 33) {
|
||||
throw new PSBTParseException("PSBT key type must be one byte plus SHA256 hash");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,7 +4,6 @@ import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.protocol.*;
|
||||
import com.sparrowwallet.drongo.silentpayments.SilentPaymentsDLEQProof;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@ -27,23 +26,10 @@ public class PSBTInput {
|
||||
public static final byte PSBT_IN_FINAL_SCRIPTSIG = 0x07;
|
||||
public static final byte PSBT_IN_FINAL_SCRIPTWITNESS = 0x08;
|
||||
public static final byte PSBT_IN_POR_COMMITMENT = 0x09;
|
||||
public static final byte PSBT_IN_RIPEMD160 = 0x0a;
|
||||
public static final byte PSBT_IN_SHA256 = 0x0b;
|
||||
public static final byte PSBT_IN_HASH160 = 0x0c;
|
||||
public static final byte PSBT_IN_HASH256 = 0x0d;
|
||||
public static final byte PSBT_IN_PREVIOUS_TXID = 0x0e;
|
||||
public static final byte PSBT_IN_OUTPUT_INDEX = 0x0f;
|
||||
public static final byte PSBT_IN_SEQUENCE = 0x10;
|
||||
public static final byte PSBT_IN_REQUIRED_TIME_LOCKTIME = 0x11;
|
||||
public static final byte PSBT_IN_REQUIRED_HEIGHT_LOCKTIME = 0x12;
|
||||
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;
|
||||
public static final byte PSBT_IN_SP_ECDH_SHARE = 0x1d;
|
||||
public static final byte PSBT_IN_SP_DLEQ = 0x1e;
|
||||
public static final byte PSBT_IN_SP_SPEND_BIP32_DERIVATION = 0x1f;
|
||||
public static final byte PSBT_IN_SP_TWEAK = 0x20;
|
||||
public static final byte PSBT_IN_PROPRIETARY = (byte)0xfc;
|
||||
|
||||
private final PSBT psbt;
|
||||
private Transaction nonWitnessUtxo;
|
||||
@ -56,38 +42,24 @@ public class PSBTInput {
|
||||
private Script finalScriptSig;
|
||||
private TransactionWitness finalScriptWitness;
|
||||
private String porCommitment;
|
||||
private byte[] ripeMd160Preimage;
|
||||
private byte[] sha256Preimage;
|
||||
private byte[] hash160Preimage;
|
||||
private byte[] hash256Preimage;
|
||||
private final Map<String, String> proprietary = new LinkedHashMap<>();
|
||||
private TransactionSignature tapKeyPathSignature;
|
||||
private Map<ECKey, Map<KeyDerivation, List<Sha256Hash>>> tapDerivedPublicKeys = new LinkedHashMap<>();
|
||||
private ECKey tapInternalKey;
|
||||
|
||||
//PSBTv2-only fields
|
||||
private Sha256Hash prevTxid;
|
||||
private Long prevIndex;
|
||||
private Long sequence;
|
||||
private Long requiredTimeLocktime;
|
||||
private Long requiredHeightLocktime;
|
||||
private final Map<ECKey, ECKey> silentPaymentsEcdhShares = new LinkedHashMap<>();
|
||||
private final Map<ECKey, SilentPaymentsDLEQProof> silentPaymentsDLEQProofs = new LinkedHashMap<>();
|
||||
private final Map<ECKey, KeyDerivation> silentPaymentsSpendDerivations = new LinkedHashMap<>();
|
||||
private byte[] silentPaymentsTweak;
|
||||
|
||||
private final Transaction transaction;
|
||||
private int index;
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(PSBTInput.class);
|
||||
|
||||
PSBTInput(PSBT psbt, int index) {
|
||||
PSBTInput(PSBT psbt, Transaction transaction, int index) {
|
||||
this.psbt = psbt;
|
||||
this.transaction = transaction;
|
||||
this.index = index;
|
||||
}
|
||||
|
||||
PSBTInput(PSBT psbt, ScriptType scriptType, int index, Transaction utxo, int utxoIndex, Long sequence, Script redeemScript, Script witnessScript,
|
||||
Map<ECKey, KeyDerivation> derivedPublicKeys, Map<String, String> proprietary, ECKey tapInternalKey, boolean alwaysAddNonWitnessTx, byte[] silentPaymentsTweak, Map<ECKey, KeyDerivation> silentPaymentsSpendDerivations) {
|
||||
this(psbt, index);
|
||||
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);
|
||||
@ -96,7 +68,7 @@ public class PSBTInput {
|
||||
}
|
||||
|
||||
if(alwaysAddNonWitnessTx) {
|
||||
//Add non-witness UTXO to segwit v0 types to handle Trezor, Bitbox and Ledger requirements
|
||||
//Add non-witness UTXO to segwit types to handle Trezor, Bitbox and Ledger requirements
|
||||
this.nonWitnessUtxo = utxo;
|
||||
}
|
||||
|
||||
@ -116,42 +88,23 @@ public class PSBTInput {
|
||||
tapDerivedPublicKeys.put(this.tapInternalKey, Map.of(tapKeyDerivation, Collections.emptyList()));
|
||||
}
|
||||
|
||||
this.sigHash = (scriptType == P2TR ? SigHash.DEFAULT : SigHash.ALL);
|
||||
|
||||
//Populate PSBTv2 fields if parent PSBT is v2
|
||||
if(psbt.getPsbtVersion() >= 2) {
|
||||
this.prevTxid = utxo.getTxId();
|
||||
this.prevIndex = (long)utxoIndex;
|
||||
this.sequence = sequence;
|
||||
}
|
||||
|
||||
this.silentPaymentsTweak = silentPaymentsTweak;
|
||||
this.silentPaymentsSpendDerivations.putAll(silentPaymentsSpendDerivations);
|
||||
this.sigHash = getDefaultSigHash();
|
||||
}
|
||||
|
||||
PSBTInput(PSBT psbt, List<PSBTEntry> inputEntries, int index) throws PSBTParseException {
|
||||
this(psbt, index);
|
||||
List<PSBTEntry> sortedEntries = new ArrayList<>(inputEntries);
|
||||
sortedEntries.sort((o1, o2) -> {
|
||||
int found1 = o1.getKeyType() == PSBT_IN_PREVIOUS_TXID || o1.getKeyType() == PSBT_IN_OUTPUT_INDEX ? 1 : 0;
|
||||
int found2 = o2.getKeyType() == PSBT_IN_PREVIOUS_TXID || o2.getKeyType() == PSBT_IN_OUTPUT_INDEX ? 1 : 0;
|
||||
return found2 - found1;
|
||||
});
|
||||
|
||||
for(PSBTEntry entry : sortedEntries) {
|
||||
switch((byte)entry.getKeyType()) {
|
||||
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:
|
||||
entry.checkOneByteKey();
|
||||
Transaction nonWitnessTx = new Transaction(entry.getData());
|
||||
nonWitnessTx.verify();
|
||||
Sha256Hash inputHash = nonWitnessTx.calculateTxId(false);
|
||||
Sha256Hash outpointHash = getPrevTxid();
|
||||
if(outpointHash == null) {
|
||||
throw new PSBTParseException("Outpoint hash not present for input " + index);
|
||||
}
|
||||
Sha256Hash outpointHash = transaction.getInputs().get(index).getOutpoint().getHash();
|
||||
if(!outpointHash.equals(inputHash)) {
|
||||
throw new PSBTParseException("Hash of provided non witness utxo transaction " + inputHash + " does not match transaction input outpoint hash " + outpointHash + " at index " + index);
|
||||
}
|
||||
|
||||
this.nonWitnessUtxo = nonWitnessTx;
|
||||
log.debug("Found input non witness utxo with txid: " + nonWitnessTx.getTxId() + " version " + nonWitnessTx.getVersion() + " size " + nonWitnessTx.getMessageSize() + " locktime " + nonWitnessTx.getLocktime());
|
||||
for(TransactionInput input: nonWitnessTx.getInputs()) {
|
||||
@ -188,9 +141,6 @@ public class PSBTInput {
|
||||
break;
|
||||
case PSBT_IN_SIGHASH_TYPE:
|
||||
entry.checkOneByteKey();
|
||||
if(entry.getData().length != 4) {
|
||||
throw new PSBTParseException("PSBT input sighash type must be 4 bytes");
|
||||
}
|
||||
long sighashType = Utils.readUint32(entry.getData(), 0);
|
||||
SigHash sigHash = SigHash.fromByte((byte)sighashType);
|
||||
this.sigHash = sigHash;
|
||||
@ -201,11 +151,7 @@ public class PSBTInput {
|
||||
Script redeemScript = new Script(entry.getData());
|
||||
Script scriptPubKey = null;
|
||||
if(this.nonWitnessUtxo != null) {
|
||||
Long prevIndex = getPrevIndex();
|
||||
if(prevIndex == null) {
|
||||
throw new PSBTParseException("Outpoint index not present for input " + index);
|
||||
}
|
||||
scriptPubKey = this.nonWitnessUtxo.getOutputs().get(prevIndex.intValue()).getScript();
|
||||
scriptPubKey = this.nonWitnessUtxo.getOutputs().get((int)transaction.getInputs().get(index).getOutpoint().getIndex()).getScript();
|
||||
} else if(this.witnessUtxo != null) {
|
||||
scriptPubKey = this.witnessUtxo.getScript();
|
||||
if(!P2WPKH.isScriptType(redeemScript) && !P2WSH.isScriptType(redeemScript)) { //Witness UTXO should only be provided for P2SH-P2WPKH or P2SH-P2WSH
|
||||
@ -268,118 +214,6 @@ public class PSBTInput {
|
||||
this.porCommitment = porMessage;
|
||||
log.debug("Found input POR commitment message " + porMessage);
|
||||
break;
|
||||
case PSBT_IN_RIPEMD160:
|
||||
entry.checkOneBytePlusRipe160Key();
|
||||
if(!Arrays.equals(entry.getKeyData(), Ripemd160.getHash(entry.getData()))) {
|
||||
throw new PSBTParseException("Hash of PSBT_IN_RIPEMD160 preimage did not match provided hash " + Utils.bytesToHex(entry.getKeyData()) + " " + Utils.bytesToHex(entry.getData()));
|
||||
}
|
||||
this.ripeMd160Preimage = entry.getData();
|
||||
log.debug("Found input RIPEMD160 preimage " + Utils.bytesToHex(entry.getData()));
|
||||
break;
|
||||
case PSBT_IN_SHA256:
|
||||
entry.checkOneBytePlusSha256Key();
|
||||
if(!Arrays.equals(entry.getKeyData(), Sha256Hash.hash(entry.getData()))) {
|
||||
throw new PSBTParseException("Hash of PSBT_IN_SHA256 preimage did not match provided hash " + Utils.bytesToHex(entry.getKeyData()) + " " + Utils.bytesToHex(entry.getData()));
|
||||
}
|
||||
this.sha256Preimage = entry.getData();
|
||||
log.debug("Found input SHA256 preimage " + Utils.bytesToHex(entry.getData()));
|
||||
break;
|
||||
case PSBT_IN_HASH160:
|
||||
entry.checkOneBytePlusRipe160Key();
|
||||
if(!Arrays.equals(entry.getKeyData(), Utils.sha256hash160(entry.getData()))) {
|
||||
throw new PSBTParseException("Hash of PSBT_IN_HASH160 preimage did not match provided hash " + Utils.bytesToHex(entry.getKeyData()) + " " + Utils.bytesToHex(entry.getData()));
|
||||
}
|
||||
this.hash160Preimage = entry.getData();
|
||||
log.debug("Found input HASH160 preimage " + Utils.bytesToHex(entry.getData()));
|
||||
break;
|
||||
case PSBT_IN_HASH256:
|
||||
entry.checkOneBytePlusSha256Key();
|
||||
if(!Arrays.equals(entry.getKeyData(), Sha256Hash.hashTwice(entry.getData()))) {
|
||||
throw new PSBTParseException("Hash of PSBT_IN_HASH256 preimage did not match provided hash " + Utils.bytesToHex(entry.getKeyData()) + " " + Utils.bytesToHex(entry.getData()));
|
||||
}
|
||||
this.hash256Preimage = entry.getData();
|
||||
log.debug("Found input HASH256 preimage " + Utils.bytesToHex(entry.getData()));
|
||||
break;
|
||||
case PSBT_IN_PREVIOUS_TXID:
|
||||
entry.checkOneByteKey();
|
||||
this.prevTxid = Sha256Hash.wrap(Utils.reverseBytes(entry.getData()));
|
||||
log.debug("Found input previous txid " + Utils.bytesToHex(entry.getData()));
|
||||
break;
|
||||
case PSBT_IN_OUTPUT_INDEX:
|
||||
entry.checkOneByteKey();
|
||||
if(entry.getData().length != 4) {
|
||||
throw new PSBTParseException("PSBT input output index must be 4 bytes");
|
||||
}
|
||||
this.prevIndex = Utils.readUint32(entry.getData(), 0);
|
||||
log.debug("Found input previous output index " + this.prevIndex);
|
||||
break;
|
||||
case PSBT_IN_SEQUENCE:
|
||||
entry.checkOneByteKey();
|
||||
if(entry.getData().length != 4) {
|
||||
throw new PSBTParseException("PSBT input sequence must be 4 bytes");
|
||||
}
|
||||
this.sequence = Utils.readUint32(entry.getData(), 0);
|
||||
log.debug("Found input sequence " + this.sequence);
|
||||
break;
|
||||
case PSBT_IN_REQUIRED_TIME_LOCKTIME:
|
||||
entry.checkOneByteKey();
|
||||
if(entry.getData().length != 4) {
|
||||
throw new PSBTParseException("PSBT input required time locktime must be 4 bytes");
|
||||
}
|
||||
long requiredTimeLocktime = Utils.readUint32(entry.getData(), 0);
|
||||
if(requiredTimeLocktime < 500000000) {
|
||||
throw new PSBTParseException("Required time locktime is less than 500000000");
|
||||
}
|
||||
this.requiredTimeLocktime = requiredTimeLocktime;
|
||||
log.debug("Found input required time locktime " + this.requiredTimeLocktime);
|
||||
break;
|
||||
case PSBT_IN_REQUIRED_HEIGHT_LOCKTIME:
|
||||
entry.checkOneByteKey();
|
||||
if(entry.getData().length != 4) {
|
||||
throw new PSBTParseException("PSBT input required height locktime must be 4 bytes");
|
||||
}
|
||||
long requiredHeightLocktime = Utils.readUint32(entry.getData(), 0);
|
||||
if(requiredHeightLocktime >= 500000000) {
|
||||
throw new PSBTParseException("Required time locktime is greater than or equal to 500000000");
|
||||
}
|
||||
this.requiredHeightLocktime = requiredHeightLocktime;
|
||||
log.debug("Found input required height locktime " + this.requiredHeightLocktime);
|
||||
break;
|
||||
case PSBT_IN_SP_ECDH_SHARE:
|
||||
entry.checkOneBytePlusPubKey();
|
||||
if(entry.getData().length != 33) {
|
||||
throw new PSBTParseException("PSBT input silent payments ECDH share data must be 33 bytes");
|
||||
}
|
||||
ECKey inputScanKey = ECKey.fromPublicOnly(entry.getKeyData());
|
||||
ECKey inputEcdhShare = ECKey.fromPublicOnly(entry.getData());
|
||||
this.silentPaymentsEcdhShares.put(inputScanKey, inputEcdhShare);
|
||||
log.debug("Found input silent payments ECDH share for scan key: " + Utils.bytesToHex(entry.getKeyData()));
|
||||
break;
|
||||
case PSBT_IN_SP_DLEQ:
|
||||
entry.checkOneBytePlusPubKey();
|
||||
if(entry.getData().length != 64) {
|
||||
throw new PSBTParseException("PSBT input silent payments DLEQ proof data must be 64 bytes");
|
||||
}
|
||||
ECKey inputProofScanKey = ECKey.fromPublicOnly(entry.getKeyData());
|
||||
SilentPaymentsDLEQProof inputDleqProof = SilentPaymentsDLEQProof.fromBytes(entry.getData());
|
||||
this.silentPaymentsDLEQProofs.put(inputProofScanKey, inputDleqProof);
|
||||
log.debug("Found input silent payments DLEQ proof for scan key: " + Utils.bytesToHex(entry.getKeyData()));
|
||||
break;
|
||||
case PSBT_IN_SP_SPEND_BIP32_DERIVATION:
|
||||
entry.checkOneBytePlusPubKey();
|
||||
ECKey spSpendPubKey = ECKey.fromPublicOnly(entry.getKeyData());
|
||||
KeyDerivation spSpendKeyDerivation = PSBTEntry.parseKeyDerivation(entry.getData());
|
||||
this.silentPaymentsSpendDerivations.put(spSpendPubKey, spSpendKeyDerivation);
|
||||
log.debug("Found input silent payments BIP32 derivation for spend key: " + Utils.bytesToHex(entry.getKeyData()));
|
||||
break;
|
||||
case PSBT_IN_SP_TWEAK:
|
||||
entry.checkOneByteKey();
|
||||
if(entry.getData().length != 32) {
|
||||
throw new PSBTParseException("PSBT input silent payments tweak must be 32 bytes");
|
||||
}
|
||||
this.silentPaymentsTweak = entry.getData();
|
||||
log.debug("Found input silent payments tweak");
|
||||
break;
|
||||
case PSBT_IN_PROPRIETARY:
|
||||
this.proprietary.put(Utils.bytesToHex(entry.getKeyData()), Utils.bytesToHex(entry.getData()));
|
||||
log.debug("Found proprietary input " + Utils.bytesToHex(entry.getKeyData()) + ": " + Utils.bytesToHex(entry.getData()));
|
||||
@ -411,9 +245,12 @@ public class PSBTInput {
|
||||
log.warn("PSBT input not recognized key type: " + entry.getKeyType());
|
||||
}
|
||||
}
|
||||
|
||||
this.transaction = transaction;
|
||||
this.index = index;
|
||||
}
|
||||
|
||||
public List<PSBTEntry> getInputEntries(int psbtVersion) {
|
||||
public List<PSBTEntry> getInputEntries() {
|
||||
List<PSBTEntry> entries = new ArrayList<>();
|
||||
|
||||
if(nonWitnessUtxo != null) {
|
||||
@ -459,44 +296,6 @@ public class PSBTInput {
|
||||
entries.add(populateEntry(PSBT_IN_POR_COMMITMENT, null, porCommitment.getBytes(StandardCharsets.UTF_8)));
|
||||
}
|
||||
|
||||
if(psbtVersion >= 2) {
|
||||
if(prevTxid != null) {
|
||||
entries.add(populateEntry(PSBT_IN_PREVIOUS_TXID, null, Utils.reverseBytes(prevTxid.getBytes())));
|
||||
}
|
||||
if(prevIndex != null) {
|
||||
byte[] prevIndexBytes = new byte[4];
|
||||
Utils.uint32ToByteArrayLE(prevIndex, prevIndexBytes, 0);
|
||||
entries.add(populateEntry(PSBT_IN_OUTPUT_INDEX, null, prevIndexBytes));
|
||||
}
|
||||
if(sequence != null) {
|
||||
byte[] sequenceBytes = new byte[4];
|
||||
Utils.uint32ToByteArrayLE(sequence, sequenceBytes, 0);
|
||||
entries.add(populateEntry(PSBT_IN_SEQUENCE, null, sequenceBytes));
|
||||
}
|
||||
if(requiredTimeLocktime != null) {
|
||||
byte[] requiredTimeLocktimeBytes = new byte[4];
|
||||
Utils.uint32ToByteArrayLE(requiredTimeLocktime, requiredTimeLocktimeBytes, 0);
|
||||
entries.add(populateEntry(PSBT_IN_REQUIRED_TIME_LOCKTIME, null, requiredTimeLocktimeBytes));
|
||||
}
|
||||
if(requiredHeightLocktime != null) {
|
||||
byte[] requiredHeightLocktimeBytes = new byte[4];
|
||||
Utils.uint32ToByteArrayLE(requiredHeightLocktime, requiredHeightLocktimeBytes, 0);
|
||||
entries.add(populateEntry(PSBT_IN_REQUIRED_HEIGHT_LOCKTIME, null, requiredHeightLocktimeBytes));
|
||||
}
|
||||
for(Map.Entry<ECKey, ECKey> entry : silentPaymentsEcdhShares.entrySet()) {
|
||||
entries.add(populateEntry(PSBT_IN_SP_ECDH_SHARE, entry.getKey().getPubKey(), entry.getValue().getPubKey()));
|
||||
}
|
||||
for(Map.Entry<ECKey, SilentPaymentsDLEQProof> entry : silentPaymentsDLEQProofs.entrySet()) {
|
||||
entries.add(populateEntry(PSBT_IN_SP_DLEQ, entry.getKey().getPubKey(), entry.getValue().getBytes()));
|
||||
}
|
||||
for(Map.Entry<ECKey, KeyDerivation> entry : silentPaymentsSpendDerivations.entrySet()) {
|
||||
entries.add(populateEntry(PSBT_IN_SP_SPEND_BIP32_DERIVATION, entry.getKey().getPubKey(), serializeKeyDerivation(entry.getValue())));
|
||||
}
|
||||
if(silentPaymentsTweak != null) {
|
||||
entries.add(populateEntry(PSBT_IN_SP_TWEAK, null, silentPaymentsTweak));
|
||||
}
|
||||
}
|
||||
|
||||
for(Map.Entry<String, String> entry : proprietary.entrySet()) {
|
||||
entries.add(populateEntry(PSBT_IN_PROPRIETARY, Utils.hexToBytes(entry.getKey()), Utils.hexToBytes(entry.getValue())));
|
||||
}
|
||||
@ -547,51 +346,6 @@ public class PSBTInput {
|
||||
porCommitment = psbtInput.porCommitment;
|
||||
}
|
||||
|
||||
if(psbtInput.ripeMd160Preimage != null) {
|
||||
ripeMd160Preimage = psbtInput.ripeMd160Preimage;
|
||||
}
|
||||
|
||||
if(psbtInput.sha256Preimage != null) {
|
||||
sha256Preimage = psbtInput.sha256Preimage;
|
||||
}
|
||||
|
||||
if(psbtInput.hash160Preimage != null) {
|
||||
hash160Preimage = psbtInput.hash160Preimage;
|
||||
}
|
||||
|
||||
if(psbtInput.hash256Preimage != null) {
|
||||
hash256Preimage = psbtInput.hash256Preimage;
|
||||
}
|
||||
|
||||
if(psbtInput.prevTxid != null) {
|
||||
prevTxid = psbtInput.prevTxid;
|
||||
}
|
||||
|
||||
if(psbtInput.prevIndex != null) {
|
||||
prevIndex = psbtInput.prevIndex;
|
||||
}
|
||||
|
||||
if(psbtInput.sequence != null) {
|
||||
sequence = psbtInput.sequence;
|
||||
}
|
||||
|
||||
if(psbtInput.requiredTimeLocktime != null) {
|
||||
requiredTimeLocktime = psbtInput.requiredTimeLocktime;
|
||||
}
|
||||
|
||||
if(psbtInput.requiredHeightLocktime != null) {
|
||||
requiredHeightLocktime = psbtInput.requiredHeightLocktime;
|
||||
}
|
||||
|
||||
silentPaymentsEcdhShares.putAll(psbtInput.silentPaymentsEcdhShares);
|
||||
silentPaymentsDLEQProofs.putAll(psbtInput.silentPaymentsDLEQProofs);
|
||||
|
||||
silentPaymentsSpendDerivations.putAll(psbtInput.silentPaymentsSpendDerivations);
|
||||
|
||||
if(psbtInput.silentPaymentsTweak != null) {
|
||||
silentPaymentsTweak = psbtInput.silentPaymentsTweak;
|
||||
}
|
||||
|
||||
proprietary.putAll(psbtInput.proprietary);
|
||||
|
||||
if(psbtInput.tapKeyPathSignature != null) {
|
||||
@ -727,122 +481,6 @@ public class PSBTInput {
|
||||
return getUtxo() != null && getScriptType() == P2TR;
|
||||
}
|
||||
|
||||
public byte[] getRipeMd160Preimage() {
|
||||
return ripeMd160Preimage;
|
||||
}
|
||||
|
||||
public void setRipeMd160Preimage(byte[] ripeMd160Preimage) {
|
||||
this.ripeMd160Preimage = ripeMd160Preimage;
|
||||
}
|
||||
|
||||
public byte[] getSha256Preimage() {
|
||||
return sha256Preimage;
|
||||
}
|
||||
|
||||
public void setSha256Preimage(byte[] sha256Preimage) {
|
||||
this.sha256Preimage = sha256Preimage;
|
||||
}
|
||||
|
||||
public byte[] getHash160Preimage() {
|
||||
return hash160Preimage;
|
||||
}
|
||||
|
||||
public void setHash160Preimage(byte[] hash160Preimage) {
|
||||
this.hash160Preimage = hash160Preimage;
|
||||
}
|
||||
|
||||
public byte[] getHash256Preimage() {
|
||||
return hash256Preimage;
|
||||
}
|
||||
|
||||
public void setHash256Preimage(byte[] hash256Preimage) {
|
||||
this.hash256Preimage = hash256Preimage;
|
||||
}
|
||||
|
||||
public Sha256Hash getPrevTxid() {
|
||||
if(psbt.getPsbtVersion() >= 2) {
|
||||
return prevTxid;
|
||||
}
|
||||
|
||||
return getInput().getOutpoint().getHash();
|
||||
}
|
||||
|
||||
Sha256Hash prevTxid() {
|
||||
return prevTxid;
|
||||
}
|
||||
|
||||
public void setPrevTxid(Sha256Hash prevTxid) {
|
||||
this.prevTxid = prevTxid;
|
||||
}
|
||||
|
||||
public Long getPrevIndex() {
|
||||
if(psbt.getPsbtVersion() >= 2) {
|
||||
return prevIndex;
|
||||
}
|
||||
|
||||
return getInput().getOutpoint().getIndex();
|
||||
}
|
||||
|
||||
Long prevIndex() {
|
||||
return prevIndex;
|
||||
}
|
||||
|
||||
public void setPrevIndex(Long prevIndex) {
|
||||
this.prevIndex = prevIndex;
|
||||
}
|
||||
|
||||
public Long getSequence() {
|
||||
if(psbt.getPsbtVersion() >= 2) {
|
||||
return sequence;
|
||||
}
|
||||
|
||||
return getInput().getSequenceNumber();
|
||||
}
|
||||
|
||||
Long sequence() {
|
||||
return sequence;
|
||||
}
|
||||
|
||||
public void setSequence(Long sequence) {
|
||||
this.sequence = sequence;
|
||||
}
|
||||
|
||||
public Long getRequiredTimeLocktime() {
|
||||
return requiredTimeLocktime;
|
||||
}
|
||||
|
||||
public void setRequiredTimeLocktime(Long requiredTimeLocktime) {
|
||||
this.requiredTimeLocktime = requiredTimeLocktime;
|
||||
}
|
||||
|
||||
public Long getRequiredHeightLocktime() {
|
||||
return requiredHeightLocktime;
|
||||
}
|
||||
|
||||
public void setRequiredHeightLocktime(Long requiredHeightLocktime) {
|
||||
this.requiredHeightLocktime = requiredHeightLocktime;
|
||||
}
|
||||
|
||||
public Map<ECKey, ECKey> getSilentPaymentsEcdhShares() {
|
||||
return silentPaymentsEcdhShares;
|
||||
}
|
||||
|
||||
public Map<ECKey, SilentPaymentsDLEQProof> getSilentPaymentsDLEQProofs() {
|
||||
return silentPaymentsDLEQProofs;
|
||||
}
|
||||
|
||||
public Map<ECKey, KeyDerivation> getSilentPaymentsSpendDerivations() {
|
||||
return silentPaymentsSpendDerivations;
|
||||
}
|
||||
|
||||
public byte[] getSilentPaymentsTweak() {
|
||||
return silentPaymentsTweak;
|
||||
}
|
||||
|
||||
public void setSilentPaymentsTweak(byte[] silentPaymentsTweak) {
|
||||
this.silentPaymentsTweak = silentPaymentsTweak;
|
||||
}
|
||||
|
||||
public boolean isSigned() {
|
||||
if(getTapKeyPathSignature() != null) {
|
||||
return true;
|
||||
@ -880,26 +518,6 @@ public class PSBTInput {
|
||||
return SigHash.ALL;
|
||||
}
|
||||
|
||||
public boolean signSilentPayments(ECKey spendPrivateKey) {
|
||||
if(getSilentPaymentsTweak() == null || getWitnessUtxo() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ECKey tweakKey = ECKey.fromPrivate(getSilentPaymentsTweak());
|
||||
ECKey tweakedKey = spendPrivateKey.addPrivate(tweakKey);
|
||||
|
||||
if(tweakedKey.hasOddYCoord()) {
|
||||
tweakedKey = tweakedKey.negatePrivate();
|
||||
}
|
||||
|
||||
ECKey outputKey = ScriptType.P2TR.getPublicKeyFromScript(getWitnessUtxo().getScript());
|
||||
if(!Arrays.equals(tweakedKey.getPubKeyXCoord(), outputKey.getPubKeyXCoord())) {
|
||||
throw new IllegalStateException("Tweaked spend key does not match output key");
|
||||
}
|
||||
|
||||
return sign(tweakedKey);
|
||||
}
|
||||
|
||||
public boolean sign(ECKey privKey) {
|
||||
return sign(new PSBTInputSigner() {
|
||||
@Override
|
||||
@ -923,12 +541,6 @@ public class PSBTInput {
|
||||
if(getNonWitnessUtxo() != null || getWitnessUtxo() != null) {
|
||||
Script signingScript = getSigningScript();
|
||||
if(signingScript != null) {
|
||||
if((localSigHash == SigHash.SINGLE || localSigHash == SigHash.ANYONECANPAY_SINGLE) && index >= psbt.getTransaction().getOutputs().size()
|
||||
&& Arrays.asList(NON_WITNESS_TYPES).contains(getScriptType())) {
|
||||
throw new IllegalStateException("Refusing to sign SIGHASH_SINGLE on legacy input " + index
|
||||
+ " with only " + psbt.getTransaction().getOutputs().size() + " output(s) as it would produce a re-broadcastable signature");
|
||||
}
|
||||
|
||||
Sha256Hash hash = getHashForSignature(signingScript, localSigHash);
|
||||
TransactionSignature.Type type = isTaproot() ? SCHNORR : ECDSA;
|
||||
TransactionSignature transactionSignature = psbtInputSigner.sign(hash, localSigHash, type);
|
||||
@ -947,27 +559,6 @@ public class PSBTInput {
|
||||
return false;
|
||||
}
|
||||
|
||||
void verifySigHash() throws PSBTSignatureException {
|
||||
if(sigHash == null || sigHash == SigHash.ALL || sigHash == SigHash.DEFAULT) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch(sigHash) {
|
||||
case NONE:
|
||||
throw new PSBTSignatureException("Input " + index + " requests SIGHASH_NONE. The signature does not commit to any of the outputs, and can be re-used on a transaction with completely different outputs.");
|
||||
case ANYONECANPAY_NONE:
|
||||
throw new PSBTSignatureException("Input " + index + " requests SIGHASH_NONE | ANYONECANPAY. The signature commits to neither inputs nor outputs and can be re-used in nearly any transaction.");
|
||||
case ANYONECANPAY_SINGLE:
|
||||
throw new PSBTSignatureException("Input " + index + " requests SIGHASH_SINGLE | ANYONECANPAY. The signature only commits to one output, and other inputs may be added after signing.");
|
||||
case SINGLE:
|
||||
throw new PSBTSignatureException("Input " + index + " requests SIGHASH_SINGLE. The signature only commits to the output at the same index, allowing other outputs to be added or modified after signing.");
|
||||
case ANYONECANPAY_ALL:
|
||||
throw new PSBTSignatureException("Input " + index + " requests SIGHASH_ALL | ANYONECANPAY. Other inputs may be added to the transaction after signing, potentially redirecting value through fees.");
|
||||
case ANYONECANPAY:
|
||||
throw new PSBTSignatureException("Input " + index + " requests a non-standard ANYONECANPAY sighash with no base type. The resulting signature has unpredictable commitment semantics.");
|
||||
}
|
||||
}
|
||||
|
||||
boolean verifySignatures() throws PSBTSignatureException {
|
||||
SigHash localSigHash = getSigHash();
|
||||
if(localSigHash == null) {
|
||||
@ -1042,8 +633,6 @@ public class PSBTInput {
|
||||
return p2sh ? P2SH_P2WPKH : P2WPKH;
|
||||
} else if(P2WSH.isScriptType(signingScript)) {
|
||||
return p2sh ? P2SH_P2WSH : P2WSH;
|
||||
} else if(MULTISIG.isScriptType(signingScript)) {
|
||||
return p2sh ? P2SH : MULTISIG;
|
||||
}
|
||||
|
||||
return ScriptType.getType(signingScript);
|
||||
@ -1087,7 +676,7 @@ public class PSBTInput {
|
||||
}
|
||||
|
||||
public TransactionInput getInput() {
|
||||
return psbt.getTransaction().getInputs().get(index);
|
||||
return transaction.getInputs().get(index);
|
||||
}
|
||||
|
||||
public TransactionOutput getUtxo() {
|
||||
@ -1095,10 +684,6 @@ public class PSBTInput {
|
||||
return getWitnessUtxo() != null ? getWitnessUtxo() : (getNonWitnessUtxo() != null ? getNonWitnessUtxo().getOutputs().get(vout) : null);
|
||||
}
|
||||
|
||||
int getIndex() {
|
||||
return index;
|
||||
}
|
||||
|
||||
void setIndex(int index) {
|
||||
this.index = index;
|
||||
}
|
||||
@ -1112,10 +697,6 @@ public class PSBTInput {
|
||||
proprietary.clear();
|
||||
tapDerivedPublicKeys.clear();
|
||||
tapKeyPathSignature = null;
|
||||
silentPaymentsEcdhShares.clear();
|
||||
silentPaymentsDLEQProofs.clear();
|
||||
silentPaymentsSpendDerivations.clear();
|
||||
silentPaymentsTweak = null;
|
||||
}
|
||||
|
||||
private Sha256Hash getHashForSignature(Script connectedScript, SigHash localSigHash) {
|
||||
@ -1124,12 +705,12 @@ public class PSBTInput {
|
||||
ScriptType scriptType = getScriptType();
|
||||
if(scriptType == ScriptType.P2TR) {
|
||||
List<TransactionOutput> spentUtxos = psbt.getPsbtInputs().stream().map(PSBTInput::getUtxo).collect(Collectors.toList());
|
||||
hash = psbt.getTransaction().hashForTaprootSignature(spentUtxos, index, !P2TR.isScriptType(connectedScript), connectedScript, localSigHash, null);
|
||||
hash = transaction.hashForTaprootSignature(spentUtxos, index, !P2TR.isScriptType(connectedScript), connectedScript, localSigHash, null);
|
||||
} else if(Arrays.asList(WITNESS_TYPES).contains(scriptType)) {
|
||||
long prevValue = getUtxo().getValue();
|
||||
hash = psbt.getTransaction().hashForWitnessSignature(index, connectedScript, prevValue, localSigHash);
|
||||
hash = transaction.hashForWitnessSignature(index, connectedScript, prevValue, localSigHash);
|
||||
} else {
|
||||
hash = psbt.getTransaction().hashForLegacySignature(index, connectedScript, localSigHash);
|
||||
hash = transaction.hashForLegacySignature(index, connectedScript, localSigHash);
|
||||
}
|
||||
|
||||
return hash;
|
||||
|
||||
@ -3,18 +3,13 @@ package com.sparrowwallet.drongo.psbt;
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.dns.DnsPayment;
|
||||
import com.sparrowwallet.drongo.dns.DnsPaymentResolver;
|
||||
import com.sparrowwallet.drongo.dns.DnsPaymentValidationException;
|
||||
import com.sparrowwallet.drongo.protocol.*;
|
||||
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
|
||||
import com.sparrowwallet.drongo.uri.BitcoinURIParseException;
|
||||
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.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import static com.sparrowwallet.drongo.protocol.ScriptType.*;
|
||||
import static com.sparrowwallet.drongo.psbt.PSBTEntry.*;
|
||||
@ -23,13 +18,8 @@ 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_AMOUNT = 0x03;
|
||||
public static final byte PSBT_OUT_SCRIPT = 0x04;
|
||||
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_SP_V0_INFO = 0x09;
|
||||
public static final byte PSBT_OUT_SP_V0_LABEL = 0x0a;
|
||||
public static final byte PSBT_OUT_DNSSEC_PROOF = 0x35;
|
||||
public static final byte PSBT_OUT_PROPRIETARY = (byte)0xfc;
|
||||
|
||||
private Script redeemScript;
|
||||
@ -38,28 +28,14 @@ public class PSBTOutput {
|
||||
private final Map<String, String> proprietary = new LinkedHashMap<>();
|
||||
private Map<ECKey, Map<KeyDerivation, List<Sha256Hash>>> tapDerivedPublicKeys = new LinkedHashMap<>();
|
||||
private ECKey tapInternalKey;
|
||||
private Map<String, byte[]> dnssecProof;
|
||||
|
||||
//PSBTv2-only fields
|
||||
private Long amount;
|
||||
private Script script;
|
||||
private SilentPaymentAddress silentPaymentAddress;
|
||||
private Long silentPaymentLabel;
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(PSBTOutput.class);
|
||||
|
||||
private final PSBT psbt;
|
||||
private int index;
|
||||
|
||||
PSBTOutput(PSBT psbt, int index) {
|
||||
this.psbt = psbt;
|
||||
this.index = index;
|
||||
PSBTOutput() {
|
||||
//empty constructor
|
||||
}
|
||||
|
||||
PSBTOutput(PSBT psbt, int index, ScriptType scriptType, Long amount, Script script, Script redeemScript, Script witnessScript, Map<ECKey, KeyDerivation> derivedPublicKeys,
|
||||
Map<String, String> proprietary, ECKey tapInternalKey, SilentPaymentAddress silentPaymentAddress, Long silentPaymentLabel, Map<String, byte[]> dnssecProof) {
|
||||
this(psbt, index);
|
||||
|
||||
PSBTOutput(ScriptType scriptType, Script redeemScript, Script witnessScript, Map<ECKey, KeyDerivation> derivedPublicKeys, Map<String, String> proprietary, ECKey tapInternalKey) {
|
||||
this.redeemScript = redeemScript;
|
||||
this.witnessScript = witnessScript;
|
||||
|
||||
@ -75,24 +51,11 @@ public class PSBTOutput {
|
||||
KeyDerivation tapKeyDerivation = derivedPublicKeys.values().iterator().next();
|
||||
tapDerivedPublicKeys.put(this.tapInternalKey, Map.of(tapKeyDerivation, Collections.emptyList()));
|
||||
}
|
||||
|
||||
this.silentPaymentAddress = silentPaymentAddress;
|
||||
this.silentPaymentLabel = silentPaymentLabel;
|
||||
this.dnssecProof = dnssecProof;
|
||||
|
||||
//Populate PSBTv2 fields if parent PSBT is v2
|
||||
if(psbt.getPsbtVersion() >= 2) {
|
||||
this.amount = amount;
|
||||
if(!script.isEmpty() || silentPaymentAddress == null) {
|
||||
this.script = script;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PSBTOutput(PSBT psbt, List<PSBTEntry> outputEntries, int index) throws PSBTParseException {
|
||||
this(psbt, index);
|
||||
PSBTOutput(List<PSBTEntry> outputEntries) throws PSBTParseException {
|
||||
for(PSBTEntry entry : outputEntries) {
|
||||
switch((byte)entry.getKeyType()) {
|
||||
switch (entry.getKeyType()) {
|
||||
case PSBT_OUT_REDEEM_SCRIPT:
|
||||
entry.checkOneByteKey();
|
||||
Script redeemScript = new Script(entry.getData());
|
||||
@ -112,20 +75,6 @@ public class PSBTOutput {
|
||||
this.derivedPublicKeys.put(derivedPublicKey, keyDerivation);
|
||||
log.debug("Found output bip32_derivation with master fingerprint " + keyDerivation.getMasterFingerprint() + " at path " + keyDerivation.getDerivationPath() + " public key " + derivedPublicKey);
|
||||
break;
|
||||
case PSBT_OUT_AMOUNT:
|
||||
entry.checkOneByteKey();
|
||||
if(entry.getData().length != 8) {
|
||||
throw new PSBTParseException("PSBT output amount must be 8 bytes");
|
||||
}
|
||||
this.amount = Utils.readInt64(entry.getData(), 0);
|
||||
log.debug("Found output amount " + this.amount);
|
||||
break;
|
||||
case PSBT_OUT_SCRIPT:
|
||||
entry.checkOneByteKey();
|
||||
Script script = new Script(entry.getData());
|
||||
this.script = script;
|
||||
log.debug("Found output script hex " + Utils.bytesToHex(script.getProgram()) + " script " + script);
|
||||
break;
|
||||
case PSBT_OUT_PROPRIETARY:
|
||||
proprietary.put(Utils.bytesToHex(entry.getKeyData()), Utils.bytesToHex(entry.getData()));
|
||||
log.debug("Found proprietary output " + Utils.bytesToHex(entry.getKeyData()) + ": " + Utils.bytesToHex(entry.getData()));
|
||||
@ -148,37 +97,13 @@ public class PSBTOutput {
|
||||
}
|
||||
}
|
||||
break;
|
||||
case PSBT_OUT_SP_V0_INFO:
|
||||
entry.checkOneByteKey();
|
||||
if(entry.getData().length != 66) {
|
||||
throw new PSBTParseException("PSBT output info data for silent payments address must contain 66 bytes");
|
||||
}
|
||||
byte[] scanKey = new byte[33];
|
||||
System.arraycopy(entry.getData(), 0, scanKey, 0, 33);
|
||||
byte[] spendKey = new byte[33];
|
||||
System.arraycopy(entry.getData(), 33, spendKey, 0, 33);
|
||||
this.silentPaymentAddress = new SilentPaymentAddress(ECKey.fromPublicOnly(scanKey), ECKey.fromPublicOnly(spendKey));
|
||||
log.debug("Found output silent payment address " + this.silentPaymentAddress);
|
||||
break;
|
||||
case PSBT_OUT_SP_V0_LABEL:
|
||||
entry.checkOneByteKey();
|
||||
if(entry.getData().length != 4) {
|
||||
throw new PSBTParseException("PSBT output silent payment label must be 4 bytes");
|
||||
}
|
||||
this.silentPaymentLabel = Utils.readUint32(entry.getData(), 0);
|
||||
log.debug("Found output silent payment label " + this.silentPaymentLabel);
|
||||
break;
|
||||
case PSBT_OUT_DNSSEC_PROOF:
|
||||
entry.checkOneByteKey();
|
||||
this.dnssecProof = parseDnssecProof(entry.getData());
|
||||
break;
|
||||
default:
|
||||
log.warn("PSBT output not recognized key type: " + entry.getKeyType());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<PSBTEntry> getOutputEntries(int psbtVersion) {
|
||||
public List<PSBTEntry> getOutputEntries() {
|
||||
List<PSBTEntry> entries = new ArrayList<>();
|
||||
|
||||
if(redeemScript != null) {
|
||||
@ -193,25 +118,6 @@ public class PSBTOutput {
|
||||
entries.add(populateEntry(PSBT_OUT_BIP32_DERIVATION, entry.getKey().getPubKey(), serializeKeyDerivation(entry.getValue())));
|
||||
}
|
||||
|
||||
if(psbtVersion >= 2) {
|
||||
if(amount != null) {
|
||||
byte[] amountBytes = new byte[8];
|
||||
Utils.int64ToByteArrayLE(amount, amountBytes, 0);
|
||||
entries.add(populateEntry(PSBT_OUT_AMOUNT, null, amountBytes));
|
||||
}
|
||||
if(script != null) {
|
||||
entries.add(populateEntry(PSBT_OUT_SCRIPT, null, script.getProgram()));
|
||||
}
|
||||
if(silentPaymentAddress != null) {
|
||||
entries.add(populateEntry(PSBT_OUT_SP_V0_INFO, null, Utils.concat(silentPaymentAddress.getScanKey().getPubKey(), silentPaymentAddress.getSpendKey().getPubKey())));
|
||||
}
|
||||
if(silentPaymentLabel != null) {
|
||||
byte[] labelBytes = new byte[4];
|
||||
Utils.uint32ToByteArrayLE(silentPaymentLabel, labelBytes, 0);
|
||||
entries.add(populateEntry(PSBT_OUT_SP_V0_LABEL, null, labelBytes));
|
||||
}
|
||||
}
|
||||
|
||||
for(Map.Entry<String, String> entry : proprietary.entrySet()) {
|
||||
entries.add(populateEntry(PSBT_OUT_PROPRIETARY, Utils.hexToBytes(entry.getKey()), Utils.hexToBytes(entry.getValue())));
|
||||
}
|
||||
@ -226,10 +132,6 @@ public class PSBTOutput {
|
||||
entries.add(populateEntry(PSBT_OUT_TAP_INTERNAL_KEY, null, tapInternalKey.getPubKeyXCoord()));
|
||||
}
|
||||
|
||||
if(dnssecProof != null) {
|
||||
entries.add(populateEntry(PSBT_OUT_DNSSEC_PROOF, null, serializeDnssecProof(dnssecProof)));
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
@ -243,30 +145,13 @@ public class PSBTOutput {
|
||||
}
|
||||
|
||||
derivedPublicKeys.putAll(psbtOutput.derivedPublicKeys);
|
||||
|
||||
if(psbtOutput.amount != null) {
|
||||
amount = psbtOutput.amount;
|
||||
}
|
||||
|
||||
if(psbtOutput.script != null) {
|
||||
script = psbtOutput.script;
|
||||
}
|
||||
proprietary.putAll(psbtOutput.proprietary);
|
||||
|
||||
tapDerivedPublicKeys.putAll(psbtOutput.tapDerivedPublicKeys);
|
||||
|
||||
if(psbtOutput.tapInternalKey != null) {
|
||||
tapInternalKey = psbtOutput.tapInternalKey;
|
||||
}
|
||||
|
||||
if(psbtOutput.silentPaymentAddress != null) {
|
||||
silentPaymentAddress = psbtOutput.silentPaymentAddress;
|
||||
}
|
||||
|
||||
if(psbtOutput.silentPaymentLabel != null) {
|
||||
silentPaymentLabel = psbtOutput.silentPaymentLabel;
|
||||
}
|
||||
|
||||
proprietary.putAll(psbtOutput.proprietary);
|
||||
}
|
||||
|
||||
public Script getRedeemScript() {
|
||||
@ -293,38 +178,6 @@ public class PSBTOutput {
|
||||
return derivedPublicKeys;
|
||||
}
|
||||
|
||||
public Long getAmount() {
|
||||
if(psbt.getPsbtVersion() >= 2) {
|
||||
return amount;
|
||||
}
|
||||
|
||||
return getOutput().getValue();
|
||||
}
|
||||
|
||||
Long amount() {
|
||||
return amount;
|
||||
}
|
||||
|
||||
public void setAmount(Long amount) {
|
||||
this.amount = amount;
|
||||
}
|
||||
|
||||
public Script getScript() {
|
||||
if(psbt.getPsbtVersion() >= 2) {
|
||||
return script;
|
||||
}
|
||||
|
||||
return getOutput().getScript();
|
||||
}
|
||||
|
||||
Script script() {
|
||||
return script;
|
||||
}
|
||||
|
||||
public void setScript(Script script) {
|
||||
this.script = script;
|
||||
}
|
||||
|
||||
public Map<String, String> getProprietary() {
|
||||
return proprietary;
|
||||
}
|
||||
@ -345,48 +198,6 @@ public class PSBTOutput {
|
||||
this.tapInternalKey = tapInternalKey;
|
||||
}
|
||||
|
||||
public SilentPaymentAddress getSilentPaymentAddress() {
|
||||
return silentPaymentAddress;
|
||||
}
|
||||
|
||||
public void setSilentPaymentAddress(SilentPaymentAddress silentPaymentAddress) {
|
||||
this.silentPaymentAddress = silentPaymentAddress;
|
||||
}
|
||||
|
||||
public Long getSilentPaymentLabel() {
|
||||
return silentPaymentLabel;
|
||||
}
|
||||
|
||||
public void setSilentPaymentLabel(Long silentPaymentLabel) {
|
||||
this.silentPaymentLabel = silentPaymentLabel;
|
||||
}
|
||||
|
||||
public Map<String, byte[]> getDnssecProof() {
|
||||
return dnssecProof;
|
||||
}
|
||||
|
||||
public Optional<DnsPayment> getDnsPayment() throws DnsPaymentValidationException, IOException, BitcoinURIParseException, ExecutionException, InterruptedException {
|
||||
if(dnssecProof == null || dnssecProof.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
String hrn = dnssecProof.keySet().iterator().next();
|
||||
DnsPaymentResolver resolver = new DnsPaymentResolver(hrn);
|
||||
return resolver.resolve(dnssecProof.get(hrn));
|
||||
}
|
||||
|
||||
public void setDnssecProof(Map<String, byte[]> dnssecProof) {
|
||||
this.dnssecProof = dnssecProof;
|
||||
}
|
||||
|
||||
public TransactionOutput getOutput() {
|
||||
return psbt.getTransaction().getOutputs().get(index);
|
||||
}
|
||||
|
||||
void setIndex(int index) {
|
||||
this.index = index;
|
||||
}
|
||||
|
||||
public void clearNonFinalFields() {
|
||||
tapDerivedPublicKeys.clear();
|
||||
}
|
||||
|
||||
@ -1,19 +0,0 @@
|
||||
package com.sparrowwallet.drongo.psbt;
|
||||
|
||||
public class PSBTProofException extends PSBTParseException {
|
||||
public PSBTProofException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public PSBTProofException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public PSBTProofException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public PSBTProofException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
package com.sparrowwallet.drongo.silentpayments;
|
||||
|
||||
public class InvalidSilentPaymentException extends Exception {
|
||||
public InvalidSilentPaymentException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public InvalidSilentPaymentException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@ -1,52 +0,0 @@
|
||||
package com.sparrowwallet.drongo.silentpayments;
|
||||
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.address.P2TRAddress;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.Payment;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
public class SilentPayment extends Payment {
|
||||
public static final Set<ScriptType> VALID_INPUT_SCRIPT_TYPES = Set.of(ScriptType.P2PKH, ScriptType.P2SH_P2WPKH, ScriptType.P2WPKH, ScriptType.P2TR);
|
||||
|
||||
private final SilentPaymentAddress silentPaymentAddress;
|
||||
|
||||
public SilentPayment(SilentPaymentAddress silentPaymentAddress, String label, long amount, boolean sendMax) {
|
||||
this(silentPaymentAddress, getDummyAddress(), label, amount, sendMax);
|
||||
}
|
||||
|
||||
public SilentPayment(SilentPaymentAddress silentPaymentAddress, String label, long amount, boolean sendMax, Type type) {
|
||||
this(silentPaymentAddress, getDummyAddress(), label, amount, sendMax, type);
|
||||
}
|
||||
|
||||
public SilentPayment(SilentPaymentAddress silentPaymentAddress, Address address, String label, long amount, boolean sendMax) {
|
||||
this(silentPaymentAddress, address, label, amount, sendMax, Type.DEFAULT);
|
||||
}
|
||||
|
||||
public SilentPayment(SilentPaymentAddress silentPaymentAddress, Address address, String label, long amount, boolean sendMax, Type type) {
|
||||
super(address == null ? getDummyAddress() : address, label, amount, sendMax, type);
|
||||
this.silentPaymentAddress = silentPaymentAddress;
|
||||
}
|
||||
|
||||
public static Address getDummyAddress() {
|
||||
return new P2TRAddress(new byte[32]);
|
||||
}
|
||||
|
||||
public boolean isAddressComputed() {
|
||||
return !getAddress().equals(getDummyAddress());
|
||||
}
|
||||
|
||||
public SilentPaymentAddress getSilentPaymentAddress() {
|
||||
return silentPaymentAddress;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayAddress() {
|
||||
if(!isAddressComputed()) {
|
||||
return silentPaymentAddress.toAbbreviatedString();
|
||||
}
|
||||
|
||||
return super.getDisplayAddress();
|
||||
}
|
||||
}
|
||||
@ -1,110 +0,0 @@
|
||||
package com.sparrowwallet.drongo.silentpayments;
|
||||
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.protocol.Bech32;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class SilentPaymentAddress {
|
||||
public static final int VERSION = 0;
|
||||
|
||||
private final ECKey scanAddress;
|
||||
private final ECKey spendAddress;
|
||||
|
||||
public SilentPaymentAddress(ECKey scanAddress, ECKey spendAddress) {
|
||||
this.scanAddress = scanAddress;
|
||||
this.spendAddress = spendAddress;
|
||||
}
|
||||
|
||||
public ECKey getScanKey() {
|
||||
return scanAddress;
|
||||
}
|
||||
|
||||
public ECKey getSpendKey() {
|
||||
return spendAddress;
|
||||
}
|
||||
|
||||
public String getAddress() {
|
||||
byte[] keys = Utils.concat(scanAddress.getPubKey(), spendAddress.getPubKey());
|
||||
return Bech32.encode(Network.get().getSilentPaymentsAddressHrp(), VERSION, Bech32.Encoding.BECH32M, keys);
|
||||
}
|
||||
|
||||
public static SilentPaymentAddress from(String address) {
|
||||
Bech32.Bech32Data data = Bech32.decode(address, 1023);
|
||||
if(data.encoding != Bech32.Encoding.BECH32M) {
|
||||
throw new IllegalArgumentException("Invalid silent payments address encoding");
|
||||
}
|
||||
|
||||
if(!Network.get().getSilentPaymentsAddressHrp().equals(data.hrp)) {
|
||||
throw new IllegalArgumentException("Invalid silent payments address hrp");
|
||||
}
|
||||
|
||||
int witnessVersion = data.data[0];
|
||||
if(witnessVersion != VERSION) {
|
||||
throw new UnsupportedOperationException("Unsupported silent payments address witness version");
|
||||
}
|
||||
|
||||
byte[] convertedProgram = Arrays.copyOfRange(data.data, 1, data.data.length);
|
||||
byte[] witnessProgram = Bech32.convertBits(convertedProgram, 0, convertedProgram.length, 5, 8, false);
|
||||
|
||||
if(witnessProgram.length != 66) {
|
||||
throw new IllegalArgumentException("Invalid silent payments address witness length");
|
||||
}
|
||||
|
||||
ECKey scanPubKey = ECKey.fromPublicOnly(Arrays.copyOfRange(witnessProgram, 0, 33));
|
||||
ECKey spendPubKey = ECKey.fromPublicOnly(Arrays.copyOfRange(witnessProgram, 33, 66));
|
||||
|
||||
return new SilentPaymentAddress(scanPubKey, spendPubKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getAddress();
|
||||
}
|
||||
|
||||
public String toAbbreviatedString() {
|
||||
String address = toString();
|
||||
return address.substring(0, 24) + "..." + address.substring(address.length() - 24);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean equals(Object o) {
|
||||
if(this == o) {
|
||||
return true;
|
||||
}
|
||||
if(!(o instanceof SilentPaymentAddress that)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return getAddress().equals(that.getAddress());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return getAddress().hashCode();
|
||||
}
|
||||
|
||||
public byte[] serialize() {
|
||||
ByteBuffer buffer = ByteBuffer.allocate(67);
|
||||
buffer.put((byte)VERSION);
|
||||
buffer.put(scanAddress.getPubKey());
|
||||
buffer.put(spendAddress.getPubKey());
|
||||
return buffer.array();
|
||||
}
|
||||
|
||||
public static SilentPaymentAddress fromBytes(byte[] bytes) {
|
||||
if(bytes.length != 67) {
|
||||
throw new IllegalArgumentException("Silent payment address must be 67 bytes");
|
||||
}
|
||||
int version = bytes[0] & 0xff;
|
||||
if(version != VERSION) {
|
||||
throw new UnsupportedOperationException("Unsupported silent payments address version " + version);
|
||||
}
|
||||
ECKey scanPubKey = ECKey.fromPublicOnly(Arrays.copyOfRange(bytes, 1, 34));
|
||||
ECKey spendPubKey = ECKey.fromPublicOnly(Arrays.copyOfRange(bytes, 34, 67));
|
||||
return new SilentPaymentAddress(scanPubKey, spendPubKey);
|
||||
}
|
||||
}
|
||||
@ -1,127 +0,0 @@
|
||||
package com.sparrowwallet.drongo.silentpayments;
|
||||
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.policy.Policy;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.Bech32;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public class SilentPaymentScanAddress extends SilentPaymentAddress {
|
||||
public static final long CHANGE_LABEL_INDEX = 0L;
|
||||
|
||||
public SilentPaymentScanAddress(ECKey scanPrivateKey, ECKey spendPublicKey) {
|
||||
super(scanPrivateKey, spendPublicKey);
|
||||
|
||||
if(scanPrivateKey.isPubKeyOnly()) {
|
||||
throw new IllegalArgumentException("Scan key must be a private key");
|
||||
}
|
||||
}
|
||||
|
||||
public ECKey getChangeTweakKey() {
|
||||
return SilentPaymentUtils.getLabelledTweakKey(getScanKey(), CHANGE_LABEL_INDEX);
|
||||
}
|
||||
|
||||
public ECKey getLabelledTweakKey(long labelIndex) {
|
||||
return SilentPaymentUtils.getLabelledTweakKey(getScanKey(), labelIndex);
|
||||
}
|
||||
|
||||
public SilentPaymentScanAddress getChangeAddress() {
|
||||
return getLabelledAddress(CHANGE_LABEL_INDEX);
|
||||
}
|
||||
|
||||
public SilentPaymentScanAddress getLabelledAddress(long labelIndex) {
|
||||
ECKey labelledSpendKey = SilentPaymentUtils.getLabelledSpendKey(getScanKey(), getSpendKey(), labelIndex);
|
||||
return new SilentPaymentScanAddress(getScanKey(), labelledSpendKey);
|
||||
}
|
||||
|
||||
public static SilentPaymentScanAddress from(DeterministicSeed deterministicSeed, int account) throws MnemonicException {
|
||||
Wallet spWallet = new Wallet();
|
||||
spWallet.setPolicyType(PolicyType.SINGLE_HD);
|
||||
spWallet.setScriptType(ScriptType.P2WPKH);
|
||||
Keystore spKeystore = Keystore.fromSeed(deterministicSeed, PolicyType.SINGLE_HD, KeyDerivation.getBip352Derivation(account));
|
||||
spWallet.getKeystores().add(spKeystore);
|
||||
spWallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE_HD, ScriptType.P2WPKH, spWallet.getKeystores(), 1));
|
||||
|
||||
WalletNode spendNode = new WalletNode(spWallet, "m/0'/0");
|
||||
WalletNode scanNode = new WalletNode(spWallet, "m/1'/0");
|
||||
|
||||
return from(spKeystore.getKey(scanNode), ECKey.fromPublicOnly(spKeystore.getKey(spendNode)));
|
||||
}
|
||||
|
||||
public static SilentPaymentScanAddress from(ECKey scanPrivateKey, ECKey spendPublicKey) {
|
||||
return new SilentPaymentScanAddress(scanPrivateKey, spendPublicKey);
|
||||
}
|
||||
|
||||
public SilentPaymentAddress getSilentPaymentAddress() {
|
||||
return new SilentPaymentAddress(ECKey.fromPublicOnly(getScanKey()), getSpendKey());
|
||||
}
|
||||
|
||||
public SilentPaymentScanAddress copy() {
|
||||
return new SilentPaymentScanAddress(getScanKey(), getSpendKey());
|
||||
}
|
||||
|
||||
public String toKeyString() {
|
||||
return Bech32.encode(Network.get().getSilentPaymentsScanKeyHrp(), 0, Bech32.Encoding.BECH32M, toBytes());
|
||||
}
|
||||
|
||||
public byte[] toBytes() {
|
||||
return Utils.concat(getScanKey().getPrivKeyBytes(), getSpendKey().getPubKey(true));
|
||||
}
|
||||
|
||||
public static boolean isValid(String encoded) {
|
||||
try {
|
||||
fromKeyString(encoded);
|
||||
} catch(Exception e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static SilentPaymentScanAddress fromKeyString(String encoded) {
|
||||
Bech32.Bech32Data data = Bech32.decode(encoded, 1023);
|
||||
if(data.encoding != Bech32.Encoding.BECH32M) {
|
||||
throw new IllegalArgumentException("Invalid silent payment key encoding");
|
||||
}
|
||||
|
||||
int version = data.data[0];
|
||||
if(version != 0) {
|
||||
throw new UnsupportedOperationException("Unsupported silent payment key version: " + version);
|
||||
}
|
||||
|
||||
byte[] payload = Bech32.convertBits(data.data, 1, data.data.length - 1, 5, 8, false);
|
||||
|
||||
String scanHrp = Network.get().getSilentPaymentsScanKeyHrp();
|
||||
String spendHrp = Network.get().getSilentPaymentsSpendKeyHrp();
|
||||
if(data.hrp.equals(scanHrp)) {
|
||||
return fromBytes(payload);
|
||||
} else if(data.hrp.equals(spendHrp)) {
|
||||
if(payload.length != 64) {
|
||||
throw new IllegalArgumentException("Invalid spspend payload length: " + payload.length);
|
||||
}
|
||||
ECKey scanKey = ECKey.fromPrivate(Arrays.copyOfRange(payload, 0, 32));
|
||||
ECKey spendKey = ECKey.fromPublicOnly(ECKey.fromPrivate(Arrays.copyOfRange(payload, 32, 64)).getPubKey());
|
||||
|
||||
return new SilentPaymentScanAddress(scanKey, spendKey);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Invalid silent payment key HRP: " + data.hrp);
|
||||
}
|
||||
}
|
||||
|
||||
public static SilentPaymentScanAddress fromBytes(byte[] bytes) {
|
||||
if(bytes == null || bytes.length != 65) {
|
||||
throw new IllegalArgumentException("Invalid silent payments scan address serialization, must be 65 bytes long");
|
||||
}
|
||||
|
||||
ECKey scanKey = ECKey.fromPrivate(Arrays.copyOfRange(bytes, 0, 32));
|
||||
ECKey spendKey = ECKey.fromPublicOnly(Arrays.copyOfRange(bytes, 32, 65));
|
||||
|
||||
return new SilentPaymentScanAddress(scanKey, spendKey);
|
||||
}
|
||||
}
|
||||
@ -1,15 +0,0 @@
|
||||
package com.sparrowwallet.drongo.silentpayments;
|
||||
|
||||
/**
|
||||
* A single output match produced by {@link SilentPaymentUtils#scanTransactionOutputs}.
|
||||
* <p>
|
||||
* The {@code tweak} is the value to store on {@code WalletNode.silentPaymentTweak}: for an unlabeled
|
||||
* receive output it is {@code t_k}; for a labeled output it is {@code (t_k + label_m_priv) mod n} —
|
||||
* the combined scalar, so {@code Keystore.getKey/getPubKey} can derive the on-chain output key by
|
||||
* adding it directly to the base spend key, with no label awareness in the signing path.
|
||||
*
|
||||
* @param outputIndex the index of the matched output within the transaction
|
||||
* @param labelIndex {@code null} for an unlabeled receive match; {@code 0} for change; positive for labeled receive
|
||||
* @param tweak 32-byte scalar; the value to persist as the WalletNode's silent-payment tweak
|
||||
*/
|
||||
public record SilentPaymentScanMatch(int outputIndex, Integer labelIndex, byte[] tweak) {}
|
||||
@ -1,513 +0,0 @@
|
||||
package com.sparrowwallet.drongo.silentpayments;
|
||||
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.protocol.*;
|
||||
import com.sparrowwallet.drongo.wallet.MnemonicException;
|
||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
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.nio.ByteOrder;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.*;
|
||||
|
||||
import static com.sparrowwallet.drongo.protocol.ScriptType.P2TR;
|
||||
|
||||
public class SilentPaymentUtils {
|
||||
private static final Logger log = LoggerFactory.getLogger(SilentPaymentUtils.class);
|
||||
|
||||
private static final List<ScriptType> SCRIPT_TYPES = List.of(ScriptType.P2TR, ScriptType.P2WPKH, ScriptType.P2SH, ScriptType.P2PKH);
|
||||
|
||||
//Alternative generator point on the secp256k1 curve (x-coordinate) generated from the SHA256 hash of "The scalar for this x is unknown"
|
||||
private static final byte[] NUMS_H = {
|
||||
(byte) 0x50, (byte) 0x92, (byte) 0x9b, (byte) 0x74, (byte) 0xc1, (byte) 0xa0, (byte) 0x49, (byte) 0x54, (byte) 0xb7, (byte) 0x8b, (byte) 0x4b, (byte) 0x60,
|
||||
(byte) 0x35, (byte) 0xe9, (byte) 0x7a, (byte) 0x5e, (byte) 0x07, (byte) 0x8a, (byte) 0x5a, (byte) 0x0f, (byte) 0x28, (byte) 0xec, (byte) 0x96, (byte) 0xd5,
|
||||
(byte) 0x47, (byte) 0xbf, (byte) 0xee, (byte) 0x9a, (byte) 0xce, (byte) 0x80, (byte) 0x3a, (byte) 0xc0
|
||||
};
|
||||
|
||||
public static final String BIP_0352_INPUTS_TAG = "BIP0352/Inputs";
|
||||
public static final String BIP_0352_SHARED_SECRET_TAG = "BIP0352/SharedSecret";
|
||||
public static final String BIP_0352_LABEL_TAG = "BIP0352/Label";
|
||||
public static final int K_MAX = 2323;
|
||||
|
||||
public static boolean isEligible(Transaction tx, Map<HashIndex, Script> spentScriptPubKeys) {
|
||||
if(!containsTaprootOutput(tx)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(getInputPubKeys(tx, spentScriptPubKeys).isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(spendsInvalidSegwitOutput(tx, spentScriptPubKeys)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static Map<TransactionInput, ECKey> getInputPubKeys(Transaction tx, Map<HashIndex, Script> spentScriptPubKeys) {
|
||||
Map<TransactionInput, ECKey> inputKeys = new LinkedHashMap<>();
|
||||
for(TransactionInput input : tx.getInputs()) {
|
||||
HashIndex hashIndex = new HashIndex(input.getOutpoint().getHash(), input.getOutpoint().getIndex());
|
||||
Script scriptPubKey = spentScriptPubKeys.get(hashIndex);
|
||||
if(scriptPubKey == null) {
|
||||
throw new IllegalStateException("No scriptPubKey found for input " + input.getOutpoint().getHash() + ":" + input.getOutpoint().getIndex());
|
||||
}
|
||||
for(ScriptType scriptType : SCRIPT_TYPES) {
|
||||
if(scriptType.isScriptType(scriptPubKey)) {
|
||||
switch(scriptType) {
|
||||
case P2TR:
|
||||
if(input.getWitness() != null && input.getWitness().getPushCount() >= 1) {
|
||||
List<byte[]> stack = input.getWitness().getPushes();
|
||||
if(stack.size() > 1 && stack.getLast().length > 0 && stack.getLast()[0] == 0x50) { //Last item is annex
|
||||
stack = stack.subList(0, stack.size() - 1);
|
||||
}
|
||||
|
||||
if(stack.size() > 1) {
|
||||
// Script path spend
|
||||
byte[] controlBlock = stack.getLast();
|
||||
// Control block is <control byte> <32 byte internal key> and 0 or more <32 byte hash>
|
||||
if(controlBlock.length >= 33) {
|
||||
byte[] internalKey = Arrays.copyOfRange(controlBlock, 1, 33);
|
||||
if(Arrays.equals(internalKey, NUMS_H)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ECKey pubKey = ScriptType.P2TR.getPublicKeyFromScript(scriptPubKey);
|
||||
if(pubKey.isCompressed()) {
|
||||
inputKeys.put(input, pubKey);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case P2SH:
|
||||
Script redeemScript = input.getScriptSig().getFirstNestedScript();
|
||||
if(redeemScript != null && ScriptType.P2WPKH.isScriptType(redeemScript)) {
|
||||
if(input.getWitness() != null && input.getWitness().getPushCount() == 2) {
|
||||
byte[] pubKey = input.getWitness().getPushes().getLast();
|
||||
if(pubKey != null && pubKey.length == 33) {
|
||||
inputKeys.put(input, ECKey.fromPublicOnly(pubKey));
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case P2WPKH:
|
||||
if(input.getWitness() != null && input.getWitness().getPushCount() == 2) {
|
||||
byte[] pubKey = input.getWitness().getPushes().getLast();
|
||||
if(pubKey != null && pubKey.length == 33) {
|
||||
inputKeys.put(input, ECKey.fromPublicOnly(pubKey));
|
||||
}
|
||||
}
|
||||
break;
|
||||
case P2PKH:
|
||||
byte[] spkHash = ScriptType.P2PKH.getHashFromScript(scriptPubKey);
|
||||
for(ScriptChunk scriptChunk : input.getScriptSig().getChunks()) {
|
||||
if(scriptChunk.isPubKey() && scriptChunk.getData().length == 33 && Arrays.equals(Utils.sha256hash160(scriptChunk.getData()), spkHash)) {
|
||||
inputKeys.put(input, scriptChunk.getPubKey());
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("Unhandled script type " + scriptType);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return inputKeys;
|
||||
}
|
||||
|
||||
public static boolean containsTaprootOutput(Transaction tx) {
|
||||
for(TransactionOutput output : tx.getOutputs()) {
|
||||
if(ScriptType.P2TR.isScriptType(output.getScript())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean spendsInvalidSegwitOutput(Transaction tx, Map<HashIndex, Script> spentScriptPubKeys) {
|
||||
for(TransactionInput input : tx.getInputs()) {
|
||||
HashIndex hashIndex = new HashIndex(input.getOutpoint().getHash(), input.getOutpoint().getIndex());
|
||||
Script scriptPubKey = spentScriptPubKeys.get(hashIndex);
|
||||
if(scriptPubKey == null) {
|
||||
throw new IllegalStateException("No scriptPubKey found for input " + input.getOutpoint().getHash() + ":" + input.getOutpoint().getIndex());
|
||||
}
|
||||
List<ScriptChunk> chunks = scriptPubKey.getChunks();
|
||||
if(chunks.size() == 2 && chunks.getFirst().isOpCode() && chunks.get(1).getData() != null
|
||||
&& chunks.getFirst().getOpcode() >= ScriptOpCodes.OP_2 && chunks.getFirst().getOpcode() <= ScriptOpCodes.OP_16) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static byte[] getTweak(Transaction tx, Map<HashIndex, Script> spentScriptPubKeys) {
|
||||
return getTweak(tx, spentScriptPubKeys, true);
|
||||
}
|
||||
|
||||
public static byte[] getTweak(Transaction tx, Map<HashIndex, Script> spentScriptPubKeys, boolean compressed) {
|
||||
if(tx.getOutputs().stream().noneMatch(output -> ScriptType.P2TR.isScriptType(output.getScript()))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if(spendsInvalidSegwitOutput(tx, spentScriptPubKeys)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Map<TransactionInput, ECKey> inputKeys = getInputPubKeys(tx, spentScriptPubKeys);
|
||||
if(inputKeys.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if(!Secp256k1Context.isEnabled()) {
|
||||
throw new IllegalStateException("libsecp256k1 is not enabled");
|
||||
}
|
||||
|
||||
try {
|
||||
byte[][] inputPubKeys = new byte[inputKeys.size()][];
|
||||
int index = 0;
|
||||
for (ECKey key : inputKeys.values()) {
|
||||
inputPubKeys[index++] = key.getPubKey(true);
|
||||
}
|
||||
byte[] combinedPubKey = NativeSecp256k1.pubKeyCombine(inputPubKeys, true);
|
||||
byte[] smallestOutpoint = tx.getInputs().stream().map(input -> input.getOutpoint().bitcoinSerialize()).min(new Utils.LexicographicByteArrayComparator()).orElseThrow();
|
||||
|
||||
byte[] inputHash = Utils.taggedHash(BIP_0352_INPUTS_TAG, Utils.concat(smallestOutpoint, combinedPubKey));
|
||||
return NativeSecp256k1.pubKeyTweakMul(combinedPubKey, inputHash, compressed);
|
||||
} catch(NativeSecp256k1Util.AssertFailException e) {
|
||||
log.error("Error computing tweak", e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the output addresses for a list of silent payments by calculating the shared secret
|
||||
* between scan keys, spend keys, and the summed private key derived from the provided UTXOs.
|
||||
* Updates each silent payment instance with the corresponding address.
|
||||
*
|
||||
* @param silentPayments the list of silent payments containing silent payment addresses and metadata
|
||||
* @param utxos a map of UTXOs (unspent transaction outputs) to wallet nodes, containing information
|
||||
* about inputs used to derive the summed private key
|
||||
* @throws InvalidSilentPaymentException if the computed shared secrets or addresses are invalid
|
||||
*/
|
||||
public static Map<ECKey, EcdhShareAndProof> computeOutputAddresses(List<SilentPayment> silentPayments, Map<HashIndex, WalletNode> utxos) throws InvalidSilentPaymentException {
|
||||
ECKey summedPrivateKey = getSummedPrivateKey(utxos.values());
|
||||
return computeOutputAddresses(silentPayments, summedPrivateKey, utxos.keySet());
|
||||
}
|
||||
|
||||
public static Map<ECKey, EcdhShareAndProof> computeOutputAddresses(List<SilentPayment> silentPayments, ECKey summedPrivateKey, Set<HashIndex> outpoints) throws InvalidSilentPaymentException {
|
||||
Map<ECKey, EcdhShareAndProof> scanKeyProofs = new LinkedHashMap<>();
|
||||
SecureRandom random = new SecureRandom();
|
||||
BigInteger inputHash = getInputHash(outpoints, summedPrivateKey);
|
||||
Map<ECKey, List<SilentPayment>> scanKeyGroups = getScanKeyGroups(silentPayments);
|
||||
for(Map.Entry<ECKey, List<SilentPayment>> scanKeyGroup : scanKeyGroups.entrySet()) {
|
||||
ECKey scanKey = scanKeyGroup.getKey();
|
||||
ECKey ecdhShare = scanKey.multiply(summedPrivateKey.getPrivKey(), true);
|
||||
SilentPaymentsDLEQProof dleqProof = SilentPaymentsDLEQProof.generate(summedPrivateKey.getPrivKey(), scanKey, random);
|
||||
scanKeyProofs.put(scanKey, new EcdhShareAndProof(ecdhShare, dleqProof));
|
||||
ECKey sharedSecret = ecdhShare.multiply(inputHash, true);
|
||||
int k = 0;
|
||||
for(SilentPayment silentPayment : scanKeyGroup.getValue()) {
|
||||
BigInteger tk = new BigInteger(1, Utils.taggedHash(BIP_0352_SHARED_SECRET_TAG,
|
||||
Utils.concat(sharedSecret.getPubKey(true), ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(k).array())));
|
||||
if(tk.equals(BigInteger.ZERO) || tk.compareTo(ECKey.CURVE.getN()) >= 0) {
|
||||
throw new InvalidSilentPaymentException("The tk value is invalid for the eligible silent payments inputs");
|
||||
}
|
||||
ECKey spendKey = silentPayment.getSilentPaymentAddress().getSpendKey();
|
||||
ECKey pkm = spendKey.add(ECKey.fromPublicOnly(ECKey.publicPointFromPrivate(tk).getEncoded(true)), true);
|
||||
silentPayment.setAddress(P2TR.getAddress(pkm.getPubKeyXCoord()));
|
||||
k++;
|
||||
}
|
||||
}
|
||||
|
||||
return scanKeyProofs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that the output scripts for silent payment outputs match the expected scripts
|
||||
* computed from the ECDH shares. This implements BIP-375 output script verification.
|
||||
*
|
||||
* @param silentPayments List of silent payments sending to a common scan key
|
||||
* @param ecdhShare The ECDH share (a * B_scan), either global or summed from per-input
|
||||
* @param summedPublicKey The sum of all eligible input public keys
|
||||
* @param outpoints Set of outpoints for eligible inputs
|
||||
* @throws InvalidSilentPaymentException if validation fails or scripts don't match
|
||||
*/
|
||||
public static void validateOutputAddresses(List<SilentPayment> silentPayments, ECKey ecdhShare, ECKey summedPublicKey, Set<HashIndex> outpoints) throws InvalidSilentPaymentException {
|
||||
BigInteger inputHash = getInputHash(outpoints, summedPublicKey);
|
||||
Map<ECKey, List<SilentPayment>> scanKeyGroups = getScanKeyGroups(silentPayments);
|
||||
for(Map.Entry<ECKey, List<SilentPayment>> scanKeyGroup : scanKeyGroups.entrySet()) {
|
||||
// Compute shared secret from ECDH share and input hash
|
||||
// Instead of: sharedSecret = scanKey.multiply(inputHash).multiply(summedPrivateKey.getPrivKey())
|
||||
// We use: sharedSecret = ecdhShare.multiply(inputHash)
|
||||
// Because ecdhShare is already (a * B_scan)
|
||||
ECKey sharedSecret = ecdhShare.multiply(inputHash, true);
|
||||
|
||||
int k = 0;
|
||||
for(SilentPayment silentPayment : scanKeyGroup.getValue()) {
|
||||
BigInteger tk = new BigInteger(1, Utils.taggedHash(BIP_0352_SHARED_SECRET_TAG,
|
||||
Utils.concat(sharedSecret.getPubKey(true), ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(k).array())));
|
||||
if(tk.equals(BigInteger.ZERO) || tk.compareTo(ECKey.CURVE.getN()) >= 0) {
|
||||
throw new InvalidSilentPaymentException("The tk value is invalid for the eligible silent payments inputs");
|
||||
}
|
||||
ECKey spendKey = silentPayment.getSilentPaymentAddress().getSpendKey();
|
||||
ECKey pkm = spendKey.add(ECKey.fromPublicOnly(ECKey.publicPointFromPrivate(tk).getEncoded(true)), true);
|
||||
Address expectedAddress = ScriptType.P2TR.getAddress(pkm.getPubKeyXCoord());
|
||||
if(!silentPayment.getAddress().equals(expectedAddress)) {
|
||||
throw new InvalidSilentPaymentException("Silent payment output address mismatch: expected " + expectedAddress + " but got " + silentPayment.getAddress());
|
||||
}
|
||||
k++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static Map<ECKey, List<SilentPayment>> getScanKeyGroups(Collection<SilentPayment> silentPayments) {
|
||||
Map<ECKey, List<SilentPayment>> scanKeyGroups = new LinkedHashMap<>();
|
||||
for(SilentPayment silentPayment : silentPayments) {
|
||||
SilentPaymentAddress address = silentPayment.getSilentPaymentAddress();
|
||||
List<SilentPayment> scanKeyGroup = scanKeyGroups.computeIfAbsent(address.getScanKey(), _ -> new ArrayList<>());
|
||||
scanKeyGroup.add(silentPayment);
|
||||
}
|
||||
|
||||
return scanKeyGroups;
|
||||
}
|
||||
|
||||
public static BigInteger getInputHash(Set<HashIndex> outpoints, ECKey summedInputKey) throws InvalidSilentPaymentException {
|
||||
byte[] smallestOutpoint = getSmallestOutpoint(outpoints);
|
||||
byte[] concat = Utils.concat(smallestOutpoint, summedInputKey.getPubKey(true));
|
||||
BigInteger inputHash = new BigInteger(1, Utils.taggedHash(BIP_0352_INPUTS_TAG, concat));
|
||||
if(inputHash.equals(BigInteger.ZERO) || inputHash.compareTo(ECKey.CURVE.getN()) >= 0) {
|
||||
throw new InvalidSilentPaymentException("The input hash is invalid for the eligible silent payments inputs");
|
||||
}
|
||||
|
||||
return inputHash;
|
||||
}
|
||||
|
||||
public static ECKey getSummedPrivateKey(Collection<WalletNode> walletNodes) throws InvalidSilentPaymentException {
|
||||
BigInteger summedPrivKey = null;
|
||||
for(WalletNode walletNode : walletNodes) {
|
||||
if(!walletNode.getWallet().canSendSilentPayments()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
ECKey rawKey = walletNode.getWallet().getKeystores().getFirst().getKey(walletNode);
|
||||
ECKey privateKey = walletNode.getWallet().getScriptType().getOutputKey(walletNode.getWallet().getPolicyType(), rawKey);
|
||||
if(walletNode.getWallet().getScriptType() == P2TR && privateKey.hasOddYCoord()) {
|
||||
privateKey = privateKey.negatePrivate();
|
||||
}
|
||||
if(summedPrivKey == null) {
|
||||
summedPrivKey = privateKey.getPrivKey();
|
||||
} else {
|
||||
summedPrivKey = summedPrivKey.add(privateKey.getPrivKey()).mod(ECKey.CURVE.getN());
|
||||
}
|
||||
} catch(MnemonicException e) {
|
||||
throw new InvalidSilentPaymentException("Invalid wallet mnemonic for sending silent payment", e);
|
||||
}
|
||||
}
|
||||
|
||||
if(summedPrivKey == null) {
|
||||
throw new InvalidSilentPaymentException("There are no eligible inputs to derive a silent payments shared secret");
|
||||
}
|
||||
|
||||
if(summedPrivKey.equals(BigInteger.ZERO)) {
|
||||
throw new InvalidSilentPaymentException("The summed private key is zero for the eligible silent payments inputs");
|
||||
}
|
||||
|
||||
return ECKey.fromPrivate(summedPrivKey);
|
||||
}
|
||||
|
||||
public static ECKey getInputPublicKey(WalletNode walletNode) {
|
||||
if(!walletNode.getWallet().canSendSilentPayments()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ECKey rawKey = walletNode.getPubKey();
|
||||
ECKey publicKey = walletNode.getWallet().getScriptType().getOutputKey(walletNode.getWallet().getPolicyType(), rawKey);
|
||||
if(walletNode.getWallet().getScriptType() == P2TR && publicKey.hasOddYCoord()) {
|
||||
publicKey = publicKey.negate();
|
||||
}
|
||||
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
public static ECKey getSummedPublicKey(Collection<ECKey> publicKeys) {
|
||||
ECKey summedKey = null;
|
||||
|
||||
for(ECKey publicKey : publicKeys) {
|
||||
if(publicKey != null) {
|
||||
if(summedKey == null) {
|
||||
summedKey = publicKey;
|
||||
} else {
|
||||
summedKey = summedKey.add(publicKey, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return summedKey;
|
||||
}
|
||||
|
||||
public static byte[] getSmallestOutpoint(Set<HashIndex> outpoints) {
|
||||
return outpoints.stream().map(outpoint -> new TransactionOutPoint(outpoint.getHash(), outpoint.getIndex())).map(TransactionOutPoint::bitcoinSerialize)
|
||||
.min(new Utils.LexicographicByteArrayComparator()).orElseThrow(() -> new IllegalArgumentException("No inputs provided to calculate silent payments input hash"));
|
||||
}
|
||||
|
||||
public static ECKey getLabelledSpendKey(ECKey scanPrivateKey, ECKey spendPublicKey, long labelIndex) {
|
||||
return spendPublicKey.add(getLabelledTweakKey(scanPrivateKey, labelIndex), true);
|
||||
}
|
||||
|
||||
public static ECKey getLabelledTweakKey(ECKey scanPrivateKey, long labelIndex) {
|
||||
return ECKey.fromPublicOnly(ECKey.publicPointFromPrivate(getLabelledTweakScalar(scanPrivateKey, labelIndex)).getEncoded(true));
|
||||
}
|
||||
|
||||
public static BigInteger getLabelledTweakScalar(ECKey scanPrivateKey, long labelIndex) {
|
||||
return new BigInteger(1, Utils.taggedHash(BIP_0352_LABEL_TAG, Utils.concat(scanPrivateKey.getPrivKeyBytes(), ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt((int)labelIndex).array())));
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans the outputs of a transaction for silent-payment outputs paying to the receiver identified by
|
||||
* {@code scanPrivateKey} and {@code spendPublicKey}, given the per-transaction {@code tweakKey}
|
||||
* (= input_hash * A_sum) supplied by the indexer.
|
||||
* <p>
|
||||
* Implements the BIP352 receive-side algorithm: ECDH against the tweak key, k-iteration with
|
||||
* tagged-hash derivation, unlabeled-then-labeled match per output, terminating when no output
|
||||
* matches at the current k. Both label 0 (change) and any caller-supplied positive labels are
|
||||
* scanned for.
|
||||
* <p>
|
||||
* For labeled matches the returned {@code tweak} is the combined scalar
|
||||
* {@code (t_k + label_m_priv) mod n}, ready for direct storage on a {@code WalletNode}.
|
||||
*
|
||||
* @param scanPrivateKey the receiver's scan private key (b_scan)
|
||||
* @param spendPublicKey the receiver's spend public key (B_spend), 33-byte compressed
|
||||
* @param labelIndices additional positive label indices to scan for; m=0 (change) is always included
|
||||
* @param tweakKey the 33-byte compressed pubkey input_hash * A_sum from the indexer
|
||||
* @param outputs the full list of outputs in the transaction (non-P2TR outputs are ignored)
|
||||
* @return matches in ascending outputIndex order, or empty if none (including server false positives)
|
||||
* @throws IllegalArgumentException if any required input is null or {@code tweakKey} is malformed
|
||||
* @throws InvalidSilentPaymentException if a derived {@code t_k} scalar is invalid (zero or {@code >= n}), per BIP352
|
||||
*/
|
||||
public static List<SilentPaymentScanMatch> scanTransactionOutputs(ECKey scanPrivateKey, ECKey spendPublicKey, Set<Integer> labelIndices, byte[] tweakKey, List<TransactionOutput> outputs) throws InvalidSilentPaymentException {
|
||||
if(scanPrivateKey == null || spendPublicKey == null || labelIndices == null || tweakKey == null || outputs == null) {
|
||||
throw new IllegalArgumentException("All arguments must be non-null");
|
||||
}
|
||||
if(tweakKey.length != 33) {
|
||||
throw new IllegalArgumentException("tweakKey must be 33 bytes (compressed pubkey)");
|
||||
}
|
||||
|
||||
ECKey tweakKeyPoint;
|
||||
try {
|
||||
tweakKeyPoint = ECKey.fromPublicOnly(tweakKey);
|
||||
} catch(Exception e) {
|
||||
throw new IllegalArgumentException("tweakKey is not a valid compressed pubkey", e);
|
||||
}
|
||||
|
||||
// shared_secret = scan_priv * tweak_key (point multiplication)
|
||||
ECKey sharedSecret = tweakKeyPoint.multiply(scanPrivateKey.getPrivKey(), true);
|
||||
byte[] sharedSecretCompressed = sharedSecret.getPubKey(true);
|
||||
|
||||
// Precompute label keys for {0} ∪ labelIndices.
|
||||
Set<Integer> allLabels = new TreeSet<>(labelIndices);
|
||||
allLabels.add(0);
|
||||
List<LabelEntry> labelEntries = new ArrayList<>();
|
||||
for(int m : allLabels) {
|
||||
labelEntries.add(new LabelEntry(m, ECKey.fromPrivate(getLabelledTweakScalar(scanPrivateKey, m))));
|
||||
}
|
||||
|
||||
// Filter to P2TR outputs, keeping their original indices and x-only pubkeys.
|
||||
List<ScanMatchCandidate> remaining = new ArrayList<>();
|
||||
for(int i = 0; i < outputs.size(); i++) {
|
||||
Script script = outputs.get(i).getScript();
|
||||
if(P2TR.isScriptType(script)) {
|
||||
byte[] xOnly = P2TR.getPublicKeyFromScript(script).getPubKeyXCoord();
|
||||
remaining.add(new ScanMatchCandidate(i, xOnly));
|
||||
}
|
||||
}
|
||||
|
||||
List<SilentPaymentScanMatch> matches = new ArrayList<>();
|
||||
int k = 0;
|
||||
|
||||
// BIP352 termination: stop when an entire pass over the remaining outputs at a given k
|
||||
// produces no match. k advances only on a match.
|
||||
while(!remaining.isEmpty() && k < K_MAX) {
|
||||
byte[] tkBytes = Utils.taggedHash(BIP_0352_SHARED_SECRET_TAG, Utils.concat(sharedSecretCompressed, ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(k).array()));
|
||||
BigInteger tk = new BigInteger(1, tkBytes);
|
||||
if(tk.equals(BigInteger.ZERO) || tk.compareTo(ECKey.CURVE.getN()) >= 0) {
|
||||
throw new InvalidSilentPaymentException("The tk value is invalid for the eligible silent payments inputs");
|
||||
}
|
||||
|
||||
ECKey tkPoint = ECKey.fromPrivate(tk); // tkPoint.pub == t_k * G
|
||||
ECKey pK = spendPublicKey.add(tkPoint, true); // P_k = B_spend + t_k * G
|
||||
|
||||
int matchedIndex = -1;
|
||||
Integer matchedLabel = null;
|
||||
byte[] matchedTweak = null;
|
||||
|
||||
for(ScanMatchCandidate candidate : remaining) {
|
||||
// Unlabeled: x_only(P_k) == output x-only?
|
||||
if(Arrays.equals(pK.getPubKeyXCoord(), candidate.xOnly())) {
|
||||
matchedIndex = candidate.index();
|
||||
matchedLabel = null;
|
||||
matchedTweak = Utils.bigIntegerToBytes(tk, 32);
|
||||
break;
|
||||
}
|
||||
// Labeled: x_only(P_k + label_m * G) == output x-only?
|
||||
for(LabelEntry label : labelEntries) {
|
||||
ECKey pkLabeled = pK.add(label.key(), true);
|
||||
if(Arrays.equals(pkLabeled.getPubKeyXCoord(), candidate.xOnly())) {
|
||||
matchedIndex = candidate.index();
|
||||
matchedLabel = label.index();
|
||||
BigInteger combined = tk.add(label.key().getPrivKey()).mod(ECKey.CURVE.getN());
|
||||
matchedTweak = Utils.bigIntegerToBytes(combined, 32);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(matchedIndex >= 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(matchedIndex < 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
matches.add(new SilentPaymentScanMatch(matchedIndex, matchedLabel, matchedTweak));
|
||||
int finalMatchedIndex = matchedIndex;
|
||||
remaining.removeIf(c -> c.index() == finalMatchedIndex);
|
||||
k++;
|
||||
}
|
||||
|
||||
matches.sort(Comparator.comparingInt(SilentPaymentScanMatch::outputIndex));
|
||||
return matches;
|
||||
}
|
||||
|
||||
public static byte[] getSecp256k1PubKey(ECKey ecKey) {
|
||||
return getSecp256k1PubKey(ecKey.getPubKey(false));
|
||||
}
|
||||
|
||||
public static byte[] getSecp256k1PubKey(byte[] uncompressedKey) {
|
||||
byte[] key = new byte[64];
|
||||
System.arraycopy(uncompressedKey, 1, key, 32, 32);
|
||||
System.arraycopy(uncompressedKey, 33, key, 0, 32);
|
||||
return Utils.reverseBytes(key);
|
||||
}
|
||||
|
||||
public record EcdhShareAndProof(ECKey ecdhShare, SilentPaymentsDLEQProof dleqProof) {}
|
||||
|
||||
private record LabelEntry(int index, ECKey key) {}
|
||||
|
||||
private record ScanMatchCandidate(int index, byte[] xOnly) {}
|
||||
}
|
||||
@ -1,150 +0,0 @@
|
||||
package com.sparrowwallet.drongo.silentpayments;
|
||||
|
||||
import com.sparrowwallet.drongo.crypto.DLEQProof;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Represents a BIP-375 Silent Payments DLEQ proof.
|
||||
*
|
||||
* This class wraps a 64-byte DLEQ proof that proves the discrete logarithm
|
||||
* equivalency between a public key and an ECDH share, as used in BIP-375
|
||||
* Silent Payments for PSBTs.
|
||||
*/
|
||||
public class SilentPaymentsDLEQProof {
|
||||
private final byte[] proof;
|
||||
|
||||
/**
|
||||
* Private constructor that validates and stores the proof bytes.
|
||||
*
|
||||
* @param proofBytes The 64-byte DLEQ proof
|
||||
* @throws IllegalArgumentException if proof is not exactly 64 bytes
|
||||
*/
|
||||
private SilentPaymentsDLEQProof(byte[] proofBytes) {
|
||||
if(proofBytes == null) {
|
||||
throw new IllegalArgumentException("DLEQ proof cannot be null");
|
||||
}
|
||||
if(proofBytes.length != 64) {
|
||||
throw new IllegalArgumentException("DLEQ proof must be exactly 64 bytes, got " + proofBytes.length);
|
||||
}
|
||||
this.proof = Arrays.copyOf(proofBytes, proofBytes.length);
|
||||
}
|
||||
|
||||
public static SilentPaymentsDLEQProof generate(BigInteger privateKey, ECKey scanKey, SecureRandom random) throws InvalidSilentPaymentException {
|
||||
byte[] auxRand = new byte[32];
|
||||
random.nextBytes(auxRand);
|
||||
return generate(privateKey, scanKey, auxRand);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a DLEQ proof for Silent Payments according to BIP-375.
|
||||
*
|
||||
* This method generates a proof that the ECDH share (a⋅B_scan) and the public key (a⋅G)
|
||||
* were both generated from the same private key a without revealing a.
|
||||
*
|
||||
* @param privateKey The private key (a) - either a single input's private key or the sum of the private keys for all eligible inputs
|
||||
* @param scanKey The scan public key (B_scan) from the silent payment address
|
||||
* @param auxRand 32 bytes of auxiliary random data (should be fresh randomness for each proof)
|
||||
* @return A new SilentPaymentsDLEQProof instance
|
||||
* @throws IllegalArgumentException if scanKey is not a public-only key, or if auxRand is not 32 bytes
|
||||
* @throws InvalidSilentPaymentException if proof generation fails
|
||||
*/
|
||||
public static SilentPaymentsDLEQProof generate(BigInteger privateKey, ECKey scanKey, byte[] auxRand) throws InvalidSilentPaymentException {
|
||||
if(auxRand == null || auxRand.length != 32) {
|
||||
throw new IllegalArgumentException("Auxiliary random data must be exactly 32 bytes");
|
||||
}
|
||||
|
||||
if(!scanKey.isPubKeyOnly()) {
|
||||
throw new IllegalArgumentException("Scan key must be a public key only");
|
||||
}
|
||||
|
||||
// Generate the proof using BIP-374 GenerateProof with:
|
||||
// - a: the private key
|
||||
// - B: the scan key
|
||||
// - r: auxiliary random data
|
||||
// - G: null (uses default secp256k1 generator)
|
||||
// - m: null (no message for BIP-375)
|
||||
byte[] proofBytes = DLEQProof.generateProof(privateKey, scanKey, auxRand, null, null);
|
||||
|
||||
if(proofBytes == null) {
|
||||
throw new InvalidSilentPaymentException("Failed to generate DLEQ proof");
|
||||
}
|
||||
|
||||
return new SilentPaymentsDLEQProof(proofBytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a SilentPaymentsDLEQProof from existing proof bytes.
|
||||
*
|
||||
* @param proofBytes The 64-byte DLEQ proof
|
||||
* @return A new SilentPaymentsDLEQProof instance
|
||||
* @throws IllegalArgumentException if proof is not exactly 64 bytes
|
||||
*/
|
||||
public static SilentPaymentsDLEQProof fromBytes(byte[] proofBytes) {
|
||||
return new SilentPaymentsDLEQProof(proofBytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify this DLEQ proof according to BIP-375.
|
||||
*
|
||||
* This verifies that the ECDH share was generated from the same private key
|
||||
* as the public key, without revealing the private key.
|
||||
*
|
||||
* @param publicKey The public key of the input, or the sum of the public keys of all eligible inputs (A = a⋅G)
|
||||
* @param scanKey The scan public key (B_scan) from the silent payment address
|
||||
* @param ecdhShare The ECDH share for the input, or the ECDH share for all inputs (C = a⋅B_scan)
|
||||
* @return true if the proof is valid, false otherwise
|
||||
* @throws IllegalArgumentException if any key is not a public-only key
|
||||
*/
|
||||
public boolean verify(ECKey publicKey, ECKey scanKey, ECKey ecdhShare) {
|
||||
if(!publicKey.isPubKeyOnly() || !scanKey.isPubKeyOnly() || !ecdhShare.isPubKeyOnly()) {
|
||||
throw new IllegalArgumentException("All keys for verification must be public keys only");
|
||||
}
|
||||
|
||||
// Verify the proof using BIP-374 VerifyProof with:
|
||||
// - A: the public key
|
||||
// - B: the scan key
|
||||
// - C: the ECDH share
|
||||
// - proof: this proof
|
||||
// - G: null (uses default secp256k1 generator)
|
||||
// - m: null (no message for BIP-375)
|
||||
return DLEQProof.verifyProof(publicKey, scanKey, ecdhShare, proof, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the raw 64-byte proof.
|
||||
*
|
||||
* @return A copy of the proof bytes
|
||||
*/
|
||||
public byte[] getBytes() {
|
||||
return Arrays.copyOf(proof, proof.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if(this == o) {
|
||||
return true;
|
||||
}
|
||||
if(!(o instanceof SilentPaymentsDLEQProof that)) {
|
||||
return false;
|
||||
}
|
||||
return Arrays.equals(proof, that.proof);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Arrays.hashCode(proof);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for(byte b : proof) {
|
||||
sb.append(String.format("%02x", b));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@ -1,11 +1,7 @@
|
||||
package com.sparrowwallet.drongo.uri;
|
||||
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.address.InvalidAddressException;
|
||||
import com.sparrowwallet.drongo.silentpayments.SilentPayment;
|
||||
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
|
||||
import com.sparrowwallet.drongo.wallet.Payment;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@ -70,7 +66,6 @@ public class BitcoinURI {
|
||||
public static final String FIELD_PAYMENT_REQUEST_URL = "r";
|
||||
public static final String FIELD_PAYJOIN_URL = "pj";
|
||||
public static final String FIELD_PAYJOIN_OUTPUT_SUBSTITUTION = "pjos";
|
||||
public static final String FIELD_SILENT_PAYMENTS_ADDRESS = Network.get().getSilentPaymentsAddressHrp();
|
||||
|
||||
public static final String BITCOIN_SCHEME = "bitcoin";
|
||||
private static final String ENCODED_SPACE_CHARACTER = "%20";
|
||||
@ -80,8 +75,6 @@ public class BitcoinURI {
|
||||
public static final DecimalFormat BTC_FORMAT = new DecimalFormat("0", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
|
||||
public static final int SMALLEST_UNIT_EXPONENT = 8;
|
||||
|
||||
private final String uriString;
|
||||
|
||||
/**
|
||||
* Contains all the parameters in the order in which they were processed
|
||||
*/
|
||||
@ -142,7 +135,9 @@ public class BitcoinURI {
|
||||
}
|
||||
}
|
||||
|
||||
this.uriString = input;
|
||||
if(addressToken.isEmpty() && getPaymentRequestUrl() == null) {
|
||||
throw new BitcoinURIParseException("No address and no r= parameter found");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -276,10 +271,10 @@ public class BitcoinURI {
|
||||
if(payjoinUrl != null) {
|
||||
try {
|
||||
URI uri = new URI(payjoinUrl);
|
||||
if(Utils.isSecureUrl(uri)) {
|
||||
if(uri.getScheme().equals("https") || uri.getHost().endsWith(".onion")) {
|
||||
return uri;
|
||||
} else {
|
||||
log.error("Insecure payjoin URL provided, must be https or http .onion: " + payjoinUrl);
|
||||
log.error("Insecure payjoin URL provided, must be https or .onion: " + payjoinUrl);
|
||||
}
|
||||
} catch(URISyntaxException e) {
|
||||
log.error("Invalid payjoin URL provided", e);
|
||||
@ -296,22 +291,6 @@ public class BitcoinURI {
|
||||
return !"0".equals(parameterMap.get(FIELD_PAYJOIN_OUTPUT_SUBSTITUTION));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The silent payments address in the URI, if provided
|
||||
*/
|
||||
public final SilentPaymentAddress getSilentPaymentAddress() {
|
||||
String address = (String)parameterMap.get(FIELD_SILENT_PAYMENTS_ADDRESS);
|
||||
if(address != null) {
|
||||
try {
|
||||
return SilentPaymentAddress.from(address);
|
||||
} catch(Exception e) {
|
||||
log.error("Invalid silent payments address provided", e);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param name The name of the parameter
|
||||
* @return The parameter value, or null if not present
|
||||
@ -336,16 +315,8 @@ public class BitcoinURI {
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
public String toURIString() {
|
||||
return uriString;
|
||||
}
|
||||
|
||||
public Payment toPayment() {
|
||||
long amount = getAmount() == null ? -1 : getAmount();
|
||||
SilentPaymentAddress silentPaymentAddress = getSilentPaymentAddress();
|
||||
if(getAddress() == null && silentPaymentAddress != null) {
|
||||
return new SilentPayment(silentPaymentAddress, getLabel(), amount, false);
|
||||
}
|
||||
return new Payment(getAddress(), getLabel(), amount, false);
|
||||
}
|
||||
|
||||
|
||||
@ -10,6 +10,8 @@ import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTInput;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
/**
|
||||
* This is a special wallet that is used solely to finalize a fully signed PSBT by reading from the partial signatures and UTXO scriptPubKey
|
||||
@ -65,7 +67,7 @@ public class FinalizingPSBTWallet extends Wallet {
|
||||
setGapLimit(0);
|
||||
purposeNode.setChildren(new TreeSet<>());
|
||||
|
||||
setPolicyType(numSignatures == 1 ? PolicyType.SINGLE_HD : PolicyType.MULTI_HD);
|
||||
setPolicyType(numSignatures == 1 ? PolicyType.SINGLE : PolicyType.MULTI);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -98,11 +100,6 @@ public class FinalizingPSBTWallet extends Wallet {
|
||||
return signedInputNodes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<PSBTInput, WalletNode> getSigningNodes(PSBT psbt, boolean useDerivationFallback) {
|
||||
return signedInputNodes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ECKey getPubKey(WalletNode node) {
|
||||
return signedNodeKeys.get(node).get(0);
|
||||
|
||||
@ -9,11 +9,12 @@ import com.sparrowwallet.drongo.bip47.PaymentCode;
|
||||
import com.sparrowwallet.drongo.crypto.*;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.silentpayments.SilentPaymentScanAddress;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class Keystore extends Persistable {
|
||||
private static final Logger log = LoggerFactory.getLogger(Keystore.class);
|
||||
@ -27,8 +28,6 @@ public class Keystore extends Persistable {
|
||||
private KeyDerivation keyDerivation;
|
||||
private ExtendedKey extendedPublicKey;
|
||||
private PaymentCode externalPaymentCode;
|
||||
private SilentPaymentScanAddress silentPaymentScanAddress;
|
||||
private byte[] deviceRegistration;
|
||||
private MasterPrivateExtendedKey masterPrivateExtendedKey;
|
||||
private DeterministicSeed seed;
|
||||
|
||||
@ -37,7 +36,6 @@ public class Keystore extends Persistable {
|
||||
|
||||
//Avoid performing repeated expensive seed derivation checks
|
||||
private transient boolean extendedPublicKeyChecked;
|
||||
private transient boolean silentPaymentScanAddressChecked;
|
||||
|
||||
public Keystore() {
|
||||
this(DEFAULT_LABEL);
|
||||
@ -52,9 +50,6 @@ public class Keystore extends Persistable {
|
||||
}
|
||||
|
||||
public String getBaseLabel() {
|
||||
if(walletModel != null && label.startsWith(walletModel.toDisplayString()) && label.substring(walletModel.toDisplayString().length()).matches("( \\d*)?$")) {
|
||||
return walletModel.toDisplayString();
|
||||
}
|
||||
return label.replaceAll(" \\d*$", "");
|
||||
}
|
||||
|
||||
@ -107,23 +102,6 @@ public class Keystore extends Persistable {
|
||||
this.externalPaymentCode = paymentCode;
|
||||
}
|
||||
|
||||
public SilentPaymentScanAddress getSilentPaymentScanAddress() {
|
||||
return silentPaymentScanAddress;
|
||||
}
|
||||
|
||||
public void setSilentPaymentScanAddress(SilentPaymentScanAddress silentPaymentScanAddress) {
|
||||
this.silentPaymentScanAddress = silentPaymentScanAddress;
|
||||
this.silentPaymentScanAddressChecked = false;
|
||||
}
|
||||
|
||||
public byte[] getDeviceRegistration() {
|
||||
return deviceRegistration;
|
||||
}
|
||||
|
||||
public void setDeviceRegistration(byte[] deviceRegistration) {
|
||||
this.deviceRegistration = deviceRegistration;
|
||||
}
|
||||
|
||||
public boolean hasMasterPrivateExtendedKey() {
|
||||
return masterPrivateExtendedKey != null;
|
||||
}
|
||||
@ -213,34 +191,14 @@ public class Keystore extends Persistable {
|
||||
}
|
||||
|
||||
public ExtendedKey getExtendedPrivateKey() throws MnemonicException {
|
||||
return getExtendedPrivateKey(true);
|
||||
}
|
||||
|
||||
public ExtendedKey getExtendedPrivateKey(boolean resetPathToDerivedDepth) throws MnemonicException {
|
||||
List<ChildNumber> derivation = getKeyDerivation().getDerivation();
|
||||
DeterministicKey derivedKey = getExtendedMasterPrivateKey().getKey(derivation);
|
||||
ExtendedKey xprv = new ExtendedKey(derivedKey, derivedKey.getParentFingerprint(), derivation.isEmpty() ? ChildNumber.ZERO : derivation.get(derivation.size() - 1));
|
||||
if(resetPathToDerivedDepth) {
|
||||
//Recreate from xprv string to reset path to single ChildNumber at the derived depth
|
||||
return ExtendedKey.fromDescriptor(xprv.toString());
|
||||
} else {
|
||||
return xprv;
|
||||
}
|
||||
//Recreate from xprv string to reset path to single ChildNumber at the derived depth
|
||||
return ExtendedKey.fromDescriptor(xprv.toString());
|
||||
}
|
||||
|
||||
public ECKey getKey(WalletNode walletNode) throws MnemonicException {
|
||||
if(silentPaymentScanAddress != null && walletNode.getWallet().getPolicyType() == PolicyType.SINGLE_SP) {
|
||||
ECKey spendPrivKey = getSpendPrivateKey(Collections.emptyMap());
|
||||
byte[] tweak = walletNode.getSilentPaymentTweak();
|
||||
if(tweak == null) {
|
||||
if(walletNode.isPurposeNode()) {
|
||||
return spendPrivKey;
|
||||
}
|
||||
throw new IllegalStateException("Silent payment tweak is required for address node " + walletNode.getDerivationPath());
|
||||
}
|
||||
return spendPrivKey.addPrivate(ECKey.fromPrivate(tweak));
|
||||
}
|
||||
|
||||
if(source == KeystoreSource.SW_PAYMENT_CODE) {
|
||||
try {
|
||||
if(walletNode.getKeyPurpose() != KeyPurpose.RECEIVE) {
|
||||
@ -264,19 +222,6 @@ public class Keystore extends Persistable {
|
||||
}
|
||||
|
||||
public ECKey getPubKey(WalletNode walletNode) {
|
||||
if(silentPaymentScanAddress != null && walletNode.getWallet().getPolicyType() == PolicyType.SINGLE_SP) {
|
||||
ECKey spendKey = silentPaymentScanAddress.getSpendKey();
|
||||
byte[] tweak = walletNode.getSilentPaymentTweak();
|
||||
if(tweak == null) {
|
||||
if(walletNode.isPurposeNode()) {
|
||||
return spendKey;
|
||||
}
|
||||
throw new IllegalStateException("Silent payment tweak is required for address node " + walletNode.getDerivationPath());
|
||||
}
|
||||
ECKey tweakPoint = ECKey.fromPublicOnly(ECKey.fromPrivate(tweak));
|
||||
return spendKey.add(tweakPoint, true);
|
||||
}
|
||||
|
||||
if(source == KeystoreSource.SW_PAYMENT_CODE) {
|
||||
try {
|
||||
PaymentAddress paymentAddress = getPaymentAddress(walletNode.getKeyPurpose(), walletNode.getIndex());
|
||||
@ -299,7 +244,7 @@ public class Keystore extends Persistable {
|
||||
}
|
||||
|
||||
public ECKey getPubKeyForDerivation(KeyDerivation keyDerivation) {
|
||||
if(keyDerivation != null && extendedPublicKey != null) {
|
||||
if(keyDerivation != null) {
|
||||
List<ChildNumber> derivation = keyDerivation.getDerivation();
|
||||
String fingerprint = Utils.bytesToHex(this.extendedPublicKey.getKey().getFingerprint());
|
||||
if(derivation.size() > this.keyDerivation.getDerivation().size()) {
|
||||
@ -318,33 +263,6 @@ public class Keystore extends Persistable {
|
||||
return null;
|
||||
}
|
||||
|
||||
public ECKey getSpendPrivateKey(Map<ECKey, KeyDerivation> spendDerivations) throws MnemonicException {
|
||||
String masterFingerprint = getKeyDerivation().getMasterFingerprint();
|
||||
for(Map.Entry<ECKey, KeyDerivation> entry : spendDerivations.entrySet()) {
|
||||
if(masterFingerprint.equals(entry.getValue().getMasterFingerprint())) {
|
||||
DeterministicKey derivedKey = getExtendedMasterPrivateKey().getKey(entry.getValue().getDerivation());
|
||||
ECKey spendPrivKey = ECKey.fromPrivate(derivedKey.getPrivKeyBytes(), true);
|
||||
|
||||
if(!Arrays.equals(spendPrivKey.getPubKey(), entry.getKey().getPubKey())) {
|
||||
throw new IllegalStateException("Derived spend private key does not match PSBT spend public key");
|
||||
}
|
||||
|
||||
return spendPrivKey;
|
||||
}
|
||||
}
|
||||
|
||||
List<ChildNumber> spendDerivation = KeyDerivation.getBip352SpendDerivation(getKeyDerivation().getDerivation());
|
||||
DeterministicKey derivedKey = getExtendedMasterPrivateKey().getKey(spendDerivation);
|
||||
ECKey spendPrivKey = ECKey.fromPrivate(derivedKey.getPrivKeyBytes(), true);
|
||||
|
||||
ECKey expectedSpendPubKey = getSilentPaymentScanAddress().getSpendKey();
|
||||
if(!Arrays.equals(spendPrivKey.getPubKey(), expectedSpendPubKey.getPubKey())) {
|
||||
throw new IllegalStateException("Derived spend private key does not match keystore spend public key");
|
||||
}
|
||||
|
||||
return spendPrivKey;
|
||||
}
|
||||
|
||||
public boolean isValid() {
|
||||
try {
|
||||
checkKeystore();
|
||||
@ -372,8 +290,8 @@ public class Keystore extends Persistable {
|
||||
throw new InvalidKeystoreException("No key derivation specified");
|
||||
}
|
||||
|
||||
if(extendedPublicKey == null && silentPaymentScanAddress == null) {
|
||||
throw new InvalidKeystoreException("No extended public key or silent payment scan address specified");
|
||||
if(extendedPublicKey == null) {
|
||||
throw new InvalidKeystoreException("No extended public key specified");
|
||||
}
|
||||
|
||||
if(label.isEmpty()) {
|
||||
@ -397,7 +315,7 @@ public class Keystore extends Persistable {
|
||||
throw new InvalidKeystoreException("Source of " + source + " but no seed or master private key is present");
|
||||
}
|
||||
|
||||
if(!extendedPublicKeyChecked && extendedPublicKey != null && ((seed != null && !seed.isEncrypted()) || (masterPrivateExtendedKey != null && !masterPrivateExtendedKey.isEncrypted()))) {
|
||||
if(!extendedPublicKeyChecked && ((seed != null && !seed.isEncrypted()) || (masterPrivateExtendedKey != null && !masterPrivateExtendedKey.isEncrypted()))) {
|
||||
try {
|
||||
List<ChildNumber> derivation = getKeyDerivation().getDerivation();
|
||||
DeterministicKey derivedKey = getExtendedMasterPrivateKey().getKey(derivation);
|
||||
@ -411,21 +329,6 @@ public class Keystore extends Persistable {
|
||||
throw new InvalidKeystoreException("Invalid mnemonic specified for seed", e);
|
||||
}
|
||||
}
|
||||
|
||||
if(!silentPaymentScanAddressChecked && silentPaymentScanAddress != null && ((seed != null && !seed.isEncrypted()) || (masterPrivateExtendedKey != null && !masterPrivateExtendedKey.isEncrypted()))) {
|
||||
try {
|
||||
List<ChildNumber> derivation = getKeyDerivation().getDerivation();
|
||||
DeterministicKey derivedScanKey = getExtendedMasterPrivateKey().getKey(KeyDerivation.getBip352ScanDerivation(derivation));
|
||||
DeterministicKey derivedSpendKey = getExtendedMasterPrivateKey().getKey(KeyDerivation.getBip352SpendDerivation(derivation));
|
||||
SilentPaymentScanAddress derivedScanAddress = new SilentPaymentScanAddress(derivedScanKey, derivedSpendKey);
|
||||
if(!derivedScanAddress.equals(getSilentPaymentScanAddress())) {
|
||||
throw new InvalidKeystoreException("Specified silent payments scan address does not match scan and spend keys derived from seed");
|
||||
}
|
||||
silentPaymentScanAddressChecked = true;
|
||||
} catch(MnemonicException e) {
|
||||
throw new InvalidKeystoreException("Invalid mnemonic specified for seed", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(source == KeystoreSource.SW_PAYMENT_CODE) {
|
||||
@ -462,54 +365,43 @@ public class Keystore extends Persistable {
|
||||
if(bip47ExtendedPrivateKey != null) {
|
||||
copy.setBip47ExtendedPrivateKey(bip47ExtendedPrivateKey.copy());
|
||||
}
|
||||
if(silentPaymentScanAddress != null) {
|
||||
copy.setSilentPaymentScanAddress(silentPaymentScanAddress.copy());
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
public static Keystore fromSeed(DeterministicSeed seed, PolicyType policyType, List<ChildNumber> derivation) throws MnemonicException {
|
||||
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, policyType, derivation);
|
||||
rederiveKeystoreFromMaster(keystore, derivation);
|
||||
return keystore;
|
||||
}
|
||||
|
||||
public static Keystore fromMasterPrivateExtendedKey(MasterPrivateExtendedKey masterPrivateExtendedKey, PolicyType policyType, List<ChildNumber> derivation) throws MnemonicException {
|
||||
public static Keystore fromMasterPrivateExtendedKey(MasterPrivateExtendedKey masterPrivateExtendedKey, List<ChildNumber> derivation) throws MnemonicException {
|
||||
Keystore keystore = new Keystore();
|
||||
keystore.setMasterPrivateExtendedKey(masterPrivateExtendedKey);
|
||||
keystore.setLabel("Master Key");
|
||||
rederiveKeystoreFromMaster(keystore, policyType, derivation);
|
||||
rederiveKeystoreFromMaster(keystore, derivation);
|
||||
return keystore;
|
||||
}
|
||||
|
||||
private static void rederiveKeystoreFromMaster(Keystore keystore, PolicyType policyType, List<ChildNumber> derivation) throws MnemonicException {
|
||||
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.setSource(KeystoreSource.SW_SEED);
|
||||
keystore.setWalletModel(WalletModel.SPARROW);
|
||||
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, KeyDerivation.writePath(derivation)));
|
||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(xpub.toString()));
|
||||
|
||||
if(policyType == PolicyType.SINGLE_SP) {
|
||||
DeterministicKey scanKey = xprv.getKey(KeyDerivation.getBip352ScanDerivation(derivation));
|
||||
DeterministicKey spendKey = xprv.getKey(KeyDerivation.getBip352SpendDerivation(derivation));
|
||||
SilentPaymentScanAddress spScanAddress = new SilentPaymentScanAddress(ECKey.fromPrivate(scanKey.getPrivKey()), ECKey.fromPublicOnly(spendKey));
|
||||
keystore.setSilentPaymentScanAddress(spScanAddress);
|
||||
} else {
|
||||
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.setExtendedPublicKey(ExtendedKey.fromDescriptor(xpub.toString()));
|
||||
|
||||
int account = ScriptType.getScriptTypesForPolicyType(PolicyType.SINGLE_HD).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()));
|
||||
}
|
||||
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() {
|
||||
|
||||
@ -24,12 +24,6 @@ public class MasterPrivateExtendedKey extends Persistable implements Encryptable
|
||||
this.encryptedKey = encryptedKey;
|
||||
}
|
||||
|
||||
public MasterPrivateExtendedKey(DeterministicKey key) {
|
||||
this.privateKey = key.getPrivKeyBytes();
|
||||
this.chainCode = key.getChainCode();
|
||||
this.encryptedKey = null;
|
||||
}
|
||||
|
||||
public DeterministicKey getPrivateKey() {
|
||||
return HDKeyDerivation.createMasterPrivKeyFromBytes(privateKey, chainCode);
|
||||
}
|
||||
|
||||
@ -1,13 +1,6 @@
|
||||
package com.sparrowwallet.drongo.wallet;
|
||||
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.address.P2AAddress;
|
||||
import com.sparrowwallet.drongo.dns.DnsPayment;
|
||||
import com.sparrowwallet.drongo.dns.DnsPaymentCache;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class Payment {
|
||||
private Address address;
|
||||
@ -22,10 +15,10 @@ public class Payment {
|
||||
|
||||
public Payment(Address address, String label, long amount, boolean sendMax, Type type) {
|
||||
this.address = address;
|
||||
this.label = label == null && address instanceof P2AAddress ? address.getOutputScriptDataType() : label;
|
||||
this.label = label;
|
||||
this.amount = amount;
|
||||
this.sendMax = sendMax;
|
||||
this.type = type == Type.DEFAULT && address instanceof P2AAddress ? Type.ANCHOR : type;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public Address getAddress() {
|
||||
@ -69,26 +62,6 @@ public class Payment {
|
||||
}
|
||||
|
||||
public enum Type {
|
||||
DEFAULT, WHIRLPOOL_FEE, FAKE_MIX, MIX, ANCHOR;
|
||||
|
||||
public String toDisplayString() {
|
||||
return Arrays.stream(this.toString().toLowerCase(Locale.ROOT).split("_"))
|
||||
.map(w -> Character.toUpperCase(w.charAt(0)) + w.substring(1))
|
||||
.collect(Collectors.joining(" "));
|
||||
}
|
||||
}
|
||||
|
||||
public String getDisplayAddress() {
|
||||
return address.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
DnsPayment dnsPayment = DnsPaymentCache.getDnsPayment(this);
|
||||
if(dnsPayment != null) {
|
||||
return dnsPayment.toString();
|
||||
}
|
||||
|
||||
return getDisplayAddress();
|
||||
DEFAULT, WHIRLPOOL_FEE, FAKE_MIX, MIX;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
package com.sparrowwallet.drongo.wallet;
|
||||
|
||||
public class Persistable {
|
||||
public static final int MAX_LABEL_LENGTH = 255;
|
||||
|
||||
private Long id;
|
||||
|
||||
public Long getId() {
|
||||
|
||||
@ -1,5 +0,0 @@
|
||||
package com.sparrowwallet.drongo.wallet;
|
||||
|
||||
public enum SortDirection {
|
||||
ASCENDING, DESCENDING
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
package com.sparrowwallet.drongo.wallet;
|
||||
|
||||
public enum TableType {
|
||||
TRANSACTIONS, UTXOS, RECEIVE_ADDRESSES, CHANGE_ADDRESSES, SEARCH_WALLET, WALLET_SUMMARY
|
||||
}
|
||||
@ -1,41 +0,0 @@
|
||||
package com.sparrowwallet.drongo.wallet;
|
||||
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
public record TransactionParameters(List<UtxoSelector> utxoSelectors, List<TxoFilter> txoFilters, List<Payment> payments, List<byte[]> opReturns,
|
||||
Set<WalletNode> excludedChangeNodes, double feeRate, double longTermFeeRate, double minRelayFeeRate, Long fee,
|
||||
Integer currentBlockHeight, boolean groupByAddress, boolean includeMempoolOutputs, boolean allowRbf) {
|
||||
|
||||
public boolean containsSendMaxPayment() {
|
||||
return payments.stream().anyMatch(Payment::isSendMax);
|
||||
}
|
||||
|
||||
public Optional<Payment> getFirstSendMaxPayment() {
|
||||
return payments.stream().filter(Payment::isSendMax).findFirst();
|
||||
}
|
||||
|
||||
public List<Address> getPaymentAddresses() {
|
||||
return payments.stream().map(Payment::getAddress).toList();
|
||||
}
|
||||
|
||||
public long getTotalPaymentAmount() {
|
||||
return payments.stream().mapToLong(Payment::getAmount).sum();
|
||||
}
|
||||
|
||||
public long getTotalPaymentAmountLessExcluded(Payment excludedPayment) {
|
||||
return payments.stream().filter(payment -> !excludedPayment.equals(payment)).mapToLong(Payment::getAmount).sum();
|
||||
}
|
||||
|
||||
public boolean isMinRelayRate() {
|
||||
return ((feeRate == minRelayFeeRate && minRelayFeeRate > 0d) || feeRate == Transaction.DEFAULT_MIN_RELAY_FEE) && fee == null;
|
||||
}
|
||||
|
||||
public long getRequiredFeeAmount(double virtualSize) {
|
||||
return fee == null ? (long)Math.floor(feeRate * virtualSize) : fee;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -5,19 +5,18 @@ 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, BLUE_WALLET, KEYSTONE, SEEDSIGNER, CARAVAN, GORDIAN_SEED_TOOL, JADE, LEDGER_NANO_S_PLUS, EPS, TAPSIGNER, SATSCARD, LABELS,
|
||||
BSMS, KRUX, SATOCHIP, TRANSACTIONS, AIRGAP_VAULT, TREZOR_SAFE_3, SATSCHIP, SAMOURAI, TREZOR_SAFE_5, LEDGER_STAX, LEDGER_FLEX, ONEKEY_CLASSIC_1S, ONEKEY_PRO,
|
||||
KEYCARD_SHELL, KEYCARD, TREZOR_SAFE_7, LEDGER_NANO_GEN5;
|
||||
BSMS, KRUX, SATOCHIP, TRANSACTIONS, AIRGAP_VAULT, TREZOR_SAFE_3, SATSCHIP, SAMOURAI, TREZOR_SAFE_5;
|
||||
|
||||
public static WalletModel getModel(String model) {
|
||||
return valueOf(model.toUpperCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
if(this == TREZOR_1 || this == TREZOR_T || this == TREZOR_SAFE_3 || this == TREZOR_SAFE_5 || this == TREZOR_SAFE_7) {
|
||||
if(this == TREZOR_1 || this == TREZOR_T || this == TREZOR_SAFE_3 || this == TREZOR_SAFE_5) {
|
||||
return "trezor";
|
||||
}
|
||||
|
||||
if(this == LEDGER_NANO_S || this == LEDGER_NANO_X || this == LEDGER_NANO_S_PLUS || this == LEDGER_STAX || this == LEDGER_FLEX || this == LEDGER_NANO_GEN5) {
|
||||
if(this == LEDGER_NANO_S || this == LEDGER_NANO_X || this == LEDGER_NANO_S_PLUS) {
|
||||
return "ledger";
|
||||
}
|
||||
|
||||
@ -53,20 +52,11 @@ public enum WalletModel {
|
||||
return "airgapvault";
|
||||
}
|
||||
|
||||
if(this == ONEKEY_CLASSIC_1S || this == ONEKEY_PRO) {
|
||||
return "onekey";
|
||||
}
|
||||
|
||||
if(this == KEYCARD_SHELL || this == KEYCARD) {
|
||||
return "keycard";
|
||||
}
|
||||
|
||||
return this.toString().toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
public boolean alwaysIncludeNonWitnessUtxo() {
|
||||
if(this == COLDCARD || this == COBO_VAULT || this == PASSPORT || this == KEYSTONE || this == GORDIAN_SEED_TOOL || this == SEEDSIGNER || this == KRUX || this == JADE ||
|
||||
this == TAPSIGNER || this == SATOCHIP || this == KEYCARD_SHELL || this == KEYCARD) {
|
||||
if(this == COLDCARD || this == COBO_VAULT || this == PASSPORT || this == KEYSTONE || this == GORDIAN_SEED_TOOL || this == SEEDSIGNER || this == KRUX) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -74,20 +64,20 @@ public enum WalletModel {
|
||||
}
|
||||
|
||||
public boolean requiresPinPrompt() {
|
||||
return (this == TREZOR_1 || this == KEEPKEY || this == ONEKEY_CLASSIC_1S);
|
||||
return (this == TREZOR_1 || this == KEEPKEY);
|
||||
}
|
||||
|
||||
public boolean externalPassphraseEntry() {
|
||||
return (this == TREZOR_1 || this == KEEPKEY || this == ONEKEY_CLASSIC_1S);
|
||||
return (this == TREZOR_1 || this == KEEPKEY);
|
||||
}
|
||||
|
||||
public boolean isCard() {
|
||||
return (this == TAPSIGNER || this == SATSCHIP || this == SATSCARD || this == SATOCHIP || this == KEYCARD);
|
||||
return (this == TAPSIGNER || this == SATSCHIP || this == SATSCARD || this == SATOCHIP);
|
||||
}
|
||||
|
||||
public boolean hasUsb() {
|
||||
return (this == TREZOR_1 || this == TREZOR_T || this == TREZOR_SAFE_3 || this == TREZOR_SAFE_5 || this == TREZOR_SAFE_7 || this == LEDGER_NANO_S || this == LEDGER_NANO_X || this == LEDGER_NANO_S_PLUS ||
|
||||
this == LEDGER_STAX || this == LEDGER_FLEX || this == LEDGER_NANO_GEN5 || this == DIGITALBITBOX_01 || this == BITBOX_02 || this == COLDCARD || this == KEEPKEY || this == JADE || this == ONEKEY_CLASSIC_1S || this == ONEKEY_PRO);
|
||||
return (this == TREZOR_1 || this == TREZOR_T || this == TREZOR_SAFE_3 || this == TREZOR_SAFE_5 || this == LEDGER_NANO_S || this == LEDGER_NANO_X || this == LEDGER_NANO_S_PLUS ||
|
||||
this == DIGITALBITBOX_01 || this == BITBOX_02 || this == COLDCARD || this == KEEPKEY || this == JADE);
|
||||
}
|
||||
|
||||
public int getMinPinLength() {
|
||||
@ -99,9 +89,7 @@ public enum WalletModel {
|
||||
}
|
||||
|
||||
public int getMaxPinLength() {
|
||||
if(this == KEYCARD) {
|
||||
return 6;
|
||||
} else if(this == SATOCHIP) {
|
||||
if(this == SATOCHIP) {
|
||||
return 16;
|
||||
} else {
|
||||
return 32;
|
||||
@ -116,16 +104,8 @@ public enum WalletModel {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasZeroInPin() {
|
||||
if(this == ONEKEY_CLASSIC_1S) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean requiresSeedInitialization() {
|
||||
if(this == SATOCHIP || this == KEYCARD) {
|
||||
if(this == SATOCHIP) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
@ -133,7 +113,7 @@ public enum WalletModel {
|
||||
}
|
||||
|
||||
public boolean supportsBackup() {
|
||||
if(this == SATOCHIP || this == SATSCHIP || this == KEYCARD) {
|
||||
if(this == SATOCHIP || this == SATSCHIP) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
@ -181,20 +161,10 @@ public enum WalletModel {
|
||||
for(String word : words) {
|
||||
if(word.equals("1")) {
|
||||
word = "one";
|
||||
} else if(Character.isDigit(word.charAt(0))) {
|
||||
word = word.toUpperCase(Locale.ROOT);
|
||||
} else if(BITBOX_02.getType().startsWith(word)) {
|
||||
word = "BitBox";
|
||||
} else if(word.equals(ONEKEY_PRO.getType())) {
|
||||
word = "OneKey";
|
||||
} else if(word.equals("diy")) {
|
||||
word = "DIY";
|
||||
}
|
||||
builder.append(Character.toUpperCase(word.charAt(0)));
|
||||
builder.append(word.substring(1));
|
||||
if(this != BLUE_WALLET) {
|
||||
builder.append(" ");
|
||||
}
|
||||
builder.append(" ");
|
||||
}
|
||||
|
||||
return builder.toString().trim();
|
||||
|
||||
@ -5,7 +5,6 @@ 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.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.Script;
|
||||
|
||||
import java.util.*;
|
||||
@ -15,7 +14,6 @@ public class WalletNode extends Persistable implements Comparable<WalletNode> {
|
||||
private final String derivationPath;
|
||||
private String label;
|
||||
private Address address;
|
||||
private byte[] silentPaymentTweak;
|
||||
private TreeSet<WalletNode> children = new TreeSet<>();
|
||||
private TreeSet<BlockTransactionHashIndex> transactionOutputs = new TreeSet<>();
|
||||
|
||||
@ -100,10 +98,6 @@ public class WalletNode extends Persistable implements Comparable<WalletNode> {
|
||||
return derivation;
|
||||
}
|
||||
|
||||
public boolean isPurposeNode() {
|
||||
return getDerivation().size() == 1;
|
||||
}
|
||||
|
||||
public String getLabel() {
|
||||
return label;
|
||||
}
|
||||
@ -112,14 +106,6 @@ public class WalletNode extends Persistable implements Comparable<WalletNode> {
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
public byte[] getSilentPaymentTweak() {
|
||||
return silentPaymentTweak;
|
||||
}
|
||||
|
||||
public void setSilentPaymentTweak(byte[] silentPaymentTweak) {
|
||||
this.silentPaymentTweak = silentPaymentTweak;
|
||||
}
|
||||
|
||||
public Long getValue() {
|
||||
if(transactionOutputs == null) {
|
||||
return null;
|
||||
@ -234,10 +220,6 @@ public class WalletNode extends Persistable implements Comparable<WalletNode> {
|
||||
}
|
||||
|
||||
public synchronized Set<WalletNode> fillToIndex(int index) {
|
||||
if(wallet.getPolicyType() == PolicyType.SINGLE_SP) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
//Optimization to check if child nodes already monotonically increment to the desired index
|
||||
int indexCheck = 0;
|
||||
for(WalletNode childNode : getChildren()) {
|
||||
@ -263,25 +245,6 @@ public class WalletNode extends Persistable implements Comparable<WalletNode> {
|
||||
return newNodes;
|
||||
}
|
||||
|
||||
public synchronized WalletNode addSilentPaymentChild(Wallet wallet, int index, byte[] tweak) {
|
||||
if(children.stream().anyMatch(n -> n.getIndex() == index)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
WalletNode node = new WalletNode(wallet, getKeyPurpose(), index);
|
||||
node.setSilentPaymentTweak(tweak);
|
||||
children.add(node);
|
||||
|
||||
if(wallet.isValid() && !wallet.getDetachedLabels().isEmpty()) {
|
||||
String label = wallet.getDetachedLabels().remove(node.getAddress().toString());
|
||||
if(label != null && (node.getLabel() == null || node.getLabel().isEmpty())) {
|
||||
node.setLabel(label);
|
||||
}
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The highest used index, or null if no addresses are used
|
||||
*/
|
||||
@ -390,7 +353,6 @@ public class WalletNode extends Persistable implements Comparable<WalletNode> {
|
||||
copy.setId(getId());
|
||||
copy.setLabel(label);
|
||||
copy.setAddress(address);
|
||||
copy.setSilentPaymentTweak(silentPaymentTweak);
|
||||
|
||||
for(WalletNode child : getChildren()) {
|
||||
copy.children.add(child.copy(walletCopy));
|
||||
|
||||
@ -1,18 +0,0 @@
|
||||
package com.sparrowwallet.drongo.wallet;
|
||||
|
||||
public class WalletNodePayment extends Payment {
|
||||
private final WalletNode walletNode;
|
||||
|
||||
public WalletNodePayment(WalletNode walletNode, String label, long amount, boolean sendMax) {
|
||||
this(walletNode, label, amount, sendMax, Type.DEFAULT);
|
||||
}
|
||||
|
||||
public WalletNodePayment(WalletNode walletNode, String label, long amount, boolean sendMax, Type type) {
|
||||
super(walletNode.getAddress(), label, amount, sendMax, type);
|
||||
this.walletNode = walletNode;
|
||||
}
|
||||
|
||||
public WalletNode getWalletNode() {
|
||||
return walletNode;
|
||||
}
|
||||
}
|
||||
@ -1,61 +0,0 @@
|
||||
package com.sparrowwallet.drongo.wallet;
|
||||
|
||||
public class WalletTable extends Persistable {
|
||||
private final TableType tableType;
|
||||
private final Double[] widths;
|
||||
private final int sortColumn;
|
||||
private final SortDirection sortDirection;
|
||||
|
||||
public WalletTable(TableType tableType, Double[] widths, int sortColumn, SortDirection sortDirection) {
|
||||
this.tableType = tableType;
|
||||
this.widths = widths;
|
||||
this.sortColumn = sortColumn;
|
||||
this.sortDirection = sortDirection;
|
||||
}
|
||||
|
||||
public WalletTable(TableType tableType, Double[] widths, Sort sort) {
|
||||
this.tableType = tableType;
|
||||
this.widths = widths;
|
||||
this.sortColumn = sort.sortColumn;
|
||||
this.sortDirection = sort.sortDirection;
|
||||
}
|
||||
|
||||
public TableType getTableType() {
|
||||
return tableType;
|
||||
}
|
||||
|
||||
public Double[] getWidths() {
|
||||
return widths;
|
||||
}
|
||||
|
||||
public int getSortColumn() {
|
||||
return sortColumn;
|
||||
}
|
||||
|
||||
public SortDirection getSortDirection() {
|
||||
return sortDirection;
|
||||
}
|
||||
|
||||
public Sort getSort() {
|
||||
return new Sort(sortColumn, sortDirection);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean equals(Object o) {
|
||||
if(this == o) {
|
||||
return true;
|
||||
}
|
||||
if(!(o instanceof WalletTable that)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return tableType == that.tableType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return tableType.hashCode();
|
||||
}
|
||||
|
||||
public record Sort(int sortColumn, SortDirection sortDirection) {}
|
||||
}
|
||||
@ -1,15 +1,10 @@
|
||||
package com.sparrowwallet.drongo.wallet;
|
||||
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.dns.DnsPayment;
|
||||
import com.sparrowwallet.drongo.dns.DnsPaymentCache;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.*;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.silentpayments.SilentPayment;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* WalletTransaction contains a draft transaction along with associated metadata. The draft transaction has empty signatures but is otherwise complete.
|
||||
@ -26,19 +21,17 @@ public class WalletTransaction {
|
||||
private final Map<Sha256Hash, BlockTransaction> inputTransactions;
|
||||
private final List<Output> outputs;
|
||||
|
||||
public WalletTransaction(Wallet wallet, Transaction transaction, List<UtxoSelector> utxoSelectors, List<Map<BlockTransactionHashIndex, WalletNode>> selectedUtxoSets, List<Payment> payments, List<Output> outputs, long fee) {
|
||||
this(wallet, transaction, utxoSelectors, selectedUtxoSets, payments, outputs, Collections.emptyMap(), 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, List<Map<BlockTransactionHashIndex, WalletNode>> selectedUtxoSets, List<Payment> payments, List<Output> outputs, Map<WalletNode, Long> changeMap, long fee) {
|
||||
this(wallet, transaction, utxoSelectors, selectedUtxoSets, payments, outputs, 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) {
|
||||
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, List<Output> outputs, Map<WalletNode, Long> changeMap, long fee, Map<Sha256Hash, BlockTransaction> inputTransactions) {
|
||||
if(!outputs.stream().map(Output::getTransactionOutput).collect(Collectors.toSet()).containsAll(transaction.getOutputs())) {
|
||||
throw new IllegalArgumentException("Transaction output list does not contain all outputs from the transaction");
|
||||
}
|
||||
|
||||
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;
|
||||
@ -47,7 +40,7 @@ public class WalletTransaction {
|
||||
this.changeMap = changeMap;
|
||||
this.fee = fee;
|
||||
this.inputTransactions = inputTransactions;
|
||||
this.outputs = outputs;
|
||||
this.outputs = calculateOutputs();
|
||||
|
||||
for(Payment payment : payments) {
|
||||
payment.setLabel(getOutputLabel(payment));
|
||||
@ -117,7 +110,7 @@ public class WalletTransaction {
|
||||
}
|
||||
|
||||
public List<Output> getOutputs() {
|
||||
return Collections.unmodifiableList(outputs);
|
||||
return outputs;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -136,6 +129,10 @@ public class WalletTransaction {
|
||||
return !utxoSelectors.isEmpty() && utxoSelectors.get(0) instanceof StonewallUtxoSelector;
|
||||
}
|
||||
|
||||
public boolean isConsolidationSend(Payment payment) {
|
||||
return isWalletSend(getWallet(), payment);
|
||||
}
|
||||
|
||||
public boolean isPremixSend(Payment payment) {
|
||||
return isWalletSend(StandardAccount.WHIRLPOOL_PREMIX, payment);
|
||||
}
|
||||
@ -162,7 +159,7 @@ public class WalletTransaction {
|
||||
return false;
|
||||
}
|
||||
|
||||
return wallet.getWalletAddresses().get(payment.getAddress()) != null;
|
||||
return getAddressNodeMap(wallet).get(payment.getAddress()) != null;
|
||||
}
|
||||
|
||||
private String getOutputLabel(Payment payment) {
|
||||
@ -171,7 +168,7 @@ public class WalletTransaction {
|
||||
}
|
||||
|
||||
if(payment.getType() == Payment.Type.WHIRLPOOL_FEE) {
|
||||
return payment.getType().toDisplayString();
|
||||
return "Whirlpool fee";
|
||||
} else if(isPremixSend(payment)) {
|
||||
int premixIndex = getOutputIndex(payment.getAddress(), payment.getAmount(), Collections.emptySet()) - 2;
|
||||
return "Premix #" + premixIndex;
|
||||
@ -192,14 +189,9 @@ public class WalletTransaction {
|
||||
public Wallet getToWallet(Collection<Wallet> wallets, Payment payment) {
|
||||
for(Wallet openWallet : wallets) {
|
||||
if(openWallet != getWallet() && openWallet.isValid()) {
|
||||
if(openWallet.getPolicyType() == PolicyType.SINGLE_SP && payment instanceof SilentPayment silentPayment
|
||||
&& silentPayment.getSilentPaymentAddress().equals(openWallet.getSilentPaymentScanAddress().getSilentPaymentAddress())) {
|
||||
return openWallet;
|
||||
} else {
|
||||
WalletNode addressNode = openWallet.getWalletAddresses().get(payment.getAddress());
|
||||
if(addressNode != null) {
|
||||
return addressNode.getWallet();
|
||||
}
|
||||
WalletNode addressNode = openWallet.getWalletAddresses().get(payment.getAddress());
|
||||
if(addressNode != null) {
|
||||
return addressNode.getWallet();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -208,17 +200,63 @@ public class WalletTransaction {
|
||||
}
|
||||
|
||||
public boolean isDuplicateAddress(Payment payment) {
|
||||
return getPayments().stream().filter(p -> payment != p && !(payment instanceof SilentPayment))
|
||||
.anyMatch(p -> payment.getAddress() != null && payment.getAddress().equals(p.getAddress()));
|
||||
return getPayments().stream().filter(p -> payment != p).anyMatch(p -> payment.getAddress() != null && payment.getAddress().equals(p.getAddress()));
|
||||
}
|
||||
|
||||
public boolean isConsolidation(Payment payment) {
|
||||
return payment instanceof WalletNodePayment || (wallet != null && wallet.getPolicyType() == PolicyType.SINGLE_SP
|
||||
&& payment instanceof SilentPayment silentPayment && wallet.getSilentPaymentScanAddress().getSilentPaymentAddress().equals(silentPayment.getSilentPaymentAddress()));
|
||||
public void updateAddressNodeMap(Map<Wallet, Map<Address, WalletNode>> addressNodeMap, Wallet wallet) {
|
||||
this.addressNodeMap = addressNodeMap;
|
||||
getAddressNodeMap(wallet);
|
||||
}
|
||||
|
||||
public List<WalletNodePayment> getWalletNodePayments() {
|
||||
return payments.stream().filter(payment -> payment instanceof WalletNodePayment).map(payment -> (WalletNodePayment)payment).collect(Collectors.toList());
|
||||
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(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 walletAddressNodeMap;
|
||||
}
|
||||
|
||||
private List<Output> calculateOutputs() {
|
||||
List<Output> outputs = new ArrayList<>();
|
||||
|
||||
for(int i = 0, paymentIndex = 0; i < transaction.getOutputs().size(); i++) {
|
||||
TransactionOutput txOutput = transaction.getOutputs().get(i);
|
||||
Address address = txOutput.getScript().getToAddress();
|
||||
if(address == null) {
|
||||
outputs.add(new NonAddressOutput(txOutput));
|
||||
} else if(paymentIndex < payments.size()) {
|
||||
Payment payment = payments.get(paymentIndex++);
|
||||
outputs.add(new PaymentOutput(txOutput, payment));
|
||||
}
|
||||
}
|
||||
|
||||
Set<Integer> seenIndexes = new HashSet<>();
|
||||
for(Map.Entry<WalletNode, Long> changeEntry : changeMap.entrySet()) {
|
||||
int outputIndex = getOutputIndex(changeEntry.getKey().getAddress(), changeEntry.getValue(), seenIndexes);
|
||||
TransactionOutput txOutput = transaction.getOutputs().get(outputIndex);
|
||||
seenIndexes.add(outputIndex);
|
||||
outputs.add(outputIndex, new ChangeOutput(txOutput, changeEntry.getKey(), changeEntry.getValue()));
|
||||
}
|
||||
|
||||
return outputs;
|
||||
}
|
||||
|
||||
public static class Output {
|
||||
@ -231,10 +269,6 @@ public class WalletTransaction {
|
||||
public TransactionOutput getTransactionOutput() {
|
||||
return transactionOutput;
|
||||
}
|
||||
|
||||
public Map<String, byte[]> getDnsSecProof() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static class NonAddressOutput extends Output {
|
||||
@ -254,48 +288,13 @@ public class WalletTransaction {
|
||||
public Payment getPayment() {
|
||||
return payment;
|
||||
}
|
||||
|
||||
public Map<String, byte[]> getDnsSecProof() {
|
||||
DnsPayment dnsPayment = DnsPaymentCache.getDnsPayment(payment);
|
||||
if(dnsPayment != null) {
|
||||
if(dnsPayment.hasAddress()) {
|
||||
return Map.of(dnsPayment.hrn(), dnsPayment.proofChain());
|
||||
} else if(dnsPayment.hasSilentPaymentAddress()) {
|
||||
return Map.of(dnsPayment.hrn(), dnsPayment.proofChain());
|
||||
}
|
||||
}
|
||||
|
||||
return super.getDnsSecProof();
|
||||
}
|
||||
}
|
||||
|
||||
public static class SilentPaymentOutput extends PaymentOutput {
|
||||
public SilentPaymentOutput(TransactionOutput transactionOutput, SilentPayment silentPayment) {
|
||||
super(transactionOutput, silentPayment);
|
||||
}
|
||||
|
||||
public SilentPayment getSilentPayment() {
|
||||
return (SilentPayment)getPayment();
|
||||
}
|
||||
}
|
||||
|
||||
public static class SilentPaymentChangeOutput extends SilentPaymentOutput {
|
||||
public SilentPaymentChangeOutput(TransactionOutput transactionOutput, SilentPayment silentPayment) {
|
||||
super(transactionOutput, silentPayment);
|
||||
}
|
||||
}
|
||||
|
||||
public static class SilentPaymentConsolidationOutput extends SilentPaymentOutput {
|
||||
public SilentPaymentConsolidationOutput(TransactionOutput transactionOutput, SilentPayment silentPayment) {
|
||||
super(transactionOutput, silentPayment);
|
||||
}
|
||||
}
|
||||
|
||||
public static class WalletNodeOutput extends Output {
|
||||
public static class ChangeOutput extends Output {
|
||||
private final WalletNode walletNode;
|
||||
private final Long value;
|
||||
|
||||
public WalletNodeOutput(TransactionOutput transactionOutput, WalletNode walletNode, Long value) {
|
||||
public ChangeOutput(TransactionOutput transactionOutput, WalletNode walletNode, Long value) {
|
||||
super(transactionOutput);
|
||||
this.walletNode = walletNode;
|
||||
this.value = value;
|
||||
@ -309,23 +308,4 @@ public class WalletTransaction {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
public static class ConsolidationOutput extends WalletNodeOutput {
|
||||
private final WalletNodePayment walletNodePayment;
|
||||
|
||||
public ConsolidationOutput(TransactionOutput transactionOutput, WalletNodePayment walletNodePayment, Long value) {
|
||||
super(transactionOutput, walletNodePayment.getWalletNode(), value);
|
||||
this.walletNodePayment = walletNodePayment;
|
||||
}
|
||||
|
||||
public WalletNodePayment getWalletNodePayment() {
|
||||
return walletNodePayment;
|
||||
}
|
||||
}
|
||||
|
||||
public static class ChangeOutput extends WalletNodeOutput {
|
||||
public ChangeOutput(TransactionOutput transactionOutput, WalletNode walletNode, Long value) {
|
||||
super(transactionOutput, walletNode, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,227 +0,0 @@
|
||||
package com.sparrowwallet.drongo.wallet.bip93;
|
||||
|
||||
import com.sparrowwallet.drongo.protocol.Bech32;
|
||||
import com.sparrowwallet.drongo.protocol.ProtocolException;
|
||||
import com.sparrowwallet.drongo.wallet.MnemonicException;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
|
||||
public class Codex32 {
|
||||
|
||||
private static final String HRP = "ms";
|
||||
private static final char SPECIAL_SHARE_INDEX = 's';
|
||||
private static final int CODEX32_ID_LEN = 4;
|
||||
|
||||
public static class Codex32Data {
|
||||
public final byte[] rawData;
|
||||
public final ChecksumType checksumType;
|
||||
|
||||
public Codex32Data(byte[] rawData, ChecksumType checksumType) {
|
||||
this.rawData = rawData;
|
||||
this.checksumType = checksumType;
|
||||
}
|
||||
|
||||
public byte getThreshold() {
|
||||
return rawData[0];
|
||||
}
|
||||
|
||||
public int getThresholdAsInt() {
|
||||
return Bech32.CHARSET.charAt(getThreshold()) - '0';
|
||||
}
|
||||
|
||||
public byte[] getIdentifier() {
|
||||
return Arrays.copyOfRange(rawData, 1, 5);
|
||||
}
|
||||
|
||||
public String identifierAsString() {
|
||||
byte[] id = getIdentifier();
|
||||
|
||||
StringBuilder sb = new StringBuilder(CODEX32_ID_LEN);
|
||||
for(byte b : id) {
|
||||
sb.append(Bech32.CHARSET.charAt(b));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public byte getShareIndex() {
|
||||
return rawData[5];
|
||||
}
|
||||
|
||||
public boolean isUnsharedSecret() {
|
||||
return getShareIndex() == Bech32.CHARSET_REV[SPECIAL_SHARE_INDEX];
|
||||
}
|
||||
|
||||
public byte[] getPayload() {
|
||||
return Arrays.copyOfRange(rawData, 6, rawData.length);
|
||||
}
|
||||
|
||||
public byte[] payloadToBip32Secret() throws MnemonicException {
|
||||
if(!isUnsharedSecret()) {
|
||||
throw new MnemonicException("Trying to get secret from non-secret share");
|
||||
}
|
||||
byte[] payload = getPayload();
|
||||
return Bech32.convertBits(payload, 0, payload.length, 5, 8, false, false);
|
||||
}
|
||||
}
|
||||
|
||||
public static String encode(Codex32Data data) throws MnemonicException {
|
||||
byte[] checksum = createChecksum(data.rawData, data.checksumType);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(Codex32.HRP);
|
||||
sb.append(Bech32.BECH32_SEPARATOR);
|
||||
for(byte b : data.rawData) {
|
||||
sb.append(Bech32.CHARSET.charAt(b));
|
||||
}
|
||||
for(byte b : checksum) {
|
||||
sb.append(Bech32.CHARSET.charAt(b));
|
||||
}
|
||||
String result = sb.toString();
|
||||
validate(result, 2);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static Codex32Data decode(String str) throws MnemonicException {
|
||||
final int separatorPos = str.lastIndexOf(Bech32.BECH32_SEPARATOR);
|
||||
|
||||
validate(str, separatorPos);
|
||||
|
||||
byte[] rawData = Bech32.rawDecode(str, separatorPos);
|
||||
int dataLen = rawData.length;
|
||||
|
||||
ChecksumType checksumType = verifyChecksum(rawData);
|
||||
|
||||
byte[] dataPart = new byte[dataLen - checksumType.length];
|
||||
System.arraycopy(rawData, 0, dataPart, 0, dataLen - checksumType.length);
|
||||
|
||||
return new Codex32Data(dataPart, checksumType);
|
||||
}
|
||||
|
||||
public static void validate(String str, int separatorPos) throws MnemonicException {
|
||||
if(str.length() < 48 || str.length() > 127) {
|
||||
throw new MnemonicException("Input invalid length: " + str.length());
|
||||
}
|
||||
|
||||
try {
|
||||
// Can set checksum len to zero, since we validate if string is too short above
|
||||
Bech32.validate(str, 127, separatorPos, 0);
|
||||
} catch(ProtocolException e) {
|
||||
throw new MnemonicException("Input is not valid Bech32 string: " + e.getMessage());
|
||||
}
|
||||
|
||||
String hrp = str.substring(0, separatorPos).toLowerCase(Locale.ROOT);
|
||||
if(!HRP.equals(hrp)) {
|
||||
throw new MnemonicException("Input does not have Codex32 \"ms\" human-readable part: " + hrp);
|
||||
}
|
||||
|
||||
if(str.charAt(3) == '0' && str.toLowerCase(Locale.ROOT).charAt(8) != SPECIAL_SHARE_INDEX) {
|
||||
throw new MnemonicException("Non zero threshold with unshared secret share: " + str.charAt(8));
|
||||
}
|
||||
|
||||
int threshold = str.charAt(3) - '0';
|
||||
if(!((threshold == 0) || (2 <= threshold && threshold <= 9))) {
|
||||
throw new MnemonicException("Threshold not in range: " + threshold);
|
||||
}
|
||||
}
|
||||
|
||||
private static ChecksumType verifyChecksum(final byte[] values) throws MnemonicException {
|
||||
int dataLen = values.length;
|
||||
|
||||
ChecksumType checksumType;
|
||||
if(dataLen <= 92) {
|
||||
checksumType = ChecksumType.CODEX32;
|
||||
} else if(dataLen >= 96 && dataLen <= 124) {
|
||||
checksumType = ChecksumType.CODEX32_LONG;
|
||||
} else {
|
||||
throw new MnemonicException("Data part invalid length: " + dataLen);
|
||||
}
|
||||
|
||||
int payloadLen = dataLen - 6 - checksumType.length;
|
||||
if((payloadLen * 5) % 8 > 4) {
|
||||
throw new MnemonicException("Payload invalid length, incomplete group greater than 4 bits");
|
||||
}
|
||||
boolean verified = checksumType.polymod(values).equals(checksumType.constant);
|
||||
if(!verified) {
|
||||
throw new MnemonicException("Invalid Checksum");
|
||||
}
|
||||
|
||||
return checksumType;
|
||||
}
|
||||
|
||||
private static byte[] createChecksum(final byte[] values, ChecksumType checksumType) {
|
||||
BigInteger polymodInt = checksumType.polymod(Arrays.copyOf(values, values.length + checksumType.length));
|
||||
polymodInt = polymodInt.xor(checksumType.constant);
|
||||
byte[] buffer = new byte[checksumType.length];
|
||||
for(int i = 0; i < checksumType.length; i++) {
|
||||
byte[] intermediate = polymodInt.shiftRight(5 * (checksumType.length - 1 - i)).toByteArray();
|
||||
buffer[i] = (byte) (intermediate[intermediate.length - 1] & (byte) 31);
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
public enum ChecksumType {
|
||||
CODEX32(new BigInteger("10ce0795c2fd1e62a", 16), 13) {
|
||||
@Override
|
||||
public BigInteger polymod(final byte[] values) {
|
||||
BigInteger gen0 = new BigInteger("19dc500ce73fde210", 16);
|
||||
BigInteger gen1 = new BigInteger("1bfae00def77fe529", 16);
|
||||
BigInteger gen2 = new BigInteger("1fbd920fffe7bee52", 16);
|
||||
BigInteger gen3 = new BigInteger("1739640bdeee3fdad", 16);
|
||||
BigInteger gen4 = new BigInteger("07729a039cfc75f5a", 16);
|
||||
|
||||
BigInteger sixtyOnes = new BigInteger("0fffffffffffffff", 16);
|
||||
BigInteger residue = new BigInteger("23181b3", 16);
|
||||
|
||||
for(byte v_i : values) {
|
||||
BigInteger b = residue.shiftRight(60);
|
||||
residue = residue.and(sixtyOnes).shiftLeft(5);
|
||||
residue = residue.xor(BigInteger.valueOf(v_i));
|
||||
if(b.shiftRight(0).and(BigInteger.ONE).equals(BigInteger.ONE)) residue = residue.xor(gen0);
|
||||
if(b.shiftRight(1).and(BigInteger.ONE).equals(BigInteger.ONE)) residue = residue.xor(gen1);
|
||||
if(b.shiftRight(2).and(BigInteger.ONE).equals(BigInteger.ONE)) residue = residue.xor(gen2);
|
||||
if(b.shiftRight(3).and(BigInteger.ONE).equals(BigInteger.ONE)) residue = residue.xor(gen3);
|
||||
if(b.shiftRight(4).and(BigInteger.ONE).equals(BigInteger.ONE)) residue = residue.xor(gen4);
|
||||
}
|
||||
return residue;
|
||||
}
|
||||
},
|
||||
CODEX32_LONG(new BigInteger("43381e570bf4798ab26", 16), 15) {
|
||||
public BigInteger polymod(final byte[] values) {
|
||||
BigInteger gen0 = new BigInteger("3d59d273535ea62d897", 16);
|
||||
BigInteger gen1 = new BigInteger("7a9becb6361c6c51507", 16);
|
||||
BigInteger gen2 = new BigInteger("543f9b7e6c38d8a2a0e", 16);
|
||||
BigInteger gen3 = new BigInteger("0c577eaeccf1990d13c", 16);
|
||||
BigInteger gen4 = new BigInteger("1887f74f8dc71b10651", 16);
|
||||
|
||||
BigInteger seventyOnes = new BigInteger("3fffffffffffffffff", 16);
|
||||
BigInteger residue = new BigInteger("23181b3", 16);
|
||||
|
||||
for(byte v_i : values) {
|
||||
BigInteger b = residue.shiftRight(70);
|
||||
residue = residue.and(seventyOnes).shiftLeft(5);
|
||||
residue = residue.xor(BigInteger.valueOf(v_i));
|
||||
if(b.shiftRight(0).and(BigInteger.ONE).equals(BigInteger.ONE)) residue = residue.xor(gen0);
|
||||
if(b.shiftRight(1).and(BigInteger.ONE).equals(BigInteger.ONE)) residue = residue.xor(gen1);
|
||||
if(b.shiftRight(2).and(BigInteger.ONE).equals(BigInteger.ONE)) residue = residue.xor(gen2);
|
||||
if(b.shiftRight(3).and(BigInteger.ONE).equals(BigInteger.ONE)) residue = residue.xor(gen3);
|
||||
if(b.shiftRight(4).and(BigInteger.ONE).equals(BigInteger.ONE)) residue = residue.xor(gen4);
|
||||
}
|
||||
return residue;
|
||||
}
|
||||
};
|
||||
|
||||
public final BigInteger constant;
|
||||
public final int length;
|
||||
|
||||
ChecksumType(BigInteger constant, int length) {
|
||||
this.constant = constant;
|
||||
this.length = length;
|
||||
}
|
||||
|
||||
public BigInteger polymod(final byte[] values) {
|
||||
return BigInteger.ZERO;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -6,8 +6,8 @@ open module com.sparrowwallet.drongo {
|
||||
requires org.slf4j;
|
||||
requires ch.qos.logback.core;
|
||||
requires ch.qos.logback.classic;
|
||||
requires org.dnsjava;
|
||||
requires com.github.benmanes.caffeine;
|
||||
requires json.simple;
|
||||
requires org.bitcoinj.secp.api;
|
||||
exports com.sparrowwallet.drongo;
|
||||
exports com.sparrowwallet.drongo.psbt;
|
||||
exports com.sparrowwallet.drongo.protocol;
|
||||
@ -18,9 +18,6 @@ open module com.sparrowwallet.drongo {
|
||||
exports com.sparrowwallet.drongo.policy;
|
||||
exports com.sparrowwallet.drongo.uri;
|
||||
exports com.sparrowwallet.drongo.bip47;
|
||||
exports com.sparrowwallet.drongo.dns;
|
||||
exports com.sparrowwallet.drongo.wallet.bip93;
|
||||
exports com.sparrowwallet.drongo.wallet.slip39;
|
||||
exports com.sparrowwallet.drongo.silentpayments;
|
||||
exports org.bitcoin;
|
||||
}
|
||||
@ -4,23 +4,16 @@ import com.sparrowwallet.drongo.NativeUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
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() {
|
||||
@ -30,56 +23,30 @@ public class Secp256k1Context {
|
||||
public static long getContext() {
|
||||
if (!enabled)
|
||||
return -1; // sanity check
|
||||
return context;
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
private static boolean loadLibrary() {
|
||||
String osName = System.getProperty("os.name");
|
||||
String osArch = System.getProperty("os.arch");
|
||||
String libName;
|
||||
if(osName.startsWith("Windows")) {
|
||||
libName = "libsecp256k1-0.dll";
|
||||
} else if(osName.startsWith("Mac")) {
|
||||
libName = "libsecp256k1.dylib";
|
||||
} else {
|
||||
libName = "libsecp256k1.so";
|
||||
}
|
||||
|
||||
// Try loading from the application image lib/ directory
|
||||
String javaHome = System.getProperty("java.home");
|
||||
if(javaHome != null) {
|
||||
File libFile = new File(javaHome, "lib" + java.io.File.separator + libName);
|
||||
if(libFile.exists()) {
|
||||
try {
|
||||
System.load(libFile.getAbsolutePath());
|
||||
return true;
|
||||
} catch(UnsatisfiedLinkError e) {
|
||||
log.debug("Could not load libsecp256k1 from java.home, falling back to JAR extraction", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: extract from JAR
|
||||
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/" + libName);
|
||||
NativeUtils.loadLibraryFromJar("/native/osx/aarch64/libsecp256k1.dylib");
|
||||
} else if(osName.startsWith("Mac")) {
|
||||
NativeUtils.loadLibraryFromJar("/native/osx/x64/" + libName);
|
||||
NativeUtils.loadLibraryFromJar("/native/osx/x64/libsecp256k1.dylib");
|
||||
} else if(osName.startsWith("Windows")) {
|
||||
NativeUtils.loadLibraryFromJar("/native/windows/x64/" + libName);
|
||||
NativeUtils.loadLibraryFromJar("/native/windows/x64/libsecp256k1-0.dll");
|
||||
} else if(osArch.equals("aarch64")) {
|
||||
NativeUtils.loadLibraryFromJar("/native/linux/aarch64/" + libName);
|
||||
NativeUtils.loadLibraryFromJar("/native/linux/aarch64/libsecp256k1.so");
|
||||
} else {
|
||||
NativeUtils.loadLibraryFromJar("/native/linux/x64/" + libName);
|
||||
NativeUtils.loadLibraryFromJar("/native/linux/x64/libsecp256k1.so");
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch(UnsatisfiedLinkError | IOException e) {
|
||||
} catch(IOException e) {
|
||||
log.error("Error loading libsecp256k1 library", e);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static native long secp256k1_init_context();
|
||||
}
|
||||
|
||||
@ -1,38 +0,0 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
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
|
||||
=PElt
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
@ -1,88 +0,0 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
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
|
||||
Tnzr2nbMgOg+oGss0ScQUQ==
|
||||
=WZyK
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
@ -1,89 +0,0 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
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
|
||||
EmR2yA==
|
||||
=t5JY
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
@ -1,52 +0,0 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
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
|
||||
=/fv8
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
@ -1,111 +0,0 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mQINBFykyRABEADKl77HiQdiSp+F01Je5IPynLe93woyfKUoEVQQFvgjfFd2ZnYB
|
||||
1mXtV3X6Y2tEKCaVilw73mCfCJEu5AGGddN3mEnkDygHB70ZGOvK1KHzA9SrQRi+
|
||||
M/qgN0e2eDFxLOgZVjexa4VxNA+bRomg/eOeSq4lDWM+Sxk70aPlLUzG2U6ww7Qc
|
||||
G3gam0/qyOKeIYqHiPnlj2MWDD03DwTE6wrim/SQODRGyFjgpBQiJoxzaTzG+uLr
|
||||
3GI/7wmpTd/zuk6o9ixg9n1xfqzcStoWmiv5szfVsvej77LS6cb1aB1VvvjgcYEz
|
||||
Bp3ZDafg+INU9PcePNYpoJCy4riqv6573allnN3SGLBkAmtyJJjsDDKMy9Ql5T1s
|
||||
QSYnQd9wFk9NPf/pW4gIeXEGi+tQ/u+9PdP5Slp67gEQqYI3rvUl1CnKDYgcayCj
|
||||
cyl9c7ScstKoODsoC80Fxy20IdZsfdd5Nd0+Kf6YpR1xTDy4x12/UL+8+DTx2wtU
|
||||
qjwkW3gLvzcn9Vr1fqAgdFWe85830wj5Uxbpvlb+SJbSXNXzzRAL2XwOk21qKtug
|
||||
DJ9sqTsiVFxjd2z8oiZ/EhqR5bePdSwrHVsTQUX77XV2WVSOm7fMtWfeoEUz0cN4
|
||||
9LqYNK02pFNQwZqJf86dI1AKseXQ4w7NeuhPH4F74/RQkl7g3J3kw0WJkwARAQAB
|
||||
tCRNYXJrbyBCZW5jdW4gPG1iZW5jdW4rcGdwQGdtYWlsLmNvbT6JAk4EEwEKADgW
|
||||
IQQiYOSCiIgsdq+qMZ1norFg902ydQUCXKTJEAIbAwULCQgHAwUVCgkICwUWAgMB
|
||||
AAIeAQIXgAAKCRBnorFg902ydcVTEADCl9VzrFoLkhI/KX0J2QDrZV7eVcbLPGMI
|
||||
ZYpsVl+VAga3Zw9Gpb8cbn73b5yWp2cYrdgcFOrz4EZPuC4SazIKf77z7EF01xQc
|
||||
k27PWm3WV7cD8+knldYJUiw9jv5M8162ns5li0DlVFHGFvCWMSDnPU53fu0JelsQ
|
||||
9qFnohKT2/3Tn5C+YgXJaIidKhc82/Tq3VL6b3jgJQPCRsDLcx9IpXUb2OBIVTwc
|
||||
lmwDGmtRQHYIgOR7VD+RM6cmmpsudDMCre2L+GbxrdPWc5HRJm7Iq0WEN2mGuTg0
|
||||
nxuKOoVaKBHs3zFJJPBuSP9hAS0iolUTG8oiDAFYI6keOO/CJHIEBj1ruDWwk939
|
||||
xWrZuu5DnueR/kKlNCviN5/MAOKIRMBI5s1v4OpXK+PiTQGCKW9od+la2QPqh+2B
|
||||
WX08CdgB/23dWxEYLPvq/MYkBfOG6Mw9cip01IG2hmucf/Y/d37WxdNwsvFIknhe
|
||||
n0xfJkOcG7coEhAtJsxwZYOwSfy95e6ZcxNL2Kow6lw2pMnm3N09eTHpo3zoA/hT
|
||||
N0tS4PWGmzG9VTM4brR11dmUKcrycI/q8ClD7y5hvpkhhW/XCTVprz6yer1sETAp
|
||||
1igpRaDSnKdKymUy5QNVSDhKFaKJeyfS4Kih4zicbKWIHcPvfqIJO2xTclMT0cm6
|
||||
FN1pNTJ244kCMwQTAQoAHRYhBEPB8+16Yktsdfed3U81WIqvDnKKBQJcpuEkAAoJ
|
||||
EE81WIqvDnKKM6sQAL3Hxpbvyc6dUZg/7g38yA4UDuus9WrGlgrF2fc8z70BmK5+
|
||||
nJ/0cdXWTYSbkiO10Ld6l0QHdcsXbX0P+SjUe5nT54z9MqiT4TF4Ix3GkeSWyWtN
|
||||
qloz3lSqHtwab0KF1GkRnqZwmoBdu4wEGibKR5WL83qYr8IKHcKsvrtpvZEQ8KJ2
|
||||
ZqIX98ArKfZoWDyAKvhixWMXnkMDWrgxmzKVBjL61X+N+N7wTcO690bPwBRo//0L
|
||||
1o0qq38Qn0qbgMvCtS61H1VJpGIILYhsQiO8a/M50kh0FmJHij+stgXZ+ig8seP0
|
||||
y+AyAehAgh3+oUqDHF2qKgcHJvbUVQATD5ZPpDTQVYdxUfd7HnbUr0QxQ6INs9vO
|
||||
kbewCTZ8NEIzbBZc/O8/+6Yv2i2mPWHQS0SPqZQ+3GpmpdZZzUFgkNg5H+D9xT4u
|
||||
Y8+m1cgkRZ6IwYgyT7VJZmFfXC32qTVSUGVhSo4nw79x2OwCgJbYwozS1FgHeg+6
|
||||
qGUXCVghkXkLMJX4rgPjMUZh0Cs8Plv57yrKEwBiXuq/xlvLWlodFLpgcNOlMHrq
|
||||
dXaWlegBY3ERy0tcGKc/DxmT7nuT9uHo/DxOrpWSq0SJAeOoVENcmbpM+5SBGiBb
|
||||
AYIoqK/GpdlipNC2YI4T8tVUuQt9hNJKGDtS0xvCbkSMoT6BN/kU9pVNCXPutCNN
|
||||
YXJrbyBCZW5jdW4gPG1hcmtvQHNoaWZ0Y3J5cHRvLmNoPokCTgQTAQoAOBYhBCJg
|
||||
5IKIiCx2r6oxnWeisWD3TbJ1BQJcquSmAhsDBQsJCAcDBRUKCQgLBRYCAwEAAh4B
|
||||
AheAAAoJEGeisWD3TbJ1BZ8P+gIx/naK/bmigFeo6s+/tyN0p48TT7WGhTrYbfjW
|
||||
q5gbETMvyyuuY2m4aCF+/y9QLHnPAcE9IayDztNIjdknUIJSD/o4MYfjYJNoO1SW
|
||||
mXERHhHU1pBQrU5ZZckJz+9kEbC84vE4t8b8Fkr90T45AqXTaxSQrxe6u3iBnuCy
|
||||
h7KnSO64LHkMpFdrRtsHB1oqV3u7BinoUmcMgk37YLwVBCmDubzPF4bEzHWQCFfb
|
||||
N5+qoIEri74RwKWc9ovAbQy400e+9nth7Sv9re5wgZXnCdL9LyhQaUWdCOCiLqZE
|
||||
B8Z6VTx1hWlHcrcHPtWgNlaQ6cINJaJdBssnYQ7MW7wXN9uxAbpcK/x/neW7sx7T
|
||||
dRKJIiQHBKFJh/FP30Hlk+t5MC+zan9rytKnukWHxFsvbNMLxgAIWEndQSP/uu5C
|
||||
w8VJeMNlzojs5LsaqraOqsMnqljkMqIHYpN3o0ZSEo4PicR3vKUW0uNoB/6hGt/m
|
||||
ZqmJ+GsfH7IThsCpr2IGH4SDDtyOO7IT0FcANx49gnazjC7e6fa8WVUoYI5+rY5e
|
||||
ONbjoxVUJc4EHifgd4mxA0RhFzBDYHtXpH4ifmHQeyeVX4Ch/CuEcNOQ+k81LZgT
|
||||
5TUcRxNNHJJZAxo1xPIFIkLk9uo/Rx7z5PUag+VEq+qjx5WuQmg5R9udDkRIq9ht
|
||||
bBWHuQINBFykzAABEADRk7lGp678clcOb/PFefNtquu8j2m279IgtimBJf7Trynq
|
||||
mHajkL4KyO4k1eifCD47lzdxSJ/MJoKS2kyuDU6gvVlNMy98HbchanzwmR3ZxLzs
|
||||
ld0StClTJqxPPFAEnaeKbYbeQ1cwVPVDCCXthlETwEdN1sSt5eLoNWBfeKpVaNNX
|
||||
eYc0imVYJXZs1PK4tPV3AMUB9oL/lv0qqOAyHhZNMNCTUqeMF9CEwAsXbxFZKDKE
|
||||
jPoXgYGIoDxs/5Z4ZW5gxLdyhFiPN0Gu+dFXKrOjDIe6HNWOFz4yfhVmgg/LvQ7/
|
||||
wWlqrAsh7eLuccfvAqrarwvwZCoSwWeEUv2PNwq2dYMAwn9gUrxJmaZAEAb50p2I
|
||||
3+zmnSp9B1BLCSKHwOneYRNBUskbjhHJcoGchnO8o8mplYGFnyUjRA2TSUGydm1i
|
||||
SHYSKLMrQZY0ABl7pTGm4xL1AOAXa2dTW0nzDMQGdu3wtN4yfzIgZtbM2cBVy7hE
|
||||
CevmkL8uqGAFWPGiGIOfOxrHv48dBbcY944OIqmwYMHGmVn95+DAgCtpBIMCmbwn
|
||||
n5VhBScPefeAHcaMQUrBVqyB3hazsK2PnuiiSMRkdSKoORNck6XV+1qjoXRc/r9C
|
||||
VDkziNqAcQ6NecJ1QjZQ//07RFNslpX1J7qaXu1ESkSbqM9aRNc6T0Puder3/wAR
|
||||
AQABiQRyBBgBCgAmFiEEImDkgoiILHavqjGdZ6KxYPdNsnUFAlykzAACGwIFCQPC
|
||||
ZwACQAkQZ6KxYPdNsnXBdCAEGQEKAB0WIQQtiHaBCrCS5FHcqJSARTiSjDfq6AUC
|
||||
XKTMAAAKCRCARTiSjDfq6BwED/4oFrXq/vfpVLlQmciYtIwHK84A8VBSbzmRCIbq
|
||||
wBNCILRE32vhf1NzmcYI2PgjNnFr9vmuWax8s6l1bjSv2nO3lNcu4eRTejU4zAkM
|
||||
h6OebYFvuCtILbsgJ6O7K81xd7Ki4HT36DzAdO+KUTZbH+dE8l75IgoaZUmvP9db
|
||||
90BOM+6kmJTu9z5kgkP8kRkVHYXCG9/6/q5E7zOkPU5LDQOORPB/scANNJt54k5V
|
||||
XbilpU4d5zBmvPdVq11dMeTZlfunVl8noQvx0DsazdXcHyCANO9KoOygcrLlIgkm
|
||||
4qrCZRw2e9R38PSQEi66iFUYatqRSD0IDx7YHtr2IpdcQ37PjwmHy8TSqcFsEie9
|
||||
szwRd0jujcxNHxQt+FB0h2AWyn4pbGelXBnxGeepBq9WYHbDIx+HHh1coHybIG1h
|
||||
lqwzESilOVYBZ6LfNq5jgP7ZPygjB9lFdsuiUaVVMjek3NrmgzgqU2RZV3jB9qQg
|
||||
VqEKgIkByG0HSRhgj5QjjiOe2KLpD5xhFEBaQNn4YvlS/7yfsMfS9yMw1J2DoMP0
|
||||
vCXOKsF0R0JiwWymEwhIyJQPhBSDkvt8VYIwrjMYxK4Bm21GsDUrJ9bqEOwIYPvQ
|
||||
b9tvwLzlpdj4tDDaI8sahA4xa0FzVfleRLvoAXK0wl0jzLAuomBU1m+LKk0sJ09+
|
||||
4/PsOyj0EAC3Z3NWNxpdeQG9Fxhzwt14iH8bOX28uLkPzfsH4NUPILdjeRVx1YhH
|
||||
AJVX/jV3Lf1sJeDtmFZXza0k064tK6NSHIyINMlFKLepuLeMaH9vvQ5p7FkF4ja8
|
||||
NClAlIq7op6eMyACffE52T4Q8hnn0l2k1ivxGQcFbtMGeHDfVF0XCpDjC+hQWcwa
|
||||
nrFTgBESgzZObcy+fv8lhcATdgLN23WNgvpKgVr0uTBRM61reMEL9sOpgWQlOdx9
|
||||
RWgp2HGt/RgMRFM5AKsYPoRH4k+G4ceWgGPtmsBTV3iEOTdTdQxMsi2lWCqhnI8v
|
||||
MF3HUnjsL2wXHZ47Dh7Up4QMhhAhZxKSpyI7Ho1165Jvtz7KyqYbbXBV2O9CecV/
|
||||
1cndI6fFaB6fTcS/eY+7C8aNyKUfX1TUfoSzEUaZDLMhf47d8zw1SHd3u8Js9Kpg
|
||||
nfZ2kOerjagXlvU8JwNpjlusLe9omgcWzirtGc94EaYjWZqTYbEVkXSHiFMaqve0
|
||||
A53IDlFTP9cTGhTyNsqlYuaaD5FzcuLdKNmxqjTatd9zqtTHTv+gRq6Sc6jr6yf3
|
||||
IHEejEni2x72F23tR7C5mfpEEL4mtTtaCK4AUgvOMcwNOQQFOR6PedHBxDQ4WDFL
|
||||
txuB9MvjNpW6VNloJp3qyLAwQmMdfWF2GafkZO8RiWHINNZxaC1AnrkCDQRcpMz8
|
||||
ARAAvUfK6kyj7Zwa0x3qSjavXqOqSF3jyaEkC8kRXII5gfta23+d6CQqpiiu0u3p
|
||||
pTfWYEtKpv26MQ9kZfAPmZMuMDeJxm5vP0HRwyQorsS9DTL9tB0f+4ryCzIlMEXc
|
||||
55q+tX4yoVNp/wTaM/ASOQzsa2AMhjMUESRBjUz63Gtm8L2q7mv8RB9FVYtom7KD
|
||||
5LNIMM0sWQB4BaLDKy7o7GVQTdUrozes/wXQxdxol0tyDeKuYOl99JXISjTf18vS
|
||||
4JDT2q7o5hyhPo8XMP/HWLaqLHuQtgN+R6yaRLeyffGP4wQkhYwnIDsdlTfCXsfG
|
||||
voL+HjOAlwiMhKOnzKhr0DPJacnAfItQKyg5pCPNr3jJQGLKV0uVpWbVQBp8egal
|
||||
/ibBGIGiEmOUBvqq5ywfq7UIbmz2GbQB50xfEoMn36kz1EzR6dUFJqsXrpUbubtF
|
||||
TDiu1wbCJ+hb6bmg2oxKFAz00bMkCG7AkGtgjD/3/H2gfl69aGTb8cbD6B6feuQ6
|
||||
qNDtEQ5K4M8w33+IninFs4YQ3+hNstiWDjHoYLy1Nv5ThFvJIN0Qlw8wOj7Sho1I
|
||||
bAiRBVWN4YjZnhE2KyKxh5JR5zi3Rw37WrZiS728y4erMRBZPX/UNOWroQKELMQ9
|
||||
RMyOkZ7XN3LZ4qEOuYz6rjiJT3qK8C68/UfTzGnKFhlVQTcAEQEAAYkCPAQYAQoA
|
||||
JhYhBCJg5IKIiCx2r6oxnWeisWD3TbJ1BQJcpMz8AhsMBQkDwmcAAAoJEGeisWD3
|
||||
TbJ19lkP/3F3qLgZo5FGW9B0t2HOgQYJrTRryFN+7H3H1b3RPiI/E90lT5oCcXx0
|
||||
d5Uf3fST08WFe1MoPmR0nTzTozr/7FyVV00ysQpaD2iDFIiqavKsAJVN8ttpCG42
|
||||
yLbeen1ixYLVCp6mfiwu8qw9gzgHcnx/X8SOZvAs4cPdVpqUjJyCRFaC0Tf1Ibye
|
||||
XvA+FbvlZnzuk0GazAkh9z6doALAWLNrycsKcGVS0UObsHSC48NT1/ijP0v7D7oU
|
||||
xi7vlqF6oXFXeYPosBQ3ySOZMxF5lltXYaKQApRmtOAE3SlPTpsagB5NLLQv284K
|
||||
eGu+P4TrO6zv6iYpKrCF82v8XxBYT7g3T3edbeU/yVxudctLatgHcJsoKv6LLTbg
|
||||
Rmi6mpxu1dDv6WZmGpVNTRXU2RV9scN0IQctiqnAQkrr9v8ME/KrqHgu0xqRRcUp
|
||||
p2RcKFhpBfTrMiXrLv3pDQVkSsnkgLWwaD/3ZJaSOaKgUzmmdXd5m2wiXeS6cxFZ
|
||||
vR40Mrn6vRKK0h3AvL60Ka7V+GDAXD1CyAUxsza+ItIuVD+3Eqyr8rZ1N/hMHdoV
|
||||
1RSZyWClJnuVaT9PvdSYOpJP4XY88Q5eokpNik1/b6WLV0AhlnAwCsGxQQ4T2+bw
|
||||
aO5aaZTyvjv5MlbkLgMJqzM7ZSixrd8PjDjoaxmx9ueAfpIy2Rzu
|
||||
=BTW8
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
@ -1,280 +0,0 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
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
|
||||
A7UR6bgzBiiQVdi1gGWJBGxbJrY=
|
||||
=JG7G
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
@ -1,109 +0,0 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
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
|
||||
=4c8j
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
@ -1,302 +0,0 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mQINBFrpUN8BEADcjP1oWBZF27u4sG/TXvtwUnJ18Un9J6CoduQubSjfqSdpeD7K
|
||||
5LiVX2bAYzYTyN0yfzZzY7v33T0xlJLVBfeuEjNibqq6ijCTVi5pLKCD8aD9cYET
|
||||
8AJ7SEt+lGU9fbr50836xmeGFuPHaVWNScReiGE9CyQHtu4xyhGgeUJRS/0wyw3h
|
||||
ymDJzR19pg5dI1l0P8h4LR11pUcjddyIxIHB6gTARvUAG/bVk3G+wlIUZWvbuNnz
|
||||
re7U0AV1VjP61zl2Byz1MU406HOaszifQ+ajs3mUJfXmJqaA/8tVITTsuaYt2oaz
|
||||
e6gBycMuQL/hIOyRYFwggpRvNlFCiUtq2Mu0Eg1wHdOeHTJuK7xdC8vSW4B/Va2o
|
||||
b2vBh0RaNHYP3oTaEom5DoOl9DRyPt6hjFuQFW8cfEHO7/stadQQ5tnGrRRK+oiK
|
||||
VEOlfUIOGG6HMHpqhVq2mN3b/EXuMrxIxV/lROGKY6schTh45eb4Qdu6Z4f7QCle
|
||||
U3rMKAMeul0TInq/nMPFjnzlUpyXMmHzaiV+HlU2ZWl4HgFeLBLgT4FPECjZgMLS
|
||||
FuwfgAHWwQLdOOMFNzx1JhIk864o5zg7oSYHhpc6DK2mtjHWc/G5WX8xSJz2fcEK
|
||||
8whG++hb2vW33tg+rzZ4qwHJb0v+3lmd04lmn/BtrlRAMnzvoLlA/MneIQARAQAB
|
||||
tCBPbGl2ZXIgR3VnZ2VyIDxndWdnZXJAZ21haWwuY29tPokCOAQTAQgALAUCWulQ
|
||||
3wkQjkJWWT8XdyACGwMFCR4TOAACGQEECwcJAwUVCAoCAwQWAAECAADGRw//eJHu
|
||||
84lr6O7m+dpT4d+oWcixWQav7EqM2+S3tDiQWbuldxE84dDN10BgLsoCXBy5N9NH
|
||||
oATsjzkDWHcx/uHvFk5DbWhxaHWKKl+S7IKGeZfiA1Gggya640u+53+B8zndYmyI
|
||||
eHICDzhvUPOlkKI+3lF4KUSIj3/Ip82YTxltAn4pdgyZ05jS8X0khgs9zev7GC4p
|
||||
JTpUJA/AwgypOUDVa1yEqUF5rSy9nscbFpM1K6terdgGamx7v2Zts8p9O2M4EmBs
|
||||
A1ljol73toO4D+j9LqlSDqos2L8w5u2BfsHKsY6GtM0N4yDp6YZF7aX1/XFTwOtL
|
||||
5j/diq0KTNHiJfDEgSuMmFneZ6g1M0ayr2rXuHeJKaXNHqxr91OTa/5GBFlB+1HV
|
||||
OXy2U5GS1NzGWQPkzohUg8IyXQxa559+GxrYy+jBsegVLOvttu31K4jD7eXLI41X
|
||||
B/kAPbt2VCJdxIMzuOU12msGjSXkW8p2cULTKRGGF/yAiIT+g3X299OAOszyGVVF
|
||||
M0LGn2Tj0KzQdU0ES6UcPwrS18DU8QxKzAmwrPES75jfT2nOadfenMeDd2SGKbtR
|
||||
7n20gvF23yn+QqFr+uXe8xt2RcUeJLP4ILfkL2D6ANGNcuHPZxxrjeBfvUqUVzpL
|
||||
fwF0RuuRwtRMHuMdcWoD92gmAw6dkTVjuWIy9laJAnAEEwEIAFoWIQSThqL7LanQ
|
||||
0x+vCBjAwHYTL/p2lQUCXbBndgWDG0whaTYaaHR0cHM6Ly9qb25hdGhhbmNyb3Nz
|
||||
LmNvbS9DMEMwNzYxMzJGRkE3Njk1LnBvbGljeS50eHQACgkQwMB2Ey/6dpWGbQ/+
|
||||
LI9AJ5pFUwMXyN60iXpNFDMK0t6JkAjFMHxq2VXsglrCM7XU7C+tU7PZx6td7g1k
|
||||
KoZwSu78dN9cLuCkUoHNKIcPI2klpp+Yvmz0CmgL8wiCDdja1SvyGA6DBX7w6X6V
|
||||
Djb/iBJK9gDyj77hoonBsjHmS+VQwnmUu23iEP6jTGkgayJn8gG+ipmIhRBYmKBc
|
||||
2jB3PbiuW2KfQreq6H0+dUCC7PbOFL166rQ4V8F0PFJFsoSVlNHce4z73QcZS1S+
|
||||
MYOE+TZWC93CCJsvo1WDmDEcarYRt5pJCusv31H3Zmc+TIWTV2V+Di5BfebhIt+8
|
||||
JXBwKyAbgwhRZp4BhAMnM1I99EBHLL6L4hka/tvB0H1nNHFdzhwXxicWYU21jE3f
|
||||
QNNu0c6dRUQFphZB84SuxOa6Jo08NsJTjXSfQCgK1bqpEWJWTY169CaYX+h+SFW1
|
||||
x89p/zUUuRQWbLj4QZ959nCvBMoWsS2E8aIg0eyXX/Y/t+ikhwn1t6TjEQEI0WDY
|
||||
78VsXlaiKl5Ks1umbTQc/41Ka3jFvPsxFFFRoCywmWfXs6F1mOZ2bAzP871sastW
|
||||
p8Bdwv/Sqa5PyJg+fyvEpROavf430ZvE6FFHcUmF1jxPIEseb6Vhpu87wEa8Z+mg
|
||||
QcNvB8Q01lCNKtJRy0NQH6JVBtPa/Bb5u19o0HDTzD25Ag0EWulQ3wEQAM86kl9M
|
||||
Acb4lSxIsmiPG0MtIRHG8GN68iLWM0HJnJwbiRFpt8ro7neQjACnURe+Cw9yoQ7B
|
||||
sf+oKGyzV+HY0xEq7pKIzaUs6DoxuDjATOjdOKImHRTAog1gZ47x4pf9VJTf9hHf
|
||||
fng6fszVcXQxK7Ei8xOjHrjBcN3L0GKI32NLCcr2ssDjyt7xXjE13pXhfZjXdpAv
|
||||
ncCAsOufKe/0gM1h4CRRXd6zSOMSpb0XjG3obF8bPpG5x53/d/GOppspG+hUwC3/
|
||||
tEFElm7r64LuXUE99MoSlJkmucyXPx7dDpspoGZrTkTx+rAuqPk6/NnFiBJTK4bQ
|
||||
kpdn+tsSw22dWQW0Vd91IzqldUyWpL2WSg3ASy4huhyFVT6VkVVynlLXxN77PlkJ
|
||||
S1L57cCt7gP1LIZ6BKoNHijZu6Kff/8vCeqmIkuy6oXfRwB1KtU4klKdfT6dcfct
|
||||
0BV28VCBTUvYrLKmCBrB2iubA/NjOGpHAN/TwnSWK2zmVI9zKCo05+9pfelnDxdO
|
||||
pUyVScRM5WRhutKxYbE/25ws64fNlCn36//jK83ndVT+0YyzODrq/ADV4JVD6ZMa
|
||||
5dKrGzdsq1PgiwO4sgN39EQ3mIXd/cS9okvLz6/EtRxAguLX/qFfUM9bmBnUU1kh
|
||||
HVZqdY1RC8iAyU7kMd/Sbcb8cJYVyG1jmbIpABEBAAGJAjUEGAEIACkFAlrpUN8J
|
||||
EI5CVlk/F3cgAhsMBQkeEzgABAsHCQMFFQgKAgMEFgABAgAANtIQAIoCHK55w8Bm
|
||||
hxvlbUuR3O3qSpWaS698gJT3/0PPsTKEy4rR33yVX68eE7CWYeroUlWqvWM6q5Fg
|
||||
On4toXGQUASkQkPEMOAk+BCRjHDRQmyt3ybzCdftKUEeIGz31OVbVCKl2GB7uiTK
|
||||
BzOzfpTaSjW/i/W2mS+GPitHw8DPRcp6aTeKq/VpG+v5Z6hU9iDu8qKr2ZQdBKGv
|
||||
TFmwvoylngWB3IO5PWcG55FpaLtBrl/9YeoYRPSqhEVQp/GD4kzv5ap8k+Jx1Vet
|
||||
0Yk3viCdAWpKzp8DNQFB9+Eq9+qaB4Nl5xCsrg7l9tAyD7yXNwqZU5OcxaUHjvcR
|
||||
sjsYn9d9Mjca+SRk9SKrmV7NHtDQykJvTFfiR61+lY4QadPkts83Pyiss13yizy1
|
||||
NyWkvjNv3Dzdlcb5esiGO/jhV49HamV+YPFoV4dhhaxvPE2xczKEaMM7QeH/4kfP
|
||||
RHodDDmQmMteY7Coi4jtE9aGw3bW3AFhSa0A/DAG3lM2MKooW6iFJXB5vsQJnL9P
|
||||
IeufqCrLG0q0g4oLArRZB9LxBVZKuxu3hI3kFL0Z7wy+KWKFWhiU3QlnuCazLfTr
|
||||
DHIE7iRGAyt21+4pkSoOfTCW9ntUTkyatessXCrSoOlndyTOAGsSQmQyq/lzzUzW
|
||||
rpRLhwN8OKO1CAG7rVyrjDdf6xp9Uhi2uQINBF2LSCcBEADJsjFK4WdmJ97qgT77
|
||||
1ZjHtQw8t7EUMKrMtpzRUT13GSRzYghjb6hwvCAisC63MPATEyg3rEW8UCbVxBDU
|
||||
C/ZqprztZdPLToVwa/+kW9R5dinsb4ErSiMNcRj/Pucr0lBEEF4Q0hbwVSOZCvWv
|
||||
gycJxGCGJfelT0ACcc/m9cjfJnbpTzT1JWRXKB801q7CuxKn4mNLnmlMA02H6xru
|
||||
L6FZXGNV0yKwr1xxYSdwV9fNmvomhtuogMIE+U4cYLRMvO2yROl5ceO9OWM8mx/J
|
||||
nQXcShW1Zsz+4VcpgZB0kasAybp90Tq8QzbU7docGzq/0cFy4Fqx97QC0tK8DCql
|
||||
DnpnmqJBqp05Je3aAbhrMUuw00kqwIoOpSUw7+JTXMjlxrPomuZAq8Yzr9ZgNd4u
|
||||
dthDizNqzXt4m/J+HjBko+10WatPFT3VmhICmCMBwCM2qrHcsRHgu8aqs4qOSgve
|
||||
qa3VTI4IXDRoNvMkdG8WQQNAbVYthejOp5JhJHqmYrw/IVxmjnwTViwhz2mEvez4
|
||||
+QRVQEPEdkMJiZ3QxQXpJZsQ5Wk2WT8hlHWRkilwLgaBrRhF2tXDgmk+KgroI3O1
|
||||
PSFSrhILHgpg5OGqkFas0O8Qkl7+l5+YwaGriDwDZ67ZLzoQG4v9emM2GC+hbopO
|
||||
73jQKLQxSetxLBuQFwuMkfl9GwARAQABiQI8BBgBCgAmFiEE9Pxw8HMQAoQk78IK
|
||||
jkJWWT8XdyAFAl2LSCcCGwwFCQPCZwAACgkQjkJWWT8XdyAcgBAAyFJq5d997bZW
|
||||
uV+vV0KJkVBLFQKJSXiw9/U/fDzApekquBN3RpwniHkldwd7LgDzcH8jwnChO2hU
|
||||
GvDp9yzTKdSoLiJru6t2VaFiYhe0fjHWqxdtSG9xJhmPbuk2rErNY330JaOUGuUb
|
||||
wsoLuNgzYCLpzYJd1RLDPfzEesp3jqprHJuujR2sKc0HEqQsbbqOC0L+eP7Qtoo4
|
||||
3mwmEBJEwkfLdZJRBEeI1H2lN7Ck5o4mW68w+g6BY5keCRyfZSS44paT6X9zFLUW
|
||||
hOAaP9MQu5kv9wg+s5jNj+ebHt4twTNoj0UKuuD0r+r01CYxgvCBnoPsZbnvEq6S
|
||||
r0pCADJtuZy2WjC7L4zkfLWKSuanm3aEm6Mma3XV+99g9QIN6Hm/Yt1pSjeJl6ir
|
||||
UohF+JXgZap+9yl65xh6fNSOPZfe2rs9UdfkZA0RbYArXHfqSE3GfXm81HM6esuA
|
||||
dxvBuR68ohcZmpZhp2MAmDXsFnQ8E67iYhHl5CvHVHvCznhZDyklt4z/9pU8lWts
|
||||
AV188gdJtu7GmLE9TY34emaoq9LfU7yAALvsuApjhpOOH+HXMBlp4H3lDyoR5nEB
|
||||
LUJ8r93YXBcjHum5s9LTG3izZVJqhhglTvzD0BcekL46F8TAX5ZyumVpli68yPFy
|
||||
iKJowkyVzFH8mawJWIOHzHX1VgLrOvW5AQ0EXYtLAAEIAN0N71t88O+UvCLDmN1n
|
||||
HaK7nYME6ymzIJmz4ATEIhxw12r1RmLb6cDqNeBErDqorNf7KuWyBkc4Mj+GoZRu
|
||||
buqxWmENv6d6sc7cvRE8w1TPZ9kuQmnmDZjLTMBwlcV1JzRUFrfHGrhMTJF1eTCc
|
||||
GbbeFyIBASkEKCaP7AVSOaaeAL4aSqZmuPsEIPsTeCImm+1SnO08aUgqF2rxEagh
|
||||
RpdqQm0ZO7vYGH9I+jglBvQy1SLUzxFAQi0p9GX5FkFs2DKy5sWdgmNlXCW9fZ0o
|
||||
hiM3xhnMUC5EtJ3avFCHOIi4Ml5y1reI4P9PT7qm357UxGYomjP+jCJsYb7NV3DL
|
||||
HuUAEQEAAYkDcgQYAQoAJgUJA8JnABYhBPT8cPBzEAKEJO/CCo5CVlk/F3cgBQJg
|
||||
ix/UAhsCAUDAdCAEGQEKAB0WIQRuAe7JZWkDsFQrjxAD22MiJnw3OwUCXYtLAAAK
|
||||
CRAD22MiJnw3O6JkB/sE+agRYh3IEGw0OzpH9cHJoaFt1jDHTL/e1QfNGoNtQwtm
|
||||
avyWWkebu4kNNeWJZqrCRsef0kbXtybszVOTqKe9LIDh1ApCgwSJEkLIyb59ULCY
|
||||
ABMMdV03oZ3702PFA2y+iSRPGM3mzh26ZHQlE1ZoY4xfHd3csgOfoFXMYsCBSryD
|
||||
ZqAatLbejHnk51gUOb2PlTc9sXmPZNaY7SSSXzPHCb4EIMTw+HnAKnv9iznXF3IG
|
||||
FVSl+C7g1g0KqYuWGL3pnxSnbTapuenfmFum2YS1xL082vMX+GAz5N+8A7fbVEot
|
||||
umENBhO4ZiJ6GyuJhfk3zbMwBiqVONgQkIG7brGBCRCOQlZZPxd3IGzAEAC9ZID9
|
||||
3KFiNTcLg0kpPmeesaNl0hm52UngMUOnBmaMe5Noa/l76q9Wxzue7VsH4C80+M74
|
||||
Jt9NxqmxwN0F/kKFvMSbGJGWcJP9BBaTeCrf75irobFug51kbD5CBnOkeZ7jzt4b
|
||||
56kymLUr8ACMno2/QkAfMV4FA6r2ND/HO8f1ijh82LIyijNu1GUIrSgchXXu8raJ
|
||||
UXqz/cmst3K9ULTxAdiMW0x1nxfKUzBYKlfpMP96P0dknU8Qfqho/8Bdla28GpNK
|
||||
D+S8HimrKdHnLpHG8x+iwB7nxw4/wTEhG10pbPqToUO6fJEAofq5nIlO8+W8Ytne
|
||||
7nB3Ek+q/QnqMaANCjTZqQ0aYeavT5JR/8aVBqcuUpGZ4ypz+yREXUUI0F5iixcw
|
||||
Lh6MLYL0XqAqjW3Q3Mqo9EUpTdRxwnEuEfI2I5Zp8uuWax+FBSJo+yPaa2cHVmCH
|
||||
zLRy7vNINyXyIc05aK7bFPxA6u4orUnnJ55N4GpULy0DjkMXG2WbvlwQ+QMEDpaQ
|
||||
siZ7yKAhiZ8/rv+5HPSPNnGQ+3PXBdWr1WiG5jzDhJWvN5tk1KrTjduv2cZyhBll
|
||||
ArGm8MlLlHS/JwfVNp0eP4yB5VMPFst6r35ToYoVWbpid309Mpt8SriPUzeeYNW6
|
||||
zv3+fmSiW69q0kjDrPnyK0LaOkZB8yP1wPcUx7kBDQRdi0shAQgAvJbbR7MB5QKe
|
||||
RpZYyTjAthZCiBEPLgPFHTT3vUhN7ZMd5BhCKijHCAM46MlJHzvvuEe3Zu/hkBdi
|
||||
IXG2rNZ0s0SjngnAZ8ztCJrFWhYSd69uyBUrEVmwofLsSxSBgHmsEShqnRj2NwLX
|
||||
5CTiwiEvYQvCUi8pbKs5YEPNwB/d4XNDw/ng+acyhr0sQW8V8XNJjrV34vX86ey8
|
||||
4YsZS5x6wX50WMkHS/zmFmYb0ehEHT3gOhzYBdT6v6OlJ9FZTJND+cr3+50TDCao
|
||||
VlMZAHjvkYckP2Wo58dHksZUJ2Vxqsu2RGc/aWlK3m16C1pCqL7lN5DGRflgr2sl
|
||||
l7ucNF+99wARAQABiQI8BBgBCgAmFiEE9Pxw8HMQAoQk78IKjkJWWT8XdyAFAl2L
|
||||
SyECGyAFCQPCZwAACgkQjkJWWT8XdyCdFhAAtmV/xbSiO/C+5g+zKWuqvLzZr0by
|
||||
lcrNKLM2iGRZe+cAi040Y8h0eCEON3hfH8Jm8zUV3DBE51aAgem8WwuZhqNiHry7
|
||||
lW3YWwzbJ/SLQ1DYj/tbx7hoNGidwIPwww/6jinyxeKxy9L8o9wopYbHazCPryCf
|
||||
wJIZ7oPtJhWGvnLTdgEUs3FPY5/1WXRtZVTH/KXNmC35t2KSd6RbUBmpFH7WdpyS
|
||||
L7oJT37tHSVcLyL05XQa+PVMUAbJ1XpdHgB/9sQYtV7cvBgYK+0ATdpU//4e2Th/
|
||||
5ANgm4nBO3u5fZgmKp/Q5KDingOZQZNvaNScdZHxW4TzABFJ75E4saiP1Pq5+XA7
|
||||
E0KoT/e91BuSiGuQanWJKidmWSTewFXiUr0rhZFl3c9sPaEvipRZWNWPAQX54B++
|
||||
bMejzFdpf6yV3Oh1juyJC76DG65L6t+rKoGJ+9kpe7amoE0RnZ2uFTJw8bFuuIpG
|
||||
Zf35YV14QQaHIu2dJpSjZPbB92i6ahwtGg3m+TVWRcQUqEybUPUXcj/Ibdskl/zv
|
||||
k+ArXrwR7L1Bo84kGFZLQWuZzX12GGLCGueSPURKEmmEtN2ldY3GWKl4vG7tYRFu
|
||||
vUDe54bBxINHByXvi/aEulnyvw5JX/E8VmC3XqkkUg0I+VcYdtaHONRG7cqUMfG7
|
||||
cRFZ+dkskBezf2S5Ag0EYIPU/gEQAL+QTjFRNyW/Hb3UqVkMI7fMwNBUohiJAOJj
|
||||
PpWACjmlMjrveyF+34HxmT9kQD+0OE6SKb0frKlkhZ4k60fL8vxGZacO0h+rNCn5
|
||||
3UEOVU4CWQQ4eRxGQeu6k73Xuj196F5hWNF1cpWeeHAEo+FVoDqcd3tN7+EzDi4a
|
||||
TtDith9RTA9zwun4VjBNsQatqyFD25IUPKbggjncdSgepJ9r6uwQae8oIxqghRYP
|
||||
ZK4EV8LUpei1fGp0z/v7PqH8Op7LHIrcSCYS19NqFbRSPHWYVfuc3hLRJvgTmbOj
|
||||
J153o7RqMxt5OT4wQND2ZoOcKiBgRDVeOTbZnRIvO5jFMibAMXpEyMA2UR3hWi2L
|
||||
O9wOd/Hp6hMTL3G7LyhK0AAy1HMHvMtUPdSGfGgjhOZVd8vepst6meVWAGojsvte
|
||||
aIuPSapTfLt0/G1r/kzg0ayww07XsfZFUtJveP330dvVzP9bNd8f9j7ybS3H0MTq
|
||||
Iwm6xrXz3hWB5XlxQfxqWQEGLJrfJyj3VuaHvjLUDYXVY7nmZobjBdnSvR8XQqEf
|
||||
cMQCbWxS10FDd7ISvkn65n+PfWmBrL9Gsi+UTj3A97dfdJaZM3DmNKGN3l4Q4DD7
|
||||
yTOae/qYLjrNSLPxd0NXiq8b16amrmmii+vx/Cw93rLk3LVEYAAYVIMqZTaCUek6
|
||||
8DX804bRABEBAAGJBHIEGAEKACYFCQPCZwAWIQT0/HDwcxAChCTvwgqOQlZZPxd3
|
||||
IAUCYIshGAIbAAJAwXQgBBkBCgAdFiEEoaiyXmGFuxjbr6YNXyJ+CPoznCAFAmCD
|
||||
1P4ACgkQXyJ+CPoznCBFzw//X4lCZo/iTSzJCSJVwvAHimPVyH6H5+3hxNfYI3nW
|
||||
jtRVb6C0/ZU9HuTYzbKv10ZTGdXsSC8lo5D5dMwpEBzw6GFHdU/xAw51YZT5b0SY
|
||||
iWO7AgDqtbUrKqjmwB7eiaQ4cVqiz/s/6gpJ/XR6S/NVWJlu1/SYwiASGeMGKkDV
|
||||
U/1mGqSY98z/i6Tm0XqIHefG96Z9yHWU1hU0IUTcFtWOTRvAg1Bx76ig13fx0HAq
|
||||
fahQ5gtMP/pZrcHDf03o9+RJ4Kb9LTyDTfhIYNssSvXKspHuenmQ//Gio4OabUFk
|
||||
GG3pPVaNDv3Zv0MTV35YhclBdKUjpt8fYYNwlK5TdsKuH1r/I/okXu0BRGEJVPNS
|
||||
JNbpH8KYsjyowNCMfw7DC33cKnznhpp21M4r6A5BdrkwUCQ8N1MuLp8+2mUaFOBe
|
||||
HwwnkRkNx7axxlduVwK3QTkMY724baD7KNrkLb3EO044SVOZ3O5HnWsbR5ssGhsM
|
||||
ZSCitokam7+MZqL9l8pvy3HWhAI4Lc7LAS4vA3quzqrcJZF2V7H1I10L3HnVXKX1
|
||||
nHe5kuOaj3YeTABMMTSwn4RNsOoVm1CRLU2zL6QVeKXPGoJzDtgGAQiBZ+rMoIOq
|
||||
GZpJh968AzfcyEBzSZIcp9m40KRJdarRDjQr5/iozFkbljWbm4vu6qrlkOgujIjL
|
||||
LDQJEI5CVlk/F3cgX6cP/RhWuagC1il6t6YmmMWI1DmssVAaAsgDp9D8p/uvvjRK
|
||||
UqLIjPPGxwSK6dqZbdKJanx5W2VWRMwcT31jxOvLYGSHsCJUEwaVYMGxzFtIKUxS
|
||||
AIzSLRmdBgQCsUPrQJVhNEfLzwcEpAuJ2CZTOwxQlo/6yD1jLrcjcxwmjOU9a0Uf
|
||||
s/nuoo3XIuf81LvAiLJYVq9HdDsfLmsnUA66LCK7sz46W7c3PR/NGeF/ZtPbXuuq
|
||||
GQLbXn/egGqnmaWij/Iv0PpQSxi6iVJHn3KrUdxweFZoeEPPDl7Em6szxI5os7EK
|
||||
1WH/VBx6YI+sdKMWy3LMJ+ah05rQcgSpowH19W5OPtDu2nHiMlQOMiyJmwWenj03
|
||||
l/NDQiNWaENXHrliv0fxE90W0M5KzG7CXTTe0hO03evVRadW+2hupuJ9wO8xmP1M
|
||||
Hjmx4x46yL1ZjZfu8KJc399hGaXI+Q2tVcXycDtdvIUMNsX7hgTXXxAxBS4ij8Gu
|
||||
i1GoGLjZ7yBHXHzFnX4Oa88AN9OCn5mR9ZuPYEZxSe48DnUD4Bt71GwCB1GOD1s7
|
||||
MPPRGJnB1DVkUZbzfYIZFT32VtF2EJ01Sqq2Tppv9X+qs3TQkiFMNUSGOPXIdkpP
|
||||
loatYsfFWY5hKJf0c3Ak6w2d6XIfKgdeQCKnDpAEQxnay4SnTS5zc/msiCIbXaFB
|
||||
uQINBGCD1RoBEADek6Qi12ixcVeqNp/ImLUC1eL3QFPUDhT3Q8aAMPoAXjcDkxAL
|
||||
Xxgl8KpZ1Jrxomh5ybiBo5Oh5ffwH62UOw9JibJveovTXvPiog8sEeg6vcGDg+8r
|
||||
WWVoHqIj/rECtLKG6ooyBC+7r+4KW2KC2Wvsv4DC3QxHtSnjutUKKLOs3orqaYXP
|
||||
YEAs8D7tWv2v2WJHE/Ygykh7wOpKolJQcpOygDtm34W8/Isx+3vENgsbl2itW++5
|
||||
OauUxld0LiVEtRUmcBaIeg+hEoXk4HJOaQJZNcPrDyiuP2AE4dzhU8OqHGo/TWFK
|
||||
UUW8Di9bn7zdVIE62RhOaoDbRCCKBrAvjUTNzFGIpHIXJFSAnF/zevtYXvMyxuFE
|
||||
aH+wbtyouEldcsc4HF07X5N/f+j0ER23cHrZaOG79tXOORxnk+XsUsch+K1Awcew
|
||||
n/5plbGnjwio5Pkrh0Pfc3Y/bETKTOpINV0oQ+VM5jkD07OEpUzPrDaeF9NlfnJn
|
||||
zvVqpPvnhNEO9wWum0vXdqkMdUaZ3HyLBksFpGCR6/QSgDobh2BkJhhq7FNFgmq7
|
||||
tmiAL6j3kggl83Llk0z34srqkNbNoVC7NYcUduujJ1zMn7Rq5rFbVm9WZFCRtsWv
|
||||
aCF0YZ4AJZwRJR3V+KssmOAq/zjPFZLPBA+3uQfbYSwd4i490rl5gUP3UwARAQAB
|
||||
iQI8BBgBCgAmBQkDwmcAFiEE9Pxw8HMQAoQk78IKjkJWWT8XdyAFAmCLISMCGwAA
|
||||
CgkQjkJWWT8XdyCBxw/+Pnj9WrBaxXzaZ9SyPkxse6p0y/ZmRqf8uvtXYHk23+e8
|
||||
aKWHOos4S/yEZzR5D7UPzzjYWZ7ruICQ8a+9TBCTbTgXjQnultqOkN1Q2gjbvwQW
|
||||
5IPVGKcB1YHJwo9VKEd3cGiXDvtpa72Ao7iACa8I+AWNPm3IkvemHvA9DMN5WFWf
|
||||
pEd6q9qdiwGrQT64FKwdyrlAM8EqqxD0Wl+K0DEz5jED6os40mOphWyeB97SpHwu
|
||||
cfcgHilIv5Tm3ngjq8drpT7xfAT9wGqspKDc/YwcqM77gWWHHedH2dFkcIlliyc3
|
||||
6cVML4kGd+L76fk0Fq7p1TMkfp/12bf2gguVYws9TqN1SiNoZh6i1EAFLF+2Lmpd
|
||||
SQG88orPWEutS1SG9gbFt+1i9WpPqVzCybjaIwOXZhIh6jmhHuZCh0GYFcP7PWW7
|
||||
dOPNUpOFjOAXnvnTvsSlGsaWyQ5IJ+lq8v7BnvU7R9PnksOSs6398drsLhoowlI7
|
||||
et1PsE5/j1pGhlgGUz1ILPy722VZstFHck5hz80XpbICsX+E1OYDpTKNsjmHkjm2
|
||||
0LuvFgsR2TUYXxotyr4n6tlspAm56Bb+j7Ai8gbsv86eJ0eDIgs2Nv3kcXWtGFPp
|
||||
G7RYmd422VFRXD8v/LB+DfRniiGhytH3vR2cHHC0I8GP5aZFgHc0D/LGs5xK4Mi5
|
||||
Ag0EYIPVPAEQALGx/+1Sf+oBOSVfvZefJ8DhSorwfERXHUM8GLLJinGIJm9SzSNB
|
||||
+ObhXmDp8y5zDBjomZc+A025b4JpzTj2nKR872DipeD3jjb5C4L8LkSqCw+32gxl
|
||||
Uben7Vz6W1GLVo+JbxAf3fL7hd0wgtNsf8ZXf24VKnEq7NL5VdCv2JCFnVy1gTwx
|
||||
nEuiAM2Ft5hhg2I28jIIlMPhWg4gP1DSciVIMTUJpr4uCu39RxIkfVvJ2gc4RJ0S
|
||||
gq6TmZw3SHGM33ToydL5eElCuIK3YSNbUyUuxeCYd09LDVa7ri7KD1TQFjpddtMh
|
||||
zwtS3YHQPqPLE5MRmpOAxCRt57uG5Mb8QGjMVSbj+eHgJqbQplHoUcdYa2QS0cHT
|
||||
94KENoC8YTUAaMV0xB5ZFdbTml3bE+dvTvSgRCtPtziIy7pA/PIVYmEQA4CopnPH
|
||||
tvzF9bPjD+U7D4Fxt2Cb4ev8nKSPWesQc2742K+EiRlvwJ2DmYqtBX/i2TO7ulVC
|
||||
AlZklpTQqdUQdSyOb5fjMBMb81pYmWDpwKuVQI4TqmnNpw0aEnHcBEZkqHTCPnS/
|
||||
sRBwh9F2p49ZQ4ZtUHdJkvZZr6BglDVWA690HchdrolIL+X7vet5zPQSRMFE4Nc9
|
||||
+JTVUeXDTT6wbJ+M55+tfyhgx7tADk0lZ/5fvgTECMGxhfwDpAEg2p5FABEBAAGJ
|
||||
AjwEGAEKACYFCQPCZwAWIQT0/HDwcxAChCTvwgqOQlZZPxd3IAUCYIshMAIbAAAK
|
||||
CRCOQlZZPxd3IPPlD/9pen4nOW/xBRZ0Aod+Zu4VVCnuFsOfGff9MrqvekVe58dG
|
||||
IXPcuVbKKXXZTO8zvgx0jAVu88BKfScLJTy3odcs6XePBckEEtn/LPaRJa0zb+8i
|
||||
LV8Ke+vaIP57rb9QmD+aOer9EmZZzxqr5Qi3TliAQ/UV5CCuAugJcQTeCMQZ1b5i
|
||||
g0qKhsz17p6HUvC6X1WoljrhDiaytxGaTZpJlqwktJ0cA5Vv/ZsgIKHOjjbb7e8c
|
||||
Dsk2yCqTSm+INkA7Asx6PjGltkHIntbU3chJDnyMBJk3vWll69Y+zL95bFCBB1Z7
|
||||
ID92S4psF4YVavkr6EGVCIa5UNbp44wbwboKhBpHlDKm7AulBeI+8Myl0DGXfNS5
|
||||
rb/yzhXJRO7ySIpRGHVkvFbDZb4013AYJ+4bX/dPAwK57k1ZX5vZ/7Rh6o/xu6Xd
|
||||
J2o3QHXOFh9QOrOQ3ON9qf40QGKml4Toymw0f7pHWQz/q/7jq8kAWKalRxpvFGuo
|
||||
E/wLeYpe5Ppef6wubYj64tOvnS+mEfjgFBqU49iLbKvKJY/sbaTjoGJV8d4ry1CS
|
||||
Txf3uYQHG4H7FecQLtvfBrs3g+RPsoz0NjZsvqDOZxT7kaZ1HOlc+MVFJH8OUn9n
|
||||
d+572/iH8tOt1SvUg8EVmscBxPaDXkj2iMxDNc9TmM87c7g+h8jw0wSKeS0dx7kC
|
||||
DQRgm5Q2ARAAtra/AVW8OjQcWRyPnS/fG5AlmBoXqIi2Q0TTPD29a64IKA1J1mnj
|
||||
20wsD9tyPXUdJhXLK1Q+ztu1v3rw5XGiE0tbuGT9Z/kD23gLgNkH1wV4PJm5xtoO
|
||||
VruLdNZ5iBA6sv1pI7EelJUSlOqpVkWYGPlEHg/etFk2T4TCtQgkNWsU6t4Hsjhh
|
||||
O1DpGg6Uekrl8KKzBBVIE0ZOSHvDS7mJSGO0FrsWQRx6mo7fs1kZxEuf8NgZJbJO
|
||||
ZYywDvmQm6dCOFx9CExQdupsqnnBrrWVxGNwiFnigSFvZYuVlge0yuwjUvHTnJW1
|
||||
OeMp7nfsTBnz72DqlL6xUbDsNDnYih09ZPXF45s2hMrI6TZWLVYeAQo91xhO4gjG
|
||||
UtgoidZmbORaAUtSnc7QtgcfpHeHsANXw9p0FDoamRBlG5OM7eytUYAYlKw5dMaO
|
||||
Xz3LrRWhczvr7ELy+Eora2ALEdsJR20w8p53HQvzlML1PlY5q6p8INlF+dLYFiwt
|
||||
KaL+L28jgD25YM1/tRyyWZBWeQThQltRe4d1LNDOzegl77aa4G20OFDt2UzaEaty
|
||||
OhnzgIldUBx6y2aRmWe+dWehiFpSxGoygF54Hkyk7gkOAWoq6pCvMyr3JniQxECY
|
||||
mHUm0wWANJV26iahlyVsTohyYPlYqQwwtjF+8NDKZgewIXqHyxIEaSEAEQEAAYkC
|
||||
PAQYAQoAJgUJA8JnABYhBPT8cPBzEAKEJO/CCo5CVlk/F3cgBQJgnj48AhsAAAoJ
|
||||
EI5CVlk/F3cg20oQAMrrQyYoAXrYx1gW8ZY8QjXjnUdF8YYkH09gFpET1J+I//yv
|
||||
viamd1V6uvIcd7/9w8sD7nRPMjOjmKBtgijD88pfGVWpRSqK6kiUwuxK7lqFKM8+
|
||||
NbrOIDKLw5AKxCsaK2UZ5DvDn3ZRe5uA7Zzd6L7W0avveILc0FLcI36OxguioSTH
|
||||
dRKslmRomvRURCl20fVRJYUY0YidKVlWxjYCir8QGcYdJpzTHuic0imErlXuASI7
|
||||
L67VBKYs6xuFOazshuNy78Y5wxuuVbQfn9EcZaQEYKnEKtppvBtFjGzjMGpbBdMk
|
||||
dOnXIlwXJ8akU3D27K5JD+fb4tnr2m1RzJyk3ufOLpFWKhM7RE+fi07+snFgzQJM
|
||||
R/yl89VCyuZmnYFsDSWno1PCA3XMyURoXUtVpTBEwJ6WEIt2gxlzPeq36YV+OGW+
|
||||
KPONqDMk8Ny3ETFIkYeNlIbA1fcxEZa/GbacbBHzLiDsVvKJQ3P/SzeAI7c2cXEZ
|
||||
mNSFrqenGObSLl2PUz4i9bb2ofg+F0kKd+AmfmbQ9LR19kw5AzK48KnlyU7HIz35
|
||||
ouey7neZPfvKTcI8OmCn4vvMELj2Nnqhs5chsGUlWpxasETh0sfdEd3sEMAIBMRo
|
||||
2IHybWUCofgAPqrMfWrSy7zUIuFWDkvKNBeeEndvSjRE96OVJwVPxdIK7K4suQIN
|
||||
BGCblLkBEAChy9KRjM6jYsBGN8Q571XzLsmS0OF1xS9JLVult4lBSzrT8bU1EYA/
|
||||
A0L2VHclxSCAf07uRp6Jt7AY04y+N8GCsvTDI2AkX5PVGa58xeWWvAdJIeLGdJNs
|
||||
cSAhxLHhDLer5piuGilCk9SbqkhmOlYpduXfh9YeTU49dtLhN4sJCx8HT/iQZrxE
|
||||
vKQlQ8l4V8RDJHg4TXVJbfcq9mHXHsX8ddCUmQ1IQBcfRlEPp2GfYRntbO0yvd9+
|
||||
7Rq8OVvnfzq0aN/pHuoeIFYpWAqhAPbZ0WtIJdxJQG1HRF5om1NtUot0iqerLGJi
|
||||
hYpy1wl/XjU127BWoihZENFyfahPUNMUjZ+wItXjX/RO0juLsZqpdFjF45umK3mO
|
||||
yrOVW0v7icDvVPQoagNtLuNbIhWVPSlFBLW/BqZa+0xhBMgPs0gtCo4IchghDtI7
|
||||
Kpqdcbx6KnU9X4z69StkipikmhEI7EBt+aAowyoEqQKwh0+KVwudLO7M1xHr88hG
|
||||
CfkMDhK4nXOLa30/8SlpsjyyS5JFdBn3ZT2uzkdFQNlqOPmqq2rZ0hh8US+xkH/f
|
||||
iiRPTU1G5vb2Y//GL3y2fnx7g6H3ChzGDF79DsEeAublpm/21p5SF4Y8UkNT9oQF
|
||||
Cr5bcCrtxvnE39LMZIA21QZlVThnJAOhPjSgc+snYxnUdcOS+GyG0QARAQABiQRy
|
||||
BBgBCgAmBQkDwmcAFiEE9Pxw8HMQAoQk78IKjkJWWT8XdyAFAmCePkcCGwACQMF0
|
||||
IAQZAQoAHRYhBObfhQJSn4a0kcZeHpiLt2JjNeP7BQJgm5S5AAoJEJiLt2JjNeP7
|
||||
cXQP/iyz3JHxwC60uduoWtLYCf2Gw9xf4VEXO3MxSUJ78vRRF9Nv2hfn6brAKNsE
|
||||
rjC/zbUhUUl8m3uWxF5XkAf0AfRPgm1QPODwNlRAp2pVgkcKRZL+RnU8uFiPSsBi
|
||||
gx6ILe8opOU36dIdgAOcAp/4rEgIorIb0cN9yehnygbdkY7pbfCfhxLyLeBhgHYa
|
||||
wm8gCq8ShkIvWPxhzr//a51FwuB0TdRn93s25k2o4Lg17OKPpuFgfyKXCM9le+Sc
|
||||
IcwmiezNre1wWbRQ4q7DfptYInmF2dAncGW1dGY8tQfU4eWyeuh3BIXRcfm+XK1S
|
||||
w99pncnGeWdF9A7XidBx5+cqQdKGwrGH7liWB3rij5T26vfw3euGxlC0p4Kgrnbf
|
||||
SMGas2GL5jGiEBaAmNrgSGsSq6qjKLHnsN/qQXFvRWyI0LgqfmFez1KE1C1YO3Ln
|
||||
gQZqoffgn5cARhvui5bhsWnylWHVeT0Da6nGVrR0MfGHvpm3uQZtS0ij8lBFCHXK
|
||||
d0sqKWf+BvAI8xx0SqSqmh5hv5od26ofwWKxcparejpbtJIhCPWlIvYJn8k+7OWO
|
||||
Of8RVf7JAPkvubzDaYkd0zutCPgqSwWlJ72udlGOFZoLGlEaYexxMbFEK+AyjFz5
|
||||
hpHTgnXwHUt5iqQAORU9CFhuC9nk10uKlURJSN2Pejy9WMypCRCOQlZZPxd3IEX2
|
||||
D/9Ed5hcToEWymsk01cKMf36zlBptwEbn2FDWp1DWROhdj9SAa2qOVwWfzXpEp7H
|
||||
kKZwerBU4YlIsVqpPsxcWYbbjgKeJhQykrq5TSlXEfPFH+bilQOt+NEUdwMXXB7H
|
||||
SZJhZSTwIolQB1NxlNjkBgDESA03iRjik/10BtUT3MwRB6iWDToqGvQMTHinoIdP
|
||||
BeoURVvHTJkm/oyDf4apfG6UmOp2M0KMExBqnX82Z96g4/FrXNDVE4SSKt17P783
|
||||
O3ze7UsqUUm/pBHjrnPS18GYgrWQpD22N6FJYYWAVKwuaBNb2oEix/qP0tPSDtJG
|
||||
G28nwRW7TDoKE3R69mU8jNectY00KmjHRjd478VyITvIhw9eVA8ZBLAcoI+4lSof
|
||||
LVa6HzXF8vPRK5HnNr0GD/PLwfyAqBLilWerhFqatJLP2W3f5oO6MRbDFcD+EW57
|
||||
Gr0IsH0sQd3bx6SS9Ys+C+sDXeZByaWwPJtve1LB460VDgGIqABhV3ZA3dGcea/o
|
||||
u5FpNXEX0dkFqlDcWbiLHHZ2qCiOEwtbHYthH5GOZ8fgIEN8dkE0rzTOU0bfsx6n
|
||||
bVJ89SzdGTBiFc10ogP46hBsfuvXjvlhgn5CanIpDJWKYRV8KaN5LpiTTiTNAYh6
|
||||
037I24yn3zCjdznnFf3sUHBMtxGQkN1Hs+iSfbKiMy3afLkCDQRgm5TYARAAlmUi
|
||||
kOtXUGOI+AUutGOFW6ImKL0Ps/0368Ji+Arr7dWoDGWNtvkvaNja2GoxgnrHAhm/
|
||||
GAjIsIp8kgYRiFj+OaZCCMAR2RWETYtki8vwGy83W7D9/FX7u+4VaTnYhqwBugL4
|
||||
P53ILF/FutotLuP+HmZYSB+obXq7rj0vAbXwVRjajzvUuQtjZ3Nd/4zgNIUV8aKN
|
||||
kOM1D/PMPZmvXGKTLV0Gx6UjEzCeYFfH/4cJHqUMKU7Oma8uATdL7QbR0E66taqb
|
||||
vBXrUZZ1DkUj83Kbb46iGtJNV4kaI5lOwi/jgVwD8sx8T7HoRERAAvTbJQR5bMaX
|
||||
vlABk6Uoqh9SBbLGhWgMqacUdUOXreCRwKW7OqskiriuOrFvDT66WmXhJlodWn9I
|
||||
b1m0RaiJzMmuo/BmiCT23NfHpzWqsRkx27kOXbV5Qav+A5DmodMLSgUXTzgbVA47
|
||||
mN0l8Peq6u8lPhR2tteSSziquO8qzyT8qhMejyzsOAGT6XHvu0yyzSEOAHhwRFrJ
|
||||
1fG1BAT8MPy07geqJm2zJ7lXYN9mkSlaIF0qVKRfLqNN0VeyoHVouUdPkKgP1Juk
|
||||
gIOMlNEkMmG7ViuzQIYwi4x3BJB1XcZltvYraiw0WD6us6I+Jim4HU8XtmtQjncG
|
||||
uSY9n/dq6or6AQG4LoV57yuEDiiXL9UQe1JiMC0AEQEAAYkEcgQYAQoAJgUJA8Jn
|
||||
ABYhBPT8cPBzEAKEJO/CCo5CVlk/F3cgBQJgnj5QAhsAAkDBdCAEGQEKAB0WIQT9
|
||||
4EtwdRE7+whQILV7vY1NldufAwUCYJuU2AAKCRB7vY1NldufAx5yD/9uq+0Kl+h+
|
||||
tknaqiovAx2p7qgjW2dRBfsEXSjQnBv2fYwoH19q2toMt+FWQRo2xYSnEfQa17Ct
|
||||
8Eigzz8SOxSIb7Nm0DGKEnAVcBMUoCNw7HFKyDnRk5H55QbXFxI5OmigqkcJDUpu
|
||||
fBGe9n/s/oUCJmzejFgdKu1lTnySZ5xm1TUzcOVTjXzXsEQDxa65jdEsKnw70Cd3
|
||||
U0p9pGCdY6cuas+hvXV6H5EljpE+lxkD2Mwk/ljeJWTKN/0csxsrQNyXWJKJEKiA
|
||||
O7YsM54lgVPQWgninyJhT8w9uVmxafgqyEQOsMs2j2tPRqBXBCq5v/7xZGwX+zJv
|
||||
yGZ6aJFNoKy7T+UbhWPTBKlO/T82FybXHX41iGOQha1evGrgZaRqAkJK6DqQNR15
|
||||
A3E6md7VvxVxtgQpkou4PZlJqRtMDXAzxoKkARw6h3sMujXE3Pby6zO2Qytc2awZ
|
||||
XbpDKQBswqA9uyYbvSCwcpdl8dAtqvazBtxuE6ewFzTT5mDa2lJqJxsqqEJcW1bm
|
||||
Ls0lobmknkAnGWi1+0bhrawnd6DJJLZo3Zfwvlv8V/f2ccXehjx+CaryeJu4qfSF
|
||||
WMzO4P3BK+bgxrY5pT27ccfP9DqW6ckl4dVT7JnIaGj/iHSwslHYrW236pzU+byM
|
||||
60Tz+CmfzsVcB3JwCG+3r1LY20bmauAa8AkQjkJWWT8XdyDVOQ//fyLS8TZbovTv
|
||||
XVGdqsY0uyf1Huf0D7LRQRxa1MP6kGIAaZA/ArUpfzhL9xIk02bvmFSmFj85x8N6
|
||||
hQrTtXKFZNlfAE7SLpGSmIj7VCD1xXj84KuhRQDVD58NKxC7AsyOosFKN5b0AdqV
|
||||
zMwAg3hrudGEmBzIX6mwxU/m0EKb3cQdY4cAJn52DpX7E2FC8Fdxr05M/G/IRS+L
|
||||
Ww2twdDXgiKKhUQWGyEIOQuAfpKBqJuPTp52memzgXK2ZS2AWGoHwk6jGl6y8aQ3
|
||||
eYe6q2W2M6U0Hq6j71GO8aLYzn3rZv3EuELhfVfKhJ3EqgniLkJbjr5RJ12ndMO0
|
||||
i4Cy5moDVoS4ZcnG/XMVsJSSOBeSGWE7MNtrFw63LpDrW6qup4u+q/wMjYktqw1O
|
||||
aW+bd7kfq2ao0T2SVCzxRBBRkS0FM6dJkMq4ST1eQKKX2vWN/MMC9OBhPr0wp3pl
|
||||
219vflh7fGAyQcoTXn0W2mfFprV2zhG2FL7hYwLT059C706QJ216admJpMmoW9GW
|
||||
RNETNv0NxE6dLhN7F8OLBG9yNeFTJLr0oxeH9i5rlAVZ3xL/HkH7lD64eqSu28Rv
|
||||
WSR14KjuoG20j0ejMH5oOHl9NBkxJstLQaLSwJ44kTBDPni1E5cFzu3IAdPrcQHt
|
||||
GJDlS+YjTuu92TulWVx1gUrUN733t5M=
|
||||
=NiKY
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
@ -1,89 +0,0 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
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
|
||||
pqiU76AXZscBb0iPCg==
|
||||
=gWi8
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
@ -1,40 +0,0 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
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
|
||||
=mmx4
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
@ -1,52 +0,0 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
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
|
||||
jLZugv6bMuMLjA==
|
||||
=0krM
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
@ -1,43 +0,0 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
Comment: Hostname:
|
||||
Version: Hockeypuck 2.2
|
||||
|
||||
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
|
||||
yQdYDjoIbgfDCQ7KTnaRHQ==
|
||||
=32hh
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
@ -1,76 +0,0 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mQINBGFsecoBEADuGBm1JdAUyDE+gDTb8cpxeoEdFAwII/DVduz+nQU7sISdwll4
|
||||
PX0J8+4W135645sdnhU8PyL6C+wJAfvutculAIfME5gQKqPloyxiIT4U6RX2JQJG
|
||||
7iiwx7IK9DA5b/KiIAmgwpfJC6dn1fpxKAc//1AYFCe9O3FC1e6G1/qmMDPRfwAu
|
||||
Ah9ZIchAxzb6KCeD6GWWt9HqsOZB36X1nU/MFKh51d5Chst5SbI3HgGkQCqswmz3
|
||||
YFDPti7zR0FGSiWRgs9fLHY2KUMOVXvGXIGD5Cpum8t5J6XKZGL0iwGmbco1tpvE
|
||||
uvl0qUamYzwhJtKo1Z29cAYz98/xeQkLhpB/FQ41TCaomrn1ljcUHsNscHr86gDb
|
||||
sAY1S49V9JwjvNLeBagDEiroAFAV6a3SDsBAn/kcKlai4+0zV03t4M46+ZiBnb9x
|
||||
igW9VjntaX7Gw/5awnT3gYYr/cGgeMBRdCs1Qx9jPDhDT5csqhYd4FgcS/lWZz5w
|
||||
Yi99cW9GjDlB88FLDY7Ss5YfFKgx0NU5WP6Z4BTdsiZDWGLe91EHWCD5m3mAnpSK
|
||||
/Ty0vZbiC0K91ydqrob4JiSAJrXamCDqiPMZFCKDQ3rTWekIHFjrVBLduZ3SCU30
|
||||
Przja7/6PT6W3f31YQnP/x2TQy0fN8ZpwAndz0CfYmJMNMcxw0KYZ6BRQQARAQAB
|
||||
tCpTcGVjdGVyIFNpZ25lciA8bm9yZXBseUBzcGVjdGVyLnNvbHV0aW9ucz6JAlQE
|
||||
EwEKAD4CGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AWIQR4WiJp7jqXNqwaT0yG
|
||||
S3z5qBH+9wUCZU5OogUJBcMIWAAKCRCGS3z5qBH+94t+EADok7pwnc3+B3MwJ33h
|
||||
zEmJL19TgN+jZQeaKWQZLXWLSk16tDweZTvn/dkCh/sd9x3xDBQfVUNi4/C5608b
|
||||
iQx3SniP6wD7jsfS/qA5CMMHHWWZ3Z+XCFy7aQc6gNKZEx/d/+rno5euTJcb7c5C
|
||||
mfPPmF/ETLO5qGvXkdR/sMLPTjPojUGPzjgWV7iv1apfolUpQvCm8QHUAs5hMGiN
|
||||
AiHpkanSLxTETFgN6qOR35+w6jBITgnsQC1EhSBSU4JSSSUZXVdHKCNmzYRQcphH
|
||||
S8x9kcKmFYELErRJ8eR+eEjfPTbIGWIjTS9hOw3e/8aalX/HfLWeIwweoFBxNECZ
|
||||
ZTo9Cq6e9Ob2kerUkr7aIOywuWFyGiadR4iigU4D5Z5dDwSCokOzGAvDhIi/b2X1
|
||||
RL3/l/G8pKX0jncZckI9H0OM5o0hkouq9xcQFhZiKW5hUmH0Foo11gGPeZlwWIDQ
|
||||
GQ9k2ihUJeRT+dzMtyxEIIcLzZO5nfDz5LJhG8MwO8YAHZaKNtK59Bd9dnDPyadW
|
||||
rwLGRobsSagpdfaCan/giw/m8yThOgda7FTQDk3yWfQgSTJHqdyYRfhJwAShdm92
|
||||
QbKBnBXwysvBLhI3KpGtHf4VcATMKSkNJgPlx62sWEWNw3PrP6eoOIunswkL/o8G
|
||||
xX04jP/M2a9zJsvp9Q4rA698yokCVAQTAQoAPhYhBHhaImnuOpc2rBpPTIZLfPmo
|
||||
Ef73BQJhbHnKAhsDBQkDwmcABQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJEIZL
|
||||
fPmoEf73KLIQAMg9R3NrlveKlCo2amAI6CbNLsRS+xLSHdFE+STT4ofXCbEznkAN
|
||||
kzknl9ZvnytCroiGsHdFw5T2wtyS8WwEZVwhxqbKrg0EBL0edgN3bW1xb3MmuSu6
|
||||
43XxsaSZ9A6J2r0zCm4W6jrVV+miw2aaMGYF5+5Izu2hllePECeMCtCWrhRImzg1
|
||||
Ws2gmjG9aLyRP8MEDXjE9IGglvKbgSIJqcv6MMxt0bChlD45IMDgbIxYL3Ux/oQY
|
||||
1vyd7dfzwXrfy6jIFM6X+AG2GEyL5SE/HVXcOq9VrtuNTu+H2AQ2pzhfnbCgKKt+
|
||||
rFeqhn/EWAT8P2wn0CsCxRgfbDJLuR+wo73je87HVOeWadm7STfD9vej6aaj3z01
|
||||
Qt9rgSaZPlHeE8nFPylqOGhGa2ScmvwjaIiz9fViRgBqvZHCsdHJbzDKIC5HTH3J
|
||||
vOIaEabWVMiEUN4954+fCNtsmH+ms9XUpXFYbtda4Sb6l4qj1vVWmPL1/UbePS80
|
||||
XdAJtIVbkwfSoFV+yLTH5dTfXPJ42ya78kH+HYu14HVIOu01VSFerZlQoMJv+SWI
|
||||
zvV2kh0a3wt1yQa1x0AgbDPSiD3DsWQzNsMWJtYdGx6UHJHxsWk2pZucypUum+/3
|
||||
jT0wvUGMJLDV2P7t9qoQKRYaLy8soDsaDNqmuzIN09pu3ukjRTD7fnYUiQIzBBAB
|
||||
CgAdFiEE7MC0q9dOcW9a3glSKLNYqIQ7AQkFAmFse6UACgkQKLNYqIQ7AQm9qw//
|
||||
RgssFn0ODLDt4KwOgh790KR+xRRih7DXioqVTiUmy/HhVEQl6TdI/yPakJZDQPe7
|
||||
ZASuw4cSXt/lItf+Iuzykh7MNXOWI8Gw90xnAZtV8XOg+SVNgp+3RUNguAzTodmq
|
||||
1F0CYmmmWUjg0fusbF+5mZq314CLLrYHPBZH1wdcJupBXJrJwiPeaUtOB0lr7o4j
|
||||
HFzGqhvs1cNINuv7QyAw8Zlxa1cqKVRtfNJ2oD+U3HpJZcRwEx+smc0yM2z4o6Mr
|
||||
rk+SrXGpB3LK25pCR01x59JPpLbBZj9O7sh5RE0tngawt65Paan1UkjV1X0Dk/An
|
||||
yYaE07xfg+KesglQMVlDGvgvmmmdNl9/Q5XSYtLisbpbr+5j9Srq47UGSzUO5Ejw
|
||||
GzS9gTf7tWmwfii1qUgqrtlhjl2JDdLmv8m8nYPrQwuJpKyaRFgAp/XLEzl8AJVW
|
||||
c0OichRaWGhnojQPzwHD+rxVJIIMGDSDbK+nWaklnzOM3cyXPrQIiNSJ0MNijMs3
|
||||
TUh2AtR/gysufgeMHuM8YOvklPJa/aTovNyg39w/wB14O4adelj0RlMQ0EBtQbeg
|
||||
RdXwPPnsxirnSa//CPa1muwPCHNb3rDINcUwRhW0/uRwxN6lN4UJXv8p1pYf+Pia
|
||||
Gdd8pzO0DqJ1eYnbUEpiOgeXtNvJvlRg3dxShZDY8SS5Ag0EYWx5ygEQAKpcyEv2
|
||||
IkCQAUlBvLEyDCStlkIsieo5IQOjJ70s4pgq5e8h4mdkuLYLAzwxuAn55R0F7bVN
|
||||
dPhwBG0doDY2IFv98PY9eZuVGLc/Ru7Je1RB2ifoZs3gjg9Vv94JxmRbBfWGWBdP
|
||||
sr1MY6hMqciX5rxdBX/0EOlVD5W3FSol+eTe/PNgUSvJ4i8oMIlgrcsoDW73bD9t
|
||||
PtOJRAIM41CAf9rYOvjH51ejsVFz2XXEJfaBV8pH9Gz1ef32PNNCYsAxxQPcZYVM
|
||||
wdQx9yD+00T0yrJp7ipaH5stOJOnWdFUAZn/Bkldb9sDld1i8mgmwBXUOrWi/u9c
|
||||
Z5EYXzJ4UHuTZ+bpitTFoVh5Li0jrJNtOTqpzekGJCkoUtDhY4Wt+526ZQDGik3G
|
||||
Wo0PNeVfV82yj8V2P8UWATADrXWu497AFAgVTegDS6Nk6ROgRR7YgU+iYEhh8pnU
|
||||
+KgYqrwMt/22qoydR3Hq0TK+eEE/W0x7hcJWMJXovRKYH3V5r7Zb1ol0PcDABBi7
|
||||
W85yH2qJx8KRF1AFrIw85B4OLjThJ9G+QQUmiP8JuGS8hwG7fL2GAkgbFm40v4uf
|
||||
EH1dN+RXvYrEqRsyVTjUHj3KgVd7T+pMAQ/Svibbb7zoy6BBQfm0WwV3ZlBXNL5A
|
||||
/pxlyy52oAxHZ0Uy+V3MisNl8ATJp4a4BA53ABEBAAGJAjwEGAEKACYWIQR4WiJp
|
||||
7jqXNqwaT0yGS3z5qBH+9wUCYWx5ygIbDAUJA8JnAAAKCRCGS3z5qBH+9+msD/0V
|
||||
Nz0uJUU6fAPsOLuI2APdIPwsGDDaBg66Co8CC1gKMsAg2bhQgBLV61u6ymbKvOej
|
||||
Oxeh8JwsfeL13GPCZIKeQejgCqKMrH4/nOSPloOUnHs45pYwsZym/wofwKBgo4XP
|
||||
hRHWzddpvLFWmrfBxMQyIjSbBYO+IrDEH3fNH1p6IQhV+mKWyKvpkFMUkHl0kPR1
|
||||
IZ8rPbO8iKxcjDG6jKHXNWJa+k9pWAOA8jqTs91h8K5mKz1SFmR21/eql5NBGPPJ
|
||||
nHFHOOvIf2CEuiHzLr9iPGV2amurrhq/hUNruUaegfeWaidxuZQDETjlxtANjD2K
|
||||
LH9gvX9QPPPfQ/9Dv7QTS8rZ6yKs0FSTWBhPgOPbYPlfdtK5ISX1yzBjQspYvKs4
|
||||
DNO6PRjoBWr8hVSEOc4rg0Y4vILLI8QcsSZ4fodMCTh2x1Td0k9+4M8oMzYq85J+
|
||||
sKsPvsjp7Qgo/uaTLyQF2eKvhPUceTBamKmx1NyyprL521Yqc1iI6xBdA0l1bfpG
|
||||
CeaCnu1zwKjdy3El3DuWQ7a6LUM5LAOVKPUUHwIdziabE54ebdM0EqkINtmkb+vt
|
||||
1qTwsyo2tvQX0MCyhz9XNjAXy3KYooxh96NidaL2dh1ZekIi3tGOEy7VaGwRpRiW
|
||||
AslyR3rqJGWyBr6RXvo0+ggdgeUn4EAWkK6mnOBVQw==
|
||||
=puwO
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user