Skip to content

Commit

Permalink
Merge pull request #7283 from alvasw/core_Implement_transaction_offse…
Browse files Browse the repository at this point in the history
…t_signing

core: Implement transaction offset signing
  • Loading branch information
alejandrogarcia83 authored Nov 8, 2024
2 parents 1f1c768 + 687f247 commit a59939d
Show file tree
Hide file tree
Showing 2 changed files with 185 additions and 0 deletions.
50 changes: 50 additions & 0 deletions core/src/main/java/bisq/core/btc/wallet/BisqTransactionSigner.java
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);
}
}
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;
}

}

0 comments on commit a59939d

Please sign in to comment.