├── .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 | 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 | 16 | 28 | 29 | 40 | 52 | 53 | 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 ![ethereum](/github/resources/ethereum.gif) 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 | ![security_permission](/github/resources/security_permission.png) 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 | --------------------------------------------------------------------------------