Skip to content

Commit 51800c7

Browse files
authored
feat: purge stale transactions (#801)
1 parent b22a9c4 commit 51800c7

File tree

11 files changed

+105
-26
lines changed

11 files changed

+105
-26
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ Once node receives a claim, it stores the claim with some metadata including the
126126
* The highest block read at the time node stores the claim
127127
* Placeholders for the actual block that was mined including the claim
128128

129-
This allows the node application to track whether or not the claim actually has been successfully saved to the Bitcoin blockchain. There is a configuration value, `maxBlockHeightDelta`, that determines how far ahead the blockchain will grow before resubmitting the claim. Comparing this value against the delta between the highest block read and the block read at the time of claim creation will determine whether node resubmits the claim.
129+
This allows the node application to track whether or not the claim actually has been successfully saved to the Bitcoin blockchain. There is a configuration value, `maximumTransactionAgeInBlocks`, that determines how far ahead the blockchain will grow before resubmitting the claim. Comparing this value against the delta between the highest block read and the block read at the time of claim creation will determine whether node resubmits the claim.
130130

131131

132132
### Po.et JS

src/BlockchainWriter/BlockchainWriter.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,8 @@ const createContainer = (
107107
.toConstantValue(new BitcoinCore(bitcoinRPCConfigurationToBitcoinCoreArguments(configuration)))
108108
container.bind<ServiceConfiguration>('ServiceConfiguration').toConstantValue({
109109
anchorIntervalInSeconds: configuration.anchorIntervalInSeconds,
110-
purgeStaleTransactionsInSeconds: configuration.purgeStaleTransactionsInSeconds,
111-
maxBlockHeightDelta: configuration.maxBlockHeightDelta,
110+
purgeStaleTransactionsIntervalInSeconds: configuration.purgeStaleTransactionsIntervalInSeconds,
111+
maximumTransactionAgeInBlocks: configuration.maximumTransactionAgeInBlocks,
112112
})
113113
container.bind<ControllerConfiguration>('ClaimControllerConfiguration').toConstantValue(configuration)
114114
container.bind<ExchangeConfiguration>('ExchangeConfiguration').toConstantValue(configuration.exchanges)

src/BlockchainWriter/Controller.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { ExchangeConfiguration } from './ExchangeConfiguration'
1515
export interface ControllerConfiguration {
1616
readonly poetNetwork: string
1717
readonly poetVersion: number
18+
readonly maximumTransactionAgeInBlocks: number
1819
}
1920

2021
export const convertLightBlockToEntry = (lightBlock: LightBlock): Entry => ({
@@ -91,6 +92,19 @@ export class Controller {
9192
await this.dao.updateAllByTransactionId(transactionIds, convertLightBlockToEntry(lightBlock))
9293
}
9394

95+
async purgeStaleTransactions(): Promise<void> {
96+
const logger = this.logger.child({ method: 'purgeStaleTransactions' })
97+
const { blocks } = await this.bitcoinCore.getBlockchainInfo()
98+
logger.info(
99+
{
100+
blocks, maximumTransactionAgeInBlocks: this.configuration.maximumTransactionAgeInBlocks,
101+
},
102+
'Purging stale transactions',
103+
)
104+
105+
await this.dao.purgeStaleTransactions(blocks - this.configuration.maximumTransactionAgeInBlocks)
106+
}
107+
94108
private async anchorIPFSDirectoryHash(ipfsDirectoryHash: string): Promise<void> {
95109
const { dao, messaging, anchorData, ipfsDirectoryHashToPoetAnchor } = this
96110
const logger = this.logger.child({ method: 'anchorIPFSDirectoryHash' })

src/BlockchainWriter/Router.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export class Router {
3535
await this.messaging.consume(this.exchange.anchorNextHashRequest, this.onAnchorNextHashRequest)
3636
await this.messaging.consume(this.exchange.batchWriterCreateNextBatchSuccess, this.onCreateBatchSuccess)
3737
await this.messaging.consumeBlockDownloaded(this.blockDownloadedConsumer)
38+
await this.messaging.consume(this.exchange.purgeStaleTransactions, this.onPurgeStaleTransactions)
3839
}
3940

4041
onAnchorNextHashRequest = async (message: any): Promise<void> => {
@@ -50,11 +51,23 @@ export class Router {
5051
}
5152
}
5253

53-
blockDownloadedConsumer = async (blockDownloaded: BlockDownloaded): Promise<void> => {
54-
await this.claimController.setBlockInformationForTransactions(
54+
blockDownloadedConsumer = async (blockDownloaded: BlockDownloaded): Promise<void> =>
55+
this.claimController.setBlockInformationForTransactions(
5556
getTxnIds(blockDownloaded.poetBlockAnchors),
5657
blockDownloaded.block,
5758
)
59+
60+
onPurgeStaleTransactions = async (): Promise<void> => {
61+
const logger = this.logger.child({ method: 'onPurgeStaleTransactions' })
62+
63+
try {
64+
await this.claimController.purgeStaleTransactions()
65+
} catch (error) {
66+
logger.trace(
67+
{ error },
68+
'Error encountered while purging stale transactions',
69+
)
70+
}
5871
}
5972

6073
onCreateBatchSuccess = async (message: any): Promise<void> => {

src/BlockchainWriter/Service.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import { ExchangeConfiguration } from './ExchangeConfiguration'
99

1010
export interface ServiceConfiguration {
1111
readonly anchorIntervalInSeconds: number
12-
readonly purgeStaleTransactionsInSeconds: number
13-
readonly maxBlockHeightDelta: number
12+
readonly purgeStaleTransactionsIntervalInSeconds: number
13+
readonly maximumTransactionAgeInBlocks: number
1414
}
1515

1616
@injectable()
@@ -33,7 +33,7 @@ export class Service {
3333
this.anchorNextHashInterval = new Interval(this.anchorNextHash, 1000 * configuration.anchorIntervalInSeconds)
3434
this.purgeStaleTransactionInterval = new Interval(
3535
this.purgeStaleTransactions,
36-
1000 * configuration.purgeStaleTransactionsInSeconds,
36+
1000 * configuration.purgeStaleTransactionsIntervalInSeconds,
3737
)
3838
}
3939

src/Configuration.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ export interface Configuration extends LoggingConfiguration, BitcoinRPCConfigura
3333

3434
readonly enableAnchoring: boolean
3535
readonly anchorIntervalInSeconds: number
36-
readonly purgeStaleTransactionsInSeconds: number
37-
readonly maxBlockHeightDelta: number
36+
readonly purgeStaleTransactionsIntervalInSeconds: number
37+
readonly maximumTransactionAgeInBlocks: number
3838

3939
readonly healthIntervalInSeconds: number
4040
readonly lowWalletBalanceInBitcoin: number
@@ -109,8 +109,8 @@ const defaultConfiguration: Configuration = {
109109

110110
enableAnchoring: false,
111111
anchorIntervalInSeconds: 30,
112-
purgeStaleTransactionsInSeconds: 600,
113-
maxBlockHeightDelta: 25,
112+
purgeStaleTransactionsIntervalInSeconds: 600,
113+
maximumTransactionAgeInBlocks: 25,
114114

115115
healthIntervalInSeconds: 30,
116116
lowWalletBalanceInBitcoin: 1,

src/app.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,8 +179,8 @@ export async function app(localVars: any = {}) {
179179
poetNetwork: configuration.poetNetwork,
180180
poetVersion: configuration.poetVersion,
181181
anchorIntervalInSeconds: configuration.anchorIntervalInSeconds,
182-
purgeStaleTransactionsInSeconds: configuration.purgeStaleTransactionsInSeconds,
183-
maxBlockHeightDelta: configuration.maxBlockHeightDelta,
182+
purgeStaleTransactionsIntervalInSeconds: configuration.purgeStaleTransactionsIntervalInSeconds,
183+
maximumTransactionAgeInBlocks: configuration.maximumTransactionAgeInBlocks,
184184
bitcoinUrl: configuration.bitcoinUrl,
185185
bitcoinPort: configuration.bitcoinPort,
186186
bitcoinNetwork: configuration.bitcoinNetwork,

tests/functional/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
import './claim_with_data'
22
import './endpoints'
3-
// import './transaction_timeout'
3+
import './transaction_timeout'

tests/functional/transaction_timeout.ts

Lines changed: 58 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
/* tslint:disable:no-relative-imports */
22
import { configureCreateVerifiableClaim, getVerifiableClaimSigner, isSignedVerifiableClaim } from '@po.et/poet-js'
3-
import { allPass, is, isNil, lensPath, path, pipeP, view } from 'ramda'
3+
import { allPass, is, isNil, lensPath, not, path, pipe, pipeP, view } from 'ramda'
44
import { describe } from 'riteway'
55

66
import { app } from '../../src/app'
77
import { issuer, privateKey } from '../helpers/Keys'
88
import { ensureBitcoinBalance, bitcoindClients, resetBitcoinServers } from '../helpers/bitcoin'
9-
import { delay, runtimeId, createDatabase } from '../helpers/utils'
9+
import { delayInSeconds, runtimeId, createDatabase } from '../helpers/utils'
1010
import { getWork, postWork } from '../helpers/works'
1111

1212
const PREFIX = `test-functional-nodeA-poet-${runtimeId()}`
@@ -21,7 +21,8 @@ const blockchainSettings = {
2121
BATCH_CREATION_INTERVAL_IN_SECONDS: 5,
2222
READ_DIRECTORY_INTERVAL_IN_SECONDS: 5,
2323
UPLOAD_CLAIM_INTERVAL_IN_SECONDS: 5,
24-
TRANSACTION_MAX_AGE_IN_SECONDS: 60,
24+
MAXIMUM_TRANSACTION_AGE_IN_BLOCKS: 1,
25+
PURGE_STALE_TRANSACTIONS_INTERVAL_IN_SECONDS: 30,
2526
}
2627

2728
const { configureSignVerifiableClaim } = getVerifiableClaimSigner()
@@ -32,7 +33,7 @@ const createClaim = pipeP(
3233
signVerifiableClaim,
3334
)
3435

35-
const { btcdClientA }: any = bitcoindClients()
36+
const { btcdClientA, btcdClientB }: any = bitcoindClients()
3637

3738
const blockHash = lensPath(['anchor', 'blockHash'])
3839
const blockHeight = lensPath(['anchor', 'blockHeight'])
@@ -41,13 +42,16 @@ const transactionId = lensPath(['anchor', 'transactionId'])
4142
const getTransactionId = view(transactionId)
4243
const getBlockHash = view(blockHash)
4344
const getBlockHeight = view(blockHeight)
45+
const isNotNil = pipe(isNil, not)
4446

4547
const lengthIsGreaterThan0 = (s: string) => s.length > 0
4648
const hasValidTxId = allPass([is(String), lengthIsGreaterThan0])
4749

4850
describe('Transaction timout will reset the transaction id for the claim', async assert => {
4951
await resetBitcoinServers()
50-
await delay(5 * 1000)
52+
await btcdClientB.addNode(btcdClientA.host, 'add')
53+
54+
await delayInSeconds(5)
5155

5256
const db = await createDatabase(PREFIX)
5357
const server = await app({
@@ -62,18 +66,23 @@ describe('Transaction timout will reset the transaction id for the claim', async
6266

6367
// Make sure node A has regtest coins to pay for transactions.
6468
await ensureBitcoinBalance(btcdClientA)
69+
await btcdClientA.setNetworkActive(false)
6570

6671
// Allow everything to finish starting.
67-
await delay(5 * 1000)
72+
await delayInSeconds(5)
73+
6874
const claim = await createClaim({
6975
name: 'Author Name',
7076
})
7177

7278
await postWorkToNode(claim)
7379

7480
// Wait for a claim batch to be submitted to the blockchain.
75-
await delay((blockchainSettings.ANCHOR_INTERVAL_IN_SECONDS +
76-
blockchainSettings.BATCH_CREATION_INTERVAL_IN_SECONDS + 5) * 1000)
81+
await delayInSeconds(
82+
blockchainSettings.ANCHOR_INTERVAL_IN_SECONDS +
83+
blockchainSettings.BATCH_CREATION_INTERVAL_IN_SECONDS +
84+
5,
85+
)
7786

7887
const firstResponse = await getWorkFromNode(claim.id)
7988
const firstGet = await firstResponse.json()
@@ -100,7 +109,10 @@ describe('Transaction timout will reset the transaction id for the claim', async
100109
expected: true,
101110
})
102111

103-
await delay(blockchainSettings.TRANSACTION_MAX_AGE_IN_SECONDS * 1000 + 5)
112+
await btcdClientB.generate(blockchainSettings.MAXIMUM_TRANSACTION_AGE_IN_BLOCKS + 1)
113+
await btcdClientA.setNetworkActive(true)
114+
115+
await delayInSeconds((blockchainSettings.PURGE_STALE_TRANSACTIONS_INTERVAL_IN_SECONDS + 5) * 2)
104116

105117
const secondResponse = await getWorkFromNode(claim.id)
106118
const secondGet = await secondResponse.json()
@@ -120,6 +132,43 @@ describe('Transaction timout will reset the transaction id for the claim', async
120132
expected: true,
121133
})
122134

135+
await btcdClientA.generate(1)
136+
await delayInSeconds(
137+
blockchainSettings.BATCH_CREATION_INTERVAL_IN_SECONDS +
138+
blockchainSettings.READ_DIRECTORY_INTERVAL_IN_SECONDS,
139+
)
140+
141+
const thirdResponse = await getWorkFromNode(claim.id)
142+
const thirdGet = await thirdResponse.json()
143+
const thirdTxId = getTransactionId(thirdGet)
144+
145+
assert({
146+
given: 'transaction mined',
147+
should: 'store the block height with the claim',
148+
actual: isNotNil(getBlockHeight(thirdGet)),
149+
expected: true,
150+
})
151+
152+
assert({
153+
given: 'transaction mined',
154+
should: 'store the block hash with the claim',
155+
actual: isNotNil(getBlockHash(thirdGet)),
156+
expected: true,
157+
})
158+
159+
await delayInSeconds(blockchainSettings.PURGE_STALE_TRANSACTIONS_INTERVAL_IN_SECONDS + 5)
160+
161+
const fourthResponse = await getWorkFromNode(claim.id)
162+
const fourthGet = await fourthResponse.json()
163+
const fourthTxId = getTransactionId(fourthGet)
164+
165+
assert({
166+
given: 'a claim with a block height and hash',
167+
should: 'have the same transaction id',
168+
actual: fourthTxId,
169+
expected: thirdTxId,
170+
})
171+
123172
await server.stop()
124173
await db.teardown()
125174
})

tests/helpers/utils.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
/* tslint:disable:no-relative-imports */
22
import { promisify } from 'util'
3-
3+
import { secondsToMiliseconds } from '../../src/Helpers/Time'
4+
import { asyncPipe } from '../../src/Helpers/asyncPipe'
45
import { app } from '../../src/app'
56
import { dbHelper } from './database'
6-
77
export const runtimeId = () => `${process.pid}-${new Date().getMilliseconds()}-${Math.floor(Math.random() * 10)}`
88
export const delay = promisify(setTimeout)
99

10+
export const delayInSeconds = asyncPipe(secondsToMiliseconds, delay)
11+
1012
export const createDatabase = async (prefix: string) => {
1113
const db = dbHelper()
1214

0 commit comments

Comments
 (0)