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.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<BitmessageAddress> clients() {
security();
return Utils.readOrCreateList(
CLIENT_LIST,
"# Clients may send incomplete objects for proof of work.\n"

View File

@ -16,58 +16,123 @@
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<byte[]> 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<byte[]> decryptionKeys;
private final ServerProofOfWorkRepository repo;
private BitmessageAddress serverIdentity;
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
public MessagePayload handle(CustomMessage message) {
try {
CryptoCustomMessage<ProofOfWorkRequest> cryptoMessage = CryptoCustomMessage.read(message.getData(),
(sender, in) -> ProofOfWorkRequest.read(sender, in));
CryptoCustomMessage<ProofOfWorkRequest> 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<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;
} 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<ProofOfWorkRequest> cryptoMessage) {
for (byte[] key : decryptionKeys) {
@ -81,4 +146,9 @@ public class ProofOfWorkRequestHandler implements CustomCommandHandler {
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;
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<Update<InventoryVector>> getUnconfirmed(BitmessageAddress client) {
List<Update<InventoryVector>> 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);
}
}
public boolean hasTask(byte[] initialHash) {
try (Connection connection = config.getConnection()) {
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 List<Task> getIncompleteTasks() {
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;
}
public void confirm(Stream<InventoryVector> unconfirmed) {
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();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public enum Status {
CALCULATING, FINISHED, CONFIRMED
public static class Task {
public final byte[] initialHash;
public final byte[] target;
private Task(byte[] initialHash, byte[] target) {
this.initialHash = initialHash;
this.target = target;
}
}
}