From 1c799a62571dd2db02973191a3242500c322e573 Mon Sep 17 00:00:00 2001 From: "fateme.r" Date: Sat, 17 Feb 2024 11:39:56 +0330 Subject: [PATCH 1/3] Add constraint to avoid duplicate not-merged commitments in reward tx Previously, there could be two commitments with the same WID in reward tx inputs. Then, they could have two rewards for the same report, or the guards could steal the RWT tokens associated with the second commitment box. Now, with added constraints, non of the plotted scenarios is possible. --- .../scala/rosen/bridge/scripts/Commitment.es | 13 +- src/test/scala/contracts/ContractTest.scala | 144 +++++++++++++++--- 2 files changed, 135 insertions(+), 22 deletions(-) diff --git a/src/main/scala/rosen/bridge/scripts/Commitment.es b/src/main/scala/rosen/bridge/scripts/Commitment.es index 83571ff..51baa2a 100644 --- a/src/main/scala/rosen/bridge/scripts/Commitment.es +++ b/src/main/scala/rosen/bridge/scripts/Commitment.es @@ -19,13 +19,19 @@ val WIDs = OUTPUTS.filter{(box:Box) => box.tokens.size > 0 && box.tokens(0)._1 == SELF.tokens(0)._1 } - .slice(0, trigger.R7[Int].get) + .map{(box:Box) => box.R4[Coll[Byte]].get} + val commitmentWIDs = INPUTS.filter{(box:Box) => + box.tokens.size > 0 && + box.tokens(0)._1 == SELF.tokens(0)._1 && + box.propositionBytes == SELF.propositionBytes + } .map{(box:Box) => box.R4[Coll[Byte]].get} val permitBox = OUTPUTS.filter {(box:Box) => box.R4[Coll[Byte]].isDefined && box.R4[Coll[Byte]].get == myWID }(0) - val WIDExists = WIDs.exists {(WID: Coll[Byte]) => myWID == WID} + val rewardWithWid = WIDs.filter {(WID: Coll[Byte]) => myWID == WID} + val commitmentWithWid = commitmentWIDs.filter{(WID: Coll[Byte]) => myWID == WID} sigmaProp( allOf( Coll( @@ -33,7 +39,8 @@ permitBox.tokens(0)._1 == SELF.tokens(0)._1, permitBox.tokens(0)._2 == SELF.tokens(0)._2, // check for duplicates - WIDExists == false, + rewardWithWid.size == 1, + commitmentWithWid.size == 1, // validate commitment blake2b256(eventData ++ myWID) == SELF.R6[Coll[Byte]].get ) diff --git a/src/test/scala/contracts/ContractTest.scala b/src/test/scala/contracts/ContractTest.scala index f1d00f8..a0bdbfc 100644 --- a/src/test/scala/contracts/ContractTest.scala +++ b/src/test/scala/contracts/ContractTest.scala @@ -707,23 +707,23 @@ class ContractTest extends TestSuite { */ property("partial return permit transaction signing should throw error by changing another watcher permit count") { networkConfig._1.ergoClient.execute(ctx => { - val prover = getProver() - val WID = Base16.decode(Boxes.getRandomHexString()).get - val WID2 = Base16.decode(Boxes.getRandomHexString()).get - val repoBox = Boxes.createRepo(ctx, 10000, 321, 100L, 1).convertToInputWith(Boxes.getRandomHexString(), 0) - val collateral = Boxes.createWatcherCollateralBoxInput(ctx, 1e9.toLong, 100, WID, 100L) - val repoOut = Boxes.createRepo(ctx, 10020, 301, 100L, 1) - val outCollateral = Boxes.createWatcherCollateralBox(ctx, 1e9.toLong, 100, WID, 80L) - val fakePermit = Boxes.createBoxForUser(ctx, prover.getAddress, 1e9.toLong, WID, new ErgoToken(Boxes.getRandomHexString(), 60L)) - val permitBox = Boxes.createPermitBox(ctx, 60L, WID2).convertToInputWith(Boxes.getRandomHexString(), 0) - val WIDBox = Boxes.createBoxForUser(ctx, prover.getAddress, 1e9.toLong, new ErgoToken(WID2, 2L)) - val permitOut = Boxes.createPermitBox(ctx, 40L, WID2) - val userOut = Boxes.createBoxCandidateForUser(ctx, prover.getAddress, 1e8.toLong, new ErgoToken(WID2, 2L), new ErgoToken(networkConfig._3.RSN, 20)) - val tx = ctx.newTxBuilder().addInputs(repoBox, collateral, fakePermit, WIDBox, permitBox) - .fee(Configs.fee) - .addOutputs(repoOut, outCollateral, permitOut, userOut) - .sendChangeTo(prover.getAddress) - .build() + val prover = getProver() + val WID = Base16.decode(Boxes.getRandomHexString()).get + val WID2 = Base16.decode(Boxes.getRandomHexString()).get + val repoBox = Boxes.createRepo(ctx, 10000, 321, 100L, 1).convertToInputWith(Boxes.getRandomHexString(), 0) + val collateral = Boxes.createWatcherCollateralBoxInput(ctx, 1e9.toLong, 100, WID, 100L) + val repoOut = Boxes.createRepo(ctx, 10020, 301, 100L, 1) + val outCollateral = Boxes.createWatcherCollateralBox(ctx, 1e9.toLong, 100, WID, 80L) + val fakePermit = Boxes.createBoxForUser(ctx, prover.getAddress, 1e9.toLong, WID, new ErgoToken(Boxes.getRandomHexString(), 60L)) + val permitBox = Boxes.createPermitBox(ctx, 60L, WID2).convertToInputWith(Boxes.getRandomHexString(), 0) + val WIDBox = Boxes.createBoxForUser(ctx, prover.getAddress, 1e9.toLong, new ErgoToken(WID2, 2L)) + val permitOut = Boxes.createPermitBox(ctx, 40L, WID2) + val userOut = Boxes.createBoxCandidateForUser(ctx, prover.getAddress, 1e8.toLong, new ErgoToken(WID2, 2L), new ErgoToken(networkConfig._3.RSN, 20)) + val tx = ctx.newTxBuilder().addInputs(repoBox, collateral, fakePermit, WIDBox, permitBox) + .fee(Configs.fee) + .addOutputs(repoOut, outCollateral, permitOut, userOut) + .sendChangeTo(prover.getAddress) + .build() assertThrows[AnyRef] { val signedTx = prover.sign(tx) println(signedTx.toJson(false)) @@ -897,8 +897,8 @@ class ContractTest extends TestSuite { val WID = Boxes.getRandomHexString().getBytes() val rwtCount = 20 val repoBox = Boxes.createRepoInput(ctx, 100000L, 200, 99L, 5) - val permitBox = Boxes.createPermitBox(ctx, rwtCount/2, WID).convertToInputWith(Boxes.getRandomHexString(), 0) - val permitBox2 = Boxes.createPermitBox(ctx, rwtCount/2, WID).convertToInputWith(Boxes.getRandomHexString(), 0) + val permitBox = Boxes.createPermitBox(ctx, rwtCount / 2, WID).convertToInputWith(Boxes.getRandomHexString(), 0) + val permitBox2 = Boxes.createPermitBox(ctx, rwtCount / 2, WID).convertToInputWith(Boxes.getRandomHexString(), 0) val WIDBox = Boxes.createBoxForUser(ctx, prover.getAddress, 1e9.toLong, new ErgoToken(WID, 2L)) val repoOut = Boxes.createRepo(ctx, 100000L + rwtCount, 200 - rwtCount, 100L, 4) val watcherCollateral = Boxes.createWatcherCollateralBoxInput(ctx, 1e9.toLong, 100, WID, rwtCount) @@ -1732,6 +1732,112 @@ class ContractTest extends TestSuite { }) } + /** + * @target reward transaction signing should throw error when there is duplicated not-merged commitments in inputs + * @dependencies + * @scenario + * - mock commitment wid list + * - mock not-merged commitments wid list with a duplicate WID + * - mock guards secrets, public keys and box with the guard NFT + * - mock lock box with required tokens + * - mock trigger box with wid list + * - mock output permits for all wids except the duplicate one + * - build and sign the reward transaction + * @expected + * - sign error for having duplicate commitment boxes in inputs, and stealing the RWTs by guards + */ + property("reward transaction signing should throw error when there is duplicated not-merged commitments in inputs") { + networkConfig._1.ergoClient.execute(ctx => { + val commitment = new Commitment() + val prover = getProver() + val WIDs = generateRandomWIDList(7) + var notMergedWIDs = generateRandomWIDList(3) + val allWIDs = WIDs ++ notMergedWIDs + notMergedWIDs = notMergedWIDs ++ Seq(notMergedWIDs(0)) + val notMergedCommitments = notMergedWIDs.map(WID => Boxes.createCommitment(ctx, WID, commitment.requestId(), commitment.hash(WID), 10L).convertToInputWith(Boxes.getRandomHexString(), 1)) + val secrets = (0 until 7).map(ind => Utils.randBigInt.bigInteger) + val guards = secrets.map(item => ctx.newProverBuilder().withDLogSecret(item).build()) + val guardsPks = guards.map(item => item.getAddress.getPublicKey.pkBytes).toArray + val eventTrigger = Boxes.createTriggerEventBox(ctx, WIDs, commitment, 70L).convertToInputWith(Boxes.getRandomHexString(), 1) + val userFee: Long = math.floor((commitment.fee * 0.6) / allWIDs.length).toLong + val guardBox = Boxes.createGuardNftBox(ctx, guardsPks, 5, 6).convertToInputWith(Boxes.getRandomHexString(), 0) + val lockBox = Boxes.createLockBox( + ctx, + Configs.minBoxValue, + new ErgoToken(commitment.targetChainTokenId, userFee * (WIDs ++ notMergedWIDs).length) + ).convertToInputWith(Boxes.getRandomHexString(), 0) + val newPermits = allWIDs.map(item => { + Boxes.createPermitBox(ctx, 10, item, new ErgoToken(commitment.targetChainTokenId, userFee)) + }) + val inputs = Seq(eventTrigger) ++ notMergedCommitments ++ Seq(lockBox) + val unsignedTx = ctx.newTxBuilder().addInputs(inputs: _*) + .fee(Configs.fee) + .addDataInputs(guardBox) + .sendChangeTo(prover.getAddress) + .addOutputs(newPermits: _*) + .build() + val multiSigProverBuilder = ctx.newProverBuilder() + secrets.map(item => multiSigProverBuilder.withDLogSecret(item)) + val multiSigProver = multiSigProverBuilder.build() + assertThrows[AnyRef] { + multiSigProver.sign(unsignedTx) + } + }) + } + + /** + * @target reward transaction signing should throw error when there is duplicated rewards for duplicated not merged commitments + * @dependencies + * @scenario + * - mock commitment wid list + * - mock not merged commitments wid list with a duplicate WID + * - mock guards secrets, public keys and box with the guard NFT + * - mock lock box with required tokens + * - mock trigger box with wid list + * - mock output permits for all wids including the duplicate WID + * - build and sign the reward transaction + * @expected + * - sign error for having duplicate rewards boxes in outputs + */ + property("reward transaction signing should throw error when there is duplicated rewards for duplicated not merged commitments") { + networkConfig._1.ergoClient.execute(ctx => { + val commitment = new Commitment() + val prover = getProver() + val WIDs = generateRandomWIDList(7) + var notMergedWIDs = generateRandomWIDList(3) + notMergedWIDs = notMergedWIDs ++ Seq(notMergedWIDs(0)) + val allWIDs = WIDs ++ notMergedWIDs + val notMergedCommitments = notMergedWIDs.map(WID => Boxes.createCommitment(ctx, WID, commitment.requestId(), commitment.hash(WID), 10L).convertToInputWith(Boxes.getRandomHexString(), 1)) + val secrets = (0 until 7).map(ind => Utils.randBigInt.bigInteger) + val guards = secrets.map(item => ctx.newProverBuilder().withDLogSecret(item).build()) + val guardsPks = guards.map(item => item.getAddress.getPublicKey.pkBytes).toArray + val eventTrigger = Boxes.createTriggerEventBox(ctx, WIDs, commitment, 70L).convertToInputWith(Boxes.getRandomHexString(), 1) + val userFee: Long = math.floor((commitment.fee * 0.6) / allWIDs.length).toLong + val guardBox = Boxes.createGuardNftBox(ctx, guardsPks, 5, 6).convertToInputWith(Boxes.getRandomHexString(), 0) + val lockBox = Boxes.createLockBox( + ctx, + Configs.minBoxValue, + new ErgoToken(commitment.targetChainTokenId, userFee * (WIDs ++ notMergedWIDs).length) + ).convertToInputWith(Boxes.getRandomHexString(), 0) + val newPermits = allWIDs.map(item => { + Boxes.createPermitBox(ctx, 10, item, new ErgoToken(commitment.targetChainTokenId, userFee)) + }) + val inputs = Seq(eventTrigger) ++ notMergedCommitments ++ Seq(lockBox) + val unsignedTx = ctx.newTxBuilder().addInputs(inputs: _*) + .fee(Configs.fee) + .addDataInputs(guardBox) + .sendChangeTo(prover.getAddress) + .addOutputs(newPermits: _*) + .build() + val multiSigProverBuilder = ctx.newProverBuilder() + secrets.map(item => multiSigProverBuilder.withDLogSecret(item)) + val multiSigProver = multiSigProverBuilder.build() + assertThrows[AnyRef] { + multiSigProver.sign(unsignedTx) + } + }) + } + property("test guard payment with not merged wrong commitment") { networkConfig._1.ergoClient.execute(ctx => { val commitment = new Commitment() From a6e44dde6334d0c6ab5edf2941ded0dcb7b3508b Mon Sep 17 00:00:00 2001 From: "fateme.r" Date: Sat, 17 Feb 2024 14:18:03 +0330 Subject: [PATCH 2/3] Check not-merged wid existence just in trigger wid list --- src/main/scala/rosen/bridge/scripts/Commitment.es | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/scala/rosen/bridge/scripts/Commitment.es b/src/main/scala/rosen/bridge/scripts/Commitment.es index 51baa2a..f4ec905 100644 --- a/src/main/scala/rosen/bridge/scripts/Commitment.es +++ b/src/main/scala/rosen/bridge/scripts/Commitment.es @@ -19,6 +19,7 @@ val WIDs = OUTPUTS.filter{(box:Box) => box.tokens.size > 0 && box.tokens(0)._1 == SELF.tokens(0)._1 } + .slice(0, trigger.R7[Int].get) .map{(box:Box) => box.R4[Coll[Byte]].get} val commitmentWIDs = INPUTS.filter{(box:Box) => box.tokens.size > 0 && @@ -30,7 +31,7 @@ box.R4[Coll[Byte]].isDefined && box.R4[Coll[Byte]].get == myWID }(0) - val rewardWithWid = WIDs.filter {(WID: Coll[Byte]) => myWID == WID} + val WIDExists = WIDs.exists {(WID: Coll[Byte]) => myWID == WID} val commitmentWithWid = commitmentWIDs.filter{(WID: Coll[Byte]) => myWID == WID} sigmaProp( allOf( @@ -39,7 +40,7 @@ permitBox.tokens(0)._1 == SELF.tokens(0)._1, permitBox.tokens(0)._2 == SELF.tokens(0)._2, // check for duplicates - rewardWithWid.size == 1, + WIDExists == false, commitmentWithWid.size == 1, // validate commitment blake2b256(eventData ++ myWID) == SELF.R6[Coll[Byte]].get @@ -49,7 +50,7 @@ } else if (blake2b256(OUTPUTS(0).propositionBytes) == eventTriggerHash){ // Event Trigger Creation - // [Commitments[]] + [(DataInput) RepoConfigBox + Repos[]] => [EventTrigger] + // [Commitments[]] + [(DataInput) RepoConfigBox + Repo] => [EventTrigger] val commitmentBoxes = INPUTS.filter{ (box: Box) => SELF.propositionBytes == box.propositionBytes && From 31eacecb49fe42dfd1fa09efad394798e326863e Mon Sep 17 00:00:00 2001 From: "fateme.r" Date: Sat, 17 Feb 2024 14:59:07 +0330 Subject: [PATCH 3/3] Optimizze commitment script filters --- src/main/scala/rosen/bridge/scripts/Commitment.es | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/main/scala/rosen/bridge/scripts/Commitment.es b/src/main/scala/rosen/bridge/scripts/Commitment.es index f4ec905..0ea9bf5 100644 --- a/src/main/scala/rosen/bridge/scripts/Commitment.es +++ b/src/main/scala/rosen/bridge/scripts/Commitment.es @@ -21,18 +21,17 @@ } .slice(0, trigger.R7[Int].get) .map{(box:Box) => box.R4[Coll[Byte]].get} - val commitmentWIDs = INPUTS.filter{(box:Box) => + val commitmentsWithMyWid = INPUTS.filter{(box:Box) => box.tokens.size > 0 && box.tokens(0)._1 == SELF.tokens(0)._1 && - box.propositionBytes == SELF.propositionBytes + box.propositionBytes == SELF.propositionBytes && + box.R4[Coll[Byte]].get == myWID } - .map{(box:Box) => box.R4[Coll[Byte]].get} val permitBox = OUTPUTS.filter {(box:Box) => box.R4[Coll[Byte]].isDefined && box.R4[Coll[Byte]].get == myWID }(0) val WIDExists = WIDs.exists {(WID: Coll[Byte]) => myWID == WID} - val commitmentWithWid = commitmentWIDs.filter{(WID: Coll[Byte]) => myWID == WID} sigmaProp( allOf( Coll( @@ -41,7 +40,7 @@ permitBox.tokens(0)._2 == SELF.tokens(0)._2, // check for duplicates WIDExists == false, - commitmentWithWid.size == 1, + commitmentsWithMyWid.size == 1, // validate commitment blake2b256(eventData ++ myWID) == SELF.R6[Coll[Byte]].get )