diff --git a/contracts/javascore/aggregator/build.gradle b/contracts/javascore/aggregator/build.gradle new file mode 100644 index 00000000..426026ca --- /dev/null +++ b/contracts/javascore/aggregator/build.gradle @@ -0,0 +1,41 @@ +version = '0.1.0' + +dependencies { + testImplementation 'foundation.icon:javaee-unittest:0.11.1' + testImplementation project(':test-lib') +} + +test { + useJUnitPlatform() + finalizedBy jacocoTestReport +} + +optimizedJar { + mainClassName = 'relay.aggregator.RelayAggregator' + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + from { + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } + } +} + +deployJar { + endpoints { + lisbon { + uri = 'https://lisbon.net.solidwallet.io/api/v3' + nid = 0x2 + } + local { + uri = 'http://localhost:9082/api/v3' + nid = 0x3 + } + mainnet { + uri = 'https://ctz.solidwallet.io/api/v3' + nid = 0x1 + } + } + keystore = rootProject.hasProperty('keystoreName') ? "$keystoreName" : '' + password = rootProject.hasProperty('keystorePass') ? "$keystorePass" : '' + parameters { + arg('_admin', "hxb6b5791be0b5ef67063b3c10b840fb81514db2fd") + } +} \ No newline at end of file diff --git a/contracts/javascore/aggregator/src/main/java/relay/aggregator/Packet.java b/contracts/javascore/aggregator/src/main/java/relay/aggregator/Packet.java new file mode 100644 index 00000000..0568de37 --- /dev/null +++ b/contracts/javascore/aggregator/src/main/java/relay/aggregator/Packet.java @@ -0,0 +1,176 @@ +package relay.aggregator; + +import java.math.BigInteger; + +import score.Context; +import score.ObjectReader; +import score.ObjectWriter; + +public class Packet { + + /** + * The ID of the source network (chain) from where the packet originated. + */ + private final String srcNetwork; + + /** + * The contract address on the source network (chain). + */ + private final String srcContractAddress; + + /** + * The sequence number of the packet in the source network (chain). + */ + private final BigInteger srcSn; + + /** + * The source height of the packet in the source network (chain). + */ + private final BigInteger srcHeight; + + /** + * The ID of the destination network (chain) where the packet is being sent. + */ + private final String dstNetwork; + + /** + * The contract address on the destination network (chain). + */ + private final String dstContractAddress; + + /** + * The payload data associated with this packet. + */ + private final byte[] data; + + /** + * Constructs a new {@code Packet} object with the specified {@code PacketID}, + * destination network, and data. + * All parameters must be non-null. + * + * @param id the unique identifier for the packet. + * @param dstNetwork the ID of the destination network (chain). + * @param data the payload data for this packet. + * @throws IllegalArgumentException if {@code srcNetwork}, + * {@code srcContractAddress}, {@code srcSn}, + * {@code srcHeight}, + * {@code dstNetwork}, + * {@code dstContractAddress}, or {@code data} + * is + * {@code null}. + */ + public Packet(String srcNetwork, String srcContractAddress, BigInteger srcSn, BigInteger srcHeight, + String dstNetwork, String dstContractAddress, + byte[] data) { + Boolean isIllegalArg = srcNetwork == null || srcContractAddress == null || srcSn == null + || srcHeight == null || dstNetwork == null || dstContractAddress == null || data == null; + Context.require(!isIllegalArg, + "srcNetwork, contractAddress, srcSn, srcHeight, dstNetwork, and data cannot be null"); + if (isIllegalArg) { + } + this.srcNetwork = srcNetwork; + this.srcContractAddress = srcContractAddress; + this.srcSn = srcSn; + this.srcHeight = srcHeight; + this.dstNetwork = dstNetwork; + this.dstContractAddress = dstContractAddress; + this.data = data; + } + + public String getId() { + return createId(this.srcNetwork, this.srcContractAddress, this.srcSn); + } + + public static String createId(String srcNetwork, String contractAddress, BigInteger srcSn) { + return srcNetwork + "/" + contractAddress + "/" + srcSn.toString(); + } + + /** + * Returns the source network (chain) from where the packet originated. + * + * @return the source network ID. + */ + public String getSrcNetwork() { + return srcNetwork; + } + + /** + * Returns the contract address on the source network (chain). + * + * @return the source contract address. + */ + public String getSrcContractAddress() { + return srcContractAddress; + } + + /** + * Returns the sequence number of the packet in the source network (chain). + * + * @return the sequence number. + */ + public BigInteger getSrcSn() { + return srcSn; + } + + /** + * Returns the height of the packet in the source network (chain). + * + * @return the source height. + */ + public BigInteger getSrcHeight() { + return srcHeight; + } + + /** + * Returns the destination network (chain) where the packet is being sent. + * + * @return the destination network ID. + */ + public String getDstNetwork() { + return dstNetwork; + } + + /** + * Returns the contract address on the destination network (chain). + * + * @return the destination contract address. + */ + public String getDstContractAddress() { + return dstContractAddress; + } + + /** + * Returns a copy of the data associated with this packet. + * + * @return a byte array containing the packet data. + */ + public byte[] getData() { + return data; + } + + public static void writeObject(ObjectWriter w, Packet p) { + w.beginList(7); + w.write(p.srcNetwork); + w.write(p.srcContractAddress); + w.write(p.srcSn); + w.write(p.srcHeight); + w.write(p.dstNetwork); + w.write(p.dstContractAddress); + w.writeNullable(p.data); + w.end(); + } + + public static Packet readObject(ObjectReader r) { + r.beginList(); + Packet p = new Packet( + r.readString(), + r.readString(), + r.readBigInteger(), + r.readBigInteger(), + r.readString(), + r.readString(), + r.readNullable(byte[].class)); + r.end(); + return p; + } +} diff --git a/contracts/javascore/aggregator/src/main/java/relay/aggregator/RelayAggregator.java b/contracts/javascore/aggregator/src/main/java/relay/aggregator/RelayAggregator.java new file mode 100644 index 00000000..ec475a99 --- /dev/null +++ b/contracts/javascore/aggregator/src/main/java/relay/aggregator/RelayAggregator.java @@ -0,0 +1,317 @@ +/* + * Copyright 2022 ICON Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package relay.aggregator; + +import java.math.BigInteger; + +import score.Context; +import score.Address; +import score.ArrayDB; +import score.VarDB; +import score.DictDB; +import score.BranchDB; +import score.ByteArrayObjectWriter; +import score.annotation.EventLog; +import score.annotation.External; +import score.ObjectReader; +import scorex.util.ArrayList; +import scorex.util.HashMap; + +public class RelayAggregator { + private final Integer DEFAULT_SIGNATURE_THRESHOLD = 1; + + private final VarDB signatureThreshold = Context.newVarDB("signatureThreshold", Integer.class); + + private final VarDB
admin = Context.newVarDB("admin", Address.class); + + private final ArrayDB
relayers = Context.newArrayDB("relayers", Address.class); + private final DictDB relayersLookup = Context.newDictDB("relayersLookup", Boolean.class); + + private final DictDB packets = Context.newDictDB("packets", Packet.class); + + private final BranchDB> signatures = Context.newBranchDB("signatures", + byte[].class); + + public RelayAggregator(Address _admin) { + if (admin.get() == null) { + admin.set(_admin); + signatureThreshold.set(DEFAULT_SIGNATURE_THRESHOLD); + addRelayer(_admin); + } + } + + @External + public void setAdmin(Address _admin) { + adminOnly(); + + Context.require(admin.get() != _admin, "admin already set"); + + // add new admin as relayer + addRelayer(_admin); + + // remove old admin from relayer list + removeRelayer(admin.get()); + + admin.set(_admin); + } + + @External(readonly = true) + public Address getAdmin() { + return admin.get(); + } + + @External + public void setSignatureThreshold(int threshold) { + adminOnly(); + Context.require(threshold > 0 && threshold <= relayers.size(), + "threshold value should be at least 1 and not greater than relayers size"); + signatureThreshold.set(threshold); + } + + @External(readonly = true) + public int getSignatureThreshold() { + return signatureThreshold.get(); + } + + @External(readonly = true) + public Address[] getRelayers() { + Address[] rlrs = new Address[relayers.size()]; + for (int i = 0; i < relayers.size(); i++) { + rlrs[i] = relayers.get(i); + } + return rlrs; + } + + @External + public void setRelayers(Address[] newRelayers, int threshold) { + adminOnly(); + + if (newRelayers.length > 0) { + HashMap newRelayersMap = new HashMap(); + for (Address newRelayer : newRelayers) { + newRelayersMap.put(newRelayer, true); + addRelayer(newRelayer); + } + + Address adminAdrr = admin.get(); + for (int i = 0; i < relayers.size(); i++) { + Address oldRelayer = relayers.get(i); + if (!oldRelayer.equals(adminAdrr) && !newRelayersMap.containsKey(oldRelayer)) { + removeRelayer(oldRelayer); + } + } + } + + Context.require(threshold > 0 && threshold <= relayers.size(), + "threshold value should be at least 1 and not greater than relayers size"); + + signatureThreshold.set(threshold); + } + + @External(readonly = true) + public boolean packetSubmitted( + Address relayer, + String srcNetwork, + String srcContractAddress, + BigInteger srcSn) { + String pktID = Packet.createId(srcNetwork, srcContractAddress, srcSn); + byte[] existingSign = signatures.at(pktID).get(relayer); + return existingSign != null; + } + + @External + public void submitPacket( + String srcNetwork, + String srcContractAddress, + BigInteger srcSn, + BigInteger srcHeight, + String dstNetwork, + String dstContractAddress, + byte[] data, + byte[] signature) { + + relayersOnly(); + + Packet pkt = new Packet(srcNetwork, srcContractAddress, srcSn, srcHeight, dstNetwork, dstContractAddress, data); + String pktID = pkt.getId(); + + if (packets.get(pktID) == null) { + packets.set(pktID, pkt); + if (signatureThreshold.get() > 1) { + PacketRegistered( + pkt.getSrcNetwork(), + pkt.getSrcContractAddress(), + pkt.getSrcSn(), + pkt.getSrcHeight(), + pkt.getDstNetwork(), + pkt.getDstContractAddress(), + pkt.getData()); + } + + } + + byte[] existingSign = signatures.at(pktID).get(Context.getCaller()); + Context.require(existingSign == null, "Signature already exists"); + + setSignature(pktID, Context.getCaller(), signature); + + if (signatureThresholdReached(pktID)) { + byte[][] sigs = getSignatures(srcNetwork, srcContractAddress, srcSn); + byte[] encodedSigs = serializeSignatures(sigs); + PacketAcknowledged( + pkt.getSrcNetwork(), + pkt.getSrcContractAddress(), + pkt.getSrcSn(), + pkt.getSrcHeight(), + pkt.getDstNetwork(), + pkt.getDstContractAddress(), + pkt.getData(), + encodedSigs); + removePacket(pktID); + } + } + + private byte[][] getSignatures(String srcNetwork, String srcContractAddress, BigInteger srcSn) { + String pktID = Packet.createId(srcNetwork, srcContractAddress, srcSn); + DictDB signDict = signatures.at(pktID); + ArrayList signatureList = new ArrayList(); + + for (int i = 0; i < relayers.size(); i++) { + Address relayer = relayers.get(i); + byte[] sign = signDict.get(relayer); + if (sign != null) { + signatureList.add(sign); + } + } + + byte[][] sigs = new byte[signatureList.size()][]; + for (int i = 0; i < signatureList.size(); i++) { + sigs[i] = signatureList.get(i); + } + return sigs; + } + + protected void setSignature(String pktID, Address addr, byte[] sign) { + signatures.at(pktID).set(addr, sign); + } + + protected static byte[] serializeSignatures(byte[][] sigs) { + ByteArrayObjectWriter w = Context.newByteArrayObjectWriter("RLPn"); + w.beginList(sigs.length); + + for (byte[] sig : sigs) { + w.write(sig); + } + + w.end(); + return w.toByteArray(); + } + + protected static byte[][] deserializeSignatures(byte[] encodedSigs) { + ObjectReader r = Context.newByteArrayObjectReader("RLPn", encodedSigs); + + ArrayList sigList = new ArrayList<>(); + + r.beginList(); + while (r.hasNext()) { + sigList.add(r.readByteArray()); + } + r.end(); + + byte[][] sigs = new byte[sigList.size()][]; + for (int i = 0; i < sigList.size(); i++) { + sigs[i] = sigList.get(i); + } + + return sigs; + } + + private void adminOnly() { + Context.require(Context.getCaller().equals(admin.get()), "Unauthorized: caller is not the leader relayer"); + } + + private void relayersOnly() { + Address caller = Context.getCaller(); + Boolean isRelayer = relayersLookup.get(caller); + Context.require(isRelayer != null && isRelayer, "Unauthorized: caller is not a registered relayer"); + } + + private void addRelayer(Address newRelayer) { + if (relayersLookup.get(newRelayer) == null) { + relayers.add(newRelayer); + relayersLookup.set(newRelayer, true); + } + } + + private void removeRelayer(Address oldRelayer) { + if (relayersLookup.get(oldRelayer)) { + relayersLookup.set(oldRelayer, null); + Address top = relayers.pop(); + for (int i = 0; i < relayers.size(); i++) { + if (oldRelayer.equals(relayers.get(i))) { + relayers.set(i, top); + break; + } + } + } + } + + private Boolean signatureThresholdReached(String pktID) { + int noOfSignatures = 0; + for (int i = 0; i < relayers.size(); i++) { + Address relayer = relayers.get(i); + byte[] relayerSign = signatures.at(pktID).get(relayer); + if (relayerSign != null) { + noOfSignatures++; + } + } + return noOfSignatures >= signatureThreshold.get(); + } + + private void removePacket(String pktID) { + packets.set(pktID, null); + DictDB signDict = signatures.at(pktID); + + for (int i = 0; i < relayers.size(); i++) { + Address relayer = relayers.get(i); + signDict.set(relayer, null); + } + } + + @EventLog(indexed = 2) + public void PacketRegistered( + String srcNetwork, + String srcContractAddress, + BigInteger srcSn, + BigInteger srcHeight, + String dstNetwork, + String dstContractAddress, + byte[] data) { + } + + @EventLog(indexed = 2) + public void PacketAcknowledged( + String srcNetwork, + String srcContractAddress, + BigInteger srcSn, + BigInteger srcHeight, + String dstNetwork, + String dstContractAddress, + byte[] data, + byte[] signatures) { + } +} \ No newline at end of file diff --git a/contracts/javascore/aggregator/src/test/java/relay/aggregator/RelayAggregatorTest.java b/contracts/javascore/aggregator/src/test/java/relay/aggregator/RelayAggregatorTest.java new file mode 100644 index 00000000..0da3391f --- /dev/null +++ b/contracts/javascore/aggregator/src/test/java/relay/aggregator/RelayAggregatorTest.java @@ -0,0 +1,367 @@ +package relay.aggregator; + +import java.math.BigInteger; +import java.util.Arrays; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; + +import score.Address; +import score.Context; +import score.UserRevertedException; +import scorex.util.HashSet; + +import com.iconloop.score.test.Account; +import com.iconloop.score.test.Score; +import com.iconloop.score.test.ServiceManager; +import com.iconloop.score.test.TestBase; + +import foundation.icon.icx.KeyWallet; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +class RelayAggregatorTest extends TestBase { + private final ServiceManager sm = getServiceManager(); + + private KeyWallet admin; + private Account adminAc; + + private KeyWallet relayerOne; + private Account relayerOneAc; + + private KeyWallet relayerTwo; + private Account relayerTwoAc; + + private KeyWallet relayerThree; + private Account relayerThreeAc; + + private KeyWallet relayerFour; + private Account relayerFourAc; + + private Score aggregator; + private RelayAggregator aggregatorSpy; + + @BeforeEach + void setup() throws Exception { + admin = KeyWallet.create(); + adminAc = sm.getAccount(Address.fromString(admin.getAddress().toString())); + + relayerOne = KeyWallet.create(); + relayerOneAc = sm.getAccount(Address.fromString(relayerOne.getAddress().toString())); + + relayerTwo = KeyWallet.create(); + relayerTwoAc = sm.getAccount(Address.fromString(relayerTwo.getAddress().toString())); + + relayerThree = KeyWallet.create(); + relayerThreeAc = sm.getAccount(Address.fromString(relayerThree.getAddress().toString())); + + relayerFour = KeyWallet.create(); + relayerFourAc = sm.getAccount(Address.fromString(relayerFour.getAddress().toString())); + + aggregator = sm.deploy(adminAc, RelayAggregator.class, adminAc.getAddress()); + + Address[] relayers = new Address[] { adminAc.getAddress(), relayerOneAc.getAddress(), relayerTwoAc.getAddress(), + relayerThreeAc.getAddress() }; + + aggregator.invoke(adminAc, "setRelayers", (Object) relayers, 2); + + aggregatorSpy = (RelayAggregator) spy(aggregator.getInstance()); + aggregator.setInstance(aggregatorSpy); + } + + @Test + public void testSetAdmin() { + Address oldAdmin = (Address) aggregator.call("getAdmin"); + + Account newAdminAc = sm.createAccount(); + aggregator.invoke(adminAc, "setAdmin", newAdminAc.getAddress()); + + Address newAdmin = (Address) aggregator.call("getAdmin"); + assertEquals(newAdminAc.getAddress(), newAdmin); + + Address[] relayers = (Address[]) aggregator.call("getRelayers"); + + boolean containsNewAdmin = Arrays.asList(relayers).contains(newAdmin); + boolean containsOldAdmin = Arrays.asList(relayers).contains(oldAdmin); + + assertTrue(containsNewAdmin); + assertFalse(containsOldAdmin); + } + + @Test + public void testSetAdmin_unauthorized() { + Account normalAc = sm.createAccount(); + Account newAdminAc = sm.createAccount(); + + Executable action = () -> aggregator.invoke(normalAc, "setAdmin", newAdminAc.getAddress()); + UserRevertedException e = assertThrows(UserRevertedException.class, action); + + assertEquals("Reverted(0): Unauthorized: caller is not the leader relayer", e.getMessage()); + } + + @Test + public void testSetSignatureThreshold() { + int threshold = 3; + aggregator.invoke(adminAc, "setSignatureThreshold", threshold); + + Integer result = (Integer) aggregator.call("getSignatureThreshold"); + assertEquals(threshold, result); + } + + @Test + public void testSetSignatureThreshold_unauthorised() { + int threshold = 3; + + Executable action = () -> aggregator.invoke(relayerOneAc, + "setSignatureThreshold", threshold); + UserRevertedException e = assertThrows(UserRevertedException.class, action); + + assertEquals("Reverted(0): Unauthorized: caller is not the leader relayer", + e.getMessage()); + } + + @Test + public void testSetRelayers() { + Account relayerFiveAc = sm.createAccount(); + Address[] newRelayers = new Address[] { relayerThreeAc.getAddress(), relayerFourAc.getAddress(), + relayerFiveAc.getAddress() }; + + Integer threshold = 3; + aggregator.invoke(adminAc, "setRelayers", (Object) newRelayers, threshold); + + Address[] updatedRelayers = (Address[]) aggregator.call("getRelayers"); + + Address[] expectedRelayers = new Address[] { adminAc.getAddress(), relayerThreeAc.getAddress(), + relayerFourAc.getAddress(), + relayerFiveAc.getAddress() }; + + HashSet
updatedRelayersSet = new HashSet
(); + for (Address rlr : updatedRelayers) { + updatedRelayersSet.add(rlr); + } + + HashSet
expectedRelayersSet = new HashSet
(); + for (Address rlr : expectedRelayers) { + expectedRelayersSet.add(rlr); + } + + assertEquals(expectedRelayersSet, updatedRelayersSet); + + Integer result = (Integer) aggregator.call("getSignatureThreshold"); + assertEquals(threshold, result); + } + + @Test + public void testSetRelayers_unauthorized() { + Account relayerFiveAc = sm.createAccount(); + Address[] newRelayers = new Address[] { relayerThreeAc.getAddress(), relayerFourAc.getAddress(), + relayerFiveAc.getAddress() }; + + Integer threshold = 3; + Executable action = () -> aggregator.invoke(relayerOneAc, "setRelayers", + (Object) newRelayers, threshold); + UserRevertedException e = assertThrows(UserRevertedException.class, action); + + assertEquals("Reverted(0): Unauthorized: caller is not the leader relayer", + e.getMessage()); + + } + + @Test + public void testSetRelayers_invalidThreshold() { + Account relayerFiveAc = sm.createAccount(); + Address[] newRelayers = new Address[] { relayerThreeAc.getAddress(), relayerFourAc.getAddress(), + relayerFiveAc.getAddress() }; + + Integer threshold = 5; + Executable action = () -> aggregator.invoke(adminAc, "setRelayers", + (Object) newRelayers, threshold); + UserRevertedException e = assertThrows(UserRevertedException.class, action); + + assertEquals("Reverted(0): threshold value should be at least 1 and not greater than relayers size", + e.getMessage()); + + } + + @Test + public void testSetRelayers_invalidThresholdZero() { + Account relayerFiveAc = sm.createAccount(); + Address[] newRelayers = new Address[] { relayerThreeAc.getAddress(), relayerFourAc.getAddress(), + relayerFiveAc.getAddress() }; + + Integer threshold = 0; + Executable action = () -> aggregator.invoke(adminAc, "setRelayers", + (Object) newRelayers, threshold); + UserRevertedException e = assertThrows(UserRevertedException.class, action); + + assertEquals("Reverted(0): threshold value should be at least 1 and not greater than relayers size", + e.getMessage()); + + } + + @Test + public void testPacketSubmitted_true() throws Exception { + String srcNetwork = "0x2.icon"; + String dstNetwork = "sui"; + BigInteger srcSn = BigInteger.ONE; + BigInteger srcHeight = BigInteger.ONE; + String srcContractAddress = "hxjuiod"; + String dstContractAddress = "hxjuiod"; + byte[] data = new byte[] { 0x01, 0x02 }; + + aggregator.invoke(adminAc, "setSignatureThreshold", 2); + + byte[] dataHash = Context.hash("sha-256", data); + byte[] sign = relayerOne.sign(dataHash); + + aggregator.invoke(relayerOneAc, "submitPacket", srcNetwork, + srcContractAddress, srcSn, srcHeight, dstNetwork, + dstContractAddress, data, + sign); + + boolean submitted = (boolean) aggregator.call("packetSubmitted", + relayerOneAc.getAddress(), srcNetwork, + srcContractAddress, srcSn); + assertEquals(submitted, true); + } + + @Test + public void testPacketSubmitted_false() throws Exception { + String srcNetwork = "0x2.icon"; + BigInteger srcSn = BigInteger.ONE; + String srcContractAddress = "hxjuiod"; + + boolean submitted = (boolean) aggregator.call("packetSubmitted", + relayerOneAc.getAddress(), srcNetwork, + srcContractAddress, srcSn); + assertEquals(submitted, false); + } + + @Test + public void testSubmitPacket() throws Exception { + String srcNetwork = "0x2.icon"; + String dstNetwork = "sui"; + BigInteger srcSn = BigInteger.ONE; + BigInteger srcHeight = BigInteger.ONE; + String srcContractAddress = "hxjuiod"; + String dstContractAddress = "hxjuiod"; + byte[] data = new byte[] { 0x01, 0x02 }; + + aggregator.invoke(adminAc, "setSignatureThreshold", 2); + + byte[] dataHash = Context.hash("sha-256", data); + byte[] sign = relayerOne.sign(dataHash); + + aggregator.invoke(relayerOneAc, "submitPacket", srcNetwork, + srcContractAddress, srcSn, srcHeight, dstNetwork, + dstContractAddress, data, + sign); + + String pktID = Packet.createId(srcNetwork, srcContractAddress, srcSn); + verify(aggregatorSpy).PacketRegistered(srcNetwork, srcContractAddress, srcSn, + srcHeight, dstNetwork, + dstContractAddress, data); + verify(aggregatorSpy).setSignature(pktID, relayerOneAc.getAddress(), sign); + } + + @Test + public void testSubmitPacket_thresholdReached() throws Exception { + String srcNetwork = "0x2.icon"; + String dstNetwork = "sui"; + BigInteger srcSn = BigInteger.ONE; + BigInteger srcHeight = BigInteger.ONE; + String srcContractAddress = "hxjuiod"; + String dstContractAddress = "hxjuiod"; + byte[] data = new byte[] { 0x01, 0x02 }; + + aggregator.invoke(adminAc, "setSignatureThreshold", 2); + + byte[] dataHash = Context.hash("sha-256", data); + + byte[] signAdmin = admin.sign(dataHash); + aggregator.invoke(adminAc, "submitPacket", srcNetwork, srcContractAddress, + srcSn, srcHeight, dstNetwork, + dstContractAddress, data, + signAdmin); + + byte[] signOne = relayerOne.sign(dataHash); + aggregator.invoke(relayerOneAc, "submitPacket", srcNetwork, + srcContractAddress, srcSn, srcHeight, dstNetwork, + dstContractAddress, + data, + signOne); + + byte[][] sigs = new byte[2][]; + sigs[0] = signAdmin; + sigs[1] = signOne; + + byte[] encodedSigs = RelayAggregator.serializeSignatures(sigs); + byte[][] decodedSigs = RelayAggregator.deserializeSignatures(encodedSigs); + + assertArrayEquals(signAdmin, decodedSigs[0]); + assertArrayEquals(signOne, decodedSigs[1]); + + verify(aggregatorSpy).PacketAcknowledged(srcNetwork, srcContractAddress, + srcSn, srcHeight, dstNetwork, + dstContractAddress, data, + encodedSigs); + } + + @Test + public void testSubmitPacket_unauthorized() throws Exception { + String srcNetwork = "0x2.icon"; + String dstNetwork = "sui"; + BigInteger srcSn = BigInteger.ONE; + BigInteger srcHeight = BigInteger.ONE; + String srcContractAddress = "hxjuiod"; + String dstContractAddress = "hxjuiod"; + byte[] data = new byte[] { 0x01, 0x02 }; + + byte[] dataHash = Context.hash("sha-256", data); + byte[] sign = relayerFour.sign(dataHash); + + Executable action = () -> aggregator.invoke(relayerFourAc, "submitPacket", + srcNetwork, srcContractAddress, + srcSn, + srcHeight, dstNetwork, dstContractAddress, data, sign); + + UserRevertedException e = assertThrows(UserRevertedException.class, action); + assertEquals("Reverted(0): Unauthorized: caller is not a registered relayer", + e.getMessage()); + } + + @Test + public void testSubmitPacket_duplicate() throws Exception { + String srcNetwork = "0x2.icon"; + String dstNetwork = "sui"; + BigInteger srcSn = BigInteger.ONE; + BigInteger srcHeight = BigInteger.ONE; + String srcContractAddress = "hxjuiod"; + String dstContractAddress = "hxjuiod"; + byte[] data = new byte[] { 0x01, 0x02 }; + + aggregator.invoke(adminAc, "setSignatureThreshold", 2); + + byte[] dataHash = Context.hash("sha-256", data); + byte[] sign = relayerOne.sign(dataHash); + + aggregator.invoke(relayerOneAc, "submitPacket", srcNetwork, + srcContractAddress, srcSn, srcHeight, dstNetwork, + dstContractAddress, data, sign); + + Executable action = () -> aggregator.invoke(relayerOneAc, "submitPacket", + srcNetwork, srcContractAddress, srcSn, + srcHeight, dstNetwork, dstContractAddress, + data, sign); + ; + UserRevertedException e = assertThrows(UserRevertedException.class, action); + assertEquals("Reverted(0): Signature already exists", e.getMessage()); + } +} diff --git a/contracts/javascore/keystore.json b/contracts/javascore/keystore.json new file mode 100644 index 00000000..4632fa28 --- /dev/null +++ b/contracts/javascore/keystore.json @@ -0,0 +1 @@ +{"address":"hx5f7afdb96154bbe9dbeb2dde3a58afcb1efbfaff","id":"7a8237b8-a61f-4501-9896-b96a39e9c72b","version":3,"coinType":"icx","crypto":{"cipher":"aes-128-ctr","cipherparams":{"iv":"c2e5a31a638992a36e3afe96d38ccfda"},"ciphertext":"68d61f1325e1198ca7fd0ee961997e31541ead36ff24b29fc04e6f39cb05eeac","kdf":"scrypt","kdfparams":{"dklen":32,"n":65536,"r":8,"p":1,"salt":"53663f4c4320aff5"},"mac":"388b1466f2a75736fee1f576e2307da2fcaa0cf3d00b968d3c45a98eb9c06f84"}} \ No newline at end of file diff --git a/contracts/javascore/settings.gradle b/contracts/javascore/settings.gradle index 6e6fdcdf..243072ad 100644 --- a/contracts/javascore/settings.gradle +++ b/contracts/javascore/settings.gradle @@ -3,7 +3,8 @@ include( 'test-lib', 'xcall', 'xcall-lib', - 'centralized-connection' + 'centralized-connection', + 'aggregator' ) include(':dapp-simple')