Search in sources :

Example 6 with CatenaStatement

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();
    }
}
Also used : CatenaStatement(org.catena.common.CatenaStatement) TransactionOutput(org.bitcoinj.core.TransactionOutput) Sha256Hash(org.bitcoinj.core.Sha256Hash) Stack(java.util.Stack) Transaction(org.bitcoinj.core.Transaction) ArrayList(java.util.ArrayList) LinkedList(java.util.LinkedList) List(java.util.List) CopyOnWriteArrayList(java.util.concurrent.CopyOnWriteArrayList) Map(java.util.Map) TransactionOutPoint(org.bitcoinj.core.TransactionOutPoint)

Aggregations

CatenaStatement (org.catena.common.CatenaStatement)6 Transaction (org.bitcoinj.core.Transaction)4 ArrayList (java.util.ArrayList)2 CopyOnWriteArrayList (java.util.concurrent.CopyOnWriteArrayList)2 Sha256Hash (org.bitcoinj.core.Sha256Hash)2 TransactionOutPoint (org.bitcoinj.core.TransactionOutPoint)2 VisibleForTesting (com.google.common.annotations.VisibleForTesting)1 LinkedList (java.util.LinkedList)1 List (java.util.List)1 Map (java.util.Map)1 Stack (java.util.Stack)1 Semaphore (java.util.concurrent.Semaphore)1 Coin (org.bitcoinj.core.Coin)1 TransactionOutput (org.bitcoinj.core.TransactionOutput)1 ClientServerTest (org.catena.common.ClientServerTest)1 Test (org.junit.Test)1