Skip to content

Commit

Permalink
feat: prevent unbundling attack
Browse files Browse the repository at this point in the history
  • Loading branch information
twoeths committed May 12, 2023
1 parent 0741eed commit c3d69c6
Show file tree
Hide file tree
Showing 2 changed files with 72 additions and 6 deletions.
24 changes: 24 additions & 0 deletions packages/beacon-node/src/sync/unknownBlock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import {ChainForkConfig} from "@lodestar/config";
import {Logger, pruneSetToMax} from "@lodestar/utils";
import {Root, RootHex} from "@lodestar/types";
import {fromHexString, toHexString} from "@chainsafe/ssz";
import {INTERVALS_PER_SLOT} from "@lodestar/params";
import {sleep} from "@lodestar/utils";
import {INetwork, NetworkEvent, PeerAction} from "../network/index.js";
import {IBeaconChain} from "../chain/index.js";
import {BlockInput} from "../chain/blocks/types.js";
Expand All @@ -26,6 +28,7 @@ export class UnknownBlockSync {
*/
private readonly pendingBlocks = new Map<RootHex, PendingBlock>();
private readonly knownBadBlocks = new Set<RootHex>();
private readonly proposerBoostSecWindow: number;

constructor(
private readonly config: ChainForkConfig,
Expand All @@ -44,6 +47,8 @@ export class UnknownBlockSync {
this.logger.debug("UnknownBlockSync disabled.");
}

this.proposerBoostSecWindow = this.config.SECONDS_PER_SLOT / INTERVALS_PER_SLOT;

if (metrics) {
metrics.syncUnknownBlock.pendingBlocks.addCollect(() =>
metrics.syncUnknownBlock.pendingBlocks.set(this.pendingBlocks.size)
Expand Down Expand Up @@ -239,6 +244,25 @@ export class UnknownBlockSync {
}

pendingBlock.status = PendingBlockStatus.processing;
// this prevents unbundling attack
// see https://lighthouse-blog.sigmaprime.io/mev-unbundling-rpc.html
const {slot: blockSlot, proposerIndex} = pendingBlock.blockInput.block.message;
if (
this.chain.clock.secFromSlot(blockSlot) < this.proposerBoostSecWindow &&
this.chain.seenBlockProposers.isKnown(blockSlot, proposerIndex)
) {
// proposer is known by a gossip block already, wait a bit to make sure this block is not
// eligible for proposer boost to prevent unbundling attack
const blockRoot = this.config
.getForkTypes(blockSlot)
.BeaconBlock.hashTreeRoot(pendingBlock.blockInput.block.message);
this.logger.verbose("Avoid proposer boost for this block of known proposer", {
blockSlot,
blockRoot: toHexString(blockRoot),
proposerIndex,
});
await sleep(this.proposerBoostSecWindow * 1000);
}
// At gossip time, it's critical to keep a good number of mesh peers.
// To do that, the Gossip Job Wait Time should be consistently <3s to avoid the behavior penalties in gossip
// Gossip Job Wait Time depends on the BLS Job Wait Time
Expand Down
54 changes: 48 additions & 6 deletions packages/beacon-node/test/unit/sync/unknownBlock.test.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,73 @@
import {expect} from "chai";
import {config} from "@lodestar/config/default";
import sinon from "sinon";
import {config as minimalConfig} from "@lodestar/config/default";
import {createChainForkConfig} from "@lodestar/config";
import {IForkChoice, ProtoBlock} from "@lodestar/fork-choice";
import {ssz} from "@lodestar/types";
import {notNullish, sleep} from "@lodestar/utils";
import {notNullish} from "@lodestar/utils";
import {toHexString} from "@chainsafe/ssz";
import {IBeaconChain} from "../../../src/chain/index.js";
import {INetwork, NetworkEvent, NetworkEventBus, PeerAction} from "../../../src/network/index.js";
import {UnknownBlockSync} from "../../../src/sync/unknownBlock.js";
import {testLogger} from "../../utils/logger.js";
import {getValidPeerId} from "../../utils/peer.js";
import {BlockSource, getBlockInput} from "../../../src/chain/blocks/types.js";
import {IClock} from "../../../src/util/clock.js";
import {SeenBlockProposers} from "../../../src/chain/seenCache/seenBlockProposers.js";

describe("sync / UnknownBlockSync", () => {
const logger = testLogger();
const sandbox = sinon.createSandbox();
const slotSec = 0.3;
// eslint-disable-next-line @typescript-eslint/naming-convention
const config = createChainForkConfig({...minimalConfig, SECONDS_PER_SLOT: slotSec});

beforeEach(() => {
sandbox.useFakeTimers({shouldAdvanceTime: true});
});

afterEach(() => {
sandbox.restore();
});

const testCases: {
id: string;
event: NetworkEvent.unknownBlockParent | NetworkEvent.unknownBlock;
finalizedSlot: number;
reportPeer: boolean;
seenBlock: boolean;
}[] = [
{
id: "fetch and process multiple unknown blocks",
event: NetworkEvent.unknownBlock,
finalizedSlot: 0,
reportPeer: false,
seenBlock: false,
},
{
id: "fetch and process multiple unknown block parents",
event: NetworkEvent.unknownBlockParent,
finalizedSlot: 0,
reportPeer: false,
seenBlock: false,
},
{
id: "downloaded parent is before finalized slot",
event: NetworkEvent.unknownBlockParent,
finalizedSlot: 2,
reportPeer: true,
seenBlock: false,
},
{
id: "unbundling attack",
event: NetworkEvent.unknownBlockParent,
finalizedSlot: 0,
reportPeer: false,
seenBlock: true,
},
];

for (const {id, event, finalizedSlot, reportPeer} of testCases) {
for (const {id, event, finalizedSlot, reportPeer, seenBlock} of testCases) {
it(id, async () => {
const peer = getValidPeerId();
const peerIdStr = peer.toString();
Expand Down Expand Up @@ -87,6 +114,15 @@ describe("sync / UnknownBlockSync", () => {
hasBlock: (root) => forkChoiceKnownRoots.has(toHexString(root)),
getFinalizedBlock: () => ({slot: finalizedSlot} as ProtoBlock),
};
const clock: Pick<IClock, "secFromSlot"> = {
secFromSlot: () => 0,
};
const seenBlockProposers: Pick<SeenBlockProposers, "isKnown"> = {
// only return seenBlock for blockC
isKnown: (blockSlot) => (blockSlot === blockC.message.slot ? seenBlock : false),
};
let blockCResolver: () => void;
const blockCProcessed = new Promise<void>((resolve) => (blockCResolver = resolve));

const chain: Partial<IBeaconChain> = {
forkChoice: forkChoice as IForkChoice,
Expand All @@ -95,9 +131,13 @@ describe("sync / UnknownBlockSync", () => {
// Simluate adding the block to the forkchoice
const blockRootHex = toHexString(ssz.phase0.BeaconBlock.hashTreeRoot(block.message));
forkChoiceKnownRoots.add(blockRootHex);
if (blockRootHex === blockRootHexC) blockCResolver();
},
clock: clock as IClock,
seenBlockProposers: seenBlockProposers as SeenBlockProposers,
};

const setTimeoutSpy = sandbox.spy(global, "setTimeout");
new UnknownBlockSync(config, network as INetwork, chain as IBeaconChain, logger, null);
if (event === NetworkEvent.unknownBlockParent) {
network.events?.emit(
Expand All @@ -114,10 +154,12 @@ describe("sync / UnknownBlockSync", () => {
expect(err[0].toString()).equal(peerIdStr);
expect([err[1], err[2]]).to.be.deep.equal([PeerAction.LowToleranceError, "BadBlockByRoot"]);
} else {
// happy path
// Wait for all blocks to be in ForkChoice store
while (forkChoiceKnownRoots.size < 3) {
await sleep(10);
await blockCProcessed;
if (seenBlock) {
expect(setTimeoutSpy).to.have.been.calledWithMatch({}, (slotSec / 3) * 1000);
} else {
expect(setTimeoutSpy).to.be.not.called;
}

// After completing the sync, all blocks should be in the ForkChoice
Expand Down

0 comments on commit c3d69c6

Please sign in to comment.