diff --git a/appkit/src/main/scala/org/ergoplatform/appkit/multisig/Signing.scala b/appkit/src/main/scala/org/ergoplatform/appkit/multisig/Signing.scala new file mode 100644 index 00000000..dcb3e822 --- /dev/null +++ b/appkit/src/main/scala/org/ergoplatform/appkit/multisig/Signing.scala @@ -0,0 +1,93 @@ +package org.ergoplatform.appkit.multisig + +import org.ergoplatform.P2PKAddress +import org.ergoplatform.appkit.{SigmaProp, ReducedErgoLikeTransaction, AppkitProvingInterpreter, ErgoProver, ReducedTransaction} +import scorex.util.ModifierId +import sigmastate.interpreter.Hint + +import scala.collection.mutable + +/** Participant of a multisig workflow. */ +case class Participant(address: P2PKAddress) { + +} + +case class Signature(hints: Seq[Hint]) { +} + +/** + * The SigningSession class represents a signing session for a specific transaction. + * It keeps track of collected signatures and provides methods for adding signatures and + * checking the completeness of the session. + * + * @param reducedTx the transaction being signed. + */ +class SigningSession(reducedTx: ReducedErgoLikeTransaction) { + /** Participants who can co-sign the transaction to provide enough signatures for all + * the reducedInputs. + */ + def participants: Seq[Participant] = ??? + + private var signatures: List[Signature] = List.empty + + def addSignature(signature: Signature): Unit = { + signatures = signature :: signatures + } + + def getSignatures: List[Signature] = signatures + + def isComplete: Boolean = ??? +} + +/** The SigningServer class represents a server that manages multiple signing sessions. + * It provides methods for creating and retrieving signing sessions. + */ +class SigningServer { + private val sessions = mutable.HashMap.empty[ModifierId, SigningSession] + + /** Creates a new signing session for the given transaction. + * + * @param reducedTx transaction to sign + */ + def createSession( + reducedTx: ReducedErgoLikeTransaction): SigningSession = { + val session = new SigningSession(reducedTx) + sessions.put(reducedTx.unsignedTx.id, session) + session + } + + /** Retrieves a signing session for the given transaction. + * + * @param modifierId id of the transaction to sign + */ + def getSession(modifierId: ModifierId): Option[SigningSession] = sessions.get(modifierId) +} + +/** Represents a participant in a multisig signing process. + * It provides methods for interacting with the SigningServer, such as initializing multisig sessions, + * adding partial signatures, listing collected signatures, and checking session completeness. + * + * @param server The SigningServer instance that the client interacts with. + */ +class Client(server: SigningServer, I: AppkitProvingInterpreter) { + /** Adds secrets from the given prover */ + def addSecrets(prover: ErgoProver): Unit = ??? + + def initMultisigSession(tx: ReducedErgoLikeTransaction): SigningSession = { + server.createSession(tx) + } + + def addPartialSignature(tx: ReducedErgoLikeTransaction): Unit = { + val signature: Signature = Signature(Seq.empty) + server.getSession(tx.unsignedTx.id).foreach(_.addSignature(signature)) + } + + def listSignatures(modifierId: ModifierId): List[Signature] = { + server.getSession(modifierId).map(_.getSignatures).getOrElse(List.empty) + } + + def isComplete(modifierId: ModifierId): Boolean = { + server.getSession(modifierId).exists(_.isComplete) + } +} + diff --git a/appkit/src/test/scala/org/ergoplatform/appkit/multisig/SigningSpec.scala b/appkit/src/test/scala/org/ergoplatform/appkit/multisig/SigningSpec.scala new file mode 100644 index 00000000..45aeeef2 --- /dev/null +++ b/appkit/src/test/scala/org/ergoplatform/appkit/multisig/SigningSpec.scala @@ -0,0 +1,14 @@ +package org.ergoplatform.appkit.multisig + +import org.scalatest.propspec.AnyPropSpec +import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks +import org.scalatest.matchers.should.Matchers +import org.ergoplatform.appkit.testing.AppkitTesting + +class SigningSpec extends AnyPropSpec with Matchers with ScalaCheckDrivenPropertyChecks + with AppkitTesting { + + property("Signing workflow") { + val server = new SigningServer + } +} diff --git a/common/src/main/java/org/ergoplatform/appkit/scalaapi/Utils.scala b/common/src/main/java/org/ergoplatform/appkit/scalaapi/Utils.scala index 164eafab..15358f50 100644 --- a/common/src/main/java/org/ergoplatform/appkit/scalaapi/Utils.scala +++ b/common/src/main/java/org/ergoplatform/appkit/scalaapi/Utils.scala @@ -1,5 +1,6 @@ package org.ergoplatform.appkit.scalaapi +import debox.cfor import scalan.ExactIntegral import scala.collection.mutable @@ -121,4 +122,16 @@ object Utils { def parseString(str: String): Option[A] = throw new NotImplementedError("operation is not supported") } + + implicit val byteArrayOrdering: Ordering[Array[Byte]] = new Ordering[Array[Byte]] { + override def compare(x: Array[Byte], y: Array[Byte]): Int = { + val len = math.min(x.length, y.length) + cfor(0)(_ < len, _ + 1) { i => + val cmp = (x(i) & 0xFF) - (y(i) & 0xFF) + if (cmp != 0) + return cmp + } + x.length - y.length + } + } } diff --git a/common/src/main/java/org/ergoplatform/appkit/scalaapi/package.scala b/common/src/main/java/org/ergoplatform/appkit/scalaapi/package.scala index 32ca22a2..3245e574 100644 --- a/common/src/main/java/org/ergoplatform/appkit/scalaapi/package.scala +++ b/common/src/main/java/org/ergoplatform/appkit/scalaapi/package.scala @@ -1,6 +1,8 @@ package org.ergoplatform.appkit import scalan.RType +import sigmastate.Values.{SigmaBoolean, ErgoTree, SigmaPropConstant} +import sigmastate.eval.SigmaDsl import special.sigma import special.sigma.{Header, Box, GroupElement, AvlTree, PreHeader} @@ -36,4 +38,20 @@ package object scalaapi { implicit val headerType: ErgoType[Header] = ErgoType.headerType() implicit val preHeaderType: ErgoType[sigma.PreHeader] = ErgoType.preHeaderType() + /** Extractors of SigmaBoolean value to be use in Scala pattern matching. */ + object SigmaProp { + def unapply(prop: special.sigma.SigmaProp): Option[SigmaBoolean] = Some(SigmaDsl.toSigmaBoolean(prop)) + } + + /** Extractor of SigmaBoolean from Pay-to-SigmaProp tree, which is the spedial case of the tree. */ + object Pay2SigmaProp { + def unapply(ergoTree: ErgoTree): Option[SigmaBoolean] = { + // get ErgoTree expression respecting constant segregation flag + val prop = ergoTree.toProposition(ergoTree.isConstantSegregation) + prop match { + case SigmaPropConstant(SigmaProp(sb)) => Some(sb) + case _ => None + } + } + } } diff --git a/common/src/test/scala/org/ergoplatform/appkit/scalaapi/ByteArrayOrderingSpec.scala b/common/src/test/scala/org/ergoplatform/appkit/scalaapi/ByteArrayOrderingSpec.scala new file mode 100644 index 00000000..17a70117 --- /dev/null +++ b/common/src/test/scala/org/ergoplatform/appkit/scalaapi/ByteArrayOrderingSpec.scala @@ -0,0 +1,48 @@ +package org.ergoplatform.appkit.scalaapi + +import org.scalacheck.{Gen, Arbitrary} +import org.ergoplatform.appkit.TestingBase +import org.scalacheck.Prop.{propBoolean} + + +class ByteArrayOrderingSpec extends TestingBase { + + implicit val arbitraryByteArray: Arbitrary[Array[Byte]] = Arbitrary(Gen.containerOf[Array, Byte](Arbitrary.arbByte.arbitrary)) + val ord = Utils.byteArrayOrdering + + property("byteArrayOrdering should satisfy required properties") { + forAll { (a: Array[Byte], b: Array[Byte], c: Array[Byte]) => + + + // reflexivity + propBoolean(ord.compare(a, a) == 0) + // antisymmetry + .&&((ord.compare(a, b) <= 0 && ord.compare(b, a) <= 0) ==> (a sameElements b)) + .&&((ord.compare(a, b) >= 0 && ord.compare(b, a) >= 0) ==> (a sameElements b)) + + // transitivity + .&&((ord.compare(a, b) <= 0 && ord.compare(b, c) <= 0) ==> (ord.compare(a, c) <= 0)) + .&&((ord.compare(a, b) >= 0 && ord.compare(b, c) >= 0) ==> (ord.compare(a, c) >= 0)) + } + } + + property("it should compare arrays with different lengths correctly") { + forAll { (a: Array[Byte], b: Array[Byte]) => + (a.length != b.length) ==> (ord.compare(a, b) != 0) + } + } + + property("it should compare arrays with equal elements correctly") { + forAll { (a: Array[Byte], b: Array[Byte]) => + val commonPrefix = a.zip(b).takeWhile { case (x, y) => x == y }.map(_._1) + val longer = a.length > b.length + val shorter = a.length < b.length + + (a startsWith commonPrefix) && (b startsWith commonPrefix) ==> ( + (longer && ord.compare(a, b) > 0) || + (shorter && ord.compare(a, b) < 0) || + (ord.compare(a, b) == 0) + ) + } + } +} diff --git a/lib-api/src/main/java/org/ergoplatform/appkit/MultisigAddress.java b/lib-api/src/main/java/org/ergoplatform/appkit/MultisigAddress.java new file mode 100644 index 00000000..4beef160 --- /dev/null +++ b/lib-api/src/main/java/org/ergoplatform/appkit/MultisigAddress.java @@ -0,0 +1,51 @@ +package org.ergoplatform.appkit; + +import org.ergoplatform.appkit.impl.MultisigAddressImpl; + +import java.util.List; + +/** + * An EIP-42 compliant multisig address + */ +public abstract class MultisigAddress { + + /** + * @return address for this multisig address + */ + public abstract Address getAddress(); + + /** + * @return list of participating p2pk addresses + */ + public abstract List
getParticipants(); + + /** + * @return number of signers required to sign a transaction for this address + */ + public abstract int getSignersRequiredCount(); + + /** Default value for tree header flags (0 means no flags). */ + public static byte DEFAULT_TREE_HEADER_FLAGS = (byte)0; + + /** + * constructs a k-out-of-n (threshold signature) address from the list of participants + * and the number of required signers + * + * @param signersRequired number k, signers required to sign a transaction for this address + * @param particpants list of p2pk addresses of possible signers + * @return MultisigAddress interface + */ + public static MultisigAddress buildFromParticipants(int signersRequired, List particpants, NetworkType networkType) { + return MultisigAddressImpl.fromParticipants(signersRequired, particpants, networkType, DEFAULT_TREE_HEADER_FLAGS); + } + + /** + * + * @param address multisig address to construct class for + * @return MultisigAddress if the given address is an EIP-42 compliant multisig address + * @throws IllegalArgumentException if given address is NOT an EIP-42 compliant multisig address + */ + public static MultisigAddress buildFromAddress(Address address) { + return MultisigAddressImpl.fromAddress(address); + } +} diff --git a/lib-api/src/main/java/org/ergoplatform/appkit/MultisigTransaction.java b/lib-api/src/main/java/org/ergoplatform/appkit/MultisigTransaction.java new file mode 100644 index 00000000..87bb0f40 --- /dev/null +++ b/lib-api/src/main/java/org/ergoplatform/appkit/MultisigTransaction.java @@ -0,0 +1,66 @@ +package org.ergoplatform.appkit; + +import java.util.List; + +/** + * EIP-41/EIP-11 compliant multi sig transaction + */ +public abstract class MultisigTransaction { + + /** + * @return transaction that is going to be signed + */ + abstract public ReducedTransaction getTransaction(); + + /** + * adds a new hint to this multisig transaction + * @param prover to add commitment for + */ + abstract public void addHint(ErgoProver prover); + + /** + * adds the hints not present on this instance from another multisig transaction instance + * for the same transaction. + */ + abstract public void mergeHints(MultisigTransaction other); + + /** + * adds the hints not present on this instance from the EIP-11 json + */ + abstract public void mergeHints(String json); + + /** + * @return list of participants that added a hint for the transaction + */ + abstract public List getCommitingParticipants(); + /** + * @return true if SignedTransaction can be built + */ + abstract public boolean isHintBagComplete(); + + /** + * @return the signed transaction if enough commitments are available + * @throws IllegalStateException if {@link #isHintBagComplete()} is false + */ + abstract public SignedTransaction toSignedTransaction(); + + /** + * @return EIP-11 compliant json string to transfer the partially signed transaction to the + * next participant + */ + abstract public String hintsToJson(); + + /** + * constructs a multi sig transaction from a reduced transaction + */ + public static MultisigTransaction fromTransaction(ReducedTransaction transaction) { + throw new UnsupportedOperationException(); + } + + /** + * constructs a multi sig transaction from EIP-11 json string + */ + public static MultisigTransaction fromJson(String json) { + throw new UnsupportedOperationException(); + } +} diff --git a/lib-api/src/main/java/org/ergoplatform/appkit/ReducedTransaction.java b/lib-api/src/main/java/org/ergoplatform/appkit/ReducedTransaction.java index 0f0fb2ad..6350784f 100644 --- a/lib-api/src/main/java/org/ergoplatform/appkit/ReducedTransaction.java +++ b/lib-api/src/main/java/org/ergoplatform/appkit/ReducedTransaction.java @@ -21,4 +21,9 @@ public interface ReducedTransaction extends Transaction { * Returns the serialized bytes of this transaction. */ byte[] toBytes(); + + /** + * @return tree of participants required or able to sign for the transaction + */ + SigningParticipants getSignersRequired(); } diff --git a/lib-api/src/main/java/org/ergoplatform/appkit/SigningParticipants.java b/lib-api/src/main/java/org/ergoplatform/appkit/SigningParticipants.java new file mode 100644 index 00000000..2f229b82 --- /dev/null +++ b/lib-api/src/main/java/org/ergoplatform/appkit/SigningParticipants.java @@ -0,0 +1,45 @@ +package org.ergoplatform.appkit; + +import java.util.List; + +/** + * Participants able to sign for a {@link ReducedTransaction} + */ +abstract public class SigningParticipants { + + /** + * @return true if multiple signers are needed + */ + abstract public boolean isMultisig(); + + /** + * Multisig requirement, k-out-of-n + */ + public static class MultisigRequirement extends SigningParticipants { + public final int hintsNeeded; + public final List