Skip to content

Commit 8892203

Browse files
committed
Merge BlockstreamResearch#63: Add new witness parser
36800e2 test: Witness error paths (Christian Lewe) b67a8d1 refactor: Remove unnecessary map_err (Christian Lewe) 2e1b7ac rename: ReusedWitness -> WitnessReused (Christian Lewe) fc507fa feat: Add constructors to TypedValue (Christian Lewe) 52cbd3a test: Use witness data in example programs (Christian Lewe) 001e902 feat: Restore support for witness files (Christian Lewe) f70e8d0 feat: Convert ConstructNode to WitnessNode (Christian Lewe) b2e51f2 feat: Add ConstructNode (Christian Lewe) 8523e24 feat: Parse WitnessValues from JSON (Christian Lewe) fa4c92f feat: Compute typed value from const expression (Christian Lewe) b6e08e0 feat: Add witness values (Christian Lewe) 031beb0 feat: Create spans from strings (Christian Lewe) e470543 refactor: Error (Christian Lewe) 52d75cc refactor: RichError::Display (Christian Lewe) 78d3510 feat: Parse values from strings (Christian Lewe) Pull request description: Add a parser for the new witness JSON format: ```json { "name": { "value": "42", "type": "u8" } } ``` First we use serde_json to parse the JSON file. Then, we parse the `type` field as a `ResolvedType`. Finally, we parse the `value` field as a const expression that can be evaluated at compile time to a value, namely the witness value. Const expressions enable us to write witness files in the same way we write Simfony programs. Replace the reimplementation of `NamedConstructNode` with a simpler reimplementation of `ConstructNode` where witness nodes contain their name. Other nodes don't need to be named. This change is messy in terms of lines of code, but it should be quite mechanical. Implement a conversion from `NamedConstructNode` to `WitnessNode`, which enables pruning. I haven't tested how well rust-simplicity prunes compiled Simfony programs. We might need to push a followup PR to fix issues if we find them, but there is little we can do on the Simfony side, in my estimation. We could do with more unit tests, but I felt like pushing this now. ACKs for top commit: apoelstra: ACK 36800e2 Tree-SHA512: ffe2b778d7c9c75dd547bd6b5038d424dd932f0ce1870be25065ab316cb5147df33582090410922d96eda100a5bc13cd9f294128c16a43b35d587fb5005a2639
2 parents af3d729 + 36800e2 commit 8892203

16 files changed

+1040
-567
lines changed

example_progs/checksigfromstackverify.simf

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@
66

77
// In the future, we could use jets to compute a custom sighash
88
let pk: u256 = 0xf9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9;
9-
let msg: u256 = 0x0000000000000000000000000000000000000000000000000000000000000000;
10-
let sig: [u8; 64] = [0xe9, 0x07, 0x83, 0x1f, 0x80, 0x84, 0x8d, 0x10, 0x69, 0xa5, 0x37, 0x1b, 0x40, 0x24, 0x10, 0x36, 0x4b, 0xdf, 0x1c, 0x5f, 0x83, 0x07, 0xb0, 0x08, 0x4c, 0x55, 0xf1, 0xce, 0x2d, 0xca, 0x82, 0x15, 0x25, 0xf6, 0x6a, 0x4a, 0x85, 0xea, 0x8b, 0x71, 0xe4, 0x82, 0xa7, 0x4f, 0x38, 0x2d, 0x2c, 0xe5, 0xeb, 0xee, 0xe8, 0xfd, 0xb2, 0x17, 0x2f, 0x47, 0x7d, 0xf4, 0x90, 0x0d, 0x31, 0x05, 0x36, 0xc0];
9+
let msg: u256 = witness("msg");
10+
let sig: [u8; 64] = witness("sig");
1111
jet_bip_0340_verify(pk, msg, sig);
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"msg": {
3+
"value": "0x0000000000000000000000000000000000000000000000000000000000000000",
4+
"type": "u256"
5+
},
6+
"sig": {
7+
"value": "[0xe9, 0x07, 0x83, 0x1f, 0x80, 0x84, 0x8d, 0x10, 0x69, 0xa5, 0x37, 0x1b, 0x40, 0x24, 0x10, 0x36, 0x4b, 0xdf, 0x1c, 0x5f, 0x83, 0x07, 0xb0, 0x08, 0x4c, 0x55, 0xf1, 0xce, 0x2d, 0xca, 0x82, 0x15, 0x25, 0xf6, 0x6a, 0x4a, 0x85, 0xea, 0x8b, 0x71, 0xe4, 0x82, 0xa7, 0x4f, 0x38, 0x2d, 0x2c, 0xe5, 0xeb, 0xee, 0xe8, 0xfd, 0xb2, 0x17, 0x2f, 0x47, 0x7d, 0xf4, 0x90, 0x0d, 0x31, 0x05, 0x36, 0xc0]",
8+
"type": "[u8; 64]"
9+
}
10+
}

example_progs/empty.wit

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1 @@
1-
{
2-
3-
}
1+
{}

example_progs/sighash_all_anyprevoutanyscript.simf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,5 @@ let ctx: Ctx8 = jet_sha_256_ctx_8_add_32(ctx, jet_output_surjection_proofs_hash(
2323
let msg: u256 = jet_sha_256_ctx_8_finalize(ctx);
2424

2525
let pk: u256 = 0xf9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9;
26-
let sig: [u8; 64] = [0xc9, 0x39, 0xe3, 0x23, 0x44, 0x64, 0x2e, 0x3c, 0x22, 0x8b, 0x01, 0x62, 0x04, 0x18, 0x24, 0xaf, 0xe1, 0x94, 0x29, 0x74, 0x77, 0x18, 0x82, 0x55, 0xd2, 0x63, 0x79, 0x08, 0xa0, 0xd0, 0x0d, 0x1f, 0xdf, 0x59, 0x21, 0x4a, 0x30, 0x4d, 0xdf, 0x18, 0x0b, 0x1a, 0x07, 0x01, 0x9a, 0x60, 0xd8, 0x08, 0xd1, 0x81, 0x70, 0xfb, 0x77, 0xfd, 0x3b, 0x00, 0xec, 0xa7, 0x66, 0x67, 0xd7, 0xaa, 0x26, 0x8e];
26+
let sig: [u8; 64] = witness("sig");
2727
jet_bip_0340_verify(pk, msg, sig);
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"sig": {
3+
"value": "[0xc9, 0x39, 0xe3, 0x23, 0x44, 0x64, 0x2e, 0x3c, 0x22, 0x8b, 0x01, 0x62, 0x04, 0x18, 0x24, 0xaf, 0xe1, 0x94, 0x29, 0x74, 0x77, 0x18, 0x82, 0x55, 0xd2, 0x63, 0x79, 0x08, 0xa0, 0xd0, 0x0d, 0x1f, 0xdf, 0x59, 0x21, 0x4a, 0x30, 0x4d, 0xdf, 0x18, 0x0b, 0x1a, 0x07, 0x01, 0x9a, 0x60, 0xd8, 0x08, 0xd1, 0x81, 0x70, 0xfb, 0x77, 0xfd, 0x3b, 0x00, 0xec, 0xa7, 0x66, 0x67, 0xd7, 0xaa, 0x26, 0x8e]",
4+
"type": "[u8; 64]"
5+
}
6+
}

example_progs/sighash_none.simf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,5 @@ let ctx: Ctx8 = jet_sha_256_ctx_8_add_4(ctx, jet_current_index());
2121
let msg: u256 = jet_sha_256_ctx_8_finalize(ctx);
2222

2323
let pk: u256 = 0xf9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9;
24-
let sig: [u8; 64] = [0x34, 0x61, 0x52, 0x58, 0x3d, 0x5b, 0x60, 0xb9, 0x72, 0xbb, 0x4c, 0x03, 0xab, 0x67, 0x2e, 0x33, 0x94, 0x31, 0x06, 0x0e, 0x2b, 0x09, 0xc4, 0x47, 0xab, 0x98, 0x3c, 0x65, 0xda, 0xbc, 0x70, 0xa4, 0x59, 0xf3, 0xbe, 0xca, 0x77, 0x88, 0xbf, 0xa5, 0xda, 0x22, 0x1c, 0xf9, 0x92, 0x27, 0xb6, 0x5b, 0x4a, 0xd3, 0x82, 0x1a, 0x20, 0x45, 0xc8, 0x47, 0xee, 0x56, 0xd4, 0x8d, 0xf2, 0x6a, 0xee, 0x9c];
24+
let sig: [u8; 64] = witness("sig");
2525
jet_bip_0340_verify(pk, msg, sig);

example_progs/sighash_none.wit

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"sig": {
3+
"value": "[0x34, 0x61, 0x52, 0x58, 0x3d, 0x5b, 0x60, 0xb9, 0x72, 0xbb, 0x4c, 0x03, 0xab, 0x67, 0x2e, 0x33, 0x94, 0x31, 0x06, 0x0e, 0x2b, 0x09, 0xc4, 0x47, 0xab, 0x98, 0x3c, 0x65, 0xda, 0xbc, 0x70, 0xa4, 0x59, 0xf3, 0xbe, 0xca, 0x77, 0x88, 0xbf, 0xa5, 0xda, 0x22, 0x1c, 0xf9, 0x92, 0x27, 0xb6, 0x5b, 0x4a, 0xd3, 0x82, 0x1a, 0x20, 0x45, 0xc8, 0x47, 0xee, 0x56, 0xd4, 0x8d, 0xf2, 0x6a, 0xee, 0x9c]",
4+
"type": "[u8; 64]"
5+
}
6+
}

src/ast.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,7 @@ impl Scope {
349349
/// Witness names may be used at most throughout the entire program.
350350
pub fn insert_witness(&mut self, name: WitnessName, ty: ResolvedType) -> Result<(), Error> {
351351
match self.witnesses.entry(name.clone()) {
352-
Entry::Occupied(_) => Err(Error::ReusedWitness(name)),
352+
Entry::Occupied(_) => Err(Error::WitnessReused(name)),
353353
Entry::Vacant(entry) => {
354354
entry.insert(ty);
355355
Ok(())
@@ -522,7 +522,6 @@ impl AbstractSyntaxTree for SingleExpression {
522522
parse::SingleExpressionInner::Witness(name) => {
523523
scope
524524
.insert_witness(name.clone(), ty.clone())
525-
.map_err(|_| Error::ReusedWitness(name.clone()))
526525
.with_span(from)?;
527526
SingleExpressionInner::Witness(name.clone())
528527
}

src/compile.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! Compile the parsed ast into a simplicity program
22
33
use either::Either;
4-
use simplicity::node::{CoreConstructible as _, JetConstructible as _};
4+
use simplicity::node::{CoreConstructible as _, JetConstructible as _, WitnessConstructible as _};
55
use simplicity::{Cmr, FailEntropy};
66

77
use crate::array::{BTreeSlice, Partition};
@@ -10,7 +10,7 @@ use crate::ast::{
1010
SingleExpressionInner, Statement,
1111
};
1212
use crate::error::{Error, RichError, WithSpan};
13-
use crate::named::{ConstructExt, ProgExt};
13+
use crate::named::CoreExt;
1414
use crate::pattern::{BasePattern, Pattern};
1515
use crate::types::{StructuralType, TypeDeconstructible};
1616
use crate::value::StructuralValue;
@@ -208,7 +208,7 @@ impl SingleExpression {
208208
let value = StructuralValue::from(value);
209209
ProgNode::unit_comp(&ProgNode::const_word(value.into()))
210210
}
211-
SingleExpressionInner::Witness(name) => ProgNode::witness(name.as_inner().clone()),
211+
SingleExpressionInner::Witness(name) => ProgNode::witness(name.clone()),
212212
SingleExpressionInner::Variable(identifier) => scope
213213
.get(&BasePattern::Identifier(identifier.clone()))
214214
.ok_or(Error::UndefinedVariable(identifier.clone()))
@@ -247,7 +247,7 @@ impl SingleExpression {
247247
})
248248
})
249249
.map(|res| res.map(|array| ProgNode::injr(&array)))
250-
.unwrap_or_else(|| Ok(ProgNode::_false()))
250+
.unwrap_or_else(|| Ok(ProgNode::bit_false()))
251251
};
252252

253253
partition.fold(process, |res_a, res_b| {
@@ -268,7 +268,8 @@ impl SingleExpression {
268268
SingleExpressionInner::Match(match_) => match_.compile(scope)?,
269269
};
270270

271-
expr.arrow()
271+
expr.cached_data()
272+
.arrow()
272273
.target
273274
.unify(&StructuralType::from(self.ty()).to_unfinalized(), "")
274275
.map_err(|e| Error::CannotCompile(e.to_string()))

src/error.rs

Lines changed: 98 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ impl<T, E: Into<Error>> WithSpan<T> for Result<T, E> {
1818
}
1919
}
2020

21-
/// Helper trait to update `Result<A, RichError>` with the the affected source file.
21+
/// Helper trait to update `Result<A, RichError>` with the affected source file.
2222
pub trait WithFile<T> {
2323
/// Update the result with the affected source file.
2424
///
@@ -71,42 +71,48 @@ impl RichError {
7171

7272
impl fmt::Display for RichError {
7373
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74-
let start_line_index = self.span.start.line.get() - 1;
75-
let end_line_index = self.span.end.line.get() - 1;
76-
let line_num_width = self.span.end.line.get().to_string().len();
77-
writeln!(f, "{:width$} |", " ", width = line_num_width)?;
78-
79-
if let Some(ref file) = self.file {
80-
let mut lines = file.lines().skip(start_line_index).peekable();
81-
let start_line_len = lines.peek().map(|l| l.len()).unwrap_or(0);
82-
for (relative_line_index, line_str) in lines
83-
.take(end_line_index - start_line_index + 1)
84-
.enumerate()
85-
{
86-
let line_num = start_line_index + relative_line_index + 1;
87-
writeln!(f, "{line_num:line_num_width$} | {line_str}")?;
74+
match self.file {
75+
Some(ref file) if !file.is_empty() => {
76+
let start_line_index = self.span.start.line.get() - 1;
77+
let n_spanned_lines = self.span.end.line.get() - start_line_index;
78+
let line_num_width = self.span.end.line.get().to_string().len();
79+
writeln!(f, "{:width$} |", " ", width = line_num_width)?;
80+
81+
let mut lines = file.lines().skip(start_line_index).peekable();
82+
let start_line_len = lines.peek().map(|l| l.len()).unwrap_or(0);
83+
84+
for (relative_line_index, line_str) in lines.take(n_spanned_lines).enumerate() {
85+
let line_num = start_line_index + relative_line_index + 1;
86+
writeln!(f, "{line_num:line_num_width$} | {line_str}")?;
87+
}
88+
89+
let (underline_start, underline_length) = match self.span.is_multiline() {
90+
true => (0, start_line_len),
91+
false => (
92+
self.span.start.col.get(),
93+
self.span.end.col.get() - self.span.start.col.get(),
94+
),
95+
};
96+
write!(f, "{:width$} |", " ", width = line_num_width)?;
97+
write!(f, "{:width$}", " ", width = underline_start)?;
98+
write!(f, "{:^<width$} ", "", width = underline_length)?;
99+
write!(f, "{}", self.error)
100+
}
101+
_ => {
102+
write!(f, "{}", self.error)
88103
}
89-
let (underline_start, underline_length) = if self.span.is_multiline() {
90-
(0, start_line_len)
91-
} else {
92-
(
93-
self.span.start.col.get(),
94-
self.span.end.col.get() - self.span.start.col.get(),
95-
)
96-
};
97-
write!(f, "{:width$} |", " ", width = line_num_width)?;
98-
write!(f, "{:width$}", " ", width = underline_start)?;
99-
write!(f, "{:^<width$} ", "", width = underline_length)?;
100-
write!(f, "{}", self.error)
101-
} else {
102-
let start_line_num = self.span.end.line.get();
103-
write!(f, "{start_line_num} | {}", self.error)
104104
}
105105
}
106106
}
107107

108108
impl std::error::Error for RichError {}
109109

110+
impl From<RichError> for Error {
111+
fn from(error: RichError) -> Self {
112+
error.error
113+
}
114+
}
115+
110116
impl From<RichError> for String {
111117
fn from(error: RichError) -> Self {
112118
error.to_string()
@@ -148,11 +154,14 @@ pub enum Error {
148154
TypeValueMismatch(ResolvedType),
149155
InvalidNumberOfArguments(usize, usize),
150156
ExpressionTypeMismatch(ResolvedType, ResolvedType),
157+
ExpressionNotConstant,
151158
IntegerOutOfBounds(UIntType),
152159
UndefinedVariable(Identifier),
153160
UndefinedAlias(Identifier),
154161
VariableReuseInPattern(Identifier),
155-
ReusedWitness(WitnessName),
162+
WitnessReused(WitnessName),
163+
WitnessTypeMismatch(WitnessName, ResolvedType, ResolvedType),
164+
WitnessReassigned(WitnessName),
156165
}
157166

158167
#[rustfmt::skip]
@@ -203,6 +212,10 @@ impl fmt::Display for Error {
203212
f,
204213
"Expected expression of type `{expected}`, found type `{found}`"
205214
),
215+
Error::ExpressionNotConstant => write!(
216+
f,
217+
"Expression cannot be evaluated at compile time"
218+
),
206219
Error::IntegerOutOfBounds(ty) => write!(
207220
f,
208221
"Value is out of bounds for type `{ty}`"
@@ -219,10 +232,18 @@ impl fmt::Display for Error {
219232
f,
220233
"Variable `{identifier}` is used twice in the pattern"
221234
),
222-
Error::ReusedWitness(name) => write!(
235+
Error::WitnessReused(name) => write!(
223236
f,
224237
"Witness `{name}` has been used before somewhere in the program"
225238
),
239+
Error::WitnessTypeMismatch(name, declared, assigned) => write!(
240+
f,
241+
"Witness `{name}` was declared with type `{declared}` but its assigned value is of type `{assigned}`"
242+
),
243+
Error::WitnessReassigned(name) => write!(
244+
f,
245+
"Witness `{name}` has already been assigned a value"
246+
)
226247
}
227248
}
228249
}
@@ -231,8 +252,8 @@ impl std::error::Error for Error {}
231252

232253
impl Error {
233254
/// Update the error with the affected span.
234-
pub fn with_span<S: Into<Span>>(self, span: S) -> RichError {
235-
RichError::new(self, span.into())
255+
pub fn with_span(self, span: Span) -> RichError {
256+
RichError::new(self, span)
236257
}
237258
}
238259

@@ -267,8 +288,8 @@ mod tests {
267288
const FILE: &str = r#"let a1: List<u32, 5> = None;
268289
let x: u32 = Left(
269290
Right(0)
270-
);
271-
"#;
291+
);"#;
292+
const EMPTY_FILE: &str = "";
272293

273294
#[test]
274295
fn display_single_line() {
@@ -297,4 +318,45 @@ let x: u32 = Left(
297318
| ^^^^^^^^^^^^^^^^^^ Cannot parse: Expected value of type `u32`, got `Either<Either<_, u32>, _>`"#;
298319
assert_eq!(&expected[1..], &error.to_string());
299320
}
321+
322+
#[test]
323+
fn display_entire_file() {
324+
let error = Error::CannotParse("This span covers the entire file".to_string())
325+
.with_span(Span::from(FILE))
326+
.with_file(Arc::from(FILE));
327+
let expected = r#"
328+
|
329+
1 | let a1: List<u32, 5> = None;
330+
2 | let x: u32 = Left(
331+
3 | Right(0)
332+
4 | );
333+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Cannot parse: This span covers the entire file"#;
334+
assert_eq!(&expected[1..], &error.to_string());
335+
}
336+
337+
#[test]
338+
fn display_no_file() {
339+
let error = Error::CannotParse("This error has no file".to_string())
340+
.with_span(Span::from(EMPTY_FILE));
341+
let expected = "Cannot parse: This error has no file";
342+
assert_eq!(&expected, &error.to_string());
343+
344+
let error = Error::CannotParse("This error has no file".to_string())
345+
.with_span(Span::new(Position::new(1, 1), Position::new(2, 2)));
346+
assert_eq!(&expected, &error.to_string());
347+
}
348+
349+
#[test]
350+
fn display_empty_file() {
351+
let error = Error::CannotParse("This error has an empty file".to_string())
352+
.with_span(Span::from(EMPTY_FILE))
353+
.with_file(Arc::from(EMPTY_FILE));
354+
let expected = "Cannot parse: This error has an empty file";
355+
assert_eq!(&expected, &error.to_string());
356+
357+
let error = Error::CannotParse("This error has an empty file".to_string())
358+
.with_span(Span::new(Position::new(1, 1), Position::new(2, 2)))
359+
.with_file(Arc::from(EMPTY_FILE));
360+
assert_eq!(&expected, &error.to_string());
361+
}
300362
}

0 commit comments

Comments
 (0)