diff --git a/FAQ.md b/FAQ.md index d69d1f0626..a041b2a8dc 100644 --- a/FAQ.md +++ b/FAQ.md @@ -44,12 +44,16 @@ The details of the Ergo emission schedule and monetary supply can be found in th * Website: https://ergoplatform.org/ -* Twitter: https://twitter.com/ergoplatformorg +* ErgoDocs: https://docs.ergoplatform.com -* Wiki: https://github.com/ergoplatform/ergo/wiki +* Twitter: https://twitter.com/Ergo_Platform + +* Telegram: https://t.me/ergoplatform +* Ecosystem: https://sigmaverse.io +* * Github: https://github.com/ergoplatform/ergo * Documents: https://ergoplatform.org/en/documents/ -* Telegram: https://t.me/ergoplatform +* Wiki: https://github.com/ergoplatform/ergo/wiki diff --git a/README.md b/README.md index d145829dc5..a296bdf8b2 100644 --- a/README.md +++ b/README.md @@ -1,68 +1,96 @@ # Ergo -This repository contains the reference implementation of the -Ergo Platform protocol, which is an alternative to -the [Bitcoin protocol](https://bitcoin.org/bitcoin.pdf). +Welcome to the official repository for the [Ergo Platform](https://ergoplatform.org/). This repository contains the reference client, also known as the node, for Ergo. Ergo is a cryptocurrency protocol that has been designed to offer a secure environment for peer-to-peer transactions. It supports programmable scarce money (Ergo) and a wide range of financial tools. -Ergo Platform website: [https://ergoplatform.org/](https://ergoplatform.org/) +The reference client is primarily written in Scala. While certain components of the protocol are implemented in other languages (for instance, [sigma-rust](https://github.com/ergoplatform/sigma-rust) is a Rust-based implementation of the ErgoScript cryptocurrency scripting language), the reference client provides the most complete and comprehensive implementation of the Ergo protocol. -## Differences from Bitcoin +## Key Features of Ergo -* Powerful contracts in the multi-stage extended UTXO model (see [ErgoScript whitepaper](https://ergoplatform.org/docs/ErgoScript.pdf)) -* Memory-hard Proof-of-Work function [Autolykos2](https://docs.ergoplatform.com/ErgoPow.pdf) -* Support for stateless clients (asymmetric, based on [https://eprint.iacr.org/2016/994](https://eprint.iacr.org/2016/994)), -[NiPoPoWs](https://eprint.iacr.org/2017/963.pdf), hybrid modes -* [Alternative transactional language](https://github.com/ScorexFoundation/sigmastate-interpreter), which is more powerful than Bitcoin Script but also safe against -heavy validation attacks -* Alternative fee model with [mandatory storage-rent component](https://fc18.ifca.ai/bitcoin/papers/bitcoin18-final18.pdf ) +Ergo, while sharing some commonalities with Bitcoin as a UTXO Proof-of-Work cryptocurrency, stands out due to its unique design and features. It has been built from the ground up, introducing several innovative elements: + +* **ErgoScript**: A powerful contract language in the multi-stage extended UTXO model. More details can be found in the [ErgoScript whitepaper](https://ergoplatform.org/docs/ErgoScript.pdf). +* **Autolykos2**: A memory-hard Proof-of-Work function, providing enhanced security. Learn more about it [here](https://docs.ergoplatform.com/ErgoPow.pdf). +* Support for Stateless Clients: Ergo supports asymmetric stateless clients, based on [this paper](https://eprint.iacr.org/2016/994), and includes features like NiPoPoWs and hybrid modes. +* **Advanced Transactional Language**: Ergo introduces an [alternative transactional language](https://github.com/ScorexFoundation/sigmastate-interpreter) that is more powerful than Bitcoin Script and is designed to be safe against heavy validation attacks. +* **Innovative Fee Model**: Ergo implements an alternative fee model with a [mandatory storage-rent component](https://fc18.ifca.ai/bitcoin/papers/bitcoin18-final18.pdf ) (also known as demurrage). ## Specifications -A [White Paper](https://ergoplatform.org/docs/whitepaper.pdf) with a brief description is available. A Yellow Paper with detailed specification is underway and will be available shortly. At the moment, there are [drafts of the Yellow Paper](https://github.com/ergoplatform/ergo/tree/master/papers/yellow) available, -and currently the reference implementation code should be considered as the specification. +* [white paper](https://ergoplatform.org/docs/whitepaper.pdf) - a brief description of the protocol +* [ErgoScript white paper](https://ergoplatform.org/docs/ErgoScript.pdf) - describes ErgoScript, a Cryptocurrency Scripting Language Supporting Noninteractive Zero-Knowledge Proofs used in Ergo + +More papers can be found at [docs.ergoplatform.com/documents](https://docs.ergoplatform.com/documents/). + +## Security Assumptions + +The Ergo client operates under certain assumptions about its environment: + +* The execution environment is trusted. Although the seed is stored in an encrypted file, and the client's wallet attempts to purge the secret key from memory as soon as it is no longer needed, the client does not have defenses against side-channel attacks, memory scans, etc. +* Clocks are expected to be synchronized to a reasonable degree. If a block's timestamp is more than 20 minutes into the future, the block will be temporarily rejected. The client does not utilize NTP or other time synchronization protocols. + +## Building and Running the Node and UI + +For instructions on how to build and run the node and UI, refer to the [official documentation](https://docs.ergoplatform.com/node/install/). + +By default, the node processes all blocks from the genesis block. However, there are other options available that may be more suitable for hardware with limited resources. + +* **Bootstrapping with a UTXO set snapshot:** This works similarly to Ethereum's snap-sync. The node first downloads a UTXO set snapshot from a secure point in the past, then downloads blocks following the UTXO set snapshot and applies them to the set. For more details and security proofs, refer to the ["Multi-mode cryptocurrency systems" paper](https://eprint.iacr.org/2018/129.pdf). To enable this feature add the following to your configuration file: +``` + +ergo { + ... + node.utxoBootstrap = true + ... +} +``` + +* The UTXO set snapshot bootstrapping can be further optimized by combining it with NiPoPoW (Non-Interactive Proofs of Proof-of-Work). This method allows for syncing the headers-chain in logarithmic time, as opposed to the linear time required by the standard SPV sync for headers. For more details, refer to the [NiPoPoW paper](https://eprint.iacr.org/2017/963.pdf). +``` + +ergo{ + ... + node.nipopow.nipopowBootstrap = true + node.utxo.utxoBootstrap = true + ... +} +``` + +* The stateless mode provides full-node security without the need to hold the entire UTXO set. This is achieved through the methods detailed in the ["Improving Authenticated Dynamic Dictionaries, with Applications to Cryptocurrencies" paper](https://eprint.iacr.org/2016/994.pdf). In this mode, it's possible to download and validate an arbitrary-sized suffix of the blockchain. Here's an example of how to configure this mode: -## Security assumptions +``` +ergo { + ... + node.stateType = "digest" + node.blocksToKeep = 2160 # store and process last three days only + node.nipopow.nipopowBootstrap = true # compatible with NiPoPoWs + ... +} +``` -This client relies on some assumptions in regards with its environment: -* execution environment is trusted. While seed is stored in encrypted files, and the client's - wallet tries to remove secret key from memory as soon as possible when it is not needed, the - client has no protection from side-channel attacks, memory scans etc. -* clocks should be more or less synchronized. If timestamp of a block is more than 20 minutes - in future, the block will be temporarily rejected. The client does not use NTP or other time - syncing protocols. +For more detailed information on different modes of node operation, please visit [docs.ergoplatform.com/node/modes](https://docs.ergoplatform.com/node/modes). -## Building and Running Node and UI +## Testing Procedures -See [documentation](https://docs.ergoplatform.com/node/install/) +Ergo utilizes three types of tests: -## Testing +1) Unit and property tests: These can be run using the `sbt test` command. +2) Integration tests: These tests require Docker to be installed. Run them with the `sudo sbt it:test` command. +3) Bootstrapping tests: These tests are time-consuming as they verify that the node is syncing with the main network in various regimes. Docker is also required for these tests. Run them with the `sudo sbt it2:test` command. -There are three kinds of tests: +## Setting up the Project in an IDE -1) Unit and property tests, run them with `sbt test` command. -2) Integration tests, they require for Docker to be installed, then run `sudo sbt it:test`. -3) Bootstrapping tests, very slow as they are checking that the node is indeed catching up with the main network in -different regimes, they require for Docker too, run as `sudo sbt it2:test`. +You can use either [IntelliJ IDEA](https://www.jetbrains.com/idea/) (Community or Ultimate edition) or [VSCode](https://code.visualstudio.com/) with the [Metals](https://scalameta.org/metals/) extension. -## Open project in IDE +Ensure that the project can be built with sbt before opening it in an IDE. You may need to resolve any dependency errors first. -Your can use [IntelliJ IDEA](https://www.jetbrains.com/idea/) (Community or Ultimate edition) or -[VSCode](https://code.visualstudio.com/) + [Metals](https://scalameta.org/metals/). -Before opening the project in IDE make sure it can be built with sbt. -You may need to fix dependency resolution errors first. +To open the project in IntelliJ IDEA, select File / Open and navigate to the project folder. This will initiate the Project Import Wizard, which uses the SBT configuration (build.sbt file) to generate the project configuration files for IDEA. You can view the project configuration in the `File / Project Structure...` dialog. If the import is successful, you should be able to compile the project in the IDE. -After that you can open the project folder in Idea (File / Open) -which will run Project Import Wizard. The wizard will use SBT configuration -(build.sbt file) to generate Idea's project configuration files. -You can open `File / Project Structure...` dialog to see project configuration. -If everything is successful you can compile the project in IDE. +## Contributing to Ergo -## Contributions +Ergo is an open-source project and we welcome contributions from developers and testers! Join the discussion on [Ergo Discord](https://discord.gg/kj7s7nb) in the #development channel and check out our [Contributing documentation](https://docs.ergoplatform.com/contribute/). -Ergo is open-source and open movement, always in need for testers and developers! Please feel free -to discuss development in [Ergo Discord](https://discord.gg/kj7s7nb), #development channel. +## Frequently Asked Questions -## FAQ -[Frequently Asked Questions](FAQ.md) +For common queries, please refer to our [Frequently Asked Questions](FAQ.md) page. diff --git a/avldb/src/main/scala/scorex/crypto/authds/avltree/batch/VersionedLDBAVLStorage.scala b/avldb/src/main/scala/scorex/crypto/authds/avltree/batch/VersionedLDBAVLStorage.scala index b7e5eb3512..2d541569e7 100644 --- a/avldb/src/main/scala/scorex/crypto/authds/avltree/batch/VersionedLDBAVLStorage.scala +++ b/avldb/src/main/scala/scorex/crypto/authds/avltree/batch/VersionedLDBAVLStorage.scala @@ -5,7 +5,7 @@ import scorex.crypto.authds.avltree.batch.Constants.{DigestType, HashFnType, has import scorex.crypto.authds.avltree.batch.VersionedLDBAVLStorage.{topNodeHashKey, topNodeHeightKey} import scorex.crypto.authds.avltree.batch.serialization.{BatchAVLProverManifest, BatchAVLProverSubtree, ProxyInternalNode} import scorex.crypto.authds.{ADDigest, ADKey} -import scorex.util.encode.Base58 +import scorex.util.encode.Base16 import scorex.crypto.hash import scorex.crypto.hash.Digest32 import scorex.db.{LDBKVStore, LDBVersionedStore} @@ -42,11 +42,12 @@ class VersionedLDBAVLStorage(store: LDBVersionedStore) override def rollback(version: ADDigest): Try[(ProverNodes[DigestType], Int)] = Try { if (!this.version.contains(version)) { // do not rollback to self + log.info(s"Doing rollback from ${this.version.map(Base16.encode)} to ${Base16.encode(version)}:") store.rollbackTo(version) } }.flatMap(_ => restorePrunedRootNode()) .recoverWith { case e => - log.warn(s"Failed to recover tree for digest ${Base58.encode(version)}:", e) + log.warn(s"Failed to recover tree for digest ${Base16.encode(version)}:", e) Failure(e) } diff --git a/build.sbt b/build.sbt index e880b0fa32..b6116b4644 100644 --- a/build.sbt +++ b/build.sbt @@ -37,7 +37,7 @@ val circeVersion = "0.13.0" val akkaVersion = "2.6.10" val akkaHttpVersion = "10.2.4" -val sigmaStateVersion = "5.0.8" +val sigmaStateVersion = "5.0.10" // for testing current sigmastate build (see sigmastate-ergo-it jenkins job) val effectiveSigmaStateVersion = Option(System.getenv().get("SIGMASTATE_VERSION")).getOrElse(sigmaStateVersion) diff --git a/ergo-wallet/src/main/scala/org/ergoplatform/wallet/interpreter/ErgoInterpreter.scala b/ergo-wallet/src/main/scala/org/ergoplatform/wallet/interpreter/ErgoInterpreter.scala index fbed24ecfb..e429ecbdad 100644 --- a/ergo-wallet/src/main/scala/org/ergoplatform/wallet/interpreter/ErgoInterpreter.scala +++ b/ergo-wallet/src/main/scala/org/ergoplatform/wallet/interpreter/ErgoInterpreter.scala @@ -4,11 +4,11 @@ import org.ergoplatform.ErgoLikeContext.Height import org.ergoplatform.sdk.wallet.protocol.context.ErgoLikeParameters import org.ergoplatform.wallet.protocol.Constants import org.ergoplatform.{ErgoBox, ErgoBoxCandidate, ErgoLikeContext, ErgoLikeInterpreter} -import scorex.crypto.authds.ADDigest import scorex.util.ScorexLogging import sigmastate.Values.ErgoTree import sigmastate.interpreter.Interpreter.{ScriptEnv, VerificationResult} import sigmastate.{AvlTreeData, AvlTreeFlags} +import special.collection.Coll import scala.util.Try @@ -100,7 +100,7 @@ object ErgoInterpreter { new ErgoInterpreter(params) /** Create [[AvlTreeData]] with the given digest and all operations enabled. */ - def avlTreeFromDigest(digest: ADDigest): AvlTreeData = { + def avlTreeFromDigest(digest: Coll[Byte]): AvlTreeData = { val flags = AvlTreeFlags(insertAllowed = true, updateAllowed = true, removeAllowed = true) AvlTreeData(digest, flags, Constants.HashLength) } diff --git a/ergo-wallet/src/main/scala/org/ergoplatform/wallet/serialization/JsonCodecsWrapper.scala b/ergo-wallet/src/main/scala/org/ergoplatform/wallet/serialization/JsonCodecsWrapper.scala index 4c6eda7818..fe9ef84fe2 100644 --- a/ergo-wallet/src/main/scala/org/ergoplatform/wallet/serialization/JsonCodecsWrapper.scala +++ b/ergo-wallet/src/main/scala/org/ergoplatform/wallet/serialization/JsonCodecsWrapper.scala @@ -1,6 +1,6 @@ package org.ergoplatform.wallet.serialization -import org.ergoplatform.JsonCodecs +import org.ergoplatform.sdk.JsonCodecs /** * JSON Codecs provided as singleton package, not trait. diff --git a/ergo-wallet/src/test/scala/org/ergoplatform/wallet/interpreter/InterpreterSpecCommon.scala b/ergo-wallet/src/test/scala/org/ergoplatform/wallet/interpreter/InterpreterSpecCommon.scala index d4af16e52d..484c7fb47a 100644 --- a/ergo-wallet/src/test/scala/org/ergoplatform/wallet/interpreter/InterpreterSpecCommon.scala +++ b/ergo-wallet/src/test/scala/org/ergoplatform/wallet/interpreter/InterpreterSpecCommon.scala @@ -1,9 +1,9 @@ package org.ergoplatform.wallet.interpreter import org.ergoplatform.sdk.wallet.protocol.context.{ErgoLikeParameters, ErgoLikeStateContext} -import scorex.crypto.authds.ADDigest import scorex.util.encode.Base16 import sigmastate.basics.CryptoConstants +import sigmastate.eval.Extensions.ArrayOps import sigmastate.eval.{CGroupElement, CPreHeader, Colls} import special.collection.Coll import special.sigma.{Header, PreHeader} @@ -39,9 +39,10 @@ trait InterpreterSpecCommon { override def sigmaLastHeaders: Coll[Header] = Colls.emptyColl - override def previousStateDigest: ADDigest = - ADDigest @@ Base16.decode("a5df145d41ab15a01e0cd3ffbab046f0d029e5412293072ad0f5827428589b9302") + override def previousStateDigest: Coll[Byte] = + Base16.decode("a5df145d41ab15a01e0cd3ffbab046f0d029e5412293072ad0f5827428589b9302") .getOrElse(throw new Error(s"Failed to parse genesisStateDigest")) + .toColl override def sigmaPreHeader: PreHeader = CPreHeader( version = 0, diff --git a/src/main/resources/.well-known/ai-plugin.json b/src/main/resources/.well-known/ai-plugin.json new file mode 100644 index 0000000000..5100ce83dd --- /dev/null +++ b/src/main/resources/.well-known/ai-plugin.json @@ -0,0 +1,18 @@ +{ + "schema_version": "v1", + "name_for_human": "Ergo Node Plugin (no auth)", + "name_for_model": "ergonode", + "description_for_human": "Plugin for interacting with Ergo node.", + "description_for_model": "Specification of Ergo Node API for ChatGPT plugin.\n The following endpoints supported \n - /blocks/chainSlice - Get headers in a specified range of heights\n - /info - Get the basic information about the status of Ergo Node. \n - /transactions/unconfirmed/byTransactionId - Get unconfirmed transaction from the mempool\n - /transactions/poolHistogram - Get histogram (waittime, (n_trans, sum(fee)) for transactions in mempool.\n - /peers/connected - Get a list of current connected peers\n - /peers/blacklisted - Get a list of blacklisted peers\n - /utils/address - Check address validity\n - /blockchain/indexedHeight - Get current indexed block height. (The indexer has processed all blocks up to this height.)\n - /blockchain/transaction/byId - Retrieve a transaction by its id\n - /blockchain/transaction/byAddress - Retrieve a list of transactions by their associated address\n - /blockchain/box/byId - Retrieve a box by its id\n - /blockchain/box/byAddress - Retrieve boxes by their associated \n - /blockchain/box/unspent/byAddress - Retrieve unspent boxes by their associated address\n - /blockchain/token/byId - Retrieve minting information about a token\n - /blockchain/balanceForAddress - Retrieve balance information of an Ergo address.", + "auth": { + "type": "none" + }, + "api": { + "type": "openapi", + "url": "http://localhost:9053/openapi.yaml", + "is_user_authenticated": false + }, + "logo_url": "https://cryptologos.cc/logos/ergo-erg-logo.png", + "contact_email": "team@ergoplatform.org", + "legal_info_url": "https://ergoplatform.org/en/legal/" +} diff --git a/src/main/resources/api/openapi-ai.yaml b/src/main/resources/api/openapi-ai.yaml new file mode 100644 index 0000000000..392309a6d1 --- /dev/null +++ b/src/main/resources/api/openapi-ai.yaml @@ -0,0 +1,1482 @@ +openapi: "3.0.2" + +info: + version: "5.0.14" + title: Ergo Node API + description: Specification of Ergo Node API for ChatGPT plugin. + The following endpoints supported + - /blocks/chainSlice - Get headers in a specified range of heights + - /info - Get the basic information about the status of Ergo Node. + - /transactions/unconfirmed/byTransactionId - Get unconfirmed transaction from the mempool + - /transactions/poolHistogram - Get histogram (waittime, (n_trans, sum(fee)) for transactions in mempool. + - /blockchain/indexedHeight - Get current indexed block height. (The indexer has processed all blocks up to this height.) + - /blockchain/transaction/byId - Retrieve a transaction by its id + - /blockchain/transaction/byAddress - Retrieve a list of transactions by their associated address + - /blockchain/box/byId - Retrieve a box by its id + - /blockchain/box/byAddress - Retrieve boxes by their associated + - /blockchain/box/unspent/byAddress - Retrieve unspent boxes by their associated address + - /blockchain/token/byId - Retrieve minting information about a token + - /blockchain/balanceForAddress - Retrieve balance information of an Ergo address. + +servers: + - url: http://localhost:9052 + description: Ergo full node API (testnet). + - url: http://localhost:9053 + description: Ergo full node API (mainnet). + +components: + schemas: + # Objects + ErgoTransactionInput: + type: object + required: + - boxId + - spendingProof + properties: + boxId: + $ref: '#/components/schemas/TransactionBoxId' + spendingProof: + $ref: '#/components/schemas/SpendingProof' + + ErgoTransactionDataInput: + type: object + required: + - boxId + properties: + boxId: + $ref: '#/components/schemas/TransactionBoxId' + + SpendingProof: + description: Spending proof for transaction input + type: object + required: + - proofBytes + - extension + properties: + proofBytes: + $ref: '#/components/schemas/SpendingProofBytes' + extension: + type: object + description: Variables to be put into context + additionalProperties: + $ref: '#/components/schemas/SValue' + example: + '1': 'a2aed72ff1b139f35d1ad2938cb44c9848a34d4dcfd6d8ab717ebde40a7304f2541cf628ffc8b5c496e6161eba3f169c6dd440704b1719e0' + + ErgoTransactionOutput: + type: object + required: + - value + - ergoTree + - additionalRegisters + - creationHeight + properties: + boxId: + $ref: '#/components/schemas/TransactionBoxId' + value: + description: Amount of Ergo token + type: integer + format: int64 + minimum: 0 + example: 147 + ergoTree: + $ref: '#/components/schemas/ErgoTree' + creationHeight: + description: Height the output was created at + type: integer + format: int32 + example: 9149 + assets: + description: Assets list in the transaction + type: array + items: + $ref: '#/components/schemas/Asset' + additionalRegisters: + $ref: '#/components/schemas/Registers' + transactionId: + $ref: '#/components/schemas/TransactionId' + index: + description: Index in the transaction outputs + type: integer + format: int32 + + BalanceInfo: + type: object + description: Represents a balance information (e.g. for an address) + required: + - nanoErgs + - tokens + properties: + nanoErgs: + type: integer + format: int64 + description: Balance of nanoERGs + tokens: + type: array + description: List of assets (aks tokens) with balances + items: + type: object + properties: + tokenId: + $ref: '#/components/schemas/ModifierId' + description: Identifier of the asset (aka token) + amount: + type: integer + format: int64 + description: Amount of the asset (aka token) + decimals: + type: integer + description: Number of decimals of the token + name: + type: string + description: Name of the token, if any + + IndexedErgoBox: + type: object + description: Box indexed with extra information + required: + - box + - confirmationsNum + - address + - creationTransaction + - spendingTransaction + - spendingHeight + - inclusionHeight + - spent + - globalIndex + properties: + box: + $ref: '#/components/schemas/ErgoTransactionOutput' + confirmationsNum: + description: Number of confirmations, if the box is included into the blockchain + type: integer + format: int32 + minimum: 0 + example: 147 + nullable: true + address: + $ref: '#/components/schemas/ErgoAddress' + creationTransaction: + description: Transaction which created the box + $ref: '#/components/schemas/ModifierId' + spendingTransaction: + description: Transaction which created the box + nullable: true + $ref: '#/components/schemas/ModifierId' + spendingHeight: + description: The height the box was spent at + type: integer + format: int32 + minimum: 0 + example: 147 + nullable: true + inclusionHeight: + description: The height the transaction containing the box was included in a block at + type: integer + format: int32 + minimum: 0 + example: 147 + spent: + description: A flag signalling whether the box was spent + type: boolean + example: false + globalIndex: + description: Global index of the output in the blockchain + type: integer + format: int64 + minimum: 0 + example: 83927 + + IndexedToken: + type: object + description: Token indexed with extra information + required: + - id + - boxId + - emissionAmount + - name + - description + - decimals + properties: + id: + description: Id of the token + $ref: '#/components/schemas/ModifierId' + boxId: + description: Id of the box that created the token + $ref: '#/components/schemas/ModifierId' + emissionAmount: + description: The total supply of the token + type: integer + format: int64 + minimum: 1 + example: 3500000 + name: + description: The name of the token + type: string + description: + description: The description of the token + type: string + decimals: + description: The number of decimals the token supports + type: integer + format: int32 + minimum: 0 + example: 8 + + ErgoTransaction: + type: object + description: ErgoTransaction is an atomic operation which changes UTXO state. + required: + - inputs + - dataInputs + - outputs + properties: + id: + description: Id of the transaction + $ref: '#/components/schemas/TransactionId' + inputs: + description: Inputs, that will be spent by this transaction + type: array + items: + $ref: '#/components/schemas/ErgoTransactionInput' + dataInputs: + description: Read-only inputs, that are not going to be spent by transaction. + type: array + items: + $ref: '#/components/schemas/ErgoTransactionDataInput' + outputs: + description: Outputs of the transaction, i.e. box candidates to be created by this transaction. + type: array + items: + $ref: '#/components/schemas/ErgoTransactionOutput' + size: + description: Size of ErgoTransaction in bytes + type: integer + format: int32 + + IndexedErgoTransaction: + type: object + description: Transaction indexed with extra information + required: + - id + - inputs + - dataInputs + - outputs + - inclusionHeight + - numConfirmations + - blockId + - timestamp + - index + - globalIndex + - size + properties: + id: + $ref: '#/components/schemas/TransactionId' + inputs: + description: Transaction inputs + type: array + items: + $ref: '#/components/schemas/ErgoTransactionInput' + dataInputs: + description: Transaction data inputs + type: array + items: + $ref: '#/components/schemas/ErgoTransactionDataInput' + outputs: + description: Transaction outputs + type: array + items: + $ref: '#/components/schemas/ErgoTransactionOutput' + inclusionHeight: + description: Height of a block the transaction was included in + type: integer + format: int32 + example: 20998 + numConfirmations: + description: Number of transaction confirmations + type: integer + format: int32 + example: 20998 + blockId: + description: Id of the block the transaction was included in + allOf: + - $ref: '#/components/schemas/ModifierId' + timestamp: + $ref: '#/components/schemas/Timestamp' + index: + description: index of the transaction in the block it was included in + type: integer + format: int32 + example: 3 + globalIndex: + description: Global index of the transaction in the blockchain + type: integer + format: int64 + example: 3565445 + size: + description: Size in bytes + type: integer + format: int32 + + ErgoAddress: + description: Encoded Ergo Address + type: string + example: '3WwbzW6u8hKWBcL1W7kNVMr25s2UHfSBnYtwSHvrRQt7DdPuoXrt' + + FullBlock: + description: Block with header and transactions + type: object + required: + - header + - blockTransactions + - adProofs + - extension + - size + properties: + header: + $ref: '#/components/schemas/BlockHeader' + blockTransactions: + $ref: '#/components/schemas/BlockTransactions' + adProofs: + $ref: '#/components/schemas/BlockADProofs' + extension: + $ref: '#/components/schemas/Extension' + size: + description: Size in bytes + type: integer + format: int32 + + PowSolutions: + description: An object containing all components of pow solution + type: object + required: + - pk + - w + - n + - d + properties: + pk: + type: string + description: Base16-encoded public key + example: '0350e25cee8562697d55275c96bb01b34228f9bd68fd9933f2a25ff195526864f5' + w: + type: string + example: '0366ea253123dfdb8d6d9ca2cb9ea98629e8f34015b1e4ba942b1d88badfcc6a12' + n: + type: string + example: '0000000000000000' + d: + type: number + example: 987654321 + + BlockHeader: + description: Header of a block. + It authenticates link to a previous block, other block sections + (transactions, UTXO set transformation proofs, extension), UTXO set, votes for blockchain parameters + to be changed and proof-of-work related data. + type: object + required: + - id + - timestamp + - version + - adProofsRoot + - stateRoot + - transactionsRoot + - nBits + - extensionHash + - powSolutions + - height + - difficulty + - parentId + - votes + properties: + id: + description: Block id + $ref: '#/components/schemas/ModifierId' + timestamp: + description: Block generation time reported by a miner + $ref: '#/components/schemas/Timestamp' + version: + description: Protocol version used to generate the block + $ref: '#/components/schemas/Version' + adProofsRoot: + description: Digest of UTXO set transformation proofs + $ref: '#/components/schemas/Digest32' + stateRoot: + description: AVL+ tree digest of UTXO set (after the block is applied) + $ref: '#/components/schemas/ADDigest' + transactionsRoot: + description: Merkle tree digest of transactions in the block (BlockTransactions section) + $ref: '#/components/schemas/Digest32' + nBits: + description: Proof-of-work target (difficulty encoded) + type: integer + format: int64 + minimum: 0 + example: 19857408 + extensionHash: + description: Merkle tree digest of the extension section of the block + $ref: '#/components/schemas/Digest32' + powSolutions: + description: Solution for the proof-of-work puzzle + $ref: '#/components/schemas/PowSolutions' + height: + description: Height of the block (genesis block height == 1) + type: integer + format: int32 + minimum: 0 + example: 667 + difficulty: + type: string + example: '9575989248' + parentId: + $ref: '#/components/schemas/ModifierId' + votes: + description: Votes for changing system parameters + $ref: '#/components/schemas/Votes' + size: + description: Size of the header in bytes + type: integer + format: int32 + extensionId: + description: Hash of the extension section of the block == hash(modifier type id, header id, extensionHash) + $ref: '#/components/schemas/ModifierId' + transactionsId: + description: Hash of the transactions section of the block == hash(modifier type id, header id, transactionsRoot) + $ref: '#/components/schemas/ModifierId' + adProofsId: + description: Hash of the UTXO set transformation proofs section of the block == hash(modifier type id, header id, adProofsRoot) + $ref: '#/components/schemas/ModifierId' + + BlockTransactions: + description: Section of a block which contains transactions. + type: object + required: + - headerId + - transactions + - size + properties: + headerId: + description: Identifier of a header of a corresponding block + $ref: '#/components/schemas/ModifierId' + transactions: + description: Transactions of the block + $ref: '#/components/schemas/Transactions' + size: + description: Size in bytes of all block transactions + type: integer + format: int32 + + BlockADProofs: + type: object + required: + - headerId + - proofBytes + - digest + - size + properties: + headerId: + description: Identifier of a header of the block which contains the proofs + $ref: '#/components/schemas/ModifierId' + proofBytes: + description: Serialized bytes of the authenticated dictionary proof + $ref: '#/components/schemas/SerializedAdProof' + digest: + description: Hash of the proofBytes + $ref: '#/components/schemas/Digest32' + size: + description: Size in bytes + type: integer + format: int32 + + Extension: + description: Section of a block which contains extension data. + type: object + required: + - headerId + - digest + - fields + properties: + headerId: + description: Identifier of a header of a corresponding block + $ref: '#/components/schemas/ModifierId' + digest: + description: Root hash (aka digest) merkelized list of key-value records + $ref: '#/components/schemas/Digest32' + fields: + description: List of key-value records + type: array + nullable: true + items: + $ref: '#/components/schemas/KeyValueItem' + + KeyValueItem: + description: Key-value record represented as a pair of hex strings in an array. + type: array + items: + $ref: '#/components/schemas/HexString' + + Peer: + type: object + required: + - address + properties: + address: + type: string + example: '127.0.0.1:5673' + restApiUrl: + type: string + example: 'https://example.com' + nullable: true + name: + type: string + example: mynode + nullable: true + lastSeen: + $ref: '#/components/schemas/Timestamp' + connectionType: + type: string + nullable: true + enum: + - Incoming + - Outgoing + + BlacklistedPeers: + type: object + required: + - addresses + properties: + addresses: + type: array + items: + type: string + description: Blacklisted node address + + NodeInfo: + description: Data container for /info API request output. + Contains information about node's state and configuration. + Contains data about best block, best header, state, etc. + Best block is the block with the maximum height. + type: object + required: + - name + - appVersion + - fullHeight + - headersHeight + - maxPeerHeight + - bestFullHeaderId + - previousFullHeaderId + - bestHeaderId + - headersScore + - fullBlocksScore + - stateRoot + - stateType + - stateVersion + - isMining + - peersCount + - unconfirmedCount + - difficulty + - currentTime + - launchTime + - genesisBlockId + - parameters + properties: + name: + description: Node's (peer) self-chosen name from config + type: string + example: my-node-1 + appVersion: + description: Node's application version + type: string + example: 0.0.1 + fullHeight: + type: integer + format: int32 + description: Height of the best block known to the node. + Can be 'null' if state is empty (no full block is applied since node launch) + minimum: 0 + example: 667 + nullable: true + headersHeight: + type: integer + format: int32 + description: The height of the best header (i.e. the one with the maximum height). + Can be 'null' if state is empty (no header applied since node launch) + minimum: 0 + example: 667 + nullable: true + maxPeerHeight: + type: integer + format: int32 + description: Maximum block height of connected peers. + Can be 'null' if state is empty (no peer connected since node launch) + minimum: 0 + example: 706162 + nullable: true + bestFullHeaderId: + type: string + description: Best full-block id (header id of such block). + Can be 'null' if no full block is applied since node launch. + nullable: true + allOf: + - $ref: '#/components/schemas/ModifierId' + previousFullHeaderId: + type: string + description: Header id of the parent block of the best full-block (i.e. previous block in the blockchain). + Can be 'null' if no full block is applied since node launch + nullable: true + allOf: + - $ref: '#/components/schemas/ModifierId' + bestHeaderId: + type: string + description: Best header ID (hex representation). + Can be 'null' if no header applied since node launch. + nullable: true + allOf: + - $ref: '#/components/schemas/ModifierId' + stateRoot: + type: string + nullable: true + description: Current UTXO set digest. + Can be 'null' if state is empty (no full block is applied since node launch) + example: 'dab9da11fc216660e974842cc3b7705e62ebb9e0bf5ff78e53f9cd40abadd117' + stateType: + description: Whether the node is storing UTXO set or only its digest. + Equals `digest` if only digest is stored, `utxo` if full UTXO set is stored. + type: string + enum: + - digest + - utxo + stateVersion: + description: Id of a block where UTXO set digest is taken from. + Can be 'null' if no full block is applied since node launch. + type: string + example: 'fab9da11fc216660e974842cc3b7705e62ebb9e0bf5ff78e53f9cd40abadd117' + nullable: true + isMining: + description: Whether the node is mining (i.e. generating blocks). + type: boolean + example: true + peersCount: + type: integer + description: Number of peers this node is connected with. + format: int32 + minimum: 0 + example: 327 + unconfirmedCount: + description: Number of unconfirmed transactions in the mempool. + type: integer + format: int32 + minimum: 0 + maximum: 10000 + example: 327 + difficulty: + type: integer + minimum: 0 + nullable: true + example: 667 + description: Difficulty on current bestFullHeaderId. + Can be 'null' if no full block is applied since node launch. + Difficulty is a BigInt integer. + currentTime: + type: integer + description: Current internal node time + allOf: + - $ref: '#/components/schemas/Timestamp' + launchTime: + type: integer + description: When the node was launched (in Java time format, UNIX time * 1000). + allOf: + - $ref: '#/components/schemas/Timestamp' + headersScore: + type: integer + description: Cumulative difficulty of best headers-chain. + Can be 'null' if no headers is applied since node launch. headersScore is a BigInt integer. + nullable: true + fullBlocksScore: + type: integer + description: Cumulative difficulty of best full blocks chain. + Can be 'null' if no full block is applied since node launch. fullBlocksScore is a BigInt integer. + nullable: true + genesisBlockId: + type: string + description: Header id of genesis block. Can be 'null' if genesis blocks is not produced yet. + nullable: true + allOf: + - $ref: '#/components/schemas/ModifierId' + parameters: + type: object + description: System parameters which could be readjusted via collective miners decision. + $ref: '#/components/schemas/Parameters' + eip27Supported: + type: boolean + example: true + description: Whether EIP-27 locked in + restApiUrl: + type: string + example: 'https://example.com' + description: Publicly accessible url of node which exposes restApi in firewall + + Parameters: + description: System parameters which could be readjusted via collective miners decision. + type: object + required: + - height + - blockVersion + - storageFeeFactor + - minValuePerByte + - maxBlockSize + - maxBlockCost + - tokenAccessCost + - inputCost + - dataInputCost + - outputCost + properties: + height: + type: integer + format: int32 + description: Height when current parameters were considered(not actual height). Can be '0' if state is empty + minimum: 0 + example: 667 + nullable: false + storageFeeFactor: + type: integer + format: int32 + description: Storage fee coefficient (per byte per storage period ~4 years) + minimum: 0 + example: 100000 + nullable: false + minValuePerByte: + type: integer + format: int32 + description: Minimum value per byte of an output + minimum: 0 + example: 360 + nullable: false + maxBlockSize: + type: integer + format: int32 + description: Maximum block size (in bytes) + minimum: 0 + example: 1048576 + nullable: false + maxBlockCost: + type: integer + format: int32 + description: Maximum cumulative computational cost of input scripts in block transactions + minimum: 0 + example: 104876 + nullable: false + blockVersion: + $ref: '#/components/schemas/Version' + nullable: false + tokenAccessCost: + type: integer + format: int32 + description: Validation cost of a single token + minimum: 0 + example: 100 + nullable: false + inputCost: + type: integer + format: int32 + description: Validation cost per one transaction input + minimum: 0 + example: 100 + nullable: false + dataInputCost: + type: integer + format: int32 + description: Validation cost per one data input + minimum: 0 + example: 100 + nullable: false + outputCost: + type: integer + format: int32 + description: Validation cost per one transaction output + minimum: 0 + example: 100 + nullable: false + + Version: + description: Ergo blockchain protocol version + type: integer + format: int8 + example: 2 + + TransactionBoxId: + description: Base16-encoded transaction box id bytes. Should be 32 bytes long + type: string + format: base16 + example: '1ab9da11fc216660e974842cc3b7705e62ebb9e0bf5ff78e53f9cd40abadd117' + + TransactionId: + description: Base16-encoded transaction id bytes + type: string + format: base16 + example: '2ab9da11fc216660e974842cc3b7705e62ebb9e0bf5ff78e53f9cd40abadd117' + + ErgoTree: + description: Base16-encoded ergo tree bytes + type: string + format: base16 + example: '0008cd0336100ef59ced80ba5f89c4178ebd57b6c1dd0f3d135ee1db9f62fc634d637041' + + Transactions: + description: List of ErgoTransaction objects + type: array + items: + $ref: '#/components/schemas/ErgoTransaction' + + FeeHistogramBin: + description: Fee histogram bin + type: object + properties: + nTxns: + type: integer + format: int32 + totalFee: + type: integer + format: int64 + + FeeHistogram: + description: Fee histogram for transactions in mempool + type: array + items: + $ref: '#/components/schemas/FeeHistogramBin' + + Asset: + description: Token detail in the transaction + type: object + required: + - tokenId + - amount + properties: + tokenId: + $ref: '#/components/schemas/Digest32' + amount: + description: Amount of the token + type: integer + format: int64 + example: 1000 + + Registers: + description: Ergo box registers + type: object + additionalProperties: + $ref: '#/components/schemas/SValue' + example: + R4: '100204a00b08cd0336100ef59ced80ba5f89c4178ebd57b6c1dd0f3d135ee1db9f62fc634d637041ea02d192a39a8cc7a70173007301' + + SValue: + description: Base-16 encoded serialized Sigma-state value + type: string + format: base16 + example: '100204a00b08cd0336100ef59ced80ba5f89c4178ebd57b6c1dd0f3d135ee1db9f62fc634d637041ea02d192a39a8cc7a70173007301' + + ModifierId: + description: Base16-encoded 32 byte modifier id + type: string + format: base16 + example: '3ab9da11fc216660e974842cc3b7705e62ebb9e0bf5ff78e53f9cd40abadd117' + + Digest32: + description: Base16-encoded 32 byte digest + type: string + format: base16 + example: '4ab9da11fc216660e974842cc3b7705e62ebb9e0bf5ff78e53f9cd40abadd117' + + HexString: + description: Base16-encoded bytes + type: string + format: base16 + example: '4ab9da11fc216660e974842cc3b7705e62ebb9e0bf5ff78e53f9cd40abadd117' + + ADDigest: + description: Base16-encoded 33 byte digest - digest with extra byte with tree height + type: string + format: base16 + example: '333ab9da11fc216660e974842cc3b7705e62ebb9e0bf5ff78e53f9cd40abadd117' + + SerializedAdProof: + description: Base16-encoded ad proofs + type: string + format: base16 + example: '3ab9da11fc216660e974842cc3b7705e62ebb9e0bf5ff78e53f9cd40abadd1173ab9da11fc216660e974842cc3b7705e62ebb9e0bf5ff78e53f9cd40abadd1173ab9da11fc216660e974842cc3b7705e62ebb9e0bf5ff78e53f9cd40abadd117' + + SpendingProofBytes: + description: Base16-encoded spending proofs + type: string + format: base16 + example: '4ab9da11fc216660e974842cc3b7705e62ebb9e0bf5ff78e53f9cd40abadd1173ab9da11fc216660e974842cc3b7705e62ebb9e0bf5ff78e53f9cd40abadd1173ab9da11fc216660e974842cc3b7705e62ebb9e0bf5ff78e53f9cd40abadd117' + + Timestamp: + description: Basic timestamp definition + type: integer + format: int64 + example: 1524143059077 + + AddressValidity: + description: Validity status of Ergo address + type: object + required: + - address + - isValid + properties: + address: + $ref: '#/components/schemas/ErgoAddress' + isValid: + type: boolean + error: + type: string + + ApiError: + description: Error response from API + type: object + required: + - error + - reason + - detail + properties: + error: + type: integer + description: Error code + example: 500 + reason: + type: string + description: Error message explaining the reason of the error + example: 'Internal server error' + detail: + type: string + nullable: true + description: Detailed error description + +paths: + /blocks/chainSlice: + get: + description: Get headers in a specified range of heights + operationId: getChainSlice + tags: + - blocks + parameters: + - in: query + name: fromHeight + required: false + description: Min header height (start of the range) + schema: + type: integer + format: int32 + default: 0 + - in: query + name: toHeight + required: false + description: Max header height of the range (last header height then omitted) + schema: + type: integer + format: int32 + default: -1 + responses: + '200': + description: Array of headers + content: + application/json: + schema: + type: array + description: Array of headers + items: + $ref: '#/components/schemas/BlockHeader' + default: + description: Error + content: + application/json: + schema: + $ref: '#/components/schemas/ApiError' + + /info: + get: + description: Get the basic information about the status of Ergo Node. + operationId: getNodeInfo + tags: + - info + responses: + '200': + description: Node info object + content: + application/json: + schema: + $ref: '#/components/schemas/NodeInfo' + default: + description: Error + content: + application/json: + schema: + $ref: '#/components/schemas/ApiError' + + /transactions/unconfirmed/byTransactionId/{txId}: + parameters: + - in: path + name: txId + required: true + description: ID of a transaction to return + schema: + type: string + get: + description: Get unconfirmed transaction from the mempool + operationId: getUnconfirmedTransactionById + tags: + - transactions + responses: + '200': + description: Ergo transaction + content: + application/json: + schema: + $ref: '#/components/schemas/ErgoTransaction' + default: + description: Error + content: + application/json: + schema: + $ref: '#/components/schemas/ApiError' + + /transactions/poolHistogram: + parameters: + - in: query + name: bins + required: false + description: The number of bins in histogram + schema: + type: integer + format: int32 + minimum: 1 + default: 10 + - in: query + name: maxtime + required: false + description: Maximal wait time in milliseconds + schema: + type: integer + format: int64 + minimum: 0 + default: 60000 + get: + description: Get histogram (waittime, (n_trans, sum(fee)) for transactions in mempool. + It contains "bins"+1 bins, where i-th elements corresponds to transaction with wait time [i*maxtime/bins, (i+1)*maxtime/bins), + and last bin corresponds to the transactions with wait time >= maxtime. + operationId: getFeeHistogram + tags: + - transactions + responses: + '200': + description: Array with fee histogram + content: + application/json: + schema: + $ref: '#/components/schemas/FeeHistogram' + default: + description: Error + content: + application/json: + schema: + $ref: '#/components/schemas/ApiError' + + /peers/connected: + get: + description: Get a list of current connected peers + operationId: getConnectedPeers + tags: + - peers + responses: + '200': + description: Array of peer objects + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Peer' + default: + description: Error + content: + application/json: + schema: + $ref: '#/components/schemas/ApiError' + + /peers/blacklisted: + get: + description: Get a list of blacklisted peers + operationId: getBlacklistedPeers + tags: + - peers + responses: + '200': + description: Array of the addresses + content: + application/json: + schema: + $ref: '#/components/schemas/BlacklistedPeers' + + default: + description: Error + content: + application/json: + schema: + $ref: '#/components/schemas/ApiError' + + /utils/address/{address}: + get: + description: Check address validity + operationId: CheckAddressValidityWithGet + tags: + - utils + parameters: + - in: path + name: address + required: true + description: address to check + schema: + $ref: '#/components/schemas/ErgoAddress' + responses: + '200': + description: Address validity with validation error + content: + application/json: + schema: + $ref: '#/components/schemas/AddressValidity' + default: + description: Error + content: + application/json: + schema: + $ref: '#/components/schemas/ApiError' + + /blockchain/indexedHeight: + get: + description: Get current indexed block height. (The indexer has processed all blocks up to this height.) + operationId: getIndexedHeight + tags: + - blockchain + responses: + '200': + description: height of the indexer and full height + content: + application/json: + schema: + properties: + indexedHeight: + type: integer + default: 0 + description: number of blocks indexed + fullHeight: + type: integer + description: number of all known blocks + + /blockchain/transaction/byId/{txId}: + get: + description: Retrieve a transaction by its id + operationId: getTxById + tags: + - blockchain + parameters: + - in: path + name: txId + required: true + description: id of the wanted transaction + schema: + type: string + responses: + '200': + description: transaction with wanted id + content: + application/json: + schema: + $ref: '#/components/schemas/IndexedErgoTransaction' + '404': + description: Transaction with this id doesn't exist + content: + application/json: + schema: + $ref: '#/components/schemas/ApiError' + default: + description: Error + content: + application/json: + schema: + $ref: '#/components/schemas/ApiError' + + /blockchain/transaction/byAddress/{address}: + post: + description: Retrieve a list of transactions by their associated address + operationId: getTxsByAddress + tags: + - blockchain + parameters: + - in: path + name: address + required: true + description: address to retrieve associated transactions + schema: + $ref: '#/components/schemas/ErgoAddress' + - in: query + name: offset + required: false + description: amount of elements to skip from the start + schema: + type: integer + format: int32 + default: 0 + - in: query + name: limit + required: false + description: amount of elements to retrieve + schema: + type: integer + format: int32 + default: 5 + responses: + '200': + description: transactions associated with the given address + content: + application/json: + schema: + type: object + properties: + items: + type: array + description: Array of transactions + items: + $ref: '#/components/schemas/IndexedErgoTransaction' + total: + type: integer + description: Total count of retrieved transactions + '404': + description: No transactions found for wanted address + content: + application/json: + schema: + $ref: '#/components/schemas/ApiError' + default: + description: Error + content: + application/json: + schema: + $ref: '#/components/schemas/ApiError' + + /blockchain/box/byId/{boxId}: + get: + description: Retrieve a box by its id + operationId: getBoxById + tags: + - blockchain + parameters: + - in: path + name: boxId + required: true + description: id of the wanted box + schema: + type: string + responses: + '200': + description: box with wanted id + content: + application/json: + schema: + $ref: '#/components/schemas/IndexedErgoBox' + '404': + description: No box found with wanted id + content: + application/json: + schema: + $ref: '#/components/schemas/ApiError' + default: + description: Error + content: + application/json: + schema: + $ref: '#/components/schemas/ApiError' + + /blockchain/box/byAddress/{address}: + post: + description: Retrieve boxes by their associated address + operationId: getBoxesByAddress + tags: + - blockchain + parameters: + - in: path + name: address + required: true + description: address to retrieve boxes for + schema: + $ref: '#/components/schemas/ErgoAddress' + - in: query + name: offset + required: false + description: amount of elements to skip from the start + schema: + type: integer + format: int32 + default: 0 + - in: query + name: limit + required: false + description: amount of elements to retrieve + schema: + type: integer + format: int32 + default: 5 + responses: + '200': + description: boxes associated with wanted address + content: + application/json: + schema: + type: object + properties: + items: + type: array + description: Array of boxes + items: + $ref: '#/components/schemas/IndexedErgoBox' + total: + type: integer + description: Total number of retreived boxes + '404': + description: No boxes found for wanted address + content: + application/json: + schema: + $ref: '#/components/schemas/ApiError' + default: + description: Error + content: + application/json: + schema: + $ref: '#/components/schemas/ApiError' + + /blockchain/box/unspent/byAddress/{address}: + get: + description: Retrieve unspent boxes by their associated address + operationId: getBoxesByAddressUnspent + tags: + - blockchain + parameters: + - in: path + name: address + required: true + description: address to retrieve boxes for + schema: + $ref: '#/components/schemas/ErgoAddress' + - in: query + name: offset + required: false + description: amount of elements to skip from the start + schema: + type: integer + format: int32 + default: 0 + - in: query + name: limit + required: false + description: amount of elements to retrieve + schema: + type: integer + format: int32 + default: 5 + - in: query + name: sortDirection + required: false + description: desc = new boxes first ; asc = old boxes first + schema: + type: string + default: desc + responses: + '200': + description: unspent boxes associated with wanted address + content: + application/json: + schema: + type: array + description: Array of boxes + items: + $ref: '#/components/schemas/IndexedErgoBox' + '404': + description: No unspent boxes found for wanted address + content: + application/json: + schema: + $ref: '#/components/schemas/ApiError' + default: + description: Error + content: + application/json: + schema: + $ref: '#/components/schemas/ApiError' + + /blockchain/token/byId/{tokenId}: + get: + description: Retrieve minting information about a token + operationId: getTokenById + tags: + - blockchain + parameters: + - in: path + name: tokenId + required: true + description: id of the wanted token + schema: + type: string + responses: + '200': + description: token with wanted id + content: + application/json: + schema: + $ref: '#/components/schemas/IndexedToken' + '404': + description: No token found with wanted id + content: + application/json: + schema: + $ref: '#/components/schemas/ApiError' + default: + description: Error + content: + application/json: + schema: + $ref: '#/components/schemas/ApiError' + + /blockchain/balanceForAddress/{address}: + get: + description: Retrieve balance information of an Ergo address. + Separately return confirmed and unconfirmed balance information. + operationId: getBalanceForAddress + tags: + - blockchain + parameters: + - in: path + name: address + required: true + description: address to retrieve balance information for + schema: + $ref: '#/components/schemas/ErgoAddress' + responses: + '200': + description: balance information + content: + application/json: + schema: + type: object + properties: + confirmed: + description: confirmed balance of the address + $ref: '#/components/schemas/BalanceInfo' + unconfirmed: + description: unconfirmed balance of the address + $ref: '#/components/schemas/BalanceInfo' + default: + description: Error + content: + application/json: + schema: + $ref: '#/components/schemas/ApiError' \ No newline at end of file diff --git a/src/main/resources/api/openapi.yaml b/src/main/resources/api/openapi.yaml index 2b537d4fb7..563abdb14e 100644 --- a/src/main/resources/api/openapi.yaml +++ b/src/main/resources/api/openapi.yaml @@ -1,7 +1,7 @@ openapi: "3.0.2" info: - version: "5.0.13" + version: "5.0.14" title: Ergo Node API description: API docs for Ergo Node. Models are shared between all Ergo products contact: @@ -302,12 +302,12 @@ components: type: object description: Token indexed with extra information required: - -id - -boxId - -emissionAmount - -name - -description - -decimals + - id + - boxId + - emissionAmount + - name + - description + - decimals properties: id: description: Id of the token @@ -362,31 +362,32 @@ components: ErgoTransaction: type: object - description: Ergo transaction + description: ErgoTransaction is an atomic operation which changes UTXO state. required: - inputs - dataInputs - outputs properties: id: + description: Id of the transaction $ref: '#/components/schemas/TransactionId' inputs: - description: Inputs of the transaction + description: Inputs, that will be spent by this transaction type: array items: $ref: '#/components/schemas/ErgoTransactionInput' dataInputs: - description: Data inputs of the transaction + description: Read-only inputs, that are not going to be spent by transaction. type: array items: $ref: '#/components/schemas/ErgoTransactionDataInput' outputs: - description: Outputs of the transaction + description: Outputs of the transaction, i.e. box candidates to be created by this transaction. type: array items: $ref: '#/components/schemas/ErgoTransactionOutput' size: - description: Size in bytes + description: Size of ErgoTransaction in bytes type: integer format: int32 @@ -1540,6 +1541,10 @@ components: $ref: '#/components/schemas/BlockHeader' BlockHeader: + description: Header of a block. + It authenticates link to a previous block, other block sections + (transactions, UTXO set transformation proofs, extension), UTXO set, votes for blockchain parameters + to be changed and proof-of-work related data. type: object required: - id @@ -1557,27 +1562,37 @@ components: - votes properties: id: + description: Block id $ref: '#/components/schemas/ModifierId' timestamp: + description: Block generation time reported by a miner $ref: '#/components/schemas/Timestamp' version: + description: Protocol version used to generate the block $ref: '#/components/schemas/Version' adProofsRoot: + description: Digest of UTXO set transformation proofs $ref: '#/components/schemas/Digest32' stateRoot: + description: AVL+ tree digest of UTXO set (after the block is applied) $ref: '#/components/schemas/ADDigest' transactionsRoot: + description: Merkle tree digest of transactions in the block (BlockTransactions section) $ref: '#/components/schemas/Digest32' nBits: + description: Proof-of-work target (difficulty encoded) type: integer format: int64 minimum: 0 example: 19857408 extensionHash: + description: Merkle tree digest of the extension section of the block $ref: '#/components/schemas/Digest32' powSolutions: + description: Solution for the proof-of-work puzzle $ref: '#/components/schemas/PowSolutions' height: + description: Height of the block (genesis block height == 1) type: integer format: int32 minimum: 0 @@ -1588,19 +1603,24 @@ components: parentId: $ref: '#/components/schemas/ModifierId' votes: + description: Votes for changing system parameters $ref: '#/components/schemas/Votes' size: - description: Size in bytes + description: Size of the header in bytes type: integer format: int32 extensionId: + description: Hash of the extension section of the block == hash(modifier type id, header id, extensionHash) $ref: '#/components/schemas/ModifierId' transactionsId: + description: Hash of the transactions section of the block == hash(modifier type id, header id, transactionsRoot) $ref: '#/components/schemas/ModifierId' adProofsId: + description: Hash of the UTXO set transformation proofs section of the block == hash(modifier type id, header id, adProofsRoot) $ref: '#/components/schemas/ModifierId' BlockTransactions: + description: Section of a block which contains transactions. type: object required: - headerId @@ -1608,11 +1628,13 @@ components: - size properties: headerId: + description: Identifier of a header of a corresponding block $ref: '#/components/schemas/ModifierId' transactions: + description: Transactions of the block $ref: '#/components/schemas/Transactions' size: - description: Size in bytes + description: Size in bytes of all block transactions type: integer format: int32 @@ -1625,10 +1647,13 @@ components: - size properties: headerId: + description: Identifier of a header of the block which contains the proofs $ref: '#/components/schemas/ModifierId' proofBytes: + description: Serialized bytes of the authenticated dictionary proof $ref: '#/components/schemas/SerializedAdProof' digest: + description: Hash of the proofBytes $ref: '#/components/schemas/Digest32' size: description: Size in bytes @@ -1636,6 +1661,7 @@ components: format: int32 Extension: + description: Section of a block which contains extension data. type: object required: - headerId @@ -1643,8 +1669,10 @@ components: - fields properties: headerId: + description: Identifier of a header of a corresponding block $ref: '#/components/schemas/ModifierId' digest: + description: Root hash (aka digest) merkelized list of key-value records $ref: '#/components/schemas/Digest32' fields: description: List of key-value records @@ -1654,6 +1682,7 @@ components: $ref: '#/components/schemas/KeyValueItem' KeyValueItem: + description: Key-value record represented as a pair of hex strings in an array. type: array items: $ref: '#/components/schemas/HexString' @@ -2047,6 +2076,10 @@ components: description: Blacklisted node address NodeInfo: + description: Data container for /info API request output. + Contains information about node's state and configuration. + Contains data about best block, best header, state, etc. + Best block is the block with the maximum height. type: object required: - name @@ -2072,76 +2105,89 @@ components: - parameters properties: name: + description: Node's (peer) self-chosen name from config type: string example: my-node-1 appVersion: + description: Node's application version type: string example: 0.0.1 fullHeight: type: integer format: int32 - description: Can be 'null' if state is empty (no full block is applied since node launch) + description: Height of the best block known to the node. + Can be 'null' if state is empty (no full block is applied since node launch) minimum: 0 example: 667 nullable: true headersHeight: type: integer format: int32 - description: Can be 'null' if state is empty (no header applied since node launch) + description: The height of the best header (i.e. the one with the maximum height). + Can be 'null' if state is empty (no header applied since node launch) minimum: 0 example: 667 nullable: true maxPeerHeight: type: integer format: int32 - description: Maximum block height of connected peers. Can be 'null' if state is empty (no peer connected since node launch) + description: Maximum block height of connected peers. + Can be 'null' if state is empty (no peer connected since node launch) minimum: 0 example: 706162 nullable: true bestFullHeaderId: type: string - description: Can be 'null' if no full block is applied since node launch + description: Best full-block id (header id of such block). + Can be 'null' if no full block is applied since node launch. nullable: true allOf: - $ref: '#/components/schemas/ModifierId' previousFullHeaderId: type: string - description: Can be 'null' if no full block is applied since node launch + description: Header id of the parent block of the best full-block (i.e. previous block in the blockchain). + Can be 'null' if no full block is applied since node launch nullable: true allOf: - $ref: '#/components/schemas/ModifierId' bestHeaderId: type: string - description: Can be 'null' if no header applied since node launch + description: Best header ID (hex representation). + Can be 'null' if no header applied since node launch. nullable: true allOf: - $ref: '#/components/schemas/ModifierId' stateRoot: type: string nullable: true - description: Can be 'null' if state is empty (no full block is applied since node launch) + description: Current UTXO set digest. + Can be 'null' if state is empty (no full block is applied since node launch) example: 'dab9da11fc216660e974842cc3b7705e62ebb9e0bf5ff78e53f9cd40abadd117' stateType: + description: Whether the node is storing UTXO set or only its digest. + Equals `digest` if only digest is stored, `utxo` if full UTXO set is stored. type: string enum: - digest - utxo stateVersion: - description: Can be 'null' if no full block is applied since node launch + description: Id of a block where UTXO set digest is taken from. + Can be 'null' if no full block is applied since node launch. type: string example: 'fab9da11fc216660e974842cc3b7705e62ebb9e0bf5ff78e53f9cd40abadd117' nullable: true isMining: + description: Whether the node is mining (i.e. generating blocks). type: boolean example: true peersCount: type: integer - description: Number of connected peers + description: Number of peers this node is connected with. format: int32 minimum: 0 example: 327 unconfirmedCount: - description: Current unconfirmed transactions count + description: Number of unconfirmed transactions in the mempool. type: integer format: int32 minimum: 0 @@ -2152,8 +2198,8 @@ components: minimum: 0 nullable: true example: 667 - description: > - Difficulty on current bestFullHeaderId. Can be 'null' if no full block is applied since node launch. + description: Difficulty on current bestFullHeaderId. + Can be 'null' if no full block is applied since node launch. Difficulty is a BigInt integer. currentTime: type: integer @@ -2162,26 +2208,28 @@ components: - $ref: '#/components/schemas/Timestamp' launchTime: type: integer - description: Time when the node was started + description: When the node was launched (in Java time format, UNIX time * 1000). allOf: - $ref: '#/components/schemas/Timestamp' headersScore: type: integer - description: Can be 'null' if no headers is applied since node launch. headersScore is a BigInt integer. + description: Cumulative difficulty of best headers-chain. + Can be 'null' if no headers is applied since node launch. headersScore is a BigInt integer. nullable: true fullBlocksScore: type: integer - description: Can be 'null' if no full block is applied since node launch. fullBlocksScore is a BigInt integer. + description: Cumulative difficulty of best full blocks chain. + Can be 'null' if no full block is applied since node launch. fullBlocksScore is a BigInt integer. nullable: true genesisBlockId: type: string - description: Can be 'null' if genesis blocks is not produced yet + description: Header id of genesis block. Can be 'null' if genesis blocks is not produced yet. nullable: true allOf: - $ref: '#/components/schemas/ModifierId' parameters: type: object - description: current parameters + description: System parameters which could be readjusted via collective miners decision. $ref: '#/components/schemas/Parameters' eip27Supported: type: boolean @@ -2193,6 +2241,7 @@ components: description: Publicly accessible url of node which exposes restApi in firewall Parameters: + description: System parameters which could be readjusted via collective miners decision. type: object required: - height @@ -2306,7 +2355,7 @@ components: example: '02a7955281885bf0f0ca4a48678848cad8dc5b328ce8bc1d4481d041c98e891ff3' Transactions: - description: Ergo transaction objects + description: List of ErgoTransaction objects type: array items: $ref: '#/components/schemas/ErgoTransaction' @@ -2472,6 +2521,7 @@ components: type: string ApiError: + description: Error response from API type: object required: - error @@ -2484,7 +2534,7 @@ components: example: 500 reason: type: string - description: String error code + description: Error message explaining the reason of the error example: 'Internal server error' detail: type: string @@ -2494,7 +2544,8 @@ components: paths: /blocks: get: - summary: Get the Array of header ids + summary: Get an array of header ids (hex encoded) for the given range of blockchain block heights. + Returns a page of the whole list starting from `offset` and containing `limit` items. operationId: getHeaderIds tags: - blocks @@ -2512,7 +2563,7 @@ paths: - in: query name: offset required: false - description: The number of items in list to skip + description: The first block height to include in the list schema: type: integer format: int32 @@ -2559,7 +2610,7 @@ paths: /blocks/at/{blockHeight}: get: - summary: Get the header ids at a given height + summary: Get header ids at the given height operationId: getFullBlockAt tags: - blocks @@ -2567,7 +2618,7 @@ paths: - in: path name: blockHeight required: true - description: Height of a wanted block + description: Height of a block to retrieve header ids schema: type: integer format: int32 @@ -2598,7 +2649,7 @@ paths: /blocks/chainSlice: get: - summary: Get headers in a specified range + summary: Get headers in a specified range of heights operationId: getChainSlice tags: - blocks @@ -2606,7 +2657,7 @@ paths: - in: query name: fromHeight required: false - description: Min header height + description: Min header height (start of the range) schema: type: integer format: int32 @@ -2614,7 +2665,7 @@ paths: - in: query name: toHeight required: false - description: Max header height (best header height by default) + description: Max header height of the range (last header height then omitted) schema: type: integer format: int32 @@ -2638,7 +2689,7 @@ paths: /blocks/{headerId}: get: - summary: Get the full block info by a given signature + summary: Get the full block info by a given header id operationId: getFullBlockById tags: - blocks @@ -2647,12 +2698,12 @@ paths: - in: path name: headerId required: true - description: ID of a wanted block + description: ID of the header the wanted block schema: type: string responses: '200': - description: Block object + description: Block object representing the full block data content: application/json: schema: @@ -2672,7 +2723,7 @@ paths: /blocks/{headerId}/header: get: - summary: Get the block header info by a given signature + summary: Get the block header info by a given header id operationId: getBlockHeaderById tags: - blocks @@ -2782,7 +2833,7 @@ paths: - in: path name: count required: true - description: count of a wanted block headers + description: a number of block headers to return schema: type: number responses: @@ -5717,7 +5768,7 @@ paths: /blockchain/indexedHeight: get: - summary: Get current block height the indexer is at + summary: Get current indexed block height. (The indexer has processed all blocks up to this height.) operationId: getIndexedHeight tags: - blockchain diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf index 6227bf7906..4df8fa50e2 100644 --- a/src/main/resources/application.conf +++ b/src/main/resources/application.conf @@ -42,9 +42,6 @@ ergo { # how many different proofs we are downloading from other peers # and comparing with each other, before choosing best one p2pNipopows = 1 - - # Minimal suffix size for NiPoPoW proof (may be pre-defined constant or settings parameter) - nipopowSuffix = 10 } # Is the node is doing mining @@ -434,7 +431,7 @@ scorex { nodeName = "ergo-node" # Network protocol version to be sent in handshakes - appVersion = 5.0.13 + appVersion = 5.0.14 # Network agent name. May contain information about client code # stack, starting from core code-base up to the end graphical interface. diff --git a/src/main/scala/org/ergoplatform/http/ErgoHttpService.scala b/src/main/scala/org/ergoplatform/http/ErgoHttpService.scala index 1bc963cbe6..c331e005e2 100644 --- a/src/main/scala/org/ergoplatform/http/ErgoHttpService.scala +++ b/src/main/scala/org/ergoplatform/http/ErgoHttpService.scala @@ -2,10 +2,14 @@ package org.ergoplatform.http import akka.actor.ActorSystem import akka.http.scaladsl.model.StatusCodes +import akka.http.scaladsl.model.HttpHeader import akka.http.scaladsl.server.{ExceptionHandler, RejectionHandler, Route} +import akka.http.scaladsl.server.Directive0 import akka.http.scaladsl.server.directives.RouteDirectives import scorex.core.api.http.{ApiErrorHandler, ApiRejectionHandler, ApiRoute, CorsHandler} +import akka.http.scaladsl.model.headers._ +import scala.collection.immutable final case class ErgoHttpService( apiRoutes: Seq[ApiRoute], @@ -17,6 +21,21 @@ final case class ErgoHttpService( def exceptionHandler: ExceptionHandler = ApiErrorHandler.exceptionHandler + private val corsResponseHeaders: List[ModeledHeader] = List[ModeledHeader]( + `Access-Control-Allow-Origin`.*, + `Access-Control-Allow-Credentials`(true), + `Access-Control-Allow-Headers`("Authorization", "Content-Type", "X-Requested-With", "api_key", + "openai-conversation-id", + "openai-ephemeral-user-id", + "baggage", + "sentry-trace" + ) + ) + + override def respondWithHeaders(responseHeaders: immutable.Seq[HttpHeader]): Directive0 = { + super.respondWithHeaders(corsResponseHeaders) + } + val compositeRoute: Route = handleRejections(rejectionHandler) { handleExceptions(exceptionHandler) { diff --git a/src/main/scala/org/ergoplatform/http/api/ApiCodecs.scala b/src/main/scala/org/ergoplatform/http/api/ApiCodecs.scala index 013a747a76..f293daa633 100644 --- a/src/main/scala/org/ergoplatform/http/api/ApiCodecs.scala +++ b/src/main/scala/org/ergoplatform/http/api/ApiCodecs.scala @@ -36,6 +36,8 @@ import sigmastate.serialization.OpCodes import special.sigma.AnyValue import org.ergoplatform.nodeView.state.SnapshotsInfo import org.ergoplatform.nodeView.state.UtxoState.ManifestId +import org.ergoplatform.sdk.JsonCodecs + import java.math.BigInteger import scala.util.{Failure, Success, Try} diff --git a/src/main/scala/org/ergoplatform/http/api/BlockchainApiRoute.scala b/src/main/scala/org/ergoplatform/http/api/BlockchainApiRoute.scala index cdc4d7feca..408f7a0364 100644 --- a/src/main/scala/org/ergoplatform/http/api/BlockchainApiRoute.scala +++ b/src/main/scala/org/ergoplatform/http/api/BlockchainApiRoute.scala @@ -68,16 +68,20 @@ case class BlockchainApiRoute(readersHolder: ActorRef, ergoSettings: ErgoSetting getTxByIdR ~ getTxByIndexR ~ getTxsByAddressR ~ + getTxsByAddressGetRoute ~ getTxRangeR ~ getBoxByIdR ~ getBoxByIndexR ~ getBoxesByAddressR ~ + getBoxesByAddressGetRoute ~ getBoxesByAddressUnspentR ~ + getBoxesByAddressUnspentGetRoute ~ getBoxRangeR ~ getBoxesByErgoTreeR ~ getBoxesByErgoTreeUnspentR ~ getTokenInfoByIdR ~ - getAddressBalanceTotalR + getAddressBalanceTotalR ~ + getAddressBalanceTotalGetRoute } else pathPrefix("blockchain") { @@ -147,14 +151,24 @@ case class BlockchainApiRoute(readersHolder: ActorRef, ergoSettings: ErgoSetting } } - private def getTxsByAddressR: Route = (post & pathPrefix("transaction" / "byAddress") & ergoAddress & paging) { (address, offset, limit) => - if(limit > MaxItems) { + private def validateAndGetTxsByAddress(address: ErgoAddress, + offset: Int, + limit: Int): Route = { + if (limit > MaxItems) { BadRequest(s"No more than $MaxItems transactions can be requested") - }else { + } else { ApiResponse(getTxsByAddress(address, offset, limit)) } } + private def getTxsByAddressR: Route = (post & pathPrefix("transaction" / "byAddress") & ergoAddress & paging) { (address, offset, limit) => + validateAndGetTxsByAddress(address, offset, limit) + } + + private def getTxsByAddressGetRoute: Route = (pathPrefix("transaction" / "byAddress") & get & addressPass & paging) { (address, offset, limit) => + validateAndGetTxsByAddress(address, offset, limit) + } + private def getTxRange(offset: Int, limit: Int): Future[Seq[ModifierId]] = getHistory.map { history => val base: Long = getIndex(GlobalTxIndexKey, history).getLong - offset @@ -205,14 +219,24 @@ case class BlockchainApiRoute(readersHolder: ActorRef, ergoSettings: ErgoSetting } } - private def getBoxesByAddressR: Route = (post & pathPrefix("box" / "byAddress") & ergoAddress & paging) { (address, offset, limit) => - if(limit > MaxItems) { + private def validateAndGetBoxesByAddress(address: ErgoAddress, + offset: Int, + limit: Int) = { + if (limit > MaxItems) { BadRequest(s"No more than $MaxItems boxes can be requested") - }else { + } else { ApiResponse(getBoxesByAddress(address, offset, limit)) } } + private def getBoxesByAddressR: Route = (post & pathPrefix("box" / "byAddress") & ergoAddress & paging) { (address, offset, limit) => + validateAndGetBoxesByAddress(address, offset, limit) + } + + private def getBoxesByAddressGetRoute: Route = (pathPrefix("box" / "byAddress") & get & addressPass & paging) { (address, offset, limit) => + validateAndGetBoxesByAddress(address, offset, limit) + } + private def getBoxesByAddressUnspent(addr: ErgoAddress, offset: Int, limit: Int, sortDir: Direction): Future[Seq[IndexedErgoBox]] = getHistory.map { history => getAddress(addr)(history) match { @@ -221,16 +245,31 @@ case class BlockchainApiRoute(readersHolder: ActorRef, ergoSettings: ErgoSetting } } - private def getBoxesByAddressUnspentR: Route = (post & pathPrefix("box" / "unspent" / "byAddress") & ergoAddress & paging & sortDir) { (address, offset, limit, dir) => - if(limit > MaxItems) { + private def validateAndGetBoxesByAddressUnspent(address: ErgoAddress, + offset: Int, + limit: Int, + dir: Direction): Route = { + if (limit > MaxItems) { BadRequest(s"No more than $MaxItems boxes can be requested") - }else if(dir == SortDirection.INVALID) { + } else if (dir == SortDirection.INVALID) { BadRequest("Invalid parameter for sort direction, valid values are \"ASC\" and \"DESC\"") - }else { + } else { ApiResponse(getBoxesByAddressUnspent(address, offset, limit, dir)) } } + private def getBoxesByAddressUnspentR: Route = + (post & pathPrefix("box" / "unspent" / "byAddress") & ergoAddress & paging & sortDir) { + (address, offset, limit, dir) => + validateAndGetBoxesByAddressUnspent(address, offset, limit, dir) + } + + private def getBoxesByAddressUnspentGetRoute: Route = + (pathPrefix("box" / "unspent" / "byAddress") & get & addressPass & paging & sortDir) { + (address, offset, limit, dir) => + validateAndGetBoxesByAddressUnspent(address, offset, limit, dir) + } + private def getBoxRange(offset: Int, limit: Int): Future[Seq[ModifierId]] = getHistory.map { history => val base: Long = getIndex(GlobalBoxIndexKey, history).getLong - offset @@ -318,6 +357,14 @@ case class BlockchainApiRoute(readersHolder: ActorRef, ergoSettings: ErgoSetting ApiResponse(getAddressBalanceTotal(address)) } + /** Parses address in the url (i.e. `balanceForAddress/{address}` into [[ErgoAddress]] using [[ErgoAddressEncoder]]. */ + private val addressPass: Directive1[ErgoAddress] = pathPrefix(Segment).flatMap(handleErgoAddress) + + private def getAddressBalanceTotalGetRoute: Route = + (pathPrefix("balanceForAddress") & get & addressPass) { address => + ApiResponse(getAddressBalanceTotal(address)) + } + } object SortDirection { diff --git a/src/main/scala/org/ergoplatform/http/api/InfoApiRoute.scala b/src/main/scala/org/ergoplatform/http/api/InfoApiRoute.scala index ce04c7fbe8..e848955fb5 100644 --- a/src/main/scala/org/ergoplatform/http/api/InfoApiRoute.scala +++ b/src/main/scala/org/ergoplatform/http/api/InfoApiRoute.scala @@ -1,6 +1,7 @@ package org.ergoplatform.http.api import akka.actor.{ActorRef, ActorRefFactory} +import akka.http.scaladsl.model.ContentTypes import akka.http.scaladsl.server.Route import akka.pattern.ask import io.circe.syntax._ @@ -16,9 +17,17 @@ case class InfoApiRoute(statsCollector: ActorRef, settings: RESTApiSettings) (implicit val context: ActorRefFactory) extends ErgoBaseApiRoute { - override val route: Route = (path("info") & get) { - val timeJson = Map("currentTime" -> System.currentTimeMillis().asJson).asJson - ApiResponse((statsCollector ? GetNodeInfo).mapTo[NodeInfo].map(_.asJson.deepMerge(timeJson))) + override val route: Route = { + (path("info") & get) { + val timeJson = Map("currentTime" -> System.currentTimeMillis().asJson).asJson + ApiResponse((statsCollector ? GetNodeInfo).mapTo[NodeInfo].map(_.asJson.deepMerge(timeJson))) + } ~ + (path(".well-known" / "ai-plugin.json") & get) { + getFromResource(".well-known/ai-plugin.json", ContentTypes.`application/json`) + } ~ + (path("openapi.yaml") & get) { + getFromResource("api/openapi-ai.yaml", ContentTypes.`text/plain(UTF-8)`) + } } } diff --git a/src/main/scala/org/ergoplatform/http/api/ScanEntities.scala b/src/main/scala/org/ergoplatform/http/api/ScanEntities.scala index 312fcbd5fb..f2ae3f0a17 100644 --- a/src/main/scala/org/ergoplatform/http/api/ScanEntities.scala +++ b/src/main/scala/org/ergoplatform/http/api/ScanEntities.scala @@ -2,7 +2,8 @@ package org.ergoplatform.http.api import io.circe.{Decoder, HCursor} import org.ergoplatform.ErgoBox.BoxId -import org.ergoplatform.{ErgoBox, JsonCodecs} +import org.ergoplatform.ErgoBox +import org.ergoplatform.sdk.JsonCodecs import org.ergoplatform.wallet.Constants.ScanId /** diff --git a/src/main/scala/org/ergoplatform/local/CleanupWorker.scala b/src/main/scala/org/ergoplatform/local/CleanupWorker.scala index 1cd15c4c3f..01232ad607 100644 --- a/src/main/scala/org/ergoplatform/local/CleanupWorker.scala +++ b/src/main/scala/org/ergoplatform/local/CleanupWorker.scala @@ -12,7 +12,9 @@ import scorex.util.{ModifierId, ScorexLogging} import scala.annotation.tailrec import scala.collection.mutable +import scala.concurrent.Future import scala.util.{Failure, Success} +import scala.concurrent.ExecutionContext.Implicits.global /** * Performs mempool transactions re-validation. Called on a new block coming. @@ -33,35 +35,34 @@ class CleanupWorker(nodeViewHolderRef: ActorRef, override def receive: Receive = { case RunCleanup(validator, mempool) => - runCleanup(validator, mempool) - sender() ! CleanupDone + val s = sender() + validatePool(validator, mempool) + .map { case (validated, toEliminate) => + log.debug(s"${validated.size} re-checked mempool transactions were ok, " + + s"${toEliminate.size} transactions were invalidated") + + if (validated.nonEmpty) { + nodeViewHolderRef ! RecheckedTransactions(validated) + } + if (toEliminate.nonEmpty) { + nodeViewHolderRef ! EliminateTransactions(toEliminate) + } + s ! CleanupDone + }.andThen { case Failure(ex) => + logger.error("Mempool validation failed", ex) + } //Should not be here, if non-expected signal comes, check logic case a: Any => log.warn(s"Strange input: $a") } - private def runCleanup(validator: UtxoStateReader, - mempool: ErgoMemPoolReader): Unit = { - val (validated, toEliminate) = validatePool(validator, mempool) - - log.debug(s"${validated.size} re-checked mempool transactions were ok, " + - s"${toEliminate.size} transactions were invalidated") - - if (validated.nonEmpty) { - nodeViewHolderRef ! RecheckedTransactions(validated) - } - if (toEliminate.nonEmpty) { - nodeViewHolderRef ! EliminateTransactions(toEliminate) - } - } - /** * Validates transactions from mempool for some specified amount of time. * * @return - updated valid transactions and invalidated transaction ids */ private def validatePool(validator: UtxoStateReader, - mempool: ErgoMemPoolReader): (Seq[UnconfirmedTransaction], Seq[ModifierId]) = { + mempool: ErgoMemPoolReader): Future[(Seq[UnconfirmedTransaction], Seq[ModifierId])] = Future { val now = System.currentTimeMillis() diff --git a/src/main/scala/org/ergoplatform/local/ErgoStatsCollector.scala b/src/main/scala/org/ergoplatform/local/ErgoStatsCollector.scala index febc4eebb3..c171e5ab28 100644 --- a/src/main/scala/org/ergoplatform/local/ErgoStatsCollector.scala +++ b/src/main/scala/org/ergoplatform/local/ErgoStatsCollector.scala @@ -165,7 +165,7 @@ object ErgoStatsCollector { * @param isMining - whether the node is mining * @param bestHeaderOpt - best header ID * @param headersScore - cumulative difficulty of best headers-chain - * @param bestFullBlockOpt - best full-block id (header id of such block) + * @param bestFullBlockOpt - Best full-block known to the node. Can be None if state is empty (no full block is applied since node launch) * @param fullBlocksScore - cumulative difficulty of best full blocks chain * @param maxPeerHeight - maximum block height of connected peers * @param launchTime - when the node was launched (in Java time format, basically, UNIX time * 1000) diff --git a/src/main/scala/org/ergoplatform/modifiers/history/extension/Extension.scala b/src/main/scala/org/ergoplatform/modifiers/history/extension/Extension.scala index 27c53bc80b..c402d88342 100644 --- a/src/main/scala/org/ergoplatform/modifiers/history/extension/Extension.scala +++ b/src/main/scala/org/ergoplatform/modifiers/history/extension/Extension.scala @@ -14,7 +14,7 @@ import scorex.util.ModifierId /** * Extension section of Ergo block. Contains key-value storage - * represented as Seq[(Array[Byte], Array[Byte])] with mandatory and optional fields. + * represented as Seq[(Array[Byte], Array[Byte])] with mandatory (consensus-critical) and optional fields. * * @param headerId - id of corresponding header * @param fields - fields as a sequence of key -> value records. A key is 2-bytes long, value is 64 bytes max. @@ -31,8 +31,8 @@ case class Extension(headerId: ModifierId, override def serializer: ErgoSerializer[Extension] = ExtensionSerializer override def toString: String = { - s"Extension(id: $id, headerId: ${Algos.encode(headerId)}, " + - s"fields: ${fields.map(kv => s"${Algos.encode(kv._1)} -> ${Algos.encode(kv._2)}")}) " + val fieldsEncoded = fields.map(kv => s"${Algos.encode(kv._1)} -> ${Algos.encode(kv._2)}") + s"Extension(id: $id, headerId: $headerId, fields: $fieldsEncoded)" } } @@ -42,20 +42,51 @@ object Extension extends ApiCodecs { val FieldKeySize: Int = 2 val FieldValueMaxSize: Int = 64 - //predefined key prefixes + /** + * Predefined key spaces. Defined by first byte of a key, then there could be up to 256 keys per key space, as + * a key is of 2 bytes always. + */ + + /** + * Every voting epoch length blocks (so every 1024 blocks for the Ergo mainnet, current blockchain parameters are + * written into extension section, to support light clients which do not processing all the blocks but need to know + * current blockchain parameters (i.e. without processing all the historical blocks) to process new blocks. + * + * All the parameters are written into key space defined by the value below. + */ val SystemParametersPrefix: Byte = 0x00 + + /** + * Every block interlinks vector needed in order to build and validate NiPoPoWs is written into extension section, + * + * All the interlinks are written into a single key space defined by the value below. + */ val InterlinksVectorPrefix: Byte = 0x01 + + /** + * It is possible to switch some soft-forkable rules and introduce new ones via 90+% mines voting. Changes in rules + * against the genesis block are to be written into a single key space defined by the value below. + */ val ValidationRulesPrefix: Byte = 0x02 + /** + * Id a type of network object encoding extension + */ + val modifierTypeId: NetworkObjectTypeId.Value = ExtensionTypeId.value + + /** + * @return key-value pair `kv` encoded as Merkle tree leaf (byte array) + */ def kvToLeaf(kv: (Array[Byte], Array[Byte])): Array[Byte] = Bytes.concat(Array(kv._1.length.toByte), kv._1, kv._2) + /** + * @return Merkle tree built on top of key-value pairs + */ def merkleTree(fields: Seq[(Array[Byte], Array[Byte])]): MerkleTree[Digest32] = { Algos.merkleTree(LeafData @@ fields.map(kvToLeaf)) } - val modifierTypeId: NetworkObjectTypeId.Value = ExtensionTypeId.value - implicit val jsonEncoder: Encoder[Extension] = { e: Extension => Map( "headerId" -> Algos.encode(e.headerId).asJson, diff --git a/src/main/scala/org/ergoplatform/modifiers/history/header/Header.scala b/src/main/scala/org/ergoplatform/modifiers/history/header/Header.scala index 2d82a70f74..cd15b11ae7 100644 --- a/src/main/scala/org/ergoplatform/modifiers/history/header/Header.scala +++ b/src/main/scala/org/ergoplatform/modifiers/history/header/Header.scala @@ -142,7 +142,7 @@ object Header extends ApiCodecs { version = header.version, parentId = header.parentId.toBytes.toColl, ADProofsRoot = header.ADProofsRoot.asInstanceOf[Array[Byte]].toColl, - stateRoot = CAvlTree(ErgoInterpreter.avlTreeFromDigest(header.stateRoot)), + stateRoot = CAvlTree(ErgoInterpreter.avlTreeFromDigest(header.stateRoot.toColl)), transactionsRoot = header.transactionsRoot.asInstanceOf[Array[Byte]].toColl, timestamp = header.timestamp, nBits = header.nBits, diff --git a/src/main/scala/org/ergoplatform/network/ErgoNodeViewSynchronizer.scala b/src/main/scala/org/ergoplatform/network/ErgoNodeViewSynchronizer.scala index 70061fc910..d00048f597 100644 --- a/src/main/scala/org/ergoplatform/network/ErgoNodeViewSynchronizer.scala +++ b/src/main/scala/org/ergoplatform/network/ErgoNodeViewSynchronizer.scala @@ -35,7 +35,6 @@ import scorex.util.{ModifierId, ScorexLogging} import scorex.core.network.DeliveryTracker import scorex.core.network.peer.PenaltyType import scorex.core.transaction.state.TransactionValidation.TooHighCostError -import scorex.core.app.Version import scorex.crypto.hash.Digest32 import org.ergoplatform.nodeView.state.UtxoState.{ManifestId, SubtreeId} import org.ergoplatform.ErgoLikeContext.Height @@ -75,8 +74,6 @@ class ErgoNodeViewSynchronizer(networkControllerRef: ActorRef, Restart } - private val blockSectionsDownloadFilter = BlockSectionsDownloadFilter(settings.nodeSettings.stateType) - private var syncInfoV1CacheByHeadersHeight: Option[(Int, ErgoSyncInfoV1)] = Option.empty private var syncInfoV2CacheByHeadersHeight: Option[(Int, ErgoSyncInfoV2)] = Option.empty @@ -530,7 +527,8 @@ class ErgoNodeViewSynchronizer(networkControllerRef: ActorRef, * @return available peers to download headers from together with the state/origin of the peer */ private def getPeersForDownloadingHeaders(callingPeer: ConnectedPeer): Iterable[ConnectedPeer] = { - syncTracker.peersByStatus.getOrElse(Older, Array(callingPeer)) + val nonFiltered: Iterable[ConnectedPeer] = syncTracker.peersByStatus.getOrElse(Older, Array(callingPeer)) + HeadersDownloadFilter.filter(nonFiltered) } /** @@ -545,7 +543,7 @@ class ErgoNodeViewSynchronizer(networkControllerRef: ActorRef, .orElse { Option(peersByStatus.getOrElse(Unknown, mutable.WrappedArray.empty) ++ peersByStatus.getOrElse(Fork, mutable.WrappedArray.empty)) .filter(_.nonEmpty) - }.map(blockSectionsDownloadFilter.filter) + }.map(BlockSectionsDownloadFilter.filter) } /** @@ -1129,14 +1127,8 @@ class ErgoNodeViewSynchronizer(networkControllerRef: ActorRef, Seq.empty } case _ => - if (peer.peerInfo.map(_.peerSpec.protocolVersion).getOrElse(Version.initial) == Version.v4043 && - modifierTypeId == Header.modifierTypeId) { - log.debug("Header ids from 4.0.43") - Seq.empty - } else { - log.info(s"Processing ${invData.ids.length} non-tx invs (of type $modifierTypeId) from $peer") - invData.ids.filter(mid => deliveryTracker.status(mid, modifierTypeId, Seq(hr)) == ModifiersStatus.Unknown) - } + log.info(s"Processing ${invData.ids.length} non-tx invs (of type $modifierTypeId) from $peer") + invData.ids.filter(mid => deliveryTracker.status(mid, modifierTypeId, Seq(hr)) == ModifiersStatus.Unknown) } if (newModifierIds.nonEmpty) { diff --git a/src/main/scala/org/ergoplatform/network/ModePeerFeature.scala b/src/main/scala/org/ergoplatform/network/ModePeerFeature.scala index e2f46a596e..b5b47f2556 100644 --- a/src/main/scala/org/ergoplatform/network/ModePeerFeature.scala +++ b/src/main/scala/org/ergoplatform/network/ModePeerFeature.scala @@ -15,36 +15,73 @@ import scorex.util.serialization.{Reader, Writer} * * @param stateType - information on whether UTXO set is store (so state type is UTXO/Digest) * @param verifyingTransactions - whether the peer is verifying transactions - * @param nipopowSuffix - whether the peer has has bootstrapped via Nipopows, and length of proof suffix + * @param nipopowBootstrapped - whether the peer has bootstrapped via Nipopows, + * and if so, supported bootstrapping options (only one currently) * @param blocksToKeep - how many last full blocks the peer is storing */ case class ModePeerFeature(stateType: StateType, verifyingTransactions: Boolean, - nipopowSuffix: Option[Int], + nipopowBootstrapped: Option[Int], blocksToKeep: Int) extends PeerFeature { override type M = ModePeerFeature override val featureId: Id = PeerFeatureDescriptors.ModeFeatureId override def serializer: ErgoSerializer[ModePeerFeature] = ModeFeatureSerializer + + /** + * @return whether the peer has all the full blocks + */ + def allBlocksAvailable: Boolean = blocksToKeep == ModePeerFeature.AllBlocksKept + + + /** + * @return whether the peer has all the headers + */ + def allHeadersAvailable: Boolean = nipopowBootstrapped.isEmpty } object ModePeerFeature { import io.circe.syntax._ + /** + * Flag which is indicating NiPoPoW bootstrap mode. Currently there is only one option (suffix proof done + * according to KMZ17 paper), which does not exclude possibility for more options in future + */ + val NiPoPoWDefaultFlag = 1 + + /** + * Flag value which is when used as length of blockchain suffix kept locally means that all the full blocks are + * stored + */ + val AllBlocksKept = -1 + + /** + * Flag value which is when used as length of blockchain suffix kept locally means that a node was bootstrapped + * via UTXO set snapshot, so not all the full blocks stored, but at the same time there is no fixed-length suffix + * as after bootstrapping there is no pruning + */ + val UTXOSetBootstrapped = -2 + def apply(nodeSettings: NodeConfigurationSettings): ModePeerFeature = { - val popowSuffix = if (nodeSettings.nipopowSettings.nipopowBootstrap) { - Some(nodeSettings.nipopowSettings.nipopowSuffix) + val popowBootstrapped = if (nodeSettings.nipopowSettings.nipopowBootstrap) { + Some(NiPoPoWDefaultFlag) } else { None } + val blocksKept = if (nodeSettings.utxoSettings.utxoBootstrap) { + UTXOSetBootstrapped + } else { + nodeSettings.blocksToKeep + } + new ModePeerFeature( nodeSettings.stateType, nodeSettings.verifyTransactions, - popowSuffix, - nodeSettings.blocksToKeep + popowBootstrapped, + blocksKept ) } @@ -76,8 +113,8 @@ object ModeFeatureSerializer extends ErgoSerializer[ModePeerFeature] { override def serialize(mf: ModePeerFeature, w: Writer): Unit = { w.put(mf.stateType.stateTypeCode) w.put(booleanToByte(mf.verifyingTransactions)) - w.putOption(mf.nipopowSuffix)(_.putInt(_)) - w.putInt(mf.blocksToKeep) // todo: put -2 if bootstrapped via utxo set snapshot? https://github.com/ergoplatform/ergo/issues/2014 + w.putOption(mf.nipopowBootstrapped)(_.putInt(_)) + w.putInt(mf.blocksToKeep) } override def parse(r: Reader): ModePeerFeature = { @@ -85,13 +122,13 @@ object ModeFeatureSerializer extends ErgoSerializer[ModePeerFeature] { val stateType = StateType.fromCode(r.getByte()) val verifyingTransactions = byteToBoolean(r.getByte()) - val popowSuffix = r.getOption(r.getInt()) + val popowBootstrapped = r.getOption(r.getInt()) val blocksToKeep = r.getInt() new ModePeerFeature( stateType, verifyingTransactions, - popowSuffix, + popowBootstrapped, blocksToKeep ) } diff --git a/src/main/scala/org/ergoplatform/network/VersionBasedPeerFilteringRule.scala b/src/main/scala/org/ergoplatform/network/VersionBasedPeerFilteringRule.scala index c7499c878e..ed831e470e 100644 --- a/src/main/scala/org/ergoplatform/network/VersionBasedPeerFilteringRule.scala +++ b/src/main/scala/org/ergoplatform/network/VersionBasedPeerFilteringRule.scala @@ -1,6 +1,5 @@ package org.ergoplatform.network -import org.ergoplatform.nodeView.state.StateType import scorex.core.app.Version import scorex.core.network.ConnectedPeer @@ -48,44 +47,6 @@ trait VersionBasedPeerFilteringRule extends PeerFilteringRule { } - -/** - * 4.0.22+ allow for downloading ADProofs that are too big in block at 667614 - * for prior versions, a peer will not deliver block # 667614 and some other blocks - */ -object DigestModeFilter extends VersionBasedPeerFilteringRule { - - override def condition(version: Version): Boolean = { - version.compare(Version.v4022) >= 0 - } - -} - -/** - * Filter out peers of 4.0.17 or 4.0.18 version as they are delivering broken block sections - */ -object BrokenModifiersFilter extends VersionBasedPeerFilteringRule { - - override def condition(version: Version): Boolean = { - version != Version.v4017 && version != Version.v4018 - } - -} - -/** - * Filter to download block sections, combining `DigestModeFilter` and `BrokenModifiersFilter` - * @param stateType - own (node's) state type - */ -final case class BlockSectionsDownloadFilter(stateType: StateType) extends VersionBasedPeerFilteringRule { - override def condition(version: Version): Boolean = { - if (stateType == StateType.Digest) { - DigestModeFilter.condition(version) - } else { - BrokenModifiersFilter.condition(version) - } - } -} - /** * If peer's version is >= 4.0.16, the peer is supporting sync V2 */ @@ -125,8 +86,29 @@ object NipopowSupportFilter extends PeerFilteringRule { override def condition(peer: ConnectedPeer): Boolean = { val version = peer.peerInfo.map(_.peerSpec.protocolVersion).getOrElse(Version.initial) - peer.mode.flatMap(_.nipopowSuffix).isEmpty && + peer.mode.flatMap(_.nipopowBootstrapped).isEmpty && version.compare(Version.NipopowActivationVersion) >= 0 } } + +/** + * Filter to download block sections (except of headers). + * Currently, it is accepting peers which do have all the full blocks (so not bootstrapped via UTXO set snapshot + * or stateless client with full blocks suffix) + */ +object BlockSectionsDownloadFilter extends PeerFilteringRule { + override def condition(peer: ConnectedPeer): Boolean = { + peer.mode.exists(_.allBlocksAvailable) + } +} + +/** + * Filter to download headers. + * Currently, it is accepting peers which do have all the headers (so not bootstrapped via NiPoPoWs. + */ +object HeadersDownloadFilter extends PeerFilteringRule { + override def condition(peer: ConnectedPeer): Boolean = { + peer.mode.exists(_.allHeadersAvailable) + } +} diff --git a/src/main/scala/org/ergoplatform/nodeView/history/ErgoHistoryReader.scala b/src/main/scala/org/ergoplatform/nodeView/history/ErgoHistoryReader.scala index 8c80963471..7dfd9c9ba4 100644 --- a/src/main/scala/org/ergoplatform/nodeView/history/ErgoHistoryReader.scala +++ b/src/main/scala/org/ergoplatform/nodeView/history/ErgoHistoryReader.scala @@ -445,7 +445,8 @@ trait ErgoHistoryReader (typedModifierById[BlockTransactions](header.transactionsId), typedModifierById[Extension](header.extensionId), typedModifierById[ADProofs](header.ADProofsId)) match { - case (Some(txs), Some(ext), Some(proofs)) => Some(ErgoFullBlock(header, txs, ext, Some(proofs))) + case (Some(txs), Some(ext), Some(proofs)) => + Some(ErgoFullBlock(header, txs, ext, Some(proofs))) case (Some(txs), Some(ext), None) if !nodeSettings.stateType.requireProofs => Some(ErgoFullBlock(header, txs, ext, None)) case _ => None diff --git a/src/main/scala/org/ergoplatform/nodeView/mempool/TransactionMembershipProof.scala b/src/main/scala/org/ergoplatform/nodeView/mempool/TransactionMembershipProof.scala index 1977571e89..2a93fefb83 100644 --- a/src/main/scala/org/ergoplatform/nodeView/mempool/TransactionMembershipProof.scala +++ b/src/main/scala/org/ergoplatform/nodeView/mempool/TransactionMembershipProof.scala @@ -1,7 +1,7 @@ package org.ergoplatform.nodeView.mempool import io.circe.{Encoder, Json} -import org.ergoplatform.JsonCodecs +import org.ergoplatform.sdk.JsonCodecs import org.ergoplatform.settings.Algos import scorex.crypto.authds.merkle.MerkleProof import scorex.crypto.hash.Digest32 diff --git a/src/main/scala/org/ergoplatform/nodeView/state/ErgoStateContext.scala b/src/main/scala/org/ergoplatform/nodeView/state/ErgoStateContext.scala index b6495e3815..83f302a3b5 100644 --- a/src/main/scala/org/ergoplatform/nodeView/state/ErgoStateContext.scala +++ b/src/main/scala/org/ergoplatform/nodeView/state/ErgoStateContext.scala @@ -18,6 +18,7 @@ import scorex.crypto.authds.ADDigest import scorex.util.ScorexLogging import scorex.util.serialization.{Reader, Writer} import sigmastate.basics.CryptoConstants.EcPointType +import sigmastate.eval.Extensions.ArrayOps import sigmastate.eval.SigmaDsl import special.collection.Coll @@ -83,10 +84,10 @@ class ErgoStateContext(val lastHeaders: Seq[Header], // todo remove from ErgoLikeContext and from ErgoStateContext // State root hash before the last block - override def previousStateDigest: ADDigest = if (sigmaLastHeaders.toArray.nonEmpty) { - ADDigest @@ sigmaLastHeaders.toArray.head.stateRoot.digest.toArray + override def previousStateDigest: Coll[Byte] = if (sigmaLastHeaders.toArray.nonEmpty) { + sigmaLastHeaders.toArray.head.stateRoot.digest } else { - genesisStateDigest + genesisStateDigest.toColl } /* NOHF PROOF: @@ -274,7 +275,7 @@ class ErgoStateContext(val lastHeaders: Seq[Header], }.flatten override def toString: String = - s"ErgoStateContext($currentHeight, ${encoder.encode(previousStateDigest)}, $lastHeaders, $currentParameters)" + s"ErgoStateContext($currentHeight, ${encoder.encode(previousStateDigest.toArray)}, $lastHeaders, $currentParameters)" /** diff --git a/src/main/scala/org/ergoplatform/nodeView/wallet/models/CollectedBoxes.scala b/src/main/scala/org/ergoplatform/nodeView/wallet/models/CollectedBoxes.scala index a8940abe55..58fb14001c 100644 --- a/src/main/scala/org/ergoplatform/nodeView/wallet/models/CollectedBoxes.scala +++ b/src/main/scala/org/ergoplatform/nodeView/wallet/models/CollectedBoxes.scala @@ -3,7 +3,8 @@ package org.ergoplatform.nodeView.wallet.models import io.circe.generic.encoding.DerivedObjectEncoder.deriveEncoder import io.circe.syntax._ import io.circe.{Encoder, Json} -import org.ergoplatform.{ErgoBox, JsonCodecs} +import org.ergoplatform.ErgoBox +import org.ergoplatform.sdk.JsonCodecs /** * Response for requested boxes that contains ErgoBoxes and ChangeBoxes diff --git a/src/main/scala/org/ergoplatform/settings/NodeConfigurationSettings.scala b/src/main/scala/org/ergoplatform/settings/NodeConfigurationSettings.scala index 1bd729b143..d1b60b5518 100644 --- a/src/main/scala/org/ergoplatform/settings/NodeConfigurationSettings.scala +++ b/src/main/scala/org/ergoplatform/settings/NodeConfigurationSettings.scala @@ -42,7 +42,7 @@ trait UtxoSettingsReader { /** * Settings related to headers-chain bootstrapping with NiPoPoWs. See ergo.node.nipopow section for settings description. */ -case class NipopowSettings(nipopowBootstrap: Boolean, p2pNipopows: Int, nipopowSuffix: Int) +case class NipopowSettings(nipopowBootstrap: Boolean, p2pNipopows: Int) /** * Custom settings reader for `NipopowSettings` @@ -51,8 +51,7 @@ trait NipopowSettingsReader { implicit val nipopowSettingsReader: ValueReader[NipopowSettings] = { (cfg, path) => NipopowSettings( cfg.as[Boolean](s"$path.nipopowBootstrap"), - cfg.as[Int](s"$path.p2pNipopows"), - cfg.as[Int](s"$path.nipopowSuffix") + cfg.as[Int](s"$path.p2pNipopows") ) } } diff --git a/src/main/scala/scorex/core/app/Version.scala b/src/main/scala/scorex/core/app/Version.scala index cb029805ad..197683bdb6 100644 --- a/src/main/scala/scorex/core/app/Version.scala +++ b/src/main/scala/scorex/core/app/Version.scala @@ -33,20 +33,12 @@ object Version { val initial: Version = Version(0, 0, 1) - val v4017: Version = Version(4, 0, 17) - - val v4018: Version = Version(4, 0, 18) - - val v4022: Version = Version(4, 0, 22) - - val v4043: Version = Version(4, 0, 43) - val Eip37ForkVersion: Version = Version(4, 0, 100) val JitSoftForkVersion: Version = Version(5, 0, 0) val UtxoSnapsnotActivationVersion: Version = Version(5, 0, 12) - val NipopowActivationVersion: Version = Version(5, 0, 13) // todo: set proper version around release + val NipopowActivationVersion: Version = Version(5, 0, 13) } object ApplicationVersionSerializer extends ErgoSerializer[Version] { diff --git a/src/main/scala/scorex/core/network/NetworkController.scala b/src/main/scala/scorex/core/network/NetworkController.scala index 6eda4d3150..2ac5376e1c 100644 --- a/src/main/scala/scorex/core/network/NetworkController.scala +++ b/src/main/scala/scorex/core/network/NetworkController.scala @@ -310,6 +310,19 @@ class NetworkController(ergoSettings: ErgoSettings, } } + /** + * Check if a given IPv4 or IPv6 address is local. + * @param remote - address to check + * @return true if the address is local, false otherwise + */ + private def checkLocalOnly(remote: InetSocketAddress): Boolean = + if(!networkSettings.localOnly) { // not only accept local + val address = remote.getAddress + address.isSiteLocalAddress || address.isLinkLocalAddress + } else { + false + } + /** * Connect to peer * @@ -320,13 +333,17 @@ class NetworkController(ergoSettings: ErgoSettings, getPeerAddress(peer) match { case Some(remote) => if (connectionForPeerAddress(remote).isEmpty && !unconfirmedConnections.contains(remote)) { - unconfirmedConnections += remote - tcpManager ! Connect( - remoteAddress = remote, - options = Nil, - timeout = Some(networkSettings.connectionTimeout), - pullMode = true - ) + if (checkLocalOnly(remote)) { + log.warn(s"Prevented attempt to connect to local peer $remote. (scorex.network.localOnly is false)") + } else { + unconfirmedConnections += remote + tcpManager ! Connect( + remoteAddress = remote, + options = Nil, + timeout = Some(networkSettings.connectionTimeout), + pullMode = true + ) + } } else { log.warn(s"Connection to peer $remote is already established") } diff --git a/src/main/scala/scorex/core/network/PeerConnectionHandler.scala b/src/main/scala/scorex/core/network/PeerConnectionHandler.scala index ad3f850a9e..80cbbce51b 100644 --- a/src/main/scala/scorex/core/network/PeerConnectionHandler.scala +++ b/src/main/scala/scorex/core/network/PeerConnectionHandler.scala @@ -4,6 +4,7 @@ import akka.actor.{Actor, ActorRef, Cancellable, Props, SupervisorStrategy} import akka.io.Tcp import akka.io.Tcp._ import akka.util.{ByteString, CompactByteString} +import scorex.core.app.Version.Eip37ForkVersion import scorex.core.app.{ScorexContext, Version} import scorex.core.network.NetworkController.ReceivableMessages.{Handshaked, PenalizePeer} import scorex.core.network.PeerConnectionHandler.ReceivableMessages @@ -61,8 +62,8 @@ class PeerConnectionHandler(scorexSettings: ScorexSettings, override def postStop(): Unit = log.info(s"Peer handler to $connectionId destroyed") private def handshaking: Receive = { - handshakeTimeoutCancellableOpt = Some(context.system.scheduler.scheduleOnce(networkSettings.handshakeTimeout) - (self ! HandshakeTimeout)) + handshakeTimeoutCancellableOpt = + Some(context.system.scheduler.scheduleOnce(networkSettings.handshakeTimeout)(self ! HandshakeTimeout)) val hb = HandshakeSerializer.toBytes(createHandshakeMessage()) connection ! Tcp.Write(ByteString(hb)) log.info(s"Handshake sent to $connectionId") @@ -70,11 +71,7 @@ class PeerConnectionHandler(scorexSettings: ScorexSettings, receiveAndHandleHandshake { receivedHandshake => log.info(s"Got a Handshake from $connectionId") - val peerInfo = PeerInfo( - receivedHandshake.peerSpec, - System.currentTimeMillis(), - Some(direction) - ) + val peerInfo = PeerInfo(receivedHandshake.peerSpec, System.currentTimeMillis(), Some(direction)) val peer = ConnectedPeer(connectionDescription.connectionId, self, 0, Some(peerInfo)) selfPeer = Some(peer) @@ -85,17 +82,27 @@ class PeerConnectionHandler(scorexSettings: ScorexSettings, } orElse handshakeTimeout orElse closeCommands } + //ban this peer for the wrong handshake message + //peer will be added to the blacklist and the network controller will send CloseConnection + private def banPeer(): Unit = { + selfPeer.foreach(c => networkControllerRef ! PenalizePeer(c.connectionId.remoteAddress, PenaltyType.PermanentPenalty)) + } + private def receiveAndHandleHandshake(handler: Handshake => Unit): Receive = { case Received(data) => HandshakeSerializer.parseBytesTry(data.toArray) match { case Success(handshake) => - handler(handshake) + if (handshake.peerSpec.protocolVersion < Eip37ForkVersion) { + // peers not suporting EIP-37 hard-fork are stuck on another chain + log.info(s"Peer of version < 4.0.100 sent handshake $handshake") + banPeer() + } else { + handler(handshake) + } case Failure(t) => - log.info(s"Error during parsing a handshake: ${t.getMessage}") - //ban the peer for the wrong handshake message - //peer will be added to the blacklist and the network controller will send CloseConnection - selfPeer.foreach(c => networkControllerRef ! PenalizePeer(c.connectionId.remoteAddress, PenaltyType.PermanentPenalty)) + log.info(s"Error during parsing a handshake: ${t.getMessage}", t) + banPeer() } } @@ -234,7 +241,7 @@ class PeerConnectionHandler(scorexSettings: ScorexSettings, } } - private def createHandshakeMessage() = { + private def createHandshakeMessage(): Handshake = { Handshake( PeerSpec( networkSettings.agentName, diff --git a/src/test/scala/org/ergoplatform/network/PeerFilteringRuleSpecification.scala b/src/test/scala/org/ergoplatform/network/PeerFilteringRuleSpecification.scala index 5ff3499832..028d9af7df 100644 --- a/src/test/scala/org/ergoplatform/network/PeerFilteringRuleSpecification.scala +++ b/src/test/scala/org/ergoplatform/network/PeerFilteringRuleSpecification.scala @@ -25,25 +25,6 @@ class PeerFilteringRuleSpecification extends ErgoPropertyTest { SyncV2Filter.filter(Seq(v1Peer, v2Peer0, v2Peer1, v2Peer2)) shouldBe Seq(v2Peer0, v2Peer1, v2Peer2) } - property("digest mode filter") { - val beforePeer = peerWithVersion(Version(4, 0, 21)) - val afterPeer0 = peerWithVersion(Version(4, 0, 22)) - val afterPeer1 = peerWithVersion(Version(5, 0, 0)) - - DigestModeFilter.filter(Seq(beforePeer, afterPeer0, afterPeer1)) shouldBe Seq(afterPeer0, afterPeer1) - } - - property("broken modifiers filter") { - val inPeer0 = peerWithVersion(Version(4, 0, 17)) - val inPeer1 = peerWithVersion(Version(4, 0, 18)) - val outPeer0 = peerWithVersion(Version(4, 0, 16)) - val outPeer1 = peerWithVersion(Version(4, 0, 19)) - val outPeer2 = peerWithVersion(Version(5, 0, 0)) - - BrokenModifiersFilter.filter(Seq(inPeer0, inPeer1, outPeer0, outPeer1, outPeer2)) shouldBe - Seq(outPeer0, outPeer1, outPeer2) - } - property("utxo set snapshot filter") { val peer0 = peerWithVersion(Version(4, 0, 17)) val peer1 = peerWithVersion(Version(4, 0, 18)) diff --git a/src/test/scala/org/ergoplatform/nodeView/history/extra/ExtraIndexerSpecification.scala b/src/test/scala/org/ergoplatform/nodeView/history/extra/ExtraIndexerSpecification.scala index af1e38d5aa..c26f9cf451 100644 --- a/src/test/scala/org/ergoplatform/nodeView/history/extra/ExtraIndexerSpecification.scala +++ b/src/test/scala/org/ergoplatform/nodeView/history/extra/ExtraIndexerSpecification.scala @@ -38,7 +38,7 @@ class ExtraIndexerSpecification extends ErgoPropertyTest with ExtraIndexerBase w override protected implicit val addressEncoder: ErgoAddressEncoder = initSettings.chainSettings.addressEncoder val nodeSettings: NodeConfigurationSettings = NodeConfigurationSettings(StateType.Utxo, verifyTransactions = true, - -1, UtxoSettings(false, 0, 2), NipopowSettings(false, 1, ChainGenerator.minimalSuffix), mining = false, ChainGenerator.txCostLimit, ChainGenerator.txSizeLimit, useExternalMiner = false, + -1, UtxoSettings(false, 0, 2), NipopowSettings(false, 1), mining = false, ChainGenerator.txCostLimit, ChainGenerator.txSizeLimit, useExternalMiner = false, internalMinersCount = 1, internalMinerPollingInterval = 1.second, miningPubKeyHex = None, offlineGeneration = false, 200, 5.minutes, 100000, 1.minute, mempoolSorting = SortingOption.FeePerByte, rebroadcastCount = 20, 1000000, 100, adProofsSuffixLength = 112 * 1024, extraIndex = false) diff --git a/src/test/scala/org/ergoplatform/settings/ErgoSettingsSpecification.scala b/src/test/scala/org/ergoplatform/settings/ErgoSettingsSpecification.scala index 4da85bf73d..e222ec2d88 100644 --- a/src/test/scala/org/ergoplatform/settings/ErgoSettingsSpecification.scala +++ b/src/test/scala/org/ergoplatform/settings/ErgoSettingsSpecification.scala @@ -25,7 +25,7 @@ class ErgoSettingsSpecification extends ErgoPropertyTest { verifyTransactions = true, 1000, utxoSettings = UtxoSettings(false, 0, 2), - nipopowSettings = NipopowSettings(false, 1, 10), + nipopowSettings = NipopowSettings(false, 1), mining = true, txCostLimit, txSizeLimit, @@ -74,7 +74,7 @@ class ErgoSettingsSpecification extends ErgoPropertyTest { verifyTransactions = true, 12, utxoSettings = UtxoSettings(false, 0, 2), - nipopowSettings = NipopowSettings(false, 1, 10), + nipopowSettings = NipopowSettings(false, 1), mining = true, txCostLimit, txSizeLimit, @@ -116,7 +116,7 @@ class ErgoSettingsSpecification extends ErgoPropertyTest { verifyTransactions = true, 13, utxoSettings = UtxoSettings(false, 0, 2), - nipopowSettings = NipopowSettings(false, 1, 10), + nipopowSettings = NipopowSettings(false, 1), mining = true, txCostLimit, txSizeLimit, diff --git a/src/test/scala/org/ergoplatform/tools/ChainGenerator.scala b/src/test/scala/org/ergoplatform/tools/ChainGenerator.scala index 19233883be..6db85c433d 100644 --- a/src/test/scala/org/ergoplatform/tools/ChainGenerator.scala +++ b/src/test/scala/org/ergoplatform/tools/ChainGenerator.scala @@ -59,7 +59,7 @@ object ChainGenerator extends App with ErgoTestHelpers { val txCostLimit = initSettings.nodeSettings.maxTransactionCost val txSizeLimit = initSettings.nodeSettings.maxTransactionSize val nodeSettings: NodeConfigurationSettings = NodeConfigurationSettings(StateType.Utxo, verifyTransactions = true, - -1, UtxoSettings(false, 0, 2), NipopowSettings(false, 1, minimalSuffix), mining = false, txCostLimit, txSizeLimit, useExternalMiner = false, + -1, UtxoSettings(false, 0, 2), NipopowSettings(false, 1), mining = false, txCostLimit, txSizeLimit, useExternalMiner = false, internalMinersCount = 1, internalMinerPollingInterval = 1.second, miningPubKeyHex = None, offlineGeneration = false, 200, 5.minutes, 100000, 1.minute, mempoolSorting = SortingOption.FeePerByte, rebroadcastCount = 20, 1000000, 100, adProofsSuffixLength = 112*1024, extraIndex = false) diff --git a/src/test/scala/org/ergoplatform/utils/HistoryTestHelpers.scala b/src/test/scala/org/ergoplatform/utils/HistoryTestHelpers.scala index 9c51136d1e..b38d920120 100644 --- a/src/test/scala/org/ergoplatform/utils/HistoryTestHelpers.scala +++ b/src/test/scala/org/ergoplatform/utils/HistoryTestHelpers.scala @@ -44,11 +44,10 @@ trait HistoryTestHelpers extends ErgoPropertyTest { initialDiffOpt: Option[BigInt] = None, genesisIdOpt: Option[ModifierId] = None): ErgoHistory = { - val minimalSuffix = 2 val txCostLimit = initSettings.nodeSettings.maxTransactionCost val txSizeLimit = initSettings.nodeSettings.maxTransactionSize val nodeSettings: NodeConfigurationSettings = NodeConfigurationSettings(stateType, verifyTransactions, blocksToKeep, - UtxoSettings(false, 0, 2), NipopowSettings(false, 1, minimalSuffix), mining = false, txCostLimit, txSizeLimit, useExternalMiner = false, + UtxoSettings(false, 0, 2), NipopowSettings(false, 1), mining = false, txCostLimit, txSizeLimit, useExternalMiner = false, internalMinersCount = 1, internalMinerPollingInterval = 1.second, miningPubKeyHex = None, offlineGeneration = false, 200, 5.minutes, 100000, 1.minute, mempoolSorting = SortingOption.FeePerByte, rebroadcastCount = 200, 1000000, 100, adProofsSuffixLength = 112*1024, extraIndex = false diff --git a/src/test/scala/org/ergoplatform/utils/NodeViewTestConfig.scala b/src/test/scala/org/ergoplatform/utils/NodeViewTestConfig.scala index 0e134f3f8c..8df386a697 100644 --- a/src/test/scala/org/ergoplatform/utils/NodeViewTestConfig.scala +++ b/src/test/scala/org/ergoplatform/utils/NodeViewTestConfig.scala @@ -18,7 +18,7 @@ case class NodeViewTestConfig(stateType: StateType, nodeSettings = defaultSettings.nodeSettings.copy( stateType = stateType, verifyTransactions = verifyTransactions, - nipopowSettings = NipopowSettings(popowBootstrap, 1, 10) + nipopowSettings = NipopowSettings(popowBootstrap, 1) ) ) } diff --git a/src/test/scala/org/ergoplatform/utils/Stubs.scala b/src/test/scala/org/ergoplatform/utils/Stubs.scala index 52d008c916..078719ccb5 100644 --- a/src/test/scala/org/ergoplatform/utils/Stubs.scala +++ b/src/test/scala/org/ergoplatform/utils/Stubs.scala @@ -366,11 +366,10 @@ trait Stubs extends ErgoGenerators with ErgoTestHelpers with ChainGenerator with epochLength: Int = 100000000, useLastEpochs: Int = 10): ErgoHistory = { - val minimalSuffix = 2 val txCostLimit = initSettings.nodeSettings.maxTransactionCost val txSizeLimit = initSettings.nodeSettings.maxTransactionSize val nodeSettings: NodeConfigurationSettings = NodeConfigurationSettings(stateType, verifyTransactions, blocksToKeep, - UtxoSettings(false, 0, 2), NipopowSettings(poPoWBootstrap, 1, minimalSuffix), mining = false, txCostLimit, txSizeLimit, useExternalMiner = false, + UtxoSettings(false, 0, 2), NipopowSettings(poPoWBootstrap, 1), mining = false, txCostLimit, txSizeLimit, useExternalMiner = false, internalMinersCount = 1, internalMinerPollingInterval = 1.second,miningPubKeyHex = None, offlineGeneration = false, 200, 5.minutes, 100000, 1.minute, mempoolSorting = SortingOption.FeePerByte, rebroadcastCount = 200, 1000000, 100, adProofsSuffixLength = 112*1024, extraIndex = false