Skip to content

Commit

Permalink
fix(core-p2p): fix rollback condition (#4432)
Browse files Browse the repository at this point in the history
* Fix rollback condition

* Check each fork height once

* Better variable names

* Fix checkNetworkHealth test

* Move blocksToRollback variable

* Better rollback tests

* Improve test readability

* Test roolback when peers are below common height

Co-authored-by: Sebastijan K <58827427+sebastijankuzner@users.noreply.github.com>
  • Loading branch information
rainydio and sebastijankuzner authored Jul 7, 2021
1 parent 39c2fa9 commit 8cde761
Show file tree
Hide file tree
Showing 2 changed files with 165 additions and 91 deletions.
189 changes: 130 additions & 59 deletions __tests__/unit/core-p2p/network-monitor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -568,88 +568,159 @@ describe("NetworkMonitor", () => {
});

describe("checkNetworkHealth", () => {
describe("when we have 0 peer", () => {
beforeEach(() => {
repository.getPeers = jest.fn().mockReturnValue([]);
});
afterEach(() => {
repository.getPeers = jest.fn();
});
it("should not rollback when there are no verified peers", async () => {
const peers = [
new Peer("180.177.54.4", 4000),
new Peer("180.177.54.4", 4000),
new Peer("180.177.54.4", 4000),
new Peer("180.177.54.4", 4000),
new Peer("180.177.54.4", 4000),
new Peer("180.177.54.4", 4000),
new Peer("180.177.54.4", 4000),
new Peer("180.177.54.4", 4000),
new Peer("180.177.54.4", 4000),
];

it("should return {forked: false}", async () => {
const networkStatus = await networkMonitor.checkNetworkHealth();
try {
repository.getPeers.mockReturnValue(peers);

const networkStatus = await networkMonitor.checkNetworkHealth();
expect(networkStatus).toEqual({ forked: false });
});
} finally {
repository.getPeers.mockReset();
}
});

describe("when majority of our peers is on our chain", () => {
it("should rollback ignoring peers how are below common height", async () => {
// 105 (4 peers)
// /
// 90 (3 peers) ... 100 ... 103 (2 peers and us)

const lastBlock = { data: { height: 103 } };

const peers = [
new Peer("180.177.54.4", 4000),
new Peer("181.177.54.4", 4000),
new Peer("182.177.54.4", 4000),
new Peer("183.177.54.4", 4000),
new Peer("184.177.54.4", 4000),
new Peer("185.177.54.4", 4000),
new Peer("186.177.54.4", 4000),
new Peer("187.177.54.4", 4000),
new Peer("188.177.54.4", 4000),
new Peer("189.177.54.4", 4000),
new Peer("180.177.54.4", 4000),
new Peer("180.177.54.4", 4000),
new Peer("180.177.54.4", 4000),
new Peer("180.177.54.4", 4000),
new Peer("180.177.54.4", 4000),
new Peer("180.177.54.4", 4000),
new Peer("180.177.54.4", 4000),
new Peer("180.177.54.4", 4000),
];
// 4 peers are forked out of 10
peers[0].verificationResult = new PeerVerificationResult(3, 4, 2);
peers[1].verificationResult = new PeerVerificationResult(3, 4, 2);
peers[2].verificationResult = new PeerVerificationResult(3, 4, 2);
peers[3].verificationResult = new PeerVerificationResult(3, 4, 2);

beforeEach(() => {
repository.getPeers = jest.fn().mockReturnValue(peers);
});
afterEach(() => {
repository.getPeers = jest.fn();
});
it("should return {forked: false}", async () => {
peers[0].verificationResult = new PeerVerificationResult(103, 90, 90);
peers[1].verificationResult = new PeerVerificationResult(103, 90, 90);
peers[2].verificationResult = new PeerVerificationResult(103, 90, 90);

peers[3].verificationResult = new PeerVerificationResult(103, 105, 100);
peers[4].verificationResult = new PeerVerificationResult(103, 105, 100);
peers[5].verificationResult = new PeerVerificationResult(103, 105, 100);
peers[6].verificationResult = new PeerVerificationResult(103, 105, 100);

peers[7].verificationResult = new PeerVerificationResult(103, 103, 103);
peers[8].verificationResult = new PeerVerificationResult(103, 103, 103);

try {
repository.getPeers.mockReturnValue(peers);
stateStore.getLastBlock.mockReturnValue(lastBlock);

const networkStatus = await networkMonitor.checkNetworkHealth();
expect(networkStatus).toEqual({ forked: true, blocksToRollback: 3 });
} finally {
repository.getPeers.mockReset();
stateStore.getLastBlock.mockReset();
}
});

expect(networkStatus).toEqual({ forked: false });
});
it("should rollback ignoring peers how are at common height", async () => {
// 105 (4 peers)
// /
// 100 (3 peers) ... 103 (2 peers and us)

const lastBlock = { data: { height: 103 } };

const peers = [
new Peer("180.177.54.4", 4000),
new Peer("180.177.54.4", 4000),
new Peer("180.177.54.4", 4000),
new Peer("180.177.54.4", 4000),
new Peer("180.177.54.4", 4000),
new Peer("180.177.54.4", 4000),
new Peer("180.177.54.4", 4000),
new Peer("180.177.54.4", 4000),
new Peer("180.177.54.4", 4000),
];

peers[0].verificationResult = new PeerVerificationResult(103, 100, 100);
peers[1].verificationResult = new PeerVerificationResult(103, 100, 100);
peers[2].verificationResult = new PeerVerificationResult(103, 100, 100);

peers[3].verificationResult = new PeerVerificationResult(103, 105, 100);
peers[4].verificationResult = new PeerVerificationResult(103, 105, 100);
peers[5].verificationResult = new PeerVerificationResult(103, 105, 100);
peers[6].verificationResult = new PeerVerificationResult(103, 105, 100);

peers[7].verificationResult = new PeerVerificationResult(103, 103, 103);
peers[8].verificationResult = new PeerVerificationResult(103, 103, 103);

try {
repository.getPeers.mockReturnValue(peers);
stateStore.getLastBlock.mockReturnValue(lastBlock);

const networkStatus = await networkMonitor.checkNetworkHealth();
expect(networkStatus).toEqual({ forked: true, blocksToRollback: 3 });
} finally {
repository.getPeers.mockReset();
stateStore.getLastBlock.mockReset();
}
});

describe("when majority of our peers is on another chain", () => {
it("should not rollback although most peers are forked", async () => {
// 47 (1 peer) 47 (3 peers) 47 (3 peers)
// / / /
// 12 ........... 31 ........... 35 ... 43 (3 peers and us)

const lastBlock = { data: { height: 103 } };

const peers = [
new Peer("180.177.54.4", 4000),
new Peer("181.177.54.4", 4000),
new Peer("182.177.54.4", 4000),
new Peer("183.177.54.4", 4000),
new Peer("184.177.54.4", 4000),
new Peer("185.177.54.4", 4000),
new Peer("186.177.54.4", 4000),
new Peer("187.177.54.4", 4000),
new Peer("188.177.54.4", 4000),
new Peer("189.177.54.4", 4000),
new Peer("180.177.54.4", 4000),
new Peer("180.177.54.4", 4000),
new Peer("180.177.54.4", 4000),
new Peer("180.177.54.4", 4000),
new Peer("180.177.54.4", 4000),
new Peer("180.177.54.4", 4000),
new Peer("180.177.54.4", 4000),
new Peer("180.177.54.4", 4000),
new Peer("180.177.54.4", 4000),
];
// 7 peers are forked out of 10
peers[0].verificationResult = new PeerVerificationResult(43, 47, 31);

peers[0].verificationResult = new PeerVerificationResult(43, 47, 12);

peers[1].verificationResult = new PeerVerificationResult(43, 47, 31);
peers[2].verificationResult = new PeerVerificationResult(43, 47, 31);
peers[3].verificationResult = new PeerVerificationResult(43, 47, 35);
peers[3].verificationResult = new PeerVerificationResult(43, 47, 31);

peers[4].verificationResult = new PeerVerificationResult(43, 47, 35);
peers[5].verificationResult = new PeerVerificationResult(43, 47, 35);
peers[6].verificationResult = new PeerVerificationResult(43, 47, 12);
peers[6].verificationResult = new PeerVerificationResult(43, 47, 35);

beforeEach(() => {
stateStore.getLastBlock = jest.fn().mockReturnValueOnce({ data: { height: 43 } });
repository.getPeers = jest.fn().mockReturnValue(peers);
});
afterEach(() => {
repository.getPeers = jest.fn();
});
peers[7].verificationResult = new PeerVerificationResult(43, 47, 43);
peers[8].verificationResult = new PeerVerificationResult(43, 47, 43);
peers[9].verificationResult = new PeerVerificationResult(43, 47, 43);

it("should return {forked: true, blocksToRollback:<current height - highestCommonHeight>}", async () => {
const networkStatus = await networkMonitor.checkNetworkHealth();
try {
repository.getPeers.mockReturnValue(peers);
stateStore.getLastBlock.mockReturnValue(lastBlock);

expect(networkStatus).toEqual({ forked: true, blocksToRollback: 43 - 35 });
});
const networkStatus = await networkMonitor.checkNetworkHealth();
expect(networkStatus).toEqual({ forked: false });
} finally {
repository.getPeers.mockReset();
stateStore.getLastBlock.mockReset();
}
});
});

Expand Down
67 changes: 35 additions & 32 deletions packages/core-p2p/src/network-monitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,50 +269,53 @@ export class NetworkMonitor implements Contracts.P2P.NetworkMonitor {
.get<Contracts.State.StateStore>(Container.Identifiers.StateStore)
.getLastBlock();

const allPeers: Contracts.P2P.Peer[] = this.repository.getPeers();
const verificationResults: Contracts.P2P.PeerVerificationResult[] = this.repository
.getPeers()
.filter((peer) => peer.verificationResult)
.map((peer) => peer.verificationResult!);

if (!allPeers.length) {
this.logger.info("No peers available.");
if (verificationResults.length === 0) {
this.logger.info("No verified peers available.");

return { forked: false };
}

const forkedPeers: Contracts.P2P.Peer[] = allPeers.filter((peer: Contracts.P2P.Peer) => peer.isForked());
const majorityOnOurChain: boolean = forkedPeers.length / allPeers.length < 0.5;
const forkVerificationResults: Contracts.P2P.PeerVerificationResult[] = verificationResults.filter(
(verificationResult: Contracts.P2P.PeerVerificationResult) => verificationResult.forked,
);

if (majorityOnOurChain) {
this.logger.info("The majority of peers is not forked. No need to rollback.");
return { forked: false };
}
const forkHeights: number[] = forkVerificationResults
.map((verificationResult: Contracts.P2P.PeerVerificationResult) => verificationResult.highestCommonHeight)
.filter((forkHeight, i, arr) => arr.indexOf(forkHeight) === i) // unique
.sort()
.reverse();

const verifiedPeers = allPeers.filter(
(peer: Contracts.P2P.Peer) => peer.verificationResult?.highestCommonHeight !== undefined,
);
const groupedByCommonHeight = Utils.groupBy(
verifiedPeers,
(peer: Contracts.P2P.Peer) => peer.verificationResult!.highestCommonHeight,
);
for (const forkHeight of forkHeights) {
const forkPeerCount = forkVerificationResults.filter((vr) => vr.highestCommonHeight === forkHeight).length;
const ourPeerCount = verificationResults.filter((vr) => vr.highestCommonHeight > forkHeight).length + 1;

const groupedByLength = Utils.groupBy(Object.values(groupedByCommonHeight), (peer) => peer.length);
if (forkPeerCount > ourPeerCount) {
const blocksToRollback = lastBlock.data.height - forkHeight;

// Sort by longest
// @ts-ignore
const longest = Object.keys(groupedByLength).sort((a, b) => b - a)[0];
const longestGroups = groupedByLength[longest];
if (blocksToRollback > 5000) {
this.logger.info(
`Rolling back 5000/${blocksToRollback} blocks to fork at height ${forkHeight} (${ourPeerCount} vs ${forkPeerCount}).`,
);

// Sort by highest common height DESC
longestGroups.sort(
(a, b) => b[0].verificationResult.highestCommonHeight - a[0].verificationResult.highestCommonHeight,
);
const peersMostCommonHeight = longestGroups[0];
return { forked: true, blocksToRollback: 5000 };
} else {
this.logger.info(
`Rolling back ${blocksToRollback} blocks to fork at height ${forkHeight} (${ourPeerCount} vs ${forkPeerCount}).`,
);

const { highestCommonHeight } = peersMostCommonHeight[0].verificationResult;
this.logger.info(
`Rolling back to most common height ${highestCommonHeight.toLocaleString()}. Own height: ${lastBlock.data.height.toLocaleString()}`,
);
return { forked: true, blocksToRollback };
}
} else {
this.logger.debug(`Ignoring fork at height ${forkHeight} (${ourPeerCount} vs ${forkPeerCount}).`);
}
}

// Now rollback blocks equal to the distance to the most common height.
return { forked: true, blocksToRollback: Math.min(lastBlock.data.height - highestCommonHeight, 5000) };
return { forked: false };
}

public async downloadBlocksFromHeight(
Expand Down

0 comments on commit 8cde761

Please sign in to comment.