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)]
|
||||
|
||||
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)]
|
||||
|
|
|
@ -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,7 +580,9 @@ 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)>)
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue