use of org.catena.common.CatenaStatement in project catena-java by alinush.
the class ClientWallet method updateCatenaLog.
/**
* Updates our building and pending queues with the latest TXNs from the Wallet. Calls our statement listeners for
* newly appended statements and withdrawn ones too. Calls the whistleblow listeners if something goes bad (lies,
* bad signatures).
*
* NOTE: Current implementation will not deliver onWithdrawn/onAppended notifications if the client restarts in the
* middle of this call because we lose some state that we don't persist on disk.
*
* NOTE: The invariant here is that the root-of-trust TX is in the wallet but it could be either PENDING or BUILDING.
*
* @param callListeners
*/
void updateCatenaLog(boolean callListeners) {
lock.lock();
try {
log.debug("callListeners={}", callListeners);
Sha256Hash rootOfTrustTxid = getCatenaExtension().getRootOfTrustTxid();
Transaction rootOfTrustTxn = getTransaction(rootOfTrustTxid);
checkNotNull(rootOfTrustTxn);
checkState(CatenaUtils.maybeCatenaTx(rootOfTrustTxn), "root-of-trust TXN looks like non-Catena TXN");
// INVARIANT: BQ and PQ have to be consistent with one anothe: BQ TXs should be correctly chained and signed.
// Also, the first statement in PQ should have come from a TXN that was connected to a once-in-BQ TXN.
// Also, the first TX in BQ/PQ, if any, should be the root-of-trust TXN.
Stack<CatenaStatement> withdrawnStack = new Stack<CatenaStatement>();
// popped TXNs back to PQ to make sure we will not be lied to about the withdrawn statements.
while (!bq.isEmpty()) {
CatenaStatement tailStmt = bq.peek();
Transaction tailTxn = getTransaction(tailStmt.getTxHash());
checkState(CatenaUtils.maybeCatenaTx(tailTxn), "broken invariant: non-Catena TXN in building queue");
byte[] tailData = tailStmt.getData();
String tailHexData = Utils.toHex(tailData);
String tailHash = tailTxn.getHashAsString().substring(0, 7) + "...";
// Check if the TXN was killed by a fork
if (TxUtils.isBuildingTxn(tailTxn) == false) {
log.debug("Pop BQ: Popping statement '" + tailHexData + "' (from tx " + tailHash + ")");
bq.pop();
// Keep track of this statement as a withdrawn statement (might be added back to BQ though)
withdrawnStack.push(tailStmt);
// Move the killed txn from BQ to PQ since it's now either DEAD, IN_CONFLICT or PENDING.
log.debug(" -> Push PQ: Pushing statement to PQ '" + tailHexData + "' (from tx " + tailHash + ")");
// We recreate the statement here to get rid of its txid, since it's in PQ now
pq.addFirst(CatenaStatement.fromData(tailData));
} else {
// We stop at the first txn that is still in BUILDING status
log.debug("Pop BQ: Stopped popping at BUILDING tx " + tailTxn.getHashAsString().substring(0, 7) + "...");
break;
}
}
// INVARIANT: Everything in BQ is BUILDING, but there might be new TXs in the wallet that are BUILDING but
// not in BQ yet. Some of these TXNs might be reissued ones for statements that have just been withdrawn
// or that are in PQ from previous updateCatenaLog calls.
// Step 2: Push BQ
//
// Push reissued and/or newly issued statements into building queue (BQ)
// Step 2.1: Find the next BUILDING txn to push in BQ
Transaction lastBuildingTxn;
if (bq.isEmpty()) {
// sure it's in BUILDING status.
if (TxUtils.isBuildingTxn(rootOfTrustTxn)) {
log.debug("Push BQ: Pushing root-of-trust TXN " + rootOfTrustTxid);
bq.push(CatenaStatement.fromTxn(rootOfTrustTxn));
// not call onWithdrawn on the root-of-trust TXN, since it's not a proper Catena statement.
if (!withdrawnStack.isEmpty()) {
withdrawnStack.pop();
}
// to BUILDING), then remove it from PQ.
if (!pq.isEmpty()) {
log.debug(" -> Pop PQ: Popping root-of-trust TXN from PQ");
pq.pollFirst();
}
lastBuildingTxn = rootOfTrustTxn;
} else {
// Root-of-trust TXN is not BUIDLING yet => no other Catena TXNs can be BUILDING either
lastBuildingTxn = null;
log.debug("Push BQ: Root-of-trust TXN is not BUILDING yet");
}
} else {
lastBuildingTxn = getTransaction(bq.peek().getTxHash());
checkState(CatenaUtils.maybeCatenaTx(lastBuildingTxn), "broken invariant: non-Catena TXN in building queue");
}
if (lastBuildingTxn != null)
log.debug("BQ: Starting to push after TXN " + lastBuildingTxn.getHashAsString().substring(0, 7) + "...");
else
log.debug("BQ: No new BUILDING TXNs were found");
// if they were killed by a fork and removed in Step 1, but new TXNs with the same data were created in the fork.
if (lastBuildingTxn != null) {
TransactionOutput prevOutput = lastBuildingTxn.getOutput(0);
Transaction nextTxn = CatenaUtils.getNextCatenaTx(this, lastBuildingTxn);
while (nextTxn != null) {
// Maintain BQ invariant: Check signature and format of TXN before adding to BQ
if (CatenaUtils.isSignedCatenaTx(nextTxn, getChainAddress(), prevOutput, true) == false) {
log.warn("Push BQ: Whistleblowing! Bad Catena TXN found (tx " + nextTxn.getHash() + ")");
queueOnWhistleblow(nextTxn, "ill-formated or incorrectly signed Catena TXN: " + nextTxn.getHash());
// We can stop execution here because invariants all hold at this point.
return;
}
if (TxUtils.isBuildingTxn(nextTxn)) {
// Add it to BQ (might be adding it back)
CatenaStatement tailStmt = CatenaStatement.fromTxn(nextTxn);
byte[] tailData = tailStmt.getData();
String tailHexData = Utils.toHex(tailData);
String tailHash = nextTxn.getHashAsString().substring(0, 7) + "...";
bq.push(tailStmt);
log.debug("Push BQ: Pushed statement " + tailHexData + " (tx " + tailHash + "...)");
// the TX has a different fee.)
if (!pq.isEmpty()) {
CatenaStatement headStmt = pq.peekFirst();
String headHexData = Utils.toHex(headStmt.getData());
log.debug("bqData={}, pqData={}", tailHexData, headHexData);
// Check if the BQ tail and the PQ head both commit the same data, or else whistleblow.
if (headStmt.hasSameData(tailStmt) == false) {
String err = Utils.fmt("expected statement '{}' (hex) but got '{}' (hex) in tx {}", headHexData, tailHexData, nextTxn.getHash());
log.warn("Push BQ: Whistleblowing! Inconsistent statements detected: " + err);
// pop the lying statement from BQ
bq.pop();
queueOnWhistleblow(nextTxn, err);
break;
}
// Not a lie, we can move it from BQ to PQ
pq.pollFirst();
// Check if the statement is being added back after being withdrawn
boolean isAddedBack = withdrawnStack.isEmpty() == false;
if (isAddedBack) {
CatenaStatement s = withdrawnStack.pop();
// Already checked for equivocation above, so this should hold
checkState(s.hasSameData(tailStmt), "broken invariant: withdrawn statement does not match reissued one");
log.debug(" -> Added back statement " + Utils.toHex(s.getData()));
} else {
// Call onStatementAppended because this is a new statement, not a reissued one!
if (callListeners)
queueOnAppend(tailStmt);
}
} else {
if (callListeners)
queueOnAppend(tailStmt);
}
} else {
// We found a non-building TXN, we are done.
break;
}
// Move on to the next BUILDING txn in the Catena chain, if any
prevOutput = nextTxn.getOutput(0);
nextTxn = CatenaUtils.getNextCatenaTx(this, nextTxn);
}
}
// INVARIANT: All BUILDING Catena TXNs are in BQ
// Step 3: Check for lies w.r.t. to the building Catena TXs: we want to catch PENDING/DEAD txns which have been
// killed because they double spend.
//
// NOTE: This is an ~O(n) kind of ordeal, but since bitcoinj does the same thing everytime it receives a TXN
// (i.e. in Wallet::isTransactionRelevant, it calls findDoubleSpendsAgainst all txns), we don't care too much.
// TODO: REFUND: Will have to slightly adjust the code here to deal with the fact that Catena TXs might have
// multiple inputs, for additional miner fees.
// We findDoubleSpendsAgainst(building, Pool.DEAD) and get a map of outpoints to txs double spending them
// Then, we iterate through the double spends and ensure they commit the same data.
Map<TransactionOutPoint, List<Transaction>> deadDoubleSpends = TxUtils.findDoubleSpendsAgainst(getCatenaBuildingTxns(), getTransactionPool(Pool.DEAD));
for (Map.Entry<TransactionOutPoint, List<Transaction>> e : deadDoubleSpends.entrySet()) {
TransactionOutPoint outp = e.getKey();
checkState(outp.getIndex() == 0, "broken invariant: Catena TX in BQ was supposed to only have one input");
Iterator<Transaction> txit = e.getValue().iterator();
checkState(txit.hasNext(), "broken invariant: findDoubleSpendsAgainst returned non-double-spends");
// The 1st returned TXN is from the set of Catena building TXNs
Transaction tx = txit.next();
byte[] origData = CatenaUtils.getCatenaTxData(tx);
log.warn("Outpoint " + outp + " was double spent");
log.warn(" -> 1st Building Catena TXN, txid={}..., stmt={}", tx.getHashAsString().substring(0, 7), Utils.toHex(origData));
// The subsequently returned TXNs are DEAD txns that double spend the Catena building TXN
while (txit.hasNext()) {
Transaction ds = txit.next();
if (CatenaUtils.maybeCatenaTx(ds)) {
byte[] lieData = CatenaUtils.getCatenaTxData(ds);
// TODO: the signature verification here can fail because ds is dead and the connected output
// we pass here is not connected to ds, but to tx (i.e., the building Catena TX)
boolean isCorrectlySigned = CatenaUtils.isSignedCatenaTx(ds, getChainAddress(), outp.getConnectedOutput(), true);
if (CatenaStatement.hasSameData(origData, lieData) == false) {
log.warn(" -> 2nd lying TXN, txid={}..., stmt={}, isCorrectlySigned={}", ds.getHashAsString().substring(0, 7), Utils.toHex(lieData), isCorrectlySigned);
queueOnWhistleblow(ds, Utils.fmt("Lie detected w.r.t. Catena txid={}, stmt={}: lying txid={}, stmt={}, isCorrectlySigned={}", tx.getHashAsString(), Utils.toHex(origData), ds.getHashAsString(), Utils.toHex(lieData), isCorrectlySigned));
} else {
if (isCorrectlySigned) {
log.warn(" -> 2nd consistent, correctly signed TXN, txid={}...", ds.getHashAsString().substring(0, 7));
} else {
// We will catch the bad signature later if this TX ever becomes BUILDING again,
// no need to whistleblow here.
log.warn(" -> 2nd consistent, but incorrectly signed TXN, txid={}...", ds.getHashAsString().substring(0, 7));
}
}
} else {
queueOnWhistleblow(ds, "non-Catena TX " + tx.getHashAsString() + " double spent outpoint " + outp);
}
}
}
// Call onStatementWithdrawn for statements that were popped in Step 1 and not added back in this Step 2.2
if (callListeners)
queueOnWithdrawn(withdrawnStack);
} finally {
lock.unlock();
}
}
Aggregations