Skip to content

Commit a7b3513

Browse files
committed
preparing eip-3076
Signed-off-by: Aliaksei Misiukevich <[email protected]>
1 parent feecc5a commit a7b3513

File tree

65 files changed

+7022
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+7022
-0
lines changed

common/eip_3076/Cargo.toml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
[package]
2+
name = "eip_3076"
3+
version = "0.1.0"
4+
authors = ["Michael Sproul <[email protected]>", "pscott <[email protected]>"]
5+
edition.workspace = true
6+
autotests = false
7+
8+
[features]
9+
arbitrary-fuzz = ["types/arbitrary-fuzz"]
10+
portable = ["types/portable"]
11+
12+
[dependencies]
13+
arbitrary = { workspace = true, features = ["derive"] }
14+
ethereum_serde_utils = { workspace = true }
15+
filesystem = { workspace = true }
16+
r2d2 = { workspace = true }
17+
r2d2_sqlite = "0.21.0"
18+
rusqlite = { workspace = true }
19+
serde = { workspace = true }
20+
serde_json = { workspace = true }
21+
tempfile = { workspace = true }
22+
tracing = { workspace = true }
23+
types = { workspace = true }
24+
25+
[dev-dependencies]
26+
rayon = { workspace = true }
27+
28+
[[test]]
29+
name = "eip_3076_tests"
30+
path = "tests/main.rs"

common/eip_3076/Makefile

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
TESTS_TAG := v5.3.0
2+
GENERATE_DIR := generated-tests
3+
OUTPUT_DIR := interchange-tests
4+
TARBALL := $(OUTPUT_DIR)-$(TESTS_TAG).tar.gz
5+
ARCHIVE_URL := https://github.com/eth-clients/slashing-protection-interchange-tests/tarball/$(TESTS_TAG)
6+
7+
ifeq ($(OS),Windows_NT)
8+
ifeq (, $(shell where rm))
9+
rmfile = if exist $(1) (del /F /Q $(1))
10+
rmdir = if exist $(1) (rmdir /Q /S $(1))
11+
makedir = if not exist $(1) (mkdir $(1))
12+
else
13+
rmfile = rm -f $(1)
14+
rmdir = rm -rf $(1)
15+
makedir = mkdir -p $(1)
16+
endif
17+
else
18+
rmfile = rm -f $(1)
19+
rmdir = rm -rf $(1)
20+
makedir = mkdir -p $(1)
21+
endif
22+
23+
$(OUTPUT_DIR): $(TARBALL)
24+
$(call rmdir,$@)
25+
$(call makedir,$@)
26+
tar --strip-components=1 -xzf $^ -C $@
27+
28+
$(TARBALL):
29+
curl --fail -L -o $@ $(ARCHIVE_URL)
30+
31+
clean-test-files:
32+
$(call rmdir,$(OUTPUT_DIR))
33+
34+
clean-archives:
35+
$(call rmfile,$(TARBALL))
36+
37+
generate:
38+
$(call rmdir,$(GENERATE_DIR))
39+
cargo run --release --bin test_generator -- $(GENERATE_DIR)
40+
41+
clean: clean-test-files clean-archives
42+
43+
.PHONY: clean clean-archives clean-test-files generate
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
name: schema checks
2+
3+
on:
4+
push:
5+
pull_request:
6+
7+
jobs:
8+
check_schema:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- uses: actions/checkout@v2
12+
- uses: actions-rs/toolchain@v1
13+
with:
14+
toolchain: stable
15+
- run: make check
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*.tar.gz
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
VERSION:=$(shell git describe --tags)
2+
3+
check:
4+
cargo run --manifest-path schema_validator/Cargo.toml --release -- schema.json tests/generated
5+
6+
# Build a tarball for a release.
7+
# To make a new release: `git tag -s vX.Y.Z`
8+
release:
9+
tar -czvf eip-3076-tests-$(VERSION).tar.gz tests/
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
# Slashing Protection Interchange Tests (EIP-3076)
2+
3+
Tests for EIP-3076:
4+
5+
https://eips.ethereum.org/EIPS/eip-3076
6+
7+
Discussion:
8+
9+
https://ethereum-magicians.org/t/eip-3076-validator-client-interchange-format-slashing-protection/4883
10+
11+
## How to run
12+
13+
Each test directory contains an interchange file and some extra data about how to test it.
14+
15+
For example:
16+
17+
```json
18+
{
19+
"name": "single_validator_genesis_attestation",
20+
"genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000",
21+
"steps": [
22+
{
23+
"should_succeed": true,
24+
"contains_slashable_data": false,
25+
"interchange": {
26+
"metadata": {
27+
"interchange_format_version": "5",
28+
"genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000"
29+
},
30+
"data": [
31+
{
32+
"pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
33+
"signed_blocks": [],
34+
"signed_attestations": [
35+
{
36+
"source_epoch": "0",
37+
"target_epoch": "0"
38+
}
39+
]
40+
}
41+
]
42+
},
43+
"blocks": [],
44+
"attestations": [
45+
{
46+
"pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
47+
"source_epoch": "0",
48+
"target_epoch": "0",
49+
"should_succeed": false
50+
}
51+
]
52+
}
53+
]
54+
}
55+
```
56+
57+
To run a test, first initialize a new (empty) slashing protection database.
58+
59+
Then for each entry in `steps`, import the `interchange`, process the `blocks` and `attestations`,
60+
and continue to the next step.
61+
62+
Determine the test outcome according to the meanings of each of the fields,
63+
which are as follows:
64+
65+
* `name: string`: the name of the test-case, informational.
66+
* `genesis_validators_root: Root`: the genesis validators root to use when
67+
creating the empty slashing protection database, or to compare the import
68+
against.
69+
* `steps[i].should_succeed: bool`: whether the `steps[i].interchange` given is valid and should
70+
be imported successfully.
71+
* `steps[i].contains_slashable_data: bool`: whether the `steps[i].interchange` contains some
72+
slashable data with respect to itself or the existing contents of the database.
73+
* `steps[i].interchange: Interchange`: slashing protection interchange data as described
74+
by the spec.
75+
* `steps[i].blocks: [object]`: a list of block signings to be attempted **after**
76+
importing the `interchange`, detailed below.
77+
* `steps[i].attestations: [object]`: a list of attestation signings to be attempted **after**
78+
importing the `interchange`, detailed below.
79+
80+
Each block in `blocks` is structured as:
81+
82+
```json
83+
{
84+
"pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
85+
"slot": "1",
86+
"signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000",
87+
"should_succeed": false,
88+
"should_succeed_complete": true
89+
}
90+
```
91+
92+
Your test-runner should attempt to sign a block with `signing_root` at the given slot from the given
93+
`pubkey`. The `should_succeed` field describes whether this signing should be accepted (true) or
94+
rejected (false) _by a client using a minimal strategy_. Clients using a complete strategy should
95+
instead use the `should_succeed_complete` field which allows signing to succeed in more cases. If
96+
the block is signed successfully it should be incorporated into the slashing protection database.
97+
98+
Each attestation in `attestations` is structured as:
99+
100+
```json
101+
{
102+
"pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
103+
"source_epoch": "11",
104+
"target_epoch": "12",
105+
"signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000",
106+
"should_succeed": true,
107+
"should_succeed_complete": true
108+
}
109+
```
110+
111+
Similarly to above, your test-runner should attempt to sign an attestation with these parameters
112+
using the given `pubkey`, and succeed based on the value of
113+
`should_succeed`/`should_succeed_complete`. Again, implementations that use the _complete_ strategy
114+
should use `should_succeed_complete`. All implementations should incorporate signed attestations
115+
into the database.
116+
117+
Note that the top-level `genesis_validators_root` is not necessarily the same
118+
as the GVR contained in the interchange, to allow us to test the case where
119+
they are mismatched.
120+
121+
## Handling Slashable Data
122+
123+
The `contains_slashable_data` parameter is to be interpreted as follows:
124+
125+
- If `should_succeed` is false, then `contains_slashable_data` is irrelevant
126+
- If `contains_slashable_data` is false, then the given interchange **must** be imported
127+
successfully, and the given block/attestation checks must pass.
128+
- If `contains_slashable_data` is true, then implementations have the option to do one of two
129+
things:
130+
- Import the interchange successfully, working around the slashable data by minification
131+
or some other mechanism. If the import succeeds, all checks must pass and the test
132+
should continue to the next step.
133+
- Reject the interchange (or partially import it), in which case the block/attestation
134+
checks and all future steps should be ignored.
135+
136+
## Downloading the tests
137+
138+
The `tests` directory is released as a versioned `.tar.gz` on the [Releases](https://github.com/eth-clients/slashing-protection-interchange-tests/releases) page.
139+
140+
Alternatively, you could use a git submodule.
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
{
2+
"title": "Signing history",
3+
"description": "This schema provides a record of the blocks and attestations signed by a set of validators",
4+
"type": "object",
5+
"properties": {
6+
"metadata": {
7+
"type": "object",
8+
"properties": {
9+
"interchange_format_version": {
10+
"type": "string",
11+
"description": "The version of the interchange format that this document adheres to"
12+
},
13+
"genesis_validators_root": {
14+
"type": "string",
15+
"description": "Calculated at Genesis time; serves to uniquely identify the chain"
16+
}
17+
},
18+
"required": [
19+
"interchange_format_version",
20+
"genesis_validators_root"
21+
]
22+
},
23+
"data": {
24+
"type": "array",
25+
"items": [
26+
{
27+
"type": "object",
28+
"properties": {
29+
"pubkey": {
30+
"type": "string",
31+
"description": "The BLS public key of the validator (encoded as a 0x-prefixed hex string)"
32+
},
33+
"signed_blocks": {
34+
"type": "array",
35+
"items": [
36+
{
37+
"type": "object",
38+
"properties": {
39+
"slot": {
40+
"type": "string",
41+
"description": "The slot number of the block that was signed"
42+
},
43+
"signing_root": {
44+
"type": "string",
45+
"description": "The output of compute_signing_root(block, domain)"
46+
}
47+
},
48+
"required": [
49+
"slot"
50+
]
51+
}
52+
]
53+
},
54+
"signed_attestations": {
55+
"type": "array",
56+
"items": [
57+
{
58+
"type": "object",
59+
"properties": {
60+
"source_epoch": {
61+
"type": "string",
62+
"description": "The attestation.data.source.epoch of the signed attestation"
63+
},
64+
"target_epoch": {
65+
"type": "string",
66+
"description": "The attestation.data.target.epoch of the signed attestation"
67+
},
68+
"signing_root": {
69+
"type": "string",
70+
"description": "The output of compute_signing_root(attestation, domain)"
71+
}
72+
},
73+
"required": [
74+
"source_epoch",
75+
"target_epoch"
76+
]
77+
}
78+
]
79+
}
80+
},
81+
"required": [
82+
"pubkey",
83+
"signed_blocks",
84+
"signed_attestations"
85+
]
86+
}
87+
]
88+
}
89+
},
90+
"required": [
91+
"metadata",
92+
"data"
93+
]
94+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/target
2+
Cargo.lock
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[package]
2+
name = "schema_validator"
3+
version = "0.1.0"
4+
authors = ["Michael Sproul <[email protected]>"]
5+
edition = "2018"
6+
7+
[dependencies]
8+
jsonschema = { version = "0.3.1", default-features = false }
9+
serde_json = "1.0.59"
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
use jsonschema::{Draft, JSONSchema};
2+
use serde_json::Value;
3+
use std::env;
4+
use std::fs::{self, File};
5+
6+
fn main() {
7+
let args: Vec<_> = env::args().collect();
8+
9+
let schema_file = File::open(&args[1]).unwrap();
10+
let schema_value = serde_json::from_reader(schema_file).unwrap();
11+
let schema = JSONSchema::compile(&schema_value, Some(Draft::Draft7)).unwrap();
12+
13+
let tests_dir = &args[2];
14+
15+
let mut success_all = true;
16+
17+
fs::read_dir(tests_dir)
18+
.expect("read_dir succeeds on test directory")
19+
.map(|e| e.unwrap().path())
20+
.filter(|path| path.is_file())
21+
.for_each(|path| {
22+
let test_file = File::open(&path).unwrap();
23+
let test_value: Value = serde_json::from_reader(test_file).unwrap();
24+
let filename = path.file_name().unwrap().to_str().unwrap();
25+
26+
let steps = test_value.get("steps").unwrap();
27+
let mut success = true;
28+
29+
for (i, step) in steps.as_array().unwrap().iter().enumerate() {
30+
let interchange_value = step.get("interchange").unwrap();
31+
if let Err(errors) = schema.validate(interchange_value) {
32+
for e in errors {
33+
println!("{} .steps[{}].interchange, error: {:?}", filename, i, e);
34+
}
35+
success = false;
36+
success_all = false;
37+
}
38+
}
39+
if success {
40+
println!("{}, ok", filename);
41+
}
42+
});
43+
assert!(success_all, "one or more tests failed, see above");
44+
}

0 commit comments

Comments
 (0)