mirror of
https://github.com/Qortal/altcoinj.git
synced 2025-02-14 11:15:51 +00:00
Rework how event listeners are called, so tx confidences are updated at the end of the receive process. Otherwise it's possible for the wallet to get confused because the state can change in the middle of processing. Document this and add a test. Update PingService. Also, change onCoinsReceived so it's only called once per transaction, to know when a tx appears in a block requires registration of a tx confidence listener.
This commit is contained in:
parent
9d5465390d
commit
c0a295eed1
@ -213,6 +213,8 @@ public class Transaction extends ChildMessage implements Serializable {
|
||||
if (updatedAt == null) {
|
||||
updatedAt = new Date(block.getHeader().getTimeSeconds() * 1000);
|
||||
}
|
||||
// This can cause event listeners on TransactionConfidence to run. After this line completes, the wallets
|
||||
// state may have changed!
|
||||
getConfidence().setAppearedAtChainHeight(block.getHeight());
|
||||
}
|
||||
}
|
||||
|
@ -334,20 +334,15 @@ public class Wallet implements Serializable {
|
||||
|
||||
// If this transaction is already in the wallet we may need to move it into a different pool. At the very
|
||||
// least we need to ensure we're manipulating the canonical object rather than a duplicate.
|
||||
Transaction wtx = null;
|
||||
Transaction wtx;
|
||||
if ((wtx = pending.remove(txHash)) != null) {
|
||||
// Make sure "tx" is always the canonical object we want to manipulate, send to event handlers, etc.
|
||||
tx = wtx;
|
||||
wtx = null;
|
||||
|
||||
log.info(" <-pending");
|
||||
// A transaction we created appeared in a block. Probably this is a spend we broadcast that has been
|
||||
// accepted by the network.
|
||||
//
|
||||
// Mark the tx as appearing in this block so we can find it later after a re-org. This also lets the
|
||||
// transaction update its confidence and timestamp bookkeeping data.
|
||||
if (block != null)
|
||||
tx.setBlockAppearance(block, bestChain);
|
||||
if (bestChain) {
|
||||
if (valueSentToMe.equals(BigInteger.ZERO)) {
|
||||
// There were no change transactions so this tx is fully spent.
|
||||
@ -394,10 +389,26 @@ public class Wallet implements Serializable {
|
||||
|
||||
log.info("Balance is now: " + bitcoinValueToFriendlyString(getBalance()));
|
||||
|
||||
// Inform anyone interested that we have new coins. Note: we may be re-entered by the event listener,
|
||||
// so we must not make assumptions about our state after this loop returns! For example the balance we just
|
||||
// received might already be spent!
|
||||
if (!reorg && bestChain && valueDifference.compareTo(BigInteger.ZERO) > 0) {
|
||||
// WARNING: The code beyond this point can trigger event listeners on transaction confidence objects, which are
|
||||
// in turn allowed to re-enter the Wallet. This means we cannot assume anything about the state of the wallet
|
||||
// from now on. The balance just received may already be spent.
|
||||
|
||||
// Mark the tx as appearing in this block so we can find it later after a re-org. This also lets the
|
||||
// transaction update its confidence and timestamp bookkeeping data.
|
||||
if (block != null) {
|
||||
tx.setBlockAppearance(block, bestChain);
|
||||
}
|
||||
|
||||
// Inform anyone interested that we have new coins but only if:
|
||||
// - This is not due to a re-org.
|
||||
// - The coins appeared on the best chain.
|
||||
// - We did in fact receive some new money.
|
||||
// - We have not already informed the user about the coins when we received the tx broadcast, or for our
|
||||
// own spends. If users want to know when a broadcast tx becomes confirmed, they need to use tx confidence
|
||||
// listeners.
|
||||
//
|
||||
// TODO: Decide whether to run the event listeners, if a tx confidence listener already modified the wallet.
|
||||
if (!reorg && bestChain && valueDifference.compareTo(BigInteger.ZERO) > 0 && wtx == null) {
|
||||
for (WalletEventListener l : eventListeners) {
|
||||
synchronized (l) {
|
||||
l.onCoinsReceived(this, tx, prevBalance, getBalance());
|
||||
|
@ -26,10 +26,12 @@ import java.math.BigInteger;
|
||||
*/
|
||||
public interface WalletEventListener {
|
||||
/**
|
||||
* This is called on a Peer thread when a block is received that sends some coins to you. Note that this will
|
||||
* also be called when downloading the block chain as the wallet balance catches up so if you don't want that
|
||||
* register the event listener after the chain is downloaded. It's safe to use methods of wallet during the
|
||||
* execution of this callback.
|
||||
* This is called on a Peer thread when a transaction is seen that sends coins to this wallet, either because it
|
||||
* was broadcast across the network or because a block was received. If a transaction is seen when it was broadcast,
|
||||
* onCoinsReceived won't be called again when a block containing it is received. If you want to know when such a
|
||||
* transaction receives its first confirmation, register a {@link TransactionConfidence} event listener using
|
||||
* the object retrieved via {@link com.google.bitcoin.core.Transaction#getConfidence()}. It's safe to modify the
|
||||
* wallet in this callback, for example, by spending the transaction just received.
|
||||
*
|
||||
* @param wallet The wallet object that received the coins/
|
||||
* @param tx The transaction which sent us the coins.
|
||||
|
@ -54,9 +54,19 @@ import java.util.Set;
|
||||
* .net/projects/bitcoin/files/Bitcoin/testnet-in-a-box/">testnet in a box</a> to do everything purely locally.</p>
|
||||
*/
|
||||
public class PingService {
|
||||
|
||||
private Wallet w;
|
||||
private final PeerGroup peerGroup;
|
||||
private final BlockChain chain;
|
||||
private final BlockStore blockStore;
|
||||
private final File walletFile;
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
BriefLogFormatter.init();
|
||||
new PingService(args);
|
||||
}
|
||||
|
||||
public PingService(String[] args) throws Exception {
|
||||
String peerHost = args.length > 0 ? args[0] : null;
|
||||
int peerPort = args.length > 1 ? Integer.parseInt(args[1]) : NetworkParameters.prodNet().port;
|
||||
|
||||
@ -65,8 +75,7 @@ public class PingService {
|
||||
String filePrefix = testNet ? "pingservice-testnet" : "pingservice-prodnet";
|
||||
|
||||
// Try to read the wallet from storage, create a new one if not possible.
|
||||
Wallet w;
|
||||
final File walletFile = new File(filePrefix + ".wallet");
|
||||
walletFile = new File(filePrefix + ".wallet");
|
||||
try {
|
||||
w = Wallet.loadFromFile(walletFile);
|
||||
} catch (IOException e) {
|
||||
@ -82,16 +91,16 @@ public class PingService {
|
||||
|
||||
// Load the block chain, if there is one stored locally.
|
||||
System.out.println("Reading block store from disk");
|
||||
BlockStore blockStore = new BoundedOverheadBlockStore(params, new File(filePrefix + ".blockchain"));
|
||||
blockStore = new BoundedOverheadBlockStore(params, new File(filePrefix + ".blockchain"));
|
||||
|
||||
// Connect to the localhost node. One minute timeout since we won't try any other peers
|
||||
System.out.println("Connecting ...");
|
||||
final BlockChain chain = new BlockChain(params, wallet, blockStore);
|
||||
|
||||
final PeerGroup peerGroup = new PeerGroup(params, chain);
|
||||
chain = new BlockChain(params, wallet, blockStore);
|
||||
|
||||
peerGroup = new PeerGroup(params, chain);
|
||||
|
||||
// Download headers only until a day ago.
|
||||
peerGroup.setFastCatchupTimeSecs((new Date().getTime() / 1000) - (60 * 60 * 24));
|
||||
|
||||
if (peerHost != null) {
|
||||
peerGroup.addAddress(new PeerAddress(InetAddress.getByName(peerHost), peerPort));
|
||||
} else {
|
||||
@ -112,10 +121,13 @@ public class PingService {
|
||||
for (Transaction tx : transactions) {
|
||||
System.out.println(tx);
|
||||
try {
|
||||
System.out.println("Work done: " + tx.getConfidence().getWorkDone(chain).toString());
|
||||
System.out.println(tx.getConfidence());
|
||||
if (tx.getConfidence().getConfidenceType() == TransactionConfidence.ConfidenceType.BUILDING)
|
||||
System.out.println("Work done: " + tx.getConfidence().getWorkDone(chain).toString());
|
||||
} catch (BlockStoreException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
System.out.println();
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -126,45 +138,38 @@ public class PingService {
|
||||
public void onCoinsReceived(Wallet w, Transaction tx, BigInteger prevBalance, BigInteger newBalance) {
|
||||
// Running on a peer thread.
|
||||
assert !newBalance.equals(BigInteger.ZERO);
|
||||
|
||||
BigInteger value = tx.getValueSentToMe(w);
|
||||
|
||||
if (tx.isPending()) {
|
||||
// Broadcast, but we can't really verify it's valid until it appears in a block.
|
||||
BigInteger value = tx.getValueSentToMe(w);
|
||||
System.out.println("Received pending tx for " + Utils.bitcoinValueToFriendlyString(value) +
|
||||
": " + tx);
|
||||
": " + tx);
|
||||
System.out.println(tx.getConfidence());
|
||||
tx.getConfidence().addEventListener(new TransactionConfidence.Listener() {
|
||||
public void onConfidenceChanged(Transaction tx2) {
|
||||
if (tx2.getConfidence().getConfidenceType() == TransactionConfidence.ConfidenceType.BUILDING) {
|
||||
// Coins were confirmed.
|
||||
bounceCoins(tx2);
|
||||
|
||||
// TODO: Make this work.
|
||||
// tx2.getConfidence().removeEventListener(this);
|
||||
} else {
|
||||
System.out.println(String.format("Confidence of %s changed, is now: %s",
|
||||
tx2.getHashAsString(), tx2.getConfidence().toString()));
|
||||
}
|
||||
}
|
||||
});
|
||||
try {
|
||||
w.saveToFile(walletFile);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
// Ignore for now, as we won't be allowed to spend until the tx is no longer pending. We'll get
|
||||
// another callback here when the tx is confirmed.
|
||||
// Ignore for now, as we won't be allowed to spend until the tx is no longer pending. This is
|
||||
// something that should be fixed in future.
|
||||
return;
|
||||
} else {
|
||||
System.out.println("Saw tx be incorporated into chain");
|
||||
System.out.println(tx.getConfidence());
|
||||
}
|
||||
|
||||
// It's impossible to pick one specific identity that you receive coins from in BitCoin as there
|
||||
// could be inputs from many addresses. So instead we just pick the first and assume they were all
|
||||
// owned by the same person.
|
||||
try {
|
||||
TransactionInput input = tx.getInputs().get(0);
|
||||
Address from = input.getFromAddress();
|
||||
System.out.println("Received " + Utils.bitcoinValueToFriendlyString(value) + " from " + from.toString());
|
||||
// Now send the coins back!
|
||||
Transaction sendTx = w.sendCoins(peerGroup, from, value);
|
||||
assert sendTx != null; // We should never try to send more coins than we have!
|
||||
System.out.println("Sent coins back! Transaction hash is " + sendTx.getHashAsString());
|
||||
w.saveToFile(walletFile);
|
||||
} catch (ScriptException e) {
|
||||
// If we didn't understand the scriptSig, just crash.
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException(e);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException(e);
|
||||
// We found the coins in a block directly, without it being broadcast first (catching up with
|
||||
// the chain), so just send them right back immediately.
|
||||
bounceCoins(tx);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -174,4 +179,28 @@ public class PingService {
|
||||
System.out.println("Waiting for coins to arrive. Press Ctrl-C to quit.");
|
||||
// The PeerGroup thread keeps us alive until something kills the process.
|
||||
}
|
||||
|
||||
private void bounceCoins(Transaction tx) {
|
||||
// It's impossible to pick one specific identity that you receive coins from in BitCoin as there
|
||||
// could be inputs from many addresses. So instead we just pick the first and assume they were all
|
||||
// owned by the same person.
|
||||
try {
|
||||
BigInteger value = tx.getValueSentToMe(w);
|
||||
TransactionInput input = tx.getInputs().get(0);
|
||||
Address from = input.getFromAddress();
|
||||
System.out.println("Received " + Utils.bitcoinValueToFriendlyString(value) + " from " + from.toString());
|
||||
// Now send the coins back!
|
||||
Transaction sendTx = w.sendCoins(peerGroup, from, value);
|
||||
assert sendTx != null; // We should never try to send more coins than we have!
|
||||
System.out.println("Sent coins back! Transaction hash is " + sendTx.getHashAsString());
|
||||
w.saveToFile(walletFile);
|
||||
} catch (ScriptException e) {
|
||||
// If we didn't understand the scriptSig, just crash.
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException(e);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -314,6 +314,7 @@ public class WalletTest {
|
||||
|
||||
// First one is "called" second is "pending".
|
||||
final boolean[] flags = new boolean[2];
|
||||
final Transaction[] notifiedTx = new Transaction[1];
|
||||
wallet.addEventListener(new AbstractWalletEventListener() {
|
||||
@Override
|
||||
public void onCoinsReceived(Wallet wallet, Transaction tx, BigInteger prevBalance, BigInteger newBalance) {
|
||||
@ -324,6 +325,7 @@ public class WalletTest {
|
||||
assertEquals(newBalance, nanos);
|
||||
flags[0] = true;
|
||||
flags[1] = tx.isPending();
|
||||
notifiedTx[0] = tx;
|
||||
}
|
||||
});
|
||||
|
||||
@ -334,14 +336,23 @@ public class WalletTest {
|
||||
// Check we don't get notified if we receive it again.
|
||||
wallet.receivePending(t1);
|
||||
assertFalse(flags[0]);
|
||||
// Now check again when we receive it via a block.
|
||||
flags[1] = true;
|
||||
// Now check again, that we should NOT be notified when we receive it via a block (we were already notified).
|
||||
// However the confidence should be updated.
|
||||
// Make a fresh copy of the tx to ensure we're testing realistically.
|
||||
flags[0] = flags[1] = false;
|
||||
notifiedTx[0].getConfidence().addEventListener(new TransactionConfidence.Listener() {
|
||||
public void onConfidenceChanged(Transaction tx) {
|
||||
flags[1] = true;
|
||||
}
|
||||
});
|
||||
assertEquals(TransactionConfidence.ConfidenceType.NOT_SEEN_IN_CHAIN,
|
||||
notifiedTx[0].getConfidence().getConfidenceType());
|
||||
final Transaction t1Copy = new Transaction(params, t1.bitcoinSerialize());
|
||||
wallet.receiveFromBlock(t1Copy, createFakeBlock(params, blockStore, t1Copy).storedBlock,
|
||||
BlockChain.NewBlockType.BEST_CHAIN);
|
||||
assertTrue(flags[0]);
|
||||
assertFalse(flags[1]); // is not pending
|
||||
assertFalse(flags[0]);
|
||||
assertTrue(flags[1]);
|
||||
assertEquals(TransactionConfidence.ConfidenceType.BUILDING, notifiedTx[0].getConfidence().getConfidenceType());
|
||||
// Check we don't get notified about an irrelevant transaction.
|
||||
flags[0] = false;
|
||||
flags[1] = false;
|
||||
|
Loading…
x
Reference in New Issue
Block a user