Repository: hashgraph/hedera-services
Issue: Exported balances CSV using SingleAccountBalances type in 0.12.0-rc.1 tag

**Summary of the defect**
In `v0.11.0` token balances in the account balances CSV are Base64-encoded serializations of [the `TokenBalances` type](https://github.com/hashgraph/hedera-services/blob/06381945ab3960f7efcc9228a12bc71dad153e02/hedera-node/src/main/java/com/hedera/services/state/exports/SignedStateBalancesExporter.java#L239):
```
	static String b64Encode(TokenBalances tokenBalances) {
		return encoder.encodeToString(tokenBalances.toByteArray());
	}
```

In `master` the Base64-encoding is of the `SingleAccountBalances` [type](https://github.com/hashgraph/hedera-services/blob/master/hedera-node/src/main/java/com/hedera/services/state/exports/SignedStateBalancesExporter.java#L364):
```
	static String b64Encode(SingleAccountBalances accountBalances) {
		return encoder.encodeToString(accountBalances.toByteArray());
	}
```

Changed files:
hedera-node/src/test/java/com/hedera/services/state/exports/SignedStateBalancesExporterTest.java
hedera-node/src/main/java/com/hedera/services/ServicesMain.java
test-clients/src/main/java/com/hedera/services/bdd/suites/token/TokenMiscOps.java
hedera-node/src/main/java/com/hedera/services/state/exports/BalancesExporter.java
hedera-node/src/main/java/com/hedera/services/state/exports/SignedStateBalancesExporter.java
hedera-node/src/test/java/com/hedera/services/sigs/factories/SigFactoryCreatorTest.java
hedera-node/src/test/java/com/hedera/services/ServicesMainTest.java

diff --git a/hedera-node/src/main/java/com/hedera/services/ServicesMain.java b/hedera-node/src/main/java/com/hedera/services/ServicesMain.java
index 4f00558ef4..12acda2796 100644
--- a/hedera-node/src/main/java/com/hedera/services/ServicesMain.java
+++ b/hedera-node/src/main/java/com/hedera/services/ServicesMain.java
@@ -126,8 +126,7 @@ public class ServicesMain implements SwirldMain {
 		}
 		if (ctx.globalDynamicProperties().shouldExportBalances() && ctx.balancesExporter().isTimeToExport(when)) {
 			try {
-				ctx.balancesExporter().toCsvFile((ServicesState) signedState, when);
-				ctx.balancesExporter().toProtoFile((ServicesState) signedState, when);
+				ctx.balancesExporter().exportBalancesFrom((ServicesState) signedState, when);
 			} catch (IllegalStateException ise) {
 				log.error("HederaNode#{} has invalid total balance in signed state, exiting!", ctx.id(), ise);
 				systemExits.fail(1);
diff --git a/hedera-node/src/main/java/com/hedera/services/state/exports/BalancesExporter.java b/hedera-node/src/main/java/com/hedera/services/state/exports/BalancesExporter.java
index e80d73aba9..cbcde98fa9 100644
--- a/hedera-node/src/main/java/com/hedera/services/state/exports/BalancesExporter.java
+++ b/hedera-node/src/main/java/com/hedera/services/state/exports/BalancesExporter.java
@@ -26,6 +26,5 @@ import java.time.Instant;
 
 public interface BalancesExporter {
 	boolean isTimeToExport(Instant now);
-	void toCsvFile(ServicesState signedState, Instant when);
-	void toProtoFile(ServicesState signedState, Instant when);
+	void exportBalancesFrom(ServicesState signedState, Instant when);
 }
diff --git a/hedera-node/src/main/java/com/hedera/services/state/exports/SignedStateBalancesExporter.java b/hedera-node/src/main/java/com/hedera/services/state/exports/SignedStateBalancesExporter.java
index f162b370df..e50a3e7329 100644
--- a/hedera-node/src/main/java/com/hedera/services/state/exports/SignedStateBalancesExporter.java
+++ b/hedera-node/src/main/java/com/hedera/services/state/exports/SignedStateBalancesExporter.java
@@ -9,9 +9,9 @@ package com.hedera.services.state.exports;
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
- * 
+ *
  *      http://www.apache.org/licenses/LICENSE-2.0
- * 
+ *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -34,15 +34,16 @@ import com.hederahashgraph.api.proto.java.AccountID;
 import com.hedera.services.stream.proto.AllAccountBalances;
 import com.hedera.services.stream.proto.SingleAccountBalances;
 import com.hedera.services.stream.proto.TokenUnitBalance;
+import com.hederahashgraph.api.proto.java.TokenBalance;
+import com.hederahashgraph.api.proto.java.TokenBalances;
 import com.hederahashgraph.api.proto.java.TokenID;
-import com.swirlds.common.Units;
 import com.swirlds.fcmap.FCMap;
+import org.apache.commons.lang3.time.StopWatch;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
 import java.io.BufferedWriter;
 import java.io.File;
-import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.Writer;
@@ -54,8 +55,8 @@ import java.util.ArrayList;
 import java.util.Base64;
 import java.util.Comparator;
 import java.util.List;
+import java.util.concurrent.TimeUnit;
 import java.util.function.UnaryOperator;
-import java.util.Optional;
 
 import static com.hedera.services.state.merkle.MerkleEntityAssociation.fromAccountTokenRel;
 import static com.hedera.services.state.merkle.MerkleEntityId.fromTokenId;
@@ -65,34 +66,37 @@ import static com.hedera.services.ledger.HederaLedger.ACCOUNT_ID_COMPARATOR;
 public class SignedStateBalancesExporter implements BalancesExporter {
 	static Logger log = LogManager.getLogger(SignedStateBalancesExporter.class);
 
-	static final String LINE_SEPARATOR = System.getProperty("line.separator");
-	static final String UNKNOWN_EXPORT_DIR = "";
+	private static final String LINE_SEPARATOR = System.getProperty("line.separator");
+	private static final String UNKNOWN_EXPORT_DIR = "";
+	private static final String BAD_EXPORT_ATTEMPT_ERROR_MSG_TPL = "Could not export to '%s'!";
+	private static final String BAD_SIGNING_ATTEMPT_ERROR_MSG_TPL = "Could not sign balance file '%s'!";
 	static final String BAD_EXPORT_DIR_ERROR_MSG_TPL = "Cannot ensure existence of export dir '%s'!";
 	static final String LOW_NODE_BALANCE_WARN_MSG_TPL = "Node '%s' has unacceptably low balance %d!";
-	static final String BAD_EXPORT_ATTEMPT_ERROR_MSG_TPL = "Could not export to '%s'!";
-	static final String BAD_SIGNING_ATTEMPT_ERROR_MSG_TPL = "Could not sign balance file '%s'!";
 	static final String GOOD_SIGNING_ATTEMPT_DEBUG_MSG_TPL = "Created balance signature file '%s'.";
 	static final String CURRENT_VERSION = "version:2";
 
-	static final String PROTOBUF_FILE_EXTENSION = ".pb";
-	static final String CSV_FILE_EXTENSION = ".csv";
+	private static final String PROTO_FILE_EXTENSION = ".pb";
+	private static final String CSV_FILE_EXTENSION = ".csv";
 
 	static final Instant NEVER = null;
-	static final Base64.Encoder encoder = Base64.getEncoder();
+	private static final Base64.Encoder encoder = Base64.getEncoder();
 
 	final long expectedFloat;
-	final UnaryOperator<byte[]> signer;
-	final GlobalDynamicProperties dynamicProperties;
+	private final UnaryOperator<byte[]> signer;
+	private final GlobalDynamicProperties dynamicProperties;
 
+	/* Used to toggle output for testing. */
+	boolean exportCsv = true, exportProto = true;
 	SigFileWriter sigFileWriter = new StandardSigFileWriter();
 	FileHashReader hashReader = new Sha384HashReader();
 	DirectoryAssurance directories = loc -> Files.createDirectories(Paths.get(loc));
 
-	String lastUsedExportDir = UNKNOWN_EXPORT_DIR;
-	Instant periodEnd = NEVER;
+	private String lastUsedExportDir = UNKNOWN_EXPORT_DIR;
+	private BalancesSummary summary;
 
+	Instant periodEnd = NEVER;
 
-	public static final Comparator<SingleAccountBalances> SINGLE_ACCOUNT_BALANCES_COMPARATOR =
+	static final Comparator<SingleAccountBalances> SINGLE_ACCOUNT_BALANCES_COMPARATOR =
 			Comparator.comparing(SingleAccountBalances::getAccountID, ACCOUNT_ID_COMPARATOR);
 
 	public SignedStateBalancesExporter(
@@ -119,57 +123,53 @@ public class SignedStateBalancesExporter implements BalancesExporter {
 	}
 
 	@Override
-	public void toCsvFile(ServicesState signedState, Instant exportTimeStamp) {
+	public void exportBalancesFrom(ServicesState signedState, Instant when) {
 		if (!ensureExportDir(signedState.getNodeAccountId())) {
 			return;
 		}
-		long startTime = System.nanoTime();
-		var summary = summarized(signedState);
+		var watch = StopWatch.createStarted();
+		summary = summarized(signedState);
 		var expected = BigInteger.valueOf(expectedFloat);
 		if (!expected.equals(summary.getTotalFloat())) {
 			throw new IllegalStateException(String.format(
 					"Signed state @ %s had total balance %d not %d!",
-					exportTimeStamp,
-					summary.getTotalFloat(),
-					expectedFloat));
+					when, summary.getTotalFloat(), expectedFloat)); }
+		log.info("Took {}ms to summarize signed state balances", watch.getTime(TimeUnit.MILLISECONDS));
+
+		if (exportCsv) {
+			toCsvFile(when);
+		}
+		if (exportProto) {
+			toProtoFile(when);
 		}
+	}
+
+	private void toCsvFile(Instant exportTimeStamp) {
+		var watch = StopWatch.createStarted();
+
 		var csvLoc = lastUsedExportDir
 				+ exportTimeStamp.toString().replace(":", "_") + "_Balances" + CSV_FILE_EXTENSION;
 		boolean exportSucceeded = exportBalancesFile(summary, csvLoc, exportTimeStamp);
 		if (exportSucceeded) {
 			tryToSign(csvLoc);
 		}
-		log.info("It took total {} Millisecond to export and sign the csv account files",
-				(System.nanoTime() - startTime) * Units.NANOSECONDS_TO_MILLISECONDS);
-	}
-
-	@Override
-	public void toProtoFile(ServicesState signedState, Instant exportTimeStamp) {
-		if (!ensureExportDir(signedState.getNodeAccountId())) {
-			return;
-		}
 
-		long startTime = System.nanoTime();
-		AllAccountBalances.Builder allAccountBalancesBuilder = AllAccountBalances.newBuilder();
+		log.info(" -> Took {}ms to export and sign CSV balances file", watch.getTime(TimeUnit.MILLISECONDS));
+	}
 
-		var expected = BigInteger.valueOf(expectedFloat);
-		var total = calcTotalAndBuildProtoMessage(signedState, exportTimeStamp, allAccountBalancesBuilder);
-
-		if (!expected.equals(total)) {
-			throw new IllegalStateException(String.format(
-					"Signed state @ %s had total balance %d not %d!",
-					exportTimeStamp,total,	expectedFloat));
-		}
+	private void toProtoFile(Instant exportTimeStamp) {
+		var watch = StopWatch.createStarted();
 
+		var builder = AllAccountBalances.newBuilder();
+		summarizeAsProto(exportTimeStamp, builder);
 		var protoLoc = lastUsedExportDir
-				+ exportTimeStamp.toString().replace(":", "_") + "_Balances" + PROTOBUF_FILE_EXTENSION;
-		boolean exportSucceeded = exportBalancesProtoFile(allAccountBalancesBuilder, protoLoc);
-
+				+ exportTimeStamp.toString().replace(":", "_") + "_Balances" + PROTO_FILE_EXTENSION;
+		boolean exportSucceeded = exportBalancesProtoFile(builder, protoLoc);
 		if (exportSucceeded) {
 			tryToSign(protoLoc);
 		}
-		log.info("It took total {} Millisecond to export and sign the proto account files",
-				(System.nanoTime() - startTime) * Units.NANOSECONDS_TO_MILLISECONDS);
+
+		log.info(" -> Took {}ms to export and sign proto balances file", watch.getTime(TimeUnit.MILLISECONDS));
 	}
 
 	private void tryToSign(String csvLoc) {
@@ -192,7 +192,7 @@ public class SignedStateBalancesExporter implements BalancesExporter {
 			} else {
 				addLegacyHeader(fout, when);
 			}
-			for (SingleAccountBalances singleAccountBalances : summary.getOrderedBalances())  {
+			for (SingleAccountBalances singleAccountBalances : summary.getOrderedBalances()) {
 				fout.write(String.format(
 						"%d,%d,%d,%d",
 						singleAccountBalances.getAccountID().getShardNum(),
@@ -200,10 +200,9 @@ public class SignedStateBalancesExporter implements BalancesExporter {
 						singleAccountBalances.getAccountID().getAccountNum(),
 						singleAccountBalances.getHbarBalance()));
 				if (dynamicProperties.shouldExportTokenBalances()) {
-					if(singleAccountBalances.getTokenUnitBalancesList().size() > 0) {
+					if (singleAccountBalances.getTokenUnitBalancesList().size() > 0) {
 						fout.write("," + b64Encode(singleAccountBalances));
-					}
-					else {
+					} else {
 						fout.write(",");
 					}
 				}
@@ -216,62 +215,14 @@ public class SignedStateBalancesExporter implements BalancesExporter {
 		return true;
 	}
 
-	private BigInteger calcTotalAndBuildProtoMessage(ServicesState signedState, Instant exportTimeStamp,
-			AllAccountBalances.Builder allAccountBalancesBuilder) {
-
-		long nodeBalanceWarnThreshold = dynamicProperties.nodeBalanceWarningThreshold();
-		BigInteger totalFloat = BigInteger.valueOf(0L);
-
-		var nodeIds = MiscUtils.getNodeAccounts(signedState.addressBook());
-		var tokens = signedState.tokens();
-		var accounts = signedState.accounts();
-		var tokenAssociations = signedState.tokenAssociations();
-
-		for (MerkleEntityId id : accounts.keySet()) {
-			var account = accounts.get(id);
-			if (!account.isDeleted()) {
-				var accountId = id.toAccountId();
-				var balance = account.getBalance();
-				if (nodeIds.contains(accountId) && balance < nodeBalanceWarnThreshold) {
-					log.warn(String.format(
-							LOW_NODE_BALANCE_WARN_MSG_TPL,
-							readableId(accountId),
-							balance));
-				}
-				totalFloat = totalFloat.add(BigInteger.valueOf(account.getBalance()));
-
-				SingleAccountBalances.Builder singleAccountBuilder =SingleAccountBalances.newBuilder();
-				singleAccountBuilder.setAccountID(accountId)
-						.setHbarBalance(balance);
-
-				if (dynamicProperties.shouldExportTokenBalances()) {
-					var accountTokens = account.tokens();
-					if (accountTokens.numAssociations() > 0) {
-						for (TokenID tokenId : accountTokens.asIds()) {
-							var token = tokens.get(fromTokenId(tokenId));
-							if (token != null && !token.isDeleted()) {
-								var relationship = tokenAssociations
-										.get(fromAccountTokenRel(accountId, tokenId));
-								singleAccountBuilder.addTokenUnitBalances(tb(tokenId, relationship.getBalance()));
-							}
-						}
-					}
-				}
-				Timestamp.Builder consensusTimeStamp = Timestamp.newBuilder();
-				consensusTimeStamp.setSeconds(exportTimeStamp.getEpochSecond())
-						.setNanos(exportTimeStamp.getNano());
-				allAccountBalancesBuilder.setConsensusTimestamp(consensusTimeStamp.build());
-				allAccountBalancesBuilder.addAllAccounts(singleAccountBuilder.build());
-			}
-		}
-		return totalFloat;
+	private void summarizeAsProto(Instant exportTimeStamp, AllAccountBalances.Builder builder) {
+		builder.setConsensusTimestamp(Timestamp.newBuilder()
+				.setSeconds(exportTimeStamp.getEpochSecond())
+				.setNanos(exportTimeStamp.getNano()));
+		builder.addAllAllAccounts(summary.getOrderedBalances());
 	}
 
 	private boolean exportBalancesProtoFile(AllAccountBalances.Builder allAccountsBuilder, String protoLoc) {
-		if(log.isDebugEnabled()) {
-			log.debug("Export all accounts to protobuf file {} ", protoLoc);
-		}
-
 		try (FileOutputStream fout = new FileOutputStream(protoLoc)) {
 			allAccountsBuilder.build().writeTo(fout);
 		} catch (IOException e) {
@@ -281,17 +232,6 @@ public class SignedStateBalancesExporter implements BalancesExporter {
 		return true;
 	}
 
-	public Optional<AllAccountBalances> importBalanceProtoFile(String protoLoc) {
-		try {
-			FileInputStream fin = new FileInputStream(protoLoc);
-			AllAccountBalances allAccountBalances = AllAccountBalances.parseFrom(fin);
-			return Optional.ofNullable(allAccountBalances);
-		} catch (IOException e) {
-			log.error("Can't read protobuf message file {}", protoLoc);
-		}
-		return Optional.empty();
-	}
-
 	private void addLegacyHeader(Writer writer, Instant at) throws IOException {
 		writer.write(String.format("TimeStamp:%s%s", at, LINE_SEPARATOR));
 		writer.write("shardNum,realmNum,accountNum,balance" + LINE_SEPARATOR);
@@ -312,7 +252,7 @@ public class SignedStateBalancesExporter implements BalancesExporter {
 		var tokens = signedState.tokens();
 		var accounts = signedState.accounts();
 		var tokenAssociations = signedState.tokenAssociations();
-		for (MerkleEntityId id : accounts.keySet())	{
+		for (MerkleEntityId id : accounts.keySet()) {
 			var account = accounts.get(id);
 			if (!account.isDeleted()) {
 				var accountId = id.toAccountId();
@@ -328,7 +268,7 @@ public class SignedStateBalancesExporter implements BalancesExporter {
 				sabBuilder.setHbarBalance(balance)
 						.setAccountID(accountId);
 				if (dynamicProperties.shouldExportTokenBalances()) {
-					addTokenBalances(accountId, account, sabBuilder,  tokens, tokenAssociations);
+					addTokenBalances(accountId, account, sabBuilder, tokens, tokenAssociations);
 				}
 				accountBalances.add(sabBuilder.build());
 			}
@@ -345,14 +285,11 @@ public class SignedStateBalancesExporter implements BalancesExporter {
 			FCMap<MerkleEntityAssociation, MerkleTokenRelStatus> tokenAssociations
 	) {
 		var accountTokens = account.tokens();
-		if (accountTokens.numAssociations() > 0) {
-
-			for (TokenID tokenId : accountTokens.asIds()) {
-				var token = tokens.get(fromTokenId(tokenId));
-				if (token != null && !token.isDeleted()) {
-					var relationship = tokenAssociations.get(fromAccountTokenRel(id, tokenId));
-					sabBuilder.addTokenUnitBalances(tb(tokenId, relationship.getBalance()));
-				}
+		for (TokenID tokenId : accountTokens.asIds()) {
+			var token = tokens.get(fromTokenId(tokenId));
+			if (token != null && !token.isDeleted()) {
+				var relationship = tokenAssociations.get(fromAccountTokenRel(id, tokenId));
+				sabBuilder.addTokenUnitBalances(tb(tokenId, relationship.getBalance()));
 			}
 		}
 	}
@@ -362,7 +299,13 @@ public class SignedStateBalancesExporter implements BalancesExporter {
 	}
 
 	static String b64Encode(SingleAccountBalances accountBalances) {
-		return encoder.encodeToString(accountBalances.toByteArray());
+		var wrapper = TokenBalances.newBuilder();
+		for (TokenUnitBalance tokenUnitBalance : accountBalances.getTokenUnitBalancesList()) {
+			wrapper.addTokenBalances(TokenBalance.newBuilder()
+					.setTokenId(tokenUnitBalance.getTokenId())
+					.setBalance(tokenUnitBalance.getBalance()));
+		}
+		return encoder.encodeToString(wrapper.build().toByteArray());
 	}
 
 	private boolean ensureExportDir(AccountID node) {
diff --git a/hedera-node/src/test/java/com/hedera/services/ServicesMainTest.java b/hedera-node/src/test/java/com/hedera/services/ServicesMainTest.java
index f9a97391b4..0d38db1e42 100644
--- a/hedera-node/src/test/java/com/hedera/services/ServicesMainTest.java
+++ b/hedera-node/src/test/java/com/hedera/services/ServicesMainTest.java
@@ -522,8 +522,7 @@ public class ServicesMainTest {
 		subject.newSignedState(signedState, when, 1L);
 
 		// then:
-		verify(balancesExporter, never()).toCsvFile(any(), any());
-		verify(balancesExporter, never()).toProtoFile(any(), any());
+		verify(balancesExporter, never()).exportBalancesFrom(any(), any());
 	}
 
 	@Test
@@ -540,8 +539,7 @@ public class ServicesMainTest {
 		subject.newSignedState(signedState, when, 1L);
 
 		// then:
-		verify(balancesExporter).toCsvFile(signedState, when);
-		verify(balancesExporter).toProtoFile(signedState, when);
+		verify(balancesExporter).exportBalancesFrom(signedState, when);
 	}
 
 	@Test
@@ -569,7 +567,9 @@ public class ServicesMainTest {
 
 		given(globalDynamicProperties.shouldExportBalances()).willReturn(true);
 		given(balancesExporter.isTimeToExport(when)).willReturn(true);
-		willThrow(IllegalStateException.class).given(balancesExporter).toCsvFile(signedState, when);
+		willThrow(IllegalStateException.class)
+				.given(balancesExporter)
+				.exportBalancesFrom(signedState, when);
 
 		// when:
 		subject.newSignedState(signedState, when, 1L);
diff --git a/hedera-node/src/test/java/com/hedera/services/sigs/factories/SigFactoryCreatorTest.java b/hedera-node/src/test/java/com/hedera/services/sigs/factories/SigFactoryCreatorTest.java
index e364453f86..64df69775e 100644
--- a/hedera-node/src/test/java/com/hedera/services/sigs/factories/SigFactoryCreatorTest.java
+++ b/hedera-node/src/test/java/com/hedera/services/sigs/factories/SigFactoryCreatorTest.java
@@ -21,6 +21,7 @@ package com.hedera.services.sigs.factories;
  */
 
 import com.google.protobuf.ByteString;
+import com.google.protobuf.InvalidProtocolBufferException;
 import com.hedera.services.state.merkle.MerkleEntityId;
 import com.hedera.services.state.merkle.MerkleSchedule;
 import com.hedera.services.utils.SignedTxnAccessor;
@@ -32,6 +33,7 @@ import com.hederahashgraph.api.proto.java.ScheduleCreateTransactionBody;
 import com.hederahashgraph.api.proto.java.ScheduleID;
 import com.hederahashgraph.api.proto.java.ScheduleSignTransactionBody;
 import com.hederahashgraph.api.proto.java.SignedTransaction;
+import com.hederahashgraph.api.proto.java.TokenBalances;
 import com.hederahashgraph.api.proto.java.Transaction;
 import com.hederahashgraph.api.proto.java.TransactionBody;
 import com.hederahashgraph.api.proto.java.TransferList;
@@ -41,6 +43,8 @@ import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
+import java.util.Base64;
+
 import static com.hedera.services.sigs.factories.PlatformSigFactoryTest.pk;
 import static com.hedera.services.sigs.factories.PlatformSigFactoryTest.sig;
 import static com.hedera.services.state.merkle.MerkleEntityId.fromScheduleId;
diff --git a/hedera-node/src/test/java/com/hedera/services/state/exports/SignedStateBalancesExporterTest.java b/hedera-node/src/test/java/com/hedera/services/state/exports/SignedStateBalancesExporterTest.java
index ea44c84f15..5f9aecfe6f 100644
--- a/hedera-node/src/test/java/com/hedera/services/state/exports/SignedStateBalancesExporterTest.java
+++ b/hedera-node/src/test/java/com/hedera/services/state/exports/SignedStateBalancesExporterTest.java
@@ -20,6 +20,7 @@ package com.hedera.services.state.exports;
  * ‍
  */
 
+import com.google.protobuf.InvalidProtocolBufferException;
 import com.hedera.services.ServicesState;
 import com.hedera.services.config.MockGlobalDynamicProps;
 import com.hedera.services.context.properties.GlobalDynamicProperties;
@@ -35,6 +36,7 @@ import com.hederahashgraph.api.proto.java.AccountID;
 import com.hedera.services.stream.proto.AllAccountBalances;
 import com.hedera.services.stream.proto.SingleAccountBalances;
 import com.hedera.services.stream.proto.TokenUnitBalance;
+import com.hederahashgraph.api.proto.java.TokenBalances;
 import com.hederahashgraph.api.proto.java.TokenID;
 
 import com.swirlds.common.Address;
@@ -47,17 +49,18 @@ import org.junit.jupiter.api.Test;
 import org.mockito.ArgumentCaptor;
 
 import java.io.File;
+import java.io.FileInputStream;
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.time.Instant;
 import java.util.ArrayList;
-import java.util.Collections;
+import java.util.Base64;
+import java.util.Base64.Decoder;
 import java.util.Comparator;
 import java.util.List;
 import java.util.function.UnaryOperator;
-import java.util.regex.Pattern;
 import java.util.Optional;
 
 import static com.hedera.services.state.exports.SignedStateBalancesExporter.GOOD_SIGNING_ATTEMPT_DEBUG_MSG_TPL;
@@ -93,16 +96,12 @@ class SignedStateBalancesExporterTest {
 
 	long thisNodeBalance = 400;
 	AccountID thisNode = asAccount("0.0.3");
-
 	long anotherNodeBalance = 100;
 	AccountID anotherNode = asAccount("0.0.4");
-
 	long firstNonNodeAccountBalance = 250;
 	AccountID firstNonNode = asAccount("0.0.1001");
-
 	long secondNonNodeAccountBalance = 250;
 	AccountID secondNonNode = asAccount("0.0.1002");
-
 	AccountID deleted = asAccount("0.0.1003");
 
 	TokenID theToken = asToken("0.0.1004");
@@ -212,7 +211,8 @@ class SignedStateBalancesExporterTest {
 		subject.directories = assurance;
 
 		// when:
-		subject.toCsvFile(state, now);
+		subject.exportProto = false;
+		subject.exportBalancesFrom(state, now);
 
 		// then:
 		verify(mockLog).error(any(String.class), any(Throwable.class));
@@ -226,7 +226,8 @@ class SignedStateBalancesExporterTest {
 		given(hashReader.readHash(loc)).willThrow(IllegalStateException.class);
 
 		// when:
-		subject.toCsvFile(state, now);
+		subject.exportProto = false;
+		subject.exportBalancesFrom(state, now);
 
 		// then:
 		verify(mockLog).error(any(String.class), any(Throwable.class));
@@ -235,12 +236,81 @@ class SignedStateBalancesExporterTest {
 		new File(loc).delete();
 	}
 
+	@Test
+	public void matchesV2OutputForCsv() throws IOException {
+		// setup:
+		ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
+		// and:
+		var loc = expectedExportLoc();
+		// and:
+		accounts.clear();
+		accounts.put(
+				new MerkleEntityId(0, 0, 1),
+				MerkleAccountFactory.newAccount().get());
+		accounts.put(
+				new MerkleEntityId(0, 0, 2),
+				MerkleAccountFactory.newAccount()
+						.balance(4999999999999999920L)
+						.tokens(asToken("0.0.1001"), asToken("0.0.1002"))
+						.get());
+		accounts.put(
+				new MerkleEntityId(0, 0, 3),
+				MerkleAccountFactory.newAccount()
+						.balance(80L)
+						.tokens(asToken("0.0.1002"))
+						.get());
+		// and:
+		tokenRels.clear();
+		tokenRels.put(
+				new MerkleEntityAssociation(0, 0, 2, 0, 0, 1001),
+				new MerkleTokenRelStatus(666L, false, false));
+		tokenRels.put(
+				new MerkleEntityAssociation(0, 0, 2, 0, 0, 1002),
+				new MerkleTokenRelStatus(444L, false, false));
+		tokenRels.put(
+				new MerkleEntityAssociation(0, 0, 3, 0, 0, 1002),
+				new MerkleTokenRelStatus(333L, false, false));
+		// and:
+		tokens.clear();
+		tokens.put(new MerkleEntityId(0, 0, 1001), token);
+		tokens.put(new MerkleEntityId(0, 0, 1002), token);
+
+		given(hashReader.readHash(loc)).willReturn(fileHash);
+		given(sigFileWriter.writeSigFile(captor.capture(), any(), any())).willReturn(loc + "_sig");
+		given(properties.getLongProperty("ledger.totalTinyBarFloat"))
+				.willReturn(5000000000000000000L);
+		given(signer.apply(fileHash)).willReturn(sig);
+		// and:
+		subject = new SignedStateBalancesExporter(properties, signer, dynamicProperties);
+		subject.sigFileWriter = sigFileWriter;
+		subject.hashReader = hashReader;
+
+		// when:
+		subject.exportProto = false;
+		subject.exportBalancesFrom(state, now);
+
+		// then:
+		var lines = Files.readAllLines(Paths.get(loc));
+		assertEquals(6, lines.size());
+		assertEquals(String.format("# " + SignedStateBalancesExporter.CURRENT_VERSION, now), lines.get(0));
+		assertEquals(String.format("# TimeStamp:%s", now), lines.get(1));
+		assertEquals("shardNum,realmNum,accountNum,balance,tokenBalances", lines.get(2));
+		assertEquals("0,0,1,0,", lines.get(3));
+		assertEquals("0,0,2,4999999999999999920,CggKAxjpBxCaBQoICgMY6gcQvAM=", lines.get(4));
+		assertEquals("0,0,3,80,CggKAxjqBxDNAg==", lines.get(5));
+		// and:
+		verify(sigFileWriter).writeSigFile(loc, sig, fileHash);
+		// and:
+		verify(mockLog).debug(String.format(GOOD_SIGNING_ATTEMPT_DEBUG_MSG_TPL, loc + "_sig"));
+
+		// cleanup:
+		new File(loc).delete();
+	}
+
 	@Test
 	public void usesNewFormatWhenExportingTokenBalances() throws IOException {
 		// setup:
 		ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
-		Pattern CSV_NAME_PATTERN = Pattern.compile(
-				".*\\\\d{4}-\\\\d{2}-\\\\d{2}T\\\\d{2}_\\\\d{2}_\\\\d{2}[.]\\\\d{9}Z_Balances.csv");
 		// and:
 		var loc = expectedExportLoc();
 
@@ -248,7 +318,8 @@ class SignedStateBalancesExporterTest {
 		given(sigFileWriter.writeSigFile(captor.capture(), any(), any())).willReturn(loc + "_sig");
 
 		// when:
-		subject.toCsvFile(state, now);
+		subject.exportProto = false;
+		subject.exportBalancesFrom(state, now);
 
 		// then:
 		var lines = Files.readAllLines(Paths.get(loc));
@@ -271,8 +342,6 @@ class SignedStateBalancesExporterTest {
 		verify(sigFileWriter).writeSigFile(loc, sig, fileHash);
 		// and:
 		verify(mockLog).debug(String.format(GOOD_SIGNING_ATTEMPT_DEBUG_MSG_TPL, loc + "_sig"));
-		// and:
-//		assertTrue(CSV_NAME_PATTERN.matcher(captor.getValue()).matches());
 
 		// cleanup:
 		new File(loc).delete();
@@ -292,7 +361,8 @@ class SignedStateBalancesExporterTest {
 		subject.hashReader = hashReader;
 
 		// when:
-		subject.toCsvFile(state, now);
+		subject.exportProto = false;
+		subject.exportBalancesFrom(state, now);
 
 		// then:
 		var lines = Files.readAllLines(Paths.get(expectedExportLoc()));
@@ -318,8 +388,6 @@ class SignedStateBalancesExporterTest {
 	public void testExportingTokenBalancesProto() throws IOException {
 		// setup:
 		ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
-		Pattern PB_NAME_PATTERN = Pattern.compile(
-				".*\\\\d{4}-\\\\d{2}-\\\\d{2}T\\\\d{2}_\\\\d{2}_\\\\d{2}[.]\\\\d{9}Z_Balances.pb");
 		// and:
 		var loc = expectedExportLoc(true);
 
@@ -327,23 +395,23 @@ class SignedStateBalancesExporterTest {
 		given(sigFileWriter.writeSigFile(captor.capture(), any(), any())).willReturn(loc + "_sig");
 
 		// when:
-		subject.toProtoFile(state, now);
+		subject.exportCsv = false;
+		subject.exportBalancesFrom(state, now);
 
 		// and:
-		java.util.Optional<AllAccountBalances> fileContent = subject.importBalanceProtoFile(loc);
+		java.util.Optional<AllAccountBalances> fileContent = importBalanceProtoFile(loc);
 
-		AllAccountBalances allAccountBalances = fileContent.get() ;
+		AllAccountBalances allAccountBalances = fileContent.get();
 
 		// then:
 		List<SingleAccountBalances> accounts = allAccountBalances.getAllAccountsList();
 
 		assertEquals(accounts.size(), 4);
 
-		for(SingleAccountBalances account : accounts) {
-			if(account.getAccountID().getAccountNum() == 1001) {
+		for (SingleAccountBalances account : accounts) {
+			if (account.getAccountID().getAccountNum() == 1001) {
 				assertEquals(account.getHbarBalance(), 250);
-			}
-			else if(account.getAccountID().getAccountNum() == 1002) {
+			} else if (account.getAccountID().getAccountNum() == 1002) {
 				assertEquals(account.getHbarBalance(), 250);
 				assertEquals(account.getTokenUnitBalances(0).getTokenId().getTokenNum(), 1004);
 				assertEquals(account.getTokenUnitBalances(0).getBalance(), 100);
@@ -354,8 +422,6 @@ class SignedStateBalancesExporterTest {
 		verify(sigFileWriter).writeSigFile(loc, sig, fileHash);
 		// and:
 		verify(mockLog).debug(String.format(GOOD_SIGNING_ATTEMPT_DEBUG_MSG_TPL, loc + "_sig"));
-		// and:
-//		assertTrue(CSV_NAME_PATTERN.matcher(captor.getValue()).matches());
 
 		// cleanup:
 		new File(loc).delete();
@@ -376,7 +442,8 @@ class SignedStateBalancesExporterTest {
 		subject.directories = assurance;
 
 		// when:
-		subject.toProtoFile(state, now);
+		subject.exportCsv = false;
+		subject.exportBalancesFrom(state, now);
 
 		// then:
 		verify(mockLog).error(any(String.class), any(Throwable.class));
@@ -386,42 +453,29 @@ class SignedStateBalancesExporterTest {
 	@Test
 	public void getEmptyAllAccountBalancesFromCorruptedProtoFileImport() throws Exception {
 		// setup:
-		ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
-		Pattern BAD_PB_NAME_PATTERN = Pattern.compile(
-				".*\\\\d{4}-\\\\d{2}-\\\\d{2}T\\\\d{2}_\\\\d{2}_\\\\d{2}[.]\\\\d{9}Z_Balances.pb");
-		// and:
 		var loc = expectedExportLoc(true);
 
 		given(hashReader.readHash(loc)).willReturn(fileHash);
 
 		// when: Pretend the .csv file is a corrupted .pb file
-		subject.toCsvFile(state, now);
+		subject.exportProto = false;
+		subject.exportBalancesFrom(state, now);
 
 		// and:
-		java.util.Optional<AllAccountBalances> accounts = subject.importBalanceProtoFile(loc);
+		java.util.Optional<AllAccountBalances> accounts = importBalanceProtoFile(loc);
 
 		// then:
 		assertEquals(Optional.empty(), accounts);
 	}
 
-	@Test
-	public void throwsOnUnexpectedTotalFloatForProtoFile() throws NegativeAccountBalanceException {
-		// given:
-		anotherNodeAccount.setBalance(anotherNodeBalance + 1);
-
-		// then:
-		assertThrows(IllegalStateException.class, () -> subject.toProtoFile(state, now));
-	}
-
-
-
 	@Test
 	public void assuresExpectedProtoFileDir() throws IOException {
 		// given:
 		subject.directories = assurance;
 
 		// when:
-		subject.toProtoFile(state, now);
+		subject.exportCsv = false;
+		subject.exportBalancesFrom(state, now);
 
 		// then:
 		verify(assurance).ensureExistenceOf(expectedExportDir());
@@ -449,22 +503,19 @@ class SignedStateBalancesExporterTest {
 		willThrow(IOException.class).given(assurance).ensureExistenceOf(any());
 
 		// when:
-		subject.toProtoFile(state, now);
+		subject.exportCsv = false;
+		subject.exportBalancesFrom(state, now);
 
 		// then:
 		verify(mockLog).error(String.format(
 				SignedStateBalancesExporter.BAD_EXPORT_DIR_ERROR_MSG_TPL, expectedExportDir()));
 	}
 
-	private String expectedBalancesName(Boolean isProto ) {
+	private String expectedBalancesName(Boolean isProto) {
 		return isProto ? now.toString().replace(":", "_") + "_Balances.pb"
 				: now.toString().replace(":", "_") + "_Balances.csv";
 	}
 
-	private String expectedBalancesName() {
-		return  expectedBalancesName(false) ;
-	}
-
 	@Test
 	public void testSingleAccountBalancingSort() {
 		// given:
@@ -534,7 +585,8 @@ class SignedStateBalancesExporterTest {
 		subject.directories = assurance;
 
 		// when:
-		subject.toCsvFile(state, now);
+		subject.exportProto = false;
+		subject.exportBalancesFrom(state, now);
 
 		// then:
 		verify(assurance).ensureExistenceOf(expectedExportDir());
@@ -546,7 +598,8 @@ class SignedStateBalancesExporterTest {
 		anotherNodeAccount.setBalance(anotherNodeBalance + 1);
 
 		// then:
-		assertThrows(IllegalStateException.class, () -> subject.toCsvFile(state, now));
+		assertThrows(IllegalStateException.class,
+				() -> subject.exportBalancesFrom(state, now));
 	}
 
 	@Test
@@ -557,7 +610,8 @@ class SignedStateBalancesExporterTest {
 		willThrow(IOException.class).given(assurance).ensureExistenceOf(any());
 
 		// when:
-		subject.toCsvFile(state, now);
+		subject.exportProto = false;
+		subject.exportBalancesFrom(state, now);
 
 		// then:
 		verify(mockLog).error(String.format(
@@ -592,4 +646,15 @@ class SignedStateBalancesExporterTest {
 				.map(Path::toFile)
 				.forEach(File::delete);
 	}
+
+	static Optional<AllAccountBalances> importBalanceProtoFile(String protoLoc) {
+		try {
+			FileInputStream fin = new FileInputStream(protoLoc);
+			AllAccountBalances allAccountBalances = AllAccountBalances.parseFrom(fin);
+			return Optional.ofNullable(allAccountBalances);
+		} catch (IOException e) {
+			SignedStateBalancesExporter.log.error("Can't read protobuf message file {}", protoLoc);
+		}
+		return Optional.empty();
+	}
 }
diff --git a/test-clients/src/main/java/com/hedera/services/bdd/suites/token/TokenMiscOps.java b/test-clients/src/main/java/com/hedera/services/bdd/suites/token/TokenMiscOps.java
index 4e7f49c7bf..e71b01cc1a 100644
--- a/test-clients/src/main/java/com/hedera/services/bdd/suites/token/TokenMiscOps.java
+++ b/test-clients/src/main/java/com/hedera/services/bdd/suites/token/TokenMiscOps.java
@@ -21,6 +21,7 @@ package com.hedera.services.bdd.suites.token;
  */
 
 import com.hedera.services.bdd.spec.HapiApiSpec;
+import com.hedera.services.bdd.spec.transactions.token.TokenMovement;
 import com.hedera.services.bdd.suites.HapiApiSuite;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
@@ -38,6 +39,7 @@ import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoTransfer;
 import static com.hedera.services.bdd.spec.transactions.TxnVerbs.mintToken;
 import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenAssociate;
 import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenCreate;
+import static com.hedera.services.bdd.spec.transactions.token.TokenMovement.moving;
 import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed;
 
 public class TokenMiscOps extends HapiApiSuite {
@@ -52,12 +54,38 @@ public class TokenMiscOps extends HapiApiSuite {
 		return allOf(
 				List.of(new HapiApiSpec[] {
 //								wellKnownAccountsHaveTokens(),
-								someInfoQueries(),
+//								someLowNumAccountsHaveTokens(),
+//								someInfoQueries(),
+								theCreation(),
 						}
 				)
 		);
 	}
 
+	public HapiApiSpec someLowNumAccountsHaveTokens() {
+		long aSupply = 666L, bSupply = 777L;
+
+		return defaultHapiSpec("SomeLowNumAccountsHaveTokens")
+				.given(
+						tokenCreate("first").treasury(GENESIS).initialSupply(aSupply),
+						tokenCreate("second").treasury(GENESIS).initialSupply(bSupply)
+				).when(
+						tokenAssociate("0.0.3", "second").signedBy(GENESIS),
+						cryptoTransfer(moving(aSupply / 2, "second")
+								.between(GENESIS, "0.0.3")).signedBy(GENESIS)
+				).then(
+						getAccountInfo(GENESIS).logged(),
+						getAccountInfo("0.0.3").logged()
+				);
+	}
+
+	public HapiApiSpec theCreation() {
+		return defaultHapiSpec("TheCreation")
+				.given().when().then(
+						cryptoCreate("adam")
+				);
+	}
+
 	public HapiApiSpec someInfoQueries() {
 		return defaultHapiSpec("SomeInfoQueries")
 				.given().when().then(