diff --git a/src/main/java/org/qortal/block/Block.java b/src/main/java/org/qortal/block/Block.java index ddfe247a..fca6fa84 100644 --- a/src/main/java/org/qortal/block/Block.java +++ b/src/main/java/org/qortal/block/Block.java @@ -10,7 +10,6 @@ import java.math.BigInteger; import java.math.RoundingMode; import java.nio.charset.StandardCharsets; import java.text.DecimalFormat; -import java.text.MessageFormat; import java.text.NumberFormat; import java.util.*; import java.util.stream.Collectors; @@ -90,7 +89,8 @@ public class Block { ONLINE_ACCOUNT_UNKNOWN(71), ONLINE_ACCOUNT_SIGNATURES_MISSING(72), ONLINE_ACCOUNT_SIGNATURES_MALFORMED(73), - ONLINE_ACCOUNT_SIGNATURE_INCORRECT(74); + ONLINE_ACCOUNT_SIGNATURE_INCORRECT(74), + ONLINE_ACCOUNT_NONCE_INCORRECT(75); public final int value; @@ -409,6 +409,31 @@ public class Block { } } + // Add nonces to the end of the online accounts signatures if mempow is active + if (timestamp >= BlockChain.getInstance().getOnlineAccountsMemoryPoWTimestamp()) { + try { + // Create ordered list of nonce values + List nonces = new ArrayList<>(); + for (int i = 0; i < onlineAccountsCount; ++i) { + Integer accountIndex = accountIndexes.get(i); + OnlineAccountData onlineAccountData = indexedOnlineAccounts.get(accountIndex); + nonces.add(onlineAccountData.getNonce()); + } + + // Encode the nonces to a byte array + byte[] encodedNonces = BlockTransformer.encodeOnlineAccountNonces(nonces); + + // Append the encoded nonces to the encoded online account signatures + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + outputStream.write(onlineAccountsSignatures); + outputStream.write(encodedNonces); + onlineAccountsSignatures = outputStream.toByteArray(); + } + catch (TransformationException | IOException e) { + return null; + } + } + byte[] minterSignature = minter.sign(BlockTransformer.getBytesForMinterSignature(parentBlockData, minter.getPublicKey(), encodedOnlineAccounts)); @@ -1016,12 +1041,15 @@ public class Block { if (this.blockData.getOnlineAccountsSignatures() == null || this.blockData.getOnlineAccountsSignatures().length == 0) return ValidationResult.ONLINE_ACCOUNT_SIGNATURES_MISSING; - if (this.blockData.getTimestamp() >= BlockChain.getInstance().getAggregateSignatureTimestamp()) { - // We expect just the one, aggregated signature - if (this.blockData.getOnlineAccountsSignatures().length != Transformer.SIGNATURE_LENGTH) + final int signaturesLength = (this.blockData.getTimestamp() >= BlockChain.getInstance().getAggregateSignatureTimestamp()) ? Transformer.SIGNATURE_LENGTH : onlineRewardShares.size() * Transformer.SIGNATURE_LENGTH; + final int noncesLength = onlineRewardShares.size() * Transformer.INT_LENGTH; + + if (this.blockData.getTimestamp() >= BlockChain.getInstance().getOnlineAccountsMemoryPoWTimestamp()) { + // We expect nonces to be appended to the online accounts signatures + if (this.blockData.getOnlineAccountsSignatures().length != signaturesLength + noncesLength) return ValidationResult.ONLINE_ACCOUNT_SIGNATURES_MALFORMED; } else { - if (this.blockData.getOnlineAccountsSignatures().length != onlineRewardShares.size() * Transformer.SIGNATURE_LENGTH) + if (this.blockData.getOnlineAccountsSignatures().length != signaturesLength) return ValidationResult.ONLINE_ACCOUNT_SIGNATURES_MALFORMED; } @@ -1029,8 +1057,37 @@ public class Block { long onlineTimestamp = this.blockData.getOnlineAccountsTimestamp(); byte[] onlineTimestampBytes = Longs.toByteArray(onlineTimestamp); + byte[] encodedOnlineAccountSignatures = this.blockData.getOnlineAccountsSignatures(); + + // Split online account signatures into signature(s) + nonces, then validate the nonces + if (this.blockData.getTimestamp() >= BlockChain.getInstance().getOnlineAccountsMemoryPoWTimestamp()) { + byte[] extractedSignatures = BlockTransformer.extract(encodedOnlineAccountSignatures, 0, signaturesLength); + byte[] extractedNonces = BlockTransformer.extract(encodedOnlineAccountSignatures, signaturesLength, onlineRewardShares.size() * Transformer.INT_LENGTH); + encodedOnlineAccountSignatures = extractedSignatures; + + List nonces = BlockTransformer.decodeOnlineAccountNonces(extractedNonces); + + // Build block's view of online accounts (without signatures, as we don't need them here) + Set onlineAccounts = new HashSet<>(); + for (int i = 0; i < onlineRewardShares.size(); ++i) { + Integer nonce = nonces.get(i); + byte[] publicKey = onlineRewardShares.get(i).getRewardSharePublicKey(); + + OnlineAccountData onlineAccountData = new OnlineAccountData(onlineTimestamp, null, publicKey, nonce); + onlineAccounts.add(onlineAccountData); + } + + // Remove those already validated & cached by online accounts manager - no need to re-validate them + OnlineAccountsManager.getInstance().removeKnown(onlineAccounts, onlineTimestamp); + + // Validate the rest + for (OnlineAccountData onlineAccount : onlineAccounts) + if (!OnlineAccountsManager.getInstance().verifyMemoryPoW(onlineAccount, this.blockData.getTimestamp())) + return ValidationResult.ONLINE_ACCOUNT_NONCE_INCORRECT; + } + // Extract online accounts' timestamp signatures from block data. Only one signature if aggregated. - List onlineAccountsSignatures = BlockTransformer.decodeTimestampSignatures(this.blockData.getOnlineAccountsSignatures()); + List onlineAccountsSignatures = BlockTransformer.decodeTimestampSignatures(encodedOnlineAccountSignatures); if (this.blockData.getTimestamp() >= BlockChain.getInstance().getAggregateSignatureTimestamp()) { // Aggregate all public keys diff --git a/src/main/java/org/qortal/transform/block/BlockTransformer.java b/src/main/java/org/qortal/transform/block/BlockTransformer.java index b61d6900..48e79699 100644 --- a/src/main/java/org/qortal/transform/block/BlockTransformer.java +++ b/src/main/java/org/qortal/transform/block/BlockTransformer.java @@ -478,4 +478,44 @@ public class BlockTransformer extends Transformer { return signatures; } + public static byte[] encodeOnlineAccountNonces(List nonces) throws TransformationException { + try { + final int length = nonces.size() * Transformer.INT_LENGTH; + ByteArrayOutputStream bytes = new ByteArrayOutputStream(length); + + for (int i = 0; i < nonces.size(); ++i) { + Integer nonce = nonces.get(i); + if (nonce == null || nonce < 0) { + throw new TransformationException("Unable to serialize online account nonces due to invalid value"); + } + bytes.write(Ints.toByteArray(nonce)); + } + + return bytes.toByteArray(); + + } catch (IOException e) { + throw new TransformationException("Unable to serialize online account nonces", e); + } + } + + public static List decodeOnlineAccountNonces(byte[] encodedNonces) { + List nonces = new ArrayList<>(); + + ByteBuffer bytes = ByteBuffer.wrap(encodedNonces); + final int count = encodedNonces.length / Transformer.INT_LENGTH; + + for (int i = 0; i < count; i++) { + Integer nonce = bytes.getInt(); + nonces.add(nonce); + } + + return nonces; + } + + public static byte[] extract(byte[] input, int pos, int length) { + byte[] output = new byte[length]; + System.arraycopy(input, pos, output, 0, length); + return output; + } + } diff --git a/src/main/resources/blockchain.json b/src/main/resources/blockchain.json index 03169723..7e906e76 100644 --- a/src/main/resources/blockchain.json +++ b/src/main/resources/blockchain.json @@ -72,7 +72,7 @@ }, "genesisInfo": { "version": 4, - "timestamp": "1593450000000", + "timestamp": "1656777099000", "transactions": [ { "type": "ISSUE_ASSET", "assetName": "QORT", "description": "QORTAL coin", "quantity": 0, "isDivisible": true, "data": "{}" }, { "type": "ISSUE_ASSET", "assetName": "Legacy-QORA", "description": "Representative legacy QORA", "quantity": 0, "isDivisible": true, "data": "{}", "isUnspendable": true }, diff --git a/src/test/java/org/qortal/test/BlockArchiveTests.java b/src/test/java/org/qortal/test/BlockArchiveTests.java index 32fd0283..3bfa4e84 100644 --- a/src/test/java/org/qortal/test/BlockArchiveTests.java +++ b/src/test/java/org/qortal/test/BlockArchiveTests.java @@ -361,7 +361,7 @@ public class BlockArchiveTests extends Common { assertEquals(0, repository.getBlockArchiveRepository().getBlockArchiveHeight()); // Write blocks 2-900 to the archive (using bulk method) - int fileSizeTarget = 425000; // Pre-calculated size of 900 blocks + int fileSizeTarget = 428600; // Pre-calculated size of 900 blocks assertTrue(HSQLDBDatabaseArchiving.buildBlockArchive(repository, fileSizeTarget)); // Ensure the block archive height has increased @@ -455,7 +455,7 @@ public class BlockArchiveTests extends Common { assertEquals(0, repository.getBlockArchiveRepository().getBlockArchiveHeight()); // Write blocks 2-900 to the archive (using bulk method) - int fileSizeTarget = 42000; // Pre-calculated size of approx 90 blocks + int fileSizeTarget = 42360; // Pre-calculated size of approx 90 blocks assertTrue(HSQLDBDatabaseArchiving.buildBlockArchive(repository, fileSizeTarget)); // Ensure 10 archive files have been created