Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.qortal</groupId>
<artifactId>qortal</artifactId>
<version>6.1.5</version> <!-- Version must be <X.Y.Z> -->
<version>6.1.6</version> <!-- Version must be <X.Y.Z> -->
<packaging>jar</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
Expand Down
42 changes: 34 additions & 8 deletions src/main/java/org/qortal/asset/Order.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import org.apache.logging.log4j.Logger;
import org.qortal.account.Account;
import org.qortal.account.PublicKeyAccount;
import org.qortal.block.BlockChain;
import org.qortal.data.asset.AssetData;
import org.qortal.data.asset.OrderData;
import org.qortal.data.asset.TradeData;
Expand Down Expand Up @@ -149,12 +150,34 @@ private void calcPricePair() throws DataException {
}

/** Returns amount of have-asset to remove from order's creator's balance on placing this order. */
private long calcHaveAssetCommittment() {
private long calcHaveAssetCommittment(boolean boundsCheckActive) throws DataException {
// Simple case: amount is in have asset
if (!this.isAmountInWantAsset)
return this.orderData.getAmount();

return Amounts.roundUpScaledMultiply(this.orderAmount, this.orderPrice);
return narrowToLong(Amounts.roundUpScaled(this.orderAmount, this.orderPrice), boundsCheckActive);
}

/**
* Narrows a non-negative scaled amount to a signed long.
* <p>
* Once the asset-order bounds fix is active (see {@link org.qortal.transaction.CreateAssetOrderTransaction#isValid()}),
* a value that does not fit in a positive signed long is a bug: validation should already have rejected the order.
* We fail closed by throwing rather than silently wrapping into a negative, which on a credit path would mint value.
* Before activation we preserve the historic wrapping behaviour so existing blocks replay identically.
*/
private static long narrowToLong(BigInteger value, boolean boundsCheckActive) throws DataException {
if (boundsCheckActive && value.bitLength() > 63)
throw new DataException("asset order amount overflows signed long");

return value.longValue();
}

private boolean isBoundsCheckActive(boolean processing) throws DataException {
// While processing, the block being applied is the next block (tip + 1).
// While orphaning, the block being reverted is still the current tip.
long blockHeight = this.repository.getBlockRepository().getBlockchainHeight() + (processing ? 1 : 0);
return blockHeight >= BlockChain.getInstance().getAssetOrderBoundsHeight();
}

private long calcHaveAssetRefund(long amount) {
Expand Down Expand Up @@ -254,9 +277,11 @@ private void logOrder(String orderPrefix, boolean isOurOrder, OrderData orderDat
}

public void process() throws DataException {
boolean boundsCheckActive = isBoundsCheckActive(true);

// Subtract have-asset from creator
Account creator = new PublicKeyAccount(this.repository, this.orderData.getCreatorPublicKey());
creator.modifyAssetBalance(haveAssetId, - this.calcHaveAssetCommittment());
creator.modifyAssetBalance(haveAssetId, - this.calcHaveAssetCommittment(boundsCheckActive));

// Save this order into repository so it's available for matching, possibly by itself
this.repository.getAssetRepository().save(this.orderData);
Expand All @@ -271,10 +296,10 @@ public void process() throws DataException {
if (orders.isEmpty())
return;

matchOrders(orders);
matchOrders(orders, boundsCheckActive);
}

private void matchOrders(List<OrderData> orders) throws DataException {
private void matchOrders(List<OrderData> orders, boolean boundsCheckActive) throws DataException {
AssetData haveAssetData = getHaveAsset();
AssetData wantAssetData = getWantAsset();

Expand Down Expand Up @@ -352,7 +377,7 @@ private void matchOrders(List<OrderData> orders) throws DataException {
// Trade can go ahead!

// Calculate the total cost to us, in return-asset, based on their price
long returnAmountTraded = Amounts.roundDownScaledMultiply(matchedAmount, theirOrderData.getPrice());
long returnAmountTraded = narrowToLong(Amounts.roundDownScaled(BigInteger.valueOf(matchedAmount), BigInteger.valueOf(theirOrderData.getPrice())), boundsCheckActive);
LOGGER.trace(() -> String.format("returnAmountTraded: %s %s", prettyAmount(returnAmountTraded), returnAssetData.getName()));

// Safety check
Expand All @@ -362,7 +387,7 @@ private void matchOrders(List<OrderData> orders) throws DataException {
long tradedHaveAmount = this.isAmountInWantAsset ? returnAmountTraded : matchedAmount;

// We also need to know how much have-asset to refund based on price improvement (only one direction applies)
long haveAssetRefund = this.isAmountInWantAsset ? Amounts.roundDownScaledMultiply(matchedAmount, Math.abs(ourPrice - theirPrice)) : 0;
long haveAssetRefund = this.isAmountInWantAsset ? narrowToLong(Amounts.roundDownScaled(BigInteger.valueOf(matchedAmount), BigInteger.valueOf(Math.abs(ourPrice - theirPrice))), boundsCheckActive) : 0;

LOGGER.trace(() -> String.format("We traded %s %s (have-asset) for %s %s (want-asset), saving %s %s (have-asset)",
prettyAmount(tradedHaveAmount), haveAssetData.getName(),
Expand Down Expand Up @@ -417,8 +442,9 @@ public void orphan() throws DataException {
this.repository.getAssetRepository().delete(this.orderData.getOrderId());

// Return asset to creator
boolean boundsCheckActive = isBoundsCheckActive(false);
Account creator = new PublicKeyAccount(this.repository, this.orderData.getCreatorPublicKey());
creator.modifyAssetBalance(haveAssetId, this.calcHaveAssetCommittment());
creator.modifyAssetBalance(haveAssetId, this.calcHaveAssetCommittment(boundsCheckActive));
}

// This is called by CancelOrderTransaction so that an Order can no longer trade
Expand Down
95 changes: 62 additions & 33 deletions src/main/java/org/qortal/block/Block.java
Original file line number Diff line number Diff line change
Expand Up @@ -419,8 +419,8 @@ public static Block mint(Repository repository, BlockData parentBlockData, Priva
else if (isOnlineAccountsBlock(height)) {
// Standard online accounts block - add online accounts in regular way

// Fetch our list of online accounts, removing any that are missing a nonce
List<OnlineAccountData> onlineAccounts = OnlineAccountsManager.getInstance().getOnlineAccounts(onlineAccountsTimestamp);
// Fetch accounts with signatures valid for this block height, then remove any missing a nonce.
List<OnlineAccountData> onlineAccounts = OnlineAccountsManager.getInstance().getOnlineAccounts(onlineAccountsTimestamp, height);
onlineAccounts.removeIf(a -> a.getNonce() == null || a.getNonce() < 0);

// After feature trigger, remove any online accounts that are level 0
Expand Down Expand Up @@ -457,7 +457,10 @@ else if (isOnlineAccountsBlock(height)) {
if (Settings.getInstance().isSingleNodeTestnet()) {
Integer nonce = new Random().nextInt(500000);
byte[] timestampBytes = Longs.toByteArray(onlineAccountsTimestamp);
byte[] signature = Qortal25519Extras.signForAggregation(minter.getPrivateKey(), timestampBytes);
// Even single-node fallback blocks must use the signature scheme active at this height.
byte[] signature = OnlineAccountsManager.isSignatureV2Active(height)
? Qortal25519Extras.sign(minter.getPrivateKey(), timestampBytes)
: Qortal25519Extras.signForAggregation(minter.getPrivateKey(), timestampBytes);
byte[] publicKey = minter.getPublicKey();
OnlineAccountData me = new OnlineAccountData(
NTP.getTime(),
Expand Down Expand Up @@ -495,25 +498,30 @@ else if (isOnlineAccountsBlock(height)) {
encodedOnlineAccounts = BlockTransformer.encodeOnlineAccounts(onlineAccountsSet);
onlineAccountsCount = onlineAccountsSet.size();

// Collate all signatures
Collection<byte[]> signaturesToAggregate = indexedOnlineAccounts.values()
.stream()
.map(OnlineAccountData::getSignature)
.collect(Collectors.toList());
// After the signature V2 height we store each account's signature individually
// (secure per-account Ed25519), otherwise the legacy forgeable aggregate single signature.
boolean signatureV2 = OnlineAccountsManager.isSignatureV2Active(height);

// Build ordered lists of signatures and nonces, in account-index order, so that block
// validation can pair each signature/nonce with the correct reward-share public key.
List<byte[]> orderedSignatures = new ArrayList<>();
List<Integer> nonces = new ArrayList<>();
for (int i = 0; i < onlineAccountsCount; ++i) {
Integer accountIndex = accountIndexes.get(i);
OnlineAccountData onlineAccountData = indexedOnlineAccounts.get(accountIndex);
orderedSignatures.add(onlineAccountData.getSignature());
nonces.add(onlineAccountData.getNonce());
}

// Aggregated, single signature
onlineAccountsSignatures = Qortal25519Extras.aggregateSignatures(signaturesToAggregate);
if (signatureV2)
// Per-account standard Ed25519 signatures, stored individually
onlineAccountsSignatures = BlockTransformer.encodeTimestampSignatures(orderedSignatures);
else
// Legacy aggregated, single signature
onlineAccountsSignatures = Qortal25519Extras.aggregateSignatures(orderedSignatures);

// Add nonces to the end of the online accounts signatures
try {
// Create ordered list of nonce values
List<Integer> 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);

Expand Down Expand Up @@ -1218,22 +1226,28 @@ else if (this.blockData.getHeight() >= BlockChain.getInstance().getIgnoreLevelFo
if (this.blockData.getOnlineAccountsSignatures() == null || this.blockData.getOnlineAccountsSignatures().length == 0)
return ValidationResult.ONLINE_ACCOUNT_SIGNATURES_MISSING;

final int signaturesLength = Transformer.SIGNATURE_LENGTH;
// Check signatures
long onlineTimestamp = this.blockData.getOnlineAccountsTimestamp();
byte[] onlineTimestampBytes = Longs.toByteArray(onlineTimestamp);

// After the signature V2 height, each online account carries its own standard Ed25519
// signature; before it, a single legacy aggregate signature covers the whole set.
boolean signatureV2 = OnlineAccountsManager.isSignatureV2Active(this.blockData.getHeight());

final int signaturesLength = signatureV2
? onlineRewardShares.size() * Transformer.SIGNATURE_LENGTH
: Transformer.SIGNATURE_LENGTH;
final int noncesLength = onlineRewardShares.size() * Transformer.INT_LENGTH;

// We expect nonces to be appended to the online accounts signatures
if (this.blockData.getOnlineAccountsSignatures().length != signaturesLength + noncesLength)
return ValidationResult.ONLINE_ACCOUNT_SIGNATURES_MALFORMED;

// Check signatures
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
byte[] extractedSignatures = BlockTransformer.extract(encodedOnlineAccountSignatures, 0, signaturesLength);
byte[] extractedNonces = BlockTransformer.extract(encodedOnlineAccountSignatures, signaturesLength, onlineRewardShares.size() * Transformer.INT_LENGTH);
byte[] extractedNonces = BlockTransformer.extract(encodedOnlineAccountSignatures, signaturesLength, noncesLength);
encodedOnlineAccountSignatures = extractedSignatures;

List<Integer> nonces = BlockTransformer.decodeOnlineAccountNonces(extractedNonces);
Expand Down Expand Up @@ -1263,18 +1277,33 @@ else if (this.blockData.getHeight() >= BlockChain.getInstance().getIgnoreLevelFo
// Extract online accounts' timestamp signatures from block data. Only one signature if aggregated.
List<byte[]> onlineAccountsSignatures = BlockTransformer.decodeTimestampSignatures(encodedOnlineAccountSignatures);

// Aggregate all public keys
Collection<byte[]> publicKeys = onlineRewardShares.stream()
.map(RewardShareData::getRewardSharePublicKey)
.collect(Collectors.toList());
if (signatureV2) {
// Secure scheme: verify each account's standard Ed25519 signature against its own public key.
// Signatures are stored in the same account-index order as onlineRewardShares.
if (onlineAccountsSignatures.size() != onlineRewardShares.size())
return ValidationResult.ONLINE_ACCOUNT_SIGNATURES_MALFORMED;

for (int i = 0; i < onlineRewardShares.size(); ++i) {
byte[] publicKey = onlineRewardShares.get(i).getRewardSharePublicKey();
byte[] signature = onlineAccountsSignatures.get(i);

byte[] aggregatePublicKey = Qortal25519Extras.aggregatePublicKeys(publicKeys);
if (!OnlineAccountsManager.getInstance().verifyOrCacheV2OnlineAccountSignature(publicKey, signature, onlineTimestamp))
return ValidationResult.ONLINE_ACCOUNT_SIGNATURE_INCORRECT;
}
} else {
// Legacy scheme: aggregate all public keys and do one-step aggregate verification.
Collection<byte[]> publicKeys = onlineRewardShares.stream()
.map(RewardShareData::getRewardSharePublicKey)
.collect(Collectors.toList());

byte[] aggregateSignature = onlineAccountsSignatures.get(0);
byte[] aggregatePublicKey = Qortal25519Extras.aggregatePublicKeys(publicKeys);

// One-step verification of aggregate signature using aggregate public key
if (!Qortal25519Extras.verifyAggregated(aggregatePublicKey, aggregateSignature, onlineTimestampBytes))
return ValidationResult.ONLINE_ACCOUNT_SIGNATURE_INCORRECT;
byte[] aggregateSignature = onlineAccountsSignatures.get(0);

// One-step verification of aggregate signature using aggregate public key
if (!Qortal25519Extras.verifyAggregated(aggregatePublicKey, aggregateSignature, onlineTimestampBytes))
return ValidationResult.ONLINE_ACCOUNT_SIGNATURE_INCORRECT;
}

// All online accounts valid, so save our list of online accounts for potential later use
this.cachedOnlineRewardShares = onlineRewardShares;
Expand Down
18 changes: 15 additions & 3 deletions src/main/java/org/qortal/block/BlockChain.java
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,9 @@ public enum FeatureTrigger {
adminQueryFixHeight,
multipleNamesPerAccountHeight,
mintedBlocksAdjustmentRemovalHeight,
atValidateHeight
atValidateHeight,
onlineAccountsSignatureV2Height,
assetOrderBoundsHeight
}

// V5.5 Default List of Historic Triggers
Expand Down Expand Up @@ -354,8 +356,10 @@ public static void fileInstance(String path, String filename) {
}

// Load in the default feature triggers
defaultFeatureTriggerHeight.put(FeatureTrigger.multipleNamesPerAccountHeight, 2206300L);
defaultFeatureTriggerHeight.put(FeatureTrigger.mintedBlocksAdjustmentRemovalHeight, 2206300L);
defaultFeatureTriggerHeight.put(FeatureTrigger.multipleNamesPerAccountHeight, 2206300L);
defaultFeatureTriggerHeight.put(FeatureTrigger.mintedBlocksAdjustmentRemovalHeight, 2206300L);
defaultFeatureTriggerHeight.put(FeatureTrigger.onlineAccountsSignatureV2Height, 9999999999999L);
defaultFeatureTriggerHeight.put(FeatureTrigger.assetOrderBoundsHeight, 9999999999999L);

try {
// Attempt to unmarshal JSON stream to BlockChain config
Expand Down Expand Up @@ -434,6 +438,10 @@ public long getOnlineAccountsModulusV3Timestamp() {
return this.onlineAccountsModulusV3Timestamp;
}

public long getOnlineAccountsSignatureV2Height() {
return this.featureTriggers.get(FeatureTrigger.onlineAccountsSignatureV2Height.name()).longValue();
}

/* Block reward batching */
public long getBlockRewardBatchStartHeight() {
return this.blockRewardBatchStartHeight;
Expand Down Expand Up @@ -711,6 +719,10 @@ public int getAtValidateHeight() {
return this.featureTriggers.get(FeatureTrigger.atValidateHeight.name()).intValue();
}

public long getAssetOrderBoundsHeight() {
return this.featureTriggers.get(FeatureTrigger.assetOrderBoundsHeight.name()).longValue();
}

// More complex getters for aspects that change by height or timestamp

public long getRewardAtHeight(int ourHeight) {
Expand Down
Loading
Loading