mirror of
https://github.com/sparrowwallet/drongo.git
synced 2025-11-05 11:56:38 +00:00
Merge branch 'master' into update-dec-2022
This commit is contained in:
commit
2a68d8eec0
39 changed files with 740 additions and 164 deletions
|
|
@ -59,6 +59,7 @@ dependencies {
|
|||
exclude group: 'org.hamcrest', module: 'hamcrest-core'
|
||||
}
|
||||
testImplementation group: 'org.hamcrest', name: 'hamcrest-core', version: '2.2'
|
||||
testImplementation 'junit:junit:4.13.1'
|
||||
}
|
||||
|
||||
processResources {
|
||||
|
|
|
|||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
3
gradle/wrapper/gradle-wrapper.properties
vendored
3
gradle/wrapper/gradle-wrapper.properties
vendored
|
|
@ -1,5 +1,6 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
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
|
||||
zipStorePath=wrapper/dists
|
||||
|
|
|
|||
12
gradlew
vendored
12
gradlew
vendored
|
|
@ -55,7 +55,7 @@
|
|||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
|
|
@ -80,10 +80,10 @@ do
|
|||
esac
|
||||
done
|
||||
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
APP_NAME="Gradle"
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
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.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
|
@ -143,12 +143,16 @@ fi
|
|||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
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 ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | 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" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
|
|
|
|||
1
gradlew.bat
vendored
1
gradlew.bat
vendored
|
|
@ -26,6 +26,7 @@ if "%OS%"=="Windows_NT" setlocal
|
|||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
|
|
|
|||
|
|
@ -90,8 +90,8 @@ public class ExtendedKey {
|
|||
ChildNumber childNumber;
|
||||
List<ChildNumber> path;
|
||||
|
||||
if(depth == 0) {
|
||||
//Poorly formatted extended key, add first child path element
|
||||
if(depth == 0 && !header.isPrivateKey()) {
|
||||
//Poorly formatted public extended key, add first child path element
|
||||
childNumber = new ChildNumber(0, false);
|
||||
} else if ((i & ChildNumber.HARDENED_BIT) != 0) {
|
||||
childNumber = new ChildNumber(i ^ ChildNumber.HARDENED_BIT, true); //already hardened
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ public class OutputDescriptor {
|
|||
private static final String INPUT_CHARSET = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ ";
|
||||
private static final String CHECKSUM_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
||||
|
||||
private static final Pattern XPUB_PATTERN = Pattern.compile("(\\[[^\\]]+\\])?(.pub[^/\\,)]{100,112})(/[/\\d*'hH<>;]+)?");
|
||||
private static final Pattern XPUB_PATTERN = Pattern.compile("(\\[[^\\]]+\\])?(.(?:pub|prv)[^/\\,)]{100,112})(/[/\\d*'hH<>;]+)?");
|
||||
private static final Pattern PUBKEY_PATTERN = Pattern.compile("(\\[[^\\]]+\\])?(0[23][0-9a-fA-F]{32})");
|
||||
private static final Pattern MULTI_PATTERN = Pattern.compile("multi\\(([\\d+])");
|
||||
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);
|
||||
try {
|
||||
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);
|
||||
keyChildDerivationMap.put(extendedPublicKey, childDerivationPath);
|
||||
} catch(ProtocolException e) {
|
||||
|
|
@ -479,6 +486,10 @@ public class OutputDescriptor {
|
|||
}
|
||||
|
||||
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();
|
||||
builder.append(scriptType.getDescriptor());
|
||||
|
||||
|
|
@ -487,14 +498,14 @@ public class OutputDescriptor {
|
|||
StringJoiner joiner = new StringJoiner(",");
|
||||
joiner.add(Integer.toString(multisigThreshold));
|
||||
for(ExtendedKey pubKey : sortExtendedPubKeys(extendedPublicKeys.keySet())) {
|
||||
String extKeyString = toString(pubKey, addKeyOrigin);
|
||||
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));
|
||||
builder.append(toString(extendedPublicKey, addKeyOrigin, addKey));
|
||||
}
|
||||
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();
|
||||
KeyDerivation keyDerivation = extendedPublicKeys.get(pubKey);
|
||||
if(keyDerivation != null && keyDerivation.getDerivationPath() != null && addKeyOrigin) {
|
||||
|
|
@ -561,6 +572,7 @@ public class OutputDescriptor {
|
|||
keyBuilder.append("]");
|
||||
}
|
||||
|
||||
if(addKey) {
|
||||
if(pubKey != null) {
|
||||
keyBuilder.append(pubKey.toString());
|
||||
}
|
||||
|
|
@ -573,6 +585,7 @@ public class OutputDescriptor {
|
|||
|
||||
keyBuilder.append(childDerivation);
|
||||
}
|
||||
}
|
||||
|
||||
return keyBuilder.toString();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import java.io.InputStream;
|
|||
import java.io.OutputStream;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.CharsetDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
|
||||
|
|
@ -22,6 +23,7 @@ public class Utils {
|
|||
|
||||
public static final String HEX_REGEX = "^[0-9A-Fa-f]+$";
|
||||
public static final String BASE64_REGEX = "^[0-9A-Za-z\\\\+=/]+$";
|
||||
public static final String NUMERIC_REGEX = "^-?\\d+(\\.\\d+)?$";
|
||||
|
||||
public static boolean isHex(String s) {
|
||||
return s.matches(HEX_REGEX);
|
||||
|
|
@ -31,6 +33,20 @@ public class Utils {
|
|||
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) {
|
||||
char[] hexChars = new char[bytes.length * 2];
|
||||
for ( int j = 0; j < bytes.length; j++ ) {
|
||||
|
|
@ -128,6 +144,20 @@ public class Utils {
|
|||
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. */
|
||||
public static long readUint32(byte[] bytes, int offset) {
|
||||
return (bytes[offset] & 0xffl) |
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ public class PaymentAddress {
|
|||
private BigInteger addSecp256k1(BigInteger b1, BigInteger b2) {
|
||||
BigInteger ret = b1.add(b2);
|
||||
|
||||
if(ret.bitLength() > CURVE.getN().bitLength()) {
|
||||
if(ret.compareTo(CURVE.getN()) > 0) {
|
||||
return ret.mod(CURVE.getN());
|
||||
}
|
||||
|
||||
|
|
@ -83,7 +83,7 @@ public class PaymentAddress {
|
|||
}
|
||||
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ import java.util.Arrays;
|
|||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static com.sparrowwallet.drongo.Utils.xor;
|
||||
|
||||
public class PaymentCode {
|
||||
private static final Logger log = LoggerFactory.getLogger(PaymentCode.class);
|
||||
|
||||
|
|
@ -313,21 +315,6 @@ public class PaymentCode {
|
|||
return HDKeyDerivation.createMasterPubKeyFromBytes(pubkey, chain);
|
||||
}
|
||||
|
||||
private static byte[] xor(byte[] a, byte[] b) {
|
||||
if(a.length != b.length) {
|
||||
log.error("Invalid length for xor: " + a.length + " vs " + b.length);
|
||||
return null;
|
||||
}
|
||||
|
||||
byte[] ret = new byte[a.length];
|
||||
|
||||
for(int i = 0; i < a.length; i++) {
|
||||
ret[i] = (byte) ((int) b[i] ^ (int) a[i]);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
public boolean isValid() {
|
||||
try {
|
||||
byte[] pcodeBytes = Base58.decodeChecked(strPaymentCode);
|
||||
|
|
|
|||
170
src/main/java/com/sparrowwallet/drongo/crypto/Bip322.java
Normal file
170
src/main/java/com/sparrowwallet/drongo/crypto/Bip322.java
Normal 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));
|
||||
}
|
||||
}
|
||||
|
|
@ -576,9 +576,13 @@ public class ECKey {
|
|||
* @throws IllegalStateException if this ECKey does not have the private part.
|
||||
*/
|
||||
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);
|
||||
Sha256Hash hash = Sha256Hash.of(data);
|
||||
ECDSASignature sig = signEcdsa(hash);
|
||||
ECDSASignature sig = ecdsaHashSigner.sign(hash);
|
||||
byte recId = findRecoveryId(hash, sig);
|
||||
int headerByte = recId + getSigningTypeConstant(scriptType);
|
||||
byte[] sigData = new byte[65]; // 1 header + 32 bytes for R + 32 bytes for S
|
||||
|
|
@ -627,6 +631,14 @@ public class ECKey {
|
|||
// This is what you get back from Bouncy Castle if base64 doesn't decode :(
|
||||
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.
|
||||
if(signatureEncoded.length < 65) {
|
||||
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 s = new BigInteger(1, Arrays.copyOfRange(signatureEncoded, 33, 65));
|
||||
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;
|
||||
if(header >= 39) { // this is a bech32 signature
|
||||
header -= 12;
|
||||
|
|
@ -863,4 +872,8 @@ public class ECKey {
|
|||
throw new RuntimeException(e); // Cannot happen.
|
||||
}
|
||||
}
|
||||
|
||||
public interface ECDSAHashSigner {
|
||||
ECDSASignature sign(Sha256Hash hash);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,25 +7,36 @@ import org.bouncycastle.crypto.params.KeyParameter;
|
|||
|
||||
public class Pbkdf2KeyDeriver implements KeyDeriver, AsymmetricKeyDeriver {
|
||||
public static final int DEFAULT_ITERATION_COUNT = 1024;
|
||||
public static final int DEFAULT_KEY_SIZE = 512;
|
||||
|
||||
private final byte[] salt;
|
||||
private final int iterationCount;
|
||||
private final int keySize;
|
||||
|
||||
public static final Pbkdf2KeyDeriver DEFAULT_INSTANCE = new Pbkdf2KeyDeriver();
|
||||
|
||||
public Pbkdf2KeyDeriver() {
|
||||
this.salt = new byte[0];
|
||||
this.iterationCount = DEFAULT_ITERATION_COUNT;
|
||||
this.keySize = DEFAULT_KEY_SIZE;
|
||||
}
|
||||
|
||||
public Pbkdf2KeyDeriver(byte[] salt) {
|
||||
this.salt = salt;
|
||||
this.iterationCount = DEFAULT_ITERATION_COUNT;
|
||||
this.keySize = DEFAULT_KEY_SIZE;
|
||||
}
|
||||
|
||||
public Pbkdf2KeyDeriver(byte[] salt, int iterationCount) {
|
||||
this.salt = salt;
|
||||
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
|
||||
|
|
@ -37,7 +48,7 @@ public class Pbkdf2KeyDeriver implements KeyDeriver, AsymmetricKeyDeriver {
|
|||
public Key deriveKey(CharSequence password) throws KeyCrypterException {
|
||||
PKCS5S2ParametersGenerator gen = new PKCS5S2ParametersGenerator(new SHA512Digest());
|
||||
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());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import java.util.regex.Pattern;
|
|||
public class Miniscript {
|
||||
private static final Pattern SINGLE_PATTERN = Pattern.compile("pkh?\\(");
|
||||
private static final Pattern TAPROOT_PATTERN = Pattern.compile("tr\\(");
|
||||
private static final Pattern MULTI_PATTERN = Pattern.compile("multi\\(([\\d+])");
|
||||
private static final Pattern MULTI_PATTERN = Pattern.compile("multi\\((\\d+)");
|
||||
|
||||
private String script;
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import com.sparrowwallet.drongo.Utils;
|
|||
import com.sparrowwallet.drongo.address.*;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import org.bouncycastle.util.encoders.Hex;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
|
|
@ -18,6 +20,8 @@ import static com.sparrowwallet.drongo.protocol.ScriptType.*;
|
|||
import static com.sparrowwallet.drongo.protocol.ScriptOpCodes.*;
|
||||
|
||||
public class Script {
|
||||
private static final Logger log = LoggerFactory.getLogger(Script.class);
|
||||
|
||||
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 ...]
|
||||
|
|
@ -32,7 +36,11 @@ public class Script {
|
|||
Script(byte[] programBytes, boolean parse) {
|
||||
program = programBytes;
|
||||
if(parse) {
|
||||
try {
|
||||
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.
|
||||
dataToRead = opcode;
|
||||
} 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();
|
||||
} else if (opcode == OP_PUSHDATA2) {
|
||||
// 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);
|
||||
} else if (opcode == OP_PUSHDATA4) {
|
||||
// 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
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import org.bouncycastle.util.encoders.Hex;
|
|||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
|
|
@ -28,7 +30,11 @@ public class ScriptChunk {
|
|||
}
|
||||
|
||||
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) {
|
||||
|
|
@ -68,7 +74,7 @@ public class ScriptChunk {
|
|||
}
|
||||
|
||||
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");
|
||||
stream.write(opcode);
|
||||
} 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() {
|
||||
if(data == null || data.length == 0) {
|
||||
return false;
|
||||
|
|
@ -213,6 +231,9 @@ public class ScriptChunk {
|
|||
if (data.length == 0) {
|
||||
return "OP_0";
|
||||
}
|
||||
if(Utils.isUtf8(data)) {
|
||||
return new String(data, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
return Hex.toHexString(data);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1320,7 +1320,7 @@ public enum ScriptType {
|
|||
//Start with length of output
|
||||
int outputVbytes = output.getLength();
|
||||
//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 (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
|
||||
*/
|
||||
public int getInputVbytes() {
|
||||
public double getInputVbytes() {
|
||||
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)) {
|
||||
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)) {
|
||||
//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)) {
|
||||
//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)) {
|
||||
//Return length of spending input with no discount
|
||||
return (32 + 4 + 1 + 107 + 4);
|
||||
|
|
|
|||
|
|
@ -61,6 +61,18 @@ public class Sha256Hash implements Comparable<Sha256Hash> {
|
|||
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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -8,10 +8,7 @@ import java.io.ByteArrayOutputStream;
|
|||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
|
||||
import static com.sparrowwallet.drongo.Utils.uint32ToByteStreamLE;
|
||||
import static com.sparrowwallet.drongo.Utils.uint64ToByteStreamLE;
|
||||
|
|
@ -634,6 +631,9 @@ public class Transaction extends ChildMessage {
|
|||
if(spentUtxos.size() != getInputs().size()) {
|
||||
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()) {
|
||||
throw new IllegalArgumentException("Input index is greater than the number of transaction inputs");
|
||||
}
|
||||
|
|
@ -685,7 +685,7 @@ public class Transaction extends ChildMessage {
|
|||
|
||||
if(anyoneCanPay) {
|
||||
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);
|
||||
Utils.uint32ToByteStreamLE(getInputs().get(inputIndex).getSequenceNumber(), bos);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ public class TransactionSignature {
|
|||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -461,10 +461,10 @@ public class PSBT {
|
|||
}
|
||||
|
||||
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();
|
||||
|
||||
baos.writeBytes(Utils.hexToBytes(PSBT_MAGIC_HEX));
|
||||
|
|
@ -481,8 +481,9 @@ public class PSBT {
|
|||
for(PSBTInput psbtInput : getPsbtInputs()) {
|
||||
List<PSBTEntry> inputEntries = psbtInput.getInputEntries();
|
||||
for(PSBTEntry entry : inputEntries) {
|
||||
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)) {
|
||||
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))
|
||||
&& (includeNonWitnessUtxos || entry.getKeyType() != PSBT_IN_NON_WITNESS_UTXO)) {
|
||||
entry.serializeToStream(baos);
|
||||
}
|
||||
}
|
||||
|
|
@ -630,7 +631,7 @@ public class PSBT {
|
|||
}
|
||||
|
||||
public String toBase64String(boolean includeXpubs) {
|
||||
return Base64.toBase64String(serialize(includeXpubs));
|
||||
return Base64.toBase64String(serialize(includeXpubs, true));
|
||||
}
|
||||
|
||||
public static boolean isPSBT(byte[] b) {
|
||||
|
|
|
|||
|
|
@ -188,7 +188,7 @@ public class PSBTEntry {
|
|||
psbtByteBuffer.get(buf);
|
||||
ByteBuffer byteBuffer = ByteBuffer.wrap(buf);
|
||||
byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
return byteBuffer.getShort();
|
||||
return Short.toUnsignedInt(byteBuffer.getShort());
|
||||
}
|
||||
case (byte) 0xfe: {
|
||||
byte[] buf = new byte[4];
|
||||
|
|
|
|||
|
|
@ -519,6 +519,20 @@ public class PSBTInput {
|
|||
}
|
||||
|
||||
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();
|
||||
if(localSigHash == null) {
|
||||
localSigHash = getDefaultSigHash();
|
||||
|
|
@ -529,12 +543,12 @@ public class PSBTInput {
|
|||
if(signingScript != null) {
|
||||
Sha256Hash hash = getHashForSignature(signingScript, localSigHash);
|
||||
TransactionSignature.Type type = isTaproot() ? SCHNORR : ECDSA;
|
||||
TransactionSignature transactionSignature = privKey.sign(hash, localSigHash, type);
|
||||
TransactionSignature transactionSignature = psbtInputSigner.sign(hash, localSigHash, type);
|
||||
|
||||
if(type == SCHNORR) {
|
||||
tapKeyPathSignature = transactionSignature;
|
||||
} else {
|
||||
ECKey pubKey = ECKey.fromPublicOnly(privKey);
|
||||
ECKey pubKey = psbtInputSigner.getPubKey();
|
||||
getPartialSignatures().put(pubKey, transactionSignature);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -229,4 +229,85 @@ public class Bip39MnemonicCode {
|
|||
bits[(i * 8) + j] = (data[i] & (1 << (7 - j))) != 0;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@ package com.sparrowwallet.drongo.wallet;
|
|||
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
|
||||
public class CoinbaseUtxoFilter implements UtxoFilter {
|
||||
public class CoinbaseTxoFilter implements TxoFilter {
|
||||
private final Wallet wallet;
|
||||
|
||||
public CoinbaseUtxoFilter(Wallet wallet) {
|
||||
public CoinbaseTxoFilter(Wallet wallet) {
|
||||
this.wallet = wallet;
|
||||
}
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
package com.sparrowwallet.drongo.wallet;
|
||||
|
||||
public class FrozenUtxoFilter implements UtxoFilter {
|
||||
public class FrozenTxoFilter implements TxoFilter {
|
||||
@Override
|
||||
public boolean isEligible(BlockTransactionHashIndex candidate) {
|
||||
return candidate.getStatus() == null || candidate.getStatus() != Status.FROZEN;
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
package com.sparrowwallet.drongo.wallet;
|
||||
|
||||
public class InsufficientFundsException extends Exception {
|
||||
private Long targetValue;
|
||||
|
||||
public InsufficientFundsException() {
|
||||
super();
|
||||
}
|
||||
|
|
@ -8,4 +10,13 @@ public class InsufficientFundsException extends Exception {
|
|||
public InsufficientFundsException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
public InsufficientFundsException(String message, Long targetValue) {
|
||||
super(message);
|
||||
this.targetValue = targetValue;
|
||||
}
|
||||
|
||||
public Long getTargetValue() {
|
||||
return targetValue;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,30 @@
|
|||
package com.sparrowwallet.drongo.wallet;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class PresetUtxoSelector extends SingleSetUtxoSelector {
|
||||
private final Collection<BlockTransactionHashIndex> presetUtxos;
|
||||
private final Collection<BlockTransactionHashIndex> excludedUtxos;
|
||||
private final boolean maintainOrder;
|
||||
private final boolean requireAll;
|
||||
|
||||
public PresetUtxoSelector(Collection<BlockTransactionHashIndex> presetUtxos) {
|
||||
this.presetUtxos = presetUtxos;
|
||||
this.maintainOrder = false;
|
||||
this(presetUtxos, new ArrayList<>());
|
||||
}
|
||||
|
||||
public PresetUtxoSelector(Collection<BlockTransactionHashIndex> presetUtxos, boolean maintainOrder) {
|
||||
public PresetUtxoSelector(Collection<BlockTransactionHashIndex> presetUtxos, Collection<BlockTransactionHashIndex> excludedUtxos) {
|
||||
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.requireAll = requireAll;
|
||||
}
|
||||
|
||||
@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;
|
||||
} else if(requireAll && !utxosSet.containsAll(presetUtxos)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
return utxos;
|
||||
|
|
@ -44,6 +55,17 @@ public class PresetUtxoSelector extends SingleSetUtxoSelector {
|
|||
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
|
||||
public boolean shuffleInputs() {
|
||||
return !maintainOrder;
|
||||
|
|
|
|||
|
|
@ -31,6 +31,13 @@ public class SeedQR {
|
|||
}
|
||||
|
||||
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) {
|
||||
throw new IllegalArgumentException("Invalid CompactSeedQR header");
|
||||
}
|
||||
|
|
@ -41,13 +48,16 @@ public class SeedQR {
|
|||
|
||||
String qrHex = Utils.bytesToHex(compactSeedQr);
|
||||
String seedHex;
|
||||
if(qrHex.endsWith("0ec")) {
|
||||
seedHex = qrHex.substring(3, qrHex.length() - 3);
|
||||
if(qrHex.endsWith("0ec11ec11")) {
|
||||
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 {
|
||||
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) {
|
||||
throw new IllegalArgumentException("Invalid CompactSeedQR length: " + compactSeedQr.length);
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
package com.sparrowwallet.drongo.wallet;
|
||||
|
||||
public interface UtxoFilter {
|
||||
public interface TxoFilter {
|
||||
boolean isEligible(BlockTransactionHashIndex candidate);
|
||||
}
|
||||
|
|
@ -799,30 +799,38 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
|
|||
}
|
||||
|
||||
public Map<BlockTransactionHashIndex, WalletNode> getWalletUtxos() {
|
||||
return getWalletUtxos(false);
|
||||
return getWalletTxos(List.of(new SpentTxoFilter()));
|
||||
}
|
||||
|
||||
public Map<BlockTransactionHashIndex, WalletNode> getWalletUtxos(boolean includeSpentMempoolOutputs) {
|
||||
Map<BlockTransactionHashIndex, WalletNode> walletUtxos = new TreeMap<>();
|
||||
public Map<BlockTransactionHashIndex, WalletNode> getSpendableUtxos() {
|
||||
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()) {
|
||||
getWalletUtxos(walletUtxos, getNode(keyPurpose), includeSpentMempoolOutputs);
|
||||
getWalletTxos(walletTxos, getNode(keyPurpose), txoFilters);
|
||||
}
|
||||
|
||||
for(Wallet childWallet : getChildWallets()) {
|
||||
if(childWallet.isNested()) {
|
||||
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(BlockTransactionHashIndex utxo : addressNode.getUnspentTransactionOutputs(includeSpentMempoolOutputs)) {
|
||||
walletUtxos.put(utxo, addressNode);
|
||||
for(BlockTransactionHashIndex utxo : addressNode.getTransactionOutputs(txoFilters)) {
|
||||
walletTxos.put(utxo, addressNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -981,29 +989,30 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
|
|||
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);
|
||||
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) {
|
||||
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) {
|
||||
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
|
||||
//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) {
|
||||
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) {
|
||||
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<>();
|
||||
selectedUtxoSets.forEach(selectedUtxos::putAll);
|
||||
long totalSelectedAmt = selectedUtxos.keySet().stream().mapToLong(BlockTransactionHashIndex::getValue).sum();
|
||||
|
|
@ -1076,7 +1085,7 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
|
|||
if(differenceAmt < noChangeFeeRequiredAmt) {
|
||||
valueRequiredAmt = totalSelectedAmt + 1;
|
||||
//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;
|
||||
}
|
||||
|
||||
|
|
@ -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 {
|
||||
List<OutputGroup> utxoPool = getGroupedUtxos(utxoFilters, feeRate, longTermFeeRate, groupByAddress, includeSpentMempoolOutputs);
|
||||
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(txoFilters, feeRate, longTermFeeRate, groupByAddress);
|
||||
|
||||
List<OutputGroup.Filter> filters = new ArrayList<>();
|
||||
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<Map<BlockTransactionHashIndex, WalletNode>> selectedInputSetsList = new ArrayList<>();
|
||||
long total = 0;
|
||||
Map<BlockTransactionHashIndex, WalletNode> utxos = getWalletUtxos(includeSpentMempoolOutputs);
|
||||
for(Collection<BlockTransactionHashIndex> selectedInputs : selectedInputSets) {
|
||||
total += selectedInputs.stream().mapToLong(BlockTransactionHashIndex::getValue).sum();
|
||||
Map<BlockTransactionHashIndex, WalletNode> selectedInputsMap = new LinkedHashMap<>();
|
||||
|
|
@ -1213,7 +1221,7 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
|
|||
Collections.shuffle(shuffledInputs);
|
||||
}
|
||||
for(BlockTransactionHashIndex shuffledInput : shuffledInputs) {
|
||||
selectedInputsMap.put(shuffledInput, utxos.get(shuffledInput));
|
||||
selectedInputsMap.put(shuffledInput, availableTxos.get(shuffledInput));
|
||||
}
|
||||
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<>();
|
||||
Map<Sha256Hash, BlockTransaction> walletTransactions = getWalletTransactions();
|
||||
Map<BlockTransactionHashIndex, WalletNode> walletTxos = getWalletTxos();
|
||||
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()) {
|
||||
if(childWallet.isNested()) {
|
||||
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;
|
||||
}
|
||||
|
||||
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();
|
||||
for(WalletNode addressNode : purposeNode.getChildren()) {
|
||||
OutputGroup outputGroup = null;
|
||||
for(BlockTransactionHashIndex utxo : addressNode.getUnspentTransactionOutputs(includeSpentMempoolOutputs)) {
|
||||
Optional<UtxoFilter> matchedFilter = utxoFilters.stream().filter(utxoFilter -> !utxoFilter.isEligible(utxo)).findAny();
|
||||
if(matchedFilter.isPresent()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for(BlockTransactionHashIndex utxo : addressNode.getTransactionOutputs(txoFilters)) {
|
||||
if(outputGroup == null || !groupByAddress) {
|
||||
outputGroup = new OutputGroup(addressNode.getWallet().getScriptType(), getStoredBlockHeight(), inputWeightUnits, feeRate, longTermFeeRate);
|
||||
outputGroups.add(outputGroup);
|
||||
|
|
@ -1323,12 +1326,12 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
|
|||
* @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)
|
||||
*/
|
||||
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;
|
||||
|
||||
Map<Wallet, Integer> cachedInputWeightUnits = new HashMap<>();
|
||||
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);
|
||||
long minInputValue = (long)Math.ceil(feeRate * inputWeightUnits / WITNESS_SCALE_FACTOR);
|
||||
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());
|
||||
}
|
||||
|
||||
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()) {
|
||||
labels.put(output.getSpentBy().getHash() + ">" + output.getSpentBy().getIndex(), output.getSpentBy().getLabel());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ package com.sparrowwallet.drongo.wallet;
|
|||
import java.util.Locale;
|
||||
|
||||
public enum WalletModel {
|
||||
SEED, SPARROW, BITCOIN_CORE, ELECTRUM, TREZOR_1, TREZOR_T, COLDCARD, LEDGER_NANO_S, LEDGER_NANO_X, DIGITALBITBOX_01, KEEPKEY, SPECTER_DESKTOP, COBO_VAULT, BITBOX_02, SPECTER_DIY, PASSPORT, 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) {
|
||||
return valueOf(model.toUpperCase(Locale.ROOT));
|
||||
|
|
@ -50,7 +51,7 @@ public enum WalletModel {
|
|||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -65,6 +66,10 @@ public enum WalletModel {
|
|||
return (this == TREZOR_1 || this == KEEPKEY);
|
||||
}
|
||||
|
||||
public boolean isCard() {
|
||||
return (this == TAPSIGNER || this == SATSCARD);
|
||||
}
|
||||
|
||||
public static WalletModel fromType(String type) {
|
||||
for(WalletModel model : values()) {
|
||||
if(model.getType().equalsIgnoreCase(type)) {
|
||||
|
|
|
|||
|
|
@ -149,6 +149,11 @@ public class WalletNode extends Persistable implements Comparable<WalletNode> {
|
|||
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()) {
|
||||
String spentByLabel = wallet.getDetachedLabels().remove(txo.getSpentBy().getHash() + ">" + txo.getSpentBy().getIndex());
|
||||
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() {
|
||||
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()) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
Binary file not shown.
113
src/test/java/com/sparrowwallet/drongo/crypto/Bip322Test.java
Normal file
113
src/test/java/com/sparrowwallet/drongo/crypto/Bip322Test.java
Normal 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);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue