verifyBchSignedTx function

VerifyResult verifyBchSignedTx(
  1. VerifyBchSignedTxArgs args
)

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());
  }
}