Skip to content

Commit 592b273

Browse files
committed
Hacking on taproot, work in progress
1 parent f41376b commit 592b273

File tree

14 files changed

+843
-88
lines changed

14 files changed

+843
-88
lines changed

lib/Bitcoin/Crypto/Constants.pm

+4
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ use constant {
3131
locktime_height_threshold => 500_000_000,
3232
max_sequence_no => 0xffffffff,
3333

34+
sighash_default => 0x00,
3435
sighash_all => 0x01,
3536
sighash_none => 0x02,
3637
sighash_single => 0x03,
@@ -44,6 +45,9 @@ use constant {
4445
psbt_global_map => 'global',
4546
psbt_input_map => 'in',
4647
psbt_output_map => 'out',
48+
49+
signing_algorithm_ecdsa => 'ecdsa',
50+
signing_algorithm_schnorr => 'schnorr',
4751
};
4852

4953
# These constants are environment-specific and internal only

lib/Bitcoin/Crypto/Exception.pm

+7
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,13 @@ sub as_string
143143
use parent -norequire, 'Bitcoin::Crypto::Exception';
144144
}
145145

146+
{
147+
148+
package Bitcoin::Crypto::Exception::KeyConvert;
149+
150+
use parent -norequire, 'Bitcoin::Crypto::Exception';
151+
}
152+
146153
{
147154

148155
package Bitcoin::Crypto::Exception::MnemonicGenerate;

lib/Bitcoin/Crypto/Key/Public.pm

+4-49
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use Bitcoin::Crypto::Base58 qw(encode_base58check);
1313
use Bitcoin::Crypto::Bech32 qw(encode_segwit);
1414
use Bitcoin::Crypto::Types -types;
1515
use Bitcoin::Crypto::Constants;
16-
use Bitcoin::Crypto::Util qw(hash160 get_public_key_compressed tagged_hash);
16+
use Bitcoin::Crypto::Util qw(hash160 get_public_key_compressed);
1717
use Bitcoin::Crypto::Helpers qw(ecc);
1818

1919
use namespace::clean;
@@ -25,51 +25,8 @@ has extended 'key_instance' => (
2525
predicate => 1,
2626
);
2727

28-
has field 'taproot_key_instance' => (
29-
isa => ByteStr,
30-
lazy => 1,
31-
writer => -hidden,
32-
predicate => 1,
33-
);
34-
3528
sub _is_private { 0 }
3629

37-
sub _validate_key
38-
{
39-
my ($self) = @_;
40-
if ($self->has_key_instance) {
41-
$self->SUPER::_validate_key;
42-
}
43-
elsif (!$self->has_taproot_key_instance) {
44-
Bitcoin::Crypto::Exception::KeyCreate->raise(
45-
'public key must have either regular or taproot key data'
46-
);
47-
}
48-
}
49-
50-
sub _build_taproot_key_instance
51-
{
52-
my ($self) = @_;
53-
54-
return ecc->xonly_public_key($self->raw_key('public_compressed'));
55-
}
56-
57-
signature_for raw_key => (
58-
method => Object,
59-
positional => [Maybe [Enum [qw(public public_compressed public_taproot)]], {default => undef}],
60-
);
61-
62-
sub raw_key
63-
{
64-
my ($self, $type) = @_;
65-
66-
if ($type && $type eq 'public_taproot') {
67-
return $self->taproot_key_instance;
68-
}
69-
70-
return $self->SUPER::raw_key($type);
71-
}
72-
7330
signature_for get_hash => (
7431
method => Object,
7532
positional => [],
@@ -118,11 +75,7 @@ sub witness_program
11875
return shift->get_hash;
11976
},
12077
(Bitcoin::Crypto::Constants::taproot_witness_version) => sub {
121-
my $self = shift;
122-
my $internal = $self->raw_key('public_taproot');
123-
my $tweaked = tagged_hash($internal, 'TapTweak');
124-
my $combined = ecc->combine_public_keys(ecc->create_public_key($tweaked), "\x02" . $internal);
125-
return ecc->xonly_public_key($combined);
78+
return shift->taproot_tweaked_key('public');
12679
},
12780
};
12881

@@ -441,6 +394,8 @@ L<Bitcoin::Crypto::Exception> namespace:
441394
442395
=item * KeyCreate - key couldn't be created correctly
443396
397+
=item * KeyConvert - taproot key couldn't be converted to ecc key
398+
444399
=item * Verify - couldn't verify the message correctly
445400
446401
=item * NetworkConfig - incomplete or corrupted network configuration

lib/Bitcoin/Crypto/Role/Key.pm

+41-5
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use Types::Common -sigs, -types;
88

99
use Bitcoin::Crypto::Types -types;
1010
use Bitcoin::Crypto::Constants;
11-
use Bitcoin::Crypto::Util qw(get_key_type);
11+
use Bitcoin::Crypto::Util qw(get_key_type tagged_hash);
1212
use Bitcoin::Crypto::Helpers qw(ensure_length ecc);
1313
use Bitcoin::Crypto::Exception;
1414

@@ -73,7 +73,7 @@ sub has_purpose
7373

7474
signature_for raw_key => (
7575
method => Object,
76-
positional => [Maybe [Enum [qw(private public public_compressed)]], {default => undef}],
76+
positional => [Maybe [Enum [qw(private public public_compressed public_xonly)]], {default => undef}],
7777
);
7878

7979
# helpers for raw_key
@@ -99,6 +99,7 @@ sub raw_key
9999
{
100100
my ($self, $type) = @_;
101101
my $is_private = $self->_is_private;
102+
my $key = $self->key_instance;
102103

103104
$type //= $is_private ? 'private' : 'public';
104105
if ($type eq 'public' && (!$self->does('Bitcoin::Crypto::Role::Compressed') || $self->compressed)) {
@@ -110,17 +111,52 @@ sub raw_key
110111
'cannot create private key from a public key'
111112
) unless $is_private;
112113

113-
return $self->__full_private($self->key_instance);
114+
return $self->__full_private($key);
115+
}
116+
elsif ($type eq 'public_xonly') {
117+
$key = $self->__private_to_public($key)
118+
if $is_private;
119+
120+
return ecc->xonly_public_key($self->__public_compressed($key, 1));
114121
}
115122
else {
116-
my $key = $self->key_instance;
117123
$key = $self->__private_to_public($key)
118124
if $is_private;
119125

120126
return $self->__public_compressed($key, $type eq 'public_compressed');
121127
}
128+
}
129+
130+
signature_for taproot_tweaked_key => (
131+
method => Object,
132+
head => [Maybe [Enum [qw(private public)]], {default => undef}],
133+
named => [
134+
tweak_suffix => Maybe [ByteStr],
135+
{default => undef},
136+
],
137+
bless => !!0,
138+
);
122139

123-
# no need to check for invalid input, since we have a signature with enum
140+
sub taproot_tweaked_key
141+
{
142+
my ($self, $type, $args) = @_;
143+
$type //= $self->_is_private ? 'private' : 'public';
144+
145+
if ($type eq 'private') {
146+
my $internal = $self->raw_key('private');
147+
my $internal_public = ecc->create_public_key($internal);
148+
$internal = ecc->negate_private_key($internal)
149+
if substr($internal_public, 0, 1) eq "\x03";
150+
151+
my $tweak = tagged_hash(ecc->xonly_public_key($internal_public) . ($args->{tweak_suffix} // ''), 'TapTweak');
152+
return ecc->add_private_key($internal, $tweak);
153+
}
154+
else {
155+
my $internal = $self->raw_key('public_xonly');
156+
my $tweak = tagged_hash($internal . ($args->{tweak_suffix} // ''), 'TapTweak');
157+
my $combined = ecc->combine_public_keys(ecc->create_public_key($tweak), "\x02" . $internal);
158+
return ecc->xonly_public_key($combined);
159+
}
124160
}
125161

126162
1;

lib/Bitcoin/Crypto/Role/SignVerify.pm

+33-5
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ use Types::Common -sigs, -types;
88

99
use Bitcoin::Crypto::Types -types;
1010
use Bitcoin::Crypto::Helpers qw(carp_once ecc);
11-
use Bitcoin::Crypto::Util qw(hash256);
11+
use Bitcoin::Crypto::Util qw(hash256 tagged_hash);
12+
use Crypt::Digest::SHA256 qw(sha256);
1213
use Bitcoin::Crypto::Transaction::Sign;
14+
use Bitcoin::Crypto::Constants;
1315
use Moo::Role;
1416

1517
requires qw(
@@ -19,22 +21,48 @@ requires qw(
1921

2022
signature_for sign_message => (
2123
method => Object,
22-
positional => [ByteStr],
24+
head => [ByteStr],
25+
named => [
26+
algorithm => SignatureAlgorithm,
27+
{default => Bitcoin::Crypto::Constants::signing_algorithm_ecdsa},
28+
taproot_tweak_suffix => Maybe [ByteStr],
29+
{default => undef},
30+
],
31+
bless => !!0,
2332
);
2433

2534
sub sign_message
2635
{
27-
my ($self, $preimage) = @_;
36+
my ($self, $preimage, $args) = @_;
2837

2938
Bitcoin::Crypto::Exception::Sign->raise(
3039
'cannot sign a message with a public key'
3140
) unless $self->_is_private;
3241

33-
my $digest = hash256($preimage);
42+
my %algorithms = (
43+
(Bitcoin::Crypto::Constants::signing_algorithm_ecdsa) => {
44+
digest => \&hash256,
45+
signing_method => sub { ecc->sign_digest(@_) },
46+
raw_key => sub { $self->raw_key },
47+
},
48+
(Bitcoin::Crypto::Constants::signing_algorithm_schnorr) => {
49+
digest => sub { tagged_hash(shift, 'TapSighash') },
50+
signing_method => sub { ecc->sign_digest_schnorr(@_) },
51+
raw_key => sub {
52+
$self->taproot_tweaked_key(
53+
'private',
54+
tweak_suffix => $args->{taproot_tweak_suffix}
55+
);
56+
},
57+
},
58+
);
59+
60+
my $key = $algorithms{$args->{algorithm}}{raw_key}->();
61+
my $digest = $algorithms{$args->{algorithm}}{digest}->($preimage);
3462

3563
return Bitcoin::Crypto::Exception::Sign->trap_into(
3664
sub {
37-
return ecc->sign_digest($self->raw_key, $digest);
65+
return $algorithms{$args->{algorithm}}{signing_method}->($key, $digest);
3866
}
3967
);
4068
}

lib/Bitcoin/Crypto/Script.pm

+17-5
Original file line numberDiff line numberDiff line change
@@ -465,13 +465,25 @@ signature_for is_native_segwit => (
465465
sub is_native_segwit
466466
{
467467
my ($self) = @_;
468-
my @segwit_types = qw(P2WPKH P2WSH);
468+
my @segwit_types = qw(P2WPKH P2WSH P2TR);
469469

470470
my $script_type = $self->type // '';
471471

472472
return any { $script_type eq $_ } @segwit_types;
473473
}
474474

475+
signature_for is_taproot => (
476+
method => Object,
477+
positional => [],
478+
);
479+
480+
sub is_taproot
481+
{
482+
my ($self) = @_;
483+
484+
return ($self->type // '') eq 'P2TR';
485+
}
486+
475487
sub get_script
476488
{
477489
my ($self) = @_;
@@ -641,12 +653,12 @@ sub get_address
641653
return encode_segwit($self->network->segwit_hrp, $version . $address);
642654
};
643655

644-
if ($self->is_native_segwit) {
645-
my $version = pack 'C', Bitcoin::Crypto::Constants::segwit_witness_version;
656+
if ($self->type eq 'P2TR') {
657+
my $version = pack 'C', Bitcoin::Crypto::Constants::taproot_witness_version;
646658
return $segwit->($version, $address);
647659
}
648-
elsif ($self->type eq 'P2TR') {
649-
my $version = pack 'C', Bitcoin::Crypto::Constants::taproot_witness_version;
660+
elsif ($self->is_native_segwit) {
661+
my $version = pack 'C', Bitcoin::Crypto::Constants::segwit_witness_version;
650662
return $segwit->($version, $address);
651663
}
652664
elsif ($self->type eq 'P2PKH') {

lib/Bitcoin/Crypto/Script/Common.pm

+9
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,15 @@ sub _make_WSH
4242
->add('OP_EQUAL');
4343
}
4444

45+
#sub _make_P2TR
46+
#{
47+
# my ($class, $script, $pubkey) = @_;
48+
#
49+
# return $script
50+
# ->push($pubkey)
51+
# ->add('OP_CHECKSIG');
52+
#}
53+
4554
sub _get_method
4655
{
4756
my ($class, $type) = @_;

lib/Bitcoin/Crypto/Transaction.pm

+21-1
Original file line numberDiff line numberDiff line change
@@ -253,9 +253,21 @@ sub get_digest
253253
{
254254
my ($self, $params) = @_;
255255

256+
return $self->get_digest_object($params)->get_digest;
257+
}
258+
259+
signature_for get_digest_object => (
260+
method => Object,
261+
positional => [HashRef, {slurpy => !!1}],
262+
);
263+
264+
sub get_digest_object
265+
{
266+
my ($self, $params) = @_;
267+
256268
$params->{transaction} = $self;
257269
my $digest = Bitcoin::Crypto::Transaction::Digest->new($params);
258-
return $digest->get_digest;
270+
return $digest;
259271
}
260272

261273
signature_for fee => (
@@ -771,6 +783,14 @@ The sighash which should be used for the digest. By default C<SIGHASH_ALL>.
771783
772784
=back
773785
786+
=head3 get_digest_object
787+
788+
$digest_object = $object->get_digest_object(%params)
789+
790+
Same as L</get_digest>, but returns an object of
791+
L<Bitcoin::Crypto::Transaction::Digest> instead of a bytestring. Advanced use
792+
only.
793+
774794
=head3 fee
775795
776796
$fee = $object->fee()

0 commit comments

Comments
 (0)