Merge branch 'master' into update-dec-2022

This commit is contained in:
HashEngineering 2023-08-26 06:52:55 -07:00 committed by GitHub
commit 2a68d8eec0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 740 additions and 164 deletions

View file

@ -59,6 +59,7 @@ dependencies {
exclude group: 'org.hamcrest', module: 'hamcrest-core' exclude group: 'org.hamcrest', module: 'hamcrest-core'
} }
testImplementation group: 'org.hamcrest', name: 'hamcrest-core', version: '2.2' testImplementation group: 'org.hamcrest', name: 'hamcrest-core', version: '2.2'
testImplementation 'junit:junit:4.13.1'
} }
processResources { processResources {

Binary file not shown.

View file

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

12
gradlew vendored
View file

@ -55,7 +55,7 @@
# Darwin, MinGW, and NonStop. # Darwin, MinGW, and NonStop.
# #
# (3) This script is generated from the Groovy template # (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project. # within the Gradle project.
# #
# You can find Gradle at https://github.com/gradle/gradle/. # You can find Gradle at https://github.com/gradle/gradle/.
@ -80,10 +80,10 @@ do
esac esac
done done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit # This is normally unused
# shellcheck disable=SC2034
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/} APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
@ -143,12 +143,16 @@ fi
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #( case $MAX_FD in #(
max*) max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
MAX_FD=$( ulimit -H -n ) || MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit" warn "Could not query maximum file descriptor limit"
esac esac
case $MAX_FD in #( case $MAX_FD in #(
'' | soft) :;; #( '' | soft) :;; #(
*) *)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
ulimit -n "$MAX_FD" || ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD" warn "Could not set maximum file descriptor limit to $MAX_FD"
esac esac

1
gradlew.bat vendored
View file

@ -26,6 +26,7 @@ if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0 set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=. if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0 set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME% set APP_HOME=%DIRNAME%

View file

@ -90,8 +90,8 @@ public class ExtendedKey {
ChildNumber childNumber; ChildNumber childNumber;
List<ChildNumber> path; List<ChildNumber> path;
if(depth == 0) { if(depth == 0 && !header.isPrivateKey()) {
//Poorly formatted extended key, add first child path element //Poorly formatted public extended key, add first child path element
childNumber = new ChildNumber(0, false); childNumber = new ChildNumber(0, false);
} else if ((i & ChildNumber.HARDENED_BIT) != 0) { } else if ((i & ChildNumber.HARDENED_BIT) != 0) {
childNumber = new ChildNumber(i ^ ChildNumber.HARDENED_BIT, true); //already hardened childNumber = new ChildNumber(i ^ ChildNumber.HARDENED_BIT, true); //already hardened

View file

@ -22,7 +22,7 @@ public class OutputDescriptor {
private static final String INPUT_CHARSET = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ "; private static final String INPUT_CHARSET = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ ";
private static final String CHECKSUM_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"; private static final String CHECKSUM_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
private static final Pattern XPUB_PATTERN = Pattern.compile("(\\[[^\\]]+\\])?(.pub[^/\\,)]{100,112})(/[/\\d*'hH<>;]+)?"); private static final Pattern XPUB_PATTERN = Pattern.compile("(\\[[^\\]]+\\])?(.(?:pub|prv)[^/\\,)]{100,112})(/[/\\d*'hH<>;]+)?");
private static final Pattern PUBKEY_PATTERN = Pattern.compile("(\\[[^\\]]+\\])?(0[23][0-9a-fA-F]{32})"); private static final Pattern PUBKEY_PATTERN = Pattern.compile("(\\[[^\\]]+\\])?(0[23][0-9a-fA-F]{32})");
private static final Pattern MULTI_PATTERN = Pattern.compile("multi\\(([\\d+])"); private static final Pattern MULTI_PATTERN = Pattern.compile("multi\\(([\\d+])");
private static final Pattern KEY_ORIGIN_PATTERN = Pattern.compile("\\[([A-Fa-f0-9]{8})([/\\d'hH]+)\\]"); private static final Pattern KEY_ORIGIN_PATTERN = Pattern.compile("\\[([A-Fa-f0-9]{8})([/\\d'hH]+)\\]");
@ -379,6 +379,13 @@ public class OutputDescriptor {
KeyDerivation keyDerivation = new KeyDerivation(masterFingerprint, keyDerivationPath); KeyDerivation keyDerivation = new KeyDerivation(masterFingerprint, keyDerivationPath);
try { try {
ExtendedKey extendedPublicKey = ExtendedKey.fromDescriptor(extPubKey); ExtendedKey extendedPublicKey = ExtendedKey.fromDescriptor(extPubKey);
if(extendedPublicKey.getKey().hasPrivKey()) {
List<ChildNumber> derivation = keyDerivation.getDerivation();
int depth = derivation.size() == 0 ? scriptType.getDefaultDerivation().size() : derivation.size();
DeterministicKey prvKey = extendedPublicKey.getKey();
DeterministicKey pubKey = new DeterministicKey(prvKey.getPath(), prvKey.getChainCode(), prvKey.getPubKey(), depth, extendedPublicKey.getParentFingerprint());
extendedPublicKey = new ExtendedKey(pubKey, pubKey.getParentFingerprint(), extendedPublicKey.getKeyChildNumber());
}
keyDerivationMap.put(extendedPublicKey, keyDerivation); keyDerivationMap.put(extendedPublicKey, keyDerivation);
keyChildDerivationMap.put(extendedPublicKey, childDerivationPath); keyChildDerivationMap.put(extendedPublicKey, childDerivationPath);
} catch(ProtocolException e) { } catch(ProtocolException e) {
@ -479,6 +486,10 @@ public class OutputDescriptor {
} }
public String toString(boolean addKeyOrigin, boolean addChecksum) { public String toString(boolean addKeyOrigin, boolean addChecksum) {
return toString(addKeyOrigin, true, addChecksum);
}
public String toString(boolean addKeyOrigin, boolean addKey, boolean addChecksum) {
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
builder.append(scriptType.getDescriptor()); builder.append(scriptType.getDescriptor());
@ -487,14 +498,14 @@ public class OutputDescriptor {
StringJoiner joiner = new StringJoiner(","); StringJoiner joiner = new StringJoiner(",");
joiner.add(Integer.toString(multisigThreshold)); joiner.add(Integer.toString(multisigThreshold));
for(ExtendedKey pubKey : sortExtendedPubKeys(extendedPublicKeys.keySet())) { for(ExtendedKey pubKey : sortExtendedPubKeys(extendedPublicKeys.keySet())) {
String extKeyString = toString(pubKey, addKeyOrigin); String extKeyString = toString(pubKey, addKeyOrigin, addKey);
joiner.add(extKeyString); joiner.add(extKeyString);
} }
builder.append(joiner.toString()); builder.append(joiner.toString());
builder.append(ScriptType.MULTISIG.getCloseDescriptor()); builder.append(ScriptType.MULTISIG.getCloseDescriptor());
} else { } else {
ExtendedKey extendedPublicKey = getSingletonExtendedPublicKey(); ExtendedKey extendedPublicKey = getSingletonExtendedPublicKey();
builder.append(toString(extendedPublicKey, addKeyOrigin)); builder.append(toString(extendedPublicKey, addKeyOrigin, addKey));
} }
builder.append(scriptType.getCloseDescriptor()); builder.append(scriptType.getCloseDescriptor());
@ -548,7 +559,7 @@ public class OutputDescriptor {
} }
} }
private String toString(ExtendedKey pubKey, boolean addKeyOrigin) { private String toString(ExtendedKey pubKey, boolean addKeyOrigin, boolean addKey) {
StringBuilder keyBuilder = new StringBuilder(); StringBuilder keyBuilder = new StringBuilder();
KeyDerivation keyDerivation = extendedPublicKeys.get(pubKey); KeyDerivation keyDerivation = extendedPublicKeys.get(pubKey);
if(keyDerivation != null && keyDerivation.getDerivationPath() != null && addKeyOrigin) { if(keyDerivation != null && keyDerivation.getDerivationPath() != null && addKeyOrigin) {
@ -561,6 +572,7 @@ public class OutputDescriptor {
keyBuilder.append("]"); keyBuilder.append("]");
} }
if(addKey) {
if(pubKey != null) { if(pubKey != null) {
keyBuilder.append(pubKey.toString()); keyBuilder.append(pubKey.toString());
} }
@ -573,6 +585,7 @@ public class OutputDescriptor {
keyBuilder.append(childDerivation); keyBuilder.append(childDerivation);
} }
}
return keyBuilder.toString(); return keyBuilder.toString();
} }

View file

@ -13,6 +13,7 @@ import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.math.BigInteger; import java.math.BigInteger;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.*; import java.util.*;
@ -22,6 +23,7 @@ public class Utils {
public static final String HEX_REGEX = "^[0-9A-Fa-f]+$"; public static final String HEX_REGEX = "^[0-9A-Fa-f]+$";
public static final String BASE64_REGEX = "^[0-9A-Za-z\\\\+=/]+$"; public static final String BASE64_REGEX = "^[0-9A-Za-z\\\\+=/]+$";
public static final String NUMERIC_REGEX = "^-?\\d+(\\.\\d+)?$";
public static boolean isHex(String s) { public static boolean isHex(String s) {
return s.matches(HEX_REGEX); return s.matches(HEX_REGEX);
@ -31,6 +33,20 @@ public class Utils {
return s.matches(BASE64_REGEX); return s.matches(BASE64_REGEX);
} }
public static boolean isNumber(String s) {
return s.matches(NUMERIC_REGEX);
}
public static boolean isUtf8(byte[] bytes) {
try {
CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder();
decoder.decode(java.nio.ByteBuffer.wrap(bytes));
return true;
} catch(Exception e) {
return false;
}
}
public static String bytesToHex(byte[] bytes) { public static String bytesToHex(byte[] bytes) {
char[] hexChars = new char[bytes.length * 2]; char[] hexChars = new char[bytes.length * 2];
for ( int j = 0; j < bytes.length; j++ ) { for ( int j = 0; j < bytes.length; j++ ) {
@ -128,6 +144,20 @@ public class Utils {
return c; return c;
} }
public static byte[] xor(byte[] a, byte[] b) {
if(a.length != b.length) {
throw new IllegalArgumentException("Invalid length for xor: " + a.length + " vs " + b.length);
}
byte[] ret = new byte[a.length];
for(int i = 0; i < a.length; i++) {
ret[i] = (byte) ((int) b[i] ^ (int) a[i]);
}
return ret;
}
/** Parse 4 bytes from the byte array (starting at the offset) as unsigned 32-bit integer in little endian format. */ /** Parse 4 bytes from the byte array (starting at the offset) as unsigned 32-bit integer in little endian format. */
public static long readUint32(byte[] bytes, int offset) { public static long readUint32(byte[] bytes, int offset) {
return (bytes[offset] & 0xffl) | return (bytes[offset] & 0xffl) |

View file

@ -71,7 +71,7 @@ public class PaymentAddress {
private BigInteger addSecp256k1(BigInteger b1, BigInteger b2) { private BigInteger addSecp256k1(BigInteger b1, BigInteger b2) {
BigInteger ret = b1.add(b2); BigInteger ret = b1.add(b2);
if(ret.bitLength() > CURVE.getN().bitLength()) { if(ret.compareTo(CURVE.getN()) > 0) {
return ret.mod(CURVE.getN()); return ret.mod(CURVE.getN());
} }
@ -83,7 +83,7 @@ public class PaymentAddress {
} }
private boolean isSecp256k1(BigInteger b) { private boolean isSecp256k1(BigInteger b) {
return b.compareTo(BigInteger.ONE) > 0 && b.bitLength() <= CURVE.getN().bitLength(); return (b.compareTo(BigInteger.ONE) > 0) && (b.compareTo(CURVE.getN()) < 0);
} }
private BigInteger secretPoint() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException, NotSecp256k1Exception { private BigInteger secretPoint() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException, NotSecp256k1Exception {

View file

@ -20,6 +20,8 @@ import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import static com.sparrowwallet.drongo.Utils.xor;
public class PaymentCode { public class PaymentCode {
private static final Logger log = LoggerFactory.getLogger(PaymentCode.class); private static final Logger log = LoggerFactory.getLogger(PaymentCode.class);
@ -313,21 +315,6 @@ public class PaymentCode {
return HDKeyDerivation.createMasterPubKeyFromBytes(pubkey, chain); return HDKeyDerivation.createMasterPubKeyFromBytes(pubkey, chain);
} }
private static byte[] xor(byte[] a, byte[] b) {
if(a.length != b.length) {
log.error("Invalid length for xor: " + a.length + " vs " + b.length);
return null;
}
byte[] ret = new byte[a.length];
for(int i = 0; i < a.length; i++) {
ret[i] = (byte) ((int) b[i] ^ (int) a[i]);
}
return ret;
}
public boolean isValid() { public boolean isValid() {
try { try {
byte[] pcodeBytes = Base58.decodeChecked(strPaymentCode); byte[] pcodeBytes = Base58.decodeChecked(strPaymentCode);

View file

@ -0,0 +1,170 @@
package com.sparrowwallet.drongo.crypto;
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;
import java.security.SignatureException;
import java.util.*;
import static com.sparrowwallet.drongo.protocol.ScriptType.P2TR;
public class Bip322 {
public static String signMessageBip322(ScriptType scriptType, String message, ECKey privKey) {
checkScriptType(scriptType);
ECKey pubKey = ECKey.fromPublicOnly(privKey);
Address address = scriptType.getAddress(pubKey);
Transaction toSpend = getBip322ToSpend(address, message);
Transaction toSign = getBip322ToSign(toSpend);
TransactionOutput utxoOutput = toSpend.getOutputs().get(0);
PSBT psbt = new PSBT(toSign);
PSBTInput psbtInput = psbt.getPsbtInputs().get(0);
psbtInput.setWitnessUtxo(utxoOutput);
psbtInput.setSigHash(SigHash.ALL);
psbtInput.sign(scriptType.getOutputKey(privKey));
TransactionSignature signature = psbtInput.isTaproot() ? psbtInput.getTapKeyPathSignature() : psbtInput.getPartialSignature(pubKey);
Transaction finalizeTransaction = new Transaction();
TransactionInput finalizedTxInput = scriptType.addSpendingInput(finalizeTransaction, utxoOutput, pubKey, signature);
return 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()) {
throw new SignatureException("Provided signature is empty.");
}
byte[] signatureEncoded;
try {
signatureEncoded = Base64.getDecoder().decode(signatureBase64);
} catch(IllegalArgumentException e) {
throw new SignatureException("Could not decode base64 signature", e);
}
TransactionWitness witness;
try {
witness = new TransactionWitness(null, signatureEncoded, 0);
} catch(Exception e) {
throw new SignatureException("Provided signature is not a BIP322 simple signature.", e);
}
TransactionSignature signature;
ECKey pubKey;
if(witness.getWitnessScript() != null) {
throw new IllegalArgumentException("Multisig signatures are not supported.");
}
if(witness.getSignatures().isEmpty()) {
throw new SignatureException("BIP322 simple signature contains no transaction signatures.");
}
if(scriptType == ScriptType.P2WPKH) {
signature = witness.getSignatures().get(0);
if(witness.getPushes().size() <= 1) {
throw new SignatureException("BIP322 simple signature for P2WPKH script type does not contain a pubkey.");
}
pubKey = ECKey.fromPublicOnly(witness.getPushes().get(1));
if(!address.equals(scriptType.getAddress(pubKey))) {
throw new SignatureException("Provided address does not match pubkey in signature");
}
} else if(scriptType == ScriptType.P2TR) {
signature = witness.getSignatures().get(0);
pubKey = P2TR.getPublicKeyFromScript(address.getOutputScript());
} else {
throw new SignatureException(scriptType + " addresses are not supported");
}
Transaction toSpend = getBip322ToSpend(address, message);
Transaction toSign = getBip322ToSign(toSpend);
PSBT psbt = new PSBT(toSign);
PSBTInput psbtInput = psbt.getPsbtInputs().get(0);
psbtInput.setWitnessUtxo(toSpend.getOutputs().get(0));
psbtInput.setSigHash(SigHash.ALL);
if(scriptType == ScriptType.P2TR) {
psbtInput.setTapKeyPathSignature(signature);
} else {
psbtInput.getPartialSignatures().put(pubKey, signature);
}
try {
psbt.verifySignatures();
} catch(PSBTSignatureException e) {
return false;
}
return true;
}
private static void checkScriptType(ScriptType scriptType) {
if(!scriptType.isAllowed(PolicyType.SINGLE)) {
throw new UnsupportedOperationException("Only singlesig addresses are currently supported");
}
if(!Arrays.asList(ScriptType.WITNESS_TYPES).contains(scriptType)) {
throw new UnsupportedOperationException("Legacy addresses are not supported for BIP322 simple signatures");
}
if(scriptType == ScriptType.P2SH_P2WPKH) {
throw new UnsupportedOperationException("The P2SH-P2WPKH script type is not currently supported");
}
}
public static boolean isSupported(ScriptType scriptType) {
return scriptType == ScriptType.P2WPKH || scriptType == P2TR;
}
public static Transaction getBip322ToSpend(Address address, String message) {
Transaction toSpend = new Transaction();
toSpend.setVersion(0);
toSpend.setLocktime(0);
List<ScriptChunk> scriptSigChunks = new ArrayList<>();
scriptSigChunks.add(ScriptChunk.fromOpcode(ScriptOpCodes.OP_0));
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.addOutput(0L, address.getOutputScript());
return toSpend;
}
public static Transaction getBip322ToSign(Transaction toSpend) {
Transaction toSign = new Transaction();
toSign.setVersion(0);
toSign.setLocktime(0);
TransactionWitness witness = new TransactionWitness(toSign);
toSign.addInput(toSpend.getTxId(), 0L, new Script(new byte[0]), witness);
toSign.getInputs().get(0).setSequenceNumber(0L);
toSign.addOutput(0, new Script(List.of(ScriptChunk.fromOpcode(ScriptOpCodes.OP_RETURN))));
return toSign;
}
public static byte[] getBip322MessageHash(String message) {
if(message == null) {
throw new IllegalArgumentException("Message cannot be null");
}
return Utils.taggedHash("BIP0322-signed-message", message.getBytes(StandardCharsets.UTF_8));
}
}

View file

@ -576,9 +576,13 @@ public class ECKey {
* @throws IllegalStateException if this ECKey does not have the private part. * @throws IllegalStateException if this ECKey does not have the private part.
*/ */
public String signMessage(String message, ScriptType scriptType) { public String signMessage(String message, ScriptType scriptType) {
return signMessage(message, scriptType, this::signEcdsa);
}
public String signMessage(String message, ScriptType scriptType, ECDSAHashSigner ecdsaHashSigner) {
byte[] data = formatMessageForSigning(message); byte[] data = formatMessageForSigning(message);
Sha256Hash hash = Sha256Hash.of(data); Sha256Hash hash = Sha256Hash.of(data);
ECDSASignature sig = signEcdsa(hash); ECDSASignature sig = ecdsaHashSigner.sign(hash);
byte recId = findRecoveryId(hash, sig); byte recId = findRecoveryId(hash, sig);
int headerByte = recId + getSigningTypeConstant(scriptType); int headerByte = recId + getSigningTypeConstant(scriptType);
byte[] sigData = new byte[65]; // 1 header + 32 bytes for R + 32 bytes for S byte[] sigData = new byte[65]; // 1 header + 32 bytes for R + 32 bytes for S
@ -627,6 +631,14 @@ public class ECKey {
// This is what you get back from Bouncy Castle if base64 doesn't decode :( // This is what you get back from Bouncy Castle if base64 doesn't decode :(
throw new SignatureException("Could not decode base64", e); throw new SignatureException("Could not decode base64", e);
} }
byte[] messageBytes = formatMessageForSigning(message);
// Note that the C++ code doesn't actually seem to specify any character encoding. Presumably it's whatever
// JSON-SPIRIT hands back. Assume UTF-8 for now.
Sha256Hash messageHash = Sha256Hash.twiceOf(messageBytes);
return signedHashToKey(messageHash, signatureEncoded, electrumFormat);
}
public static ECKey signedHashToKey(Sha256Hash messageHash, byte[] signatureEncoded, boolean electrumFormat) throws SignatureException {
// Parse the signature bytes into r/s and the selector value. // Parse the signature bytes into r/s and the selector value.
if(signatureEncoded.length < 65) { if(signatureEncoded.length < 65) {
throw new SignatureException("Signature truncated, expected 65 bytes and got " + signatureEncoded.length); throw new SignatureException("Signature truncated, expected 65 bytes and got " + signatureEncoded.length);
@ -640,10 +652,7 @@ public class ECKey {
BigInteger r = new BigInteger(1, Arrays.copyOfRange(signatureEncoded, 1, 33)); BigInteger r = new BigInteger(1, Arrays.copyOfRange(signatureEncoded, 1, 33));
BigInteger s = new BigInteger(1, Arrays.copyOfRange(signatureEncoded, 33, 65)); BigInteger s = new BigInteger(1, Arrays.copyOfRange(signatureEncoded, 33, 65));
ECDSASignature sig = new ECDSASignature(r, s); ECDSASignature sig = new ECDSASignature(r, s);
byte[] messageBytes = formatMessageForSigning(message);
// Note that the C++ code doesn't actually seem to specify any character encoding. Presumably it's whatever
// JSON-SPIRIT hands back. Assume UTF-8 for now.
Sha256Hash messageHash = Sha256Hash.of(messageBytes);
boolean compressed = false; boolean compressed = false;
if(header >= 39) { // this is a bech32 signature if(header >= 39) { // this is a bech32 signature
header -= 12; header -= 12;
@ -863,4 +872,8 @@ public class ECKey {
throw new RuntimeException(e); // Cannot happen. throw new RuntimeException(e); // Cannot happen.
} }
} }
public interface ECDSAHashSigner {
ECDSASignature sign(Sha256Hash hash);
}
} }

View file

@ -7,25 +7,36 @@ import org.bouncycastle.crypto.params.KeyParameter;
public class Pbkdf2KeyDeriver implements KeyDeriver, AsymmetricKeyDeriver { public class Pbkdf2KeyDeriver implements KeyDeriver, AsymmetricKeyDeriver {
public static final int DEFAULT_ITERATION_COUNT = 1024; public static final int DEFAULT_ITERATION_COUNT = 1024;
public static final int DEFAULT_KEY_SIZE = 512;
private final byte[] salt; private final byte[] salt;
private final int iterationCount; private final int iterationCount;
private final int keySize;
public static final Pbkdf2KeyDeriver DEFAULT_INSTANCE = new Pbkdf2KeyDeriver(); public static final Pbkdf2KeyDeriver DEFAULT_INSTANCE = new Pbkdf2KeyDeriver();
public Pbkdf2KeyDeriver() { public Pbkdf2KeyDeriver() {
this.salt = new byte[0]; this.salt = new byte[0];
this.iterationCount = DEFAULT_ITERATION_COUNT; this.iterationCount = DEFAULT_ITERATION_COUNT;
this.keySize = DEFAULT_KEY_SIZE;
} }
public Pbkdf2KeyDeriver(byte[] salt) { public Pbkdf2KeyDeriver(byte[] salt) {
this.salt = salt; this.salt = salt;
this.iterationCount = DEFAULT_ITERATION_COUNT; this.iterationCount = DEFAULT_ITERATION_COUNT;
this.keySize = DEFAULT_KEY_SIZE;
} }
public Pbkdf2KeyDeriver(byte[] salt, int iterationCount) { public Pbkdf2KeyDeriver(byte[] salt, int iterationCount) {
this.salt = salt; this.salt = salt;
this.iterationCount = iterationCount; this.iterationCount = iterationCount;
this.keySize = DEFAULT_KEY_SIZE;
}
public Pbkdf2KeyDeriver(byte[] salt, int iterationCount, int keySize) {
this.salt = salt;
this.iterationCount = iterationCount;
this.keySize = keySize;
} }
@Override @Override
@ -37,7 +48,7 @@ public class Pbkdf2KeyDeriver implements KeyDeriver, AsymmetricKeyDeriver {
public Key deriveKey(CharSequence password) throws KeyCrypterException { public Key deriveKey(CharSequence password) throws KeyCrypterException {
PKCS5S2ParametersGenerator gen = new PKCS5S2ParametersGenerator(new SHA512Digest()); PKCS5S2ParametersGenerator gen = new PKCS5S2ParametersGenerator(new SHA512Digest());
gen.init(SecureString.toBytesUTF8(password), salt, iterationCount); gen.init(SecureString.toBytesUTF8(password), salt, iterationCount);
byte[] keyBytes = ((KeyParameter)gen.generateDerivedParameters(512)).getKey(); byte[] keyBytes = ((KeyParameter)gen.generateDerivedParameters(keySize)).getKey();
return new Key(keyBytes, salt, getDeriverType()); return new Key(keyBytes, salt, getDeriverType());
} }

View file

@ -6,7 +6,7 @@ import java.util.regex.Pattern;
public class Miniscript { public class Miniscript {
private static final Pattern SINGLE_PATTERN = Pattern.compile("pkh?\\("); private static final Pattern SINGLE_PATTERN = Pattern.compile("pkh?\\(");
private static final Pattern TAPROOT_PATTERN = Pattern.compile("tr\\("); private static final Pattern TAPROOT_PATTERN = Pattern.compile("tr\\(");
private static final Pattern MULTI_PATTERN = Pattern.compile("multi\\(([\\d+])"); private static final Pattern MULTI_PATTERN = Pattern.compile("multi\\((\\d+)");
private String script; private String script;

View file

@ -4,6 +4,8 @@ import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.address.*; import com.sparrowwallet.drongo.address.*;
import com.sparrowwallet.drongo.crypto.ECKey; import com.sparrowwallet.drongo.crypto.ECKey;
import org.bouncycastle.util.encoders.Hex; import org.bouncycastle.util.encoders.Hex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
@ -18,6 +20,8 @@ import static com.sparrowwallet.drongo.protocol.ScriptType.*;
import static com.sparrowwallet.drongo.protocol.ScriptOpCodes.*; import static com.sparrowwallet.drongo.protocol.ScriptOpCodes.*;
public class Script { public class Script {
private static final Logger log = LoggerFactory.getLogger(Script.class);
public static final long MAX_SCRIPT_ELEMENT_SIZE = 520; public static final long MAX_SCRIPT_ELEMENT_SIZE = 520;
// The program is a set of chunks where each element is either [opcode] or [data, data, data ...] // The program is a set of chunks where each element is either [opcode] or [data, data, data ...]
@ -32,7 +36,11 @@ public class Script {
Script(byte[] programBytes, boolean parse) { Script(byte[] programBytes, boolean parse) {
program = programBytes; program = programBytes;
if(parse) { if(parse) {
try {
parse(); parse();
} catch(ProtocolException e) {
log.warn("Invalid script, continuing with already parsed chunks", e);
}
} }
} }
@ -59,16 +67,16 @@ public class Script {
// Read some bytes of data, where how many is the opcode value itself. // Read some bytes of data, where how many is the opcode value itself.
dataToRead = opcode; dataToRead = opcode;
} else if (opcode == OP_PUSHDATA1) { } else if (opcode == OP_PUSHDATA1) {
if (bis.available() < 1) throw new ProtocolException("Unexpected end of script"); if (bis.available() < 1) throw new ProtocolException("Unexpected end of script - OP_PUSHDATA1 was followed by " + bis.available() + " bytes");
dataToRead = bis.read(); dataToRead = bis.read();
} else if (opcode == OP_PUSHDATA2) { } else if (opcode == OP_PUSHDATA2) {
// Read a short, then read that many bytes of data. // Read a short, then read that many bytes of data.
if (bis.available() < 2) throw new ProtocolException("Unexpected end of script"); if (bis.available() < 2) throw new ProtocolException("Unexpected end of script - OP_PUSHDATA2 was followed by only " + bis.available() + " bytes");
dataToRead = Utils.readUint16FromStream(bis); dataToRead = Utils.readUint16FromStream(bis);
} else if (opcode == OP_PUSHDATA4) { } else if (opcode == OP_PUSHDATA4) {
// Read a uint32, then read that many bytes of data. // Read a uint32, then read that many bytes of data.
// Though this is allowed, because its value cannot be > 520, it should never actually be used // Though this is allowed, because its value cannot be > 520, it should never actually be used
if (bis.available() < 4) throw new ProtocolException("Unexpected end of script"); if (bis.available() < 4) throw new ProtocolException("Unexpected end of script - OP_PUSHDATA4 was followed by only " + bis.available() + " bytes");
dataToRead = Utils.readUint32FromStream(bis); dataToRead = Utils.readUint32FromStream(bis);
} }

View file

@ -7,6 +7,8 @@ import org.bouncycastle.util.encoders.Hex;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Arrays; import java.util.Arrays;
import java.util.Objects; import java.util.Objects;
@ -28,7 +30,11 @@ public class ScriptChunk {
} }
public static ScriptChunk fromOpcode(int opcode) { public static ScriptChunk fromOpcode(int opcode) {
return new ScriptChunk(opcode, null); return new ScriptChunk(opcode, opcode == ScriptOpCodes.OP_0 ? new byte[0] : null);
}
public static ScriptChunk fromString(String strData, Charset charset) {
return fromData(strData.getBytes(charset));
} }
public static ScriptChunk fromData(byte[] data) { public static ScriptChunk fromData(byte[] data) {
@ -68,7 +74,7 @@ public class ScriptChunk {
} }
public void write(OutputStream stream) throws IOException { public void write(OutputStream stream) throws IOException {
if (isOpCode()) { if (isOpCode() && opcode != ScriptOpCodes.OP_0) {
if(data != null) throw new IllegalStateException("Data must be null for opcode chunk"); if(data != null) throw new IllegalStateException("Data must be null for opcode chunk");
stream.write(opcode); stream.write(opcode);
} else if (data != null) { } else if (data != null) {
@ -126,6 +132,18 @@ public class ScriptChunk {
} }
} }
public boolean isString() {
if(data == null || data.length == 0) {
return false;
}
if(isSignature() || isPubKey()) {
return false;
}
return Utils.isUtf8(data);
}
public boolean isScript() { public boolean isScript() {
if(data == null || data.length == 0) { if(data == null || data.length == 0) {
return false; return false;
@ -213,6 +231,9 @@ public class ScriptChunk {
if (data.length == 0) { if (data.length == 0) {
return "OP_0"; return "OP_0";
} }
if(Utils.isUtf8(data)) {
return new String(data, StandardCharsets.UTF_8);
}
return Hex.toHexString(data); return Hex.toHexString(data);
} }

View file

@ -1320,7 +1320,7 @@ public enum ScriptType {
//Start with length of output //Start with length of output
int outputVbytes = output.getLength(); int outputVbytes = output.getLength();
//Add length of spending input (with or without discount depending on script type) //Add length of spending input (with or without discount depending on script type)
int inputVbytes = getInputVbytes(); double inputVbytes = getInputVbytes();
//Return fee rate in sats/vByte multiplied by the calculated output and input vByte lengths //Return fee rate in sats/vByte multiplied by the calculated output and input vByte lengths
return (long)(feeRate * outputVbytes + longTermFeeRate * inputVbytes); return (long)(feeRate * outputVbytes + longTermFeeRate * inputVbytes);
@ -1333,17 +1333,17 @@ public enum ScriptType {
* *
* @return The number of vBytes required for an input of this script type * @return The number of vBytes required for an input of this script type
*/ */
public int getInputVbytes() { public double getInputVbytes() {
if(P2SH_P2WPKH.equals(this)) { if(P2SH_P2WPKH.equals(this)) {
return (32 + 4 + 1 + 13 + (107 / WITNESS_SCALE_FACTOR) + 4); return (32 + 4 + 1 + 13 + ((double)107 / WITNESS_SCALE_FACTOR) + 4);
} else if(P2SH_P2WSH.equals(this)) { } else if(P2SH_P2WSH.equals(this)) {
return (32 + 4 + 1 + 35 + (107 / WITNESS_SCALE_FACTOR) + 4); return (32 + 4 + 1 + 35 + ((double)107 / WITNESS_SCALE_FACTOR) + 4);
} else if(P2TR.equals(this)) { } else if(P2TR.equals(this)) {
//Assume a default keypath spend //Assume a default keypath spend
return (32 + 4 + 1 + (66 / WITNESS_SCALE_FACTOR) + 4); return (32 + 4 + 1 + ((double)66 / WITNESS_SCALE_FACTOR) + 4);
} else if(Arrays.asList(WITNESS_TYPES).contains(this)) { } else if(Arrays.asList(WITNESS_TYPES).contains(this)) {
//Return length of spending input with 75% discount to script size //Return length of spending input with 75% discount to script size
return (32 + 4 + 1 + (107 / WITNESS_SCALE_FACTOR) + 4); return (32 + 4 + 1 + ((double)107 / WITNESS_SCALE_FACTOR) + 4);
} else if(Arrays.asList(NON_WITNESS_TYPES).contains(this)) { } else if(Arrays.asList(NON_WITNESS_TYPES).contains(this)) {
//Return length of spending input with no discount //Return length of spending input with no discount
return (32 + 4 + 1 + 107 + 4); return (32 + 4 + 1 + 107 + 4);

View file

@ -61,6 +61,18 @@ public class Sha256Hash implements Comparable<Sha256Hash> {
return wrap(Utils.hexToBytes(hexString)); return wrap(Utils.hexToBytes(hexString));
} }
/**
* Duplicate method with default name for deserialization mappers.
*
* @param hexString a hash value represented as a hex string
* @return a new instance
* @throws IllegalArgumentException if the given string is not a valid
* hex string, or if it does not represent exactly 32 bytes
*/
public static Sha256Hash fromString(String hexString) {
return wrap(hexString);
}
/** /**
* Creates a new instance that wraps the given hash value, but with byte order reversed. * Creates a new instance that wraps the given hash value, but with byte order reversed.
* *

View file

@ -8,10 +8,7 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.math.BigInteger; import java.math.BigInteger;
import java.util.ArrayList; import java.util.*;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import static com.sparrowwallet.drongo.Utils.uint32ToByteStreamLE; import static com.sparrowwallet.drongo.Utils.uint32ToByteStreamLE;
import static com.sparrowwallet.drongo.Utils.uint64ToByteStreamLE; import static com.sparrowwallet.drongo.Utils.uint64ToByteStreamLE;
@ -634,6 +631,9 @@ public class Transaction extends ChildMessage {
if(spentUtxos.size() != getInputs().size()) { if(spentUtxos.size() != getInputs().size()) {
throw new IllegalArgumentException("Provided spent UTXOs length does not equal the number of transaction inputs"); throw new IllegalArgumentException("Provided spent UTXOs length does not equal the number of transaction inputs");
} }
if(spentUtxos.stream().anyMatch(Objects::isNull)) {
throw new IllegalArgumentException("Not all spent UTXOs are provided");
}
if(inputIndex >= getInputs().size()) { if(inputIndex >= getInputs().size()) {
throw new IllegalArgumentException("Input index is greater than the number of transaction inputs"); throw new IllegalArgumentException("Input index is greater than the number of transaction inputs");
} }
@ -685,7 +685,7 @@ public class Transaction extends ChildMessage {
if(anyoneCanPay) { if(anyoneCanPay) {
getInputs().get(inputIndex).getOutpoint().bitcoinSerializeToStream(bos); getInputs().get(inputIndex).getOutpoint().bitcoinSerializeToStream(bos);
Utils.uint32ToByteStreamLE(spentUtxos.get(inputIndex).getValue(), bos); Utils.int64ToByteStreamLE(spentUtxos.get(inputIndex).getValue(), bos);
byteArraySerialize(spentUtxos.get(inputIndex).getScriptBytes(), bos); byteArraySerialize(spentUtxos.get(inputIndex).getScriptBytes(), bos);
Utils.uint32ToByteStreamLE(getInputs().get(inputIndex).getSequenceNumber(), bos); Utils.uint32ToByteStreamLE(getInputs().get(inputIndex).getSequenceNumber(), bos);
} else { } else {

View file

@ -103,7 +103,7 @@ public class TransactionSignature {
} }
public static TransactionSignature decodeFromBitcoin(byte[] bytes, boolean requireCanonicalEncoding) throws SignatureDecodeException { public static TransactionSignature decodeFromBitcoin(byte[] bytes, boolean requireCanonicalEncoding) throws SignatureDecodeException {
if(bytes.length == 64) { if(bytes.length == 64 || bytes.length == 65) {
return decodeFromBitcoin(Type.SCHNORR, bytes, requireCanonicalEncoding); return decodeFromBitcoin(Type.SCHNORR, bytes, requireCanonicalEncoding);
} }

View file

@ -461,10 +461,10 @@ public class PSBT {
} }
public byte[] serialize() { public byte[] serialize() {
return serialize(true); return serialize(true, true);
} }
public byte[] serialize(boolean includeXpubs) { public byte[] serialize(boolean includeXpubs, boolean includeNonWitnessUtxos) {
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream();
baos.writeBytes(Utils.hexToBytes(PSBT_MAGIC_HEX)); baos.writeBytes(Utils.hexToBytes(PSBT_MAGIC_HEX));
@ -481,8 +481,9 @@ public class PSBT {
for(PSBTInput psbtInput : getPsbtInputs()) { for(PSBTInput psbtInput : getPsbtInputs()) {
List<PSBTEntry> inputEntries = psbtInput.getInputEntries(); List<PSBTEntry> inputEntries = psbtInput.getInputEntries();
for(PSBTEntry entry : inputEntries) { for(PSBTEntry entry : inputEntries) {
if(includeXpubs || (entry.getKeyType() != PSBT_IN_BIP32_DERIVATION && entry.getKeyType() != PSBT_IN_PROPRIETARY if((includeXpubs || (entry.getKeyType() != PSBT_IN_BIP32_DERIVATION && entry.getKeyType() != PSBT_IN_PROPRIETARY
&& entry.getKeyType() != PSBT_IN_TAP_INTERNAL_KEY && entry.getKeyType() != PSBT_IN_TAP_BIP32_DERIVATION)) { && entry.getKeyType() != PSBT_IN_TAP_INTERNAL_KEY && entry.getKeyType() != PSBT_IN_TAP_BIP32_DERIVATION))
&& (includeNonWitnessUtxos || entry.getKeyType() != PSBT_IN_NON_WITNESS_UTXO)) {
entry.serializeToStream(baos); entry.serializeToStream(baos);
} }
} }
@ -630,7 +631,7 @@ public class PSBT {
} }
public String toBase64String(boolean includeXpubs) { public String toBase64String(boolean includeXpubs) {
return Base64.toBase64String(serialize(includeXpubs)); return Base64.toBase64String(serialize(includeXpubs, true));
} }
public static boolean isPSBT(byte[] b) { public static boolean isPSBT(byte[] b) {

View file

@ -188,7 +188,7 @@ public class PSBTEntry {
psbtByteBuffer.get(buf); psbtByteBuffer.get(buf);
ByteBuffer byteBuffer = ByteBuffer.wrap(buf); ByteBuffer byteBuffer = ByteBuffer.wrap(buf);
byteBuffer.order(ByteOrder.LITTLE_ENDIAN); byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
return byteBuffer.getShort(); return Short.toUnsignedInt(byteBuffer.getShort());
} }
case (byte) 0xfe: { case (byte) 0xfe: {
byte[] buf = new byte[4]; byte[] buf = new byte[4];

View file

@ -519,6 +519,20 @@ public class PSBTInput {
} }
public boolean sign(ECKey privKey) { public boolean sign(ECKey privKey) {
return sign(new PSBTInputSigner() {
@Override
public TransactionSignature sign(Sha256Hash hash, SigHash sigHash, TransactionSignature.Type signatureType) {
return privKey.sign(hash, sigHash, signatureType);
}
@Override
public ECKey getPubKey() {
return ECKey.fromPublicOnly(privKey);
}
});
}
public boolean sign(PSBTInputSigner psbtInputSigner) {
SigHash localSigHash = getSigHash(); SigHash localSigHash = getSigHash();
if(localSigHash == null) { if(localSigHash == null) {
localSigHash = getDefaultSigHash(); localSigHash = getDefaultSigHash();
@ -529,12 +543,12 @@ public class PSBTInput {
if(signingScript != null) { if(signingScript != null) {
Sha256Hash hash = getHashForSignature(signingScript, localSigHash); Sha256Hash hash = getHashForSignature(signingScript, localSigHash);
TransactionSignature.Type type = isTaproot() ? SCHNORR : ECDSA; TransactionSignature.Type type = isTaproot() ? SCHNORR : ECDSA;
TransactionSignature transactionSignature = privKey.sign(hash, localSigHash, type); TransactionSignature transactionSignature = psbtInputSigner.sign(hash, localSigHash, type);
if(type == SCHNORR) { if(type == SCHNORR) {
tapKeyPathSignature = transactionSignature; tapKeyPathSignature = transactionSignature;
} else { } else {
ECKey pubKey = ECKey.fromPublicOnly(privKey); ECKey pubKey = psbtInputSigner.getPubKey();
getPartialSignatures().put(pubKey, transactionSignature); getPartialSignatures().put(pubKey, transactionSignature);
} }

View file

@ -0,0 +1,11 @@
package com.sparrowwallet.drongo.psbt;
import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.protocol.SigHash;
import com.sparrowwallet.drongo.protocol.TransactionSignature;
public interface PSBTInputSigner {
TransactionSignature sign(Sha256Hash hash, SigHash sigHash, TransactionSignature.Type signatureType);
ECKey getPubKey();
}

View file

@ -229,4 +229,85 @@ public class Bip39MnemonicCode {
bits[(i * 8) + j] = (data[i] & (1 << (7 - j))) != 0; bits[(i * 8) + j] = (data[i] & (1 << (7 - j))) != 0;
return bits; return bits;
} }
public List<String> getPossibleLastWords(List<String> previousWords) throws MnemonicException.MnemonicLengthException, MnemonicException.MnemonicWordException {
if((previousWords.size() + 1) % 3 > 0) {
throw new MnemonicException.MnemonicLengthException("Previous word list size must be multiple of three words, less one.");
}
// Look up all the words in the list and construct the
// concatenation of the original entropy and the checksum.
//
int concatLenBits = previousWords.size() * 11;
boolean[] concatBits = new boolean[concatLenBits];
int wordindex = 0;
for (String word : previousWords) {
// Find the words index in the wordlist.
int ndx = Collections.binarySearch(this.wordList, word);
if (ndx < 0) {
throw new MnemonicException.MnemonicWordException(word);
}
// Set the next 11 bits to the value of the index.
for (int ii = 0; ii < 11; ++ii) {
concatBits[(wordindex * 11) + ii] = (ndx & (1 << (10 - ii))) != 0;
}
++wordindex;
}
int checksumLengthBits = (concatLenBits + 11) / 33;
int entropyLengthBits = (concatLenBits + 11) - checksumLengthBits;
int varyingLengthBits = entropyLengthBits - concatLenBits;
boolean[][] bitPermutations = getBitPermutations(varyingLengthBits);
ArrayList<String> possibleWords = new ArrayList<>();
for(boolean[] bitPermutation : bitPermutations) {
boolean[] entropyBits = new boolean[concatLenBits + varyingLengthBits];
System.arraycopy(concatBits, 0, entropyBits, 0, concatBits.length);
System.arraycopy(bitPermutation, 0, entropyBits, concatBits.length, varyingLengthBits);
byte[] entropy = new byte[entropyLengthBits / 8];
for(int ii = 0; ii < entropy.length; ++ii) {
for(int jj = 0; jj < 8; ++jj) {
if(entropyBits[(ii * 8) + jj]) {
entropy[ii] |= 1 << (7 - jj);
}
}
}
byte[] hash = Sha256Hash.hash(entropy);
boolean[] hashBits = bytesToBits(hash);
boolean[] wordBits = new boolean[11];
System.arraycopy(bitPermutation, 0, wordBits, 0, varyingLengthBits);
System.arraycopy(hashBits, 0, wordBits, varyingLengthBits, checksumLengthBits);
int index = 0;
for(int j = 0; j < 11; ++j) {
index <<= 1;
if(wordBits[j]) {
index |= 0x1;
}
}
possibleWords.add(this.wordList.get(index));
}
Collections.sort(possibleWords);
return possibleWords;
}
public static boolean[][] getBitPermutations(int length) {
int numPermutations = (int) Math.pow(2, length);
boolean[][] permutations = new boolean[numPermutations][length];
for (int i = 0; i < numPermutations; i++) {
for (int j = 0; j < length; j++) {
permutations[i][j] = ((i >> j) & 1) == 1;
}
}
return permutations;
}
} }

View file

@ -2,10 +2,10 @@ package com.sparrowwallet.drongo.wallet;
import com.sparrowwallet.drongo.protocol.Transaction; import com.sparrowwallet.drongo.protocol.Transaction;
public class CoinbaseUtxoFilter implements UtxoFilter { public class CoinbaseTxoFilter implements TxoFilter {
private final Wallet wallet; private final Wallet wallet;
public CoinbaseUtxoFilter(Wallet wallet) { public CoinbaseTxoFilter(Wallet wallet) {
this.wallet = wallet; this.wallet = wallet;
} }

View file

@ -0,0 +1,31 @@
package com.sparrowwallet.drongo.wallet;
import java.util.ArrayList;
import java.util.Collection;
public class ExcludeTxoFilter implements TxoFilter {
private final Collection<BlockTransactionHashIndex> excludedTxos;
public ExcludeTxoFilter() {
this.excludedTxos = new ArrayList<>();
}
public ExcludeTxoFilter(Collection<BlockTransactionHashIndex> excludedTxos) {
this.excludedTxos = new ArrayList<>(excludedTxos);
}
@Override
public boolean isEligible(BlockTransactionHashIndex candidate) {
for(BlockTransactionHashIndex excludedTxo : excludedTxos) {
if(candidate.getHash().equals(excludedTxo.getHash()) && candidate.getIndex() == excludedTxo.getIndex()) {
return false;
}
}
return true;
}
public Collection<BlockTransactionHashIndex> getExcludedTxos() {
return excludedTxos;
}
}

View file

@ -1,31 +0,0 @@
package com.sparrowwallet.drongo.wallet;
import java.util.ArrayList;
import java.util.Collection;
public class ExcludeUtxoFilter implements UtxoFilter {
private final Collection<BlockTransactionHashIndex> excludedUtxos;
public ExcludeUtxoFilter() {
this.excludedUtxos = new ArrayList<>();
}
public ExcludeUtxoFilter(Collection<BlockTransactionHashIndex> excludedUtxos) {
this.excludedUtxos = new ArrayList<>(excludedUtxos);
}
@Override
public boolean isEligible(BlockTransactionHashIndex candidate) {
for(BlockTransactionHashIndex excludedUtxo : excludedUtxos) {
if(candidate.getHash().equals(excludedUtxo.getHash()) && candidate.getIndex() == excludedUtxo.getIndex()) {
return false;
}
}
return true;
}
public Collection<BlockTransactionHashIndex> getExcludedUtxos() {
return excludedUtxos;
}
}

View file

@ -1,6 +1,6 @@
package com.sparrowwallet.drongo.wallet; package com.sparrowwallet.drongo.wallet;
public class FrozenUtxoFilter implements UtxoFilter { public class FrozenTxoFilter implements TxoFilter {
@Override @Override
public boolean isEligible(BlockTransactionHashIndex candidate) { public boolean isEligible(BlockTransactionHashIndex candidate) {
return candidate.getStatus() == null || candidate.getStatus() != Status.FROZEN; return candidate.getStatus() == null || candidate.getStatus() != Status.FROZEN;

View file

@ -1,6 +1,8 @@
package com.sparrowwallet.drongo.wallet; package com.sparrowwallet.drongo.wallet;
public class InsufficientFundsException extends Exception { public class InsufficientFundsException extends Exception {
private Long targetValue;
public InsufficientFundsException() { public InsufficientFundsException() {
super(); super();
} }
@ -8,4 +10,13 @@ public class InsufficientFundsException extends Exception {
public InsufficientFundsException(String msg) { public InsufficientFundsException(String msg) {
super(msg); super(msg);
} }
public InsufficientFundsException(String message, Long targetValue) {
super(message);
this.targetValue = targetValue;
}
public Long getTargetValue() {
return targetValue;
}
} }

View file

@ -1,22 +1,30 @@
package com.sparrowwallet.drongo.wallet; package com.sparrowwallet.drongo.wallet;
import java.util.ArrayList; import java.util.*;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class PresetUtxoSelector extends SingleSetUtxoSelector { public class PresetUtxoSelector extends SingleSetUtxoSelector {
private final Collection<BlockTransactionHashIndex> presetUtxos; private final Collection<BlockTransactionHashIndex> presetUtxos;
private final Collection<BlockTransactionHashIndex> excludedUtxos;
private final boolean maintainOrder; private final boolean maintainOrder;
private final boolean requireAll;
public PresetUtxoSelector(Collection<BlockTransactionHashIndex> presetUtxos) { public PresetUtxoSelector(Collection<BlockTransactionHashIndex> presetUtxos) {
this.presetUtxos = presetUtxos; this(presetUtxos, new ArrayList<>());
this.maintainOrder = false;
} }
public PresetUtxoSelector(Collection<BlockTransactionHashIndex> presetUtxos, boolean maintainOrder) { public PresetUtxoSelector(Collection<BlockTransactionHashIndex> presetUtxos, Collection<BlockTransactionHashIndex> excludedUtxos) {
this.presetUtxos = presetUtxos; this.presetUtxos = presetUtxos;
this.excludedUtxos = excludedUtxos;
this.maintainOrder = false;
this.requireAll = false;
}
public PresetUtxoSelector(Collection<BlockTransactionHashIndex> presetUtxos, boolean maintainOrder, boolean requireAll) {
this.presetUtxos = presetUtxos;
this.excludedUtxos = new ArrayList<>();
this.maintainOrder = maintainOrder; this.maintainOrder = maintainOrder;
this.requireAll = requireAll;
} }
@Override @Override
@ -33,8 +41,11 @@ public class PresetUtxoSelector extends SingleSetUtxoSelector {
} }
} }
if(maintainOrder && utxos.containsAll(presetUtxos)) { Set<BlockTransactionHashIndex> utxosSet = new HashSet<>(utxos);
if(maintainOrder && utxosSet.containsAll(presetUtxos)) {
return presetUtxos; return presetUtxos;
} else if(requireAll && !utxosSet.containsAll(presetUtxos)) {
return Collections.emptyList();
} }
return utxos; return utxos;
@ -44,6 +55,17 @@ public class PresetUtxoSelector extends SingleSetUtxoSelector {
return presetUtxos; return presetUtxos;
} }
public Collection<BlockTransactionHashIndex> getExcludedUtxos() {
return excludedUtxos;
}
public TxoFilter asExcludeTxoFilter() {
List<BlockTransactionHashIndex> utxos = new ArrayList<>();
utxos.addAll(presetUtxos);
utxos.addAll(excludedUtxos);
return new ExcludeTxoFilter(utxos);
}
@Override @Override
public boolean shuffleInputs() { public boolean shuffleInputs() {
return !maintainOrder; return !maintainOrder;

View file

@ -31,6 +31,13 @@ public class SeedQR {
} }
public static DeterministicSeed getSeed(byte[] compactSeedQr) { public static DeterministicSeed getSeed(byte[] compactSeedQr) {
byte[] seed;
if(compactSeedQr.length == 16 || compactSeedQr.length == 32) {
//Assume scan contains seed only
seed = compactSeedQr;
} else {
//Assume scan contains header, seed and EC bytes
if(compactSeedQr[0] != 0x41 && compactSeedQr[0] != 0x42) { if(compactSeedQr[0] != 0x41 && compactSeedQr[0] != 0x42) {
throw new IllegalArgumentException("Invalid CompactSeedQR header"); throw new IllegalArgumentException("Invalid CompactSeedQR header");
} }
@ -41,13 +48,16 @@ public class SeedQR {
String qrHex = Utils.bytesToHex(compactSeedQr); String qrHex = Utils.bytesToHex(compactSeedQr);
String seedHex; String seedHex;
if(qrHex.endsWith("0ec")) { if(qrHex.endsWith("0ec11ec11")) {
seedHex = qrHex.substring(3, qrHex.length() - 3); seedHex = qrHex.substring(3, qrHex.length() - 9); //12 word, high EC
} else if(qrHex.endsWith("0ec")) {
seedHex = qrHex.substring(3, qrHex.length() - 3); //12 word, low EC
} else { } else {
seedHex = qrHex.substring(3, qrHex.length() - 1); seedHex = qrHex.substring(3, qrHex.length() - 1); //24 word
} }
byte[] seed = Utils.hexToBytes(seedHex); seed = Utils.hexToBytes(seedHex);
}
if(seed.length < 16 || seed.length > 32 || seed.length % 4 > 0) { if(seed.length < 16 || seed.length > 32 || seed.length % 4 > 0) {
throw new IllegalArgumentException("Invalid CompactSeedQR length: " + compactSeedQr.length); throw new IllegalArgumentException("Invalid CompactSeedQR length: " + compactSeedQr.length);

View file

@ -0,0 +1,24 @@
package com.sparrowwallet.drongo.wallet;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
public class SpentTxoFilter implements TxoFilter {
private final Sha256Hash replacedTxid;
public SpentTxoFilter() {
replacedTxid = null;
}
public SpentTxoFilter(Sha256Hash replacedTxid) {
this.replacedTxid = replacedTxid;
}
@Override
public boolean isEligible(BlockTransactionHashIndex candidate) {
return !isSpentOrReplaced(candidate);
}
private boolean isSpentOrReplaced(BlockTransactionHashIndex candidate) {
return candidate.getHash().equals(replacedTxid) || (candidate.isSpent() && !candidate.getSpentBy().getHash().equals(replacedTxid));
}
}

View file

@ -1,5 +1,5 @@
package com.sparrowwallet.drongo.wallet; package com.sparrowwallet.drongo.wallet;
public interface UtxoFilter { public interface TxoFilter {
boolean isEligible(BlockTransactionHashIndex candidate); boolean isEligible(BlockTransactionHashIndex candidate);
} }

View file

@ -799,30 +799,38 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
} }
public Map<BlockTransactionHashIndex, WalletNode> getWalletUtxos() { public Map<BlockTransactionHashIndex, WalletNode> getWalletUtxos() {
return getWalletUtxos(false); return getWalletTxos(List.of(new SpentTxoFilter()));
} }
public Map<BlockTransactionHashIndex, WalletNode> getWalletUtxos(boolean includeSpentMempoolOutputs) { public Map<BlockTransactionHashIndex, WalletNode> getSpendableUtxos() {
Map<BlockTransactionHashIndex, WalletNode> walletUtxos = new TreeMap<>(); return getWalletTxos(List.of(new SpentTxoFilter(), new FrozenTxoFilter(), new CoinbaseTxoFilter(this)));
}
public Map<BlockTransactionHashIndex, WalletNode> getSpendableUtxos(BlockTransaction replacedTransaction) {
return getWalletTxos(List.of(new SpentTxoFilter(replacedTransaction == null ? null : replacedTransaction.getHash()), new FrozenTxoFilter(), new CoinbaseTxoFilter(this)));
}
public Map<BlockTransactionHashIndex, WalletNode> getWalletTxos(Collection<TxoFilter> txoFilters) {
Map<BlockTransactionHashIndex, WalletNode> walletTxos = new TreeMap<>();
for(KeyPurpose keyPurpose : getWalletKeyPurposes()) { for(KeyPurpose keyPurpose : getWalletKeyPurposes()) {
getWalletUtxos(walletUtxos, getNode(keyPurpose), includeSpentMempoolOutputs); getWalletTxos(walletTxos, getNode(keyPurpose), txoFilters);
} }
for(Wallet childWallet : getChildWallets()) { for(Wallet childWallet : getChildWallets()) {
if(childWallet.isNested()) { if(childWallet.isNested()) {
for(KeyPurpose keyPurpose : childWallet.getWalletKeyPurposes()) { for(KeyPurpose keyPurpose : childWallet.getWalletKeyPurposes()) {
getWalletUtxos(walletUtxos, childWallet.getNode(keyPurpose), includeSpentMempoolOutputs); getWalletTxos(walletTxos, childWallet.getNode(keyPurpose), txoFilters);
} }
} }
} }
return walletUtxos; return walletTxos;
} }
private void getWalletUtxos(Map<BlockTransactionHashIndex, WalletNode> walletUtxos, WalletNode purposeNode, boolean includeSpentMempoolOutputs) { private void getWalletTxos(Map<BlockTransactionHashIndex, WalletNode> walletTxos, WalletNode purposeNode, Collection<TxoFilter> txoFilters) {
for(WalletNode addressNode : purposeNode.getChildren()) { for(WalletNode addressNode : purposeNode.getChildren()) {
for(BlockTransactionHashIndex utxo : addressNode.getUnspentTransactionOutputs(includeSpentMempoolOutputs)) { for(BlockTransactionHashIndex utxo : addressNode.getTransactionOutputs(txoFilters)) {
walletUtxos.put(utxo, addressNode); walletTxos.put(utxo, addressNode);
} }
} }
} }
@ -981,29 +989,30 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
return getFee(changeOutput, feeRate, longTermFeeRate); return getFee(changeOutput, feeRate, longTermFeeRate);
} }
public WalletTransaction createWalletTransaction(List<UtxoSelector> utxoSelectors, List<UtxoFilter> utxoFilters, List<Payment> payments, List<byte[]> opReturns, Set<WalletNode> excludedChangeNodes, double feeRate, double longTermFeeRate, Long fee, Integer currentBlockHeight, boolean groupByAddress, boolean includeMempoolOutputs, boolean includeSpentMempoolOutputs) throws InsufficientFundsException { public WalletTransaction createWalletTransaction(List<UtxoSelector> utxoSelectors, List<TxoFilter> txoFilters, List<Payment> payments, List<byte[]> opReturns, Set<WalletNode> excludedChangeNodes, double feeRate, double longTermFeeRate, Long fee, Integer currentBlockHeight, boolean groupByAddress, boolean includeMempoolOutputs) throws InsufficientFundsException {
boolean sendMax = payments.stream().anyMatch(Payment::isSendMax); boolean sendMax = payments.stream().anyMatch(Payment::isSendMax);
long totalPaymentAmount = payments.stream().map(Payment::getAmount).mapToLong(v -> v).sum(); long totalPaymentAmount = payments.stream().map(Payment::getAmount).mapToLong(v -> v).sum();
long totalUtxoValue = getWalletUtxos().keySet().stream().mapToLong(BlockTransactionHashIndex::getValue).sum(); Map<BlockTransactionHashIndex, WalletNode> availableTxos = getWalletTxos(txoFilters);
long totalAvailableValue = availableTxos.keySet().stream().mapToLong(BlockTransactionHashIndex::getValue).sum();
if(fee != null && feeRate != Transaction.DEFAULT_MIN_RELAY_FEE) { if(fee != null && feeRate != Transaction.DEFAULT_MIN_RELAY_FEE) {
throw new IllegalArgumentException("Use an input fee rate of 1 sat/vB when using a defined fee amount so UTXO selectors overestimate effective value"); throw new IllegalArgumentException("Use an input fee rate of 1 sat/vB when using a defined fee amount so UTXO selectors overestimate effective value");
} }
long maxSpendableAmt = getMaxSpendable(payments.stream().map(Payment::getAddress).collect(Collectors.toList()), feeRate, includeSpentMempoolOutputs); long maxSpendableAmt = getMaxSpendable(payments.stream().map(Payment::getAddress).collect(Collectors.toList()), feeRate, availableTxos);
if(maxSpendableAmt < 0) { if(maxSpendableAmt < 0) {
throw new InsufficientFundsException("Not enough combined value in all available UTXOs to send a transaction to the provided addresses at this fee rate"); throw new InsufficientFundsException("Not enough combined value in all available UTXOs to send a transaction to the provided addresses at this fee rate");
} }
//When a user fee is set, we can calculate the fees to spend all UTXOs because we assume all UTXOs are spendable at a fee rate of 1 sat/vB //When a user fee is set, we can calculate the fees to spend all UTXOs because we assume all UTXOs are spendable at a fee rate of 1 sat/vB
//We can then add the user set fee less this amount as a "phantom payment amount" to the value required to find (which cannot include transaction fees) //We can then add the user set fee less this amount as a "phantom payment amount" to the value required to find (which cannot include transaction fees)
long valueRequiredAmt = totalPaymentAmount + (fee != null ? fee - (totalUtxoValue - maxSpendableAmt) : 0); long valueRequiredAmt = totalPaymentAmount + (fee != null ? fee - (totalAvailableValue - maxSpendableAmt) : 0);
if(maxSpendableAmt < valueRequiredAmt) { if(maxSpendableAmt < valueRequiredAmt) {
throw new InsufficientFundsException("Not enough combined value in all available UTXOs to send a transaction to send the provided payments at the user set fee" + (fee == null ? " rate" : "")); throw new InsufficientFundsException("Not enough combined value in all available UTXOs to send a transaction to send the provided payments at the user set fee" + (fee == null ? " rate" : ""));
} }
while(true) { while(true) {
List<Map<BlockTransactionHashIndex, WalletNode>> selectedUtxoSets = selectInputSets(utxoSelectors, utxoFilters, valueRequiredAmt, feeRate, longTermFeeRate, groupByAddress, includeMempoolOutputs, includeSpentMempoolOutputs, sendMax); List<Map<BlockTransactionHashIndex, WalletNode>> selectedUtxoSets = selectInputSets(availableTxos, utxoSelectors, txoFilters, valueRequiredAmt, feeRate, longTermFeeRate, groupByAddress, includeMempoolOutputs, sendMax);
Map<BlockTransactionHashIndex, WalletNode> selectedUtxos = new LinkedHashMap<>(); Map<BlockTransactionHashIndex, WalletNode> selectedUtxos = new LinkedHashMap<>();
selectedUtxoSets.forEach(selectedUtxos::putAll); selectedUtxoSets.forEach(selectedUtxos::putAll);
long totalSelectedAmt = selectedUtxos.keySet().stream().mapToLong(BlockTransactionHashIndex::getValue).sum(); long totalSelectedAmt = selectedUtxos.keySet().stream().mapToLong(BlockTransactionHashIndex::getValue).sum();
@ -1076,7 +1085,7 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
if(differenceAmt < noChangeFeeRequiredAmt) { if(differenceAmt < noChangeFeeRequiredAmt) {
valueRequiredAmt = totalSelectedAmt + 1; valueRequiredAmt = totalSelectedAmt + 1;
//If we haven't selected all UTXOs yet, don't require more than the max spendable amount //If we haven't selected all UTXOs yet, don't require more than the max spendable amount
if(valueRequiredAmt > maxSpendableAmt && transaction.getInputs().size() < getWalletUtxos().size()) { if(valueRequiredAmt > maxSpendableAmt && transaction.getInputs().size() < availableTxos.size()) {
valueRequiredAmt = maxSpendableAmt; valueRequiredAmt = maxSpendableAmt;
} }
@ -1180,8 +1189,8 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
} }
} }
private List<Map<BlockTransactionHashIndex, WalletNode>> selectInputSets(List<UtxoSelector> utxoSelectors, List<UtxoFilter> utxoFilters, Long targetValue, double feeRate, double longTermFeeRate, boolean groupByAddress, boolean includeMempoolOutputs, boolean includeSpentMempoolOutputs, boolean sendMax) throws InsufficientFundsException { private List<Map<BlockTransactionHashIndex, WalletNode>> selectInputSets(Map<BlockTransactionHashIndex, WalletNode> availableTxos, List<UtxoSelector> utxoSelectors, List<TxoFilter> txoFilters, Long targetValue, double feeRate, double longTermFeeRate, boolean groupByAddress, boolean includeMempoolOutputs, boolean sendMax) throws InsufficientFundsException {
List<OutputGroup> utxoPool = getGroupedUtxos(utxoFilters, feeRate, longTermFeeRate, groupByAddress, includeSpentMempoolOutputs); List<OutputGroup> utxoPool = getGroupedUtxos(txoFilters, feeRate, longTermFeeRate, groupByAddress);
List<OutputGroup.Filter> filters = new ArrayList<>(); List<OutputGroup.Filter> filters = new ArrayList<>();
filters.add(new OutputGroup.Filter(1, 6, false)); filters.add(new OutputGroup.Filter(1, 6, false));
@ -1204,7 +1213,6 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
List<Collection<BlockTransactionHashIndex>> selectedInputSets = utxoSelector.selectSets(targetValue, filteredPool); List<Collection<BlockTransactionHashIndex>> selectedInputSets = utxoSelector.selectSets(targetValue, filteredPool);
List<Map<BlockTransactionHashIndex, WalletNode>> selectedInputSetsList = new ArrayList<>(); List<Map<BlockTransactionHashIndex, WalletNode>> selectedInputSetsList = new ArrayList<>();
long total = 0; long total = 0;
Map<BlockTransactionHashIndex, WalletNode> utxos = getWalletUtxos(includeSpentMempoolOutputs);
for(Collection<BlockTransactionHashIndex> selectedInputs : selectedInputSets) { for(Collection<BlockTransactionHashIndex> selectedInputs : selectedInputSets) {
total += selectedInputs.stream().mapToLong(BlockTransactionHashIndex::getValue).sum(); total += selectedInputs.stream().mapToLong(BlockTransactionHashIndex::getValue).sum();
Map<BlockTransactionHashIndex, WalletNode> selectedInputsMap = new LinkedHashMap<>(); Map<BlockTransactionHashIndex, WalletNode> selectedInputsMap = new LinkedHashMap<>();
@ -1213,7 +1221,7 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
Collections.shuffle(shuffledInputs); Collections.shuffle(shuffledInputs);
} }
for(BlockTransactionHashIndex shuffledInput : shuffledInputs) { for(BlockTransactionHashIndex shuffledInput : shuffledInputs) {
selectedInputsMap.put(shuffledInput, utxos.get(shuffledInput)); selectedInputsMap.put(shuffledInput, availableTxos.get(shuffledInput));
} }
selectedInputSetsList.add(selectedInputsMap); selectedInputSetsList.add(selectedInputsMap);
} }
@ -1224,21 +1232,21 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
} }
} }
throw new InsufficientFundsException("Not enough combined value in UTXOs for output value " + targetValue); throw new InsufficientFundsException("Not enough combined value in UTXOs for output value " + targetValue, targetValue);
} }
private List<OutputGroup> getGroupedUtxos(List<UtxoFilter> utxoFilters, double feeRate, double longTermFeeRate, boolean groupByAddress, boolean includeSpentMempoolOutputs) { public List<OutputGroup> getGroupedUtxos(List<TxoFilter> txoFilters, double feeRate, double longTermFeeRate, boolean groupByAddress) {
List<OutputGroup> outputGroups = new ArrayList<>(); List<OutputGroup> outputGroups = new ArrayList<>();
Map<Sha256Hash, BlockTransaction> walletTransactions = getWalletTransactions(); Map<Sha256Hash, BlockTransaction> walletTransactions = getWalletTransactions();
Map<BlockTransactionHashIndex, WalletNode> walletTxos = getWalletTxos(); Map<BlockTransactionHashIndex, WalletNode> walletTxos = getWalletTxos();
for(KeyPurpose keyPurpose : getWalletKeyPurposes()) { for(KeyPurpose keyPurpose : getWalletKeyPurposes()) {
getGroupedUtxos(outputGroups, getNode(keyPurpose), utxoFilters, walletTransactions, walletTxos, feeRate, longTermFeeRate, groupByAddress, includeSpentMempoolOutputs); getGroupedUtxos(outputGroups, getNode(keyPurpose), txoFilters, walletTransactions, walletTxos, feeRate, longTermFeeRate, groupByAddress);
} }
for(Wallet childWallet : getChildWallets()) { for(Wallet childWallet : getChildWallets()) {
if(childWallet.isNested()) { if(childWallet.isNested()) {
for(KeyPurpose keyPurpose : childWallet.getWalletKeyPurposes()) { for(KeyPurpose keyPurpose : childWallet.getWalletKeyPurposes()) {
childWallet.getGroupedUtxos(outputGroups, childWallet.getNode(keyPurpose), utxoFilters, walletTransactions, walletTxos, feeRate, longTermFeeRate, groupByAddress, includeSpentMempoolOutputs); childWallet.getGroupedUtxos(outputGroups, childWallet.getNode(keyPurpose), txoFilters, walletTransactions, walletTxos, feeRate, longTermFeeRate, groupByAddress);
} }
} }
} }
@ -1246,16 +1254,11 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
return outputGroups; return outputGroups;
} }
private void getGroupedUtxos(List<OutputGroup> outputGroups, WalletNode purposeNode, List<UtxoFilter> utxoFilters, Map<Sha256Hash, BlockTransaction> walletTransactions, Map<BlockTransactionHashIndex, WalletNode> walletTxos, double feeRate, double longTermFeeRate, boolean groupByAddress, boolean includeSpentMempoolOutputs) { private void getGroupedUtxos(List<OutputGroup> outputGroups, WalletNode purposeNode, List<TxoFilter> txoFilters, Map<Sha256Hash, BlockTransaction> walletTransactions, Map<BlockTransactionHashIndex, WalletNode> walletTxos, double feeRate, double longTermFeeRate, boolean groupByAddress) {
int inputWeightUnits = getInputWeightUnits(); int inputWeightUnits = getInputWeightUnits();
for(WalletNode addressNode : purposeNode.getChildren()) { for(WalletNode addressNode : purposeNode.getChildren()) {
OutputGroup outputGroup = null; OutputGroup outputGroup = null;
for(BlockTransactionHashIndex utxo : addressNode.getUnspentTransactionOutputs(includeSpentMempoolOutputs)) { for(BlockTransactionHashIndex utxo : addressNode.getTransactionOutputs(txoFilters)) {
Optional<UtxoFilter> matchedFilter = utxoFilters.stream().filter(utxoFilter -> !utxoFilter.isEligible(utxo)).findAny();
if(matchedFilter.isPresent()) {
continue;
}
if(outputGroup == null || !groupByAddress) { if(outputGroup == null || !groupByAddress) {
outputGroup = new OutputGroup(addressNode.getWallet().getScriptType(), getStoredBlockHeight(), inputWeightUnits, feeRate, longTermFeeRate); outputGroup = new OutputGroup(addressNode.getWallet().getScriptType(), getStoredBlockHeight(), inputWeightUnits, feeRate, longTermFeeRate);
outputGroups.add(outputGroup); outputGroups.add(outputGroup);
@ -1323,12 +1326,12 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
* @param feeRate the fee rate in sats/vB * @param feeRate the fee rate in sats/vB
* @return the maximum spendable amount (can be negative if the fee is higher than the combined UTXO value) * @return the maximum spendable amount (can be negative if the fee is higher than the combined UTXO value)
*/ */
public long getMaxSpendable(List<Address> paymentAddresses, double feeRate, boolean includeSpentMempoolOutputs) { public long getMaxSpendable(List<Address> paymentAddresses, double feeRate, Map<BlockTransactionHashIndex, WalletNode> availableTxos) {
long maxInputValue = 0; long maxInputValue = 0;
Map<Wallet, Integer> cachedInputWeightUnits = new HashMap<>(); Map<Wallet, Integer> cachedInputWeightUnits = new HashMap<>();
Transaction transaction = new Transaction(); Transaction transaction = new Transaction();
for(Map.Entry<BlockTransactionHashIndex, WalletNode> utxo : getWalletUtxos(includeSpentMempoolOutputs).entrySet()) { for(Map.Entry<BlockTransactionHashIndex, WalletNode> utxo : availableTxos.entrySet()) {
int inputWeightUnits = cachedInputWeightUnits.computeIfAbsent(utxo.getValue().getWallet(), Wallet::getInputWeightUnits); int inputWeightUnits = cachedInputWeightUnits.computeIfAbsent(utxo.getValue().getWallet(), Wallet::getInputWeightUnits);
long minInputValue = (long)Math.ceil(feeRate * inputWeightUnits / WITNESS_SCALE_FACTOR); long minInputValue = (long)Math.ceil(feeRate * inputWeightUnits / WITNESS_SCALE_FACTOR);
if(utxo.getKey().getValue() > minInputValue) { if(utxo.getKey().getValue() > minInputValue) {
@ -1660,6 +1663,10 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
labels.put(output.getHash().toString() + "<" + output.getIndex(), output.getLabel()); labels.put(output.getHash().toString() + "<" + output.getIndex(), output.getLabel());
} }
if(output.getStatus() != null) {
labels.put(output.getHash().toString() + ":" + output.getIndex(), output.getStatus().toString());
}
if(output.isSpent() && output.getSpentBy().getLabel() != null && !output.getSpentBy().getLabel().isEmpty()) { if(output.isSpent() && output.getSpentBy().getLabel() != null && !output.getSpentBy().getLabel().isEmpty()) {
labels.put(output.getSpentBy().getHash() + ">" + output.getSpentBy().getIndex(), output.getSpentBy().getLabel()); labels.put(output.getSpentBy().getHash() + ">" + output.getSpentBy().getIndex(), output.getSpentBy().getLabel());
} }

View file

@ -3,7 +3,8 @@ package com.sparrowwallet.drongo.wallet;
import java.util.Locale; import java.util.Locale;
public enum WalletModel { public enum WalletModel {
SEED, SPARROW, BITCOIN_CORE, ELECTRUM, TREZOR_1, TREZOR_T, COLDCARD, LEDGER_NANO_S, LEDGER_NANO_X, DIGITALBITBOX_01, KEEPKEY, SPECTER_DESKTOP, COBO_VAULT, BITBOX_02, SPECTER_DIY, PASSPORT, BLUE_WALLET, KEYSTONE, SEEDSIGNER, CARAVAN, GORDIAN_SEED_TOOL, JADE, LEDGER_NANO_S_PLUS, EPS; 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;
public static WalletModel getModel(String model) { public static WalletModel getModel(String model) {
return valueOf(model.toUpperCase(Locale.ROOT)); return valueOf(model.toUpperCase(Locale.ROOT));
@ -50,7 +51,7 @@ public enum WalletModel {
} }
public boolean alwaysIncludeNonWitnessUtxo() { public boolean alwaysIncludeNonWitnessUtxo() {
if(this == COLDCARD || this == COBO_VAULT || this == PASSPORT || this == KEYSTONE || this == GORDIAN_SEED_TOOL) { if(this == COLDCARD || this == COBO_VAULT || this == PASSPORT || this == KEYSTONE || this == GORDIAN_SEED_TOOL || this == SEEDSIGNER) {
return false; return false;
} }
@ -65,6 +66,10 @@ public enum WalletModel {
return (this == TREZOR_1 || this == KEEPKEY); return (this == TREZOR_1 || this == KEEPKEY);
} }
public boolean isCard() {
return (this == TAPSIGNER || this == SATSCARD);
}
public static WalletModel fromType(String type) { public static WalletModel fromType(String type) {
for(WalletModel model : values()) { for(WalletModel model : values()) {
if(model.getType().equalsIgnoreCase(type)) { if(model.getType().equalsIgnoreCase(type)) {

View file

@ -149,6 +149,11 @@ public class WalletNode extends Persistable implements Comparable<WalletNode> {
txo.setLabel(label); txo.setLabel(label);
} }
String status = wallet.getDetachedLabels().remove(txo.getHash().toString() + ":" + txo.getIndex());
if(status != null && txo.getStatus() == null) {
txo.setStatus(Status.valueOf(status));
}
if(txo.isSpent()) { if(txo.isSpent()) {
String spentByLabel = wallet.getDetachedLabels().remove(txo.getSpentBy().getHash() + ">" + txo.getSpentBy().getIndex()); String spentByLabel = wallet.getDetachedLabels().remove(txo.getSpentBy().getHash() + ">" + txo.getSpentBy().getIndex());
if(spentByLabel != null && (txo.getSpentBy().getLabel() == null || txo.getSpentBy().getLabel().isEmpty())) { if(spentByLabel != null && (txo.getSpentBy().getLabel() == null || txo.getSpentBy().getLabel().isEmpty())) {
@ -163,16 +168,16 @@ public class WalletNode extends Persistable implements Comparable<WalletNode> {
} }
public Set<BlockTransactionHashIndex> getUnspentTransactionOutputs() { public Set<BlockTransactionHashIndex> getUnspentTransactionOutputs() {
return getUnspentTransactionOutputs(false); return getTransactionOutputs(List.of(new SpentTxoFilter()));
} }
public Set<BlockTransactionHashIndex> getUnspentTransactionOutputs(boolean includeSpentMempoolOutputs) { public Set<BlockTransactionHashIndex> getTransactionOutputs(Collection<TxoFilter> txoFilters) {
if(transactionOutputs.isEmpty()) { if(transactionOutputs.isEmpty()) {
return Collections.emptySet(); return Collections.emptySet();
} }
Set<BlockTransactionHashIndex> unspentTXOs = new TreeSet<>(transactionOutputs); Set<BlockTransactionHashIndex> unspentTXOs = new TreeSet<>(transactionOutputs);
unspentTXOs.removeIf(txo -> txo.isSpent() && (!includeSpentMempoolOutputs || txo.getSpentBy().getHeight() > 0)); unspentTXOs.removeIf(txo -> !txoFilters.stream().allMatch(txoFilter -> txoFilter.isEligible(txo)));
return unspentTXOs; return unspentTXOs;
} }

View file

@ -0,0 +1,113 @@
package com.sparrowwallet.drongo.crypto;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.address.InvalidAddressException;
import com.sparrowwallet.drongo.protocol.ScriptType;
import org.junit.Assert;
import org.junit.Test;
import java.security.SignatureException;
public class Bip322Test {
@Test
public void getBip322TaggedHash() {
byte[] empty = Bip322.getBip322MessageHash("");
Assert.assertArrayEquals(Utils.hexToBytes("c90c269c4f8fcbe6880f72a721ddfbf1914268a794cbb21cfafee13770ae19f1"), empty);
byte[] hello = Bip322.getBip322MessageHash("Hello World");
Assert.assertArrayEquals(Utils.hexToBytes("f0eb03b1a75ac6d9847f55c624a99169b5dccba2a31f5b23bea77ba270de0a7a"), hello);
}
@Test
public void signMessageBip322() {
ECKey privKey = DumpedPrivateKey.fromBase58("L3VFeEujGtevx9w18HD1fhRbCH67Az2dpCymeRE1SoPK6XQtaN2k").getKey();
Address address = ScriptType.P2WPKH.getAddress(privKey);
Assert.assertEquals("bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l", address.toString());
String signature = Bip322.signMessageBip322(ScriptType.P2WPKH, "", privKey);
Assert.assertEquals("AkcwRAIgM2gBAQqvZX15ZiysmKmQpDrG83avLIT492QBzLnQIxYCIBaTpOaD20qRlEylyxFSeEA2ba9YOixpX8z46TSDtS40ASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=", signature);
String signature2 = Bip322.signMessageBip322(ScriptType.P2WPKH, "Hello World", privKey);
Assert.assertEquals("AkcwRAIgZRfIY3p7/DoVTty6YZbWS71bc5Vct9p9Fia83eRmw2QCICK/ENGfwLtptFluMGs2KsqoNSk89pO7F29zJLUx9a/sASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=", signature2);
}
@Test
public void verifyMessageBip322Fail() throws InvalidAddressException, SignatureException {
Address address = Address.fromString("bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l");
String message1 = "";
String signature2 = "AkcwRAIgZRfIY3p7/DoVTty6YZbWS71bc5Vct9p9Fia83eRmw2QCICK/ENGfwLtptFluMGs2KsqoNSk89pO7F29zJLUx9a/sASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=";
Assert.assertFalse(Bip322.verifyMessageBip322(ScriptType.P2WPKH, address, message1, signature2));
}
@Test
public void verifyMessageBip322() throws InvalidAddressException, SignatureException {
Address address = Address.fromString("bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l");
String message1 = "";
String signature1 = "AkcwRAIgM2gBAQqvZX15ZiysmKmQpDrG83avLIT492QBzLnQIxYCIBaTpOaD20qRlEylyxFSeEA2ba9YOixpX8z46TSDtS40ASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=";
Assert.assertTrue(Bip322.verifyMessageBip322(ScriptType.P2WPKH, address, message1, signature1));
String message2 = "Hello World";
String signature2 = "AkcwRAIgZRfIY3p7/DoVTty6YZbWS71bc5Vct9p9Fia83eRmw2QCICK/ENGfwLtptFluMGs2KsqoNSk89pO7F29zJLUx9a/sASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=";
Assert.assertTrue(Bip322.verifyMessageBip322(ScriptType.P2WPKH, address, message2, signature2));
String signature3 = "AkgwRQIhAOzyynlqt93lOKJr+wmmxIens//zPzl9tqIOua93wO6MAiBi5n5EyAcPScOjf1lAqIUIQtr3zKNeavYabHyR8eGhowEhAsfxIAMZZEKUPYWI4BruhAQjzFT8FSFSajuFwrDL1Yhy";
Assert.assertTrue(Bip322.verifyMessageBip322(ScriptType.P2WPKH, address, message2, signature3));
}
@Test
public void signMessageBip322Taproot() {
ECKey privKey = DumpedPrivateKey.fromBase58("L3VFeEujGtevx9w18HD1fhRbCH67Az2dpCymeRE1SoPK6XQtaN2k").getKey();
Address address = ScriptType.P2TR.getAddress(privKey);
Assert.assertEquals("bc1ppv609nr0vr25u07u95waq5lucwfm6tde4nydujnu8npg4q75mr5sxq8lt3", address.toString());
String signature = Bip322.signMessageBip322(ScriptType.P2TR, "Hello World", privKey);
Assert.assertEquals("AUHd69PrJQEv+oKTfZ8l+WROBHuy9HKrbFCJu7U1iK2iiEy1vMU5EfMtjc+VSHM7aU0SDbak5IUZRVno2P5mjSafAQ==", signature);
}
@Test
public void verifyMessageBip322Taproot() throws SignatureException {
ECKey privKey = DumpedPrivateKey.fromBase58("L3VFeEujGtevx9w18HD1fhRbCH67Az2dpCymeRE1SoPK6XQtaN2k").getKey();
Address address = ScriptType.P2TR.getAddress(privKey);
Assert.assertEquals("bc1ppv609nr0vr25u07u95waq5lucwfm6tde4nydujnu8npg4q75mr5sxq8lt3", address.toString());
String message1 = "Hello World";
String signature1 = "AUHd69PrJQEv+oKTfZ8l+WROBHuy9HKrbFCJu7U1iK2iiEy1vMU5EfMtjc+VSHM7aU0SDbak5IUZRVno2P5mjSafAQ==";
Assert.assertTrue(Bip322.verifyMessageBip322(ScriptType.P2TR, address, message1, signature1));
}
@Test(expected = UnsupportedOperationException.class)
public void signMessageBip322NestedSegwit() {
ECKey privKey = DumpedPrivateKey.fromBase58("L3VFeEujGtevx9w18HD1fhRbCH67Az2dpCymeRE1SoPK6XQtaN2k").getKey();
Address address = ScriptType.P2SH_P2WPKH.getAddress(privKey);
Assert.assertEquals("37qyp7jQAzqb2rCBpMvVtLDuuzKAUCVnJb", address.toString());
String signature = Bip322.signMessageBip322(ScriptType.P2SH_P2WPKH, "Hello World", privKey);
Assert.assertEquals("AkcwRAIgHx821fcP3D4R6RsXHF8kXza4d/SqpKGaGu++AEQjJz0CIH9cN5XGDkgkqqF9OMTbYvhgI7Yp9NoHXEgLstjqDOqDASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=", signature);
}
@Test(expected = UnsupportedOperationException.class)
public void verifyMessageBip322NestedSegwit() throws SignatureException {
ECKey privKey = DumpedPrivateKey.fromBase58("L3VFeEujGtevx9w18HD1fhRbCH67Az2dpCymeRE1SoPK6XQtaN2k").getKey();
Address address = ScriptType.P2SH_P2WPKH.getAddress(privKey);
Assert.assertEquals("37qyp7jQAzqb2rCBpMvVtLDuuzKAUCVnJb", address.toString());
String message1 = "Hello World";
String signature1 = "AkcwRAIgHx821fcP3D4R6RsXHF8kXza4d/SqpKGaGu++AEQjJz0CIH9cN5XGDkgkqqF9OMTbYvhgI7Yp9NoHXEgLstjqDOqDASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=";
Bip322.verifyMessageBip322(ScriptType.P2SH_P2WPKH, address, message1, signature1);
}
@Test(expected = IllegalArgumentException.class)
public void verifyMessageBip322Multisig() throws SignatureException, InvalidAddressException {
Address address = Address.fromString("bc1ppv609nr0vr25u07u95waq5lucwfm6tde4nydujnu8npg4q75mr5sxq8lt3");
String message1 = "This will be a p2wsh 3-of-3 multisig BIP 322 signed message";
String signature1 = "BQBIMEUCIQDQoXvGKLH58exuujBOta+7+GN7vi0lKwiQxzBpuNuXuAIgIE0XYQlFDOfxbegGYYzlf+tqegleAKE6SXYIa1U+uCcBRzBEAiATegywVl6GWrG9jJuPpNwtgHKyVYCX2yfuSSDRFATAaQIgTLlU6reLQsSIrQSF21z3PtUO2yAUseUWGZqRUIE7VKoBSDBFAiEAgxtpidsU0Z4u/+5RB9cyeQtoCW5NcreLJmWXZ8kXCZMCIBR1sXoEinhZE4CF9P9STGIcMvCuZjY6F5F0XTVLj9SjAWlTIQP3dyWvTZjUENWJowMWBsQrrXCUs20Gu5YF79CG5Ga0XSEDwqI5GVBOuFkFzQOGH5eTExSAj2Z/LDV/hbcvAPQdlJMhA17FuuJd+4wGuj+ZbVxEsFapTKAOwyhfw9qpch52JKxbU64=";
Bip322.verifyMessageBip322(ScriptType.P2TR, address, message1, signature1);
}
}