Add transaction trace capability for debugging scripts

This commit is contained in:
Andrew Poelstra 2014-08-16 19:04:57 -07:00
parent 8d1a3e1f7c
commit 62dd2e7cee
3 changed files with 214 additions and 18 deletions

View File

@ -20,11 +20,13 @@
#![allow(non_camel_case_types)]
use serialize::json;
/// Submodule to handle -all- opcodes. Outside of this module we use
/// a restricted set where the push/return/noop/illegal opcodes have
/// a more convienient representation.
pub mod all {
use serialize::json;
// Heavy stick to translate between opcode types
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
pub static OP_FALSE: Opcode = OP_PUSHBYTES_0;
/// Number 1 is also TRUE
@ -624,7 +632,7 @@ pub mod all {
}
/// Broad categories of opcodes with similar behavior
#[deriving(PartialEq, Eq, Show)]
#[deriving(Clone, PartialEq, Eq, Show)]
pub enum OpcodeClass {
/// Pushes the given number onto the stack
PushNum(int),
@ -640,6 +648,12 @@ pub enum OpcodeClass {
Ordinary(Opcode)
}
impl json::ToJson for OpcodeClass {
fn to_json(&self) -> json::Json {
json::String(self.to_string())
}
}
macro_rules! ordinary_opcode(
($($op:ident),*) => (
#[repr(u8)]

View File

@ -98,6 +98,23 @@ pub enum ScriptError {
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,
/// specifically in the last 5 bits `byte & 31`
#[deriving(PartialEq, Eq, Show, Clone)]
@ -563,8 +580,10 @@ impl Script {
}
/// Evaluate the script, modifying the stack in place
pub fn evaluate<'a>(&'a self, stack: &mut Vec<MaybeOwned<'a>>, input_context: Option<(&Transaction, uint)>)
-> Result<(), ScriptError> {
pub fn evaluate<'a>(&'a self, stack: &mut Vec<MaybeOwned<'a>>,
input_context: Option<(&Transaction, uint)>,
mut trace: Option<&mut Vec<TraceIteration>>)
-> Result<(), ScriptError> {
let &Script(ref raw) = self;
let secp = Secp256k1::new();
@ -868,8 +887,20 @@ impl Script {
}
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(())
@ -1237,8 +1268,8 @@ mod test {
for (n, script) in script_pk.iter().enumerate() {
let mut stack = vec![];
assert_eq!(tx.input[n].script_sig.evaluate(&mut stack, Some((&tx, n))), Ok(()));
assert_eq!(script.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)), None), Ok(()));
assert!(stack.len() >= 1);
assert_eq!(read_scriptbool(stack.pop().unwrap().as_slice()), true);
}
@ -1299,10 +1330,10 @@ mod test {
#[test]
fn script_eval_simple() {
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);
assert!(script.evaluate(&mut vec![], None).is_err());
assert!(script.evaluate(&mut vec![], None, None).is_err());
}
#[test]
@ -1311,13 +1342,13 @@ mod test {
let script_pk: Script = deserialize(hex_pk.clone()).ok().expect("scriptpk");
// Should be able to check that the sig is there and pk correct
// before needing a transaction
assert_eq!(script_pk.evaluate(&mut vec![], None), Err(PopEmptyStack));
assert_eq!(script_pk.evaluate(&mut vec![Owned(vec![]), Owned(vec![])], None),
assert_eq!(script_pk.evaluate(&mut vec![], None, None), Err(PopEmptyStack));
assert_eq!(script_pk.evaluate(&mut vec![Owned(vec![]), Owned(vec![])], None, None),
Err(EqualVerifyFailed("e729dea4a3a81108e16376d1cc329c91db589994".to_string(),
"b472a266d0bd89c13706a4132ccfb16f7c3b9fcb".to_string())));
// 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![0]), 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, None), Err(NoTransaction));
}
#[test]
@ -1331,8 +1362,8 @@ mod test {
let script_pk: Script = deserialize(output_hex.clone()).ok().expect("scriptpk");
let mut stack = vec![];
assert_eq!(tx.input[0].script_sig.evaluate(&mut stack, None), Ok(()));
assert_eq!(script_pk.evaluate(&mut stack, Some((&tx, 0))), Ok(()));
assert_eq!(tx.input[0].script_sig.evaluate(&mut stack, None, None), Ok(()));
assert_eq!(script_pk.evaluate(&mut stack, Some((&tx, 0)), None), Ok(()));
assert_eq!(stack.len(), 1);
assert_eq!(read_scriptbool(stack.pop().unwrap().as_slice()), true);
}

View File

@ -24,9 +24,11 @@
//!
use std::default::Default;
use serialize::hex::ToHex;
use serialize::json;
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 network::encodable::ConsensusEncodable;
use network::serialize::BitcoinHash;
@ -87,6 +89,10 @@ pub enum TransactionError {
OutputScriptFailure(ScriptError),
/// P2SH serialized script failed (script error)
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
ScriptReturnedFalse,
/// Script ended with nothing in the stack
@ -95,6 +101,43 @@ pub enum TransactionError {
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 {
/// Check a transaction for validity
@ -107,7 +150,7 @@ impl Transaction {
let mut p2sh_script = Script::new();
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(_) => {}
Err(e) => { return Err(InputScriptFailure(e)); }
}
@ -119,7 +162,7 @@ impl Transaction {
None => unreachable!()
};
}
match txo.script_pubkey.evaluate(&mut stack, Some((self, n))) {
match txo.script_pubkey.evaluate(&mut stack, Some((self, n)), None) {
Ok(_) => {}
Err(e) => { return Err(OutputScriptFailure(e)); }
}
@ -132,10 +175,18 @@ impl Transaction {
None => { return Err(ScriptReturnedEmptyStack); }
}
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(_) => {}
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)); }
@ -143,6 +194,106 @@ impl Transaction {
}
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 {