Package lighthouse.model

Source Code of lighthouse.model.LighthouseBackendTest

package lighthouse.model;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.util.concurrent.Uninterruptibles;
import com.google.protobuf.ByteString;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import javafx.collections.ObservableList;
import javafx.collections.ObservableMap;
import javafx.collections.ObservableSet;
import javafx.collections.SetChangeListener;
import lighthouse.LighthouseBackend;
import lighthouse.files.AppDirectory;
import lighthouse.files.DiskManager;
import lighthouse.protocol.Ex;
import lighthouse.protocol.LHProtos;
import lighthouse.protocol.Project;
import lighthouse.protocol.TestUtils;
import lighthouse.threading.AffinityExecutor;
import lighthouse.wallet.PledgingWallet;
import org.bitcoinj.core.*;
import org.bitcoinj.store.BlockStoreException;
import org.bitcoinj.testing.FakeTxBuilder;
import org.bitcoinj.testing.InboundMessageQueuer;
import org.bitcoinj.testing.TestWithPeerGroup;
import org.bitcoinj.utils.BriefLogFormatter;
import org.javatuples.Triplet;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.spongycastle.crypto.params.ECPrivateKeyParameters;
import org.spongycastle.crypto.signers.ECDSASigner;

import javax.annotation.Nullable;
import java.io.IOException;
import java.io.OutputStream;
import java.math.BigInteger;
import java.net.InetSocketAddress;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Instant;
import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

import static java.net.HttpURLConnection.HTTP_OK;
import static lighthouse.LighthouseBackend.Mode.CLIENT;
import static lighthouse.LighthouseBackend.Mode.SERVER;
import static lighthouse.protocol.LHUtils.*;
import static org.bitcoinj.testing.FakeTxBuilder.createFakeBlock;
import static org.junit.Assert.*;

public class LighthouseBackendTest extends TestWithPeerGroup {
    private LighthouseBackend backend;
    private Path tmpDir;
    private AffinityExecutor.Gate gate;
    private Project project;
    private LinkedBlockingQueue<HttpExchange> httpReqs;
    private ProjectModel projectModel;
    private HttpServer localServer;
    private Address to;
    private VersionMessage supportingVer;
    private PledgingWallet pledgingWallet;
    private AffinityExecutor.ServiceAffinityExecutor executor;
    private DiskManager diskManager;

    private LHProtos.Pledge injectedPledge;

    public LighthouseBackendTest() {
        super(ClientType.BLOCKING_CLIENT_MANAGER);
    }

    @Override
    @Before
    public void setUp() throws Exception {
        pledgingWallet = new PledgingWallet(params) {
            @Nullable
            @Override
            public LHProtos.Pledge getPledgeFor(Project project) {
                if (injectedPledge != null) {
                    return injectedPledge;
                } else {
                    return super.getPledgeFor(project);
                }
            }

            @Override
            public Set<LHProtos.Pledge> getPledges() {
                if (injectedPledge != null)
                    return ImmutableSet.<LHProtos.Pledge>builder().addAll(super.getPledges()).add(injectedPledge).build();
                else
                    return super.getPledges();
            }
        };
        wallet = pledgingWallet;
        super.setUp();
        BriefLogFormatter.init();

        tmpDir = Files.createTempDirectory("lighthouse-dmtest");
        AppDirectory.overrideAppDir(tmpDir);
        AppDirectory.initAppDir("lhtests");

        // Give data backend its own thread. The "gate" lets us just run commands in the context of the unit test thread.
        Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
            e.printStackTrace();
            fail("Uncaught exception");
        });
        gate = new AffinityExecutor.Gate();
        executor = new AffinityExecutor.ServiceAffinityExecutor("test thread");
        diskManager = new DiskManager(executor);
        backend = new LighthouseBackend(CLIENT, peerGroup, blockChain, pledgingWallet, diskManager, executor);
        backend.setMinPeersForUTXOQuery(1);
        backend.setMaxJitterSeconds(0);

        // Wait to start up.
        backend.executor.fetchFrom(() -> null);

        projectModel = new ProjectModel(pledgingWallet);
        to = new ECKey().toAddress(params);
        projectModel.address.set(to.toString());
        projectModel.title.set("Foo");
        projectModel.memo.set("Bar");
        projectModel.goalAmount.set(Coin.COIN.value);
        project = projectModel.getProject();

        supportingVer = new VersionMessage(params, 1);
        supportingVer.localServices = VersionMessage.NODE_NETWORK | VersionMessage.NODE_GETUTXOS;
        supportingVer.clientVersion = GetUTXOsMessage.MIN_PROTOCOL_VERSION;

        httpReqs = new LinkedBlockingQueue<>();
        localServer = HttpServer.create(new InetSocketAddress("localhost", HTTP_LOCAL_TEST_PORT), 100);
        localServer.createContext(HTTP_PATH_PREFIX, exchange -> {
            gate.checkOnThread();
            Uninterruptibles.putUninterruptibly(httpReqs, exchange);
        });
        localServer.setExecutor(gate);
        localServer.start();

        // Make peers selected for tx broadcast deterministic.
        TransactionBroadcast.random = new Random(1);
    }

    @After
    public void tearDown() {
        super.tearDown();
        executor.service.shutdown();
        localServer.stop(Integer.MAX_VALUE);
    }

    private void sendServerStatus(HttpExchange exchange, LHProtos.Pledge... scrubbedPledges) throws IOException {
        LHProtos.ProjectStatus.Builder status = LHProtos.ProjectStatus.newBuilder();
        status.setId(project.getID());
        status.setTimestamp(Instant.now().getEpochSecond());
        status.setValuePledgedSoFar(Coin.COIN.value);
        for (LHProtos.Pledge pledge : scrubbedPledges) {
            status.addPledges(pledge);
        }
        byte[] bits = status.build().toByteArray();
        exchange.sendResponseHeaders(HTTP_OK, bits.length);
        exchange.getResponseBody().write(bits);
        exchange.close();
    }

    private LHProtos.Pledge makeScrubbedPledge() {
        final LHProtos.Pledge pledge = LHProtos.Pledge.newBuilder()
                .setTotalInputValue(Coin.COIN.value)
                .setProjectId(project.getID())
                .setTimestamp(Utils.currentTimeSeconds())
                .addTransactions(ByteString.copyFromUtf8("not a real tx"))
                .setPledgeDetails(LHProtos.PledgeDetails.newBuilder().build())
                .build();
        final Sha256Hash origHash = Sha256Hash.create(pledge.toByteArray());
        return pledge.toBuilder()
                .clearTransactions()
                .setOrigHash(ByteString.copyFrom(origHash.getBytes()))
                .build();
    }

    private Path writeProjectToDisk() throws IOException {
        return writeProjectToDisk(AppDirectory.dir());
    }

    private Path writeProjectToDisk(Path dir) throws IOException {
        Path file = dir.resolve("test-project" + DiskManager.PROJECT_FILE_EXTENSION);
        try (OutputStream stream = Files.newOutputStream(file)) {
            project.getProto().writeTo(stream);
        }
        // Backend should now notice the new project in the app dir.
        return file;
    }

    @Test
    public void projectAddedWithServer() throws Exception {
        // Check that if we add a path containing a project, it's noticed and the projects set is updated.
        // Also check that the status is queried from the HTTP server it's linked to.

        projectModel.serverName.set("localhost");
        project = projectModel.getProject();
        final LHProtos.Pledge scrubbedPledge = makeScrubbedPledge();

        ObservableList<Project> projects = backend.mirrorProjects(gate);
        assertEquals(0, projects.size());
        writeProjectToDisk();
        assertEquals(0, projects.size());
        gate.waitAndRun();
        // Is now loaded from disk.
        assertEquals(1, projects.size());
        final Project project1 = projects.iterator().next();
        assertEquals("Foo", project1.getTitle());

        // Let's watch out for pledges from the server.
        ObservableSet<LHProtos.Pledge> pledges = backend.mirrorOpenPledges(project1, gate);

        // HTTP request was made to server to learn about existing pledges.
        gate.waitAndRun();
        HttpExchange exchange = httpReqs.take();
        sendServerStatus(exchange, scrubbedPledge);

        // We got a pledge list update relayed into our thread.
        pledges.addListener((SetChangeListener<LHProtos.Pledge>) c -> {
            assertTrue(c.wasAdded());
        });
        gate.waitAndRun();
        assertEquals(1, pledges.size());
        assertEquals(Coin.COIN.value, pledges.iterator().next().getTotalInputValue());
    }

    @Test
    public void projectCreated() throws Exception {
        // Check that if we save a project, we get a set change mirrored back into our own thread and the file is
        // stored to disk correctly.
        ObservableList<Project> projects = backend.mirrorProjects(gate);
        assertEquals(0, projects.size());
        backend.saveProject(project);
        assertEquals(0, projects.size());
        gate.waitAndRun();
        assertEquals(1, projects.size());
        assertEquals("Foo", projects.iterator().next().getTitle());
        assertTrue(Files.exists(tmpDir.resolve("Foo" + DiskManager.PROJECT_FILE_EXTENSION)));
    }

    @Test
    public void serverCheckStatus() throws Exception {
        // Check that the server status map is updated correctly.
        projectModel.serverName.set("localhost");
        project = projectModel.getProject();
        final LHProtos.Pledge scrubbedPledge = makeScrubbedPledge();
        ObservableMap<Project, LighthouseBackend.CheckStatus> statuses = backend.mirrorCheckStatuses(gate);
        assertEquals(0, statuses.size());
        writeProjectToDisk();
        gate.waitAndRun();
        // Is now loaded from disk.
        assertEquals(1, statuses.size());
        assertNotNull(statuses.get(project));
        assertTrue(statuses.get(project).inProgress);
        assertNull(statuses.get(project).error);
        // Doing request to server.
        gate.waitAndRun();
        HttpExchange exchange = httpReqs.take();
        exchange.sendResponseHeaders(404, -1);   // not found!
        gate.waitAndRun();
        // Error shows up in map.
        assertEquals(1, statuses.size());
        assertFalse(statuses.get(project).inProgress);
        final Throwable error = statuses.get(project).error;
        assertNotNull(error);
        assertEquals(java.io.FileNotFoundException.class, error.getClass());
        // Try again ...
        backend.refreshProjectStatusFromServer(project);
        gate.waitAndRun();
        assertEquals(1, statuses.size());
        assertTrue(statuses.get(project).inProgress);
        gate.waitAndRun();
        exchange = httpReqs.take();
        sendServerStatus(exchange, scrubbedPledge);
        gate.waitAndRun();
        assertEquals(0, statuses.size());
    }

    @Test
    public void serverAndLocalAreDeduped() throws Exception {
        // Verify that if the backend knows about a pledge, and receives the same pledge back in scrubbed form,
        // it knows they are the same and doesn't duplicate.
        projectModel.serverName.set("localhost");
        project = projectModel.getProject();
        final LHProtos.Pledge pledge = LHProtos.Pledge.newBuilder()
                .setTotalInputValue(Coin.COIN.value)
                .setProjectId(project.getID())
                .setTimestamp(Utils.currentTimeSeconds())
                .addTransactions(ByteString.copyFromUtf8("not a real tx"))
                .setPledgeDetails(LHProtos.PledgeDetails.newBuilder().build())
                .build();
        final Sha256Hash origHash = Sha256Hash.create(pledge.toByteArray());
        final LHProtos.Pledge scrubbedPledge = pledge.toBuilder()
                .clearTransactions()
                .setOrigHash(ByteString.copyFrom(origHash.getBytes()))
                .build();

        // Make the wallet return the above pledge without having to screw around with actually using the wallet.
        injectedPledge = pledge;
        backend.shutdown();
        executor.service.shutdown();
        executor.service.awaitTermination(5, TimeUnit.SECONDS);
        executor = new AffinityExecutor.ServiceAffinityExecutor("test thread 2");
        diskManager = new DiskManager(executor);
        writeProjectToDisk();
        backend = new LighthouseBackend(CLIENT, peerGroup, blockChain, pledgingWallet, diskManager, executor);

        // Let's watch out for pledges from the server.
        ObservableSet<LHProtos.Pledge> pledges = backend.mirrorOpenPledges(project, gate);
        assertEquals(1, pledges.size());
        assertEquals(pledge, pledges.iterator().next());

        // HTTP request was made to server to learn about existing pledges.
        gate.waitAndRun();
        sendServerStatus(httpReqs.take(), scrubbedPledge);

        // Because we want to test the absence of action in an async process, we forcibly repeat the server lookup
        // that just occurred so we can wait for it, and be sure that the scrubbed version of our own pledge was not
        // mistakenly added. Attempting to just test here without waiting would race, as the backend is processing
        // the reply we have above in parallel.
        CompletableFuture future = backend.refreshProjectStatusFromServer(project);
        gate.waitAndRun();
        sendServerStatus(httpReqs.take(), scrubbedPledge);
        future.get();
        assertEquals(0, gate.getTaskQueueSize());    // No pending set changes now.
        assertEquals(1, pledges.size());
        assertEquals(pledge, pledges.iterator().next());
    }

    @Test
    public void projectAddedP2P() throws Exception {
        peerGroup.startAsync();
        peerGroup.awaitRunning();

        // Check that if we add an path containing a project, it's noticed and the projects set is updated.
        // Also check that pledges are loaded from disk and checked against the P2P network.
        ObservableList<Project> projects = backend.mirrorProjects(gate);

        Path dropDir = Files.createTempDirectory("lh-droptest");
        Path downloadedFile = writeProjectToDisk(dropDir);
        backend.importProjectFrom(downloadedFile);

        gate.waitAndRun();
        // Is now loaded from disk.
        assertEquals(1, projects.size());

        // P2P getutxo message was used to find out if the pledge was already revoked.
        Triplet<Transaction, Transaction, LHProtos.Pledge> data = TestUtils.makePledge(project, to, project.getGoalAmount());
        Transaction stubTx = data.getValue0();
        Transaction pledgeTx = data.getValue1();
        LHProtos.Pledge pledge = data.getValue2();

        // Let's watch out for pledges as they are loaded from disk and checked.
        ObservableSet<LHProtos.Pledge> pledges = backend.mirrorOpenPledges(project, gate);

        // The user drops the pledge.
        try (OutputStream stream = Files.newOutputStream(dropDir.resolve("dropped-pledge" + DiskManager.PLEDGE_FILE_EXTENSION))) {
            pledge.writeTo(stream);
        }

        // App finds a peer that supports getutxo.
        InboundMessageQueuer p1 = connectPeer(1);
        assertNull(outbound(p1));
        InboundMessageQueuer p2 = connectPeer(2, supportingVer);
        GetUTXOsMessage getutxos = (GetUTXOsMessage) waitForOutbound(p2);
        assertNotNull(getutxos);
        assertEquals(pledgeTx.getInput(0).getOutpoint(), getutxos.getOutPoints().get(0));

        // We reply with the data it expects.
        inbound(p2, new UTXOsMessage(params,
                ImmutableList.of(stubTx.getOutput(0)),
                new long[]{UTXOsMessage.MEMPOOL_HEIGHT},
                blockStore.getChainHead().getHeader().getHash(),
                blockStore.getChainHead().getHeight()));

        // App sets a new Bloom filter so it finds out about revocations. Filter contains the outpoint of the stub.
        BloomFilter filter = (BloomFilter) waitForOutbound(p2);
        assertEquals(filter, waitForOutbound(p1));
        assertTrue(filter.contains(stubTx.getOutput(0).getOutPointFor().bitcoinSerialize()));
        assertFalse(filter.contains(pledgeTx.bitcoinSerialize()));
        assertFalse(filter.contains(pledgeTx.getHash().getBytes()));

        assertEquals(MemoryPoolMessage.class, waitForOutbound(p1).getClass());
        assertEquals(MemoryPoolMessage.class, waitForOutbound(p2).getClass());

        // We got a pledge list update relayed into our thread.
        AtomicBoolean flag = new AtomicBoolean(false);
        pledges.addListener((SetChangeListener<LHProtos.Pledge>) c -> {
            flag.set(c.wasAdded());
        });
        gate.waitAndRun();
        assertTrue(flag.get());
        assertEquals(1, pledges.size());
        final LHProtos.Pledge pledge2 = pledges.iterator().next();
        assertEquals(Coin.COIN.value / 2, pledge2.getTotalInputValue());

        // New block: let's pretend this block contains a revocation transaction. LighthouseBackend should recheck.
        Transaction revocation = new Transaction(params);
        revocation.addInput(stubTx.getOutput(0));
        revocation.addOutput(stubTx.getOutput(0).getValue(), new ECKey().toAddress(params));
        Block newBlock = FakeTxBuilder.makeSolvedTestBlock(blockChain.getChainHead().getHeader(), revocation);
        FilteredBlock filteredBlock = filter.applyAndUpdate(newBlock);
        inbound(p1, filteredBlock);
        for (Transaction transaction : filteredBlock.getAssociatedTransactions().values()) {
            inbound(p1, transaction);
        }
        inbound(p1, new Ping(123));   // Force processing of the filtered merkle block.

        gate.waitAndRun();
        assertEquals(0, pledges.size());   // was revoked

        peerGroup.stopAsync();
        peerGroup.awaitTerminated();
    }

    @Test
    public void mergePeerAnswers() throws Exception {
        // Check that we throw an exception if peers disagree on the state of the UTXO set. Such a pledge would
        // be considered invalid.
        peerGroup.startAsync();
        peerGroup.awaitRunning();

        InboundMessageQueuer p1 = connectPeer(1, supportingVer);
        InboundMessageQueuer p2 = connectPeer(2, supportingVer);
        InboundMessageQueuer p3 = connectPeer(3, supportingVer);

        // Set ourselves up to check a pledge.
        ObservableList<Project> projects = backend.mirrorProjects(gate);
        ObservableMap<Project, LighthouseBackend.CheckStatus> statuses = backend.mirrorCheckStatuses(gate);

        Path dropDir = Files.createTempDirectory("lh-droptest");
        Path downloadedFile = writeProjectToDisk(dropDir);

        backend.importProjectFrom(downloadedFile);

        gate.waitAndRun();
        assertEquals(1, projects.size());
        // This triggers a Bloom filter update so we can spot claims.
        checkBloomFilter(p1, p2, p3);

        Triplet<Transaction, Transaction, LHProtos.Pledge> data = TestUtils.makePledge(project, to, project.getGoalAmount());
        Transaction stubTx = data.getValue0();
        Transaction pledgeTx = data.getValue1();
        LHProtos.Pledge pledge = data.getValue2();
        try (OutputStream stream = Files.newOutputStream(dropDir.resolve("dropped-pledge" + DiskManager.PLEDGE_FILE_EXTENSION))) {
            pledge.writeTo(stream);
        }

        gate.waitAndRun();
        assertEquals(1, statuses.size());
        assertTrue(statuses.get(project).inProgress);

        // App finds a few peers that support getutxos and queries all of them.
        GetUTXOsMessage getutxos1, getutxos2, getutxos3;
        getutxos1 = (GetUTXOsMessage) waitForOutbound(p1);
        getutxos2 = (GetUTXOsMessage) waitForOutbound(p2);
        getutxos3 = (GetUTXOsMessage) waitForOutbound(p3);
        assertNotNull(getutxos1);
        assertNotNull(getutxos2);
        assertNotNull(getutxos3);
        assertEquals(getutxos1, getutxos2);
        assertEquals(getutxos2, getutxos3);
        assertEquals(pledgeTx.getInput(0).getOutpoint(), getutxos1.getOutPoints().get(0));

        // Two peers reply with the data it expects, one replies with a lie (claiming unspent when really spent).
        UTXOsMessage lie = new UTXOsMessage(params,
                ImmutableList.of(stubTx.getOutput(0)),
                new long[]{UTXOsMessage.MEMPOOL_HEIGHT},
                blockStore.getChainHead().getHeader().getHash(),
                blockStore.getChainHead().getHeight());
        UTXOsMessage correct = new UTXOsMessage(params,
                ImmutableList.of(),
                new long[]{},
                blockStore.getChainHead().getHeader().getHash(),
                blockStore.getChainHead().getHeight());
        inbound(p1, correct);
        inbound(p2, lie);
        inbound(p3, correct);

        gate.waitAndRun();
        assertEquals(1, statuses.size());
        assertFalse(statuses.get(project).inProgress);
        assertTrue(statuses.get(project).error instanceof Ex.InconsistentUTXOAnswers);
    }

    private BloomFilter checkBloomFilter(InboundMessageQueuer... peers) throws InterruptedException {
        BloomFilter result = null;
        for (InboundMessageQueuer peer : peers) {
            result = (BloomFilter) waitForOutbound(peer);
            assertTrue(waitForOutbound(peer) instanceof MemoryPoolMessage);
        }
        return result;
    }

    @Test
    public void pledgeAddedViaWallet() throws Exception {
        ObservableList<Project> projects = backend.mirrorProjects(gate);
        writeProjectToDisk();
        gate.waitAndRun();
        // Is now loaded from disk.
        assertEquals(1, projects.size());

        Transaction payment = FakeTxBuilder.createFakeTx(params, Coin.COIN, pledgingWallet.currentReceiveAddress());
        FakeTxBuilder.BlockPair bp = createFakeBlock(blockStore, payment);
        wallet.receiveFromBlock(payment, bp.storedBlock, AbstractBlockChain.NewBlockType.BEST_CHAIN, 0);
        wallet.notifyNewBestBlock(bp.storedBlock);
        PledgingWallet.PendingPledge pendingPledge = pledgingWallet.createPledge(project, Coin.COIN.value / 2, null);

        ObservableSet<LHProtos.Pledge> pledges = backend.mirrorOpenPledges(project, gate);
        assertEquals(0, pledges.size());
        LHProtos.Pledge proto = pendingPledge.commit(true);
        gate.waitAndRun();
        assertEquals(1, pledges.size());
        assertEquals(proto, pledges.iterator().next());
    }

    @Test
    public void submitPledgeViaHTTP() throws Exception {
        backend.shutdown();
        backend = new LighthouseBackend(SERVER, peerGroup, blockChain, pledgingWallet, diskManager, executor);
        backend.setMinPeersForUTXOQuery(1);
        backend.setMaxJitterSeconds(0);
        // Test the process of broadcasting a pledge's dependencies, then checking the UTXO set to see if it was
        // revoked already. If all is OK then it should show up in the verified pledges set.
        peerGroup.setMinBroadcastConnections(2);
        peerGroup.startAsync();
        peerGroup.awaitRunning();

        Triplet<Transaction, Transaction, LHProtos.Pledge> data = TestUtils.makePledge(project, to, project.getGoalAmount());
        Transaction stubTx = data.getValue0();
        Transaction pledgeTx = data.getValue1();
        LHProtos.Pledge pledge = data.getValue2();

        writeProjectToDisk();

        ObservableSet<LHProtos.Pledge> pledges = backend.mirrorOpenPledges(project, gate);

        // The dependency TX doesn't really have to be a dependency at the moment, it could be anything so we lazily
        // just make an unrelated fake tx to check the ordering of things.
        Transaction depTx = FakeTxBuilder.createFakeTx(params, Coin.COIN, address);
        pledge = pledge.toBuilder().setTransactions(0, ByteString.copyFrom(depTx.bitcoinSerialize()))
                                   .addTransactions(ByteString.copyFrom(pledgeTx.bitcoinSerialize()))
                                   .build();

        InboundMessageQueuer p1 = connectPeer(1);
        InboundMessageQueuer p2 = connectPeer(2, supportingVer);

        // Pledge is submitted to the server via HTTP.
        CompletableFuture<LHProtos.Pledge> future = backend.submitPledge(project, pledge);
        assertFalse(future.isDone());

        // Broadcast happens.
        Transaction broadcast = (Transaction) waitForOutbound(p1);
        assertEquals(depTx, broadcast);
        assertNull(outbound(p2));
        InventoryMessage inv = new InventoryMessage(params);
        inv.addTransaction(depTx);
        inbound(p2, inv);
        // Broadcast is now complete, so query.
        GetUTXOsMessage getutxos = (GetUTXOsMessage) waitForOutbound(p2);
        assertNotNull(getutxos);
        assertEquals(pledgeTx.getInput(0).getOutpoint(), getutxos.getOutPoints().get(0));

        // We reply with the data it expects.
        inbound(p2, new UTXOsMessage(params,
                ImmutableList.of(stubTx.getOutput(0)),
                new long[]{UTXOsMessage.MEMPOOL_HEIGHT},
                blockStore.getChainHead().getHeader().getHash(),
                blockStore.getChainHead().getHeight()));

        // We got a pledge list update relayed into our thread.
        AtomicBoolean flag = new AtomicBoolean(false);
        pledges.addListener((SetChangeListener<LHProtos.Pledge>) c -> {
            flag.set(c.wasAdded());
        });
        gate.waitAndRun();
        assertTrue(flag.get());
        assertEquals(1, pledges.size());
        final LHProtos.Pledge pledge2 = pledges.iterator().next();
        assertEquals(Coin.COIN.value / 2, pledge2.getTotalInputValue());

        future.get();

        // And the pledge was saved to disk named after the hash of the pledge contents.
        final Sha256Hash pledgeHash = Sha256Hash.create(pledge.toByteArray());
        final List<Path> dirFiles = mapList(listDir(AppDirectory.dir()), Path::getFileName);
        assertTrue(dirFiles.contains(Paths.get(pledgeHash.toString() + DiskManager.PLEDGE_FILE_EXTENSION)));

        peerGroup.stopAsync();
        peerGroup.awaitTerminated();
    }

    @Test
    public void claimServerless() throws Exception {
        // Create enough pledges to satisfy the project, broadcast the claim transaction, make sure the backend
        // spots the claim and understands the current state of the project.
        peerGroup.setMinBroadcastConnections(2);
        peerGroup.setDownloadTxDependencies(false);
        peerGroup.startAsync();
        peerGroup.awaitRunning();

        Path dropDir = Files.createTempDirectory("lh-droptest");
        Path downloadedFile = writeProjectToDisk(dropDir);

        backend.importProjectFrom(downloadedFile);

        ObservableSet<LHProtos.Pledge> openPledges = backend.mirrorOpenPledges(project, gate);
        ObservableSet<LHProtos.Pledge> claimedPledges = backend.mirrorClaimedPledges(project, gate);
        assertEquals(0, claimedPledges.size());
        assertEquals(0, openPledges.size());

        Triplet<Transaction, Transaction, LHProtos.Pledge> data = TestUtils.makePledge(project, to, project.getGoalAmount());
        LHProtos.Pledge pledge1 = data.getValue2();

        Triplet<Transaction, Transaction, LHProtos.Pledge> data2 = TestUtils.makePledge(project, to, project.getGoalAmount());
        LHProtos.Pledge pledge2 = data2.getValue2();

        InboundMessageQueuer p1 = connectPeer(1);
        InboundMessageQueuer p2 = connectPeer(2, supportingVer);

        // The user drops the pledges.
        try (OutputStream stream = Files.newOutputStream(dropDir.resolve("dropped-pledge1" + DiskManager.PLEDGE_FILE_EXTENSION))) {
            pledge1.writeTo(stream);
        }
        try (OutputStream stream = Files.newOutputStream(dropDir.resolve("dropped-pledge2" + DiskManager.PLEDGE_FILE_EXTENSION))) {
            pledge2.writeTo(stream);
        }

        int q = 0;
        for (int i = 0; i < 6; i++) {
            Message m = waitForOutbound(p2);
            if (m instanceof GetUTXOsMessage) {
                doGetUTXOAnswer(q++ == 0 ? data.getValue0().getOutput(0) : data2.getValue0().getOutput(0), p2);
            }
        }

        gate.waitAndRun();
        gate.waitAndRun();
        assertEquals(2, openPledges.size());

        ObservableMap<String, LighthouseBackend.ProjectStateInfo> states = backend.mirrorProjectStates(gate);
        assertEquals(LighthouseBackend.ProjectState.OPEN, states.get(project.getID()).state);

        Transaction contract = project.completeContract(ImmutableSet.of(pledge1, pledge2));
        inbound(p1, InventoryMessage.with(contract));
        waitForOutbound(p1);   // getdata for the contract.
        inbound(p2, InventoryMessage.with(contract));
        inbound(p1, contract);
        for (int i = 0; i < 2; i++) {
            Message m = waitForOutbound(p1);
            assertTrue(m instanceof MemoryPoolMessage || m instanceof BloomFilter);
        }

        for (int i = 0; i < 5; i++) {
            gate.waitAndRun();   // updates to lists and things.
        }

        assertEquals(LighthouseBackend.ProjectState.CLAIMED, states.get(project.getID()).state);
        assertEquals(contract.getHash(), states.get(project.getID()).claimedBy);
        assertTrue(Files.exists(AppDirectory.dir().resolve(DiskManager.PROJECT_STATUS_FILENAME)));

        assertEquals(2, claimedPledges.size());
        assertTrue(claimedPledges.contains(pledge1));
        assertTrue(claimedPledges.contains(pledge2));

        // TODO: Craft a test that verifies double spending of the claim is handled properly.

        peerGroup.stopAsync();
        peerGroup.awaitTerminated();
    }

    @Test
    public void duplicatePledgesNotAllowed() throws Exception {
        // Pledges should not share outputs, otherwise someone could pledge the same money twice either by accident
        // or maliciously. Note that in the case where two pledges have two different dependencies that both double
        // spend the same output, this will be caught by the backend trying to broadcast the dependencies itself
        // (in the server case), and then observing that the second pledge has a dependency that's failing to propagate.
        Path dropDir = Files.createTempDirectory("lh-droptest");
        Path downloadedFile = writeProjectToDisk(dropDir);

        backend.importProjectFrom(downloadedFile);
        ObservableSet<LHProtos.Pledge> openPledges = backend.mirrorOpenPledges(project, gate);

        peerGroup.setMinBroadcastConnections(2);
        peerGroup.startAsync();
        peerGroup.awaitRunning();

        Transaction doubleSpentTx = new Transaction(params);
        doubleSpentTx.addInput(TestUtils.makeRandomInput());

        // Make a key that doesn't use deterministic signing, to make it easy for us to double spend with bitwise
        // different pledges.
        ECKey signingKey = new ECKey() {
            @Override
            protected ECDSASignature doSign(Sha256Hash input, BigInteger privateKeyForSigning) {
                ECDSASigner signer = new ECDSASigner();
                ECPrivateKeyParameters privKey = new ECPrivateKeyParameters(privateKeyForSigning, CURVE);
                signer.init(true, privKey);
                BigInteger[] components = signer.generateSignature(input.getBytes());
                return new ECDSASignature(components[0], components[1]).toCanonicalised();
            }
        };
        TransactionOutput output = doubleSpentTx.addOutput(Coin.COIN.divide(2), signingKey.toAddress(params));

        LHProtos.Pledge.Builder pledge1 = makeSimpleHalfPledge(signingKey, output);
        LHProtos.Pledge.Builder pledge2 = makeSimpleHalfPledge(signingKey, output);
        assertNotEquals(pledge1.getTransactions(0), pledge2.getTransactions(0));

        ObservableMap<Project, LighthouseBackend.CheckStatus> statuses = backend.mirrorCheckStatuses(gate);

        InboundMessageQueuer p1 = connectPeer(1, supportingVer);
        InboundMessageQueuer p2 = connectPeer(2, supportingVer);
        // User drops pledge 1
        try (OutputStream stream = Files.newOutputStream(dropDir.resolve("dropped-pledge1" + DiskManager.PLEDGE_FILE_EXTENSION))) {
            pledge1.build().writeTo(stream);
        }
        for (int i = 0; i < 3; i++) {
            Message m = waitForOutbound(p1);
            if (m instanceof GetUTXOsMessage)
                doGetUTXOAnswer(output, p1);
            m = waitForOutbound(p2);
            if (m instanceof GetUTXOsMessage)
                doGetUTXOAnswer(output, p2);
        }
        gate.waitAndRun();   // statuses (start lookup)
        gate.waitAndRun();   // openPledges
        gate.waitAndRun();   // statuses (end lookup)

        // First pledge is accepted.
        assertEquals(1, openPledges.size());
        assertEquals(pledge1.build(), openPledges.iterator().next());

        // User drops pledge 2
        try (OutputStream stream = Files.newOutputStream(dropDir.resolve("dropped-pledge2" + DiskManager.PLEDGE_FILE_EXTENSION))) {
            pledge2.build().writeTo(stream);
        }

        for (int i = 0; i < 3; i++) {
            Message m = waitForOutbound(p1);
            if (m instanceof GetUTXOsMessage)
                doGetUTXOAnswer(output, p1);
            m = waitForOutbound(p2);
            if (m instanceof GetUTXOsMessage)
                doGetUTXOAnswer(output, p2);
        }

        // Wait for check status to update.
        gate.waitAndRun();   // statuses (start lookup)
        gate.waitAndRun();   // statuses (error result)
        //noinspection ConstantConditions
        assertEquals(VerificationException.DuplicatedOutPoint.class, statuses.get(project).error.getClass());

        peerGroup.stopAsync();
        peerGroup.awaitTerminated();
    }

    @Test
    public void serverPledgeSync() throws Exception {
        // Test that the client backend stays in sync with the server as pledges are added and revoked.
        projectModel.serverName.set("localhost");
        project = projectModel.getProject();
        ObservableSet<LHProtos.Pledge> openPledges = backend.mirrorOpenPledges(project, gate);
        final LHProtos.Pledge scrubbedPledge = makeScrubbedPledge();
        writeProjectToDisk();
        gate.waitAndRun();
        // Is now loaded from disk and doing request to server.
        HttpExchange exchange = httpReqs.take();
        sendServerStatus(exchange, scrubbedPledge);
        gate.waitAndRun();
        assertEquals(1, openPledges.size());
        // Pledge gets revoked.
        backend.refreshProjectStatusFromServer(project);
        gate.waitAndRun();
        sendServerStatus(httpReqs.take());
        gate.waitAndRun();
        assertEquals(0, openPledges.size());
    }

    private LHProtos.Pledge.Builder makeSimpleHalfPledge(ECKey signingKey, TransactionOutput output) {
        LHProtos.Pledge.Builder pledge = LHProtos.Pledge.newBuilder();
        Transaction tx = new Transaction(params);
        tx.addOutput(project.getOutputs().get(0));   // Project output.
        tx.addSignedInput(output, signingKey, Transaction.SigHash.ALL, true);
        pledge.addTransactions(ByteString.copyFrom(tx.bitcoinSerialize()));
        pledge.setTotalInputValue(Coin.COIN.divide(2).value);
        pledge.setProjectId(project.getID());
        pledge.setTimestamp(Utils.currentTimeSeconds());
        pledge.getPledgeDetailsBuilder();
        return pledge;
    }

    private void doGetUTXOAnswer(TransactionOutput output, InboundMessageQueuer p) throws InterruptedException, BlockStoreException {
        inbound(p, new UTXOsMessage(params,
                ImmutableList.of(output),
                new long[]{UTXOsMessage.MEMPOOL_HEIGHT},
                blockStore.getChainHead().getHeader().getHash(),
                blockStore.getChainHead().getHeight()));
    }
}
TOP

Related Classes of lighthouse.model.LighthouseBackendTest

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.