From e08e5b21d09c6d656aa8ca49caf491b604051f78 Mon Sep 17 00:00:00 2001 From: Sam Holmes Date: Fri, 1 Nov 2024 16:31:20 -0700 Subject: [PATCH 1/4] Upgrade cleaners@^0.3.17 --- package.json | 2 +- yarn.lock | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 35ff9a76..2d6d1bc9 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "bs58": "^4.0.1", "bs58grscheck": "^2.1.2", "bs58smartcheck": "^2.0.4", - "cleaners": "^0.3.12", + "cleaners": "^0.3.17", "disklet": "^0.4.5", "ecpair": "https://github.com/EdgeApp/ecpair.git#b193eb8ea2ec0c93b528f4b0223a605407ff43e4", "edge-sync-client": "^0.2.7", diff --git a/yarn.lock b/yarn.lock index cb71488e..cd79730e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2936,11 +2936,16 @@ clean-stack@^2.0.0: resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== -cleaners@^0.3.12, cleaners@^0.3.14, cleaners@^0.3.9: +cleaners@^0.3.14, cleaners@^0.3.9: version "0.3.16" resolved "https://registry.yarnpkg.com/cleaners/-/cleaners-0.3.16.tgz#d3a7ab936cb78b0d6ac19ba87b2d28ec41f30502" integrity sha512-Ecu8Fwv3wT7GV44K4Zas2CQJI11ZV/yPwoh8Gg9BSV6rAmhNsqFeWceA1RII1czbarFSG3XClUePrpI806OvRw== +cleaners@^0.3.17: + version "0.3.17" + resolved "https://registry.yarnpkg.com/cleaners/-/cleaners-0.3.17.tgz#dae498f3d49b7e9364050402d2f4ad09abcd31ba" + integrity sha512-X5acjsLwJK+JEK5hv0Rve7G78+E6iYh1TzJZ40z7Yjrba0WhW6spTq28WgG9w+AK+YQIOHtQTrzaiuntMBBIwQ== + cli-boxes@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f" From 3539983b03e5c9f0c184b86b2d21a7bf2f255c37 Mon Sep 17 00:00:00 2001 From: Sam Holmes Date: Mon, 4 Nov 2024 17:10:41 -0800 Subject: [PATCH 2/4] Fix kink in error handling chain for pickNextTaskCb generator The errors through were crashing the `taskGeneratorFn` routine, halting the wallet entirely. Instead, the errors should be delegated to the pickNextTask routine to be handled or not. If handled, then continue the routine with result, if not, then assume a end result, and continue. --- CHANGELOG.md | 2 ++ src/common/utxobased/engine/ServerStates.ts | 25 +++++++++++++++------ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d32d83d7..f3fdc53c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- fixed: Improper handling of WebSocket message processing errors, causing sync-halting. + ## 3.4.3 (2024-10-15) - fixed: Re-publish to NPM with missing files included. diff --git a/src/common/utxobased/engine/ServerStates.ts b/src/common/utxobased/engine/ServerStates.ts index e709e991..20a538b5 100644 --- a/src/common/utxobased/engine/ServerStates.ts +++ b/src/common/utxobased/engine/ServerStates.ts @@ -231,13 +231,13 @@ export function makeServerStates(config: ServerStateConfig): ServerStates { if (uri == null || !(uri in serverStatesCache)) return false const generator = pickNextTaskCB(uri) - let nextValue: unknown - while (true) { - const result: IteratorResult< - WsTask | boolean, - boolean - > = await generator.next(nextValue) + let result: IteratorResult< + WsTask | boolean, + boolean + > = await generator.next() + + while (true) { if (result?.done === true) { return result.value } @@ -250,7 +250,18 @@ export function makeServerStates(config: ServerStateConfig): ServerStates { )}` log(`${uri} nextTask: ${taskMessage}`) } - nextValue = yield task + try { + const nextValue = yield task + result = await generator.next(nextValue) + } catch (error) { + // Delegate the error handling to the task generator: + result = await generator.throw(error).catch(error => { + // If unhandled, log the error up to the core: + log.error(error) + // End the task generator routine (task threw unhandled error) + return { done: true, value: false } + }) + } } } From d17a38e2291df6f9d7751e59fb1411a8a26e3f33 Mon Sep 17 00:00:00 2001 From: Sam Holmes Date: Mon, 4 Nov 2024 16:58:43 -0800 Subject: [PATCH 3/4] Keep object context with scriptPubkey in utxoFromTransactionDataInput --- src/common/utxobased/db/util/utxo.ts | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/common/utxobased/db/util/utxo.ts b/src/common/utxobased/db/util/utxo.ts index 611deef7..4b243c69 100644 --- a/src/common/utxobased/db/util/utxo.ts +++ b/src/common/utxobased/db/util/utxo.ts @@ -12,11 +12,10 @@ export const utxoFromTransactionDataInput = async ( inputIndex: number ): Promise => { const input = transactionData.inputs[inputIndex] - const { scriptPubkey } = input - const address = await dataLayer.fetchAddress(scriptPubkey) + const address = await dataLayer.fetchAddress(input.scriptPubkey) if (address == null) - throw new Error(`Cannot find address for ${scriptPubkey}`) + throw new Error(`Cannot find address for ${input.scriptPubkey}`) if (address.path == null) throw new Error(`Address has no derivation path information`) @@ -24,9 +23,7 @@ export const utxoFromTransactionDataInput = async ( const redeemScript = address.redeemScript const purposeType = currencyFormatToPurposeType(address.path.format) - const getScripts = async ( - scriptPubkey: string - ): Promise<{ + const getScripts = async (): Promise<{ script: string scriptType: ScriptTypeEnum }> => { @@ -41,26 +38,26 @@ export const utxoFromTransactionDataInput = async ( } case BIP43PurposeTypeEnum.WrappedSegwit: return { - script: scriptPubkey, + script: input.scriptPubkey, scriptType: ScriptTypeEnum.p2wpkhp2sh } case BIP43PurposeTypeEnum.Segwit: return { - script: scriptPubkey, + script: input.scriptPubkey, scriptType: ScriptTypeEnum.p2wpkh } default: throw new Error(`Unknown purpose type ${purposeType}`) } } - const { script, scriptType } = await getScripts(scriptPubkey) + const { script, scriptType } = await getScripts() return { id: `${input.txId}_${input.outputIndex}`, txid: input.txId, vout: input.outputIndex, value: input.amount, - scriptPubkey, + scriptPubkey: input.scriptPubkey, script, redeemScript, scriptType, From b166a70f171cb5b482712d037dd594ba4fa77f1c Mon Sep 17 00:00:00 2001 From: Sam Holmes Date: Mon, 4 Nov 2024 17:13:28 -0800 Subject: [PATCH 4/4] Add support for coinbase inputs on block reward transactions --- CHANGELOG.md | 1 + .../utxobased/engine/UtxoEngineProcessor.ts | 47 ++++++++---- src/common/utxobased/network/blockbookApi.ts | 76 ++++++++++++------- .../utxobased/network/Blockbook.spec.ts | 28 ++++++- 4 files changed, 108 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f3fdc53c..58716b03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased - fixed: Improper handling of WebSocket message processing errors, causing sync-halting. +- fixed: Add support for processing coinbase inputs on block reward transactions. ## 3.4.3 (2024-10-15) diff --git a/src/common/utxobased/engine/UtxoEngineProcessor.ts b/src/common/utxobased/engine/UtxoEngineProcessor.ts index c8bc4363..6f2608ba 100644 --- a/src/common/utxobased/engine/UtxoEngineProcessor.ts +++ b/src/common/utxobased/engine/UtxoEngineProcessor.ts @@ -1257,43 +1257,60 @@ const processTransactionResponse = ( args: { txResponse: TransactionResponse } ): TransactionData => { const { txResponse } = args - const inputs = txResponse.vin.map(vin => { + + let outputTotalValue = '0' + const outputs: TransactionData['outputs'] = txResponse.vout.map(vout => { const scriptPubkey = - // Note: Blockbook has empirically not sent a hex value as the - // scriptPubkey for vins. If we discover this to be changed for some - // cases, we may want to use the `hex` field as an optimization. + vout.hex ?? asMaybe( raw => validScriptPubkeyFromAddress(raw), 'unknown' )({ - address: vin.addresses[0], + address: vout.addresses[0], coin: common.pluginInfo.coinInfo.name }) + outputTotalValue = add(outputTotalValue, vout.value) return { - txId: vin.txid, - outputIndex: vin.vout, - n: vin.n, + n: vout.n, scriptPubkey, - sequence: vin.sequence, - amount: vin.value + amount: vout.value } }) - const outputs = txResponse.vout.map(vout => { + + const inputs: TransactionData['inputs'] = txResponse.vin.map(vin => { + // Handle coinbase input + if (!vin.isAddress) { + return { + amount: outputTotalValue, + n: 0, + outputIndex: 4294967295, + scriptPubkey: '', // Maybe there's a bogus scriptPubkey we can use here? + sequence: vin.sequence, + txId: '0000000000000000000000000000000000000000000000000000000000000000' + } + } + const scriptPubkey = - vout.hex ?? + // Note: Blockbook has empirically not sent a hex value as the + // scriptPubkey for vins. If we discover this to be changed for some + // cases, we may want to use the `hex` field as an optimization. asMaybe( raw => validScriptPubkeyFromAddress(raw), 'unknown' )({ - address: vout.addresses[0], + address: vin.addresses[0], coin: common.pluginInfo.coinInfo.name }) return { - n: vout.n, + txId: vin.txid, + outputIndex: vin.vout, + n: vin.n, scriptPubkey, - amount: vout.value + sequence: vin.sequence, + amount: vin.value } }) + return { txid: txResponse.txid, hex: txResponse.hex, diff --git a/src/common/utxobased/network/blockbookApi.ts b/src/common/utxobased/network/blockbookApi.ts index 5bb54ede..c679ec9b 100644 --- a/src/common/utxobased/network/blockbookApi.ts +++ b/src/common/utxobased/network/blockbookApi.ts @@ -1,12 +1,14 @@ import { asArray, asBoolean, + asEither, asMaybe, asNumber, asObject, asOptional, asString, asUnknown, + asValue, Cleaner, uncleaner } from 'cleaners' @@ -57,16 +59,24 @@ export interface BlockbookTransaction { confirmations: number blockTime: number fees: string - vin: Array<{ - txid: string - sequence: number - n: number - vout: number - addresses: string[] - isAddress: boolean - value: string - hex?: string - }> + vin: Array< + | { + addresses: string[] + hex?: string + isAddress: true + n: number + sequence: number + txid: string + value: string + vout: number + } + | { + coinbase: string + isAddress: false + n: number + sequence: number + } + > vout: Array<{ n: number value: string @@ -85,22 +95,36 @@ export const asBlockbookTransaction = ( blockTime: asNumber, fees: asString, vin: asArray( - asObject({ - txid: asString, - // Empirically observed omitted sequence is possible for when sequence is zero. - // Is the case for tx `19ecc679cfc7e71ad616a22bbee96fd5abe8616e4f408f1f5daaf137400ae091`. - sequence: asOptional(asNumber, 0), - n: asNumber, - // If Blockbook doesn't provide vout, assume 0. Empirically observed - // case for tx `fefac8c22ba1178df5d7c90b78cc1c203d1a9f5f5506f7b8f6f469fa821c2674` - // which has no `vout` for input in WebSocket response payload but block - // will show the input's vout value to be `0`. - vout: asOptional(asNumber, 0), - addresses: asArray(asAddress), - isAddress: asBoolean, - value: asString, - hex: asOptional(asString) - }) + asEither( + // Address input: + asObject({ + addresses: asArray(asAddress), + // `isAddress` is a boolean flag that indicates whether the input is an address. + // And therefore has `addresses` field. If `isAddress` is false, then the input is likely a coinbase input. + isAddress: asValue(true), + // This is the index of the input. Not to be confused with the index of the previous output (vout). + n: asNumber, + // Empirically observed omitted sequence is possible for when sequence is zero. + // Is the case for tx `19ecc679cfc7e71ad616a22bbee96fd5abe8616e4f408f1f5daaf137400ae091`. + sequence: asOptional(asNumber, 0), + txid: asString, + value: asString, + // If Blockbook doesn't provide vout, assume 0. Empirically observed + // case for tx `fefac8c22ba1178df5d7c90b78cc1c203d1a9f5f5506f7b8f6f469fa821c2674` + // which has no `vout` for input in WebSocket response payload but block + // will show the input's vout value to be `0`. + vout: asOptional(asNumber, 0), + hex: asOptional(asString) + }), + // Coinbase input: + asObject({ + // Coinbase input is a string of hex data (see example c6e617656b7b6d9fdcf8800fb5370479e5aceea4b6fe2fd74bd7bb0f3f2c64db) + coinbase: asString, + isAddress: asValue(false), + n: asNumber, + sequence: asNumber + }) + ) ), vout: asArray( asObject({ diff --git a/test/common/utxobased/network/Blockbook.spec.ts b/test/common/utxobased/network/Blockbook.spec.ts index 9c799708..ace89b65 100644 --- a/test/common/utxobased/network/Blockbook.spec.ts +++ b/test/common/utxobased/network/Blockbook.spec.ts @@ -272,14 +272,16 @@ describe('Blockbook', function () { }) describe('fetchTransaction', function () { - const satoshiHash = - '3ed86f1b0a0a6fe180195bc1f93fd9d0801aea8c8ad5018de82c026dc21e2b15' it('should fetch details from a transaction hash', async function () { + const satoshiHash = + '3ed86f1b0a0a6fe180195bc1f93fd9d0801aea8c8ad5018de82c026dc21e2b15' const tx = await blockbook.fetchTransaction(satoshiHash) tx.txid.should.equal(satoshiHash) tx.fees.should.equal('226') tx.blockHeight.should.equal(651329) + tx.vin[0].isAddress.should.equal(true) + if (!tx.vin[0].isAddress) throw new Error('Type assertion failed') tx.vin[0].value.should.equal('97373') tx.vin[0].txid.should.equal( 'fac5994d454817db2daec796cfa79cce670a372e7505fdef2a259289d5df0814' @@ -289,12 +291,32 @@ describe('Blockbook', function () { tx.vin[0].addresses.should.eqls([ 'bc1qg6lwu6c8yqhhw7rrq69akknepxcft09agkkuqv' ]) - tx.vin[0].isAddress.should.equal(true) tx.vin[0].value.should.equal('97373') tx.vout[1].value.should.equal('95000') tx.should.have.property('confirmations') tx.should.have.property('blockTime') }) + + it('should fetch details from a coinbase transaction hash', async function () { + const blockRewardTxid = + 'c6e617656b7b6d9fdcf8800fb5370479e5aceea4b6fe2fd74bd7bb0f3f2c64db' + const tx = await blockbook.fetchTransaction(blockRewardTxid) + + tx.txid.should.equal(blockRewardTxid) + tx.fees.should.equal('0') + tx.blockHeight.should.equal(868078) + tx.vin[0].isAddress.should.equal(false) + if (tx.vin[0].isAddress) throw new Error('Type assertion failed') + tx.vin[0].coinbase.should.equal( + '03ee3e0d1e3c204f4345414e2e58595a203e0f456c656b74726f6e20456e657267790007155cb4e4ff33420eb387b0ccf1ee1567020000000000' + ) + expect(tx.vin[0].sequence).to.equal(4294967295) + tx.vin[0].n.should.equal(0) + + tx.vout[1].value.should.equal('135549329') + tx.should.have.property('confirmations') + tx.should.have.property('blockTime') + }) }) })