Skip to content

Commit

Permalink
Even more temp changes
Browse files Browse the repository at this point in the history
Add RedirectionTransactionRecoveryService and get signature hiding logic
working end-to-end. TODO: UI tx recovery logic.
  • Loading branch information
stejbac committed Jan 19, 2025
1 parent 5e5baeb commit 790f05c
Show file tree
Hide file tree
Showing 12 changed files with 740 additions and 18 deletions.
4 changes: 4 additions & 0 deletions core/src/main/java/bisq/core/btc/BitcoinModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,16 @@
import bisq.common.app.AppModule;
import bisq.common.config.Config;

import org.bitcoinj.core.NetworkParameters;

import com.google.inject.Singleton;
import com.google.inject.TypeLiteral;

import java.io.File;

import java.util.Arrays;
import java.util.List;
import java.util.function.Supplier;

import static bisq.common.config.Config.PROVIDERS;
import static bisq.common.config.Config.WALLET_DIR;
Expand Down Expand Up @@ -72,6 +75,7 @@ protected void configure() {
} else {
bind(RegTestHost.class).toInstance(RegTestHost.REMOTE_HOST);
}
bind(new TypeLiteral<Supplier<NetworkParameters>>(){}).toInstance(Config::baseCurrencyNetworkParameters);

bind(File.class).annotatedWith(named(WALLET_DIR)).toInstance(config.walletDir);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,17 @@ public Transaction finalizeRedirectionTransaction(TransactionOutput warningTxOut
ECKey.ECDSASignature buyerECDSASignature = ECKey.ECDSASignature.decodeFromDER(buyerSignature);
ECKey.ECDSASignature sellerECDSASignature = ECKey.ECDSASignature.decodeFromDER(sellerSignature);

return finalizeRedirectionTransaction(warningTxOutput, redirectionTx, redeemScript, buyerECDSASignature, sellerECDSASignature);
}

// Also used by redirect tx recovery logic.
Transaction finalizeRedirectionTransaction(TransactionOutput warningTxOutput,
Transaction redirectionTx,
Script redeemScript,
ECKey.ECDSASignature buyerECDSASignature,
ECKey.ECDSASignature sellerECDSASignature)
throws TransactionVerificationException {

checkArgument(!buyerECDSASignature.r.testBit(255), "buyer signature should be low-R");
checkArgument(!sellerECDSASignature.r.testBit(255), "seller signature should be low-R");

Expand Down

Large diffs are not rendered by default.

36 changes: 30 additions & 6 deletions core/src/main/java/bisq/core/btc/wallet/TradeWalletService.java
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,7 @@ private PreparedDepositTxAndMakerInputs makerCreatesDepositTx(boolean makerIsBuy
* @param sellerInputs the connected outputs for all inputs of the seller
* @param buyerPubKey the public key of the buyer
* @param sellerPubKey the public key of the seller
* @param scalarToHide an optional 256-bit scalar to hide in the signature of the taker's first input
* @throws SigningException if (one of) the taker input(s) was of an unrecognized type for signing
* @throws TransactionVerificationException if a non-P2WH maker-as-buyer input wasn't signed, the maker's MultiSig
* script, contract hash or output amount doesn't match the taker's, or there was an unexpected problem with the
Expand All @@ -583,7 +584,8 @@ public Transaction takerSignsDepositTx(boolean takerIsSeller,
List<RawTransactionInput> buyerInputs,
List<RawTransactionInput> sellerInputs,
byte[] buyerPubKey,
byte[] sellerPubKey)
byte[] sellerPubKey,
@Nullable BigInteger scalarToHide)
throws SigningException, TransactionVerificationException, WalletException {
Transaction makersDepositTx = new Transaction(params, makersDepositTxSerialized);

Expand Down Expand Up @@ -651,7 +653,7 @@ public Transaction takerSignsDepositTx(boolean takerIsSeller,
int end = takerIsSeller ? depositTx.getInputs().size() : buyerInputs.size();
for (int i = start; i < end; i++) {
TransactionInput input = depositTx.getInput(i);
signInput(depositTx, input, i);
signInput(depositTx, input, i, i == start ? scalarToHide : null);
WalletService.checkScriptSig(depositTx, input, i);
}

Expand Down Expand Up @@ -700,6 +702,20 @@ public void sellerAddsBuyerWitnessesToDepositTx(Transaction myDepositTx,
}
}

public void makerResignsDepositTxWithHiddenScalar(Transaction depositTx,
RawTransactionInput rawTransactionInput,
BigInteger scalarToHide,
int inputIndex)
throws SigningException, TransactionVerificationException {
TransactionInput input = depositTx.getInput(inputIndex);
TransactionInput connectedInput = getTransactionInput(depositTx, input.getScriptBytes(), rawTransactionInput);
checkArgument(input.equals(connectedInput), "mismatched rawTransactionInput and depositTx input");
signInput(depositTx, connectedInput, inputIndex, checkNotNull(scalarToHide));
input.setScriptSig(connectedInput.getScriptSig());
input.setWitness(connectedInput.getWitness());
WalletService.checkScriptSig(depositTx, connectedInput, inputIndex);
}


///////////////////////////////////////////////////////////////////////////////////////////
// Delayed payout tx
Expand Down Expand Up @@ -1557,17 +1573,24 @@ private Transaction createPayoutTx(Transaction depositTx,
}

private void signInput(Transaction transaction, TransactionInput input, int inputIndex) throws SigningException {
signInput(transaction, input, inputIndex, null);
}

private void signInput(Transaction transaction,
TransactionInput input,
int inputIndex,
@Nullable BigInteger scalarToHide) throws SigningException {
checkNotNull(input.getConnectedOutput(), "input.getConnectedOutput() must not be null");
Script scriptPubKey = input.getConnectedOutput().getScriptPubKey();
ECKey sigKey = LowRSigningKey.from(input.getOutpoint().getConnectedKey(wallet));
LowRSigningKey sigKey = LowRSigningKey.from(input.getOutpoint().getConnectedKey(wallet));
checkNotNull(sigKey, "signInput: sigKey must not be null. input.getOutpoint()=%s", input.getOutpoint());
if (sigKey.isEncrypted()) {
checkNotNull(aesKey);
}

if (ScriptPattern.isP2PK(scriptPubKey) || ScriptPattern.isP2PKH(scriptPubKey)) {
Sha256Hash hash = transaction.hashForSignature(inputIndex, scriptPubKey, Transaction.SigHash.ALL, false);
ECKey.ECDSASignature signature = sigKey.sign(hash, aesKey);
ECKey.ECDSASignature signature = sigKey.sign(hash, scalarToHide, aesKey);
TransactionSignature txSig = new TransactionSignature(signature, Transaction.SigHash.ALL, false);
if (ScriptPattern.isP2PK(scriptPubKey)) {
input.setScriptSig(ScriptBuilder.createInputScript(txSig));
Expand All @@ -1578,8 +1601,9 @@ private void signInput(Transaction transaction, TransactionInput input, int inpu
// scriptCode is expected to have the format of a legacy P2PKH output script
Script scriptCode = ScriptBuilder.createP2PKHOutputScript(sigKey);
Coin value = input.getValue();
TransactionSignature txSig = transaction.calculateWitnessSignature(inputIndex, sigKey, aesKey, scriptCode, value,
Transaction.SigHash.ALL, false);
Sha256Hash hash = transaction.hashForWitnessSignature(inputIndex, scriptCode, value, Transaction.SigHash.ALL, false);
ECKey.ECDSASignature signature = sigKey.sign(hash, scalarToHide, aesKey);
TransactionSignature txSig = new TransactionSignature(signature, Transaction.SigHash.ALL, false);
input.setScriptSig(ScriptBuilder.createEmpty());
input.setWitness(TransactionWitness.redeemP2WPKH(txSig, sigKey));
} else {
Expand Down
8 changes: 7 additions & 1 deletion core/src/main/java/bisq/core/crypto/LowRSigningKey.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import org.bitcoinj.crypto.LazyECPoint;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Sets;

import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.crypto.params.KeyParameter;
Expand Down Expand Up @@ -98,7 +99,12 @@ public Set<BigInteger> recoveredHiddenScalarCandidates(Sha256Hash sigHash,
BigInteger e = DeterministicDSAKCalculator.toScalar(sigHash.getBytes(), n).mod(n);
BigInteger k = signature.s.modInverse(n).multiply(e.add(d.multiply(signature.r))).mod(n);
var kCalculator = new ScalarHidingDSAKCalculator();
return kCalculator.recoveredHiddenScalarCandidates(k, LowRSigningKey::liftsToLowR);
kCalculator.init(n, d, sigHash.getBytes());
// Because the input signature was likely canonicalized (that is, malleated to make it low-S),
// we don't know whether k or -k (mod n) was the original nonce, so must combine results of both.
return Sets.union(
kCalculator.recoveredHiddenScalarCandidates(k, LowRSigningKey::liftsToLowR),
kCalculator.recoveredHiddenScalarCandidates(n.subtract(k), LowRSigningKey::liftsToLowR));
}

@VisibleForTesting
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,10 +145,15 @@ public static Set<ReceiverFlag> flagsActivatedBy(Date date) {

// We use a snapshot blockHeight to avoid failed trades in case maker and taker have different block heights.
// The selection is deterministic based on DAO data.
// The block height is the last mod(10) height from the range of the last 10-20 blocks (139 -> 120; 140 -> 130, 141 -> 130).
// The block height is the last mod(10) height from the range of the last 5-14 blocks (134 -> 120; 135 -> 130, 136 -> 130).
// We do not have the latest dao state by that but can ensure maker and taker have the same block.
public int getBurningManSelectionHeight() {
return getSnapshotHeight(daoStateService.getGenesisBlockHeight(), currentChainHeight);
return getBurningManSelectionHeight(currentChainHeight);
}

// Used by redirect tx recovery logic, which involves guessing the creation chain height.
public int getBurningManSelectionHeight(int chainHeight) {
return getSnapshotHeight(daoStateService.getGenesisBlockHeight(), chainHeight);
}

public List<Tuple2<Long, String>> getReceivers(int burningManSelectionHeight,
Expand All @@ -170,7 +175,7 @@ public List<Tuple2<Long, String>> getReceivers(int burningManSelectionHeight,
long tradeTxFee,
long minTxWeight,
Set<ReceiverFlag> receiverFlags) {
checkArgument(burningManSelectionHeight >= MIN_SNAPSHOT_HEIGHT, "Selection height must be >= " + MIN_SNAPSHOT_HEIGHT);
checkArgument(burningManSelectionHeight >= MIN_SNAPSHOT_HEIGHT, "Selection height must be >= %s", MIN_SNAPSHOT_HEIGHT);

boolean isBugfix6699Activated = receiverFlags.contains(BUGFIX_6699);
boolean isProposal412Activated = receiverFlags.contains(PROPOSAL_412);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,11 @@
import bisq.common.taskrunner.TaskRunner;

import org.bitcoinj.core.Coin;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.Transaction;

import java.math.BigInteger;

import java.util.Arrays;
import java.util.List;
import java.util.Optional;
Expand Down Expand Up @@ -84,14 +87,21 @@ protected void run() {

List<RawTransactionInput> sellerInputs = checkNotNull(tradingPeer.getRawTransactionInputs());
byte[] sellerMultiSigPubKey = tradingPeer.getMultiSigPubKey();

// If v5 protocol, hide the s-component of the peer's signature for our redirect tx in our deposit
// tx signatures. (The r-component was already hidden in our signature for the peer's warning tx.)
BigInteger scalarToHide = processModel.getRedirectTxSellerSignature() != null ?
ECKey.ECDSASignature.decodeFromDER(processModel.getRedirectTxSellerSignature()).s : null;

Transaction depositTx = processModel.getTradeWalletService().takerSignsDepositTx(
false,
processModel.getPreparedDepositTx(),
msOutputAmount,
buyerInputs,
sellerInputs,
buyerMultiSigPubKey,
sellerMultiSigPubKey);
sellerMultiSigPubKey,
scalarToHide);
processModel.setDepositTx(depositTx);

processModel.getTradeManager().requestPersistence();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,11 @@
import bisq.common.taskrunner.TaskRunner;

import org.bitcoinj.core.Coin;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.Transaction;

import java.math.BigInteger;

import java.util.Arrays;
import java.util.List;
import java.util.Optional;
Expand Down Expand Up @@ -60,8 +63,7 @@ protected void run() {
checkArgument(optionalMultiSigAddressEntry.isPresent(), "addressEntryOptional must be present");
AddressEntry sellerMultiSigAddressEntry = optionalMultiSigAddressEntry.get();
byte[] sellerMultiSigPubKey = processModel.getMyMultiSigPubKey();
checkArgument(Arrays.equals(sellerMultiSigPubKey,
sellerMultiSigAddressEntry.getPubKey()),
checkArgument(Arrays.equals(sellerMultiSigPubKey, sellerMultiSigAddressEntry.getPubKey()),
"sellerMultiSigPubKey from AddressEntry must match the one from the trade data. trade id =" + id);

Coin sellerInput = Coin.valueOf(sellerInputs.stream().mapToLong(input -> input.value).sum());
Expand All @@ -77,14 +79,20 @@ protected void run() {

TradingPeer tradingPeer = processModel.getTradePeer();

// If v5 protocol, hide the s-component of the peer's signature for our redirect tx in our deposit
// tx signatures. (The r-component was already hidden in our signature for the peer's warning tx.)
BigInteger scalarToHide = processModel.getRedirectTxBuyerSignature() != null ?
ECKey.ECDSASignature.decodeFromDER(processModel.getRedirectTxBuyerSignature()).s : null;

Transaction depositTx = processModel.getTradeWalletService().takerSignsDepositTx(
true,
processModel.getPreparedDepositTx(),
msOutputAmount,
checkNotNull(tradingPeer.getRawTransactionInputs()),
sellerInputs,
tradingPeer.getMultiSigPubKey(),
sellerMultiSigPubKey);
sellerMultiSigPubKey,
scalarToHide);

// We set the deposit tx to trade once we have it published
processModel.setDepositTx(depositTx);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@

import bisq.common.config.Config;

import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.params.RegTestParams;

public class StagedPayoutTxParameters {
// 10 days
private static final long CLAIM_DELAY = 144 * 10;
Expand All @@ -36,18 +39,26 @@ public class StagedPayoutTxParameters {
private static final long MIN_TX_FEE_RATE = 10;

public static long getClaimDelay() {
return Config.baseCurrencyNetwork().isRegtest() ? 5 : CLAIM_DELAY;
return getClaimDelay(Config.baseCurrencyNetworkParameters());
}

public static long getClaimDelay(NetworkParameters params) {
return params.getId().equals(RegTestParams.ID_REGTEST) ? 5 : CLAIM_DELAY;
}

public static long getWarningTxMiningFee(long depositTxFeeRate) {
return (getFeePerVByte(depositTxFeeRate) * StagedPayoutTxParameters.WARNING_TX_EXPECTED_WEIGHT + 3) / 4;
return (getFeePerVByte(depositTxFeeRate) * WARNING_TX_EXPECTED_WEIGHT + 3) / 4;
}

public static long getClaimTxMiningFee(long txFeePerVByte) {
return (txFeePerVByte * StagedPayoutTxParameters.CLAIM_TX_EXPECTED_WEIGHT + 3) / 4;
return (txFeePerVByte * CLAIM_TX_EXPECTED_WEIGHT + 3) / 4;
}

private static long getFeePerVByte(long depositTxFeeRate) {
return Math.max(StagedPayoutTxParameters.MIN_TX_FEE_RATE, depositTxFeeRate);
return Math.max(MIN_TX_FEE_RATE, depositTxFeeRate);
}

public static long recoverDepositTxFeeRate(long warningTxMiningFee) {
return warningTxMiningFee * 4 / WARNING_TX_EXPECTED_WEIGHT;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@

import bisq.common.taskrunner.TaskRunner;

import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.Transaction;

import lombok.extern.slf4j.Slf4j;

import static bisq.core.util.Validator.checkTradeId;
Expand All @@ -48,6 +51,16 @@ protected void run() {
processModel.setRedirectTxSellerSignature(request.getBuyersRedirectTxSellerSignature());
processModel.getTradePeer().setRedirectTxSellerSignature(request.getSellersRedirectTxSellerSignature());

// Re-sign the prepared deposit tx, to hide the s-component of the peer's signature for our redirect tx in
// our first input witness. (The r-component was already hidden in our signature for the peer's warning tx.)
Transaction preparedDepositTx = processModel.getBtcWalletService().getTxFromSerializedTx(processModel.getPreparedDepositTx());
processModel.getTradeWalletService().makerResignsDepositTxWithHiddenScalar(
preparedDepositTx,
checkNotNull(processModel.getRawTransactionInputs()).get(0),
ECKey.ECDSASignature.decodeFromDER(request.getBuyersRedirectTxSellerSignature()).s,
0);
processModel.setPreparedDepositTx(preparedDepositTx.bitcoinSerialize());

// TODO: takerFeeTxTd:
// When we receive that message the taker has published the taker fee, so we apply it to the trade.
// The takerFeeTx was sent in the first message. It should be part of DelayedPayoutTxSignatureRequest
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@

import bisq.common.taskrunner.TaskRunner;

import org.bitcoinj.core.ECKey;

import lombok.extern.slf4j.Slf4j;

import static bisq.core.util.Validator.checkTradeId;
Expand Down Expand Up @@ -52,6 +54,17 @@ protected void run() {
processModel.getTradePeer().setRedirectTxBuyerSignature(message.getBuyersRedirectTxBuyerSignature());
processModel.setRedirectTxBuyerSignature(message.getSellersRedirectTxBuyerSignature());
processModel.setDepositTx(processModel.getBtcWalletService().getTxFromSerializedTx(processModel.getPreparedDepositTx()));

// Re-sign the deposit tx, to hide the s-component of the peer's signature for our redirect tx in our
// first input witness. (The r-component was already hidden in our signature for the peer's warning tx.)
int numBuyerInputs = (int) processModel.getDepositTx().getInputs().stream()
.filter(input -> !input.hasWitness())
.count();
processModel.getTradeWalletService().makerResignsDepositTxWithHiddenScalar(
processModel.getDepositTx(),
checkNotNull(processModel.getRawTransactionInputs()).get(0),
ECKey.ECDSASignature.decodeFromDER(message.getSellersRedirectTxBuyerSignature()).s,
numBuyerInputs);
}

// The deposit tx is finalized by adding all the buyer witnesses.
Expand Down
Loading

0 comments on commit 790f05c

Please sign in to comment.