diff --git a/ironfish/src/network/__fixtures__/blockFetcher.test.ts.fixture b/ironfish/src/network/__fixtures__/blockFetcher.test.ts.fixture index f4cb224734..8b7388849d 100644 --- a/ironfish/src/network/__fixtures__/blockFetcher.test.ts.fixture +++ b/ironfish/src/network/__fixtures__/blockFetcher.test.ts.fixture @@ -1127,5 +1127,33 @@ } ] } + ], + "BlockFetcher correctly sends the first peer to send us a block to telemetry": [ + { + "header": { + "sequence": 2, + "previousBlockHash": "88B6FA8D745A4E53BDA001318E60B04EE2E4EE06A38095688D58049CB6F15ACA", + "noteCommitment": { + "type": "Buffer", + "data": "base64:Px07oo4o5eD8bwJc8+Lwj05tbzpOkTtMvOlZAXFNkQI=" + }, + "transactionCommitment": { + "type": "Buffer", + "data": "base64:qmz/w5yAd3chiU3gikRdoRBa0Q/hGDQqZk81NmGF/K4=" + }, + "target": "883423532389192164791648750371459257913741948437809479060803100646309888", + "randomness": "0", + "timestamp": 1693954844245, + "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", + "noteSize": 4, + "work": "0" + }, + "transactions": [ + { + "type": "Buffer", + "data": "base64:AgAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAAS2N5wYtofXzfUcBVvTGuSHHt/dHM4ZpYJuhPnc02+UKoS94gAqXjnbpW3L13mtT2EZPM8UnWnb9eOZb9y2lCln6E5rbatrAQKACe1zOQFJ6MYNho6KhsPGEmHlUwYfFVupBY3pzW2w8T8mZ16OGBXm1SV07nb9iGAtmhIFD/z1IELEHvuuHhZ9YJ8nPaEo8kajk4dPrdO3FJn1Z4hcnohjwOilJ+HctHWStH9zHDZAmJOtm/CYr7SBz2xw0Avl3M4tj+MwQVB/G9tHdZzBS1aVlAMjJmkVGp0KMMhaS6xYwSwjmnTHw/sGPw8AtLF0Fn/wgjRowJl6awXFZB0BNZuIm3XQAHH3n87bz92jDKi6W39YjnqWjJEXiam7avPfZEsolL9bQLBz1I+BFdJ3ae3R8Y1LU1aIbd+V1C0aM15V6ZkdLJ3MP/rpuEog5I2HVDjbFTho20S46QKjvBIrRT0ILl0uJ5lyZDY82jZnA+ifWB9kesyUY7PCl0gi9nPCtzJH2P3/wU0cnXm/VTDVhUxSXLEwviYhnoJ92iKXD1CXNSA+g3LOnDxzwW9ytSH2B0oumrAHi4Mk3oa1oSObZ/QRL6YdNo7/4EoW1eqbquqwZ0x5I6SwNmMElyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw+yOB3pVLpqIg/6GR2JgVgzoaWlnnW0+Tl+n2+PBSXcPnKBRo80iHhMeat2x9o4IvIoHtgPM7GdOk6MkbrQymAg==" + } + ] + } ] } \ No newline at end of file diff --git a/ironfish/src/network/blockFetcher.test.ts b/ironfish/src/network/blockFetcher.test.ts index bb97e73c5f..f37ad119db 100644 --- a/ironfish/src/network/blockFetcher.test.ts +++ b/ironfish/src/network/blockFetcher.test.ts @@ -464,4 +464,42 @@ describe('BlockFetcher', () => { await peerNetwork.stop() }) + + it('correctly sends the first peer to send us a block to telemetry', async () => { + const { peerNetwork, chain, node } = nodeTest + + const peers = getConnectedPeersWithSpies(peerNetwork.peerManager, 50) + + const newBlock = await useMinerBlockFixture(chain) + + // Accept transactions into mempool so the node can fill the compact block + // without an additional request + for (const transaction of newBlock.transactions.slice(1)) { + node.memPool.acceptTransaction(transaction) + } + + for (const { peer } of peers) { + await peerNetwork.peerManager.onMessage.emitAsync(...newHashMessageEvent(peer, newBlock)) + } + + jest.runOnlyPendingTimers() + + const telemetrySpy = jest.spyOn(node.telemetry, 'submitNewBlockSeen') + + const sentPeers = peers.filter(({ sendSpy }) => sendSpy.mock.calls.length > 0) + const call = sentPeers[0].sendSpy.mock.calls[0][0] as GetCompactBlockRequest + + const message = new GetCompactBlockResponse(newBlock.toCompactBlock(), call.rpcId) + for (const { peer } of sentPeers) { + await peerNetwork.peerManager.onMessage.emitAsync(...peerMessage(peer, message)) + } + + expect(telemetrySpy).toHaveBeenCalledWith( + newBlock, + expect.any(Date), + peers[0].peer.state.identity, + ) + + await peerNetwork.stop() + }) }) diff --git a/ironfish/src/network/blockFetcher.ts b/ironfish/src/network/blockFetcher.ts index 392f20193b..be9f8b46f6 100644 --- a/ironfish/src/network/blockFetcher.ts +++ b/ironfish/src/network/blockFetcher.ts @@ -35,6 +35,7 @@ type BlockState = action: 'COMPACT_BLOCK_REQUEST_SCHEDULED' timeout: NodeJS.Timeout sources: Set // Set of peers that have sent us the hash or compact block + firstSeenBy: Identity } | { action: 'COMPACT_BLOCK_REQUEST_IN_FLIGHT' @@ -42,12 +43,14 @@ type BlockState = timeout: NodeJS.Timeout clearDisconnectHandler: () => void sources: Set // Set of peers that have sent us the hash or compact block + firstSeenBy: Identity } | { action: 'PROCESSING_COMPACT_BLOCK' peer: Identity compactBlock: CompactBlock sources: Set // Set of peers that have sent us the hash or compact block + firstSeenBy: Identity } | { action: 'TRANSACTION_REQUEST_IN_FLIGHT' @@ -57,6 +60,7 @@ type BlockState = timeout: NodeJS.Timeout clearDisconnectHandler: () => void sources: Set // Set of peers that have sent us the hash or compact block + firstSeenBy: Identity } | { action: 'FULL_BLOCK_REQUEST_IN_FLIGHT' @@ -64,10 +68,12 @@ type BlockState = timeout: NodeJS.Timeout clearDisconnectHandler: () => void sources: Set // Set of peers that have sent us the hash or compact block + firstSeenBy: Identity } | { action: 'PROCESSING_FULL_BLOCK' block: Block + firstSeenBy: Identity | null } export class BlockFetcher { @@ -110,6 +116,7 @@ export class BlockFetcher { action: 'COMPACT_BLOCK_REQUEST_SCHEDULED', timeout, sources, + firstSeenBy: peer.state.identity, }) } @@ -173,6 +180,7 @@ export class BlockFetcher { timeout, clearDisconnectHandler, sources: currentState.sources, + firstSeenBy: currentState.firstSeenBy, }) } @@ -215,10 +223,19 @@ export class BlockFetcher { peer: peer.state.identity, compactBlock, sources: currentState ? currentState.sources : new Set(), + firstSeenBy: currentState ? currentState.firstSeenBy : peer.state.identity, }) return true } + /** + * Return the first peer that notified us of this block + */ + firstSeenBy(hash: BlockHash): Identity | null { + const currentState = this.pending.get(hash) + return currentState ? currentState.firstSeenBy : null + } + requestBlockTransactions( peer: Peer, header: BlockHeader, @@ -267,6 +284,7 @@ export class BlockFetcher { timeout, clearDisconnectHandler, sources: currentState.sources, + firstSeenBy: currentState.firstSeenBy, }) return @@ -302,6 +320,7 @@ export class BlockFetcher { this.pending.set(hash, { action: 'PROCESSING_FULL_BLOCK', block, + firstSeenBy: currentState.firstSeenBy, }) return block @@ -353,13 +372,14 @@ export class BlockFetcher { timeout, clearDisconnectHandler, sources: currentState.sources, + firstSeenBy: currentState.firstSeenBy, }) } /** * Called when a block has been assembled from a compact block * but has not yet been validated and added to the chain. */ - receivedFullBlock(block: Block): void { + receivedFullBlock(block: Block, peer: Peer): void { const hash = block.header.hash const currentState = this.pending.get(hash) @@ -369,6 +389,7 @@ export class BlockFetcher { this.pending.set(hash, { action: 'PROCESSING_FULL_BLOCK', block, + firstSeenBy: currentState?.firstSeenBy ?? peer.state.identity ?? null, }) } } diff --git a/ironfish/src/network/peerNetwork.ts b/ironfish/src/network/peerNetwork.ts index f106ce5b63..0791837141 100644 --- a/ironfish/src/network/peerNetwork.ts +++ b/ironfish/src/network/peerNetwork.ts @@ -1199,12 +1199,13 @@ export class PeerNetwork { } // Mark that we've assembled a full block in the block fetcher - this.blockFetcher.receivedFullBlock(block) + this.blockFetcher.receivedFullBlock(block, peer) this.broadcastBlock(block) // log that we've validated the block enough to gossip it - this.telemetry.submitNewBlockSeen(block, new Date()) + const firstPeer = this.blockFetcher.firstSeenBy(block.header.hash) + this.telemetry.submitNewBlockSeen(block, new Date(), firstPeer) // verify the full block const verified = await this.chain.verifier.verifyBlockAdd(block, prevHeader) diff --git a/ironfish/src/telemetry/telemetry.ts b/ironfish/src/telemetry/telemetry.ts index d67dafecea..c48a56dd18 100644 --- a/ironfish/src/telemetry/telemetry.ts +++ b/ironfish/src/telemetry/telemetry.ts @@ -403,7 +403,7 @@ export class Telemetry { }) } - submitNewBlockSeen(block: Block, seenAt: Date): void { + submitNewBlockSeen(block: Block, seenAt: Date, peerId: Identity | null): void { this.submit({ measurement: 'block_propagation', timestamp: seenAt, @@ -419,6 +419,11 @@ export class Telemetry { type: 'integer', value: block.header.timestamp.valueOf(), }, + { + name: 'firstSeenBy', + type: 'string', + value: peerId ?? '', + }, { name: 'sequence', type: 'integer',