diff --git a/app/controllers/blocks_controller.rb b/app/controllers/blocks_controller.rb index b84095d..e49e313 100644 --- a/app/controllers/blocks_controller.rb +++ b/app/controllers/blocks_controller.rb @@ -1,4 +1,5 @@ require 'timeout' +require 'method_source' class BlocksController < ApplicationController @@ -72,7 +73,14 @@ def address # custom script. when :id is given, the :sig_hash can be overridden, and when # no :sig_hash is given or found, signature validation will be skipped. def script - require 'method_source' + @options = { + verify_dersig: true, + verify_low_s: true, + verify_strictenc: true, + verify_sigpushonly: true, + verify_minimaldata: true, + verify_cleanstack: true } + @options.keys.each {|k| @options[k] = params[k] == "1" if params[k] } # if tx_hash:idx is given, fetch tx from db and use those scripts if params[:id] =~ /([0-9a-fA-F]{64}):(\d+)/ @@ -92,7 +100,16 @@ def script # if there is a script, execute it if @script_sig && @pk_script @script = Bitcoin::Script.new(@script_sig, @pk_script) - @result = @script.run do |pubkey, sig, hash_type, subscript| + + if @options[:verify_sigpushonly] && !@script.is_push_only?(@script_sig) + return (@result, @debug = false, [[], :verify_sigpushonly]) + end + + if @options[:verify_minimaldata] && !@script.pushes_are_canonical? + return (@result, @debug = false, [[], :verify_minimaldata]) + end + + @result = @script.run(Time.now.to_i, @options) do |pubkey, sig, hash_type, subscript| # get sig_hash from tx if tx is given and sig_hash isn't already set @sig_hash ||= @tx.signature_hash_for_input(@txin.tx_idx, subscript, hash_type) if @tx && @txin # if there is a sig_hash (either computed or passed as parameter), verify signature, @@ -100,6 +117,11 @@ def script @sig_hash ? Bitcoin.verify_signature(@sig_hash, sig, pubkey.unpack("H*")[0]) : true end @debug = @script.debug + + if @options[:verify_cleanstack] && !@script.stack.empty? + @result = false + @debug += [@script.stack, :verify_cleanstack] + end end @page_title = "Debug Script Execution" diff --git a/app/views/blocks/script.html.haml b/app/views/blocks/script.html.haml index a6bd399..2b7896c 100644 --- a/app/views/blocks/script.html.haml +++ b/app/views/blocks/script.html.haml @@ -25,6 +25,23 @@ %td = text_field_tag :sig_hash, @sig_hash_str, size: 64 Optional; If empty, all signatures are assumed to be valid. + + %tr + %th Verify + %td + - { minimaldata: "Check that all numbers are encoded with the minimum possible number of bytes", + sigpushonly: "Check that the script_sig only contains pushes, no opcodes", + cleanstack: "Check that the scripts leaves a clean stack", + dersig: "Check that all signatures are in valid DER encoding", + low_s: "Check that all signatures have non-negative S values", + strictenc: "Check that all signatures and pubkeys are in canonical encoding", + }.each do |flag, desc| + = hidden_field_tag "verify_#{flag}", "0" + = check_box_tag "verify_#{flag}", "1", @options[:"verify_#{flag}"], id: "verify_#{flag}_cb" + - klass = (@debug && @debug[-1] == :"verify_#{flag}") ? "error" : "" + = label_tag "verify_#{flag}_cb", flag, title: desc, class: klass + (hover for info) + %tr %th Result %th @@ -57,6 +74,12 @@ %pre The number #{op.split("_")[1]} is pushed onto the stack. - when "RESULT" %pre True if the remaining stack is anything other than empty or 0. + - when :verify_minimaldata + %pre Not all numbers are encoded with the minimum possible number of bytes! + - when :verify_sigpushonly + %pre The script_sig may only contain PUSHDATA operations, no other opcodes! + - when :verify_cleanstack + %pre The script may not leave any values on the stack after successful execution! - else - comment = Bitcoin::Script.instance_method(@debug[i+1].downcase).comment rescue "" %pre= comment.split(/\n?#\s/).map(&:strip).join("\n") diff --git a/spec/controllers/blocks_controller_spec.rb b/spec/controllers/blocks_controller_spec.rb index 9bbf9d6..afa156d 100644 --- a/spec/controllers/blocks_controller_spec.rb +++ b/spec/controllers/blocks_controller_spec.rb @@ -145,7 +145,8 @@ end it "should execute arbitrary script with sighash" do - get :script, script_sig: script_sig, pk_script: pk_script, sig_hash: sig_hash + get :script, script_sig: script_sig, pk_script: pk_script, sig_hash: sig_hash, + verify_low_s: "0" assigns(:result).should == true end @@ -162,10 +163,55 @@ it "should ignore signatures when no sighash is given" do s, p = script_sig.split(" "); s[140] = "a"; s[141] = "a"; script_sig = [s, p].join(" ") - get :script, script_sig: script_sig, pk_script: pk_script, sig_hash: "" + get :script, script_sig: script_sig, pk_script: pk_script, sig_hash: "", verify_low_s: "0" assigns(:result).should == true end + describe :verify do + + it "should verify minimaldata" do + opts = { script_sig: "01", pk_script: "", sig_hash: "" } + + get :script, opts + assigns(:result).should == false + assigns(:debug).should == [[], :verify_minimaldata] + + get :script, opts.merge(verify_minimaldata: "0") + assigns(:result).should == true + end + + it "should verify sigpushonly" do + opts = { script_sig: "1 OP_DROP 1", pk_script: "", sig_hash: "" } + + get :script, opts + assigns(:result).should == false + assigns(:debug).should == [[], :verify_sigpushonly] + + get :script, opts.merge(verify_sigpushonly: "0") + assigns(:result).should == true + end + + it "should verify cleanstack" do + get :script, script_sig: "1 1", pk_script: "", sig_hash: "" + assigns(:result).should == false + assigns(:debug).should == [[], "OP_1", [1], "OP_1", [1, 1], "OP_CODESEPARATOR", [1, 1], "RESULT", [1], :verify_cleanstack] + + get :script, script_sig: "1 1", pk_script: "", sig_hash: "", verify_cleanstack: "0" + assigns(:result).should == true + end + + it "should verify low_s" do + opts = { script_sig: script_sig, pk_script: pk_script, sighash: sig_hash } + + get :script, opts + assigns(:result).should == false + + get :script, opts.merge(verify_low_s: "0") + assigns(:result).should == true + end + + end + end describe :scripts do