@smartledger/bsv 8.0.1 ā 8.2.0
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/CHANGELOG.md +118 -0
- package/README.md +38 -38
- package/bsv-gdaf.min.js +47 -47
- package/bsv-ltp.min.js +1 -1
- package/bsv-smartcontract.min.js +1 -1
- package/bsv.bundle.js +48 -48
- package/bsv.min.js +48 -48
- package/docs/AUDIT_SCOPE.md +13 -13
- package/docs/MODULE_REFERENCE_COMPLETE.md +27 -27
- package/docs/advanced/UTXO_MANAGER_GUIDE.md +1 -1
- package/docs/getting-started/INSTALLATION.md +23 -23
- package/docs/getting-started/QUICK_START.md +7 -7
- package/docs/migration/FROM_BSV_1_5_6.md +5 -5
- package/lib/gdaf/attestation-signer.js +90 -2
- package/lib/gdaf/attestation-verifier.js +41 -25
- package/lib/gdaf/zk-prover.js +143 -46
- package/lib/script/interpreter.js +20 -1
- package/package.json +3 -2
- package/test/gdaf/canonicalization.js +106 -0
- package/test/gdaf/zk_prover.js +204 -0
- package/test/script/genesis_limits.js +37 -6
- package/tools/minimal_reproduction.js +119 -0
- package/tools/opcode_map.js +342 -0
- package/tools/server.js +97 -0
- package/tools/simple_real_tx.js +136 -0
- package/tools/sv-sighash-harness.js +123 -0
- package/tools/sv-sighash-report.js +71 -0
- package/tools/sv-tx-harness.js +172 -0
- package/tools/sv-tx-report.js +43 -0
- package/tools/sv-vector-harness.js +304 -0
- package/tools/sv-vector-report.js +88 -0
- package/version.js +1 -1
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Simple Real BSV Transaction Test
|
|
5
|
+
*
|
|
6
|
+
* Uses only real UTXOs to create and broadcast a transaction
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const bsv = require('./index.js');
|
|
10
|
+
const https = require('https');
|
|
11
|
+
|
|
12
|
+
console.log('š Simple Real BSV Transaction Test');
|
|
13
|
+
console.log('===================================\n');
|
|
14
|
+
|
|
15
|
+
const privateKey = new bsv.PrivateKey('L1aW4aubDFB7yfras2S1mN3bqg9nwySY8nkoLmJebSLD5BWv3ENZ');
|
|
16
|
+
const address = privateKey.toAddress().toString();
|
|
17
|
+
|
|
18
|
+
console.log(`Address: ${address}`);
|
|
19
|
+
console.log(`Private Key: ${privateKey.toString()}\n`);
|
|
20
|
+
|
|
21
|
+
// Simple API call
|
|
22
|
+
function makeRequest(url) {
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
https.get(url, (res) => {
|
|
25
|
+
let data = '';
|
|
26
|
+
res.on('data', chunk => data += chunk);
|
|
27
|
+
res.on('end', () => {
|
|
28
|
+
try {
|
|
29
|
+
resolve(JSON.parse(data));
|
|
30
|
+
} catch (error) {
|
|
31
|
+
reject(new Error(`Failed to parse response: ${error.message}`));
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}).on('error', reject);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function createSimpleTransaction() {
|
|
39
|
+
try {
|
|
40
|
+
// Get real UTXO
|
|
41
|
+
console.log('š Fetching real UTXO...');
|
|
42
|
+
const utxos = await makeRequest(`https://api.whatsonchain.com/v1/bsv/main/address/${address}/unspent`);
|
|
43
|
+
|
|
44
|
+
if (utxos.length === 0) {
|
|
45
|
+
throw new Error('No UTXOs found');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const utxo = utxos[0]; // Use the first UTXO
|
|
49
|
+
console.log(`Found UTXO: ${utxo.tx_hash}:${utxo.tx_pos} = ${utxo.value} sats`);
|
|
50
|
+
|
|
51
|
+
// Create P2PKH script for the UTXO
|
|
52
|
+
const addr = bsv.Address.fromString(address);
|
|
53
|
+
const p2pkhScript = bsv.Script.buildPublicKeyHashOut(address);
|
|
54
|
+
|
|
55
|
+
// Format UTXO for BSV transaction
|
|
56
|
+
const formattedUTXO = {
|
|
57
|
+
txid: utxo.tx_hash,
|
|
58
|
+
vout: utxo.tx_pos,
|
|
59
|
+
address: address,
|
|
60
|
+
satoshis: utxo.value,
|
|
61
|
+
script: p2pkhScript.toHex()
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
console.log(`UTXO Script: ${p2pkhScript.toHex()}`);
|
|
65
|
+
|
|
66
|
+
// Create transaction - send smaller amount since we now have the result of previous tx
|
|
67
|
+
const sendAmount = Math.min(500, Math.floor(utxo.value * 0.8)); // Send 80% or 500 sats, whichever is smaller
|
|
68
|
+
const feePerKb = 10; // 10 sats per KB
|
|
69
|
+
|
|
70
|
+
// Estimate transaction size (typical P2PKH transaction is ~225 bytes)
|
|
71
|
+
const estimatedSize = 225;
|
|
72
|
+
const fee = Math.ceil((estimatedSize * feePerKb) / 1000);
|
|
73
|
+
const changeAmount = utxo.value - sendAmount - fee;
|
|
74
|
+
|
|
75
|
+
if (changeAmount < 0) {
|
|
76
|
+
throw new Error(`Insufficient funds. Need ${sendAmount + fee}, have ${utxo.value}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
console.log(`\nšø Creating transaction:`);
|
|
80
|
+
console.log(`Send: ${sendAmount} sats`);
|
|
81
|
+
console.log(`Fee rate: ${feePerKb} sats/KB`);
|
|
82
|
+
console.log(`Estimated fee: ${fee} sats`);
|
|
83
|
+
console.log(`Change: ${changeAmount} sats`);
|
|
84
|
+
|
|
85
|
+
const transaction = new bsv.Transaction()
|
|
86
|
+
.from(formattedUTXO)
|
|
87
|
+
.to(address, sendAmount) // Send to ourselves
|
|
88
|
+
.change(address) // Change back to ourselves
|
|
89
|
+
.feePerKb(feePerKb) // Set fee rate
|
|
90
|
+
.sign(privateKey);
|
|
91
|
+
|
|
92
|
+
console.log(`\nā
Transaction created successfully!`);
|
|
93
|
+
console.log(`Transaction ID: ${transaction.id}`);
|
|
94
|
+
console.log(`Transaction size: ${transaction.toBuffer().length} bytes`);
|
|
95
|
+
|
|
96
|
+
// Validation
|
|
97
|
+
console.log(`\nš Validation:`);
|
|
98
|
+
const isValid = transaction.verify();
|
|
99
|
+
console.log(`Basic BSV validation: ${isValid ? 'ā
VALID' : 'ā INVALID'}`);
|
|
100
|
+
|
|
101
|
+
if (isValid) {
|
|
102
|
+
console.log(`Raw transaction: ${transaction.toString()}`);
|
|
103
|
+
|
|
104
|
+
// Test broadcast
|
|
105
|
+
if (process.argv.includes('--broadcast')) {
|
|
106
|
+
console.log('\nš” Broadcasting transaction...');
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const response = await fetch('https://api.whatsonchain.com/v1/bsv/main/tx/raw', {
|
|
110
|
+
method: 'POST',
|
|
111
|
+
headers: { 'Content-Type': 'application/json' },
|
|
112
|
+
body: JSON.stringify({ txhex: transaction.toString() })
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
if (response.ok) {
|
|
116
|
+
const result = await response.text();
|
|
117
|
+
console.log(`ā
Transaction broadcast successful!`);
|
|
118
|
+
console.log(`Transaction ID: ${result}`);
|
|
119
|
+
} else {
|
|
120
|
+
const error = await response.text();
|
|
121
|
+
console.log(`ā Broadcast failed: ${response.status} "${error}"`);
|
|
122
|
+
}
|
|
123
|
+
} catch (error) {
|
|
124
|
+
console.log(`ā Broadcast failed: ${error.message}`);
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
console.log('\nš” To broadcast: node simple_real_tx.js --broadcast');
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
} catch (error) {
|
|
132
|
+
console.log(`ā Error: ${error.message}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
createSimpleTransaction();
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared harness for the SV Node transaction digest vectors in
|
|
5
|
+
* test/data/bitcoin-sv/sighash.json.
|
|
6
|
+
*
|
|
7
|
+
* Used by tools/sv-sighash-report.js (which reports) and
|
|
8
|
+
* test/consensus/sv-sighash-vectors.js (which ratchets), so the progress
|
|
9
|
+
* report and the regression gate cannot disagree about what passes.
|
|
10
|
+
*
|
|
11
|
+
* Row layout, from sighash_tests.cpp in the node:
|
|
12
|
+
*
|
|
13
|
+
* [ raw_transaction, script, input_index, hashType,
|
|
14
|
+
* signature_hash (regular), signature_hash (no forkid) ]
|
|
15
|
+
*
|
|
16
|
+
* Two columns pin the routing in SignatureHash() rather than one branch of it.
|
|
17
|
+
* The node takes BIP143 only when forkid is enabled AND requested AND the
|
|
18
|
+
* signature has not asked for the original algorithm:
|
|
19
|
+
*
|
|
20
|
+
* if(enabledSighashForkid && sigHashType.hasForkId() && !sigHashType.hasChronicle())
|
|
21
|
+
* return SignatureHashBIP143(...);
|
|
22
|
+
* return SignatureHashOriginal(...);
|
|
23
|
+
*
|
|
24
|
+
* Note where the Chronicle bit is NOT consulted: the node does not ask whether
|
|
25
|
+
* Chronicle is enabled before honouring it. That gating lives in
|
|
26
|
+
* CheckSignatureEncoding, which rejects a signature carrying the bit outside
|
|
27
|
+
* Chronicle as SCRIPT_ERR_ILLEGAL_CHRONICLE ā so such a signature never
|
|
28
|
+
* reaches a digest at all. For a row whose hash type sets 0x20 both columns
|
|
29
|
+
* are therefore identical, the original algorithm being taken either way, and
|
|
30
|
+
* that is what makes carrying both columns worth the trouble.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
const bsv = require('..')
|
|
34
|
+
const Script = bsv.Script
|
|
35
|
+
const BN = bsv.crypto.BN
|
|
36
|
+
const Transaction = bsv.Transaction
|
|
37
|
+
const Signature = bsv.crypto.Signature
|
|
38
|
+
const Interpreter = bsv.Script.Interpreter
|
|
39
|
+
const sighash = Transaction.Sighash
|
|
40
|
+
|
|
41
|
+
const rawVectors = require('../test/data/bitcoin-sv/sighash.json')
|
|
42
|
+
|
|
43
|
+
const FORKID = Interpreter.SCRIPT_ENABLE_SIGHASH_FORKID
|
|
44
|
+
const zeroBN = BN.Zero
|
|
45
|
+
|
|
46
|
+
/** The first row of the corpus names the columns rather than carrying data. */
|
|
47
|
+
function rows () {
|
|
48
|
+
return rawVectors.slice(1).filter(v => Array.isArray(v) && v.length >= 6)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Which digest the node produces for this hash type, under each flag setting.
|
|
53
|
+
* Named rather than inferred, so a routing regression reports as one.
|
|
54
|
+
*/
|
|
55
|
+
function algorithm (nhashtype, forkidEnabled) {
|
|
56
|
+
const t = nhashtype >>> 0
|
|
57
|
+
const bip143 = forkidEnabled &&
|
|
58
|
+
(t & Signature.SIGHASH_FORKID) !== 0 &&
|
|
59
|
+
(t & Signature.SIGHASH_CHRONICLE) === 0
|
|
60
|
+
return bip143 ? 'BIP143' : 'OTDA'
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Run every vector. Each result is
|
|
65
|
+
*
|
|
66
|
+
* { index, hashType, chronicleBit, algorithm, passed, reason }
|
|
67
|
+
*
|
|
68
|
+
* where `algorithm` is the one the node's routing selects with forkid enabled.
|
|
69
|
+
*/
|
|
70
|
+
function runAll () {
|
|
71
|
+
return rows().map(function (vector, i) {
|
|
72
|
+
const nhashtype = vector[3]
|
|
73
|
+
const chronicleBit = ((nhashtype >>> 0) & Signature.SIGHASH_CHRONICLE) !== 0
|
|
74
|
+
|
|
75
|
+
const base = {
|
|
76
|
+
index: i + 1,
|
|
77
|
+
hashType: nhashtype,
|
|
78
|
+
chronicleBit,
|
|
79
|
+
algorithm: algorithm(nhashtype, true)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// A fresh Script per call: the original algorithm removes code separators
|
|
83
|
+
// in place, so sharing one would let the first call change what the second
|
|
84
|
+
// is given.
|
|
85
|
+
function digest (flags) {
|
|
86
|
+
const tx = new Transaction(Buffer.from(vector[0], 'hex'))
|
|
87
|
+
return sighash.sighash(tx, nhashtype, vector[2],
|
|
88
|
+
Script(Buffer.from(vector[1], 'hex')), zeroBN, flags).toString('hex')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let regular, noForkId
|
|
92
|
+
try {
|
|
93
|
+
regular = digest(FORKID)
|
|
94
|
+
noForkId = digest(0)
|
|
95
|
+
} catch (e) {
|
|
96
|
+
return Object.assign(base, {
|
|
97
|
+
passed: false,
|
|
98
|
+
reason: 'threw: ' + String(e.message).slice(0, 70)
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (regular !== vector[4]) {
|
|
103
|
+
return Object.assign(base, {
|
|
104
|
+
passed: false,
|
|
105
|
+
reason: 'regular column: ' + regular + ' != ' + vector[4]
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
if (noForkId !== vector[5]) {
|
|
109
|
+
return Object.assign(base, {
|
|
110
|
+
passed: false,
|
|
111
|
+
reason: 'no-forkid column: ' + noForkId + ' != ' + vector[5]
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
return Object.assign(base, { passed: true, reason: null })
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Rows whose two columns must agree, because the Chronicle bit is set. */
|
|
119
|
+
function chronicleRows () {
|
|
120
|
+
return rows().filter(v => ((v[3] >>> 0) & Signature.SIGHASH_CHRONICLE) !== 0)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports = { runAll, rows, chronicleRows, algorithm }
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* How far this library's transaction digest is from the reference node.
|
|
5
|
+
*
|
|
6
|
+
* npm run vectors:sv-sighash
|
|
7
|
+
* npm run vectors:sv-sighash -- --verbose
|
|
8
|
+
*
|
|
9
|
+
* There is no accept/reject direction to report here as there is for the
|
|
10
|
+
* script and transaction vectors: a digest either matches or it does not, and
|
|
11
|
+
* a wrong one is equally bad either way ā it makes valid signatures unverifiable
|
|
12
|
+
* and, worse, signs something other than what the caller was shown.
|
|
13
|
+
*
|
|
14
|
+
* The breakdown by algorithm is the useful reading, because the failure this
|
|
15
|
+
* corpus is best at catching is a routing mistake rather than a hashing one.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const harness = require('./sv-sighash-harness')
|
|
19
|
+
|
|
20
|
+
const verbose = process.argv.indexOf('--verbose') !== -1
|
|
21
|
+
const results = harness.runAll()
|
|
22
|
+
|
|
23
|
+
const failing = results.filter(r => !r.passed)
|
|
24
|
+
const passing = results.length - failing.length
|
|
25
|
+
const chronicle = results.filter(r => r.chronicleBit)
|
|
26
|
+
const bip143 = results.filter(r => r.algorithm === 'BIP143')
|
|
27
|
+
const otda = results.filter(r => r.algorithm === 'OTDA')
|
|
28
|
+
|
|
29
|
+
function line (label, subset) {
|
|
30
|
+
const bad = subset.filter(r => !r.passed).length
|
|
31
|
+
console.log(' ' + label.padEnd(34),
|
|
32
|
+
String(subset.length - bad).padStart(4) + '/' + String(subset.length).padEnd(5),
|
|
33
|
+
bad ? ' <- ' + bad + ' failing' : '')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
console.log()
|
|
37
|
+
console.log('=== SV Node v1.2.0 transaction digest vectors ===')
|
|
38
|
+
console.log()
|
|
39
|
+
console.log('vectors run :', results.length, '(each checked on both columns)')
|
|
40
|
+
console.log('passing :', passing,
|
|
41
|
+
' (' + (100 * passing / results.length).toFixed(1) + '%)')
|
|
42
|
+
console.log('failing :', failing.length)
|
|
43
|
+
console.log()
|
|
44
|
+
console.log('--- by routing ---')
|
|
45
|
+
line('BIP-143 digest', bip143)
|
|
46
|
+
line('original digest (OTDA)', otda)
|
|
47
|
+
line(' of which set SIGHASH_CHRONICLE', chronicle)
|
|
48
|
+
|
|
49
|
+
if (failing.length) {
|
|
50
|
+
const byReason = {}
|
|
51
|
+
failing.forEach(function (r) {
|
|
52
|
+
const key = String(r.reason).replace(/:.*/, '')
|
|
53
|
+
byReason[key] = (byReason[key] || 0) + 1
|
|
54
|
+
})
|
|
55
|
+
console.log()
|
|
56
|
+
console.log('--- failure reasons ---')
|
|
57
|
+
Object.keys(byReason).sort((a, b) => byReason[b] - byReason[a])
|
|
58
|
+
.forEach(k => console.log(String(byReason[k]).padStart(5), k))
|
|
59
|
+
|
|
60
|
+
console.log()
|
|
61
|
+
console.log('--- failing rows ---')
|
|
62
|
+
failing.slice(0, verbose ? failing.length : 10).forEach(function (r) {
|
|
63
|
+
console.log(' #' + String(r.index).padStart(4) +
|
|
64
|
+
' hashType 0x' + (r.hashType >>> 0).toString(16).padStart(2, '0') +
|
|
65
|
+
' ' + r.algorithm.padEnd(7) + r.reason)
|
|
66
|
+
})
|
|
67
|
+
if (!verbose && failing.length > 10) {
|
|
68
|
+
console.log(' ... and ' + (failing.length - 10) + ' more; pass --verbose')
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
console.log()
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Harness for the SV Node transaction vectors in test/data/bitcoin-sv.
|
|
5
|
+
*
|
|
6
|
+
* These reach a level the script vectors do not. A script vector evaluates one
|
|
7
|
+
* unlocking script against one locking script; these deserialise a whole
|
|
8
|
+
* transaction, resolve each input against the outputs it claims to spend, and
|
|
9
|
+
* require every one of them to verify ā across multiple inputs, with locktimes
|
|
10
|
+
* and sequence numbers in play, and with the transaction's own structural
|
|
11
|
+
* checks applied.
|
|
12
|
+
*
|
|
13
|
+
* Row layout, from the node's src/test/transaction_tests.cpp:
|
|
14
|
+
*
|
|
15
|
+
* [ [[prevout hash, prevout n, scriptPubKey, amount?], ...],
|
|
16
|
+
* serialized transaction,
|
|
17
|
+
* flags ]
|
|
18
|
+
*
|
|
19
|
+
* A row in tx_valid.json must verify; one in tx_invalid.json must not. A
|
|
20
|
+
* prevout index of -1 is the node casting to an unsigned int, so it means
|
|
21
|
+
* 0xffffffff. The amount is present only where a row needs one.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const bsv = require('..')
|
|
25
|
+
const Transaction = bsv.Transaction
|
|
26
|
+
const Interpreter = bsv.Script.Interpreter
|
|
27
|
+
const BN = bsv.crypto.BN
|
|
28
|
+
|
|
29
|
+
const harness = require('./sv-vector-harness')
|
|
30
|
+
|
|
31
|
+
const validVectors = require('../test/data/bitcoin-sv/tx_valid.json')
|
|
32
|
+
const invalidVectors = require('../test/data/bitcoin-sv/tx_invalid.json')
|
|
33
|
+
|
|
34
|
+
/** A stable id for a row, so a list of known failures survives reordering. */
|
|
35
|
+
function vectorId (raw) {
|
|
36
|
+
return harness.vectorId(raw)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Resolve each input against the output it spends and verify it.
|
|
41
|
+
*
|
|
42
|
+
* Returns { ok, reason }. `ok` is whether the transaction verifies overall,
|
|
43
|
+
* which requires the structural checks to pass and every non-null input to
|
|
44
|
+
* verify.
|
|
45
|
+
*/
|
|
46
|
+
function evaluate (row) {
|
|
47
|
+
const prevouts = {}
|
|
48
|
+
row[0].forEach(function (p) {
|
|
49
|
+
// -1 is the node's cast of an unsigned index.
|
|
50
|
+
const n = p[1] === -1 ? 0xffffffff : p[1]
|
|
51
|
+
prevouts[p[0] + ':' + n] = {
|
|
52
|
+
script: harness.fromBitcoindString(p[2]),
|
|
53
|
+
satoshis: p.length > 3 ? p[3] : 0
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
const tx = new Transaction(row[1])
|
|
58
|
+
const cls = harness.classifyFlags(row[2])
|
|
59
|
+
|
|
60
|
+
// The node runs these vectors at ProtocolEra::PreGenesis explicitly ā
|
|
61
|
+
// transaction_tests.cpp says so ā which is why one tx_valid row legitimately
|
|
62
|
+
// carries a P2SH output, an output Genesis stopped anyone creating. This
|
|
63
|
+
// library's verify() has no era and applies the pre-Genesis rules, which is
|
|
64
|
+
// what these vectors want.
|
|
65
|
+
let structural
|
|
66
|
+
try {
|
|
67
|
+
structural = tx.verify()
|
|
68
|
+
} catch (e) {
|
|
69
|
+
return { ok: false, reason: 'tx.verify threw: ' + String(e.message).slice(0, 60) }
|
|
70
|
+
}
|
|
71
|
+
if (structural !== true) {
|
|
72
|
+
return { ok: false, reason: 'tx.verify: ' + structural }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
for (let i = 0; i < tx.inputs.length; i++) {
|
|
76
|
+
const input = tx.inputs[i]
|
|
77
|
+
if (input.isNull()) {
|
|
78
|
+
continue
|
|
79
|
+
}
|
|
80
|
+
const key = input.prevTxId.toString('hex') + ':' + input.outputIndex
|
|
81
|
+
const prevout = prevouts[key]
|
|
82
|
+
if (prevout === undefined) {
|
|
83
|
+
return { ok: false, reason: 'no prevout given for input ' + i }
|
|
84
|
+
}
|
|
85
|
+
const interp = new Interpreter()
|
|
86
|
+
const verified = interp.verify(
|
|
87
|
+
input.script, prevout.script, tx, i, cls.flags, new BN(prevout.satoshis)
|
|
88
|
+
)
|
|
89
|
+
if (!verified) {
|
|
90
|
+
return { ok: false, reason: 'input ' + i + ': ' + interp.errstr }
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return { ok: true, reason: null }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Run both files. Each result is
|
|
99
|
+
*
|
|
100
|
+
* { id, expected, ok, passed, reason, direction, comment }
|
|
101
|
+
*
|
|
102
|
+
* `direction` is 'accept' where we accepted a transaction the node rejects ā
|
|
103
|
+
* the dangerous way round ā and 'reject' for the converse.
|
|
104
|
+
*/
|
|
105
|
+
function runAll () {
|
|
106
|
+
const results = []
|
|
107
|
+
|
|
108
|
+
const run = function (vectors, expected) {
|
|
109
|
+
let comment = ''
|
|
110
|
+
vectors.forEach(function (raw) {
|
|
111
|
+
if (!Array.isArray(raw)) return
|
|
112
|
+
if (raw.length === 1) {
|
|
113
|
+
// A single-element row is a comment describing the rows below it.
|
|
114
|
+
comment = String(raw[0])
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let got = null
|
|
119
|
+
let thrown = null
|
|
120
|
+
try {
|
|
121
|
+
got = evaluate(raw)
|
|
122
|
+
} catch (e) {
|
|
123
|
+
thrown = e
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let reason = null
|
|
127
|
+
let direction = null
|
|
128
|
+
const ok = thrown === null && got.ok
|
|
129
|
+
|
|
130
|
+
if (thrown !== null) {
|
|
131
|
+
reason = 'threw: ' + String(thrown.message).slice(0, 70)
|
|
132
|
+
direction = expected ? 'reject' : null
|
|
133
|
+
if (!expected) {
|
|
134
|
+
// Throwing on a transaction the node also rejects is the right
|
|
135
|
+
// outcome reached untidily, not a consensus failure.
|
|
136
|
+
reason = null
|
|
137
|
+
}
|
|
138
|
+
} else if (ok !== expected) {
|
|
139
|
+
reason = expected
|
|
140
|
+
? 'expected valid, rejected with ' + got.reason
|
|
141
|
+
: 'expected invalid, but ACCEPTED'
|
|
142
|
+
direction = expected ? 'reject' : 'accept'
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
results.push({
|
|
146
|
+
id: vectorId(raw),
|
|
147
|
+
expected,
|
|
148
|
+
ok,
|
|
149
|
+
passed: reason === null,
|
|
150
|
+
reason,
|
|
151
|
+
direction,
|
|
152
|
+
comment
|
|
153
|
+
})
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
run(validVectors, true)
|
|
158
|
+
run(invalidVectors, false)
|
|
159
|
+
return results
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** A one-line description of a row, for failure output. */
|
|
163
|
+
function describe (result) {
|
|
164
|
+
return (result.expected ? 'tx_valid' : 'tx_invalid') +
|
|
165
|
+
(result.comment ? ' // ' + result.comment.slice(0, 70) : '')
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
module.exports = {
|
|
169
|
+
runAll,
|
|
170
|
+
evaluate,
|
|
171
|
+
describe
|
|
172
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* How far this interpreter is from the node at the transaction level.
|
|
5
|
+
*
|
|
6
|
+
* npm run vectors:sv-tx
|
|
7
|
+
*
|
|
8
|
+
* A script vector evaluates one unlocking script against one locking script.
|
|
9
|
+
* These deserialise a whole transaction, resolve each input against the output
|
|
10
|
+
* it claims to spend, and require every one to verify ā with locktimes,
|
|
11
|
+
* sequence numbers and multiple inputs in play. That reaches rules no single
|
|
12
|
+
* script can.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const harness = require('./sv-tx-harness')
|
|
16
|
+
|
|
17
|
+
const results = harness.runAll()
|
|
18
|
+
const failing = results.filter(r => !r.passed)
|
|
19
|
+
const accepts = failing.filter(r => r.direction === 'accept')
|
|
20
|
+
const rejects = failing.filter(r => r.direction === 'reject')
|
|
21
|
+
|
|
22
|
+
console.log()
|
|
23
|
+
console.log('=== SV Node v1.2.0 transaction vectors ===')
|
|
24
|
+
console.log()
|
|
25
|
+
console.log('vectors run :', results.length,
|
|
26
|
+
'(' + results.filter(r => r.expected).length + ' must verify, ' +
|
|
27
|
+
results.filter(r => !r.expected).length + ' must not)')
|
|
28
|
+
console.log('passing :', results.length - failing.length,
|
|
29
|
+
' (' + (100 * (results.length - failing.length) / results.length).toFixed(1) + '%)')
|
|
30
|
+
console.log()
|
|
31
|
+
console.log(' false accepts (we accept, node rejects) :', accepts.length,
|
|
32
|
+
accepts.length ? ' <- the direction that can cost money' : '')
|
|
33
|
+
console.log(' false rejects (we reject, node accepts) :', rejects.length)
|
|
34
|
+
|
|
35
|
+
if (failing.length) {
|
|
36
|
+
console.log()
|
|
37
|
+
console.log('--- failing ---')
|
|
38
|
+
failing.forEach(function (r) {
|
|
39
|
+
console.log(' ' + r.id + ' ' + r.reason)
|
|
40
|
+
console.log(' ' + harness.describe(r))
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
console.log()
|