Skip to content

Commit

Permalink
NODE-2609 Light node implementation (#3874)
Browse files Browse the repository at this point in the history
  • Loading branch information
ivan-mashonskiy authored Oct 6, 2023
1 parent c82177a commit a659a53
Show file tree
Hide file tree
Showing 123 changed files with 3,072 additions and 1,413 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ abstract class DBState extends ScorexLogging {
new RocksDBWriter(
rdb,
settings.blockchainSettings,
settings.dbSettings.copy(maxCacheSize = 1)
settings.dbSettings.copy(maxCacheSize = 1),
settings.enableLightMode
)

AddressScheme.current = new AddressScheme { override val chainId: Byte = 'W' }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ object RollbackBenchmark extends ScorexLogging {
val settings = Application.loadApplicationConfig(Some(new File(args(0))))
val rdb = RDB.open(settings.dbSettings)
val time = new NTP(settings.ntpServer)
val rocksDBWriter = new RocksDBWriter(rdb, settings.blockchainSettings, settings.dbSettings)
val rocksDBWriter = new RocksDBWriter(rdb, settings.blockchainSettings, settings.dbSettings, settings.enableLightMode)

val issuer = KeyPair(new Array[Byte](32))

Expand Down Expand Up @@ -80,6 +80,7 @@ object RollbackBenchmark extends ScorexLogging {
0,
None,
genesisBlock.header.generationSignature,
ByteStr.empty,
genesisBlock
)

Expand All @@ -103,7 +104,7 @@ object RollbackBenchmark extends ScorexLogging {
val nextSnapshot = StateSnapshot.build(rocksDBWriter, portfolios2.toMap).explicitGet()

log.info("Appending next block")
rocksDBWriter.append(nextSnapshot, 0, 0, None, ByteStr.empty, nextBlock)
rocksDBWriter.append(nextSnapshot, 0, 0, None, ByteStr.empty, ByteStr.empty, nextBlock)

log.info("Rolling back")
val start = System.nanoTime()
Expand Down
10 changes: 5 additions & 5 deletions benchmark/src/test/scala/com/wavesplatform/state/BaseState.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package com.wavesplatform.state

import java.io.File
import java.nio.file.Files

import com.typesafe.config.ConfigFactory
import com.wavesplatform.account.KeyPair
import com.wavesplatform.block.Block
Expand Down Expand Up @@ -76,11 +75,12 @@ trait BaseState {
.block

private def append(prev: Option[Block], next: Block): Unit = {
val preconditionSnapshot =
BlockDiffer.fromBlock(state, prev, next, MiningConstraint.Unlimited, next.header.generationSignature)
val differResult =
BlockDiffer
.fromBlock(state, prev, next, None, MiningConstraint.Unlimited, next.header.generationSignature)
.explicitGet()
.snapshot
state.append(preconditionSnapshot, 0, 0, None, next.header.generationSignature, next)

state.append(differResult.snapshot, 0, 0, None, next.header.generationSignature, differResult.computedStateHash, next)
}

def applyBlock(b: Block): Unit = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ object RocksDBWriterBenchmark {
RDB.open(wavesSettings.dbSettings)
}

val db = new RocksDBWriter(rawDB, wavesSettings.blockchainSettings, wavesSettings.dbSettings)
val db = new RocksDBWriter(rawDB, wavesSettings.blockchainSettings, wavesSettings.dbSettings, wavesSettings.enableLightMode)

def loadBlockInfoAt(height: Int): Option[(BlockMeta, Seq[(TxMeta, Transaction)])] =
loadBlockMetaAt(height).map { meta =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ object WavesEnvironmentBenchmark {
}

val environment: Environment[Id] = {
val state = new RocksDBWriter(rdb, wavesSettings.blockchainSettings, wavesSettings.dbSettings)
val state = new RocksDBWriter(rdb, wavesSettings.blockchainSettings, wavesSettings.dbSettings, wavesSettings.enableLightMode)
new WavesEnvironment(
AddressScheme.current.chainId,
Coeval.raiseError(new NotImplementedError("`tx` is not implemented")),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -860,6 +860,7 @@ class BlockchainUpdatesSpec extends FreeSpec with WithBUDomain with ScalaFutures
xtnBuybackAddress,
d.blockchain
)

append.stateUpdate.get.balances shouldBe Seq(
PBBalanceUpdate(
challengingMiner.toAddress.toByteString,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ object BaseTargetChecker {
blockchainUpdater.isFeatureActivated(BlockchainFeatures.TransactionStateSnapshot)
)
.explicitGet()
blockchainUpdater.processBlock(genesisBlock, genesisBlock.header.generationSignature)
blockchainUpdater.processBlock(genesisBlock, genesisBlock.header.generationSignature, None)

NodeConfigs.Default.map(_.withFallback(sharedConfig)).collect {
case cfg if cfg.as[Boolean]("waves.miner.enable") =>
Expand Down
2 changes: 2 additions & 0 deletions node-it/src/test/scala/com/wavesplatform/it/NodeConfigs.scala
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ object NodeConfigs {
s"waves.blockchain.custom.functionality.min-asset-info-update-interval = $blocks"

val nonMiner: String = "waves.miner.enable = no"

val lightNode: String = "waves.enable-light-mode = true"
}

}
24 changes: 21 additions & 3 deletions node-it/src/test/scala/com/wavesplatform/it/api/model.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import com.wavesplatform.common.state.ByteStr
import com.wavesplatform.state.DataEntry
import com.wavesplatform.transaction.assets.exchange.AssetPair
import com.wavesplatform.transaction.transfer.MassTransferTransaction.Transfer
import io.grpc.{Metadata, Status => GrpcStatus}
import play.api.libs.json._
import io.grpc.{Metadata, Status as GrpcStatus}
import play.api.libs.json.*

import scala.util.{Failure, Success}

Expand Down Expand Up @@ -802,10 +802,11 @@ case class Block(
reward: Option[Long],
desiredReward: Option[Long],
vrf: Option[String],
challengedHeader: Option[ChallengedBlockHeader],
version: Option[Byte] = None
)
object Block {
import PublicKey._
import PublicKey.*

implicit val blockFormat: Format[Block] = Format(
Reads(jsv =>
Expand All @@ -830,6 +831,7 @@ object Block {
baseTarget <- (jsv \ "nxt-consensus" \ "base-target").validateOpt[Int]
transactionsRoot <- (jsv \ "transactionsRoot").validateOpt[String]
vrf <- (jsv \ "VRF").validateOpt[String]
challengedHeader <- (jsv \ "challengedHeader").validateOpt[ChallengedBlockHeader]
} yield Block(
id,
signature,
Expand All @@ -850,6 +852,7 @@ object Block {
reward,
desiredReward,
vrf,
challengedHeader,
version
)
),
Expand All @@ -873,6 +876,7 @@ case class BlockHeader(
desiredReward: Option[Long],
totalFee: Long,
vrf: Option[String],
challengedHeader: Option[ChallengedBlockHeader],
version: Option[Byte] = None
)
object BlockHeader {
Expand All @@ -895,6 +899,7 @@ object BlockHeader {
baseTarget <- (jsv \ "nxt-consensus" \ "base-target").validateOpt[Int]
transactionsRoot <- (jsv \ "transactionsRoot").validateOpt[String]
vrf <- (jsv \ "VRF").validateOpt[String]
challengedHeader <- (jsv \ "challengedHeader").validateOpt[ChallengedBlockHeader]
} yield BlockHeader(
id,
signature,
Expand All @@ -911,13 +916,26 @@ object BlockHeader {
desiredReward,
totalFee,
vrf,
challengedHeader,
version
)
),
Json.writes[BlockHeader]
)
}

case class ChallengedBlockHeader(
headerSignature: String,
features: Set[Short],
generator: String,
generatorPublicKey: String,
desiredReward: Long,
stateHash: Option[String]
)
object ChallengedBlockHeader {
implicit val challengedBlockHeaderFormat: Format[ChallengedBlockHeader] = Json.format
}

case class GenerationSignatureResponse(generationSignature: String)
object GenerationSignatureResponse {
implicit val generationSignatureResponseFormat: Format[GenerationSignatureResponse] = Json.format
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
package com.wavesplatform.it.sync

import com.typesafe.config.Config
import com.wavesplatform.account.KeyPair
import com.wavesplatform.block.Block
import com.wavesplatform.common.state.ByteStr
import com.wavesplatform.common.utils.EitherExt2
import com.wavesplatform.consensus.FairPoSCalculator
import com.wavesplatform.{TestValues, crypto}
import com.wavesplatform.features.BlockchainFeatures
import com.wavesplatform.it.api.Block as ApiBlock
import com.wavesplatform.it.{BaseFunSuite, Node, NodeConfigs, TransferSending}
import com.wavesplatform.it.api.AsyncNetworkApi.NodeAsyncNetworkApi
import com.wavesplatform.it.api.SyncHttpApi.*
import com.wavesplatform.lang.directives.values.V8
import com.wavesplatform.lang.v1.compiler.TestCompiler
import com.wavesplatform.network.RawBytes
import com.wavesplatform.transaction.Asset.Waves
import com.wavesplatform.transaction.assets.exchange.OrderType
import com.wavesplatform.transaction.transfer.MassTransferTransaction.ParsedTransfer
import com.wavesplatform.transaction.{Transaction, TxHelpers, TxNonNegativeAmount}

import scala.concurrent.Await
import scala.concurrent.duration.*

class BlockChallengeSuite extends BaseFunSuite with TransferSending {
override def nodeConfigs: Seq[Config] =
NodeConfigs.newBuilder
.overrideBase(_.quorum(4))
.overrideBase(_.minAssetInfoUpdateInterval(0))
.overrideBase(
_.preactivatedFeatures(
BlockchainFeatures.SynchronousCalls.id.toInt -> 0,
BlockchainFeatures.RideV6.id.toInt -> 0,
BlockchainFeatures.ConsensusImprovements.id.toInt -> 0,
BlockchainFeatures.TransactionStateSnapshot.id.toInt -> 0
)
)
.withDefault(1)
.withSpecial(1, _.lightNode)
.withSpecial(2, _.nonMiner)
.buildNonConflicting()

test("NODE-1167, NODE-1174, NODE-1175. All nodes should receive and apply block with challenge") {
val challenger = nodes.head
val sender = nodes(1)
val malicious = nodes.last

val height = challenger.height
val lastBlock = challenger.blockAt(height)

val elidedTransfer = TxHelpers.transfer(malicious.keyPair, amount = malicious.balance(malicious.address).balance + 200000000)
val txs = createTxs(challenger, sender, nodes(2)) :+ elidedTransfer
val invalidBlock = createBlockWithInvalidStateHash(lastBlock, height, malicious.keyPair, txs)
waitForBlockTime(invalidBlock)
Await.ready(challenger.sendByNetwork(RawBytes.fromBlock(invalidBlock)), 2.minutes)

txs.foreach { tx =>
val txInfo = nodes.waitForTransaction(tx.id().toString)
val expectedStatus = if (tx.id() == elidedTransfer.id()) "elided" else "succeeded"
txInfo.applicationStatus shouldBe Some(expectedStatus)
}

val challengingIds = nodes.map { node =>
val challengingBlock = node.blockAt(height + 1)
checkChallengingBlock(challengingBlock, invalidBlock, challenger.address, txs)
challengingBlock.id
}

challengingIds.toSet.size shouldBe 1
}

private def checkChallengingBlock(challengingBlock: ApiBlock, challengedBlock: Block, challengerAddress: String, txs: Seq[Transaction]) = {
challengingBlock.challengedHeader shouldBe defined
val challengedHeader = challengingBlock.challengedHeader.get
challengedHeader.headerSignature shouldBe challengedBlock.signature.toString
challengedHeader.features shouldBe challengedBlock.header.featureVotes.toSet
challengedHeader.desiredReward shouldBe challengedBlock.header.rewardVote
challengedHeader.stateHash shouldBe challengedBlock.header.stateHash.map(_.toString)
challengedHeader.generator shouldBe challengedBlock.header.generator.toAddress.toString
challengedHeader.generatorPublicKey shouldBe challengedBlock.header.generator.toString
challengingBlock.generator shouldBe challengerAddress
challengingBlock.transactions.map(_.id).toSet shouldBe txs.map(_.id().toString).toSet
}

private def createBlockWithInvalidStateHash(lastBlock: ApiBlock, height: Int, signer: KeyPair, txs: Seq[Transaction]): Block = {
val lastBlockVrfOrGenSig = lastBlock.vrf.orElse(lastBlock.generationSignature).map(str => ByteStr.decodeBase58(str).get).get.arr
val genSig: ByteStr = crypto.signVRF(signer.privateKey, lastBlockVrfOrGenSig)

val hitSource =
crypto.verifyVRF(genSig, lastBlockVrfOrGenSig, signer.publicKey).explicitGet()

val posCalculator = FairPoSCalculator.V1
val version = 5.toByte

val validBlockDelay: Long = posCalculator
.calculateDelay(
hit(hitSource.arr),
lastBlock.baseTarget.get,
nodes.head.accountBalances(signer.toAddress.toString)._2
)

val baseTarget: Long = posCalculator
.calculateBaseTarget(
10,
height,
lastBlock.baseTarget.get,
lastBlock.timestamp,
None,
lastBlock.timestamp + validBlockDelay
)

Block
.buildAndSign(
version = version,
timestamp = lastBlock.timestamp + validBlockDelay,
reference = ByteStr.decodeBase58(lastBlock.id).get,
baseTarget = baseTarget,
generationSignature = genSig,
txs = txs,
signer = signer,
featureVotes = Seq(22),
rewardVote = 1000000000L,
stateHash = Some(ByteStr.fill(32)(1)),
challengedHeader = None
)
.explicitGet()
}

private def hit(generatorSignature: Array[Byte]): BigInt = BigInt(1, generatorSignature.take(8).reverse)

private def waitForBlockTime(block: Block): Unit = {
val timeout = block.header.timestamp - System.currentTimeMillis()

if (timeout > 0) Thread.sleep(timeout)
}

private def createTxs(challenger: Node, sender: Node, dApp: Node): Seq[Transaction] = {
val dAppScript = TestCompiler(V8).compileContract(
s"""
|@Callable(i)
|func foo() = []
|""".stripMargin
)
val assetScript = TestCompiler(V8).compileAsset("true")

val issue = TxHelpers.issue(sender.keyPair)
val issueSmart = TxHelpers.issue(sender.keyPair, name = "smart", script = Some(assetScript))
val lease = TxHelpers.lease(sender.keyPair)

Seq(
issue,
issueSmart,
TxHelpers.setScript(dApp.keyPair, dAppScript),
TxHelpers.burn(issue.asset, sender = sender.keyPair),
TxHelpers.createAlias("alias", sender.keyPair),
TxHelpers.dataSingle(sender.keyPair),
TxHelpers.exchange(
TxHelpers.order(OrderType.BUY, issue.asset, Waves, sender = sender.keyPair, matcher = sender.keyPair),
TxHelpers.order(OrderType.SELL, issue.asset, Waves, sender = sender.keyPair, matcher = sender.keyPair),
sender.keyPair
),
TxHelpers.invoke(dApp.keyPair.toAddress, Some("foo"), invoker = sender.keyPair),
lease,
TxHelpers.leaseCancel(lease.id(), sender.keyPair),
TxHelpers
.massTransfer(
sender.keyPair,
Seq(ParsedTransfer(challenger.keyPair.toAddress, TxNonNegativeAmount.unsafeFrom(1))),
fee = TestValues.fee
),
TxHelpers.reissue(issue.asset, sender.keyPair),
TxHelpers.setAssetScript(sender.keyPair, issueSmart.asset, assetScript, fee = 200000000),
TxHelpers.transfer(sender.keyPair, challenger.keyPair.toAddress, 1),
TxHelpers.sponsor(issue.asset, sender = sender.keyPair),
TxHelpers.updateAssetInfo(issue.assetId, sender = sender.keyPair)
)
}
}
Loading

0 comments on commit a659a53

Please sign in to comment.