@bsv/sdk 1.2.17 → 1.2.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/src/primitives/BigNumber.js +85 -89
- package/dist/cjs/src/primitives/BigNumber.js.map +1 -1
- package/dist/cjs/src/primitives/PublicKey.js +5 -2
- package/dist/cjs/src/primitives/PublicKey.js.map +1 -1
- package/dist/cjs/src/primitives/Random.js +3 -2
- package/dist/cjs/src/primitives/Random.js.map +1 -1
- package/dist/cjs/src/primitives/utils.js +71 -62
- package/dist/cjs/src/primitives/utils.js.map +1 -1
- package/dist/cjs/src/totp/totp.js +0 -1
- package/dist/cjs/src/totp/totp.js.map +1 -1
- package/dist/cjs/src/transaction/Beef.js +26 -40
- package/dist/cjs/src/transaction/Beef.js.map +1 -1
- package/dist/cjs/src/transaction/BeefParty.js +1 -1
- package/dist/cjs/src/transaction/BeefTx.js +75 -73
- package/dist/cjs/src/transaction/BeefTx.js.map +1 -1
- package/dist/cjs/src/transaction/MerklePath.js +12 -1
- package/dist/cjs/src/transaction/MerklePath.js.map +1 -1
- package/dist/cjs/src/transaction/Transaction.js +70 -96
- package/dist/cjs/src/transaction/Transaction.js.map +1 -1
- package/dist/cjs/src/transaction/chaintrackers/WhatsOnChain.js +20 -2
- package/dist/cjs/src/transaction/chaintrackers/WhatsOnChain.js.map +1 -1
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/src/primitives/BigNumber.js +85 -89
- package/dist/esm/src/primitives/BigNumber.js.map +1 -1
- package/dist/esm/src/primitives/PublicKey.js +5 -2
- package/dist/esm/src/primitives/PublicKey.js.map +1 -1
- package/dist/esm/src/primitives/Random.js +2 -2
- package/dist/esm/src/primitives/Random.js.map +1 -1
- package/dist/esm/src/primitives/utils.js +70 -61
- package/dist/esm/src/primitives/utils.js.map +1 -1
- package/dist/esm/src/totp/totp.js +0 -1
- package/dist/esm/src/totp/totp.js.map +1 -1
- package/dist/esm/src/transaction/Beef.js +25 -39
- package/dist/esm/src/transaction/Beef.js.map +1 -1
- package/dist/esm/src/transaction/BeefParty.js +1 -1
- package/dist/esm/src/transaction/BeefTx.js +76 -74
- package/dist/esm/src/transaction/BeefTx.js.map +1 -1
- package/dist/esm/src/transaction/MerklePath.js +12 -1
- package/dist/esm/src/transaction/MerklePath.js.map +1 -1
- package/dist/esm/src/transaction/Transaction.js +71 -97
- package/dist/esm/src/transaction/Transaction.js.map +1 -1
- package/dist/esm/src/transaction/chaintrackers/WhatsOnChain.js +20 -2
- package/dist/esm/src/transaction/chaintrackers/WhatsOnChain.js.map +1 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/types/src/primitives/BigNumber.d.ts +24 -22
- package/dist/types/src/primitives/BigNumber.d.ts.map +1 -1
- package/dist/types/src/primitives/PublicKey.d.ts.map +1 -1
- package/dist/types/src/primitives/utils.d.ts +17 -17
- package/dist/types/src/primitives/utils.d.ts.map +1 -1
- package/dist/types/src/transaction/Beef.d.ts +9 -12
- package/dist/types/src/transaction/Beef.d.ts.map +1 -1
- package/dist/types/src/transaction/BeefParty.d.ts +1 -1
- package/dist/types/src/transaction/BeefTx.d.ts +5 -2
- package/dist/types/src/transaction/BeefTx.d.ts.map +1 -1
- package/dist/types/src/transaction/ChainTracker.d.ts +6 -0
- package/dist/types/src/transaction/ChainTracker.d.ts.map +1 -1
- package/dist/types/src/transaction/MerklePath.d.ts +1 -0
- package/dist/types/src/transaction/MerklePath.d.ts.map +1 -1
- package/dist/types/src/transaction/Transaction.d.ts +6 -0
- package/dist/types/src/transaction/Transaction.d.ts.map +1 -1
- package/dist/types/src/transaction/chaintrackers/WhatsOnChain.d.ts +2 -1
- package/dist/types/src/transaction/chaintrackers/WhatsOnChain.d.ts.map +1 -1
- package/dist/types/tsconfig.types.tsbuildinfo +1 -1
- package/dist/umd/bundle.js +1 -1
- package/docs/compat.md +13 -11
- package/docs/primitives.md +152 -188
- package/docs/transaction.md +78 -76
- package/package.json +1 -1
- package/src/primitives/BigNumber.ts +111 -111
- package/src/primitives/PublicKey.ts +5 -2
- package/src/primitives/Random.ts +2 -2
- package/src/primitives/utils.ts +92 -77
- package/src/totp/totp.ts +0 -1
- package/src/transaction/Beef.ts +20 -43
- package/src/transaction/BeefParty.ts +1 -1
- package/src/transaction/BeefTx.ts +89 -57
- package/src/transaction/ChainTracker.ts +6 -0
- package/src/transaction/MerklePath.ts +13 -1
- package/src/transaction/Transaction.ts +77 -100
- package/src/transaction/__tests/Beef.test.ts +9 -9
- package/src/transaction/__tests/MerklePath.test.ts +23 -2
- package/src/transaction/__tests/Transaction.benchmarks.test.ts +1 -1
- package/src/transaction/__tests/Transaction.test.ts +3 -3
- package/src/transaction/broadcasters/__tests/WhatsOnChainBroadcaster.test.ts +2 -2
- package/src/transaction/chaintrackers/WhatsOnChain.ts +20 -2
- package/src/transaction/chaintrackers/__tests/WhatsOnChainChainTracker.test.ts +32 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { hash256 } from '../primitives/Hash.js'
|
|
2
2
|
import { Reader, Writer, toHex, toArray } from '../primitives/utils.js'
|
|
3
3
|
import Transaction from './Transaction.js'
|
|
4
|
-
import {
|
|
4
|
+
import { BEEF_V2, TX_DATA_FORMAT } from './Beef.js'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* A single bitcoin transaction associated with a `Beef` validity proof set.
|
|
@@ -42,20 +42,32 @@ export default class BeefTx {
|
|
|
42
42
|
|
|
43
43
|
get txid () {
|
|
44
44
|
if (this._txid) return this._txid
|
|
45
|
-
if (this._tx)
|
|
46
|
-
|
|
45
|
+
if (this._tx) {
|
|
46
|
+
this._txid = this._tx.id('hex')
|
|
47
|
+
return this._txid
|
|
48
|
+
}
|
|
49
|
+
if (this._rawTx) {
|
|
50
|
+
this._txid = toHex(hash256(this._rawTx))
|
|
51
|
+
return this._txid
|
|
52
|
+
}
|
|
47
53
|
throw new Error('Internal')
|
|
48
54
|
}
|
|
49
55
|
|
|
50
56
|
get tx () {
|
|
51
57
|
if (this._tx) return this._tx
|
|
52
|
-
if (this._rawTx)
|
|
58
|
+
if (this._rawTx) {
|
|
59
|
+
this._tx = Transaction.fromBinary(this._rawTx)
|
|
60
|
+
return this._tx
|
|
61
|
+
}
|
|
53
62
|
return undefined
|
|
54
63
|
}
|
|
55
64
|
|
|
56
65
|
get rawTx () {
|
|
57
66
|
if (this._rawTx) return this._rawTx
|
|
58
|
-
if (this._tx)
|
|
67
|
+
if (this._tx) {
|
|
68
|
+
this._rawTx = this._tx.toBinary()
|
|
69
|
+
return this._rawTx
|
|
70
|
+
}
|
|
59
71
|
return undefined
|
|
60
72
|
}
|
|
61
73
|
|
|
@@ -66,87 +78,107 @@ export default class BeefTx {
|
|
|
66
78
|
constructor (tx: Transaction | number[] | string, bumpIndex?: number) {
|
|
67
79
|
if (typeof tx === 'string') {
|
|
68
80
|
this._txid = tx
|
|
81
|
+
} else if (Array.isArray(tx)) {
|
|
82
|
+
this._rawTx = tx
|
|
69
83
|
} else {
|
|
70
|
-
|
|
71
|
-
this._rawTx = tx
|
|
72
|
-
} else {
|
|
73
|
-
this._tx = tx
|
|
74
|
-
}
|
|
84
|
+
this._tx = tx
|
|
75
85
|
}
|
|
76
86
|
this.bumpIndex = bumpIndex
|
|
77
87
|
this.updateInputTxids()
|
|
78
88
|
}
|
|
79
89
|
|
|
90
|
+
static fromTx (tx: Transaction, bumpIndex?: number): BeefTx {
|
|
91
|
+
return new BeefTx(tx, bumpIndex)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
static fromRawTx (rawTx: number[], bumpIndex?: number): BeefTx {
|
|
95
|
+
return new BeefTx(rawTx, bumpIndex)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
static fromTxid (txid: string, bumpIndex?: number): BeefTx {
|
|
99
|
+
return new BeefTx(txid, bumpIndex)
|
|
100
|
+
}
|
|
101
|
+
|
|
80
102
|
private updateInputTxids () {
|
|
81
|
-
if (this.hasProof || !this.tx)
|
|
82
|
-
|
|
83
|
-
|
|
103
|
+
if (this.hasProof || !this.tx) {
|
|
104
|
+
// If we have a proof, or don't have a parsed transaction
|
|
105
|
+
this.inputTxids = []
|
|
106
|
+
} else {
|
|
84
107
|
const inputTxids = {}
|
|
85
108
|
for (const input of this.tx.inputs) { inputTxids[input.sourceTXID] = true }
|
|
86
109
|
this.inputTxids = Object.keys(inputTxids)
|
|
87
110
|
}
|
|
88
111
|
}
|
|
89
112
|
|
|
90
|
-
toWriter (writer: Writer,
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
113
|
+
toWriter (writer: Writer, version: number): void {
|
|
114
|
+
const writeByte = (bb: number) => {
|
|
115
|
+
writer.writeUInt8(bb)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const writeTxid = () => {
|
|
119
|
+
writer.writeReverse(toArray(this._txid, 'hex'))
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const writeTx = () => {
|
|
123
|
+
if (this._rawTx) {
|
|
124
|
+
writer.write(this._rawTx)
|
|
125
|
+
} else if (this._tx) {
|
|
126
|
+
writer.write(this._tx.toBinary())
|
|
127
|
+
} else {
|
|
128
|
+
throw new Error('a valid serialized Transaction is expected')
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const writeBumpIndex = () => {
|
|
98
133
|
if (this.bumpIndex === undefined) {
|
|
99
|
-
|
|
134
|
+
writeByte(TX_DATA_FORMAT.RAWTX) // 0
|
|
100
135
|
} else {
|
|
101
|
-
|
|
102
|
-
writer.writeVarIntNum(this.bumpIndex)
|
|
136
|
+
writeByte(TX_DATA_FORMAT.RAWTX_AND_BUMP_INDEX) // 1
|
|
137
|
+
writer.writeVarIntNum(this.bumpIndex) // the index of the associated bump
|
|
103
138
|
}
|
|
104
|
-
}
|
|
105
|
-
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (version === BEEF_V2) {
|
|
106
142
|
if (this.isTxidOnly) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
143
|
+
writeByte(TX_DATA_FORMAT.TXID_ONLY)
|
|
144
|
+
writeTxid()
|
|
145
|
+
} else if (this.bumpIndex !== undefined) {
|
|
146
|
+
writeByte(TX_DATA_FORMAT.RAWTX_AND_BUMP_INDEX)
|
|
147
|
+
writer.writeVarIntNum(this.bumpIndex)
|
|
148
|
+
writeTx()
|
|
110
149
|
} else {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
} else {
|
|
114
|
-
writer.writeUInt8(1)
|
|
115
|
-
writer.writeVarIntNum(this.bumpIndex)
|
|
116
|
-
}
|
|
117
|
-
if (this._rawTx) { writer.write(this._rawTx) } else if (this._tx) { writer.write(this._tx.toBinary()) } else { throw new Error('a valid serialized Transaction is expected') }
|
|
150
|
+
writeByte(TX_DATA_FORMAT.RAWTX)
|
|
151
|
+
writeTx()
|
|
118
152
|
}
|
|
153
|
+
} else {
|
|
154
|
+
writeTx()
|
|
155
|
+
writeBumpIndex()
|
|
119
156
|
}
|
|
120
157
|
}
|
|
121
158
|
|
|
122
|
-
static fromReader (br: Reader,
|
|
123
|
-
let
|
|
159
|
+
static fromReader (br: Reader, version: number): BeefTx {
|
|
160
|
+
let data: Transaction | number[] | string | undefined
|
|
124
161
|
let bumpIndex: number | undefined
|
|
125
|
-
|
|
126
|
-
if (
|
|
127
|
-
// V1
|
|
128
|
-
const version = br.readUInt32LE()
|
|
129
|
-
if (version === BEEF_MAGIC_TXID_ONLY_EXTENSION) {
|
|
130
|
-
// This is the extension to support known transactions
|
|
131
|
-
tx = toHex(br.readReverse(32))
|
|
132
|
-
} else {
|
|
133
|
-
br.pos -= 4 // Unread the version...
|
|
134
|
-
tx = Transaction.fromReader(br)
|
|
135
|
-
}
|
|
136
|
-
bumpIndex = br.readUInt8() ? br.readVarIntNum() : undefined
|
|
137
|
-
} else {
|
|
162
|
+
let beefTx: BeefTx | undefined
|
|
163
|
+
if (version === BEEF_V2) {
|
|
138
164
|
// V2
|
|
139
165
|
const format = br.readUInt8()
|
|
140
|
-
if (format ===
|
|
141
|
-
|
|
142
|
-
tx = toHex(br.readReverse(32))
|
|
166
|
+
if (format === TX_DATA_FORMAT.TXID_ONLY) {
|
|
167
|
+
beefTx = BeefTx.fromTxid(toHex(br.readReverse(32)))
|
|
143
168
|
} else {
|
|
144
|
-
if (format ===
|
|
145
|
-
|
|
169
|
+
if (format === TX_DATA_FORMAT.RAWTX_AND_BUMP_INDEX) {
|
|
170
|
+
bumpIndex = br.readVarIntNum()
|
|
171
|
+
}
|
|
172
|
+
data = Transaction.fromReader(br)
|
|
173
|
+
beefTx = BeefTx.fromTx(data, bumpIndex)
|
|
146
174
|
}
|
|
175
|
+
} else {
|
|
176
|
+
// V1
|
|
177
|
+
data = Transaction.fromReader(br)
|
|
178
|
+
bumpIndex = br.readUInt8() ? br.readVarIntNum() : undefined
|
|
179
|
+
beefTx = BeefTx.fromTx(data, bumpIndex)
|
|
147
180
|
}
|
|
148
181
|
|
|
149
|
-
const beefTx = new BeefTx(tx, bumpIndex)
|
|
150
182
|
return beefTx
|
|
151
183
|
}
|
|
152
184
|
}
|
|
@@ -10,13 +10,19 @@
|
|
|
10
10
|
* @function isValidRootForHeight - A method to verify the validity of a Merkle root
|
|
11
11
|
* for a given block height.
|
|
12
12
|
*
|
|
13
|
+
* @function currentHeight - A method to get the current block height.
|
|
14
|
+
*
|
|
13
15
|
* @example
|
|
14
16
|
* const chainTracker = {
|
|
15
17
|
* isValidRootForHeight: async (root, height) => {
|
|
16
18
|
* // Implementation to check if the Merkle root is valid for the specified block height.
|
|
17
19
|
* }
|
|
20
|
+
* currentHeight: async () => {
|
|
21
|
+
* // Implementation to get the current block height.
|
|
22
|
+
* }
|
|
18
23
|
* };
|
|
19
24
|
*/
|
|
20
25
|
export default interface ChainTracker {
|
|
21
26
|
isValidRootForHeight: (root: string, height: number) => Promise<boolean>
|
|
27
|
+
currentHeight: () => Promise<number>
|
|
22
28
|
}
|
|
@@ -192,6 +192,11 @@ export default class MerklePath {
|
|
|
192
192
|
return toHex(this.toBinary())
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
+
//
|
|
196
|
+
private indexOf (txid: string): number {
|
|
197
|
+
return this.path[0].find(l => l.hash === txid).offset
|
|
198
|
+
}
|
|
199
|
+
|
|
195
200
|
/**
|
|
196
201
|
* Computes the Merkle root from the provided transaction ID.
|
|
197
202
|
*
|
|
@@ -204,7 +209,7 @@ export default class MerklePath {
|
|
|
204
209
|
txid = this.path[0].find(leaf => Boolean(leaf?.hash)).hash
|
|
205
210
|
}
|
|
206
211
|
// Find the index of the txid at the lowest level of the Merkle tree
|
|
207
|
-
const index = this.
|
|
212
|
+
const index = this.indexOf(txid)
|
|
208
213
|
if (typeof index !== 'number') {
|
|
209
214
|
throw new Error(`This proof does not contain the txid: ${txid}`)
|
|
210
215
|
}
|
|
@@ -282,6 +287,13 @@ export default class MerklePath {
|
|
|
282
287
|
*/
|
|
283
288
|
async verify (txid: string, chainTracker: ChainTracker): Promise<boolean> {
|
|
284
289
|
const root = this.computeRoot(txid)
|
|
290
|
+
if (this.indexOf(txid) === 0) {
|
|
291
|
+
// Coinbase transaction outputs can only be spent once they're 100 blocks deep.
|
|
292
|
+
const height = await chainTracker.currentHeight()
|
|
293
|
+
if (this.blockHeight + 100 < height) {
|
|
294
|
+
return false
|
|
295
|
+
}
|
|
296
|
+
}
|
|
285
297
|
// Use the chain tracker to determine whether this is a valid merkle root at the given block height
|
|
286
298
|
return await chainTracker.isValidRootForHeight(root, this.blockHeight)
|
|
287
299
|
}
|
|
@@ -12,7 +12,7 @@ import Spend from '../script/Spend.js'
|
|
|
12
12
|
import ChainTracker from './ChainTracker.js'
|
|
13
13
|
import { defaultBroadcaster } from './broadcasters/DefaultBroadcaster.js'
|
|
14
14
|
import { defaultChainTracker } from './chaintrackers/DefaultChainTracker.js'
|
|
15
|
-
import { ATOMIC_BEEF,
|
|
15
|
+
import { ATOMIC_BEEF, BEEF_V1 } from './Beef.js'
|
|
16
16
|
import P2PKH from '../script/templates/P2PKH.js'
|
|
17
17
|
|
|
18
18
|
/**
|
|
@@ -59,6 +59,29 @@ export default class Transaction {
|
|
|
59
59
|
merklePath?: MerklePath
|
|
60
60
|
private cachedHash?: number[]
|
|
61
61
|
|
|
62
|
+
// Recursive function for adding merkle proofs or input transactions
|
|
63
|
+
private static addPathOrInputs (obj: { pathIndex?: number, tx: Transaction }, transactions: Record<string, {
|
|
64
|
+
pathIndex?: number
|
|
65
|
+
tx: Transaction
|
|
66
|
+
}>, BUMPs: MerklePath[]): void {
|
|
67
|
+
if (typeof obj.pathIndex === 'number') {
|
|
68
|
+
const path = BUMPs[obj.pathIndex]
|
|
69
|
+
if (typeof path !== 'object') {
|
|
70
|
+
throw new Error('Invalid merkle path index found in BEEF!')
|
|
71
|
+
}
|
|
72
|
+
obj.tx.merklePath = path
|
|
73
|
+
} else {
|
|
74
|
+
for (const input of obj.tx.inputs) {
|
|
75
|
+
const sourceObj = transactions[input.sourceTXID]
|
|
76
|
+
if (typeof sourceObj !== 'object') {
|
|
77
|
+
throw new Error(`Reference to unknown TXID in BEEF: ${input.sourceTXID}`)
|
|
78
|
+
}
|
|
79
|
+
input.sourceTransaction = sourceObj.tx
|
|
80
|
+
this.addPathOrInputs(sourceObj, transactions, BUMPs)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
62
85
|
/**
|
|
63
86
|
* Creates a new transaction, linked to its inputs and their associated merkle paths, from a BEEF (BRC-62) structure.
|
|
64
87
|
* Optionally, you can provide a specific TXID to retrieve a particular transaction from the BEEF data.
|
|
@@ -81,28 +104,7 @@ export default class Transaction {
|
|
|
81
104
|
throw new Error(`Transaction with TXID ${targetTXID} not found in BEEF data.`)
|
|
82
105
|
}
|
|
83
106
|
|
|
84
|
-
|
|
85
|
-
const addPathOrInputs = (obj: { pathIndex?: number, tx: Transaction }): void => {
|
|
86
|
-
if (typeof obj.pathIndex === 'number') {
|
|
87
|
-
const path = BUMPs[obj.pathIndex]
|
|
88
|
-
if (typeof path !== 'object') {
|
|
89
|
-
throw new Error('Invalid merkle path index found in BEEF!')
|
|
90
|
-
}
|
|
91
|
-
obj.tx.merklePath = path
|
|
92
|
-
} else {
|
|
93
|
-
for (let i = 0; i < obj.tx.inputs.length; i++) {
|
|
94
|
-
const input = obj.tx.inputs[i]
|
|
95
|
-
const sourceObj = transactions[input.sourceTXID]
|
|
96
|
-
if (typeof sourceObj !== 'object') {
|
|
97
|
-
throw new Error(`Reference to unknown TXID in BEEF: ${input.sourceTXID}`)
|
|
98
|
-
}
|
|
99
|
-
input.sourceTransaction = sourceObj.tx
|
|
100
|
-
addPathOrInputs(sourceObj)
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
addPathOrInputs(transactions[targetTXID])
|
|
107
|
+
this.addPathOrInputs(transactions[targetTXID], transactions, BUMPs)
|
|
106
108
|
|
|
107
109
|
return transactions[targetTXID].tx
|
|
108
110
|
}
|
|
@@ -172,28 +174,7 @@ export default class Transaction {
|
|
|
172
174
|
throw new Error(`Unrelated transaction with TXID ${txid} found in Atomic BEEF data.`)
|
|
173
175
|
}
|
|
174
176
|
|
|
175
|
-
|
|
176
|
-
const addPathOrInputs = (obj: { pathIndex?: number, tx: Transaction }): void => {
|
|
177
|
-
if (typeof obj.pathIndex === 'number') {
|
|
178
|
-
const path = BUMPs[obj.pathIndex]
|
|
179
|
-
if (typeof path !== 'object') {
|
|
180
|
-
throw new Error('Invalid merkle path index found in BEEF!')
|
|
181
|
-
}
|
|
182
|
-
obj.tx.merklePath = path
|
|
183
|
-
} else {
|
|
184
|
-
for (let i = 0; i < obj.tx.inputs.length; i++) {
|
|
185
|
-
const input = obj.tx.inputs[i]
|
|
186
|
-
const sourceObj = transactions[input.sourceTXID]
|
|
187
|
-
if (typeof sourceObj !== 'object') {
|
|
188
|
-
throw new Error(`Reference to unknown TXID in BEEF: ${input.sourceTXID}`)
|
|
189
|
-
}
|
|
190
|
-
input.sourceTransaction = sourceObj.tx
|
|
191
|
-
addPathOrInputs(sourceObj)
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
addPathOrInputs(transactions[subjectTXID])
|
|
177
|
+
this.addPathOrInputs(transactions[subjectTXID], transactions, BUMPs)
|
|
197
178
|
|
|
198
179
|
return transactions[subjectTXID].tx
|
|
199
180
|
}
|
|
@@ -207,8 +188,8 @@ export default class Transaction {
|
|
|
207
188
|
private static parseBEEFData (reader: Reader): { transactions: Record<string, { pathIndex?: number, tx: Transaction }>, BUMPs: MerklePath[] } {
|
|
208
189
|
// Read the version
|
|
209
190
|
const version = reader.readUInt32LE()
|
|
210
|
-
if (version !==
|
|
211
|
-
throw new Error(`Invalid BEEF version. Expected ${
|
|
191
|
+
if (version !== BEEF_V1) {
|
|
192
|
+
throw new Error(`Invalid BEEF version. Expected ${BEEF_V1}, received ${version}.`)
|
|
212
193
|
}
|
|
213
194
|
|
|
214
195
|
// Read the BUMPs
|
|
@@ -511,7 +492,15 @@ export default class Transaction {
|
|
|
511
492
|
}
|
|
512
493
|
}
|
|
513
494
|
const fee = await modelOrFee.computeFee(this)
|
|
514
|
-
|
|
495
|
+
const change = this.calculateChange(fee)
|
|
496
|
+
if (change <= 0) {
|
|
497
|
+
this.outputs = this.outputs.filter(output => !output.change)
|
|
498
|
+
return
|
|
499
|
+
}
|
|
500
|
+
this.distributeChange(change, changeDistribution)
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
private calculateChange (fee: number): number {
|
|
515
504
|
let change = 0
|
|
516
505
|
for (const input of this.inputs) {
|
|
517
506
|
if (typeof input.sourceTransaction !== 'object') {
|
|
@@ -520,71 +509,60 @@ export default class Transaction {
|
|
|
520
509
|
change += input.sourceTransaction.outputs[input.sourceOutputIndex].satoshis
|
|
521
510
|
}
|
|
522
511
|
change -= fee
|
|
523
|
-
let changeCount = 0
|
|
524
512
|
for (const out of this.outputs) {
|
|
525
513
|
if (!out.change) {
|
|
526
514
|
change -= out.satoshis
|
|
527
|
-
} else {
|
|
528
|
-
changeCount++
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
if (change <= changeCount) {
|
|
533
|
-
// There is not enough change to distribute among the change outputs.
|
|
534
|
-
// We'll remove all change outputs and leave the extra for the miners.
|
|
535
|
-
for (let i = 0; i < this.outputs.length; i++) {
|
|
536
|
-
if (this.outputs[i].change) {
|
|
537
|
-
this.outputs.splice(i, 1)
|
|
538
|
-
i--
|
|
539
|
-
}
|
|
540
515
|
}
|
|
541
|
-
return
|
|
542
516
|
}
|
|
517
|
+
return change
|
|
518
|
+
}
|
|
543
519
|
|
|
544
|
-
|
|
520
|
+
private distributeChange (change: number, changeDistribution: 'equal' | 'random'): void {
|
|
545
521
|
let distributedChange = 0
|
|
522
|
+
const changeOutputs = this.outputs.filter(out => out.change)
|
|
546
523
|
if (changeDistribution === 'random') {
|
|
547
|
-
|
|
548
|
-
const changeOutputs = this.outputs.filter(out => out.change)
|
|
549
|
-
let changeToUse = change
|
|
550
|
-
|
|
551
|
-
// Helper function to generate a number approximating Benford's Law
|
|
552
|
-
const benfordNumber = (min: number, max: number): number => {
|
|
553
|
-
const d = Math.floor(Math.random() * 9) + 1
|
|
554
|
-
return Math.floor(min + (max - min) * Math.log10(1 + 1 / d) / Math.log10(10))
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
const benfordNumbers = Array(changeOutputs.length).fill(1)
|
|
558
|
-
changeToUse -= changeOutputs.length
|
|
559
|
-
distributedChange += changeOutputs.length
|
|
560
|
-
for (let i = 0; i < changeOutputs.length - 1; i++) {
|
|
561
|
-
const portion = benfordNumber(0, changeToUse)
|
|
562
|
-
benfordNumbers[i] += portion
|
|
563
|
-
distributedChange += portion
|
|
564
|
-
changeToUse -= portion
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
for (let i = 0; i < this.outputs.length; i++) {
|
|
568
|
-
if (this.outputs[i].change) {
|
|
569
|
-
const t = benfordNumbers.shift()
|
|
570
|
-
this.outputs[i].satoshis = t
|
|
571
|
-
}
|
|
572
|
-
}
|
|
524
|
+
distributedChange = this.distributeRandomChange(change, changeOutputs)
|
|
573
525
|
} else if (changeDistribution === 'equal') {
|
|
574
|
-
|
|
575
|
-
for (const out of this.outputs) {
|
|
576
|
-
if (out.change) {
|
|
577
|
-
distributedChange += perOutput
|
|
578
|
-
out.satoshis = perOutput
|
|
579
|
-
}
|
|
580
|
-
}
|
|
526
|
+
distributedChange = this.distributeEqualChange(change, changeOutputs)
|
|
581
527
|
}
|
|
582
|
-
// if there's any remaining change, add it to the last output
|
|
583
528
|
if (distributedChange < change) {
|
|
584
529
|
this.outputs[this.outputs.length - 1].satoshis += (change - distributedChange)
|
|
585
530
|
}
|
|
586
531
|
}
|
|
587
532
|
|
|
533
|
+
private distributeRandomChange (change: number, changeOutputs: TransactionOutput[]): number {
|
|
534
|
+
let distributedChange = 0
|
|
535
|
+
let changeToUse = change
|
|
536
|
+
const benfordNumbers = Array(changeOutputs.length).fill(1)
|
|
537
|
+
changeToUse -= changeOutputs.length
|
|
538
|
+
distributedChange += changeOutputs.length
|
|
539
|
+
for (let i = 0; i < changeOutputs.length - 1; i++) {
|
|
540
|
+
const portion = this.benfordNumber(0, changeToUse)
|
|
541
|
+
benfordNumbers[i] += portion
|
|
542
|
+
distributedChange += portion
|
|
543
|
+
changeToUse -= portion
|
|
544
|
+
}
|
|
545
|
+
for (const output of this.outputs) {
|
|
546
|
+
if (output.change) output.satoshis = benfordNumbers.shift()
|
|
547
|
+
}
|
|
548
|
+
return distributedChange
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
private distributeEqualChange (change: number, changeOutputs: TransactionOutput[]): number {
|
|
552
|
+
let distributedChange = 0
|
|
553
|
+
const perOutput = Math.floor(change / changeOutputs.length)
|
|
554
|
+
for (const out of changeOutputs) {
|
|
555
|
+
distributedChange += perOutput
|
|
556
|
+
out.satoshis = perOutput
|
|
557
|
+
}
|
|
558
|
+
return distributedChange
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
private benfordNumber (min: number, max: number): number {
|
|
562
|
+
const d = Math.floor(Math.random() * 9) + 1
|
|
563
|
+
return Math.floor(min + (max - min) * Math.log10(1 + 1 / d) / Math.log10(10))
|
|
564
|
+
}
|
|
565
|
+
|
|
588
566
|
/**
|
|
589
567
|
* Utility method that returns the current fee based on inputs and outputs
|
|
590
568
|
*
|
|
@@ -923,9 +901,8 @@ export default class Transaction {
|
|
|
923
901
|
* @throws Error if there are any missing sourceTransactions unless `allowPartial` is true.
|
|
924
902
|
*/
|
|
925
903
|
toBEEF (allowPartial?: boolean): number[] {
|
|
926
|
-
this.outputs.length
|
|
927
904
|
const writer = new Writer()
|
|
928
|
-
writer.writeUInt32LE(
|
|
905
|
+
writer.writeUInt32LE(BEEF_V1)
|
|
929
906
|
const BUMPs: MerklePath[] = []
|
|
930
907
|
const txs: Array<{ tx: Transaction, pathIndex?: number }> = []
|
|
931
908
|
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
// The following imports allow flag a large number type checking errors by the VsCode editor due to missing type information:
|
|
2
2
|
import BeefTx from "../../../dist/cjs/src/transaction/BeefTx"
|
|
3
|
-
import Beef from "../../../dist/cjs/src/transaction/Beef"
|
|
4
3
|
import BeefParty from "../../../dist/cjs/src/transaction/BeefParty"
|
|
5
|
-
import {
|
|
4
|
+
import Beef, { BEEF_V1, BEEF_V2 } from "../../../dist/cjs/src/transaction/Beef"
|
|
6
5
|
import Transaction from "../../../dist/cjs/src/transaction/Transaction"
|
|
7
6
|
import { fromBase58 } from '../../../dist/cjs/src/primitives/utils'
|
|
8
7
|
|
|
@@ -11,7 +10,7 @@ import { fromBase58 } from '../../../dist/cjs/src/primitives/utils'
|
|
|
11
10
|
import BeefTx from '../BeefTx'
|
|
12
11
|
import Beef from '../Beef'
|
|
13
12
|
import BeefParty from "../BeefParty"
|
|
14
|
-
import {
|
|
13
|
+
import { BEEF_V1, BEEF_V2 } from "../Beef"
|
|
15
14
|
import Transaction from "../Transaction"
|
|
16
15
|
import { fromBase58 } from "../../primitives/utils"
|
|
17
16
|
*/
|
|
@@ -23,6 +22,7 @@ describe('Beef tests', () => {
|
|
|
23
22
|
isValidRootForHeight: async (root: string, height: number) => {
|
|
24
23
|
switch (height) {
|
|
25
24
|
case 1631619: return root === "b3975a6b69b5ce7fa200649d879f79a11f4d95c054cfe024570be7d60306ecf6"
|
|
25
|
+
case 875732: return root === "a19c54129ab996c72cda7721b4555b47d11b21e1fe67aa63c59843edb302b6c2"
|
|
26
26
|
default: throw new Error(`unknown height ${height}`)
|
|
27
27
|
}
|
|
28
28
|
}
|
|
@@ -101,7 +101,7 @@ describe('Beef tests', () => {
|
|
|
101
101
|
{
|
|
102
102
|
const version = 4290641921
|
|
103
103
|
expect(() => Beef.fromString(beefs[1]))
|
|
104
|
-
.toThrow(`Serialized BEEF must start with ${
|
|
104
|
+
.toThrow(`Serialized BEEF must start with ${BEEF_V1} or ${BEEF_V2} but starts with ${version}`)
|
|
105
105
|
}
|
|
106
106
|
})
|
|
107
107
|
|
|
@@ -262,7 +262,7 @@ describe('Beef tests', () => {
|
|
|
262
262
|
{
|
|
263
263
|
const beef = Beef.fromString(beefs[0])
|
|
264
264
|
expect(beef.toHex()).toBe(beefs[0])
|
|
265
|
-
|
|
265
|
+
beef.sortTxs()
|
|
266
266
|
const beefHex = beef.toHex()
|
|
267
267
|
const tx = beef.txs[beef.txs.length - 1].tx!
|
|
268
268
|
expect(tx).toBeTruthy()
|
|
@@ -284,7 +284,7 @@ describe('Beef tests', () => {
|
|
|
284
284
|
const beef = Beef.fromString(beefs[0])
|
|
285
285
|
expect(beef.toHex()).toBe(beefs[0])
|
|
286
286
|
beef.mergeTransaction(Transaction.fromHex(txs[0]))
|
|
287
|
-
|
|
287
|
+
beef.sortTxs()
|
|
288
288
|
const beefHex = beef.toHex()
|
|
289
289
|
const tx = beef.txs[beef.txs.length - 1].tx!
|
|
290
290
|
expect(tx).toBeTruthy()
|
|
@@ -301,8 +301,8 @@ describe('Beef tests', () => {
|
|
|
301
301
|
const beef = Beef.fromString(beefs[0])
|
|
302
302
|
const tx = Transaction.fromHex(txs[0])
|
|
303
303
|
beef.mergeTransaction(tx)
|
|
304
|
-
|
|
305
|
-
|
|
304
|
+
beef.sortTxs()
|
|
305
|
+
beef.toLogString()
|
|
306
306
|
const atomic = beef.toBinaryAtomic(tx.id('hex'))
|
|
307
307
|
const t2 = Transaction.fromAtomicBEEF(atomic)
|
|
308
308
|
const beef2 = t2.toAtomicBEEF()
|
|
@@ -321,7 +321,7 @@ describe('Beef tests', () => {
|
|
|
321
321
|
{
|
|
322
322
|
const beef = new Beef()
|
|
323
323
|
const atx = beef.mergeTxidOnly('a')
|
|
324
|
-
|
|
324
|
+
beef.mergeTxidOnly('b')
|
|
325
325
|
atx.inputTxids = ['b']
|
|
326
326
|
beef.sortTxs()
|
|
327
327
|
expect(beef.txs[1].txid).toBe('a')
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import ChainTracker from '../ChainTracker'
|
|
2
|
+
import MerklePath from '../../../dist/cjs/src/transaction/MerklePath.js'
|
|
2
3
|
import invalidBumps from './bump.invalid.vectors'
|
|
3
4
|
import validBumps from './bump.valid.vectors'
|
|
4
5
|
|
|
@@ -111,6 +112,15 @@ const BRC74TXID1 = '304e737fdfcb017a1a322e78b067ecebb5e07b44f0a36ed1f01264d2014f
|
|
|
111
112
|
const BRC74TXID2 = 'd888711d588021e588984e8278a2decf927298173a06737066e43f3e75534e00'
|
|
112
113
|
const BRC74TXID3 = '98c9c5dd79a18f40837061d5e0395ffb52e700a2689e641d19f053fc9619445e'
|
|
113
114
|
|
|
115
|
+
class FakeChainTracker implements ChainTracker {
|
|
116
|
+
async isValidRootForHeight (root: string, height: number): Promise<boolean> {
|
|
117
|
+
return root === 'd5377a7aba0c0e0dbaef230f8917217b453484c83579e11a14c8299faa57ef02' && height === 10000
|
|
118
|
+
}
|
|
119
|
+
async currentHeight (): Promise<number> {
|
|
120
|
+
return 10100
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
114
124
|
describe('MerklePath', () => {
|
|
115
125
|
it('Parses from hex', () => {
|
|
116
126
|
const path = MerklePath.fromHex(BRC74Hex)
|
|
@@ -129,7 +139,8 @@ describe('MerklePath', () => {
|
|
|
129
139
|
it('Verifies using a ChainTracker', async () => {
|
|
130
140
|
const path = new MerklePath(BRC74JSON.blockHeight, BRC74JSON.path)
|
|
131
141
|
const tracker = {
|
|
132
|
-
isValidRootForHeight: jest.fn((root, height) => root === BRC74Root && height === BRC74JSON.blockHeight)
|
|
142
|
+
isValidRootForHeight: jest.fn(async (root, height) => root === BRC74Root && height === BRC74JSON.blockHeight),
|
|
143
|
+
currentHeight: jest.fn(async () => 2029209)
|
|
133
144
|
}
|
|
134
145
|
const result = await path.verify(BRC74TXID1, tracker)
|
|
135
146
|
expect(result).toBe(true)
|
|
@@ -192,4 +203,14 @@ describe('MerklePath', () => {
|
|
|
192
203
|
it('Creates a valid MerklePath from a txid', () => {
|
|
193
204
|
expect(() => MerklePath.fromCoinbaseTxidAndHeight('d5377a7aba0c0e0dbaef230f8917217b453484c83579e11a14c8299faa57ef02', 1)).not.toThrowError()
|
|
194
205
|
})
|
|
206
|
+
it('Valid for Coinbase if currentHeight is more than 100 blocks prior', async () => {
|
|
207
|
+
const mp = MerklePath.fromCoinbaseTxidAndHeight('d5377a7aba0c0e0dbaef230f8917217b453484c83579e11a14c8299faa57ef02', 10000)
|
|
208
|
+
const isValid = await mp.verify('d5377a7aba0c0e0dbaef230f8917217b453484c83579e11a14c8299faa57ef02', new FakeChainTracker())
|
|
209
|
+
expect(isValid).toBe(true)
|
|
210
|
+
})
|
|
211
|
+
it('Invalid for Coinbase if currentheight is less than 100 blocks prior ', async () => {
|
|
212
|
+
const mp = MerklePath.fromCoinbaseTxidAndHeight('d5377a7aba0c0e0dbaef230f8917217b453484c83579e11a14c8299faa57ef02', 10099)
|
|
213
|
+
const isValid = await mp.verify('d5377a7aba0c0e0dbaef230f8917217b453484c83579e11a14c8299faa57ef02', new FakeChainTracker())
|
|
214
|
+
expect(isValid).toBe(false)
|
|
215
|
+
})
|
|
195
216
|
})
|
|
@@ -20,7 +20,7 @@ async function measureTime(fn: () => Promise<void>): Promise<number> {
|
|
|
20
20
|
describe('Transaction Verification Benchmark', () => {
|
|
21
21
|
const privateKey = new PrivateKey(1)
|
|
22
22
|
const publicKey = privateKey.toPublicKey()
|
|
23
|
-
const publicKeyHash =
|
|
23
|
+
const publicKeyHash = publicKey.toHash()
|
|
24
24
|
const p2pkh = new P2PKH()
|
|
25
25
|
|
|
26
26
|
it('verifies a transaction with a deep input chain', async () => {
|
|
@@ -11,7 +11,7 @@ import Curve from '../../../dist/cjs/src/primitives/Curve'
|
|
|
11
11
|
import P2PKH from '../../../dist/cjs/src/script/templates/P2PKH'
|
|
12
12
|
import fromUtxo from '../../../dist/cjs/src/compat/Utxo'
|
|
13
13
|
import MerklePath from '../../../dist/cjs/src/transaction/MerklePath'
|
|
14
|
-
import {
|
|
14
|
+
import { BEEF_V1 } from '../../../dist/cjs/src/transaction/Beef'
|
|
15
15
|
|
|
16
16
|
import sighashVectors from '../../primitives/__tests/sighash.vectors'
|
|
17
17
|
import invalidTransactions from './tx.invalid.vectors'
|
|
@@ -817,7 +817,7 @@ describe('Transaction', () => {
|
|
|
817
817
|
const fakeTXID = toArray('00'.repeat(32), 'hex')
|
|
818
818
|
writer.write(fakeTXID)
|
|
819
819
|
// Write empty BEEF data
|
|
820
|
-
writer.writeUInt32LE(
|
|
820
|
+
writer.writeUInt32LE(BEEF_V1) // BEEF version
|
|
821
821
|
writer.writeVarIntNum(0) // No BUMPs
|
|
822
822
|
writer.writeVarIntNum(0) // No transactions
|
|
823
823
|
|
|
@@ -890,7 +890,7 @@ describe('Transaction', () => {
|
|
|
890
890
|
|
|
891
891
|
// Build BEEF data
|
|
892
892
|
const beefWriter = new Writer()
|
|
893
|
-
beefWriter.writeUInt32LE(
|
|
893
|
+
beefWriter.writeUInt32LE(BEEF_V1) // BEEF version
|
|
894
894
|
// BUMPs
|
|
895
895
|
beefWriter.writeVarIntNum(2)
|
|
896
896
|
beefWriter.write(sourceTx1.merklePath.toBinary())
|
|
@@ -46,8 +46,8 @@ describe('WhatsOnChainBroadcaster', () => {
|
|
|
46
46
|
it('should broadcast successfully using Node.js https', async () => {
|
|
47
47
|
// Mocking Node.js https module
|
|
48
48
|
mockedHttps(successResponse)
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
global.window = {} as any
|
|
50
|
+
|
|
51
51
|
const broadcaster = new WhatsOnChainBroadcaster(network)
|
|
52
52
|
const response = await broadcaster.broadcast(transaction)
|
|
53
53
|
|