Skip to content

Commit

Permalink
Fix wrong inputs due to mutable contract outputs
Browse files Browse the repository at this point in the history
  • Loading branch information
tdroxler committed Dec 10, 2024
1 parent 635234a commit be27313
Show file tree
Hide file tree
Showing 7 changed files with 222 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import org.alephium.protocol.model.BlockHash
@SuppressWarnings(Array("org.wartremover.warts.AnyVal"))
object Migrations extends StrictLogging {

val latestVersion: MigrationVersion = MigrationVersion(4)
val latestVersion: MigrationVersion = MigrationVersion(5)

def migration1(implicit ec: ExecutionContext): DBActionAll[Unit] = {
// We retrigger the download of fungible and non-fungible tokens' metadata that have sub-category
Expand Down Expand Up @@ -90,7 +90,9 @@ object Migrations extends StrictLogging {
private def migrations(implicit ec: ExecutionContext): Seq[DBActionAll[Unit]] = Seq(
migration1,
migration2,
migration3
migration3,
migration4,
migration5
)

def backgroundCoinbaseMigration()(implicit
Expand Down Expand Up @@ -148,6 +150,66 @@ object Migrations extends StrictLogging {
"""
}

/*
* Empty transaction due to the coinbase migration being disabled.
*/
def migration4: DBActionAll[Unit] = DBIOAction.successful(())

/*
* Update the inputs with the correct output amount from the mutable contract outputs.
*/
def migration5(implicit ec: ExecutionContext): DBActionAll[Unit] = {
for {
i1 <- sqlu"""
UPDATE inputs i
SET output_ref_tx_hash = o2.tx_hash, output_ref_amount = o2.amount, output_ref_tokens = o2.tokens
FROM outputs o1
JOIN outputs o2
ON
o1.address = o2.address
AND o1.key = o2.key
AND o1.tx_hash = o2.tx_hash
AND o1.main_chain = FALSE
AND o2.main_chain = TRUE
AND o1.amount <> o2.amount
AND o1.block_hash <> o2.block_hash
AND o1.fixed_output = FALSE
AND o2.fixed_output = FALSE
WHERE
i.output_ref_address = o1.address
AND i.output_ref_key = o1.key
AND i.output_ref_tx_hash = o1.tx_hash
AND i.main_chain = TRUE
AND i.output_ref_amount <> o2.amount;
"""
i2 <- sqlu"""
-- Update inputs with the correct output amount from main_chain = FALSE
UPDATE inputs i
SET output_ref_tx_hash = o1.tx_hash, output_ref_amount = o1.amount, output_ref_tokens = o1.tokens
FROM outputs o1
JOIN outputs o2
ON
o1.address = o2.address
AND o1.key = o2.key
AND o1.tx_hash = o2.tx_hash
AND o1.main_chain = FALSE
AND o2.main_chain = TRUE
AND o1.amount <> o2.amount
AND o1.block_hash <> o2.block_hash
AND o1.fixed_output = FALSE
AND o2.fixed_output = FALSE
WHERE
i.output_ref_address = o1.address
AND i.output_ref_key = o1.key
AND i.output_ref_tx_hash = o1.tx_hash
AND i.main_chain = FALSE
AND i.output_ref_amount <> o1.amount;
"""
} yield {
logger.info(s"Updated ${i1 + i2} inputs with the correct output amount")
}
}

def migrationsQuery(
versionOpt: Option[MigrationVersion]
)(implicit ec: ExecutionContext): DBActionAll[Unit] = {
Expand Down Expand Up @@ -183,7 +245,7 @@ object Migrations extends StrictLogging {
case Some(MigrationVersion(current)) if current > latestVersion.version =>
throw new Exception("Incompatible migration versions, please reset your database")
case Some(MigrationVersion(current)) =>
if (current <= 3) {
if (current <= 5) {
logger.info(s"Background migrations needed, but will be done in a future release")
/*
* The coinbase migration is heavy and we had some performance issues due to the increase of users.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,22 @@ object InputUpdateQueries {
(Address, Option[ArraySeq[Token]], TransactionId, BlockHash, TimeStamp, Int, Boolean)

def updateInputs()(implicit ec: ExecutionContext): DBActionWT[Unit] = {
(for {
fixed <- updateFixedInputs()
mutableContracts <- updateMutableContractInputs()
contracts <- updateContractInputs()
_ <- internalUpdates(fixed ++ contracts ++ mutableContracts)
} yield {}).transactionally
}

/*
* Updates the inputs table with information from fixed (immutable) outputs.
*
* This function handles cases where the output is guaranteed to be immutable, meaning:
* - Each output reference key (`output_ref_key`) is unique.
* - The output's amount, tokens, and address will always remain the same for a given key.
*/
private def updateFixedInputs() = {
sql"""
UPDATE inputs
SET
Expand All @@ -46,11 +62,65 @@ object InputUpdateQueries {
FROM outputs
WHERE inputs.output_ref_key = outputs.key
AND inputs.output_ref_amount IS NULL
AND inputs.contract_input = false
AND outputs.fixed_output = true
RETURNING outputs.address, outputs.tokens, inputs.tx_hash, inputs.block_hash, inputs.block_timestamp, inputs.tx_order, inputs.main_chain
"""
.as[UpdateReturn]
}

/*
* Updates the inputs table with information from mutable contract outputs.
*
* This function handles cases where:
* - The referenced contract outputs are mutable, meaning their amount can differ for the same key
* based on blockchain context (e.g., main chain vs. side chains).
* - Each input-output pair may have different amounts, depending on whether it's
* from the main chain, side chain, or an uncle block.
*/
private def updateMutableContractInputs() = {
sql"""
UPDATE inputs
SET
output_ref_tx_hash = outputs.tx_hash,
output_ref_address = outputs.address,
output_ref_amount = outputs.amount,
output_ref_tokens = outputs.tokens
FROM outputs
WHERE inputs.output_ref_key = outputs.key
AND inputs.output_ref_amount IS NULL
AND inputs.main_chain = outputs.main_chain
AND inputs.contract_input = true
AND outputs.fixed_output = false
RETURNING outputs.address, outputs.tokens, inputs.tx_hash, inputs.block_hash, inputs.block_timestamp, inputs.tx_order, inputs.main_chain
"""
.as[UpdateReturn]
}

/*
* Updates the inputs table for contract outputs where the amount is the same
* between main chain and side chain outputs.
*
* This function is similar to `updateMutableContractInputs`, but it does **not**
* require the `main_chain` status to match between inputs and outputs.
* This is useful for general contract outputs not covered by `updateMutableContractInputs`.
*/
private def updateContractInputs() = {
sql"""
UPDATE inputs
SET
output_ref_tx_hash = outputs.tx_hash,
output_ref_address = outputs.address,
output_ref_amount = outputs.amount,
output_ref_tokens = outputs.tokens
FROM outputs
WHERE inputs.output_ref_key = outputs.key
AND inputs.output_ref_amount IS NULL
AND inputs.contract_input = true
AND outputs.fixed_output = false
RETURNING outputs.address, outputs.tokens, inputs.tx_hash, inputs.block_hash, inputs.block_timestamp, inputs.tx_order, inputs.main_chain
"""
.as[UpdateReturn]
.flatMap(internalUpdates)
.transactionally
}

// format: off
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,7 @@ trait ExplorerSpec
tx.outputs.exists(_.address == address) || tx.inputs
.exists(_.address == Some(address))
}
}.distinct
}

val res = response.as[ArraySeq[Transaction]]

Expand Down
8 changes: 7 additions & 1 deletion app/src/test/scala/org/alephium/explorer/GenCoreApi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import org.alephium.protocol.model.{
GroupIndex,
Hint,
NetworkId,
ScriptHint,
Target
}
import org.alephium.serde._
Expand Down Expand Up @@ -144,7 +145,7 @@ object GenCoreApi {
unsigned <- unsignedTxGen
scriptExecutionOk <- arbitrary[Boolean]
contractInputsSize <- Gen.choose(0, 1)
contractInputs <- Gen.listOfN(contractInputsSize, outputRefProtocolGen)
contractInputs <- Gen.listOfN(contractInputsSize, contractOutputRefProtocolGen)
generatedOutputsSize <- Gen.choose(0, 1)
generatedOutputs <- Gen.listOfN(generatedOutputsSize, outputProtocolGen)
inputSignatures <- Gen.listOfN(1, bytesGen)
Expand Down Expand Up @@ -358,6 +359,11 @@ object GenCoreApi {
key <- hashGen
} yield OutputRef(hint, key)

val contractOutputRefProtocolGen: Gen[OutputRef] = for {
scriptHash <- hashGen
key <- hashGen
} yield OutputRef(Hint.ofContract(ScriptHint.fromHash(scriptHash)).value, key)

val inputProtocolGen: Gen[AssetInput] = for {
outputRef <- outputRefProtocolGen
unlockScript <- unlockScriptProtocolGen
Expand Down
20 changes: 15 additions & 5 deletions app/src/test/scala/org/alephium/explorer/GenDBModel.scala
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ object GenDBModel {
fixedOutput = fixedOutput
)

val fixedOutputEntityGen: Gen[OutputEntity] = outputEntityGen.map(_.copy(fixedOutput = true))
val contractOutputEntityGen: Gen[OutputEntity] = outputEntityGen.map(_.copy(fixedOutput = false))

val finalizedOutputEntityGen: Gen[OutputEntity] =
for {
output <- outputEntityGen
Expand Down Expand Up @@ -121,10 +124,9 @@ object GenDBModel {
@SuppressWarnings(Array("org.wartremover.warts.DefaultArguments"))
def inputEntityGen(outputEntityGen: Gen[OutputEntity] = outputEntityGen): Gen[InputEntity] =
for {
outputEntity <- outputEntityGen
unlockScript <- Gen.option(unlockScriptGen)
txOrder <- arbitrary[Int]
contractInput <- arbitrary[Boolean]
outputEntity <- outputEntityGen
unlockScript <- Gen.option(unlockScriptGen)
txOrder <- arbitrary[Int]
} yield {
InputEntity(
blockHash = outputEntity.blockHash,
Expand All @@ -140,10 +142,18 @@ object GenDBModel {
None,
None,
None,
contractInput
!outputEntity.fixedOutput
)
}

def fixedInputEntityGen(outputEntityGen: Gen[OutputEntity] = outputEntityGen): Gen[InputEntity] =
inputEntityGen(outputEntityGen).map(_.copy(contractInput = false))

def contractInputEntityGen(
outputEntityGen: Gen[OutputEntity] = outputEntityGen
): Gen[InputEntity] =
inputEntityGen(outputEntityGen).map(_.copy(contractInput = true))

@SuppressWarnings(Array("org.wartremover.warts.DefaultArguments"))
def uinputEntityGen(
transactionHash: Gen[TransactionId] = transactionHashGen,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ package org.alephium.explorer.persistence.queries
import slick.jdbc.PostgresProfile.api._

import org.alephium.explorer.AlephiumFutureSpec
import org.alephium.explorer.GenCoreProtocol._
import org.alephium.explorer.GenCoreUtil._
import org.alephium.explorer.GenDBModel._
import org.alephium.explorer.persistence.{DatabaseFixtureForEach, DBRunner}
import org.alephium.explorer.persistence.schema.{InputSchema, OutputSchema}
Expand All @@ -27,8 +29,8 @@ import org.alephium.explorer.persistence.schema.CustomJdbcTypes._
class InputUpdateQueriesSpec extends AlephiumFutureSpec with DatabaseFixtureForEach with DBRunner {

"Input Update" should {
"update inputs when address is already set" in {
forAll(outputEntityGen, inputEntityGen()) { case (output, input) =>
"update fixed inputs when address is already set" in {
forAll(fixedOutputEntityGen, fixedInputEntityGen()) { case (output, input) =>
run(for {
_ <- OutputSchema.table += output
_ <- InputSchema.table +=
Expand All @@ -51,8 +53,8 @@ class InputUpdateQueriesSpec extends AlephiumFutureSpec with DatabaseFixtureForE
}
}

"update inputs when address is not set" in {
forAll(outputEntityGen, inputEntityGen()) { case (output, input) =>
"update fixed inputs when address is not set" in {
forAll(fixedOutputEntityGen, fixedInputEntityGen()) { case (output, input) =>
run(for {
_ <- OutputSchema.table += output
_ <- InputSchema.table +=
Expand All @@ -68,5 +70,60 @@ class InputUpdateQueriesSpec extends AlephiumFutureSpec with DatabaseFixtureForE
updatedInput.outputRefAmount is Some(output.amount)
}
}

"update contract inputs when two outputs with same key have different value. See issue #584" in {
forAll(contractOutputEntityGen, contractInputEntityGen(), amountGen, amountGen) {
case (output, input, amount1, amount2) =>
val mainChainOutput =
output.copy(mainChain = true, amount = amount1)
val nonMainChainOutput =
output.copy(mainChain = false, amount = amount2, blockHash = blockHashGen.sample.get)

val mainChainInput = input.copy(mainChain = true, outputRefKey = mainChainOutput.key)
val nonMainChainInput = input.copy(
mainChain = false,
outputRefKey = nonMainChainOutput.key,
blockHash = blockHashGen.sample.get
)
run(for {
_ <- OutputSchema.table ++= Seq(mainChainOutput, nonMainChainOutput)
_ <- InputSchema.table ++= Seq(mainChainInput, nonMainChainInput)
} yield ()).futureValue

run(InputUpdateQueries.updateInputs()).futureValue

val updatedInputs =
run(InputSchema.table.filter(_.outputRefKey === output.key).result).futureValue

updatedInputs.find(_.mainChain == true).get.outputRefAddress is Some(output.address)
updatedInputs.find(_.mainChain == true).get.outputRefAmount is Some(amount1)

updatedInputs.find(_.mainChain == false).get.outputRefAddress is Some(output.address)
updatedInputs.find(_.mainChain == false).get.outputRefAmount is Some(amount2)
}
}

"update contract 2 input (main_chain, non_main_chain) using same output with same amoun" in {
forAll(contractOutputEntityGen, contractInputEntityGen()) { case (output, input) =>
val input1 = input.copy(mainChain = true, outputRefKey = output.key)
val input2 = input.copy(
mainChain = false,
outputRefKey = output.key,
blockHash = blockHashGen.sample.get
)
run(for {
_ <- OutputSchema.table += output
_ <- InputSchema.table ++= Seq(input1, input2)
} yield ()).futureValue

run(InputUpdateQueries.updateInputs()).futureValue

val updatedInputs =
run(InputSchema.table.filter(_.outputRefKey === output.key).result).futureValue

updatedInputs.foreach(_.outputRefAddress is Some(output.address))
updatedInputs.foreach(_.outputRefAmount is Some(output.amount))
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -658,7 +658,7 @@ class TransactionQueriesSpec extends AlephiumFutureSpec with DatabaseFixtureForE
false,
None,
None,
fixedOutput = false
fixedOutput = true
)

def input(hint: Int, outputRefKey: Hash): InputEntity =
Expand Down

0 comments on commit be27313

Please sign in to comment.