From eb3a8e2272a52cb355cd7a392776ee6a603ca936 Mon Sep 17 00:00:00 2001 From: Bruno Faustino Date: Thu, 6 Nov 2025 06:54:49 +0000 Subject: [PATCH 01/16] Update protocol to not send the whole blockchain in a propose - Solves issue #3 --- src/StreamletApp/BlockchainManager.java | 68 +++++++++++++---------- src/StreamletApp/StreamletNode.java | 13 ++--- src/utils/application/BlockWithChain.java | 6 -- 3 files changed, 44 insertions(+), 43 deletions(-) delete mode 100644 src/utils/application/BlockWithChain.java diff --git a/src/StreamletApp/BlockchainManager.java b/src/StreamletApp/BlockchainManager.java index e8a1aea..d052f9d 100644 --- a/src/StreamletApp/BlockchainManager.java +++ b/src/StreamletApp/BlockchainManager.java @@ -1,7 +1,6 @@ package StreamletApp; import utils.application.Block; -import utils.application.BlockWithChain; import utils.application.Transaction; import utils.logs.AppLogger; @@ -14,9 +13,10 @@ public class BlockchainManager { private static final int SHA1_LENGTH = 20; private static final Block GENESIS_BLOCK = new Block(new byte[SHA1_LENGTH], 0, 0, new Transaction[0]); + private final Set seenNotarizedChains = new HashSet<>(); private final Set finalizedChains = new HashSet<>(); - private final HashMap pendingProposes = new HashMap<>(); + private final Map pendingProposes = new HashMap<>(); private LinkedList biggestNotarizedChain = new LinkedList<>(); public BlockchainManager() { @@ -24,20 +24,48 @@ public BlockchainManager() { seenNotarizedChains.add(new ChainView(biggestNotarizedChain)); } + public LinkedList getBiggestNotarizedChain() { + return biggestNotarizedChain; + } + + public boolean onPropose(Block proposedBlock) { + Optional chainOpt = seenNotarizedChains.stream() + .filter(notarizedChain -> + Arrays.equals(proposedBlock.parentHash(), notarizedChain.blocks().getLast().getSHA1())) + .findFirst(); + if (chainOpt.isEmpty()) return false; + + LinkedList proposedChain = new LinkedList<>(chainOpt.get().blocks()); + proposedChain.add(proposedBlock); + ChainView parentChain = new ChainView(proposedChain); + + boolean isStrictlyLonger = seenNotarizedChains.stream() + .anyMatch(notarizedChain -> proposedBlock.length() > notarizedChain.blocks().getLast().length()); + if (!isStrictlyLonger) { + return false; + } + pendingProposes.put(proposedBlock, parentChain); + return true; + } + public void notarizeBlock(Block headerBlock) { - BlockWithChain proposal = pendingProposes.get(headerBlock); - if (proposal == null) { + ChainView chain = pendingProposes.get(headerBlock); + if (chain == null) { return; } - LinkedList chain = proposal.chain(); - chain.add(proposal.block()); + + Block addedBlock = chain.blocks().removeLast(); + seenNotarizedChains.remove(chain); + chain.blocks().add(addedBlock); + seenNotarizedChains.add(chain); + pendingProposes.remove(headerBlock); - seenNotarizedChains.add(new ChainView(chain)); - if (chain.getLast().length() > biggestNotarizedChain.getLast().length()) { - biggestNotarizedChain = chain; + + if (chain.blocks().getLast().length() > biggestNotarizedChain.getLast().length()) { + biggestNotarizedChain = chain.blocks(); } AppLogger.logInfo("Block notarized: epoch " + headerBlock.epoch() + " length " + headerBlock.length()); - tryToFinalizeChain(chain); + tryToFinalizeChain(chain.blocks()); } private void tryToFinalizeChain(LinkedList chain) { @@ -55,22 +83,6 @@ private void tryToFinalizeChain(LinkedList chain) { } } - public boolean onPropose(BlockWithChain proposal) { - Block proposedBlock = proposal.block(); - LinkedList chain = proposal.chain(); - Block parentTip = chain.getLast(); - - if (!Arrays.equals(proposedBlock.parentHash(), parentTip.getSHA1())) return false; - - boolean isStrictlyLonger = seenNotarizedChains.stream() - .anyMatch(notarizedChain -> proposedBlock.length() > notarizedChain.blocks().getLast().length()); - if (!isStrictlyLonger) { - return false; - } - pendingProposes.put(proposedBlock, proposal); - return true; - } - public void printBiggestFinalizedChain() { final String GREEN = "\u001B[32m"; final String RESET = "\u001B[0m"; @@ -104,8 +116,4 @@ public void printBiggestFinalizedChain() { } } - - public LinkedList getBiggestNotarizedChain() { - return biggestNotarizedChain; - } } diff --git a/src/StreamletApp/StreamletNode.java b/src/StreamletApp/StreamletNode.java index 396e49c..9654093 100644 --- a/src/StreamletApp/StreamletNode.java +++ b/src/StreamletApp/StreamletNode.java @@ -130,8 +130,7 @@ private void consumeMessages() { } private void proposeNewBlock(int epoch) throws NoSuchAlgorithmException { - LinkedList parentChain = blockchainManager.getBiggestNotarizedChain(); - Block parent = parentChain.getLast(); + Block parent = blockchainManager.getBiggestNotarizedChain().getLast(); Transaction[] transactions; if (isClientGeneratingTransactions) { transactions = new Transaction[clientPendingTransactionsQueue.size()]; @@ -145,7 +144,7 @@ private void proposeNewBlock(int epoch) throws NoSuchAlgorithmException { Block newBlock = new Block(parent.getSHA1(), epoch, parent.length() + 1, transactions); AppLogger.logDebug("Proposed block: " + newBlock + " with transactions: " + Arrays.toString(transactions)); - urbNode.broadcastFromLocal(new Message(MessageType.PROPOSE, new BlockWithChain(newBlock, parentChain), localId)); + urbNode.broadcastFromLocal(new Message(MessageType.PROPOSE, newBlock, localId)); } private void handleMessageDelivery(Message message) { @@ -153,19 +152,19 @@ private void handleMessageDelivery(Message message) { switch (message.type()) { case PROPOSE -> handlePropose(message); case VOTE -> handleVote(message); + default -> {} } } private void handlePropose(Message message) { - BlockWithChain blockWithChain = (BlockWithChain) message.content(); - SeenProposal proposal = new SeenProposal(message.sender(), blockWithChain.block().epoch()); + Block fullBlock = (Block) message.content(); + SeenProposal proposal = new SeenProposal(message.sender(), fullBlock.epoch()); if (seenProposals.contains(proposal) - || !blockchainManager.onPropose(blockWithChain)) + || !blockchainManager.onPropose(fullBlock)) return; seenProposals.add(proposal); - Block fullBlock = blockWithChain.block(); Block blockHeader = new Block(fullBlock.parentHash(), fullBlock.epoch(), fullBlock.length(), new Transaction[0]); urbNode.broadcastFromLocal(new Message(MessageType.VOTE, blockHeader, localId)); AppLogger.logDebug("Voted for block from leader " + message.sender() + " epoch " + fullBlock.epoch()); diff --git a/src/utils/application/BlockWithChain.java b/src/utils/application/BlockWithChain.java deleted file mode 100644 index 48e5ccf..0000000 --- a/src/utils/application/BlockWithChain.java +++ /dev/null @@ -1,6 +0,0 @@ -package utils.application; - -import java.util.LinkedList; - -public record BlockWithChain(Block block, LinkedList chain) implements Content { -} From 83a1bb770ea44f40810cf09e7a38a9853693cb39 Mon Sep 17 00:00:00 2001 From: Bruno Faustino Date: Fri, 7 Nov 2025 08:01:18 +0000 Subject: [PATCH 02/16] Add global protocol start time in config and add epoch synchronization using it for both before the protocol starts (remove countdown latch) and after (for node recovery in case of crash). Note: there can be a very small millisecond difference between the nodes - Solves issue #6 --- config.txt | 3 ++ src/GroupCommunication/P2PNode.java | 8 --- src/Streamlet.java | 7 +-- src/StreamletApp/StreamletNode.java | 75 ++++++++++++++++++++++------- src/URB/URBNode.java | 5 -- src/utils/ConfigParser.java | 23 +++++++++ 6 files changed, 88 insertions(+), 33 deletions(-) diff --git a/config.txt b/config.txt index b0fe795..b29997b 100644 --- a/config.txt +++ b/config.txt @@ -5,6 +5,9 @@ P2P=127.0.0.1:54582 P2P=127.0.0.1:54583 P2P=127.0.0.1:54584 +# Agreed time for every server to start protocol dd:MM:yyyy hh:mm:ss +start=07-11-2025 07:29:00 + # NORMAL and DEBUG mode for logs logLevel=NORMAL diff --git a/src/GroupCommunication/P2PNode.java b/src/GroupCommunication/P2PNode.java index 13d5133..8a6f311 100644 --- a/src/GroupCommunication/P2PNode.java +++ b/src/GroupCommunication/P2PNode.java @@ -21,7 +21,6 @@ public class P2PNode implements Runnable, AutoCloseable { private static final long RETRY_DELAY_MS = 2500; private final PeerInfo localPeerInfo; - private final CountDownLatch allPeersConnectedLatch; private final Map peerInfoById; private final ConcurrentLinkedQueue outgoingMessageQueue = new ConcurrentLinkedQueue<>(); private final BlockingQueue incomingMessageQueue = new LinkedBlockingQueue<>(); @@ -35,7 +34,6 @@ public P2PNode(PeerInfo localPeerInfo, List remotePeersInfo) throws IO this.localPeerInfo = localPeerInfo; this.peerInfoById = remotePeersInfo.stream() .collect(Collectors.toMap(PeerInfo::id, Function.identity())); - this.allPeersConnectedLatch = new CountDownLatch(peerInfoById.size()); initializeServerSocket(); } @@ -201,7 +199,6 @@ private void handleConnectComplete(SelectionKey key) throws IOException { clientChannel.write(idBuffer); if (connectedPeers.add(remotePeer.id())) { - allPeersConnectedLatch.countDown(); peerConnectionBackoff.remove(remotePeer.id()); } @@ -241,7 +238,6 @@ private void handleIncomingConnection(SelectionKey key) throws IOException { peerConnections.put(remotePeerId, incomingChannel); if (connectedPeers.add(remotePeerId)) { - allPeersConnectedLatch.countDown(); peerConnectionBackoff.remove(remotePeerId); } @@ -297,10 +293,6 @@ public void enqueueIncomingMessage(Message message) { incomingMessageQueue.add(message); } - public void waitForAllPeersConnected() throws InterruptedException { - allPeersConnectedLatch.await(); - } - private void processOutgoingMessages() { MessageWithReceiver messageWithReceiver; while ((messageWithReceiver = outgoingMessageQueue.poll()) != null) { diff --git a/src/Streamlet.java b/src/Streamlet.java index e93c9cd..942cfcf 100644 --- a/src/Streamlet.java +++ b/src/Streamlet.java @@ -14,14 +14,15 @@ void main(String[] args) throws IOException, InterruptedException { ConfigParser.ConfigData configData = ConfigParser.parseConfig(); + LocalDateTime start = configData.start; + List peerInfos = configData.peers; AppLogger.updateLoggerLevel(configData.logLevel); PeerInfo localPeer = peerInfos.get(nodeId); List remotePeers = peerInfos.stream().filter(p -> p.id() != nodeId).toList(); AppLogger.logDebug(remotePeers.toString()); - - AppLogger.logInfo("Waiting all peers to connect..."); - StreamletNode node = new StreamletNode(localPeer, remotePeers, 1, configData.isClientGeneratingTransactions, configData.servers.get(nodeId)); + + StreamletNode node = new StreamletNode(localPeer, remotePeers, 1, start, configData.isClientGeneratingTransactions, configData.servers.get(nodeId)); node.startProtocol(); } diff --git a/src/StreamletApp/StreamletNode.java b/src/StreamletApp/StreamletNode.java index 9654093..594c04b 100644 --- a/src/StreamletApp/StreamletNode.java +++ b/src/StreamletApp/StreamletNode.java @@ -11,6 +11,9 @@ import java.net.ServerSocket; import java.net.Socket; import java.security.NoSuchAlgorithmException; +import java.time.LocalDateTime; +import java.time.temporal.ChronoField; +import java.time.temporal.ChronoUnit; import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; @@ -23,6 +26,7 @@ public class StreamletNode { private static final int CONFUSION_START = 0; private static final int CONFUSION_DURATION = 2; private final int deltaInSeconds; + private final LocalDateTime start; private final int numberOfDistinctNodes; private final TransactionPoolSimulator transactionPoolSimulator; private final Random random = new Random(1L); @@ -43,11 +47,12 @@ public class StreamletNode { private final Address myClientAddress; public StreamletNode(PeerInfo localPeerInfo, List remotePeersInfo, int deltaInSeconds, - boolean isClientGeneratingTransactions, Address myClientAddress) + LocalDateTime start, boolean isClientGeneratingTransactions, Address myClientAddress) throws IOException { localId = localPeerInfo.id(); numberOfDistinctNodes = 1 + remotePeersInfo.size(); this.deltaInSeconds = deltaInSeconds; + this.start = start; transactionPoolSimulator = new TransactionPoolSimulator(numberOfDistinctNodes); blockchainManager = new BlockchainManager(); urbNode = new URBNode(localPeerInfo, remotePeersInfo, derivableQueue::add); @@ -57,10 +62,59 @@ public StreamletNode(PeerInfo localPeerInfo, List remotePeersInfo, int public void startProtocol() throws InterruptedException { launchThreads(); - urbNode.waitForAllPeersToConnect(); long epochDuration = 2L * deltaInSeconds; - scheduler.scheduleAtFixedRate(this::safeAdvanceEpoch, 0, epochDuration, TimeUnit.SECONDS); + long nanoSecondsToWait = waitStartOrRecover(); + scheduler.scheduleAtFixedRate( + this::safeAdvanceEpoch, nanoSecondsToWait, (long) (epochDuration*1e9), TimeUnit.NANOSECONDS + ); + } + + private void launchThreads() { + executor.submit(() -> { + try { + urbNode.startURBNode(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + + executor.submit(this::consumeMessages); + if (isClientGeneratingTransactions) executor.submit(this::receiveClientTransactionsRequests); + } + + private long waitStartOrRecover() { + LocalDateTime now = LocalDateTime.now(); + if (now.isBefore(start)) return waitStart(); + if (now.isAfter(start)) return recover(); + return 0; + } + + private long waitStart() { + AppLogger.logInfo("Waiting for protocol to start..."); + long nanoSecondsToWait = ChronoUnit.NANOS.between(LocalDateTime.now(), start); + return nanoSecondsToWait > 0 ? nanoSecondsToWait : 0; + } + + private long recover() { + AppLogger.logInfo("(Re)joining in late..."); + + /*TODO - Ask for missing chain and random to determine leader (somehow...) + (that might be better inside the epoch instead of here so we avoid possibly losing on a proposed block + that may arrive after we get the chain back, but before we start the epoch scheduling)*/ + + int epochDuration = 2 * this.deltaInSeconds; + long protocolAgeInSeconds = ChronoUnit.SECONDS.between(start, LocalDateTime.now()); + // Ignore the seconds spent on the current epoch + protocolAgeInSeconds -= protocolAgeInSeconds % epochDuration; + int ongoingEpoch = (int) (protocolAgeInSeconds / epochDuration); + currentEpoch.compareAndSet(0, ongoingEpoch + 1); + + LocalDateTime nextEpochDate = start.plusSeconds(protocolAgeInSeconds + epochDuration); + long nanoSecondsToWait = ChronoUnit.NANOS.between( + LocalDateTime.now(), nextEpochDate + ); + return nanoSecondsToWait > 0 ? nanoSecondsToWait : 0; } private void safeAdvanceEpoch() { @@ -74,7 +128,7 @@ private void safeAdvanceEpoch() { private void advanceEpoch() { int epoch = currentEpoch.get(); int currentLeaderId = calculateLeaderId(epoch); - AppLogger.logInfo("#### EPOCH = " + epoch + " LEADER= " + currentLeaderId + " ####"); + AppLogger.logInfo("#### EPOCH = " + epoch + " LEADER = " + currentLeaderId + " ####"); if (localId == currentLeaderId) { try { @@ -92,19 +146,6 @@ private void advanceEpoch() { currentEpoch.incrementAndGet(); } - private void launchThreads() { - executor.submit(() -> { - try { - urbNode.startURBNode(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - }); - - executor.submit(this::consumeMessages); - if (isClientGeneratingTransactions) executor.submit(this::receiveClientTransactionsRequests); - } - private void consumeMessages() { final Queue bufferedMessages = new LinkedList<>(); try { diff --git a/src/URB/URBNode.java b/src/URB/URBNode.java index 12a3109..d430ff2 100644 --- a/src/URB/URBNode.java +++ b/src/URB/URBNode.java @@ -33,12 +33,7 @@ public URBNode(PeerInfo localPeerInfo, this.callback = callback; } - public void waitForAllPeersToConnect() throws InterruptedException { - networkLayer.waitForAllPeersConnected(); - } - public void startURBNode() throws InterruptedException { - waitForAllPeersToConnect(); AppLogger.logInfo("P2PNode " + localPeerId + " is ready"); executor.submit(this::processIncomingMessages); } diff --git a/src/utils/ConfigParser.java b/src/utils/ConfigParser.java index e417427..1c5a424 100644 --- a/src/utils/ConfigParser.java +++ b/src/utils/ConfigParser.java @@ -7,17 +7,25 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; +import utils.logs.AppLogger; public class ConfigParser { public final static String CONFIG_FILE = "config.txt"; + public final static DateTimeFormatter START_FORMAT = DateTimeFormatter.ofPattern("dd-MM-yyyy HH:mm:ss"); + public final static LocalDateTime DEFAULT_START_DATE = + LocalDateTime.parse("01-01-2000 00:00:00", START_FORMAT); private static final Pattern P2P_PATTERN = Pattern.compile("^P2P\\s*=\\s*(.+)$", Pattern.CASE_INSENSITIVE); + private static final Pattern START_PATTERN = Pattern.compile("^start\\s*=\\s*(\\d{2}-\\d{2}-\\d{4} \\d{2}:\\d{2}:\\d{2})$", Pattern.CASE_INSENSITIVE); private static final Pattern SERVER_PATTERN = Pattern.compile("^server\\s*=\\s*(.+)$", Pattern.CASE_INSENSITIVE); private static final Pattern LOGLEVEL_PATTERN = Pattern.compile("^logLevel\\s*=\\s*(.+)$", Pattern.CASE_INSENSITIVE); private static final Pattern TRANSACTION_MODE_PATTERN = Pattern.compile("^transactionsMode\\s*=\\s*(.+)$", Pattern.CASE_INSENSITIVE); @@ -33,12 +41,15 @@ public static ConfigData parseConfig() throws IOException { if (line.isEmpty() || line.startsWith("#")) continue; // skip empty lines or comments Matcher p2pMatcher = P2P_PATTERN.matcher(line); + Matcher startMatcher = START_PATTERN.matcher(line); Matcher serverMatcher = SERVER_PATTERN.matcher(line); Matcher logLevelMatcher = LOGLEVEL_PATTERN.matcher(line); Matcher transactionMatcher = TRANSACTION_MODE_PATTERN.matcher(line); if (p2pMatcher.matches()) { configData.peers.add(new PeerInfo(peerIndex++, Address.fromString(p2pMatcher.group(1).trim()))); + } else if (startMatcher.matches()) { + configData.start = parseToDate(startMatcher.group(1).trim()); } else if (serverMatcher.matches()) { configData.servers.put(serverIndex++, Address.fromString(serverMatcher.group(1).trim())); } else if (logLevelMatcher.matches()) { @@ -57,8 +68,20 @@ public static ConfigData parseConfig() throws IOException { public static class ConfigData { public List peers = new ArrayList<>(); + public LocalDateTime start = DEFAULT_START_DATE; public Map servers = new HashMap<>(); public LogLevel logLevel = LogLevel.NORMAL; public boolean isClientGeneratingTransactions = false; } + + private static LocalDateTime parseToDate(String dateStr) { + try { + return LocalDateTime.parse(dateStr, START_FORMAT); + } catch (DateTimeParseException e) { + AppLogger.logWarning( + "Invalid format for protocol start date. Default start date was used. Should be: dd-MM-yyyy HH:mm:ss." + ); + return DEFAULT_START_DATE; + } + } } From 0c506859ce4839364c6b31b01e4de28f02149f51 Mon Sep 17 00:00:00 2001 From: Bruno Faustino Date: Fri, 7 Nov 2025 08:07:09 +0000 Subject: [PATCH 03/16] Change config comment for the start time to show the right expected format --- config.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.txt b/config.txt index b29997b..062ed41 100644 --- a/config.txt +++ b/config.txt @@ -5,7 +5,7 @@ P2P=127.0.0.1:54582 P2P=127.0.0.1:54583 P2P=127.0.0.1:54584 -# Agreed time for every server to start protocol dd:MM:yyyy hh:mm:ss +# Agreed time for every server to start protocol dd-MM-yyyy HH:mm:ss start=07-11-2025 07:29:00 # NORMAL and DEBUG mode for logs From 78a8e195a5ab0dcb9461120a516e8ddb481c4735 Mon Sep 17 00:00:00 2001 From: Bruno Faustino Date: Sun, 9 Nov 2025 04:52:01 +0000 Subject: [PATCH 04/16] Add node recovery by asking for the missing blocks from its blockchain and the state of the leader randomizer. Add new message types JOIN and UPDATE accordingly, with a new message content for each of them. Update Block to be comparable by epoch. Note: all the missing blocks are sent to every process and then again through echoes which is not desirable. Solves issue #4 --- config.txt | 2 +- src/StreamletApp/BlockchainManager.java | 62 ++++++++++++++++++ src/StreamletApp/StreamletNode.java | 82 ++++++++++++++++++++---- src/URB/URBNode.java | 2 +- src/utils/application/Block.java | 7 +- src/utils/application/CatchUp.java | 39 +++++++++++ src/utils/application/MessageType.java | 2 + src/utils/application/MissingEpochs.java | 3 + 8 files changed, 182 insertions(+), 17 deletions(-) create mode 100644 src/utils/application/CatchUp.java create mode 100644 src/utils/application/MissingEpochs.java diff --git a/config.txt b/config.txt index 062ed41..0dcda93 100644 --- a/config.txt +++ b/config.txt @@ -6,7 +6,7 @@ P2P=127.0.0.1:54583 P2P=127.0.0.1:54584 # Agreed time for every server to start protocol dd-MM-yyyy HH:mm:ss -start=07-11-2025 07:29:00 +start=09-11-2025 04:34:00 # NORMAL and DEBUG mode for logs logLevel=NORMAL diff --git a/src/StreamletApp/BlockchainManager.java b/src/StreamletApp/BlockchainManager.java index d052f9d..134993a 100644 --- a/src/StreamletApp/BlockchainManager.java +++ b/src/StreamletApp/BlockchainManager.java @@ -83,6 +83,68 @@ private void tryToFinalizeChain(LinkedList chain) { } } + public int getLastEpoch() { + // When we persist the blockchain in the disk, + // the file should contain the current epoch in the 1st line to increase performance + int lastEpoch = -1; + for (ChainView chain : seenNotarizedChains) { + Block lastChainBlock = chain.blocks().getLast(); + int highestEpochInChain = lastChainBlock.epoch(); + if (!lastChainBlock.equals(GENESIS_BLOCK) && highestEpochInChain > lastEpoch) + lastEpoch = highestEpochInChain; + } + return lastEpoch; + } + + public LinkedList blocksFromToEpoch(int from, int to) { + LinkedList missingBlocks = new LinkedList<>(); + seenNotarizedChains.stream() + .forEach(chain -> chain.blocks().stream() + .forEach(block -> { + if (from <= block.epoch() && block.epoch() < to) missingBlocks.add(block); + })); + + LinkedList missingBlocksSorted = new LinkedList<>( + missingBlocks.stream() + .distinct() + .sorted() + .toList() + ); + return missingBlocksSorted; + } + + public void insertMissingBlocks(LinkedList missingBlocks) { + for (Block b : missingBlocks) { + byte[] parentHash = b.parentHash(); + LinkedList chainOfTheBlock = null; + boolean newChain = false; + + for (ChainView chain : seenNotarizedChains) { + LinkedList chainBlocks = chain.blocks(); + for (int i = 0; i < chainBlocks.size(); i++) { + Block bChain = chainBlocks.get(i); + if (!Arrays.equals(parentHash, bChain.getSHA1())) continue; + + if (bChain.equals(chainBlocks.getLast())) { + chainOfTheBlock = chainBlocks; + newChain = false; + } else { + chainOfTheBlock = new LinkedList<>(chainBlocks.subList(0, chainBlocks.indexOf(bChain) + 1)); + newChain = true; + } + break; + } + if (chainOfTheBlock != null) break; + } + + if (chainOfTheBlock == null) continue; + chainOfTheBlock.add(b); + if (newChain) { + seenNotarizedChains.add(new ChainView(chainOfTheBlock)); + } + } + } + public void printBiggestFinalizedChain() { final String GREEN = "\u001B[32m"; final String RESET = "\u001B[0m"; diff --git a/src/StreamletApp/StreamletNode.java b/src/StreamletApp/StreamletNode.java index 594c04b..b1e8d3c 100644 --- a/src/StreamletApp/StreamletNode.java +++ b/src/StreamletApp/StreamletNode.java @@ -12,7 +12,6 @@ import java.net.Socket; import java.security.NoSuchAlgorithmException; import java.time.LocalDateTime; -import java.time.temporal.ChronoField; import java.time.temporal.ChronoUnit; import java.util.*; import java.util.concurrent.*; @@ -29,8 +28,8 @@ public class StreamletNode { private final LocalDateTime start; private final int numberOfDistinctNodes; private final TransactionPoolSimulator transactionPoolSimulator; - private final Random random = new Random(1L); private final AtomicInteger currentEpoch = new AtomicInteger(0); + private final AtomicInteger currentLeaderId = new AtomicInteger(0); private final int localId; private final URBNode urbNode; @@ -43,9 +42,13 @@ public class StreamletNode { private final ExecutorService executor = Executors.newCachedThreadPool(); private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + private final CountDownLatch waitForCatchUp = new CountDownLatch(1); private final boolean isClientGeneratingTransactions; private final Address myClientAddress; + private Random random = new Random(1L); // To determine epoch leader + private volatile boolean needsToRecover = false; + public StreamletNode(PeerInfo localPeerInfo, List remotePeersInfo, int deltaInSeconds, LocalDateTime start, boolean isClientGeneratingTransactions, Address myClientAddress) throws IOException { @@ -98,10 +101,7 @@ private long waitStart() { private long recover() { AppLogger.logInfo("(Re)joining in late..."); - - /*TODO - Ask for missing chain and random to determine leader (somehow...) - (that might be better inside the epoch instead of here so we avoid possibly losing on a proposed block - that may arrive after we get the chain back, but before we start the epoch scheduling)*/ + needsToRecover = true; int epochDuration = 2 * this.deltaInSeconds; long protocolAgeInSeconds = ChronoUnit.SECONDS.between(start, LocalDateTime.now()); @@ -117,6 +117,18 @@ private long recover() { return nanoSecondsToWait > 0 ? nanoSecondsToWait : 0; } + private void catchUp(int fromEpoch, int toEpoch) { + MissingEpochs missingEpochs = new MissingEpochs(fromEpoch, toEpoch); + Message join = new Message(MessageType.JOIN, missingEpochs, localId); + urbNode.broadcastFromLocal(join); + + try { + waitForCatchUp.await(); + } catch (InterruptedException e) { + AppLogger.logError("Failed to wait for missing blocks - Interrupted", e); + } + } + private void safeAdvanceEpoch() { try { advanceEpoch(); @@ -127,10 +139,12 @@ private void safeAdvanceEpoch() { private void advanceEpoch() { int epoch = currentEpoch.get(); - int currentLeaderId = calculateLeaderId(epoch); + // After catching up, this node receives the already advanced Random (if not in confusion epoch) + if (needsToRecover) catchUp(blockchainManager.getLastEpoch() + 1, epoch); + else calculateLeaderId(epoch); AppLogger.logInfo("#### EPOCH = " + epoch + " LEADER = " + currentLeaderId + " ####"); - if (localId == currentLeaderId) { + if (localId == currentLeaderId.get()) { try { if (!isClientGeneratingTransactions || !clientPendingTransactionsQueue.isEmpty()) { AppLogger.logDebug("Node " + localId + " is leader: proposing new block"); @@ -158,10 +172,21 @@ private void consumeMessages() { continue; } - while (!bufferedMessages.isEmpty()) { - handleMessageDelivery(bufferedMessages.poll()); + // Store new proposals until this node is done recovering + if (needsToRecover && message.type().equals(MessageType.PROPOSE)) { + bufferedMessages.add(message); + continue; + } + + // This node cannot help if it is also recovering + if (needsToRecover && message.type().equals(MessageType.JOIN)) { + continue; } + if (!needsToRecover) + while (!bufferedMessages.isEmpty()) + handleMessageDelivery(bufferedMessages.poll()); + handleMessageDelivery(message); } } catch (InterruptedException e) { @@ -193,6 +218,8 @@ private void handleMessageDelivery(Message message) { switch (message.type()) { case PROPOSE -> handlePropose(message); case VOTE -> handleVote(message); + case JOIN -> handleJoin(message); + case UPDATE -> handleUpdate(message); default -> {} } } @@ -221,14 +248,41 @@ private void handleVote(Message message) { } } + private void handleJoin(Message message) { + if (message.sender() == localId) return; + + MissingEpochs missingEpochs = (MissingEpochs) message.content(); + LinkedList missingBlocks = blockchainManager.blocksFromToEpoch(missingEpochs.from(), missingEpochs.to()); + + Message catchUp = new Message( + MessageType.UPDATE, + new CatchUp(message.sender(), missingBlocks, random, currentLeaderId.get(), currentEpoch.get()), + localId + ); + urbNode.broadcastFromLocal(catchUp); + } + + private void handleUpdate(Message message) { + if (!needsToRecover) return; + CatchUp catchUp = (CatchUp) message.content(); + if (catchUp.slackerId() != localId) return; + + this.random = catchUp.leaderRand(); + this.currentLeaderId.set(catchUp.leaderId()); + blockchainManager.insertMissingBlocks(catchUp.missingChain()); + + needsToRecover = false; + AppLogger.logInfo("Finished recovering"); + waitForCatchUp.countDown(); + } + private boolean inConfusionEpoch(int epoch) { return epoch >= CONFUSION_START && epoch <= CONFUSION_START + CONFUSION_DURATION - 1; } - - private int calculateLeaderId(int epoch) { - return inConfusionEpoch(epoch) ? epoch % numberOfDistinctNodes - : random.nextInt(numberOfDistinctNodes); + private void calculateLeaderId(int epoch) { + if (inConfusionEpoch(epoch)) currentLeaderId.set(epoch % numberOfDistinctNodes); + else currentLeaderId.set(random.nextInt(numberOfDistinctNodes)); } private void receiveClientTransactionsRequests() { diff --git a/src/URB/URBNode.java b/src/URB/URBNode.java index d430ff2..af7948d 100644 --- a/src/URB/URBNode.java +++ b/src/URB/URBNode.java @@ -67,7 +67,7 @@ private void deliverMessage(Message message) { broadcastFromLocal(contentMessage); } } - case PROPOSE, VOTE -> { + case PROPOSE, VOTE, JOIN, UPDATE -> { Message echoMessage = new Message(MessageType.ECHO, message, localPeerId); broadcastToPeers(echoMessage); deliverToApplication(message); diff --git a/src/utils/application/Block.java b/src/utils/application/Block.java index aa949e9..d48332c 100644 --- a/src/utils/application/Block.java +++ b/src/utils/application/Block.java @@ -7,7 +7,7 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; -public record Block(byte[] parentHash, Integer epoch, Integer length, Transaction[] transactions) implements Content { +public record Block(byte[] parentHash, Integer epoch, Integer length, Transaction[] transactions) implements Content, Comparable { public Block(byte[] parentHash, Integer epoch, Integer length, Transaction[] transactions) { this.parentHash = parentHash; @@ -57,6 +57,11 @@ public int hashCode() { return result; } + @Override + public int compareTo(Block o) { + return epoch - o.epoch; + } + public String toStringSummary() { String partialParentHash = IntStream.range(0, Math.min(4, parentHash.length)) .mapToObj(i -> String.format("%02X", parentHash[i])) diff --git a/src/utils/application/CatchUp.java b/src/utils/application/CatchUp.java new file mode 100644 index 0000000..000d1ad --- /dev/null +++ b/src/utils/application/CatchUp.java @@ -0,0 +1,39 @@ +package utils.application; + +import java.util.LinkedList; +import java.util.Random; + +public record CatchUp( + Integer slackerId, + LinkedList missingChain, + Random leaderRand, + Integer leaderId, + Integer currentEpoch +) implements Content { + + public CatchUp( + Integer slackerId, + LinkedList missingChain, + Random leaderRand, + Integer leaderId, + Integer currentEpoch + ) { + this.slackerId = slackerId; + this.missingChain = new LinkedList<>(missingChain); + this.leaderRand = leaderRand; + this.leaderId = leaderId; + this.currentEpoch = currentEpoch; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof CatchUp catchUp)) return false; + + return slackerId.equals(catchUp.slackerId) && currentEpoch.equals(catchUp.currentEpoch); + } + + @Override + public int hashCode() { + return 31 * slackerId.hashCode() + currentEpoch.hashCode(); + } +} diff --git a/src/utils/application/MessageType.java b/src/utils/application/MessageType.java index 23545e0..a132614 100644 --- a/src/utils/application/MessageType.java +++ b/src/utils/application/MessageType.java @@ -1,6 +1,8 @@ package utils.application; public enum MessageType { + JOIN, + UPDATE, PROPOSE, VOTE, ECHO diff --git a/src/utils/application/MissingEpochs.java b/src/utils/application/MissingEpochs.java new file mode 100644 index 0000000..ac6fc42 --- /dev/null +++ b/src/utils/application/MissingEpochs.java @@ -0,0 +1,3 @@ +package utils.application; + +public record MissingEpochs(Integer from, Integer to) implements Content {} From 466598d627d7a41946402a3663c8c686e381b423 Mon Sep 17 00:00:00 2001 From: Bruno Faustino Date: Thu, 13 Nov 2025 02:48:03 +0000 Subject: [PATCH 05/16] Reimplement blockchain data structure to be a tree of blocks with a finalized status where children blocks can be accessed by the hash of the parent block. Allow for out of order proposals in case of network problems by letting the blockchain hold those blocks (which are not possible to access if the search starts at the genesis block) and by finalizing a given block if one of its children can find a finalized block or if this block is in an interval of blocks from consecutive epochs of enough size for it to be finalized --- config.txt | 4 +- src/StreamletApp/BlockNode.java | 33 +++ src/StreamletApp/BlockchainManager.java | 271 +++++++++++++++--------- src/StreamletApp/ChainView.java | 25 --- src/StreamletApp/Hash.java | 18 ++ src/StreamletApp/StreamletNode.java | 9 +- src/utils/application/Block.java | 7 +- src/utils/application/CatchUp.java | 6 +- 8 files changed, 228 insertions(+), 145 deletions(-) create mode 100644 src/StreamletApp/BlockNode.java delete mode 100644 src/StreamletApp/ChainView.java create mode 100644 src/StreamletApp/Hash.java diff --git a/config.txt b/config.txt index 0dcda93..c2e183d 100644 --- a/config.txt +++ b/config.txt @@ -6,10 +6,10 @@ P2P=127.0.0.1:54583 P2P=127.0.0.1:54584 # Agreed time for every server to start protocol dd-MM-yyyy HH:mm:ss -start=09-11-2025 04:34:00 +start=13-11-2025 02:27:00 # NORMAL and DEBUG mode for logs -logLevel=NORMAL +logLevel=DEBUG # SIMULATION: transactions are generated randomly by the servers # CLIENT: waits for client to send transactions diff --git a/src/StreamletApp/BlockNode.java b/src/StreamletApp/BlockNode.java new file mode 100644 index 0000000..1cc8820 --- /dev/null +++ b/src/StreamletApp/BlockNode.java @@ -0,0 +1,33 @@ +package StreamletApp; + +import java.io.Serializable; +import utils.application.Block; + +public class BlockNode implements Serializable { + + private final Block block; + private Boolean finalized; + + public BlockNode(Block block, Boolean finalized) { + this.block = block; + this.finalized = finalized; + } + + public Block block() {return block;} + + public Boolean finalized() {return finalized;} + + public void finalizeBlock() {finalized = true;} + + @Override + public boolean equals(Object o) { + if (!(o instanceof BlockNode blockNode)) return false; + + return block.equals(blockNode.block); + } + + @Override + public int hashCode() { + return block.hashCode(); + } +} diff --git a/src/StreamletApp/BlockchainManager.java b/src/StreamletApp/BlockchainManager.java index 134993a..1f65614 100644 --- a/src/StreamletApp/BlockchainManager.java +++ b/src/StreamletApp/BlockchainManager.java @@ -5,8 +5,8 @@ import utils.logs.AppLogger; import java.util.*; +import java.util.function.Predicate; import java.util.stream.Collectors; -import java.util.stream.Gatherers; public class BlockchainManager { public static final int FINALIZATION_MIN_SIZE = 3; @@ -14,134 +14,202 @@ public class BlockchainManager { private static final Block GENESIS_BLOCK = new Block(new byte[SHA1_LENGTH], 0, 0, new Transaction[0]); - private final Set seenNotarizedChains = new HashSet<>(); - private final Set finalizedChains = new HashSet<>(); - private final Map pendingProposes = new HashMap<>(); - private LinkedList biggestNotarizedChain = new LinkedList<>(); + private final Map> blockchain = new HashMap<>(); + private final Map hashToBlockNode = new HashMap<>(); + private final Set blocksForRecovery = new HashSet<>(); + private final Set pendingProposes = new HashSet<>(); + + private int mostRecentEpoch = -1; public BlockchainManager() { - biggestNotarizedChain.add(GENESIS_BLOCK); - seenNotarizedChains.add(new ChainView(biggestNotarizedChain)); + BlockNode genesis = new BlockNode(GENESIS_BLOCK, true); + Hash genesisParentHash = new Hash(GENESIS_BLOCK.parentHash()); + Hash genesisHash = new Hash(GENESIS_BLOCK.getSHA1()); + + List root = new LinkedList<>(); + root.add(genesis); + blockchain.put(genesisParentHash, root); + blockchain.put(genesisHash, new LinkedList<>()); + + hashToBlockNode.put(genesisHash, genesis); + } + + public List getBiggestNotarizedChain() { + return findBiggestChainFromPredicate(new Hash(GENESIS_BLOCK.parentHash()), _ -> true); } - public LinkedList getBiggestNotarizedChain() { - return biggestNotarizedChain; + public List getBiggestFinalizedChain() { + return findBiggestChainFromPredicate(new Hash(GENESIS_BLOCK.parentHash()), BlockNode::finalized); + } + + private List findBiggestChainFromPredicate(Hash parentHash, Predicate predicate) { + List result = new LinkedList<>(); + + if (!parentHash.equals(new Hash(GENESIS_BLOCK.parentHash()))) + result.add(hashToBlockNode.get(parentHash).block()); + + result.addAll( + blockchain.get(parentHash).stream() + .filter(predicate::test) + .map(child -> findBiggestChainFromPredicate(new Hash(child.block().getSHA1()), predicate)) + .max(Comparator.comparing(chain -> chain.size())) + .orElseGet(() -> new LinkedList<>()) + ); + return result; } public boolean onPropose(Block proposedBlock) { - Optional chainOpt = seenNotarizedChains.stream() - .filter(notarizedChain -> - Arrays.equals(proposedBlock.parentHash(), notarizedChain.blocks().getLast().getSHA1())) - .findFirst(); - if (chainOpt.isEmpty()) return false; - - LinkedList proposedChain = new LinkedList<>(chainOpt.get().blocks()); - proposedChain.add(proposedBlock); - ChainView parentChain = new ChainView(proposedChain); - - boolean isStrictlyLonger = seenNotarizedChains.stream() - .anyMatch(notarizedChain -> proposedBlock.length() > notarizedChain.blocks().getLast().length()); + // Check if this block is strictly longer than any notarized chain this block has seen thus far + boolean isStrictlyLonger = blockchain.keySet().stream() + .filter(parentHash -> blockchain.get(parentHash).isEmpty()) + .anyMatch(hash -> proposedBlock.length() > hashToBlockNode.get(hash).block().length()); if (!isStrictlyLonger) { return false; } - pendingProposes.put(proposedBlock, parentChain); + pendingProposes.add(proposedBlock); + + // While recovering from crash, a node will likely receive propose messages + // even if it still does not have some of the previous blocks it needs + // so this block is placed in the data structure anyway and + // it can be linked with the others later when/if this node receives the previous blocks + Hash blockHash = new Hash(proposedBlock.getSHA1()); + BlockNode blockNode = new BlockNode(proposedBlock, false); + hashToBlockNode.put(blockHash, blockNode); return true; } public void notarizeBlock(Block headerBlock) { - ChainView chain = pendingProposes.get(headerBlock); - if (chain == null) { - return; + // The same block with or without its transactions are considered equal + Block fullBlock = null; + for (Block pendingBlock : pendingProposes) { + if (headerBlock.equals(pendingBlock)) { + fullBlock = pendingBlock; + break; + } } + if (fullBlock == null) return; + + // Add to the blockchain and prepare another entry with the hash of this block as parent + // if there was not one already due to out of order proposals + Hash parentHash = new Hash(fullBlock.parentHash()); + Hash blockHash = new Hash(fullBlock.getSHA1()); + BlockNode block = hashToBlockNode.get(blockHash); + blockchain.computeIfAbsent(parentHash, _ -> new LinkedList<>()) + .add(block); + blockchain.computeIfAbsent(blockHash, _ -> new LinkedList<>()); - Block addedBlock = chain.blocks().removeLast(); - seenNotarizedChains.remove(chain); - chain.blocks().add(addedBlock); - seenNotarizedChains.add(chain); + if (headerBlock.epoch() > mostRecentEpoch) mostRecentEpoch = headerBlock.epoch(); pendingProposes.remove(headerBlock); - if (chain.blocks().getLast().length() > biggestNotarizedChain.getLast().length()) { - biggestNotarizedChain = chain.blocks(); - } AppLogger.logInfo("Block notarized: epoch " + headerBlock.epoch() + " length " + headerBlock.length()); - tryToFinalizeChain(chain.blocks()); + tryToFinalizeChain(block); + } + + private void tryToFinalizeChain(BlockNode block) { + // For every child of this block or of the next blocks, finalize the chain if the child is finalized + finalizeFromTheNextBlocks(new Hash(block.block().getSHA1())); + + // Finds an epoch interval of blocks from the epoch of this block +- + // the min consecutive blocks needed to finalize a chain. + // If this interval of consecutive blocks has enough size, + // finalize the chain until the last block of this interval + finalizeFromConsecutivesOf(block); } - private void tryToFinalizeChain(LinkedList chain) { - int size = chain.size(); - if (size < FINALIZATION_MIN_SIZE) return; - boolean shouldChainBeFinalized = chain - .subList(chain.size() - FINALIZATION_MIN_SIZE, chain.size()) - .stream() - .map(Block::epoch) - .gather(Gatherers.windowSliding(2)) // zip xs $ tail xs - .map(window -> window.getLast() - window.getFirst()) - .allMatch(delta -> delta == 1); - if (shouldChainBeFinalized) { - finalizedChains.add(new ChainView(new LinkedList<>(biggestNotarizedChain.subList(0, size - 1)))); + private void finalizeFromTheNextBlocks(Hash parentHash) { + for (BlockNode child : blockchain.get(parentHash)) { + if (child.finalized()) finalizeFrom(child); + finalizeFromTheNextBlocks(new Hash(child.block().getSHA1())); } } - public int getLastEpoch() { - // When we persist the blockchain in the disk, - // the file should contain the current epoch in the 1st line to increase performance - int lastEpoch = -1; - for (ChainView chain : seenNotarizedChains) { - Block lastChainBlock = chain.blocks().getLast(); - int highestEpochInChain = lastChainBlock.epoch(); - if (!lastChainBlock.equals(GENESIS_BLOCK) && highestEpochInChain > lastEpoch) - lastEpoch = highestEpochInChain; + private void finalizeFromConsecutivesOf(BlockNode block) { + List beforeBlockInConsecutiveEpochs = new LinkedList<>(); + List afterBlockInConsecutiveEpochs = new LinkedList<>(); + + // After block in consecutive epochs + BlockNode currBlock = block; + int currEpoch = block.block().epoch(); + for (int i = 1; i < FINALIZATION_MIN_SIZE; i++) { + List children = blockchain.get(new Hash(currBlock.block().getSHA1())); + + final int currEpochCopy = currEpoch; + Optional maybeChild = children.stream() + .filter(blockNode -> blockNode.block().epoch() == currEpochCopy + 1) + .findFirst(); + + if (maybeChild.isEmpty()) break; + BlockNode child = maybeChild.get(); + + currEpoch++; + afterBlockInConsecutiveEpochs.add(child); + currBlock = child; + } + + // Before block in consecutive epochs + currBlock = block; + currEpoch = block.block().epoch(); + for (int i = 1; i < FINALIZATION_MIN_SIZE; i++) { + currBlock = hashToBlockNode.get(new Hash(currBlock.block().parentHash())); + if (currBlock == null + || currBlock.block().epoch() != currEpoch - 1 + || currBlock.equals(new BlockNode(GENESIS_BLOCK, true))) + break; + + currEpoch--; + beforeBlockInConsecutiveEpochs.add(currBlock); } - return lastEpoch; + + // Build interval of consecutive blocks + List finalizeInterval = new LinkedList<>(); + finalizeInterval.addAll(beforeBlockInConsecutiveEpochs); + finalizeInterval.add(block); + finalizeInterval.addAll(afterBlockInConsecutiveEpochs); + + if (finalizeInterval.size() >= FINALIZATION_MIN_SIZE) finalizeFrom(finalizeInterval.getLast()); } - public LinkedList blocksFromToEpoch(int from, int to) { - LinkedList missingBlocks = new LinkedList<>(); - seenNotarizedChains.stream() - .forEach(chain -> chain.blocks().stream() - .forEach(block -> { - if (from <= block.epoch() && block.epoch() < to) missingBlocks.add(block); - })); - - LinkedList missingBlocksSorted = new LinkedList<>( - missingBlocks.stream() - .distinct() - .sorted() - .toList() - ); - return missingBlocksSorted; + private void finalizeFrom(BlockNode block) { + BlockNode finalizationStarter = hashToBlockNode.get(new Hash(block.block().parentHash())); + + for (BlockNode currBlock = finalizationStarter; + !currBlock.block().equals(GENESIS_BLOCK); + currBlock = hashToBlockNode.get(new Hash(currBlock.block().parentHash()))) + { + if (currBlock.finalized()) break; + currBlock.finalizeBlock(); + } } - public void insertMissingBlocks(LinkedList missingBlocks) { - for (Block b : missingBlocks) { - byte[] parentHash = b.parentHash(); - LinkedList chainOfTheBlock = null; - boolean newChain = false; - - for (ChainView chain : seenNotarizedChains) { - LinkedList chainBlocks = chain.blocks(); - for (int i = 0; i < chainBlocks.size(); i++) { - Block bChain = chainBlocks.get(i); - if (!Arrays.equals(parentHash, bChain.getSHA1())) continue; - - if (bChain.equals(chainBlocks.getLast())) { - chainOfTheBlock = chainBlocks; - newChain = false; - } else { - chainOfTheBlock = new LinkedList<>(chainBlocks.subList(0, chainBlocks.indexOf(bChain) + 1)); - newChain = true; - } - break; - } - if (chainOfTheBlock != null) break; - } + public int getLastEpoch() { + return mostRecentEpoch; + } - if (chainOfTheBlock == null) continue; - chainOfTheBlock.add(b); - if (newChain) { - seenNotarizedChains.add(new ChainView(chainOfTheBlock)); - } + public List blocksFromToEpoch(int from, int to) { + return blockchain.values().stream() + .flatMap(List::stream) + .sorted(Comparator.comparing(block -> block.block().epoch())) + .dropWhile(block -> block.block().epoch() < from) + .takeWhile(block -> block.block().epoch() < to) + .toList(); + } + + public void insertMissingBlocks(List missingBlocks) { + // It is possible that a proposed block that arrives to the blockchain before calling this method + // would supposingly finalize any of these missing blocks but in that case, + // then they will eventually be finalized because the next proposed blocks + // extend from the biggest chain which would have to be this one according to the consistency proof + for (BlockNode missingBlock : missingBlocks) { + // Ignore if it was already inserted in the blockchain from a previous UPDATE message + if (!blocksForRecovery.add(missingBlock)) continue; + + Hash parentHash = new Hash(missingBlock.block().parentHash()); + Hash blockHash = new Hash(missingBlock.block().getSHA1()); + blockchain.computeIfAbsent(parentHash, _ -> new LinkedList<>()) + .add(missingBlock); + blockchain.computeIfAbsent(blockHash, _ -> new LinkedList<>()); + hashToBlockNode.put(blockHash, missingBlock); } } @@ -152,10 +220,7 @@ public void printBiggestFinalizedChain() { String header = "=== LONGEST FINALIZED CHAIN ==="; String border = "=".repeat(header.length()); - LinkedList biggestFinalizedChain = finalizedChains.stream() - .max(Comparator.comparing(c -> c.blocks().getLast().length())) - .map(ChainView::blocks) - .orElse(new LinkedList<>()); + List biggestFinalizedChain = getBiggestFinalizedChain(); String chainString = biggestFinalizedChain.stream() .skip(1) @@ -167,7 +232,7 @@ public void printBiggestFinalizedChain() { border, header, border, - biggestFinalizedChain.isEmpty() ? "No Finalized Chain Yet" : chainString, + biggestFinalizedChain.size() == 1 ? "No Finalized Chain Yet" : chainString, border ); diff --git a/src/StreamletApp/ChainView.java b/src/StreamletApp/ChainView.java deleted file mode 100644 index 0611251..0000000 --- a/src/StreamletApp/ChainView.java +++ /dev/null @@ -1,25 +0,0 @@ -package StreamletApp; - -import utils.application.Block; - -import java.util.Arrays; -import java.util.LinkedList; -import java.util.Objects; - -// this is just a wrapper to overwrite both hashcode and equals -record ChainView(LinkedList blocks) { - @Override - public int hashCode() { - return Objects.hash(blocks.stream().map(Block::getSHA1).toArray()); - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof ChainView(LinkedList blocks1))) return false; - if (blocks.size() != blocks1.size()) return false; - for (int i = 0; i < blocks.size(); i++) { - if (!Arrays.equals(blocks.get(i).getSHA1(), blocks1.get(i).getSHA1())) return false; - } - return true; - } -} \ No newline at end of file diff --git a/src/StreamletApp/Hash.java b/src/StreamletApp/Hash.java new file mode 100644 index 0000000..faae324 --- /dev/null +++ b/src/StreamletApp/Hash.java @@ -0,0 +1,18 @@ +package StreamletApp; + +import java.util.Arrays; + +record Hash(byte[] hash) { + + @Override + public boolean equals(Object o) { + if (!(o instanceof Hash hashOther)) return false; + + return Arrays.equals(this.hash, hashOther.hash); + } + + @Override + public int hashCode() { + return Arrays.hashCode(hash); + } +} diff --git a/src/StreamletApp/StreamletNode.java b/src/StreamletApp/StreamletNode.java index b1e8d3c..0920232 100644 --- a/src/StreamletApp/StreamletNode.java +++ b/src/StreamletApp/StreamletNode.java @@ -178,11 +178,6 @@ private void consumeMessages() { continue; } - // This node cannot help if it is also recovering - if (needsToRecover && message.type().equals(MessageType.JOIN)) { - continue; - } - if (!needsToRecover) while (!bufferedMessages.isEmpty()) handleMessageDelivery(bufferedMessages.poll()); @@ -249,10 +244,10 @@ private void handleVote(Message message) { } private void handleJoin(Message message) { - if (message.sender() == localId) return; + if (message.sender() == localId || needsToRecover) return; MissingEpochs missingEpochs = (MissingEpochs) message.content(); - LinkedList missingBlocks = blockchainManager.blocksFromToEpoch(missingEpochs.from(), missingEpochs.to()); + List missingBlocks = blockchainManager.blocksFromToEpoch(missingEpochs.from(), missingEpochs.to()); Message catchUp = new Message( MessageType.UPDATE, diff --git a/src/utils/application/Block.java b/src/utils/application/Block.java index d48332c..aa949e9 100644 --- a/src/utils/application/Block.java +++ b/src/utils/application/Block.java @@ -7,7 +7,7 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; -public record Block(byte[] parentHash, Integer epoch, Integer length, Transaction[] transactions) implements Content, Comparable { +public record Block(byte[] parentHash, Integer epoch, Integer length, Transaction[] transactions) implements Content { public Block(byte[] parentHash, Integer epoch, Integer length, Transaction[] transactions) { this.parentHash = parentHash; @@ -57,11 +57,6 @@ public int hashCode() { return result; } - @Override - public int compareTo(Block o) { - return epoch - o.epoch; - } - public String toStringSummary() { String partialParentHash = IntStream.range(0, Math.min(4, parentHash.length)) .mapToObj(i -> String.format("%02X", parentHash[i])) diff --git a/src/utils/application/CatchUp.java b/src/utils/application/CatchUp.java index 000d1ad..4ae5439 100644 --- a/src/utils/application/CatchUp.java +++ b/src/utils/application/CatchUp.java @@ -1,11 +1,13 @@ package utils.application; +import StreamletApp.BlockNode; import java.util.LinkedList; +import java.util.List; import java.util.Random; public record CatchUp( Integer slackerId, - LinkedList missingChain, + List missingChain, Random leaderRand, Integer leaderId, Integer currentEpoch @@ -13,7 +15,7 @@ public record CatchUp( public CatchUp( Integer slackerId, - LinkedList missingChain, + List missingChain, Random leaderRand, Integer leaderId, Integer currentEpoch From 7ee2875afde93b51f867851f521ca71b743a7bd9 Mon Sep 17 00:00:00 2001 From: Bruno Faustino Date: Thu, 13 Nov 2025 04:15:27 +0000 Subject: [PATCH 06/16] Change recovery so that it no longer blocks until receiving the first UPDATE message and processes all the blocks received from the other nodes, inserting only the ones that it has not seen before (if any) --- config.txt | 2 +- src/StreamletApp/StreamletNode.java | 50 +++++++++-------------------- src/utils/application/CatchUp.java | 19 ++--------- 3 files changed, 19 insertions(+), 52 deletions(-) diff --git a/config.txt b/config.txt index c2e183d..10ca320 100644 --- a/config.txt +++ b/config.txt @@ -6,7 +6,7 @@ P2P=127.0.0.1:54583 P2P=127.0.0.1:54584 # Agreed time for every server to start protocol dd-MM-yyyy HH:mm:ss -start=13-11-2025 02:27:00 +start=13-11-2025 04:01:00 # NORMAL and DEBUG mode for logs logLevel=DEBUG diff --git a/src/StreamletApp/StreamletNode.java b/src/StreamletApp/StreamletNode.java index 0920232..888d0b7 100644 --- a/src/StreamletApp/StreamletNode.java +++ b/src/StreamletApp/StreamletNode.java @@ -29,7 +29,7 @@ public class StreamletNode { private final int numberOfDistinctNodes; private final TransactionPoolSimulator transactionPoolSimulator; private final AtomicInteger currentEpoch = new AtomicInteger(0); - private final AtomicInteger currentLeaderId = new AtomicInteger(0); + private final Random random = new Random(1L); // To determine epoch leader private final int localId; private final URBNode urbNode; @@ -42,11 +42,9 @@ public class StreamletNode { private final ExecutorService executor = Executors.newCachedThreadPool(); private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); - private final CountDownLatch waitForCatchUp = new CountDownLatch(1); private final boolean isClientGeneratingTransactions; private final Address myClientAddress; - private Random random = new Random(1L); // To determine epoch leader private volatile boolean needsToRecover = false; public StreamletNode(PeerInfo localPeerInfo, List remotePeersInfo, int deltaInSeconds, @@ -122,11 +120,8 @@ private void catchUp(int fromEpoch, int toEpoch) { Message join = new Message(MessageType.JOIN, missingEpochs, localId); urbNode.broadcastFromLocal(join); - try { - waitForCatchUp.await(); - } catch (InterruptedException e) { - AppLogger.logError("Failed to wait for missing blocks - Interrupted", e); - } + // Update leader randomizer to reflect the same current state as the other nodes + for (int epoch = 0; epoch < toEpoch; epoch++) calculateLeaderId(epoch); } private void safeAdvanceEpoch() { @@ -139,12 +134,14 @@ private void safeAdvanceEpoch() { private void advanceEpoch() { int epoch = currentEpoch.get(); - // After catching up, this node receives the already advanced Random (if not in confusion epoch) - if (needsToRecover) catchUp(blockchainManager.getLastEpoch() + 1, epoch); - else calculateLeaderId(epoch); + if (needsToRecover) { + catchUp(blockchainManager.getLastEpoch() + 1, epoch); + needsToRecover = false; + } + int currentLeaderId = calculateLeaderId(epoch); AppLogger.logInfo("#### EPOCH = " + epoch + " LEADER = " + currentLeaderId + " ####"); - if (localId == currentLeaderId.get()) { + if (localId == currentLeaderId) { try { if (!isClientGeneratingTransactions || !clientPendingTransactionsQueue.isEmpty()) { AppLogger.logDebug("Node " + localId + " is leader: proposing new block"); @@ -172,15 +169,8 @@ private void consumeMessages() { continue; } - // Store new proposals until this node is done recovering - if (needsToRecover && message.type().equals(MessageType.PROPOSE)) { - bufferedMessages.add(message); - continue; - } - - if (!needsToRecover) - while (!bufferedMessages.isEmpty()) - handleMessageDelivery(bufferedMessages.poll()); + while (!bufferedMessages.isEmpty()) + handleMessageDelivery(bufferedMessages.poll()); handleMessageDelivery(message); } @@ -244,40 +234,32 @@ private void handleVote(Message message) { } private void handleJoin(Message message) { - if (message.sender() == localId || needsToRecover) return; + if (message.sender() == localId) return; MissingEpochs missingEpochs = (MissingEpochs) message.content(); List missingBlocks = blockchainManager.blocksFromToEpoch(missingEpochs.from(), missingEpochs.to()); Message catchUp = new Message( MessageType.UPDATE, - new CatchUp(message.sender(), missingBlocks, random, currentLeaderId.get(), currentEpoch.get()), + new CatchUp(message.sender(), missingBlocks, currentEpoch.get()), localId ); urbNode.broadcastFromLocal(catchUp); } private void handleUpdate(Message message) { - if (!needsToRecover) return; CatchUp catchUp = (CatchUp) message.content(); if (catchUp.slackerId() != localId) return; - - this.random = catchUp.leaderRand(); - this.currentLeaderId.set(catchUp.leaderId()); blockchainManager.insertMissingBlocks(catchUp.missingChain()); - - needsToRecover = false; - AppLogger.logInfo("Finished recovering"); - waitForCatchUp.countDown(); } private boolean inConfusionEpoch(int epoch) { return epoch >= CONFUSION_START && epoch <= CONFUSION_START + CONFUSION_DURATION - 1; } - private void calculateLeaderId(int epoch) { - if (inConfusionEpoch(epoch)) currentLeaderId.set(epoch % numberOfDistinctNodes); - else currentLeaderId.set(random.nextInt(numberOfDistinctNodes)); + private int calculateLeaderId(int epoch) { + return inConfusionEpoch(epoch) ? epoch % numberOfDistinctNodes + : random.nextInt(numberOfDistinctNodes); } private void receiveClientTransactionsRequests() { diff --git a/src/utils/application/CatchUp.java b/src/utils/application/CatchUp.java index 4ae5439..72ed70c 100644 --- a/src/utils/application/CatchUp.java +++ b/src/utils/application/CatchUp.java @@ -3,27 +3,12 @@ import StreamletApp.BlockNode; import java.util.LinkedList; import java.util.List; -import java.util.Random; -public record CatchUp( - Integer slackerId, - List missingChain, - Random leaderRand, - Integer leaderId, - Integer currentEpoch -) implements Content { +public record CatchUp(Integer slackerId, List missingChain, Integer currentEpoch) implements Content { - public CatchUp( - Integer slackerId, - List missingChain, - Random leaderRand, - Integer leaderId, - Integer currentEpoch - ) { + public CatchUp(Integer slackerId, List missingChain, Integer currentEpoch) { this.slackerId = slackerId; this.missingChain = new LinkedList<>(missingChain); - this.leaderRand = leaderRand; - this.leaderId = leaderId; this.currentEpoch = currentEpoch; } From e42e60a74cadce85770ed360ffd97dcb5b7b371e Mon Sep 17 00:00:00 2001 From: sousanamain Date: Wed, 3 Dec 2025 08:49:35 +0000 Subject: [PATCH 07/16] folders by standard convention shouldn't use PascalCase --- src/Streamlet.java | 2 +- src/{StreamletApp => app}/BlockNode.java | 5 +++-- src/{StreamletApp => app}/BlockchainManager.java | 2 +- src/{StreamletApp => app}/Hash.java | 6 +++--- src/{StreamletApp => app}/StreamletNode.java | 4 ++-- src/{StreamletApp => app}/TransactionPoolSimulator.java | 2 +- .../P2PNode.java | 7 +++++-- src/{URB => urb}/URBCallback.java | 2 +- src/{URB => urb}/URBNode.java | 4 ++-- src/utils/application/CatchUp.java | 3 ++- 10 files changed, 21 insertions(+), 16 deletions(-) rename src/{StreamletApp => app}/BlockNode.java (96%) rename src/{StreamletApp => app}/BlockchainManager.java (99%) rename src/{StreamletApp => app}/Hash.java (59%) rename src/{StreamletApp => app}/StreamletNode.java (99%) rename src/{StreamletApp => app}/TransactionPoolSimulator.java (97%) rename src/{GroupCommunication => groupcommunication}/P2PNode.java (98%) rename src/{URB => urb}/URBCallback.java (89%) rename src/{URB => urb}/URBNode.java (98%) diff --git a/src/Streamlet.java b/src/Streamlet.java index 942cfcf..4bd929f 100644 --- a/src/Streamlet.java +++ b/src/Streamlet.java @@ -1,4 +1,4 @@ -import StreamletApp.StreamletNode; +import app.StreamletNode; import utils.ConfigParser; import utils.communication.PeerInfo; import utils.logs.AppLogger; diff --git a/src/StreamletApp/BlockNode.java b/src/app/BlockNode.java similarity index 96% rename from src/StreamletApp/BlockNode.java rename to src/app/BlockNode.java index 1cc8820..abf5bd3 100644 --- a/src/StreamletApp/BlockNode.java +++ b/src/app/BlockNode.java @@ -1,8 +1,9 @@ -package StreamletApp; +package app; -import java.io.Serializable; import utils.application.Block; +import java.io.Serializable; + public class BlockNode implements Serializable { private final Block block; diff --git a/src/StreamletApp/BlockchainManager.java b/src/app/BlockchainManager.java similarity index 99% rename from src/StreamletApp/BlockchainManager.java rename to src/app/BlockchainManager.java index 1f65614..3f7d17a 100644 --- a/src/StreamletApp/BlockchainManager.java +++ b/src/app/BlockchainManager.java @@ -1,4 +1,4 @@ -package StreamletApp; +package app; import utils.application.Block; import utils.application.Transaction; diff --git a/src/StreamletApp/Hash.java b/src/app/Hash.java similarity index 59% rename from src/StreamletApp/Hash.java rename to src/app/Hash.java index faae324..9aba027 100644 --- a/src/StreamletApp/Hash.java +++ b/src/app/Hash.java @@ -1,4 +1,4 @@ -package StreamletApp; +package app; import java.util.Arrays; @@ -6,9 +6,9 @@ record Hash(byte[] hash) { @Override public boolean equals(Object o) { - if (!(o instanceof Hash hashOther)) return false; + if (!(o instanceof Hash(byte[] hash1))) return false; - return Arrays.equals(this.hash, hashOther.hash); + return Arrays.equals(this.hash, hash1); } @Override diff --git a/src/StreamletApp/StreamletNode.java b/src/app/StreamletNode.java similarity index 99% rename from src/StreamletApp/StreamletNode.java rename to src/app/StreamletNode.java index 888d0b7..a50efd5 100644 --- a/src/StreamletApp/StreamletNode.java +++ b/src/app/StreamletNode.java @@ -1,6 +1,6 @@ -package StreamletApp; +package app; -import URB.URBNode; +import urb.URBNode; import utils.application.*; import utils.communication.Address; import utils.communication.PeerInfo; diff --git a/src/StreamletApp/TransactionPoolSimulator.java b/src/app/TransactionPoolSimulator.java similarity index 97% rename from src/StreamletApp/TransactionPoolSimulator.java rename to src/app/TransactionPoolSimulator.java index f7f90cf..5cceadd 100644 --- a/src/StreamletApp/TransactionPoolSimulator.java +++ b/src/app/TransactionPoolSimulator.java @@ -1,4 +1,4 @@ -package StreamletApp; +package app; import utils.application.Transaction; diff --git a/src/GroupCommunication/P2PNode.java b/src/groupcommunication/P2PNode.java similarity index 98% rename from src/GroupCommunication/P2PNode.java rename to src/groupcommunication/P2PNode.java index 8a6f311..cf86045 100644 --- a/src/GroupCommunication/P2PNode.java +++ b/src/groupcommunication/P2PNode.java @@ -1,4 +1,4 @@ -package GroupCommunication; +package groupcommunication; import utils.application.Message; import utils.communication.KeyType; @@ -14,7 +14,10 @@ import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.*; -import java.util.concurrent.*; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.LinkedBlockingQueue; import java.util.function.Function; import java.util.stream.Collectors; diff --git a/src/URB/URBCallback.java b/src/urb/URBCallback.java similarity index 89% rename from src/URB/URBCallback.java rename to src/urb/URBCallback.java index 80f5c06..0ed0f2b 100644 --- a/src/URB/URBCallback.java +++ b/src/urb/URBCallback.java @@ -1,4 +1,4 @@ -package URB; +package urb; import utils.application.Message; diff --git a/src/URB/URBNode.java b/src/urb/URBNode.java similarity index 98% rename from src/URB/URBNode.java rename to src/urb/URBNode.java index af7948d..4faa7f1 100644 --- a/src/URB/URBNode.java +++ b/src/urb/URBNode.java @@ -1,6 +1,6 @@ -package URB; +package urb; -import GroupCommunication.P2PNode; +import groupcommunication.P2PNode; import utils.application.Message; import utils.application.MessageType; import utils.communication.MessageWithReceiver; diff --git a/src/utils/application/CatchUp.java b/src/utils/application/CatchUp.java index 72ed70c..94a11a0 100644 --- a/src/utils/application/CatchUp.java +++ b/src/utils/application/CatchUp.java @@ -1,6 +1,7 @@ package utils.application; -import StreamletApp.BlockNode; +import app.BlockNode; + import java.util.LinkedList; import java.util.List; From 348af01bd7400982a192f7bbb5eff2d2da9e91a8 Mon Sep 17 00:00:00 2001 From: sousanamain Date: Wed, 3 Dec 2025 08:50:33 +0000 Subject: [PATCH 08/16] reformat --- src/Streamlet.java | 2 +- src/app/BlockNode.java | 12 +++++++++--- src/app/BlockchainManager.java | 23 +++++++++++------------ src/app/StreamletNode.java | 13 +++++++------ src/utils/ConfigParser.java | 24 ++++++++++++------------ src/utils/application/MissingEpochs.java | 3 ++- 6 files changed, 42 insertions(+), 35 deletions(-) diff --git a/src/Streamlet.java b/src/Streamlet.java index 4bd929f..8ffdd6a 100644 --- a/src/Streamlet.java +++ b/src/Streamlet.java @@ -22,7 +22,7 @@ void main(String[] args) throws IOException, InterruptedException { List remotePeers = peerInfos.stream().filter(p -> p.id() != nodeId).toList(); AppLogger.logDebug(remotePeers.toString()); - + StreamletNode node = new StreamletNode(localPeer, remotePeers, 1, start, configData.isClientGeneratingTransactions, configData.servers.get(nodeId)); node.startProtocol(); } diff --git a/src/app/BlockNode.java b/src/app/BlockNode.java index abf5bd3..92df390 100644 --- a/src/app/BlockNode.java +++ b/src/app/BlockNode.java @@ -14,11 +14,17 @@ public BlockNode(Block block, Boolean finalized) { this.finalized = finalized; } - public Block block() {return block;} + public Block block() { + return block; + } - public Boolean finalized() {return finalized;} + public Boolean finalized() { + return finalized; + } - public void finalizeBlock() {finalized = true;} + public void finalizeBlock() { + finalized = true; + } @Override public boolean equals(Object o) { diff --git a/src/app/BlockchainManager.java b/src/app/BlockchainManager.java index 3f7d17a..3966618 100644 --- a/src/app/BlockchainManager.java +++ b/src/app/BlockchainManager.java @@ -49,11 +49,11 @@ private List findBiggestChainFromPredicate(Hash parentHash, Predicate findBiggestChainFromPredicate(new Hash(child.block().getSHA1()), predicate)) - .max(Comparator.comparing(chain -> chain.size())) - .orElseGet(() -> new LinkedList<>()) + blockchain.get(parentHash).stream() + .filter(predicate::test) + .map(child -> findBiggestChainFromPredicate(new Hash(child.block().getSHA1()), predicate)) + .max(Comparator.comparing(List::size)) + .orElseGet(LinkedList::new) ); return result; } @@ -127,7 +127,7 @@ private void finalizeFromTheNextBlocks(Hash parentHash) { private void finalizeFromConsecutivesOf(BlockNode block) { List beforeBlockInConsecutiveEpochs = new LinkedList<>(); List afterBlockInConsecutiveEpochs = new LinkedList<>(); - + // After block in consecutive epochs BlockNode currBlock = block; int currEpoch = block.block().epoch(); @@ -138,7 +138,7 @@ private void finalizeFromConsecutivesOf(BlockNode block) { Optional maybeChild = children.stream() .filter(blockNode -> blockNode.block().epoch() == currEpochCopy + 1) .findFirst(); - + if (maybeChild.isEmpty()) break; BlockNode child = maybeChild.get(); @@ -153,8 +153,8 @@ private void finalizeFromConsecutivesOf(BlockNode block) { for (int i = 1; i < FINALIZATION_MIN_SIZE; i++) { currBlock = hashToBlockNode.get(new Hash(currBlock.block().parentHash())); if (currBlock == null - || currBlock.block().epoch() != currEpoch - 1 - || currBlock.equals(new BlockNode(GENESIS_BLOCK, true))) + || currBlock.block().epoch() != currEpoch - 1 + || currBlock.equals(new BlockNode(GENESIS_BLOCK, true))) break; currEpoch--; @@ -174,9 +174,8 @@ private void finalizeFrom(BlockNode block) { BlockNode finalizationStarter = hashToBlockNode.get(new Hash(block.block().parentHash())); for (BlockNode currBlock = finalizationStarter; - !currBlock.block().equals(GENESIS_BLOCK); - currBlock = hashToBlockNode.get(new Hash(currBlock.block().parentHash()))) - { + !currBlock.block().equals(GENESIS_BLOCK); + currBlock = hashToBlockNode.get(new Hash(currBlock.block().parentHash()))) { if (currBlock.finalized()) break; currBlock.finalizeBlock(); } diff --git a/src/app/StreamletNode.java b/src/app/StreamletNode.java index a50efd5..f4923bb 100644 --- a/src/app/StreamletNode.java +++ b/src/app/StreamletNode.java @@ -67,7 +67,7 @@ public void startProtocol() throws InterruptedException { long epochDuration = 2L * deltaInSeconds; long nanoSecondsToWait = waitStartOrRecover(); scheduler.scheduleAtFixedRate( - this::safeAdvanceEpoch, nanoSecondsToWait, (long) (epochDuration*1e9), TimeUnit.NANOSECONDS + this::safeAdvanceEpoch, nanoSecondsToWait, (long) (epochDuration * 1e9), TimeUnit.NANOSECONDS ); } @@ -110,7 +110,7 @@ private long recover() { LocalDateTime nextEpochDate = start.plusSeconds(protocolAgeInSeconds + epochDuration); long nanoSecondsToWait = ChronoUnit.NANOS.between( - LocalDateTime.now(), nextEpochDate + LocalDateTime.now(), nextEpochDate ); return nanoSecondsToWait > 0 ? nanoSecondsToWait : 0; } @@ -205,7 +205,8 @@ private void handleMessageDelivery(Message message) { case VOTE -> handleVote(message); case JOIN -> handleJoin(message); case UPDATE -> handleUpdate(message); - default -> {} + default -> { + } } } @@ -240,9 +241,9 @@ private void handleJoin(Message message) { List missingBlocks = blockchainManager.blocksFromToEpoch(missingEpochs.from(), missingEpochs.to()); Message catchUp = new Message( - MessageType.UPDATE, - new CatchUp(message.sender(), missingBlocks, currentEpoch.get()), - localId + MessageType.UPDATE, + new CatchUp(message.sender(), missingBlocks, currentEpoch.get()), + localId ); urbNode.broadcastFromLocal(catchUp); } diff --git a/src/utils/ConfigParser.java b/src/utils/ConfigParser.java index 1c5a424..483a218 100644 --- a/src/utils/ConfigParser.java +++ b/src/utils/ConfigParser.java @@ -2,6 +2,7 @@ import utils.communication.Address; import utils.communication.PeerInfo; +import utils.logs.AppLogger; import utils.logs.LogLevel; import java.io.IOException; @@ -16,13 +17,12 @@ import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; -import utils.logs.AppLogger; public class ConfigParser { public final static String CONFIG_FILE = "config.txt"; public final static DateTimeFormatter START_FORMAT = DateTimeFormatter.ofPattern("dd-MM-yyyy HH:mm:ss"); - public final static LocalDateTime DEFAULT_START_DATE = - LocalDateTime.parse("01-01-2000 00:00:00", START_FORMAT); + public final static LocalDateTime DEFAULT_START_DATE = + LocalDateTime.parse("01-01-2000 00:00:00", START_FORMAT); private static final Pattern P2P_PATTERN = Pattern.compile("^P2P\\s*=\\s*(.+)$", Pattern.CASE_INSENSITIVE); private static final Pattern START_PATTERN = Pattern.compile("^start\\s*=\\s*(\\d{2}-\\d{2}-\\d{4} \\d{2}:\\d{2}:\\d{2})$", Pattern.CASE_INSENSITIVE); @@ -66,22 +66,22 @@ public static ConfigData parseConfig() throws IOException { return configData; } - public static class ConfigData { - public List peers = new ArrayList<>(); - public LocalDateTime start = DEFAULT_START_DATE; - public Map servers = new HashMap<>(); - public LogLevel logLevel = LogLevel.NORMAL; - public boolean isClientGeneratingTransactions = false; - } - private static LocalDateTime parseToDate(String dateStr) { try { return LocalDateTime.parse(dateStr, START_FORMAT); } catch (DateTimeParseException e) { AppLogger.logWarning( - "Invalid format for protocol start date. Default start date was used. Should be: dd-MM-yyyy HH:mm:ss." + "Invalid format for protocol start date. Default start date was used. Should be: dd-MM-yyyy HH:mm:ss." ); return DEFAULT_START_DATE; } } + + public static class ConfigData { + public List peers = new ArrayList<>(); + public LocalDateTime start = DEFAULT_START_DATE; + public Map servers = new HashMap<>(); + public LogLevel logLevel = LogLevel.NORMAL; + public boolean isClientGeneratingTransactions = false; + } } diff --git a/src/utils/application/MissingEpochs.java b/src/utils/application/MissingEpochs.java index ac6fc42..d4a463e 100644 --- a/src/utils/application/MissingEpochs.java +++ b/src/utils/application/MissingEpochs.java @@ -1,3 +1,4 @@ package utils.application; -public record MissingEpochs(Integer from, Integer to) implements Content {} +public record MissingEpochs(Integer from, Integer to) implements Content { +} From 39a4aea0ce05ce1e41882496f4c086ffc215d529 Mon Sep 17 00:00:00 2001 From: sousanamain Date: Wed, 3 Dec 2025 09:03:10 +0000 Subject: [PATCH 09/16] changed static class (config data) to be a record, also simplified a bit the logic of the parsing --- src/Streamlet.java | 8 +++--- src/StreamletClient.java | 4 +-- src/utils/ConfigParser.java | 55 ++++++++++++++++++++++--------------- 3 files changed, 39 insertions(+), 28 deletions(-) diff --git a/src/Streamlet.java b/src/Streamlet.java index 8ffdd6a..07e4747 100644 --- a/src/Streamlet.java +++ b/src/Streamlet.java @@ -14,15 +14,15 @@ void main(String[] args) throws IOException, InterruptedException { ConfigParser.ConfigData configData = ConfigParser.parseConfig(); - LocalDateTime start = configData.start; + LocalDateTime start = configData.start(); - List peerInfos = configData.peers; - AppLogger.updateLoggerLevel(configData.logLevel); + List peerInfos = configData.peers(); + AppLogger.updateLoggerLevel(configData.logLevel()); PeerInfo localPeer = peerInfos.get(nodeId); List remotePeers = peerInfos.stream().filter(p -> p.id() != nodeId).toList(); AppLogger.logDebug(remotePeers.toString()); - StreamletNode node = new StreamletNode(localPeer, remotePeers, 1, start, configData.isClientGeneratingTransactions, configData.servers.get(nodeId)); + StreamletNode node = new StreamletNode(localPeer, remotePeers, 1, start, configData.isClientGeneratingTransactions(), configData.servers().get(nodeId)); node.startProtocol(); } diff --git a/src/StreamletClient.java b/src/StreamletClient.java index 2ef22b0..5f0baa6 100644 --- a/src/StreamletClient.java +++ b/src/StreamletClient.java @@ -20,10 +20,10 @@ void main() throws IOException { })); ConfigParser.ConfigData configData = ConfigParser.parseConfig(); - AppLogger.updateLoggerLevel(configData.logLevel); + AppLogger.updateLoggerLevel(configData.logLevel()); userInput = new BufferedReader(new InputStreamReader(System.in)); - serverAddressees = ConfigParser.parseConfig().servers; + serverAddressees = ConfigParser.parseConfig().servers(); printInfoGui(); while (running) { diff --git a/src/utils/ConfigParser.java b/src/utils/ConfigParser.java index 483a218..3b23abe 100644 --- a/src/utils/ConfigParser.java +++ b/src/utils/ConfigParser.java @@ -1,3 +1,4 @@ + package utils; import utils.communication.Address; @@ -19,9 +20,9 @@ import java.util.regex.Pattern; public class ConfigParser { - public final static String CONFIG_FILE = "config.txt"; - public final static DateTimeFormatter START_FORMAT = DateTimeFormatter.ofPattern("dd-MM-yyyy HH:mm:ss"); - public final static LocalDateTime DEFAULT_START_DATE = + public static final String CONFIG_FILE = "config.txt"; + public static final DateTimeFormatter START_FORMAT = DateTimeFormatter.ofPattern("dd-MM-yyyy HH:mm:ss"); + public static final LocalDateTime DEFAULT_START_DATE = LocalDateTime.parse("01-01-2000 00:00:00", START_FORMAT); private static final Pattern P2P_PATTERN = Pattern.compile("^P2P\\s*=\\s*(.+)$", Pattern.CASE_INSENSITIVE); @@ -31,14 +32,19 @@ public class ConfigParser { private static final Pattern TRANSACTION_MODE_PATTERN = Pattern.compile("^transactionsMode\\s*=\\s*(.+)$", Pattern.CASE_INSENSITIVE); public static ConfigData parseConfig() throws IOException { - ConfigData configData = new ConfigData(); + List peers = new ArrayList<>(); + LocalDateTime start = DEFAULT_START_DATE; + Map servers = new HashMap<>(); + LogLevel logLevel = LogLevel.NORMAL; + boolean isClientGeneratingTransactions = false; + List lines = Files.readAllLines(Paths.get(CONFIG_FILE)); int peerIndex = 0; int serverIndex = 0; for (String line : lines) { line = line.trim(); - if (line.isEmpty() || line.startsWith("#")) continue; // skip empty lines or comments + if (line.isEmpty() || line.startsWith("#")) continue; Matcher p2pMatcher = P2P_PATTERN.matcher(line); Matcher startMatcher = START_PATTERN.matcher(line); @@ -47,23 +53,19 @@ public static ConfigData parseConfig() throws IOException { Matcher transactionMatcher = TRANSACTION_MODE_PATTERN.matcher(line); if (p2pMatcher.matches()) { - configData.peers.add(new PeerInfo(peerIndex++, Address.fromString(p2pMatcher.group(1).trim()))); + peers.add(new PeerInfo(peerIndex++, Address.fromString(p2pMatcher.group(1).trim()))); } else if (startMatcher.matches()) { - configData.start = parseToDate(startMatcher.group(1).trim()); + start = parseToDate(startMatcher.group(1).trim()); } else if (serverMatcher.matches()) { - configData.servers.put(serverIndex++, Address.fromString(serverMatcher.group(1).trim())); + servers.put(serverIndex++, Address.fromString(serverMatcher.group(1).trim())); } else if (logLevelMatcher.matches()) { - try { - configData.logLevel = LogLevel.valueOf(logLevelMatcher.group(1).trim().toUpperCase()); - } catch (IllegalArgumentException ignored) { - configData.logLevel = LogLevel.NORMAL; - } + logLevel = parseLogLevel(logLevelMatcher.group(1).trim()); } else if (transactionMatcher.matches()) { - configData.isClientGeneratingTransactions = transactionMatcher.group(1).trim().equalsIgnoreCase("CLIENT"); + isClientGeneratingTransactions = transactionMatcher.group(1).trim().equalsIgnoreCase("CLIENT"); } } - return configData; + return new ConfigData(peers, start, servers, logLevel, isClientGeneratingTransactions); } private static LocalDateTime parseToDate(String dateStr) { @@ -77,11 +79,20 @@ private static LocalDateTime parseToDate(String dateStr) { } } - public static class ConfigData { - public List peers = new ArrayList<>(); - public LocalDateTime start = DEFAULT_START_DATE; - public Map servers = new HashMap<>(); - public LogLevel logLevel = LogLevel.NORMAL; - public boolean isClientGeneratingTransactions = false; + private static LogLevel parseLogLevel(String levelStr) { + try { + return LogLevel.valueOf(levelStr.toUpperCase()); + } catch (IllegalArgumentException ignored) { + return LogLevel.NORMAL; + } + } + + public record ConfigData( + List peers, + LocalDateTime start, + Map servers, + LogLevel logLevel, + boolean isClientGeneratingTransactions + ) { } -} +} \ No newline at end of file From 83ee8ed8baabecffe95e762345e6c2ef45baca9b Mon Sep 17 00:00:00 2001 From: sousanamain Date: Wed, 3 Dec 2025 09:48:26 +0000 Subject: [PATCH 10/16] reviewed and done some minor changes on fixed code --- src/app/BlockchainManager.java | 264 +++++++++--------- src/app/StreamletNode.java | 271 ++++++++++--------- src/{app => utils/application}/Hash.java | 4 +- src/utils/application/MissingEpochRange.java | 4 + src/utils/application/MissingEpochs.java | 4 - 5 files changed, 292 insertions(+), 255 deletions(-) rename src/{app => utils/application}/Hash.java (82%) create mode 100644 src/utils/application/MissingEpochRange.java delete mode 100644 src/utils/application/MissingEpochs.java diff --git a/src/app/BlockchainManager.java b/src/app/BlockchainManager.java index 3966618..34d1dcb 100644 --- a/src/app/BlockchainManager.java +++ b/src/app/BlockchainManager.java @@ -1,6 +1,8 @@ + package app; import utils.application.Block; +import utils.application.Hash; import utils.application.Transaction; import utils.logs.AppLogger; @@ -14,201 +16,208 @@ public class BlockchainManager { private static final Block GENESIS_BLOCK = new Block(new byte[SHA1_LENGTH], 0, 0, new Transaction[0]); - private final Map> blockchain = new HashMap<>(); - private final Map hashToBlockNode = new HashMap<>(); - private final Set blocksForRecovery = new HashSet<>(); - private final Set pendingProposes = new HashSet<>(); + private final Map> blockchainByParentHash = new HashMap<>(); + private final Map blockNodesByHash = new HashMap<>(); + private final Set recoveredBlocks = new HashSet<>(); + private final Set pendingProposals = new HashSet<>(); + private final Hash genesisParentHash; - private int mostRecentEpoch = -1; + private int mostRecentNotarizedEpoch = -1; public BlockchainManager() { - BlockNode genesis = new BlockNode(GENESIS_BLOCK, true); - Hash genesisParentHash = new Hash(GENESIS_BLOCK.parentHash()); + BlockNode genesisNode = new BlockNode(GENESIS_BLOCK, true); + genesisParentHash = new Hash(GENESIS_BLOCK.parentHash()); Hash genesisHash = new Hash(GENESIS_BLOCK.getSHA1()); - List root = new LinkedList<>(); - root.add(genesis); - blockchain.put(genesisParentHash, root); - blockchain.put(genesisHash, new LinkedList<>()); + List genesisRoot = new LinkedList<>(); + genesisRoot.add(genesisNode); + blockchainByParentHash.put(genesisParentHash, genesisRoot); + blockchainByParentHash.put(genesisHash, new LinkedList<>()); - hashToBlockNode.put(genesisHash, genesis); + blockNodesByHash.put(genesisHash, genesisNode); } public List getBiggestNotarizedChain() { - return findBiggestChainFromPredicate(new Hash(GENESIS_BLOCK.parentHash()), _ -> true); + return findBiggestChainMatching(genesisParentHash, _ -> true); } public List getBiggestFinalizedChain() { - return findBiggestChainFromPredicate(new Hash(GENESIS_BLOCK.parentHash()), BlockNode::finalized); + return findBiggestChainMatching(genesisParentHash, BlockNode::finalized); } - private List findBiggestChainFromPredicate(Hash parentHash, Predicate predicate) { - List result = new LinkedList<>(); + private List findBiggestChainMatching(Hash parentHash, Predicate predicate) { + List chain = new LinkedList<>(); - if (!parentHash.equals(new Hash(GENESIS_BLOCK.parentHash()))) - result.add(hashToBlockNode.get(parentHash).block()); + if (!parentHash.equals(genesisParentHash)) { + chain.add(blockNodesByHash.get(parentHash).block()); + } - result.addAll( - blockchain.get(parentHash).stream() - .filter(predicate::test) - .map(child -> findBiggestChainFromPredicate(new Hash(child.block().getSHA1()), predicate)) + chain.addAll( + blockchainByParentHash.get(parentHash).stream() + .filter(predicate) + .map(child -> findBiggestChainMatching(new Hash(child.block().getSHA1()), predicate)) .max(Comparator.comparing(List::size)) .orElseGet(LinkedList::new) ); - return result; + return chain; } public boolean onPropose(Block proposedBlock) { - // Check if this block is strictly longer than any notarized chain this block has seen thus far - boolean isStrictlyLonger = blockchain.keySet().stream() - .filter(parentHash -> blockchain.get(parentHash).isEmpty()) - .anyMatch(hash -> proposedBlock.length() > hashToBlockNode.get(hash).block().length()); - if (!isStrictlyLonger) { + boolean isLongerThanAnyChain = blockchainByParentHash.keySet().stream() + .filter(parentHash -> blockchainByParentHash.get(parentHash).isEmpty()) + .map(blockNodesByHash::get) + .filter(Objects::nonNull) + .anyMatch(blockNode -> proposedBlock.length() > blockNode.block().length()); + + if (!isLongerThanAnyChain) { return false; } - pendingProposes.add(proposedBlock); - // While recovering from crash, a node will likely receive propose messages - // even if it still does not have some of the previous blocks it needs - // so this block is placed in the data structure anyway and - // it can be linked with the others later when/if this node receives the previous blocks + pendingProposals.add(proposedBlock); + Hash blockHash = new Hash(proposedBlock.getSHA1()); BlockNode blockNode = new BlockNode(proposedBlock, false); - hashToBlockNode.put(blockHash, blockNode); + blockNodesByHash.put(blockHash, blockNode); return true; } - public void notarizeBlock(Block headerBlock) { - // The same block with or without its transactions are considered equal - Block fullBlock = null; - for (Block pendingBlock : pendingProposes) { - if (headerBlock.equals(pendingBlock)) { - fullBlock = pendingBlock; - break; - } - } + public void notarizeBlock(Block blockHeader) { + Block fullBlock = pendingProposals.stream() + .filter(blockHeader::equals) + .findFirst() + .orElse(null); if (fullBlock == null) return; - // Add to the blockchain and prepare another entry with the hash of this block as parent - // if there was not one already due to out of order proposals Hash parentHash = new Hash(fullBlock.parentHash()); Hash blockHash = new Hash(fullBlock.getSHA1()); - BlockNode block = hashToBlockNode.get(blockHash); - blockchain.computeIfAbsent(parentHash, _ -> new LinkedList<>()) - .add(block); - blockchain.computeIfAbsent(blockHash, _ -> new LinkedList<>()); + BlockNode blockNode = blockNodesByHash.get(blockHash); - if (headerBlock.epoch() > mostRecentEpoch) mostRecentEpoch = headerBlock.epoch(); + blockchainByParentHash.computeIfAbsent(parentHash, _ -> new LinkedList<>()) + .add(blockNode); + blockchainByParentHash.computeIfAbsent(blockHash, _ -> new LinkedList<>()); - pendingProposes.remove(headerBlock); + if (blockHeader.epoch() > mostRecentNotarizedEpoch) { + mostRecentNotarizedEpoch = blockHeader.epoch(); + } + + pendingProposals.remove(blockHeader); - AppLogger.logInfo("Block notarized: epoch " + headerBlock.epoch() + " length " + headerBlock.length()); - tryToFinalizeChain(block); + AppLogger.logInfo("Block notarized: epoch " + blockHeader.epoch() + " length " + blockHeader.length()); + finalizeAndPropagate(blockNode); } - private void tryToFinalizeChain(BlockNode block) { - // For every child of this block or of the next blocks, finalize the chain if the child is finalized - finalizeFromTheNextBlocks(new Hash(block.block().getSHA1())); + private void finalizeAndPropagate(BlockNode targetBlock) { + propagateFinalizedStatusDownstream(new Hash(targetBlock.block().getSHA1())); + finalizeByConsecutiveEpochBlocks(targetBlock); + } - // Finds an epoch interval of blocks from the epoch of this block +- - // the min consecutive blocks needed to finalize a chain. - // If this interval of consecutive blocks has enough size, - // finalize the chain until the last block of this interval - finalizeFromConsecutivesOf(block); + private void propagateFinalizedStatusDownstream(Hash parentHash) { + for (BlockNode child : blockchainByParentHash.get(parentHash)) { + if (child.finalized()) { + finalizeChainUpstream(child); + } + propagateFinalizedStatusDownstream(new Hash(child.block().getSHA1())); + } } - private void finalizeFromTheNextBlocks(Hash parentHash) { - for (BlockNode child : blockchain.get(parentHash)) { - if (child.finalized()) finalizeFrom(child); - finalizeFromTheNextBlocks(new Hash(child.block().getSHA1())); + private void finalizeByConsecutiveEpochBlocks(BlockNode anchorBlock) { + List blocksBefore = collectPrecedingConsecutiveBlocks(anchorBlock); + List blocksAfter = collectFollowingConsecutiveBlocks(anchorBlock); + + List finalizationCandidate = new LinkedList<>(blocksBefore); + finalizationCandidate.add(anchorBlock); + finalizationCandidate.addAll(blocksAfter); + + if (finalizationCandidate.size() >= FINALIZATION_MIN_SIZE) { + finalizeChainUpstream(finalizationCandidate.getLast()); } } - private void finalizeFromConsecutivesOf(BlockNode block) { - List beforeBlockInConsecutiveEpochs = new LinkedList<>(); - List afterBlockInConsecutiveEpochs = new LinkedList<>(); + private List collectFollowingConsecutiveBlocks(BlockNode startBlock) { + List consecutiveBlocks = new LinkedList<>(); + BlockNode currentBlock = startBlock; + int currentEpoch = startBlock.block().epoch(); - // After block in consecutive epochs - BlockNode currBlock = block; - int currEpoch = block.block().epoch(); for (int i = 1; i < FINALIZATION_MIN_SIZE; i++) { - List children = blockchain.get(new Hash(currBlock.block().getSHA1())); + List children = blockchainByParentHash.get(new Hash(currentBlock.block().getSHA1())); - final int currEpochCopy = currEpoch; - Optional maybeChild = children.stream() - .filter(blockNode -> blockNode.block().epoch() == currEpochCopy + 1) + int targetEpoch = currentEpoch + 1; + Optional nextBlock = children.stream() + .filter(blockNode -> blockNode.block().epoch() == targetEpoch) .findFirst(); - if (maybeChild.isEmpty()) break; - BlockNode child = maybeChild.get(); + if (nextBlock.isEmpty()) break; - currEpoch++; - afterBlockInConsecutiveEpochs.add(child); - currBlock = child; + BlockNode child = nextBlock.get(); + currentEpoch++; + consecutiveBlocks.add(child); + currentBlock = child; } - // Before block in consecutive epochs - currBlock = block; - currEpoch = block.block().epoch(); + return consecutiveBlocks; + } + + private List collectPrecedingConsecutiveBlocks(BlockNode startBlock) { + List consecutiveBlocks = new LinkedList<>(); + BlockNode currentBlock = startBlock; + int currentEpoch = startBlock.block().epoch(); + for (int i = 1; i < FINALIZATION_MIN_SIZE; i++) { - currBlock = hashToBlockNode.get(new Hash(currBlock.block().parentHash())); - if (currBlock == null - || currBlock.block().epoch() != currEpoch - 1 - || currBlock.equals(new BlockNode(GENESIS_BLOCK, true))) + BlockNode parentBlock = blockNodesByHash.get(new Hash(currentBlock.block().parentHash())); + + if (parentBlock == null + || parentBlock.block().epoch() != currentEpoch - 1 + || isGenesis(parentBlock)) { break; + } - currEpoch--; - beforeBlockInConsecutiveEpochs.add(currBlock); + currentEpoch--; + consecutiveBlocks.add(parentBlock); + currentBlock = parentBlock; } - // Build interval of consecutive blocks - List finalizeInterval = new LinkedList<>(); - finalizeInterval.addAll(beforeBlockInConsecutiveEpochs); - finalizeInterval.add(block); - finalizeInterval.addAll(afterBlockInConsecutiveEpochs); - - if (finalizeInterval.size() >= FINALIZATION_MIN_SIZE) finalizeFrom(finalizeInterval.getLast()); + return consecutiveBlocks; } - private void finalizeFrom(BlockNode block) { - BlockNode finalizationStarter = hashToBlockNode.get(new Hash(block.block().parentHash())); + private void finalizeChainUpstream(BlockNode anchorBlock) { + BlockNode parentBlock = blockNodesByHash.get(new Hash(anchorBlock.block().parentHash())); - for (BlockNode currBlock = finalizationStarter; - !currBlock.block().equals(GENESIS_BLOCK); - currBlock = hashToBlockNode.get(new Hash(currBlock.block().parentHash()))) { - if (currBlock.finalized()) break; - currBlock.finalizeBlock(); + for (BlockNode currentBlock = parentBlock; + currentBlock != null && !isGenesis(currentBlock); + currentBlock = blockNodesByHash.get(new Hash(currentBlock.block().parentHash()))) { + if (currentBlock.finalized()) break; + currentBlock.finalizeBlock(); } } - public int getLastEpoch() { - return mostRecentEpoch; + private boolean isGenesis(BlockNode block) { + return block.block().equals(GENESIS_BLOCK); + } + + public int getLastNotarizedEpoch() { + return mostRecentNotarizedEpoch; } - public List blocksFromToEpoch(int from, int to) { - return blockchain.values().stream() + public List getBlocksInEpochRange(int fromEpoch, int toEpoch) { + return blockchainByParentHash.values().stream() .flatMap(List::stream) .sorted(Comparator.comparing(block -> block.block().epoch())) - .dropWhile(block -> block.block().epoch() < from) - .takeWhile(block -> block.block().epoch() < to) + .dropWhile(block -> block.block().epoch() < fromEpoch) + .takeWhile(block -> block.block().epoch() < toEpoch) .toList(); } public void insertMissingBlocks(List missingBlocks) { - // It is possible that a proposed block that arrives to the blockchain before calling this method - // would supposingly finalize any of these missing blocks but in that case, - // then they will eventually be finalized because the next proposed blocks - // extend from the biggest chain which would have to be this one according to the consistency proof - for (BlockNode missingBlock : missingBlocks) { - // Ignore if it was already inserted in the blockchain from a previous UPDATE message - if (!blocksForRecovery.add(missingBlock)) continue; - - Hash parentHash = new Hash(missingBlock.block().parentHash()); - Hash blockHash = new Hash(missingBlock.block().getSHA1()); - blockchain.computeIfAbsent(parentHash, _ -> new LinkedList<>()) - .add(missingBlock); - blockchain.computeIfAbsent(blockHash, _ -> new LinkedList<>()); - hashToBlockNode.put(blockHash, missingBlock); + for (BlockNode blockNode : missingBlocks) { + if (!recoveredBlocks.add(blockNode)) continue; + + Hash parentHash = new Hash(blockNode.block().parentHash()); + Hash blockHash = new Hash(blockNode.block().getSHA1()); + + blockchainByParentHash.computeIfAbsent(parentHash, _ -> new LinkedList<>()) + .add(blockNode); + blockchainByParentHash.computeIfAbsent(blockHash, _ -> new LinkedList<>()); + blockNodesByHash.put(blockHash, blockNode); } } @@ -219,9 +228,9 @@ public void printBiggestFinalizedChain() { String header = "=== LONGEST FINALIZED CHAIN ==="; String border = "=".repeat(header.length()); - List biggestFinalizedChain = getBiggestFinalizedChain(); + List finalizedChain = getBiggestFinalizedChain(); - String chainString = biggestFinalizedChain.stream() + String chainString = finalizedChain.stream() .skip(1) .map(block -> "%sBlock[%d-%d]%s".formatted(GREEN, block.epoch(), block.length(), RESET)) .collect(Collectors.joining(" <- ", "%sGENESIS%s <- ".formatted(GREEN, RESET), "")); @@ -231,7 +240,7 @@ public void printBiggestFinalizedChain() { border, header, border, - biggestFinalizedChain.size() == 1 ? "No Finalized Chain Yet" : chainString, + finalizedChain.size() == 1 ? "No Finalized Chain Yet" : chainString, border ); @@ -240,6 +249,5 @@ public void printBiggestFinalizedChain() { AppLogger.logInfo(line); } } - } -} +} \ No newline at end of file diff --git a/src/app/StreamletNode.java b/src/app/StreamletNode.java index f4923bb..bdd03f1 100644 --- a/src/app/StreamletNode.java +++ b/src/app/StreamletNode.java @@ -1,3 +1,4 @@ + package app; import urb.URBNode; @@ -21,57 +22,56 @@ record SeenProposal(int leader, int epoch) { } public class StreamletNode { - public static final int BLOCK_CHAIN_PRINT_EPOCH_FREQUENCY = 5; - private static final int CONFUSION_START = 0; - private static final int CONFUSION_DURATION = 2; + public static final int BLOCKCHAIN_PRINT_EPOCH_INTERVAL = 5; + private static final int CONFUSION_EPOCH_START = 0; + private static final int CONFUSION_EPOCH_DURATION = 2; private final int deltaInSeconds; - private final LocalDateTime start; - private final int numberOfDistinctNodes; + private final LocalDateTime protocolStartTime; + private final int numberOfNodes; private final TransactionPoolSimulator transactionPoolSimulator; private final AtomicInteger currentEpoch = new AtomicInteger(0); - private final Random random = new Random(1L); // To determine epoch leader + private final Random epochLeaderRandomizer = new Random(1L); - private final int localId; + private final int localNodeId; private final URBNode urbNode; private final BlockchainManager blockchainManager; - private final Map> votedBlocks = new HashMap<>(); - private final BlockingQueue derivableQueue = new LinkedBlockingQueue<>(1000); + private final Map> blockVotes = new HashMap<>(); + private final BlockingQueue deliveredMessagesQueue = new LinkedBlockingQueue<>(1000); private final Set seenProposals = new HashSet<>(); - private final ConcurrentLinkedQueue clientPendingTransactionsQueue = new ConcurrentLinkedQueue<>(); - + private final ConcurrentLinkedQueue pendingClientTransactions = new ConcurrentLinkedQueue<>(); private final ExecutorService executor = Executors.newCachedThreadPool(); - private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + private final ScheduledExecutorService epochScheduler = Executors.newSingleThreadScheduledExecutor(); private final boolean isClientGeneratingTransactions; - private final Address myClientAddress; + private final Address clientServerAddress; private volatile boolean needsToRecover = false; public StreamletNode(PeerInfo localPeerInfo, List remotePeersInfo, int deltaInSeconds, - LocalDateTime start, boolean isClientGeneratingTransactions, Address myClientAddress) + LocalDateTime protocolStartTime, boolean isClientGeneratingTransactions, Address clientServerAddress) throws IOException { - localId = localPeerInfo.id(); - numberOfDistinctNodes = 1 + remotePeersInfo.size(); + localNodeId = localPeerInfo.id(); + numberOfNodes = 1 + remotePeersInfo.size(); this.deltaInSeconds = deltaInSeconds; - this.start = start; - transactionPoolSimulator = new TransactionPoolSimulator(numberOfDistinctNodes); + this.protocolStartTime = protocolStartTime; + transactionPoolSimulator = new TransactionPoolSimulator(numberOfNodes); blockchainManager = new BlockchainManager(); - urbNode = new URBNode(localPeerInfo, remotePeersInfo, derivableQueue::add); + urbNode = new URBNode(localPeerInfo, remotePeersInfo, deliveredMessagesQueue::add); this.isClientGeneratingTransactions = isClientGeneratingTransactions; - this.myClientAddress = myClientAddress; + this.clientServerAddress = clientServerAddress; } - public void startProtocol() throws InterruptedException { - launchThreads(); + public void startProtocol() { + launchBackgroundThreads(); - long epochDuration = 2L * deltaInSeconds; - long nanoSecondsToWait = waitStartOrRecover(); - scheduler.scheduleAtFixedRate( - this::safeAdvanceEpoch, nanoSecondsToWait, (long) (epochDuration * 1e9), TimeUnit.NANOSECONDS + long epochDurationNanos = 2L * deltaInSeconds * 1_000_000_000L; + long delayUntilFirstEpochNanos = calculateDelayUntilFirstEpoch(); + epochScheduler.scheduleAtFixedRate( + this::safeAdvanceEpoch, delayUntilFirstEpochNanos, epochDurationNanos, TimeUnit.NANOSECONDS ); } - private void launchThreads() { + private void launchBackgroundThreads() { executor.submit(() -> { try { urbNode.startURBNode(); @@ -80,48 +80,57 @@ private void launchThreads() { } }); - executor.submit(this::consumeMessages); - if (isClientGeneratingTransactions) executor.submit(this::receiveClientTransactionsRequests); + executor.submit(this::processDeliveredMessages); + if (isClientGeneratingTransactions) { + executor.submit(this::acceptClientTransactions); + } } - private long waitStartOrRecover() { + private long calculateDelayUntilFirstEpoch() { LocalDateTime now = LocalDateTime.now(); - if (now.isBefore(start)) return waitStart(); - if (now.isAfter(start)) return recover(); + if (now.isBefore(protocolStartTime)) { + return calculateDelayUntilProtocolStart(); + } + if (now.isAfter(protocolStartTime)) { + return recoverAndGetDelayToNextEpoch(); + } return 0; } - private long waitStart() { + private long calculateDelayUntilProtocolStart() { AppLogger.logInfo("Waiting for protocol to start..."); - long nanoSecondsToWait = ChronoUnit.NANOS.between(LocalDateTime.now(), start); - return nanoSecondsToWait > 0 ? nanoSecondsToWait : 0; + long delayNanos = ChronoUnit.NANOS.between(LocalDateTime.now(), protocolStartTime); + return Math.max(delayNanos, 0); } - private long recover() { - AppLogger.logInfo("(Re)joining in late..."); + private long recoverAndGetDelayToNextEpoch() { + AppLogger.logInfo("(Re)joining protocol that has already started..."); needsToRecover = true; - int epochDuration = 2 * this.deltaInSeconds; - long protocolAgeInSeconds = ChronoUnit.SECONDS.between(start, LocalDateTime.now()); - // Ignore the seconds spent on the current epoch - protocolAgeInSeconds -= protocolAgeInSeconds % epochDuration; - int ongoingEpoch = (int) (protocolAgeInSeconds / epochDuration); - currentEpoch.compareAndSet(0, ongoingEpoch + 1); + int epochDurationSeconds = 2 * this.deltaInSeconds; + long elapsedSeconds = ChronoUnit.SECONDS.between(protocolStartTime, LocalDateTime.now()); - LocalDateTime nextEpochDate = start.plusSeconds(protocolAgeInSeconds + epochDuration); - long nanoSecondsToWait = ChronoUnit.NANOS.between( - LocalDateTime.now(), nextEpochDate - ); - return nanoSecondsToWait > 0 ? nanoSecondsToWait : 0; + long completedEpochsSeconds = elapsedSeconds - (elapsedSeconds % epochDurationSeconds); + int completedEpochs = (int) (completedEpochsSeconds / epochDurationSeconds); + currentEpoch.compareAndSet(0, completedEpochs + 1); + + LocalDateTime nextEpochStartTime = protocolStartTime.plusSeconds(completedEpochsSeconds + epochDurationSeconds); + long delayToNextEpochNanos = ChronoUnit.NANOS.between(LocalDateTime.now(), nextEpochStartTime); + return Math.max(delayToNextEpochNanos, 0); } - private void catchUp(int fromEpoch, int toEpoch) { - MissingEpochs missingEpochs = new MissingEpochs(fromEpoch, toEpoch); - Message join = new Message(MessageType.JOIN, missingEpochs, localId); - urbNode.broadcastFromLocal(join); + private void requestMissingBlocksFromPeers(int fromEpoch, int toEpoch) { + MissingEpochRange missingEpochRange = new MissingEpochRange(fromEpoch, toEpoch); + Message joinRequest = new Message(MessageType.JOIN, missingEpochRange, localNodeId); + urbNode.broadcastFromLocal(joinRequest); - // Update leader randomizer to reflect the same current state as the other nodes - for (int epoch = 0; epoch < toEpoch; epoch++) calculateLeaderId(epoch); + synchronizeEpochWithPeers(toEpoch); + } + + private void synchronizeEpochWithPeers(int targetEpoch) { + for (int epoch = 0; epoch < targetEpoch; epoch++) { + determineEpochLeader(epoch); + } } private void safeAdvanceEpoch() { @@ -134,45 +143,50 @@ private void safeAdvanceEpoch() { private void advanceEpoch() { int epoch = currentEpoch.get(); + if (needsToRecover) { - catchUp(blockchainManager.getLastEpoch() + 1, epoch); + requestMissingBlocksFromPeers(blockchainManager.getLastNotarizedEpoch() + 1, epoch); needsToRecover = false; } - int currentLeaderId = calculateLeaderId(epoch); - AppLogger.logInfo("#### EPOCH = " + epoch + " LEADER = " + currentLeaderId + " ####"); - if (localId == currentLeaderId) { + int epochLeader = determineEpochLeader(epoch); + AppLogger.logInfo("#### EPOCH = " + epoch + " LEADER = " + epochLeader + " ####"); + + if (localNodeId == epochLeader) { try { - if (!isClientGeneratingTransactions || !clientPendingTransactionsQueue.isEmpty()) { - AppLogger.logDebug("Node " + localId + " is leader: proposing new block"); + if (!isClientGeneratingTransactions || !pendingClientTransactions.isEmpty()) { + AppLogger.logDebug("Node " + localNodeId + " is leader: proposing new block"); proposeNewBlock(epoch); } - proposeNewBlock(epoch); } catch (NoSuchAlgorithmException e) { AppLogger.logError("Error proposing new block: " + e.getMessage(), e); } } - if (epoch != 0 && epoch % BLOCK_CHAIN_PRINT_EPOCH_FREQUENCY == 0) + + if (epoch != 0 && epoch % BLOCKCHAIN_PRINT_EPOCH_INTERVAL == 0) { blockchainManager.printBiggestFinalizedChain(); + } + currentEpoch.incrementAndGet(); } - private void consumeMessages() { + private void processDeliveredMessages() { final Queue bufferedMessages = new LinkedList<>(); try { while (true) { - Message message = derivableQueue.poll(100, TimeUnit.MILLISECONDS); + Message message = deliveredMessagesQueue.poll(100, TimeUnit.MILLISECONDS); if (message == null) continue; - if (inConfusionEpoch(currentEpoch.get())) { + if (isInConfusionPhase(currentEpoch.get())) { bufferedMessages.add(message); continue; } - while (!bufferedMessages.isEmpty()) - handleMessageDelivery(bufferedMessages.poll()); + while (!bufferedMessages.isEmpty()) { + processMessage(bufferedMessages.poll()); + } - handleMessageDelivery(message); + processMessage(message); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); @@ -181,102 +195,117 @@ private void consumeMessages() { } private void proposeNewBlock(int epoch) throws NoSuchAlgorithmException { - Block parent = blockchainManager.getBiggestNotarizedChain().getLast(); - Transaction[] transactions; + Block parentBlock = blockchainManager.getBiggestNotarizedChain().getLast(); + Transaction[] transactions = collectBlockTransactions(); + + Block newBlock = new Block( + parentBlock.getSHA1(), + epoch, + parentBlock.length() + 1, + transactions + ); + AppLogger.logDebug("Proposed block: " + newBlock + " with transactions: " + Arrays.toString(transactions)); + urbNode.broadcastFromLocal(new Message(MessageType.PROPOSE, newBlock, localNodeId)); + } + + private Transaction[] collectBlockTransactions() { if (isClientGeneratingTransactions) { - transactions = new Transaction[clientPendingTransactionsQueue.size()]; - int i = 0; - while (!clientPendingTransactionsQueue.isEmpty()) { - transactions[i++] = clientPendingTransactionsQueue.poll(); + Transaction[] transactions = new Transaction[pendingClientTransactions.size()]; + int index = 0; + while (!pendingClientTransactions.isEmpty()) { + transactions[index++] = pendingClientTransactions.poll(); } + return transactions; } else { - transactions = transactionPoolSimulator.generateTransactions(); + return transactionPoolSimulator.generateTransactions(); } - - Block newBlock = new Block(parent.getSHA1(), epoch, parent.length() + 1, transactions); - AppLogger.logDebug("Proposed block: " + newBlock + " with transactions: " + Arrays.toString(transactions)); - urbNode.broadcastFromLocal(new Message(MessageType.PROPOSE, newBlock, localId)); } - private void handleMessageDelivery(Message message) { - AppLogger.logDebug("Delivering message from " + message.sender() + ": " + message.type()); + private void processMessage(Message message) { + AppLogger.logDebug("Processing message from " + message.sender() + ": " + message.type()); switch (message.type()) { - case PROPOSE -> handlePropose(message); - case VOTE -> handleVote(message); - case JOIN -> handleJoin(message); - case UPDATE -> handleUpdate(message); - default -> { - } + case JOIN -> handleJoinRequest(message); + case PROPOSE -> handleProposalMessage(message); + case VOTE -> handleVoteMessage(message); + case UPDATE -> handleUpdateMessage(message); } } - private void handlePropose(Message message) { - Block fullBlock = (Block) message.content(); - SeenProposal proposal = new SeenProposal(message.sender(), fullBlock.epoch()); + private void handleProposalMessage(Message message) { + Block proposedBlock = (Block) message.content(); + SeenProposal proposal = new SeenProposal(message.sender(), proposedBlock.epoch()); - if (seenProposals.contains(proposal) - || !blockchainManager.onPropose(fullBlock)) + if (seenProposals.contains(proposal) || !blockchainManager.onPropose(proposedBlock)) { return; + } seenProposals.add(proposal); - Block blockHeader = new Block(fullBlock.parentHash(), fullBlock.epoch(), fullBlock.length(), new Transaction[0]); - urbNode.broadcastFromLocal(new Message(MessageType.VOTE, blockHeader, localId)); - AppLogger.logDebug("Voted for block from leader " + message.sender() + " epoch " + fullBlock.epoch()); + Block blockHeader = new Block( + proposedBlock.parentHash(), + proposedBlock.epoch(), + proposedBlock.length(), + new Transaction[0] + ); + urbNode.broadcastFromLocal(new Message(MessageType.VOTE, blockHeader, localNodeId)); + AppLogger.logDebug("Voted for block from leader " + message.sender() + " epoch " + proposedBlock.epoch()); } - - private void handleVote(Message message) { + private void handleVoteMessage(Message message) { Block block = (Block) message.content(); - votedBlocks.computeIfAbsent(block, _ -> new HashSet<>()).add(message.sender()); + blockVotes.computeIfAbsent(block, _ -> new HashSet<>()).add(message.sender()); + + int totalVotes = blockVotes.get(block).size(); - if (votedBlocks.get(block).size() > numberOfDistinctNodes / 2) { + if (totalVotes > numberOfNodes / 2) { blockchainManager.notarizeBlock(block); } } - private void handleJoin(Message message) { - if (message.sender() == localId) return; + private void handleJoinRequest(Message message) { + if (message.sender() == localNodeId) return; - MissingEpochs missingEpochs = (MissingEpochs) message.content(); - List missingBlocks = blockchainManager.blocksFromToEpoch(missingEpochs.from(), missingEpochs.to()); + MissingEpochRange requestedRange = (MissingEpochRange) message.content(); + List missingBlocks = blockchainManager.getBlocksInEpochRange( + requestedRange.from(), + requestedRange.to() + ); - Message catchUp = new Message( + Message catchUpResponse = new Message( MessageType.UPDATE, new CatchUp(message.sender(), missingBlocks, currentEpoch.get()), - localId + localNodeId ); - urbNode.broadcastFromLocal(catchUp); + urbNode.broadcastFromLocal(catchUpResponse); } - private void handleUpdate(Message message) { + private void handleUpdateMessage(Message message) { CatchUp catchUp = (CatchUp) message.content(); - if (catchUp.slackerId() != localId) return; + if (catchUp.slackerId() != localNodeId) return; blockchainManager.insertMissingBlocks(catchUp.missingChain()); } - private boolean inConfusionEpoch(int epoch) { - return epoch >= CONFUSION_START && epoch <= CONFUSION_START + CONFUSION_DURATION - 1; + private boolean isInConfusionPhase(int epoch) { + return epoch >= CONFUSION_EPOCH_START && epoch < CONFUSION_EPOCH_START + CONFUSION_EPOCH_DURATION; } - private int calculateLeaderId(int epoch) { - return inConfusionEpoch(epoch) ? epoch % numberOfDistinctNodes - : random.nextInt(numberOfDistinctNodes); + private int determineEpochLeader(int epoch) { + return isInConfusionPhase(epoch) ? epoch % numberOfNodes : epochLeaderRandomizer.nextInt(numberOfNodes); } - private void receiveClientTransactionsRequests() { - try (ServerSocket serverSocket = new ServerSocket(myClientAddress.port())) { - AppLogger.logInfo("Transaction client server listening on port " + myClientAddress.port()); + private void acceptClientTransactions() { + try (ServerSocket serverSocket = new ServerSocket(clientServerAddress.port())) { + AppLogger.logInfo("Transaction server listening on port " + clientServerAddress.port()); while (true) { Socket clientSocket = serverSocket.accept(); - executor.submit(() -> handleReceiveClientRequest(clientSocket)); + executor.submit(() -> handleClientConnection(clientSocket)); } } catch (IOException e) { - AppLogger.logError("Error in transaction client server: " + e.getMessage(), e); + AppLogger.logError("Error in transaction server: " + e.getMessage(), e); } } - private void handleReceiveClientRequest(Socket clientSocket) { - AppLogger.logDebug("Handling client " + clientSocket.getInetAddress() + " connection..."); + private void handleClientConnection(Socket clientSocket) { + AppLogger.logDebug("Handling client connection from " + clientSocket.getInetAddress() + "..."); try (Socket s = clientSocket; ObjectInputStream ois = new ObjectInputStream(s.getInputStream())) { @@ -284,7 +313,7 @@ private void handleReceiveClientRequest(Socket clientSocket) { try { Transaction transaction = (Transaction) ois.readObject(); AppLogger.logInfo("Received transaction from client " + s.getInetAddress() + ": " + transaction); - clientPendingTransactionsQueue.add(transaction); + pendingClientTransactions.add(transaction); } catch (ClassNotFoundException e) { AppLogger.logError("Received unknown object from client " + s.getInetAddress(), e); } diff --git a/src/app/Hash.java b/src/utils/application/Hash.java similarity index 82% rename from src/app/Hash.java rename to src/utils/application/Hash.java index 9aba027..0434d9e 100644 --- a/src/app/Hash.java +++ b/src/utils/application/Hash.java @@ -1,8 +1,8 @@ -package app; +package utils.application; import java.util.Arrays; -record Hash(byte[] hash) { +public record Hash(byte[] hash) { @Override public boolean equals(Object o) { diff --git a/src/utils/application/MissingEpochRange.java b/src/utils/application/MissingEpochRange.java new file mode 100644 index 0000000..53abffe --- /dev/null +++ b/src/utils/application/MissingEpochRange.java @@ -0,0 +1,4 @@ +package utils.application; + +public record MissingEpochRange(Integer from, Integer to) implements Content { +} diff --git a/src/utils/application/MissingEpochs.java b/src/utils/application/MissingEpochs.java deleted file mode 100644 index d4a463e..0000000 --- a/src/utils/application/MissingEpochs.java +++ /dev/null @@ -1,4 +0,0 @@ -package utils.application; - -public record MissingEpochs(Integer from, Integer to) implements Content { -} From 91c41171a5cd1bf8046ab757699d23846491d0df Mon Sep 17 00:00:00 2001 From: sousanamain Date: Wed, 3 Dec 2025 18:10:06 +0000 Subject: [PATCH 11/16] add persistence-related functions (need to be changed from "persistance" to "persistence"), InitializeFromFile is not perfect, needs to be updated --- config.txt | 2 +- src/app/BlockNode.java | 22 +++ src/app/BlockchainManager.java | 181 ++++++++++++++++++++++++- src/app/StreamletNode.java | 15 +- src/utils/application/Block.java | 33 +++++ src/utils/application/Hash.java | 9 ++ src/utils/application/Transaction.java | 20 +++ 7 files changed, 269 insertions(+), 13 deletions(-) diff --git a/config.txt b/config.txt index 10ca320..56bcad1 100644 --- a/config.txt +++ b/config.txt @@ -6,7 +6,7 @@ P2P=127.0.0.1:54583 P2P=127.0.0.1:54584 # Agreed time for every server to start protocol dd-MM-yyyy HH:mm:ss -start=13-11-2025 04:01:00 +start=03-12-2025 16:15:00 # NORMAL and DEBUG mode for logs logLevel=DEBUG diff --git a/src/app/BlockNode.java b/src/app/BlockNode.java index 92df390..d21f8ac 100644 --- a/src/app/BlockNode.java +++ b/src/app/BlockNode.java @@ -3,8 +3,13 @@ import utils.application.Block; import java.io.Serializable; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class BlockNode implements Serializable { + private static final Pattern BLOCK_NODE_REGEX = Pattern.compile( + "BlockNode\\[(?true|false),(?.*)]" + ); private final Block block; private Boolean finalized; @@ -37,4 +42,21 @@ public boolean equals(Object o) { public int hashCode() { return block.hashCode(); } + + public static BlockNode fromPersistanceString(String persistanceString) { + Matcher matcher = BLOCK_NODE_REGEX.matcher(persistanceString); + if (!matcher.matches()) { + return null; + } + + Boolean finalized = Boolean.parseBoolean(matcher.group("finalized")); + String blockString = matcher.group("block"); + Block block = Block.fromPersistanceString(blockString); + + return new BlockNode(block, finalized); + } + + public String getPersistanceString() { + return "BlockNode[%s,%s]".formatted(finalized, block.getPersistanceString()); + } } diff --git a/src/app/BlockchainManager.java b/src/app/BlockchainManager.java index 34d1dcb..db6e41e 100644 --- a/src/app/BlockchainManager.java +++ b/src/app/BlockchainManager.java @@ -1,4 +1,3 @@ - package app; import utils.application.Block; @@ -6,35 +5,203 @@ import utils.application.Transaction; import utils.logs.AppLogger; +import java.io.IOException; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; import java.util.*; import java.util.function.Predicate; import java.util.stream.Collectors; public class BlockchainManager { public static final int FINALIZATION_MIN_SIZE = 3; + public static final String LOG_FILE_NAME = "log.txt"; + public static final String BLOCK_CHAIN_FILE_NAME = "blockChain.txt"; private static final int SHA1_LENGTH = 20; private static final Block GENESIS_BLOCK = new Block(new byte[SHA1_LENGTH], 0, 0, new Transaction[0]); + private final Hash genesisParentHash; + + //{1:2} + //----- + //{1:[],} + //----- + //{} + //----- + //{2} + //removePending(2) + //addPending(3) + //onPropose(3) + //finalizeByConsecutiveEpochBlocks() + + private final Map blockNodesByHash = new HashMap<>(); // blocos private final Map> blockchainByParentHash = new HashMap<>(); - private final Map blockNodesByHash = new HashMap<>(); private final Set recoveredBlocks = new HashSet<>(); private final Set pendingProposals = new HashSet<>(); - private final Hash genesisParentHash; + + private final Path logFilePath; + private final Path blockchainFilePath; private int mostRecentNotarizedEpoch = -1; - public BlockchainManager() { + public BlockchainManager(Path outputPath) { + this.logFilePath = outputPath.resolve(LOG_FILE_NAME); + this.blockchainFilePath = outputPath.resolve(BLOCK_CHAIN_FILE_NAME); + try { + createIfNotExistsOutputFiles(outputPath); + initializeFromFile(); + } catch (IOException e) { + throw new RuntimeException(e); + } BlockNode genesisNode = new BlockNode(GENESIS_BLOCK, true); genesisParentHash = new Hash(GENESIS_BLOCK.parentHash()); Hash genesisHash = new Hash(GENESIS_BLOCK.getSHA1()); List genesisRoot = new LinkedList<>(); genesisRoot.add(genesisNode); - blockchainByParentHash.put(genesisParentHash, genesisRoot); - blockchainByParentHash.put(genesisHash, new LinkedList<>()); + blockchainByParentHash.putIfAbsent(genesisParentHash, genesisRoot); + blockchainByParentHash.putIfAbsent(genesisHash, new LinkedList<>()); + + blockNodesByHash.putIfAbsent(genesisHash, genesisNode); + } + + + private void initializeFromFile() throws IOException { + String content = Files.readString(blockchainFilePath); + if (content.isBlank()) { + return; + } + + String[] sections = content.split("\n\n"); + + String[] blockNodeLines = sections[0].trim().split("\n"); + for (String line : blockNodeLines) { + if (line.isBlank()) continue; + int colonIndex = line.indexOf(":"); + if (colonIndex == -1) continue; + + String hashStr = line.substring(0, colonIndex).trim(); + String blockNodeStr = line.substring(colonIndex + 1).trim(); + + try { + Hash hash = Hash.fromPersistanceString(hashStr); + BlockNode blockNode = BlockNode.fromPersistanceString(blockNodeStr); + + if (blockNode != null) { + blockNodesByHash.put(hash, blockNode); + } + } catch (IllegalArgumentException e) { + AppLogger.logWarning("Failed to parse blockNode entry: " + line); + } + } + + String[] chainLines = sections[1].trim().split("\n"); + for (String line : chainLines) { + if (line.isBlank()) continue; - blockNodesByHash.put(genesisHash, genesisNode); + int lastBracketIndex = line.lastIndexOf("]"); + if (lastBracketIndex == -1) continue; + + int startBracketIndex = line.lastIndexOf("["); + if (startBracketIndex == -1) continue; + + String hashStr = line.substring(0, startBracketIndex).trim(); + if (hashStr.endsWith(",")) { + hashStr = hashStr.substring(0, hashStr.length() - 1).trim(); + } + String childrenStr = line.substring(startBracketIndex, lastBracketIndex + 1); + + try { + Hash hash = Hash.fromPersistanceString(hashStr); + + List children = new LinkedList<>(); + if (childrenStr.startsWith("[") && childrenStr.endsWith("]")) { + String innerContent = childrenStr.substring(1, childrenStr.length() - 1); + if (!innerContent.isBlank()) { + String[] blockNodeStrings = innerContent.split(",(?=BlockNode\\[)"); + for (String blockNodeStr : blockNodeStrings) { + BlockNode blockNode = BlockNode.fromPersistanceString(blockNodeStr.trim()); + if (blockNode != null) { + children.add(blockNode); + } + } + } + } + blockchainByParentHash.put(hash, children); + } catch (IllegalArgumentException e) { + AppLogger.logWarning("Failed to parse chain entry: " + line); + } + } + + String[] recoveredLines = sections[2].trim().split("\n"); + for (String line : recoveredLines) { + if (line.isBlank()) continue; + try { + BlockNode blockNode = BlockNode.fromPersistanceString(line.trim()); + if (blockNode != null) { + recoveredBlocks.add(blockNode); + } + } catch (Exception e) { + AppLogger.logWarning("Failed to parse recovered block: " + line); + } + } + + String[] pendingLines = sections[3].trim().split("\n"); + for (String line : pendingLines) { + if (line.isBlank()) continue; + try { + Block block = Block.fromPersistanceString(line.trim()); + if (block != null) { + pendingProposals.add(block); + } + } catch (Exception e) { + AppLogger.logWarning("Failed to parse pending proposal: " + line); + } + } + } + + public void persistToFile() { + try { + Files.writeString(blockchainFilePath, getPersistanceString(), StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE); + Files.writeString(logFilePath, "", StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE); + } catch (Exception ignored) { + } + } + + private String getPersistanceString() { + StringBuilder sb = new StringBuilder(); + blockNodesByHash.forEach((hash, blockNode) -> { + sb.append("%s:%s".formatted(hash.getPersistanceString(), blockNode.getPersistanceString())); + sb.append("\n"); + }); + sb.append("\n\n"); + blockchainByParentHash.forEach((hash, children) -> { + sb.append("%s,[%s]".formatted( + hash.getPersistanceString(), + children.stream().map(BlockNode::getPersistanceString).collect(Collectors.joining(",")) + ) + ); + sb.append("\n"); + }); + sb.append("\n\n"); + sb.append("%s".formatted(recoveredBlocks.stream().map(BlockNode::getPersistanceString).collect(Collectors.joining("\n")))); + sb.append("\n\n"); + sb.append("%s".formatted(pendingProposals.stream().map(Block::getPersistanceString).collect(Collectors.joining("\n")))); + return sb.toString(); + } + + private void createIfNotExistsOutputFiles(Path outputPath) throws IOException { + Files.createDirectories(outputPath); + try { + Files.createFile(logFilePath); + } catch (FileAlreadyExistsException ignored) { + } + try { + Files.createFile(blockchainFilePath); + } catch (FileAlreadyExistsException ignored) { + } } public List getBiggestNotarizedChain() { diff --git a/src/app/StreamletNode.java b/src/app/StreamletNode.java index bdd03f1..d4527ef 100644 --- a/src/app/StreamletNode.java +++ b/src/app/StreamletNode.java @@ -11,6 +11,7 @@ import java.io.ObjectInputStream; import java.net.ServerSocket; import java.net.Socket; +import java.nio.file.Path; import java.security.NoSuchAlgorithmException; import java.time.LocalDateTime; import java.time.temporal.ChronoUnit; @@ -25,6 +26,7 @@ public class StreamletNode { public static final int BLOCKCHAIN_PRINT_EPOCH_INTERVAL = 5; private static final int CONFUSION_EPOCH_START = 0; private static final int CONFUSION_EPOCH_DURATION = 2; + private static final int BLOCKCHAIN_PERSISTENCE_INTERVAL = 10; private final int deltaInSeconds; private final LocalDateTime protocolStartTime; private final int numberOfNodes; @@ -55,7 +57,7 @@ public StreamletNode(PeerInfo localPeerInfo, List remotePeersInfo, int this.deltaInSeconds = deltaInSeconds; this.protocolStartTime = protocolStartTime; transactionPoolSimulator = new TransactionPoolSimulator(numberOfNodes); - blockchainManager = new BlockchainManager(); + blockchainManager = new BlockchainManager(Path.of("output", "node_%d".formatted(localNodeId))); // output/node_1/log.txt output/node_1/blockChain.txt urbNode = new URBNode(localPeerInfo, remotePeersInfo, deliveredMessagesQueue::add); this.isClientGeneratingTransactions = isClientGeneratingTransactions; this.clientServerAddress = clientServerAddress; @@ -149,6 +151,13 @@ private void advanceEpoch() { needsToRecover = false; } + if (epoch != 0 && epoch % BLOCKCHAIN_PRINT_EPOCH_INTERVAL == 0) { + blockchainManager.printBiggestFinalizedChain(); + } + + if (epoch != 0 && epoch % BLOCKCHAIN_PERSISTENCE_INTERVAL == 0) { + blockchainManager.persistToFile(); + } int epochLeader = determineEpochLeader(epoch); AppLogger.logInfo("#### EPOCH = " + epoch + " LEADER = " + epochLeader + " ####"); @@ -163,10 +172,6 @@ private void advanceEpoch() { } } - if (epoch != 0 && epoch % BLOCKCHAIN_PRINT_EPOCH_INTERVAL == 0) { - blockchainManager.printBiggestFinalizedChain(); - } - currentEpoch.incrementAndGet(); } diff --git a/src/utils/application/Block.java b/src/utils/application/Block.java index aa949e9..db2a4fb 100644 --- a/src/utils/application/Block.java +++ b/src/utils/application/Block.java @@ -4,10 +4,16 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Arrays; +import java.util.Base64; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.IntStream; public record Block(byte[] parentHash, Integer epoch, Integer length, Transaction[] transactions) implements Content { + private static final Pattern BLOCK_REGEX = Pattern.compile( + "Block\\[(?\\d+),(?\\d+),(?.*),\\[(?.*)]]" + ); public Block(byte[] parentHash, Integer epoch, Integer length, Transaction[] transactions) { this.parentHash = parentHash; @@ -71,4 +77,31 @@ public String toStringSummary() { epoch, length, partialParentHash, txSummary ); } + + public static Block fromPersistanceString(String persistanceString) { + Matcher matcher = BLOCK_REGEX.matcher(persistanceString); + if (!matcher.matches()) { + return null; + } + + Integer epoch = Integer.parseInt(matcher.group("epoch")); + Integer length = Integer.parseInt(matcher.group("length")); + byte[] parentHash = Base64.getDecoder().decode(matcher.group("parentHash")); + + String transactionsString = matcher.group("transactions"); + Transaction[] transactions = transactionsString.isEmpty() ? new Transaction[0] : Arrays.stream(transactionsString.substring(1, transactionsString.length() - 1).split(",")) + .map(Transaction::fromPersistanceString) + .toArray(Transaction[]::new); + + return new Block(parentHash, epoch, length, transactions); + } + + public String getPersistanceString() { + return "Block[%s,%s,%s,[%s]]".formatted( + epoch, + length, + Base64.getEncoder().encodeToString(parentHash), + Arrays.stream(transactions).map(Transaction::getPersistanceString).collect(Collectors.joining(",")) + ); + } } diff --git a/src/utils/application/Hash.java b/src/utils/application/Hash.java index 0434d9e..f426f63 100644 --- a/src/utils/application/Hash.java +++ b/src/utils/application/Hash.java @@ -1,9 +1,14 @@ package utils.application; import java.util.Arrays; +import java.util.Base64; public record Hash(byte[] hash) { + public static Hash fromPersistanceString(String persistanceString) { + return new Hash(Base64.getDecoder().decode(persistanceString)); + } + @Override public boolean equals(Object o) { if (!(o instanceof Hash(byte[] hash1))) return false; @@ -15,4 +20,8 @@ public boolean equals(Object o) { public int hashCode() { return Arrays.hashCode(hash); } + + public String getPersistanceString() { + return Base64.getEncoder().encodeToString(hash); + } } diff --git a/src/utils/application/Transaction.java b/src/utils/application/Transaction.java index ee32094..5731fcc 100644 --- a/src/utils/application/Transaction.java +++ b/src/utils/application/Transaction.java @@ -1,9 +1,29 @@ package utils.application; import java.io.Serializable; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public record Transaction(Long id, Double amount, Integer sender, Integer receiver) implements Serializable { + private static final Pattern TX_REGEX = Pattern.compile("Tx\\[(?\\d+),(?\\d+(.\\d+)?),(?\\d+),(?\\d+)\\]"); + public String toStringSummary() { return String.format("id=%d, %d→%d: %.2f", id, sender, receiver, amount); } + + public static Transaction fromPersistanceString(String persistanceString) { + Matcher matcher = TX_REGEX.matcher(persistanceString); + if (!matcher.matches()) { + return null; + } + Long id = Long.parseLong(matcher.group("id")); + Double amount = Double.parseDouble(matcher.group("amount")); + Integer sender = Integer.parseInt(matcher.group("sender")); + Integer receiver = Integer.parseInt(matcher.group("receiver")); + return new Transaction(id, amount, sender, receiver); + } + + public String getPersistanceString() { + return "Tx[%d,%f,%d,%d]".formatted(id, amount, sender, receiver); + } } From ce400456500a8981a9171810607f8121723e8225 Mon Sep 17 00:00:00 2001 From: Bruno Faustino Date: Wed, 3 Dec 2025 18:36:21 +0000 Subject: [PATCH 12/16] Fix persistence functions spelling --- src/app/BlockNode.java | 10 ++++----- src/app/BlockchainManager.java | 31 +++++++++++--------------- src/app/StreamletNode.java | 1 + src/utils/application/Block.java | 10 ++++----- src/utils/application/Hash.java | 6 ++--- src/utils/application/Transaction.java | 6 ++--- 6 files changed, 30 insertions(+), 34 deletions(-) diff --git a/src/app/BlockNode.java b/src/app/BlockNode.java index d21f8ac..e8ada01 100644 --- a/src/app/BlockNode.java +++ b/src/app/BlockNode.java @@ -43,20 +43,20 @@ public int hashCode() { return block.hashCode(); } - public static BlockNode fromPersistanceString(String persistanceString) { - Matcher matcher = BLOCK_NODE_REGEX.matcher(persistanceString); + public static BlockNode fromPersistenceString(String persistenceString) { + Matcher matcher = BLOCK_NODE_REGEX.matcher(persistenceString); if (!matcher.matches()) { return null; } Boolean finalized = Boolean.parseBoolean(matcher.group("finalized")); String blockString = matcher.group("block"); - Block block = Block.fromPersistanceString(blockString); + Block block = Block.fromPersistenceString(blockString); return new BlockNode(block, finalized); } - public String getPersistanceString() { - return "BlockNode[%s,%s]".formatted(finalized, block.getPersistanceString()); + public String getPersistenceString() { + return "BlockNode[%s,%s]".formatted(finalized, block.getPersistenceString()); } } diff --git a/src/app/BlockchainManager.java b/src/app/BlockchainManager.java index db6e41e..187cd73 100644 --- a/src/app/BlockchainManager.java +++ b/src/app/BlockchainManager.java @@ -31,11 +31,6 @@ public class BlockchainManager { //----- //{2} - //removePending(2) - //addPending(3) - //onPropose(3) - //finalizeByConsecutiveEpochBlocks() - private final Map blockNodesByHash = new HashMap<>(); // blocos private final Map> blockchainByParentHash = new HashMap<>(); private final Set recoveredBlocks = new HashSet<>(); @@ -86,8 +81,8 @@ private void initializeFromFile() throws IOException { String blockNodeStr = line.substring(colonIndex + 1).trim(); try { - Hash hash = Hash.fromPersistanceString(hashStr); - BlockNode blockNode = BlockNode.fromPersistanceString(blockNodeStr); + Hash hash = Hash.fromPersistenceString(hashStr); + BlockNode blockNode = BlockNode.fromPersistenceString(blockNodeStr); if (blockNode != null) { blockNodesByHash.put(hash, blockNode); @@ -114,7 +109,7 @@ private void initializeFromFile() throws IOException { String childrenStr = line.substring(startBracketIndex, lastBracketIndex + 1); try { - Hash hash = Hash.fromPersistanceString(hashStr); + Hash hash = Hash.fromPersistenceString(hashStr); List children = new LinkedList<>(); if (childrenStr.startsWith("[") && childrenStr.endsWith("]")) { @@ -122,7 +117,7 @@ private void initializeFromFile() throws IOException { if (!innerContent.isBlank()) { String[] blockNodeStrings = innerContent.split(",(?=BlockNode\\[)"); for (String blockNodeStr : blockNodeStrings) { - BlockNode blockNode = BlockNode.fromPersistanceString(blockNodeStr.trim()); + BlockNode blockNode = BlockNode.fromPersistenceString(blockNodeStr.trim()); if (blockNode != null) { children.add(blockNode); } @@ -139,7 +134,7 @@ private void initializeFromFile() throws IOException { for (String line : recoveredLines) { if (line.isBlank()) continue; try { - BlockNode blockNode = BlockNode.fromPersistanceString(line.trim()); + BlockNode blockNode = BlockNode.fromPersistenceString(line.trim()); if (blockNode != null) { recoveredBlocks.add(blockNode); } @@ -152,7 +147,7 @@ private void initializeFromFile() throws IOException { for (String line : pendingLines) { if (line.isBlank()) continue; try { - Block block = Block.fromPersistanceString(line.trim()); + Block block = Block.fromPersistenceString(line.trim()); if (block != null) { pendingProposals.add(block); } @@ -164,31 +159,31 @@ private void initializeFromFile() throws IOException { public void persistToFile() { try { - Files.writeString(blockchainFilePath, getPersistanceString(), StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE); + Files.writeString(blockchainFilePath, getPersistenceString(), StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE); Files.writeString(logFilePath, "", StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE); } catch (Exception ignored) { } } - private String getPersistanceString() { + private String getPersistenceString() { StringBuilder sb = new StringBuilder(); blockNodesByHash.forEach((hash, blockNode) -> { - sb.append("%s:%s".formatted(hash.getPersistanceString(), blockNode.getPersistanceString())); + sb.append("%s:%s".formatted(hash.getPersistenceString(), blockNode.getPersistenceString())); sb.append("\n"); }); sb.append("\n\n"); blockchainByParentHash.forEach((hash, children) -> { sb.append("%s,[%s]".formatted( - hash.getPersistanceString(), - children.stream().map(BlockNode::getPersistanceString).collect(Collectors.joining(",")) + hash.getPersistenceString(), + children.stream().map(BlockNode::getPersistenceString).collect(Collectors.joining(",")) ) ); sb.append("\n"); }); sb.append("\n\n"); - sb.append("%s".formatted(recoveredBlocks.stream().map(BlockNode::getPersistanceString).collect(Collectors.joining("\n")))); + sb.append("%s".formatted(recoveredBlocks.stream().map(BlockNode::getPersistenceString).collect(Collectors.joining("\n")))); sb.append("\n\n"); - sb.append("%s".formatted(pendingProposals.stream().map(Block::getPersistanceString).collect(Collectors.joining("\n")))); + sb.append("%s".formatted(pendingProposals.stream().map(Block::getPersistenceString).collect(Collectors.joining("\n")))); return sb.toString(); } diff --git a/src/app/StreamletNode.java b/src/app/StreamletNode.java index d4527ef..0b11274 100644 --- a/src/app/StreamletNode.java +++ b/src/app/StreamletNode.java @@ -233,6 +233,7 @@ private void processMessage(Message message) { case PROPOSE -> handleProposalMessage(message); case VOTE -> handleVoteMessage(message); case UPDATE -> handleUpdateMessage(message); + default -> {} } } diff --git a/src/utils/application/Block.java b/src/utils/application/Block.java index db2a4fb..5a40e51 100644 --- a/src/utils/application/Block.java +++ b/src/utils/application/Block.java @@ -78,8 +78,8 @@ public String toStringSummary() { ); } - public static Block fromPersistanceString(String persistanceString) { - Matcher matcher = BLOCK_REGEX.matcher(persistanceString); + public static Block fromPersistenceString(String persistenceString) { + Matcher matcher = BLOCK_REGEX.matcher(persistenceString); if (!matcher.matches()) { return null; } @@ -90,18 +90,18 @@ public static Block fromPersistanceString(String persistanceString) { String transactionsString = matcher.group("transactions"); Transaction[] transactions = transactionsString.isEmpty() ? new Transaction[0] : Arrays.stream(transactionsString.substring(1, transactionsString.length() - 1).split(",")) - .map(Transaction::fromPersistanceString) + .map(Transaction::fromPersistenceString) .toArray(Transaction[]::new); return new Block(parentHash, epoch, length, transactions); } - public String getPersistanceString() { + public String getPersistenceString() { return "Block[%s,%s,%s,[%s]]".formatted( epoch, length, Base64.getEncoder().encodeToString(parentHash), - Arrays.stream(transactions).map(Transaction::getPersistanceString).collect(Collectors.joining(",")) + Arrays.stream(transactions).map(Transaction::getPersistenceString).collect(Collectors.joining(",")) ); } } diff --git a/src/utils/application/Hash.java b/src/utils/application/Hash.java index f426f63..1d26e99 100644 --- a/src/utils/application/Hash.java +++ b/src/utils/application/Hash.java @@ -5,8 +5,8 @@ public record Hash(byte[] hash) { - public static Hash fromPersistanceString(String persistanceString) { - return new Hash(Base64.getDecoder().decode(persistanceString)); + public static Hash fromPersistenceString(String persistenceString) { + return new Hash(Base64.getDecoder().decode(persistenceString)); } @Override @@ -21,7 +21,7 @@ public int hashCode() { return Arrays.hashCode(hash); } - public String getPersistanceString() { + public String getPersistenceString() { return Base64.getEncoder().encodeToString(hash); } } diff --git a/src/utils/application/Transaction.java b/src/utils/application/Transaction.java index 5731fcc..97f4fba 100644 --- a/src/utils/application/Transaction.java +++ b/src/utils/application/Transaction.java @@ -11,8 +11,8 @@ public String toStringSummary() { return String.format("id=%d, %d→%d: %.2f", id, sender, receiver, amount); } - public static Transaction fromPersistanceString(String persistanceString) { - Matcher matcher = TX_REGEX.matcher(persistanceString); + public static Transaction fromPersistenceString(String persistenceString) { + Matcher matcher = TX_REGEX.matcher(persistenceString); if (!matcher.matches()) { return null; } @@ -23,7 +23,7 @@ public static Transaction fromPersistanceString(String persistanceString) { return new Transaction(id, amount, sender, receiver); } - public String getPersistanceString() { + public String getPersistenceString() { return "Tx[%d,%f,%d,%d]".formatted(id, amount, sender, receiver); } } From 5aeae232cb2d3f510a3fd4830330483c5032bf33 Mon Sep 17 00:00:00 2001 From: Bruno Faustino Date: Thu, 4 Dec 2025 08:57:07 +0000 Subject: [PATCH 13/16] Fix blockchain persistence and abstract persistence file related logic to another class. Update git ignore to ignore the output folder. (has a lot of debug messages and there is still a bug related with the finalization of the last notarized block when the node crashes and then comes back) --- .gitignore | 3 +- config.txt | 2 +- src/app/BlockNode.java | 10 +- src/app/BlockchainManager.java | 173 +++++----------------- src/app/PersistenceFilesManager.java | 190 +++++++++++++++++++++++++ src/app/StreamletNode.java | 30 ++-- src/utils/application/Block.java | 36 +++-- src/utils/application/Hash.java | 8 +- src/utils/application/Transaction.java | 19 +-- 9 files changed, 289 insertions(+), 182 deletions(-) create mode 100644 src/app/PersistenceFilesManager.java diff --git a/.gitignore b/.gitignore index 2e81a92..35d771e 100644 --- a/.gitignore +++ b/.gitignore @@ -31,4 +31,5 @@ bin/ ### .idea -StreamLet.iml \ No newline at end of file +StreamLet.iml +output \ No newline at end of file diff --git a/config.txt b/config.txt index 56bcad1..e4e7b80 100644 --- a/config.txt +++ b/config.txt @@ -6,7 +6,7 @@ P2P=127.0.0.1:54583 P2P=127.0.0.1:54584 # Agreed time for every server to start protocol dd-MM-yyyy HH:mm:ss -start=03-12-2025 16:15:00 +start=04-12-2025 08:36:00 # NORMAL and DEBUG mode for logs logLevel=DEBUG diff --git a/src/app/BlockNode.java b/src/app/BlockNode.java index e8ada01..94e860d 100644 --- a/src/app/BlockNode.java +++ b/src/app/BlockNode.java @@ -43,20 +43,20 @@ public int hashCode() { return block.hashCode(); } + public String getPersistenceString() { + return "BlockNode[%s,%s]".formatted(finalized, block.getPersistenceString()); + } + public static BlockNode fromPersistenceString(String persistenceString) { Matcher matcher = BLOCK_NODE_REGEX.matcher(persistenceString); if (!matcher.matches()) { return null; } - Boolean finalized = Boolean.parseBoolean(matcher.group("finalized")); + boolean finalized = Boolean.parseBoolean(matcher.group("finalized")); String blockString = matcher.group("block"); Block block = Block.fromPersistenceString(blockString); return new BlockNode(block, finalized); } - - public String getPersistenceString() { - return "BlockNode[%s,%s]".formatted(finalized, block.getPersistenceString()); - } } diff --git a/src/app/BlockchainManager.java b/src/app/BlockchainManager.java index 187cd73..a854fa1 100644 --- a/src/app/BlockchainManager.java +++ b/src/app/BlockchainManager.java @@ -5,11 +5,7 @@ import utils.application.Transaction; import utils.logs.AppLogger; -import java.io.IOException; -import java.nio.file.FileAlreadyExistsException; -import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.StandardOpenOption; import java.util.*; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -23,37 +19,36 @@ public class BlockchainManager { new Block(new byte[SHA1_LENGTH], 0, 0, new Transaction[0]); private final Hash genesisParentHash; - //{1:2} - //----- - //{1:[],} - //----- - //{} - //----- - //{2} - - private final Map blockNodesByHash = new HashMap<>(); // blocos + private final Map blockNodesByHash = new HashMap<>(); private final Map> blockchainByParentHash = new HashMap<>(); private final Set recoveredBlocks = new HashSet<>(); private final Set pendingProposals = new HashSet<>(); - private final Path logFilePath; - private final Path blockchainFilePath; + private final PersistenceFilesManager persistenceManager; private int mostRecentNotarizedEpoch = -1; public BlockchainManager(Path outputPath) { - this.logFilePath = outputPath.resolve(LOG_FILE_NAME); - this.blockchainFilePath = outputPath.resolve(BLOCK_CHAIN_FILE_NAME); - try { - createIfNotExistsOutputFiles(outputPath); - initializeFromFile(); - } catch (IOException e) { - throw new RuntimeException(e); - } + Path logFilePath = outputPath.resolve(LOG_FILE_NAME); + Path blockchainFilePath = outputPath.resolve(BLOCK_CHAIN_FILE_NAME); + persistenceManager = new PersistenceFilesManager(logFilePath, blockchainFilePath, outputPath); + mostRecentNotarizedEpoch = persistenceManager.initializeFromFile( + blockNodesByHash, blockchainByParentHash, recoveredBlocks, pendingProposals + ); + BlockNode genesisNode = new BlockNode(GENESIS_BLOCK, true); genesisParentHash = new Hash(GENESIS_BLOCK.parentHash()); Hash genesisHash = new Hash(GENESIS_BLOCK.getSHA1()); + AppLogger.logWarning("RESTARTING AND THE GENESIS'S PARENT HAS THIS MANY CHILDREN..."); + if (blockchainByParentHash.get(genesisParentHash) != null) { + AppLogger.logWarning("" + blockchainByParentHash.get(genesisParentHash).size()); + AppLogger.logWarning("LENGTH BLOCK NODES BY HASH: " + blockNodesByHash.size()); + AppLogger.logWarning("LENGTH BLOCK CHAIN BY PARENT HASH: " + blockchainByParentHash.size()); + } else { + AppLogger.logWarning("GENESIS NOT INSERTED"); + } + List genesisRoot = new LinkedList<>(); genesisRoot.add(genesisNode); blockchainByParentHash.putIfAbsent(genesisParentHash, genesisRoot); @@ -62,107 +57,8 @@ public BlockchainManager(Path outputPath) { blockNodesByHash.putIfAbsent(genesisHash, genesisNode); } - - private void initializeFromFile() throws IOException { - String content = Files.readString(blockchainFilePath); - if (content.isBlank()) { - return; - } - - String[] sections = content.split("\n\n"); - - String[] blockNodeLines = sections[0].trim().split("\n"); - for (String line : blockNodeLines) { - if (line.isBlank()) continue; - int colonIndex = line.indexOf(":"); - if (colonIndex == -1) continue; - - String hashStr = line.substring(0, colonIndex).trim(); - String blockNodeStr = line.substring(colonIndex + 1).trim(); - - try { - Hash hash = Hash.fromPersistenceString(hashStr); - BlockNode blockNode = BlockNode.fromPersistenceString(blockNodeStr); - - if (blockNode != null) { - blockNodesByHash.put(hash, blockNode); - } - } catch (IllegalArgumentException e) { - AppLogger.logWarning("Failed to parse blockNode entry: " + line); - } - } - - String[] chainLines = sections[1].trim().split("\n"); - for (String line : chainLines) { - if (line.isBlank()) continue; - - int lastBracketIndex = line.lastIndexOf("]"); - if (lastBracketIndex == -1) continue; - - int startBracketIndex = line.lastIndexOf("["); - if (startBracketIndex == -1) continue; - - String hashStr = line.substring(0, startBracketIndex).trim(); - if (hashStr.endsWith(",")) { - hashStr = hashStr.substring(0, hashStr.length() - 1).trim(); - } - String childrenStr = line.substring(startBracketIndex, lastBracketIndex + 1); - - try { - Hash hash = Hash.fromPersistenceString(hashStr); - - List children = new LinkedList<>(); - if (childrenStr.startsWith("[") && childrenStr.endsWith("]")) { - String innerContent = childrenStr.substring(1, childrenStr.length() - 1); - if (!innerContent.isBlank()) { - String[] blockNodeStrings = innerContent.split(",(?=BlockNode\\[)"); - for (String blockNodeStr : blockNodeStrings) { - BlockNode blockNode = BlockNode.fromPersistenceString(blockNodeStr.trim()); - if (blockNode != null) { - children.add(blockNode); - } - } - } - } - blockchainByParentHash.put(hash, children); - } catch (IllegalArgumentException e) { - AppLogger.logWarning("Failed to parse chain entry: " + line); - } - } - - String[] recoveredLines = sections[2].trim().split("\n"); - for (String line : recoveredLines) { - if (line.isBlank()) continue; - try { - BlockNode blockNode = BlockNode.fromPersistenceString(line.trim()); - if (blockNode != null) { - recoveredBlocks.add(blockNode); - } - } catch (Exception e) { - AppLogger.logWarning("Failed to parse recovered block: " + line); - } - } - - String[] pendingLines = sections[3].trim().split("\n"); - for (String line : pendingLines) { - if (line.isBlank()) continue; - try { - Block block = Block.fromPersistenceString(line.trim()); - if (block != null) { - pendingProposals.add(block); - } - } catch (Exception e) { - AppLogger.logWarning("Failed to parse pending proposal: " + line); - } - } - } - public void persistToFile() { - try { - Files.writeString(blockchainFilePath, getPersistenceString(), StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE); - Files.writeString(logFilePath, "", StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE); - } catch (Exception ignored) { - } + persistenceManager.persistToFile(getPersistenceString()); } private String getPersistenceString() { @@ -173,7 +69,7 @@ private String getPersistenceString() { }); sb.append("\n\n"); blockchainByParentHash.forEach((hash, children) -> { - sb.append("%s,[%s]".formatted( + sb.append("%s:[%s]".formatted( hash.getPersistenceString(), children.stream().map(BlockNode::getPersistenceString).collect(Collectors.joining(",")) ) @@ -182,24 +78,17 @@ private String getPersistenceString() { }); sb.append("\n\n"); sb.append("%s".formatted(recoveredBlocks.stream().map(BlockNode::getPersistenceString).collect(Collectors.joining("\n")))); - sb.append("\n\n"); + sb.append("\n\n\n"); sb.append("%s".formatted(pendingProposals.stream().map(Block::getPersistenceString).collect(Collectors.joining("\n")))); return sb.toString(); } - private void createIfNotExistsOutputFiles(Path outputPath) throws IOException { - Files.createDirectories(outputPath); - try { - Files.createFile(logFilePath); - } catch (FileAlreadyExistsException ignored) { - } - try { - Files.createFile(blockchainFilePath); - } catch (FileAlreadyExistsException ignored) { - } - } - public List getBiggestNotarizedChain() { + AppLogger.logWarning("STARTING THE SEARCH..."); + AppLogger.logWarning("LENGTH BLOCK NODES BY HASH: " + blockNodesByHash.size()); + AppLogger.logWarning("LENGTH BLOCK CHAIN BY PARENT HASH: " + blockchainByParentHash.size()); + AppLogger.logWarning("LENGTH OF GENESIS PARENT CHILDREN: " + blockchainByParentHash.get(genesisParentHash).size()); + AppLogger.logWarning("GENESIS PARENT HASH: " + Base64.getEncoder().encodeToString(genesisParentHash.hash())); return findBiggestChainMatching(genesisParentHash, _ -> true); } @@ -210,10 +99,19 @@ public List getBiggestFinalizedChain() { private List findBiggestChainMatching(Hash parentHash, Predicate predicate) { List chain = new LinkedList<>(); + AppLogger.logWarning("FINDING BIGGEST CHAIN ON EPOCH..."); + if (!parentHash.equals(genesisParentHash)) { + AppLogger.logWarning(blockNodesByHash.get(parentHash).block().epoch().toString()); chain.add(blockNodesByHash.get(parentHash).block()); } + for (BlockNode child : blockchainByParentHash.get(parentHash)) { + AppLogger.logWarning("\tCHILD"); + AppLogger.logWarning("\t" + child.getPersistenceString()); + AppLogger.logWarning("\tWITH HASH: " + Base64.getEncoder().encodeToString(child.block().getSHA1())); + } + chain.addAll( blockchainByParentHash.get(parentHash).stream() .filter(predicate) @@ -380,6 +278,7 @@ public void insertMissingBlocks(List missingBlocks) { .add(blockNode); blockchainByParentHash.computeIfAbsent(blockHash, _ -> new LinkedList<>()); blockNodesByHash.put(blockHash, blockNode); + finalizeAndPropagate(blockNode); } } diff --git a/src/app/PersistenceFilesManager.java b/src/app/PersistenceFilesManager.java new file mode 100644 index 0000000..e3233b7 --- /dev/null +++ b/src/app/PersistenceFilesManager.java @@ -0,0 +1,190 @@ +package app; + +import java.io.IOException; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.Base64; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import utils.application.Block; +import utils.application.Hash; +import utils.logs.AppLogger; + +public class PersistenceFilesManager { + + private final Path logFilePath; + private final Path blockchainFilePath; + + public PersistenceFilesManager(Path logFilePath, Path blockchainFilePath, Path outputPath) { + this.logFilePath = logFilePath; + this.blockchainFilePath = blockchainFilePath; + + try { + createIfNotExistsOutputFiles(outputPath); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private void createIfNotExistsOutputFiles(Path outputPath) throws IOException { + Files.createDirectories(outputPath); + try { + Files.createFile(logFilePath); + } catch (FileAlreadyExistsException ignored) { + } + try { + Files.createFile(blockchainFilePath); + } catch (FileAlreadyExistsException ignored) { + } + } + + public int initializeFromFile( + Map blockNodesByHash, Map> blockchainByParentHash, + Set recoveredBlocks, Set pendingProposals + ) { + + String content = ""; + + try { + content = Files.readString(blockchainFilePath); + } catch (IOException e) {} + if (content.isBlank()) return -1; + + String[] sections = content.split("\n\n"); + + String[] blockNodeLines = sections[0].trim().split("\n"); + initializeBlockNodesByHash(blockNodesByHash, blockNodeLines); + + String[] chainLines = sections[1].trim().split("\n"); + int mostRecentEpoch = initializeBlockChainByParentHash(blockchainByParentHash, chainLines); + + if (sections.length > 2) { + String[] recoveredLines = sections[2].trim().split("\n"); + initializeRecoveredBlocks(recoveredBlocks, recoveredLines); + } + + if (sections.length > 3) { + String[] pendingLines = sections[3].trim().split("\n"); + initializePendingProposals(pendingProposals, pendingLines); + } + return mostRecentEpoch; + } + + private void initializeBlockNodesByHash(Map blockNodesByHash, String[] blockNodeLines) { + for (String line : blockNodeLines) { + if (line.isBlank()) continue; + + int colonIndex = line.indexOf(":"); + if (colonIndex == -1) continue; + + String hashStr = line.substring(0, colonIndex).trim(); + String blockNodeStr = line.substring(colonIndex + 1).trim(); + + AppLogger.logWarning("[PERSISTENCE] BLOCKNODE OF HASH " + hashStr); + AppLogger.logWarning("[PERSISTENCE] " + blockNodeStr); + + try { + Hash hash = Hash.fromPersistenceString(hashStr); + BlockNode blockNode = BlockNode.fromPersistenceString(blockNodeStr); + + if (blockNode != null) { + blockNodesByHash.put(hash, blockNode); + } + } catch (IllegalArgumentException e) { + AppLogger.logWarning("Failed to parse blockNode entry: " + line); + } + } + } + + private int initializeBlockChainByParentHash(Map> blockchainByParentHash, String[] chainLines) { + int mostRecentEpoch = -1; + + for (String line : chainLines) { + if (line.isBlank()) continue; + + int colonIndex = line.indexOf(":"); + if (colonIndex == -1) continue; + + int startBracketIndex = line.indexOf("["); + if (startBracketIndex == -1) continue; + + int lastBracketIndex = line.lastIndexOf("]"); + if (lastBracketIndex == -1) continue; + + String hashStr = line.substring(0, colonIndex).trim(); + String childrenStr = line.substring(startBracketIndex + 1, lastBracketIndex); + + try { + Hash hash = Hash.fromPersistenceString(hashStr); + + List children = new LinkedList<>(); + + if (!childrenStr.isBlank()) { + String[] blockNodeStrings = childrenStr.split(",(?=BlockNode\\[)"); + for (String blockNodeStr : blockNodeStrings) { + BlockNode blockNode = BlockNode.fromPersistenceString(blockNodeStr.trim()); + AppLogger.logWarning("[PERSISTENCE] FOUND A BLOCK CHILD OF " + Base64.getEncoder().encodeToString(hash.hash())); + if (blockNode != null) { + AppLogger.logWarning("[PERSISTENCE] AND IT IS NOT NULL!"); + children.add(blockNode); + int blockEpoch = blockNode.block().epoch(); + if (blockEpoch > mostRecentEpoch) { + mostRecentEpoch = blockEpoch; + } + } + } + } + + blockchainByParentHash.put(hash, children); + } catch (IllegalArgumentException e) { + AppLogger.logWarning("Failed to parse chain entry: " + line); + } + } + return mostRecentEpoch; + } + + private void initializeRecoveredBlocks(Set recoveredBlocks, String[] recoveredLines) { + for (String line : recoveredLines) { + if (line.isBlank()) continue; + + try { + BlockNode blockNode = BlockNode.fromPersistenceString(line.trim()); + + if (blockNode != null) { + recoveredBlocks.add(blockNode); + } + } catch (Exception e) { + AppLogger.logWarning("Failed to parse recovered block: " + line); + } + } + } + + private void initializePendingProposals(Set pendingProposals, String[] pendingLines) { + for (String line : pendingLines) { + if (line.isBlank()) continue; + + try { + Block block = Block.fromPersistenceString(line.trim()); + + if (block != null) { + pendingProposals.add(block); + } + } catch (Exception e) { + AppLogger.logWarning("Failed to parse pending proposal: " + line); + } + } + } + + public void persistToFile(String persistenceString) { + try { + Files.writeString(blockchainFilePath, persistenceString, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE); + Files.writeString(logFilePath, "", StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE); + } catch (IOException ignored) {} + } + +} diff --git a/src/app/StreamletNode.java b/src/app/StreamletNode.java index 0b11274..4b2924d 100644 --- a/src/app/StreamletNode.java +++ b/src/app/StreamletNode.java @@ -57,7 +57,7 @@ public StreamletNode(PeerInfo localPeerInfo, List remotePeersInfo, int this.deltaInSeconds = deltaInSeconds; this.protocolStartTime = protocolStartTime; transactionPoolSimulator = new TransactionPoolSimulator(numberOfNodes); - blockchainManager = new BlockchainManager(Path.of("output", "node_%d".formatted(localNodeId))); // output/node_1/log.txt output/node_1/blockChain.txt + blockchainManager = new BlockchainManager(Path.of("output", "node_%d".formatted(localNodeId))); urbNode = new URBNode(localPeerInfo, remotePeersInfo, deliveredMessagesQueue::add); this.isClientGeneratingTransactions = isClientGeneratingTransactions; this.clientServerAddress = clientServerAddress; @@ -152,11 +152,15 @@ private void advanceEpoch() { } if (epoch != 0 && epoch % BLOCKCHAIN_PRINT_EPOCH_INTERVAL == 0) { - blockchainManager.printBiggestFinalizedChain(); + synchronized (blockchainManager) { + blockchainManager.printBiggestFinalizedChain(); + } } if (epoch != 0 && epoch % BLOCKCHAIN_PERSISTENCE_INTERVAL == 0) { - blockchainManager.persistToFile(); + synchronized (blockchainManager) { + blockchainManager.persistToFile(); + } } int epochLeader = determineEpochLeader(epoch); AppLogger.logInfo("#### EPOCH = " + epoch + " LEADER = " + epochLeader + " ####"); @@ -164,8 +168,10 @@ private void advanceEpoch() { if (localNodeId == epochLeader) { try { if (!isClientGeneratingTransactions || !pendingClientTransactions.isEmpty()) { - AppLogger.logDebug("Node " + localNodeId + " is leader: proposing new block"); - proposeNewBlock(epoch); + synchronized (blockchainManager) { + AppLogger.logDebug("Node " + localNodeId + " is leader: proposing new block"); + proposeNewBlock(epoch); + } } } catch (NoSuchAlgorithmException e) { AppLogger.logError("Error proposing new block: " + e.getMessage(), e); @@ -228,12 +234,14 @@ private Transaction[] collectBlockTransactions() { private void processMessage(Message message) { AppLogger.logDebug("Processing message from " + message.sender() + ": " + message.type()); - switch (message.type()) { - case JOIN -> handleJoinRequest(message); - case PROPOSE -> handleProposalMessage(message); - case VOTE -> handleVoteMessage(message); - case UPDATE -> handleUpdateMessage(message); - default -> {} + synchronized (blockchainManager) { + switch (message.type()) { + case JOIN -> handleJoinRequest(message); + case PROPOSE -> handleProposalMessage(message); + case VOTE -> handleVoteMessage(message); + case UPDATE -> handleUpdateMessage(message); + default -> {} + } } } diff --git a/src/utils/application/Block.java b/src/utils/application/Block.java index 5a40e51..ec5b7c8 100644 --- a/src/utils/application/Block.java +++ b/src/utils/application/Block.java @@ -5,14 +5,16 @@ import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.Base64; +import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.IntStream; +import utils.logs.AppLogger; public record Block(byte[] parentHash, Integer epoch, Integer length, Transaction[] transactions) implements Content { private static final Pattern BLOCK_REGEX = Pattern.compile( - "Block\\[(?\\d+),(?\\d+),(?.*),\\[(?.*)]]" + "Block\\{(?\\d+),(?\\d+),(?.*),\\&(?.*)&}" ); public Block(byte[] parentHash, Integer epoch, Integer length, Transaction[] transactions) { @@ -36,7 +38,8 @@ public byte[] getSHA1() { for (Transaction transaction : transactions) { ByteBuffer transactionBuffer = ByteBuffer.allocate(24); transactionBuffer.putLong(transaction.id()); - transactionBuffer.putDouble(transaction.amount()); + double amount = Double.parseDouble(String.format(Locale.US, "%.2f", transaction.amount())); + transactionBuffer.putDouble(amount); transactionBuffer.putInt(transaction.sender()); transactionBuffer.putInt(transaction.receiver()); sha1.update(transactionBuffer.array()); @@ -78,30 +81,35 @@ public String toStringSummary() { ); } + public String getPersistenceString() { + AppLogger.logWarning("[BLOCK] I (" + epoch + ") have these many transactions..."); + AppLogger.logWarning("[BLOCK] " + transactions.length); + return "Block{%s,%s,%s,&%s&}".formatted( + epoch, + length, + Base64.getEncoder().encodeToString(parentHash), + Arrays.stream(transactions).map(Transaction::getPersistenceString).collect(Collectors.joining(",")) + ); + } + public static Block fromPersistenceString(String persistenceString) { Matcher matcher = BLOCK_REGEX.matcher(persistenceString); if (!matcher.matches()) { return null; } - Integer epoch = Integer.parseInt(matcher.group("epoch")); - Integer length = Integer.parseInt(matcher.group("length")); + int epoch = Integer.parseInt(matcher.group("epoch")); + int length = Integer.parseInt(matcher.group("length")); byte[] parentHash = Base64.getDecoder().decode(matcher.group("parentHash")); String transactionsString = matcher.group("transactions"); - Transaction[] transactions = transactionsString.isEmpty() ? new Transaction[0] : Arrays.stream(transactionsString.substring(1, transactionsString.length() - 1).split(",")) + AppLogger.logWarning("[BLOCK] I (" + epoch + ") HAVE THESE TRANSACTIONS..."); + AppLogger.logWarning("[BLOCK] " + transactionsString); + Transaction[] transactions = transactionsString.isEmpty() ? new Transaction[0] : + Arrays.stream(transactionsString.split(",(?=Tx\\<)")) .map(Transaction::fromPersistenceString) .toArray(Transaction[]::new); return new Block(parentHash, epoch, length, transactions); } - - public String getPersistenceString() { - return "Block[%s,%s,%s,[%s]]".formatted( - epoch, - length, - Base64.getEncoder().encodeToString(parentHash), - Arrays.stream(transactions).map(Transaction::getPersistenceString).collect(Collectors.joining(",")) - ); - } } diff --git a/src/utils/application/Hash.java b/src/utils/application/Hash.java index 1d26e99..f0111b5 100644 --- a/src/utils/application/Hash.java +++ b/src/utils/application/Hash.java @@ -5,10 +5,6 @@ public record Hash(byte[] hash) { - public static Hash fromPersistenceString(String persistenceString) { - return new Hash(Base64.getDecoder().decode(persistenceString)); - } - @Override public boolean equals(Object o) { if (!(o instanceof Hash(byte[] hash1))) return false; @@ -24,4 +20,8 @@ public int hashCode() { public String getPersistenceString() { return Base64.getEncoder().encodeToString(hash); } + + public static Hash fromPersistenceString(String persistenceString) { + return new Hash(Base64.getDecoder().decode(persistenceString)); + } } diff --git a/src/utils/application/Transaction.java b/src/utils/application/Transaction.java index 97f4fba..575276c 100644 --- a/src/utils/application/Transaction.java +++ b/src/utils/application/Transaction.java @@ -1,29 +1,30 @@ package utils.application; import java.io.Serializable; +import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; public record Transaction(Long id, Double amount, Integer sender, Integer receiver) implements Serializable { - private static final Pattern TX_REGEX = Pattern.compile("Tx\\[(?\\d+),(?\\d+(.\\d+)?),(?\\d+),(?\\d+)\\]"); + private static final Pattern TX_REGEX = Pattern.compile("Tx\\<(?\\d+),(?\\d+(.\\d+)?),(?\\d+),(?\\d+)>"); public String toStringSummary() { return String.format("id=%d, %d→%d: %.2f", id, sender, receiver, amount); } + public String getPersistenceString() { + return String.format(Locale.US, "Tx<%d,%.2f,%d,%d>", id, amount, sender, receiver); + } + public static Transaction fromPersistenceString(String persistenceString) { Matcher matcher = TX_REGEX.matcher(persistenceString); if (!matcher.matches()) { return null; } - Long id = Long.parseLong(matcher.group("id")); - Double amount = Double.parseDouble(matcher.group("amount")); - Integer sender = Integer.parseInt(matcher.group("sender")); - Integer receiver = Integer.parseInt(matcher.group("receiver")); + long id = Long.parseLong(matcher.group("id")); + double amount = Double.parseDouble(matcher.group("amount")); + int sender = Integer.parseInt(matcher.group("sender")); + int receiver = Integer.parseInt(matcher.group("receiver")); return new Transaction(id, amount, sender, receiver); } - - public String getPersistenceString() { - return "Tx[%d,%f,%d,%d]".formatted(id, amount, sender, receiver); - } } From 5082275aa9d2250f6dc41c2a9e270c94fff73a55 Mon Sep 17 00:00:00 2001 From: sousanamain Date: Thu, 4 Dec 2025 10:20:59 +0000 Subject: [PATCH 14/16] update makefile's clean to clear output folder --- makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/makefile b/makefile index d938a36..04b63dc 100644 --- a/makefile +++ b/makefile @@ -15,3 +15,4 @@ compile: $(OUT_DIR) clean: rm -rf out + rm -rf output From b983b0e0f4bd58f030636e90b395cfc93689e33d Mon Sep 17 00:00:00 2001 From: sousanamain Date: Thu, 4 Dec 2025 10:23:52 +0000 Subject: [PATCH 15/16] fix some minor issues on the implementation according to persistence (logging is still pending...), also, now instead of storing the blockNode twice, the "blockchain" is just a chain of hashes. (this code REALLY needs to be reviewed and to ensure every function is needed) --- src/app/BlockchainManager.java | 127 ++++++++++++++++++--------- src/app/PersistenceFilesManager.java | 86 ++++++++++-------- 2 files changed, 134 insertions(+), 79 deletions(-) diff --git a/src/app/BlockchainManager.java b/src/app/BlockchainManager.java index a854fa1..0704a78 100644 --- a/src/app/BlockchainManager.java +++ b/src/app/BlockchainManager.java @@ -20,20 +20,20 @@ public class BlockchainManager { private final Hash genesisParentHash; private final Map blockNodesByHash = new HashMap<>(); - private final Map> blockchainByParentHash = new HashMap<>(); + private final Map> blockchainByParentHash = new HashMap<>(); private final Set recoveredBlocks = new HashSet<>(); private final Set pendingProposals = new HashSet<>(); private final PersistenceFilesManager persistenceManager; - private int mostRecentNotarizedEpoch = -1; + private int mostRecentNotarizedEpoch; public BlockchainManager(Path outputPath) { Path logFilePath = outputPath.resolve(LOG_FILE_NAME); Path blockchainFilePath = outputPath.resolve(BLOCK_CHAIN_FILE_NAME); persistenceManager = new PersistenceFilesManager(logFilePath, blockchainFilePath, outputPath); mostRecentNotarizedEpoch = persistenceManager.initializeFromFile( - blockNodesByHash, blockchainByParentHash, recoveredBlocks, pendingProposals + blockNodesByHash, blockchainByParentHash, recoveredBlocks, pendingProposals ); BlockNode genesisNode = new BlockNode(GENESIS_BLOCK, true); @@ -49,8 +49,8 @@ public BlockchainManager(Path outputPath) { AppLogger.logWarning("GENESIS NOT INSERTED"); } - List genesisRoot = new LinkedList<>(); - genesisRoot.add(genesisNode); + List genesisRoot = new LinkedList<>(); + genesisRoot.add(genesisHash); blockchainByParentHash.putIfAbsent(genesisParentHash, genesisRoot); blockchainByParentHash.putIfAbsent(genesisHash, new LinkedList<>()); @@ -63,32 +63,31 @@ public void persistToFile() { private String getPersistenceString() { StringBuilder sb = new StringBuilder(); + blockNodesByHash.forEach((hash, blockNode) -> { sb.append("%s:%s".formatted(hash.getPersistenceString(), blockNode.getPersistenceString())); sb.append("\n"); }); sb.append("\n\n"); - blockchainByParentHash.forEach((hash, children) -> { - sb.append("%s:[%s]".formatted( - hash.getPersistenceString(), - children.stream().map(BlockNode::getPersistenceString).collect(Collectors.joining(",")) - ) - ); + + blockchainByParentHash.forEach((hash, childHashes) -> { + String childrenStr = childHashes.stream() + .map(Hash::getPersistenceString) + .collect(Collectors.joining(",")); + + sb.append("%s:[%s]".formatted(hash.getPersistenceString(), childrenStr)); sb.append("\n"); }); + sb.append("\n\n"); sb.append("%s".formatted(recoveredBlocks.stream().map(BlockNode::getPersistenceString).collect(Collectors.joining("\n")))); - sb.append("\n\n\n"); + sb.append("\n\n"); sb.append("%s".formatted(pendingProposals.stream().map(Block::getPersistenceString).collect(Collectors.joining("\n")))); return sb.toString(); } public List getBiggestNotarizedChain() { AppLogger.logWarning("STARTING THE SEARCH..."); - AppLogger.logWarning("LENGTH BLOCK NODES BY HASH: " + blockNodesByHash.size()); - AppLogger.logWarning("LENGTH BLOCK CHAIN BY PARENT HASH: " + blockchainByParentHash.size()); - AppLogger.logWarning("LENGTH OF GENESIS PARENT CHILDREN: " + blockchainByParentHash.get(genesisParentHash).size()); - AppLogger.logWarning("GENESIS PARENT HASH: " + Base64.getEncoder().encodeToString(genesisParentHash.hash())); return findBiggestChainMatching(genesisParentHash, _ -> true); } @@ -102,18 +101,29 @@ private List findBiggestChainMatching(Hash parentHash, Predicate childrenHashes = blockchainByParentHash.get(parentHash); + if (childrenHashes == null) childrenHashes = new ArrayList<>(); + + for (Hash childHash : childrenHashes) { + BlockNode child = blockNodesByHash.get(childHash); + if (child != null) { + AppLogger.logWarning("\tCHILD"); + AppLogger.logWarning("\t" + child.getPersistenceString()); + AppLogger.logWarning("\tWITH HASH: " + Base64.getEncoder().encodeToString(child.block().getSHA1())); + } } chain.addAll( - blockchainByParentHash.get(parentHash).stream() + childrenHashes.stream() + .map(blockNodesByHash::get) // Convert Hash -> Node + .filter(Objects::nonNull) .filter(predicate) .map(child -> findBiggestChainMatching(new Hash(child.block().getSHA1()), predicate)) .max(Comparator.comparing(List::size)) @@ -124,7 +134,10 @@ private List findBiggestChainMatching(Hash parentHash, Predicate blockchainByParentHash.get(parentHash).isEmpty()) + .filter(parentHash -> { + List children = blockchainByParentHash.get(parentHash); + return children == null || children.isEmpty(); + }) .map(blockNodesByHash::get) .filter(Objects::nonNull) .anyMatch(blockNode -> proposedBlock.length() > blockNode.block().length()); @@ -153,7 +166,7 @@ public void notarizeBlock(Block blockHeader) { BlockNode blockNode = blockNodesByHash.get(blockHash); blockchainByParentHash.computeIfAbsent(parentHash, _ -> new LinkedList<>()) - .add(blockNode); + .add(blockHash); blockchainByParentHash.computeIfAbsent(blockHash, _ -> new LinkedList<>()); if (blockHeader.epoch() > mostRecentNotarizedEpoch) { @@ -172,7 +185,13 @@ private void finalizeAndPropagate(BlockNode targetBlock) { } private void propagateFinalizedStatusDownstream(Hash parentHash) { - for (BlockNode child : blockchainByParentHash.get(parentHash)) { + List childrenHashes = blockchainByParentHash.get(parentHash); + if (childrenHashes == null || childrenHashes.isEmpty()) return; + + for (Hash childHash : childrenHashes) { + BlockNode child = blockNodesByHash.get(childHash); + if (child == null) continue; + if (child.finalized()) { finalizeChainUpstream(child); } @@ -189,7 +208,22 @@ private void finalizeByConsecutiveEpochBlocks(BlockNode anchorBlock) { finalizationCandidate.addAll(blocksAfter); if (finalizationCandidate.size() >= FINALIZATION_MIN_SIZE) { - finalizeChainUpstream(finalizationCandidate.getLast()); + BlockNode lastBlock = finalizationCandidate.getLast(); + finalizeChainUpstream(lastBlock); + verifyAndFinalizeBasedOnChildren(lastBlock); + } + } + + private void verifyAndFinalizeBasedOnChildren(BlockNode lastBlock) { + List childrenHashes = blockchainByParentHash.get(new Hash(lastBlock.block().getSHA1())); + + if (childrenHashes != null && !childrenHashes.isEmpty()) { + for (Hash childHash : childrenHashes) { + BlockNode child = blockNodesByHash.get(childHash); + if (child != null && !child.finalized()) { + finalizeByConsecutiveEpochBlocks(child); + } + } } } @@ -199,10 +233,13 @@ private List collectFollowingConsecutiveBlocks(BlockNode startBlock) int currentEpoch = startBlock.block().epoch(); for (int i = 1; i < FINALIZATION_MIN_SIZE; i++) { - List children = blockchainByParentHash.get(new Hash(currentBlock.block().getSHA1())); + List childHashes = blockchainByParentHash.get(new Hash(currentBlock.block().getSHA1())); + if (childHashes == null) break; int targetEpoch = currentEpoch + 1; - Optional nextBlock = children.stream() + Optional nextBlock = childHashes.stream() + .map(blockNodesByHash::get) + .filter(Objects::nonNull) .filter(blockNode -> blockNode.block().epoch() == targetEpoch) .findFirst(); @@ -240,9 +277,7 @@ private List collectPrecedingConsecutiveBlocks(BlockNode startBlock) } private void finalizeChainUpstream(BlockNode anchorBlock) { - BlockNode parentBlock = blockNodesByHash.get(new Hash(anchorBlock.block().parentHash())); - - for (BlockNode currentBlock = parentBlock; + for (BlockNode currentBlock = anchorBlock; currentBlock != null && !isGenesis(currentBlock); currentBlock = blockNodesByHash.get(new Hash(currentBlock.block().parentHash()))) { if (currentBlock.finalized()) break; @@ -259,8 +294,7 @@ public int getLastNotarizedEpoch() { } public List getBlocksInEpochRange(int fromEpoch, int toEpoch) { - return blockchainByParentHash.values().stream() - .flatMap(List::stream) + return blockNodesByHash.values().stream() .sorted(Comparator.comparing(block -> block.block().epoch())) .dropWhile(block -> block.block().epoch() < fromEpoch) .takeWhile(block -> block.block().epoch() < toEpoch) @@ -268,18 +302,31 @@ public List getBlocksInEpochRange(int fromEpoch, int toEpoch) { } public void insertMissingBlocks(List missingBlocks) { - for (BlockNode blockNode : missingBlocks) { - if (!recoveredBlocks.add(blockNode)) continue; + Set affectedBlocks = new HashSet<>(); + for (BlockNode blockNode : missingBlocks) { Hash parentHash = new Hash(blockNode.block().parentHash()); Hash blockHash = new Hash(blockNode.block().getSHA1()); - blockchainByParentHash.computeIfAbsent(parentHash, _ -> new LinkedList<>()) - .add(blockNode); - blockchainByParentHash.computeIfAbsent(blockHash, _ -> new LinkedList<>()); + recoveredBlocks.add(blockNode); blockNodesByHash.put(blockHash, blockNode); - finalizeAndPropagate(blockNode); + + List siblings = blockchainByParentHash.computeIfAbsent(parentHash, _ -> new LinkedList<>()); + + if (!siblings.contains(blockHash)) { + siblings.add(blockHash); + } + + blockchainByParentHash.computeIfAbsent(blockHash, _ -> new LinkedList<>()); + + affectedBlocks.add(parentHash); + affectedBlocks.add(blockHash); } + + affectedBlocks.stream() + .map(blockNodesByHash::get) + .filter(Objects::nonNull) + .forEach(this::finalizeAndPropagate); } public void printBiggestFinalizedChain() { diff --git a/src/app/PersistenceFilesManager.java b/src/app/PersistenceFilesManager.java index e3233b7..8ff8122 100644 --- a/src/app/PersistenceFilesManager.java +++ b/src/app/PersistenceFilesManager.java @@ -1,20 +1,19 @@ package app; +import utils.application.Block; +import utils.application.Hash; +import utils.logs.AppLogger; + import java.io.IOException; import java.nio.file.FileAlreadyExistsException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; -import java.util.Base64; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; -import utils.application.Block; -import utils.application.Hash; -import utils.logs.AppLogger; - public class PersistenceFilesManager { private final Path logFilePath; @@ -44,24 +43,33 @@ private void createIfNotExistsOutputFiles(Path outputPath) throws IOException { } public int initializeFromFile( - Map blockNodesByHash, Map> blockchainByParentHash, - Set recoveredBlocks, Set pendingProposals + Map blockNodesByHash, + Map> blockchainByParentHash, + Set recoveredBlocks, + Set pendingProposals ) { String content = ""; - + try { content = Files.readString(blockchainFilePath); - } catch (IOException e) {} + } catch (IOException e) { + } if (content.isBlank()) return -1; String[] sections = content.split("\n\n"); - String[] blockNodeLines = sections[0].trim().split("\n"); - initializeBlockNodesByHash(blockNodesByHash, blockNodeLines); + int mostRecentEpoch = -1; - String[] chainLines = sections[1].trim().split("\n"); - int mostRecentEpoch = initializeBlockChainByParentHash(blockchainByParentHash, chainLines); + if (sections.length > 0) { + String[] blockNodeLines = sections[0].trim().split("\n"); + mostRecentEpoch = initializeBlockNodesByHash(blockNodesByHash, blockNodeLines); + } + + if (sections.length > 1) { + String[] chainLines = sections[1].trim().split("\n"); + initializeBlockChainByParentHash(blockchainByParentHash, chainLines); + } if (sections.length > 2) { String[] recoveredLines = sections[2].trim().split("\n"); @@ -75,7 +83,9 @@ public int initializeFromFile( return mostRecentEpoch; } - private void initializeBlockNodesByHash(Map blockNodesByHash, String[] blockNodeLines) { + private int initializeBlockNodesByHash(Map blockNodesByHash, String[] blockNodeLines) { + int maxEpoch = -1; + for (String line : blockNodeLines) { if (line.isBlank()) continue; @@ -86,7 +96,6 @@ private void initializeBlockNodesByHash(Map blockNodesByHash, S String blockNodeStr = line.substring(colonIndex + 1).trim(); AppLogger.logWarning("[PERSISTENCE] BLOCKNODE OF HASH " + hashStr); - AppLogger.logWarning("[PERSISTENCE] " + blockNodeStr); try { Hash hash = Hash.fromPersistenceString(hashStr); @@ -94,16 +103,19 @@ private void initializeBlockNodesByHash(Map blockNodesByHash, S if (blockNode != null) { blockNodesByHash.put(hash, blockNode); + + if (blockNode.block().epoch() > maxEpoch) { + maxEpoch = blockNode.block().epoch(); + } } } catch (IllegalArgumentException e) { AppLogger.logWarning("Failed to parse blockNode entry: " + line); } } + return maxEpoch; } - private int initializeBlockChainByParentHash(Map> blockchainByParentHash, String[] chainLines) { - int mostRecentEpoch = -1; - + private void initializeBlockChainByParentHash(Map> blockchainByParentHash, String[] chainLines) { for (String line : chainLines) { if (line.isBlank()) continue; @@ -111,41 +123,38 @@ private int initializeBlockChainByParentHash(Map> blockcha if (colonIndex == -1) continue; int startBracketIndex = line.indexOf("["); - if (startBracketIndex == -1) continue; - int lastBracketIndex = line.lastIndexOf("]"); - if (lastBracketIndex == -1) continue; + if (startBracketIndex == -1 || lastBracketIndex == -1) continue; String hashStr = line.substring(0, colonIndex).trim(); String childrenStr = line.substring(startBracketIndex + 1, lastBracketIndex); try { - Hash hash = Hash.fromPersistenceString(hashStr); + Hash parentHash = Hash.fromPersistenceString(hashStr); - List children = new LinkedList<>(); + List childrenHashes = new LinkedList<>(); if (!childrenStr.isBlank()) { - String[] blockNodeStrings = childrenStr.split(",(?=BlockNode\\[)"); - for (String blockNodeStr : blockNodeStrings) { - BlockNode blockNode = BlockNode.fromPersistenceString(blockNodeStr.trim()); - AppLogger.logWarning("[PERSISTENCE] FOUND A BLOCK CHILD OF " + Base64.getEncoder().encodeToString(hash.hash())); - if (blockNode != null) { - AppLogger.logWarning("[PERSISTENCE] AND IT IS NOT NULL!"); - children.add(blockNode); - int blockEpoch = blockNode.block().epoch(); - if (blockEpoch > mostRecentEpoch) { - mostRecentEpoch = blockEpoch; - } + String[] childHashStrings = childrenStr.split(","); + + for (String childHashStr : childHashStrings) { + if (childHashStr.isBlank()) continue; + + try { + Hash childHash = Hash.fromPersistenceString(childHashStr.trim()); + childrenHashes.add(childHash); + AppLogger.logWarning("[PERSISTENCE] FOUND CHILD HASH FOR PARENT " + hashStr); + } catch (Exception e) { + AppLogger.logWarning("Could not parse child hash: " + childHashStr); } } } - blockchainByParentHash.put(hash, children); + blockchainByParentHash.put(parentHash, childrenHashes); } catch (IllegalArgumentException e) { AppLogger.logWarning("Failed to parse chain entry: " + line); } } - return mostRecentEpoch; } private void initializeRecoveredBlocks(Set recoveredBlocks, String[] recoveredLines) { @@ -167,7 +176,7 @@ private void initializeRecoveredBlocks(Set recoveredBlocks, String[] private void initializePendingProposals(Set pendingProposals, String[] pendingLines) { for (String line : pendingLines) { if (line.isBlank()) continue; - + try { Block block = Block.fromPersistenceString(line.trim()); @@ -186,5 +195,4 @@ public void persistToFile(String persistenceString) { Files.writeString(logFilePath, "", StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE); } catch (IOException ignored) {} } - -} +} \ No newline at end of file From c425de54eee8c621888d28408c9e6682c01b87e9 Mon Sep 17 00:00:00 2001 From: sousanamain Date: Thu, 4 Dec 2025 17:26:48 +0000 Subject: [PATCH 16/16] add logging of operations such that sequential persistence only needs to be made with an x round interval --- src/app/BlockchainManager.java | 31 ++++++++++++- src/app/Operation.java | 68 ++++++++++++++++++++++++++++ src/app/PersistenceFilesManager.java | 46 ++++++++++++++++++- 3 files changed, 142 insertions(+), 3 deletions(-) create mode 100644 src/app/Operation.java diff --git a/src/app/BlockchainManager.java b/src/app/BlockchainManager.java index 0704a78..b032efb 100644 --- a/src/app/BlockchainManager.java +++ b/src/app/BlockchainManager.java @@ -35,6 +35,7 @@ public BlockchainManager(Path outputPath) { mostRecentNotarizedEpoch = persistenceManager.initializeFromFile( blockNodesByHash, blockchainByParentHash, recoveredBlocks, pendingProposals ); + persistenceManager.getPendingOperations().forEach(this::processOperation); BlockNode genesisNode = new BlockNode(GENESIS_BLOCK, true); genesisParentHash = new Hash(GENESIS_BLOCK.parentHash()); @@ -57,6 +58,20 @@ public BlockchainManager(Path outputPath) { blockNodesByHash.putIfAbsent(genesisHash, genesisNode); } + private void processOperation(Operation operation) { + switch (operation.getType()) { + case PROPOSE -> onPropose(operation.getBlock()); + case NOTARIZE -> notarizeBlock(operation.getBlock()); + case FINALIZE -> { + Hash hash = new Hash(operation.getBlock().getSHA1()); + BlockNode node = blockNodesByHash.get(hash); + if (node != null && !node.finalized()) { + finalizeChainUpstream(node); + } + } + } + } + public void persistToFile() { persistenceManager.persistToFile(getPersistenceString()); } @@ -122,7 +137,7 @@ private List findBiggestChainMatching(Hash parentHash, Predicate Node + .map(blockNodesByHash::get) .filter(Objects::nonNull) .filter(predicate) .map(child -> findBiggestChainMatching(new Hash(child.block().getSHA1()), predicate)) @@ -133,6 +148,12 @@ private List findBiggestChainMatching(Hash parentHash, Predicate { List children = blockchainByParentHash.get(parentHash); @@ -148,9 +169,11 @@ public boolean onPropose(Block proposedBlock) { pendingProposals.add(proposedBlock); - Hash blockHash = new Hash(proposedBlock.getSHA1()); BlockNode blockNode = new BlockNode(proposedBlock, false); blockNodesByHash.put(blockHash, blockNode); + + persistenceManager.appendToLog(new Operation.Propose(proposedBlock)); + return true; } @@ -175,6 +198,8 @@ public void notarizeBlock(Block blockHeader) { pendingProposals.remove(blockHeader); + persistenceManager.appendToLog(new Operation.Notarize(fullBlock)); + AppLogger.logInfo("Block notarized: epoch " + blockHeader.epoch() + " length " + blockHeader.length()); finalizeAndPropagate(blockNode); } @@ -282,6 +307,8 @@ private void finalizeChainUpstream(BlockNode anchorBlock) { currentBlock = blockNodesByHash.get(new Hash(currentBlock.block().parentHash()))) { if (currentBlock.finalized()) break; currentBlock.finalizeBlock(); + + persistenceManager.appendToLog(new Operation.Finalize(currentBlock.block())); } } diff --git a/src/app/Operation.java b/src/app/Operation.java new file mode 100644 index 0000000..efb7a32 --- /dev/null +++ b/src/app/Operation.java @@ -0,0 +1,68 @@ +package app; + +import utils.application.Block; + +public interface Operation { + Type getType(); + + Block getBlock(); + + String getPersistenceString(); + + enum Type { + PROPOSE, + NOTARIZE, + FINALIZE + } + + record Propose(Block block) implements Operation { + @Override + public Type getType() { + return Type.PROPOSE; + } + + @Override + public Block getBlock() { + return block; + } + + @Override + public String getPersistenceString() { + return "PROPOSE:" + block.getPersistenceString(); + } + } + + record Notarize(Block block) implements Operation { + @Override + public Type getType() { + return Type.NOTARIZE; + } + + @Override + public Block getBlock() { + return block; + } + + @Override + public String getPersistenceString() { + return "NOTARIZE:" + block.getPersistenceString(); + } + } + + record Finalize(Block block) implements Operation { + @Override + public Type getType() { + return Type.FINALIZE; + } + + @Override + public Block getBlock() { + return block; + } + + @Override + public String getPersistenceString() { + return "FINALIZE:" + block.getPersistenceString(); + } + } +} diff --git a/src/app/PersistenceFilesManager.java b/src/app/PersistenceFilesManager.java index 8ff8122..282b1af 100644 --- a/src/app/PersistenceFilesManager.java +++ b/src/app/PersistenceFilesManager.java @@ -53,7 +53,7 @@ public int initializeFromFile( try { content = Files.readString(blockchainFilePath); - } catch (IOException e) { + } catch (IOException _) { } if (content.isBlank()) return -1; @@ -195,4 +195,48 @@ public void persistToFile(String persistenceString) { Files.writeString(logFilePath, "", StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE); } catch (IOException ignored) {} } + + public List getPendingOperations() { + List operations = new LinkedList<>(); + List lines; + + try { + lines = Files.readAllLines(logFilePath); + } catch (IOException e) { + return operations; + } + + for (String line : lines) { + if (line.isBlank()) continue; + + int splitIndex = line.indexOf(":"); + if (splitIndex == -1) continue; + + String type = line.substring(0, splitIndex); + String blockData = line.substring(splitIndex + 1); + + try { + Block block = Block.fromPersistenceString(blockData); + if (block == null) continue; + + switch (type) { + case "PROPOSE" -> operations.add(new Operation.Propose(block)); + case "NOTARIZE" -> operations.add(new Operation.Notarize(block)); + case "FINALIZE" -> operations.add(new Operation.Finalize(block)); + } + } catch (Exception e) { + AppLogger.logWarning("Corrupt log entry skipped: " + line); + } + } + return operations; + } + + public void appendToLog(Operation operation) { + String logEntry = operation.getPersistenceString() + "\n"; + try { + Files.writeString(logFilePath, logEntry, StandardOpenOption.APPEND, StandardOpenOption.CREATE); + } catch (IOException e) { + AppLogger.logWarning("Failed to append operation to log: " + e.getMessage()); + } + } } \ No newline at end of file