From eb876e12c84fa6621902961269726d8f098d639a Mon Sep 17 00:00:00 2001 From: CalDescent Date: Fri, 1 Apr 2022 12:06:02 +0100 Subject: [PATCH] Modified block minting and validation to support extended OnlineAccountData. This doesn't require changes to the transformation of the outer Block components, since the "onlineAccountsSignatures" component is already variable length. It does however affect the encoding of the data within "onlineAccountsSignatures". New encoding becomes active once the block timestamp reaches onlineAccountsMemoryPoWTimestamp. --- src/main/java/org/qortal/block/Block.java | 58 ++++++---- .../transform/block/BlockTransformer.java | 103 ++++++++++++++++-- .../test/network/OnlineAccountsTests.java | 61 +++++++++++ 3 files changed, 194 insertions(+), 28 deletions(-) diff --git a/src/main/java/org/qortal/block/Block.java b/src/main/java/org/qortal/block/Block.java index 7800f2a1..3a53d61c 100644 --- a/src/main/java/org/qortal/block/Block.java +++ b/src/main/java/org/qortal/block/Block.java @@ -85,7 +85,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; @@ -313,6 +314,15 @@ public class Block { int version = parentBlock.getNextBlockVersion(); byte[] reference = parentBlockData.getSignature(); + // Qortal: minter is always a reward-share, so find actual minter and get their effective minting level + int minterLevel = Account.getRewardShareEffectiveMintingLevel(repository, minter.getPublicKey()); + if (minterLevel == 0) { + LOGGER.error("Minter effective level returned zero?"); + return null; + } + + long timestamp = calcTimestamp(parentBlockData, minter.getPublicKey(), minterLevel); + // Fetch our list of online accounts List onlineAccounts = OnlineAccountsManager.getInstance().getOnlineAccounts(); if (onlineAccounts.isEmpty()) { @@ -355,26 +365,13 @@ public class Block { byte[] encodedOnlineAccounts = BlockTransformer.encodeOnlineAccounts(onlineAccountsSet); int onlineAccountsCount = onlineAccountsSet.size(); - // Concatenate online account timestamp signatures (in correct order) - byte[] onlineAccountsSignatures = new byte[onlineAccountsCount * Transformer.SIGNATURE_LENGTH]; - for (int i = 0; i < onlineAccountsCount; ++i) { - Integer accountIndex = accountIndexes.get(i); - OnlineAccountData onlineAccountData = indexedOnlineAccounts.get(accountIndex); - System.arraycopy(onlineAccountData.getSignature(), 0, onlineAccountsSignatures, i * Transformer.SIGNATURE_LENGTH, Transformer.SIGNATURE_LENGTH); - } + // Build the onlineAccountsSignatures byte array + byte[] onlineAccountsSignatures = BlockTransformer.encodeOnlineAccountSignatures(indexedOnlineAccounts, + accountIndexes, onlineAccountsCount, timestamp); byte[] minterSignature = minter.sign(BlockTransformer.getBytesForMinterSignature(parentBlockData, minter.getPublicKey(), encodedOnlineAccounts)); - // Qortal: minter is always a reward-share, so find actual minter and get their effective minting level - int minterLevel = Account.getRewardShareEffectiveMintingLevel(repository, minter.getPublicKey()); - if (minterLevel == 0) { - LOGGER.error("Minter effective level returned zero?"); - return null; - } - - long timestamp = calcTimestamp(parentBlockData, minter.getPublicKey(), minterLevel); - int transactionCount = 0; byte[] transactionsSignature = null; int height = parentBlockData.getHeight() + 1; @@ -979,7 +976,14 @@ public class Block { if (this.blockData.getOnlineAccountsSignatures() == null || this.blockData.getOnlineAccountsSignatures().length == 0) return ValidationResult.ONLINE_ACCOUNT_SIGNATURES_MISSING; - if (this.blockData.getOnlineAccountsSignatures().length != onlineRewardShares.size() * Transformer.SIGNATURE_LENGTH) + // Verify the online account signatures length + int expectedLength; + if (this.blockData.getTimestamp() >= BlockChain.getInstance().getOnlineAccountsMemoryPoWTimestamp()) + expectedLength = onlineRewardShares.size() * (Transformer.SIGNATURE_LENGTH + Transformer.REDUCED_SIGNATURE_LENGTH + Transformer.INT_LENGTH + (OnlineAccountsManager.MAX_NONCE_COUNT * Transformer.INT_LENGTH)); + else + expectedLength = onlineRewardShares.size() * Transformer.SIGNATURE_LENGTH; + + if (this.blockData.getOnlineAccountsSignatures().length != expectedLength) return ValidationResult.ONLINE_ACCOUNT_SIGNATURES_MALFORMED; // Check signatures @@ -993,17 +997,25 @@ public class Block { List latestBlocksOnlineAccounts = OnlineAccountsManager.getInstance().getLatestBlocksOnlineAccounts(); // Extract online accounts' timestamp signatures from block data - List onlineAccountsSignatures = BlockTransformer.decodeTimestampSignatures(this.blockData.getOnlineAccountsSignatures()); + List onlineAccountsSignatures = BlockTransformer.decodeOnlineAccountSignatures( + this.blockData.getOnlineAccountsSignatures(), onlineRewardShares.size(), this.blockData.getTimestamp()); // We'll build up a list of online accounts to hand over to Controller if block is added to chain // and this will become latestBlocksOnlineAccounts (above) to reduce CPU load when we process next block... List ourOnlineAccounts = new ArrayList<>(); for (int i = 0; i < onlineAccountsSignatures.size(); ++i) { - byte[] signature = onlineAccountsSignatures.get(i); + // onlineAccountsSignatures will contain OnlineAccountData objects with at least a signature, and + // also a reduced block signature and nonce(s) if the mempow feature is active. + // It won't contain a public key or timestamp, so these must be added below. + OnlineAccountData onlineAccountSignatureData = onlineAccountsSignatures.get(i); + byte[] signature = onlineAccountSignatureData.getSignature(); + byte[] reducedBlockSignature = onlineAccountSignatureData.getReducedBlockSignature(); + List nonces = onlineAccountSignatureData.getNonces(); byte[] publicKey = onlineRewardShares.get(i).getRewardSharePublicKey(); - OnlineAccountData onlineAccountData = new OnlineAccountData(onlineTimestamp, signature, publicKey); + // It's simpler to create a new OnlineAccountData object rather than trying to modify the one we already have + OnlineAccountData onlineAccountData = new OnlineAccountData(onlineTimestamp, signature, publicKey, nonces, reducedBlockSignature); ourOnlineAccounts.add(onlineAccountData); // If signature is still current then no need to perform Ed25519 verify @@ -1018,6 +1030,10 @@ public class Block { if (!Crypto.verify(publicKey, signature, onlineTimestampBytes)) return ValidationResult.ONLINE_ACCOUNT_SIGNATURE_INCORRECT; + + if (this.blockData.getTimestamp() >= BlockChain.getInstance().getOnlineAccountsMemoryPoWTimestamp()) + if (!OnlineAccountsManager.getInstance().verifyMemoryPoW(onlineAccountData)) + return ValidationResult.ONLINE_ACCOUNT_NONCE_INCORRECT; } // All online accounts valid, so save our list of online accounts for potential later use diff --git a/src/main/java/org/qortal/transform/block/BlockTransformer.java b/src/main/java/org/qortal/transform/block/BlockTransformer.java index cce3e7d7..4621491b 100644 --- a/src/main/java/org/qortal/transform/block/BlockTransformer.java +++ b/src/main/java/org/qortal/transform/block/BlockTransformer.java @@ -6,11 +6,13 @@ import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Map; import org.qortal.block.Block; import org.qortal.block.BlockChain; import org.qortal.data.at.ATStateData; import org.qortal.data.block.BlockData; +import org.qortal.data.network.OnlineAccountData; import org.qortal.data.transaction.TransactionData; import org.qortal.repository.DataException; import org.qortal.transaction.Transaction; @@ -27,6 +29,8 @@ import com.google.common.primitives.Longs; import io.druid.extendedset.intset.ConciseSet; +import static org.qortal.controller.OnlineAccountsManager.MAX_NONCE_COUNT; + public class BlockTransformer extends Transformer { private static final int VERSION_LENGTH = INT_LENGTH; @@ -416,16 +420,101 @@ public class BlockTransformer extends Transformer { return encodedSignatures; } - public static List decodeTimestampSignatures(byte[] encodedSignatures) { - List signatures = new ArrayList<>(); + public static byte[] encodeOnlineAccountSignatures(Map indexedOnlineAccounts, + List accountIndexes, + int onlineAccountsCount, + long timestamp) { + byte[] onlineAccountsSignatures; - for (int i = 0; i < encodedSignatures.length; i += Transformer.SIGNATURE_LENGTH) { - byte[] signature = new byte[Transformer.SIGNATURE_LENGTH]; - System.arraycopy(encodedSignatures, i, signature, 0, Transformer.SIGNATURE_LENGTH); - signatures.add(signature); + if (timestamp >= BlockChain.getInstance().getOnlineAccountsMemoryPoWTimestamp()) { + // Online accounts must include at least one nonce and a reduced block signature from this time onwards + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + for (int i = 0; i < onlineAccountsCount; ++i) { + Integer accountIndex = accountIndexes.get(i); + OnlineAccountData onlineAccountData = indexedOnlineAccounts.get(accountIndex); + + List nonces = onlineAccountData.getNonces(); + byte[] reducedBlockSignature = onlineAccountData.getReducedBlockSignature(); + if (nonces == null || nonces.isEmpty() || nonces.size() > MAX_NONCE_COUNT || reducedBlockSignature == null) { + // Missing or invalid data, so exclude this online account + continue; + } + + try { + outputStream.write(onlineAccountData.getSignature()); + + outputStream.write(reducedBlockSignature); + + outputStream.write(Ints.toByteArray(nonces.size())); + + for (int n = 0; n < nonces.size(); ++n) { + Integer nonce = nonces.get(n); + outputStream.write(Ints.toByteArray(nonce)); + } + + } catch (IOException e) { + // Couldn't serialize this online account, so exclude it + continue; + } + } + onlineAccountsSignatures = outputStream.toByteArray(); + } + else { + // Exclude nonce and reference block signature from online accounts data + // Concatenate online account timestamp signatures (in correct order) + onlineAccountsSignatures = new byte[onlineAccountsCount * Transformer.SIGNATURE_LENGTH]; + for (int i = 0; i < onlineAccountsCount; ++i) { + Integer accountIndex = accountIndexes.get(i); + OnlineAccountData onlineAccountData = indexedOnlineAccounts.get(accountIndex); + System.arraycopy(onlineAccountData.getSignature(), 0, onlineAccountsSignatures, i * Transformer.SIGNATURE_LENGTH, Transformer.SIGNATURE_LENGTH); + } } - return signatures; + return onlineAccountsSignatures; + } + + public static List decodeOnlineAccountSignatures(byte[] encodedSignatures, int count, long timestamp) { + List onlineAccountSignatures = new ArrayList<>(); + + if (timestamp >= BlockChain.getInstance().getOnlineAccountsMemoryPoWTimestamp()) { + // byte array contains signatures, reduced signatures, and nonces + ByteBuffer byteBuffer = ByteBuffer.wrap(encodedSignatures); + + for (int i = 0; i < count; ++i) { + byte[] signature = new byte[Transformer.SIGNATURE_LENGTH]; + byteBuffer.get(signature); + + byte[] reducedBlockSignature = new byte[Transformer.REDUCED_SIGNATURE_LENGTH]; + byteBuffer.get(reducedBlockSignature); + + int nonceCount = byteBuffer.getInt(); + + List nonces = new ArrayList<>(); + for (int n = 0; n < nonceCount; ++n) { // TODO: check against NONCE_COUNT in block validation + Integer nonce = byteBuffer.getInt(); + nonces.add(nonce); + } + + // Create an OnlineAccountData wrapper object containing the signature, nonce(s), and reduced block signature + OnlineAccountData onlineAccountDataWrapper = new OnlineAccountData(0, signature, null, nonces, reducedBlockSignature); + onlineAccountSignatures.add(onlineAccountDataWrapper); + } + + } + else { + // byte array contains signatures only + for (int i = 0; i < encodedSignatures.length; i += Transformer.SIGNATURE_LENGTH) { + byte[] signature = new byte[Transformer.SIGNATURE_LENGTH]; + System.arraycopy(encodedSignatures, i, signature, 0, Transformer.SIGNATURE_LENGTH); + + // Create an OnlineAccountData wrapper object containing only the signature + OnlineAccountData onlineAccountDataWrapper = new OnlineAccountData(0, signature, null); + onlineAccountSignatures.add(onlineAccountDataWrapper); + } + } + + return onlineAccountSignatures; } } diff --git a/src/test/java/org/qortal/test/network/OnlineAccountsTests.java b/src/test/java/org/qortal/test/network/OnlineAccountsTests.java index e4ffc4b6..478709af 100644 --- a/src/test/java/org/qortal/test/network/OnlineAccountsTests.java +++ b/src/test/java/org/qortal/test/network/OnlineAccountsTests.java @@ -178,4 +178,65 @@ public class OnlineAccountsTests extends Common { assertTrue(onlineAccountSignatures.size() >= 1 && onlineAccountSignatures.size() <= 3); } } + + @Test + public void testBeforeMemoryPoW() throws IllegalAccessException, DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + + // Set feature trigger timestamp to MAX long so that it is inactive + FieldUtils.writeField(BlockChain.getInstance(), "onlineAccountsMemoryPoWTimestamp", Long.MAX_VALUE, true); + + // Mint some blocks + for (int i = 0; i < 10; i++) { + BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share")); + } + } + } + + @Test + public void testMemoryPoW() throws IllegalAccessException, DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + + // Set feature trigger timestamp to 0 so that it is active + FieldUtils.writeField(BlockChain.getInstance(), "onlineAccountsMemoryPoWTimestamp", 0L, true); + + // Set difficulty to 5, to speed up test + FieldUtils.writeField(OnlineAccountsManager.getInstance(), "POW_DIFFICULTY", 5, true); + + // Mint some blocks + for (int i = 0; i < 10; i++) { + BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share")); + } + } + } + + @Test + public void testTransitionToMemoryPoW() throws IllegalAccessException, DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + + // Set feature trigger timestamp to now + 5 mins + long featureTriggerTimestamp = NTP.getTime() + (5 * 60 * 1000L); + FieldUtils.writeField(BlockChain.getInstance(), "onlineAccountsMemoryPoWTimestamp", featureTriggerTimestamp, true); + + // Set difficulty to 5, to speed up test + FieldUtils.writeField(OnlineAccountsManager.getInstance(), "POW_DIFFICULTY", 5, true); + + // Mint a block + Block block = BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share")); + assertEquals(1, block.getBlockData().getOnlineAccountsCount()); + + // Ensure online accounts signatures are in legacy format (no nonce or reduced block signature) + assertEquals(64, block.getBlockData().getOnlineAccountsSignatures().length); + + // Mint some blocks (at least 5 minutes' worth, to allow mempow to kick in) + for (int i = 0; i < 10; i++) { + block = BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share")); + assertEquals(1, block.getBlockData().getOnlineAccountsCount()); + } + + // Ensure online accounts signatures are in new format (with 1 nonce and a reduced block signature) + assertEquals(80, block.getBlockData().getOnlineAccountsSignatures().length); + } + } + }