Merge pull request #2532 from ManfredKarrer/add-hash-of-dao-state

Add hash of dao state
This commit is contained in:
Manfred Karrer 2019-03-16 14:02:37 -05:00 committed by GitHub
commit 545eb8c4e7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
95 changed files with 5397 additions and 160 deletions

View file

@ -31,5 +31,6 @@ public enum Capability {
PROPOSAL,
BLIND_VOTE,
ACK_MSG,
BSQ_BLOCK
BSQ_BLOCK,
DAO_STATE
}

View file

@ -161,7 +161,7 @@ public class Utilities {
public static boolean isMacMenuBarDarkMode() {
try {
// check for exit status only. Once there are more modes than "dark" and "default", we might need to analyze string contents..
final Process process = Runtime.getRuntime().exec(new String[] {"defaults", "read", "-g", "AppleInterfaceStyle"});
Process process = Runtime.getRuntime().exec(new String[]{"defaults", "read", "-g", "AppleInterfaceStyle"});
process.waitFor(100, TimeUnit.MILLISECONDS);
return process.exitValue() == 0;
} catch (IOException | InterruptedException | IllegalThreadStateException ex) {
@ -512,15 +512,29 @@ public class Utilities {
throw new LimitedKeyStrengthException();
}
public static String toTruncatedString(Object message, int maxLength) {
if (message != null) {
return StringUtils.abbreviate(message.toString(), maxLength).replace("\n", "");
}
return "null";
public static String toTruncatedString(Object message) {
return toTruncatedString(message, 200, true);
}
public static String toTruncatedString(Object message) {
return toTruncatedString(message, 200);
public static String toTruncatedString(Object message, int maxLength) {
return toTruncatedString(message, maxLength, true);
}
public static String toTruncatedString(Object message, boolean removeLinebreaks) {
return toTruncatedString(message, 200, removeLinebreaks);
}
public static String toTruncatedString(Object message, int maxLength, boolean removeLinebreaks) {
if (message == null)
return "null";
String result = StringUtils.abbreviate(message.toString(), maxLength);
if (removeLinebreaks)
return result.replace("\n", "");
return result;
}
public static String getRandomPrefix(int minLength, int maxLength) {

View file

@ -57,6 +57,15 @@ message NetworkEnvelope {
AddPersistableNetworkPayloadMessage add_persistable_network_payload_message = 31;
AckMessage ack_message = 32;
RepublishGovernanceDataRequest republish_governance_data_request = 33;
NewDaoStateHashMessage new_dao_state_hash_message = 34;
GetDaoStateHashesRequest get_dao_state_hashes_request = 35;
GetDaoStateHashesResponse get_dao_state_hashes_response = 36;
NewProposalStateHashMessage new_proposal_state_hash_message = 37;
GetProposalStateHashesRequest get_proposal_state_hashes_request = 38;
GetProposalStateHashesResponse get_proposal_state_hashes_response = 39;
NewBlindVoteStateHashMessage new_blind_vote_state_hash_message = 40;
GetBlindVoteStateHashesRequest get_blind_vote_state_hashes_request = 41;
GetBlindVoteStateHashesResponse get_blind_vote_state_hashes_response = 42;
}
}
@ -324,6 +333,48 @@ message NewBlockBroadcastMessage {
message RepublishGovernanceDataRequest {
}
message NewDaoStateHashMessage {
DaoStateHash state_hash = 1;
}
message NewProposalStateHashMessage {
ProposalStateHash state_hash = 1;
}
message NewBlindVoteStateHashMessage {
BlindVoteStateHash state_hash = 1;
}
message GetDaoStateHashesRequest {
int32 height = 1;
int32 nonce = 2;
}
message GetProposalStateHashesRequest {
int32 height = 1;
int32 nonce = 2;
}
message GetBlindVoteStateHashesRequest {
int32 height = 1;
int32 nonce = 2;
}
message GetDaoStateHashesResponse {
repeated DaoStateHash state_hashes = 1;
int32 request_nonce = 2;
}
message GetProposalStateHashesResponse {
repeated ProposalStateHash state_hashes = 1;
int32 request_nonce = 2;
}
message GetBlindVoteStateHashesResponse {
repeated BlindVoteStateHash state_hashes = 1;
int32 request_nonce = 2;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Payload
///////////////////////////////////////////////////////////////////////////////////////////
@ -915,7 +966,6 @@ message AdvancedCashAccountPayload {
string account_nr = 1;
}
///////////////////////////////////////////////////////////////////////////////////////////
// PersistableEnvelope
///////////////////////////////////////////////////////////////////////////////////////////
@ -1713,6 +1763,27 @@ message DecryptedBallotsWithMerits {
message DaoStateStore {
BsqState bsq_state = 1;
repeated DaoStateHash dao_state_hash = 2;
}
message DaoStateHash {
int32 height = 1;
bytes hash = 2;
bytes prev_hash = 3;
}
message ProposalStateHash {
int32 height = 1;
bytes hash = 2;
bytes prev_hash = 3;
int32 num_proposals = 4;
}
message BlindVoteStateHash {
int32 height = 1;
bytes hash = 2;
bytes prev_hash = 3;
int32 num_blind_votes = 4;
}
///////////////////////////////////////////////////////////////////////////////////////////

View file

@ -56,9 +56,12 @@ public class BitcoinModule extends AppModule {
protected void configure() {
// We we have selected BTC_DAO_TESTNET we use our master regtest node, otherwise the specified host or default
// (localhost)
String regTestHost = BisqEnvironment.getBaseCurrencyNetwork().isDaoTestNet() ?
"104.248.31.39" :
environment.getProperty(BtcOptionKeys.REG_TEST_HOST, String.class, RegTestHost.DEFAULT_HOST);
String regTestHost = environment.getProperty(BtcOptionKeys.REG_TEST_HOST, String.class, "");
if (regTestHost.isEmpty()) {
regTestHost = BisqEnvironment.getBaseCurrencyNetwork().isDaoTestNet() ?
"104.248.31.39" :
RegTestHost.DEFAULT_HOST;
}
RegTestHost.HOST = regTestHost;
if (Arrays.asList("localhost", "127.0.0.1").contains(regTestHost)) {

View file

@ -0,0 +1,69 @@
/*
* 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

@ -64,6 +64,12 @@ import bisq.core.dao.governance.voteresult.MissingDataRequestService;
import bisq.core.dao.governance.voteresult.VoteResultService;
import bisq.core.dao.governance.voteresult.issuance.IssuanceService;
import bisq.core.dao.governance.votereveal.VoteRevealService;
import bisq.core.dao.monitoring.BlindVoteStateMonitoringService;
import bisq.core.dao.monitoring.DaoStateMonitoringService;
import bisq.core.dao.monitoring.ProposalStateMonitoringService;
import bisq.core.dao.monitoring.network.BlindVoteStateNetworkService;
import bisq.core.dao.monitoring.network.DaoStateNetworkService;
import bisq.core.dao.monitoring.network.ProposalStateNetworkService;
import bisq.core.dao.node.BsqNodeProvider;
import bisq.core.dao.node.explorer.ExportJsonFilesService;
import bisq.core.dao.node.full.FullNode;
@ -99,6 +105,7 @@ 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
@ -116,6 +123,12 @@ public class DaoModule extends AppModule {
bind(DaoStateService.class).in(Singleton.class);
bind(DaoStateSnapshotService.class).in(Singleton.class);
bind(DaoStateStorageService.class).in(Singleton.class);
bind(DaoStateMonitoringService.class).in(Singleton.class);
bind(DaoStateNetworkService.class).in(Singleton.class);
bind(ProposalStateMonitoringService.class).in(Singleton.class);
bind(ProposalStateNetworkService.class).in(Singleton.class);
bind(BlindVoteStateMonitoringService.class).in(Singleton.class);
bind(BlindVoteStateNetworkService.class).in(Singleton.class);
bind(UnconfirmedBsqChangeOutputListService.class).in(Singleton.class);
bind(ExportJsonFilesService.class).in(Singleton.class);

View file

@ -31,6 +31,9 @@ import bisq.core.dao.governance.proposal.ProposalService;
import bisq.core.dao.governance.voteresult.MissingDataRequestService;
import bisq.core.dao.governance.voteresult.VoteResultService;
import bisq.core.dao.governance.votereveal.VoteRevealService;
import bisq.core.dao.monitoring.BlindVoteStateMonitoringService;
import bisq.core.dao.monitoring.DaoStateMonitoringService;
import bisq.core.dao.monitoring.ProposalStateMonitoringService;
import bisq.core.dao.node.BsqNode;
import bisq.core.dao.node.BsqNodeProvider;
import bisq.core.dao.node.explorer.ExportJsonFilesService;
@ -69,11 +72,20 @@ public class DaoSetup {
ProofOfBurnService proofOfBurnService,
DaoFacade daoFacade,
ExportJsonFilesService exportJsonFilesService,
DaoKillSwitch daoKillSwitch) {
DaoKillSwitch daoKillSwitch,
DaoStateMonitoringService daoStateMonitoringService,
ProposalStateMonitoringService proposalStateMonitoringService,
BlindVoteStateMonitoringService blindVoteStateMonitoringService,
DaoEventCoordinator daoEventCoordinator) {
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);
@ -92,6 +104,10 @@ public class DaoSetup {
daoSetupServices.add(daoFacade);
daoSetupServices.add(exportJsonFilesService);
daoSetupServices.add(daoKillSwitch);
daoSetupServices.add(daoStateMonitoringService);
daoSetupServices.add(proposalStateMonitoringService);
daoSetupServices.add(blindVoteStateMonitoringService);
daoSetupServices.add(bsqNodeProvider.getBsqNode());
}

View file

@ -105,7 +105,7 @@ public class BlindVoteListService implements AppendOnlyDataStoreListener, DaoSta
@Override
public void onAdded(PersistableNetworkPayload payload) {
onAppendOnlyDataAdded(payload);
onAppendOnlyDataAdded(payload, true);
}
@ -120,16 +120,23 @@ public class BlindVoteListService implements AppendOnlyDataStoreListener, DaoSta
.collect(Collectors.toList());
}
public List<BlindVote> getConfirmedBlindVotes() {
return blindVotePayloads.stream()
.filter(blindVotePayload -> blindVoteValidator.areDataFieldsValidAndTxConfirmed(blindVotePayload.getBlindVote()))
.map(BlindVotePayload::getBlindVote)
.collect(Collectors.toList());
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////
private void fillListFromAppendOnlyDataStore() {
p2PService.getP2PDataStorage().getAppendOnlyDataStoreMap().values().forEach(this::onAppendOnlyDataAdded);
p2PService.getP2PDataStorage().getAppendOnlyDataStoreMap().values().forEach(e -> onAppendOnlyDataAdded(e, false));
}
private void onAppendOnlyDataAdded(PersistableNetworkPayload persistableNetworkPayload) {
private void onAppendOnlyDataAdded(PersistableNetworkPayload persistableNetworkPayload, boolean doLog) {
if (persistableNetworkPayload instanceof BlindVotePayload) {
BlindVotePayload blindVotePayload = (BlindVotePayload) persistableNetworkPayload;
if (!blindVotePayloads.contains(blindVotePayload)) {
@ -140,7 +147,9 @@ public class BlindVoteListService implements AppendOnlyDataStoreListener, DaoSta
if (blindVoteValidator.areDataFieldsValid(blindVote)) {
// We don't validate as we might receive blindVotes from other cycles or phases at startup.
blindVotePayloads.add(blindVotePayload);
log.info("We received a blindVotePayload. blindVoteTxId={}", txId);
if (doLog) {
log.info("We received a blindVotePayload. blindVoteTxId={}", txId);
}
} else {
log.warn("We received an invalid blindVotePayload. blindVoteTxId={}", txId);
}

View file

@ -47,7 +47,7 @@ public class MyBlindVoteList extends PersistableList<BlindVote> implements Conse
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
private MyBlindVoteList(List<BlindVote> list) {
public MyBlindVoteList(List<BlindVote> list) {
super(list);
}

View file

@ -145,6 +145,7 @@ public class MyBlindVoteListService implements PersistedDataHost, DaoStateListen
@Override
public void addListeners() {
daoStateService.addDaoStateListener(this);
p2PService.getNumConnectedPeers().addListener(numConnectedPeersListener);
}
@Override

View file

@ -79,7 +79,7 @@ public class CycleService implements DaoStateListener, DaoSetupService {
public void onNewBlockHeight(int blockHeight) {
if (blockHeight != genesisBlockHeight)
maybeCreateNewCycle(blockHeight, daoStateService.getCycles())
.ifPresent(daoStateService.getCycles()::add);
.ifPresent(daoStateService::addCycle);
}
@ -88,7 +88,7 @@ public class CycleService implements DaoStateListener, DaoSetupService {
///////////////////////////////////////////////////////////////////////////////////////////
public void addFirstCycle() {
daoStateService.getCycles().add(getFirstCycle());
daoStateService.addCycle(getFirstCycle());
}
public int getCycleIndex(Cycle cycle) {

View file

@ -85,7 +85,7 @@ public final class PeriodService {
.isPresent();
}
private Optional<Cycle> getCycle(int height) {
public Optional<Cycle> getCycle(int height) {
return daoStateService.getCycle(height);
}

View file

@ -31,12 +31,12 @@ import java.util.stream.Collectors;
import lombok.EqualsAndHashCode;
/**
* PersistableEnvelope wrapper for list of ballots. Used in vote consensus, so changes can break consensus!
* PersistableEnvelope wrapper for list of proposals. Used in vote consensus, so changes can break consensus!
*/
@EqualsAndHashCode(callSuper = true)
public class MyProposalList extends PersistableList<Proposal> implements ConsensusCritical {
private MyProposalList(List<Proposal> list) {
public MyProposalList(List<Proposal> list) {
super(list);
}

View file

@ -96,6 +96,7 @@ public class MyProposalListService implements PersistedDataHost, DaoStateListene
numConnectedPeersListener = (observable, oldValue, newValue) -> rePublishOnceWellConnected();
daoStateService.addDaoStateListener(this);
p2PService.getNumConnectedPeers().addListener(numConnectedPeersListener);
}
@ -130,9 +131,6 @@ public class MyProposalListService implements PersistedDataHost, DaoStateListene
// API
///////////////////////////////////////////////////////////////////////////////////////////
public void start() {
}
// Broadcast tx and publish proposal to P2P network
public void publishTxAndPayload(Proposal proposal, Transaction transaction, ResultHandler resultHandler,
ErrorMessageHandler errorMessageHandler) {

View file

@ -131,7 +131,7 @@ public class ProposalService implements HashMapChangedListener, AppendOnlyDataSt
@Override
public void onAdded(ProtectedStorageEntry entry) {
onProtectedDataAdded(entry);
onProtectedDataAdded(entry, true);
}
@Override
@ -146,7 +146,7 @@ public class ProposalService implements HashMapChangedListener, AppendOnlyDataSt
@Override
public void onAdded(PersistableNetworkPayload payload) {
onAppendOnlyDataAdded(payload);
onAppendOnlyDataAdded(payload, true);
}
@ -190,11 +190,11 @@ public class ProposalService implements HashMapChangedListener, AppendOnlyDataSt
///////////////////////////////////////////////////////////////////////////////////////////
private void fillListFromProtectedStore() {
p2PService.getDataMap().values().forEach(this::onProtectedDataAdded);
p2PService.getDataMap().values().forEach(e -> onProtectedDataAdded(e, false));
}
private void fillListFromAppendOnlyDataStore() {
p2PService.getP2PDataStorage().getAppendOnlyDataStoreMap().values().forEach(this::onAppendOnlyDataAdded);
p2PService.getP2PDataStorage().getAppendOnlyDataStoreMap().values().forEach(e -> onAppendOnlyDataAdded(e, false));
}
private void publishToAppendOnlyDataStore() {
@ -211,7 +211,7 @@ public class ProposalService implements HashMapChangedListener, AppendOnlyDataSt
});
}
private void onProtectedDataAdded(ProtectedStorageEntry entry) {
private void onProtectedDataAdded(ProtectedStorageEntry entry, boolean doLog) {
ProtectedStoragePayload protectedStoragePayload = entry.getProtectedStoragePayload();
if (protectedStoragePayload instanceof TempProposalPayload) {
Proposal proposal = ((TempProposalPayload) protectedStoragePayload).getProposal();
@ -219,8 +219,10 @@ public class ProposalService implements HashMapChangedListener, AppendOnlyDataSt
// available/confirmed. But we check if we are in the proposal phase.
if (!tempProposals.contains(proposal)) {
if (proposalValidator.isValidOrUnconfirmed(proposal)) {
log.info("We received a TempProposalPayload and store it to our protectedStoreList. proposalTxId={}",
proposal.getTxId());
if (doLog) {
log.info("We received a TempProposalPayload and store it to our protectedStoreList. proposalTxId={}",
proposal.getTxId());
}
tempProposals.add(proposal);
} else {
log.debug("We received an invalid proposal from the P2P network. Proposal.txId={}, blockHeight={}",
@ -256,14 +258,16 @@ public class ProposalService implements HashMapChangedListener, AppendOnlyDataSt
}
}
private void onAppendOnlyDataAdded(PersistableNetworkPayload persistableNetworkPayload) {
private void onAppendOnlyDataAdded(PersistableNetworkPayload persistableNetworkPayload, boolean doLog) {
if (persistableNetworkPayload instanceof ProposalPayload) {
ProposalPayload proposalPayload = (ProposalPayload) persistableNetworkPayload;
if (!proposalPayloads.contains(proposalPayload)) {
Proposal proposal = proposalPayload.getProposal();
if (proposalValidator.areDataFieldsValid(proposal)) {
log.info("We received a ProposalPayload and store it to our appendOnlyStoreList. proposalTxId={}",
proposal.getTxId());
if (doLog) {
log.info("We received a ProposalPayload and store it to our appendOnlyStoreList. proposalTxId={}",
proposal.getTxId());
}
proposalPayloads.add(proposalPayload);
} else {
log.warn("We received a invalid append-only proposal from the P2P network. " +

View file

@ -235,6 +235,7 @@ public class VoteResultService implements DaoStateListener, DaoSetupService {
}
// Those which did not get accepted will be added to the nonBsq map
// FIXME add check for cycle as now we call addNonBsqTxOutput for past rejected comp requests as well
daoStateService.getIssuanceCandidateTxOutputs().stream()
.filter(txOutput -> !daoStateService.isIssuanceTx(txOutput.getTxId()))
.forEach(daoStateService::addNonBsqTxOutput);
@ -304,6 +305,8 @@ public class VoteResultService implements DaoStateListener, DaoSetupService {
return getDecryptedBallotsWithMerits(voteRevealTxId, currentCycle, voteRevealOpReturnData,
blindVoteTxId, hashOfBlindVoteList, blindVoteStake, optionalBlindVote.get());
}
// We are missing P2P network data
return getEmptyDecryptedBallotsWithMerits(voteRevealTxId, blindVoteTxId, hashOfBlindVoteList,
blindVoteStake);
} catch (Throwable e) {

View file

@ -0,0 +1,317 @@
/*
* 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.monitoring;
import bisq.core.dao.DaoSetupService;
import bisq.core.dao.governance.blindvote.BlindVote;
import bisq.core.dao.governance.blindvote.BlindVoteListService;
import bisq.core.dao.governance.blindvote.MyBlindVoteList;
import bisq.core.dao.governance.period.PeriodService;
import bisq.core.dao.monitoring.model.BlindVoteStateBlock;
import bisq.core.dao.monitoring.model.BlindVoteStateHash;
import bisq.core.dao.monitoring.network.BlindVoteStateNetworkService;
import bisq.core.dao.monitoring.network.messages.GetBlindVoteStateHashesRequest;
import bisq.core.dao.monitoring.network.messages.NewBlindVoteStateHashMessage;
import bisq.core.dao.state.DaoStateListener;
import bisq.core.dao.state.DaoStateService;
import bisq.core.dao.state.GenesisTxInfo;
import bisq.core.dao.state.model.blockchain.Block;
import bisq.core.dao.state.model.governance.Cycle;
import bisq.core.dao.state.model.governance.DaoPhase;
import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.network.Connection;
import bisq.common.UserThread;
import bisq.common.crypto.Hash;
import javax.inject.Inject;
import org.apache.commons.lang3.ArrayUtils;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.Random;
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.checkNotNull;
/**
* Monitors the BlindVote P2P network payloads with using a hash of a sorted list of BlindVotes from one cycle and
* make it accessible to the network so we can detect quickly if any consensus issue arise.
* We create that hash at the first block of the VoteReveal phase. There is one hash created per cycle.
* The hash contains the hash of the previous block so we can ensure the validity of the whole history by
* comparing the last block.
*
* We request the state from the connected seed nodes after batch processing of BSQ is complete as well as we start
* to listen for broadcast messages from our peers about dao state of new blocks.
*
* We do NOT persist that chain of hashes as there is only one per cycle and the performance costs are very low.
*/
@Slf4j
public class BlindVoteStateMonitoringService implements DaoSetupService, DaoStateListener, BlindVoteStateNetworkService.Listener<NewBlindVoteStateHashMessage, GetBlindVoteStateHashesRequest, BlindVoteStateHash> {
public interface Listener {
void onBlindVoteStateBlockChainChanged();
}
private final DaoStateService daoStateService;
private final BlindVoteStateNetworkService blindVoteStateNetworkService;
private final GenesisTxInfo genesisTxInfo;
private final PeriodService periodService;
private final BlindVoteListService blindVoteListService;
@Getter
private final LinkedList<BlindVoteStateBlock> blindVoteStateBlockChain = new LinkedList<>();
@Getter
private final LinkedList<BlindVoteStateHash> blindVoteStateHashChain = new LinkedList<>();
private final List<Listener> listeners = new CopyOnWriteArrayList<>();
@Getter
private boolean isInConflict;
private boolean parseBlockChainComplete;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
public BlindVoteStateMonitoringService(DaoStateService daoStateService,
BlindVoteStateNetworkService blindVoteStateNetworkService,
GenesisTxInfo genesisTxInfo,
PeriodService periodService,
BlindVoteListService blindVoteListService) {
this.daoStateService = daoStateService;
this.blindVoteStateNetworkService = blindVoteStateNetworkService;
this.genesisTxInfo = genesisTxInfo;
this.periodService = periodService;
this.blindVoteListService = blindVoteListService;
}
///////////////////////////////////////////////////////////////////////////////////////////
// DaoSetupService
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void addListeners() {
daoStateService.addDaoStateListener(this);
blindVoteStateNetworkService.addListener(this);
}
@Override
public void start() {
}
///////////////////////////////////////////////////////////////////////////////////////////
// DaoStateListener
///////////////////////////////////////////////////////////////////////////////////////////
@SuppressWarnings("Duplicates")
@Override
public void onDaoStateChanged(Block block) {
int blockHeight = block.getHeight();
int genesisBlockHeight = genesisTxInfo.getGenesisBlockHeight();
if (blindVoteStateBlockChain.isEmpty() && blockHeight > genesisBlockHeight) {
// Takes about 150 ms for dao testnet data
long ts = System.currentTimeMillis();
for (int i = genesisBlockHeight; i < blockHeight; i++) {
maybeUpdateHashChain(i);
}
log.info("updateHashChain for {} items took {} ms",
blockHeight - genesisBlockHeight,
System.currentTimeMillis() - ts);
}
maybeUpdateHashChain(blockHeight);
}
@SuppressWarnings("Duplicates")
@Override
public void onParseBlockChainComplete() {
parseBlockChainComplete = true;
blindVoteStateNetworkService.addListeners();
// We wait for processing messages until we have completed batch processing
// We request data from last 5 cycles. We ignore possible duration changes done by voting as that request
// period is arbitrary anyway...
Cycle currentCycle = periodService.getCurrentCycle();
checkNotNull(currentCycle, "currentCycle must not be null");
int fromHeight = Math.max(genesisTxInfo.getGenesisBlockHeight(), daoStateService.getChainHeight() - currentCycle.getDuration() * 5);
blindVoteStateNetworkService.requestHashesFromAllConnectedSeedNodes(fromHeight);
}
///////////////////////////////////////////////////////////////////////////////////////////
// StateNetworkService.Listener
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void onNewStateHashMessage(NewBlindVoteStateHashMessage newStateHashMessage, Connection connection) {
if (newStateHashMessage.getStateHash().getHeight() <= daoStateService.getChainHeight()) {
processPeersBlindVoteStateHash(newStateHashMessage.getStateHash(), connection.getPeersNodeAddressOptional(), true);
}
}
@Override
public void onGetStateHashRequest(Connection connection, GetBlindVoteStateHashesRequest getStateHashRequest) {
int fromHeight = getStateHashRequest.getHeight();
List<BlindVoteStateHash> blindVoteStateHashes = blindVoteStateBlockChain.stream()
.filter(e -> e.getHeight() >= fromHeight)
.map(BlindVoteStateBlock::getMyStateHash)
.collect(Collectors.toList());
blindVoteStateNetworkService.sendGetStateHashesResponse(connection, getStateHashRequest.getNonce(), blindVoteStateHashes);
}
@Override
public void onPeersStateHashes(List<BlindVoteStateHash> stateHashes, Optional<NodeAddress> peersNodeAddress) {
AtomicBoolean hasChanged = new AtomicBoolean(false);
stateHashes.forEach(daoStateHash -> {
boolean changed = processPeersBlindVoteStateHash(daoStateHash, peersNodeAddress, false);
if (changed) {
hasChanged.set(true);
}
});
if (hasChanged.get()) {
listeners.forEach(Listener::onBlindVoteStateBlockChainChanged);
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
public void requestHashesFromGenesisBlockHeight(String peersAddress) {
blindVoteStateNetworkService.requestHashes(genesisTxInfo.getGenesisBlockHeight(), peersAddress);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Listeners
///////////////////////////////////////////////////////////////////////////////////////////
public void addListener(Listener listener) {
listeners.add(listener);
}
public void removeListener(Listener listener) {
listeners.remove(listener);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////
private void maybeUpdateHashChain(int blockHeight) {
// We use first block in blind vote phase to create the hash of our blindVotes. We prefer to wait as long as
// possible to increase the chance that we have received all blindVotes.
if (!isFirstBlockOfBlindVotePhase(blockHeight)) {
return;
}
periodService.getCycle(blockHeight).ifPresent(cycle -> {
List<BlindVote> blindVotes = blindVoteListService.getConfirmedBlindVotes().stream()
.filter(e -> periodService.isTxInCorrectCycle(e.getTxId(), blockHeight))
.sorted(Comparator.comparing(BlindVote::getTxId)).collect(Collectors.toList());
// We use MyBlindVoteList to get the serialized bytes from the blindVotes list
byte[] serializedBlindVotes = new MyBlindVoteList(blindVotes).toProtoMessage().toByteArray();
byte[] prevHash;
if (blindVoteStateBlockChain.isEmpty()) {
prevHash = new byte[0];
} else {
prevHash = blindVoteStateBlockChain.getLast().getHash();
}
byte[] combined = ArrayUtils.addAll(prevHash, serializedBlindVotes);
byte[] hash = Hash.getSha256Ripemd160hash(combined);
BlindVoteStateHash myBlindVoteStateHash = new BlindVoteStateHash(blockHeight, hash, prevHash, blindVotes.size());
BlindVoteStateBlock blindVoteStateBlock = new BlindVoteStateBlock(myBlindVoteStateHash);
blindVoteStateBlockChain.add(blindVoteStateBlock);
blindVoteStateHashChain.add(myBlindVoteStateHash);
// 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::onBlindVoteStateBlockChainChanged);
// 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);
UserThread.runAfter(() -> blindVoteStateNetworkService.broadcastMyStateHash(myBlindVoteStateHash), delayInSec);
}
});
}
private boolean processPeersBlindVoteStateHash(BlindVoteStateHash blindVoteStateHash, Optional<NodeAddress> peersNodeAddress, boolean notifyListeners) {
AtomicBoolean changed = new AtomicBoolean(false);
AtomicBoolean isInConflict = new AtomicBoolean(this.isInConflict);
StringBuilder sb = new StringBuilder();
blindVoteStateBlockChain.stream()
.filter(e -> e.getHeight() == blindVoteStateHash.getHeight()).findAny()
.ifPresent(daoStateBlock -> {
String peersNodeAddressAsString = peersNodeAddress.map(NodeAddress::getFullAddress)
.orElseGet(() -> "Unknown peer " + new Random().nextInt(10000));
daoStateBlock.putInPeersMap(peersNodeAddressAsString, blindVoteStateHash);
if (!daoStateBlock.getMyStateHash().hasEqualHash(blindVoteStateHash)) {
daoStateBlock.putInConflictMap(peersNodeAddressAsString, blindVoteStateHash);
isInConflict.set(true);
sb.append("We received a block hash from peer ")
.append(peersNodeAddressAsString)
.append(" which conflicts with our block hash.\n")
.append("my blindVoteStateHash=")
.append(daoStateBlock.getMyStateHash())
.append("\npeers blindVoteStateHash=")
.append(blindVoteStateHash);
}
changed.set(true);
});
this.isInConflict = isInConflict.get();
String conflictMsg = sb.toString();
if (this.isInConflict && !conflictMsg.isEmpty()) {
log.warn(conflictMsg);
}
if (notifyListeners && changed.get()) {
listeners.forEach(Listener::onBlindVoteStateBlockChainChanged);
}
return changed.get();
}
private boolean isFirstBlockOfBlindVotePhase(int blockHeight) {
return blockHeight == periodService.getFirstBlockOfPhase(blockHeight, DaoPhase.Phase.VOTE_REVEAL);
}
}

View file

@ -0,0 +1,298 @@
/*
* 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.monitoring;
import bisq.core.dao.DaoSetupService;
import bisq.core.dao.monitoring.model.DaoStateBlock;
import bisq.core.dao.monitoring.model.DaoStateHash;
import bisq.core.dao.monitoring.network.DaoStateNetworkService;
import bisq.core.dao.monitoring.network.messages.GetDaoStateHashesRequest;
import bisq.core.dao.monitoring.network.messages.NewDaoStateHashMessage;
import bisq.core.dao.state.DaoStateListener;
import bisq.core.dao.state.DaoStateService;
import bisq.core.dao.state.GenesisTxInfo;
import bisq.core.dao.state.model.blockchain.Block;
import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.network.Connection;
import bisq.common.UserThread;
import bisq.common.crypto.Hash;
import javax.inject.Inject;
import org.apache.commons.lang3.ArrayUtils;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.Random;
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;
/**
* Monitors the DaoState with using a hash fo the complete daoState and make it accessible to the network
* so we can detect quickly if any consensus issue arise.
* We create that hash after parsing and processing of a block is completed. There is one hash created per block.
* The hash contains the hash of the previous block so we can ensure the validity of the whole history by
* comparing the last block.
*
* We request the state from the connected seed nodes after batch processing of BSQ is complete as well as we start
* to listen for broadcast messages from our peers about dao state of new blocks. It could be that the received dao
* state from the peers is already covering the next block we have not received yet. So we only take data in account
* which are inside the block height we have already. To avoid such race conditions we delay the broadcasting of our
* state to the peers to not get ignored it in case they have not received the block yet.
*
* We do persist that chain of hashes with the snapshot.
*/
@Slf4j
public class DaoStateMonitoringService implements DaoSetupService, DaoStateListener,
DaoStateNetworkService.Listener<NewDaoStateHashMessage, GetDaoStateHashesRequest, DaoStateHash> {
public interface Listener {
void onChangeAfterBatchProcessing();
}
private final DaoStateService daoStateService;
private final DaoStateNetworkService daoStateNetworkService;
private final GenesisTxInfo genesisTxInfo;
@Getter
private final LinkedList<DaoStateBlock> daoStateBlockChain = new LinkedList<>();
@Getter
private final LinkedList<DaoStateHash> daoStateHashChain = new LinkedList<>();
private final List<Listener> listeners = new CopyOnWriteArrayList<>();
private boolean parseBlockChainComplete;
@Getter
private boolean isInConflict;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
public DaoStateMonitoringService(DaoStateService daoStateService,
DaoStateNetworkService daoStateNetworkService,
GenesisTxInfo genesisTxInfo) {
this.daoStateService = daoStateService;
this.daoStateNetworkService = daoStateNetworkService;
this.genesisTxInfo = genesisTxInfo;
}
///////////////////////////////////////////////////////////////////////////////////////////
// DaoSetupService
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void addListeners() {
daoStateService.addDaoStateListener(this);
daoStateNetworkService.addListener(this);
}
@Override
public void start() {
}
///////////////////////////////////////////////////////////////////////////////////////////
// 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;
daoStateNetworkService.addListeners();
// We wait for processing messages until we have completed batch processing
int fromHeight = daoStateService.getChainHeight() - 10;
daoStateNetworkService.requestHashesFromAllConnectedSeedNodes(fromHeight);
}
///////////////////////////////////////////////////////////////////////////////////////////
// StateNetworkService.Listener
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void onNewStateHashMessage(NewDaoStateHashMessage newStateHashMessage, Connection connection) {
if (newStateHashMessage.getStateHash().getHeight() <= daoStateService.getChainHeight()) {
processPeersDaoStateHash(newStateHashMessage.getStateHash(), connection.getPeersNodeAddressOptional(), true);
}
}
@Override
public void onGetStateHashRequest(Connection connection, GetDaoStateHashesRequest getStateHashRequest) {
int fromHeight = getStateHashRequest.getHeight();
List<DaoStateHash> daoStateHashes = daoStateBlockChain.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);
}
public void requestHashesFromGenesisBlockHeight(String peersAddress) {
daoStateNetworkService.requestHashes(genesisTxInfo.getGenesisBlockHeight(), peersAddress);
}
public void applySnapshot(LinkedList<DaoStateHash> persistedDaoStateHashChain) {
// We could got a reset from a reorg, so we clear all and start over from the genesis block.
daoStateHashChain.clear();
daoStateBlockChain.clear();
daoStateNetworkService.reset();
if (!persistedDaoStateHashChain.isEmpty()) {
log.info("Apply snapshot with {} daoStateHashes. Last daoStateHash={}",
persistedDaoStateHashChain.size(), persistedDaoStateHashChain.getLast());
}
daoStateHashChain.addAll(persistedDaoStateHashChain);
daoStateHashChain.forEach(e -> daoStateBlockChain.add(new DaoStateBlock(e)));
}
///////////////////////////////////////////////////////////////////////////////////////////
// Listeners
///////////////////////////////////////////////////////////////////////////////////////////
public void addListener(Listener listener) {
listeners.add(listener);
}
public void removeListener(Listener listener) {
listeners.remove(listener);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////
private void updateHashChain(Block block) {
byte[] prevHash;
int height = block.getHeight();
if (daoStateBlockChain.isEmpty()) {
// Only at genesis we allow an empty prevHash
if (height == genesisTxInfo.getGenesisBlockHeight()) {
prevHash = new byte[0];
} else {
log.warn("DaoStateBlockchain is empty but we received the block which was not the genesis block. " +
"We stop execution here.");
return;
}
} 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();
}
byte[] stateHash = daoStateService.getSerializedDaoState();
// 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, stateHash);
byte[] hash = Hash.getSha256Ripemd160hash(combined);
DaoStateHash myDaoStateHash = new DaoStateHash(height, hash, prevHash);
DaoStateBlock daoStateBlock = new DaoStateBlock(myDaoStateHash);
daoStateBlockChain.add(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);
UserThread.runAfter(() -> daoStateNetworkService.broadcastMyStateHash(myDaoStateHash), delayInSec);
}
}
private boolean processPeersDaoStateHash(DaoStateHash daoStateHash, Optional<NodeAddress> peersNodeAddress, boolean notifyListeners) {
AtomicBoolean changed = new AtomicBoolean(false);
AtomicBoolean isInConflict = new AtomicBoolean(this.isInConflict);
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);
isInConflict.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);
});
this.isInConflict = isInConflict.get();
String conflictMsg = sb.toString();
if (this.isInConflict && !conflictMsg.isEmpty()) {
log.warn(conflictMsg);
}
if (notifyListeners && changed.get()) {
listeners.forEach(Listener::onChangeAfterBatchProcessing);
}
return changed.get();
}
}

View file

@ -0,0 +1,313 @@
/*
* 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.monitoring;
import bisq.core.dao.DaoSetupService;
import bisq.core.dao.governance.period.PeriodService;
import bisq.core.dao.governance.proposal.MyProposalList;
import bisq.core.dao.governance.proposal.ProposalService;
import bisq.core.dao.monitoring.model.ProposalStateBlock;
import bisq.core.dao.monitoring.model.ProposalStateHash;
import bisq.core.dao.monitoring.network.ProposalStateNetworkService;
import bisq.core.dao.monitoring.network.messages.GetProposalStateHashesRequest;
import bisq.core.dao.monitoring.network.messages.NewProposalStateHashMessage;
import bisq.core.dao.state.DaoStateListener;
import bisq.core.dao.state.DaoStateService;
import bisq.core.dao.state.GenesisTxInfo;
import bisq.core.dao.state.model.blockchain.Block;
import bisq.core.dao.state.model.governance.Cycle;
import bisq.core.dao.state.model.governance.DaoPhase;
import bisq.core.dao.state.model.governance.Proposal;
import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.network.Connection;
import bisq.common.UserThread;
import bisq.common.crypto.Hash;
import javax.inject.Inject;
import org.apache.commons.lang3.ArrayUtils;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.Random;
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.checkNotNull;
/**
* Monitors the Proposal P2P network payloads with using a hash of a sorted list of Proposals from one cycle and
* make it accessible to the network so we can detect quickly if any consensus issue arise.
* We create that hash at the first block of the BlindVote phase. There is one hash created per cycle.
* The hash contains the hash of the previous block so we can ensure the validity of the whole history by
* comparing the last block.
*
* We request the state from the connected seed nodes after batch processing of BSQ is complete as well as we start
* to listen for broadcast messages from our peers about dao state of new blocks.
*
* We do NOT persist that chain of hashes as there is only one per cycle and the performance costs are very low.
*/
@Slf4j
public class ProposalStateMonitoringService implements DaoSetupService, DaoStateListener, ProposalStateNetworkService.Listener<NewProposalStateHashMessage, GetProposalStateHashesRequest, ProposalStateHash> {
public interface Listener {
void onProposalStateBlockChainChanged();
}
private final DaoStateService daoStateService;
private final ProposalStateNetworkService proposalStateNetworkService;
private final GenesisTxInfo genesisTxInfo;
private final PeriodService periodService;
private final ProposalService proposalService;
@Getter
private final LinkedList<ProposalStateBlock> proposalStateBlockChain = new LinkedList<>();
@Getter
private final LinkedList<ProposalStateHash> proposalStateHashChain = new LinkedList<>();
private final List<Listener> listeners = new CopyOnWriteArrayList<>();
@Getter
private boolean isInConflict;
private boolean parseBlockChainComplete;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
public ProposalStateMonitoringService(DaoStateService daoStateService,
ProposalStateNetworkService proposalStateNetworkService,
GenesisTxInfo genesisTxInfo,
PeriodService periodService,
ProposalService proposalService) {
this.daoStateService = daoStateService;
this.proposalStateNetworkService = proposalStateNetworkService;
this.genesisTxInfo = genesisTxInfo;
this.periodService = periodService;
this.proposalService = proposalService;
}
///////////////////////////////////////////////////////////////////////////////////////////
// DaoSetupService
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void addListeners() {
daoStateService.addDaoStateListener(this);
proposalStateNetworkService.addListener(this);
}
@Override
public void start() {
}
///////////////////////////////////////////////////////////////////////////////////////////
// DaoStateListener
///////////////////////////////////////////////////////////////////////////////////////////
@SuppressWarnings("Duplicates")
public void onDaoStateChanged(Block block) {
int blockHeight = block.getHeight();
int genesisBlockHeight = genesisTxInfo.getGenesisBlockHeight();
if (proposalStateBlockChain.isEmpty() && blockHeight > genesisBlockHeight) {
// Takes about 150 ms for dao testnet data
long ts = System.currentTimeMillis();
for (int i = genesisBlockHeight; i < blockHeight; i++) {
maybeUpdateHashChain(i);
}
log.info("updateHashChain for {} items took {} ms",
blockHeight - genesisBlockHeight,
System.currentTimeMillis() - ts);
}
maybeUpdateHashChain(blockHeight);
}
@SuppressWarnings("Duplicates")
@Override
public void onParseBlockChainComplete() {
parseBlockChainComplete = true;
proposalStateNetworkService.addListeners();
// We wait for processing messages until we have completed batch processing
// We request data from last 5 cycles. We ignore possible duration changes done by voting as that request
// period is arbitrary anyway...
Cycle currentCycle = periodService.getCurrentCycle();
checkNotNull(currentCycle, "currentCycle must not be null");
int fromHeight = Math.max(genesisTxInfo.getGenesisBlockHeight(), daoStateService.getChainHeight() - currentCycle.getDuration() * 5);
proposalStateNetworkService.requestHashesFromAllConnectedSeedNodes(fromHeight);
}
///////////////////////////////////////////////////////////////////////////////////////////
// StateNetworkService.Listener
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void onNewStateHashMessage(NewProposalStateHashMessage newStateHashMessage, Connection connection) {
if (newStateHashMessage.getStateHash().getHeight() <= daoStateService.getChainHeight()) {
processPeersProposalStateHash(newStateHashMessage.getStateHash(), connection.getPeersNodeAddressOptional(), true);
}
}
@Override
public void onGetStateHashRequest(Connection connection, GetProposalStateHashesRequest getStateHashRequest) {
int fromHeight = getStateHashRequest.getHeight();
List<ProposalStateHash> proposalStateHashes = proposalStateBlockChain.stream()
.filter(e -> e.getHeight() >= fromHeight)
.map(ProposalStateBlock::getMyStateHash)
.collect(Collectors.toList());
proposalStateNetworkService.sendGetStateHashesResponse(connection, getStateHashRequest.getNonce(), proposalStateHashes);
}
@Override
public void onPeersStateHashes(List<ProposalStateHash> stateHashes, Optional<NodeAddress> peersNodeAddress) {
AtomicBoolean hasChanged = new AtomicBoolean(false);
stateHashes.forEach(daoStateHash -> {
boolean changed = processPeersProposalStateHash(daoStateHash, peersNodeAddress, false);
if (changed) {
hasChanged.set(true);
}
});
if (hasChanged.get()) {
listeners.forEach(Listener::onProposalStateBlockChainChanged);
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
public void requestHashesFromGenesisBlockHeight(String peersAddress) {
proposalStateNetworkService.requestHashes(genesisTxInfo.getGenesisBlockHeight(), peersAddress);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Listeners
///////////////////////////////////////////////////////////////////////////////////////////
public void addListener(Listener listener) {
listeners.add(listener);
}
public void removeListener(Listener listener) {
listeners.remove(listener);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////
private void maybeUpdateHashChain(int blockHeight) {
// We use first block in blind vote phase to create the hash of our proposals. We prefer to wait as long as
// possible to increase the chance that we have received all proposals.
if (!isFirstBlockOfBlindVotePhase(blockHeight)) {
return;
}
periodService.getCycle(blockHeight).ifPresent(cycle -> {
List<Proposal> proposals = proposalService.getValidatedProposals().stream()
.filter(e -> periodService.isTxInPhaseAndCycle(e.getTxId(), DaoPhase.Phase.PROPOSAL, blockHeight))
.sorted(Comparator.comparing(Proposal::getTxId)).collect(Collectors.toList());
// We use MyProposalList to get the serialized bytes from the proposals list
byte[] serializedProposals = new MyProposalList(proposals).toProtoMessage().toByteArray();
byte[] prevHash;
if (proposalStateBlockChain.isEmpty()) {
prevHash = new byte[0];
} else {
prevHash = proposalStateBlockChain.getLast().getHash();
}
byte[] combined = ArrayUtils.addAll(prevHash, serializedProposals);
byte[] hash = Hash.getSha256Ripemd160hash(combined);
ProposalStateHash myProposalStateHash = new ProposalStateHash(blockHeight, hash, prevHash, proposals.size());
ProposalStateBlock proposalStateBlock = new ProposalStateBlock(myProposalStateHash);
proposalStateBlockChain.add(proposalStateBlock);
proposalStateHashChain.add(myProposalStateHash);
// 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::onProposalStateBlockChainChanged);
// 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);
UserThread.runAfter(() -> proposalStateNetworkService.broadcastMyStateHash(myProposalStateHash), delayInSec);
}
});
}
private boolean processPeersProposalStateHash(ProposalStateHash proposalStateHash, Optional<NodeAddress> peersNodeAddress, boolean notifyListeners) {
AtomicBoolean changed = new AtomicBoolean(false);
AtomicBoolean isInConflict = new AtomicBoolean(this.isInConflict);
StringBuilder sb = new StringBuilder();
proposalStateBlockChain.stream()
.filter(e -> e.getHeight() == proposalStateHash.getHeight()).findAny()
.ifPresent(daoStateBlock -> {
String peersNodeAddressAsString = peersNodeAddress.map(NodeAddress::getFullAddress)
.orElseGet(() -> "Unknown peer " + new Random().nextInt(10000));
daoStateBlock.putInPeersMap(peersNodeAddressAsString, proposalStateHash);
if (!daoStateBlock.getMyStateHash().hasEqualHash(proposalStateHash)) {
daoStateBlock.putInConflictMap(peersNodeAddressAsString, proposalStateHash);
isInConflict.set(true);
sb.append("We received a block hash from peer ")
.append(peersNodeAddressAsString)
.append(" which conflicts with our block hash.\n")
.append("my proposalStateHash=")
.append(daoStateBlock.getMyStateHash())
.append("\npeers proposalStateHash=")
.append(proposalStateHash);
}
changed.set(true);
});
this.isInConflict = isInConflict.get();
String conflictMsg = sb.toString();
if (this.isInConflict && !conflictMsg.isEmpty()) {
log.warn(conflictMsg);
}
if (notifyListeners && changed.get()) {
listeners.forEach(Listener::onProposalStateBlockChainChanged);
}
return changed.get();
}
private boolean isFirstBlockOfBlindVotePhase(int blockHeight) {
return blockHeight == periodService.getFirstBlockOfPhase(blockHeight, DaoPhase.Phase.BLIND_VOTE);
}
}

View file

@ -0,0 +1,33 @@
/*
* 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.monitoring.model;
import lombok.EqualsAndHashCode;
import lombok.Getter;
@Getter
@EqualsAndHashCode(callSuper = true)
public class BlindVoteStateBlock extends StateBlock<BlindVoteStateHash> {
public BlindVoteStateBlock(BlindVoteStateHash myBlindVoteStateHash) {
super(myBlindVoteStateHash);
}
public int getNumBlindVotes() {
return myStateHash.getNumBlindVotes();
}
}

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.core.dao.monitoring.model;
import io.bisq.generated.protobuffer.PB;
import com.google.protobuf.ByteString;
import lombok.EqualsAndHashCode;
import lombok.Getter;
@EqualsAndHashCode(callSuper = true)
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);
this.numBlindVotes = numBlindVotes;
}
///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public PB.BlindVoteStateHash toProtoMessage() {
return PB.BlindVoteStateHash.newBuilder()
.setHeight(height)
.setHash(ByteString.copyFrom(hash))
.setPrevHash(ByteString.copyFrom(prevHash))
.setNumBlindVotes(numBlindVotes).build();
}
public static BlindVoteStateHash fromProto(PB.BlindVoteStateHash proto) {
return new BlindVoteStateHash(proto.getHeight(),
proto.getHash().toByteArray(),
proto.getPrevHash().toByteArray(),
proto.getNumBlindVotes());
}
@Override
public String toString() {
return "BlindVoteStateHash{" +
"\n numBlindVotes=" + numBlindVotes +
"\n} " + super.toString();
}
}

View file

@ -0,0 +1,29 @@
/*
* 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.monitoring.model;
import lombok.EqualsAndHashCode;
import lombok.Getter;
@Getter
@EqualsAndHashCode(callSuper = true)
public class DaoStateBlock extends StateBlock<DaoStateHash> {
public DaoStateBlock(DaoStateHash myDaoStateHash) {
super(myDaoStateHash);
}
}

View file

@ -0,0 +1,50 @@
/*
* 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.monitoring.model;
import io.bisq.generated.protobuffer.PB;
import com.google.protobuf.ByteString;
import lombok.EqualsAndHashCode;
@EqualsAndHashCode(callSuper = true)
public final class DaoStateHash extends StateHash {
public DaoStateHash(int height, byte[] hash, byte[] prevHash) {
super(height, hash, prevHash);
}
///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public PB.DaoStateHash toProtoMessage() {
return PB.DaoStateHash.newBuilder()
.setHeight(height)
.setHash(ByteString.copyFrom(hash))
.setPrevHash(ByteString.copyFrom(prevHash)).build();
}
public static DaoStateHash fromProto(PB.DaoStateHash proto) {
return new DaoStateHash(proto.getHeight(),
proto.getHash().toByteArray(),
proto.getPrevHash().toByteArray());
}
}

View file

@ -0,0 +1,33 @@
/*
* 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.monitoring.model;
import lombok.EqualsAndHashCode;
import lombok.Getter;
@Getter
@EqualsAndHashCode(callSuper = true)
public class ProposalStateBlock extends StateBlock<ProposalStateHash> {
public ProposalStateBlock(ProposalStateHash myProposalStateHash) {
super(myProposalStateHash);
}
public int getNumProposals() {
return myStateHash.getNumProposals();
}
}

View file

@ -0,0 +1,66 @@
/*
* 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.monitoring.model;
import io.bisq.generated.protobuffer.PB;
import com.google.protobuf.ByteString;
import lombok.EqualsAndHashCode;
import lombok.Getter;
@EqualsAndHashCode(callSuper = true)
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);
this.numProposals = numProposals;
}
///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public PB.ProposalStateHash toProtoMessage() {
return PB.ProposalStateHash.newBuilder()
.setHeight(height)
.setHash(ByteString.copyFrom(hash))
.setPrevHash(ByteString.copyFrom(prevHash))
.setNumProposals(numProposals).build();
}
public static ProposalStateHash fromProto(PB.ProposalStateHash proto) {
return new ProposalStateHash(proto.getHeight(),
proto.getHash().toByteArray(),
proto.getPrevHash().toByteArray(),
proto.getNumProposals());
}
@Override
public String toString() {
return "ProposalStateHash{" +
"\n numProposals=" + numProposals +
"\n} " + super.toString();
}
}

View file

@ -0,0 +1,71 @@
/*
* 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.monitoring.model;
import java.util.HashMap;
import java.util.Map;
import lombok.EqualsAndHashCode;
import lombok.Getter;
/**
* Contains my StateHash at a particular block height and the received stateHash from our peers.
* The maps get updated over time, this is not an immutable class.
*/
@Getter
@EqualsAndHashCode
public abstract class StateBlock<T extends StateHash> {
protected final T myStateHash;
private final Map<String, T> peersMap = new HashMap<>();
private final Map<String, T> inConflictMap = new HashMap<>();
StateBlock(T myStateHash) {
this.myStateHash = myStateHash;
}
public void putInPeersMap(String peersNodeAddress, T stateHash) {
peersMap.putIfAbsent(peersNodeAddress, stateHash);
}
public void putInConflictMap(String peersNodeAddress, T stateHash) {
inConflictMap.putIfAbsent(peersNodeAddress, stateHash);
}
// Delegates
public int getHeight() {
return myStateHash.getHeight();
}
public byte[] getHash() {
return myStateHash.getHash();
}
public byte[] getPrevHash() {
return myStateHash.getPrevHash();
}
@Override
public String toString() {
return "StateBlock{" +
"\n myStateHash=" + myStateHash +
",\n peersMap=" + peersMap +
",\n inConflictMap=" + inConflictMap +
"\n}";
}
}

View file

@ -0,0 +1,73 @@
/*
* 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.monitoring.model;
import bisq.common.proto.network.NetworkPayload;
import bisq.common.proto.persistable.PersistablePayload;
import bisq.common.util.Utilities;
import java.util.Arrays;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
/**
* Contains the blockHeight, the hash and the previous hash of the state.
* As the hash is created from the state at the particular height including the previous hash we get the history of
* the full chain included and we know if the hash matches at a particular height that all the past blocks need to match
* as well.
*/
@EqualsAndHashCode
@Getter
@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) {
this.height = height;
this.hash = hash;
this.prevHash = prevHash;
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public boolean hasEqualHash(StateHash other) {
return Arrays.equals(hash, other.getHash());
}
public byte[] getHash() {
return hash;
}
@Override
public String toString() {
return "StateHash{" +
"\n height=" + height +
",\n hash=" + Utilities.bytesAsHexString(hash) +
",\n prevHash=" + Utilities.bytesAsHexString(prevHash) +
"\n}";
}
}

View file

@ -0,0 +1,85 @@
/*
* 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.monitoring.network;
import bisq.core.dao.monitoring.model.BlindVoteStateHash;
import bisq.core.dao.monitoring.network.messages.GetBlindVoteStateHashesRequest;
import bisq.core.dao.monitoring.network.messages.GetBlindVoteStateHashesResponse;
import bisq.core.dao.monitoring.network.messages.NewBlindVoteStateHashMessage;
import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.network.NetworkNode;
import bisq.network.p2p.peers.Broadcaster;
import bisq.network.p2p.peers.PeerManager;
import bisq.common.proto.network.NetworkEnvelope;
import javax.inject.Inject;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class BlindVoteStateNetworkService extends StateNetworkService<NewBlindVoteStateHashMessage,
GetBlindVoteStateHashesRequest,
GetBlindVoteStateHashesResponse,
RequestBlindVoteStateHashesHandler,
BlindVoteStateHash> {
@Inject
public BlindVoteStateNetworkService(NetworkNode networkNode,
PeerManager peerManager,
Broadcaster broadcaster) {
super(networkNode, peerManager, broadcaster);
}
@Override
protected GetBlindVoteStateHashesRequest castToGetStateHashRequest(NetworkEnvelope networkEnvelope) {
return (GetBlindVoteStateHashesRequest) networkEnvelope;
}
@Override
protected boolean isGetStateHashesRequest(NetworkEnvelope networkEnvelope) {
return networkEnvelope instanceof GetBlindVoteStateHashesRequest;
}
@Override
protected NewBlindVoteStateHashMessage castToNewStateHashMessage(NetworkEnvelope networkEnvelope) {
return (NewBlindVoteStateHashMessage) networkEnvelope;
}
@Override
protected boolean isNewStateHashMessage(NetworkEnvelope networkEnvelope) {
return networkEnvelope instanceof NewBlindVoteStateHashMessage;
}
@Override
protected GetBlindVoteStateHashesResponse getGetStateHashesResponse(int nonce, List<BlindVoteStateHash> stateHashes) {
return new GetBlindVoteStateHashesResponse(stateHashes, nonce);
}
@Override
protected NewBlindVoteStateHashMessage getNewStateHashMessage(BlindVoteStateHash myStateHash) {
return new NewBlindVoteStateHashMessage(myStateHash);
}
@Override
protected RequestBlindVoteStateHashesHandler getRequestStateHashesHandler(NodeAddress nodeAddress, RequestStateHashesHandler.Listener<GetBlindVoteStateHashesResponse> listener) {
return new RequestBlindVoteStateHashesHandler(networkNode, peerManager, nodeAddress, listener);
}
}

View file

@ -0,0 +1,86 @@
/*
* 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.monitoring.network;
import bisq.core.dao.monitoring.model.DaoStateHash;
import bisq.core.dao.monitoring.network.messages.GetDaoStateHashesRequest;
import bisq.core.dao.monitoring.network.messages.GetDaoStateHashesResponse;
import bisq.core.dao.monitoring.network.messages.NewDaoStateHashMessage;
import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.network.NetworkNode;
import bisq.network.p2p.peers.Broadcaster;
import bisq.network.p2p.peers.PeerManager;
import bisq.common.proto.network.NetworkEnvelope;
import javax.inject.Inject;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class DaoStateNetworkService extends StateNetworkService<NewDaoStateHashMessage,
GetDaoStateHashesRequest,
GetDaoStateHashesResponse,
RequestDaoStateHashesHandler,
DaoStateHash> {
@Inject
public DaoStateNetworkService(NetworkNode networkNode,
PeerManager peerManager,
Broadcaster broadcaster) {
super(networkNode, peerManager, broadcaster);
}
@Override
protected GetDaoStateHashesRequest castToGetStateHashRequest(NetworkEnvelope networkEnvelope) {
return (GetDaoStateHashesRequest) networkEnvelope;
}
@Override
protected boolean isGetStateHashesRequest(NetworkEnvelope networkEnvelope) {
return networkEnvelope instanceof GetDaoStateHashesRequest;
}
@Override
protected NewDaoStateHashMessage castToNewStateHashMessage(NetworkEnvelope networkEnvelope) {
return (NewDaoStateHashMessage) networkEnvelope;
}
@Override
protected boolean isNewStateHashMessage(NetworkEnvelope networkEnvelope) {
return networkEnvelope instanceof NewDaoStateHashMessage;
}
@Override
protected GetDaoStateHashesResponse getGetStateHashesResponse(int nonce, List<DaoStateHash> stateHashes) {
return new GetDaoStateHashesResponse(stateHashes, nonce);
}
@Override
protected NewDaoStateHashMessage getNewStateHashMessage(DaoStateHash myStateHash) {
return new NewDaoStateHashMessage(myStateHash);
}
@Override
protected RequestDaoStateHashesHandler getRequestStateHashesHandler(NodeAddress nodeAddress, RequestStateHashesHandler.Listener<GetDaoStateHashesResponse> listener) {
return new RequestDaoStateHashesHandler(networkNode, peerManager, nodeAddress, listener);
}
}

View file

@ -0,0 +1,86 @@
/*
* 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.monitoring.network;
import bisq.core.dao.monitoring.model.ProposalStateHash;
import bisq.core.dao.monitoring.network.messages.GetProposalStateHashesRequest;
import bisq.core.dao.monitoring.network.messages.GetProposalStateHashesResponse;
import bisq.core.dao.monitoring.network.messages.NewProposalStateHashMessage;
import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.network.NetworkNode;
import bisq.network.p2p.peers.Broadcaster;
import bisq.network.p2p.peers.PeerManager;
import bisq.common.proto.network.NetworkEnvelope;
import javax.inject.Inject;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class ProposalStateNetworkService extends StateNetworkService<NewProposalStateHashMessage,
GetProposalStateHashesRequest,
GetProposalStateHashesResponse,
RequestProposalStateHashesHandler,
ProposalStateHash> {
@Inject
public ProposalStateNetworkService(NetworkNode networkNode,
PeerManager peerManager,
Broadcaster broadcaster) {
super(networkNode, peerManager, broadcaster);
}
@Override
protected GetProposalStateHashesRequest castToGetStateHashRequest(NetworkEnvelope networkEnvelope) {
return (GetProposalStateHashesRequest) networkEnvelope;
}
@Override
protected boolean isGetStateHashesRequest(NetworkEnvelope networkEnvelope) {
return networkEnvelope instanceof GetProposalStateHashesRequest;
}
@Override
protected NewProposalStateHashMessage castToNewStateHashMessage(NetworkEnvelope networkEnvelope) {
return (NewProposalStateHashMessage) networkEnvelope;
}
@Override
protected boolean isNewStateHashMessage(NetworkEnvelope networkEnvelope) {
return networkEnvelope instanceof NewProposalStateHashMessage;
}
@Override
protected GetProposalStateHashesResponse getGetStateHashesResponse(int nonce, List<ProposalStateHash> stateHashes) {
return new GetProposalStateHashesResponse(stateHashes, nonce);
}
@Override
protected NewProposalStateHashMessage getNewStateHashMessage(ProposalStateHash myStateHash) {
return new NewProposalStateHashMessage(myStateHash);
}
@Override
protected RequestProposalStateHashesHandler getRequestStateHashesHandler(NodeAddress nodeAddress, RequestStateHashesHandler.Listener<GetProposalStateHashesResponse> listener) {
return new RequestProposalStateHashesHandler(networkNode, peerManager, nodeAddress, listener);
}
}

View file

@ -0,0 +1,54 @@
/*
* 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.monitoring.network;
import bisq.core.dao.monitoring.network.messages.GetBlindVoteStateHashesRequest;
import bisq.core.dao.monitoring.network.messages.GetBlindVoteStateHashesResponse;
import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.network.NetworkNode;
import bisq.network.p2p.peers.PeerManager;
import bisq.common.proto.network.NetworkEnvelope;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class RequestBlindVoteStateHashesHandler extends RequestStateHashesHandler<GetBlindVoteStateHashesRequest, GetBlindVoteStateHashesResponse> {
RequestBlindVoteStateHashesHandler(NetworkNode networkNode,
PeerManager peerManager,
NodeAddress nodeAddress,
Listener<GetBlindVoteStateHashesResponse> listener) {
super(networkNode, peerManager, nodeAddress, listener);
}
@Override
protected GetBlindVoteStateHashesRequest getGetStateHashesRequest(int fromHeight) {
return new GetBlindVoteStateHashesRequest(fromHeight, nonce);
}
@Override
protected GetBlindVoteStateHashesResponse castToGetStateHashesResponse(NetworkEnvelope networkEnvelope) {
return (GetBlindVoteStateHashesResponse) networkEnvelope;
}
@Override
protected boolean isGetStateHashesResponse(NetworkEnvelope networkEnvelope) {
return networkEnvelope instanceof GetBlindVoteStateHashesResponse;
}
}

View file

@ -0,0 +1,54 @@
/*
* 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.monitoring.network;
import bisq.core.dao.monitoring.network.messages.GetDaoStateHashesRequest;
import bisq.core.dao.monitoring.network.messages.GetDaoStateHashesResponse;
import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.network.NetworkNode;
import bisq.network.p2p.peers.PeerManager;
import bisq.common.proto.network.NetworkEnvelope;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class RequestDaoStateHashesHandler extends RequestStateHashesHandler<GetDaoStateHashesRequest, GetDaoStateHashesResponse> {
RequestDaoStateHashesHandler(NetworkNode networkNode,
PeerManager peerManager,
NodeAddress nodeAddress,
Listener<GetDaoStateHashesResponse> listener) {
super(networkNode, peerManager, nodeAddress, listener);
}
@Override
protected GetDaoStateHashesRequest getGetStateHashesRequest(int fromHeight) {
return new GetDaoStateHashesRequest(fromHeight, nonce);
}
@Override
protected GetDaoStateHashesResponse castToGetStateHashesResponse(NetworkEnvelope networkEnvelope) {
return (GetDaoStateHashesResponse) networkEnvelope;
}
@Override
protected boolean isGetStateHashesResponse(NetworkEnvelope networkEnvelope) {
return networkEnvelope instanceof GetDaoStateHashesResponse;
}
}

View file

@ -0,0 +1,54 @@
/*
* 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.monitoring.network;
import bisq.core.dao.monitoring.network.messages.GetProposalStateHashesRequest;
import bisq.core.dao.monitoring.network.messages.GetProposalStateHashesResponse;
import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.network.NetworkNode;
import bisq.network.p2p.peers.PeerManager;
import bisq.common.proto.network.NetworkEnvelope;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class RequestProposalStateHashesHandler extends RequestStateHashesHandler<GetProposalStateHashesRequest, GetProposalStateHashesResponse> {
RequestProposalStateHashesHandler(NetworkNode networkNode,
PeerManager peerManager,
NodeAddress nodeAddress,
Listener<GetProposalStateHashesResponse> listener) {
super(networkNode, peerManager, nodeAddress, listener);
}
@Override
protected GetProposalStateHashesRequest getGetStateHashesRequest(int fromHeight) {
return new GetProposalStateHashesRequest(fromHeight, nonce);
}
@Override
protected GetProposalStateHashesResponse castToGetStateHashesResponse(NetworkEnvelope networkEnvelope) {
return (GetProposalStateHashesResponse) networkEnvelope;
}
@Override
protected boolean isGetStateHashesResponse(NetworkEnvelope networkEnvelope) {
return networkEnvelope instanceof GetProposalStateHashesResponse;
}
}

View file

@ -0,0 +1,227 @@
/*
* 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.monitoring.network;
import bisq.core.dao.monitoring.network.messages.GetStateHashesRequest;
import bisq.core.dao.monitoring.network.messages.GetStateHashesResponse;
import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.network.CloseConnectionReason;
import bisq.network.p2p.network.Connection;
import bisq.network.p2p.network.MessageListener;
import bisq.network.p2p.network.NetworkNode;
import bisq.network.p2p.peers.PeerManager;
import bisq.common.Timer;
import bisq.common.UserThread;
import bisq.common.proto.network.NetworkEnvelope;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.SettableFuture;
import java.util.Optional;
import java.util.Random;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@Slf4j
abstract class RequestStateHashesHandler<Req extends GetStateHashesRequest, Res extends GetStateHashesResponse> implements MessageListener {
private static final long TIMEOUT = 120;
///////////////////////////////////////////////////////////////////////////////////////////
// Listener
///////////////////////////////////////////////////////////////////////////////////////////
public interface Listener<Res extends GetStateHashesResponse> {
void onComplete(Res getStateHashesResponse, Optional<NodeAddress> peersNodeAddress);
@SuppressWarnings("UnusedParameters")
void onFault(String errorMessage, @SuppressWarnings("SameParameterValue") @Nullable Connection connection);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Class fields
///////////////////////////////////////////////////////////////////////////////////////////
private final NetworkNode networkNode;
private final PeerManager peerManager;
private final NodeAddress nodeAddress;
private final Listener<Res> listener;
private Timer timeoutTimer;
final int nonce = new Random().nextInt();
private boolean stopped;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
RequestStateHashesHandler(NetworkNode networkNode,
PeerManager peerManager,
NodeAddress nodeAddress,
Listener<Res> listener) {
this.networkNode = networkNode;
this.peerManager = peerManager;
this.nodeAddress = nodeAddress;
this.listener = listener;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Abstract
///////////////////////////////////////////////////////////////////////////////////////////
protected abstract Req getGetStateHashesRequest(int fromHeight);
protected abstract Res castToGetStateHashesResponse(NetworkEnvelope networkEnvelope);
protected abstract boolean isGetStateHashesResponse(NetworkEnvelope networkEnvelope);
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
public void requestStateHashes(int fromHeight) {
if (!stopped) {
Req getStateHashesRequest = getGetStateHashesRequest(fromHeight);
if (timeoutTimer == null) {
timeoutTimer = UserThread.runAfter(() -> { // setup before sending to avoid race conditions
if (!stopped) {
String errorMessage = "A timeout occurred at sending getStateHashesRequest:" + getStateHashesRequest +
" on peersNodeAddress:" + nodeAddress;
log.debug(errorMessage + " / RequestStateHashesHandler=" + RequestStateHashesHandler.this);
handleFault(errorMessage, nodeAddress, CloseConnectionReason.SEND_MSG_TIMEOUT);
} else {
log.trace("We have stopped already. We ignore that timeoutTimer.run call. " +
"Might be caused by an previous networkNode.sendMessage.onFailure.");
}
},
TIMEOUT);
}
log.info("We send to peer {} a {}.", nodeAddress, getStateHashesRequest);
networkNode.addMessageListener(this);
SettableFuture<Connection> future = networkNode.sendMessage(nodeAddress, getStateHashesRequest);
Futures.addCallback(future, new FutureCallback<>() {
@Override
public void onSuccess(Connection connection) {
if (!stopped) {
log.info("Sending of {} message to peer {} succeeded.",
getStateHashesRequest.getClass().getSimpleName(),
nodeAddress.getFullAddress());
} else {
log.trace("We have stopped already. We ignore that networkNode.sendMessage.onSuccess call." +
"Might be caused by an previous timeout.");
}
}
@Override
public void onFailure(@NotNull Throwable throwable) {
if (!stopped) {
String errorMessage = "Sending getStateHashesRequest to " + nodeAddress +
" failed. That is expected if the peer is offline.\n\t" +
"getStateHashesRequest=" + getStateHashesRequest + "." +
"\n\tException=" + throwable.getMessage();
log.error(errorMessage);
handleFault(errorMessage, nodeAddress, CloseConnectionReason.SEND_MSG_FAILURE);
} else {
log.trace("We have stopped already. We ignore that networkNode.sendMessage.onFailure call. " +
"Might be caused by an previous timeout.");
}
}
});
} else {
log.warn("We have stopped already. We ignore that requestProposalsHash call.");
}
}
public void cancel() {
cleanup();
}
///////////////////////////////////////////////////////////////////////////////////////////
// MessageListener implementation
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) {
if (isGetStateHashesResponse(networkEnvelope)) {
if (connection.getPeersNodeAddressOptional().isPresent() && connection.getPeersNodeAddressOptional().get().equals(nodeAddress)) {
if (!stopped) {
Res getStateHashesResponse = castToGetStateHashesResponse(networkEnvelope);
if (getStateHashesResponse.getRequestNonce() == nonce) {
stopTimeoutTimer();
cleanup();
log.info("We received from peer {} a {} with {} stateHashes",
nodeAddress.getFullAddress(), getStateHashesResponse.getClass().getSimpleName(),
getStateHashesResponse.getStateHashes().size());
listener.onComplete(getStateHashesResponse, connection.getPeersNodeAddressOptional());
} else {
log.warn("Nonce not matching. That can happen rarely if we get a response after a canceled " +
"handshake (timeout causes connection close but peer might have sent a msg before " +
"connection was closed).\n\t" +
"We drop that message. nonce={} / requestNonce={}",
nonce, getStateHashesResponse.getRequestNonce());
}
} else {
log.warn("We have stopped already.");
}
} else if (connection.getPeersNodeAddressOptional().isPresent()) {
log.debug("{}: We got a message from another node. We ignore that.",
this.getClass().getSimpleName());
}
}
}
public void stop() {
cleanup();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////
@SuppressWarnings("UnusedParameters")
private void handleFault(String errorMessage, NodeAddress nodeAddress, CloseConnectionReason closeConnectionReason) {
cleanup();
peerManager.handleConnectionFault(nodeAddress);
listener.onFault(errorMessage, null);
}
private void cleanup() {
stopped = true;
networkNode.removeMessageListener(this);
stopTimeoutTimer();
}
private void stopTimeoutTimer() {
if (timeoutTimer != null) {
timeoutTimer.stop();
timeoutTimer = null;
}
}
}

View file

@ -0,0 +1,208 @@
/*
* 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.monitoring.network;
import bisq.core.dao.monitoring.model.StateHash;
import bisq.core.dao.monitoring.network.messages.GetStateHashesRequest;
import bisq.core.dao.monitoring.network.messages.GetStateHashesResponse;
import bisq.core.dao.monitoring.network.messages.NewStateHashMessage;
import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.network.Connection;
import bisq.network.p2p.network.MessageListener;
import bisq.network.p2p.network.NetworkNode;
import bisq.network.p2p.peers.Broadcaster;
import bisq.network.p2p.peers.PeerManager;
import bisq.common.proto.network.NetworkEnvelope;
import javax.inject.Inject;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CopyOnWriteArrayList;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
@Slf4j
public abstract class StateNetworkService<Msg extends NewStateHashMessage,
Req extends GetStateHashesRequest,
Res extends GetStateHashesResponse<StH>,
Han extends RequestStateHashesHandler,
StH extends StateHash> implements MessageListener {
public interface Listener<Msg extends NewStateHashMessage, Req extends GetStateHashesRequest, StH extends StateHash> {
void onNewStateHashMessage(Msg newStateHashMessage, Connection connection);
void onGetStateHashRequest(Connection connection, Req getStateHashRequest);
void onPeersStateHashes(List<StH> stateHashes, Optional<NodeAddress> peersNodeAddress);
}
protected final NetworkNode networkNode;
protected final PeerManager peerManager;
private final Broadcaster broadcaster;
@Getter
private final Map<NodeAddress, Han> requestStateHashHandlerMap = new HashMap<>();
private final List<Listener<Msg, Req, StH>> listeners = new CopyOnWriteArrayList<>();
private boolean messageListenerAdded;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
public StateNetworkService(NetworkNode networkNode,
PeerManager peerManager,
Broadcaster broadcaster) {
this.networkNode = networkNode;
this.peerManager = peerManager;
this.broadcaster = broadcaster;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Abstract
///////////////////////////////////////////////////////////////////////////////////////////
protected abstract Req castToGetStateHashRequest(NetworkEnvelope networkEnvelope);
protected abstract boolean isGetStateHashesRequest(NetworkEnvelope networkEnvelope);
protected abstract Msg castToNewStateHashMessage(NetworkEnvelope networkEnvelope);
protected abstract boolean isNewStateHashMessage(NetworkEnvelope networkEnvelope);
protected abstract Res getGetStateHashesResponse(int nonce, List<StH> stateHashes);
protected abstract Msg getNewStateHashMessage(StH myStateHash);
protected abstract Han getRequestStateHashesHandler(NodeAddress nodeAddress, RequestStateHashesHandler.Listener<Res> listener);
///////////////////////////////////////////////////////////////////////////////////////////
// MessageListener implementation
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) {
if (isNewStateHashMessage(networkEnvelope)) {
Msg newStateHashMessage = castToNewStateHashMessage(networkEnvelope);
log.info("We received a {} from peer {} with stateHash={} ",
newStateHashMessage.getClass().getSimpleName(),
connection.getPeersNodeAddressOptional(),
newStateHashMessage.getStateHash());
listeners.forEach(e -> e.onNewStateHashMessage(newStateHashMessage, connection));
} else if (isGetStateHashesRequest(networkEnvelope)) {
Req getStateHashRequest = castToGetStateHashRequest(networkEnvelope);
log.info("We received a {} from peer {} for height={} ",
getStateHashRequest.getClass().getSimpleName(),
connection.getPeersNodeAddressOptional(),
getStateHashRequest.getHeight());
listeners.forEach(e -> e.onGetStateHashRequest(connection, getStateHashRequest));
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
public void addListeners() {
if (!messageListenerAdded) {
networkNode.addMessageListener(this);
messageListenerAdded = true;
}
}
public void sendGetStateHashesResponse(Connection connection, int nonce, List<StH> stateHashes) {
Res getStateHashesResponse = getGetStateHashesResponse(nonce, stateHashes);
log.info("Send {} with {} stateHashes to peer {}", getStateHashesResponse.getClass().getSimpleName(),
stateHashes.size(), connection.getPeersNodeAddressOptional());
connection.sendMessage(getStateHashesResponse);
}
public void requestHashesFromAllConnectedSeedNodes(int fromHeight) {
networkNode.getConfirmedConnections().stream()
.filter(peerManager::isSeedNode)
.forEach(connection -> connection.getPeersNodeAddressOptional()
.ifPresent(e -> requestHashesFromSeedNode(fromHeight, e)));
}
public void broadcastMyStateHash(StH myStateHash) {
NewStateHashMessage newStateHashMessage = getNewStateHashMessage(myStateHash);
broadcaster.broadcast(newStateHashMessage, networkNode.getNodeAddress(), null, true);
}
public void requestHashes(int fromHeight, String peersAddress) {
requestHashesFromSeedNode(fromHeight, new NodeAddress(peersAddress));
}
public void reset() {
requestStateHashHandlerMap.clear();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Listeners
///////////////////////////////////////////////////////////////////////////////////////////
public void addListener(Listener<Msg, Req, StH> listener) {
listeners.add(listener);
}
public void removeListener(Listener<Msg, Req, StH> listener) {
listeners.remove(listener);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////
private void requestHashesFromSeedNode(int fromHeight, NodeAddress nodeAddress) {
RequestStateHashesHandler.Listener<Res> listener = new RequestStateHashesHandler.Listener<>() {
@Override
public void onComplete(Res getStateHashesResponse, Optional<NodeAddress> peersNodeAddress) {
requestStateHashHandlerMap.remove(nodeAddress);
List<StH> stateHashes = getStateHashesResponse.getStateHashes();
listeners.forEach(e -> e.onPeersStateHashes(stateHashes, peersNodeAddress));
}
@Override
public void onFault(String errorMessage, @Nullable Connection connection) {
log.warn("requestDaoStateHashesHandler with outbound connection failed.\n\tnodeAddress={}\n\t" +
"ErrorMessage={}", nodeAddress, errorMessage);
requestStateHashHandlerMap.remove(nodeAddress);
}
};
Han requestStateHashesHandler = getRequestStateHashesHandler(nodeAddress, listener);
requestStateHashHandlerMap.put(nodeAddress, requestStateHashesHandler);
requestStateHashesHandler.requestStateHashes(fromHeight);
}
}

View file

@ -0,0 +1,56 @@
/*
* 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.monitoring.network.messages;
import bisq.common.app.Version;
import bisq.common.proto.network.NetworkEnvelope;
import io.bisq.generated.protobuffer.PB;
import lombok.EqualsAndHashCode;
import lombok.Getter;
@EqualsAndHashCode(callSuper = true)
@Getter
public final class GetBlindVoteStateHashesRequest extends GetStateHashesRequest {
public GetBlindVoteStateHashesRequest(int fromCycleStartHeight, int nonce) {
super(fromCycleStartHeight, nonce, Version.getP2PMessageVersion());
}
///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
private GetBlindVoteStateHashesRequest(int height, int nonce, int messageVersion) {
super(height, nonce, messageVersion);
}
@Override
public PB.NetworkEnvelope toProtoNetworkEnvelope() {
return getNetworkEnvelopeBuilder()
.setGetBlindVoteStateHashesRequest(PB.GetBlindVoteStateHashesRequest.newBuilder()
.setHeight(height)
.setNonce(nonce))
.build();
}
public static NetworkEnvelope fromProto(PB.GetBlindVoteStateHashesRequest proto, int messageVersion) {
return new GetBlindVoteStateHashesRequest(proto.getHeight(), proto.getNonce(), messageVersion);
}
}

View file

@ -0,0 +1,72 @@
/*
* 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.monitoring.network.messages;
import bisq.core.dao.monitoring.model.BlindVoteStateHash;
import bisq.common.app.Version;
import bisq.common.proto.network.NetworkEnvelope;
import io.bisq.generated.protobuffer.PB;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import lombok.EqualsAndHashCode;
import lombok.Getter;
@EqualsAndHashCode(callSuper = true)
@Getter
public final class GetBlindVoteStateHashesResponse extends GetStateHashesResponse<BlindVoteStateHash> {
public GetBlindVoteStateHashesResponse(List<BlindVoteStateHash> stateHashes, int requestNonce) {
super(stateHashes, requestNonce, Version.getP2PMessageVersion());
}
///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
private GetBlindVoteStateHashesResponse(List<BlindVoteStateHash> stateHashes,
int requestNonce,
int messageVersion) {
super(stateHashes, requestNonce, messageVersion);
}
@Override
public PB.NetworkEnvelope toProtoNetworkEnvelope() {
return getNetworkEnvelopeBuilder()
.setGetBlindVoteStateHashesResponse(PB.GetBlindVoteStateHashesResponse.newBuilder()
.addAllStateHashes(stateHashes.stream()
.map(BlindVoteStateHash::toProtoMessage)
.collect(Collectors.toList()))
.setRequestNonce(requestNonce))
.build();
}
public static NetworkEnvelope fromProto(PB.GetBlindVoteStateHashesResponse proto, int messageVersion) {
return new GetBlindVoteStateHashesResponse(proto.getStateHashesList().isEmpty() ?
new ArrayList<>() :
proto.getStateHashesList().stream()
.map(BlindVoteStateHash::fromProto)
.collect(Collectors.toList()),
proto.getRequestNonce(),
messageVersion);
}
}

View file

@ -0,0 +1,56 @@
/*
* 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.monitoring.network.messages;
import bisq.common.app.Version;
import bisq.common.proto.network.NetworkEnvelope;
import io.bisq.generated.protobuffer.PB;
import lombok.EqualsAndHashCode;
import lombok.Getter;
@EqualsAndHashCode(callSuper = true)
@Getter
public final class GetDaoStateHashesRequest extends GetStateHashesRequest {
public GetDaoStateHashesRequest(int height, int nonce) {
super(height, nonce, Version.getP2PMessageVersion());
}
///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
private GetDaoStateHashesRequest(int height, int nonce, int messageVersion) {
super(height, nonce, messageVersion);
}
@Override
public PB.NetworkEnvelope toProtoNetworkEnvelope() {
return getNetworkEnvelopeBuilder()
.setGetDaoStateHashesRequest(PB.GetDaoStateHashesRequest.newBuilder()
.setHeight(height)
.setNonce(nonce))
.build();
}
public static NetworkEnvelope fromProto(PB.GetDaoStateHashesRequest proto, int messageVersion) {
return new GetDaoStateHashesRequest(proto.getHeight(), proto.getNonce(), messageVersion);
}
}

View file

@ -0,0 +1,72 @@
/*
* 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.monitoring.network.messages;
import bisq.core.dao.monitoring.model.DaoStateHash;
import bisq.common.app.Version;
import bisq.common.proto.network.NetworkEnvelope;
import io.bisq.generated.protobuffer.PB;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import lombok.EqualsAndHashCode;
import lombok.Getter;
@EqualsAndHashCode(callSuper = true)
@Getter
public final class GetDaoStateHashesResponse extends GetStateHashesResponse<DaoStateHash> {
public GetDaoStateHashesResponse(List<DaoStateHash> daoStateHashes, int requestNonce) {
super(daoStateHashes, requestNonce, Version.getP2PMessageVersion());
}
///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
private GetDaoStateHashesResponse(List<DaoStateHash> daoStateHashes,
int requestNonce,
int messageVersion) {
super(daoStateHashes, requestNonce, messageVersion);
}
@Override
public PB.NetworkEnvelope toProtoNetworkEnvelope() {
return getNetworkEnvelopeBuilder()
.setGetDaoStateHashesResponse(PB.GetDaoStateHashesResponse.newBuilder()
.addAllStateHashes(stateHashes.stream()
.map(DaoStateHash::toProtoMessage)
.collect(Collectors.toList()))
.setRequestNonce(requestNonce))
.build();
}
public static NetworkEnvelope fromProto(PB.GetDaoStateHashesResponse proto, int messageVersion) {
return new GetDaoStateHashesResponse(proto.getStateHashesList().isEmpty() ?
new ArrayList<>() :
proto.getStateHashesList().stream()
.map(DaoStateHash::fromProto)
.collect(Collectors.toList()),
proto.getRequestNonce(),
messageVersion);
}
}

View file

@ -0,0 +1,56 @@
/*
* 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.monitoring.network.messages;
import bisq.common.app.Version;
import bisq.common.proto.network.NetworkEnvelope;
import io.bisq.generated.protobuffer.PB;
import lombok.EqualsAndHashCode;
import lombok.Getter;
@EqualsAndHashCode(callSuper = true)
@Getter
public final class GetProposalStateHashesRequest extends GetStateHashesRequest {
public GetProposalStateHashesRequest(int fromCycleStartHeight, int nonce) {
super(fromCycleStartHeight, nonce, Version.getP2PMessageVersion());
}
///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
private GetProposalStateHashesRequest(int height, int nonce, int messageVersion) {
super(height, nonce, messageVersion);
}
@Override
public PB.NetworkEnvelope toProtoNetworkEnvelope() {
return getNetworkEnvelopeBuilder()
.setGetProposalStateHashesRequest(PB.GetProposalStateHashesRequest.newBuilder()
.setHeight(height)
.setNonce(nonce))
.build();
}
public static NetworkEnvelope fromProto(PB.GetProposalStateHashesRequest proto, int messageVersion) {
return new GetProposalStateHashesRequest(proto.getHeight(), proto.getNonce(), messageVersion);
}
}

View file

@ -0,0 +1,72 @@
/*
* 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.monitoring.network.messages;
import bisq.core.dao.monitoring.model.ProposalStateHash;
import bisq.common.app.Version;
import bisq.common.proto.network.NetworkEnvelope;
import io.bisq.generated.protobuffer.PB;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import lombok.EqualsAndHashCode;
import lombok.Getter;
@EqualsAndHashCode(callSuper = true)
@Getter
public final class GetProposalStateHashesResponse extends GetStateHashesResponse<ProposalStateHash> {
public GetProposalStateHashesResponse(List<ProposalStateHash> proposalStateHashes, int requestNonce) {
super(proposalStateHashes, requestNonce, Version.getP2PMessageVersion());
}
///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
private GetProposalStateHashesResponse(List<ProposalStateHash> proposalStateHashes,
int requestNonce,
int messageVersion) {
super(proposalStateHashes, requestNonce, messageVersion);
}
@Override
public PB.NetworkEnvelope toProtoNetworkEnvelope() {
return getNetworkEnvelopeBuilder()
.setGetProposalStateHashesResponse(PB.GetProposalStateHashesResponse.newBuilder()
.addAllStateHashes(stateHashes.stream()
.map(ProposalStateHash::toProtoMessage)
.collect(Collectors.toList()))
.setRequestNonce(requestNonce))
.build();
}
public static NetworkEnvelope fromProto(PB.GetProposalStateHashesResponse proto, int messageVersion) {
return new GetProposalStateHashesResponse(proto.getStateHashesList().isEmpty() ?
new ArrayList<>() :
proto.getStateHashesList().stream()
.map(ProposalStateHash::fromProto)
.collect(Collectors.toList()),
proto.getRequestNonce(),
messageVersion);
}
}

View file

@ -0,0 +1,54 @@
/*
* 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.monitoring.network.messages;
import bisq.network.p2p.DirectMessage;
import bisq.network.p2p.storage.payload.CapabilityRequiringPayload;
import bisq.common.app.Capabilities;
import bisq.common.app.Capability;
import bisq.common.proto.network.NetworkEnvelope;
import lombok.EqualsAndHashCode;
import lombok.Getter;
@EqualsAndHashCode(callSuper = true)
@Getter
public abstract class GetStateHashesRequest extends NetworkEnvelope implements DirectMessage, CapabilityRequiringPayload {
protected final int height;
protected final int nonce;
protected GetStateHashesRequest(int height, int nonce, int messageVersion) {
super(messageVersion);
this.height = height;
this.nonce = nonce;
}
@Override
public Capabilities getRequiredCapabilities() {
return new Capabilities(Capability.DAO_STATE);
}
@Override
public String toString() {
return "GetStateHashesRequest{" +
",\n height=" + height +
",\n nonce=" + nonce +
"\n} " + super.toString();
}
}

View file

@ -0,0 +1,53 @@
/*
* 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.monitoring.network.messages;
import bisq.core.dao.monitoring.model.StateHash;
import bisq.network.p2p.DirectMessage;
import bisq.network.p2p.ExtendedDataSizePermission;
import bisq.common.proto.network.NetworkEnvelope;
import java.util.List;
import lombok.EqualsAndHashCode;
import lombok.Getter;
@EqualsAndHashCode(callSuper = true)
@Getter
public abstract class GetStateHashesResponse<T extends StateHash> extends NetworkEnvelope implements DirectMessage, ExtendedDataSizePermission {
protected final List<T> stateHashes;
protected final int requestNonce;
protected GetStateHashesResponse(List<T> stateHashes,
int requestNonce,
int messageVersion) {
super(messageVersion);
this.stateHashes = stateHashes;
this.requestNonce = requestNonce;
}
@Override
public String toString() {
return "GetStateHashesResponse{" +
"\n stateHashes=" + stateHashes +
",\n requestNonce=" + requestNonce +
"\n} " + super.toString();
}
}

View file

@ -0,0 +1,64 @@
/*
* 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.monitoring.network.messages;
import bisq.core.dao.monitoring.model.BlindVoteStateHash;
import bisq.common.app.Capabilities;
import bisq.common.app.Capability;
import bisq.common.app.Version;
import bisq.common.proto.network.NetworkEnvelope;
import io.bisq.generated.protobuffer.PB;
import lombok.EqualsAndHashCode;
import lombok.Getter;
@EqualsAndHashCode(callSuper = true)
@Getter
public final class NewBlindVoteStateHashMessage extends NewStateHashMessage<BlindVoteStateHash> {
public NewBlindVoteStateHashMessage(BlindVoteStateHash stateHash) {
super(stateHash, Version.getP2PMessageVersion());
}
///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
private NewBlindVoteStateHashMessage(BlindVoteStateHash stateHash, int messageVersion) {
super(stateHash, messageVersion);
}
@Override
public PB.NetworkEnvelope toProtoNetworkEnvelope() {
return getNetworkEnvelopeBuilder()
.setNewBlindVoteStateHashMessage(PB.NewBlindVoteStateHashMessage.newBuilder()
.setStateHash(stateHash.toProtoMessage()))
.build();
}
public static NetworkEnvelope fromProto(PB.NewBlindVoteStateHashMessage proto, int messageVersion) {
return new NewBlindVoteStateHashMessage(BlindVoteStateHash.fromProto(proto.getStateHash()), messageVersion);
}
@Override
public Capabilities getRequiredCapabilities() {
return new Capabilities(Capability.DAO_STATE);
}
}

View file

@ -0,0 +1,64 @@
/*
* 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.monitoring.network.messages;
import bisq.core.dao.monitoring.model.DaoStateHash;
import bisq.common.app.Capabilities;
import bisq.common.app.Capability;
import bisq.common.app.Version;
import bisq.common.proto.network.NetworkEnvelope;
import io.bisq.generated.protobuffer.PB;
import lombok.EqualsAndHashCode;
import lombok.Getter;
@EqualsAndHashCode(callSuper = true)
@Getter
public final class NewDaoStateHashMessage extends NewStateHashMessage<DaoStateHash> {
public NewDaoStateHashMessage(DaoStateHash daoStateHash) {
super(daoStateHash, Version.getP2PMessageVersion());
}
///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
private NewDaoStateHashMessage(DaoStateHash daoStateHash, int messageVersion) {
super(daoStateHash, messageVersion);
}
@Override
public PB.NetworkEnvelope toProtoNetworkEnvelope() {
return getNetworkEnvelopeBuilder()
.setNewDaoStateHashMessage(PB.NewDaoStateHashMessage.newBuilder()
.setStateHash(stateHash.toProtoMessage()))
.build();
}
public static NetworkEnvelope fromProto(PB.NewDaoStateHashMessage proto, int messageVersion) {
return new NewDaoStateHashMessage(DaoStateHash.fromProto(proto.getStateHash()), messageVersion);
}
@Override
public Capabilities getRequiredCapabilities() {
return new Capabilities(Capability.DAO_STATE);
}
}

View file

@ -0,0 +1,64 @@
/*
* 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.monitoring.network.messages;
import bisq.core.dao.monitoring.model.ProposalStateHash;
import bisq.common.app.Capabilities;
import bisq.common.app.Capability;
import bisq.common.app.Version;
import bisq.common.proto.network.NetworkEnvelope;
import io.bisq.generated.protobuffer.PB;
import lombok.EqualsAndHashCode;
import lombok.Getter;
@EqualsAndHashCode(callSuper = true)
@Getter
public final class NewProposalStateHashMessage extends NewStateHashMessage<ProposalStateHash> {
public NewProposalStateHashMessage(ProposalStateHash proposalStateHash) {
super(proposalStateHash, Version.getP2PMessageVersion());
}
///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
private NewProposalStateHashMessage(ProposalStateHash proposalStateHash, int messageVersion) {
super(proposalStateHash, messageVersion);
}
@Override
public PB.NetworkEnvelope toProtoNetworkEnvelope() {
return getNetworkEnvelopeBuilder()
.setNewProposalStateHashMessage(PB.NewProposalStateHashMessage.newBuilder()
.setStateHash(stateHash.toProtoMessage()))
.build();
}
public static NetworkEnvelope fromProto(PB.NewProposalStateHashMessage proto, int messageVersion) {
return new NewProposalStateHashMessage(ProposalStateHash.fromProto(proto.getStateHash()), messageVersion);
}
@Override
public Capabilities getRequiredCapabilities() {
return new Capabilities(Capability.DAO_STATE);
}
}

View file

@ -0,0 +1,52 @@
/*
* 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.monitoring.network.messages;
import bisq.core.dao.monitoring.model.StateHash;
import bisq.network.p2p.storage.messages.BroadcastMessage;
import bisq.network.p2p.storage.payload.CapabilityRequiringPayload;
import bisq.common.app.Capabilities;
import bisq.common.app.Capability;
import lombok.EqualsAndHashCode;
import lombok.Getter;
@EqualsAndHashCode(callSuper = true)
@Getter
public abstract class NewStateHashMessage<T extends StateHash> extends BroadcastMessage implements CapabilityRequiringPayload {
protected final T stateHash;
protected NewStateHashMessage(T stateHash, int messageVersion) {
super(messageVersion);
this.stateHash = stateHash;
}
@Override
public Capabilities getRequiredCapabilities() {
return new Capabilities(Capability.DAO_STATE);
}
@Override
public String toString() {
return "NewStateHashMessage{" +
"\n stateHash=" + stateHash +
"\n} " + super.toString();
}
}

View file

@ -185,7 +185,9 @@ public class FullNode extends BsqNode {
} else {
log.info("parseBlocksIfNewBlockAvailable did not result in a new block, so we complete.");
log.info("parse {} blocks took {} seconds", blocksToParseInBatch, (System.currentTimeMillis() - parseInBatchStartTime) / 1000d);
onParseBlockChainComplete();
if (!parseBlockchainComplete) {
onParseBlockChainComplete();
}
}
},
this::handleError);

View file

@ -74,7 +74,6 @@ public class RpcService {
private final String rpcPassword;
private final String rpcPort;
private final String rpcBlockPort;
private final boolean dumpBlockchainData;
private BtcdClient client;
private BtcdDaemon daemon;
@ -92,8 +91,7 @@ public class RpcService {
@Inject
public RpcService(Preferences preferences,
@Named(DaoOptionKeys.RPC_PORT) String rpcPort,
@Named(DaoOptionKeys.RPC_BLOCK_NOTIFICATION_PORT) String rpcBlockPort,
@Named(DaoOptionKeys.DUMP_BLOCKCHAIN_DATA) boolean dumpBlockchainData) {
@Named(DaoOptionKeys.RPC_BLOCK_NOTIFICATION_PORT) String rpcBlockPort) {
this.rpcUser = preferences.getRpcUser();
this.rpcPassword = preferences.getRpcPw();
@ -109,8 +107,6 @@ public class RpcService {
"18443"; // regtest
this.rpcBlockPort = rpcBlockPort != null && !rpcBlockPort.isEmpty() ? rpcBlockPort : "5125";
this.dumpBlockchainData = dumpBlockchainData;
log.info("Version of btcd-cli4j library: {}", BtcdCli4jVersion.VERSION);
}
@ -305,7 +301,7 @@ public class RpcService {
// We don't support raw MS which are the only case where scriptPubKey.getAddresses()>1
String address = scriptPubKey.getAddresses() != null &&
scriptPubKey.getAddresses().size() == 1 ? scriptPubKey.getAddresses().get(0) : null;
PubKeyScript pubKeyScript = dumpBlockchainData ? new PubKeyScript(scriptPubKey) : null;
PubKeyScript pubKeyScript = new PubKeyScript(scriptPubKey);
return new RawTxOutput(rawBtcTxOutput.getN(),
rawBtcTxOutput.getValue().movePointRight(8).longValue(),
rawBtcTx.getTxId(),

View file

@ -125,7 +125,7 @@ public class RequestBlocksHandler implements MessageListener {
TIMEOUT);
}
log.info("We send to peer {} a {}.", nodeAddress, getBlocksRequest);
log.info("We request blocks from peer {} from block height {}.", nodeAddress, getBlocksRequest.getFromBlockHeight());
networkNode.addMessageListener(this);
SettableFuture<Connection> future = networkNode.sendMessage(nodeAddress, getBlocksRequest);
Futures.addCallback(future, new FutureCallback<Connection>() {
@ -190,7 +190,7 @@ public class RequestBlocksHandler implements MessageListener {
log.warn("We have stopped already. We ignore that onDataRequest call.");
}
} else {
log.warn("We got a message from another connection and ignore it. That should never happen.");
log.warn("We got a message from ourselves. That should never happen.");
}
}
}

View file

@ -32,4 +32,9 @@ public interface DaoStateListener {
default void onParseBlockCompleteAfterBatchProcessing(Block block) {
}
// Called after the parsing of a block is complete and we do not allow any change in the daoState until the next
// block arrives.
default void onDaoStateChanged(Block block) {
}
}

View file

@ -46,9 +46,9 @@ import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@ -71,6 +71,7 @@ public class DaoStateService implements DaoSetupService {
private final List<DaoStateListener> daoStateListeners = new CopyOnWriteArrayList<>();
@Getter
private boolean parseBlockChainComplete;
private boolean allowDaoStateChange;
///////////////////////////////////////////////////////////////////////////////////////////
@ -95,6 +96,8 @@ public class DaoStateService implements DaoSetupService {
@Override
public void start() {
allowDaoStateChange = true;
assertDaoStateChange();
daoState.setChainHeight(genesisTxInfo.getGenesisBlockHeight());
}
@ -104,6 +107,9 @@ public class DaoStateService implements DaoSetupService {
///////////////////////////////////////////////////////////////////////////////////////////
public void applySnapshot(DaoState snapshot) {
allowDaoStateChange = true;
assertDaoStateChange();
log.info("Apply snapshot with chain height {}", snapshot.getChainHeight());
daoState.setChainHeight(snapshot.getChainHeight());
@ -117,15 +123,18 @@ public class DaoStateService implements DaoSetupService {
daoState.getUnspentTxOutputMap().clear();
daoState.getUnspentTxOutputMap().putAll(snapshot.getUnspentTxOutputMap());
daoState.getNonBsqTxOutputMap().clear();
daoState.getNonBsqTxOutputMap().putAll(snapshot.getNonBsqTxOutputMap());
daoState.getSpentInfoMap().clear();
daoState.getSpentInfoMap().putAll(snapshot.getSpentInfoMap());
daoState.getConfiscatedLockupTxList().clear();
daoState.getConfiscatedLockupTxList().addAll(snapshot.getConfiscatedLockupTxList());
daoState.getIssuanceMap().clear();
daoState.getIssuanceMap().putAll(snapshot.getIssuanceMap());
daoState.getSpentInfoMap().clear();
daoState.getSpentInfoMap().putAll(snapshot.getSpentInfoMap());
daoState.getParamChangeList().clear();
daoState.getParamChangeList().addAll(snapshot.getParamChangeList());
@ -144,6 +153,10 @@ public class DaoStateService implements DaoSetupService {
return DaoState.getClone(snapshotCandidate);
}
public byte[] getSerializedDaoState() {
return daoState.toProtoMessage().toByteArray();
}
///////////////////////////////////////////////////////////////////////////////////////////
// ChainHeight
@ -162,6 +175,11 @@ public class DaoStateService implements DaoSetupService {
return daoState.getCycles();
}
public void addCycle(Cycle cycle) {
assertDaoStateChange();
getCycles().add(cycle);
}
@Nullable
public Cycle getCurrentCycle() {
return !getCycles().isEmpty() ? getCycles().getLast() : null;
@ -190,12 +208,14 @@ public class DaoStateService implements DaoSetupService {
// First we get the blockHeight set
public void onNewBlockHeight(int blockHeight) {
allowDaoStateChange = true;
daoState.setChainHeight(blockHeight);
daoStateListeners.forEach(listener -> listener.onNewBlockHeight(blockHeight));
}
// Second we get the block added with empty txs
public void onNewBlockWithEmptyTxs(Block block) {
assertDaoStateChange();
if (daoState.getBlocks().isEmpty() && block.getHeight() != getGenesisBlockHeight()) {
log.warn("We don't have any blocks yet and we received a block which is not the genesis block. " +
"We ignore that block as the first block need to be the genesis block. " +
@ -215,14 +235,19 @@ public class DaoStateService implements DaoSetupService {
// VoteResult and other listeners like balances usually listen on onParseTxsCompleteAfterBatchProcessing
// so we need to make sure that vote result calculation is completed before (e.g. for comp. request to
// update balance).
// TODO the dependency on ordering is nto good here.... Listeners should not depend on order of execution.
daoStateListeners.forEach(l -> l.onParseBlockComplete(block));
// We use 2 different handlers as we don't want to update domain listeners during batch processing of all
// blocks as that cause performance issues. In earlier versions when we updated at each block it took
// 50 sec. for 4000 blocks, after that change it was about 4 sec.
// Clients
if (parseBlockChainComplete)
daoStateListeners.forEach(l -> l.onParseBlockCompleteAfterBatchProcessing(block));
// Here listeners must not trigger any state change in the DAO as we trigger the validation service to
// generate a hash of the state.
allowDaoStateChange = false;
daoStateListeners.forEach(l -> l.onDaoStateChanged(block));
}
// Called after parsing of all pending blocks is completed
@ -321,8 +346,8 @@ public class DaoStateService implements DaoSetupService {
.flatMap(block -> block.getTxs().stream());
}
public Map<String, Tx> getTxMap() {
return getTxStream().collect(Collectors.toMap(Tx::getId, tx -> tx));
public TreeMap<String, Tx> getTxMap() {
return new TreeMap<>(getTxStream().collect(Collectors.toMap(Tx::getId, tx -> tx)));
}
public Set<Tx> getTxs() {
@ -405,15 +430,17 @@ public class DaoStateService implements DaoSetupService {
// UnspentTxOutput
///////////////////////////////////////////////////////////////////////////////////////////
public Map<TxOutputKey, TxOutput> getUnspentTxOutputMap() {
public TreeMap<TxOutputKey, TxOutput> getUnspentTxOutputMap() {
return daoState.getUnspentTxOutputMap();
}
public void addUnspentTxOutput(TxOutput txOutput) {
assertDaoStateChange();
getUnspentTxOutputMap().put(txOutput.getKey(), txOutput);
}
public void removeUnspentTxOutput(TxOutput txOutput) {
assertDaoStateChange();
getUnspentTxOutputMap().remove(txOutput.getKey());
}
@ -547,6 +574,7 @@ public class DaoStateService implements DaoSetupService {
///////////////////////////////////////////////////////////////////////////////////////////
public void addIssuance(Issuance issuance) {
assertDaoStateChange();
daoState.getIssuanceMap().put(issuance.getTxId(), issuance);
}
@ -595,16 +623,18 @@ public class DaoStateService implements DaoSetupService {
// Non-BSQ
///////////////////////////////////////////////////////////////////////////////////////////
//TODO we never remove NonBsqTxOutput!
//FIXME called at result phase even if there is not a new one (passed txo from prev. cycle which was already added)
public void addNonBsqTxOutput(TxOutput txOutput) {
assertDaoStateChange();
checkArgument(txOutput.getTxOutputType() == TxOutputType.ISSUANCE_CANDIDATE_OUTPUT,
"txOutput must be type ISSUANCE_CANDIDATE_OUTPUT");
log.info("addNonBsqTxOutput: txOutput={}", txOutput);
daoState.getNonBsqTxOutputMap().put(txOutput.getKey(), txOutput);
}
public Optional<TxOutput> getBtcTxOutput(TxOutputKey key) {
// Issuance candidates which did not got accepted in voting are covered here
Map<TxOutputKey, TxOutput> nonBsqTxOutputMap = daoState.getNonBsqTxOutputMap();
TreeMap<TxOutputKey, TxOutput> nonBsqTxOutputMap = daoState.getNonBsqTxOutputMap();
if (nonBsqTxOutputMap.containsKey(key))
return Optional.of(nonBsqTxOutputMap.get(key));
@ -827,6 +857,7 @@ public class DaoStateService implements DaoSetupService {
}
private void doConfiscateBond(String lockupTxId) {
assertDaoStateChange();
log.warn("TxId {} added to confiscatedLockupTxIdList.", lockupTxId);
daoState.getConfiscatedLockupTxList().add(lockupTxId);
}
@ -855,6 +886,7 @@ public class DaoStateService implements DaoSetupService {
///////////////////////////////////////////////////////////////////////////////////////////
public void setNewParam(int blockHeight, Param param, String paramValue) {
assertDaoStateChange();
List<ParamChange> paramChangeList = daoState.getParamChangeList();
getStartHeightOfNextCycle(blockHeight)
.ifPresent(heightOfNewCycle -> {
@ -903,6 +935,7 @@ public class DaoStateService implements DaoSetupService {
///////////////////////////////////////////////////////////////////////////////////////////
public void setSpentInfo(TxOutputKey txOutputKey, SpentInfo spentInfo) {
assertDaoStateChange();
daoState.getSpentInfoMap().put(txOutputKey, spentInfo);
}
@ -920,9 +953,14 @@ public class DaoStateService implements DaoSetupService {
}
public void addEvaluatedProposalSet(Set<EvaluatedProposal> evaluatedProposals) {
assertDaoStateChange();
evaluatedProposals.stream()
.filter(e -> !daoState.getEvaluatedProposalList().contains(e))
.forEach(daoState.getEvaluatedProposalList()::add);
// We need deterministic order for the hash chain
daoState.getEvaluatedProposalList().sort(Comparator.comparing(EvaluatedProposal::getProposalTxId));
}
public List<DecryptedBallotsWithMerits> getDecryptedBallotsWithMeritsList() {
@ -930,9 +968,14 @@ public class DaoStateService implements DaoSetupService {
}
public void addDecryptedBallotsWithMeritsSet(Set<DecryptedBallotsWithMerits> decryptedBallotsWithMeritsSet) {
assertDaoStateChange();
decryptedBallotsWithMeritsSet.stream()
.filter(e -> !daoState.getDecryptedBallotsWithMeritsList().contains(e))
.forEach(daoState.getDecryptedBallotsWithMeritsList()::add);
// We need deterministic order for the hash chain
daoState.getDecryptedBallotsWithMeritsList().sort(Comparator.comparing(DecryptedBallotsWithMerits::getBlindVoteTxId));
}
@ -964,5 +1007,24 @@ public class DaoStateService implements DaoSetupService {
public void removeDaoStateListener(DaoStateListener listener) {
daoStateListeners.remove(listener);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Utils
///////////////////////////////////////////////////////////////////////////////////////////
public String daoStateToString() {
return daoState.toString();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////
private void assertDaoStateChange() {
if (!allowDaoStateChange)
throw new RuntimeException("We got a call which would change the daoState outside of the allowed event phase");
}
}

View file

@ -18,6 +18,8 @@
package bisq.core.dao.state;
import bisq.core.dao.governance.period.CycleService;
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;
@ -37,17 +39,20 @@ import lombok.extern.slf4j.Slf4j;
* SNAPSHOT_GRID old not less than 2 times the SNAPSHOT_GRID old.
*/
@Slf4j
public class DaoStateSnapshotService implements DaoStateListener {
public class DaoStateSnapshotService {
private static final int SNAPSHOT_GRID = 20;
private final DaoStateService daoStateService;
private final GenesisTxInfo genesisTxInfo;
private final CycleService cycleService;
private final DaoStateStorageService daoStateStorageService;
private final DaoStateMonitoringService daoStateMonitoringService;
private DaoState snapshotCandidate;
private DaoState daoStateSnapshotCandidate;
private LinkedList<DaoStateHash> daoStateHashChainSnapshotCandidate = new LinkedList<>();
private int chainHeightOfLastApplySnapshot;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
@ -56,47 +61,13 @@ public class DaoStateSnapshotService implements DaoStateListener {
public DaoStateSnapshotService(DaoStateService daoStateService,
GenesisTxInfo genesisTxInfo,
CycleService cycleService,
DaoStateStorageService daoStateStorageService) {
DaoStateStorageService daoStateStorageService,
DaoStateMonitoringService daoStateMonitoringService) {
this.daoStateService = daoStateService;
this.genesisTxInfo = genesisTxInfo;
this.cycleService = cycleService;
this.daoStateStorageService = daoStateStorageService;
this.daoStateService.addDaoStateListener(this);
}
///////////////////////////////////////////////////////////////////////////////////////////
// DaoStateListener
///////////////////////////////////////////////////////////////////////////////////////////
// We listen to each ParseTxsComplete event even if the batch processing of all blocks at startup is not completed
// as we need to write snapshots during that batch processing.
@Override
public void onParseBlockComplete(Block block) {
int chainHeight = block.getHeight();
// Either we don't have a snapshot candidate yet, or if we have one the height at that snapshot candidate must be
// different to our current height.
boolean noSnapshotCandidateOrDifferentHeight = snapshotCandidate == null || snapshotCandidate.getChainHeight() != chainHeight;
if (isSnapshotHeight(chainHeight) &&
!daoStateService.getBlocks().isEmpty() &&
isValidHeight(daoStateService.getBlocks().getLast().getHeight()) &&
noSnapshotCandidateOrDifferentHeight) {
// At trigger event we store the latest snapshotCandidate to disc
if (snapshotCandidate != null) {
// We clone because storage is in a threaded context and we set the snapshotCandidate to our current
// state in the next step
DaoState cloned = daoStateService.getClone(snapshotCandidate);
daoStateStorageService.persist(cloned);
log.info("Saved snapshotCandidate with height {} to Disc at height {} ",
snapshotCandidate.getChainHeight(), chainHeight);
}
// Now we clone and keep it in memory for the next trigger event
snapshotCandidate = daoStateService.getClone();
log.info("Cloned new snapshotCandidate at height " + chainHeight);
}
this.daoStateMonitoringService = daoStateMonitoringService;
}
@ -104,31 +75,66 @@ public class DaoStateSnapshotService implements DaoStateListener {
// 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();
// Either we don't have a snapshot candidate yet, or if we have one the height at that snapshot candidate must be
// different to our current height.
boolean noSnapshotCandidateOrDifferentHeight = daoStateSnapshotCandidate == null ||
daoStateSnapshotCandidate.getChainHeight() != chainHeight;
if (isSnapshotHeight(chainHeight) &&
!daoStateService.getBlocks().isEmpty() &&
isValidHeight(daoStateService.getBlocks().getLast().getHeight()) &&
noSnapshotCandidateOrDifferentHeight) {
// At trigger event we store the latest snapshotCandidate to disc
if (daoStateSnapshotCandidate != null) {
// We clone because storage is in a threaded context and we set the snapshotCandidate to our current
// state in the next step
DaoState clonedDaoState = daoStateService.getClone(daoStateSnapshotCandidate);
LinkedList<DaoStateHash> clonedDaoStateHashChain = new LinkedList<>(daoStateHashChainSnapshotCandidate);
daoStateStorageService.persist(clonedDaoState, clonedDaoStateHashChain);
log.info("Saved snapshotCandidate with height {} to Disc at height {} ",
daoStateSnapshotCandidate.getChainHeight(), chainHeight);
}
// 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 " + chainHeight);
}
}
public void applySnapshot(boolean fromReorg) {
DaoState persisted = daoStateStorageService.getPersistedBsqState();
if (persisted != null) {
LinkedList<Block> blocks = persisted.getBlocks();
int chainHeightOfPersisted = persisted.getChainHeight();
DaoState persistedBsqState = daoStateStorageService.getPersistedBsqState();
LinkedList<DaoStateHash> persistedDaoStateHashChain = daoStateStorageService.getPersistedDaoStateHashChain();
if (persistedBsqState != null) {
LinkedList<Block> blocks = persistedBsqState.getBlocks();
int chainHeightOfPersisted = persistedBsqState.getChainHeight();
if (!blocks.isEmpty()) {
int heightOfLastBlock = blocks.getLast().getHeight();
log.info("applySnapshot from persisted daoState with height of last block {}", heightOfLastBlock);
log.info("applySnapshot from persistedBsqState daoState with height of last block {}", heightOfLastBlock);
if (isValidHeight(heightOfLastBlock)) {
if (chainHeightOfLastApplySnapshot != chainHeightOfPersisted) {
chainHeightOfLastApplySnapshot = chainHeightOfPersisted;
daoStateService.applySnapshot(persisted);
daoStateService.applySnapshot(persistedBsqState);
daoStateMonitoringService.applySnapshot(persistedDaoStateHashChain);
} else {
// The reorg might have been caused by the previous parsing which might contains a range of
// blocks.
log.warn("We applied already a snapshot with chainHeight {}. We will reset the daoState and " +
"start over from the genesis transaction again.", chainHeightOfLastApplySnapshot);
persisted = new DaoState();
applyEmptySnapshot(persisted);
applyEmptySnapshot();
}
}
} else if (fromReorg) {
log.info("We got a reorg and we want to apply the snapshot but it is empty. That is expected in the first blocks until the " +
"first snapshot has been created. We use our applySnapshot method and restart from the genesis tx");
applyEmptySnapshot(persisted);
applyEmptySnapshot();
}
} else {
log.info("Try to apply snapshot but no stored snapshot available. That is expected at first blocks.");
@ -144,13 +150,16 @@ public class DaoStateSnapshotService implements DaoStateListener {
return heightOfLastBlock >= genesisTxInfo.getGenesisBlockHeight();
}
private void applyEmptySnapshot(DaoState persisted) {
private void applyEmptySnapshot() {
DaoState emptyDaoState = new DaoState();
int genesisBlockHeight = genesisTxInfo.getGenesisBlockHeight();
persisted.setChainHeight(genesisBlockHeight);
emptyDaoState.setChainHeight(genesisBlockHeight);
chainHeightOfLastApplySnapshot = genesisBlockHeight;
daoStateService.applySnapshot(persisted);
daoStateService.applySnapshot(emptyDaoState);
// In case we apply an empty snapshot we need to trigger the cycleService.addFirstCycle method
cycleService.addFirstCycle();
daoStateMonitoringService.applySnapshot(new LinkedList<>());
}
@VisibleForTesting

View file

@ -17,6 +17,8 @@
package bisq.core.dao.state;
import bisq.core.dao.monitoring.DaoStateMonitoringService;
import bisq.core.dao.monitoring.model.DaoStateHash;
import bisq.core.dao.state.model.DaoState;
import bisq.network.p2p.storage.persistence.ResourceDataStoreService;
@ -30,6 +32,7 @@ import javax.inject.Named;
import java.io.File;
import java.util.LinkedList;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
@ -39,9 +42,13 @@ import lombok.extern.slf4j.Slf4j;
*/
@Slf4j
public class DaoStateStorageService extends StoreService<DaoStateStore> {
private static final String FILE_NAME = "DaoStateStore";
//TODO We need to rename as we have a new file structure with the hashchain feature and need to enforce the
// new file to be used.
// We can rename to DaoStateStore before mainnet launch again.
private static final String FILE_NAME = "DaoStateStore2";
private DaoState daoState;
private final DaoState daoState;
private final DaoStateMonitoringService daoStateMonitoringService;
///////////////////////////////////////////////////////////////////////////////////////////
@ -51,10 +58,12 @@ public class DaoStateStorageService extends StoreService<DaoStateStore> {
@Inject
public DaoStateStorageService(ResourceDataStoreService resourceDataStoreService,
DaoState daoState,
DaoStateMonitoringService daoStateMonitoringService,
@Named(Storage.STORAGE_DIR) File storageDir,
Storage<DaoStateStore> daoSnapshotStorage) {
super(storageDir, daoSnapshotStorage);
this.daoState = daoState;
this.daoStateMonitoringService = daoStateMonitoringService;
resourceDataStoreService.addService(this);
}
@ -69,12 +78,13 @@ public class DaoStateStorageService extends StoreService<DaoStateStore> {
return FILE_NAME;
}
public void persist(DaoState daoState) {
persist(daoState, 200);
public void persist(DaoState daoState, LinkedList<DaoStateHash> daoStateHashChain) {
persist(daoState, daoStateHashChain, 200);
}
public void persist(DaoState daoState, long delayInMilli) {
private void persist(DaoState daoState, LinkedList<DaoStateHash> daoStateHashChain, long delayInMilli) {
store.setDaoState(daoState);
store.setDaoStateHashChain(daoStateHashChain);
storage.queueUpForSave(store, delayInMilli);
}
@ -82,8 +92,12 @@ public class DaoStateStorageService extends StoreService<DaoStateStore> {
return store.getDaoState();
}
public LinkedList<DaoStateHash> getPersistedDaoStateHashChain() {
return store.getDaoStateHashChain();
}
public void resetDaoState(Runnable resultHandler) {
persist(new DaoState(), 1);
persist(new DaoState(), new LinkedList<>(), 1);
UserThread.runAfter(resultHandler, 300, TimeUnit.MILLISECONDS);
}
@ -94,6 +108,6 @@ public class DaoStateStorageService extends StoreService<DaoStateStore> {
@Override
protected DaoStateStore createStore() {
return new DaoStateStore(DaoState.getClone(daoState));
return new DaoStateStore(DaoState.getClone(daoState), new LinkedList<>(daoStateMonitoringService.getDaoStateHashChain()));
}
}

View file

@ -17,6 +17,7 @@
package bisq.core.dao.state;
import bisq.core.dao.monitoring.model.DaoStateHash;
import bisq.core.dao.state.model.DaoState;
import bisq.common.proto.persistable.PersistableEnvelope;
@ -25,12 +26,13 @@ import io.bisq.generated.protobuffer.PB;
import com.google.protobuf.Message;
import java.util.LinkedList;
import java.util.stream.Collectors;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
import static com.google.common.base.Preconditions.checkNotNull;
@ -38,13 +40,16 @@ import static com.google.common.base.Preconditions.checkNotNull;
public class DaoStateStore implements PersistableEnvelope {
// DaoState is always a clone and must not be used for read access beside initial read from disc when we apply
// the snapshot!
@Nullable
@Getter
@Setter
DaoState daoState;
private DaoState daoState;
@Getter
@Setter
private LinkedList<DaoStateHash> daoStateHashChain;
DaoStateStore(DaoState daoState) {
DaoStateStore(DaoState daoState, LinkedList<DaoStateHash> daoStateHashChain) {
this.daoState = daoState;
this.daoStateHashChain = daoStateHashChain;
}
@ -55,13 +60,21 @@ public class DaoStateStore implements PersistableEnvelope {
public Message toProtoMessage() {
checkNotNull(daoState, "daoState must not be null when toProtoMessage is invoked");
PB.DaoStateStore.Builder builder = PB.DaoStateStore.newBuilder()
.setBsqState(daoState.getBsqStateBuilder());
.setBsqState(daoState.getBsqStateBuilder())
.addAllDaoStateHash(daoStateHashChain.stream()
.map(DaoStateHash::toProtoMessage)
.collect(Collectors.toList()));
return PB.PersistableEnvelope.newBuilder()
.setDaoStateStore(builder)
.build();
}
public static PersistableEnvelope fromProto(PB.DaoStateStore proto) {
return new DaoStateStore(DaoState.fromProto(proto.getBsqState()));
LinkedList<DaoStateHash> daoStateHashList = proto.getDaoStateHashList().isEmpty() ?
new LinkedList<>() :
new LinkedList<>(proto.getDaoStateHashList().stream()
.map(DaoStateHash::fromProto)
.collect(Collectors.toList()));
return new DaoStateStore(DaoState.fromProto(proto.getBsqState()), daoStateHashList);
}
}

View file

@ -36,10 +36,10 @@ import com.google.protobuf.Message;
import javax.inject.Inject;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
import lombok.Getter;
@ -51,6 +51,9 @@ import lombok.extern.slf4j.Slf4j;
* Holds both blockchain data as well as data derived from the governance process (voting).
* <p>
* One BSQ block with empty txs adds 152 bytes which results in about 8 MB/year
*
* For supporting the hashChain we need to ensure deterministic sorting behaviour of all collections so we use a
* TreeMap which is sorted by the key.
*/
@Slf4j
public class DaoState implements PersistablePayload {
@ -77,17 +80,17 @@ public class DaoState implements PersistablePayload {
// These maps represent mutual data which can get changed at parsing a transaction
@Getter
private final Map<TxOutputKey, TxOutput> unspentTxOutputMap;
private final TreeMap<TxOutputKey, TxOutput> unspentTxOutputMap;
@Getter
private final Map<TxOutputKey, TxOutput> nonBsqTxOutputMap;
private final TreeMap<TxOutputKey, TxOutput> nonBsqTxOutputMap;
@Getter
private final Map<TxOutputKey, SpentInfo> spentInfoMap;
private final TreeMap<TxOutputKey, SpentInfo> spentInfoMap;
// These maps are related to state change triggered by voting
@Getter
private final List<String> confiscatedLockupTxList;
@Getter
private final Map<String, Issuance> issuanceMap; // key is txId
private final TreeMap<String, Issuance> issuanceMap; // key is txId
@Getter
private final List<ParamChange> paramChangeList;
@ -109,11 +112,11 @@ public class DaoState implements PersistablePayload {
this(0,
new LinkedList<>(),
new LinkedList<>(),
new HashMap<>(),
new HashMap<>(),
new HashMap<>(),
new TreeMap<>(),
new TreeMap<>(),
new TreeMap<>(),
new ArrayList<>(),
new HashMap<>(),
new TreeMap<>(),
new ArrayList<>(),
new ArrayList<>(),
new ArrayList<>()
@ -128,11 +131,11 @@ public class DaoState implements PersistablePayload {
private DaoState(int chainHeight,
LinkedList<Block> blocks,
LinkedList<Cycle> cycles,
Map<TxOutputKey, TxOutput> unspentTxOutputMap,
Map<TxOutputKey, TxOutput> nonBsqTxOutputMap,
Map<TxOutputKey, SpentInfo> spentInfoMap,
TreeMap<TxOutputKey, TxOutput> unspentTxOutputMap,
TreeMap<TxOutputKey, TxOutput> nonBsqTxOutputMap,
TreeMap<TxOutputKey, SpentInfo> spentInfoMap,
List<String> confiscatedLockupTxList,
Map<String, Issuance> issuanceMap,
TreeMap<String, Issuance> issuanceMap,
List<ParamChange> paramChangeList,
List<EvaluatedProposal> evaluatedProposalList,
List<DecryptedBallotsWithMerits> decryptedBallotsWithMeritsList) {
@ -182,15 +185,15 @@ public class DaoState implements PersistablePayload {
.collect(Collectors.toCollection(LinkedList::new));
LinkedList<Cycle> cycles = proto.getCyclesList().stream()
.map(Cycle::fromProto).collect(Collectors.toCollection(LinkedList::new));
Map<TxOutputKey, TxOutput> unspentTxOutputMap = proto.getUnspentTxOutputMapMap().entrySet().stream()
.collect(Collectors.toMap(e -> TxOutputKey.getKeyFromString(e.getKey()), e -> TxOutput.fromProto(e.getValue())));
Map<TxOutputKey, TxOutput> nonBsqTxOutputMap = proto.getNonBsqTxOutputMapMap().entrySet().stream()
.collect(Collectors.toMap(e -> TxOutputKey.getKeyFromString(e.getKey()), e -> TxOutput.fromProto(e.getValue())));
Map<TxOutputKey, SpentInfo> spentInfoMap = proto.getSpentInfoMapMap().entrySet().stream()
.collect(Collectors.toMap(e -> TxOutputKey.getKeyFromString(e.getKey()), e -> SpentInfo.fromProto(e.getValue())));
TreeMap<TxOutputKey, TxOutput> unspentTxOutputMap = new TreeMap<>(proto.getUnspentTxOutputMapMap().entrySet().stream()
.collect(Collectors.toMap(e -> TxOutputKey.getKeyFromString(e.getKey()), e -> TxOutput.fromProto(e.getValue()))));
TreeMap<TxOutputKey, TxOutput> nonBsqTxOutputMap = new TreeMap<>(proto.getNonBsqTxOutputMapMap().entrySet().stream()
.collect(Collectors.toMap(e -> TxOutputKey.getKeyFromString(e.getKey()), e -> TxOutput.fromProto(e.getValue()))));
TreeMap<TxOutputKey, SpentInfo> spentInfoMap = new TreeMap<>(proto.getSpentInfoMapMap().entrySet().stream()
.collect(Collectors.toMap(e -> TxOutputKey.getKeyFromString(e.getKey()), e -> SpentInfo.fromProto(e.getValue()))));
List<String> confiscatedLockupTxList = new ArrayList<>(proto.getConfiscatedLockupTxListList());
Map<String, Issuance> issuanceMap = proto.getIssuanceMapMap().entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> Issuance.fromProto(e.getValue())));
TreeMap<String, Issuance> issuanceMap = new TreeMap<>(proto.getIssuanceMapMap().entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> Issuance.fromProto(e.getValue()))));
List<ParamChange> paramChangeList = proto.getParamChangeListList().stream()
.map(ParamChange::fromProto).collect(Collectors.toCollection(ArrayList::new));
List<EvaluatedProposal> evaluatedProposalList = proto.getEvaluatedProposalListList().stream()
@ -218,4 +221,21 @@ public class DaoState implements PersistablePayload {
public void setChainHeight(int chainHeight) {
this.chainHeight = chainHeight;
}
@Override
public String toString() {
return "DaoState{" +
"\n chainHeight=" + chainHeight +
",\n blocks=" + blocks +
",\n cycles=" + cycles +
",\n unspentTxOutputMap=" + unspentTxOutputMap +
",\n nonBsqTxOutputMap=" + nonBsqTxOutputMap +
",\n spentInfoMap=" + spentInfoMap +
",\n confiscatedLockupTxList=" + confiscatedLockupTxList +
",\n issuanceMap=" + issuanceMap +
",\n paramChangeList=" + paramChangeList +
",\n evaluatedProposalList=" + evaluatedProposalList +
",\n decryptedBallotsWithMeritsList=" + decryptedBallotsWithMeritsList +
"\n}";
}
}

View file

@ -45,9 +45,10 @@ public abstract class BaseTxOutput implements ImmutableDaoStateModel {
protected final long value;
protected final String txId;
// Only set if dumpBlockchainData is true
// Before v0.9.6 it was only set if dumpBlockchainData was set to true but we changed that with 0.9.6
// so that is is always set. We still need to support it because of backward compatibility.
@Nullable
protected final PubKeyScript pubKeyScript;
protected final PubKeyScript pubKeyScript; // Has about 50 bytes, total size of TxOutput is about 300 bytes.
@Nullable
protected final String address;
@Nullable
@ -69,7 +70,6 @@ public abstract class BaseTxOutput implements ImmutableDaoStateModel {
this.address = address;
this.opReturnData = opReturnData;
this.blockHeight = blockHeight;
}

View file

@ -21,6 +21,8 @@ import bisq.core.dao.state.model.ImmutableDaoStateModel;
import lombok.Value;
import org.jetbrains.annotations.NotNull;
import javax.annotation.concurrent.Immutable;
/**
@ -29,7 +31,7 @@ import javax.annotation.concurrent.Immutable;
*/
@Immutable
@Value
public final class TxOutputKey implements ImmutableDaoStateModel {
public final class TxOutputKey implements ImmutableDaoStateModel, Comparable {
private final String txId;
private final int index;
@ -47,4 +49,9 @@ public final class TxOutputKey implements ImmutableDaoStateModel {
final String[] tokens = keyAsString.split(":");
return new TxOutputKey(tokens[0], Integer.valueOf(tokens[1]));
}
@Override
public int compareTo(@NotNull Object o) {
return toString().compareTo(o.toString());
}
}

View file

@ -28,6 +28,15 @@ import bisq.core.arbitration.messages.PeerOpenedDisputeMessage;
import bisq.core.arbitration.messages.PeerPublishedDisputePayoutTxMessage;
import bisq.core.dao.governance.blindvote.network.messages.RepublishGovernanceDataRequest;
import bisq.core.dao.governance.proposal.storage.temp.TempProposalPayload;
import bisq.core.dao.monitoring.network.messages.GetBlindVoteStateHashesRequest;
import bisq.core.dao.monitoring.network.messages.GetBlindVoteStateHashesResponse;
import bisq.core.dao.monitoring.network.messages.GetDaoStateHashesRequest;
import bisq.core.dao.monitoring.network.messages.GetDaoStateHashesResponse;
import bisq.core.dao.monitoring.network.messages.GetProposalStateHashesRequest;
import bisq.core.dao.monitoring.network.messages.GetProposalStateHashesResponse;
import bisq.core.dao.monitoring.network.messages.NewBlindVoteStateHashMessage;
import bisq.core.dao.monitoring.network.messages.NewDaoStateHashMessage;
import bisq.core.dao.monitoring.network.messages.NewProposalStateHashMessage;
import bisq.core.dao.node.messages.GetBlocksRequest;
import bisq.core.dao.node.messages.GetBlocksResponse;
import bisq.core.dao.node.messages.NewBlockBroadcastMessage;
@ -153,7 +162,6 @@ public class CoreNetworkProtoResolver extends CoreProtoResolver implements Netwo
return GetBlocksResponse.fromProto(proto.getGetBlocksResponse(), messageVersion);
case NEW_BLOCK_BROADCAST_MESSAGE:
return NewBlockBroadcastMessage.fromProto(proto.getNewBlockBroadcastMessage(), messageVersion);
case ADD_PERSISTABLE_NETWORK_PAYLOAD_MESSAGE:
return AddPersistableNetworkPayloadMessage.fromProto(proto.getAddPersistableNetworkPayloadMessage(), this, messageVersion);
case ACK_MESSAGE:
@ -161,6 +169,27 @@ public class CoreNetworkProtoResolver extends CoreProtoResolver implements Netwo
case REPUBLISH_GOVERNANCE_DATA_REQUEST:
return RepublishGovernanceDataRequest.fromProto(proto.getRepublishGovernanceDataRequest(), messageVersion);
case NEW_DAO_STATE_HASH_MESSAGE:
return NewDaoStateHashMessage.fromProto(proto.getNewDaoStateHashMessage(), messageVersion);
case GET_DAO_STATE_HASHES_REQUEST:
return GetDaoStateHashesRequest.fromProto(proto.getGetDaoStateHashesRequest(), messageVersion);
case GET_DAO_STATE_HASHES_RESPONSE:
return GetDaoStateHashesResponse.fromProto(proto.getGetDaoStateHashesResponse(), messageVersion);
case NEW_PROPOSAL_STATE_HASH_MESSAGE:
return NewProposalStateHashMessage.fromProto(proto.getNewProposalStateHashMessage(), messageVersion);
case GET_PROPOSAL_STATE_HASHES_REQUEST:
return GetProposalStateHashesRequest.fromProto(proto.getGetProposalStateHashesRequest(), messageVersion);
case GET_PROPOSAL_STATE_HASHES_RESPONSE:
return GetProposalStateHashesResponse.fromProto(proto.getGetProposalStateHashesResponse(), messageVersion);
case NEW_BLIND_VOTE_STATE_HASH_MESSAGE:
return NewBlindVoteStateHashMessage.fromProto(proto.getNewBlindVoteStateHashMessage(), messageVersion);
case GET_BLIND_VOTE_STATE_HASHES_REQUEST:
return GetBlindVoteStateHashesRequest.fromProto(proto.getGetBlindVoteStateHashesRequest(), messageVersion);
case GET_BLIND_VOTE_STATE_HASHES_RESPONSE:
return GetBlindVoteStateHashesResponse.fromProto(proto.getGetBlindVoteStateHashesResponse(), messageVersion);
default:
throw new ProtobufferException("Unknown proto message case (PB.NetworkEnvelope). messageCase=" +
proto.getMessageCase() + "; proto raw data=" + proto.toString());

View file

@ -39,6 +39,7 @@ public class CoreNetworkCapabilities {
supportedCapabilities.add(Capability.PROPOSAL);
supportedCapabilities.add(Capability.BLIND_VOTE);
supportedCapabilities.add(Capability.BSQ_BLOCK);
supportedCapabilities.add(Capability.DAO_STATE);
String isFullDaoNode = bisqEnvironment.getProperty(DaoOptionKeys.FULL_DAO_NODE, String.class, "");
if (isFullDaoNode != null && !isFullDaoNode.isEmpty())

View file

@ -1260,6 +1260,7 @@ dao.tab.bsqWallet=BSQ wallet
dao.tab.proposals=Governance
dao.tab.bonding=Bonding
dao.tab.proofOfBurn=Asset listing fee/Proof of burn
dao.tab.monitor=Network monitor
dao.tab.news=News
dao.paidWithBsq=paid with BSQ
@ -1311,6 +1312,7 @@ dao.results.proposals.table.header.result=Vote result
dao.results.proposals.voting.detail.header=Vote results for selected proposal
dao.results.exceptions=Vote result exception(s)
# suppress inspection "UnusedProperty"
dao.param.UNDEFINED=Undefined
@ -1742,7 +1744,6 @@ dao.wallet.dashboard.burntTx=No. of all fee payments transactions
dao.wallet.dashboard.price=Latest BSQ/BTC trade price (in Bisq)
dao.wallet.dashboard.marketCap=Market capitalisation (based on trade price)
dao.wallet.receive.fundYourWallet=Your BSQ receive address
dao.wallet.receive.bsqAddress=BSQ wallet address (Fresh unused address)
@ -1857,6 +1858,51 @@ dao.news.DAOOnTestnet.fourthSection.title=4. Explore a BSQ Block Explorer
dao.news.DAOOnTestnet.fourthSection.content=Since BSQ is just bitcoin, you can see BSQ transactions on our bitcoin block explorer.
dao.news.DAOOnTestnet.readMoreLink=Read the full documentation
dao.monitor.daoState=DAO state
dao.monitor.proposals=Proposals state
dao.monitor.blindVotes=Blind votes state
dao.monitor.table.peers=Peers
dao.monitor.table.conflicts=Conflicts
dao.monitor.state=Status
dao.monitor.requestAlHashes=Request all hashes
dao.monitor.resync=Resync DAO state
dao.monitor.table.header.cycleBlockHeight=Cycle / block height
dao.monitor.table.cycleBlockHeight=Cycle {0} / block {1}
dao.monitor.daoState.headline=DAO state
dao.monitor.daoState.daoStateInSync=Your local DAO state is in consensus with the network
dao.monitor.daoState.daoStateNotInSync=Your local DAO state is not in consensus with the network. Please resync your \
DAO state.
dao.monitor.daoState.table.headline=Chain of DAO state hashes
dao.monitor.daoState.table.blockHeight=Block height
dao.monitor.daoState.table.hash=Hash of DAO state
dao.monitor.daoState.table.prev=Previous hash
dao.monitor.daoState.conflictTable.headline=DAO state hashes from peers in conflict
dao.monitor.proposal.headline=Proposals state
dao.monitor.proposal.daoStateInSync=Your local proposals state is in consensus with the network
dao.monitor.proposal.daoStateNotInSync=Your local proposals state is not in consensus with the network. Please restart your \
application.
dao.monitor.proposal.table.headline=Chain of proposal state hashes
dao.monitor.proposal.conflictTable.headline=Proposal state hashes from peers in conflict
dao.monitor.proposal.table.hash=Hash of proposal state
dao.monitor.proposal.table.prev=Previous hash
dao.monitor.proposal.table.numProposals=No. proposals
dao.monitor.blindVote.headline=Blind votes state
dao.monitor.blindVote.daoStateInSync=Your local blind votes state is in consensus with the network
dao.monitor.blindVote.daoStateNotInSync=Your local blind votes state is not in consensus with the network. Please restart your \
application.
dao.monitor.blindVote.table.headline=Chain of blind vote state hashes
dao.monitor.blindVote.conflictTable.headline=Blind vote state hashes from peers in conflict
dao.monitor.blindVote.table.hash=Hash of blind vote state
dao.monitor.blindVote.table.prev=Previous hash
dao.monitor.blindVote.table.numBlindVotes=No. blind votes
####################################################################
# Windows
####################################################################

View file

@ -38,6 +38,7 @@ public class DaoStateServiceTest {
);
Block block = new Block(0, 1534800000, "fakeblockhash0", null);
stateService.onNewBlockHeight(0);
stateService.onNewBlockWithEmptyTxs(block);
Assert.assertEquals(
"Block has to be genesis block to get added.",
@ -52,10 +53,13 @@ public class DaoStateServiceTest {
);
block = new Block(1, 1534800001, "fakeblockhash1", null);
stateService.onNewBlockHeight(1);
stateService.onNewBlockWithEmptyTxs(block);
block = new Block(2, 1534800002, "fakeblockhash2", null);
stateService.onNewBlockHeight(2);
stateService.onNewBlockWithEmptyTxs(block);
block = new Block(3, 1534800003, "fakeblockhash3", null);
stateService.onNewBlockHeight(3);
stateService.onNewBlockWithEmptyTxs(block);
Assert.assertEquals(
"Block that was never added should still not exist after adding more blocks.",

View file

@ -18,6 +18,7 @@
package bisq.core.dao.state;
import bisq.core.dao.governance.period.CycleService;
import bisq.core.dao.monitoring.DaoStateMonitoringService;
import org.powermock.core.classloader.annotations.PowerMockIgnore;
import org.powermock.core.classloader.annotations.PrepareForTest;
@ -33,7 +34,7 @@ import static org.junit.Assert.assertTrue;
import static org.powermock.api.mockito.PowerMockito.mock;
@RunWith(PowerMockRunner.class)
@PrepareForTest({DaoStateService.class, GenesisTxInfo.class, CycleService.class, DaoStateStorageService.class})
@PrepareForTest({DaoStateService.class, GenesisTxInfo.class, CycleService.class, DaoStateStorageService.class, DaoStateMonitoringService.class})
@PowerMockIgnore({"com.sun.org.apache.xerces.*", "javax.xml.*", "org.xml.*"})
public class DaoStateSnapshotServiceTest {
@ -44,7 +45,8 @@ public class DaoStateSnapshotServiceTest {
daoStateSnapshotService = new DaoStateSnapshotService(mock(DaoStateService.class),
mock(GenesisTxInfo.class),
mock(CycleService.class),
mock(DaoStateStorageService.class));
mock(DaoStateStorageService.class),
mock(DaoStateMonitoringService.class));
}
@Test

View file

@ -2049,6 +2049,14 @@ textfield */
-fx-fill: -fx-accent;
}
.dao-inSync {
-fx-text-fill: -bs-rd-green;
}
.dao-inConflict {
-fx-text-fill: -bs-rd-error-red;
}
/********************************************************************************************************************
* *
* Notifications *

View file

@ -28,6 +28,7 @@ import bisq.desktop.main.MainView;
import bisq.desktop.main.dao.bonding.BondingView;
import bisq.desktop.main.dao.burnbsq.BurnBsqView;
import bisq.desktop.main.dao.governance.GovernanceView;
import bisq.desktop.main.dao.monitor.MonitorView;
import bisq.desktop.main.dao.news.NewsView;
import bisq.desktop.main.dao.wallet.BsqWalletView;
import bisq.desktop.main.dao.wallet.dashboard.BsqDashboardView;
@ -52,7 +53,7 @@ import javafx.beans.value.ChangeListener;
public class DaoView extends ActivatableViewAndModel<TabPane, Activatable> {
@FXML
private Tab bsqWalletTab, proposalsTab, bondingTab, burnBsqTab, daoNewsTab;
private Tab bsqWalletTab, proposalsTab, bondingTab, burnBsqTab, daoNewsTab, monitor;
private Navigation.Listener navigationListener;
private ChangeListener<Tab> tabChangeListener;
@ -80,23 +81,26 @@ public class DaoView extends ActivatableViewAndModel<TabPane, Activatable> {
proposalsTab = new Tab(Res.get("dao.tab.proposals").toUpperCase());
bondingTab = new Tab(Res.get("dao.tab.bonding").toUpperCase());
burnBsqTab = new Tab(Res.get("dao.tab.proofOfBurn").toUpperCase());
monitor = new Tab(Res.get("dao.tab.monitor").toUpperCase());
bsqWalletTab.setClosable(false);
proposalsTab.setClosable(false);
bondingTab.setClosable(false);
burnBsqTab.setClosable(false);
monitor.setClosable(false);
if (!DevEnv.isDaoActivated()) {
bsqWalletTab.setDisable(true);
proposalsTab.setDisable(true);
bondingTab.setDisable(true);
burnBsqTab.setDisable(true);
bsqWalletTab.setDisable(true);
monitor.setDisable(true);
daoNewsTab = new Tab(Res.get("dao.tab.news").toUpperCase());
root.getTabs().add(daoNewsTab);
} else {
root.getTabs().addAll(bsqWalletTab, proposalsTab, bondingTab, burnBsqTab);
root.getTabs().addAll(bsqWalletTab, proposalsTab, bondingTab, burnBsqTab, monitor);
}
navigationListener = viewPath -> {
@ -121,6 +125,8 @@ public class DaoView extends ActivatableViewAndModel<TabPane, Activatable> {
navigation.navigateTo(MainView.class, DaoView.class, BondingView.class);
} else if (newValue == burnBsqTab) {
navigation.navigateTo(MainView.class, DaoView.class, BurnBsqView.class);
} else if (newValue == monitor) {
navigation.navigateTo(MainView.class, DaoView.class, MonitorView.class);
}
};
}
@ -141,6 +147,8 @@ public class DaoView extends ActivatableViewAndModel<TabPane, Activatable> {
navigation.navigateTo(MainView.class, DaoView.class, BondingView.class);
else if (selectedItem == burnBsqTab)
navigation.navigateTo(MainView.class, DaoView.class, BurnBsqView.class);
else if (selectedItem == monitor)
navigation.navigateTo(MainView.class, DaoView.class, MonitorView.class);
}
} else {
loadView(NewsView.class);
@ -173,6 +181,8 @@ public class DaoView extends ActivatableViewAndModel<TabPane, Activatable> {
selectedTab = bondingTab;
} else if (view instanceof BurnBsqView) {
selectedTab = burnBsqTab;
} else if (view instanceof MonitorView) {
selectedTab = monitor;
} else if (view instanceof NewsView) {
selectedTab = daoNewsTab;
}

View file

@ -107,6 +107,7 @@ public class BondsView extends ActivatableView<GridPane, Void> {
bondedReputationRepository.getBonds().addListener(bondedReputationListener);
bondedRolesRepository.getBonds().addListener(bondedRolesListener);
updateList();
GUIUtil.setFitToRowsForTableView(tableView, 37, 28, 2, 30);
}
@Override

View file

@ -189,6 +189,7 @@ public class MyReputationView extends ActivatableView<GridPane, Void> implements
setNewRandomSalt();
updateList();
GUIUtil.setFitToRowsForTableView(tableView, 41, 28, 2, 30);
}
@Override

View file

@ -103,6 +103,7 @@ public class RolesView extends ActivatableView<GridPane, Void> {
sortedList.comparatorProperty().bind(tableView.comparatorProperty());
daoFacade.getBondedRoles().addListener(bondedRoleListChangeListener);
updateList();
GUIUtil.setFitToRowsForTableView(tableView, 41, 28, 2, 30);
}
@Override

View file

@ -191,6 +191,7 @@ public class AssetFeeView extends ActivatableView<GridPane, Void> implements Bsq
});
updateList();
GUIUtil.setFitToRowsForTableView(tableView, 41, 28, 2, 10);
updateButtonState();
feeAmountInputTextField.resetValidation();

View file

@ -187,6 +187,8 @@ public class ProofOfBurnView extends ActivatableView<GridPane, Void> implements
preImageTextField.setValidator(new InputValidator());
updateList();
GUIUtil.setFitToRowsForTableView(myItemsTableView, 41, 28, 2, 4);
GUIUtil.setFitToRowsForTableView(allTxsTableView, 41, 28, 2, 10);
updateButtonState();
}

View file

@ -51,7 +51,6 @@ import java.util.List;
@FxmlView
public class GovernanceView extends ActivatableViewAndModel {
private final ViewLoader viewLoader;
private final Navigation navigation;
private final DaoFacade daoFacade;
@ -102,6 +101,7 @@ public class GovernanceView extends ActivatableViewAndModel {
ProposalsView.class, baseNavPath);
result = new MenuItem(navigation, toggleGroup, Res.get("dao.proposal.menuItem.result"),
VoteResultView.class, baseNavPath);
leftVBox.getChildren().addAll(dashboard, make, open, result);
}

View file

@ -184,8 +184,6 @@ public class ProposalsView extends ActivatableView<GridPane, Void> implements Bs
public void initialize() {
super.initialize();
root.getStyleClass().add("vote-root");
gridRow = phasesView.addGroup(root, gridRow);
proposalDisplayGridPane = new GridPane();
@ -226,6 +224,7 @@ public class ProposalsView extends ActivatableView<GridPane, Void> implements Bs
bsqWalletService.getUnlockingBondsBalance());
updateListItems();
GUIUtil.setFitToRowsForTableView(tableView, 38, 28, 2, 6);
updateViews();
}
@ -334,8 +333,6 @@ public class ProposalsView extends ActivatableView<GridPane, Void> implements Bs
}
GUIUtil.setFitToRowsForTableView(tableView, 38, 28, 2, 6);
tableView.layout();
root.layout();
}
private void createAllFieldsOnProposalDisplay(Proposal proposal, @Nullable Ballot ballot,

View file

@ -149,6 +149,7 @@ public class VoteResultView extends ActivatableView<GridPane, Void> implements D
private ChangeListener<CycleListItem> selectedVoteResultListItemListener;
private ResultsOfCycle resultsOfCycle;
private ProposalListItem selectedProposalListItem;
private TableView<VoteListItem> votesTableView;
///////////////////////////////////////////////////////////////////////////////////////////
@ -211,6 +212,13 @@ public class VoteResultView extends ActivatableView<GridPane, Void> implements D
JsonElement cyclesJsonArray = getVotingHistoryJson();
GUIUtil.exportJSON("voteResultsHistory.json", cyclesJsonArray, (Stage) root.getScene().getWindow());
});
if (proposalsTableView != null) {
GUIUtil.setFitToRowsForTableView(proposalsTableView, 25, 28, 2, 4);
}
if (votesTableView != null) {
GUIUtil.setFitToRowsForTableView(votesTableView, 25, 28, 2, 4);
}
GUIUtil.setFitToRowsForTableView(cyclesTableView, 25, 28, 2, 4);
}
@Override
@ -513,7 +521,7 @@ public class VoteResultView extends ActivatableView<GridPane, Void> implements D
GridPane.setColumnSpan(votesTableHeader, 2);
root.getChildren().add(votesTableHeader);
TableView<VoteListItem> votesTableView = new TableView<>();
votesTableView = new TableView<>();
votesTableView.setPlaceholder(new AutoTooltipLabel(Res.get("table.placeholder.noData")));
votesTableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);

View file

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ 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/>.
-->
<?import javafx.scene.control.ScrollPane?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.VBox?>
<AnchorPane fx:id="root" fx:controller="bisq.desktop.main.dao.monitor.MonitorView"
xmlns:fx="http://javafx.com/fxml">
<VBox fx:id="leftVBox" prefWidth="240" spacing="5" AnchorPane.bottomAnchor="20" AnchorPane.leftAnchor="15"
AnchorPane.topAnchor="15"/>
<ScrollPane fitToWidth="true" hbarPolicy="NEVER"
AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="270.0"
AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<AnchorPane fx:id="content" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0"
AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0"/>
</ScrollPane>
</AnchorPane>

View file

@ -0,0 +1,131 @@
/*
* 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.desktop.main.dao.monitor;
import bisq.desktop.Navigation;
import bisq.desktop.common.view.ActivatableViewAndModel;
import bisq.desktop.common.view.CachingViewLoader;
import bisq.desktop.common.view.FxmlView;
import bisq.desktop.common.view.View;
import bisq.desktop.common.view.ViewLoader;
import bisq.desktop.common.view.ViewPath;
import bisq.desktop.components.MenuItem;
import bisq.desktop.main.MainView;
import bisq.desktop.main.dao.DaoView;
import bisq.desktop.main.dao.monitor.blindvotes.BlindVoteStateMonitorView;
import bisq.desktop.main.dao.monitor.daostate.DaoStateMonitorView;
import bisq.desktop.main.dao.monitor.proposals.ProposalStateMonitorView;
import bisq.core.locale.Res;
import javax.inject.Inject;
import javafx.fxml.FXML;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.VBox;
import java.util.Arrays;
import java.util.List;
@FxmlView
public class MonitorView extends ActivatableViewAndModel {
private final ViewLoader viewLoader;
private final Navigation navigation;
private MenuItem daoState, proposals, blindVotes;
private Navigation.Listener navigationListener;
@FXML
private VBox leftVBox;
@FXML
private AnchorPane content;
private Class<? extends View> selectedViewClass;
private ToggleGroup toggleGroup;
@Inject
private MonitorView(CachingViewLoader viewLoader, Navigation navigation) {
this.viewLoader = viewLoader;
this.navigation = navigation;
}
@Override
public void initialize() {
navigationListener = viewPath -> {
if (viewPath.size() != 4 || viewPath.indexOf(MonitorView.class) != 2)
return;
selectedViewClass = viewPath.tip();
loadView(selectedViewClass);
};
toggleGroup = new ToggleGroup();
List<Class<? extends View>> baseNavPath = Arrays.asList(MainView.class, DaoView.class, MonitorView.class);
daoState = new MenuItem(navigation, toggleGroup, Res.get("dao.monitor.daoState"),
DaoStateMonitorView.class, baseNavPath);
proposals = new MenuItem(navigation, toggleGroup, Res.get("dao.monitor.proposals"),
ProposalStateMonitorView.class, baseNavPath);
blindVotes = new MenuItem(navigation, toggleGroup, Res.get("dao.monitor.blindVotes"),
BlindVoteStateMonitorView.class, baseNavPath);
leftVBox.getChildren().addAll(daoState, proposals, blindVotes);
}
@Override
protected void activate() {
proposals.activate();
blindVotes.activate();
daoState.activate();
navigation.addListener(navigationListener);
ViewPath viewPath = navigation.getCurrentPath();
if (viewPath.size() == 3 && viewPath.indexOf(MonitorView.class) == 2 ||
viewPath.size() == 2 && viewPath.indexOf(DaoView.class) == 1) {
if (selectedViewClass == null)
selectedViewClass = DaoStateMonitorView.class;
loadView(selectedViewClass);
} else if (viewPath.size() == 4 && viewPath.indexOf(MonitorView.class) == 2) {
selectedViewClass = viewPath.get(3);
loadView(selectedViewClass);
}
}
@SuppressWarnings("Duplicates")
@Override
protected void deactivate() {
navigation.removeListener(navigationListener);
proposals.deactivate();
blindVotes.deactivate();
daoState.deactivate();
}
private void loadView(Class<? extends View> viewClass) {
View view = viewLoader.load(viewClass);
content.getChildren().setAll(view.getRoot());
if (view instanceof DaoStateMonitorView) toggleGroup.selectToggle(daoState);
else if (view instanceof ProposalStateMonitorView) toggleGroup.selectToggle(proposals);
else if (view instanceof BlindVoteStateMonitorView) toggleGroup.selectToggle(blindVotes);
}
}

View file

@ -0,0 +1,52 @@
/*
* 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.desktop.main.dao.monitor;
import bisq.core.dao.monitoring.model.StateBlock;
import bisq.core.dao.monitoring.model.StateHash;
import bisq.core.locale.Res;
import bisq.common.util.Utilities;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Getter
@EqualsAndHashCode
public abstract class StateBlockListItem<StH extends StateHash, StB extends StateBlock<StH>> {
protected final StateBlock<StH> stateBlock;
protected final String height;
protected final String hash;
protected final String prevHash;
protected final String numNetworkMessages;
protected final String numMisMatches;
protected final boolean isInSync;
protected StateBlockListItem(StB stateBlock, int cycleIndex) {
this.stateBlock = stateBlock;
height = Res.get("dao.monitor.table.cycleBlockHeight", cycleIndex + 1, String.valueOf(stateBlock.getHeight()));
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);
isInSync = size == 0;
}
}

View file

@ -0,0 +1,47 @@
/*
* 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.desktop.main.dao.monitor;
import bisq.core.dao.monitoring.model.StateHash;
import bisq.core.locale.Res;
import bisq.common.util.Utilities;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Getter
@EqualsAndHashCode
public abstract class StateInConflictListItem<T extends StateHash> {
private final String peerAddress;
private final String height;
private final String hash;
private final String prevHash;
private final T stateHash;
protected StateInConflictListItem(String peerAddress, T stateHash, int cycleIndex) {
this.stateHash = stateHash;
this.peerAddress = peerAddress;
height = Res.get("dao.monitor.table.cycleBlockHeight", cycleIndex + 1, String.valueOf(stateHash.getHeight()));
hash = Utilities.bytesAsHexString(stateHash.getHash());
prevHash = stateHash.getPrevHash().length > 0 ?
Utilities.bytesAsHexString(stateHash.getPrevHash()) : "-";
}
}

View file

@ -0,0 +1,563 @@
/*
* 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.desktop.main.dao.monitor;
import bisq.desktop.common.view.ActivatableView;
import bisq.desktop.common.view.FxmlView;
import bisq.desktop.components.AutoTooltipButton;
import bisq.desktop.components.AutoTooltipLabel;
import bisq.desktop.components.AutoTooltipTableColumn;
import bisq.desktop.components.TableGroupHeadline;
import bisq.desktop.util.FormBuilder;
import bisq.desktop.util.GUIUtil;
import bisq.desktop.util.Layout;
import bisq.core.dao.DaoFacade;
import bisq.core.dao.governance.period.CycleService;
import bisq.core.dao.governance.period.PeriodService;
import bisq.core.dao.monitoring.model.StateBlock;
import bisq.core.dao.monitoring.model.StateHash;
import bisq.core.dao.state.DaoStateListener;
import bisq.core.dao.state.DaoStateService;
import bisq.core.locale.Res;
import javax.inject.Inject;
import de.jensd.fx.fontawesome.AwesomeIcon;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
import javafx.geometry.Insets;
import org.fxmisc.easybind.EasyBind;
import org.fxmisc.easybind.Subscription;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.SortedList;
import javafx.util.Callback;
import java.util.Comparator;
import java.util.Map;
import java.util.stream.Collectors;
@FxmlView
public abstract class StateMonitorView<StH extends StateHash,
StB extends StateBlock<StH>,
BLI extends StateBlockListItem<StH, StB>,
CLI extends StateInConflictListItem<StH>>
extends ActivatableView<GridPane, Void> implements DaoStateListener {
protected final DaoStateService daoStateService;
protected final DaoFacade daoFacade;
protected final CycleService cycleService;
protected final PeriodService periodService;
protected TextField statusTextField;
protected Button resyncButton;
protected TableView<BLI> tableView;
protected TableView<CLI> conflictTableView;
protected final ObservableList<BLI> listItems = FXCollections.observableArrayList();
private final SortedList<BLI> sortedList = new SortedList<>(listItems);
private final ObservableList<CLI> conflictListItems = FXCollections.observableArrayList();
private final SortedList<CLI> sortedConflictList = new SortedList<>(conflictListItems);
protected int gridRow = 0;
private Subscription selectedItemSubscription;
protected final BooleanProperty isInConflict = new SimpleBooleanProperty();
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor, lifecycle
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
public StateMonitorView(DaoStateService daoStateService,
DaoFacade daoFacade,
CycleService cycleService,
PeriodService periodService) {
this.daoStateService = daoStateService;
this.daoFacade = daoFacade;
this.cycleService = cycleService;
this.periodService = periodService;
}
@Override
public void initialize() {
createTableView();
createDetailsView();
}
@Override
protected void activate() {
selectedItemSubscription = EasyBind.subscribe(tableView.getSelectionModel().selectedItemProperty(), this::onSelectItem);
sortedList.comparatorProperty().bind(tableView.comparatorProperty());
sortedConflictList.comparatorProperty().bind(conflictTableView.comparatorProperty());
daoStateService.addDaoStateListener(this);
resyncButton.visibleProperty().bind(isInConflict);
resyncButton.managedProperty().bind(isInConflict);
if (daoStateService.isParseBlockChainComplete()) {
onDataUpdate();
}
GUIUtil.setFitToRowsForTableView(tableView, 25, 28, 2, 5);
GUIUtil.setFitToRowsForTableView(conflictTableView, 38, 28, 2, 4);
}
@Override
protected void deactivate() {
selectedItemSubscription.unsubscribe();
sortedList.comparatorProperty().unbind();
sortedConflictList.comparatorProperty().unbind();
daoStateService.removeDaoStateListener(this);
resyncButton.visibleProperty().unbind();
resyncButton.managedProperty().unbind();
resyncButton.setOnAction(null);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Abstract
///////////////////////////////////////////////////////////////////////////////////////////
protected abstract BLI getStateBlockListItem(StB e);
protected abstract CLI getStateInConflictListItem(Map.Entry<String, StH> mapEntry);
protected abstract void requestHashesFromGenesisBlockHeight(String peerAddress);
protected abstract String getConflictsTableHeader();
protected abstract String getPeersTableHeader();
protected abstract String getPrevHashTableHeader();
protected abstract String getHashTableHeader();
protected abstract String getBlockHeightTableHeader();
protected abstract String getRequestHashes();
protected abstract String getTableHeadLine();
protected abstract String getConflictTableHeadLine();
///////////////////////////////////////////////////////////////////////////////////////////
// DaoStateListener
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void onParseBlockChainComplete() {
onDataUpdate();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Create table views
///////////////////////////////////////////////////////////////////////////////////////////
private void createTableView() {
TableGroupHeadline headline = new TableGroupHeadline(getTableHeadLine());
GridPane.setRowIndex(headline, ++gridRow);
GridPane.setMargin(headline, new Insets(Layout.GROUP_DISTANCE, -10, -10, -10));
root.getChildren().add(headline);
tableView = new TableView<>();
tableView.setPlaceholder(new AutoTooltipLabel(Res.get("table.placeholder.noData")));
tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
tableView.setPrefHeight(100);
createColumns();
GridPane.setRowIndex(tableView, gridRow);
GridPane.setHgrow(tableView, Priority.ALWAYS);
GridPane.setMargin(tableView, new Insets(Layout.FIRST_ROW_AND_GROUP_DISTANCE, -10, -25, -10));
root.getChildren().add(tableView);
tableView.setItems(sortedList);
}
private void createDetailsView() {
TableGroupHeadline conflictTableHeadline = new TableGroupHeadline(getConflictTableHeadLine());
GridPane.setRowIndex(conflictTableHeadline, ++gridRow);
GridPane.setMargin(conflictTableHeadline, new Insets(Layout.GROUP_DISTANCE, -10, -10, -10));
root.getChildren().add(conflictTableHeadline);
conflictTableView = new TableView<>();
conflictTableView.setPlaceholder(new AutoTooltipLabel(Res.get("table.placeholder.noData")));
conflictTableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
conflictTableView.setPrefHeight(100);
createConflictColumns();
GridPane.setRowIndex(conflictTableView, gridRow);
GridPane.setHgrow(conflictTableView, Priority.ALWAYS);
GridPane.setMargin(conflictTableView, new Insets(Layout.FIRST_ROW_AND_GROUP_DISTANCE, -10, -25, -10));
root.getChildren().add(conflictTableView);
conflictTableView.setItems(sortedConflictList);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Handler
///////////////////////////////////////////////////////////////////////////////////////////
private void onSelectItem(BLI item) {
if (item != null) {
conflictListItems.setAll(item.getStateBlock().getInConflictMap().entrySet().stream()
.map(this::getStateInConflictListItem).collect(Collectors.toList()));
GUIUtil.setFitToRowsForTableView(conflictTableView, 38, 28, 2, 4);
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////
protected void onDataUpdate() {
GUIUtil.setFitToRowsForTableView(tableView, 25, 28, 2, 5);
}
///////////////////////////////////////////////////////////////////////////////////////////
// TableColumns
///////////////////////////////////////////////////////////////////////////////////////////
protected void createColumns() {
TableColumn<BLI, BLI> column;
column = new AutoTooltipTableColumn<>(getBlockHeightTableHeader());
column.setMinWidth(120);
column.getStyleClass().add("first-column");
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.getHeight());
else
setText("");
}
};
}
});
column.setComparator(Comparator.comparing(e -> e.getStateBlock().getHeight()));
column.setSortType(TableColumn.SortType.DESCENDING);
tableView.getSortOrder().add(column);
tableView.getColumns().add(column);
column = new AutoTooltipTableColumn<>(getHashTableHeader());
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.getHash());
else
setText("");
}
};
}
});
column.setComparator(Comparator.comparing(BLI::getHash));
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());
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.getNumNetworkMessages());
else
setText("");
}
};
}
});
column.setComparator(Comparator.comparing(e -> e.getStateBlock().getPeersMap().size()));
tableView.getColumns().add(column);
column = new AutoTooltipTableColumn<>(getConflictsTableHeader());
column.setMinWidth(80);
column.setMaxWidth(column.getMinWidth());
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.getNumMisMatches());
else
setText("");
}
};
}
});
column.setComparator(Comparator.comparing(e -> e.getStateBlock().getInConflictMap().size()));
tableView.getColumns().add(column);
column = new AutoTooltipTableColumn<>("");
column.setMinWidth(40);
column.setMaxWidth(column.getMinWidth());
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 && !empty) {
Label icon;
if (!item.getStateBlock().getPeersMap().isEmpty()) {
if (item.isInSync()) {
icon = FormBuilder.getIcon(AwesomeIcon.OK_CIRCLE);
icon.getStyleClass().addAll("icon", "dao-inSync");
} else {
icon = FormBuilder.getIcon(AwesomeIcon.REMOVE_CIRCLE);
icon.getStyleClass().addAll("icon", "dao-inConflict");
}
setGraphic(icon);
} else {
setGraphic(null);
}
} else {
setGraphic(null);
}
}
};
}
});
column.setSortable(false);
tableView.getColumns().add(column);
}
protected void createConflictColumns() {
TableColumn<CLI, CLI> column;
column = new AutoTooltipTableColumn<>(getBlockHeightTableHeader());
column.setMinWidth(120);
column.getStyleClass().add("first-column");
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.getHeight());
else
setText("");
}
};
}
});
column.setComparator(Comparator.comparing(e -> e.getStateHash().getHeight()));
column.setSortType(TableColumn.SortType.DESCENDING);
conflictTableView.getColumns().add(column);
conflictTableView.getSortOrder().add(column);
column = new AutoTooltipTableColumn<>(getPeersTableHeader());
column.setMinWidth(80);
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.getPeerAddress());
else
setText("");
}
};
}
});
column.setComparator(Comparator.comparing(CLI::getPeerAddress));
conflictTableView.getColumns().add(column);
column = new AutoTooltipTableColumn<>(getHashTableHeader());
column.setMinWidth(150);
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.getHash());
else
setText("");
}
};
}
});
column.setComparator(Comparator.comparing(CLI::getHash));
conflictTableView.getColumns().add(column);
column = new AutoTooltipTableColumn<>(getPrevHashTableHeader());
column.setMinWidth(150);
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(100);
column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue()));
column.setCellFactory(
new Callback<>() {
@Override
public TableCell<CLI, CLI> call(
TableColumn<CLI, CLI> column) {
return new TableCell<>() {
Button button;
@Override
public void updateItem(final CLI item, boolean empty) {
super.updateItem(item, empty);
if (item != null && !empty) {
if (button == null) {
button = new AutoTooltipButton(getRequestHashes());
setGraphic(button);
}
button.setOnAction(e -> requestHashesFromGenesisBlockHeight(item.getPeerAddress()));
} else {
setGraphic(null);
if (button != null) {
button.setOnAction(null);
button = null;
}
}
}
};
}
});
column.setSortable(false);
conflictTableView.getColumns().add(column);
}
}

View file

@ -0,0 +1,40 @@
/*
* 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.desktop.main.dao.monitor.blindvotes;
import bisq.desktop.main.dao.monitor.StateBlockListItem;
import bisq.core.dao.monitoring.model.BlindVoteStateBlock;
import bisq.core.dao.monitoring.model.BlindVoteStateHash;
import lombok.EqualsAndHashCode;
import lombok.Value;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Value
@EqualsAndHashCode(callSuper = true)
class BlindVoteStateBlockListItem extends StateBlockListItem<BlindVoteStateHash, BlindVoteStateBlock> {
private final String numBlindVotes;
BlindVoteStateBlockListItem(BlindVoteStateBlock stateBlock, int cycleIndex) {
super(stateBlock, cycleIndex);
numBlindVotes = String.valueOf(stateBlock.getNumBlindVotes());
}
}

View file

@ -0,0 +1,39 @@
/*
* 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.desktop.main.dao.monitor.blindvotes;
import bisq.desktop.main.dao.monitor.StateInConflictListItem;
import bisq.core.dao.monitoring.model.BlindVoteStateHash;
import lombok.EqualsAndHashCode;
import lombok.Value;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Value
@EqualsAndHashCode(callSuper = true)
class BlindVoteStateInConflictListItem extends StateInConflictListItem<BlindVoteStateHash> {
private final String numBlindVotes;
BlindVoteStateInConflictListItem(String peerAddress, BlindVoteStateHash stateHash, int cycleIndex) {
super(peerAddress, stateHash, cycleIndex);
numBlindVotes = String.valueOf(stateHash.getNumBlindVotes());
}
}

View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ 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/>.
-->
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<GridPane fx:id="root" fx:controller="bisq.desktop.main.dao.monitor.blindvotes.BlindVoteStateMonitorView"
hgap="5.0" vgap="5.0"
AnchorPane.bottomAnchor="20.0" AnchorPane.leftAnchor="20.0"
AnchorPane.rightAnchor="25.0" AnchorPane.topAnchor="20.0"
xmlns:fx="http://javafx.com/fxml">
<columnConstraints>
<ColumnConstraints percentWidth="100"/>
</columnConstraints>
</GridPane>

View file

@ -0,0 +1,257 @@
/*
* 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.desktop.main.dao.monitor.blindvotes;
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;
import bisq.core.dao.DaoFacade;
import bisq.core.dao.governance.period.CycleService;
import bisq.core.dao.governance.period.PeriodService;
import bisq.core.dao.monitoring.BlindVoteStateMonitoringService;
import bisq.core.dao.monitoring.model.BlindVoteStateBlock;
import bisq.core.dao.monitoring.model.BlindVoteStateHash;
import bisq.core.dao.state.DaoStateService;
import bisq.core.locale.Res;
import javax.inject.Inject;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.util.Callback;
import java.util.Comparator;
import java.util.Map;
import java.util.stream.Collectors;
@FxmlView
public class BlindVoteStateMonitorView extends StateMonitorView<BlindVoteStateHash, BlindVoteStateBlock, BlindVoteStateBlockListItem, BlindVoteStateInConflictListItem>
implements BlindVoteStateMonitoringService.Listener {
private final BlindVoteStateMonitoringService blindVoteStateMonitoringService;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor, lifecycle
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
private BlindVoteStateMonitorView(DaoStateService daoStateService,
DaoFacade daoFacade,
BlindVoteStateMonitoringService blindVoteStateMonitoringService,
CycleService cycleService,
PeriodService periodService) {
super(daoStateService, daoFacade, cycleService, periodService);
this.blindVoteStateMonitoringService = blindVoteStateMonitoringService;
}
@Override
public void initialize() {
FormBuilder.addTitledGroupBg(root, gridRow, 3, Res.get("dao.monitor.blindVote.headline"));
statusTextField = FormBuilder.addTopLabelTextField(root, ++gridRow,
Res.get("dao.monitor.state")).second;
resyncButton = FormBuilder.addButton(root, ++gridRow, Res.get("dao.monitor.resync"), 10);
super.initialize();
}
@Override
protected void activate() {
super.activate();
blindVoteStateMonitoringService.addListener(this);
resyncButton.setOnAction(e -> daoFacade.resyncDao(() ->
new Popup<>().attention(Res.get("setting.preferences.dao.resync.popup"))
.useShutDownButton()
.hideCloseButton()
.show())
);
}
@Override
protected void deactivate() {
super.deactivate();
blindVoteStateMonitoringService.removeListener(this);
}
///////////////////////////////////////////////////////////////////////////////////////////
// BlindVoteStateMonitoringService.Listener
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void onBlindVoteStateBlockChainChanged() {
if (daoStateService.isParseBlockChainComplete()) {
onDataUpdate();
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// Implementation abstract methods
///////////////////////////////////////////////////////////////////////////////////////////
@Override
protected BlindVoteStateBlockListItem getStateBlockListItem(BlindVoteStateBlock daoStateBlock) {
int cycleIndex = periodService.getCycle(daoStateBlock.getHeight()).map(cycleService::getCycleIndex).orElse(0);
return new BlindVoteStateBlockListItem(daoStateBlock, cycleIndex);
}
@Override
protected BlindVoteStateInConflictListItem getStateInConflictListItem(Map.Entry<String, BlindVoteStateHash> mapEntry) {
BlindVoteStateHash blindVoteStateHash = mapEntry.getValue();
int cycleIndex = periodService.getCycle(blindVoteStateHash.getHeight()).map(cycleService::getCycleIndex).orElse(0);
return new BlindVoteStateInConflictListItem(mapEntry.getKey(), mapEntry.getValue(), cycleIndex);
}
@Override
protected String getTableHeadLine() {
return Res.get("dao.monitor.blindVote.table.headline");
}
@Override
protected String getConflictTableHeadLine() {
return Res.get("dao.monitor.blindVote.conflictTable.headline");
}
@Override
protected String getConflictsTableHeader() {
return Res.get("dao.monitor.table.conflicts");
}
@Override
protected String getPeersTableHeader() {
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");
}
@Override
protected String getBlockHeightTableHeader() {
return Res.get("dao.monitor.table.header.cycleBlockHeight");
}
@Override
protected String getRequestHashes() {
return Res.get("dao.monitor.requestAlHashes");
}
///////////////////////////////////////////////////////////////////////////////////////////
// Override
///////////////////////////////////////////////////////////////////////////////////////////
@Override
protected void onDataUpdate() {
isInConflict.set(blindVoteStateMonitoringService.isInConflict());
if (isInConflict.get()) {
statusTextField.setText(Res.get("dao.monitor.blindVote.daoStateNotInSync"));
statusTextField.getStyleClass().add("dao-inConflict");
} else {
statusTextField.setText(Res.get("dao.monitor.blindVote.daoStateInSync"));
statusTextField.getStyleClass().remove("dao-inConflict");
}
listItems.setAll(blindVoteStateMonitoringService.getBlindVoteStateBlockChain().stream()
.map(this::getStateBlockListItem)
.collect(Collectors.toList()));
super.onDataUpdate();
}
@Override
protected void requestHashesFromGenesisBlockHeight(String peerAddress) {
blindVoteStateMonitoringService.requestHashesFromGenesisBlockHeight(peerAddress);
}
@Override
protected void createColumns() {
super.createColumns();
TableColumn<BlindVoteStateBlockListItem, BlindVoteStateBlockListItem> column;
column = new AutoTooltipTableColumn<>(Res.get("dao.monitor.blindVote.table.numBlindVotes"));
column.setMinWidth(110);
column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue()));
column.setCellFactory(
new Callback<>() {
@Override
public TableCell<BlindVoteStateBlockListItem, BlindVoteStateBlockListItem> call(
TableColumn<BlindVoteStateBlockListItem, BlindVoteStateBlockListItem> column) {
return new TableCell<>() {
@Override
public void updateItem(BlindVoteStateBlockListItem item, boolean empty) {
super.updateItem(item, empty);
if (item != null)
setText(item.getNumBlindVotes());
else
setText("");
}
};
}
});
column.setComparator(Comparator.comparing(e -> e.getStateBlock().getMyStateHash().getNumBlindVotes()));
tableView.getColumns().add(1, column);
}
protected void createConflictColumns() {
super.createConflictColumns();
TableColumn<BlindVoteStateInConflictListItem, BlindVoteStateInConflictListItem> column;
column = new AutoTooltipTableColumn<>(Res.get("dao.monitor.blindVote.table.numBlindVotes"));
column.setMinWidth(110);
column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue()));
column.setCellFactory(
new Callback<>() {
@Override
public TableCell<BlindVoteStateInConflictListItem, BlindVoteStateInConflictListItem> call(
TableColumn<BlindVoteStateInConflictListItem, BlindVoteStateInConflictListItem> column) {
return new TableCell<>() {
@Override
public void updateItem(BlindVoteStateInConflictListItem item, boolean empty) {
super.updateItem(item, empty);
if (item != null)
setText(item.getNumBlindVotes());
else
setText("");
}
};
}
});
column.setComparator(Comparator.comparing(e -> e.getStateHash().getNumBlindVotes()));
conflictTableView.getColumns().add(1, column);
}
}

View file

@ -0,0 +1,37 @@
/*
* 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.desktop.main.dao.monitor.daostate;
import bisq.desktop.main.dao.monitor.StateBlockListItem;
import bisq.core.dao.monitoring.model.DaoStateBlock;
import bisq.core.dao.monitoring.model.DaoStateHash;
import lombok.EqualsAndHashCode;
import lombok.Value;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Value
@EqualsAndHashCode(callSuper = true)
class DaoStateBlockListItem extends StateBlockListItem<DaoStateHash, DaoStateBlock> {
DaoStateBlockListItem(DaoStateBlock stateBlock, int cycleIndex) {
super(stateBlock, cycleIndex);
}
}

View file

@ -0,0 +1,35 @@
/*
* 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.desktop.main.dao.monitor.daostate;
import bisq.desktop.main.dao.monitor.StateInConflictListItem;
import bisq.core.dao.monitoring.model.DaoStateHash;
import lombok.EqualsAndHashCode;
import lombok.Value;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Value
@EqualsAndHashCode(callSuper = true)
class DaoStateInConflictListItem extends StateInConflictListItem<DaoStateHash> {
DaoStateInConflictListItem(String peerAddress, DaoStateHash stateHash, int cycleIndex) {
super(peerAddress, stateHash, cycleIndex);
}
}

View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ 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/>.
-->
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<GridPane fx:id="root" fx:controller="bisq.desktop.main.dao.monitor.daostate.DaoStateMonitorView"
hgap="5.0" vgap="5.0"
AnchorPane.bottomAnchor="20.0" AnchorPane.leftAnchor="20.0"
AnchorPane.rightAnchor="25.0" AnchorPane.topAnchor="20.0"
xmlns:fx="http://javafx.com/fxml">
<columnConstraints>
<ColumnConstraints percentWidth="100"/>
</columnConstraints>
</GridPane>

View file

@ -0,0 +1,188 @@
/*
* 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.desktop.main.dao.monitor.daostate;
import bisq.desktop.common.view.FxmlView;
import bisq.desktop.main.dao.monitor.StateMonitorView;
import bisq.desktop.main.overlays.popups.Popup;
import bisq.desktop.util.FormBuilder;
import bisq.core.dao.DaoFacade;
import bisq.core.dao.governance.period.CycleService;
import bisq.core.dao.governance.period.PeriodService;
import bisq.core.dao.monitoring.DaoStateMonitoringService;
import bisq.core.dao.monitoring.model.DaoStateBlock;
import bisq.core.dao.monitoring.model.DaoStateHash;
import bisq.core.dao.state.DaoStateService;
import bisq.core.locale.Res;
import javax.inject.Inject;
import java.util.Map;
import java.util.stream.Collectors;
@FxmlView
public class DaoStateMonitorView extends StateMonitorView<DaoStateHash, DaoStateBlock, DaoStateBlockListItem, DaoStateInConflictListItem>
implements DaoStateMonitoringService.Listener {
private final DaoStateMonitoringService daoStateMonitoringService;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor, lifecycle
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
private DaoStateMonitorView(DaoStateService daoStateService,
DaoFacade daoFacade,
DaoStateMonitoringService daoStateMonitoringService,
CycleService cycleService,
PeriodService periodService) {
super(daoStateService, daoFacade, cycleService, periodService);
this.daoStateMonitoringService = daoStateMonitoringService;
}
@Override
public void initialize() {
FormBuilder.addTitledGroupBg(root, gridRow, 3, Res.get("dao.monitor.daoState.headline"));
statusTextField = FormBuilder.addTopLabelTextField(root, ++gridRow,
Res.get("dao.monitor.state")).second;
resyncButton = FormBuilder.addButton(root, ++gridRow, Res.get("dao.monitor.resync"), 10);
super.initialize();
}
@Override
protected void activate() {
super.activate();
daoStateMonitoringService.addListener(this);
resyncButton.setOnAction(e -> daoFacade.resyncDao(() ->
new Popup<>().attention(Res.get("setting.preferences.dao.resync.popup"))
.useShutDownButton()
.hideCloseButton()
.show())
);
}
@Override
protected void deactivate() {
super.deactivate();
daoStateMonitoringService.removeListener(this);
}
///////////////////////////////////////////////////////////////////////////////////////////
// DaoStateMonitoringService.Listener
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void onChangeAfterBatchProcessing() {
if (daoStateService.isParseBlockChainComplete()) {
onDataUpdate();
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// Implementation abstract methods
///////////////////////////////////////////////////////////////////////////////////////////
@Override
protected DaoStateBlockListItem getStateBlockListItem(DaoStateBlock daoStateBlock) {
int cycleIndex = periodService.getCycle(daoStateBlock.getHeight()).map(cycleService::getCycleIndex).orElse(0);
return new DaoStateBlockListItem(daoStateBlock, cycleIndex);
}
@Override
protected DaoStateInConflictListItem getStateInConflictListItem(Map.Entry<String, DaoStateHash> mapEntry) {
DaoStateHash daoStateHash = mapEntry.getValue();
int cycleIndex = periodService.getCycle(daoStateHash.getHeight()).map(cycleService::getCycleIndex).orElse(0);
return new DaoStateInConflictListItem(mapEntry.getKey(), daoStateHash, cycleIndex);
}
@Override
protected String getTableHeadLine() {
return Res.get("dao.monitor.daoState.table.headline");
}
@Override
protected String getConflictTableHeadLine() {
return Res.get("dao.monitor.daoState.conflictTable.headline");
}
@Override
protected String getConflictsTableHeader() {
return Res.get("dao.monitor.table.conflicts");
}
@Override
protected String getPeersTableHeader() {
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");
}
@Override
protected String getBlockHeightTableHeader() {
return Res.get("dao.monitor.daoState.table.blockHeight");
}
@Override
protected String getRequestHashes() {
return Res.get("dao.monitor.requestAlHashes");
}
///////////////////////////////////////////////////////////////////////////////////////////
// Override
///////////////////////////////////////////////////////////////////////////////////////////
@Override
protected void onDataUpdate() {
isInConflict.set(daoStateMonitoringService.isInConflict());
if (isInConflict.get()) {
statusTextField.setText(Res.get("dao.monitor.daoState.daoStateNotInSync"));
statusTextField.getStyleClass().add("dao-inConflict");
} else {
statusTextField.setText(Res.get("dao.monitor.daoState.daoStateInSync"));
statusTextField.getStyleClass().remove("dao-inConflict");
}
listItems.setAll(daoStateMonitoringService.getDaoStateBlockChain().stream()
.map(this::getStateBlockListItem)
.collect(Collectors.toList()));
super.onDataUpdate();
}
@Override
protected void requestHashesFromGenesisBlockHeight(String peerAddress) {
daoStateMonitoringService.requestHashesFromGenesisBlockHeight(peerAddress);
}
}

View file

@ -0,0 +1,40 @@
/*
* 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.desktop.main.dao.monitor.proposals;
import bisq.desktop.main.dao.monitor.StateBlockListItem;
import bisq.core.dao.monitoring.model.ProposalStateBlock;
import bisq.core.dao.monitoring.model.ProposalStateHash;
import lombok.EqualsAndHashCode;
import lombok.Value;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Value
@EqualsAndHashCode(callSuper = true)
class ProposalStateBlockListItem extends StateBlockListItem<ProposalStateHash, ProposalStateBlock> {
private final String numProposals;
ProposalStateBlockListItem(ProposalStateBlock stateBlock, int cycleIndex) {
super(stateBlock, cycleIndex);
numProposals = String.valueOf(stateBlock.getNumProposals());
}
}

View file

@ -0,0 +1,39 @@
/*
* 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.desktop.main.dao.monitor.proposals;
import bisq.desktop.main.dao.monitor.StateInConflictListItem;
import bisq.core.dao.monitoring.model.ProposalStateHash;
import lombok.EqualsAndHashCode;
import lombok.Value;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Value
@EqualsAndHashCode(callSuper = true)
class ProposalStateInConflictListItem extends StateInConflictListItem<ProposalStateHash> {
private final String numProposals;
ProposalStateInConflictListItem(String peerAddress, ProposalStateHash stateHash, int cycleIndex) {
super(peerAddress, stateHash, cycleIndex);
numProposals = String.valueOf(stateHash.getNumProposals());
}
}

View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ 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/>.
-->
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<GridPane fx:id="root" fx:controller="bisq.desktop.main.dao.monitor.proposals.ProposalStateMonitorView"
hgap="5.0" vgap="5.0"
AnchorPane.bottomAnchor="20.0" AnchorPane.leftAnchor="20.0"
AnchorPane.rightAnchor="25.0" AnchorPane.topAnchor="20.0"
xmlns:fx="http://javafx.com/fxml">
<columnConstraints>
<ColumnConstraints percentWidth="100"/>
</columnConstraints>
</GridPane>

View file

@ -0,0 +1,257 @@
/*
* 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.desktop.main.dao.monitor.proposals;
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;
import bisq.core.dao.DaoFacade;
import bisq.core.dao.governance.period.CycleService;
import bisq.core.dao.governance.period.PeriodService;
import bisq.core.dao.monitoring.ProposalStateMonitoringService;
import bisq.core.dao.monitoring.model.ProposalStateBlock;
import bisq.core.dao.monitoring.model.ProposalStateHash;
import bisq.core.dao.state.DaoStateService;
import bisq.core.locale.Res;
import javax.inject.Inject;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.util.Callback;
import java.util.Comparator;
import java.util.Map;
import java.util.stream.Collectors;
@FxmlView
public class ProposalStateMonitorView extends StateMonitorView<ProposalStateHash, ProposalStateBlock, ProposalStateBlockListItem, ProposalStateInConflictListItem>
implements ProposalStateMonitoringService.Listener {
private final ProposalStateMonitoringService proposalStateMonitoringService;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor, lifecycle
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
private ProposalStateMonitorView(DaoStateService daoStateService,
DaoFacade daoFacade,
ProposalStateMonitoringService proposalStateMonitoringService,
CycleService cycleService,
PeriodService periodService) {
super(daoStateService, daoFacade, cycleService, periodService);
this.proposalStateMonitoringService = proposalStateMonitoringService;
}
@Override
public void initialize() {
FormBuilder.addTitledGroupBg(root, gridRow, 3, Res.get("dao.monitor.proposal.headline"));
statusTextField = FormBuilder.addTopLabelTextField(root, ++gridRow,
Res.get("dao.monitor.state")).second;
resyncButton = FormBuilder.addButton(root, ++gridRow, Res.get("dao.monitor.resync"), 10);
super.initialize();
}
@Override
protected void activate() {
super.activate();
proposalStateMonitoringService.addListener(this);
resyncButton.setOnAction(e -> daoFacade.resyncDao(() ->
new Popup<>().attention(Res.get("setting.preferences.dao.resync.popup"))
.useShutDownButton()
.hideCloseButton()
.show())
);
}
@Override
protected void deactivate() {
super.deactivate();
proposalStateMonitoringService.removeListener(this);
}
///////////////////////////////////////////////////////////////////////////////////////////
// ProposalStateMonitoringService.Listener
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void onProposalStateBlockChainChanged() {
if (daoStateService.isParseBlockChainComplete()) {
onDataUpdate();
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// Implementation abstract methods
///////////////////////////////////////////////////////////////////////////////////////////
@Override
protected ProposalStateBlockListItem getStateBlockListItem(ProposalStateBlock daoStateBlock) {
int cycleIndex = periodService.getCycle(daoStateBlock.getHeight()).map(cycleService::getCycleIndex).orElse(0);
return new ProposalStateBlockListItem(daoStateBlock, cycleIndex);
}
@Override
protected ProposalStateInConflictListItem getStateInConflictListItem(Map.Entry<String, ProposalStateHash> mapEntry) {
ProposalStateHash proposalStateHash = mapEntry.getValue();
int cycleIndex = periodService.getCycle(proposalStateHash.getHeight()).map(cycleService::getCycleIndex).orElse(0);
return new ProposalStateInConflictListItem(mapEntry.getKey(), mapEntry.getValue(), cycleIndex);
}
@Override
protected String getTableHeadLine() {
return Res.get("dao.monitor.proposal.table.headline");
}
@Override
protected String getConflictTableHeadLine() {
return Res.get("dao.monitor.proposal.conflictTable.headline");
}
@Override
protected String getConflictsTableHeader() {
return Res.get("dao.monitor.table.conflicts");
}
@Override
protected String getPeersTableHeader() {
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");
}
@Override
protected String getBlockHeightTableHeader() {
return Res.get("dao.monitor.table.header.cycleBlockHeight");
}
@Override
protected String getRequestHashes() {
return Res.get("dao.monitor.requestAlHashes");
}
///////////////////////////////////////////////////////////////////////////////////////////
// Override
///////////////////////////////////////////////////////////////////////////////////////////
@Override
protected void onDataUpdate() {
isInConflict.set(proposalStateMonitoringService.isInConflict());
if (isInConflict.get()) {
statusTextField.setText(Res.get("dao.monitor.proposal.daoStateNotInSync"));
statusTextField.getStyleClass().add("dao-inConflict");
} else {
statusTextField.setText(Res.get("dao.monitor.proposal.daoStateInSync"));
statusTextField.getStyleClass().remove("dao-inConflict");
}
listItems.setAll(proposalStateMonitoringService.getProposalStateBlockChain().stream()
.map(this::getStateBlockListItem)
.collect(Collectors.toList()));
super.onDataUpdate();
}
@Override
protected void requestHashesFromGenesisBlockHeight(String peerAddress) {
proposalStateMonitoringService.requestHashesFromGenesisBlockHeight(peerAddress);
}
@Override
protected void createColumns() {
super.createColumns();
TableColumn<ProposalStateBlockListItem, ProposalStateBlockListItem> column;
column = new AutoTooltipTableColumn<>(Res.get("dao.monitor.proposal.table.numProposals"));
column.setMinWidth(110);
column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue()));
column.setCellFactory(
new Callback<>() {
@Override
public TableCell<ProposalStateBlockListItem, ProposalStateBlockListItem> call(
TableColumn<ProposalStateBlockListItem, ProposalStateBlockListItem> column) {
return new TableCell<>() {
@Override
public void updateItem(ProposalStateBlockListItem item, boolean empty) {
super.updateItem(item, empty);
if (item != null)
setText(item.getNumProposals());
else
setText("");
}
};
}
});
column.setComparator(Comparator.comparing(e -> e.getStateBlock().getMyStateHash().getNumProposals()));
tableView.getColumns().add(1, column);
}
protected void createConflictColumns() {
super.createConflictColumns();
TableColumn<ProposalStateInConflictListItem, ProposalStateInConflictListItem> column;
column = new AutoTooltipTableColumn<>(Res.get("dao.monitor.proposal.table.numProposals"));
column.setMinWidth(110);
column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue()));
column.setCellFactory(
new Callback<>() {
@Override
public TableCell<ProposalStateInConflictListItem, ProposalStateInConflictListItem> call(
TableColumn<ProposalStateInConflictListItem, ProposalStateInConflictListItem> column) {
return new TableCell<>() {
@Override
public void updateItem(ProposalStateInConflictListItem item, boolean empty) {
super.updateItem(item, empty);
if (item != null)
setText(item.getNumProposals());
else
setText("");
}
};
}
});
column.setComparator(Comparator.comparing(e -> e.getStateHash().getNumProposals()));
conflictTableView.getColumns().add(1, column);
}
}

View file

@ -297,7 +297,9 @@ public class Connection extends Capabilities implements Runnable, MessageListene
}
if (!result)
log.info("We did not send the message because the peer does not support our required capabilities. message={}, peers supportedCapabilities={}", msg, capabilities);
log.info("We did not send the message because the peer does not support our required capabilities. " +
"message={}, peer={}, peers supportedCapabilities={}",
msg, peersNodeAddressOptional, capabilities);
return result;
}

Binary file not shown.