├── .circleci ├── config.yml └── publish.sh ├── .gitignore ├── .mill-version ├── .scalafmt.conf ├── LICENSE ├── README.md ├── build.sc ├── docs ├── architecture.png ├── components.png └── master-based.png ├── flake.lock ├── flake.nix ├── metronome ├── checkpointing │ ├── app │ │ ├── resources │ │ │ ├── application.conf │ │ │ └── logback.xml │ │ ├── specs │ │ │ ├── resources │ │ │ │ └── test.conf │ │ │ └── src │ │ │ │ └── io │ │ │ │ └── iohk │ │ │ │ └── metronome │ │ │ │ └── checkpointing │ │ │ │ └── app │ │ │ │ └── config │ │ │ │ └── CheckpointingConfigParserSpec.scala │ │ └── src │ │ │ └── io │ │ │ └── iohk │ │ │ └── metronome │ │ │ └── checkpointing │ │ │ └── app │ │ │ ├── CheckpointingApp.scala │ │ │ ├── CheckpointingComposition.scala │ │ │ ├── CheckpointingKeyGen.scala │ │ │ ├── CheckpointingNamespaces.scala │ │ │ ├── codecs │ │ │ └── CheckpointingCodecs.scala │ │ │ ├── config │ │ │ ├── CheckpointingConfig.scala │ │ │ ├── CheckpointingConfigParser.scala │ │ │ └── CheckpointingOptions.scala │ │ │ └── tracing │ │ │ ├── CheckpointingConsensusTracers.scala │ │ │ ├── CheckpointingNetworkTracers.scala │ │ │ ├── CheckpointingServiceTracers.scala │ │ │ └── CheckpointingSyncTracers.scala │ ├── interpreter │ │ ├── props │ │ │ └── src │ │ │ │ └── io │ │ │ │ └── iohk │ │ │ │ └── metronome │ │ │ │ └── checkpointing │ │ │ │ └── interpreter │ │ │ │ ├── codecs │ │ │ │ └── DefaultInterpreterCodecsProps.scala │ │ │ │ └── messages │ │ │ │ └── ArbitraryInstances.scala │ │ ├── specs │ │ │ ├── resources │ │ │ │ └── logback-test.xml │ │ │ └── src │ │ │ │ └── io │ │ │ │ └── iohk │ │ │ │ └── metronome │ │ │ │ └── checkpointing │ │ │ │ └── interpreter │ │ │ │ └── InterpreterServiceSpec.scala │ │ └── src │ │ │ └── io │ │ │ └── iohk │ │ │ └── metronome │ │ │ └── checkpointing │ │ │ └── interpreter │ │ │ ├── InterpreterRPC.scala │ │ │ ├── InterpreterService.scala │ │ │ ├── ServiceRPC.scala │ │ │ ├── codecs │ │ │ └── DefaultInterpreterCodecs.scala │ │ │ ├── messages │ │ │ └── InterpreterMessage.scala │ │ │ ├── package.scala │ │ │ └── tracing │ │ │ └── InterpreterEvent.scala │ ├── models │ │ ├── props │ │ │ └── src │ │ │ │ └── io │ │ │ │ └── iohk │ │ │ │ └── metronome │ │ │ │ └── checkpointing │ │ │ │ └── models │ │ │ │ ├── ArbitraryInstances.scala │ │ │ │ ├── CheckpointCertificateProps.scala │ │ │ │ ├── LedgerProps.scala │ │ │ │ ├── MerkleTreeProps.scala │ │ │ │ └── RLPCodecsProps.scala │ │ ├── specs │ │ │ ├── resources │ │ │ │ └── golden │ │ │ │ │ ├── RLPCodec[CheckpointCertificate].rlp │ │ │ │ │ ├── RLPCodec[CheckpointCertificate].txt │ │ │ │ │ ├── RLPCodec[Ledger].rlp │ │ │ │ │ ├── RLPCodec[Ledger].txt │ │ │ │ │ ├── RLPCodec[Transaction].rlp │ │ │ │ │ └── RLPCodec[Transaction].txt │ │ │ └── src │ │ │ │ └── io │ │ │ │ └── iohk │ │ │ │ └── metronome │ │ │ │ └── checkpointing │ │ │ │ └── models │ │ │ │ ├── CheckpointingSigningSpec.scala │ │ │ │ └── RLPCodecsSpec.scala │ │ └── src │ │ │ └── io │ │ │ └── iohk │ │ │ └── metronome │ │ │ └── checkpointing │ │ │ ├── CheckpointingAgreement.scala │ │ │ ├── models │ │ │ ├── Block.scala │ │ │ ├── CheckpointCertificate.scala │ │ │ ├── CheckpointingSigning.scala │ │ │ ├── Ledger.scala │ │ │ ├── Mempool.scala │ │ │ ├── MerkleTree.scala │ │ │ ├── RLPCodecs.scala │ │ │ ├── RLPHash.scala │ │ │ └── Transaction.scala │ │ │ └── package.scala │ └── service │ │ ├── props │ │ └── src │ │ │ └── io │ │ │ └── iohk │ │ │ └── metronome │ │ │ └── checkpointing │ │ │ └── service │ │ │ ├── BlockCreationProps.scala │ │ │ ├── CheckpointingServiceFixtures.scala │ │ │ ├── CheckpointingServiceProps.scala │ │ │ └── storage │ │ │ └── LedgerStorageProps.scala │ │ ├── specs │ │ └── src │ │ │ └── io │ │ │ └── iohk │ │ │ └── metronome │ │ │ └── checkpointing │ │ │ └── service │ │ │ └── InterpreterClientSpec.scala │ │ └── src │ │ └── io │ │ └── iohk │ │ └── metronome │ │ └── checkpointing │ │ └── service │ │ ├── CheckpointingService.scala │ │ ├── InterpreterClient.scala │ │ ├── messages │ │ └── CheckpointingMessage.scala │ │ ├── storage │ │ └── LedgerStorage.scala │ │ └── tracing │ │ └── CheckpointingEvent.scala ├── config │ ├── specs │ │ ├── resources │ │ │ ├── array.conf │ │ │ ├── complex.conf │ │ │ ├── override.conf │ │ │ └── simple.conf │ │ └── src │ │ │ └── io │ │ │ └── iohk │ │ │ └── metronome │ │ │ └── config │ │ │ └── ConfigParserSpec.scala │ └── src │ │ └── io │ │ └── iohk │ │ └── metronome │ │ └── config │ │ ├── ConfigDecoders.scala │ │ └── ConfigParser.scala ├── core │ ├── specs │ │ └── src │ │ │ └── io │ │ │ └── iohk │ │ │ └── metronome │ │ │ └── core │ │ │ ├── PipeSpec.scala │ │ │ ├── fibers │ │ │ ├── FiberMapSpec.scala │ │ │ └── FiberSetSpec.scala │ │ │ └── messages │ │ │ └── RPCTrackerSpec.scala │ └── src │ │ └── io │ │ └── iohk │ │ └── metronome │ │ └── core │ │ ├── Pipe.scala │ │ ├── Tagger.scala │ │ ├── Validated.scala │ │ ├── fibers │ │ ├── DeferredTask.scala │ │ ├── FiberMap.scala │ │ └── FiberSet.scala │ │ ├── messages │ │ ├── RPCMessage.scala │ │ ├── RPCSupport.scala │ │ └── RPCTracker.scala │ │ └── package.scala ├── crypto │ ├── specs │ │ └── src │ │ │ └── io │ │ │ └── iohk │ │ │ └── metronome │ │ │ └── crypto │ │ │ └── hash │ │ │ └── Keccak256Spec.scala │ └── src │ │ └── io │ │ └── iohk │ │ └── metronome │ │ └── crypto │ │ ├── ECKeyPair.scala │ │ ├── ECPrivateKey.scala │ │ ├── ECPublicKey.scala │ │ ├── GroupSignature.scala │ │ ├── PartialSignature.scala │ │ └── hash │ │ ├── Hash.scala │ │ ├── Keccak256.scala │ │ └── package.scala ├── examples │ ├── resources │ │ ├── application.conf │ │ └── logback.xml │ ├── robot.sh │ ├── specs │ │ └── src │ │ │ └── io │ │ │ └── iohk │ │ │ └── metronome │ │ │ └── examples │ │ │ └── robot │ │ │ └── app │ │ │ ├── RobotIntegrationSpec.scala │ │ │ ├── RobotTestComposition.scala │ │ │ ├── RobotTestConnectionManager.scala │ │ │ └── config │ │ │ └── RobotConfigParserSpec.scala │ └── src │ │ └── io │ │ └── iohk │ │ └── metronome │ │ └── examples │ │ └── robot │ │ ├── RobotAgreement.scala │ │ ├── app │ │ ├── RobotApp.scala │ │ ├── RobotComposition.scala │ │ ├── RobotNamespaces.scala │ │ ├── config │ │ │ ├── RobotConfig.scala │ │ │ ├── RobotConfigParser.scala │ │ │ └── RobotOptions.scala │ │ └── tracing │ │ │ ├── RobotConsensusTracers.scala │ │ │ ├── RobotNetworkTracers.scala │ │ │ └── RobotSyncTracers.scala │ │ ├── codecs │ │ └── RobotCodecs.scala │ │ ├── models │ │ ├── Robot.scala │ │ ├── RobotBlock.scala │ │ ├── RobotSigning.scala │ │ └── package.scala │ │ ├── package.scala │ │ └── service │ │ ├── RobotService.scala │ │ ├── messages │ │ └── RobotMessage.scala │ │ └── tracing │ │ ├── RobotEvent.scala │ │ └── RobotTracers.scala ├── hotstuff │ ├── consensus │ │ ├── props │ │ │ └── src │ │ │ │ └── io │ │ │ │ └── iohk │ │ │ │ └── metronome │ │ │ │ └── hotstuff │ │ │ │ └── consensus │ │ │ │ ├── ArbitraryInstances.scala │ │ │ │ ├── LeaderSelectionProps.scala │ │ │ │ └── basic │ │ │ │ ├── ProtocolStateProps.scala │ │ │ │ └── Secp256k1SigningProps.scala │ │ ├── specs │ │ │ └── src │ │ │ │ └── io │ │ │ │ └── iohk │ │ │ │ └── metronome │ │ │ │ └── hotstuff │ │ │ │ └── consensus │ │ │ │ └── FederationSpec.scala │ │ └── src │ │ │ └── io │ │ │ └── iohk │ │ │ └── metronome │ │ │ └── hotstuff │ │ │ └── consensus │ │ │ ├── Federation.scala │ │ │ ├── LeaderSelection.scala │ │ │ ├── ViewNumber.scala │ │ │ ├── basic │ │ │ ├── Agreement.scala │ │ │ ├── Block.scala │ │ │ ├── Effect.scala │ │ │ ├── Event.scala │ │ │ ├── Message.scala │ │ │ ├── Phase.scala │ │ │ ├── ProtocolError.scala │ │ │ ├── ProtocolState.scala │ │ │ ├── QuorumCertificate.scala │ │ │ ├── Secp256k1Agreement.scala │ │ │ ├── Secp256k1Signing.scala │ │ │ └── Signing.scala │ │ │ └── package.scala │ └── service │ │ ├── props │ │ └── src │ │ │ └── io │ │ │ └── iohk │ │ │ └── metronome │ │ │ └── hotstuff │ │ │ └── service │ │ │ ├── execution │ │ │ └── BlockExecutorProps.scala │ │ │ ├── storage │ │ │ ├── BlockStorageProps.scala │ │ │ └── ViewStateStorageProps.scala │ │ │ └── sync │ │ │ ├── BlockSynchronizerProps.scala │ │ │ └── ViewSynchronizerProps.scala │ │ ├── specs │ │ └── src │ │ │ └── io │ │ │ └── iohk │ │ │ └── metronome │ │ │ └── hotstuff │ │ │ └── service │ │ │ └── MessageStashSpec.scala │ │ └── src │ │ └── io │ │ └── iohk │ │ └── metronome │ │ └── hotstuff │ │ └── service │ │ ├── ApplicationService.scala │ │ ├── ConsensusService.scala │ │ ├── HotStuffService.scala │ │ ├── Status.scala │ │ ├── SyncService.scala │ │ ├── codecs │ │ ├── DefaultConsensusCodecs.scala │ │ ├── DefaultDuplexMessageCodecs.scala │ │ ├── DefaultMessageCodecs.scala │ │ ├── DefaultProtocolCodecs.scala │ │ └── DefaultSecp256k1Codecs.scala │ │ ├── execution │ │ └── BlockExecutor.scala │ │ ├── messages │ │ ├── DuplexMessage.scala │ │ ├── HotStuffMessage.scala │ │ └── SyncMessage.scala │ │ ├── pipes │ │ ├── SyncPipe.scala │ │ └── package.scala │ │ ├── storage │ │ ├── BlockPruning.scala │ │ ├── BlockStorage.scala │ │ └── ViewStateStorage.scala │ │ ├── sync │ │ ├── BlockSynchronizer.scala │ │ └── ViewSynchronizer.scala │ │ └── tracing │ │ ├── ConsensusEvent.scala │ │ ├── ConsensusTracers.scala │ │ ├── SyncEvent.scala │ │ └── SyncTracers.scala ├── logging │ └── src │ │ └── io │ │ └── iohk │ │ └── metronome │ │ └── logging │ │ ├── HybridLog.scala │ │ ├── HybridLogObject.scala │ │ ├── InMemoryLogTracer.scala │ │ └── LogTracer.scala ├── networking │ ├── specs │ │ ├── resources │ │ │ └── logback-test.xml │ │ └── src │ │ │ └── io │ │ │ └── iohk │ │ │ └── metronome │ │ │ └── networking │ │ │ ├── ConnectionHandlerSpec.scala │ │ │ ├── MockEncryptedConnectionProvider.scala │ │ │ ├── NetworkSpec.scala │ │ │ ├── RemoteConnectionManagerTestUtils.scala │ │ │ ├── RemoteConnectionManagerWithMockProviderSpec.scala │ │ │ └── RemoteConnectionManagerWithScalanetProviderSpec.scala │ └── src │ │ └── io │ │ └── iohk │ │ └── metronome │ │ └── networking │ │ ├── ConnectionHandler.scala │ │ ├── ConnectionsRegister.scala │ │ ├── EncryptedConnectionProvider.scala │ │ ├── LocalConnectionManager.scala │ │ ├── Network.scala │ │ ├── NetworkEvent.scala │ │ ├── NetworkTracers.scala │ │ ├── RemoteConnectionManager.scala │ │ └── ScalanetConnectionProvider.scala ├── rocksdb │ ├── props │ │ └── src │ │ │ └── io │ │ │ └── iohk │ │ │ └── metronome │ │ │ └── rocksdb │ │ │ └── RocksDBStoreProps.scala │ └── src │ │ └── io │ │ └── iohk │ │ └── metronome │ │ └── rocksdb │ │ ├── NamespaceRegistry.scala │ │ └── RocksDBStore.scala ├── storage │ ├── specs │ │ └── src │ │ │ └── io │ │ │ └── iohk │ │ │ └── metronome │ │ │ └── storage │ │ │ └── KVStoreStateSpec.scala │ └── src │ │ └── io │ │ └── iohk │ │ └── metronome │ │ └── storage │ │ ├── InMemoryKVStore.scala │ │ ├── KVCollection.scala │ │ ├── KVRingBuffer.scala │ │ ├── KVStore.scala │ │ ├── KVStoreOp.scala │ │ ├── KVStoreRead.scala │ │ ├── KVStoreRunner.scala │ │ ├── KVStoreState.scala │ │ ├── KVTree.scala │ │ └── package.scala └── tracing │ └── src │ └── io │ └── iohk │ └── metronome │ └── tracer │ └── Tracer.scala ├── nix ├── metronome.nix ├── mill-derivation.nix └── overlay.nix └── versionFile └── version /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/openjdk:11-jdk 6 | 7 | working_directory: ~/repo 8 | 9 | environment: 10 | JVM_OPTS: -Xmx3200m 11 | TERM: dumb 12 | 13 | steps: 14 | - checkout 15 | 16 | # Download and cache dependencies 17 | - restore_cache: 18 | keys: 19 | - v1-dependencies-{{ checksum "build.sc" }} 20 | # fallback to using the latest cache if no exact match is found 21 | - v1-dependencies- 22 | 23 | # https://circleci.com/docs/2.0/env-vars/#using-bash_env-to-set-environment-variables 24 | - run: 25 | name: install coursier 26 | command: | 27 | curl -fLo cs https://git.io/coursier-cli-"$(uname | tr LD ld)" 28 | chmod +x cs 29 | ./cs install cs 30 | rm cs 31 | echo "export PATH=$PATH:/home/circleci/.local/share/coursier/bin" >> $BASH_ENV 32 | 33 | - run: 34 | name: install scalafmt 35 | command: cs install scalafmt 36 | 37 | - run: 38 | name: install mill 39 | command: | 40 | mkdir -p ~/.local/bin 41 | (echo "#!/usr/bin/env sh" && curl -L https://github.com/lihaoyi/mill/releases/download/0.8.0/0.8.0) > ~/.local/bin/mill 42 | chmod +x ~/.local/bin/mill 43 | 44 | - run: 45 | name: check that the code is formatted properly 46 | command: scalafmt --test 47 | 48 | # For some reason if I try to separate compile and test, then the subsequent test step does nothing. 49 | - run: 50 | name: compile and test 51 | command: mill __.test 52 | 53 | - save_cache: 54 | paths: 55 | - ~/.ivy2 56 | - ~/.cache 57 | key: v1-dependencies-{{ checksum "build.sc" }} 58 | 59 | - when: 60 | condition: 61 | or: 62 | - equal: [ master, << pipeline.git.branch >> ] 63 | - equal: [ develop, << pipeline.git.branch >> ] 64 | steps: 65 | - run: 66 | name: install gpg2 67 | # GPG in docker needs to be run with some additional flags 68 | # and we are not able to change how mill uses it 69 | # this is why we're creating wrapper that adds the flags 70 | command: | 71 | sudo apt update 72 | sudo apt install -y gnupg2 73 | sudo mv /usr/bin/gpg /usr/bin/gpg-vanilla 74 | sudo sh -c "echo '#!/bin/sh\n\n/usr/bin/gpg-vanilla --no-tty --pinentry loopback \$@' > /usr/bin/gpg" 75 | sudo chmod 755 /usr/bin/gpg 76 | cat /usr/bin/gpg 77 | 78 | - run: 79 | name: install base64 80 | command: sudo apt update && sudo apt install -y cl-base64 81 | 82 | - run: 83 | name: publish 84 | command: .circleci/publish.sh 85 | no_output_timeout: 30m 86 | 87 | workflows: 88 | build_and_publish: 89 | jobs: 90 | - build 91 | -------------------------------------------------------------------------------- /.circleci/publish.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euv 4 | 5 | echo $GPG_KEY | base64 --decode | gpg --batch --import 6 | 7 | gpg --passphrase $GPG_PASSPHRASE --batch --yes -a -b LICENSE 8 | 9 | if [[ "$CIRCLE_BRANCH" == "develop" ]]; then 10 | 11 | mill mill.scalalib.PublishModule/publishAll \ 12 | __.publishArtifacts \ 13 | "$OSS_USERNAME":"$OSS_PASSWORD" \ 14 | --gpgArgs --passphrase="$GPG_PASSPHRASE",--batch,--yes,-a,-b 15 | 16 | elif [[ "$CIRCLE_BRANCH" == "master" ]]; then 17 | 18 | mill versionFile.setReleaseVersion 19 | mill mill.scalalib.PublishModule/publishAll \ 20 | __.publishArtifacts \ 21 | "$OSS_USERNAME":"$OSS_PASSWORD" \ 22 | --gpgArgs --passphrase="$GPG_PASSPHRASE",--batch,--yes,-a,-b \ 23 | --readTimeout 600000 \ 24 | --awaitTimeout 600000 \ 25 | --release true 26 | 27 | else 28 | 29 | echo "Skipping publish step" 30 | 31 | fi 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | .bloop 4 | .metals 5 | .vscode 6 | out/ 7 | *.iml 8 | /.idea* 9 | -------------------------------------------------------------------------------- /.mill-version: -------------------------------------------------------------------------------- 1 | 0.9.9 2 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = "2.7.4" 2 | maxColumn = 80 3 | align.preset = more 4 | -------------------------------------------------------------------------------- /docs/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/input-output-hk/metronome/fb8fea9862fb130e4d896ca326d6f66857a5e0f4/docs/architecture.png -------------------------------------------------------------------------------- /docs/components.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/input-output-hk/metronome/fb8fea9862fb130e4d896ca326d6f66857a5e0f4/docs/components.png -------------------------------------------------------------------------------- /docs/master-based.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/input-output-hk/metronome/fb8fea9862fb130e4d896ca326d6f66857a5e0f4/docs/master-based.png -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1628572867, 6 | "narHash": "sha256-CBGONA03V6JUvutdsYsEcC5PwsMNM+Yay6y+bUKg1bE=", 7 | "owner": "nixos", 8 | "repo": "nixpkgs", 9 | "rev": "2457ddc9522b0861649ee5e952fa2e505c1743b7", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "nixos", 14 | "ref": "nixpkgs-unstable", 15 | "repo": "nixpkgs", 16 | "type": "github" 17 | } 18 | }, 19 | "root": { 20 | "inputs": { 21 | "nixpkgs": "nixpkgs", 22 | "utils": "utils" 23 | } 24 | }, 25 | "utils": { 26 | "locked": { 27 | "lastModified": 1623875721, 28 | "narHash": "sha256-A8BU7bjS5GirpAUv4QA+QnJ4CceLHkcXdRp4xITDB0s=", 29 | "owner": "numtide", 30 | "repo": "flake-utils", 31 | "rev": "f7e004a55b120c02ecb6219596820fcd32ca8772", 32 | "type": "github" 33 | }, 34 | "original": { 35 | "owner": "numtide", 36 | "repo": "flake-utils", 37 | "type": "github" 38 | } 39 | } 40 | }, 41 | "root": "root", 42 | "version": 7 43 | } 44 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Metronome"; 3 | 4 | inputs = { 5 | utils.url = "github:numtide/flake-utils"; 6 | nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; 7 | }; 8 | 9 | outputs = { self, nixpkgs, utils }: 10 | let 11 | localOverlay = import ./nix/overlay.nix; 12 | pkgsForSystem = system: import nixpkgs { 13 | overlays = [ 14 | localOverlay 15 | ]; 16 | inherit system; 17 | }; 18 | in utils.lib.eachSystem [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" ] (system: rec { 19 | legacyPackages = pkgsForSystem system; 20 | packages = utils.lib.flattenTree { 21 | inherit (legacyPackages) devShell metronome; 22 | }; 23 | defaultPackage = packages.metronome; 24 | apps.metronome = utils.lib.mkApp { drv = packages.metronome; }; # use as `nix run ` 25 | hydraJobs = { inherit (legacyPackages) metronome; }; 26 | }) // { 27 | overlay = localOverlay; 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /metronome/checkpointing/app/resources/application.conf: -------------------------------------------------------------------------------- 1 | metronome { 2 | checkpointing { 3 | # A name for the node that we can use to distinguish 4 | # if we run multiple instances on the same machine. 5 | name = service 6 | 7 | federation { 8 | # Public address of this federation member; required. 9 | self { 10 | host = null 11 | port = 9080 12 | # Private ECDSA key of this federation member in hexadecimal format; required. 13 | # It can either the the key itself, or a path to a file which contains the key. 14 | # The public key will be derived from the private key. 15 | private-key = null 16 | } 17 | 18 | # List of other federation members; records of {host, port, public-key}. 19 | others = [] 20 | 21 | # The maximum number of tolerated Byzantine nodes; optional. 22 | # At most (n-1)/3, but can be lower to require smaller quorum. 23 | maxFaulty = null 24 | } 25 | 26 | consensus { 27 | # Minimum time to allow for a HotStuff round. 28 | min-timeout = 5s 29 | # Maximum time to allow for a HotStuff round, after numerous timeouts. 30 | max-timeout = 15s 31 | # Increment factor to apply on the timeout after a failed round. 32 | timeout-factor = 1.2 33 | } 34 | 35 | # Network configuration to accept connections from remote federation nodes. 36 | remote { 37 | # Bind address for the checkpointing service remote interface. 38 | listen { 39 | host = 0.0.0.0 40 | port = ${metronome.checkpointing.federation.self.port} 41 | } 42 | # Request roundtrip timeout. 43 | timeout = 3s 44 | } 45 | 46 | # Network configuration to accept connection from the local interpreter. 47 | local { 48 | # Bind address for the checkpointing service local interface. 49 | listen { 50 | host = 127.0.0.1 51 | port = 9081 52 | } 53 | # Node of the PoW Interpreter. 54 | interpreter { 55 | host = 127.0.0.1 56 | port = 9082 57 | # ECDSA key used by the interpreter to secure the connection; required. 58 | public-key = null 59 | } 60 | # Request roundtrip timeout. 61 | timeout = 3s 62 | # Whether we should expect the Interpreter to send us notifications about 63 | # the arrival of a checkpoint height, or check in every time we have to 64 | # create a block. Depends on how the Interpreter is implemented, it's an 65 | # optimisation to save unnecessary round trips. 66 | expect-checkpoint-candidate-notifications = false 67 | } 68 | 69 | database { 70 | # Storage location for RocksDB. 71 | path = ${user.home}"/.metronome/checkpointing/db/"${metronome.checkpointing.name} 72 | # Size of the ring buffer for the checkpointing ledger. 73 | state-history-size = 100 74 | # Number of blocks to keep before pruning. 75 | block-history-size = 100 76 | # Time to wait before pruning a block from history. 77 | prune-interval = 60s 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /metronome/checkpointing/app/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | ${log.console.level} 12 | 13 | 14 | ${encoder.pattern} 15 | 16 | 17 | 18 | 19 | ${log.file.dir}/${log.file.name}.log 20 | true 21 | 22 | ${log.file.dir}/${log.file.name}.%i.log.zip 23 | 1 24 | 10 25 | 26 | 27 | 10MB 28 | 29 | 30 | ${encoder.pattern} 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /metronome/checkpointing/app/specs/resources/test.conf: -------------------------------------------------------------------------------- 1 | # Extend the defaults. 2 | # The leading "/"" works when a file refers to the default module included in the resources. 3 | # Here we could use "application" or "application.conf" without it, but when executed with 4 | # `java -Dconfig.file=example.conf` only the one that begins with "/" seems to work. 5 | include "/application" 6 | 7 | metronome { 8 | checkpointing { 9 | federation { 10 | self { 11 | host = localhost 12 | port = 40000 13 | private-key = cd2a249a76d8e9fd0e538e651b9e97c3fc5efcceeb10fc98dd57fbdd156457e6 14 | } 15 | 16 | others = [ 17 | {host = localhost, port = 40001, public-key = ff7849206b7faef9557cf53333739ecd947698d76ba11ffabf2587435322b9a8b4f063faf97e5aace2a75b8f6714e5bd3d483cad6e830ae3036afcc4ff1b5369, private-key = 15cc92810f61bc705f939432197fee100bcc1a99d6cc66c7c28fa158d4144f84} 18 | {host = localhost, port = 40002, public-key = cb020251d396614a35038dd2ff88fd2f1a5fd74c8bcad4b353fa605405c8b1b8c80ee12d2a10b1fca59424b16890c8115fbc94a68026369acc3c2603595e6387, private-key = a4769d076bb7eefeb1aba8aa97520d8f7f8bcd65049a128c3040f9dd5d3eeae6} 19 | {host = localhost, port = 40003, public-key = 23fcab42e8f1078880b27aab4849092489bfa8d3e3b0faa54c9db89e89223c783ec7a3b2f8e6461b27778f78cea261a2272abe31c5601173b2964ef14af897dc, private-key = 9441f3e96104a11405cb0e03ceb693f889770dd2c155dab7573023e00e878ace} 20 | ] 21 | } 22 | local { 23 | interpreter { 24 | public-key = 65e2f6da1bb1e7f0b07f5b892c568acb5429833e30af3974eedd2137ebc9f1fb8b0c462d4ca558dda64c5da8cf10280a1f579556ac8a611bd2fa7199f5a2c69a 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /metronome/checkpointing/app/specs/src/io/iohk/metronome/checkpointing/app/config/CheckpointingConfigParserSpec.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.checkpointing.app.config 2 | 3 | import com.typesafe.config.ConfigFactory 4 | import org.scalatest.flatspec.AnyFlatSpec 5 | import org.scalatest.Inside 6 | import org.scalatest.matchers.should.Matchers 7 | import java.nio.file.Path 8 | 9 | class CheckpointingConfigParserSpec 10 | extends AnyFlatSpec 11 | with Inside 12 | with Matchers { 13 | 14 | behavior of "CheckpointingConfigParser" 15 | 16 | it should "not parse the default configuration due to missing data" in { 17 | inside(CheckpointingConfigParser.parse(ConfigFactory.load())) { 18 | case Left(_) => 19 | succeed 20 | } 21 | } 22 | 23 | it should "parse the with the test overrides" in { 24 | inside( 25 | CheckpointingConfigParser.parse(ConfigFactory.load("test.conf")) 26 | ) { case Right(config) => 27 | config.remote.listen.port shouldBe config.federation.self.port 28 | config.federation.self.privateKey.isLeft shouldBe true 29 | } 30 | } 31 | 32 | it should "parse when the private key is a path" in { 33 | inside( 34 | CheckpointingConfigParser.parse { 35 | ConfigFactory.parseString( 36 | "metronome.checkpointing.federation.self.private-key=./node.key" 37 | ) withFallback ConfigFactory.load("test.conf") 38 | } 39 | ) { case Right(config) => 40 | config.federation.self.privateKey shouldBe Right(Path.of("./node.key")) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /metronome/checkpointing/app/src/io/iohk/metronome/checkpointing/app/CheckpointingApp.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.checkpointing.app 2 | 3 | import cats.effect.ExitCode 4 | import com.typesafe.config.ConfigFactory 5 | import monix.eval.{Task, TaskApp} 6 | import io.iohk.metronome.checkpointing.app.config.{ 7 | CheckpointingConfigParser, 8 | CheckpointingOptions 9 | } 10 | 11 | object CheckpointingApp extends TaskApp { 12 | override def run(args: List[String]): Task[ExitCode] = { 13 | CheckpointingOptions.parse(args) match { 14 | case None => 15 | Task.pure(ExitCode.Error) 16 | 17 | case Some(opts) => 18 | run(opts) 19 | } 20 | } 21 | 22 | def run(opts: CheckpointingOptions): Task[ExitCode] = 23 | opts.mode match { 24 | case CheckpointingOptions.KeyGen => 25 | setLogProperties(opts, "keygen") >> 26 | // Not parsing the configuration for this as it may be incomplete without the keys. 27 | CheckpointingKeyGen.generateAndPrint.as(ExitCode.Success) 28 | 29 | case CheckpointingOptions.Service => 30 | CheckpointingConfigParser.parse(ConfigFactory.load()) match { 31 | case Left(error) => 32 | Task 33 | .delay(println(s"Error parsing configuration: $error")) 34 | .as(ExitCode.Error) 35 | 36 | case Right(config) => 37 | setLogProperties(opts, config.name) >> 38 | CheckpointingComposition 39 | .compose(config) 40 | .use(_ => Task.never.as(ExitCode.Success)) 41 | } 42 | } 43 | 44 | /** Set dynamic system properties expected by `logback.xml` before any logging module is loaded. */ 45 | def setLogProperties( 46 | opts: CheckpointingOptions, 47 | name: String 48 | ): Task[Unit] = Task { 49 | // Separate log file for each node. 50 | System.setProperty("log.file.name", name) 51 | // Control how much logging goes on the console. 52 | System.setProperty("log.console.level", opts.logLevel.toString) 53 | }.void 54 | } 55 | -------------------------------------------------------------------------------- /metronome/checkpointing/app/src/io/iohk/metronome/checkpointing/app/CheckpointingKeyGen.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.checkpointing.app 2 | 3 | import monix.eval.Task 4 | import io.iohk.metronome.crypto.ECKeyPair 5 | import io.circe.Json 6 | 7 | /** Generate an ECDSA key pair and print it on the console. 8 | * 9 | * Using JSON format so that it's obvious which part is what. 10 | * We can use `jq` to parse on the command line if necessary. 11 | */ 12 | object CheckpointingKeyGen { 13 | def format(pair: ECKeyPair): String = { 14 | val json = Json.obj( 15 | "publicKey" -> Json.fromString(pair.pub.bytes.toHex), 16 | "privateKey" -> Json.fromString(pair.prv.bytes.toHex) 17 | ) 18 | json.spaces2 19 | } 20 | 21 | def print(pair: ECKeyPair): Task[Unit] = 22 | Task(println(format(pair))) 23 | 24 | def generateAndPrint: Task[Unit] = 25 | for { 26 | rng <- Task(new java.security.SecureRandom()) 27 | keys = ECKeyPair.generate(rng) 28 | _ <- print(keys) 29 | } yield () 30 | } 31 | -------------------------------------------------------------------------------- /metronome/checkpointing/app/src/io/iohk/metronome/checkpointing/app/CheckpointingNamespaces.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.checkpointing.app 2 | 3 | import io.iohk.metronome.rocksdb.NamespaceRegistry 4 | 5 | object CheckpointingNamespaces extends NamespaceRegistry { 6 | val Block = register("block") 7 | val BlockMeta = register("block-meta") 8 | val BlockToChildren = register("block-to-children") 9 | val ViewState = register("view-state") 10 | val Ledger = register("ledger") 11 | val LedgerMeta = register("ledger-meta") 12 | } 13 | -------------------------------------------------------------------------------- /metronome/checkpointing/app/src/io/iohk/metronome/checkpointing/app/codecs/CheckpointingCodecs.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.checkpointing.app.codecs 2 | 3 | import io.iohk.ethereum.rlp, rlp.RLPCodec 4 | import io.iohk.metronome.hotstuff.service.codecs._ 5 | import io.iohk.metronome.checkpointing.CheckpointingAgreement 6 | import io.iohk.metronome.checkpointing.service.messages.CheckpointingMessage 7 | import io.iohk.metronome.checkpointing.models.{Block, RLPCodecs, Ledger} 8 | import scodec.{Codec, Attempt} 9 | import scodec.codecs._ 10 | import scodec.bits.ByteVector 11 | import scala.util.Try 12 | 13 | object CheckpointingCodecs 14 | extends DefaultConsensusCodecs[CheckpointingAgreement] 15 | with DefaultProtocolCodecs[CheckpointingAgreement] 16 | with DefaultSecp256k1Codecs[CheckpointingAgreement] 17 | with DefaultMessageCodecs[CheckpointingAgreement] 18 | with DefaultDuplexMessageCodecs[ 19 | CheckpointingAgreement, 20 | CheckpointingMessage 21 | ] { 22 | 23 | import scodec.codecs.implicits._ 24 | import RLPCodecs.{rlpBlock, rlpLedger} 25 | 26 | private implicit def codecFromRLPCodec[T: RLPCodec]: Codec[T] = 27 | Codec[ByteVector].exmap( 28 | bytes => Attempt.fromTry(Try(rlp.decode[T](bytes.toArray))), 29 | value => Attempt.successful(ByteVector(rlp.encode(value))) 30 | ) 31 | 32 | override implicit lazy val hashCodec: Codec[Block.Header.Hash] = 33 | Codec[ByteVector].xmap(Block.Header.Hash(_), identity) 34 | 35 | implicit lazy val ledgerHashCodec: Codec[Ledger.Hash] = 36 | Codec[ByteVector].xmap(Ledger.Hash(_), identity) 37 | 38 | override implicit lazy val blockCodec: Codec[Block] = 39 | codecFromRLPCodec[Block] 40 | 41 | implicit lazy val ledgerCodec: Codec[Ledger] = 42 | codecFromRLPCodec[Ledger] 43 | 44 | implicit lazy val getStateRequestCodec 45 | : Codec[CheckpointingMessage.GetStateRequest] = 46 | Codec.deriveLabelledGeneric 47 | 48 | implicit lazy val getStateResponseCodec 49 | : Codec[CheckpointingMessage.GetStateResponse] = 50 | Codec.deriveLabelledGeneric 51 | 52 | override implicit lazy val applicationMessageCodec 53 | : Codec[CheckpointingMessage] = 54 | discriminated[CheckpointingMessage] 55 | .by(uint2) 56 | .typecase(0, getStateRequestCodec) 57 | .typecase(1, getStateResponseCodec) 58 | 59 | } 60 | -------------------------------------------------------------------------------- /metronome/checkpointing/app/src/io/iohk/metronome/checkpointing/app/config/CheckpointingConfig.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.checkpointing.app.config 2 | 3 | import io.iohk.metronome.crypto.{ECPublicKey, ECPrivateKey} 4 | import java.nio.file.Path 5 | import java.net.InetSocketAddress 6 | import scala.concurrent.duration.FiniteDuration 7 | 8 | case class CheckpointingConfig( 9 | name: String, 10 | federation: CheckpointingConfig.Federation, 11 | consensus: CheckpointingConfig.Consensus, 12 | remote: CheckpointingConfig.RemoteNetwork, 13 | local: CheckpointingConfig.LocalNetwork, 14 | database: CheckpointingConfig.Database 15 | ) 16 | 17 | object CheckpointingConfig { 18 | trait HasAddress { 19 | def host: String 20 | def port: Int 21 | lazy val address = new InetSocketAddress(host, port) 22 | } 23 | 24 | case class Federation( 25 | self: LocalNode, 26 | others: List[RemoteNode], 27 | maxFaulty: Option[Int] 28 | ) 29 | 30 | case class Consensus( 31 | minTimeout: FiniteDuration, 32 | maxTimeout: FiniteDuration, 33 | timeoutFactor: Double 34 | ) 35 | 36 | case class RemoteNode( 37 | val host: String, 38 | val port: Int, 39 | publicKey: ECPublicKey 40 | ) extends HasAddress 41 | 42 | case class LocalNode( 43 | val host: String, 44 | val port: Int, 45 | privateKey: Either[ECPrivateKey, Path] 46 | ) extends HasAddress 47 | 48 | case class Socket( 49 | val host: String, 50 | val port: Int 51 | ) extends HasAddress 52 | 53 | case class RemoteNetwork( 54 | listen: Socket, 55 | timeout: FiniteDuration 56 | ) 57 | 58 | case class LocalNetwork( 59 | listen: Socket, 60 | interpreter: RemoteNode, 61 | timeout: FiniteDuration, 62 | expectCheckpointCandidateNotifications: Boolean 63 | ) 64 | 65 | case class Database( 66 | path: Path, 67 | stateHistorySize: Int, 68 | blockHistorySize: Int, 69 | pruneInterval: FiniteDuration 70 | ) 71 | } 72 | -------------------------------------------------------------------------------- /metronome/checkpointing/app/src/io/iohk/metronome/checkpointing/app/config/CheckpointingConfigParser.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.checkpointing.app.config 2 | 3 | import com.typesafe.config.Config 4 | import io.iohk.metronome.config.{ConfigParser, ConfigDecoders} 5 | import io.iohk.metronome.crypto.{ECPublicKey, ECPrivateKey} 6 | import io.circe._, io.circe.generic.semiauto._ 7 | import java.nio.file.Path 8 | import scodec.bits.ByteVector 9 | import scala.util.Try 10 | 11 | object CheckpointingConfigParser { 12 | def parse(root: Config): ConfigParser.Result[CheckpointingConfig] = { 13 | ConfigParser.parse[CheckpointingConfig]( 14 | root.getConfig("metronome.checkpointing").root(), 15 | prefix = "METRONOME_CHECKPOINTING" 16 | ) 17 | } 18 | 19 | import ConfigDecoders._ 20 | 21 | def hexDecoder[T](f: ByteVector => T): Decoder[T] = 22 | Decoder[String].emap { str => 23 | ByteVector.fromHex(str) match { 24 | case None => 25 | Left("$str is not a valid hexadecimal value") 26 | case Some(bytes) => 27 | Try(f(bytes)).toEither.left.map(_.getMessage) 28 | } 29 | } 30 | 31 | implicit val ecPublicKeyDecoder: Decoder[ECPublicKey] = 32 | hexDecoder(ECPublicKey(_)) 33 | 34 | implicit val ecPrivateKeyDecoder: Decoder[ECPrivateKey] = 35 | hexDecoder(ECPrivateKey(_)) 36 | 37 | implicit val pathDecoder: Decoder[Path] = 38 | Decoder[String].map(Path.of(_)) 39 | 40 | implicit val ecPrivateKeyOrPathDecoder: Decoder[Either[ECPrivateKey, Path]] = 41 | ecPrivateKeyDecoder.map(Left(_)) or pathDecoder.map(Right(_)) 42 | 43 | implicit val localNodeDecoder: Decoder[CheckpointingConfig.LocalNode] = 44 | deriveDecoder 45 | 46 | implicit val remoteNodeDecoder: Decoder[CheckpointingConfig.RemoteNode] = 47 | deriveDecoder 48 | 49 | implicit val federationDecoder: Decoder[CheckpointingConfig.Federation] = 50 | deriveDecoder 51 | 52 | implicit val consensusDecoder: Decoder[CheckpointingConfig.Consensus] = 53 | deriveDecoder 54 | 55 | implicit val socketDecoder: Decoder[CheckpointingConfig.Socket] = 56 | deriveDecoder 57 | 58 | implicit val remoteDecoder: Decoder[CheckpointingConfig.RemoteNetwork] = 59 | deriveDecoder 60 | 61 | implicit val localDecoder: Decoder[CheckpointingConfig.LocalNetwork] = 62 | deriveDecoder 63 | 64 | implicit val dbDecoder: Decoder[CheckpointingConfig.Database] = 65 | deriveDecoder 66 | 67 | implicit val configDecoder: Decoder[CheckpointingConfig] = deriveDecoder 68 | } 69 | -------------------------------------------------------------------------------- /metronome/checkpointing/app/src/io/iohk/metronome/checkpointing/app/config/CheckpointingOptions.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.checkpointing.app.config 2 | 3 | import scopt.OParser 4 | import ch.qos.logback.classic.Level 5 | 6 | case class CheckpointingOptions( 7 | mode: CheckpointingOptions.Mode, 8 | logLevel: Level 9 | ) 10 | 11 | object CheckpointingOptions { 12 | 13 | sealed trait Mode 14 | case object Service extends Mode 15 | case object KeyGen extends Mode 16 | 17 | private val LogLevels = List( 18 | Level.OFF, 19 | Level.ERROR, 20 | Level.WARN, 21 | Level.INFO, 22 | Level.DEBUG, 23 | Level.TRACE 24 | ) 25 | 26 | val default = CheckpointingOptions( 27 | mode = Service, 28 | logLevel = Level.INFO 29 | ) 30 | 31 | /** Parse the options. Return `None` if there was an error, 32 | * which has already been printed to the console. 33 | */ 34 | def parse( 35 | args: List[String] 36 | ): Option[CheckpointingOptions] = 37 | OParser.parse( 38 | CheckpointingOptions.oparser, 39 | args, 40 | CheckpointingOptions.default 41 | ) 42 | 43 | private val oparser = { 44 | val builder = OParser.builder[CheckpointingOptions] 45 | import builder._ 46 | 47 | OParser.sequence( 48 | programName("checkpointing"), 49 | opt[String]('l', "log-level") 50 | .action((x, opts) => opts.copy(logLevel = Level.toLevel(x))) 51 | .text( 52 | s"log level; one of [${LogLevels.map(_.toString).mkString("|")}]" 53 | ) 54 | .optional() 55 | .validate(x => 56 | Either.cond( 57 | LogLevels.map(_.toString).contains(x.toUpperCase), 58 | (), 59 | s"Must be between one of ${LogLevels.map(_.toString)}" 60 | ) 61 | ), 62 | cmd("service") 63 | .text("run the checkpointing service") 64 | .action((_, opts) => opts.copy(mode = Service)), 65 | cmd("keygen") 66 | .text("generate an ECDSA key pair") 67 | .action((_, opts) => opts.copy(mode = KeyGen)) 68 | ) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /metronome/checkpointing/app/src/io/iohk/metronome/checkpointing/app/tracing/CheckpointingNetworkTracers.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.checkpointing.app.tracing 2 | 3 | import monix.eval.Task 4 | import io.iohk.metronome.checkpointing.CheckpointingAgreement 5 | import io.iohk.metronome.checkpointing.service.messages.CheckpointingMessage 6 | import io.iohk.metronome.hotstuff.service.messages.DuplexMessage 7 | import io.iohk.metronome.checkpointing.interpreter.messages.InterpreterMessage 8 | import io.iohk.metronome.networking.{NetworkTracers, NetworkEvent} 9 | import io.iohk.metronome.logging.{HybridLog, HybridLogObject, LogTracer} 10 | import io.circe.{Encoder, JsonObject} 11 | 12 | trait CheckpointingNetworkTracers[M] { 13 | type CheckpointingNetworkEvent = 14 | NetworkEvent[CheckpointingAgreement.PKey, M] 15 | 16 | implicit val networkEventHybridLog 17 | : HybridLog[Task, CheckpointingNetworkEvent] = { 18 | import NetworkEvent._ 19 | import io.circe.syntax._ 20 | 21 | implicit val keyEncoder: Encoder[CheckpointingAgreement.PKey] = 22 | Encoder[String].contramap[CheckpointingAgreement.PKey](_.bytes.toHex) 23 | 24 | implicit val peerEncoder 25 | : Encoder.AsObject[Peer[CheckpointingAgreement.PKey]] = 26 | Encoder.AsObject.instance { case Peer(key, address) => 27 | JsonObject( 28 | "publicKey" -> key.asJson, 29 | "address" -> address.toString.asJson 30 | ) 31 | } 32 | 33 | HybridLog.instance[Task, CheckpointingNetworkEvent]( 34 | level = { 35 | case _: ConnectionRegistered[_] => HybridLogObject.Level.Info 36 | case _: ConnectionDeregistered[_] => HybridLogObject.Level.Info 37 | case _ => HybridLogObject.Level.Debug 38 | }, 39 | message = _.getClass.getSimpleName, 40 | event = { 41 | case e: ConnectionUnknown[_] => e.peer.asJsonObject 42 | case e: ConnectionRegistered[_] => e.peer.asJsonObject 43 | case e: ConnectionDeregistered[_] => e.peer.asJsonObject 44 | case e: ConnectionDiscarded[_] => e.peer.asJsonObject 45 | case e: ConnectionSendError[_] => e.peer.asJsonObject 46 | case e: ConnectionFailed[_] => 47 | e.peer.asJsonObject.add("error", e.error.toString.asJson) 48 | case e: ConnectionReceiveError[_] => 49 | e.peer.asJsonObject.add("error", e.error.toString.asJson) 50 | case e: NetworkEvent.MessageReceived[_, _] => 51 | e.peer.asJsonObject 52 | .add("message", e.message.toString.asJson) 53 | case e: NetworkEvent.MessageSent[_, _] => 54 | e.peer.asJsonObject 55 | .add("message", e.message.toString.asJson) 56 | } 57 | ) 58 | } 59 | 60 | implicit val networkEventHybridLogTracer = 61 | LogTracer.hybrid[Task, CheckpointingNetworkEvent] 62 | 63 | implicit val networkHybridLogTracers = 64 | NetworkTracers(networkEventHybridLogTracer) 65 | } 66 | 67 | object CheckpointingLocalNetworkTracers 68 | extends CheckpointingNetworkTracers[InterpreterMessage] 69 | 70 | object CheckpointingRemoteNetworkTracers 71 | extends CheckpointingNetworkTracers[ 72 | DuplexMessage[CheckpointingAgreement, CheckpointingMessage] 73 | ] 74 | -------------------------------------------------------------------------------- /metronome/checkpointing/interpreter/props/src/io/iohk/metronome/checkpointing/interpreter/codecs/DefaultInterpreterCodecsProps.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.checkpointing.interpreter.codecs 2 | 3 | import io.iohk.metronome.checkpointing.interpreter.messages.{ 4 | ArbitraryInstances, 5 | InterpreterMessage 6 | } 7 | import org.scalacheck._ 8 | import org.scalacheck.Prop.forAll 9 | import scala.reflect.ClassTag 10 | import scodec.Codec 11 | 12 | object DefaultInterpreterCodecsProps 13 | extends Properties("DefaultInterpreterCodecs") { 14 | import ArbitraryInstances._ 15 | import DefaultInterpreterCodecs._ 16 | import InterpreterMessage._ 17 | 18 | /** Test that encoding to and decoding from RLP preserves the value. */ 19 | def propRoundTrip[T: Codec: Arbitrary: ClassTag] = 20 | property(implicitly[ClassTag[T]].runtimeClass.getSimpleName) = forAll { 21 | (value0: T) => 22 | val bytes = Codec[T].encode(value0).require 23 | val value1 = Codec[T].decodeValue(bytes).require 24 | value0 == value1 25 | } 26 | 27 | propRoundTrip[NewProposerBlockRequest] 28 | propRoundTrip[NewCheckpointCandidateRequest] 29 | propRoundTrip[CreateBlockBodyRequest] 30 | propRoundTrip[CreateBlockBodyResponse] 31 | propRoundTrip[ValidateBlockBodyRequest] 32 | propRoundTrip[ValidateBlockBodyResponse] 33 | propRoundTrip[NewCheckpointCertificateRequest] 34 | propRoundTrip[InterpreterMessage] 35 | } 36 | -------------------------------------------------------------------------------- /metronome/checkpointing/interpreter/specs/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %d{HH:mm:ss.SSS} %-5level %logger{36} %msg%n 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /metronome/checkpointing/interpreter/src/io/iohk/metronome/checkpointing/interpreter/InterpreterRPC.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.checkpointing.interpreter 2 | 3 | import io.iohk.metronome.checkpointing.models.{Block, Transaction, Ledger} 4 | import io.iohk.metronome.checkpointing.models.CheckpointCertificate 5 | 6 | /** `InterpreterRPC` is the interface that the Checkpointing Service can call to send 7 | * queries and commands to the Interpreter. It provides RPC style methods for some of the 8 | * `InterpeterMessage` types, namely the ones `with Request with FromService`, expecting 9 | * `with Response with FromInterpreter` in return. 10 | * 11 | * It is also the interface that the Interpreter implements in order to process the said 12 | * queries and commands. Thus we have separate client- and server-side implementations. 13 | * 14 | * See the `InterpreterMessage` for longer descriptions of the message types behind 15 | * the RPC facade. 16 | * 17 | * The return values are optional, so the Interpreter always has the option to not 18 | * send any response due to data availability issues, or just not being in a position 19 | * to produce an answer. For example if it's asked whether a block on a different fork 20 | * is valid, and it would have to roll back its current fork to execute the alternative 21 | * blocks that lead up to the one in question, it can decide that this is too expensive 22 | * and stay silent, until the federation decides that everyone has to switch. 23 | */ 24 | trait InterpreterRPC[F[_]] { 25 | 26 | def createBlockBody( 27 | ledger: Ledger, 28 | mempool: Seq[Transaction.ProposerBlock] 29 | ): F[Option[InterpreterRPC.CreateResult]] 30 | 31 | def validateBlockBody( 32 | blockBody: Block.Body, 33 | ledger: Ledger 34 | ): F[Option[Boolean]] 35 | 36 | def newCheckpointCertificate( 37 | checkpointCertificate: CheckpointCertificate 38 | ): F[Unit] 39 | } 40 | 41 | object InterpreterRPC { 42 | 43 | /** The block body created by the interpreter with an optional 44 | * set of items to unconditionally purge from the mempool. 45 | */ 46 | type CreateResult = (Block.Body, Set[Transaction.ProposerBlock]) 47 | 48 | object CreateResult { 49 | val empty: CreateResult = 50 | (Block.Body.empty, Set.empty) 51 | 52 | def apply( 53 | blockBody: Block.Body, 54 | purgeFromMempool: Set[Transaction.ProposerBlock] = Set.empty 55 | ): CreateResult = 56 | blockBody -> purgeFromMempool 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /metronome/checkpointing/interpreter/src/io/iohk/metronome/checkpointing/interpreter/ServiceRPC.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.checkpointing.interpreter 2 | 3 | import io.iohk.metronome.checkpointing.models.Transaction 4 | 5 | /** `ServiceRPC` is the interface that the Interpreter can call on the Service 6 | * side to send notifications. It provides RPC style methods for some of the 7 | * `InterpeterMessage` types, namely the ones `with Request with FromInterpeter`. 8 | * 9 | * See the `InterpreterMessage` for longer descriptions of the message types behind 10 | * the RPC facade. 11 | */ 12 | trait ServiceRPC[F[_]] { 13 | 14 | def newProposerBlock( 15 | proposerBlock: Transaction.ProposerBlock 16 | ): F[Unit] 17 | 18 | def newCheckpointCandidate: F[Unit] 19 | } 20 | -------------------------------------------------------------------------------- /metronome/checkpointing/interpreter/src/io/iohk/metronome/checkpointing/interpreter/codecs/DefaultInterpreterCodecs.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.checkpointing.interpreter.codecs 2 | 3 | import io.iohk.ethereum.rlp 4 | import io.iohk.ethereum.rlp.RLPCodec 5 | import io.iohk.metronome.checkpointing.interpreter.messages.InterpreterMessage 6 | import io.iohk.metronome.checkpointing.models.RLPCodecs 7 | import scodec.{Codec, Attempt} 8 | import scodec.bits.BitVector 9 | import scala.util.Try 10 | 11 | trait DefaultInterpreterCodecs { 12 | import scodec.codecs._ 13 | import scodec.codecs.implicits._ 14 | import InterpreterMessage._ 15 | import RLPCodecs._ 16 | 17 | // Piggybacking on `Codec[BitVector]` so that the length of each RLP 18 | // encoded field is properly carried over to the scodec derived data. 19 | private implicit def codecFromRLPCodec[T: RLPCodec]: Codec[T] = 20 | Codec[BitVector].exmap[T]( 21 | (bits: BitVector) => { 22 | val tryDecode = Try(rlp.decode[T](bits.toByteArray)) 23 | Attempt.fromTry(tryDecode) 24 | }, 25 | (value: T) => { 26 | val bytes = rlp.encode(value) 27 | Attempt.successful(BitVector(bytes)) 28 | } 29 | ) 30 | 31 | private implicit def `Codec[Seq[T]]`[T: Codec]: Codec[Seq[T]] = 32 | Codec[List[T]].xmap(_.toSeq, _.toList) 33 | 34 | private implicit def `Codec[Set[T]]`[T: Codec]: Codec[Set[T]] = 35 | Codec[List[T]].xmap(_.toSet, _.toList) 36 | 37 | implicit val newProposerBlockRequestCodec: Codec[NewProposerBlockRequest] = 38 | Codec.deriveLabelledGeneric 39 | 40 | implicit val newCheckpointCandidateRequestCodec 41 | : Codec[NewCheckpointCandidateRequest] = 42 | Codec.deriveLabelledGeneric 43 | 44 | implicit val createBlockBodyRequestCodec: Codec[CreateBlockBodyRequest] = 45 | Codec.deriveLabelledGeneric 46 | 47 | implicit val createBlockBodyResponseCodec: Codec[CreateBlockBodyResponse] = 48 | Codec.deriveLabelledGeneric 49 | 50 | implicit val validateBlockBodyRequestCodec: Codec[ValidateBlockBodyRequest] = 51 | Codec.deriveLabelledGeneric 52 | 53 | implicit val validateBlockBodyResponseCodec 54 | : Codec[ValidateBlockBodyResponse] = 55 | Codec.deriveLabelledGeneric 56 | 57 | implicit val newCheckpointCertificateRequestCodec 58 | : Codec[NewCheckpointCertificateRequest] = 59 | Codec.deriveLabelledGeneric 60 | 61 | implicit val interpreterMessageCodec: Codec[InterpreterMessage] = 62 | discriminated[InterpreterMessage] 63 | .by(uint4) 64 | .typecase(0, newProposerBlockRequestCodec) 65 | .typecase(1, newCheckpointCandidateRequestCodec) 66 | .typecase(2, createBlockBodyRequestCodec) 67 | .typecase(3, createBlockBodyResponseCodec) 68 | .typecase(4, validateBlockBodyRequestCodec) 69 | .typecase(5, validateBlockBodyResponseCodec) 70 | .typecase(6, newCheckpointCertificateRequestCodec) 71 | } 72 | 73 | object DefaultInterpreterCodecs extends DefaultInterpreterCodecs 74 | -------------------------------------------------------------------------------- /metronome/checkpointing/interpreter/src/io/iohk/metronome/checkpointing/interpreter/package.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.checkpointing 2 | 3 | import io.iohk.metronome.networking.LocalConnectionManager 4 | import io.iohk.metronome.checkpointing.interpreter.messages.InterpreterMessage 5 | 6 | package object interpreter { 7 | type InterpreterConnection[F[_]] = 8 | LocalConnectionManager[ 9 | F, 10 | CheckpointingAgreement.PKey, 11 | InterpreterMessage 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /metronome/checkpointing/interpreter/src/io/iohk/metronome/checkpointing/interpreter/tracing/InterpreterEvent.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.checkpointing.interpreter.tracing 2 | 3 | import io.iohk.metronome.checkpointing.interpreter.messages.InterpreterMessage 4 | import io.iohk.metronome.checkpointing.interpreter.messages.InterpreterMessage._ 5 | 6 | sealed trait InterpreterEvent 7 | 8 | object InterpreterEvent { 9 | 10 | /** The PoW Interpreter did not produce a response in time. */ 11 | case class InterpreterTimeout( 12 | message: InterpreterMessage with Request with FromService 13 | ) extends InterpreterEvent 14 | 15 | /** The Service could not be reached. */ 16 | case class ServiceUnavailable( 17 | message: InterpreterMessage with FromInterpreter 18 | ) extends InterpreterEvent 19 | 20 | /** Error handling a Service message by the Interpreter. */ 21 | case class InterpreterError( 22 | message: InterpreterMessage with Request with FromService, 23 | error: Throwable 24 | ) extends InterpreterEvent 25 | 26 | /** An unexpected error. */ 27 | case class Error(error: Throwable) extends InterpreterEvent 28 | } 29 | -------------------------------------------------------------------------------- /metronome/checkpointing/models/props/src/io/iohk/metronome/checkpointing/models/LedgerProps.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.checkpointing.models 2 | 3 | import org.scalacheck._ 4 | import org.scalacheck.Prop.forAll 5 | 6 | object LedgerProps extends Properties("Ledger") { 7 | import ArbitraryInstances._ 8 | 9 | property("update") = forAll { (ledger: Ledger, transaction: Transaction) => 10 | val updated = ledger.update(transaction) 11 | 12 | transaction match { 13 | case _: Transaction.ProposerBlock 14 | if ledger.proposerBlocks.contains(transaction) => 15 | updated == ledger 16 | 17 | case _: Transaction.ProposerBlock => 18 | updated.proposerBlocks.last == transaction && 19 | updated.proposerBlocks.distinct == updated.proposerBlocks && 20 | updated.maybeLastCheckpoint == ledger.maybeLastCheckpoint 21 | 22 | case _: Transaction.CheckpointCandidate => 23 | updated.maybeLastCheckpoint.contains(transaction) && 24 | updated.proposerBlocks.isEmpty 25 | } 26 | } 27 | 28 | property("hash") = forAll { (ledger1: Ledger, ledger2: Ledger) => 29 | ledger1 == ledger2 && ledger1.hash == ledger2.hash || 30 | ledger1 != ledger2 && ledger1.hash != ledger2.hash 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /metronome/checkpointing/models/props/src/io/iohk/metronome/checkpointing/models/MerkleTreeProps.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.checkpointing.models 2 | 3 | import org.scalacheck.{Gen, Properties} 4 | import org.scalacheck.Arbitrary.arbitrary 5 | import ArbitraryInstances.arbMerkleHash 6 | import org.scalacheck.Prop.forAll 7 | 8 | object MerkleTreeProps extends Properties("MerkleTree") { 9 | 10 | def genElements(max: Int = 256): Gen[List[MerkleTree.Hash]] = 11 | Gen.choose(0, max).flatMap { n => 12 | Gen.listOfN(n, arbitrary(arbMerkleHash)) 13 | } 14 | 15 | property("inclusionProof") = forAll(genElements()) { elements => 16 | val merkleTree = MerkleTree.build(elements) 17 | elements.zipWithIndex.forall { case (elem, idx) => 18 | val fromHash = MerkleTree.generateProofFromHash(merkleTree, elem) 19 | val fromIndex = MerkleTree.generateProofFromIndex(merkleTree, idx) 20 | fromHash == fromIndex && fromHash.isDefined 21 | } 22 | } 23 | 24 | property("proofVerification") = forAll(genElements()) { elements => 25 | val merkleTree = MerkleTree.build(elements) 26 | elements.forall { elem => 27 | val maybeProof = MerkleTree.generateProofFromHash(merkleTree, elem) 28 | maybeProof.exists(MerkleTree.verifyProof(_, merkleTree.hash, elem)) 29 | } 30 | } 31 | 32 | property("noFalseInclusion") = forAll(genElements(128), genElements(32)) { 33 | (elements, other) => 34 | val nonElements = other.diff(elements) 35 | val merkleTree = MerkleTree.build(elements) 36 | 37 | val noFalseProof = nonElements.forall { nonElem => 38 | MerkleTree.generateProofFromHash(merkleTree, nonElem).isEmpty 39 | } 40 | 41 | val noFalseVerification = elements.forall { elem => 42 | val proof = MerkleTree.generateProofFromHash(merkleTree, elem).get 43 | !nonElements.exists(MerkleTree.verifyProof(proof, merkleTree.hash, _)) 44 | } 45 | 46 | noFalseProof && noFalseVerification 47 | } 48 | 49 | property("emptyTree") = { 50 | val empty = MerkleTree.build(Nil) 51 | 52 | MerkleTree.generateProofFromHash(empty, MerkleTree.empty.hash).isEmpty && 53 | empty.hash == MerkleTree.empty.hash 54 | } 55 | 56 | property("singleElementTree") = forAll(arbMerkleHash.arbitrary) { elem => 57 | val tree = MerkleTree.build(elem :: Nil) 58 | 59 | tree.hash == elem && 60 | MerkleTree 61 | .generateProofFromHash(tree, elem) 62 | .map(MerkleTree.verifyProof(_, tree.hash, elem)) 63 | .contains(true) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /metronome/checkpointing/models/props/src/io/iohk/metronome/checkpointing/models/RLPCodecsProps.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.checkpointing.models 2 | 3 | import io.iohk.ethereum.crypto.ECDSASignature 4 | import io.iohk.ethereum.rlp 5 | import io.iohk.ethereum.rlp.RLPCodec 6 | import org.scalacheck._ 7 | import org.scalacheck.Prop.forAll 8 | import scala.reflect.ClassTag 9 | 10 | object RLPCodecsProps extends Properties("RLPCodecs") { 11 | import ArbitraryInstances._ 12 | import RLPCodecs._ 13 | 14 | /** Test that encoding to and decoding from RLP preserves the value. */ 15 | def propRoundTrip[T: RLPCodec: Arbitrary: ClassTag] = 16 | property(implicitly[ClassTag[T]].runtimeClass.getSimpleName) = forAll { 17 | (value0: T) => 18 | val bytes = rlp.encode(value0) 19 | val value1 = rlp.decode[T](bytes) 20 | value0 == value1 21 | } 22 | 23 | propRoundTrip[Ledger] 24 | propRoundTrip[Transaction] 25 | propRoundTrip[Block] 26 | propRoundTrip[ECDSASignature] 27 | propRoundTrip[CheckpointCertificate] 28 | } 29 | -------------------------------------------------------------------------------- /metronome/checkpointing/models/specs/resources/golden/RLPCodec[CheckpointCertificate].rlp: -------------------------------------------------------------------------------- 1 | f901d7f8ccf864a07f80dddfff7f3a0b809480dd53010196127f7f7f01270027ff00803651c4fc7f64a0ffff597f000000007fd4486a017f80a57f5cd17f0000ffd41032f77f0080d77fa0ff51807f7f7fc33d7f00d4ff017f7fd5ff008e01007f94017ffb57800174d9fff864a080ff63ff9a798000177f9ab900419d80345eff00007f000144d7e67fffef800020a0147fa3550080ff6c007fd59028ba1b7f4313eed26a7f52c4ad4dd28d804a289ba001a90100ff0096fe4c7f800199747f80ff01ff8085176c00146d8064fff58001d2917f707f808000f7ff5380617d5201005b7ff84502f842a0800380b0c46000ff2c805bfba901c90104011f99241801477e1fa70b8000a101a080ff1315ea810000ee00000db0ed007fff00a79d0000ffb400ffff274f012531f8ad030aa001ffcdffffff0d157f347f018dd1ec012180ea011900a001379f7f01800e3715f888f886b841000000000000000000000000000000000000000000000000000000000000005e00000000000000000000000000000000000000000000000000000000000000111cb841000000000000000000000000000000000000000000000000000000000000002700000000000000000000000000000000000000000000000000000000000000461c -------------------------------------------------------------------------------- /metronome/checkpointing/models/specs/resources/golden/RLPCodec[CheckpointCertificate].txt: -------------------------------------------------------------------------------- 1 | CheckpointCertificate(NonEmptyList(Header(ByteVector(32 bytes, 0x7f80dddfff7f3a0b809480dd53010196127f7f7f01270027ff00803651c4fc7f),100,ByteVector(32 bytes, 0xffff597f000000007fd4486a017f80a57f5cd17f0000ffd41032f77f0080d77f),ByteVector(32 bytes, 0xff51807f7f7fc33d7f00d4ff017f7fd5ff008e01007f94017ffb57800174d9ff)), Header(ByteVector(32 bytes, 0x80ff63ff9a798000177f9ab900419d80345eff00007f000144d7e67fffef8000),32,ByteVector(32 bytes, 0x147fa3550080ff6c007fd59028ba1b7f4313eed26a7f52c4ad4dd28d804a289b),ByteVector(32 bytes, 0x01a90100ff0096fe4c7f800199747f80ff01ff8085176c00146d8064fff58001))),CheckpointCandidate(BitVector(136 bits, 0x7f707f808000f7ff5380617d5201005b7f)),Proof(2,Vector(ByteVector(32 bytes, 0x800380b0c46000ff2c805bfba901c90104011f99241801477e1fa70b8000a101), ByteVector(32 bytes, 0x80ff1315ea810000ee00000db0ed007fff00a79d0000ffb400ffff274f012531))),QuorumCertificate(Commit,10,ByteVector(32 bytes, 0x01ffcdffffff0d157f347f018dd1ec012180ea011900a001379f7f01800e3715),GroupSignature(List(ECDSASignature(94,17,28), ECDSASignature(39,70,28))))) -------------------------------------------------------------------------------- /metronome/checkpointing/models/specs/resources/golden/RLPCodec[Ledger].rlp: -------------------------------------------------------------------------------- 1 | f874e9e8a780a2017fe6000001f6ff6562991fa96676ab000100c6eaff7fb080d1017f4900047f00fbb1ff17f848de9d80d80100567f8f7f4d00ff27843963ffff7aff7f4101ff7f00ffff8001e8a700cb05f2ffff2dd91fff57446e803f3001d7cf80e3b5007f7601ff0708808001e000a0ff6e8057 -------------------------------------------------------------------------------- /metronome/checkpointing/models/specs/resources/golden/RLPCodec[Ledger].txt: -------------------------------------------------------------------------------- 1 | Ledger(Some(CheckpointCandidate(BitVector(312 bits, 0x80a2017fe6000001f6ff6562991fa96676ab000100c6eaff7fb080d1017f4900047f00fbb1ff17))),Vector(ProposerBlock(BitVector(232 bits, 0x80d80100567f8f7f4d00ff27843963ffff7aff7f4101ff7f00ffff8001)), ProposerBlock(BitVector(312 bits, 0x00cb05f2ffff2dd91fff57446e803f3001d7cf80e3b5007f7601ff0708808001e000a0ff6e8057)))) -------------------------------------------------------------------------------- /metronome/checkpointing/models/specs/resources/golden/RLPCodec[Transaction].rlp: -------------------------------------------------------------------------------- 1 | ef01adcbff7913ff0000ac1a01009bb245579601b680016500cf02597f070080c318000004ad002faa27b58001ea7f00 -------------------------------------------------------------------------------- /metronome/checkpointing/models/specs/resources/golden/RLPCodec[Transaction].txt: -------------------------------------------------------------------------------- 1 | ProposerBlock(BitVector(360 bits, 0xcbff7913ff0000ac1a01009bb245579601b680016500cf02597f070080c318000004ad002faa27b58001ea7f00)) -------------------------------------------------------------------------------- /metronome/checkpointing/models/specs/src/io/iohk/metronome/checkpointing/models/CheckpointingSigningSpec.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.checkpointing.models 2 | 3 | import io.iohk.metronome.crypto.ECKeyPair 4 | import io.iohk.metronome.checkpointing.CheckpointingAgreement 5 | import io.iohk.metronome.hotstuff.consensus.basic.VotingPhase 6 | import io.iohk.metronome.hotstuff.consensus.{ 7 | Federation, 8 | LeaderSelection, 9 | ViewNumber 10 | } 11 | import org.scalatest.flatspec.AnyFlatSpec 12 | import org.scalatest.matchers.should.Matchers 13 | 14 | import java.security.SecureRandom 15 | import scodec.bits.ByteVector 16 | 17 | /** Simple test cases to verify type interoperability. 18 | * 19 | * See [[io.iohk.metronome.hotstuff.consensus.basic.Secp256k1SigningProps]] for a more in-depth test 20 | */ 21 | class CheckpointingSigningSpec extends AnyFlatSpec with Matchers { 22 | import ArbitraryInstances._ 23 | 24 | val keyPairs = IndexedSeq.fill(2)(ECKeyPair.generate(new SecureRandom)) 25 | val federation = Federation(keyPairs.map(_.pub))(LeaderSelection.RoundRobin) 26 | .getOrElse(throw new Exception("Could not build federation")) 27 | 28 | "Checkpoint signing" should "work :)" in { 29 | val signing = new CheckpointingSigning(Block.Header.Hash(ByteVector.empty)) 30 | 31 | val phase = sample[VotingPhase] 32 | val viewNumber = sample[ViewNumber] 33 | val hash = sample[CheckpointingAgreement.Hash] 34 | 35 | val partialSigs = 36 | keyPairs.map(kp => signing.sign(kp.prv, phase, viewNumber, hash)) 37 | 38 | val groupSig = signing.combine(partialSigs) 39 | 40 | signing.validate( 41 | federation, 42 | groupSig, 43 | phase, 44 | viewNumber, 45 | hash 46 | ) shouldBe true 47 | } 48 | 49 | it should "accept the genesis with no signatures" in { 50 | val genesisHash = sample[CheckpointingAgreement.Hash] 51 | val signing = new CheckpointingSigning(genesisHash) 52 | 53 | val phase = sample[VotingPhase] 54 | val viewNumber = sample[ViewNumber] 55 | val groupSig = signing.combine(Nil) 56 | 57 | signing.validate( 58 | federation, 59 | groupSig, 60 | phase, 61 | viewNumber, 62 | genesisHash 63 | ) shouldBe true 64 | } 65 | 66 | it should "not accept the genesis with signatures" in { 67 | val genesisHash = sample[CheckpointingAgreement.Hash] 68 | val signing = new CheckpointingSigning(genesisHash) 69 | 70 | val phase = sample[VotingPhase] 71 | val viewNumber = sample[ViewNumber] 72 | 73 | val partialSigs = 74 | keyPairs.map(kp => signing.sign(kp.prv, phase, viewNumber, genesisHash)) 75 | 76 | val groupSig = signing.combine(partialSigs) 77 | 78 | signing.validate( 79 | federation, 80 | groupSig, 81 | phase, 82 | viewNumber, 83 | genesisHash 84 | ) shouldBe false 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /metronome/checkpointing/models/src/io/iohk/metronome/checkpointing/CheckpointingAgreement.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.checkpointing 2 | 3 | import io.iohk.metronome.hotstuff.consensus 4 | import io.iohk.metronome.hotstuff.consensus.basic.{ 5 | Agreement, 6 | Secp256k1Agreement 7 | } 8 | 9 | object CheckpointingAgreement extends Secp256k1Agreement { 10 | override type Block = models.Block 11 | override type Hash = models.Block.Hash 12 | 13 | implicit val block: consensus.basic.Block[CheckpointingAgreement] = 14 | new consensus.basic.Block[CheckpointingAgreement] { 15 | override def blockHash(b: models.Block) = 16 | b.hash 17 | override def parentBlockHash(b: models.Block) = 18 | b.header.parentHash 19 | override def height(b: Block): Long = 20 | b.header.height 21 | override def isValid(b: models.Block) = 22 | models.Block.isValid(b) 23 | } 24 | 25 | type GroupSignature = Agreement.GroupSignature[CheckpointingAgreement] 26 | } 27 | -------------------------------------------------------------------------------- /metronome/checkpointing/models/src/io/iohk/metronome/checkpointing/models/CheckpointingSigning.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.checkpointing.models 2 | 3 | import io.iohk.ethereum.rlp 4 | import io.iohk.metronome.hotstuff.consensus.{Federation, ViewNumber} 5 | import io.iohk.metronome.hotstuff.consensus.basic.{ 6 | Secp256k1Signing, 7 | VotingPhase, 8 | Signing 9 | } 10 | import io.iohk.metronome.checkpointing.CheckpointingAgreement 11 | import io.iohk.metronome.checkpointing.models.RLPCodecs._ 12 | import scodec.bits.ByteVector 13 | 14 | class CheckpointingSigning( 15 | genesisHash: Block.Hash 16 | ) extends Secp256k1Signing[CheckpointingAgreement]((phase, viewNumber, hash) => 17 | ByteVector( 18 | rlp.encode(phase) ++ rlp.encode(viewNumber) ++ rlp.encode(hash) 19 | ) 20 | ) { 21 | 22 | /** Override quorum certificate validation rule so we accept the quorum 23 | * certificate we can determinsiticially fabricate without a group signature. 24 | */ 25 | override def validate( 26 | federation: Federation[CheckpointingAgreement.PKey], 27 | signature: Signing.GroupSig[CheckpointingAgreement], 28 | phase: VotingPhase, 29 | viewNumber: ViewNumber, 30 | blockHash: CheckpointingAgreement.Hash 31 | ): Boolean = 32 | if (blockHash == genesisHash) { 33 | signature.sig.isEmpty 34 | } else { 35 | super.validate( 36 | federation, 37 | signature, 38 | phase, 39 | viewNumber, 40 | blockHash 41 | ) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /metronome/checkpointing/models/src/io/iohk/metronome/checkpointing/models/Ledger.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.checkpointing.models 2 | 3 | /** Current state of the ledger after applying all previous blocks. 4 | * 5 | * Basically it's the last checkpoint, plus any accumulated proposer blocks 6 | * since then. Initially the last checkpoint is empty; conceptually it could 7 | * be the genesis block of the PoW chain, but we don't know what that is 8 | * until we talk to the interpreter, and we also can't produce it on our 9 | * own since it's opaque data. 10 | */ 11 | case class Ledger( 12 | maybeLastCheckpoint: Option[Transaction.CheckpointCandidate], 13 | proposerBlocks: IndexedSeq[Transaction.ProposerBlock] 14 | ) extends RLPHash[Ledger, Ledger.Hash] { 15 | 16 | /** Apply a validated transaction to produce the next ledger state. 17 | * 18 | * The transaction should have been validated against the PoW ledger 19 | * by this point, so we know for example that the new checkpoint is 20 | * a valid extension of the previous one. 21 | */ 22 | def update(transaction: Transaction): Ledger = 23 | transaction match { 24 | case t @ Transaction.ProposerBlock(_) => 25 | if (proposerBlocks.contains(t)) 26 | this 27 | else 28 | copy(proposerBlocks = proposerBlocks :+ t) 29 | 30 | case t @ Transaction.CheckpointCandidate(_) => 31 | Ledger(Some(t), Vector.empty) 32 | } 33 | 34 | def update(transactions: Iterable[Transaction]): Ledger = 35 | transactions.foldLeft(this)(_ update _) 36 | } 37 | 38 | object Ledger extends RLPHashCompanion[Ledger]()(RLPCodecs.rlpLedger) { 39 | val empty = Ledger(None, Vector.empty) 40 | } 41 | -------------------------------------------------------------------------------- /metronome/checkpointing/models/src/io/iohk/metronome/checkpointing/models/Mempool.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.checkpointing.models 2 | 3 | import io.iohk.metronome.checkpointing.models.Transaction.ProposerBlock 4 | 5 | case class Mempool( 6 | proposerBlocks: IndexedSeq[ProposerBlock], 7 | hasNewCheckpointCandidate: Boolean 8 | ) { 9 | def isEmpty: Boolean = 10 | proposerBlocks.isEmpty && !hasNewCheckpointCandidate 11 | 12 | def add(proposerBlock: ProposerBlock): Mempool = 13 | copy(proposerBlocks = proposerBlocks :+ proposerBlock) 14 | 15 | def add(proposerBlocks: Iterable[ProposerBlock]): Mempool = 16 | proposerBlocks.foldLeft(this)(_ add _) 17 | 18 | def removeProposerBlocks(toRemove: Seq[ProposerBlock]): Mempool = 19 | copy(proposerBlocks = proposerBlocks.diff(toRemove)) 20 | 21 | def withNewCheckpointCandidate: Mempool = 22 | copy(hasNewCheckpointCandidate = true) 23 | 24 | def clearCheckpointCandidate: Mempool = 25 | copy(hasNewCheckpointCandidate = false) 26 | } 27 | 28 | object Mempool { 29 | 30 | /** Initial Mempool state 31 | * 32 | * Starting with `hasNewCheckpointCandidate` in case the PoW side 33 | * is unable to produce checkpoint notifications 34 | */ 35 | val init: Mempool = Mempool(Vector.empty, true) 36 | } 37 | -------------------------------------------------------------------------------- /metronome/checkpointing/models/src/io/iohk/metronome/checkpointing/models/RLPHash.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.checkpointing.models 2 | 3 | import io.iohk.ethereum.rlp 4 | import io.iohk.ethereum.rlp.RLPEncoder 5 | import io.iohk.metronome.crypto 6 | import io.iohk.metronome.crypto.hash.Keccak256 7 | import io.iohk.metronome.core.Tagger 8 | import scodec.bits.ByteVector 9 | import scala.language.implicitConversions 10 | 11 | /** Type class to produce a specific type of hash based on the RLP 12 | * representation of a type, where the hash type is typically 13 | * defined in the companion object of the type. 14 | */ 15 | trait RLPHasher[T] { 16 | type Hash 17 | def hash(value: T): Hash 18 | } 19 | object RLPHasher { 20 | type Aux[T, H] = RLPHasher[T] { 21 | type Hash = H 22 | } 23 | } 24 | 25 | /** Base class for types that have a hash value based on their RLP representation. */ 26 | abstract class RLPHash[T, H](implicit ev: RLPHasher.Aux[T, H]) { self: T => 27 | lazy val hash: H = ev.hash(self) 28 | } 29 | 30 | /** Base class for companion objects for types that need hashes based on RLP. 31 | * 32 | * Every companion will define a separate `Hash` type, so we don't mix them up. 33 | */ 34 | abstract class RLPHashCompanion[T: RLPEncoder] extends RLPHasher[T] { self => 35 | object Hash extends Tagger[ByteVector] 36 | override type Hash = Hash.Tagged 37 | 38 | override def hash(value: T): Hash = 39 | Hash(Keccak256(rlp.encode(value))) 40 | 41 | implicit val hasher: RLPHasher.Aux[T, Hash] = this 42 | 43 | implicit def `Hash => crypto.Hash`(h: Hash): crypto.hash.Hash = 44 | crypto.hash.Hash(h) 45 | } 46 | -------------------------------------------------------------------------------- /metronome/checkpointing/models/src/io/iohk/metronome/checkpointing/models/Transaction.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.checkpointing.models 2 | 3 | import scodec.bits.BitVector 4 | 5 | /** Transactions are what comprise the block body used by the Checkpointing Service. 6 | * 7 | * The HotStuff BFT Agreement doesn't need to know about them, their execution and 8 | * validation is delegated to the Checkpointing Service, which, in turn, delegates 9 | * to the interpreter. The only component that truly has to understand the contents 10 | * is the PoW specific interpreter. 11 | * 12 | * What the Checkpointing Service has to know is the different kinds of transactions 13 | * we support, which is to register proposer blocks in the ledger, required by Advocate, 14 | * and to register checkpoint candidates. 15 | */ 16 | sealed trait Transaction extends RLPHash[Transaction, Transaction.Hash] 17 | 18 | object Transaction 19 | extends RLPHashCompanion[Transaction]()(RLPCodecs.rlpTransaction) { 20 | 21 | /** In PoW chains that support Advocate checkpointing, the Checkpoint Certificate 22 | * can enforce the inclusion of proposed blocks on the chain via references; think 23 | * uncle blocks that also get executed. 24 | * 25 | * In order to know which proposed blocks can be enforced, i.e. ones that are valid 26 | * and have saturated the network, first the federation members need to reach BFT 27 | * agreement over the list of existing proposer blocks. 28 | * 29 | * The `ProposerBlock` transaction adds one of these blocks that exist on the PoW 30 | * chain to the Checkpointing Ledger, iff it can be validated by the members. 31 | * 32 | * The contents of the transaction are opaque, they only need to be understood 33 | * by the PoW side interpreter. 34 | * 35 | * Using Advocate is optional; if the PoW chain doesn't support references, 36 | * it will just use `CheckpointCandidate` transactions. 37 | */ 38 | case class ProposerBlock(value: BitVector) extends Transaction 39 | 40 | /** When a federation member is leading a round, it will ask the PoW side interpreter 41 | * if it wants to propose a checkpoint candidate. The interpreter decides if the 42 | * circumstances are right, e.g. enough new blocks have been build on the previous 43 | * checkpoint that a new one has to be issued. If so, a `CheckpointCandidate` 44 | * transaction is added to the next block, which is sent to the HotStuff replicas 45 | * in a `Prepare` message, to be validated and committed. 46 | * 47 | * If the BFT agreement is successful, a Checkpoint Certificate will be formed 48 | * during block execution which will include the `CheckpointCandidate`. 49 | * 50 | * The contents of the transaction are opaque, they only need to be understood 51 | * by teh PoW side interpreter, either for validation, or for following the 52 | * fork indicated by the checkpoint. 53 | */ 54 | case class CheckpointCandidate(value: BitVector) extends Transaction 55 | } 56 | -------------------------------------------------------------------------------- /metronome/checkpointing/models/src/io/iohk/metronome/checkpointing/package.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome 2 | 3 | package object checkpointing { 4 | type CheckpointingAgreement = CheckpointingAgreement.type 5 | } 6 | -------------------------------------------------------------------------------- /metronome/checkpointing/service/props/src/io/iohk/metronome/checkpointing/service/storage/LedgerStorageProps.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.checkpointing.service.storage 2 | 3 | import cats.implicits._ 4 | import io.iohk.metronome.core.Tagger 5 | import io.iohk.metronome.checkpointing.models.Ledger 6 | import io.iohk.metronome.checkpointing.models.ArbitraryInstances 7 | import io.iohk.metronome.storage.{KVCollection, KVStoreState} 8 | import org.scalacheck.{Properties, Gen, Arbitrary}, Arbitrary.arbitrary 9 | import org.scalacheck.Prop.{forAll, all, propBoolean} 10 | import scodec.Codec 11 | import scodec.bits.BitVector 12 | import org.scalacheck.Shrink 13 | import scala.annotation.nowarn 14 | 15 | object LedgerStorageProps extends Properties("LedgerStorage") { 16 | import ArbitraryInstances.arbLedger 17 | 18 | type Namespace = String 19 | object Namespace { 20 | val Ledgers = "ledgers" 21 | val LedgerMeta = "ledger-meta" 22 | } 23 | 24 | /** The in-memory KVStoreState doesn't invoke the codecs. */ 25 | implicit def neverUsedCodec[T] = 26 | Codec[T]( 27 | (_: T) => sys.error("Didn't expect to encode."), 28 | (_: BitVector) => sys.error("Didn't expect to decode.") 29 | ) 30 | 31 | object TestKVStore extends KVStoreState[Namespace] 32 | 33 | object HistorySize extends Tagger[Int] { 34 | @nowarn 35 | implicit val shrink: Shrink[HistorySize] = Shrink(s => Stream.empty) 36 | implicit val arb: Arbitrary[HistorySize] = Arbitrary { 37 | Gen.choose(1, 10).map(HistorySize(_)) 38 | } 39 | } 40 | type HistorySize = HistorySize.Tagged 41 | 42 | property("buffer") = forAll( 43 | for { 44 | ledgers <- arbitrary[List[Ledger]] 45 | maxSize <- arbitrary[HistorySize] 46 | } yield (ledgers, maxSize) 47 | ) { case (ledgers, maxSize) => 48 | val ledgerStorage = new LedgerStorage[Namespace]( 49 | new KVCollection[Namespace, Ledger.Hash, Ledger](Namespace.Ledgers), 50 | Namespace.LedgerMeta, 51 | maxHistorySize = maxSize 52 | ) 53 | 54 | val store = 55 | TestKVStore 56 | .compile(ledgers.traverse(ledgerStorage.put)) 57 | .runS(Map.empty) 58 | .value 59 | 60 | def getByHash(ledgerHash: Ledger.Hash) = 61 | TestKVStore.compile(ledgerStorage.get(ledgerHash)).run(store) 62 | 63 | val ledgerMap = store.get(Namespace.Ledgers).getOrElse(Map.empty[Any, Any]) 64 | 65 | val (current, old) = { 66 | val (lastN, prev) = ledgers.reverse.splitAt(maxSize) 67 | // There can be duplicates, re-insertions. 68 | (lastN, prev.filterNot(lastN.contains)) 69 | } 70 | 71 | all( 72 | "max-history" |: ledgerMap.values.size <= maxSize, 73 | "contains current" |: current.forall { ledger => 74 | getByHash(ledger.hash).contains(ledger) 75 | }, 76 | "not contain old" |: old.forall { ledger => 77 | getByHash(ledger.hash).isEmpty 78 | } 79 | ) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /metronome/checkpointing/service/src/io/iohk/metronome/checkpointing/service/messages/CheckpointingMessage.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.checkpointing.service.messages 2 | 3 | import io.iohk.metronome.checkpointing.models.Ledger 4 | import io.iohk.metronome.core.messages.{RPCMessage, RPCMessageCompanion} 5 | 6 | /** Checkpointing specific messages that the HotStuff service doesn't handle, 7 | * which is the synchronisation of committed ledger state. 8 | * 9 | * These will be wrapped in an `ApplicationMessage`. 10 | */ 11 | sealed trait CheckpointingMessage { self: RPCMessage => } 12 | 13 | object CheckpointingMessage extends RPCMessageCompanion { 14 | 15 | /** Request the ledger state given by a specific hash. 16 | * 17 | * The hash is something coming from a block that was 18 | * pointed at by a Commit Q.C. 19 | */ 20 | case class GetStateRequest( 21 | requestId: RequestId, 22 | stateHash: Ledger.Hash 23 | ) extends CheckpointingMessage 24 | with Request 25 | 26 | case class GetStateResponse( 27 | requestId: RequestId, 28 | state: Ledger 29 | ) extends CheckpointingMessage 30 | with Response 31 | 32 | implicit val getStatePair = 33 | pair[GetStateRequest, GetStateResponse] 34 | } 35 | -------------------------------------------------------------------------------- /metronome/checkpointing/service/src/io/iohk/metronome/checkpointing/service/storage/LedgerStorage.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.checkpointing.service.storage 2 | 3 | import cats.implicits._ 4 | import io.iohk.metronome.checkpointing.models.Ledger 5 | import io.iohk.metronome.storage.{KVRingBuffer, KVCollection, KVStore} 6 | import scodec.Codec 7 | 8 | /** Storing the committed and executed checkpoint ledger. 9 | * 10 | * Strictly speaking the application only needs the committed state, 11 | * since it has been signed by the federation and we know it's not 12 | * going to be rolled back. Uncommitted state can be kept in memory. 13 | * 14 | * However we want to support other nodes catching up by: 15 | * 1. requesting the latest Commit Q.C., then 16 | * 2. requesting the block the Commit Q.C. points at, then 17 | * 3. requesting the ledger state the header points at. 18 | * 19 | * We have to allow some time before we get rid of historical state, 20 | * so that it doesn't disappear between step 2 and 3, resulting in 21 | * nodes trying and trying to catch up but always missing the beat. 22 | * 23 | * Therefore we keep a collection of the last N ledgers in a ring buffer. 24 | */ 25 | class LedgerStorage[N]( 26 | ledgerColl: KVCollection[N, Ledger.Hash, Ledger], 27 | ledgerMetaNamespace: N, 28 | maxHistorySize: Int 29 | )(implicit codecH: Codec[Ledger.Hash]) 30 | extends KVRingBuffer[N, Ledger.Hash, Ledger]( 31 | ledgerColl, 32 | ledgerMetaNamespace, 33 | maxHistorySize 34 | ) { 35 | 36 | /** Save a new ledger and remove the oldest one, if we reached 37 | * the maximum history size. Since we only store committed 38 | * state, they form a chain. They will always be retrieved 39 | * by going through a block pointing at them directly. 40 | */ 41 | def put(ledger: Ledger): KVStore[N, Unit] = 42 | put(ledger.hash, ledger).void 43 | } 44 | -------------------------------------------------------------------------------- /metronome/checkpointing/service/src/io/iohk/metronome/checkpointing/service/tracing/CheckpointingEvent.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.checkpointing.service.tracing 2 | 3 | import io.iohk.metronome.checkpointing.CheckpointingAgreement 4 | import io.iohk.metronome.checkpointing.interpreter.messages.InterpreterMessage 5 | import io.iohk.metronome.checkpointing.service.messages.CheckpointingMessage 6 | import io.iohk.metronome.checkpointing.models.{Block, Ledger} 7 | import io.iohk.metronome.checkpointing.models.CheckpointCertificate 8 | 9 | sealed trait CheckpointingEvent 10 | 11 | object CheckpointingEvent { 12 | import InterpreterMessage._ 13 | 14 | /** The Interpreter did not produce a response in time. */ 15 | case class InterpreterTimeout( 16 | message: InterpreterMessage with Request with FromService 17 | ) extends CheckpointingEvent 18 | 19 | /** The Interpreter could not be reached. */ 20 | case class InterpreterUnavailable( 21 | message: InterpreterMessage with FromService 22 | ) extends CheckpointingEvent 23 | 24 | /** The Interpreter sent us a response which we ignored, most likely because it was late. */ 25 | case class InterpreterResponseIgnored( 26 | message: InterpreterMessage with Response with FromInterpreter, 27 | maybeError: Option[Throwable] 28 | ) extends CheckpointingEvent 29 | 30 | /** A peer did not produce a response in time. */ 31 | case class NetworkTimeout( 32 | recipient: CheckpointingAgreement.PKey, 33 | message: CheckpointingMessage with CheckpointingMessage.Request 34 | ) extends CheckpointingEvent 35 | 36 | /** A peer sent an unsolicited response, or the response arrived too late. */ 37 | case class NetworkResponseIgnored( 38 | from: CheckpointingAgreement.PKey, 39 | message: CheckpointingMessage with CheckpointingMessage.Response, 40 | maybeError: Option[Throwable] 41 | ) extends CheckpointingEvent 42 | 43 | /** This node has created a block, to be proposed to the federation. */ 44 | case class Proposing( 45 | block: Block 46 | ) extends CheckpointingEvent 47 | 48 | /** The federation committed to a new state. */ 49 | case class NewState(state: Ledger) extends CheckpointingEvent 50 | 51 | /** Pushing a new certificate to the interpreter. */ 52 | case class NewCheckpointCertificate(certificate: CheckpointCertificate) 53 | extends CheckpointingEvent 54 | 55 | /** A block could not be validated because we could not produce the corresponding state. */ 56 | case class StateUnavailable(block: Block) extends CheckpointingEvent 57 | 58 | /** The interpreter thought the block was invalid. */ 59 | case class InterpreterValidationFailed(block: Block) 60 | extends CheckpointingEvent 61 | 62 | /** An unexpected error. */ 63 | case class Error(error: Throwable) extends CheckpointingEvent 64 | } 65 | -------------------------------------------------------------------------------- /metronome/config/specs/resources/array.conf: -------------------------------------------------------------------------------- 1 | { 2 | field: ["valueA", "valueB", "valueC"] 3 | } 4 | -------------------------------------------------------------------------------- /metronome/config/specs/resources/complex.conf: -------------------------------------------------------------------------------- 1 | metronome { 2 | metrics { 3 | enabled = false 4 | } 5 | network { 6 | bootstrap = [ 7 | "localhost:40001" 8 | ], 9 | timeout = 5s 10 | max-packet-size = 512kB 11 | max-incoming-connections = 10 12 | client-id = null 13 | } 14 | blockchain { 15 | consensus = "research-and-development" 16 | default { 17 | max-block-size = 1MB 18 | view-timeout = 15s 19 | } 20 | research-and-development = ${metronome.blockchain.default} { 21 | max-block-size = 10MB 22 | } 23 | main = ${metronome.blockchain.default} { 24 | view-timeout = 5s 25 | } 26 | } 27 | chain-id = test-chain 28 | } 29 | -------------------------------------------------------------------------------- /metronome/config/specs/resources/override.conf: -------------------------------------------------------------------------------- 1 | override { 2 | metrics { 3 | enabled = false 4 | } 5 | network { 6 | bootstrap = [ 7 | "localhost:40001", 8 | "localhost:40002" 9 | ] 10 | } 11 | optional = null 12 | numeric = 123 13 | textual = Hello World 14 | boolean = true 15 | } 16 | 17 | # Other setting that shouldn't be affected. 18 | other { 19 | metrics { 20 | enabled = false 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /metronome/config/specs/resources/simple.conf: -------------------------------------------------------------------------------- 1 | # The root we are going to start from. 2 | simple { 3 | # Property name with a dash. 4 | nested-structure { 5 | foo = 10 6 | # Property name with an underscore. 7 | bar_baz { 8 | spam = eggs 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /metronome/core/specs/src/io/iohk/metronome/core/PipeSpec.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.core 2 | 3 | import org.scalatest.flatspec.AsyncFlatSpec 4 | import monix.eval.Task 5 | import monix.execution.Scheduler.Implicits.global 6 | import org.scalatest.matchers.should.Matchers 7 | 8 | class PipeSpec extends AsyncFlatSpec with Matchers { 9 | 10 | behavior of "Pipe" 11 | 12 | it should "send messages between the sides" in { 13 | val test = for { 14 | pipe <- Pipe[Task, String, Int] 15 | _ <- pipe.left.send("foo") 16 | _ <- pipe.left.send("bar") 17 | _ <- pipe.right.send(1) 18 | rs <- pipe.right.receive.take(2).toListL 19 | ls <- pipe.left.receive.headOptionL 20 | } yield { 21 | rs shouldBe List("foo", "bar") 22 | ls shouldBe Some(1) 23 | } 24 | 25 | test.runToFuture 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /metronome/core/specs/src/io/iohk/metronome/core/fibers/FiberSetSpec.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.core.fibers 2 | 3 | import monix.eval.Task 4 | import monix.execution.Scheduler.Implicits.global 5 | import monix.execution.atomic.AtomicInt 6 | import org.scalatest.compatible.Assertion 7 | import org.scalatest.flatspec.AsyncFlatSpec 8 | import org.scalatest.matchers.should.Matchers 9 | import org.scalatest.Inside 10 | import scala.concurrent.duration._ 11 | 12 | class FiberSetSpec extends AsyncFlatSpec with Matchers with Inside { 13 | 14 | def test(t: Task[Assertion]) = 15 | t.timeout(10.seconds).runToFuture 16 | 17 | behavior of "FiberSet" 18 | 19 | it should "reject new submissions after shutdown" in test { 20 | FiberSet[Task].allocated.flatMap { case (fiberSet, release) => 21 | for { 22 | _ <- fiberSet.submit(Task("foo")) 23 | _ <- release 24 | r <- fiberSet.submit(Task("bar")).attempt 25 | } yield { 26 | inside(r) { case Left(ex) => 27 | ex shouldBe a[IllegalStateException] 28 | ex.getMessage should include("shut down") 29 | } 30 | } 31 | } 32 | } 33 | 34 | it should "cancel and raise errors in already submitted tasks after shutdown" in test { 35 | FiberSet[Task].allocated.flatMap { case (fiberSet, release) => 36 | for { 37 | r <- fiberSet.submit(Task.never) 38 | _ <- release 39 | r <- r.attempt 40 | } yield { 41 | inside(r) { case Left(ex) => 42 | ex shouldBe a[DeferredTask.CanceledException] 43 | } 44 | } 45 | } 46 | } 47 | 48 | it should "return a value we can wait on" in test { 49 | FiberSet[Task].use { fiberSet => 50 | for { 51 | task <- fiberSet.submit(Task("spam")) 52 | value <- task 53 | } yield { 54 | value shouldBe "spam" 55 | } 56 | } 57 | } 58 | 59 | it should "process tasks concurrently" in test { 60 | FiberSet[Task].use { fiberSet => 61 | val running = AtomicInt(0) 62 | val maxRunning = AtomicInt(0) 63 | 64 | for { 65 | handles <- Task.traverse(1 to 10) { _ => 66 | val task = for { 67 | r <- Task(running.incrementAndGet()) 68 | _ <- Task(maxRunning.getAndTransform(m => math.max(m, r))) 69 | _ <- Task.sleep(20.millis) // Increase chance for overlap. 70 | _ <- Task(running.decrement()) 71 | } yield () 72 | 73 | fiberSet.submit(task) 74 | } 75 | _ <- Task.parTraverse(handles)(identity) 76 | } yield { 77 | running.get() shouldBe 0 78 | maxRunning.get() should be > 1 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /metronome/core/specs/src/io/iohk/metronome/core/messages/RPCTrackerSpec.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.core.messages 2 | 3 | import monix.eval.Task 4 | import monix.execution.Scheduler.Implicits.global 5 | import org.scalatest.flatspec.AsyncFlatSpec 6 | import org.scalatest.matchers.should.Matchers 7 | import org.scalatest.compatible.Assertion 8 | import org.scalatest.Inside 9 | import scala.concurrent.Future 10 | import scala.concurrent.duration._ 11 | 12 | class RPCTrackerSpec extends AsyncFlatSpec with Matchers with Inside { 13 | 14 | sealed trait TestMessage extends RPCMessage 15 | object TestMessage extends RPCMessageCompanion { 16 | case class FooRequest(requestId: RequestId) extends TestMessage with Request 17 | case class FooResponse(requestId: RequestId, value: Int) 18 | extends TestMessage 19 | with Response 20 | case class BarRequest(requestId: RequestId) extends TestMessage with Request 21 | case class BarResponse(requestId: RequestId, value: String) 22 | extends TestMessage 23 | with Response 24 | 25 | implicit val foo = pair[FooRequest, FooResponse] 26 | implicit val bar = pair[BarRequest, BarResponse] 27 | } 28 | import TestMessage._ 29 | 30 | def test( 31 | f: RPCTracker[Task, TestMessage] => Task[Assertion] 32 | ): Future[Assertion] = 33 | RPCTracker[Task, TestMessage](10.seconds) 34 | .flatMap(f) 35 | .timeout(5.seconds) 36 | .runToFuture 37 | 38 | behavior of "RPCTracker" 39 | 40 | it should "complete responses within the timeout" in test { tracker => 41 | val req = FooRequest(RequestId()) 42 | val res = FooResponse(req.requestId, 1) 43 | for { 44 | join <- tracker.register(req) 45 | ok <- tracker.complete(res) 46 | got <- join 47 | } yield { 48 | ok shouldBe Right(true) 49 | got shouldBe Some(res) 50 | } 51 | } 52 | 53 | it should "complete responses with None after the timeout" in test { 54 | tracker => 55 | val req = FooRequest(RequestId()) 56 | val res = FooResponse(req.requestId, 1) 57 | for { 58 | join <- tracker.register(req, timeout = 50.millis) 59 | _ <- Task.sleep(100.millis) 60 | ok <- tracker.complete(res) 61 | got <- join 62 | } yield { 63 | ok shouldBe Right(false) 64 | got shouldBe empty 65 | } 66 | } 67 | 68 | it should "complete responses with None if the wrong type of response arrives" in test { 69 | tracker => 70 | for { 71 | rid <- RequestId[Task] 72 | req = FooRequest(rid) 73 | res = BarResponse(rid, "one") 74 | join <- tracker.register(req) 75 | ok <- tracker.complete(res) 76 | got <- join 77 | } yield { 78 | inside(ok) { case Left(error) => 79 | error.getMessage should include("Invalid response type") 80 | } 81 | got shouldBe empty 82 | } 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /metronome/core/src/io/iohk/metronome/core/Pipe.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.core 2 | 3 | import cats.implicits._ 4 | import cats.effect.{Concurrent, ContextShift, Sync} 5 | import monix.tail.Iterant 6 | import monix.catnap.ConcurrentQueue 7 | 8 | /** A `Pipe` is a connection between two components where 9 | * messages of type `L` are going from left to right and 10 | * messages of type `R` are going from right to left. 11 | */ 12 | trait Pipe[F[_], L, R] { 13 | type Left = Pipe.Side[F, L, R] 14 | type Right = Pipe.Side[F, R, L] 15 | 16 | def left: Left 17 | def right: Right 18 | } 19 | object Pipe { 20 | 21 | /** One side of a `Pipe` with 22 | * messages of type `I` going in and 23 | * messages of type `O` coming out. 24 | */ 25 | trait Side[F[_], I, O] { 26 | def send(in: I): F[Unit] 27 | def receive: Iterant[F, O] 28 | } 29 | object Side { 30 | def apply[F[_]: Sync: ContextShift, I, O]( 31 | iq: ConcurrentQueue[F, I], 32 | oq: ConcurrentQueue[F, O] 33 | ): Side[F, I, O] = new Side[F, I, O] { 34 | override def send(in: I): F[Unit] = 35 | iq.offer(in) 36 | 37 | override def receive: Iterant[F, O] = 38 | Iterant.repeatEvalF(oq.poll) 39 | } 40 | } 41 | 42 | def apply[F[_]: Concurrent: ContextShift, L, R]: F[Pipe[F, L, R]] = 43 | for { 44 | lq <- ConcurrentQueue.unbounded[F, L](None) 45 | rq <- ConcurrentQueue.unbounded[F, R](None) 46 | } yield new Pipe[F, L, R] { 47 | override val left = Side[F, L, R](lq, rq) 48 | override val right = Side[F, R, L](rq, lq) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /metronome/core/src/io/iohk/metronome/core/Tagger.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.core 2 | 3 | import shapeless.tag, tag.@@ 4 | 5 | /** Helper class to make it easier to tag raw types such as BitVector 6 | * to specializations so that the compiler can help make sure we are 7 | * passign the right values to methods. 8 | * 9 | * ``` 10 | * object MyType extends Tagger[ByteVector] 11 | * type MyType = MyType.Tagged 12 | * 13 | * val myThing: MyType = MyType(ByteVector.empty) 14 | * ``` 15 | */ 16 | trait Tagger[U] { 17 | trait Tag 18 | type Tagged = U @@ Tag 19 | 20 | def apply(underlying: U): Tagged = 21 | tag[Tag][U](underlying) 22 | } 23 | 24 | /** Helper class to tag not a specific raw type, but to apply a common tag to any type. 25 | * 26 | * ``` 27 | * object Validated extends GenericTagger 28 | * type Validated[U] = Validated.Tagged[U] 29 | * ``` 30 | */ 31 | trait GenericTagger { 32 | trait Tag 33 | type Tagged[U] = U @@ Tag 34 | 35 | def apply[U](underlying: U): Tagged[U] = 36 | tag[Tag][U](underlying) 37 | } 38 | -------------------------------------------------------------------------------- /metronome/core/src/io/iohk/metronome/core/Validated.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.core 2 | 3 | /** Can be used to tag any particular type as validated, for example: 4 | * 5 | * ``` 6 | * def validateBlock(block: Block): Either[Error, Validated[Block]] 7 | * def storeBlock(block: Validated[Block]) 8 | * ``` 9 | * 10 | * It's a bit more lightweight than opting into the `ValidatedNel` from `cats`, 11 | * mostly just serves as control that the right methods have been called in a 12 | * pipeline. 13 | */ 14 | object Validated extends GenericTagger 15 | -------------------------------------------------------------------------------- /metronome/core/src/io/iohk/metronome/core/fibers/DeferredTask.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.core.fibers 2 | 3 | import cats.implicits._ 4 | import cats.effect.Sync 5 | import cats.effect.concurrent.Deferred 6 | import cats.effect.Concurrent 7 | import scala.util.control.NoStackTrace 8 | 9 | /** A task that can be executed on a fiber pool, or canceled if the pool is shut down.. */ 10 | protected[fibers] class DeferredTask[F[_]: Sync, A]( 11 | deferred: Deferred[F, Either[Throwable, A]], 12 | task: F[A] 13 | ) { 14 | import DeferredTask.CanceledException 15 | 16 | /** Execute the task and set the success/failure result on the deferred. */ 17 | def execute: F[Unit] = 18 | task.attempt.flatMap(deferred.complete) 19 | 20 | /** Get the result of the execution, raising an error if it failed. */ 21 | def join: F[A] = 22 | deferred.get.rethrow 23 | 24 | /** Signal to the submitter that this task is canceled. */ 25 | def cancel: F[Unit] = 26 | deferred 27 | .complete(Left(new CanceledException)) 28 | .attempt 29 | .void 30 | } 31 | 32 | object DeferredTask { 33 | class CanceledException 34 | extends RuntimeException("This task has been canceled.") 35 | with NoStackTrace 36 | 37 | def apply[F[_]: Concurrent, A](task: F[A]): F[DeferredTask[F, A]] = 38 | Deferred[F, Either[Throwable, A]].map { d => 39 | new DeferredTask[F, A](d, task) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /metronome/core/src/io/iohk/metronome/core/fibers/FiberSet.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.core.fibers 2 | 3 | import cats.implicits._ 4 | import cats.effect.{Concurrent, Fiber, Resource} 5 | import cats.effect.concurrent.{Ref, Deferred} 6 | 7 | /** Execute tasks in the background, canceling all fibers if the resource is released. 8 | * 9 | * Facilitates structured concurrency where the release of the component that submitted 10 | * these fibers causes the cancelation of all of its scheduled tasks. 11 | */ 12 | class FiberSet[F[_]: Concurrent]( 13 | isShutdownRef: Ref[F, Boolean], 14 | fibersRef: Ref[F, Set[Fiber[F, Unit]]], 15 | tasksRef: Ref[F, Set[DeferredTask[F, _]]] 16 | ) { 17 | private def raiseIfShutdown: F[Unit] = 18 | isShutdownRef.get.ifM( 19 | Concurrent[F].raiseError(new FiberSet.ShutdownException), 20 | ().pure[F] 21 | ) 22 | 23 | def submit[A](task: F[A]): F[F[A]] = for { 24 | _ <- raiseIfShutdown 25 | deferredFiber <- Deferred[F, Fiber[F, Unit]] 26 | 27 | // Run the task, then remove the fiber from the tracker. 28 | background: F[A] = for { 29 | exec <- task.attempt 30 | fiber <- deferredFiber.get 31 | _ <- fibersRef.update(_ - fiber) 32 | result <- Concurrent[F].delay(exec).rethrow 33 | } yield result 34 | 35 | wrapper <- DeferredTask[F, A](background) 36 | _ <- tasksRef.update(_ + wrapper) 37 | 38 | // Start running in the background. Only now do we know the identity of the fiber. 39 | fiber <- Concurrent[F].start(wrapper.execute) 40 | 41 | // Add the fiber to the collectin first, so that if the effect is 42 | // already finished, it gets to remove it and we're not leaking memory. 43 | _ <- fibersRef.update(_ + fiber) 44 | _ <- deferredFiber.complete(fiber) 45 | 46 | } yield wrapper.join 47 | 48 | def shutdown: F[Unit] = for { 49 | _ <- isShutdownRef.set(true) 50 | fibers <- fibersRef.get 51 | _ <- fibers.toList.traverse(_.cancel) 52 | tasks <- tasksRef.get 53 | _ <- tasks.toList.traverse(_.cancel) 54 | } yield () 55 | } 56 | 57 | object FiberSet { 58 | class ShutdownException 59 | extends IllegalStateException("The pool is already shut down.") 60 | 61 | def apply[F[_]: Concurrent]: Resource[F, FiberSet[F]] = 62 | Resource.make[F, FiberSet[F]] { 63 | for { 64 | isShutdownRef <- Ref[F].of(false) 65 | fibersRef <- Ref[F].of(Set.empty[Fiber[F, Unit]]) 66 | tasksRef <- Ref[F].of(Set.empty[DeferredTask[F, _]]) 67 | } yield new FiberSet[F](isShutdownRef, fibersRef, tasksRef) 68 | }(_.shutdown) 69 | } 70 | -------------------------------------------------------------------------------- /metronome/core/src/io/iohk/metronome/core/messages/RPCMessage.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.core.messages 2 | 3 | import cats.effect.Sync 4 | import java.util.UUID 5 | 6 | /** Messages that go in request/response pairs. */ 7 | trait RPCMessage { 8 | 9 | /** Unique identifier for request, which is expected to be 10 | * included in the response message that comes back. 11 | */ 12 | def requestId: UUID 13 | } 14 | 15 | abstract class RPCMessageCompanion { 16 | type RequestId = UUID 17 | 18 | object RequestId { 19 | def apply(): RequestId = 20 | UUID.randomUUID() 21 | 22 | def apply[F[_]: Sync]: F[RequestId] = 23 | Sync[F].delay(apply()) 24 | } 25 | 26 | trait Request extends RPCMessage 27 | trait Response extends RPCMessage 28 | 29 | /** Establish a relationship between a request and a response 30 | * type so the compiler can infer the return value of methods 31 | * based on the request parameter, or validate that two generic 32 | * parameters belong with each other. 33 | */ 34 | def pair[A <: Request, B <: Response]: RPCPair.Aux[A, B] = 35 | new RPCPair[A] { type Response = B } 36 | } 37 | 38 | /** A request can be associated with at most one response type. 39 | * On the other hand a response type can serve multiple requests. 40 | */ 41 | trait RPCPair[Request] { 42 | type Response 43 | } 44 | object RPCPair { 45 | type Aux[A, B] = RPCPair[A] { 46 | type Response = B 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /metronome/core/src/io/iohk/metronome/core/messages/RPCSupport.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.core.messages 2 | 3 | import cats.implicits._ 4 | import cats.effect.Sync 5 | import scala.reflect.ClassTag 6 | 7 | /** Utility base class to capture the pattern of interaction with `RPCTracker`. 8 | * It provides send and receive methods for requests and responses. 9 | */ 10 | abstract class RPCSupport[ 11 | F[_]: Sync, 12 | K, 13 | M, 14 | Request <: RPCMessageCompanion#Request, 15 | Response <: RPCMessageCompanion#Response 16 | ]( 17 | rpcTracker: RPCTracker[F, M], 18 | requestId: F[RPCMessageCompanion#RequestId] 19 | )(implicit ev1: Request <:< M, ev2: Response <:< M) { 20 | 21 | protected def sendRequest: (K, Request) => F[Unit] 22 | 23 | protected val requestTimeout: (K, Request) => F[Unit] = 24 | (_, _) => ().pure[F] 25 | 26 | protected val responseIgnored: (K, Response, Option[Throwable]) => F[Unit] = 27 | (_, _, _) => ().pure[F] 28 | 29 | /** Send a request to the peer and track the response. 30 | * 31 | * Returns `None` if we're not connected or the request times out. 32 | */ 33 | protected def sendRequest[Req <: Request, Res <: Response]( 34 | to: K, 35 | mkRequest: RPCMessageCompanion#RequestId => Req 36 | )(implicit 37 | ev: RPCPair.Aux[Req, Res], 38 | ct: ClassTag[Res] 39 | ): F[Option[Res]] = 40 | for { 41 | requestId <- requestId 42 | request = mkRequest(requestId) 43 | join <- rpcTracker.register[Req, Res](request) 44 | _ <- sendRequest(to, request) 45 | maybeRes <- join 46 | _ <- requestTimeout(to, request).whenA(maybeRes.isEmpty) 47 | } yield maybeRes 48 | 49 | /** Try to complete a request when the response arrives. */ 50 | protected def receiveResponse[Res <: Response]( 51 | from: K, 52 | response: Res 53 | ): F[Unit] = 54 | rpcTracker.complete(response).flatMap { 55 | case Right(ok) => 56 | responseIgnored(from, response, None).whenA(!ok) 57 | case Left(ex) => 58 | responseIgnored(from, response, Some(ex)) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /metronome/core/src/io/iohk/metronome/core/package.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome 2 | 3 | package object core { 4 | type Validated[U] = Validated.Tagged[U] 5 | } 6 | -------------------------------------------------------------------------------- /metronome/crypto/specs/src/io/iohk/metronome/crypto/hash/Keccak256Spec.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.crypto.hash 2 | 3 | import scodec.bits._ 4 | import org.scalatest.flatspec.AnyFlatSpec 5 | import org.scalatest.matchers.should.Matchers 6 | 7 | class Keccak256Spec extends AnyFlatSpec with Matchers { 8 | behavior of "Keccak256" 9 | 10 | it should "hash empty data" in { 11 | Keccak256( 12 | "".getBytes 13 | ) shouldBe hex"c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470" 14 | } 15 | 16 | it should "hash non-empty data" in { 17 | Keccak256( 18 | "abc".getBytes 19 | ) shouldBe hex"4e03657aea45a94fc7d47ba826c8d667c0d1e6e33a64a036ec44f58fa12d6c45" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /metronome/crypto/src/io/iohk/metronome/crypto/ECKeyPair.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.crypto 2 | 3 | import org.bouncycastle.crypto.AsymmetricCipherKeyPair 4 | 5 | import java.security.SecureRandom 6 | 7 | /** The pair of EC private and public keys for Secp256k1 elliptic curve */ 8 | case class ECKeyPair(prv: ECPrivateKey, pub: ECPublicKey) { 9 | 10 | /** The bouncycastle's underlying type for efficient use with 11 | * `io.iohk.ethereum.crypto.ECDSASignature` 12 | */ 13 | def underlying: AsymmetricCipherKeyPair = prv.underlying 14 | } 15 | 16 | object ECKeyPair { 17 | 18 | def apply(keyPair: AsymmetricCipherKeyPair): ECKeyPair = { 19 | val (prv, pub) = io.iohk.ethereum.crypto.keyPairToByteArrays(keyPair) 20 | ECKeyPair(ECPrivateKey(prv), ECPublicKey(pub)) 21 | } 22 | 23 | /** Generates a new keypair on the Secp256k1 elliptic curve */ 24 | def generate(secureRandom: SecureRandom): ECKeyPair = { 25 | val kp = io.iohk.ethereum.crypto.generateKeyPair(secureRandom) 26 | ECKeyPair(kp) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /metronome/crypto/src/io/iohk/metronome/crypto/ECPrivateKey.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.crypto 2 | 3 | import org.bouncycastle.crypto.AsymmetricCipherKeyPair 4 | import scodec.bits.ByteVector 5 | import io.iohk.ethereum.crypto.keyPairFromPrvKey 6 | 7 | /** Wraps the bytes representing an EC private key */ 8 | case class ECPrivateKey(bytes: ByteVector) { 9 | require( 10 | bytes.length == ECPrivateKey.Length, 11 | s"Key must be ${ECPrivateKey.Length} bytes long" 12 | ) 13 | 14 | /** Converts the byte representation to bouncycastle's `AsymmetricCipherKeyPair` for efficient use with 15 | * `io.iohk.ethereum.crypto.ECDSASignature` 16 | */ 17 | val underlying: AsymmetricCipherKeyPair = keyPairFromPrvKey( 18 | bytes.toArray 19 | ) 20 | } 21 | 22 | object ECPrivateKey { 23 | val Length = 32 24 | 25 | def apply(bytes: Array[Byte]): ECPrivateKey = 26 | ECPrivateKey(ByteVector(bytes)) 27 | } 28 | -------------------------------------------------------------------------------- /metronome/crypto/src/io/iohk/metronome/crypto/ECPublicKey.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.crypto 2 | 3 | import scodec.{Attempt, Codec} 4 | import scodec.bits.ByteVector 5 | import scodec.codecs.bytes 6 | import scala.util.Try 7 | 8 | /** Wraps the bytes representing an EC public key in uncompressed format and without the compression indicator */ 9 | case class ECPublicKey(bytes: ByteVector) { 10 | require( 11 | bytes.length == ECPublicKey.Length, 12 | s"Key must be ${ECPublicKey.Length} bytes long" 13 | ) 14 | } 15 | 16 | object ECPublicKey { 17 | val Length = 64 18 | 19 | def apply(bytes: Array[Byte]): ECPublicKey = 20 | ECPublicKey(ByteVector(bytes)) 21 | 22 | implicit val codec: Codec[ECPublicKey] = 23 | bytes.exmap[ECPublicKey]( 24 | bytes => Attempt.fromTry(Try(ECPublicKey(bytes))), 25 | key => Attempt.successful(key.bytes) 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /metronome/crypto/src/io/iohk/metronome/crypto/GroupSignature.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.crypto 2 | 3 | /** Group signature of members with identity `K` over some content `H`, 4 | * represented by type `G`, e.g. `G` could be a `List[Secp256k1Signature]` 5 | * or a single combined threshold signature of some sort. 6 | */ 7 | case class GroupSignature[K, H, G](sig: G) 8 | -------------------------------------------------------------------------------- /metronome/crypto/src/io/iohk/metronome/crypto/PartialSignature.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.crypto 2 | 3 | /** An individual signature of a member with identity `K` over some content `H`, 4 | * represented by type `P`, e.g. `P` could be a single `Secp256k1Signature` 5 | * or a partial threshold signature of some sort. 6 | */ 7 | case class PartialSignature[K, H, P](sig: P) 8 | -------------------------------------------------------------------------------- /metronome/crypto/src/io/iohk/metronome/crypto/hash/Hash.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.crypto.hash 2 | 3 | import io.iohk.metronome.core.Tagger 4 | import scodec.bits.ByteVector 5 | 6 | object Hash extends Tagger[ByteVector] 7 | -------------------------------------------------------------------------------- /metronome/crypto/src/io/iohk/metronome/crypto/hash/Keccak256.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.crypto.hash 2 | 3 | import org.bouncycastle.crypto.digests.KeccakDigest 4 | import scodec.bits.{BitVector, ByteVector} 5 | 6 | object Keccak256 { 7 | def apply(data: Array[Byte]): Hash = { 8 | val output = new Array[Byte](32) 9 | val digest = new KeccakDigest(256) 10 | digest.update(data, 0, data.length) 11 | digest.doFinal(output, 0) 12 | Hash(ByteVector(output)) 13 | } 14 | 15 | def apply(data: ByteVector): Hash = 16 | apply(data.toArray) 17 | 18 | def apply(data: BitVector): Hash = 19 | apply(data.toByteArray) 20 | } 21 | -------------------------------------------------------------------------------- /metronome/crypto/src/io/iohk/metronome/crypto/hash/package.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.crypto 2 | 3 | package object hash { 4 | type Hash = Hash.Tagged 5 | } 6 | -------------------------------------------------------------------------------- /metronome/examples/resources/application.conf: -------------------------------------------------------------------------------- 1 | metronome { 2 | examples { 3 | robot { 4 | network { 5 | # Generated with `mill --interactive metronome[2.12.13].examples.console`: 6 | # import io.iohk.metronome.crypto.ECKeyPair 7 | # val rnd = new java.security.SecureRandom() 8 | # val keys = List.fill(4)(ECKeyPair.generate(rnd)) 9 | # keys.zipWithIndex.map { case (pair, i) => s"{host = localhost, port = 4000$i, public-key = ${pair.pub.bytes.toHex}, private-key = ${pair.prv.bytes.toHex}}" }.foreach(println) 10 | nodes = [ 11 | {host = localhost, port = 40000, public-key = 65e2f6da1bb1e7f0b07f5b892c568acb5429833e30af3974eedd2137ebc9f1fb8b0c462d4ca558dda64c5da8cf10280a1f579556ac8a611bd2fa7199f5a2c69a, private-key = cd2a249a76d8e9fd0e538e651b9e97c3fc5efcceeb10fc98dd57fbdd156457e6} 12 | {host = localhost, port = 40001, public-key = ff7849206b7faef9557cf53333739ecd947698d76ba11ffabf2587435322b9a8b4f063faf97e5aace2a75b8f6714e5bd3d483cad6e830ae3036afcc4ff1b5369, private-key = 15cc92810f61bc705f939432197fee100bcc1a99d6cc66c7c28fa158d4144f84} 13 | {host = localhost, port = 40002, public-key = cb020251d396614a35038dd2ff88fd2f1a5fd74c8bcad4b353fa605405c8b1b8c80ee12d2a10b1fca59424b16890c8115fbc94a68026369acc3c2603595e6387, private-key = a4769d076bb7eefeb1aba8aa97520d8f7f8bcd65049a128c3040f9dd5d3eeae6} 14 | {host = localhost, port = 40003, public-key = 23fcab42e8f1078880b27aab4849092489bfa8d3e3b0faa54c9db89e89223c783ec7a3b2f8e6461b27778f78cea261a2272abe31c5601173b2964ef14af897dc, private-key = 9441f3e96104a11405cb0e03ceb693f889770dd2c155dab7573023e00e878ace} 15 | ] 16 | 17 | timeout = 3s 18 | } 19 | 20 | model { 21 | max-row = 40 22 | max-col = 60 23 | simulated-decision-time = 1s 24 | } 25 | 26 | db { 27 | path = ${user.home}"/.metronome/examples/robot/db" 28 | state-history-size = 100 29 | block-history-size = 100 30 | prune-interval = 60s 31 | } 32 | 33 | consensus { 34 | min-timeout = 5s 35 | max-timeout = 15s 36 | timeout-factor = 1.2 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /metronome/examples/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | ${log.console.level} 12 | 13 | 14 | ${encoder.pattern} 15 | 16 | 17 | 18 | 19 | ${log.file.dir}/${log.file.name}.log 20 | true 21 | 22 | ${log.file.dir}/${log.file.name}.%i.log.zip 23 | 1 24 | 10 25 | 26 | 27 | 10MB 28 | 29 | 30 | ${encoder.pattern} 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /metronome/examples/robot.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | PROJECT_DIR=$(dirname $0)/../.. 6 | SCALA_VER=2.13.4 7 | ASSEMBLY_JAR=${PROJECT_DIR}/out/metronome/${SCALA_VER}/examples/assembly/dest/out.jar 8 | 9 | cd $PROJECT_DIR 10 | mill metronome[${SCALA_VER}].examples.assembly 11 | 12 | exec java -cp ${ASSEMBLY_JAR} io.iohk.metronome.examples.robot.app.RobotApp --node-index $1 13 | -------------------------------------------------------------------------------- /metronome/examples/specs/src/io/iohk/metronome/examples/robot/app/config/RobotConfigParserSpec.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.examples.robot.app.config 2 | 3 | import org.scalatest.flatspec.AnyFlatSpec 4 | import org.scalatest.Inside 5 | 6 | class RobotConfigParserSpec extends AnyFlatSpec with Inside { 7 | behavior of "RobotConfigParser" 8 | 9 | it should "parse the default configuration" in { 10 | inside(RobotConfigParser.parse) { case Right(_) => 11 | succeed 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /metronome/examples/src/io/iohk/metronome/examples/robot/RobotAgreement.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.examples.robot 2 | 3 | import io.iohk.metronome.crypto 4 | import io.iohk.metronome.hotstuff.consensus 5 | import io.iohk.metronome.hotstuff.consensus.basic.Secp256k1Agreement 6 | import io.iohk.metronome.examples.robot.models.RobotBlock 7 | 8 | object RobotAgreement extends Secp256k1Agreement { 9 | override type Block = RobotBlock 10 | override type Hash = crypto.hash.Hash 11 | 12 | implicit val block: consensus.basic.Block[RobotAgreement] = 13 | new consensus.basic.Block[RobotAgreement] { 14 | override def blockHash(b: RobotBlock) = b.hash 15 | override def parentBlockHash(b: RobotBlock) = b.parentHash 16 | override def height(b: RobotBlock): Long = b.height 17 | override def isValid(b: RobotBlock) = true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /metronome/examples/src/io/iohk/metronome/examples/robot/app/RobotApp.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.examples.robot.app 2 | 3 | import cats.effect.ExitCode 4 | import monix.eval.{Task, TaskApp} 5 | import io.iohk.metronome.examples.robot.app.config.{ 6 | RobotConfigParser, 7 | RobotConfig, 8 | RobotOptions 9 | } 10 | 11 | object RobotApp extends TaskApp { 12 | 13 | override def run(args: List[String]): Task[ExitCode] = { 14 | RobotConfigParser.parse match { 15 | case Left(error) => 16 | Task 17 | .delay(println(s"Error parsing configuration: $error")) 18 | .as(ExitCode.Error) 19 | case Right(config) => 20 | RobotOptions.parse(config, args) match { 21 | case None => 22 | Task.pure(ExitCode.Error) 23 | case Some(opts) => 24 | setLogProperties(opts) >> 25 | run(opts, config) 26 | } 27 | } 28 | } 29 | 30 | def run(opts: RobotOptions, config: RobotConfig): Task[ExitCode] = 31 | RobotComposition 32 | .compose(opts, config) 33 | .use(_ => Task.never.as(ExitCode.Success)) 34 | 35 | /** Set dynamic system properties expected by `logback.xml` before any logging module is loaded. */ 36 | def setLogProperties(opts: RobotOptions): Task[Unit] = Task { 37 | // Separate log file for each node. 38 | System.setProperty("log.file.name", s"robot/logs/node-${opts.nodeIndex}") 39 | // Less logging to the console so we can display robot position. 40 | System.setProperty("log.console.level", s"INFO") 41 | }.void 42 | } 43 | -------------------------------------------------------------------------------- /metronome/examples/src/io/iohk/metronome/examples/robot/app/RobotNamespaces.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.examples.robot.app 2 | 3 | import io.iohk.metronome.rocksdb.NamespaceRegistry 4 | 5 | object RobotNamespaces extends NamespaceRegistry { 6 | val Block = register("block") 7 | val BlockMeta = register("block-meta") 8 | val BlockToChildren = register("block-to-children") 9 | val ViewState = register("view-state") 10 | val State = register("state") 11 | val StateMeta = register("state-meta") 12 | } 13 | -------------------------------------------------------------------------------- /metronome/examples/src/io/iohk/metronome/examples/robot/app/config/RobotConfig.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.examples.robot.app.config 2 | 3 | import io.iohk.metronome.crypto.{ECPublicKey, ECPrivateKey} 4 | import java.net.InetSocketAddress 5 | import scala.concurrent.duration.FiniteDuration 6 | import java.nio.file.Path 7 | 8 | case class RobotConfig( 9 | network: RobotConfig.Network, 10 | model: RobotConfig.Model, 11 | db: RobotConfig.Database, 12 | consensus: RobotConfig.Consensus 13 | ) 14 | object RobotConfig { 15 | case class Network( 16 | nodes: List[RobotConfig.Node], 17 | timeout: FiniteDuration 18 | ) 19 | 20 | case class Node( 21 | host: String, 22 | port: Int, 23 | publicKey: ECPublicKey, 24 | // Because this is just an example application, we also have the private key 25 | // for each node, so we can just strat one of them by index. 26 | privateKey: ECPrivateKey 27 | ) { 28 | lazy val address = new InetSocketAddress(host, port) 29 | } 30 | 31 | case class Model( 32 | maxRow: Int, 33 | maxCol: Int, 34 | simulatedDecisionTime: FiniteDuration 35 | ) 36 | 37 | case class Database( 38 | path: Path, 39 | stateHistorySize: Int, 40 | blockHistorySize: Int, 41 | pruneInterval: FiniteDuration 42 | ) 43 | 44 | case class Consensus( 45 | minTimeout: FiniteDuration, 46 | maxTimeout: FiniteDuration, 47 | timeoutFactor: Double 48 | ) 49 | } 50 | -------------------------------------------------------------------------------- /metronome/examples/src/io/iohk/metronome/examples/robot/app/config/RobotConfigParser.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.examples.robot.app.config 2 | 3 | import com.typesafe.config.ConfigFactory 4 | import io.iohk.metronome.config.{ConfigParser, ConfigDecoders} 5 | import io.circe._, io.circe.generic.semiauto._ 6 | import io.iohk.metronome.crypto.{ECPublicKey, ECPrivateKey} 7 | import java.nio.file.Path 8 | import scodec.bits.ByteVector 9 | import scala.concurrent.duration.FiniteDuration 10 | import scala.util.Try 11 | 12 | object RobotConfigParser { 13 | 14 | def parse: ConfigParser.Result[RobotConfig] = { 15 | ConfigParser.parse[RobotConfig]( 16 | ConfigFactory.load().getConfig("metronome.examples.robot").root(), 17 | prefix = "METRONOME_ROBOT" 18 | ) 19 | } 20 | 21 | implicit val ecPublicKeyDecoder: Decoder[ECPublicKey] = 22 | Decoder[String].emap { str => 23 | ByteVector.fromHex(str) match { 24 | case None => 25 | Left("$str is not a valid hexadecimal key") 26 | case Some(bytes) => 27 | Try(ECPublicKey(bytes)).toEither.left.map(_.getMessage) 28 | } 29 | } 30 | 31 | implicit val ecPrivateKeyDecoder: Decoder[ECPrivateKey] = 32 | Decoder[String].emap { str => 33 | ByteVector.fromHex(str) match { 34 | case None => 35 | Left("$str is not a valid hexadecimal key") 36 | case Some(bytes) => 37 | Try(ECPrivateKey(bytes)).toEither.left.map(_.getMessage) 38 | } 39 | } 40 | 41 | implicit val pathDecoder: Decoder[Path] = 42 | Decoder[String].map(Path.of(_)) 43 | 44 | implicit val finiteDurationDecoder: Decoder[FiniteDuration] = 45 | ConfigDecoders.durationDecoder 46 | 47 | implicit val nodeDecoder: Decoder[RobotConfig.Node] = deriveDecoder 48 | implicit val networkDecoder: Decoder[RobotConfig.Network] = deriveDecoder 49 | implicit val databaseDecoder: Decoder[RobotConfig.Database] = deriveDecoder 50 | implicit val modelDecoder: Decoder[RobotConfig.Model] = deriveDecoder 51 | implicit val consensusDecoder: Decoder[RobotConfig.Consensus] = deriveDecoder 52 | implicit val configDecoder: Decoder[RobotConfig] = deriveDecoder 53 | 54 | } 55 | -------------------------------------------------------------------------------- /metronome/examples/src/io/iohk/metronome/examples/robot/app/config/RobotOptions.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.examples.robot.app.config 2 | 3 | import scopt.OParser 4 | 5 | /** Command Line Options. */ 6 | case class RobotOptions( 7 | nodeIndex: Int = 0 8 | ) 9 | 10 | object RobotOptions { 11 | 12 | /** Parse the options. Return `None` if there was an error, 13 | * which has already been printed to the console. 14 | */ 15 | def parse(config: RobotConfig, args: List[String]): Option[RobotOptions] = 16 | OParser.parse( 17 | RobotOptions.oparser(config), 18 | args, 19 | RobotOptions() 20 | ) 21 | 22 | private def oparser(config: RobotConfig) = { 23 | val builder = OParser.builder[RobotOptions] 24 | import builder._ 25 | 26 | OParser.sequence( 27 | programName("robot"), 28 | opt[Int]('i', "node-index") 29 | .action((i, opts) => opts.copy(nodeIndex = i)) 30 | .text("index of example node to run") 31 | .required() 32 | .validate(i => 33 | Either.cond( 34 | 0 <= i && i < config.network.nodes.length, 35 | (), 36 | s"Must be between 0 and ${config.network.nodes.length - 1}" 37 | ) 38 | ) 39 | ) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /metronome/examples/src/io/iohk/metronome/examples/robot/app/tracing/RobotNetworkTracers.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.examples.robot.app.tracing 2 | 3 | import monix.eval.Task 4 | import io.iohk.metronome.examples.robot.RobotAgreement 5 | import io.iohk.metronome.examples.robot.service.messages.RobotMessage 6 | import io.iohk.metronome.hotstuff.service.messages.DuplexMessage 7 | import io.iohk.metronome.networking.{NetworkTracers, NetworkEvent} 8 | import io.iohk.metronome.logging.{HybridLog, HybridLogObject, LogTracer} 9 | import io.circe.{Encoder, JsonObject} 10 | 11 | object RobotNetworkTracers { 12 | type RobotNetworkMessage = DuplexMessage[RobotAgreement, RobotMessage] 13 | type RobotNetworkEvent = 14 | NetworkEvent[RobotAgreement.PKey, RobotNetworkMessage] 15 | 16 | implicit val networkEventHybridLog: HybridLog[Task, RobotNetworkEvent] = { 17 | import NetworkEvent._ 18 | import io.circe.syntax._ 19 | 20 | implicit val keyEncoder: Encoder[RobotAgreement.PKey] = 21 | Encoder[String].contramap[RobotAgreement.PKey](_.bytes.toHex) 22 | 23 | implicit val peerEncoder: Encoder.AsObject[Peer[RobotAgreement.PKey]] = 24 | Encoder.AsObject.instance { case Peer(key, address) => 25 | JsonObject( 26 | "publicKey" -> key.asJson, 27 | "address" -> address.toString.asJson 28 | ) 29 | } 30 | 31 | HybridLog.instance[Task, RobotNetworkEvent]( 32 | level = { 33 | case _: ConnectionRegistered[_] => HybridLogObject.Level.Info 34 | case _: ConnectionDeregistered[_] => HybridLogObject.Level.Info 35 | case _ => HybridLogObject.Level.Debug 36 | }, 37 | message = _.getClass.getSimpleName, 38 | event = { 39 | case e: ConnectionUnknown[_] => e.peer.asJsonObject 40 | case e: ConnectionRegistered[_] => e.peer.asJsonObject 41 | case e: ConnectionDeregistered[_] => e.peer.asJsonObject 42 | case e: ConnectionDiscarded[_] => e.peer.asJsonObject 43 | case e: ConnectionSendError[_] => e.peer.asJsonObject 44 | case e: ConnectionFailed[_] => 45 | e.peer.asJsonObject.add("error", e.error.toString.asJson) 46 | case e: ConnectionReceiveError[_] => 47 | e.peer.asJsonObject.add("error", e.error.toString.asJson) 48 | case e: NetworkEvent.MessageReceived[_, _] => 49 | e.peer.asJsonObject 50 | .add("message", e.message.toString.asJson) 51 | case e: NetworkEvent.MessageSent[_, _] => 52 | e.peer.asJsonObject 53 | .add("message", e.message.toString.asJson) 54 | } 55 | ) 56 | } 57 | 58 | implicit val networkEventHybridLogTracer = 59 | LogTracer.hybrid[Task, RobotNetworkEvent] 60 | 61 | implicit val networkHybridLogTracers = 62 | NetworkTracers(networkEventHybridLogTracer) 63 | } 64 | -------------------------------------------------------------------------------- /metronome/examples/src/io/iohk/metronome/examples/robot/codecs/RobotCodecs.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.examples.robot.codecs 2 | 3 | import io.iohk.metronome.crypto.hash.Hash 4 | import io.iohk.metronome.hotstuff.service.codecs._ 5 | import io.iohk.metronome.examples.robot.RobotAgreement 6 | import io.iohk.metronome.examples.robot.models.{Robot, RobotBlock} 7 | import io.iohk.metronome.examples.robot.service.messages.RobotMessage 8 | import scodec.Codec 9 | import scodec.codecs._ 10 | import scodec.bits.ByteVector 11 | 12 | object RobotCodecs 13 | extends DefaultConsensusCodecs[RobotAgreement] 14 | with DefaultProtocolCodecs[RobotAgreement] 15 | with DefaultSecp256k1Codecs[RobotAgreement] 16 | with DefaultMessageCodecs[RobotAgreement] 17 | with DefaultDuplexMessageCodecs[RobotAgreement, RobotMessage] { 18 | import scodec.codecs.implicits._ 19 | 20 | override implicit lazy val hashCodec: Codec[Hash] = 21 | Codec[ByteVector].xmap(Hash(_), identity) 22 | 23 | implicit lazy val commandCodec: Codec[Robot.Command] = { 24 | import Robot.Command._ 25 | mappedEnum( 26 | uint2, 27 | Rest -> 0, 28 | MoveForward -> 1, 29 | TurnLeft -> 2, 30 | TurnRight -> 3 31 | ) 32 | } 33 | implicit lazy val robotPositionCodec: Codec[Robot.Position] = 34 | Codec.deriveLabelledGeneric 35 | 36 | implicit lazy val robotOrientationCodec: Codec[Robot.Orientation] = { 37 | import Robot.Orientation._ 38 | mappedEnum(uint2, North -> 0, East -> 1, South -> 2, West -> 3) 39 | } 40 | 41 | implicit lazy val robotStateCodec: Codec[Robot.State] = 42 | Codec.deriveLabelledGeneric 43 | 44 | override implicit lazy val blockCodec: Codec[RobotBlock] = 45 | Codec.deriveLabelledGeneric 46 | 47 | implicit lazy val getStateRequestCodec: Codec[RobotMessage.GetStateRequest] = 48 | Codec.deriveLabelledGeneric 49 | 50 | implicit lazy val getStateResponseCodec 51 | : Codec[RobotMessage.GetStateResponse] = 52 | Codec.deriveLabelledGeneric 53 | 54 | override implicit lazy val applicationMessageCodec: Codec[RobotMessage] = 55 | discriminated[RobotMessage] 56 | .by(uint2) 57 | .typecase(0, getStateRequestCodec) 58 | .typecase(1, getStateResponseCodec) 59 | } 60 | -------------------------------------------------------------------------------- /metronome/examples/src/io/iohk/metronome/examples/robot/models/Robot.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.examples.robot.models 2 | 3 | import io.iohk.metronome.crypto.hash.Hash 4 | import io.iohk.metronome.examples.robot.codecs.RobotCodecs 5 | 6 | object Robot { 7 | sealed trait Command extends Product with Serializable 8 | object Command { 9 | case object Rest extends Command 10 | case object MoveForward extends Command 11 | case object TurnLeft extends Command 12 | case object TurnRight extends Command 13 | } 14 | 15 | case class State( 16 | position: Position, 17 | orientation: Orientation 18 | ) { 19 | import Command._ 20 | 21 | lazy val hash: Hash = codecHash(this)(RobotCodecs.robotStateCodec) 22 | 23 | def update(command: Command): State = 24 | command match { 25 | case Rest => 26 | this 27 | case MoveForward => 28 | copy(position = position.move(orientation)) 29 | case TurnLeft => 30 | copy(orientation = orientation.left) 31 | case TurnRight => 32 | copy(orientation = orientation.right) 33 | } 34 | } 35 | 36 | case class Position(row: Int, col: Int) { 37 | import Orientation._ 38 | 39 | def move(orientation: Orientation): Position = 40 | orientation match { 41 | case North => copy(row = row - 1) 42 | case East => copy(col = col + 1) 43 | case South => copy(row = row + 1) 44 | case West => copy(col = col - 1) 45 | } 46 | } 47 | 48 | sealed trait Orientation { 49 | import Orientation._ 50 | 51 | def left: Orientation = 52 | this match { 53 | case North => West 54 | case East => North 55 | case South => East 56 | case West => South 57 | } 58 | 59 | def right: Orientation = 60 | this match { 61 | case North => East 62 | case East => South 63 | case South => West 64 | case West => North 65 | } 66 | } 67 | object Orientation { 68 | case object North extends Orientation 69 | case object East extends Orientation 70 | case object South extends Orientation 71 | case object West extends Orientation 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /metronome/examples/src/io/iohk/metronome/examples/robot/models/RobotBlock.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.examples.robot.models 2 | 3 | import io.iohk.metronome.crypto.hash.Hash 4 | import io.iohk.metronome.examples.robot.codecs.RobotCodecs 5 | 6 | case class RobotBlock( 7 | parentHash: Hash, 8 | height: Long, 9 | postStateHash: Hash, 10 | command: Robot.Command 11 | ) { 12 | lazy val hash: Hash = codecHash(this)(RobotCodecs.blockCodec) 13 | } 14 | -------------------------------------------------------------------------------- /metronome/examples/src/io/iohk/metronome/examples/robot/models/RobotSigning.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.examples.robot.models 2 | 3 | import io.iohk.metronome.examples.robot.RobotAgreement 4 | import io.iohk.metronome.examples.robot.codecs.RobotCodecs 5 | import io.iohk.metronome.hotstuff.consensus.Federation 6 | import io.iohk.metronome.hotstuff.consensus.basic.{ 7 | QuorumCertificate, 8 | Secp256k1Signing, 9 | VotingPhase 10 | } 11 | 12 | class RobotSigning( 13 | genesisHash: RobotAgreement.Hash 14 | ) extends Secp256k1Signing[RobotAgreement]((phase, viewNumber, hash) => 15 | RobotCodecs.contentCodec 16 | .encode((phase, viewNumber, hash)) 17 | .require 18 | .toByteVector 19 | ) { 20 | 21 | /** Override quorum certificate validation rule so we accept the quorum 22 | * certificate we can determinsiticially fabricate without a group signature. 23 | */ 24 | override def validate( 25 | federation: Federation[RobotAgreement.PKey], 26 | quorumCertificate: QuorumCertificate[RobotAgreement, VotingPhase] 27 | ): Boolean = 28 | if (quorumCertificate.blockHash == genesisHash) { 29 | quorumCertificate.signature.sig.isEmpty 30 | } else { 31 | super.validate(federation, quorumCertificate) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /metronome/examples/src/io/iohk/metronome/examples/robot/models/package.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.examples.robot 2 | 3 | import io.iohk.metronome.crypto.hash.{Hash, Keccak256} 4 | import scodec.Codec 5 | 6 | package object models { 7 | def codecHash[T: Codec](data: T): Hash = 8 | Keccak256(implicitly[Codec[T]].encode(data).require) 9 | } 10 | -------------------------------------------------------------------------------- /metronome/examples/src/io/iohk/metronome/examples/robot/package.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.examples 2 | 3 | package object robot { 4 | type RobotAgreement = RobotAgreement.type 5 | } 6 | -------------------------------------------------------------------------------- /metronome/examples/src/io/iohk/metronome/examples/robot/service/messages/RobotMessage.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.examples.robot.service.messages 2 | 3 | import io.iohk.metronome.crypto.hash.Hash 4 | import io.iohk.metronome.core.messages.{RPCMessage, RPCMessageCompanion} 5 | import io.iohk.metronome.examples.robot.models.Robot 6 | 7 | sealed trait RobotMessage { self: RPCMessage => } 8 | 9 | object RobotMessage extends RPCMessageCompanion { 10 | 11 | case class GetStateRequest( 12 | requestId: RequestId, 13 | stateHash: Hash 14 | ) extends RobotMessage 15 | with Request 16 | 17 | case class GetStateResponse( 18 | requestId: RequestId, 19 | state: Robot.State 20 | ) extends RobotMessage 21 | with Response 22 | 23 | implicit val getStatePair = 24 | pair[GetStateRequest, GetStateResponse] 25 | } 26 | -------------------------------------------------------------------------------- /metronome/examples/src/io/iohk/metronome/examples/robot/service/tracing/RobotEvent.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.examples.robot.service.tracing 2 | 3 | import io.iohk.metronome.examples.robot.models.{Robot, RobotBlock} 4 | 5 | sealed trait RobotEvent 6 | 7 | object RobotEvent { 8 | 9 | /** This node is proposing a block. */ 10 | case class Proposing(block: RobotBlock) extends RobotEvent 11 | 12 | /** The federation committed to a new state. */ 13 | case class NewState(state: Robot.State) extends RobotEvent 14 | } 15 | -------------------------------------------------------------------------------- /metronome/examples/src/io/iohk/metronome/examples/robot/service/tracing/RobotTracers.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.examples.robot.service.tracing 2 | 3 | import cats.implicits._ 4 | import io.iohk.metronome.tracer.Tracer 5 | import io.iohk.metronome.examples.robot.models.{Robot, RobotBlock} 6 | 7 | case class RobotTracers[F[_]]( 8 | proposing: Tracer[F, RobotBlock], 9 | newState: Tracer[F, Robot.State] 10 | ) 11 | 12 | object RobotTracers { 13 | import RobotEvent._ 14 | 15 | def apply[F[_]](tracer: Tracer[F, RobotEvent]): RobotTracers[F] = 16 | RobotTracers[F]( 17 | proposing = tracer.contramap[RobotBlock](Proposing(_)), 18 | newState = tracer.contramap[Robot.State](NewState(_)) 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /metronome/hotstuff/consensus/props/src/io/iohk/metronome/hotstuff/consensus/ArbitraryInstances.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.hotstuff.consensus 2 | 3 | import io.iohk.metronome.crypto.hash.Hash 4 | import io.iohk.metronome.hotstuff.consensus.basic.Phase.{ 5 | Commit, 6 | PreCommit, 7 | Prepare 8 | } 9 | import io.iohk.metronome.hotstuff.consensus.basic.VotingPhase 10 | import org.scalacheck.Arbitrary.arbitrary 11 | import org.scalacheck.rng.Seed 12 | import org.scalacheck.{Arbitrary, Gen} 13 | import scodec.bits.ByteVector 14 | 15 | trait ArbitraryInstances { 16 | 17 | def sample[T: Arbitrary]: T = arbitrary[T].sample.get 18 | 19 | //TODO: rename / remove above? 20 | def sample0[T: Arbitrary](implicit seed: Seed): T = 21 | implicitly[Arbitrary[T]].arbitrary(Gen.Parameters.default, seed).get 22 | 23 | implicit val arbViewNumber: Arbitrary[ViewNumber] = Arbitrary { 24 | Gen.posNum[Long].map(ViewNumber(_)) 25 | } 26 | 27 | implicit val arbVotingPhase: Arbitrary[VotingPhase] = Arbitrary { 28 | Gen.oneOf(Prepare, PreCommit, Commit) 29 | } 30 | 31 | implicit val arbHash: Arbitrary[Hash] = 32 | Arbitrary { 33 | Gen.listOfN(32, arbitrary[Byte]).map(ByteVector(_)).map(Hash(_)) 34 | } 35 | } 36 | 37 | object ArbitraryInstances extends ArbitraryInstances 38 | -------------------------------------------------------------------------------- /metronome/hotstuff/consensus/props/src/io/iohk/metronome/hotstuff/consensus/LeaderSelectionProps.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.hotstuff.consensus 2 | 3 | import io.iohk.metronome.core.Tagger 4 | import io.iohk.metronome.hotstuff.consensus.ArbitraryInstances._ 5 | import org.scalacheck.Prop.forAll 6 | import org.scalacheck._ 7 | 8 | abstract class LeaderSelectionProps(name: String, val selector: LeaderSelection) 9 | extends Properties(name) { 10 | 11 | object Size extends Tagger[Int] 12 | type Size = Size.Tagged 13 | 14 | implicit val arbFederationSize: Arbitrary[Size] = Arbitrary { 15 | Gen.posNum[Int].map(Size(_)) 16 | } 17 | 18 | property("leaderOf") = forAll { (viewNumber: ViewNumber, size: Size) => 19 | val idx = selector.leaderOf(viewNumber, size) 20 | 0 <= idx && idx < size 21 | } 22 | } 23 | 24 | object RoundRobinSelectionProps 25 | extends LeaderSelectionProps( 26 | "LeaderSelection.RoundRobin", 27 | LeaderSelection.RoundRobin 28 | ) { 29 | 30 | property("round-robin") = forAll { (viewNumber: ViewNumber, size: Size) => 31 | val idx0 = selector.leaderOf(viewNumber, size) 32 | val idx1 = selector.leaderOf(viewNumber.next, size) 33 | idx1 == idx0 + 1 || idx0 == size - 1 && idx1 == 0 34 | } 35 | } 36 | 37 | object HashingSelectionProps 38 | extends LeaderSelectionProps( 39 | "LeaderSelection.Hashing", 40 | LeaderSelection.Hashing 41 | ) 42 | -------------------------------------------------------------------------------- /metronome/hotstuff/consensus/specs/src/io/iohk/metronome/hotstuff/consensus/FederationSpec.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.hotstuff.consensus 2 | 3 | import org.scalatest.flatspec.AnyFlatSpec 4 | import org.scalatest.matchers.should.Matchers 5 | import org.scalatest.Inside 6 | import org.scalatest.prop.TableDrivenPropertyChecks._ 7 | 8 | class FederationSpec extends AnyFlatSpec with Matchers with Inside { 9 | 10 | implicit val ls = LeaderSelection.RoundRobin 11 | 12 | behavior of "Federation" 13 | 14 | it should "not create an empty federation" in { 15 | Federation(Vector.empty).isLeft shouldBe true 16 | } 17 | 18 | it should "not create a federation with duplicate keys" in { 19 | Federation(Vector(1, 2, 1)).isLeft shouldBe true 20 | } 21 | 22 | it should "not create a federation with too high configured f" in { 23 | Federation(1 to 4, maxFaulty = 2).isLeft shouldBe true 24 | } 25 | 26 | it should "determine the correct f and q based on n" in { 27 | val examples = Table( 28 | ("n", "f", "q"), 29 | (10, 3, 7), 30 | (1, 0, 1), 31 | (3, 0, 2), 32 | (4, 1, 3) 33 | ) 34 | forAll(examples) { case (n, f, q) => 35 | inside(Federation(1 to n)) { case Right(federation) => 36 | federation.maxFaulty shouldBe f 37 | federation.quorumSize shouldBe q 38 | } 39 | } 40 | } 41 | 42 | it should "use lower quorum size if there are less faulties" in { 43 | val examples = Table( 44 | ("n", "f", "q"), 45 | (10, 2, 7), 46 | (10, 1, 6), 47 | (10, 0, 6), 48 | (9, 0, 5), 49 | (100, 0, 51), 50 | (100, 1, 51) 51 | ) 52 | forAll(examples) { case (n, f, q) => 53 | inside(Federation(1 to n, f)) { case Right(federation) => 54 | federation.maxFaulty shouldBe f 55 | federation.quorumSize shouldBe q 56 | } 57 | } 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /metronome/hotstuff/consensus/src/io/iohk/metronome/hotstuff/consensus/LeaderSelection.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.hotstuff.consensus 2 | 3 | import io.iohk.metronome.crypto.hash.Keccak256 4 | import scodec.bits.ByteVector 5 | 6 | /** Strategy to pick the leader for a given view number from 7 | * federation of with a fixed size. 8 | */ 9 | trait LeaderSelection { 10 | 11 | /** Return the index of the federation member who should lead the view. */ 12 | def leaderOf(viewNumber: ViewNumber, size: Int): Int 13 | } 14 | 15 | object LeaderSelection { 16 | 17 | /** Simple strategy cycling through leaders in a static order. */ 18 | object RoundRobin extends LeaderSelection { 19 | override def leaderOf(viewNumber: ViewNumber, size: Int): Int = 20 | (viewNumber % size).toInt 21 | } 22 | 23 | /** Leader assignment based on view-number has not been discussed in the Hotstuff 24 | * paper and in general, it does not affect the safety and liveness. 25 | * However, it does affect worst-case latency. 26 | * 27 | * Consider a static adversary under a round-robin leader change scheme. 28 | * All the f nodes can set their public keys so that they are consecutive. 29 | * In such a scenario those f consecutive leaders can create timeouts leading 30 | * to an O(f) confirmation latency. (Recall that in a normal case, the latency is O(1)). 31 | * 32 | * A minor improvement to this is to assign leaders based on 33 | * "publicKeys((H256(viewNumber).toInt % size).toInt)". 34 | * 35 | * This leader order randomization via a hash function will ensure that even 36 | * if adversarial public keys are consecutive in PublicKey set, they are not 37 | * necessarily consecutive in leader order. 38 | * 39 | * Note that the above policy will not ensure that adversarial leaders are never consecutive, 40 | * but the probability of such occurrence will be lower under a static adversary. 41 | */ 42 | object Hashing extends LeaderSelection { 43 | override def leaderOf(viewNumber: ViewNumber, size: Int): Int = { 44 | val bytes = ByteVector.fromLong(viewNumber) // big-endian 45 | val hash = Keccak256(bytes) 46 | // If we prepend 0.toByte then it would treat it as unsigned, at the cost of an array copy. 47 | // Instead of doing that I'll just make sure we deal with negative modulo. 48 | val num = BigInt(hash.toArray) 49 | val mod = (num % size).toInt 50 | if (mod < 0) mod + size else mod 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /metronome/hotstuff/consensus/src/io/iohk/metronome/hotstuff/consensus/ViewNumber.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.hotstuff.consensus 2 | 3 | import io.iohk.metronome.core.Tagger 4 | import cats.kernel.Order 5 | 6 | object ViewNumber extends Tagger[Long] { 7 | implicit class Ops(val vn: ViewNumber) extends AnyVal { 8 | def next: ViewNumber = ViewNumber(vn + 1) 9 | def prev: ViewNumber = ViewNumber(vn - 1) 10 | } 11 | 12 | implicit val ord: Ordering[ViewNumber] = 13 | Ordering.by(identity[Long]) 14 | 15 | implicit val order: Order[ViewNumber] = 16 | Order.fromOrdering(ord) 17 | } 18 | -------------------------------------------------------------------------------- /metronome/hotstuff/consensus/src/io/iohk/metronome/hotstuff/consensus/basic/Agreement.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.hotstuff.consensus.basic 2 | 3 | import io.iohk.metronome.crypto 4 | import io.iohk.metronome.hotstuff.consensus.ViewNumber 5 | 6 | /** Capture all the generic types in the BFT agreement, 7 | * so we don't have to commit to any particular set of content. 8 | */ 9 | trait Agreement { 10 | 11 | /** The container type that the agreement is about. */ 12 | type Block 13 | 14 | /** The type we use for hashing blocks, 15 | * so they don't have to be sent in entirety in votes. 16 | */ 17 | type Hash 18 | 19 | /** The concrete type that represents a partial signature. */ 20 | type PSig 21 | 22 | /** The concrete type that represents a group signature. */ 23 | type GSig 24 | 25 | /** The public key identity of federation members. */ 26 | type PKey 27 | 28 | /** The secret key used for signing partial messages. */ 29 | type SKey 30 | } 31 | 32 | object Agreement { 33 | // Convenience alias for groups signatures appearing in Quorum Certificates.. 34 | type GroupSignature[A <: Agreement] = crypto.GroupSignature[ 35 | A#PKey, 36 | (VotingPhase, ViewNumber, A#Hash), 37 | A#GSig 38 | ] 39 | 40 | // Convenience alias for partial signatures appearing in Votes. 41 | type PartialSignature[A <: Agreement] = crypto.PartialSignature[ 42 | A#PKey, 43 | (VotingPhase, ViewNumber, A#Hash), 44 | A#PSig 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /metronome/hotstuff/consensus/src/io/iohk/metronome/hotstuff/consensus/basic/Block.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.hotstuff.consensus.basic 2 | 3 | /** Type class to project the properties we need a HotStuff block to have 4 | * from the generic `Block` type in the `Agreement`. 5 | * 6 | * This allows the block to include use-case specific details HotStuff doesn't 7 | * care about, for example to build up a ledger state that can be synchronised 8 | * directly, rather than just carry out a sequence of commands on all replicas. 9 | * This would require the blocks to contain ledger state hashes, which other 10 | * use cases may have no use for. 11 | */ 12 | trait Block[A <: Agreement] { 13 | def blockHash(b: A#Block): A#Hash 14 | def parentBlockHash(b: A#Block): A#Hash 15 | def height(b: A#Block): Long 16 | 17 | /** Perform simple content validation, e.g. 18 | * whether the block hash matches the header 19 | * and the header content matches the body. 20 | */ 21 | def isValid(b: A#Block): Boolean 22 | 23 | def isParentOf(parent: A#Block, child: A#Block): Boolean = { 24 | parentBlockHash(child) == blockHash(parent) && 25 | height(child) == height(parent) + 1 26 | } 27 | } 28 | 29 | object Block { 30 | def apply[A <: Agreement: Block]: Block[A] = implicitly[Block[A]] 31 | } 32 | -------------------------------------------------------------------------------- /metronome/hotstuff/consensus/src/io/iohk/metronome/hotstuff/consensus/basic/Effect.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.hotstuff.consensus.basic 2 | 3 | import scala.concurrent.duration.FiniteDuration 4 | 5 | import io.iohk.metronome.hotstuff.consensus.ViewNumber 6 | 7 | /** Represent all possible effects that a protocol transition can 8 | * ask the host system to carry out, e.g. send messages to replicas. 9 | */ 10 | sealed trait Effect[+A <: Agreement] 11 | 12 | object Effect { 13 | 14 | /** Schedule a callback after a timeout to initiate the next view 15 | * if the current rounds ends without an agreement. 16 | */ 17 | case class ScheduleNextView( 18 | viewNumber: ViewNumber, 19 | timeout: FiniteDuration 20 | ) extends Effect[Nothing] 21 | 22 | /** Send a message to a federation member. 23 | * 24 | * The recipient can be the current member itself (i.e. the leader 25 | * sending itself a message to trigger its own vote). It is best 26 | * if the host system carries out these effects before it talks 27 | * to the external world, to avoid any possible phase mismatches. 28 | * 29 | * The `ProtocolState` could do it on its own but this way it's 30 | * slightly closer to the pseudo code. 31 | */ 32 | case class SendMessage[A <: Agreement]( 33 | recipient: A#PKey, 34 | message: Message[A] 35 | ) extends Effect[A] 36 | 37 | /** The leader of the round wants to propose a new block 38 | * on top of the last prepared one. The host environment 39 | * should consult the mempool and create one, passing the 40 | * result as an event. 41 | * 42 | * The block must be built as a child of `highQC.blockHash`. 43 | */ 44 | case class CreateBlock[A <: Agreement]( 45 | viewNumber: ViewNumber, 46 | highQC: QuorumCertificate[A, Phase.Prepare] 47 | ) extends Effect[A] 48 | 49 | /** Once the Prepare Q.C. has been established for a block, 50 | * we know that it's not spam, it's safe to be persisted. 51 | * 52 | * This prevents a rouge leader from sending us many `Prepare` 53 | * messages in the same view with the intention of eating up 54 | * space using the included block. 55 | * 56 | * It's also a way for us to delay saving a block we created 57 | * as a leader to the time when it's been voted on. Since it's 58 | * part of the `Prepare` message, replicas shouldn't be asking 59 | * for it anyway, so it's not a problem if it's not yet persisted. 60 | */ 61 | case class SaveBlock[A <: Agreement]( 62 | preparedBlock: A#Block 63 | ) extends Effect[A] 64 | 65 | /** Execute blocks after a decision, from the last executed hash 66 | * up to the block included in the Quorum Certificate. 67 | */ 68 | case class ExecuteBlocks[A <: Agreement]( 69 | lastExecutedBlockHash: A#Hash, 70 | quorumCertificate: QuorumCertificate[A, Phase.Commit] 71 | ) extends Effect[A] 72 | 73 | } 74 | -------------------------------------------------------------------------------- /metronome/hotstuff/consensus/src/io/iohk/metronome/hotstuff/consensus/basic/Event.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.hotstuff.consensus.basic 2 | 3 | import io.iohk.metronome.hotstuff.consensus.ViewNumber 4 | 5 | /** Input events for the protocol model. */ 6 | sealed trait Event[+A <: Agreement] 7 | 8 | object Event { 9 | 10 | /** A scheduled timeout for the round, initiating the next view. */ 11 | case class NextView(viewNumber: ViewNumber) extends Event[Nothing] 12 | 13 | /** A message received from a federation member. */ 14 | case class MessageReceived[A <: Agreement]( 15 | sender: A#PKey, 16 | message: Message[A] 17 | ) extends Event[A] 18 | 19 | /** The block the leader asked to be created is ready. */ 20 | case class BlockCreated[A <: Agreement]( 21 | viewNumber: ViewNumber, 22 | block: A#Block, 23 | // The certificate which the block extended. 24 | highQC: QuorumCertificate[A, Phase.Prepare] 25 | ) extends Event[A] 26 | } 27 | -------------------------------------------------------------------------------- /metronome/hotstuff/consensus/src/io/iohk/metronome/hotstuff/consensus/basic/Message.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.hotstuff.consensus.basic 2 | 3 | import io.iohk.metronome.crypto.PartialSignature 4 | import io.iohk.metronome.hotstuff.consensus.ViewNumber 5 | 6 | /** Basic HotStuff protocol messages. */ 7 | sealed trait Message[A <: Agreement] { 8 | 9 | /** Messages are only accepted if they match the node's current view number. */ 10 | def viewNumber: ViewNumber 11 | } 12 | 13 | /** Message from the leader to the replica. */ 14 | sealed trait LeaderMessage[A <: Agreement] extends Message[A] 15 | 16 | /** Message from the replica to the leader. */ 17 | sealed trait ReplicaMessage[A <: Agreement] extends Message[A] 18 | 19 | object Message { 20 | 21 | /** The leader proposes a new block in the `Prepare` phase, 22 | * using the High Q.C. gathered from `NewView` messages. 23 | */ 24 | case class Prepare[A <: Agreement]( 25 | viewNumber: ViewNumber, 26 | block: A#Block, 27 | highQC: QuorumCertificate[A, Phase.Prepare] 28 | ) extends LeaderMessage[A] 29 | 30 | /** Having received one of the leader messages, the replica 31 | * casts its vote with its partical signature. 32 | * 33 | * The vote carries either the hash of the block, which 34 | * was either received full in the `Prepare` message, 35 | * or as part of a `QuorumCertificate`. 36 | */ 37 | case class Vote[A <: Agreement]( 38 | viewNumber: ViewNumber, 39 | phase: VotingPhase, 40 | blockHash: A#Hash, 41 | signature: PartialSignature[ 42 | A#PKey, 43 | (VotingPhase, ViewNumber, A#Hash), 44 | A#PSig 45 | ] 46 | ) extends ReplicaMessage[A] 47 | 48 | /** Having collected enough votes from replicas, 49 | * the leader combines the votes into a Q.C. and 50 | * broadcasts it to replicas: 51 | * - Prepare votes combine into a Prepare Q.C., expected in the PreCommit phase. 52 | * - PreCommit votes combine into a PreCommit Q.C., expected in the Commit phase. 53 | * - Commit votes combine into a Commit Q.C, expected in the Decide phase. 54 | * 55 | * The certificate contains the hash of the block to vote on. 56 | */ 57 | case class Quorum[A <: Agreement]( 58 | viewNumber: ViewNumber, 59 | quorumCertificate: QuorumCertificate[A, VotingPhase] 60 | ) extends LeaderMessage[A] 61 | 62 | /** At the end of the round, replicas send the `NewView` message 63 | * to the next leader with the last Prepare Q.C. 64 | */ 65 | case class NewView[A <: Agreement]( 66 | viewNumber: ViewNumber, 67 | prepareQC: QuorumCertificate[A, Phase.Prepare] 68 | ) extends ReplicaMessage[A] 69 | } 70 | -------------------------------------------------------------------------------- /metronome/hotstuff/consensus/src/io/iohk/metronome/hotstuff/consensus/basic/Phase.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.hotstuff.consensus.basic 2 | 3 | /** All phases of the basic HotStuff protocol. */ 4 | sealed trait Phase { 5 | import Phase._ 6 | def next: Phase = 7 | this match { 8 | case Prepare => PreCommit 9 | case PreCommit => Commit 10 | case Commit => Decide 11 | case Decide => Prepare 12 | } 13 | 14 | def prev: Phase = 15 | this match { 16 | case Prepare => Decide 17 | case PreCommit => Prepare 18 | case Commit => PreCommit 19 | case Decide => Commit 20 | } 21 | 22 | /** Check that *within the same view* phase this phase precedes the other. */ 23 | def isBefore(other: Phase): Boolean = 24 | (this, other) match { 25 | case (Prepare, PreCommit | Commit | Decide) => true 26 | case (PreCommit, Commit | Decide) => true 27 | case (Commit, Decide) => true 28 | case _ => false 29 | } 30 | 31 | /** Check that *within the same view* this phase follows the other. */ 32 | def isAfter(other: Phase): Boolean = 33 | (this, other) match { 34 | case (PreCommit, Prepare) => true 35 | case (Commit, Prepare | PreCommit) => true 36 | case (Decide, Prepare | PreCommit | Commit) => true 37 | case _ => false 38 | } 39 | } 40 | 41 | /** Subset of phases over which there can be vote and a Quorum Certificate. */ 42 | sealed trait VotingPhase extends Phase 43 | 44 | object Phase { 45 | case object Prepare extends VotingPhase 46 | case object PreCommit extends VotingPhase 47 | case object Commit extends VotingPhase 48 | case object Decide extends Phase 49 | 50 | type Prepare = Prepare.type 51 | type PreCommit = PreCommit.type 52 | type Commit = Commit.type 53 | } 54 | -------------------------------------------------------------------------------- /metronome/hotstuff/consensus/src/io/iohk/metronome/hotstuff/consensus/basic/ProtocolError.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.hotstuff.consensus.basic 2 | 3 | import io.iohk.metronome.hotstuff.consensus.ViewNumber 4 | 5 | sealed trait ProtocolError[A <: Agreement] 6 | 7 | object ProtocolError { 8 | 9 | /** A leader message was received from a replica that isn't the leader of the view. */ 10 | case class NotFromLeader[A <: Agreement]( 11 | event: Event.MessageReceived[A], 12 | expected: A#PKey 13 | ) extends ProtocolError[A] 14 | 15 | /** A replica message was received in a view that this replica is not leading. */ 16 | case class NotToLeader[A <: Agreement]( 17 | event: Event.MessageReceived[A], 18 | expected: A#PKey 19 | ) extends ProtocolError[A] 20 | 21 | /** A message coming from outside the federation members. */ 22 | case class NotFromFederation[A <: Agreement]( 23 | event: Event.MessageReceived[A] 24 | ) extends ProtocolError[A] 25 | 26 | /** The vote signature doesn't match the content. */ 27 | case class InvalidVote[A <: Agreement]( 28 | sender: A#PKey, 29 | message: Message.Vote[A] 30 | ) extends ProtocolError[A] 31 | 32 | /** The Q.C. signature doesn't match the content. */ 33 | case class InvalidQuorumCertificate[A <: Agreement]( 34 | sender: A#PKey, 35 | quorumCertificate: QuorumCertificate[A, VotingPhase] 36 | ) extends ProtocolError[A] 37 | 38 | /** The block in the prepare message doesn't extend the previous Q.C. */ 39 | case class UnsafeExtension[A <: Agreement]( 40 | sender: A#PKey, 41 | message: Message.Prepare[A] 42 | ) extends ProtocolError[A] 43 | 44 | /** A message we didn't expect to receive in the given state. */ 45 | case class UnexpectedBlockHash[A <: Agreement]( 46 | event: Event.MessageReceived[A], 47 | expected: A#Hash 48 | ) extends ProtocolError[A] 49 | 50 | /** A message that we received slightly earlier than we expected. 51 | * 52 | * One reason for this could be that the peer is slightly ahead of us, 53 | * e.g. already finished the `Decide` phase and sent out the `NewView` 54 | * to us, the next leader, in which case the view number would not 55 | * match up. Or maybe a quorum has already formed for the next round 56 | * and we receive a `Prepare`, while we're still in `Decide`. 57 | * 58 | * The host system passing the events and processing the effects 59 | * is expected to inspect `TooEarly` messages and decide what to do: 60 | * - if the message is for the next round or next phase, then just re-deliver it after the view transition 61 | * - if the message is far in the future, perhaps it's best to re-sync the status with everyone 62 | */ 63 | case class TooEarly[A <: Agreement]( 64 | event: Event.MessageReceived[A], 65 | expectedInViewNumber: ViewNumber, 66 | expectedInPhase: Phase 67 | ) extends ProtocolError[A] 68 | 69 | /** A message we didn't expect to receive in the given state. 70 | * 71 | * The host system can maintain some metrics so we can see if we're completely out of 72 | * alignment with all the other peers. 73 | */ 74 | case class Unexpected[A <: Agreement]( 75 | event: Event.MessageReceived[A] 76 | ) extends ProtocolError[A] 77 | } 78 | -------------------------------------------------------------------------------- /metronome/hotstuff/consensus/src/io/iohk/metronome/hotstuff/consensus/basic/QuorumCertificate.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.hotstuff.consensus.basic 2 | 3 | import io.iohk.metronome.crypto.GroupSignature 4 | import io.iohk.metronome.hotstuff.consensus.ViewNumber 5 | import scala.reflect.ClassTag 6 | 7 | /** A Quorum Certifcate (QC) over a tuple (message-type, view-number, block-hash) is a data type 8 | * that combines a collection of signatures for the same tuple signed by (n − f) replicas. 9 | */ 10 | case class QuorumCertificate[A <: Agreement, +P <: VotingPhase]( 11 | phase: P, 12 | viewNumber: ViewNumber, 13 | blockHash: A#Hash, 14 | signature: GroupSignature[A#PKey, (VotingPhase, ViewNumber, A#Hash), A#GSig] 15 | ) { 16 | 17 | /** In protocol messages we can treat QCs as `QuorumCertificate[A, VotingPhase]`, 18 | * and coerce to a specific type after checking what it is. We can also coerce 19 | * back into the supertype, if necessary. 20 | */ 21 | def coerce[V <: VotingPhase](implicit 22 | ct: ClassTag[V] 23 | ): QuorumCertificate[A, V] = { 24 | assert( 25 | ct.unapply(phase).isDefined, 26 | s"Can only coerce between VotingPhase and a subclass; attempted to cast ${phase} to ${ct.runtimeClass.getSimpleName}" 27 | ) 28 | this.asInstanceOf[QuorumCertificate[A, V]] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /metronome/hotstuff/consensus/src/io/iohk/metronome/hotstuff/consensus/basic/Secp256k1Agreement.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.hotstuff.consensus.basic 2 | 3 | import io.iohk.ethereum.crypto.ECDSASignature 4 | import io.iohk.metronome.crypto.{ECPrivateKey, ECPublicKey} 5 | 6 | trait Secp256k1Agreement extends Agreement { 7 | override final type SKey = ECPrivateKey 8 | override final type PKey = ECPublicKey 9 | override final type PSig = ECDSASignature 10 | // TODO (PM-2935): Replace list with theshold signatures. 11 | override final type GSig = List[ECDSASignature] 12 | } 13 | -------------------------------------------------------------------------------- /metronome/hotstuff/consensus/src/io/iohk/metronome/hotstuff/consensus/basic/Secp256k1Signing.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.hotstuff.consensus.basic 2 | 3 | import io.iohk.ethereum.crypto.ECDSASignature 4 | import io.iohk.metronome.crypto.hash.Keccak256 5 | import io.iohk.metronome.crypto.{ 6 | ECPrivateKey, 7 | ECPublicKey, 8 | GroupSignature, 9 | PartialSignature 10 | } 11 | import io.iohk.metronome.hotstuff.consensus.basic.Signing.{GroupSig, PartialSig} 12 | import io.iohk.metronome.hotstuff.consensus.{Federation, ViewNumber} 13 | import scodec.bits.ByteVector 14 | 15 | /** Facilitates a Secp256k1 elliptic curve signing scheme using 16 | * `io.iohk.ethereum.crypto.ECDSASignature` 17 | * A group signature is simply a concatenation (sequence) of partial signatures 18 | */ 19 | class Secp256k1Signing[A <: Secp256k1Agreement]( 20 | contentSerializer: (VotingPhase, ViewNumber, A#Hash) => ByteVector 21 | ) extends Signing[A] { 22 | 23 | override def sign( 24 | signingKey: ECPrivateKey, 25 | phase: VotingPhase, 26 | viewNumber: ViewNumber, 27 | blockHash: A#Hash 28 | ): PartialSig[A] = { 29 | val msgHash = contentHash(phase, viewNumber, blockHash) 30 | PartialSignature(ECDSASignature.sign(msgHash, signingKey.underlying)) 31 | } 32 | 33 | override def combine( 34 | signatures: Seq[PartialSig[A]] 35 | ): GroupSig[A] = 36 | GroupSignature(signatures.map(_.sig).toList) 37 | 38 | /** Validate that partial signature was created by a given public key. 39 | * 40 | * Check that the signer is part of the federation. 41 | */ 42 | override def validate( 43 | publicKey: ECPublicKey, 44 | signature: PartialSig[A], 45 | phase: VotingPhase, 46 | viewNumber: ViewNumber, 47 | blockHash: A#Hash 48 | ): Boolean = { 49 | val msgHash = contentHash(phase, viewNumber, blockHash) 50 | signature.sig 51 | .publicKey(msgHash) 52 | .map(ECPublicKey(_)) 53 | .contains(publicKey) 54 | } 55 | 56 | /** Validate a group signature. 57 | * 58 | * Check that enough members of the federation signed, 59 | * and only the members. 60 | */ 61 | override def validate( 62 | federation: Federation[ECPublicKey], 63 | signature: GroupSig[A], 64 | phase: VotingPhase, 65 | viewNumber: ViewNumber, 66 | blockHash: A#Hash 67 | ): Boolean = { 68 | val msgHash = contentHash(phase, viewNumber, blockHash) 69 | val signers = 70 | signature.sig 71 | .flatMap(s => s.publicKey(msgHash).map(ECPublicKey(_))) 72 | .toSet 73 | 74 | val areUniqueSigners = signers.size == signature.sig.size 75 | val areFederationMembers = (signers -- federation.publicKeys).isEmpty 76 | val isQuorumReached = signers.size == federation.quorumSize 77 | 78 | areUniqueSigners && areFederationMembers && isQuorumReached 79 | } 80 | 81 | private def contentHash( 82 | phase: VotingPhase, 83 | viewNumber: ViewNumber, 84 | blockHash: A#Hash 85 | ): Array[Byte] = 86 | Keccak256(contentSerializer(phase, viewNumber, blockHash)).toArray 87 | } 88 | -------------------------------------------------------------------------------- /metronome/hotstuff/consensus/src/io/iohk/metronome/hotstuff/consensus/basic/Signing.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.hotstuff.consensus.basic 2 | 3 | import io.iohk.metronome.crypto.{GroupSignature, PartialSignature} 4 | import io.iohk.metronome.hotstuff.consensus.{Federation, ViewNumber} 5 | import scodec.bits.ByteVector 6 | 7 | trait Signing[A <: Agreement] { 8 | 9 | def sign( 10 | signingKey: A#SKey, 11 | phase: VotingPhase, 12 | viewNumber: ViewNumber, 13 | blockHash: A#Hash 14 | ): Signing.PartialSig[A] 15 | 16 | def combine( 17 | signatures: Seq[Signing.PartialSig[A]] 18 | ): Signing.GroupSig[A] 19 | 20 | /** Validate that partial signature was created by a given public key. */ 21 | def validate( 22 | publicKey: A#PKey, 23 | signature: Signing.PartialSig[A], 24 | phase: VotingPhase, 25 | viewNumber: ViewNumber, 26 | blockHash: A#Hash 27 | ): Boolean 28 | 29 | /** Validate a group signature. 30 | * 31 | * Check that enough members of the federation signed, 32 | * and only the members. 33 | */ 34 | def validate( 35 | federation: Federation[A#PKey], 36 | signature: Signing.GroupSig[A], 37 | phase: VotingPhase, 38 | viewNumber: ViewNumber, 39 | blockHash: A#Hash 40 | ): Boolean 41 | 42 | def validate(sender: A#PKey, vote: Message.Vote[A]): Boolean = 43 | validate( 44 | sender, 45 | vote.signature, 46 | vote.phase, 47 | vote.viewNumber, 48 | vote.blockHash 49 | ) 50 | 51 | def validate( 52 | federation: Federation[A#PKey], 53 | quorumCertificate: QuorumCertificate[A, VotingPhase] 54 | ): Boolean = 55 | validate( 56 | federation, 57 | quorumCertificate.signature, 58 | quorumCertificate.phase, 59 | quorumCertificate.viewNumber, 60 | quorumCertificate.blockHash 61 | ) 62 | } 63 | 64 | object Signing { 65 | def apply[A <: Agreement: Signing]: Signing[A] = implicitly[Signing[A]] 66 | 67 | def secp256k1[A <: Secp256k1Agreement]( 68 | contentSerializer: (VotingPhase, ViewNumber, A#Hash) => ByteVector 69 | ): Signing[A] = new Secp256k1Signing[A](contentSerializer) 70 | 71 | type PartialSig[A <: Agreement] = 72 | PartialSignature[A#PKey, (VotingPhase, ViewNumber, A#Hash), A#PSig] 73 | 74 | type GroupSig[A <: Agreement] = 75 | GroupSignature[A#PKey, (VotingPhase, ViewNumber, A#Hash), A#GSig] 76 | } 77 | -------------------------------------------------------------------------------- /metronome/hotstuff/consensus/src/io/iohk/metronome/hotstuff/consensus/package.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.hotstuff 2 | 3 | package object consensus { 4 | type ViewNumber = ViewNumber.Tagged 5 | } 6 | -------------------------------------------------------------------------------- /metronome/hotstuff/service/src/io/iohk/metronome/hotstuff/service/ApplicationService.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.hotstuff.service 2 | 3 | import cats.data.{NonEmptyVector, NonEmptyList} 4 | import io.iohk.metronome.hotstuff.consensus.basic.{ 5 | Agreement, 6 | QuorumCertificate, 7 | Phase 8 | } 9 | 10 | /** Represents the "application" domain to the HotStuff module, 11 | * performing all delegations that HotStuff can't do on its own. 12 | */ 13 | trait ApplicationService[F[_], A <: Agreement] { 14 | // TODO (PM-3109): Create block. 15 | def createBlock( 16 | highQC: QuorumCertificate[A, Phase.Prepare] 17 | ): F[Option[A#Block]] 18 | 19 | // TODO (PM-3132, PM-3133): Block validation. 20 | // Returns None if validation cannot be carried out due to data availability issues within a given timeout. 21 | def validateBlock(block: A#Block): F[Option[Boolean]] 22 | 23 | // TODO (PM-3108, PM-3107, PM-3137, PM-3110): Tell the application to execute a block. 24 | // I cannot be sure that all blocks that get committed to gether fit into memory, 25 | // so we pass them one by one, but all of them are accompanied by the final Commit Q.C. 26 | // and the path of block hashes from the block being executed to the one committed. 27 | // Perhaps the application service can cache the headers if it needs to produce a 28 | // proof of the BFT agreement at the end. 29 | // Returns a flag to indicate whether the block execution results have been persisted, 30 | // whether the block and any corresponding state can be used as a starting point after a restart. 31 | def executeBlock( 32 | block: A#Block, 33 | commitQC: QuorumCertificate[A, Phase.Commit], 34 | commitPath: NonEmptyList[A#Hash] 35 | ): F[Boolean] 36 | 37 | // TODO (PM-3135): Tell the application to sync any state of the block, i.e. the Ledger. 38 | // The `sources` are peers who most probably have this state. 39 | // The full `block` is given because it may not be persisted yet. 40 | // Return `true` if the block storage can be pruned after this operation from earlier blocks, 41 | // which may not be the case if the application syncs by downloading all the blocks. 42 | def syncState(sources: NonEmptyVector[A#PKey], block: A#Block): F[Boolean] 43 | } 44 | -------------------------------------------------------------------------------- /metronome/hotstuff/service/src/io/iohk/metronome/hotstuff/service/HotStuffService.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.hotstuff.service 2 | 3 | import cats.Parallel 4 | import cats.effect.{Concurrent, ContextShift, Resource, Timer} 5 | import io.iohk.metronome.hotstuff.consensus.basic.{ 6 | Agreement, 7 | ProtocolState, 8 | Message, 9 | Block, 10 | Signing 11 | } 12 | import io.iohk.metronome.hotstuff.service.execution.BlockExecutor 13 | import io.iohk.metronome.hotstuff.service.messages.{ 14 | HotStuffMessage, 15 | SyncMessage 16 | } 17 | import io.iohk.metronome.hotstuff.service.pipes.SyncPipe 18 | import io.iohk.metronome.hotstuff.service.storage.{ 19 | BlockStorage, 20 | ViewStateStorage 21 | } 22 | import io.iohk.metronome.hotstuff.service.tracing.{ 23 | ConsensusTracers, 24 | SyncTracers 25 | } 26 | import io.iohk.metronome.networking.Network 27 | import io.iohk.metronome.storage.KVStoreRunner 28 | 29 | object HotStuffService { 30 | 31 | /** Start up the HotStuff service stack. */ 32 | def apply[ 33 | F[_]: Concurrent: ContextShift: Timer: Parallel, 34 | N, 35 | A <: Agreement: Block: Signing 36 | ]( 37 | network: Network[F, A#PKey, HotStuffMessage[A]], 38 | appService: ApplicationService[F, A], 39 | blockStorage: BlockStorage[N, A], 40 | viewStateStorage: ViewStateStorage[N, A], 41 | initState: ProtocolState[A], 42 | consensusConfig: ConsensusService.Config 43 | )(implicit 44 | consensusTracers: ConsensusTracers[F, A], 45 | syncTracers: SyncTracers[F, A], 46 | storeRunner: KVStoreRunner[F, N] 47 | ): Resource[F, Unit] = 48 | for { 49 | (consensusNetwork, syncNetwork) <- Network 50 | .splitter[F, A#PKey, HotStuffMessage[A], Message[A], SyncMessage[A]]( 51 | network 52 | )( 53 | split = { 54 | case HotStuffMessage.ConsensusMessage(message) => Left(message) 55 | case HotStuffMessage.SyncMessage(message) => Right(message) 56 | }, 57 | merge = { 58 | case Left(message) => HotStuffMessage.ConsensusMessage(message) 59 | case Right(message) => HotStuffMessage.SyncMessage(message) 60 | } 61 | ) 62 | 63 | syncPipe <- Resource.liftF { SyncPipe[F, A] } 64 | 65 | blockExecutor <- BlockExecutor[F, N, A]( 66 | appService, 67 | blockStorage, 68 | viewStateStorage 69 | ) 70 | 71 | consensusService <- ConsensusService( 72 | initState.publicKey, 73 | consensusNetwork, 74 | appService, 75 | blockExecutor, 76 | blockStorage, 77 | viewStateStorage, 78 | syncPipe.left, 79 | initState, 80 | consensusConfig 81 | ) 82 | 83 | syncService <- SyncService( 84 | initState.publicKey, 85 | initState.federation, 86 | syncNetwork, 87 | appService, 88 | blockExecutor, 89 | blockStorage, 90 | viewStateStorage, 91 | syncPipe.right, 92 | consensusService.getState 93 | ) 94 | } yield () 95 | } 96 | -------------------------------------------------------------------------------- /metronome/hotstuff/service/src/io/iohk/metronome/hotstuff/service/Status.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.hotstuff.service 2 | 3 | import io.iohk.metronome.hotstuff.consensus.ViewNumber 4 | import io.iohk.metronome.hotstuff.consensus.basic.{ 5 | Agreement, 6 | QuorumCertificate, 7 | Phase 8 | } 9 | 10 | /** Status has all the fields necessary for nodes to sync with each other. 11 | * 12 | * This is to facilitate nodes rejoining the network, 13 | * or re-syncing their views after some network glitch. 14 | */ 15 | case class Status[A <: Agreement]( 16 | viewNumber: ViewNumber, 17 | prepareQC: QuorumCertificate[A, Phase.Prepare], 18 | commitQC: QuorumCertificate[A, Phase.Commit] 19 | ) 20 | -------------------------------------------------------------------------------- /metronome/hotstuff/service/src/io/iohk/metronome/hotstuff/service/codecs/DefaultConsensusCodecs.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.hotstuff.service.codecs 2 | 3 | import scodec.Codec 4 | import scodec.codecs._ 5 | import io.iohk.metronome.hotstuff.consensus.ViewNumber 6 | import io.iohk.metronome.hotstuff.consensus.basic.{ 7 | Phase, 8 | VotingPhase, 9 | Agreement 10 | } 11 | 12 | trait DefaultConsensusCodecs[A <: Agreement] { 13 | import scodec.codecs.implicits._ 14 | 15 | implicit def hashCodec: Codec[A#Hash] 16 | 17 | implicit val phaseCodec: Codec[VotingPhase] = { 18 | import Phase._ 19 | mappedEnum(uint2, Prepare -> 1, PreCommit -> 2, Commit -> 3) 20 | } 21 | 22 | implicit val viewNumberCodec: Codec[ViewNumber] = 23 | Codec[Long].xmap(ViewNumber(_), identity) 24 | 25 | implicit val contentCodec: Codec[(VotingPhase, ViewNumber, A#Hash)] = 26 | phaseCodec ~~ viewNumberCodec ~~ hashCodec 27 | } 28 | -------------------------------------------------------------------------------- /metronome/hotstuff/service/src/io/iohk/metronome/hotstuff/service/codecs/DefaultDuplexMessageCodecs.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.hotstuff.service.codecs 2 | 3 | import scodec.Codec 4 | import scodec.codecs._ 5 | import io.iohk.metronome.hotstuff.consensus.basic.Agreement 6 | import io.iohk.metronome.hotstuff.service.messages.DuplexMessage 7 | 8 | trait DefaultDuplexMessageCodecs[A <: Agreement, M] { 9 | self: DefaultMessageCodecs[A] => 10 | 11 | implicit def applicationMessageCodec: Codec[M] 12 | 13 | implicit val duplexAgreementMessageCodec 14 | : Codec[DuplexMessage.AgreementMessage[A]] = 15 | Codec.deriveLabelledGeneric 16 | 17 | implicit val duplexApplicationMessageCodec 18 | : Codec[DuplexMessage.ApplicationMessage[M]] = 19 | Codec.deriveLabelledGeneric 20 | 21 | implicit val duplexMessageCodec: Codec[DuplexMessage[A, M]] = 22 | discriminated[DuplexMessage[A, M]] 23 | .by(uint2) 24 | .typecase(0, duplexAgreementMessageCodec) 25 | .typecase(1, duplexApplicationMessageCodec) 26 | } 27 | -------------------------------------------------------------------------------- /metronome/hotstuff/service/src/io/iohk/metronome/hotstuff/service/codecs/DefaultMessageCodecs.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.hotstuff.service.codecs 2 | 3 | import scodec.Codec 4 | import scodec.codecs._ 5 | import io.iohk.metronome.hotstuff.consensus.basic.Agreement 6 | import io.iohk.metronome.hotstuff.service.messages.{ 7 | SyncMessage, 8 | HotStuffMessage 9 | } 10 | 11 | trait DefaultMessageCodecs[A <: Agreement] { 12 | self: DefaultConsensusCodecs[A] with DefaultProtocolCodecs[A] => 13 | import scodec.codecs.implicits._ 14 | 15 | implicit val getStatusRequestCodec: Codec[SyncMessage.GetStatusRequest[A]] = 16 | Codec.deriveLabelledGeneric 17 | 18 | implicit val getStatusResponseCodec: Codec[SyncMessage.GetStatusResponse[A]] = 19 | Codec.deriveLabelledGeneric 20 | 21 | implicit val getBlockRequestCodec: Codec[SyncMessage.GetBlockRequest[A]] = 22 | Codec.deriveLabelledGeneric 23 | 24 | implicit val getBlockResponseCodec: Codec[SyncMessage.GetBlockResponse[A]] = 25 | Codec.deriveLabelledGeneric 26 | 27 | implicit val syncMessageCodec: Codec[SyncMessage[A]] = 28 | discriminated[SyncMessage[A]] 29 | .by(uint2) 30 | .typecase(0, getStatusRequestCodec) 31 | .typecase(1, getStatusResponseCodec) 32 | .typecase(2, getBlockRequestCodec) 33 | .typecase(3, getBlockResponseCodec) 34 | 35 | implicit val hotstuffConsensusMessageCodec 36 | : Codec[HotStuffMessage.ConsensusMessage[A]] = 37 | Codec.deriveLabelledGeneric 38 | 39 | implicit val hotstuffSyncMessageCodec: Codec[HotStuffMessage.SyncMessage[A]] = 40 | Codec.deriveLabelledGeneric 41 | 42 | implicit val hotstuffMessageCodec: Codec[HotStuffMessage[A]] = 43 | discriminated[HotStuffMessage[A]] 44 | .by(uint2) 45 | .typecase(0, hotstuffConsensusMessageCodec) 46 | .typecase(1, hotstuffSyncMessageCodec) 47 | } 48 | -------------------------------------------------------------------------------- /metronome/hotstuff/service/src/io/iohk/metronome/hotstuff/service/codecs/DefaultSecp256k1Codecs.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.hotstuff.service.codecs 2 | 3 | import io.iohk.ethereum.crypto.ECDSASignature 4 | import io.iohk.metronome.hotstuff.consensus.basic.{ 5 | Secp256k1Agreement, 6 | Agreement 7 | } 8 | import scodec.{Codec, Attempt, Err} 9 | import scodec.bits.BitVector 10 | 11 | trait DefaultSecp256k1Codecs[A <: Secp256k1Agreement] { 12 | self: DefaultProtocolCodecs[A] => 13 | import scodec.codecs.implicits._ 14 | 15 | implicit val ecdsaSignatureCodec: Codec[ECDSASignature] = { 16 | import akka.util.ByteString 17 | Codec[BitVector].exmap( 18 | bits => 19 | Attempt.fromOption( 20 | ECDSASignature.fromBytes(ByteString.fromArray(bits.toByteArray)), 21 | Err("Not a valid signature.") 22 | ), 23 | sig => Attempt.successful(BitVector(sig.toBytes.toArray[Byte])) 24 | ) 25 | } 26 | 27 | override implicit def groupSignatureCodec 28 | : Codec[Agreement.GroupSignature[A]] = 29 | Codec.deriveLabelledGeneric 30 | 31 | override implicit def partialSignatureCodec 32 | : Codec[Agreement.PartialSignature[A]] = 33 | Codec.deriveLabelledGeneric 34 | 35 | } 36 | -------------------------------------------------------------------------------- /metronome/hotstuff/service/src/io/iohk/metronome/hotstuff/service/messages/DuplexMessage.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.hotstuff.service.messages 2 | 3 | import io.iohk.metronome.hotstuff 4 | import io.iohk.metronome.hotstuff.consensus.basic.Agreement 5 | 6 | /** Messages type to use in the networking layer if the use case has 7 | * application specific message types, e.g. ledger synchronisation, 8 | * not just the general BFT agreement (which could be enough if 9 | * we need to execute all blocks to synchronize state). 10 | */ 11 | sealed trait DuplexMessage[+A <: Agreement, +M] 12 | 13 | object DuplexMessage { 14 | 15 | /** General BFT agreement message. */ 16 | case class AgreementMessage[A <: Agreement]( 17 | message: hotstuff.service.messages.HotStuffMessage[A] 18 | ) extends DuplexMessage[A, Nothing] 19 | 20 | /** Application specific message. */ 21 | case class ApplicationMessage[M]( 22 | message: M 23 | ) extends DuplexMessage[Nothing, M] 24 | } 25 | -------------------------------------------------------------------------------- /metronome/hotstuff/service/src/io/iohk/metronome/hotstuff/service/messages/HotStuffMessage.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.hotstuff.service.messages 2 | 3 | import io.iohk.metronome.hotstuff 4 | import io.iohk.metronome.hotstuff.consensus.basic.Agreement 5 | 6 | /** Messages which are generic to any HotStuff BFT agreement. */ 7 | sealed trait HotStuffMessage[A <: Agreement] 8 | 9 | object HotStuffMessage { 10 | 11 | /** Messages which are part of the basic HotStuff BFT algorithm itself. */ 12 | case class ConsensusMessage[A <: Agreement]( 13 | message: hotstuff.consensus.basic.Message[A] 14 | ) extends HotStuffMessage[A] 15 | 16 | /** Messages that support the HotStuff BFT agreement but aren't part of 17 | * the core algorithm, e.g. block and view number synchronisation. 18 | */ 19 | case class SyncMessage[A <: Agreement]( 20 | message: hotstuff.service.messages.SyncMessage[A] 21 | ) extends HotStuffMessage[A] 22 | 23 | } 24 | -------------------------------------------------------------------------------- /metronome/hotstuff/service/src/io/iohk/metronome/hotstuff/service/messages/SyncMessage.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.hotstuff.service.messages 2 | 3 | import io.iohk.metronome.core.messages.{RPCMessage, RPCMessageCompanion} 4 | import io.iohk.metronome.hotstuff.consensus.basic.Agreement 5 | import io.iohk.metronome.hotstuff.service.Status 6 | 7 | /** Messages needed to fully realise the HotStuff protocol, 8 | * without catering for any application specific concerns. 9 | */ 10 | sealed trait SyncMessage[+A <: Agreement] { self: RPCMessage => } 11 | 12 | object SyncMessage extends RPCMessageCompanion { 13 | case class GetStatusRequest[A <: Agreement]( 14 | requestId: RequestId 15 | ) extends SyncMessage[A] 16 | with Request 17 | 18 | case class GetStatusResponse[A <: Agreement]( 19 | requestId: RequestId, 20 | status: Status[A] 21 | ) extends SyncMessage[A] 22 | with Response 23 | 24 | case class GetBlockRequest[A <: Agreement]( 25 | requestId: RequestId, 26 | blockHash: A#Hash 27 | ) extends SyncMessage[A] 28 | with Request 29 | 30 | case class GetBlockResponse[A <: Agreement]( 31 | requestId: RequestId, 32 | block: A#Block 33 | ) extends SyncMessage[A] 34 | with Response 35 | 36 | implicit def getBlockPair[A <: Agreement] = 37 | pair[GetBlockRequest[A], GetBlockResponse[A]] 38 | 39 | implicit def getStatusPair[A <: Agreement] = 40 | pair[GetStatusRequest[A], GetStatusResponse[A]] 41 | } 42 | -------------------------------------------------------------------------------- /metronome/hotstuff/service/src/io/iohk/metronome/hotstuff/service/pipes/SyncPipe.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.hotstuff.service.pipes 2 | 3 | import cats.effect.{Concurrent, ContextShift} 4 | import io.iohk.metronome.core.Pipe 5 | import io.iohk.metronome.hotstuff.consensus.ViewNumber 6 | import io.iohk.metronome.hotstuff.consensus.basic.{Agreement, Message} 7 | import io.iohk.metronome.hotstuff.service.Status 8 | 9 | object SyncPipe { 10 | 11 | sealed trait Request[+A <: Agreement] 12 | sealed trait Response[+A <: Agreement] 13 | 14 | /** Request the synchronization component to download 15 | * any missing dependencies up to the High Q.C., 16 | * perform any application specific validation, 17 | * including the block in the `Prepare` message, 18 | * and persist the blocks up to, but not including 19 | * the block in the `Prepare` message. 20 | * 21 | * This is because the block being prepared is 22 | * subject to further validation and voting, 23 | * while the one in the High Q.C. has gathered 24 | * a quorum from the federation. 25 | */ 26 | case class PrepareRequest[A <: Agreement]( 27 | sender: A#PKey, 28 | prepare: Message.Prepare[A] 29 | ) extends Request[A] 30 | 31 | /** Respond with the outcome of whether the 32 | * block we're being asked to prepare is 33 | * valid, according to the application rules. 34 | */ 35 | case class PrepareResponse[A <: Agreement]( 36 | request: PrepareRequest[A], 37 | isValid: Boolean 38 | ) extends Response[A] 39 | 40 | /** Request that the view state is synchronized with the whole federation, 41 | * including downloading the block and state corresponding to the latest 42 | * Commit Q.C. 43 | * 44 | * The eventual response should contain the new view status to be applied 45 | * on the protocol state. 46 | */ 47 | case class StatusRequest(viewNumber: ViewNumber) extends Request[Nothing] 48 | 49 | /** Response with the new status to resume the protocol from, after the 50 | * state has been synchronized up to the included Commit Q.C. 51 | */ 52 | case class StatusResponse[A <: Agreement]( 53 | status: Status[A] 54 | ) extends Response[A] 55 | 56 | def apply[F[_]: Concurrent: ContextShift, A <: Agreement]: F[SyncPipe[F, A]] = 57 | Pipe[F, SyncPipe.Request[A], SyncPipe.Response[A]] 58 | } 59 | -------------------------------------------------------------------------------- /metronome/hotstuff/service/src/io/iohk/metronome/hotstuff/service/pipes/package.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.hotstuff.service 2 | 3 | import io.iohk.metronome.core.Pipe 4 | import io.iohk.metronome.hotstuff.consensus.basic.Agreement 5 | 6 | package object pipes { 7 | 8 | /** Communication pipe with the block synchronization and validation component. */ 9 | type SyncPipe[F[_], A <: Agreement] = 10 | Pipe[F, SyncPipe.Request[A], SyncPipe.Response[A]] 11 | } 12 | -------------------------------------------------------------------------------- /metronome/hotstuff/service/src/io/iohk/metronome/hotstuff/service/storage/BlockPruning.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.hotstuff.service.storage 2 | 3 | import io.iohk.metronome.hotstuff.consensus.basic.Agreement 4 | import io.iohk.metronome.storage.KVStore 5 | 6 | import cats.implicits._ 7 | 8 | object BlockPruning { 9 | 10 | /** Prune blocks which are not descendants of the N-th ancestor of the last executed block. */ 11 | def prune[N, A <: Agreement]( 12 | blockStorage: BlockStorage[N, A], 13 | viewStateStorage: ViewStateStorage[N, A], 14 | blockHistorySize: Int 15 | ): KVStore[N, Unit] = { 16 | for { 17 | // Always keep the last executed block. 18 | lastExecutedBlock <- viewStateStorage.getLastExecutedBlockHash.lift 19 | pathFromRoot <- blockStorage.getPathFromRoot(lastExecutedBlock).lift 20 | 21 | // Everything but the last N blocks in the chain leading up to the 22 | // last executed block can be pruned. We do so by making the Nth 23 | // ancestor of the last executed block the new root of the tree. 24 | maybeNewRoot = pathFromRoot.reverse.lift(blockHistorySize - 1) 25 | 26 | _ <- maybeNewRoot match { 27 | case Some(newRoot) => 28 | blockStorage.pruneNonDescendants(newRoot) >> 29 | viewStateStorage.setRootBlockHash(newRoot) 30 | 31 | case None => 32 | KVStore.instance[N].unit 33 | } 34 | } yield () 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /metronome/hotstuff/service/src/io/iohk/metronome/hotstuff/service/storage/BlockStorage.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.hotstuff.service.storage 2 | 3 | import io.iohk.metronome.storage.{KVCollection, KVTree} 4 | import io.iohk.metronome.hotstuff.consensus.basic.{Agreement, Block} 5 | 6 | /** Storage for blocks that maintains parent-child relationships as well, 7 | * to facilitate tree traversal and pruning. 8 | * 9 | * It is assumed that the application maintains some pointers into the tree 10 | * where it can start traversing from, e.g. the last Commit Quorum Certificate 11 | * would point at a block hash which would serve as the entry point. 12 | */ 13 | class BlockStorage[N, A <: Agreement: Block]( 14 | blockColl: KVCollection[N, A#Hash, A#Block], 15 | blockMetaColl: KVCollection[N, A#Hash, KVTree.NodeMeta[A#Hash]], 16 | parentToChildrenColl: KVCollection[N, A#Hash, Set[A#Hash]] 17 | ) extends KVTree[N, A#Hash, A#Block]( 18 | blockColl, 19 | blockMetaColl, 20 | parentToChildrenColl 21 | )(BlockStorage.node[A]) 22 | 23 | object BlockStorage { 24 | implicit def node[A <: Agreement: Block]: KVTree.Node[A#Hash, A#Block] = 25 | new KVTree.Node[A#Hash, A#Block] { 26 | override def key(value: A#Block): A#Hash = 27 | Block[A].blockHash(value) 28 | override def parentKey(value: A#Block): A#Hash = 29 | Block[A].parentBlockHash(value) 30 | override def height(value: A#Block): Long = 31 | Block[A].height(value) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /metronome/hotstuff/service/src/io/iohk/metronome/hotstuff/service/tracing/ConsensusEvent.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.hotstuff.service.tracing 2 | 3 | import io.iohk.metronome.hotstuff.consensus.ViewNumber 4 | import io.iohk.metronome.hotstuff.consensus.basic.{ 5 | Agreement, 6 | Event, 7 | ProtocolError 8 | } 9 | import io.iohk.metronome.hotstuff.consensus.basic.{ 10 | QuorumCertificate, 11 | VotingPhase 12 | } 13 | import io.iohk.metronome.hotstuff.service.ConsensusService.MessageCounter 14 | import io.iohk.metronome.hotstuff.service.Status 15 | 16 | sealed trait ConsensusEvent[+A <: Agreement] 17 | 18 | object ConsensusEvent { 19 | 20 | /** The round ended without having reached decision. */ 21 | case class Timeout( 22 | viewNumber: ViewNumber, 23 | messageCounter: MessageCounter 24 | ) extends ConsensusEvent[Nothing] 25 | 26 | /** A full view synchronization was requested after timing out without any in-sync messages. */ 27 | case class ViewSync( 28 | viewNumber: ViewNumber 29 | ) extends ConsensusEvent[Nothing] 30 | 31 | /** Adopting the view of the federation after a sync. */ 32 | case class AdoptView[A <: Agreement]( 33 | status: Status[A] 34 | ) extends ConsensusEvent[A] 35 | 36 | /** The state advanced to a new view. */ 37 | case class NewView(viewNumber: ViewNumber) extends ConsensusEvent[Nothing] 38 | 39 | /** Quorum over some block. */ 40 | case class Quorum[A <: Agreement]( 41 | quorumCertificate: QuorumCertificate[A, VotingPhase] 42 | ) extends ConsensusEvent[A] 43 | 44 | /** A formally valid message was received from an earlier view number. */ 45 | case class FromPast[A <: Agreement](message: Event.MessageReceived[A]) 46 | extends ConsensusEvent[A] 47 | 48 | /** A formally valid message was received from a future view number. */ 49 | case class FromFuture[A <: Agreement](message: Event.MessageReceived[A]) 50 | extends ConsensusEvent[A] 51 | 52 | /** An event that arrived too early but got stashed and will be redelivered. */ 53 | case class Stashed[A <: Agreement]( 54 | error: ProtocolError.TooEarly[A] 55 | ) extends ConsensusEvent[A] 56 | 57 | /** A rejected event. */ 58 | case class Rejected[A <: Agreement]( 59 | error: ProtocolError[A] 60 | ) extends ConsensusEvent[A] 61 | 62 | /** A block has been removed from storage by the time it was to be executed. */ 63 | case class ExecutionSkipped[A <: Agreement]( 64 | blockHash: A#Hash 65 | ) extends ConsensusEvent[A] 66 | 67 | /** A block has been executed. */ 68 | case class BlockExecuted[A <: Agreement]( 69 | blockHash: A#Hash 70 | ) extends ConsensusEvent[A] 71 | 72 | /** An unexpected error in one of the background tasks. */ 73 | case class Error( 74 | message: String, 75 | error: Throwable 76 | ) extends ConsensusEvent[Nothing] 77 | } 78 | -------------------------------------------------------------------------------- /metronome/hotstuff/service/src/io/iohk/metronome/hotstuff/service/tracing/ConsensusTracers.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.hotstuff.service.tracing 2 | 3 | import cats.implicits._ 4 | import io.iohk.metronome.tracer.Tracer 5 | import io.iohk.metronome.hotstuff.consensus.ViewNumber 6 | import io.iohk.metronome.hotstuff.consensus.basic.{ 7 | Agreement, 8 | Event, 9 | ProtocolError, 10 | QuorumCertificate, 11 | VotingPhase 12 | } 13 | import io.iohk.metronome.hotstuff.service.ConsensusService.MessageCounter 14 | import io.iohk.metronome.hotstuff.service.Status 15 | 16 | case class ConsensusTracers[F[_], A <: Agreement]( 17 | timeout: Tracer[F, (ViewNumber, MessageCounter)], 18 | viewSync: Tracer[F, ViewNumber], 19 | adoptView: Tracer[F, Status[A]], 20 | newView: Tracer[F, ViewNumber], 21 | quorum: Tracer[F, QuorumCertificate[A, _]], 22 | fromPast: Tracer[F, Event.MessageReceived[A]], 23 | fromFuture: Tracer[F, Event.MessageReceived[A]], 24 | stashed: Tracer[F, ProtocolError.TooEarly[A]], 25 | rejected: Tracer[F, ProtocolError[A]], 26 | executionSkipped: Tracer[F, A#Hash], 27 | blockExecuted: Tracer[F, A#Hash], 28 | error: Tracer[F, (String, Throwable)] 29 | ) 30 | 31 | object ConsensusTracers { 32 | import ConsensusEvent._ 33 | 34 | def apply[F[_], A <: Agreement]( 35 | tracer: Tracer[F, ConsensusEvent[A]] 36 | ): ConsensusTracers[F, A] = 37 | ConsensusTracers[F, A]( 38 | timeout = tracer.contramap[(ViewNumber, MessageCounter)]( 39 | (Timeout.apply _).tupled 40 | ), 41 | viewSync = tracer.contramap[ViewNumber](ViewSync(_)), 42 | adoptView = tracer.contramap[Status[A]](AdoptView(_)), 43 | newView = tracer.contramap[ViewNumber](NewView(_)), 44 | quorum = tracer.contramap[QuorumCertificate[A, _]](qc => 45 | Quorum(qc.coerce[VotingPhase]) 46 | ), 47 | fromPast = tracer.contramap[Event.MessageReceived[A]](FromPast(_)), 48 | fromFuture = tracer.contramap[Event.MessageReceived[A]](FromFuture(_)), 49 | stashed = tracer.contramap[ProtocolError.TooEarly[A]](Stashed(_)), 50 | rejected = tracer.contramap[ProtocolError[A]](Rejected(_)), 51 | executionSkipped = tracer.contramap[A#Hash](ExecutionSkipped(_)), 52 | blockExecuted = tracer.contramap[A#Hash](BlockExecuted(_)), 53 | error = tracer.contramap[(String, Throwable)]((Error.apply _).tupled) 54 | ) 55 | } 56 | -------------------------------------------------------------------------------- /metronome/hotstuff/service/src/io/iohk/metronome/hotstuff/service/tracing/SyncEvent.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.hotstuff.service.tracing 2 | 3 | import io.iohk.metronome.core.Validated 4 | import io.iohk.metronome.hotstuff.consensus.basic.{Agreement, ProtocolError} 5 | import io.iohk.metronome.hotstuff.service.messages.SyncMessage 6 | import io.iohk.metronome.hotstuff.service.Status 7 | 8 | sealed trait SyncEvent[+A <: Agreement] 9 | 10 | object SyncEvent { 11 | 12 | /** A federation member is sending us so many requests that its work queue is full. */ 13 | case class QueueFull[A <: Agreement]( 14 | sender: A#PKey 15 | ) extends SyncEvent[A] 16 | 17 | /** A request we sent couldn't be matched with a response in time. */ 18 | case class RequestTimeout[A <: Agreement]( 19 | recipient: A#PKey, 20 | request: SyncMessage[A] with SyncMessage.Request 21 | ) extends SyncEvent[A] 22 | 23 | /** A response was ignored either because the request ID didn't match, or it already timed out, 24 | * or the response type didn't match the expected one based on the request. 25 | */ 26 | case class ResponseIgnored[A <: Agreement]( 27 | sender: A#PKey, 28 | response: SyncMessage[A] with SyncMessage.Response, 29 | maybeError: Option[Throwable] 30 | ) extends SyncEvent[A] 31 | 32 | /** Performed a poll for `Status` across the federation. 33 | * Only contains results for federation members that responded within the timeout. 34 | */ 35 | case class StatusPoll[A <: Agreement]( 36 | statuses: Map[A#PKey, Validated[Status[A]]] 37 | ) extends SyncEvent[A] 38 | 39 | /** A federation members sent a `Status` with invalid content. */ 40 | case class InvalidStatus[A <: Agreement]( 41 | status: Status[A], 42 | error: ProtocolError.InvalidQuorumCertificate[A], 43 | hint: String 44 | ) extends SyncEvent[A] 45 | 46 | /** An unexpected error in one of the background tasks. */ 47 | case class Error(error: Throwable) extends SyncEvent[Nothing] 48 | } 49 | -------------------------------------------------------------------------------- /metronome/hotstuff/service/src/io/iohk/metronome/hotstuff/service/tracing/SyncTracers.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.hotstuff.service.tracing 2 | 3 | import cats.implicits._ 4 | import io.iohk.metronome.core.Validated 5 | import io.iohk.metronome.tracer.Tracer 6 | import io.iohk.metronome.hotstuff.consensus.basic.{Agreement, ProtocolError} 7 | import io.iohk.metronome.hotstuff.service.messages.SyncMessage 8 | import io.iohk.metronome.hotstuff.service.Status 9 | 10 | case class SyncTracers[F[_], A <: Agreement]( 11 | queueFull: Tracer[F, A#PKey], 12 | requestTimeout: Tracer[F, SyncTracers.Request[A]], 13 | responseIgnored: Tracer[F, SyncTracers.Response[A]], 14 | statusPoll: Tracer[F, SyncTracers.Statuses[A]], 15 | invalidStatus: Tracer[F, SyncTracers.StatusError[A]], 16 | error: Tracer[F, Throwable] 17 | ) 18 | 19 | object SyncTracers { 20 | import SyncEvent._ 21 | 22 | type Request[A <: Agreement] = 23 | (A#PKey, SyncMessage[A] with SyncMessage.Request) 24 | 25 | type Response[A <: Agreement] = 26 | (A#PKey, SyncMessage[A] with SyncMessage.Response, Option[Throwable]) 27 | 28 | type Statuses[A <: Agreement] = 29 | Map[A#PKey, Validated[Status[A]]] 30 | 31 | type StatusError[A <: Agreement] = 32 | (Status[A], ProtocolError.InvalidQuorumCertificate[A], String) 33 | 34 | def apply[F[_], A <: Agreement]( 35 | tracer: Tracer[F, SyncEvent[A]] 36 | ): SyncTracers[F, A] = 37 | SyncTracers[F, A]( 38 | queueFull = tracer.contramap[A#PKey](QueueFull(_)), 39 | requestTimeout = tracer 40 | .contramap[Request[A]]((RequestTimeout.apply[A] _).tupled), 41 | responseIgnored = tracer 42 | .contramap[Response[A]]((ResponseIgnored.apply[A] _).tupled), 43 | statusPoll = tracer 44 | .contramap[Statuses[A]](StatusPoll(_)), 45 | invalidStatus = 46 | tracer.contramap[StatusError[A]]((InvalidStatus.apply[A] _).tupled), 47 | error = tracer.contramap[Throwable](Error(_)) 48 | ) 49 | } 50 | -------------------------------------------------------------------------------- /metronome/logging/src/io/iohk/metronome/logging/HybridLog.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.logging 2 | 3 | import cats.implicits._ 4 | import cats.effect.{Clock, Sync} 5 | import io.circe.JsonObject 6 | import java.time.Instant 7 | import scala.reflect.ClassTag 8 | import java.util.concurrent.TimeUnit 9 | 10 | /** Type class to transform instances of `T` to `HybridLogObject`. 11 | * 12 | * Using an effect because we attach a timestamp. 13 | */ 14 | trait HybridLog[F[_], T] { 15 | def apply(value: T): F[HybridLogObject] 16 | } 17 | 18 | object HybridLog { 19 | 20 | /** Create an instance of `HybridLog` for a type `T` by passing 21 | * functions to transform instances of `T` to message and JSON. 22 | */ 23 | def instance[F[_]: Sync: Clock, T: ClassTag]( 24 | level: T => HybridLogObject.Level, 25 | message: T => String, 26 | event: T => JsonObject 27 | ): HybridLog[F, T] = 28 | new HybridLog[F, T] { 29 | val source = implicitly[ClassTag[T]].runtimeClass.getName 30 | 31 | override def apply(value: T): F[HybridLogObject] = 32 | Clock[F].realTime(TimeUnit.MILLISECONDS).map { millis => 33 | HybridLogObject( 34 | level = level(value), 35 | timestamp = Instant.ofEpochMilli(millis), 36 | source = source, 37 | message = message(value), 38 | event = event(value) 39 | ) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /metronome/logging/src/io/iohk/metronome/logging/HybridLogObject.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.logging 2 | 3 | import io.circe.JsonObject 4 | import io.circe.syntax._ 5 | import java.time.Instant 6 | import cats.Show 7 | 8 | /** A hybrid log has a human readable message, which is intended to be static, 9 | * and some key-value paramters that vary by events. 10 | * 11 | * See https://medium.com/unomaly/logging-wisdom-how-to-log-5a19145e35ec 12 | */ 13 | case class HybridLogObject( 14 | timestamp: Instant, 15 | source: String, 16 | level: HybridLogObject.Level, 17 | // Something captured about what emitted this event. 18 | // Human readable message, which typically shouldn't 19 | // change between events emitted at the same place. 20 | message: String, 21 | // Key-Value pairs that capture arbitrary data. 22 | event: JsonObject 23 | ) 24 | object HybridLogObject { 25 | sealed trait Level 26 | object Level { 27 | case object Error extends Level 28 | case object Warn extends Level 29 | case object Info extends Level 30 | case object Debug extends Level 31 | case object Trace extends Level 32 | } 33 | 34 | implicit val show: Show[HybridLogObject] = Show.show { 35 | case HybridLogObject(t, s, l, m, e) => 36 | s"$t ${l.toString.toUpperCase.padTo(5, ' ')} - ${s.split('.').last}: $m ${e.asJson.noSpaces}" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /metronome/logging/src/io/iohk/metronome/logging/InMemoryLogTracer.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.logging 2 | 3 | import cats.implicits._ 4 | import cats.effect.Sync 5 | import cats.effect.concurrent.Ref 6 | import io.iohk.metronome.tracer.Tracer 7 | 8 | /** Collect logs in memory, so we can inspect them in tests. */ 9 | object InMemoryLogTracer { 10 | 11 | class HybridLogTracer[F[_]: Sync]( 12 | logRef: Ref[F, Vector[HybridLogObject]] 13 | ) extends Tracer[F, HybridLogObject] { 14 | 15 | override def apply(a: => HybridLogObject): F[Unit] = 16 | logRef.update(_ :+ a) 17 | 18 | def getLogs: F[Seq[HybridLogObject]] = 19 | logRef.get.map(_.toSeq) 20 | 21 | def getLevel(l: HybridLogObject.Level) = 22 | getLogs.map(_.filter(_.level == l)) 23 | 24 | def getErrors = getLevel(HybridLogObject.Level.Error) 25 | def getWarns = getLevel(HybridLogObject.Level.Warn) 26 | def getInfos = getLevel(HybridLogObject.Level.Info) 27 | def getDebugs = getLevel(HybridLogObject.Level.Debug) 28 | def getTraces = getLevel(HybridLogObject.Level.Trace) 29 | 30 | def clear: F[Unit] = logRef.set(Vector.empty) 31 | } 32 | 33 | /** For example: 34 | * 35 | * ``` 36 | * val logTracer = InMemoryLogTracer.hybrid[Task] 37 | * val networkEventTracer = InMemoryLogTracer.hybrid[Task, NetworkEvent](logTracer) 38 | * val consensusEventTracer = InMemoryLogTracer.hybrid[Task, ConsensusEvent](logTracer) 39 | * 40 | * val test = for { 41 | * msg <- network.nextMessage 42 | * _ <- consensus.handleMessage(msg) 43 | * warns <- logTracer.getWarns 44 | * } yield { 45 | * warns shouldBe empty 46 | * } 47 | * 48 | * ``` 49 | */ 50 | def hybrid[F[_]: Sync]: HybridLogTracer[F] = 51 | new HybridLogTracer[F](Ref.unsafe[F, Vector[HybridLogObject]](Vector.empty)) 52 | 53 | def hybrid[F[_]: Sync, T]( 54 | tracer: HybridLogTracer[F] 55 | )(implicit ev: HybridLog[F, T]): Tracer[F, T] = 56 | Tracer.contramapM[F, T, HybridLogObject](ev.apply _, tracer) 57 | } 58 | -------------------------------------------------------------------------------- /metronome/logging/src/io/iohk/metronome/logging/LogTracer.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.logging 2 | 3 | import cats.effect.Sync 4 | import io.circe.syntax._ 5 | import io.iohk.metronome.tracer.Tracer 6 | import org.slf4j.LoggerFactory 7 | 8 | /** Forward traces to SLF4J logs. */ 9 | object LogTracer { 10 | 11 | /** Create a logger for `HybridLogObject` that delegates to SLF4J. */ 12 | def hybrid[F[_]: Sync]: Tracer[F, HybridLogObject] = 13 | new Tracer[F, HybridLogObject] { 14 | override def apply(log: => HybridLogObject): F[Unit] = Sync[F].delay { 15 | val logger = LoggerFactory.getLogger(log.source) 16 | 17 | def message = s"${log.message} ${log.event.asJson.noSpaces}" 18 | 19 | log.level match { 20 | case HybridLogObject.Level.Error => 21 | if (logger.isErrorEnabled) logger.error(message) 22 | case HybridLogObject.Level.Warn => 23 | if (logger.isWarnEnabled) logger.warn(message) 24 | case HybridLogObject.Level.Info => 25 | if (logger.isInfoEnabled) logger.info(message) 26 | case HybridLogObject.Level.Debug => 27 | if (logger.isDebugEnabled) logger.debug(message) 28 | case HybridLogObject.Level.Trace => 29 | if (logger.isTraceEnabled) logger.trace(message) 30 | } 31 | } 32 | } 33 | 34 | /** Create a logger for a type that can be transformed to a `HybridLogObject`. */ 35 | def hybrid[F[_]: Sync, T](implicit ev: HybridLog[F, T]): Tracer[F, T] = 36 | Tracer.contramapM[F, T, HybridLogObject](ev.apply _, hybrid[F]) 37 | } 38 | -------------------------------------------------------------------------------- /metronome/networking/specs/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %d{HH:mm:ss.SSS} %-5level %logger{36} %msg%n 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /metronome/networking/specs/src/io/iohk/metronome/networking/RemoteConnectionManagerTestUtils.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.networking 2 | 3 | import cats.effect.Resource 4 | import io.iohk.metronome.crypto.{ECKeyPair, ECPublicKey} 5 | 6 | import java.net.{InetSocketAddress, ServerSocket} 7 | import java.security.SecureRandom 8 | import monix.eval.Task 9 | import monix.execution.Scheduler 10 | import org.scalatest.Assertion 11 | 12 | import scala.concurrent.Future 13 | import scala.util.Random 14 | import scodec.bits.ByteVector 15 | import scodec.Codec 16 | 17 | object RemoteConnectionManagerTestUtils { 18 | def customTestCaseResourceT[T]( 19 | fixture: Resource[Task, T] 20 | )(theTest: T => Task[Assertion])(implicit s: Scheduler): Future[Assertion] = { 21 | fixture.use(fix => theTest(fix)).runToFuture 22 | } 23 | 24 | def customTestCaseT[T]( 25 | test: => Task[Assertion] 26 | )(implicit s: Scheduler): Future[Assertion] = { 27 | test.runToFuture 28 | } 29 | 30 | def randomAddress(): InetSocketAddress = { 31 | val s = new ServerSocket(0) 32 | try { 33 | new InetSocketAddress("localhost", s.getLocalPort) 34 | } finally { 35 | s.close() 36 | } 37 | } 38 | 39 | import scodec.codecs._ 40 | 41 | sealed abstract class TestMessage 42 | case class MessageA(i: Int) extends TestMessage 43 | case class MessageB(s: String) extends TestMessage 44 | 45 | object TestMessage { 46 | implicit val messageCodec: Codec[TestMessage] = discriminated[TestMessage] 47 | .by(uint8) 48 | .typecase(1, int32.as[MessageA]) 49 | .typecase(2, utf8.as[MessageB]) 50 | } 51 | 52 | def getFakeRandomKey(): ECPublicKey = { 53 | val array = new Array[Byte](ECPublicKey.Length) 54 | Random.nextBytes(array) 55 | ECPublicKey(ByteVector(array)) 56 | } 57 | 58 | case class NodeInfo(keyPair: ECKeyPair) 59 | 60 | object NodeInfo { 61 | def generateRandom(secureRandom: SecureRandom): NodeInfo = { 62 | val keyPair = ECKeyPair.generate(secureRandom) 63 | NodeInfo(keyPair) 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /metronome/networking/src/io/iohk/metronome/networking/ConnectionsRegister.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.networking 2 | 3 | import cats.effect.Concurrent 4 | import cats.effect.concurrent.Ref 5 | import io.iohk.metronome.networking.ConnectionHandler.HandledConnection 6 | import cats.implicits._ 7 | 8 | class ConnectionsRegister[F[_]: Concurrent, K, M]( 9 | registerRef: Ref[F, Map[K, HandledConnection[F, K, M]]] 10 | ) { 11 | 12 | def registerIfAbsent( 13 | connection: HandledConnection[F, K, M] 14 | ): F[Option[HandledConnection[F, K, M]]] = { 15 | registerRef.modify { register => 16 | val connectionKey = connection.key 17 | 18 | if (register.contains(connectionKey)) { 19 | (register, register.get(connectionKey)) 20 | } else { 21 | (register.updated(connectionKey, connection), None) 22 | } 23 | } 24 | } 25 | 26 | def isNewConnection(connectionKey: K): F[Boolean] = { 27 | registerRef.get.map(register => !register.contains(connectionKey)) 28 | } 29 | 30 | def deregisterConnection( 31 | connection: HandledConnection[F, K, M] 32 | ): F[Unit] = { 33 | registerRef.update(register => register - (connection.key)) 34 | } 35 | 36 | def getAllRegisteredConnections: F[Set[HandledConnection[F, K, M]]] = { 37 | registerRef.get.map(register => register.values.toSet) 38 | } 39 | 40 | def getConnection( 41 | connectionKey: K 42 | ): F[Option[HandledConnection[F, K, M]]] = 43 | registerRef.get.map(register => register.get(connectionKey)) 44 | 45 | def replace( 46 | connection: HandledConnection[F, K, M] 47 | ): F[Option[HandledConnection[F, K, M]]] = { 48 | registerRef.modify { register => 49 | register.updated(connection.key, connection) -> register.get( 50 | connection.key 51 | ) 52 | } 53 | } 54 | 55 | } 56 | 57 | object ConnectionsRegister { 58 | def empty[F[_]: Concurrent, K, M]: F[ConnectionsRegister[F, K, M]] = { 59 | Ref 60 | .of(Map.empty[K, HandledConnection[F, K, M]]) 61 | .map(ref => new ConnectionsRegister[F, K, M](ref)) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /metronome/networking/src/io/iohk/metronome/networking/EncryptedConnectionProvider.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.networking 2 | 3 | import io.iohk.metronome.networking.EncryptedConnectionProvider.{ 4 | ConnectionError, 5 | HandshakeFailed 6 | } 7 | 8 | import java.net.InetSocketAddress 9 | 10 | trait EncryptedConnection[F[_], K, M] { 11 | def localAddress: InetSocketAddress 12 | def remotePeerInfo: (K, InetSocketAddress) 13 | def sendMessage(m: M): F[Unit] 14 | def incomingMessage: F[Option[Either[ConnectionError, M]]] 15 | def close: F[Unit] 16 | } 17 | 18 | trait EncryptedConnectionProvider[F[_], K, M] { 19 | def localPeerInfo: (K, InetSocketAddress) 20 | def connectTo( 21 | k: K, 22 | address: InetSocketAddress 23 | ): F[EncryptedConnection[F, K, M]] 24 | def incomingConnection 25 | : F[Option[Either[HandshakeFailed, EncryptedConnection[F, K, M]]]] 26 | } 27 | 28 | object EncryptedConnectionProvider { 29 | case class HandshakeFailed(ex: Throwable, remoteAddress: InetSocketAddress) 30 | 31 | sealed trait ConnectionError 32 | case object DecodingError extends ConnectionError 33 | case class UnexpectedError(ex: Throwable) extends ConnectionError 34 | 35 | case class ConnectionAlreadyClosed(address: InetSocketAddress) 36 | extends RuntimeException 37 | 38 | } 39 | -------------------------------------------------------------------------------- /metronome/networking/src/io/iohk/metronome/networking/LocalConnectionManager.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.networking 2 | 3 | import cats.implicits._ 4 | import cats.effect.{Concurrent, Timer, Resource, ContextShift} 5 | import java.net.InetSocketAddress 6 | import monix.eval.{TaskLift, TaskLike} 7 | import monix.tail.Iterant 8 | import scodec.Codec 9 | 10 | trait LocalConnectionManager[F[_], K, M] { 11 | def isConnected: F[Boolean] 12 | def incomingMessages: Iterant[F, M] 13 | def sendMessage( 14 | message: M 15 | ): F[Either[ConnectionHandler.ConnectionAlreadyClosedException[K], Unit]] 16 | } 17 | 18 | /** Connect to a single local process and keep the connection alive. */ 19 | object LocalConnectionManager { 20 | 21 | def apply[ 22 | F[_]: Concurrent: TaskLift: TaskLike: Timer: ContextShift, 23 | K: Codec, 24 | M: Codec 25 | ]( 26 | encryptedConnectionsProvider: EncryptedConnectionProvider[F, K, M], 27 | targetKey: K, 28 | targetAddress: InetSocketAddress, 29 | retryConfig: RemoteConnectionManager.RetryConfig 30 | )(implicit 31 | tracers: NetworkTracers[F, K, M] 32 | ): Resource[F, LocalConnectionManager[F, K, M]] = { 33 | for { 34 | remoteConnectionManager <- RemoteConnectionManager[F, K, M]( 35 | encryptedConnectionsProvider, 36 | RemoteConnectionManager.ClusterConfig[K]( 37 | Set(targetKey -> targetAddress) 38 | ), 39 | retryConfig 40 | ) 41 | localConnectionManager = new LocalConnectionManager[F, K, M] { 42 | override def isConnected = 43 | remoteConnectionManager.getAcquiredConnections.map( 44 | _.contains(targetKey) 45 | ) 46 | 47 | override def incomingMessages = 48 | remoteConnectionManager.incomingMessages.map { 49 | case ConnectionHandler.MessageReceived(_, m) => m 50 | } 51 | 52 | override def sendMessage(message: M) = 53 | remoteConnectionManager.sendMessage(targetKey, message) 54 | } 55 | } yield localConnectionManager 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /metronome/networking/src/io/iohk/metronome/networking/Network.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.networking 2 | 3 | import cats.implicits._ 4 | import cats.effect.{Sync, Resource, Concurrent, ContextShift} 5 | import io.iohk.metronome.networking.ConnectionHandler.MessageReceived 6 | import monix.tail.Iterant 7 | import monix.catnap.ConcurrentQueue 8 | 9 | /** Network adapter for specializing messages. */ 10 | trait Network[F[_], K, M] { 11 | 12 | /** Receive incoming messages from the network. */ 13 | def incomingMessages: Iterant[F, MessageReceived[K, M]] 14 | 15 | /** Try sending a message to a federation member, if we are connected. */ 16 | def sendMessage(recipient: K, message: M): F[Unit] 17 | } 18 | 19 | object Network { 20 | 21 | def fromRemoteConnnectionManager[F[_]: Sync, K, M]( 22 | manager: RemoteConnectionManager[F, K, M] 23 | ): Network[F, K, M] = new Network[F, K, M] { 24 | override def incomingMessages = 25 | manager.incomingMessages 26 | 27 | override def sendMessage(recipient: K, message: M) = 28 | // Not returning an error if we are trying to send to someone no longer connected, 29 | // this should be handled transparently, delivery is best-effort. 30 | manager.sendMessage(recipient, message).void 31 | } 32 | 33 | /** Consume messges from a network and dispatch them either left or right, 34 | * based on a splitter function. Combine messages the other way. 35 | */ 36 | def splitter[F[_]: Concurrent: ContextShift, K, M, L, R]( 37 | network: Network[F, K, M] 38 | )( 39 | split: M => Either[L, R], 40 | merge: Either[L, R] => M 41 | ): Resource[F, (Network[F, K, L], Network[F, K, R])] = 42 | for { 43 | leftQueue <- makeQueue[F, K, L] 44 | rightQueue <- makeQueue[F, K, R] 45 | 46 | _ <- Concurrent[F].background { 47 | network.incomingMessages.mapEval { 48 | case MessageReceived(from, message) => 49 | split(message) match { 50 | case Left(leftMessage) => 51 | leftQueue.offer(MessageReceived(from, leftMessage)) 52 | case Right(rightMessage) => 53 | rightQueue.offer(MessageReceived(from, rightMessage)) 54 | } 55 | }.completedL 56 | } 57 | 58 | leftNetwork = new SplitNetwork[F, K, L]( 59 | leftQueue.poll, 60 | (r, m) => network.sendMessage(r, merge(Left(m))) 61 | ) 62 | 63 | rightNetwork = new SplitNetwork[F, K, R]( 64 | rightQueue.poll, 65 | (r, m) => network.sendMessage(r, merge(Right(m))) 66 | ) 67 | 68 | } yield (leftNetwork, rightNetwork) 69 | 70 | private def makeQueue[F[_]: Concurrent: ContextShift, K, M] = 71 | Resource.liftF { 72 | ConcurrentQueue.unbounded[F, MessageReceived[K, M]](None) 73 | } 74 | 75 | private class SplitNetwork[F[_]: Sync, K, M]( 76 | poll: F[MessageReceived[K, M]], 77 | send: (K, M) => F[Unit] 78 | ) extends Network[F, K, M] { 79 | override def incomingMessages: Iterant[F, MessageReceived[K, M]] = 80 | Iterant.repeatEvalF(poll) 81 | 82 | def sendMessage(recipient: K, message: M) = 83 | send(recipient, message) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /metronome/networking/src/io/iohk/metronome/networking/NetworkEvent.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.networking 2 | 3 | import java.net.InetSocketAddress 4 | 5 | /** Events we want to trace. */ 6 | sealed trait NetworkEvent[K, +M] 7 | 8 | object NetworkEvent { 9 | import ConnectionHandler.HandledConnection.HandledConnectionDirection 10 | 11 | case class Peer[K](key: K, address: InetSocketAddress) 12 | 13 | /** The connection to/from the peer has been added to the register. */ 14 | case class ConnectionRegistered[K]( 15 | peer: Peer[K], 16 | direction: HandledConnectionDirection 17 | ) extends NetworkEvent[K, Nothing] 18 | 19 | /** The connection to/from the peer has been closed and removed from the register. */ 20 | case class ConnectionDeregistered[K]( 21 | peer: Peer[K], 22 | direction: HandledConnectionDirection 23 | ) extends NetworkEvent[K, Nothing] 24 | 25 | /** We had two connections to/from the peer and discarded one of them. */ 26 | case class ConnectionDiscarded[K]( 27 | peer: Peer[K], 28 | direction: HandledConnectionDirection 29 | ) extends NetworkEvent[K, Nothing] 30 | 31 | /** Failed to establish connection to remote peer. */ 32 | case class ConnectionFailed[K]( 33 | peer: Peer[K], 34 | numberOfFailures: Int, 35 | error: Throwable 36 | ) extends NetworkEvent[K, Nothing] 37 | 38 | /** Error reading data from a connection. */ 39 | case class ConnectionReceiveError[K]( 40 | peer: Peer[K], 41 | error: EncryptedConnectionProvider.ConnectionError 42 | ) extends NetworkEvent[K, Nothing] 43 | 44 | /** Error sending data over a connection, already disconnected. */ 45 | case class ConnectionSendError[K]( 46 | peer: Peer[K] 47 | ) extends NetworkEvent[K, Nothing] 48 | 49 | /** Incoming connection from someone outside the federation. */ 50 | case class ConnectionUnknown[K](peer: Peer[K]) 51 | extends NetworkEvent[K, Nothing] 52 | 53 | /** Received incoming message from peer. */ 54 | case class MessageReceived[K, M](peer: Peer[K], message: M) 55 | extends NetworkEvent[K, M] 56 | 57 | /** Sent outgoing message to peer. */ 58 | case class MessageSent[K, M](peer: Peer[K], message: M) 59 | extends NetworkEvent[K, M] 60 | 61 | } 62 | -------------------------------------------------------------------------------- /metronome/rocksdb/src/io/iohk/metronome/rocksdb/NamespaceRegistry.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.rocksdb 2 | 3 | trait NamespaceRegistry { 4 | private var registry: Vector[RocksDBStore.Namespace] = Vector.empty 5 | 6 | protected def register(key: String): RocksDBStore.Namespace = { 7 | val ns = key.map(_.toByte) 8 | require(!registry.contains(ns)) 9 | registry = registry :+ ns 10 | ns 11 | } 12 | 13 | def all: Seq[RocksDBStore.Namespace] = registry 14 | } 15 | -------------------------------------------------------------------------------- /metronome/storage/specs/src/io/iohk/metronome/storage/KVStoreStateSpec.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.storage 2 | 3 | import org.scalatest.flatspec.AnyFlatSpec 4 | import org.scalatest.matchers.should.Matchers 5 | import scodec.codecs.implicits._ 6 | 7 | class KVStoreStateSpec extends AnyFlatSpec with Matchers { 8 | import KVStoreStateSpec._ 9 | 10 | behavior of "KVStoreState" 11 | 12 | it should "compose multiple collections" in { 13 | type Namespace = String 14 | // Two independent collections with different types of keys and values. 15 | val collA = new KVCollection[Namespace, Int, RecordA](namespace = "a") 16 | val collB = new KVCollection[Namespace, String, RecordB](namespace = "b") 17 | 18 | val program: KVStore[Namespace, Option[RecordA]] = for { 19 | _ <- collA.put(1, RecordA("one")) 20 | _ <- collB.put("two", RecordB(2)) 21 | b <- collB.get("three") 22 | _ <- collB.put("three", RecordB(3)) 23 | _ <- collB.delete("two") 24 | _ <- 25 | if (b.isEmpty) collA.put(4, RecordA("four")) 26 | else KVStore.unit[Namespace] 27 | a <- collA.read(1).lift 28 | } yield a 29 | 30 | val compiler = new KVStoreState[Namespace] 31 | 32 | val (store, maybeA) = compiler.compile(program).run(Map.empty).value 33 | 34 | maybeA shouldBe Some(RecordA("one")) 35 | store shouldBe Map( 36 | "a" -> Map(1 -> RecordA("one"), 4 -> RecordA("four")), 37 | "b" -> Map("three" -> RecordB(3)) 38 | ) 39 | } 40 | } 41 | 42 | object KVStoreStateSpec { 43 | case class RecordA(a: String) 44 | case class RecordB(b: Int) 45 | } 46 | -------------------------------------------------------------------------------- /metronome/storage/src/io/iohk/metronome/storage/InMemoryKVStore.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.storage 2 | 3 | import cats.implicits._ 4 | import cats.effect.Sync 5 | import cats.effect.concurrent.Ref 6 | 7 | /** Simple in-memory key-value store based on `KVStoreState` and `KVStoreRunner`. */ 8 | object InMemoryKVStore { 9 | def apply[F[_]: Sync, N]: F[KVStoreRunner[F, N]] = 10 | Ref.of[F, KVStoreState[N]#Store](Map.empty).map(apply(_)) 11 | 12 | def apply[F[_]: Sync, N]( 13 | storeRef: Ref[F, KVStoreState[N]#Store] 14 | ): KVStoreRunner[F, N] = 15 | new KVStoreState[N] with KVStoreRunner[F, N] { 16 | def runReadOnly[A](query: KVStoreRead[N, A]): F[A] = 17 | storeRef.get.map(compile(query).run) 18 | 19 | def runReadWrite[A](query: KVStore[N, A]): F[A] = 20 | storeRef.modify { store => 21 | compile(query).run(store).value 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /metronome/storage/src/io/iohk/metronome/storage/KVCollection.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.storage 2 | 3 | import scodec.Codec 4 | 5 | /** Storage for a specific type of data, e.g. blocks, in a given namespace. 6 | * 7 | * We should be able to string together KVStore operations across multiple 8 | * collections and execute them in one batch. 9 | */ 10 | class KVCollection[N, K: Codec, V: Codec](namespace: N) { 11 | 12 | private implicit val kvsRW = KVStore.instance[N] 13 | private implicit val kvsRO = KVStoreRead.instance[N] 14 | 15 | /** Get a value by key, if it exists, for a read-only operation. */ 16 | def read(key: K): KVStoreRead[N, Option[V]] = 17 | KVStoreRead[N].read(namespace, key) 18 | 19 | /** Put a value under a key. */ 20 | def put(key: K, value: V): KVStore[N, Unit] = 21 | KVStore[N].put(namespace, key, value) 22 | 23 | /** Get a value by key, if it exists, for potentially doing 24 | * updates based on its value, i.e. the result can be composed 25 | * with `put` and `delete`. 26 | */ 27 | def get(key: K): KVStore[N, Option[V]] = 28 | KVStore[N].get(namespace, key) 29 | 30 | /** Delete a value by key. */ 31 | def delete(key: K): KVStore[N, Unit] = 32 | KVStore[N].delete(namespace, key) 33 | 34 | /** Update a key by getting the value and applying a function on it, if the value exists. */ 35 | def update(key: K)(f: V => V): KVStore[N, Unit] = 36 | KVStore[N].update(namespace, key)(f) 37 | 38 | /** Insert, update or delete a value, depending on whether it exists. */ 39 | def alter(key: K)(f: Option[V] => Option[V]): KVStore[N, Unit] = 40 | KVStore[N].alter(namespace, key)(f) 41 | } 42 | -------------------------------------------------------------------------------- /metronome/storage/src/io/iohk/metronome/storage/KVStore.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.storage 2 | 3 | import cats.{~>} 4 | import cats.free.Free 5 | import cats.free.Free.liftF 6 | import scodec.{Encoder, Decoder, Codec} 7 | 8 | /** Helper methods to read/write a key-value store. */ 9 | object KVStore { 10 | 11 | def unit[N]: KVStore[N, Unit] = 12 | pure(()) 13 | 14 | def pure[N, A](a: A): KVStore[N, A] = 15 | Free.pure(a) 16 | 17 | def instance[N]: Ops[N] = new Ops[N] {} 18 | 19 | def apply[N: Ops] = implicitly[Ops[N]] 20 | 21 | /** Scope all operations under the `N` type, which can be more convenient, 22 | * e.g. `KVStore[String].pure(1)` instead of `KVStore.pure[String, Int](1)` 23 | */ 24 | trait Ops[N] { 25 | import KVStoreOp._ 26 | 27 | type KVNamespacedOp[A] = ({ type L[A] = KVStoreOp[N, A] })#L[A] 28 | type KVNamespacedReadOp[A] = ({ type L[A] = KVStoreReadOp[N, A] })#L[A] 29 | 30 | def unit: KVStore[N, Unit] = KVStore.unit[N] 31 | 32 | def pure[A](a: A) = KVStore.pure[N, A](a) 33 | 34 | /** Insert or replace a value under a key. */ 35 | def put[K: Encoder, V: Encoder]( 36 | namespace: N, 37 | key: K, 38 | value: V 39 | ): KVStore[N, Unit] = 40 | liftF[KVNamespacedOp, Unit]( 41 | Put[N, K, V](namespace, key, value) 42 | ) 43 | 44 | /** Get a value under a key, if it exists. */ 45 | def get[K: Encoder, V: Decoder]( 46 | namespace: N, 47 | key: K 48 | ): KVStore[N, Option[V]] = 49 | liftF[KVNamespacedOp, Option[V]]( 50 | Get[N, K, V](namespace, key) 51 | ) 52 | 53 | /** Delete a value under a key. */ 54 | def delete[K: Encoder](namespace: N, key: K): KVStore[N, Unit] = 55 | liftF[KVNamespacedOp, Unit]( 56 | Delete[N, K](namespace, key) 57 | ) 58 | 59 | /** Apply a function on a value, if it exists. */ 60 | def update[K: Encoder, V: Codec](namespace: N, key: K)( 61 | f: V => V 62 | ): KVStore[N, Unit] = 63 | alter[K, V](namespace, key)(_ map f) 64 | 65 | /** Insert, update or delete a value, depending on whether it exists. */ 66 | def alter[K: Encoder, V: Codec](namespace: N, key: K)( 67 | f: Option[V] => Option[V] 68 | ): KVStore[N, Unit] = 69 | get[K, V](namespace, key).flatMap { current => 70 | (current, f(current)) match { 71 | case ((None, None)) => unit 72 | case ((Some(existing), Some(value))) if existing == value => unit 73 | case (_, Some(value)) => put(namespace, key, value) 74 | case (Some(_), None) => delete(namespace, key) 75 | } 76 | } 77 | 78 | /** Lift a read-only operation into a read-write one, so that we can chain them together. */ 79 | def lift[A](read: KVStoreRead[N, A]): KVStore[N, A] = 80 | read.mapK(liftCompiler) 81 | 82 | private val liftCompiler: KVNamespacedReadOp ~> KVNamespacedOp = 83 | new (KVNamespacedReadOp ~> KVNamespacedOp) { 84 | def apply[A](fa: KVNamespacedReadOp[A]): KVNamespacedOp[A] = 85 | fa 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /metronome/storage/src/io/iohk/metronome/storage/KVStoreOp.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.storage 2 | 3 | import scodec.{Encoder, Decoder} 4 | 5 | /** Representing key-value storage operations as a Free Monad, 6 | * so that we can pick an execution strategy that best fits 7 | * the database technology at hand: 8 | * - execute multiple writes atomically by batching 9 | * - execute all reads and writes in a transaction 10 | * 11 | * The key-value store is expected to store binary data, 12 | * so a scodec.Codec is required for all operations to 13 | * serialize the keys and the values. 14 | * 15 | * https://typelevel.org/cats/datatypes/freemonad.html 16 | */ 17 | sealed trait KVStoreOp[N, A] 18 | sealed trait KVStoreReadOp[N, A] extends KVStoreOp[N, A] 19 | sealed trait KVStoreWriteOp[N, A] extends KVStoreOp[N, A] 20 | 21 | object KVStoreOp { 22 | case class Put[N, K, V](namespace: N, key: K, value: V)(implicit 23 | val keyEncoder: Encoder[K], 24 | val valueEncoder: Encoder[V] 25 | ) extends KVStoreWriteOp[N, Unit] 26 | 27 | case class Get[N, K, V](namespace: N, key: K)(implicit 28 | val keyEncoder: Encoder[K], 29 | val valueDecoder: Decoder[V] 30 | ) extends KVStoreReadOp[N, Option[V]] 31 | 32 | case class Delete[N, K](namespace: N, key: K)(implicit 33 | val keyEncoder: Encoder[K] 34 | ) extends KVStoreWriteOp[N, Unit] 35 | } 36 | -------------------------------------------------------------------------------- /metronome/storage/src/io/iohk/metronome/storage/KVStoreRead.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.storage 2 | 3 | import cats.free.Free 4 | import cats.free.Free.liftF 5 | import scodec.{Encoder, Decoder} 6 | 7 | /** Helper methods to compose operations that strictly only do reads, no writes. 8 | * 9 | * Basically the same as `KVStore` without `put` and `delete`. 10 | */ 11 | object KVStoreRead { 12 | 13 | def unit[N]: KVStoreRead[N, Unit] = 14 | pure(()) 15 | 16 | def pure[N, A](a: A): KVStoreRead[N, A] = 17 | Free.pure(a) 18 | 19 | def instance[N]: Ops[N] = new Ops[N] {} 20 | 21 | def apply[N: Ops] = implicitly[Ops[N]] 22 | 23 | trait Ops[N] { 24 | import KVStoreOp._ 25 | 26 | type KVNamespacedOp[A] = ({ type L[A] = KVStoreReadOp[N, A] })#L[A] 27 | 28 | def unit: KVStoreRead[N, Unit] = KVStoreRead.unit[N] 29 | 30 | def pure[A](a: A) = KVStoreRead.pure[N, A](a) 31 | 32 | def read[K: Encoder, V: Decoder]( 33 | namespace: N, 34 | key: K 35 | ): KVStoreRead[N, Option[V]] = 36 | liftF[KVNamespacedOp, Option[V]]( 37 | Get[N, K, V](namespace, key) 38 | ) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /metronome/storage/src/io/iohk/metronome/storage/KVStoreRunner.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.storage 2 | 3 | /** Convenience interface to turn KVStore queries into effects. */ 4 | trait KVStoreRunner[F[_], N] { 5 | def runReadOnly[A](query: KVStoreRead[N, A]): F[A] 6 | def runReadWrite[A](query: KVStore[N, A]): F[A] 7 | } 8 | -------------------------------------------------------------------------------- /metronome/storage/src/io/iohk/metronome/storage/KVStoreState.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome.storage 2 | 3 | import cats.{~>} 4 | import cats.data.{State, Reader} 5 | import io.iohk.metronome.storage.KVStoreOp.{Put, Get, Delete} 6 | 7 | /** A pure implementation of the Free interpreter using the State monad. 8 | * 9 | * It uses a specific namespace type, which is common to all collections. 10 | */ 11 | class KVStoreState[N] { 12 | 13 | // Ignoring the Codec for the in-memory use case. 14 | type Store = Map[N, Map[Any, Any]] 15 | // Type aliases to support the `~>` transformation with types that 16 | // only have 1 generic type argument `A`. 17 | type KVNamespacedState[A] = State[Store, A] 18 | type KVNamespacedOp[A] = ({ type L[A] = KVStoreOp[N, A] })#L[A] 19 | 20 | type KVNamespacedReader[A] = Reader[Store, A] 21 | type KVNamespacedReadOp[A] = ({ type L[A] = KVStoreReadOp[N, A] })#L[A] 22 | 23 | private val stateCompiler: KVNamespacedOp ~> KVNamespacedState = 24 | new (KVNamespacedOp ~> KVNamespacedState) { 25 | def apply[A](fa: KVNamespacedOp[A]): KVNamespacedState[A] = 26 | fa match { 27 | case Put(n, k, v) => 28 | State.modify { nkvs => 29 | val kvs = nkvs.getOrElse(n, Map.empty) 30 | nkvs.updated(n, kvs.updated(k, v)) 31 | } 32 | 33 | case Get(n, k) => 34 | State.inspect { nkvs => 35 | for { 36 | kvs <- nkvs.get(n) 37 | v <- kvs.get(k) 38 | // NOTE: This should be fine as long as we access it through 39 | // `KVCollection` which works with 1 kind of value; 40 | // otherwise we could change the effect to allow errors: 41 | // `State[Store, Either[Throwable, A]]` 42 | 43 | // The following cast would work but it's not required: 44 | // .asInstanceOf[A] 45 | } yield v 46 | } 47 | 48 | case Delete(n, k) => 49 | State.modify { nkvs => 50 | val kvs = nkvs.getOrElse(n, Map.empty) - k 51 | if (kvs.isEmpty) nkvs - n else nkvs.updated(n, kvs) 52 | } 53 | } 54 | } 55 | 56 | private val readerCompiler: KVNamespacedReadOp ~> KVNamespacedReader = 57 | new (KVNamespacedReadOp ~> KVNamespacedReader) { 58 | def apply[A](fa: KVNamespacedReadOp[A]): KVNamespacedReader[A] = 59 | fa match { 60 | case Get(n, k) => 61 | Reader { nkvs => 62 | for { 63 | kvs <- nkvs.get(n) 64 | v <- kvs.get(k) 65 | } yield v 66 | } 67 | } 68 | } 69 | 70 | /** Compile a KVStore program to a State monad, which can be executed like: 71 | * 72 | * `new KvStoreState[String].compile(program).run(Map.empty).value` 73 | */ 74 | def compile[A](program: KVStore[N, A]): KVNamespacedState[A] = 75 | program.foldMap(stateCompiler) 76 | 77 | /** Compile a KVStore program to a Reader monad, which can be executed like: 78 | * 79 | * `new KvStoreState[String].compile(program).run(Map.empty)` 80 | */ 81 | def compile[A](program: KVStoreRead[N, A]): KVNamespacedReader[A] = 82 | program.foldMap(readerCompiler) 83 | } 84 | -------------------------------------------------------------------------------- /metronome/storage/src/io/iohk/metronome/storage/package.scala: -------------------------------------------------------------------------------- 1 | package io.iohk.metronome 2 | 3 | import cats.free.Free 4 | 5 | package object storage { 6 | 7 | /** Read/Write operations over a key-value store. */ 8 | type KVStore[N, A] = Free[({ type L[A] = KVStoreOp[N, A] })#L, A] 9 | 10 | /** Read-only operations over a key-value store. */ 11 | type KVStoreRead[N, A] = Free[({ type L[A] = KVStoreReadOp[N, A] })#L, A] 12 | 13 | /** Extension method to lift a read-only operation to read-write. */ 14 | implicit class KVStoreReadOps[N, A](val read: KVStoreRead[N, A]) 15 | extends AnyVal { 16 | def lift: KVStore[N, A] = KVStore.instance[N].lift(read) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /nix/metronome.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | , lib 3 | , mill-derivation 4 | }: 5 | 6 | mill-derivation rec { 7 | pname = "metronome"; 8 | version = "0.0.1"; 9 | 10 | src = builtins.path { path = ../.; name = "source"; }; 11 | 12 | depsWarmupTarget = "'metronome[2.13.4].examples.compile'"; 13 | depsSha256 = "sha256-Utb4qPoFe0qC7TyhwdVG3Z0wBWUwJuZhFVy8xFW++RI="; 14 | 15 | millTarget = "'metronome[2.13.4].examples.assembly'"; 16 | 17 | 18 | } 19 | -------------------------------------------------------------------------------- /nix/overlay.nix: -------------------------------------------------------------------------------- 1 | final: prev: { 2 | 3 | mill = prev.mill.override { jre = prev.jdk11; }; 4 | 5 | mill-derivation = final.callPackage ./mill-derivation.nix { }; 6 | 7 | metronome = final.callPackage ./metronome.nix { }; 8 | 9 | devShell = with final; mkShell { 10 | nativeBuildInputs = [ 11 | mill 12 | ]; 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /versionFile/version: -------------------------------------------------------------------------------- 1 | 0.5.0-SNAPSHOT --------------------------------------------------------------------------------