Skip to content

Commit

Permalink
Merge pull request #866 from wavesplatform/mass-transfer-with-proofs
Browse files Browse the repository at this point in the history
Mass transfer with proofs
  • Loading branch information
ismagin authored Feb 22, 2018
2 parents 8321898 + 5ded053 commit 4281c88
Show file tree
Hide file tree
Showing 13 changed files with 71 additions and 57 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import scorex.transaction.assets.MassTransferTransaction.ParsedTransfer
import scorex.transaction.assets._
import scorex.transaction.assets.exchange.{AssetPair, ExchangeTransaction, Order}
import scorex.transaction.lease.{LeaseCancelTransaction, LeaseTransaction}
import scorex.transaction.{CreateAliasTransaction, Transaction, ValidationError}
import scorex.transaction.{CreateAliasTransaction, Proofs, Transaction, ValidationError}
import scorex.utils.LoggerFacade

import scala.concurrent.duration._
Expand Down Expand Up @@ -140,7 +140,7 @@ class NarrowTransactionGenerator(settings: Settings,
})
} else Some(randomFrom(accounts).get, None)
senderAndAssetOpt.flatMap { case (sender, asset) =>
logOption(MassTransferTransaction.create(asset, sender, transfers.toList, ts, moreThatStandartFee,
logOption(MassTransferTransaction.selfSigned(Proofs.Version, asset, sender, transfers.toList, ts, moreThatStandartFee,
Array.fill(r.nextInt(100))(r.nextInt().toByte)))
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import com.wavesplatform.generator.utils.Implicits._
import scorex.account.{Address, PrivateKeyAccount}
import scorex.transaction.assets.MassTransferTransaction.ParsedTransfer
import scorex.transaction.assets.{MassTransferTransaction, TransferTransaction}
import scorex.transaction.{Transaction, TransactionParser}
import scorex.transaction.{Proofs, Transaction, TransactionParser}

object Gen {
private def random = ThreadLocalRandom.current
Expand Down Expand Up @@ -40,7 +40,7 @@ object Gen {
senderGen.zip(transferCountGen).map { case (sender, count) =>
val transfers = List.tabulate(count)(_ => ParsedTransfer(recipientGen.next(), amountGen.next()))
val fee = 100000 + count * 50000
MassTransferTransaction.create(None, sender, transfers, System.currentTimeMillis, fee, Array.emptyByteArray)
MassTransferTransaction.selfSigned(Proofs.Version, None, sender, transfers, System.currentTimeMillis, fee, Array.emptyByteArray)
}.collect { case Right(tx) => tx }
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,8 @@ class TransactionsApiSuite extends BaseTransactionSuite {
"sender" -> firstAddress,
"transfers" -> Json.toJson(Seq(Transfer(secondAddress, 1.waves), Transfer(thirdAddress, 2.waves))),
"fee" -> 200000,
"attachment" -> Base58.encode("masspay".getBytes)))
"attachment" -> Base58.encode("masspay".getBytes)),
usesProofs = true)
}

test("/transactions/sign should produce lease/cancel transactions that are good for /transactions/broadcast") {
Expand All @@ -167,12 +168,16 @@ class TransactionsApiSuite extends BaseTransactionSuite {
"fee" -> 100000))
}

private def signAndBroadcast(json: JsObject): String = {
private def signAndBroadcast(json: JsObject, usesProofs: Boolean = false): String = {
val f = for {
rs <- sender.postJsonWithApiKey("/transactions/sign", json)
_ = assert(rs.getStatusCode == HttpConstants.ResponseStatusCodes.OK_200)
body = Json.parse(rs.getResponseBody)
_ = assert((body \ "signature").as[String].nonEmpty)
signed: Boolean = if (usesProofs) {
val proofs = (body \ "proofs").as[Seq[String]]
proofs.lengthCompare(1) == 0 && proofs.head.nonEmpty
} else (body \ "signature").as[String].nonEmpty
_ = assert(signed)
rb <- sender.postJson("/transactions/broadcast", body)
_ = assert(rb.getStatusCode == HttpConstants.ResponseStatusCodes.OK_200)
id = (Json.parse(rb.getResponseBody) \ "id").as[String]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import org.scalatest.CancelAfterFailure
import scorex.account.AddressOrAlias
import scorex.api.http.assets.SignedMassTransferRequest
import scorex.crypto.encode.Base58
import scorex.transaction.Proofs
import scorex.transaction.TransactionParser.TransactionType
import scorex.transaction.assets.MassTransferTransaction
import scorex.transaction.assets.MassTransferTransaction.MaxTransferCount
import scorex.transaction.assets.MassTransferTransaction.{ParsedTransfer, Transfer}

import scala.concurrent.duration._
import scorex.transaction.assets.TransferTransaction.MaxAttachmentSize

Expand Down Expand Up @@ -102,8 +104,8 @@ class MassTransferTransactionSuite extends BaseTransactionSuite with CancelAfter
test("invalid transfer should not be in UTX or blockchain") {
import scorex.transaction.assets.MassTransferTransaction.MaxTransferCount
val address2 = AddressOrAlias.fromString(secondAddress).right.get
val valid = MassTransferTransaction.create(
None, sender.privateKey,
val valid = MassTransferTransaction.selfSigned(
Proofs.Version, None, sender.privateKey,
List(ParsedTransfer(address2, transferAmount)),
System.currentTimeMillis,
calcFee(1), Array.emptyByteArray).right.get
Expand All @@ -128,7 +130,6 @@ class MassTransferTransactionSuite extends BaseTransactionSuite with CancelAfter


test("huuuge transactions are allowed") {

val (balance1, eff1) = notMiner.accountBalances(firstAddress)
val fee = calcFee(MaxTransferCount)
val amount = (balance1 - fee) / MaxTransferCount
Expand All @@ -138,8 +139,6 @@ class MassTransferTransactionSuite extends BaseTransactionSuite with CancelAfter

nodes.waitForHeightAraiseAndTxPresent(transferId)
notMiner.assertBalances(firstAddress, balance1 - fee, eff1 - fee)


}

private def createSignedMassTransferRequest(tx: MassTransferTransaction): SignedMassTransferRequest = {
Expand All @@ -151,7 +150,7 @@ class MassTransferTransactionSuite extends BaseTransactionSuite with CancelAfter
fee,
timestamp,
attachment.headOption.map(_ => Base58.encode(attachment)),
signature.base58
proofs.base58().toList
)
}
}
7 changes: 4 additions & 3 deletions src/main/scala/scorex/api/http/BroadcastRequest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@ package scorex.api.http

import com.wavesplatform.state2.ByteStr
import scorex.transaction.ValidationError
import scorex.transaction.ValidationError.Validation

trait BroadcastRequest {
protected def parseBase58(v: String, error: String, maxLength: Int): Either[ValidationError, ByteStr] =
protected def parseBase58(v: String, error: String, maxLength: Int): Validation[ByteStr] =
if (v.length > maxLength) Left(ValidationError.GenericError(error))
else ByteStr.decodeBase58(v).toOption.toRight(ValidationError.GenericError(error))

protected def parseBase58(v: Option[String], error: String, maxLength: Int): Either[ValidationError, ByteStr] =
protected def parseBase58(v: Option[String], error: String, maxLength: Int): Validation[ByteStr] =
v.fold[Either[ValidationError, ByteStr]](Right(ByteStr(Array.emptyByteArray)))(_v => parseBase58(_v, error, maxLength))

protected def parseBase58ToOption(v: Option[String], error: String, maxLength: Int): Either[ValidationError, Option[ByteStr]] =
protected def parseBase58ToOption(v: Option[String], error: String, maxLength: Int): Validation[Option[ByteStr]] =
v.fold[Either[ValidationError, Option[ByteStr]]](Right(None)) { s => parseBase58(s, error, maxLength).map(b => Option(b)) }
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package scorex.api.http.assets

import cats.implicits._
import io.swagger.annotations.{ApiModel, ApiModelProperty}
import play.api.libs.json.{Format, Json}
import scorex.account.PublicKeyAccount
import scorex.api.http.BroadcastRequest
import scorex.transaction.TransactionParser.SignatureStringLength
import scorex.transaction.assets.MassTransferTransaction.Transfer
import scorex.transaction.assets.{MassTransferTransaction, TransferTransaction}
import scorex.transaction.{AssetIdStringLength, ValidationError}
import scorex.transaction.{AssetIdStringLength, Proofs, ValidationError}

object SignedMassTransferRequest {
implicit val jsonFormat: Format[SignedMassTransferRequest] = Json.format
Expand All @@ -27,13 +27,14 @@ case class SignedMassTransferRequest(@ApiModelProperty(value = "Base58 encoded s
@ApiModelProperty(value = "Base58 encoded attachment")
attachment: Option[String],
@ApiModelProperty(required = true)
signature: String) extends BroadcastRequest {
proofs: List[String]) extends BroadcastRequest {
def toTx: Either[ValidationError, MassTransferTransaction] = for {
_sender <- PublicKeyAccount.fromBase58String(senderPublicKey)
_assetId <- parseBase58ToOption(assetId.filter(_.length > 0), "invalid.assetId", AssetIdStringLength)
_signature <- parseBase58(signature, "invalid.signature", SignatureStringLength)
_proofBytes <- proofs.traverse(s => parseBase58(s, "invalid proof", Proofs.MaxProofSize))
_proofs <- Proofs.create(_proofBytes)
_attachment <- parseBase58(attachment.filter(_.length > 0), "invalid.attachment", TransferTransaction.MaxAttachmentStringSize)
_transfers <- MassTransferTransaction.parseTransfersList(transfers)
t <- MassTransferTransaction.create(_assetId, _sender, _transfers, timestamp, fee, _attachment.arr, _signature)
t <- MassTransferTransaction.create(Proofs.Version, _assetId, _sender, _transfers, timestamp, fee, _attachment.arr, _proofs)
} yield t
}
4 changes: 3 additions & 1 deletion src/main/scala/scorex/transaction/Proofs.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,22 @@ package scorex.transaction

import com.wavesplatform.state2._
import monix.eval.Coeval
import scorex.crypto.encode.Base58
import scorex.serialization.Deser
import scorex.transaction.ValidationError.GenericError

import scala.util.Try

case class Proofs private(proofs: Seq[ByteStr]) {
val bytes: Coeval[Array[Byte]] = Coeval.evalOnce(Proofs.Version +: Deser.serializeArrays(proofs.map(_.arr)))
val base58: Coeval[Seq[String]] = Coeval.evalOnce(proofs.map(p => Base58.encode(p.arr)))
}

object Proofs {

val Version = 1: Byte
val MaxProofs = 8
val MaxProofSize = 64
val MaxProofSize = 512

lazy val empty = create(Seq.empty).explicitGet()

Expand Down
3 changes: 2 additions & 1 deletion src/main/scala/scorex/transaction/TransactionFactory.scala
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ object TransactionFactory {
for {
senderPrivateKey <- wallet.findWallet(request.sender)
transfers <- MassTransferTransaction.parseTransfersList(request.transfers)
tx <- MassTransferTransaction.create(
tx <- MassTransferTransaction.selfSigned(
Proofs.Version,
request.assetId.map(s => ByteStr.decodeBase58(s).get),
senderPrivateKey,
transfers,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package scorex.transaction.assets
import cats.implicits._
import com.google.common.primitives.{Bytes, Longs, Shorts}
import com.wavesplatform.crypto
import com.wavesplatform.state2.ByteStr
import com.wavesplatform.state2._
import monix.eval.Coeval
import play.api.libs.json.{Format, JsObject, JsValue, Json}
import scorex.account.{AddressOrAlias, PrivateKeyAccount, PublicKeyAccount}
Expand All @@ -16,13 +16,14 @@ import scorex.transaction.assets.MassTransferTransaction.{ParsedTransfer, toJson

import scala.util.{Either, Failure, Success, Try}

case class MassTransferTransaction private(assetId: Option[AssetId],
case class MassTransferTransaction private(version: Byte,
assetId: Option[AssetId],
sender: PublicKeyAccount,
transfers: List[ParsedTransfer],
timestamp: Long,
fee: Long,
attachment: Array[Byte],
signature: ByteStr) extends SignedTransaction with FastHashId {
proofs: Proofs) extends ProvenTransaction with FastHashId {
override val transactionType: TransactionType.Value = TransactionType.MassTransferTransaction

override val assetFee: (Option[AssetId], Long) = (None, fee)
Expand All @@ -35,6 +36,7 @@ case class MassTransferTransaction private(assetId: Option[AssetId],

Bytes.concat(
Array(transactionType.id.toByte),
Array(version),
sender.publicKey,
assetIdBytes,
Shorts.toByteArray(transfers.size.toShort),
Expand All @@ -58,7 +60,7 @@ case class MassTransferTransaction private(assetId: Option[AssetId],
def compactJson(recipient: AddressOrAlias): JsObject = jsonBase() ++ Json.obj(
"transfers" -> toJson(transfers.filter(_.address == recipient)))

override val bytes: Coeval[Array[Byte]] = Coeval.evalOnce(Bytes.concat(bodyBytes(), signature.arr))
override val bytes: Coeval[Array[Byte]] = Coeval.evalOnce(Bytes.concat(bodyBytes(), proofs.bytes()))
}

object MassTransferTransaction {
Expand All @@ -71,8 +73,9 @@ object MassTransferTransaction {
implicit val transferFormat: Format[Transfer] = Json.format

def parseTail(bytes: Array[Byte]): Try[MassTransferTransaction] = Try {
val sender = PublicKeyAccount(bytes.slice(0, KeyLength))
val (assetIdOpt, s0) = Deser.parseByteArrayOption(bytes, KeyLength, AssetIdLength)
val version = bytes(0)
val sender = PublicKeyAccount(bytes.slice(1, KeyLength + 1))
val (assetIdOpt, s0) = Deser.parseByteArrayOption(bytes, KeyLength + 1, AssetIdLength)
val transferCount = Shorts.fromByteArray(bytes.slice(s0, s0 + 2))

def readTransfer(offset: Int): (Validation[ParsedTransfer], Int) = {
Expand All @@ -92,20 +95,21 @@ object MassTransferTransaction {
transfers <- transfersList.map { case (ei, _) => ei }.sequence
timestamp = Longs.fromByteArray(bytes.slice(s1, s1 + 8))
feeAmount = Longs.fromByteArray(bytes.slice(s1 + 8, s1 + 16))
(attachment, s2) = Deser.parseArraySize(bytes, s1 + 16)
signature = ByteStr(bytes.slice(s2, s2 + SignatureLength))
mtt <- MassTransferTransaction.create(assetIdOpt.map(ByteStr(_)), sender, transfers, timestamp, feeAmount, attachment, signature)
(attachment, attachEnd) = Deser.parseArraySize(bytes, s1 + 16)
proofs <- Proofs.fromBytes(bytes.drop(attachEnd))
mtt <- MassTransferTransaction.create(version, assetIdOpt.map(ByteStr(_)), sender, transfers, timestamp, feeAmount, attachment, proofs)
} yield mtt
tx.fold(left => Failure(new Exception(left.toString)), right => Success(right))
}.flatten

def create(assetId: Option[AssetId],
def create(version: Byte,
assetId: Option[AssetId],
sender: PublicKeyAccount,
transfers: List[ParsedTransfer],
timestamp: Long,
feeAmount: Long,
attachment: Array[Byte],
signature: ByteStr): Either[ValidationError, MassTransferTransaction] = {
proofs: Proofs): Either[ValidationError, MassTransferTransaction] = {
Try {
transfers.map(_.amount).fold(feeAmount)(Math.addExact)
}.fold(
Expand All @@ -120,19 +124,20 @@ object MassTransferTransaction {
} else if (feeAmount <= 0) {
Left(ValidationError.InsufficientFee)
} else {
Right(MassTransferTransaction(assetId, sender, transfers, timestamp, feeAmount, attachment, signature))
Right(MassTransferTransaction(version, assetId, sender, transfers, timestamp, feeAmount, attachment, proofs))
}
)
}

def create(assetId: Option[AssetId],
sender: PrivateKeyAccount,
transfers: List[ParsedTransfer],
timestamp: Long,
feeAmount: Long,
attachment: Array[Byte]): Either[ValidationError, MassTransferTransaction] = {
create(assetId, sender, transfers, timestamp, feeAmount, attachment, ByteStr.empty).right.map { unsigned =>
unsigned.copy(signature = ByteStr(crypto.sign(sender, unsigned.bodyBytes())))
def selfSigned(version: Byte,
assetId: Option[AssetId],
sender: PrivateKeyAccount,
transfers: List[ParsedTransfer],
timestamp: Long,
feeAmount: Long,
attachment: Array[Byte]): Either[ValidationError, MassTransferTransaction] = {
create(version, assetId, sender, transfers, timestamp, feeAmount, attachment, Proofs.empty).right.map { unsigned =>
unsigned.copy(proofs = Proofs.create(Seq(ByteStr(crypto.sign(sender, unsigned.bodyBytes())))).explicitGet())
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,14 @@ case class ScriptTransferTransaction private(version: Byte,
val amountBytes = Longs.toByteArray(amount)
val feeBytes = Longs.toByteArray(fee)

Bytes.concat(Bytes.concat(Array(version),
Bytes.concat(Array(version),
sender.publicKey,
assetIdBytes,
timestampBytes,
amountBytes,
feeBytes,
recipient.bytes.arr,
Deser.serializeArray(attachment)))
Deser.serializeArray(attachment))
}

override val json: Coeval[JsObject] = Coeval.evalOnce(jsonBase() ++ Json.obj(
Expand Down
4 changes: 2 additions & 2 deletions src/test/scala/com/wavesplatform/TransactionGen.scala
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ trait TransactionGen extends ScriptGen {

def massTransferGeneratorP(sender: PrivateKeyAccount, transfers: List[ParsedTransfer], assetId: Option[AssetId]): Gen[MassTransferTransaction] = for {
(_, _, _, _, timestamp, _, feeAmount, attachment) <- transferParamGen
} yield MassTransferTransaction.create(assetId, sender, transfers, timestamp, feeAmount, attachment).right.get
} yield MassTransferTransaction.selfSigned(Proofs.Version, assetId, sender, transfers, timestamp, feeAmount, attachment).right.get

def createWavesTransfer(sender: PrivateKeyAccount, recipient: Address,
amount: Long, fee: Long, timestamp: Long): Either[ValidationError, TransferTransaction] =
Expand Down Expand Up @@ -227,7 +227,7 @@ trait TransactionGen extends ScriptGen {
amount <- Gen.choose(1L, Long.MaxValue / MaxTransferCount)
} yield ParsedTransfer(recipient, amount)
recipients <- Gen.listOfN(transferCount, transferGen)
} yield MassTransferTransaction.create(assetId, sender, recipients, timestamp, feeAmount, attachment).right.get
} yield MassTransferTransaction.selfSigned(Proofs.Version, assetId, sender, recipients, timestamp, feeAmount, attachment).right.get
}.label("massTransferTransaction")

val MinIssueFee = 100000000
Expand Down
4 changes: 2 additions & 2 deletions src/test/scala/com/wavesplatform/UtxPoolSpecification.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import scorex.transaction.TransactionParser.TransactionType
import scorex.transaction.ValidationError.SenderIsBlacklisted
import scorex.transaction.assets.MassTransferTransaction.ParsedTransfer
import scorex.transaction.assets.{MassTransferTransaction, TransferTransaction}
import scorex.transaction.{FeeCalculator, Transaction}
import scorex.transaction.{FeeCalculator, Proofs, Transaction}
import scorex.utils.Time

import scala.concurrent.duration._
Expand Down Expand Up @@ -70,7 +70,7 @@ class UtxPoolSpecification extends FreeSpec
val transfers = recipients.map(r => ParsedTransfer(r.toAddress, amount))
val txs = for {
fee <- chooseNum(1, amount)
} yield MassTransferTransaction.create(None, sender, transfers, time.getTimestamp(), fee, Array.empty[Byte]).right.get
} yield MassTransferTransaction.selfSigned(Proofs.Version, None, sender, transfers, time.getTimestamp(), fee, Array.empty[Byte]).right.get
txs.label("transferWithRecipient")
}

Expand Down
Loading

0 comments on commit 4281c88

Please sign in to comment.