From 59eeaf0a1e498c34e161d1141ae9706a6275f5d4 Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Tue, 28 Feb 2023 19:27:31 +0100 Subject: [PATCH 01/11] Multisig method stubs --- .../ergoplatform/appkit/MultisigAddress.java | 52 ++++++++++++ .../appkit/MultisigTransaction.java | 80 +++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 lib-api/src/main/java/org/ergoplatform/appkit/MultisigAddress.java create mode 100644 lib-api/src/main/java/org/ergoplatform/appkit/MultisigTransaction.java 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..02b7e9c7 --- /dev/null +++ b/lib-api/src/main/java/org/ergoplatform/appkit/MultisigAddress.java @@ -0,0 +1,52 @@ +package org.ergoplatform.appkit; + +import java.util.List; + +/** + * An EIP-41 compliant multisig address + */ +public class MultisigAddress { + + /** + * @return address for this multisig address + */ + public Address getAddress() { + throw new UnsupportedOperationException(); + } + + /** + * @return list of participating p2pk addresses + */ + public List
getParticipants() { + throw new UnsupportedOperationException(); + } + + /** + * @return number of signers required to sign a transaction for this address + */ + public int getSignersRequiredCount() { + throw new UnsupportedOperationException(); + } + + /** + * constructs an N out of M address from the list of particpants and the number of required + * signers + * + * @param signersRequired number N, signers required to sign a transaction for this addres + * @param particpants list of p2pk addresses of possible signers + * @return MultisigAddress class + */ + public static MultisigAddress buildFromParticipants(int signersRequired, List
particpants) { + throw new UnsupportedOperationException(); + } + + /** + * + * @param address multisig address to construct class for + * @return MultisigAddress if the given address is an EIP-41 compliant multisig address + * @throws IllegalArgumentException if given address is not an EIP-41 compliant multisig address + */ + public static MultisigAddress buildFromAddress(Address address) { + throw new UnsupportedOperationException(); + } +} 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..a76f8d51 --- /dev/null +++ b/lib-api/src/main/java/org/ergoplatform/appkit/MultisigTransaction.java @@ -0,0 +1,80 @@ +package org.ergoplatform.appkit; + +import java.util.List; + +/** + * EIP-41/EIP-11 compliant multi sig transaction + */ +public class MultisigTransaction { + + /** + * @return transaction that is going to be signed + */ + public Transaction getTransaction() { + throw new UnsupportedOperationException(); + } + + /** + * @return multisig address this transaction was created for + */ + public MultisigAddress getMultisigAddress() { + throw new UnsupportedOperationException(); + } + + /** + * adds a new commitment to this multisig transaction + * @param prover to add commitment for + */ + public void addCommitment(ErgoProver prover) { + throw new UnsupportedOperationException(); + } + + /** + * @return list of participants that added a commitment for the transaction + */ + public List
getCommitingParticipants() { + throw new UnsupportedOperationException(); + } + + public boolean hasEnoughCommitments() { + throw new UnsupportedOperationException(); + } + + /** + * @return the signed transaction if enough commitments are available + * @throws IllegalStateException if {@link #hasEnoughCommitments()} is false + */ + public SignedTransaction toSignedTransaction() { + throw new UnsupportedOperationException(); + } + + /** + * @return EIP-11 compliant json string to transfer the partially signed transaction to the + * next particpant + */ + public String toJson() { + throw new UnsupportedOperationException(); + } + + /** + * constructs a multi sig transaction from an unsigned transaction. The first multi sig address + * in input boxes is used. + */ + public static MultisigTransaction fromTransaction(UnsignedTransaction transaction) { + throw new UnsupportedOperationException(); + } + + /** + * constructs a multi sig transaction from a reduced transaction + */ + public static MultisigTransaction fromTransaction(ReducedTransaction transaction, MultisigAddress address) { + throw new UnsupportedOperationException(); + } + + /** + * constructs a multi sig transaction from EIP-11 json string + */ + public static MultisigTransaction fromJson(String json) { + throw new UnsupportedOperationException(); + } +} From f273219ccf6f92be20335c0d6da48adbad7ff8e9 Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Sat, 4 Mar 2023 20:34:24 +0100 Subject: [PATCH 02/11] Multisig method stubs --- .../java/org/ergoplatform/appkit/MultisigTransaction.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib-api/src/main/java/org/ergoplatform/appkit/MultisigTransaction.java b/lib-api/src/main/java/org/ergoplatform/appkit/MultisigTransaction.java index a76f8d51..66215529 100644 --- a/lib-api/src/main/java/org/ergoplatform/appkit/MultisigTransaction.java +++ b/lib-api/src/main/java/org/ergoplatform/appkit/MultisigTransaction.java @@ -29,6 +29,14 @@ public void addCommitment(ErgoProver prover) { throw new UnsupportedOperationException(); } + /** + * adds the commitments not present on this instance from another multisig transaction instance + * for the same transaction. + */ + public void addCommitments(MultisigTransaction other) { + throw new UnsupportedOperationException(); + } + /** * @return list of participants that added a commitment for the transaction */ From d22792355e766b8b4f4097784e502e8bfeb641be Mon Sep 17 00:00:00 2001 From: Alexander Slesarenko Date: Sun, 19 Mar 2023 17:20:43 +0100 Subject: [PATCH 03/11] multisig: implementation of MultisigAddress --- .../appkit/scalaapi/package.scala | 18 +++++ .../ergoplatform/appkit/MultisigAddress.java | 41 +++++----- .../appkit/impl/MultisigAddressImpl.scala | 77 +++++++++++++++++++ .../appkit/impl/MultisigAddressTests.scala | 59 ++++++++++++++ 4 files changed, 174 insertions(+), 21 deletions(-) create mode 100644 lib-api/src/main/java/org/ergoplatform/appkit/impl/MultisigAddressImpl.scala create mode 100644 lib-impl/src/test/java/org/ergoplatform/appkit/impl/MultisigAddressTests.scala 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/lib-api/src/main/java/org/ergoplatform/appkit/MultisigAddress.java b/lib-api/src/main/java/org/ergoplatform/appkit/MultisigAddress.java index 02b7e9c7..652339c0 100644 --- a/lib-api/src/main/java/org/ergoplatform/appkit/MultisigAddress.java +++ b/lib-api/src/main/java/org/ergoplatform/appkit/MultisigAddress.java @@ -1,52 +1,51 @@ package org.ergoplatform.appkit; +import org.ergoplatform.appkit.impl.MultisigAddressImpl; + import java.util.List; /** - * An EIP-41 compliant multisig address + * An EIP-42 compliant multisig address */ -public class MultisigAddress { +public interface MultisigAddress { /** * @return address for this multisig address */ - public Address getAddress() { - throw new UnsupportedOperationException(); - } + Address getAddress(); /** * @return list of participating p2pk addresses */ - public List
getParticipants() { - throw new UnsupportedOperationException(); - } + List
getParticipants(); /** * @return number of signers required to sign a transaction for this address */ - public int getSignersRequiredCount() { - throw new UnsupportedOperationException(); - } + int getSignersRequiredCount(); + + /** Default value for tree header flags (0 means no flags). */ + byte DEFAULT_TREE_HEADER_FLAGS = (byte)0; /** - * constructs an N out of M address from the list of particpants and the number of required - * signers + * constructs a k-out-of-n (threshold signature) address from the list of participants + * and the number of required signers * - * @param signersRequired number N, signers required to sign a transaction for this addres + * @param signersRequired number k, signers required to sign a transaction for this address * @param particpants list of p2pk addresses of possible signers - * @return MultisigAddress class + * @return MultisigAddress interface */ - public static MultisigAddress buildFromParticipants(int signersRequired, List
particpants) { - throw new UnsupportedOperationException(); + 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-41 compliant multisig address - * @throws IllegalArgumentException if given address is not an EIP-41 compliant multisig address + * @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) { - throw new UnsupportedOperationException(); + static MultisigAddress buildFromAddress(Address address) { + return MultisigAddressImpl.fromAddress(address); } } diff --git a/lib-api/src/main/java/org/ergoplatform/appkit/impl/MultisigAddressImpl.scala b/lib-api/src/main/java/org/ergoplatform/appkit/impl/MultisigAddressImpl.scala new file mode 100644 index 00000000..ea8528dc --- /dev/null +++ b/lib-api/src/main/java/org/ergoplatform/appkit/impl/MultisigAddressImpl.scala @@ -0,0 +1,77 @@ +package org.ergoplatform.appkit.impl + +import org.ergoplatform.appkit.scalaapi.Pay2SigmaProp +import org.ergoplatform.{Pay2SAddress, P2PKAddress} +import org.ergoplatform.appkit.{Address, NetworkType, MultisigAddress} +import sigmastate.CTHRESHOLD +import sigmastate.Values.ErgoTree +import sigmastate.utils.Helpers.TryOps + +import java.util +import scala.collection.JavaConverters._ +import scala.util.Try + +case class MultisigAddressImpl( + numRequiredSigners: Int, participants: Seq[P2PKAddress], + networkType: NetworkType, treeHeaderFlags: Byte) extends MultisigAddress { + val threshold = CTHRESHOLD(numRequiredSigners, participants.map(_.pubkey)) + + override def getAddress: Address = { + // create ErgoTree without segregated constants + val tree = ErgoTree.fromSigmaBoolean(treeHeaderFlags, threshold) + Address.fromErgoTree(tree, networkType) + } + + override def getParticipants: util.List[Address] = participants.map(new Address(_)).asJava + + override def getSignersRequiredCount: Int = numRequiredSigners +} + +object MultisigAddressImpl { + /** Creates a multisignature address from a list of participant addresses. + * @param numRequiredSigners the minimum number of signers required to spend funds from the address + * @param participants the list of participant addresses + * @param networkType the network type on which the address is used + * @param treeHeaderFlags optional flags to put in the header of the underlying ErgoTree (no flags by default) + * @return a new multisignature address + */ + def fromParticipants( + numRequiredSigners: Int, participants: java.util.List[Address], + networkType: NetworkType, treeHeaderFlags: Byte = MultisigAddress.DEFAULT_TREE_HEADER_FLAGS): MultisigAddress = { + val p2pkAddresses = participants.asScala.map(_.asP2PK()) + new MultisigAddressImpl(numRequiredSigners, p2pkAddresses, networkType, treeHeaderFlags) + } + + /** Attempts to create a multi-signature address from an existing address. + * + * @param address the address to convert to a multisig address + * + * @return a [[Try]] that wraps a MultisigAddress if successful, or an exception if the address is not a valid multisig address + */ + def fromAddressTry(address: Address): Try[MultisigAddress] = Try { + address.getErgoAddress match { + case p2s: Pay2SAddress => + p2s.script match { + case Pay2SigmaProp(CTHRESHOLD(k, participants)) => + fromParticipants( + numRequiredSigners = k, + participants = participants.map(Address.fromSigmaBoolean(_, address.getNetworkType)).asJava, + networkType = address.getNetworkType) + + case _ => + throw new IllegalArgumentException( + s"P2S address $address is not a valid multisig address") + } + case _ => + throw new IllegalArgumentException( + s"Address $address is not a P2S address and cannot be multisig address") + } + } + + /** Creates a multi-signature address from an existing address. + * @param address the address to convert to a multisig address + * @return a MultisigAddress object + * @throws IllegalArgumentException if the address is not a valid multisig address + */ + def fromAddress(address: Address): MultisigAddress = fromAddressTry(address).getOrThrow +} diff --git a/lib-impl/src/test/java/org/ergoplatform/appkit/impl/MultisigAddressTests.scala b/lib-impl/src/test/java/org/ergoplatform/appkit/impl/MultisigAddressTests.scala new file mode 100644 index 00000000..59843994 --- /dev/null +++ b/lib-impl/src/test/java/org/ergoplatform/appkit/impl/MultisigAddressTests.scala @@ -0,0 +1,59 @@ +package org.ergoplatform.appkit.impl + +import org.ergoplatform.appkit.{Address, NetworkType, MultisigAddress} +import org.scalacheck.Gen +import sigmastate.serialization.generators.ObjectGenerators +import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks +import org.scalatest.{Matchers, PropSpec} +import sigmastate.CTHRESHOLD +import sigmastate.basics.DLogProtocol.ProveDlog +import sigmastate.helpers.NegativeTesting + +import scala.collection.JavaConverters._ + +class MultisigAddressTests extends PropSpec with Matchers with ScalaCheckDrivenPropertyChecks + with ObjectGenerators with NegativeTesting { + + lazy val thresholdGen = for { + n <- Gen.choose(1, 20) + k <- Gen.choose(1, n) + children <- Gen.listOfN(n, proveDlogGen) + } yield CTHRESHOLD(k, children) + + property("getAddress/fromAddress roundtrip") { + // do create a multisig address, convert to Address and back + forAll(thresholdGen, MinSuccessful(50)) { threshold: CTHRESHOLD => + val multisigAddress = MultisigAddress.buildFromParticipants( + threshold.k, + threshold.children.map(Address.fromSigmaBoolean(_, NetworkType.MAINNET)).asJava, + NetworkType.MAINNET) + + multisigAddress.getSignersRequiredCount shouldBe threshold.k + multisigAddress.getParticipants.asScala.map(_.asP2PK().pubkey) shouldBe threshold.children.map(_.asInstanceOf[ProveDlog]) + + // check roundtrip + val address = multisigAddress.getAddress + val multisigAddress2 = MultisigAddress.buildFromAddress(address) + multisigAddress2 shouldBe multisigAddress + } + } + + property("fromAddress negative") { + // check that fromAddress fails on non-P2S addresses + forAll(proveDlogGen) { dlog: ProveDlog => + val address = Address.fromSigmaBoolean(dlog, NetworkType.MAINNET) + assertExceptionThrown( + MultisigAddress.buildFromAddress(address), + exceptionLike[IllegalArgumentException]("is not a P2S address and cannot be multisig address") + ) + } + + { // check that fromAddress fails on non-Pay2SigmaProp addresses + val address = Address.fromErgoTree(TrueTree, NetworkType.MAINNET) + assertExceptionThrown( + MultisigAddress.buildFromAddress(address), + exceptionLike[IllegalArgumentException]("is not a valid multisig address") + ) + } + } +} From bfa061b7c292f3a19a863cc0d633199ffca24c72 Mon Sep 17 00:00:00 2001 From: Alexander Slesarenko Date: Sun, 19 Mar 2023 17:54:54 +0100 Subject: [PATCH 04/11] multisig: fix Scala 2.11 compilation --- build.sbt | 2 +- .../org/ergoplatform/appkit/MultisigAddress.java | 14 +++++++------- .../appkit/impl/MultisigAddressImpl.scala | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/build.sbt b/build.sbt index f5a09161..eb2015a8 100644 --- a/build.sbt +++ b/build.sbt @@ -20,7 +20,7 @@ lazy val scala211 = "2.11.12" lazy val commonSettings = Seq( organization := "org.ergoplatform", crossScalaVersions := Seq(scala213, scala212, scala211), - scalaVersion := scala213, + scalaVersion := scala211, resolvers ++= Seq(sonatypeReleases, "SonaType" at "https://oss.sonatype.org/content/groups/public", "Typesafe maven releases" at "https://dl.bintray.com/typesafe/maven-releases/", diff --git a/lib-api/src/main/java/org/ergoplatform/appkit/MultisigAddress.java b/lib-api/src/main/java/org/ergoplatform/appkit/MultisigAddress.java index 652339c0..4beef160 100644 --- a/lib-api/src/main/java/org/ergoplatform/appkit/MultisigAddress.java +++ b/lib-api/src/main/java/org/ergoplatform/appkit/MultisigAddress.java @@ -7,25 +7,25 @@ /** * An EIP-42 compliant multisig address */ -public interface MultisigAddress { +public abstract class MultisigAddress { /** * @return address for this multisig address */ - Address getAddress(); + public abstract Address getAddress(); /** * @return list of participating p2pk addresses */ - List
getParticipants(); + public abstract List
getParticipants(); /** * @return number of signers required to sign a transaction for this address */ - int getSignersRequiredCount(); + public abstract int getSignersRequiredCount(); /** Default value for tree header flags (0 means no flags). */ - byte DEFAULT_TREE_HEADER_FLAGS = (byte)0; + public static byte DEFAULT_TREE_HEADER_FLAGS = (byte)0; /** * constructs a k-out-of-n (threshold signature) address from the list of participants @@ -35,7 +35,7 @@ public interface MultisigAddress { * @param particpants list of p2pk addresses of possible signers * @return MultisigAddress interface */ - static MultisigAddress buildFromParticipants(int signersRequired, List
particpants, NetworkType networkType) { + public static MultisigAddress buildFromParticipants(int signersRequired, List
particpants, NetworkType networkType) { return MultisigAddressImpl.fromParticipants(signersRequired, particpants, networkType, DEFAULT_TREE_HEADER_FLAGS); } @@ -45,7 +45,7 @@ static MultisigAddress buildFromParticipants(int signersRequired, List
* @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 */ - static MultisigAddress buildFromAddress(Address address) { + public static MultisigAddress buildFromAddress(Address address) { return MultisigAddressImpl.fromAddress(address); } } diff --git a/lib-api/src/main/java/org/ergoplatform/appkit/impl/MultisigAddressImpl.scala b/lib-api/src/main/java/org/ergoplatform/appkit/impl/MultisigAddressImpl.scala index ea8528dc..dbac4d79 100644 --- a/lib-api/src/main/java/org/ergoplatform/appkit/impl/MultisigAddressImpl.scala +++ b/lib-api/src/main/java/org/ergoplatform/appkit/impl/MultisigAddressImpl.scala @@ -38,7 +38,7 @@ object MultisigAddressImpl { def fromParticipants( numRequiredSigners: Int, participants: java.util.List[Address], networkType: NetworkType, treeHeaderFlags: Byte = MultisigAddress.DEFAULT_TREE_HEADER_FLAGS): MultisigAddress = { - val p2pkAddresses = participants.asScala.map(_.asP2PK()) + val p2pkAddresses = participants.asScala.map(_.asP2PK()).toSeq new MultisigAddressImpl(numRequiredSigners, p2pkAddresses, networkType, treeHeaderFlags) } From 8df0bfd448ad01f3acf12bc97a6cc3203e67b199 Mon Sep 17 00:00:00 2001 From: Alexander Slesarenko Date: Sun, 19 Mar 2023 17:56:11 +0100 Subject: [PATCH 05/11] multisig: switch back to Scala 2.12 --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index eb2015a8..5dc77413 100644 --- a/build.sbt +++ b/build.sbt @@ -20,7 +20,7 @@ lazy val scala211 = "2.11.12" lazy val commonSettings = Seq( organization := "org.ergoplatform", crossScalaVersions := Seq(scala213, scala212, scala211), - scalaVersion := scala211, + scalaVersion := scala212, resolvers ++= Seq(sonatypeReleases, "SonaType" at "https://oss.sonatype.org/content/groups/public", "Typesafe maven releases" at "https://dl.bintray.com/typesafe/maven-releases/", From bad831754dfdae62391ea6409c9ab42f0c504374 Mon Sep 17 00:00:00 2001 From: Alexander Slesarenko Date: Sun, 19 Mar 2023 18:02:51 +0100 Subject: [PATCH 06/11] multisig: switch back to Scala 2.13 --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 5dc77413..f5a09161 100644 --- a/build.sbt +++ b/build.sbt @@ -20,7 +20,7 @@ lazy val scala211 = "2.11.12" lazy val commonSettings = Seq( organization := "org.ergoplatform", crossScalaVersions := Seq(scala213, scala212, scala211), - scalaVersion := scala212, + scalaVersion := scala213, resolvers ++= Seq(sonatypeReleases, "SonaType" at "https://oss.sonatype.org/content/groups/public", "Typesafe maven releases" at "https://dl.bintray.com/typesafe/maven-releases/", From 41f9b201790b29a86e5307fe1c7809350ecb2f2f Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Mon, 20 Mar 2023 13:25:35 +0100 Subject: [PATCH 07/11] Multisig: Added test for address order stability --- .../appkit/impl/MultisigAddressTests.scala | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/lib-impl/src/test/java/org/ergoplatform/appkit/impl/MultisigAddressTests.scala b/lib-impl/src/test/java/org/ergoplatform/appkit/impl/MultisigAddressTests.scala index 59843994..853e404a 100644 --- a/lib-impl/src/test/java/org/ergoplatform/appkit/impl/MultisigAddressTests.scala +++ b/lib-impl/src/test/java/org/ergoplatform/appkit/impl/MultisigAddressTests.scala @@ -38,6 +38,24 @@ class MultisigAddressTests extends PropSpec with Matchers with ScalaCheckDrivenP } } + property("address order stability") { + // do create a multisig address, convert to Address and back + forAll(thresholdGen, MinSuccessful(50)) { threshold: CTHRESHOLD => + val addresses = threshold.children.map(Address.fromSigmaBoolean(_, NetworkType.MAINNET)) + val multisigAddress = MultisigAddress.buildFromParticipants( + threshold.k, + addresses.asJava, + NetworkType.MAINNET) + + val multisigAddress2 = MultisigAddress.buildFromParticipants( + threshold.k, + addresses.reverse.asJava, + NetworkType.MAINNET) + + multisigAddress.getAddress shouldBe multisigAddress2.getAddress + } + } + property("fromAddress negative") { // check that fromAddress fails on non-P2S addresses forAll(proveDlogGen) { dlog: ProveDlog => From 6e2fb0b67f0f003004865d6faffa2d16bf75bc0c Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Tue, 21 Mar 2023 12:32:22 +0100 Subject: [PATCH 08/11] Multisig: Generalized for any multisig --- .../appkit/MultisigTransaction.java | 64 ++++++------------- .../appkit/ReducedTransaction.java | 5 ++ .../appkit/SigningParticipants.java | 45 +++++++++++++ .../appkit/impl/ReducedTransactionImpl.java | 5 ++ 4 files changed, 76 insertions(+), 43 deletions(-) create mode 100644 lib-api/src/main/java/org/ergoplatform/appkit/SigningParticipants.java diff --git a/lib-api/src/main/java/org/ergoplatform/appkit/MultisigTransaction.java b/lib-api/src/main/java/org/ergoplatform/appkit/MultisigTransaction.java index 66215529..87bb0f40 100644 --- a/lib-api/src/main/java/org/ergoplatform/appkit/MultisigTransaction.java +++ b/lib-api/src/main/java/org/ergoplatform/appkit/MultisigTransaction.java @@ -5,77 +5,55 @@ /** * EIP-41/EIP-11 compliant multi sig transaction */ -public class MultisigTransaction { +public abstract class MultisigTransaction { /** * @return transaction that is going to be signed */ - public Transaction getTransaction() { - throw new UnsupportedOperationException(); - } - - /** - * @return multisig address this transaction was created for - */ - public MultisigAddress getMultisigAddress() { - throw new UnsupportedOperationException(); - } + abstract public ReducedTransaction getTransaction(); /** - * adds a new commitment to this multisig transaction + * adds a new hint to this multisig transaction * @param prover to add commitment for */ - public void addCommitment(ErgoProver prover) { - throw new UnsupportedOperationException(); - } + abstract public void addHint(ErgoProver prover); /** - * adds the commitments not present on this instance from another multisig transaction instance + * adds the hints not present on this instance from another multisig transaction instance * for the same transaction. */ - public void addCommitments(MultisigTransaction other) { - throw new UnsupportedOperationException(); - } + abstract public void mergeHints(MultisigTransaction other); /** - * @return list of participants that added a commitment for the transaction + * adds the hints not present on this instance from the EIP-11 json */ - public List
getCommitingParticipants() { - throw new UnsupportedOperationException(); - } - - public boolean hasEnoughCommitments() { - throw new UnsupportedOperationException(); - } + abstract public void mergeHints(String json); /** - * @return the signed transaction if enough commitments are available - * @throws IllegalStateException if {@link #hasEnoughCommitments()} is false + * @return list of participants that added a hint for the transaction */ - public SignedTransaction toSignedTransaction() { - throw new UnsupportedOperationException(); - } + abstract public List
getCommitingParticipants(); + /** + * @return true if SignedTransaction can be built + */ + abstract public boolean isHintBagComplete(); /** - * @return EIP-11 compliant json string to transfer the partially signed transaction to the - * next particpant + * @return the signed transaction if enough commitments are available + * @throws IllegalStateException if {@link #isHintBagComplete()} is false */ - public String toJson() { - throw new UnsupportedOperationException(); - } + abstract public SignedTransaction toSignedTransaction(); /** - * constructs a multi sig transaction from an unsigned transaction. The first multi sig address - * in input boxes is used. + * @return EIP-11 compliant json string to transfer the partially signed transaction to the + * next participant */ - public static MultisigTransaction fromTransaction(UnsignedTransaction transaction) { - throw new UnsupportedOperationException(); - } + abstract public String hintsToJson(); /** * constructs a multi sig transaction from a reduced transaction */ - public static MultisigTransaction fromTransaction(ReducedTransaction transaction, MultisigAddress address) { + public static MultisigTransaction fromTransaction(ReducedTransaction transaction) { 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 hintbag; + + public MultisigRequirement(int hintsNeeded, List hintbag) { + this.hintsNeeded = hintsNeeded; + this.hintbag = hintbag; + } + + @Override + public boolean isMultisig() { + return hintsNeeded > 1; + } + } + + public static class SingleSigner extends SigningParticipants { + public final Address address; + + public SingleSigner(Address address) { + this.address = address; + } + + @Override + public boolean isMultisig() { + return false; + } + } +} diff --git a/lib-impl/src/main/java/org/ergoplatform/appkit/impl/ReducedTransactionImpl.java b/lib-impl/src/main/java/org/ergoplatform/appkit/impl/ReducedTransactionImpl.java index a1d7adba..a723e233 100644 --- a/lib-impl/src/main/java/org/ergoplatform/appkit/impl/ReducedTransactionImpl.java +++ b/lib-impl/src/main/java/org/ergoplatform/appkit/impl/ReducedTransactionImpl.java @@ -68,6 +68,11 @@ public byte[] toBytes() { return w.toBytes(); } + @Override + public SigningParticipants getSignersRequired() { + throw new UnsupportedOperationException(); + } + @Override public int hashCode() { return _tx.hashCode(); From 38e4373ac2a8b30084d6cafd5c8edff16ac2c80e Mon Sep 17 00:00:00 2001 From: Alexander Slesarenko Date: Tue, 21 Mar 2023 13:39:11 +0100 Subject: [PATCH 09/11] multisig: implement ordering of participants --- .../ergoplatform/appkit/scalaapi/Utils.scala | 13 +++++ .../scalaapi/ByteArrayOrderingSpec.scala | 48 +++++++++++++++++++ .../appkit/impl/MultisigAddressImpl.scala | 6 ++- .../appkit/impl/MultisigAddressTests.scala | 4 +- 4 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 common/src/test/scala/org/ergoplatform/appkit/scalaapi/ByteArrayOrderingSpec.scala 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/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/impl/MultisigAddressImpl.scala b/lib-api/src/main/java/org/ergoplatform/appkit/impl/MultisigAddressImpl.scala index dbac4d79..a51ccfc4 100644 --- a/lib-api/src/main/java/org/ergoplatform/appkit/impl/MultisigAddressImpl.scala +++ b/lib-api/src/main/java/org/ergoplatform/appkit/impl/MultisigAddressImpl.scala @@ -38,7 +38,11 @@ object MultisigAddressImpl { def fromParticipants( numRequiredSigners: Int, participants: java.util.List[Address], networkType: NetworkType, treeHeaderFlags: Byte = MultisigAddress.DEFAULT_TREE_HEADER_FLAGS): MultisigAddress = { - val p2pkAddresses = participants.asScala.map(_.asP2PK()).toSeq + import org.ergoplatform.appkit.scalaapi.Utils.byteArrayOrdering + val p2pkAddresses = participants.asScala + .map { a => val pk = a.asP2PK(); (pk, pk.pubkey.pkBytes) } + .sortBy(_._2) + .map(_._1).toSeq new MultisigAddressImpl(numRequiredSigners, p2pkAddresses, networkType, treeHeaderFlags) } diff --git a/lib-impl/src/test/java/org/ergoplatform/appkit/impl/MultisigAddressTests.scala b/lib-impl/src/test/java/org/ergoplatform/appkit/impl/MultisigAddressTests.scala index 853e404a..7d45ccd2 100644 --- a/lib-impl/src/test/java/org/ergoplatform/appkit/impl/MultisigAddressTests.scala +++ b/lib-impl/src/test/java/org/ergoplatform/appkit/impl/MultisigAddressTests.scala @@ -13,12 +13,14 @@ import scala.collection.JavaConverters._ class MultisigAddressTests extends PropSpec with Matchers with ScalaCheckDrivenPropertyChecks with ObjectGenerators with NegativeTesting { + import org.ergoplatform.appkit.scalaapi.Utils.byteArrayOrdering + /** Generate CTHRESHOLD with sorted children. */ lazy val thresholdGen = for { n <- Gen.choose(1, 20) k <- Gen.choose(1, n) children <- Gen.listOfN(n, proveDlogGen) - } yield CTHRESHOLD(k, children) + } yield CTHRESHOLD(k, children.sortBy(_.pkBytes)) property("getAddress/fromAddress roundtrip") { // do create a multisig address, convert to Address and back From 5b6dd26b3a63a58068628897c9a21553f355b890 Mon Sep 17 00:00:00 2001 From: Alexander Slesarenko Date: Sun, 23 Apr 2023 21:13:41 +0200 Subject: [PATCH 10/11] multisig: fix after merge with sigma-v5.0.7 --- .../org/ergoplatform/appkit/impl/MultisigAddressTests.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib-impl/src/test/java/org/ergoplatform/appkit/impl/MultisigAddressTests.scala b/lib-impl/src/test/java/org/ergoplatform/appkit/impl/MultisigAddressTests.scala index 7d45ccd2..eaddc790 100644 --- a/lib-impl/src/test/java/org/ergoplatform/appkit/impl/MultisigAddressTests.scala +++ b/lib-impl/src/test/java/org/ergoplatform/appkit/impl/MultisigAddressTests.scala @@ -2,16 +2,17 @@ package org.ergoplatform.appkit.impl import org.ergoplatform.appkit.{Address, NetworkType, MultisigAddress} import org.scalacheck.Gen +import org.scalatest.matchers.should.Matchers +import org.scalatest.propspec.AnyPropSpec import sigmastate.serialization.generators.ObjectGenerators import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks -import org.scalatest.{Matchers, PropSpec} import sigmastate.CTHRESHOLD import sigmastate.basics.DLogProtocol.ProveDlog import sigmastate.helpers.NegativeTesting import scala.collection.JavaConverters._ -class MultisigAddressTests extends PropSpec with Matchers with ScalaCheckDrivenPropertyChecks +class MultisigAddressTests extends AnyPropSpec with Matchers with ScalaCheckDrivenPropertyChecks with ObjectGenerators with NegativeTesting { import org.ergoplatform.appkit.scalaapi.Utils.byteArrayOrdering From ddc6c7cb0ffc8af077fb9d078426fba9dba4f636 Mon Sep 17 00:00:00 2001 From: Alexander Slesarenko Date: Mon, 24 Apr 2023 00:39:53 +0200 Subject: [PATCH 11/11] multisig: SigningServer, Client, SigningSession --- .../appkit/multisig/Signing.scala | 93 +++++++++++++++++++ .../appkit/multisig/SigningSpec.scala | 14 +++ 2 files changed, 107 insertions(+) create mode 100644 appkit/src/main/scala/org/ergoplatform/appkit/multisig/Signing.scala create mode 100644 appkit/src/test/scala/org/ergoplatform/appkit/multisig/SigningSpec.scala 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 + } +}