Compare commits
136 Commits
secp256k1-
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
077d2142cc | ||
|
|
cab72b2037 | ||
|
|
7d0699faa0 | ||
|
|
e96aa0d3f8 | ||
|
|
20670b9b7d | ||
|
|
3779317388 | ||
|
|
cc55b5f13a | ||
|
|
3b1e9e5817 | ||
|
|
dfd947cb69 | ||
|
|
698f8b08a1 | ||
|
|
7df781c77c | ||
|
|
e4b63fbd19 | ||
|
|
29cd4b7909 | ||
|
|
c87d5cc3c2 | ||
|
|
3fbad787a4 | ||
|
|
9c53c6d6d0 | ||
|
|
ee2f339136 | ||
|
|
1468b5d50d | ||
|
|
9c28138cd3 | ||
|
|
854eb513e9 | ||
|
|
bc526c90e1 | ||
|
|
15e5aaa4b9 | ||
|
|
570bf886eb | ||
|
|
3c12447a63 | ||
|
|
71604b9489 | ||
|
|
934b0ad890 | ||
|
|
70c4c94312 | ||
|
|
3b8677e59b | ||
|
|
3a1712bdbf | ||
|
|
dad9fe2fcc | ||
|
|
f5a52f9eae | ||
|
|
6cbb144326 | ||
|
|
4049ebcdda | ||
|
|
af031d9425 | ||
|
|
53c999a01b | ||
|
|
77bc7ffe2b | ||
|
|
184ccc225d | ||
|
|
951c119b42 | ||
|
|
5b724b448a | ||
|
|
b9887be8d3 | ||
|
|
5ce39fefaf | ||
|
|
0a500ea002 | ||
|
|
eb4b219221 | ||
|
|
5cc1b0c551 | ||
|
|
03e2e76b24 | ||
|
|
7b0ce57009 | ||
|
|
167ed35693 | ||
|
|
3aff03df53 | ||
|
|
b70c74d067 | ||
|
|
a378e1a33b | ||
|
|
cb6fc2a066 | ||
|
|
36847e1170 | ||
|
|
49ecc4810e | ||
|
|
dd94f745a3 | ||
|
|
6aca78cade | ||
|
|
6bd3d80303 | ||
|
|
8e134305c5 | ||
|
|
cc1b434f3d | ||
|
|
8dd8b9efc0 | ||
|
|
a675acccea | ||
|
|
a4d0ec4062 | ||
|
|
e975cbe6f8 | ||
|
|
ad90ea0d38 | ||
|
|
4e68815fa9 | ||
|
|
286e04ad25 | ||
|
|
2ced4c1996 | ||
|
|
3b069c12ca | ||
|
|
b25289b7b5 | ||
|
|
6eb46da87a | ||
|
|
73acc00ab6 | ||
|
|
a896809286 | ||
|
|
af879a30f1 | ||
|
|
7f707017b7 | ||
|
|
9c826d7819 | ||
|
|
1623f923b3 | ||
|
|
6c7662ca09 | ||
|
|
0b3b1a5c3f | ||
|
|
7c0aa1545d | ||
|
|
da736c8cef | ||
|
|
a4d86f9ee3 | ||
|
|
68966e5c26 | ||
|
|
e12fdfa47c | ||
|
|
d30cc4432c | ||
|
|
23f2b9197a | ||
|
|
b69e8f3629 | ||
|
|
0aedd1df46 | ||
|
|
f5d5e9dc30 | ||
|
|
eb06840de0 | ||
|
|
92c57d276c | ||
|
|
0ce32e4314 | ||
|
|
056d5f83a6 | ||
|
|
58cc096f8e | ||
|
|
2a456dd602 | ||
|
|
e1f2ce41ad | ||
|
|
13e1fafbe8 | ||
|
|
ad02b8a33c | ||
|
|
abb598d3b0 | ||
|
|
3b36947419 | ||
|
|
41cd6a68c0 | ||
|
|
e42931cd55 | ||
|
|
2468578e72 | ||
|
|
66ff275f46 | ||
|
|
5fd8e9416a | ||
|
|
7666060c8e | ||
|
|
0dddf3095f | ||
|
|
42968028cc | ||
|
|
419ed1a699 | ||
|
|
f7d5b4fb8f | ||
|
|
ad60a37d0e | ||
|
|
ca758e1288 | ||
|
|
342c85a39e | ||
|
|
b2c362d5a7 | ||
|
|
1805aeb374 | ||
|
|
378ab611f5 | ||
|
|
f67a2caf53 | ||
|
|
0df1f79e5c | ||
|
|
89a6b1296e | ||
|
|
7b9affb3de | ||
|
|
64a3f1c00b | ||
|
|
3cb3d322a0 | ||
|
|
a26ba49bc6 | ||
|
|
df7529b1a1 | ||
|
|
6170157daa | ||
|
|
d5393bd436 | ||
|
|
817458a0c3 | ||
|
|
a90d553f1e | ||
|
|
3b9998180f | ||
|
|
efc9d9d554 | ||
|
|
96df6284e1 | ||
|
|
4564c5d25a | ||
|
|
dba1a9a2be | ||
|
|
35bebe13bc | ||
|
|
acb1d767e8 | ||
|
|
f8f50c0dd9 | ||
|
|
6b89a0c5ea | ||
|
|
87b5f992d0 |
@ -6,7 +6,7 @@ Drongo is a Java Bitcoin library built mainly to support [Sparrow Wallet](https:
|
||||
|
||||
Drongo can be built with
|
||||
|
||||
`./gradlew shadowJar`
|
||||
`./gradlew jar`
|
||||
|
||||
## License
|
||||
|
||||
|
||||
46
build.gradle
46
build.gradle
@ -1,14 +1,5 @@
|
||||
buildscript {
|
||||
repositories {
|
||||
maven {
|
||||
url "https://plugins.gradle.org/m2/"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id 'java-library'
|
||||
id 'extra-java-module-info'
|
||||
}
|
||||
|
||||
tasks.withType(AbstractArchiveTask) {
|
||||
@ -27,41 +18,28 @@ repositories {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation ('com.googlecode.json-simple:json-simple:1.1.1') {
|
||||
exclude group: 'junit', module: 'junit'
|
||||
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 ('org.bouncycastle:bcprov-jdk18on:1.77')
|
||||
implementation('org.pgpainless:pgpainless-core:1.6.7')
|
||||
implementation ('de.mkammerer:argon2-jvm:2.11') {
|
||||
implementation ('de.mkammerer:argon2-jvm:2.12') {
|
||||
exclude group: 'net.java.dev.jna', module: 'jna'
|
||||
}
|
||||
implementation ('net.java.dev.jna:jna:5.8.0')
|
||||
implementation ('ch.qos.logback:logback-classic:1.4.14') {
|
||||
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') {
|
||||
exclude group: 'org.slf4j'
|
||||
}
|
||||
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')
|
||||
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')
|
||||
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')
|
||||
}
|
||||
|
||||
@ -1,21 +0,0 @@
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,54 +0,0 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
@ -1,52 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -1,164 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@ -1,53 +0,0 @@
|
||||
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-8.9-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
15
gradlew
vendored
15
gradlew
vendored
@ -1,7 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
# Copyright © 2015 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,6 +15,8 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
@ -55,7 +57,7 @@
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
@ -84,7 +86,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 "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
@ -112,7 +114,6 @@ 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.
|
||||
@ -170,7 +171,6 @@ 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,15 +203,14 @@ fi
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# * DEFAULT_JVM_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" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
|
||||
25
gradlew.bat
vendored
25
gradlew.bat
vendored
@ -13,6 +13,8 @@
|
||||
@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 ##########################################################################
|
||||
@ -43,11 +45,11 @@ set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
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.
|
||||
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
|
||||
|
||||
goto fail
|
||||
|
||||
@ -57,22 +59,21 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
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.
|
||||
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
|
||||
|
||||
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%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
|
||||
@ -75,10 +75,14 @@ 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);
|
||||
Header header = Header.getHeader(headerInt, ignoreNetwork);
|
||||
if(header == null) {
|
||||
throw new IllegalArgumentException("Unknown header bytes for extended key on " + Network.getCanonical().getName() + ": " + DeterministicKey.toBase58(serializedKey).substring(0, 4));
|
||||
}
|
||||
@ -239,7 +243,7 @@ public class ExtendedKey {
|
||||
return Network.get().getXpubHeader();
|
||||
}
|
||||
|
||||
private static Header getHeader(int header) {
|
||||
private static Header getHeader(int header, boolean ignoreNetwork) {
|
||||
for(Header extendedKeyHeader : getHeaders(Network.get())) {
|
||||
if(header == extendedKeyHeader.header) {
|
||||
return extendedKeyHeader;
|
||||
@ -249,6 +253,9 @@ 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.");
|
||||
}
|
||||
}
|
||||
|
||||
5
src/main/java/com/sparrowwallet/drongo/FileType.java
Normal file
5
src/main/java/com/sparrowwallet/drongo/FileType.java
Normal file
@ -0,0 +1,5 @@
|
||||
package com.sparrowwallet.drongo;
|
||||
|
||||
public enum FileType {
|
||||
TEXT, JSON, BINARY, UNKNOWN;
|
||||
}
|
||||
157
src/main/java/com/sparrowwallet/drongo/IOUtils.java
Normal file
157
src/main/java/com/sparrowwallet/drongo/IOUtils.java
Normal file
@ -0,0 +1,157 @@
|
||||
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,6 +8,8 @@ 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;
|
||||
@ -17,9 +19,13 @@ 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() {
|
||||
@ -100,6 +106,24 @@ 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,6 +5,10 @@ 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
|
||||
@ -110,9 +114,33 @@ public class NativeUtils {
|
||||
String tempDir = System.getProperty("java.io.tmpdir");
|
||||
File generatedDir = new File(tempDir, prefix + System.nanoTime());
|
||||
|
||||
if (!generatedDir.mkdir())
|
||||
if(!createOwnerOnlyDirectory(generatedDir)) {
|
||||
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", 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);
|
||||
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);
|
||||
|
||||
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, 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, String spAddressHrp, String spScanKeyHrp, String spSpendKeyHrp, ExtendedKey.Header xprvHeader, ExtendedKey.Header xpubHeader, int dumpedPrivateKeyHeader, int defaultPort) {
|
||||
this.name = name;
|
||||
this.displayName = displayName;
|
||||
this.home = home;
|
||||
@ -21,6 +21,9 @@ 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;
|
||||
@ -35,6 +38,9 @@ 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;
|
||||
@ -70,6 +76,18 @@ 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;
|
||||
}
|
||||
|
||||
48
src/main/java/com/sparrowwallet/drongo/OsType.java
Normal file
48
src/main/java/com/sparrowwallet/drongo/OsType.java
Normal file
@ -0,0 +1,48 @@
|
||||
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,15 +6,18 @@ 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;
|
||||
|
||||
@ -22,12 +25,17 @@ public class OutputDescriptor {
|
||||
private static final String INPUT_CHARSET = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ ";
|
||||
private static final String CHECKSUM_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
||||
|
||||
private static final Pattern XPUB_PATTERN = Pattern.compile("(\\[[^\\]]+\\])?(.(?:pub|prv)[^/\\,)]{100,112})(/[/\\d*'hH<>;]+)?");
|
||||
private static final Pattern PUBKEY_PATTERN = Pattern.compile("(\\[[^\\]]+\\])?(0[23][0-9a-fA-F]{32})");
|
||||
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 MULTI_PATTERN = Pattern.compile("multi\\((\\d+)");
|
||||
private static final Pattern KEY_ORIGIN_PATTERN = Pattern.compile("\\[([A-Fa-f0-9]{8})([/\\d'hH]+)?\\]");
|
||||
public 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;
|
||||
@ -35,6 +43,9 @@ 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));
|
||||
@ -61,18 +72,45 @@ 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);
|
||||
}
|
||||
@ -81,6 +119,10 @@ public class OutputDescriptor {
|
||||
return multisigThreshold;
|
||||
}
|
||||
|
||||
public Map<ExtendedKey, String> getChildDerivationsMap() {
|
||||
return Collections.unmodifiableMap(mapChildrenDerivations);
|
||||
}
|
||||
|
||||
public String getChildDerivationPath(ExtendedKey extendedPublicKey) {
|
||||
return mapChildrenDerivations.get(extendedPublicKey);
|
||||
}
|
||||
@ -153,7 +195,7 @@ public class OutputDescriptor {
|
||||
}
|
||||
|
||||
public boolean isCosigner() {
|
||||
return !isMultisig() && scriptType.isAllowed(PolicyType.MULTI);
|
||||
return !isMultisig() && scriptType.isAllowed(PolicyType.MULTI_HD);
|
||||
}
|
||||
|
||||
public ExtendedKey getSingletonExtendedPublicKey() {
|
||||
@ -168,6 +210,22 @@ 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)) {
|
||||
@ -224,7 +282,7 @@ public class OutputDescriptor {
|
||||
}
|
||||
|
||||
public Address getAddress(DeterministicKey childKey) {
|
||||
return scriptType.getAddress(childKey);
|
||||
return scriptType.getAddress(PolicyType.SINGLE_HD, childKey);
|
||||
}
|
||||
|
||||
private Address getAddress(Script multisigScript) {
|
||||
@ -256,8 +314,12 @@ public class OutputDescriptor {
|
||||
}
|
||||
|
||||
public Wallet toWallet() {
|
||||
if(isSilentPayments()) {
|
||||
return toSilentPaymentWallet();
|
||||
}
|
||||
|
||||
Wallet wallet = new Wallet();
|
||||
wallet.setPolicyType(isMultisig() || isCosigner() ? PolicyType.MULTI : PolicyType.SINGLE);
|
||||
wallet.setPolicyType(isMultisig() || isCosigner() ? PolicyType.MULTI_HD : PolicyType.SINGLE_HD);
|
||||
wallet.setScriptType(scriptType);
|
||||
|
||||
for(Map.Entry<ExtendedKey,KeyDerivation> extKeyEntry : extendedPublicKeys.entrySet()) {
|
||||
@ -267,11 +329,13 @@ 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/*") || childDerivation.endsWith("/1/*")) {
|
||||
if(childDerivation.endsWith("/<0;1>/*")) {
|
||||
childDerivation = childDerivation.substring(0, childDerivation.length() - 8);
|
||||
} else if(childDerivation.endsWith("/0/*") || childDerivation.endsWith("/1/*")) {
|
||||
childDerivation = childDerivation.substring(0, childDerivation.length() - 4);
|
||||
}
|
||||
try {
|
||||
keystore = Keystore.fromMasterPrivateExtendedKey(masterPrivateExtendedKey, KeyDerivation.parsePath(childDerivation));
|
||||
keystore = Keystore.fromMasterPrivateExtendedKey(masterPrivateExtendedKey, wallet.getPolicyType(), KeyDerivation.parsePath(childDerivation));
|
||||
} catch(MnemonicException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
@ -287,10 +351,54 @@ 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.");
|
||||
@ -307,14 +415,21 @@ public class OutputDescriptor {
|
||||
keystore.setExtendedPublicKey(extendedKey);
|
||||
setKeystoreLabel(keystore);
|
||||
wallet.getKeystores().add(keystore);
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(isCosigner() ? PolicyType.MULTI : PolicyType.SINGLE, wallet.getScriptType(), wallet.getKeystores(), 1));
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(isCosigner() ? PolicyType.MULTI_HD : PolicyType.SINGLE_HD, wallet.getScriptType(), wallet.getKeystores(), 1));
|
||||
applyAnnotations(wallet);
|
||||
|
||||
return wallet;
|
||||
}
|
||||
|
||||
public void setKeystoreLabel(Keystore keystore) {
|
||||
String label = null;
|
||||
if(keystore.getExtendedPublicKey() != null && mapExtendedPublicKeyLabels.get(keystore.getExtendedPublicKey()) != null) {
|
||||
String label = mapExtendedPublicKeyLabels.get(keystore.getExtendedPublicKey()).trim();
|
||||
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();
|
||||
if(label.length() > Keystore.MAX_LABEL_LENGTH) {
|
||||
label = label.substring(0, Keystore.MAX_LABEL_LENGTH);
|
||||
}
|
||||
@ -339,6 +454,16 @@ 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()) {
|
||||
@ -360,12 +485,45 @@ 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);
|
||||
@ -377,7 +535,7 @@ public class OutputDescriptor {
|
||||
}
|
||||
|
||||
int threshold = getMultisigThreshold(descriptor);
|
||||
return getOutputDescriptorImpl(scriptType, threshold, descriptor, mapExtendedPublicKeyLabels);
|
||||
return getOutputDescriptorImpl(scriptType, threshold, descriptor, mapExtendedPublicKeyLabels, annotations);
|
||||
}
|
||||
|
||||
private static int getMultisigThreshold(String descriptor) {
|
||||
@ -390,56 +548,47 @@ public class OutputDescriptor {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
private static OutputDescriptor getOutputDescriptorImpl(ScriptType scriptType, int multisigThreshold, String descriptor, Map<ExtendedKey, String> mapExtendedPublicKeyLabels, Map<String, Integer> annotations) {
|
||||
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 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);
|
||||
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);
|
||||
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.size() == 0 ? scriptType.getDefaultDerivation().size() : derivation.size();
|
||||
int depth = derivation.isEmpty() ? scriptType.getDefaultDerivation().size() : derivation.size();
|
||||
DeterministicKey prvKey = extendedKey.getKey();
|
||||
DeterministicKey pubKey = new DeterministicKey(prvKey.getPath(), prvKey.getChainCode(), prvKey.getPubKey(), depth, extendedKey.getParentFingerprint());
|
||||
DeterministicKey pubKey = new DeterministicKey(List.of(prvKey.getPath().getLast()), prvKey.getChainCode(), prvKey.getPubKey(), depth, extendedKey.getParentFingerprint());
|
||||
extendedKey = new ExtendedKey(pubKey, pubKey.getParentFingerprint(), extendedKey.getKeyChildNumber());
|
||||
|
||||
if(derivation.size() == 0) {
|
||||
if(derivation.isEmpty()) {
|
||||
masterPrivateKeyMap.put(extendedKey, privateExtendedKey);
|
||||
}
|
||||
}
|
||||
@ -457,7 +606,170 @@ public class OutputDescriptor {
|
||||
}
|
||||
}
|
||||
|
||||
return new OutputDescriptor(scriptType, multisigThreshold, keyDerivationMap, keyChildDerivationMap, mapExtendedPublicKeyLabels, masterPrivateKeyMap);
|
||||
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;
|
||||
}
|
||||
|
||||
public static String normalize(String descriptor) {
|
||||
@ -547,24 +859,53 @@ public class OutputDescriptor {
|
||||
}
|
||||
|
||||
public String toString(boolean addKeyOrigin, boolean addKey, boolean addChecksum) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append(scriptType.getDescriptor());
|
||||
return toString(addKeyOrigin, addKey, addChecksum, false);
|
||||
}
|
||||
|
||||
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);
|
||||
public String toString(boolean addKeyOrigin, boolean addKey, boolean addChecksum, boolean alternateForm) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
||||
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));
|
||||
}
|
||||
builder.append(joiner.toString());
|
||||
builder.append(ScriptType.MULTISIG.getCloseDescriptor());
|
||||
builder.append(")");
|
||||
} else {
|
||||
ExtendedKey extendedPublicKey = getSingletonExtendedPublicKey();
|
||||
builder.append(toString(extendedPublicKey, addKeyOrigin, addKey));
|
||||
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);
|
||||
}
|
||||
builder.append(scriptType.getCloseDescriptor());
|
||||
|
||||
if(addChecksum) {
|
||||
String descriptor = builder.toString();
|
||||
@ -575,7 +916,7 @@ public class OutputDescriptor {
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private List<ExtendedKey> sortExtendedPubKeys(Collection<ExtendedKey> keys) {
|
||||
public List<ExtendedKey> sortExtendedPubKeys(Collection<ExtendedKey> keys) {
|
||||
List<ExtendedKey> sortedKeys = new ArrayList<>(keys);
|
||||
if(mapChildrenDerivations == null || mapChildrenDerivations.isEmpty() || mapChildrenDerivations.containsKey(null)) {
|
||||
return sortedKeys;
|
||||
@ -622,13 +963,60 @@ 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()) {
|
||||
keyBuilder.append(keyDerivation.getDerivationPath().replaceFirst("^m?/", "/").replace('\'', 'h'));
|
||||
String path = KeyDerivation.writePath(KeyDerivation.parsePath(keyDerivation.getDerivationPath()), useApostrophes).substring(1);
|
||||
keyBuilder.append(path);
|
||||
}
|
||||
keyBuilder.append("]");
|
||||
}
|
||||
@ -649,4 +1037,44 @@ 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,6 +4,8 @@ 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;
|
||||
@ -12,9 +14,13 @@ 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 {
|
||||
@ -47,6 +53,56 @@ 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++ ) {
|
||||
@ -338,6 +394,21 @@ 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) {
|
||||
|
||||
55
src/main/java/com/sparrowwallet/drongo/Version.java
Normal file
55
src/main/java/com/sparrowwallet/drongo/Version.java
Normal file
@ -0,0 +1,55 @@
|
||||
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());
|
||||
return Arrays.equals(data, address.data) && getVersion(Network.get()) == address.getVersion(Network.get()) && getScriptType() == address.getScriptType();
|
||||
}
|
||||
|
||||
public int hashCode() {
|
||||
return Arrays.hashCode(data) + getVersion(Network.get());
|
||||
return Arrays.hashCode(data) + getVersion(Network.get()) + getScriptType().hashCode();
|
||||
}
|
||||
|
||||
public static Address fromString(String address) throws InvalidAddressException {
|
||||
@ -134,6 +134,8 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,31 @@
|
||||
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,6 +5,7 @@ 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;
|
||||
@ -82,7 +83,7 @@ public class PaymentCode {
|
||||
}
|
||||
|
||||
public Address getNotificationAddress() {
|
||||
return ScriptType.P2PKH.getAddress(getNotificationKey());
|
||||
return ScriptType.P2PKH.getAddress(PolicyType.SINGLE_HD, 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,41 +16,117 @@ 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(pubKey);
|
||||
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);
|
||||
|
||||
Transaction toSpend = getBip322ToSpend(address, message);
|
||||
Transaction toSign = getBip322ToSign(toSpend);
|
||||
|
||||
TransactionOutput utxoOutput = toSpend.getOutputs().get(0);
|
||||
TransactionOutput utxoOutput = toSpend.getOutputs().getFirst();
|
||||
|
||||
PSBT psbt = new PSBT(toSign);
|
||||
PSBTInput psbtInput = psbt.getPsbtInputs().get(0);
|
||||
psbt.setGenericSignedMessage(message);
|
||||
PSBTInput psbtInput = psbt.getPsbtInputs().getFirst();
|
||||
psbtInput.setWitnessUtxo(utxoOutput);
|
||||
psbtInput.setSigHash(SigHash.ALL);
|
||||
psbtInput.sign(scriptType.getOutputKey(privKey));
|
||||
|
||||
TransactionSignature signature = psbtInput.isTaproot() ? psbtInput.getTapKeyPathSignature() : psbtInput.getPartialSignature(pubKey);
|
||||
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();
|
||||
TransactionInput finalizedTxInput = scriptType.addSpendingInput(finalizeTransaction, utxoOutput, pubKey, signature);
|
||||
TransactionWitness witness = new TransactionWitness(finalizeTransaction, signature);
|
||||
finalizeTransaction.addInput(Sha256Hash.ZERO_HASH, 0, new Script(new byte[0]), witness);
|
||||
|
||||
return Base64.getEncoder().encodeToString(finalizedTxInput.getWitness().toByteArray());
|
||||
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);
|
||||
|
||||
return SIMPLE_PREFIX + Base64.getEncoder().encodeToString(finalizedTxInput.getWitness().toByteArray());
|
||||
}
|
||||
|
||||
public static boolean verifyMessageBip322(ScriptType scriptType, Address address, String message, String signatureBase64) throws SignatureException {
|
||||
checkScriptType(scriptType);
|
||||
|
||||
if(signatureBase64.trim().isEmpty()) {
|
||||
String trimmed = signatureBase64.trim();
|
||||
if(trimmed.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(signatureBase64);
|
||||
signatureEncoded = Base64.getDecoder().decode(trimmed);
|
||||
} catch(IllegalArgumentException e) {
|
||||
throw new SignatureException("Could not decode base64 signature", e);
|
||||
}
|
||||
@ -74,17 +150,17 @@ public class Bip322 {
|
||||
}
|
||||
|
||||
if(scriptType == ScriptType.P2WPKH) {
|
||||
signature = witness.getSignatures().get(0);
|
||||
signature = witness.getSignatures().getFirst();
|
||||
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(pubKey))) {
|
||||
if(!address.equals(scriptType.getAddress(PolicyType.SINGLE_HD, pubKey))) {
|
||||
throw new SignatureException("Provided address does not match pubkey in signature");
|
||||
}
|
||||
} else if(scriptType == ScriptType.P2TR) {
|
||||
signature = witness.getSignatures().get(0);
|
||||
signature = witness.getSignatures().getFirst();
|
||||
pubKey = P2TR.getPublicKeyFromScript(address.getOutputScript());
|
||||
} else {
|
||||
throw new SignatureException(scriptType + " addresses are not supported");
|
||||
@ -94,8 +170,8 @@ public class Bip322 {
|
||||
Transaction toSign = getBip322ToSign(toSpend);
|
||||
|
||||
PSBT psbt = new PSBT(toSign);
|
||||
PSBTInput psbtInput = psbt.getPsbtInputs().get(0);
|
||||
psbtInput.setWitnessUtxo(toSpend.getOutputs().get(0));
|
||||
PSBTInput psbtInput = psbt.getPsbtInputs().getFirst();
|
||||
psbtInput.setWitnessUtxo(toSpend.getOutputs().getFirst());
|
||||
psbtInput.setSigHash(SigHash.ALL);
|
||||
|
||||
if(scriptType == ScriptType.P2TR) {
|
||||
@ -114,7 +190,7 @@ public class Bip322 {
|
||||
}
|
||||
|
||||
private static void checkScriptType(ScriptType scriptType) {
|
||||
if(!scriptType.isAllowed(PolicyType.SINGLE)) {
|
||||
if(!scriptType.isAllowed(PolicyType.SINGLE_HD)) {
|
||||
throw new UnsupportedOperationException("Only singlesig addresses are currently supported");
|
||||
}
|
||||
|
||||
@ -141,7 +217,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().get(0).setSequenceNumber(0L);
|
||||
toSpend.getInputs().getFirst().setSequenceNumber(0L);
|
||||
toSpend.addOutput(0L, address.getOutputScript());
|
||||
|
||||
return toSpend;
|
||||
@ -154,7 +230,7 @@ public class Bip322 {
|
||||
|
||||
TransactionWitness witness = new TransactionWitness(toSign);
|
||||
toSign.addInput(toSpend.getTxId(), 0L, new Script(new byte[0]), witness);
|
||||
toSign.getInputs().get(0).setSequenceNumber(0L);
|
||||
toSign.getInputs().getFirst().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;
|
||||
}
|
||||
|
||||
private static boolean hasHardenedBit(int a) {
|
||||
public static boolean hasHardenedBit(int a) {
|
||||
return (a & HARDENED_BIT) != 0;
|
||||
}
|
||||
|
||||
|
||||
218
src/main/java/com/sparrowwallet/drongo/crypto/DLEQProof.java
Normal file
218
src/main/java/com/sparrowwallet/drongo/crypto/DLEQProof.java
Normal file
@ -0,0 +1,218 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -326,6 +326,68 @@ 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).
|
||||
@ -435,53 +497,20 @@ 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() {
|
||||
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());
|
||||
ECKey key = this;
|
||||
|
||||
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);
|
||||
if(pub.hasOddYCoord()) {
|
||||
key = hasPrivKey() ? key.negatePrivate() : key.negate();
|
||||
}
|
||||
|
||||
return ECKey.fromPublicOnly(outputKey, true);
|
||||
}
|
||||
byte[] tweakHash = Utils.taggedHash("TapTweak", key.getPubKeyXCoord());
|
||||
ECKey tweakKey = ECKey.fromPrivate(tweakHash);
|
||||
|
||||
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;
|
||||
}
|
||||
return hasPrivKey() ? key.addPrivate(tweakKey, true) : key.add(tweakKey, true);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -68,6 +68,10 @@ public class LazyECPoint {
|
||||
return xcoord;
|
||||
}
|
||||
|
||||
public boolean hasOddYCoord() {
|
||||
return get().normalize().getYCoord().toBigInteger().testBit(0);
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return Hex.toHexString(getEncoded());
|
||||
}
|
||||
|
||||
@ -0,0 +1,39 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
128
src/main/java/com/sparrowwallet/drongo/crypto/X25519Key.java
Normal file
128
src/main/java/com/sparrowwallet/drongo/crypto/X25519Key.java
Normal file
@ -0,0 +1,128 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,59 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
40
src/main/java/com/sparrowwallet/drongo/dns/DnsAddress.java
Normal file
40
src/main/java/com/sparrowwallet/drongo/dns/DnsAddress.java
Normal file
@ -0,0 +1,40 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
58
src/main/java/com/sparrowwallet/drongo/dns/DnsPayment.java
Normal file
58
src/main/java/com/sparrowwallet/drongo/dns/DnsPayment.java
Normal file
@ -0,0 +1,58 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,69 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,193 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
package com.sparrowwallet.drongo.dns;
|
||||
|
||||
public class DnsPaymentValidationException extends Exception {
|
||||
public DnsPaymentValidationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
185
src/main/java/com/sparrowwallet/drongo/dns/OfflineResolver.java
Normal file
185
src/main/java/com/sparrowwallet/drongo/dns/OfflineResolver.java
Normal file
@ -0,0 +1,185 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,79 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
123
src/main/java/com/sparrowwallet/drongo/dns/RecordUtils.java
Normal file
123
src/main/java/com/sparrowwallet/drongo/dns/RecordUtils.java
Normal file
@ -0,0 +1,123 @@
|
||||
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,5 +1,6 @@
|
||||
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;
|
||||
@ -22,14 +23,11 @@ import java.io.InputStream;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.*;
|
||||
|
||||
public class PGPUtils {
|
||||
private static final Logger log = LoggerFactory.getLogger(PGPUtils.class);
|
||||
public static final String APPLICATION_KEYRING = "/gpg/pubkeys.asc";
|
||||
public static final String APPLICATION_KEYRING_DIR = "gpg/";
|
||||
public static final String PUBRING_GPG = "pubring.gpg";
|
||||
public static final String PUBRING_KBX = "pubring.kbx";
|
||||
|
||||
@ -53,9 +51,7 @@ public class PGPUtils {
|
||||
if(userPgpPublicKeyRingCollection != null) {
|
||||
options.addVerificationCerts(userPgpPublicKeyRingCollection);
|
||||
}
|
||||
if(appPgpPublicKeyRingCollection != null) {
|
||||
options.addVerificationCerts(appPgpPublicKeyRingCollection);
|
||||
}
|
||||
options.addVerificationCerts(appPgpPublicKeyRingCollection);
|
||||
if(detachedSignatureStream != null) {
|
||||
options.addVerificationOfDetachedSignatures(detachedSignatureStream);
|
||||
}
|
||||
@ -80,8 +76,7 @@ public class PGPUtils {
|
||||
signedByKey = publicKeyRing.getPublicKey(primaryKeyId);
|
||||
keySource = PGPKeySource.USER;
|
||||
log.debug("Signed using provided public key");
|
||||
} else if(appPgpPublicKeyRingCollection != null && appPgpPublicKeyRingCollection.getPublicKey(primaryKeyId) != null
|
||||
&& !isExpired(appPgpPublicKeyRingCollection.getPublicKey(primaryKeyId))) {
|
||||
} else if(appPgpPublicKeyRingCollection.getPublicKey(primaryKeyId) != null && !isExpired(appPgpPublicKeyRingCollection.getPublicKey(primaryKeyId))) {
|
||||
signedByKey = appPgpPublicKeyRingCollection.getPublicKey(primaryKeyId);
|
||||
keySource = PGPKeySource.APPLICATION;
|
||||
log.debug("Signed using application public key");
|
||||
@ -89,7 +84,7 @@ public class PGPUtils {
|
||||
signedByKey = userPgpPublicKeyRingCollection.getPublicKey(primaryKeyId);
|
||||
keySource = PGPKeySource.GPG;
|
||||
log.debug("Signed using user public key");
|
||||
} else if(appPgpPublicKeyRingCollection != null && appPgpPublicKeyRingCollection.getPublicKey(primaryKeyId) != null) {
|
||||
} else if(appPgpPublicKeyRingCollection.getPublicKey(primaryKeyId) != null) {
|
||||
signedByKey = appPgpPublicKeyRingCollection.getPublicKey(primaryKeyId);
|
||||
keySource = PGPKeySource.APPLICATION;
|
||||
log.debug("Signed using expired application public key");
|
||||
@ -115,9 +110,9 @@ public class PGPUtils {
|
||||
}
|
||||
|
||||
if(!result.getRejectedDetachedSignatures().isEmpty()) {
|
||||
throw new PGPVerificationException(result.getRejectedDetachedSignatures().get(0).getValidationException().getMessage());
|
||||
throw new PGPVerificationException(result.getRejectedDetachedSignatures().getFirst().getValidationException().getMessage());
|
||||
} else if(!result.getRejectedInlineSignatures().isEmpty()) {
|
||||
throw new PGPVerificationException(result.getRejectedInlineSignatures().get(0).getValidationException().getMessage());
|
||||
throw new PGPVerificationException(result.getRejectedInlineSignatures().getFirst().getValidationException().getMessage());
|
||||
}
|
||||
|
||||
throw new PGPVerificationException("No signatures found");
|
||||
@ -127,16 +122,22 @@ public class PGPUtils {
|
||||
}
|
||||
}
|
||||
|
||||
private static PGPPublicKeyRingCollection getApplicationKeyRingCollection() throws IOException {
|
||||
try(InputStream pubKeyStream = PGPUtils.class.getResourceAsStream(APPLICATION_KEYRING)) {
|
||||
if(pubKeyStream != null) {
|
||||
return PGPainless.readKeyRing().publicKeyRingCollection(pubKeyStream);
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.warn("Error loading application key rings", e);
|
||||
}
|
||||
|
||||
return null;
|
||||
return new PGPPublicKeyRingCollection(rings);
|
||||
}
|
||||
|
||||
private static PGPPublicKeyRingCollection getUserKeyRingCollection() {
|
||||
|
||||
@ -4,8 +4,9 @@ import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class Miniscript {
|
||||
private static final Pattern SINGLE_PATTERN = Pattern.compile("pkh?\\(");
|
||||
private static final Pattern KEYHASH_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;
|
||||
@ -23,7 +24,7 @@ public class Miniscript {
|
||||
}
|
||||
|
||||
public int getNumSignaturesRequired() {
|
||||
Matcher singleMatcher = SINGLE_PATTERN.matcher(script);
|
||||
Matcher singleMatcher = KEYHASH_PATTERN.matcher(script);
|
||||
if(singleMatcher.find()) {
|
||||
return 1;
|
||||
}
|
||||
@ -33,6 +34,11 @@ 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.equals(policyType)) {
|
||||
if(SINGLE_HD.equals(policyType)) {
|
||||
return new Policy(new Miniscript(scriptType.getDescriptor() + keystores.get(0).getScriptName() + scriptType.getCloseDescriptor()));
|
||||
}
|
||||
|
||||
if(MULTI.equals(policyType)) {
|
||||
if(MULTI_HD.equals(policyType)) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append(scriptType.getDescriptor());
|
||||
builder.append(MULTISIG.getDescriptor());
|
||||
@ -58,6 +58,10 @@ 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,13 +5,15 @@ import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import static com.sparrowwallet.drongo.protocol.ScriptType.*;
|
||||
|
||||
public enum PolicyType {
|
||||
SINGLE("Single Signature", P2WPKH), MULTI("Multi Signature", P2WSH), CUSTOM("Custom", P2WSH);
|
||||
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);
|
||||
|
||||
private String name;
|
||||
private ScriptType defaultScriptType;
|
||||
private final String name;
|
||||
private final String description;
|
||||
private final ScriptType defaultScriptType;
|
||||
|
||||
PolicyType(String name, ScriptType defaultScriptType) {
|
||||
PolicyType(String name, String description, ScriptType defaultScriptType) {
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
this.defaultScriptType = defaultScriptType;
|
||||
}
|
||||
|
||||
@ -19,6 +21,10 @@ 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. */
|
||||
private static final String CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
||||
public static final String CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
||||
|
||||
/** The Bech32 character set for decoding. */
|
||||
private static final byte[] CHARSET_REV = {
|
||||
public 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,6 +36,9 @@ 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;
|
||||
@ -102,12 +105,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 + 6];
|
||||
byte[] enc = new byte[hrpExpanded.length + values.length + BECH32_CHECKSUM_LEN];
|
||||
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[6];
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
byte[] ret = new byte[BECH32_CHECKSUM_LEN];
|
||||
for (int i = 0; i < BECH32_CHECKSUM_LEN; ++i) {
|
||||
ret[i] = (byte) ((mod >>> (5 * (5 - i))) & 31);
|
||||
}
|
||||
return ret;
|
||||
@ -121,6 +124,11 @@ 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));
|
||||
}
|
||||
|
||||
@ -141,7 +149,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('1');
|
||||
sb.append(BECH32_SEPARATOR);
|
||||
for (byte b : combined) {
|
||||
sb.append(CHARSET.charAt(b));
|
||||
}
|
||||
@ -154,6 +162,21 @@ 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());
|
||||
@ -173,23 +196,23 @@ public class Bech32 {
|
||||
upper = true;
|
||||
}
|
||||
}
|
||||
final int pos = str.lastIndexOf('1');
|
||||
if (pos < 1) throw new ProtocolException("Missing human-readable part");
|
||||
final int dataPartLength = str.length() - 1 - pos;
|
||||
if (dataPartLength < 6) throw new ProtocolException("Data part too short: " + dataPartLength);
|
||||
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;
|
||||
byte[] values = new byte[dataPartLength];
|
||||
for (int i = 0; i < dataPartLength; ++i) {
|
||||
char c = str.charAt(i + pos + 1);
|
||||
if (CHARSET_REV[c] == -1) throw new ProtocolException("Invalid character " + c + " at position " + i);
|
||||
char c = str.charAt(i + separator_pos + 1);
|
||||
values[i] = CHARSET_REV[c];
|
||||
}
|
||||
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);
|
||||
return values;
|
||||
}
|
||||
|
||||
private static byte[] encode(int witnessVersion, byte[] witnessProgram) {
|
||||
@ -204,7 +227,12 @@ 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) {
|
||||
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) {
|
||||
int acc = 0;
|
||||
int bits = 0;
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(64);
|
||||
@ -226,7 +254,11 @@ public class Bech32 {
|
||||
if (pad) {
|
||||
if (bits > 0)
|
||||
out.write((acc << (toBits - bits)) & maxv);
|
||||
} else if (bits >= fromBits || ((acc << (toBits - bits)) & maxv) != 0) {
|
||||
} 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
|
||||
throw new ProtocolException("Could not convert bits, invalid padding");
|
||||
}
|
||||
return out.toByteArray();
|
||||
|
||||
45
src/main/java/com/sparrowwallet/drongo/protocol/Block.java
Normal file
45
src/main/java/com/sparrowwallet/drongo/protocol/Block.java
Normal file
@ -0,0 +1,45 @@
|
||||
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,6 +19,10 @@ 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,6 +3,7 @@ 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;
|
||||
@ -198,7 +199,7 @@ public class Script {
|
||||
|
||||
for(ScriptType scriptType : SINGLE_KEY_TYPES) {
|
||||
if(scriptType.isScriptType(this)) {
|
||||
return new Address[] { scriptType.getAddress(scriptType.getPublicKeyFromScript(this)) };
|
||||
return new Address[] { scriptType.getAddress(PolicyType.SINGLE_HD, scriptType.getPublicKeyFromScript(this)) };
|
||||
}
|
||||
}
|
||||
|
||||
@ -212,6 +213,10 @@ 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(ECKey key) {
|
||||
public Address getAddress(PolicyType policyType, ECKey key) {
|
||||
return getAddress(key.getPubKey());
|
||||
}
|
||||
|
||||
@ -48,7 +48,7 @@ public enum ScriptType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Script getOutputScript(ECKey key) {
|
||||
public Script getOutputScript(PolicyType policyType, ECKey key) {
|
||||
return getOutputScript(key.getPubKey());
|
||||
}
|
||||
|
||||
@ -100,7 +100,7 @@ public enum ScriptType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Script getScriptSig(Script scriptPubKey, ECKey pubKey, TransactionSignature signature) {
|
||||
public Script getScriptSig(PolicyType policyType, 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(Transaction transaction, TransactionOutput prevOutput, ECKey pubKey, TransactionSignature signature) {
|
||||
Script scriptSig = getScriptSig(prevOutput.getScript(), pubKey, signature);
|
||||
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(Script scriptPubKey, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
public Script getMultisigScriptSig(PolicyType policyType, Script scriptPubKey, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
throw new ProtocolException(getName() + " is not a multisig script type");
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransactionInput addMultisigSpendingInput(Transaction transaction, TransactionOutput prevOutput, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
public TransactionInput addMultisigSpendingInput(PolicyType policyType, 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);
|
||||
return List.of(SINGLE_HD);
|
||||
}
|
||||
},
|
||||
P2PKH("P2PKH", "Legacy (P2PKH)", "m/44'/0'/0'") {
|
||||
@ -143,7 +143,7 @@ public enum ScriptType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Address getAddress(ECKey key) {
|
||||
public Address getAddress(PolicyType policyType, ECKey key) {
|
||||
return getAddress(key.getPubKeyHash());
|
||||
}
|
||||
|
||||
@ -165,7 +165,7 @@ public enum ScriptType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Script getOutputScript(ECKey key) {
|
||||
public Script getOutputScript(PolicyType policyType, ECKey key) {
|
||||
return getOutputScript(key.getPubKeyHash());
|
||||
}
|
||||
|
||||
@ -216,7 +216,7 @@ public enum ScriptType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Script getScriptSig(Script scriptPubKey, ECKey pubKey, TransactionSignature signature) {
|
||||
public Script getScriptSig(PolicyType policyType, 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(Transaction transaction, TransactionOutput prevOutput, ECKey pubKey, TransactionSignature signature) {
|
||||
Script scriptSig = getScriptSig(prevOutput.getScript(), pubKey, signature);
|
||||
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(Script scriptPubKey, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
public Script getMultisigScriptSig(PolicyType policyType, Script scriptPubKey, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
throw new ProtocolException(getName() + " is not a multisig script type");
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransactionInput addMultisigSpendingInput(Transaction transaction, TransactionOutput prevOutput, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
public TransactionInput addMultisigSpendingInput(PolicyType policyType, 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);
|
||||
return List.of(SINGLE_HD);
|
||||
}
|
||||
},
|
||||
MULTISIG("Bare Multisig", "Bare Multisig", "m/44'/0'/0'") {
|
||||
@ -266,7 +266,7 @@ public enum ScriptType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Address getAddress(ECKey key) {
|
||||
public Address getAddress(PolicyType policyType, ECKey key) {
|
||||
throw new ProtocolException("No single key address for multisig script type");
|
||||
}
|
||||
|
||||
@ -281,7 +281,7 @@ public enum ScriptType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Script getOutputScript(ECKey key) {
|
||||
public Script getOutputScript(PolicyType policyType, 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(Script scriptPubKey, ECKey pubKey, TransactionSignature signature) {
|
||||
public Script getScriptSig(PolicyType policyType, Script scriptPubKey, ECKey pubKey, TransactionSignature signature) {
|
||||
throw new ProtocolException(getName() + " is a multisig script type");
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransactionInput addSpendingInput(Transaction transaction, TransactionOutput prevOutput, ECKey pubKey, TransactionSignature signature) {
|
||||
public TransactionInput addSpendingInput(PolicyType policyType, Transaction transaction, TransactionOutput prevOutput, ECKey pubKey, TransactionSignature signature) {
|
||||
throw new ProtocolException(getName() + " is a multisig script type");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Script getMultisigScriptSig(Script scriptPubKey, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
public Script getMultisigScriptSig(PolicyType policyType, 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(Transaction transaction, TransactionOutput prevOutput, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
Script scriptSig = getMultisigScriptSig(prevOutput.getScript(), threshold, pubKeySignatures);
|
||||
public TransactionInput addMultisigSpendingInput(PolicyType policyType, Transaction transaction, TransactionOutput prevOutput, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
Script scriptSig = getMultisigScriptSig(policyType, 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);
|
||||
return List.of(MULTI_HD);
|
||||
}
|
||||
},
|
||||
P2SH("P2SH", "Legacy (P2SH)", "m/45'") {
|
||||
@ -452,7 +452,7 @@ public enum ScriptType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Address getAddress(ECKey key) {
|
||||
public Address getAddress(PolicyType policyType, ECKey key) {
|
||||
throw new ProtocolException("No single key address for script hash type");
|
||||
}
|
||||
|
||||
@ -472,7 +472,7 @@ public enum ScriptType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Script getOutputScript(ECKey key) {
|
||||
public Script getOutputScript(PolicyType policyType, 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(Script scriptPubKey, ECKey pubKey, TransactionSignature signature) {
|
||||
public Script getScriptSig(PolicyType policyType, Script scriptPubKey, ECKey pubKey, TransactionSignature signature) {
|
||||
throw new ProtocolException("Only multisig scriptSigs supported for " + getName() + " scriptPubKeys");
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransactionInput addSpendingInput(Transaction transaction, TransactionOutput prevOutput, ECKey pubKey, TransactionSignature signature) {
|
||||
public TransactionInput addSpendingInput(PolicyType policyType, Transaction transaction, TransactionOutput prevOutput, ECKey pubKey, TransactionSignature signature) {
|
||||
throw new ProtocolException("Only multisig scriptSigs supported for " + getName() + " scriptPubKeys");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Script getMultisigScriptSig(Script scriptPubKey, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
public Script getMultisigScriptSig(PolicyType policyType, 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(redeemScript, threshold, pubKeySignatures);
|
||||
Script multisigScript = MULTISIG.getMultisigScriptSig(policyType, 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(Transaction transaction, TransactionOutput prevOutput, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
Script scriptSig = getMultisigScriptSig(prevOutput.getScript(), threshold, pubKeySignatures);
|
||||
public TransactionInput addMultisigSpendingInput(PolicyType policyType, Transaction transaction, TransactionOutput prevOutput, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
Script scriptSig = getMultisigScriptSig(policyType, 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);
|
||||
return List.of(MULTI_HD);
|
||||
}
|
||||
},
|
||||
P2SH_P2WPKH("P2SH-P2WPKH", "Nested Segwit (P2SH-P2WPKH)", "m/49'/0'/0'") {
|
||||
@ -582,7 +582,7 @@ public enum ScriptType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Address getAddress(ECKey key) {
|
||||
public Address getAddress(PolicyType policyType, ECKey key) {
|
||||
Script p2wpkhScript = P2WPKH.getOutputScript(key.getPubKeyHash());
|
||||
return P2SH.getAddress(p2wpkhScript);
|
||||
}
|
||||
@ -602,7 +602,7 @@ public enum ScriptType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Script getOutputScript(ECKey key) {
|
||||
public Script getOutputScript(PolicyType policyType, ECKey key) {
|
||||
Script p2wpkhScript = P2WPKH.getOutputScript(key.getPubKeyHash());
|
||||
return P2SH.getOutputScript(p2wpkhScript);
|
||||
}
|
||||
@ -642,12 +642,12 @@ public enum ScriptType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Script getScriptSig(Script scriptPubKey, ECKey pubKey, TransactionSignature signature) {
|
||||
public Script getScriptSig(PolicyType policyType, Script scriptPubKey, ECKey pubKey, TransactionSignature signature) {
|
||||
if(!isScriptType(scriptPubKey)) {
|
||||
throw new ProtocolException("Provided scriptPubKey is not a " + getName() + " script");
|
||||
}
|
||||
|
||||
Script redeemScript = P2WPKH.getOutputScript(pubKey);
|
||||
Script redeemScript = P2WPKH.getOutputScript(policyType, 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(Transaction transaction, TransactionOutput prevOutput, ECKey pubKey, TransactionSignature signature) {
|
||||
Script scriptSig = getScriptSig(prevOutput.getScript(), pubKey, signature);
|
||||
public TransactionInput addSpendingInput(PolicyType policyType, Transaction transaction, TransactionOutput prevOutput, ECKey pubKey, TransactionSignature signature) {
|
||||
Script scriptSig = getScriptSig(policyType, prevOutput.getScript(), pubKey, signature);
|
||||
TransactionWitness witness = new TransactionWitness(transaction, pubKey, signature);
|
||||
return transaction.addInput(prevOutput.getHash(), prevOutput.getIndex(), scriptSig, witness);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Script getMultisigScriptSig(Script scriptPubKey, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
public Script getMultisigScriptSig(PolicyType policyType, Script scriptPubKey, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
throw new ProtocolException(getName() + " is not a multisig script type");
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransactionInput addMultisigSpendingInput(Transaction transaction, TransactionOutput prevOutput, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
public TransactionInput addMultisigSpendingInput(PolicyType policyType, 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);
|
||||
return List.of(SINGLE_HD);
|
||||
}
|
||||
},
|
||||
P2SH_P2WSH("P2SH-P2WSH", "Nested Segwit (P2SH-P2WSH)", "m/48'/0'/0'/1'") {
|
||||
@ -690,7 +690,7 @@ public enum ScriptType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Address getAddress(ECKey key) {
|
||||
public Address getAddress(PolicyType policyType, 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(ECKey key) {
|
||||
public Script getOutputScript(PolicyType policyType, 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(Script scriptPubKey, ECKey pubKey, TransactionSignature signature) {
|
||||
public Script getScriptSig(PolicyType policyType, Script scriptPubKey, ECKey pubKey, TransactionSignature signature) {
|
||||
throw new ProtocolException("Only multisig scriptSigs supported for " + getName() + " scriptPubKeys");
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransactionInput addSpendingInput(Transaction transaction, TransactionOutput prevOutput, ECKey pubKey, TransactionSignature signature) {
|
||||
public TransactionInput addSpendingInput(PolicyType policyType, Transaction transaction, TransactionOutput prevOutput, ECKey pubKey, TransactionSignature signature) {
|
||||
throw new ProtocolException("Only multisig scriptSigs supported for " + getName() + " scriptPubKeys");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Script getMultisigScriptSig(Script scriptPubKey, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
public Script getMultisigScriptSig(PolicyType policyType, 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(Transaction transaction, TransactionOutput prevOutput, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
Script scriptSig = getMultisigScriptSig(prevOutput.getScript(), threshold, pubKeySignatures);
|
||||
public TransactionInput addMultisigSpendingInput(PolicyType policyType, Transaction transaction, TransactionOutput prevOutput, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
Script scriptSig = getMultisigScriptSig(policyType, 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, CUSTOM);
|
||||
return List.of(MULTI_HD);
|
||||
}
|
||||
},
|
||||
P2WPKH("P2WPKH", "Native Segwit (P2WPKH)", "m/84'/0'/0'") {
|
||||
@ -796,7 +796,7 @@ public enum ScriptType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Address getAddress(ECKey key) {
|
||||
public Address getAddress(PolicyType policyType, ECKey key) {
|
||||
return getAddress(key.getPubKeyHash());
|
||||
}
|
||||
|
||||
@ -815,7 +815,7 @@ public enum ScriptType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Script getOutputScript(ECKey key) {
|
||||
public Script getOutputScript(PolicyType policyType, ECKey key) {
|
||||
return getOutputScript(key.getPubKeyHash());
|
||||
}
|
||||
|
||||
@ -860,12 +860,12 @@ public enum ScriptType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Script getScriptSig(Script scriptPubKey, ECKey pubKey, TransactionSignature signature) {
|
||||
public Script getScriptSig(PolicyType policyType, Script scriptPubKey, ECKey pubKey, TransactionSignature signature) {
|
||||
if(!isScriptType(scriptPubKey)) {
|
||||
throw new ProtocolException("Provided scriptPubKey is not a " + getName() + " script");
|
||||
}
|
||||
|
||||
if(!scriptPubKey.equals(getOutputScript(pubKey))) {
|
||||
if(!scriptPubKey.equals(getOutputScript(policyType, 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(Transaction transaction, TransactionOutput prevOutput, ECKey pubKey, TransactionSignature signature) {
|
||||
Script scriptSig = getScriptSig(prevOutput.getScript(), pubKey, signature);
|
||||
public TransactionInput addSpendingInput(PolicyType policyType, Transaction transaction, TransactionOutput prevOutput, ECKey pubKey, TransactionSignature signature) {
|
||||
Script scriptSig = getScriptSig(policyType, prevOutput.getScript(), pubKey, signature);
|
||||
TransactionWitness witness = new TransactionWitness(transaction, pubKey, signature);
|
||||
return transaction.addInput(prevOutput.getHash(), prevOutput.getIndex(), scriptSig, witness);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Script getMultisigScriptSig(Script scriptPubKey, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
public Script getMultisigScriptSig(PolicyType policyType, Script scriptPubKey, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
throw new ProtocolException(getName() + " is not a multisig script type");
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransactionInput addMultisigSpendingInput(Transaction transaction, TransactionOutput prevOutput, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
public TransactionInput addMultisigSpendingInput(PolicyType policyType, 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);
|
||||
return List.of(SINGLE_HD);
|
||||
}
|
||||
},
|
||||
P2WSH("P2WSH", "Native Segwit (P2WSH)", "m/48'/0'/0'/2'") {
|
||||
@ -906,7 +906,7 @@ public enum ScriptType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Address getAddress(ECKey key) {
|
||||
public Address getAddress(PolicyType policyType, 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(ECKey key) {
|
||||
public Script getOutputScript(PolicyType policyType, 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(Script scriptPubKey, ECKey pubKey, TransactionSignature signature) {
|
||||
public Script getScriptSig(PolicyType policyType, Script scriptPubKey, ECKey pubKey, TransactionSignature signature) {
|
||||
throw new ProtocolException("Only multisig scriptSigs supported for " + getName() + " scriptPubKeys");
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransactionInput addSpendingInput(Transaction transaction, TransactionOutput prevOutput, ECKey pubKey, TransactionSignature signature) {
|
||||
public TransactionInput addSpendingInput(PolicyType policyType, Transaction transaction, TransactionOutput prevOutput, ECKey pubKey, TransactionSignature signature) {
|
||||
throw new ProtocolException("Only multisig scriptSigs supported for " + getName() + " scriptPubKeys");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Script getMultisigScriptSig(Script scriptPubKey, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
public Script getMultisigScriptSig(PolicyType policyType, 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(Transaction transaction, TransactionOutput prevOutput, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
Script scriptSig = getMultisigScriptSig(prevOutput.getScript(), threshold, pubKeySignatures);
|
||||
public TransactionInput addMultisigSpendingInput(PolicyType policyType, Transaction transaction, TransactionOutput prevOutput, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
Script scriptSig = getMultisigScriptSig(policyType, 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, CUSTOM);
|
||||
return List.of(MULTI_HD);
|
||||
}
|
||||
},
|
||||
P2TR("P2TR", "Taproot (P2TR)", "m/86'/0'/0'") {
|
||||
@Override
|
||||
public ECKey getOutputKey(ECKey derivedKey) {
|
||||
return derivedKey.getTweakedOutputKey();
|
||||
public ECKey getOutputKey(PolicyType policyType, ECKey derivedKey) {
|
||||
return policyType == SINGLE_SP ? derivedKey : derivedKey.getTweakedOutputKey();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -1027,8 +1027,8 @@ public enum ScriptType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Address getAddress(ECKey derivedKey) {
|
||||
return getAddress(getOutputKey(derivedKey).getPubKeyXCoord());
|
||||
public Address getAddress(PolicyType policyType, ECKey derivedKey) {
|
||||
return getAddress(getOutputKey(policyType, derivedKey).getPubKeyXCoord());
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -1046,8 +1046,8 @@ public enum ScriptType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Script getOutputScript(ECKey derivedKey) {
|
||||
return getOutputScript(getOutputKey(derivedKey).getPubKeyXCoord());
|
||||
public Script getOutputScript(PolicyType policyType, ECKey derivedKey) {
|
||||
return getOutputScript(getOutputKey(policyType, derivedKey).getPubKeyXCoord());
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -1096,12 +1096,12 @@ public enum ScriptType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Script getScriptSig(Script scriptPubKey, ECKey pubKey, TransactionSignature signature) {
|
||||
public Script getScriptSig(PolicyType policyType, Script scriptPubKey, ECKey pubKey, TransactionSignature signature) {
|
||||
if(!isScriptType(scriptPubKey)) {
|
||||
throw new ProtocolException("Provided scriptPubKey is not a " + getName() + " script");
|
||||
}
|
||||
|
||||
if(!scriptPubKey.equals(getOutputScript(pubKey))) {
|
||||
if(!scriptPubKey.equals(getOutputScript(policyType, pubKey))) {
|
||||
throw new ProtocolException("Provided P2TR scriptPubKey does not match constructed pubkey script");
|
||||
}
|
||||
|
||||
@ -1109,19 +1109,19 @@ public enum ScriptType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransactionInput addSpendingInput(Transaction transaction, TransactionOutput prevOutput, ECKey pubKey, TransactionSignature signature) {
|
||||
Script scriptSig = getScriptSig(prevOutput.getScript(), pubKey, signature);
|
||||
public TransactionInput addSpendingInput(PolicyType policyType, Transaction transaction, TransactionOutput prevOutput, ECKey pubKey, TransactionSignature signature) {
|
||||
Script scriptSig = getScriptSig(policyType, prevOutput.getScript(), pubKey, signature);
|
||||
TransactionWitness witness = new TransactionWitness(transaction, signature);
|
||||
return transaction.addInput(prevOutput.getHash(), prevOutput.getIndex(), scriptSig, witness);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Script getMultisigScriptSig(Script scriptPubKey, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
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(Transaction transaction, TransactionOutput prevOutput, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||
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");
|
||||
}
|
||||
|
||||
@ -1132,7 +1132,123 @@ public enum ScriptType {
|
||||
|
||||
@Override
|
||||
public List<PolicyType> getAllowedPolicyTypes() {
|
||||
return List.of(SINGLE);
|
||||
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();
|
||||
}
|
||||
};
|
||||
|
||||
@ -1154,6 +1270,10 @@ 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;
|
||||
}
|
||||
@ -1214,19 +1334,19 @@ public enum ScriptType {
|
||||
return getAllowedPolicyTypes().contains(policyType);
|
||||
}
|
||||
|
||||
public ECKey getOutputKey(ECKey derivedKey) {
|
||||
public ECKey getOutputKey(PolicyType policyType, ECKey derivedKey) {
|
||||
return derivedKey;
|
||||
}
|
||||
|
||||
public abstract Address getAddress(byte[] bytes);
|
||||
|
||||
public abstract Address getAddress(ECKey key);
|
||||
public abstract Address getAddress(PolicyType policyType, ECKey key);
|
||||
|
||||
public abstract Address getAddress(Script script);
|
||||
|
||||
public abstract Script getOutputScript(byte[] bytes);
|
||||
|
||||
public abstract Script getOutputScript(ECKey key);
|
||||
public abstract Script getOutputScript(PolicyType policyType, ECKey key);
|
||||
|
||||
public abstract Script getOutputScript(Script script);
|
||||
|
||||
@ -1246,6 +1366,10 @@ 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) {
|
||||
@ -1264,13 +1388,13 @@ public enum ScriptType {
|
||||
throw new ProtocolException("Script type " + this + " is not a multisig script");
|
||||
}
|
||||
|
||||
public abstract Script getScriptSig(Script scriptPubKey, ECKey pubKey, TransactionSignature signature);
|
||||
public abstract Script getScriptSig(PolicyType policyType, Script scriptPubKey, ECKey pubKey, TransactionSignature signature);
|
||||
|
||||
public abstract TransactionInput addSpendingInput(Transaction transaction, TransactionOutput prevOutput, ECKey pubKey, TransactionSignature signature);
|
||||
public abstract TransactionInput addSpendingInput(PolicyType policyType, Transaction transaction, TransactionOutput prevOutput, ECKey pubKey, TransactionSignature signature);
|
||||
|
||||
public abstract Script getMultisigScriptSig(Script scriptPubKey, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures);
|
||||
public abstract Script getMultisigScriptSig(PolicyType policyType, Script scriptPubKey, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures);
|
||||
|
||||
public abstract TransactionInput addMultisigSpendingInput(Transaction transaction, TransactionOutput prevOutput, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures);
|
||||
public abstract TransactionInput addMultisigSpendingInput(PolicyType policyType, Transaction transaction, TransactionOutput prevOutput, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures);
|
||||
|
||||
public abstract TransactionSignature.Type getSignatureType();
|
||||
|
||||
@ -1278,11 +1402,13 @@ 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};
|
||||
public static final ScriptType[] ADDRESSABLE_TYPES = {P2PKH, P2SH, P2SH_P2WPKH, P2SH_P2WSH, P2WPKH, P2WSH, P2TR, P2A};
|
||||
|
||||
public static final ScriptType[] NON_WITNESS_TYPES = {P2PK, P2PKH, P2SH};
|
||||
|
||||
public static final ScriptType[] WITNESS_TYPES = {P2SH_P2WPKH, P2SH_P2WSH, P2WPKH, P2WSH, P2TR};
|
||||
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 List<ScriptType> getScriptTypesForPolicyType(PolicyType policyType) {
|
||||
return Arrays.stream(values()).filter(scriptType -> scriptType.isAllowed(policyType)).collect(Collectors.toList());
|
||||
@ -1360,6 +1486,8 @@ 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,6 +3,7 @@ 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;
|
||||
@ -14,7 +15,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 = 1000 * 1000;
|
||||
public static final int MAX_BLOCK_SIZE_VBYTES = 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;
|
||||
@ -53,6 +54,10 @@ public class Transaction extends ChildMessage {
|
||||
super(rawtx, 0);
|
||||
}
|
||||
|
||||
public Transaction(byte[] blockdata, int offset) {
|
||||
super(blockdata, offset);
|
||||
}
|
||||
|
||||
public long getVersion() {
|
||||
return version;
|
||||
}
|
||||
@ -99,8 +104,8 @@ public class Transaction extends ChildMessage {
|
||||
}
|
||||
|
||||
public Sha256Hash getTxId() {
|
||||
if (cachedTxId == null) {
|
||||
if (!hasWitnesses() && cachedWTxId != null) {
|
||||
if(cachedTxId == null) {
|
||||
if(!isSegwit() && cachedWTxId != null) {
|
||||
cachedTxId = cachedWTxId;
|
||||
} else {
|
||||
cachedTxId = calculateTxId(false);
|
||||
@ -110,11 +115,11 @@ public class Transaction extends ChildMessage {
|
||||
}
|
||||
|
||||
public Sha256Hash getWTxId() {
|
||||
if (cachedWTxId == null) {
|
||||
if (!hasWitnesses() && cachedTxId != null) {
|
||||
if(cachedWTxId == null) {
|
||||
if(!isSegwit() && cachedTxId != null) {
|
||||
cachedWTxId = cachedTxId;
|
||||
} else {
|
||||
cachedWTxId = calculateTxId(true);
|
||||
cachedWTxId = calculateTxId(isSegwit());
|
||||
}
|
||||
}
|
||||
return cachedWTxId;
|
||||
@ -369,8 +374,8 @@ public class Transaction extends ChildMessage {
|
||||
return Collections.unmodifiableList(outputs);
|
||||
}
|
||||
|
||||
public void shuffleOutputs() {
|
||||
Collections.shuffle(outputs);
|
||||
public void swapOutputs(int i, int j) {
|
||||
Collections.swap(outputs, i, j);
|
||||
}
|
||||
|
||||
public TransactionOutput addOutput(long value, Script script) {
|
||||
@ -382,7 +387,7 @@ public class Transaction extends ChildMessage {
|
||||
}
|
||||
|
||||
public TransactionOutput addOutput(long value, ECKey pubkey) {
|
||||
return addOutput(new TransactionOutput(this, value, ScriptType.P2PK.getOutputScript(pubkey)));
|
||||
return addOutput(new TransactionOutput(this, value, ScriptType.P2PK.getOutputScript(PolicyType.SINGLE_HD, pubkey)));
|
||||
}
|
||||
|
||||
public TransactionOutput addOutput(TransactionOutput output) {
|
||||
@ -395,7 +400,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)
|
||||
if (this.getMessageSize() > (MAX_BLOCK_SIZE_VBYTES * WITNESS_SCALE_FACTOR))
|
||||
throw new VerificationException.LargerThanMaxBlockSize();
|
||||
|
||||
HashSet<TransactionOutPoint> outpoints = new HashSet<>();
|
||||
@ -437,11 +442,18 @@ public class Transaction extends ChildMessage {
|
||||
|
||||
public static boolean isTransaction(byte[] bytes) {
|
||||
//Incomplete quick test
|
||||
if(bytes.length == 0) {
|
||||
if(bytes.length <= 5 || bytes.length > (MAX_BLOCK_SIZE_VBYTES * WITNESS_SCALE_FACTOR)) {
|
||||
return false;
|
||||
}
|
||||
long version = Utils.readUint32(bytes, 0);
|
||||
return version > 0 && version < 5;
|
||||
if(version <= 0) {
|
||||
return false;
|
||||
}
|
||||
boolean segwit = (bytes[4] == 0);
|
||||
if(segwit && bytes[5] == 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public Sha256Hash hashForLegacySignature(int inputIndex, Script redeemScript, SigHash sigHash) {
|
||||
|
||||
@ -7,6 +7,7 @@ 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,6 +60,20 @@ 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,12 +89,8 @@ 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);
|
||||
if(push.length == 1 && push[0] == 0) {
|
||||
length++;
|
||||
} else {
|
||||
length += new VarInt(push.length).getSizeInBytes();
|
||||
length += push.length;
|
||||
}
|
||||
length += new VarInt(push.length).getSizeInBytes();
|
||||
length += push.length;
|
||||
}
|
||||
|
||||
return length;
|
||||
@ -104,12 +100,8 @@ 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);
|
||||
if(push.length == 1 && push[0] == 0) {
|
||||
stream.write(push);
|
||||
} else {
|
||||
stream.write(new VarInt(push.length).encode());
|
||||
stream.write(push);
|
||||
}
|
||||
stream.write(new VarInt(push.length).encode());
|
||||
stream.write(push);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -9,10 +9,8 @@ import com.sparrowwallet.drongo.protocol.VarInt;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
|
||||
public class PSBTEntry {
|
||||
private final byte[] key;
|
||||
@ -27,7 +25,7 @@ public class PSBTEntry {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
PSBTEntry(ByteBuffer psbtByteBuffer) throws PSBTParseException {
|
||||
public PSBTEntry(ByteBuffer psbtByteBuffer) throws PSBTParseException {
|
||||
int keyLen = readCompactInt(psbtByteBuffer);
|
||||
|
||||
if (keyLen == 0x00) {
|
||||
@ -79,6 +77,9 @@ 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");
|
||||
}
|
||||
@ -144,6 +145,39 @@ 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));
|
||||
@ -259,4 +293,16 @@ 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,6 +4,7 @@ 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;
|
||||
|
||||
@ -26,10 +27,23 @@ 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_PROPRIETARY = (byte)0xfc;
|
||||
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_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;
|
||||
@ -42,24 +56,38 @@ 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;
|
||||
|
||||
private final Transaction transaction;
|
||||
//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 int index;
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(PSBTInput.class);
|
||||
|
||||
PSBTInput(PSBT psbt, Transaction transaction, int index) {
|
||||
PSBTInput(PSBT psbt, int index) {
|
||||
this.psbt = psbt;
|
||||
this.transaction = transaction;
|
||||
this.index = 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);
|
||||
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);
|
||||
|
||||
if(Arrays.asList(ScriptType.WITNESS_TYPES).contains(scriptType)) {
|
||||
this.witnessUtxo = utxo.getOutputs().get(utxoIndex);
|
||||
@ -68,7 +96,7 @@ public class PSBTInput {
|
||||
}
|
||||
|
||||
if(alwaysAddNonWitnessTx) {
|
||||
//Add non-witness UTXO to segwit types to handle Trezor, Bitbox and Ledger requirements
|
||||
//Add non-witness UTXO to segwit v0 types to handle Trezor, Bitbox and Ledger requirements
|
||||
this.nonWitnessUtxo = utxo;
|
||||
}
|
||||
|
||||
@ -88,23 +116,42 @@ public class PSBTInput {
|
||||
tapDerivedPublicKeys.put(this.tapInternalKey, Map.of(tapKeyDerivation, Collections.emptyList()));
|
||||
}
|
||||
|
||||
this.sigHash = getDefaultSigHash();
|
||||
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);
|
||||
}
|
||||
|
||||
PSBTInput(PSBT psbt, List<PSBTEntry> inputEntries, Transaction transaction, int index) throws PSBTParseException {
|
||||
this.psbt = psbt;
|
||||
for(PSBTEntry entry : inputEntries) {
|
||||
switch(entry.getKeyType()) {
|
||||
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()) {
|
||||
case PSBT_IN_NON_WITNESS_UTXO:
|
||||
entry.checkOneByteKey();
|
||||
Transaction nonWitnessTx = new Transaction(entry.getData());
|
||||
nonWitnessTx.verify();
|
||||
Sha256Hash inputHash = nonWitnessTx.calculateTxId(false);
|
||||
Sha256Hash outpointHash = transaction.getInputs().get(index).getOutpoint().getHash();
|
||||
Sha256Hash outpointHash = getPrevTxid();
|
||||
if(outpointHash == null) {
|
||||
throw new PSBTParseException("Outpoint hash not present for input " + index);
|
||||
}
|
||||
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()) {
|
||||
@ -141,6 +188,9 @@ 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;
|
||||
@ -151,7 +201,11 @@ public class PSBTInput {
|
||||
Script redeemScript = new Script(entry.getData());
|
||||
Script scriptPubKey = null;
|
||||
if(this.nonWitnessUtxo != null) {
|
||||
scriptPubKey = this.nonWitnessUtxo.getOutputs().get((int)transaction.getInputs().get(index).getOutpoint().getIndex()).getScript();
|
||||
Long prevIndex = getPrevIndex();
|
||||
if(prevIndex == null) {
|
||||
throw new PSBTParseException("Outpoint index not present for input " + index);
|
||||
}
|
||||
scriptPubKey = this.nonWitnessUtxo.getOutputs().get(prevIndex.intValue()).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
|
||||
@ -214,6 +268,118 @@ 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()));
|
||||
@ -245,12 +411,9 @@ public class PSBTInput {
|
||||
log.warn("PSBT input not recognized key type: " + entry.getKeyType());
|
||||
}
|
||||
}
|
||||
|
||||
this.transaction = transaction;
|
||||
this.index = index;
|
||||
}
|
||||
|
||||
public List<PSBTEntry> getInputEntries() {
|
||||
public List<PSBTEntry> getInputEntries(int psbtVersion) {
|
||||
List<PSBTEntry> entries = new ArrayList<>();
|
||||
|
||||
if(nonWitnessUtxo != null) {
|
||||
@ -296,6 +459,44 @@ 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())));
|
||||
}
|
||||
@ -346,6 +547,51 @@ 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) {
|
||||
@ -481,6 +727,122 @@ 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;
|
||||
@ -518,6 +880,26 @@ 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
|
||||
@ -541,6 +923,12 @@ 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);
|
||||
@ -559,6 +947,27 @@ 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) {
|
||||
@ -633,6 +1042,8 @@ 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);
|
||||
@ -676,7 +1087,7 @@ public class PSBTInput {
|
||||
}
|
||||
|
||||
public TransactionInput getInput() {
|
||||
return transaction.getInputs().get(index);
|
||||
return psbt.getTransaction().getInputs().get(index);
|
||||
}
|
||||
|
||||
public TransactionOutput getUtxo() {
|
||||
@ -684,6 +1095,10 @@ 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;
|
||||
}
|
||||
@ -697,6 +1112,10 @@ public class PSBTInput {
|
||||
proprietary.clear();
|
||||
tapDerivedPublicKeys.clear();
|
||||
tapKeyPathSignature = null;
|
||||
silentPaymentsEcdhShares.clear();
|
||||
silentPaymentsDLEQProofs.clear();
|
||||
silentPaymentsSpendDerivations.clear();
|
||||
silentPaymentsTweak = null;
|
||||
}
|
||||
|
||||
private Sha256Hash getHashForSignature(Script connectedScript, SigHash localSigHash) {
|
||||
@ -705,12 +1124,12 @@ public class PSBTInput {
|
||||
ScriptType scriptType = getScriptType();
|
||||
if(scriptType == ScriptType.P2TR) {
|
||||
List<TransactionOutput> spentUtxos = psbt.getPsbtInputs().stream().map(PSBTInput::getUtxo).collect(Collectors.toList());
|
||||
hash = transaction.hashForTaprootSignature(spentUtxos, index, !P2TR.isScriptType(connectedScript), connectedScript, localSigHash, null);
|
||||
hash = psbt.getTransaction().hashForTaprootSignature(spentUtxos, index, !P2TR.isScriptType(connectedScript), connectedScript, localSigHash, null);
|
||||
} else if(Arrays.asList(WITNESS_TYPES).contains(scriptType)) {
|
||||
long prevValue = getUtxo().getValue();
|
||||
hash = transaction.hashForWitnessSignature(index, connectedScript, prevValue, localSigHash);
|
||||
hash = psbt.getTransaction().hashForWitnessSignature(index, connectedScript, prevValue, localSigHash);
|
||||
} else {
|
||||
hash = transaction.hashForLegacySignature(index, connectedScript, localSigHash);
|
||||
hash = psbt.getTransaction().hashForLegacySignature(index, connectedScript, localSigHash);
|
||||
}
|
||||
|
||||
return hash;
|
||||
|
||||
@ -3,13 +3,18 @@ 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.protocol.Script;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
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 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.*;
|
||||
@ -18,8 +23,13 @@ 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;
|
||||
@ -28,14 +38,28 @@ 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);
|
||||
|
||||
PSBTOutput() {
|
||||
//empty constructor
|
||||
private final PSBT psbt;
|
||||
private int index;
|
||||
|
||||
PSBTOutput(PSBT psbt, int index) {
|
||||
this.psbt = psbt;
|
||||
this.index = index;
|
||||
}
|
||||
|
||||
PSBTOutput(ScriptType scriptType, Script redeemScript, Script witnessScript, Map<ECKey, KeyDerivation> derivedPublicKeys, Map<String, String> proprietary, ECKey tapInternalKey) {
|
||||
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);
|
||||
|
||||
this.redeemScript = redeemScript;
|
||||
this.witnessScript = witnessScript;
|
||||
|
||||
@ -51,11 +75,24 @@ 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(List<PSBTEntry> outputEntries) throws PSBTParseException {
|
||||
PSBTOutput(PSBT psbt, List<PSBTEntry> outputEntries, int index) throws PSBTParseException {
|
||||
this(psbt, index);
|
||||
for(PSBTEntry entry : outputEntries) {
|
||||
switch (entry.getKeyType()) {
|
||||
switch((byte)entry.getKeyType()) {
|
||||
case PSBT_OUT_REDEEM_SCRIPT:
|
||||
entry.checkOneByteKey();
|
||||
Script redeemScript = new Script(entry.getData());
|
||||
@ -75,6 +112,20 @@ 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()));
|
||||
@ -97,13 +148,37 @@ 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() {
|
||||
public List<PSBTEntry> getOutputEntries(int psbtVersion) {
|
||||
List<PSBTEntry> entries = new ArrayList<>();
|
||||
|
||||
if(redeemScript != null) {
|
||||
@ -118,6 +193,25 @@ 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())));
|
||||
}
|
||||
@ -132,6 +226,10 @@ 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;
|
||||
}
|
||||
|
||||
@ -145,13 +243,30 @@ public class PSBTOutput {
|
||||
}
|
||||
|
||||
derivedPublicKeys.putAll(psbtOutput.derivedPublicKeys);
|
||||
proprietary.putAll(psbtOutput.proprietary);
|
||||
|
||||
if(psbtOutput.amount != null) {
|
||||
amount = psbtOutput.amount;
|
||||
}
|
||||
|
||||
if(psbtOutput.script != null) {
|
||||
script = psbtOutput.script;
|
||||
}
|
||||
|
||||
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() {
|
||||
@ -178,6 +293,38 @@ 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;
|
||||
}
|
||||
@ -198,6 +345,48 @@ 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();
|
||||
}
|
||||
|
||||
@ -0,0 +1,19 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,52 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,110 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,127 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
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) {}
|
||||
@ -0,0 +1,513 @@
|
||||
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) {}
|
||||
}
|
||||
@ -0,0 +1,150 @@
|
||||
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,7 +1,11 @@
|
||||
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;
|
||||
@ -66,6 +70,7 @@ 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";
|
||||
@ -75,6 +80,8 @@ 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
|
||||
*/
|
||||
@ -135,9 +142,7 @@ public class BitcoinURI {
|
||||
}
|
||||
}
|
||||
|
||||
if(addressToken.isEmpty() && getPaymentRequestUrl() == null) {
|
||||
throw new BitcoinURIParseException("No address and no r= parameter found");
|
||||
}
|
||||
this.uriString = input;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -271,10 +276,10 @@ public class BitcoinURI {
|
||||
if(payjoinUrl != null) {
|
||||
try {
|
||||
URI uri = new URI(payjoinUrl);
|
||||
if(uri.getScheme().equals("https") || uri.getHost().endsWith(".onion")) {
|
||||
if(Utils.isSecureUrl(uri)) {
|
||||
return uri;
|
||||
} else {
|
||||
log.error("Insecure payjoin URL provided, must be https or .onion: " + payjoinUrl);
|
||||
log.error("Insecure payjoin URL provided, must be https or http .onion: " + payjoinUrl);
|
||||
}
|
||||
} catch(URISyntaxException e) {
|
||||
log.error("Invalid payjoin URL provided", e);
|
||||
@ -291,6 +296,22 @@ 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
|
||||
@ -315,8 +336,16 @@ 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,8 +10,6 @@ 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
|
||||
@ -67,7 +65,7 @@ public class FinalizingPSBTWallet extends Wallet {
|
||||
setGapLimit(0);
|
||||
purposeNode.setChildren(new TreeSet<>());
|
||||
|
||||
setPolicyType(numSignatures == 1 ? PolicyType.SINGLE : PolicyType.MULTI);
|
||||
setPolicyType(numSignatures == 1 ? PolicyType.SINGLE_HD : PolicyType.MULTI_HD);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -100,6 +98,11 @@ 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,12 +9,11 @@ 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.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
|
||||
public class Keystore extends Persistable {
|
||||
private static final Logger log = LoggerFactory.getLogger(Keystore.class);
|
||||
@ -28,6 +27,8 @@ 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;
|
||||
|
||||
@ -36,6 +37,7 @@ 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);
|
||||
@ -50,6 +52,9 @@ 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*$", "");
|
||||
}
|
||||
|
||||
@ -102,6 +107,23 @@ 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;
|
||||
}
|
||||
@ -191,14 +213,34 @@ 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));
|
||||
//Recreate from xprv string to reset path to single ChildNumber at the derived depth
|
||||
return ExtendedKey.fromDescriptor(xprv.toString());
|
||||
if(resetPathToDerivedDepth) {
|
||||
//Recreate from xprv string to reset path to single ChildNumber at the derived depth
|
||||
return ExtendedKey.fromDescriptor(xprv.toString());
|
||||
} else {
|
||||
return xprv;
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -222,6 +264,19 @@ 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());
|
||||
@ -244,7 +299,7 @@ public class Keystore extends Persistable {
|
||||
}
|
||||
|
||||
public ECKey getPubKeyForDerivation(KeyDerivation keyDerivation) {
|
||||
if(keyDerivation != null) {
|
||||
if(keyDerivation != null && extendedPublicKey != null) {
|
||||
List<ChildNumber> derivation = keyDerivation.getDerivation();
|
||||
String fingerprint = Utils.bytesToHex(this.extendedPublicKey.getKey().getFingerprint());
|
||||
if(derivation.size() > this.keyDerivation.getDerivation().size()) {
|
||||
@ -263,6 +318,33 @@ 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();
|
||||
@ -290,8 +372,8 @@ public class Keystore extends Persistable {
|
||||
throw new InvalidKeystoreException("No key derivation specified");
|
||||
}
|
||||
|
||||
if(extendedPublicKey == null) {
|
||||
throw new InvalidKeystoreException("No extended public key specified");
|
||||
if(extendedPublicKey == null && silentPaymentScanAddress == null) {
|
||||
throw new InvalidKeystoreException("No extended public key or silent payment scan address specified");
|
||||
}
|
||||
|
||||
if(label.isEmpty()) {
|
||||
@ -315,7 +397,7 @@ public class Keystore extends Persistable {
|
||||
throw new InvalidKeystoreException("Source of " + source + " but no seed or master private key is present");
|
||||
}
|
||||
|
||||
if(!extendedPublicKeyChecked && ((seed != null && !seed.isEncrypted()) || (masterPrivateExtendedKey != null && !masterPrivateExtendedKey.isEncrypted()))) {
|
||||
if(!extendedPublicKeyChecked && extendedPublicKey != null && ((seed != null && !seed.isEncrypted()) || (masterPrivateExtendedKey != null && !masterPrivateExtendedKey.isEncrypted()))) {
|
||||
try {
|
||||
List<ChildNumber> derivation = getKeyDerivation().getDerivation();
|
||||
DeterministicKey derivedKey = getExtendedMasterPrivateKey().getKey(derivation);
|
||||
@ -329,6 +411,21 @@ 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) {
|
||||
@ -365,43 +462,54 @@ 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, List<ChildNumber> derivation) throws MnemonicException {
|
||||
public static Keystore fromSeed(DeterministicSeed seed, PolicyType policyType, List<ChildNumber> derivation) throws MnemonicException {
|
||||
Keystore keystore = new Keystore();
|
||||
keystore.setSeed(seed);
|
||||
keystore.setLabel(seed.getType().name());
|
||||
rederiveKeystoreFromMaster(keystore, derivation);
|
||||
rederiveKeystoreFromMaster(keystore, policyType, derivation);
|
||||
return keystore;
|
||||
}
|
||||
|
||||
public static Keystore fromMasterPrivateExtendedKey(MasterPrivateExtendedKey masterPrivateExtendedKey, List<ChildNumber> derivation) throws MnemonicException {
|
||||
public static Keystore fromMasterPrivateExtendedKey(MasterPrivateExtendedKey masterPrivateExtendedKey, PolicyType policyType, List<ChildNumber> derivation) throws MnemonicException {
|
||||
Keystore keystore = new Keystore();
|
||||
keystore.setMasterPrivateExtendedKey(masterPrivateExtendedKey);
|
||||
keystore.setLabel("Master Key");
|
||||
rederiveKeystoreFromMaster(keystore, derivation);
|
||||
rederiveKeystoreFromMaster(keystore, policyType, derivation);
|
||||
return keystore;
|
||||
}
|
||||
|
||||
private static void rederiveKeystoreFromMaster(Keystore keystore, List<ChildNumber> derivation) throws MnemonicException {
|
||||
private static void rederiveKeystoreFromMaster(Keystore keystore, PolicyType policyType, 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()));
|
||||
|
||||
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()));
|
||||
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()));
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isEncrypted() {
|
||||
|
||||
@ -24,6 +24,12 @@ 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,6 +1,13 @@
|
||||
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;
|
||||
@ -15,10 +22,10 @@ public class Payment {
|
||||
|
||||
public Payment(Address address, String label, long amount, boolean sendMax, Type type) {
|
||||
this.address = address;
|
||||
this.label = label;
|
||||
this.label = label == null && address instanceof P2AAddress ? address.getOutputScriptDataType() : label;
|
||||
this.amount = amount;
|
||||
this.sendMax = sendMax;
|
||||
this.type = type;
|
||||
this.type = type == Type.DEFAULT && address instanceof P2AAddress ? Type.ANCHOR : type;
|
||||
}
|
||||
|
||||
public Address getAddress() {
|
||||
@ -62,6 +69,26 @@ public class Payment {
|
||||
}
|
||||
|
||||
public enum Type {
|
||||
DEFAULT, WHIRLPOOL_FEE, FAKE_MIX, MIX;
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
package com.sparrowwallet.drongo.wallet;
|
||||
|
||||
public class Persistable {
|
||||
public static final int MAX_LABEL_LENGTH = 255;
|
||||
|
||||
private Long id;
|
||||
|
||||
public Long getId() {
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
package com.sparrowwallet.drongo.wallet;
|
||||
|
||||
public enum SortDirection {
|
||||
ASCENDING, DESCENDING
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
package com.sparrowwallet.drongo.wallet;
|
||||
|
||||
public enum TableType {
|
||||
TRANSACTIONS, UTXOS, RECEIVE_ADDRESSES, CHANGE_ADDRESSES, SEARCH_WALLET, WALLET_SUMMARY
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
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,18 +5,19 @@ 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;
|
||||
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;
|
||||
|
||||
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) {
|
||||
if(this == TREZOR_1 || this == TREZOR_T || this == TREZOR_SAFE_3 || this == TREZOR_SAFE_5 || this == TREZOR_SAFE_7) {
|
||||
return "trezor";
|
||||
}
|
||||
|
||||
if(this == LEDGER_NANO_S || this == LEDGER_NANO_X || this == LEDGER_NANO_S_PLUS) {
|
||||
if(this == LEDGER_NANO_S || this == LEDGER_NANO_X || this == LEDGER_NANO_S_PLUS || this == LEDGER_STAX || this == LEDGER_FLEX || this == LEDGER_NANO_GEN5) {
|
||||
return "ledger";
|
||||
}
|
||||
|
||||
@ -52,11 +53,20 @@ 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) {
|
||||
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) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -64,20 +74,20 @@ public enum WalletModel {
|
||||
}
|
||||
|
||||
public boolean requiresPinPrompt() {
|
||||
return (this == TREZOR_1 || this == KEEPKEY);
|
||||
return (this == TREZOR_1 || this == KEEPKEY || this == ONEKEY_CLASSIC_1S);
|
||||
}
|
||||
|
||||
public boolean externalPassphraseEntry() {
|
||||
return (this == TREZOR_1 || this == KEEPKEY);
|
||||
return (this == TREZOR_1 || this == KEEPKEY || this == ONEKEY_CLASSIC_1S);
|
||||
}
|
||||
|
||||
public boolean isCard() {
|
||||
return (this == TAPSIGNER || this == SATSCHIP || this == SATSCARD || this == SATOCHIP);
|
||||
return (this == TAPSIGNER || this == SATSCHIP || this == SATSCARD || this == SATOCHIP || this == KEYCARD);
|
||||
}
|
||||
|
||||
public boolean hasUsb() {
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
public int getMinPinLength() {
|
||||
@ -89,7 +99,9 @@ public enum WalletModel {
|
||||
}
|
||||
|
||||
public int getMaxPinLength() {
|
||||
if(this == SATOCHIP) {
|
||||
if(this == KEYCARD) {
|
||||
return 6;
|
||||
} else if(this == SATOCHIP) {
|
||||
return 16;
|
||||
} else {
|
||||
return 32;
|
||||
@ -104,8 +116,16 @@ public enum WalletModel {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasZeroInPin() {
|
||||
if(this == ONEKEY_CLASSIC_1S) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean requiresSeedInitialization() {
|
||||
if(this == SATOCHIP) {
|
||||
if(this == SATOCHIP || this == KEYCARD) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
@ -113,7 +133,7 @@ public enum WalletModel {
|
||||
}
|
||||
|
||||
public boolean supportsBackup() {
|
||||
if(this == SATOCHIP || this == SATSCHIP) {
|
||||
if(this == SATOCHIP || this == SATSCHIP || this == KEYCARD) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
@ -161,10 +181,20 @@ 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));
|
||||
builder.append(" ");
|
||||
if(this != BLUE_WALLET) {
|
||||
builder.append(" ");
|
||||
}
|
||||
}
|
||||
|
||||
return builder.toString().trim();
|
||||
|
||||
@ -5,6 +5,7 @@ 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.*;
|
||||
@ -14,6 +15,7 @@ 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<>();
|
||||
|
||||
@ -98,6 +100,10 @@ public class WalletNode extends Persistable implements Comparable<WalletNode> {
|
||||
return derivation;
|
||||
}
|
||||
|
||||
public boolean isPurposeNode() {
|
||||
return getDerivation().size() == 1;
|
||||
}
|
||||
|
||||
public String getLabel() {
|
||||
return label;
|
||||
}
|
||||
@ -106,6 +112,14 @@ 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;
|
||||
@ -220,6 +234,10 @@ 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()) {
|
||||
@ -245,6 +263,25 @@ 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
|
||||
*/
|
||||
@ -353,6 +390,7 @@ 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));
|
||||
|
||||
@ -0,0 +1,18 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,61 @@
|
||||
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,10 +1,15 @@
|
||||
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.
|
||||
@ -21,17 +26,19 @@ public class WalletTransaction {
|
||||
private final Map<Sha256Hash, BlockTransaction> inputTransactions;
|
||||
private final List<Output> outputs;
|
||||
|
||||
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, long fee) {
|
||||
this(wallet, transaction, utxoSelectors, selectedUtxoSets, payments, outputs, Collections.emptyMap(), fee);
|
||||
}
|
||||
|
||||
public WalletTransaction(Wallet wallet, Transaction transaction, List<UtxoSelector> utxoSelectors, List<Map<BlockTransactionHashIndex, WalletNode>> selectedUtxoSets, List<Payment> payments, Map<WalletNode, Long> changeMap, long fee) {
|
||||
this(wallet, transaction, utxoSelectors, selectedUtxoSets, payments, changeMap, fee, Collections.emptyMap());
|
||||
public WalletTransaction(Wallet wallet, Transaction transaction, List<UtxoSelector> utxoSelectors, List<Map<BlockTransactionHashIndex, WalletNode>> selectedUtxoSets, List<Payment> payments, 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, Map<Sha256Hash, BlockTransaction> inputTransactions) {
|
||||
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");
|
||||
}
|
||||
|
||||
this.wallet = wallet;
|
||||
this.transaction = transaction;
|
||||
this.utxoSelectors = utxoSelectors;
|
||||
@ -40,7 +47,7 @@ public class WalletTransaction {
|
||||
this.changeMap = changeMap;
|
||||
this.fee = fee;
|
||||
this.inputTransactions = inputTransactions;
|
||||
this.outputs = calculateOutputs();
|
||||
this.outputs = outputs;
|
||||
|
||||
for(Payment payment : payments) {
|
||||
payment.setLabel(getOutputLabel(payment));
|
||||
@ -110,7 +117,7 @@ public class WalletTransaction {
|
||||
}
|
||||
|
||||
public List<Output> getOutputs() {
|
||||
return outputs;
|
||||
return Collections.unmodifiableList(outputs);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -129,10 +136,6 @@ 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);
|
||||
}
|
||||
@ -159,7 +162,7 @@ public class WalletTransaction {
|
||||
return false;
|
||||
}
|
||||
|
||||
return getAddressNodeMap(wallet).get(payment.getAddress()) != null;
|
||||
return wallet.getWalletAddresses().get(payment.getAddress()) != null;
|
||||
}
|
||||
|
||||
private String getOutputLabel(Payment payment) {
|
||||
@ -168,7 +171,7 @@ public class WalletTransaction {
|
||||
}
|
||||
|
||||
if(payment.getType() == Payment.Type.WHIRLPOOL_FEE) {
|
||||
return "Whirlpool fee";
|
||||
return payment.getType().toDisplayString();
|
||||
} else if(isPremixSend(payment)) {
|
||||
int premixIndex = getOutputIndex(payment.getAddress(), payment.getAmount(), Collections.emptySet()) - 2;
|
||||
return "Premix #" + premixIndex;
|
||||
@ -189,9 +192,14 @@ public class WalletTransaction {
|
||||
public Wallet getToWallet(Collection<Wallet> wallets, Payment payment) {
|
||||
for(Wallet openWallet : wallets) {
|
||||
if(openWallet != getWallet() && openWallet.isValid()) {
|
||||
WalletNode addressNode = openWallet.getWalletAddresses().get(payment.getAddress());
|
||||
if(addressNode != null) {
|
||||
return addressNode.getWallet();
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -200,63 +208,17 @@ public class WalletTransaction {
|
||||
}
|
||||
|
||||
public boolean isDuplicateAddress(Payment payment) {
|
||||
return getPayments().stream().filter(p -> payment != p).anyMatch(p -> payment.getAddress() != null && payment.getAddress().equals(p.getAddress()));
|
||||
return getPayments().stream().filter(p -> payment != p && !(payment instanceof SilentPayment))
|
||||
.anyMatch(p -> payment.getAddress() != null && payment.getAddress().equals(p.getAddress()));
|
||||
}
|
||||
|
||||
public void updateAddressNodeMap(Map<Wallet, Map<Address, WalletNode>> addressNodeMap, Wallet wallet) {
|
||||
this.addressNodeMap = addressNodeMap;
|
||||
getAddressNodeMap(wallet);
|
||||
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 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 List<WalletNodePayment> getWalletNodePayments() {
|
||||
return payments.stream().filter(payment -> payment instanceof WalletNodePayment).map(payment -> (WalletNodePayment)payment).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public static class Output {
|
||||
@ -269,6 +231,10 @@ public class WalletTransaction {
|
||||
public TransactionOutput getTransactionOutput() {
|
||||
return transactionOutput;
|
||||
}
|
||||
|
||||
public Map<String, byte[]> getDnsSecProof() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static class NonAddressOutput extends Output {
|
||||
@ -288,13 +254,48 @@ 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 ChangeOutput extends Output {
|
||||
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 {
|
||||
private final WalletNode walletNode;
|
||||
private final Long value;
|
||||
|
||||
public ChangeOutput(TransactionOutput transactionOutput, WalletNode walletNode, Long value) {
|
||||
public WalletNodeOutput(TransactionOutput transactionOutput, WalletNode walletNode, Long value) {
|
||||
super(transactionOutput);
|
||||
this.walletNode = walletNode;
|
||||
this.value = value;
|
||||
@ -308,4 +309,23 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
227
src/main/java/com/sparrowwallet/drongo/wallet/bip93/Codex32.java
Normal file
227
src/main/java/com/sparrowwallet/drongo/wallet/bip93/Codex32.java
Normal file
@ -0,0 +1,227 @@
|
||||
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,7 +6,8 @@ open module com.sparrowwallet.drongo {
|
||||
requires org.slf4j;
|
||||
requires ch.qos.logback.core;
|
||||
requires ch.qos.logback.classic;
|
||||
requires json.simple;
|
||||
requires org.dnsjava;
|
||||
requires com.github.benmanes.caffeine;
|
||||
exports com.sparrowwallet.drongo;
|
||||
exports com.sparrowwallet.drongo.psbt;
|
||||
exports com.sparrowwallet.drongo.protocol;
|
||||
@ -17,6 +18,9 @@ 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,6 +4,7 @@ import com.sparrowwallet.drongo.NativeUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
public class Secp256k1Context {
|
||||
@ -33,23 +34,47 @@ public class Secp256k1Context {
|
||||
}
|
||||
|
||||
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/libsecp256k1.dylib");
|
||||
NativeUtils.loadLibraryFromJar("/native/osx/aarch64/" + libName);
|
||||
} else if(osName.startsWith("Mac")) {
|
||||
NativeUtils.loadLibraryFromJar("/native/osx/x64/libsecp256k1.dylib");
|
||||
NativeUtils.loadLibraryFromJar("/native/osx/x64/" + libName);
|
||||
} else if(osName.startsWith("Windows")) {
|
||||
NativeUtils.loadLibraryFromJar("/native/windows/x64/libsecp256k1-0.dll");
|
||||
NativeUtils.loadLibraryFromJar("/native/windows/x64/" + libName);
|
||||
} else if(osArch.equals("aarch64")) {
|
||||
NativeUtils.loadLibraryFromJar("/native/linux/aarch64/libsecp256k1.so");
|
||||
NativeUtils.loadLibraryFromJar("/native/linux/aarch64/" + libName);
|
||||
} else {
|
||||
NativeUtils.loadLibraryFromJar("/native/linux/x64/libsecp256k1.so");
|
||||
NativeUtils.loadLibraryFromJar("/native/linux/x64/" + libName);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch(IOException e) {
|
||||
} catch(UnsatisfiedLinkError | IOException e) {
|
||||
log.error("Error loading libsecp256k1 library", e);
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,38 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
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
|
||||
=PElt
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
@ -0,0 +1,88 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
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
|
||||
Tnzr2nbMgOg+oGss0ScQUQ==
|
||||
=WZyK
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
89
src/main/resources/gpg/Craig_Raw_craig_sparrowwallet.com.asc
Normal file
89
src/main/resources/gpg/Craig_Raw_craig_sparrowwallet.com.asc
Normal file
@ -0,0 +1,89 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
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
|
||||
EmR2yA==
|
||||
=t5JY
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
@ -0,0 +1,52 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
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
|
||||
=/fv8
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
111
src/main/resources/gpg/Marko_Bencun_marko_shiftcrypto.ch.asc
Normal file
111
src/main/resources/gpg/Marko_Bencun_marko_shiftcrypto.ch.asc
Normal file
@ -0,0 +1,111 @@
|
||||
-----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-----
|
||||
@ -0,0 +1,280 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
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
|
||||
A7UR6bgzBiiQVdi1gGWJBGxbJrY=
|
||||
=JG7G
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
109
src/main/resources/gpg/Olaoluwa_Osuntokun_laolu32_gmail.com.asc
Normal file
109
src/main/resources/gpg/Olaoluwa_Osuntokun_laolu32_gmail.com.asc
Normal file
@ -0,0 +1,109 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
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
|
||||
=4c8j
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
302
src/main/resources/gpg/Oliver_Gugger_gugger_gmail.com.asc
Normal file
302
src/main/resources/gpg/Oliver_Gugger_gugger_gmail.com.asc
Normal file
@ -0,0 +1,302 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
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
|
||||
GJDlS+YjTuu92TulWVx1gUrUN733t5M=
|
||||
=NiKY
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
89
src/main/resources/gpg/Peter_D._Gray_peter_coinkite.com.asc
Normal file
89
src/main/resources/gpg/Peter_D._Gray_peter_coinkite.com.asc
Normal file
@ -0,0 +1,89 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
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
|
||||
pqiU76AXZscBb0iPCg==
|
||||
=gWi8
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
40
src/main/resources/gpg/SatoshiLabs_2021_Signing_Key.asc
Normal file
40
src/main/resources/gpg/SatoshiLabs_2021_Signing_Key.asc
Normal file
@ -0,0 +1,40 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
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
|
||||
=mmx4
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
@ -0,0 +1,52 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
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
|
||||
jLZugv6bMuMLjA==
|
||||
=0krM
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
@ -0,0 +1,43 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
Comment: Hostname:
|
||||
Version: Hockeypuck 2.2
|
||||
|
||||
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
|
||||
yQdYDjoIbgfDCQ7KTnaRHQ==
|
||||
=32hh
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
@ -0,0 +1,76 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
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
|
||||
=puwO
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
@ -0,0 +1,11 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mE8EX3YN+BMFK4EEAAoCAwRhTEFptcVn6WAE4PbCTCeeHRmqQ5Yd49wCdycs9Sje
|
||||
2B/rBPEGADac9PRmIXhjtWn3pifZ/sG2hFzl7G3VGIDttElTdGVwYW4gU25pZ2ly
|
||||
ZXYgKFNwZWN0ZXIgcmVsZWFzZSBzaWduaW5nIGtleSkgPHNuaWdpcmV2LnN0ZXBh
|
||||
bkBnbWFpbC5jb20+iJYEExMIAD4CGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AW
|
||||
IQRvFuNU+DOT1uUuwl827TV6skuRXwUCY2bpqAUJDVbdMAAKCRA27TV6skuRX4CZ
|
||||
AP9Ad+uiNdSBelmn9B+/N0rWiH+xWL+nun5zDjHChKSm6gD/ei3znO5TH7x0fXuv
|
||||
OK61DwMTSU3GjoZH+fd4mn9ZczQ=
|
||||
=cUgx
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
@ -0,0 +1,51 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
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
|
||||
KacID3YioW9iTP4/YO7JZ3raYzg=
|
||||
=q8Nb
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
@ -0,0 +1,713 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
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
|
||||
CRbW
|
||||
=vP22
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user