Bit-Wasp / bitcoin-php

Bitcoin implementation in PHP
The Unlicense
1.05k stars 418 forks source link

How to make a transaction with multiple outputs and sign it #790

Open maxspb89 opened 5 years ago

maxspb89 commented 5 years ago

I use this code to create a TX with multiple inputs and multiple outputs. After sending raw transaction to network it hangs unconfirmed for a long time. I think it deals with signing process. I don't understand what TX output should I use when signing with: $input = $signer->input(0, $txOut); Outputs are taken from TX inputs? Any help is welcome.

public function sendBtc($amount, $fee, $address): string
    {
        try {
            $balance = $this->getBalance($this->_address);
            if ($amount + $fee > $balance)
                throw new \Exception("Inssuficient funds to make a transaction ({$balance} BTC available)");

            $txId = "";
            $unspentTxs = $this->getUnspentTxs($this->_address);

            //create a list of inputs for the transaction
            $txIns = [];

            $amountToSpend = $amount;
            $totalAmount = 0.0;

            foreach ($unspentTxs as $utx) {
                $id = $utx["txid"];
                $txValue = (float)$utx["value"];
                $totalAmount += $txValue;
                $prevOutput = $utx["output_no"];
                $txIn = new TransactionInput(new OutPoint(Buffer::hex($id), $prevOutput), new Script());
                array_push($txIns, $txIn);

                if ($txValue >= $amountToSpend + $fee)
                    break;
                $amountToSpend -= $txValue;

            }
            $ret = $totalAmount - $amount - $fee;
            Helpers::logInfo("Amount: {$amount}, to be sent: {$totalAmount}, to be returned: {$ret} , fee: {$fee}");

            //create a list of outputs for the transaction
            $txOuts = [];

            $addressCreator = new AddressCreator();

            array_push($txOuts,
                new TransactionOutput($this->btcToSatoshi($amount), $addressCreator->fromString($address)->getScriptPubKey()));

            //if we have something to return
            if ($ret > 0) {
                array_push($txOuts,
                    new TransactionOutput($this->btcToSatoshi($ret), $addressCreator->fromString($this->_address)->getScriptPubKey()));
            }

            $transaction = new Transaction(1, $txIns, $txOuts, [], 0);

            Helpers::logInfo("Transaction hex: {$transaction->getHex()}");

            $signer = new Signer($transaction);

            $txOut = new TransactionOutput($this->btcToSatoshi($amount + $fee), ScriptFactory::scriptPubKey()->payToPubKeyHash($this->_privateKey->getPubKeyHash()));

            $input = $signer->input(0, $txOut);
            $input->sign($this->_privateKey);
            echo "input valid? " . ($input->verify() ? "true" : "false") . PHP_EOL;

            $signed = $signer->get();

            echo "txid: {$signed->getTxId()->getHex()}\n";
            echo "raw: {$signed->getHex()}\n";

            $this->sendTransaction($signed->getHex());

            return $txId;
        } catch (\Exception $e) {
            throw new \Exception("Unable to send {$amount} bitcoins to {$address}: {$e->getMessage()}");
        }

    }
afk11 commented 5 years ago

This is a really common point of confusion - I hope to reword that part soon.

Think of it this way. (Except coinbase transactions -) all inputs are outputs which were created in a previous transaction, right?

How can you have an input (txid, vout) without knowing which transaction created it? Which output in that funding transaction created the input you wanna spend?

Most importantly - how can you sign an input, if you don't have the output script which says it's P2PKH, multisig, etc? You need to know this info from the previous transaction.

afk11 commented 5 years ago

Regarding why you transaction is stuck.. It seems you're calling sendBtc() with a specific fee, BUT, that is able to select it's own inputs. You're paying too little fee. Usually the best way to work around this is to not pass $fee, but pass $feeRate.. every byte costs something. you dunno how many inputs(=bytes) you'll need when calling the function, so you can't accurately estimate the fee.

while you loop and sum up inputs for your transaction, you should multiply the inputs eventual size by your fee rate, and dynamically calculate the fee. That way, no matter how many inputs you spend, your fee rate is always correct.

(btw, if your transaction broadcasts, I suspect you've managed to solve the txout question on multiple outputs :) well done)

maxspb89 commented 5 years ago

This is a really common point of confusion - I hope to reword that part soon.

Think of it this way. (Except coinbase transactions -) all inputs are outputs which were created in a previous transaction, right?

How can you have an input (txid, vout) without knowing which transaction created it? Which output in that funding transaction created the input you wanna spend?

Most importantly - how can you sign an input, if you don't have the output script which says it's P2PKH, multisig, etc? You need to know this info from the previous transaction.

Thanks for the support. So I need to create an output for each input used in TX (use amount, provided by input "value" and take locking scripts) and sign it? Like this:

for($i=0;$i<count($txIns);$i++){
                $txOut = new TransactionOutput($this->btcToSatoshi($unspentTxs[$i]["value"]), ScriptFactory::fromHex($unspentTxs[$i]["script_hex"]));
                $input = $signer->input($i, $txOut);
                $input->sign($this->_privateKey);
                echo "input valid? " . ($input->verify() ? "true" : "false") . PHP_EOL;
            }
afk11 commented 4 years ago

That looks right!

Sorry for delay, I guess you tried this in the end? Can we close the issue?

athamidn commented 4 years ago

Maybe this helps somebody:

Bitcoin::setNetwork(NetworkFactory::bitcoinTestnet());
$network = Bitcoin::getNetwork();
$ecAdapter = Bitcoin::getEcAdapter();

$addrCreator = new AddressCreator();
$privFactory = new PrivateKeyFactory($ecAdapter);

$txIns = [];

//TxId1 = 358f321ca44a6c584c04f110decb315e7b4adfbc907f220b6a9661a7933ee8eb
//We want to spend index 0 from this transaction.It belongs to us and we have the private key for that.
$txIn = new TransactionInput(new OutPoint(Buffer::hex("358f321ca44a6c584c04f110decb315e7b4adfbc907f220b6a9661a7933ee8eb"), 0), new Script());
array_push($txIns, $txIn);

//txid2 = ccd2545b6cc5f44ed3d95612e4f249826caa0b3ef3609eed6c6815261c572e00
//We want to spend the zero index of this transaction, we have the private key for that too.

$txIn = new TransactionInput(new OutPoint(Buffer::hex("ccd2545b6cc5f44ed3d95612e4f249826caa0b3ef3609eed6c6815261c572e00"), 0), new Script());
array_push($txIns, $txIn);

//Now we create outputs
//we want to send these funds to one address: n2WTLoNczpTQnaUobCtt8cjZRinYdS33J5

$txOuts = [];
$addressCreator = new AddressCreator();

$txOuts[] = new TransactionOutput(80000, $addressCreator->fromString("n2WTLoNczpTQnaUobCtt8cjZRinYdS33J5")->getScriptPubKey());
$txOuts[] = new TransactionOutput(80000, $addressCreator->fromString("n2WTLoNczpTQnaUobCtt8cjZRinYdS33J5")->getScriptPubKey());

//OK, we can create a transaction with Inputs and Outputs in version 1 with 0 locktime
$transaction = new Transaction(1, $txIns, $txOuts, [], 0);

//And now, sign inputs and connect them to outputs

$signer = new Signer($transaction);

//private key for the first input that we want to spend it

$privateKey =$privFactory->fromWif("cT9G5WNfYQPNPVpBrfiob6RNse6QSYbccRTnrkyDQatYBJK9yVhC");

$txOut = new TransactionOutput(80000, ScriptFactory::scriptPubKey()->payToPubKeyHash($privateKey->getPubKeyHash()));

//connect first input with this output
$input = $signer->input(0, $txOut);
//sign it with private key
$input->sign($privateKey);

//now other input 
$privateKey1 =$privFactory->fromWif("cPojMp5n1Jn8jJxMBJgGkghcvauho5cau4cv95rU6aazbJvVAWD4");

$txOut1 = new TransactionOutput(80000, ScriptFactory::scriptPubKey()->payToPubKeyHash($privateKey1->getPubKeyHash()));

$input = $signer->input(1, $txOut1);

$input->sign($privateKey1);

$signed = $signer->get();

echo "txid: {$signed->getTxId()->getHex()}";

echo "raw: {$signed->getHex()}";

you can calculate fee dynamically or better loop with more inputs and outputs.

afk11 commented 4 years ago

@athamidn Thanks for your comment! I'm gonna edit it to use githubs PHP code formatting to aid in copy/paste :)

athamidn commented 4 years ago

It's cool :) I developed it in more dynamically, may be you want to improve that and create an example. You can choice untx by yourself or let the system select the best one in your untxs list.

It can be very better than this, it was only for test and I'll developed it on Lumen with better coding, I hope this one be useful to anybody.

And I have a question, If I want to have a multi coin support, can I use from your repository to create transaction and sign them? or each network and coin has different roles and here only we can switch between bitcoin main net and test net? what about ETH? Can I create Trx and sign?

require_once __DIR__."/../vendor/autoload.php";
use BitWasp\Bitcoin\Address\AddressCreator;
use BitWasp\Bitcoin\Bitcoin;
use BitWasp\Bitcoin\Key\Factory\PrivateKeyFactory;
use BitWasp\Bitcoin\Network\NetworkFactory;
use BitWasp\Bitcoin\Script\ScriptFactory;
use BitWasp\Bitcoin\Transaction\Factory\Signer;
use BitWasp\Bitcoin\Transaction\TransactionFactory;
use BitWasp\Bitcoin\Transaction\TransactionOutput;

ini_set('display_errors', 1);

Bitcoin::setNetwork(NetworkFactory::bitcoinTestnet());
$network = Bitcoin::getNetwork();
$ecAdapter = Bitcoin::getEcAdapter();

$addrCreator = new AddressCreator();
$privFactory = new PrivateKeyFactory($ecAdapter);

$builder = TransactionFactory::build()->version(1)->locktime(0);

//if user wants to spend from the selected untx list
//structure: all untxs are here, the user selected has address item
/*
(
    [d99cbba653c161e14cc9de1cde46103dd27ca28606eeb22f618480b336e783f2] => Array
        (
            [index] => 0
            [hash] => d99cbba653c161e14cc9de1cde46103dd27ca28606eeb22f618480b336e783f2
            [value] => 128580
        )
 [2d14479254fcd4291ee3ac655dffa70d943acc3c07466ec7a11e67587d33b359] => Array
        ( /* this one is selected by user
            [address] => n2WTLoNczpTQnaUobCtt8cjZRinYdS33J5
            [index] => 0
            [hash] => 2d14479254fcd4291ee3ac655dffa70d943acc3c07466ec7a11e67587d33b359
            [value] => 88480
        )) */

$selectedUNTX = $_POST['selectedUNTX'];

//or if user doesn't have any selected list and the system should select automatically from total exist untx list
//structure:
/*
//all exist untx
 [0] => stdClass Object
        (
            [address] => n2WTLoNczpTQnaUobCtt8cjZRinYdS33J5
            [index] => 0
            [hash] => 96136f0faa1e1705f479ded02450837b7179c73f79ef5fe2eb5179d2b761d127
            [value] => 78480
        )

    [1] => stdClass Object
        (
            [address] => mogry6GNxy9c9zX889fh1H9v5tEczSwVH9
            [index] => 0
            [hash] => dd97fd2879bd73e17d8ccdda26aaf2ac5a4f84884cb05655ac3c9440d433bbf8
            [value] => 50000
        )
 */

$allUNTXList = json_decode($_POST['allUNTXList']);

//the address that we want to send fund to it.
// if you want to be an array, you need to change it to an array and create a foreach on it
//here we want to send fund to an address and return extra fund to our defined address

$sendTo = $_POST['sendTo'];

//convert list from json
//because of my wallet is developed in HD wallet, so I don't save my address any place
//if you have a db, you can select all address from db or other way
/*
 *structure:
 stdClass Object
( address => privkey
    [mogry6GNxy9c9zX889fh1H9v5tEczSwVH9] => cQYyQCWzhbLgucb1QCfpMHsv4ccd1nvCq8fQfFGbqHgkuBxi4M5b
    [n3gpjYsGhMVcYN2zwxwpWXvjpHA12FQTFR] => cT9G5WNfYQPNPVpBrfiob6RNse6QSYbccRTnrkyDQatYBJK9yVhC
    [mtzVQSdZhGpR5b63hCJtXV92Nq9XcsXNhv] => cQ8wT9ggHBEMaZWNgqACuptzyfNh2iW3N59SqYqXRkVw7javRCAE
)
*/
$listOfGeneratedAddressJson = json_decode($_POST['listOfGeneratedAddress']);

//you can find this rate from google
//current normal fee rate for btc

$feeRate = 60;

//this is the value that we want to send it to an address
//ex: 1000

$totalWantsToSpend =intval($sendTo['value']);

if (!isset($selectedUNTX) && (!isset($allUNTXList) || $allUNTXList == [])) {
    $message = "Oops! there is not any untx";
    $success = false;
    $arr = [
        "success" => $success,
        "message" => $message
    ];
    //this one is because of my ajax calling, you can change it
    echo json_encode($arr);
    return 1;
}

//get all selected untx

$selectedUNTX = array_filter($selectedUNTX, function ($var) {
    if (isset($var['address'])){
        return $var;
    }
});

//if user selected a list from all the untx, it can be one or more

if (!empty($selectedUNTX)) {

    //get count of selected untx to calculate final rate
    $inputCount = count($selectedUNTX);
    //calculate all amount of selected untx
    $totalUnspentAmount = array_sum(array_column($selectedUNTX, 'value'));

    if ($totalUnspentAmount < $totalWantsToSpend) {
        $message = "Oops! you don't have enough money to spend :(";
        $success = false;
        $arr = [
            "success" => $success,
            "message" => $message
        ];
        //this one is because of my ajax calling, you can change it
        echo json_encode($arr);
        return 1;
    }
    $finalUNTX = $selectedUNTX;
} else {

    //The user wants to select the appropriate untx/untxs by machine automatically
    if (!isset($allUNTXList) || $allUNTXList == []) {
        $message = "Oops! there is not any untx in your wallet :(";
        $success = false;
        $arr = [
            "success" => $success,
            "message" => $message
        ];
        echo json_encode($arr);

        return 1;
    }

    //it means we have untx
    //but is that enough to spend?

    $totalUnspentAmount = array_sum(array_column($allUNTXList, 'value'));

    if ($totalUnspentAmount < $totalWantsToSpend) {
        $message = "Oops! you don't have enough money to spend :(";

        $success = false;
        $arr = [
            "success" => $success,
            "message" => $message
        ];
        echo json_encode($arr);

        return 1;
    }

    //it means we have enough money to spend and send to the selected address
    //but witch one is appropriate
    //we have to use less input for saving fee amount
    //find first >= amount that we can use it

    $allUNTXList = (array) $allUNTXList;
    usort($allUNTXList, function ($item1, $item2) {
        $item1 = (array)$item1;
        $item2 = (array)$item2;
        return $item1['value'] <=> $item2['value'];
    });

    $automaticallyList = [];
    foreach ($allUNTXList as $eachUntxItem) {

        $eachUntxItem = (array)$eachUntxItem;
        if ($eachUntxItem['value'] >= $totalWantsToSpend) {
            $automaticallyList[] = $eachUntxItem;
            $inputCount = 1;
            $totalUnspentAmount = $eachUntxItem['value'];
            break;
        }
    }

    //it means we have many untx that together are equal or more that $totalWantsToSpend
    // find appropriate untx

    if ($automaticallyList == []) {
        usort($allUNTXList, function ($item1, $item2) {
            $item1 = (array)$item1;
            $item2 = (array)$item2;
            return $item2['value'] <=> $item1['value'];
        });
        $sumOfValues = 0;
        $inputCount = 0;
        foreach ($allUNTXList as $eachUntxItem) {
            $eachUntxItem = (array)$eachUntxItem;
            $automaticallyList [] = $eachUntxItem;
            $sumOfValues =intval($eachUntxItem['value'] + $sumOfValues);
            $inputCount++;
            if ($sumOfValues >= $totalWantsToSpend) {
                break;
            }
        }
        $totalUnspentAmount = $sumOfValues;
    }
    $finalUNTX = $automaticallyList;
}

//do we need to send extra to our address? change address

if ($totalUnspentAmount == $totalWantsToSpend) {
    $outputCount = 1;
} elseif ($totalUnspentAmount > $totalWantsToSpend) {
    $outputCount = 2;
}

//calculate fee

$fee =intval(($inputCount * 148 + $outputCount * 34 + 10) * $feeRate);

//all fee is more than amount that we want to spend?

if ($fee >= $totalWantsToSpend) {
    $message = "Oops! the fee is more than your amount!";
    $success = false;
    $arr = [
        "success" => $success,
        "message" => $message
    ];
    echo json_encode($arr);

    return 1;
}

//recipient will pay the fee amount

$userWillReceive = $totalWantsToSpend - $fee;

//we will receive extra amount in change address

$totalExtraAmount = $totalUnspentAmount - $totalWantsToSpend;
$addressForExtraAmount = "mogry6GNxy9c9zX889fh1H9v5tEczSwVH9";

$inputIndex = 0;

//input

foreach ($finalUNTX as &$perSelectedUNTX) {
    $perSelectedUNTX = (array)$perSelectedUNTX;
    $builder->input($perSelectedUNTX["hash"], $perSelectedUNTX["index"]);
    $totalUnspentAmount += $perSelectedUNTX["value"];
    $perSelectedUNTX["input_index"] = $inputIndex;
    $inputIndex++;
}
$addressCreator = new AddressCreator();

//output

if (! empty($sendTo["address"]) && ! empty($sendTo["value"])) {
    $builder->payToAddress($userWillReceive, $addressCreator->fromString($sendTo["address"], $network));
}

if ($outputCount > 1) {
    //it means we have extra money
    $builder->payToAddress($totalExtraAmount, $addressCreator->fromString($addressForExtraAmount, $network));
}

$unsigned = $builder->get();
$signer = new Signer($unsigned, $ecAdapter);

//signing

foreach ($finalUNTX as $eachSelectedUNTX) {
    //find private key from the address list
        $extArr = (array) $listOfGeneratedAddressJson;
    $privateKey = $privFactory->fromWif($extArr[$eachSelectedUNTX["address"]], $network);
        $txOut = new TransactionOutput($eachSelectedUNTX["value"],
            ScriptFactory::scriptPubKey()->payToPubKeyHash($privateKey->getPubKeyHash()));
        $signer->sign($eachSelectedUNTX["input_index"], $privateKey, $txOut);
}

$signed = $signer->get();
$success = true;
$message = "successfully!";

$arr = [
    "success"       => $success,
    "input_count"   => $inputCount,
    "output_count"  => $outputCount,
    "total_fee"     => $fee,
    "fee_rate"      => $feeRate,
    "user_amount"   => $userWillReceive,
    "extra_amount"  => $totalExtraAmount,
    "extra_address" => $addressForExtraAmount,
    "txid"          => $signed->getTxId()->getHex(),
    "raw"           => $signed->getHex(),
    "message"       => $message
];
echo json_encode($arr);
afk11 commented 4 years ago

And I have a question, If I want to have a multi coin support, can I use from your repository to create transaction and sign them? or each network and coin has different roles and here only we can switch between bitcoin main net and test net? what about ETH?

Forks of bitcoin will work, though obviously if they changed something, you might have some work adapting the library to work with the coins new changes. An example of this is how the BCH/bitcoin private/etc all basically just tweaked the sighash algorithm.

But it won't work with ETH, no, because ETH has completely different data structures to bitcoin

roleenboticario1 commented 2 years ago

Hello, can you help me how to send bitcoin from wallet 1 to another wallet? Do you have code for this? Thanks