Skip to content

Commit

Permalink
Support a chain registry:
Browse files Browse the repository at this point in the history
- Consensus Client works only if it is approved on a chain registry;
- Approvals and rejections are detected during work;
- Migrated settings from ficus to pureconfig;
- New tests.
  • Loading branch information
vsuharnikov committed Nov 22, 2024
1 parent e51a320 commit 6f265f0
Show file tree
Hide file tree
Showing 24 changed files with 242 additions and 68 deletions.
18 changes: 11 additions & 7 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,17 @@ inScope(Global)(
name := "consensus-client"
maintainer := "Units Network Team"

libraryDependencies ++= Seq(
"com.wavesplatform" % "node-testkit" % "1.5.8" % Test,
"com.wavesplatform" % "node" % "1.5.8" % Provided,
"com.softwaremill.sttp.client3" % "core_2.13" % "3.10.1",
"com.softwaremill.sttp.client3" %% "play-json" % "3.10.1",
"com.github.jwt-scala" %% "jwt-play-json" % "10.0.1"
)
libraryDependencies ++= {
val node = "1.5-3977-SNAPSHOT"
val sttp = "3.10.1"
Seq(
"com.wavesplatform" % "node-testkit" % node % Test,
"com.wavesplatform" % "node" % node % Provided,
"com.softwaremill.sttp.client3" % "core_2.13" % sttp,
"com.softwaremill.sttp.client3" %% "play-json" % sttp,
"com.github.jwt-scala" %% "jwt-play-json" % "10.0.1"
)
}

Compile / packageDoc / publishArtifact := false

Expand Down
1 change: 1 addition & 0 deletions consensus-client-it/src/test/scala/units/Accounts.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import units.eth.EthAddress
import java.nio.charset.StandardCharsets

trait Accounts {
val chainRegistryAccount: KeyPair = mkKeyPair("devnet registry", 0)
val chainContractAccount: KeyPair = mkKeyPair("devnet cc", 0)

val miner11Account = mkKeyPair("devnet-1", 0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ trait BaseDockerTestSuite
}

protected def setupChain(): Unit = {
log.info("Approve chain on registry")
waves1.api.broadcast(ChainRegistry.approve())

log.info("Set script")
waves1.api.broadcastAndWait(ChainContract.setScript())

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package units

class RegistryDockerTestSuite extends BaseDockerTestSuite {
"Approved, rejected, approved - mining works when approved" in {
step(s"Wait miner 1 (${miner11Account.toAddress}) forge at least one block")
chainContract.waitForHeight(1L)

step("Broadcast a reject transaction")
val rejectTxnHeight = waves1.api.broadcastAndWait(ChainRegistry.reject()).height

step("Wait a rejection height")
waves1.api.waitForHeight(rejectTxnHeight + 1)
val lastElBlock1 = chainContract.getLastBlockMeta(0L).value

step("Expect no mining")
waves1.api.waitForHeight(rejectTxnHeight + 2)

val lastElBlock2 = chainContract.getLastBlockMeta(0L).value
withClue("Same block - no mining: ") {
lastElBlock2.hash shouldBe lastElBlock1.hash
}

val approveTxnHeight = waves1.api.broadcastAndWait(ChainRegistry.approve()).height
step("Wait an approval height")
waves1.api.waitForHeight(approveTxnHeight + 1)

step("Mining working")
chainContract.waitForHeight(lastElBlock2.height + 1)
}
}
2 changes: 1 addition & 1 deletion docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
ARG baseImage=ghcr.io/wavesplatform/waves:1.5.8-1
ARG baseImage=ghcr.io/wavesplatform/waves:units-registry
FROM $baseImage
COPY target /tmp/
RUN tar zxvf /tmp/consensus-client.tgz -C $WAVES_INSTALL_PATH --strip-components=1
9 changes: 9 additions & 0 deletions local-network/configs/wavesnode/genesis-template.conf
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,15 @@ genesis-generator {
# Address: 3FVp6fUSmVehs7Q3uJBXe5kWtpz57f7azzA
{ seed-text = "devnet dao", nonce = 0, amount = 100000000 }

# Chain registry
# Seed text: devnet registry
# Nonce: 0
# Seed: 3oujRTe9jVvnMHcKe9vGG
# Private key: 3kqzGQiNy5qTKEGDzCxh6Kk8LLXW5Lh22pLmZvFNqDm1
# Public key: HRHrDistEQkJJ7dBxBBzxfSsbN8oEF8xt3FeUHLNBYfX
# Address: 3FibjK5ZAXFfJf9J3gDsheia88Vz2itLPu1
{ seed-text = "devnet registry", nonce = 0, amount = 100000000 }

# Chain contract
# Seed text: devnet cc
# Nonce: 0
Expand Down
3 changes: 3 additions & 0 deletions local-network/configs/wavesnode/waves.conf
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,17 @@ waves {
dao-address = "3FVGizGxjdi8m4e8WKjbwtzg4rdiiUGkLCu"
xtn-buyback-address = "3FZQMZsyypqDUk2r5katP1AUChe7Uzc9dC4"
xtn-buyback-reward-period = 20
units-registry-address = "3FibjK5ZAXFfJf9J3gDsheia88Vz2itLPu1"
}

rewards {
term = 100000
term-after-capped-reward-feature = 5
initial = 600000000
min-increment = 50000000
voting-interval = 1
}

include "genesis.conf"
include "/etc/it/genesis.conf"
}
Expand Down
6 changes: 6 additions & 0 deletions local-network/deploy/deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,14 @@
log.info(f"Wait for {min_peers} peers, now: {r}")
sleep(2)

log.info(f"Registry address: {network.cl_registry.oracleAddress}")
log.info(f"Chain contract address: {network.cl_chain_contract.oracleAddress}")

log.info("Approve chain contract on registry")
network.cl_registry.storeData(
f"unit_{network.cl_chain_contract.oracleAddress}_approved", "boolean", True
)

script_info = network.cl_chain_contract.oracleAcc.scriptInfo()
if script_info["script"] is None:
log.info("Set chain contract script")
Expand Down
6 changes: 5 additions & 1 deletion local-network/deploy/local/network.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,13 @@ class Miner:

class ExtendedNetwork(Network):
@cached_property
def cl_dao(self) -> ChainContract:
def cl_dao(self) -> pw.Address:
return pw.Address(seed="devnet dao", nonce=0)

@cached_property
def cl_registry(self) -> pw.Oracle:
return pw.Oracle(seed="devnet registry")

@cached_property
def cl_chain_contract(self) -> ChainContract:
return ChainContract(seed="devnet cc", nonce=0)
Expand Down
7 changes: 0 additions & 7 deletions src/main/scala/units/ClientConfig.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@ package units
import com.wavesplatform.account.Address
import com.wavesplatform.common.utils.EitherExt2
import com.wavesplatform.settings.*
import net.ceedubs.ficus.Ficus.*
import net.ceedubs.ficus.readers.ArbitraryTypeReader.arbitraryTypeValueReader
import net.ceedubs.ficus.readers.{Generated, ValueReader}
import units.client.JsonRpcClient

import scala.concurrent.duration.FiniteDuration
Expand All @@ -29,7 +26,3 @@ case class ClientConfig(
apiRequestRetryWaitTime = apiRequestRetryWaitTime
)
}

object ClientConfig {
implicit val valueReader: Generated[ValueReader[ClientConfig]] = arbitraryTypeValueReader
}
13 changes: 10 additions & 3 deletions src/main/scala/units/ConsensusClient.scala
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
package units

import com.typesafe.config.Config
import com.typesafe.scalalogging.StrictLogging
import com.wavesplatform.block.{Block, MicroBlock}
import com.wavesplatform.common.state.ByteStr
import com.wavesplatform.common.utils.EitherExt2
import com.wavesplatform.events.BlockchainUpdateTriggers
import com.wavesplatform.extensions.{Extension, Context as ExtensionContext}
import com.wavesplatform.state.{Blockchain, StateSnapshot}
import io.netty.channel.group.DefaultChannelGroup
import monix.execution.{CancelableFuture, Scheduler}
import net.ceedubs.ficus.Ficus.*
import pureconfig.ConfigSource
import pureconfig.generic.auto.*
import units.ConsensusClient.ChainHandler
import units.client.engine.EngineApiClient
import units.network.*
Expand All @@ -28,15 +31,18 @@ class ConsensusClient(context: ExtensionContext) extends StrictLogging with Exte
private val chainHandlers: Seq[ChainHandler] = {
val defaultConfig = context.settings.config.getConfig("units.defaults")

def load(cfg: Config): ClientConfig =
ConfigSource.fromConfig(cfg.withFallback(defaultConfig).resolve()).loadOrThrow[ClientConfig]

val legacyChainConfig =
Try(context.settings.config.getConfig("waves.l2")).toOption.map(_.withFallback(defaultConfig).as[ClientConfig]).tapEach { _ =>
Try(context.settings.config.getConfig("waves.l2")).toOption.map(load).tapEach { _ =>
logger.info("Consensus client settings at waves.l2 path have been deprecated, please update your config file")
}

val newChainConfigs = context.settings.config
.getConfigList("units.chains")
.asScala
.map(cfg => cfg.withFallback(defaultConfig).resolve().as[ClientConfig])
.map(load)

val allChainConfigs = legacyChainConfig ++ newChainConfigs

Expand Down Expand Up @@ -93,6 +99,7 @@ object ConsensusClient {
config,
context.time,
context.wallet,
context.settings.blockchainSettings.functionalitySettings.unitsRegistryAddressParsed.explicitGet(),
blockObserver.loadBlock,
context.broadcastTransaction,
eluScheduler,
Expand Down
57 changes: 32 additions & 25 deletions src/main/scala/units/ELUpdater.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import com.wavesplatform.crypto
import com.wavesplatform.lang.ValidationError
import com.wavesplatform.lang.v1.compiler.Terms.FUNCTION_CALL
import com.wavesplatform.network.ChannelGroupExt
import com.wavesplatform.state.Blockchain
import com.wavesplatform.state.diffs.FeeValidation.{FeeConstants, FeeUnit, ScriptExtraFee}
import com.wavesplatform.state.diffs.TransactionDiffer.TransactionValidationError
import com.wavesplatform.state.{Blockchain, BooleanDataEntry}
import com.wavesplatform.transaction.TxValidationError.InvokeRejectError
import com.wavesplatform.transaction.smart.InvokeScriptTransaction
import com.wavesplatform.transaction.smart.script.trace.TracedResult
Expand Down Expand Up @@ -50,6 +50,7 @@ class ELUpdater(
config: ClientConfig,
time: Time,
wallet: Wallet,
registryAddress: Option[Address],
requestBlockFromPeers: BlockHash => CancelableFuture[BlockWithChannel],
broadcastTx: Transaction => TracedResult[ValidationError, Boolean],
scheduler: Scheduler,
Expand Down Expand Up @@ -147,23 +148,6 @@ class ELUpdater(
}
}

private def cleanPriorityPool(): Unit = {
// A transaction moves to priority pool when a new key block references one of the previous micro blocks.
// When we add a new fresh transaction (extendMainChain) to UTX, it is validated against a stale transaction changes.
// Removing here, because we have these transactions in PP after the onProcessBlock trigger
utx.getPriorityPool.foreach { pp =>
val staleTxs = pp.priorityTransactions.filter {
case tx: InvokeScriptTransaction => tx.dApp == contractAddress
case _ => false
}

if (staleTxs.nonEmpty) {
logger.debug(s"Removing stale transactions: ${staleTxs.map(_.id()).mkString(", ")}")
utx.removeAll(staleTxs)
}
}
}

private def callContract(fc: FUNCTION_CALL, blockData: EcBlock, invoker: KeyPair): JobResult[Unit] = {
val extraFee = if (blockchain.hasPaidVerifier(invoker.toAddress)) ScriptExtraFee else 0

Expand All @@ -180,7 +164,6 @@ class ELUpdater(
blockchain.settings.addressSchemeCharacter.toByte
).signWith(invoker.privateKey)
logger.info(s"Invoking $contractAddress '${fc.function.funcName}' for block ${blockData.hash}->${blockData.parentHash}, txId=${tx.id()}")
cleanPriorityPool()

broadcastTx(tx).resultE match {
case Right(true) => Either.unit
Expand Down Expand Up @@ -250,7 +233,7 @@ class ELUpdater(
)
}
case Working(_, _, _, _, _, _: Mining | _: FollowingChain, _, _) =>
// a new epoch started and we trying to apply a previous epoch payload:
// a new epoch started, and we're trying to apply a previous epoch payload:
// Mining - we mine again
// FollowingChain - we validate
case other => logger.debug(s"Unexpected state $other attempting to finish building $payloadId")
Expand Down Expand Up @@ -485,10 +468,32 @@ class ELUpdater(
}

private def handleConsensusLayerChanged(): Unit = {
state match {
case Starting => updateStartingState()
case w: Working[ChainStatus] => updateWorkingState(w)
case other => logger.debug(s"Unprocessed state: $other")
def stopMining(): Unit = setState("26", Starting)

isChainEnabled match {
case Left(e) =>
logger.warn(s"$contractAddress chain is disabled: $e")
stopMining()

case Right(false) =>
logger.warn(s"$contractAddress chain is disabled")
stopMining()

case Right(true) =>
state match {
case Starting => updateStartingState()
case w: Working[ChainStatus] => updateWorkingState(w)
case other => logger.debug(s"Unprocessed state: $other")
}
}
}

private def isChainEnabled: Either[String, Boolean] = registryAddress.fold(true.asRight[String]) { registryAddress =>
val key = registryKey(contractAddress)
blockchain.accountData(registryAddress, key) match {
case Some(BooleanDataEntry(_, isEnabled)) => isEnabled.asRight
case None => false.asRight
case Some(x) => s"Expected '$key' to be a boolean, got: $x".asLeft
}
}

Expand Down Expand Up @@ -1572,7 +1577,7 @@ object ELUpdater {

case class ChainSwitchInfo(prevChainId: Long, referenceBlock: ContractBlock)

/** We haven't received a EC-block {@link missedBlock} of a previous epoch when started a mining on a new epoch. We can return to the main chain, if
/** We haven't received an EC-block [[missedBlock]] of a previous epoch when started a mining on a new epoch. We can return to the main chain, if we
* get a missed EC-block.
*/
case class ReturnToMainChainInfo(missedBlock: ContractBlock, missedBlockParent: EcBlock, chainId: Long)
Expand All @@ -1599,4 +1604,6 @@ object ELUpdater {
val msg = hitSource.arr ++ HexBytesConverter.toBytes(parentHash)
HexBytesConverter.toHex(crypto.secureHash(msg))
}

def registryKey(chainContract: Address): String = s"unit_${chainContract}_approved"
}
7 changes: 3 additions & 4 deletions src/main/scala/units/client/JsonRpcClient.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@ package units.client

import cats.Id
import cats.syntax.either.*
import net.ceedubs.ficus.Ficus.*
import net.ceedubs.ficus.readers.ArbitraryTypeReader.arbitraryTypeValueReader
import net.ceedubs.ficus.readers.{Generated, ValueReader}
import play.api.libs.json.{JsError, JsValue, Reads, Writes}
import pureconfig.*
import pureconfig.generic.semiauto.*
import sttp.client3.*
import sttp.client3.playJson.*
import units.ClientError
Expand Down Expand Up @@ -79,7 +78,7 @@ object JsonRpcClient {
}

object Config {
implicit val configValueReader: Generated[ValueReader[Config]] = arbitraryTypeValueReader
implicit val configValueReader: ConfigReader[Config] = deriveReader
}

def newRequestId: Int = ThreadLocalRandom.current().nextInt(10000, 100000)
Expand Down
Loading

0 comments on commit 6f265f0

Please sign in to comment.