diff --git a/sigmastate/src/main/scala/sigmastate/SigSerializer.scala b/sigmastate/src/main/scala/sigmastate/SigSerializer.scala index 65f5c87c09..4e3b4cd39d 100644 --- a/sigmastate/src/main/scala/sigmastate/SigSerializer.scala +++ b/sigmastate/src/main/scala/sigmastate/SigSerializer.scala @@ -21,7 +21,7 @@ object SigSerializer { def traverseNode(node: UncheckedSigmaTree, acc: Array[Byte], writeChallenge: Boolean = true): Array[Byte] = { - val parentChal = (if (writeChallenge) node.challenge else Array.emptyByteArray) + val parentChal = if (writeChallenge) node.challenge else Array.emptyByteArray node match { case dl: UncheckedSchnorr => acc ++ diff --git a/sigmastate/src/main/scala/sigmastate/UnprovenTree.scala b/sigmastate/src/main/scala/sigmastate/UnprovenTree.scala index 4e816470ec..c3e74e7191 100644 --- a/sigmastate/src/main/scala/sigmastate/UnprovenTree.scala +++ b/sigmastate/src/main/scala/sigmastate/UnprovenTree.scala @@ -6,6 +6,9 @@ import com.google.common.primitives.Shorts import gf2t.GF2_192_Poly import sigmastate.basics.DLogProtocol.{FirstDLogProverMessage, ProveDlog} import sigmastate.basics.VerifierMessage.Challenge +import sigmastate.Values.{SigmaBoolean, SigmaPropConstant} +import sigmastate.basics.{FirstDiffieHellmanTupleProverMessage, FirstProverMessage, ProveDHTuple, SigmaProtocol} +import sigmastate.serialization.ErgoTreeSerializer import sigmastate.Values.{ErgoTree, SigmaBoolean, SigmaPropConstant} import sigmastate.basics.{FirstDiffieHellmanTupleProverMessage, FirstProverMessage, ProveDHTuple} import sigmastate.serialization.ErgoTreeSerializer.DefaultSerializer @@ -24,7 +27,7 @@ trait ProofTree extends Product trait ProofTreeLeaf extends ProofTree { val proposition: SigmaBoolean - val commitmentOpt: Option[FirstProverMessage[_]] + val commitmentOpt: Option[FirstProverMessage] } trait ProofTreeConjecture extends ProofTree { diff --git a/sigmastate/src/main/scala/sigmastate/Values.scala b/sigmastate/src/main/scala/sigmastate/Values.scala index 6de2980bf0..5b1cea0cce 100644 --- a/sigmastate/src/main/scala/sigmastate/Values.scala +++ b/sigmastate/src/main/scala/sigmastate/Values.scala @@ -597,7 +597,7 @@ object Values { case FalseProp.opCode => FalseProp case TrueProp.opCode => TrueProp case ProveDlogCode => dlogSerializer.parse(r) - case ProveDHTupleCode => dhtSerializer.parse(r) + case ProveDiffieHellmanTupleCode => dhtSerializer.parse(r) case AndCode => val n = r.getUShort() val children = new Array[SigmaBoolean](n) diff --git a/sigmastate/src/main/scala/sigmastate/basics/BcDlogGroup.scala b/sigmastate/src/main/scala/sigmastate/basics/BcDlogGroup.scala index c248788491..a75447bc68 100644 --- a/sigmastate/src/main/scala/sigmastate/basics/BcDlogGroup.scala +++ b/sigmastate/src/main/scala/sigmastate/basics/BcDlogGroup.scala @@ -166,13 +166,13 @@ abstract class BcDlogGroup[ElemType <: ECPoint](val x9params: X9ECParameters) ex override lazy val identity: ElemType = curve.getInfinity.asInstanceOf[ElemType] - def createPoint(x: BigInteger, y: BigInteger): ElemType = curve.createPoint(x, y).asInstanceOf[ElemType] - - /** - * Creates ECPoint.Fp with infinity values + * Create point from its affine coordinates + * @param x - X coordinate + * @param y - Y coordinate + * @return */ - lazy val getInfinity: ElemType = curve.getInfinity.asInstanceOf[ElemType] + def createPoint(x: BigInteger, y: BigInteger): ElemType = curve.createPoint(x, y).asInstanceOf[ElemType] /** @@ -182,7 +182,7 @@ abstract class BcDlogGroup[ElemType <: ECPoint](val x9params: X9ECParameters) ex * @return the inverse element of the given GroupElement * @throws IllegalArgumentException **/ - override def getInverse(groupElement: ElemType): ElemType = + override def inverseOf(groupElement: ElemType): ElemType = groupElement.negate().asInstanceOf[ElemType] /** @@ -413,54 +413,9 @@ abstract class BcDlogGroup[ElemType <: ECPoint](val x9params: X9ECParameters) ex } } -object SecP384R1 extends BcDlogGroup[SecP384R1Point](CustomNamedCurves.getByName("secp384r1")) with App { - val elems = 5000 - val base = generator - val exps = (1 to elems).map { _ => - val one = BigInteger.ONE - val qMinusOne = x9params.getN.subtract(one) - // choose a random number x in Zq* - BigIntegers.createRandomInRange(one, qMinusOne, secureRandom) - }.toArray - - println(exps.map(e => exponentiateWithPreComputedValues(base, e) == exponentiate(base, e)).forall(_ == true)) - - var t0 = System.currentTimeMillis() - exps.foreach(exp => exponentiate(base, exp)) - println(System.currentTimeMillis() - t0) +object SecP384R1 extends BcDlogGroup[SecP384R1Point](CustomNamedCurves.getByName("secp384r1")) - - t0 = System.currentTimeMillis() - exps.foreach(exp => exponentiateWithPreComputedValues(base, exp)) - println(System.currentTimeMillis() - t0) -} - -object SecP521R1 extends BcDlogGroup[SecP521R1Point](CustomNamedCurves.getByName("secp521r1")) with App { - val elems = 1000 - val bases = (1 to elems).map(_ => createRandomGenerator()).toArray - val exps = (1 to elems).map { _ => - val one = BigInteger.ONE - val qMinusOne = x9params.getN.subtract(one) - // choose a random number x in Zq* - BigIntegers.createRandomInRange(one, qMinusOne, secureRandom) - }.toArray - - var t0 = System.currentTimeMillis() - val naive = computeNaive(bases, exps) - println(System.currentTimeMillis() - t0) - - t0 = System.currentTimeMillis() - val ll = computeLL(bases, exps) - println(System.currentTimeMillis() - t0) - - println(naive.normalize().getAffineXCoord) - println(naive.normalize().getAffineYCoord) - - println(ll.normalize().getAffineXCoord) - println(ll.normalize().getAffineYCoord) - - println(naive == ll) -} +object SecP521R1 extends BcDlogGroup[SecP521R1Point](CustomNamedCurves.getByName("secp521r1")) object Curve25519 extends BcDlogGroup[Curve25519Point](CustomNamedCurves.getByName("curve25519")) diff --git a/sigmastate/src/main/scala/sigmastate/basics/DLogProtocol.scala b/sigmastate/src/main/scala/sigmastate/basics/DLogProtocol.scala index 1474ca9c25..e20f2d2360 100644 --- a/sigmastate/src/main/scala/sigmastate/basics/DLogProtocol.scala +++ b/sigmastate/src/main/scala/sigmastate/basics/DLogProtocol.scala @@ -5,6 +5,7 @@ import java.math.BigInteger import org.bouncycastle.util.BigIntegers import sigmastate.Values._ import Value.PropositionCode +import scorex.util.encode.Base16 import sigmastate._ import sigmastate.eval._ import sigmastate.basics.VerifierMessage.Challenge @@ -23,7 +24,8 @@ object DLogProtocol { /** Construct a new SigmaBoolean value representing public key of discrete logarithm signature protocol. */ case class ProveDlog(value: EcPointType) - extends SigmaProofOfKnowledgeTree[DLogSigmaProtocol, DLogProverInput] { + extends SigmaProofOfKnowledgeLeaf[DLogSigmaProtocol, DLogProverInput] { + override val opCode: OpCode = OpCodes.ProveDlogCode lazy val h: EcPointType = value lazy val pkBytes: Array[Byte] = GroupElementSerializer.toBytes(h) @@ -64,14 +66,17 @@ object DLogProtocol { } } - case class FirstDLogProverMessage(ecData: EcPointType) extends FirstProverMessage[DLogSigmaProtocol] { + case class FirstDLogProverMessage(ecData: EcPointType) extends FirstProverMessage { + override type SP = DLogSigmaProtocol override def bytes: Array[Byte] = { GroupElementSerializer.toBytes(ecData) } + + override def toString: Idn = s"FirstDLogProverMessage(${Base16.encode(bytes)})" } - case class SecondDLogProverMessage(z: BigInt) extends SecondProverMessage[DLogSigmaProtocol] { - override def bytes: Array[Byte] = z.toByteArray + case class SecondDLogProverMessage(z: BigInt) extends SecondProverMessage { + override type SP = DLogSigmaProtocol } class DLogInteractiveProver(override val publicInput: ProveDlog, override val privateInputOpt: Option[DLogProverInput]) @@ -83,7 +88,7 @@ object DLogProtocol { assert(privateInputOpt.isDefined, "Secret is not known") assert(rOpt.isEmpty, "Already generated r") - val (r, fm) = DLogInteractiveProver.firstMessage(publicInput) + val (r, fm) = DLogInteractiveProver.firstMessage() rOpt = Some(r) fm } @@ -110,7 +115,7 @@ object DLogProtocol { object DLogInteractiveProver { import CryptoConstants.secureRandom - def firstMessage(publicInput: ProveDlog): (BigInteger, FirstDLogProverMessage) = { + def firstMessage(): (BigInteger, FirstDLogProverMessage) = { import CryptoConstants.dlogGroup val qMinusOne = dlogGroup.order.subtract(BigInteger.ONE) @@ -164,7 +169,7 @@ object DLogProtocol { dlogGroup.multiplyGroupElements( dlogGroup.exponentiate(g, secondMessage.z.underlying()), - dlogGroup.getInverse(dlogGroup.exponentiate(h, new BigInteger(1, challenge)))) + dlogGroup.inverseOf(dlogGroup.exponentiate(h, new BigInteger(1, challenge)))) } } diff --git a/sigmastate/src/main/scala/sigmastate/basics/DiffieHellmanTupleProtocol.scala b/sigmastate/src/main/scala/sigmastate/basics/DiffieHellmanTupleProtocol.scala index d2d23de5d6..db27256ddc 100644 --- a/sigmastate/src/main/scala/sigmastate/basics/DiffieHellmanTupleProtocol.scala +++ b/sigmastate/src/main/scala/sigmastate/basics/DiffieHellmanTupleProtocol.scala @@ -26,6 +26,7 @@ case class DiffieHellmanTupleProverInput(w: BigInteger, commonInput: ProveDHTupl } object DiffieHellmanTupleProverInput { + import sigmastate.interpreter.CryptoConstants.dlogGroup def random(): DiffieHellmanTupleProverInput = { @@ -43,24 +44,28 @@ object DiffieHellmanTupleProverInput { //a = g^r, b = h^r case class FirstDiffieHellmanTupleProverMessage(a: CryptoConstants.EcPointType, b: CryptoConstants.EcPointType) - extends FirstProverMessage[DiffieHellmanTupleProtocol] { + extends FirstProverMessage { + + override type SP = DiffieHellmanTupleProtocol + override def bytes: Array[Byte] = { GroupElementSerializer.toBytes(a) ++ GroupElementSerializer.toBytes(b) } } //z = r + ew mod q -case class SecondDiffieHellmanTupleProverMessage(z: BigInteger) - extends SecondProverMessage[DiffieHellmanTupleProtocol] { - override def bytes: Array[PropositionCode] = ??? +case class SecondDiffieHellmanTupleProverMessage(z: BigInteger) extends SecondProverMessage { + + override type SP = DiffieHellmanTupleProtocol + } /** Construct a new SigmaProp value representing public key of Diffie Hellman signature protocol. - * Common input: (g,h,u,v)*/ + * Common input: (g,h,u,v) */ case class ProveDHTuple(gv: EcPointType, hv: EcPointType, uv: EcPointType, vv: EcPointType) extends SigmaProtocolCommonInput[DiffieHellmanTupleProtocol] - with SigmaProofOfKnowledgeTree[DiffieHellmanTupleProtocol, DiffieHellmanTupleProverInput] { - override val opCode: OpCode = OpCodes.ProveDHTupleCode + with SigmaProofOfKnowledgeLeaf[DiffieHellmanTupleProtocol, DiffieHellmanTupleProverInput] { + override val opCode: OpCode = OpCodes.ProveDiffieHellmanTupleCode lazy val g = gv lazy val h = hv lazy val u = uv @@ -115,6 +120,7 @@ class DiffieHellmanTupleInteractiveProver(override val publicInput: ProveDHTuple } object DiffieHellmanTupleInteractiveProver { + import sigmastate.interpreter.CryptoConstants.dlogGroup def firstMessage(publicInput: ProveDHTuple): (BigInteger, FirstDiffieHellmanTupleProverMessage) = { @@ -136,7 +142,7 @@ object DiffieHellmanTupleInteractiveProver { } def simulate(publicInput: ProveDHTuple, challenge: Challenge): - (FirstDiffieHellmanTupleProverMessage, SecondDiffieHellmanTupleProverMessage) = { + (FirstDiffieHellmanTupleProverMessage, SecondDiffieHellmanTupleProverMessage) = { val qMinusOne = dlogGroup.order.subtract(BigInteger.ONE) @@ -187,8 +193,8 @@ object DiffieHellmanTupleInteractiveProver { val uToE = dlogGroup.exponentiate(u, e) val vToE = dlogGroup.exponentiate(v, e) - val a = dlogGroup.multiplyGroupElements(gToZ, dlogGroup.getInverse(uToE)) - val b = dlogGroup.multiplyGroupElements(hToZ, dlogGroup.getInverse(vToE)) + val a = dlogGroup.multiplyGroupElements(gToZ, dlogGroup.inverseOf(uToE)) + val b = dlogGroup.multiplyGroupElements(hToZ, dlogGroup.inverseOf(vToE)) a -> b } } \ No newline at end of file diff --git a/sigmastate/src/main/scala/sigmastate/basics/DlogGroup.scala b/sigmastate/src/main/scala/sigmastate/basics/DlogGroup.scala index 9bfced419c..8df0f87e00 100644 --- a/sigmastate/src/main/scala/sigmastate/basics/DlogGroup.scala +++ b/sigmastate/src/main/scala/sigmastate/basics/DlogGroup.scala @@ -17,22 +17,6 @@ import org.bouncycastle.math.ec.ECPoint * In cryptography, we are interested in groups for which the discrete logarithm problem * (Dlog for short) is assumed to be hard. The most known groups of that kind are some Elliptic curve groups. * - * Another issue pertaining elliptic curves is the need to find a suitable mapping that will convert an arbitrary - * message (that is some binary string) to an element of the group and vice-versa. - * - * Only a subset of the messages can be effectively mapped to a group element in such a way that there is a one-to-one - * injection that converts the string to a group element and vice-versa. - * - * On the other hand, any group element can be mapped to some string. - * - * In this case, the operation is not invertible. This functionality is implemented by the functions: - * - {@code encodeByteArrayToGroupElement(binaryString: Array[Byte]): ElemType} - * - {@code decodeGroupElementToByteArray(element: ElemType) : Array[Byte]} - * - {@code mapAnyGroupElementToByteArray(element: ElemType): Array[Byte]} - * - * The first two work as a pair and decodeGroupElementToByteArray is the inverse of encodeByteArrayToGroupElement, - * whereas the last one works alone and does not have an inverse. - * * @tparam ElemType is concrete type */ trait DlogGroup[ElemType <: ECPoint] { @@ -73,7 +57,7 @@ trait DlogGroup[ElemType <: ECPoint] { * @return the inverse element of the given GroupElement * @throws IllegalArgumentException **/ - def getInverse(groupElement: ElemType): ElemType + def inverseOf(groupElement: ElemType): ElemType /** * Raises the base GroupElement to the exponent. The result is another GroupElement. diff --git a/sigmastate/src/main/scala/sigmastate/basics/SigmaProtocolFunctions.scala b/sigmastate/src/main/scala/sigmastate/basics/SigmaProtocolFunctions.scala index b5b02b711b..d1acb7a9cd 100644 --- a/sigmastate/src/main/scala/sigmastate/basics/SigmaProtocolFunctions.scala +++ b/sigmastate/src/main/scala/sigmastate/basics/SigmaProtocolFunctions.scala @@ -4,7 +4,7 @@ import java.security.SecureRandom import sigmastate.basics.VerifierMessage.Challenge import sigmastate.interpreter.CryptoConstants -import sigmastate.{SigmaProofOfKnowledgeTree, UncheckedTree} +import sigmastate.{SigmaProofOfKnowledgeLeaf, UncheckedTree} import supertagged.TaggedType import scala.concurrent.Future @@ -23,7 +23,6 @@ import scala.concurrent.Future trait TranscriptMessage { - def bytes: Array[Byte] } /** The message sent by a prover to its associated verifier as part of a sigma protocol interaction. */ @@ -39,17 +38,23 @@ object VerifierMessage { } /** First message from the prover (message `a` of `SigmaProtocol`)*/ -trait FirstProverMessage[SP <: SigmaProtocol[SP]] extends ProverMessage +trait FirstProverMessage extends ProverMessage { + type SP <: SigmaProtocol[SP] + + def bytes: Array[Byte] +} /** Second message from the prover (message `z` of `SigmaProtocol`)*/ -trait SecondProverMessage[SP <: SigmaProtocol[SP]] extends ProverMessage +trait SecondProverMessage extends ProverMessage { + type SP <: SigmaProtocol[SP] +} /** Abstract template for sigma protocols. * For details see the following book * [1] Efficient Secure Two-Party Protocols - Techniques and Constructions, p.150)*/ trait SigmaProtocol[SP <: SigmaProtocol[SP]] { - type A <: FirstProverMessage[SP] - type Z <: SecondProverMessage[SP] + type A <: FirstProverMessage + type Z <: SecondProverMessage } @@ -96,7 +101,7 @@ trait ZeroKnowledgeProofOfKnowledge[SP <: SigmaProtocol[SP]] trait NonInteractiveProver[SP <: SigmaProtocol[SP], PI <: SigmaProtocolPrivateInput[SP, CI], - CI <: SigmaProofOfKnowledgeTree[SP, PI], + CI <: SigmaProofOfKnowledgeLeaf[SP, PI], P <: UncheckedTree] extends Prover[SP, CI, PI] { diff --git a/sigmastate/src/main/scala/sigmastate/eval/Evaluation.scala b/sigmastate/src/main/scala/sigmastate/eval/Evaluation.scala index bac1ef19fe..a063fe9394 100644 --- a/sigmastate/src/main/scala/sigmastate/eval/Evaluation.scala +++ b/sigmastate/src/main/scala/sigmastate/eval/Evaluation.scala @@ -295,7 +295,7 @@ trait Evaluation extends RuntimeCosting { IR: IRContext => case SDBM.blake2b256(_, _) => CalcBlake2b256Code case SDBM.sha256(_, _) => CalcSha256Code case SDBM.proveDlog(_, _) => ProveDlogCode - case SDBM.proveDHTuple(_, _, _, _, _) => ProveDHTupleCode + case SDBM.proveDHTuple(_, _, _, _, _) => ProveDiffieHellmanTupleCode case SDBM.sigmaProp(_, _) => BoolToSigmaPropCode case SDBM.decodePoint(_, _) => DecodePointCode case SDBM.xorOf(_, _) => XorOfCode diff --git a/sigmastate/src/main/scala/sigmastate/interpreter/Hint.scala b/sigmastate/src/main/scala/sigmastate/interpreter/Hint.scala new file mode 100644 index 0000000000..1998bfd15d --- /dev/null +++ b/sigmastate/src/main/scala/sigmastate/interpreter/Hint.scala @@ -0,0 +1,125 @@ +package sigmastate.interpreter + +import java.math.BigInteger + +import sigmastate.UncheckedTree +import sigmastate.Values.SigmaBoolean +import sigmastate.basics.FirstProverMessage +import sigmastate.basics.VerifierMessage.Challenge + +/** + * A hint for a prover which helps the prover to prove a statement. For example, if the statement is "pk1 && pk2", + * and the prover knows only a secret for the public key pk1, the prover fails on proving without a hint. But if the + * prover knows that pk2 is known to another party, the prover may prove the statement (with an empty proof for "pk2"). + */ +trait Hint + +/** + * A hint which is indicating that a secret associated with its public image "image" is already proven. + */ +abstract class SecretProven extends Hint { + + /** + * Public image of a secret which is proven + */ + def image: SigmaBoolean + + /** + * Challenge used for a proof + */ + def challenge: Challenge + + /** + * Proof in a tree form + */ + def uncheckedTree: UncheckedTree +} + +/** + * A hint which contains a proof-of-knowledge for a secret associated with its public image "image", + * with also the mark that the proof is real. + */ +case class RealSecretProof(image: SigmaBoolean, + challenge: Challenge, + uncheckedTree: UncheckedTree) extends SecretProven + +/** + * A hint which contains a proof-of-knowledge for a secret associated with its public image "image", + * with also the mark that the proof is real. + */ +case class SimulatedSecretProof(image: SigmaBoolean, + challenge: Challenge, + uncheckedTree: UncheckedTree) extends SecretProven + + +/** + * A family of hints which are about a correspondence between a public image of a secret image and prover's commitment + * to randomness ("a" in a sigma protocol). + */ +abstract class CommitmentHint extends Hint { + def image: SigmaBoolean + def commitment: FirstProverMessage +} + +/** + * A hint which a commitment to randomness associated with a public image of a secret, as well as randomness itself. + * Please note that this randomness should be kept in secret by the prover. + * + * @param image - image of a secret + * @param secretRandomness - randomness + * @param commitment - commitment to randomness used while proving knowledge of the secret + */ +case class OwnCommitment(override val image: SigmaBoolean, + secretRandomness: BigInteger, + commitment: FirstProverMessage) extends CommitmentHint + +/** + * A hint which contains a commitment to randomness associated with a public image of a secret. + * + * @param image - image of a secret + * @param commitment - commitment to randomness used while proving knowledge of the secret + */ +case class RealCommitment(override val image: SigmaBoolean, commitment: FirstProverMessage) extends CommitmentHint + +/** + * A hint which contains a commitment to randomness associated with a public image of a secret. + * + * @param image - image of a secret + * @param commitment - commitment to randomness used while proving knowledge of the secret + */ +case class SimulatedCommitment(override val image: SigmaBoolean, commitment: FirstProverMessage) extends CommitmentHint + + +/** + * Collection of hints to be used by a prover + * + * @param hints - hints stored in the bag + */ +case class HintsBag(hints: Seq[Hint]) { + + lazy val realProofs: Seq[RealSecretProof] = hints.collect { case osp: RealSecretProof => osp } + lazy val simulatedProofs: Seq[SimulatedSecretProof] = hints.collect { case osp: SimulatedSecretProof => osp } + + lazy val proofs: Seq[SecretProven] = realProofs ++ simulatedProofs + + lazy val commitments: Seq[CommitmentHint] = hints.collect { case ch: CommitmentHint => ch } + lazy val realCommitments: Seq[RealCommitment] = hints.collect { case rc: RealCommitment => rc } + lazy val ownCommitments: Seq[OwnCommitment] = hints.collect { case oc: OwnCommitment => oc } + + lazy val realImages: Seq[SigmaBoolean] = realProofs.map(_.image) ++ realCommitments.map(_.image) + + def addHint(hint: Hint): HintsBag = HintsBag(hint +: hints) + + def addHints(newHints: Hint*): HintsBag = HintsBag(newHints ++ hints) + + def ++(other: HintsBag): HintsBag = HintsBag(other.hints ++ hints) + + override def toString: String = s"HintsBag(${hints.mkString("\n")})" + +} + +object HintsBag { + + val empty = HintsBag(Seq.empty) + +} diff --git a/sigmastate/src/main/scala/sigmastate/interpreter/Interpreter.scala b/sigmastate/src/main/scala/sigmastate/interpreter/Interpreter.scala index f867715b13..1ddbce9ad5 100644 --- a/sigmastate/src/main/scala/sigmastate/interpreter/Interpreter.scala +++ b/sigmastate/src/main/scala/sigmastate/interpreter/Interpreter.scala @@ -3,18 +3,19 @@ package sigmastate.interpreter import java.util import java.lang.{Math => JMath} -import org.bitbucket.inkytonik.kiama.rewriting.Rewriter.{strategy, rule, everywherebu} +import org.bitbucket.inkytonik.kiama.rewriting.Rewriter.{everywherebu, rule, strategy} import org.bitbucket.inkytonik.kiama.rewriting.Strategy -import sigmastate.basics.DLogProtocol.{FirstDLogProverMessage, DLogInteractiveProver} +import org.ergoplatform.validation.SigmaValidationSettings +import sigmastate.basics.DLogProtocol.{DLogInteractiveProver, FirstDLogProverMessage} import scorex.util.ScorexLogging import sigmastate.SCollection.SByteArray import sigmastate.Values._ import sigmastate.eval.{IRContext, Sized} import sigmastate.lang.Terms.ValueOps import sigmastate.basics._ -import sigmastate.interpreter.Interpreter.{VerificationResult, ScriptEnv} -import sigmastate.lang.exceptions.{InterpreterException, CostLimitException} -import sigmastate.serialization.{ValueSerializer, SigmaSerializer} +import sigmastate.interpreter.Interpreter.{ScriptEnv, VerificationResult} +import sigmastate.lang.exceptions.{CostLimitException, InterpreterException} +import sigmastate.serialization.{SigmaSerializer, ValueSerializer} import sigmastate.utxo.DeserializeContext import sigmastate.{SType, _} import org.ergoplatform.validation.ValidationRules._ @@ -85,6 +86,22 @@ trait Interpreter extends ScorexLogging { class MutableCell[T](var value: T) + /** Extracts proposition for ErgoTree handing soft-fork condition. + * @note soft-fork handler */ + def propositionFromErgoTree(ergoTree: ErgoTree, context: CTX): SigmaPropValue = { + val validationSettings = context.validationSettings + val prop = ergoTree.root match { + case Right(_) => + ergoTree.toProposition(ergoTree.isConstantSegregation) + case Left(UnparsedErgoTree(_, error)) if validationSettings.isSoftFork(error) => + TrueSigmaProp + case Left(UnparsedErgoTree(_, error)) => + throw new InterpreterException( + "Script has not been recognized due to ValidationException, and it cannot be accepted as soft-fork.", None, Some(error)) + } + prop + } + /** Substitute Deserialize* nodes with deserialized subtrees * We can estimate cost of the tree evaluation only after this step.*/ def applyDeserializeContext(context: CTX, exp: Value[SType]): (BoolValue, CTX) = { @@ -99,7 +116,7 @@ trait Interpreter extends ScorexLogging { def checkCost(context: CTX, exp: Value[SType], costF: Ref[((Int, IR.Size[IR.Context])) => Int]): Int = { import IR.Size._ - import IR.Context._; + import IR.Context._ val costingCtx = context.toSigmaContext(IR, isCost = true) val maxCost = context.costLimit val costFun = IR.compile[(Int, SSize[SContext]), Int, (Int, Size[Context]), Int](IR.getDataEnv, costF, Some(maxCost)) @@ -127,7 +144,7 @@ trait Interpreter extends ScorexLogging { res } - /** This method is used in both prover and verifier to compute SigmaProp value. + /** This method is used in both prover and verifier to compute SigmaBoolean value. * As the first step the cost of computing the `exp` expression in the given context is estimated. * If cost is above limit * then exception is returned and `exp` is not executed @@ -168,19 +185,34 @@ trait Interpreter extends ScorexLogging { def reduceToCrypto(context: CTX, exp: Value[SType]): Try[ReductionResult] = reduceToCrypto(context, Interpreter.emptyEnv, exp) - /** Extracts proposition for ErgoTree handing soft-fork condition. - * @note soft-fork handler */ - def propositionFromErgoTree(tree: ErgoTree, ctx: CTX): SigmaPropValue = { - val prop = tree.root match { - case Right(_) => - tree.toProposition(tree.isConstantSegregation) - case Left(UnparsedErgoTree(_, error)) if ctx.validationSettings.isSoftFork(error) => - TrueSigmaProp - case Left(UnparsedErgoTree(_, error)) => - throw new InterpreterException( - "Script has not been recognized due to ValidationException, and it cannot be accepted as soft-fork.", None, Some(error)) + + /** + * Full reduction of initial expression given in the ErgoTree form to a SigmaBoolean value + * (which encodes whether a sigma-protocol proposition or a boolean value, so true or false). + * + * Works as follows: + * 1) parse ErgoTree instance into a typed AST + * 2) go bottom-up the tree to replace DeserializeContext nodes only + * 3) estimate cost and reduce the AST to a SigmaBoolean instance (so sigma-tree or trivial boolean value) + * + * + * @param ergoTree - input ErgoTree expression to reduce + * @param context - context used in reduction + * @param env - script environment + * @return sigma boolean and the updated cost counter after reduction + */ + def fullReduction(ergoTree: ErgoTree, + context: CTX, + env: ScriptEnv): (SigmaBoolean, Long) = { + implicit val vs: SigmaValidationSettings = context.validationSettings + val prop = propositionFromErgoTree(ergoTree, context) + val (propTree, context2) = trySoftForkable[(BoolValue, CTX)](whenSoftFork = (TrueLeaf, context)) { + applyDeserializeContext(context, prop) } - prop + + // here we assume that when `propTree` is TrueProp then `reduceToCrypto` always succeeds + // and the rest of the verification is also trivial + reduceToCrypto(context2, env, propTree).getOrThrow } /** Executes the script in a given context. @@ -189,41 +221,34 @@ trait Interpreter extends ScorexLogging { * Step 3: Verify that the proof is presented to satisfy SigmaProp conditions. * * @param env environment of system variables used by the interpreter internally - * @param tree ErgoTree to execute in the given context and verify its result + * @param ergoTree ErgoTree expression to execute in the given context and verify its result * @param context the context in which `exp` should be executed * @param proof The proof of knowledge of the secrets which is expected by the resulting SigmaProp * @param message message bytes, which are used in verification of the proof * * @return verification result or Exception. - * If if the estimated cost of execution of the `tree` exceeds the limit (given in `context`), + * If if the estimated cost of execution of the `exp` exceeds the limit (given in `context`), * then exception if thrown and packed in Try. * If left component is false, then: * 1) script executed to false or - * 2) the given proof faild to validate resulting SigmaProp conditions. + * 2) the given proof failed to validate resulting SigmaProp conditions. * @see `reduceToCrypto` */ - def verify(env: ScriptEnv, tree: ErgoTree, + def verify(env: ScriptEnv, + ergoTree: ErgoTree, context: CTX, proof: Array[Byte], message: Array[Byte]): Try[VerificationResult] = { val (res, t) = BenchmarkUtil.measureTime(Try { - val initCost = JMath.addExact(tree.complexity.toLong, context.initCost) + val initCost = JMath.addExact(ergoTree.complexity.toLong, context.initCost) val remainingLimit = context.costLimit - initCost if (remainingLimit <= 0) throw new CostLimitException(initCost, msgCostLimitError(initCost, context.costLimit), None) - val context1 = context.withInitCost(initCost).asInstanceOf[CTX] - val prop = propositionFromErgoTree(tree, context1) - - implicit val vs = context1.validationSettings - val (propTree, context2) = trySoftForkable[(BoolValue, CTX)](whenSoftFork = (TrueLeaf, context1)) { - applyDeserializeContext(context1, prop) - } + val contextWithCost = context.withInitCost(initCost).asInstanceOf[CTX] - // here we assume that when `propTree` is TrueProp then `reduceToCrypto` always succeeds - // and the rest of the verification is also trivial - val (cProp, cost) = reduceToCrypto(context2, env, propTree).getOrThrow + val (cProp, cost) = fullReduction(ergoTree, contextWithCost, env) val checkingResult = cProp match { case TrivialProp.TrueProp => true @@ -285,29 +310,29 @@ trait Interpreter extends ScorexLogging { case _ => ??? }) - def verify(exp: ErgoTree, + def verify(ergoTree: ErgoTree, context: CTX, proverResult: ProverResult, message: Array[Byte]): Try[VerificationResult] = { val ctxv = context.withExtension(proverResult.extension).asInstanceOf[CTX] - verify(Interpreter.emptyEnv, exp, ctxv, proverResult.proof, message) + verify(Interpreter.emptyEnv, ergoTree, ctxv, proverResult.proof, message) } def verify(env: ScriptEnv, - exp: ErgoTree, + ergoTree: ErgoTree, context: CTX, proverResult: ProverResult, message: Array[Byte]): Try[VerificationResult] = { val ctxv = context.withExtension(proverResult.extension).asInstanceOf[CTX] - verify(env, exp, ctxv, proverResult.proof, message) + verify(env, ergoTree, ctxv, proverResult.proof, message) } - def verify(exp: ErgoTree, + def verify(ergoTree: ErgoTree, context: CTX, proof: ProofT, message: Array[Byte]): Try[VerificationResult] = { - verify(Interpreter.emptyEnv, exp, context, SigSerializer.toBytes(proof), message) + verify(Interpreter.emptyEnv, ergoTree, context, SigSerializer.toBytes(proof), message) } } diff --git a/sigmastate/src/main/scala/sigmastate/interpreter/ProverInterpreter.scala b/sigmastate/src/main/scala/sigmastate/interpreter/ProverInterpreter.scala index 81b642ceaf..95e620076e 100644 --- a/sigmastate/src/main/scala/sigmastate/interpreter/ProverInterpreter.scala +++ b/sigmastate/src/main/scala/sigmastate/interpreter/ProverInterpreter.scala @@ -1,76 +1,30 @@ package sigmastate.interpreter -import java.util +import java.math.BigInteger import gf2t.{GF2_192, GF2_192_Poly} import org.bitbucket.inkytonik.kiama.attribution.AttributionCore import org.bitbucket.inkytonik.kiama.rewriting.Rewriter.{everywherebu, everywheretd, rule} import org.bitbucket.inkytonik.kiama.rewriting.Strategy -import org.ergoplatform.settings.ErgoAlgos import scalan.util.CollectionUtil._ import sigmastate.Values._ import sigmastate._ import sigmastate.basics.DLogProtocol._ import sigmastate.basics.VerifierMessage.Challenge -import sigmastate.basics.{DiffieHellmanTupleInteractiveProver, DiffieHellmanTupleProverInput, ProveDHTuple, SigmaProtocolPrivateInput} +import sigmastate.basics.{DiffieHellmanTupleInteractiveProver, DiffieHellmanTupleProverInput, FirstDiffieHellmanTupleProverMessage, ProveDHTuple, SecondDiffieHellmanTupleProverMessage, SigmaProtocolPrivateInput} import sigmastate.lang.exceptions.CostLimitException -import sigmastate.serialization.SigmaSerializer -import sigmastate.utils.{Helpers, SigmaByteReader, SigmaByteWriter} -import Helpers._ -import scala.util.Try - -/** - * Proof of correctness of tx spending - * - * @param proof - proof that satisfies final sigma proposition - * @param extension - user-defined variables to be put into context - */ -class ProverResult(val proof: Array[Byte], val extension: ContextExtension) { - override def hashCode(): Int = util.Arrays.hashCode(proof) * 31 + extension.hashCode() - - override def equals(obj: scala.Any): Boolean = obj match { - case obj: ProverResult => - util.Arrays.equals(proof, obj.proof) && extension == obj.extension - case _ => false - } +import sigmastate.utils.Helpers - override def toString: Idn = s"ProverResult(${ErgoAlgos.encode(proof)},$extension)" -} - -object ProverResult { - val empty: ProverResult = ProverResult(Array[Byte](), ContextExtension.empty) - - def apply(proof: Array[Byte], extension: ContextExtension): ProverResult = - new ProverResult(proof, extension) - - object serializer extends SigmaSerializer[ProverResult, ProverResult] { - - override def serialize(obj: ProverResult, w: SigmaByteWriter): Unit = { - w.putUShort(obj.proof.length) - w.putBytes(obj.proof) - ContextExtension.serializer.serialize(obj.extension, w) - } - - override def parse(r: SigmaByteReader): ProverResult = { - val sigBytesCount = r.getUShort() - val proofBytes = r.getBytes(sigBytesCount) - val ce = ContextExtension.serializer.parse(r) - ProverResult(proofBytes, ce) - } - } -} +import scala.util.Try -case class CostedProverResult(override val proof: Array[Byte], - override val extension: ContextExtension, - cost: Long) extends ProverResult(proof, extension) /** * Interpreter with enhanced functionality to prove statements. */ -trait ProverInterpreter extends Interpreter with AttributionCore { +trait ProverInterpreter extends Interpreter with ProverUtils with AttributionCore { - import CryptoConstants.secureRandomBytes import Interpreter._ + import CryptoConstants.secureRandomBytes override type ProofT = UncheckedTree @@ -78,7 +32,8 @@ trait ProverInterpreter extends Interpreter with AttributionCore { /** * The comments in this section are taken from the algorithm for the - * Sigma-protocol prover as described in the white paper + * Sigma-protocol prover as described in the ErgoScript white-paper + * https://ergoplatform.org/docs/ErgoScript.pdf , Appendix A * */ // todo: if we are concerned about timing attacks against the prover, we should make sure that this code @@ -87,10 +42,10 @@ trait ProverInterpreter extends Interpreter with AttributionCore { // todo: once the right value is (or is not) found. We should also make all loops look similar, the same // todo: amount of copying is done regardless of what's real or simulated, // todo: real vs. simulated computations take the same time, etc. - protected def prove(unprovenTree: UnprovenTree, message: Array[Byte]): ProofT = { + protected def prove(unprovenTree: UnprovenTree, message: Array[Byte], hintsBag: HintsBag): ProofT = { // Prover Step 1: Mark as real everything the prover can prove - val step1 = markReal(unprovenTree).get.asInstanceOf[UnprovenTree] + val step1 = markReal(hintsBag)(unprovenTree).get.asInstanceOf[UnprovenTree] // Prover Step 2: If the root of the tree is marked "simulated" then the prover does not have enough witnesses // to perform the proof. Abort. @@ -102,52 +57,62 @@ trait ProverInterpreter extends Interpreter with AttributionCore { // Prover Steps 4, 5, and 6 together: find challenges for simulated nodes; simulate simulated leaves; // compute commitments for real leaves - val step6 = simulateAndCommit(step3).get.asInstanceOf[UnprovenTree] + val step6 = simulateAndCommit(hintsBag)(step3).get.asInstanceOf[UnprovenTree] // Prover Steps 7: convert the relevant information in the tree (namely, tree structure, node types, - // the statements being proven and commitments at the leaves) - // to a string - val s = FiatShamirTree.toBytes(step6) + // the statements being proven and commitments at the leaves) to a bitstring. + // This bitstring corresponding to a proposition to prove is needed for Strong Fiat-Shamir transformation. + // See [BPW12] paper on Strong vs Weak Fiat-Shamir, + // (https://link.springer.com/content/pdf/10.1007/978-3-642-34961-4_38.pdf) + val propBytes = FiatShamirTree.toBytes(step6) - // Prover Step 8: compute the challenge for the root of the tree as the Fiat-Shamir hash of s + // Prover Step 8: compute the challenge for the root of the tree as the Fiat-Shamir hash of propBytes // and the message being signed. - val rootChallenge = Challenge @@ CryptoFunctions.hashFn(s ++ message) + val rootChallenge = Challenge @@ CryptoFunctions.hashFn(propBytes ++ message) val step8 = step6.withChallenge(rootChallenge) // Prover Step 9: complete the proof by computing challenges at real nodes and additionally responses at real leaves - val step9 = proving(step8).get.asInstanceOf[ProofTree] + val step9 = proving(hintsBag)(step8).get.asInstanceOf[ProofTree] // Syntactic step that performs a type conversion only convertToUnchecked(step9) } - def prove(exp: ErgoTree, context: CTX, message: Array[Byte]): Try[CostedProverResult] = - prove(emptyEnv, exp, context, message) + def prove(ergoTree: ErgoTree, + context: CTX, + message: Array[Byte], + hintsBag: HintsBag): Try[CostedProverResult] = + prove(emptyEnv, ergoTree, context, message, hintsBag) + + def prove(ergoTree: ErgoTree, + context: CTX, + message: Array[Byte]): Try[CostedProverResult] = + prove(emptyEnv, ergoTree, context, message, HintsBag.empty) + - def prove(env: ScriptEnv, tree: ErgoTree, ctx: CTX, message: Array[Byte]): Try[CostedProverResult] = Try { + def prove(env: ScriptEnv, + ergoTree: ErgoTree, + context: CTX, + message: Array[Byte], + hintsBag: HintsBag = HintsBag.empty): Try[CostedProverResult] = Try { import TrivialProp._ - val initCost = tree.complexity + ctx.initCost - val remainingLimit = ctx.costLimit - initCost + val initCost = ergoTree.complexity + context.initCost + val remainingLimit = context.costLimit - initCost if (remainingLimit <= 0) throw new CostLimitException(initCost, - s"Estimated execution cost $initCost exceeds the limit ${ctx.costLimit}", None) + s"Estimated execution cost $initCost exceeds the limit ${context.costLimit}", None) - val ctxUpdInitCost = ctx.withInitCost(initCost).asInstanceOf[CTX] + val ctxUpdInitCost = context.withInitCost(initCost).asInstanceOf[CTX] - val prop = propositionFromErgoTree(tree, ctxUpdInitCost) - val (propTree, _) = applyDeserializeContext(ctxUpdInitCost, prop) - val tried = reduceToCrypto(ctxUpdInitCost, env, propTree) - val (reducedProp, cost) = tried.getOrThrow - - def errorReducedToFalse = error("Script reduced to false") + val (reducedProp, cost) = fullReduction(ergoTree, ctxUpdInitCost, env) val proofTree = reducedProp match { case TrueProp => NoProof - case FalseProp => errorReducedToFalse + case FalseProp => error("Script reduced to false") case sigmaTree => val unprovenTree = convertToUnproven(sigmaTree) - prove(unprovenTree, message) + prove(unprovenTree, message, hintsBag) } // Prover Step 10: output the right information into the proof val proof = SigSerializer.toBytes(proofTree) @@ -160,8 +125,9 @@ trait ProverInterpreter extends Interpreter with AttributionCore { * necessary number of witnesses (for example, more than one child of an OR). * This will be corrected in the next step. * In a bottom-up traversal of the tree, do the following for each node: + * */ - val markReal: Strategy = everywherebu(rule[UnprovenTree] { + def markReal(hintsBag: HintsBag): Strategy = everywherebu(rule[UnprovenTree] { case and: CAndUnproven => // If the node is AND, mark it "real" if all of its children are marked real; else mark it "simulated" val simulated = and.children.exists(_.asInstanceOf[UnprovenTree].simulated) @@ -171,25 +137,21 @@ trait ProverInterpreter extends Interpreter with AttributionCore { val simulated = or.children.forall(_.asInstanceOf[UnprovenTree].simulated) or.copy(simulated = simulated) case t: CThresholdUnproven => - // If the node is TRESHOLD(k), mark it "real" if at least k of its children are marked real; else mark it "simulated" - val c = t.children.foldLeft(0) { (count, child) => + // If the node is THRESHOLD(k), mark it "real" if at least k of its children are marked real; else mark it "simulated" + val realCount = t.children.foldLeft(0) { (count, child) => count + (if (child.asInstanceOf[UnprovenTree].simulated) 0 else 1) } - t.copy(simulated = c < t.k) - case su: UnprovenSchnorr => - // If the node is a leaf, mark it "real'' if the witness for it is available; else mark it "simulated" - val secretKnown = secrets.exists { - case in: DLogProverInput => in.publicImage == su.proposition - case _ => false - } - su.copy(simulated = !secretKnown) - case dhu: UnprovenDiffieHellmanTuple => - // If the node is a leaf, mark it "real" if the witness for it is available; else mark it "simulated" - val secretKnown = secrets.exists { - case in: DiffieHellmanTupleProverInput => in.publicImage == dhu.proposition + t.copy(simulated = realCount < t.k) + // UnprovenSchnorr | UnprovenDiffieHellmanTuple case + case ul: UnprovenLeaf => + // If the node is a leaf, mark it "real'' if either the witness for it is available or a hint shows the secret + // is known to an external participant in multi-signing; + // else mark it "simulated" + val isReal = hintsBag.realImages.contains(ul.proposition) || secrets.exists { + case in: SigmaProtocolPrivateInput[_, _] => in.publicImage == ul.proposition case _ => false } - dhu.copy(simulated = !secretKnown) + ul.withSimulated(!isReal) case t => error(s"Don't know how to markReal($t)") }) @@ -236,11 +198,10 @@ trait ProverInterpreter extends Interpreter with AttributionCore { // We'll mark the first k real ones real val newChildren = t.children.foldLeft((Seq[UnprovenTree](), 0)) { case ((children, countOfReal), child) => val kid = child.asInstanceOf[UnprovenTree] - val (newKid, newCountOfReal) = kid.real match { - case false => (kid, countOfReal) - case true => ( { - if (countOfReal >= t.k) kid.withSimulated(true) else kid - }, countOfReal + 1) + val (newKid, newCountOfReal) = if (kid.real) { + ( { if (countOfReal >= t.k) kid.withSimulated(true) else kid }, countOfReal + 1) + } else { + (kid, countOfReal) } (children :+ newKid, newCountOfReal) }._1 @@ -258,7 +219,7 @@ trait ProverInterpreter extends Interpreter with AttributionCore { * Prover Step 6: For every leaf marked "real", use the first prover step of the Sigma-protocol for that leaf to * compute the commitment a. */ - val simulateAndCommit: Strategy = everywheretd(rule[ProofTree] { + def simulateAndCommit(hintsBag: HintsBag): Strategy = everywheretd(rule[ProofTree] { // Step 4 part 1: If the node is marked "real", then each of its simulated children gets a fresh uniformly // random challenge in {0,1}^t. case and: CAndUnproven if and.real => and // A real AND node has no simulated children @@ -266,8 +227,16 @@ trait ProverInterpreter extends Interpreter with AttributionCore { //real OR or Threshold case case uc: UnprovenConjecture if uc.real => val newChildren = uc.children.cast[UnprovenTree].map(c => - if (c.real) c - else c.withChallenge(Challenge @@ secureRandomBytes(CryptoFunctions.soundnessBytes)) + if (c.real) { + c + } else { + // take challenge from previously done proof stored in the hints bag, + // or generate random challenge for simulated child + val newChallenge = hintsBag.proofs.find(_.image == c.proposition).map(_.challenge).getOrElse( + Challenge @@ secureRandomBytes(CryptoFunctions.soundnessBytes) + ) + c.withChallenge(newChallenge) + } ) uc match { case or: COrUnproven => or.copy(children = newChildren) @@ -346,36 +315,50 @@ trait ProverInterpreter extends Interpreter with AttributionCore { t.withPolynomial(q).copy(children=newChildren) */ - - case su: UnprovenSchnorr => - if (su.simulated) { - // Step 5 (simulated leaf -- complete the simulation) - assert(su.challengeOpt.isDefined) - val (fm, sm) = DLogInteractiveProver.simulate(su.proposition, su.challengeOpt.get) - UncheckedSchnorr(su.proposition, Some(fm), su.challengeOpt.get, sm) - } else { - // Step 6 (real leaf -- compute the commitment a) - val (r, commitment) = DLogInteractiveProver.firstMessage(su.proposition) - su.copy(commitmentOpt = Some(commitment), randomnessOpt = Some(r)) + // Steps 5 & 6: first try pulling out commitment from the hints bag. If it exists proceed with it, + // otherwise, compute the commitment (if the node is real) or simulate it (if the node is simulated) + + // Step 6 (real leaf -- compute the commitment a or take it from the hints bag) + hintsBag.commitments.find(_.image == su.proposition).map { cmtHint => + su.copy(commitmentOpt = Some(cmtHint.commitment.asInstanceOf[FirstDLogProverMessage])) + }.getOrElse { + if (su.simulated) { + // Step 5 (simulated leaf -- complete the simulation) + assert(su.challengeOpt.isDefined) + val (fm, sm) = DLogInteractiveProver.simulate(su.proposition, su.challengeOpt.get) + UncheckedSchnorr(su.proposition, Some(fm), su.challengeOpt.get, sm) + } else { + // Step 6 -- compute the commitment + val (r, commitment) = DLogInteractiveProver.firstMessage() + su.copy(commitmentOpt = Some(commitment), randomnessOpt = Some(r)) + } } case dhu: UnprovenDiffieHellmanTuple => - if (dhu.simulated) { - // Step 5 (simulated leaf -- complete the simulation) - assert(dhu.challengeOpt.isDefined) - val (fm, sm) = DiffieHellmanTupleInteractiveProver.simulate(dhu.proposition, dhu.challengeOpt.get) - UncheckedDiffieHellmanTuple(dhu.proposition, Some(fm), dhu.challengeOpt.get, sm) - } else { - // Step 6 (real leaf -- compute the commitment a) - val (r, fm) = DiffieHellmanTupleInteractiveProver.firstMessage(dhu.proposition) - dhu.copy(commitmentOpt = Some(fm), randomnessOpt = Some(r)) - } + //Steps 5 & 6: pull out commitment from the hints bag, otherwise, compute the commitment(if the node is real), + // or simulate it (if the node is simulated) + + // Step 6 (real leaf -- compute the commitment a or take it from the hints bag) + hintsBag.commitments.find(_.image == dhu.proposition).map { cmtHint => + dhu.copy(commitmentOpt = Some(cmtHint.commitment.asInstanceOf[FirstDiffieHellmanTupleProverMessage])) + }.getOrElse { + if (dhu.simulated) { + // Step 5 (simulated leaf -- complete the simulation) + assert(dhu.challengeOpt.isDefined) + val (fm, sm) = DiffieHellmanTupleInteractiveProver.simulate(dhu.proposition, dhu.challengeOpt.get) + UncheckedDiffieHellmanTuple(dhu.proposition, Some(fm), dhu.challengeOpt.get, sm) + } else { + // Step 6 -- compute the commitment + val (r, fm) = DiffieHellmanTupleInteractiveProver.firstMessage(dhu.proposition) + dhu.copy(commitmentOpt = Some(fm), randomnessOpt = Some(r)) + } + } case a: Any => error(s"Don't know how to challengeSimulated($a)") }) - def extractChallenge(pt: ProofTree): Option[Array[Byte]] = pt match { + private def extractChallenge(pt: ProofTree): Option[Array[Byte]] = pt match { case upt: UnprovenTree => upt.challengeOpt case sn: UncheckedSchnorr => Some(sn.challenge) case dh: UncheckedDiffieHellmanTuple => Some(dh.challenge) @@ -387,7 +370,7 @@ trait ProverInterpreter extends Interpreter with AttributionCore { * the challenge e for every node marked "real" below the root and, additionally, the response z for every leaf * marked "real" */ - val proving: Strategy = everywheretd(rule[ProofTree] { + def proving(hintsBag: HintsBag): Strategy = everywheretd(rule[ProofTree] { // If the node is a non-leaf marked real whose challenge is e_0, proceed as follows: case and: CAndUnproven if and.real => assert(and.challengeOpt.isDefined) @@ -441,25 +424,76 @@ trait ProverInterpreter extends Interpreter with AttributionCore { t.withPolynomial(q).copy(children = newChildren) // If the node is a leaf marked "real", compute its response according to the second prover step - // of the Sigma-protocol given the commitment, challenge, and witness + // of the Sigma-protocol given the commitment, challenge, and witness, or pull response from the hints bag case su: UnprovenSchnorr if su.real => - assert(su.challengeOpt.isDefined, s"Real UnprovenTree $su should have challenge defined") - val privKey = secrets + assert(su.challengeOpt.isDefined, s"Real UnprovenSchnorr $su should have challenge defined") + val privKeyOpt = secrets .filter(_.isInstanceOf[DLogProverInput]) .find(_.asInstanceOf[DLogProverInput].publicImage == su.proposition) - .get.asInstanceOf[DLogProverInput] - val z = DLogInteractiveProver.secondMessage(privKey, su.randomnessOpt.get, su.challengeOpt.get) + + val z = privKeyOpt match { + case Some(privKey: DLogProverInput) => + hintsBag.ownCommitments.find(_.image == su.proposition).map { oc => + DLogInteractiveProver.secondMessage( + privKey, + oc.secretRandomness, + su.challengeOpt.get) + }.getOrElse { + DLogInteractiveProver.secondMessage( + privKey, + su.randomnessOpt.get, + su.challengeOpt.get) + } + + case None => + hintsBag.realProofs.find(_.image == su.proposition).map { proof => + val provenSchnorr = proof.uncheckedTree.asInstanceOf[UncheckedSchnorr] + provenSchnorr.secondMessage + }.getOrElse { + val bs = secureRandomBytes(32) + SecondDLogProverMessage(new BigInteger(1, bs).mod(CryptoConstants.groupOrder)) + } + } UncheckedSchnorr(su.proposition, None, su.challengeOpt.get, z) + // If the node is a leaf marked "real", compute its response according to the second prover step + // of the Sigma-protocol given the commitment, challenge, and witness, or pull response from the hints bag case dhu: UnprovenDiffieHellmanTuple if dhu.real => - assert(dhu.challengeOpt.isDefined) - val privKey = secrets + assert(dhu.challengeOpt.isDefined, s"Real UnprovenDiffieHellmanTuple $dhu should have challenge defined") + val privKeyOpt = secrets .filter(_.isInstanceOf[DiffieHellmanTupleProverInput]) .find(_.asInstanceOf[DiffieHellmanTupleProverInput].publicImage == dhu.proposition) - .get.asInstanceOf[DiffieHellmanTupleProverInput] - val z = DiffieHellmanTupleInteractiveProver.secondMessage(privKey, dhu.randomnessOpt.get, dhu.challengeOpt.get) + + val z = privKeyOpt match { + case Some(privKey) => + hintsBag.ownCommitments.find(_.image == dhu.proposition).map { oc => + DiffieHellmanTupleInteractiveProver.secondMessage( + privKey.asInstanceOf[DiffieHellmanTupleProverInput], + oc.secretRandomness, + dhu.challengeOpt.get) + }.getOrElse { + DiffieHellmanTupleInteractiveProver.secondMessage( + privKey.asInstanceOf[DiffieHellmanTupleProverInput], + dhu.randomnessOpt.get, + dhu.challengeOpt.get) + } + + case None => + hintsBag.realProofs.find(_.image == dhu.proposition).map { proof => + val provenSchnorr = proof.uncheckedTree.asInstanceOf[UncheckedDiffieHellmanTuple] + provenSchnorr.secondMessage + }.getOrElse { + val bs = secureRandomBytes(32) + SecondDiffieHellmanTupleProverMessage(new BigInteger(1, bs).mod(CryptoConstants.groupOrder)) + } + } UncheckedDiffieHellmanTuple(dhu.proposition, None, dhu.challengeOpt.get, z) + // if the simulated node is proven by someone else, take it from hints bag + case su: UnprovenLeaf if su.simulated => + hintsBag.simulatedProofs.find(_.image == su.proposition).map { proof => + proof.uncheckedTree + }.getOrElse(su) case sn: UncheckedSchnorr => sn @@ -497,6 +531,8 @@ trait ProverInterpreter extends Interpreter with AttributionCore { CThresholdUncheckedNode(t.challengeOpt.get, t.children.map(convertToUnchecked), t.k, t.polynomialOpt) case s: UncheckedSchnorr => s case d: UncheckedDiffieHellmanTuple => d - case _ => ??? + case a: Any => + error(s"Cannot convertToUnproven($a)") } + } diff --git a/sigmastate/src/main/scala/sigmastate/interpreter/ProverResult.scala b/sigmastate/src/main/scala/sigmastate/interpreter/ProverResult.scala new file mode 100644 index 0000000000..99b79d442b --- /dev/null +++ b/sigmastate/src/main/scala/sigmastate/interpreter/ProverResult.scala @@ -0,0 +1,55 @@ +package sigmastate.interpreter + +import java.util + +import scorex.util.encode.Base16 +import sigmastate.Values.Idn +import sigmastate.serialization.SigmaSerializer +import sigmastate.utils.{SigmaByteReader, SigmaByteWriter} + +/** + * Proof of correctness of tx spending + * + * @param proof - proof that satisfies final sigma proposition + * @param extension - user-defined variables to be put into context + */ +class ProverResult(val proof: Array[Byte], val extension: ContextExtension) { + override def hashCode(): Int = util.Arrays.hashCode(proof) * 31 + extension.hashCode() + + override def equals(obj: scala.Any): Boolean = obj match { + case obj: ProverResult => + util.Arrays.equals(proof, obj.proof) && extension == obj.extension + case _ => false + } + + override def toString: Idn = s"ProverResult(${Base16.encode(proof)},$extension)" +} + +object ProverResult { + val empty: ProverResult = ProverResult(Array[Byte](), ContextExtension.empty) + + def apply(proof: Array[Byte], extension: ContextExtension): ProverResult = + new ProverResult(proof, extension) + + object serializer extends SigmaSerializer[ProverResult, ProverResult] { + + override def serialize(obj: ProverResult, w: SigmaByteWriter): Unit = { + w.putUShort(obj.proof.length) + w.putBytes(obj.proof) + ContextExtension.serializer.serialize(obj.extension, w) + } + + override def parse(r: SigmaByteReader): ProverResult = { + val sigBytesCount = r.getUShort() + val proofBytes = r.getBytes(sigBytesCount) + val ce = ContextExtension.serializer.parse(r) + ProverResult(proofBytes, ce) + } + } + +} + + +case class CostedProverResult(override val proof: Array[Byte], + override val extension: ContextExtension, + cost: Long) extends ProverResult(proof, extension) diff --git a/sigmastate/src/main/scala/sigmastate/interpreter/ProverUtils.scala b/sigmastate/src/main/scala/sigmastate/interpreter/ProverUtils.scala new file mode 100644 index 0000000000..40d057bf4d --- /dev/null +++ b/sigmastate/src/main/scala/sigmastate/interpreter/ProverUtils.scala @@ -0,0 +1,66 @@ +package sigmastate.interpreter + +import sigmastate.{ProofTree, SigSerializer, UncheckedConjecture, UncheckedLeaf, UncheckedSigmaTree} +import sigmastate.Values.{ErgoTree, SigmaBoolean} +import sigmastate.basics.VerifierMessage.Challenge + + +trait ProverUtils extends Interpreter { + + /** + * A method which is extracting partial proofs of secret knowledge for particular secrets with their + * respective public images given. Useful for distributed signature applications. + * + * See DistributedSigSpecification for examples of usage. + * + * @param context - context used to reduce the proposition + * @param exp - proposition to reduce + * @param proof - proof for reduced proposition + * @param realSecretsToExtract - public keys of secrets with real proofs + * @param simulatedSecretsToExtract - public keys of secrets with simulated proofs + * @return - bag of OtherSecretProven and OtherCommitment hints + */ + def bagForMultisig(context: CTX, + exp: ErgoTree, + proof: Array[Byte], + realSecretsToExtract: Seq[SigmaBoolean], + simulatedSecretsToExtract: Seq[SigmaBoolean] = Seq.empty): HintsBag = { + + val reducedTree = fullReduction(exp, context, Interpreter.emptyEnv)._1 + + val ut = SigSerializer.parseAndComputeChallenges(reducedTree, proof) + val proofTree = computeCommitments(ut).get.asInstanceOf[UncheckedSigmaTree] + + def traverseNode(tree: ProofTree, + realPropositions: Seq[SigmaBoolean], + simulatedPropositions: Seq[SigmaBoolean], + hintsBag: HintsBag): HintsBag = { + tree match { + case inner: UncheckedConjecture => + inner.children.foldLeft(hintsBag) { case (hb, c) => + traverseNode(c, realPropositions, simulatedPropositions, hb) + } + case leaf: UncheckedLeaf[_] => + val realFound = realPropositions.contains(leaf.proposition) + val simulatedFound = simulatedPropositions.contains(leaf.proposition) + if (realFound || simulatedFound) { + val hints = if (realFound) { + Seq( + RealCommitment(leaf.proposition, leaf.commitmentOpt.get), + RealSecretProof(leaf.proposition, Challenge @@ leaf.challenge, leaf) + ) + } else { + Seq( + SimulatedCommitment(leaf.proposition, leaf.commitmentOpt.get), + SimulatedSecretProof(leaf.proposition, Challenge @@ leaf.challenge, leaf) + ) + } + hintsBag.addHints(hints: _*) + } else hintsBag + } + } + + traverseNode(proofTree, realSecretsToExtract, simulatedSecretsToExtract, HintsBag.empty) + } + +} diff --git a/sigmastate/src/main/scala/sigmastate/serialization/OpCodes.scala b/sigmastate/src/main/scala/sigmastate/serialization/OpCodes.scala index ef9d5fe068..852507db00 100644 --- a/sigmastate/src/main/scala/sigmastate/serialization/OpCodes.scala +++ b/sigmastate/src/main/scala/sigmastate/serialization/OpCodes.scala @@ -155,7 +155,7 @@ object OpCodes extends ValueCodes { val CalcBlake2b256Code : OpCode = newOpCode(91) val CalcSha256Code : OpCode = newOpCode(92) val ProveDlogCode : OpCode = newOpCode(93) - val ProveDHTupleCode : OpCode = newOpCode(94) + val ProveDiffieHellmanTupleCode: OpCode = newOpCode(94) val SigmaPropIsProvenCode : OpCode = newOpCode(95) val SigmaPropBytesCode : OpCode = newOpCode(96) val BoolToSigmaPropCode : OpCode = newOpCode(97) diff --git a/sigmastate/src/main/scala/sigmastate/trees.scala b/sigmastate/src/main/scala/sigmastate/trees.scala index 800c1525e2..8b88870cc9 100644 --- a/sigmastate/src/main/scala/sigmastate/trees.scala +++ b/sigmastate/src/main/scala/sigmastate/trees.scala @@ -76,7 +76,7 @@ case class CTHRESHOLD(k: Int, sigmaBooleans: Seq[SigmaBoolean]) extends SigmaBoo override val opCode: OpCode = OpCodes.AtLeastCode } -trait SigmaProofOfKnowledgeTree[SP <: SigmaProtocol[SP], S <: SigmaProtocolPrivateInput[SP, _]] +trait SigmaProofOfKnowledgeLeaf[SP <: SigmaProtocol[SP], S <: SigmaProtocolPrivateInput[SP, _]] extends SigmaBoolean with SigmaProtocolCommonInput[SP] /** Represents boolean values (true/false) in SigmaBoolean tree. @@ -154,7 +154,7 @@ case class CreateProveDHTuple(gv: Value[SGroupElement.type], override def opType = SFunc(IndexedSeq(SGroupElement, SGroupElement, SGroupElement, SGroupElement), SSigmaProp) } object CreateProveDHTuple extends ValueCompanion { - override def opCode: OpCode = OpCodes.ProveDHTupleCode + override def opCode: OpCode = OpCodes.ProveDiffieHellmanTupleCode } trait SigmaTransformer[IV <: SigmaPropValue, OV <: SigmaPropValue] extends SigmaPropValue { diff --git a/sigmastate/src/main/scala/sigmastate/utils/Helpers.scala b/sigmastate/src/main/scala/sigmastate/utils/Helpers.scala index 83b57a864c..cb79246333 100644 --- a/sigmastate/src/main/scala/sigmastate/utils/Helpers.scala +++ b/sigmastate/src/main/scala/sigmastate/utils/Helpers.scala @@ -57,7 +57,6 @@ object Helpers { def concatArrays[T:ClassTag](arr1: Array[T], arr2: Array[T]): Array[T] = { val length: Int = arr1.length + arr2.length val result: Array[T] = new Array[T](length) - var pos: Int = 0 System.arraycopy(arr1, 0, result, 0, arr1.length) System.arraycopy(arr2, 0, result, arr1.length, arr2.length) result diff --git a/sigmastate/src/test/scala/sigmastate/FailingToProveSpec.scala b/sigmastate/src/test/scala/sigmastate/FailingToProveSpec.scala index 475e52f020..ac846b2b6c 100644 --- a/sigmastate/src/test/scala/sigmastate/FailingToProveSpec.scala +++ b/sigmastate/src/test/scala/sigmastate/FailingToProveSpec.scala @@ -1,5 +1,7 @@ package sigmastate +import org.ergoplatform.{ErgoBox, ErgoLikeContext} +import sigmastate.helpers.{ContextEnrichingTestProvingInterpreter, ErgoLikeTestInterpreter, SigmaTestingCommons} import org.ergoplatform.{ErgoBox, ErgoLikeContext, ErgoLikeInterpreter, ErgoLikeTransaction} import sigmastate.helpers.{ContextEnrichingTestProvingInterpreter, ErgoLikeContextTesting, ErgoLikeTestInterpreter, SigmaTestingCommons} import sigmastate.lang.Terms._ diff --git a/sigmastate/src/test/scala/sigmastate/crypto/GroupLawsSpecification.scala b/sigmastate/src/test/scala/sigmastate/crypto/GroupLawsSpecification.scala index f2cd2ca808..b77d3c5b0a 100644 --- a/sigmastate/src/test/scala/sigmastate/crypto/GroupLawsSpecification.scala +++ b/sigmastate/src/test/scala/sigmastate/crypto/GroupLawsSpecification.scala @@ -2,33 +2,56 @@ package sigmastate.crypto import java.math.BigInteger +import org.scalacheck.Gen import sigmastate.helpers.SigmaTestingCommons import sigmastate.interpreter.CryptoConstants +import sigmastate.interpreter.CryptoConstants.EcPointType + +import scala.util.Random class GroupLawsSpecification extends SigmaTestingCommons { private val group = CryptoConstants.dlogGroup - property("multiplication law is complete") { - val identity = group.identity - val ge = group.createRandomGenerator() - - group.multiplyGroupElements(ge, ge) shouldBe group.exponentiate(ge, new BigInteger("2")) - group.multiplyGroupElements(ge, identity) shouldBe group.exponentiate(ge, BigInteger.ONE) - group.multiplyGroupElements(ge, identity) shouldBe ge - group.multiplyGroupElements(identity, identity) shouldBe identity + val groupElementGen: Gen[EcPointType] = Gen.const(group.createRandomElement()) + val groupGeneratorGen: Gen[EcPointType] = Gen.const(group.createRandomGenerator()) + val bigIntGen: Gen[BigInteger] = Gen.const{ + val bytes = Array.fill(32)(Random.nextInt(Byte.MaxValue).toByte) + new BigInteger(1, bytes).mod(group.order) + } - val inverse = group.getInverse(ge) - group.multiplyGroupElements(ge, inverse) shouldBe identity + property("multiplication law is complete") { + forAll(groupElementGen) { ge => + val identity = group.identity + group.multiplyGroupElements(ge, ge) shouldBe group.exponentiate(ge, new BigInteger("2")) + group.multiplyGroupElements(ge, identity) shouldBe group.exponentiate(ge, BigInteger.ONE) + group.multiplyGroupElements(ge, identity) shouldBe ge + group.multiplyGroupElements(identity, identity) shouldBe identity + + val inverse = group.inverseOf(ge) + group.multiplyGroupElements(ge, inverse) shouldBe identity + } } property("exponentiation") { val identity = group.identity - val ge = group.createRandomGenerator() + forAll(groupElementGen) { ge => + group.exponentiate(ge, BigInteger.ZERO) shouldBe identity + group.exponentiate(ge, BigInteger.ONE) shouldBe ge + group.exponentiate(ge, group.order) shouldBe identity + group.exponentiate(ge, group.order.add(BigInteger.ONE)) shouldBe ge + } + } + + property("double inverse") { + forAll(groupElementGen) { ge => + group.inverseOf(group.inverseOf(ge)) shouldBe ge + } + } - group.exponentiate(ge, BigInteger.ZERO) shouldBe identity - group.exponentiate(ge, BigInteger.ONE) shouldBe ge - group.exponentiate(ge, group.order) shouldBe identity - group.exponentiate(ge, group.order.add(BigInteger.ONE)) shouldBe ge + property("precomputed"){ + forAll(groupGeneratorGen, bigIntGen) { case (base, exp) => + group.exponentiateWithPreComputedValues(base, exp) shouldBe group.exponentiate(base, exp) + } } } diff --git a/sigmastate/src/test/scala/sigmastate/helpers/ContextEnrichingProverInterpreter.scala b/sigmastate/src/test/scala/sigmastate/helpers/ContextEnrichingProverInterpreter.scala index ccd8c0d467..f88d30ddcc 100644 --- a/sigmastate/src/test/scala/sigmastate/helpers/ContextEnrichingProverInterpreter.scala +++ b/sigmastate/src/test/scala/sigmastate/helpers/ContextEnrichingProverInterpreter.scala @@ -3,7 +3,7 @@ package sigmastate.helpers import sigmastate.SType import sigmastate.Values.{ErgoTree, EvaluatedValue} import sigmastate.interpreter.Interpreter.ScriptEnv -import sigmastate.interpreter.{ContextExtension, CostedProverResult, ProverInterpreter} +import sigmastate.interpreter.{ContextExtension, CostedProverResult, HintsBag, ProverInterpreter} import scala.util.Try @@ -24,7 +24,7 @@ trait ContextEnrichingProverInterpreter extends ProverInterpreter { /** * Replace context.extension to knownExtensions and prove script in different context. */ - override def prove(env: ScriptEnv, exp: ErgoTree, context: CTX, message: Array[Byte]): Try[CostedProverResult] = { + override def prove(env: ScriptEnv, exp: ErgoTree, context: CTX, message: Array[Byte], hintsBag: HintsBag): Try[CostedProverResult] = { val enrichedContext = context.withExtension(knownExtensions).asInstanceOf[CTX] super.prove(env, exp, enrichedContext, message) } diff --git a/sigmastate/src/test/scala/sigmastate/utxo/ComplexSigSpecification.scala b/sigmastate/src/test/scala/sigmastate/utxo/ComplexSigSpecification.scala index e11bcb6969..2a88e05826 100644 --- a/sigmastate/src/test/scala/sigmastate/utxo/ComplexSigSpecification.scala +++ b/sigmastate/src/test/scala/sigmastate/utxo/ComplexSigSpecification.scala @@ -1,6 +1,6 @@ package sigmastate.utxo -import org.ergoplatform.{ErgoLikeContext, ErgoLikeTransaction, Height} +import org.ergoplatform.Height import org.scalacheck.Gen import sigmastate.Values.IntConstant import sigmastate._ @@ -10,7 +10,8 @@ import sigmastate.helpers.{ContextEnrichingTestProvingInterpreter, ErgoLikeConte import scala.util.Random class ComplexSigSpecification extends SigmaTestingCommons { - implicit lazy val IR = new TestingIRContext + implicit lazy val IR: TestingIRContext = new TestingIRContext + private def proverGen: Gen[ContextEnrichingTestProvingInterpreter] = for { _ <- Gen.const(1) } yield new ContextEnrichingTestProvingInterpreter() @@ -612,4 +613,44 @@ class ComplexSigSpecification extends SigmaTestingCommons { } } } + + property("nested thresholds") { + val prover = new ContextEnrichingTestProvingInterpreter + val verifier = new ErgoLikeTestInterpreter + + val secret1 = prover.dlogSecrets.head + val secret2 = prover.dlogSecrets(1) + val secret3 = prover.dlogSecrets(2) + val secret4 = prover.dlogSecrets(3) + + val pdlog1 = secret1.publicImage + val pdlog2 = secret2.publicImage + val pdlog3 = secret3.publicImage + val pdlog4 = secret4.publicImage + + val otherProver = new ContextEnrichingTestProvingInterpreter + + val unknownSecret1 = otherProver.dlogSecrets.head + val unknownSecret2 = otherProver.dlogSecrets(1) + val unknownSecret3 = otherProver.dlogSecrets(2) + + val unknownPdlog1 = unknownSecret1.publicImage + val unknownPdlog2 = unknownSecret2.publicImage + val unknownPdlog3 = unknownSecret3.publicImage + + val c1 = CTHRESHOLD(2, Seq(pdlog1, pdlog2, unknownPdlog1)) + val c2 = CTHRESHOLD(2, Seq(pdlog3, pdlog4, unknownPdlog2)) + val c3 = CTHRESHOLD(2, Seq(unknownPdlog1, unknownPdlog2, unknownPdlog3)) + + val prop = CTHRESHOLD(2, Seq(c1, c2, c3)) + + val ctx = fakeContext + + val pr = prover.prove(prop, ctx, fakeMessage).get + + otherProver.prove(prop, ctx, fakeMessage).isFailure shouldBe true + + verifier.verify(prop, ctx, pr, fakeMessage).isSuccess shouldBe true + } + } diff --git a/sigmastate/src/test/scala/sigmastate/utxo/DistributedSigSpecification.scala b/sigmastate/src/test/scala/sigmastate/utxo/DistributedSigSpecification.scala new file mode 100644 index 0000000000..8374057d86 --- /dev/null +++ b/sigmastate/src/test/scala/sigmastate/utxo/DistributedSigSpecification.scala @@ -0,0 +1,415 @@ +package sigmastate.utxo + +import sigmastate._ +import sigmastate.basics.DLogProtocol.DLogInteractiveProver +import sigmastate.basics.DiffieHellmanTupleInteractiveProver +import sigmastate.helpers.{ContextEnrichingTestProvingInterpreter, ErgoLikeTestProvingInterpreter, SigmaTestingCommons} +import sigmastate.interpreter._ +import sigmastate.lang.Terms._ + +class DistributedSigSpecification extends SigmaTestingCommons { + + implicit lazy val IR: TestingIRContext = new TestingIRContext + + private val ctx = fakeContext + + /** + * An example test where Alice (A) and Bob (B) are signing an input in a distributed way. A statement which + * protects the box to spend is "pubkey_Alice && pubkey_Bob". Note that a signature in this case is about + * a transcript of a Sigma-protocol ((a_Alice, a_Bob), e, (z_Alice, z_Bob)), + * which is done in non-interactive way (thus "e" is got via a Fiat-Shamir transformation). + * + * For that, they are going through following steps: + * + * - Bob is generating first protocol message a_Bob and sends it to Alice + * - Alice forms a hint which contain Bob's commitment "a_Bob", and puts the hint into a hints bag + * - She proves the statement using the bag, getting the partial protocol transcript + * (a_Alice, e, z_Alice) as a result and sends "a_Alice" and "z_Alice" to Bob. + * Please note that "e" is got from both a_Alice and a_Bob. + * + * - Bob now also knows a_Alice, so can generate the same "e" as Alice. Thus Bob is generating valid + * proof ((a_Alice, a_Bob), e, (z_Alice, z_Bob)). + */ + property("distributed AND (2 out of 2)") { + val proverA = new ErgoLikeTestProvingInterpreter + val proverB = new ErgoLikeTestProvingInterpreter + val verifier: ContextEnrichingTestProvingInterpreter = new ContextEnrichingTestProvingInterpreter + + val pubkeyAlice = proverA.dlogSecrets.head.publicImage + val pubkeyBob = proverB.dlogSecrets.head.publicImage + + val env = Map("pubkeyA" -> pubkeyAlice, "pubkeyB" -> pubkeyBob) + val prop: Values.Value[SSigmaProp.type] = compile(env, """pubkeyA && pubkeyB""").asSigmaProp + + val (rBob, aBob) = DLogInteractiveProver.firstMessage() + + val hintFromBob: Hint = RealCommitment(pubkeyBob, aBob) + val bagA = HintsBag(Seq(hintFromBob)) + + val proofAlice = proverA.prove(prop, ctx, fakeMessage, bagA).get + + val bagB = proverB.bagForMultisig(ctx, prop, proofAlice.proof, Seq(pubkeyAlice)) + .addHint(OwnCommitment(pubkeyBob, rBob, aBob)) + + 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 + + // Compound proof from Bob is correct + verifier.verify(prop, ctx, proofBob, fakeMessage).get._1 shouldBe true + } + + /** + * 3-out-of-3 AND signature + */ + property("distributed AND (3 out of 3)") { + val proverA = new ErgoLikeTestProvingInterpreter + val proverB = new ErgoLikeTestProvingInterpreter + val proverC = new ErgoLikeTestProvingInterpreter + val verifier: ContextEnrichingTestProvingInterpreter = new ContextEnrichingTestProvingInterpreter + + val pubkeyAlice = proverA.dlogSecrets.head.publicImage + val pubkeyBob = proverB.dlogSecrets.head.publicImage + val pubkeyCarol = proverC.dlogSecrets.head.publicImage + + val env = Map("pubkeyA" -> pubkeyAlice, "pubkeyB" -> pubkeyBob, "pubkeyC" -> pubkeyCarol) + val prop: Values.Value[SSigmaProp.type] = compile(env, """pubkeyA && pubkeyB && pubkeyC""").asSigmaProp + + val (rBob, aBob) = DLogInteractiveProver.firstMessage() + val (rCarol, aCarol) = DLogInteractiveProver.firstMessage() + + val dlBKnown: Hint = RealCommitment(pubkeyBob, aBob) + val dlCKnown: Hint = RealCommitment(pubkeyCarol, aCarol) + val bagA = HintsBag(Seq(dlBKnown, dlCKnown)) + + val proofAlice = proverA.prove(prop, ctx, fakeMessage, bagA).get + + val bagC = proverB.bagForMultisig(ctx, prop, proofAlice.proof, Seq(pubkeyAlice)) + .addHint(OwnCommitment(pubkeyCarol, rCarol, aCarol)) + .addHint(dlBKnown) + + val proofCarol = proverC.prove(prop, ctx, fakeMessage, bagC).get + + val bagB = proverB.bagForMultisig(ctx, prop, proofCarol.proof, Seq(pubkeyAlice, pubkeyCarol)) + .addHint(OwnCommitment(pubkeyBob, rBob, aBob)) + + 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 + verifier.verify(prop, ctx, proofCarol, fakeMessage).get._1 shouldBe false + + // Compound proof from Bob is correct + verifier.verify(prop, ctx, proofBob, fakeMessage).get._1 shouldBe true + } + + + /** + * An example test where Alice (A), Bob (B) and Carol (C) are signing in a distributed way an input, which is + * protected by 2-out-of-3 threshold multi-signature. + * + * A statement which protects the box to spend is "atLeast(2, Coll(pubkeyA, pubkeyB, pubkeyC))". + * + * A scheme for multisigning is following: + * + * - Bob is generating first protocol message (commitment to his randomness) "a" and sends it to Alice + * - Alice is generating her proof having Bob's "a" as a hint. She then puts Bob's randomness and fill his + * response with zero bits. Thus Alice's signature is not valid. Alice is sending her signature to Bob. + * - Bob is extracting Alice's commitment to randomness and response, and also Carol's commitment and response. + * He's using his randomness from his first step and completes the (valid) signature. + */ + property("distributed THRESHOLD - 2 out of 3") { + val proverA = new ErgoLikeTestProvingInterpreter + val proverB = new ErgoLikeTestProvingInterpreter + val proverC = new ErgoLikeTestProvingInterpreter + val verifier = new ContextEnrichingTestProvingInterpreter + + val pubkeyAlice = proverA.dlogSecrets.head.publicImage + val pubkeyBob = proverB.dlogSecrets.head.publicImage + val pubkeyCarol = proverC.dlogSecrets.head.publicImage + + val env = Map("pubkeyA" -> pubkeyAlice, "pubkeyB" -> pubkeyBob, "pubkeyC" -> pubkeyCarol) + val prop = compile(env, """atLeast(2, Coll(pubkeyA, pubkeyB, pubkeyC))""").asSigmaProp + + val (rBob, aBob) = DLogInteractiveProver.firstMessage() + val dlBKnown: Hint = RealCommitment(pubkeyBob, aBob) + + val bagA = HintsBag(Seq(dlBKnown)) + val proofAlice = proverA.prove(prop, ctx, fakeMessage, bagA).get + + val bagB = proverB.bagForMultisig(ctx, prop, proofAlice.proof, Seq(pubkeyAlice), Seq(pubkeyCarol)) + .addHint(OwnCommitment(pubkeyBob, rBob, aBob)) + + 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 + + // Compound proof from Bob is correct + verifier.verify(prop, ctx, proofBob, fakeMessage).get._1 shouldBe true + } + + /** + * Distributed threshold signature, 3 out of 4 case. + */ + property("distributed THRESHOLD - 3 out of 4") { + val proverA = new ErgoLikeTestProvingInterpreter + val proverB = new ErgoLikeTestProvingInterpreter + val proverC = new ErgoLikeTestProvingInterpreter + val proverD = new ErgoLikeTestProvingInterpreter + val verifier = new ContextEnrichingTestProvingInterpreter + + val pubkeyAlice = proverA.dlogSecrets.head.publicImage + val pubkeyBob = proverB.dlogSecrets.head.publicImage + val pubkeyCarol = proverC.dlogSecrets.head.publicImage + val pubkeyDave = proverD.dlogSecrets.head.publicImage + + val env = Map("pubkeyA" -> pubkeyAlice, "pubkeyB" -> pubkeyBob, "pubkeyC" -> pubkeyCarol, "pubkeyD" -> pubkeyDave) + val prop = compile(env, """atLeast(3, Coll(pubkeyA, pubkeyB, pubkeyC, pubkeyD))""").asSigmaProp + + //Alice, Bob and Carol are signing + val (rBob, aBob) = DLogInteractiveProver.firstMessage() + val dlBKnown: Hint = RealCommitment(pubkeyBob, aBob) + + val (rCarol, aCarol) = DLogInteractiveProver.firstMessage() + val dlCKnown: Hint = RealCommitment(pubkeyCarol, aCarol) + + val bagA = HintsBag(Seq(dlBKnown, dlCKnown)) + val proofAlice = proverA.prove(prop, ctx, fakeMessage, bagA).get + + val bagC = proverC.bagForMultisig(ctx, prop, proofAlice.proof, Seq(pubkeyAlice), Seq(pubkeyDave)) ++ + HintsBag(Seq(dlBKnown, OwnCommitment(pubkeyCarol, rCarol, aCarol))) + val proofCarol = proverC.prove(prop, ctx, fakeMessage, bagC).get + + val bagB = (proverB.bagForMultisig(ctx, prop, proofAlice.proof, Seq(pubkeyAlice), Seq(pubkeyDave)) ++ + proverB.bagForMultisig(ctx, prop, proofCarol.proof, Seq(pubkeyCarol))) + .addHint(OwnCommitment(pubkeyBob, rBob, aBob)) + + 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 + verifier.verify(prop, ctx, proofCarol, fakeMessage).get._1 shouldBe false + + // Compound proof from Bob is correct + verifier.verify(prop, ctx, proofBob, fakeMessage).get._1 shouldBe true + } + + /** + * Distributed threshold signature, 3 out of 4 case, 1 real and 1 simulated secrets are of DH kind. + */ + property("distributed THRESHOLD - 3 out of 4 - w. DH") { + val proverA = new ErgoLikeTestProvingInterpreter + val proverB = new ErgoLikeTestProvingInterpreter + val proverC = new ErgoLikeTestProvingInterpreter + val proverD = new ErgoLikeTestProvingInterpreter + val verifier = new ContextEnrichingTestProvingInterpreter + + val pubkeyAlice = proverA.dlogSecrets.head.publicImage + val pubkeyBob = proverB.dhSecrets.head.publicImage + val pubkeyCarol = proverC.dhSecrets.head.publicImage + val pubkeyDave = proverD.dhSecrets.head.publicImage + + val env = Map("pubkeyA" -> pubkeyAlice, "pubkeyB" -> pubkeyBob, "pubkeyC" -> pubkeyCarol, "pubkeyD" -> pubkeyDave) + val prop = compile(env, """atLeast(3, Coll(pubkeyA, pubkeyB, pubkeyC, pubkeyD))""").asSigmaProp + + // Alice, Bob and Carol are signing + val (rBob, aBob) = DiffieHellmanTupleInteractiveProver.firstMessage(pubkeyBob) + val dlBKnown: Hint = RealCommitment(pubkeyBob, aBob) + + val (rCarol, aCarol) = DiffieHellmanTupleInteractiveProver.firstMessage(pubkeyCarol) + val dlCKnown: Hint = RealCommitment(pubkeyCarol, aCarol) + + val bagA = HintsBag(Seq(dlBKnown, dlCKnown)) + val proofAlice = proverA.prove(prop, ctx, fakeMessage, bagA).get + + val bagC = proverC.bagForMultisig(ctx, prop, proofAlice.proof, Seq(pubkeyAlice), Seq(pubkeyDave)) ++ + HintsBag(Seq(dlBKnown, OwnCommitment(pubkeyCarol, rCarol, aCarol))) + val proofCarol = proverC.prove(prop, ctx, fakeMessage, bagC).get + + val bagB = (proverB.bagForMultisig(ctx, prop, proofAlice.proof, Seq(pubkeyAlice), Seq(pubkeyDave)) ++ + proverB.bagForMultisig(ctx, prop, proofCarol.proof, Seq(pubkeyCarol))) + .addHint(OwnCommitment(pubkeyBob, rBob, aBob)) + + 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 + + // Compound proof from Bob is correct + verifier.verify(prop, ctx, proofBob, fakeMessage).get._1 shouldBe true + } + + property("distributed THRESHOLD - 2 out of 5") { + + val proverA = new ErgoLikeTestProvingInterpreter + val proverB = new ErgoLikeTestProvingInterpreter + val proverC = new ErgoLikeTestProvingInterpreter + val proverD = new ErgoLikeTestProvingInterpreter + val proverE = new ErgoLikeTestProvingInterpreter + val verifier = new ContextEnrichingTestProvingInterpreter + + val pubkeyAlice = proverA.dlogSecrets.head.publicImage + val pubkeyBob = proverB.dlogSecrets.head.publicImage + val pubkeyCarol = proverC.dlogSecrets.head.publicImage + val pubkeyDave = proverD.dlogSecrets.head.publicImage + val pubkeyEmma = proverE.dlogSecrets.head.publicImage + + val env = Map("pubkeyA" -> pubkeyAlice, "pubkeyB" -> pubkeyBob, "pubkeyC" -> pubkeyCarol, + "pubkeyD" -> pubkeyDave, "pubkeyE" -> pubkeyEmma) + val prop = compile(env, """atLeast(2, Coll(pubkeyA, pubkeyB, pubkeyC, pubkeyD, pubkeyE))""").asSigmaProp + + //Alice and Dave are signing + val (rDave, aDave) = DLogInteractiveProver.firstMessage() + val dlDKnown: Hint = RealCommitment(pubkeyDave, aDave) + + val bagA = HintsBag(Seq(dlDKnown)) + val proofAlice = proverA.prove(prop, ctx, fakeMessage, bagA).get + + // Proof generated by Alice without interaction w. Dave is not correct + verifier.verify(prop, ctx, proofAlice, fakeMessage).get._1 shouldBe false + + val bagD = proverD + .bagForMultisig(ctx, prop, proofAlice.proof, Seq(pubkeyAlice), Seq(pubkeyBob, pubkeyCarol, pubkeyEmma)) + .addHint(OwnCommitment(pubkeyDave, rDave, aDave)) + + val proofDave = proverD.prove(prop, ctx, fakeMessage, bagD).get + verifier.verify(prop, ctx, proofDave, fakeMessage).get._1 shouldBe true + } + + property("distributed THRESHOLD - 4 out of 8 - DH") { + + val proverA = new ErgoLikeTestProvingInterpreter + val proverB = new ErgoLikeTestProvingInterpreter + val proverC = new ErgoLikeTestProvingInterpreter + val proverD = new ErgoLikeTestProvingInterpreter + val proverE = new ErgoLikeTestProvingInterpreter + val proverF = new ErgoLikeTestProvingInterpreter + val proverG = new ErgoLikeTestProvingInterpreter + val proverH = new ErgoLikeTestProvingInterpreter + val verifier = new ContextEnrichingTestProvingInterpreter + + val pubkeyAlice = proverA.dhSecrets.head.publicImage + val pubkeyBob = proverB.dhSecrets.head.publicImage + val pubkeyCarol = proverC.dhSecrets.head.publicImage + val pubkeyDave = proverD.dhSecrets.head.publicImage + val pubkeyEmma = proverE.dhSecrets.head.publicImage + val pubkeyFrank = proverF.dhSecrets.head.publicImage + val pubkeyGerard = proverG.dhSecrets.head.publicImage + val pubkeyHannah = proverH.dhSecrets.head.publicImage + + val env = Map("pubkeyA" -> pubkeyAlice, "pubkeyB" -> pubkeyBob, "pubkeyC" -> pubkeyCarol, + "pubkeyD" -> pubkeyDave, "pubkeyE" -> pubkeyEmma, "pubkeyF" -> pubkeyFrank, + "pubkeyG" -> pubkeyGerard, "pubkeyH" -> pubkeyHannah) + val script = """atLeast(4, Coll(pubkeyA, pubkeyB, pubkeyC, pubkeyD, pubkeyE, pubkeyF, pubkeyG, pubkeyH))""" + val prop = compile(env, script).asSigmaProp + + //Alice, Bob, Gerard, and Hannah are signing, others are simulated + + //first, commitments are needed from real signers + val (rAlice, aAlice) = DiffieHellmanTupleInteractiveProver.firstMessage(pubkeyAlice) + val dlAKnown: Hint = RealCommitment(pubkeyAlice, aAlice) + val secretCmtA = OwnCommitment(pubkeyAlice, rAlice, aAlice) + + val (rBob, aBob) = DiffieHellmanTupleInteractiveProver.firstMessage(pubkeyBob) + val dlBKnown: Hint = RealCommitment(pubkeyBob, aBob) + val secretCmtB = OwnCommitment(pubkeyBob, rBob, aBob) + + val (rGerard, aGerard) = DiffieHellmanTupleInteractiveProver.firstMessage(pubkeyGerard) + val dlGKnown: Hint = RealCommitment(pubkeyGerard, aGerard) + val secretCmtG = OwnCommitment(pubkeyGerard, rGerard, aGerard) + + val (rHannah, aHannah) = DiffieHellmanTupleInteractiveProver.firstMessage(pubkeyHannah) + val secretCmtH = OwnCommitment(pubkeyHannah, rHannah, aHannah) + + val bagH = HintsBag(Seq(dlAKnown, dlBKnown, dlGKnown, secretCmtH)) + val proofHannah = proverH.prove(prop, ctx, fakeMessage, bagH).get + + // Proof generated by Hannah only is not correct + verifier.verify(prop, ctx, proofHannah, fakeMessage).get._1 shouldBe false + + //hints after the first real proof done. + val bag1 = proverH + .bagForMultisig(ctx, prop, proofHannah.proof, + Seq(pubkeyHannah), + Seq(pubkeyCarol, pubkeyDave, pubkeyEmma, pubkeyFrank)) + + //now real proofs can be done in any order + val bagB = bag1.addHints(OwnCommitment(pubkeyBob, rBob, aBob), dlAKnown, dlGKnown) + val proofBob = proverB.prove(prop, ctx, fakeMessage, bagB).get + val partialBobProofBag = proverB.bagForMultisig(ctx, prop, proofBob.proof, Seq(pubkeyBob), Seq.empty).realProofs.head + + val bagA = bag1.addHints(OwnCommitment(pubkeyAlice, rAlice, aAlice), dlBKnown, dlGKnown) + val proofAlice = proverA.prove(prop, ctx, fakeMessage, bagA).get + val partialAliceProofBag = proverA.bagForMultisig(ctx, prop, proofAlice.proof, Seq(pubkeyAlice), Seq.empty).realProofs.head + + val bagG = bag1.addHints(OwnCommitment(pubkeyGerard, rGerard, aGerard), dlAKnown, dlBKnown) + val proofGerard = proverG.prove(prop, ctx, fakeMessage, bagG).get + val partialGerardProofBag = proverG.bagForMultisig(ctx, prop, proofGerard.proof, Seq(pubkeyGerard), Seq.empty).realProofs.head + + val bag = bag1 + .addHints(partialAliceProofBag, partialBobProofBag, partialGerardProofBag) + .addHints(dlAKnown, dlBKnown, dlGKnown) + + val validProofA = proverA.prove(prop, ctx, fakeMessage, bag.addHint(secretCmtA)).get + verifier.verify(prop, ctx, validProofA, fakeMessage).get._1 shouldBe true + + val validProofB = proverB.prove(prop, ctx, fakeMessage, bag.addHint(secretCmtB)).get + verifier.verify(prop, ctx, validProofB, fakeMessage).get._1 shouldBe true + + val validProofG = proverG.prove(prop, ctx, fakeMessage, bag.addHint(secretCmtG)).get + verifier.verify(prop, ctx, validProofG, fakeMessage).get._1 shouldBe true + + val validProofH = proverH.prove(prop, ctx, fakeMessage, bag.addHint(secretCmtH)).get + verifier.verify(prop, ctx, validProofH, fakeMessage).get._1 shouldBe true + + validProofA.proof.sameElements(validProofB.proof) shouldBe true + validProofB.proof.sameElements(validProofG.proof) shouldBe true + validProofG.proof.sameElements(validProofH.proof) shouldBe true + } + + property("distributed THRESHOLD - (1-out-of-2) and (1-out-of-2) - DLOG and DH") { + val proverA = new ErgoLikeTestProvingInterpreter + val proverB = new ErgoLikeTestProvingInterpreter + val proverC = new ErgoLikeTestProvingInterpreter + val proverD = new ErgoLikeTestProvingInterpreter + + val verifier = new ContextEnrichingTestProvingInterpreter + + val pubkeyAlice = proverA.dlogSecrets.head.publicImage + val pubkeyBob = proverB.dlogSecrets.head.publicImage + val pubkeyCarol = proverC.dhSecrets.head.publicImage + val pubkeyDave = proverD.dhSecrets.head.publicImage + + val env = Map("pubkeyA" -> pubkeyAlice, "pubkeyB" -> pubkeyBob, "pubkeyC" -> pubkeyCarol, "pubkeyD" -> pubkeyDave) + val script = """(pubkeyA || pubkeyB) && (pubkeyC || pubkeyD)""" + val prop = compile(env, script).asSigmaProp + + //Alice and Dave are signing + + //first, commitments are needed from real signers + val (rAlice, aAlice) = DLogInteractiveProver.firstMessage() + val secretCmtA = OwnCommitment(pubkeyAlice, rAlice, aAlice) + + val (rDave, aDave) = DiffieHellmanTupleInteractiveProver.firstMessage(pubkeyDave) + val dlDKnown: Hint = RealCommitment(pubkeyDave, aDave) + val secretCmtD = OwnCommitment(pubkeyDave, rDave, aDave) + + val bagA = HintsBag(Seq(secretCmtA, dlDKnown)) + val proofAlice = proverA.prove(prop, ctx, fakeMessage, bagA).get + // Proof generated by Alice only is not correct + verifier.verify(prop, ctx, proofAlice, fakeMessage).get._1 shouldBe false + + val bag = proverA.bagForMultisig(ctx, prop, proofAlice.proof, + Seq(pubkeyAlice, pubkeyDave), Seq(pubkeyBob, pubkeyCarol)) + + val validProofD = proverD.prove(prop, ctx, fakeMessage, bag.addHint(secretCmtD)).get + verifier.verify(prop, ctx, validProofD, fakeMessage).get._1 shouldBe true + } + +} diff --git a/sigmastate/src/test/scala/sigmastate/utxo/benchmarks/CrowdFundingKernelContract.scala b/sigmastate/src/test/scala/sigmastate/utxo/benchmarks/CrowdFundingKernelContract.scala index c15933dd47..8cd0722134 100644 --- a/sigmastate/src/test/scala/sigmastate/utxo/benchmarks/CrowdFundingKernelContract.scala +++ b/sigmastate/src/test/scala/sigmastate/utxo/benchmarks/CrowdFundingKernelContract.scala @@ -8,7 +8,6 @@ import sigmastate.basics.DLogProtocol.{DLogInteractiveProver, DLogProverInput, F import sigmastate.basics.VerifierMessage.Challenge import scorex.crypto.hash.Blake2b256 import sigmastate._ -import sigmastate.lang.Terms._ import sigmastate.helpers.ContextEnrichingTestProvingInterpreter import sigmastate.interpreter.{CryptoConstants, Interpreter} import sigmastate.utils.Helpers @@ -35,7 +34,7 @@ class CrowdFundingKernelContract( assert(su.challengeOpt.isDefined) DLogInteractiveProver.simulate(su.proposition,su.challengeOpt.get).asInstanceOf[UnprovenTree] } else { - val (r, commitment) = DLogInteractiveProver.firstMessage(pubKey) + val (r, commitment) = DLogInteractiveProver.firstMessage() UnprovenSchnorr(pubKey, Some(commitment), Some(r), None, simulated = false) } @@ -77,7 +76,7 @@ class CrowdFundingKernelContract( val a = dlog.multiplyGroupElements( dlog.exponentiate(g, sn.secondMessage.z.underlying()), - dlog.getInverse(dlog.exponentiate(h, new BigInteger(1, sn.challenge)))) + dlog.inverseOf(dlog.exponentiate(h, new BigInteger(1, sn.challenge)))) val rootCommitment = FirstDLogProverMessage(a)