Skip to content

Commit

Permalink
[nis]: update MosaicDefinitionCreationObserver to track expirations
Browse files Browse the repository at this point in the history
 problem: prior to mosaic redefinition fork, a mosaic description change
          would effectively zero all balances
solution: update MosaicDefinitionCreationObserver to track these
          balance resets as mosaic expirations
  • Loading branch information
Jaguar0625 committed Oct 9, 2024
1 parent 44ba93f commit b5a936f
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,10 @@ private AggregateBlockTransactionObserverBuilder createBuilder(final NisCache ni
builder.add(new MultisigMinCosignatoriesModificationObserver(accountStateCache));
builder.add(new TransactionHashesObserver(nisCache.getTransactionHashCache()));
builder.add(new ProvisionNamespaceObserver(nisCache.getNamespaceCache(), accountStateCache, nisCache.getExpiredMosaicCache()));
builder.add(new MosaicDefinitionCreationObserver(nisCache.getNamespaceCache()));
builder.add(new MosaicDefinitionCreationObserver(
nisCache.getNamespaceCache(),
nisCache.getExpiredMosaicCache(),
this.forkConfiguration.getMosaicRedefinitionForkHeight()));
builder.add(new MosaicSupplyChangeObserver(nisCache.getNamespaceCache(), accountStateCache));
builder.add(new MosaicTransferObserver(nisCache.getNamespaceCache()));

Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,33 @@
package org.nem.nis.secret;

import org.nem.core.model.mosaic.MosaicDefinition;
import org.nem.core.model.mosaic.*;
import org.nem.core.model.observers.*;
import org.nem.nis.cache.NamespaceCache;
import org.nem.nis.state.Mosaics;
import org.nem.core.model.primitive.BlockHeight;
import org.nem.nis.cache.*;
import org.nem.nis.state.*;

/**
* An observer that updates mosaic definition information.
*/
public class MosaicDefinitionCreationObserver implements BlockTransactionObserver {
private final NamespaceCache namespaceCache;
private final ExpiredMosaicCache expiredMosaicCache;
private final BlockHeight mosaicRedefinitionForkHeight;

/**
* Creates a new observer.
*
* @param namespaceCache The namespace cache.
* @param namespaceCache Namespace cache.
* @param expiredMosaicCache Expired mosaic cache.
* @param mosaicRedefinitionForkHeight Mosaic redefinition fork height.
*/
public MosaicDefinitionCreationObserver(final NamespaceCache namespaceCache) {
public MosaicDefinitionCreationObserver(
final NamespaceCache namespaceCache,
final ExpiredMosaicCache expiredMosaicCache,
final BlockHeight mosaicRedefinitionForkHeight) {
this.namespaceCache = namespaceCache;
this.expiredMosaicCache = expiredMosaicCache;
this.mosaicRedefinitionForkHeight = mosaicRedefinitionForkHeight;
}

@Override
Expand All @@ -31,11 +41,24 @@ public void notify(final Notification notification, final BlockNotificationConte

private void notify(final MosaicDefinitionCreationNotification notification, final BlockNotificationContext context) {
final MosaicDefinition mosaicDefinition = notification.getMosaicDefinition();
final Mosaics mosaics = this.namespaceCache.get(mosaicDefinition.getId().getNamespaceId()).getMosaics();
final MosaicId mosaicId = mosaicDefinition.getId();

final Mosaics mosaics = this.namespaceCache.get(mosaicId.getNamespaceId()).getMosaics();
if (NotificationTrigger.Execute == context.getTrigger()) {
if (context.getHeight().compareTo(this.mosaicRedefinitionForkHeight) < 0) {
final MosaicEntry mosaicEntry = mosaics.get(mosaicId);
if (null != mosaicEntry) {
this.expiredMosaicCache.addExpiration(context.getHeight(), mosaicId, mosaicEntry.getBalances(), ExpiredMosaicType.Expired);
}
}

mosaics.add(mosaicDefinition, context.getHeight());
} else {
mosaics.remove(mosaicDefinition.getId());
mosaics.remove(mosaicId);

if (context.getHeight().compareTo(this.mosaicRedefinitionForkHeight) < 0) {
this.expiredMosaicCache.removeExpiration(context.getHeight(), mosaicId);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ private static class TestContext {
private final ForkConfiguration forkConfiguration = new ForkConfiguration.Builder().build();
private final DefaultNamespaceCache namespaceCache = new DefaultNamespaceCache(forkConfiguration.getMosaicRedefinitionForkHeight())
.copy();
private final ExpiredMosaicCache expiredMosaicCache = new DefaultExpiredMosaicCache().copy();
private final AccountStateCache accountStateCache = new DefaultAccountStateCache().copy();

public TestContext() {
Expand All @@ -215,7 +216,7 @@ public void addMosaicDefinitionToCache() {
private BlockTransactionObserver createObserver() {
// note that this observer is dependent on MosaicDefinitionCreationObserver and MosaicTransferObserver
final AggregateBlockTransactionObserverBuilder builder = new AggregateBlockTransactionObserverBuilder();
builder.add(new MosaicDefinitionCreationObserver(this.namespaceCache));
builder.add(new MosaicDefinitionCreationObserver(this.namespaceCache, this.expiredMosaicCache, this.forkConfiguration.getMosaicRedefinitionForkHeight()));
builder.add(new MosaicTransferObserver(this.namespaceCache));
builder.add(new AccountInfoMosaicIdsObserver(this.namespaceCache, this.accountStateCache));
return builder.build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import org.nem.nis.BlockMarkerConstants;
import org.nem.nis.ForkConfiguration;
import org.nem.nis.cache.*;
import org.nem.nis.state.MosaicEntry;
import org.nem.nis.state.*;
import org.nem.nis.test.NisUtils;

import java.util.Arrays;
Expand Down Expand Up @@ -70,18 +70,21 @@ public void notifyExecuteCreatesUntouchedMosaicEntryIfPropertiesChangedAtAnyHeig
final MosaicDefinition mosaicDefinition = Utils.createMosaicDefinition(7, Utils.createMosaicPropertiesWithInitialSupply(0L));

// Assert: since the supply is 0 the balances are empty
Arrays.stream(HEIGHTS_BEFORE_FORK).forEach(height -> assertMosaicRedefinitionBehavior(mosaicDefinition, height, 0L, 0));
Arrays.stream(HEIGHTS_AT_AND_AFTER_FORK).forEach(height -> assertMosaicRedefinitionBehavior(mosaicDefinition, height, 0L, 0));
// [notice this should be prevented by validator unless owner owns whole supply]
Arrays.stream(HEIGHTS_BEFORE_FORK).forEach(height -> assertMosaicRedefinitionBehavior(mosaicDefinition, height, 0L, 0, 1));
Arrays.stream(HEIGHTS_AT_AND_AFTER_FORK).forEach(height -> assertMosaicRedefinitionBehavior(mosaicDefinition, height, 0L, 0, 0));
}

@Test
public void notifyExecuteCreatesUntouchedMosaicEntryIfLevyChangedAtAnyHeight() {
// Arrange:
final MosaicDefinition mosaicDefinition = Utils.createMosaicDefinition(7, Utils.createMosaicPropertiesWithInitialSupply(5L),
Utils.createMosaicLevy());

// Assert:
Arrays.stream(HEIGHTS_BEFORE_FORK).forEach(height -> assertMosaicRedefinitionBehavior(mosaicDefinition, height, 5L, 1));
Arrays.stream(HEIGHTS_AT_AND_AFTER_FORK).forEach(height -> assertMosaicRedefinitionBehavior(mosaicDefinition, height, 5L, 1));
// [notice this should be prevented by validator unless owner owns whole supply]
Arrays.stream(HEIGHTS_BEFORE_FORK).forEach(height -> assertMosaicRedefinitionBehavior(mosaicDefinition, height, 5L, 1, 1));
Arrays.stream(HEIGHTS_AT_AND_AFTER_FORK).forEach(height -> assertMosaicRedefinitionBehavior(mosaicDefinition, height, 5L, 1, 0));
}

@Test
Expand All @@ -91,7 +94,7 @@ public void notifyExecuteCreatesUntouchedMosaicEntryIfOnlyDescriptorChangedBefor
new MosaicDescriptor("This is a new description"));

// Assert:
Arrays.stream(HEIGHTS_BEFORE_FORK).forEach(height -> assertMosaicRedefinitionBehavior(mosaicDefinition, height, 5L, 1));
Arrays.stream(HEIGHTS_BEFORE_FORK).forEach(height -> assertMosaicRedefinitionBehavior(mosaicDefinition, height, 5L, 1, 1));
}

@Test
Expand All @@ -101,11 +104,11 @@ public void notifyExecuteCreatesMosaicEntryWithInheritedDataIfOnlyDescriptorChan
new MosaicDescriptor("This is a new description"));

// Assert:
Arrays.stream(HEIGHTS_AT_AND_AFTER_FORK).forEach(height -> assertMosaicRedefinitionBehavior(mosaicDefinition, height, 15L, 2));
Arrays.stream(HEIGHTS_AT_AND_AFTER_FORK).forEach(height -> assertMosaicRedefinitionBehavior(mosaicDefinition, height, 15L, 2, 0));
}

private void assertMosaicRedefinitionBehavior(final MosaicDefinition mosaicDefinition, final long height, final long expectedSupply,
final int expectedBalancesSize) {
final int expectedBalancesSize, final int expectedExpiredMosaicCacheSize) {
// Arrange: initial supply is 5
final TestContext context = new TestContext();
this.notifyMosaicDefinitionCreation(context, NotificationTrigger.Execute);
Expand All @@ -121,6 +124,8 @@ private void assertMosaicRedefinitionBehavior(final MosaicDefinition mosaicDefin

// Assert:
assertMosaicEntry(context, expectedSupply, expectedBalancesSize);

MatcherAssert.assertThat(context.expiredMosaicCache.size(), IsEqual.equalTo(expectedExpiredMosaicCacheSize));
}

private static void assertMosaicEntry(final TestContext context, final Long supply, final int numBalances) {
Expand All @@ -129,6 +134,48 @@ private static void assertMosaicEntry(final TestContext context, final Long supp
MatcherAssert.assertThat(entry.getBalances().size(), IsEqual.equalTo(numBalances));
}

@Test
public void notifyUndoRemovesExpirationsBeforeFork() {
// Arrange:
final MosaicDefinition mosaicDefinition = Utils.createMosaicDefinition(7, Utils.createMosaicPropertiesWithInitialSupply(5L),
new MosaicDescriptor("This is a new description"));

// Assert: before fork removeExpiration should be called
Arrays.stream(HEIGHTS_BEFORE_FORK).forEach(height -> assertMosaicRedefinitionUndoBehavior(mosaicDefinition, height, 0));
}

@Test
public void notifyUndoDoesNotRemoveExpirationsAfterFork() {
// Arrange:
final MosaicDefinition mosaicDefinition = Utils.createMosaicDefinition(7, Utils.createMosaicPropertiesWithInitialSupply(5L),
new MosaicDescriptor("This is a new description"));

// Assert: at and after fork removeExpiration should NOT be called
Arrays.stream(HEIGHTS_AT_AND_AFTER_FORK).forEach(height -> assertMosaicRedefinitionUndoBehavior(mosaicDefinition, height, 1));
}

private void assertMosaicRedefinitionUndoBehavior(final MosaicDefinition mosaicDefinition, final long height, final int expectedExpiredMosaicCacheSize) {
// Arrange: initial supply is 5
final TestContext context = new TestContext();
this.notifyMosaicDefinitionCreation(context, NotificationTrigger.Execute);
final Address address = Utils.generateRandomAddress();
context.increaseSupply(10L);
context.incrementBalance(address, 8L);

context.expiredMosaicCache.addExpiration(new BlockHeight(height), mosaicDefinition.getId(), new MosaicBalances(), ExpiredMosaicType.Expired);

// Sanity:
assertMosaicEntry(context, 15L, 2);
MatcherAssert.assertThat(context.expiredMosaicCache.size(), IsEqual.equalTo(1));

// Act:
this.notifyMosaicDefinitionCreation(context, mosaicDefinition, height, NotificationTrigger.Undo);

// Assert:
MatcherAssert.assertThat(context.getMosaicEntry(), IsEqual.equalTo(null));
MatcherAssert.assertThat(context.expiredMosaicCache.size(), IsEqual.equalTo(expectedExpiredMosaicCacheSize));
}

// endregion

// region other types
Expand Down Expand Up @@ -171,6 +218,7 @@ private class TestContext {
Utils.createMosaicPropertiesWithInitialSupply(5L));
final ForkConfiguration forkConfiguration = new ForkConfiguration.Builder().build();
private final NamespaceCache namespaceCache = new DefaultNamespaceCache(forkConfiguration.getMosaicRedefinitionForkHeight()).copy();
private final ExpiredMosaicCache expiredMosaicCache = new DefaultExpiredMosaicCache().copy();

public TestContext() {
this.namespaceCache.add(
Expand Down Expand Up @@ -203,7 +251,7 @@ public void incrementBalance(final Address address, final Long increase) {
}

public MosaicDefinitionCreationObserver createObserver() {
return new MosaicDefinitionCreationObserver(this.namespaceCache);
return new MosaicDefinitionCreationObserver(this.namespaceCache, this.expiredMosaicCache, this.forkConfiguration.getMosaicRedefinitionForkHeight());
}
}
}

0 comments on commit b5a936f

Please sign in to comment.