-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #7283 from alvasw/core_Implement_transaction_offse…
…t_signing core: Implement transaction offset signing
- Loading branch information
Showing
2 changed files
with
185 additions
and
0 deletions.
There are no files selected for viewing
50 changes: 50 additions & 0 deletions
50
core/src/main/java/bisq/core/btc/wallet/BisqTransactionSigner.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
package bisq.core.btc.wallet; | ||
|
||
import org.bitcoinj.core.Transaction; | ||
import org.bitcoinj.core.TransactionInput; | ||
import org.bitcoinj.core.TransactionOutput; | ||
import org.bitcoinj.script.Script; | ||
import org.bitcoinj.signers.MissingSigResolutionSigner; | ||
import org.bitcoinj.signers.TransactionSigner; | ||
import org.bitcoinj.wallet.DecryptingKeyBag; | ||
import org.bitcoinj.wallet.KeyBag; | ||
import org.bitcoinj.wallet.RedeemData; | ||
import org.bitcoinj.wallet.Wallet; | ||
|
||
import java.util.Objects; | ||
|
||
import lombok.extern.slf4j.Slf4j; | ||
|
||
@Slf4j | ||
public class BisqTransactionSigner { | ||
public static void sign(Wallet wallet, Transaction tx, int inputOffset) { | ||
var localOffsetTransactionSigner = new LocalOffsetTransactionSigner(inputOffset); | ||
KeyBag maybeDecryptingKeyBag = new DecryptingKeyBag(wallet, null); | ||
|
||
int numInputs = tx.getInputs().size(); | ||
for (int i = 0; i < numInputs; i++) { | ||
if (i < inputOffset) { | ||
continue; | ||
} | ||
|
||
TransactionInput txIn = tx.getInput(i); | ||
TransactionOutput connectedOutput = txIn.getConnectedOutput(); | ||
Objects.requireNonNull(connectedOutput, "Connected output of transaction input #" + i + " is null"); | ||
|
||
Script scriptPubKey = connectedOutput.getScriptPubKey(); | ||
|
||
RedeemData redeemData = txIn.getConnectedRedeemData(maybeDecryptingKeyBag); | ||
Objects.requireNonNull(redeemData, "Transaction exists in wallet that we cannot redeem: " + txIn.getOutpoint().getHash()); | ||
txIn.setScriptSig(scriptPubKey.createEmptyInputScript(redeemData.keys.get(0), redeemData.redeemScript)); | ||
txIn.setWitness(scriptPubKey.createEmptyWitness(redeemData.keys.get(0))); | ||
} | ||
|
||
TransactionSigner.ProposedTransaction proposal = new TransactionSigner.ProposedTransaction(tx); | ||
if (!localOffsetTransactionSigner.signInputs(proposal, maybeDecryptingKeyBag)) { | ||
log.info("{} returned false for the tx", localOffsetTransactionSigner.getClass().getName()); | ||
} | ||
|
||
// resolve missing sigs if any | ||
new MissingSigResolutionSigner(Wallet.MissingSigsMode.THROW).signInputs(proposal, maybeDecryptingKeyBag); | ||
} | ||
} |
135 changes: 135 additions & 0 deletions
135
core/src/main/java/bisq/core/btc/wallet/LocalOffsetTransactionSigner.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
package bisq.core.btc.wallet; | ||
|
||
import org.bitcoinj.core.Coin; | ||
import org.bitcoinj.core.ECKey; | ||
import org.bitcoinj.core.Transaction; | ||
import org.bitcoinj.core.TransactionInput; | ||
import org.bitcoinj.core.TransactionOutput; | ||
import org.bitcoinj.core.TransactionWitness; | ||
import org.bitcoinj.crypto.DeterministicKey; | ||
import org.bitcoinj.crypto.TransactionSignature; | ||
import org.bitcoinj.script.Script; | ||
import org.bitcoinj.script.ScriptBuilder; | ||
import org.bitcoinj.script.ScriptException; | ||
import org.bitcoinj.script.ScriptPattern; | ||
import org.bitcoinj.signers.TransactionSigner; | ||
import org.bitcoinj.wallet.KeyBag; | ||
import org.bitcoinj.wallet.RedeemData; | ||
|
||
import java.util.Arrays; | ||
import java.util.EnumSet; | ||
|
||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
public class LocalOffsetTransactionSigner implements TransactionSigner { | ||
private static final Logger log = LoggerFactory.getLogger(LocalOffsetTransactionSigner.class); | ||
|
||
/** | ||
* Verify flags that are safe to use when testing if an input is already | ||
* signed. | ||
*/ | ||
private static final EnumSet<Script.VerifyFlag> MINIMUM_VERIFY_FLAGS = EnumSet.of(Script.VerifyFlag.P2SH, | ||
Script.VerifyFlag.NULLDUMMY); | ||
|
||
private final int startOffset; | ||
|
||
public LocalOffsetTransactionSigner(int startOffset) { | ||
this.startOffset = startOffset; | ||
} | ||
|
||
@Override | ||
public boolean isReady() { | ||
return true; | ||
} | ||
|
||
@Override | ||
public boolean signInputs(ProposedTransaction propTx, KeyBag keyBag) { | ||
Transaction tx = propTx.partialTx; | ||
int numInputs = tx.getInputs().size(); | ||
for (int i = 0; i < numInputs; i++) { | ||
if (i < startOffset) { | ||
continue; | ||
} | ||
|
||
TransactionInput txIn = tx.getInput(i); | ||
final TransactionOutput connectedOutput = txIn.getConnectedOutput(); | ||
if (connectedOutput == null) { | ||
log.warn("Missing connected output, assuming input {} is already signed.", i); | ||
continue; | ||
} | ||
Script scriptPubKey = connectedOutput.getScriptPubKey(); | ||
|
||
try { | ||
// We assume if its already signed, its hopefully got a SIGHASH type that will not invalidate when | ||
// we sign missing pieces (to check this would require either assuming any signatures are signing | ||
// standard output types or a way to get processed signatures out of script execution) | ||
txIn.getScriptSig().correctlySpends(tx, i, txIn.getWitness(), connectedOutput.getValue(), | ||
connectedOutput.getScriptPubKey(), MINIMUM_VERIFY_FLAGS); | ||
log.warn("Input {} already correctly spends output, assuming SIGHASH type used will be safe and skipping signing.", i); | ||
continue; | ||
} catch (ScriptException e) { | ||
// Expected. | ||
} | ||
|
||
RedeemData redeemData = txIn.getConnectedRedeemData(keyBag); | ||
|
||
// For P2SH inputs we need to share derivation path of the signing key with other signers, so that they | ||
// use correct key to calculate their signatures. | ||
// Married keys all have the same derivation path, so we can safely just take first one here. | ||
ECKey pubKey = redeemData.keys.get(0); | ||
if (pubKey instanceof DeterministicKey) | ||
propTx.keyPaths.put(scriptPubKey, (((DeterministicKey) pubKey).getPath())); | ||
|
||
ECKey key; | ||
// locate private key in redeem data. For P2PKH and P2PK inputs RedeemData will always contain | ||
// only one key (with private bytes). For P2SH inputs RedeemData will contain multiple keys, one of which MAY | ||
// have private bytes | ||
if ((key = redeemData.getFullKey()) == null) { | ||
log.warn("No local key found for input {}", i); | ||
continue; | ||
} | ||
|
||
Script inputScript = txIn.getScriptSig(); | ||
// script here would be either a standard CHECKSIG program for P2PKH or P2PK inputs or | ||
// a CHECKMULTISIG program for P2SH inputs | ||
byte[] script = redeemData.redeemScript.getProgram(); | ||
try { | ||
if (ScriptPattern.isP2PK(scriptPubKey) || ScriptPattern.isP2PKH(scriptPubKey) | ||
|| ScriptPattern.isP2SH(scriptPubKey)) { | ||
TransactionSignature signature = tx.calculateSignature(i, key, script, Transaction.SigHash.ALL, | ||
false); | ||
|
||
// at this point we have incomplete inputScript with OP_0 in place of one or more signatures. We | ||
// already have calculated the signature using the local key and now need to insert it in the | ||
// correct place within inputScript. For P2PKH and P2PK script there is only one signature and it | ||
// always goes first in an inputScript (sigIndex = 0). In P2SH input scripts we need to figure out | ||
// our relative position relative to other signers. Since we don't have that information at this | ||
// point, and since we always run first, we have to depend on the other signers rearranging the | ||
// signatures as needed. Therefore, always place as first signature. | ||
int sigIndex = 0; | ||
inputScript = scriptPubKey.getScriptSigWithSignature(inputScript, signature.encodeToBitcoin(), | ||
sigIndex); | ||
txIn.setScriptSig(inputScript); | ||
txIn.setWitness(null); | ||
} else if (ScriptPattern.isP2WPKH(scriptPubKey)) { | ||
Script scriptCode = ScriptBuilder.createP2PKHOutputScript(key); | ||
Coin value = txIn.getValue(); | ||
TransactionSignature signature = tx.calculateWitnessSignature(i, key, scriptCode, value, | ||
Transaction.SigHash.ALL, false); | ||
txIn.setScriptSig(ScriptBuilder.createEmpty()); | ||
txIn.setWitness(TransactionWitness.redeemP2WPKH(signature, key)); | ||
} else { | ||
throw new IllegalStateException(Arrays.toString(script)); | ||
} | ||
} catch (ECKey.KeyIsEncryptedException e) { | ||
throw e; | ||
} catch (ECKey.MissingPrivateKeyException e) { | ||
log.warn("No private key in keypair for input {}", i); | ||
} | ||
|
||
} | ||
return true; | ||
} | ||
|
||
} |