Author

Topic: My own coin control utility (Read 2158 times)

legendary
Activity: 1974
Merit: 1029
May 03, 2013, 02:00:58 PM
#5
You need a dollar sign at the start of that walletpassphrase line.

Ah, copy-paste woes. Thanks for pointing it out Smiley. I'm going to update the post with the version I'm using now, which has less rounding bugs and allows a --list or -l parameter to just list the unspent outputs and exit.
legendary
Activity: 2940
Merit: 1333
May 03, 2013, 02:58:11 AM
#4
I've decided to publish the program I use to create custom transactions

Quote
#!/usr/bin/perl

rpc->walletpassphrase ($wpp, 1);

You need a dollar sign at the start of that walletpassphrase line.

Other than that it seems to work fine.  Good stuff!  Smiley
legendary
Activity: 1974
Merit: 1029
April 04, 2013, 03:01:05 AM
#3
You might want to add extra safeguards when it comes to input/output selection, to make sure that a user does not accidentally create a transaction with 94 BTC in fees

The usage flow reaches a point where the user is asked for a change address. This step is required. When an address is given, the balance in the inputs is completely assigned to the outputs (ie no fee at this point). Later, the fee is calculated, suggested and then the user confirms/overrides it. Granted, I could still check that the user entered a sane value but I would tag that as paranoid (I'm not implying that it's bad).
legendary
Activity: 1596
Merit: 1100
April 03, 2013, 05:41:53 PM
#2
Good stuff!

You might want to add extra safeguards when it comes to input/output selection, to make sure that a user does not accidentally create a transaction with 94 BTC in fees (this happened recently).  This depends on your user interface of choice, but a manual sanity check for "inputs - outputs > 1 BTC" or something could probably save users much money and grief.

That is the danger and power of raw transaction APIs.  They give you enough roof to hang yourself.

legendary
Activity: 1974
Merit: 1029
April 03, 2013, 04:50:44 PM
#1
I've decided to publish the program I use to create custom transactions, given the level of noise this subject is generating lately. It's in Perl, for Linux, uses the module JSON::RPC::Client (available in the ubuntu package libjson-rpc-perl) and assumes a bitcoin configuration in the current user's ~/.bitcoin/bitcoin.conf.

Here's a sample session:

Code:
$ mktx.pl 
idx       amount                             address  btcdays grp                                                               vout
 0)  18.43147597  1A7y8jy7xxxxxxxxxxxxxxxxxxxxxxxxxx    29.44   8 9c26c17f780e9e9415a6b8a58fxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:0
 1)  13.30826540  19GgbRa5xxxxxxxxxxxxxxxxxxxxxxxxxx   144.73   0 9d7ffe1562756e216c92935a71xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:0
 2)   9.00000000  18NAfDsdxxxxxxxxxxxxxxxxxxxxxxxxxx  1192.31  10 0ba6400f87c554b7e41d0fe8b8xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:1
 3)   3.58293316  15HK4cyhxxxxxxxxxxxxxxxxxxxxxxxxxx    14.18  12 a2dad2d0289449b80850d77b2dxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:2
 4)   2.57453211  1KXoabe8xxxxxxxxxxxxxxxxxxxxxxxxxx   349.31   0 5f94d690d75bca1becdc91cacdxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:0
 5)   2.29610578   1ydDG6nxxxxxxxxxxxxxxxxxxxxxxxxxx     9.09   2 a2dad2d0289449b80850d77b2dxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:0
 6)   1.83458911  1EmHjWp7xxxxxxxxxxxxxxxxxxxxxxxxxx     7.29  11 df64fb188d1965a6607032b502xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:1
 7)   0.80863146  1LMUtqtJxxxxxxxxxxxxxxxxxxxxxxxxxx     3.21   6 df64fb188d1965a6607032b502xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:0
 8)   0.59664818  1HyZb7Psxxxxxxxxxxxxxxxxxxxxxxxxxx     2.36   1 16703bc4ce9f35bd75a64e07e5xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:1

Select inputs (separated by spaces): 0 1 4

Enter outputs: destination address and amount separated by a space (enter to finish)
Enter output (34.31427348 available): 12XCzX7ogxxxxxxxxxxxxxxxxxxxxxxxxx 26.34859225
Enter output (7.96568123 available):

  Debug: inputs total amount is '34.31427348', assigned '26.34859225'

Enter address where to send 7.96568123 as change: new

  Change will go to '1BZoDTY3NGr65axxxxxxxxxxxxxxxxxxxx'

  Debug: estimated transaction size: 614 bytes (fee required at 10000 bytes or more)
  Debug: transaction priority: 122770.67M (fee required at 57.6M or less)

Enter desired fee (default 0):
Enter wallet passphrase (needed for signing the transaction):

A list of unspent outputs is shown, sorted by amount. The column "grp" shows the same number for the coins that are known to be linked, so using them as inputs doesn't leak any privacy. There's also a column showing bitcoindays to aid in the coin selection. The "vout" column is there just because.

Enter the outputs that you want to use as the inputs of the new transaction, then enter the destination addresses and their amounts. You can create as many outputs as you want. Enter an empty line to finish this step, then you'll be asked for an address for the change. Note that I've entered 'new', you can enter 'new' in the previous step too. After that, the required fee is calculated and you are given the option to use the suggested one or override it with your own (ie you can pay a fee even if it isn't needed, or not pay a fee when you really should—use at your own risk!). The fee is deducted from the change output.

As a last step, the program asks for the wallet encryption key in order to sign the transaction, then spits out the resulting data structure and, at the bottom, the raw serialized transaction is printed, so you can manually feed it to the sendrawtransaction RPC command or paste it in blochain.info/pushtx. Inputs and outputs are shuffled to enhance privacy (I don't think it's needed on inputs but decided to play safe) (outputs are implicitly shuffled due to the nature of Perl hashes). The fact that this program doesn't send the transaction itself is a feature, not a bug.

Interestingly, the sample session is from a real transaction, which took 3 blocks to confirm even when its priority seems to be quite high.

Criticism welcome (preferably constructive Wink).

Update 2013-05-03: new version, that accept the parameter --list (or -l) to list the unpent outputs and exit.

Update 2013-05-06: was applying a fee per 10 Kb instead of per Kb. Ouch.

Code:
#!/usr/bin/perl

## TODO: could add: if ($fee) { print "you're paying %d satoshis per kb", $fee*1e8 / ($tx_size/1000); }

use warnings;
use strict;
use File::Spec;
use Scalar::Util qw/looks_like_number/;
use List::Util qw/min sum shuffle/;
use Getopt::Long;
use JSON::RPC::Client;
use Data::Dumper;

my $MIN_FEE = 0.0005;
my $cfgfile = File::Spec->catfile ($ENV{'HOME'}, '.bitcoin', 'bitcoin.conf');
my ($rpcuser, $rpcpass);

sub get_rpc {
    -f $cfgfile or die "bitcoin configuration not found\n";

    open my $fd, '<', $cfgfile or die "open: '$cfgfile': $!";
    while (<$fd>) {
        if (/^rpcuser=(.*)/)     { $rpcuser = $1; }
        if (/^rpcpassword=(.*)/) { $rpcpass = $1; }
    }    
    close $fd;

    if (!$rpcuser or !$rpcpass) { die "can't find RPC credentials in bitcoin configuration\n"; }

    my $url = "http://$rpcuser:$rpcpass\@localhost:8332/";      
    my $rpc = JSON::RPC::Client->new;
    $rpc->prepare ($url, [ qw/
        createrawtransaction decoderawtransaction getnewaddress getrawtransaction listaddressgroupings
        listunspent signrawtransaction validateaddress walletpassphrase
    / ]);

    return $rpc;
}

sub addr2grp {
    my ($rpc) = @_;

    my $groupings = $rpc->listaddressgroupings;
    my %addr2grp;
    foreach my $group_idx (0 .. $#$groupings) {
        foreach my $entry (@{ $groupings->[$group_idx] }) {
            $addr2grp{ $entry->[0] } = $group_idx;
        }
    }
    return %addr2grp;
}

sub vouts {
    my ($rpc) = @_;

    my $unspent = $rpc->listunspent;

    my $vouts;
    foreach my $u (@$unspent) {
        my $rawtx = $rpc->getrawtransaction ($u->{'txid'});
        $rawtx = $rpc->decoderawtransaction ($rawtx);
        my $vout = $rawtx->{'vout'}[$u->{'vout'}];

        next if 'pubkeyhash' ne $vout->{'scriptPubKey'}{'type'} and 'scripthash' ne $vout->{'scriptPubKey'}{'type'};

        $u->{'address'} = $vout->{'scriptPubKey'}{'addresses'}[0];
        $u->{'btcdays'} = $u->{'amount'} * $u->{'confirmations'} / 144;

        push @$vouts, $u;
    }

    return $vouts;
}

sub get_selected_outs {
    my ($largest_out) = @_;

    print "\n";
    OUTER: {
        print 'Select inputs (separated by spaces): ';
        my $selected_ins = <>; chomp $selected_ins; my @selected_ins = split /\s+/, $selected_ins;
        foreach my $in (@selected_ins) {
            if ($in !~ /^\d+$/)     { warn "Error: invalid input '$in'\n"; redo OUTER; }
            if ($in > $largest_out) { warn "Error: input '$in' too large\n"; redo OUTER; }
        }
        return @selected_ins;
    }
}

sub get_dests {
    my ($rpc, $avail) = @_;

    my %dests;
    print "\nEnter outputs: destination address and amount separated by a space (enter to finish)\n";
    while (1) {
        print "Enter output ($avail available): ";
        my $dest = <>; chomp $dest;
        if (!length $dest) {
            last if %dests;
            warn "enter at least one destination\n";
            redo;
        }

        my ($dest_addr, $dest_amnt) = split /\s+/, $dest;
        if (!defined $dest_amnt) {
            print "error: enter destination address and amount separated by a space\n";
            next;
        }
        if (!looks_like_number $dest_amnt)                    { warn "amount '$dest_amnt' isn't a number\n"; redo; }
        if ($dest_amnt <= 0)                                  { warn "amount '$dest_amnt' isn't positive\n"; redo; }
        if ('new' eq $dest_addr) {
            $dest_addr = $rpc->getnewaddress;
            print "Using address '$dest_addr'\n";
        }
        if (!$rpc->validateaddress ($dest_addr)->{'isvalid'}) { warn "address '$dest_addr' is invalid\n"; redo; }
        if (exists $dests{$dest_addr})                        { warn "there's already an output to that address\n"; redo; }
        if ($dest_amnt > $avail)                              { warn "Error: not enough funds in the selected inputs\n"; redo; }

        $dests{$dest_addr} = 0+$dest_amnt;
        $avail = 0+sprintf '%.8f', $avail - $dest_amnt;
        last unless $avail;
    }
    print "\n";
    return %dests;
}

sub fee {
    my ($selected, $dests, $change_addr) = @_;

    my $tx_size = 10 + 180*@$selected + 32*keys %$dests;
    print "  Debug: estimated transaction size: $tx_size bytes (fee required at 10000 bytes or more)\n";
    my $tx_size_ok = $tx_size < 10000;

    my $smallest_out = min values %$dests;
    my $out_amounts_ok = $smallest_out >= 0.01;

    my $tx_prio = sum map { $_->{'amount'}*10e8 * $_->{'confirmations'} } @$selected;
    $tx_prio /= $tx_size;
    my $tx_prio_ok = $tx_prio > 57_600_000;
    printf "  Debug: transaction priority: %.2fM (fee required at 57.6M or less)\n", $tx_prio/1e6;

    my $sugg_fee = 0;
    if (!$tx_size_ok || !$out_amounts_ok) {
        if (!$tx_size_ok)     { print "  Transaction too big ($tx_size bytes), fee recommended\n"; }
        if (!$out_amounts_ok) { print "  Some of the outputs is smaller than 0.01 BTC ($smallest_out), fee recommended\n"; }
        my $rounded_tx_size = int ($tx_size / 1000); $rounded_tx_size++;
        $sugg_fee = $MIN_FEE * $rounded_tx_size;
    }
    if (!$tx_prio_ok) {
        print "  Transaction priority too low ($tx_prio), fee recommended\n";
        $sugg_fee += $MIN_FEE;
    }

    print "  Warning: a fee is recommended but this transaction hasn't a change output from which substract the fee\n" if $sugg_fee && !$change_addr;
    print "  Warning: suggested fee is greater than the change output\n" if $change_addr and $sugg_fee > $dests->{$change_addr};
    print "\nEnter desired fee (default $sugg_fee): "; my $fee = <>; chomp $fee; $fee ||= $sugg_fee;
    if (!$fee) {
        print "  Warning: a fee is recommended\n" if $sugg_fee;
        return;
    }
    die "Error: no change output from which substract the fee\n" if $fee && !$change_addr;
    die "Error: fee is greater than the change output\n" if $change_addr and $fee > $dests->{$change_addr};
    if ($fee == $dests->{$change_addr}) {
        delete $dests->{$change_addr};   ## oops, this alters the tx size, therefore it potentially affects the fee itself
    } else {
        $dests->{$change_addr} -= $fee;
    }
}

###################################################

GetOptions \my %opts, '--list' or die "getopt failed\n";

my $rpc = get_rpc;
my %addr_to_group = addr2grp $rpc;
my $vouts = vouts $rpc or do { print "No funds\n"; exit; };

printf qq/%3s %12s %35s %8s %3s %66s\n/, qw/idx amount address btcdays grp vout/;
my $index = 0;
@$vouts = reverse sort { $a->{'amount'} <=> $b->{'amount'} } @$vouts;
foreach my $vout (@$vouts) {
    #my $amnt = $vout->{'amount'}; if ($amnt !~ /\./) { $amnt .= '.'; } $amnt .= '00000000'; $amnt =~ s/(\..{8}).*/$1/;
    my $amnt = sprintf '%.8f', $vout->{'amount'};
    printf qq/%2s) %12s %35s %8.2f %3s %s:%s\n/, $index, $amnt, @$vout{qw/address btcdays/}, $addr_to_group{ $vout->{'address'} }, @$vout{qw/txid vout/};
    $index++;
}

exit if $opts{'list'};

my @selected_ins = get_selected_outs $#$vouts;

my @selected = @$vouts[@selected_ins];
my $available_amnt = sum map { $_->{'amount'} } @selected;

my %dests = get_dests $rpc, $available_amnt;
my $txamnt = sum values %dests;

my $unassigned = sprintf '%.8f', $available_amnt - $txamnt;
my $change_addr;
if ($unassigned >= 1e-8) {
    ## $unassigned may have extra decimal places due to floating point issues, see if those decimals
    ## are already present in these variables or they appear in the substraction above
    print "  Debug: inputs total amount is '$available_amnt', assigned '$txamnt'\n\n";
    {
        print "Enter address where to send $unassigned as change: "; $change_addr = <>; chomp $change_addr;
        if ('new' eq $change_addr) {
            $change_addr = $rpc->getnewaddress;
            print "\n  Change will go to '$change_addr'\n";
        }
        if (!$rpc->validateaddress ($change_addr)->{'isvalid'}) { warn "address '$change_addr' is invalid\n"; redo; }
        if (exists $dests{$change_addr})                        { warn "there's already an output to that address\n"; redo; }
        $dests{$change_addr} = 0+$unassigned;
        last;
    }
    print "\n";
}

fee \@selected, \%dests, $change_addr;

my $ins  = [ shuffle map { { txid => $_->{'txid'}, vout => 0+$_->{'vout'} } } @selected ];
my $outs = \%dests;
my $tx = $rpc->createrawtransaction ([ $ins, $outs ]);

print 'Enter wallet passphrase (needed for signing the transaction): ';
system 'stty -echo' and die "fork/exec: $!";   ## this could be improved
my $wpp = <>; chomp $wpp; print "\n";
system 'stty echo' and die "fork/exec: $!";

$rpc->walletpassphrase ($wpp, 1);
my $signed_tx = $rpc->signrawtransaction ($tx)->{'hex'};
my $decoded = $rpc->decoderawtransaction ($signed_tx);
print Data::Dumper->Dump (sub{\@_}->(\$decoded), ['Transaction']);
print "Raw: $signed_tx\n";

END { system 'stty echo'; }


Jump to: