Server POW should now work

This commit is contained in:
Christian Basler 2015-12-21 15:21:30 +01:00
parent 891294f267
commit aafa4a4a11
4 changed files with 208 additions and 81 deletions

View File

@ -19,12 +19,11 @@ package ch.dissem.bitmessage.server;
import ch.dissem.bitmessage.BitmessageContext; import ch.dissem.bitmessage.BitmessageContext;
import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.BitmessageAddress;
import ch.dissem.bitmessage.networking.DefaultNetworkHandler; import ch.dissem.bitmessage.networking.DefaultNetworkHandler;
import ch.dissem.bitmessage.ports.MemoryNodeRegistry; import ch.dissem.bitmessage.ports.*;
import ch.dissem.bitmessage.repository.JdbcAddressRepository; import ch.dissem.bitmessage.repository.*;
import ch.dissem.bitmessage.repository.JdbcConfig;
import ch.dissem.bitmessage.repository.JdbcInventory;
import ch.dissem.bitmessage.repository.JdbcMessageRepository;
import ch.dissem.bitmessage.security.bc.BouncySecurity; 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.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@ -47,17 +46,74 @@ public class JabitServerConfig {
@Value("${bitmessage.connection.limit}") @Value("${bitmessage.connection.limit}")
private int connectionLimit; 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 @Bean
public BitmessageContext bitmessageContext() { public BitmessageContext bitmessageContext() {
JdbcConfig config = new JdbcConfig("jdbc:h2:file:./jabit;AUTO_SERVER=TRUE", "sa", null);
return new BitmessageContext.Builder() return new BitmessageContext.Builder()
.addressRepo(new JdbcAddressRepository(config)) .addressRepo(addressRepo())
.inventory(new JdbcInventory(config)) .inventory(inventory())
.messageRepo(new JdbcMessageRepository(config)) .messageRepo(messageRepo())
.nodeRegistry(new MemoryNodeRegistry()) .nodeRegistry(nodeRegistry())
.networkHandler(new DefaultNetworkHandler()) .powRepo(proofOfWorkRepo())
.listener(new ServerListener(admins(), clients(), whitelist(), shortlist(), blacklist())) .networkHandler(networkHandler())
.security(new BouncySecurity()) .listener(serverListener())
.customCommandHandler(commandHandler())
.security(security())
.port(port) .port(port)
.connectionLimit(connectionLimit) .connectionLimit(connectionLimit)
.connectionTTL(connectionTTL) .connectionTTL(connectionTTL)
@ -74,6 +130,7 @@ public class JabitServerConfig {
@Bean @Bean
public Set<BitmessageAddress> clients() { public Set<BitmessageAddress> clients() {
security();
return Utils.readOrCreateList( return Utils.readOrCreateList(
CLIENT_LIST, CLIENT_LIST,
"# Clients may send incomplete objects for proof of work.\n" "# Clients may send incomplete objects for proof of work.\n"

View File

@ -16,57 +16,122 @@
package ch.dissem.bitmessage.server; 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.CustomMessage;
import ch.dissem.bitmessage.entity.MessagePayload; import ch.dissem.bitmessage.entity.MessagePayload;
import ch.dissem.bitmessage.entity.valueobject.PrivateKey;
import ch.dissem.bitmessage.exception.DecryptionFailedException; import ch.dissem.bitmessage.exception.DecryptionFailedException;
import ch.dissem.bitmessage.extensions.CryptoCustomMessage; import ch.dissem.bitmessage.extensions.CryptoCustomMessage;
import ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest; import ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest;
import ch.dissem.bitmessage.ports.CustomCommandHandler; import ch.dissem.bitmessage.ports.CustomCommandHandler;
import ch.dissem.bitmessage.ports.ProofOfWorkEngine; 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.io.IOException;
import java.util.ArrayList; import java.util.Collection;
import java.util.List; 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 * @author Christian Basler
*/ */
public class ProofOfWorkRequestHandler implements CustomCommandHandler { public class ProofOfWorkRequestHandler implements CustomCommandHandler, InternalContext.ContextHolder {
private final List<byte[]> decryptionKeys = new ArrayList<>(); private final static Logger LOG = LoggerFactory.getLogger(ProofOfWorkRequestHandler.class);
private ProofOfWorkRepository repo;
private final List<byte[]> decryptionKeys;
private final ServerProofOfWorkRepository repo;
private BitmessageAddress serverIdentity;
private ProofOfWorkEngine engine; private ProofOfWorkEngine engine;
private InternalContext context;
public ProofOfWorkRequestHandler(ServerProofOfWorkRepository repo, Collection<BitmessageAddress> 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<ServerProofOfWorkRepository.Task> 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 @Override
public MessagePayload handle(CustomMessage message) { public MessagePayload handle(CustomMessage message) {
try { try {
CryptoCustomMessage<ProofOfWorkRequest> cryptoMessage = CryptoCustomMessage.read(message.getData(), CryptoCustomMessage<ProofOfWorkRequest> cryptoMessage = CryptoCustomMessage.read(message,
(sender, in) -> ProofOfWorkRequest.read(sender, in)); ProofOfWorkRequest::read);
ProofOfWorkRequest request = decrypt(cryptoMessage); 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()) { switch (request.getRequest()) {
case CALCULATE: case CALCULATE:
repo.storeTask(request); // FIXME if (!repo.hasTask(request.getInitialHash())) {
engine.calculateNonce(request.getInitialHash(), request.getData(), nonce -> { 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<ProofOfWorkRequest> 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; return null;
} catch (IOException e) { } catch (IOException e) {
return error(e.getMessage()); return CustomMessage.error(e.getMessage());
} }
} }
private MessagePayload error(String message) { private BitmessageAddress getIdentity() {
try { if (serverIdentity == null) {
ByteArrayOutputStream out = new ByteArrayOutputStream(); synchronized (this) {
out.write("ERROR\n".getBytes("UTF-8")); if (serverIdentity == null) {
out.write(message.getBytes("UTF-8")); serverIdentity = context.getAddressRepository().getIdentities().stream().findFirst().orElseGet(() -> {
return new CustomMessage(out.toByteArray()); final BitmessageAddress identity = new BitmessageAddress(new PrivateKey(
} catch (Exception e) { false,
throw new RuntimeException(e); context.getStreams()[0],
context.getNetworkNonceTrialsPerByte(),
context.getNetworkExtraBytes()
));
context.getAddressRepository().save(identity);
return identity;
});
}
}
} }
return serverIdentity;
} }
private ProofOfWorkRequest decrypt(CryptoCustomMessage<ProofOfWorkRequest> cryptoMessage) { private ProofOfWorkRequest decrypt(CryptoCustomMessage<ProofOfWorkRequest> cryptoMessage) {
@ -81,4 +146,9 @@ public class ProofOfWorkRequestHandler implements CustomCommandHandler {
return null; return null;
} }
@Override
public void setContext(InternalContext context) {
this.context = context;
this.engine = context.getProofOfWorkEngine();
}
} }

View File

@ -16,44 +16,26 @@
package ch.dissem.bitmessage.server.repository; 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.extensions.pow.ProofOfWorkRequest;
import ch.dissem.bitmessage.repository.JdbcConfig; import ch.dissem.bitmessage.repository.JdbcConfig;
import ch.dissem.bitmessage.repository.JdbcHelper; 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.Connection;
import java.sql.PreparedStatement; import java.sql.PreparedStatement;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.Collection;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.stream.Stream;
import static ch.dissem.bitmessage.server.repository.ProofOfWorkRepository.Status.*;
/** /**
* @author Christian Basler * @author Christian Basler
*/ */
public class ProofOfWorkRepository extends JdbcHelper implements InternalContext.ContextHolder { public class ServerProofOfWorkRepository extends JdbcHelper {
private static final Logger LOG = LoggerFactory.getLogger(ProofOfWorkRepository.class);
private InternalContext context; public ServerProofOfWorkRepository(JdbcConfig config) {
protected ProofOfWorkRepository(JdbcConfig config) {
super(config); super(config);
} }
@Override
public void setContext(InternalContext context) {
this.context = context;
}
/** /**
* client (can be removed once the new IV is returned) * client (can be removed once the new IV is returned)
* IV (without nonce) * IV (without nonce)
@ -64,61 +46,79 @@ public class ProofOfWorkRepository extends JdbcHelper implements InternalContext
public void storeTask(ProofOfWorkRequest request) { public void storeTask(ProofOfWorkRequest request) {
try (Connection connection = config.getConnection()) { try (Connection connection = config.getConnection()) {
PreparedStatement ps = connection.prepareStatement( 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.setBytes(1, request.getInitialHash());
ps.setString(2, request.getClient().getAddress()); ps.setString(2, request.getSender().getAddress());
ps.setBytes(3, request.getData()); ps.setBytes(3, request.getData());
ps.setString(4, CALCULATING.name());
ps.executeUpdate(); ps.executeUpdate();
} catch (SQLException e) { } catch (SQLException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
public void updateTask(InventoryVector temporaryIV, InventoryVector newIV) { public void updateTask(byte[] initalHash, byte[] nonce) {
try (Connection connection = config.getConnection()) { try (Connection connection = config.getConnection()) {
PreparedStatement ps = connection.prepareStatement( PreparedStatement ps = connection.prepareStatement(
"UPDATE ProofOfWorkTask SET IV = ?, status = ?, data = NULL WHERE temporaryIV = ?"); "UPDATE ProofOfWorkTask SET nonce = ? WHERE initial_hash = ?");
ps.setBytes(1, newIV.getHash()); ps.setBytes(1, nonce);
ps.setString(2, FINISHED.name()); ps.setBytes(2, initalHash);
ps.setBytes(3, temporaryIV.getHash());
ps.executeUpdate(); ps.executeUpdate();
} catch (SQLException e) { } catch (SQLException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
public Collection<Update<InventoryVector>> getUnconfirmed(BitmessageAddress client) { public byte[] getNonce(ProofOfWorkRequest request) {
List<Update<InventoryVector>> result = new LinkedList<>();
try (Connection connection = config.getConnection()) { try (Connection connection = config.getConnection()) {
PreparedStatement ps = connection.prepareStatement("SELECT temporaryIV, IV FROM ProofOfWorkTask WHERE client = ? AND status = ?"); PreparedStatement ps = connection.prepareStatement("SELECT nonce FROM ProofOfWorkTask WHERE initial_hash = ?");
ps.setString(1, client.getAddress()); ps.setBytes(1, request.getInitialHash());
ps.setString(2, FINISHED.name());
ResultSet rs = ps.executeQuery(); ResultSet rs = ps.executeQuery();
while (rs.next()) { if (rs.next()) {
InventoryVector temporaryIV = new InventoryVector(rs.getBytes(1)); return rs.getBytes(1);
InventoryVector iv = new InventoryVector(rs.getBytes(2)); } else {
result.add(new Update<>(temporaryIV, iv)); return null;
} }
} catch (SQLException e) { } catch (SQLException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
return result;
} }
public void confirm(Stream<InventoryVector> unconfirmed) { public boolean hasTask(byte[] initialHash) {
try (Connection connection = config.getConnection()) { try (Connection connection = config.getConnection()) {
PreparedStatement ps = connection.prepareStatement( PreparedStatement ps = connection.prepareStatement("SELECT count(1) FROM ProofOfWorkTask WHERE initial_hash = ?");
"UPDATE ProofOfWorkTask SET status = ?, IV = NULL, client = NULL WHERE IV = ANY(?)"); ps.setBytes(1, initialHash);
ps.setString(1, CONFIRMED.name()); ResultSet rs = ps.executeQuery();
ps.setArray(2, connection.createArrayOf("BINARY", unconfirmed.map(InventoryVector::getHash).toArray())); rs.next();
ps.executeUpdate(); return rs.getInt(1) > 0;
} catch (SQLException e) { } catch (SQLException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
public enum Status { public List<Task> getIncompleteTasks() {
CALCULATING, FINISHED, CONFIRMED try (Connection connection = config.getConnection()) {
List<Task> 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;
}
} }
} }