diff --git a/src/main/java/ch/dissem/bitmessage/server/JabitServerConfig.java b/src/main/java/ch/dissem/bitmessage/server/JabitServerConfig.java index b3b314e..205154c 100644 --- a/src/main/java/ch/dissem/bitmessage/server/JabitServerConfig.java +++ b/src/main/java/ch/dissem/bitmessage/server/JabitServerConfig.java @@ -19,12 +19,11 @@ package ch.dissem.bitmessage.server; import ch.dissem.bitmessage.BitmessageContext; import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.networking.DefaultNetworkHandler; -import ch.dissem.bitmessage.ports.MemoryNodeRegistry; -import ch.dissem.bitmessage.repository.JdbcAddressRepository; -import ch.dissem.bitmessage.repository.JdbcConfig; -import ch.dissem.bitmessage.repository.JdbcInventory; -import ch.dissem.bitmessage.repository.JdbcMessageRepository; +import ch.dissem.bitmessage.ports.*; +import ch.dissem.bitmessage.repository.*; import ch.dissem.bitmessage.security.bc.BouncySecurity; +import ch.dissem.bitmessage.server.repository.ServerProofOfWorkRepository; +import ch.dissem.bitmessage.utils.Singleton; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -47,17 +46,74 @@ public class JabitServerConfig { @Value("${bitmessage.connection.limit}") private int connectionLimit; + @Bean + public JdbcConfig jdbcConfig() { + return new JdbcConfig("jdbc:h2:file:./jabit;AUTO_SERVER=TRUE;DB_CLOSE_DELAY=10", "sa", null); + } + + @Bean + public AddressRepository addressRepo() { + return new JdbcAddressRepository(jdbcConfig()); + } + + @Bean + public Inventory inventory() { + return new JdbcInventory(jdbcConfig()); + } + + @Bean + public MessageRepository messageRepo() { + return new JdbcMessageRepository(jdbcConfig()); + } + + @Bean + public ProofOfWorkRepository proofOfWorkRepo() { + return new JdbcProofOfWorkRepository(jdbcConfig()); + } + + @Bean + public NodeRegistry nodeRegistry() { + return new MemoryNodeRegistry(); + } + + @Bean + public NetworkHandler networkHandler() { + return new DefaultNetworkHandler(); + } + + @Bean + public Security security() { + BouncySecurity security = new BouncySecurity(); + Singleton.initialize(security); // needed for admins and clients + return security; + } + + @Bean + public BitmessageContext.Listener serverListener() { + return new ServerListener(admins(), clients(), whitelist(), shortlist(), blacklist()); + } + + @Bean + public ServerProofOfWorkRepository serverProofOfWorkRepository() { + return new ServerProofOfWorkRepository(jdbcConfig()); + } + @Bean + public CustomCommandHandler commandHandler() { + return new ProofOfWorkRequestHandler(serverProofOfWorkRepository(), clients()); + } + @Bean public BitmessageContext bitmessageContext() { - JdbcConfig config = new JdbcConfig("jdbc:h2:file:./jabit;AUTO_SERVER=TRUE", "sa", null); return new BitmessageContext.Builder() - .addressRepo(new JdbcAddressRepository(config)) - .inventory(new JdbcInventory(config)) - .messageRepo(new JdbcMessageRepository(config)) - .nodeRegistry(new MemoryNodeRegistry()) - .networkHandler(new DefaultNetworkHandler()) - .listener(new ServerListener(admins(), clients(), whitelist(), shortlist(), blacklist())) - .security(new BouncySecurity()) + .addressRepo(addressRepo()) + .inventory(inventory()) + .messageRepo(messageRepo()) + .nodeRegistry(nodeRegistry()) + .powRepo(proofOfWorkRepo()) + .networkHandler(networkHandler()) + .listener(serverListener()) + .customCommandHandler(commandHandler()) + .security(security()) .port(port) .connectionLimit(connectionLimit) .connectionTTL(connectionTTL) @@ -74,6 +130,7 @@ public class JabitServerConfig { @Bean public Set clients() { + security(); return Utils.readOrCreateList( CLIENT_LIST, "# Clients may send incomplete objects for proof of work.\n" diff --git a/src/main/java/ch/dissem/bitmessage/server/ProofOfWorkRequestHandler.java b/src/main/java/ch/dissem/bitmessage/server/ProofOfWorkRequestHandler.java index d5d95bd..eafe85c 100644 --- a/src/main/java/ch/dissem/bitmessage/server/ProofOfWorkRequestHandler.java +++ b/src/main/java/ch/dissem/bitmessage/server/ProofOfWorkRequestHandler.java @@ -16,57 +16,122 @@ package ch.dissem.bitmessage.server; +import ch.dissem.bitmessage.InternalContext; +import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.CustomMessage; import ch.dissem.bitmessage.entity.MessagePayload; +import ch.dissem.bitmessage.entity.valueobject.PrivateKey; import ch.dissem.bitmessage.exception.DecryptionFailedException; import ch.dissem.bitmessage.extensions.CryptoCustomMessage; import ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest; import ch.dissem.bitmessage.ports.CustomCommandHandler; import ch.dissem.bitmessage.ports.ProofOfWorkEngine; -import ch.dissem.bitmessage.server.repository.ProofOfWorkRepository; +import ch.dissem.bitmessage.server.repository.ServerProofOfWorkRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.util.ArrayList; +import java.util.Collection; import java.util.List; +import java.util.Timer; +import java.util.TimerTask; +import java.util.stream.Collectors; + +import static ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest.Request.CALCULATING; +import static ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest.Request.COMPLETE; /** * @author Christian Basler */ -public class ProofOfWorkRequestHandler implements CustomCommandHandler { - private final List decryptionKeys = new ArrayList<>(); - private ProofOfWorkRepository repo; +public class ProofOfWorkRequestHandler implements CustomCommandHandler, InternalContext.ContextHolder { + private final static Logger LOG = LoggerFactory.getLogger(ProofOfWorkRequestHandler.class); + + private final List decryptionKeys; + private final ServerProofOfWorkRepository repo; + private BitmessageAddress serverIdentity; private ProofOfWorkEngine engine; + private InternalContext context; + + public ProofOfWorkRequestHandler(ServerProofOfWorkRepository repo, Collection clients) { + this.repo = repo; + decryptionKeys = clients.stream().map(BitmessageAddress::getPublicDecryptionKey).collect(Collectors.toList()); + new Timer().schedule(new TimerTask() { + @Override + public void run() { + doMissingProofOfWork(); + } + }, 15_000); // After 15 seconds + } + + public void doMissingProofOfWork() { + List incompleteTasks = repo.getIncompleteTasks(); + LOG.info("Doing POW for " + incompleteTasks.size() + " tasks."); + for (ServerProofOfWorkRepository.Task task : incompleteTasks) { + engine.calculateNonce(task.initialHash, task.target, repo::updateTask); + } + } @Override public MessagePayload handle(CustomMessage message) { try { - CryptoCustomMessage cryptoMessage = CryptoCustomMessage.read(message.getData(), - (sender, in) -> ProofOfWorkRequest.read(sender, in)); + CryptoCustomMessage cryptoMessage = CryptoCustomMessage.read(message, + ProofOfWorkRequest::read); ProofOfWorkRequest request = decrypt(cryptoMessage); - if (request == null) return error("Unknown encryption key."); + if (request == null) + return CustomMessage.error( + "Unknown sender. Please ask the server's administrator to add you as a client. " + + "For this he'll need your identity." + ); switch (request.getRequest()) { case CALCULATE: - repo.storeTask(request); // FIXME - engine.calculateNonce(request.getInitialHash(), request.getData(), nonce -> { - - }); + if (!repo.hasTask(request.getInitialHash())) { + repo.storeTask(request); + // TODO: This is probably the place to do some book-keeping + // if we want to bill our customers. + engine.calculateNonce(request.getInitialHash(), request.getData(), repo::updateTask); + return new CryptoCustomMessage<>( + new ProofOfWorkRequest(getIdentity(), request.getInitialHash(), CALCULATING, new byte[0]) + ); + } else { + byte[] nonce = repo.getNonce(request); + CryptoCustomMessage response; + if (nonce != null) { + response = new CryptoCustomMessage<>( + new ProofOfWorkRequest(getIdentity(), request.getInitialHash(), COMPLETE, nonce) + ); + } else { + response = new CryptoCustomMessage<>( + new ProofOfWorkRequest(getIdentity(), request.getInitialHash(), CALCULATING, new byte[0]) + ); + } + response.signAndEncrypt(serverIdentity, request.getSender().getPubkey().getEncryptionKey()); + return response; + } } return null; } catch (IOException e) { - return error(e.getMessage()); + return CustomMessage.error(e.getMessage()); } } - private MessagePayload error(String message) { - try { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - out.write("ERROR\n".getBytes("UTF-8")); - out.write(message.getBytes("UTF-8")); - return new CustomMessage(out.toByteArray()); - } catch (Exception e) { - throw new RuntimeException(e); + private BitmessageAddress getIdentity() { + if (serverIdentity == null) { + synchronized (this) { + if (serverIdentity == null) { + serverIdentity = context.getAddressRepository().getIdentities().stream().findFirst().orElseGet(() -> { + final BitmessageAddress identity = new BitmessageAddress(new PrivateKey( + false, + context.getStreams()[0], + context.getNetworkNonceTrialsPerByte(), + context.getNetworkExtraBytes() + )); + context.getAddressRepository().save(identity); + return identity; + }); + } + } } + return serverIdentity; } private ProofOfWorkRequest decrypt(CryptoCustomMessage cryptoMessage) { @@ -81,4 +146,9 @@ public class ProofOfWorkRequestHandler implements CustomCommandHandler { return null; } + @Override + public void setContext(InternalContext context) { + this.context = context; + this.engine = context.getProofOfWorkEngine(); + } } diff --git a/src/main/java/ch/dissem/bitmessage/server/repository/ProofOfWorkRepository.java b/src/main/java/ch/dissem/bitmessage/server/repository/ServerProofOfWorkRepository.java similarity index 51% rename from src/main/java/ch/dissem/bitmessage/server/repository/ProofOfWorkRepository.java rename to src/main/java/ch/dissem/bitmessage/server/repository/ServerProofOfWorkRepository.java index 5db2215..e37e200 100644 --- a/src/main/java/ch/dissem/bitmessage/server/repository/ProofOfWorkRepository.java +++ b/src/main/java/ch/dissem/bitmessage/server/repository/ServerProofOfWorkRepository.java @@ -16,44 +16,26 @@ package ch.dissem.bitmessage.server.repository; -import ch.dissem.bitmessage.InternalContext; -import ch.dissem.bitmessage.entity.BitmessageAddress; -import ch.dissem.bitmessage.entity.valueobject.InventoryVector; import ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest; import ch.dissem.bitmessage.repository.JdbcConfig; import ch.dissem.bitmessage.repository.JdbcHelper; -import ch.dissem.bitmessage.server.entities.Update; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; -import java.util.Collection; import java.util.LinkedList; import java.util.List; -import java.util.stream.Stream; - -import static ch.dissem.bitmessage.server.repository.ProofOfWorkRepository.Status.*; /** * @author Christian Basler */ -public class ProofOfWorkRepository extends JdbcHelper implements InternalContext.ContextHolder { - private static final Logger LOG = LoggerFactory.getLogger(ProofOfWorkRepository.class); +public class ServerProofOfWorkRepository extends JdbcHelper { - private InternalContext context; - - protected ProofOfWorkRepository(JdbcConfig config) { + public ServerProofOfWorkRepository(JdbcConfig config) { super(config); } - @Override - public void setContext(InternalContext context) { - this.context = context; - } - /** * client (can be removed once the new IV is returned) * IV (without nonce) @@ -64,61 +46,79 @@ public class ProofOfWorkRepository extends JdbcHelper implements InternalContext public void storeTask(ProofOfWorkRequest request) { try (Connection connection = config.getConnection()) { PreparedStatement ps = connection.prepareStatement( - "INSERT INTO ProofOfWorkTask (initial_hash, client, target, status) VALUES (?, ?, ?, ?)"); + "INSERT INTO ProofOfWorkTask (initial_hash, client, target) VALUES (?, ?, ?)"); ps.setBytes(1, request.getInitialHash()); - ps.setString(2, request.getClient().getAddress()); + ps.setString(2, request.getSender().getAddress()); ps.setBytes(3, request.getData()); - ps.setString(4, CALCULATING.name()); ps.executeUpdate(); } catch (SQLException e) { throw new RuntimeException(e); } } - public void updateTask(InventoryVector temporaryIV, InventoryVector newIV) { + public void updateTask(byte[] initalHash, byte[] nonce) { try (Connection connection = config.getConnection()) { PreparedStatement ps = connection.prepareStatement( - "UPDATE ProofOfWorkTask SET IV = ?, status = ?, data = NULL WHERE temporaryIV = ?"); - ps.setBytes(1, newIV.getHash()); - ps.setString(2, FINISHED.name()); - ps.setBytes(3, temporaryIV.getHash()); + "UPDATE ProofOfWorkTask SET nonce = ? WHERE initial_hash = ?"); + ps.setBytes(1, nonce); + ps.setBytes(2, initalHash); ps.executeUpdate(); } catch (SQLException e) { throw new RuntimeException(e); } } - public Collection> getUnconfirmed(BitmessageAddress client) { - List> result = new LinkedList<>(); + public byte[] getNonce(ProofOfWorkRequest request) { try (Connection connection = config.getConnection()) { - PreparedStatement ps = connection.prepareStatement("SELECT temporaryIV, IV FROM ProofOfWorkTask WHERE client = ? AND status = ?"); - ps.setString(1, client.getAddress()); - ps.setString(2, FINISHED.name()); + PreparedStatement ps = connection.prepareStatement("SELECT nonce FROM ProofOfWorkTask WHERE initial_hash = ?"); + ps.setBytes(1, request.getInitialHash()); ResultSet rs = ps.executeQuery(); - while (rs.next()) { - InventoryVector temporaryIV = new InventoryVector(rs.getBytes(1)); - InventoryVector iv = new InventoryVector(rs.getBytes(2)); - result.add(new Update<>(temporaryIV, iv)); + if (rs.next()) { + return rs.getBytes(1); + } else { + return null; } } catch (SQLException e) { throw new RuntimeException(e); } - return result; } - public void confirm(Stream unconfirmed) { + public boolean hasTask(byte[] initialHash) { try (Connection connection = config.getConnection()) { - PreparedStatement ps = connection.prepareStatement( - "UPDATE ProofOfWorkTask SET status = ?, IV = NULL, client = NULL WHERE IV = ANY(?)"); - ps.setString(1, CONFIRMED.name()); - ps.setArray(2, connection.createArrayOf("BINARY", unconfirmed.map(InventoryVector::getHash).toArray())); - ps.executeUpdate(); + PreparedStatement ps = connection.prepareStatement("SELECT count(1) FROM ProofOfWorkTask WHERE initial_hash = ?"); + ps.setBytes(1, initialHash); + ResultSet rs = ps.executeQuery(); + rs.next(); + return rs.getInt(1) > 0; } catch (SQLException e) { throw new RuntimeException(e); } } - public enum Status { - CALCULATING, FINISHED, CONFIRMED + public List getIncompleteTasks() { + try (Connection connection = config.getConnection()) { + List result = new LinkedList<>(); + ResultSet rs = connection.createStatement().executeQuery( + "SELECT initial_hash, target FROM ProofOfWorkTask WHERE nonce IS NULL"); + while (rs.next()) { + result.add(new Task( + rs.getBytes(1), + rs.getBytes(2) + )); + } + return result; + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + public static class Task { + public final byte[] initialHash; + public final byte[] target; + + private Task(byte[] initialHash, byte[] target) { + this.initialHash = initialHash; + this.target = target; + } } } diff --git a/src/main/resources/db/migration/V1.0_ProofOfWorkTask.sql b/src/main/resources/db/migration/V1.2.1__ProofOfWorkTask.sql similarity index 100% rename from src/main/resources/db/migration/V1.0_ProofOfWorkTask.sql rename to src/main/resources/db/migration/V1.2.1__ProofOfWorkTask.sql