Skip to content

Commit

Permalink
implemented bitcoind tests and fixed errors discovered in testing pro…
Browse files Browse the repository at this point in the history
…cess
  • Loading branch information
tmrlvi committed Sep 17, 2023
1 parent e4320b0 commit 73a8727
Show file tree
Hide file tree
Showing 10 changed files with 5,663 additions and 64 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crypto/txscript/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ serde.workspace = true
criterion.workspace = true
smallvec.workspace = true
hex = "0.4"
serde_json = "1.0"

[[bench]]
name = "bench"
Expand Down
8 changes: 5 additions & 3 deletions crypto/txscript/errors/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@ pub enum TxScriptError {
MalformedPushSize(Vec<u8>),
#[error("opcode requires {0} bytes, but script only has {1} remaining")]
MalformedPush(usize, usize),
// We return error if stack entry is false
#[error("false stack entry at end of script execution")]
FalseStackEntry,
#[error("transaction input index {0} >= {1}")]
InvalidIndex(usize, usize),
#[error("combined stack size {0} > max allowed {1}")]
Expand All @@ -23,6 +20,7 @@ pub enum TxScriptError {
EmptyStack,
#[error("stack contains {0} unexpected items")]
CleanStack(usize),
// We return error if stack entry is false
#[error("false stack entry at end of script execution")]
EvalFalse,
#[error("script returned early")]
Expand Down Expand Up @@ -67,4 +65,8 @@ pub enum TxScriptError {
SignatureScriptNotPushOnly,
#[error("end of script reached in conditional execution")]
ErrUnbalancedConditional,
#[error("opcode requires at least {0} but stack has only {1}")]
InvalidStackOperation(usize, usize),
#[error("script of size {0} exceeded maximum allowed size of {1}")]
ScriptSize(usize, usize),
}
18 changes: 9 additions & 9 deletions crypto/txscript/src/data_stack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ impl DataStack for Stack {
Vec<u8>: OpcodeData<T>,
{
if self.len() < SIZE {
return Err(TxScriptError::EmptyStack);
return Err(TxScriptError::InvalidStackOperation(SIZE, self.len()));
}
Ok(<[T; SIZE]>::try_from(self.split_off(self.len() - SIZE).iter().map(|v| v.deserialize()).collect::<Result<Vec<T>, _>>()?)
.expect("Already exact item"))
Expand All @@ -193,7 +193,7 @@ impl DataStack for Stack {
Vec<u8>: OpcodeData<T>,
{
if self.len() < SIZE {
return Err(TxScriptError::EmptyStack);
return Err(TxScriptError::InvalidStackOperation(SIZE, self.len()));
}
Ok(<[T; SIZE]>::try_from(self[self.len() - SIZE..].iter().map(|v| v.deserialize()).collect::<Result<Vec<T>, _>>()?)
.expect("Already exact item"))
Expand All @@ -202,15 +202,15 @@ impl DataStack for Stack {
#[inline]
fn pop_raw<const SIZE: usize>(&mut self) -> Result<[Vec<u8>; SIZE], TxScriptError> {
if self.len() < SIZE {
return Err(TxScriptError::EmptyStack);
return Err(TxScriptError::InvalidStackOperation(SIZE, self.len()));
}
Ok(<[Vec<u8>; SIZE]>::try_from(self.split_off(self.len() - SIZE)).expect("Already exact item"))
}

#[inline]
fn peek_raw<const SIZE: usize>(&self) -> Result<[Vec<u8>; SIZE], TxScriptError> {
if self.len() < SIZE {
return Err(TxScriptError::EmptyStack);
return Err(TxScriptError::InvalidStackOperation(SIZE, self.len()));
}
Ok(<[Vec<u8>; SIZE]>::try_from(self[self.len() - SIZE..].to_vec()).expect("Already exact item"))
}
Expand All @@ -230,7 +230,7 @@ impl DataStack for Stack {
self.truncate(self.len() - SIZE);
Ok(())
}
false => Err(TxScriptError::EmptyStack),
false => Err(TxScriptError::InvalidStackOperation(SIZE, self.len())),
}
}

Expand All @@ -241,7 +241,7 @@ impl DataStack for Stack {
self.extend_from_within(self.len() - SIZE..);
Ok(())
}
false => Err(TxScriptError::EmptyStack),
false => Err(TxScriptError::InvalidStackOperation(SIZE, self.len())),
}
}

Expand All @@ -252,7 +252,7 @@ impl DataStack for Stack {
self.extend_from_within(self.len() - 2 * SIZE..self.len() - SIZE);
Ok(())
}
false => Err(TxScriptError::EmptyStack),
false => Err(TxScriptError::InvalidStackOperation(2 * SIZE, self.len())),
}
}

Expand All @@ -264,7 +264,7 @@ impl DataStack for Stack {
self.extend(drained);
Ok(())
}
false => Err(TxScriptError::EmptyStack),
false => Err(TxScriptError::InvalidStackOperation(3 * SIZE, self.len())),
}
}

Expand All @@ -276,7 +276,7 @@ impl DataStack for Stack {
self.extend(drained);
Ok(())
}
false => Err(TxScriptError::EmptyStack),
false => Err(TxScriptError::InvalidStackOperation(2 * SIZE, self.len())),
}
}
}
Expand Down
220 changes: 214 additions & 6 deletions crypto/txscript/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,14 @@ impl<'a, T: VerifiableTransaction> TxScriptEngine<'a, T> {
fn execute_script(&mut self, script: &[u8], verify_only_push: bool) -> Result<(), TxScriptError> {
let script_result = parse_script(script).try_for_each(|opcode| {
let opcode = opcode?;
if opcode.is_disabled() {
return Err(TxScriptError::OpcodeDisabled(format!("{:?}", opcode)));
}

if opcode.always_illegal() {
return Err(TxScriptError::OpcodeReserved(format!("{:?}", opcode)));
}

if verify_only_push && !opcode.is_push_opcode() {
return Err(TxScriptError::SignatureScriptNotPushOnly);
}
Expand All @@ -237,7 +245,7 @@ impl<'a, T: VerifiableTransaction> TxScriptEngine<'a, T> {
});

// Moving between scripts - we can't be inside an if
if !self.cond_stack.is_empty() {
if script_result.is_ok() && !self.cond_stack.is_empty() {
return Err(TxScriptError::ErrUnbalancedConditional);
}

Expand Down Expand Up @@ -270,10 +278,10 @@ impl<'a, T: VerifiableTransaction> TxScriptEngine<'a, T> {
}

if scripts.iter().all(|e| e.is_empty()) {
return Err(TxScriptError::FalseStackEntry);
return Err(TxScriptError::EvalFalse);
}
if scripts.iter().any(|e| e.len() > MAX_SCRIPTS_SIZE) {
return Err(TxScriptError::FalseStackEntry);
if let Some(s) = scripts.iter().find(|e| e.len() > MAX_SCRIPTS_SIZE) {
return Err(TxScriptError::ScriptSize(s.len(), MAX_SCRIPTS_SIZE));
}

let mut saved_stack: Option<Vec<Vec<u8>>> = None;
Expand Down Expand Up @@ -353,7 +361,7 @@ impl<'a, T: VerifiableTransaction> TxScriptEngine<'a, T> {

let pub_keys = match self.dstack.len() >= num_keys_usize {
true => self.dstack.split_off(self.dstack.len() - num_keys_usize),
false => return Err(TxScriptError::EmptyStack),
false => return Err(TxScriptError::InvalidStackOperation(num_keys_usize, self.dstack.len())),
};

let [num_sigs]: [i32; 1] = self.dstack.pop_items()?;
Expand All @@ -366,7 +374,7 @@ impl<'a, T: VerifiableTransaction> TxScriptEngine<'a, T> {

let signatures = match self.dstack.len() >= num_sigs {
true => self.dstack.split_off(self.dstack.len() - num_sigs),
false => return Err(TxScriptError::EmptyStack),
false => return Err(TxScriptError::InvalidStackOperation(num_sigs, self.dstack.len())),
};

let mut failed = false;
Expand Down Expand Up @@ -498,8 +506,10 @@ mod tests {
use std::iter::once;

use crate::opcodes::codes::{OpBlake2b, OpCheckSig, OpData1, OpData2, OpData32, OpDup, OpEqual, OpPushData1, OpTrue};
use crate::script_builder::ScriptBuilderError;

use super::*;
use kaspa_consensus_core::constants::MAX_TX_IN_SEQUENCE_NUM;
use kaspa_consensus_core::tx::{
PopulatedTransaction, ScriptPublicKey, Transaction, TransactionId, TransactionOutpoint, TransactionOutput,
};
Expand Down Expand Up @@ -904,4 +914,202 @@ mod tests {
);
}
}

// Bitcoind tests
use serde::Deserialize;
use std::fs::File;
use std::io::BufReader;
use std::path::Path;

#[derive(PartialEq, Eq, Debug, Clone)]
enum UnifiedError {
TxScriptError(TxScriptError),
ScriptBuilderError(ScriptBuilderError),
}

#[derive(PartialEq, Eq, Debug, Clone)]
struct TestError {
expected_result: String,
result: Result<(), UnifiedError>,
}

#[derive(Deserialize, Debug, Clone)]
#[serde(untagged)]
enum JsonTestRow {
Test(String, String, String, String),
TestWithComment(String, String, String, String, String),
Comment((String,)),
}

fn create_spending_transaction(sig_script: Vec<u8>, script_public_key: ScriptPublicKey) -> Transaction {
let coinbase = Transaction::new(
1,
vec![TransactionInput::new(
TransactionOutpoint::new(TransactionId::default(), 0xffffffffu32),
vec![0, 0],
MAX_TX_IN_SEQUENCE_NUM,
Default::default(),
)],
vec![TransactionOutput::new(0, script_public_key)],
Default::default(),
Default::default(),
Default::default(),
Default::default(),
);

Transaction::new(
1,
vec![TransactionInput::new(
TransactionOutpoint::new(coinbase.id(), 0u32),
sig_script,
MAX_TX_IN_SEQUENCE_NUM,
Default::default(),
)],
vec![TransactionOutput::new(0, Default::default())],
Default::default(),
Default::default(),
Default::default(),
Default::default(),
)
}

impl JsonTestRow {
fn test_row(&self) -> Result<(), TestError> {
// Parse test to objects
let (ss, spk, expected_result) = match self.clone() {
JsonTestRow::Test(ss, spk, _, expected_result) => (ss, spk, expected_result),
JsonTestRow::TestWithComment(ss, spk, _, expected_result, _) => (ss, spk, expected_result),
JsonTestRow::Comment(_) => {
return Ok(());
}
};

let result = Self::run_test(ss, spk);

match Self::result_name(result.clone()).contains(&expected_result.as_str()) {
true => Ok(()),
false => Err(TestError { expected_result, result }),
}
}

fn run_test(ss: String, spk: String) -> Result<(), UnifiedError> {
let script_sig = opcodes::parse_short_form(ss).map_err(UnifiedError::ScriptBuilderError)?;
let script_pub_key =
ScriptPublicKey::from_vec(0, opcodes::parse_short_form(spk).map_err(UnifiedError::ScriptBuilderError)?);

// Create transaction
let tx = create_spending_transaction(script_sig, script_pub_key.clone());
let entry = UtxoEntry::new(0, script_pub_key.clone(), 0, true);
let populated_tx = PopulatedTransaction::new(&tx, vec![entry]);

// Run transaction
let sig_cache = Cache::new(10_000);
let mut reused_values = SigHashReusedValues::new();
let mut vm = TxScriptEngine::from_transaction_input(
&populated_tx,
&populated_tx.tx().inputs[0],
0,
&populated_tx.entries[0],
&mut reused_values,
&sig_cache,
)
.map_err(UnifiedError::TxScriptError)?;
vm.execute().map_err(UnifiedError::TxScriptError)
}

/*
// Ensure there were no errors when the expected result is OK.
// At this point an error was expected so ensure the result of
// the execution matches it.
success := false
for _, code := range allowedErrorCodes {
if IsErrorCode(err, code) {
success = true
break
}
}
if !success {
var scriptErr Error
if ok := errors.As(err, &scriptErr); ok {
t.Errorf("%s: want error codes %v, got %v", name,
allowedErrorCodes, scriptErr.ErrorCode)
continue
}
t.Errorf("%s: want error codes %v, got err: %v (%T)",
name, allowedErrorCodes, err, err)
continue
}*/

fn result_name(result: Result<(), UnifiedError>) -> Vec<&'static str> {
match result {
Ok(_) => vec!["OK"],
Err(ue) => match ue {
UnifiedError::TxScriptError(e) => match e {
TxScriptError::NumberTooBig(_) => vec!["UNKNOWN_ERROR"],
TxScriptError::PubKeyFormat => vec!["PUBKEYFORMAT"],
TxScriptError::EvalFalse => vec!["EVAL_FALSE"],
TxScriptError::EmptyStack => {
vec!["EMPTY_STACK", "EVAL_FALSE", "UNBALANCED_CONDITIONAL", "INVALID_ALTSTACK_OPERATION"]
}
TxScriptError::NullFail => vec!["NULLFAIL"],
TxScriptError::SigLength(_) => vec!["NULLFAIL"],
//SIG_HIGH_S
TxScriptError::InvalidSigHashType(_) => vec!["SIG_HASHTYPE"],
TxScriptError::SignatureScriptNotPushOnly => vec!["SIG_PUSHONLY"],
TxScriptError::CleanStack(_) => vec!["CLEANSTACK"],
TxScriptError::OpcodeReserved(_) => vec!["BAD_OPCODE"],
TxScriptError::MalformedPush(_, _) => vec!["BAD_OPCODE"],
TxScriptError::InvalidOpcode(_) => vec!["BAD_OPCODE"],
TxScriptError::ErrUnbalancedConditional => vec!["UNBALANCED_CONDITIONAL"],
TxScriptError::InvalidState(s) if s == "condition stack empty" => vec!["UNBALANCED_CONDITIONAL"],
//ErrInvalidStackOperation
TxScriptError::EarlyReturn => vec!["OP_RETURN"],
TxScriptError::VerifyError => vec!["VERIFY", "EQUALVERIFY"],
TxScriptError::InvalidStackOperation(_, _) => vec!["INVALID_STACK_OPERATION", "INVALID_ALTSTACK_OPERATION"],
TxScriptError::InvalidState(s) if s == "pick at an invalid location" => vec!["INVALID_STACK_OPERATION"],
TxScriptError::InvalidState(s) if s == "roll at an invalid location" => vec!["INVALID_STACK_OPERATION"],
TxScriptError::OpcodeDisabled(_) => vec!["DISABLED_OPCODE"],
TxScriptError::ElementTooBig(_, _) => vec!["PUSH_SIZE"],
TxScriptError::TooManyOperations(_) => vec!["OP_COUNT"],
TxScriptError::StackSizeExceeded(_, _) => vec!["STACK_SIZE"],
TxScriptError::InvalidPubKeyCount(_) => vec!["PUBKEY_COUNT"],
TxScriptError::InvalidSignatureCount(_) => vec!["SIG_COUNT"],
TxScriptError::NotMinimalData(_) => vec!["MINIMALDATA", "UNKNOWN_ERROR"],
//ErrNegativeLockTime
TxScriptError::UnsatisfiedLockTime(_) => vec!["UNSATISFIED_LOCKTIME"],
TxScriptError::InvalidState(s) if s == "expected boolean" => vec!["MINIMALIF"],
TxScriptError::ScriptSize(_, _) => vec!["SCRIPT_SIZE"],
_ => vec![],
},
UnifiedError::ScriptBuilderError(e) => match e {
ScriptBuilderError::ElementExceedsMaxSize(_) => vec!["PUSH_SIZE"],
_ => vec![],
},
},
}
}
}

#[test]
fn test_bitcoind_tests() {
let file = File::open(Path::new(env!("CARGO_MANIFEST_DIR")).join("test-data").join("script_tests.json"))
.expect("Could not find test file");
let reader = BufReader::new(file);

// Read the JSON contents of the file as an instance of `User`.
let tests: Vec<JsonTestRow> = serde_json::from_reader(reader).expect("Failed Parsing {:?}");
let mut had_errors = 0;
let total_tests = tests.len();
for row in tests {
if let Err(error) = row.test_row() {
println!("Test: {:?} failed: {:?}", row.clone(), error);
had_errors += 1;
}
}
if had_errors > 0 {
panic!("{}/{} json tests failed", had_errors, total_tests)
}
}
}
Loading

0 comments on commit 73a8727

Please sign in to comment.