Skip to content

Commit e9f70d2

Browse files
committed
feat: add support for p2wpkh and p2wsh
1 parent 50b8704 commit e9f70d2

File tree

5 files changed

+166
-8
lines changed

5 files changed

+166
-8
lines changed

lib/bitcoin/script.rb

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
require_relative '../bitcoin_data_io'
22
require_relative '../encoding_helper'
3+
require_relative '../hash_helper'
34
require_relative './op'
45

56
module Bitcoin
@@ -88,14 +89,14 @@ def serialize
8889
encode_varint(raw.length) + raw
8990
end
9091

91-
def evaluate(z) # rubocop:disable Metrics/MethodLength
92+
def evaluate(z, witness: nil)
9293
cmds = @cmds.clone
9394
stack = []
9495
altstack = []
9596

9697
while cmds.any?
9798
cmd = cmds.shift
98-
return false unless resolve_cmd(cmd, cmds, stack, altstack, z)
99+
return false unless resolve_cmd(cmd, cmds, stack, altstack, z, witness: witness)
99100
end
100101

101102
return false if stack.empty? || stack.pop.empty?
@@ -110,6 +111,18 @@ def p2sh?(cmds = @cmds)
110111
&& cmds[2] == 135
111112
end
112113

114+
def p2wpkh?(cmds = @cmds)
115+
cmds.length == 2 \
116+
&& cmds[0] == 0 \
117+
&& cmds[1].is_a?(String) && cmds[1].length == 20
118+
end
119+
120+
def p2wsh?(cmds = @cmds)
121+
cmds.length == 2 \
122+
&& cmds[0] == 0 \
123+
&& cmds[1].is_a?(String) && cmds[1].length == 32
124+
end
125+
113126
def self.p2pkh(hash160)
114127
Script.new([118, 169, hash160, 136, 172])
115128
end
@@ -124,14 +137,18 @@ def self.p2wsh(hash256)
124137

125138
private
126139

127-
def resolve_cmd(cmd, cmds, stack, altstack, z)
140+
def resolve_cmd(cmd, cmds, stack, altstack, z, witness: nil)
128141
if cmd.is_a? Integer
129142
return execute_operation(cmd, cmds, stack, altstack, z)
130143
else
131144
stack.append(cmd)
132145

133146
if p2sh?(cmds)
134147
return execute_p2sh(cmd, cmds, stack)
148+
elsif p2wpkh?
149+
return execute_p2wpkh(cmds, stack, witness)
150+
elsif p2wsh?
151+
return execute_p2wsh(cmds, stack, witness)
135152
end
136153
end
137154

@@ -154,6 +171,30 @@ def execute_p2sh(cmd, cmds, stack)
154171
cmds.concat self.class.parse(stream).cmds
155172
end
156173

174+
def execute_p2wpkh(cmds, stack, witness)
175+
h160 = stack.pop
176+
stack.pop
177+
cmds.concat witness
178+
cmds.concat self.class.p2wpkh(h160).cmds
179+
end
180+
181+
def execute_p2wsh(cmds, stack, witness)
182+
s256_stack = stack.pop
183+
stack.pop
184+
cmds.concat witness[0...-1]
185+
witness_script = witness.last
186+
s256_script = HashHelper.hash256(witness_script)
187+
188+
unless s256_stack == s256_script
189+
raise "Witness script hash mismatch: \n"\
190+
"stack: #{bytes_to_hex(s256_stack)} \n"\
191+
"script: #{bytes_to_hex(s256_script)}"
192+
end
193+
194+
stream = encode_varint(witness_script.size) + witness_script
195+
cmds.concat parse(stream).cmds
196+
end
197+
157198
def raw_serialize
158199
@cmds.map do |cmd|
159200
cmd.is_a?(Integer) ? int_to_little_endian(cmd, 1) : serialized_element_prefix(cmd) + cmd

lib/bitcoin/tx.rb

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -263,17 +263,41 @@ def hash_outputs
263263
def verify_input(input_index)
264264
tx_in = ins[input_index]
265265
script_pubkey = tx_in.script_pubkey testnet: @testnet
266+
z, witness = build_z_and_witness(script_pubkey, tx_in, input_index)
266267

268+
combined = tx_in.script_sig + script_pubkey
269+
combined.evaluate(z, witness: witness)
270+
end
271+
272+
def build_z_and_witness(script_pubkey, tx_in, input_index) # rubocop:disable Metrics/MethodLength
267273
if script_pubkey.p2sh?
268274
cmd = tx_in.script_sig.cmds[-1]
269275
raw_redeem = encode_varint(cmd.length) + cmd
270276
redeem_script = Script.parse(StringIO.new(raw_redeem))
277+
278+
if redeem_script.p2wpkh?
279+
[sig_hash_bip143(input_index, redeem_script), tx_in.witness]
280+
elsif redeem_script.p2wsh?
281+
build_z_from_witness(tx_in, input_index)
282+
else
283+
[sig_hash(input_index, redeem_script), nil]
284+
end
285+
286+
elsif script_pubkey.p2wpkh?
287+
[sig_hash_bip143(input_index, redeem_script: redeem_script), tx_in.witness]
288+
elsif script_pubkey.p2wsh?
289+
build_z_from_witness(tx_in, input_index)
271290
else
272-
redeem_script = nil
291+
[sig_hash(input_index), nil]
273292
end
274-
z = sig_hash(input_index, redeem_script)
275-
combined = tx_in.script_sig + script_pubkey
276-
combined.evaluate(z)
293+
end
294+
295+
def build_z_from_witness(tx_in, input_index)
296+
cmd = tx_in.witness.last
297+
raw_witness = encode_varint(cmd.size) + cmd
298+
witness_script = Script.parse(StringIO.new(raw_witness))
299+
300+
[sig_hash_bip143(input_index, witness_script: witness_script), tx_in.witness]
277301
end
278302

279303
def verify?

spec/bitcoin/script_spec.rb

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
require 'bitcoin/script'
22
require 'bitcoin/op'
33
require 'encoding_helper'
4+
require 'pry'
45

56
RSpec.describe Bitcoin::Script do
67
def _raw_script(hex_script)
@@ -222,6 +223,75 @@ def _raw_script(hex_script)
222223
end
223224
end
224225

226+
describe '#p2wpkh?' do
227+
let!(:script) { described_class.new(commands) }
228+
let(:commands) { [] }
229+
230+
context "when the script matches the p2wpkh pattern" do
231+
let(:hash160) { HashHelper.hash160('') }
232+
let(:commands) do
233+
[
234+
0,
235+
hash160
236+
]
237+
end
238+
let!(:script) { described_class.new(commands) }
239+
240+
it 'returns true' do
241+
expect(script.p2wpkh?).to be true
242+
end
243+
end
244+
245+
context "when the script does not match the p2wpkh pattern" do
246+
let(:commands) do
247+
[
248+
'',
249+
18,
250+
135
251+
]
252+
end
253+
let!(:script) { described_class.new(commands) }
254+
255+
it 'returns false' do
256+
expect(script.p2wpkh?).to be false
257+
end
258+
end
259+
end
260+
261+
describe '#p2wsh?' do
262+
let!(:script) { described_class.new(commands) }
263+
let(:commands) { [] }
264+
265+
context "when the script matches the p2wsh pattern" do
266+
let(:hash256) { HashHelper.hash256('') }
267+
let(:commands) do
268+
[
269+
0,
270+
hash256
271+
]
272+
end
273+
let!(:script) { described_class.new(commands) }
274+
275+
it 'returns true' do
276+
expect(script.p2wsh?).to be true
277+
end
278+
end
279+
280+
context "when the script does not match the p2wsh pattern" do
281+
let(:commands) do
282+
[
283+
'',
284+
18
285+
]
286+
end
287+
let!(:script) { described_class.new(commands) }
288+
289+
it 'returns false' do
290+
expect(script.p2wsh?).to be false
291+
end
292+
end
293+
end
294+
225295
describe "#+" do
226296
it "adds the commands arrays" do
227297
script1 = described_class.new([1, 2])

spec/bitcoin/tx_spec.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,27 @@ def bytes_to_hex(_bytes)
225225
expect(tx.verify_input(0)).to be true
226226
end
227227
end
228+
229+
context 'when tx is segwit and the script pubkey is p2wpkh' do
230+
let(:tx_p2wpkh) { described_class.parse raw_tx_sw, tx_fetcher: tx_fetcher }
231+
232+
it 'verifies unlocking script unlocks the script' do
233+
skip
234+
expect(tx_p2wpkh.verify_input(0)).to be true
235+
end
236+
end
237+
238+
context 'when tx is segwit and the script pubkey is p2wsh' do
239+
let(:raw_tx_p2wsh) do
240+
resolve_tx '98abf6f18cedc5e527775ff2b0d4235b16fd40774c33ab7c599e20099fd11259'
241+
end
242+
let(:tx_p2wsh) { described_class.parse raw_tx_p2wsh, tx_fetcher: tx_fetcher }
243+
244+
it 'verifies unlocking script unlocks the script' do
245+
skip
246+
expect(tx_p2wsh.verify_input(0)).to be true
247+
end
248+
end
228249
end
229250

230251
describe '#sign_input' do

spec/fixtures/transactions.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,7 @@
1717
"d37f9e7282f81b7fd3af0fde8b462a1c28024f1d83cf13637ec18d03f4518feb": "0100000001b74780c0b9903472f84f8697a7449faebbfb1af659ecb8148ce8104347f3f72d010000006b483045022100bb8792c98141bcf4dab4fd4030743b4eff9edde59cec62380c60ffb90121ab7802204b439e3572b51382540c3b652b01327ee8b14cededc992fbc69b1e077a2c3f9f0121027c975c8bdc9717de310998494a2ae63f01b7a390bd34ef5b4c346fa717cba012ffffffff01a627c901000000001976a914af24b3f3e987c23528b366122a7ed2af199b36bc88ac00000000",
1818
"d869f854e1f8788bcff294cc83b280942a8c728de71eb709a2c29d10bfe21b7c": "0100000000010115e180dc28a2327e687facc33f10f2a20da717e5548406f7ae8b4c811072f8560100000000ffffffff0100b4f505000000001976a9141d7cd6c75c2e86f4cbf98eaed221b30bd9a0b92888ac02483045022100df7b7e5cda14ddf91290e02ea10786e03eb11ee36ec02dd862fe9a326bbcb7fd02203f5b4496b667e6e281cc654a2da9e4f08660c620a1051337fa8965f727eb19190121038262a6c6cec93c2d3ecd6c6072efea86d02ff8e3328bbd0242b20af3425990ac00000000",
1919
"9b4fc533a9a69ed0eb030b08e40150999a8aa871e918345cb19855298c103ba3":
20-
"02000000000101e874ba97f9d35aa2814023894c732c83745fc629a0f5b82e58d6e4efa583be2c0000000000ffffffff0274b10200000000001976a91433e73d0a40a60d02d19c8b8d38ad6da14306683b88ac4c3c21000000000016001424ca8b17be9dfa365929dc9251da7d1533ca8b5e024730440220635a9629c9eb17f7ebbfc200e674aff253ec4213c3e6d3207e86886af9f6a751022072e2d54a9b8079e37cefd856ccf3c2383858346f55197d32c3a7fad18eac48e0012102550e51f143d27ed811e8cdf9008a21211bf26c73866a4b077117496b432421bd00000000"
20+
"02000000000101e874ba97f9d35aa2814023894c732c83745fc629a0f5b82e58d6e4efa583be2c0000000000ffffffff0274b10200000000001976a91433e73d0a40a60d02d19c8b8d38ad6da14306683b88ac4c3c21000000000016001424ca8b17be9dfa365929dc9251da7d1533ca8b5e024730440220635a9629c9eb17f7ebbfc200e674aff253ec4213c3e6d3207e86886af9f6a751022072e2d54a9b8079e37cefd856ccf3c2383858346f55197d32c3a7fad18eac48e0012102550e51f143d27ed811e8cdf9008a21211bf26c73866a4b077117496b432421bd00000000",
21+
"98abf6f18cedc5e527775ff2b0d4235b16fd40774c33ab7c599e20099fd11259":
22+
"01000000000101a7fe0333427c213e06447a1d44c3a47389654ab41d3e258aa0f6dcd2e51e59440100000000ffffffff0308c712000000000017a914f5926cf4b6797b550bd3d28ab3ec74fd84a01a1387103bcd00000000001976a9145ff42d2ae630e66ddd2cb29094e05c9710d3db9e88ac1407130300000000220020701a8d401c84fb13e6baf169d59684e17abd9fa216c8cc5b9fc63d622ff8c58d040047304402202157e477ddd81dc2be1089de72d5d7ad30a29daa1ed710d685ee8bc731afbee102207078681d21f6a03a8a6210f4c44af20a1478e469c3f27b540ba53121c4c980620147304402206338f38f78deef81a3d2fdf0359a8e685cb0087d76a58477f0c5fce44f70b784022075f892d5662fd63d8e35c1b93f9729130ca70e9145a736d18bc24be964344f73016952210375e00eb72e29da82b89367947f29ef34afb75e8654f6ea368e0acdfd92976b7c2103a1b26313f430c4b15bb1fdce663207659d8cac749a0e53d70eff01874496feff2103c96d495bfdd5ba4145e3e046fee45e84a8a48ad05bd8dbb395c011a32cf9f88053ae00000000"
2123
}

0 commit comments

Comments
 (0)