Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
44218b4
EIP-31 Basic implementation of data classes and transaction to create…
MrStahlfelge Nov 1, 2022
c8a613a
EIP-0031 add BabelFeeOperations.cancelBabelFeeContract
MrStahlfelge Nov 1, 2022
448ff3d
EIP-0031 Add BabelFeeOperations#getBabelFeeTransactionBuilder
MrStahlfelge Nov 1, 2022
d440b20
EIP-0031 BabelFeeOperations#findBabelFeeBox
MrStahlfelge Nov 1, 2022
fd5ac70
Fix Java 8 compile
MrStahlfelge Nov 1, 2022
daa5610
eip-31-babelfees: typo fixes
aslesarenko Nov 3, 2022
61b6657
eip-31-babelfees: some formatting fixes
aslesarenko Nov 3, 2022
5102a52
EIP-0031 use ErgoTreeSerializer.DefaultSerializer.substituteConstants…
MrStahlfelge Nov 3, 2022
31f229f
Improve exception message
MrStahlfelge Nov 3, 2022
77b7096
EIP-0031 test for findBabelFeeBox, documentation changes, improve Bab…
MrStahlfelge Nov 3, 2022
6610b7f
Merge branch 'transactionbuilder-addmethods' into eip-31-babelfees
MrStahlfelge Nov 5, 2022
933f8a7
EIP-0031 use new methods from #205
MrStahlfelge Nov 5, 2022
93f2eb7
EIP-0031 use ErgoTreeTemplate
MrStahlfelge Nov 5, 2022
c2c965a
Merge remote-tracking branch 'origin/develop' into eip-31-babelfees
MrStahlfelge Nov 7, 2022
15fee58
transactionbuilder-addmethods: correct implementation of ErgoTreeTemp…
aslesarenko Nov 7, 2022
fe3f0fd
transactionbuilder-addmethods: tests for ErgoTreeTemplate
aslesarenko Nov 7, 2022
9ec3f36
Added BabelFeeOperations.findBabelFeeBox parameter to search through …
MrStahlfelge Nov 9, 2022
e4400fc
Linked to EIP version from PR
MrStahlfelge Nov 9, 2022
93faed4
eip-31-babelfees: fix box loading + formatting
aslesarenko Nov 10, 2022
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
139 changes: 139 additions & 0 deletions appkit/src/test/scala/org/ergoplatform/appkit/BabelFeeSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package org.ergoplatform.appkit

import org.ergoplatform.appkit.babelfee.{BabelFeeBoxContract, BabelFeeBoxState, BabelFeeOperations}
import org.scalatest.{Matchers, PropSpec}
import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks

import java.util
import java.util.Arrays

class BabelFeeSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChecks
with HttpClientTesting
with AppkitTestingCommon {

val mockTokenId = "f9e5ce5aa0d95f5d54a7bc89c46730d9662397067250aa18a0039631c0f5b809"

property("babel fee box creation and revoke") {
val ergoClient = createMockedErgoClient(MockData(Nil, Nil))
ergoClient.execute { ctx: BlockchainContext =>
val creator = address

val amountToSend = Parameters.OneErg * 100

val input1 = ctx.newTxBuilder.outBoxBuilder
.value(amountToSend + Parameters.MinFee)
.contract(creator.toErgoContract)
.build().convertToInputWith(mockTokenId, 0)

val txCreate = BabelFeeOperations.createNewBabelContractTx(
BoxOperations.createForSender(creator, ctx)
.withAmountToSpend(amountToSend)
.withInputBoxesLoader(new MockedBoxesLoader(Arrays.asList(input1))),
ErgoId.create(mockTokenId),
Parameters.OneErg
)

ctx.newProverBuilder().build().reduce(txCreate, 0)

val babelFeeErgoBox = txCreate.getOutputs.get(0).convertToInputWith(mockTokenId, 0)

// now we cancel the babel box
val txCancel = BabelFeeOperations.cancelBabelFeeContract(
BoxOperations.createForSender(creator, ctx)
.withInputBoxesLoader(new MockedBoxesLoader(Arrays.asList(input1))),
babelFeeErgoBox)

ctx.newProverBuilder()
.withMnemonic(mnemonic, SecretString.empty(), false)
.build()
.sign(txCancel)

// check if the contract really is for the token
new BabelFeeBoxContract(babelFeeErgoBox.getErgoTree).getTokenId.toString shouldBe mockTokenId
// check template hash
ErgoTreeTemplate.fromErgoTree(babelFeeErgoBox.getErgoTree).getTemplateHashHex shouldBe BabelFeeBoxContract.templateHash
}
}

property("babel fee box use") {
val ergoClient = createMockedErgoClient(MockData(Nil, Nil))
ergoClient.execute { ctx: BlockchainContext =>
val sender = address

val txB = ctx.newTxBuilder
val babelFeeBoxState = BabelFeeBoxState.newBuilder().withValue(Parameters.OneErg)
.withTokenId(ErgoId.create(mockTokenId))
.withBoxCreator(Address.create(secondEip3AddrStr))
.withPricePerToken(Parameters.MinFee)
.build()

val fee = Parameters.MinFee

val output = txB.outBoxBuilder
.value(Parameters.MinChangeValue)
.contract(sender.toErgoContract)
.tokens(new ErgoToken(
ErgoId.create(mockTokenId),
1000 - babelFeeBoxState.calcTokensToSellForErgAmount(fee)))
.build()

val input = txB.outBoxBuilder
.value(Parameters.MinChangeValue)
.contract(sender.toErgoContract)
.tokens(new ErgoToken(ErgoId.create(mockTokenId), 1000))
.build().convertToInputWith(mockTokenId, 0)

txB.fee(fee)
.outputs(output)
.boxesToSpend(util.Arrays.asList(input))
.sendChangeTo(sender)

BabelFeeOperations.addBabelFeeBoxes(txB,
babelFeeBoxState.buildOutbox(txB, null)
.convertToInputWith(mockTokenId, 0),
fee)

val tx = txB.build()

ctx.newProverBuilder()
.withMnemonic(mnemonic, SecretString.empty(), false)
.build()
.sign(tx)
}
}

property("fetch babel fee boxes") {
val ergoClient = createMockedErgoClient(MockData(Nil, Nil))
ergoClient.execute { ctx: BlockchainContext =>
val creator = address

val tockenId = ErgoId.create(mockTokenId)

// find no boxes
val babelBox1 = BabelFeeOperations.findBabelFeeBox(ctx,
new MockedBoxesLoader(new util.ArrayList[InputBox]()),
tockenId, Parameters.MinFee, 1)

babelBox1 shouldBe (null)

val inputBabelBox = BabelFeeBoxState.newBuilder()
.withValue(Parameters.OneErg)
.withTokenId(tockenId)
.withPricePerToken(Parameters.MinFee)
.withBoxCreator(creator)
.build().buildOutbox(ctx.newTxBuilder(), null)
.convertToInputWith(mockTokenId, 0)

val babelBox2 = BabelFeeOperations.findBabelFeeBox(ctx, new MockedBoxesLoader(util.Arrays.asList(inputBabelBox)),
tockenId, Parameters.MinFee, 1)

babelBox2 shouldBe inputBabelBox

// the amount needed (2 ERG) is more than inputBabelBox can offer, so it is discarded
val babelBox3 = BabelFeeOperations.findBabelFeeBox(ctx, new MockedBoxesLoader(util.Arrays.asList(inputBabelBox)),
tockenId, Parameters.OneErg * 2, 1)

babelBox3 shouldBe (null)
}
}
}
98 changes: 78 additions & 20 deletions common/src/main/java/org/ergoplatform/appkit/ErgoTreeTemplate.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
package org.ergoplatform.appkit;

import scala.NotImplementedError;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import scala.collection.IndexedSeq;
import scala.collection.immutable.StringOps;
import scala.collection.mutable.ArrayOps;
import scorex.util.encode.Base16;
import sigmastate.SType;
import sigmastate.Values;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import sigmastate.Values.Constant;
import sigmastate.serialization.ErgoTreeSerializer;

/**
* Represents ErgoTree template, which is an ErgoTree instance with placeholders.
Expand All @@ -17,14 +20,37 @@
*/
public class ErgoTreeTemplate {

private static int[] _noParameters = new int[0]; // immutable and shared by all instances
private final Values.ErgoTree _tree;
private final byte[] _templateBytes;
private int[] _parameterPositions = _noParameters;

private ErgoTreeTemplate(Values.ErgoTree tree) {
_tree = tree;
_templateBytes = JavaHelpers.ergoTreeTemplateBytes(_tree);
}

/**
* Specifies which ErgoTree constants will be used as template parameters.
* Which tree constants to be used as parameters depends on the contract and use case.
*
* @param positions zero-based indexes in `ErgoTree.constants` array which can be
* substituted as parameters using
* {@link ErgoTreeTemplate#applyParameters(ErgoValue[])} method.
* @see sigmastate.Values.ErgoTree
*/
public ErgoTreeTemplate withParameterPositions(int[] positions) {
if (Arrays.stream(positions).distinct().count() != positions.length)
throw new IllegalArgumentException("Duplicate positions: " +
new ArrayOps.ofInt(positions).mkString("[", ",", "]"));

for (int p : positions)
if (!_tree.constants().isDefinedAt(p))
throw new IllegalArgumentException("Invalid parameter position " + p);
_parameterPositions = positions;
return this;
}

@Override
public boolean equals(java.lang.Object o) {
if (this == o) {
Expand All @@ -48,32 +74,52 @@ public int hashCode() {
* @return template bytes at the tail of the serialized ErgoTree (i.e. exclusing header and segregated
* constants)
*/
public byte[] getBytes() { return _templateBytes; }
public byte[] getBytes() {
return _templateBytes;
}

/**
* Returns template bytes encoded as Base16 string.
*
* @see ErgoTreeTemplate#getBytes
*/
public String getEncodedBytes() { return Base16.encode(getBytes()); }
public String getEncodedBytes() {
return Base16.encode(getBytes());
}

public String getTemplateHashHex() {
return Base16.encode(scorex.crypto.hash.Sha256.hash(_templateBytes));
}

/**
* A number of placeholders in the template, which can be substituted (aka parameters).
* This is immutable property of a {@link ErgoTreeTemplate}, which counts all the constants in the
* {@link sigmastate.Values.ErgoTree} which can be replaced by new values using
* {@link ErgoTreeTemplate#applyParameters} method.
* In general, constants of ErgoTree cannot be replaced, but every placeholder can.
* A number of parameters in this template.
* In general, there may be more constants of ErgoTree then template parameters because
* not every constant make sense as a parameter.
*/
public int getParameterCount() { return _tree.constants().length(); }
public int getParameterCount() {
return _parameterPositions.length;
}

/**
* Returns types of all template parameters (placeholders in the ErgoTree).
* Returns types of all template parameters (i.e. specified constants in the ErgoTree).
*/
public List<ErgoType<?>> getParameterTypes() {
Iso<List<Values.Constant<SType>>, IndexedSeq<Values.Constant<SType>>> iso =
Iso.JListToIndexedSeq(Iso.identityIso());
List<Values.Constant<SType>> ergoValues = iso.from(_tree.constants());
return ergoValues.stream().map(v -> Iso.isoErgoTypeToSType().from(v.tpe())).collect(Collectors.toList());
List<ErgoType<?>> types = new ArrayList<>();
IndexedSeq<Constant<SType>> constants = _tree.constants();
for (int position : _parameterPositions) {
SType tpe = constants.apply(position).tpe();
types.add(Iso.isoErgoTypeToSType().from(tpe));
}
return types;
}

/**
* @param paramIndex 0-based index of parameter in [0 .. getParameterCount()) range
* @return ErgoValue of the given parameter
*/
public ErgoValue<?> getParameterValue(int paramIndex) {
Constant<SType> c = _tree.constants().apply(_parameterPositions[paramIndex]);
return Iso.isoErgoValueToSValue().from(c);
}
Comment thread
aslesarenko marked this conversation as resolved.

/**
Expand All @@ -89,11 +135,23 @@ public List<ErgoType<?>> getParameterTypes() {
* @return new ErgoTree with the same template as this but with all it's parameters
* replaced with `newValues`
*/
public Values.ErgoTree applyParameters(ErgoValue<?> newValues) {
throw new NotImplementedError();
public Values.ErgoTree applyParameters(ErgoValue<?>... newValues) {
if (newValues.length != _parameterPositions.length)
throw new IllegalArgumentException(
"Wrong number of newValues. Expected " + _parameterPositions.length +
" but was " + newValues.length);
return JavaHelpers.substituteErgoTreeConstants(_tree.bytes(), _parameterPositions, newValues);
}

public static ErgoTreeTemplate fromErgoTree(Values.ErgoTree tree) {
return new ErgoTreeTemplate(tree);
}

public static ErgoTreeTemplate fromErgoTreeBytes(byte[] treeBytes) {
Values.ErgoTree ergoTree =
ErgoTreeSerializer.DefaultSerializer().deserializeErgoTree(treeBytes);
return fromErgoTree(ergoTree);
}

// TODO public static ErgoTreeTemplate fromTemplateBytes(byte[] templateBytes)
}
4 changes: 4 additions & 0 deletions common/src/main/java/org/ergoplatform/appkit/ErgoValue.java
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,10 @@ static public ErgoValue<special.sigma.SigmaProp> of(Values.SigmaBoolean value) {
return new ErgoValue<>(JavaHelpers.SigmaDsl().SigmaProp(value), ErgoType.sigmaPropType());
}

static public ErgoValue<special.sigma.SigmaProp> of(org.ergoplatform.appkit.SigmaProp value) {
return new ErgoValue<>(JavaHelpers.SigmaDsl().SigmaProp(value.getSigmaBoolean()), ErgoType.sigmaPropType());
}

static public ErgoValue<AvlTree> of(AvlTreeData value) {
return new ErgoValue<>(JavaHelpers.SigmaDsl().avlTree(value), ErgoType.avlTreeType());
}
Expand Down
20 changes: 20 additions & 0 deletions common/src/main/java/org/ergoplatform/appkit/JavaHelpers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import org.bouncycastle.crypto.generators.PKCS5S2ParametersGenerator
import org.bouncycastle.crypto.params.KeyParameter
import org.ergoplatform.appkit.JavaHelpers.{TokenColl, TokenIdRType}
import sigmastate.eval.Colls.outerJoin
import sigmastate.eval.CostingSigmaDslBuilder.validationSettings
import special.collection.ExtensionMethods.PairCollOps

/** Type-class of isomorphisms between types.
Expand Down Expand Up @@ -327,6 +328,25 @@ object JavaHelpers {
ErgoTreeSerializer.DefaultSerializer.deserializeErgoTree(Base16.decode(base16).get)
}

/** Transforms serialized bytes of ErgoTree with segregated constants by
* replacing constants at given positions with new values. This operation
* allow to use serialized scripts as pre-defined templates.
* See [[sigmastate.SubstConstants]] for details.
*
* @param ergoTreeBytes serialized ErgoTree with ConstantSegregationFlag set to 1.
* @param positions zero based indexes in ErgoTree.constants array which
* should be replaced with new values
* @param newValues new values to be injected into the corresponding
* positions in ErgoTree.constants array
* @return a new ErgoTree such that only specified constants
* are replaced and all other remain exactly the same
*/
def substituteErgoTreeConstants(ergoTreeBytes: Array[Byte], positions: Array[Int], newValues: Array[ErgoValue[_]]): ErgoTree = {
val (newBytes, _) = ErgoTreeSerializer.DefaultSerializer.substituteConstants(
ergoTreeBytes, positions, newValues.map(Iso.isoErgoValueToSValue.to))
ErgoTreeSerializer.DefaultSerializer.deserializeErgoTree(newBytes)
}

def createP2PKAddress(pk: ProveDlog, networkPrefix: NetworkPrefix): P2PKAddress = {
implicit val ergoAddressEncoder: ErgoAddressEncoder = ErgoAddressEncoder(networkPrefix)
P2PKAddress(pk)
Expand Down
Loading