Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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)
}
}

Original file line number Diff line number Diff line change
@@ -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
}
}
13 changes: 13 additions & 0 deletions common/src/main/java/org/ergoplatform/appkit/scalaapi/Utils.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.ergoplatform.appkit.scalaapi

import debox.cfor
import scalan.ExactIntegral

import scala.collection.mutable
Expand Down Expand Up @@ -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
}
}
}
Original file line number Diff line number Diff line change
@@ -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}

Expand Down Expand Up @@ -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
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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)
)
}
}
}
51 changes: 51 additions & 0 deletions lib-api/src/main/java/org/ergoplatform/appkit/MultisigAddress.java
Original file line number Diff line number Diff line change
@@ -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<Address> 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<Address> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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<Address> 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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Original file line number Diff line number Diff line change
@@ -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<SigningParticipants> hintbag;

public MultisigRequirement(int hintsNeeded, List<SigningParticipants> 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;
}
}
}
Loading