From 5206ff1089ad906e5657b4fbacf0e2ea1c41719b Mon Sep 17 00:00:00 2001 From: Alexander Slesarenko Date: Fri, 28 Jul 2023 22:03:50 +0200 Subject: [PATCH] multisig: SigmaBoolean.collectLeaves v1 --- .../src/main/scala/sigmastate/Values.scala | 18 +++++ .../basics/DiffieHellmanTupleProtocol.scala | 5 +- .../src/main/scala/sigmastate/trees.scala | 12 +++- .../SigmaProtocolSpecification.scala | 38 +++++++++- .../utxo/DistributedSigSpecification.scala | 5 +- .../ergoplatform/sdk/multisig/Signer.scala | 15 +--- .../sdk/multisig/SigningSpec.scala | 71 +++++++++++++++---- 7 files changed, 130 insertions(+), 34 deletions(-) diff --git a/interpreter/shared/src/main/scala/sigmastate/Values.scala b/interpreter/shared/src/main/scala/sigmastate/Values.scala index 1e9114a782..7ce2b61497 100644 --- a/interpreter/shared/src/main/scala/sigmastate/Values.scala +++ b/interpreter/shared/src/main/scala/sigmastate/Values.scala @@ -735,8 +735,12 @@ object Values { trait SigmaBoolean { /** Unique id of the node class used in serialization of SigmaBoolean. */ val opCode: OpCode + /** Size of the proposition tree (number of nodes). */ def size: Int + + /** Recursively collect all the leaves of this sigma expression into `buf`. */ + def collectLeaves(buf: mutable.ArrayBuffer[SigmaLeaf]): Unit } object SigmaBoolean { @@ -1006,6 +1010,20 @@ object Values { s"ProveDHTuple(${showECPoint(gv)}, ${showECPoint(hv)}, ${showECPoint(uv)}, ${showECPoint(vv)})" case _ => sb.toString } + + /** Traverses the tree and returns all leaves nodes of sigma proposition tree. */ + def leaves(): Seq[SigmaLeaf] = { + val buf = mutable.ArrayBuffer.empty[SigmaLeaf] + sb.collectLeaves(buf) + buf.toSeq + } + + /** Traverses the tree and returns all DISTINCT leaves of sigma proposition tree. */ + def distinctLeaves: Set[SigmaLeaf] = { + val buf = mutable.ArrayBuffer.empty[SigmaLeaf] + sb.collectLeaves(buf) + buf.iterator.toSet + } } sealed trait BlockItem extends NotReadyValue[SType] { diff --git a/interpreter/shared/src/main/scala/sigmastate/basics/DiffieHellmanTupleProtocol.scala b/interpreter/shared/src/main/scala/sigmastate/basics/DiffieHellmanTupleProtocol.scala index e3579db2ed..c3ac730bd4 100644 --- a/interpreter/shared/src/main/scala/sigmastate/basics/DiffieHellmanTupleProtocol.scala +++ b/interpreter/shared/src/main/scala/sigmastate/basics/DiffieHellmanTupleProtocol.scala @@ -1,17 +1,18 @@ package sigmastate.basics import java.math.BigInteger - import sigmastate.crypto.BigIntegers import sigmastate.Values.Value.PropositionCode import sigmastate._ import sigmastate.basics.VerifierMessage.Challenge import sigmastate.eval.SigmaDsl import CryptoConstants.EcPointType -import sigmastate.serialization.{OpCodes, GroupElementSerializer} +import sigmastate.serialization.{GroupElementSerializer, OpCodes} import sigmastate.serialization.OpCodes.OpCode import special.sigma.SigmaProp +import scala.collection.mutable + trait DiffieHellmanTupleProtocol extends SigmaProtocol[DiffieHellmanTupleProtocol] { override type A = FirstDiffieHellmanTupleProverMessage diff --git a/interpreter/shared/src/main/scala/sigmastate/trees.scala b/interpreter/shared/src/main/scala/sigmastate/trees.scala index e745a88c91..74ae31e96a 100644 --- a/interpreter/shared/src/main/scala/sigmastate/trees.scala +++ b/interpreter/shared/src/main/scala/sigmastate/trees.scala @@ -32,12 +32,21 @@ import scala.collection.mutable.ArrayBuffer */ trait SigmaConjecture extends SigmaBoolean { def children: Seq[SigmaBoolean] + + override def collectLeaves(buf: mutable.ArrayBuffer[SigmaLeaf]): Unit = { + cfor(0)(_ < children.length, _ + 1) { i => + children(i).collectLeaves(buf) + } + } } /** * Basic trait for leafs of crypto-trees, such as ProveDlog and ProveDiffieHellman instances */ -trait SigmaLeaf extends SigmaBoolean +trait SigmaLeaf extends SigmaBoolean { + override def collectLeaves(buf: mutable.ArrayBuffer[SigmaLeaf]): Unit = + buf += this +} /** @@ -130,6 +139,7 @@ case class CTHRESHOLD(k: Int, children: Seq[SigmaBoolean]) extends SigmaConjectu abstract class TrivialProp(val condition: Boolean) extends SigmaBoolean with Product1[Boolean] { override def _1: Boolean = condition override def canEqual(that: Any): Boolean = that != null && that.isInstanceOf[TrivialProp] + override def collectLeaves(buf: mutable.ArrayBuffer[SigmaLeaf]): Unit = () // not a leaf } object TrivialProp { // NOTE: the corresponding unapply is missing because any implementation (even using Nullable) diff --git a/interpreter/shared/src/test/scala/sigmastate/SigmaProtocolSpecification.scala b/interpreter/shared/src/test/scala/sigmastate/SigmaProtocolSpecification.scala index 96931f5e84..91c36f612f 100644 --- a/interpreter/shared/src/test/scala/sigmastate/SigmaProtocolSpecification.scala +++ b/interpreter/shared/src/test/scala/sigmastate/SigmaProtocolSpecification.scala @@ -1,10 +1,16 @@ package sigmastate +import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks +import sigmastate.TrivialProp.{FalseProp, TrueProp} +import sigmastate.Values.SigmaBoolean +import sigmastate.basics.DLogProtocol.ProveDlog +import sigmastate.basics.ProveDHTuple import sigmastate.basics.VerifierMessage.Challenge import sigmastate.crypto.{GF2_192, GF2_192_Poly} +import sigmastate.utils.Helpers import special.sigma.SigmaTestingData -class SigmaProtocolSpecification extends SigmaTestingData { +class SigmaProtocolSpecification extends SigmaTestingData with ScalaCheckPropertyChecks { property("CThresholdUncheckedNode equality") { val c1 = Challenge @@ Array[Byte](1) @@ -24,4 +30,34 @@ class SigmaProtocolSpecification extends SigmaTestingData { assertResult(true)(n4 != n5) } + property("collecting SigmaBoolean leaves") { + + val dlog1 = ProveDlog(Helpers.decodeECPoint("0297c44a12f4eb99a85d298fa3ba829b5b42b9f63798c980ece801cc663cc5fc9e")) + val dlog2 = ProveDlog(Helpers.decodeECPoint("02af645874c3b53465a5e9d820eb207d6001258c3b708f0d31d7c2e342833dce64")) + val dht1 = ProveDHTuple( + Helpers.decodeECPoint("021b4c0f54304b9c427d5c87846dd56a2fa361cd34a6cb8a2090aef043c9383198"), + Helpers.decodeECPoint("026826a4a9d0ec937c24d72da381ee6b5e74e49fb79a6a23a03fe0aa2cab3448ba"), + Helpers.decodeECPoint("02535153378ce30df1b31137680748de728f8512d29dfeeb1f331ac6a787cd00d8"), + Helpers.decodeECPoint("03d00d0174cdffd7ce3b77ef45ef9573c18fb76929fb3340f7ceea8d0be9bf5c4a") + ) + val and = CAND(Seq(dlog1, dlog2)) + val or = COR(Seq(dlog1, dlog2)) + val th = CTHRESHOLD(1, Seq(dlog1, dht1)) + val th2 = CTHRESHOLD(2, Seq(TrueProp, and, or, th, dlog1, dht1)) + + val table = Table(("proposition", "leafs"), + (TrueProp, Seq()), + (FalseProp, Seq()), + (dlog1, Seq(dlog1)), + (dht1, Seq(dht1)), + (and, Seq(dlog1, dlog2)), + (or, Seq(dlog1, dlog2)), + (th, Seq(dlog1, dht1)), + (th2, Seq(dlog1, dlog2, dlog1, dlog2, dlog1, dht1, dlog1, dht1)) + ) + forAll(table) { (prop: SigmaBoolean, leafs: Seq[SigmaLeaf]) => + prop.leaves shouldBe leafs + } + th2.leaves.iterator.distinct.toSet shouldBe Set(dlog1, dlog2, dht1) + } } diff --git a/sc/shared/src/test/scala/sigmastate/utxo/DistributedSigSpecification.scala b/sc/shared/src/test/scala/sigmastate/utxo/DistributedSigSpecification.scala index 780c233287..29863ee30d 100644 --- a/sc/shared/src/test/scala/sigmastate/utxo/DistributedSigSpecification.scala +++ b/sc/shared/src/test/scala/sigmastate/utxo/DistributedSigSpecification.scala @@ -84,23 +84,26 @@ class DistributedSigSpecification extends CompilerTestingCommons val bagA = HintsBag(Seq(dlBKnown, dlCKnown)) + // Alice starts co-signing (needs all commitments in the hingsBag) val proofAlice = proverA.prove(prop, ctx, fakeMessage, bagA).get val bagC = proverB.bagForMultisig(ctx, prop, proofAlice.proof, Seq(pubkeyAlice)) .addHint(carolHints.ownCommitments.head) .addHint(dlBKnown) + // Carol continues co-signing val proofCarol = proverC.prove(prop, ctx, fakeMessage, bagC).get val bagB = proverB.bagForMultisig(ctx, prop, proofCarol.proof, Seq(pubkeyAlice, pubkeyCarol)) .addHint(bobHints.ownCommitments.head) + // Bob finishes co-signing val proofBob = proverB.prove(prop, ctx, fakeMessage, bagB).get // Proof generated by Alice without getting Bob's part is not correct verifier.verify(prop, ctx, proofAlice, fakeMessage).get._1 shouldBe false - // Proof generated by Alice without getting Bob's part is not correct + // Proof generated by Carol without getting Bob's part is not correct verifier.verify(prop, ctx, proofCarol, fakeMessage).get._1 shouldBe false // Compound proof from Bob is correct diff --git a/sdk/shared/src/main/scala/org/ergoplatform/sdk/multisig/Signer.scala b/sdk/shared/src/main/scala/org/ergoplatform/sdk/multisig/Signer.scala index 9eee05238f..57f73fca10 100644 --- a/sdk/shared/src/main/scala/org/ergoplatform/sdk/multisig/Signer.scala +++ b/sdk/shared/src/main/scala/org/ergoplatform/sdk/multisig/Signer.scala @@ -1,18 +1,5 @@ package org.ergoplatform.sdk.multisig import org.ergoplatform.P2PKAddress -import org.ergoplatform.sdk.SigmaProver +import org.ergoplatform.sdk.{ReducedTransaction, SigmaProver} -case class Signer(prover: SigmaProver) { - def masterAddress: P2PKAddress = prover.getP2PKAddress - def eip3Addresses: Seq[P2PKAddress] = prover.getEip3Addresses -} - -object Signer { - - - def main(args: Array[String]): Unit = { - -// val pkAlice = - } -} \ No newline at end of file diff --git a/sdk/shared/src/test/scala/org/ergoplatform/sdk/multisig/SigningSpec.scala b/sdk/shared/src/test/scala/org/ergoplatform/sdk/multisig/SigningSpec.scala index 577063b703..0c07bc7c74 100644 --- a/sdk/shared/src/test/scala/org/ergoplatform/sdk/multisig/SigningSpec.scala +++ b/sdk/shared/src/test/scala/org/ergoplatform/sdk/multisig/SigningSpec.scala @@ -1,15 +1,15 @@ package org.ergoplatform.sdk.multisig import org.ergoplatform.sdk.Extensions.DoubleOps -import org.ergoplatform.sdk.NetworkType.Mainnet import org.ergoplatform.sdk.wallet.protocol.context.BlockchainStateContext import org.ergoplatform.sdk._ -import org.ergoplatform.{ErgoAddress, ErgoTreePredef} +import org.ergoplatform.{ErgoAddress, ErgoTreePredef, P2PKAddress} import org.scalatest.matchers.should.Matchers import org.scalatest.propspec.AnyPropSpec import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks import scalan.util.CollectionUtil.AnyOps -import sigmastate.TestsBase +import scorex.util.ModifierId +import sigmastate.{SigmaLeaf, TestsBase} import sigmastate.Values.{Constant, ErgoTree} import special.sigma.SigmaTestingData @@ -43,6 +43,19 @@ class SigningSpec extends AnyPropSpec with ScalaCheckPropertyChecks with Matcher ) ) + case class Signer(prover: SigmaProver) { + def masterAddress: P2PKAddress = prover.getP2PKAddress + + def eip3Addresses: Seq[P2PKAddress] = prover.getEip3Addresses + + def startCosigning(reduced: ReducedTransaction): SigningSession = { + SigningSession(reduced) + } + } + + object Signer { + } + def createSigner(secret: String): Signer = Signer( ProverBuilder.forMainnet(mainnetParameters) .withMnemonic(SecretString.create(secret), SecretString.empty()) @@ -125,24 +138,44 @@ class SigningSpec extends AnyPropSpec with ScalaCheckPropertyChecks with Matcher } } - class SigningSession( - val reduced: ReducedTransaction - ) - object SigningSession { - def apply(reduced: ReducedTransaction): SigningSession = { + case class SigningSession( + reduced: ReducedTransaction + ) { + def txId: ModifierId = reduced.ergoTx.unsignedTx.id + + /** Returns a set of public keys (leaf sigma propositions) for each input. */ + def participants: Seq[Set[SigmaLeaf]] = { val inputs = reduced.ergoTx.reducedInputs - val participants = inputs.zipWithIndex.map { case (reducedInput, i) => + inputs.map { reducedInput => val sb = reducedInput.reductionResult.value - sb + sb.distinctLeaves } - new SigningSession(reduced) } } + object SigningSession { + } + class CosigningServer( val addressBook: AddressBook - ) - + ) { + val sessions: mutable.Map[String, SigningSession] = mutable.Map.empty + + def addSession(session: SigningSession): Unit = { + require(!sessions.contains(session.txId), s"Session for tx ${session.txId} already exists") + sessions.put(session.txId, session) + } + + def getSessionsFor(signer: Signer): Seq[SigningSession] = { + sessions.values.toSeq + } + } + object CosigningServer { + def apply(addressBook: AddressBook): CosigningServer = { + new CosigningServer(addressBook) + } + } + property("Signing workflow") { val cosigners = Seq(alice, bob, carol) val inputs = cosigners.map(createInput(ctx, _)) @@ -150,7 +183,7 @@ class SigningSpec extends AnyPropSpec with ScalaCheckPropertyChecks with Matcher reduced shouldNot be(null) - // neither of cosigners can sign the transaction + // none of cosigners can sign the transaction cosigners.foreach(signer => assertExceptionThrown( signer.prover.signReduced(reduced), @@ -163,7 +196,15 @@ class SigningSpec extends AnyPropSpec with ScalaCheckPropertyChecks with Matcher addressBook.get(s.masterAddress) shouldBe Some(s) ) - val session = new SigningSession(reduced) + val server = CosigningServer(addressBook) + + // anyone can start a session (e.g. Alice) + server.addSession(alice.startCosigning(reduced)) + + // participants can retrieve the session + { + val session = server.getSessionsFor(alice).head + } } }