Merge pull request #5782 from chimp1984/redesign-dao-state-monitoring

Redesign dao state monitoring [4]
This commit is contained in:
Christoph Atteneder 2021-11-09 12:12:22 +01:00 committed by GitHub
commit 65c308f5ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 570 additions and 347 deletions

View File

@ -0,0 +1,65 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.apitest.method.trade;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import bisq.apitest.method.offer.AbstractOfferTest;
// @Disabled
@Slf4j
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class BsqSwapTradeTestLoop extends AbstractOfferTest {
@BeforeAll
public static void setUp() {
AbstractOfferTest.setUp();
createBsqSwapBsqPaymentAccounts();
}
@Test
@Order(1)
public void testGetBalancesBeforeTrade() {
BsqSwapTradeTest test = new BsqSwapTradeTest();
runTradeLoop(test);
}
private void runTradeLoop(BsqSwapTradeTest test) {
// TODO Fix wallet inconsistency bugs after 2nd trades.
for (int tradeCount = 1; tradeCount <= 2; tradeCount++) {
log.warn("================================ Trade # {} ================================", tradeCount);
test.testGetBalancesBeforeTrade();
test.testAliceCreateBsqSwapBuyOffer();
genBtcBlocksThenWait(1, 8000);
test.testBobTakesBsqSwapOffer();
genBtcBlocksThenWait(1, 8000);
test.testGetBalancesAfterTrade();
}
}
}

View File

@ -501,8 +501,6 @@ public class PersistenceManager<T extends PersistableEnvelope> {
if (completeHandler != null) {
UserThread.execute(completeHandler);
}
GcUtil.maybeReleaseMemory();
}
}

View File

@ -18,6 +18,7 @@
package bisq.common.util;
import bisq.common.UserThread;
import bisq.common.app.DevEnv;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
@ -47,18 +48,31 @@ public class GcUtil {
* @param trigger Threshold for free memory in MB when we invoke the garbage collector
*/
private static void autoReleaseMemory(long trigger) {
UserThread.runPeriodically(() -> maybeReleaseMemory(trigger), 60);
UserThread.runPeriodically(() -> maybeReleaseMemory(trigger), 120);
}
/**
* @param trigger Threshold for free memory in MB when we invoke the garbage collector
*/
private static void maybeReleaseMemory(long trigger) {
long totalMemory = Runtime.getRuntime().totalMemory();
if (totalMemory > trigger * 1024 * 1024) {
log.info("Invoke garbage collector. Total memory: {} {} {}", Utilities.readableFileSize(totalMemory), totalMemory, trigger * 1024 * 1024);
long ts = System.currentTimeMillis();
long preGcMemory = Runtime.getRuntime().totalMemory();
if (preGcMemory > trigger * 1024 * 1024) {
System.gc();
log.info("Total memory after gc() call: {}", Utilities.readableFileSize(Runtime.getRuntime().totalMemory()));
long postGcMemory = Runtime.getRuntime().totalMemory();
log.info("GC reduced memory by {}. Total memory before/after: {}/{}. Took {} ms.",
Utilities.readableFileSize(preGcMemory - postGcMemory),
Utilities.readableFileSize(preGcMemory),
Utilities.readableFileSize(postGcMemory),
System.currentTimeMillis() - ts);
if (DevEnv.isDevMode()) {
try {
// To see from where we got called
throw new RuntimeException("Dummy Exception for print stacktrace at maybeReleaseMemory");
} catch (Throwable t) {
t.printStackTrace();
}
}
}
}
}

View File

@ -28,6 +28,7 @@ import bisq.core.dao.governance.proofofburn.MyProofOfBurnListService;
import bisq.core.dao.governance.proposal.MyProposalListService;
import bisq.core.filter.FilterManager;
import bisq.core.trade.statistics.TradeStatisticsManager;
import bisq.core.user.Preferences;
import bisq.network.p2p.P2PService;
import bisq.network.p2p.peers.PeerManager;
@ -42,6 +43,7 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j
public class AppSetupWithP2PAndDAO extends AppSetupWithP2P {
private final DaoSetup daoSetup;
private final Preferences preferences;
@Inject
public AppSetupWithP2PAndDAO(P2PService p2PService,
@ -58,6 +60,7 @@ public class AppSetupWithP2PAndDAO extends AppSetupWithP2P {
MyProposalListService myProposalListService,
MyReputationListService myReputationListService,
MyProofOfBurnListService myProofOfBurnListService,
Preferences preferences,
Config config) {
super(p2PService,
p2PDataStorage,
@ -69,6 +72,7 @@ public class AppSetupWithP2PAndDAO extends AppSetupWithP2P {
config);
this.daoSetup = daoSetup;
this.preferences = preferences;
// TODO Should be refactored/removed. In the meantime keep in sync with CorePersistedDataHost
if (config.daoActivated) {
@ -86,5 +90,8 @@ public class AppSetupWithP2PAndDAO extends AppSetupWithP2P {
super.onBasicServicesInitialized();
daoSetup.onAllServicesInitialized(log::error, log::warn);
// For seed nodes we need to set default value to true
preferences.setUseFullModeDaoMonitor(true);
}
}

View File

@ -1,69 +0,0 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.dao;
import bisq.core.dao.monitoring.DaoStateMonitoringService;
import bisq.core.dao.state.DaoStateListener;
import bisq.core.dao.state.DaoStateService;
import bisq.core.dao.state.DaoStateSnapshotService;
import bisq.core.dao.state.model.blockchain.Block;
import javax.inject.Inject;
public class DaoEventCoordinator implements DaoSetupService, DaoStateListener {
private final DaoStateService daoStateService;
private final DaoStateSnapshotService daoStateSnapshotService;
private final DaoStateMonitoringService daoStateMonitoringService;
@Inject
public DaoEventCoordinator(DaoStateService daoStateService,
DaoStateSnapshotService daoStateSnapshotService,
DaoStateMonitoringService daoStateMonitoringService) {
this.daoStateService = daoStateService;
this.daoStateSnapshotService = daoStateSnapshotService;
this.daoStateMonitoringService = daoStateMonitoringService;
}
///////////////////////////////////////////////////////////////////////////////////////////
// DaoSetupService
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void addListeners() {
this.daoStateService.addDaoStateListener(this);
}
@Override
public void start() {
}
///////////////////////////////////////////////////////////////////////////////////////////
// DaoStateListener
///////////////////////////////////////////////////////////////////////////////////////////
// We listen onDaoStateChanged to ensure the dao state has been processed from listener clients after parsing.
// We need to listen during batch processing as well to write snapshots during that process.
@Override
public void onDaoStateChanged(Block block) {
// We need to execute first the daoStateMonitoringService
daoStateMonitoringService.createHashFromBlock(block);
daoStateSnapshotService.maybeCreateSnapshot(block);
}
}

View File

@ -102,7 +102,6 @@ public class DaoModule extends AppModule {
protected void configure() {
bind(DaoSetup.class).in(Singleton.class);
bind(DaoFacade.class).in(Singleton.class);
bind(DaoEventCoordinator.class).in(Singleton.class);
bind(DaoKillSwitch.class).in(Singleton.class);
// Node, parser

View File

@ -39,6 +39,7 @@ import bisq.core.dao.node.BsqNode;
import bisq.core.dao.node.BsqNodeProvider;
import bisq.core.dao.node.explorer.ExportJsonFilesService;
import bisq.core.dao.state.DaoStateService;
import bisq.core.dao.state.DaoStateSnapshotService;
import com.google.inject.Inject;
@ -78,16 +79,11 @@ public class DaoSetup {
DaoStateMonitoringService daoStateMonitoringService,
ProposalStateMonitoringService proposalStateMonitoringService,
BlindVoteStateMonitoringService blindVoteStateMonitoringService,
DaoEventCoordinator daoEventCoordinator) {
DaoStateSnapshotService daoStateSnapshotService) {
bsqNode = bsqNodeProvider.getBsqNode();
// We need to take care of order of execution.
// For order critical event flow we use the daoEventCoordinator to delegate the calls from anonymous listeners
// to concrete clients.
daoSetupServices.add(daoEventCoordinator);
daoSetupServices.add(daoStateService);
daoSetupServices.add(cycleService);
daoSetupServices.add(ballotListService);
@ -110,6 +106,7 @@ public class DaoSetup {
daoSetupServices.add(daoStateMonitoringService);
daoSetupServices.add(proposalStateMonitoringService);
daoSetupServices.add(blindVoteStateMonitoringService);
daoSetupServices.add(daoStateSnapshotService);
daoSetupServices.add(bsqNodeProvider.getBsqNode());
}

View File

@ -90,8 +90,7 @@ public class CycleService implements DaoStateListener, DaoSetupService {
}
public int getCycleIndex(Cycle cycle) {
Optional<Cycle> previousCycle = getCycle(cycle.getHeightOfFirstBlock() - 1, daoStateService.getCycles());
return previousCycle.map(cycle1 -> getCycleIndex(cycle1) + 1).orElse(0);
return daoStateService.getCycles().indexOf(cycle);
}
public boolean isTxInCycle(Cycle cycle, String txId) {

View File

@ -274,7 +274,7 @@ public class BlindVoteStateMonitoringService implements DaoSetupService, DaoStat
byte[] combined = ArrayUtils.addAll(prevHash, serializedBlindVotes);
byte[] hash = Hash.getSha256Ripemd160hash(combined);
BlindVoteStateHash myBlindVoteStateHash = new BlindVoteStateHash(blockHeight, hash, prevHash, blindVotes.size());
BlindVoteStateHash myBlindVoteStateHash = new BlindVoteStateHash(blockHeight, hash, blindVotes.size());
BlindVoteStateBlock blindVoteStateBlock = new BlindVoteStateBlock(myBlindVoteStateHash);
blindVoteStateBlockChain.add(blindVoteStateBlock);
blindVoteStateHashChain.add(myBlindVoteStateHash);

View File

@ -31,6 +31,7 @@ import bisq.core.dao.state.GenesisTxInfo;
import bisq.core.dao.state.model.blockchain.BaseTxOutput;
import bisq.core.dao.state.model.blockchain.Block;
import bisq.core.dao.state.model.governance.IssuanceType;
import bisq.core.user.Preferences;
import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.network.Connection;
@ -40,7 +41,6 @@ import bisq.common.UserThread;
import bisq.common.config.Config;
import bisq.common.crypto.Hash;
import bisq.common.file.FileUtil;
import bisq.common.util.GcUtil;
import bisq.common.util.Utilities;
import javax.inject.Inject;
@ -54,19 +54,20 @@ import javafx.collections.ObservableList;
import java.io.File;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import static com.google.common.base.Preconditions.checkArgument;
import javax.annotation.Nullable;
/**
* Monitors the DaoState by using a hash for the complete daoState and make it accessible to the network
@ -88,7 +89,7 @@ public class DaoStateMonitoringService implements DaoSetupService, DaoStateListe
DaoStateNetworkService.Listener<NewDaoStateHashMessage, GetDaoStateHashesRequest, DaoStateHash> {
public interface Listener {
void onChangeAfterBatchProcessing();
void onDaoStateHashesChanged();
void onCheckpointFail();
}
@ -98,7 +99,6 @@ public class DaoStateMonitoringService implements DaoSetupService, DaoStateListe
private final GenesisTxInfo genesisTxInfo;
private final Set<String> seedNodeAddresses;
@Getter
private final LinkedList<DaoStateBlock> daoStateBlockChain = new LinkedList<>();
@Getter
@ -110,6 +110,8 @@ public class DaoStateMonitoringService implements DaoSetupService, DaoStateListe
@Getter
private boolean isInConflictWithSeedNode;
@Getter
private boolean daoStateBlockChainNotConnecting;
@Getter
private final ObservableList<UtxoMismatch> utxoMismatches = FXCollections.observableArrayList();
private final List<Checkpoint> checkpoints = Arrays.asList(
@ -120,7 +122,13 @@ public class DaoStateMonitoringService implements DaoSetupService, DaoStateListe
private int numCalls;
private long accumulatedDuration;
private final Preferences preferences;
private final File storageDir;
@Nullable
private Runnable createSnapshotHandler;
// Lookup map
private Map<Integer, DaoStateBlock> daoStateBlockByHeight = new HashMap<>();
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
@ -131,11 +139,13 @@ public class DaoStateMonitoringService implements DaoSetupService, DaoStateListe
DaoStateNetworkService daoStateNetworkService,
GenesisTxInfo genesisTxInfo,
SeedNodeRepository seedNodeRepository,
Preferences preferences,
@Named(Config.STORAGE_DIR) File storageDir,
@Named(Config.IGNORE_DEV_MSG) boolean ignoreDevMsg) {
this.daoStateService = daoStateService;
this.daoStateNetworkService = daoStateNetworkService;
this.genesisTxInfo = genesisTxInfo;
this.preferences = preferences;
this.storageDir = storageDir;
this.ignoreDevMsg = ignoreDevMsg;
seedNodeAddresses = seedNodeRepository.getSeedNodeAddresses().stream()
@ -163,16 +173,19 @@ public class DaoStateMonitoringService implements DaoSetupService, DaoStateListe
// DaoStateListener
///////////////////////////////////////////////////////////////////////////////////////////
// We do not use onDaoStateChanged but let the DaoEventCoordinator call createHashFromBlock to ensure the
// correct order of execution.
@Override
public void onParseBlockChainComplete() {
parseBlockChainComplete = true;
daoStateService.getLastBlock().ifPresent(this::checkUtxos);
daoStateNetworkService.addListeners();
// We wait for processing messages until we have completed batch processing
int fromHeight = daoStateService.getChainHeight() - 10;
// We take either the height of the previous hashBlock we have or 10 blocks below the chain tip.
int nextBlockHeight = daoStateBlockChain.isEmpty() ?
genesisTxInfo.getGenesisBlockHeight() :
daoStateBlockChain.getLast().getHeight() + 1;
int past10 = daoStateService.getChainHeight() - 10;
int fromHeight = Math.min(nextBlockHeight, past10);
daoStateNetworkService.requestHashesFromAllConnectedSeedNodes(fromHeight);
if (!ignoreDevMsg) {
@ -188,16 +201,9 @@ public class DaoStateMonitoringService implements DaoSetupService, DaoStateListe
@Override
public void onDaoStateChanged(Block block) {
long genesisTotalSupply = daoStateService.getGenesisTotalSupply().value;
long compensationIssuance = daoStateService.getTotalIssuedAmount(IssuanceType.COMPENSATION);
long reimbursementIssuance = daoStateService.getTotalIssuedAmount(IssuanceType.REIMBURSEMENT);
long totalAmountOfBurntBsq = daoStateService.getTotalAmountOfBurntBsq();
// confiscated funds are still in the utxo set
long sumUtxo = daoStateService.getUnspentTxOutputMap().values().stream().mapToLong(BaseTxOutput::getValue).sum();
long sumBsq = genesisTotalSupply + compensationIssuance + reimbursementIssuance - totalAmountOfBurntBsq;
if (sumBsq != sumUtxo) {
utxoMismatches.add(new UtxoMismatch(block.getHeight(), sumUtxo, sumBsq));
// During syncing we do not call checkUtxos as its a bit slow (about 4 ms)
if (parseBlockChainComplete) {
checkUtxos(block);
}
}
@ -208,44 +214,47 @@ public class DaoStateMonitoringService implements DaoSetupService, DaoStateListe
@Override
public void onNewStateHashMessage(NewDaoStateHashMessage newStateHashMessage, Connection connection) {
if (newStateHashMessage.getStateHash().getHeight() <= daoStateService.getChainHeight()) {
processPeersDaoStateHash(newStateHashMessage.getStateHash(), connection.getPeersNodeAddressOptional(), true);
// Called when receiving NewDaoStateHashMessages from peers after a new block
DaoStateHash peersDaoStateHash = newStateHashMessage.getStateHash();
if (peersDaoStateHash.getHeight() <= daoStateService.getChainHeight()) {
putInPeersMapAndCheckForConflicts(getPeersAddress(connection.getPeersNodeAddressOptional()), peersDaoStateHash);
listeners.forEach(Listener::onDaoStateHashesChanged);
}
}
@Override
public void onPeersStateHashes(List<DaoStateHash> stateHashes, Optional<NodeAddress> peersNodeAddress) {
// Called when receiving GetDaoStateHashesResponse from seed nodes
processPeersDaoStateHashes(stateHashes, peersNodeAddress);
listeners.forEach(Listener::onDaoStateHashesChanged);
if (createSnapshotHandler != null) {
createSnapshotHandler.run();
// As we get called multiple times from hashes of diff. seed nodes we want to avoid to
// call our handler multiple times.
createSnapshotHandler = null;
}
}
@Override
public void onGetStateHashRequest(Connection connection, GetDaoStateHashesRequest getStateHashRequest) {
int fromHeight = getStateHashRequest.getHeight();
List<DaoStateHash> daoStateHashes = daoStateBlockChain.stream()
List<DaoStateHash> daoStateHashes = daoStateHashChain.stream()
.filter(e -> e.getHeight() >= fromHeight)
.map(DaoStateBlock::getMyStateHash)
.collect(Collectors.toList());
daoStateNetworkService.sendGetStateHashesResponse(connection, getStateHashRequest.getNonce(), daoStateHashes);
}
@Override
public void onPeersStateHashes(List<DaoStateHash> stateHashes, Optional<NodeAddress> peersNodeAddress) {
AtomicBoolean hasChanged = new AtomicBoolean(false);
stateHashes.forEach(daoStateHash -> {
boolean changed = processPeersDaoStateHash(daoStateHash, peersNodeAddress, false);
if (changed) {
hasChanged.set(true);
}
});
if (hasChanged.get()) {
listeners.forEach(Listener::onChangeAfterBatchProcessing);
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
public void createHashFromBlock(Block block) {
updateHashChain(block);
createDaoStateBlock(block);
if (parseBlockChainComplete) {
// We notify listeners only after batch processing to avoid performance issues at UI code
listeners.forEach(Listener::onDaoStateHashesChanged);
}
}
public void requestHashesFromGenesisBlockHeight(String peersAddress) {
@ -256,6 +265,7 @@ public class DaoStateMonitoringService implements DaoSetupService, DaoStateListe
// We could get a reset from a reorg, so we clear all and start over from the genesis block.
daoStateHashChain.clear();
daoStateBlockChain.clear();
daoStateBlockByHeight.clear();
daoStateNetworkService.reset();
if (!persistedDaoStateHashChain.isEmpty()) {
@ -263,7 +273,15 @@ public class DaoStateMonitoringService implements DaoSetupService, DaoStateListe
persistedDaoStateHashChain.size(), persistedDaoStateHashChain.getLast());
}
daoStateHashChain.addAll(persistedDaoStateHashChain);
daoStateHashChain.forEach(e -> daoStateBlockChain.add(new DaoStateBlock(e)));
daoStateHashChain.forEach(daoStateHash -> {
DaoStateBlock daoStateBlock = new DaoStateBlock(daoStateHash);
daoStateBlockChain.add(daoStateBlock);
daoStateBlockByHeight.put(daoStateHash.getHeight(), daoStateBlock);
});
}
public void setCreateSnapshotHandler(Runnable handler) {
createSnapshotHandler = handler;
}
@ -284,7 +302,7 @@ public class DaoStateMonitoringService implements DaoSetupService, DaoStateListe
// Private
///////////////////////////////////////////////////////////////////////////////////////////
private void updateHashChain(Block block) {
private Optional<DaoStateBlock> createDaoStateBlock(Block block) {
long ts = System.currentTimeMillis();
byte[] prevHash;
int height = block.getHeight();
@ -295,34 +313,45 @@ public class DaoStateMonitoringService implements DaoSetupService, DaoStateListe
} else {
log.warn("DaoStateBlockchain is empty but we received the block which was not the genesis block. " +
"We stop execution here.");
return;
daoStateBlockChainNotConnecting = true;
listeners.forEach(Listener::onDaoStateHashesChanged);
return Optional.empty();
}
} else {
checkArgument(height == daoStateBlockChain.getLast().getHeight() + 1,
"New block must be 1 block above previous block. height={}, " +
"daoStateBlockChain.getLast().getHeight()={}",
height, daoStateBlockChain.getLast().getHeight());
prevHash = daoStateBlockChain.getLast().getHash();
DaoStateBlock last = daoStateBlockChain.getLast();
int heightOfLastBlock = last.getHeight();
if (height == heightOfLastBlock + 1) {
prevHash = last.getHash();
} else {
log.warn("New block must be 1 block above previous block. height={}, " +
"daoStateBlockChain.getLast().getHeight()={}",
height, heightOfLastBlock);
daoStateBlockChainNotConnecting = true;
listeners.forEach(Listener::onDaoStateHashesChanged);
return Optional.empty();
}
}
byte[] stateAsBytes = daoStateService.getSerializedStateForHashChain();
// We include the prev. hash in our new hash so we can be sure that if one hash is matching all the past would
// match as well.
byte[] combined = ArrayUtils.addAll(prevHash, stateAsBytes);
byte[] hash = Hash.getSha256Ripemd160hash(combined);
DaoStateHash myDaoStateHash = new DaoStateHash(height, hash, prevHash);
DaoStateHash myDaoStateHash = new DaoStateHash(height, hash, true);
DaoStateBlock daoStateBlock = new DaoStateBlock(myDaoStateHash);
daoStateBlockChain.add(daoStateBlock);
daoStateBlockByHeight.put(height, daoStateBlock);
daoStateHashChain.add(myDaoStateHash);
// We only broadcast after parsing of blockchain is complete
if (parseBlockChainComplete) {
// We notify listeners only after batch processing to avoid performance issues at UI code
listeners.forEach(Listener::onChangeAfterBatchProcessing);
// We delay broadcast to give peers enough time to have received the block.
// Otherwise they would ignore our data if received block is in future to their local blockchain.
int delayInSec = 5 + new Random().nextInt(10);
if (Config.baseCurrencyNetwork().isRegtest()) {
delayInSec = 1;
}
UserThread.runAfter(() -> daoStateNetworkService.broadcastMyStateHash(myDaoStateHash), delayInSec);
}
long duration = System.currentTimeMillis() - ts;
@ -332,59 +361,93 @@ public class DaoStateMonitoringService implements DaoSetupService, DaoStateListe
duration);
accumulatedDuration += duration;
numCalls++;
return Optional.of(daoStateBlock);
}
private boolean processPeersDaoStateHash(DaoStateHash daoStateHash,
Optional<NodeAddress> peersNodeAddress,
boolean notifyListeners) {
GcUtil.maybeReleaseMemory();
private void processPeersDaoStateHashes(List<DaoStateHash> stateHashes, Optional<NodeAddress> peersNodeAddress) {
boolean useDaoMonitor = preferences.isUseFullModeDaoMonitor();
stateHashes.forEach(peersHash -> {
Optional<DaoStateBlock> optionalDaoStateBlock;
// If we do not add own hashes during initial parsing we fill the missing hashes from the peer and create
// at the last block our own hash.
int height = peersHash.getHeight();
if (!useDaoMonitor &&
!findDaoStateBlock(height).isPresent()) {
if (daoStateService.getChainHeight() == height) {
// At the most recent block we create our own hash
optionalDaoStateBlock = daoStateService.getLastBlock()
.map(this::createDaoStateBlock)
.orElse(findDaoStateBlock(height));
} else {
// Otherwise we create a block from the peers daoStateHash
DaoStateHash daoStateHash = new DaoStateHash(height, peersHash.getHash(), false);
DaoStateBlock daoStateBlock = new DaoStateBlock(daoStateHash);
daoStateBlockChain.add(daoStateBlock);
daoStateBlockByHeight.put(height, daoStateBlock);
daoStateHashChain.add(daoStateHash);
optionalDaoStateBlock = Optional.of(daoStateBlock);
}
} else {
optionalDaoStateBlock = findDaoStateBlock(height);
}
AtomicBoolean changed = new AtomicBoolean(false);
AtomicBoolean inConflictWithNonSeedNode = new AtomicBoolean(this.isInConflictWithNonSeedNode);
AtomicBoolean inConflictWithSeedNode = new AtomicBoolean(this.isInConflictWithSeedNode);
StringBuilder sb = new StringBuilder();
daoStateBlockChain.stream()
.filter(e -> e.getHeight() == daoStateHash.getHeight()).findAny()
.ifPresent(daoStateBlock -> {
String peersNodeAddressAsString = peersNodeAddress.map(NodeAddress::getFullAddress)
.orElseGet(() -> "Unknown peer " + new Random().nextInt(10000));
daoStateBlock.putInPeersMap(peersNodeAddressAsString, daoStateHash);
if (!daoStateBlock.getMyStateHash().hasEqualHash(daoStateHash)) {
daoStateBlock.putInConflictMap(peersNodeAddressAsString, daoStateHash);
if (seedNodeAddresses.contains(peersNodeAddressAsString)) {
inConflictWithSeedNode.set(true);
} else {
inConflictWithNonSeedNode.set(true);
}
sb.append("We received a block hash from peer ")
.append(peersNodeAddressAsString)
.append(" which conflicts with our block hash.\n")
.append("my daoStateHash=")
.append(daoStateBlock.getMyStateHash())
.append("\npeers daoStateHash=")
.append(daoStateHash);
}
changed.set(true);
});
// In any case we add the peer to our peersMap and check for conflicts on the relevant daoStateBlock
putInPeersMapAndCheckForConflicts(optionalDaoStateBlock, getPeersAddress(peersNodeAddress), peersHash);
});
}
this.isInConflictWithNonSeedNode = inConflictWithNonSeedNode.get();
this.isInConflictWithSeedNode = inConflictWithSeedNode.get();
private void putInPeersMapAndCheckForConflicts(String peersAddress, DaoStateHash peersHash) {
putInPeersMapAndCheckForConflicts(findDaoStateBlock(peersHash.getHeight()), peersAddress, peersHash);
}
String conflictMsg = sb.toString();
if (!conflictMsg.isEmpty()) {
if (this.isInConflictWithSeedNode)
log.warn("Conflict with seed nodes: {}", conflictMsg);
else if (this.isInConflictWithNonSeedNode)
log.debug("Conflict with non-seed nodes: {}", conflictMsg);
private void putInPeersMapAndCheckForConflicts(Optional<DaoStateBlock> optionalDaoStateBlock,
String peersAddress,
DaoStateHash peersHash) {
optionalDaoStateBlock.ifPresent(daoStateBlock -> {
daoStateBlock.putInPeersMap(peersAddress, peersHash);
checkForHashConflicts(peersHash, peersAddress, daoStateBlock);
});
}
private void checkForHashConflicts(DaoStateHash peersDaoStateHash,
String peersNodeAddress,
DaoStateBlock daoStateBlock) {
if (daoStateBlock.getMyStateHash().hasEqualHash(peersDaoStateHash)) {
return;
}
if (notifyListeners && changed.get()) {
listeners.forEach(Listener::onChangeAfterBatchProcessing);
daoStateBlock.putInConflictMap(peersNodeAddress, peersDaoStateHash);
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("We received a block hash from peer ")
.append(peersNodeAddress)
.append(" which conflicts with our block hash.\n")
.append("my peersDaoStateHash=")
.append(daoStateBlock.getMyStateHash())
.append("\npeers peersDaoStateHash=")
.append(peersDaoStateHash);
String conflictMsg = stringBuilder.toString();
if (isSeedNode(peersNodeAddress)) {
isInConflictWithSeedNode = true;
log.warn("Conflict with seed nodes: {}", conflictMsg);
} else {
isInConflictWithNonSeedNode = true;
log.debug("Conflict with non-seed nodes: {}", conflictMsg);
}
}
GcUtil.maybeReleaseMemory();
private void checkUtxos(Block block) {
long genesisTotalSupply = daoStateService.getGenesisTotalSupply().value;
long compensationIssuance = daoStateService.getTotalIssuedAmount(IssuanceType.COMPENSATION);
long reimbursementIssuance = daoStateService.getTotalIssuedAmount(IssuanceType.REIMBURSEMENT);
long totalAmountOfBurntBsq = daoStateService.getTotalAmountOfBurntBsq();
// confiscated funds are still in the utxo set
long sumUtxo = daoStateService.getUnspentTxOutputMap().values().stream().mapToLong(BaseTxOutput::getValue).sum();
long sumBsq = genesisTotalSupply + compensationIssuance + reimbursementIssuance - totalAmountOfBurntBsq;
return changed.get();
if (sumBsq != sumUtxo) {
utxoMismatches.add(new UtxoMismatch(block.getHeight(), sumUtxo, sumBsq));
}
}
private void verifyCheckpoints() {
@ -431,4 +494,17 @@ public class DaoStateMonitoringService implements DaoSetupService, DaoStateListe
log.error(t.toString());
}
}
private boolean isSeedNode(String peersNodeAddress) {
return seedNodeAddresses.contains(peersNodeAddress);
}
private String getPeersAddress(Optional<NodeAddress> peersNodeAddress) {
return peersNodeAddress.map(NodeAddress::getFullAddress)
.orElseGet(() -> "Unknown peer " + new Random().nextInt(10000));
}
private Optional<DaoStateBlock> findDaoStateBlock(int height) {
return Optional.ofNullable(daoStateBlockByHeight.get(height));
}
}

View File

@ -275,7 +275,7 @@ public class ProposalStateMonitoringService implements DaoSetupService, DaoState
}
byte[] combined = ArrayUtils.addAll(prevHash, serializedProposals);
byte[] hash = Hash.getSha256Ripemd160hash(combined);
ProposalStateHash myProposalStateHash = new ProposalStateHash(blockHeight, hash, prevHash, proposals.size());
ProposalStateHash myProposalStateHash = new ProposalStateHash(blockHeight, hash, proposals.size());
ProposalStateBlock proposalStateBlock = new ProposalStateBlock(myProposalStateHash);
proposalStateBlockChain.add(proposalStateBlock);
proposalStateHashChain.add(myProposalStateHash);

View File

@ -29,8 +29,8 @@ public final class BlindVoteStateHash extends StateHash {
@Getter
private final int numBlindVotes;
public BlindVoteStateHash(int cycleStartBlockHeight, byte[] hash, byte[] prevHash, int numBlindVotes) {
super(cycleStartBlockHeight, hash, prevHash);
public BlindVoteStateHash(int cycleStartBlockHeight, byte[] hash, int numBlindVotes) {
super(cycleStartBlockHeight, hash);
this.numBlindVotes = numBlindVotes;
}
@ -43,14 +43,12 @@ public final class BlindVoteStateHash extends StateHash {
return protobuf.BlindVoteStateHash.newBuilder()
.setHeight(height)
.setHash(ByteString.copyFrom(hash))
.setPrevHash(ByteString.copyFrom(prevHash))
.setNumBlindVotes(numBlindVotes).build();
}
public static BlindVoteStateHash fromProto(protobuf.BlindVoteStateHash proto) {
return new BlindVoteStateHash(proto.getHeight(),
proto.getHash().toByteArray(),
proto.getPrevHash().toByteArray(),
proto.getNumBlindVotes());
}

View File

@ -18,12 +18,14 @@
package bisq.core.dao.monitoring.model;
import lombok.EqualsAndHashCode;
import lombok.Getter;
@Getter
@EqualsAndHashCode(callSuper = true)
public class DaoStateBlock extends StateBlock<DaoStateHash> {
public DaoStateBlock(DaoStateHash myDaoStateHash) {
super(myDaoStateHash);
}
public boolean isSelfCreated() {
return myStateHash.isSelfCreated();
}
}

View File

@ -21,11 +21,17 @@ package bisq.core.dao.monitoring.model;
import com.google.protobuf.ByteString;
import lombok.EqualsAndHashCode;
import lombok.Getter;
@Getter
@EqualsAndHashCode(callSuper = true)
public final class DaoStateHash extends StateHash {
public DaoStateHash(int height, byte[] hash, byte[] prevHash) {
super(height, hash, prevHash);
// If we have built the hash by ourself opposed to that we got delivered the hash from seed nodes or resources
private final boolean isSelfCreated;
public DaoStateHash(int height, byte[] hash, boolean isSelfCreated) {
super(height, hash);
this.isSelfCreated = isSelfCreated;
}
@ -38,12 +44,18 @@ public final class DaoStateHash extends StateHash {
return protobuf.DaoStateHash.newBuilder()
.setHeight(height)
.setHash(ByteString.copyFrom(hash))
.setPrevHash(ByteString.copyFrom(prevHash)).build();
.setIsSelfCreated(isSelfCreated)
.build();
}
public static DaoStateHash fromProto(protobuf.DaoStateHash proto) {
return new DaoStateHash(proto.getHeight(),
proto.getHash().toByteArray(),
proto.getPrevHash().toByteArray());
return new DaoStateHash(proto.getHeight(), proto.getHash().toByteArray(), proto.getIsSelfCreated());
}
@Override
public String toString() {
return "DaoStateHash{" +
"\r\n isSelfCreated=" + isSelfCreated +
"\r\n} " + super.toString();
}
}

View File

@ -29,8 +29,8 @@ public final class ProposalStateHash extends StateHash {
@Getter
private final int numProposals;
public ProposalStateHash(int cycleStartBlockHeight, byte[] hash, byte[] prevHash, int numProposals) {
super(cycleStartBlockHeight, hash, prevHash);
public ProposalStateHash(int cycleStartBlockHeight, byte[] hash, int numProposals) {
super(cycleStartBlockHeight, hash);
this.numProposals = numProposals;
}
@ -43,14 +43,12 @@ public final class ProposalStateHash extends StateHash {
return protobuf.ProposalStateHash.newBuilder()
.setHeight(height)
.setHash(ByteString.copyFrom(hash))
.setPrevHash(ByteString.copyFrom(prevHash))
.setNumProposals(numProposals).build();
}
public static ProposalStateHash fromProto(protobuf.ProposalStateHash proto) {
return new ProposalStateHash(proto.getHeight(),
proto.getHash().toByteArray(),
proto.getPrevHash().toByteArray(),
proto.getNumProposals());
}

View File

@ -56,10 +56,6 @@ public abstract class StateBlock<T extends StateHash> {
return myStateHash.getHash();
}
public byte[] getPrevHash() {
return myStateHash.getPrevHash();
}
@Override
public String toString() {
return "StateBlock{" +

View File

@ -40,13 +40,10 @@ import lombok.extern.slf4j.Slf4j;
public abstract class StateHash implements PersistablePayload, NetworkPayload {
protected final int height;
protected final byte[] hash;
// For first block the prevHash is an empty byte array
protected final byte[] prevHash;
StateHash(int height, byte[] hash, byte[] prevHash) {
StateHash(int height, byte[] hash) {
this.height = height;
this.hash = hash;
this.prevHash = prevHash;
}
///////////////////////////////////////////////////////////////////////////////////////////
@ -67,7 +64,6 @@ public abstract class StateHash implements PersistablePayload, NetworkPayload {
return "StateHash{" +
"\n height=" + height +
",\n hash=" + Utilities.bytesAsHexString(hash) +
",\n prevHash=" + Utilities.bytesAsHexString(prevHash) +
"\n}";
}
}

View File

@ -102,7 +102,8 @@ public abstract class StateNetworkService<Msg extends NewStateHashMessage,
protected abstract Msg getNewStateHashMessage(StH myStateHash);
protected abstract Han getRequestStateHashesHandler(NodeAddress nodeAddress, RequestStateHashesHandler.Listener<Res> listener);
protected abstract Han getRequestStateHashesHandler(NodeAddress nodeAddress,
RequestStateHashesHandler.Listener<Res> listener);
///////////////////////////////////////////////////////////////////////////////////////////
@ -155,8 +156,7 @@ public abstract class StateNetworkService<Msg extends NewStateHashMessage,
}
public void broadcastMyStateHash(StH myStateHash) {
NewStateHashMessage newStateHashMessage = getNewStateHashMessage(myStateHash);
broadcaster.broadcast(newStateHashMessage, networkNode.getNodeAddress());
broadcaster.broadcast(getNewStateHashMessage(myStateHash), networkNode.getNodeAddress());
}
public void requestHashes(int fromHeight, String peersAddress) {
@ -167,6 +167,10 @@ public abstract class StateNetworkService<Msg extends NewStateHashMessage,
requestStateHashHandlerMap.clear();
}
public boolean isSeedNode(NodeAddress nodeAddress) {
return peerManager.isSeedNode(nodeAddress);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Listeners

View File

@ -135,10 +135,11 @@ public class FullNode extends BsqNode {
protected void onParseBlockChainComplete() {
super.onParseBlockChainComplete();
if (p2pNetworkReady)
if (p2pNetworkReady) {
addBlockHandler();
else
} else {
log.info("onParseBlockChainComplete but P2P network is not ready yet.");
}
}

View File

@ -222,7 +222,7 @@ public class LiteNode extends BsqNode {
runDelayedBatchProcessing(new ArrayList<>(blockList),
() -> {
log.info("runDelayedBatchProcessing Parsing {} blocks took {} seconds.", blockList.size(),
log.info("Parsing {} blocks took {} seconds.", blockList.size(),
(System.currentTimeMillis() - ts) / 1000d);
// We only request again if wallet is synced, otherwise we would get repeated calls we want to avoid.
// We deal with that case at the setupWalletBestBlockListener method above.

View File

@ -200,6 +200,7 @@ public class LiteNodeNetworkService implements MessageListener, ConnectionListen
@Override
public void onAllConnectionsLost() {
log.info("onAllConnectionsLost");
closeAllHandlers();
stopRetryTimer();
stopped = true;
@ -208,6 +209,7 @@ public class LiteNodeNetworkService implements MessageListener, ConnectionListen
@Override
public void onNewConnectionAfterAllConnectionsLost() {
log.info("onNewConnectionAfterAllConnectionsLost");
closeAllHandlers();
stopped = false;
tryWithNewSeedNode(lastRequestedBlockHeight);

View File

@ -24,7 +24,6 @@ import bisq.core.dao.state.DaoStateService;
import bisq.core.dao.state.model.blockchain.Block;
import bisq.common.app.DevEnv;
import bisq.common.util.GcUtil;
import org.bitcoinj.core.Coin;
@ -113,10 +112,12 @@ public class BlockParser {
.ifPresent(tx -> daoStateService.onNewTxForLastBlock(block, tx)));
daoStateService.onParseBlockComplete(block);
log.info("Parsing {} transactions at block height {} took {} ms", rawBlock.getRawTxs().size(),
blockHeight, System.currentTimeMillis() - startTs);
long duration = System.currentTimeMillis() - startTs;
if (duration > 10) {
log.info("Parsing {} transactions at block height {} took {} ms", rawBlock.getRawTxs().size(),
blockHeight, duration);
}
GcUtil.maybeReleaseMemory();
return block;
}

View File

@ -17,11 +17,13 @@
package bisq.core.dao.state;
import bisq.core.dao.DaoSetupService;
import bisq.core.dao.monitoring.DaoStateMonitoringService;
import bisq.core.dao.monitoring.model.DaoStateHash;
import bisq.core.dao.state.model.DaoState;
import bisq.core.dao.state.model.blockchain.Block;
import bisq.core.dao.state.storage.DaoStateStorageService;
import bisq.core.user.Preferences;
import bisq.common.config.Config;
import bisq.common.util.GcUtil;
@ -49,13 +51,14 @@ import javax.annotation.Nullable;
* SNAPSHOT_GRID old not less than 2 times the SNAPSHOT_GRID old.
*/
@Slf4j
public class DaoStateSnapshotService {
public class DaoStateSnapshotService implements DaoSetupService, DaoStateListener {
private static final int SNAPSHOT_GRID = 20;
private final DaoStateService daoStateService;
private final GenesisTxInfo genesisTxInfo;
private final DaoStateStorageService daoStateStorageService;
private final DaoStateMonitoringService daoStateMonitoringService;
private final Preferences preferences;
private final File storageDir;
private DaoState daoStateSnapshotCandidate;
@ -64,7 +67,8 @@ public class DaoStateSnapshotService {
@Setter
@Nullable
private Runnable daoRequiresRestartHandler;
private boolean requestPersistenceCalled;
private boolean readyForPersisting = true;
private boolean isParseBlockChainComplete;
///////////////////////////////////////////////////////////////////////////////////////////
@ -76,21 +80,86 @@ public class DaoStateSnapshotService {
GenesisTxInfo genesisTxInfo,
DaoStateStorageService daoStateStorageService,
DaoStateMonitoringService daoStateMonitoringService,
Preferences preferences,
@Named(Config.STORAGE_DIR) File storageDir) {
this.daoStateService = daoStateService;
this.genesisTxInfo = genesisTxInfo;
this.daoStateStorageService = daoStateStorageService;
this.daoStateMonitoringService = daoStateMonitoringService;
this.preferences = preferences;
this.storageDir = storageDir;
}
///////////////////////////////////////////////////////////////////////////////////////////
// DaoSetupService
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void addListeners() {
daoStateService.addDaoStateListener(this);
}
@Override
public void start() {
}
///////////////////////////////////////////////////////////////////////////////////////////
// DaoStateListener
///////////////////////////////////////////////////////////////////////////////////////////
// We listen onDaoStateChanged to ensure the dao state has been processed from listener clients after parsing.
// We need to listen during batch processing as well to write snapshots during that process.
@Override
public void onDaoStateChanged(Block block) {
// If we have isUseDaoMonitor activated we apply the hash and snapshots at each new block during initial parsing.
// Otherwise we do it only after the initial blockchain parsing is completed to not delay the parsing.
// In that case we get the missing hashes from the seed nodes. At any new block we do the hash calculation
// ourself and therefore get back confidence that our DAO state is in sync with the network.
if (preferences.isUseFullModeDaoMonitor() || isParseBlockChainComplete) {
// We need to execute first the daoStateMonitoringService.createHashFromBlock to get the hash created
daoStateMonitoringService.createHashFromBlock(block);
maybeCreateSnapshot(block);
}
}
@Override
public void onParseBlockChainComplete() {
isParseBlockChainComplete = true;
// In case we have dao monitoring deactivated we create the snapshot after we are completed with parsing
// and we got called back from daoStateMonitoringService once the hashes are created from peers data.
if (!preferences.isUseFullModeDaoMonitor()) {
// We register a callback handler once the daoStateMonitoringService has received the missing hashes from
// the seed node and applied the latest hash. After that we are ready to make a snapshot and persist it.
daoStateMonitoringService.setCreateSnapshotHandler(() -> {
// As we did not have created any snapshots during initial parsing we create it now. We cannot use the past
// snapshot height as we have not cloned a candidate (that would cause quite some delay during parsing).
// The next snapshots will be created again according to the snapshot height grid (each 20 blocks).
// This also comes with the improvement that the user does not need to load the past blocks back to the last
// snapshot height. Though it comes also with the small risk that in case of re-orgs the user need to do
// a resync in case the dao state would have been affected by that reorg.
long ts = System.currentTimeMillis();
// We do not keep a copy of the clone as we use it immediately for persistence.
GcUtil.maybeReleaseMemory();
log.info("Create snapshot at height {}", daoStateService.getChainHeight());
daoStateStorageService.requestPersistence(daoStateService.getClone(),
new LinkedList<>(daoStateMonitoringService.getDaoStateHashChain()),
() -> {
log.info("Persisted daoState after parsing completed at height {}. Took {} ms",
daoStateService.getChainHeight(), System.currentTimeMillis() - ts);
});
GcUtil.maybeReleaseMemory();
});
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
// We do not use DaoStateListener.onDaoStateChanged but let the DaoEventCoordinator call maybeCreateSnapshot to ensure the
// correct order of execution.
// We need to process during batch processing as well to write snapshots during that process.
public void maybeCreateSnapshot(Block block) {
int chainHeight = block.getHeight();
@ -107,42 +176,68 @@ public class DaoStateSnapshotService {
// We protect to get called while we are not completed with persisting the daoState. This can take about
// 20 seconds and it is not expected that we get triggered another snapshot event in that period, but this
// check guards that we would skip such calls..
if (requestPersistenceCalled) {
log.warn("We try to persist a daoState but the previous call has not completed yet. " +
"We ignore that call and skip that snapshot. " +
"Snapshot will be created at next snapshot height again. This is not to be expected with live " +
"blockchain data.");
if (!readyForPersisting) {
if (preferences.isUseFullModeDaoMonitor()) {
// In case we dont use isUseFullModeDaoMonitor we might called here too often as the parsing is much
// faster than the persistence and we likely create only 1 snapshot during initial parsing, so
// we log only if isUseFullModeDaoMonitor is true as then parsing is likely slower and we would
// expect that we do a snapshot at each trigger block.
log.info("We try to persist a daoState but the previous call has not completed yet. " +
"We ignore that call and skip that snapshot. " +
"Snapshot will be created at next snapshot height again. This is not to be expected with live " +
"blockchain data.");
}
return;
}
GcUtil.maybeReleaseMemory();
// At trigger event we store the latest snapshotCandidate to disc
long ts = System.currentTimeMillis();
requestPersistenceCalled = true;
daoStateStorageService.requestPersistence(daoStateSnapshotCandidate,
daoStateHashChainSnapshotCandidate,
() -> {
log.info("Serializing snapshotCandidate for writing to Disc with height {} at height {} took {} ms",
daoStateSnapshotCandidate != null ? daoStateSnapshotCandidate.getChainHeight() : "N/A",
chainHeight,
System.currentTimeMillis() - ts);
long ts2 = System.currentTimeMillis();
GcUtil.maybeReleaseMemory();
// Now we clone and keep it in memory for the next trigger event
daoStateSnapshotCandidate = daoStateService.getClone();
daoStateHashChainSnapshotCandidate = new LinkedList<>(daoStateMonitoringService.getDaoStateHashChain());
log.info("Cloned new snapshotCandidate at height {} took {} ms", chainHeight, System.currentTimeMillis() - ts2);
requestPersistenceCalled = false;
GcUtil.maybeReleaseMemory();
});
if (daoStateSnapshotCandidate != null) {
persist();
} else {
createClones();
}
}
}
private void persist() {
long ts = System.currentTimeMillis();
readyForPersisting = false;
daoStateStorageService.requestPersistence(daoStateSnapshotCandidate,
daoStateHashChainSnapshotCandidate,
() -> {
log.info("Serializing snapshotCandidate for writing to Disc at chainHeight {} took {} ms.\n" +
"daoStateSnapshotCandidate.height={};\n" +
"daoStateHashChainSnapshotCandidate.height={}",
daoStateService.getChainHeight(),
System.currentTimeMillis() - ts,
daoStateSnapshotCandidate != null ? daoStateSnapshotCandidate.getChainHeight() : "N/A",
daoStateHashChainSnapshotCandidate != null && !daoStateHashChainSnapshotCandidate.isEmpty() ?
daoStateHashChainSnapshotCandidate.getLast().getHeight() : "N/A");
createClones();
readyForPersisting = true;
});
}
private void createClones() {
long ts = System.currentTimeMillis();
// Now we clone and keep it in memory for the next trigger event
// We do not fit into the target grid of 20 blocks as we get called here once persistence is
// done from the write thread (mapped back to user thread).
// As we want to prevent to maintain 2 clones we prefer that strategy. If we would do the clone
// after the persist call we would keep an additional copy in memory.
daoStateSnapshotCandidate = daoStateService.getClone();
daoStateHashChainSnapshotCandidate = new LinkedList<>(daoStateMonitoringService.getDaoStateHashChain());
GcUtil.maybeReleaseMemory();
log.info("Cloned new snapshotCandidate at chainHeight {} took {} ms.\n" +
"daoStateSnapshotCandidate.height={};\n" +
"daoStateHashChainSnapshotCandidate.height={}",
daoStateService.getChainHeight(), System.currentTimeMillis() - ts,
daoStateSnapshotCandidate != null ? daoStateSnapshotCandidate.getChainHeight() : "N/A",
daoStateHashChainSnapshotCandidate != null && !daoStateHashChainSnapshotCandidate.isEmpty() ?
daoStateHashChainSnapshotCandidate.getLast().getHeight() : "N/A");
}
public void applySnapshot(boolean fromReorg) {
DaoState persistedBsqState = daoStateStorageService.getPersistedBsqState();
LinkedList<DaoStateHash> persistedDaoStateHashChain = daoStateStorageService.getPersistedDaoStateHashChain();
@ -156,6 +251,7 @@ public class DaoStateSnapshotService {
chainHeightOfLastApplySnapshot = chainHeightOfPersisted;
daoStateService.applySnapshot(persistedBsqState);
daoStateMonitoringService.applySnapshot(persistedDaoStateHashChain);
daoStateStorageService.pruneStore();
} else {
// The reorg might have been caused by the previous parsing which might contains a range of
// blocks.

View File

@ -27,6 +27,7 @@ import bisq.network.p2p.storage.persistence.StoreService;
import bisq.common.config.Config;
import bisq.common.file.FileUtil;
import bisq.common.persistence.PersistenceManager;
import bisq.common.util.GcUtil;
import javax.inject.Inject;
import javax.inject.Named;
@ -92,15 +93,20 @@ public class DaoStateStorageService extends StoreService<DaoStateStore> {
new Thread(() -> {
Thread.currentThread().setName("Serialize and write DaoState");
persistenceManager.persistNow(() -> {
// After we have written to disk we remove the the daoState in the store to avoid that it stays in
// After we have written to disk we remove the daoState in the store to avoid that it stays in
// memory there until the next persist call.
store.setDaoState(null);
pruneStore();
completeHandler.run();
});
}).start();
}
public void pruneStore() {
store.setDaoState(null);
store.setDaoStateHashChain(null);
GcUtil.maybeReleaseMemory();
}
public DaoState getPersistedBsqState() {
return store.getDaoState();
}

View File

@ -82,7 +82,6 @@ public class TradeStatisticsManager {
this.storageDir = storageDir;
this.dumpStatistics = dumpStatistics;
appendOnlyDataStoreService.addService(tradeStatistics3StorageService);
}

View File

@ -138,7 +138,7 @@ class XmrTxProofRequestsPerTrade implements AssetTxProofRequestsPerTrade {
// We set serviceAddresses at request time. If user changes AutoConfirmSettings after request has started
// it will have no impact on serviceAddresses and numRequiredSuccessResults.
// Thought numRequiredConfirmations can be changed during request process and will be read from
// Though numRequiredConfirmations can be changed during request process and will be read from
// autoConfirmSettings at result parsing.
List<String> serviceAddresses = autoConfirmSettings.getServiceAddresses();
numRequiredSuccessResults = serviceAddresses.size();

View File

@ -800,6 +800,11 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid
requestPersistence();
}
public void setUseFullModeDaoMonitor(boolean value) {
prefPayload.setUseFullModeDaoMonitor(value);
requestPersistence();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Getter
@ -1115,5 +1120,7 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid
void setDenyApiTaker(boolean value);
void setNotifyOnPreRelease(boolean value);
void setUseFullModeDaoMonitor(boolean value);
}
}

View File

@ -134,6 +134,7 @@ public final class PreferencesPayload implements PersistableEnvelope {
private boolean showOffersMatchingMyAccounts;
private boolean denyApiTaker;
private boolean notifyOnPreRelease;
private boolean useFullModeDaoMonitor;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
@ -201,7 +202,8 @@ public final class PreferencesPayload implements PersistableEnvelope {
.setHideNonAccountPaymentMethods(hideNonAccountPaymentMethods)
.setShowOffersMatchingMyAccounts(showOffersMatchingMyAccounts)
.setDenyApiTaker(denyApiTaker)
.setNotifyOnPreRelease(notifyOnPreRelease);
.setNotifyOnPreRelease(notifyOnPreRelease)
.setUseFullModeDaoMonitor(useFullModeDaoMonitor);
Optional.ofNullable(backupDirectory).ifPresent(builder::setBackupDirectory);
Optional.ofNullable(preferredTradeCurrency).ifPresent(e -> builder.setPreferredTradeCurrency((protobuf.TradeCurrency) e.toProtoMessage()));
@ -299,7 +301,8 @@ public final class PreferencesPayload implements PersistableEnvelope {
proto.getHideNonAccountPaymentMethods(),
proto.getShowOffersMatchingMyAccounts(),
proto.getDenyApiTaker(),
proto.getNotifyOnPreRelease()
proto.getNotifyOnPreRelease(),
proto.getUseFullModeDaoMonitor()
);
}
}

View File

@ -1351,6 +1351,18 @@ setting.preferences.dao.resyncFromGenesis.resync=Resync from genesis and shutdow
setting.preferences.dao.isDaoFullNode=Run Bisq as DAO full node
setting.preferences.dao.activated=DAO activated
setting.preferences.dao.activated.popup=The change will be applied after a restart
setting.preferences.dao.fullModeDaoMonitor=Full-mode DAO state monitoring
setting.preferences.dao.fullModeDaoMonitor.popup=If full-mode DAO state monitoring is activated the DAO state \
hashes are created during parsing the BSQ blocks. This comes with considerable performance costs at the initial DAO sync.\n\n\
For users who are regularily using Bisq this should not be an issue as there are not many blocks pending for parsing, though for \
users who only use Bisq casually creating the DAO state hashes for 100s or 1000s of blocks degrades heavily the user experience.\n\n\
In case full-mode is deactivated (default) the missing DAO state hashes are requested from network nodes and \
the DAO state hash based on the most recent block will be created by the user. As all hashes are connected by \
reference to the previous hash a correct hash at the chain tip means that all past hashes are correct as well. The \
main functionality of the DAO state monitoring - to detect if the local DAO state is out of sync with the rest of the network - \
is therefore still fulfilled.
setting.preferences.dao.rpcUser=RPC username
setting.preferences.dao.rpcPw=RPC password
setting.preferences.dao.blockNotifyPort=Block notify port
@ -2499,6 +2511,9 @@ dao.monitor.proposals=Proposals state
dao.monitor.blindVotes=Blind votes state
dao.monitor.table.peers=Peers
dao.monitor.table.hashCreator=Hash creator
dao.monitor.table.hashCreator.self=Self
dao.monitor.table.hashCreator.peer=Peer
dao.monitor.table.conflicts=Conflicts
dao.monitor.state=Status
dao.monitor.requestAlHashes=Request all hashes
@ -2532,6 +2547,8 @@ dao.monitor.isInConflictWithSeedNode=Your local data is not in consensus with at
Please resync the DAO state.
dao.monitor.isInConflictWithNonSeedNode=One of your peers is not in consensus with the network but your node \
is in sync with the seed nodes.
dao.monitor.isDaoStateBlockChainNotConnecting=Your DAO state chain is not connecting with the new data. \
Please resync the DAO state.
dao.monitor.daoStateInSync=Your local node is in consensus with the network
dao.monitor.blindVote.headline=Blind votes state

View File

@ -38,6 +38,7 @@ public class DaoStateSnapshotServiceTest {
mock(GenesisTxInfo.class),
mock(DaoStateStorageService.class),
mock(DaoStateMonitoringService.class),
null,
null);
}

View File

@ -416,7 +416,7 @@ public class MainView extends InitializableView<StackPane, MainViewModel>
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void onChangeAfterBatchProcessing() {
public void onDaoStateHashesChanged() {
}
@Override

View File

@ -36,10 +36,9 @@ import lombok.extern.slf4j.Slf4j;
@Getter
@EqualsAndHashCode
public abstract class StateBlockListItem<StH extends StateHash, StB extends StateBlock<StH>> {
private final StateBlock<StH> stateBlock;
protected final StateBlock<StH> stateBlock;
private final Supplier<String> height;
private final String hash;
private final String prevHash;
private final String numNetworkMessages;
private final String numMisMatches;
private final boolean isInSync;
@ -58,7 +57,6 @@ public abstract class StateBlockListItem<StH extends StateHash, StB extends Stat
Res.get("dao.monitor.table.cycleBlockHeight", cycleIndexSupplier.getAsInt() + 1,
String.valueOf(stateBlock.getHeight())))::get;
hash = Utilities.bytesAsHexString(stateBlock.getHash());
prevHash = stateBlock.getPrevHash().length > 0 ? Utilities.bytesAsHexString(stateBlock.getPrevHash()) : "-";
numNetworkMessages = String.valueOf(stateBlock.getPeersMap().size());
int size = stateBlock.getInConflictMap().size();
numMisMatches = String.valueOf(size);

View File

@ -38,7 +38,6 @@ public abstract class StateInConflictListItem<T extends StateHash> {
private final String peerAddressString;
private final String height;
private final String hash;
private final String prevHash;
private final T stateHash;
protected StateInConflictListItem(String peerAddress, T stateHash, int cycleIndex,
@ -52,7 +51,5 @@ public abstract class StateInConflictListItem<T extends StateHash> {
cycleIndex + 1,
String.valueOf(stateHash.getHeight()));
hash = Utilities.bytesAsHexString(stateHash.getHash());
prevHash = stateHash.getPrevHash().length > 0 ?
Utilities.bytesAsHexString(stateHash.getPrevHash()) : "-";
}
}

View File

@ -99,6 +99,7 @@ public abstract class StateMonitorView<StH extends StateHash,
private Subscription selectedItemSubscription;
protected final BooleanProperty isInConflictWithNonSeedNode = new SimpleBooleanProperty();
protected final BooleanProperty isInConflictWithSeedNode = new SimpleBooleanProperty();
protected final BooleanProperty isDaoStateBlockChainNotConnecting = new SimpleBooleanProperty();
///////////////////////////////////////////////////////////////////////////////////////////
@ -134,8 +135,10 @@ public abstract class StateMonitorView<StH extends StateHash,
daoStateService.addDaoStateListener(this);
resyncButton.visibleProperty().bind(isInConflictWithSeedNode);
resyncButton.managedProperty().bind(isInConflictWithSeedNode);
resyncButton.visibleProperty().bind(isInConflictWithSeedNode
.or(isDaoStateBlockChainNotConnecting));
resyncButton.managedProperty().bind(isInConflictWithSeedNode
.or(isDaoStateBlockChainNotConnecting));
resyncButton.setOnAction(ev -> resyncDaoState());
@ -177,8 +180,6 @@ public abstract class StateMonitorView<StH extends StateHash,
protected abstract String getPeersTableHeader();
protected abstract String getPrevHashTableHeader();
protected abstract String getHashTableHeader();
protected abstract String getBlockHeightTableHeader();
@ -272,6 +273,9 @@ public abstract class StateMonitorView<StH extends StateHash,
} else if (isInConflictWithNonSeedNode.get()) {
statusTextField.setText(Res.get("dao.monitor.isInConflictWithNonSeedNode"));
statusTextField.getStyleClass().remove("dao-inConflict");
} else if (isDaoStateBlockChainNotConnecting.get()) {
statusTextField.setText(Res.get("dao.monitor.isDaoStateBlockChainNotConnecting"));
statusTextField.getStyleClass().add("dao-inConflict");
} else {
statusTextField.setText(Res.get("dao.monitor.daoStateInSync"));
statusTextField.getStyleClass().remove("dao-inConflict");
@ -328,7 +332,6 @@ public abstract class StateMonitorView<StH extends StateHash,
tableView.getSortOrder().add(column);
tableView.getColumns().add(column);
column = new AutoTooltipTableColumn<>(getHashTableHeader());
column.setMinWidth(120);
column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue()));
@ -353,31 +356,6 @@ public abstract class StateMonitorView<StH extends StateHash,
tableView.getColumns().add(column);
column = new AutoTooltipTableColumn<>(getPrevHashTableHeader());
column.setMinWidth(120);
column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue()));
column.setCellFactory(
new Callback<>() {
@Override
public TableCell<BLI, BLI> call(TableColumn<BLI,
BLI> column) {
return new TableCell<>() {
@Override
public void updateItem(final BLI item, boolean empty) {
super.updateItem(item, empty);
if (item != null)
setText(item.getPrevHash());
else
setText("");
}
};
}
});
column.setComparator(Comparator.comparing(BLI::getPrevHash));
tableView.getColumns().add(column);
column = new AutoTooltipTableColumn<>(getPeersTableHeader());
column.setMinWidth(80);
column.setMaxWidth(column.getMinWidth());
@ -543,30 +521,6 @@ public abstract class StateMonitorView<StH extends StateHash,
conflictTableView.getColumns().add(column);
column = new AutoTooltipTableColumn<>(getPrevHashTableHeader());
column.setMinWidth(120);
column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue()));
column.setCellFactory(
new Callback<>() {
@Override
public TableCell<CLI, CLI> call(
TableColumn<CLI, CLI> column) {
return new TableCell<>() {
@Override
public void updateItem(final CLI item, boolean empty) {
super.updateItem(item, empty);
if (item != null)
setText(item.getPrevHash());
else
setText("");
}
};
}
});
column.setComparator(Comparator.comparing(CLI::getPrevHash));
conflictTableView.getColumns().add(column);
column = new AutoTooltipTableColumn<>("");
column.setMinWidth(120);
column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue()));

View File

@ -149,11 +149,6 @@ public class BlindVoteStateMonitorView extends StateMonitorView<BlindVoteStateHa
return Res.get("dao.monitor.table.peers");
}
@Override
protected String getPrevHashTableHeader() {
return Res.get("dao.monitor.blindVote.table.prev");
}
@Override
protected String getHashTableHeader() {
return Res.get("dao.monitor.blindVote.table.hash");
@ -221,7 +216,6 @@ public class BlindVoteStateMonitorView extends StateMonitorView<BlindVoteStateHa
tableView.getColumns().add(1, column);
}
protected void createConflictColumns() {
super.createConflictColumns();

View File

@ -21,6 +21,7 @@ import bisq.desktop.main.dao.monitor.StateBlockListItem;
import bisq.core.dao.monitoring.model.DaoStateBlock;
import bisq.core.dao.monitoring.model.DaoStateHash;
import bisq.core.locale.Res;
import java.util.function.IntSupplier;
@ -35,4 +36,10 @@ class DaoStateBlockListItem extends StateBlockListItem<DaoStateHash, DaoStateBlo
DaoStateBlockListItem(DaoStateBlock stateBlock, IntSupplier cycleIndexSupplier) {
super(stateBlock, cycleIndexSupplier);
}
String hashCreator() {
return ((DaoStateBlock) stateBlock).isSelfCreated() ?
Res.get("dao.monitor.table.hashCreator.self") :
Res.get("dao.monitor.table.hashCreator.peer");
}
}

View File

@ -18,6 +18,7 @@
package bisq.desktop.main.dao.monitor.daostate;
import bisq.desktop.common.view.FxmlView;
import bisq.desktop.components.AutoTooltipTableColumn;
import bisq.desktop.main.dao.monitor.StateMonitorView;
import bisq.desktop.main.overlays.popups.Popup;
import bisq.desktop.util.FormBuilder;
@ -40,10 +41,18 @@ import bisq.common.util.Utilities;
import javax.inject.Inject;
import javax.inject.Named;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.collections.ListChangeListener;
import javafx.util.Callback;
import java.io.File;
import java.util.Comparator;
import java.util.Map;
import java.util.function.IntSupplier;
import java.util.stream.Collectors;
@ -60,7 +69,6 @@ public class DaoStateMonitorView extends StateMonitorView<DaoStateHash, DaoState
// Constructor, lifecycle
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
private DaoStateMonitorView(DaoStateService daoStateService,
DaoFacade daoFacade,
@ -111,7 +119,7 @@ public class DaoStateMonitorView extends StateMonitorView<DaoStateHash, DaoState
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void onChangeAfterBatchProcessing() {
public void onDaoStateHashesChanged() {
if (daoStateService.isParseBlockChainComplete()) {
onDataUpdate();
}
@ -160,11 +168,6 @@ public class DaoStateMonitorView extends StateMonitorView<DaoStateHash, DaoState
return Res.get("dao.monitor.table.peers");
}
@Override
protected String getPrevHashTableHeader() {
return Res.get("dao.monitor.daoState.table.prev");
}
@Override
protected String getHashTableHeader() {
return Res.get("dao.monitor.daoState.table.hash");
@ -189,6 +192,7 @@ public class DaoStateMonitorView extends StateMonitorView<DaoStateHash, DaoState
protected void onDataUpdate() {
isInConflictWithSeedNode.set(daoStateMonitoringService.isInConflictWithSeedNode());
isInConflictWithNonSeedNode.set(daoStateMonitoringService.isInConflictWithNonSeedNode());
isDaoStateBlockChainNotConnecting.set(daoStateMonitoringService.isDaoStateBlockChainNotConnecting());
listItems.setAll(daoStateMonitoringService.getDaoStateBlockChain().stream()
.map(this::getStateBlockListItem)
@ -202,6 +206,35 @@ public class DaoStateMonitorView extends StateMonitorView<DaoStateHash, DaoState
daoStateMonitoringService.requestHashesFromGenesisBlockHeight(peerAddress);
}
@Override
protected void createColumns() {
super.createColumns();
TableColumn<DaoStateBlockListItem, DaoStateBlockListItem> column = new AutoTooltipTableColumn<>(Res.get("dao.monitor.table.hashCreator"));
column.setMinWidth(90);
column.setMaxWidth(column.getMinWidth());
column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue()));
column.setCellFactory(
new Callback<>() {
@Override
public TableCell<DaoStateBlockListItem, DaoStateBlockListItem> call(
TableColumn<DaoStateBlockListItem, DaoStateBlockListItem> column) {
return new TableCell<>() {
@Override
public void updateItem(final DaoStateBlockListItem item, boolean empty) {
super.updateItem(item, empty);
if (item != null)
setText(item.hashCreator());
else
setText("");
}
};
}
});
column.setComparator(Comparator.comparing(e -> e.getStateBlock().getPeersMap().size()));
tableView.getColumns().add(2, column);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private

View File

@ -147,11 +147,6 @@ public class ProposalStateMonitorView extends StateMonitorView<ProposalStateHash
return Res.get("dao.monitor.table.peers");
}
@Override
protected String getPrevHashTableHeader() {
return Res.get("dao.monitor.proposal.table.prev");
}
@Override
protected String getHashTableHeader() {
return Res.get("dao.monitor.proposal.table.hash");
@ -219,7 +214,6 @@ public class ProposalStateMonitorView extends StateMonitorView<ProposalStateHash
tableView.getColumns().add(1, column);
}
protected void createConflictColumns() {
super.createConflictColumns();

View File

@ -48,6 +48,7 @@ import bisq.core.locale.TradeCurrency;
import bisq.core.payment.PaymentAccount;
import bisq.core.payment.payload.PaymentMethod;
import bisq.core.provider.fee.FeeService;
import bisq.core.user.DontShowAgainLookup;
import bisq.core.user.Preferences;
import bisq.core.user.User;
import bisq.core.util.FormattingUtils;
@ -122,13 +123,12 @@ public class PreferencesView extends ActivatableViewAndModel<GridPane, Preferenc
private ToggleButton showOwnOffersInOfferBook, useAnimations, useDarkMode, sortMarketCurrenciesNumerically,
avoidStandbyMode, useCustomFee, autoConfirmXmrToggle, hideNonAccountPaymentMethodsToggle, denyApiTakerToggle,
notifyOnPreReleaseToggle;
notifyOnPreReleaseToggle, isDaoFullNodeToggleButton, daoActivatedToggleButton, fullModeDaoMonitorToggleButton;
private int gridRow = 0;
private int displayCurrenciesGridRowIndex = 0;
private InputTextField transactionFeeInputTextField, ignoreTradersListInputTextField, ignoreDustThresholdInputTextField,
autoConfRequiredConfirmationsTf, autoConfServiceAddressTf, autoConfTradeLimitTf, /*referralIdInputTextField,*/
autoConfRequiredConfirmationsTf, autoConfServiceAddressTf, autoConfTradeLimitTf,
rpcUserTextField, blockNotifyPortTextField;
private ToggleButton isDaoFullNodeToggleButton, daoActivatedToggleButton;
private PasswordTextField rpcPwTextField;
private TitledGroupBg daoOptionsTitledGroupBg;
@ -623,7 +623,7 @@ public class PreferencesView extends ActivatableViewAndModel<GridPane, Preferenc
}
private void initializeDaoOptions() {
int rowSpan = DevEnv.isDaoActivated() ? 5 : 1;
int rowSpan = DevEnv.isDaoActivated() ? 6 : 1;
daoOptionsTitledGroupBg = addTitledGroupBg(root, ++gridRow, rowSpan,
Res.get("setting.preferences.daoOptions"), Layout.GROUP_DISTANCE);
daoActivatedToggleButton = addSlideToggleButton(root, gridRow,
@ -632,6 +632,9 @@ public class PreferencesView extends ActivatableViewAndModel<GridPane, Preferenc
return;
}
fullModeDaoMonitorToggleButton = addSlideToggleButton(root, ++gridRow,
Res.get("setting.preferences.dao.fullModeDaoMonitor"));
resyncDaoFromResourcesButton = addButton(root, ++gridRow, Res.get("setting.preferences.dao.resyncFromResources.label"));
resyncDaoFromResourcesButton.setMaxWidth(Double.MAX_VALUE);
GridPane.setHgrow(resyncDaoFromResourcesButton, Priority.ALWAYS);
@ -1021,6 +1024,21 @@ public class PreferencesView extends ActivatableViewAndModel<GridPane, Preferenc
return;
}
fullModeDaoMonitorToggleButton.setSelected(preferences.isUseFullModeDaoMonitor());
fullModeDaoMonitorToggleButton.setOnAction(e -> {
preferences.setUseFullModeDaoMonitor(fullModeDaoMonitorToggleButton.isSelected());
if (fullModeDaoMonitorToggleButton.isSelected()) {
String key = "fullModeDaoMonitor";
if (DontShowAgainLookup.showAgain(key)) {
new Popup().information(Res.get("setting.preferences.dao.fullModeDaoMonitor.popup"))
.width(1000)
.dontShowAgainId(key)
.closeButtonText(Res.get("shared.iUnderstand"))
.show();
}
}
});
boolean daoFullNode = preferences.isDaoFullNode();
isDaoFullNodeToggleButton.setSelected(daoFullNode);
@ -1173,6 +1191,7 @@ public class PreferencesView extends ActivatableViewAndModel<GridPane, Preferenc
return;
}
fullModeDaoMonitorToggleButton.setOnAction(null);
resyncDaoFromResourcesButton.setOnAction(null);
resyncDaoFromGenesisButton.setOnAction(null);
bsqAverageTrimThresholdTextField.textProperty().removeListener(bsqAverageTrimThresholdListener);

View File

@ -41,7 +41,7 @@ public class DaoStateBlockListItemTest {
@Test
public void testEqualsAndHashCode() {
var block = new DaoStateBlock(new DaoStateHash(0, new byte[0], new byte[0]));
var block = new DaoStateBlock(new DaoStateHash(0, new byte[0], true));
var item1 = new DaoStateBlockListItem(block, newSupplier(1));
var item2 = new DaoStateBlockListItem(block, newSupplier(2));
var item3 = new DaoStateBlockListItem(block, newSupplier(1));

View File

@ -1945,6 +1945,7 @@ message PreferencesPayload {
bool show_offers_matching_my_accounts = 59;
bool deny_api_taker = 60;
bool notify_on_pre_release = 61;
bool use_full_mode_dao_monitor = 62;
}
message AutoConfirmSettings {
@ -2381,20 +2382,21 @@ message DaoStateStore {
message DaoStateHash {
int32 height = 1;
bytes hash = 2;
bytes prev_hash = 3;
bytes prev_hash = 3 [deprecated = true];
bool is_self_created = 4;
}
message ProposalStateHash {
int32 height = 1;
bytes hash = 2;
bytes prev_hash = 3;
bytes prev_hash = 3 [deprecated = true];
int32 num_proposals = 4;
}
message BlindVoteStateHash {
int32 height = 1;
bytes hash = 2;
bytes prev_hash = 3;
bytes prev_hash = 3 [deprecated = true];
int32 num_blind_votes = 4;
}