Skip to content

Commit

Permalink
Wallet: Implement witness fee discount.
Browse files Browse the repository at this point in the history
Fee is now specified in virtual (kilo)bytes. For non-segwit transactions a virtual byte is the same as a byte so the change is backward compatible.

cherry pick bitcoinj@c168e67
  • Loading branch information
Andreas Schildbach authored and oscarguindzberg committed Oct 26, 2020
1 parent be766f0 commit 3f1b3fd
Show file tree
Hide file tree
Showing 7 changed files with 93 additions and 67 deletions.
4 changes: 2 additions & 2 deletions core/src/main/java/org/bitcoinj/core/Context.java
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public Context(NetworkParameters params) {
*
* @param params The network parameters that will be associated with this context.
* @param eventHorizon Number of blocks after which the library will delete data and be unable to always process reorgs. See {@link #getEventHorizon()}.
* @param feePerKb The default fee per 1000 bytes of transaction data to pay when completing transactions. For details, see {@link SendRequest#feePerKb}.
* @param feePerKb The default fee per 1000 virtual bytes of transaction data to pay when completing transactions. For details, see {@link SendRequest#feePerKb}.
* @param ensureMinRequiredFee Whether to ensure the minimum required fee by default when completing transactions. For details, see {@link SendRequest#ensureMinRequiredFee}.
*/
public Context(NetworkParameters params, int eventHorizon, Coin feePerKb, boolean ensureMinRequiredFee) {
Expand Down Expand Up @@ -181,7 +181,7 @@ public int getEventHorizon() {
}

/**
* The default fee per 1000 bytes of transaction data to pay when completing transactions. For details, see {@link SendRequest#feePerKb}.
* The default fee per 1000 virtual bytes of transaction data to pay when completing transactions. For details, see {@link SendRequest#feePerKb}.
*/
public Coin getFeePerKb() {
return feePerKb;
Expand Down
3 changes: 3 additions & 0 deletions core/src/main/java/org/bitcoinj/core/Transaction.java
Original file line number Diff line number Diff line change
Expand Up @@ -917,6 +917,9 @@ public String toString(@Nullable AbstractBlockChain chain, @Nullable CharSequenc
final Coin fee = getFee();
if (fee != null) {
s.append(indent).append(" fee ");
s.append(fee.multiply(1000).divide(weight).toFriendlyString()).append("/wu, ");
if (size != vsize)
s.append(fee.multiply(1000).divide(vsize).toFriendlyString()).append("/vkB, ");
s.append(fee.multiply(1000).divide(size).toFriendlyString()).append("/kB ");
s.append(fee.toFriendlyString()).append('\n');
}
Expand Down
8 changes: 6 additions & 2 deletions core/src/main/java/org/bitcoinj/wallet/SendRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -101,13 +101,17 @@ public class SendRequest {
* a way for people to prioritize their transactions over others and is used as a way to make denial of service
* attacks expensive.</p>
*
* <p>This is a dynamic fee (in satoshis) which will be added to the transaction for each kilobyte in size
* <p>This is a dynamic fee (in satoshis) which will be added to the transaction for each virtual kilobyte in size
* including the first. This is useful as as miners usually sort pending transactions by their fee per unit size
* when choosing which transactions to add to a block. Note that, to keep this equivalent to Bitcoin Core
* definition, a kilobyte is defined as 1000 bytes, not 1024.</p>
* definition, a virtual kilobyte is defined as 1000 virtual bytes, not 1024.</p>
*/
public Coin feePerKb = Context.get().getFeePerKb();

public void setFeePerVkb(Coin feePerVkb) {
this.feePerKb = feePerVkb;
}

/**
* <p>Requires that there be enough fee for a default Bitcoin Core to at least relay the transaction.
* (ie ensure the transaction will not be outright rejected by the network). Defaults to true, you should
Expand Down
31 changes: 16 additions & 15 deletions core/src/main/java/org/bitcoinj/wallet/Wallet.java
Original file line number Diff line number Diff line change
Expand Up @@ -4129,7 +4129,7 @@ public void completeTx(SendRequest req) throws InsufficientMoneyException {
value = value.add(output.getValue());
}

log.info("Completing send tx with {} outputs totalling {} and a fee of {}/kB", req.tx.getOutputs().size(),
log.info("Completing send tx with {} outputs totalling {} and a fee of {}/vkB", req.tx.getOutputs().size(),
value.toFriendlyString(), req.feePerKb.toFriendlyString());

// If any inputs have already been added, we don't need to get their value from wallet
Expand Down Expand Up @@ -4303,8 +4303,8 @@ public void signTransaction(SendRequest req) {
/** Reduce the value of the first output of a transaction to pay the given feePerKb as appropriate for its size. */
private boolean adjustOutputDownwardsForFee(Transaction tx, CoinSelection coinSelection, Coin baseFee, Coin feePerKb,
boolean ensureMinRequiredFee) {
final int size = tx.unsafeBitcoinSerialize().length + estimateBytesForSigning(coinSelection);
Coin fee = baseFee.add(feePerKb.multiply(size).divide(1000));
final int vsize = tx.getVsize() + estimateVirtualBytesForSigning(coinSelection);
Coin fee = baseFee.add(feePerKb.multiply(vsize).divide(1000));
if (ensureMinRequiredFee && fee.compareTo(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE) < 0)
fee = Transaction.REFERENCE_DEFAULT_MIN_TX_FEE;
TransactionOutput output = tx.getOutput(0);
Expand Down Expand Up @@ -5122,13 +5122,12 @@ private FeeCalculation calculateFee(SendRequest req, Coin value, List<Transactio
checkState(!input.hasWitness());
}

int size = tx.unsafeBitcoinSerialize().length;
size += estimateBytesForSigning(selection);
final int vsize = tx.getVsize() + estimateVirtualBytesForSigning(selection);

Coin baseFeeNeeded = req.fee == null ? Coin.ZERO : req.fee;
Coin feePerKbNeeded = req.feePerKb;
Coin feeNeeded = baseFeeNeeded.add(feePerKbNeeded.multiply(size).divide(1000));
Coin minFeeNeeded = Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.multiply(size).divide(1000);
Coin feeNeeded = baseFeeNeeded.add(feePerKbNeeded.multiply(vsize).divide(1000));
Coin minFeeNeeded = Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.multiply(vsize).divide(1000);
if (needAtLeastReferenceFee && feeNeeded.isLessThan(minFeeNeeded)) {
feeNeeded = minFeeNeeded;
}
Expand All @@ -5150,33 +5149,35 @@ private void addSuppliedInputs(Transaction tx, List<TransactionInput> originalIn
tx.addInput(new TransactionInput(params, tx, input.bitcoinSerialize()));
}

private int estimateBytesForSigning(CoinSelection selection) {
int size = 0;
private int estimateVirtualBytesForSigning(CoinSelection selection) {
int vsize = 0;
for (TransactionOutput output : selection.gathered) {
try {
Script script = output.getScriptPubKey();
ECKey key = null;
Script redeemScript = null;
if (ScriptPattern.isP2PKH(script)) {
key = findKeyFromPubKeyHash(ScriptPattern.extractHashFromP2PKH(script),
Script.ScriptType.P2PKH);
key = findKeyFromPubKeyHash(ScriptPattern.extractHashFromP2PKH(script), Script.ScriptType.P2PKH);
checkNotNull(key, "Coin selection includes unspendable outputs");
vsize += script.getNumberOfBytesRequiredToSpend(key, redeemScript);
} else if (ScriptPattern.isP2WPKH(script)) {
key = findKeyFromPubKeyHash(ScriptPattern.extractHashFromP2WH(script),
Script.ScriptType.P2WPKH);
key = findKeyFromPubKeyHash(ScriptPattern.extractHashFromP2WH(script), Script.ScriptType.P2WPKH);
checkNotNull(key, "Coin selection includes unspendable outputs");
vsize += (script.getNumberOfBytesRequiredToSpend(key, redeemScript) + 3) / 4; // round up
} else if (ScriptPattern.isP2SH(script)) {
redeemScript = findRedeemDataFromScriptHash(ScriptPattern.extractHashFromP2SH(script)).redeemScript;
checkNotNull(redeemScript, "Coin selection includes unspendable outputs");
vsize += script.getNumberOfBytesRequiredToSpend(key, redeemScript);
} else {
vsize += script.getNumberOfBytesRequiredToSpend(key, redeemScript);
}
size += script.getNumberOfBytesRequiredToSpend(key, redeemScript);
} catch (ScriptException e) {
// If this happens it means an output script in a wallet tx could not be understood. That should never
// happen, if it does it means the wallet has got into an inconsistent state.
throw new IllegalStateException(e);
}
}
return size;
return vsize;
}

//endregion
Expand Down
18 changes: 18 additions & 0 deletions core/src/test/java/org/bitcoinj/wallet/WalletTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ public class WalletTest extends TestWithWallet {
private static final CharSequence WRONG_PASSWORD = "nothing noone nobody nowhere";

private final Address OTHER_ADDRESS = LegacyAddress.fromKey(UNITTEST, new ECKey());
private final Address OTHER_SEGWIT_ADDRESS = SegwitAddress.fromKey(UNITTEST, new ECKey());

@Before
@Override
Expand Down Expand Up @@ -2724,6 +2725,23 @@ public void transactionGetFeeTest() throws Exception {
assertEquals(Coin.valueOf(22700), request.tx.getFee());
}

@Test
public void witnessTransactionGetFeeTest() throws Exception {
Wallet mySegwitWallet = Wallet.createDeterministic(UNITTEST, Script.ScriptType.P2WPKH);
Address mySegwitAddress = mySegwitWallet.freshReceiveAddress(Script.ScriptType.P2WPKH);

// Prepare wallet to spend
StoredBlock block = new StoredBlock(makeSolvedTestBlock(blockStore, OTHER_SEGWIT_ADDRESS), BigInteger.ONE, 1);
Transaction tx = createFakeTx(UNITTEST, COIN, mySegwitAddress);
mySegwitWallet.receiveFromBlock(tx, block, AbstractBlockChain.NewBlockType.BEST_CHAIN, 0);

// Create a transaction
SendRequest request = SendRequest.to(OTHER_SEGWIT_ADDRESS, CENT);
request.feePerKb = Transaction.DEFAULT_TX_FEE;
mySegwitWallet.completeTx(request);
assertEquals(Coin.valueOf(14000), request.tx.getFee());
}

@Test
public void lowerThanDefaultFee() throws InsufficientMoneyException {
int feeFactor = 50;
Expand Down
Loading

0 comments on commit 3f1b3fd

Please sign in to comment.