-
Notifications
You must be signed in to change notification settings - Fork 677
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Code Review: Smart contract VM 01/30/19 #908
Conversation
…epresentation of lisp s-exps.
…t. use a global context, which will work for our limited lisp dialect
… serializers for Secp256k1PublicKey. Also, update new location of BitcoinNetworkType
…)serializer methods for Secp256k1PublicKey
… trait to instantiate history rows from individual burn chain operations; implement helpers to insert and select rows of burn chain operations and convert them to and from database rows; implement basic DB tests
…tom) data type from a Sqlite3 Row
…o describe the order in which their column fields should be SELECT'ed
blockstack-vm/src/database.rs
Outdated
Ok(()) | ||
} | ||
|
||
fn insert_entry(&mut self, key: Value, value: Value) -> InterpreterResult { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we consider putting an upper bound on how many bytes an inserted entry can be?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd rather enforce it at the type level, as in, we should have a maximum byte limit for types, and the database should allow any entry that is a legal type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed re: making Value have a maximum byte size.
binary_comparison(args, &|x, y| x < y) | ||
} | ||
|
||
pub fn native_add(args: &[Value]) -> InterpreterResult { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens if args
has length 0 or 1? Also, will args
ever have length > 2? Same question for native_sub
, native_mul
, and native_div
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(+ 1 2 3)
evaluates to 6.
(+)
returns 0
For native_sub (-)
errors, (- 1)
returns the negation of it's single argument, (- 1 2 3)
return -4.
Multiply and divide behave similarly ((*)
returns 1, (/)
is an error)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it 👍
} | ||
}).collect(); | ||
|
||
let names = coerced_atoms?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is the ?
operator on its own line? Just curious.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The type inference didn't want to work otherwise -- if I move the ?
operator up and eliminate the Result<>
wrapper:
--> src/functions/define.rs:31:46
|
31 | arguments: arg_names.iter().map(|x| (*x).clone()).collect(),
| ^ consider giving this closure parameter a type
|
= note: type must be known at this point
"Illegal operation: attempted to re-define a value type.".to_string())), | ||
NamedParameter(ref _value) => Err(Error::InvalidArguments( | ||
"Illegal operation: attempted to re-define a named parameter.".to_string())), | ||
List(ref function_signature) => handle_define_function(&function_signature, &elements[2]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the user allowed to define functions that have reserved names?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right now, it will allow it, though they'll never get called, because lookup_function
tries the reserved names first, but it should be an error on define
Err(Error::InvalidArguments("(define ...) requires 2 arguments".to_string())) | ||
} else { | ||
match elements[1] { | ||
Atom(ref variable) => handle_define_variable(variable, &elements[2], env), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the user allowed to define atoms that collide with reserved names?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above -- we need to enforce name legality at define
and let
-- #914
blockstack-vm/src/functions/lists.rs
Outdated
use super::super::representations::SymbolicExpression::{AtomValue}; | ||
use super::super::{Context,Environment,eval,apply,lookup_function}; | ||
|
||
pub fn list_cons(args: &[Value]) -> InterpreterResult { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there an upper bound on how long a list can be?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right now, no, but I think we should let the "maximum value size" enforce this (as in, if we have a maximum value size of 1000 bytes, then you cannot have a list of longer than 1000/16 integers, e.g.)
blockstack-vm/src/lib.rs
Outdated
fn lookup_variable(name: &str, context: &Context, env: &Environment) -> InterpreterResult { | ||
// first off, are we talking about a constant? | ||
if name.starts_with(char::is_numeric) { | ||
match i128::from_str_radix(name, 10) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we want to support alternative radixes? Like, say, base-16?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe -- I was planning on lexing hexstrings into buffers, rather than ints. We could support both. Will create an issue for it.
blockstack-vm/src/lib.rs
Outdated
} | ||
} | ||
|
||
pub fn lookup_function<'a> (name: &str, env: &Environment)-> Result<CallableType<'a>, Error> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From this code, it looks like the user will not be able to change the runtime behavior of a reserved function by creating a function with the same name. However, I didn't see any code that prevents the user from doing so?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right -- reserved functions are used first, before a user defined function would be. However, we would want to generate a runtime error at the point of the (define)
(and also, when the static analyzer is implemented, a static error). I started an issue #914.
blockstack-vm/src/lib.rs
Outdated
*/ | ||
pub fn eval_all(expressions: &[SymbolicExpression], | ||
contract_db: Option<Box<database::ContractDatabase>>) -> InterpreterResult { | ||
let db_instance = match contract_db { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This feels weird to me. I think the caller should always supply a contract DB.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, I'll change that -- I'll move "blank db instantiation" to the execute
function, the intent of which is just running a program in a single, transient smart contract context. That's not just for our testing purposes, but as a simple path for a developer wanting to test a script.
blockstack-vm/src/parser/mod.rs
Outdated
result.push(value); | ||
} | ||
}, | ||
_ => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we want to accept non-printable characters with this match arm? Or should we just reject those as parse errors? I feel like our smart contract VM shouldn't accept non-printable characters (and should probably not use unicode) in order to ensure that the source code is unambiguous and doesn't have any homoglyph attacks or nefarious VT100 control codes embedded within it (which could manifest if you cat
'ed the smart contract code to stdout in a terminal, for example).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, definitely should cause parse errors. The next code review will have a lexer that enforces that. It'll be a much more traditional munch-lexer.
blockstack-vm/src/types.rs
Outdated
Value::Tuple(_a) => Err(Error::InvalidArguments("Cannot construct list of tuple types".to_string())), | ||
_ => { | ||
let mut base_type = TypeSignature::type_of(x); | ||
base_type.dimension += 1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this be a checked_add
? What happens if the dimension overflows?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, good catch, thanks
blockstack-vm/src/types.rs
Outdated
Ok(TypeSignature::new(atom_type, dimension)) | ||
} | ||
|
||
pub fn parse_type_str(x: &str) -> Result<TypeSignature, Error> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How long can this string get? Asking because if we're going to split
it, we might end up doing O(poly(n))
work for n
occurrences of -
. We might want to first count up the number of -
occurrences first, and if it's greater than 4, error out.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Won't split only ever do O(n)
work ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Depending on the allocator implementation, it could do O(n)
malloc()
s, which each could take O(log m)
time (for m
blocks allocated). Not sure what the actual implementation does, but I'm of the school of thought that we should prepare for the worst -- especially since the attacker controls the input.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, yes, avoiding non-constant object instantions is a good idea. Will just switch to using splitn
here.
This looks really cool! This looks good to me to merge to |
Yep, sounds good to me @jcnelson |
Codecov Report
@@ Coverage Diff @@
## develop #908 +/- ##
==========================================
Coverage ? 64.78%
==========================================
Files ? 44
Lines ? 5637
Branches ? 0
==========================================
Hits ? 3652
Misses ? 1985
Partials ? 0
Continue to review full report at Codecov.
|
I ripped |
Cool -- I'm going to merge this. I think this is also going to merge PR #910. |
This pull request has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. |
This is the first code review submission for the smart contract VM:
This code implements a basic LISP language, with support for ints, bools, tuples, and lists, for now. Recursion in this language is illegal. There is limited run-time type enforcement -- lists must all be single typed, tuples are strongly typed when used with data-map functions, native functions that expect ints, bools, etc. check their argument types.
At the moment, data-map functions like
fetch-entry!
, etc., all operate within a global context, set by theeval_all
function. You can see test programs in thetests/
directory.