Compare commits

...

136 Commits

Author SHA1 Message Date
Craig Raw
077d2142cc revise wording for non-default sighash warnings 2026-05-31 12:27:02 +02:00
Craig Raw
cab72b2037 improve verification of psbt sighash types 2026-05-31 11:55:09 +02:00
Craig Raw
7d0699faa0 refactor secure url check for additional call sites 2026-05-30 11:33:51 +02:00
Craig Raw
e96aa0d3f8 verify precomputed outputs match bip375 proofs 2026-05-24 10:22:46 +02:00
Craig Raw
20670b9b7d add support for bip 375 verification of psbts 2026-05-23 14:51:03 +02:00
Craig Raw
3779317388 update bip322 implementation to match completed spec 2026-05-22 15:00:37 +02:00
Craig Raw
cc55b5f13a finalize external inputs in cross-wallet psbts to avoid empty witnesses 2026-05-19 17:26:02 +02:00
Craig Raw
3b1e9e5817 add specter signer 2026 firmware signing key 2026-05-19 11:46:14 +02:00
Craig Raw
dfd947cb69 add sp support for bip322 message signing 2026-05-18 14:55:15 +02:00
Craig Raw
698f8b08a1 add support for computed silent payment address persistence 2026-05-18 12:41:01 +02:00
Craig Raw
7df781c77c guard against incorrect sighash types when signing txs with sp outputs 2026-05-15 09:34:43 +02:00
Craig Raw
e4b63fbd19 add needed scan start helper 2026-05-14 12:39:20 +02:00
Craig Raw
29cd4b7909 prevent tweakless address nodes in sp wallets 2026-05-08 09:26:40 +02:00
Craig Raw
c87d5cc3c2 add silent payment consolidation output and dust threshold change check 2026-05-07 09:37:46 +02:00
Craig Raw
3fbad787a4 add sp wallet helper methods 2026-05-04 14:01:30 +02:00
Craig Raw
9c53c6d6d0 add silent payments scan utility function and unit tests 2026-04-29 09:40:25 +02:00
Craig Raw
ee2f339136 fix address comparison issue by including script type in equals and hashcode 2026-04-27 14:58:37 +02:00
Craig Raw
1468b5d50d explicitly set either keystore xpub or spscan fields depending on wallet policy type 2026-04-26 10:16:02 +02:00
Craig Raw
9c28138cd3 improve sp related output descriptor and psbt behaviour 2026-04-24 12:36:42 +02:00
Craig Raw
854eb513e9 implement silent payments change outputs and other sp related fixes 2026-04-23 12:22:19 +02:00
Craig Raw
bc526c90e1 various sp wallet related fixes and improvements 2026-04-22 14:51:52 +02:00
Craig Raw
15e5aaa4b9 refactor script type methods to accept and handle wallet policy type requirements internally 2026-04-21 09:37:18 +02:00
Craig Raw
570bf886eb integrate silent payments to the wallet and keystore structure 2026-04-21 07:36:38 +02:00
Craig Raw
3c12447a63 support spending silent payments outputs as per bip376 2026-04-20 09:53:47 +02:00
Craig Raw
71604b9489 support constructing output descriptors from sp wallets 2026-04-16 10:44:29 +02:00
Craig Raw
934b0ad890 support output descriptor annotations per bip393 2026-04-16 10:06:31 +02:00
Craig Raw
70c4c94312 support silent payment output descriptors as per bip392 2026-04-15 15:26:48 +02:00
Craig Raw
3b8677e59b add initial support for silent payments wallets 2026-04-14 15:11:47 +02:00
Craig Raw
3a1712bdbf fix intermediate zero sum logic when summing private keys 2026-04-14 13:52:53 +02:00
Craig Raw
dad9fe2fcc support loading libsecp256k1 from jvm lib folder 2026-03-13 07:58:32 +02:00
Craig Raw
f5a52f9eae add bip32 derivation fallback when retreiving signing nodes for high-index inputs 2026-03-11 17:31:13 +02:00
Craig Raw
6cbb144326 avoid npe when the extracting signature from a bip322 psbt 2026-03-10 12:07:48 +02:00
Craig Raw
4049ebcdda add setters for psbtv2 tx version and locktime 2026-03-06 09:14:20 +02:00
Craig Raw
af031d9425 refactor bip322 implementation for qr and file signing support 2026-03-05 13:36:47 +02:00
Craig Raw
53c999a01b upgrade bcprov to v1.82 and pgpainless to v1.7.7 2026-03-04 14:06:49 +02:00
Craig Raw
77bc7ffe2b upgrade junit to v5.14.1 2026-03-04 13:04:35 +02:00
Craig Raw
184ccc225d upgrade caffeine to v3.2.3 2026-03-04 12:34:41 +02:00
Craig Raw
951c119b42 upgrade dnsjava to v3.6.4 2026-03-04 12:31:52 +02:00
Craig Raw
5b724b448a upgrade argon2-jvm to v2.12 2026-03-04 12:13:46 +02:00
Craig Raw
b9887be8d3 upgrade jna to v5.18.1 2026-03-04 11:35:53 +02:00
Craig Raw
5ce39fefaf upgrade logback-classic to v1.5.32 and slf4j-api to v2.0.17 2026-03-04 11:29:31 +02:00
Craig Raw
0a500ea002 remove warning on gradle test task 2026-02-04 12:56:47 +02:00
Craig Raw
eb4b219221 use network-specific hrp as silent payments bitcoin uri parameter 2026-01-23 09:33:28 +02:00
Craig Raw
5cc1b0c551 fix handling of non-standard key derivations when writing output descriptors 2026-01-15 10:32:25 +02:00
Craig Raw
03e2e76b24 implement method to add missing key path information to psbts 2026-01-03 14:21:01 +02:00
Craig Raw
7b0ce57009 add trezor safe 7 and ledger nano gen5 wallet models 2025-12-11 11:07:04 +02:00
Ian McKenzie
167ed35693
add bip93 package to exports 2025-12-09 12:02:09 +02:00
Craig Raw
3aff03df53 followup 2025-12-01 11:37:45 +02:00
Johannes Zweng
b70c74d067
improve calculation of taproot tweaked output key
* improve calculation of Taproot tweaked output key

* add BIP341 test vectors for taproot key tweaking

* simplify and use helper methods as proposed in PR review
2025-12-01 11:28:48 +02:00
Ian McKenzie
a378e1a33b
add initial support for codex32
* Refactor Bech32 to separate validation, decoding, and checksum

Also make the charset and some things public, and add a new flag
to convertBits whether to enforce the last padded bits are zeros.
This will be used to support Codex32/BIP93.

* Add initial BIP93 support

This adds initial support for Codex32, specifically support for
importing secret shares and deriving BIP32 secrets from them.
Deriving shares is not yet implemented.
2025-11-27 07:37:38 +02:00
Ian McKenzie
cb6fc2a066
add unit tests for bip32 key derivation 2025-11-24 11:29:56 +02:00
Craig Raw
36847e1170 improve psbt2 matching and combine functionality, add psbt copy method 2025-11-24 11:14:30 +02:00
Craig Raw
49ecc4810e validate silent payment outputs on psbt transaction extraction 2025-11-19 16:03:13 +02:00
Ian McKenzie
dd94f745a3
add bip173 and bip350 segwit address test vectors 2025-11-19 12:11:01 +02:00
Craig Raw
6aca78cade use psbtv2 as the default internal representation 2025-11-19 11:56:02 +02:00
Craig Raw
6bd3d80303 implement bip375 with tests 2025-11-18 10:35:58 +02:00
Craig Raw
8e134305c5 add silent payments dleq proof wrapper class 2025-11-13 13:57:55 +02:00
Craig Raw
cc1b434f3d add bip374 dleq proof implementation and unit test 2025-11-13 13:19:54 +02:00
Michele Balistreri
8dd8b9efc0
add keycard and keycard shell to wallet models 2025-11-12 16:55:38 +02:00
craigraw
a675acccea
add bech32 unit test with BIP-173/350 test vectors 2025-11-12 11:11:03 +02:00
Ian McKenzie
a4d0ec4062 Add Bech32 unit test with BIP-173/350 test vectors
Just the tests for valid and invalid Bech32/Bech32m strings, as
taken from the BIP test vectors.
2025-11-11 10:23:58 -08:00
Craig Raw
e975cbe6f8 refactor to use transaction parameters record object when creating a wallet transaction 2025-10-21 12:05:34 +02:00
Craig Raw
ad90ea0d38 increment fee amount when desired fee rate is equal to common default rate of 1 sat/vb to ensure maximum relayability 2025-10-21 09:50:36 +02:00
Craig Raw
4e68815fa9 use declarative style to indicate consolidation payments and include their bip32 derivations in psbt outputs 2025-10-17 10:25:52 +02:00
Craig Raw
286e04ad25 fix missing psbt output script test 2025-10-03 11:51:38 +02:00
Craig Raw
2ced4c1996 fix annotation related compile issue 2025-09-30 12:22:49 +02:00
Craig Raw
3b069c12ca upgrade caffeine to remove unsafe access method 2025-09-30 09:36:55 +02:00
Craig Raw
b25289b7b5 upgrade to gradle v9.1.0 2025-09-29 13:19:54 +02:00
Craig Raw
6eb46da87a create temporary native library load directory with restricted permissions on posix filesystems 2025-09-29 12:38:11 +02:00
Craig Raw
73acc00ab6 improve dns hrn support 2025-09-29 11:53:17 +02:00
Craig Raw
a896809286 implement silent payments support in wallet with psbt output field 2025-09-29 08:34:38 +02:00
Craig Raw
af879a30f1 support uncompressed raw keys for silent payments scans 2025-09-18 16:14:42 +02:00
Craig Raw
7f707017b7 add silent payment change tests 2025-09-16 14:05:06 +02:00
Craig Raw
9c826d7819 fix npe on null p2sh redeem script 2025-09-12 15:42:05 +02:00
Craig Raw
1623f923b3 add public key negation support 2025-09-11 18:16:25 +02:00
Craig Raw
6c7662ca09 add support for sending silent payments 2025-09-11 16:33:57 +02:00
Craig Raw
0b3b1a5c3f align input pubkey retrieval to silent payments reference implementation 2025-09-10 13:10:22 +02:00
Craig Raw
7c0aa1545d override equals and hashcode for sp address 2025-09-04 16:35:55 +02:00
Craig Raw
da736c8cef use map of scriptpubkeys instead of transaction outputs for tweak computation 2025-08-21 11:43:19 +02:00
Craig Raw
a4d86f9ee3 improve pubkey checks to consider just x-only and compressed keys 2025-08-20 07:46:09 +02:00
Craig Raw
68966e5c26 followup 2025-08-19 16:17:53 +02:00
Craig Raw
e12fdfa47c add initial silent payments library support 2025-08-19 15:22:00 +02:00
Craig Raw
d30cc4432c add support for block parsing 2025-08-16 13:02:17 +02:00
Craig Raw
23f2b9197a fix bluewallet spelling 2025-08-12 08:08:47 +02:00
Craig Raw
b69e8f3629 upgrade to gradle 8.14.3 2025-08-07 11:18:14 +02:00
Craig Raw
0aedd1df46 fix non bip32 child derivation test 2025-08-07 08:55:06 +02:00
Craig Raw
f5d5e9dc30 revert range support, derive master fingerprint from master key if not provided 2025-08-05 09:26:56 +02:00
Craig Raw
eb06840de0 support parsing ranges in output descriptor child derivations 2025-08-04 15:30:46 +02:00
Craig Raw
92c57d276c fix serialization issue affecting single byte witness elements with a value of zero 2025-08-04 13:37:41 +02:00
Craig Raw
0ce32e4314 add has zero in pin check for onekey classic 2025-07-29 14:44:48 +02:00
Craig Raw
056d5f83a6 improve dnssec validation for cnames, wildcards and overrides 2025-07-29 12:50:48 +02:00
Craig Raw
58cc096f8e add dnssec resolver for bip353 names and associated psbt output field for dnssec proof 2025-07-24 14:30:04 +02:00
Craig Raw
2a456dd602 support a variable min tx relay fee rate when creating a wallet transaction 2025-07-17 09:11:59 +02:00
Craig Raw
e1f2ce41ad fix issue of including parent path elements in deterministic key when deriving child xpub from an output descriptor containing more than two child path elements 2025-07-09 10:26:37 +02:00
Craig Raw
13e1fafbe8 fix specter diy capitalization 2025-06-07 11:22:44 +02:00
Craig Raw
ad02b8a33c derive to maximum bip32 account level where child path in output descriptor contains more than two elements 2025-06-06 11:45:32 +02:00
Craig Raw
abb598d3b0 add pay to anchor script and address type 2025-04-14 15:49:02 +02:00
Craig Raw
3b36947419 support parsing xpubs encoded for a different network 2025-04-03 15:17:12 +02:00
Craig Raw
41cd6a68c0 upgrade bouncy castle, pgpainless and logback 2025-04-01 14:59:37 +02:00
Craig Raw
e42931cd55 update jna to v5.13.0 2025-03-11 16:17:13 +02:00
Craig Raw
2468578e72 split app gpg keyring into individual files 2025-03-04 15:07:30 +02:00
Craig Raw
66ff275f46 support invalid script type warnings 2025-03-04 11:47:22 +02:00
Craig Raw
5fd8e9416a fix camelcase on wallet model displayed names 2025-02-20 17:03:00 +02:00
Craig Raw
7666060c8e followup 2025-02-20 13:49:55 +02:00
Craig Raw
0dddf3095f add onekey wallet models 2025-02-20 13:28:24 +02:00
Craig Raw
42968028cc add bip47 notification transaction test 2025-02-19 11:31:18 +02:00
Craig Raw
419ed1a699 improve support for keystore relabelling 2025-02-13 08:42:07 +02:00
Craig Raw
f7d5b4fb8f trim whitespace chars before testing if byte array contains only hex or base64 chars 2025-02-08 09:29:33 +02:00
Craig Raw
ad60a37d0e add byte array tests for hex and base64 2025-02-06 15:33:11 +02:00
Craig Raw
ca758e1288 strip non-numeric trailing version info 2025-02-04 19:52:40 +02:00
Craig Raw
342c85a39e add max label length constant to match db schema 2025-01-30 14:49:23 +02:00
Craig Raw
b2c362d5a7 store treetable column sort on adjustment, and restore on wallet load 2025-01-28 12:52:52 +02:00
Craig Raw
1805aeb374 add wallet table to store layout settings 2025-01-28 10:32:57 +02:00
Craig Raw
378ab611f5 exclude taproot wallets and jade, tapsigner and satochip hwws from requiring non witness tx in psbts 2025-01-23 15:39:33 +02:00
Craig Raw
f67a2caf53 add device registration field to store ledger multisig hmacs 2025-01-22 16:24:43 +02:00
Craig Raw
0df1f79e5c ensure consistent keypair implementation is used for all constructors 2025-01-22 11:33:45 +02:00
Craig Raw
89a6b1296e fix incorrect script type returned for p2sh multisig 2025-01-21 08:50:16 +02:00
Craig Raw
7b9affb3de improve quick transaction test 2025-01-20 09:16:29 +02:00
Craig Raw
64a3f1c00b ensure consistent xpub ordering when copying output descriptors without child derivations 2024-12-02 10:07:55 +02:00
Craig Raw
3cb3d322a0 move ostype to drongo 2024-11-26 11:09:53 +02:00
Craig Raw
a26ba49bc6 move version class to drongo 2024-11-25 15:53:11 +02:00
Craig Raw
df7529b1a1 remove unneeded module info plugin 2024-11-25 15:23:47 +02:00
Craig Raw
6170157daa remove unneeded dependencies 2024-11-25 15:18:36 +02:00
Craig Raw
d5393bd436 add output descriptor accessors and copy function 2024-11-19 10:46:22 +02:00
Craig Raw
817458a0c3 add equals and hashcode to output descriptor 2024-11-18 15:14:20 +02:00
Craig Raw
a90d553f1e fix psbtv2 output amount serialization 2024-11-18 13:05:13 +02:00
Craig Raw
3b9998180f reverse prevtxid byte ordering during serialization and deserialization 2024-11-18 12:43:45 +02:00
Craig Raw
efc9d9d554 allow hardened character selection when writing key 2024-11-15 16:31:50 +02:00
Craig Raw
96df6284e1 add psbt v2 support 2024-11-15 12:15:41 +02:00
Craig Raw
4564c5d25a add eckey arithmetic functions 2024-10-31 17:02:26 +02:00
Craig Raw
dba1a9a2be add support for x25519 and secp256r1 keys 2024-10-30 13:04:20 +02:00
Craig Raw
35bebe13bc fix build instructions 2024-10-21 09:26:01 +02:00
Craig Raw
acb1d767e8 add helper method to multiply a public key 2024-10-08 10:31:23 +02:00
Craig Raw
f8f50c0dd9 add ledger stax and flex hardware wallet models 2024-09-13 13:12:17 +02:00
Craig Raw
6b89a0c5ea improve performance of wallet transactions update 2024-09-12 14:29:21 +02:00
Craig Raw
87b5f992d0 add constructor to optionally rewrite derivation path 2024-08-22 11:03:01 +02:00
122 changed files with 12653 additions and 3233 deletions

View File

@ -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

View File

@ -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')
}

View File

@ -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"
}
}
}

View File

@ -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));
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

Binary file not shown.

View File

@ -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
View File

@ -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
View File

@ -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

View File

@ -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.");
}
}

View File

@ -0,0 +1,5 @@
package com.sparrowwallet.drongo;
public enum FileType {
TEXT, JSON, BINARY, UNKNOWN;
}

View 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;
}
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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;
}

View 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;
}
}

View File

@ -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);
}
}

View File

@ -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) {

View 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;
}
}

View File

@ -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);
}
}
}

View File

@ -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";
}
}

View File

@ -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) {

View File

@ -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;

View File

@ -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;
}

View 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 = aG and C = aB 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 = aG
ECKey A = G.multiply(a, true);
// Let C = aB
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 = kG
ECKey R1 = G.multiply(k, true);
// Let R2 = kB
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 + ea) 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 (aB)
* @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 = sG - eA
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 = sB - eC
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 = aG
* @param B The public key B
* @param C The shared secret C = aB
* @param R1 The first commitment R1 = kG
* @param R2 The second commitment R2 = kB
* @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);
}
}

View File

@ -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);
}
/**

View File

@ -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());
}

View File

@ -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));
}
}

View 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;
}
}
}

View File

@ -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;
}
}

View 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;
}
}

View 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();
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -0,0 +1,7 @@
package com.sparrowwallet.drongo.dns;
public class DnsPaymentValidationException extends Exception {
public DnsPaymentValidationException(String message) {
super(message);
}
}

View 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;
}
}

View File

@ -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();
}
}

View 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;
}
}

View File

@ -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() {

View File

@ -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);

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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();

View 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;
}
}

View File

@ -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;

View File

@ -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());
}

View File

@ -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);

View File

@ -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) {

View File

@ -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;

View File

@ -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);

View File

@ -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

View File

@ -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");
}
}
}

View File

@ -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;

View File

@ -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();
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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) {}

View File

@ -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) {}
}

View File

@ -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 (aB_scan) and the public key (aG)
* 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 = aG)
* @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 = aB_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();
}
}

View File

@ -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);
}

View File

@ -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);

View File

@ -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() {

View File

@ -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);
}

View File

@ -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();
}
}

View File

@ -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() {

View File

@ -0,0 +1,5 @@
package com.sparrowwallet.drongo.wallet;
public enum SortDirection {
ASCENDING, DESCENDING
}

View File

@ -0,0 +1,5 @@
package com.sparrowwallet.drongo.wallet;
public enum TableType {
TRANSACTIONS, UTXOS, RECEIVE_ADDRESSES, CHANGE_ADDRESSES, SEARCH_WALLET, WALLET_SUMMARY
}

View File

@ -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

View File

@ -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();

View File

@ -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));

View File

@ -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;
}
}

View File

@ -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) {}
}

View File

@ -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);
}
}
}

View 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;
}
}
}

View File

@ -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;
}

View File

@ -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);
}

View File

@ -0,0 +1,38 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
=PElt
-----END PGP PUBLIC KEY BLOCK-----

View File

@ -0,0 +1,88 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
Tnzr2nbMgOg+oGss0ScQUQ==
=WZyK
-----END PGP PUBLIC KEY BLOCK-----

View File

@ -0,0 +1,89 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
EmR2yA==
=t5JY
-----END PGP PUBLIC KEY BLOCK-----

View File

@ -0,0 +1,52 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
=/fv8
-----END PGP PUBLIC KEY BLOCK-----

View 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-----

View File

@ -0,0 +1,280 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
A7UR6bgzBiiQVdi1gGWJBGxbJrY=
=JG7G
-----END PGP PUBLIC KEY BLOCK-----

View File

@ -0,0 +1,109 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
=4c8j
-----END PGP PUBLIC KEY BLOCK-----

View File

@ -0,0 +1,302 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
GJDlS+YjTuu92TulWVx1gUrUN733t5M=
=NiKY
-----END PGP PUBLIC KEY BLOCK-----

View File

@ -0,0 +1,89 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQENBFJ8Ct4BCAC5jlWYlkqNQsiyt63Hothb2W1+DUH+19C2qGg54PG0wV//yAaL
xhpxejQogYZ415fYM1NHBf+vCbvbwn6yfrO8kSLj+EFVaob2io6GoG3N9Ftzeljs
U5dsUaAwxNV/bHHTt9vi5GQRAvMZYDLWwNZ+ET476gEYt8q0VX/PojowHZ4gD0Cj
UxtOh+2zk2jAGZ9MpX/9IXxfNQ9Y3kBE8ZB1KHzuFM4WsfO08uMV0wCZ6HOsgauv
bG+7JQakcpWcA7OdkNJdwkjrDtB2Cv9xedUSTzROmJhIQ3xkRI0+7Dh/pb1qwoHZ
Aj+Ve1RwZtxAGuGzk6b3ZFcJ8ey7AptwKr9VABEBAAG0IlBldGVyIEQuIEdyYXkg
PHBldGVyQGNvaW5raXRlLmNvbT6JATMEEAEIAB0WIQTWt51QtpI7GOM3XgZFFyqy
RrHoFAUCXMJKrQAKCRBFFyqyRrHoFCf6B/9H6B1gC1nB26o80w3hBPuX8zsALDzH
Wy8315uZLlTwmx3ABzRsUzqUVOL3z6GY9pRe+Fa7B00T/g3MrP/F+xB4Fu/+51qz
0J338xZYgs4d0EuTJMQRqxjucjPpOoiO2UCO+RAxSu/dwtFLAFxH8ess6ULhR+iH
s70APk9z7BDHHvl57FEvJ7PY+nEr7Be3MpCfB7xhN+CwZwysNMoOjCt3fVoT3pHb
KyWVpaaOi499R3OxJJtcU5HfpXxqIG5A3GfzuVdfS2q7objdOdHOEa1KjcZ9n6Y5
2Q/Fy1ddOfn7CjSvHc9JKF5xZhp7YqlHzShRjlPntBwItKNGN55QqFdUiQEzBBAB
CAAdFiEE1redULaSOxjjN14GRRcqskax6BQFAl2ENIEACgkQRRcqskax6BR4nQgA
t6Ko1JbukCM9TOsuHQuKJPjjhv7YzaCSCDtehA370ASG+bGvdGOaW8kKBzh+6dRw
M/Qjk1xK7DZdbpLIGYA7A7GLEXHZzOOquAGt1pU494rFi9nSqS8fB2wZARP6vBYM
O4pGjmQ7GI39eA6dGvKq6aEBk1SKkEz7z/lIZTLkaHFPUcLGtHNjZXya0PKXBCxZ
pFOPOD7aR7PTg/6oU9bo2Gb8lTNHnKFDP/R6gxHobMV6c0+qyekLlajde8Slb3HC
mhB0fjQS5Cvn3wpqsMDzRBHdSZQ0uCInw+8wcnqZgRbLYEPe0L6OD2rLHYyBLELm
JKsbKjkncK5mc2ciibvvTokBPQQTAQoAJwUCUnwK3gIbAwUJLSS9AAULCQgHAwUV
CgkICwUWAgMBAAIeAQIXgAAKCRCjoxutWipbED6pCACaPoFwbxn/Q3h9EDIgDn4a
fg2atPLfn+92ynqj2JAcHcDoDuMcf162p7ConM8YQkMpwlLwhcjpLEOga1reI583
Y6K6lwhHHsoPr2Pb4r6nxlmJdxTWg899WZWuT9Jjjqei9WpjArkm0TRdEn0527rE
fWqfCQrV2w1oO9WpkBHiQMRcD7jYctaFDsqng2d2NPI9cj8lytHGfiESIa4ttHDC
+ocDYSk7mjbJvnXf8EcJNgQwnZYgE+JAi6+TWSm0GAsxhnMij9Mf3RYrGimCTG4m
qApUnb38TyR05JRvRZk3KR1qp32dcqMlwicHLw7jVzKGyMFyjBIkBQNVF9PAZRtB
iQIzBBABCAAdFiEEf+COngH9+4OI+Zsy58ev6jMIbBkFAlzFI9IACgkQ58ev6jMI
bBkATA//fda3rmGg5Ca7zc4cAstALqhqwEUG6ZfQ6Zt5HGh5XUjznWYQeLqcMSWX
rj4u/PYGAlPBXkDekaeKjXwGMDndP35lPmCRONhmqy0vfD6zjoBY/M4d+ite2efb
JpTlvKtIWwQ9sL+lH69aNzpbHUesJLvH/UotAbH0ImPp42FS2K2958cV209WMK70
KErzbdWcue7yQNxz6B3IBlfifQQLJ3ZayCjpIizmoliTXuWPc2rF2O08QUxfNeeu
4rJuodvOxBJM2b3n/1OZwhl3kyfFtUmEoWhEu3JU1mOaoJACs9eYt7VO2esTe+U2
CsGmcW8WDuviOrxozpHnH7WFs34NjMf7xnHDUowugwDUZCF41Ys9Rq9/3veh3Nu9
arNMZJw5SeztBQ0Ud57Q16ppIteORuQQ4tnJnPeJ4pG9YCB5Y9dTCQ8MHi4fsYLJ
+FeKSXLbidsFhNV2ytYLbEK1xfTnm//erG2hBcaUtP35aNGUFWu02v0sigSioWLI
A3jUUARHleNhZqD4yW5tcH8qfJKeIWR3oKeZp2NcYaYkE4vr8KNxQj/pfUG7hkPL
sWYUn5ku47wwwN6oup5a9TKiLEC7hXiKw38/TKkIYZTeCstYq5tukJKl+tpOjp0z
016P1+5xHqMZ9sNyamb+Gfh5APvio7EknQMCdHvGliWTRTsamfWJAjMEEAEKAB0W
IQT1Tdgn9LbuAJxu3p40PysiFM2qzAUCXP4hYwAKCRA0PysiFM2qzBRND/938B1N
hOCFStNEhO2Lg/XrCx0ygweS+0LwjAFGvdVRsTKXtEqNNMXozFvo1DiuQ7wJDllX
f/MHrMZNiQ46Wj1Dh3lQWQuOg8eD96tiS6SRqgnlAUtp1aIpS9RcGXMPbbgKGsd5
ZtQFYslg0A8w2gO7xjvH3wADO7muKFvc6EovZ2oqipOtKF1k4LqRvuu+VgmzcOmn
1YvPKTIoYSXn/jJsjwrH4rZ/hbFUdK0hz5PW1TH4UsmgSnRlJs6MufPL20SoQ8yw
djHAb3DnluVD+qn2Zga09myfsCENqOrspNnfCkkc9xnBjXkp07znv3wxmxKFO2sJ
qCI14DAE+zq9enMFiz3FyCvJp5uf8HHIHIgiwza+Vb6Azq09UOYLQAbQyruETUgx
hvwdQaR4c6OjnrmA/4S7DrUUQXFW0OfVmBg70rxNHs4zAhnewQ9KIVVQc1P304FH
ypWHjVgZFMndSvBEFh5yA8T3gkHjNfMBaMuShGyB9vSUTKt87WwMBzMalHA9kCQ6
ehSgW2LyC8w3Vmrh9U09kMq8UnJDH8o8NgmshP9rZDuWdFr2vwod0LAXPiMLhPSE
hPSQlC8jSrm3VQUzRxPTpjDXR6YEvh3RvLEYLPnh3+ftoC+1C8PVK+fR3nPiCrU7
xu8OD8/gNoWQEMdxWjX8JgcGcaZY1QP3FJnS9YkBIgQSAQoADAUCUnwNPQWDB4Yf
gAAKCRAc3a2gtETN2mdSCADmW56v3YGNCoEgOuFYmJhlCLgr5/9IBeX6ovYmVv3m
P+BFk/8hx1dW6CNBheZI5jXxNh5Km1okgWI337JIRuE4Cq2iAyoeJN8xT5dAGf4k
4WwcYXS6SZpfioGHbL6xNMbTvhWvdEEJWMi9csBmtgdKbjA7qHDb/cFcdRZWGCsk
I8lPQnvoSYTuYR83W7UKFj2LXQewZ9ReedubE7y/an8mInb91sgt/FVwYVHF7YbT
cq4L57eqGBBL7U3m3TgXM1/lg+leA2CYKVZXH24U+ykP6uCkWeF+vRk92YSsGWjy
pw4CFdixtY7Qd251CEBFEmBPmaI5gwV4DGvf4LH0/0v1iQIzBBABCgAdFiEE2A71
10s7OMrD8rvWHJ4DPGxlhgYFAlzIY3YACgkQHJ4DPGxlhgapeBAAhfEqTpLYVEz/
NKxGI1E42/fRvN58bL84SKL+qpqYTHydxzgF4686H3k0gOBqHnCFTvLLfyd5pk0d
FIfHKeWJ2YSI9b9T0mc/LfyBq+DZGvBSee+YFn072KMElbXVgL75Cx2e868f7p3O
k7GJaXkcyVRQPVJCYSAXvlJrvKSWf2C0N/SNLboXjk3uiIsKaFa8WDjkNRFjOqT4
kWNq6vb4B/OeoJxFNhYxlQuAA9lj+6QxSoPl9lDSdUhMUO8jK+1eL+F/A6wE5YF7
j4A13GoS5LEgfm/OBLTJ1S51WxM+K0suqwYd3Sti/U+L4kIz3bctTrL1tvUmEMqh
9ffbu4/T7QhuvDpNDMGt16p8EPEyFPjOx6JiHQxDcL1YuEP+/zBmforhtXaV+L+y
g0oGP0mUm71jX6rkge36vnTSJrTv3UVOyiekVyctZ+PaWh5XGEoux29E6wjQJoGT
uJ9KPRMFjiDY71W/0S3BHr6YOchVXNdjCq7uppB3zGOL1aEuGppDumHeGTZa86Oe
Lv3ZPRt7TwwokTkt1f0jxfIksQu9jSYpetWXeqk9veovIjyWz9zs3aqScrZO9Ap3
K17tbS7KZnbFHh4VKiHeCHnU8jzHU+489GyOrNWJ0gzMG3XXHmzpVhnABjww3hJm
NrSXWGHIQErP7ncH/hHssbbgtGgsJ1WIdQQQFggAHRYhBCU29p6cNyVmK2wUa9zx
f3oBJyAgBQJdG5F5AAoJENzxf3oBJyAgHX8BANoWLz4jJnpCZU5fN2qpPFhO+k5b
O8zx21cN+mZUQNohAP4iUvunANJS1GBtwdWrkR666fcLpqiU76AXZscBb0iPCrkB
DQRSfAreAQgAsQeTIhoI14vOW3swFKU6NfO6sURb4mxTZBJmGr+QqMjeUw48z3PO
aWFmoCCq/ZkcfVqa7RpUnY6J7SZvhUXtdCJ/yn/TpPO3X8/INyDUBPWlh77xwYfj
HnukUK4yowOxjN2Fun2TpdtfGxF2iBoBwRVBv04x/MTQAeQFT+R2KtSWv5aaYBax
e7fEto7ZTNOXyPY+ErZvtq2Bz642elUor4+4XHN/WuP9Emf4QLUoKfrgGiPOy8la
Poq5XDQojRPvykYVM6Fydeqyr4CSbSaA0RWM+vWtHRx/vnAjBEIlA9ELy6DG9RY6
h/JOkvrDLmMu5A7Q0EWiQ1jkCAiC1Yr71QARAQABiQElBBgBCgAPBQJSfAreAhsM
BQktJL0AAAoJEKOjG61aKlsQjAQIAKbdC/xUlcxfbrwAObEtdagOieW2zuHCXitU
FB73saKWeAt2H31QR/+7SMToQUX8+BITT0kBwAGLyOzHMhZ+Gu+lEMJqhIYmCdfb
7dnsNh6NKpx1j0MMO87FIVa6F5HczjwgfysBKcQoZnl2YcpAL7snvHw3jNVySQVS
YmsWSiYQ2EhqE/rALWrLquiKnepZkbebX9n27oy6S1ajSD+3n2K6YJk2pD9WpIKX
SwA+i9jTrZY3kaY6qz3SFAV7ruhhFursMBHmlmqPDs1XbaFu4UrzF8WOnu07Yw43
F7lrpx8cYIuYZczSOoQnO7gZW92QGTHnMSSIfK0D3LMKn/OKXfCIdQQQFggAHRYh
BCU29p6cNyVmK2wUa9zxf3oBJyAgBQJdG5F5AAoJENzxf3oBJyAgHX8BANoWLz4j
JnpCZU5fN2qpPFhO+k5bO8zx21cN+mZUQNohAP4iUvunANJS1GBtwdWrkR666fcL
pqiU76AXZscBb0iPCg==
=gWi8
-----END PGP PUBLIC KEY BLOCK-----

View File

@ -0,0 +1,40 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
=mmx4
-----END PGP PUBLIC KEY BLOCK-----

View File

@ -0,0 +1,52 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
jLZugv6bMuMLjA==
=0krM
-----END PGP PUBLIC KEY BLOCK-----

View File

@ -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-----

View File

@ -0,0 +1,76 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
=puwO
-----END PGP PUBLIC KEY BLOCK-----

View File

@ -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-----

View File

@ -0,0 +1,51 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
KacID3YioW9iTP4/YO7JZ3raYzg=
=q8Nb
-----END PGP PUBLIC KEY BLOCK-----

View File

@ -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