diff --git a/.gitignore b/.gitignore index 57462fb..0d42f5d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ # Project specific files +admins.conf +clients.conf *list.conf config.properties /*.db diff --git a/build.gradle b/build.gradle index 7f6b199..0fe15a1 100644 --- a/build.gradle +++ b/build.gradle @@ -44,6 +44,7 @@ dependencies { compile 'ch.dissem.jabit:jabit-networking:0.2.1-SNAPSHOT' compile 'ch.dissem.jabit:jabit-repositories:0.2.1-SNAPSHOT' compile 'ch.dissem.jabit:jabit-security-bouncy:0.2.1-SNAPSHOT' + compile 'ch.dissem.jabit:jabit-extensions:0.2.1-SNAPSHOT' compile 'com.h2database:h2:1.4.187' diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 66e6c70..94f382d 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip diff --git a/src/main/java/ch/dissem/bitmessage/server/CleanupJob.java b/src/main/java/ch/dissem/bitmessage/server/CleanupJob.java index 8b93cea..2294d42 100644 --- a/src/main/java/ch/dissem/bitmessage/server/CleanupJob.java +++ b/src/main/java/ch/dissem/bitmessage/server/CleanupJob.java @@ -1,3 +1,19 @@ +/* + * Copyright 2015 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package ch.dissem.bitmessage.server; import ch.dissem.bitmessage.BitmessageContext; @@ -7,7 +23,7 @@ import org.slf4j.LoggerFactory; import java.util.TimerTask; /** - * Created by chrigu on 04.10.15. + * @author Christian Basler */ public class CleanupJob extends TimerTask { private static final Logger LOG = LoggerFactory.getLogger(CleanupJob.class); diff --git a/src/main/java/ch/dissem/bitmessage/server/Constants.java b/src/main/java/ch/dissem/bitmessage/server/Constants.java new file mode 100644 index 0000000..a25f019 --- /dev/null +++ b/src/main/java/ch/dissem/bitmessage/server/Constants.java @@ -0,0 +1,13 @@ +package ch.dissem.bitmessage.server; + +/** + * Created by chrigu on 22.11.15. + */ +public interface Constants { + String ADMIN_LIST = "admins.conf"; + String CLIENT_LIST = "clients.conf"; + + String WHITELIST = "whitelist.conf"; + String SHORTLIST = "shortlist.conf"; + String BLACKLIST = "blacklist.conf"; +} diff --git a/src/main/java/ch/dissem/bitmessage/server/Converter.java b/src/main/java/ch/dissem/bitmessage/server/Converter.java index 0e387c4..364831a 100644 --- a/src/main/java/ch/dissem/bitmessage/server/Converter.java +++ b/src/main/java/ch/dissem/bitmessage/server/Converter.java @@ -1,3 +1,19 @@ +/* + * Copyright 2015 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package ch.dissem.bitmessage.server; import ch.dissem.bitmessage.entity.BitmessageAddress; diff --git a/src/main/java/ch/dissem/bitmessage/server/JabitServerConfig.java b/src/main/java/ch/dissem/bitmessage/server/JabitServerConfig.java index 6dcd8f6..7237e79 100644 --- a/src/main/java/ch/dissem/bitmessage/server/JabitServerConfig.java +++ b/src/main/java/ch/dissem/bitmessage/server/JabitServerConfig.java @@ -1,13 +1,29 @@ +/* + * Copyright 2015 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + 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; @@ -16,6 +32,9 @@ import springfox.documentation.spring.web.plugins.Docket; import java.util.Set; +import static ch.dissem.bitmessage.server.Constants.*; +import static java.util.stream.Collectors.toSet; + @Configuration public class JabitServerConfig { public static final int SHORTLIST_SIZE = 5; @@ -27,28 +46,102 @@ 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()) - .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) - .listener(plaintext -> { - }) .build(); } + @Bean + public Set admins() { + security(); + return Utils.readOrCreateList( + ADMIN_LIST, + "# Admins can send commands to the server.\n" + ).stream().map(BitmessageAddress::new).collect(toSet()); + } + + @Bean + public Set clients() { + security(); + return Utils.readOrCreateList( + CLIENT_LIST, + "# Clients may send incomplete objects for proof of work.\n" + ).stream().map(BitmessageAddress::new).collect(toSet()); + } + @Bean public Set whitelist() { return Utils.readOrCreateList( - "whitelist.conf", + WHITELIST, "# If there are any Bitmessage addresses in the whitelist, only those will be shown.\n" + "# blacklist.conf will be ignored, but shortlist.conf will be applied to whitelisted addresses.\n" ); @@ -57,7 +150,7 @@ public class JabitServerConfig { @Bean public Set shortlist() { return Utils.readOrCreateList( - "shortlist.conf", + SHORTLIST, "# Broadcasts of these addresses will be restricted to the last " + SHORTLIST_SIZE + " entries.\n\n" + "# Time Service:\n" + "BM-BcbRqcFFSQUUmXFKsPJgVQPSiFA3Xash\n\n" + @@ -69,7 +162,7 @@ public class JabitServerConfig { @Bean public Set blacklist() { return Utils.readOrCreateList( - "blacklist.conf", + BLACKLIST, "# Bitmessage addresses in this file are being ignored and their broadcasts won't be returned.\n" ); } diff --git a/src/main/java/ch/dissem/bitmessage/server/JabitServerController.java b/src/main/java/ch/dissem/bitmessage/server/JabitServerController.java index 9b458a9..6607955 100644 --- a/src/main/java/ch/dissem/bitmessage/server/JabitServerController.java +++ b/src/main/java/ch/dissem/bitmessage/server/JabitServerController.java @@ -1,3 +1,19 @@ +/* + * Copyright 2015 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package ch.dissem.bitmessage.server; import ch.dissem.bitmessage.BitmessageContext; diff --git a/src/main/java/ch/dissem/bitmessage/server/ProofOfWorkRequestHandler.java b/src/main/java/ch/dissem/bitmessage/server/ProofOfWorkRequestHandler.java new file mode 100644 index 0000000..ab98b10 --- /dev/null +++ b/src/main/java/ch/dissem/bitmessage/server/ProofOfWorkRequestHandler.java @@ -0,0 +1,163 @@ +/* + * Copyright 2015 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +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.ServerProofOfWorkRepository; +import ch.dissem.bitmessage.utils.UnixTime; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +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; +import static ch.dissem.bitmessage.utils.UnixTime.DAY; + +/** + * @author Christian Basler + */ +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 + new Timer().schedule(new TimerTask() { + @Override + public void run() { + repo.cleanupTasks(7 * DAY); + } + }, 60_000, DAY * 1000); // First time after 1 minute, then daily + } + + 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, + ProofOfWorkRequest::read); + ProofOfWorkRequest request = decrypt(cryptoMessage); + 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: + 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 CustomMessage.error(e.getMessage()); + } + } + + 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) { + for (byte[] key : decryptionKeys) { + try { + return cryptoMessage.decrypt(key); + } catch (DecryptionFailedException ignore) { + } catch (IOException e) { + throw new RuntimeException(e); + } + } + 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/ServerListener.java b/src/main/java/ch/dissem/bitmessage/server/ServerListener.java new file mode 100644 index 0000000..228c6bc --- /dev/null +++ b/src/main/java/ch/dissem/bitmessage/server/ServerListener.java @@ -0,0 +1,137 @@ +/* + * Copyright 2015 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.server; + +import ch.dissem.bitmessage.BitmessageContext; +import ch.dissem.bitmessage.entity.BitmessageAddress; +import ch.dissem.bitmessage.entity.Plaintext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collection; +import java.util.Scanner; + +import static ch.dissem.bitmessage.server.Constants.*; + +/** + * @author Christian Basler + */ +public class ServerListener implements BitmessageContext.Listener { + private final static Logger LOG = LoggerFactory.getLogger(ServerListener.class); + + private final Collection admins; + private final Collection clients; + + private final Collection whitelist; + private final Collection shortlist; + private final Collection blacklist; + + public ServerListener(Collection admins, + Collection clients, + Collection whitelist, + Collection shortlist, + Collection blacklist) { + this.admins = admins; + this.clients = clients; + this.whitelist = whitelist; + this.shortlist = shortlist; + this.blacklist = blacklist; + } + + @Override + public void receive(Plaintext message) { + if (admins.contains(message.getFrom())) { + String[] command = message.getSubject().trim().toLowerCase().split("\\s+"); + String data = message.getText(); + if (command.length == 2) { + switch (command[1]) { + case "client": + case "clients": + updateUserList(CLIENT_LIST, clients, command[0], data); + break; + case "admin": + case "admins": + case "administrator": + case "administrators": + updateUserList(ADMIN_LIST, admins, command[0], data); + break; + case "whitelist": + updateList(WHITELIST, whitelist, command[0], data); + break; + case "shortlist": + updateList(SHORTLIST, shortlist, command[0], data); + break; + case "blacklist": + updateList(BLACKLIST, blacklist, command[0], data); + break; + default: + LOG.trace("ignoring unknown command " + message.getSubject()); + } + } + } + } + + private void updateUserList(String file, Collection list, String command, String data) { + switch (command) { + case "set": + list.clear(); + case "add": + Scanner scanner = new Scanner(data); + while (scanner.hasNextLine()) { + String line = scanner.nextLine(); + try { + list.add(new BitmessageAddress(line)); + } catch (Exception e) { + LOG.info(command + " " + file + ": ignoring line: " + line); + } + } + Utils.saveList(file, list.stream().map(BitmessageAddress::getAddress)); + break; + case "remove": + list.removeIf(address -> data.contains(address.getAddress())); + Utils.saveList(file, list.stream().map(BitmessageAddress::getAddress)); + break; + default: + LOG.info("unknown command " + command + " on list " + file); + } + } + + private void updateList(String file, Collection list, String command, String data) { + switch (command) { + case "set": + list.clear(); + case "add": + Scanner scanner = new Scanner(data); + while (scanner.hasNextLine()) { + String line = scanner.nextLine(); + try { + list.add(new BitmessageAddress(line).getAddress()); + } catch (Exception e) { + LOG.info(command + " " + file + ": ignoring line: " + line); + } + } + Utils.saveList(file, list.stream()); + break; + case "remove": + list.removeIf(data::contains); + Utils.saveList(file, list.stream()); + break; + default: + LOG.info("unknown command " + command + " on list " + file); + } + } +} diff --git a/src/main/java/ch/dissem/bitmessage/server/Utils.java b/src/main/java/ch/dissem/bitmessage/server/Utils.java index 2f9a8d5..50d6a5f 100644 --- a/src/main/java/ch/dissem/bitmessage/server/Utils.java +++ b/src/main/java/ch/dissem/bitmessage/server/Utils.java @@ -1,10 +1,29 @@ +/* + * Copyright 2015 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package ch.dissem.bitmessage.server; import java.io.File; import java.io.FileNotFoundException; import java.io.FileWriter; import java.io.IOException; -import java.util.*; +import java.util.HashSet; +import java.util.Scanner; +import java.util.Set; +import java.util.stream.Stream; public class Utils { public static Set readOrCreateList(String filename, String content) { @@ -23,6 +42,24 @@ public class Utils { } } + public static void saveList(String filename, Stream content) { + try { + File file = new File(filename); + try (FileWriter fw = new FileWriter(file)) { + content.forEach(l -> { + try { + fw.write(l); + fw.write(System.lineSeparator()); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + public static Set readList(File file) { Set result = new HashSet<>(); try { @@ -38,4 +75,11 @@ public class Utils { } return result; } + + public static boolean zero(byte[] nonce) { + for (byte b : nonce) { + if (b != 0) return false; + } + return true; + } } diff --git a/src/main/java/ch/dissem/bitmessage/server/entities/Update.java b/src/main/java/ch/dissem/bitmessage/server/entities/Update.java new file mode 100644 index 0000000..3aebada --- /dev/null +++ b/src/main/java/ch/dissem/bitmessage/server/entities/Update.java @@ -0,0 +1,30 @@ +/* + * Copyright 2015 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.server.entities; + +/** + * @author Christian Basler + */ +public class Update { + public final T oldValue; + public final T newValue; + + public Update(T oldValue, T newValue) { + this.oldValue = oldValue; + this.newValue = newValue; + } +} diff --git a/src/main/java/ch/dissem/bitmessage/server/repository/ServerProofOfWorkRepository.java b/src/main/java/ch/dissem/bitmessage/server/repository/ServerProofOfWorkRepository.java new file mode 100644 index 0000000..c4af9b2 --- /dev/null +++ b/src/main/java/ch/dissem/bitmessage/server/repository/ServerProofOfWorkRepository.java @@ -0,0 +1,137 @@ +/* + * Copyright 2015 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.server.repository; + +import ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest; +import ch.dissem.bitmessage.repository.JdbcConfig; +import ch.dissem.bitmessage.repository.JdbcHelper; +import ch.dissem.bitmessage.utils.UnixTime; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.LinkedList; +import java.util.List; + +/** + * @author Christian Basler + */ +public class ServerProofOfWorkRepository extends JdbcHelper { + + public ServerProofOfWorkRepository(JdbcConfig config) { + super(config); + } + + /** + * client (can be removed once the new IV is returned) + * IV (without nonce) + * IV (with nonce, can be removed once the new IV is returned) + * status: calculating, finished, confirmed + * data (can be removed once POW calculation is done) + */ + public void storeTask(ProofOfWorkRequest request) { + try (Connection connection = config.getConnection()) { + PreparedStatement ps = connection.prepareStatement( + "INSERT INTO ProofOfWorkTask (initial_hash, client, target, timestamp) VALUES (?, ?, ?, ?)"); + ps.setBytes(1, request.getInitialHash()); + ps.setString(2, request.getSender().getAddress()); + ps.setBytes(3, request.getData()); + ps.setLong(4, UnixTime.now()); + ps.executeUpdate(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + public void updateTask(byte[] initalHash, byte[] nonce) { + try (Connection connection = config.getConnection()) { + PreparedStatement ps = connection.prepareStatement( + "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 byte[] getNonce(ProofOfWorkRequest request) { + try (Connection connection = config.getConnection()) { + PreparedStatement ps = connection.prepareStatement("SELECT nonce FROM ProofOfWorkTask WHERE initial_hash = ?"); + ps.setBytes(1, request.getInitialHash()); + ResultSet rs = ps.executeQuery(); + 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 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 void cleanupTasks(long ageInSeconds) { + try (Connection connection = config.getConnection()) { + PreparedStatement ps = connection.prepareStatement( + "DELETE FROM ProofOfWorkTask WHERE timestamp < ?"); + ps.setLong(1, UnixTime.now(-ageInSeconds)); + ps.executeUpdate(); + } 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.2.1__ProofOfWorkTask.sql b/src/main/resources/db/migration/V1.2.1__ProofOfWorkTask.sql new file mode 100644 index 0000000..0561d9d --- /dev/null +++ b/src/main/resources/db/migration/V1.2.1__ProofOfWorkTask.sql @@ -0,0 +1,7 @@ +CREATE TABLE ProofOfWorkTask ( + initial_hash BINARY(64) NOT NULL PRIMARY KEY, + client VARCHAR(40) NOT NULL, + target BINARY(32), + nonce BINARY(8), + timestamp BIGINT NOT NULL, +); \ No newline at end of file