Skip to content

Commit

Permalink
Merge pull request #417 from EdgeApp/sam/blockrewards
Browse files Browse the repository at this point in the history
Block Reward Transaction Support (Coinbase Txs)
  • Loading branch information
samholmes authored Nov 8, 2024
2 parents 7fd2074 + b166a70 commit 0e0686a
Show file tree
Hide file tree
Showing 8 changed files with 142 additions and 63 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## 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)

- fixed: Re-publish to NPM with missing files included.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
17 changes: 7 additions & 10 deletions src/common/utxobased/db/util/utxo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,18 @@ export const utxoFromTransactionDataInput = async (
inputIndex: number
): Promise<UtxoData> => {
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`)

// Optional redeemScript for "our addresses"
const redeemScript = address.redeemScript

const purposeType = currencyFormatToPurposeType(address.path.format)
const getScripts = async (
scriptPubkey: string
): Promise<{
const getScripts = async (): Promise<{
script: string
scriptType: ScriptTypeEnum
}> => {
Expand All @@ -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,
Expand Down
25 changes: 18 additions & 7 deletions src/common/utxobased/engine/ServerStates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<unknown> | boolean,
boolean
> = await generator.next(nextValue)

let result: IteratorResult<
WsTask<unknown> | boolean,
boolean
> = await generator.next()

while (true) {
if (result?.done === true) {
return result.value
}
Expand All @@ -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 }
})
}
}
}

Expand Down
47 changes: 32 additions & 15 deletions src/common/utxobased/engine/UtxoEngineProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
76 changes: 50 additions & 26 deletions src/common/utxobased/network/blockbookApi.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import {
asArray,
asBoolean,
asEither,
asMaybe,
asNumber,
asObject,
asOptional,
asString,
asUnknown,
asValue,
Cleaner,
uncleaner
} from 'cleaners'
Expand Down Expand Up @@ -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
Expand All @@ -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({
Expand Down
28 changes: 25 additions & 3 deletions test/common/utxobased/network/Blockbook.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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')
})
})
})
7 changes: 6 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit 0e0686a

Please sign in to comment.