Add transaction trace capability for debugging scripts
This commit is contained in:
parent
8d1a3e1f7c
commit
62dd2e7cee
|
@ -20,11 +20,13 @@
|
||||||
|
|
||||||
#![allow(non_camel_case_types)]
|
#![allow(non_camel_case_types)]
|
||||||
|
|
||||||
|
use serialize::json;
|
||||||
|
|
||||||
/// Submodule to handle -all- opcodes. Outside of this module we use
|
/// Submodule to handle -all- opcodes. Outside of this module we use
|
||||||
/// a restricted set where the push/return/noop/illegal opcodes have
|
/// a restricted set where the push/return/noop/illegal opcodes have
|
||||||
/// a more convienient representation.
|
/// a more convienient representation.
|
||||||
pub mod all {
|
pub mod all {
|
||||||
|
use serialize::json;
|
||||||
// Heavy stick to translate between opcode types
|
// Heavy stick to translate between opcode types
|
||||||
use std::mem::transmute;
|
use std::mem::transmute;
|
||||||
|
|
||||||
|
@ -617,6 +619,12 @@ pub mod all {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl json::ToJson for Opcode {
|
||||||
|
fn to_json(&self) -> json::Json {
|
||||||
|
json::String(self.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Empty stack is also FALSE
|
/// Empty stack is also FALSE
|
||||||
pub static OP_FALSE: Opcode = OP_PUSHBYTES_0;
|
pub static OP_FALSE: Opcode = OP_PUSHBYTES_0;
|
||||||
/// Number 1 is also TRUE
|
/// Number 1 is also TRUE
|
||||||
|
@ -624,7 +632,7 @@ pub mod all {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Broad categories of opcodes with similar behavior
|
/// Broad categories of opcodes with similar behavior
|
||||||
#[deriving(PartialEq, Eq, Show)]
|
#[deriving(Clone, PartialEq, Eq, Show)]
|
||||||
pub enum OpcodeClass {
|
pub enum OpcodeClass {
|
||||||
/// Pushes the given number onto the stack
|
/// Pushes the given number onto the stack
|
||||||
PushNum(int),
|
PushNum(int),
|
||||||
|
@ -640,6 +648,12 @@ pub enum OpcodeClass {
|
||||||
Ordinary(Opcode)
|
Ordinary(Opcode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl json::ToJson for OpcodeClass {
|
||||||
|
fn to_json(&self) -> json::Json {
|
||||||
|
json::String(self.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
macro_rules! ordinary_opcode(
|
macro_rules! ordinary_opcode(
|
||||||
($($op:ident),*) => (
|
($($op:ident),*) => (
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
|
|
|
@ -98,6 +98,23 @@ pub enum ScriptError {
|
||||||
VerifyFailed,
|
VerifyFailed,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl json::ToJson for ScriptError {
|
||||||
|
fn to_json(&self) -> json::Json {
|
||||||
|
json::String(self.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A single iteration of a script execution
|
||||||
|
#[deriving(PartialEq, Eq, Show, Clone)]
|
||||||
|
pub struct TraceIteration {
|
||||||
|
index: uint,
|
||||||
|
opcode: allops::Opcode,
|
||||||
|
effect: opcodes::OpcodeClass,
|
||||||
|
stack: Vec<String>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_json!(TraceIteration, index, opcode, effect, stack)
|
||||||
|
|
||||||
/// Hashtype of a transaction, encoded in the last byte of a signature,
|
/// Hashtype of a transaction, encoded in the last byte of a signature,
|
||||||
/// specifically in the last 5 bits `byte & 31`
|
/// specifically in the last 5 bits `byte & 31`
|
||||||
#[deriving(PartialEq, Eq, Show, Clone)]
|
#[deriving(PartialEq, Eq, Show, Clone)]
|
||||||
|
@ -563,8 +580,10 @@ impl Script {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Evaluate the script, modifying the stack in place
|
/// Evaluate the script, modifying the stack in place
|
||||||
pub fn evaluate<'a>(&'a self, stack: &mut Vec<MaybeOwned<'a>>, input_context: Option<(&Transaction, uint)>)
|
pub fn evaluate<'a>(&'a self, stack: &mut Vec<MaybeOwned<'a>>,
|
||||||
-> Result<(), ScriptError> {
|
input_context: Option<(&Transaction, uint)>,
|
||||||
|
mut trace: Option<&mut Vec<TraceIteration>>)
|
||||||
|
-> Result<(), ScriptError> {
|
||||||
let &Script(ref raw) = self;
|
let &Script(ref raw) = self;
|
||||||
let secp = Secp256k1::new();
|
let secp = Secp256k1::new();
|
||||||
|
|
||||||
|
@ -868,8 +887,20 @@ impl Script {
|
||||||
}
|
}
|
||||||
if op == opcodes::OP_CHECKMULTISIGVERIFY { op_verify!(stack, VerifyFailed); }
|
if op == opcodes::OP_CHECKMULTISIGVERIFY { op_verify!(stack, VerifyFailed); }
|
||||||
}
|
}
|
||||||
}
|
} // end opcode match
|
||||||
|
} // end classification match
|
||||||
|
} // end loop
|
||||||
|
match trace {
|
||||||
|
Some(ref mut t) => {
|
||||||
|
let opcode = allops::Opcode::from_u8(byte);
|
||||||
|
t.push(TraceIteration {
|
||||||
|
index: index - 1,
|
||||||
|
opcode: opcode,
|
||||||
|
effect: opcode.classify(),
|
||||||
|
stack: stack.iter().map(|elem| elem.as_slice().to_hex()).collect()
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
None => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -1237,8 +1268,8 @@ mod test {
|
||||||
|
|
||||||
for (n, script) in script_pk.iter().enumerate() {
|
for (n, script) in script_pk.iter().enumerate() {
|
||||||
let mut stack = vec![];
|
let mut stack = vec![];
|
||||||
assert_eq!(tx.input[n].script_sig.evaluate(&mut stack, Some((&tx, n))), Ok(()));
|
assert_eq!(tx.input[n].script_sig.evaluate(&mut stack, Some((&tx, n)), None), Ok(()));
|
||||||
assert_eq!(script.evaluate(&mut stack, Some((&tx, n))), Ok(()));
|
assert_eq!(script.evaluate(&mut stack, Some((&tx, n)), None), Ok(()));
|
||||||
assert!(stack.len() >= 1);
|
assert!(stack.len() >= 1);
|
||||||
assert_eq!(read_scriptbool(stack.pop().unwrap().as_slice()), true);
|
assert_eq!(read_scriptbool(stack.pop().unwrap().as_slice()), true);
|
||||||
}
|
}
|
||||||
|
@ -1299,10 +1330,10 @@ mod test {
|
||||||
#[test]
|
#[test]
|
||||||
fn script_eval_simple() {
|
fn script_eval_simple() {
|
||||||
let mut script = Script::new();
|
let mut script = Script::new();
|
||||||
assert!(script.evaluate(&mut vec![], None).is_ok());
|
assert!(script.evaluate(&mut vec![], None, None).is_ok());
|
||||||
|
|
||||||
script.push_opcode(opcodes::all::OP_RETURN);
|
script.push_opcode(opcodes::all::OP_RETURN);
|
||||||
assert!(script.evaluate(&mut vec![], None).is_err());
|
assert!(script.evaluate(&mut vec![], None, None).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -1311,13 +1342,13 @@ mod test {
|
||||||
let script_pk: Script = deserialize(hex_pk.clone()).ok().expect("scriptpk");
|
let script_pk: Script = deserialize(hex_pk.clone()).ok().expect("scriptpk");
|
||||||
// Should be able to check that the sig is there and pk correct
|
// Should be able to check that the sig is there and pk correct
|
||||||
// before needing a transaction
|
// before needing a transaction
|
||||||
assert_eq!(script_pk.evaluate(&mut vec![], None), Err(PopEmptyStack));
|
assert_eq!(script_pk.evaluate(&mut vec![], None, None), Err(PopEmptyStack));
|
||||||
assert_eq!(script_pk.evaluate(&mut vec![Owned(vec![]), Owned(vec![])], None),
|
assert_eq!(script_pk.evaluate(&mut vec![Owned(vec![]), Owned(vec![])], None, None),
|
||||||
Err(EqualVerifyFailed("e729dea4a3a81108e16376d1cc329c91db589994".to_string(),
|
Err(EqualVerifyFailed("e729dea4a3a81108e16376d1cc329c91db589994".to_string(),
|
||||||
"b472a266d0bd89c13706a4132ccfb16f7c3b9fcb".to_string())));
|
"b472a266d0bd89c13706a4132ccfb16f7c3b9fcb".to_string())));
|
||||||
// But if the signature is there, we need a tx to check it
|
// But if the signature is there, we need a tx to check it
|
||||||
assert_eq!(script_pk.evaluate(&mut vec![Owned(vec![]), Owned("026d5d4cfef5f3d97d2263941b4d8e7aaa82910bf8e6f7c6cf1d8f0d755b9d2d1a".from_hex().unwrap())], None), Err(NoTransaction));
|
assert_eq!(script_pk.evaluate(&mut vec![Owned(vec![]), Owned("026d5d4cfef5f3d97d2263941b4d8e7aaa82910bf8e6f7c6cf1d8f0d755b9d2d1a".from_hex().unwrap())], None, None), Err(NoTransaction));
|
||||||
assert_eq!(script_pk.evaluate(&mut vec![Owned(vec![0]), Owned("026d5d4cfef5f3d97d2263941b4d8e7aaa82910bf8e6f7c6cf1d8f0d755b9d2d1a".from_hex().unwrap())], None), Err(NoTransaction));
|
assert_eq!(script_pk.evaluate(&mut vec![Owned(vec![0]), Owned("026d5d4cfef5f3d97d2263941b4d8e7aaa82910bf8e6f7c6cf1d8f0d755b9d2d1a".from_hex().unwrap())], None, None), Err(NoTransaction));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -1331,8 +1362,8 @@ mod test {
|
||||||
let script_pk: Script = deserialize(output_hex.clone()).ok().expect("scriptpk");
|
let script_pk: Script = deserialize(output_hex.clone()).ok().expect("scriptpk");
|
||||||
|
|
||||||
let mut stack = vec![];
|
let mut stack = vec![];
|
||||||
assert_eq!(tx.input[0].script_sig.evaluate(&mut stack, None), Ok(()));
|
assert_eq!(tx.input[0].script_sig.evaluate(&mut stack, None, None), Ok(()));
|
||||||
assert_eq!(script_pk.evaluate(&mut stack, Some((&tx, 0))), Ok(()));
|
assert_eq!(script_pk.evaluate(&mut stack, Some((&tx, 0)), None), Ok(()));
|
||||||
assert_eq!(stack.len(), 1);
|
assert_eq!(stack.len(), 1);
|
||||||
assert_eq!(read_scriptbool(stack.pop().unwrap().as_slice()), true);
|
assert_eq!(read_scriptbool(stack.pop().unwrap().as_slice()), true);
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,9 +24,11 @@
|
||||||
//!
|
//!
|
||||||
|
|
||||||
use std::default::Default;
|
use std::default::Default;
|
||||||
|
use serialize::hex::ToHex;
|
||||||
|
use serialize::json;
|
||||||
|
|
||||||
use util::hash::Sha256dHash;
|
use util::hash::Sha256dHash;
|
||||||
use blockdata::script::{mod, Script, ScriptError, read_scriptbool};
|
use blockdata::script::{mod, Script, ScriptError, TraceIteration, read_scriptbool};
|
||||||
use blockdata::utxoset::UtxoSet;
|
use blockdata::utxoset::UtxoSet;
|
||||||
use network::encodable::ConsensusEncodable;
|
use network::encodable::ConsensusEncodable;
|
||||||
use network::serialize::BitcoinHash;
|
use network::serialize::BitcoinHash;
|
||||||
|
@ -87,6 +89,10 @@ pub enum TransactionError {
|
||||||
OutputScriptFailure(ScriptError),
|
OutputScriptFailure(ScriptError),
|
||||||
/// P2SH serialized script failed (script error)
|
/// P2SH serialized script failed (script error)
|
||||||
P2shScriptFailure(ScriptError),
|
P2shScriptFailure(ScriptError),
|
||||||
|
/// P2SH serialized script ended with false at the top of the stack
|
||||||
|
P2shScriptReturnedFalse,
|
||||||
|
/// P2SH serialized script ended with nothing in the stack
|
||||||
|
P2shScriptReturnedEmptyStack,
|
||||||
/// Script ended with false at the top of the stack
|
/// Script ended with false at the top of the stack
|
||||||
ScriptReturnedFalse,
|
ScriptReturnedFalse,
|
||||||
/// Script ended with nothing in the stack
|
/// Script ended with nothing in the stack
|
||||||
|
@ -95,6 +101,43 @@ pub enum TransactionError {
|
||||||
InputNotFound(Sha256dHash, u32),
|
InputNotFound(Sha256dHash, u32),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl json::ToJson for TransactionError {
|
||||||
|
fn to_json(&self) -> json::Json {
|
||||||
|
json::String(self.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A trace of a script execution
|
||||||
|
#[deriving(PartialEq, Eq, Show, Clone)]
|
||||||
|
pub struct ScriptTrace {
|
||||||
|
initial_stack: Vec<String>,
|
||||||
|
iterations: Vec<TraceIteration>,
|
||||||
|
error: Option<ScriptError>
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A trace of a transaction input's script execution
|
||||||
|
#[deriving(PartialEq, Eq, Clone, Show)]
|
||||||
|
pub struct InputTrace {
|
||||||
|
sig_trace: ScriptTrace,
|
||||||
|
pubkey_trace: Option<ScriptTrace>,
|
||||||
|
p2sh_trace: Option<ScriptTrace>,
|
||||||
|
error: Option<TransactionError>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_json!(ScriptTrace, initial_stack, iterations, error)
|
||||||
|
impl_json!(InputTrace, sig_trace, pubkey_trace, p2sh_trace, error)
|
||||||
|
|
||||||
|
/// A trace of a transaction's execution
|
||||||
|
#[deriving(PartialEq, Eq, Clone, Show)]
|
||||||
|
pub struct TransactionTrace {
|
||||||
|
input_trace: Vec<InputTrace>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl json::ToJson for TransactionTrace {
|
||||||
|
fn to_json(&self) -> json::Json {
|
||||||
|
self.input_trace.to_json()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Transaction {
|
impl Transaction {
|
||||||
/// Check a transaction for validity
|
/// Check a transaction for validity
|
||||||
|
@ -107,7 +150,7 @@ impl Transaction {
|
||||||
let mut p2sh_script = Script::new();
|
let mut p2sh_script = Script::new();
|
||||||
|
|
||||||
let mut stack = Vec::with_capacity(6);
|
let mut stack = Vec::with_capacity(6);
|
||||||
match input.script_sig.evaluate(&mut stack, Some((self, n))) {
|
match input.script_sig.evaluate(&mut stack, Some((self, n)), None) {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(e) => { return Err(InputScriptFailure(e)); }
|
Err(e) => { return Err(InputScriptFailure(e)); }
|
||||||
}
|
}
|
||||||
|
@ -119,7 +162,7 @@ impl Transaction {
|
||||||
None => unreachable!()
|
None => unreachable!()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
match txo.script_pubkey.evaluate(&mut stack, Some((self, n))) {
|
match txo.script_pubkey.evaluate(&mut stack, Some((self, n)), None) {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(e) => { return Err(OutputScriptFailure(e)); }
|
Err(e) => { return Err(OutputScriptFailure(e)); }
|
||||||
}
|
}
|
||||||
|
@ -132,10 +175,18 @@ impl Transaction {
|
||||||
None => { return Err(ScriptReturnedEmptyStack); }
|
None => { return Err(ScriptReturnedEmptyStack); }
|
||||||
}
|
}
|
||||||
if txo.script_pubkey.is_p2sh() {
|
if txo.script_pubkey.is_p2sh() {
|
||||||
match p2sh_script.evaluate(&mut p2sh_stack, Some((self, n))) {
|
match p2sh_script.evaluate(&mut p2sh_stack, Some((self, n)), None) {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(e) => { return Err(P2shScriptFailure(e)); }
|
Err(e) => { return Err(P2shScriptFailure(e)); }
|
||||||
}
|
}
|
||||||
|
match p2sh_stack.pop() {
|
||||||
|
Some(v) => {
|
||||||
|
if !read_scriptbool(v.as_slice()) {
|
||||||
|
return Err(P2shScriptReturnedFalse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => { return Err(P2shScriptReturnedEmptyStack); }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => { return Err(InputNotFound(input.prev_hash, input.prev_index)); }
|
None => { return Err(InputNotFound(input.prev_hash, input.prev_index)); }
|
||||||
|
@ -143,6 +194,106 @@ impl Transaction {
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Produce a trace of a transaction's execution
|
||||||
|
pub fn trace(&self, utxoset: &UtxoSet) -> TransactionTrace {
|
||||||
|
let mut ret = TransactionTrace { input_trace: Vec::with_capacity(self.input.len()) };
|
||||||
|
for (n, input) in self.input.iter().enumerate() {
|
||||||
|
// Setup trace
|
||||||
|
let mut trace = InputTrace {
|
||||||
|
sig_trace: ScriptTrace {
|
||||||
|
initial_stack: vec![],
|
||||||
|
iterations: vec![],
|
||||||
|
error: None
|
||||||
|
},
|
||||||
|
pubkey_trace: None,
|
||||||
|
p2sh_trace: None,
|
||||||
|
error: None
|
||||||
|
};
|
||||||
|
// Run through the input
|
||||||
|
let txo = utxoset.get_utxo(input.prev_hash, input.prev_index);
|
||||||
|
match txo {
|
||||||
|
Some(txo) => {
|
||||||
|
let mut p2sh_stack = Vec::new();
|
||||||
|
let mut p2sh_script = Script::new();
|
||||||
|
|
||||||
|
let mut stack = Vec::with_capacity(6);
|
||||||
|
match input.script_sig.evaluate(&mut stack,
|
||||||
|
Some((self, n)),
|
||||||
|
Some(&mut trace.sig_trace.iterations)) {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(e) => {
|
||||||
|
trace.sig_trace.error = Some(e.clone());
|
||||||
|
trace.error = Some(InputScriptFailure(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if txo.script_pubkey.is_p2sh() && stack.len() > 0 {
|
||||||
|
p2sh_stack = stack.clone();
|
||||||
|
p2sh_script = match p2sh_stack.pop() {
|
||||||
|
Some(script::Owned(v)) => Script::from_vec(v),
|
||||||
|
Some(script::Slice(s)) => Script::from_vec(Vec::from_slice(s)),
|
||||||
|
None => unreachable!()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if trace.error.is_none() {
|
||||||
|
let mut pk_trace = ScriptTrace {
|
||||||
|
initial_stack: stack.iter().map(|elem| elem.as_slice().to_hex()).collect(),
|
||||||
|
iterations: vec![],
|
||||||
|
error: None
|
||||||
|
};
|
||||||
|
match txo.script_pubkey.evaluate(&mut stack,
|
||||||
|
Some((self, n)),
|
||||||
|
Some(&mut pk_trace.iterations)) {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(e) => {
|
||||||
|
pk_trace.error = Some(e.clone());
|
||||||
|
trace.error = Some(OutputScriptFailure(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match stack.pop() {
|
||||||
|
Some(v) => {
|
||||||
|
if !read_scriptbool(v.as_slice()) {
|
||||||
|
trace.error = Some(ScriptReturnedFalse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => { trace.error = Some(ScriptReturnedEmptyStack); }
|
||||||
|
}
|
||||||
|
trace.pubkey_trace = Some(pk_trace);
|
||||||
|
if trace.error.is_none() && txo.script_pubkey.is_p2sh() {
|
||||||
|
let mut p2sh_trace = ScriptTrace {
|
||||||
|
initial_stack: p2sh_stack.iter().map(|elem| elem.as_slice().to_hex()).collect(),
|
||||||
|
iterations: vec![],
|
||||||
|
error: None
|
||||||
|
};
|
||||||
|
match p2sh_script.evaluate(&mut p2sh_stack,
|
||||||
|
Some((self, n)),
|
||||||
|
Some(&mut p2sh_trace.iterations)) {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(e) => {
|
||||||
|
p2sh_trace.error = Some(e.clone());
|
||||||
|
trace.error = Some(P2shScriptFailure(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match p2sh_stack.pop() {
|
||||||
|
Some(v) => {
|
||||||
|
if !read_scriptbool(v.as_slice()) {
|
||||||
|
trace.error = Some(P2shScriptReturnedFalse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => { trace.error = Some(P2shScriptReturnedEmptyStack); }
|
||||||
|
}
|
||||||
|
trace.p2sh_trace = Some(p2sh_trace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
trace.error = Some(InputNotFound(input.prev_hash, input.prev_index));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ret.input_trace.push(trace);
|
||||||
|
}
|
||||||
|
ret
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BitcoinHash for Transaction {
|
impl BitcoinHash for Transaction {
|
||||||
|
|
Loading…
Reference in New Issue