package lighthouse.model;
import com.google.common.base.*;
import com.google.common.collect.*;
import com.google.common.util.concurrent.*;
import com.google.protobuf.*;
import com.sun.net.httpserver.*;
import javafx.collections.*;
import lighthouse.*;
import lighthouse.files.*;
import lighthouse.protocol.*;
import lighthouse.threading.*;
import lighthouse.wallet.*;
import org.bitcoinj.core.*;
import org.bitcoinj.core.Message;
import org.bitcoinj.store.*;
import org.bitcoinj.testing.*;
import org.bitcoinj.utils.*;
import org.javatuples.*;
import org.jetbrains.annotations.*;
import org.junit.*;
import org.spongycastle.crypto.params.*;
import org.spongycastle.crypto.signers.*;
import javax.annotation.Nullable;
import java.io.*;
import java.math.*;
import java.net.*;
import java.nio.file.*;
import java.time.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
import static java.net.HttpURLConnection.*;
import static lighthouse.LighthouseBackend.Mode.*;
import static lighthouse.protocol.LHUtils.*;
import static org.bitcoinj.testing.FakeTxBuilder.*;
import static org.junit.Assert.*;
public class LighthouseBackendTest extends TestWithPeerGroup {
private LighthouseBackend backend;
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 LHProtos.Pledge injectedPledge;
private Path tmpDir, appDir;
private IBitcoinBackend mockBitcoinBackend;
public LighthouseBackendTest() {
super(ClientType.BLOCKING_CLIENT_MANAGER);
}
@Override
@Before
public void setUp() throws Exception {
Context context = new Context(params);
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();
peerGroup.start();
BriefLogFormatter.init();
tmpDir = Files.createTempDirectory("lighthouse-dmtest");
appDir = Files.createDirectory(tmpDir.resolve("appDir"));
AppDirectory.overrideAppDir(appDir);
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");
});
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 -> {
Uninterruptibles.putUninterruptibly(httpReqs, exchange);
});
localServer.start();
// Make peers selected for tx broadcast deterministic.
TransactionBroadcast.random = new Random(1);
}
public void initCoreState(LighthouseBackend.Mode client) {
gate = new AffinityExecutor.Gate();
executor = new AffinityExecutor.ServiceAffinityExecutor("test thread");
mockBitcoinBackend = new IBitcoinBackend() {
@NotNull
@Override
public PeerGroup getXtPeers() {
return peerGroup;
}
@NotNull @Override
public PeerGroup getPeers() {
return peerGroup;
}
@NotNull @Override
public BlockChain getChain() {
return blockChain;
}
@NotNull @Override
public BlockStore getStore() {
return blockStore;
}
@NotNull @Override
public PledgingWallet getWallet() {
return pledgingWallet;
}
};
backend = new LighthouseBackend(client, params, mockBitcoinBackend, executor);
backend.setMinPeersForUTXOQuery(1);
backend.setMaxJitterSeconds(0);
backend.start();
// Wait to start up.
backend.getExecutor().fetchFrom(() -> null);
}
@After
public void tearDown() {
super.tearDown();
backend.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(Coin pledgedCoin) {
final LHProtos.Pledge pledge = LHProtos.Pledge.newBuilder()
.addTransactions(ByteString.copyFromUtf8("not a real tx"))
.setPledgeDetails(LHProtos.PledgeDetails.newBuilder()
.setTotalInputValue(pledgedCoin.value)
.setProjectId(project.getID())
.setTimestamp(Utils.currentTimeSeconds())
.build())
.build();
final Sha256Hash origHash = Sha256Hash.of(pledge.toByteArray());
LHProtos.Pledge.Builder builder = pledge.toBuilder().clearTransactions();
builder.getPledgeDetailsBuilder().setOrigHash(ByteString.copyFrom(origHash.getBytes()));
return builder.build();
}
private Path writeProjectToDisk() throws IOException {
Path path = writeProjectToDisk(tmpDir);
backend.importProjectFrom(path);
return path;
}
private Path writeProjectToDisk(Path dir) throws IOException {
Path file = dir.resolve("test-project" + LighthouseBackend.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.
initCoreState(CLIENT);
projectModel.serverName.set("localhost");
project = projectModel.getProject();
final LHProtos.Pledge scrubbedPledge = makeScrubbedPledge(Coin.COIN);
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.
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().getPledgeDetails().getTotalInputValue());
}
@Test
public void projectCreated() throws Exception {
initCoreState(CLIENT);
// 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(appDir.resolve("Foo" + LighthouseBackend.PROJECT_FILE_EXTENSION)));
}
@Test
public void serverCheckStatus() throws Exception {
initCoreState(CLIENT);
// Check that the server status map is updated correctly.
projectModel.serverName.set("localhost");
project = projectModel.getProject();
final LHProtos.Pledge scrubbedPledge = makeScrubbedPledge(Coin.COIN);
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).getInProgress());
assertNull(statuses.get(project).getError());
// Doing request to server.
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).getInProgress());
final Throwable error = statuses.get(project).getError();
assertNotNull(error);
assertEquals(java.io.FileNotFoundException.class, error.getClass());
// Try again ...
backend.refreshProjectStatusFromServer(project);
gate.waitAndRun();
assertEquals(1, statuses.size());
assertTrue(statuses.get(project).getInProgress());
exchange = httpReqs.take();
sendServerStatus(exchange, scrubbedPledge);
gate.waitAndRun();
assertEquals(0, statuses.size());
}
@Test
public void serverAndLocalAreDeduped() throws Exception {
initCoreState(CLIENT);
// 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();
Transaction tx = FakeTxBuilder.createFakeTx(params, Coin.COIN, new ECKey());
final LHProtos.Pledge pledge = LHProtos.Pledge.newBuilder()
.addTransactions(ByteString.copyFrom(tx.bitcoinSerialize()))
.setPledgeDetails(LHProtos.PledgeDetails.newBuilder()
.setTotalInputValue(Coin.COIN.value)
.setProjectId(project.getID())
.setTimestamp(Utils.currentTimeSeconds())
.build())
.build();
final Sha256Hash origHash = Sha256Hash.of(pledge.toByteArray());
final LHProtos.Pledge.Builder scrubbedPledgeBuilder = pledge.toBuilder().clearTransactions();
scrubbedPledgeBuilder.getPledgeDetailsBuilder().setOrigHash(ByteString.copyFrom(origHash.getBytes()));
final LHProtos.Pledge scrubbedPledge = scrubbedPledgeBuilder.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");
writeProjectToDisk();
backend = new LighthouseBackend(CLIENT, params, mockBitcoinBackend, executor);
backend.start();
// 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.
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);
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 {
initCoreState(CLIENT);
// 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 imports the pledge
writePledgeToDisk(dropDir, pledge);
// 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.getPledgeDetails().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
}
@Test
public void mergePeerAnswers() throws Exception {
initCoreState(CLIENT);
// Check that we throw an exception if peers disagree on the state of the UTXO set. Such a pledge would
// be considered invalid.
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();
writePledgeToDisk(dropDir, pledge);
gate.waitAndRun();
assertEquals(1, statuses.size());
assertTrue(statuses.get(project).getInProgress());
// 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).getInProgress());
assertTrue(statuses.get(project).getError() instanceof Ex.InconsistentUTXOAnswers);
}
public void writePledgeToDisk(Path dropDir, LHProtos.Pledge pledge) throws IOException {
Path path = dropDir.resolve("dropped-pledge" + LighthouseBackend.PLEDGE_FILE_EXTENSION);
try (OutputStream stream = Files.newOutputStream(path)) {
pledge.writeTo(stream);
}
backend.importPledgeFrom(path);
}
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 {
initCoreState(CLIENT);
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, 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());
// The pledge is saved to disk where the backend can see it. Nothing should happen because the pledge is known
// already and the change listener ignores it.
writePledgeToDisk(AppDirectory.dir(), proto);
// Now restart the backend so it doesn't see changes anymore but fresh state: we still don't recheck the pledge.
initCoreState(CLIENT);
ObservableMap<Project, LighthouseBackend.CheckStatus> statuses = backend.mirrorCheckStatuses(gate);
assertEquals(0, statuses.size());
}
@Test
public void submitPledgeViaHTTP() throws Exception {
initCoreState(SERVER);
// 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);
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, wallet.currentReceiveAddress());
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.
doGetUTXOAnswer(p2, stubTx.getOutput(0));
// 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.getPledgeDetails().getTotalInputValue());
future.get();
// And the pledge was saved to disk named after the hash of the pledge contents.
final Sha256Hash pledgeHash = Sha256Hash.of(pledge.toByteArray());
final List<Path> dirFiles = mapList(listDir(AppDirectory.dir()), Path::getFileName);
assertTrue(dirFiles.contains(Paths.get(pledgeHash + LighthouseBackend.PLEDGE_FILE_EXTENSION)));
}
@Test
public void submitPledgeViaHTTPWithError() throws Exception {
// Same as above but this time, we make the pledge too small to be accepted and verify that it doesn't work.
initCoreState(SERVER);
peerGroup.setMinBroadcastConnections(2);
Triplet<Transaction, Transaction, LHProtos.Pledge> data = TestUtils.makePledge(project, to, project.getGoalAmount(), project.getMinPledgeAmount().divide(2));
Transaction stubTx = data.getValue0();
Transaction pledgeTx = data.getValue1();
LHProtos.Pledge pledge = data.getValue2();
writeProjectToDisk();
// Wait for the project load to be finished, so things happen nicely in order.
backend.getExecutor().fetchFrom(() -> null);
InboundMessageQueuer p1 = connectPeer(1);
InboundMessageQueuer p2 = connectPeer(2, supportingVer);
// 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, wallet.currentReceiveAddress());
pledge = pledge.toBuilder().setTransactions(0, ByteString.copyFrom(depTx.bitcoinSerialize()))
.addTransactions(ByteString.copyFrom(pledgeTx.bitcoinSerialize()))
.build();
// 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.
doGetUTXOAnswer(p2, stubTx.getOutput(0));
// And now we expect it to notice that the pledge is bad.
try {
future.get();
fail();
} catch (ExecutionException e) {
assertTrue(e.toString(), Throwables.getRootCause(e) instanceof Ex.PledgeTooSmall);
}
// Pledge NOT saved to disk.
final Sha256Hash pledgeHash = Sha256Hash.of(pledge.toByteArray());
final List<Path> dirFiles = mapList(listDir(AppDirectory.dir()), Path::getFileName);
assertFalse(dirFiles.contains(Paths.get(pledgeHash + LighthouseBackend.PLEDGE_FILE_EXTENSION)));
}
@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.
initCoreState(CLIENT);
peerGroup.setMinBroadcastConnections(2);
peerGroup.setDownloadTxDependencies(false);
writeProjectToDisk();
ObservableSet<LHProtos.Pledge> pledges = backend.mirrorOpenPledges(project, gate);
assertEquals(0, pledges.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 imports the pledges.
Path pledgePath1 = tmpDir.resolve("dropped-pledge1" + LighthouseBackend.PLEDGE_FILE_EXTENSION);
try (OutputStream stream = Files.newOutputStream(pledgePath1)) {
pledge1.writeTo(stream);
}
Path pledgePath2 = tmpDir.resolve("dropped-pledge2" + LighthouseBackend.PLEDGE_FILE_EXTENSION);
try (OutputStream stream = Files.newOutputStream(pledgePath2)) {
pledge2.writeTo(stream);
}
backend.importPledgeFrom(pledgePath1);
backend.importPledgeFrom(pledgePath2);
for (int i = 0; i < 4; i++) {
Message m = waitForOutbound(p2);
if (m instanceof GetUTXOsMessage) {
// query order is not stable.
if (((GetUTXOsMessage)m).getOutPoints().get(0).equals(data.getValue0().getOutput(0).getOutPointFor()))
doGetUTXOAnswer(p2, data.getValue0().getOutput(0), data2.getValue0().getOutput(0));
else
doGetUTXOAnswer(p2, data2.getValue0().getOutput(0), data.getValue0().getOutput(0));
}
}
gate.waitAndRun();
gate.waitAndRun();
assertEquals(2, pledges.size());
ObservableMap<Sha256Hash, LighthouseBackend.ProjectStateInfo> states = backend.mirrorProjectStates(gate);
assertEquals(LighthouseBackend.ProjectState.OPEN, states.get(project.getIDHash()).getState());
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);
gate.waitAndRun();
assertEquals(LighthouseBackend.ProjectState.CLAIMED, states.get(project.getIDHash()).getState());
assertEquals(contract.getHash(), states.get(project.getIDHash()).getClaimedBy());
assertTrue(Files.exists(appDir.resolve(LighthouseBackend.PROJECT_STATUS_FILENAME)));
assertEquals(2, pledges.size());
assertTrue(pledges.contains(pledge1));
assertTrue(pledges.contains(pledge2));
// Now simulate a backend restart, and check that the pledges are still available despite being claimed.
backend.shutdown();
initCoreState(CLIENT);
pledges = backend.mirrorOpenPledges(project, gate);
assertEquals(2, pledges.size());
assertEquals(LighthouseBackend.ProjectState.CLAIMED, backend.mirrorProjectStates(gate).get(project.getIDHash()).getState());
// TODO: Craft a test that verifies double spending of the claim is handled properly.
}
@Test
public void duplicatePledgesNotAllowed() throws Exception {
initCoreState(CLIENT);
// 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);
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
Files.write(tmpDir.resolve("pledge1"), pledge1.build().toByteArray());
backend.importPledgeFrom(tmpDir.resolve("pledge1"));
for (int i = 0; i < 3; i++) {
Message m = waitForOutbound(p1);
if (m instanceof GetUTXOsMessage)
doGetUTXOAnswer(p1, output);
m = waitForOutbound(p2);
if (m instanceof GetUTXOsMessage)
doGetUTXOAnswer(p2, output);
}
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
Files.write(tmpDir.resolve("pledge2"), pledge2.build().toByteArray());
backend.importPledgeFrom(tmpDir.resolve("pledge2"));
Message m = waitForOutbound(p1);
if (m instanceof GetUTXOsMessage)
doGetUTXOAnswer(p1, output, output);
m = waitForOutbound(p2);
if (m instanceof GetUTXOsMessage)
doGetUTXOAnswer(p2, output, output);
// 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).getError().getClass());
}
@Test
public void serverPledgeSync() throws Exception {
initCoreState(CLIENT);
Utils.setMockClock();
// 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(Coin.COIN);
writeProjectToDisk();
// 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);
sendServerStatus(httpReqs.take());
gate.waitAndRun();
assertEquals(0, openPledges.size());
// New pledges are made.
Utils.rollMockClock(60);
LHProtos.Pledge scrubbedPledge2 = makeScrubbedPledge(Coin.COIN.divide(2));
Utils.rollMockClock(60);
LHProtos.Pledge scrubbedPledge3 = makeScrubbedPledge(Coin.COIN.divide(2));
backend.refreshProjectStatusFromServer(project);
sendServerStatus(httpReqs.take(), scrubbedPledge2, scrubbedPledge3);
gate.waitAndRun();
gate.waitAndRun();
assertEquals(2, openPledges.size());
// And now the project is claimed.
backend.refreshProjectStatusFromServer(project);
LHProtos.ProjectStatus.Builder status = LHProtos.ProjectStatus.newBuilder();
status.setId(project.getID());
status.setTimestamp(Instant.now().getEpochSecond());
status.setValuePledgedSoFar(Coin.COIN.value);
status.addPledges(scrubbedPledge2);
status.addPledges(scrubbedPledge3);
status.setClaimedBy(ByteString.copyFrom(Sha256Hash.ZERO_HASH.getBytes()));
byte[] bits = status.build().toByteArray();
exchange = httpReqs.take();
exchange.sendResponseHeaders(HTTP_OK, bits.length);
exchange.getResponseBody().write(bits);
exchange.close();
assertEquals(2, 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.getPledgeDetailsBuilder().setTotalInputValue(Coin.COIN.divide(2).value);
pledge.getPledgeDetailsBuilder().setProjectId(project.getID());
pledge.getPledgeDetailsBuilder().setTimestamp(Utils.currentTimeSeconds());
return pledge;
}
private void doGetUTXOAnswer(InboundMessageQueuer p, TransactionOutput... outputs) throws InterruptedException, BlockStoreException {
long[] heights = new long[outputs.length];
Arrays.fill(heights, UTXOsMessage.MEMPOOL_HEIGHT);
inbound(p, new UTXOsMessage(params,
Lists.newArrayList(outputs),
heights,
blockStore.getChainHead().getHeader().getHash(),
blockStore.getChainHead().getHeight()));
}
}