refactor and initial tests

This commit is contained in:
Craig Raw 2020-02-23 18:04:43 +02:00
parent ce2b0648a6
commit e5d2ad427c
7 changed files with 569 additions and 376 deletions

View file

@ -2,6 +2,7 @@ package com.craigraw.drongo.protocol;
import com.craigraw.drongo.Utils; import com.craigraw.drongo.Utils;
import com.craigraw.drongo.address.*; import com.craigraw.drongo.address.*;
import org.bouncycastle.util.encoders.Hex;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
@ -101,6 +102,10 @@ public class Script {
} }
} }
public String getProgramAsHex() {
return Hex.toHexString(getProgram());
}
/** /**
* Returns true if this script has the required form to contain a destination address * Returns true if this script has the required form to contain a destination address
*/ */

View file

@ -1,10 +1,12 @@
package com.craigraw.drongo.protocol; package com.craigraw.drongo.protocol;
import com.craigraw.drongo.Utils; import com.craigraw.drongo.Utils;
import org.bouncycastle.util.encoders.Hex;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; import java.util.List;
public class TransactionWitness { public class TransactionWitness {
@ -35,4 +37,42 @@ public class TransactionWitness {
stream.write(push); stream.write(push);
} }
} }
@Override
public String toString() {
StringBuffer buffer = new StringBuffer();
for (byte[] push : pushes) {
if (push == null) {
buffer.append("NULL");
} else if (push.length == 0) {
buffer.append("EMPTY");
} else {
buffer.append(Hex.toHexString(push));
}
buffer.append(" ");
}
return buffer.toString().trim();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
TransactionWitness other = (TransactionWitness) o;
if (pushes.size() != other.pushes.size()) return false;
for (int i = 0; i < pushes.size(); i++) {
if (!Arrays.equals(pushes.get(i), other.pushes.get(i))) return false;
}
return true;
}
@Override
public int hashCode() {
int hashCode = 1;
for (byte[] push : pushes) {
hashCode = 31 * hashCode + (push == null ? 0 : Arrays.hashCode(push));
}
return hashCode;
}
} }

View file

@ -3,9 +3,6 @@ package com.craigraw.drongo.psbt;
import com.craigraw.drongo.ExtendedPublicKey; import com.craigraw.drongo.ExtendedPublicKey;
import com.craigraw.drongo.KeyDerivation; import com.craigraw.drongo.KeyDerivation;
import com.craigraw.drongo.Utils; import com.craigraw.drongo.Utils;
import com.craigraw.drongo.crypto.ChildNumber;
import com.craigraw.drongo.crypto.ECKey;
import com.craigraw.drongo.crypto.LazyECPoint;
import com.craigraw.drongo.protocol.*; import com.craigraw.drongo.protocol.*;
import org.bouncycastle.util.encoders.Base64; import org.bouncycastle.util.encoders.Base64;
import org.bouncycastle.util.encoders.Hex; import org.bouncycastle.util.encoders.Hex;
@ -17,29 +14,14 @@ import java.nio.ByteBuffer;
import java.nio.ByteOrder; import java.nio.ByteOrder;
import java.util.*; import java.util.*;
import static com.craigraw.drongo.psbt.PSBTEntry.parseKeyDerivation;
public class PSBT { public class PSBT {
public static final byte PSBT_GLOBAL_UNSIGNED_TX = 0x00; public static final byte PSBT_GLOBAL_UNSIGNED_TX = 0x00;
public static final byte PSBT_GLOBAL_BIP32_PUBKEY = 0x01; public static final byte PSBT_GLOBAL_BIP32_PUBKEY = 0x01;
public static final byte PSBT_GLOBAL_VERSION = (byte)0xfb; public static final byte PSBT_GLOBAL_VERSION = (byte)0xfb;
public static final byte PSBT_GLOBAL_PROPRIETARY = (byte)0xfc; public static final byte PSBT_GLOBAL_PROPRIETARY = (byte)0xfc;
public static final byte PSBT_IN_NON_WITNESS_UTXO = 0x00;
public static final byte PSBT_IN_WITNESS_UTXO = 0x01;
public static final byte PSBT_IN_PARTIAL_SIG = 0x02;
public static final byte PSBT_IN_SIGHASH_TYPE = 0x03;
public static final byte PSBT_IN_REDEEM_SCRIPT = 0x04;
public static final byte PSBT_IN_WITNESS_SCRIPT = 0x05;
public static final byte PSBT_IN_BIP32_DERIVATION = 0x06;
public static final byte PSBT_IN_FINAL_SCRIPTSIG = 0x07;
public static final byte PSBT_IN_FINAL_SCRIPTWITNESS = 0x08;
public static final byte PSBT_IN_POR_COMMITMENT = 0x09;
public static final byte PSBT_IN_PROPRIETARY = (byte)0xfc;
public static final byte PSBT_OUT_REDEEM_SCRIPT = 0x00;
public static final byte PSBT_OUT_WITNESS_SCRIPT = 0x01;
public static final byte PSBT_OUT_BIP32_DERIVATION = 0x02;
public static final byte PSBT_OUT_PROPRIETARY = (byte)0xfc;
public static final String PSBT_MAGIC = "70736274"; public static final String PSBT_MAGIC = "70736274";
private static final int STATE_GLOBALS = 1; private static final int STATE_GLOBALS = 1;
@ -51,11 +33,8 @@ public class PSBT {
private int inputs = 0; private int inputs = 0;
private int outputs = 0; private int outputs = 0;
private boolean parseOK = false;
private String strPSBT = null; private byte[] psbtBytes;
private byte[] psbtBytes = null;
private ByteBuffer psbtByteBuffer = null;
private Transaction transaction = null; private Transaction transaction = null;
private Integer version = null; private Integer version = null;
@ -67,270 +46,120 @@ public class PSBT {
private static final Logger log = LoggerFactory.getLogger(PSBT.class); private static final Logger log = LoggerFactory.getLogger(PSBT.class);
public PSBT(String strPSBT) throws Exception { public PSBT(byte[] psbt) {
if (!isPSBT(strPSBT)) { this.psbtBytes = psbt;
log.debug("Provided string is not a PSBT"); parse();
return;
} }
if (Utils.isBase64(strPSBT) && !Utils.isHex(strPSBT)) { private void parse() {
this.strPSBT = Hex.toHexString(Base64.decode(strPSBT));
} else {
this.strPSBT = strPSBT;
}
psbtBytes = Hex.decode(this.strPSBT);
psbtByteBuffer = ByteBuffer.wrap(psbtBytes);
read();
}
public PSBT(byte[] psbt) throws Exception {
this(Hex.toHexString(psbt));
}
public void read() throws Exception {
int seenInputs = 0; int seenInputs = 0;
int seenOutputs = 0; int seenOutputs = 0;
psbtBytes = Hex.decode(strPSBT); ByteBuffer psbtByteBuffer = ByteBuffer.wrap(psbtBytes);
psbtByteBuffer = ByteBuffer.wrap(psbtBytes);
log.debug("--- ***** START ***** ---");
log.debug("--- PSBT length:" + psbtBytes.length + "---");
log.debug("--- parsing header ---");
byte[] magicBuf = new byte[4]; byte[] magicBuf = new byte[4];
psbtByteBuffer.get(magicBuf); psbtByteBuffer.get(magicBuf);
if (!PSBT.PSBT_MAGIC.equalsIgnoreCase(Hex.toHexString(magicBuf))) { if (!PSBT_MAGIC.equalsIgnoreCase(Hex.toHexString(magicBuf))) {
throw new Exception("Invalid magic value"); throw new IllegalStateException("PSBT has invalid magic value");
} }
byte sep = psbtByteBuffer.get(); byte sep = psbtByteBuffer.get();
if (sep != (byte) 0xff) { if (sep != (byte) 0xff) {
throw new Exception("Bad 0xff separator:" + Hex.toHexString(new byte[]{sep})); throw new IllegalStateException("PSBT has bad initial separator:" + Hex.toHexString(new byte[]{sep}));
} }
int currentState = STATE_GLOBALS; int currentState = STATE_GLOBALS;
PSBTInput currentInput = new PSBTInput(); List<PSBTEntry> globalEntries = new ArrayList<>();
PSBTOutput currentOutput = new PSBTOutput(); List<List<PSBTEntry>> inputEntryLists = new ArrayList<>();
List<List<PSBTEntry>> outputEntryLists = new ArrayList<>();
List<PSBTEntry> inputEntries = new ArrayList<>();
List<PSBTEntry> outputEntries = new ArrayList<>();
while (psbtByteBuffer.hasRemaining()) { while (psbtByteBuffer.hasRemaining()) {
if (currentState == STATE_GLOBALS) { PSBTEntry entry = parseEntry(psbtByteBuffer);
log.debug("--- parsing globals ---");
} else if (currentState == STATE_INPUTS) {
log.debug("--- parsing inputs ---");
} else if (currentState == STATE_OUTPUTS) {
log.debug("--- parsing outputs ---");
}
PSBTEntry entry = parse(); if(entry.getKey() == null) { // length == 0
if (entry == null) {
log.debug("PSBT parse returned null entry");
}
if (entry.getKey() == null) { // length == 0
switch (currentState) { switch (currentState) {
case STATE_GLOBALS: case STATE_GLOBALS:
currentState = STATE_INPUTS; currentState = STATE_INPUTS;
parseGlobalEntries(globalEntries);
break; break;
case STATE_INPUTS: case STATE_INPUTS:
psbtInputs.add(currentInput); inputEntryLists.add(inputEntries);
currentInput = new PSBTInput(); inputEntries = new ArrayList<>();
seenInputs++; seenInputs++;
if (seenInputs == inputs) { if (seenInputs == inputs) {
currentState = STATE_OUTPUTS; currentState = STATE_OUTPUTS;
parseInputEntries(inputEntryLists);
} }
break; break;
case STATE_OUTPUTS: case STATE_OUTPUTS:
psbtOutputs.add(currentOutput); outputEntryLists.add(outputEntries);
currentOutput = new PSBTOutput(); outputEntries = new ArrayList<>();
seenOutputs++; seenOutputs++;
if (seenOutputs == outputs) { if (seenOutputs == outputs) {
currentState = STATE_END; currentState = STATE_END;
parseOutputEntries(outputEntryLists);
} }
break; break;
case STATE_END: case STATE_END:
parseOK = true;
break; break;
default: default:
log.debug("PSBT read is in unknown state"); throw new IllegalStateException("PSBT read is in unknown state");
break;
} }
} else if (currentState == STATE_GLOBALS) { } else if (currentState == STATE_GLOBALS) {
switch (entry.getKeyType()[0]) { globalEntries.add(entry);
case PSBT.PSBT_GLOBAL_UNSIGNED_TX:
Transaction transaction = new Transaction(entry.getData());
inputs = transaction.getInputs().size();
outputs = transaction.getOutputs().size();
log.debug("Transaction with txid: " + transaction.getTxId() + " version " + transaction.getVersion() + " size " + transaction.getMessageSize() + " locktime " + transaction.getLockTime());
for(TransactionInput input: transaction.getInputs()) {
log.debug(" Transaction input references txid: " + input.getOutpoint().getHash() + " vout " + input.getOutpoint().getIndex() + " with script " + input.getScript());
}
for(TransactionOutput output: transaction.getOutputs()) {
log.debug(" Transaction output value: " + output.getValue() + " to addresses " + Arrays.asList(output.getScript().getToAddresses()) + " with script hex " + Hex.toHexString(output.getScript().getProgram()) + " to script " + output.getScript());
}
setTransaction(transaction);
break;
case PSBT.PSBT_GLOBAL_BIP32_PUBKEY:
KeyDerivation keyDerivation = parseKeyDerivation(entry.getData());
ExtendedPublicKey pubKey = ExtendedPublicKey.fromDescriptor(keyDerivation.getMasterFingerprint(), keyDerivation.getDerivationPath(), Base58.encodeChecked(entry.getKeyData()), null);
addExtendedPublicKey(pubKey, keyDerivation);
log.debug("Pubkey with master fingerprint " + pubKey.getMasterFingerprint() + " at path " + pubKey.getKeyDerivationPath() + ": " + pubKey.getExtendedPublicKey());
break;
case PSBT.PSBT_GLOBAL_VERSION:
int version = (int)Utils.readUint32(entry.getData(), 0);
setVersion(version);
log.debug("PSBT version: " + version);
break;
case PSBT.PSBT_GLOBAL_PROPRIETARY:
addProprietary(Hex.toHexString(entry.getKeyData()), Hex.toHexString(entry.getData()));
log.debug("PSBT global proprietary data: " + Hex.toHexString(entry.getData()));
break;
default:
log.debug("PSBT global not recognized key type: " + entry.getKeyType()[0]);
break;
}
} else if (currentState == STATE_INPUTS) { } else if (currentState == STATE_INPUTS) {
switch (entry.getKeyType()[0]) { inputEntries.add(entry);
case PSBT.PSBT_IN_NON_WITNESS_UTXO:
Transaction nonWitnessTx = new Transaction(entry.getData());
currentInput.setNonWitnessUtxo(nonWitnessTx);
log.debug("Found input non witness utxo with txid: " + nonWitnessTx.getTxId() + " version " + nonWitnessTx.getVersion() + " size " + nonWitnessTx.getMessageSize() + " locktime " + nonWitnessTx.getLockTime());
for(TransactionInput input: nonWitnessTx.getInputs()) {
log.debug(" Transaction input references txid: " + input.getOutpoint().getHash() + " vout " + input.getOutpoint().getIndex() + " with script " + input.getScript());
}
for(TransactionOutput output: nonWitnessTx.getOutputs()) {
log.debug(" Transaction output value: " + output.getValue() + " to addresses " + Arrays.asList(output.getScript().getToAddresses()) + " with script hex " + Hex.toHexString(output.getScript().getProgram()) + " to script " + output.getScript());
}
break;
case PSBT.PSBT_IN_WITNESS_UTXO:
TransactionOutput witnessTxOutput = new TransactionOutput(null, entry.getData(), 0);
currentInput.setWitnessUtxo(witnessTxOutput);
log.debug("Found input witness utxo amount " + witnessTxOutput.getValue() + " script hex " + Hex.toHexString(witnessTxOutput.getScript().getProgram()) + " script " + witnessTxOutput.getScript() + " addresses " + Arrays.asList(witnessTxOutput.getScript().getToAddresses()));
break;
case PSBT.PSBT_IN_PARTIAL_SIG:
LazyECPoint sigPublicKey = new LazyECPoint(ECKey.CURVE.getCurve(), entry.getKeyData());
currentInput.addPartialSignature(sigPublicKey, entry.getData());
log.debug("Found input partial signature with public key " + sigPublicKey + " signature " + Hex.toHexString(entry.getData()));
break;
case PSBT.PSBT_IN_SIGHASH_TYPE:
long sighashType = Utils.readUint32(entry.getData(), 0);
Transaction.SigHash sigHash = Transaction.SigHash.fromInt((int)sighashType);
currentInput.setSigHash(sigHash);
log.debug("Found input sighash_type " + sigHash.toString());
break;
case PSBT.PSBT_IN_REDEEM_SCRIPT:
Script redeemScript = new Script(entry.getData());
currentInput.setRedeemScript(redeemScript);
log.debug("Found input redeem script hex " + Hex.toHexString(redeemScript.getProgram()) + " script " + redeemScript);
break;
case PSBT.PSBT_IN_WITNESS_SCRIPT:
Script witnessScript = new Script(entry.getData());
currentInput.setWitnessScript(witnessScript);
log.debug("Found input witness script hex " + Hex.toHexString(witnessScript.getProgram()) + " script " + witnessScript);
break;
case PSBT.PSBT_IN_BIP32_DERIVATION:
LazyECPoint derivedPublicKey = new LazyECPoint(ECKey.CURVE.getCurve(), entry.getKeyData());
KeyDerivation keyDerivation = parseKeyDerivation(entry.getData());
currentInput.addDerivedPublicKey(derivedPublicKey, keyDerivation);
log.debug("Found input bip32_derivation with master fingerprint " + keyDerivation.getMasterFingerprint() + " at path " + keyDerivation.getDerivationPath() + " public key " + derivedPublicKey);
break;
case PSBT.PSBT_IN_FINAL_SCRIPTSIG:
Script finalScriptSig = new Script(entry.getData());
currentInput.setFinalScriptSig(finalScriptSig);
log.debug("Found input final scriptSig script hex " + Hex.toHexString(finalScriptSig.getProgram()) + " script " + finalScriptSig.toString());
break;
case PSBT.PSBT_IN_FINAL_SCRIPTWITNESS:
Script finalScriptWitness = new Script(entry.getData());
currentInput.setFinalScriptWitness(finalScriptWitness);
log.debug("Found input final scriptWitness script hex " + Hex.toHexString(finalScriptWitness.getProgram()) + " script " + finalScriptWitness.toString());
break;
case PSBT.PSBT_IN_POR_COMMITMENT:
String porMessage = new String(entry.getData(), "UTF-8");
currentInput.setPorCommitment(porMessage);
log.debug("Found input POR commitment message " + porMessage);
break;
case PSBT.PSBT_IN_PROPRIETARY:
currentInput.addProprietary(Hex.toHexString(entry.getKeyData()), Hex.toHexString(entry.getData()));
log.debug("Found proprietary input " + Hex.toHexString(entry.getKeyData()) + ": " + Hex.toHexString(entry.getData()));
break;
default:
log.debug("PSBT input not recognized key type:" + entry.getKeyType()[0]);
break;
}
} else if (currentState == STATE_OUTPUTS) { } else if (currentState == STATE_OUTPUTS) {
switch (entry.getKeyType()[0]) { outputEntries.add(entry);
case PSBT.PSBT_OUT_REDEEM_SCRIPT:
Script redeemScript = new Script(entry.getData());
currentOutput.setRedeemScript(redeemScript);
log.debug("Found output redeem script hex " + Hex.toHexString(redeemScript.getProgram()) + " script " + redeemScript);
break;
case PSBT.PSBT_OUT_WITNESS_SCRIPT:
Script witnessScript = new Script(entry.getData());
currentOutput.setWitnessScript(witnessScript);
log.debug("Found output witness script hex " + Hex.toHexString(witnessScript.getProgram()) + " script " + witnessScript);
break;
case PSBT.PSBT_OUT_BIP32_DERIVATION:
LazyECPoint publicKey = new LazyECPoint(ECKey.CURVE.getCurve(), entry.getKeyData());
KeyDerivation keyDerivation = parseKeyDerivation(entry.getData());
currentOutput.addDerivedPublicKey(publicKey, keyDerivation);
log.debug("Found output bip32_derivation with master fingerprint " + keyDerivation.getMasterFingerprint() + " at path " + keyDerivation.getDerivationPath() + " public key " + publicKey);
break;
case PSBT.PSBT_OUT_PROPRIETARY:
currentOutput.addProprietary(Hex.toHexString(entry.getKeyData()), Hex.toHexString(entry.getData()));
log.debug("Found proprietary output " + Hex.toHexString(entry.getKeyData()) + ": " + Hex.toHexString(entry.getData()));
break;
default:
log.debug("PSBT output not recognized key type:" + entry.getKeyType()[0]);
break;
}
} else { } else {
log.debug("PSBT structure invalid"); throw new IllegalStateException("PSBT structure invalid");
}
}
if (currentState == STATE_END) {
log.debug("--- ***** END ***** ---");
} }
} }
private PSBTEntry parse() { if(currentState != STATE_END) {
if(transaction == null) {
throw new IllegalStateException("Missing transaction");
}
if(currentState == STATE_INPUTS) {
throw new IllegalStateException("Missing inputs");
}
if(currentState == STATE_OUTPUTS) {
throw new IllegalStateException("Missing outputs");
}
}
}
private PSBTEntry parseEntry(ByteBuffer psbtByteBuffer) {
PSBTEntry entry = new PSBTEntry(); PSBTEntry entry = new PSBTEntry();
try { try {
int keyLen = PSBT.readCompactInt(psbtByteBuffer); int keyLen = PSBT.readCompactInt(psbtByteBuffer);
log.debug("PSBT entry key length: " + keyLen);
if (keyLen == 0x00) { if (keyLen == 0x00) {
log.debug("PSBT entry separator 0x00");
return entry; return entry;
} }
byte[] key = new byte[keyLen]; byte[] key = new byte[keyLen];
psbtByteBuffer.get(key); psbtByteBuffer.get(key);
log.debug("PSBT entry key: " + Hex.toHexString(key));
byte[] keyType = new byte[1]; byte keyType = key[0];
keyType[0] = key[0];
log.debug("PSBT entry key type: " + Hex.toHexString(keyType));
byte[] keyData = null; byte[] keyData = null;
if (key.length > 1) { if (key.length > 1) {
keyData = new byte[key.length - 1]; keyData = new byte[key.length - 1];
System.arraycopy(key, 1, keyData, 0, keyData.length); System.arraycopy(key, 1, keyData, 0, keyData.length);
log.debug("PSBT entry key data: " + Hex.toHexString(keyData));
} }
int dataLen = PSBT.readCompactInt(psbtByteBuffer); int dataLen = PSBT.readCompactInt(psbtByteBuffer);
log.debug("PSBT entry data length: " + dataLen);
byte[] data = new byte[dataLen]; byte[] data = new byte[dataLen];
psbtByteBuffer.get(data); psbtByteBuffer.get(data);
log.debug("PSBT entry data: " + Hex.toHexString(data));
entry.setKey(key); entry.setKey(key);
entry.setKeyType(keyType); entry.setKeyType(keyType);
@ -340,14 +169,13 @@ public class PSBT {
return entry; return entry;
} catch (Exception e) { } catch (Exception e) {
log.debug("Error parsing PSBT entry", e); throw new IllegalStateException("Error parsing PSBT entry", e);
return null;
} }
} }
private PSBTEntry populateEntry(byte type, byte[] keydata, byte[] data) throws Exception { private PSBTEntry populateEntry(byte type, byte[] keydata, byte[] data) throws Exception {
PSBTEntry entry = new PSBTEntry(); PSBTEntry entry = new PSBTEntry();
entry.setKeyType(new byte[]{type}); entry.setKeyType(type);
entry.setKey(new byte[]{type}); entry.setKey(new byte[]{type});
if (keydata != null) { if (keydata != null) {
entry.setKeyData(keydata); entry.setKeyData(keydata);
@ -357,6 +185,89 @@ public class PSBT {
return entry; return entry;
} }
private void parseGlobalEntries(List<PSBTEntry> globalEntries) {
PSBTEntry duplicate = findDuplicateKey(globalEntries);
if(duplicate != null) {
throw new IllegalStateException("Found duplicate key for PSBT global: " + Hex.toHexString(duplicate.getKey()));
}
for(PSBTEntry entry : globalEntries) {
switch(entry.getKeyType()) {
case PSBT_GLOBAL_UNSIGNED_TX:
entry.checkOneByteKey();
Transaction transaction = new Transaction(entry.getData());
inputs = transaction.getInputs().size();
outputs = transaction.getOutputs().size();
log.debug("Transaction with txid: " + transaction.getTxId() + " version " + transaction.getVersion() + " size " + transaction.getMessageSize() + " locktime " + transaction.getLockTime());
for(TransactionInput input: transaction.getInputs()) {
if(input.getScript().getProgram().length != 0) {
throw new IllegalStateException("Unsigned tx input does not have empty scriptSig");
}
log.debug(" Transaction input references txid: " + input.getOutpoint().getHash() + " vout " + input.getOutpoint().getIndex() + " with script " + input.getScript());
}
for(TransactionOutput output: transaction.getOutputs()) {
log.debug(" Transaction output value: " + output.getValue() + " to addresses " + Arrays.asList(output.getScript().getToAddresses()) + " with script hex " + Hex.toHexString(output.getScript().getProgram()) + " to script " + output.getScript());
}
this.transaction = transaction;
break;
case PSBT_GLOBAL_BIP32_PUBKEY:
entry.checkOneBytePlusXpubKey();
KeyDerivation keyDerivation = parseKeyDerivation(entry.getData());
ExtendedPublicKey pubKey = ExtendedPublicKey.fromDescriptor(keyDerivation.getMasterFingerprint(), keyDerivation.getDerivationPath(), Base58.encodeChecked(entry.getKeyData()), null);
this.extendedPublicKeys.put(pubKey, keyDerivation);
log.debug("Pubkey with master fingerprint " + pubKey.getMasterFingerprint() + " at path " + pubKey.getKeyDerivationPath() + ": " + pubKey.getExtendedPublicKey());
break;
case PSBT_GLOBAL_VERSION:
entry.checkOneByteKey();
int version = (int)Utils.readUint32(entry.getData(), 0);
this.version = version;
log.debug("PSBT version: " + version);
break;
case PSBT_GLOBAL_PROPRIETARY:
globalProprietary.put(Hex.toHexString(entry.getKeyData()), Hex.toHexString(entry.getData()));
log.debug("PSBT global proprietary data: " + Hex.toHexString(entry.getData()));
break;
default:
throw new IllegalStateException("PSBT global not recognized key type: " + entry.getKeyType());
}
}
}
private void parseInputEntries(List<List<PSBTEntry>> inputEntryLists) {
for(List<PSBTEntry> inputEntries : inputEntryLists) {
PSBTEntry duplicate = findDuplicateKey(inputEntries);
if(duplicate != null) {
throw new IllegalStateException("Found duplicate key for PSBT input: " + Hex.toHexString(duplicate.getKey()));
}
PSBTInput input = new PSBTInput(inputEntries);
this.psbtInputs.add(input);
}
}
private void parseOutputEntries(List<List<PSBTEntry>> outputEntryLists) {
for(List<PSBTEntry> outputEntries : outputEntryLists) {
PSBTEntry duplicate = findDuplicateKey(outputEntries);
if(duplicate != null) {
throw new IllegalStateException("Found duplicate key for PSBT output: " + Hex.toHexString(duplicate.getKey()));
}
PSBTOutput output = new PSBTOutput(outputEntries);
this.psbtOutputs.add(output);
}
}
private PSBTEntry findDuplicateKey(List<PSBTEntry> entries) {
Set checkSet = new HashSet();
for(PSBTEntry entry: entries) {
if(!checkSet.add(Hex.toHexString(entry.getKey())) ) {
return entry;
}
}
return null;
}
public byte[] serialize() throws IOException { public byte[] serialize() throws IOException {
ByteArrayOutputStream transactionbaos = new ByteArrayOutputStream(); ByteArrayOutputStream transactionbaos = new ByteArrayOutputStream();
transaction.bitcoinSerialize(transactionbaos); transaction.bitcoinSerialize(transactionbaos);
@ -365,7 +276,7 @@ public class PSBT {
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream();
// magic // magic
baos.write(Hex.decode(PSBT.PSBT_MAGIC), 0, Hex.decode(PSBT.PSBT_MAGIC).length); baos.write(Hex.decode(PSBT_MAGIC), 0, Hex.decode(PSBT_MAGIC).length);
// separator // separator
baos.write((byte) 0xff); baos.write((byte) 0xff);
@ -412,8 +323,6 @@ public class PSBT {
baos.write((byte) 0x00); baos.write((byte) 0x00);
psbtBytes = baos.toByteArray(); psbtBytes = baos.toByteArray();
strPSBT = Hex.toHexString(psbtBytes);
log.debug("Wrote PSBT: " + strPSBT);
return psbtBytes; return psbtBytes;
} }
@ -430,20 +339,10 @@ public class PSBT {
return transaction; return transaction;
} }
public void setTransaction(Transaction transaction) {
testIfNull(this.transaction);
this.transaction = transaction;
}
public Integer getVersion() { public Integer getVersion() {
return version; return version;
} }
public void setVersion(Integer version) {
testIfNull(this.version);
this.version = version;
}
public KeyDerivation getKeyDerivation(ExtendedPublicKey publicKey) { public KeyDerivation getKeyDerivation(ExtendedPublicKey publicKey) {
return extendedPublicKeys.get(publicKey); return extendedPublicKeys.get(publicKey);
} }
@ -452,24 +351,6 @@ public class PSBT {
return new ArrayList<ExtendedPublicKey>(extendedPublicKeys.keySet()); return new ArrayList<ExtendedPublicKey>(extendedPublicKeys.keySet());
} }
public void addExtendedPublicKey(ExtendedPublicKey publicKey, KeyDerivation derivation) {
if(extendedPublicKeys.containsKey(publicKey)) {
throw new IllegalStateException("Duplicate public key in scope");
}
this.extendedPublicKeys.put(publicKey, derivation);
}
public void addProprietary(String key, String data) {
globalProprietary.put(key, data);
}
private void testIfNull(Object obj) {
if(obj != null) {
throw new IllegalStateException("Duplicate keys in scope");
}
}
public String toString() { public String toString() {
try { try {
return Hex.toHexString(serialize()); return Hex.toHexString(serialize());
@ -558,41 +439,6 @@ public class PSBT {
return ret; return ret;
} }
public KeyDerivation parseKeyDerivation(byte[] data) {
String masterFingerprint = getMasterFingerprint(Arrays.copyOfRange(data, 0, 4));
List<ChildNumber> bip32pathList = readBIP32Derivation(Arrays.copyOfRange(data, 4, data.length));
String bip32path = KeyDerivation.writePath(bip32pathList);
return new KeyDerivation(masterFingerprint, bip32path);
}
public static String getMasterFingerprint(byte[] data) {
return Hex.toHexString(data);
}
public static List<ChildNumber> readBIP32Derivation(byte[] data) {
List<ChildNumber> path = new ArrayList<>();
ByteBuffer bb = ByteBuffer.wrap(data);
byte[] buf = new byte[4];
do {
bb.get(buf);
reverse(buf);
ByteBuffer pbuf = ByteBuffer.wrap(buf);
path.add(new ChildNumber(pbuf.getInt()));
} while(bb.hasRemaining());
return path;
}
private static void reverse(byte[] array) {
for (int i = 0; i < array.length / 2; i++) {
byte temp = array[i];
array[i] = array[array.length - i - 1];
array[array.length - i - 1] = temp;
}
}
public static byte[] writeBIP32Derivation(byte[] fingerprint, int purpose, int type, int account, int chain, int index) { public static byte[] writeBIP32Derivation(byte[] fingerprint, int purpose, int type, int account, int chain, int index) {
// fingerprint and integer values to BIP32 derivation buffer // fingerprint and integer values to BIP32 derivation buffer
byte[] bip32buf = new byte[24]; byte[] bip32buf = new byte[24];
@ -634,17 +480,30 @@ public class PSBT {
} }
public static boolean isPSBT(String s) { public static boolean isPSBT(String s) {
if (Utils.isHex(s) && s.startsWith(PSBT.PSBT_MAGIC)) { if (Utils.isHex(s) && s.startsWith(PSBT_MAGIC)) {
return true; return true;
} else if (Utils.isBase64(s) && Hex.toHexString(Base64.decode(s)).startsWith(PSBT.PSBT_MAGIC)) { } else if (Utils.isBase64(s) && Hex.toHexString(Base64.decode(s)).startsWith(PSBT_MAGIC)) {
return true; return true;
} else { } else {
return false; return false;
} }
} }
public static PSBT fromString(String strPSBT) {
if (!isPSBT(strPSBT)) {
throw new IllegalArgumentException("Provided string is not a PSBT");
}
if (Utils.isBase64(strPSBT) && !Utils.isHex(strPSBT)) {
strPSBT = Hex.toHexString(Base64.decode(strPSBT));
}
byte[] psbtBytes = Hex.decode(strPSBT);
return new PSBT(psbtBytes);
}
public static void main(String[] args) throws Exception { public static void main(String[] args) throws Exception {
String psbtBase64 = "cHNidP8BAMkCAAAAA3lxWr8zSZt5tiGZegyFWmd8b62cew6qi/4rTZGGif8OAAAAAAD/////td4T4zmwdQ8R2SbwRjRj+alAy1VX8mYZD2o9ZmefNIsAAAAAAP////+k9Xvvp9Lpap1TWd51NWu+MIfojG+MCqmguPyjII+5YgAAAAAA/////wKMz/AIAAAAABl2qRSE7GtWKUoaFcVQ8n9qfMYi41Yh0YisjM/wCAAAAAAZdqkUmka3O8TiIRG8h+a1mDLFQVTfJEiIrAAAAAAAAQBVAgAAAAGt3gAAAAAAAO++AAAAAAAAAAAAAAAAAAAAAAAAAAAAAEkAAAAA/////wEA4fUFAAAAABl2qRSvQiRNb8B3El3G+KdspA3+DRvH1IisAAAAACIGA383lPO+TErMCGrITWkCwCVxPqv4iQ8g9ErPCzTjwPD3DHSXSzsAAAAAAAAAAAABAFUCAAAAAa3eAAAAAAAA774AAAAAAAAAAAAAAAAAAAAAAAAAAAAASQAAAAD/////AQDh9QUAAAAAGXapFAn8nw1IXPh34v8wuhJrcu34Xg8qiKwAAAAAIgYDTr6iJ7sP/u+0gz4wi+Muuc4IxEoJaGYedN/uqwmSfbgMdJdLOwAAAAABAAAAAAEAVQIAAAABrd4AAAAAAADvvgAAAAAAAAAAAAAAAAAAAAAAAAAAAABJAAAAAP////8BAOH1BQAAAAAZdqkUGMIzFJsgyFIYzDbThZ5S2zTnvRiIrAAAAAAiBgK7oYu+Z/kEK6XK3urdEDW2ngkwnXD1gZBjEgRW0wD7Igx0l0s7AAAAAAIAAAAAACICAyw+nsM8JYHohVqRsQ2qilEwjZPh+OkGPqkO2kYZczCZEHSXSzsMAAAAIgAAADcBAAAA"; String psbtBase64 = "cHNidP8BAHUCAAAAASaBcTce3/KF6Tet7qSze3gADAVmy7OtZGQXE8pCFxv2AAAAAAD+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAAAAA";
PSBT psbt = null; PSBT psbt = null;
String filename = "default.psbt"; String filename = "default.psbt";
@ -656,7 +515,7 @@ public class PSBT {
stream.close(); stream.close();
psbt = new PSBT(psbtBytes); psbt = new PSBT(psbtBytes);
} else { } else {
psbt = new PSBT(psbtBase64); psbt = PSBT.fromString(psbtBase64);
} }
System.out.println(psbt); System.out.println(psbt);

View file

@ -1,8 +1,17 @@
package com.craigraw.drongo.psbt; package com.craigraw.drongo.psbt;
import com.craigraw.drongo.KeyDerivation;
import com.craigraw.drongo.crypto.ChildNumber;
import org.bouncycastle.util.encoders.Hex;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class PSBTEntry { public class PSBTEntry {
private byte[] key = null; private byte[] key = null;
private byte[] keyType = null; private byte keyType;
private byte[] keyData = null; private byte[] keyData = null;
private byte[] data = null; private byte[] data = null;
@ -14,11 +23,11 @@ public class PSBTEntry {
this.key = key; this.key = key;
} }
public byte[] getKeyType() { public byte getKeyType() {
return keyType; return keyType;
} }
public void setKeyType(byte[] keyType) { public void setKeyType(byte keyType) {
this.keyType = keyType; this.keyType = keyType;
} }
@ -37,4 +46,57 @@ public class PSBTEntry {
public void setData(byte[] data) { public void setData(byte[] data) {
this.data = data; this.data = data;
} }
public static KeyDerivation parseKeyDerivation(byte[] data) {
String masterFingerprint = getMasterFingerprint(Arrays.copyOfRange(data, 0, 4));
List<ChildNumber> bip32pathList = readBIP32Derivation(Arrays.copyOfRange(data, 4, data.length));
String bip32path = KeyDerivation.writePath(bip32pathList);
return new KeyDerivation(masterFingerprint, bip32path);
}
public static String getMasterFingerprint(byte[] data) {
return Hex.toHexString(data);
}
public static List<ChildNumber> readBIP32Derivation(byte[] data) {
List<ChildNumber> path = new ArrayList<>();
ByteBuffer bb = ByteBuffer.wrap(data);
byte[] buf = new byte[4];
do {
bb.get(buf);
reverse(buf);
ByteBuffer pbuf = ByteBuffer.wrap(buf);
path.add(new ChildNumber(pbuf.getInt()));
} while(bb.hasRemaining());
return path;
}
private static void reverse(byte[] array) {
for (int i = 0; i < array.length / 2; i++) {
byte temp = array[i];
array[i] = array[array.length - i - 1];
array[array.length - i - 1] = temp;
}
}
public void checkOneByteKey() {
if(this.getKey().length != 1) {
throw new IllegalStateException("PSBT key type must be one byte");
}
}
public void checkOneBytePlusXpubKey() {
if(this.getKey().length != 79) {
throw new IllegalStateException("PSBT key type must be one byte");
}
}
public void checkOneBytePlusPubKey() {
if(this.getKey().length != 34) {
throw new IllegalStateException("PSBT key type must be one byte");
}
}
} }

View file

@ -1,15 +1,38 @@
package com.craigraw.drongo.psbt; package com.craigraw.drongo.psbt;
import com.craigraw.drongo.KeyDerivation; import com.craigraw.drongo.KeyDerivation;
import com.craigraw.drongo.Utils;
import com.craigraw.drongo.crypto.ECKey;
import com.craigraw.drongo.crypto.LazyECPoint; import com.craigraw.drongo.crypto.LazyECPoint;
import com.craigraw.drongo.protocol.Script; import com.craigraw.drongo.protocol.Script;
import com.craigraw.drongo.protocol.Transaction; import com.craigraw.drongo.protocol.Transaction;
import com.craigraw.drongo.protocol.TransactionInput;
import com.craigraw.drongo.protocol.TransactionOutput; import com.craigraw.drongo.protocol.TransactionOutput;
import org.bouncycastle.util.encoders.Hex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import static com.craigraw.drongo.psbt.PSBTEntry.parseKeyDerivation;
public class PSBTInput { public class PSBTInput {
public static final byte PSBT_IN_NON_WITNESS_UTXO = 0x00;
public static final byte PSBT_IN_WITNESS_UTXO = 0x01;
public static final byte PSBT_IN_PARTIAL_SIG = 0x02;
public static final byte PSBT_IN_SIGHASH_TYPE = 0x03;
public static final byte PSBT_IN_REDEEM_SCRIPT = 0x04;
public static final byte PSBT_IN_WITNESS_SCRIPT = 0x05;
public static final byte PSBT_IN_BIP32_DERIVATION = 0x06;
public static final byte PSBT_IN_FINAL_SCRIPTSIG = 0x07;
public static final byte PSBT_IN_FINAL_SCRIPTWITNESS = 0x08;
public static final byte PSBT_IN_POR_COMMITMENT = 0x09;
public static final byte PSBT_IN_PROPRIETARY = (byte)0xfc;
private Transaction nonWitnessUtxo; private Transaction nonWitnessUtxo;
private TransactionOutput witnessUtxo; private TransactionOutput witnessUtxo;
private Map<LazyECPoint, byte[]> partialSignatures = new LinkedHashMap<>(); private Map<LazyECPoint, byte[]> partialSignatures = new LinkedHashMap<>();
@ -22,109 +45,138 @@ public class PSBTInput {
private String porCommitment; private String porCommitment;
private Map<String, String> proprietary = new LinkedHashMap<>(); private Map<String, String> proprietary = new LinkedHashMap<>();
public Transaction getNonWitnessUtxo() { private static final Logger log = LoggerFactory.getLogger(PSBTInput.class);
return nonWitnessUtxo;
PSBTInput(List<PSBTEntry> inputEntries) {
for(PSBTEntry entry : inputEntries) {
switch(entry.getKeyType()) {
case PSBT_IN_NON_WITNESS_UTXO:
entry.checkOneByteKey();
Transaction nonWitnessTx = new Transaction(entry.getData());
this.nonWitnessUtxo = nonWitnessTx;
log.debug("Found input non witness utxo with txid: " + nonWitnessTx.getTxId() + " version " + nonWitnessTx.getVersion() + " size " + nonWitnessTx.getMessageSize() + " locktime " + nonWitnessTx.getLockTime());
for(TransactionInput input: nonWitnessTx.getInputs()) {
log.debug(" Transaction input references txid: " + input.getOutpoint().getHash() + " vout " + input.getOutpoint().getIndex() + " with script " + input.getScript());
}
for(TransactionOutput output: nonWitnessTx.getOutputs()) {
log.debug(" Transaction output value: " + output.getValue() + " to addresses " + Arrays.asList(output.getScript().getToAddresses()) + " with script hex " + Hex.toHexString(output.getScript().getProgram()) + " to script " + output.getScript());
}
break;
case PSBT_IN_WITNESS_UTXO:
entry.checkOneByteKey();
TransactionOutput witnessTxOutput = new TransactionOutput(null, entry.getData(), 0);
this.witnessUtxo = witnessTxOutput;
log.debug("Found input witness utxo amount " + witnessTxOutput.getValue() + " script hex " + Hex.toHexString(witnessTxOutput.getScript().getProgram()) + " script " + witnessTxOutput.getScript() + " addresses " + Arrays.asList(witnessTxOutput.getScript().getToAddresses()));
break;
case PSBT_IN_PARTIAL_SIG:
entry.checkOneBytePlusPubKey();
LazyECPoint sigPublicKey = new LazyECPoint(ECKey.CURVE.getCurve(), entry.getKeyData());
this.partialSignatures.put(sigPublicKey, entry.getData());
log.debug("Found input partial signature with public key " + sigPublicKey + " signature " + Hex.toHexString(entry.getData()));
break;
case PSBT_IN_SIGHASH_TYPE:
entry.checkOneByteKey();
long sighashType = Utils.readUint32(entry.getData(), 0);
Transaction.SigHash sigHash = Transaction.SigHash.fromInt((int)sighashType);
this.sigHash = sigHash;
log.debug("Found input sighash_type " + sigHash.toString());
break;
case PSBT_IN_REDEEM_SCRIPT:
entry.checkOneByteKey();
Script redeemScript = new Script(entry.getData());
this.redeemScript = redeemScript;
log.debug("Found input redeem script hex " + Hex.toHexString(redeemScript.getProgram()) + " script " + redeemScript);
break;
case PSBT_IN_WITNESS_SCRIPT:
entry.checkOneByteKey();
Script witnessScript = new Script(entry.getData());
this.witnessScript = witnessScript;
log.debug("Found input witness script hex " + Hex.toHexString(witnessScript.getProgram()) + " script " + witnessScript);
break;
case PSBT_IN_BIP32_DERIVATION:
entry.checkOneBytePlusPubKey();
LazyECPoint derivedPublicKey = new LazyECPoint(ECKey.CURVE.getCurve(), entry.getKeyData());
KeyDerivation keyDerivation = parseKeyDerivation(entry.getData());
this.derivedPublicKeys.put(derivedPublicKey, keyDerivation);
log.debug("Found input bip32_derivation with master fingerprint " + keyDerivation.getMasterFingerprint() + " at path " + keyDerivation.getDerivationPath() + " public key " + derivedPublicKey);
break;
case PSBT_IN_FINAL_SCRIPTSIG:
entry.checkOneByteKey();
Script finalScriptSig = new Script(entry.getData());
this.finalScriptSig = finalScriptSig;
log.debug("Found input final scriptSig script hex " + Hex.toHexString(finalScriptSig.getProgram()) + " script " + finalScriptSig.toString());
break;
case PSBT_IN_FINAL_SCRIPTWITNESS:
entry.checkOneByteKey();
Script finalScriptWitness = new Script(entry.getData());
this.finalScriptWitness = finalScriptWitness;
log.debug("Found input final scriptWitness script hex " + Hex.toHexString(finalScriptWitness.getProgram()) + " script " + finalScriptWitness.toString());
break;
case PSBT_IN_POR_COMMITMENT:
entry.checkOneByteKey();
String porMessage = new String(entry.getData(), StandardCharsets.UTF_8);
this.porCommitment = porMessage;
log.debug("Found input POR commitment message " + porMessage);
break;
case PSBT_IN_PROPRIETARY:
this.proprietary.put(Hex.toHexString(entry.getKeyData()), Hex.toHexString(entry.getData()));
log.debug("Found proprietary input " + Hex.toHexString(entry.getKeyData()) + ": " + Hex.toHexString(entry.getData()));
break;
default:
throw new IllegalStateException("PSBT input not recognized key type: " + entry.getKeyType());
}
}
} }
public void setNonWitnessUtxo(Transaction nonWitnessUtxo) { public Transaction getNonWitnessUtxo() {
testIfNull(this.nonWitnessUtxo); return nonWitnessUtxo;
this.nonWitnessUtxo = nonWitnessUtxo;
} }
public TransactionOutput getWitnessUtxo() { public TransactionOutput getWitnessUtxo() {
return witnessUtxo; return witnessUtxo;
} }
public void setWitnessUtxo(TransactionOutput witnessUtxo) {
testIfNull(this.witnessUtxo);
this.witnessUtxo = witnessUtxo;
}
public byte[] getPartialSignature(LazyECPoint publicKey) { public byte[] getPartialSignature(LazyECPoint publicKey) {
return partialSignatures.get(publicKey); return partialSignatures.get(publicKey);
} }
public void addPartialSignature(LazyECPoint publicKey, byte[] partialSignature) {
if(partialSignatures.containsKey(publicKey)) {
throw new IllegalStateException("Duplicate public key signature in scope");
}
this.partialSignatures.put(publicKey, partialSignature);
}
public Transaction.SigHash getSigHash() { public Transaction.SigHash getSigHash() {
return sigHash; return sigHash;
} }
public void setSigHash(Transaction.SigHash sigHash) {
testIfNull(this.sigHash);
this.sigHash = sigHash;
}
public Script getRedeemScript() { public Script getRedeemScript() {
return redeemScript; return redeemScript;
} }
public void setRedeemScript(Script redeemScript) {
testIfNull(this.redeemScript);
this.redeemScript = redeemScript;
}
public Script getWitnessScript() { public Script getWitnessScript() {
return witnessScript; return witnessScript;
} }
public void setWitnessScript(Script witnessScript) {
testIfNull(this.witnessScript);
this.witnessScript = witnessScript;
}
public KeyDerivation getKeyDerivation(LazyECPoint publicKey) { public KeyDerivation getKeyDerivation(LazyECPoint publicKey) {
return derivedPublicKeys.get(publicKey); return derivedPublicKeys.get(publicKey);
} }
public void addDerivedPublicKey(LazyECPoint publicKey, KeyDerivation derivation) {
if(derivedPublicKeys.containsKey(publicKey)) {
throw new IllegalStateException("Duplicate public key in scope");
}
this.derivedPublicKeys.put(publicKey, derivation);
}
public Script getFinalScriptSig() { public Script getFinalScriptSig() {
return finalScriptSig; return finalScriptSig;
} }
public void setFinalScriptSig(Script finalScriptSig) {
testIfNull(this.finalScriptSig);
this.finalScriptSig = finalScriptSig;
}
public Script getFinalScriptWitness() { public Script getFinalScriptWitness() {
return finalScriptWitness; return finalScriptWitness;
} }
public void setFinalScriptWitness(Script finalScriptWitness) {
testIfNull(this.finalScriptWitness);
this.finalScriptWitness = finalScriptWitness;
}
public String getPorCommitment() { public String getPorCommitment() {
return porCommitment; return porCommitment;
} }
public void setPorCommitment(String porCommitment) { public Map<LazyECPoint, byte[]> getPartialSignatures() {
testIfNull(this.porCommitment); return partialSignatures;
this.porCommitment = porCommitment;
} }
public void addProprietary(String key, String data) { public Map<LazyECPoint, KeyDerivation> getDerivedPublicKeys() {
proprietary.put(key, data); return derivedPublicKeys;
} }
private void testIfNull(Object obj) { public Map<String, String> getProprietary() {
if(obj != null) { return proprietary;
throw new IllegalStateException("Duplicate keys in scope");
}
} }
} }

View file

@ -1,55 +1,79 @@
package com.craigraw.drongo.psbt; package com.craigraw.drongo.psbt;
import com.craigraw.drongo.KeyDerivation; import com.craigraw.drongo.KeyDerivation;
import com.craigraw.drongo.crypto.ECKey;
import com.craigraw.drongo.crypto.LazyECPoint; import com.craigraw.drongo.crypto.LazyECPoint;
import com.craigraw.drongo.protocol.Script; import com.craigraw.drongo.protocol.Script;
import org.bouncycastle.util.encoders.Hex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
public class PSBTOutput { public class PSBTOutput {
public static final byte PSBT_OUT_REDEEM_SCRIPT = 0x00;
public static final byte PSBT_OUT_WITNESS_SCRIPT = 0x01;
public static final byte PSBT_OUT_BIP32_DERIVATION = 0x02;
public static final byte PSBT_OUT_PROPRIETARY = (byte)0xfc;
private Script redeemScript; private Script redeemScript;
private Script witnessScript; private Script witnessScript;
private Map<LazyECPoint, KeyDerivation> derivedPublicKeys = new LinkedHashMap<>(); private Map<LazyECPoint, KeyDerivation> derivedPublicKeys = new LinkedHashMap<>();
private Map<String, String> proprietary = new LinkedHashMap<>(); private Map<String, String> proprietary = new LinkedHashMap<>();
public Script getRedeemScript() { private static final Logger log = LoggerFactory.getLogger(PSBTOutput.class);
return redeemScript;
PSBTOutput(List<PSBTEntry> outputEntries) {
for(PSBTEntry entry : outputEntries) {
switch (entry.getKeyType()) {
case PSBT_OUT_REDEEM_SCRIPT:
entry.checkOneByteKey();
Script redeemScript = new Script(entry.getData());
this.redeemScript = redeemScript;
log.debug("Found output redeem script hex " + Hex.toHexString(redeemScript.getProgram()) + " script " + redeemScript);
break;
case PSBT_OUT_WITNESS_SCRIPT:
entry.checkOneByteKey();
Script witnessScript = new Script(entry.getData());
this.witnessScript = witnessScript;
log.debug("Found output witness script hex " + Hex.toHexString(witnessScript.getProgram()) + " script " + witnessScript);
break;
case PSBT_OUT_BIP32_DERIVATION:
entry.checkOneBytePlusPubKey();
LazyECPoint publicKey = new LazyECPoint(ECKey.CURVE.getCurve(), entry.getKeyData());
KeyDerivation keyDerivation = PSBTEntry.parseKeyDerivation(entry.getData());
this.derivedPublicKeys.put(publicKey, keyDerivation);
log.debug("Found output bip32_derivation with master fingerprint " + keyDerivation.getMasterFingerprint() + " at path " + keyDerivation.getDerivationPath() + " public key " + publicKey);
break;
case PSBT_OUT_PROPRIETARY:
proprietary.put(Hex.toHexString(entry.getKeyData()), Hex.toHexString(entry.getData()));
log.debug("Found proprietary output " + Hex.toHexString(entry.getKeyData()) + ": " + Hex.toHexString(entry.getData()));
break;
default:
throw new IllegalStateException("PSBT output not recognized key type: " + entry.getKeyType());
}
}
} }
public void setRedeemScript(Script redeemScript) { public Script getRedeemScript() {
testIfNull(this.redeemScript); return redeemScript;
this.redeemScript = redeemScript;
} }
public Script getWitnessScript() { public Script getWitnessScript() {
return witnessScript; return witnessScript;
} }
public void setWitnessScript(Script witnessScript) {
testIfNull(this.witnessScript);
this.witnessScript = witnessScript;
}
public KeyDerivation getKeyDerivation(LazyECPoint publicKey) { public KeyDerivation getKeyDerivation(LazyECPoint publicKey) {
return derivedPublicKeys.get(publicKey); return derivedPublicKeys.get(publicKey);
} }
public void addDerivedPublicKey(LazyECPoint publicKey, KeyDerivation derivation) { public Map<LazyECPoint, KeyDerivation> getDerivedPublicKeys() {
if(derivedPublicKeys.containsKey(publicKey)) { return derivedPublicKeys;
throw new IllegalStateException("Duplicate public key in scope");
} }
this.derivedPublicKeys.put(publicKey, derivation); public Map<String, String> getProprietary() {
} return proprietary;
public void addProprietary(String key, String data) {
proprietary.put(key, data);
}
private void testIfNull(Object obj) {
if(obj != null) {
throw new IllegalStateException("Duplicate keys in scope");
}
} }
} }

View file

@ -0,0 +1,151 @@
package com.craigraw.drongo.psbt;
import org.junit.Assert;
import org.junit.Test;
public class PSBTTest {
@Test(expected = IllegalArgumentException.class)
public void invalidNotPSBT() {
String psbt = "AgAAAAEmgXE3Ht/yhek3re6ks3t4AAwFZsuzrWRkFxPKQhcb9gAAAABqRzBEAiBwsiRRI+a/R01gxbUMBD1MaRpdJDXwmjSnZiqdwlF5CgIgATKcqdrPKAvfMHQOwDkEIkIsgctFg5RXrrdvwS7dlbMBIQJlfRGNM1e44PTCzUbbezn22cONmnCry5st5dyNv+TOMf7///8C09/1BQAAAAAZdqkU0MWZA8W6woaHYOkP1SGkZlqnZSCIrADh9QUAAAAAF6kUNUXm4zuDLEcFDyTT7rk8nAOUi8eHsy4TAA==";
PSBT.fromString(psbt);
}
@Test(expected = IllegalStateException.class)
public void missingOutputs() {
String psbt = "cHNidP8BAHUCAAAAASaBcTce3/KF6Tet7qSze3gADAVmy7OtZGQXE8pCFxv2AAAAAAD+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAAA==";
PSBT.fromString(psbt);
}
@Test(expected = IllegalStateException.class)
public void unsignedTxWithScriptSig() {
String psbt = "cHNidP8BAP0KAQIAAAACqwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QAAAAAakcwRAIgR1lmF5fAGwNrJZKJSGhiGDR9iYZLcZ4ff89X0eURZYcCIFMJ6r9Wqk2Ikf/REf3xM286KdqGbX+EhtdVRs7tr5MZASEDXNxh/HupccC1AaZGoqg7ECy0OIEhfKaC3Ibi1z+ogpL+////qwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QBAAAAAP7///8CYDvqCwAAAAAZdqkUdopAu9dAy+gdmI5x3ipNXHE5ax2IrI4kAAAAAAAAGXapFG9GILVT+glechue4O/p+gOcykWXiKwAAAAAAAABASAA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHhwEEFgAUhdE1N/LiZUBaNNuvqePdoB+4IwgAAAA=";
PSBT.fromString(psbt);
}
@Test(expected = IllegalStateException.class)
public void noTransactionOnlyInputs() {
String psbt = "cHNidP8AAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAAA==";
PSBT.fromString(psbt);
}
@Test(expected = IllegalStateException.class)
public void duplicateInputKeys() {
String psbt = "cHNidP8BAHUCAAAAASaBcTce3/KF6Tet7qSze3gADAVmy7OtZGQXE8pCFxv2AAAAAAD+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAAQA/AgAAAAH//////////////////////////////////////////wAAAAAA/////wEAAAAAAAAAAANqAQAAAAAAAAAA";
PSBT.fromString(psbt);
}
@Test(expected = IllegalStateException.class)
public void invalidGlobalTransactionKeyType() {
String psbt = "cHNidP8CAAFVAgAAAAEnmiMjpd+1H8RfIg+liw/BPh4zQnkqhdfjbNYzO1y8OQAAAAAA/////wGgWuoLAAAAABl2qRT/6cAGEJfMO2NvLLBGD6T8Qn0rRYisAAAAAAABASCVXuoLAAAAABepFGNFIA9o0YnhrcDfHE0W6o8UwNvrhyICA7E0HMunaDtq9PEjjNbpfnFn1Wn6xH8eSNR1QYRDVb1GRjBDAiAEJLWO/6qmlOFVnqXJO7/UqJBkIkBVzfBwtncUaUQtBwIfXI6w/qZRbWC4rLM61k7eYOh4W/s6qUuZvfhhUduamgEBBCIAIHcf0YrUWWZt1J89Vk49vEL0yEd042CtoWgWqO1IjVaBAQVHUiEDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUYhA95V0eHayAXj+KWMH7+blMAvPbqv4Sf+/KSZXyb4IIO9Uq4iBgOxNBzLp2g7avTxI4zW6X5xZ9Vp+sR/HkjUdUGEQ1W9RhC0prpnAAAAgAAAAIAEAACAIgYD3lXR4drIBeP4pYwfv5uUwC89uq/hJ/78pJlfJvggg70QtKa6ZwAAAIAAAACABQAAgAAA";
PSBT.fromString(psbt);
}
@Test(expected = IllegalStateException.class)
public void invalidInputWitnessUtxoKeyType() {
String psbt = "cHNidP8BAFUCAAAAASeaIyOl37UfxF8iD6WLD8E+HjNCeSqF1+Ns1jM7XLw5AAAAAAD/////AaBa6gsAAAAAGXapFP/pwAYQl8w7Y28ssEYPpPxCfStFiKwAAAAAAAIBACCVXuoLAAAAABepFGNFIA9o0YnhrcDfHE0W6o8UwNvrhyICA7E0HMunaDtq9PEjjNbpfnFn1Wn6xH8eSNR1QYRDVb1GRjBDAiAEJLWO/6qmlOFVnqXJO7/UqJBkIkBVzfBwtncUaUQtBwIfXI6w/qZRbWC4rLM61k7eYOh4W/s6qUuZvfhhUduamgEBBCIAIHcf0YrUWWZt1J89Vk49vEL0yEd042CtoWgWqO1IjVaBAQVHUiEDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUYhA95V0eHayAXj+KWMH7+blMAvPbqv4Sf+/KSZXyb4IIO9Uq4iBgOxNBzLp2g7avTxI4zW6X5xZ9Vp+sR/HkjUdUGEQ1W9RhC0prpnAAAAgAAAAIAEAACAIgYD3lXR4drIBeP4pYwfv5uUwC89uq/hJ/78pJlfJvggg70QtKa6ZwAAAIAAAACABQAAgAAA";
PSBT.fromString(psbt);
}
@Test(expected = IllegalStateException.class)
public void invalidInputPartialSignatureKeyType() {
String psbt = "cHNidP8BAFUCAAAAASeaIyOl37UfxF8iD6WLD8E+HjNCeSqF1+Ns1jM7XLw5AAAAAAD/////AaBa6gsAAAAAGXapFP/pwAYQl8w7Y28ssEYPpPxCfStFiKwAAAAAAAEBIJVe6gsAAAAAF6kUY0UgD2jRieGtwN8cTRbqjxTA2+uHIQIDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUYwQwIgBCS1jv+qppThVZ6lyTu/1KiQZCJAVc3wcLZ3FGlELQcCH1yOsP6mUW1guKyzOtZO3mDoeFv7OqlLmb34YVHbmpoBAQQiACB3H9GK1FlmbdSfPVZOPbxC9MhHdONgraFoFqjtSI1WgQEFR1IhA7E0HMunaDtq9PEjjNbpfnFn1Wn6xH8eSNR1QYRDVb1GIQPeVdHh2sgF4/iljB+/m5TALz26r+En/vykmV8m+CCDvVKuIgYDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUYQtKa6ZwAAAIAAAACABAAAgCIGA95V0eHayAXj+KWMH7+blMAvPbqv4Sf+/KSZXyb4IIO9ELSmumcAAACAAAAAgAUAAIAAAA==";
PSBT.fromString(psbt);
}
@Test(expected = IllegalStateException.class)
public void invalidInputRedeemScriptKeyType() {
String psbt = "cHNidP8BAFUCAAAAASeaIyOl37UfxF8iD6WLD8E+HjNCeSqF1+Ns1jM7XLw5AAAAAAD/////AaBa6gsAAAAAGXapFP/pwAYQl8w7Y28ssEYPpPxCfStFiKwAAAAAAAEBIJVe6gsAAAAAF6kUY0UgD2jRieGtwN8cTRbqjxTA2+uHIgIDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUZGMEMCIAQktY7/qqaU4VWepck7v9SokGQiQFXN8HC2dxRpRC0HAh9cjrD+plFtYLisszrWTt5g6Hhb+zqpS5m9+GFR25qaAQIEACIAIHcf0YrUWWZt1J89Vk49vEL0yEd042CtoWgWqO1IjVaBAQVHUiEDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUYhA95V0eHayAXj+KWMH7+blMAvPbqv4Sf+/KSZXyb4IIO9Uq4iBgOxNBzLp2g7avTxI4zW6X5xZ9Vp+sR/HkjUdUGEQ1W9RhC0prpnAAAAgAAAAIAEAACAIgYD3lXR4drIBeP4pYwfv5uUwC89uq/hJ/78pJlfJvggg70QtKa6ZwAAAIAAAACABQAAgAAA";
PSBT.fromString(psbt);
}
@Test(expected = IllegalStateException.class)
public void invalidInputWitnessScriptKeyType() {
String psbt = "cHNidP8BAFUCAAAAASeaIyOl37UfxF8iD6WLD8E+HjNCeSqF1+Ns1jM7XLw5AAAAAAD/////AaBa6gsAAAAAGXapFP/pwAYQl8w7Y28ssEYPpPxCfStFiKwAAAAAAAEBIJVe6gsAAAAAF6kUY0UgD2jRieGtwN8cTRbqjxTA2+uHIgIDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUZGMEMCIAQktY7/qqaU4VWepck7v9SokGQiQFXN8HC2dxRpRC0HAh9cjrD+plFtYLisszrWTt5g6Hhb+zqpS5m9+GFR25qaAQEEIgAgdx/RitRZZm3Unz1WTj28QvTIR3TjYK2haBao7UiNVoECBQBHUiEDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUYhA95V0eHayAXj+KWMH7+blMAvPbqv4Sf+/KSZXyb4IIO9Uq4iBgOxNBzLp2g7avTxI4zW6X5xZ9Vp+sR/HkjUdUGEQ1W9RhC0prpnAAAAgAAAAIAEAACAIgYD3lXR4drIBeP4pYwfv5uUwC89uq/hJ/78pJlfJvggg70QtKa6ZwAAAIAAAACABQAAgAAA";
PSBT.fromString(psbt);
}
@Test(expected = IllegalStateException.class)
public void invalidInputBip32KeyType() {
String psbt = "cHNidP8BAFUCAAAAASeaIyOl37UfxF8iD6WLD8E+HjNCeSqF1+Ns1jM7XLw5AAAAAAD/////AaBa6gsAAAAAGXapFP/pwAYQl8w7Y28ssEYPpPxCfStFiKwAAAAAAAEBIJVe6gsAAAAAF6kUY0UgD2jRieGtwN8cTRbqjxTA2+uHIgIDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUZGMEMCIAQktY7/qqaU4VWepck7v9SokGQiQFXN8HC2dxRpRC0HAh9cjrD+plFtYLisszrWTt5g6Hhb+zqpS5m9+GFR25qaAQEEIgAgdx/RitRZZm3Unz1WTj28QvTIR3TjYK2haBao7UiNVoEBBUdSIQOxNBzLp2g7avTxI4zW6X5xZ9Vp+sR/HkjUdUGEQ1W9RiED3lXR4drIBeP4pYwfv5uUwC89uq/hJ/78pJlfJvggg71SriEGA7E0HMunaDtq9PEjjNbpfnFn1Wn6xH8eSNR1QYRDVb0QtKa6ZwAAAIAAAACABAAAgCIGA95V0eHayAXj+KWMH7+blMAvPbqv4Sf+/KSZXyb4IIO9ELSmumcAAACAAAAAgAUAAIAAAA==";
PSBT.fromString(psbt);
}
@Test(expected = IllegalStateException.class)
public void invalidInputNonWitnessUtxoKeyType() {
String psbt = "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAIAALsCAAAAAarXOTEBi9JfhK5AC2iEi+CdtwbqwqwYKYur7nGrZW+LAAAAAEhHMEQCIFj2/HxqM+GzFUjUgcgmwBW9MBNarULNZ3kNq2bSrSQ7AiBKHO0mBMZzW2OT5bQWkd14sA8MWUL7n3UYVvqpOBV9ugH+////AoDw+gIAAAAAF6kUD7lGNCFpa4LIM68kHHjBfdveSTSH0PIKJwEAAAAXqRQpynT4oI+BmZQoGFyXtdhS5AY/YYdlAAAAAQfaAEcwRAIgdAGK1BgAl7hzMjwAFXILNoTMgSOJEEjn282bVa1nnJkCIHPTabdA4+tT3O+jOCPIBwUUylWn3ZVE8VfBZ5EyYRGMAUgwRQIhAPYQOLMI3B2oZaNIUnRvAVdyk0IIxtJEVDk82ZvfIhd3AiAFbmdaZ1ptCgK4WxTl4pB02KJam1dgvqKBb2YZEKAG6gFHUiEClYO/Oa4KYJdHrRma3dY0+mEIVZ1sXNObTCGD8auW4H8hAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXUq4AAQEgAMLrCwAAAAAXqRS39fr0Dj1ApaRZsds1NfK3L6kh6IcBByMiACCMI1MXN0O1ld+0oHtyuo5C43l9p06H/n2ddJfjsgKJAwEI2gQARzBEAiBi63pVYQenxz9FrEq1od3fb3B1+xJ1lpp/OD7/94S8sgIgDAXbt0cNvy8IVX3TVscyXB7TCRPpls04QJRdsSIo2l8BRzBEAiBl9FulmYtZon/+GnvtAWrx8fkNVLOqj3RQql9WolEDvQIgf3JHA60e25ZoCyhLVtT/y4j3+3Weq74IqjDym4UTg9IBR1IhAwidwQx6xttU+RMpr2FzM9s4jOrQwjH3IzedG5kDCwLcIQI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8Oc1KuACICA6mkw39ZltOqJdusa1cK8GUDlEkpQkYLNUdT7Z7spYdxENkMak8AAACAAAAAgAQAAIAAIgICf2OZdX0u/1WhNq0CxoSxg4tlVuXxtrNCgqlLa1AFEJYQ2QxqTwAAAIAAAACABQAAgAA=";
PSBT.fromString(psbt);
}
@Test(expected = IllegalStateException.class)
public void invalidInputFinalScriptSigKeyType() {
String psbt = "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAACBwDaAEcwRAIgdAGK1BgAl7hzMjwAFXILNoTMgSOJEEjn282bVa1nnJkCIHPTabdA4+tT3O+jOCPIBwUUylWn3ZVE8VfBZ5EyYRGMAUgwRQIhAPYQOLMI3B2oZaNIUnRvAVdyk0IIxtJEVDk82ZvfIhd3AiAFbmdaZ1ptCgK4WxTl4pB02KJam1dgvqKBb2YZEKAG6gFHUiEClYO/Oa4KYJdHrRma3dY0+mEIVZ1sXNObTCGD8auW4H8hAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXUq4AAQEgAMLrCwAAAAAXqRS39fr0Dj1ApaRZsds1NfK3L6kh6IcBByMiACCMI1MXN0O1ld+0oHtyuo5C43l9p06H/n2ddJfjsgKJAwEI2gQARzBEAiBi63pVYQenxz9FrEq1od3fb3B1+xJ1lpp/OD7/94S8sgIgDAXbt0cNvy8IVX3TVscyXB7TCRPpls04QJRdsSIo2l8BRzBEAiBl9FulmYtZon/+GnvtAWrx8fkNVLOqj3RQql9WolEDvQIgf3JHA60e25ZoCyhLVtT/y4j3+3Weq74IqjDym4UTg9IBR1IhAwidwQx6xttU+RMpr2FzM9s4jOrQwjH3IzedG5kDCwLcIQI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8Oc1KuACICA6mkw39ZltOqJdusa1cK8GUDlEkpQkYLNUdT7Z7spYdxENkMak8AAACAAAAAgAQAAIAAIgICf2OZdX0u/1WhNq0CxoSxg4tlVuXxtrNCgqlLa1AFEJYQ2QxqTwAAAIAAAACABQAAgAA=";
PSBT.fromString(psbt);
}
@Test(expected = IllegalStateException.class)
public void invalidInputFinalScriptWitnessKeyType() {
String psbt = "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAABB9oARzBEAiB0AYrUGACXuHMyPAAVcgs2hMyBI4kQSOfbzZtVrWecmQIgc9Npt0Dj61Pc76M4I8gHBRTKVafdlUTxV8FnkTJhEYwBSDBFAiEA9hA4swjcHahlo0hSdG8BV3KTQgjG0kRUOTzZm98iF3cCIAVuZ1pnWm0KArhbFOXikHTYolqbV2C+ooFvZhkQoAbqAUdSIQKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfyEC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtdSrgABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohwEHIyIAIIwjUxc3Q7WV37Sge3K6jkLjeX2nTof+fZ10l+OyAokDAggA2gQARzBEAiBi63pVYQenxz9FrEq1od3fb3B1+xJ1lpp/OD7/94S8sgIgDAXbt0cNvy8IVX3TVscyXB7TCRPpls04QJRdsSIo2l8BRzBEAiBl9FulmYtZon/+GnvtAWrx8fkNVLOqj3RQql9WolEDvQIgf3JHA60e25ZoCyhLVtT/y4j3+3Weq74IqjDym4UTg9IBR1IhAwidwQx6xttU+RMpr2FzM9s4jOrQwjH3IzedG5kDCwLcIQI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8Oc1KuACICA6mkw39ZltOqJdusa1cK8GUDlEkpQkYLNUdT7Z7spYdxENkMak8AAACAAAAAgAQAAIAAIgICf2OZdX0u/1WhNq0CxoSxg4tlVuXxtrNCgqlLa1AFEJYQ2QxqTwAAAIAAAACABQAAgAA=";
PSBT.fromString(psbt);
}
@Test(expected = IllegalStateException.class)
public void invalidOutputBip32PubKey() {
String psbt = "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAABB9oARzBEAiB0AYrUGACXuHMyPAAVcgs2hMyBI4kQSOfbzZtVrWecmQIgc9Npt0Dj61Pc76M4I8gHBRTKVafdlUTxV8FnkTJhEYwBSDBFAiEA9hA4swjcHahlo0hSdG8BV3KTQgjG0kRUOTzZm98iF3cCIAVuZ1pnWm0KArhbFOXikHTYolqbV2C+ooFvZhkQoAbqAUdSIQKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfyEC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtdSrgABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohwEHIyIAIIwjUxc3Q7WV37Sge3K6jkLjeX2nTof+fZ10l+OyAokDAQjaBABHMEQCIGLrelVhB6fHP0WsSrWh3d9vcHX7EnWWmn84Pv/3hLyyAiAMBdu3Rw2/LwhVfdNWxzJcHtMJE+mWzThAlF2xIijaXwFHMEQCIGX0W6WZi1mif/4ae+0BavHx+Q1Us6qPdFCqX1aiUQO9AiB/ckcDrR7blmgLKEtW1P/LiPf7dZ6rvgiqMPKbhROD0gFHUiEDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwhAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zUq4AIQIDqaTDf1mW06ol26xrVwrwZQOUSSlCRgs1R1PtnuylhxDZDGpPAAAAgAAAAIAEAACAACICAn9jmXV9Lv9VoTatAsaEsYOLZVbl8bazQoKpS2tQBRCWENkMak8AAACAAAAAgAUAAIAA";
PSBT.fromString(psbt);
}
@Test(expected = IllegalStateException.class)
public void invalidInputSigHashKeyType() {
String psbt = "cHNidP8BAHMCAAAAATAa6YblFqHsisW0vGVz0y+DtGXiOtdhZ9aLOOcwtNvbAAAAAAD/////AnR7AQAAAAAAF6kUA6oXrogrXQ1Usl1jEE5P/s57nqKHYEOZOwAAAAAXqRS5IbG6b3IuS/qDtlV6MTmYakLsg4cAAAAAAAEBHwDKmjsAAAAAFgAU0tlLZK4IWH7vyO6xh8YB6Tn5A3wCAwABAAAAAAEAFgAUYunpgv/zTdgjlhAxawkM0qO3R8sAAQAiACCHa62DLx0WgBXtQSMqnqZaGBXZ7xPA74dZ9ktbKyeKZQEBJVEhA7fOI6AcW0vwCmQlN836uzFbZoMyhnR471EwnSvVf4qHUa4A";
PSBT.fromString(psbt);
}
@Test(expected = IllegalStateException.class)
public void invalidOutputRedeemScriptKeyType() {
String psbt = "cHNidP8BAHMCAAAAATAa6YblFqHsisW0vGVz0y+DtGXiOtdhZ9aLOOcwtNvbAAAAAAD/////AnR7AQAAAAAAF6kUA6oXrogrXQ1Usl1jEE5P/s57nqKHYEOZOwAAAAAXqRS5IbG6b3IuS/qDtlV6MTmYakLsg4cAAAAAAAEBHwDKmjsAAAAAFgAU0tlLZK4IWH7vyO6xh8YB6Tn5A3wAAgAAFgAUYunpgv/zTdgjlhAxawkM0qO3R8sAAQAiACCHa62DLx0WgBXtQSMqnqZaGBXZ7xPA74dZ9ktbKyeKZQEBJVEhA7fOI6AcW0vwCmQlN836uzFbZoMyhnR471EwnSvVf4qHUa4A";
PSBT.fromString(psbt);
}
@Test(expected = IllegalStateException.class)
public void invalidOutputWitnessScriptKeyType() {
String psbt = "cHNidP8BAHMCAAAAATAa6YblFqHsisW0vGVz0y+DtGXiOtdhZ9aLOOcwtNvbAAAAAAD/////AnR7AQAAAAAAF6kUA6oXrogrXQ1Usl1jEE5P/s57nqKHYEOZOwAAAAAXqRS5IbG6b3IuS/qDtlV6MTmYakLsg4cAAAAAAAEBHwDKmjsAAAAAFgAU0tlLZK4IWH7vyO6xh8YB6Tn5A3wAAQAWABRi6emC//NN2COWEDFrCQzSo7dHywABACIAIIdrrYMvHRaAFe1BIyqeploYFdnvE8Dvh1n2S1srJ4plIQEAJVEhA7fOI6AcW0vwCmQlN836uzFbZoMyhnR471EwnQbVf4qHUa4A";
PSBT.fromString(psbt);
}
@Test
public void validP2pkhOneInputEmptyOutputs() {
String psbt = "cHNidP8BAHUCAAAAASaBcTce3/KF6Tet7qSze3gADAVmy7OtZGQXE8pCFxv2AAAAAAD+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAAAAA";
PSBT psbt1 = PSBT.fromString(psbt);
Assert.assertEquals(1, psbt1.getPsbtInputs().size());
Assert.assertEquals(2, psbt1.getPsbtOutputs().size());
Assert.assertEquals("af2cac1e0e33d896d9d0751d66fcb2fa54b737c7a13199281fb57e4f497bb652", psbt1.getTransaction().getTxId().toString());
Assert.assertEquals("f61b1742ca13176464adb3cb66050c00787bb3a4eead37e985f2df1e37718126", psbt1.getTransaction().getInputs().get(0).getOutpoint().getHash().toString());
Assert.assertEquals(0, psbt1.getTransaction().getInputs().get(0).getOutpoint().getIndex());
Assert.assertEquals(0, psbt1.getTransaction().getInputs().get(0).getScriptBytes().length);
Assert.assertEquals(4294967294L, psbt1.getTransaction().getInputs().get(0).getSequenceNumber());
Assert.assertEquals(99999699L, psbt1.getTransaction().getOutputs().get(0).getValue());
Assert.assertEquals("1L2tGENeoh4mSoiUZrSbs1J3jazSdJH9QS", psbt1.getTransaction().getOutputs().get(0).getScript().getToAddresses()[0].toString());
Assert.assertEquals("76a914d0c59903c5bac2868760e90fd521a4665aa7652088ac", psbt1.getTransaction().getOutputs().get(0).getScript().getProgramAsHex());
Assert.assertEquals("OP_DUP OP_HASH160 d0c59903c5bac2868760e90fd521a4665aa76520 OP_EQUALVERIFY OP_CHECKSIG", psbt1.getTransaction().getOutputs().get(0).getScript().toString());
Assert.assertEquals(100000000L, psbt1.getTransaction().getOutputs().get(1).getValue());
Assert.assertEquals("36YhUacEtcnkfhSbxwm11wDCexLGBLgJF6", psbt1.getTransaction().getOutputs().get(1).getScript().getToAddresses()[0].toString());
Assert.assertEquals("a9143545e6e33b832c47050f24d3eeb93c9c03948bc787", psbt1.getTransaction().getOutputs().get(1).getScript().getProgramAsHex());
Assert.assertEquals("OP_HASH160 3545e6e33b832c47050f24d3eeb93c9c03948bc7 OP_EQUAL", psbt1.getTransaction().getOutputs().get(1).getScript().toString());
Assert.assertEquals("f61b1742ca13176464adb3cb66050c00787bb3a4eead37e985f2df1e37718126", psbt1.getPsbtInputs().get(0).getNonWitnessUtxo().getTxId().toString());
Assert.assertEquals(421, psbt1.getPsbtInputs().get(0).getNonWitnessUtxo().getMessageSize());
Assert.assertEquals("e567952fb6cc33857f392efa3a46c995a28f69cca4bb1b37e0204dab1ec7a389", psbt1.getPsbtInputs().get(0).getNonWitnessUtxo().getInputs().get(0).getOutpoint().getHash().toString());
Assert.assertEquals("160014be18d152a9b012039daf3da7de4f53349eecb985", psbt1.getPsbtInputs().get(0).getNonWitnessUtxo().getInputs().get(0).getScript().getProgramAsHex());
Assert.assertEquals("304402202712be22e0270f394f568311dc7ca9a68970b8025fdd3b240229f07f8a5f3a240220018b38d7dcd314e734c9276bd6fb40f673325bc4baa144c800d2f2f02db2765c01 03d2e15674941bad4a996372cb87e1856d3652606d98562fe39c5e9e7e413f2105", psbt1.getPsbtInputs().get(0).getNonWitnessUtxo().getInputs().get(0).getWitness().toString());
Assert.assertEquals("b490486aec3ae671012dddb2bb08466bef37720a533a894814ff1da743aaf886", psbt1.getPsbtInputs().get(0).getNonWitnessUtxo().getInputs().get(1).getOutpoint().getHash().toString());
Assert.assertEquals(200000000L, psbt1.getPsbtInputs().get(0).getNonWitnessUtxo().getOutputs().get(0).getValue());
Assert.assertEquals("76a91485cff1097fd9e008bb34af709c62197b38978a4888ac", psbt1.getPsbtInputs().get(0).getNonWitnessUtxo().getOutputs().get(0).getScript().getProgramAsHex());
Assert.assertEquals(190303501938L, psbt1.getPsbtInputs().get(0).getNonWitnessUtxo().getOutputs().get(1).getValue());
Assert.assertEquals("a914339725ba21efd62ac753a9bcd067d6c7a6a39d0587", psbt1.getPsbtInputs().get(0).getNonWitnessUtxo().getOutputs().get(1).getScript().getProgramAsHex());
Assert.assertEquals(0, psbt1.getPsbtOutputs().get(0).getDerivedPublicKeys().size());
}
}