com.bitsofproof.supernode.core.TxHandler.java Source code

Java tutorial

Introduction

Here is the source code for com.bitsofproof.supernode.core.TxHandler.java

Source

/*
 * Copyright 2013 bits of proof zrt.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.bitsofproof.supernode.core;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;

import com.bitsofproof.supernode.api.Hash;
import com.bitsofproof.supernode.api.ValidationException;
import com.bitsofproof.supernode.messages.BitcoinMessageListener;
import com.bitsofproof.supernode.messages.GetDataMessage;
import com.bitsofproof.supernode.messages.InvMessage;
import com.bitsofproof.supernode.messages.MempoolMessage;
import com.bitsofproof.supernode.messages.TxMessage;
import com.bitsofproof.supernode.model.Blk;
import com.bitsofproof.supernode.model.Tx;
import com.bitsofproof.supernode.model.TxIn;
import com.bitsofproof.supernode.model.TxOut;

public class TxHandler implements TrunkListener {
    private static final Logger log = LoggerFactory.getLogger(TxHandler.class);

    private final BitcoinNetwork network;

    private final Set<String> heard = Collections.synchronizedSet(new HashSet<String>());
    private final Map<String, Tx> unconfirmed = Collections.synchronizedMap(new HashMap<String, Tx>());
    private final Set<String> own = Collections.synchronizedSet(new HashSet<String>());
    private TxOutCache availableOutput = null;
    private PlatformTransactionManager transactionManager;

    private final List<TxListener> transactionListener = new ArrayList<TxListener>();

    public void addTransactionListener(TxListener listener) {
        transactionListener.add(listener);
    }

    public void setTransactionManager(PlatformTransactionManager transactionManager) {
        this.transactionManager = transactionManager;
    }

    public TxHandler(final BitcoinNetwork network) {
        this.network = network;
        final BlockStore store = network.getStore();

        network.scheduleJobWithFixedDelay(new Runnable() {
            @Override
            public void run() {
                // give retransmits of previously failed tx a chance
                synchronized (heard) {
                    heard.clear();
                }
                // re-transmit own until not in a block
                synchronized (own) {
                    if (!own.isEmpty()) {
                        for (BitcoinPeer peer : network.getConnectPeers()) {
                            InvMessage tm = (InvMessage) peer.createMessage("inv");
                            for (String t : own) {
                                log.debug("Re-broadcast " + t);
                                tm.getTransactionHashes().add(new Hash(t).toByteArray());
                            }
                            peer.send(tm);
                        }
                    }
                }
            }
        }, 10, 10, TimeUnit.MINUTES);

        store.addTrunkListener(this);
        network.getStore().runInCacheContext(new BlockStore.CacheContextRunnable() {
            @Override
            public void run(TxOutCache cache) {
                availableOutput = new ImplementTxOutCacheDelta(cache);
            }
        });

        network.addListener("inv", new BitcoinMessageListener<InvMessage>() {
            @Override
            public void process(InvMessage im, BitcoinPeer peer) {
                GetDataMessage get = (GetDataMessage) peer.createMessage("getdata");
                for (byte[] h : im.getTransactionHashes()) {
                    String hash = new Hash(h).toString();
                    synchronized (unconfirmed) {
                        synchronized (heard) {
                            if (!heard.contains(hash)) {
                                heard.add(hash);
                                if (!unconfirmed.containsKey(hash)) {
                                    log.trace("heard about new transaction " + hash + " from " + peer.getAddress());
                                    get.getTransactions().add(h);
                                }
                            }
                        }
                    }
                }
                if (get.getTransactions().size() > 0) {
                    log.trace("asking for transaction details from " + peer.getAddress());
                    peer.send(get);
                }
            }
        });
        network.addListener("tx", new BitcoinMessageListener<TxMessage>() {
            @Override
            public void process(final TxMessage txm, final BitcoinPeer peer) {
                log.trace(
                        "received transaction details for " + txm.getTx().getHash() + " from " + peer.getAddress());
                try {
                    validateCacheAndSend(txm.getTx(), peer);
                } catch (ValidationException e) {
                }
            }
        });
        network.addListener("mempool", new BitcoinMessageListener<MempoolMessage>() {
            @Override
            public void process(final MempoolMessage m, final BitcoinPeer peer) {
                log.trace("received mempool request from " + peer.getAddress());
                InvMessage tm = (InvMessage) peer.createMessage("inv");
                synchronized (unconfirmed) {
                    for (Tx tx : unconfirmed.values()) {
                        tm.getTransactionHashes().add(new Hash(tx.getHash()).toByteArray());
                    }
                }
                peer.send(tm);
                log.debug("sent mempool to " + peer.getAddress());
            }
        });

    }

    public void validateCacheAndSend(final Tx t, final BitcoinPeer peer) throws ValidationException {
        ValidationException exception = network.getStore().runInCacheContext(new BlockStore.CacheContextRunnable() {
            @Override
            public void run(TxOutCache cache) throws ValidationException {
                synchronized (unconfirmed) {
                    if (!unconfirmed.containsKey(t.getHash())) {
                        ValidationException exception = new TransactionTemplate(transactionManager)
                                .execute(new TransactionCallback<ValidationException>() {
                                    @Override
                                    public ValidationException doInTransaction(TransactionStatus status) {
                                        status.setRollbackOnly();

                                        try {
                                            network.getStore().validateTransaction(t, availableOutput);
                                            sendTransaction(t, peer);
                                            cacheTransaction(t);
                                            notifyListener(t);
                                            if (peer == null) {
                                                synchronized (own) {
                                                    own.add(t.getHash());
                                                }
                                            }
                                            return null;
                                        } catch (ValidationException e) {
                                            return e;
                                        }
                                    }
                                });
                        if (exception != null) {
                            throw exception;
                        }
                    }
                }
            }
        });
        if (exception != null) {
            log.debug("REJECTING transaction " + t.getHash());
            throw exception;
        }
    }

    public Tx getTransaction(String hash) {
        return unconfirmed.get(hash);
    }

    public List<Tx> getUnconfirmedForHashes(Set<String> hashes) {
        List<Tx> result = new ArrayList<Tx>();
        synchronized (unconfirmed) {
            for (Tx t : unconfirmed.values()) {
                for (TxIn i : t.getInputs()) {
                    if (hashes.contains(i.getSourceHash())) {
                        result.add(t);
                    }
                }
            }
        }
        return result;
    }

    private void cacheTransaction(Tx tx) {
        log.trace("Caching unconfirmed transaction " + tx.getHash());
        unconfirmed.put(tx.getHash(), tx);

        for (TxOut out : tx.getOutputs()) {
            availableOutput.add(out);
        }

        for (TxIn in : tx.getInputs()) {
            availableOutput.remove(in.getSourceHash(), in.getIx());
        }
    }

    private void sendTransaction(Tx tx, BitcoinPeer peer) {
        for (BitcoinPeer p : network.getConnectPeers()) {
            if (p != peer) {
                if (peer.isRelay() || peer.getFilter() == null || tx.passesFilter(peer.getFilter())) {
                    InvMessage tm = (InvMessage) p.createMessage("inv");
                    tm.getTransactionHashes().add(new Hash(tx.getHash()).toByteArray());
                    p.send(tm);
                }
            }
        }
        log.debug("relaying transaction " + tx.getHash());
    }

    private void notifyListener(Tx tx) {
        for (TxListener l : transactionListener) {
            // This further extends transaction and cache context
            l.process(tx);
        }
    }

    @Override
    public void trunkUpdate(final List<Blk> removedBlocks, final List<Blk> addedBlocks) {
        try {
            // this is already running in cache and transaction context
            List<String> dropped = new ArrayList<String>();

            synchronized (unconfirmed) {
                for (Blk blk : removedBlocks) {
                    for (Tx tx : blk.getTransactions()) {
                        if (!unconfirmed.containsKey(tx.getHash())) {
                            cacheTransaction(tx.flatCopy());
                            dropped.add(tx.getHash());
                        }
                    }
                }
                for (Blk blk : addedBlocks) {
                    for (Tx tx : blk.getTransactions()) {
                        if (unconfirmed.containsKey(tx.getHash())) {
                            unconfirmed.remove(tx.getHash());
                            synchronized (own) {
                                own.remove(tx.getHash());
                            }
                            for (TxOut o : tx.getOutputs()) {
                                availableOutput.remove(o.getTxHash(), o.getIx());
                            }
                        }
                    }
                }
                for (String o : dropped) {
                    Tx tx = unconfirmed.get(o);
                    if (tx != null) {
                        sendTransaction(tx, null);
                        notifyListener(tx);
                    }
                }
            }
        } catch (Exception e) {
            log.error("Error broadcasting trunk update");
        }
    }
}