verifyBchSignedTx function
Verify a signed BCH transaction against the request that asked for it.
Implementation
VerifyResult verifyBchSignedTx(VerifyBchSignedTxArgs args) {
try {
final tx = decodeBchRawTx(args.rawTx);
// Everything the verifier does not read is unchecked (the sighash is
// recomputed FROM the decoded tx, so it is self-consistent with any
// version/locktime/sequence). Pin the parameters the signer fixes.
if (tx.version != _bchTxVersion || tx.locktime != _bchTxLocktime) {
return failed(
'transaction parameters (version ${tx.version}, locktime ${tx.locktime}) '
"are not the device signer's (1, 0)",
);
}
for (var i = 0; i < tx.inputs.length; i++) {
if (tx.inputs[i].sequence != _bchTxSequence) {
return failed(
"input $i sequence is not the device signer's 0xfffffffd");
}
}
if (tx.inputs.length != args.inputs.length) {
return failed(
'signed transaction has ${tx.inputs.length} inputs, the request had ${args.inputs.length}',
);
}
if (tx.outputs.length != args.outputs.length) {
return failed(
'signed transaction has ${tx.outputs.length} outputs, the request had ${args.outputs.length}',
);
}
for (var i = 0; i < args.outputs.length; i++) {
final requested = args.outputs[i];
final actual = tx.outputs[i];
if (actual.value != _toBigintValue(requested.value, 'output $i value')) {
return failed('output $i value differs from the request');
}
if (!equalBytes(actual.script, _scriptForAddress(requested.address))) {
return failed('output $i does not pay the requested address');
}
}
for (var i = 0; i < args.inputs.length; i++) {
final requested = args.inputs[i];
final actual = tx.inputs[i];
final txidLE =
Uint8List.fromList(hexToBytes(requested.txid).reversed.toList());
if (!equalBytes(actual.txidLE, txidLE) ||
actual.index != requested.index) {
return failed(
'input $i spends a different outpoint than the request named');
}
// scriptSig must be exactly push(sig‖0x41) push(pubkey33).
final script = actual.scriptSig;
if (script.length < 2) return failed('input $i has no signature');
final sigLen = script[0];
if (sigLen < 9 || 1 + sigLen + 1 > script.length) {
return failed('input $i scriptSig is not a signature push');
}
final sigWithType = Uint8List.sublistView(script, 1, 1 + sigLen);
final pubLen = script[1 + sigLen];
if (pubLen != 33 || 1 + sigLen + 1 + pubLen != script.length) {
return failed(
'input $i scriptSig does not end with a compressed public key push',
);
}
final pubkey = Uint8List.sublistView(script, 1 + sigLen + 1);
if (!equalBytes(pubkey, _toPublicKeyBytes(requested.publicKey))) {
return failed(
'input $i was signed with a different public key than the request named — '
'the transaction cannot spend the requested UTXO',
);
}
final hashType = sigWithType[sigWithType.length - 1];
if (hashType != _sighashForkidAll) {
return failed(
'input $i uses sighash 0x${hashType.toRadixString(16)}, expected SIGHASH_ALL|FORKID (0x41)',
);
}
final sighash = computeBchSighash(
tx: tx,
inputIndex: i,
scriptCode: _p2pkhScript(hash160(pubkey)),
value: _toBigintValue(requested.value, 'input $i value'),
);
final signature = _derToCompact(
Uint8List.sublistView(sigWithType, 0, sigWithType.length - 1),
);
if (!Secp256k1.verify(signature, sighash, pubkey)) {
return failed(
'input $i signature does not verify against the BIP-143 FORKID sighash',
);
}
}
final txId = args.txId;
if (txId != null) {
final computed = bytesToHex(Uint8List.fromList(
sha256d(hexToBytes(args.rawTx)).reversed.toList(),
));
if (computed != txId.toLowerCase()) {
return failed(
'reply txId does not match the hash of the signed transaction');
}
}
return verified;
} on EraSdkError catch (e) {
return failed(e.message);
} on Object catch (e) {
return failed(e is FormatException ? e.message : e.toString());
}
}