├── .air.toml
├── .dockerignore
├── .github
└── workflows
│ ├── full-test.yml
│ └── lint.yml
├── .gitignore
├── .golangci.yaml
├── .idea
├── misc.xml
├── modules.xml
├── runConfigurations
│ ├── node_1.xml
│ ├── node_2.xml
│ ├── node_3.xml
│ └── node_4.xml
├── ssv.iml
├── vcs.xml
└── watcherTasks.xml
├── Dockerfile
├── Makefile
├── PHASE_1_TESTNET.md
├── README.md
├── beacon
├── beacon.go
└── prysmgrpc
│ ├── aggrate.go
│ ├── attest.go
│ ├── attest_protect.go
│ ├── propose.go
│ └── prysmgrpc.go
├── cli
├── boot_node.go
├── cli.go
├── export_keys_from_mnemonic.go
├── flags
│ ├── boot_node.go
│ ├── export_keys_from_mnemonic.go
│ ├── node.go
│ └── threshold.go
├── generate_operator_keys.go
├── node.go
└── threshold.go
├── cmd
└── ssvnode
│ └── main.go
├── dev.Dockerfile
├── docker-compose.yaml
├── eth1
├── contract_event.go
├── eth1.go
└── goeth
│ ├── goETH.go
│ └── goETH_test.go
├── fixtures
└── reference_ssv.go
├── github
└── resources
│ ├── IBFTChart1.png
│ ├── IBFTChart2.png
│ ├── blox_logo.gif
│ ├── ethereum.gif
│ ├── port_permissions.gif
│ └── security_permission.png
├── go.mod
├── go.sum
├── ibft
├── IBFT.md
├── README.md
├── change_round.go
├── change_round_test.go
├── commit.go
├── commit_test.go
├── ibft.go
├── ibft_decided.go
├── ibft_decided_test.go
├── ibft_network.go
├── ibft_sequence.go
├── ibft_sequence_test.go
├── ibft_sync.go
├── ibft_sync_test.go
├── instance.go
├── instance_test.go
├── leader.go
├── leader
│ ├── README.md
│ ├── constant.go
│ ├── deterministic.go
│ ├── deterministic_test.go
│ └── selector.go
├── msgcont
│ ├── inmem
│ │ ├── inmem.go
│ │ └── inmem_test.go
│ └── msgcont.go
├── pipeline.go
├── pipeline
│ ├── auth
│ │ ├── msg_auth.go
│ │ ├── msg_auth_test.go
│ │ ├── msg_lambda.go
│ │ ├── msg_lambda_test.go
│ │ ├── msg_quorum.go
│ │ ├── msg_round.go
│ │ ├── msg_round_test.go
│ │ ├── msg_seq.go
│ │ ├── msg_seq_test.go
│ │ ├── msg_type_check.go
│ │ ├── msg_type_check_test.go
│ │ ├── msg_validator_pk.go
│ │ └── msg_validator_pk_test.go
│ ├── changeround
│ │ ├── add_message.go
│ │ ├── upon_full_quorum.go
│ │ ├── upon_partial_quorun.go
│ │ ├── validate.go
│ │ └── validate_test.go
│ ├── decided
│ │ └── prev_instance_decided.go
│ ├── pipeline.go
│ └── preprepare
│ │ ├── validate.go
│ │ └── validate_test.go
├── pre_prepare.go
├── pre_prepare_test.go
├── prepare.go
├── prepare_test.go
├── proto
│ ├── beacon.pb.go
│ ├── beacon.proto
│ ├── generate.go
│ ├── msgs.go
│ ├── msgs.pb.go
│ ├── msgs.proto
│ ├── msgs_test.go
│ ├── params.go
│ ├── params.pb.go
│ ├── params.proto
│ ├── params_test.go
│ ├── state.go
│ ├── state.pb.go
│ └── state.proto
├── spectesting
│ ├── algorithm_test.go
│ ├── nodes.go
│ ├── sign.go
│ ├── tests
│ │ ├── change_round_and_decide.go
│ │ ├── decide_different_value.go
│ │ ├── duplicate_messages.go
│ │ ├── non_justified_pre_prepare.go
│ │ ├── prepare_at_different_round.go
│ │ ├── prepare_change_round_and_decide.go
│ │ ├── spec_test.go
│ │ └── valid_simple_run.go
│ └── utils.go
├── sync
│ ├── history.go
│ ├── history_test.go
│ ├── incoming.go
│ └── test_utils.go
└── valcheck
│ ├── README.md
│ └── value_check.go
├── install.sh
├── internals
├── documentation
│ └── operator_getting_started.md
└── img
│ └── bloxstaking_header_image.png
├── network
├── generate.go
├── local
│ ├── local.go
│ ├── local_test.go
│ └── stream.go
├── msgqueue
│ ├── indexes.go
│ ├── message_queue.go
│ └── message_queue_test.go
├── network.go
├── network_msgs.pb.go
├── network_msgs.proto
└── p2p
│ ├── config.go
│ ├── discovery.go
│ ├── p2p.go
│ ├── p2p_decided.go
│ ├── p2p_ibft.go
│ ├── p2p_signatures.go
│ ├── p2p_stream.go
│ ├── p2p_sync.go
│ ├── p2p_sync_test.go
│ ├── p2p_test.go
│ └── test_utils.go
├── node
├── node.go
└── valcheck
│ └── attestation.go
├── phase_1_testnet
├── 919be6832b27567a7ee2792417dfe27f9c2263a763ca600ab395f74e05187435b16418d825da490fed83115e19365e50
│ ├── node1.zip
│ ├── node2.zip
│ ├── node3.zip
│ └── node4.zip
├── 99d8485216f6a37372a294d51f85d85bfca4b6c3201cbd389a1cdc62565f12f4ee5c491575fd85b6faa3b86eafedce57
│ ├── node1.zip
│ ├── node2.zip
│ ├── node3.zip
│ └── node4.zip
├── a106c0ab76a728fba808276e43f896a853fe4653860dc5a40d1b096cb95e9ffdf85975aabff19214f32a5cefbab54113
│ ├── node1.zip
│ ├── node2.zip
│ ├── node3.zip
│ └── node4.zip
├── aa96176258df64d1a83c9a1669b3f119065df40f7b7b6f1e22b3e3b1ddec7dc890698b11d0f5a293494d8813c6aa149c
│ ├── node1.zip
│ ├── node2.zip
│ ├── node3.zip
│ └── node4.zip
└── b3119aa267189ba9069cbb93900b70f0b837ed944334f92122d95b72eb1791ccd890c9396aea890f29b9e97b5ff7b13f
│ ├── node1.zip
│ ├── node2.zip
│ ├── node3.zip
│ └── node4.zip
├── pubsub
├── observer.go
├── subject.go
└── subject_test.go
├── scripts
└── protogen.sh
├── shared
└── params
│ ├── config.go
│ ├── config_utils_develop.go
│ └── testnet_config.go
├── slotqueue
└── slotqueue.go
├── storage
├── collections
│ ├── ibft_storage.go
│ ├── ibft_storage_test.go
│ ├── operator_storage.go
│ ├── operator_storage_test.go
│ ├── validator_storage.go
│ └── validator_storage_test.go
├── db_event.go
├── inmem
│ ├── inmem.go
│ └── inmem_test.go
├── kv
│ ├── badger.go
│ └── badger_test.go
└── storage.go
├── utils
├── boot_node
│ └── node.go
├── cliflag
│ └── cliflag.go
├── dataval
│ └── bytesval
│ │ └── validation.go
├── grpcex
│ └── grpcex.go
├── logex
│ └── zap.go
├── rsaencryption
│ ├── rsa_encryption.go
│ └── rsa_encryption_test.go
└── threshold
│ ├── reconstruct.go
│ ├── threshold.go
│ └── threshold_test.go
└── validator
├── duty_execution.go
├── duty_execution_test.go
├── signature.go
├── signature_test.go
├── test_utils.go
└── validator.go
/.air.toml:
--------------------------------------------------------------------------------
1 | # Config file for [Air](https://github.com/cosmtrek/air) in TOML format
2 |
3 | # Working directory
4 | # . or absolute path, please note that the directories following must be under root
5 | root = "."
6 | tmp_dir = "/bin/tmp"
7 |
8 | [build]
9 | # Just plain old shell command. You could use `make` as well.
10 | cmd = 'CGO_ENABLED=1 GOOS=linux go build -gcflags "all=-N -l" -tags blst_enabled -ldflags "-linkmode external -extldflags \"-static -lm\"" -o ${BUILD_PATH} ./cmd/ssvnode'
11 |
12 | # Binary file yields from `cmd`.
13 | bin = "${BUILD_PATH}/cmd/ssvnode"
14 |
15 | # Customize binary.
16 | # This is how you start to run your application. Since my application will works like CLI, so to run it, like to make a CLI call.
17 | full_bin="make BUILD_PATH=${BUILD_PATH} start-node"
18 |
19 | # This log file places in your tmp_dir.
20 | log = "air_errors.log"
21 | # Watch these filename extensions.
22 | include_ext = ["go", "yaml"]
23 | # Ignore these filename extensions or directories.
24 | exclude_dir = ["/bin"]
25 | # It's not necessary to trigger build each time file changes if it's too frequent.
26 | delay = 1000 # ms
27 |
28 | [log]
29 | # Show log time
30 | time = true
31 |
32 | [misc]
33 | # Delete tmp directory on exit
34 | clean_on_exit = true
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | bin
--------------------------------------------------------------------------------
/.github/workflows/full-test.yml:
--------------------------------------------------------------------------------
1 | name: full-test
2 |
3 | on:
4 | push:
5 | branches:
6 | - '**'
7 | pull_request:
8 | branches:
9 | - '**'
10 |
11 | workflow_dispatch:
12 |
13 | jobs:
14 | lint:
15 | runs-on: ubuntu-latest
16 |
17 | steps:
18 | - uses: actions/checkout@v2
19 |
20 | - name: Setup make
21 | run: sudo apt-get update && sudo apt-get install make
22 |
23 | - name: Run make test
24 | run: make full-test
25 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | name: Lint
2 |
3 | on:
4 | push:
5 | branches:
6 | - '**'
7 | pull_request:
8 | branches:
9 | - '**'
10 |
11 | workflow_dispatch:
12 |
13 | jobs:
14 | lint:
15 | runs-on: ubuntu-latest
16 |
17 | steps:
18 | - uses: actions/checkout@v2
19 |
20 | - name: Setup make
21 | run: sudo apt-get update && sudo apt-get install make
22 |
23 | - name: Run lint-prepare
24 | run: make lint-prepare
25 |
26 | - name: Run make lint
27 | run: make lint
28 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries for programs and plugins
2 | *.exe
3 | *.exe~
4 | *.dll
5 | *.so
6 | *.dylib
7 |
8 | # Test binary, built with `go test -c`
9 | *.test
10 |
11 | # Output of the go coverage tool, specifically when used with LiteIDE
12 | *.out
13 |
14 | # Dependency directories (remove the comment below to include it)
15 | vendor/
16 |
17 | .env*
18 |
19 | # User-specific stuff:
20 | .idea/**/workspace.xml
21 | .idea/**/tasks.xml
22 | .idea/dictionaries
23 |
24 | # Sensitive or high-churn files:
25 | .idea/**/dataSources/
26 | .idea/**/dataSources.ids
27 | .idea/**/dataSources.xml
28 | .idea/**/dataSources.local.xml
29 | .idea/**/sqlDataSources.xml
30 | .idea/**/dynamic.xml
31 | .idea/**/uiDesigner.xml
32 |
33 | # Gradle:
34 | .idea/**/gradle.xml
35 | .idea/**/libraries
36 |
37 |
38 | .DS_Store
39 |
40 | bin/
41 | /data/
42 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/node_1.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/node_2.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/node_3.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/node_4.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/ssv.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/watcherTasks.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | #
2 | # STEP 1: Prepare environment
3 | #
4 | FROM golang:1.15 AS preparer
5 |
6 | RUN apt-get update && \
7 | DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \
8 | curl git zip unzip wget g++ python gcc-aarch64-linux-gnu \
9 | && rm -rf /var/lib/apt/lists/*
10 |
11 | RUN go version
12 | RUN python --version
13 |
14 | WORKDIR /go/src/github.com/bloxapp/ssv/
15 | COPY go.mod .
16 | COPY go.sum .
17 | RUN go mod download
18 |
19 | #
20 | # STEP 2: Build executable binary
21 | #
22 | FROM preparer AS builder
23 |
24 | # Copy files and install app
25 | COPY . .
26 |
27 | RUN go get -d -v ./...
28 | RUN CGO_ENABLED=1 GOOS=linux go install -tags blst_enabled -ldflags "-linkmode external -extldflags \"-static -lm\"" ./cmd/ssvnode
29 |
30 | #
31 | # STEP 3: Prepare image to run the binary
32 | #
33 | FROM alpine:3.12 AS runner
34 |
35 | # Install ca-certificates, bash
36 | RUN apk -v --update add ca-certificates bash make bind-tools && \
37 | rm /var/cache/apk/*
38 |
39 | COPY --from=builder /go/bin/ssvnode /go/bin/ssvnode
40 | COPY ./Makefile .env* ./
41 |
42 |
43 | # Expose port for load balancing
44 | EXPOSE 5678 5000 4000/udp
45 |
46 | #ENTRYPOINT ["/go/bin/ssvnode"]
47 |
--------------------------------------------------------------------------------
/PHASE_1_TESTNET.md:
--------------------------------------------------------------------------------
1 | # Phase 1 Testnet deployment 
2 |
3 | #### Server Preparation
4 | ##### Create a server of your choice and expose on ports 12000 UDP and 13000 TCP
5 | * (AWS example at the bottom)
6 |
7 | ##### SHH permissions and login to server-
8 | ```
9 | $ cd ./{path to where the ssh downloaded}
10 |
11 | $ chmod 400 {ssh file name}
12 |
13 | $ ssh -i {ssh file name} ubuntu@{server public ip}
14 | ```
15 |
16 | #### .env file
17 |
18 | - Export all required params
19 | * Fill the fields according to the .env provided for you by Blox
20 | ```
21 | $ touch .env
22 |
23 | $ echo "CONSENSUS_TYPE=validation" >> .env
24 | $ echo "NETWORK=pyrmont" >> .env
25 | $ echo "BEACON_NODE_ADDR={ETH 2.0 node}" >> .env
26 | $ echo "VALIDATOR_PUBLIC_KEY={validator public key}" >> .env
27 | $ echo "NODE_ID={provided node index}" >> .env
28 | $ echo "SSV_PRIVATE_KEY={provided node private key}" >> .env
29 | $ echo "PUBKEY_NODE_1={provided node index 1 public key}" >> .env
30 | $ echo "PUBKEY_NODE_2={provided node index 2 public key}" >> .env
31 | $ echo "PUBKEY_NODE_3={provided node index 3 public key}" >> .env
32 | $ echo "PUBKEY_NODE_4={provided node index 4 public key}" >> .env
33 | ```
34 |
35 | ##### Download and run install.sh script
36 | ```
37 | $ sudo su
38 |
39 | $ wget https://raw.githubusercontent.com/ethereum/eth2-ssv/stage/install.sh
40 |
41 | $ chmod +x install.sh
42 |
43 | $ ./install.sh
44 | ```
45 |
46 | - Expected output - docker container id
47 |
48 | - You can watch logs using that cmd -
49 | ```
50 | $ docker logs ssv_node --follow
51 | ```
52 |
53 | ### Create EC2 server guides
54 | #### AWS -
55 | - In the search bar search for "ec2"
56 | - Launch new instance
57 | - choose "ubuntu server 20.04"
58 | - choose "t2.micro" (free tire)
59 | - skip to "security group" section
60 | - make sure you have 3 rules. UDP, TCP and SSH -
61 | 
62 | - after launch, add new key pair and download the ssh file
63 | - launch instance
64 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Migration notice
2 |
3 | The repository has migrated to https://github.com/bloxapp/ssv
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | # SSV - Secret Shared Validator
16 |
17 | SSV is a protocol for distributing an eth2 validator key between multiple operators governed by a consensus protocol ([Istanbul BFT](https://arxiv.org/pdf/2002.03613.pdf)).
18 |
19 | ## Getting started
20 | An SSV operator's getting started [documentation](./internals/documentation/operator_getting_started.md)
21 |
22 | ## Common commands
23 | ```bash
24 | # Build binary
25 | $ CGO_ENABLED=1 go build -o ./bin/ssvnode ./cmd/ssvnode/
26 |
27 | # Run local 4 node network (requires docker and a .env file as shown below)
28 | $ make docker-debug
29 |
30 | # Lint
31 | $ make lint-prepare
32 |
33 | $ make lint
34 |
35 | # Full test
36 | $ make full-test
37 |
38 | ```
39 |
40 | ## Splitting a key
41 | We split an eth2 BLS validator key into shares via Shamir-Secret-Sharing(SSS) to be used between the SSV nodes.
42 | ```bash
43 | # Extract Private keys from mnemonic (optional, skip if you have the public/private keys )
44 | $ ./bin/ssvnode export-keys --mnemonic={mnemonic} --index={keyIndex}
45 |
46 | # Generate threshold keys
47 | $ ./bin/ssvnode create-threshold --count {# of ssv nodes} --private-key {privateKey}
48 | ```
49 |
50 | ## Example .env file
51 | ```
52 | NETWORK=pyrmont
53 | DISCOVERY_TYPE=
54 | STORAGE_PATH=
55 | BOOT_NODE_EXTERNAL_IP=
56 | BOOT_NODE_PRIVATE_KEY=
57 | BEACON_NODE_ADDR=
58 | NODE_ID=
59 | VALIDATOR_PUBLIC_KEY=
60 | SSV_PRIVATE_KEY=
61 | PUBKEY_NODE_1=
62 | PUBKEY_NODE_2=
63 | PUBKEY_NODE_3=
64 | PUBKEY_NODE_4=
65 | ```
66 | For a 4 node SSV network, 4 .env.node.<1/2/3/4> files need to be created.
67 |
68 | ### Progress
69 | [X] Free standing, reference iBFT Go implementation\
70 | [X] SSV specific iBFT implementor\
71 | [X] Port POC code to Glang\
72 | [ ] Single standing instance running with Prysm's validator client\
73 | [X] Networking and discovery\
74 | [X] db, persistance and recovery\
75 | [ ] Between instance persistence (pevent starting a new instance if previous not decided)\
76 | [ ] Multi network support (being part of multiple SSV groups)\
77 | [ ] Aggregation and Proposal support\
78 | [X] Key sharing\
79 | [X] Deployment\
80 | [\\] Documentation\
81 | [X] Phase 1 testing\
82 | [ ] Audit
83 |
84 | ** X=done, \\=WIP
85 |
86 |
87 | ### Research (Deprecated)
88 | - Secret Shared Validators on Eth2
89 | - [Litepaper](https://medium.com/coinmonks/eth2-secret-shared-validators-85824df8cbc0)
90 | - iBTF
91 | - [Paper](https://arxiv.org/pdf/2002.03613.pdf)
92 | - [EIP650](https://github.com/ethereum/EIPs/issues/650)
93 | - [Liveness issues](https://github.com/ConsenSys/quorum/issues/305) - should have been addressed in the paper
94 | - [Consensys short description](https://docs.goquorum.consensys.net/en/stable/Concepts/Consensus/IBFT/)
95 | - POC
96 | - [SSV Python node](https://github.com/dankrad/python-ssv)
97 | - [iBFT Python](https://github.com/dankrad/python-ibft)
98 | - [Prysm adapted validator client](https://github.com/alonmuroch/prysm/tree/ssv)
99 | - Other implementations
100 | - [Consensys Quorum](https://github.com/ConsenSys/quorum)
101 | - [Besu Hyperledger](https://besu.hyperledger.org/en/stable/HowTo/Configure/Consensus-Protocols/IBFT/)
102 | - [code]( https://github.com/hyperledger/besu/tree/master/consensus/ibft)
103 | - DKG
104 | - [Blox's eth2 pools research](https://github.com/bloxapp/eth2-staking-pools-research)
105 | - [ETH DKG](https://github.com/PhilippSchindler/ethdkg)
106 |
--------------------------------------------------------------------------------
/beacon/beacon.go:
--------------------------------------------------------------------------------
1 | package beacon
2 |
3 | import (
4 | "context"
5 | "github.com/bloxapp/ssv/slotqueue"
6 | "github.com/herumi/bls-eth-go-binary/bls"
7 |
8 | ethpb "github.com/prysmaticlabs/ethereumapis/eth/v1alpha1"
9 | )
10 |
11 | // Role represents the validator role for a specific duty
12 | type Role int
13 |
14 | // String returns name of the role
15 | func (r Role) String() string {
16 | switch r {
17 | case RoleUnknown:
18 | return "UNKNOWN"
19 | case RoleAttester:
20 | return "ATTESTER"
21 | case RoleAggregator:
22 | return "AGGREGATOR"
23 | case RoleProposer:
24 | return "PROPOSER"
25 | default:
26 | return "UNDEFINED"
27 | }
28 | }
29 |
30 | // List of roles
31 | const (
32 | RoleUnknown = iota
33 | RoleAttester
34 | RoleAggregator
35 | RoleProposer
36 | )
37 |
38 | // Beacon represents the behavior of the beacon node connector
39 | type Beacon interface {
40 | // StreamDuties returns channel with duties stream
41 | StreamDuties(ctx context.Context, pubKey [][]byte) (<-chan *ethpb.DutiesResponse_Duty, error)
42 |
43 | // GetAttestationData returns attestation data by the given slot and committee index
44 | GetAttestationData(ctx context.Context, slot, committeeIndex uint64) (*ethpb.AttestationData, error)
45 |
46 | // SignAttestation signs the given attestation
47 | SignAttestation(ctx context.Context, data *ethpb.AttestationData, duty *ethpb.DutiesResponse_Duty, key *bls.SecretKey) (*ethpb.Attestation, []byte, error)
48 |
49 | // SubmitAttestation submits attestation fo the given slot using the given public key
50 | SubmitAttestation(ctx context.Context, attestation *ethpb.Attestation, validatorIndex uint64, key *bls.PublicKey) error
51 |
52 | // GetAggregationData returns aggregation data for the given slot and committee index
53 | GetAggregationData(ctx context.Context, duty slotqueue.Duty) (*ethpb.AggregateAttestationAndProof, error)
54 |
55 | // SignAggregation signs the given aggregation data
56 | SignAggregation(ctx context.Context, data *ethpb.AggregateAttestationAndProof, duty slotqueue.Duty) (*ethpb.SignedAggregateAttestationAndProof, error)
57 |
58 | // SubmitAggregation submits the given signed aggregation data
59 | SubmitAggregation(ctx context.Context, data *ethpb.SignedAggregateAttestationAndProof) error
60 |
61 | // GetProposalData returns proposal block for the given slot
62 | GetProposalData(ctx context.Context, slot uint64, duty slotqueue.Duty) (*ethpb.BeaconBlock, error)
63 |
64 | // SignProposal signs the given proposal block
65 | SignProposal(ctx context.Context, block *ethpb.BeaconBlock, duty slotqueue.Duty) (*ethpb.SignedBeaconBlock, error)
66 |
67 | // SubmitProposal submits the given signed block
68 | SubmitProposal(ctx context.Context, block *ethpb.SignedBeaconBlock) error
69 |
70 | // RolesAt slot returns the validator roles at the given slot. Returns nil if the
71 | // validator is known to not have a roles at the at slot. Returns UNKNOWN if the
72 | // validator assignments are unknown. Otherwise returns a valid validatorRole map.
73 | RolesAt(ctx context.Context, slot uint64, duty *ethpb.DutiesResponse_Duty, key *bls.PublicKey, shareKey *bls.SecretKey) ([]Role, error)
74 | }
75 |
--------------------------------------------------------------------------------
/beacon/prysmgrpc/attest_protect.go:
--------------------------------------------------------------------------------
1 | package prysmgrpc
2 |
3 | import (
4 | "context"
5 |
6 | ethpb "github.com/prysmaticlabs/ethereumapis/eth/v1alpha1"
7 | )
8 |
9 | // slashableAttestationCheck checks if an attestation is slashable by comparing it with the attesting
10 | // history for the given public key in our DB. If it is not, we then update the history
11 | // with new values and save it to the database.
12 | func (b *prysmGRPC) slashableAttestationCheck(ctx context.Context, indexedAtt *ethpb.IndexedAttestation, pubKey []byte, signingRoot [32]byte) error {
13 | // TODO: Implement
14 | return nil
15 | }
16 |
--------------------------------------------------------------------------------
/beacon/prysmgrpc/propose.go:
--------------------------------------------------------------------------------
1 | package prysmgrpc
2 |
3 | import (
4 | "context"
5 | "github.com/bloxapp/ssv/slotqueue"
6 |
7 | "github.com/pkg/errors"
8 | ethpb "github.com/prysmaticlabs/ethereumapis/eth/v1alpha1"
9 | "github.com/prysmaticlabs/prysm/beacon-chain/core/helpers"
10 | "github.com/prysmaticlabs/prysm/shared/params"
11 | )
12 |
13 | // GetProposalData implements Beacon interface
14 | func (b *prysmGRPC) GetProposalData(ctx context.Context, slot uint64, duty slotqueue.Duty) (*ethpb.BeaconBlock, error) {
15 | randaoReveal, err := b.signRandaoReveal(ctx, slot, duty)
16 | if err != nil {
17 | return nil, errors.Wrap(err, "failed to sign randao reveal")
18 | }
19 |
20 | block, err := b.validatorClient.GetBlock(ctx, ðpb.BlockRequest{
21 | Slot: slot,
22 | RandaoReveal: randaoReveal,
23 | Graffiti: b.graffiti,
24 | })
25 | if err != nil {
26 | return nil, errors.Wrap(err, "failed to get block")
27 | }
28 |
29 | return block, nil
30 | }
31 |
32 | // SignProposal implements Beacon interface
33 | func (b *prysmGRPC) SignProposal(ctx context.Context, block *ethpb.BeaconBlock, duty slotqueue.Duty) (*ethpb.SignedBeaconBlock, error) {
34 | // TODO: Check this
35 | /*if err := b.preBlockSignValidations(ctx, block); err != nil {
36 | return nil, errors.Wrapf(err, "failed block safety check for slot %d", block.Slot)
37 | }*/
38 |
39 | sig, err := b.signBlock(ctx, block, duty)
40 | if err != nil {
41 | return nil, errors.Wrap(err, "could not sign block")
42 | }
43 |
44 | // TODO: Check this
45 | /*if err := b.postBlockSignUpdate(ctx, block, domain); err != nil {
46 | return nil, errors.Wrapf(err, "failed post block signing validations for slot %d", blk.Block.Slot)
47 | }*/
48 |
49 | return ðpb.SignedBeaconBlock{
50 | Block: block,
51 | Signature: sig,
52 | }, nil
53 | }
54 |
55 | // SubmitProposal implements Beacon interface
56 | func (b *prysmGRPC) SubmitProposal(ctx context.Context, block *ethpb.SignedBeaconBlock) error {
57 | if _, err := b.validatorClient.ProposeBlock(ctx, block); err != nil {
58 | return errors.Wrap(err, "failed to propose block")
59 | }
60 |
61 | return nil
62 | }
63 |
64 | // signRandaoReveal signs randao reveal with randao domain and private key.
65 | func (b *prysmGRPC) signRandaoReveal(ctx context.Context, slot uint64, duty slotqueue.Duty) ([]byte, error) {
66 | domain, err := b.domainData(ctx, slot, params.BeaconConfig().DomainRandao[:])
67 | if err != nil {
68 | return nil, errors.Wrap(err, "failed to get domain data")
69 | }
70 |
71 | if domain == nil {
72 | return nil, errors.New("domain data is empty")
73 | }
74 |
75 | root, err := helpers.ComputeSigningRoot(b.network.EstimatedEpochAtSlot(slot), domain.SignatureDomain)
76 | if err != nil {
77 | return nil, err
78 | }
79 |
80 | return duty.ShareSK.SignByte(root[:]).Serialize(), nil
81 | }
82 |
83 | func (b *prysmGRPC) signBlock(ctx context.Context, block *ethpb.BeaconBlock, duty slotqueue.Duty) ([]byte, error) {
84 | domain, err := b.domainData(ctx, b.network.EstimatedEpochAtSlot(block.GetSlot()), params.BeaconConfig().DomainBeaconProposer[:])
85 | if err != nil {
86 | return nil, errors.Wrap(err, "failed to get domain data")
87 | }
88 |
89 | // TODO: A patch to randao signature!!
90 | root, err := helpers.ComputeSigningRoot(block, domain.SignatureDomain)
91 | if err != nil {
92 | return nil, errors.Wrap(err, "failed to compute signing root")
93 | }
94 |
95 | return duty.ShareSK.SignByte(root[:]).Serialize(), nil
96 | }
97 |
--------------------------------------------------------------------------------
/cli/boot_node.go:
--------------------------------------------------------------------------------
1 | package cli
2 |
3 | import (
4 | "github.com/bloxapp/ssv/cli/flags"
5 | bootnode "github.com/bloxapp/ssv/utils/boot_node"
6 |
7 | "github.com/spf13/cobra"
8 | "go.uber.org/zap"
9 | )
10 |
11 | // startNodeCmd is the command to start SSV node
12 | var startBootNodeCmd = &cobra.Command{
13 | Use: "start-boot-node",
14 | Short: "Starts boot node for discovery based ENR",
15 | Run: func(cmd *cobra.Command, args []string) {
16 | logger := Logger.Named("boot-node")
17 |
18 | privateKey, err := flags.GetBootNodePrivateKeyFlagValue(cmd)
19 | if err != nil {
20 | logger.Fatal("failed to get private key flag value", zap.Error(err))
21 | }
22 |
23 | externalIP, err := flags.GetExternalIPFlagValue(cmd)
24 | if err != nil {
25 | logger.Fatal("failed to get external ip flag value", zap.Error(err))
26 | }
27 |
28 | bootNode := bootnode.New(bootnode.Options{
29 | Logger: logger,
30 | PrivateKey: privateKey,
31 | ExternalIP: externalIP,
32 | })
33 |
34 | if err := bootNode.Start(cmd.Context()); err != nil {
35 | logger.Fatal("failed to start boot node", zap.Error(err))
36 | }
37 | },
38 | }
39 |
40 | func init() {
41 | flags.AddBootNodePrivateKeyFlag(startBootNodeCmd)
42 | flags.AddExternalIPFlag(startBootNodeCmd)
43 | RootCmd.AddCommand(startBootNodeCmd)
44 | }
45 |
--------------------------------------------------------------------------------
/cli/cli.go:
--------------------------------------------------------------------------------
1 | package cli
2 |
3 | import (
4 | "github.com/spf13/cobra"
5 | "go.uber.org/zap"
6 | )
7 |
8 | // Logger is the default logger
9 | var Logger *zap.Logger
10 |
11 | // RootCmd represents the root command of SSV CLI
12 | var RootCmd = &cobra.Command{
13 | Use: "ssv-cli",
14 | Long: `ssv-cli is a CLI for running SSV-related operations.`,
15 | }
16 |
17 | // Execute executes the root command
18 | func Execute(appName, version string) {
19 | RootCmd.Short = appName
20 | RootCmd.Version = version
21 |
22 | if err := RootCmd.Execute(); err != nil {
23 | Logger.Fatal("failed to execute root command", zap.Error(err))
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/cli/export_keys_from_mnemonic.go:
--------------------------------------------------------------------------------
1 | package cli
2 |
3 | import (
4 | "encoding/hex"
5 | "fmt"
6 |
7 | "github.com/bloxapp/eth2-key-manager/core"
8 | "github.com/spf13/cobra"
9 | util "github.com/wealdtech/go-eth2-util"
10 | "go.uber.org/zap"
11 |
12 | "github.com/bloxapp/ssv/cli/flags"
13 | )
14 |
15 | // exportKeysCmd is the command to export private/public keys based on given mnemonic
16 | var exportKeysCmd = &cobra.Command{
17 | Use: "export-keys",
18 | Short: "exports private/public keys based on given mnemonic",
19 | Run: func(cmd *cobra.Command, args []string) {
20 | mnemonicKey, err := flags.GetMnemonicFlagValue(cmd)
21 | if err != nil {
22 | Logger.Fatal("failed to get mnemonic key flag value", zap.Error(err))
23 | }
24 |
25 | index, err := flags.GetKeyIndexFlagValue(cmd)
26 | if err != nil {
27 | Logger.Fatal("failed to get key index flag value", zap.Error(err))
28 | }
29 |
30 | seed, err := core.SeedFromMnemonic(mnemonicKey, "")
31 | if err != nil {
32 | Logger.Fatal("failed to get seed from mnemonic", zap.Error(err))
33 | }
34 |
35 | fmt.Println("Seed:", hex.EncodeToString(seed))
36 | fmt.Println("Generating keys for index:", index)
37 | path := core.PyrmontNetwork.FullPath(fmt.Sprintf("/%d/0/0", index))
38 | key, err := util.PrivateKeyFromSeedAndPath(seed, path)
39 | if err != nil {
40 | Logger.Fatal("failed to get private key from seed", zap.Error(err))
41 | }
42 |
43 | fmt.Println("Private Key:", hex.EncodeToString(key.Marshal()))
44 | fmt.Println("Public Key:", hex.EncodeToString(key.PublicKey().Marshal()))
45 | },
46 | }
47 |
48 | func init() {
49 | flags.AddMnemonicFlag(exportKeysCmd)
50 | flags.AddKeyIndexFlag(exportKeysCmd)
51 |
52 | RootCmd.AddCommand(exportKeysCmd)
53 | }
54 |
--------------------------------------------------------------------------------
/cli/flags/boot_node.go:
--------------------------------------------------------------------------------
1 | package flags
2 |
3 | import (
4 | "github.com/spf13/cobra"
5 |
6 | "github.com/bloxapp/ssv/utils/cliflag"
7 | )
8 |
9 | // Flag names.
10 | const (
11 | bootNodePrivateKeyFlag = "private-key"
12 | bootNodeExternalIPFlag = "external-ip"
13 | )
14 |
15 | // AddBootNodePrivateKeyFlag adds the boot node private key flag to the command
16 | func AddBootNodePrivateKeyFlag(c *cobra.Command) {
17 | cliflag.AddPersistentStringFlag(c, bootNodePrivateKeyFlag, "", "boot node private", false)
18 | }
19 |
20 | // GetBootNodePrivateKeyFlagValue get the boot node private key flag to the command
21 | func GetBootNodePrivateKeyFlagValue(c *cobra.Command) (string, error) {
22 | return c.Flags().GetString(bootNodePrivateKeyFlag)
23 | }
24 |
25 | // GetExternalIPFlagValue gets the external ip flag from the command
26 | func GetExternalIPFlagValue(c *cobra.Command) (string, error) {
27 | return c.Flags().GetString(bootNodeExternalIPFlag)
28 | }
29 |
30 | // AddExternalIPFlag adds the external ip flag to the command
31 | func AddExternalIPFlag(c *cobra.Command) {
32 | cliflag.AddPersistentStringFlag(c, bootNodeExternalIPFlag, "", "external ip", false)
33 | }
34 |
--------------------------------------------------------------------------------
/cli/flags/export_keys_from_mnemonic.go:
--------------------------------------------------------------------------------
1 | package flags
2 |
3 | import (
4 | "github.com/spf13/cobra"
5 |
6 | "github.com/bloxapp/ssv/utils/cliflag"
7 | )
8 |
9 | // Flag names.
10 | const (
11 | mnemonicFlag = "mnemonic"
12 | indexFlag = "index"
13 | )
14 |
15 | // AddMnemonicFlag adds the mnemonic key flag to the command
16 | func AddMnemonicFlag(c *cobra.Command) {
17 | cliflag.AddPersistentStringFlag(c, mnemonicFlag, "", "24 letter mnemonic phrase", true)
18 | }
19 |
20 | // GetMnemonicFlagValue gets the mnemonic key flag from the command
21 | func GetMnemonicFlagValue(c *cobra.Command) (string, error) {
22 | return c.Flags().GetString(mnemonicFlag)
23 | }
24 |
25 | // AddKeyIndexFlag adds the key index flag to the command
26 | func AddKeyIndexFlag(c *cobra.Command) {
27 | cliflag.AddPersistentIntFlag(c, indexFlag, 0, "Index of the key to export from mnemonic", false)
28 | }
29 |
30 | // GetKeyIndexFlagValue gets the key index flag to the command
31 | func GetKeyIndexFlagValue(c *cobra.Command) (uint64, error) {
32 | return c.Flags().GetUint64(indexFlag)
33 | }
34 |
--------------------------------------------------------------------------------
/cli/flags/threshold.go:
--------------------------------------------------------------------------------
1 | package flags
2 |
3 | import (
4 | "github.com/spf13/cobra"
5 |
6 | "github.com/bloxapp/ssv/utils/cliflag"
7 | )
8 |
9 | // Flag names.
10 | const (
11 | privKeyFlag = "private-key"
12 | keysCountFlag = "count"
13 | )
14 |
15 | // AddPrivKeyFlag adds the private key flag to the command
16 | func AddPrivKeyFlag(c *cobra.Command) {
17 | cliflag.AddPersistentStringFlag(c, privKeyFlag, "", "Hex encoded private key", true)
18 | }
19 |
20 | // GetPrivKeyFlagValue gets the private key flag from the command
21 | func GetPrivKeyFlagValue(c *cobra.Command) (string, error) {
22 | return c.Flags().GetString(privKeyFlag)
23 | }
24 |
25 | // AddKeysCountFlag adds the keys count flag to the command
26 | func AddKeysCountFlag(c *cobra.Command) {
27 | cliflag.AddPersistentIntFlag(c, keysCountFlag, 4, "Count of threshold keys to be generated", false)
28 | }
29 |
30 | // GetKeysCountFlagValue gets the keys count flag from the command
31 | func GetKeysCountFlagValue(c *cobra.Command) (uint64, error) {
32 | return c.Flags().GetUint64(keysCountFlag)
33 | }
34 |
--------------------------------------------------------------------------------
/cli/generate_operator_keys.go:
--------------------------------------------------------------------------------
1 | package cli
2 |
3 | import (
4 | "github.com/bloxapp/ssv/utils/logex"
5 | "github.com/spf13/cobra"
6 | "go.uber.org/zap"
7 | "go.uber.org/zap/zapcore"
8 |
9 | "github.com/bloxapp/ssv/utils/rsaencryption"
10 | )
11 |
12 | // generateOperatorKeysCmd is the command to generate operator private/public keys
13 | var generateOperatorKeysCmd = &cobra.Command{
14 | Use: "generate-operator-keys",
15 | Short: "generates ssv operator keys",
16 | Run: func(cmd *cobra.Command, args []string) {
17 | logger := logex.Build(RootCmd.Short, zapcore.DebugLevel)
18 |
19 | pk, sk, err := rsaencryption.GenerateKeys()
20 | if err != nil{
21 | logger.Fatal("Failed to generate operator keys", zap.Error(err))
22 | }
23 | logger.Info("generated public key (base64)", zap.Any("pk", pk))
24 | logger.Info("generated private key (base64)", zap.Any("sk", sk))
25 | },
26 | }
27 |
28 | func init() {
29 | RootCmd.AddCommand(generateOperatorKeysCmd)
30 | }
31 |
--------------------------------------------------------------------------------
/cli/threshold.go:
--------------------------------------------------------------------------------
1 | package cli
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/herumi/bls-eth-go-binary/bls"
7 | "github.com/spf13/cobra"
8 | "go.uber.org/zap"
9 |
10 | "github.com/bloxapp/ssv/cli/flags"
11 | "github.com/bloxapp/ssv/utils/threshold"
12 | )
13 |
14 | // createThreshold is the command to create threshold based on the given private key
15 | var createThresholdCmd = &cobra.Command{
16 | Use: "create-threshold",
17 | Short: "Turns a private key into a threshold key",
18 | Run: func(cmd *cobra.Command, args []string) {
19 | privKey, err := flags.GetPrivKeyFlagValue(cmd)
20 | if err != nil {
21 | Logger.Fatal("failed to get private key flag value", zap.Error(err))
22 | }
23 |
24 | keysCount, err := flags.GetKeysCountFlagValue(cmd)
25 | if err != nil {
26 | Logger.Fatal("failed to get keys count flag value", zap.Error(err))
27 | }
28 |
29 | baseKey := &bls.SecretKey{}
30 | if err := baseKey.SetHexString(privKey); err != nil {
31 | Logger.Fatal("failed to set hex private key", zap.Error(err))
32 | }
33 |
34 | // https://github.com/ethereum/eth2-ssv/issues/22
35 | // currently support 4 nodes threshold is keysCount-1(3). need to align based open the issue to
36 | // support k(2f+1) and n (3f+1) and allow to pass it as flag
37 | privKeys, err := threshold.Create(baseKey.Serialize(), keysCount-1, keysCount)
38 | if err != nil {
39 | Logger.Fatal("failed to turn a private key into a threshold key", zap.Error(err))
40 | }
41 |
42 | // TODO: export to json file
43 | fmt.Println("Generating threshold keys for validator", baseKey.GetPublicKey().SerializeToHexStr())
44 | for i, pk := range privKeys {
45 | fmt.Println()
46 | fmt.Println("Public key", i, pk.GetPublicKey().SerializeToHexStr())
47 | fmt.Println("Private key", i, pk.SerializeToHexStr())
48 | }
49 | },
50 | }
51 |
52 | func init() {
53 | flags.AddPrivKeyFlag(createThresholdCmd)
54 | flags.AddKeysCountFlag(createThresholdCmd)
55 |
56 | RootCmd.AddCommand(createThresholdCmd)
57 | }
58 |
--------------------------------------------------------------------------------
/cmd/ssvnode/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/bloxapp/ssv/cli"
5 | )
6 |
7 | var (
8 | // AppName is the application name
9 | AppName = "SSV-CLI"
10 |
11 | // Version is the app version
12 | Version = "latest"
13 | )
14 |
15 | func main() {
16 | cli.Execute(AppName, Version)
17 | }
18 |
--------------------------------------------------------------------------------
/dev.Dockerfile:
--------------------------------------------------------------------------------
1 | #
2 | # STEP 1: Prepare environment
3 | #
4 | FROM golang:1.15 AS preparer
5 |
6 | RUN apt-get update && apt upgrade -y && \
7 | DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \
8 | make curl git zip unzip wget dnsutils g++ gcc-aarch64-linux-gnu \
9 | && rm -rf /var/lib/apt/lists/*
10 |
11 | RUN go version
12 |
13 | RUN go get github.com/go-delve/delve/cmd/dlv
14 | RUN go get -u github.com/cosmtrek/air
15 |
16 | WORKDIR /go/src/github.com/bloxapp/ssv/
17 | COPY go.mod .
18 | COPY go.sum .
19 | RUN go mod download
20 |
21 | COPY . .
22 |
23 | CMD air
24 |
--------------------------------------------------------------------------------
/docker-compose.yaml:
--------------------------------------------------------------------------------
1 | version: '3.5'
2 |
3 | x-base:
4 | &default-base
5 | build:
6 | context: .
7 | dockerfile: Dockerfile
8 | image: ssvnode:latest
9 | command: make BUILD_PATH=/go/bin/ssvnode start-node
10 | network_mode: host
11 | # networks:
12 | # - bloxapp-docker
13 | restart: always
14 |
15 | x-base-dev:
16 | &default-dev
17 | << : *default-base
18 | image: ssvnode-debug:latest
19 | build:
20 | context: .
21 | dockerfile: dev.Dockerfile
22 | command: air
23 | security_opt:
24 | - "seccomp:unconfined"
25 | cap_add:
26 | - SYS_PTRACE
27 | volumes:
28 | - ./:/go/src/github.com/bloxapp/ssv
29 |
30 | services:
31 | ssv-node-1:
32 | <<: *default-base
33 | container_name: ssv-node-1
34 | env_file:
35 | - .env.node.1
36 |
37 | ssv-node-2:
38 | <<: *default-base
39 | container_name: ssv-node-2
40 | env_file:
41 | - .env.node.2
42 |
43 | ssv-node-3:
44 | <<: *default-base
45 | container_name: ssv-node-3
46 | env_file:
47 | - .env.node.3
48 |
49 | ssv-node-4:
50 | <<: *default-base
51 | container_name: ssv-node-4
52 | env_file:
53 | - .env.node.4
54 |
55 |
56 | ssv-node-1-dev:
57 | << : *default-dev
58 | container_name: ssv-node-1-dev
59 | # ports:
60 | # - 40005:40005
61 | env_file:
62 | - .env.node.1
63 | environment:
64 | BUILD_PATH: /bin/tmp/ssv
65 | DEBUG_PORT: 40005
66 |
67 | ssv-node-2-dev:
68 | << : *default-dev
69 | container_name: ssv-node-2-dev
70 | # ports:
71 | # - 40006:40006
72 | env_file:
73 | - .env.node.2
74 | environment:
75 | BUILD_PATH: /bin/tmp/ssv
76 | DEBUG_PORT: 40006
77 |
78 | ssv-node-3-dev:
79 | << : *default-dev
80 | container_name: ssv-node-3-dev
81 | # ports:
82 | # - 40007:40007
83 | env_file:
84 | - .env.node.3
85 | environment:
86 | BUILD_PATH: /bin/tmp/ssv
87 | DEBUG_PORT: 40007
88 |
89 | ssv-node-4-dev:
90 | << : *default-dev
91 | container_name: ssv-node-4-dev
92 | # ports:
93 | # - 40008:40008
94 | env_file:
95 | - .env.node.4
96 | environment:
97 | BUILD_PATH: /bin/tmp/ssv
98 | DEBUG_PORT: 40008
99 |
100 | #networks:
101 | # bloxapp-docker:
102 | # driver: bridge
103 | # name: bloxapp-docker
--------------------------------------------------------------------------------
/eth1/contract_event.go:
--------------------------------------------------------------------------------
1 | package eth1
2 |
3 | import (
4 | "math/big"
5 |
6 | "github.com/ethereum/go-ethereum/common"
7 | "github.com/ethereum/go-ethereum/core/types"
8 |
9 | "github.com/bloxapp/ssv/pubsub"
10 | )
11 |
12 | // ContractEvent struct is an implementation of BaseSubject that notify about an event from the smart contract to all the registered observers
13 | type ContractEvent struct {
14 | pubsub.BaseSubject
15 | Log types.Log
16 | Data interface{}
17 | }
18 |
19 | // Oess struct stands for operator encrypted secret share
20 | type Oess struct {
21 | Index *big.Int
22 | OperatorPublicKey []byte
23 | SharedPublicKey []byte
24 | EncryptedKey []byte
25 | }
26 |
27 | // ValidatorAddedEvent struct represents event received by the smart contract
28 | type ValidatorAddedEvent struct {
29 | PublicKey []byte
30 | OwnerAddress common.Address
31 | OessList []Oess
32 | }
33 |
34 | // OperatorAddedEvent struct represents event received by the smart contract
35 | type OperatorAddedEvent struct {
36 | Name string
37 | Pubkey []byte
38 | PaymentAddress common.Address
39 | }
40 |
41 | // NewContractEvent create new event subject
42 | func NewContractEvent(name string) *ContractEvent {
43 | return &ContractEvent{
44 | BaseSubject: pubsub.BaseSubject{
45 | Name: name,
46 | },
47 | }
48 | }
49 |
50 | // NotifyAll notify all subscribe observables
51 | func (e *ContractEvent) NotifyAll() {
52 | for _, observer := range e.ObserverList {
53 | observer.InformObserver(e.Data)
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/eth1/eth1.go:
--------------------------------------------------------------------------------
1 | package eth1
2 |
3 | // Eth1 represents the behavior of the eth1 node connector
4 | type Eth1 interface {
5 | GetContractEvent() *ContractEvent
6 | }
7 |
--------------------------------------------------------------------------------
/fixtures/reference_ssv.go:
--------------------------------------------------------------------------------
1 | package fixtures
2 |
3 | import "encoding/hex"
4 |
5 | func _byteArray(input string) []byte {
6 | res, _ := hex.DecodeString(input)
7 | return res
8 | }
9 |
10 | var (
11 | // RefSk is a reference testing private key
12 | RefSk = _byteArray("2c083f2c8fc923fa2bd32a70ab72b4b46247e8c1f347adc30b2f8036a355086c")
13 | // RefPk is the PK of RefSk
14 | RefPk = _byteArray("a9cf360aa15fb1d1d30ee2b578dc5884823c19661886ae8b892775ccb3bd96b7d7345569a2aa0b14e4d015c54a6a0c54")
15 |
16 | // RefSplitShares is RefSk split into 4 shares
17 | RefSplitShares = [][]byte{ // sk split to 4: 2c083f2c8fc923fa2bd32a70ab72b4b46247e8c1f347adc30b2f8036a355086c
18 | _byteArray("1a1b411e54ebb0973dc0f133c8b192cc4320fd464cbdcfe3be38b77f821f30bc"),
19 | _byteArray("6a93d37661cfe9cbaff9f051f2dd1d1995905932375e09357be1a50f7f4de323"),
20 | _byteArray("3596a78e633ad5071c0a77bb16b1a391b21ab47fb32ba1ba442a48e89ae11f9f"),
21 | _byteArray("62ff0c0cac676cd9e866377f4772d63f403b5734c02351701712a308d4d8e632"),
22 | }
23 | // RefSplitSharesPubKeys is the PK for RefSplitShares
24 | RefSplitSharesPubKeys = [][]byte{
25 | _byteArray("84d90424a5511e3741ac3c99ee1dba39007a290410e805049d0ae40cde74191d785d7848f08b2dfb99b742ebfe846e3b"),
26 | _byteArray("b6ac738a09a6b7f3fb4f85bac26d8965f6329d431f484e8b43633f7b7e9afce0085bb592ea90df6176b2f2bd97dfd7f3"),
27 | _byteArray("a261c25548320f1aabfc2aac5da3737a0b8bbc992a5f4f937259d22d39fbf6ebf8ec561720de3a04f661c9772fcace96"),
28 | _byteArray("85dd2d89a3e320995507c46320f371dc85eb16f349d1c56d71b58663b5b6a5fd390fcf41cf9098471eb5437fd95be1ac"),
29 | }
30 | )
31 |
--------------------------------------------------------------------------------
/github/resources/IBFTChart1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ethereum/ssv/bd3ccbed4786b2be6842a1ff0cc3cc3507c382e1/github/resources/IBFTChart1.png
--------------------------------------------------------------------------------
/github/resources/IBFTChart2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ethereum/ssv/bd3ccbed4786b2be6842a1ff0cc3cc3507c382e1/github/resources/IBFTChart2.png
--------------------------------------------------------------------------------
/github/resources/blox_logo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ethereum/ssv/bd3ccbed4786b2be6842a1ff0cc3cc3507c382e1/github/resources/blox_logo.gif
--------------------------------------------------------------------------------
/github/resources/ethereum.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ethereum/ssv/bd3ccbed4786b2be6842a1ff0cc3cc3507c382e1/github/resources/ethereum.gif
--------------------------------------------------------------------------------
/github/resources/port_permissions.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ethereum/ssv/bd3ccbed4786b2be6842a1ff0cc3cc3507c382e1/github/resources/port_permissions.gif
--------------------------------------------------------------------------------
/github/resources/security_permission.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ethereum/ssv/bd3ccbed4786b2be6842a1ff0cc3cc3507c382e1/github/resources/security_permission.png
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/bloxapp/ssv
2 |
3 | go 1.15
4 |
5 | require (
6 | github.com/bloxapp/eth2-key-manager v1.0.4
7 | github.com/dgraph-io/badger v1.6.1
8 | github.com/dgraph-io/badger/v3 v3.2011.1
9 | github.com/ethereum/go-ethereum v1.9.25
10 | github.com/gogo/protobuf v1.3.2
11 | github.com/golang/protobuf v1.4.3
12 | github.com/grpc-ecosystem/go-grpc-middleware v1.2.2
13 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0
14 | github.com/herumi/bls-eth-go-binary v0.0.0-20210102080045-a126987eca2b
15 | github.com/ipfs/go-ipfs-addr v0.0.1
16 | github.com/libp2p/go-libp2p v0.12.1-0.20201208224947-3155ff3089c0
17 | github.com/libp2p/go-libp2p-core v0.7.0
18 | github.com/libp2p/go-libp2p-noise v0.1.2
19 | github.com/libp2p/go-libp2p-pubsub v0.4.0
20 | github.com/libp2p/go-tcp-transport v0.2.1
21 | github.com/multiformats/go-multiaddr v0.3.1
22 | github.com/patrickmn/go-cache v2.1.0+incompatible
23 | github.com/pborman/uuid v1.2.1
24 | github.com/pkg/errors v0.9.1
25 | github.com/prysmaticlabs/ethereumapis v0.0.0-20210118163152-3569d231d255
26 | github.com/prysmaticlabs/go-bitfield v0.0.0-20210107162333-9e9cf77d4921
27 | github.com/prysmaticlabs/prysm v1.1.0
28 | github.com/sirupsen/logrus v1.7.0 // indirect
29 | github.com/spf13/cobra v1.1.1
30 | github.com/stretchr/testify v1.6.1
31 | github.com/wealdtech/go-eth2-util v1.6.2
32 | go.opencensus.io v0.22.5
33 | go.uber.org/zap v1.16.0
34 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c // indirect
35 | google.golang.org/grpc v1.33.1
36 | )
37 |
38 | replace github.com/ethereum/go-ethereum => github.com/prysmaticlabs/bazel-go-ethereum v0.0.0-20201113091623-013fd65b3791
39 |
40 | replace github.com/google/flatbuffers => github.com/google/flatbuffers v1.11.0
41 |
--------------------------------------------------------------------------------
/ibft/README.md:
--------------------------------------------------------------------------------
1 | # SSV - IBFT
2 |
3 | ---
4 | - **[How does IBFT works?](IBFT.md)**
5 | - **[Blox IBFT implementation](#blox-ibft-implementation)**
6 | - **[Codebase Structure](#codebase-structure)**
7 | - **[Starting SSV environment locally](#how-to-start-ssv-environment-locally)**
8 | ---
9 | ### Blox IBFT implementation
10 |
11 | ### Codebase Structure
12 |
13 | ### How to start SSV environment locally?
14 |
--------------------------------------------------------------------------------
/ibft/commit.go:
--------------------------------------------------------------------------------
1 | package ibft
2 |
3 | import (
4 | "bytes"
5 | "encoding/hex"
6 | "errors"
7 | "github.com/bloxapp/ssv/ibft/pipeline/auth"
8 |
9 | "go.uber.org/zap"
10 |
11 | "github.com/bloxapp/ssv/ibft/pipeline"
12 | "github.com/bloxapp/ssv/ibft/proto"
13 | )
14 |
15 | func (i *Instance) commitMsgPipeline() pipeline.Pipeline {
16 | return pipeline.Combine(
17 | auth.MsgTypeCheck(proto.RoundState_Commit),
18 | auth.ValidateLambdas(i.State.Lambda),
19 | auth.ValidateRound(i.State.Round),
20 | auth.ValidatePKs(i.State.ValidatorPk),
21 | auth.ValidateSequenceNumber(i.State.SeqNumber),
22 | auth.AuthorizeMsg(i.Params),
23 | i.uponCommitMsg(),
24 | )
25 | }
26 |
27 | // CommittedAggregatedMsg returns a signed message for the state's committed value with the max known signatures
28 | func (i *Instance) CommittedAggregatedMsg() (*proto.SignedMessage, error) {
29 | if i.State.PreparedValue == nil {
30 | return nil, errors.New("state not prepared")
31 | }
32 |
33 | msgs := i.CommitMessages.ReadOnlyMessagesByRound(i.State.Round)
34 | if len(msgs) == 0 {
35 | return nil, errors.New("no commit msgs")
36 | }
37 |
38 | var ret *proto.SignedMessage
39 | var err error
40 | for _, msg := range msgs {
41 | if !bytes.Equal(msg.Message.Value, i.State.PreparedValue) {
42 | continue
43 | }
44 | if ret == nil {
45 | ret, err = msg.DeepCopy()
46 | if err != nil {
47 | return nil, err
48 | }
49 | } else {
50 | if err := ret.Aggregate(msg); err != nil {
51 | return nil, err
52 | }
53 | }
54 | }
55 | return ret, nil
56 | }
57 |
58 | func (i *Instance) commitQuorum(round uint64, inputValue []byte) (quorum bool, t int, n int) {
59 | // TODO - calculate quorum one way (for prepare, commit, change round and decided) and refactor
60 | cnt := 0
61 | msgs := i.CommitMessages.ReadOnlyMessagesByRound(round)
62 | for _, v := range msgs {
63 | if bytes.Equal(inputValue, v.Message.Value) {
64 | cnt++
65 | }
66 | }
67 | quorum = cnt*3 >= i.Params.CommitteeSize()*2
68 | return quorum, cnt, i.Params.CommitteeSize()
69 | }
70 |
71 | /**
72 | upon receiving a quorum Qcommit of valid ⟨COMMIT, λi, round, value⟩ messages do:
73 | set timer i to stopped
74 | Decide(λi , value, Qcommit)
75 | */
76 | func (i *Instance) uponCommitMsg() pipeline.Pipeline {
77 | return pipeline.WrapFunc("upon commit msg", func(signedMessage *proto.SignedMessage) error {
78 | // add to prepare messages
79 | i.CommitMessages.AddMessage(signedMessage)
80 | i.Logger.Info("received valid commit message for round",
81 | zap.String("sender_ibft_id", signedMessage.SignersIDString()),
82 | zap.Uint64("round", signedMessage.Message.Round))
83 |
84 | // check if quorum achieved, act upon it.
85 | if i.Stage() == proto.RoundState_Decided {
86 | i.Logger.Info("already decided, not processing commit message")
87 | return nil // no reason to commit again
88 | }
89 | quorum, t, n := i.commitQuorum(signedMessage.Message.Round, signedMessage.Message.Value)
90 | if quorum {
91 | i.Logger.Info("decided iBFT instance",
92 | zap.String("Lambda", hex.EncodeToString(i.State.Lambda)), zap.Uint64("round", i.State.Round),
93 | zap.Int("got_votes", t), zap.Int("total_votes", n))
94 |
95 | // mark instance decided
96 | i.SetStage(proto.RoundState_Decided)
97 | i.stopRoundChangeTimer()
98 | }
99 | return nil
100 | })
101 | }
102 |
103 | func (i *Instance) generateCommitMessage(value []byte) *proto.Message {
104 | return &proto.Message{
105 | Type: proto.RoundState_Commit,
106 | Round: i.State.Round,
107 | Lambda: i.State.Lambda,
108 | SeqNumber: i.State.SeqNumber,
109 | Value: value,
110 | ValidatorPk: i.State.ValidatorPk,
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/ibft/commit_test.go:
--------------------------------------------------------------------------------
1 | package ibft
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/require"
7 |
8 | msgcontinmem "github.com/bloxapp/ssv/ibft/msgcont/inmem"
9 | "github.com/bloxapp/ssv/ibft/proto"
10 | )
11 |
12 | func TestCommittedAggregatedMsg(t *testing.T) {
13 | sks, nodes := GenerateNodes(4)
14 | instance := &Instance{
15 | CommitMessages: msgcontinmem.New(3),
16 | Params: &proto.InstanceParams{
17 | ConsensusParams: proto.DefaultConsensusParams(),
18 | IbftCommittee: nodes,
19 | },
20 | State: &proto.State{
21 | Round: 3,
22 | },
23 | }
24 |
25 | // not prepared
26 | _, err := instance.CommittedAggregatedMsg()
27 | require.EqualError(t, err, "state not prepared")
28 |
29 | // set prepared state
30 | instance.State.PreparedRound = 1
31 | instance.State.PreparedValue = []byte("value")
32 |
33 | // test prepared but no committed msgs
34 | _, err = instance.CommittedAggregatedMsg()
35 | require.EqualError(t, err, "no commit msgs")
36 |
37 | // test valid aggregation
38 | instance.CommitMessages.AddMessage(SignMsg(t, 1, sks[1], &proto.Message{
39 | Type: proto.RoundState_Commit,
40 | Round: 3,
41 | Lambda: []byte("Lambda"),
42 | Value: []byte("value"),
43 | }))
44 | instance.CommitMessages.AddMessage(SignMsg(t, 2, sks[2], &proto.Message{
45 | Type: proto.RoundState_Commit,
46 | Round: 3,
47 | Lambda: []byte("Lambda"),
48 | Value: []byte("value"),
49 | }))
50 | instance.CommitMessages.AddMessage(SignMsg(t, 3, sks[3], &proto.Message{
51 | Type: proto.RoundState_Commit,
52 | Round: 3,
53 | Lambda: []byte("Lambda"),
54 | Value: []byte("value"),
55 | }))
56 |
57 | // test aggregation
58 | msg, err := instance.CommittedAggregatedMsg()
59 | require.NoError(t, err)
60 | require.ElementsMatch(t, []uint64{1, 2, 3}, msg.SignerIds)
61 |
62 | // test that doesn't aggregate different value
63 | instance.CommitMessages.AddMessage(SignMsg(t, 3, sks[3], &proto.Message{
64 | Type: proto.RoundState_Commit,
65 | Round: 3,
66 | Lambda: []byte("Lambda"),
67 | Value: []byte("value2"),
68 | }))
69 | msg, err = instance.CommittedAggregatedMsg()
70 | require.NoError(t, err)
71 | require.ElementsMatch(t, []uint64{1, 2, 3}, msg.SignerIds)
72 |
73 | // test verification
74 | params := &proto.InstanceParams{
75 | ConsensusParams: proto.DefaultConsensusParams(),
76 | IbftCommittee: nodes,
77 | }
78 | require.NoError(t, params.VerifySignedMessage(msg))
79 | }
80 |
81 | func TestCommitPipeline(t *testing.T) {
82 | _, nodes := GenerateNodes(4)
83 | instance := &Instance{
84 | PrepareMessages: msgcontinmem.New(3),
85 | Params: &proto.InstanceParams{
86 | ConsensusParams: proto.DefaultConsensusParams(),
87 | IbftCommittee: nodes,
88 | },
89 | State: &proto.State{
90 | Round: 1,
91 | },
92 | }
93 | pipeline := instance.commitMsgPipeline()
94 | require.EqualValues(t, "combination of: type check, lambda, round, validator PK, sequence, authorize, upon commit msg, ", pipeline.Name())
95 | }
96 |
--------------------------------------------------------------------------------
/ibft/ibft_network.go:
--------------------------------------------------------------------------------
1 | package ibft
2 |
3 | import (
4 | "github.com/bloxapp/ssv/network"
5 | "go.uber.org/zap"
6 | "time"
7 | )
8 |
9 | func (i *ibftImpl) waitForMinPeerCount(minPeerCount int) {
10 | for {
11 | time.Sleep(time.Second)
12 |
13 | peers, err := i.network.AllPeers(i.ValidatorShare.ValidatorPK.Serialize())
14 | if err != nil {
15 | i.logger.Error("failed fetching peers", zap.Error(err))
16 | continue
17 | }
18 |
19 | i.logger.Debug("waiting for min peer count", zap.Int("current peer count", len(peers)))
20 | if len(peers) == minPeerCount {
21 | break
22 | }
23 | }
24 | }
25 |
26 | func (i *ibftImpl) listenToNetworkMessages() {
27 | msgChan := i.network.ReceivedMsgChan()
28 | go func() {
29 | for msg := range msgChan {
30 | i.msgQueue.AddMessage(&network.Message{
31 | Lambda: msg.Message.Lambda,
32 | SignedMessage: msg,
33 | Type: network.NetworkMsg_IBFTType,
34 | })
35 | }
36 | }()
37 |
38 | // decided messages
39 | decidedChan := i.network.ReceivedDecidedChan()
40 | go func() {
41 | for msg := range decidedChan {
42 | i.ProcessDecidedMessage(msg)
43 | }
44 | }()
45 | }
46 |
47 | func (i *ibftImpl) listenToSyncMessages() {
48 | // sync messages
49 | syncChan := i.network.ReceivedSyncMsgChan()
50 | go func() {
51 | for msg := range syncChan {
52 | i.ProcessSyncMessage(msg)
53 | }
54 | }()
55 | }
56 |
--------------------------------------------------------------------------------
/ibft/ibft_sequence.go:
--------------------------------------------------------------------------------
1 | package ibft
2 |
3 | import (
4 | "errors"
5 | "github.com/bloxapp/ssv/ibft/proto"
6 | )
7 |
8 | /**
9 | IBFT Sequence is the equivalent of block number in a blockchain.
10 | An incremental number for a new iBFT instance.
11 | A fully synced iBFT node must have all sequences to be fully synced, no skips or missing sequences.
12 | */
13 |
14 | func (i *ibftImpl) canStartNewInstance(opts InstanceOptions) error {
15 | if !i.initFinished {
16 | return errors.New("iBFT hasn't initialized yet")
17 | }
18 |
19 | highestKnown, err := i.HighestKnownDecided()
20 | if err != nil {
21 | return err
22 | }
23 |
24 | highestSeqKnown := uint64(0)
25 | if highestKnown != nil {
26 | highestSeqKnown = highestKnown.Message.SeqNumber
27 | }
28 |
29 | if opts.SeqNumber == 0 {
30 | return nil
31 | }
32 | if opts.SeqNumber != highestSeqKnown+1 {
33 | return errors.New("instance seq invalid")
34 | }
35 |
36 | return nil
37 | }
38 |
39 | // NextSeqNumber returns the previous decided instance seq number + 1
40 | // In case it's the first instance it returns 0
41 | func (i *ibftImpl) NextSeqNumber() (uint64, error) {
42 | knownDecided, err := i.HighestKnownDecided()
43 | if err != nil {
44 | return 0, err
45 | }
46 | if knownDecided == nil {
47 | return 0, nil
48 | }
49 | return knownDecided.Message.SeqNumber + 1, nil
50 | }
51 |
52 | func (i *ibftImpl) instanceOptionsFromStartOptions(opts StartOptions) InstanceOptions {
53 | return InstanceOptions{
54 | Logger: opts.Logger,
55 | Me: &proto.Node{
56 | IbftId: opts.ValidatorShare.NodeID,
57 | Pk: opts.ValidatorShare.ShareKey.GetPublicKey().Serialize(),
58 | Sk: opts.ValidatorShare.ShareKey.Serialize(),
59 | },
60 | Network: i.network,
61 | Queue: i.msgQueue,
62 | ValueCheck: opts.ValueCheck,
63 | LeaderSelector: i.leaderSelector,
64 | Params: &proto.InstanceParams{
65 | ConsensusParams: i.params.ConsensusParams,
66 | IbftCommittee: opts.ValidatorShare.Committee,
67 | },
68 | Lambda: opts.Identifier,
69 | SeqNumber: opts.SeqNumber,
70 | ValidatorPK: opts.ValidatorShare.ValidatorPK.Serialize(),
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/ibft/ibft_sequence_test.go:
--------------------------------------------------------------------------------
1 | package ibft
2 |
3 | import (
4 | "github.com/bloxapp/ssv/ibft/proto"
5 | "github.com/bloxapp/ssv/storage/collections"
6 | "github.com/bloxapp/ssv/storage/inmem"
7 | "github.com/stretchr/testify/require"
8 | "go.uber.org/zap"
9 | "testing"
10 | )
11 |
12 | func testIBFTInstance(t *testing.T) *ibftImpl {
13 | return &ibftImpl{
14 | //instances: make([]*Instance, 0),
15 | }
16 | }
17 |
18 | func TestCanStartNewInstance(t *testing.T) {
19 | sks, nodes := GenerateNodes(4)
20 |
21 | tests := []struct {
22 | name string
23 | opts StartOptions
24 | storage collections.Iibft
25 | initFinished bool
26 | expectedError string
27 | }{
28 | {
29 | "valid next instance start",
30 | StartOptions{
31 | Identifier: []byte("lambda_10"),
32 | SeqNumber: 11,
33 | Duty: nil,
34 | ValidatorShare: collections.ValidatorShare{
35 | NodeID: 1,
36 | ValidatorPK: validatorPK(sks),
37 | ShareKey: sks[1],
38 | Committee: nodes,
39 | },
40 | },
41 | populatedStorage(t, sks, 10),
42 | true,
43 | "",
44 | },
45 | {
46 | "valid first instance",
47 | StartOptions{
48 | Identifier: []byte("lambda_0"),
49 | SeqNumber: 0,
50 | Duty: nil,
51 | ValidatorShare: collections.ValidatorShare{
52 | NodeID: 1,
53 | ValidatorPK: validatorPK(sks),
54 | ShareKey: sks[1],
55 | Committee: nodes,
56 | },
57 | },
58 | nil,
59 | true,
60 | "",
61 | },
62 | {
63 | "didn't finish initialization",
64 | StartOptions{
65 | Identifier: []byte("lambda_0"),
66 | SeqNumber: 0,
67 | Duty: nil,
68 | ValidatorShare: collections.ValidatorShare{
69 | NodeID: 1,
70 | ValidatorPK: validatorPK(sks),
71 | ShareKey: sks[1],
72 | Committee: nodes,
73 | },
74 | },
75 | nil,
76 | false,
77 | "iBFT hasn't initialized yet",
78 | },
79 | {
80 | "sequence skips",
81 | StartOptions{
82 | Identifier: []byte("lambda_12"),
83 | SeqNumber: 12,
84 | Duty: nil,
85 | ValidatorShare: collections.ValidatorShare{
86 | NodeID: 1,
87 | ValidatorPK: validatorPK(sks),
88 | ShareKey: sks[1],
89 | Committee: nodes,
90 | },
91 | },
92 | populatedStorage(t, sks, 10),
93 | true,
94 | "instance seq invalid",
95 | },
96 | {
97 | "past instance",
98 | StartOptions{
99 | Identifier: []byte("lambda_10"),
100 | SeqNumber: 10,
101 | Duty: nil,
102 | ValidatorShare: collections.ValidatorShare{
103 | NodeID: 1,
104 | ValidatorPK: validatorPK(sks),
105 | ShareKey: sks[1],
106 | Committee: nodes,
107 | },
108 | },
109 | populatedStorage(t, sks, 10),
110 | true,
111 | "instance seq invalid",
112 | },
113 | }
114 |
115 | for _, test := range tests {
116 | t.Run(test.name, func(t *testing.T) {
117 | i := testIBFTInstance(t)
118 | i.initFinished = test.initFinished
119 | if test.storage != nil {
120 | i.ibftStorage = test.storage
121 | } else {
122 | s := collections.NewIbft(inmem.New(), zap.L(), "attestation")
123 | i.ibftStorage = &s
124 | }
125 |
126 | i.ValidatorShare = &test.opts.ValidatorShare
127 | i.params = &proto.InstanceParams{
128 | ConsensusParams: proto.DefaultConsensusParams(),
129 | IbftCommittee: nodes,
130 | }
131 | //i.instances = test.prevInstances
132 | instanceOpts := i.instanceOptionsFromStartOptions(test.opts)
133 | //instanceOpts.SeqNumber = test.seqNumber
134 | err := i.canStartNewInstance(instanceOpts)
135 |
136 | if len(test.expectedError) > 0 {
137 | require.EqualError(t, err, test.expectedError)
138 | } else {
139 | require.NoError(t, err)
140 | }
141 | })
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/ibft/ibft_sync.go:
--------------------------------------------------------------------------------
1 | package ibft
2 |
3 | import (
4 | "github.com/bloxapp/ssv/ibft/sync"
5 | "github.com/bloxapp/ssv/network"
6 | )
7 |
8 | func (i *ibftImpl) ProcessSyncMessage(msg *network.SyncChanObj) {
9 | s := sync.NewReqHandler(i.logger, msg.Msg.ValidatorPk, i.network, i.ibftStorage)
10 | go s.Process(msg)
11 | }
12 |
13 | // SyncIBFT will fetch best known decided message (highest sequence) from the network and sync to it.
14 | func (i *ibftImpl) SyncIBFT() {
15 | s := sync.NewHistorySync(i.logger, i.ValidatorShare.ValidatorPK.Serialize(), i.network, i.ibftStorage, i.validateDecidedMsg)
16 | s.Start()
17 | }
18 |
--------------------------------------------------------------------------------
/ibft/leader.go:
--------------------------------------------------------------------------------
1 | package ibft
2 |
3 | // IsLeader checks and return true for round leader, false otherwise
4 | func (i *Instance) IsLeader() bool {
5 | return i.Me.IbftId == i.ThisRoundLeader()
6 | }
7 |
8 | // ThisRoundLeader returns the round leader
9 | func (i *Instance) ThisRoundLeader() uint64 {
10 | return i.RoundLeader(i.State.Round)
11 | }
12 |
13 | // RoundLeader checks the round leader
14 | func (i *Instance) RoundLeader(round uint64) uint64 {
15 | return i.LeaderSelector.Current(uint64(i.Params.CommitteeSize()))
16 | }
17 |
--------------------------------------------------------------------------------
/ibft/leader/README.md:
--------------------------------------------------------------------------------
1 | # iBFT round leader selection
2 |
3 | A leader can be selected in many ways, we've implemented a simple deterministic leader selection based on a provided seed for each instance, from which the first leader is selected.
4 |
5 | Each round the following operator id is selected in a round-robin fashion.
--------------------------------------------------------------------------------
/ibft/leader/constant.go:
--------------------------------------------------------------------------------
1 | package leader
2 |
3 | // Constant robin leader selection will always return the same leader
4 | type Constant struct {
5 | LeaderIndex uint64
6 | }
7 |
8 | // Current returns the current leader
9 | func (rr *Constant) Current(committeeSize uint64) uint64 {
10 | return rr.LeaderIndex
11 | }
12 |
13 | // Bump to the index
14 | func (rr *Constant) Bump() {
15 | }
16 |
17 | // SetSeed takes []byte and converts to uint64,returns error if fails.
18 | func (rr *Constant) SetSeed(seed []byte, index uint64) error {
19 | return nil
20 | }
21 |
--------------------------------------------------------------------------------
/ibft/leader/deterministic.go:
--------------------------------------------------------------------------------
1 | package leader
2 |
3 | import (
4 | "bytes"
5 | "encoding/binary"
6 | )
7 |
8 | // Deterministic Round robin leader selection is a fair and sequential leader selection.
9 | // Each instance/ round change the next leader is selected one-by-one.
10 | type Deterministic struct {
11 | index uint64
12 | baseInt uint64
13 | }
14 | // Current returns the current leader
15 | func (rr *Deterministic) Current(committeeSize uint64) uint64 {
16 | return (rr.baseInt + rr.index) % committeeSize
17 | }
18 |
19 | // Bump to the index
20 | func (rr *Deterministic) Bump() {
21 | rr.index++
22 | }
23 |
24 | // SetSeed takes []byte and converts to uint64,returns error if fails.
25 | func (rr *Deterministic) SetSeed(seed []byte, index uint64) error {
26 | rr.index = index
27 | return binary.Read(bytes.NewBuffer(maxEightByteSlice(seed)), binary.LittleEndian, &rr.baseInt)
28 | }
29 |
30 | func maxEightByteSlice(input []byte) []byte {
31 | if len(input) > 8 {
32 | return input[0:8]
33 | }
34 | return input
35 | }
36 |
--------------------------------------------------------------------------------
/ibft/leader/deterministic_test.go:
--------------------------------------------------------------------------------
1 | package leader
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/require"
7 | )
8 |
9 | func TestDeterministic_SetSeed(t *testing.T) {
10 | tests := []struct {
11 | name string
12 | seed []byte
13 | committeeSize uint64
14 | expectedLeader uint64
15 | expectedErr string
16 | }{
17 | {
18 | "valid 8 byte seed",
19 | []byte{1, 1, 1, 1, 1, 1, 1, 1},
20 | 10,
21 | 3,
22 | "",
23 | },
24 | {
25 | "valid >8 byte seed",
26 | []byte{1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
27 | 10,
28 | 3,
29 | "",
30 | },
31 | {
32 | "invalid <8 byte seed",
33 | []byte{1, 1, 1, 1, 1, 1, 1},
34 | 10,
35 | 3,
36 | "unexpected EOF",
37 | },
38 | }
39 |
40 | for _, test := range tests {
41 | t.Run(test.name, func(t *testing.T) {
42 | d := &Deterministic{}
43 | if len(test.expectedErr) > 0 {
44 | require.EqualError(t, d.SetSeed(test.seed, 0), test.expectedErr)
45 | } else {
46 | require.NoError(t, d.SetSeed(test.seed, 0))
47 | require.EqualValues(t, test.expectedLeader, d.Current(test.committeeSize))
48 | }
49 |
50 | })
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/ibft/leader/selector.go:
--------------------------------------------------------------------------------
1 | package leader
2 |
3 | // Selector is interface to implement the leader selection logic
4 | type Selector interface {
5 | // Current returns the current leader as calculated by the implementation.
6 | Current(committeeSize uint64) uint64
7 |
8 | // Bump is a util function used to keep track of round and instance changes internally to the implementation.
9 | Bump()
10 |
11 | // SetSeed sets seed from which the leader is deterministically determined
12 | SetSeed(seed []byte, index uint64) error
13 | }
14 |
--------------------------------------------------------------------------------
/ibft/msgcont/inmem/inmem.go:
--------------------------------------------------------------------------------
1 | package inmem
2 |
3 | import (
4 | "encoding/hex"
5 | "sync"
6 |
7 | "github.com/bloxapp/ssv/ibft/msgcont"
8 | "github.com/bloxapp/ssv/ibft/proto"
9 | )
10 |
11 | // messagesContainer is a simple container for messagesByRound used to count messagesByRound and decide if quorum was achieved.
12 | type messagesContainer struct {
13 | messagesByRound map[uint64][]*proto.SignedMessage
14 | messagesByRoundAndValue map[uint64]map[string][]*proto.SignedMessage // map[round]map[valueHex]msgs
15 | exitingMsgSigners map[uint64]map[uint64]bool
16 | quorumThreshold uint64
17 | lock sync.RWMutex
18 | }
19 |
20 | // New is the constructor of MessagesContainer
21 | func New(quorumThreshold uint64) msgcont.MessageContainer {
22 | return &messagesContainer{
23 | messagesByRound: make(map[uint64][]*proto.SignedMessage),
24 | messagesByRoundAndValue: make(map[uint64]map[string][]*proto.SignedMessage),
25 | exitingMsgSigners: make(map[uint64]map[uint64]bool),
26 | quorumThreshold: quorumThreshold,
27 | }
28 | }
29 |
30 | // ReadOnlyMessagesByRound returns messagesByRound by the given round
31 | func (c *messagesContainer) ReadOnlyMessagesByRound(round uint64) []*proto.SignedMessage {
32 | c.lock.RLock()
33 | defer c.lock.RUnlock()
34 | return c.messagesByRound[round]
35 | }
36 |
37 | func (c *messagesContainer) readOnlyMessagesByRoundAndValue(round uint64, value []byte) []*proto.SignedMessage {
38 | c.lock.RLock()
39 | defer c.lock.RUnlock()
40 | valueHex := hex.EncodeToString(value)
41 |
42 | if _, found := c.messagesByRoundAndValue[round]; !found {
43 | return nil
44 | }
45 | return c.messagesByRoundAndValue[round][valueHex]
46 | }
47 |
48 | func (c *messagesContainer) QuorumAchieved(round uint64, value []byte) (bool, []*proto.SignedMessage) {
49 | if msgs := c.readOnlyMessagesByRoundAndValue(round, value); msgs != nil {
50 | signers := 0
51 | retMsgs := make([]*proto.SignedMessage, 0)
52 | for _, msg := range msgs {
53 | signers += len(msg.SignerIds)
54 | retMsgs = append(retMsgs, msg)
55 | }
56 |
57 | if uint64(signers) >= c.quorumThreshold {
58 | return true, retMsgs
59 | }
60 | }
61 | return false, nil
62 | }
63 |
64 | // AddMessage adds the given message to the container
65 | func (c *messagesContainer) AddMessage(msg *proto.SignedMessage) {
66 | c.lock.Lock()
67 | defer c.lock.Unlock()
68 |
69 | valueHex := hex.EncodeToString(msg.Message.Value)
70 |
71 | // check msg is not duplicate
72 | if c.exitingMsgSigners[msg.Message.Round] != nil {
73 | for _, signer := range msg.SignerIds {
74 | if _, found := c.exitingMsgSigners[msg.Message.Round][signer]; found {
75 | return
76 | }
77 | }
78 | }
79 |
80 | // add messagesByRound
81 | _, found := c.messagesByRound[msg.Message.Round]
82 | if !found {
83 | c.messagesByRound[msg.Message.Round] = make([]*proto.SignedMessage, 0)
84 | }
85 | c.messagesByRound[msg.Message.Round] = append(c.messagesByRound[msg.Message.Round], msg)
86 |
87 | // add messages by round and value
88 | _, found = c.messagesByRoundAndValue[msg.Message.Round]
89 | if !found {
90 | c.messagesByRoundAndValue[msg.Message.Round] = make(map[string][]*proto.SignedMessage)
91 | c.exitingMsgSigners[msg.Message.Round] = make(map[uint64]bool)
92 | }
93 | _, found = c.messagesByRoundAndValue[msg.Message.Round][valueHex]
94 | if !found {
95 | c.messagesByRoundAndValue[msg.Message.Round][valueHex] = make([]*proto.SignedMessage, 0)
96 | }
97 |
98 | for _, signer := range msg.SignerIds {
99 | c.exitingMsgSigners[msg.Message.Round][signer] = true
100 | }
101 | c.messagesByRoundAndValue[msg.Message.Round][valueHex] = append(c.messagesByRoundAndValue[msg.Message.Round][valueHex], msg)
102 | }
103 |
--------------------------------------------------------------------------------
/ibft/msgcont/inmem/inmem_test.go:
--------------------------------------------------------------------------------
1 | package inmem
2 |
3 | import (
4 | "github.com/bloxapp/ssv/ibft/proto"
5 | "github.com/stretchr/testify/require"
6 | "testing"
7 | )
8 |
9 | func TestMessagesContainer_AddMessage(t *testing.T) {
10 | c := New(3)
11 | c.AddMessage(&proto.SignedMessage{
12 | Message: &proto.Message{
13 | Round: 1,
14 | Lambda: nil,
15 | Value: []byte{1, 1, 1, 1},
16 | },
17 | Signature: nil,
18 | SignerIds: []uint64{1, 2, 3, 4},
19 | })
20 |
21 | require.Len(t, c.ReadOnlyMessagesByRound(1), 1)
22 | require.Len(t, c.ReadOnlyMessagesByRound(2), 0)
23 |
24 | // try to add duplicate
25 | c.AddMessage(&proto.SignedMessage{
26 | Message: &proto.Message{
27 | Round: 1,
28 | Lambda: nil,
29 | Value: []byte{1, 1, 1, 1},
30 | },
31 | Signature: nil,
32 | SignerIds: []uint64{4, 5},
33 | })
34 | require.Len(t, c.ReadOnlyMessagesByRound(1), 1)
35 | require.Len(t, c.ReadOnlyMessagesByRound(2), 0)
36 | c.AddMessage(&proto.SignedMessage{
37 | Message: &proto.Message{
38 | Round: 1,
39 | Lambda: nil,
40 | Value: []byte{1, 1, 1, 1},
41 | },
42 | Signature: nil,
43 | SignerIds: []uint64{4},
44 | })
45 | require.Len(t, c.ReadOnlyMessagesByRound(1), 1)
46 | require.Len(t, c.ReadOnlyMessagesByRound(2), 0)
47 | }
48 |
49 | func TestMessagesContainer_ReadOnlyMessagesByRound(t *testing.T) {
50 | c := New(3)
51 | c.AddMessage(&proto.SignedMessage{
52 | Message: &proto.Message{
53 | Round: 1,
54 | Lambda: nil,
55 | Value: []byte{1, 1, 1, 1},
56 | },
57 | Signature: nil,
58 | SignerIds: []uint64{1, 2, 3, 4},
59 | })
60 | c.AddMessage(&proto.SignedMessage{
61 | Message: &proto.Message{
62 | Round: 1,
63 | Lambda: nil,
64 | Value: []byte{1, 1, 1, 1},
65 | },
66 | Signature: nil,
67 | SignerIds: []uint64{5},
68 | })
69 |
70 | msgs := c.ReadOnlyMessagesByRound(1)
71 | require.EqualValues(t, 1, msgs[0].Message.Round)
72 | require.EqualValues(t, 1, msgs[1].Message.Round)
73 | require.EqualValues(t, []byte{1, 1, 1, 1}, msgs[0].Message.Value)
74 | require.EqualValues(t, []byte{1, 1, 1, 1}, msgs[1].Message.Value)
75 | require.EqualValues(t, []uint64{1, 2, 3, 4}, msgs[0].SignerIds)
76 | require.EqualValues(t, []uint64{5}, msgs[1].SignerIds)
77 | }
78 |
79 | func TestMessagesContainer_QuorumAchieved(t *testing.T) {
80 | c := New(3)
81 | c.AddMessage(&proto.SignedMessage{
82 | Message: &proto.Message{
83 | Round: 1,
84 | Lambda: nil,
85 | Value: []byte{1, 1, 1, 1},
86 | },
87 | Signature: nil,
88 | SignerIds: []uint64{1, 2, 3},
89 | })
90 | res, _ := c.QuorumAchieved(1, []byte{1, 1, 1, 1})
91 | require.True(t, res)
92 | res, _ = c.QuorumAchieved(0, []byte{1, 1, 1, 1})
93 | require.False(t, res)
94 | res, _ = c.QuorumAchieved(1, []byte{1, 1, 1, 0})
95 | require.False(t, res)
96 |
97 | c.AddMessage(&proto.SignedMessage{
98 | Message: &proto.Message{
99 | Round: 2,
100 | Lambda: nil,
101 | Value: []byte{1, 1, 1, 1},
102 | },
103 | Signature: nil,
104 | SignerIds: []uint64{1, 2},
105 | })
106 | res, _ = c.QuorumAchieved(2, []byte{1, 1, 1, 1})
107 | require.False(t, res)
108 | c.AddMessage(&proto.SignedMessage{
109 | Message: &proto.Message{
110 | Round: 2,
111 | Lambda: nil,
112 | Value: []byte{1, 1, 1, 1},
113 | },
114 | Signature: nil,
115 | SignerIds: []uint64{3},
116 | })
117 | res, _ = c.QuorumAchieved(2, []byte{1, 1, 1, 1})
118 | require.True(t, res)
119 | res, _ = c.QuorumAchieved(3, []byte{1, 1, 1, 1})
120 | require.False(t, res)
121 | res, _ = c.QuorumAchieved(2, []byte{1, 1, 1, 0})
122 | require.False(t, res)
123 | }
124 |
--------------------------------------------------------------------------------
/ibft/msgcont/msgcont.go:
--------------------------------------------------------------------------------
1 | package msgcont
2 |
3 | import (
4 | "github.com/bloxapp/ssv/ibft/proto"
5 | )
6 |
7 | // MessageContainer represents the behavior of the message container
8 | type MessageContainer interface {
9 | // ReadOnlyMessagesByRound returns messages by the given round
10 | ReadOnlyMessagesByRound(round uint64) []*proto.SignedMessage
11 |
12 | // QuorumAchieved returns true if enough msgs were received (round, value)
13 | QuorumAchieved(round uint64, value []byte) (bool, []*proto.SignedMessage)
14 |
15 | // AddMessage adds the given message to the container
16 | AddMessage(msg *proto.SignedMessage)
17 | }
18 |
--------------------------------------------------------------------------------
/ibft/pipeline.go:
--------------------------------------------------------------------------------
1 | package ibft
2 |
3 | import (
4 | "github.com/bloxapp/ssv/ibft/pipeline"
5 | "github.com/bloxapp/ssv/ibft/proto"
6 | "github.com/bloxapp/ssv/network/msgqueue"
7 | "go.uber.org/zap"
8 | )
9 |
10 | // ProcessMessage pulls messages from the queue to be processed sequentially
11 | func (i *Instance) ProcessMessage() (processedMsg bool, err error) {
12 | if netMsg := i.MsgQueue.PopMessage(msgqueue.IBFTRoundIndexKey(i.State.Lambda, i.State.Round)); netMsg != nil {
13 | var pp pipeline.Pipeline
14 | switch netMsg.SignedMessage.Message.Type {
15 | case proto.RoundState_PrePrepare:
16 | pp = i.prePrepareMsgPipeline()
17 | case proto.RoundState_Prepare:
18 | pp = i.prepareMsgPipeline()
19 | case proto.RoundState_Commit:
20 | pp = i.commitMsgPipeline()
21 | case proto.RoundState_ChangeRound:
22 | pp = i.changeRoundMsgPipeline()
23 | default:
24 | i.Logger.Warn("undefined message type", zap.Any("msg", netMsg.SignedMessage))
25 | return true, nil
26 | }
27 |
28 | if err := pp.Run(netMsg.SignedMessage); err != nil {
29 | return true, err
30 | }
31 | return true, nil
32 | }
33 | return false, nil
34 | }
35 |
--------------------------------------------------------------------------------
/ibft/pipeline/auth/msg_auth.go:
--------------------------------------------------------------------------------
1 | package auth
2 |
3 | import (
4 | "github.com/bloxapp/ssv/ibft/pipeline"
5 | "github.com/bloxapp/ssv/ibft/proto"
6 | )
7 |
8 | // AuthorizeMsg is the pipeline to authorize message
9 | func AuthorizeMsg(params *proto.InstanceParams) pipeline.Pipeline {
10 | return pipeline.WrapFunc("authorize", func(signedMessage *proto.SignedMessage) error {
11 | return params.VerifySignedMessage(signedMessage)
12 | })
13 | }
14 |
--------------------------------------------------------------------------------
/ibft/pipeline/auth/msg_lambda.go:
--------------------------------------------------------------------------------
1 | package auth
2 |
3 | import (
4 | "bytes"
5 |
6 | "github.com/pkg/errors"
7 |
8 | "github.com/bloxapp/ssv/ibft/pipeline"
9 | "github.com/bloxapp/ssv/ibft/proto"
10 | )
11 |
12 | // ValidateLambdas validates current and previous lambdas
13 | func ValidateLambdas(lambda []byte) pipeline.Pipeline {
14 | return pipeline.WrapFunc("lambda", func(signedMessage *proto.SignedMessage) error {
15 | if !bytes.Equal(signedMessage.Message.Lambda, lambda) {
16 | return errors.Errorf("message Lambda (%s) does not equal expected Lambda (%s)", string(signedMessage.Message.Lambda), string(lambda))
17 | }
18 | return nil
19 | })
20 | }
21 |
--------------------------------------------------------------------------------
/ibft/pipeline/auth/msg_lambda_test.go:
--------------------------------------------------------------------------------
1 | package auth
2 |
3 | import (
4 | "github.com/bloxapp/ssv/ibft/proto"
5 | "github.com/stretchr/testify/require"
6 | "testing"
7 | )
8 |
9 | func TestMsgLambda(t *testing.T) {
10 | tests := []struct {
11 | name string
12 | expectedLambda []byte
13 | actualLambda []byte
14 | expectedError string
15 | }{
16 | {
17 | "valid",
18 | []byte{1, 2, 3, 4},
19 | []byte{1, 2, 3, 4},
20 | "",
21 | },
22 | {
23 | "different msg lambda",
24 | []byte{1, 2, 3, 4},
25 | []byte{1, 2, 3, 3},
26 | "message Lambda (\x01\x02\x03\x03) does not equal expected Lambda (\x01\x02\x03\x04)",
27 | },
28 | }
29 |
30 | for _, test := range tests {
31 | t.Run(test.name, func(t *testing.T) {
32 | pipeline := ValidateLambdas(test.expectedLambda)
33 | err := pipeline.Run(&proto.SignedMessage{
34 | Message: &proto.Message{
35 | Lambda: test.actualLambda,
36 | },
37 | })
38 |
39 | if len(test.expectedError) == 0 {
40 | require.NoError(t, err)
41 | } else {
42 | require.EqualError(t, err, test.expectedError)
43 | }
44 | })
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/ibft/pipeline/auth/msg_quorum.go:
--------------------------------------------------------------------------------
1 | package auth
2 |
3 | import (
4 | "errors"
5 | "github.com/bloxapp/ssv/ibft/pipeline"
6 | "github.com/bloxapp/ssv/ibft/proto"
7 | )
8 |
9 | // ValidateQuorum is the pipeline to validate msg quorum requirement
10 | func ValidateQuorum(threshold int) pipeline.Pipeline {
11 | return pipeline.WrapFunc("quorum", func(signedMessage *proto.SignedMessage) error {
12 | if len(signedMessage.SignerIds) < threshold {
13 | return errors.New("quorum not achieved")
14 | }
15 | return nil
16 | })
17 | }
18 |
--------------------------------------------------------------------------------
/ibft/pipeline/auth/msg_round.go:
--------------------------------------------------------------------------------
1 | package auth
2 |
3 | import (
4 | "github.com/pkg/errors"
5 |
6 | "github.com/bloxapp/ssv/ibft/pipeline"
7 | "github.com/bloxapp/ssv/ibft/proto"
8 | )
9 |
10 | // ValidateRound validates round
11 | func ValidateRound(round uint64) pipeline.Pipeline {
12 | return pipeline.WrapFunc("round", func(signedMessage *proto.SignedMessage) error {
13 | if round != signedMessage.Message.Round {
14 | return errors.Errorf("message round (%d) does not equal State round (%d)", signedMessage.Message.Round, round)
15 | }
16 |
17 | return nil
18 | })
19 | }
20 |
--------------------------------------------------------------------------------
/ibft/pipeline/auth/msg_round_test.go:
--------------------------------------------------------------------------------
1 | package auth
2 |
3 | import (
4 | "github.com/bloxapp/ssv/ibft/proto"
5 | "github.com/stretchr/testify/require"
6 | "testing"
7 | )
8 |
9 | func TestMsgRound(t *testing.T) {
10 | tests := []struct {
11 | name string
12 | expectedRound uint64
13 | actualRound uint64
14 | expectedError string
15 | }{
16 | {
17 | "valid",
18 | 1,
19 | 1,
20 | "",
21 | },
22 | {
23 | "different msg rounds",
24 | 1,
25 | 2,
26 | "message round (2) does not equal State round (1)",
27 | },
28 | }
29 |
30 | for _, test := range tests {
31 | t.Run(test.name, func(t *testing.T) {
32 | pipeline := ValidateRound(test.expectedRound)
33 | err := pipeline.Run(&proto.SignedMessage{
34 | Message: &proto.Message{
35 | Round: test.actualRound,
36 | },
37 | })
38 |
39 | if len(test.expectedError) == 0 {
40 | require.NoError(t, err)
41 | } else {
42 | require.EqualError(t, err, test.expectedError)
43 | }
44 | })
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/ibft/pipeline/auth/msg_seq.go:
--------------------------------------------------------------------------------
1 | package auth
2 |
3 | import (
4 | "github.com/bloxapp/ssv/ibft/pipeline"
5 | "github.com/bloxapp/ssv/ibft/proto"
6 | "github.com/pkg/errors"
7 | )
8 |
9 | // ValidateSequenceNumber validates msg seq number
10 | func ValidateSequenceNumber(seq uint64) pipeline.Pipeline {
11 | return pipeline.WrapFunc("sequence", func(signedMessage *proto.SignedMessage) error {
12 | if signedMessage.Message.SeqNumber != seq {
13 | return errors.New("invalid message sequence number")
14 | }
15 | return nil
16 | })
17 | }
18 |
--------------------------------------------------------------------------------
/ibft/pipeline/auth/msg_seq_test.go:
--------------------------------------------------------------------------------
1 | package auth
2 |
3 | import (
4 | "github.com/bloxapp/ssv/ibft/proto"
5 | "github.com/stretchr/testify/require"
6 | "testing"
7 | )
8 |
9 | func TestMsgSeq(t *testing.T) {
10 | tests := []struct {
11 | name string
12 | expectedSeq uint64
13 | actualSeq uint64
14 | expectedError string
15 | }{
16 | {
17 | "valid",
18 | 1,
19 | 1,
20 | "",
21 | },
22 | {
23 | "different msg seq",
24 | 1,
25 | 2,
26 | "invalid message sequence number",
27 | },
28 | }
29 |
30 | for _, test := range tests {
31 | t.Run(test.name, func(t *testing.T) {
32 | pipeline := ValidateSequenceNumber(test.expectedSeq)
33 | err := pipeline.Run(&proto.SignedMessage{
34 | Message: &proto.Message{
35 | SeqNumber: test.actualSeq,
36 | },
37 | })
38 |
39 | if len(test.expectedError) == 0 {
40 | require.NoError(t, err)
41 | } else {
42 | require.EqualError(t, err, test.expectedError)
43 | }
44 | })
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/ibft/pipeline/auth/msg_type_check.go:
--------------------------------------------------------------------------------
1 | package auth
2 |
3 | import (
4 | "errors"
5 |
6 | "github.com/bloxapp/ssv/ibft/pipeline"
7 | "github.com/bloxapp/ssv/ibft/proto"
8 | )
9 |
10 | // MsgTypeCheck is the pipeline to check message type
11 | func MsgTypeCheck(expected proto.RoundState) pipeline.Pipeline {
12 | return pipeline.WrapFunc("type check", func(signedMessage *proto.SignedMessage) error {
13 | if signedMessage.Message.Type != expected {
14 | return errors.New("message type is wrong")
15 | }
16 | return nil
17 | })
18 | }
19 |
--------------------------------------------------------------------------------
/ibft/pipeline/auth/msg_type_check_test.go:
--------------------------------------------------------------------------------
1 | package auth
2 |
3 | import (
4 | "github.com/bloxapp/ssv/ibft/proto"
5 | "github.com/stretchr/testify/require"
6 | "testing"
7 | )
8 |
9 | func TestMsgTypeCheck(t *testing.T) {
10 | tests := []struct {
11 | name string
12 | expectedType proto.RoundState
13 | actualType proto.RoundState
14 | expectedError string
15 | }{
16 | {
17 | "valid",
18 | proto.RoundState_Prepare,
19 | proto.RoundState_Prepare,
20 | "",
21 | },
22 | {
23 | "different round state",
24 | proto.RoundState_Prepare,
25 | proto.RoundState_Decided,
26 | "message type is wrong",
27 | },
28 | }
29 |
30 | for _, test := range tests {
31 | t.Run(test.name, func(t *testing.T) {
32 | pipeline := MsgTypeCheck(test.expectedType)
33 | err := pipeline.Run(&proto.SignedMessage{
34 | Message: &proto.Message{
35 | Type: test.actualType,
36 | },
37 | })
38 |
39 | if len(test.expectedError) == 0 {
40 | require.NoError(t, err)
41 | } else {
42 | require.EqualError(t, err, test.expectedError)
43 | }
44 | })
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/ibft/pipeline/auth/msg_validator_pk.go:
--------------------------------------------------------------------------------
1 | package auth
2 |
3 | import (
4 | "bytes"
5 | "github.com/bloxapp/ssv/ibft/pipeline"
6 | "github.com/bloxapp/ssv/ibft/proto"
7 | "github.com/pkg/errors"
8 | )
9 |
10 | // ValidatePKs validates a msgs pk
11 | func ValidatePKs(pk []byte) pipeline.Pipeline {
12 | return pipeline.WrapFunc("validator PK", func(signedMessage *proto.SignedMessage) error {
13 | if len(signedMessage.Message.ValidatorPk) != 48 || !bytes.Equal(pk, signedMessage.Message.ValidatorPk) {
14 | return errors.New("invalid message validator PK")
15 | }
16 | return nil
17 | })
18 | }
19 |
--------------------------------------------------------------------------------
/ibft/pipeline/auth/msg_validator_pk_test.go:
--------------------------------------------------------------------------------
1 | package auth
2 |
3 | import (
4 | "github.com/bloxapp/ssv/ibft/proto"
5 | "github.com/stretchr/testify/require"
6 | "testing"
7 | )
8 |
9 | func TestMsgValidatorPK(t *testing.T) {
10 | tests := []struct {
11 | name string
12 | expectedPK []byte
13 | actualPK []byte
14 | expectedError string
15 | }{
16 | {
17 | "valid",
18 | _byteArray("86b78e9d24f3efacbb3ca5958b39cdcb9b3e97d241e91c903f71392e1e4f5d7706a6c8e731e76d4e0e2ac52ccd35fcb9"),
19 | _byteArray("86b78e9d24f3efacbb3ca5958b39cdcb9b3e97d241e91c903f71392e1e4f5d7706a6c8e731e76d4e0e2ac52ccd35fcb9"),
20 | "",
21 | },
22 | {
23 | "invalid(not same as state PK)",
24 | _byteArray("86b78e9d24f3efacbb3ca5958b39cdcb9b3e97d241e91c903f71392e1e4f5d7706a6c8e731e76d4e0e2ac52ccd35fcb9"),
25 | _byteArray("86b78e9d24f3efacbb3ca5958b39cdcb9b3e97d241e91c903f71392e1e4f5d7706a6c8e731e76d4e0e2ac52ccd35fcb8"),
26 | "invalid message validator PK",
27 | },
28 | {
29 | "pk too short",
30 | _byteArray("86b78e9d24f3efacbb3ca5958b39cdcb9b3e97d241e91c903f71392e1e4f5d7706a6c8e731e76d4e0e2ac52ccd35fcb9"),
31 | _byteArray("86b78e9d24f3efacbb3ca5958b39cdcb9b3e97d241e91c903f71392e1e4f5d7706a6c8e731e76d4e0e2ac52ccd35fc"),
32 | "invalid message validator PK",
33 | },
34 | {
35 | "pk too long",
36 | _byteArray("86b78e9d24f3efacbb3ca5958b39cdcb9b3e97d241e91c903f71392e1e4f5d7706a6c8e731e76d4e0e2ac52ccd35fcb9"),
37 | _byteArray("86b78e9d24f3efacbb3ca5958b39cdcb9b3e97d241e91c903f71392e1e4f5d7706a6c8e731e76d4e0e2ac52ccd35fcb9b9"),
38 | "invalid message validator PK",
39 | },
40 | {
41 | "pk nil",
42 | _byteArray("86b78e9d24f3efacbb3ca5958b39cdcb9b3e97d241e91c903f71392e1e4f5d7706a6c8e731e76d4e0e2ac52ccd35fcb9"),
43 | nil,
44 | "invalid message validator PK",
45 | },
46 | }
47 |
48 | for _, test := range tests {
49 | t.Run(test.name, func(t *testing.T) {
50 | pipeline := ValidatePKs(test.expectedPK)
51 | err := pipeline.Run(&proto.SignedMessage{
52 | Message: &proto.Message{
53 | ValidatorPk: test.actualPK,
54 | },
55 | })
56 |
57 | if len(test.expectedError) == 0 {
58 | require.NoError(t, err)
59 | } else {
60 | require.EqualError(t, err, test.expectedError)
61 | }
62 | })
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/ibft/pipeline/changeround/add_message.go:
--------------------------------------------------------------------------------
1 | package changeround
2 |
3 | import (
4 | "go.uber.org/zap"
5 |
6 | "github.com/bloxapp/ssv/ibft/msgcont"
7 | "github.com/bloxapp/ssv/ibft/pipeline"
8 | "github.com/bloxapp/ssv/ibft/proto"
9 | )
10 |
11 | // addChangeRoundMessage implements pipeline.Pipeline interface
12 | type addChangeRoundMessage struct {
13 | logger *zap.Logger
14 | changeRoundMessages msgcont.MessageContainer
15 | state *proto.State
16 | }
17 |
18 | // AddChangeRoundMessage is the constructor of addChangeRoundMessage
19 | func AddChangeRoundMessage(logger *zap.Logger, changeRoundMessages msgcont.MessageContainer, state *proto.State) pipeline.Pipeline {
20 | return &addChangeRoundMessage{
21 | logger: logger,
22 | changeRoundMessages: changeRoundMessages,
23 | state: state,
24 | }
25 | }
26 |
27 | // Run implements pipeline.Pipeline interface
28 | func (p *addChangeRoundMessage) Run(signedMessage *proto.SignedMessage) error {
29 | // TODO - if instance decidedChan should we process round change?
30 | if p.state.Stage == proto.RoundState_Decided {
31 | // TODO - can't get here, fails on round verification in pipeline
32 | p.logger.Info("received change round after decision, sending decidedChan message")
33 | return nil
34 | }
35 |
36 | // add to prepare messages
37 | p.changeRoundMessages.AddMessage(signedMessage)
38 | p.logger.Info("received valid change round message for round",
39 | zap.String("ibft_id", signedMessage.SignersIDString()),
40 | zap.Uint64("round", signedMessage.Message.Round))
41 |
42 | return nil
43 | }
44 |
45 | // Name implements pipeline.Pipeline interface
46 | func (p *addChangeRoundMessage) Name() string {
47 | return "add change round msg"
48 | }
49 |
--------------------------------------------------------------------------------
/ibft/pipeline/changeround/upon_full_quorum.go:
--------------------------------------------------------------------------------
1 | package changeround
2 |
3 | import (
4 | "go.uber.org/zap"
5 |
6 | "github.com/bloxapp/ssv/ibft/pipeline"
7 | "github.com/bloxapp/ssv/ibft/proto"
8 | )
9 |
10 | // uponFullQuorum implements pipeline.Pipeline interface
11 | type uponFullQuorum struct {
12 | logger *zap.Logger
13 | }
14 |
15 | // UponFullQuorum is the constructor of uponFullQuorum
16 | func UponFullQuorum(logger *zap.Logger) pipeline.Pipeline {
17 | return &uponFullQuorum{
18 | logger: logger,
19 | }
20 | }
21 |
22 | // Run implements pipeline.Pipeline interface
23 | //
24 | // upon receiving a quorum Qrc of valid ⟨ROUND-CHANGE, λi, ri, −, −⟩ messages such that
25 | // leader(λi, ri) = pi ∧ JustifyRoundChange(Qrc) do
26 | // if HighestPrepared(Qrc) ̸= ⊥ then
27 | // let v such that (−, v) = HighestPrepared(Qrc))
28 | // else
29 | // let v such that v = inputValue i
30 | // broadcast ⟨PRE-PREPARE, λi, ri, v⟩
31 | func (p *uponFullQuorum) Run(signedMessage *proto.SignedMessage) error {
32 | panic("not implemented yet")
33 | }
34 |
35 | // Name implements pipeline.Pipeline interface
36 | func (p *uponFullQuorum) Name() string {
37 | return "upon full quorum"
38 | }
39 |
--------------------------------------------------------------------------------
/ibft/pipeline/changeround/upon_partial_quorun.go:
--------------------------------------------------------------------------------
1 | package changeround
2 |
3 | import (
4 | "github.com/bloxapp/ssv/ibft/pipeline"
5 | "github.com/bloxapp/ssv/ibft/proto"
6 | )
7 |
8 | // uponPartialQuorum implements pipeline.Pipeline interface
9 | type uponPartialQuorum struct {
10 | }
11 |
12 | // UponPartialQuorum is the constructor of uponPartialQuorum
13 | func UponPartialQuorum() pipeline.Pipeline {
14 | return &uponPartialQuorum{}
15 | }
16 |
17 | // Run implements pipeline.Pipeline interface
18 | //
19 | // upon receiving a set Frc of f + 1 valid ⟨ROUND-CHANGE, λi, rj, −, −⟩ messages such that:
20 | // ∀⟨ROUND-CHANGE, λi, rj, −, −⟩ ∈ Frc : rj > ri do
21 | // let ⟨ROUND-CHANGE, hi, rmin, −, −⟩ ∈ Frc such that:
22 | // ∀⟨ROUND-CHANGE, λi, rj, −, −⟩ ∈ Frc : rmin ≤ rj
23 | // ri ← rmin
24 | // set timer i to running and expire after t(ri)
25 | // broadcast ⟨ROUND-CHANGE, λi, ri, pri, pvi⟩
26 | func (p *uponPartialQuorum) Run(signedMessage *proto.SignedMessage) error {
27 | return nil // TODO
28 | }
29 |
30 | // Name implements pipeline.Pipeline interface
31 | func (p *uponPartialQuorum) Name() string {
32 | return "upon partial quorum"
33 | }
34 |
--------------------------------------------------------------------------------
/ibft/pipeline/changeround/validate.go:
--------------------------------------------------------------------------------
1 | package changeround
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "github.com/pkg/errors"
7 |
8 | "github.com/bloxapp/ssv/ibft/pipeline"
9 | "github.com/bloxapp/ssv/ibft/proto"
10 | )
11 |
12 | // validate implements pipeline.Pipeline interface
13 | type validate struct {
14 | params *proto.InstanceParams
15 | }
16 |
17 | // Validate is the constructor of validate
18 | func Validate(params *proto.InstanceParams) pipeline.Pipeline {
19 | return &validate{
20 | params: params,
21 | }
22 | }
23 |
24 | // Run implements pipeline.Pipeline interface
25 | func (p *validate) Run(signedMessage *proto.SignedMessage) error {
26 | if signedMessage.Message.Value == nil {
27 | return errors.New("change round justification msg is nil")
28 | }
29 | data := &proto.ChangeRoundData{}
30 | if err := json.Unmarshal(signedMessage.Message.Value, data); err != nil {
31 | return err
32 | }
33 | if data.PreparedValue == nil { // no justification
34 | return nil
35 | }
36 | if data.JustificationMsg.Type != proto.RoundState_Prepare {
37 | return errors.New("change round justification msg type not Prepare")
38 | }
39 | if signedMessage.Message.Round <= data.JustificationMsg.Round {
40 | return errors.New("change round justification round lower or equal to message round")
41 | }
42 | if data.PreparedRound != data.JustificationMsg.Round {
43 | return errors.New("change round prepared round not equal to justification msg round")
44 | }
45 | if !bytes.Equal(signedMessage.Message.Lambda, data.JustificationMsg.Lambda) {
46 | return errors.New("change round justification msg Lambda not equal to msg Lambda not equal to instance lambda")
47 | }
48 | if !bytes.Equal(data.PreparedValue, data.JustificationMsg.Value) {
49 | return errors.New("change round prepared value not equal to justification msg value")
50 | }
51 | if len(data.SignerIds) < p.params.ThresholdSize() {
52 | return errors.New("change round justification does not constitute a quorum")
53 | }
54 |
55 | // validate justification signature
56 | pks, err := p.params.PubKeysByID(data.SignerIds)
57 | if err != nil {
58 | return errors.Wrap(err, "change round could not get pubkey")
59 | }
60 | aggregated := pks.Aggregate()
61 | res, err := data.VerifySig(aggregated)
62 | if err != nil {
63 | return errors.Wrap(err, "change round could not verify signature")
64 |
65 | }
66 | if !res {
67 | return errors.New("change round justification signature doesn't verify")
68 | }
69 |
70 | return nil
71 | }
72 |
73 | // Name implements pipeline.Pipeline interface
74 | func (p *validate) Name() string {
75 | return "validate msg"
76 | }
77 |
--------------------------------------------------------------------------------
/ibft/pipeline/decided/prev_instance_decided.go:
--------------------------------------------------------------------------------
1 | package decided
2 |
3 | import (
4 | "github.com/bloxapp/ssv/ibft/pipeline"
5 | "github.com/bloxapp/ssv/ibft/proto"
6 | "github.com/pkg/errors"
7 | )
8 |
9 | // PrevInstanceDecided verifies value is true
10 | func PrevInstanceDecided(value bool) pipeline.Pipeline {
11 | return pipeline.WrapFunc("verify true", func(signedMessage *proto.SignedMessage) error {
12 | if !value {
13 | return errors.New("value is not true")
14 | }
15 | return nil
16 | })
17 | }
18 |
--------------------------------------------------------------------------------
/ibft/pipeline/pipeline.go:
--------------------------------------------------------------------------------
1 | package pipeline
2 |
3 | import "github.com/bloxapp/ssv/ibft/proto"
4 |
5 | // SetStage sets the given stage
6 | type SetStage func(stage proto.RoundState)
7 |
8 | // SignAndBroadcast is the function to sign and broadcast message
9 | type SignAndBroadcast func(msg *proto.Message) error
10 |
11 | // Pipeline represents the behavior of round pipeline
12 | type Pipeline interface {
13 | // Run runs the pipeline
14 | Run(signedMessage *proto.SignedMessage) error
15 | Name() string
16 | }
17 |
18 | // pipelinesCombination implements Pipeline interface with multiple pipelines logic.
19 | type pipelinesCombination struct {
20 | pipelines []Pipeline
21 | }
22 |
23 | // Combine is the constructor of pipelinesCombination
24 | func Combine(pipelines ...Pipeline) Pipeline {
25 | return &pipelinesCombination{
26 | pipelines: pipelines,
27 | }
28 | }
29 |
30 | // Run implements Pipeline interface
31 | func (p *pipelinesCombination) Run(signedMessage *proto.SignedMessage) error {
32 | for _, pp := range p.pipelines {
33 | if err := pp.Run(signedMessage); err != nil {
34 | return err
35 | }
36 | }
37 | return nil
38 | }
39 |
40 | // Name implements Pipeline interface
41 | func (p *pipelinesCombination) Name() string {
42 | ret := "combination of: "
43 | for _, p := range p.pipelines {
44 | ret += p.Name() + ", "
45 | }
46 | return ret
47 | }
48 |
49 | // pipelineFunc implements Pipeline interface using just a function.
50 | type pipelineFunc struct {
51 | fn func(signedMessage *proto.SignedMessage) error
52 | name string
53 | }
54 |
55 | // WrapFunc represents the given function as a pipeline implementor
56 | func WrapFunc(name string, fn func(signedMessage *proto.SignedMessage) error) Pipeline {
57 | return &pipelineFunc{
58 | fn: fn,
59 | name: name,
60 | }
61 | }
62 |
63 | // Run implements Pipeline interface
64 | func (p *pipelineFunc) Run(signedMessage *proto.SignedMessage) error {
65 | return p.fn(signedMessage)
66 | }
67 |
68 | // Name implements Pipeline interface
69 | func (p *pipelineFunc) Name() string {
70 | return p.name
71 | }
72 |
--------------------------------------------------------------------------------
/ibft/pipeline/preprepare/validate.go:
--------------------------------------------------------------------------------
1 | package preprepare
2 |
3 | import (
4 | "github.com/bloxapp/ssv/ibft/leader"
5 | "github.com/bloxapp/ssv/ibft/valcheck"
6 | "github.com/pkg/errors"
7 |
8 | "github.com/bloxapp/ssv/ibft/pipeline"
9 | "github.com/bloxapp/ssv/ibft/proto"
10 | )
11 |
12 | // ValidatePrePrepareMsg validates pre-prepare message
13 | func ValidatePrePrepareMsg(valueCheck valcheck.ValueCheck, leaderSelector leader.Selector, params *proto.InstanceParams) pipeline.Pipeline {
14 | return pipeline.WrapFunc("validate pre-prepare", func(signedMessage *proto.SignedMessage) error {
15 | if len(signedMessage.SignerIds) != 1 {
16 | return errors.New("invalid number of signers for pre-prepare message")
17 | }
18 |
19 | if signedMessage.SignerIds[0] != leaderSelector.Current(uint64(params.CommitteeSize())) {
20 | return errors.New("pre-prepare message sender is not the round's leader")
21 | }
22 |
23 | if err := valueCheck.Check(signedMessage.Message.Value); err != nil {
24 | return errors.Wrap(err, "failed while validating pre-prepare")
25 | }
26 |
27 | return nil
28 | })
29 | }
30 |
--------------------------------------------------------------------------------
/ibft/pipeline/preprepare/validate_test.go:
--------------------------------------------------------------------------------
1 | package preprepare
2 |
3 | import (
4 | "github.com/herumi/bls-eth-go-binary/bls"
5 | "testing"
6 | "time"
7 |
8 | "github.com/stretchr/testify/require"
9 |
10 | "github.com/bloxapp/ssv/ibft/proto"
11 | "github.com/bloxapp/ssv/utils/dataval/bytesval"
12 | )
13 |
14 | // GenerateNodes generates randomly nodes
15 | func GenerateNodes(cnt int) (map[uint64]*bls.SecretKey, map[uint64]*proto.Node) {
16 | _ = bls.Init(bls.BLS12_381)
17 | nodes := make(map[uint64]*proto.Node)
18 | sks := make(map[uint64]*bls.SecretKey)
19 | for i := 0; i < cnt; i++ {
20 | sk := &bls.SecretKey{}
21 | sk.SetByCSPRNG()
22 |
23 | nodes[uint64(i)] = &proto.Node{
24 | IbftId: uint64(i),
25 | Pk: sk.GetPublicKey().Serialize(),
26 | }
27 | sks[uint64(i)] = sk
28 | }
29 | return sks, nodes
30 | }
31 |
32 | // SignMsg signs the given message by the given private key
33 | func SignMsg(t *testing.T, id uint64, sk *bls.SecretKey, msg *proto.Message) *proto.SignedMessage {
34 | bls.Init(bls.BLS12_381)
35 |
36 | signature, err := msg.Sign(sk)
37 | require.NoError(t, err)
38 | return &proto.SignedMessage{
39 | Message: msg,
40 | Signature: signature.Serialize(),
41 | SignerIds: []uint64{id},
42 | }
43 | }
44 |
45 | type testLeaderSelector struct {
46 | }
47 |
48 | func (s *testLeaderSelector) Current(committeeSize uint64) uint64 {
49 | return 1
50 | }
51 | func (s *testLeaderSelector) Bump() {}
52 | func (s *testLeaderSelector) SetSeed(seed []byte, index uint64) error { return nil }
53 |
54 | func TestValidatePrePrepareValue(t *testing.T) {
55 | sks, nodes := GenerateNodes(4)
56 | params := &proto.InstanceParams{
57 | ConsensusParams: proto.DefaultConsensusParams(),
58 | IbftCommittee: nodes,
59 | }
60 | consensus := bytesval.New([]byte(time.Now().Weekday().String()))
61 |
62 | tests := []struct {
63 | name string
64 | err string
65 | msg *proto.SignedMessage
66 | }{
67 | {
68 | "no signers",
69 | "invalid number of signers for pre-prepare message",
70 | &proto.SignedMessage{
71 | Message: &proto.Message{
72 | Type: proto.RoundState_PrePrepare,
73 | Round: 1,
74 | Lambda: []byte("Lambda"),
75 | Value: []byte(time.Now().Weekday().String()),
76 | },
77 | Signature: []byte{},
78 | SignerIds: []uint64{},
79 | },
80 | },
81 | {
82 | "only 2 signers",
83 | "invalid number of signers for pre-prepare message",
84 | &proto.SignedMessage{
85 | Message: &proto.Message{
86 | Type: proto.RoundState_PrePrepare,
87 | Round: 1,
88 | Lambda: []byte("Lambda"),
89 | Value: []byte(time.Now().Weekday().String()),
90 | },
91 | Signature: []byte{},
92 | SignerIds: []uint64{1, 2},
93 | },
94 | },
95 | {
96 | "wrong message",
97 | "failed while validating pre-prepare: msg value is wrong",
98 | SignMsg(t, 1, sks[1], &proto.Message{
99 | Type: proto.RoundState_PrePrepare,
100 | Round: 1,
101 | Lambda: []byte("Lambda"),
102 | Value: []byte("wrong value"),
103 | }),
104 | },
105 | {
106 | "non-leader sender",
107 | "pre-prepare message sender is not the round's leader",
108 | SignMsg(t, 2, sks[2], &proto.Message{
109 | Type: proto.RoundState_PrePrepare,
110 | Round: 1,
111 | Lambda: []byte("Lambda"),
112 | Value: []byte("wrong value"),
113 | }),
114 | },
115 | {
116 | "valid message",
117 | "",
118 | SignMsg(t, 1, sks[1], &proto.Message{
119 | Type: proto.RoundState_PrePrepare,
120 | Round: 1,
121 | Lambda: []byte("Lambda"),
122 | Value: []byte(time.Now().Weekday().String()),
123 | }),
124 | },
125 | }
126 | for _, test := range tests {
127 | t.Run(test.name, func(t *testing.T) {
128 | err := ValidatePrePrepareMsg(consensus, &testLeaderSelector{}, params).Run(test.msg)
129 | if len(test.err) > 0 {
130 | require.EqualError(t, err, test.err)
131 | } else {
132 | require.NoError(t, err)
133 | }
134 | })
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/ibft/pre_prepare.go:
--------------------------------------------------------------------------------
1 | package ibft
2 |
3 | import (
4 | "github.com/pkg/errors"
5 | "go.uber.org/zap"
6 |
7 | "github.com/bloxapp/ssv/ibft/pipeline"
8 | "github.com/bloxapp/ssv/ibft/pipeline/auth"
9 | "github.com/bloxapp/ssv/ibft/pipeline/preprepare"
10 | "github.com/bloxapp/ssv/ibft/proto"
11 | )
12 |
13 | func (i *Instance) prePrepareMsgPipeline() pipeline.Pipeline {
14 | return pipeline.Combine(
15 | auth.MsgTypeCheck(proto.RoundState_PrePrepare),
16 | auth.ValidateLambdas(i.State.Lambda),
17 | auth.ValidateRound(i.State.Round),
18 | auth.ValidatePKs(i.State.ValidatorPk),
19 | auth.ValidateSequenceNumber(i.State.SeqNumber),
20 | auth.AuthorizeMsg(i.Params),
21 | preprepare.ValidatePrePrepareMsg(i.ValueCheck, i.LeaderSelector, i.Params),
22 | i.UponPrePrepareMsg(),
23 | )
24 | }
25 |
26 | // JustifyPrePrepare implements:
27 | // predicate JustifyPrePrepare(hPRE-PREPARE, λi, round, valuei)
28 | // return
29 | // round = 1
30 | // ∨ received a quorum Qrc of valid messages such that:
31 | // ∀ ∈ Qrc : prj = ⊥ ∧ prj = ⊥
32 | // ∨ received a quorum of valid messages such that:
33 | // (pr, value) = HighestPrepared(Qrc)
34 | func (i *Instance) JustifyPrePrepare(round uint64) (bool, error) {
35 | if round == 1 {
36 | return true, nil
37 | }
38 |
39 | if quorum, _, _ := i.changeRoundQuorum(round); quorum {
40 | return i.JustifyRoundChange(round)
41 | }
42 | return false, nil
43 | }
44 |
45 | /*
46 | UponPrePrepareMsg Algorithm 2 IBFT pseudocode for process pi: normal case operation
47 | upon receiving a valid ⟨PRE-PREPARE, λi, ri, value⟩ message m from leader(λi, round) such that:
48 | JustifyPrePrepare(m) do
49 | set timer i to running and expire after t(ri)
50 | broadcast ⟨PREPARE, λi, ri, value⟩
51 | */
52 | func (i *Instance) UponPrePrepareMsg() pipeline.Pipeline {
53 | return pipeline.WrapFunc("upon pre-prepare msg", func(signedMessage *proto.SignedMessage) error {
54 | // add to pre-prepare messages
55 | i.PrePrepareMessages.AddMessage(signedMessage)
56 | i.Logger.Info("received valid pre-prepare message for round",
57 | zap.String("sender_ibft_id", signedMessage.SignersIDString()),
58 | zap.Uint64("round", signedMessage.Message.Round))
59 |
60 | // Pre-prepare justification
61 | justified, err := i.JustifyPrePrepare(signedMessage.Message.Round)
62 | if err != nil {
63 | return err
64 | }
65 | if !justified {
66 | return errors.New("received un-justified pre-prepare message")
67 | }
68 |
69 | // mark State
70 | i.SetStage(proto.RoundState_PrePrepare)
71 |
72 | // broadcast prepare msg
73 | broadcastMsg := i.generatePrepareMessage(signedMessage.Message.Value)
74 | if err := i.SignAndBroadcast(broadcastMsg); err != nil {
75 | i.Logger.Error("could not broadcast prepare message", zap.Error(err))
76 | return err
77 | }
78 | return nil
79 | })
80 | }
81 |
82 | func (i *Instance) generatePrePrepareMessage(value []byte) *proto.Message {
83 | return &proto.Message{
84 | Type: proto.RoundState_PrePrepare,
85 | Round: i.State.Round,
86 | Lambda: i.State.Lambda,
87 | SeqNumber: i.State.SeqNumber,
88 | Value: value,
89 | ValidatorPk: i.State.ValidatorPk,
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/ibft/prepare.go:
--------------------------------------------------------------------------------
1 | package ibft
2 |
3 | import (
4 | "bytes"
5 | "encoding/hex"
6 | "github.com/bloxapp/ssv/ibft/pipeline/auth"
7 |
8 | "github.com/pkg/errors"
9 | "go.uber.org/zap"
10 |
11 | "github.com/bloxapp/ssv/ibft/pipeline"
12 | "github.com/bloxapp/ssv/ibft/proto"
13 | )
14 |
15 | func (i *Instance) prepareMsgPipeline() pipeline.Pipeline {
16 | return pipeline.Combine(
17 | auth.MsgTypeCheck(proto.RoundState_Prepare),
18 | auth.ValidateLambdas(i.State.Lambda),
19 | auth.ValidateRound(i.State.Round),
20 | auth.ValidatePKs(i.State.ValidatorPk),
21 | auth.ValidateSequenceNumber(i.State.SeqNumber),
22 | auth.AuthorizeMsg(i.Params),
23 | i.uponPrepareMsg(),
24 | )
25 | }
26 |
27 | // PreparedAggregatedMsg returns a signed message for the state's prepared value with the max known signatures
28 | func (i *Instance) PreparedAggregatedMsg() (*proto.SignedMessage, error) {
29 | if i.State.PreparedValue == nil {
30 | return nil, errors.New("state not prepared")
31 | }
32 |
33 | msgs := i.PrepareMessages.ReadOnlyMessagesByRound(i.State.PreparedRound)
34 | if len(msgs) == 0 {
35 | return nil, errors.New("no prepare msgs")
36 | }
37 |
38 | var ret *proto.SignedMessage
39 | var err error
40 | for _, msg := range msgs {
41 | if !bytes.Equal(msg.Message.Value, i.State.PreparedValue) {
42 | continue
43 | }
44 | if ret == nil {
45 | ret, err = msg.DeepCopy()
46 | if err != nil {
47 | return nil, err
48 | }
49 | } else {
50 | if err := ret.Aggregate(msg); err != nil {
51 | return nil, err
52 | }
53 | }
54 | }
55 | return ret, nil
56 | }
57 |
58 | /**
59 | ### Algorithm 2 IBFT pseudocode for process pi: normal case operation
60 | upon receiving a quorum of valid ⟨PREPARE, λi, ri, value⟩ messages do:
61 | pri ← ri
62 | pvi ← value
63 | broadcast ⟨COMMIT, λi, ri, value⟩
64 | */
65 | func (i *Instance) uponPrepareMsg() pipeline.Pipeline {
66 | // TODO - concurrency lock?
67 | return pipeline.WrapFunc("upon prepare msg", func(signedMessage *proto.SignedMessage) error {
68 | // add to prepare messages
69 | i.PrepareMessages.AddMessage(signedMessage)
70 | i.Logger.Info("received valid prepare message from round",
71 | zap.String("sender_ibft_id", signedMessage.SignersIDString()),
72 | zap.Uint64("round", signedMessage.Message.Round))
73 |
74 | // If already prepared (or moved forward to commit) no reason to prepare again.
75 | if i.Stage() == proto.RoundState_Prepare ||
76 | i.Stage() == proto.RoundState_Decided {
77 | i.Logger.Info("already prepared, not processing prepare message")
78 | return nil // no reason to prepare again
79 | }
80 |
81 | // TODO - calculate quorum one way (for prepare, commit, change round and decided) and refactor
82 | if quorum, _ := i.PrepareMessages.QuorumAchieved(signedMessage.Message.Round, signedMessage.Message.Value); quorum {
83 | i.Logger.Info("prepared instance",
84 | zap.String("Lambda", hex.EncodeToString(i.State.Lambda)), zap.Uint64("round", i.State.Round))
85 |
86 | // set prepared State
87 | i.State.PreparedRound = signedMessage.Message.Round
88 | i.State.PreparedValue = signedMessage.Message.Value
89 | i.SetStage(proto.RoundState_Prepare)
90 |
91 | // send commit msg
92 | broadcastMsg := i.generateCommitMessage(i.State.PreparedValue)
93 | if err := i.SignAndBroadcast(broadcastMsg); err != nil {
94 | i.Logger.Info("could not broadcast commit message", zap.Error(err))
95 | return err
96 | }
97 | return nil
98 | }
99 | return nil
100 | })
101 | }
102 |
103 | func (i *Instance) generatePrepareMessage(value []byte) *proto.Message {
104 | return &proto.Message{
105 | Type: proto.RoundState_Prepare,
106 | Round: i.State.Round,
107 | Lambda: i.State.Lambda,
108 | SeqNumber: i.State.SeqNumber,
109 | Value: value,
110 | ValidatorPk: i.State.ValidatorPk,
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/ibft/prepare_test.go:
--------------------------------------------------------------------------------
1 | package ibft
2 |
3 | import (
4 | "github.com/stretchr/testify/require"
5 | "testing"
6 |
7 | msgcontinmem "github.com/bloxapp/ssv/ibft/msgcont/inmem"
8 | "github.com/bloxapp/ssv/ibft/proto"
9 | )
10 |
11 | func TestPreparedAggregatedMsg(t *testing.T) {
12 | sks, nodes := GenerateNodes(4)
13 | instance := &Instance{
14 | PrepareMessages: msgcontinmem.New(3),
15 | Params: &proto.InstanceParams{
16 | ConsensusParams: proto.DefaultConsensusParams(),
17 | IbftCommittee: nodes,
18 | },
19 | State: &proto.State{
20 | Round: 1,
21 | },
22 | }
23 |
24 | // not prepared
25 | _, err := instance.PreparedAggregatedMsg()
26 | require.EqualError(t, err, "state not prepared")
27 |
28 | // set prepared state
29 | instance.State.PreparedRound = 1
30 | instance.State.PreparedValue = []byte("value")
31 |
32 | // test prepared but no msgs
33 | _, err = instance.PreparedAggregatedMsg()
34 | require.EqualError(t, err, "no prepare msgs")
35 |
36 | // test valid aggregation
37 | instance.PrepareMessages.AddMessage(SignMsg(t, 1, sks[1], &proto.Message{
38 | Type: proto.RoundState_Prepare,
39 | Round: 1,
40 | Lambda: []byte("Lambda"),
41 | Value: []byte("value"),
42 | }))
43 | instance.PrepareMessages.AddMessage(SignMsg(t, 2, sks[2], &proto.Message{
44 | Type: proto.RoundState_Prepare,
45 | Round: 1,
46 | Lambda: []byte("Lambda"),
47 | Value: []byte("value"),
48 | }))
49 | instance.PrepareMessages.AddMessage(SignMsg(t, 3, sks[3], &proto.Message{
50 | Type: proto.RoundState_Prepare,
51 | Round: 1,
52 | Lambda: []byte("Lambda"),
53 | Value: []byte("value"),
54 | }))
55 |
56 | // test aggregation
57 | msg, err := instance.PreparedAggregatedMsg()
58 | require.NoError(t, err)
59 | require.ElementsMatch(t, []uint64{1, 2, 3}, msg.SignerIds)
60 |
61 | // test that doesn't aggregate different value
62 | instance.PrepareMessages.AddMessage(SignMsg(t, 4, sks[4], &proto.Message{
63 | Type: proto.RoundState_Prepare,
64 | Round: 1,
65 | Lambda: []byte("Lambda"),
66 | Value: []byte("value2"),
67 | }))
68 | msg, err = instance.PreparedAggregatedMsg()
69 | require.NoError(t, err)
70 | require.ElementsMatch(t, []uint64{1, 2, 3}, msg.SignerIds)
71 | }
72 |
73 | func TestPreparePipeline(t *testing.T) {
74 | _, nodes := GenerateNodes(4)
75 | instance := &Instance{
76 | PrepareMessages: msgcontinmem.New(3),
77 | Params: &proto.InstanceParams{
78 | ConsensusParams: proto.DefaultConsensusParams(),
79 | IbftCommittee: nodes,
80 | },
81 | State: &proto.State{
82 | Round: 1,
83 | },
84 | }
85 | pipeline := instance.prepareMsgPipeline()
86 | require.EqualValues(t, "combination of: type check, lambda, round, validator PK, sequence, authorize, upon prepare msg, ", pipeline.Name())
87 | }
88 |
--------------------------------------------------------------------------------
/ibft/proto/beacon.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package proto;
4 |
5 | option go_package = "github.com/bloxapp/ssv/ibft/proto";
6 |
7 | import "eth/v1alpha1/attestation.proto";
8 | import "eth/v1alpha1/beacon_block.proto";
9 |
10 | message InputValue {
11 | oneof data {
12 | ethereum.eth.v1alpha1.AttestationData attestation_data = 2;
13 | ethereum.eth.v1alpha1.AggregateAttestationAndProof aggregation_data = 3;
14 | ethereum.eth.v1alpha1.BeaconBlock beacon_block = 4;
15 | }
16 |
17 | oneof signed_data {
18 | ethereum.eth.v1alpha1.Attestation attestation = 5;
19 | ethereum.eth.v1alpha1.SignedAggregateAttestationAndProof aggregation = 6;
20 | ethereum.eth.v1alpha1.SignedBeaconBlock block = 7;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/ibft/proto/generate.go:
--------------------------------------------------------------------------------
1 | package proto
2 |
3 | //go:generate protoc -I $GOPATH/src/github.com/prysmaticlabs/ethereumapis --proto_path=$GOPATH/src:. --go_out=$GOPATH/src $GOPATH/src/github.com/bloxapp/ssv/ibft/proto/msgs.proto
4 | //go:generate protoc -I $GOPATH/src/github.com/prysmaticlabs/ethereumapis --proto_path=$GOPATH/src:. --go_out=$GOPATH/src $GOPATH/src/github.com/bloxapp/ssv/ibft/proto/params.proto
5 | //go:generate protoc -I $GOPATH/src/github.com/prysmaticlabs/ethereumapis --proto_path=$GOPATH/src:. --go_out=$GOPATH/src $GOPATH/src/github.com/bloxapp/ssv/ibft/proto/state.proto
6 | //go:generate protoc -I $GOPATH/src/github.com/prysmaticlabs/ethereumapis --proto_path=$GOPATH/src:. --go_out=$GOPATH/src $GOPATH/src/github.com/bloxapp/ssv/ibft/proto/beacon.proto
7 |
--------------------------------------------------------------------------------
/ibft/proto/msgs.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package proto;
4 |
5 | option go_package = "github.com/bloxapp/ssv/ibft/proto";
6 |
7 | import "github.com/gogo/protobuf/gogoproto/gogo.proto";
8 |
9 | enum RoundState {
10 | NotStarted = 0;
11 | PrePrepare = 1;
12 | Prepare = 2;
13 | Commit = 3; // Commit is when an instance receives a qualified quorum of prepare msgs, then sends a commit msg.
14 | ChangeRound = 4;
15 | Decided = 5; // Decided is when an instance receives a qualified quorum of commit msgs
16 | Stopped = 6;
17 | }
18 |
19 | message Message {
20 | RoundState type = 1;
21 | uint64 round = 2;
22 | bytes lambda = 3;
23 | // sequence number is an incremental number for each instance, much like a block number would be in a blockchain
24 | uint64 seq_number = 4;
25 | bytes value = 5;
26 | bytes validator_pk = 6;
27 | }
28 |
29 | message SignedMessage{
30 | Message message = 1 [(gogoproto.nullable) = false];
31 | bytes signature = 2 [(gogoproto.nullable) = false];
32 | repeated uint64 signer_ids = 3;
33 | }
34 |
35 | message ChangeRoundData{
36 | uint64 prepared_round = 1;
37 | bytes prepared_value = 2 [(gogoproto.nullable) = false];
38 | Message justification_msg = 3 [(gogoproto.nullable) = false];
39 | bytes justification_sig = 4 [(gogoproto.nullable) = false];
40 | repeated uint64 signer_ids = 5;
41 | }
--------------------------------------------------------------------------------
/ibft/proto/params.go:
--------------------------------------------------------------------------------
1 | package proto
2 |
3 | import (
4 | "errors"
5 | "math"
6 | "time"
7 |
8 | "github.com/herumi/bls-eth-go-binary/bls"
9 | )
10 |
11 | // PubKeys defines the type for public keys object representation
12 | type PubKeys []*bls.PublicKey
13 |
14 | // Aggregate iterates over public keys and adds them to the bls PublicKey
15 | func (keys PubKeys) Aggregate() bls.PublicKey {
16 | ret := bls.PublicKey{}
17 | for _, k := range keys {
18 | ret.Add(k)
19 | }
20 | return ret
21 | }
22 |
23 | //DefaultConsensusParams returns the default round change duration time
24 | func DefaultConsensusParams() *ConsensusParams {
25 | return &ConsensusParams{
26 | RoundChangeDuration: int64(time.Second * 3),
27 | LeaderPreprepareDelay: int64(time.Second * 1),
28 | }
29 | }
30 |
31 | // CommitteeSize returns the IBFT committee size
32 | func (p *InstanceParams) CommitteeSize() int {
33 | return len(p.IbftCommittee)
34 | }
35 |
36 | // ThresholdSize returns the minimum IBFT committee members that needs to sign for a quorum
37 | func (p *InstanceParams) ThresholdSize() int {
38 | return int(math.Ceil(float64(len(p.IbftCommittee)) * 2 / 3))
39 | }
40 |
41 | // PubKeysByID returns the public keys with the associated ids
42 | func (p *InstanceParams) PubKeysByID(ids []uint64) (PubKeys, error) {
43 | ret := make([]*bls.PublicKey, 0)
44 | for _, id := range ids {
45 | if val, ok := p.IbftCommittee[id]; ok {
46 | pk := &bls.PublicKey{}
47 | if err := pk.Deserialize(val.Pk); err != nil {
48 | return ret, err
49 | }
50 | ret = append(ret, pk)
51 | } else {
52 | return nil, errors.New("pk for id not found")
53 | }
54 | }
55 | return ret, nil
56 | }
57 |
58 | // VerifySignedMessage returns true of signed message verifies against pks
59 | func (p *InstanceParams) VerifySignedMessage(msg *SignedMessage) error {
60 | pks, err := p.PubKeysByID(msg.SignerIds)
61 | if err != nil {
62 | return err
63 | }
64 | if len(pks) == 0 {
65 | return errors.New("could not find public key")
66 | }
67 |
68 | res, err := msg.VerifyAggregatedSig(pks)
69 | if err != nil {
70 | return err
71 | }
72 | if !res {
73 | return errors.New("could not verify message signature")
74 | }
75 |
76 | return nil
77 | }
78 |
--------------------------------------------------------------------------------
/ibft/proto/params.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package proto;
4 |
5 | option go_package = "github.com/bloxapp/ssv/ibft/proto";
6 |
7 | import "github.com/gogo/protobuf/gogoproto/gogo.proto";
8 |
9 | message ConsensusParams{
10 | int64 round_change_duration = 1;
11 | int64 leader_preprepare_delay = 2; // The time a round leader waits before broadcasting pre-prepare message
12 | }
13 |
14 | message Node{
15 | uint64 ibft_id = 1;
16 | bytes pk = 2 [(gogoproto.nullable) = false];
17 | bytes sk = 3;
18 | }
19 |
20 | message InstanceParams{
21 | ConsensusParams consensus_params = 1;
22 | map ibft_committee = 2;
23 | }
--------------------------------------------------------------------------------
/ibft/proto/params_test.go:
--------------------------------------------------------------------------------
1 | package proto
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/herumi/bls-eth-go-binary/bls"
7 | "github.com/stretchr/testify/require"
8 | )
9 |
10 | func generateNodes(cnt int) (map[uint64]*bls.SecretKey, map[uint64]*Node) {
11 | bls.Init(bls.BLS12_381)
12 | nodes := make(map[uint64]*Node)
13 | sks := make(map[uint64]*bls.SecretKey)
14 | for i := 0; i < cnt; i++ {
15 | sk := &bls.SecretKey{}
16 | sk.SetByCSPRNG()
17 |
18 | nodes[uint64(i)] = &Node{
19 | IbftId: uint64(i),
20 | Pk: sk.GetPublicKey().Serialize(),
21 | }
22 | sks[uint64(i)] = sk
23 | }
24 | return sks, nodes
25 | }
26 |
27 | func signMsg(id uint64, secretKey *bls.SecretKey, msg *Message) (*SignedMessage, *bls.Sign) {
28 | signature, _ := msg.Sign(secretKey)
29 | return &SignedMessage{
30 | Message: msg,
31 | Signature: signature.Serialize(),
32 | SignerIds: []uint64{id},
33 | }, signature
34 | }
35 |
36 | func TestInstanceParams_ThresholdSize(t *testing.T) {
37 | for i := 1; i < 50; i++ {
38 | p := &InstanceParams{IbftCommittee: make(map[uint64]*Node)}
39 | // populate
40 | for j := 1; j <= 3*i+1; j++ {
41 | p.IbftCommittee[uint64(j)] = &Node{}
42 | }
43 | require.EqualValues(t, 2*i+1, p.ThresholdSize())
44 | }
45 | }
46 |
47 | func TestPubKeysById(t *testing.T) {
48 | secretKeys, nodes := generateNodes(4)
49 | params := &InstanceParams{
50 | IbftCommittee: nodes,
51 | }
52 |
53 | t.Run("test single", func(t *testing.T) {
54 | pks, err := params.PubKeysByID([]uint64{0})
55 | require.NoError(t, err)
56 | require.Len(t, pks, 1)
57 | require.EqualValues(t, pks[0].Serialize(), secretKeys[0].GetPublicKey().Serialize())
58 | })
59 |
60 | t.Run("test multiple", func(t *testing.T) {
61 | pks, err := params.PubKeysByID([]uint64{0, 1})
62 | require.NoError(t, err)
63 | require.Len(t, pks, 2)
64 | require.EqualValues(t, pks[0].Serialize(), secretKeys[0].GetPublicKey().Serialize())
65 | require.EqualValues(t, pks[1].Serialize(), secretKeys[1].GetPublicKey().Serialize())
66 | })
67 |
68 | t.Run("test multiple with invalid", func(t *testing.T) {
69 | _, err := params.PubKeysByID([]uint64{0, 5})
70 | require.EqualError(t, err, "pk for id not found")
71 | })
72 | }
73 |
74 | func TestVerifySignedMsg(t *testing.T) {
75 | secretKeys, nodes := generateNodes(4)
76 | params := &InstanceParams{
77 | IbftCommittee: nodes,
78 | }
79 |
80 | msg := &Message{
81 | Type: RoundState_Decided,
82 | Round: 1,
83 | Lambda: []byte{1, 2, 3, 4},
84 | SeqNumber: 1,
85 | }
86 | aggMessage, aggregated := signMsg(1, secretKeys[1], msg)
87 | _, sig2 := signMsg(2, secretKeys[2], msg)
88 | aggregated.Add(sig2)
89 | aggMessage.Signature = aggregated.Serialize()
90 | aggMessage.SignerIds = []uint64{1, 2}
91 |
92 | require.NoError(t, params.VerifySignedMessage(aggMessage))
93 | aggMessage.SignerIds = []uint64{1, 3}
94 | require.EqualError(t, params.VerifySignedMessage(aggMessage), "could not verify message signature")
95 | }
96 |
--------------------------------------------------------------------------------
/ibft/proto/state.go:
--------------------------------------------------------------------------------
1 | package proto
2 |
3 | // PreviouslyPrepared checks if state prepare round and value are set
4 | func (s *State) PreviouslyPrepared() bool {
5 | return s.PreparedRound != 0 && s.PreparedValue != nil
6 | }
7 |
--------------------------------------------------------------------------------
/ibft/proto/state.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package proto;
4 |
5 | option go_package = "github.com/bloxapp/ssv/ibft/proto";
6 |
7 | import "github.com/bloxapp/ssv/ibft/proto/msgs.proto";
8 |
9 | message State {
10 | RoundState stage = 1;
11 | // lambda is an instance unique identifier, much like a block hash in a blockchain
12 | bytes lambda = 2;
13 | // sequence number is an incremental number for each instance, much like a block number would be in a blockchain
14 | uint64 seq_number = 3;
15 | bytes input_value = 4;
16 | uint64 round = 5;
17 | uint64 prepared_round = 6;
18 | bytes prepared_value = 7;
19 | bytes validator_pk = 8;
20 | }
--------------------------------------------------------------------------------
/ibft/spectesting/algorithm_test.go:
--------------------------------------------------------------------------------
1 | package spectesting
2 |
3 | //// IBFT ALGORITHM 2: Happy flow - a normal case operation
4 | //func TestUponPrePrepareMessagesBroadcastsPrepare(t *testing.T) {
5 | // secretKeys, nodes := GenerateNodes(4)
6 | // instance := prepareInstance(t, nodes, secretKeys)
7 | //
8 | // // Upon receiving valid PRE-PREPARE messages - 1, 2, 3
9 | // message := setupMessage(1, secretKeys[1], proto.RoundState_PrePrepare)
10 | // instance.PrePrepareMessages.AddMessage(message)
11 | //
12 | // message = setupMessage(1, secretKeys[2], proto.RoundState_PrePrepare)
13 | // instance.PrePrepareMessages.AddMessage(message)
14 | //
15 | // message = setupMessage(1, secretKeys[3], proto.RoundState_PrePrepare)
16 | // instance.PrePrepareMessages.AddMessage(message)
17 | //
18 | // require.NoError(t, instance.UponPrePrepareMsg().Run(message))
19 | //
20 | // // ...such that JUSTIFY PREPARE is true
21 | // res, err := instance.JustifyPrePrepare(1)
22 | // require.NoError(t, err)
23 | // require.True(t, res)
24 | //
25 | // // broadcasts PREPARE message
26 | // prepareMessage := setupMessage(1, secretKeys[3], proto.RoundState_Prepare)
27 | // instance.PrepareMessages.AddMessage(prepareMessage)
28 | //}
29 |
30 | //func TestRoundRobinLeaderChange(t *testing.T) {
31 | // // should bump between instances
32 | // // should bump between round changes
33 | // t.Fail()
34 | //}
35 |
36 | //func setupMessage(id uint64, secretKey *bls.SecretKey, roundState proto.RoundState) *proto.SignedMessage {
37 | // return SignMsg(id, secretKey, &proto.Message{
38 | // Type: roundState,
39 | // Round: 1,
40 | // Lambda: []byte("Lambda"),
41 | // Value: []byte(time.Now().Weekday().String()),
42 | // })
43 | //}
44 |
--------------------------------------------------------------------------------
/ibft/spectesting/nodes.go:
--------------------------------------------------------------------------------
1 | package spectesting
2 |
3 | import (
4 | "github.com/herumi/bls-eth-go-binary/bls"
5 |
6 | "github.com/bloxapp/ssv/ibft/proto"
7 | )
8 |
9 | // GenerateNodes generates randomly nodes
10 | func GenerateNodes(cnt int) (map[uint64]*bls.SecretKey, map[uint64]*proto.Node) {
11 | _ = bls.Init(bls.BLS12_381)
12 | nodes := make(map[uint64]*proto.Node)
13 | sks := make(map[uint64]*bls.SecretKey)
14 | for i := 0; i < cnt; i++ {
15 | sk := &bls.SecretKey{}
16 | sk.SetByCSPRNG()
17 |
18 | nodes[uint64(i)] = &proto.Node{
19 | IbftId: uint64(i),
20 | Pk: sk.GetPublicKey().Serialize(),
21 | }
22 | sks[uint64(i)] = sk
23 | }
24 | return sks, nodes
25 | }
26 |
--------------------------------------------------------------------------------
/ibft/spectesting/sign.go:
--------------------------------------------------------------------------------
1 | package spectesting
2 |
3 | import (
4 | "github.com/bloxapp/ssv/fixtures"
5 | "github.com/herumi/bls-eth-go-binary/bls"
6 | "github.com/stretchr/testify/require"
7 | "testing"
8 |
9 | "github.com/bloxapp/ssv/ibft/proto"
10 | )
11 |
12 | // SignMsg signs the given message by the given private key
13 | func SignMsg(t *testing.T, id uint64, skByts []byte, msg *proto.Message) *proto.SignedMessage {
14 | require.NoError(t, bls.Init(bls.BLS12_381))
15 |
16 | // add validator PK to all msgs
17 | msg.ValidatorPk = fixtures.RefPk
18 |
19 | sk := &bls.SecretKey{}
20 | require.NoError(t, sk.Deserialize(skByts))
21 |
22 | signature, err := msg.Sign(sk)
23 | require.NoError(t, err)
24 | return &proto.SignedMessage{
25 | Message: msg,
26 | Signature: signature.Serialize(),
27 | SignerIds: []uint64{id},
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/ibft/spectesting/tests/change_round_and_decide.go:
--------------------------------------------------------------------------------
1 | package tests
2 |
3 | import (
4 | "github.com/bloxapp/ssv/ibft"
5 | "github.com/bloxapp/ssv/ibft/proto"
6 | "github.com/bloxapp/ssv/ibft/spectesting"
7 | "github.com/bloxapp/ssv/network"
8 | "github.com/stretchr/testify/require"
9 | "testing"
10 | )
11 |
12 | // ChangeRoundAndDecide tests coming to consensus after a non prepared change round
13 | type ChangeRoundAndDecide struct {
14 | instance *ibft.Instance
15 | inputValue []byte
16 | lambda []byte
17 | prevLambda []byte
18 | }
19 |
20 | // Name returns test name
21 | func (test *ChangeRoundAndDecide) Name() string {
22 | return "pre-prepare -> change round -> pre-prepare -> prepare -> decide"
23 | }
24 |
25 | // Prepare prepares the test
26 | func (test *ChangeRoundAndDecide) Prepare(t *testing.T) {
27 | test.lambda = []byte{1, 2, 3, 4}
28 | test.prevLambda = []byte{0, 0, 0, 0}
29 | test.inputValue = spectesting.TestInputValue()
30 |
31 | test.instance = spectesting.TestIBFTInstance(t, test.lambda, test.prevLambda)
32 | test.instance.State.Round = 1
33 |
34 | // load messages to queue
35 | for _, msg := range test.MessagesSequence(t) {
36 | test.instance.MsgQueue.AddMessage(&network.Message{
37 | Lambda: test.lambda,
38 | SignedMessage: msg,
39 | Type: network.NetworkMsg_IBFTType,
40 | })
41 | }
42 | }
43 |
44 | // MessagesSequence includes all test messages
45 | func (test *ChangeRoundAndDecide) MessagesSequence(t *testing.T) []*proto.SignedMessage {
46 | return []*proto.SignedMessage{
47 | spectesting.PrePrepareMsg(t, spectesting.TestSKs()[0], test.lambda, test.prevLambda, test.inputValue, 1, 1),
48 |
49 | spectesting.ChangeRoundMsg(t, spectesting.TestSKs()[0], test.lambda, test.prevLambda, 2, 1),
50 | spectesting.ChangeRoundMsg(t, spectesting.TestSKs()[1], test.lambda, test.prevLambda, 2, 2),
51 | spectesting.ChangeRoundMsg(t, spectesting.TestSKs()[2], test.lambda, test.prevLambda, 2, 3),
52 | spectesting.ChangeRoundMsg(t, spectesting.TestSKs()[3], test.lambda, test.prevLambda, 2, 4),
53 |
54 | spectesting.PrePrepareMsg(t, spectesting.TestSKs()[0], test.lambda, test.prevLambda, test.inputValue, 2, 1),
55 |
56 | spectesting.PrepareMsg(t, spectesting.TestSKs()[0], test.lambda, test.prevLambda, test.inputValue, 2, 1),
57 | spectesting.PrepareMsg(t, spectesting.TestSKs()[1], test.lambda, test.prevLambda, test.inputValue, 2, 2),
58 | spectesting.PrepareMsg(t, spectesting.TestSKs()[2], test.lambda, test.prevLambda, test.inputValue, 2, 3),
59 | spectesting.PrepareMsg(t, spectesting.TestSKs()[3], test.lambda, test.prevLambda, test.inputValue, 2, 4),
60 |
61 | spectesting.CommitMsg(t, spectesting.TestSKs()[0], test.lambda, test.prevLambda, test.inputValue, 2, 1),
62 | spectesting.CommitMsg(t, spectesting.TestSKs()[1], test.lambda, test.prevLambda, test.inputValue, 2, 2),
63 | spectesting.CommitMsg(t, spectesting.TestSKs()[2], test.lambda, test.prevLambda, test.inputValue, 2, 3),
64 | spectesting.CommitMsg(t, spectesting.TestSKs()[3], test.lambda, test.prevLambda, test.inputValue, 2, 4),
65 | }
66 | }
67 |
68 | // Run runs the test
69 | func (test *ChangeRoundAndDecide) Run(t *testing.T) {
70 | // pre-prepare
71 | spectesting.RequireProcessedMessage(t, test.instance.ProcessMessage)
72 | spectesting.SimulateTimeout(test.instance, 2)
73 |
74 | // change round
75 | spectesting.RequireProcessedMessage(t, test.instance.ProcessMessage)
76 | spectesting.RequireProcessedMessage(t, test.instance.ProcessMessage)
77 | spectesting.RequireProcessedMessage(t, test.instance.ProcessMessage)
78 | spectesting.RequireProcessedMessage(t, test.instance.ProcessMessage)
79 | justified, err := test.instance.JustifyRoundChange(2)
80 | require.NoError(t, err)
81 | require.True(t, justified)
82 |
83 | // check pre-prepare justification
84 | justified, err = test.instance.JustifyPrePrepare(2)
85 | require.NoError(t, err)
86 | require.True(t, justified)
87 |
88 | // process all messages
89 | for {
90 | if res, _ := test.instance.ProcessMessage(); !res {
91 | break
92 | }
93 | }
94 | require.EqualValues(t, proto.RoundState_Decided, test.instance.State.Stage)
95 | }
96 |
--------------------------------------------------------------------------------
/ibft/spectesting/tests/non_justified_pre_prepare.go:
--------------------------------------------------------------------------------
1 | package tests
2 |
3 | import (
4 | "github.com/bloxapp/ssv/ibft"
5 | "github.com/bloxapp/ssv/ibft/proto"
6 | "github.com/bloxapp/ssv/ibft/spectesting"
7 | "github.com/bloxapp/ssv/network"
8 | "testing"
9 | )
10 |
11 | // NonJustifiedPrePrepapre tests coming to consensus after a non prepared change round
12 | type NonJustifiedPrePrepapre struct {
13 | instance *ibft.Instance
14 | inputValue []byte
15 | lambda []byte
16 | prevLambda []byte
17 | }
18 |
19 | // Name returns test name
20 | func (test *NonJustifiedPrePrepapre) Name() string {
21 | return "pre-prepare -> simulate round timeout -> unjustified pre-prepare"
22 | }
23 |
24 | // Prepare prepares the test
25 | func (test *NonJustifiedPrePrepapre) Prepare(t *testing.T) {
26 | test.lambda = []byte{1, 2, 3, 4}
27 | test.prevLambda = []byte{0, 0, 0, 0}
28 | test.inputValue = spectesting.TestInputValue()
29 |
30 | test.instance = spectesting.TestIBFTInstance(t, test.lambda, test.prevLambda)
31 | test.instance.State.Round = 1
32 |
33 | // load messages to queue
34 | for _, msg := range test.MessagesSequence(t) {
35 | test.instance.MsgQueue.AddMessage(&network.Message{
36 | Lambda: test.lambda,
37 | SignedMessage: msg,
38 | Type: network.NetworkMsg_IBFTType,
39 | })
40 | }
41 | }
42 |
43 | // MessagesSequence includes all test messages
44 | func (test *NonJustifiedPrePrepapre) MessagesSequence(t *testing.T) []*proto.SignedMessage {
45 | return []*proto.SignedMessage{
46 | spectesting.PrePrepareMsg(t, spectesting.TestSKs()[0], test.lambda, test.prevLambda, test.inputValue, 1, 1),
47 | spectesting.PrePrepareMsg(t, spectesting.TestSKs()[0], test.lambda, test.prevLambda, test.inputValue, 2, 1),
48 | }
49 | }
50 |
51 | // Run runs the test
52 | func (test *NonJustifiedPrePrepapre) Run(t *testing.T) {
53 | // pre-prepare
54 | spectesting.RequireProcessedMessage(t, test.instance.ProcessMessage)
55 | spectesting.SimulateTimeout(test.instance, 2)
56 |
57 | // try to broadcast unjustified pre-prepare
58 | spectesting.RequireProcessMessageError(t, test.instance.ProcessMessage, "received un-justified pre-prepare message")
59 | }
60 |
--------------------------------------------------------------------------------
/ibft/spectesting/tests/spec_test.go:
--------------------------------------------------------------------------------
1 | package tests
2 |
3 | import (
4 | "github.com/bloxapp/ssv/ibft/proto"
5 | "testing"
6 | )
7 |
8 | type SpecTest interface {
9 | Name() string
10 | // Prepare sets all testing fixtures and params before running the test
11 | Prepare(t *testing.T)
12 | // MessagesSequence returns a sequence of messages to be processed when Run is called
13 | MessagesSequence(t *testing.T) []*proto.SignedMessage
14 | // Run will execute the test.
15 | Run(t *testing.T)
16 | }
17 |
18 | var tests = []SpecTest{
19 | &PrepareAtDifferentRound{},
20 | &ChangeRoundAndDecide{},
21 | &PrepareChangeRoundAndDecide{},
22 | &DecideDifferentValue{},
23 | &PrepareAtDifferentRound{},
24 | &NonJustifiedPrePrepapre{},
25 | &DuplicateMessages{},
26 | }
27 |
28 | func TestAllSpecTests(t *testing.T) {
29 | for _, test := range tests {
30 | t.Run(test.Name(), func(tt *testing.T) {
31 | test.Prepare(tt)
32 | test.Run(tt)
33 | })
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/ibft/spectesting/tests/valid_simple_run.go:
--------------------------------------------------------------------------------
1 | package tests
2 |
3 | import (
4 | "github.com/bloxapp/ssv/ibft"
5 | "github.com/bloxapp/ssv/ibft/proto"
6 | "github.com/bloxapp/ssv/ibft/spectesting"
7 | "github.com/bloxapp/ssv/network"
8 | "github.com/stretchr/testify/require"
9 | "testing"
10 | )
11 |
12 | // ValidSimpleRun is a simple happy flow of iBFT
13 | type ValidSimpleRun struct {
14 | instance *ibft.Instance
15 | inputValue []byte
16 | lambda []byte
17 | prevLambda []byte
18 | }
19 |
20 | // Name returns test name
21 | func (test *ValidSimpleRun) Name() string {
22 | return "Valid simple test"
23 | }
24 |
25 | // Prepare prepares the test
26 | func (test *ValidSimpleRun) Prepare(t *testing.T) {
27 | test.lambda = []byte{1, 2, 3, 4}
28 | test.prevLambda = []byte{0, 0, 0, 0}
29 | test.inputValue = spectesting.TestInputValue()
30 |
31 | test.instance = spectesting.TestIBFTInstance(t, test.lambda, test.prevLambda)
32 | test.instance.State.Round = 1
33 |
34 | // load messages to queue
35 | for _, msg := range test.MessagesSequence(t) {
36 | test.instance.MsgQueue.AddMessage(&network.Message{
37 | Lambda: test.lambda,
38 | SignedMessage: msg,
39 | Type: network.NetworkMsg_IBFTType,
40 | })
41 | }
42 | }
43 |
44 | // MessagesSequence includes all messages
45 | func (test *ValidSimpleRun) MessagesSequence(t *testing.T) []*proto.SignedMessage {
46 | return []*proto.SignedMessage{
47 | spectesting.PrePrepareMsg(t, spectesting.TestSKs()[0], test.lambda, test.prevLambda, test.inputValue, 1, 1),
48 |
49 | spectesting.PrepareMsg(t, spectesting.TestSKs()[0], test.lambda, test.prevLambda, test.inputValue, 1, 1),
50 | spectesting.PrepareMsg(t, spectesting.TestSKs()[1], test.lambda, test.prevLambda, test.inputValue, 1, 2),
51 | spectesting.PrepareMsg(t, spectesting.TestSKs()[2], test.lambda, test.prevLambda, test.inputValue, 1, 3),
52 | spectesting.PrepareMsg(t, spectesting.TestSKs()[3], test.lambda, test.prevLambda, test.inputValue, 1, 4),
53 |
54 | spectesting.CommitMsg(t, spectesting.TestSKs()[0], test.lambda, test.prevLambda, test.inputValue, 1, 1),
55 | spectesting.CommitMsg(t, spectesting.TestSKs()[1], test.lambda, test.prevLambda, test.inputValue, 1, 2),
56 | spectesting.CommitMsg(t, spectesting.TestSKs()[2], test.lambda, test.prevLambda, test.inputValue, 1, 3),
57 | spectesting.CommitMsg(t, spectesting.TestSKs()[3], test.lambda, test.prevLambda, test.inputValue, 1, 4),
58 | }
59 | }
60 |
61 | // Run runs the test
62 | func (test *ValidSimpleRun) Run(t *testing.T) {
63 | // pre-prepare
64 | spectesting.RequireProcessedMessage(t, test.instance.ProcessMessage)
65 | // non qualified prepare quorum
66 | spectesting.RequireProcessedMessage(t, test.instance.ProcessMessage)
67 | quorum, _ := test.instance.PrepareMessages.QuorumAchieved(1, test.inputValue)
68 | require.False(t, quorum)
69 | // qualified prepare quorum
70 | spectesting.RequireProcessedMessage(t, test.instance.ProcessMessage)
71 | spectesting.RequireProcessedMessage(t, test.instance.ProcessMessage)
72 | spectesting.RequireProcessedMessage(t, test.instance.ProcessMessage)
73 | quorum, _ = test.instance.PrepareMessages.QuorumAchieved(1, test.inputValue)
74 | require.True(t, quorum)
75 | // non qualified commit quorum
76 | spectesting.RequireProcessedMessage(t, test.instance.ProcessMessage)
77 | quorum, _ = test.instance.CommitMessages.QuorumAchieved(1, test.inputValue)
78 | require.False(t, quorum)
79 | // qualified commit quorum
80 | spectesting.RequireProcessedMessage(t, test.instance.ProcessMessage)
81 | spectesting.RequireProcessedMessage(t, test.instance.ProcessMessage)
82 | spectesting.RequireNotProcessedMessage(t, test.instance.ProcessMessage) // we purge all messages after decided was reached
83 | quorum, _ = test.instance.CommitMessages.QuorumAchieved(1, test.inputValue)
84 | require.True(t, quorum)
85 |
86 | require.EqualValues(t, proto.RoundState_Decided, test.instance.State.Stage)
87 | }
88 |
--------------------------------------------------------------------------------
/ibft/sync/incoming.go:
--------------------------------------------------------------------------------
1 | package sync
2 |
3 | import (
4 | "github.com/bloxapp/ssv/ibft/proto"
5 | "github.com/bloxapp/ssv/network"
6 | "github.com/bloxapp/ssv/storage/collections"
7 | "go.uber.org/zap"
8 | )
9 |
10 | // ReqHandler is responsible for syncing and iBFT instance when needed by
11 | // fetching decided messages from the network
12 | type ReqHandler struct {
13 | // paginationMaxSize is the max number of returned elements in a single response
14 | paginationMaxSize uint64
15 | validatorPK []byte
16 | network network.Network
17 | storage collections.Iibft
18 | logger *zap.Logger
19 | }
20 |
21 | // NewReqHandler returns a new instance of ReqHandler
22 | func NewReqHandler(logger *zap.Logger, validatorPK []byte, network network.Network, storage collections.Iibft) *ReqHandler {
23 | return &ReqHandler{
24 | paginationMaxSize: 25, // TODO - change to be a param
25 | logger: logger,
26 | validatorPK: validatorPK,
27 | network: network,
28 | storage: storage,
29 | }
30 | }
31 |
32 | // Process takes a req and processes it
33 | func (s *ReqHandler) Process(msg *network.SyncChanObj) {
34 | switch msg.Msg.Type {
35 | case network.Sync_GetHighestType:
36 | s.handleGetHighestReq(msg)
37 | case network.Sync_GetInstanceRange:
38 | s.handleGetDecidedReq(msg)
39 | default:
40 | s.logger.Error("sync req handler received un-supported type", zap.Uint64("received type", uint64(msg.Msg.Type)))
41 | }
42 | }
43 |
44 | func (s *ReqHandler) handleGetDecidedReq(msg *network.SyncChanObj) {
45 | if len(msg.Msg.Params) != 2 {
46 | panic("implement")
47 | }
48 | if msg.Msg.Params[0] > msg.Msg.Params[1] {
49 | panic("implement")
50 | }
51 |
52 | // enforce max page size
53 | startSeq := msg.Msg.Params[0]
54 | endSeq := msg.Msg.Params[1]
55 | if endSeq-startSeq > s.paginationMaxSize {
56 | endSeq = startSeq + s.paginationMaxSize
57 | }
58 |
59 | ret := make([]*proto.SignedMessage, 0)
60 | for i := startSeq; i <= endSeq; i++ {
61 | decidedMsg, err := s.storage.GetDecided(msg.Msg.ValidatorPk, i)
62 | if err != nil {
63 | panic("implement")
64 | }
65 |
66 | ret = append(ret, decidedMsg)
67 | }
68 |
69 | retMsg := &network.SyncMessage{
70 | SignedMessages: ret,
71 | ValidatorPk: msg.Msg.ValidatorPk,
72 | Type: network.Sync_GetInstanceRange,
73 | }
74 | if err := s.network.RespondToGetDecidedByRange(msg.Stream, retMsg); err != nil {
75 | s.logger.Error("failed to send get decided by range response", zap.Error(err))
76 | }
77 | }
78 |
79 | func (s *ReqHandler) handleGetHighestReq(msg *network.SyncChanObj) {
80 | highest, err := s.storage.GetHighestDecidedInstance(msg.Msg.ValidatorPk)
81 | if err != nil {
82 | s.logger.Error("failed to get highest decided from db", zap.Error(err))
83 | }
84 | res := &network.SyncMessage{
85 | SignedMessages: []*proto.SignedMessage{highest},
86 | Type: network.Sync_GetHighestType,
87 | }
88 |
89 | if err := s.network.RespondToHighestDecidedInstance(msg.Stream, res); err != nil {
90 | s.logger.Error("failed to send highest decided response", zap.Error(err))
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/ibft/valcheck/README.md:
--------------------------------------------------------------------------------
1 | # Value check
2 |
3 | IBFT tests a received value (pre-prepare message) via an interface called ValueCheck.
4 | This is a simple interface which passes a byte slice to an implementation, for SSV we can check AttestationData/ ProposalData and so on.
--------------------------------------------------------------------------------
/ibft/valcheck/value_check.go:
--------------------------------------------------------------------------------
1 | package valcheck
2 |
3 | // ValueCheck is an interface which validates the pre-prepare value passed to the node.
4 | // It's kept minimal to allow the implementation to have all the check logic.
5 | type ValueCheck interface {
6 | Check(value []byte) error
7 | }
8 |
--------------------------------------------------------------------------------
/install.sh:
--------------------------------------------------------------------------------
1 | # install dependencies
2 | sudo apt-get -y update
3 | sudo apt-get -y install \
4 | apt-transport-https \
5 | ca-certificates \
6 | curl \
7 | gnupg \
8 | lsb-release
9 |
10 | # install docker
11 | curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
12 | echo \
13 | "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
14 | $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
15 | sudo apt-get -y update
16 | sudo apt-get -y install docker-ce docker-ce-cli containerd.io
17 |
18 |
19 | # install more dependencies
20 | sudo apt-get -y update && \
21 | apt-get install --no-install-recommends bash curl wget \
22 | && rm -rf /var/lib/apt/lists/*
23 |
24 | # manually installing yq
25 | sudo wget -O /usr/local/bin/yq https://github.com/mikefarah/yq/releases/download/3.3.0/yq_linux_amd64
26 | sudo chmod +x /usr/local/bin/yq
27 |
--------------------------------------------------------------------------------
/internals/documentation/operator_getting_started.md:
--------------------------------------------------------------------------------
1 | [
](https://www.bloxstaking.com/)
2 |
3 |
4 |
5 |
6 | # Secret-Shared-Validator(SSV)
7 | Secret Shared Validator ('SSV') is a unique technology that enables the distributed control and operation of an Ethereum validator.\
8 | SSV uses an MPC threshold scheme with a consensus layer on top, that governs the network. Its core strength is in its robustness and\
9 | fault tolerance which leads the way for an open network of staking operators to run validators in a decentralized and trustless way.
10 |
11 | ## SSV Operator
12 | An operator is an entity running the SSV node code (found here) and joining the SSV network.
13 |
14 | ### General SSV information (Semi technical read)
15 | * Article by [Blox](https://medium.com/bloxstaking/an-introduction-to-secret-shared-validators-ssv-for-ethereum-2-0-faf49efcabee)
16 | * Article by [Mara Schmiedt and Collin Mayers](https://medium.com/coinmonks/eth2-secret-shared-validators-85824df8cbc0)
17 |
18 | ### Technical iBFT and SSV read
19 | * [iBFT Paper](https://arxiv.org/pdf/2002.03613.pdf)
20 | * [iBFT annotated paper (By Blox)](https://docs.google.com/document/d/1aIJVw92k4I3p5SM3Qarp0AvxJo70ZdM0s5a1arKgVGg/edit?usp=sharing)
21 |
22 | ## Running a local network
23 | 1. Clone the github repo
24 | ```bash
25 | $ git clone https://github.com/ethereum/eth2-ssv.git
26 | ```
27 | 2. Split validator key into 4 shares (can be more than 4 shares but at the moment it’s hard coded)
28 | ```bash
29 | # Extract Private keys from mnemonic (optional, skip if you have the public/private keys )
30 | $ ./bin/ssvnode export-keys --mnemonic={mnemonic} --index={keyIndex}
31 |
32 | # Generate threshold keys
33 | $ ./bin/ssvnode create-threshold --count {# of ssv nodes} --private-key {privateKey}
34 | ```
35 | 3. Create 4 .env files with the names .env.node.1, .env.node.2, .env.node.3, .env.node.4
36 | 4. Use the template .env file for step #3
37 | ```bash
38 | NETWORK=pyrmont
39 | DISCOVERY_TYPE=
40 | STORAGE_PATH=
41 | BOOT_NODE_EXTERNAL_IP=
42 | BOOT_NODE_PRIVATE_KEY=
43 | BEACON_NODE_ADDR=
44 | NODE_ID=
45 | VALIDATOR_PUBLIC_KEY=
46 | SSV_PRIVATE_KEY=
47 | PUBKEY_NODE_1=
48 | PUBKEY_NODE_2=
49 | PUBKEY_NODE_3=
50 | PUBKEY_NODE_4=
51 | ```
52 | 5. place the 4 .env files in the same folder as the eth2-SSV project
53 | 6. Run a local network
54 | ```bash
55 | # Build binary
56 | $ CGO_ENABLED=1 go build -o ./bin/ssvnode ./cmd/ssvnode/
57 |
58 | # Run local 4 node network (requires docker and a .env file as shown below)
59 | $ make docker-debug
60 | ```
61 |
--------------------------------------------------------------------------------
/internals/img/bloxstaking_header_image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ethereum/ssv/bd3ccbed4786b2be6842a1ff0cc3cc3507c382e1/internals/img/bloxstaking_header_image.png
--------------------------------------------------------------------------------
/network/generate.go:
--------------------------------------------------------------------------------
1 | package network
2 |
3 | //go:generate protoc -I ../ibft/proto -I $GOPATH/src/github.com/prysmaticlabs/ethereumapis --proto_path=$GOPATH/src:. --go_out=$GOPATH/src $GOPATH/src/github.com/bloxapp/ssv/network/network_msgs.proto
4 |
--------------------------------------------------------------------------------
/network/local/stream.go:
--------------------------------------------------------------------------------
1 | package local
2 |
3 | import (
4 | "github.com/bloxapp/ssv/network"
5 | )
6 |
7 | // Stream is used by local network
8 | type Stream struct {
9 | From string
10 | To string
11 | ReceiveChan chan *network.SyncMessage
12 | }
13 |
14 | // NewLocalStream returs a stream instance
15 | func NewLocalStream(From string, To string) *Stream {
16 | return &Stream{
17 | From: From,
18 | To: To,
19 | ReceiveChan: make(chan *network.SyncMessage),
20 | }
21 | }
22 |
23 | // Read implementation
24 | func (s *Stream) Read(p []byte) (n int, err error) {
25 | panic("implement")
26 | }
27 |
28 | // WriteSynMsg implementation
29 | func (s *Stream) WriteSynMsg(msg *network.SyncMessage) (n int, err error) {
30 | s.ReceiveChan <- msg
31 | return 0, nil
32 | }
33 |
34 | // Write implementation
35 | func (s *Stream) Write(p []byte) (n int, err error) {
36 | panic("implement")
37 | }
38 |
39 | // Close implementation
40 | func (s *Stream) Close() error {
41 | panic("implement")
42 | }
43 |
44 | // CloseWrite implementation
45 | func (s *Stream) CloseWrite() error {
46 | panic("implement")
47 | }
48 |
49 | // RemotePeer implementation
50 | func (s *Stream) RemotePeer() string {
51 | panic("implement")
52 | }
53 |
--------------------------------------------------------------------------------
/network/msgqueue/indexes.go:
--------------------------------------------------------------------------------
1 | package msgqueue
2 |
3 | import (
4 | "encoding/hex"
5 | "fmt"
6 | "github.com/bloxapp/ssv/network"
7 | )
8 |
9 | // IBFTRoundIndexKey is the ibft index key
10 | func IBFTRoundIndexKey(lambda []byte, round uint64) string {
11 | return fmt.Sprintf("lambda_%s_round_%d", hex.EncodeToString(lambda), round)
12 | }
13 | func iBFTMessageIndex() IndexFunc {
14 | return func(msg *network.Message) []string {
15 | if msg.Type == network.NetworkMsg_IBFTType {
16 | return []string{
17 | IBFTRoundIndexKey(msg.Lambda, msg.SignedMessage.Message.Round),
18 | }
19 | }
20 | return []string{}
21 | }
22 | }
23 |
24 | // SigRoundIndexKey is the SSV node signature collection index key
25 | func SigRoundIndexKey(lambda []byte) string {
26 | return fmt.Sprintf("sig_lambda_%s", hex.EncodeToString(lambda))
27 | }
28 | func sigMessageIndex() IndexFunc {
29 | return func(msg *network.Message) []string {
30 | if msg.Type == network.NetworkMsg_SignatureType {
31 | return []string{
32 | SigRoundIndexKey(msg.Lambda),
33 | }
34 | }
35 | return []string{}
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/network/msgqueue/message_queue.go:
--------------------------------------------------------------------------------
1 | package msgqueue
2 |
3 | import (
4 | "github.com/bloxapp/ssv/network"
5 | "github.com/pborman/uuid"
6 | "sync"
7 | )
8 |
9 | // IndexFunc is the function that indexes messages to be later pulled by those indexes
10 | type IndexFunc func(msg *network.Message) []string
11 |
12 | type messageContainer struct {
13 | id string
14 | msg *network.Message
15 | indexes []string
16 | }
17 |
18 | // MessageQueue is a broker of messages for the IBFT instance to process.
19 | // Messages can come in various times, even next round's messages can come "early" as other nodes can change round before this node.
20 | // To solve this issue we have a message broker from which the instance pulls new messages, this also reduces concurrency issues as the instance is now single threaded.
21 | // The message queue has internal logic to organize messages by their round.
22 | type MessageQueue struct {
23 | msgMutex sync.Mutex
24 | indexFuncs []IndexFunc
25 | queue map[string][]*messageContainer // = map[index][messageContainer.id]messageContainer
26 | }
27 |
28 | // New is the constructor of MessageQueue
29 | func New() *MessageQueue {
30 | return &MessageQueue{
31 | msgMutex: sync.Mutex{},
32 | queue: make(map[string][]*messageContainer),
33 | indexFuncs: []IndexFunc{
34 | iBFTMessageIndex(),
35 | sigMessageIndex(),
36 | },
37 | }
38 | }
39 |
40 | // AddIndexFunc adds an index function that will be activated every new message the queue receives
41 | func (q *MessageQueue) AddIndexFunc(f IndexFunc) {
42 | q.indexFuncs = append(q.indexFuncs, f)
43 | }
44 |
45 | // AddMessage adds a message the queue based on the message round.
46 | // AddMessage is thread safe
47 | func (q *MessageQueue) AddMessage(msg *network.Message) {
48 | q.msgMutex.Lock()
49 | defer q.msgMutex.Unlock()
50 |
51 | // index msg
52 | indexes := make([]string, 0)
53 | for _, f := range q.indexFuncs {
54 | indexes = append(indexes, f(msg)...)
55 | }
56 |
57 | // add it to queue
58 | msgContainer := &messageContainer{
59 | id: uuid.New(),
60 | msg: msg,
61 | indexes: indexes,
62 | }
63 |
64 | for _, idx := range indexes {
65 | if q.queue[idx] == nil {
66 | q.queue[idx] = make([]*messageContainer, 0)
67 | }
68 | q.queue[idx] = append(q.queue[idx], msgContainer)
69 | }
70 | }
71 |
72 | // PopMessage will return a message by its index if found, will also delete all other index occurrences of that message
73 | func (q *MessageQueue) PopMessage(index string) *network.Message {
74 | q.msgMutex.Lock()
75 | defer q.msgMutex.Unlock()
76 |
77 | var ret *network.Message
78 | if len(q.queue[index]) > 0 {
79 | c := q.queue[index][0]
80 | ret = c.msg
81 |
82 | // delete all indexes
83 | q.deleteMessageFromAllIndexes(c.indexes, c.id)
84 | }
85 | return ret
86 | }
87 |
88 | // MsgCount will return a count of messages by their index
89 | func (q *MessageQueue) MsgCount(index string) int {
90 | return len(q.queue[index])
91 | }
92 |
93 | func (q *MessageQueue) deleteMessageFromAllIndexes(indexes []string, id string) {
94 | for _, indx := range indexes {
95 | newIndexQ := make([]*messageContainer, 0)
96 | for _, msg := range q.queue[indx] {
97 | if msg.id != id {
98 | newIndexQ = append(newIndexQ, msg)
99 | }
100 | }
101 | q.queue[indx] = newIndexQ
102 | }
103 | }
104 |
105 | // PurgeIndexedMessages will delete all indexed messages for the given index
106 | func (q *MessageQueue) PurgeIndexedMessages(index string) {
107 | q.msgMutex.Lock()
108 | defer q.msgMutex.Unlock()
109 |
110 | q.queue[index] = make([]*messageContainer, 0)
111 | }
112 |
--------------------------------------------------------------------------------
/network/msgqueue/message_queue_test.go:
--------------------------------------------------------------------------------
1 | package msgqueue
2 |
3 | import (
4 | "github.com/bloxapp/ssv/ibft/proto"
5 | "github.com/bloxapp/ssv/network"
6 | "github.com/stretchr/testify/require"
7 | "testing"
8 | )
9 |
10 | func TestMessageQueue_PurgeAllIndexedMessages(t *testing.T) {
11 | msgQ := New()
12 | msgQ.AddMessage(&network.Message{
13 | Lambda: []byte{1, 2, 3, 4},
14 | SignedMessage: &proto.SignedMessage{
15 | Message: &proto.Message{
16 | Round: 1,
17 | },
18 | },
19 | Type: network.NetworkMsg_IBFTType,
20 | })
21 | msgQ.AddMessage(&network.Message{
22 | Lambda: []byte{1, 2, 3, 4},
23 | SignedMessage: &proto.SignedMessage{
24 | Message: &proto.Message{
25 | Round: 1,
26 | },
27 | },
28 | Type: network.NetworkMsg_SignatureType,
29 | })
30 |
31 | require.Len(t, msgQ.queue["lambda_01020304_round_1"], 1)
32 | require.Len(t, msgQ.queue["sig_lambda_01020304"], 1)
33 |
34 | msgQ.PurgeIndexedMessages(IBFTRoundIndexKey([]byte{1, 2, 3, 4}, 1))
35 | require.Len(t, msgQ.queue["lambda_01020304_round_1"], 0)
36 | require.Len(t, msgQ.queue["sig_lambda_01020304"], 1)
37 |
38 | msgQ.PurgeIndexedMessages(SigRoundIndexKey([]byte{1, 2, 3, 4}))
39 | require.Len(t, msgQ.queue["lambda_01020304_round_1"], 0)
40 | require.Len(t, msgQ.queue["sig_lambda_01020304"], 0)
41 | }
42 |
43 | func TestMessageQueue_AddMessage(t *testing.T) {
44 | msgQ := New()
45 | msgQ.AddMessage(&network.Message{
46 | Lambda: []byte{1, 2, 3, 4},
47 | SignedMessage: &proto.SignedMessage{
48 | Message: &proto.Message{
49 | Round: 1,
50 | },
51 | },
52 | Type: network.NetworkMsg_IBFTType,
53 | })
54 | require.NotNil(t, msgQ.queue["lambda_01020304_round_1"])
55 |
56 | msgQ.AddMessage(&network.Message{
57 | Lambda: []byte{1, 2, 3, 5},
58 | SignedMessage: &proto.SignedMessage{
59 | Message: &proto.Message{
60 | Round: 7,
61 | },
62 | },
63 | Type: network.NetworkMsg_IBFTType,
64 | })
65 | require.NotNil(t, msgQ.queue["lambda_01020305_round_7"])
66 |
67 | // custom index
68 | msgQ.indexFuncs = append(msgQ.indexFuncs, func(msg *network.Message) []string {
69 | return []string{"a", "b", "c"}
70 | })
71 | msgQ.AddMessage(&network.Message{
72 | Lambda: []byte{1, 2, 3, 5},
73 | SignedMessage: &proto.SignedMessage{
74 | Message: &proto.Message{
75 | Round: 3,
76 | },
77 | },
78 | Type: network.NetworkMsg_IBFTType,
79 | })
80 |
81 | require.NotNil(t, msgQ.queue["a"])
82 | require.NotNil(t, msgQ.queue["b"])
83 | require.NotNil(t, msgQ.queue["c"])
84 | require.Nil(t, msgQ.PopMessage("d"))
85 | }
86 |
87 | func TestMessageQueue_PopMessage(t *testing.T) {
88 | msgQ := New()
89 | msgQ.indexFuncs = []IndexFunc{
90 | func(msg *network.Message) []string {
91 | return []string{"a", "b", "c"}
92 | },
93 | }
94 | msgQ.AddMessage(&network.Message{
95 | Lambda: []byte{1, 2, 3, 4},
96 | SignedMessage: &proto.SignedMessage{
97 | Message: &proto.Message{
98 | Round: 1,
99 | },
100 | },
101 | Type: network.NetworkMsg_IBFTType,
102 | })
103 |
104 | require.NotNil(t, msgQ.PopMessage("a"))
105 | require.Nil(t, msgQ.PopMessage("a"))
106 | require.Nil(t, msgQ.PopMessage("b"))
107 | require.Nil(t, msgQ.PopMessage("c"))
108 | }
109 |
--------------------------------------------------------------------------------
/network/network.go:
--------------------------------------------------------------------------------
1 | package network
2 |
3 | import (
4 | "github.com/bloxapp/ssv/ibft/proto"
5 | "github.com/herumi/bls-eth-go-binary/bls"
6 | "io"
7 | )
8 |
9 | // SyncChanObj is a wrapper object for streaming of sync messages
10 | type SyncChanObj struct {
11 | Msg *SyncMessage
12 | Stream SyncStream
13 | }
14 |
15 | // SyncStream is a interface for all stream related functions for the sync process.
16 | type SyncStream interface {
17 | io.Reader
18 | io.Writer
19 | io.Closer
20 |
21 | // CloseWrite closes the stream for writing but leaves it open for
22 | // reading.
23 | //
24 | // CloseWrite does not free the stream, users must still call Close or
25 | // Reset.
26 | CloseWrite() error
27 |
28 | // RemotePeer returns a string identifier of the remote peer connected to this stream
29 | RemotePeer() string
30 | }
31 |
32 | // Network represents the behavior of the network
33 | type Network interface {
34 | // Broadcast propagates a signed message to all peers
35 | Broadcast(msg *proto.SignedMessage) error
36 |
37 | // ReceivedMsgChan is a channel that forwards new propagated messages to a subscriber
38 | ReceivedMsgChan() <-chan *proto.SignedMessage
39 |
40 | // BroadcastSignature broadcasts the given signature for the given lambda
41 | BroadcastSignature(msg *proto.SignedMessage) error
42 |
43 | // ReceivedSignatureChan returns the channel with signatures
44 | ReceivedSignatureChan() <-chan *proto.SignedMessage
45 |
46 | // BroadcastDecided broadcasts a decided instance with collected signatures
47 | BroadcastDecided(msg *proto.SignedMessage) error
48 |
49 | // ReceivedDecidedChan returns the channel for decided messages
50 | ReceivedDecidedChan() <-chan *proto.SignedMessage
51 |
52 | // GetHighestDecidedInstance sends a highest decided request to peers and returns answers.
53 | // If peer list is nil, broadcasts to all.
54 | GetHighestDecidedInstance(peerID string, msg *SyncMessage) (*SyncMessage, error)
55 |
56 | // RespondToHighestDecidedInstance responds to a GetHighestDecidedInstance
57 | RespondToHighestDecidedInstance(stream SyncStream, msg *SyncMessage) error
58 |
59 | // GetDecidedByRange returns a list of decided signed messages up to 25 in a batch.
60 | GetDecidedByRange(peerID string, msg *SyncMessage) (*SyncMessage, error)
61 |
62 | // RespondToGetDecidedByRange responds to a GetDecidedByRange
63 | RespondToGetDecidedByRange(stream SyncStream, msg *SyncMessage) error
64 |
65 | // ReceivedSyncMsgChan returns the channel for sync messages
66 | ReceivedSyncMsgChan() <-chan *SyncChanObj
67 |
68 | // SubscribeToValidatorNetwork subscribing and listen to validator network
69 | SubscribeToValidatorNetwork(validatorPk *bls.PublicKey) error
70 |
71 | // AllPeers returns all connected peers for a validator PK
72 | AllPeers(validatorPk []byte) ([]string, error)
73 | }
74 |
--------------------------------------------------------------------------------
/network/network_msgs.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package network;
4 |
5 | option go_package = "github.com/bloxapp/ssv/network";
6 |
7 | import "msgs.proto";
8 |
9 | enum NetworkMsg {
10 | // IBFTType are all iBFT related messages
11 | IBFTType = 0;
12 | // DecidedType is an iBFT specific message for broadcasting post consensus decided message with signatures
13 | DecidedType = 1;
14 | // SignatureType is an SSV node specific message for broadcasting post consensus signatures on eth2 duties
15 | SignatureType = 2;
16 | // SyncType is an SSV iBFT specific message that a node uses to sync up with other nodes
17 | SyncType = 3;
18 | }
19 |
20 | enum Sync {
21 | // GetHighestType is a request from peers to return the highest decided/ prepared instance they know of
22 | GetHighestType = 0;
23 | // GetInstanceRange is a request from peers to return instances and their decided/ prepared justifications
24 | GetInstanceRange = 1;
25 | }
26 |
27 | message SyncMessage {
28 | repeated proto.SignedMessage SignedMessages = 1;
29 | string FromPeerID = 2;
30 | repeated uint64 params = 3;
31 | bytes validator_pk = 4;
32 | Sync Type = 5;
33 | }
34 |
35 | // Message is a wrapper struct for all network message types
36 | message Message {
37 | bytes Lambda = 1;
38 | proto.SignedMessage SignedMessage = 2;
39 | SyncMessage SyncMessage = 3;
40 | NetworkMsg Type = 4;
41 | }
--------------------------------------------------------------------------------
/network/p2p/config.go:
--------------------------------------------------------------------------------
1 | package p2p
2 |
3 | import (
4 | "github.com/libp2p/go-libp2p-core/peer"
5 | pubsub "github.com/libp2p/go-libp2p-pubsub"
6 | "time"
7 | )
8 |
9 | // Config - describe the config options for p2p network
10 | type Config struct {
11 | DiscoveryType string
12 | BootstrapNodeAddr []string
13 | Discv5BootStrapAddr []string
14 | UDPPort int
15 | TCPPort int
16 | HostAddress string
17 | HostDNS string
18 | HostID peer.ID
19 | Topics map[string]*pubsub.Topic
20 | Subs []*pubsub.Subscription
21 |
22 | // params
23 | MaxBatchResponse uint64 // maximum number of returned objects in a batch
24 | RequestTimeout time.Duration
25 | }
26 |
--------------------------------------------------------------------------------
/network/p2p/p2p_decided.go:
--------------------------------------------------------------------------------
1 | package p2p
2 |
3 | import (
4 | "encoding/json"
5 | "github.com/bloxapp/ssv/ibft/proto"
6 | "github.com/bloxapp/ssv/network"
7 | "github.com/pkg/errors"
8 | "go.uber.org/zap"
9 | )
10 |
11 | // BroadcastDecided broadcasts a decided instance with collected signatures
12 | func (n *p2pNetwork) BroadcastDecided(msg *proto.SignedMessage) error {
13 | msgBytes, err := json.Marshal(network.Message{
14 | Lambda: msg.Message.Lambda,
15 | SignedMessage: msg,
16 | Type: network.NetworkMsg_DecidedType,
17 | })
18 | if err != nil {
19 | return errors.Wrap(err, "failed to marshal message")
20 | }
21 |
22 | topic, err := n.getTopic(msg.Message.GetValidatorPk())
23 | if err != nil {
24 | return errors.Wrap(err, "failed to get topic")
25 | }
26 |
27 | n.logger.Debug("Broadcasting to topic", zap.Any("topic", topic), zap.Any("peers", topic.ListPeers()))
28 | return topic.Publish(n.ctx, msgBytes)
29 | }
30 |
31 | // ReceivedDecidedChan returns the channel for decided messages
32 | func (n *p2pNetwork) ReceivedDecidedChan() <-chan *proto.SignedMessage {
33 | ls := listener{
34 | decidedCh: make(chan *proto.SignedMessage, MsgChanSize),
35 | }
36 |
37 | n.listenersLock.Lock()
38 | n.listeners = append(n.listeners, ls)
39 | n.listenersLock.Unlock()
40 |
41 | return ls.decidedCh
42 | }
43 |
--------------------------------------------------------------------------------
/network/p2p/p2p_ibft.go:
--------------------------------------------------------------------------------
1 | package p2p
2 |
3 | import (
4 | "encoding/json"
5 | "github.com/bloxapp/ssv/ibft/proto"
6 | "github.com/bloxapp/ssv/network"
7 | "github.com/pkg/errors"
8 | "go.uber.org/zap"
9 | )
10 |
11 | // Broadcast propagates a signed message to all peers
12 | func (n *p2pNetwork) Broadcast(msg *proto.SignedMessage) error {
13 | msgBytes, err := json.Marshal(network.Message{
14 | Lambda: msg.Message.Lambda,
15 | SignedMessage: msg,
16 | Type: network.NetworkMsg_IBFTType,
17 | })
18 | if err != nil {
19 | return errors.Wrap(err, "failed to marshal message")
20 | }
21 |
22 | topic, err := n.getTopic(msg.Message.GetValidatorPk())
23 | if err != nil {
24 | return errors.Wrap(err, "failed to get topic")
25 | }
26 |
27 | n.logger.Debug("Broadcasting to topic", zap.Any("topic", topic), zap.Any("peers", topic.ListPeers()))
28 | return topic.Publish(n.ctx, msgBytes)
29 | }
30 |
31 | // ReceivedMsgChan return a channel with messages
32 | func (n *p2pNetwork) ReceivedMsgChan() <-chan *proto.SignedMessage {
33 | ls := listener{
34 | msgCh: make(chan *proto.SignedMessage, MsgChanSize),
35 | }
36 |
37 | n.listenersLock.Lock()
38 | n.listeners = append(n.listeners, ls)
39 | n.listenersLock.Unlock()
40 |
41 | return ls.msgCh
42 | }
43 |
--------------------------------------------------------------------------------
/network/p2p/p2p_signatures.go:
--------------------------------------------------------------------------------
1 | package p2p
2 |
3 | import (
4 | "encoding/json"
5 | "github.com/bloxapp/ssv/ibft/proto"
6 | "github.com/bloxapp/ssv/network"
7 | "github.com/pkg/errors"
8 | "go.uber.org/zap"
9 | )
10 |
11 | // BroadcastSignature broadcasts the given signature for the given lambda
12 | func (n *p2pNetwork) BroadcastSignature(msg *proto.SignedMessage) error {
13 | msgBytes, err := json.Marshal(network.Message{
14 | Lambda: msg.Message.Lambda,
15 | SignedMessage: msg,
16 | Type: network.NetworkMsg_SignatureType,
17 | })
18 | if err != nil {
19 | return errors.Wrap(err, "failed to marshal message")
20 | }
21 | topic, err := n.getTopic(msg.Message.GetValidatorPk())
22 | if err != nil {
23 | return errors.Wrap(err, "failed to get topic")
24 | }
25 |
26 | n.logger.Debug("Broadcasting to topic", zap.Any("topic", topic), zap.Any("peers", topic.ListPeers()))
27 | return topic.Publish(n.ctx, msgBytes)
28 | }
29 |
30 | // ReceivedSignatureChan returns the channel with signatures
31 | func (n *p2pNetwork) ReceivedSignatureChan() <-chan *proto.SignedMessage {
32 | ls := listener{
33 | sigCh: make(chan *proto.SignedMessage, MsgChanSize),
34 | }
35 |
36 | n.listenersLock.Lock()
37 | n.listeners = append(n.listeners, ls)
38 | n.listenersLock.Unlock()
39 |
40 | return ls.sigCh
41 | }
42 |
--------------------------------------------------------------------------------
/network/p2p/p2p_stream.go:
--------------------------------------------------------------------------------
1 | package p2p
2 |
3 | import (
4 | "encoding/json"
5 | "github.com/bloxapp/ssv/network"
6 | core "github.com/libp2p/go-libp2p-core"
7 | "go.uber.org/zap"
8 | "io/ioutil"
9 | )
10 |
11 | func readMessageData(stream network.SyncStream) (*network.Message, error) {
12 | data := &network.Message{}
13 | buf, err := ioutil.ReadAll(stream)
14 | if err != nil {
15 | return nil, err
16 | }
17 |
18 | // unmarshal
19 | if err := json.Unmarshal(buf, data); err != nil {
20 | return nil, err
21 | }
22 | return data, nil
23 | }
24 |
25 | // SyncStream is a wrapper struct for the core.Stream interface to match the network.SyncStream interface
26 | type SyncStream struct {
27 | stream core.Stream
28 | }
29 |
30 | // Read reads data to p
31 | func (s *SyncStream) Read(p []byte) (n int, err error) {
32 | return s.stream.Read(p)
33 | }
34 |
35 | // Write writes p to stream
36 | func (s *SyncStream) Write(p []byte) (n int, err error) {
37 | return s.stream.Write(p)
38 | }
39 |
40 | // Close closes the stream
41 | func (s *SyncStream) Close() error {
42 | return s.stream.Close()
43 | }
44 |
45 | // CloseWrite closes write stream
46 | func (s *SyncStream) CloseWrite() error {
47 | return s.stream.CloseWrite()
48 | }
49 |
50 | // RemotePeer returns connected peer
51 | func (s *SyncStream) RemotePeer() string {
52 | return s.stream.Conn().RemotePeer().String()
53 | }
54 |
55 | // handleStream sets a stream handler for the host to process streamed messages
56 | func (n *p2pNetwork) handleStream() {
57 | n.host.SetStreamHandler(syncStreamProtocol, func(stream core.Stream) {
58 | netSyncStream := &SyncStream{stream: stream}
59 | cm, err := readMessageData(netSyncStream)
60 | if err != nil {
61 | n.logger.Error("could not read and parse stream", zap.Error(err))
62 | return
63 | }
64 |
65 | // send to listeners
66 | for _, ls := range n.listeners {
67 | go func(ls listener) {
68 | switch cm.Type {
69 | case network.NetworkMsg_SyncType:
70 | cm.SyncMessage.FromPeerID = stream.Conn().RemotePeer().String()
71 | ls.syncCh <- &network.SyncChanObj{Msg: cm.SyncMessage, Stream: netSyncStream}
72 | }
73 | }(ls)
74 | }
75 | })
76 | }
77 |
--------------------------------------------------------------------------------
/network/p2p/p2p_test.go:
--------------------------------------------------------------------------------
1 | package p2p
2 |
3 | import (
4 | "context"
5 | "github.com/herumi/bls-eth-go-binary/bls"
6 | "testing"
7 | "time"
8 |
9 | "github.com/stretchr/testify/require"
10 | "go.uber.org/zap/zaptest"
11 |
12 | "github.com/bloxapp/ssv/ibft/proto"
13 | )
14 |
15 | func TestP2PNetworker(t *testing.T) {
16 | logger := zaptest.NewLogger(t)
17 |
18 | peer1, err := New(context.Background(), logger, &Config{
19 | DiscoveryType: "mdns",
20 | BootstrapNodeAddr: []string{"enr:-LK4QMIAfHA47rJnVBaGeoHwXOrXcCNvUaxFiDEE2VPCxQ40cu_k2hZsGP6sX9xIQgiVnI72uxBBN7pOQCo5d9izhkcBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpD1pf1CAAAAAP__________gmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQJu41tZ3K8fb60in7AarjEP_i2zv35My_XW_D_t6Y1fJ4N0Y3CCE4iDdWRwgg-g"},
21 | UDPPort: 12000,
22 | TCPPort: 13000,
23 | })
24 | require.NoError(t, err)
25 |
26 | peer2, err := New(context.Background(), logger, &Config{
27 | DiscoveryType: "mdns",
28 | BootstrapNodeAddr: []string{"enr:-LK4QMIAfHA47rJnVBaGeoHwXOrXcCNvUaxFiDEE2VPCxQ40cu_k2hZsGP6sX9xIQgiVnI72uxBBN7pOQCo5d9izhkcBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpD1pf1CAAAAAP__________gmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQJu41tZ3K8fb60in7AarjEP_i2zv35My_XW_D_t6Y1fJ4N0Y3CCE4iDdWRwgg-g"},
29 | UDPPort: 12001,
30 | TCPPort: 13001,
31 | })
32 | require.NoError(t, err)
33 |
34 | pk := &bls.PublicKey{}
35 | require.NoError(t, pk.Deserialize(refPk))
36 | require.NoError(t, peer1.SubscribeToValidatorNetwork(pk))
37 | require.NoError(t, peer2.SubscribeToValidatorNetwork(pk))
38 |
39 | lambda := []byte("test-lambda")
40 | messageToBroadcast := &proto.SignedMessage{
41 | Message: &proto.Message{
42 | Type: proto.RoundState_PrePrepare,
43 | Round: 1,
44 | Lambda: lambda,
45 | Value: []byte("test-value"),
46 | ValidatorPk: refPk,
47 | },
48 | }
49 |
50 | time.Sleep(time.Second)
51 |
52 | peer1Chan := peer1.ReceivedMsgChan()
53 | peer2Chan := peer2.ReceivedMsgChan()
54 |
55 | time.Sleep(time.Second)
56 |
57 | err = peer1.Broadcast(messageToBroadcast)
58 | require.NoError(t, err)
59 |
60 | time.Sleep(time.Second)
61 |
62 | t.Run("peer 1 receives message", func(t *testing.T) {
63 | msgFromPeer1 := <-peer1Chan
64 | require.Equal(t, messageToBroadcast, msgFromPeer1)
65 | })
66 |
67 | t.Run("peer 2 receives message", func(t *testing.T) {
68 | msgFromPeer2 := <-peer2Chan
69 | require.Equal(t, messageToBroadcast, msgFromPeer2)
70 | })
71 | }
72 |
--------------------------------------------------------------------------------
/network/p2p/test_utils.go:
--------------------------------------------------------------------------------
1 | package p2p
2 |
3 | import (
4 | "github.com/bloxapp/ssv/fixtures"
5 | "github.com/bloxapp/ssv/ibft/proto"
6 | "github.com/bloxapp/ssv/storage/collections"
7 | "github.com/herumi/bls-eth-go-binary/bls"
8 | )
9 |
10 | var (
11 | refPk = fixtures.RefPk
12 | )
13 |
14 | func validators() []*collections.ValidatorShare {
15 | //pk := &bls.PublicKey{}
16 | //pk.Deserialize(refPk)
17 | return []*collections.ValidatorShare{
18 | {
19 | NodeID: 1,
20 | ValidatorPK: nil,
21 | ShareKey: nil,
22 | Committee: nil,
23 | },
24 | }
25 | }
26 |
27 | // TestValidatorStorage implementation
28 | type TestValidatorStorage struct {
29 | }
30 |
31 | // LoadFromConfig implementation
32 | func (v *TestValidatorStorage) LoadFromConfig(nodeID uint64, pubKey *bls.PublicKey, shareKey *bls.SecretKey, ibftCommittee map[uint64]*proto.Node) error {
33 | return nil
34 | }
35 | // SaveValidatorShare implementation
36 | func (v *TestValidatorStorage) SaveValidatorShare(validator *collections.ValidatorShare) error {
37 | return nil
38 | }
39 | // GetAllValidatorsShare implementation
40 | func (v *TestValidatorStorage) GetAllValidatorsShare() ([]*collections.ValidatorShare, error) {
41 | return validators(), nil
42 | }
43 |
--------------------------------------------------------------------------------
/node/valcheck/attestation.go:
--------------------------------------------------------------------------------
1 | package valcheck
2 |
3 | import (
4 | "encoding/json"
5 | "github.com/bloxapp/ssv/ibft/proto"
6 | "github.com/pkg/errors"
7 | )
8 |
9 | // AttestationValueCheck checks for an Attestation type value
10 | type AttestationValueCheck struct {
11 | }
12 |
13 | // Check returns error if value is invalid
14 | func (v *AttestationValueCheck) Check(value []byte) error {
15 | // try and parse to attestation data
16 | inputValue := &proto.InputValue_Attestation{}
17 | if err := json.Unmarshal(value, &inputValue); err != nil {
18 | return errors.Wrap(err, "could not parse input value storing attestation data")
19 | }
20 |
21 | if inputValue.Attestation.Data.Slot == 0 {
22 | return errors.New("this is an example test error")
23 | }
24 |
25 | // TODO - test for slashing protection
26 | return nil
27 | }
28 |
--------------------------------------------------------------------------------
/phase_1_testnet/919be6832b27567a7ee2792417dfe27f9c2263a763ca600ab395f74e05187435b16418d825da490fed83115e19365e50/node1.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ethereum/ssv/bd3ccbed4786b2be6842a1ff0cc3cc3507c382e1/phase_1_testnet/919be6832b27567a7ee2792417dfe27f9c2263a763ca600ab395f74e05187435b16418d825da490fed83115e19365e50/node1.zip
--------------------------------------------------------------------------------
/phase_1_testnet/919be6832b27567a7ee2792417dfe27f9c2263a763ca600ab395f74e05187435b16418d825da490fed83115e19365e50/node2.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ethereum/ssv/bd3ccbed4786b2be6842a1ff0cc3cc3507c382e1/phase_1_testnet/919be6832b27567a7ee2792417dfe27f9c2263a763ca600ab395f74e05187435b16418d825da490fed83115e19365e50/node2.zip
--------------------------------------------------------------------------------
/phase_1_testnet/919be6832b27567a7ee2792417dfe27f9c2263a763ca600ab395f74e05187435b16418d825da490fed83115e19365e50/node3.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ethereum/ssv/bd3ccbed4786b2be6842a1ff0cc3cc3507c382e1/phase_1_testnet/919be6832b27567a7ee2792417dfe27f9c2263a763ca600ab395f74e05187435b16418d825da490fed83115e19365e50/node3.zip
--------------------------------------------------------------------------------
/phase_1_testnet/919be6832b27567a7ee2792417dfe27f9c2263a763ca600ab395f74e05187435b16418d825da490fed83115e19365e50/node4.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ethereum/ssv/bd3ccbed4786b2be6842a1ff0cc3cc3507c382e1/phase_1_testnet/919be6832b27567a7ee2792417dfe27f9c2263a763ca600ab395f74e05187435b16418d825da490fed83115e19365e50/node4.zip
--------------------------------------------------------------------------------
/phase_1_testnet/99d8485216f6a37372a294d51f85d85bfca4b6c3201cbd389a1cdc62565f12f4ee5c491575fd85b6faa3b86eafedce57/node1.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ethereum/ssv/bd3ccbed4786b2be6842a1ff0cc3cc3507c382e1/phase_1_testnet/99d8485216f6a37372a294d51f85d85bfca4b6c3201cbd389a1cdc62565f12f4ee5c491575fd85b6faa3b86eafedce57/node1.zip
--------------------------------------------------------------------------------
/phase_1_testnet/99d8485216f6a37372a294d51f85d85bfca4b6c3201cbd389a1cdc62565f12f4ee5c491575fd85b6faa3b86eafedce57/node2.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ethereum/ssv/bd3ccbed4786b2be6842a1ff0cc3cc3507c382e1/phase_1_testnet/99d8485216f6a37372a294d51f85d85bfca4b6c3201cbd389a1cdc62565f12f4ee5c491575fd85b6faa3b86eafedce57/node2.zip
--------------------------------------------------------------------------------
/phase_1_testnet/99d8485216f6a37372a294d51f85d85bfca4b6c3201cbd389a1cdc62565f12f4ee5c491575fd85b6faa3b86eafedce57/node3.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ethereum/ssv/bd3ccbed4786b2be6842a1ff0cc3cc3507c382e1/phase_1_testnet/99d8485216f6a37372a294d51f85d85bfca4b6c3201cbd389a1cdc62565f12f4ee5c491575fd85b6faa3b86eafedce57/node3.zip
--------------------------------------------------------------------------------
/phase_1_testnet/99d8485216f6a37372a294d51f85d85bfca4b6c3201cbd389a1cdc62565f12f4ee5c491575fd85b6faa3b86eafedce57/node4.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ethereum/ssv/bd3ccbed4786b2be6842a1ff0cc3cc3507c382e1/phase_1_testnet/99d8485216f6a37372a294d51f85d85bfca4b6c3201cbd389a1cdc62565f12f4ee5c491575fd85b6faa3b86eafedce57/node4.zip
--------------------------------------------------------------------------------
/phase_1_testnet/a106c0ab76a728fba808276e43f896a853fe4653860dc5a40d1b096cb95e9ffdf85975aabff19214f32a5cefbab54113/node1.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ethereum/ssv/bd3ccbed4786b2be6842a1ff0cc3cc3507c382e1/phase_1_testnet/a106c0ab76a728fba808276e43f896a853fe4653860dc5a40d1b096cb95e9ffdf85975aabff19214f32a5cefbab54113/node1.zip
--------------------------------------------------------------------------------
/phase_1_testnet/a106c0ab76a728fba808276e43f896a853fe4653860dc5a40d1b096cb95e9ffdf85975aabff19214f32a5cefbab54113/node2.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ethereum/ssv/bd3ccbed4786b2be6842a1ff0cc3cc3507c382e1/phase_1_testnet/a106c0ab76a728fba808276e43f896a853fe4653860dc5a40d1b096cb95e9ffdf85975aabff19214f32a5cefbab54113/node2.zip
--------------------------------------------------------------------------------
/phase_1_testnet/a106c0ab76a728fba808276e43f896a853fe4653860dc5a40d1b096cb95e9ffdf85975aabff19214f32a5cefbab54113/node3.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ethereum/ssv/bd3ccbed4786b2be6842a1ff0cc3cc3507c382e1/phase_1_testnet/a106c0ab76a728fba808276e43f896a853fe4653860dc5a40d1b096cb95e9ffdf85975aabff19214f32a5cefbab54113/node3.zip
--------------------------------------------------------------------------------
/phase_1_testnet/a106c0ab76a728fba808276e43f896a853fe4653860dc5a40d1b096cb95e9ffdf85975aabff19214f32a5cefbab54113/node4.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ethereum/ssv/bd3ccbed4786b2be6842a1ff0cc3cc3507c382e1/phase_1_testnet/a106c0ab76a728fba808276e43f896a853fe4653860dc5a40d1b096cb95e9ffdf85975aabff19214f32a5cefbab54113/node4.zip
--------------------------------------------------------------------------------
/phase_1_testnet/aa96176258df64d1a83c9a1669b3f119065df40f7b7b6f1e22b3e3b1ddec7dc890698b11d0f5a293494d8813c6aa149c/node1.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ethereum/ssv/bd3ccbed4786b2be6842a1ff0cc3cc3507c382e1/phase_1_testnet/aa96176258df64d1a83c9a1669b3f119065df40f7b7b6f1e22b3e3b1ddec7dc890698b11d0f5a293494d8813c6aa149c/node1.zip
--------------------------------------------------------------------------------
/phase_1_testnet/aa96176258df64d1a83c9a1669b3f119065df40f7b7b6f1e22b3e3b1ddec7dc890698b11d0f5a293494d8813c6aa149c/node2.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ethereum/ssv/bd3ccbed4786b2be6842a1ff0cc3cc3507c382e1/phase_1_testnet/aa96176258df64d1a83c9a1669b3f119065df40f7b7b6f1e22b3e3b1ddec7dc890698b11d0f5a293494d8813c6aa149c/node2.zip
--------------------------------------------------------------------------------
/phase_1_testnet/aa96176258df64d1a83c9a1669b3f119065df40f7b7b6f1e22b3e3b1ddec7dc890698b11d0f5a293494d8813c6aa149c/node3.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ethereum/ssv/bd3ccbed4786b2be6842a1ff0cc3cc3507c382e1/phase_1_testnet/aa96176258df64d1a83c9a1669b3f119065df40f7b7b6f1e22b3e3b1ddec7dc890698b11d0f5a293494d8813c6aa149c/node3.zip
--------------------------------------------------------------------------------
/phase_1_testnet/aa96176258df64d1a83c9a1669b3f119065df40f7b7b6f1e22b3e3b1ddec7dc890698b11d0f5a293494d8813c6aa149c/node4.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ethereum/ssv/bd3ccbed4786b2be6842a1ff0cc3cc3507c382e1/phase_1_testnet/aa96176258df64d1a83c9a1669b3f119065df40f7b7b6f1e22b3e3b1ddec7dc890698b11d0f5a293494d8813c6aa149c/node4.zip
--------------------------------------------------------------------------------
/phase_1_testnet/b3119aa267189ba9069cbb93900b70f0b837ed944334f92122d95b72eb1791ccd890c9396aea890f29b9e97b5ff7b13f/node1.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ethereum/ssv/bd3ccbed4786b2be6842a1ff0cc3cc3507c382e1/phase_1_testnet/b3119aa267189ba9069cbb93900b70f0b837ed944334f92122d95b72eb1791ccd890c9396aea890f29b9e97b5ff7b13f/node1.zip
--------------------------------------------------------------------------------
/phase_1_testnet/b3119aa267189ba9069cbb93900b70f0b837ed944334f92122d95b72eb1791ccd890c9396aea890f29b9e97b5ff7b13f/node2.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ethereum/ssv/bd3ccbed4786b2be6842a1ff0cc3cc3507c382e1/phase_1_testnet/b3119aa267189ba9069cbb93900b70f0b837ed944334f92122d95b72eb1791ccd890c9396aea890f29b9e97b5ff7b13f/node2.zip
--------------------------------------------------------------------------------
/phase_1_testnet/b3119aa267189ba9069cbb93900b70f0b837ed944334f92122d95b72eb1791ccd890c9396aea890f29b9e97b5ff7b13f/node3.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ethereum/ssv/bd3ccbed4786b2be6842a1ff0cc3cc3507c382e1/phase_1_testnet/b3119aa267189ba9069cbb93900b70f0b837ed944334f92122d95b72eb1791ccd890c9396aea890f29b9e97b5ff7b13f/node3.zip
--------------------------------------------------------------------------------
/phase_1_testnet/b3119aa267189ba9069cbb93900b70f0b837ed944334f92122d95b72eb1791ccd890c9396aea890f29b9e97b5ff7b13f/node4.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ethereum/ssv/bd3ccbed4786b2be6842a1ff0cc3cc3507c382e1/phase_1_testnet/b3119aa267189ba9069cbb93900b70f0b837ed944334f92122d95b72eb1791ccd890c9396aea890f29b9e97b5ff7b13f/node4.zip
--------------------------------------------------------------------------------
/pubsub/observer.go:
--------------------------------------------------------------------------------
1 | package pubsub
2 |
3 | import "go.uber.org/zap"
4 |
5 | // Observer interface that notified by the subject registered to
6 | type Observer interface {
7 | InformObserver(interface{}) // TODO need to use golang channels
8 | GetObserverID() string
9 | }
10 |
11 | // BaseObserver struct that implements Observer
12 | type BaseObserver struct {
13 | ID string
14 | Logger zap.Logger
15 | }
16 |
17 | // InformObserver get inform by the subject and passes interface that need to cast for specific type
18 | func (b BaseObserver) InformObserver(i interface{}) {
19 | b.Logger.Info("InformObserver")
20 | }
21 |
22 | // GetObserverID get observer ID
23 | func (b BaseObserver) GetObserverID() string {
24 | return "BaseObserver"
25 | }
26 |
27 |
--------------------------------------------------------------------------------
/pubsub/subject.go:
--------------------------------------------------------------------------------
1 | package pubsub
2 |
3 | // subject interface that manage registered Observers and passing notify interface (each implementation will cast by it use)
4 | type subject interface {
5 | Register(Observer Observer)
6 | Deregister(Observer Observer)
7 | notifyAll()
8 | }
9 |
10 | // BaseSubject struct is the base implementation of subject
11 | type BaseSubject struct {
12 | subject
13 | ObserverList []Observer
14 | Name string
15 | }
16 |
17 | // Register add Observer to subject
18 | func (b *BaseSubject) Register(o Observer) {
19 | // TODO add only if not exists (change to map)
20 | b.ObserverList = append(b.ObserverList, o)
21 | }
22 |
23 | // Deregister remove Observer from subject
24 | func (b *BaseSubject) Deregister(o Observer) {
25 | b.ObserverList = removeFromSlice(b.ObserverList, o)
26 | }
27 |
28 | // notifyAll notify all observers
29 | func (b *BaseSubject) notifyAll() {
30 | for _, observer := range b.ObserverList {
31 | observer.InformObserver(b.Name)
32 | }
33 | }
34 |
35 | // removeFromSlice help func
36 | func removeFromSlice(observerList []Observer, observerToRemove Observer) []Observer {
37 | observerListLength := len(observerList)
38 | for i, observer := range observerList {
39 | if observerToRemove.GetObserverID() == observer.GetObserverID() {
40 | observerList[observerListLength-1], observerList[i] = observerList[i], observerList[observerListLength-1]
41 | return observerList[:observerListLength-1]
42 | }
43 | }
44 | return observerList
45 | }
46 |
--------------------------------------------------------------------------------
/pubsub/subject_test.go:
--------------------------------------------------------------------------------
1 | package pubsub
2 |
3 | import (
4 | "github.com/ethereum/go-ethereum/core/types"
5 | "github.com/stretchr/testify/require"
6 | "testing"
7 | )
8 |
9 | func TestPubSub(t *testing.T) {
10 | t.Run("Successfully Register", func(t *testing.T) {
11 | s := struct {
12 | BaseSubject
13 | Log types.Log
14 | Data interface{}
15 | }{}
16 |
17 | o1 := BaseObserver{
18 | ID: "BaseObserver1",
19 | }
20 |
21 | s.Register(o1)
22 | require.Equal(t, 1, len(s.ObserverList))
23 |
24 | o2 := BaseObserver{
25 | ID: "BaseObserver2",
26 | }
27 |
28 | s.Register(o2)
29 | require.Equal(t, 2, len(s.ObserverList))
30 |
31 | s.Deregister(o1)
32 | require.NotEqual(t, 2, len(s.ObserverList))
33 | })
34 | }
35 |
--------------------------------------------------------------------------------
/scripts/protogen.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -eo pipefail
4 |
5 | # Generate protoc script
6 | # We use the vendor dir to link 3rd party dependencies into the generate proto files.
7 | # It is necessary to delete the vendor dir when we finish
8 |
9 | go generate $GOPATH/src/github.com/bloxapp/ssv/ibft/proto/generate.go
10 | go generate $GOPATH/src/github.com/bloxapp/ssv/network/generate.go
11 |
--------------------------------------------------------------------------------
/shared/params/config.go:
--------------------------------------------------------------------------------
1 | package params
2 |
3 | // SsvNetworkConfig contains constant configs for node to participate in ssv network.
4 | type SsvNetworkConfig struct {
5 | OperatorContractAddress string
6 | ContractABI string
7 | OperatorPublicKey string
8 | }
9 |
--------------------------------------------------------------------------------
/shared/params/config_utils_develop.go:
--------------------------------------------------------------------------------
1 | package params
2 |
3 | var ssvConfig = TestnetConfig()
4 |
5 | // SsvConfig retrieves ssv config.
6 | func SsvConfig() *SsvNetworkConfig {
7 | return ssvConfig
8 | }
9 |
--------------------------------------------------------------------------------
/shared/params/testnet_config.go:
--------------------------------------------------------------------------------
1 | package params
2 |
3 | // UseTestnetConfig sets config for the testnet
4 | func UseTestnetConfig() {
5 | ssvConfig = TestnetConfig()
6 | }
7 |
8 | // TestnetConfig defines the config for the testnet.
9 | func TestnetConfig() *SsvNetworkConfig {
10 | return testnetSsvConfig
11 | }
12 |
13 | var testnetSsvConfig = &SsvNetworkConfig{
14 | OperatorContractAddress: "0x9573C41F0Ed8B72f3bD6A9bA6E3e15426A0aa65B",
15 | ContractABI: `[{"anonymous":false,"inputs":[{"indexed":false,"internalType":"bytes","name":"validatorPublicKey","type":"bytes"},{"indexed":false,"internalType":"uint256","name":"index","type":"uint256"},{"indexed":false,"internalType":"bytes","name":"operatorPublicKey","type":"bytes"},{"indexed":false,"internalType":"bytes","name":"sharedPublicKey","type":"bytes"},{"indexed":false,"internalType":"bytes","name":"encryptedKey","type":"bytes"}],"name":"OessAdded","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"string","name":"name","type":"string"},{"indexed":false,"internalType":"address","name":"ownerAddress","type":"address"},{"indexed":false,"internalType":"bytes","name":"publicKey","type":"bytes"}],"name":"OperatorAdded","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"ownerAddress","type":"address"},{"indexed":false,"internalType":"bytes","name":"publicKey","type":"bytes"},{"components":[{"internalType":"uint256","name":"index","type":"uint256"},{"internalType":"bytes","name":"operatorPublicKey","type":"bytes"},{"internalType":"bytes","name":"sharedPublicKey","type":"bytes"},{"internalType":"bytes","name":"encryptedKey","type":"bytes"}],"indexed":false,"internalType":"struct ISSVNetwork.Oess[]","name":"oessList","type":"tuple[]"}],"name":"ValidatorAdded","type":"event"},{"inputs":[{"internalType":"string","name":"_name","type":"string"},{"internalType":"address","name":"_ownerAddress","type":"address"},{"internalType":"bytes","name":"_publicKey","type":"bytes"}],"name":"addOperator","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_ownerAddress","type":"address"},{"internalType":"bytes","name":"_publicKey","type":"bytes"},{"internalType":"bytes[]","name":"_operatorPublicKeys","type":"bytes[]"},{"internalType":"bytes[]","name":"_sharesPublicKeys","type":"bytes[]"},{"internalType":"bytes[]","name":"_encryptedKeys","type":"bytes[]"}],"name":"addValidator","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"operatorCount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes","name":"","type":"bytes"}],"name":"operators","outputs":[{"internalType":"string","name":"name","type":"string"},{"internalType":"address","name":"ownerAddress","type":"address"},{"internalType":"bytes","name":"publicKey","type":"bytes"},{"internalType":"uint256","name":"score","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"validatorCount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}]`,
16 | OperatorPublicKey: "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN2pXcExremd2TXdvRzhNdEVyUjIKRGhUMk11dElmYUd0VmxMeDVWK2g4amwrdnlxT1YvcmxKREVlQy9HMzVpV0M0WEU3RnFKUVc1QmpvQWZ1TXhQegpRQzZ6MEE1b1I3enRuWHU2c0V3TkhJSFh3REFITHlTdVdQM3BGYlo0Qnc5b1FZTUJmbVNsL3hXR0syVnN3aVhkCkNFcUZKRmdNUFk3NlJQY0o2R2dkTWcrWVRRWVVFamlRTjFpdmJKZjRWaUpCRTcrbVNteFZNNTAzVmlyQWZndkIKenBndTNzdHZIdHpRV1Z2eHJ0NTR0Rm9DMHRmWE1RRXNSU0VtTVRoVkhocVorZTJCOC9kTWQ2R1FodnE5ZXR1RQphQkxoSlpFUXlpMklpUU02Ulg2a01vZGdGUmcvemttTFZXQ0VITzEzaFV5Rkoxang1L0M5bEIyU2VENW9jd1h4CmJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K",
17 | }
18 |
--------------------------------------------------------------------------------
/slotqueue/slotqueue.go:
--------------------------------------------------------------------------------
1 | package slotqueue
2 |
3 | import (
4 | "fmt"
5 | "github.com/bloxapp/ssv/ibft/proto"
6 | "github.com/herumi/bls-eth-go-binary/bls"
7 |
8 | "time"
9 |
10 | "github.com/bloxapp/eth2-key-manager/core"
11 | "github.com/patrickmn/go-cache"
12 | ethpb "github.com/prysmaticlabs/ethereumapis/eth/v1alpha1"
13 | "github.com/prysmaticlabs/prysm/shared/slotutil"
14 | )
15 |
16 | // Queue represents the behavior of the slot queue
17 | type Queue interface {
18 | // Next returns the next slot with its duties at its time
19 | Next(pubKey []byte) (uint64, *ethpb.DutiesResponse_Duty, bool, error)
20 |
21 | // Schedule schedules execution of the given slot and puts it into the queue
22 | Schedule(pubKey []byte, slot uint64, duty *ethpb.DutiesResponse_Duty) error
23 | }
24 |
25 | // queue implements Queue
26 | type queue struct {
27 | data *cache.Cache
28 | ticker *slotutil.SlotTicker
29 | }
30 |
31 | // Duty struct TODO: need to remove
32 | type Duty struct {
33 | NodeID uint64
34 | Duty *ethpb.DutiesResponse_Duty
35 | // ValidatorPK is the validator's public key
36 | ValidatorPK *bls.PublicKey
37 | // ShareSK is this node's share secret key
38 | ShareSK *bls.SecretKey
39 | Committee map[uint64]*proto.Node
40 | }
41 |
42 | // New is the constructor of queue
43 | func New(network core.Network) Queue {
44 | genesisTime := time.Unix(int64(network.MinGenesisTime()), 0)
45 | slotTicker := slotutil.GetSlotTicker(genesisTime, uint64(network.SlotDurationSec().Seconds()))
46 | return &queue{
47 | data: cache.New(time.Minute*30, time.Minute*31),
48 | ticker: slotTicker,
49 | }
50 | }
51 |
52 | // Next returns the next slot with its duties at its time
53 | func (q *queue) Next(pubKey []byte) (uint64, *ethpb.DutiesResponse_Duty, bool, error) {
54 | for currentSlot := range q.ticker.C() {
55 | key := q.getKey(pubKey, currentSlot)
56 | dataRaw, ok := q.data.Get(key)
57 | if !ok {
58 | continue
59 | }
60 |
61 | duty, ok := dataRaw.(*ethpb.DutiesResponse_Duty)
62 | if !ok {
63 | continue
64 | }
65 |
66 | return currentSlot, duty, true, nil
67 | }
68 |
69 | return 0, nil, false, nil
70 | }
71 |
72 | // Schedule schedules execution of the given slot and puts it into the queue
73 | func (q *queue) Schedule(pubKey []byte, slot uint64, duty *ethpb.DutiesResponse_Duty) error {
74 | q.data.SetDefault(q.getKey(pubKey, slot), duty)
75 | return nil
76 | }
77 |
78 | func (q *queue) getKey(pubKey []byte, slot uint64) string {
79 | return fmt.Sprintf("%d_%#v", slot, pubKey)
80 | }
81 |
--------------------------------------------------------------------------------
/storage/collections/ibft_storage_test.go:
--------------------------------------------------------------------------------
1 | package collections
2 |
3 | import (
4 | "github.com/bloxapp/ssv/ibft/proto"
5 | "github.com/bloxapp/ssv/storage/inmem"
6 | "github.com/stretchr/testify/require"
7 | "go.uber.org/zap"
8 | "testing"
9 | )
10 |
11 | func TestIbftStorage_SaveDecided(t *testing.T) {
12 | storage := NewIbft(inmem.New(), zap.L(), "attestation")
13 | err := storage.SaveDecided(&proto.SignedMessage{
14 | Message: &proto.Message{
15 | Type: proto.RoundState_Decided,
16 | Round: 2,
17 | Lambda: []byte{1, 2, 3, 4},
18 | ValidatorPk: []byte{1, 2, 3, 4},
19 | SeqNumber: 1,
20 | },
21 | Signature: []byte{1, 2, 3, 4},
22 | SignerIds: []uint64{1, 2, 3},
23 | })
24 | require.NoError(t, err)
25 |
26 | value, err := storage.GetDecided([]byte{1, 2, 3, 4}, 1)
27 | require.NoError(t, err)
28 | require.EqualValues(t, []byte{1, 2, 3, 4}, value.Message.ValidatorPk)
29 | require.EqualValues(t, 1, value.Message.SeqNumber)
30 | require.EqualValues(t, []byte{1, 2, 3, 4}, value.Signature)
31 |
32 | // not found
33 | _, err = storage.GetDecided([]byte{1, 2, 3, 3}, 1)
34 | require.EqualError(t, err, EntryNotFoundError)
35 | }
36 |
37 | func TestIbftStorage_SaveCurrentInstance(t *testing.T) {
38 | storage := NewIbft(inmem.New(), zap.L(), "attestation")
39 | err := storage.SaveCurrentInstance(&proto.State{
40 | Stage: proto.RoundState_Decided,
41 | SeqNumber: 2,
42 | Round: 0,
43 | ValidatorPk: []byte{1, 2, 3, 4},
44 | })
45 | require.NoError(t, err)
46 |
47 | value, err := storage.GetCurrentInstance([]byte{1, 2, 3, 4})
48 | require.NoError(t, err)
49 | require.EqualValues(t, []byte{1, 2, 3, 4}, value.ValidatorPk)
50 | require.EqualValues(t, 2, value.SeqNumber)
51 |
52 | // not found
53 | _, err = storage.GetCurrentInstance([]byte{1, 2, 3, 3})
54 | require.EqualError(t, err, EntryNotFoundError)
55 | }
56 |
57 | func TestIbftStorage_GetHighestDecidedInstance(t *testing.T) {
58 | storage := NewIbft(inmem.New(), zap.L(), "attestation")
59 | err := storage.SaveHighestDecidedInstance(&proto.SignedMessage{
60 | Message: &proto.Message{
61 | Type: proto.RoundState_Decided,
62 | Round: 2,
63 | Lambda: []byte{1, 2, 3, 4},
64 | ValidatorPk: []byte{1, 2, 3, 4},
65 | SeqNumber: 1,
66 | },
67 | Signature: []byte{1, 2, 3, 4},
68 | SignerIds: []uint64{1, 2, 3},
69 | })
70 | require.NoError(t, err)
71 |
72 | value, err := storage.GetHighestDecidedInstance([]byte{1, 2, 3, 4})
73 | require.NoError(t, err)
74 | require.EqualValues(t, []byte{1, 2, 3, 4}, value.Message.ValidatorPk)
75 | require.EqualValues(t, 1, value.Message.SeqNumber)
76 | require.EqualValues(t, []byte{1, 2, 3, 4}, value.Signature)
77 |
78 | // not found
79 | _, err = storage.GetHighestDecidedInstance([]byte{1, 2, 3, 3})
80 | require.EqualError(t, err, EntryNotFoundError)
81 | }
82 |
--------------------------------------------------------------------------------
/storage/collections/operator_storage.go:
--------------------------------------------------------------------------------
1 | package collections
2 |
3 | import (
4 | "crypto/rsa"
5 | "encoding/base64"
6 | "github.com/dgraph-io/badger"
7 | "github.com/pkg/errors"
8 | "go.uber.org/zap"
9 |
10 | "github.com/bloxapp/ssv/pubsub"
11 | "github.com/bloxapp/ssv/shared/params"
12 | "github.com/bloxapp/ssv/storage"
13 | "github.com/bloxapp/ssv/utils/rsaencryption"
14 | )
15 |
16 | // IOperatorStorage interface that managing all operator settings
17 | type IOperatorStorage interface {
18 | GetPrivateKey() (*rsa.PrivateKey, error)
19 | SetupPrivateKey(operatorKey string) error
20 | }
21 |
22 | // OperatorStorage implement IOperatorStorage
23 | type OperatorStorage struct {
24 | prefix []byte
25 | db storage.IKvStorage
26 | logger *zap.Logger
27 | pubsub.BaseObserver
28 | }
29 |
30 | // NewOperatorStorage init new instance of operator storage
31 | func NewOperatorStorage(db storage.IKvStorage, logger *zap.Logger) OperatorStorage {
32 | validator := OperatorStorage{
33 | prefix: []byte("operator-"),
34 | db: db,
35 | logger: logger,
36 | }
37 | return validator
38 | }
39 |
40 | // GetPrivateKey return rsa private key
41 | func (o OperatorStorage) GetPrivateKey() (*rsa.PrivateKey, error) {
42 | obj, err := o.db.Get(o.prefix, []byte("private-key"))
43 | if err != nil {
44 | return nil, err
45 | }
46 | sk, err := rsaencryption.ConvertPemToPrivateKey(string(obj.Value))
47 | if err != nil {
48 | return nil, err
49 | }
50 | return sk, nil
51 | }
52 |
53 | // SetupPrivateKey setup operator private key at the init of the node and set OperatorPublicKey config
54 | func (o OperatorStorage) SetupPrivateKey(operatorKeyBase64 string) error {
55 | operatorKeyByte, err := base64.StdEncoding.DecodeString(operatorKeyBase64)
56 | if err != nil {
57 | return errors.Wrap(err, "Failed to decode base64")
58 | }
59 | var operatorKey = string(operatorKeyByte)
60 |
61 | newSk, err := o.verifyPrivateKeyExist(operatorKey)
62 | if err != nil {
63 | return errors.Wrap(err, "failed to verify operator private key")
64 | }
65 | if operatorKey != "" || newSk != "" {
66 | if newSk != "" { // newly sk generated, need to set the operator key
67 | operatorKey = newSk
68 | }
69 |
70 | if err := o.savePrivateKey(operatorKey); err != nil {
71 | return errors.Wrap(err, "failed to save operator private key")
72 | }
73 | }
74 |
75 | sk, err := o.GetPrivateKey()
76 | if err != nil {
77 | return errors.Wrap(err, "failed to get operator private key")
78 | }
79 | operatorPublicKey, err := rsaencryption.ExtractPublicKey(sk)
80 | if err != nil {
81 | return errors.Wrap(err, "failed to extract operator public key")
82 | }
83 | o.logger.Info("operator public key", zap.Any("key", operatorPublicKey))
84 | params.SsvConfig().OperatorPublicKey = operatorPublicKey
85 | return nil
86 | }
87 |
88 |
89 | // SavePrivateKey save operator private key
90 | func (o OperatorStorage) savePrivateKey(operatorKey string) error {
91 | if err := o.db.Set(o.prefix, []byte("private-key"), []byte(operatorKey)); err != nil {
92 | return err
93 | }
94 | return nil
95 | }
96 |
97 | // verifyPrivateKeyExist return true if key exist and no new key passed else return new generated key
98 | func (o OperatorStorage) verifyPrivateKeyExist(operatorKey string) (string, error) {
99 | // check if sk is exist or passedKey is passed. if not, generate new operator key
100 | if _, err := o.GetPrivateKey(); err != nil { // need to generate new operator key
101 | if err.Error() == badger.ErrKeyNotFound.Error() && operatorKey == "" {
102 | _, skByte, err := rsaencryption.GenerateKeys()
103 | if err != nil {
104 | return "", errors.Wrap(err, "failed to generate new keys")
105 | }
106 | return string(skByte), nil // new key generated
107 | } else if err.Error() != badger.ErrKeyNotFound.Error() {
108 | return "", errors.Wrap(err, "failed to get private key")
109 | }
110 | }
111 | return "", nil // key already exist, no need to return sk
112 | }
113 |
--------------------------------------------------------------------------------
/storage/collections/validator_storage_test.go:
--------------------------------------------------------------------------------
1 | package collections
2 |
3 | import (
4 | "github.com/bloxapp/ssv/fixtures"
5 | "github.com/bloxapp/ssv/ibft/proto"
6 | "github.com/bloxapp/ssv/storage"
7 | "github.com/bloxapp/ssv/utils/threshold"
8 | "github.com/herumi/bls-eth-go-binary/bls"
9 | "testing"
10 |
11 | "github.com/stretchr/testify/require"
12 | "go.uber.org/zap"
13 |
14 | "github.com/bloxapp/ssv/storage/kv"
15 | )
16 |
17 | func TestValidatorSerializer(t *testing.T) {
18 | validatorShare := generateRandomValidatorShare()
19 | b, err := validatorShare.Serialize()
20 | require.NoError(t, err)
21 |
22 | obj := storage.Obj{
23 | Key: validatorShare.ValidatorPK.Serialize(),
24 | Value: b,
25 | }
26 | v, err := validatorShare.Deserialize(obj)
27 | require.NoError(t, err)
28 | require.NotNil(t, v.ValidatorPK)
29 | require.Equal(t, v.ValidatorPK.SerializeToHexStr(), validatorShare.ValidatorPK.SerializeToHexStr())
30 | require.NotNil(t, v.ShareKey)
31 | require.Equal(t, v.ShareKey.SerializeToHexStr(), validatorShare.ShareKey.SerializeToHexStr())
32 | require.NotNil(t, v.Committee)
33 | require.NotNil(t, v.NodeID)
34 | }
35 |
36 | func TestSaveAndGetValidatorStorage(t *testing.T) {
37 | db, err := kv.New("./data/db", *zap.L(), &kv.Options{InMemory: true})
38 | require.NoError(t, err)
39 | defer db.Close()
40 |
41 | validatorStorage := ValidatorStorage{
42 | prefix: []byte("validator-"),
43 | db: db,
44 | logger: nil,
45 | }
46 |
47 | validatorShare := generateRandomValidatorShare()
48 | require.NoError(t, validatorStorage.SaveValidatorShare(&validatorShare))
49 |
50 | validatorShare2 := generateRandomValidatorShare()
51 | require.NoError(t, validatorStorage.SaveValidatorShare(&validatorShare2))
52 |
53 | validatorShareByKey, err := validatorStorage.GetValidatorsShare(validatorShare.ValidatorPK.Serialize())
54 | require.NoError(t, err)
55 | require.EqualValues(t, validatorShareByKey.ValidatorPK.SerializeToHexStr(), validatorShare.ValidatorPK.SerializeToHexStr())
56 |
57 | validators, err := validatorStorage.GetAllValidatorShares()
58 | require.NoError(t, err)
59 | require.EqualValues(t, len(validators), 2)
60 | }
61 |
62 | func generateRandomValidatorShare() ValidatorShare {
63 | threshold.Init()
64 | sk := bls.SecretKey{}
65 | sk.SetByCSPRNG()
66 |
67 | ibftCommittee := map[uint64]*proto.Node{
68 | 1: {
69 | IbftId: 1,
70 | Pk: fixtures.RefSplitSharesPubKeys[0],
71 | Sk: sk.Serialize(),
72 | },
73 | 2: {
74 | IbftId: 2,
75 | Pk: fixtures.RefSplitSharesPubKeys[1],
76 | },
77 | 3: {
78 | IbftId: 3,
79 | Pk: fixtures.RefSplitSharesPubKeys[2],
80 | },
81 | 4: {
82 | IbftId: 4,
83 | Pk: fixtures.RefSplitSharesPubKeys[3],
84 | },
85 | }
86 |
87 | return ValidatorShare{
88 | NodeID: 1,
89 | ValidatorPK: sk.GetPublicKey(),
90 | ShareKey: &sk,
91 | Committee: ibftCommittee,
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/storage/db_event.go:
--------------------------------------------------------------------------------
1 | package storage
2 |
3 | import (
4 | "github.com/ethereum/go-ethereum/core/types"
5 |
6 | "github.com/bloxapp/ssv/pubsub"
7 | )
8 |
9 | // DBEvent struct
10 | type DBEvent struct {
11 | pubsub.BaseSubject
12 | Log types.Log
13 | Data interface{}
14 | }
15 |
16 | // NewDBEvent create new event subject
17 | func NewDBEvent(name string) *DBEvent {
18 | return &DBEvent{
19 | BaseSubject: pubsub.BaseSubject{
20 | Name: name,
21 | },
22 | }
23 | }
24 |
25 | // NotifyAll notify all subscribe observables
26 | func (e *DBEvent) NotifyAll() {
27 | for _, observer := range e.ObserverList {
28 | observer.InformObserver(e.Data)
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/storage/inmem/inmem.go:
--------------------------------------------------------------------------------
1 | package inmem
2 |
3 | import (
4 | "encoding/hex"
5 | "errors"
6 | "github.com/bloxapp/ssv/storage"
7 | "sync"
8 | )
9 |
10 | // inMemStorage implements storage.Storage interface
11 | type inMemStorage struct {
12 | lock sync.RWMutex
13 | memory map[string]map[string][]byte
14 | }
15 |
16 |
17 | // New is the constructor of inMemStorage
18 | func New() storage.IKvStorage {
19 | return &inMemStorage{memory: make(map[string]map[string][]byte)}
20 | }
21 |
22 | func (i *inMemStorage) Set(prefix []byte, key []byte, value []byte) error {
23 | i.lock.Lock()
24 | defer i.lock.Unlock()
25 | if i.memory[hex.EncodeToString(prefix)] == nil {
26 | i.memory[hex.EncodeToString(prefix)] = make(map[string][]byte)
27 | }
28 | i.memory[hex.EncodeToString(prefix)][hex.EncodeToString(key)] = value
29 | return nil
30 | }
31 |
32 | func (i *inMemStorage) Get(prefix []byte, key []byte) (storage.Obj, error) {
33 | i.lock.RLock()
34 | defer i.lock.RUnlock()
35 | if _, found := i.memory[hex.EncodeToString(prefix)]; found {
36 | if value, found := i.memory[hex.EncodeToString(prefix)][hex.EncodeToString(key)]; found {
37 | return storage.Obj{
38 | Key: key,
39 | Value: value,
40 | }, nil
41 | }
42 | return storage.Obj{}, errors.New("not found")
43 | }
44 | return storage.Obj{}, errors.New("not found")
45 | }
46 |
47 | func (i *inMemStorage) GetAllByCollection(prefix []byte) ([]storage.Obj, error) {
48 | ret := make([]storage.Obj, 0)
49 | if _, found := i.memory[hex.EncodeToString(prefix)]; found {
50 | for k, v := range i.memory[hex.EncodeToString(prefix)] {
51 | key, err := hex.DecodeString(k)
52 | if err != nil {
53 | return []storage.Obj{}, err
54 | }
55 |
56 | ret = append(ret, storage.Obj{
57 | Key: key,
58 | Value: v,
59 | })
60 | }
61 | return ret, nil
62 | }
63 | return []storage.Obj{}, errors.New("not found")
64 | }
65 |
66 |
67 | func (i *inMemStorage) Close() {
68 | panic("implement me")
69 | }
70 |
--------------------------------------------------------------------------------
/storage/inmem/inmem_test.go:
--------------------------------------------------------------------------------
1 | package inmem
2 |
3 | import (
4 | "github.com/stretchr/testify/require"
5 | "testing"
6 | )
7 |
8 | func TestInMemStorage(t *testing.T) {
9 | storage := New()
10 | require.NoError(t, storage.Set([]byte("prefix"), []byte("key"), []byte("value")))
11 |
12 | obj, err := storage.Get([]byte("prefix"), []byte("key"))
13 | require.NoError(t, err)
14 | require.EqualValues(t, []byte("key"), obj.Key)
15 | require.EqualValues(t, []byte("value"), obj.Value)
16 |
17 | obj, err = storage.Get([]byte("prefix"), []byte("no key"))
18 | require.EqualError(t, err, "not found")
19 | }
20 |
--------------------------------------------------------------------------------
/storage/kv/badger.go:
--------------------------------------------------------------------------------
1 | package kv
2 |
3 | import (
4 | "bytes"
5 | "github.com/bloxapp/ssv/storage"
6 | "github.com/dgraph-io/badger/v3"
7 | "github.com/pkg/errors"
8 | "go.uber.org/zap"
9 | )
10 |
11 | // Options for badger config
12 | type Options struct {
13 | InMemory bool
14 | }
15 |
16 | // BadgerDb struct
17 | type BadgerDb struct {
18 | db *badger.DB
19 | logger zap.Logger
20 | }
21 |
22 | // New create new instance of Badger db
23 | func New(path string, logger zap.Logger, opts *Options) (storage.IKvStorage, error) {
24 | // Open the Badger database located in the /tmp/badger directory.
25 | // It will be created if it doesn't exist.
26 | opt := badger.DefaultOptions(path)
27 | if opts.InMemory {
28 | opt.InMemory = opts.InMemory
29 | opt.Dir = ""
30 | opt.ValueDir = ""
31 | }
32 |
33 | opt.ValueLogFileSize = 1024 * 1024 * 100 // TODO:need to set the vlog proper (max) size
34 |
35 | db, err := badger.Open(opt)
36 | if err != nil {
37 | return &BadgerDb{}, errors.Wrap(err, "failed to open badger")
38 | }
39 | _db := BadgerDb{
40 | db: db,
41 | logger: logger,
42 | }
43 |
44 | logger.Info("Badger db initialized")
45 | return &_db, nil
46 | }
47 |
48 | // Set save value with key to storage
49 | func (b *BadgerDb) Set(prefix []byte, key []byte, value []byte) error {
50 | return b.db.Update(func(txn *badger.Txn) error {
51 | err := txn.Set(append(prefix, key...), value)
52 | return err
53 | })
54 | }
55 |
56 | // Get return value for specified key
57 | func (b *BadgerDb) Get(prefix []byte, key []byte) (storage.Obj, error) {
58 | var resValue []byte
59 | err := b.db.View(func(txn *badger.Txn) error {
60 | item, err := txn.Get(append(prefix, key...))
61 | if err != nil {
62 | return err
63 | }
64 | resValue, err = item.ValueCopy(nil)
65 | return err
66 | })
67 | return storage.Obj{
68 | Key: key,
69 | Value: resValue,
70 | }, err
71 | }
72 |
73 | // GetAllByCollection return all array of Obj for all keys under specified prefix(bucket)
74 | func (b *BadgerDb) GetAllByCollection(prefix []byte) ([]storage.Obj, error) {
75 | var res []storage.Obj
76 | var err error
77 | err = b.db.View(func(txn *badger.Txn) error {
78 | opt := badger.DefaultIteratorOptions
79 | opt.Prefix = prefix
80 | it := txn.NewIterator(opt)
81 | defer it.Close()
82 | for it.Seek(prefix); it.ValidForPrefix(prefix); it.Next() {
83 | item := it.Item()
84 | resKey := item.Key()
85 | trimmedResKey := bytes.TrimPrefix(resKey, prefix)
86 | val, err := item.ValueCopy(nil)
87 | if err != nil {
88 | b.logger.Error("failed to copy value", zap.Error(err))
89 | continue
90 | }
91 | obj := storage.Obj{
92 | Key: trimmedResKey,
93 | Value: val,
94 | }
95 | res = append(res, obj)
96 | }
97 | return err
98 | })
99 | return res, err
100 | }
101 |
102 | // Close close db
103 | func (b *BadgerDb) Close() {
104 | if err := b.db.Close(); err != nil{
105 | b.logger.Fatal("failed to close db", zap.Error(err))
106 | }
107 | }
--------------------------------------------------------------------------------
/storage/kv/badger_test.go:
--------------------------------------------------------------------------------
1 | package kv
2 |
3 | import (
4 | "github.com/stretchr/testify/require"
5 | "go.uber.org/zap"
6 | "testing"
7 | )
8 |
9 | func TestBadgerEndToEnd(t *testing.T) {
10 | db, err := New("./data/db", *zap.L(), &Options{InMemory: true})
11 | require.NoError(t, err)
12 |
13 | toSave := []struct {
14 | prefix []byte
15 | key []byte
16 | value []byte
17 | }{
18 | {
19 | []byte("prefix1"),
20 | []byte("key1"),
21 | []byte("value"),
22 | },
23 | {
24 | []byte("prefix1"),
25 | []byte("key2"),
26 | []byte("value"),
27 | },
28 | {
29 | []byte("prefix2"),
30 | []byte("key1"),
31 | []byte("value"),
32 | },
33 | }
34 |
35 | for _, save := range toSave {
36 | require.NoError(t, db.Set(save.prefix, save.key, save.value))
37 | }
38 |
39 | obj, err := db.Get(toSave[0].prefix, toSave[0].key)
40 | require.NoError(t, err)
41 | require.EqualValues(t, toSave[0].key, obj.Key)
42 | require.EqualValues(t, toSave[0].value, obj.Value)
43 |
44 | objs, err := db.GetAllByCollection(toSave[0].prefix)
45 | require.NoError(t, err)
46 | require.EqualValues(t, 2, len(objs))
47 |
48 | obj, err = db.Get(toSave[2].prefix, toSave[2].key)
49 | require.NoError(t, err)
50 | require.EqualValues(t, toSave[2].key, obj.Key)
51 | require.EqualValues(t, toSave[2].value, obj.Value)
52 | }
53 |
--------------------------------------------------------------------------------
/storage/storage.go:
--------------------------------------------------------------------------------
1 | package storage
2 |
3 | // IKvStorage interface for all db kind
4 | type IKvStorage interface {
5 | Set(prefix []byte, key []byte, value []byte) error
6 | Get(prefix []byte, key []byte) (Obj, error)
7 | GetAllByCollection(prefix []byte) ([]Obj, error)
8 | Close()
9 | }
10 |
11 | // Obj struct for getting key/value from storage
12 | type Obj struct {
13 | Key []byte
14 | Value []byte
15 | }
16 |
17 |
18 |
--------------------------------------------------------------------------------
/utils/cliflag/cliflag.go:
--------------------------------------------------------------------------------
1 | package cliflag
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/spf13/cobra"
7 | )
8 |
9 | // AddPersistentStringFlag adds a string flag to the command
10 | func AddPersistentStringFlag(c *cobra.Command, flag string, value string, description string, isRequired bool) {
11 | req := ""
12 | if isRequired {
13 | req = " (required)"
14 | }
15 |
16 | c.PersistentFlags().String(flag, value, fmt.Sprintf("%s%s", description, req))
17 |
18 | if isRequired {
19 | _ = c.MarkPersistentFlagRequired(flag)
20 | }
21 | }
22 |
23 | // AddPersistentIntFlag adds a int flag to the command
24 | func AddPersistentIntFlag(c *cobra.Command, flag string, value uint64, description string, isRequired bool) {
25 | req := ""
26 | if isRequired {
27 | req = " (required)"
28 | }
29 |
30 | c.PersistentFlags().Uint64(flag, value, fmt.Sprintf("%s%s", description, req))
31 |
32 | if isRequired {
33 | _ = c.MarkPersistentFlagRequired(flag)
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/utils/dataval/bytesval/validation.go:
--------------------------------------------------------------------------------
1 | package bytesval
2 |
3 | import (
4 | "bytes"
5 | "errors"
6 | "github.com/bloxapp/ssv/ibft/valcheck"
7 | )
8 |
9 | // bytesValidation implements val.ValueImplementation interface
10 | // The logic is to compare bytes from the input with the original ones.
11 | type bytesValidation struct {
12 | val []byte
13 | }
14 |
15 | // New is the constructor of bytesValidation
16 | func New(val []byte) valcheck.ValueCheck {
17 | return &bytesValidation{
18 | val: val,
19 | }
20 | }
21 |
22 | // Validate implements dataval.ValidatorStorage interface
23 | func (c *bytesValidation) Check(value []byte) error {
24 | if !bytes.Equal(value, c.val) {
25 | return errors.New("msg value is wrong")
26 | }
27 |
28 | return nil
29 | }
30 |
--------------------------------------------------------------------------------
/utils/grpcex/grpcex.go:
--------------------------------------------------------------------------------
1 | package grpcex
2 |
3 | import (
4 | "time"
5 |
6 | middleware "github.com/grpc-ecosystem/go-grpc-middleware"
7 | grpc_retry "github.com/grpc-ecosystem/go-grpc-middleware/retry"
8 | grpc_opentracing "github.com/grpc-ecosystem/go-grpc-middleware/tracing/opentracing"
9 | grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus"
10 | "github.com/pkg/errors"
11 | "github.com/prysmaticlabs/prysm/shared/grpcutils"
12 | "go.opencensus.io/plugin/ocgrpc"
13 | "google.golang.org/grpc"
14 | )
15 |
16 | // Default values.
17 | const (
18 | defaultMaxCallRecvMsgSize = 10 * 5 << 20 // Default 50Mb
19 | defaultGRPCRetries uint = 2
20 | )
21 |
22 | // DialConn dials GRPC connection
23 | func DialConn(addr string) (*grpc.ClientConn, error) {
24 | baseOpts, err := constructDialOptions(defaultMaxCallRecvMsgSize, defaultGRPCRetries, )
25 | if err != nil {
26 | return nil, errors.Wrap(err, "failed to construct base dial options")
27 | }
28 |
29 | conn, err := grpc.Dial(addr, baseOpts...)
30 | if err != nil {
31 | return nil, errors.Wrap(err, "failed to dial context")
32 | }
33 |
34 | return conn, nil
35 | }
36 |
37 | // constructDialOptions constructs a list of grpc dial options
38 | func constructDialOptions(
39 | maxCallRecvMsgSize int,
40 | grpcRetries uint,
41 | extraOpts ...grpc.DialOption,
42 | ) ([]grpc.DialOption, error) {
43 | if maxCallRecvMsgSize == 0 {
44 | maxCallRecvMsgSize = 10 * 5 << 20 // Default 50Mb
45 | }
46 |
47 | interceptors := []grpc.UnaryClientInterceptor{
48 | grpc_opentracing.UnaryClientInterceptor(),
49 | grpc_prometheus.UnaryClientInterceptor,
50 | grpc_retry.UnaryClientInterceptor(),
51 | grpcutils.LogGRPCRequests,
52 | }
53 |
54 | dialOpts := []grpc.DialOption{
55 | grpc.WithInsecure(),
56 | grpc.WithDefaultCallOptions(
57 | grpc.MaxCallRecvMsgSize(maxCallRecvMsgSize),
58 | grpc_retry.WithMax(grpcRetries),
59 | grpc_retry.WithBackoff(grpc_retry.BackoffLinear(time.Second)),
60 | ),
61 | grpc.WithStatsHandler(&ocgrpc.ClientHandler{}),
62 | grpc.WithUnaryInterceptor(middleware.ChainUnaryClient(interceptors...)),
63 | grpc.WithChainStreamInterceptor(
64 | grpcutils.LogGRPCStream,
65 | grpc_opentracing.StreamClientInterceptor(),
66 | grpc_prometheus.StreamClientInterceptor,
67 | grpc_retry.StreamClientInterceptor(),
68 | ),
69 | }
70 |
71 | dialOpts = append(dialOpts, extraOpts...)
72 | return dialOpts, nil
73 | }
74 |
--------------------------------------------------------------------------------
/utils/logex/zap.go:
--------------------------------------------------------------------------------
1 | package logex
2 |
3 | import (
4 | "log"
5 | "time"
6 |
7 | "go.uber.org/zap"
8 | "go.uber.org/zap/zapcore"
9 | )
10 |
11 | // Build builds the default zap logger, and sets the global zap logger to the configured logger instance.
12 | func Build(appName string, level zapcore.Level) *zap.Logger {
13 | cfg := zap.Config{
14 | Encoding: "console",
15 | Level: zap.NewAtomicLevelAt(level),
16 | OutputPaths: []string{"stdout"},
17 | EncoderConfig: zapcore.EncoderConfig{
18 | MessageKey: "message",
19 | LevelKey: "level",
20 | EncodeLevel: zapcore.CapitalColorLevelEncoder,
21 | TimeKey: "time",
22 | EncodeTime: func(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
23 | enc.AppendString(iso3339CleanTime(t))
24 | },
25 | CallerKey: "caller",
26 | EncodeCaller: zapcore.ShortCallerEncoder,
27 | EncodeDuration: zapcore.StringDurationEncoder,
28 | },
29 | }
30 |
31 | logger, err := cfg.Build()
32 | if err != nil {
33 | log.Fatalf("err making logger: %+v", err)
34 | }
35 |
36 | logger = logger.With(zap.String("app", appName))
37 | zap.ReplaceGlobals(logger)
38 | return logger
39 | }
40 |
41 | // iso3339CleanTime converts the given time to ISO 3339 format
42 | func iso3339CleanTime(t time.Time) string {
43 | return t.UTC().Format("2006-01-02T15:04:05.000000Z")
44 | }
45 |
--------------------------------------------------------------------------------
/utils/rsaencryption/rsa_encryption.go:
--------------------------------------------------------------------------------
1 | package rsaencryption
2 |
3 | import (
4 | "crypto/rand"
5 | "crypto/rsa"
6 | "crypto/x509"
7 | "encoding/base64"
8 | "encoding/pem"
9 | "github.com/pkg/errors"
10 | )
11 |
12 | var keySize = 2048
13 |
14 | // GenerateKeys using rsa random generate keys and return []byte bas64
15 | func GenerateKeys() ([]byte, []byte, error) {
16 | // generate random private key (secret)
17 | sk, err := rsa.GenerateKey(rand.Reader, keySize)
18 | if err != nil {
19 | return nil, nil, errors.Wrap(err, "Failed to generate private key")
20 | }
21 | // retrieve public key from the newly generated secret
22 | pk := &sk.PublicKey
23 |
24 | // convert to bytes
25 | skPem := pem.EncodeToMemory(
26 | &pem.Block{
27 | Type: "RSA PRIVATE KEY",
28 | Bytes: x509.MarshalPKCS1PrivateKey(sk),
29 | },
30 | )
31 | pkBytes, err := x509.MarshalPKIXPublicKey(pk)
32 | if err != nil {
33 | return nil, nil, errors.Wrap(err, "Failed to marshal public key")
34 | }
35 | pkPem := pem.EncodeToMemory(
36 | &pem.Block{
37 | Type: "RSA PUBLIC KEY",
38 | Bytes: pkBytes,
39 | },
40 | )
41 | return pkPem, skPem, nil
42 | }
43 |
44 | // DecodeKey with secret key (base64) and hash (base64), return the encrypted key string
45 | func DecodeKey(sk *rsa.PrivateKey, hashBase64 string) (string, error) {
46 | hash, _ := base64.StdEncoding.DecodeString(hashBase64)
47 | decryptedKey, err := rsa.DecryptPKCS1v15(rand.Reader, sk, hash)
48 | if err != nil {
49 | return "", errors.Wrap(err, "Failed to decrypt key")
50 | }
51 | return string(decryptedKey), nil
52 | }
53 |
54 | // ConvertPemToPrivateKey return rsa private key from secret key
55 | func ConvertPemToPrivateKey(skPem string) (*rsa.PrivateKey, error) {
56 | block, _ := pem.Decode([]byte(skPem))
57 | enc := x509.IsEncryptedPEMBlock(block)
58 | b := block.Bytes
59 | if enc {
60 | var err error
61 | b, err = x509.DecryptPEMBlock(block, nil)
62 | if err != nil {
63 | return nil, errors.Wrap(err, "Failed to decrypt private key")
64 | }
65 | }
66 | parsedSk, err := x509.ParsePKCS1PrivateKey(b)
67 | if err != nil {
68 | return nil, errors.Wrap(err, "Failed to parse private key")
69 | }
70 | return parsedSk, nil
71 | }
72 |
73 | // PrivateKeyToByte converts privateKey to []byte
74 | func PrivateKeyToByte(sk *rsa.PrivateKey) []byte {
75 | return pem.EncodeToMemory(
76 | &pem.Block{
77 | Type: "RSA PRIVATE KEY",
78 | Bytes: x509.MarshalPKCS1PrivateKey(sk),
79 | },
80 | )
81 | }
82 |
83 | // ExtractPublicKey get public key from private key and return []byte represent the public key
84 | func ExtractPublicKey(sk *rsa.PrivateKey) (string, error) {
85 | pkBytes, err := x509.MarshalPKIXPublicKey(&sk.PublicKey)
86 | if err != nil {
87 | return "", errors.Wrap(err, "Failed to marshal private key")
88 | }
89 | pemByte := pem.EncodeToMemory(
90 | &pem.Block{
91 | Type: "RSA PUBLIC KEY",
92 | Bytes: pkBytes,
93 | },
94 | )
95 |
96 | return base64.StdEncoding.EncodeToString(pemByte), nil
97 | }
98 |
--------------------------------------------------------------------------------
/utils/rsaencryption/rsa_encryption_test.go:
--------------------------------------------------------------------------------
1 | package rsaencryption
2 |
3 | import (
4 | "github.com/stretchr/testify/require"
5 | "testing"
6 | )
7 |
8 | var (
9 | //pkPem = "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb3dFN09FYnd5TGt2clowVFU0amoKb295SUZ4TnZnclk4RmorV3NseVpUbHlqOFVEZkZyWWg1VW4ydTRZTWRBZStjUGYxWEsrQS9QOVhYN09CNG5mMQpPb0dWQjZ3ckMvamhMYnZPSDY1MHJ5VVlvcGVZaGxTWHhHbkQ0dmN2VHZjcUxMQit1ZTIvaXlTeFFMcFpSLzZWCnNUM2ZGckVvbnpGVHFuRkN3Q0YyOGlQbkpWQmpYNlQvSGNUSjU1SURrYnRvdGFyVTZjd3dOT0huSGt6V3J2N2kKdHlQa1I0R2UxMWhtVkc5UWpST3Q1NmVoWGZGc0ZvNU1xU3ZxcFlwbFhrSS96VU5tOGovbHFFZFUwUlhVcjQxTAoyaHlLWS9wVmpzZ21lVHNONy9acUFDa0h5ZTlGYmtWOVYvVmJUaDdoV1ZMVHFHU2g3QlkvRDdnd093ZnVLaXEyClR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K"
10 | skPem = "-----BEGIN RSA PRIVATE KEY-----\nMIIEpQIBAAKCAQEAowE7OEbwyLkvrZ0TU4jjooyIFxNvgrY8Fj+WslyZTlyj8UDf\nFrYh5Un2u4YMdAe+cPf1XK+A/P9XX7OB4nf1OoGVB6wrC/jhLbvOH650ryUYopeY\nhlSXxGnD4vcvTvcqLLB+ue2/iySxQLpZR/6VsT3fFrEonzFTqnFCwCF28iPnJVBj\nX6T/HcTJ55IDkbtotarU6cwwNOHnHkzWrv7ityPkR4Ge11hmVG9QjROt56ehXfFs\nFo5MqSvqpYplXkI/zUNm8j/lqEdU0RXUr41L2hyKY/pVjsgmeTsN7/ZqACkHye9F\nbkV9V/VbTh7hWVLTqGSh7BY/D7gwOwfuKiq2TwIDAQABAoIBADjO3Qyn7JKHt44S\nCAI82thzkZo5M8uiJx652pMeom8k6h3SNe18XCPEuzBvbzeg20YTpHdA0vtZIeJA\ndSuwEs7pCj86SWZKvm9p3FQ+QHwpuYQwwP9Py/Svx4z6CIrEqPYaLJAvw2mCyCN+\nzk7A8vpqTa1i4H1ae4YTIuhCwWlxe1ttD6rVUYfC2rVaFJ+b8JlzFRq4bnAR8yme\nrE4iAlfgTOj9zL814qRlYQeeZhMvA8T0qWUohbr1imo5XzIJZayLocvqhZEbk0dj\nq9qKWdIpAATRjWvb+7PkjmlwNjLOhJ1phtCkc/S4j2cvo9gcS7WafxaqCl/ix4Yt\n5KvPJ8ECgYEA0Em4nMMEFXbuSM/l5UCzv3kT6H/TYO7FVh071G7QAFoloxJBZDFV\n7fHsc+uCimlG2Xt3CrGo9tsOnF/ZgDKNmtDvvjxmlPnAb5g4uhXgYNMsKQShpeRW\n/ay8CmWbsRqXZaLoI5br2kCTLwsVz2hpabAzBOr2YV3vMRB5i7COYSMCgYEAyFgL\n3DkKwsTTyVyplenoAZaS/o0mKxZnffRnHNP5QgRfT4pQkuogk+MYAeBuGsc4cTi7\nrTtytUMBABXEKGIJkAbNoASHQMUcO1vvcwhBW7Ay+oxuc0JSlnaXjowS0C0o/4qr\nQ/rpUneir+Vu/N8+6edETRkNj+5unmePEe9NBuUCgYEAgtUr31woHot8FcRxNdW0\nkpstRCe20PZqgjMOt9t7UB1P8uSuqo7K2RHTYuUWNHb4h/ejyNXbumPTA6q5Zmta\nw1pmnWo3TXCrze0iBNFlBazf2kwMdbW+Zs2vuCAm8dIwMylnA6PzNj7FtRETfBqr\nzDVfdsFYTcTBUGJ21qXqaV0CgYEAmuMPEEv9WMTo43VDGsaCeq/Zpvii+I7SphsM\nmMn8m6Bbu1e4oUxmsU7RoanMFeHNbiMpXW1namGJ5XHufDYHJJVN5Zd6pYV+JRoX\njjxkoyke0Hs/bNZqmS7ITwlWBiHT33Rqohzaw8oAObLMUq2ZqyYDtQNYa90vIkH3\n5yq1x00CgYEAs4ztQhGRbeUlqnW6Z6yfRJ6XXYqdMPhxuBxvNn/dxJ10T4W2DUuC\njSdpGXrY+ECYyXUwlXBqbaKx1K5AQD7nmu9J3l0oMkX6tSBj1OE5MabATrsW6wvT\nhkTPJZMyPUYhoBkivPUKyQXswrQV/nUQAsAcLeJShTW4gSs0M6weQAc=\n-----END RSA PRIVATE KEY-----\n"
11 | encryptedKeyBase64 = "NW/6N5Ubo5T+oiT9My2wXFH5TWT7iQnN8YKUlcoFeg00OzL1S4yKrIPemdr7SM3EbPeHlBtOAM3z+06EmaNlwVdBiexSRJmgnknqwt/Ught4pKZK/WdJAEhMRwjZ3nx1Qi1TYcw7oZBaOdeTdm65QEAnsqOHk1htnUTXqsqYxVF750u8JWq3Mzr3oCN65ydSJRQoSa+lo3DikIDrXSYe1LRY5epMRrOq3cujuykuAVZQWp1vzv4w4V6mffmxaDbPpln/w28FKCxYkxG/WhwGuXR1GK6IWr3xpXPKcG+lzfvlmh4UiK1Lad/YD460oMXOKZT8apn4HL4tl9HOb6RyWQ=="
12 | )
13 |
14 | func TestGenerateKeys(t *testing.T) {
15 | _, skByte, err := GenerateKeys()
16 | require.NoError(t, err)
17 | sk, err := ConvertPemToPrivateKey(string(skByte))
18 | require.NoError(t, err)
19 | require.Equal(t, 2048, sk.N.BitLen())
20 | require.NoError(t, sk.Validate())
21 | }
22 |
23 | func TestDecodeKey(t *testing.T) {
24 | sk, err := ConvertPemToPrivateKey(skPem)
25 | require.NoError(t, err)
26 | key, err := DecodeKey(sk, encryptedKeyBase64)
27 | require.NoError(t, err)
28 | require.Equal(t, "626d6a13ae5b1458c310700941764f3841f279f9c8de5f4ba94abd01dc082517", key)
29 | }
30 |
--------------------------------------------------------------------------------
/utils/threshold/reconstruct.go:
--------------------------------------------------------------------------------
1 | package threshold
2 |
3 | import (
4 | "fmt"
5 | "github.com/herumi/bls-eth-go-binary/bls"
6 | )
7 |
8 | // ReconstructSignatures receives a map of user indexes and serialized bls.Sign.
9 | // It then reconstructs the original threshold signature using lagrange interpolation
10 | func ReconstructSignatures(signatures map[uint64][]byte) (*bls.Sign, error) {
11 | reconstructedSig := bls.Sign{}
12 |
13 | idVec := make([]bls.ID, 0)
14 | sigVec := make([]bls.Sign, 0)
15 |
16 | for index, signature := range signatures {
17 | blsID := bls.ID{}
18 | err := blsID.SetDecString(fmt.Sprintf("%d", index))
19 | if err != nil {
20 | return nil, err
21 | }
22 |
23 | idVec = append(idVec, blsID)
24 | blsSig := bls.Sign{}
25 |
26 | err = blsSig.Deserialize(signature)
27 | if err != nil {
28 | return nil, err
29 | }
30 |
31 | sigVec = append(sigVec, blsSig)
32 | }
33 | err := reconstructedSig.Recover(sigVec, idVec)
34 | return &reconstructedSig, err
35 | }
36 |
--------------------------------------------------------------------------------
/utils/threshold/threshold.go:
--------------------------------------------------------------------------------
1 | package threshold
2 |
3 | import (
4 | "fmt"
5 | "github.com/herumi/bls-eth-go-binary/bls"
6 | "math/big"
7 | )
8 |
9 | var (
10 | curveOrder = new(big.Int)
11 | )
12 |
13 | // Init initializes BLS
14 | func Init() {
15 | _ = bls.Init(bls.BLS12_381)
16 | _ = bls.SetETHmode(bls.EthModeDraft07)
17 |
18 | curveOrder, _ = curveOrder.SetString(bls.GetCurveOrder(), 10)
19 | }
20 |
21 | // Create receives a bls.SecretKey hex and count.
22 | // Will split the secret key into count shares
23 | func Create(skBytes []byte, threshold uint64, count uint64) (map[uint64]*bls.SecretKey, error) {
24 | // master key Polynomial
25 | msk := make([]bls.SecretKey, threshold)
26 |
27 | sk := &bls.SecretKey{}
28 | if err := sk.Deserialize(skBytes); err != nil {
29 | return nil, err
30 | }
31 | msk[0] = *sk
32 |
33 | // construct poly
34 | for i := uint64(1); i < threshold; i++ {
35 | sk := bls.SecretKey{}
36 | sk.SetByCSPRNG()
37 | msk[i] = sk
38 | }
39 |
40 | // evaluate shares - starting from 1 because 0 is master key
41 | shares := make(map[uint64]*bls.SecretKey)
42 | for i := uint64(1); i <= count; i++ {
43 | blsID := bls.ID{}
44 |
45 | err := blsID.SetDecString(fmt.Sprintf("%d", i))
46 | if err != nil {
47 | return nil, err
48 | }
49 |
50 | sk := bls.SecretKey{}
51 |
52 | err = sk.Set(msk, &blsID)
53 | if err != nil {
54 | return nil, err
55 | }
56 |
57 | shares[i] = &sk
58 | }
59 | return shares, nil
60 | }
61 |
--------------------------------------------------------------------------------
/validator/signature_test.go:
--------------------------------------------------------------------------------
1 | package validator
2 |
3 | import (
4 | "github.com/bloxapp/ssv/beacon"
5 | "github.com/herumi/bls-eth-go-binary/bls"
6 | "github.com/stretchr/testify/require"
7 | "testing"
8 | )
9 |
10 | func TestVerifyPartialSignature(t *testing.T) {
11 | tests := []struct{
12 | name string
13 | skByts []byte
14 | root []byte
15 | useWrongRoot bool
16 | ibftID uint64
17 | expectedError string
18 | }{
19 | {
20 | "valid/ id 1" ,
21 | refSplitShares[0],
22 | []byte{0,1,2,3,4,5,6,7,8,9},
23 | false,
24 | 1,
25 | "",
26 | },
27 | {
28 | "valid/ id 2" ,
29 | refSplitShares[1],
30 | []byte{0,1,2,3,4,5,6,7,8,1},
31 | false,
32 | 2,
33 | "",
34 | },
35 | {
36 | "valid/ id 3" ,
37 | refSplitShares[2],
38 | []byte{0,1,2,3,4,5,6,7,8,2},
39 | false,
40 | 3,
41 | "",
42 | },
43 | {
44 | "wrong ibft id" ,
45 | refSplitShares[2],
46 | []byte{0,1,2,3,4,5,6,7,8,2},
47 | false,
48 | 2,
49 | "could not verify signature from iBFT member 2",
50 | },
51 | {
52 | "wrong root" ,
53 | refSplitShares[2],
54 | []byte{0,1,2,3,4,5,6,7,8,2},
55 | true,
56 | 3,
57 | "could not verify signature from iBFT member 3",
58 | },
59 | }
60 |
61 | for _, test := range tests {
62 | t.Run(test.name, func(t *testing.T) {
63 | node := testingValidator(t,true, 4)
64 |
65 | sk := &bls.SecretKey{}
66 | require.NoError(t, sk.Deserialize(test.skByts))
67 |
68 | sig := sk.SignByte(test.root)
69 |
70 | usedRoot := test.root
71 | if test.useWrongRoot {
72 | usedRoot = []byte{0,0,0,0,0,0,0}
73 | }
74 |
75 | err := node.verifyPartialSignature(sig.Serialize(), usedRoot, test.ibftID, node.ibfts[beacon.RoleAttester].GetIBFTCommittee()) // TODO need to fetch the committee from storage
76 | if len(test.expectedError) > 0 {
77 | require.EqualError(t, err, test.expectedError)
78 | } else {
79 | require.NoError(t, err)
80 | }
81 | })
82 | }
83 | }
84 |
--------------------------------------------------------------------------------