Issue 586 fixed, 0BTC transaction with OP_RETURN will work.

This commit is contained in:
Wojciech Langiewicz
2014-10-25 21:40:24 +02:00
committed by Andreas Schildbach
parent 855fd2832f
commit dd37fe90c6
4 changed files with 139 additions and 16 deletions

View File

@@ -253,7 +253,7 @@ public class TransactionOutput extends ChildMessage implements Serializable {
/**
* Returns the minimum value for this output to be considered "not dust", i.e. the transaction will be relayable
* and mined by default miners. For normal pay to address outputs, this is 5460 satoshis, the same as
* and mined by default miners. For normal pay to address outputs, this is 546 satoshis, the same as
* {@link Transaction#MIN_NONDUST_OUTPUT}.
*/
public Coin getMinNonDustValue() {

View File

@@ -57,8 +57,6 @@ import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
@@ -3371,6 +3369,8 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
public static class CompletionException extends RuntimeException {}
public static class DustySendRequested extends CompletionException {}
public static class MultipleOpReturnRequested extends CompletionException {}
/**
* Thrown when we were trying to empty the wallet, and the total amount of money we were trying to empty after
* being reduced for the fee was smaller than the min payment. Note that the missing field will be null in this
@@ -3413,17 +3413,29 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
value = value.subtract(totalInput);
List<TransactionInput> originalInputs = new ArrayList<TransactionInput>(req.tx.getInputs());
int opReturnCount = 0;
// We need to know if we need to add an additional fee because one of our values are smaller than 0.01 BTC
boolean needAtLeastReferenceFee = false;
if (req.ensureMinRequiredFee && !req.emptyWallet) { // min fee checking is handled later for emptyWallet
for (TransactionOutput output : req.tx.getOutputs())
if (req.ensureMinRequiredFee && !req.emptyWallet) { // Min fee checking is handled later for emptyWallet.
for (TransactionOutput output : req.tx.getOutputs()) {
if (output.getValue().compareTo(Coin.CENT) < 0) {
if (output.getValue().compareTo(output.getMinNonDustValue()) < 0)
throw new DustySendRequested();
needAtLeastReferenceFee = true;
if (output.getValue().compareTo(output.getMinNonDustValue()) < 0) { // Is transaction a "dust".
if (output.getScriptPubKey().isOpReturn()) { // Transactions that are OP_RETURN can't be dust regardless of their value.
++opReturnCount;
continue;
} else {
throw new DustySendRequested();
}
}
break;
}
}
}
if (opReturnCount > 1) { // Only 1 OP_RETURN per transaction allowed.
throw new MultipleOpReturnRequested();
}
// Calculate a list of ALL potential candidates for spending and then ask a coin selector to provide us

View File

@@ -725,6 +725,10 @@ public class Script {
return Utils.decodeMPI(Utils.reverseBytes(chunk), false);
}
public boolean isOpReturn() {
return chunks.size() == 2 && chunks.get(0).equalsOpCode(OP_RETURN);
}
/**
* Exposes the script interpreter. Normally you should not use this directly, instead use
* {@link org.bitcoinj.core.TransactionInput#verify(org.bitcoinj.core.TransactionOutput)} or

View File

@@ -19,6 +19,9 @@ package org.bitcoinj.core;
import org.bitcoinj.core.Wallet.SendRequest;
import org.bitcoinj.crypto.*;
import org.bitcoinj.script.Script;
import org.bitcoinj.script.ScriptBuilder;
import org.bitcoinj.script.ScriptOpCodes;
import org.bitcoinj.signers.StatelessTransactionSigner;
import org.bitcoinj.signers.TransactionSigner;
import org.bitcoinj.store.BlockStoreException;
@@ -362,28 +365,31 @@ public class WalletTest extends TestWithWallet {
}
private void receiveATransaction(Wallet wallet, Address toAddress) throws Exception {
Coin v1 = COIN;
final ListenableFuture<Coin> availFuture = wallet.getBalanceFuture(v1, Wallet.BalanceType.AVAILABLE);
final ListenableFuture<Coin> estimatedFuture = wallet.getBalanceFuture(v1, Wallet.BalanceType.ESTIMATED);
receiveATransactionAmount(wallet, toAddress, COIN);
}
private void receiveATransactionAmount(Wallet wallet, Address toAddress, Coin amount) throws IOException {
final ListenableFuture<Coin> availFuture = wallet.getBalanceFuture(amount, Wallet.BalanceType.AVAILABLE);
final ListenableFuture<Coin> estimatedFuture = wallet.getBalanceFuture(amount, Wallet.BalanceType.ESTIMATED);
assertFalse(availFuture.isDone());
assertFalse(estimatedFuture.isDone());
// Send some pending coins to the wallet.
Transaction t1 = sendMoneyToWallet(wallet, v1, toAddress, null);
Transaction t1 = sendMoneyToWallet(wallet, amount, toAddress, null);
Threading.waitForUserCode();
final ListenableFuture<Transaction> depthFuture = t1.getConfidence().getDepthFuture(1);
assertFalse(depthFuture.isDone());
assertEquals(ZERO, wallet.getBalance());
assertEquals(v1, wallet.getBalance(Wallet.BalanceType.ESTIMATED));
assertEquals(amount, wallet.getBalance(Wallet.BalanceType.ESTIMATED));
assertFalse(availFuture.isDone());
// Our estimated balance has reached the requested level.
assertTrue(estimatedFuture.isDone());
assertEquals(1, wallet.getPoolSize(Pool.PENDING));
assertEquals(0, wallet.getPoolSize(WalletTransaction.Pool.UNSPENT));
assertEquals(0, wallet.getPoolSize(Pool.UNSPENT));
// Confirm the coins.
sendMoneyToWallet(wallet, t1, AbstractBlockChain.NewBlockType.BEST_CHAIN);
assertEquals("Incorrect confirmed tx balance", v1, wallet.getBalance());
assertEquals("Incorrect confirmed tx PENDING pool size", 0, wallet.getPoolSize(WalletTransaction.Pool.PENDING));
assertEquals("Incorrect confirmed tx UNSPENT pool size", 1, wallet.getPoolSize(WalletTransaction.Pool.UNSPENT));
assertEquals("Incorrect confirmed tx balance", amount, wallet.getBalance());
assertEquals("Incorrect confirmed tx PENDING pool size", 0, wallet.getPoolSize(Pool.PENDING));
assertEquals("Incorrect confirmed tx UNSPENT pool size", 1, wallet.getPoolSize(Pool.UNSPENT));
assertEquals("Incorrect confirmed tx ALL pool size", 1, wallet.getTransactions(true).size());
Threading.waitForUserCode();
assertTrue(availFuture.isDone());
@@ -1596,6 +1602,107 @@ public class WalletTest extends TestWithWallet {
wallet.completeTx(req);
}
@Test
public void opReturnOneOutputTest() throws Exception {
// Tests basic send of transaction with one output that doesn't transfer any value but just writes OP_RETURN.
receiveATransaction(wallet, myAddress);
Transaction tx = new Transaction(params);
Coin messagePrice = Coin.ZERO;
Script script = new ScriptBuilder().op(ScriptOpCodes.OP_RETURN).data("hello world!".getBytes()).build();
tx.addOutput(messagePrice, script);
SendRequest request = Wallet.SendRequest.forTx(tx);
wallet.completeTx(request);
}
@Test
public void opReturnOneOutputWithValueTest() throws Exception {
// Tests basic send of transaction with one output that destroys coins and has an OP_RETURN.
receiveATransaction(wallet, myAddress);
Transaction tx = new Transaction(params);
Coin messagePrice = CENT;
Script script = new ScriptBuilder().op(ScriptOpCodes.OP_RETURN).data("hello world!".getBytes()).build();
tx.addOutput(messagePrice, script);
SendRequest request = Wallet.SendRequest.forTx(tx);
wallet.completeTx(request);
}
@Test
public void opReturnTwoOutputsTest() throws Exception {
// Tests sending transaction where one output transfers BTC, the other one writes OP_RETURN.
receiveATransaction(wallet, myAddress);
Address notMyAddr = new ECKey().toAddress(params);
Transaction tx = new Transaction(params);
Coin messagePrice = Coin.ZERO;
Script script = new ScriptBuilder().op(ScriptOpCodes.OP_RETURN).data("hello world!".getBytes()).build();
tx.addOutput(CENT, notMyAddr);
tx.addOutput(messagePrice, script);
SendRequest request = Wallet.SendRequest.forTx(tx);
wallet.completeTx(request);
}
@Test(expected = Wallet.MultipleOpReturnRequested.class)
public void twoOpReturnsPerTransactionTest() throws Exception {
// Tests sending transaction where there are 2 attempts to write OP_RETURN scripts - this should fail and throw MultipleOpReturnRequested.
receiveATransaction(wallet, myAddress);
Transaction tx = new Transaction(params);
Coin messagePrice = Coin.ZERO;
Script script1 = new ScriptBuilder().op(ScriptOpCodes.OP_RETURN).data("hello world 1!".getBytes()).build();
Script script2 = new ScriptBuilder().op(ScriptOpCodes.OP_RETURN).data("hello world 2!".getBytes()).build();
tx.addOutput(messagePrice, script1);
tx.addOutput(messagePrice, script2);
SendRequest request = Wallet.SendRequest.forTx(tx);
wallet.completeTx(request);
}
@Test(expected = Wallet.DustySendRequested.class)
public void sendDustTest() throws InsufficientMoneyException {
// Tests sending dust, should throw DustySendRequested.
Transaction tx = new Transaction(params);
Address notMyAddr = new ECKey().toAddress(params);
tx.addOutput(Transaction.MIN_NONDUST_OUTPUT.subtract(SATOSHI), notMyAddr);
SendRequest request = Wallet.SendRequest.forTx(tx);
wallet.completeTx(request);
}
@Test
public void sendMultipleCentsTest() throws Exception {
receiveATransactionAmount(wallet, myAddress, Coin.COIN);
Transaction tx = new Transaction(params);
Address notMyAddr = new ECKey().toAddress(params);
tx.addOutput(COIN.CENT.subtract(SATOSHI), notMyAddr);
tx.addOutput(COIN.CENT.subtract(SATOSHI), notMyAddr);
tx.addOutput(COIN.CENT.subtract(SATOSHI), notMyAddr);
tx.addOutput(COIN.CENT.subtract(SATOSHI), notMyAddr);
SendRequest request = Wallet.SendRequest.forTx(tx);
wallet.completeTx(request);
}
@Test(expected = Wallet.DustySendRequested.class)
public void sendDustAndOpReturnWithoutValueTest() throws Exception {
// Tests sending dust and OP_RETURN without value, should throw DustySendRequested because sending sending dust is not allowed in any case.
receiveATransactionAmount(wallet, myAddress, Coin.COIN);
Transaction tx = new Transaction(params);
Address notMyAddr = new ECKey().toAddress(params);
Script script = new ScriptBuilder().op(ScriptOpCodes.OP_RETURN).data("hello world!".getBytes()).build();
tx.addOutput(Coin.ZERO, script);
tx.addOutput(Coin.SATOSHI, notMyAddr);
SendRequest request = Wallet.SendRequest.forTx(tx);
wallet.completeTx(request);
}
@Test(expected = Wallet.DustySendRequested.class)
public void sendDustAndMessageWithValueTest() throws Exception {
//Tests sending dust and OP_RETURN with value, should throw DustySendRequested
receiveATransaction(wallet, myAddress);
Transaction tx = new Transaction(params);
Address notMyAddr = new ECKey().toAddress(params);
Script script = new ScriptBuilder().op(ScriptOpCodes.OP_RETURN).data("hello world!".getBytes()).build();
tx.addOutput(Coin.CENT, script);
tx.addOutput(Transaction.MIN_NONDUST_OUTPUT.subtract(SATOSHI), notMyAddr);
SendRequest request = Wallet.SendRequest.forTx(tx);
wallet.completeTx(request);
}
@Test
public void feeSolverAndCoinSelectionTest() throws Exception {
// Tests basic fee solving works