1

I'm trying to "extract" sender's addresses from OP_RETURN Bitcoin transactions, but my code is not working properly.

getWalletAddressOfSender(final Transaction tx)

public static Address getWalletAddressOfSender(final Transaction tx) {

    Address fromAddress = null;

    for (final TransactionInput ti : tx.getInputs()) {

        try {

            Script scriptSig = ti.getScriptSig();
            List<ScriptChunk> chunks = scriptSig.getChunks();

            byte[] pubKey = scriptSig.getPubKey();
            fromAddress = new Address(MainNetParams.get(), Utils.sha256hash160(pubKey));// scriptSig.getFromAddress(MainNetParams.get());

            return fromAddress;

        } catch (final ScriptException x) {
            System.out.println(x.getMessage());
        }
    }

    return null;
}

When the TransactionInput is from a address starting with 3... (P2SH) the list 'scriptSig.getChunks()' will have 5 items and 'getPubKey()' will throw the exception 'Script not of right size, expecting 2 but got 5'.

scriptSig.getPubKey()

/**
 * Returns the public key in this script. If a script contains two constants and nothing else, it is assumed to
 * be a scriptSig (input) for a pay-to-address output and the second constant is returned (the first is the
 * signature). If a script contains a constant and an OP_CHECKSIG opcode, the constant is returned as it is
 * assumed to be a direct pay-to-key scriptPubKey (output) and the first constant is the public key.
 *
 * @throws ScriptException if the script is none of the named forms.
 */
public byte[] getPubKey() throws ScriptException {
    if (chunks.size() != 2) {
        throw new ScriptException("Script not of right size, expecting 2 but got " + chunks.size());
    }
    final ScriptChunk chunk0 = chunks.get(0);
    final byte[] chunk0data = chunk0.data;
    final ScriptChunk chunk1 = chunks.get(1);
    final byte[] chunk1data = chunk1.data;
    if (chunk0data != null && chunk0data.length > 2 && chunk1data != null && chunk1data.length > 2) {
        // If we have two large constants assume the input to a pay-to-address output.
        return chunk1data;
    } else if (chunk1.equalsOpCode(OP_CHECKSIG) && chunk0data != null && chunk0data.length > 2) {
        // A large constant followed by an OP_CHECKSIG is the key.
        return chunk0data;
    } else {
        throw new ScriptException("Script did not match expected form: " + this);
    }
}

So i changed the code to

public static Address getWalletAddressOfSender(final Transaction tx) {

    Address fromAddress = null;

    for (final TransactionInput ti : tx.getInputs()) {

        try {
            Script scriptSig = ti.getScriptSig();
            List<ScriptChunk> chunks = scriptSig.getChunks();
            if(chunks.size() > 2) {
                System.out.print("This is a 5 chunks transaction... ");
                byte[] pubKeyHash = scriptSig.getPubKeyHash();
                fromAddress = Address.fromP2SHHash(MainNetParams.get(), pubKeyHash);
            } else {
                byte[] pubKey = scriptSig.getPubKey();
                fromAddress = new Address(MainNetParams.get(), Utils.sha256hash160(pubKey));// scriptSig.getFromAddress(MainNetParams.get());   
            }

            return fromAddress;
        } catch (final ScriptException x) {
            System.out.println(x.getMessage());
        }
    }

    return null;
}

but it will also throw another exception 'Script not in the standard scriptPubKey form' (when chunks.size() > 2)

scriptSig.getPubKeyHash()

/**
 * <p>If a program matches the standard template DUP HASH160 &lt;pubkey hash&gt; EQUALVERIFY CHECKSIG
 * then this function retrieves the third element.
 * In this case, this is useful for fetching the destination address of a transaction.</p>
 * 
 * <p>If a program matches the standard template HASH160 &lt;script hash&gt; EQUAL
 * then this function retrieves the second element.
 * In this case, this is useful for fetching the hash of the redeem script of a transaction.</p>
 * 
 * <p>Otherwise it throws a ScriptException.</p>
 *
 */
public byte[] getPubKeyHash() throws ScriptException {
    if (isSentToAddress())
        return chunks.get(2).data;
    else if (isPayToScriptHash())
        return chunks.get(1).data;
    else
        throw new ScriptException("Script not in the standard scriptPubKey form");
}

Here's a transaction where chunks.size() == 2 and i'm able to extract sender's address (1KYiKJEfdJtap9QX2v9BXJMpz2SfU4pgZw)

https://www.blockchain.com/btc/tx/b5765d54e275794939eb48c77dd8862a6e865dee6d71bc7004660dca32de8c43

These are some transactions where chunks.size() == 5 and i'm NOT able to extract the sender's address (3....)

https://www.blockchain.com/btc/tx/b02e17479660a4685daba4e8f0f73aea96e0c36ab14142b68f868ac76a77455a

https://www.blockchain.com/btc/tx/f0a6708167eca88b9fe4dad4c110ddff2b3f6c5e08771793b8ca40400d4effab

https://www.blockchain.com/btc/tx/28a91393393916367e890965200d4f8af04416b65ee6fea22c0adf29af8ea3b8

What's the correct way of getting the sender's address in these cases ?

FYI: here's my shitty method to get the recipient's address

// THIS IF A FUCKED UP WORKAROUND, FIX ASAP
@Nullable
public static Address getWalletAddressOfReceiver(final Transaction tx, final Address senderAddress) {

    for (final TransactionOutput output : tx.getOutputs()) {
        try {
            final Script script = output.getScriptPubKey();     
            Address receiverAddress = script.getToAddress(MainNetParams.get(), true);
            if(receiverAddress.equals(senderAddress))
                continue;
            return receiverAddress;

        } catch (final ScriptException x) {

        }
    }

    return null;
}

As the recipient's address is in a random position in "tx.getOutputs()" i just skip it if it's equal to sender's address and get the next one.

1 Answer 1

2

When an input redeems a P2PKH output (pay to an address starting with a 1), the format of the input script (the scriptSig) is: [signature] [public key]. Because public keys are included in both standard input scripts (redeeming P2PKH outputs) and output scripts (P2PK), the Script.getPubKey() method has been written to return the public key in either input scripts or output scripts, hence it can work for you here.

Public key hashes (addresses), however, are only included in standard output scripts. So the Script.getPubKeyHash() method assumes you're talking about an output script, so it won't work for you here.

An input script redeeming a P2SH (pay to address starting with a 3) output is not necessarily 5 chunks, but the address is the hash of the redeem script, and the redeem script is always the last chunk. (Specifically, the input script is: [script sig for the redeem script (may be multiple chunks)] [redeem script].) So all you'll need to do is get the last chunk (which is also what you're doing for P2PKH) and hash it.

The one place you might run into trouble is if you're redeeming a P2SH output, and the input script is 2 chunks. (This could come up, for instance, if the redeem script is [public key] OP_CHECKSIG - then the sigScript would be in the form [signature] [20-byte hash], indistinguishable from one redeeming a P2PKH output.) The way to check this would be to look at the previous transaction and check its output scripts (which would also give you the addresses you're looking for more easily).

But assuming that when you redeem P2SH outputs the input script is more than 2 chunks, something like this should work:

public static Address getWalletAddressOfSender(final Transaction tx) {

    Address fromAddress = null;

    for (final TransactionInput ti : tx.getInputs()) {

        try {

            Script scriptSig = ti.getScriptSig();
            List<ScriptChunk> chunks = scriptSig.getChunks();

            byte[] pubKey = chunks.get(chunks.size() - 1).data;
            byte[] pubKeyHash = Utils.sha256hash160(pubKey);

            if (chunks.size() > 2) { // assume P2SH
                fromAddress = Address.fromP2SHHash(MainNetParams.get(), pubKeyHash);
            } else if (chunks.size() == 2) { // assume P2PKH
                fromAddress = new Address(MainNetParams.get(), pubKeyHash);
            }
            return fromAddress;

        } catch (final ScriptException x) {
            System.out.println(x.getMessage());
        }
    }

    return null;
}

(Note I used the variable names pubKey and pubKeyHash here, but in the case of P2SH these will instead be a script and a script hash. This method could also cause problems if the output being referenced is a multisig, P2PK, or something non-standard.)

I also notice your method only returns the first sender address. If a transaction has inputs from multiple sources, it might be worth returning a list or a set of those sender addresses. Just a thought.

Hope this helps!

5
  • Awesome! It works for most cases, however i encountered some transactions from P2SH addresses that have only 1 chunk. So for it to work on these cases i changed the code to if (chunks.size() > 2 || chunks.size() == 1) { // assume P2SH. And now it works. Check this one for example f8eb444bdc11b4d85dfe44fb6aa2eb20628a93d23fde84ec60835a25acb617fd
    – Bruno
    Jan 14, 2019 at 4:43
  • Also, using the same transaction example above, how do i detect that 3AJyCVDv5Nb55tpCnMxYxsqc3cAYQXZWEP is an input address (so the code won't consider it the "destination" address) ? (Please consider my "getWalletAddressOfReceiver" code from original question). The getWalletAddressOfSender method returns 3BMEtyRXqVGRujANXNEXyTZ3tY3kRqraQQ as the sender's address, which is the correct one btw. But the "getWalletAddressOfReceiver" will return 3AJyCVDv5Nb55tpCnMxYxsqc3cAYQXZWEP(incorrect) instead of 1BNY3BKk6AhWNq8iYrCuJxiNMtHHSGbg4o (correct).
    – Bruno
    Jan 14, 2019 at 5:02
  • 1
    Really a P2SH input could have any number of inputs. You could check the formatting more rigorously, i.e. if you're spending a P2PKH output it should have two chunks: The first should be a signature, usually of size 71-73 (althought it technically could be smaller than 71 bytes), and the second chunk will be a public key: either 33 or 65 bytes. But ultimately, you can only tell for sure from outputs, not from inputs. I'll add a bit to my answer about the receiver address.
    – Vecna
    Jan 15, 2019 at 5:08
  • 1
    I think your problem with getting the receiver address is that you're only looking at one address at a time. This transaction has 2 inputs and 2 outputs that pay to addresses, so it's not enough to just return the first address from each place. So if you walk through getting the sender address and it's the first one, 3BMEtyRXqVGRujANXNEXyTZ3tY3kRqraQQ and then compare it to the first output address, 3AJyCVDv5Nb55tpCnMxYxsqc3cAYQXZWEP, then the code will say, "These are different. Return this receiver address." I would suggest using sets for the addresses, instead of just assuming one.
    – Vecna
    Jan 15, 2019 at 5:26
  • 1
    You'll also probably run into trouble with that second output because it's an OP_RETURN, so I'd suggest putting in a check before you call script.getToAddress, e.g. if (script.isSentToAddress() || script.isPayToScriptHash()) { Address receiverAddress = script.getToAddress(MainNetParams.get(), true); }
    – Vecna
    Jan 15, 2019 at 5:30

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.