├── .github ├── settings.yml └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .travis.yml ├── CONTRIBUTING ├── LICENSE ├── README.md ├── build.sbt ├── ci └── import_gpg.sh ├── doc └── peer │ ├── bibliography.bib │ ├── compile.sh │ ├── llncs.cls │ ├── main.aux │ ├── main.bbl │ ├── main.blg │ ├── main.out │ ├── main.pdf │ ├── main.tex │ └── main.toc ├── examples ├── README.md ├── build.sbt ├── project │ └── plugins.sbt └── src │ ├── main │ ├── resources │ │ ├── api │ │ │ └── testApi.yaml │ │ ├── reference.conf │ │ ├── scripts │ │ │ ├── ips.sh │ │ │ ├── twinschainConfigGenerator.sh │ │ │ ├── twinschainRebuild.sh │ │ │ ├── twinschainRebuildAndStart.sh │ │ │ ├── twinschainRerun.sh │ │ │ └── twinschainStatus.sh │ │ ├── settings.conf │ │ ├── settings10.conf │ │ ├── settings2.conf │ │ ├── settings3.conf │ │ ├── settings4.conf │ │ ├── settings5.conf │ │ ├── settings6.conf │ │ ├── settings7.conf │ │ ├── settings8.conf │ │ └── settings9.conf │ └── scala │ │ └── examples │ │ ├── commons │ │ ├── FileLogger.scala │ │ ├── PublicKey25519NoncedBox.scala │ │ ├── SimpleBoxTransaction.scala │ │ ├── SimpleBoxTransactionMemPool.scala │ │ └── curvepos.scala │ │ ├── hybrid │ │ ├── HLocalInterface.scala │ │ ├── HybridApp.scala │ │ ├── HybridNodeViewHolder.scala │ │ ├── README.md │ │ ├── api │ │ │ └── http │ │ │ │ ├── DebugApiRoute.scala │ │ │ │ ├── StatsApiRoute.scala │ │ │ │ └── WalletApiRoute.scala │ │ ├── blocks │ │ │ ├── HybridBlock.scala │ │ │ ├── PosBlock.scala │ │ │ └── PowBlock.scala │ │ ├── history │ │ │ ├── HistoryStorage.scala │ │ │ ├── HybridHistory.scala │ │ │ └── HybridSyncInfo.scala │ │ ├── mining │ │ │ ├── HybridMiningSettings.scala │ │ │ ├── PosForger.scala │ │ │ └── PowMiner.scala │ │ ├── simulations │ │ │ └── PrivateChain.scala │ │ ├── state │ │ │ └── HBoxStoredState.scala │ │ ├── util │ │ │ ├── Cancellable.scala │ │ │ └── FileFunctions.scala │ │ ├── validation │ │ │ ├── DifficultyBlockValidator.scala │ │ │ ├── ParentBlockValidator.scala │ │ │ └── SemanticBlockValidator.scala │ │ └── wallet │ │ │ ├── HBoxWallet.scala │ │ │ └── SimpleBoxTransactionGenerator.scala │ │ ├── spv │ │ ├── Constants.scala │ │ ├── Header.scala │ │ ├── KLS16Proof.scala │ │ ├── KMZProof.scala │ │ ├── SpvAlgos.scala │ │ └── simulation │ │ │ ├── SPVSimulator.scala │ │ │ └── SimulatorFuctions.scala │ │ └── trimchain │ │ ├── README.md │ │ ├── core │ │ ├── Algos.scala │ │ ├── Constants.scala │ │ ├── Ticket.scala │ │ └── core.scala │ │ ├── modifiers │ │ ├── BlockHeader.scala │ │ ├── TBlock.scala │ │ ├── TModifier.scala │ │ └── UtxoSnapshot.scala │ │ ├── simulation │ │ ├── InMemoryAuthenticatedUtxo.scala │ │ ├── OneMinerSimulation.scala │ │ ├── Simulators.scala │ │ ├── SpaceSavingsCalculator.scala │ │ └── ValidationSimulator.scala │ │ └── utxo │ │ └── PersistentAuthenticatedUtxo.scala │ └── test │ ├── resources │ └── settings.conf │ └── scala │ ├── commons │ └── ExamplesCommonGenerators.scala │ ├── hybrid │ ├── HistoryGenerators.scala │ ├── HybridGenerators.scala │ ├── HybridSanity.scala │ ├── HybridTypes.scala │ ├── ModifierGenerators.scala │ ├── NodeViewHolderGenerators.scala │ ├── NodeViewHolderSpec.scala │ ├── NodeViewSynchronizerGenerators.scala │ ├── NodeViewSynchronizerSpec.scala │ ├── StateGenerators.scala │ ├── StoreGenerators.scala │ ├── history │ │ ├── HybridHistorySpecification.scala │ │ └── IODBSpecification.scala │ ├── primitives │ │ └── PrivateKey25519Suite.scala │ ├── serialization │ │ └── SerializationTests.scala │ ├── state │ │ ├── HBoxStoredStateSpecification.scala │ │ └── SimpleBoxTransactionSpecification.scala │ ├── transaction │ │ └── TransactionSuite.scala │ ├── validation │ │ └── SemanticBlockValidatorSpecification.scala │ └── wallet │ │ └── HWalletSpecification.scala │ ├── spv │ ├── ChainTests.scala │ ├── SPVGenerators.scala │ └── serialization │ │ └── SerializationTests.scala │ └── trimchain │ ├── TrimchainGenerators.scala │ └── serialization │ └── SerializationTests.scala ├── project ├── build.properties └── plugins.sbt ├── release-notes.md ├── scalastyle-config.xml ├── src ├── main │ ├── resources │ │ ├── logback.xml │ │ ├── reference.conf │ │ └── swagger-ui │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── index.html │ │ │ ├── oauth2-redirect.html │ │ │ ├── swagger-ui-bundle.js │ │ │ ├── swagger-ui-bundle.js.map │ │ │ ├── swagger-ui-standalone-preset.js │ │ │ ├── swagger-ui-standalone-preset.js.map │ │ │ ├── swagger-ui.css │ │ │ ├── swagger-ui.css.map │ │ │ ├── swagger-ui.js │ │ │ └── swagger-ui.js.map │ └── scala │ │ └── scorex │ │ ├── ObjectGenerators.scala │ │ ├── core │ │ ├── ModifiersCache.scala │ │ ├── NodeViewComponent.scala │ │ ├── NodeViewHolder.scala │ │ ├── NodeViewModifier.scala │ │ ├── api │ │ │ ├── client │ │ │ │ └── ApiClient.scala │ │ │ └── http │ │ │ │ ├── ApiDirectives.scala │ │ │ │ ├── ApiError.scala │ │ │ │ ├── ApiErrorHandler.scala │ │ │ │ ├── ApiRejectionHandler.scala │ │ │ │ ├── ApiResponse.scala │ │ │ │ ├── ApiRoute.scala │ │ │ │ ├── ApiRouteWithFullView.scala │ │ │ │ ├── ApiTry.scala │ │ │ │ ├── CompositeHttpService.scala │ │ │ │ ├── CorsHandler.scala │ │ │ │ ├── NodeViewApiRoute.scala │ │ │ │ ├── PeersApiRoute.scala │ │ │ │ ├── UtilsApiRoute.scala │ │ │ │ └── swagger │ │ │ │ └── SwaggerConfigRoute.scala │ │ ├── app │ │ │ ├── Application.scala │ │ │ ├── ScorexContext.scala │ │ │ └── Version.scala │ │ ├── block │ │ │ ├── Block.scala │ │ │ └── BlockValidator.scala │ │ ├── consensus │ │ │ ├── BlockChain.scala │ │ │ ├── ConsensusSettings.scala │ │ │ ├── ContainsModifiers.scala │ │ │ ├── History.scala │ │ │ ├── HistoryReader.scala │ │ │ ├── ModifierSemanticValidity.scala │ │ │ └── SyncInfo.scala │ │ ├── core.scala │ │ ├── network │ │ │ ├── ConnectedPeer.scala │ │ │ ├── ConnectionDescription.scala │ │ │ ├── ConnectionDirection.scala │ │ │ ├── ConnectionId.scala │ │ │ ├── DeliveryTracker.scala │ │ │ ├── Handshake.scala │ │ │ ├── MaliciousBehaviorException.scala │ │ │ ├── ModifiersStatus.scala │ │ │ ├── NetworkController.scala │ │ │ ├── NodeViewSynchronizer.scala │ │ │ ├── PeerConnectionHandler.scala │ │ │ ├── PeerFeature.scala │ │ │ ├── PeerSpec.scala │ │ │ ├── PeerSynchronizer.scala │ │ │ ├── SendingStrategy.scala │ │ │ ├── SyncTracker.scala │ │ │ ├── Synchronizer.scala │ │ │ ├── UPnP.scala │ │ │ ├── message │ │ │ │ ├── BasicMessagesRepo.scala │ │ │ │ ├── Message.scala │ │ │ │ ├── MessageSerializer.scala │ │ │ │ └── MessageSpec.scala │ │ │ └── peer │ │ │ │ ├── InMemoryPeerDatabase.scala │ │ │ │ ├── LocalAddressPeerFeature.scala │ │ │ │ ├── PeerDatabase.scala │ │ │ │ ├── PeerInfo.scala │ │ │ │ ├── PeerManager.scala │ │ │ │ ├── PenaltyType.scala │ │ │ │ └── SessionIdPeerFeature.scala │ │ ├── serialization │ │ │ ├── BytesSerializable.scala │ │ │ ├── ScorexSerializer.scala │ │ │ └── SerializerRegistry.scala │ │ ├── settings │ │ │ ├── Settings.scala │ │ │ └── SettingsReaders.scala │ │ ├── transaction │ │ │ ├── BoxTransaction.scala │ │ │ ├── MemoryPool.scala │ │ │ ├── MempoolReader.scala │ │ │ ├── Transaction.scala │ │ │ ├── account │ │ │ │ └── PublicKeyNoncedBox.scala │ │ │ ├── box │ │ │ │ ├── Box.scala │ │ │ │ ├── BoxUnlocker.scala │ │ │ │ └── proposition │ │ │ │ │ ├── Proposition.scala │ │ │ │ │ └── PublicKey25519Proposition.scala │ │ │ ├── proof │ │ │ │ ├── Proof.scala │ │ │ │ └── Signature25519.scala │ │ │ ├── state │ │ │ │ ├── BoxStateChanges.scala │ │ │ │ ├── MinimalState.scala │ │ │ │ ├── SecretHolder.scala │ │ │ │ └── StateReader.scala │ │ │ └── wallet │ │ │ │ ├── BoxWallet.scala │ │ │ │ ├── Vault.scala │ │ │ │ └── VaultReader.scala │ │ ├── utils │ │ │ ├── ActorHelper.scala │ │ │ ├── BlockTypeable.scala │ │ │ ├── LocalTimeProvider.scala │ │ │ ├── NetworkTime.scala │ │ │ ├── NetworkUtils.scala │ │ │ ├── ScorexEncoder.scala │ │ │ ├── ScorexEncoding.scala │ │ │ ├── SerializationConstants.scala │ │ │ ├── TimeProvider.scala │ │ │ └── utils.scala │ │ └── validation │ │ │ ├── ModifierError.scala │ │ │ ├── ModifierValidator.scala │ │ │ ├── ValidationResult.scala │ │ │ └── ValidationSettings.scala │ │ ├── mid │ │ └── state │ │ │ └── BoxMinimalState.scala │ │ └── util │ │ └── serialization │ │ ├── VLQByteStringReader.scala │ │ └── VLQByteStringWriter.scala └── test │ └── scala │ └── scorex │ ├── core │ ├── DefaultModifiersCacheSpecification.scala │ ├── api │ │ └── http │ │ │ ├── ApiResponseTest.scala │ │ │ ├── PeersApiRouteSpec.scala │ │ │ ├── Stubs.scala │ │ │ └── UtilsApiRouteSpec.scala │ ├── network │ │ ├── HandshakeSpecification.scala │ │ └── peer │ │ │ ├── InMemoryPeerDatabaseSpec.scala │ │ │ └── PeerManagerSpec.scala │ ├── serialization │ │ └── SerializerRegistrySpec.scala │ ├── transaction │ │ └── box │ │ │ └── proposition │ │ │ └── PublicKey25519PropositionSpecification.scala │ └── validation │ │ └── ValidationSpec.scala │ ├── crypto │ └── SigningFunctionsSpecification.scala │ ├── network │ ├── DeliveryTrackerSpecification.scala │ ├── MessageSpecification.scala │ ├── NetworkControllerSpec.scala │ ├── NetworkTests.scala │ └── PeerConnectionHandlerSpecification.scala │ └── util │ ├── ScorexLoggingSpec.scala │ └── serialization │ └── VQLByteStringReaderWriterSpecification.scala └── testkit ├── README.md ├── build.sbt ├── project └── plugins.sbt └── src └── main └── scala └── scorex └── testkit ├── BlockchainPerformance.scala ├── BlockchainSanity.scala ├── SerializationTests.scala ├── TestkitHelpers.scala ├── generators ├── AllModifierProducers.scala ├── ArbitraryTransactionsCarryingModifierProducer.scala ├── CoreGenerators.scala ├── CustomModifierProducer.scala ├── SemanticallyInvalidModifierProducer.scala ├── SemanticallyValidModifierProducer.scala ├── SemanticallyValidTransactionsCarryingModifier.scala ├── SyntacticallyTargetedModifierProducer.scala └── TotallyValidModifierProducer.scala ├── properties ├── HistoryTests.scala ├── NodeViewHolderTests.scala ├── NodeViewSynchronizerTests.scala ├── WalletSecretsTest.scala ├── mempool │ ├── MemoryPoolTest.scala │ ├── MempoolFilterPerformanceTest.scala │ ├── MempoolRemovalTest.scala │ └── MempoolTransactionsTest.scala └── state │ ├── StateApplicationTest.scala │ ├── StateTests.scala │ └── box │ ├── BoxStateApplyChangesTest.scala │ ├── BoxStateChangesGenerationTest.scala │ ├── BoxStateRollbackTest.scala │ └── BoxStateTests.scala └── utils ├── AkkaFixture.scala ├── FileUtils.scala └── NoShrink.scala /.github/settings.yml: -------------------------------------------------------------------------------- 1 | # 2 | # SPDX-License-Identifier: Apache-2.0 3 | # 4 | 5 | repository: 6 | name: Scorex 7 | archived: true 8 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - main 7 | - develop 8 | pull_request: 9 | types: 10 | - opened 11 | - synchronize 12 | jobs: 13 | test: 14 | strategy: 15 | matrix: 16 | os: [ubuntu-latest] 17 | scala: [2.12.12] 18 | java: [adopt@1.8] 19 | runs-on: ${{ matrix.os }} 20 | env: 21 | HAS_SECRETS: ${{ secrets.SONATYPE_PASSWORD != '' }} 22 | steps: 23 | - name: Checkout current branch (full) 24 | uses: actions/checkout@v2 25 | with: 26 | fetch-depth: 0 27 | 28 | - name: Setup Java and Scala 29 | uses: olafurpg/setup-scala@v10 30 | with: 31 | java-version: ${{ matrix.java }} 32 | 33 | - name: Cache sbt 34 | uses: actions/cache@v2 35 | with: 36 | path: | 37 | ~/.sbt 38 | ~/.ivy2/cache 39 | ~/.coursier/cache/v1 40 | ~/.cache/coursier/v1 41 | ~/AppData/Local/Coursier/Cache/v1 42 | ~/Library/Caches/Coursier/v1 43 | key: ${{ runner.os }}-sbt-cache-v2-${{ hashFiles('**/*.sbt') }}-${{ hashFiles('project/build.properties') }} 44 | 45 | - name: Run tests 46 | run: sbt "project examples" clean coverage test && sbt test 47 | 48 | - name: Publish a snapshot 49 | if: env.HAS_SECRETS == 'true' 50 | run: sbt ++${{ matrix.scala }} publish 51 | env: 52 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 53 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 54 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish a release 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | publish_release: 9 | name: Publish release to Sonatype 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - name: Setup Java and Scala 15 | uses: olafurpg/setup-scala@v10 16 | with: 17 | java-version: adopt@1.8 18 | 19 | - name: Cache sbt 20 | uses: actions/cache@v2 21 | with: 22 | path: | 23 | ~/.sbt 24 | ~/.ivy2/cache 25 | ~/.coursier/cache/v1 26 | ~/.cache/coursier/v1 27 | ~/AppData/Local/Coursier/Cache/v1 28 | ~/Library/Caches/Coursier/v1 29 | key: ${{ runner.os }}-sbt-cache-v2-${{ hashFiles('**/*.sbt') }}-${{ hashFiles('project/build.properties') }} 30 | 31 | - name: Import GPG key 32 | run: ci/import_gpg.sh 33 | env: 34 | GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }} 35 | 36 | - name: Publish release 37 | run: sbt +publishSigned sonatypeBundleRelease 38 | env: 39 | PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} 40 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 41 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE/Editor files 2 | .idea 3 | .ensime 4 | .ensime_cache/ 5 | scorex.yaml 6 | 7 | # Eclipse/Scala IDE files 8 | .classpath 9 | .project 10 | .settings 11 | .cache-main 12 | .cache-tests 13 | /bin/ 14 | 15 | # SBT's temporary files 16 | .history 17 | 18 | # scala build folders 19 | target 20 | 21 | # dotfiles 22 | .dockerignore 23 | .editorconfig 24 | 25 | # standalone docker 26 | Dockerfile 27 | 28 | # logs 29 | *.log 30 | 31 | # db files 32 | 33 | *.data 34 | 35 | 36 | # for temporary experiments 37 | sheet.scala 38 | examples/src/main/scala/examples/sigmastate/ 39 | examples/lib/ 40 | -------------------------------------------------------------------------------- /ci/import_gpg.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # setting up gpg2 for reading passphrase from parameters 3 | # via https://github.com/beautiful-scala/scalastyle/blob/master/.github/workflows/release.yml#L16 4 | # from https://github.com/olafurpg/sbt-ci-release/issues/95 5 | 6 | # setup gpg 7 | mkdir ~/.gnupg && chmod 700 ~/.gnupg 8 | echo use-agent >> ~/.gnupg/gpg.conf 9 | echo pinentry-mode loopback >> ~/.gnupg/gpg.conf 10 | echo allow-loopback-pinentry >> ~/.gnupg/gpg-agent.conf 11 | chmod 600 ~/.gnupg/* 12 | echo RELOADAGENT | gpg-connect-agent 13 | 14 | # decode key 15 | # private key should be previously exported with: 16 | # gpg --export-secret-keys [id] | base64 | pbcopy 17 | # and stored as github repository secret under the following name (see env var name below) 18 | printf "$GPG_SIGNING_KEY" | base64 --decode > ~/.gnupg/private.key 19 | 20 | # import key 21 | gpg --no-tty --batch --yes --import ~/.gnupg/private.key 22 | -------------------------------------------------------------------------------- /doc/peer/compile.sh: -------------------------------------------------------------------------------- 1 | rm main.pdf 2 | pdflatex main 3 | bibtex main 4 | pdflatex main 5 | pdflatex main 6 | rm main.aux 7 | rm main.log 8 | rm main.blg 9 | rm main.bbl 10 | rm main.out 11 | -------------------------------------------------------------------------------- /doc/peer/main.bbl: -------------------------------------------------------------------------------- 1 | \begin{thebibliography}{} 2 | 3 | \end{thebibliography} 4 | -------------------------------------------------------------------------------- /doc/peer/main.blg: -------------------------------------------------------------------------------- 1 | This is BibTeX, Version 0.99d (TeX Live 2016) 2 | Capacity: max_strings=35307, hash_size=35307, hash_prime=30011 3 | The top-level auxiliary file: main.aux 4 | The style file: alpha.bst 5 | I found no \citation commands---while reading file main.aux 6 | Database file #1: bibliography.bib 7 | You've used 0 entries, 8 | 2543 wiz_defined-function locations, 9 | 558 strings with 4439 characters, 10 | and the built_in function-call counts, 24 in all, are: 11 | = -- 0 12 | > -- 0 13 | < -- 0 14 | + -- 0 15 | - -- 0 16 | * -- 2 17 | := -- 10 18 | add.period$ -- 0 19 | call.type$ -- 0 20 | change.case$ -- 0 21 | chr.to.int$ -- 0 22 | cite$ -- 0 23 | duplicate$ -- 0 24 | empty$ -- 1 25 | format.name$ -- 0 26 | if$ -- 2 27 | int.to.chr$ -- 1 28 | int.to.str$ -- 0 29 | missing$ -- 0 30 | newline$ -- 3 31 | num.names$ -- 0 32 | pop$ -- 0 33 | preamble$ -- 1 34 | purify$ -- 0 35 | quote$ -- 0 36 | skip$ -- 2 37 | stack$ -- 0 38 | substring$ -- 0 39 | swap$ -- 0 40 | text.length$ -- 0 41 | text.prefix$ -- 0 42 | top$ -- 0 43 | type$ -- 0 44 | warning$ -- 0 45 | while$ -- 0 46 | width$ -- 0 47 | write$ -- 2 48 | (There was 1 error message) 49 | -------------------------------------------------------------------------------- /doc/peer/main.out: -------------------------------------------------------------------------------- 1 | \BOOKMARK [0][-]{chapter.1}{A Flexible Peer Management Architecture \040for Blockchain Systems}{}% 1 2 | -------------------------------------------------------------------------------- /doc/peer/main.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperledger-labs/Scorex/9e1cd58a5589a9062dd9974f1884cc640c1d51b3/doc/peer/main.pdf -------------------------------------------------------------------------------- /doc/peer/main.toc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperledger-labs/Scorex/9e1cd58a5589a9062dd9974f1884cc640c1d51b3/doc/peer/main.toc -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Scorex Example: Twinscoin 2 | 3 | 4 | Currently, Scorex's distribution contains one example of blockchain system built on top of Scorex: **TwinsCoin**. 5 | 6 | Twinscoin is a hybrid Proof-of-Work(PoW) and Proof-of-Stake(PoS) cryptocurrency with 7 | 1:1 correspondence between PoW and PoS blocks. The main goal is to protect the hybrid 8 | chain against majority of hashing power being adversarial. 9 | 10 | The main characteristic of the implementation are: 11 | 12 | * The use of the [Twinscoin consensus protocol](https://eprint.iacr.org/2017/232.pdf); 13 | 14 | * A Bitcoin-like transaction model with a transaction having multiple inputs and outputs (with no [scripts](https://en.bitcoin.it/wiki/Script), however); 15 | 16 | * Persistence through the [IODB](https://github.com/input-output-hk/iodb) versioned key-value database. 17 | 18 | 19 | ### Execution 20 | 21 | To run, you need to have Java8 and [SBT (Scala Build Tool)](http://www.scala-sbt.org) installed. 22 | 23 | In the terminal, run `sbt`, and then type the following commands in the SBT shell: 24 | 25 | 1. `> project examples` 26 | 2. `> runMain examples.hybrid.HybridApp src/main/resources/settings.conf` 27 | 28 | This will create and run a node with the given settings. To create and run more nodes, repeat the steps above using the other setting files (e.g. `settings2.conf`, `settings3.conf`, ...) in the `src/main/resources/` folder. 29 | 30 | 31 | _Note to MacOS users:_ The 127.0.0.X loopback addresses (for X > 1) are disabled by default in MacOS. Therefore, the creation of nodes with `settingsX.conf` (for X > 1) fails. To prevent the failure, [enable the addresses](https://superuser.com/questions/458875/how-do-you-get-loopback-addresses-other-than-127-0-0-1-to-work-on-os-x?noredirect=1&lq=1). 32 | 33 | 34 | ### Web User Interface 35 | 36 | You can interact with the nodes you created through a web browser. For the node created with `settings.conf`, visit `localhost:9085`. 37 | 38 | TODO: Add instructions about how to perform a transaction through `wallet/transfer`. 39 | 40 | -------------------------------------------------------------------------------- /examples/build.sbt: -------------------------------------------------------------------------------- 1 | name := "scorex-examples" 2 | 3 | libraryDependencies ++= Seq( 4 | "org.scalactic" %% "scalactic" % "3.0.1" % "test", 5 | "org.scalatest" %% "scalatest" % "3.1.1" % "test", 6 | "org.scalacheck" %% "scalacheck" % "1.14.+" % "test", 7 | "org.scalatestplus" %% "scalatestplus-scalacheck" % "3.1.0.0-RC2" % Test, 8 | "org.scorexfoundation" %% "iodb" % "0.3.2", 9 | "com.typesafe.akka" %% "akka-testkit" % "2.6.10" % "test" 10 | ) 11 | 12 | mainClass in assembly := Some("examples.hybrid.HybridApp") 13 | 14 | assemblyJarName in assembly := "twinsChain.jar" 15 | 16 | parallelExecution in Test := true 17 | 18 | testForkedParallel in Test := true 19 | 20 | test in assembly := {} 21 | 22 | coverageExcludedPackages := "examples\\.hybrid\\.api\\.http.*" 23 | -------------------------------------------------------------------------------- /examples/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.5.1") -------------------------------------------------------------------------------- /examples/src/main/resources/reference.conf: -------------------------------------------------------------------------------- 1 | app { 2 | modifierIdSize = 32 3 | } -------------------------------------------------------------------------------- /examples/src/main/resources/scripts/ips.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | #seed="52.40.210.252" 4 | #ips=("52.88.43.127" "35.167.200.197" "35.163.27.226" "35.167.74.210" 5 | #"54.70.0.50" "52.89.211.42" "35.167.43.13" "35.167.184.105" "35.165.178.93" 6 | #"54.191.32.214" "52.26.217.230" "52.24.190.196" "34.209.224.39" "52.25.11.49" 7 | #"52.41.61.115" "35.163.92.164" "35.161.164.5" "52.40.158.125" "52.24.217.224") 8 | 9 | seed="34.210.250.171" 10 | ips=("52.41.112.126" "34.211.124.152" "52.11.218.194" "34.212.72.35" "34.208.192.219" "52.41.191.24" "35.160.179.146" 11 | "34.211.207.229" "52.43.112.221" "34.209.49.46" "35.166.124.70") 12 | 13 | allIps=("${ips[@]}") 14 | allIps+=("$seed") -------------------------------------------------------------------------------- /examples/src/main/resources/scripts/twinschainConfigGenerator.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | my_dir="$(dirname "$0")" 4 | source "$my_dir/ips.sh" 5 | 6 | echo "Arguments: number of configs, data folder" 7 | limit=$(($1+21)) 8 | for i in $(seq 21 $limit); 9 | do 10 | folder="$2/data-$i" 11 | port=`expr 19000 + $i` 12 | rpcPort=`expr 29000 + $i` 13 | original_string="genesis$i" 14 | seed="${original_string/0/o}" 15 | peers="\"52.40.210.252:9084\", \"52.88.43.127:9084\", \"35.167.200.197:9084\", \"35.163.27.226:9084\", \"35.167.74.210:9084\", \"54.70.0.50:9084\", \"52.89.211.42:9084\", \"35.167.43.13:9084\", \"35.167.184.105:9084\", \"35.165.178.93:9084\", \"54.191.32.214:9084\", \"52.26.217.230:9084\", \"52.24.190.196:9084\", \"34.209.224.39:9084\", \"52.25.11.49:9084\", \"52.41.61.115:9084\", \"35.163.92.164:9084\", \"35.161.164.5:9084\", \"52.40.158.125:9084\", \"52.24.217.224:9084\"" 16 | conf="{\n \"p2p\": {\n \"name\": \"generatedNode$i\",\n \"bindAddress\": \"0.0.0.0\",\n \"upnp\": false,\n \"upnpGatewayTimeout\": 7000,\n \"upnpDiscoverTimeout\": 3000,\n \"port\": $port,\n \"knownPeers\": [$peers],\n \"addedMaxDelay\": 0,\n \"maxConnections\": 100\n },\n \"RparamX10\": 8,\n \"targetBlockDelayMillis\": 60000,\n \"walletDir\": \"$folder/wallet\",\n \"walletPassword\": \"cookies\",\n \"walletSeed\": \"$seed\",\n \"dataDir\": \"$folder/data\",\n \"logDir\": \"$folder/logs\",\n \"rpcPort\": $rpcPort,\n \"rpcAllowed\": [],\n \"agent\": \"generatedNode$i\",\n \"version\": [0, 0, 1],\n \"maxRollback\": 100,\n \"testnet\": true,\n \"offlineGeneration\": false,\n \"blockGenerationDelay\": 100,\n \"cors\": false\n}" 17 | printf "$conf" >> "settings-$i.json" 18 | done 19 | -------------------------------------------------------------------------------- /examples/src/main/resources/scripts/twinschainRebuild.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | my_dir="$(dirname "$0")" 4 | source "$my_dir/ips.sh" 5 | 6 | for ip in "${ips[@]}" 7 | do 8 | echo "$ip" 9 | ssh ubuntu@$ip rm -rf /home/ubuntu/Scorex/examples/target/scala-2.12/twinsChain.jar 10 | ssh ubuntu@$ip rm -rf /home/ubuntu/data/twinsChain.jar 11 | ssh ubuntu@$ip "cd /home/ubuntu/Scorex && git pull origin master && sbt publishLocal && sbt \"project examples\" \"assembly\" && cp /home/ubuntu/Scorex/examples/target/scala-2.12/twinsChain.jar /home/ubuntu/data/twinsChain.jar" 12 | done 13 | -------------------------------------------------------------------------------- /examples/src/main/resources/scripts/twinschainRebuildAndStart.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | my_dir="$(dirname "$0")" 4 | source "$my_dir/ips.sh" 5 | 6 | for ip in "${ips[@]}" 7 | do 8 | echo "$ip" 9 | ssh ubuntu@$ip "pkill -f twinsChain" 10 | ssh ubuntu@$ip "rm -f /tmp/l.log && rm -rf /home/ubuntu/data/data && rm -f /home/ubuntu/data/l.log && rm -rf /home/ubuntu/Scorex/examples/target/scala-2.12/twinsChain.jar && rm -rf /home/ubuntu/data/twinsChain.jar" 11 | 12 | ssh ubuntu@$ip "cd /home/ubuntu/Scorex && git pull origin master && sbt publishLocal && sbt \"project examples\" \"assembly\" && cp /home/ubuntu/Scorex/examples/target/scala-2.12/twinsChain.jar /home/ubuntu/data/twinsChain.jar" 13 | 14 | ssh ubuntu@$ip "nohup java -jar -XX:-UseGCOverheadLimit /home/ubuntu/data/twinsChain.jar /home/ubuntu/data/settings.json > /tmp/l.log 2>&1 &" 15 | sleep 30 16 | done 17 | -------------------------------------------------------------------------------- /examples/src/main/resources/scripts/twinschainRerun.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | my_dir="$(dirname "$0")" 4 | source "$my_dir/ips.sh" 5 | 6 | for ip in "${ips[@]}" 7 | do 8 | echo "ubuntu@$ip" 9 | ssh ubuntu@$ip "pkill -f twinsChain" 10 | ssh ubuntu@$ip "rm -f /tmp/l.log && rm -rf /home/ubuntu/data/data && rm -f /home/ubuntu/data/l.log" 11 | 12 | # ssh ubuntu@$ip "sed -i '/52.40.210.252:9084/c\ \"34.210.250.171:9084\"' /home/ubuntu/data/settings.json" 13 | ssh ubuntu@$ip "sed -i '/34.210.250.171:9084/c\ \"34.210.250.171:9084\"' /home/ubuntu/data/settings.json" 14 | # ssh ubuntu@$ip "sed -i '/\"node1\"/c\ \"name\": \"genesisNode\",' /home/ubuntu/data/settings.json" 15 | # ssh ubuntu@$ip "sed -i '/\"RparamX10\"/c\ \"RparamX10\": 2,' /home/ubuntu/data/settings.json" 16 | # ssh ubuntu@$ip "sed -i '/\"addedMaxDelay\"/c\ \"addedMaxDelay\": 0,' /home/ubuntu/data/settings.json" 17 | # ssh ubuntu@$ip "sed -i '/\"targetBlockDelayMillis\"/c\ \"targetBlockDelayMillis\": 60000,' /home/ubuntu/data/settings.json" 18 | 19 | ssh ubuntu@$ip "nohup java -jar -XX:-UseGCOverheadLimit /home/ubuntu/data/twinsChain.jar /home/ubuntu/data/settings.json > /tmp/l.log 2>&1 &" 20 | # sleep 30 21 | curl -s -X GET --header 'Accept: application/json' 'http://'$ip':9085/debug/info' | grep height 22 | done 23 | 24 | 25 | -------------------------------------------------------------------------------- /examples/src/main/resources/scripts/twinschainStatus.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | my_dir="$(dirname "$0")" 4 | source "$my_dir/ips.sh" 5 | 6 | for ip in "${allIps[@]}" 7 | do 8 | echo "$ip" 9 | curl -s -X GET --header 'Accept: application/json' 'http://'$ip':9085/debug/info' | grep height 10 | done -------------------------------------------------------------------------------- /examples/src/main/resources/settings.conf: -------------------------------------------------------------------------------- 1 | scorex { 2 | dataDir = /tmp/scorex/data/blockchain 3 | logDir = /tmp/scorex/data/log 4 | 5 | restApi { 6 | bindAddress = "127.0.0.1:9085" 7 | api-key-hash = "" 8 | } 9 | 10 | network { 11 | nodeName = "generatorNode1" 12 | bindAddress = "127.0.0.1:9084" 13 | knownPeers = [] 14 | agentName = "2-Hop" 15 | } 16 | 17 | 18 | 19 | miner { 20 | offlineGeneration = true 21 | targetBlockDelay = 5s 22 | blockGenerationDelay = 100ms 23 | rParamX10 = 8 24 | initialDifficulty = 1 25 | posAttachmentSize = 100 26 | } 27 | 28 | wallet { 29 | seed = "minerNode1" 30 | password = "cookies" 31 | walletDir = "/tmp/scorex/data/wallet" 32 | } 33 | } -------------------------------------------------------------------------------- /examples/src/main/resources/settings10.conf: -------------------------------------------------------------------------------- 1 | scorex { 2 | dataDir = /tmp/scorex/data10/blockchain 3 | logDir = /tmp/scorex/data10/log 4 | 5 | restApi { 6 | bindAddress = "127.0.0.10:9495" 7 | api-key-hash = "" 8 | } 9 | 10 | network { 11 | nodeName = "node10" 12 | bindAddress = "127.0.0.10:9494" 13 | knownPeers = ["127.0.0.1:9084","127.0.0.9:9303"] 14 | agentName = "2-Hop" 15 | addedMaxDelay = 2s 16 | } 17 | 18 | miner { 19 | offlineGeneration = false 20 | targetBlockDelay = 100s 21 | blockGenerationDelay = 100ms 22 | rParamX10 = 8 23 | initialDifficulty = 1 24 | posAttachmentSize = 100 25 | } 26 | 27 | wallet { 28 | seed = "node10seed" 29 | password = "cookies10" 30 | walletDir = "/tmp/scorex/data10/wallet" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/src/main/resources/settings2.conf: -------------------------------------------------------------------------------- 1 | scorex { 2 | dataDir = /tmp/scorex/data2/blockchain 3 | logDir = /tmp/scorex/data2/log 4 | 5 | restApi { 6 | bindAddress = "127.0.0.2:9089" 7 | api-key-hash = "" 8 | } 9 | 10 | network { 11 | nodeName = "node2" 12 | bindAddress = "127.0.0.2:9088" 13 | knownPeers = ["127.0.0.1:9084"] 14 | agentName = "2-Hop" 15 | } 16 | 17 | miner { 18 | offlineGeneration = false 19 | targetBlockDelay = 5s 20 | blockGenerationDelay = 100ms 21 | rParamX10 = 8 22 | initialDifficulty = 1 23 | posAttachmentSize = 100 24 | } 25 | 26 | wallet { 27 | seed = "minerNode2" 28 | password = "cookies2" 29 | walletDir = "/tmp/scorex/data2/wallet" 30 | } 31 | } -------------------------------------------------------------------------------- /examples/src/main/resources/settings3.conf: -------------------------------------------------------------------------------- 1 | scorex { 2 | dataDir = /tmp/scorex/data3/blockchain 3 | logDir = /tmp/scorex/data3/log 4 | 5 | restApi { 6 | bindAddress = "127.0.0.3:9093" 7 | api-key-hash = "" 8 | } 9 | 10 | network { 11 | nodeName = "node3" 12 | bindAddress = "127.0.0.3:9092" 13 | knownPeers = ["127.0.0.1:9084"] 14 | agentName = "2-Hop" 15 | } 16 | 17 | miner { 18 | offlineGeneration = false 19 | targetBlockDelay = 100s 20 | blockGenerationDelay = 100ms 21 | rParamX10 = 8 22 | initialDifficulty = 1 23 | posAttachmentSize = 100 24 | } 25 | 26 | wallet { 27 | seed = "minerNode3" 28 | password = "cookies3" 29 | walletDir = "/tmp/scorex/data3/wallet" 30 | } 31 | } -------------------------------------------------------------------------------- /examples/src/main/resources/settings4.conf: -------------------------------------------------------------------------------- 1 | scorex { 2 | dataDir = /tmp/scorex/data4/blockchain 3 | logDir = /tmp/scorex/data4/log 4 | 5 | restApi { 6 | bindAddress = "127.0.0.4:9095" 7 | api-key-hash = "" 8 | } 9 | 10 | network { 11 | nodeName = "node4" 12 | bindAddress = "127.0.0.3:9094" 13 | knownPeers = ["127.0.0.1:9084", "127.0.0.3:9092"] 14 | agentName = "2-Hop" 15 | } 16 | 17 | miner { 18 | offlineGeneration = false 19 | targetBlockDelay = 100s 20 | blockGenerationDelay = 100ms 21 | rParamX10 = 8 22 | initialDifficulty = 1 23 | posAttachmentSize = 100 24 | } 25 | 26 | wallet { 27 | seed = "node4seed" 28 | password = "cookies4" 29 | walletDir = "/tmp/scorex/data4/wallet" 30 | } 31 | } -------------------------------------------------------------------------------- /examples/src/main/resources/settings5.conf: -------------------------------------------------------------------------------- 1 | scorex { 2 | dataDir = /tmp/scorex/data5/blockchain 3 | logDir = /tmp/scorex/data5/log 4 | 5 | restApi { 6 | bindAddress = "127.0.0.5:9097" 7 | api-key-hash = "" 8 | } 9 | 10 | network { 11 | nodeName = "node5" 12 | bindAddress = "127.0.0.3:9096" 13 | knownPeers = ["127.0.0.1:9084", "127.0.0.3:9092"] 14 | agentName = "2-Hop" 15 | } 16 | 17 | miner { 18 | offlineGeneration = false 19 | targetBlockDelay = 100s 20 | blockGenerationDelay = 100ms 21 | rParamX10 = 8 22 | initialDifficulty = 1 23 | posAttachmentSize = 100 24 | } 25 | 26 | wallet { 27 | seed = "node5seed" 28 | password = "cookies5" 29 | walletDir = "/tmp/scorex/data5/wallet" 30 | } 31 | } -------------------------------------------------------------------------------- /examples/src/main/resources/settings6.conf: -------------------------------------------------------------------------------- 1 | scorex { 2 | dataDir = /tmp/scorex/data6/blockchain 3 | logDir = /tmp/scorex/data6/log 4 | 5 | restApi { 6 | bindAddress = "127.0.0.6:9099" 7 | api-key-hash = "" 8 | } 9 | 10 | network { 11 | nodeName = "node6" 12 | bindAddress = "127.0.0.6:9098" 13 | knownPeers = ["127.0.0.1:9084", "127.0.0.4:9094"] 14 | agentName = "2-Hop" 15 | addedMaxDelay = 5s 16 | } 17 | 18 | miner { 19 | offlineGeneration = false 20 | targetBlockDelay = 100s 21 | blockGenerationDelay = 0ms 22 | rParamX10 = 8 23 | initialDifficulty = 1 24 | posAttachmentSize = 100 25 | } 26 | 27 | wallet { 28 | seed = "node6seed" 29 | password = "cookies6" 30 | walletDir = "/tmp/scorex/data6/wallet" 31 | } 32 | } -------------------------------------------------------------------------------- /examples/src/main/resources/settings7.conf: -------------------------------------------------------------------------------- 1 | scorex { 2 | dataDir = /tmp/scorex/data7/blockchain 3 | logDir = /tmp/scorex/data7/log 4 | 5 | restApi { 6 | bindAddress = "127.0.0.7:9101" 7 | api-key-hash = "" 8 | } 9 | 10 | network { 11 | nodeName = "node7" 12 | bindAddress = "127.0.0.7:9100" 13 | knownPeers = ["127.0.0.1:9084"] 14 | agentName = "2-Hop" 15 | addedMaxDelay = 3s 16 | } 17 | 18 | miner { 19 | offlineGeneration = false 20 | targetBlockDelay = 100s 21 | blockGenerationDelay = 100ms 22 | rParamX10 = 8 23 | initialDifficulty = 1 24 | posAttachmentSize = 100 25 | } 26 | 27 | wallet { 28 | seed = "node7seed" 29 | password = "cookies7" 30 | walletDir = "/tmp/scorex/data7/wallet" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/src/main/resources/settings8.conf: -------------------------------------------------------------------------------- 1 | scorex { 2 | dataDir = /tmp/scorex/data7/blockchain 3 | logDir = /tmp/scorex/data7/log 4 | 5 | restApi { 6 | bindAddress = "127.0.0.8:9200" 7 | api-key-hash = "" 8 | } 9 | 10 | network { 11 | nodeName = "node8" 12 | bindAddress = "127.0.0.8:9201" 13 | knownPeers = ["127.0.0.1:9084"] 14 | agentName = "2-Hop" 15 | } 16 | 17 | miner { 18 | offlineGeneration = false 19 | targetBlockDelay = 100s 20 | blockGenerationDelay = 100ms 21 | rParamX10 = 8 22 | initialDifficulty = 1 23 | posAttachmentSize = 100 24 | } 25 | 26 | wallet { 27 | seed = "node8seed" 28 | password = "cookies8" 29 | walletDir = "/tmp/scorex/data8/wallet" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/src/main/resources/settings9.conf: -------------------------------------------------------------------------------- 1 | scorex { 2 | dataDir = /tmp/scorex/data7/blockchain 3 | logDir = /tmp/scorex/data7/log 4 | 5 | restApi { 6 | bindAddress = "127.0.0.9:9304" 7 | api-key-hash = "" 8 | } 9 | 10 | network { 11 | nodeName = "node9" 12 | bindAddress = "127.0.0.9:9303" 13 | knownPeers = ["127.0.0.1:9084","127.0.0.7:9100"] 14 | agentName = "2-Hop" 15 | } 16 | 17 | miner { 18 | offlineGeneration = false 19 | targetBlockDelay = 100s 20 | blockGenerationDelay = 100ms 21 | rParamX10 = 8 22 | initialDifficulty = 1 23 | posAttachmentSize = 100 24 | } 25 | 26 | wallet { 27 | seed = "node9seed" 28 | password = "cookies9" 29 | walletDir = "/tmp/scorex/data9/wallet" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/src/main/scala/examples/commons/FileLogger.scala: -------------------------------------------------------------------------------- 1 | package examples.commons 2 | 3 | import java.nio.file.{Files, Path, Paths, StandardOpenOption} 4 | 5 | class FileLogger(filePath: String) { 6 | 7 | val path: Path = Paths.get(filePath) 8 | private val f = path.toFile 9 | f.getParentFile().mkdirs() 10 | f.createNewFile() 11 | 12 | def appendString(string: String): Unit = { 13 | Files.write(path, (string + "\n").getBytes(), StandardOpenOption.APPEND); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/src/main/scala/examples/commons/PublicKey25519NoncedBox.scala: -------------------------------------------------------------------------------- 1 | package examples.commons 2 | 3 | import io.circe.Encoder 4 | import io.circe.syntax._ 5 | import scorex.core.serialization.ScorexSerializer 6 | import scorex.core.transaction.account.PublicKeyNoncedBox 7 | import scorex.core.transaction.box.proposition.{PublicKey25519Proposition, PublicKey25519PropositionSerializer} 8 | import scorex.core.utils.ScorexEncoding 9 | import scorex.util.encode.Base16 10 | import scorex.crypto.hash.Blake2b256 11 | import scorex.crypto.signatures.Curve25519 12 | import scorex.util.serialization.{Reader, Writer} 13 | 14 | case class PublicKey25519NoncedBox(override val proposition: PublicKey25519Proposition, 15 | override val nonce: Nonce, 16 | override val value: Value) extends PublicKeyNoncedBox[PublicKey25519Proposition] { 17 | 18 | override type M = PublicKey25519NoncedBox 19 | 20 | override def serializer: ScorexSerializer[PublicKey25519NoncedBox] = PublicKey25519NoncedBoxSerializer 21 | 22 | override def toString: String = 23 | s"PublicKey25519NoncedBox(id: ${Base16.encode(id)}, proposition: $proposition, nonce: $nonce, value: $value)" 24 | } 25 | 26 | object PublicKey25519NoncedBox extends ScorexEncoding { 27 | val BoxKeyLength: Int = Blake2b256.DigestSize 28 | val BoxLength: Int = Curve25519.KeyLength + 2 * 8 29 | 30 | implicit val publicKey25519NoncedBoxEncoder: Encoder[PublicKey25519NoncedBox] = (pknb: PublicKey25519NoncedBox) => 31 | Map( 32 | "id" -> encoder.encode(pknb.id).asJson, 33 | "address" -> pknb.proposition.address.asJson, 34 | "publicKey" -> encoder.encode(pknb.proposition.pubKeyBytes).asJson, 35 | "nonce" -> pknb.nonce.toLong.asJson, 36 | "value" -> pknb.value.toLong.asJson 37 | ).asJson 38 | } 39 | 40 | object PublicKey25519NoncedBoxSerializer extends ScorexSerializer[PublicKey25519NoncedBox] { 41 | 42 | 43 | override def serialize(obj: PublicKey25519NoncedBox, w: Writer): Unit = { 44 | PublicKey25519PropositionSerializer.serialize(obj.proposition, w) 45 | w.putLong(obj.nonce) 46 | w.putULong(obj.value) 47 | } 48 | 49 | override def parse(r: Reader): PublicKey25519NoncedBox = { 50 | PublicKey25519NoncedBox( 51 | PublicKey25519PropositionSerializer.parse(r), 52 | Nonce @@ r.getLong(), 53 | Value @@ r.getULong() 54 | ) 55 | } 56 | } 57 | 58 | -------------------------------------------------------------------------------- /examples/src/main/scala/examples/commons/SimpleBoxTransactionMemPool.scala: -------------------------------------------------------------------------------- 1 | package examples.commons 2 | 3 | import scorex.core.transaction.MemoryPool 4 | import scorex.util.ModifierId 5 | 6 | import scala.collection.concurrent.TrieMap 7 | import scala.util.{Success, Try} 8 | 9 | 10 | case class SimpleBoxTransactionMemPool(unconfirmed: TrieMap[ModifierId, SimpleBoxTransaction]) 11 | extends MemoryPool[SimpleBoxTransaction, SimpleBoxTransactionMemPool] { 12 | override type NVCT = SimpleBoxTransactionMemPool 13 | 14 | //getters 15 | override def modifierById(id: ModifierId): Option[SimpleBoxTransaction] = unconfirmed.get(id) 16 | 17 | override def contains(id: ModifierId): Boolean = unconfirmed.contains(id) 18 | 19 | override def getAll(ids: Seq[ModifierId]): Seq[SimpleBoxTransaction] = ids.flatMap(getById) 20 | 21 | //modifiers 22 | override def put(tx: SimpleBoxTransaction): Try[SimpleBoxTransactionMemPool] = Success { 23 | unconfirmed.put(tx.id, tx) 24 | this 25 | } 26 | 27 | //todo 28 | override def put(txs: Iterable[SimpleBoxTransaction]): Try[SimpleBoxTransactionMemPool] = Success(putWithoutCheck(txs)) 29 | 30 | override def putWithoutCheck(txs: Iterable[SimpleBoxTransaction]): SimpleBoxTransactionMemPool = { 31 | txs.foreach(tx => unconfirmed.put(tx.id, tx)) 32 | this 33 | } 34 | 35 | override def remove(tx: SimpleBoxTransaction): SimpleBoxTransactionMemPool = { 36 | unconfirmed.remove(tx.id) 37 | this 38 | } 39 | 40 | override def take(limit: Int): Iterable[SimpleBoxTransaction] = 41 | unconfirmed.values.toSeq.sortBy(-_.fee).take(limit) 42 | 43 | override def filter(condition: SimpleBoxTransaction => Boolean): SimpleBoxTransactionMemPool = { 44 | unconfirmed.retain { (_, v) => 45 | condition(v) 46 | } 47 | this 48 | } 49 | 50 | override def size: Int = unconfirmed.size 51 | } 52 | 53 | 54 | object SimpleBoxTransactionMemPool { 55 | lazy val emptyPool: SimpleBoxTransactionMemPool = SimpleBoxTransactionMemPool(TrieMap()) 56 | } 57 | -------------------------------------------------------------------------------- /examples/src/main/scala/examples/commons/curvepos.scala: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import io.iohk.iodb.ByteArrayWrapper 4 | import scorex.core.{VersionTag, versionToBytes} 5 | import scorex.util.{ModifierId, idToBytes} 6 | import supertagged.TaggedType 7 | 8 | package object commons { 9 | 10 | object Value extends TaggedType[Long] 11 | object Nonce extends TaggedType[Long] 12 | 13 | type Value = Value.Type 14 | type Nonce = Nonce.Type 15 | 16 | def idToBAW(id: ModifierId): ByteArrayWrapper = ByteArrayWrapper(idToBytes(id)) 17 | 18 | def versionToBAW(id: VersionTag): ByteArrayWrapper = ByteArrayWrapper(versionToBytes(id)) 19 | 20 | } 21 | -------------------------------------------------------------------------------- /examples/src/main/scala/examples/hybrid/README.md: -------------------------------------------------------------------------------- 1 | Hybrid Proof-of-Work/Proof-of-Stake consensus protocol implementation 2 | ====================================================================== 3 | 4 | -------------------------------------------------------------------------------- /examples/src/main/scala/examples/hybrid/api/http/StatsApiRoute.scala: -------------------------------------------------------------------------------- 1 | package examples.hybrid.api.http 2 | 3 | import akka.actor.{ActorRef, ActorRefFactory} 4 | import akka.http.scaladsl.server.Route 5 | import examples.commons.SimpleBoxTransactionMemPool 6 | import examples.hybrid.history.HybridHistory 7 | import examples.hybrid.state.HBoxStoredState 8 | import examples.hybrid.wallet.HBoxWallet 9 | import io.circe.syntax._ 10 | import scorex.core.api.http.{ApiResponse, ApiRouteWithFullView, ApiTry} 11 | import scorex.core.settings.RESTApiSettings 12 | import scorex.core.utils.ScorexEncoding 13 | import scorex.util.ModifierId 14 | 15 | import scala.util.Try 16 | 17 | case class StatsApiRoute(override val settings: RESTApiSettings, nodeViewHolderRef: ActorRef) 18 | (implicit val context: ActorRefFactory) 19 | extends ApiRouteWithFullView[HybridHistory, HBoxStoredState, HBoxWallet, SimpleBoxTransactionMemPool] 20 | with ScorexEncoding { 21 | 22 | override val route: Route = (pathPrefix("stats")) { 23 | corsHandler( 24 | tail ~ meanDifficulty 25 | ) 26 | } 27 | 28 | def tail: Route = (get & path("tail" / IntNumber)) { count => 29 | withNodeView { view => 30 | val lastBlockIds = view.history.lastBlockIds(view.history.bestBlock, count) 31 | val tail = lastBlockIds.map(id => encoder.encodeId(id).asJson) 32 | ApiResponse("count" -> count.asJson, "tail" -> tail.asJson) 33 | } 34 | } 35 | 36 | def meanDifficulty: Route = (get & path("meanDifficulty" / IntNumber / IntNumber)) { (start, end) => 37 | withNodeView { view => 38 | ApiTry { 39 | val count = (view.history.height - start).toInt 40 | val ids: Seq[ModifierId] = view.history.lastBlockIds(view.history.bestBlock, count).take(end - start) 41 | val posDiff = ids.flatMap(id => Try(view.history.storage.getPoSDifficulty(id)).toOption) 42 | val powDiff = ids.flatMap(id => Try(view.history.storage.getPoWDifficulty(Some(id))).toOption) 43 | ApiResponse( 44 | "posDiff" -> (posDiff.sum / posDiff.length).asJson, 45 | "powDiff" -> (powDiff.sum / powDiff.length).asJson 46 | ) 47 | } 48 | } 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /examples/src/main/scala/examples/hybrid/blocks/HybridBlock.scala: -------------------------------------------------------------------------------- 1 | package examples.hybrid.blocks 2 | 3 | import examples.commons.SimpleBoxTransaction 4 | import scorex.core.PersistentNodeViewModifier 5 | import scorex.core.block.Block 6 | 7 | trait HybridBlock extends PersistentNodeViewModifier with Block[SimpleBoxTransaction] 8 | -------------------------------------------------------------------------------- /examples/src/main/scala/examples/hybrid/history/HybridSyncInfo.scala: -------------------------------------------------------------------------------- 1 | package examples.hybrid.history 2 | 3 | import examples.hybrid.blocks.{PosBlock, PowBlock} 4 | import scorex.core.consensus.SyncInfo 5 | import scorex.core.network.message.SyncInfoMessageSpec 6 | import scorex.core.serialization.ScorexSerializer 7 | import scorex.core.{ModifierTypeId, NodeViewModifier} 8 | import scorex.util.serialization.{Reader, Writer} 9 | import scorex.util.{ModifierId, bytesToId, idToBytes} 10 | 11 | /** 12 | * Stores up to 50 last PoW & Pos blocks 13 | * Thus maximum message size is about 100 * 32 ~= 3.2 KB 14 | * TODO answer is never used 15 | */ 16 | case class HybridSyncInfo(answer: Boolean, 17 | lastPowBlockIds: Seq[ModifierId], 18 | lastPosBlockId: ModifierId 19 | ) extends SyncInfo { 20 | 21 | import HybridSyncInfo.MaxLastPowBlocks 22 | 23 | require(lastPowBlockIds.size <= MaxLastPowBlocks) 24 | 25 | override def startingPoints: Seq[(ModifierTypeId, ModifierId)] = 26 | Seq(lastPowBlockIds.map(b => PowBlock.ModifierTypeId -> b) ++ Seq(PosBlock.ModifierTypeId -> lastPosBlockId)).flatten 27 | 28 | 29 | override type M = HybridSyncInfo 30 | 31 | override def serializer: ScorexSerializer[HybridSyncInfo] = HybridSyncInfoSerializer 32 | } 33 | 34 | object HybridSyncInfo { 35 | val MaxLastPowBlocks: Byte = 50 //don't make it more than 127 without changing serialization! 36 | } 37 | 38 | object HybridSyncInfoSerializer extends ScorexSerializer[HybridSyncInfo] { 39 | 40 | import HybridSyncInfo.MaxLastPowBlocks 41 | 42 | override def serialize(obj: HybridSyncInfo, w: Writer): Unit = { 43 | w.put(if (obj.answer) 1 else 0) 44 | w.put(obj.lastPowBlockIds.size.toByte) 45 | 46 | obj.lastPowBlockIds.foreach { b => 47 | w.putBytes(idToBytes(b)) 48 | } 49 | w.putBytes(idToBytes(obj.lastPosBlockId)) 50 | } 51 | 52 | override def parse(r: Reader): HybridSyncInfo = { 53 | val answer: Boolean = r.getByte() == 1.toByte 54 | val lastPowBlockIdsSize = r.getByte() 55 | 56 | require(lastPowBlockIdsSize >= 0 && lastPowBlockIdsSize <= MaxLastPowBlocks) 57 | require(r.remaining == (lastPowBlockIdsSize + 1) * NodeViewModifier.ModifierIdSize) 58 | 59 | val lastPowBlockIds = (0 until lastPowBlockIdsSize).map { _ => 60 | bytesToId(r.getBytes(NodeViewModifier.ModifierIdSize)) 61 | } 62 | 63 | val lastPosBlockId = bytesToId(r.getBytes(NodeViewModifier.ModifierIdSize)) 64 | 65 | HybridSyncInfo(answer, lastPowBlockIds, lastPosBlockId) 66 | } 67 | } 68 | 69 | object HybridSyncInfoMessageSpec extends SyncInfoMessageSpec[HybridSyncInfo](HybridSyncInfoSerializer) 70 | -------------------------------------------------------------------------------- /examples/src/main/scala/examples/hybrid/mining/HybridMiningSettings.scala: -------------------------------------------------------------------------------- 1 | package examples.hybrid.mining 2 | 3 | import java.io.File 4 | 5 | import com.typesafe.config.Config 6 | import net.ceedubs.ficus.Ficus._ 7 | import net.ceedubs.ficus.readers.ArbitraryTypeReader._ 8 | import net.ceedubs.ficus.readers.ValueReader 9 | import scorex.core.bytesToId 10 | import scorex.core.settings.ScorexSettings.readConfigFromPath 11 | import scorex.core.settings._ 12 | import scorex.util.ScorexLogging 13 | 14 | import scala.concurrent.duration._ 15 | 16 | case class HybridSettings(mining: HybridMiningSettings, 17 | walletSettings: WalletSettings, 18 | scorexSettings: ScorexSettings) 19 | 20 | case class WalletSettings(seed: String, 21 | password: String, 22 | walletDir: File) 23 | 24 | case class HybridMiningSettings(offlineGeneration: Boolean, 25 | targetBlockDelay: FiniteDuration, 26 | blockGenerationDelay: FiniteDuration, 27 | posAttachmentSize: Int, 28 | rParamX10: Int, 29 | initialDifficulty: BigInt) { 30 | lazy val MaxTarget = BigInt(1, Array.fill(32)(Byte.MinValue)) 31 | lazy val GenesisParentId = bytesToId(Array.fill(32)(1: Byte)) 32 | } 33 | 34 | object HybridSettings extends ScorexLogging with SettingsReaders { 35 | def read(userConfigPath: Option[String]): HybridSettings = { 36 | fromConfig(readConfigFromPath(userConfigPath, "scorex")) 37 | } 38 | 39 | implicit val networkSettingsValueReader: ValueReader[HybridSettings] = 40 | (cfg: Config, path: String) => fromConfig(cfg.getConfig(path)) 41 | 42 | private def fromConfig(config: Config): HybridSettings = { 43 | log.info(config.toString) 44 | val walletSettings = config.as[WalletSettings]("scorex.wallet") 45 | val miningSettings = config.as[HybridMiningSettings]("scorex.miner") 46 | val scorexSettings = config.as[ScorexSettings]("scorex") 47 | HybridSettings(miningSettings, walletSettings, scorexSettings) 48 | } 49 | } 50 | 51 | -------------------------------------------------------------------------------- /examples/src/main/scala/examples/hybrid/util/Cancellable.scala: -------------------------------------------------------------------------------- 1 | package examples.hybrid.util 2 | 3 | import scala.concurrent.{Future, Promise} 4 | import scala.util.Success 5 | 6 | 7 | trait CancellableStatus { 8 | def isCancelled: Boolean 9 | 10 | def nonCancelled: Boolean = !isCancelled 11 | } 12 | 13 | trait Cancellable { 14 | def cancel(): Boolean 15 | 16 | def status: CancellableStatus 17 | } 18 | 19 | object Cancellable { 20 | def apply(): Cancellable = new Cancellable { 21 | val p = Promise[Unit]() 22 | 23 | override def cancel(): Boolean = p.tryComplete(Success(())) 24 | 25 | val status: CancellableStatus = new CancellableStatus { 26 | override def isCancelled: Boolean = p.future.value.isDefined 27 | } 28 | } 29 | 30 | def run()(cont: CancellableStatus => Future[Unit]): Cancellable = { 31 | val cancellable = Cancellable() 32 | cont(cancellable.status) // run continuation feeding status 33 | cancellable 34 | } 35 | } -------------------------------------------------------------------------------- /examples/src/main/scala/examples/hybrid/util/FileFunctions.scala: -------------------------------------------------------------------------------- 1 | package examples.hybrid.util 2 | 3 | import java.io.FileWriter 4 | 5 | object FileFunctions { 6 | def append(filename: String, record: String): Unit = { 7 | val fw = new FileWriter(filename, true) 8 | try { 9 | fw.write(s"$record\n") 10 | } 11 | finally fw.close() 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/src/main/scala/examples/hybrid/validation/DifficultyBlockValidator.scala: -------------------------------------------------------------------------------- 1 | package examples.hybrid.validation 2 | 3 | import examples.hybrid.blocks.{HybridBlock, PosBlock, PowBlock} 4 | import examples.hybrid.history.HistoryStorage 5 | import examples.hybrid.mining.{HybridMiningSettings, PosForger} 6 | import scorex.core.block.BlockValidator 7 | import scorex.core.utils.ScorexEncoding 8 | 9 | import scala.util.Try 10 | 11 | class DifficultyBlockValidator(settings: HybridMiningSettings, storage: HistoryStorage) 12 | extends BlockValidator[HybridBlock] with ScorexEncoding { 13 | 14 | def validate(block: HybridBlock): Try[Unit] = block match { 15 | case b: PowBlock => checkPoWConsensusRules(b) 16 | case b: PosBlock => checkPoSConsensusRules(b, settings) 17 | } 18 | 19 | //PoW consensus rules checks, work/references 20 | //throws exception if anything wrong 21 | private def checkPoWConsensusRules(powBlock: PowBlock): Try[Unit] = Try { 22 | val powDifficulty = storage.getPoWDifficulty(Some(powBlock.prevPosId)) 23 | //check work 24 | require(powBlock.correctWork(powDifficulty, settings), 25 | s"Work done is incorrect for block ${encoder.encodeId(powBlock.id)} and difficulty $powDifficulty") 26 | 27 | //some brothers work 28 | require(powBlock.brothers.forall(_.correctWork(powDifficulty, settings))) 29 | 30 | } 31 | 32 | //PoS consensus rules checks, throws exception if anything wrong 33 | private def checkPoSConsensusRules(posBlock: PosBlock, miningSettings: HybridMiningSettings): Try[Unit] = Try { 34 | if (!storage.isGenesis(posBlock)) { 35 | // TODO: review me - .get 36 | @SuppressWarnings(Array("org.wartremover.warts.OptionPartial")) 37 | val parentPoW: PowBlock = storage.modifierById(posBlock.parentId).get.asInstanceOf[PowBlock] 38 | val hit = PosForger.hit(parentPoW)(posBlock.generatorBox) 39 | val posDifficulty = storage.getPoSDifficulty(parentPoW.prevPosId) 40 | val target = (miningSettings.MaxTarget / posDifficulty) * posBlock.generatorBox.value 41 | require(hit < target, s"$hit < $target failed, $posDifficulty, ") 42 | } 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /examples/src/main/scala/examples/hybrid/validation/ParentBlockValidator.scala: -------------------------------------------------------------------------------- 1 | package examples.hybrid.validation 2 | 3 | import examples.hybrid.blocks.{HybridBlock, PosBlock, PowBlock} 4 | import examples.hybrid.history.HistoryStorage 5 | import scorex.core.block.BlockValidator 6 | import scorex.core.utils.ScorexEncoding 7 | 8 | import scala.util.Try 9 | 10 | class ParentBlockValidator(storage: HistoryStorage) 11 | extends BlockValidator[HybridBlock] with ScorexEncoding { 12 | 13 | def validate(block: HybridBlock): Try[Unit] = Try { 14 | block match { 15 | case powBlock: PowBlock => if (!storage.isGenesis(powBlock)) { 16 | //check PoW parent id ??? 17 | require(storage.modifierById(powBlock.parentId).isDefined, s"Parent ${encoder.encodeId(powBlock.parentId)} missed") 18 | //check referenced PoS block exists as well 19 | // TODO: review me - .get 20 | @SuppressWarnings(Array("org.wartremover.warts.OptionPartial")) 21 | val posBlock = storage.modifierById(powBlock.prevPosId).get 22 | 23 | //check referenced PoS block points to parent PoW block 24 | require(posBlock.parentId == posBlock.parentId, "ref rule broken") 25 | } 26 | case posBlock: PosBlock => 27 | //check PoW block exists 28 | require(storage.modifierById(posBlock.parentId).isDefined) 29 | } 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /examples/src/main/scala/examples/hybrid/validation/SemanticBlockValidator.scala: -------------------------------------------------------------------------------- 1 | package examples.hybrid.validation 2 | 3 | import examples.hybrid.blocks.{HybridBlock, PosBlock, PowBlock} 4 | import scorex.core.block.BlockValidator 5 | import scorex.crypto.hash.{CryptographicHash, Digest} 6 | 7 | import scala.util.Try 8 | 9 | class SemanticBlockValidator(hash: CryptographicHash[_ <: Digest]) extends BlockValidator[HybridBlock] { 10 | 11 | def validate(block: HybridBlock): Try[Unit] = Try { 12 | block match { 13 | case powBlock: PowBlock => 14 | require(powBlock.brothersCount >= 0) 15 | require(powBlock.timestamp >= 0) 16 | 17 | //check brothers data 18 | require(powBlock.brothers.size == powBlock.brothersCount) 19 | if (powBlock.brothersCount > 0) { 20 | require(java.util.Arrays.equals(hash(powBlock.brotherBytes), powBlock.brothersHash)) 21 | } 22 | case posBlock: PosBlock => 23 | require(posBlock.timestamp >= 0) 24 | require(PosBlock.signatureValid(posBlock)) 25 | } 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /examples/src/main/scala/examples/spv/Constants.scala: -------------------------------------------------------------------------------- 1 | package examples.spv 2 | 3 | import scorex.crypto.hash.Blake2b256 4 | 5 | object Constants { 6 | val hashfn: Blake2b256.type = Blake2b256 7 | 8 | lazy val MaxTarget: BigInt = BigInt(1, Array.fill(32)(Byte.MinValue)) 9 | val InitialDifficulty: BigInt = BigInt(1) 10 | } -------------------------------------------------------------------------------- /examples/src/main/scala/examples/spv/simulation/SPVSimulator.scala: -------------------------------------------------------------------------------- 1 | package examples.spv.simulation 2 | 3 | import examples.spv.{Header, KMZProofSerializer, SpvAlgos} 4 | import scorex.core.transaction.state.PrivateKey25519Companion 5 | import scorex.crypto.hash.Blake2b256 6 | 7 | object SPVSimulator extends App with SimulatorFuctions { 8 | 9 | private val Height = 500000 10 | private val Difficulty = BigInt(1) 11 | private val stateRoot = Blake2b256("") 12 | private val minerKeys = PrivateKey25519Companion.generateKeys(stateRoot) 13 | 14 | private val genesis = genGenesisHeader(stateRoot, minerKeys._2) 15 | val st: Long = System.currentTimeMillis() 16 | private val headerChain: Seq[Header] = genChain(Height, Difficulty, stateRoot, IndexedSeq(genesis)) 17 | 18 | val lastBlock: Option[Header] = headerChain.lastOption 19 | var minDiff: BigInt = Difficulty 20 | 21 | private val k = 6 22 | 23 | println(s"Chain of length $Height, k=$k") 24 | println("m,proofLength,blockNum,uniqueBlockNum") 25 | 26 | Seq(6, 15, 30, 50, 100, 127) foreach { m => 27 | val proof = SpvAlgos.constructKMZProof(m, k, headerChain).get 28 | proof.valid.get 29 | val blocks:Seq[Header] = proof.suffix ++ proof.prefixProofs.flatten 30 | val blockNum = blocks.length 31 | val uniqueBlockNum = blocks.map(_.encodedId).toSet.size 32 | println( m + "," + KMZProofSerializer.toBytes(proof).length + "," + blockNum + "," + uniqueBlockNum) 33 | } 34 | 35 | 36 | } 37 | -------------------------------------------------------------------------------- /examples/src/main/scala/examples/trimchain/README.md: -------------------------------------------------------------------------------- 1 | TrimChain consensus protocol implementation 2 | =========================================== 3 | -------------------------------------------------------------------------------- /examples/src/main/scala/examples/trimchain/core/Constants.scala: -------------------------------------------------------------------------------- 1 | package examples.trimchain.core 2 | 3 | import scorex.crypto.hash.Blake2b256 4 | 5 | 6 | object Constants { 7 | val n: Int = 20 8 | val k: Int = 1 9 | val NElementsInProof: Int = 10 10 | 11 | val hashfn: Blake2b256.type = Blake2b256 12 | 13 | val StateRootLength: Int = hashfn.DigestSize 14 | 15 | val TxRootLength: Int = hashfn.DigestSize 16 | 17 | 18 | lazy val MaxTarget = BigInt(1, Array.fill(32)(Byte.MinValue)) 19 | lazy val Difficulty = BigInt("2") 20 | } -------------------------------------------------------------------------------- /examples/src/main/scala/examples/trimchain/core/Ticket.scala: -------------------------------------------------------------------------------- 1 | package examples.trimchain.core 2 | 3 | import io.circe.Encoder 4 | import io.circe.syntax._ 5 | import scorex.util.serialization.{Reader, Writer} 6 | import scorex.core.serialization.ScorexSerializer 7 | import scorex.core.utils.ScorexEncoding 8 | import scorex.crypto.authds.SerializedAdProof 9 | import scorex.crypto.signatures.Curve25519 10 | 11 | import scala.annotation.tailrec 12 | import scorex.util.Extensions._ 13 | 14 | case class Ticket(minerKey: Array[Byte], partialProofs: Seq[SerializedAdProof]) { 15 | 16 | override def toString: String = this.asJson.noSpaces 17 | } 18 | 19 | object Ticket extends ScorexEncoding { 20 | implicit val ticketEncoder: Encoder[Ticket] = (t: Ticket) => 21 | Map( 22 | "minerKey" -> encoder.encode(t.minerKey).asJson, 23 | "proofs" -> t.partialProofs.map(encoder.encode).asJson 24 | ).asJson 25 | } 26 | 27 | object TicketSerializer extends ScorexSerializer[Ticket] { 28 | 29 | val MinerKeySize: Int = Curve25519.KeyLength 30 | 31 | override def serialize(obj: Ticket, w: Writer): Unit = { 32 | w.putBytes(obj.minerKey) 33 | w.putShort(obj.partialProofs.length.toShortExact) 34 | 35 | obj.partialProofs.map { bytes => 36 | require(bytes.length == bytes.length.toShort) 37 | w.putShort(bytes.length.toShort) 38 | w.putBytes(bytes) 39 | } 40 | } 41 | 42 | override def parse(r: Reader): Ticket = { 43 | val minerKey = r.getBytes(MinerKeySize) 44 | val proofNum = r.getShort() 45 | 46 | @tailrec 47 | def parseProofs(proofNum: Int, acc: Seq[SerializedAdProof] = Seq.empty): Seq[SerializedAdProof] = { 48 | if (proofNum > 0) { 49 | val proofSize = r.getShort() 50 | val proof = SerializedAdProof @@ r.getBytes(proofSize) 51 | parseProofs(proofNum - 1, proof +: acc) 52 | } else { 53 | acc 54 | } 55 | } 56 | 57 | val proofs = parseProofs(proofNum).reverse 58 | Ticket(minerKey, proofs) 59 | } 60 | } -------------------------------------------------------------------------------- /examples/src/main/scala/examples/trimchain/core/core.scala: -------------------------------------------------------------------------------- 1 | package examples.trimchain 2 | 3 | import supertagged.TaggedType 4 | 5 | package object core { 6 | 7 | object StateRoot extends TaggedType[Array[Byte]] 8 | 9 | object TransactionsRoot extends TaggedType[Array[Byte]] 10 | 11 | type StateRoot = StateRoot.Type 12 | 13 | type TransactionsRoot = TransactionsRoot.Type 14 | 15 | } 16 | -------------------------------------------------------------------------------- /examples/src/main/scala/examples/trimchain/modifiers/BlockHeader.scala: -------------------------------------------------------------------------------- 1 | package examples.trimchain.modifiers 2 | 3 | import examples.trimchain.core._ 4 | import io.circe.Encoder 5 | import io.circe.syntax._ 6 | import scorex.core.ModifierTypeId 7 | import scorex.util.serialization.{Reader, Writer} 8 | import scorex.core.serialization.ScorexSerializer 9 | import scorex.core.utils.ScorexEncoding 10 | import scorex.util.{ModifierId, bytesToId, idToBytes} 11 | 12 | //TODO compact proof of ticket in header 13 | case class BlockHeader(override val parentId: ModifierId, 14 | stateRoot: StateRoot, 15 | txRoot: TransactionsRoot, 16 | ticket: Ticket, 17 | powNonce: Long 18 | ) extends TModifier { 19 | override type M = BlockHeader 20 | 21 | override val modifierTypeId: ModifierTypeId = TModifier.Header 22 | 23 | override lazy val id: ModifierId = bytesToId(Constants.hashfn.hash(bytes)) 24 | 25 | def correctWorkDone(difficulty: BigInt): Boolean = { 26 | val target = Constants.MaxTarget / difficulty 27 | BigInt(1, idToBytes(id)) < target 28 | } 29 | 30 | override lazy val serializer = BlockHeaderSerializer 31 | } 32 | 33 | object BlockHeader extends ScorexEncoding { 34 | implicit val blockHeaderEncoder: Encoder[BlockHeader] = (bh: BlockHeader) => 35 | Map( 36 | "id" -> encoder.encodeId(bh.id).asJson, 37 | "parentId" -> encoder.encodeId(bh.parentId).asJson, 38 | "stateRoot" -> encoder.encode(bh.stateRoot).asJson, 39 | "txRoot" -> encoder.encode(bh.txRoot).asJson, 40 | "ticket" -> bh.ticket.asJson, 41 | "powNonce" -> bh.powNonce.asJson 42 | ).asJson 43 | } 44 | 45 | object BlockHeaderSerializer extends ScorexSerializer[BlockHeader] { 46 | private val ds = Constants.hashfn.DigestSize 47 | 48 | override def serialize(obj: BlockHeader, w: Writer): Unit = { 49 | w.putBytes(idToBytes(obj.parentId)) 50 | w.putBytes(obj.stateRoot) 51 | w.putBytes(obj.txRoot) 52 | w.putLong(obj.powNonce) 53 | TicketSerializer.serialize(obj.ticket, w) 54 | } 55 | 56 | override def parse(r: Reader): BlockHeader = { 57 | val parentId = bytesToId(r.getBytes(ds)) 58 | val stateRoot = StateRoot @@ r.getBytes(ds) 59 | val txRoot = TransactionsRoot @@ r.getBytes(ds) 60 | val powNonce = r.getLong() 61 | val ticket = TicketSerializer.parse(r) 62 | BlockHeader(parentId, stateRoot, txRoot, ticket, powNonce) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /examples/src/main/scala/examples/trimchain/modifiers/TBlock.scala: -------------------------------------------------------------------------------- 1 | package examples.trimchain.modifiers 2 | 3 | import examples.commons.{SimpleBoxTransaction, SimpleBoxTransactionSerializer} 4 | import io.circe.Encoder 5 | import io.circe.syntax._ 6 | import scorex.core.ModifierTypeId 7 | import scorex.core.block.Block 8 | import scorex.core.block.Block.{Timestamp, Version} 9 | import scorex.util.serialization.{Reader, Writer} 10 | import scorex.core.serialization.ScorexSerializer 11 | import scorex.util.ModifierId 12 | 13 | import scala.annotation.tailrec 14 | 15 | case class TBlock(header: BlockHeader, body: Seq[SimpleBoxTransaction], timestamp: Timestamp) 16 | extends TModifier with Block[SimpleBoxTransaction] { 17 | 18 | override def version: Version = 0: Version 19 | 20 | override def parentId: ModifierId = header.parentId 21 | 22 | override def transactions: Seq[SimpleBoxTransaction] = body 23 | 24 | override val modifierTypeId: ModifierTypeId = TModifier.Block 25 | 26 | override def id: ModifierId = header.id 27 | 28 | override type M = TBlock 29 | 30 | override def serializer: ScorexSerializer[TBlock] = TBlockSerializer 31 | } 32 | 33 | object TBlockSerializer extends ScorexSerializer[TBlock] { 34 | 35 | override def serialize(obj: TBlock, w: Writer): Unit = { 36 | w.putLong(obj.timestamp) 37 | val headerWriter = w.newWriter() 38 | BlockHeaderSerializer.serialize(obj.header, headerWriter) 39 | w.putShort(headerWriter.length().toShort) 40 | w.append(headerWriter) 41 | 42 | obj.body.foreach { tx => 43 | val txw = w.newWriter() 44 | SimpleBoxTransactionSerializer.serialize(tx, txw) 45 | w.putShort(txw.length().toShort) 46 | w.append(txw) 47 | } 48 | } 49 | 50 | override def parse(r: Reader): TBlock = { 51 | val timestamp = r.getLong() 52 | val headerLength = r.getShort() 53 | val header = BlockHeaderSerializer.parse(r.newReader(r.getChunk(headerLength))) 54 | 55 | @tailrec 56 | def parseTxs(acc: Seq[SimpleBoxTransaction] = Seq()): Seq[SimpleBoxTransaction] = { 57 | if (r.remaining > 0) { 58 | val txLength = r.getShort() 59 | val tx = SimpleBoxTransactionSerializer.parse(r.newReader(r.getChunk(txLength))) 60 | parseTxs(tx +: acc) 61 | } else { 62 | acc 63 | } 64 | } 65 | val body = parseTxs().reverse 66 | TBlock(header, body, timestamp) 67 | } 68 | } 69 | 70 | object TBlock { 71 | implicit val tBlockEncoder: Encoder[TBlock] = (tb: TBlock) => 72 | Map( 73 | "header" -> tb.header.asJson, 74 | "body" -> tb.body.map(_.asJson).asJson, 75 | "timestamp" -> tb.timestamp.asJson 76 | ).asJson 77 | } 78 | -------------------------------------------------------------------------------- /examples/src/main/scala/examples/trimchain/modifiers/TModifier.scala: -------------------------------------------------------------------------------- 1 | package examples.trimchain.modifiers 2 | 3 | import scorex.core._ 4 | 5 | 6 | trait TModifier extends PersistentNodeViewModifier 7 | 8 | object TModifier { 9 | val Header: ModifierTypeId = ModifierTypeId @@ 0.toByte 10 | val UtxoSnapshot: ModifierTypeId = ModifierTypeId @@ 1.toByte 11 | val Block: ModifierTypeId = ModifierTypeId @@ 2.toByte 12 | } -------------------------------------------------------------------------------- /examples/src/main/scala/examples/trimchain/modifiers/UtxoSnapshot.scala: -------------------------------------------------------------------------------- 1 | package examples.trimchain.modifiers 2 | 3 | import examples.trimchain.utxo.PersistentAuthenticatedUtxo 4 | import scorex.core.ModifierTypeId 5 | import scorex.core.serialization.ScorexSerializer 6 | import scorex.util.ModifierId 7 | 8 | class UtxoSnapshot(override val parentId: ModifierId, 9 | header: BlockHeader, 10 | utxo: PersistentAuthenticatedUtxo) extends TModifier { 11 | 12 | override val modifierTypeId: ModifierTypeId = TModifier.UtxoSnapshot 13 | 14 | //todo: check statically or dynamically output size 15 | override def id: ModifierId = header.id 16 | 17 | //todo: for Dmitry: implement header + utxo root printing 18 | 19 | override type M = UtxoSnapshot 20 | 21 | //todo: for Dmitry: implement: dump all the boxes 22 | override def serializer: ScorexSerializer[M] = ??? 23 | } 24 | -------------------------------------------------------------------------------- /examples/src/main/scala/examples/trimchain/simulation/Simulators.scala: -------------------------------------------------------------------------------- 1 | package examples.trimchain.simulation 2 | 3 | import examples.commons.{PublicKey25519NoncedBox, SimpleBoxTransaction} 4 | import examples.trimchain.core.Constants._ 5 | import examples.trimchain.core.{Algos, Constants, StateRoot, TransactionsRoot} 6 | import examples.trimchain.modifiers.TBlock 7 | import examples.trimchain.utxo.PersistentAuthenticatedUtxo 8 | import scorex.core._ 9 | import scorex.core.transaction.state.PrivateKey25519Companion 10 | 11 | trait Simulators { 12 | 13 | 14 | val minerKeys = PrivateKey25519Companion.generateKeys(scorex.utils.Random.randomBytes(32)) 15 | val minerPubKey = minerKeys._2 16 | val minerPrivKey = minerKeys._1 17 | 18 | val defaultBytes = Array.fill(32)(0: Byte) 19 | val defaultId = bytesToId(defaultBytes) 20 | val defaultVersion = bytesToVersion(defaultBytes) 21 | 22 | def generateBlock(txs: Seq[SimpleBoxTransaction], 23 | currentUtxo: InMemoryAuthenticatedUtxo, 24 | miningUtxos: IndexedSeq[InMemoryAuthenticatedUtxo]): (TBlock, Seq[PublicKey25519NoncedBox], InMemoryAuthenticatedUtxo) = { 25 | //todo: fix, hashchain instead of Merkle tree atm 26 | val txsHash = TransactionsRoot @@ hashfn(scorex.core.utils.concatBytes(txs.map(_.bytes))) 27 | 28 | val changes = PersistentAuthenticatedUtxo.changes(txs).get 29 | val updUtxo = currentUtxo.applyChanges(changes, bytesToVersion(scorex.utils.Random.randomBytes())).get 30 | 31 | // TODO: review me - .get.get 32 | @SuppressWarnings(Array("org.wartremover.warts.OptionPartial")) 33 | val h = Algos.pow(defaultId, txsHash, StateRoot @@ currentUtxo.rootHash, minerPubKey.pubKeyBytes, 34 | miningUtxos, Constants.Difficulty, 10000).get.get 35 | 36 | val newRichBoxes = txs.flatMap(_.newBoxes).filter(_.value > 5) 37 | (TBlock(h, txs, System.currentTimeMillis()), newRichBoxes, updUtxo) 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /examples/src/main/scala/examples/trimchain/simulation/SpaceSavingsCalculator.scala: -------------------------------------------------------------------------------- 1 | package examples.trimchain.simulation 2 | 3 | import java.io.File 4 | 5 | import examples.commons.PublicKey25519NoncedBox 6 | 7 | object SpaceSavingsCalculator extends App { 8 | 9 | private val eta = 100 10 | private val start = 1000 11 | private val finish = 10000 12 | 13 | private val outSize = PublicKey25519NoncedBox.BoxLength 14 | 15 | private val file = new File("/home/pozharko/Code/papers/trimchain/results-20.csv") 16 | 17 | private val lines = scala.io.Source.fromFile(file).getLines().toIndexedSeq 18 | 19 | // println(lines.head) 20 | 21 | // TODO: fixme, What should we do if `lines` is empty? 22 | @SuppressWarnings(Array("org.wartremover.warts.TraversableOps")) 23 | private val data = lines.tail.take(finish).map(_.split(",")) 24 | 25 | private val blockSizes = data.map(_.apply(8)).map(_.toLong) 26 | 27 | private val headerSizes = data.map(_.apply(5)).map(_.toLong) 28 | 29 | private val currentUtxoSizes = data.map(_.apply(2)).map(_.toLong * outSize) 30 | 31 | println(s"height,full,spv,light,mining,f/s,f/l,f/m") 32 | (start to finish).foreach{h => 33 | val fullChain = blockSizes.take(h).sum 34 | val lightChain = headerSizes.take(h - eta).sum + blockSizes.slice(h - eta, h).sum 35 | 36 | val spvchain = headerSizes.take(h).sum 37 | 38 | val fullSet = fullChain + currentUtxoSizes(h - 1) 39 | 40 | val lightSet = lightChain + currentUtxoSizes(h - 1) 41 | 42 | val miningSet = lightSet + currentUtxoSizes(h - eta - 1) 43 | 44 | println(s"$h,$fullSet,$spvchain,$lightSet,$miningSet,${fullSet/spvchain.toDouble},${fullSet / lightSet.toDouble},${fullSet / miningSet.toDouble}") 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /examples/src/main/scala/examples/trimchain/simulation/ValidationSimulator.scala: -------------------------------------------------------------------------------- 1 | package examples.trimchain.simulation 2 | 3 | import com.google.common.primitives.{Ints, Longs} 4 | import examples.commons.{Nonce, PublicKey25519NoncedBox, Value} 5 | import examples.trimchain.core.Constants._ 6 | import examples.trimchain.core.{Algos, Constants} 7 | import scorex.core.idToVersion 8 | import scorex.core.transaction.box.proposition.PublicKey25519Proposition 9 | import scorex.core.transaction.state.{BoxStateChanges, Insertion} 10 | import scorex.crypto.authds.ADDigest 11 | 12 | object ValidationSimulator extends App with Simulators { 13 | 14 | // NElementsInProof: 2, validation time: 0.000434440 seconds 15 | // NElementsInProof: 5, validation time: 0.000532366 seconds 16 | // NElementsInProof: 10, validation time: 0.000530379 seconds 17 | // NElementsInProof: 20, validation time: 0.000618186 seconds 18 | 19 | val StateSize: Int = 5000000 20 | val ValidationNum: Int = 10000 21 | 22 | val genesisBoxes = (1 to StateSize) map { i => 23 | PublicKey25519NoncedBox( 24 | minerPubKey, 25 | Nonce @@ Longs.fromByteArray(hashfn(minerPubKey.pubKeyBytes ++ Ints.toByteArray(i)).take(8)), 26 | Value @@ 10000000000L 27 | ) 28 | } 29 | 30 | val genesisChanges: BoxStateChanges[PublicKey25519Proposition, PublicKey25519NoncedBox] = 31 | BoxStateChanges(genesisBoxes.map(box => Insertion[PublicKey25519Proposition, PublicKey25519NoncedBox](box))) 32 | 33 | val genesisUtxo: InMemoryAuthenticatedUtxo = InMemoryAuthenticatedUtxo(genesisBoxes.size, None, idToVersion(defaultId)) 34 | .applyChanges(genesisChanges, idToVersion(defaultId)).get 35 | val rootHash: ADDigest = genesisUtxo.rootHash 36 | 37 | var validationTime: Long = 0L 38 | (0 until ValidationNum) foreach { _ => 39 | val block = generateBlock(Seq(), genesisUtxo, IndexedSeq(genesisUtxo))._1 40 | val start = System.nanoTime() 41 | Algos.validatePow(block.header, IndexedSeq(rootHash), Constants.Difficulty) 42 | println("!! " + (System.nanoTime() - start)) 43 | validationTime += System.nanoTime() - start 44 | } 45 | println(s"One block validation takes ${validationTime / ValidationNum} nanos") 46 | } 47 | -------------------------------------------------------------------------------- /examples/src/test/resources/settings.conf: -------------------------------------------------------------------------------- 1 | scorex { 2 | dataDir = /tmp/scorex-test/data/blockchain 3 | logDir = /tmp/scorex-test/data/log 4 | 5 | restApi { 6 | bindAddress = "127.0.0.1:9085" 7 | apiKeyHash = "" 8 | } 9 | 10 | network { 11 | nodeName = "node1" 12 | bindAddress = "127.0.0.1:9084" 13 | knownPeers = ["127.0.0.2:9088"] 14 | agentName = "2-Hop" 15 | } 16 | 17 | miner { 18 | offlineGeneration = true 19 | targetBlockDelay = 100s 20 | blockGenerationDelay = 100ms 21 | rParamX10 = 8 22 | initialDifficulty = 1 23 | posAttachmentSize = 100 24 | } 25 | 26 | wallet { 27 | seed = "genesisoo" 28 | password = "cookies" 29 | walletDir = "/tmp/scorex-test/data/wallet" 30 | } 31 | } -------------------------------------------------------------------------------- /examples/src/test/scala/commons/ExamplesCommonGenerators.scala: -------------------------------------------------------------------------------- 1 | package commons 2 | 3 | import examples.commons.{Nonce, SimpleBoxTransaction, Value} 4 | import org.scalacheck.Gen 5 | import scorex.core.transaction.box.proposition.PublicKey25519Proposition 6 | import scorex.core.transaction.state.PrivateKey25519 7 | import scorex.testkit.generators.CoreGenerators 8 | 9 | trait ExamplesCommonGenerators extends CoreGenerators { 10 | lazy val nonceGen: Gen[Nonce] = positiveLongGen.map(a => Nonce @@ a) 11 | 12 | lazy val valueGen: Gen[Value] = positiveLongGen.map(a => Value @@ a) 13 | 14 | lazy val pGen: Gen[(PublicKey25519Proposition, Value)] = for { 15 | prop <- propositionGen 16 | long <- valueGen 17 | } yield (prop, long) 18 | 19 | lazy val privGen: Gen[(PrivateKey25519, Nonce)] = for { 20 | prop <- key25519Gen.map(_._1) 21 | long <- nonceGen 22 | } yield (prop, long) 23 | 24 | lazy val simpleBoxTransactionGen: Gen[SimpleBoxTransaction] = for { 25 | fee <- positiveLongGen 26 | timestamp <- positiveLongGen 27 | from: IndexedSeq[(PrivateKey25519, Nonce)] <- smallInt.flatMap(i => Gen.listOfN(i + 1, privGen).map(_.toIndexedSeq)) 28 | to: IndexedSeq[(PublicKey25519Proposition, Value)] <- smallInt.flatMap(i => Gen.listOfN(i, pGen).map(_.toIndexedSeq)) 29 | } yield SimpleBoxTransaction(from, to, fee, timestamp) 30 | 31 | lazy val simpleBoxTransactionsGen: Gen[List[SimpleBoxTransaction]] = for { 32 | txs <- smallInt.flatMap(i => Gen.listOfN(i, simpleBoxTransactionGen)) 33 | } yield txs 34 | 35 | def simpleBoxTransactionGenCustomMakeBoxes(toBoxes: IndexedSeq[(PublicKey25519Proposition, Value)]): Gen[SimpleBoxTransaction] = for { 36 | fee <- positiveLongGen 37 | timestamp <- positiveLongGen 38 | from: IndexedSeq[(PrivateKey25519, Nonce)] <- Gen.choose(1, 1).flatMap(i => Gen.listOfN(i + 1, privGen).map(_.toIndexedSeq)) 39 | to = toBoxes 40 | } yield SimpleBoxTransaction(from, to, fee, timestamp) 41 | 42 | def simpleBoxTransactionGenCustomUseBoxes(fromBoxes: IndexedSeq[(PrivateKey25519, Nonce)]): Gen[SimpleBoxTransaction] = for { 43 | fee <- positiveLongGen 44 | timestamp <- positiveLongGen 45 | from = fromBoxes 46 | to: IndexedSeq[(PublicKey25519Proposition, Value)] <- Gen.choose(1, 1).flatMap(i => Gen.listOfN(i, pGen).map(_.toIndexedSeq)) 47 | } yield SimpleBoxTransaction(from, to, fee, timestamp) 48 | } 49 | -------------------------------------------------------------------------------- /examples/src/test/scala/hybrid/HistoryGenerators.scala: -------------------------------------------------------------------------------- 1 | package hybrid 2 | 3 | import examples.hybrid.blocks.PowBlock 4 | import examples.hybrid.history.{HistoryStorage, HybridHistory} 5 | import examples.hybrid.mining.HybridSettings 6 | import org.scalacheck.Gen 7 | import scorex.core.transaction.box.proposition.PublicKey25519Proposition 8 | import scorex.core.utils.NetworkTimeProvider 9 | import scorex.crypto.signatures.PublicKey 10 | 11 | import scala.concurrent.ExecutionContext.Implicits.global 12 | 13 | trait HistoryGenerators { 14 | this: StoreGenerators => 15 | 16 | def settings: HybridSettings 17 | 18 | private val historyTimestamp = 1478164225796L 19 | private val historyNonce = -308545845552064644L 20 | private val historyBrothersCount = 0 21 | private val historyBrothersHash = Array.fill(32)(0: Byte) 22 | private val historyBrothers = Seq.empty 23 | private val historyProposition = PublicKey25519Proposition(PublicKey @@ scorex.utils.Random.randomBytes(32)) 24 | 25 | private lazy val genesisBlock = PowBlock( 26 | settings.mining.GenesisParentId, 27 | settings.mining.GenesisParentId, 28 | historyTimestamp, 29 | historyNonce, 30 | historyBrothersCount, 31 | historyBrothersHash, 32 | historyProposition, 33 | historyBrothers) 34 | 35 | val historyGen: Gen[HybridHistory] = lsmStoreGen.map { blockStorage => 36 | val storage = new HistoryStorage(blockStorage, settings.mining) 37 | //we don't care about validation here 38 | val validators = Seq() 39 | new HybridHistory(storage, settings.mining, validators, None, new NetworkTimeProvider(settings.scorexSettings.ntp)) 40 | .append(genesisBlock).get._1 41 | .ensuring(_.modifierById(genesisBlock.id).isDefined) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /examples/src/test/scala/hybrid/HybridSanity.scala: -------------------------------------------------------------------------------- 1 | package hybrid 2 | 3 | import examples.commons.{PublicKey25519NoncedBox, SimpleBoxTransaction, SimpleBoxTransactionMemPool} 4 | import examples.hybrid.blocks.{HybridBlock, PosBlock} 5 | import examples.hybrid.history.{HybridHistory, HybridSyncInfo} 6 | import examples.hybrid.state.HBoxStoredState 7 | import examples.hybrid.wallet.HBoxWallet 8 | import org.scalacheck.Gen 9 | import scorex.core.transaction.box.proposition.PublicKey25519Proposition 10 | import scorex.testkit.{BlockchainPerformance, BlockchainSanity} 11 | 12 | 13 | class HybridSanity extends BlockchainSanity[PublicKey25519Proposition, 14 | SimpleBoxTransaction, 15 | HybridBlock, 16 | PosBlock, 17 | HybridSyncInfo, 18 | PublicKey25519NoncedBox, 19 | SimpleBoxTransactionMemPool, 20 | HBoxStoredState, 21 | HybridHistory] with BlockchainPerformance[SimpleBoxTransaction, HybridBlock, HybridSyncInfo, 22 | SimpleBoxTransactionMemPool, HBoxStoredState, HybridHistory] 23 | with HybridGenerators { 24 | 25 | private val walletSettings = originalSettings.walletSettings.copy(seed = "p") 26 | 27 | //Node view components 28 | override lazy val memPool: SimpleBoxTransactionMemPool = SimpleBoxTransactionMemPool.emptyPool 29 | override lazy val memPoolGenerator: Gen[SimpleBoxTransactionMemPool] = emptyMemPoolGen 30 | override lazy val transactionGenerator: Gen[TX] = simpleBoxTransactionGen 31 | override lazy val wallet = (0 until 100).foldLeft(HBoxWallet.readOrGenerate(walletSettings))((w, _) => w.generateNewSecret()) 32 | } -------------------------------------------------------------------------------- /examples/src/test/scala/hybrid/HybridTypes.scala: -------------------------------------------------------------------------------- 1 | package hybrid 2 | 3 | import examples.commons.{SimpleBoxTransaction, SimpleBoxTransactionMemPool} 4 | import examples.hybrid.HybridNodeViewHolder 5 | import examples.hybrid.blocks._ 6 | import examples.hybrid.history.{HybridHistory, HybridSyncInfo, HybridSyncInfoMessageSpec} 7 | import examples.hybrid.state.HBoxStoredState 8 | import scorex.core.consensus.SyncInfo 9 | import scorex.core.transaction.box.proposition.PublicKey25519Proposition 10 | 11 | trait HybridTypes { 12 | 13 | type P = PublicKey25519Proposition 14 | type TX = SimpleBoxTransaction 15 | type PM = HybridBlock 16 | type SI = SyncInfo 17 | type HSI = HybridSyncInfo 18 | type SIS = HybridSyncInfoMessageSpec.type 19 | 20 | type NODE = HybridNodeViewHolder 21 | type ST = HBoxStoredState 22 | type HT = HybridHistory 23 | type MP = SimpleBoxTransactionMemPool 24 | 25 | } -------------------------------------------------------------------------------- /examples/src/test/scala/hybrid/NodeViewHolderGenerators.scala: -------------------------------------------------------------------------------- 1 | package hybrid 2 | 3 | import akka.actor.{ActorRef, ActorSystem, Props} 4 | import akka.testkit.TestProbe 5 | import examples.commons.SimpleBoxTransactionMemPool 6 | import examples.hybrid.HybridNodeViewHolder 7 | import examples.hybrid.wallet.HBoxWallet 8 | import io.iohk.iodb.ByteArrayWrapper 9 | import scorex.core._ 10 | import scorex.core.utils.NetworkTimeProvider 11 | 12 | import scala.concurrent.ExecutionContext.Implicits.global 13 | 14 | @SuppressWarnings(Array("org.wartremover.warts.TraversableOps")) 15 | trait NodeViewHolderGenerators { 16 | this: ModifierGenerators with StateGenerators with HistoryGenerators with HybridTypes => 17 | 18 | class NodeViewHolderForTests(h: HT, s: ST) extends HybridNodeViewHolder(settings, new NetworkTimeProvider(settings.scorexSettings.ntp)) { 19 | 20 | override protected def genesisState: (HIS, MS, VL, MP) = { 21 | @SuppressWarnings(Array("org.wartremover.warts.OptionPartial")) 22 | val store = lsmStoreGen.sample.get 23 | val seed = Array.fill(10)(1: Byte) 24 | val gw = new HBoxWallet(seed, store) 25 | (h, s, gw, SimpleBoxTransactionMemPool.emptyPool) 26 | } 27 | 28 | override def restoreState(): Option[(HIS, MS, VL, MP)] = None 29 | } 30 | 31 | object NodeViewHolderForTests { 32 | def props(h: HT, s: ST): Props = Props(new NodeViewHolderForTests(h, s)) 33 | } 34 | 35 | def nodeViewHolder(implicit system: ActorSystem): (ActorRef, TestProbe, PM, ST, HT) = { 36 | @SuppressWarnings(Array("org.wartremover.warts.OptionPartial")) 37 | val h = historyGen.sample.get 38 | @SuppressWarnings(Array("org.wartremover.warts.OptionPartial")) 39 | val sRaw = stateGen.sample.get 40 | val v = h.openSurfaceIds().last 41 | sRaw.store.update(ByteArrayWrapper(idToBytes(v)), Seq(), Seq()) 42 | val s = sRaw.copy(version = idToVersion(v)) 43 | val ref = system.actorOf(NodeViewHolderForTests.props(h, s)) 44 | val m = totallyValidModifier(h, s) 45 | val eventListener = TestProbe() 46 | (ref, eventListener, m, s, h) 47 | } 48 | } -------------------------------------------------------------------------------- /examples/src/test/scala/hybrid/NodeViewHolderSpec.scala: -------------------------------------------------------------------------------- 1 | package hybrid 2 | 3 | import examples.commons.{SimpleBoxTransaction, SimpleBoxTransactionMemPool} 4 | import examples.hybrid.blocks.HybridBlock 5 | import examples.hybrid.history.{HybridHistory, HybridSyncInfo} 6 | import examples.hybrid.state.HBoxStoredState 7 | import examples.hybrid.wallet.HBoxWallet 8 | import scorex.testkit.properties.NodeViewHolderTests 9 | 10 | class NodeViewHolderSpec extends NodeViewHolderTests[SimpleBoxTransaction, HybridBlock, HBoxStoredState, 11 | HybridSyncInfo, HybridHistory, SimpleBoxTransactionMemPool] 12 | with HybridGenerators { 13 | type VL = HBoxWallet 14 | } 15 | -------------------------------------------------------------------------------- /examples/src/test/scala/hybrid/NodeViewSynchronizerSpec.scala: -------------------------------------------------------------------------------- 1 | package hybrid 2 | 3 | import examples.commons.{SimpleBoxTransaction, SimpleBoxTransactionMemPool} 4 | import examples.hybrid.blocks.HybridBlock 5 | import examples.hybrid.history.{HybridHistory, HybridSyncInfo} 6 | import examples.hybrid.state.HBoxStoredState 7 | import scorex.testkit.properties.NodeViewSynchronizerTests 8 | 9 | class NodeViewSynchronizerSpec 10 | extends NodeViewSynchronizerTests[SimpleBoxTransaction, HybridBlock, HBoxStoredState, HybridSyncInfo, 11 | HybridHistory, SimpleBoxTransactionMemPool] with HybridGenerators { 12 | 13 | override lazy val memPool: SimpleBoxTransactionMemPool = SimpleBoxTransactionMemPool.emptyPool 14 | } 15 | -------------------------------------------------------------------------------- /examples/src/test/scala/hybrid/StateGenerators.scala: -------------------------------------------------------------------------------- 1 | package hybrid 2 | 3 | import examples.commons.{Nonce, PublicKey25519NoncedBox, Value} 4 | import examples.hybrid.state.HBoxStoredState 5 | import io.iohk.iodb.LSMStore 6 | import org.scalacheck.Gen 7 | import scorex.core.transaction.box.proposition.PublicKey25519Proposition 8 | import scorex.core.transaction.state.{BoxStateChanges, Insertion, PrivateKey25519Companion} 9 | import scorex.testkit.generators.CoreGenerators 10 | 11 | import scala.util.Random 12 | 13 | trait StateGenerators extends StoreGenerators { this: HybridGenerators with CoreGenerators with StoreGenerators => 14 | 15 | private val valueSeed = 5000000 16 | 17 | @SuppressWarnings(Array("org.wartremover.warts.OptionPartial")) 18 | val stateGen: Gen[HBoxStoredState] = 19 | for { 20 | dir <- tempDirGen 21 | keepVersions <- Gen.chooseNum(5, 20) 22 | } yield { 23 | def randomBox(): PublicKey25519NoncedBox = { 24 | val value: Value = Value @@ (Random.nextInt(valueSeed) + valueSeed).toLong 25 | val nonce: Nonce = Nonce @@ Random.nextLong() 26 | val keyPair = privKey(value) 27 | PublicKey25519NoncedBox(keyPair._2, nonce, value) 28 | .ensuring(box => PrivateKey25519Companion.owns(keyPair._1, box)) 29 | } 30 | 31 | val store = new LSMStore(dir, keepVersions = keepVersions) 32 | val s0 = HBoxStoredState(store, versionTagGen.sample.get) 33 | val inserts = (1 to 10000) 34 | .map(_ => Insertion[PublicKey25519Proposition, PublicKey25519NoncedBox](randomBox())) 35 | s0.applyChanges(BoxStateChanges(inserts), versionTagGen.sample.get).get 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /examples/src/test/scala/hybrid/StoreGenerators.scala: -------------------------------------------------------------------------------- 1 | package hybrid 2 | 3 | import io.iohk.iodb.{LSMStore, QuickStore} 4 | import org.scalacheck.Gen 5 | import scorex.testkit.utils.FileUtils 6 | 7 | trait StoreGenerators extends FileUtils { 8 | 9 | protected val minKeepVersions = 10 10 | protected val maxKeepVersions = 20 11 | 12 | protected lazy val keepVersionsGen = Gen.chooseNum(minKeepVersions, maxKeepVersions) 13 | 14 | lazy val lsmStoreGen: Gen[LSMStore] = for { 15 | dir <- tempDirGen 16 | keepVersions <- keepVersionsGen 17 | } yield new LSMStore(dir, keepVersions = keepVersions) 18 | 19 | lazy val quickStoreGen: Gen[QuickStore] = for { 20 | dir <- tempDirGen 21 | keepVersions <- keepVersionsGen 22 | } yield new QuickStore(dir, keepVersions = keepVersions) 23 | 24 | } 25 | -------------------------------------------------------------------------------- /examples/src/test/scala/hybrid/primitives/PrivateKey25519Suite.scala: -------------------------------------------------------------------------------- 1 | package hybrid.primitives 2 | 3 | import examples.commons.PublicKey25519NoncedBox 4 | import hybrid.HybridGenerators 5 | import org.scalatest.matchers.should.Matchers 6 | import org.scalatest.propspec.AnyPropSpec 7 | import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks 8 | import scorex.core.transaction.state.PrivateKey25519Companion 9 | 10 | @SuppressWarnings(Array("org.wartremover.warts.OptionPartial")) 11 | class PrivateKey25519Suite extends AnyPropSpec 12 | with ScalaCheckPropertyChecks 13 | with Matchers 14 | with HybridGenerators { 15 | 16 | property("Public key is deterministic") { 17 | forAll(genBytes(32), minSuccessful(1000)){ seed => 18 | val pair1 = PrivateKey25519Companion.generateKeys(seed) 19 | val pair2 = PrivateKey25519Companion.generateKeys(seed) 20 | 21 | pair1._1.privKeyBytes shouldBe pair2._1.privKeyBytes 22 | pair1._1.publicKeyBytes shouldBe pair2._1.publicKeyBytes 23 | pair1._1.publicKeyBytes shouldBe pair2._2.pubKeyBytes 24 | pair1._1.publicKeyBytes shouldBe pair2._2.bytes 25 | } 26 | } 27 | 28 | property("PublicKey25519NoncedBox right owner") { 29 | forAll(key25519Gen, nonceGen, valueGen, minSuccessful(1000)){ case(keyPair, nonce, amount) => 30 | val box = PublicKey25519NoncedBox(keyPair._2, nonce, amount) 31 | PrivateKey25519Companion.owns(keyPair._1, box) shouldBe true 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/src/test/scala/hybrid/state/HBoxStoredStateSpecification.scala: -------------------------------------------------------------------------------- 1 | package hybrid.state 2 | 3 | import examples.hybrid.blocks.HybridBlock 4 | import examples.hybrid.state.HBoxStoredState 5 | import hybrid.HybridGenerators 6 | import org.scalatest.matchers.should.Matchers 7 | import org.scalatest.propspec.AnyPropSpec 8 | import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks 9 | import scorex.core.transaction.state.{Insertion, Removal} 10 | import scorex.testkit.properties.state.StateTests 11 | 12 | @SuppressWarnings(Array("org.wartremover.warts.OptionPartial")) 13 | class HBoxStoredStateSpecification extends AnyPropSpec 14 | with ScalaCheckPropertyChecks 15 | with Matchers 16 | with HybridGenerators 17 | with StateTests[HybridBlock, HBoxStoredState] { 18 | 19 | property("added boxes are always there") { 20 | forAll(stateGen){state => 21 | var st = state 22 | check(checksToMake) { _ => 23 | val c = stateChangesGenerator(state).sample.get 24 | st = st.applyChanges(c, versionTagGen.sample.get).get 25 | c.toAppend.foreach { case Insertion(b) => 26 | st.closedBox(b.id) shouldBe Some(b) 27 | } 28 | c.toRemove.foreach { case Removal(bid) => 29 | st.closedBox(bid) shouldBe None 30 | } 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/src/test/scala/hybrid/transaction/TransactionSuite.scala: -------------------------------------------------------------------------------- 1 | package hybrid.transaction 2 | 3 | import examples.commons.SimpleBoxTransaction 4 | import hybrid.HybridGenerators 5 | import io.iohk.iodb.ByteArrayWrapper 6 | import org.scalatest.matchers.should.Matchers 7 | import org.scalatest.propspec.AnyPropSpec 8 | import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks 9 | import scorex.core.transaction.state.PrivateKey25519Companion 10 | 11 | 12 | class TransactionSuite extends AnyPropSpec 13 | with ScalaCheckPropertyChecks 14 | with Matchers 15 | with HybridGenerators { 16 | 17 | property("SimpleBoxTransaction preservers inputs ids") { 18 | forAll(noncedBoxWithKeyListGen, noncedBoxWithKeyListGen) { case (boxesWithKeysIn, boxesWithKeysOut) => 19 | 20 | val inputs = boxesWithKeysIn.map { case (box, k) => k -> box.nonce }.toIndexedSeq 21 | 22 | boxesWithKeysIn.foreach { case (box, k) => 23 | PrivateKey25519Companion.owns(k, box) shouldBe true 24 | } 25 | 26 | val boxIds = boxesWithKeysIn.map(_._1.id).map(ByteArrayWrapper.apply) 27 | 28 | val to = boxesWithKeysOut.map { case (box, _) => 29 | box.proposition -> box.value 30 | }.toIndexedSeq 31 | 32 | val tx: SimpleBoxTransaction = SimpleBoxTransaction(inputs, to, 0, 0) 33 | 34 | val outKeys = boxesWithKeysOut.map(_._2).map(_.publicKeyBytes).map(ByteArrayWrapper.apply) 35 | 36 | tx.newBoxes.foreach { newBox => 37 | outKeys.contains(ByteArrayWrapper(newBox.proposition.pubKeyBytes)) shouldBe true 38 | } 39 | 40 | tx.boxIdsToOpen.map(ByteArrayWrapper.apply).forall { bid => 41 | boxIds.contains(bid) 42 | } shouldBe true 43 | } 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /examples/src/test/scala/hybrid/validation/SemanticBlockValidatorSpecification.scala: -------------------------------------------------------------------------------- 1 | package hybrid.validation 2 | 3 | import examples.hybrid.validation.SemanticBlockValidator 4 | import hybrid.HybridGenerators 5 | import org.scalatest.matchers.should.Matchers 6 | import org.scalatest.propspec.AnyPropSpec 7 | import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks 8 | import scorex.crypto.hash.Blake2b256 9 | 10 | 11 | class SemanticBlockValidatorSpecification extends AnyPropSpec 12 | with ScalaCheckPropertyChecks 13 | with Matchers 14 | with HybridGenerators { 15 | 16 | private val validator = new SemanticBlockValidator(Blake2b256) 17 | 18 | property("Generated PoS block semantics is valid") { 19 | forAll(posBlockGen) { posBlock => 20 | validator.validate(posBlock).get 21 | } 22 | } 23 | 24 | property("Generated PoW block semantics is valid") { 25 | forAll(powBlockGen) { powBlock => 26 | validator.validate(powBlock).get 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/src/test/scala/spv/SPVGenerators.scala: -------------------------------------------------------------------------------- 1 | package spv 2 | 3 | import commons.ExamplesCommonGenerators 4 | import examples.spv._ 5 | import org.scalacheck.{Arbitrary, Gen} 6 | import scorex.util.ModifierId 7 | 8 | trait SPVGenerators extends ExamplesCommonGenerators { 9 | 10 | val blockHeaderGen: Gen[Header] = for { 11 | parentId: ModifierId <- modifierIdGen 12 | innerchainLinks: Seq[ModifierId] <- Gen.listOf(modifierIdGen).map(_.take(128)) 13 | txRoot: Array[Byte] <- genBytes(Constants.hashfn.DigestSize) 14 | stateRoot: Array[Byte] <- genBytes(Constants.hashfn.DigestSize) 15 | timestamp: Long <- Arbitrary.arbitrary[Long].map(t => Math.abs(t)) 16 | powNonce: Int <- Arbitrary.arbitrary[Int] 17 | } yield Header(parentId, innerchainLinks, stateRoot, txRoot, timestamp, powNonce) 18 | 19 | } 20 | -------------------------------------------------------------------------------- /examples/src/test/scala/spv/serialization/SerializationTests.scala: -------------------------------------------------------------------------------- 1 | package spv.serialization 2 | 3 | import examples.spv.{Header, HeaderSerializer} 4 | import org.scalatest.matchers.should.Matchers 5 | import org.scalatest.propspec.AnyPropSpec 6 | import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks 7 | import spv.SPVGenerators 8 | 9 | class SerializationTests extends AnyPropSpec 10 | with ScalaCheckPropertyChecks 11 | with Matchers 12 | with SPVGenerators { 13 | 14 | property("BlockHeader serialization") { 15 | forAll(blockHeaderGen) { b: Header => 16 | val serializer = HeaderSerializer 17 | val parsed = serializer.parseBytes(serializer.toBytes(b)) 18 | serializer.toBytes(b) shouldEqual serializer.toBytes(parsed) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/src/test/scala/trimchain/TrimchainGenerators.scala: -------------------------------------------------------------------------------- 1 | package trimchain 2 | 3 | import commons.ExamplesCommonGenerators 4 | import examples.commons.SimpleBoxTransaction 5 | import examples.trimchain.core._ 6 | import examples.trimchain.modifiers.{BlockHeader, TBlock} 7 | import org.scalacheck.{Arbitrary, Gen} 8 | import scorex.util.ModifierId 9 | import scorex.crypto.authds.SerializedAdProof 10 | 11 | trait TrimchainGenerators extends ExamplesCommonGenerators { 12 | 13 | lazy val stateRootGen: Gen[StateRoot] = genBytes(Constants.StateRootLength).map(r => StateRoot @@ r) 14 | lazy val txRootGen: Gen[TransactionsRoot] = genBytes(Constants.TxRootLength).map(r => TransactionsRoot @@ r) 15 | 16 | val ticketGen: Gen[Ticket] = for { 17 | minerKey: Array[Byte] <- genBytes(TicketSerializer.MinerKeySize) 18 | partialProofs: Seq[SerializedAdProof] <- Gen.nonEmptyListOf(nonEmptyBytesGen).map(b => SerializedAdProof @@ b) 19 | } yield Ticket(minerKey, partialProofs) 20 | 21 | val blockHeaderGen: Gen[BlockHeader] = for { 22 | parentId: ModifierId <- modifierIdGen 23 | stateRoot: StateRoot <- stateRootGen 24 | txRoot: TransactionsRoot <- txRootGen 25 | powNonce: Long <- Arbitrary.arbitrary[Long] 26 | ticket: Ticket <- ticketGen 27 | } yield BlockHeader(parentId, stateRoot, txRoot, ticket, powNonce) 28 | 29 | val TBlockGen: Gen[TBlock] = for { 30 | header: BlockHeader <- blockHeaderGen 31 | body: Seq[SimpleBoxTransaction] <- Gen.listOf(simpleBoxTransactionGen) 32 | timestamp: Long <- Arbitrary.arbitrary[Long] 33 | } yield TBlock(header, body, timestamp) 34 | } 35 | -------------------------------------------------------------------------------- /examples/src/test/scala/trimchain/serialization/SerializationTests.scala: -------------------------------------------------------------------------------- 1 | package trimchain.serialization 2 | 3 | import examples.trimchain.core.{Ticket, TicketSerializer} 4 | import examples.trimchain.modifiers.{BlockHeader, BlockHeaderSerializer, TBlock, TBlockSerializer} 5 | import org.scalatest.matchers.should.Matchers 6 | import org.scalatest.propspec.AnyPropSpec 7 | import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks 8 | import trimchain.TrimchainGenerators 9 | 10 | class SerializationTests extends AnyPropSpec 11 | with ScalaCheckPropertyChecks 12 | with Matchers 13 | with TrimchainGenerators { 14 | 15 | property("Ticket serialization") { 16 | forAll(ticketGen) { b: Ticket => 17 | val serializer = TicketSerializer 18 | val parsed = serializer.parseBytes(serializer.toBytes(b)) 19 | b.minerKey shouldEqual parsed.minerKey 20 | serializer.toBytes(b) shouldEqual serializer.toBytes(parsed) 21 | } 22 | } 23 | 24 | property("BlockHeader serialization") { 25 | forAll(blockHeaderGen) { b: BlockHeader => 26 | val serializer = BlockHeaderSerializer 27 | val parsed = serializer.parseBytes(serializer.toBytes(b)) 28 | serializer.toBytes(b) shouldEqual serializer.toBytes(parsed) 29 | } 30 | } 31 | 32 | property("TBlock serialization") { 33 | forAll(TBlockGen) { b: TBlock => 34 | val serializer = TBlockSerializer 35 | val parsed = serializer.parseBytes(serializer.toBytes(b)) 36 | serializer.toBytes(b) shouldEqual serializer.toBytes(parsed) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.2.8 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | // Comment to get more information during initialization 2 | logLevel := Level.Warn 3 | 4 | // The Typesafe repository 5 | resolvers += "Typesafe repository" at "https://repo.typesafe.com/typesafe/releases/" 6 | 7 | addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.5") 8 | 9 | libraryDependencies += "com.typesafe" % "config" % "1.3.0" 10 | 11 | addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.6") 12 | 13 | addSbtPlugin("com.jsuereth" % "sbt-pgp" % "2.0.0") 14 | 15 | addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.5.1") 16 | 17 | addSbtPlugin("org.scoverage" % "sbt-coveralls" % "1.2.2") 18 | 19 | addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.9.0") 20 | 21 | addSbtPlugin("com.github.sbt" % "sbt-findbugs" % "2.0.0") 22 | 23 | addSbtPlugin("org.wartremover" % "sbt-wartremover" % "2.2.1") 24 | 25 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.8") 26 | 27 | addSbtPlugin("com.dwijnand" % "sbt-dynver" % "4.1.1") 28 | -------------------------------------------------------------------------------- /release-notes.md: -------------------------------------------------------------------------------- 1 | 2.0.0-RC4 2 | --------- 3 | * *modifierCompanions* renamed to *modifierSerializers* in *NodeViewHolder* 4 | 5 | 2.0.0-RC3 6 | --------- 7 | * *MinimalState* interface simplification: *validate()* puled away from the basic trait 8 | * *maxRollback* field added to *MinimalState* 9 | * No *transactions* field with an optional value in *PersistentNodeViewModifier*, 10 | use *TransactionsCarryingPersistentNodeViewModifier* descendant for modifiers with transactions. 11 | * Non-exhaustive pattern-matching fix in *NodeViewholder.pmodModify()* 12 | * Simplification of type parameters in many classes around the whole codebase 13 | * *FastCryptographicHash* removed 14 | * Some obsolete code removed, such as *temp/mining* folder, *ScoreObserver* class 15 | * Scrypto 2.0.0 16 | * Using tagged types instead of *Array[Byte]*, *suppertagged* microframework is used for that 17 | 18 | 2.0.0-RC2 19 | --------- 20 | * *MinimalState* interface made minimal 21 | * protocolVersion in P2P Handshake 22 | * Scrypto 1.2.3 23 | * *BoxMinimalState* moved to *scorex.mid.state* 24 | 25 | 2.0.0-RC1 26 | --------- 27 | * Transaction interface simplified (*fee* & *timestamp* fields removed) 28 | * Scala 2.12 29 | * IODB 0.3.1 30 | * *reportInvalid()* in History 31 | * Issue #19 fixed 32 | * MapDB dependency removed 33 | * StateChanges reworked 34 | * TwinsCoin example improved 35 | 36 | 2.0.0-M4 37 | -------- 38 | 39 | * IODB dependency upgraded to 0.2.+ 40 | * TwinsChain example massively improved, README on it has been added 41 | (see "examples" folder) 42 | * akka-http dependency removed, Swagger updated 43 | 44 | 45 | 2.0.0-M3 46 | -------- 47 | 48 | Serialization have been reworked throughout the core to be consistent 49 | (see the new Serializer interface). 50 | 51 | Handshake timeout implemented: (p2p/handshakeTimeout in settings.json) 52 | Agent name and version have been moved to settings.json 53 | ("agent" and "version" settings) 54 | 55 | Hybrid chain example got bugfixing and new tests. 56 | 57 | 58 | 2.0.0-M2 59 | -------- 60 | 61 | * Wallet interface added 62 | * MvStore dependency removed 63 | * StoredBlockchain trait removed 64 | * ViewSynchronizer trait removed 65 | * Miner and MiningController are removed from the core 66 | * Maven artefact has been renamed from "scorex-basics" to "scorex-core" 67 | * blockFields method of a Block has been removed -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | scorex-errors.log 5 | true 6 | 7 | %date{MM/dd HH:mm:ss} %-5level[%.15thread] %logger{1} %msg%n 8 | 9 | 10 | WARN 11 | 12 | 13 | 14 | 15 | scorex.log 16 | true 17 | 18 | %date{MM/dd HH:mm:ss} %-5level[%.15thread] %logger{1} %msg%n 19 | 20 | 21 | 22 | scorex.%d{yyyy-MM-dd}.%i.log 23 | 24 | 100MB 25 | 60 26 | 20GB 27 | 28 | 29 | 30 | 31 | System.out 32 | 33 | INFO 34 | 35 | 36 | [%thread] >> [%-5level] %logger{36} >> %d{HH:mm:ss.SSS} %msg%n 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/main/resources/swagger-ui/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperledger-labs/Scorex/9e1cd58a5589a9062dd9974f1884cc640c1d51b3/src/main/resources/swagger-ui/favicon-16x16.png -------------------------------------------------------------------------------- /src/main/resources/swagger-ui/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperledger-labs/Scorex/9e1cd58a5589a9062dd9974f1884cc640c1d51b3/src/main/resources/swagger-ui/favicon-32x32.png -------------------------------------------------------------------------------- /src/main/resources/swagger-ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Swagger UI 7 | 8 | 9 | 10 | 31 | 32 | 33 | 34 |
35 | 36 | 37 | 38 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /src/main/resources/swagger-ui/oauth2-redirect.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 68 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/NodeViewComponent.scala: -------------------------------------------------------------------------------- 1 | package scorex.core 2 | 3 | trait NodeViewComponent { 4 | self => 5 | 6 | type NVCT >: self.type <: NodeViewComponent 7 | } 8 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/NodeViewModifier.scala: -------------------------------------------------------------------------------- 1 | package scorex.core 2 | 3 | import com.typesafe.config.ConfigFactory 4 | import scorex.core.serialization.BytesSerializable 5 | import scorex.core.transaction.Transaction 6 | import scorex.core.utils.ScorexEncoding 7 | 8 | import scala.util.Try 9 | 10 | sealed trait NodeViewModifier extends BytesSerializable with ScorexEncoding { 11 | self => 12 | 13 | val modifierTypeId: ModifierTypeId 14 | 15 | //todo: check statically or dynamically output size 16 | def id: scorex.util.ModifierId 17 | 18 | def encodedId: String = encoder.encodeId(id) 19 | 20 | override def equals(obj: scala.Any): Boolean = obj match { 21 | case that: NodeViewModifier => (that.id == id) && (that.modifierTypeId == modifierTypeId) 22 | case _ => false 23 | } 24 | } 25 | 26 | trait EphemerealNodeViewModifier extends NodeViewModifier 27 | 28 | /** 29 | * It is supposed that all the modifiers (offchain transactions, blocks, blockheaders etc) 30 | * have identifiers of the some length fixed with the ModifierIdSize constant 31 | */ 32 | object NodeViewModifier { 33 | private val DefaultIdSize: Byte = 32 // in bytes 34 | 35 | val ModifierIdSize: Int = Try(ConfigFactory.load().getConfig("app").getInt("modifierIdSize")).getOrElse(DefaultIdSize) 36 | } 37 | 38 | /** 39 | * Persistent node view modifier is a part of replicated log of state transformations. 40 | * The log should be deterministic and ordered to get a deterministic state after its 41 | * application to the genesis state and thus every modifier should have parent - modifier, 42 | * which application should precede this modifier application 43 | */ 44 | trait PersistentNodeViewModifier extends NodeViewModifier { 45 | /** 46 | * Id modifier, which should be applied to the node view before this modifier 47 | */ 48 | def parentId: scorex.util.ModifierId 49 | } 50 | 51 | 52 | trait TransactionsCarryingPersistentNodeViewModifier[TX <: Transaction] 53 | extends PersistentNodeViewModifier { 54 | 55 | def transactions: Seq[TX] 56 | } 57 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/api/client/ApiClient.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.api.client 2 | 3 | import java.net.{HttpURLConnection, URL} 4 | 5 | import scorex.core.settings.{RESTApiSettings, ScorexSettings} 6 | 7 | import scala.io.{Source, StdIn} 8 | import scala.util.{Failure, Success, Try} 9 | 10 | 11 | class ApiClient(settings: RESTApiSettings) { 12 | private val OkHttpCode = 200 13 | 14 | def executeCommand(command: String): String = { 15 | if (command.equals("help")) { 16 | " \n Type quit to stop." 17 | } else Try { 18 | val args = command.split(" ") 19 | val method = args.head.toUpperCase 20 | val path = args(1) 21 | 22 | val content = if (method.equals("POST")) { 23 | command.substring((method + " " + path + " ").length()) 24 | } else "" 25 | 26 | // fixme: magic string "http://127.0.0.1:" should come from settings. 27 | // fixme: Shouldn' we use the address from the bind address instead of 127.0.0.1? 28 | val url = new URL("http://127.0.0.1:" + settings.bindAddress.getPort + "/" + path) 29 | val connection = url.openConnection().asInstanceOf[HttpURLConnection] 30 | connection.setRequestMethod(method) 31 | 32 | if (method.equals("POST")) { 33 | connection.setDoOutput(true) 34 | connection.getOutputStream.write(content.getBytes) 35 | connection.getOutputStream.flush() 36 | connection.getOutputStream.close() 37 | } 38 | 39 | val stream = connection.getResponseCode match { 40 | case OkHttpCode => connection.getInputStream 41 | case _ => connection.getErrorStream 42 | } 43 | 44 | val result = Source.fromInputStream(stream).mkString("") 45 | Try(result) 46 | }.flatten match { 47 | case Success(result) => result 48 | case Failure(e) => 49 | s"Problem occurred $e! \n Type help to get a list of commands." 50 | } 51 | } 52 | } 53 | 54 | object ApiClient extends App { 55 | private val settingsFilename = args.headOption.getOrElse("settings.conf") 56 | private val settings = ScorexSettings.read(Some(settingsFilename)) 57 | private val apiClient = new ApiClient(settings.restApi) 58 | 59 | println("Welcome to the Scorex command-line client...") 60 | Iterator.continually(StdIn.readLine()).takeWhile(!_.equals("quit")).foreach { command => 61 | println(s"[$command RESULT] " + apiClient.executeCommand(command)) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/api/http/ApiDirectives.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.api.http 2 | 3 | import akka.http.scaladsl.server.{AuthorizationFailedRejection, Directive0} 4 | import scorex.core.settings.RESTApiSettings 5 | import scorex.core.utils.ScorexEncoding 6 | import scorex.crypto.hash.Blake2b256 7 | 8 | trait ApiDirectives extends CorsHandler with ScorexEncoding { 9 | val settings: RESTApiSettings 10 | val apiKeyHeaderName: String 11 | 12 | lazy val withAuth: Directive0 = optionalHeaderValueByName(apiKeyHeaderName).flatMap { 13 | case _ if settings.apiKeyHash.isEmpty => pass 14 | case None => reject(AuthorizationFailedRejection) 15 | case Some(key) => 16 | val keyHashStr: String = encoder.encode(Blake2b256(key)) 17 | if (settings.apiKeyHash.contains(keyHashStr)) pass 18 | else reject(AuthorizationFailedRejection) 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/api/http/ApiError.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.api.http 2 | 3 | import akka.http.scaladsl.model.{ContentTypes, HttpEntity, StatusCode, StatusCodes} 4 | import akka.http.scaladsl.server.{Directives, Route} 5 | import io.circe.syntax._ 6 | 7 | import scala.language.implicitConversions 8 | 9 | case class ApiError(statusCode: StatusCode, reason: String = "") { 10 | 11 | def apply(detail: String): Route = complete(detail) 12 | 13 | def defaultRoute: Route = complete() 14 | 15 | def complete(detail: String = ""): Route = { 16 | val nonEmptyReason = if (reason.isEmpty) statusCode.reason else reason 17 | val detailOpt = if (detail.isEmpty) None else Some(detail) 18 | val response = Map( 19 | "error" -> statusCode.intValue.asJson, 20 | "reason" -> nonEmptyReason.asJson, 21 | "detail" -> detailOpt.asJson 22 | ).asJson 23 | val entity = HttpEntity(ContentTypes.`application/json`, response.spaces2) 24 | Directives.complete(statusCode.intValue() -> entity) 25 | } 26 | } 27 | 28 | object ApiError { 29 | 30 | def apply(s: String): Route = InternalError(s) 31 | def apply(e: Throwable): Route = InternalError(safeMessage(e)) 32 | def apply(causes: Seq[Throwable]): Route = InternalError(mkString(causes)) 33 | def mkString(causes: Seq[Throwable]): String = causes.map(safeMessage).mkString(", ") 34 | private def safeMessage(e: Throwable): String = Option(e.getMessage).getOrElse(e.toString) 35 | 36 | implicit def toRoute(error: ApiError): Route = error.defaultRoute 37 | 38 | object InternalError extends ApiError(StatusCodes.InternalServerError, "internal.error") 39 | object InvalidJson extends ApiError(StatusCodes.BadRequest, "invalid.json") 40 | object BadRequest extends ApiError(StatusCodes.BadRequest, "bad.request") 41 | object ApiKeyNotValid extends ApiError(StatusCodes.Forbidden, "invalid.api-key") 42 | object NotExists extends ApiError(StatusCodes.NotFound, "not-found") 43 | object Forbidden extends ApiError(StatusCodes.Forbidden, "forbidden") 44 | } 45 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/api/http/ApiErrorHandler.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.api.http 2 | 3 | import akka.http.scaladsl.server.ExceptionHandler 4 | 5 | import scala.util.control.NonFatal 6 | 7 | object ApiErrorHandler { 8 | 9 | implicit val exceptionHandler: ExceptionHandler = ExceptionHandler { 10 | case NonFatal(e) => ApiError(e) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/api/http/ApiRejectionHandler.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.api.http 2 | 3 | import akka.http.scaladsl.server._ 4 | 5 | object ApiRejectionHandler { 6 | 7 | implicit val rejectionHandler: RejectionHandler = RejectionHandler.newBuilder() 8 | .handleAll[SchemeRejection] { rejections => 9 | val schemes = rejections.map(_.supported).mkString(", ") 10 | ApiError.BadRequest(s"Uri scheme not allowed, supported schemes: $schemes") 11 | } 12 | .handle { 13 | case AuthorizationFailedRejection => 14 | ApiError.Forbidden("The supplied authentication is not authorized to access this resource") 15 | } 16 | .handle { 17 | case MalformedRequestContentRejection(msg, _) => 18 | ApiError.BadRequest("The request content was malformed:\n" + msg) 19 | } 20 | .handle { 21 | case InvalidOriginRejection(allowedOrigins) => 22 | ApiError.Forbidden(s"Allowed `Origin` header values: ${allowedOrigins.mkString(", ")}") 23 | } 24 | .handle { 25 | case MissingQueryParamRejection(paramName) => 26 | ApiError.NotExists(s"Request is missing required query parameter '$paramName'") 27 | } 28 | .handle { 29 | case RequestEntityExpectedRejection => 30 | ApiError.BadRequest("Request entity expected but not supplied") 31 | } 32 | .handle { case ValidationRejection(msg, _) => ApiError.BadRequest(msg) } 33 | .handle { case x => ApiError.InternalError(s"Unhandled rejection: $x") } 34 | .handleNotFound { ApiError.NotExists("The requested resource could not be found.") } 35 | .result() 36 | } 37 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/api/http/ApiResponse.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.api.http 2 | 3 | import akka.http.scaladsl.model.{ContentTypes, HttpEntity, StatusCode, StatusCodes} 4 | import akka.http.scaladsl.server.{Directives, Route} 5 | import io.circe.syntax._ 6 | import io.circe.{Encoder, Json} 7 | 8 | import scala.concurrent.Future 9 | import scala.language.implicitConversions 10 | 11 | /** Bunch of methods to wrap json result to route with the given status code, completing it. 12 | * When receiving `Null` json, then complete route with `404 not found` status code. 13 | */ 14 | class ApiResponse(statusCode: StatusCode) { 15 | 16 | def apply(result: Json): Route = withJson(result) 17 | def apply[R: Encoder](result: R): Route = withJson(result) 18 | def apply[R: Encoder](result: Future[R]): Route = Directives.onSuccess(result)(withJson) 19 | def apply(result: Future[Json]): Route = Directives.onSuccess(result)(withJson) 20 | 21 | def defaultRoute: Route = withString(defaultMessage) 22 | def defaultMessage: String = statusCode.reason 23 | 24 | def withString(s: String): Route = complete(s.asJson) 25 | 26 | def withJson[R](result: R)(implicit encoder: Encoder[R]): Route = complete(encoder(result)) 27 | 28 | def complete(result: Json): Route = { 29 | if (result.isNull) { 30 | ApiError.NotExists 31 | } else { 32 | val httpEntity = HttpEntity(ContentTypes.`application/json`, result.spaces2) 33 | Directives.complete(statusCode.intValue() -> httpEntity) 34 | } 35 | } 36 | } 37 | 38 | object ApiResponse { 39 | implicit def toRoute(jsonRoute: ApiResponse): Route = jsonRoute.defaultRoute 40 | 41 | def apply[R: Encoder](result: R): Route = OK(result) 42 | def apply[R: Encoder](result: Future[R]): Route = OK(result) 43 | def apply(result: Either[Throwable, Json]): Route = result.fold(ApiError.apply, OK.apply) 44 | def apply(keyValues: (String, Json)*): Route = apply(Map(keyValues: _*)) 45 | def apply(keyValue: (String, String)): Route = apply(Map(keyValue)) 46 | 47 | object OK extends ApiResponse(StatusCodes.OK) 48 | } 49 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/api/http/ApiRoute.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.api.http 2 | 3 | import akka.actor.ActorRefFactory 4 | import akka.http.scaladsl.server.Route 5 | import akka.http.scaladsl.unmarshalling.PredefinedFromEntityUnmarshallers 6 | import akka.util.Timeout 7 | import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport 8 | import io.circe.Printer 9 | import scorex.core.utils.ActorHelper 10 | import scorex.util.ScorexLogging 11 | 12 | import scala.language.implicitConversions 13 | 14 | trait ApiRoute 15 | extends ApiDirectives 16 | with ActorHelper 17 | with FailFastCirceSupport 18 | with PredefinedFromEntityUnmarshallers 19 | with ScorexLogging { 20 | 21 | def context: ActorRefFactory 22 | def route: Route 23 | 24 | //TODO: should we move it to the settings? 25 | override val apiKeyHeaderName: String = "api_key" 26 | 27 | implicit val printer: Printer = Printer.spaces2.copy(dropNullValues = true) 28 | implicit lazy val timeout: Timeout = Timeout(settings.timeout) 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/api/http/ApiRouteWithFullView.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.api.http 2 | 3 | import akka.actor.ActorRef 4 | import akka.http.scaladsl.server.Route 5 | import akka.pattern.ask 6 | import scorex.core.NodeViewHolder.CurrentView 7 | 8 | import scala.concurrent.Future 9 | 10 | trait ApiRouteWithFullView[HIS, MS, VL, MP] extends ApiRoute { 11 | 12 | import scorex.core.NodeViewHolder.ReceivableMessages.GetDataFromCurrentView 13 | 14 | val nodeViewHolderRef: ActorRef 15 | 16 | def withNodeView(f: CurrentView[HIS, MS, VL, MP] => Route): Route = onSuccess(viewAsync())(f) 17 | 18 | //TODO Data received in current view is mutable and may be inconsistent. 19 | //Better get concrete data you need from NodeViewHolder 20 | protected def viewAsync(): Future[CurrentView[HIS, MS, VL, MP]] = { 21 | def f(v: CurrentView[HIS, MS, VL, MP]) = v 22 | (nodeViewHolderRef ? GetDataFromCurrentView(f)) 23 | .mapTo[CurrentView[HIS, MS, VL, MP]] 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/api/http/ApiTry.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.api.http 2 | 3 | import akka.http.scaladsl.server.Route 4 | 5 | import scala.util.{Failure, Success, Try} 6 | 7 | object ApiTry { 8 | 9 | def apply(f: => Route): Route = Try { 10 | f 11 | } match { 12 | case Success(r) => r 13 | case Failure(e) => e.printStackTrace(); ApiError(e) 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/api/http/CompositeHttpService.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.api.http 2 | 3 | import akka.actor.ActorSystem 4 | import akka.http.scaladsl.model.StatusCodes 5 | import akka.http.scaladsl.server.Route 6 | import akka.http.scaladsl.server.directives.RouteDirectives 7 | import scorex.core.api.http.swagger.SwaggerConfigRoute 8 | import scorex.core.settings.RESTApiSettings 9 | 10 | case class CompositeHttpService(system: ActorSystem, routes: Seq[ApiRoute], settings: RESTApiSettings, swaggerConf: String) 11 | extends CorsHandler { 12 | 13 | implicit val actorSystem: ActorSystem = system 14 | 15 | val swaggerService: SwaggerConfigRoute = new SwaggerConfigRoute(swaggerConf: String, settings: RESTApiSettings) 16 | 17 | val redirectToSwagger: Route = path("" | "/") { 18 | redirect("/swagger", StatusCodes.PermanentRedirect) 19 | } 20 | 21 | val compositeRoute: Route = 22 | routes.map(_.route).reduceOption(_ ~ _).getOrElse(RouteDirectives.reject) ~ 23 | swaggerService.route ~ 24 | path("swagger")(getFromResource("swagger-ui/index.html")) ~ 25 | getFromResourceDirectory("swagger-ui") ~ 26 | redirectToSwagger 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/api/http/CorsHandler.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.api.http 2 | 3 | import akka.http.scaladsl.marshalling.ToResponseMarshallable.apply 4 | import akka.http.scaladsl.model.HttpMethods._ 5 | import akka.http.scaladsl.model.headers._ 6 | import akka.http.scaladsl.model.{HttpResponse, StatusCodes} 7 | import akka.http.scaladsl.server.Directive.addByNameNullaryApply 8 | import akka.http.scaladsl.server.{Directive0, Directives, Route} 9 | 10 | /** 11 | * Provides tools for handling a Cross-Origin Resource Sharing spec workflow 12 | * (including `OPTIONS` pre-flight requests). 13 | */ 14 | trait CorsHandler extends Directives { 15 | 16 | private val corsResponseHeaders: List[ModeledHeader] = List[ModeledHeader]( 17 | `Access-Control-Allow-Origin`.*, 18 | `Access-Control-Allow-Credentials`(true), 19 | `Access-Control-Allow-Headers`("Authorization", "Content-Type", "X-Requested-With", "api_key") 20 | ) 21 | 22 | def corsHandler(r: Route): Route = addAccessControlHeaders { 23 | preflightRequestHandler ~ r 24 | } 25 | 26 | def addCorsHeaders(response: HttpResponse): HttpResponse = 27 | response.withHeaders(corsResponseHeaders) 28 | 29 | private def addAccessControlHeaders: Directive0 = 30 | respondWithHeaders(corsResponseHeaders) 31 | 32 | private def preflightRequestHandler: Route = options { 33 | complete { 34 | HttpResponse(StatusCodes.OK) 35 | .withHeaders(`Access-Control-Allow-Methods`(OPTIONS, POST, PUT, GET, DELETE)) 36 | } 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/api/http/UtilsApiRoute.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.api.http 2 | 3 | import java.security.SecureRandom 4 | 5 | import akka.actor.ActorRefFactory 6 | import akka.http.scaladsl.server.Route 7 | import io.circe.Json 8 | import scorex.core.settings.RESTApiSettings 9 | import scorex.core.utils.ScorexEncoding 10 | import scorex.crypto.hash.Blake2b256 11 | 12 | 13 | case class UtilsApiRoute(override val settings: RESTApiSettings)(implicit val context: ActorRefFactory) 14 | extends ApiRoute with ScorexEncoding { 15 | 16 | private val SeedSize = 32 17 | 18 | private def seed(length: Int): String = { 19 | val seed = new Array[Byte](length) 20 | new SecureRandom().nextBytes(seed) //seed mutated here! 21 | encoder.encode(seed) 22 | } 23 | 24 | override val route: Route = pathPrefix("utils") { 25 | seedRoute ~ length ~ hashBlake2b 26 | } 27 | 28 | def seedRoute: Route = (get & path("seed")) { 29 | ApiResponse(seed(SeedSize)) 30 | } 31 | 32 | def length: Route = (get & path("seed" / IntNumber)) { length => 33 | ApiResponse(seed(length)) 34 | } 35 | 36 | def hashBlake2b: Route = { 37 | (post & path("hash" / "blake2b") & entity(as[Json])) { json => 38 | json.asString match { 39 | case Some(message) => ApiResponse(encoder.encode(Blake2b256(message))) 40 | case None => ApiError.BadRequest 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/api/http/swagger/SwaggerConfigRoute.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.api.http.swagger 2 | 3 | import akka.actor.ActorRefFactory 4 | import akka.http.scaladsl.model.{ContentTypes, HttpEntity} 5 | import akka.http.scaladsl.server.Route 6 | import scorex.core.api.http.ApiRoute 7 | import scorex.core.settings.RESTApiSettings 8 | 9 | class SwaggerConfigRoute(swaggerConf: String, override val settings: RESTApiSettings)(implicit val context: ActorRefFactory) 10 | extends ApiRoute { 11 | 12 | override val route: Route = { 13 | (get & path("api-docs" / "swagger.conf")) { 14 | complete(HttpEntity(ContentTypes.`application/json`, swaggerConf)) 15 | } 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/app/ScorexContext.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.app 2 | 3 | import java.net.InetSocketAddress 4 | 5 | import scorex.core.network.{PeerFeature, UPnPGateway} 6 | import scorex.core.network.message.MessageSpec 7 | import scorex.core.utils.TimeProvider 8 | 9 | case class ScorexContext(messageSpecs: Seq[MessageSpec[_]], 10 | features: Seq[PeerFeature], 11 | upnpGateway: Option[UPnPGateway], 12 | timeProvider: TimeProvider, 13 | externalNodeAddress: Option[InetSocketAddress]) 14 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/app/Version.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.app 2 | 3 | import scorex.core.serialization.{BytesSerializable, ScorexSerializer} 4 | import scorex.util.serialization._ 5 | 6 | /** 7 | * Version of p2p protocol. Node can only process messages of it's version or lower. 8 | */ 9 | case class Version(firstDigit: Byte, secondDigit: Byte, thirdDigit: Byte) extends BytesSerializable with Ordered[Version] { 10 | override type M = Version 11 | 12 | override def serializer: ScorexSerializer[Version] = ApplicationVersionSerializer 13 | 14 | override def compare(that: Version): Int = if (this.firstDigit != that.firstDigit) { 15 | this.firstDigit - that.firstDigit 16 | } else if (this.secondDigit != that.secondDigit) { 17 | this.secondDigit - that.secondDigit 18 | } else { 19 | this.thirdDigit - that.thirdDigit 20 | } 21 | } 22 | 23 | object Version { 24 | def apply(v: String): Version = { 25 | val splitted = v.split("\\.") 26 | Version(splitted(0).toByte, splitted(1).toByte, splitted(2).toByte) 27 | } 28 | 29 | val initial: Version = Version(0, 0, 1) 30 | val last: Version = Version(0, 0, 1) 31 | 32 | } 33 | 34 | object ApplicationVersionSerializer extends ScorexSerializer[Version] { 35 | val SerializedVersionLength: Int = 3 36 | 37 | 38 | override def serialize(obj: Version, w: Writer): Unit = { 39 | w.put(obj.firstDigit) 40 | w.put(obj.secondDigit) 41 | w.put(obj.thirdDigit) 42 | } 43 | 44 | override def parse(r: Reader): Version = { 45 | Version( 46 | r.getByte(), 47 | r.getByte(), 48 | r.getByte() 49 | ) 50 | } 51 | } -------------------------------------------------------------------------------- /src/main/scala/scorex/core/block/Block.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.block 2 | 3 | import scorex.core.block.Block.{Timestamp, Version} 4 | import scorex.core.consensus.History 5 | import scorex.core.serialization.ScorexSerializer 6 | import scorex.core.transaction._ 7 | import scorex.core.transaction.box.proposition.Proposition 8 | import scorex.core.{NodeViewModifier, TransactionsCarryingPersistentNodeViewModifier} 9 | import scorex.util.ModifierId 10 | 11 | /** 12 | * A block is an atomic piece of data network participates are agreed on. 13 | * 14 | * A block has: 15 | * - transactional data: a sequence of transactions, where a transaction is an atomic state update. 16 | * Some metadata is possible as well(transactions Merkle tree root, state Merkle tree root etc). 17 | * 18 | * - consensus data to check whether block was generated by a right party in a right way. E.g. 19 | * "baseTarget" & "generatorSignature" fields in the Nxt block structure, nonce & difficulty in the 20 | * Bitcoin block structure. 21 | * 22 | * - a signature(s) of a block generator(s) 23 | * 24 | * - additional data: block structure version no, timestamp etc 25 | */ 26 | 27 | trait Block[TX <: Transaction] 28 | extends TransactionsCarryingPersistentNodeViewModifier[TX] { 29 | 30 | def version: Version 31 | 32 | def timestamp: Timestamp 33 | } 34 | 35 | object Block { 36 | type BlockId = ModifierId 37 | type Timestamp = Long 38 | type Version = Byte 39 | 40 | val BlockIdLength: Int = NodeViewModifier.ModifierIdSize 41 | 42 | } 43 | 44 | trait BlockCompanion[P <: Proposition, TX <: Transaction, B <: Block[TX]] 45 | extends ScorexSerializer[B] { 46 | 47 | def isValid(block: B): Boolean 48 | 49 | /** 50 | * Get block producers(miners/forgers). Usually one miner produces a block, but in some proposals not 51 | * (see e.g. Proof-of-Activity paper of Bentov et al. http://eprint.iacr.org/2014/452.pdf) 52 | * 53 | * @return blocks' producers 54 | */ 55 | def producers(block: B, history: History[B, _, _]): Seq[P] 56 | } 57 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/block/BlockValidator.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.block 2 | 3 | import scala.util.Try 4 | 5 | trait BlockValidator[PM <: Block[_]] { 6 | def validate(block: PM): Try[Unit] 7 | } 8 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/consensus/ConsensusSettings.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.consensus 2 | 3 | import io.circe.Json 4 | 5 | trait ConsensusSettings { 6 | val settingsJSON: Map[String, Json] 7 | 8 | private val DefaultMaxRollback = 100 9 | lazy val MaxRollback = settingsJSON.get("max-rollback").flatMap(_.asNumber).flatMap(_.toInt).getOrElse(DefaultMaxRollback) 10 | } 11 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/consensus/ContainsModifiers.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.consensus 2 | 3 | import scorex.core.NodeViewModifier 4 | import scorex.util.ModifierId 5 | 6 | /** 7 | * Object that contains modifiers of type `MOD` 8 | */ 9 | trait ContainsModifiers[MOD <: NodeViewModifier] { 10 | 11 | /** 12 | * 13 | * @param persistentModifier - modifier to check 14 | * @return `true` if this object contains this modifier, `false` otherwise 15 | */ 16 | def contains(persistentModifier: MOD): Boolean = contains(persistentModifier.id) 17 | 18 | /** 19 | * 20 | * @param id - modifier's id 21 | * @return `true` if this object contains modifier with specified id, `false` otherwise 22 | */ 23 | def contains(id: ModifierId): Boolean = modifierById(id).isDefined 24 | 25 | /** 26 | * @param modifierId - modifier id to get 27 | * @return modifier of type MOD with id == modifierId if exist 28 | */ 29 | def modifierById(modifierId: ModifierId): Option[MOD] 30 | } 31 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/consensus/HistoryReader.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.consensus 2 | 3 | import scorex.core.{NodeViewComponent, PersistentNodeViewModifier} 4 | import scorex.util.ModifierId 5 | 6 | import scala.util.Try 7 | 8 | 9 | trait HistoryReader[PM <: PersistentNodeViewModifier, SI <: SyncInfo] extends NodeViewComponent 10 | with ContainsModifiers[PM] { 11 | 12 | import History._ 13 | 14 | /** 15 | * Is there's no history, even genesis block 16 | */ 17 | def isEmpty: Boolean 18 | 19 | /** 20 | * Whether a modifier could be applied to the history 21 | * 22 | * @param modifier - modifier to apply 23 | * @return `Success` if modifier can be applied, `Failure(ModifierError)` if can not 24 | */ 25 | def applicableTry(modifier: PM): Try[Unit] 26 | 27 | /** 28 | * Return semantic validity status of modifier with id == modifierId 29 | * 30 | * @param modifierId - modifier id to check 31 | * @return 32 | */ 33 | def isSemanticallyValid(modifierId: ModifierId): ModifierSemanticValidity 34 | 35 | //todo: output should be ID | Seq[ID] 36 | def openSurfaceIds(): Seq[ModifierId] 37 | 38 | /** 39 | * Ids of modifiers, that node with info should download and apply to synchronize 40 | */ 41 | def continuationIds(info: SI, size: Int): ModifierIds 42 | 43 | /** 44 | * Information about our node synchronization status. Other node should be able to compare it's view with ours by 45 | * this syncInfo message and calculate modifiers missed by our node. 46 | * 47 | * @return 48 | */ 49 | def syncInfo: SI 50 | 51 | /** 52 | * Whether another's node syncinfo shows that another node is ahead or behind ours 53 | * 54 | * @param other other's node sync info 55 | * @return Equal if nodes have the same history, Younger if another node is behind, Older if a new node is ahead 56 | */ 57 | def compare(other: SI): HistoryComparisonResult 58 | } 59 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/consensus/ModifierSemanticValidity.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.consensus 2 | 3 | sealed trait ModifierSemanticValidity { 4 | val code: Byte 5 | } 6 | 7 | object ModifierSemanticValidity { 8 | def restoreFromCode(code: Byte): ModifierSemanticValidity = 9 | if (code == Valid.code) Valid 10 | else if (code == Unknown.code) Unknown 11 | else if (code == Invalid.code) Invalid 12 | else Absent 13 | 14 | case object Absent extends ModifierSemanticValidity { 15 | override val code: Byte = 0 16 | } 17 | 18 | case object Unknown extends ModifierSemanticValidity { 19 | override val code: Byte = 1 20 | } 21 | 22 | case object Valid extends ModifierSemanticValidity { 23 | override val code: Byte = 2 24 | } 25 | 26 | case object Invalid extends ModifierSemanticValidity { 27 | override val code: Byte = 3 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/consensus/SyncInfo.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.consensus 2 | 3 | import scorex.core.serialization.BytesSerializable 4 | 5 | /** 6 | * Syncing info provides information about starting points this node recommends another to start 7 | * synchronization from 8 | */ 9 | trait SyncInfo extends BytesSerializable { 10 | def startingPoints: History.ModifierIds 11 | } 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/core.scala: -------------------------------------------------------------------------------- 1 | package scorex 2 | 3 | import scorex.core.network.message.InvData 4 | import scorex.core.utils.ScorexEncoder 5 | import scorex.util.encode.Base16 6 | import supertagged.TaggedType 7 | 8 | package object core { 9 | 10 | //TODO implement ModifierTypeId as a trait 11 | object ModifierTypeId extends TaggedType[Byte] 12 | 13 | @deprecated("use `scorex.util.ModifierId`", "") 14 | type ModifierId = scorex.util.ModifierId.Type 15 | 16 | @deprecated("use `scorex.util.ModifierId`", "") 17 | val ModifierId: scorex.util.ModifierId.type = scorex.util.ModifierId 18 | 19 | object VersionTag extends TaggedType[String] 20 | 21 | type ModifierTypeId = ModifierTypeId.Type 22 | 23 | type VersionTag = VersionTag.Type 24 | 25 | def idsToString(ids: Seq[(ModifierTypeId, util.ModifierId)])(implicit enc: ScorexEncoder): String = { 26 | List(ids.headOption, ids.lastOption) 27 | .flatten 28 | .map { case (typeId, id) => s"($typeId,${enc.encodeId(id)})" } 29 | .mkString("[", "..", "]") 30 | } 31 | 32 | def idsToString(modifierType: ModifierTypeId, ids: Seq[util.ModifierId])(implicit encoder: ScorexEncoder): String = { 33 | idsToString(ids.map(id => (modifierType, id))) 34 | } 35 | 36 | def idsToString(invData: InvData)(implicit encoder: ScorexEncoder): String = idsToString(invData.typeId, invData.ids) 37 | 38 | def bytesToId: Array[Byte] => util.ModifierId = scorex.util.bytesToId 39 | 40 | def idToBytes: util.ModifierId => Array[Byte] = scorex.util.idToBytes 41 | 42 | def bytesToVersion(bytes: Array[Byte]): VersionTag = VersionTag @@ Base16.encode(bytes) 43 | 44 | def versionToBytes(id: VersionTag): Array[Byte] = Base16.decode(id).get 45 | 46 | def versionToId(version: VersionTag): util.ModifierId = util.ModifierId @@ version 47 | 48 | def idToVersion(id: util.ModifierId): VersionTag = VersionTag @@ id 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/network/ConnectedPeer.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.network 2 | 3 | import akka.actor.ActorRef 4 | import scorex.core.network.peer.PeerInfo 5 | 6 | /** 7 | * Peer connected to our node 8 | * 9 | * @param connectionId - connection address 10 | * @param handlerRef - reference to PeerConnectionHandler that is responsible for communication with this peer 11 | * @param lastMessage - timestamp of last received message 12 | * @param peerInfo - information about this peer. May be None if peer is connected, but is not handshaked yet 13 | */ 14 | case class ConnectedPeer(connectionId: ConnectionId, 15 | handlerRef: ActorRef, 16 | var lastMessage: Long, 17 | peerInfo: Option[PeerInfo]) { 18 | 19 | override def hashCode(): Int = connectionId.hashCode() 20 | 21 | override def equals(obj: Any): Boolean = obj match { 22 | case that: ConnectedPeer => this.connectionId.remoteAddress == that.connectionId.remoteAddress 23 | case _ => false 24 | } 25 | 26 | override def toString: String = s"ConnectedPeer($connectionId)" 27 | } 28 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/network/ConnectionDescription.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.network 2 | 3 | import java.net.InetSocketAddress 4 | 5 | import akka.actor.ActorRef 6 | 7 | case class ConnectionDescription(connection: ActorRef, 8 | connectionId: ConnectionId, 9 | ownSocketAddress: Option[InetSocketAddress], 10 | localFeatures: Seq[PeerFeature]) 11 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/network/ConnectionDirection.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.network 2 | 3 | sealed trait ConnectionDirection { 4 | def isIncoming: Boolean 5 | def isOutgoing: Boolean = !isIncoming 6 | } 7 | 8 | case object Incoming extends ConnectionDirection { 9 | override val isIncoming: Boolean = true 10 | } 11 | 12 | case object Outgoing extends ConnectionDirection { 13 | override val isIncoming: Boolean = false 14 | } 15 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/network/ConnectionId.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.network 2 | 3 | import java.net.InetSocketAddress 4 | 5 | /** 6 | * Wraps (remoteAddress, localAddress, direction) tuple, which allows to precisely identify peer. 7 | */ 8 | final case class ConnectionId(remoteAddress: InetSocketAddress, 9 | localAddress: InetSocketAddress, 10 | direction: ConnectionDirection) { 11 | 12 | override def toString: String = 13 | s"ConnectionId(remote=${remoteAddress.toString}, local=${localAddress.toString}, direction=$direction)" 14 | } 15 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/network/Handshake.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.network 2 | 3 | /** 4 | * Network message to be send when nodes establish a new connection. 5 | * When a node creates an outgoing connection, it will immediately advertise its Handshake. 6 | * The remote node will respond with its Handshake. 7 | * No further communication is possible until both peers have exchanged their handshakes. 8 | * 9 | * @param peerSpec - general (declared) information about peer 10 | * @param time - handshake time 11 | */ 12 | case class Handshake(peerSpec: PeerSpec, time: Long) 13 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/network/MaliciousBehaviorException.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.network 2 | 3 | /** 4 | * Custom exception to distinguish malicious behaviour of external peers from non-adversarial network issues 5 | * 6 | * @param message - exception message 7 | */ 8 | case class MaliciousBehaviorException(message: String) extends Exception(message) -------------------------------------------------------------------------------- /src/main/scala/scorex/core/network/ModifiersStatus.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.network 2 | 3 | sealed trait ModifiersStatus 4 | 5 | object ModifiersStatus { 6 | 7 | /** 8 | * This modifier is unknown to our node 9 | */ 10 | case object Unknown extends ModifiersStatus 11 | 12 | /** 13 | * Our node have requested this modifier from other peers but did not received it yet. 14 | */ 15 | case object Requested extends ModifiersStatus 16 | 17 | /** 18 | * Our node have received this modifier from other peers but did not applied yet. 19 | * The modifier might be in ModifiersCache or on the way to it 20 | */ 21 | case object Received extends ModifiersStatus 22 | 23 | /** 24 | * This modifier is already on NodeViewHoder - applied to History if it is PersistentModifier or 25 | * in MemPool if it is Ephemereal modifier. 26 | */ 27 | case object Held extends ModifiersStatus 28 | 29 | /** 30 | * This modifier is permanently invalid - never try to download it 31 | */ 32 | case object Invalid extends ModifiersStatus 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/network/PeerFeature.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.network 2 | 3 | import scorex.core.serialization.{BytesSerializable, ScorexSerializer} 4 | 5 | /** 6 | * An abstract trait to describe peer capabilities. 7 | * During a handshake peers are sending list of their "features" to each other. 8 | * It is assumed that features are not changing when the node runs. 9 | * Maximum theoretical size of a serialized feature is 32,767 bytes. 10 | * However, handshake size limit is also to be considered 11 | * (for all the features to be sent during the handshake). 12 | */ 13 | trait PeerFeature extends BytesSerializable { 14 | override type M >: this.type <: PeerFeature 15 | val featureId: PeerFeature.Id 16 | } 17 | 18 | object PeerFeature { 19 | type Id = Byte 20 | type Serializers = Map[Id, ScorexSerializer[_ <: PeerFeature]] 21 | } -------------------------------------------------------------------------------- /src/main/scala/scorex/core/network/SendingStrategy.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.network 2 | 3 | import scala.util.Random 4 | 5 | trait SendingStrategy { 6 | def choose(peers: Seq[ConnectedPeer]): Seq[ConnectedPeer] 7 | } 8 | 9 | object SendToRandom extends SendingStrategy { 10 | override def choose(peers: Seq[ConnectedPeer]): Seq[ConnectedPeer] = { 11 | if (peers.nonEmpty) { 12 | Seq(peers(Random.nextInt(peers.length))) 13 | } else { 14 | Seq.empty 15 | } 16 | } 17 | } 18 | 19 | case object Broadcast extends SendingStrategy { 20 | override def choose(peers: Seq[ConnectedPeer]): Seq[ConnectedPeer] = peers 21 | } 22 | 23 | case class BroadcastExceptOf(exceptOf: Seq[ConnectedPeer]) extends SendingStrategy { 24 | override def choose(peers: Seq[ConnectedPeer]): Seq[ConnectedPeer] = 25 | peers.filterNot(exceptOf.contains) 26 | } 27 | 28 | case class SendToPeer(chosenPeer: ConnectedPeer) extends SendingStrategy { 29 | override def choose(peers: Seq[ConnectedPeer]): Seq[ConnectedPeer] = Seq(chosenPeer) 30 | } 31 | 32 | case class SendToPeers(chosenPeers: Seq[ConnectedPeer]) extends SendingStrategy { 33 | override def choose(peers: Seq[ConnectedPeer]): Seq[ConnectedPeer] = chosenPeers 34 | } 35 | 36 | case class SendToRandomFromChosen(chosenPeers: Seq[ConnectedPeer]) extends SendingStrategy { 37 | override def choose(peers: Seq[ConnectedPeer]): Seq[ConnectedPeer] = 38 | Seq(chosenPeers(Random.nextInt(chosenPeers.length))) 39 | } 40 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/network/Synchronizer.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.network 2 | 3 | import scorex.core.network.message.MessageSpec 4 | import scorex.util.ScorexLogging 5 | 6 | import scala.util.{Failure, Success} 7 | 8 | trait Synchronizer extends ScorexLogging { 9 | 10 | // these are the case statements for identifying the message handlers 11 | protected val msgHandlers: PartialFunction[(MessageSpec[_], _, ConnectedPeer), Unit] 12 | 13 | /** 14 | * This method will attempt to parse a message from a remote peer into it class representation and use 15 | * the defined message handlers for processing the message 16 | * 17 | * @param spec the message specification (basically a header informing of the message type) 18 | * @param msgBytes a ByteString of the message data that must be parsed 19 | * @param source the remote peer that sent the message 20 | */ 21 | protected def parseAndHandle(spec: MessageSpec[Any], msgBytes: Array[Byte], source: ConnectedPeer): Unit = { 22 | // attempt to parse the message 23 | spec.parseBytesTry(msgBytes) match { 24 | // if a message could be parsed, match the type of content found and ensure a handler is defined 25 | case Success(content) => 26 | val parsedMsg = (spec, content, source) 27 | if (msgHandlers.isDefinedAt(parsedMsg)) { 28 | msgHandlers.apply(parsedMsg) 29 | } else { 30 | log.error(s"Function handler not found for the parsed message: $parsedMsg") 31 | } 32 | 33 | // if a message could not be parsed, penalize the remote peer 34 | case Failure(e) => 35 | log.error(s"Failed to deserialize data from $source: ", e) 36 | penalizeMaliciousPeer(source) 37 | } 38 | } 39 | 40 | /** 41 | * Handles how a peer that sent un-parsable data should be handled 42 | * 43 | * @param peer peer that sent the offending message 44 | */ 45 | protected def penalizeMaliciousPeer(peer: ConnectedPeer): Unit 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/network/message/Message.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.network.message 2 | 3 | 4 | import akka.actor.DeadLetterSuppression 5 | import scorex.core.network.ConnectedPeer 6 | import scala.util.{Success, Try} 7 | 8 | 9 | /** 10 | * Wrapper for a network message, whether come from external peer or generated locally 11 | * 12 | * @param spec - message specification 13 | * @param input - message being wrapped, whether in byte-array form (if from outside), 14 | * or structured data (if formed locally) 15 | * @param source - source peer, if the message is from outside 16 | * @tparam Content - message data type 17 | */ 18 | case class Message[Content](spec: MessageSpec[Content], 19 | input: Either[Array[Byte], Content], 20 | source: Option[ConnectedPeer]) 21 | extends DeadLetterSuppression { 22 | 23 | import Message._ 24 | 25 | /** 26 | * Message data bytes 27 | */ 28 | lazy val dataBytes: Array[Byte] = input match { 29 | case Left(db) => db 30 | case Right(d) => spec.toBytes(d) 31 | } 32 | 33 | /** 34 | * Structured message content 35 | */ 36 | lazy val data: Try[Content] = input match { 37 | case Left(db) => spec.parseBytesTry(db) 38 | case Right(d) => Success(d) 39 | } 40 | 41 | lazy val dataLength: Int = dataBytes.length 42 | 43 | /** 44 | * @return serialized message length in bytes 45 | */ 46 | def messageLength: Int = { 47 | if (dataLength > 0) { 48 | HeaderLength + ChecksumLength + dataLength 49 | } else { 50 | HeaderLength 51 | } 52 | } 53 | 54 | } 55 | 56 | object Message { 57 | type MessageCode = Byte 58 | 59 | val MagicLength: Int = 4 60 | 61 | val ChecksumLength: Int = 4 62 | 63 | val HeaderLength: Int = MagicLength + 5 64 | } 65 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/network/message/MessageSpec.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.network.message 2 | 3 | import scorex.core.app.Version 4 | import scorex.core.serialization.ScorexSerializer 5 | 6 | /** 7 | * Base trait for app p2p messages in the network 8 | */ 9 | trait MessageSpec[Content] extends ScorexSerializer[Content] { 10 | 11 | /** 12 | * The p2p protocol version in which this message type first appeared 13 | */ 14 | val protocolVersion: Version 15 | 16 | /** 17 | * Code which identifies what message type is contained in the payload 18 | */ 19 | 20 | val messageCode: Message.MessageCode 21 | 22 | /** 23 | * Name of this message type. For debug purposes only. 24 | */ 25 | val messageName: String 26 | 27 | override def toString: String = s"MessageSpec($messageCode: $messageName)" 28 | } 29 | 30 | /** 31 | * P2p messages, that where implemented since the beginning. 32 | */ 33 | trait MessageSpecV1[Content] extends MessageSpec[Content] { 34 | 35 | override val protocolVersion: Version = Version.initial 36 | 37 | } -------------------------------------------------------------------------------- /src/main/scala/scorex/core/network/peer/LocalAddressPeerFeature.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.network.peer 2 | 3 | import java.net.{InetAddress, InetSocketAddress} 4 | 5 | import scorex.core.network.PeerFeature 6 | import scorex.core.network.PeerFeature.Id 7 | import scorex.util.serialization._ 8 | import scorex.core.serialization.ScorexSerializer 9 | import scorex.util.Extensions._ 10 | 11 | case class LocalAddressPeerFeature(address: InetSocketAddress) extends PeerFeature { 12 | override type M = LocalAddressPeerFeature 13 | override val featureId: Id = LocalAddressPeerFeature.featureId 14 | 15 | override def serializer: LocalAddressPeerFeatureSerializer.type = LocalAddressPeerFeatureSerializer 16 | } 17 | 18 | object LocalAddressPeerFeature { 19 | val featureId: Id = 2: Byte 20 | } 21 | 22 | object LocalAddressPeerFeatureSerializer extends ScorexSerializer[LocalAddressPeerFeature] { 23 | 24 | private val AddressLength = 4 25 | 26 | override def serialize(obj: LocalAddressPeerFeature, w: Writer): Unit = { 27 | w.putBytes(obj.address.getAddress.getAddress) 28 | w.putUInt(obj.address.getPort) 29 | } 30 | 31 | override def parse(r: Reader): LocalAddressPeerFeature = { 32 | val fa = r.getBytes(AddressLength) 33 | val port = r.getUInt().toIntExact 34 | LocalAddressPeerFeature(new InetSocketAddress(InetAddress.getByAddress(fa), port)) 35 | } 36 | } -------------------------------------------------------------------------------- /src/main/scala/scorex/core/network/peer/PeerDatabase.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.network.peer 2 | 3 | import java.net.{InetAddress, InetSocketAddress} 4 | 5 | trait PeerDatabase { 6 | 7 | def get(peer: InetSocketAddress): Option[PeerInfo] 8 | 9 | def isEmpty: Boolean 10 | 11 | /** 12 | * Add peer to the database, or update it 13 | * @param peerInfo - peer record 14 | */ 15 | def addOrUpdateKnownPeer(peerInfo: PeerInfo): Unit 16 | 17 | def knownPeers: Map[InetSocketAddress, PeerInfo] 18 | 19 | def addToBlacklist(address: InetSocketAddress, penaltyType: PenaltyType): Unit 20 | 21 | def removeFromBlacklist(address: InetAddress): Unit 22 | 23 | def blacklistedPeers: Seq[InetAddress] 24 | 25 | def isBlacklisted(address: InetAddress): Boolean 26 | 27 | def remove(address: InetSocketAddress): Unit 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/network/peer/PeerInfo.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.network.peer 2 | 3 | import java.net.InetSocketAddress 4 | 5 | import scorex.core.app.Version 6 | import scorex.core.network.{ConnectionDirection, PeerSpec} 7 | 8 | /** 9 | * Information about peer to be stored in PeerDatabase 10 | * 11 | * @param peerSpec - general information about the peer 12 | * @param lastHandshake - timestamp when last handshake was done 13 | * @param connectionType - type of connection (Incoming/Outgoing) established to this peer if any 14 | */ 15 | case class PeerInfo(peerSpec: PeerSpec, 16 | lastHandshake: Long, 17 | connectionType: Option[ConnectionDirection] = None) 18 | 19 | /** 20 | * Information about P2P layer status 21 | * 22 | * @param lastIncomingMessage - timestamp of last received message from any peer 23 | * @param currentNetworkTime - current network time 24 | */ 25 | case class PeersStatus(lastIncomingMessage: Long, currentNetworkTime: Long) 26 | 27 | object PeerInfo { 28 | 29 | /** 30 | * Create peer info from address only, when we don't know other fields 31 | * (e.g. we got this information from config or from API) 32 | */ 33 | def fromAddress(address: InetSocketAddress): PeerInfo = { 34 | val peerSpec = PeerSpec("unknown", Version.initial, s"unknown-$address", Some(address), Seq()) 35 | PeerInfo(peerSpec, 0L, None) 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /src/main/scala/scorex/core/network/peer/PenaltyType.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.network.peer 2 | 3 | /** 4 | * A trait describing all possible types of the network participant misbehavior. 5 | * `penaltyScore` - a number defining how bad concrete kind of misbehavior is, 6 | * `isPermanent` - a flag defining whether a penalty is permanent. 7 | */ 8 | sealed trait PenaltyType { 9 | val penaltyScore: Int 10 | val isPermanent: Boolean = false 11 | } 12 | 13 | object PenaltyType { 14 | 15 | case object NonDeliveryPenalty extends PenaltyType { 16 | override val penaltyScore: Int = 2 17 | } 18 | 19 | case object MisbehaviorPenalty extends PenaltyType { 20 | override val penaltyScore: Int = 10 21 | } 22 | 23 | case object SpamPenalty extends PenaltyType { 24 | override val penaltyScore: Int = 25 25 | } 26 | 27 | case object PermanentPenalty extends PenaltyType { 28 | override val penaltyScore: Int = 1000000000 29 | override val isPermanent: Boolean = true 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/network/peer/SessionIdPeerFeature.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.network.peer 2 | 3 | import scorex.core.network.PeerFeature 4 | import scorex.core.network.PeerFeature.Id 5 | import scorex.core.network.message.Message 6 | import scorex.util.serialization._ 7 | import scorex.core.serialization.ScorexSerializer 8 | 9 | /** 10 | * This peer feature allows to more reliably detect connections to self node and connections from other networks 11 | * 12 | * @param networkMagic network magic bytes (taken from settings) 13 | * @param sessionId randomly generated 64-bit session identifier 14 | */ 15 | case class SessionIdPeerFeature(networkMagic: Array[Byte], 16 | sessionId: Long = scala.util.Random.nextLong()) extends PeerFeature { 17 | 18 | override type M = SessionIdPeerFeature 19 | override val featureId: Id = SessionIdPeerFeature.featureId 20 | 21 | override def serializer: SessionIdPeerFeatureSerializer.type = SessionIdPeerFeatureSerializer 22 | 23 | } 24 | 25 | object SessionIdPeerFeature { 26 | 27 | val featureId: Id = 3: Byte 28 | 29 | } 30 | 31 | object SessionIdPeerFeatureSerializer extends ScorexSerializer[SessionIdPeerFeature] { 32 | 33 | override def serialize(obj: SessionIdPeerFeature, w: Writer): Unit = { 34 | w.putBytes(obj.networkMagic) 35 | w.putLong(obj.sessionId) 36 | } 37 | 38 | override def parse(r: Reader): SessionIdPeerFeature = { 39 | val networkMagic = r.getBytes(Message.MagicLength) 40 | val sessionId = r.getLong() 41 | SessionIdPeerFeature(networkMagic, sessionId) 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/serialization/BytesSerializable.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.serialization 2 | 3 | trait BytesSerializable extends Serializable { 4 | 5 | type M >: this.type <: BytesSerializable 6 | 7 | def bytes: Array[Byte] = serializer.toBytes(this) 8 | 9 | def serializer: ScorexSerializer[M] 10 | } 11 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/serialization/ScorexSerializer.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.serialization 2 | 3 | import java.nio.ByteBuffer 4 | 5 | import akka.util.ByteString 6 | import scorex.util.ByteArrayBuilder 7 | import scorex.util.serialization._ 8 | 9 | import scala.util.Try 10 | 11 | trait ScorexSerializer[T] extends Serializer[T, T, Reader, Writer] { 12 | 13 | def toByteString(obj: T): ByteString = { 14 | val writer = new VLQByteStringWriter() 15 | serialize(obj, writer) 16 | writer.result() 17 | } 18 | 19 | def parseByteString(byteString: ByteString): T = { 20 | val reader = new VLQByteStringReader(byteString) 21 | parse(reader) 22 | } 23 | 24 | def parseByteStringTry(byteString: ByteString): Try[T] = { 25 | Try(parseByteString(byteString)) 26 | } 27 | 28 | def toBytes(obj: T): Array[Byte] = { 29 | val writer = new VLQByteBufferWriter(new ByteArrayBuilder()) 30 | serialize(obj, writer) 31 | writer.result().toBytes 32 | } 33 | 34 | def parseBytes(bytes: Array[Byte]): T = { 35 | val reader = new VLQByteBufferReader(ByteBuffer.wrap(bytes)) 36 | parse(reader) 37 | } 38 | 39 | def parseBytesTry(bytes: Array[Byte]): Try[T] = { 40 | Try(parseBytes(bytes)) 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/serialization/SerializerRegistry.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.serialization 2 | 3 | import io.circe.{Encoder, Json} 4 | 5 | import scala.reflect.{ClassTag, classTag} 6 | 7 | 8 | object SerializerRegistry { 9 | 10 | case class SerializerRecord[T: ClassTag](enc: Encoder[T]) { 11 | val ct: ClassTag[T] = classTag[T] 12 | } 13 | 14 | def apply(records: Seq[SerializerRecord[_]]): SerializerRegistry = new SerializerRegistry(records) 15 | } 16 | 17 | sealed class SerializerRegistry(ss: Seq[SerializerRegistry.SerializerRecord[_]]) { 18 | 19 | private val evTypeAndEncoder = ss.map { sr => 20 | (sr.ct.runtimeClass, sr.enc)}.toMap[Class[_], Encoder[_]] 21 | 22 | 23 | def toJson[C](key: Class[_], c: C): Either[Throwable, Json] = { 24 | evTypeAndEncoder.get(key) match { 25 | case Some(e) => Right(e.asInstanceOf[Encoder[C]].apply(c)) 26 | case None => Left(new RuntimeException(s"Circe encoder is not registered for $c")) 27 | } 28 | } 29 | } 30 | 31 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/settings/SettingsReaders.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.settings 2 | 3 | import java.io.File 4 | import java.net.InetSocketAddress 5 | 6 | import com.typesafe.config.Config 7 | import net.ceedubs.ficus.readers.ValueReader 8 | 9 | trait SettingsReaders { 10 | 11 | implicit val fileReader: ValueReader[File] = (cfg, path) => new File(cfg.getString(path)) 12 | implicit val byteValueReader: ValueReader[Byte] = (cfg, path) => cfg.getInt(path).toByte 13 | implicit val inetSocketAddressReader: ValueReader[InetSocketAddress] = { (config: Config, path: String) => 14 | val split = config.getString(path).split(":") 15 | new InetSocketAddress(split(0), split(1).toInt) 16 | } 17 | } -------------------------------------------------------------------------------- /src/main/scala/scorex/core/transaction/BoxTransaction.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.transaction 2 | 3 | import scorex.core.transaction.box.proposition.Proposition 4 | import scorex.core.transaction.box.{Box, BoxUnlocker} 5 | 6 | 7 | abstract class BoxTransaction[P <: Proposition, BX <: Box[P]] extends Transaction { 8 | 9 | val unlockers: Traversable[BoxUnlocker[P]] 10 | val newBoxes: Traversable[BX] 11 | 12 | val fee: Long 13 | 14 | val timestamp: Long 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/transaction/MemoryPool.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.transaction 2 | 3 | import scala.util.Try 4 | 5 | /** 6 | * Unconfirmed transactions pool 7 | * 8 | * @tparam TX -type of transaction the pool contains 9 | */ 10 | trait MemoryPool[TX <: Transaction, M <: MemoryPool[TX, M]] extends MempoolReader[TX] { 11 | 12 | /** 13 | * Method to put a transaction into the memory pool. Validation of tha transactions against 14 | * the state is done in NodeVieHolder. This put() method can check whether a transaction is valid 15 | * @param tx 16 | * @return Success(updatedPool), if transaction successfully added to the pool, Failure(_) otherwise 17 | */ 18 | def put(tx: TX): Try[M] 19 | 20 | def put(txs: Iterable[TX]): Try[M] 21 | 22 | def putWithoutCheck(txs: Iterable[TX]): M 23 | 24 | def remove(tx: TX): M 25 | 26 | def filter(txs: Seq[TX]): M = filter(t => !txs.exists(_.id == t.id)) 27 | 28 | def filter(condition: TX => Boolean): M 29 | 30 | /** 31 | * @return read-only copy of this history 32 | */ 33 | def getReader: MempoolReader[TX] = this 34 | } -------------------------------------------------------------------------------- /src/main/scala/scorex/core/transaction/MempoolReader.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.transaction 2 | 3 | import scorex.core.consensus.ContainsModifiers 4 | import scorex.core.NodeViewComponent 5 | import scorex.util.ModifierId 6 | 7 | /** 8 | * Unconfirmed transactions pool 9 | * 10 | * @tparam TX -type of transaction the pool contains 11 | */ 12 | trait MempoolReader[TX <: Transaction] extends NodeViewComponent with ContainsModifiers[TX] { 13 | 14 | //getters 15 | override def modifierById(modifierId: ModifierId): Option[TX] 16 | 17 | @deprecated("use modifierById instead", "2018-08-14") 18 | def getById(id: ModifierId): Option[TX] = modifierById(id) 19 | 20 | def contains(id: ModifierId): Boolean 21 | 22 | //get ids from Seq, not presenting in mempool 23 | def notIn(ids: Seq[ModifierId]): Seq[ModifierId] = ids.filter(id => !contains(id)) 24 | 25 | def getAll(ids: Seq[ModifierId]): Seq[TX] 26 | 27 | def size: Int 28 | 29 | def take(limit: Int): Iterable[TX] 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/transaction/Transaction.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.transaction 2 | 3 | import scorex.core.{EphemerealNodeViewModifier, ModifierTypeId} 4 | import scorex.crypto.hash.Blake2b256 5 | import scorex.util.{ModifierId, bytesToId} 6 | 7 | 8 | /** 9 | * A transaction is an atomic state modifier 10 | */ 11 | trait Transaction extends EphemerealNodeViewModifier { 12 | override val modifierTypeId: ModifierTypeId = Transaction.ModifierTypeId 13 | 14 | val messageToSign: Array[Byte] 15 | 16 | override lazy val id: ModifierId = bytesToId(Blake2b256(messageToSign)) 17 | } 18 | 19 | 20 | object Transaction { 21 | val ModifierTypeId: scorex.core.ModifierTypeId = scorex.core.ModifierTypeId @@ 2.toByte 22 | } 23 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/transaction/account/PublicKeyNoncedBox.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.transaction.account 2 | 3 | import com.google.common.primitives.Longs 4 | import scorex.core.transaction.box.Box 5 | import scorex.core.transaction.box.proposition.PublicKey25519Proposition 6 | import scorex.crypto.authds.ADKey 7 | import scorex.crypto.hash.Blake2b256 8 | 9 | trait PublicKeyNoncedBox[PKP <: PublicKey25519Proposition] extends Box[PKP] { 10 | val nonce: Long 11 | 12 | lazy val id: ADKey = PublicKeyNoncedBox.idFromBox(proposition, nonce) 13 | 14 | lazy val publicKey: PKP = proposition 15 | 16 | override def equals(obj: Any): Boolean = obj match { 17 | case acc: PublicKeyNoncedBox[PKP] => java.util.Arrays.equals(acc.id, this.id) && acc.value == this.value 18 | case _ => false 19 | } 20 | 21 | override def hashCode(): Int = proposition.hashCode() 22 | } 23 | 24 | object PublicKeyNoncedBox { 25 | def idFromBox[PKP <: PublicKey25519Proposition](prop: PKP, nonce: Long): ADKey = 26 | ADKey @@ Blake2b256(prop.pubKeyBytes ++ Longs.toByteArray(nonce)) 27 | } -------------------------------------------------------------------------------- /src/main/scala/scorex/core/transaction/box/Box.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.transaction.box 2 | 3 | import scorex.core.serialization.BytesSerializable 4 | import scorex.core.transaction.box.proposition.Proposition 5 | import scorex.crypto.authds._ 6 | 7 | /** 8 | * Box is a state element locked by some proposition. 9 | */ 10 | trait Box[P <: Proposition] extends BytesSerializable { 11 | val value: Box.Amount 12 | val proposition: P 13 | 14 | val id: ADKey 15 | } 16 | 17 | object Box { 18 | type Amount = Long 19 | } 20 | 21 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/transaction/box/BoxUnlocker.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.transaction.box 2 | 3 | import scorex.core.transaction.box.proposition.Proposition 4 | import scorex.core.transaction.proof.Proof 5 | import scorex.core.utils.ScorexEncoding 6 | import scorex.crypto.authds.ADKey 7 | 8 | trait BoxUnlocker[P <: Proposition] extends ScorexEncoding { 9 | val closedBoxId: ADKey 10 | val boxKey: Proof[P] 11 | 12 | override def toString: String = s"BoxUnlocker(id: ${encoder.encode(closedBoxId)}, boxKey: $boxKey)" 13 | } 14 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/transaction/box/proposition/Proposition.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.transaction.box.proposition 2 | 3 | import scorex.core.serialization.BytesSerializable 4 | import scorex.core.transaction.state.Secret 5 | 6 | trait Proposition extends BytesSerializable 7 | 8 | trait ProofOfKnowledgeProposition[S <: Secret] extends Proposition 9 | 10 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/transaction/proof/Proof.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.transaction.proof 2 | 3 | import scorex.core.serialization.BytesSerializable 4 | import scorex.core.transaction.box.proposition.{ProofOfKnowledgeProposition, Proposition} 5 | import scorex.core.transaction.state.Secret 6 | 7 | /** 8 | * The most general abstraction of fact a prover can provide a non-interactive proof 9 | * to open a box or to modify an account 10 | * 11 | * A proof is non-interactive and thus serializable 12 | */ 13 | 14 | trait Proof[P <: Proposition] extends BytesSerializable { 15 | def isValid(proposition: P, message: Array[Byte]): Boolean 16 | } 17 | 18 | trait ProofOfKnowledge[S <: Secret, P <: ProofOfKnowledgeProposition[S]] extends Proof[P] 19 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/transaction/proof/Signature25519.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.transaction.proof 2 | 3 | import scorex.util.serialization._ 4 | import scorex.core.serialization.ScorexSerializer 5 | import scorex.core.transaction.box.proposition.PublicKey25519Proposition 6 | import scorex.core.transaction.state.PrivateKey25519 7 | import scorex.core.utils.ScorexEncoding 8 | import scorex.crypto.signatures.{Curve25519, Signature} 9 | 10 | /** 11 | * @param signature 25519 signature 12 | */ 13 | case class Signature25519(signature: Signature) extends ProofOfKnowledge[PrivateKey25519, PublicKey25519Proposition] 14 | with ScorexEncoding { 15 | 16 | require(signature.isEmpty || signature.length == Curve25519.SignatureLength, 17 | s"${signature.length} != ${Curve25519.SignatureLength}") 18 | 19 | override def isValid(proposition: PublicKey25519Proposition, message: Array[Byte]): Boolean = 20 | Curve25519.verify(signature, message, proposition.pubKeyBytes) 21 | 22 | override type M = Signature25519 23 | 24 | override def serializer: ScorexSerializer[Signature25519] = Signature25519Serializer 25 | 26 | override def toString: String = s"Signature25519(${encoder.encode(signature)})" 27 | } 28 | 29 | object Signature25519Serializer extends ScorexSerializer[Signature25519] { 30 | 31 | override def serialize(obj: Signature25519, w: Writer): Unit = { 32 | w.putBytes(obj.signature) 33 | } 34 | 35 | override def parse(r: Reader): Signature25519 = { 36 | Signature25519(Signature @@ r.getBytes(Curve25519.SignatureLength)) 37 | } 38 | } 39 | 40 | object Signature25519 { 41 | lazy val SignatureSize = Curve25519.SignatureLength 42 | } 43 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/transaction/state/BoxStateChanges.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.transaction.state 2 | 3 | import scorex.core.transaction.box.Box 4 | import scorex.core.transaction.box.proposition.Proposition 5 | import scorex.core.utils.ScorexEncoding 6 | import scorex.crypto.authds._ 7 | 8 | abstract class BoxStateChangeOperation[P <: Proposition, BX <: Box[P]] 9 | 10 | case class Removal[P <: Proposition, BX <: Box[P]](boxId: ADKey) extends BoxStateChangeOperation[P, BX] with ScorexEncoding { 11 | override def toString: String = s"Removal(id: ${encoder.encode(boxId)})" 12 | } 13 | 14 | case class Insertion[P <: Proposition, BX <: Box[P]](box: BX) extends BoxStateChangeOperation[P, BX] 15 | 16 | case class BoxStateChanges[P <: Proposition, BX <: Box[P]](operations: Seq[BoxStateChangeOperation[P, BX]]) { 17 | lazy val toAppend: Seq[Insertion[P, BX]] = operations.filter { op => 18 | op match { 19 | case _: Insertion[P, BX] => true 20 | case _ => false 21 | } 22 | }.asInstanceOf[Seq[Insertion[P, BX]]] 23 | 24 | lazy val toRemove: Seq[Removal[P, BX]] = operations.filter { op => 25 | op match { 26 | case _: Removal[P, BX] => true 27 | case _ => false 28 | } 29 | }.asInstanceOf[Seq[Removal[P, BX]]] 30 | } 31 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/transaction/state/MinimalState.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.transaction.state 2 | 3 | import scorex.core.transaction._ 4 | import scorex.core.transaction.box.proposition.Proposition 5 | import scorex.core.{PersistentNodeViewModifier, VersionTag} 6 | 7 | import scala.util.Try 8 | 9 | /** 10 | * Abstract functional interface of state which is a result of a sequential blocks applying 11 | */ 12 | trait MinimalState[M <: PersistentNodeViewModifier, MS <: MinimalState[M, MS]] extends StateReader { 13 | self: MS => 14 | 15 | def applyModifier(mod: M): Try[MS] 16 | 17 | def rollbackTo(version: VersionTag): Try[MS] 18 | 19 | /** 20 | * @return read-only copy of this state 21 | */ 22 | def getReader: StateReader = this 23 | 24 | } 25 | 26 | 27 | trait StateFeature 28 | 29 | trait TransactionValidation[TX <: Transaction] extends StateFeature { 30 | def isValid(tx: TX): Boolean = validate(tx).isSuccess 31 | 32 | def filterValid(txs: Seq[TX]): Seq[TX] = txs.filter(isValid) 33 | 34 | def validate(tx: TX): Try[Unit] 35 | } 36 | 37 | trait ModifierValidation[M <: PersistentNodeViewModifier] extends StateFeature { 38 | def validate(mod: M): Try[Unit] 39 | } 40 | 41 | trait BalanceSheet[P <: Proposition] extends StateFeature { 42 | def balance(id: P, height: Option[Int] = None): Long 43 | } 44 | 45 | trait AccountTransactionsHistory[P <: Proposition, TX <: Transaction] extends StateFeature { 46 | def accountTransactions(id: P): Array[TX] 47 | } 48 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/transaction/state/StateReader.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.transaction.state 2 | 3 | import scorex.core.{NodeViewComponent, VersionTag} 4 | 5 | trait StateReader extends NodeViewComponent { 6 | 7 | //must be ID of last applied modifier 8 | def version: VersionTag 9 | 10 | def maxRollbackDepth: Int 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/transaction/wallet/Vault.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.transaction.wallet 2 | 3 | import scorex.core.transaction.Transaction 4 | import scorex.core.{PersistentNodeViewModifier, VersionTag} 5 | 6 | import scala.util.Try 7 | 8 | /** 9 | * Abstract interface for Vault, a storage for node-specific information 10 | */ 11 | 12 | trait Vault[TX <: Transaction, PMOD <: PersistentNodeViewModifier, V <: Vault[TX, PMOD, V]] extends VaultReader { 13 | self: V => 14 | 15 | def scanOffchain(tx: TX): V 16 | 17 | def scanOffchain(txs: Seq[TX]): V 18 | 19 | def scanPersistent(modifier: PMOD): V 20 | 21 | def scanPersistent(modifiers: Option[PMOD]): V = modifiers.foldLeft(this) { case (v, mod) => 22 | v.scanPersistent(mod) 23 | } 24 | 25 | def rollback(to: VersionTag): Try[V] 26 | 27 | /** 28 | * @return read-only copy of this state 29 | */ 30 | def getReader: VaultReader = this 31 | 32 | } -------------------------------------------------------------------------------- /src/main/scala/scorex/core/transaction/wallet/VaultReader.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.transaction.wallet 2 | 3 | import scorex.core.NodeViewComponent 4 | 5 | /** 6 | * Reader for vault. 7 | * As vault is implementation-specific component, it does not contain any mandatory methods. 8 | */ 9 | trait VaultReader extends NodeViewComponent -------------------------------------------------------------------------------- /src/main/scala/scorex/core/utils/ActorHelper.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.utils 2 | 3 | import akka.actor.ActorRef 4 | import akka.pattern.ask 5 | import akka.util.Timeout 6 | 7 | import scala.concurrent.Future 8 | import scala.reflect.ClassTag 9 | 10 | /** 11 | * Helper that encapsulates ask patter for actors and returns Future[_] 12 | */ 13 | trait ActorHelper { 14 | 15 | def askActor[A: ClassTag](actorRef: ActorRef, question: Any) 16 | (implicit timeout: Timeout): Future[A] = (actorRef ? question).mapTo[A] 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/utils/BlockTypeable.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.utils 2 | 3 | import scorex.core.block.Block 4 | import scorex.core.transaction.Transaction 5 | import shapeless.Typeable 6 | 7 | class BlockTypeable[TX <: Transaction] extends Typeable[Block[TX]] { 8 | 9 | def cast(t: Any): Option[Block[TX]] = t match { 10 | case b: Block[TX] => Some(b) 11 | case _ => None 12 | } 13 | 14 | def describe: String = "Block[TX <: Transaction]" 15 | 16 | override def toString: String = s"Typeable[$describe]" 17 | } -------------------------------------------------------------------------------- /src/main/scala/scorex/core/utils/LocalTimeProvider.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.utils 2 | 3 | object LocalTimeProvider extends TimeProvider { 4 | override def time(): TimeProvider.Time = { 5 | System.currentTimeMillis() 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/utils/NetworkTime.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.utils 2 | 3 | import java.net.InetAddress 4 | import java.util.concurrent.atomic.AtomicLong 5 | 6 | import org.apache.commons.net.ntp.NTPUDPClient 7 | 8 | import scala.concurrent.duration._ 9 | import scala.concurrent.{ExecutionContext, Future} 10 | import scala.util.{Failure, Success} 11 | 12 | object NetworkTime { 13 | def localWithOffset(offset: Long): Long = System.currentTimeMillis() + offset 14 | type Offset = Long 15 | } 16 | 17 | case class NetworkTimeProviderSettings(server: String, updateEvery: FiniteDuration, timeout: FiniteDuration) 18 | 19 | class NetworkTimeProvider(ntpSettings: NetworkTimeProviderSettings)(implicit ec: ExecutionContext) 20 | extends TimeProvider with scorex.util.ScorexLogging { 21 | 22 | private val lastUpdate = new AtomicLong(0) 23 | private var offset = new AtomicLong(0) 24 | private val client = new NTPUDPClient() 25 | client.setDefaultTimeout(ntpSettings.timeout.toMillis.toInt) 26 | client.open() 27 | 28 | override def time(): TimeProvider.Time = { 29 | checkUpdateRequired() 30 | NetworkTime.localWithOffset(offset.get()) 31 | } 32 | 33 | private def updateOffset(): Future[NetworkTime.Offset] = Future { 34 | val info = client.getTime(InetAddress.getByName(ntpSettings.server)) 35 | info.computeDetails() 36 | info.getOffset 37 | } 38 | 39 | private def checkUpdateRequired(): Unit = { 40 | val time = NetworkTime.localWithOffset(offset.get()) 41 | // set lastUpdate to current time so other threads won't start to update it 42 | val lu = lastUpdate.getAndSet(time) 43 | if (time > lu + ntpSettings.updateEvery.toMillis) { 44 | // time to update offset 45 | updateOffset().onComplete { 46 | case Success(newOffset) => 47 | offset.set(newOffset) 48 | log.info("New offset adjusted: " + offset) 49 | lastUpdate.set(time) 50 | case Failure(e) => 51 | log.warn("Problems with NTP: ", e) 52 | lastUpdate.compareAndSet(time, lu) 53 | } 54 | } else { 55 | // No update required. Set lastUpdate back to it's initial value 56 | lastUpdate.compareAndSet(time, lu) 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /src/main/scala/scorex/core/utils/NetworkUtils.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.utils 2 | 3 | import java.net.{Inet4Address, InetSocketAddress, NetworkInterface} 4 | import scala.collection.JavaConverters._ 5 | 6 | object NetworkUtils { 7 | 8 | def getListenAddresses(bindAddress: InetSocketAddress): Set[InetSocketAddress] = { 9 | if (bindAddress.getAddress.isAnyLocalAddress || bindAddress.getAddress.isLoopbackAddress) { 10 | NetworkInterface.getNetworkInterfaces.asScala 11 | .flatMap(_.getInetAddresses.asScala) 12 | .collect { case a: Inet4Address => a} 13 | .map(a => new InetSocketAddress(a, bindAddress.getPort)) 14 | .toSet 15 | } else { 16 | Set(bindAddress) 17 | } 18 | } 19 | 20 | def isSelf(peerAddress: InetSocketAddress, 21 | bindAddress: InetSocketAddress, 22 | externalNodeAddress: Option[InetSocketAddress]): Boolean = { 23 | NetworkUtils.getListenAddresses(bindAddress).contains(peerAddress) || 24 | externalNodeAddress.contains(peerAddress) 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/utils/ScorexEncoder.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.utils 2 | 3 | import scorex.core.VersionTag 4 | import scorex.util.ModifierId 5 | import scorex.util.encode.{Base16, BytesEncoder} 6 | 7 | import scala.util.Try 8 | 9 | class ScorexEncoder extends BytesEncoder { 10 | @inline 11 | override val Alphabet: String = Base16.Alphabet 12 | 13 | @inline 14 | override def encode(input: Array[Byte]): String = Base16.encode(input) 15 | 16 | @inline 17 | override def decode(input: String): Try[Array[Byte]] = Base16.decode(input) 18 | 19 | /** 20 | * This method might be useful and reimplemented, if encoding of ModifierId and VersionTag 21 | * is different form default bytes encoding, e.g. this method should be reimplemented together 22 | * with encode() and decode methods 23 | */ 24 | @inline 25 | def encode(input: String): String = input 26 | 27 | /** 28 | * This method might be useful and reimplemented, if encoding of ModifierId and VersionTag 29 | * is different form default bytes encoding, e.g. this method should be reimplemented together 30 | * with encode() and decode methods 31 | */ 32 | @inline 33 | def encodeVersion(input: VersionTag): String = input 34 | 35 | /** 36 | * This method might be useful and reimplemented, if encoding of ModifierId and VersionTag 37 | * is different form default bytes encoding, e.g. this method should be reimplemented together 38 | * with encode() and decode methods 39 | */ 40 | @inline 41 | def encodeId(input: ModifierId): String = input 42 | 43 | } 44 | 45 | object ScorexEncoder { 46 | val default: ScorexEncoder = new ScorexEncoder() 47 | } 48 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/utils/ScorexEncoding.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.utils 2 | 3 | /** 4 | * Trait with bytes to string encoder 5 | * TODO extract to ScorexUtils project 6 | */ 7 | trait ScorexEncoding { 8 | implicit val encoder: ScorexEncoder = ScorexEncoder.default 9 | } 10 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/utils/SerializationConstants.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.utils 2 | 3 | object SerializationConstants { 4 | val IntSize: Int = 4 5 | val LongSize: Int = 8 6 | } 7 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/utils/TimeProvider.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.utils 2 | 3 | object TimeProvider { 4 | type Time = Long 5 | } 6 | 7 | trait TimeProvider { 8 | def time(): TimeProvider.Time 9 | } 10 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/utils/utils.scala: -------------------------------------------------------------------------------- 1 | package scorex.core 2 | 3 | import java.security.SecureRandom 4 | 5 | import scala.annotation.tailrec 6 | import scala.concurrent.duration._ 7 | import scala.util.{Failure, Success, Try} 8 | 9 | package object utils { 10 | 11 | @deprecated("Use scorex.util.ScorexLogging instead.", "scorex-util 0.1.0") 12 | type ScorexLogging = scorex.util.ScorexLogging 13 | 14 | /** 15 | * @param block - function to profile 16 | * @return - execution time in seconds and function result 17 | */ 18 | def profile[R](block: => R): (Float, R) = { 19 | val t0 = System.nanoTime() 20 | val result = block // call-by-name 21 | val t1 = System.nanoTime() 22 | ((t1 - t0).toFloat / 1000000000, result) 23 | } 24 | 25 | def toTry(b: Boolean, msg: String): Try[Unit] = b match { 26 | case true => Success(Unit) 27 | case false => Failure(new Exception(msg)) 28 | } 29 | 30 | @tailrec 31 | final def untilTimeout[T](timeout: FiniteDuration, 32 | delay: FiniteDuration = 100.milliseconds)(fn: => T): T = { 33 | Try { 34 | fn 35 | } match { 36 | case Success(x) => x 37 | case _ if timeout > delay => 38 | Thread.sleep(delay.toMillis) 39 | untilTimeout(timeout - delay, delay)(fn) 40 | case Failure(e) => throw e 41 | } 42 | } 43 | 44 | def randomBytes(howMany: Int): Array[Byte] = { 45 | val r = new Array[Byte](howMany) 46 | new SecureRandom().nextBytes(r) //overrides r 47 | r 48 | } 49 | 50 | def concatBytes(seq: Traversable[Array[Byte]]): Array[Byte] = { 51 | val length: Int = seq.map(_.length).sum 52 | val result: Array[Byte] = new Array[Byte](length) 53 | var pos: Int = 0 54 | seq.foreach { array => 55 | System.arraycopy(array, 0, result, pos, array.length) 56 | pos += array.length 57 | } 58 | result 59 | } 60 | 61 | def concatFixLengthBytes(seq: Traversable[Array[Byte]]): Array[Byte] = seq.headOption match { 62 | case None => Array[Byte]() 63 | case Some(head) => concatFixLengthBytes(seq, head.length) 64 | } 65 | 66 | 67 | def concatFixLengthBytes(seq: Traversable[Array[Byte]], length: Int): Array[Byte] = { 68 | val result: Array[Byte] = new Array[Byte](seq.toSeq.length * length) 69 | var index = 0 70 | seq.foreach { s => 71 | Array.copy(s, 0, result, index, length) 72 | index += length 73 | } 74 | result 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/validation/ModifierError.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.validation 2 | 3 | import scala.util.control.NoStackTrace 4 | 5 | /** Base trait for errors that were occurred during NodeView Modifier validation 6 | */ 7 | trait ModifierError { 8 | def message: String 9 | def isFatal: Boolean 10 | def toThrowable: Throwable 11 | 12 | def info: String = { 13 | val fatality = if (isFatal) "fatally" else "recoverably" 14 | s"Modifier Validation failed $fatality: $message" 15 | } 16 | } 17 | 18 | /** Permanent modifier error that could not be recovered in future even after any history updates 19 | */ 20 | @SuppressWarnings(Array("org.wartremover.warts.Null")) 21 | class MalformedModifierError(val message: String, cause: Option[Throwable] = None) 22 | extends Exception(message, cause.orNull) with ModifierError { 23 | def isFatal: Boolean = true 24 | def toThrowable: Throwable = this 25 | } 26 | 27 | /** Temporary modifier error that may be recovered in future after some history updates. 28 | * When an instance is created, the stack trace is not collected which makes this exception lightweight. 29 | */ 30 | @SuppressWarnings(Array("org.wartremover.warts.Null")) 31 | class RecoverableModifierError(val message: String, cause: Option[Throwable] = None) 32 | extends Exception(message, cause.orNull) with ModifierError with NoStackTrace { 33 | def isFatal: Boolean = false 34 | def toThrowable: Throwable = this 35 | } 36 | 37 | 38 | /** Composite error class that can hold more than one modifier error inside. This was not made a `ModifierError` instance 39 | * intentionally to prevent nesting `MultipleErrors` to `MultipleErrors` 40 | */ 41 | @SuppressWarnings(Array("org.wartremover.warts.Null")) 42 | case class MultipleErrors(errors: Seq[ModifierError]) 43 | extends Exception(errors.mkString(" | "), errors.headOption.map(_.toThrowable).orNull) { 44 | def isFatal: Boolean = errors.exists(_.isFatal) 45 | } 46 | -------------------------------------------------------------------------------- /src/main/scala/scorex/core/validation/ValidationSettings.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.validation 2 | 3 | import scorex.core.validation.ValidationResult.Invalid 4 | 5 | /** 6 | * Specifies the strategy to by used (fail-fast or error-accumulative), a set of 7 | * activated validation rules with corresponding error messages 8 | */ 9 | abstract class ValidationSettings { 10 | val isFailFast: Boolean 11 | 12 | def getError(id: Short, e: Throwable): Invalid = getError(id, e.getMessage) 13 | 14 | def getError(id: Short, details: String): Invalid 15 | 16 | def getError(id: Short): Invalid = getError(id, "") 17 | 18 | def isActive(id: Short): Boolean 19 | } 20 | -------------------------------------------------------------------------------- /src/main/scala/scorex/util/serialization/VLQByteStringReader.scala: -------------------------------------------------------------------------------- 1 | package scorex.util.serialization 2 | 3 | import java.nio.ByteOrder 4 | 5 | import akka.util.ByteString 6 | 7 | class VLQByteStringReader(byteString: ByteString) extends VLQReader { 8 | 9 | type CH = ByteString 10 | 11 | private var it = byteString.iterator 12 | private var _position = 0 13 | private var _mark = 0 14 | private implicit val byteOrder: ByteOrder = ByteOrder.BIG_ENDIAN 15 | 16 | @inline 17 | override def newReader(chunk: ByteString): Reader.Aux[CH] = { 18 | new VLQByteStringReader(chunk) 19 | } 20 | 21 | /** 22 | * Get a byte at current position without advancing the position. 23 | * 24 | * @return byte at current position 25 | */ 26 | @inline 27 | override def peekByte(): Byte = byteString(position) 28 | 29 | @inline 30 | override def getByte(): Byte = { 31 | incPosition() 32 | it.getByte 33 | } 34 | 35 | @inline 36 | override def getBytes(size: Int): Array[Byte] = { 37 | require(size <= remaining, s"Not enough bytes in the buffer: $size") 38 | incPosition(size) 39 | it.getBytes(size) 40 | } 41 | 42 | @inline 43 | override def getChunk(size: Int): ByteString = { 44 | it.getByteString(size) 45 | } 46 | 47 | @inline 48 | override def mark(): this.type = { 49 | _mark = _position 50 | this 51 | } 52 | 53 | @inline 54 | override def consumed: Int = _position - _mark 55 | 56 | @inline 57 | override def position: Int = _position 58 | 59 | @inline 60 | override def position_=(p: Int): Unit = { 61 | _position = p 62 | it = byteString.iterator.drop(p) 63 | } 64 | 65 | @inline 66 | override def remaining: Int = it.len 67 | 68 | @inline 69 | private def incPosition(): Unit = { 70 | _position += 1 71 | } 72 | 73 | @inline 74 | private def incPosition(size: Int): Unit = { 75 | _position += size 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/main/scala/scorex/util/serialization/VLQByteStringWriter.scala: -------------------------------------------------------------------------------- 1 | package scorex.util.serialization 2 | 3 | import java.nio.ByteOrder 4 | 5 | import akka.util.ByteString 6 | 7 | class VLQByteStringWriter extends VLQWriter { 8 | override type CH = ByteString 9 | private implicit val byteOrder: ByteOrder = ByteOrder.BIG_ENDIAN 10 | 11 | @inline 12 | override def newWriter(): Writer.Aux[CH] = { 13 | new VLQByteStringWriter() 14 | } 15 | 16 | private val builder = ByteString.createBuilder 17 | 18 | @inline 19 | override def length(): Int = builder.length 20 | 21 | @inline 22 | override def putChunk(byteString: ByteString): this.type = { 23 | builder.append(byteString) 24 | this 25 | } 26 | 27 | @inline 28 | override def put(x: Byte): this.type = { 29 | builder.putByte(x) 30 | this 31 | } 32 | 33 | @inline 34 | override def putBoolean(x: Boolean): this.type = { 35 | val byte: Byte = if (x) 0x01 else 0x00 36 | builder.putByte(byte) 37 | this 38 | } 39 | 40 | override def putBytes(xs: Array[Byte], 41 | offset: Int, 42 | length: Int): VLQByteStringWriter.this.type = { 43 | builder.putBytes(xs, offset, length) 44 | this 45 | } 46 | 47 | @inline 48 | override def putBytes(xs: Array[Byte]): this.type = { 49 | builder.putBytes(xs) 50 | this 51 | } 52 | 53 | @inline 54 | override def result(): ByteString = { 55 | builder.result() 56 | } 57 | 58 | @inline override def toBytes: Array[Byte] = { 59 | builder.result().toArray 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/test/scala/scorex/core/api/http/UtilsApiRouteSpec.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.api.http 2 | 3 | import java.net.InetSocketAddress 4 | 5 | import akka.http.scaladsl.model.{ContentTypes, StatusCodes, HttpEntity} 6 | import akka.http.scaladsl.server 7 | import akka.http.scaladsl.testkit.{ScalatestRouteTest, RouteTestTimeout} 8 | import akka.testkit.TestDuration 9 | import io.circe.syntax._ 10 | import org.scalatest.flatspec.AnyFlatSpec 11 | import org.scalatest.matchers.should.Matchers 12 | import scorex.core.settings.RESTApiSettings 13 | 14 | import scala.concurrent.duration._ 15 | import scala.language.postfixOps 16 | 17 | class UtilsApiRouteSpec extends AnyFlatSpec 18 | with Matchers 19 | with ScalatestRouteTest 20 | with Stubs { 21 | 22 | implicit val timeout: RouteTestTimeout = RouteTestTimeout(15.seconds dilated) 23 | 24 | val addr: InetSocketAddress = new InetSocketAddress("localhost", 8080) 25 | val restApiSettings: RESTApiSettings = RESTApiSettings(addr, None, None, 10 seconds) 26 | val prefix: String = "/utils" 27 | val routes: server.Route = UtilsApiRoute(restApiSettings).route 28 | 29 | it should "send random seeds" in { 30 | Get(prefix + "/seed") ~> routes ~> check { 31 | status shouldBe StatusCodes.OK 32 | responseAs[String] should not be empty 33 | } 34 | 35 | Get(prefix + "/seed/32") ~> routes ~> check { 36 | status shouldBe StatusCodes.OK 37 | responseAs[String] should not be empty 38 | } 39 | 40 | Get(prefix + "/seed/64") ~> routes ~> check { 41 | status shouldBe StatusCodes.OK 42 | responseAs[String] should not be empty 43 | } 44 | } 45 | 46 | it should "hash string with blake2b" in { 47 | val msg = HttpEntity("hash_me".asJson.toString).withContentType(ContentTypes.`application/json`) 48 | Post(prefix + "/hash/blake2b", msg) ~> routes ~> check { 49 | status shouldBe StatusCodes.OK 50 | responseAs[String] should not be empty 51 | responseAs[String] should not be msg 52 | } 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/test/scala/scorex/core/network/HandshakeSpecification.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.network 2 | 3 | import java.net.InetSocketAddress 4 | 5 | import org.scalacheck.Gen 6 | import org.scalatest.matchers.should.Matchers 7 | import org.scalatest.propspec.AnyPropSpec 8 | import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks 9 | import scorex.ObjectGenerators 10 | import scorex.core.app.Version 11 | import scorex.core.network.message.HandshakeSpec 12 | 13 | 14 | class HandshakeSpecification extends AnyPropSpec 15 | with ScalaCheckPropertyChecks 16 | with Matchers 17 | with ObjectGenerators { 18 | 19 | private val featSerializers = Map(FullNodePeerFeature.featureId -> FullNodePeerFeature.serializer) 20 | private val noSerializers: PeerFeature.Serializers = Map() 21 | 22 | property("handshake should remain the same after serialization/deserialization") { 23 | val serializersGen = Gen.oneOf(featSerializers, noSerializers) 24 | val featuresGen: Gen[Seq[PeerFeature]] = Gen.oneOf(Seq(FullNodePeerFeature), Seq[PeerFeature]()) 25 | 26 | forAll(Gen.alphaStr, appVersionGen, Gen.alphaStr, Gen.option(inetSocketAddressGen), Gen.posNum[Long], serializersGen) { 27 | (appName: String, 28 | av: Version, 29 | nodeName: String, 30 | isaOpt: Option[InetSocketAddress], 31 | time: Long, 32 | serializers: PeerFeature.Serializers) => 33 | 34 | whenever(appName.nonEmpty) { 35 | val feats: Seq[PeerFeature] = featuresGen.sample.getOrElse(Seq()) 36 | 37 | val handshakeSerializer = new HandshakeSpec(serializers, Int.MaxValue) 38 | 39 | val h = Handshake(PeerSpec(appName, av, nodeName, isaOpt, feats), time) 40 | val hr = handshakeSerializer.parseBytes(handshakeSerializer.toBytes(h)) 41 | hr.peerSpec.agentName should be(h.peerSpec.agentName) 42 | hr.peerSpec.protocolVersion should be(h.peerSpec.protocolVersion) 43 | hr.peerSpec.declaredAddress should be(h.peerSpec.declaredAddress) 44 | if (serializers.nonEmpty) hr.peerSpec.features shouldBe h.peerSpec.features else hr.peerSpec.features.isEmpty shouldBe true 45 | hr.time should be(h.time) 46 | } 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /src/test/scala/scorex/core/network/peer/PeerManagerSpec.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.network.peer 2 | 3 | import java.net.InetSocketAddress 4 | 5 | import akka.actor.{ActorRef, ActorSystem} 6 | import akka.testkit.TestProbe 7 | import scorex.core.app.ScorexContext 8 | import scorex.network.NetworkTests 9 | 10 | 11 | class PeerManagerSpec extends NetworkTests { 12 | 13 | import scorex.core.network.peer.PeerManager.ReceivableMessages.{AddOrUpdatePeer, GetAllPeers} 14 | 15 | type Data = Map[InetSocketAddress, PeerInfo] 16 | private val DefaultPort = 27017 17 | 18 | it should "ignore adding self as a peer" in { 19 | implicit val system = ActorSystem() 20 | val p = TestProbe("p")(system) 21 | implicit val defaultSender: ActorRef = p.testActor 22 | 23 | 24 | val selfAddress = settings.network.bindAddress 25 | val scorexContext = ScorexContext(Seq.empty, Seq.empty, None, timeProvider, Some(selfAddress)) 26 | val peerManager = PeerManagerRef(settings, scorexContext)(system) 27 | val peerInfo = getPeerInfo(selfAddress) 28 | 29 | peerManager ! AddOrUpdatePeer(peerInfo) 30 | peerManager ! GetAllPeers 31 | val data = p.expectMsgClass(classOf[Data]) 32 | 33 | data.keySet should not contain selfAddress 34 | system.terminate() 35 | } 36 | 37 | it should "added peer be returned in GetAllPeers" in { 38 | implicit val system = ActorSystem() 39 | val p = TestProbe("p")(system) 40 | implicit val defaultSender: ActorRef = p.testActor 41 | 42 | val scorexContext = ScorexContext(Seq.empty, Seq.empty, None, timeProvider, None) 43 | val peerManager = PeerManagerRef(settings, scorexContext)(system) 44 | val peerAddress = new InetSocketAddress("1.1.1.1", DefaultPort) 45 | val peerInfo = getPeerInfo(peerAddress) 46 | 47 | peerManager ! AddOrUpdatePeer(peerInfo) 48 | peerManager ! GetAllPeers 49 | 50 | val data = p.expectMsgClass(classOf[Data]) 51 | data.keySet should contain(peerAddress) 52 | system.terminate() 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/test/scala/scorex/core/serialization/SerializerRegistrySpec.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.serialization 2 | 3 | import io.circe.Encoder 4 | import io.circe.syntax._ 5 | import org.scalatest.flatspec.AnyFlatSpec 6 | import org.scalatest.matchers.should.Matchers 7 | import scorex.core.serialization.SerializerRegistry.SerializerRecord 8 | 9 | import scala.reflect.ClassTag 10 | import scala.util.{Random, Try} 11 | 12 | class SerializerRegistrySpec extends AnyFlatSpec with Matchers { 13 | 14 | sealed trait Creature 15 | sealed case class Human(name: String, country: String) extends Creature 16 | sealed case class Animal(family: String, legs: Int) extends Creature 17 | 18 | val humanEnc: Encoder[Human] = (h: Human) => 19 | Map( 20 | "name" -> h.name.asJson, 21 | "country" -> h.country.asJson 22 | ).asJson 23 | 24 | val animalEnc: Encoder[Animal] = (a: Animal) => 25 | Map( 26 | "family" -> a.family.asJson, 27 | "legs" -> a.legs.asJson 28 | ).asJson 29 | 30 | 31 | "`toJson`" should "be able to generate json at runtime when encoders are registered" in { 32 | 33 | val reg = SerializerRegistry(Seq(SerializerRecord(humanEnc), SerializerRecord(animalEnc))) 34 | 35 | val c: Creature = randomCreature 36 | 37 | val clazz = ClassTag(c.getClass).runtimeClass 38 | 39 | val result = reg.toJson(clazz, c) 40 | result.isRight shouldBe true 41 | result.right.get shouldBe Try(humanEnc.apply(c.asInstanceOf[Human])).getOrElse(animalEnc.apply(c.asInstanceOf[Animal])) 42 | } 43 | 44 | 45 | "`toJson`" should "generate an exception when encoders are not registered" in { 46 | 47 | val reg = SerializerRegistry(Seq(SerializerRecord(humanEnc))) 48 | 49 | val c: Creature = Animal("big cats", 4) 50 | 51 | val clazz = ClassTag(c.getClass).runtimeClass 52 | 53 | val result = reg.toJson(clazz, c) 54 | result.isRight shouldBe false 55 | } 56 | 57 | private def randomCreature: Creature = { 58 | if (Random.nextInt() % 2 == 0) Animal("big cats", 4) 59 | else Human("Kim", "North Korea") 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/test/scala/scorex/core/transaction/box/proposition/PublicKey25519PropositionSpecification.scala: -------------------------------------------------------------------------------- 1 | package scorex.core.transaction.box.proposition 2 | 3 | import org.scalatest.matchers.should.Matchers 4 | import org.scalatest.propspec.AnyPropSpec 5 | import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks 6 | import scorex.core.transaction.state.PrivateKey25519Companion 7 | 8 | class PublicKey25519PropositionSpecification extends AnyPropSpec 9 | with ScalaCheckPropertyChecks 10 | with Matchers { 11 | 12 | property("PublicKey25519Proposition generates valid addresses") { 13 | forAll() { (seed: Array[Byte]) => 14 | val pub = PrivateKey25519Companion.generateKeys(seed)._2 15 | PublicKey25519Proposition.validPubKey(pub.address).isSuccess shouldBe true 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /src/test/scala/scorex/crypto/SigningFunctionsSpecification.scala: -------------------------------------------------------------------------------- 1 | package scorex.crypto 2 | 3 | import org.scalatest.matchers.should.Matchers 4 | import org.scalatest.propspec.AnyPropSpec 5 | import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks 6 | import scorex.core.transaction.state.{PrivateKey25519Serializer, PrivateKey25519Companion} 7 | 8 | 9 | class SigningFunctionsSpecification extends AnyPropSpec 10 | with ScalaCheckPropertyChecks 11 | with Matchers { 12 | 13 | property("PrivateKey25519Companion generates valid keypair") { 14 | forAll() { (seed1: Array[Byte], message1: Array[Byte], seed2: Array[Byte], message2: Array[Byte]) => 15 | whenever(!seed1.sameElements(seed2) && !message1.sameElements(message2)) { 16 | val priv = PrivateKey25519Companion.generateKeys(seed1)._1 17 | val priv2 = PrivateKey25519Companion.generateKeys(seed2)._1 18 | val sig = PrivateKey25519Companion.sign(priv, message1) 19 | sig.isValid(priv.publicImage, message1) shouldBe true 20 | 21 | sig.isValid(priv.publicImage, message2) shouldBe false 22 | sig.isValid(priv2.publicImage, message1) shouldBe false 23 | } 24 | } 25 | } 26 | 27 | property("PrivateKey25519Companion serialization") { 28 | forAll() { (seed: Array[Byte], message: Array[Byte]) => 29 | val priv = PrivateKey25519Companion.generateKeys(seed)._1 30 | val parsed = PrivateKey25519Serializer.parseByteString(PrivateKey25519Serializer.toByteString(priv)) 31 | 32 | parsed.publicImage.address shouldBe priv.publicImage.address 33 | PrivateKey25519Companion.sign(parsed, message) 34 | .isValid(priv.publicImage, message) shouldBe true 35 | } 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /src/test/scala/scorex/network/NetworkTests.scala: -------------------------------------------------------------------------------- 1 | package scorex.network 2 | 3 | import java.net.InetSocketAddress 4 | 5 | import org.scalatest.flatspec.AnyFlatSpec 6 | import org.scalatest.matchers.should.Matchers 7 | import scorex.core.app.Version 8 | import scorex.core.network.PeerSpec 9 | import scorex.core.network.peer.PeerInfo 10 | import scorex.core.settings.ScorexSettings 11 | import scorex.core.utils.{NetworkTimeProvider, TimeProvider} 12 | import scala.concurrent.ExecutionContext.Implicits.global 13 | 14 | class NetworkTests extends AnyFlatSpec with Matchers { 15 | 16 | protected val settings: ScorexSettings = ScorexSettings.read(None) 17 | protected val timeProvider: NetworkTimeProvider = new NetworkTimeProvider(settings.ntp) 18 | 19 | protected def currentTime(): TimeProvider.Time = timeProvider.time() 20 | 21 | protected def getPeerInfo(address: InetSocketAddress, nameOpt: Option[String] = None): PeerInfo = { 22 | val data = PeerSpec("full node", Version.last, nameOpt.getOrElse(address.toString), Some(address), Seq()) 23 | PeerInfo(data, currentTime(), None) 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /src/test/scala/scorex/network/PeerConnectionHandlerSpecification.scala: -------------------------------------------------------------------------------- 1 | package scorex.network 2 | 3 | import org.scalatest.flatspec.AnyFlatSpec 4 | import org.scalatest.matchers.should.Matchers 5 | 6 | class PeerConnectionHandlerSpecification extends AnyFlatSpec with Matchers { 7 | //todo: for Dmitry (?) : write tests for handshaking process 8 | } 9 | -------------------------------------------------------------------------------- /src/test/scala/scorex/util/ScorexLoggingSpec.scala: -------------------------------------------------------------------------------- 1 | package scorex.util 2 | 3 | import org.scalatest.flatspec.AnyFlatSpec 4 | import org.scalatest.matchers.should.Matchers 5 | 6 | class ScorexLoggingSpec extends AnyFlatSpec with Matchers with ScorexLogging { 7 | 8 | "Logger" should "evaluate messages only if the respective log level is enabled" in { 9 | var i = 0 10 | log.info(s"Info level message, should be evaluated ${i = 1} i = $i") 11 | i shouldBe 1 12 | log.trace(s"Trace level message, should not be evaluated ${i = 2} i = $i") 13 | i shouldBe 1 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /src/test/scala/scorex/util/serialization/VQLByteStringReaderWriterSpecification.scala: -------------------------------------------------------------------------------- 1 | package scorex.util.serialization 2 | 3 | import akka.util.ByteString 4 | 5 | class VQLByteStringReaderWriterSpecification extends VLQReaderWriterSpecification { 6 | 7 | override def byteBufReader(bytes: Array[Byte]): VLQReader = { 8 | new VLQByteStringReader(ByteString(bytes)) 9 | } 10 | 11 | override def byteArrayWriter(): VLQWriter = { 12 | new VLQByteStringWriter() 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /testkit/build.sbt: -------------------------------------------------------------------------------- 1 | name := "scorex-testkit" 2 | 3 | libraryDependencies ++= Seq( 4 | "org.scalactic" %% "scalactic" % "3.0.1", 5 | "org.scalatest" %% "scalatest" % "3.1.1", 6 | "org.scalacheck" %% "scalacheck" % "1.14.+", 7 | "org.scalatestplus" %% "scalatestplus-scalacheck" % "3.1.0.0-RC2", 8 | "com.typesafe.akka" %% "akka-testkit" % "2.6.10" 9 | ) 10 | 11 | fork in Test := true 12 | 13 | javaOptions in Test ++= Seq("-Xmx2G") 14 | 15 | parallelExecution in Test := false -------------------------------------------------------------------------------- /testkit/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.5.1") -------------------------------------------------------------------------------- /testkit/src/main/scala/scorex/testkit/BlockchainPerformance.scala: -------------------------------------------------------------------------------- 1 | package scorex.testkit 2 | 3 | import scorex.core.PersistentNodeViewModifier 4 | import scorex.core.consensus.{History, SyncInfo} 5 | import scorex.core.transaction.box.proposition.Proposition 6 | import scorex.core.transaction.state.MinimalState 7 | import scorex.core.transaction.{MemoryPool, Transaction} 8 | import scorex.testkit.properties.mempool.MempoolFilterPerformanceTest 9 | 10 | /** 11 | * Performance test for implementations 12 | */ 13 | trait BlockchainPerformance[ 14 | TX <: Transaction, 15 | PM <: PersistentNodeViewModifier, 16 | SI <: SyncInfo, 17 | MPool <: MemoryPool[TX, MPool], 18 | ST <: MinimalState[PM, ST], 19 | HT <: History[PM, SI, HT]] extends MempoolFilterPerformanceTest[TX, MPool] 20 | -------------------------------------------------------------------------------- /testkit/src/main/scala/scorex/testkit/BlockchainSanity.scala: -------------------------------------------------------------------------------- 1 | package scorex.testkit 2 | 3 | import scorex.core.{PersistentNodeViewModifier, TransactionsCarryingPersistentNodeViewModifier} 4 | import scorex.core.consensus.{History, SyncInfo} 5 | import scorex.core.transaction.box.Box 6 | import scorex.core.transaction.box.proposition.Proposition 7 | import scorex.core.transaction.{BoxTransaction, MemoryPool} 8 | import scorex.mid.state.BoxMinimalState 9 | import scorex.testkit.generators.AllModifierProducers 10 | import scorex.testkit.properties._ 11 | import scorex.testkit.properties.mempool.{MempoolFilterPerformanceTest, MempoolRemovalTest, MempoolTransactionsTest} 12 | import scorex.testkit.properties.state.StateApplicationTest 13 | import scorex.testkit.properties.state.box.{BoxStateApplyChangesTest, BoxStateChangesGenerationTest, BoxStateRollbackTest} 14 | 15 | /** 16 | * The idea of this class is to get some generators and test some situations, common for all blockchains 17 | */ 18 | trait BlockchainSanity[P <: Proposition, 19 | TX <: BoxTransaction[P, B], 20 | PM <: PersistentNodeViewModifier, 21 | CTM <: PM with TransactionsCarryingPersistentNodeViewModifier[TX], 22 | SI <: SyncInfo, 23 | B <: Box[P], 24 | MPool <: MemoryPool[TX, MPool], 25 | ST <: BoxMinimalState[P, B, TX, PM, ST], 26 | HT <: History[PM, SI, HT]] 27 | extends 28 | BoxStateChangesGenerationTest[P, TX, PM, B, ST] 29 | with StateApplicationTest[PM, ST] 30 | with HistoryTests[TX, PM, SI, HT] 31 | with BoxStateApplyChangesTest[P, TX, PM, B, ST] 32 | with WalletSecretsTest[P, TX, PM] 33 | with BoxStateRollbackTest[P, TX, PM, CTM, B, ST] 34 | with MempoolTransactionsTest[TX, MPool] 35 | with MempoolFilterPerformanceTest[TX, MPool] 36 | with MempoolRemovalTest[TX, MPool, PM, CTM, HT, SI] 37 | with AllModifierProducers[TX, MPool, PM, CTM, ST, SI, HT] 38 | with NodeViewHolderTests[TX, PM, ST, SI, HT, MPool] 39 | with NodeViewSynchronizerTests[TX, PM, ST, SI, HT, MPool] { 40 | } 41 | -------------------------------------------------------------------------------- /testkit/src/main/scala/scorex/testkit/SerializationTests.scala: -------------------------------------------------------------------------------- 1 | package scorex.testkit 2 | 3 | import org.scalacheck.Gen 4 | import org.scalatest.Assertion 5 | import org.scalatest.matchers.should.Matchers 6 | import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks 7 | import scorex.core.serialization.ScorexSerializer 8 | 9 | trait SerializationTests extends ScalaCheckPropertyChecks with Matchers { 10 | def checkSerializationRoundtrip[A](generator: Gen[A], serializer: ScorexSerializer[A]): Assertion = { 11 | forAll(generator) { b: A => 12 | val recovered = serializer.parseBytes(serializer.toBytes(b)) 13 | serializer.toBytes(b) shouldEqual serializer.toBytes(recovered) 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /testkit/src/main/scala/scorex/testkit/TestkitHelpers.scala: -------------------------------------------------------------------------------- 1 | package scorex.testkit 2 | 3 | trait TestkitHelpers { 4 | 5 | val MinTestsOk = 100 6 | 7 | def check(minTestsOk:Int)(f: Int => Unit): Unit = (0 until minTestsOk) foreach (i => f(i)) 8 | 9 | def check(f: Int => Unit): Unit = check(MinTestsOk)(f) 10 | } 11 | -------------------------------------------------------------------------------- /testkit/src/main/scala/scorex/testkit/generators/AllModifierProducers.scala: -------------------------------------------------------------------------------- 1 | package scorex.testkit.generators 2 | 3 | import scorex.core.consensus.{History, SyncInfo} 4 | import scorex.core.{PersistentNodeViewModifier, TransactionsCarryingPersistentNodeViewModifier} 5 | import scorex.core.transaction.{MemoryPool, Transaction} 6 | import scorex.core.transaction.state.MinimalState 7 | 8 | 9 | trait AllModifierProducers[ 10 | TX <: Transaction, 11 | MPool <: MemoryPool[TX, MPool], 12 | PM <: PersistentNodeViewModifier, 13 | CTM <: PM with TransactionsCarryingPersistentNodeViewModifier[TX], 14 | ST <: MinimalState[PM, ST], 15 | SI <: SyncInfo, HT <: History[PM, SI, HT]] 16 | extends SemanticallyValidModifierProducer[PM, ST] 17 | with SyntacticallyTargetedModifierProducer[PM, SI, HT] 18 | with ArbitraryTransactionsCarryingModifierProducer[TX, MPool, PM, CTM] 19 | with TotallyValidModifierProducer[PM, ST, SI, HT] 20 | with SemanticallyValidTransactionsCarryingModifier[TX, PM, CTM, ST] 21 | -------------------------------------------------------------------------------- /testkit/src/main/scala/scorex/testkit/generators/ArbitraryTransactionsCarryingModifierProducer.scala: -------------------------------------------------------------------------------- 1 | package scorex.testkit.generators 2 | 3 | import scorex.core.{PersistentNodeViewModifier, TransactionsCarryingPersistentNodeViewModifier} 4 | import scorex.core.transaction.{MemoryPool, Transaction} 5 | import scorex.core.transaction.box.proposition.Proposition 6 | 7 | /** 8 | * Produces a modifier with transactions, not necessary syntatically or semantically valid 9 | */ 10 | trait ArbitraryTransactionsCarryingModifierProducer[ 11 | TX <: Transaction, 12 | MPool <: MemoryPool[TX, MPool], 13 | PM <: PersistentNodeViewModifier, 14 | CTM <: PM with TransactionsCarryingPersistentNodeViewModifier[TX]] { 15 | 16 | def modifierWithTransactions(memoryPoolOpt: Option[MPool], customTransactionsOpt: Option[Seq[TX]]): CTM 17 | } 18 | -------------------------------------------------------------------------------- /testkit/src/main/scala/scorex/testkit/generators/CoreGenerators.scala: -------------------------------------------------------------------------------- 1 | package scorex.testkit.generators 2 | 3 | import org.scalacheck.Gen 4 | import scorex.ObjectGenerators 5 | import scorex.core.{VersionTag, idToVersion} 6 | 7 | //Generators of objects from scorex-core 8 | trait CoreGenerators extends ObjectGenerators { 9 | lazy val versionTagGen: Gen[VersionTag] = modifierIdGen.map(id => idToVersion(id)) 10 | } -------------------------------------------------------------------------------- /testkit/src/main/scala/scorex/testkit/generators/CustomModifierProducer.scala: -------------------------------------------------------------------------------- 1 | package scorex.testkit.generators 2 | 3 | import scorex.core.PersistentNodeViewModifier 4 | import scorex.core.consensus.{History, SyncInfo} 5 | import scorex.core.transaction.state.MinimalState 6 | 7 | sealed trait ModifierProducerTemplateItem 8 | 9 | case object SynInvalid extends ModifierProducerTemplateItem 10 | case object Valid extends ModifierProducerTemplateItem 11 | 12 | trait CustomModifierProducer[PM <: PersistentNodeViewModifier, ST <: MinimalState[PM, ST], 13 | SI <: SyncInfo, HT <: History[PM, SI, HT]] { 14 | 15 | def customModifiers(history: HT, 16 | state: ST, 17 | template: Seq[ModifierProducerTemplateItem]): Seq[PM] 18 | } 19 | -------------------------------------------------------------------------------- /testkit/src/main/scala/scorex/testkit/generators/SemanticallyInvalidModifierProducer.scala: -------------------------------------------------------------------------------- 1 | package scorex.testkit.generators 2 | 3 | import scorex.core.PersistentNodeViewModifier 4 | import scorex.core.transaction.state.MinimalState 5 | 6 | 7 | trait SemanticallyInvalidModifierProducer[PM <: PersistentNodeViewModifier, ST <: MinimalState[PM, ST]] { 8 | def semanticallyInvalidModifier(state: ST): PM 9 | } 10 | -------------------------------------------------------------------------------- /testkit/src/main/scala/scorex/testkit/generators/SemanticallyValidModifierProducer.scala: -------------------------------------------------------------------------------- 1 | package scorex.testkit.generators 2 | 3 | import scorex.core.PersistentNodeViewModifier 4 | import scorex.core.transaction.state.MinimalState 5 | 6 | 7 | trait SemanticallyValidModifierProducer[PM <: PersistentNodeViewModifier, ST <: MinimalState[PM, ST]] { 8 | def semanticallyValidModifier(state: ST): PM 9 | } 10 | 11 | 12 | -------------------------------------------------------------------------------- /testkit/src/main/scala/scorex/testkit/generators/SemanticallyValidTransactionsCarryingModifier.scala: -------------------------------------------------------------------------------- 1 | package scorex.testkit.generators 2 | 3 | import scorex.core.{PersistentNodeViewModifier, TransactionsCarryingPersistentNodeViewModifier} 4 | import scorex.core.transaction.Transaction 5 | import scorex.core.transaction.box.proposition.Proposition 6 | import scorex.core.transaction.state.MinimalState 7 | 8 | 9 | trait SemanticallyValidTransactionsCarryingModifier[TX <: Transaction, 10 | PM <: PersistentNodeViewModifier, 11 | CTM <: PM with TransactionsCarryingPersistentNodeViewModifier[TX], 12 | ST <: MinimalState[PM, ST]] { 13 | 14 | def semanticallyValidModifier(state: ST): CTM 15 | def genValidTransactionPair(state: ST): Seq[TX] 16 | def semanticallyValidModifierWithCustomTransactions(state: ST, transactions: Seq[TX]): CTM 17 | } 18 | -------------------------------------------------------------------------------- /testkit/src/main/scala/scorex/testkit/generators/SyntacticallyTargetedModifierProducer.scala: -------------------------------------------------------------------------------- 1 | package scorex.testkit.generators 2 | 3 | import scorex.core.PersistentNodeViewModifier 4 | import scorex.core.consensus.{History, SyncInfo} 5 | 6 | 7 | trait SyntacticallyTargetedModifierProducer[PM <: PersistentNodeViewModifier, SI <: SyncInfo, HT <: History[PM, SI, HT]] { 8 | def syntacticallyValidModifier(history: HT): PM 9 | 10 | def syntacticallyInvalidModifier(history: HT): PM 11 | } 12 | -------------------------------------------------------------------------------- /testkit/src/main/scala/scorex/testkit/generators/TotallyValidModifierProducer.scala: -------------------------------------------------------------------------------- 1 | package scorex.testkit.generators 2 | 3 | import scorex.core.PersistentNodeViewModifier 4 | import scorex.core.consensus.{History, SyncInfo} 5 | import scorex.core.transaction.state.MinimalState 6 | 7 | 8 | trait TotallyValidModifierProducer[PM <: PersistentNodeViewModifier, ST <: MinimalState[PM, ST], 9 | SI <: SyncInfo, HT <: History[PM, SI, HT]] { 10 | 11 | def totallyValidModifier(history: HT, state: ST): PM 12 | 13 | def totallyValidModifiers(history: HT, state: ST, count: Int): Seq[PM] 14 | } -------------------------------------------------------------------------------- /testkit/src/main/scala/scorex/testkit/properties/WalletSecretsTest.scala: -------------------------------------------------------------------------------- 1 | package scorex.testkit.properties 2 | 3 | import org.scalatest.matchers.should.Matchers 4 | import org.scalatest.propspec.AnyPropSpec 5 | import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks 6 | import scorex.core.PersistentNodeViewModifier 7 | import scorex.core.transaction.Transaction 8 | import scorex.core.transaction.box.proposition.Proposition 9 | import scorex.core.transaction.wallet.BoxWallet 10 | 11 | trait WalletSecretsTest[P <: Proposition, TX <: Transaction, PM <: PersistentNodeViewModifier] 12 | extends AnyPropSpec 13 | with ScalaCheckPropertyChecks 14 | with Matchers { 15 | 16 | val wallet: BoxWallet[P, TX, PM, _] 17 | 18 | property("Wallet should contain secrets for all it's public propositions") { 19 | val publicImages = wallet.publicKeys 20 | assert(publicImages.nonEmpty, "please provide wallet with at least one secret") 21 | publicImages.foreach(pi => wallet.secretByPublicImage(pi).isDefined shouldBe true) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /testkit/src/main/scala/scorex/testkit/properties/mempool/MemoryPoolTest.scala: -------------------------------------------------------------------------------- 1 | package scorex.testkit.properties.mempool 2 | 3 | import org.scalacheck.Gen 4 | import scorex.core.transaction.box.proposition.Proposition 5 | import scorex.core.transaction.{MemoryPool, Transaction} 6 | 7 | 8 | trait MemoryPoolTest[TX <: Transaction, MPool <: MemoryPool[TX, MPool]] { 9 | val memPool: MPool 10 | val memPoolGenerator: Gen[MPool] 11 | val transactionGenerator: Gen[TX] 12 | } 13 | -------------------------------------------------------------------------------- /testkit/src/main/scala/scorex/testkit/properties/mempool/MempoolFilterPerformanceTest.scala: -------------------------------------------------------------------------------- 1 | package scorex.testkit.properties.mempool 2 | 3 | import java.security.MessageDigest 4 | 5 | import org.scalatest.matchers.should.Matchers 6 | import org.scalatest.propspec.AnyPropSpec 7 | import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks 8 | import scorex.core.transaction.{MemoryPool, Transaction} 9 | import scorex.core.utils._ 10 | 11 | trait MempoolFilterPerformanceTest[TX <: Transaction, MPool <: MemoryPool[TX, MPool]] 12 | extends AnyPropSpec 13 | with ScalaCheckPropertyChecks 14 | with Matchers 15 | with MemoryPoolTest[TX, MPool] { 16 | 17 | var initializedMempool: Option[MPool] = None 18 | 19 | val thresholdInHashes = 500000 20 | 21 | private val HeatJVMHashesCount = 1000000 //to heat up JVM, just in case it is cold 22 | 23 | val thresholdSecs: Double = { 24 | //heat up 25 | (1 to HeatJVMHashesCount).foreach(i => MessageDigest.getInstance("SHA-256").digest(("dummy" + i).getBytes())) 26 | 27 | val t0 = System.currentTimeMillis() 28 | (1 to thresholdInHashes).foreach(i => MessageDigest.getInstance("SHA-256").digest(("dummy" + i).getBytes())) 29 | val t = System.currentTimeMillis() 30 | (t - t0) / 1000.0 31 | } 32 | 33 | property("Mempool should be able to store a lot of transactions") { 34 | var m: MPool = memPool 35 | (0 until 1000) foreach { _ => 36 | forAll(transactionGenerator) { tx: TX => 37 | m = m.put(tx).get 38 | } 39 | } 40 | m.size should be > 1000 41 | initializedMempool = Some(m) 42 | } 43 | 44 | property("Mempool filter of non-existing transaction should be fast") { 45 | @SuppressWarnings(Array("org.wartremover.warts.OptionPartial")) 46 | val m = initializedMempool.get 47 | forAll(transactionGenerator) { tx: TX => 48 | val (time, _) = profile(m.filter(Seq(tx))) 49 | assert(time < thresholdSecs) 50 | } 51 | } 52 | 53 | property("Mempool filter of existing transaction should be fast") { 54 | @SuppressWarnings(Array("org.wartremover.warts.OptionPartial")) 55 | var m = initializedMempool.get 56 | forAll(transactionGenerator) { tx: TX => 57 | m = m.put(tx).get 58 | val (time, _) = profile(m.filter(Seq(tx))) 59 | assert(time < thresholdSecs) 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /testkit/src/main/scala/scorex/testkit/properties/mempool/MempoolRemovalTest.scala: -------------------------------------------------------------------------------- 1 | package scorex.testkit.properties.mempool 2 | 3 | import org.scalacheck.Gen 4 | import org.scalatest.matchers.should.Matchers 5 | import org.scalatest.propspec.AnyPropSpec 6 | import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks 7 | import scorex.core.{PersistentNodeViewModifier, TransactionsCarryingPersistentNodeViewModifier} 8 | import scorex.core.consensus.{History, SyncInfo} 9 | import scorex.core.transaction.{MemoryPool, Transaction} 10 | import scorex.testkit.TestkitHelpers 11 | import scorex.testkit.generators.ArbitraryTransactionsCarryingModifierProducer 12 | import scorex.util.ScorexLogging 13 | 14 | trait MempoolRemovalTest[ 15 | TX <: Transaction, 16 | MPool <: MemoryPool[TX, MPool], 17 | PM <: PersistentNodeViewModifier, 18 | CTM <: PM with TransactionsCarryingPersistentNodeViewModifier[TX], 19 | HT <: History[PM, SI, HT], 20 | SI <: SyncInfo] extends AnyPropSpec 21 | with ScalaCheckPropertyChecks 22 | with Matchers 23 | with ScorexLogging 24 | with TestkitHelpers 25 | with MemoryPoolTest[TX, MPool] 26 | with ArbitraryTransactionsCarryingModifierProducer[TX, MPool, PM, CTM] { 27 | 28 | val historyGen: Gen[HT] 29 | 30 | //todo: this test doesn't check anything. It should be reworked as a test for node view holder 31 | property("Transactions once added to block should be removed from Mempool") { 32 | val min = 1 33 | val max = 10 34 | forAll(Gen.choose(min, max)) { noOfTransactionsFromMempool: Int => 35 | var m: MPool = memPool 36 | @SuppressWarnings(Array("org.wartremover.warts.OptionPartial")) 37 | var h: HT = historyGen.sample.get 38 | forAll(transactionGenerator) { tx: TX => 39 | m = m.put(tx).get 40 | } 41 | // var prevMempoolSize = m.size 42 | val b = modifierWithTransactions(Some(m), None) 43 | //todo: fix (m.size + b.transactions.get.size) shouldEqual prevMempoolSize 44 | } 45 | } 46 | } 47 | 48 | 49 | -------------------------------------------------------------------------------- /testkit/src/main/scala/scorex/testkit/properties/state/StateTests.scala: -------------------------------------------------------------------------------- 1 | package scorex.testkit.properties.state 2 | 3 | import org.scalacheck.Gen 4 | import org.scalatest.matchers.should.Matchers 5 | import org.scalatest.propspec.AnyPropSpec 6 | import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks 7 | import scorex.core.PersistentNodeViewModifier 8 | import scorex.core.transaction.state.MinimalState 9 | import scorex.testkit.TestkitHelpers 10 | import scorex.testkit.generators.{SemanticallyValidModifierProducer, SemanticallyInvalidModifierProducer, CoreGenerators} 11 | 12 | trait StateTests[PM <: PersistentNodeViewModifier, ST <: MinimalState[PM, ST]] 13 | extends AnyPropSpec 14 | with ScalaCheckPropertyChecks 15 | with Matchers 16 | with CoreGenerators 17 | with TestkitHelpers 18 | with SemanticallyValidModifierProducer[PM, ST] 19 | with SemanticallyInvalidModifierProducer[PM, ST] { 20 | 21 | val checksToMake = 10 22 | 23 | val stateGen: Gen[ST] 24 | } 25 | -------------------------------------------------------------------------------- /testkit/src/main/scala/scorex/testkit/properties/state/box/BoxStateApplyChangesTest.scala: -------------------------------------------------------------------------------- 1 | package scorex.testkit.properties.state.box 2 | 3 | import org.scalacheck.Gen 4 | import scorex.core._ 5 | import scorex.core.transaction.BoxTransaction 6 | import scorex.core.transaction.box.Box 7 | import scorex.core.transaction.box.proposition.Proposition 8 | import scorex.core.transaction.state.BoxStateChanges 9 | import scorex.mid.state.BoxMinimalState 10 | 11 | import scala.util.Random 12 | 13 | @SuppressWarnings(Array("org.wartremover.warts.OptionPartial")) 14 | trait BoxStateApplyChangesTest[P <: Proposition, 15 | TX <: BoxTransaction[P, B], 16 | PM <: PersistentNodeViewModifier, 17 | B <: Box[P], 18 | ST <: BoxMinimalState[P, B, TX, PM, ST]] extends BoxStateTests[P, B, TX, PM, ST] { 19 | 20 | def stateChangesGenerator(state: ST): Gen[BoxStateChanges[P, B]] 21 | 22 | property("BoxMinimalState should be able to add and remove boxes") { 23 | forAll(stateGen, minSuccessful(2)) { state => 24 | val changes = stateChangesGenerator(state).sample.get 25 | changes.toAppend.foreach { insertion => 26 | state.closedBox(insertion.box.id).isDefined shouldBe false 27 | } 28 | changes.toRemove.foreach { removal => 29 | state.closedBox(removal.boxId).isDefined shouldBe true 30 | } 31 | val newVersion = bytesToVersion(Array.fill(32)(Random.nextInt(Byte.MaxValue).toByte)) 32 | val newState = state.applyChanges(changes, newVersion).get 33 | changes.toAppend.foreach { insertion => 34 | newState.closedBox(insertion.box.id).isDefined shouldBe true 35 | } 36 | changes.toRemove.foreach { removal => 37 | newState.closedBox(removal.boxId).isDefined shouldBe false 38 | } 39 | } 40 | 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /testkit/src/main/scala/scorex/testkit/properties/state/box/BoxStateChangesGenerationTest.scala: -------------------------------------------------------------------------------- 1 | package scorex.testkit.properties.state.box 2 | 3 | import scorex.core.{PersistentNodeViewModifier, VersionTag, idToVersion} 4 | import scorex.core.transaction.BoxTransaction 5 | import scorex.core.transaction.box.Box 6 | import scorex.core.transaction.box.proposition.Proposition 7 | import scorex.core.transaction.state.Insertion 8 | import scorex.mid.state.BoxMinimalState 9 | import scorex.testkit.TestkitHelpers 10 | import scorex.testkit.generators.SemanticallyValidModifierProducer 11 | 12 | trait BoxStateChangesGenerationTest[P <: Proposition, 13 | TX <: BoxTransaction[P, B], 14 | PM <: PersistentNodeViewModifier, 15 | B <: Box[P], 16 | ST <: BoxMinimalState[P, B, TX, PM, ST]] 17 | extends BoxStateTests[P, B, TX, PM, ST] 18 | with TestkitHelpers 19 | with SemanticallyValidModifierProducer[PM, ST] { 20 | 21 | 22 | property("State should be able to generate changes from block and apply them") { 23 | check(checksToMake) { _ => 24 | @SuppressWarnings(Array("org.wartremover.warts.OptionPartial")) 25 | val state1 = stateGen.sample.get 26 | val block = semanticallyValidModifier(state1) 27 | val blockChanges = state1.changes(block).get 28 | 29 | blockChanges.toAppend.foreach { case Insertion(b) => 30 | state1.closedBox(b.id) shouldBe None 31 | } 32 | 33 | blockChanges.toRemove.foreach { r => 34 | state1.closedBox(r.boxId).isDefined shouldBe true 35 | } 36 | 37 | val state2 = state1.applyChanges(blockChanges, idToVersion(block.id)).get 38 | 39 | blockChanges.toAppend.foreach { case Insertion(b) => 40 | state2.closedBox(b.id) shouldBe Some(b) 41 | } 42 | blockChanges.toRemove.foreach { r => 43 | state2.closedBox(r.boxId).isDefined shouldBe false 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /testkit/src/main/scala/scorex/testkit/properties/state/box/BoxStateTests.scala: -------------------------------------------------------------------------------- 1 | package scorex.testkit.properties.state.box 2 | 3 | import scorex.core.PersistentNodeViewModifier 4 | import scorex.core.transaction.BoxTransaction 5 | import scorex.core.transaction.box.Box 6 | import scorex.core.transaction.box.proposition.Proposition 7 | import scorex.mid.state.BoxMinimalState 8 | import scorex.testkit.properties.state.StateTests 9 | 10 | 11 | trait BoxStateTests[P <: Proposition, 12 | B <: Box[P], 13 | TX <: BoxTransaction[P, B], 14 | PM <: PersistentNodeViewModifier, 15 | BST <: BoxMinimalState[P, B, TX, PM, BST]] extends StateTests[PM, BST]{ 16 | } 17 | -------------------------------------------------------------------------------- /testkit/src/main/scala/scorex/testkit/utils/AkkaFixture.scala: -------------------------------------------------------------------------------- 1 | package scorex.testkit.utils 2 | 3 | import java.util.concurrent.atomic.AtomicInteger 4 | 5 | import akka.actor.ActorSystem 6 | import akka.testkit.{ImplicitSender, TestKit} 7 | 8 | object SysId { 9 | private val i = new AtomicInteger() 10 | def incrementAndGet(): Int = i.incrementAndGet() 11 | } 12 | 13 | class AkkaFixture 14 | extends TestKit(ActorSystem("WithIsoFix-%d".format(SysId.incrementAndGet()))) 15 | with ImplicitSender 16 | -------------------------------------------------------------------------------- /testkit/src/main/scala/scorex/testkit/utils/FileUtils.scala: -------------------------------------------------------------------------------- 1 | package scorex.testkit.utils 2 | 3 | import java.nio.file.Path 4 | 5 | import org.scalacheck.Gen 6 | 7 | trait FileUtils { 8 | 9 | protected val randomPrefixLength = 10 10 | 11 | val basePath: Path = java.nio.file.Files.createTempDirectory(s"scorex-${System.nanoTime()}") 12 | 13 | sys.addShutdownHook { 14 | remove(basePath) 15 | } 16 | 17 | def createTempFile: java.io.File = { 18 | val dir = createTempDir 19 | val prefix = scala.util.Random.alphanumeric.take(randomPrefixLength).mkString 20 | val suffix = scala.util.Random.alphanumeric.take(randomPrefixLength).mkString 21 | val file = java.nio.file.Files.createTempFile(dir.toPath, prefix, suffix).toFile 22 | file.deleteOnExit() 23 | file 24 | } 25 | 26 | def createTempDir: java.io.File = { 27 | val rndString = scala.util.Random.alphanumeric.take(randomPrefixLength).mkString 28 | createTempDirForPrefix(rndString) 29 | } 30 | 31 | def tempDirGen: Gen[java.io.File] = Gen.listOfN(randomPrefixLength, Gen.alphaNumChar).map { p => 32 | val prefix = p.mkString("") 33 | createTempDirForPrefix(prefix) 34 | } 35 | 36 | /** 37 | * Recursively remove all the files and directories in `root` 38 | */ 39 | def remove(root: Path): Unit = { 40 | 41 | @SuppressWarnings(Array("org.wartremover.warts.Recursion")) 42 | def deleteRecursive(dir: java.io.File): Unit = { 43 | for (file <- dir.listFiles) { 44 | if (file.isDirectory){ 45 | deleteRecursive(file) 46 | } 47 | file.delete() 48 | } 49 | } 50 | 51 | deleteRecursive(root.toFile) 52 | } 53 | 54 | private def createTempDirForPrefix(prefix: String): java.io.File = { 55 | val file = java.nio.file.Files.createTempDirectory(basePath, prefix).toFile 56 | file.deleteOnExit() 57 | file 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /testkit/src/main/scala/scorex/testkit/utils/NoShrink.scala: -------------------------------------------------------------------------------- 1 | package scorex.testkit.utils 2 | 3 | import org.scalacheck.Shrink 4 | 5 | trait NoShrink { 6 | protected implicit def noShrink[A]: Shrink[A] = Shrink(_ => Stream.empty) 7 | } 8 | --------------------------------------------------------------------------------