diff --git a/interpreter/shared/src/main/scala/sigmastate/interpreter/Hint.scala b/interpreter/shared/src/main/scala/sigmastate/interpreter/Hint.scala index ef8fda667e..844ee51a12 100644 --- a/interpreter/shared/src/main/scala/sigmastate/interpreter/Hint.scala +++ b/interpreter/shared/src/main/scala/sigmastate/interpreter/Hint.scala @@ -1,7 +1,7 @@ package sigmastate.interpreter import java.math.BigInteger -import sigmastate.{NodePosition, SigmaLeaf, UncheckedTree} +import sigmastate.{NodePosition, PositionedLeaf, SigmaLeaf, UncheckedTree} import sigmastate.Values.SigmaBoolean import sigmastate.basics.FirstProverMessage import sigmastate.basics.VerifierMessage.Challenge @@ -128,6 +128,14 @@ case class HintsBag(hints: Seq[Hint]) { /** @return a new bag with hints satisfying the predicate `p`. */ def filter(p: Hint => Boolean): HintsBag = HintsBag(hints.filter(p)) + /** @return true if there is a proof in this bag for the given leaf of sigma proposition. */ + def hasProofFor(pl: PositionedLeaf): Boolean = { + hints.exists { + case RealSecretProof(image, _, _, position) => pl.leaf == image && pl.position == position + case _ => false + } + } + override def toString: String = s"HintsBag(${hints.mkString("\n")})" } diff --git a/sdk/shared/src/main/scala/org/ergoplatform/sdk/AppkitProvingInterpreter.scala b/sdk/shared/src/main/scala/org/ergoplatform/sdk/AppkitProvingInterpreter.scala index dc289ba4e6..a0a8c7f3b2 100644 --- a/sdk/shared/src/main/scala/org/ergoplatform/sdk/AppkitProvingInterpreter.scala +++ b/sdk/shared/src/main/scala/org/ergoplatform/sdk/AppkitProvingInterpreter.scala @@ -212,21 +212,25 @@ class AppkitProvingInterpreter( * Note, this method doesn't require context to generate proofs (aka signatures). * * @param reducedTx unsigend transaction augmented with reduced + * @param baseCost initial cost before signing + * @param inputBagsOpt optional sequence of hints bags for each input * @return a new signed transaction with all inputs signed and the cost of this transaction * The returned cost includes: * - the costs of obtaining reduced transaction * - the cost of verification of each signed input */ - def signReduced(reducedTx: ReducedTransaction, baseCost: Int): SignedTransaction = { + def signReduced(reducedTx: ReducedTransaction, baseCost: Int, inputBagsOpt: Option[IndexedSeq[HintsBag]] = None): SignedTransaction = { val provedInputs = mutable.ArrayBuilder.make[Input] val unsignedTx = reducedTx.ergoTx.unsignedTx + val inputBags = inputBagsOpt.getOrElse( + IndexedSeq.fill(unsignedTx.inputs.length)(HintsBag.empty)) val maxCost = params.maxBlockCost var currentCost: Long = baseCost - for ((reducedInput, boxIdx) <- reducedTx.ergoTx.reducedInputs.zipWithIndex ) { + for ((reducedInput, boxIdx) <- reducedTx.ergoTx.reducedInputs.zipWithIndex) { val unsignedInput = unsignedTx.inputs(boxIdx) - val proverResult = proveReduced(reducedInput, unsignedTx.messageToSign) + val proverResult = proveReduced(reducedInput, unsignedTx.messageToSign, inputBags(boxIdx)) val signedInput = Input(unsignedInput.boxId, proverResult) val verificationCost = estimateCryptoVerifyCost(reducedInput.reductionResult.value).toBlockCost diff --git a/sdk/shared/src/main/scala/org/ergoplatform/sdk/Extensions.scala b/sdk/shared/src/main/scala/org/ergoplatform/sdk/Extensions.scala index e8244322a6..11d0cd731a 100644 --- a/sdk/shared/src/main/scala/org/ergoplatform/sdk/Extensions.scala +++ b/sdk/shared/src/main/scala/org/ergoplatform/sdk/Extensions.scala @@ -8,7 +8,7 @@ import special.collection.{Coll, CollBuilder, PairColl} import special.sigma.{Header, PreHeader} import scala.collection.compat.BuildFrom -import scala.collection.{GenIterable, immutable} +import scala.collection.{GenIterable, immutable, mutable} import scala.reflect.ClassTag object Extensions { @@ -222,4 +222,20 @@ object Extensions { v.updated(i, newItem).asInstanceOf[C[T]] } } + + /** extension methods for IndexedSeq */ + implicit class MutableMapOps[K, V](val m: mutable.Map[K, V]) extends AnyVal { + /** Modifies the Map by applying a function to the given element if it exists. + * + * @param k the key of the element to modify. + * @param f the function to apply to the element. + * @return the new value associated with the specified key + */ + def modifyIfExists(k: K)(f: V => V): Option[V] = { + m.updateWith(k) { + case Some(v) => Some(f(v)) + case None => None + } + } + } } diff --git a/sdk/shared/src/main/scala/org/ergoplatform/sdk/SigmaProver.scala b/sdk/shared/src/main/scala/org/ergoplatform/sdk/SigmaProver.scala index 6baba1e3b0..c877c7bc4c 100644 --- a/sdk/shared/src/main/scala/org/ergoplatform/sdk/SigmaProver.scala +++ b/sdk/shared/src/main/scala/org/ergoplatform/sdk/SigmaProver.scala @@ -88,6 +88,18 @@ class SigmaProver(private[sdk] val _prover: AppkitProvingInterpreter, networkPre _prover.signReduced(tx, tx.ergoTx.cost) } + /** Signs a given ReducedTransaction using the prover's secret keys and hints. + * @param tx - transaction to sign + * @param inputHints - hints containing proofs for all inputs + */ + def signReduced(tx: ReducedTransaction, inputBags: IndexedSeq[HintsBag]): SignedTransaction = { + val nInputs = tx.ergoTx.reducedInputs.length + require(nInputs == inputBags.length, + s"Number of bags ${inputBags.length} must be equal to number of inputs $nInputs") + + _prover.signReduced(tx, tx.ergoTx.cost, Some(inputBags)) + } + def generateCommitments(sigmaTree: SigmaBoolean): HintsBag = { _prover.generateCommitments(sigmaTree) } diff --git a/sdk/shared/src/test/scala/org/ergoplatform/sdk/multisig/CosigningServer.scala b/sdk/shared/src/test/scala/org/ergoplatform/sdk/multisig/CosigningServer.scala index 1d8154d7ea..f1cf273fb2 100644 --- a/sdk/shared/src/test/scala/org/ergoplatform/sdk/multisig/CosigningServer.scala +++ b/sdk/shared/src/test/scala/org/ergoplatform/sdk/multisig/CosigningServer.scala @@ -1,5 +1,6 @@ package org.ergoplatform.sdk.multisig +import org.ergoplatform.sdk.Extensions.MutableMapOps import sigmastate.SigmaLeaf import scala.collection.mutable @@ -29,6 +30,14 @@ class CosigningServer { def updateSession(session: SigningSession): Unit = { sessions.put(session.id, session) } + + def getReadySessions: Seq[SigningSession] = { + sessions.values.filter(_.isReady).toSeq + } + + def finishSession(sessionId: SessionId): Unit = { + sessions.modifyIfExists(sessionId)(_.finish()) + } } object CosigningServer { diff --git a/sdk/shared/src/test/scala/org/ergoplatform/sdk/multisig/Signer.scala b/sdk/shared/src/test/scala/org/ergoplatform/sdk/multisig/Signer.scala index e654873696..44a5af5934 100644 --- a/sdk/shared/src/test/scala/org/ergoplatform/sdk/multisig/Signer.scala +++ b/sdk/shared/src/test/scala/org/ergoplatform/sdk/multisig/Signer.scala @@ -1,7 +1,7 @@ package org.ergoplatform.sdk.multisig import org.ergoplatform.P2PKAddress -import org.ergoplatform.sdk.{ReducedTransaction, SigmaProver} +import org.ergoplatform.sdk.{ReducedTransaction, SigmaProver, SignedTransaction} import scalan.reflection.memoize import sigmastate.SigmaLeaf import sigmastate.Values.SigmaBoolean @@ -89,6 +89,10 @@ class Signer(val prover: SigmaProver) { session.addHintsAt(action.inputIndex, newHints) } + def createSignedTransaction(session: SigningSession): SignedTransaction = { + prover.signReduced(session.reduced, session.collectedHints) + } + override def equals(obj: Any): Boolean = (this eq obj.asInstanceOf[AnyRef]) || (obj match { case s: Signer => s.prover == prover case _ => false diff --git a/sdk/shared/src/test/scala/org/ergoplatform/sdk/multisig/SigningSession.scala b/sdk/shared/src/test/scala/org/ergoplatform/sdk/multisig/SigningSession.scala index 39a9e07bcc..09511653a0 100644 --- a/sdk/shared/src/test/scala/org/ergoplatform/sdk/multisig/SigningSession.scala +++ b/sdk/shared/src/test/scala/org/ergoplatform/sdk/multisig/SigningSession.scala @@ -2,8 +2,8 @@ package org.ergoplatform.sdk.multisig import org.ergoplatform.sdk.Extensions.IndexedSeqOps import org.ergoplatform.sdk.ReducedTransaction -import sigmastate.{PositionedLeaf, SigmaLeaf} import sigmastate.interpreter.{Hint, HintsBag} +import sigmastate.{PositionedLeaf, SigmaLeaf} case class SessionId(value: String) extends AnyVal @@ -20,7 +20,8 @@ case class CreateSignature(signerPk: SigmaLeaf, inputIndex: Int, leaf: Positione case class SigningSession( reduced: ReducedTransaction, - collectedHints: Vector[HintsBag] + collectedHints: Vector[HintsBag], + isFinished: Boolean = false ) { require(reduced.ergoTx.reducedInputs.length == collectedHints.length, s"Collected hints should be provided for each input, but got ${collectedHints.length} hints for ${reduced.ergoTx.reducedInputs.length} inputs") @@ -42,6 +43,17 @@ case class SigningSession( copy(collectedHints = collectedHints.modify(inputIndex, _.addHints(hints: _*))) } + def isReady: Boolean = { + positionsToProve.zipWithIndex.forall { case (positions, inputIndex) => + val inputHints = collectedHints(inputIndex) + positions.forall { pl => inputHints.hasProofFor(pl) } + } + } + + /** Mark this session as finished, no more actions can be performed. */ + def finish(): SigningSession = { + copy(isFinished = true) + } } 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 d2206d15cf..e833142e56 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 @@ -50,6 +50,9 @@ class SigningSpec extends AnyPropSpec with ScalaCheckPropertyChecks with Matcher .build() ) + /** Special signer without secretes */ + val publicSigner = Signer(ProverBuilder.forMainnet(mainnetParameters).build()) + val alice = createSigner("Alice secret") val bob = createSigner("Bob secret") val carol = createSigner("Carol secret") @@ -121,7 +124,7 @@ class SigningSpec extends AnyPropSpec with ScalaCheckPropertyChecks with Matcher // anyone can start a session (e.g. Alice) val sessionId = server.addSession(alice.startCosigning(reduced)) - // each cosigner generated a commitment and stores it in the session + // each cosigner generates a commitment and stores it in the session cosigners.zipWithIndex.foreach { case (signer, i) => val signerPk = signer.pubkey @@ -152,7 +155,7 @@ class SigningSpec extends AnyPropSpec with ScalaCheckPropertyChecks with Matcher server.getSession(sessionId).get.collectedHints.size shouldBe cosigners.size - // each cosigner generated a commitment and stores it in the session + // each cosigner generates a proof and stores it in the session cosigners.zipWithIndex.foreach { case (signer, i) => val signerPk = signer.pubkey val session = server.getSessionsFor(signer.allKeys).head @@ -165,6 +168,13 @@ class SigningSpec extends AnyPropSpec with ScalaCheckPropertyChecks with Matcher server.updateSession(newSession) } + // complete signing of the ready session + val session = server.getReadySessions.head + val signedTx = publicSigner.createSignedTransaction(session) + + // send the signed transaction to the network and wait for confirmation + // then finish the session + server.finishSession(session.id) } }